feat: 完善 Face 一级编辑与隔离计算

This commit is contained in:
2026-08-05 15:08:16 +08:00
parent a76282d7dd
commit ed1faf51d8
28 changed files with 3740 additions and 876 deletions
+12 -8
View File
@@ -151,6 +151,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.edge_actor = None
self.highlight_actor = None
self.edge_highlight_actor = None
self.highlight_signature: tuple[object, ...] | None = None
self.hover_face_actor = None
self.hover_edge_actor = None
self.hover_signature: tuple[str, int] | None = None
@@ -238,6 +239,9 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.last_id_kind = "Feature"
self.feature_detection_level = "current-only"
self.property_editor_updating = False
self._control_state_cache: dict[int, tuple[bool, str]] = {}
self._selected_action_info_cache_key: tuple[object, ...] | None = None
self._selected_action_info_cache_value: dict[str, object] | None = None
self.property_editor_specs: list[dict[str, object]] = []
self.property_table_expanded = False
self.property_table_collapsed_rows = 6
@@ -835,7 +839,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.show_internal_edges_checkbox = QCheckBox("显示同域内部边")
help_tip(
self.show_internal_edges_checkbox,
"显示同一平面或同一圆柱面内部的拓扑分割边。关闭时会隐藏布尔拉后常见的视觉接缝线。",
"显示同一平面或同一圆柱面内部的拓扑分割边。关闭时会隐藏布尔拉伸/切除后常见的视觉接缝线。",
)
self.show_internal_edges_checkbox.toggled.connect(self._on_internal_edges_toggled)
view_layout.addWidget(self.show_internal_edges_checkbox)
@@ -981,9 +985,9 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
edit_layout = QGridLayout(edit_box)
edit_layout.addWidget(QLabel("移动距离"), 0, 0)
self.offset_input = QLineEdit("5.0")
help_tip(self.offset_input, "平面拉距离。正数通常向外加料,负数通常向内切削;单位沿用 STEP 模型单位。")
help_tip(self.offset_input, "平面拉伸/切除距离。正数通常向外加料,负数通常向内切削;单位沿用 STEP 模型单位。")
edit_layout.addWidget(self.offset_input, 0, 1)
self.push_button = QPushButton("拉平面")
self.push_button = QPushButton("伸/切除平面")
help_tip(self.push_button, "移动当前选中的平面区域:正数加料,负数切削。会先显示半透明预览,再后台执行。")
self.push_button.clicked.connect(self.push_pull_face)
edit_layout.addWidget(self.push_button, 1, 0, 1, 2)
@@ -1081,10 +1085,10 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
help_tip(self.face_area_input, "目标面面积。当前选中对象表会使用这个隐藏输入执行所属特征或 Solid 的均匀缩放。")
self.face_width_input = QLineEdit("", edit_box)
self.face_width_input.setVisible(False)
help_tip(self.face_width_input, "目标面宽。当前选中对象表会使用这个隐藏输入修改 Face 平面内第一个方向的尺寸。")
help_tip(self.face_width_input, "目标U向尺寸。当前选中对象表会使用这个隐藏输入修改 Face 平面内第一个方向的尺寸。")
self.face_height_input = QLineEdit("", edit_box)
self.face_height_input.setVisible(False)
help_tip(self.face_height_input, "目标面高。当前选中对象表会使用这个隐藏输入修改 Face 平面内第二个方向的尺寸。")
help_tip(self.face_height_input, "目标V向尺寸。当前选中对象表会使用这个隐藏输入修改 Face 平面内第二个方向的尺寸。")
self.resize_boss_button = QPushButton("调整圆柱凸台直径")
help_tip(self.resize_boss_button, "修改完整圆柱凸台直径。变大会加料,变小会重建凸台区域。")
self.resize_boss_button.clicked.connect(self.resize_boss)
@@ -1214,14 +1218,14 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.resize_edge_length_button.clicked.connect(self.resize_any_edge_length)
edit_layout.addWidget(self.resize_edge_length_button, 19, 0, 1, 2)
edit_layout.addWidget(QLabel("薄壁厚度"), 20, 0)
edit_layout.addWidget(QLabel("壳体厚度"), 20, 0)
self.shell_thickness_input = QLineEdit("")
help_tip(
self.shell_thickness_input,
"薄壁/壳体局部区域的目标厚度。选中有相对平面的平面候选后会自动填一个参考值。",
"壳体局部区域的目标厚度。选中有相对平面的平面候选后会自动填一个参考值。",
)
edit_layout.addWidget(self.shell_thickness_input, 20, 1)
self.resize_shell_thickness_button = QPushButton("调整薄壁厚度")
self.resize_shell_thickness_button = QPushButton("调整壳体厚度")
help_tip(
self.resize_shell_thickness_button,
"移动当前平面区域来达到目标厚度。当前版本基于相对平面估算,执行前会预览并可回滚。",
+9
View File
@@ -42,6 +42,15 @@ SNAPSHOT_FACE_LOGICAL_IDS_KEY = "__face_logical_ids__"
FACE_SELECTION_FEATURE_INFO_SURFACES = {"plane", "cylinder", "cone", "sphere", "torus"}
FREEFORM_FACE_SURFACES = {
"bezier surface",
"b-spline surface",
"surface of revolution",
"surface of extrusion",
"offset surface",
"other surface",
}
CURVE_TYPES = {
GeomAbs_Line: "line",
GeomAbs_Circle: "circle",
+6 -6
View File
@@ -536,7 +536,7 @@ class FeatureMixin:
"confidence": item["confidence"],
"note": (
"这是完整圆柱凸台候选;点击后会选中该 face,可在当前选中对象里修改凸台高度。"
"执行时会拉识别到的端盖 Face。"
"执行时会拉伸/切除识别到的端盖 Face。"
),
}
)
@@ -681,11 +681,11 @@ class FeatureMixin:
confidence = "pending"
risk = "medium"
status = "caution"
note = "快速扫描:拉方向会在选中Face或执行编辑前再详细判断。"
note = "快速扫描:拉伸/切除方向会在选中Face或执行编辑前再详细判断。"
candidates.append(
{
"operation_key": "push_pull_plane",
"operation": "拉平面",
"operation": "伸/切除平面",
"target_kind": "face",
"target_id": face_id,
"face_id": face_id,
@@ -710,7 +710,7 @@ class FeatureMixin:
candidates.append(
{
"operation_key": "resize_shell_thickness",
"operation": "调整薄壁厚度",
"operation": "调整壳体厚度",
"target_kind": "face",
"target_id": face_id,
"face_id": face_id,
@@ -725,7 +725,7 @@ class FeatureMixin:
"confidence": shell_confidence,
"note": (
"快速扫描:已找到投影重叠的相对平面;点击后会填入参考目标厚度,"
"执行时会移动当前平面区域来改变局部薄壁/壳体厚度。"
"执行时会移动当前平面区域来改变局部壳体厚度。"
),
}
)
@@ -2591,7 +2591,7 @@ class FeatureMixin:
if not bottom_face_ids:
return {
"context_status": "blocked",
"context_message": "当前版本的孔深调整需要疑似底面;如果自动识别失败,请手动填写底面Face ID。",
"context_message": "当前版本的孔深调整需要疑似底面;如果自动识别失败,请手动填写底面 Face ID。",
}
cyl = surf.Cylinder()
+12
View File
@@ -22,6 +22,8 @@ from .workers import EditWorker, ScanWorker
class InfoPanelMixin:
def set_info(self, info: dict[str, object]) -> None:
if hasattr(self, "_invalidate_selected_action_info_cache"):
self._invalidate_selected_action_info_cache()
self.current_info_values = dict(info)
self.current_info_text = _info_to_text(info)
self.info_text.setPlainText(self.current_info_text)
@@ -44,6 +46,8 @@ class InfoPanelMixin:
self._update_action_states()
def set_plain_info(self, text: str) -> None:
if hasattr(self, "_invalidate_selected_action_info_cache"):
self._invalidate_selected_action_info_cache()
self.current_info_values = {}
self.current_info_text = text
self.info_tree.clear()
@@ -262,6 +266,14 @@ class InfoPanelMixin:
if self.selected_kind == "solid" and self.selected_solid_id is not None:
return f"solid {self.selected_solid_id}"
if self.selected_kind in {"face", "feature"} and self.selected_face_id is not None:
for key in ("selection_display_id", "face_region_logical_id", "logical_face_id"):
value = self.current_info_values.get(key)
if value in {None, ""}:
continue
try:
return f"face {int(value)}"
except (TypeError, ValueError):
continue
return f"face {self.selected_face_id}"
if self.selected_kind == "edge" and self.selected_edge_id is not None:
return f"edge {self.selected_edge_id}"
+80
View File
@@ -8,6 +8,19 @@ from pathlib import Path
from .model import StepModel
def _optional_int(value: object) -> int | None:
if value is None or value == "":
return None
return int(value)
def _point3(value: object, operation: str) -> tuple[float, float, float]:
point = list(value) if isinstance(value, (list, tuple)) else []
if len(point) != 3:
raise ValueError(f"{operation} requires a 3D target center.")
return (float(point[0]), float(point[1]), float(point[2]))
def _execute(model: StepModel, operation: str, args: list[object]) -> str:
if operation == "push_pull_face":
return model.push_pull_face(int(args[0]), float(args[1]))
@@ -47,6 +60,73 @@ def _execute(model: StepModel, operation: str, args: list[object]) -> str:
return model.resize_spherical_radius(int(args[0]), float(args[1]))
if operation == "resize_torus_radius":
return model.resize_toroidal_radius(int(args[0]), float(args[1]), str(args[2]))
if operation == "resize_cylindrical_hole":
return model.resize_cylindrical_hole(int(args[0]), float(args[1]))
if operation == "resize_cylindrical_owning_scale":
return model.resize_cylindrical_owning_scale(int(args[0]), float(args[1]))
if operation == "move_cylindrical_hole_axis":
return model.move_cylindrical_hole_axis(int(args[0]), _point3(args[1], operation))
if operation == "suppress_cylindrical_hole":
return model.suppress_cylindrical_hole(int(args[0]))
if operation == "resize_cylindrical_depth":
bottom_face_id = _optional_int(args[2]) if len(args) > 2 else None
return model.resize_cylindrical_depth(int(args[0]), float(args[1]), bottom_face_id=bottom_face_id)
if operation == "resize_cylindrical_depth_owning_scale":
bottom_face_id = _optional_int(args[2]) if len(args) > 2 else None
return model.resize_cylindrical_depth_owning_scale(int(args[0]), float(args[1]), bottom_face_id=bottom_face_id)
if operation == "move_cylindrical_slot_axis":
return model.move_cylindrical_slot_axis(int(args[0]), _point3(args[1], operation))
if operation == "resize_cylindrical_slot_width":
pair_face_id = _optional_int(args[2]) if len(args) > 2 else None
return model.resize_cylindrical_slot_width(int(args[0]), float(args[1]), pair_face_id=pair_face_id)
if operation == "resize_cylindrical_slot_depth":
pair_face_id = _optional_int(args[2]) if len(args) > 2 else None
return model.resize_cylindrical_slot_depth(int(args[0]), float(args[1]), pair_face_id=pair_face_id)
if operation == "resize_cylindrical_slot_arc_length":
pair_face_id = _optional_int(args[2]) if len(args) > 2 else None
return model.resize_cylindrical_slot_arc_length(int(args[0]), float(args[1]), pair_face_id=pair_face_id)
if operation == "resize_cylindrical_slot_angular_span":
return model.resize_cylindrical_slot_angular_span(int(args[0]), float(args[1]))
if operation == "resize_cylindrical_slot_total_length":
pair_face_id = _optional_int(args[2]) if len(args) > 2 else None
return model.resize_cylindrical_slot_total_length(int(args[0]), float(args[1]), pair_face_id=pair_face_id)
if operation == "resize_cylindrical_slot_center_distance":
pair_face_id = _optional_int(args[2]) if len(args) > 2 else None
return model.resize_cylindrical_slot_center_distance(
int(args[0]),
float(args[1]),
pair_face_id=pair_face_id,
)
if operation == "resize_general_edge_length":
anchor_mode = str(args[2]) if len(args) > 2 else "auto"
strategy_mode = str(args[3]) if len(args) > 3 else "auto"
return model.resize_general_edge_length(
int(args[0]),
float(args[1]),
anchor_mode=anchor_mode,
strategy_mode=strategy_mode,
)
if operation == "move_edge_endpoint":
return model.move_edge_endpoint(int(args[0]), str(args[1]), _point3(args[2], operation))
if operation == "move_edge_center":
return model.move_edge_center(int(args[0]), _point3(args[1], operation))
if operation == "move_circular_edge_axis_center":
return model.move_circular_edge_axis_center(int(args[0]), _point3(args[1], operation))
if operation == "resize_ellipse_edge_axis_radius":
axis_kind = str(args[2]) if len(args) > 2 else "major"
return model.resize_ellipse_edge_axis_radius(int(args[0]), float(args[1]), axis_kind=axis_kind)
if operation == "resize_existing_fillet":
return model.resize_existing_fillet(int(args[0]), float(args[1]))
if operation == "fillet_edge":
return model.fillet_edge(int(args[0]), float(args[1]))
if operation == "chamfer_edge":
return model.chamfer_edge(int(args[0]), float(args[1]))
if operation == "chamfer_edge_asymmetric":
reference_face_id = _optional_int(args[3]) if len(args) > 3 else None
return model.chamfer_edge_asymmetric(int(args[0]), float(args[1]), float(args[2]), reference_face_id)
if operation == "chamfer_edge_distance_angle":
reference_face_id = _optional_int(args[3]) if len(args) > 3 else None
return model.chamfer_edge_distance_angle(int(args[0]), float(args[1]), float(args[2]), reference_face_id)
raise ValueError(f"Unsupported isolated edit operation: {operation}")
+70 -34
View File
@@ -74,7 +74,7 @@ from OCC.Core.XCAFDoc import XCAFDoc_DocumentTool
from OCC.Core.gp import gp_Ax1, gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf, gp_Vec
from OCC.Extend.TopologyUtils import TopologyExplorer, discretize_edge
from .constants import CURVE_TYPES, SNAPSHOT_FACE_LOGICAL_IDS_KEY, SURFACE_TYPES
from .constants import CURVE_TYPES, FREEFORM_FACE_SURFACES, SNAPSHOT_FACE_LOGICAL_IDS_KEY, SURFACE_TYPES
from .export import ExportMixin
from .features import FeatureMixin
from .operations import OperationMixin
@@ -352,19 +352,20 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
info["normal"] = _dir_tuple(direction)
info["oriented_normal"] = _oriented_dir_tuple(direction, face)
info["push_pull_confidence"] = "unchecked"
info["push_pull_note"] = "快速选择阶段不判断材料内外方向;执行拉时会重新计算。"
cap_direction = self._cylindrical_cap_push_pull_direction(face_id, surf)
if cap_direction is not None:
info.update(cap_direction)
info["push_pull_note"] = "快速选择阶段不判断材料内外方向;执行拉伸/切除时会重新计算。"
# Keep ordinary selection cheap. Cylindrical-cap direction can
# scan adjacent surfaces and cost hundreds of ms on large STEP
# files; the full push/pull plan recomputes it when the user
# actually edits.
info["push_pull_status"] = "candidate"
info["feature_type"] = "拉平面候选"
info["feature_type"] = "可拉伸/切除平面候选"
info["feature_source_face_id"] = face_id
info["feature_highlight_face_ids"] = (face_id,)
info["feature_edit_actions"] = "拉平面"
info["feature_edit_actions"] = "伸/切除平面"
if bool(info.get("has_inner_boundaries")):
info["local_face_deform_ready"] = False
info["local_face_deform_face_count"] = 1
info["local_face_deform_blocker"] = "当前 Face 有内孔/内边界;请优先使用推拉当前面、孔或槽的专门修改入口。"
info["local_face_deform_blocker"] = "当前 Face 有内孔/内边界;请优先使用拉伸/切除、孔或槽的专门修改入口。"
else:
info["local_face_deform_ready"] = True
info["local_face_deform_face_count"] = 1
@@ -430,6 +431,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
info["minor_radius"] = torus.MinorRadius()
info["feature_torus_major_radius"] = torus.MajorRadius()
info["feature_torus_minor_radius"] = torus.MinorRadius()
elif surface_label in FREEFORM_FACE_SURFACES:
info.update(self._freeform_face_limit_fields(surface_label))
info.update(self._recognition_summary_fields(info))
self._quick_face_info_cache[face_id] = dict(info)
@@ -528,10 +531,33 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
info["axis"] = _dir_tuple(torus.Axis().Direction())
info["major_radius"] = torus.MajorRadius()
info["minor_radius"] = torus.MinorRadius()
elif str(info.get("surface") or "") in FREEFORM_FACE_SURFACES:
info.update(self._freeform_face_limit_fields(str(info.get("surface") or "")))
info.update(self._recognition_summary_fields(info))
self._face_info_cache[face_id] = dict(info)
return dict(info)
def _freeform_face_limit_fields(self, surface: str) -> dict[str, object]:
blocker = (
"当前 Face 是自由曲面/非基础解析曲面;STEP 里通常没有可直接修改的历史参数,"
"当前一级阶段不开放面积、尺寸、半径或偏移类参数化修改。"
"请先保留为只读诊断,后续需要专门的曲面控制点或曲面替换语义。"
)
return {
"feature_type": "自由曲面 Face(暂不支持参数化编辑)",
"feature_edit_actions": "只读诊断;不开放参数化修改",
"freeform_face_status": "blocked",
"freeform_face_risk": "blocked",
"freeform_face_blockers": blocker,
"local_face_deform_ready": False,
"local_face_deform_face_count": 1,
"local_face_deform_blocker": blocker,
"recognition_confidence": "low",
"recognition_risk": "blocked",
"recognition_blockers": blocker,
"note": f"{surface} 当前按自由曲面处理,不会伪装成平面、圆柱、圆锥、球面或环面参数。",
}
def face_surface_kind(self, face_id: int) -> str:
if face_id < 0 or face_id >= len(self.faces):
return ""
@@ -588,12 +614,14 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
confidence = "high"
elif surface == "torus" and _float_or_none(info.get("major_radius")) is not None and _float_or_none(info.get("minor_radius")) is not None:
confidence = "high"
if surface in FREEFORM_FACE_SURFACES:
confidence = "low"
risk_rank = {"low": 0, "medium": 1, "high": 2, "blocked": 3}
capability_specs = (
("resize_status", "resize_risk", "resize_blockers", "孔/槽/圆柱直径"),
("push_pull_status", "push_pull_risk", "push_pull_blockers", "平面"),
("shell_status", "shell_risk", "shell_blockers", "薄壁厚度"),
("push_pull_status", "push_pull_risk", "push_pull_blockers", "平面拉伸/切除"),
("shell_status", "shell_risk", "shell_blockers", "壳体厚度"),
("cylinder_resize_status", "cylinder_resize_risk", "cylinder_resize_blockers", "圆柱直径/半径"),
("boss_resize_status", "boss_resize_risk", "boss_resize_blockers", "圆柱凸台直径/高度/轴心"),
("depth_status", "depth_risk", "depth_blockers", "盲孔/盲槽深度"),
@@ -611,8 +639,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
has_planar_push_pull_candidate = (
surface == "plane"
and (
"" in feature_type_text
or "" in feature_actions_text
"伸/切除" in feature_type_text
or "伸/切除" in feature_actions_text
or str(info.get("push_pull_status") or "").strip() == "candidate"
)
)
@@ -632,10 +660,13 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
for key in (
"risk",
"first_level_topology_risk",
"freeform_face_risk",
):
value = str(info.get(key) or "").strip()
if risk_rank.get(value, -1) > risk_rank.get(risk, -1):
risk = value
if surface in FREEFORM_FACE_SURFACES:
risk = "blocked"
for status_key, risk_key, _blocker_key, _label in capability_specs:
value = str(info.get(risk_key) or "").strip()
if value == "blocked" and has_available_capability:
@@ -696,11 +727,11 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
bottom_count = len(tuple(info.get("feature_bottom_face_ids") or ()))
except TypeError:
bottom_count = 0
add("bottom_faces", f"疑似底面Face={bottom_count}")
add("bottom_faces", f"疑似底面 Face={bottom_count}")
if info.get("slot_status") == "candidate":
add("slot_geometry", "部分圆柱槽/半孔几何")
if info.get("shell_region_status") == "candidate":
add("shell_opposite_face", "找到相对平面/薄壁候选")
add("shell_opposite_face", "找到相对平面/壳体候选")
ready_actions: list[str] = []
limited_actions: list[str] = []
@@ -724,13 +755,13 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
add_action(limited_actions, label)
if has_planar_push_pull_candidate:
add_action(ready_actions, "平面")
add_action(ready_actions, "平面拉伸/切除")
if has_local_face_deform:
add_action(ready_actions, "当前面面积/面宽/面高/中心/偏移")
add_action(ready_actions, "当前面面积/U向尺寸/V向尺寸/中心/偏移")
elif str(info.get("local_face_deform_blocker") or "").strip():
add_action(limited_actions, "当前面局部尺寸/中心/偏移")
add_action(limited_actions, "局部重建尺寸/中心/偏移")
if has_shell_candidate:
add_action(ready_actions, "薄壁厚度")
add_action(ready_actions, "壳体厚度")
if has_slot_candidate:
add_action(ready_actions, "槽/半孔宽度/深度/弧长")
if surface == "cone" and feature_type_text:
@@ -1079,25 +1110,25 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
shell_info = self._planar_shell_region_info(face_id, coplanar_face_ids, info)
prismatic_info = self._planar_rectangular_profile_info(face_id, coplanar_face_ids, info, shell_info)
if len(coplanar_face_ids) > 1:
scope_note = f"已检测到 {len(coplanar_face_ids)} 个共面且相接/重叠的 face拉时会作为同一片平面区域处理。"
scope_note = f"已检测到 {len(coplanar_face_ids)} 个共面且相接/重叠的 face,拉伸/切除时会作为同一片平面区域处理。"
else:
scope_note = "当前 face 没有检测到可一起拉的共面相接/重叠邻居。"
edit_actions = "拉平面"
scope_note = "当前 face 没有检测到可一起拉伸/切除的共面相接/重叠邻居。"
edit_actions = "伸/切除平面"
if shell_info.get("shell_region_status") == "candidate":
edit_actions += ";调整薄壁/壳体厚度"
edit_actions += ";调整壳体厚度"
if prismatic_info.get("prismatic_profile_status") == "candidate":
edit_actions = "调整规则矩形轮廓长度/宽度"
if prismatic_info.get("prismatic_extrusion_status") == "candidate":
edit_actions += ";调整棱柱高度/凹槽深度"
else:
edit_actions += ";沿法向"
edit_actions += ";沿法向拉伸/切除"
highlight_face_ids = set(coplanar_face_ids)
highlight_face_ids.update(_int_values(prismatic_info.get("prismatic_highlight_face_ids")))
result = dict(info)
result.update(
{
"kind": "feature",
"feature_type": "拉平面候选",
"feature_type": "可拉伸/切除平面候选",
"feature_source_face_id": face_id,
"feature_face_ids": tuple(coplanar_face_ids),
"feature_highlight_face_ids": tuple(coplanar_face_ids),
@@ -1363,7 +1394,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
except Exception:
return {
"shell_region_status": "not-detected",
"shell_region_note": "无法读取当前平面,不能估算薄壁/壳体区域。",
"shell_region_note": "无法读取当前平面,不能估算壳体区域。",
}
if source_surf.GetType() != GeomAbs_Plane:
return {}
@@ -1382,7 +1413,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
if source_interval is None:
return {
"shell_region_status": "not-detected",
"shell_region_note": "当前平面区域缺少稳定投影范围,不能估算薄壁/壳体厚度。",
"shell_region_note": "当前平面区域缺少稳定投影范围,不能估算壳体厚度。",
}
def interval_length(interval: tuple[float, float]) -> float:
@@ -1440,7 +1471,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
if best is None:
return {
"shell_region_status": "not-detected",
"shell_region_note": "未找到与当前平面投影重叠的相对平面;暂不能估算局部壳体/薄壁厚度。",
"shell_region_note": "未找到与当前平面投影重叠的相对平面;暂不能估算局部壳体厚度。",
}
thickness = float(best["shell_thickness_estimate"])
@@ -1454,7 +1485,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
"shell_opposite_face_id": best["shell_opposite_face_id"],
"shell_thickness_estimate": thickness,
"shell_overlap_ratio_estimate": overlap_ratio,
"shell_region_note": "相对平面距离明显大于当前面的局部短边,不按薄壁厚度处理。",
"shell_region_note": "相对平面距离明显大于当前面的局部短边,不按壳体厚度处理。",
}
thin_ratio = thickness / diagonal
if overlap_ratio >= 0.55 and thin_ratio <= 0.08:
@@ -1471,7 +1502,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
"shell_source_face_ids": tuple(valid_region_ids),
**best,
"shell_note": (
"通过同一 solid 内投影重叠的相对平面估算薄壁/壳体厚度;"
"通过同一 solid 内投影重叠的相对平面估算壳体厚度;"
"这是 B-Rep 几何近似,不等同于原 CAD 壳命令参数。"
),
}
@@ -1501,18 +1532,23 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
opening_face_ids = end_faces["opening_face_ids"]
guess = str(info.get("feature_guess", "cylindrical face"))
has_two_axial_caps = bool(end_faces["start_end_face_ids"] and end_faces["end_end_face_ids"])
support_face_ids_for_round = sorted(set(adjacent_face_ids) - set(end_face_ids))
material_toward = str(info.get("material_toward_axis", "") or "")
material_away = str(info.get("material_away_axis", "") or "")
complete_cylinder_with_caps = combined_angular_span >= math.tau * 0.92 and has_two_axial_caps
if (
guess == "round/fillet candidate"
and has_two_axial_caps
and material_toward == "inside"
and "outside" in material_away
and (complete_cylinder_with_caps or len(support_face_ids_for_round) < 2)
):
info = dict(info)
info["feature_guess"] = "boss/outer-round candidate"
info["confidence"] = "medium"
info["note"] = "partial cylinder has material inside its axis and explicit planar caps at both ends"
info["note"] = (
"cocylindrical region has material inside its axis and explicit planar caps at both ends"
)
domain_info["feature_guess"] = info["feature_guess"]
slot_info = self._cylindrical_slot_info(face_id, adjacent_face_ids, end_face_ids, domain_info)
fillet_info = self._cylindrical_existing_fillet_info(face_id, adjacent_face_ids, end_face_ids, domain_info)
@@ -2261,7 +2297,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
{
"local_face_deform_ready": False,
"local_face_deform_face_count": 1,
"local_face_deform_blocker": "找不到当前 Face 所属 Solid,不能做“只改当前面”的局部重建。",
"local_face_deform_blocker": "找不到当前 Face 所属 Solid,不能做局部重建。",
}
)
solid_face_ids = [index for index, item in enumerate(self.face_solid_ids) if item == solid_id]
@@ -2273,7 +2309,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
{
"local_face_deform_ready": False,
"local_face_deform_face_count": face_count,
"local_face_deform_blocker": "所属 Solid 的 Face 数量超过 128,当前版本不开放“只改当前面”的局部重建。",
"local_face_deform_blocker": "所属 Solid 的 Face 数量超过 128,当前版本不开放局部重建。",
}
)
@@ -2296,7 +2332,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
{
"local_face_deform_ready": False,
"local_face_deform_face_count": face_count,
"local_face_deform_blocker": "所属 Solid 含有曲面,当前版本只对简单全平面 Solid 开放“只改当前面”",
"local_face_deform_blocker": "所属 Solid 含有曲面,当前版本只对简单全平面 Solid 开放局部重建",
}
)
wire_info = self._face_boundary_wire_info(face)
@@ -2305,7 +2341,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
{
"local_face_deform_ready": False,
"local_face_deform_face_count": face_count,
"local_face_deform_blocker": "所属 Solid 里有带内孔/内边界的 Face,请优先使用孔、槽或拉等专门修改方式。",
"local_face_deform_blocker": "所属 Solid 里有带内孔/内边界的 Face,请优先使用孔、槽或拉伸/切除等专门修改方式。",
}
)
try:
+238 -167
View File
File diff suppressed because it is too large Load Diff
+56 -47
View File
@@ -18,6 +18,9 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
"file",
"part_id",
"solid_id",
"selection_title",
"selection_display_id",
"selection_topological_face_id",
"logical_face_id",
"face_region_logical_id",
"topological_face_id",
@@ -392,10 +395,13 @@ INFO_LABELS = {
"file": "文件",
"part_id": "零件 ID",
"solid_id": "Solid ID",
"selection_title": "当前选中对象",
"selection_display_id": "逻辑选择 ID",
"selection_topological_face_id": "当前拓扑 Face ID",
"logical_face_id": "逻辑 Face ID",
"face_region_logical_id": "面区域逻辑 ID",
"topological_face_id": "拓扑 Face ID",
"face_id": "Face ID",
"face_id": "当前拓扑 Face ID",
"face_region_ids": "面区域 Face",
"face_region_count": "面区域 Face 数",
"edge_id": "Edge ID",
@@ -544,24 +550,24 @@ INFO_LABELS = {
"center_of_mass": "重心",
"surface_center": "表面积中心",
"area_center": "面积中心",
"local_face_width": "当前面宽",
"local_face_height": "当前面高",
"local_face_width_direction": "面宽 方向",
"local_face_height_direction": "面高 方向",
"local_face_size_center": "面宽/面高 中心",
"face_size_axis": "面宽/面高 方向",
"face_size_label": "面宽/面高 名称",
"current_face_width": "当前面宽",
"target_face_width": "目标面宽",
"current_face_height": "当前面高",
"target_face_height": "目标面高",
"current_face_size": "当前面宽/面高",
"target_face_size": "目标面宽/面高",
"face_size_delta": "面宽/面高 变化量",
"face_size_delta_ratio": "面宽/面高 变化比例",
"face_size_scale": "面宽/面高 缩放比例",
"face_size_center": "面宽/面高 缩放中心",
"face_size_axis_direction": "面宽/面高 缩放方向",
"local_face_width": "U向尺寸",
"local_face_height": "V向尺寸",
"local_face_width_direction": "U向方向",
"local_face_height_direction": "V向方向",
"local_face_size_center": "U/V 尺寸中心",
"face_size_axis": "U/V 尺寸方向",
"face_size_label": "U/V 尺寸名称",
"current_face_width": "当前 U向尺寸",
"target_face_width": "目标 U向尺寸",
"current_face_height": "当前 V向尺寸",
"target_face_height": "目标 V向尺寸",
"current_face_size": "当前 U/V 尺寸",
"target_face_size": "目标 U/V 尺寸",
"face_size_delta": "U/V 尺寸变化量",
"face_size_delta_ratio": "U/V 尺寸变化比例",
"face_size_scale": "U/V 尺寸缩放比例",
"face_size_center": "U/V 尺寸缩放中心",
"face_size_axis_direction": "U/V 尺寸缩放方向",
"owning_face_size_rebuild_mode": "所属对象尺寸重建模式",
"length_center": "长度中心",
"bbox_min": "包围盒最小点",
@@ -574,7 +580,7 @@ INFO_LABELS = {
"end_face_id": "端面 Face",
"end_face_label": "端面位置",
"end_face_plane_distance": "端面匹配距离",
"push_pull_distance": "端面推拉距离",
"push_pull_distance": "伸/切除距离",
"normal": "几何法向",
"oriented_normal": "拓扑修正法向",
"plane_origin": "平面原点",
@@ -585,37 +591,37 @@ INFO_LABELS = {
"rotation_axis": "旋转轴",
"end_face_outward_direction": "端面向外方向",
"desired_movement_vector": "目标移动向量",
"push_pull_outward_direction": "拉向外方向",
"push_pull_inward_direction": "推拉向内方向",
"push_pull_outward_direction": "向外方向",
"push_pull_inward_direction": "切除向内方向",
"push_pull_plus_side": "原始法向侧",
"push_pull_minus_side": "反向法向侧",
"push_pull_confidence": "拉方向置信度",
"push_pull_note": "拉方向说明",
"push_pull_status": "拉状态",
"push_pull_risk": "拉风险",
"push_pull_message": "拉说明",
"push_pull_scope_face_ids": "拉共面区域 Face",
"push_pull_scope_face_count": "拉共面区域 Face 数",
"push_pull_scope_area": "拉共面区域面积",
"push_pull_scope_note": "拉共面区域说明",
"shell_region_kind": "壳体/薄壁候选类型",
"shell_region_status": "壳体/薄壁识别状态",
"shell_confidence": "壳体/薄壁置信度",
"shell_thickness": "薄壁厚度",
"shell_source_face_ids": "壳体/薄壁源平面 Face",
"push_pull_confidence": "伸/切除方向置信度",
"push_pull_note": "伸/切除方向说明",
"push_pull_status": "伸/切除状态",
"push_pull_risk": "伸/切除风险",
"push_pull_message": "伸/切除说明",
"push_pull_scope_face_ids": "伸/切除共面区域 Face",
"push_pull_scope_face_count": "伸/切除共面区域 Face 数",
"push_pull_scope_area": "伸/切除共面区域面积",
"push_pull_scope_note": "伸/切除共面区域说明",
"shell_region_kind": "壳体候选类型",
"shell_region_status": "壳体识别状态",
"shell_confidence": "壳体置信度",
"shell_thickness": "壳体厚度",
"shell_source_face_ids": "壳体源平面 Face",
"shell_opposite_face_id": "相对平面 Face",
"shell_thickness_estimate": "薄壁厚度估算",
"shell_current_thickness": "当前薄壁厚度",
"shell_target_thickness": "目标薄壁厚度",
"shell_delta_thickness": "薄壁厚度变化量",
"shell_delta_ratio": "薄壁厚度变化比例",
"shell_signed_thickness": "有符号薄壁厚度",
"shell_thickness_estimate": "壳体厚度估算",
"shell_current_thickness": "当前壳体厚度",
"shell_target_thickness": "目标壳体厚度",
"shell_delta_thickness": "壳体厚度变化量",
"shell_delta_ratio": "壳体厚度变化比例",
"shell_signed_thickness": "有符号壳体厚度",
"shell_overlap_ratio_estimate": "相对平面重叠率估算",
"shell_opposite_normal_dot": "相对平面法向点积",
"shell_desired_movement_vector": "薄壁目标移动向量",
"shell_movement_alignment": "薄壁移动方向匹配度",
"shell_note": "壳体/薄壁识别说明",
"shell_region_note": "壳体/薄壁识别说明",
"shell_desired_movement_vector": "壳体目标移动向量",
"shell_movement_alignment": "壳体移动方向匹配度",
"shell_note": "壳体识别说明",
"shell_region_note": "壳体识别说明",
"u_range": "U 参数范围",
"v_range": "V 参数范围",
"first_parameter": "起始参数",
@@ -700,6 +706,9 @@ INFO_LABELS = {
"existing_fillet_arc_length_estimate": "已有圆角圆弧长度估算",
"existing_fillet_note": "已有圆角识别说明",
"feature_edit_actions": "当前可用操作",
"freeform_face_status": "自由曲面状态",
"freeform_face_risk": "自由曲面风险",
"freeform_face_blockers": "自由曲面限制",
"resize_status": "切削状态",
"resize_strategy": "编辑策略",
"edit_strategy_label": "编辑策略说明",
@@ -940,7 +949,7 @@ def _edit_quality_warnings(before_part_stats, after_part_stats) -> list[str]:
warnings.append(
"目标零件Solid数发生变化:"
f"{before_part_stats.solids} -> {after_part_stats.solids}"
"如果这是一次局部拉/孔径修改,请重点检查导出后是否仍是一体实体。"
"如果这是一次局部拉伸/切除/孔径修改,请重点检查导出后是否仍是一体实体。"
)
return warnings
File diff suppressed because it is too large Load Diff
+46 -19
View File
@@ -149,9 +149,9 @@ class WindowCoreMixin:
self.interactor.AddObserver("StartInteractionEvent", self.on_camera_interaction_start)
self.interactor.AddObserver("EndInteractionEvent", self.on_camera_interaction_end)
if hasattr(self.interactor, "SetDesiredUpdateRate"):
self.interactor.SetDesiredUpdateRate(24.0)
self.interactor.SetDesiredUpdateRate(60.0)
if hasattr(self.interactor, "SetStillUpdateRate"):
self.interactor.SetStillUpdateRate(0.2)
self.interactor.SetStillUpdateRate(1.0)
light = vtk.vtkLight()
light.SetLightTypeToHeadlight()
@@ -252,7 +252,8 @@ class WindowCoreMixin:
"show_internal_edges": show_internal_edges,
}
if build_polydata:
result["model_polydata"] = new_model.build_face_polydata(deflection=deflection)
face_polydata = new_model.build_face_polydata(deflection=deflection)
result["model_polydata"] = _smooth_surface_polydata(face_polydata)
result["edge_polydata"] = new_model.build_edge_polydata(
deflection=deflection,
show_same_domain_internal_edges=show_internal_edges,
@@ -297,8 +298,8 @@ class WindowCoreMixin:
@Slot(object)
def _start_initial_load_worker(self, expected_path: Path) -> None:
# Qt/VTK/OCCT visualization objects are not safe to construct from this
# worker path in the current PySide build. Keep the old entry point as a
# main-thread fallback so stale signal paths cannot crash the process.
# worker path in the current PySide build. Keep this as a main-thread
# fallback so stale signal paths cannot crash the process.
self._run_deferred_initial_load(expected_path)
def _load_step_sync(
@@ -348,7 +349,7 @@ class WindowCoreMixin:
if polydata is None:
return None
copied = vtk.vtkPolyData()
copied.DeepCopy(polydata)
copied.ShallowCopy(polydata)
return copied
def _detach_worker_polydata_result(self, result: dict[str, object]) -> dict[str, object]:
@@ -1027,8 +1028,14 @@ class WindowCoreMixin:
self.renderer.RemoveAllViewProps()
self.renderer.SetBackground(*VIEW_BACKGROUND_COLOR)
self.scene_isolated = False
self.highlight_signature = None
self.model_polydata = _smooth_surface_polydata(model_polydata)
point_normals = None
try:
point_normals = model_polydata.GetPointData().GetNormals()
except Exception:
point_normals = None
self.model_polydata = model_polydata if point_normals is not None else _smooth_surface_polydata(model_polydata)
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")
@@ -1569,7 +1576,7 @@ class WindowCoreMixin:
elif action == "resize_shell_thickness":
self._set_selection_mode("Feature")
self.select_feature(int(target_id))
self.statusBar().showMessage(f"已选择可调整薄壁厚度候选Face {target_id}")
self.statusBar().showMessage(f"已选择可调整壳体厚度候选Face {target_id}")
elif action == "suppress_cylinder":
self._set_selection_mode("Feature")
self.select_feature(int(target_id))
@@ -1601,7 +1608,7 @@ class WindowCoreMixin:
else:
self._set_selection_mode("Face")
self.select_face(int(target_id))
self.statusBar().showMessage(f"已选择可拉平面Face {target_id}")
self.statusBar().showMessage(f"已选择可拉伸/切除平面Face {target_id}")
def select_by_id(self, kind: str | None = None) -> None:
if self.model is None:
@@ -1712,18 +1719,23 @@ class WindowCoreMixin:
highlight_face_ids = _int_values(info.get("feature_highlight_face_ids")) or [face_id]
else:
highlight_face_ids = [face_id]
logical_id = int(info.get("logical_face_id", face_id))
selection_fields = self._selection_identity_fields(
face_id,
"特征来源 Face" if feature_mode else "Face",
)
logical_id = int(selection_fields["selection_display_id"])
input_info = dict(info)
input_info.update(selection_fields)
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._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 not feature_mode and logical_id != face_id else ""
message = f"已选择Face {logical_id}{raw_note}" if not feature_mode else f"已选择特征来源Face {face_id}"
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}"
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:
@@ -1733,6 +1745,9 @@ class WindowCoreMixin:
return
info = self._feature_context_info(face_id)
info["kind"] = "feature"
selection_fields = self._selection_identity_fields(face_id, "特征来源 Face")
logical_id = int(selection_fields["selection_display_id"])
info.update(selection_fields)
self._reset_selection(clear_highlight=False, clear_info=False)
self.selected_kind = "feature"
self.selected_face_id = face_id
@@ -1743,11 +1758,12 @@ class WindowCoreMixin:
highlight_face_ids = _int_values(info.get("feature_highlight_face_ids"))
self._highlight_faces(face_ids=highlight_face_ids or [face_id])
self._show_pick_marker(pick_position)
self._sync_id_picker("Feature", face_id)
self._sync_id_picker("Feature", logical_id)
self.set_info(self._with_pick_info(info, pick_position))
feature_type = str(info.get("feature_type", "特征候选"))
self._update_action_states()
self.statusBar().showMessage(self._selection_status(f"已选择 {feature_type},来源Face {face_id}", pick_position))
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))
def _sync_cylindrical_edit_inputs(self, info: dict[str, object]) -> None:
fillet_radius_suggestion: str | None = None
@@ -1927,8 +1943,13 @@ class WindowCoreMixin:
def _highlight_faces(self, face_ids=None, part_ids=None) -> None:
if self.model is None:
return
face_key = _int_tuple_or_none(face_ids)
part_key = _int_tuple_or_none(part_ids)
signature = ("faces", face_key, part_key)
if signature == getattr(self, "highlight_signature", None) and self.highlight_actor is not None:
return
self._clear_highlight()
polydata = self._cached_face_overlay_polydata(face_ids=face_ids, part_ids=part_ids, smooth=True)
polydata = self._cached_face_overlay_polydata(face_ids=face_key, part_ids=part_key, smooth=True)
if polydata is None:
return
mapper = vtk.vtkPolyDataMapper()
@@ -1945,12 +1966,16 @@ class WindowCoreMixin:
actor.GetProperty().SetLineWidth(2)
self._offset_overlay_actor_toward_camera(actor, scale=0.00035)
self.highlight_actor = actor
self.highlight_signature = signature
self.renderer.AddActor(actor)
self.render_window.Render()
def _highlight_edge(self, edge_id: int) -> None:
if self.model is None:
return
signature = ("edge", int(edge_id))
if signature == getattr(self, "highlight_signature", None) and self.edge_highlight_actor is not None:
return
self._clear_highlight()
polydata = self._cached_edge_overlay_polydata(edge_id)
if polydata is None:
@@ -1965,6 +1990,7 @@ class WindowCoreMixin:
actor.GetProperty().SetDiffuse(0.8)
actor.GetProperty().SetLineWidth(5)
self.edge_highlight_actor = actor
self.highlight_signature = signature
self.renderer.AddActor(actor)
self.render_window.Render()
@@ -2090,6 +2116,7 @@ class WindowCoreMixin:
if self.pick_marker_actor is not None:
self.renderer.RemoveActor(self.pick_marker_actor)
self.pick_marker_actor = None
self.highlight_signature = None
def clear_edit_preview(self, render: bool = True) -> None:
if self.edit_preview_timer is not None:
@@ -2138,7 +2165,7 @@ class WindowCoreMixin:
if polydata is None:
raise RuntimeError("当前显示网格里没有可复用的选中 Face 预览数据")
except Exception as exc:
self.statusBar().showMessage(f"拉预览不可用:{exc}")
self.statusBar().showMessage(f"伸/切除预览不可用:{exc}")
return
if distance >= 0:
color = (0.0, 0.86, 0.34)
@@ -2172,9 +2199,9 @@ class WindowCoreMixin:
if polydata is None:
polydata = self._cached_face_overlay_polydata(face_ids=[face_id], smooth=False)
if polydata is None:
raise RuntimeError("当前显示网格里没有可复用的薄壁预览数据")
raise RuntimeError("当前显示网格里没有可复用的壳体预览数据")
except Exception as exc:
self.statusBar().showMessage(f"薄壁厚度调整预览不可用:{exc}")
self.statusBar().showMessage(f"壳体厚度调整预览不可用:{exc}")
return
movement = _triple_or_none(plan.get("shell_desired_movement_vector")) if isinstance(plan, dict) else None
if movement is None and isinstance(plan, dict):
File diff suppressed because it is too large Load Diff