feat: 完善 STEP 一级参数化编辑识别与关系式建模

This commit is contained in:
2026-08-14 18:42:39 +08:00
parent 70b59c1de6
commit a633b5a338
26 changed files with 4676 additions and 77 deletions
+197 -4
View File
@@ -19,6 +19,7 @@ from PySide6.QtWidgets import (
)
from .model import StepModel
from .asitus_bridge import run_asitus_hole_recognition
from .records import OperationRecord
from .ui_helpers import * # noqa: F403
from .workers import EditWorker, LoadWorker, ScanWorker
@@ -178,6 +179,10 @@ class WindowCoreMixin:
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)
elif watched is getattr(self, "relation_formula_input", None):
if event.type() == QEvent.Type.KeyPress and event.key() == Qt.Key.Key_Tab:
if hasattr(self, "_accept_relation_formula_completion") and self._accept_relation_formula_completion():
return True
return super().eventFilter(watched, event)
def _should_suppress_transient_tooltip(self, watched) -> bool:
@@ -959,6 +964,11 @@ class WindowCoreMixin:
self.statusBar().showMessage("扫描正在进行,请等待扫描完成后再关闭窗口。")
event.ignore()
return
asitus_thread_running = bool(self.asitus_thread is not None and self.asitus_thread.isRunning())
if asitus_thread_running:
self.statusBar().showMessage("孔组识别正在后台预热,请稍后再关闭窗口。")
event.ignore()
return
super().closeEvent(event)
def _request_thread_quit(self, thread: QThread | None) -> None:
@@ -1174,6 +1184,85 @@ class WindowCoreMixin:
self._update_action_states()
if edge_deferred:
QTimer.singleShot(80, self._rebuild_deferred_edge_display)
QTimer.singleShot(160, self._start_asitus_hole_recognition_preload)
def _start_asitus_hole_recognition_preload(self) -> None:
if self.model is None or self.step_path is None:
return
if self.asitus_thread is not None and self.asitus_thread.isRunning():
return
if not hasattr(self.model, "begin_asitus_hole_region_load"):
return
if not self.model.begin_asitus_hole_region_load():
return
step_path = Path(self.step_path)
context = {
"path": step_path,
"model_id": id(self.model),
}
self.pending_asitus_context = dict(context)
def action(path=step_path):
return run_asitus_hole_recognition(path)
thread = QThread(self)
worker = ScanWorker(action)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.finished.connect(self._finish_asitus_hole_recognition, Qt.ConnectionType.QueuedConnection)
worker.failed.connect(self._fail_asitus_hole_recognition, Qt.ConnectionType.QueuedConnection)
thread.finished.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
thread.finished.connect(self._forget_asitus_thread)
self.asitus_thread = thread
self.asitus_worker = worker
try:
thread.start(QThread.Priority.LowPriority)
except TypeError:
thread.start()
@Slot(object)
def _finish_asitus_hole_recognition(self, result: object) -> None:
try:
context = dict(self.pending_asitus_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
mapped_groups = self.model.install_asitus_hole_recognition_result(result)
if mapped_groups:
self._refresh_selection_after_asitus_holes(mapped_groups)
finally:
self._request_thread_quit(self.asitus_thread)
@Slot(str)
def _fail_asitus_hole_recognition(self, message: str) -> None:
try:
context = dict(self.pending_asitus_context or {})
if self.model is not None and id(self.model) == context.get("model_id"):
self.model.fail_asitus_hole_region_load(message)
finally:
self._request_thread_quit(self.asitus_thread)
def _refresh_selection_after_asitus_holes(self, mapped_groups: list[tuple[int, ...]]) -> None:
if self.model is None or self.selected_face_id is None:
return
selected_id = int(self.selected_face_id)
if not any(selected_id in group for group in mapped_groups):
return
if getattr(self, "operation_in_progress", False):
return
pick_position = self.selected_pick_position
if self.selected_kind == "feature":
self.select_feature(selected_id, pick_position=pick_position)
elif self.selected_kind == "face":
self.select_face(selected_id, pick_position=pick_position)
@Slot()
def _forget_asitus_thread(self) -> None:
self.asitus_thread = None
self.asitus_worker = None
self.pending_asitus_context = None
@Slot(object)
def _finish_initial_load(self, result: object) -> None:
@@ -1567,6 +1656,12 @@ class WindowCoreMixin:
face_ids = _int_values(info.get("feature_highlight_face_ids")) or [self.selected_face_id]
edge_ids = _int_values(info.get("feature_boundary_edge_ids"))
label = f"特征 Face {self.selected_face_id}"
elif self.selected_kind == "multi_feature":
info = self._multi_hole_selection_info() if hasattr(self, "_multi_hole_selection_info") else {}
face_ids = _int_values(info.get("feature_highlight_face_ids"))
if not face_ids:
face_ids = _int_values(getattr(self, "multi_selected_feature_face_ids", []))
label = f"多选孔 {len(getattr(self, 'multi_selected_hole_entries', []) or [])}"
elif self.selected_kind == "face" and self.selected_face_id is not None:
face_ids = [self.selected_face_id]
edge_ids = self.model.face_boundary_edge_ids(self.selected_face_id)
@@ -1639,6 +1734,13 @@ class WindowCoreMixin:
face_polydata = self._cached_face_overlay_polydata(face_ids=face_ids, smooth=False)
if edge_ids:
edge_polydata = self.model.build_edge_polydata(edge_ids=edge_ids)
elif self.selected_kind == "multi_feature":
info = self._multi_hole_selection_info() if hasattr(self, "_multi_hole_selection_info") else {}
face_ids = _int_values(info.get("feature_highlight_face_ids"))
if not face_ids:
face_ids = _int_values(getattr(self, "multi_selected_feature_face_ids", []))
if face_ids:
face_polydata = self._cached_face_overlay_polydata(face_ids=face_ids, smooth=False)
elif self.selected_kind == "face" and self.selected_face_id is not None:
face_ids = self._selection_same_domain_face_ids(self.selected_face_id) or [self.selected_face_id]
face_polydata = self._cached_face_overlay_polydata(face_ids=face_ids, smooth=False)
@@ -1675,6 +1777,13 @@ class WindowCoreMixin:
info = {}
face_ids = _int_values(info.get("feature_highlight_face_ids")) or [self.selected_face_id]
self._highlight_faces(face_ids=face_ids)
elif self.selected_kind == "multi_feature":
info = self._multi_hole_selection_info() if hasattr(self, "_multi_hole_selection_info") else {}
face_ids = _int_values(info.get("feature_highlight_face_ids"))
if not face_ids:
face_ids = _int_values(getattr(self, "multi_selected_feature_face_ids", []))
if face_ids:
self._highlight_faces(face_ids=face_ids)
elif self.selected_kind == "face" and self.selected_face_id is not None:
face_ids = self._selection_same_domain_face_ids(self.selected_face_id) or [self.selected_face_id]
self._highlight_faces(face_ids=face_ids)
@@ -2077,8 +2186,94 @@ class WindowCoreMixin:
return
self._clear_hover(render=False)
if self._is_multi_feature_toggle_request(target):
self._toggle_multi_feature_selection(target, pick_position=target.get("pick_position"))
return
self._select_pick_target(target)
def _is_multi_feature_toggle_request(self, target: dict[str, object]) -> bool:
if str(target.get("kind") or "") not in {"feature", "face"}:
return False
try:
return bool(QApplication.keyboardModifiers() & Qt.KeyboardModifier.ControlModifier)
except Exception:
return False
def _toggle_multi_feature_selection(
self,
target: dict[str, object],
*,
pick_position: tuple[float, float, float] | None = None,
) -> None:
if self.model is None:
return
face_id = _safe_int_or_none(target.get("target_id"))
if face_id is None:
return
entry = self._hole_multi_select_entry(face_id) if hasattr(self, "_hole_multi_select_entry") else None
if entry is None:
self.statusBar().showMessage("多选第一版只支持完整圆柱孔;请按 Ctrl 选择孔壁。")
return
entries = [dict(item) for item in (getattr(self, "multi_selected_hole_entries", []) or [])]
if not entries and self.selected_kind == "feature" and self.selected_face_id is not None:
current_entry = self._hole_multi_select_entry(int(self.selected_face_id))
if current_entry is not None:
entries.append(dict(current_entry))
logical_id = int(entry.get("logical_id", face_id))
existing_index = next(
(
index
for index, item in enumerate(entries)
if int(item.get("logical_id", -1)) == logical_id
),
None,
)
if existing_index is None:
entries.append(dict(entry))
else:
entries.pop(existing_index)
if len(entries) < 2:
if entries:
self.select_feature(int(entries[0]["face_id"]), pick_position=pick_position)
else:
self._reset_selection()
return
part_ids = {
int(item["part_id"])
for item in entries
if item.get("part_id") not in {None, ""}
}
solid_ids = {
int(item["solid_id"])
for item in entries
if item.get("solid_id") not in {None, ""}
}
self._reset_selection(clear_highlight=False, clear_info=False)
self.multi_selected_hole_entries = entries
self.multi_selected_feature_face_ids = [int(item["face_id"]) for item in entries]
self.multi_selection_active = True
self.selected_kind = "multi_feature"
self.selected_face_id = int(entries[-1]["face_id"])
self.selected_edge_id = None
self.selected_part_id = next(iter(part_ids)) if len(part_ids) == 1 else None
self.selected_solid_id = next(iter(solid_ids)) if len(solid_ids) == 1 else None
self.selected_pick_position = pick_position
info = self._multi_hole_selection_info()
if hasattr(self, "_set_selection_mode"):
self._set_selection_mode("Feature")
if hasattr(self, "_sync_id_picker"):
self._sync_id_picker("Feature", int(entries[-1].get("logical_id", entries[-1]["face_id"])))
self._highlight_faces(face_ids=_int_values(info.get("feature_highlight_face_ids")) or self.multi_selected_feature_face_ids)
self._show_pick_marker(pick_position)
self.set_info(self._with_pick_info(info, pick_position))
ids_text = ", ".join(str(item.get("logical_id")) for item in entries if item.get("logical_id") is not None)
self.statusBar().showMessage(f"已多选 {len(entries)} 个孔:{ids_text}。再次 Ctrl 点击可移除。")
def on_pointer_button_press(self, _obj, _event) -> None:
if not self._is_ui_thread():
self._invoke_on_ui_thread(self._handle_pointer_button_press)
@@ -2642,8 +2837,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()
raw_note = f"(当前拓扑 Face {face_id}" if logical_id != face_id else ""
message = f"已选择Face {logical_id}{raw_note}" if not feature_mode else f"已选择特征来源 Face {logical_id}{raw_note}"
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]:
@@ -2687,8 +2881,7 @@ class WindowCoreMixin:
if hasattr(self, "_feature_display_label"):
feature_type = self._feature_display_label(feature_type)
self._update_action_states()
raw_note = f"(当前拓扑 Face {face_id}" if logical_id != face_id else ""
self.statusBar().showMessage(self._selection_status(f"已选择 {feature_type},来源 Face {logical_id}{raw_note}", pick_position))
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:
fillet_radius_suggestion: str | None = None