feat: 完善 SCDM-first 参数化编辑交付版
This commit is contained in:
+231
-98
@@ -133,13 +133,14 @@ class WindowCoreMixin:
|
||||
self.vtk_widget.Start()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.render_window.Render()
|
||||
except Exception:
|
||||
pass
|
||||
self._render_window_safely(force=True)
|
||||
|
||||
def _is_ui_thread(self) -> bool:
|
||||
return QThread.currentThread() == self.thread()
|
||||
try:
|
||||
owner_thread = self.thread()
|
||||
except AttributeError:
|
||||
return True
|
||||
return QThread.currentThread() == owner_thread
|
||||
|
||||
def _invoke_on_ui_thread(self, callback) -> None:
|
||||
if self._is_ui_thread():
|
||||
@@ -148,6 +149,21 @@ class WindowCoreMixin:
|
||||
if hasattr(self, "ui_task_requested"):
|
||||
self.ui_task_requested.emit(callback)
|
||||
|
||||
def _render_window_safely(self, *, force: bool = False) -> None:
|
||||
render_window = getattr(self, "render_window", None)
|
||||
if render_window is None:
|
||||
return
|
||||
if not self._is_ui_thread():
|
||||
# VTK/Qt 渲染窗口只能在 UI 线程触碰;后台 worker 请求渲染时转成 queued UI 任务。
|
||||
self._invoke_on_ui_thread(lambda force=force: self._render_window_safely(force=force))
|
||||
return
|
||||
if bool(getattr(self, "scene_rebuild_in_progress", False)) and not force:
|
||||
return
|
||||
try:
|
||||
render_window.Render()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _reroute_to_ui_thread(self, callback) -> bool:
|
||||
if self._is_ui_thread():
|
||||
return False
|
||||
@@ -193,6 +209,8 @@ class WindowCoreMixin:
|
||||
editor = getattr(self, "relation_formula_input", None)
|
||||
popup = self._relation_formula_popup_widget()
|
||||
if editor is not None and hasattr(editor, "backspace"):
|
||||
# QCompleter 的 popup 会抢走 Backspace/Tab 焦点;手动把按键转回输入框,
|
||||
# 否则用户补全后继续编辑时会出现“失焦、删不动”的体验。
|
||||
if popup is not None and hasattr(popup, "hide"):
|
||||
popup.hide()
|
||||
if hasattr(editor, "setFocus"):
|
||||
@@ -483,9 +501,7 @@ class WindowCoreMixin:
|
||||
return
|
||||
self._fps_idle_rendering_zero = True
|
||||
try:
|
||||
render_window.Render()
|
||||
except Exception:
|
||||
pass
|
||||
self._render_window_safely(force=True)
|
||||
finally:
|
||||
self._fps_idle_rendering_zero = False
|
||||
|
||||
@@ -1005,10 +1021,7 @@ class WindowCoreMixin:
|
||||
self.renderer.ResetCameraClippingRange()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.render_window.Render()
|
||||
except Exception:
|
||||
pass
|
||||
self._render_window_safely()
|
||||
|
||||
def _hover_suppressed_after_camera(self) -> bool:
|
||||
ended_at = getattr(self, "last_camera_interaction_ended_at", None)
|
||||
@@ -1180,6 +1193,7 @@ class WindowCoreMixin:
|
||||
show_internal_edges: bool,
|
||||
status_prefix: str,
|
||||
defer_edges: bool = True,
|
||||
reset_camera: bool = True,
|
||||
) -> bool:
|
||||
self.statusBar().showMessage(f"{status_prefix} {new_path.name}...")
|
||||
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
||||
@@ -1198,7 +1212,24 @@ class WindowCoreMixin:
|
||||
return False
|
||||
finally:
|
||||
QApplication.restoreOverrideCursor()
|
||||
self._apply_loaded_model_result(result, reset_camera=True)
|
||||
try:
|
||||
self._apply_loaded_model_result(result, reset_camera=reset_camera)
|
||||
except Exception as exc:
|
||||
pending = getattr(self, "pending_scdm_edit_reload", None)
|
||||
if isinstance(pending, dict) and hasattr(self, "_reject_scdm_result_after_reload"):
|
||||
self.pending_scdm_edit_reload = None
|
||||
self._reject_scdm_result_after_reload(
|
||||
pending,
|
||||
{
|
||||
"ok": False,
|
||||
"reason": "result-load-failed",
|
||||
"message": f"SCDM 修改结果 STEP 已生成,但加载到当前场景失败:{exc}",
|
||||
},
|
||||
)
|
||||
else:
|
||||
QMessageBox.critical(self, "Load failed", str(exc))
|
||||
self.statusBar().showMessage("STEP load failed; current model was left unchanged.")
|
||||
return False
|
||||
timing_text = _timing_summary(result.get("timings") if isinstance(result, dict) else None)
|
||||
suffix = f";{timing_text}" if timing_text else ""
|
||||
self.statusBar().showMessage(f"Loaded {self.step_path.name}{suffix}")
|
||||
@@ -1224,7 +1255,10 @@ class WindowCoreMixin:
|
||||
if polydata is None:
|
||||
return None
|
||||
copied = vtk.vtkPolyData()
|
||||
copied.ShallowCopy(polydata)
|
||||
try:
|
||||
copied.DeepCopy(polydata)
|
||||
except Exception:
|
||||
copied.ShallowCopy(polydata)
|
||||
return copied
|
||||
|
||||
def _detach_worker_polydata_result(self, result: dict[str, object]) -> dict[str, object]:
|
||||
@@ -1239,10 +1273,12 @@ class WindowCoreMixin:
|
||||
apply_started = time.perf_counter()
|
||||
new_path = Path(result["path"])
|
||||
stats = result["stats"]
|
||||
pending_scdm_reload = isinstance(getattr(self, "pending_scdm_edit_reload", None), dict)
|
||||
self.model = result["model"]
|
||||
self.step_path = new_path
|
||||
self._invalidate_scdm_feature_cache("模型已重新加载,SCDM cache 已失效。")
|
||||
self._clear_history()
|
||||
if not pending_scdm_reload:
|
||||
self._clear_history()
|
||||
if hasattr(self, "measure_text"):
|
||||
self.clear_measurement()
|
||||
path_text = str(self.step_path)
|
||||
@@ -1250,7 +1286,10 @@ class WindowCoreMixin:
|
||||
self.path_label.setToolTip(path_text)
|
||||
self.path_label.setCursorPosition(0)
|
||||
self._populate_part_tree()
|
||||
self._reset_selection()
|
||||
if pending_scdm_reload:
|
||||
self._reset_selection(clear_highlight=True, clear_info=False)
|
||||
else:
|
||||
self._reset_selection()
|
||||
large_interaction_model = self._large_model_interaction_mode(stats=stats)
|
||||
self.hide_edges_during_camera_interaction = large_interaction_model
|
||||
self.hide_overlays_during_camera_interaction = large_interaction_model
|
||||
@@ -1307,14 +1346,11 @@ class WindowCoreMixin:
|
||||
self.statusBar().showMessage(
|
||||
f"Loaded {self.step_path.name}; 大模型已跳过全量边线补绘,旋转会更流畅,切到 Edge 选择时再按需生成。"
|
||||
)
|
||||
pending_scdm_reload = isinstance(getattr(self, "pending_scdm_edit_reload", None), dict)
|
||||
scdm_cache_restored = False if pending_scdm_reload else self._restore_scdm_feature_cache_from_disk()
|
||||
if large_interaction_model and not pending_scdm_reload and not scdm_cache_restored:
|
||||
self._defer_large_model_recognition_preloads()
|
||||
else:
|
||||
QTimer.singleShot(160, self._start_asitus_hole_recognition_preload)
|
||||
if pending_scdm_reload or not scdm_cache_restored:
|
||||
QTimer.singleShot(240, lambda: self._start_scdm_probe_preload(force=pending_scdm_reload))
|
||||
if pending_scdm_reload:
|
||||
QTimer.singleShot(120, lambda: self._start_scdm_probe_preload(force=True))
|
||||
elif not scdm_cache_restored:
|
||||
self._defer_post_import_recognition_preloads()
|
||||
|
||||
def _large_model_interaction_mode(self, stats: object | None = None) -> bool:
|
||||
if stats is not None:
|
||||
@@ -1343,6 +1379,36 @@ class WindowCoreMixin:
|
||||
if hasattr(self, "_update_current_capability_panel"):
|
||||
self._update_current_capability_panel()
|
||||
|
||||
def _defer_post_import_recognition_preloads(self) -> None:
|
||||
self.scdm_feature_cache_state = "deferred"
|
||||
self.scdm_feature_cache_message = (
|
||||
"模型已先完成显示;SCDM 可修改参数将在用户选择对象后按需识别,避免导入阶段与首次渲染抢占资源。"
|
||||
)
|
||||
if self.model is not None:
|
||||
try:
|
||||
self.model.fail_asitus_hole_region_load("导入后已延后 Analysis Situs 全量孔组识别。")
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(self, "_update_current_capability_panel"):
|
||||
self._update_current_capability_panel()
|
||||
|
||||
def _maybe_start_scdm_probe_for_selection(self) -> None:
|
||||
if self.model is None or self.step_path is None:
|
||||
return
|
||||
if str(getattr(self, "scdm_feature_cache_state", "") or "") != "deferred":
|
||||
return
|
||||
thread = getattr(self, "scdm_thread", None)
|
||||
if thread is not None and thread.isRunning():
|
||||
return
|
||||
self.scdm_feature_cache_message = (
|
||||
"已选择对象,正在按需调用 SCDM 识别可修改参数;"
|
||||
"识别完成前暂不把圆柱类本地兜底判断当作已确认孔/槽能力。"
|
||||
)
|
||||
if hasattr(self, "_update_current_capability_panel"):
|
||||
self._update_current_capability_panel()
|
||||
self.statusBar().showMessage("正在按需识别 SCDM 可修改参数,可继续查看模型。")
|
||||
QTimer.singleShot(0, lambda: self._start_scdm_probe_preload(force=True))
|
||||
|
||||
def _invalidate_scdm_feature_cache(self, message: str = "") -> None:
|
||||
self.scdm_feature_cache = None
|
||||
self.scdm_feature_cache_state = "stale"
|
||||
@@ -1356,7 +1422,30 @@ class WindowCoreMixin:
|
||||
if hasattr(self, "_update_current_capability_panel"):
|
||||
self._update_current_capability_panel()
|
||||
|
||||
def _install_scdm_feature_cache(self, cache: dict[str, object], *, cache_path: str = "", message: str = "") -> None:
|
||||
def _scdm_feature_cache_valid_for_loaded_step(self, cache: object) -> bool:
|
||||
if self.step_path is None or not isinstance(cache, dict):
|
||||
return False
|
||||
try:
|
||||
fingerprint = file_fingerprint(Path(self.step_path))
|
||||
except OSError:
|
||||
return False
|
||||
revision = _safe_int_or_none(cache.get("mapperRevision")) or 0
|
||||
return str(cache.get("modelFingerprint") or "") == fingerprint and revision >= SCDM_FEATURE_CACHE_REVISION
|
||||
|
||||
def _install_scdm_feature_cache(self, cache: dict[str, object], *, cache_path: str = "", message: str = "") -> bool:
|
||||
if not self._scdm_feature_cache_valid_for_loaded_step(cache):
|
||||
self.scdm_feature_cache = None
|
||||
self.scdm_feature_cache_state = "stale"
|
||||
self.scdm_feature_cache_message = "本地 SCDM cache 与当前模型或当前 mapper 版本不匹配,已丢弃。"
|
||||
self.scdm_feature_cache_path = ""
|
||||
if self.model is not None:
|
||||
try:
|
||||
self.model.scdm_feature_cache = None
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(self, "_update_current_capability_panel"):
|
||||
self._update_current_capability_panel()
|
||||
return False
|
||||
self.scdm_feature_cache = dict(cache)
|
||||
self.scdm_feature_cache_state = "ready"
|
||||
self.scdm_feature_cache_message = message or "SCDM 可修改参数识别完成。"
|
||||
@@ -1370,6 +1459,7 @@ class WindowCoreMixin:
|
||||
self._update_current_capability_panel()
|
||||
if hasattr(self, "_refresh_property_editor"):
|
||||
self._refresh_property_editor()
|
||||
return True
|
||||
|
||||
def _restore_scdm_feature_cache_from_disk(self) -> bool:
|
||||
if self.model is None or self.step_path is None:
|
||||
@@ -1382,7 +1472,6 @@ class WindowCoreMixin:
|
||||
project_root = Path(__file__).resolve().parent.parent
|
||||
work_dir = default_scdm_work_dir(step_path, project_root=project_root, fingerprint=fingerprint)
|
||||
cache_path = work_dir / "scdm_feature_cache.json"
|
||||
raw_path = work_dir / "scdm_raw_features.json"
|
||||
|
||||
cache: dict[str, object] | None = None
|
||||
if cache_path.is_file():
|
||||
@@ -1391,43 +1480,25 @@ class WindowCoreMixin:
|
||||
revision = _safe_int_or_none(candidate.get("mapperRevision")) or 0
|
||||
if str(candidate.get("modelFingerprint") or "") == fingerprint and revision >= SCDM_FEATURE_CACHE_REVISION:
|
||||
cache = candidate
|
||||
except Exception:
|
||||
cache = None
|
||||
|
||||
if cache is None and raw_path.is_file():
|
||||
try:
|
||||
raw = read_json(raw_path)
|
||||
raw_model = raw.get("model")
|
||||
raw_fingerprint = str(raw_model.get("fingerprint") or "") if isinstance(raw_model, dict) else ""
|
||||
if raw_fingerprint == fingerprint:
|
||||
cache = attach_local_face_ids_to_scdm_cache(
|
||||
map_scdm_raw_features(raw),
|
||||
self._scdm_local_face_signatures(),
|
||||
)
|
||||
write_json(cache_path, cache)
|
||||
elif str(candidate.get("modelFingerprint") or "") == fingerprint:
|
||||
self.scdm_feature_cache_message = "本地 SCDM cache 版本过旧,将在选择对象后按需重新识别。"
|
||||
except Exception:
|
||||
cache = None
|
||||
|
||||
if cache is None:
|
||||
return False
|
||||
self._install_scdm_feature_cache(
|
||||
installed = self._install_scdm_feature_cache(
|
||||
cache,
|
||||
cache_path=str(cache_path),
|
||||
message="已从本地 SCDM 识别缓存恢复;模型变更后会重新识别。",
|
||||
)
|
||||
if not installed:
|
||||
return False
|
||||
self.statusBar().showMessage("已恢复本地 SCDM 识别缓存,参数表可直接使用。")
|
||||
return True
|
||||
|
||||
def _current_scdm_feature_cache_matches_loaded_step(self) -> bool:
|
||||
if self.step_path is None or not isinstance(getattr(self, "scdm_feature_cache", None), dict):
|
||||
return False
|
||||
cache = getattr(self, "scdm_feature_cache", None)
|
||||
try:
|
||||
fingerprint = file_fingerprint(Path(self.step_path))
|
||||
except OSError:
|
||||
return False
|
||||
revision = _safe_int_or_none(cache.get("mapperRevision")) or 0
|
||||
return str(cache.get("modelFingerprint") or "") == fingerprint and revision >= SCDM_FEATURE_CACHE_REVISION
|
||||
return self._scdm_feature_cache_valid_for_loaded_step(getattr(self, "scdm_feature_cache", None))
|
||||
|
||||
def _start_scdm_probe_preload(self, *, force: bool = False) -> None:
|
||||
if self.model is None or self.step_path is None:
|
||||
@@ -1441,11 +1512,14 @@ class WindowCoreMixin:
|
||||
QTimer.singleShot(500, lambda: self._start_scdm_probe_preload(force=force))
|
||||
return
|
||||
step_path = Path(self.step_path)
|
||||
try:
|
||||
local_face_signatures = [dict(item) for item in self._scdm_local_face_signatures()]
|
||||
except Exception:
|
||||
local_face_signatures = []
|
||||
context = {
|
||||
"path": step_path,
|
||||
"model_id": id(self.model),
|
||||
}
|
||||
model = self.model
|
||||
self.pending_scdm_context = dict(context)
|
||||
self.scdm_feature_cache_state = "running"
|
||||
self.scdm_feature_cache_message = "正在识别 SCDM 可修改参数。"
|
||||
@@ -1453,7 +1527,7 @@ class WindowCoreMixin:
|
||||
if hasattr(self, "_update_current_capability_panel"):
|
||||
self._update_current_capability_panel()
|
||||
|
||||
def action(path=step_path, model=model):
|
||||
def action(path=step_path, face_signatures=tuple(local_face_signatures)):
|
||||
probe = run_scdm_probe(path, project_root=Path(__file__).resolve().parent.parent, timeout_seconds=180.0)
|
||||
if not isinstance(probe, dict) or probe.get("ok") is not True:
|
||||
return {
|
||||
@@ -1465,13 +1539,6 @@ class WindowCoreMixin:
|
||||
raw = probe.get("raw")
|
||||
if not isinstance(raw, dict):
|
||||
return {"ok": False, "reason": "missing-raw", "message": "SCDM probe did not return raw feature data.", "probe": probe}
|
||||
face_signatures = []
|
||||
builder = getattr(model, "scdm_local_face_signatures", None)
|
||||
if callable(builder):
|
||||
try:
|
||||
face_signatures = [dict(item) for item in builder() if isinstance(item, dict)]
|
||||
except Exception:
|
||||
face_signatures = []
|
||||
cache = attach_local_face_ids_to_scdm_cache(
|
||||
map_scdm_raw_features(raw),
|
||||
face_signatures,
|
||||
@@ -1553,11 +1620,16 @@ class WindowCoreMixin:
|
||||
return
|
||||
if isinstance(result.get("backend"), dict):
|
||||
self.scdm_backend_status = dict(result["backend"])
|
||||
self._install_scdm_feature_cache(
|
||||
installed = self._install_scdm_feature_cache(
|
||||
dict(cache),
|
||||
cache_path=str(result.get("cache_path") or ""),
|
||||
message="SCDM 可修改参数识别完成。",
|
||||
)
|
||||
if not installed:
|
||||
self.statusBar().showMessage("SCDM cache 与当前模型不匹配,已丢弃并保持参数表不可用。")
|
||||
if hasattr(self, "_finish_pending_scdm_edit_reload"):
|
||||
self._finish_pending_scdm_edit_reload(cache_ready=False, message="SCDM cache 与当前模型不匹配")
|
||||
return
|
||||
objects = cache.get("objects")
|
||||
count = len(objects) if isinstance(objects, list) else 0
|
||||
self.statusBar().showMessage(f"SCDM 可修改参数识别完成:{count} 个产品化对象。")
|
||||
@@ -1754,6 +1826,7 @@ class WindowCoreMixin:
|
||||
self.statusBar().showMessage(f"Quick preview is available; display refinement failed: {message}")
|
||||
|
||||
def _end_load_task(self) -> None:
|
||||
self.scene_rebuild_in_progress = False
|
||||
self.load_in_progress = False
|
||||
self.pending_load_path = None
|
||||
self._update_action_states()
|
||||
@@ -2136,7 +2209,7 @@ class WindowCoreMixin:
|
||||
QMessageBox.information(self, "未选择对象", "请先选择零件、Solid、Face、Edge 或特征。")
|
||||
return
|
||||
self._fit_camera_to_bounds(bounds)
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
self.statusBar().showMessage("已对准选中对象")
|
||||
|
||||
def _selected_focus_bounds(self) -> tuple[float, float, float, float, float, float] | None:
|
||||
@@ -2275,7 +2348,7 @@ class WindowCoreMixin:
|
||||
self.renderer.AddActor(self.edge_actor)
|
||||
self.edge_overlay_polydata_cache.clear()
|
||||
if render:
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
@Slot()
|
||||
def _rebuild_deferred_edge_display(self) -> None:
|
||||
@@ -2327,13 +2400,52 @@ class WindowCoreMixin:
|
||||
if self.step_path is None or Path(result.get("path", "")) != Path(self.step_path):
|
||||
return
|
||||
edge_polydata = self._copy_polydata_for_ui_thread(result.get("edge_polydata"))
|
||||
self._install_edge_polydata(edge_polydata, render=True)
|
||||
self.large_model_edge_overlay_skipped = False
|
||||
elapsed = float(result.get("elapsed") or 0.0)
|
||||
self.statusBar().showMessage(f"模型边线已补充,用时 {elapsed:.1f}s")
|
||||
deferred_result = dict(result)
|
||||
deferred_result["edge_polydata"] = edge_polydata
|
||||
if self._defer_scene_actor_update_if_interacting(
|
||||
lambda deferred_result=deferred_result: self._apply_deferred_edge_display_result(deferred_result)
|
||||
):
|
||||
return
|
||||
self._apply_deferred_edge_display_result(deferred_result)
|
||||
finally:
|
||||
self._request_thread_quit(self.load_refine_thread)
|
||||
|
||||
def _apply_deferred_edge_display_result(self, result: dict[str, object]) -> None:
|
||||
if not isinstance(result, dict):
|
||||
return
|
||||
if self.model is None or id(self.model) != result.get("model_id"):
|
||||
return
|
||||
if self.step_path is None or Path(result.get("path", "")) != Path(self.step_path):
|
||||
return
|
||||
if self._defer_scene_actor_update_if_interacting(
|
||||
lambda result=result: self._apply_deferred_edge_display_result(result)
|
||||
):
|
||||
return
|
||||
self._install_edge_polydata(result.get("edge_polydata"), render=True)
|
||||
self.large_model_edge_overlay_skipped = False
|
||||
elapsed = float(result.get("elapsed") or 0.0)
|
||||
self.statusBar().showMessage(f"模型边线已补充,用时 {elapsed:.1f}s")
|
||||
|
||||
def _defer_scene_actor_update_if_interacting(self, callback, *, delay_ms: int = 140) -> bool:
|
||||
if not self._is_ui_thread():
|
||||
self._invoke_on_ui_thread(callback)
|
||||
return True
|
||||
if not self._scene_actor_update_should_wait_for_camera():
|
||||
return False
|
||||
QTimer.singleShot(max(int(delay_ms), 1), callback)
|
||||
return True
|
||||
|
||||
def _scene_actor_update_should_wait_for_camera(self) -> bool:
|
||||
if bool(getattr(self, "camera_interaction_active", False)):
|
||||
return True
|
||||
if bool(getattr(self, "pointer_button_down", False)):
|
||||
return True
|
||||
ended_at = getattr(self, "last_camera_interaction_ended_at", None)
|
||||
if ended_at is None:
|
||||
return False
|
||||
elapsed_ms = (datetime.now() - ended_at).total_seconds() * 1000.0
|
||||
return elapsed_ms < 120.0
|
||||
|
||||
@Slot(str)
|
||||
def _fail_deferred_edge_display(self, message: str) -> None:
|
||||
if self._reroute_to_ui_thread(lambda message=message: self._fail_deferred_edge_display(message)):
|
||||
@@ -2433,6 +2545,7 @@ class WindowCoreMixin:
|
||||
return polydata
|
||||
|
||||
def _rebuild_scene_from_polydata(self, model_polydata, edge_polydata, reset_camera: bool = False) -> None:
|
||||
self.scene_rebuild_in_progress = True
|
||||
self._clear_overlay_polydata_cache()
|
||||
self.renderer.RemoveAllViewProps()
|
||||
self.renderer.SetBackground(*VIEW_BACKGROUND_COLOR)
|
||||
@@ -2486,7 +2599,8 @@ class WindowCoreMixin:
|
||||
self.renderer.ResetCamera()
|
||||
self._ensure_step_coordinate_axes()
|
||||
self.renderer.ResetCameraClippingRange()
|
||||
self.render_window.Render()
|
||||
self.scene_rebuild_in_progress = False
|
||||
self._render_window_safely(force=True)
|
||||
|
||||
def _on_mode_changed(self, mode: str) -> None:
|
||||
self._clear_hover(render=True)
|
||||
@@ -2620,6 +2734,7 @@ class WindowCoreMixin:
|
||||
getattr(self, "load_in_progress", False)
|
||||
or getattr(self, "operation_in_progress", False)
|
||||
or getattr(self, "scan_in_progress", False)
|
||||
or getattr(self, "scene_rebuild_in_progress", False)
|
||||
or getattr(self, "model", None) is None
|
||||
):
|
||||
return None
|
||||
@@ -2659,14 +2774,14 @@ class WindowCoreMixin:
|
||||
if press_signature is None or release_signature != press_signature:
|
||||
self._cancel_hover_tracking(render=False)
|
||||
if hasattr(self, "render_window"):
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
if self.selected_kind is not None:
|
||||
self.statusBar().showMessage("左键按下和松开未命中同一对象,已保持当前选择")
|
||||
return
|
||||
if target is None:
|
||||
self._cancel_hover_tracking(render=False)
|
||||
if hasattr(self, "render_window"):
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
if self.selected_kind is not None:
|
||||
self.statusBar().showMessage("未命中对象,已保持当前选择")
|
||||
else:
|
||||
@@ -2815,6 +2930,7 @@ class WindowCoreMixin:
|
||||
self.operation_in_progress
|
||||
or self.scan_in_progress
|
||||
or self.load_in_progress
|
||||
or getattr(self, "scene_rebuild_in_progress", False)
|
||||
or self.model is None
|
||||
or self.model_actor is None
|
||||
or self._hover_suppressed_after_camera()
|
||||
@@ -2828,6 +2944,7 @@ class WindowCoreMixin:
|
||||
self.operation_in_progress
|
||||
or self.scan_in_progress
|
||||
or self.load_in_progress
|
||||
or getattr(self, "scene_rebuild_in_progress", False)
|
||||
or self.model is None
|
||||
or self.model_actor is None
|
||||
):
|
||||
@@ -2843,9 +2960,11 @@ class WindowCoreMixin:
|
||||
if (
|
||||
getattr(self, "pointer_button_down", False)
|
||||
or getattr(self, "camera_interaction_active", False)
|
||||
or getattr(self, "scene_rebuild_in_progress", False)
|
||||
or getattr(self, "large_model_hover_disabled", False)
|
||||
or self._hover_suppressed_after_camera()
|
||||
):
|
||||
# 鼠标拖动旋转和场景重建期间不做 hover/pick,避免“只是看模型却自动选面”。
|
||||
return
|
||||
position = (int(x), int(y))
|
||||
threshold = int(getattr(self, "hover_move_threshold_px", 0) or 0)
|
||||
@@ -2869,6 +2988,7 @@ class WindowCoreMixin:
|
||||
self.operation_in_progress
|
||||
or self.scan_in_progress
|
||||
or self.load_in_progress
|
||||
or getattr(self, "scene_rebuild_in_progress", False)
|
||||
or self.model is None
|
||||
or self.model_actor is None
|
||||
or self.pending_hover_position is None
|
||||
@@ -2918,19 +3038,27 @@ class WindowCoreMixin:
|
||||
return None
|
||||
|
||||
def _pick_actor_cell(self, actor, x: int, y: int) -> dict[str, object] | None:
|
||||
if actor is None:
|
||||
if actor is None or bool(getattr(self, "scene_rebuild_in_progress", False)):
|
||||
return None
|
||||
self.picker.InitializePickList()
|
||||
self.picker.PickFromListOn()
|
||||
self.picker.AddPickList(actor)
|
||||
picked = self.picker.Pick(int(x), int(y), 0, self.renderer)
|
||||
self.picker.PickFromListOff()
|
||||
if not picked or self.picker.GetCellId() < 0:
|
||||
try:
|
||||
self.picker.InitializePickList()
|
||||
self.picker.PickFromListOn()
|
||||
self.picker.AddPickList(actor)
|
||||
# 只拾取指定 actor,避免坐标轴、高亮 overlay 或边线 actor 抢到 Face/Edge 选择。
|
||||
picked = self.picker.Pick(int(x), int(y), 0, self.renderer)
|
||||
self.picker.PickFromListOff()
|
||||
if not picked or self.picker.GetCellId() < 0:
|
||||
return None
|
||||
return {
|
||||
"cell_id": int(self.picker.GetCellId()),
|
||||
"pick_position": _vector_tuple(self.picker.GetPickPosition()),
|
||||
}
|
||||
except Exception:
|
||||
try:
|
||||
self.picker.PickFromListOff()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
return {
|
||||
"cell_id": int(self.picker.GetCellId()),
|
||||
"pick_position": _vector_tuple(self.picker.GetPickPosition()),
|
||||
}
|
||||
|
||||
def _pick_face_cell(self, x: int, y: int) -> dict[str, object] | None:
|
||||
hit = self._pick_actor_cell(self.model_actor, x, y)
|
||||
@@ -3263,6 +3391,7 @@ class WindowCoreMixin:
|
||||
self._sync_id_picker("Part", part_id)
|
||||
self.set_info(self._with_pick_info(info, pick_position))
|
||||
self._update_action_states()
|
||||
self._maybe_start_scdm_probe_for_selection()
|
||||
kind_label = _part_tree_kind_label(str(info.get("kind", "part")))
|
||||
self.statusBar().showMessage(self._selection_status(f"已选择{kind_label} {part_id}", pick_position))
|
||||
|
||||
@@ -3288,6 +3417,7 @@ class WindowCoreMixin:
|
||||
self._sync_id_picker("Solid", solid_id)
|
||||
self.set_info(self._with_pick_info(info, pick_position))
|
||||
self._update_action_states()
|
||||
self._maybe_start_scdm_probe_for_selection()
|
||||
self.statusBar().showMessage(self._selection_status(f"已选择Solid {solid_id}", pick_position))
|
||||
|
||||
def select_face(
|
||||
@@ -3328,6 +3458,7 @@ class WindowCoreMixin:
|
||||
self._sync_id_picker("Feature" if feature_mode else "Face", logical_id)
|
||||
self.set_info(self._with_pick_info(input_info, pick_position))
|
||||
self._update_action_states()
|
||||
self._maybe_start_scdm_probe_for_selection()
|
||||
message = f"已选择Face {logical_id}" if not feature_mode else f"已选择特征来源 Face {logical_id}"
|
||||
self.statusBar().showMessage(self._selection_status(message, pick_position))
|
||||
|
||||
@@ -3383,6 +3514,7 @@ class WindowCoreMixin:
|
||||
if hasattr(self, "_feature_display_label"):
|
||||
feature_type = self._feature_display_label(feature_type)
|
||||
self._update_action_states()
|
||||
self._maybe_start_scdm_probe_for_selection()
|
||||
self.statusBar().showMessage(self._selection_status(f"已选择 {feature_type},来源 Face {logical_id}", pick_position))
|
||||
|
||||
def _sync_cylindrical_edit_inputs(self, info: dict[str, object]) -> None:
|
||||
@@ -3559,6 +3691,7 @@ class WindowCoreMixin:
|
||||
self._sync_id_picker("Edge", edge_id)
|
||||
self.set_info(self._with_pick_info(info, pick_position))
|
||||
self._update_action_states()
|
||||
self._maybe_start_scdm_probe_for_selection()
|
||||
self.statusBar().showMessage(self._selection_status(f"已选择Edge {edge_id}", pick_position))
|
||||
|
||||
def _highlight_faces(self, face_ids=None, part_ids=None) -> None:
|
||||
@@ -3590,7 +3723,7 @@ class WindowCoreMixin:
|
||||
self.highlight_actor = actor
|
||||
self.highlight_signature = signature
|
||||
self.renderer.AddActor(actor)
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def _highlight_edge(self, edge_id: int) -> None:
|
||||
if self.model is None:
|
||||
@@ -3614,7 +3747,7 @@ class WindowCoreMixin:
|
||||
self.edge_highlight_actor = actor
|
||||
self.highlight_signature = signature
|
||||
self.renderer.AddActor(actor)
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def _hover_signature_for_target(self, target: dict[str, object] | None) -> tuple[str, int] | None:
|
||||
if self.model is None or target is None:
|
||||
@@ -3633,7 +3766,7 @@ class WindowCoreMixin:
|
||||
|
||||
self._clear_hover(render=False)
|
||||
if target is None:
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
return
|
||||
|
||||
kind = str(target["kind"])
|
||||
@@ -3652,7 +3785,7 @@ class WindowCoreMixin:
|
||||
face_ids = self._selection_same_domain_face_ids(target_id) or [target_id]
|
||||
self._highlight_hover_faces(face_ids=face_ids)
|
||||
self.hover_signature = signature
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def _highlight_hover_faces(self, face_ids=None, part_ids=None) -> None:
|
||||
if self.model is None:
|
||||
@@ -3729,7 +3862,7 @@ class WindowCoreMixin:
|
||||
removed = True
|
||||
self.hover_signature = None
|
||||
if render and removed and hasattr(self, "render_window"):
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def _clear_highlight(self) -> None:
|
||||
if self.highlight_actor is not None:
|
||||
@@ -3754,7 +3887,7 @@ class WindowCoreMixin:
|
||||
self.edit_preview_actors = []
|
||||
self.edit_preview_actor = None
|
||||
if render and hasattr(self, "render_window"):
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def _add_edit_preview_actor(
|
||||
self,
|
||||
@@ -3806,7 +3939,7 @@ class WindowCoreMixin:
|
||||
)
|
||||
self._add_edit_preview_actor(polydata, color, opacity=0.38, position=position)
|
||||
self._start_edit_preview_pulse()
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def _show_shell_thickness_preview(
|
||||
self,
|
||||
@@ -3840,7 +3973,7 @@ class WindowCoreMixin:
|
||||
)
|
||||
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()
|
||||
self._render_window_safely()
|
||||
|
||||
def _show_cylinder_resize_preview(self, face_id: int, diameter: float) -> None:
|
||||
if self.model is None:
|
||||
@@ -3857,7 +3990,7 @@ class WindowCoreMixin:
|
||||
opacity = 0.28 if role == "fill" else 0.32
|
||||
self._add_edit_preview_actor(preview["polydata"], color, opacity=opacity)
|
||||
self._start_edit_preview_pulse()
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def _show_cylinder_boss_resize_preview(self, face_id: int, diameter: float) -> None:
|
||||
if self.model is None:
|
||||
@@ -3874,7 +4007,7 @@ class WindowCoreMixin:
|
||||
opacity = 0.3 if role == "fill" else 0.34
|
||||
self._add_edit_preview_actor(preview["polydata"], color, opacity=opacity)
|
||||
self._start_edit_preview_pulse()
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def _show_cylinder_boss_height_preview(self, face_id: int, target_height: float) -> None:
|
||||
if self.model is None:
|
||||
@@ -3887,7 +4020,7 @@ class WindowCoreMixin:
|
||||
return
|
||||
self._add_edit_preview_actor(polydata, (0.0, 0.72, 0.78), opacity=0.34)
|
||||
self._start_edit_preview_pulse()
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def _show_cylinder_suppress_preview(self, face_id: int) -> None:
|
||||
if self.model is None:
|
||||
@@ -3901,7 +4034,7 @@ class WindowCoreMixin:
|
||||
for preview in previews:
|
||||
self._add_edit_preview_actor(preview["polydata"], (0.0, 0.86, 0.34), opacity=0.3)
|
||||
self._start_edit_preview_pulse()
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def _show_cylinder_depth_preview(
|
||||
self,
|
||||
@@ -3927,7 +4060,7 @@ class WindowCoreMixin:
|
||||
opacity = 0.3 if role == "fill" else 0.34
|
||||
self._add_edit_preview_actor(preview["polydata"], color, opacity=opacity)
|
||||
self._start_edit_preview_pulse()
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def _show_existing_fillet_resize_preview(self, face_id: int, target_radius: float) -> None:
|
||||
if self.model is None:
|
||||
@@ -3941,7 +4074,7 @@ class WindowCoreMixin:
|
||||
for preview in previews:
|
||||
self._add_edit_preview_actor(preview["polydata"], (0.35, 0.45, 1.0), opacity=0.36)
|
||||
self._start_edit_preview_pulse()
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def _show_edge_fillet_preview(self, edge_id: int, radius: float) -> None:
|
||||
if self.model is None:
|
||||
@@ -3982,7 +4115,7 @@ class WindowCoreMixin:
|
||||
else:
|
||||
self._add_edit_preview_actor(polydata, (0.0, 0.72, 0.78), opacity=0.34)
|
||||
self._start_edit_preview_pulse()
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def _show_edge_tube_preview(
|
||||
self,
|
||||
@@ -4018,7 +4151,7 @@ class WindowCoreMixin:
|
||||
tube.Update()
|
||||
self._add_edit_preview_actor(tube.GetOutput(), color, opacity=0.34)
|
||||
self._start_edit_preview_pulse()
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def _start_edit_preview_pulse(self) -> None:
|
||||
if self.edit_preview_timer is None:
|
||||
@@ -4036,7 +4169,7 @@ class WindowCoreMixin:
|
||||
opacity = self.edit_preview_base_opacity * (0.75 + 0.25 * (math.sin(self.edit_preview_phase) + 1.0) / 2.0)
|
||||
for actor in self.edit_preview_actors:
|
||||
actor.GetProperty().SetOpacity(opacity)
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
|
||||
def clear_diff_preview(self, render: bool = True) -> None:
|
||||
if not hasattr(self, "renderer"):
|
||||
@@ -4046,7 +4179,7 @@ class WindowCoreMixin:
|
||||
self.renderer.RemoveViewProp(actor)
|
||||
self.diff_actors.clear()
|
||||
if render and hasattr(self, "render_window"):
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
self.statusBar().showMessage("已清除差异预览")
|
||||
if hasattr(self, "mode_combo"):
|
||||
self._update_action_states()
|
||||
@@ -4070,7 +4203,7 @@ class WindowCoreMixin:
|
||||
self.diff_actors.append(scalar_bar)
|
||||
for actor in self.diff_actors:
|
||||
self.renderer.AddViewProp(actor)
|
||||
self.render_window.Render()
|
||||
self._render_window_safely()
|
||||
self._update_action_states()
|
||||
if heatmap_stats:
|
||||
record.diff_stats = heatmap_stats
|
||||
|
||||
Reference in New Issue
Block a user