feat: 完善 Face 一级关系编辑和稳定性
This commit is contained in:
+168
-20
@@ -46,6 +46,40 @@ class WindowCoreMixin:
|
||||
def _run_ui_task(self, callback) -> None:
|
||||
callback()
|
||||
|
||||
def showEvent(self, event) -> None:
|
||||
super().showEvent(event)
|
||||
if getattr(self, "first_show_handled", False):
|
||||
return
|
||||
self.first_show_handled = True
|
||||
QTimer.singleShot(0, self._after_first_show)
|
||||
|
||||
@Slot()
|
||||
def _after_first_show(self) -> None:
|
||||
self._ensure_vtk_interactor_started()
|
||||
if getattr(self, "auto_load_on_show", False):
|
||||
self.auto_load_on_show = False
|
||||
self.load_step(self.step_path, background=True)
|
||||
|
||||
def _ensure_vtk_interactor_started(self) -> None:
|
||||
if getattr(self, "vtk_interactor_started", False):
|
||||
return
|
||||
self.vtk_interactor_started = True
|
||||
try:
|
||||
self.vtk_widget.Initialize()
|
||||
except Exception:
|
||||
try:
|
||||
self.interactor.Initialize()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.vtk_widget.Start()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.render_window.Render()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _is_ui_thread(self) -> bool:
|
||||
return QThread.currentThread() == self.thread()
|
||||
|
||||
@@ -93,7 +127,11 @@ class WindowCoreMixin:
|
||||
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.render_window.AddRenderer(self.renderer)
|
||||
if hasattr(self.renderer, "UseFXAAOff"):
|
||||
self.renderer.UseFXAAOff()
|
||||
|
||||
self.interactor = self.render_window.GetInteractor()
|
||||
self.interactor.SetInteractorStyle(vtk.vtkInteractorStyleTrackballCamera())
|
||||
@@ -105,7 +143,9 @@ class WindowCoreMixin:
|
||||
self.interactor.AddObserver("MiddleButtonReleaseEvent", self.on_pointer_button_release)
|
||||
self.interactor.AddObserver("RightButtonPressEvent", self.on_pointer_button_press)
|
||||
self.interactor.AddObserver("RightButtonReleaseEvent", self.on_pointer_button_release)
|
||||
self.interactor.AddObserver("MouseMoveEvent", self.on_mouse_move)
|
||||
# Qt mouse tracking already drives hover. Avoid routing every VTK
|
||||
# camera-move event through Python; that made rotation feel sticky on
|
||||
# large STEP meshes.
|
||||
self.interactor.AddObserver("StartInteractionEvent", self.on_camera_interaction_start)
|
||||
self.interactor.AddObserver("EndInteractionEvent", self.on_camera_interaction_end)
|
||||
if hasattr(self.interactor, "SetDesiredUpdateRate"):
|
||||
@@ -119,6 +159,12 @@ class WindowCoreMixin:
|
||||
light.SetIntensity(0.9)
|
||||
self.renderer.AddLight(light)
|
||||
|
||||
fill_light = vtk.vtkLight()
|
||||
fill_light.SetLightTypeToCameraLight()
|
||||
fill_light.SetPosition(-1, -1, 1)
|
||||
fill_light.SetIntensity(0.28)
|
||||
self.renderer.AddLight(fill_light)
|
||||
|
||||
self.interactor.Initialize()
|
||||
|
||||
def on_camera_interaction_start(self, _obj, _event) -> None:
|
||||
@@ -143,6 +189,19 @@ class WindowCoreMixin:
|
||||
if not getattr(self, "camera_interaction_active", False):
|
||||
return
|
||||
self.camera_interaction_active = False
|
||||
self.last_camera_interaction_ended_at = datetime.now()
|
||||
self.pending_hover_position = None
|
||||
self.last_hover_pick_position = None
|
||||
|
||||
def _hover_suppressed_after_camera(self) -> bool:
|
||||
ended_at = getattr(self, "last_camera_interaction_ended_at", None)
|
||||
if ended_at is None:
|
||||
return False
|
||||
cooldown_ms = int(getattr(self, "hover_after_camera_cooldown_ms", 0) or 0)
|
||||
if cooldown_ms <= 0:
|
||||
return False
|
||||
elapsed_ms = (datetime.now() - ended_at).total_seconds() * 1000.0
|
||||
return elapsed_ms < cooldown_ms
|
||||
|
||||
def closeEvent(self, event) -> None:
|
||||
load_thread_running = bool(
|
||||
@@ -213,11 +272,32 @@ class WindowCoreMixin:
|
||||
|
||||
self.load_in_progress = True
|
||||
self.pending_load_path = new_path
|
||||
self.statusBar().showMessage(f"Preparing fast load for {new_path.name}...")
|
||||
self.statusBar().showMessage(f"正在读取 STEP 可视化网格:{new_path.name}...")
|
||||
self._clear_hover(render=True)
|
||||
self._update_action_states()
|
||||
QTimer.singleShot(0, lambda path=new_path: self._run_deferred_initial_load(path))
|
||||
|
||||
@Slot(object)
|
||||
def _run_packaged_initial_load(self, expected_path: Path) -> None:
|
||||
if self.pending_load_path != expected_path:
|
||||
return
|
||||
try:
|
||||
self._load_step_sync(
|
||||
expected_path,
|
||||
deflection=self.preview_load_deflection,
|
||||
show_internal_edges=self._show_same_domain_internal_edges(),
|
||||
status_prefix="读取 STEP 可视化网格",
|
||||
)
|
||||
finally:
|
||||
self._end_load_task()
|
||||
|
||||
@Slot(object)
|
||||
def _start_initial_load_worker(self, expected_path: Path) -> None:
|
||||
# Qt/VTK/OCCT visualization objects are not safe to construct from this
|
||||
# worker path in the current PySide build. Keep the old entry point as a
|
||||
# main-thread fallback so stale signal paths cannot crash the process.
|
||||
self._run_deferred_initial_load(expected_path)
|
||||
|
||||
def _load_step_sync(
|
||||
self,
|
||||
new_path: Path,
|
||||
@@ -253,13 +333,29 @@ class WindowCoreMixin:
|
||||
try:
|
||||
self._load_step_sync(
|
||||
expected_path,
|
||||
deflection=self.initial_load_deflection,
|
||||
show_internal_edges=True,
|
||||
status_prefix="Fast loading",
|
||||
deflection=self.preview_load_deflection,
|
||||
show_internal_edges=self._show_same_domain_internal_edges(),
|
||||
status_prefix="读取 STEP 可视化网格",
|
||||
)
|
||||
finally:
|
||||
self._end_load_task()
|
||||
|
||||
@staticmethod
|
||||
def _copy_polydata_for_ui_thread(polydata: object) -> object:
|
||||
if polydata is None:
|
||||
return None
|
||||
copied = vtk.vtkPolyData()
|
||||
copied.DeepCopy(polydata)
|
||||
return copied
|
||||
|
||||
def _detach_worker_polydata_result(self, result: dict[str, object]) -> dict[str, object]:
|
||||
detached = dict(result)
|
||||
if detached.get("model_polydata") is not None:
|
||||
detached["model_polydata"] = self._copy_polydata_for_ui_thread(detached["model_polydata"])
|
||||
if detached.get("edge_polydata") is not None:
|
||||
detached["edge_polydata"] = self._copy_polydata_for_ui_thread(detached["edge_polydata"])
|
||||
return detached
|
||||
|
||||
def _apply_loaded_model_result(self, result: dict[str, object], *, reset_camera: bool) -> None:
|
||||
new_path = Path(result["path"])
|
||||
stats = result["stats"]
|
||||
@@ -309,9 +405,18 @@ class WindowCoreMixin:
|
||||
try:
|
||||
if not isinstance(result, dict):
|
||||
raise RuntimeError("Load task returned an unexpected result.")
|
||||
result = self._detach_worker_polydata_result(result)
|
||||
self._apply_loaded_model_result(result, reset_camera=True)
|
||||
self.statusBar().showMessage(f"Loaded {self.step_path.name}")
|
||||
self._end_load_task()
|
||||
self.load_in_progress = False
|
||||
self.pending_load_path = None
|
||||
self._update_action_states()
|
||||
if float(getattr(self, "preview_load_deflection", 0.0) or 0.0) > float(
|
||||
getattr(self, "initial_load_deflection", 0.0) or 0.0
|
||||
):
|
||||
self.statusBar().showMessage(f"已快速显示模型:{self.step_path.name},正在后台细化显示...")
|
||||
self._start_load_refine(result)
|
||||
else:
|
||||
self.statusBar().showMessage(f"Loaded {self.step_path.name}")
|
||||
except Exception as exc:
|
||||
self._end_load_task()
|
||||
QMessageBox.critical(self, "Load failed", str(exc))
|
||||
@@ -326,14 +431,18 @@ class WindowCoreMixin:
|
||||
self.statusBar().showMessage("STEP load failed; current model was left unchanged.")
|
||||
|
||||
def _start_load_refine(self, initial_result: dict[str, object]) -> None:
|
||||
self._end_load_task()
|
||||
if self.model is not None:
|
||||
self.statusBar().showMessage(f"Loaded {self.step_path.name}")
|
||||
self.statusBar().showMessage(f"Loaded {self.step_path.name}")
|
||||
|
||||
@Slot(object)
|
||||
def _finish_load_refine(self, result: object) -> None:
|
||||
try:
|
||||
if isinstance(result, dict) and result.get("model") is self.model:
|
||||
if (
|
||||
isinstance(result, dict)
|
||||
and Path(result.get("path", "")) == self.step_path
|
||||
and not getattr(self, "operation_history", [])
|
||||
):
|
||||
self.model = result["model"]
|
||||
result = self._detach_worker_polydata_result(result)
|
||||
self._rebuild_scene_from_polydata(
|
||||
result["model_polydata"],
|
||||
result["edge_polydata"],
|
||||
@@ -351,7 +460,7 @@ class WindowCoreMixin:
|
||||
"display": "ready",
|
||||
}
|
||||
)
|
||||
self.statusBar().showMessage(f"Loaded {self.step_path.name}")
|
||||
self.statusBar().showMessage(f"精细显示已完成:{self.step_path.name}")
|
||||
finally:
|
||||
self._end_load_task()
|
||||
|
||||
@@ -385,12 +494,24 @@ class WindowCoreMixin:
|
||||
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
|
||||
)
|
||||
if path:
|
||||
self.load_step(path)
|
||||
self.step_path = Path(path)
|
||||
if hasattr(self, "path_label"):
|
||||
path_text = str(self.step_path)
|
||||
self.path_label.setText(path_text)
|
||||
self.path_label.setToolTip(path_text)
|
||||
self.path_label.setCursorPosition(0)
|
||||
self.statusBar().showMessage(f"已选择 STEP 文件:{self.step_path.name},正在读取模型...")
|
||||
self._update_action_states()
|
||||
self._ensure_vtk_interactor_started()
|
||||
self.load_step(self.step_path, background=True)
|
||||
|
||||
def reload_step(self) -> None:
|
||||
if self._edit_busy("请等待当前编辑完成后再重新加载。"):
|
||||
return
|
||||
self.load_step(self.step_path)
|
||||
path_text = self.path_label.text().strip() if hasattr(self, "path_label") else ""
|
||||
path = Path(path_text) if path_text else self.step_path
|
||||
self._ensure_vtk_interactor_started()
|
||||
self.load_step(path)
|
||||
|
||||
def _clear_history(self) -> None:
|
||||
self.undo_stack.clear()
|
||||
@@ -1107,6 +1228,7 @@ class WindowCoreMixin:
|
||||
or self.load_in_progress
|
||||
or self.model is None
|
||||
or self.model_actor is None
|
||||
or self._hover_suppressed_after_camera()
|
||||
):
|
||||
self._clear_hover(render=True)
|
||||
return
|
||||
@@ -1129,7 +1251,11 @@ class WindowCoreMixin:
|
||||
if not self._is_ui_thread():
|
||||
self._invoke_on_ui_thread(lambda x=int(x), y=int(y): self._queue_hover_position(x, y))
|
||||
return
|
||||
if getattr(self, "pointer_button_down", False) or getattr(self, "camera_interaction_active", False):
|
||||
if (
|
||||
getattr(self, "pointer_button_down", False)
|
||||
or getattr(self, "camera_interaction_active", False)
|
||||
or self._hover_suppressed_after_camera()
|
||||
):
|
||||
return
|
||||
position = (int(x), int(y))
|
||||
threshold = int(getattr(self, "hover_move_threshold_px", 0) or 0)
|
||||
@@ -1156,6 +1282,7 @@ class WindowCoreMixin:
|
||||
or self.model is None
|
||||
or self.model_actor is None
|
||||
or self.pending_hover_position is None
|
||||
or self._hover_suppressed_after_camera()
|
||||
):
|
||||
self._clear_hover(render=True)
|
||||
return
|
||||
@@ -1576,7 +1703,7 @@ class WindowCoreMixin:
|
||||
self.selected_pick_position = pick_position
|
||||
info = self.model.quick_face_info(face_id)
|
||||
if feature_mode:
|
||||
info = self._feature_info_for_selected_face(face_id, info)
|
||||
info = self._feature_context_info(face_id)
|
||||
info["kind"] = "feature"
|
||||
info.setdefault("feature_mode", "当前是几何候选判断,不等同于 CAD 历史特征")
|
||||
highlight_face_ids = _int_values(info.get("feature_highlight_face_ids")) or [face_id]
|
||||
@@ -1601,7 +1728,7 @@ class WindowCoreMixin:
|
||||
return
|
||||
if self._edit_busy("编辑计算中,暂时不能选择特征。"):
|
||||
return
|
||||
info = self._feature_info_for_selected_face(face_id, self.model.quick_face_info(face_id))
|
||||
info = self._feature_context_info(face_id)
|
||||
info["kind"] = "feature"
|
||||
self._reset_selection(clear_highlight=False, clear_info=False)
|
||||
self.selected_kind = "feature"
|
||||
@@ -2026,16 +2153,37 @@ class WindowCoreMixin:
|
||||
self._start_edit_preview_pulse()
|
||||
self.render_window.Render()
|
||||
|
||||
def _show_shell_thickness_preview(self, face_id: int, target_thickness: float) -> None:
|
||||
def _show_shell_thickness_preview(
|
||||
self,
|
||||
face_id: int,
|
||||
target_thickness: float,
|
||||
plan: dict[str, object] | None = None,
|
||||
) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
self.clear_edit_preview(render=False)
|
||||
try:
|
||||
polydata = self.model.shell_thickness_preview_polydata(face_id, target_thickness)
|
||||
plan = dict(plan or self.model.shell_thickness_plan(face_id, target_thickness))
|
||||
scope_face_ids = _int_values(plan.get("shell_source_face_ids")) or [face_id]
|
||||
polydata = self._cached_face_overlay_polydata(face_ids=scope_face_ids, smooth=False)
|
||||
if polydata is None:
|
||||
polydata = self._cached_face_overlay_polydata(face_ids=[face_id], smooth=False)
|
||||
if polydata is None:
|
||||
raise RuntimeError("当前显示网格里没有可复用的薄壁预览数据")
|
||||
except Exception as exc:
|
||||
self.statusBar().showMessage(f"薄壁厚度调整预览不可用:{exc}")
|
||||
return
|
||||
self._add_edit_preview_actor(polydata, (0.0, 0.72, 0.78), opacity=0.34)
|
||||
movement = _triple_or_none(plan.get("shell_desired_movement_vector")) if isinstance(plan, dict) else None
|
||||
if movement is None and isinstance(plan, dict):
|
||||
outward = _triple_or_none(plan.get("outward_direction"))
|
||||
distance = _float_or_none(plan.get("push_pull_distance"))
|
||||
if outward is not None and distance is not None:
|
||||
movement = (
|
||||
float(outward[0]) * float(distance),
|
||||
float(outward[1]) * float(distance),
|
||||
float(outward[2]) * float(distance),
|
||||
)
|
||||
self._add_edit_preview_actor(polydata, (0.0, 0.72, 0.78), opacity=0.34, position=movement)
|
||||
self._start_edit_preview_pulse()
|
||||
self.render_window.Render()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user