feat: 完善一级关系编辑 UI 与视图体验
This commit is contained in:
+683
-5
@@ -4,15 +4,18 @@ from datetime import datetime
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
import vtk
|
||||
from PySide6.QtCore import QEvent, Qt, QThread, QTimer, Slot
|
||||
from PySide6.QtWidgets import QToolTip
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QFileDialog,
|
||||
QMessageBox,
|
||||
QTableWidgetItem,
|
||||
QTreeWidgetItem,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from .model import StepModel
|
||||
@@ -22,6 +25,9 @@ from .workers import EditWorker, LoadWorker, ScanWorker
|
||||
|
||||
|
||||
VIEW_BACKGROUND_COLOR = (226 / 255.0, 238 / 255.0, 247 / 255.0)
|
||||
AXIS_X_COLOR = (0.82, 0.12, 0.10)
|
||||
AXIS_Y_COLOR = (0.12, 0.58, 0.20)
|
||||
AXIS_Z_COLOR = (0.10, 0.28, 0.88)
|
||||
|
||||
|
||||
def _prepare_static_mapper(mapper) -> None:
|
||||
@@ -91,15 +97,20 @@ class WindowCoreMixin:
|
||||
self.ui_task_requested.emit(callback)
|
||||
|
||||
def eventFilter(self, watched, event):
|
||||
if event.type() == QEvent.Type.ToolTip and self._should_suppress_transient_tooltip(watched):
|
||||
QToolTip.hideText()
|
||||
return True
|
||||
if watched is getattr(self, "vtk_widget", None):
|
||||
event_type = event.type()
|
||||
if event_type == QEvent.Type.MouseButtonRelease:
|
||||
QToolTip.hideText()
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
x, y = self._vtk_position_from_qt_event(event)
|
||||
self.skip_next_vtk_left_release = True
|
||||
self._handle_left_button_release(x, y)
|
||||
self.pointer_button_down = bool(event.buttons() != Qt.MouseButton.NoButton)
|
||||
elif event_type == QEvent.Type.MouseButtonPress:
|
||||
QToolTip.hideText()
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
x, y = self._vtk_position_from_qt_event(event)
|
||||
self.skip_next_vtk_left_press = True
|
||||
@@ -114,8 +125,58 @@ class WindowCoreMixin:
|
||||
self._queue_hover_from_qt_event(event)
|
||||
elif event_type == QEvent.Type.Leave:
|
||||
self._cancel_hover_tracking(render=True)
|
||||
elif event_type == QEvent.Type.Resize and hasattr(self, "_sync_vtk_render_window_size"):
|
||||
QTimer.singleShot(0, self._sync_vtk_render_window_size)
|
||||
elif watched is getattr(self, "property_table", None):
|
||||
if event.type() == QEvent.Type.Resize and hasattr(self, "_resize_property_table_columns"):
|
||||
QTimer.singleShot(0, self._resize_property_table_columns)
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def _should_suppress_transient_tooltip(self, watched) -> bool:
|
||||
if not isinstance(watched, QWidget):
|
||||
return False
|
||||
suppressed_roots = (
|
||||
getattr(self, "vtk_widget", None),
|
||||
getattr(self, "object_edit_box", None),
|
||||
getattr(self, "property_card_scroll", None),
|
||||
getattr(self, "property_card_container", None),
|
||||
)
|
||||
for root in suppressed_roots:
|
||||
if root is None:
|
||||
continue
|
||||
widget = watched
|
||||
while widget is not None:
|
||||
if widget is root:
|
||||
return True
|
||||
widget = widget.parentWidget()
|
||||
return False
|
||||
|
||||
def _sync_vtk_render_window_size(self) -> None:
|
||||
widget = getattr(self, "vtk_widget", None)
|
||||
render_window = getattr(self, "render_window", None)
|
||||
if widget is None or render_window is None:
|
||||
return
|
||||
try:
|
||||
scale = widget._getPixelRatio() if hasattr(widget, "_getPixelRatio") else widget.devicePixelRatioF()
|
||||
except Exception:
|
||||
scale = 1.0
|
||||
width = max(int(round(float(widget.width()) * float(scale))), 1)
|
||||
height = max(int(round(float(widget.height()) * float(scale))), 1)
|
||||
try:
|
||||
current_width, current_height = render_window.GetSize()
|
||||
except Exception:
|
||||
current_width, current_height = width, height
|
||||
if int(current_width) == width and int(current_height) == height:
|
||||
return
|
||||
try:
|
||||
render_window.SetSize(width, height)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.interactor.SetSize(width, height)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _vtk_position_from_qt_event(self, event) -> tuple[int, int]:
|
||||
position = event.position() if hasattr(event, "position") else event.pos()
|
||||
scale = self.vtk_widget._getPixelRatio() if hasattr(self.vtk_widget, "_getPixelRatio") else 1.0
|
||||
@@ -123,15 +184,53 @@ class WindowCoreMixin:
|
||||
y = int(round((float(self.vtk_widget.height()) - float(position.y()) - 1.0) * scale))
|
||||
return x, y
|
||||
|
||||
def _configure_render_antialiasing(self) -> None:
|
||||
fxaa_enabled = False
|
||||
if bool(getattr(self, "prefer_fxaa_antialiasing", False)) and hasattr(self.renderer, "UseFXAAOn"):
|
||||
try:
|
||||
self.renderer.UseFXAAOn()
|
||||
fxaa_enabled = True
|
||||
except Exception:
|
||||
fxaa_enabled = False
|
||||
if not fxaa_enabled and hasattr(self.renderer, "UseFXAAOff"):
|
||||
try:
|
||||
self.renderer.UseFXAAOff()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
fallback_samples = max(int(getattr(self, "fallback_multi_samples", 2) or 0), 0)
|
||||
if fxaa_enabled:
|
||||
self.interactive_multi_samples = 0
|
||||
self.still_multi_samples = 0
|
||||
else:
|
||||
self.interactive_multi_samples = fallback_samples
|
||||
self.still_multi_samples = fallback_samples
|
||||
self.fxaa_antialiasing_enabled = fxaa_enabled
|
||||
self._set_render_window_multisamples(int(getattr(self, "still_multi_samples", fallback_samples)))
|
||||
|
||||
def _build_vtk(self) -> None:
|
||||
self.renderer = vtk.vtkRenderer()
|
||||
self.renderer.SetBackground(*VIEW_BACKGROUND_COLOR)
|
||||
self.render_window = self.vtk_widget.GetRenderWindow()
|
||||
if hasattr(self.render_window, "SetMultiSamples"):
|
||||
self.render_window.SetMultiSamples(0)
|
||||
self._configure_render_antialiasing()
|
||||
if hasattr(self.render_window, "SetNumberOfLayers"):
|
||||
self.render_window.SetNumberOfLayers(3)
|
||||
if hasattr(self.renderer, "SetLayer"):
|
||||
self.renderer.SetLayer(0)
|
||||
self.render_window.AddRenderer(self.renderer)
|
||||
if hasattr(self.renderer, "UseFXAAOff"):
|
||||
self.renderer.UseFXAAOff()
|
||||
self._build_scene_axes_renderer()
|
||||
self._build_fps_overlay()
|
||||
self._fps_frame_count = 0
|
||||
self._fps_window_started_at = time.perf_counter()
|
||||
self._fps_last_text_update_at = self._fps_window_started_at
|
||||
self._fps_last_value = 0.0
|
||||
self._fps_idle_timeout_ms = 900
|
||||
self._fps_idle_zero_displayed = False
|
||||
self._fps_idle_rendering_zero = False
|
||||
self.fps_idle_timer = QTimer(self)
|
||||
self.fps_idle_timer.setSingleShot(True)
|
||||
self.fps_idle_timer.timeout.connect(self._show_idle_fps_zero)
|
||||
self.render_window.AddObserver("EndEvent", self._on_render_window_end_event)
|
||||
|
||||
self.interactor = self.render_window.GetInteractor()
|
||||
self.interactor.SetInteractorStyle(vtk.vtkInteractorStyleTrackballCamera())
|
||||
@@ -166,6 +265,523 @@ class WindowCoreMixin:
|
||||
self.renderer.AddLight(fill_light)
|
||||
|
||||
self.interactor.Initialize()
|
||||
self._build_orientation_marker()
|
||||
|
||||
def _build_scene_axes_renderer(self) -> None:
|
||||
try:
|
||||
axes_renderer = vtk.vtkRenderer()
|
||||
axes_renderer.SetLayer(1)
|
||||
axes_renderer.InteractiveOff()
|
||||
axes_renderer.EraseOff()
|
||||
axes_renderer.PreserveColorBufferOn()
|
||||
axes_renderer.PreserveDepthBufferOff()
|
||||
axes_renderer.SetActiveCamera(self.renderer.GetActiveCamera())
|
||||
axes_light = vtk.vtkLight()
|
||||
axes_light.SetLightTypeToHeadlight()
|
||||
axes_light.SetIntensity(0.8)
|
||||
axes_renderer.AddLight(axes_light)
|
||||
self.render_window.AddRenderer(axes_renderer)
|
||||
except Exception:
|
||||
self.scene_axes_renderer = None
|
||||
return
|
||||
self.scene_axes_renderer = axes_renderer
|
||||
|
||||
def _build_fps_overlay(self) -> None:
|
||||
self.fps_actor = vtk.vtkTextActor()
|
||||
self.fps_actor.SetInput("FPS --")
|
||||
text_property = self.fps_actor.GetTextProperty()
|
||||
text_property.SetFontFamilyToArial()
|
||||
text_property.SetFontSize(17)
|
||||
text_property.BoldOn()
|
||||
text_property.SetColor(0.05, 0.08, 0.12)
|
||||
text_property.SetOpacity(0.92)
|
||||
text_property.SetJustificationToRight()
|
||||
text_property.SetVerticalJustificationToTop()
|
||||
if hasattr(text_property, "SetBackgroundColor"):
|
||||
text_property.SetBackgroundColor(1.0, 1.0, 1.0)
|
||||
if hasattr(text_property, "SetBackgroundOpacity"):
|
||||
text_property.SetBackgroundOpacity(0.58)
|
||||
position = self.fps_actor.GetPositionCoordinate()
|
||||
position.SetCoordinateSystemToNormalizedViewport()
|
||||
position.SetValue(0.985, 0.982)
|
||||
self.renderer.AddActor2D(self.fps_actor)
|
||||
|
||||
def _ensure_fps_overlay(self) -> None:
|
||||
actor = getattr(self, "fps_actor", None)
|
||||
if actor is None:
|
||||
self._build_fps_overlay()
|
||||
return
|
||||
try:
|
||||
self.renderer.AddActor2D(actor)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_render_window_end_event(self, _obj, _event) -> None:
|
||||
now = time.perf_counter()
|
||||
if bool(getattr(self, "_fps_idle_rendering_zero", False)):
|
||||
self._fps_idle_rendering_zero = False
|
||||
self._fps_frame_count = 0
|
||||
self._fps_window_started_at = now
|
||||
self._fps_last_text_update_at = now
|
||||
return
|
||||
self._fps_idle_zero_displayed = False
|
||||
self._restart_fps_idle_timer()
|
||||
self._fps_frame_count = int(getattr(self, "_fps_frame_count", 0) or 0) + 1
|
||||
window_started_at = float(getattr(self, "_fps_window_started_at", now) or now)
|
||||
elapsed = now - window_started_at
|
||||
if elapsed < 0.35:
|
||||
return
|
||||
fps = self._fps_frame_count / max(elapsed, 1e-9)
|
||||
previous = float(getattr(self, "_fps_last_value", 0.0) or 0.0)
|
||||
if previous > 0.0:
|
||||
fps = previous * 0.35 + fps * 0.65
|
||||
self._fps_last_value = fps
|
||||
self._fps_frame_count = 0
|
||||
self._fps_window_started_at = now
|
||||
actor = getattr(self, "fps_actor", None)
|
||||
if actor is not None:
|
||||
actor.SetInput(f"FPS {fps:4.1f}")
|
||||
|
||||
def _restart_fps_idle_timer(self) -> None:
|
||||
timer = getattr(self, "fps_idle_timer", None)
|
||||
if timer is None:
|
||||
return
|
||||
try:
|
||||
timer.start(int(getattr(self, "_fps_idle_timeout_ms", 900) or 900))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _show_idle_fps_zero(self) -> None:
|
||||
if bool(getattr(self, "_fps_idle_zero_displayed", False)):
|
||||
return
|
||||
actor = getattr(self, "fps_actor", None)
|
||||
render_window = getattr(self, "render_window", None)
|
||||
if actor is None or render_window is None:
|
||||
return
|
||||
self._fps_idle_zero_displayed = True
|
||||
self._fps_last_value = 0.0
|
||||
self._fps_frame_count = 0
|
||||
self._fps_window_started_at = time.perf_counter()
|
||||
try:
|
||||
actor.SetInput("FPS 0.0")
|
||||
except Exception:
|
||||
return
|
||||
self._fps_idle_rendering_zero = True
|
||||
try:
|
||||
render_window.Render()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._fps_idle_rendering_zero = False
|
||||
|
||||
def _axis_caption_property(self, caption_actor):
|
||||
try:
|
||||
return caption_actor.GetCaptionTextProperty()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
text_actor = caption_actor.GetTextActor()
|
||||
return text_actor.GetTextProperty()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _style_axis_caption(self, caption_actor, color: tuple[float, float, float], scale: float) -> None:
|
||||
text_property = self._axis_caption_property(caption_actor)
|
||||
if text_property is not None:
|
||||
try:
|
||||
text_property.SetFontFamilyToArial()
|
||||
text_property.BoldOn()
|
||||
text_property.SetColor(*color)
|
||||
text_property.SetOpacity(0.96)
|
||||
except Exception:
|
||||
pass
|
||||
for method_name in ("SetWidth", "SetHeight"):
|
||||
try:
|
||||
getattr(caption_actor, method_name)(scale)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _configure_axes_actor(self, axes_actor, length: float, caption_scale: float) -> None:
|
||||
length = max(float(length), 1e-6)
|
||||
axes_actor.SetTotalLength(length, length, length)
|
||||
axes_actor.SetXAxisLabelText("X")
|
||||
axes_actor.SetYAxisLabelText("Y")
|
||||
axes_actor.SetZAxisLabelText("Z")
|
||||
axes_actor.AxisLabelsOn()
|
||||
for method_name, args in (
|
||||
("SetShaftTypeToCylinder", ()),
|
||||
("SetCylinderRadius", (0.012,)),
|
||||
("SetConeRadius", (0.045,)),
|
||||
("SetSphereRadius", (0.035,)),
|
||||
("SetNormalizedShaftLength", (0.78, 0.78, 0.78)),
|
||||
("SetNormalizedTipLength", (0.22, 0.22, 0.22)),
|
||||
):
|
||||
try:
|
||||
getattr(axes_actor, method_name)(*args)
|
||||
except Exception:
|
||||
pass
|
||||
for shaft_name, tip_name, caption_name, color in (
|
||||
("GetXAxisShaftProperty", "GetXAxisTipProperty", "GetXAxisCaptionActor2D", AXIS_X_COLOR),
|
||||
("GetYAxisShaftProperty", "GetYAxisTipProperty", "GetYAxisCaptionActor2D", AXIS_Y_COLOR),
|
||||
("GetZAxisShaftProperty", "GetZAxisTipProperty", "GetZAxisCaptionActor2D", AXIS_Z_COLOR),
|
||||
):
|
||||
for prop_name in (shaft_name, tip_name):
|
||||
try:
|
||||
prop = getattr(axes_actor, prop_name)()
|
||||
prop.SetColor(*color)
|
||||
prop.SetAmbient(0.25)
|
||||
prop.SetDiffuse(0.78)
|
||||
prop.SetSpecular(0.2)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._style_axis_caption(getattr(axes_actor, caption_name)(), color, caption_scale)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
axes_actor.PickableOff()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _blend_axis_color(
|
||||
color: tuple[float, float, float],
|
||||
target: tuple[float, float, float],
|
||||
amount: float,
|
||||
) -> tuple[float, float, float]:
|
||||
amount = min(max(float(amount), 0.0), 1.0)
|
||||
return tuple(color[index] * (1.0 - amount) + target[index] * amount for index in range(3))
|
||||
|
||||
def _apply_axis_material(
|
||||
self,
|
||||
actor,
|
||||
color: tuple[float, float, float],
|
||||
*,
|
||||
opacity: float = 1.0,
|
||||
ambient: float = 0.48,
|
||||
diffuse: float = 0.7,
|
||||
specular: float = 0.22,
|
||||
specular_power: float = 28.0,
|
||||
) -> None:
|
||||
try:
|
||||
prop = actor.GetProperty()
|
||||
prop.SetColor(*color)
|
||||
prop.SetOpacity(float(opacity))
|
||||
prop.SetAmbient(float(ambient))
|
||||
prop.SetDiffuse(float(diffuse))
|
||||
prop.SetSpecular(float(specular))
|
||||
prop.SetSpecularPower(float(specular_power))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
actor.PickableOff()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _make_axis_tube_actor(
|
||||
self,
|
||||
start: tuple[float, float, float],
|
||||
end: tuple[float, float, float],
|
||||
radius: float,
|
||||
color: tuple[float, float, float],
|
||||
*,
|
||||
opacity: float = 1.0,
|
||||
):
|
||||
line = vtk.vtkLineSource()
|
||||
line.SetPoint1(*start)
|
||||
line.SetPoint2(*end)
|
||||
tube = vtk.vtkTubeFilter()
|
||||
tube.SetInputConnection(line.GetOutputPort())
|
||||
tube.SetRadius(max(float(radius), 1e-9))
|
||||
tube.SetNumberOfSides(28)
|
||||
tube.CappingOn()
|
||||
mapper = vtk.vtkPolyDataMapper()
|
||||
mapper.SetInputConnection(tube.GetOutputPort())
|
||||
actor = vtk.vtkActor()
|
||||
actor.SetMapper(mapper)
|
||||
self._apply_axis_material(actor, color, opacity=opacity)
|
||||
return actor
|
||||
|
||||
def _make_axis_cone_actor(
|
||||
self,
|
||||
center: tuple[float, float, float],
|
||||
direction: tuple[float, float, float],
|
||||
height: float,
|
||||
radius: float,
|
||||
color: tuple[float, float, float],
|
||||
):
|
||||
cone = vtk.vtkConeSource()
|
||||
cone.SetCenter(*center)
|
||||
cone.SetDirection(*direction)
|
||||
cone.SetHeight(max(float(height), 1e-9))
|
||||
cone.SetRadius(max(float(radius), 1e-9))
|
||||
cone.SetResolution(36)
|
||||
mapper = vtk.vtkPolyDataMapper()
|
||||
mapper.SetInputConnection(cone.GetOutputPort())
|
||||
actor = vtk.vtkActor()
|
||||
actor.SetMapper(mapper)
|
||||
self._apply_axis_material(actor, color, ambient=0.3, diffuse=0.72, specular=0.36, specular_power=42.0)
|
||||
return actor
|
||||
|
||||
def _make_axis_sphere_actor(
|
||||
self,
|
||||
center: tuple[float, float, float],
|
||||
radius: float,
|
||||
color: tuple[float, float, float],
|
||||
):
|
||||
sphere = vtk.vtkSphereSource()
|
||||
sphere.SetCenter(*center)
|
||||
sphere.SetRadius(max(float(radius), 1e-9))
|
||||
sphere.SetThetaResolution(36)
|
||||
sphere.SetPhiResolution(24)
|
||||
mapper = vtk.vtkPolyDataMapper()
|
||||
mapper.SetInputConnection(sphere.GetOutputPort())
|
||||
actor = vtk.vtkActor()
|
||||
actor.SetMapper(mapper)
|
||||
self._apply_axis_material(actor, color, ambient=0.42, diffuse=0.6, specular=0.4, specular_power=46.0)
|
||||
return actor
|
||||
|
||||
def _make_axis_label_actor(
|
||||
self,
|
||||
text: str,
|
||||
position: tuple[float, float, float],
|
||||
scale: float,
|
||||
color: tuple[float, float, float],
|
||||
):
|
||||
vector_text = vtk.vtkVectorText()
|
||||
vector_text.SetText(text)
|
||||
mapper = vtk.vtkPolyDataMapper()
|
||||
mapper.SetInputConnection(vector_text.GetOutputPort())
|
||||
actor = vtk.vtkFollower()
|
||||
actor.SetMapper(mapper)
|
||||
actor.SetScale(scale, scale, scale)
|
||||
actor.SetPosition(*position)
|
||||
try:
|
||||
actor.SetCamera(self.renderer.GetActiveCamera())
|
||||
except Exception:
|
||||
pass
|
||||
self._apply_axis_material(actor, color, ambient=0.78, diffuse=0.38, specular=0.0)
|
||||
return actor
|
||||
|
||||
def _set_annotated_cube_property(
|
||||
self,
|
||||
cube_actor,
|
||||
method_name: str,
|
||||
color: tuple[float, float, float],
|
||||
opacity: float,
|
||||
) -> None:
|
||||
try:
|
||||
prop = getattr(cube_actor, method_name)()
|
||||
prop.SetColor(*color)
|
||||
prop.SetOpacity(float(opacity))
|
||||
prop.SetAmbient(0.35)
|
||||
prop.SetDiffuse(0.65)
|
||||
prop.SetSpecular(0.18)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _build_orientation_marker_prop(self):
|
||||
axes_actor = vtk.vtkAxesActor()
|
||||
self._configure_axes_actor(axes_actor, 1.72, caption_scale=0.14)
|
||||
|
||||
cube_actor = vtk.vtkAnnotatedCubeActor()
|
||||
cube_actor.SetXPlusFaceText("+X")
|
||||
cube_actor.SetXMinusFaceText("-X")
|
||||
cube_actor.SetYPlusFaceText("+Y")
|
||||
cube_actor.SetYMinusFaceText("-Y")
|
||||
cube_actor.SetZPlusFaceText("+Z")
|
||||
cube_actor.SetZMinusFaceText("-Z")
|
||||
cube_actor.SetFaceTextScale(0.54)
|
||||
cube_actor.SetTextEdgesVisibility(1)
|
||||
cube_actor.SetCubeVisibility(1)
|
||||
self._set_annotated_cube_property(cube_actor, "GetCubeProperty", (0.88, 0.92, 0.96), 0.72)
|
||||
self._set_annotated_cube_property(cube_actor, "GetXPlusFaceProperty", AXIS_X_COLOR, 0.92)
|
||||
self._set_annotated_cube_property(
|
||||
cube_actor,
|
||||
"GetXMinusFaceProperty",
|
||||
self._blend_axis_color(AXIS_X_COLOR, (1.0, 1.0, 1.0), 0.52),
|
||||
0.78,
|
||||
)
|
||||
self._set_annotated_cube_property(cube_actor, "GetYPlusFaceProperty", AXIS_Y_COLOR, 0.92)
|
||||
self._set_annotated_cube_property(
|
||||
cube_actor,
|
||||
"GetYMinusFaceProperty",
|
||||
self._blend_axis_color(AXIS_Y_COLOR, (1.0, 1.0, 1.0), 0.52),
|
||||
0.78,
|
||||
)
|
||||
self._set_annotated_cube_property(cube_actor, "GetZPlusFaceProperty", AXIS_Z_COLOR, 0.92)
|
||||
self._set_annotated_cube_property(
|
||||
cube_actor,
|
||||
"GetZMinusFaceProperty",
|
||||
self._blend_axis_color(AXIS_Z_COLOR, (1.0, 1.0, 1.0), 0.52),
|
||||
0.78,
|
||||
)
|
||||
try:
|
||||
edge_property = cube_actor.GetTextEdgesProperty()
|
||||
edge_property.SetColor(0.09, 0.12, 0.17)
|
||||
edge_property.SetOpacity(0.75)
|
||||
edge_property.SetLineWidth(1.5)
|
||||
except Exception:
|
||||
pass
|
||||
for actor in (axes_actor, cube_actor):
|
||||
try:
|
||||
actor.PickableOff()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
prop = vtk.vtkPropAssembly()
|
||||
prop.AddPart(axes_actor)
|
||||
prop.AddPart(cube_actor)
|
||||
return prop, axes_actor, cube_actor
|
||||
|
||||
def _build_orientation_marker(self) -> None:
|
||||
try:
|
||||
prop, axes_actor, cube_actor = self._build_orientation_marker_prop()
|
||||
marker_widget = vtk.vtkOrientationMarkerWidget()
|
||||
marker_widget.SetOrientationMarker(prop)
|
||||
marker_widget.SetInteractor(self.interactor)
|
||||
marker_widget.SetViewport(0.015, 0.015, 0.235, 0.235)
|
||||
if hasattr(marker_widget, "SetOutlineColor"):
|
||||
marker_widget.SetOutlineColor(0.72, 0.78, 0.86)
|
||||
marker_widget.SetEnabled(1)
|
||||
marker_widget.InteractiveOff()
|
||||
except Exception:
|
||||
self.orientation_marker_widget = None
|
||||
self.orientation_marker_prop = None
|
||||
self.orientation_marker_axes_actor = None
|
||||
self.orientation_marker_cube_actor = None
|
||||
return
|
||||
self.orientation_marker_widget = marker_widget
|
||||
self.orientation_marker_prop = prop
|
||||
self.orientation_marker_axes_actor = axes_actor
|
||||
self.orientation_marker_cube_actor = cube_actor
|
||||
|
||||
def _valid_bounds_tuple(self, bounds) -> tuple[float, float, float, float, float, float] | None:
|
||||
try:
|
||||
values = tuple(float(value) for value in bounds)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if len(values) != 6:
|
||||
return None
|
||||
if not all(math.isfinite(value) for value in values):
|
||||
return None
|
||||
x0, x1, y0, y1, z0, z1 = values
|
||||
if x1 < x0 or y1 < y0 or z1 < z0:
|
||||
return None
|
||||
return values
|
||||
|
||||
def _current_model_bounds(self) -> tuple[float, float, float, float, float, float] | None:
|
||||
polydata = getattr(self, "model_polydata", None)
|
||||
if polydata is None:
|
||||
return None
|
||||
try:
|
||||
return self._valid_bounds_tuple(polydata.GetBounds())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _step_coordinate_axis_length(self) -> float:
|
||||
bounds = self._current_model_bounds()
|
||||
if bounds is None:
|
||||
return 1.0
|
||||
x0, x1, y0, y1, z0, z1 = bounds
|
||||
dx = max(x1 - x0, 0.0)
|
||||
dy = max(y1 - y0, 0.0)
|
||||
dz = max(z1 - z0, 0.0)
|
||||
model_size = max(math.sqrt(dx * dx + dy * dy + dz * dz), dx, dy, dz, 1e-6)
|
||||
return max(model_size * 0.18, 1e-4)
|
||||
|
||||
def _scene_coordinate_axes_anchor(self, length: float) -> tuple[float, float, float]:
|
||||
# STEP display geometry is already in the file/global coordinate frame.
|
||||
# Keep the scene axes at the real file origin; visibility is handled by
|
||||
# the foreground renderer, not by moving the origin marker.
|
||||
return (0.0, 0.0, 0.0)
|
||||
|
||||
def _build_scene_coordinate_axes(self, length: float, anchor: tuple[float, float, float]):
|
||||
length = max(float(length), 1e-6)
|
||||
assembly = vtk.vtkAssembly()
|
||||
assembly.SetPosition(*anchor)
|
||||
shaft_radius = length * 0.012
|
||||
tip_length = length * 0.18
|
||||
tip_radius = length * 0.045
|
||||
negative_length = length * 0.28
|
||||
label_scale = length * 0.075
|
||||
origin_radius = length * 0.032
|
||||
|
||||
assembly.AddPart(self._make_axis_sphere_actor((0.0, 0.0, 0.0), origin_radius, (0.96, 0.98, 1.0)))
|
||||
for label, direction, color in (
|
||||
("X", (1.0, 0.0, 0.0), AXIS_X_COLOR),
|
||||
("Y", (0.0, 1.0, 0.0), AXIS_Y_COLOR),
|
||||
("Z", (0.0, 0.0, 1.0), AXIS_Z_COLOR),
|
||||
):
|
||||
shaft_end = tuple(direction[index] * (length - tip_length * 0.78) for index in range(3))
|
||||
cone_center = tuple(direction[index] * (length - tip_length * 0.5) for index in range(3))
|
||||
label_position = tuple(direction[index] * (length * 1.08) for index in range(3))
|
||||
negative_start = tuple(-direction[index] * negative_length for index in range(3))
|
||||
pale_color = self._blend_axis_color(color, (1.0, 1.0, 1.0), 0.48)
|
||||
|
||||
assembly.AddPart(
|
||||
self._make_axis_tube_actor(
|
||||
negative_start,
|
||||
(0.0, 0.0, 0.0),
|
||||
shaft_radius * 0.58,
|
||||
pale_color,
|
||||
opacity=0.62,
|
||||
)
|
||||
)
|
||||
assembly.AddPart(self._make_axis_tube_actor((0.0, 0.0, 0.0), shaft_end, shaft_radius, color))
|
||||
assembly.AddPart(self._make_axis_cone_actor(cone_center, direction, tip_length, tip_radius, color))
|
||||
assembly.AddPart(self._make_axis_label_actor(label, label_position, label_scale, color))
|
||||
try:
|
||||
assembly.PickableOff()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
assembly.UseBoundsOff()
|
||||
except Exception:
|
||||
pass
|
||||
return assembly
|
||||
|
||||
def _ensure_step_coordinate_axes(self) -> None:
|
||||
previous_actor = getattr(self, "step_coordinate_axes_actor", None)
|
||||
if previous_actor is not None:
|
||||
for renderer in (getattr(self, "scene_axes_renderer", None), self.renderer):
|
||||
if renderer is None:
|
||||
continue
|
||||
try:
|
||||
renderer.RemoveViewProp(previous_actor)
|
||||
except Exception:
|
||||
pass
|
||||
axes_renderer = getattr(self, "scene_axes_renderer", None) or self.renderer
|
||||
try:
|
||||
axes_renderer.SetActiveCamera(self.renderer.GetActiveCamera())
|
||||
except Exception:
|
||||
pass
|
||||
length = self._step_coordinate_axis_length()
|
||||
anchor = self._scene_coordinate_axes_anchor(length)
|
||||
try:
|
||||
axes_actor = self._build_scene_coordinate_axes(length, anchor)
|
||||
self.step_coordinate_axes_actor = axes_actor
|
||||
except Exception:
|
||||
axes_actor = vtk.vtkAxesActor()
|
||||
axes_actor.SetPosition(*anchor)
|
||||
self._configure_axes_actor(
|
||||
axes_actor,
|
||||
length,
|
||||
caption_scale=0.055,
|
||||
)
|
||||
self.step_coordinate_axes_actor = axes_actor
|
||||
for method_name in ("PickableOff", "UseBoundsOff"):
|
||||
try:
|
||||
getattr(axes_actor, method_name)()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
axes_renderer.AddViewProp(axes_actor)
|
||||
except Exception:
|
||||
try:
|
||||
axes_renderer.AddActor(axes_actor)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_camera_interaction_start(self, _obj, _event) -> None:
|
||||
if not self._is_ui_thread():
|
||||
@@ -184,15 +800,73 @@ class WindowCoreMixin:
|
||||
return
|
||||
self.camera_interaction_active = True
|
||||
self._cancel_hover_tracking(render=False)
|
||||
self._set_camera_interaction_visual_mode(True)
|
||||
|
||||
def _end_camera_interaction(self) -> None:
|
||||
if not getattr(self, "camera_interaction_active", False):
|
||||
return
|
||||
self.camera_interaction_active = False
|
||||
self._set_camera_interaction_visual_mode(False)
|
||||
self.last_camera_interaction_ended_at = datetime.now()
|
||||
self.pending_hover_position = None
|
||||
self.last_hover_pick_position = None
|
||||
|
||||
def _set_render_window_multisamples(self, samples: int) -> bool:
|
||||
render_window = getattr(self, "render_window", None)
|
||||
if render_window is None or not hasattr(render_window, "SetMultiSamples"):
|
||||
return False
|
||||
target = max(int(samples), 0)
|
||||
try:
|
||||
if hasattr(render_window, "GetMultiSamples") and int(render_window.GetMultiSamples()) == target:
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
render_window.SetMultiSamples(target)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _set_camera_interaction_visual_mode(self, active: bool) -> None:
|
||||
edge_actor = getattr(self, "edge_actor", None)
|
||||
changed = False
|
||||
if active:
|
||||
if bool(getattr(self, "hide_edges_during_camera_interaction", True)) and edge_actor is not None:
|
||||
try:
|
||||
self.edge_visibility_before_camera_interaction = int(edge_actor.GetVisibility())
|
||||
if self.edge_visibility_before_camera_interaction:
|
||||
edge_actor.VisibilityOff()
|
||||
changed = True
|
||||
except Exception:
|
||||
self.edge_visibility_before_camera_interaction = None
|
||||
changed = self._set_render_window_multisamples(
|
||||
int(getattr(self, "interactive_multi_samples", 0))
|
||||
) or changed
|
||||
self.camera_interaction_visual_changed = changed
|
||||
return
|
||||
|
||||
if edge_actor is not None and self.edge_visibility_before_camera_interaction is not None:
|
||||
try:
|
||||
previous_visibility = int(self.edge_visibility_before_camera_interaction)
|
||||
if int(edge_actor.GetVisibility()) != previous_visibility:
|
||||
edge_actor.SetVisibility(previous_visibility)
|
||||
changed = True
|
||||
except Exception:
|
||||
pass
|
||||
self.edge_visibility_before_camera_interaction = None
|
||||
changed = self._set_render_window_multisamples(int(getattr(self, "still_multi_samples", 4))) or changed
|
||||
changed = bool(getattr(self, "camera_interaction_visual_changed", False)) or changed
|
||||
self.camera_interaction_visual_changed = False
|
||||
if changed:
|
||||
try:
|
||||
self.renderer.ResetCameraClippingRange()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.render_window.Render()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _hover_suppressed_after_camera(self) -> bool:
|
||||
ended_at = getattr(self, "last_camera_interaction_ended_at", None)
|
||||
if ended_at is None:
|
||||
@@ -268,7 +942,7 @@ class WindowCoreMixin:
|
||||
if not background:
|
||||
self._load_step_sync(
|
||||
new_path,
|
||||
deflection=0.8,
|
||||
deflection=float(getattr(self, "preview_load_deflection", 0.35) or 0.35),
|
||||
show_internal_edges=self._show_same_domain_internal_edges(),
|
||||
status_prefix="Loading",
|
||||
)
|
||||
@@ -1027,6 +1701,7 @@ class WindowCoreMixin:
|
||||
self._clear_overlay_polydata_cache()
|
||||
self.renderer.RemoveAllViewProps()
|
||||
self.renderer.SetBackground(*VIEW_BACKGROUND_COLOR)
|
||||
self._ensure_fps_overlay()
|
||||
self.scene_isolated = False
|
||||
self.highlight_signature = None
|
||||
|
||||
@@ -1067,6 +1742,7 @@ class WindowCoreMixin:
|
||||
self.edge_actor.SetMapper(edge_mapper)
|
||||
self.edge_actor.GetProperty().SetColor(0.08, 0.09, 0.1)
|
||||
self.edge_actor.GetProperty().SetLineWidth(1.0)
|
||||
self.edge_actor.GetProperty().LightingOff()
|
||||
self.renderer.AddActor(self.edge_actor)
|
||||
|
||||
self.highlight_actor = None
|
||||
@@ -1082,6 +1758,8 @@ class WindowCoreMixin:
|
||||
self.diff_actors = []
|
||||
if reset_camera:
|
||||
self.renderer.ResetCamera()
|
||||
self._ensure_step_coordinate_axes()
|
||||
self.renderer.ResetCameraClippingRange()
|
||||
self.render_window.Render()
|
||||
|
||||
def _on_mode_changed(self, mode: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user