feat: 完善参数化编辑语义和槽孔中心距

This commit is contained in:
2026-07-30 17:54:01 +08:00
parent c937e13d43
commit 27e4f7236c
22 changed files with 8769 additions and 668 deletions
+155 -72
View File
@@ -24,6 +24,23 @@ from .workers import EditWorker, LoadWorker, ScanWorker
VIEW_BACKGROUND_COLOR = (226 / 255.0, 238 / 255.0, 247 / 255.0)
def _prepare_static_mapper(mapper) -> None:
"""Hint that displayed STEP geometry is static while the camera moves."""
if hasattr(mapper, "ScalarVisibilityOff"):
mapper.ScalarVisibilityOff()
if hasattr(mapper, "StaticOn"):
mapper.StaticOn()
elif hasattr(mapper, "SetStatic"):
mapper.SetStatic(True)
def _safe_int_or_none(value: object) -> int | None:
try:
return int(value)
except (TypeError, ValueError):
return None
class WindowCoreMixin:
@Slot(object)
def _run_ui_task(self, callback) -> None:
@@ -121,37 +138,11 @@ class WindowCoreMixin:
return
self.camera_interaction_active = True
self._cancel_hover_tracking(render=False)
if self.edge_actor is not None:
try:
self.edge_actor_visible_before_interaction = bool(self.edge_actor.GetVisibility())
self.edge_actor.SetVisibility(False)
except Exception:
self.edge_actor_visible_before_interaction = True
if self.model_actor is not None:
try:
prop = self.model_actor.GetProperty()
self.model_interpolation_before_interaction = int(prop.GetInterpolation())
prop.SetInterpolationToGouraud()
except Exception:
self.model_interpolation_before_interaction = None
def _end_camera_interaction(self) -> None:
if not getattr(self, "camera_interaction_active", False):
return
self.camera_interaction_active = False
if self.edge_actor is not None:
try:
self.edge_actor.SetVisibility(bool(getattr(self, "edge_actor_visible_before_interaction", True)))
except Exception:
pass
if self.model_actor is not None and self.model_interpolation_before_interaction is not None:
try:
self.model_actor.GetProperty().SetInterpolation(int(self.model_interpolation_before_interaction))
except Exception:
pass
self.model_interpolation_before_interaction = None
if hasattr(self, "render_window"):
self.render_window.Render()
def closeEvent(self, event) -> None:
load_thread_running = bool(
@@ -669,13 +660,15 @@ class WindowCoreMixin:
edge_ids = self.model.edge_ids_for_solid(self.selected_solid_id)
label = f"Solid {self.selected_solid_id}"
elif self.selected_kind == "feature" and self.selected_face_id is not None:
info = self.model.feature_info(self.selected_face_id)
info = dict(getattr(self, "current_info_values", {}) or {})
if _safe_int_or_none(info.get("feature_source_face_id", self.selected_face_id)) != self.selected_face_id:
info = {}
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 == "face" and self.selected_face_id is not None:
face_ids = self.model.connected_same_domain_face_ids(self.selected_face_id) or [self.selected_face_id]
edge_ids = self.model.face_region_boundary_edge_ids(self.selected_face_id)
face_ids = [self.selected_face_id]
edge_ids = self.model.face_boundary_edge_ids(self.selected_face_id)
label = f"Face {self.selected_face_id}"
elif self.selected_kind == "edge" and self.selected_edge_id is not None:
info = self.model.edge_info(self.selected_edge_id)
@@ -737,16 +730,18 @@ class WindowCoreMixin:
face_polydata = self.model.build_face_polydata(face_ids=face_ids)
edge_polydata = self.model.build_edge_polydata(edge_ids=edge_ids)
elif self.selected_kind == "feature" and self.selected_face_id is not None:
info = self.model.feature_info(self.selected_face_id)
info = dict(getattr(self, "current_info_values", {}) or {})
if _safe_int_or_none(info.get("feature_source_face_id", self.selected_face_id)) != self.selected_face_id:
info = {}
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"))
face_polydata = self.model.build_face_polydata(face_ids=face_ids)
edge_polydata = self.model.build_edge_polydata(edge_ids=edge_ids)
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 == "face" and self.selected_face_id is not None:
face_ids = self.model.connected_same_domain_face_ids(self.selected_face_id) or [self.selected_face_id]
face_polydata = self.model.build_face_polydata(face_ids=face_ids)
face_polydata = self._cached_face_overlay_polydata(face_ids=[self.selected_face_id], smooth=False)
elif self.selected_kind == "edge" and self.selected_edge_id is not None:
edge_polydata = self.model.build_edge_polydata(edge_ids=[self.selected_edge_id])
edge_polydata = self._cached_edge_overlay_polydata(self.selected_edge_id)
return _merge_polydata_bounds(face_polydata, edge_polydata)
@@ -773,12 +768,13 @@ class WindowCoreMixin:
face_ids = [index for index, solid_id in enumerate(self.model.face_solid_ids) if solid_id == self.selected_solid_id]
self._highlight_faces(face_ids=face_ids)
elif self.selected_kind == "feature" and self.selected_face_id is not None:
info = self.model.feature_info(self.selected_face_id)
info = dict(getattr(self, "current_info_values", {}) or {})
if _safe_int_or_none(info.get("feature_source_face_id", self.selected_face_id)) != self.selected_face_id:
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 == "face" and self.selected_face_id is not None:
face_ids = self.model.connected_same_domain_face_ids(self.selected_face_id) or [self.selected_face_id]
self._highlight_faces(face_ids=face_ids)
self._highlight_faces(face_ids=[self.selected_face_id])
elif self.selected_kind == "edge" and self.selected_edge_id is not None:
self._highlight_edge(self.selected_edge_id)
@@ -786,6 +782,33 @@ class WindowCoreMixin:
self.face_overlay_polydata_cache.clear()
self.edge_overlay_polydata_cache.clear()
def _rebuild_polydata_cell_indexes(self) -> None:
self.model_face_cell_ids_by_face = {}
self.model_face_cell_ids_by_part = {}
self.model_face_cell_ids_by_solid = {}
if self.model_polydata is None:
return
face_arr = self.model_face_id_array
part_arr = self.model_part_id_array
solid_arr = self.model_solid_id_array
if face_arr is None or part_arr is None or solid_arr is None:
return
for cell_id in range(self.model_polydata.GetNumberOfCells()):
face_id = int(face_arr.GetValue(cell_id))
part_id = int(part_arr.GetValue(cell_id))
solid_id = int(solid_arr.GetValue(cell_id))
self.model_face_cell_ids_by_face.setdefault(face_id, []).append(cell_id)
self.model_face_cell_ids_by_part.setdefault(part_id, []).append(cell_id)
self.model_face_cell_ids_by_solid.setdefault(solid_id, []).append(cell_id)
def _rebuild_edge_polydata_cell_index(self) -> None:
self.edge_cell_ids_by_edge = {}
if self.edge_polydata is None or self.edge_id_array is None:
return
for cell_id in range(self.edge_polydata.GetNumberOfCells()):
edge_id = int(self.edge_id_array.GetValue(cell_id))
self.edge_cell_ids_by_edge.setdefault(edge_id, []).append(cell_id)
def _remember_overlay_cache_item(self, cache: dict, key: object, value: object) -> object:
if len(cache) >= self.overlay_cache_limit:
try:
@@ -826,8 +849,22 @@ class WindowCoreMixin:
part_arr = self.model_part_id_array
if face_arr is None or part_arr is None:
return None
candidate_cell_ids = None
if face_set is not None and self.model_face_cell_ids_by_face:
candidate_cell_ids = []
for face_id in face_set:
candidate_cell_ids.extend(self.model_face_cell_ids_by_face.get(int(face_id), ()))
elif part_set is not None and self.model_face_cell_ids_by_part:
candidate_cell_ids = []
for part_id in part_set:
candidate_cell_ids.extend(self.model_face_cell_ids_by_part.get(int(part_id), ()))
cell_ids = (
range(self.model_polydata.GetNumberOfCells())
if candidate_cell_ids is None
else sorted(set(candidate_cell_ids))
)
ids = vtk.vtkIdList()
for cell_id in range(self.model_polydata.GetNumberOfCells()):
for cell_id in cell_ids:
if face_set is not None and int(face_arr.GetValue(cell_id)) not in face_set:
continue
if part_set is not None and int(part_arr.GetValue(cell_id)) not in part_set:
@@ -838,8 +875,11 @@ class WindowCoreMixin:
def _extract_visible_edge_polydata(self, edge_id: int):
if self.edge_polydata is None or self.edge_id_array is None:
return None
cell_ids = self.edge_cell_ids_by_edge.get(int(edge_id))
if cell_ids is None:
cell_ids = range(self.edge_polydata.GetNumberOfCells())
ids = vtk.vtkIdList()
for cell_id in range(self.edge_polydata.GetNumberOfCells()):
for cell_id in cell_ids:
if int(self.edge_id_array.GetValue(cell_id)) == edge_id:
ids.InsertNextId(cell_id)
return self._extract_cells_as_polydata(self.edge_polydata, ids)
@@ -868,8 +908,10 @@ class WindowCoreMixin:
self.model_face_id_array = self.model_polydata.GetCellData().GetArray("face_id")
self.model_part_id_array = self.model_polydata.GetCellData().GetArray("part_id")
self.model_solid_id_array = self.model_polydata.GetCellData().GetArray("solid_id")
self._rebuild_polydata_cell_indexes()
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(self.model_polydata)
_prepare_static_mapper(mapper)
self.model_actor = vtk.vtkActor()
self.model_actor.SetMapper(mapper)
self.model_actor.GetProperty().SetColor(0.68, 0.72, 0.73)
@@ -886,8 +928,10 @@ class WindowCoreMixin:
self.edge_polydata = edge_polydata
self.edge_id_array = self.edge_polydata.GetCellData().GetArray("edge_id")
self._rebuild_edge_polydata_cell_index()
edge_mapper = vtk.vtkPolyDataMapper()
edge_mapper.SetInputData(self.edge_polydata)
_prepare_static_mapper(edge_mapper)
self.edge_actor = vtk.vtkActor()
self.edge_actor.SetMapper(edge_mapper)
self.edge_actor.GetProperty().SetColor(0.08, 0.09, 0.1)
@@ -941,7 +985,7 @@ class WindowCoreMixin:
self.pending_hover_position = None
self.last_hover_pick_position = None
self.hover_timer.stop()
self._clear_hover(render=True)
self._clear_hover(render=False)
def on_left_button_release(self, _obj, _event) -> None:
if getattr(self, "skip_next_vtk_left_release", False):
@@ -999,10 +1043,12 @@ class WindowCoreMixin:
target = self._pick_selection_target(mode, x, y)
if target is None:
self._cancel_hover_tracking(render=False)
self._reset_selection(clear_highlight=True)
if hasattr(self, "render_window"):
self.render_window.Render()
self.statusBar().showMessage("已取消选择")
if self.selected_kind is not None:
self.statusBar().showMessage("未命中对象,已保持当前选择")
else:
self.statusBar().showMessage("未命中对象")
return
self._clear_hover(render=False)
@@ -1019,7 +1065,7 @@ class WindowCoreMixin:
self.pending_hover_position = None
self.last_hover_pick_position = None
self.hover_timer.stop()
self._clear_hover(render=True)
self._clear_hover(render=False)
def on_pointer_button_release(self, _obj, _event) -> None:
if not self._is_ui_thread():
@@ -1083,6 +1129,8 @@ 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):
return
position = (int(x), int(y))
threshold = int(getattr(self, "hover_move_threshold_px", 0) or 0)
if threshold > 0:
@@ -1093,10 +1141,14 @@ class WindowCoreMixin:
if max(dx, dy) < threshold:
return
self.pending_hover_position = position
if not self.hover_timer.isActive():
self.hover_timer.start(self.hover_interval_ms)
if self.hover_timer.isActive():
self.hover_timer.stop()
self.hover_timer.start(self.hover_interval_ms)
def _update_hover_target(self) -> None:
if getattr(self, "pointer_button_down", False) or getattr(self, "camera_interaction_active", False):
self._clear_hover(render=False)
return
if (
self.operation_in_progress
or self.scan_in_progress
@@ -1109,9 +1161,21 @@ class WindowCoreMixin:
return
x, y = self.pending_hover_position
self.last_hover_pick_position = (x, y)
target = self._pick_selection_target(self._current_selection_mode(), x, y)
target = self._pick_hover_target(self._current_selection_mode(), x, y)
self._show_hover_target(target)
def _pick_hover_target(self, mode: str, x: int, y: int) -> dict[str, object] | None:
if self.model is None:
return None
if mode == "Edge":
edge_hit = self._pick_edge_cell(x, y)
return self._edge_target_from_cell_hit(edge_hit) if edge_hit is not None else None
face_hit = self._pick_face_cell(x, y)
if face_hit is None:
return None
return self._target_from_face_hit(face_hit, mode)
def _pick_selection_target(self, mode: str, x: int, y: int) -> dict[str, object] | None:
if self.model is None:
return None
@@ -1510,29 +1574,26 @@ class WindowCoreMixin:
self.selected_kind = "feature" if feature_mode else "face"
self.selected_face_id = face_id
self.selected_pick_position = pick_position
info = self.model.face_info(face_id)
info = self.model.quick_face_info(face_id)
if feature_mode:
info["feature_mode"] = "当前是几何候选判断,不等同于 CAD 历史特征"
highlight_face_ids = [face_id]
info = self._feature_info_for_selected_face(face_id, info)
info["kind"] = "feature"
info.setdefault("feature_mode", "当前是几何候选判断,不等同于 CAD 历史特征")
highlight_face_ids = _int_values(info.get("feature_highlight_face_ids")) or [face_id]
else:
highlight_face_ids = self.model.connected_same_domain_face_ids(face_id)
if len(highlight_face_ids) > 1:
info["same_domain_face_ids"] = tuple(highlight_face_ids)
info["same_domain_face_count"] = len(highlight_face_ids)
info["same_domain_note"] = "已高亮属于同一几何面且范围相接/重叠的连续Face区域。"
logical_id = self.model.face_region_logical_id(face_id)
input_info = self.model.feature_info(face_id) if info.get("surface") == "plane" else info
highlight_face_ids = [face_id]
logical_id = int(info.get("logical_face_id", face_id))
input_info = dict(info)
self._sync_cylindrical_edit_inputs(input_info)
self.selected_part_id = int(info["part_id"])
self.selected_solid_id = int(info["solid_id"]) if int(info["solid_id"]) >= 0 else None
self._highlight_faces(face_ids=highlight_face_ids or [face_id])
self._show_pick_marker(pick_position)
self._sync_id_picker("Feature" if feature_mode else "Face", face_id if feature_mode else logical_id)
self.set_info(self._with_pick_info(info, pick_position))
self.set_info(self._with_pick_info(input_info, pick_position))
self._update_action_states()
suffix = f",同域区域 {len(highlight_face_ids)} 个Face" if not feature_mode and len(highlight_face_ids) > 1 else ""
raw_note = f"(拓扑Face {face_id}" if not feature_mode and logical_id != face_id else ""
message = f"已选择逻辑面区域 {logical_id}{raw_note}{suffix}" if not feature_mode else f"已选择Face {face_id}"
message = f"已选择Face {logical_id}{raw_note}" if not feature_mode else f"已选择特征来源Face {face_id}"
self.statusBar().showMessage(self._selection_status(message, pick_position))
def select_feature(self, face_id: int, pick_position: tuple[float, float, float] | None = None) -> None:
@@ -1540,7 +1601,8 @@ class WindowCoreMixin:
return
if self._edit_busy("编辑计算中,暂时不能选择特征。"):
return
info = self.model.feature_info(face_id)
info = self._feature_info_for_selected_face(face_id, self.model.quick_face_info(face_id))
info["kind"] = "feature"
self._reset_selection(clear_highlight=False, clear_info=False)
self.selected_kind = "feature"
self.selected_face_id = face_id
@@ -1559,6 +1621,23 @@ class WindowCoreMixin:
def _sync_cylindrical_edit_inputs(self, info: dict[str, object]) -> None:
fillet_radius_suggestion: str | None = None
for target_attr in (
"hole_center_x_input",
"hole_center_y_input",
"hole_center_z_input",
"slot_center_x_input",
"slot_center_y_input",
"slot_center_z_input",
"boss_center_x_input",
"boss_center_y_input",
"boss_center_z_input",
"cone_reference_radius_input",
"sphere_radius_input",
"torus_radius_input",
"face_area_input",
):
if hasattr(self, target_attr):
getattr(self, target_attr).clear()
shell_thickness = _float_or_none(info.get("shell_thickness_estimate"))
if hasattr(self, "shell_thickness_input"):
if info.get("shell_region_status") == "candidate" and shell_thickness is not None and shell_thickness > 0:
@@ -1764,11 +1843,6 @@ class WindowCoreMixin:
return None
kind = str(target["kind"])
target_id = int(target["target_id"])
if kind in {"face", "feature"} and 0 <= target_id < len(self.model.faces):
try:
return (kind, self.model.face_region_logical_id(target_id))
except Exception:
return (kind, target_id)
return (kind, target_id)
def _show_hover_target(self, target: dict[str, object] | None) -> None:
@@ -1792,13 +1866,11 @@ class WindowCoreMixin:
face_ids = [index for index, solid_id in enumerate(self.model.face_solid_ids) if solid_id == target_id]
self._highlight_hover_faces(face_ids=face_ids)
elif kind == "feature":
face_ids = self.model.connected_same_domain_face_ids(target_id) or [target_id]
self._highlight_hover_faces(face_ids=face_ids)
self._highlight_hover_faces(face_ids=[target_id])
elif kind == "edge":
self._highlight_hover_edge(target_id)
else:
face_ids = self.model.connected_same_domain_face_ids(target_id)
self._highlight_hover_faces(face_ids=face_ids or [target_id])
self._highlight_hover_faces(face_ids=[target_id])
self.hover_signature = signature
self.render_window.Render()
@@ -2078,12 +2150,23 @@ class WindowCoreMixin:
return
self._show_edge_tube_preview(edge_id, distance, (1.0, 0.55, 0.08), "倒角预览")
def _show_edge_length_preview(self, edge_id: int, target_length: float, anchor_mode: str = "auto") -> None:
def _show_edge_length_preview(
self,
edge_id: int,
target_length: float,
anchor_mode: str = "auto",
strategy_mode: str = "auto",
) -> None:
if self.model is None:
return
self.clear_edit_preview(render=False)
try:
polydata = self.model.straight_edge_length_preview_polydata(edge_id, target_length, anchor_mode=anchor_mode)
polydata = self.model.straight_edge_length_preview_polydata(
edge_id,
target_length,
anchor_mode=anchor_mode,
strategy_mode=strategy_mode,
)
except Exception as exc:
self.statusBar().showMessage(f"Edge长度修改预览不可用:{exc}")
return