feat: 完善一级关系编辑 UI 与视图体验

This commit is contained in:
2026-08-06 18:15:14 +08:00
parent 57d75541c3
commit eef9efcc1e
29 changed files with 3907 additions and 178 deletions
+683 -5
View File
@@ -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: