feat: 完善 STEP 编辑器最小系统和稳定性

This commit is contained in:
2026-07-28 14:05:14 +08:00
parent 26216064e0
commit 7c263d1e96
19 changed files with 3173 additions and 931 deletions
+384 -61
View File
@@ -87,7 +87,7 @@ class WindowStateMixin:
self.statusBar().showMessage(message)
return True
if self.scan_in_progress:
self.statusBar().showMessage("后台扫描中,请等待扫描完成后再执行该操作。")
self.statusBar().showMessage("扫描中,请等待扫描完成后再执行该操作。")
return True
return False
@@ -98,33 +98,229 @@ class WindowStateMixin:
def _update_action_states(self) -> None:
if not hasattr(self, "export_all_button"):
return
has_model = (
self.model is not None
and not self.operation_in_progress
and not self.scan_in_progress
and not self.load_in_progress
)
has_loaded_model = self.model is not None
busy = self.operation_in_progress or self.scan_in_progress or self.load_in_progress
has_model = has_loaded_model and not busy
selected_kind = self.selected_kind
self.export_all_button.setEnabled(has_model)
self.export_check_button.setEnabled(has_model)
has_selection = any(
item is not None
for item in (
self.selected_part_id,
self.selected_solid_id,
self.selected_face_id,
self.selected_edge_id,
)
)
has_part = self.selected_part_id is not None
has_solid = self.selected_solid_id is not None
has_face = self.selected_face_id is not None and selected_kind in {"face", "feature"}
has_edge = self.selected_edge_id is not None and selected_kind == "edge"
id_text = self.id_input.text().strip() if hasattr(self, "id_input") else ""
history_row = self.history_list.currentRow() if hasattr(self, "history_list") else -1
has_history = has_model and bool(getattr(self, "operation_history", []))
has_history_selection = has_history and 0 <= history_row < len(self.operation_history)
has_diff_preview = has_model and bool(getattr(self, "diff_actors", []))
has_measure_points = bool(getattr(self, "measure_point_a", None) or getattr(self, "measure_point_b", None))
has_measure_result = bool(getattr(self, "measure_point_a", None) and getattr(self, "measure_point_b", None))
pick_value = getattr(self, "current_info_values", {}).get("pick_position")
has_pick_position = self.selected_pick_position is not None or (
isinstance(pick_value, (tuple, list)) and len(pick_value) == 3
)
selected_id_text = self._selected_id_text() if hasattr(self, "_selected_id_text") else ""
has_current_info = bool(getattr(self, "current_info_text", ""))
wait_or_load_tip = "请先加载 STEP 文件,或等待当前后台任务完成。"
if hasattr(self, "open_button"):
self._set_control_state(
self.open_button,
not busy,
"选择并打开一个 .step 或 .stp 文件。",
"请等待当前后台任务完成后再打开其他 STEP 文件。",
)
self._set_control_state(
self.reload_button,
has_loaded_model and not busy,
"从磁盘重新读取当前 STEP 文件。",
wait_or_load_tip,
)
if hasattr(self, "part_tree"):
self._set_control_state(
self.part_tree,
has_model,
"从结构树中选择 Part、Solid 或装配节点。",
wait_or_load_tip,
)
if hasattr(self, "mode_combo"):
self._set_control_state(
self.mode_combo,
not busy,
"选择鼠标点击模型时要选什么对象。",
"请等待当前后台任务完成后再切换鼠标选择模式。",
)
if hasattr(self, "id_input"):
self._set_control_state(
self.id_input,
has_model,
"输入要选中的对象 ID。",
wait_or_load_tip,
)
self._set_control_state(
self.select_id_button,
has_model and bool(id_text),
"按当前选择模式跳转到输入的 ID。",
"请先加载模型并输入整数 ID。",
)
self._set_control_state(
self.export_all_button,
has_model,
"导出当前完整 STEP 模型。",
wait_or_load_tip,
)
self._set_control_state(
self.export_check_button,
has_model,
"检查当前导出对象的 B-Rep、数量、体积和包围盒等质量信息。",
wait_or_load_tip,
)
if hasattr(self, "repair_model_button"):
self._set_control_state(
self.repair_model_button,
has_model,
"对当前完整模型执行 ShapeFix 和同域面/边合并,并写入撤销历史。",
"请先加载 STEP 文件,或等待当前后台任务完成。",
wait_or_load_tip,
)
self._set_control_state(
self.repair_selected_button,
has_model and (self.selected_solid_id is not None or self.selected_part_id is not None),
"优先修复当前选中对象所属 solid;没有 solid 时修复所属零件,并写入撤销历史。",
"请先选择 part、solid、face 或 edge,或等待当前后台任务完成。",
has_model and (has_solid or has_part),
"优先修复当前选中对象所属Solid;没有Solid时修复所属零件,并写入撤销历史。",
"请先选择 Part、Solid、Face 或 Edge,或等待当前后台任务完成。",
)
self._set_control_state(
self.export_part_button,
has_model and has_part,
"导出当前选中对象所属零件。",
"请先选择 Part,或选择属于某个 Part 的 Solid、Face 或 Edge。",
)
self._set_control_state(
self.export_solid_button,
has_model and has_solid,
"导出当前选中对象所属 Solid。",
"请先选择 Solid,或选择属于某个 Solid 的 Face 或 Edge。",
)
self._set_control_state(
self.export_face_button,
has_model and has_face,
"导出当前选中的 Face 或同域面区域。",
"请先在 Face 或 Feature 模式下选择一个 Face。",
)
self._set_control_state(
self.export_feature_button,
has_model and selected_kind == "feature" and self.selected_face_id is not None,
"导出 Feature 模式识别到的局部特征区域。",
"请先切换到 Feature 模式并选择孔、槽、圆角或凸台候选。",
)
self._set_control_state(
self.export_edge_button,
has_model and has_edge,
"导出当前选中的 Edge。",
"请先切换到 Edge 模式并选择一条 Edge。",
)
if hasattr(self, "isolate_button"):
self._set_control_state(
self.isolate_button,
has_model and has_selection,
"只显示当前选中的对象或区域。",
"请先加载模型并选择 Part、Solid、Face、Edge 或 Feature。",
)
self._set_control_state(
self.fit_button,
has_model and has_selection,
"把相机对准当前选中对象。",
"请先加载模型并选择一个对象。",
)
self._set_control_state(
self.show_all_button,
has_model,
"恢复显示完整模型。",
wait_or_load_tip,
)
self._set_control_state(
self.show_internal_edges_checkbox,
has_model,
"显示或隐藏同域内部拓扑边。",
wait_or_load_tip,
)
if hasattr(self, "set_measure_a_button"):
measure_point_tip = "把当前选中对象的拾取点或中心设为测量点。"
self._set_control_state(
self.set_measure_a_button,
has_model and has_selection,
measure_point_tip,
"请先加载模型并选择一个对象。",
)
self._set_control_state(
self.set_measure_b_button,
has_model and has_selection,
measure_point_tip,
"请先加载模型并选择一个对象。",
)
self._set_control_state(
self.copy_measure_button,
has_measure_result,
"复制 A/B 两点距离和 X/Y/Z 差值。",
"请先设置测量点 A 和 B。",
)
self._set_control_state(
self.clear_measure_button,
has_measure_points,
"清除 A/B 测量点和 3D 测量线。",
"当前没有可清除的测量点。",
)
if hasattr(self, "history_list"):
self._set_control_state(
self.history_list,
has_history,
"点击历史记录可查看参数、定位目标并显示差异预览。",
"当前还没有编辑历史。",
)
self._set_control_state(
self.clear_diff_button,
has_diff_preview,
"清除当前显示的差异预览。",
"当前没有正在显示的差异预览。",
)
self._set_control_state(
self.export_diff_button,
has_history_selection,
"导出当前选中历史记录的差异报告。",
"请先在操作历史中选择一条记录。",
)
self._set_control_state(
self.export_history_button,
has_history,
"导出本次会话的完整编辑历史 JSON。",
"当前还没有可导出的编辑历史。",
)
if hasattr(self, "copy_id_button"):
self._set_control_state(
self.copy_id_button,
bool(selected_id_text),
"复制当前选中对象的 ID。",
"请先选择一个对象。",
)
self._set_control_state(
self.copy_pick_button,
has_pick_position,
"复制最近一次鼠标拾取到的三维坐标。",
"当前没有可复制的拾取坐标。",
)
self._set_control_state(
self.copy_info_button,
has_current_info,
"复制当前属性区里的完整文本。",
"当前属性区还没有可复制的信息。",
)
self.export_part_button.setEnabled(has_model and selected_kind == "part" and self.selected_part_id is not None)
self.export_solid_button.setEnabled(has_model and selected_kind == "solid" and self.selected_solid_id is not None)
self.export_face_button.setEnabled(has_model and selected_kind == "face" and self.selected_face_id is not None)
self.export_feature_button.setEnabled(has_model and selected_kind == "feature" and self.selected_face_id is not None)
self.export_edge_button.setEnabled(has_model and selected_kind == "edge" and self.selected_edge_id is not None)
self._update_edit_action_states(has_model)
def _update_edit_action_states(self, has_model: bool) -> None:
@@ -139,6 +335,7 @@ class WindowStateMixin:
has_face = self.selected_face_id is not None and self.selected_kind in {"face", "feature"}
has_edge = self.selected_edge_id is not None and self.selected_kind == "edge"
is_plane = has_face and surface == "plane"
is_shell_candidate = is_plane and action_info.get("shell_region_status") == "candidate"
is_cylinder = has_face and surface == "cylinder" and "diameter" in action_info
is_hole_or_groove = is_cylinder and feature_guess == "hole/groove candidate"
is_boss = is_cylinder and feature_guess == "boss/outer-round candidate"
@@ -158,111 +355,237 @@ class WindowStateMixin:
)
has_fillet_support = len(_int_values(action_info.get("feature_existing_fillet_support_face_ids"))) >= 2
is_line_edge = has_edge and curve == "line"
can_push = has_model and is_plane
can_resize_shell = has_model and is_shell_candidate
can_resize_hole = has_model and is_hole_or_groove
can_resize_slot = has_model and is_slot_or_half_hole
can_resize_boss = has_model and is_boss and is_full_cylinder
can_suppress_hole = has_model and is_hole_or_groove and is_full_cylinder
can_resize_depth = has_model and is_hole_or_groove and ((is_blind and has_bottom) or has_manual_bottom)
can_fillet_edge = has_model and is_line_edge
can_resize_existing_fillet = has_model and is_existing_fillet and has_fillet_support
can_chamfer_edge = has_model and is_line_edge
can_resize_edge_length = has_model and has_edge
can_transform_part = has_model and self.selected_part_id is not None
can_transform_solid = has_model and self.selected_solid_id is not None
self._set_control_state(
self.offset_input,
can_push,
"输入平面 Face 的推拉距离。",
"请先选择一个可推拉的平面 Face。",
)
self._set_control_state(
self.push_button,
has_model and is_plane,
"对当前平面 face 执行推拉。",
"请先选择一个平面 face,或在 Feature 模式下选择可推拉平面候选。",
can_push,
"对当前平面 Face 执行推拉。",
"请先选择一个平面 Face,或在 Feature 模式下选择可推拉平面候选。",
)
if hasattr(self, "resize_shell_thickness_button"):
self._set_control_state(
self.shell_thickness_input,
can_resize_shell,
"输入薄壁/壳体局部区域的目标厚度。",
"请先选择一个已识别到相对平面的薄壁/壳体平面候选。",
)
self._set_control_state(
self.resize_shell_thickness_button,
can_resize_shell,
"按目标厚度推拉当前薄壁/壳体平面区域。",
"请先选择一个已识别到相对平面的薄壁/壳体平面候选。",
)
self._set_control_state(
self.hole_diameter_input,
can_resize_hole,
"输入圆柱孔/槽的目标直径。",
"请先选择被识别为孔/槽候选的圆柱 Face。",
)
self._set_control_state(
self.resize_button,
has_model and is_hole_or_groove,
can_resize_hole,
"调整当前圆柱孔/槽候选的直径。",
"请先选择被识别为孔/槽候选的圆柱 face。",
"请先选择被识别为孔/槽候选的圆柱 Face。",
)
if hasattr(self, "resize_slot_button"):
self._set_control_state(
self.slot_width_input,
can_resize_slot,
"输入槽/半孔候选的目标宽度。",
"请先选择被识别为槽/半孔的部分圆柱 Face。",
)
self._set_control_state(
self.resize_slot_button,
has_model and is_slot_or_half_hole,
"按目标槽宽调整当前槽/半孔候选;第一版会换算为对应圆柱直径后执行。",
"请先选择被识别为槽/半孔的部分圆柱 face。",
can_resize_slot,
"按目标槽宽调整当前槽/半孔候选;当前版本会换算为对应圆柱直径后执行。",
"请先选择被识别为槽/半孔的部分圆柱 Face。",
)
self._set_control_state(
self.boss_diameter_input,
can_resize_boss,
"输入完整圆柱凸台候选的目标直径。",
"请先选择被识别为完整凸台/外圆候选的圆柱 Face。",
)
self._set_control_state(
self.resize_boss_button,
has_model and is_boss and is_full_cylinder,
can_resize_boss,
"调整当前完整圆柱凸台候选的直径。",
"请先选择被识别为完整凸台/外圆候选的圆柱 face。",
"请先选择被识别为完整凸台/外圆候选的圆柱 Face。",
)
self._set_control_state(
self.suppress_button,
has_model and is_hole_or_groove and is_full_cylinder,
can_suppress_hole,
"封堵当前完整圆柱孔候选。",
"请先选择接近完整圆柱的孔候选;半孔/槽不会放行。",
)
self._set_control_state(
self.hole_depth_input,
can_resize_hole,
"输入盲孔/盲槽的目标深度。",
"请先选择孔/槽圆柱面。",
)
self._set_control_state(
self.hole_bottom_face_input,
can_resize_hole,
"必要时输入底面 Face ID,用来明确盲孔/盲槽深度。",
"请先选择孔/槽圆柱面。",
)
self._set_control_state(
self.resize_depth_button,
has_model and is_hole_or_groove and ((is_blind and has_bottom) or has_manual_bottom),
can_resize_depth,
"调整当前盲孔/盲槽深度;自动底面不稳定时可手动填写底面 Face ID。",
"请先选择孔/槽圆柱面;如果没有自动识别到底面,请填写底面 Face ID。",
)
self._set_control_state(
self.edge_fillet_radius_input,
can_fillet_edge or can_resize_existing_fillet,
"输入新圆角或已有圆角的目标半径。",
"请先选择直线Edge,或选择可修改的已有圆角 Face。",
)
self._set_control_state(
self.fillet_edge_button,
has_model and is_line_edge,
"给当前直线 edge 添加新圆角。",
"请先选择一条直线 edge。",
can_fillet_edge,
"给当前直线Edge添加新圆角。",
"请先选择一条直线Edge。",
)
self._set_control_state(
self.resize_existing_fillet_button,
has_model and is_existing_fillet and has_fillet_support,
can_resize_existing_fillet,
"尝试修改当前已有圆角/倒圆候选的半径。",
"请先选择一个已有圆角/倒圆候选 face第一版需要识别到至少两个支撑 face。",
"请先选择一个已有圆角/倒圆候选 Face当前版本需要识别到至少两个支撑 Face。",
)
self._set_control_state(
self.edge_chamfer_distance_input,
can_chamfer_edge,
"输入当前直线Edge的倒角距离。",
"请先选择一条直线Edge。",
)
self._set_control_state(
self.chamfer_edge_button,
has_model and is_line_edge,
"给当前直线 edge 添加倒角。",
"请先选择一条直线 edge。",
can_chamfer_edge,
"给当前直线Edge添加倒角。",
"请先选择一条直线Edge。",
)
self._set_control_state(
self.edge_target_length_input,
can_resize_edge_length,
"输入当前Edge的目标长度。",
"请先选择一条Edge。",
)
self._set_control_state(
self.edge_length_anchor_combo,
can_resize_edge_length,
"选择修改Edge长度时尽量固定哪一端。",
"请先选择一条Edge。",
)
self._set_control_state(
self.resize_edge_length_button,
has_model and has_edge,
"直接修改当前 edge 的长度;直线优先端面推拉,其他 edge 使用高风险几何 fallback",
"请先选择一条 edge。",
can_resize_edge_length,
"修改当前Edge的长度;直线Edge优先局部形变,圆/椭圆/平面曲线优先用局部或径向策略",
"请先选择一条Edge。",
)
for widget in (self.translate_x_input, self.translate_y_input, self.translate_z_input):
self._set_control_state(
widget,
can_transform_part or can_transform_solid,
"输入选中零件或 Solid 的平移距离。",
"请先选择一个零件、Solid,或其所属 Face/Edge。",
)
self._set_control_state(
self.translate_part_button,
has_model and self.selected_part_id is not None,
can_transform_part,
"平移当前选中对象所属零件。",
"请先选择一个零件,或选择属于某个零件的对象。",
)
self._set_control_state(
self.rotate_part_button,
has_model and self.selected_part_id is not None,
can_transform_part,
"旋转当前选中对象所属零件。",
"请先选择一个零件,或选择属于某个零件的对象。",
)
self._set_control_state(
self.rotate_axis_combo,
can_transform_part or can_transform_solid,
"选择选中零件或 Solid 的旋转轴。",
"请先选择一个零件、Solid,或其所属 Face/Edge。",
)
self._set_control_state(
self.rotate_angle_input,
can_transform_part or can_transform_solid,
"输入选中零件或 Solid 的旋转角度。",
"请先选择一个零件、Solid,或其所属 Face/Edge。",
)
self._set_control_state(
self.translate_solid_button,
has_model and self.selected_solid_id is not None,
"平移当前选中对象所属 solid。",
"请先选择一个 solid,或选择属于某个 solid 的 face/edge。",
can_transform_solid,
"平移当前选中对象所属 Solid。",
"请先选择一个 Solid,或选择属于某个 Solid 的 Face/Edge。",
)
self._set_control_state(
self.rotate_solid_button,
has_model and self.selected_solid_id is not None,
"旋转当前选中对象所属 solid。",
"请先选择一个 solid,或选择属于某个 solid 的 face/edge。",
can_transform_solid,
"旋转当前选中对象所属 Solid。",
"请先选择一个 Solid,或选择属于某个 Solid 的 Face/Edge。",
)
self._set_control_state(
self.cylinders_button,
has_model,
"扫描当前模型中的圆柱候选。",
"扫描当前模型中的圆柱候选。",
"请先加载 STEP 文件。",
)
if hasattr(self, "editable_refresh_button"):
self._set_control_state(
self.editable_refresh_button,
has_model,
"扫描第一版可编辑对象。",
"扫描可编辑对象。",
"请先加载 STEP 文件,或等待当前后台任务完成。",
)
self._set_control_state(
self.editable_deep_scan_button,
has_model,
"深度扫描更多第一版可编辑对象。",
"深度扫描更多可编辑对象。",
"请先加载 STEP 文件,或等待当前后台任务完成。",
)
self._set_control_state(
self.editable_table,
has_model and self.editable_table.rowCount() > 0,
"点击一行可选中对应对象并填入相关编辑参数。",
"当前没有可编辑对象列表;请先点击扫描对象。",
)
if hasattr(self, "candidate_filter_combo"):
has_cylinder_candidates = has_model and bool(getattr(self, "cylinder_candidates_loaded", False))
self._set_control_state(
self.candidate_filter_combo,
has_cylinder_candidates,
"筛选已扫描的圆柱面候选。",
"请先点击扫描圆柱面。",
)
self._set_control_state(
self.cylinder_table,
has_model and self.cylinder_table.rowCount() > 0,
"点击一行可选中对应圆柱 Face。",
"当前没有圆柱面候选列表;请先点击扫描圆柱面。",
)
self._set_control_state(
self.undo_button,
has_model and bool(self.undo_stack),
@@ -292,7 +615,7 @@ class WindowStateMixin:
return self.model.feature_info(self.selected_face_id)
if self.selected_kind == "face" and self.selected_face_id is not None:
info = self.model.face_info(self.selected_face_id)
if info.get("surface") == "cylinder":
if info.get("surface") in {"cylinder", "plane"}:
return self.model.feature_info(self.selected_face_id)
return info
if self.selected_kind == "edge" and self.selected_edge_id is not None:
@@ -330,23 +653,23 @@ class WindowStateMixin:
face_ids = self.model.face_region_ids(resolved_face_id)
self._highlight_faces(face_ids=face_ids)
located = True
region_note = f";同域面区域 {len(face_ids)} face" if len(face_ids) > 1 else ""
region_note = f";同域面区域 {len(face_ids)}Face" if len(face_ids) > 1 else ""
id_note = (
f"逻辑 Face ID {current_logical_id}"
if record.target_logical_id is not None
else f"face {record.target_id}"
else f"Face {record.target_id}"
)
if resolved_face_id != record.target_id:
id_note += f"(当前拓扑 face {resolved_face_id}"
id_note += f"(当前拓扑Face {resolved_face_id}"
locator_note = (
f"定位: 已尝试高亮当前模型中的 {id_note}{region_note}"
"布尔编辑后 face ID 可能发生语义变化,请结合拾取点确认。"
"布尔编辑后Face ID可能发生语义变化,请结合拾取点确认。"
)
else:
if record.target_logical_id is not None:
locator_note = f"定位: 原目标逻辑 Face ID {record.target_logical_id} 在当前模型索引中已经不存在。"
else:
locator_note = f"定位: 原目标 face {record.target_id} 在当前模型索引中已经不存在。"
locator_note = f"定位: 原目标Face {record.target_id} 在当前模型索引中已经不存在。"
elif record.target_kind == "edge" and record.target_id is not None:
if 0 <= record.target_id < len(self.model.edges):
@@ -361,11 +684,11 @@ class WindowStateMixin:
self._highlight_edge(record.target_id)
located = True
locator_note = (
f"定位: 已尝试高亮当前模型中的 edge {record.target_id}"
"布尔/倒圆编辑后 edge ID 可能发生语义变化,请结合拾取点确认。"
f"定位: 已尝试高亮当前模型中的Edge {record.target_id}"
"布尔/倒圆编辑后Edge ID可能发生语义变化,请结合拾取点确认。"
)
else:
locator_note = f"定位: 原目标 edge {record.target_id} 在当前模型索引中已经不存在。"
locator_note = f"定位: 原目标Edge {record.target_id} 在当前模型索引中已经不存在。"
if record.pick_position is not None:
self._show_pick_marker(record.pick_position)