feat: 推进SCDM-first后端接入和大模型编辑优化
This commit is contained in:
+419
-29
@@ -21,6 +21,9 @@ from PySide6.QtWidgets import (
|
||||
from .model import StepModel
|
||||
from .asitus_bridge import run_asitus_hole_recognition
|
||||
from .records import OperationRecord
|
||||
from .scdm_feature_mapper import attach_local_face_ids_to_scdm_cache, map_scdm_raw_features
|
||||
from .scdm_probe import run_scdm_probe
|
||||
from .scdm_schema import write_json
|
||||
from .ui_helpers import * # noqa: F403
|
||||
from .workers import EditWorker, LoadWorker, ScanWorker
|
||||
|
||||
@@ -145,6 +148,12 @@ class WindowCoreMixin:
|
||||
if hasattr(self, "ui_task_requested"):
|
||||
self.ui_task_requested.emit(callback)
|
||||
|
||||
def _reroute_to_ui_thread(self, callback) -> bool:
|
||||
if self._is_ui_thread():
|
||||
return False
|
||||
self._invoke_on_ui_thread(callback)
|
||||
return True
|
||||
|
||||
def eventFilter(self, watched, event):
|
||||
if event.type() == QEvent.Type.ToolTip and self._should_suppress_transient_tooltip(watched):
|
||||
QToolTip.hideText()
|
||||
@@ -194,11 +203,16 @@ class WindowCoreMixin:
|
||||
event.accept()
|
||||
return True
|
||||
elif watched is getattr(self, "relation_formula_input", None):
|
||||
if event.type() == QEvent.Type.FocusIn:
|
||||
if hasattr(self, "_update_relation_formula_completions"):
|
||||
QTimer.singleShot(0, self._update_relation_formula_completions)
|
||||
if event.type() == QEvent.Type.MouseButtonPress:
|
||||
if hasattr(self, "_hide_relation_formula_completion_popup"):
|
||||
self._hide_relation_formula_completion_popup()
|
||||
if hasattr(watched, "setFocus"):
|
||||
watched.setFocus(Qt.FocusReason.MouseFocusReason)
|
||||
if hasattr(self, "_update_relation_formula_completions"):
|
||||
QTimer.singleShot(0, self._update_relation_formula_completions)
|
||||
if event.type() in {QEvent.Type.ShortcutOverride, QEvent.Type.KeyPress} and event.key() == Qt.Key.Key_Tab:
|
||||
if bool(getattr(self, "_relation_formula_tab_completion_accepted", False)):
|
||||
self._relation_formula_tab_completion_accepted = False
|
||||
@@ -940,6 +954,21 @@ class WindowCoreMixin:
|
||||
changed = True
|
||||
except Exception:
|
||||
self.edge_visibility_before_camera_interaction = None
|
||||
if bool(getattr(self, "hide_overlays_during_camera_interaction", False)):
|
||||
overlay_visibility: dict[str, int] = {}
|
||||
for attr_name in ("highlight_actor", "edge_highlight_actor", "pick_marker_actor"):
|
||||
actor = getattr(self, attr_name, None)
|
||||
if actor is None:
|
||||
continue
|
||||
try:
|
||||
visibility = int(actor.GetVisibility())
|
||||
overlay_visibility[attr_name] = visibility
|
||||
if visibility:
|
||||
actor.VisibilityOff()
|
||||
changed = True
|
||||
except Exception:
|
||||
continue
|
||||
self.overlay_visibility_before_camera_interaction = overlay_visibility
|
||||
changed = self._set_render_window_multisamples(
|
||||
int(getattr(self, "interactive_multi_samples", 0))
|
||||
) or changed
|
||||
@@ -955,6 +984,19 @@ class WindowCoreMixin:
|
||||
except Exception:
|
||||
pass
|
||||
self.edge_visibility_before_camera_interaction = None
|
||||
overlay_visibility = getattr(self, "overlay_visibility_before_camera_interaction", {}) or {}
|
||||
if isinstance(overlay_visibility, dict):
|
||||
for attr_name, previous_visibility in overlay_visibility.items():
|
||||
actor = getattr(self, str(attr_name), None)
|
||||
if actor is None:
|
||||
continue
|
||||
try:
|
||||
if int(actor.GetVisibility()) != int(previous_visibility):
|
||||
actor.SetVisibility(int(previous_visibility))
|
||||
changed = True
|
||||
except Exception:
|
||||
continue
|
||||
self.overlay_visibility_before_camera_interaction = {}
|
||||
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
|
||||
@@ -1005,6 +1047,11 @@ class WindowCoreMixin:
|
||||
self.statusBar().showMessage("孔组识别正在后台预热,请稍后再关闭窗口。")
|
||||
event.ignore()
|
||||
return
|
||||
scdm_thread_running = bool(self.scdm_thread is not None and self.scdm_thread.isRunning())
|
||||
if scdm_thread_running:
|
||||
self.statusBar().showMessage("SCDM 可修改参数正在后台识别,请稍后再关闭窗口。")
|
||||
event.ignore()
|
||||
return
|
||||
super().closeEvent(event)
|
||||
|
||||
def _request_thread_quit(self, thread: QThread | None) -> None:
|
||||
@@ -1031,23 +1078,25 @@ class WindowCoreMixin:
|
||||
started = time.perf_counter()
|
||||
stats = new_model.stats()
|
||||
timings["stats"] = time.perf_counter() - started
|
||||
display_deflection = _large_model_display_deflection_for_stats(stats, deflection)
|
||||
result = {
|
||||
"path": path,
|
||||
"model": new_model,
|
||||
"stats": stats,
|
||||
"deflection": deflection,
|
||||
"deflection": display_deflection,
|
||||
"requested_deflection": deflection,
|
||||
"show_internal_edges": show_internal_edges,
|
||||
"timings": timings,
|
||||
}
|
||||
if build_polydata:
|
||||
started = time.perf_counter()
|
||||
face_polydata = new_model.build_face_polydata(deflection=deflection)
|
||||
face_polydata = new_model.build_face_polydata(deflection=display_deflection)
|
||||
result["model_polydata"] = _smooth_surface_polydata(face_polydata)
|
||||
timings["display_faces"] = time.perf_counter() - started
|
||||
if build_edges:
|
||||
started = time.perf_counter()
|
||||
result["edge_polydata"] = new_model.build_edge_polydata(
|
||||
deflection=deflection,
|
||||
deflection=display_deflection,
|
||||
show_same_domain_internal_edges=show_internal_edges,
|
||||
)
|
||||
timings["display_edges"] = time.perf_counter() - started
|
||||
@@ -1077,7 +1126,30 @@ class WindowCoreMixin:
|
||||
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))
|
||||
deflection = float(getattr(self, "preview_load_deflection", 0.35) or 0.35)
|
||||
show_internal_edges = self._show_same_domain_internal_edges()
|
||||
load_step_result = type(self)._load_step_result
|
||||
|
||||
def action(path=new_path, deflection=deflection, show_internal_edges=show_internal_edges, load_step_result=load_step_result):
|
||||
return load_step_result(
|
||||
path,
|
||||
deflection=deflection,
|
||||
show_internal_edges=show_internal_edges,
|
||||
build_edges=False,
|
||||
)
|
||||
|
||||
thread = QThread(self)
|
||||
worker = LoadWorker(action)
|
||||
worker.moveToThread(thread)
|
||||
thread.started.connect(worker.run)
|
||||
worker.finished.connect(self._finish_initial_load, Qt.ConnectionType.QueuedConnection)
|
||||
worker.failed.connect(self._fail_initial_load, Qt.ConnectionType.QueuedConnection)
|
||||
thread.finished.connect(worker.deleteLater)
|
||||
thread.finished.connect(thread.deleteLater)
|
||||
thread.finished.connect(self._forget_load_thread)
|
||||
self.load_thread = thread
|
||||
self.load_worker = worker
|
||||
thread.start()
|
||||
|
||||
@Slot(object)
|
||||
def _run_packaged_initial_load(self, expected_path: Path) -> None:
|
||||
@@ -1169,6 +1241,7 @@ class WindowCoreMixin:
|
||||
stats = result["stats"]
|
||||
self.model = result["model"]
|
||||
self.step_path = new_path
|
||||
self._invalidate_scdm_feature_cache("模型已重新加载,SCDM cache 已失效。")
|
||||
self._clear_history()
|
||||
if hasattr(self, "measure_text"):
|
||||
self.clear_measurement()
|
||||
@@ -1178,6 +1251,12 @@ class WindowCoreMixin:
|
||||
self.path_label.setCursorPosition(0)
|
||||
self._populate_part_tree()
|
||||
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
|
||||
self.hover_after_camera_cooldown_ms = 700 if large_interaction_model else 420
|
||||
self.large_model_edge_overlay_skipped = False
|
||||
self.large_model_hover_disabled = large_interaction_model
|
||||
model_polydata = result.get("model_polydata")
|
||||
edge_polydata = result.get("edge_polydata")
|
||||
edge_deferred = bool(result.get("edge_polydata_deferred"))
|
||||
@@ -1203,7 +1282,11 @@ class WindowCoreMixin:
|
||||
timings["apply_loaded"] = time.perf_counter() - apply_started
|
||||
display_state = result.get("display", "quick preview" if self.load_in_progress else "ready")
|
||||
if edge_deferred:
|
||||
display_state = f"{display_state}; edge display pending"
|
||||
if large_interaction_model:
|
||||
self.large_model_edge_overlay_skipped = True
|
||||
display_state = f"{display_state}; edge display skipped for large model"
|
||||
else:
|
||||
display_state = f"{display_state}; edge display pending"
|
||||
info_payload = {
|
||||
"file": str(self.step_path),
|
||||
"parts": stats.parts,
|
||||
@@ -1218,13 +1301,228 @@ class WindowCoreMixin:
|
||||
info_payload["load_performance"] = timing_text
|
||||
self.set_info(info_payload)
|
||||
self._update_action_states()
|
||||
if edge_deferred:
|
||||
if edge_deferred and not large_interaction_model:
|
||||
QTimer.singleShot(80, self._rebuild_deferred_edge_display)
|
||||
QTimer.singleShot(160, self._start_asitus_hole_recognition_preload)
|
||||
elif edge_deferred and large_interaction_model:
|
||||
self.statusBar().showMessage(
|
||||
f"Loaded {self.step_path.name}; 大模型已跳过全量边线补绘,旋转会更流畅,切到 Edge 选择时再按需生成。"
|
||||
)
|
||||
pending_scdm_reload = isinstance(getattr(self, "pending_scdm_edit_reload", None), dict)
|
||||
if large_interaction_model and not pending_scdm_reload:
|
||||
self._defer_large_model_recognition_preloads()
|
||||
else:
|
||||
QTimer.singleShot(160, self._start_asitus_hole_recognition_preload)
|
||||
QTimer.singleShot(240, lambda: self._start_scdm_probe_preload(force=pending_scdm_reload))
|
||||
|
||||
def _start_asitus_hole_recognition_preload(self) -> None:
|
||||
def _large_model_interaction_mode(self, stats: object | None = None) -> bool:
|
||||
if stats is not None:
|
||||
try:
|
||||
return int(getattr(stats, "faces", 0) or 0) > 1000 or int(getattr(stats, "edges", 0) or 0) > 2500
|
||||
except Exception:
|
||||
return False
|
||||
if self.model is None:
|
||||
return False
|
||||
try:
|
||||
return len(getattr(self.model, "faces", ()) or ()) > 1000 or len(getattr(self.model, "edges", ()) or ()) > 2500
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _defer_large_model_recognition_preloads(self) -> None:
|
||||
self.scdm_feature_cache_state = "deferred"
|
||||
self.scdm_feature_cache_message = (
|
||||
"大模型已延后 SCDM/Analysis Situs 全量识别,优先保证查看、旋转和点选流畅;"
|
||||
"本地 OCCT 快路径仍可直接用于已稳定验证的参数。"
|
||||
)
|
||||
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 _invalidate_scdm_feature_cache(self, message: str = "") -> None:
|
||||
self.scdm_feature_cache = None
|
||||
self.scdm_feature_cache_state = "stale"
|
||||
self.scdm_feature_cache_message = message
|
||||
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()
|
||||
|
||||
def _start_scdm_probe_preload(self, *, force: bool = False) -> None:
|
||||
if self.model is None or self.step_path is None:
|
||||
return
|
||||
if not force and self._large_model_interaction_mode():
|
||||
self._defer_large_model_recognition_preloads()
|
||||
return
|
||||
if self.scdm_thread is not None and self.scdm_thread.isRunning():
|
||||
QTimer.singleShot(500, lambda: self._start_scdm_probe_preload(force=force))
|
||||
return
|
||||
step_path = Path(self.step_path)
|
||||
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 可修改参数。"
|
||||
self.statusBar().showMessage("正在后台识别 SCDM 可修改参数,可继续旋转查看模型。")
|
||||
if hasattr(self, "_update_current_capability_panel"):
|
||||
self._update_current_capability_panel()
|
||||
|
||||
def action(path=step_path, model=model):
|
||||
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 {
|
||||
"ok": False,
|
||||
"reason": str(probe.get("reason") if isinstance(probe, dict) else "probe-failed"),
|
||||
"message": str(probe.get("message") if isinstance(probe, dict) else "SCDM probe failed."),
|
||||
"probe": probe,
|
||||
}
|
||||
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,
|
||||
)
|
||||
cache_path = Path(str(probe.get("raw_features_path") or path)).with_name("scdm_feature_cache.json")
|
||||
write_json(cache_path, cache)
|
||||
return {
|
||||
"ok": True,
|
||||
"reason": "ok",
|
||||
"cache": cache,
|
||||
"cache_path": str(cache_path),
|
||||
"probe": probe,
|
||||
}
|
||||
|
||||
thread = QThread(self)
|
||||
worker = ScanWorker(action)
|
||||
worker.moveToThread(thread)
|
||||
thread.started.connect(worker.run)
|
||||
worker.finished.connect(self._finish_scdm_probe_preload, Qt.ConnectionType.QueuedConnection)
|
||||
worker.failed.connect(self._fail_scdm_probe_preload, Qt.ConnectionType.QueuedConnection)
|
||||
thread.finished.connect(worker.deleteLater)
|
||||
thread.finished.connect(thread.deleteLater)
|
||||
thread.finished.connect(self._forget_scdm_thread)
|
||||
self.scdm_thread = thread
|
||||
self.scdm_worker = worker
|
||||
try:
|
||||
thread.start(QThread.Priority.LowPriority)
|
||||
except TypeError:
|
||||
thread.start()
|
||||
|
||||
def _scdm_local_face_signatures(self) -> list[dict[str, object]]:
|
||||
if self.model is None:
|
||||
return []
|
||||
builder = getattr(self.model, "scdm_local_face_signatures", None)
|
||||
if callable(builder):
|
||||
try:
|
||||
return [dict(item) for item in builder() if isinstance(item, dict)]
|
||||
except Exception:
|
||||
return []
|
||||
return []
|
||||
|
||||
@Slot(object)
|
||||
def _finish_scdm_probe_preload(self, result: object) -> None:
|
||||
if self._reroute_to_ui_thread(lambda result=result: self._finish_scdm_probe_preload(result)):
|
||||
return
|
||||
try:
|
||||
context = dict(self.pending_scdm_context or {})
|
||||
if self.model is None or id(self.model) != context.get("model_id"):
|
||||
return
|
||||
if self.step_path is None or Path(context.get("path", "")) != Path(self.step_path):
|
||||
return
|
||||
if not isinstance(result, dict) or result.get("ok") is not True:
|
||||
reason = str(result.get("reason") if isinstance(result, dict) else "probe-failed")
|
||||
message = str(result.get("message") if isinstance(result, dict) else "SCDM probe failed.")
|
||||
if isinstance(result, dict) and isinstance(result.get("backend"), dict):
|
||||
self.scdm_backend_status = dict(result["backend"])
|
||||
self.scdm_feature_cache = None
|
||||
self.scdm_feature_cache_state = "failed"
|
||||
self.scdm_feature_cache_message = message
|
||||
self.statusBar().showMessage(f"SCDM 可修改参数识别未启用:{reason}")
|
||||
if hasattr(self, "_update_current_capability_panel"):
|
||||
self._update_current_capability_panel()
|
||||
if hasattr(self, "maybe_prompt_missing_scdm_backend"):
|
||||
self.maybe_prompt_missing_scdm_backend(reason=reason, message=message)
|
||||
if hasattr(self, "_finish_pending_scdm_edit_reload"):
|
||||
self._finish_pending_scdm_edit_reload(cache_ready=False, message=f"SCDM cache 刷新失败:{reason}")
|
||||
return
|
||||
cache = result.get("cache")
|
||||
if not isinstance(cache, dict):
|
||||
if isinstance(result.get("backend"), dict):
|
||||
self.scdm_backend_status = dict(result["backend"])
|
||||
self.scdm_feature_cache_state = "failed"
|
||||
self.scdm_feature_cache_message = "SCDM cache 格式无效。"
|
||||
self.statusBar().showMessage("SCDM 可修改参数识别失败:cache 格式无效。")
|
||||
if hasattr(self, "_update_current_capability_panel"):
|
||||
self._update_current_capability_panel()
|
||||
if hasattr(self, "_finish_pending_scdm_edit_reload"):
|
||||
self._finish_pending_scdm_edit_reload(cache_ready=False, message="SCDM cache 刷新失败:格式无效")
|
||||
return
|
||||
if isinstance(result.get("backend"), dict):
|
||||
self.scdm_backend_status = dict(result["backend"])
|
||||
self.scdm_feature_cache = dict(cache)
|
||||
self.scdm_feature_cache_state = "ready"
|
||||
self.scdm_feature_cache_message = "SCDM 可修改参数识别完成。"
|
||||
self.scdm_feature_cache_path = str(result.get("cache_path") or "")
|
||||
try:
|
||||
self.model.scdm_feature_cache = dict(cache)
|
||||
except Exception:
|
||||
pass
|
||||
objects = cache.get("objects")
|
||||
count = len(objects) if isinstance(objects, list) else 0
|
||||
self.statusBar().showMessage(f"SCDM 可修改参数识别完成:{count} 个产品化对象。")
|
||||
if hasattr(self, "_update_current_capability_panel"):
|
||||
self._update_current_capability_panel()
|
||||
if hasattr(self, "_refresh_property_editor"):
|
||||
self._refresh_property_editor()
|
||||
if hasattr(self, "_finish_pending_scdm_edit_reload"):
|
||||
self._finish_pending_scdm_edit_reload(cache_ready=True)
|
||||
finally:
|
||||
self._request_thread_quit(self.scdm_thread)
|
||||
|
||||
@Slot(str)
|
||||
def _fail_scdm_probe_preload(self, message: str) -> None:
|
||||
if self._reroute_to_ui_thread(lambda message=message: self._fail_scdm_probe_preload(message)):
|
||||
return
|
||||
try:
|
||||
self.scdm_feature_cache = None
|
||||
self.scdm_feature_cache_state = "failed"
|
||||
self.scdm_feature_cache_message = message
|
||||
self.statusBar().showMessage("SCDM 可修改参数识别失败,不影响当前模型查看。")
|
||||
if hasattr(self, "_update_current_capability_panel"):
|
||||
self._update_current_capability_panel()
|
||||
if hasattr(self, "_finish_pending_scdm_edit_reload"):
|
||||
self._finish_pending_scdm_edit_reload(cache_ready=False, message=f"SCDM cache 刷新失败:{message}")
|
||||
finally:
|
||||
self._request_thread_quit(self.scdm_thread)
|
||||
|
||||
@Slot()
|
||||
def _forget_scdm_thread(self) -> None:
|
||||
self.scdm_thread = None
|
||||
self.scdm_worker = None
|
||||
self.pending_scdm_context = None
|
||||
|
||||
def _start_asitus_hole_recognition_preload(self, *, force: bool = False) -> None:
|
||||
if self.model is None or self.step_path is None:
|
||||
return
|
||||
if not force and self._large_model_interaction_mode():
|
||||
return
|
||||
if self.asitus_thread is not None and self.asitus_thread.isRunning():
|
||||
return
|
||||
if not hasattr(self.model, "begin_asitus_hole_region_load"):
|
||||
@@ -1259,6 +1557,8 @@ class WindowCoreMixin:
|
||||
|
||||
@Slot(object)
|
||||
def _finish_asitus_hole_recognition(self, result: object) -> None:
|
||||
if self._reroute_to_ui_thread(lambda result=result: self._finish_asitus_hole_recognition(result)):
|
||||
return
|
||||
try:
|
||||
context = dict(self.pending_asitus_context or {})
|
||||
if self.model is None or id(self.model) != context.get("model_id"):
|
||||
@@ -1273,6 +1573,8 @@ class WindowCoreMixin:
|
||||
|
||||
@Slot(str)
|
||||
def _fail_asitus_hole_recognition(self, message: str) -> None:
|
||||
if self._reroute_to_ui_thread(lambda message=message: self._fail_asitus_hole_recognition(message)):
|
||||
return
|
||||
try:
|
||||
context = dict(self.pending_asitus_context or {})
|
||||
if self.model is not None and id(self.model) == context.get("model_id"):
|
||||
@@ -1302,6 +1604,8 @@ class WindowCoreMixin:
|
||||
|
||||
@Slot(object)
|
||||
def _finish_initial_load(self, result: object) -> None:
|
||||
if self._reroute_to_ui_thread(lambda result=result: self._finish_initial_load(result)):
|
||||
return
|
||||
try:
|
||||
if not isinstance(result, dict):
|
||||
raise RuntimeError("Load task returned an unexpected result.")
|
||||
@@ -1328,15 +1632,21 @@ class WindowCoreMixin:
|
||||
|
||||
@Slot(str)
|
||||
def _fail_initial_load(self, message: str) -> None:
|
||||
if self._reroute_to_ui_thread(lambda message=message: self._fail_initial_load(message)):
|
||||
return
|
||||
self._end_load_task()
|
||||
QMessageBox.critical(self, "Load failed", message)
|
||||
self.statusBar().showMessage("STEP load failed; current model was left unchanged.")
|
||||
if hasattr(self, "_fail_pending_scdm_edit_reload"):
|
||||
self._fail_pending_scdm_edit_reload(message)
|
||||
|
||||
def _start_load_refine(self, initial_result: dict[str, object]) -> None:
|
||||
self.statusBar().showMessage(f"Loaded {self.step_path.name}")
|
||||
|
||||
@Slot(object)
|
||||
def _finish_load_refine(self, result: object) -> None:
|
||||
if self._reroute_to_ui_thread(lambda result=result: self._finish_load_refine(result)):
|
||||
return
|
||||
try:
|
||||
if (
|
||||
isinstance(result, dict)
|
||||
@@ -1368,6 +1678,8 @@ class WindowCoreMixin:
|
||||
|
||||
@Slot(str)
|
||||
def _fail_load_refine(self, message: str) -> None:
|
||||
if self._reroute_to_ui_thread(lambda message=message: self._fail_load_refine(message)):
|
||||
return
|
||||
self._end_load_task()
|
||||
self.statusBar().showMessage(f"Quick preview is available; display refinement failed: {message}")
|
||||
|
||||
@@ -1651,13 +1963,20 @@ class WindowCoreMixin:
|
||||
return "实体"
|
||||
return f"面 {info.get('faces', 0)} 个 | 边 {info.get('edges', 0)} 条"
|
||||
|
||||
def _rebuild_scene(self, reset_camera: bool = False) -> None:
|
||||
def _rebuild_scene(self, reset_camera: bool = False, build_edges: bool | None = None) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
model_polydata = self.model.build_face_polydata()
|
||||
edge_polydata = self.model.build_edge_polydata(
|
||||
show_same_domain_internal_edges=self._show_same_domain_internal_edges()
|
||||
)
|
||||
large_model = len(getattr(self.model, "faces", ()) or ()) > 1000 or len(getattr(self.model, "edges", ()) or ()) > 2500
|
||||
should_build_edges = (not large_model) if build_edges is None else bool(build_edges)
|
||||
if should_build_edges:
|
||||
edge_polydata = self.model.build_edge_polydata(
|
||||
show_same_domain_internal_edges=self._show_same_domain_internal_edges()
|
||||
)
|
||||
self.large_model_edge_overlay_skipped = False
|
||||
else:
|
||||
edge_polydata = _empty_edge_polydata()
|
||||
self.large_model_edge_overlay_skipped = large_model
|
||||
self._rebuild_scene_from_polydata(model_polydata, edge_polydata, reset_camera=reset_camera)
|
||||
|
||||
def _show_same_domain_internal_edges(self) -> bool:
|
||||
@@ -1669,7 +1988,7 @@ class WindowCoreMixin:
|
||||
if self.scene_isolated and self.selected_kind is not None:
|
||||
self.isolate_selected()
|
||||
else:
|
||||
self._rebuild_scene(reset_camera=False)
|
||||
self._rebuild_scene(reset_camera=False, build_edges=True)
|
||||
self._refresh_selection_highlight()
|
||||
state = "显示" if checked else "隐藏"
|
||||
self.statusBar().showMessage(f"已{state}同域内部拓扑边")
|
||||
@@ -1892,20 +2211,67 @@ class WindowCoreMixin:
|
||||
def _rebuild_deferred_edge_display(self) -> None:
|
||||
if self.model is None or self.operation_in_progress or self.load_in_progress:
|
||||
return
|
||||
if self.load_refine_thread is not None and self.load_refine_thread.isRunning():
|
||||
return
|
||||
started = time.perf_counter()
|
||||
self.statusBar().showMessage("模型已显示,正在补充边线...")
|
||||
QApplication.processEvents()
|
||||
try:
|
||||
edge_polydata = self.model.build_edge_polydata(
|
||||
deflection=float(getattr(self, "preview_load_deflection", 0.35) or 0.35),
|
||||
show_same_domain_internal_edges=self._show_same_domain_internal_edges(),
|
||||
|
||||
model = self.model
|
||||
path = Path(self.step_path) if self.step_path is not None else None
|
||||
deflection = float(getattr(self, "preview_load_deflection", 0.35) or 0.35)
|
||||
show_internal_edges = self._show_same_domain_internal_edges()
|
||||
context = {"path": path, "model_id": id(model), "started": started}
|
||||
|
||||
def action(model=model, context=context, deflection=deflection, show_internal_edges=show_internal_edges):
|
||||
edge_polydata = model.build_edge_polydata(
|
||||
deflection=deflection,
|
||||
show_same_domain_internal_edges=show_internal_edges,
|
||||
)
|
||||
self._install_edge_polydata(edge_polydata, render=True)
|
||||
except Exception as exc:
|
||||
self.statusBar().showMessage(f"模型已显示;边线补充失败:{exc}")
|
||||
return {**context, "edge_polydata": edge_polydata, "elapsed": time.perf_counter() - started}
|
||||
|
||||
thread = QThread(self)
|
||||
worker = LoadWorker(action)
|
||||
worker.moveToThread(thread)
|
||||
thread.started.connect(worker.run)
|
||||
worker.finished.connect(self._finish_deferred_edge_display, Qt.ConnectionType.QueuedConnection)
|
||||
worker.failed.connect(self._fail_deferred_edge_display, Qt.ConnectionType.QueuedConnection)
|
||||
thread.finished.connect(worker.deleteLater)
|
||||
thread.finished.connect(thread.deleteLater)
|
||||
thread.finished.connect(self._forget_load_refine_thread)
|
||||
self.load_refine_thread = thread
|
||||
self.load_refine_worker = worker
|
||||
try:
|
||||
thread.start(QThread.Priority.LowPriority)
|
||||
except TypeError:
|
||||
thread.start()
|
||||
|
||||
@Slot(object)
|
||||
def _finish_deferred_edge_display(self, result: object) -> None:
|
||||
if self._reroute_to_ui_thread(lambda result=result: self._finish_deferred_edge_display(result)):
|
||||
return
|
||||
elapsed = time.perf_counter() - started
|
||||
self.statusBar().showMessage(f"模型边线已补充,用时 {elapsed:.1f}s")
|
||||
try:
|
||||
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
|
||||
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")
|
||||
finally:
|
||||
self._request_thread_quit(self.load_refine_thread)
|
||||
|
||||
@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)):
|
||||
return
|
||||
try:
|
||||
self.statusBar().showMessage(f"模型已显示;边线补充失败:{message}")
|
||||
finally:
|
||||
self._request_thread_quit(self.load_refine_thread)
|
||||
|
||||
def _remember_overlay_cache_item(self, cache: dict, key: object, value: object) -> object:
|
||||
if len(cache) >= self.overlay_cache_limit:
|
||||
@@ -2054,6 +2420,8 @@ class WindowCoreMixin:
|
||||
|
||||
def _on_mode_changed(self, mode: str) -> None:
|
||||
self._clear_hover(render=True)
|
||||
if str(mode) == "Edge" and bool(getattr(self, "large_model_edge_overlay_skipped", False)):
|
||||
QTimer.singleShot(0, self._rebuild_deferred_edge_display)
|
||||
if hasattr(self, "_update_id_select_title"):
|
||||
self._update_id_select_title(mode)
|
||||
|
||||
@@ -2082,6 +2450,7 @@ class WindowCoreMixin:
|
||||
self.left_button_press_position = (int(x), int(y))
|
||||
self.left_button_dragged = False
|
||||
self.left_button_press_camera_state = self._camera_state_signature()
|
||||
self.left_button_press_target_unknown = False
|
||||
self.left_button_press_target = self._selection_target_at_position(x, y)
|
||||
self.pending_hover_position = None
|
||||
self.last_hover_pick_position = None
|
||||
@@ -2107,17 +2476,21 @@ class WindowCoreMixin:
|
||||
camera_changed = self._left_button_camera_changed()
|
||||
self.pointer_button_down = False
|
||||
press_target = getattr(self, "left_button_press_target", None)
|
||||
press_target_unknown = bool(getattr(self, "left_button_press_target_unknown", False))
|
||||
self.left_button_press_position = None
|
||||
self.left_button_dragged = False
|
||||
self.left_button_press_camera_state = None
|
||||
self.left_button_press_target = None
|
||||
self.left_button_press_target_unknown = False
|
||||
self.pending_hover_position = None
|
||||
self.last_hover_pick_position = None
|
||||
if getattr(self, "camera_interaction_active", False):
|
||||
self._end_camera_interaction()
|
||||
if was_dragged or camera_changed or self._selection_target_signature(press_target) is None:
|
||||
if was_dragged or camera_changed:
|
||||
return
|
||||
self._handle_left_click(x, y, required_press_target=press_target)
|
||||
if not press_target_unknown and self._selection_target_signature(press_target) is None:
|
||||
return
|
||||
self._handle_left_click(x, y, required_press_target=None if press_target_unknown else press_target)
|
||||
|
||||
def _camera_state_signature(self) -> tuple[float, ...] | None:
|
||||
renderer = getattr(self, "renderer", None)
|
||||
@@ -2206,7 +2579,10 @@ class WindowCoreMixin:
|
||||
if self.model is None:
|
||||
return
|
||||
mode = self._current_selection_mode()
|
||||
target = self._pick_selection_target(mode, x, y)
|
||||
if required_press_target is not None and not bool(self._large_model_interaction_mode()):
|
||||
target = self._pick_selection_target(mode, x, y)
|
||||
else:
|
||||
target = required_press_target if required_press_target is not None else self._pick_selection_target(mode, x, y)
|
||||
if required_press_target is not None:
|
||||
press_signature = self._selection_target_signature(required_press_target)
|
||||
release_signature = self._selection_target_signature(target)
|
||||
@@ -2397,6 +2773,7 @@ class WindowCoreMixin:
|
||||
if (
|
||||
getattr(self, "pointer_button_down", False)
|
||||
or getattr(self, "camera_interaction_active", False)
|
||||
or getattr(self, "large_model_hover_disabled", False)
|
||||
or self._hover_suppressed_after_camera()
|
||||
):
|
||||
return
|
||||
@@ -2425,6 +2802,7 @@ class WindowCoreMixin:
|
||||
or self.model is None
|
||||
or self.model_actor is None
|
||||
or self.pending_hover_position is None
|
||||
or getattr(self, "large_model_hover_disabled", False)
|
||||
or self._hover_suppressed_after_camera()
|
||||
):
|
||||
self._clear_hover(render=True)
|
||||
@@ -2863,10 +3241,11 @@ class WindowCoreMixin:
|
||||
info.setdefault("feature_mode", "当前是几何候选判断,不等同于 CAD 历史特征")
|
||||
highlight_face_ids = _int_values(info.get("feature_highlight_face_ids")) or [face_id]
|
||||
else:
|
||||
highlight_face_ids = self._selection_same_domain_face_ids(face_id) or [face_id]
|
||||
highlight_face_ids = self._selection_same_domain_face_ids(face_id, info) or [face_id]
|
||||
selection_fields = self._selection_identity_fields(
|
||||
face_id,
|
||||
"特征来源 Face" if feature_mode else "Face",
|
||||
region_face_ids=highlight_face_ids,
|
||||
)
|
||||
logical_id = int(selection_fields["selection_display_id"])
|
||||
input_info = dict(info)
|
||||
@@ -2882,9 +3261,16 @@ class WindowCoreMixin:
|
||||
message = f"已选择Face {logical_id}" if not feature_mode else f"已选择特征来源 Face {logical_id}"
|
||||
self.statusBar().showMessage(self._selection_status(message, pick_position))
|
||||
|
||||
def _selection_same_domain_face_ids(self, face_id: int) -> list[int]:
|
||||
def _selection_same_domain_face_ids(self, face_id: int, fallback_info: dict[str, object] | None = None) -> list[int]:
|
||||
if self.model is None:
|
||||
return [face_id]
|
||||
fallback_ids = _int_values((fallback_info or {}).get("feature_highlight_face_ids")) or _int_values(
|
||||
(fallback_info or {}).get("same_domain_face_ids")
|
||||
)
|
||||
if fallback_ids:
|
||||
return sorted({int(item) for item in fallback_ids if 0 <= int(item) < len(self.model.faces)}) or [face_id]
|
||||
if bool(getattr(self, "_large_model_interaction_mode", lambda: False)()):
|
||||
return [face_id]
|
||||
try:
|
||||
face_ids = self.model.face_region_ids(face_id)
|
||||
except Exception:
|
||||
@@ -2904,7 +3290,11 @@ class WindowCoreMixin:
|
||||
return
|
||||
info = self._feature_context_info(face_id)
|
||||
info["kind"] = "feature"
|
||||
selection_fields = self._selection_identity_fields(face_id, "特征来源 Face")
|
||||
selection_fields = self._selection_identity_fields(
|
||||
face_id,
|
||||
"特征来源 Face",
|
||||
region_face_ids=_int_values(info.get("feature_highlight_face_ids")) or [face_id],
|
||||
)
|
||||
logical_id = int(selection_fields["selection_display_id"])
|
||||
info.update(selection_fields)
|
||||
self._reset_selection(clear_highlight=False, clear_info=False)
|
||||
|
||||
Reference in New Issue
Block a user