feat: 完善特征编辑和选择稳定性

This commit is contained in:
2026-07-29 15:43:28 +08:00
parent 720c2dc970
commit c937e13d43
13 changed files with 8294 additions and 290 deletions
+8 -1
View File
@@ -1,6 +1,13 @@
from __future__ import annotations
from .app import StepEditorWindow, main
from .model import StepModel
__all__ = ["StepEditorWindow", "StepModel", "main"]
def __getattr__(name: str):
if name in {"StepEditorWindow", "main"}:
from .app import StepEditorWindow, main
return {"StepEditorWindow": StepEditorWindow, "main": main}[name]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+223 -37
View File
@@ -103,11 +103,19 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.hover_face_actor = None
self.hover_edge_actor = None
self.hover_signature: tuple[str, int] | None = None
self.hover_interval_ms = 85
self.hover_move_threshold_px = 5
self.hover_interval_ms = 140
self.hover_move_threshold_px = 8
self.pending_hover_position: tuple[int, int] | None = None
self.last_hover_pick_position: tuple[int, int] | None = None
self.camera_interaction_active = False
self.edge_actor_visible_before_interaction = True
self.model_interpolation_before_interaction: int | None = None
self.pointer_button_down = False
self.left_button_press_position: tuple[int, int] | None = None
self.left_button_dragged = False
self.left_click_drag_threshold_px = 6
self.skip_next_vtk_left_press = False
self.skip_next_vtk_left_release = False
self.pick_marker_actor = None
self.edit_preview_actor = None
self.edit_preview_actors: list[object] = []
@@ -143,6 +151,11 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.cylinder_candidate_cache: list[dict[str, object]] = []
self.cylinder_candidates_loaded = False
self.operation_in_progress = False
self.edit_busy_timer = QTimer(self)
self.edit_busy_timer.timeout.connect(self._pulse_edit_busy_status)
self.edit_busy_base_text = ""
self.edit_busy_started_at: datetime | None = None
self.edit_busy_phase = 0
self.edit_thread: QThread | None = None
self.edit_worker: EditWorker | None = None
self.pending_edit_context: dict[str, object] | None = None
@@ -158,6 +171,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.load_refine_worker: LoadWorker | None = None
self.pending_load_path: Path | None = None
self.initial_load_deflection = 2.0
self.edit_result_deflection = 1.6
self.last_id_kind = "Face"
self.property_editor_updating = False
self.property_editor_specs: list[dict[str, object]] = []
@@ -394,23 +408,76 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
font-weight: 500;
}
QPushButton#propertyRowEditButton {
background: #fff3d9;
border: 1px solid #d97706;
background: #ea580c;
border: 1px solid #c2410c;
border-radius: 5px;
color: #92400e;
color: #ffffff;
font-weight: 700;
min-height: 18px;
padding: 1px 6px;
}
QPushButton#propertyRowEditButton:hover {
background: #ffe6b8;
border-color: #b45309;
background: #f97316;
border-color: #9a3412;
}
QPushButton#propertyRowEditButton:pressed {
background: #9a3412;
border-color: #7c2d12;
color: #ffffff;
padding-top: 2px;
padding-left: 7px;
}
QPushButton#propertyRowEditButton[changed="true"] {
background: #dcfce7;
border: 1px solid #16a34a;
color: #14532d;
}
QPushButton#propertyRowEditButton[changed="true"]:hover {
background: #bbf7d0;
border-color: #15803d;
}
QPushButton#propertyRowEditButton:disabled {
background: #eef2f6;
border: 1px dashed #bcc7d4;
color: #8f99a8;
}
QLineEdit#propertyTargetEditor {
background: #fff7ed;
border: 2px solid #f97316;
border-radius: 5px;
color: #111827;
min-height: 18px;
padding: 0px 5px;
selection-background-color: #bfdbfe;
}
QLineEdit#propertyTargetEditor:hover {
background: #ffedd5;
border-color: #ea580c;
}
QLineEdit#propertyTargetEditor:focus {
background: #ffffff;
border: 2px solid #2563eb;
padding: 0px 4px;
}
QPushButton#propertyExpandBar {
background: #f8fafc;
border: 1px solid #d7dee8;
border-radius: 4px;
color: #475569;
font-weight: 600;
min-height: 14px;
max-height: 20px;
padding: 1px 6px;
}
QPushButton#propertyExpandBar:hover {
background: #eef6ff;
border-color: #9dbbe0;
color: #1e3a8a;
}
QPushButton#propertyExpandBar:pressed {
background: #dbeafe;
padding-top: 2px;
}
QLineEdit:disabled,
QComboBox:disabled,
QPlainTextEdit:disabled {
@@ -743,14 +810,24 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.property_table.itemChanged.connect(self._on_property_table_item_changed)
object_edit_layout.addWidget(self.property_table)
self.property_expand_button = QPushButton("展开全部属性")
self.property_expand_button.setObjectName("propertyExpandBar")
help_tip(self.property_expand_button, "当前选中对象属性较多时,默认只显示前 6 行;点击这里展开或收起属性表。")
self.property_expand_button.clicked.connect(self.toggle_property_table_expanded)
self.property_expand_button.setVisible(False)
self.property_expand_button.setMaximumHeight(22)
self.property_expand_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
object_edit_layout.addWidget(self.property_expand_button)
property_action_row = QHBoxLayout()
property_action_row.setContentsMargins(0, 0, 0, 0)
self.apply_property_button = QPushButton("参数化建模")
help_tip(self.apply_property_button, "按当前属性表里修改过的目标值执行参数化建模;一次只执行一个几何修改,成功后可撤销。")
self.apply_property_button.clicked.connect(self.apply_current_property_edit)
object_edit_layout.addWidget(self.apply_property_button)
self.quick_export_all_button = QPushButton("导出当前完整STEP")
help_tip(self.quick_export_all_button, "把当前编辑后的完整模型导出为 STEP 文件。导出前会做基础质量检查。")
self.quick_export_all_button.clicked.connect(self.export_all)
property_action_row.addWidget(self.apply_property_button)
property_action_row.addWidget(self.quick_export_all_button)
object_edit_layout.addLayout(property_action_row)
edit_box = QGroupBox(panel)
edit_box.setObjectName("editSection")
@@ -770,6 +847,18 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.hole_diameter_input = QLineEdit("")
help_tip(self.hole_diameter_input, "圆柱孔/槽的目标直径。选中候选后会自动填一个参考值,可以手动改。")
edit_layout.addWidget(self.hole_diameter_input, 2, 1)
self.hole_center_x_input = QLineEdit("", edit_box)
self.hole_center_y_input = QLineEdit("", edit_box)
self.hole_center_z_input = QLineEdit("", edit_box)
for hole_center_input in (
self.hole_center_x_input,
self.hole_center_y_input,
self.hole_center_z_input,
):
hole_center_input.setVisible(False)
help_tip(self.hole_center_x_input, "圆柱孔轴心目标 X 坐标。当前选中对象表会使用这个隐藏输入执行孔位置修改。")
help_tip(self.hole_center_y_input, "圆柱孔轴心目标 Y 坐标。当前选中对象表会使用这个隐藏输入执行孔位置修改。")
help_tip(self.hole_center_z_input, "圆柱孔轴心目标 Z 坐标。当前选中对象表会使用这个隐藏输入执行孔位置修改。")
self.resize_button = QPushButton("调整圆柱孔径")
help_tip(self.resize_button, "修改孔或圆柱槽的直径。扩大时切削,缩小时会先补料再重切。")
self.resize_button.clicked.connect(self.resize_hole)
@@ -784,21 +873,75 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.resize_slot_button.clicked.connect(self.resize_slot_width)
edit_layout.addWidget(self.resize_slot_button, 5, 0, 1, 2)
edit_layout.addWidget(QLabel("凸台直径"), 6, 0)
edit_layout.addWidget(QLabel("槽/半孔深度"), 6, 0)
self.slot_depth_input = QLineEdit("")
help_tip(self.slot_depth_input, "槽或半孔的目标凹入深度。程序会按当前圆弧角度换算成对应圆柱直径来执行。")
edit_layout.addWidget(self.slot_depth_input, 6, 1)
self.resize_slot_depth_button = QPushButton("调整槽/半孔深度")
help_tip(self.resize_slot_depth_button, "修改已识别槽/半孔候选的凹入深度。当前版本是几何近似,失败会回滚。")
self.resize_slot_depth_button.clicked.connect(self.resize_slot_depth)
edit_layout.addWidget(self.resize_slot_depth_button, 7, 0, 1, 2)
self.slot_arc_length_input = QLineEdit("", edit_box)
self.slot_arc_length_input.setVisible(False)
help_tip(self.slot_arc_length_input, "槽或半孔的目标圆弧长度。当前选中对象表会使用这个隐藏输入执行圆弧长度修改。")
self.slot_angular_span_input = QLineEdit("", edit_box)
self.slot_angular_span_input.setVisible(False)
help_tip(self.slot_angular_span_input, "槽或半孔的目标圆弧角度。当前选中对象表会使用这个隐藏输入执行角度修改。")
self.slot_pair_face_input = QLineEdit("", edit_box)
self.slot_pair_face_input.setVisible(False)
help_tip(self.slot_pair_face_input, "长圆槽/槽孔的另一个半圆端 Face ID。自动配对不准时可在当前选中对象表里手动设置。")
self.slot_total_length_input = QLineEdit("", edit_box)
self.slot_total_length_input.setVisible(False)
help_tip(self.slot_total_length_input, "长圆槽/槽孔的目标总长度。只有识别到成对半圆槽端时才会启用。")
self.slot_center_x_input = QLineEdit("", edit_box)
self.slot_center_x_input.setVisible(False)
self.slot_center_y_input = QLineEdit("", edit_box)
self.slot_center_y_input.setVisible(False)
self.slot_center_z_input = QLineEdit("", edit_box)
self.slot_center_z_input.setVisible(False)
help_tip(self.slot_center_x_input, "槽/半孔目标轴心坐标 X。当前选中对象表会使用这个隐藏输入执行轴心移动。")
help_tip(self.slot_center_y_input, "槽/半孔目标轴心坐标 Y。当前选中对象表会使用这个隐藏输入执行轴心移动。")
help_tip(self.slot_center_z_input, "槽/半孔目标轴心坐标 Z。当前选中对象表会使用这个隐藏输入执行轴心移动。")
edit_layout.addWidget(QLabel("凸台直径"), 8, 0)
self.boss_diameter_input = QLineEdit("")
help_tip(self.boss_diameter_input, "圆柱凸台的目标直径。选中凸台候选后会自动填一个参考值。")
edit_layout.addWidget(self.boss_diameter_input, 6, 1)
edit_layout.addWidget(self.boss_diameter_input, 8, 1)
self.boss_height_input = QLineEdit("", edit_box)
self.boss_height_input.setVisible(False)
help_tip(self.boss_height_input, "圆柱凸台的目标高度。当前选中对象表会使用这个隐藏输入执行高度修改。")
self.boss_center_x_input = QLineEdit("", edit_box)
self.boss_center_x_input.setVisible(False)
self.boss_center_y_input = QLineEdit("", edit_box)
self.boss_center_y_input.setVisible(False)
self.boss_center_z_input = QLineEdit("", edit_box)
self.boss_center_z_input.setVisible(False)
help_tip(self.boss_center_x_input, "圆柱凸台目标轴心坐标 X。当前选中对象表会使用这个隐藏输入执行轴心移动。")
help_tip(self.boss_center_y_input, "圆柱凸台目标轴心坐标 Y。当前选中对象表会使用这个隐藏输入执行轴心移动。")
help_tip(self.boss_center_z_input, "圆柱凸台目标轴心坐标 Z。当前选中对象表会使用这个隐藏输入执行轴心移动。")
self.cone_reference_radius_input = QLineEdit("", edit_box)
self.cone_reference_radius_input.setVisible(False)
help_tip(self.cone_reference_radius_input, "圆锥面的目标参考半径。当前选中对象表会使用这个隐藏输入执行圆锥面修改。")
self.sphere_radius_input = QLineEdit("", edit_box)
self.sphere_radius_input.setVisible(False)
help_tip(self.sphere_radius_input, "球面的目标半径。当前选中对象表会使用这个隐藏输入执行球面修改。")
self.torus_radius_input = QLineEdit("", edit_box)
self.torus_radius_input.setVisible(False)
help_tip(self.torus_radius_input, "环面的目标半径。当前选中对象表会使用这个隐藏输入执行环面修改。")
self.face_area_input = QLineEdit("", edit_box)
self.face_area_input.setVisible(False)
help_tip(self.face_area_input, "复杂曲面的目标Face面积。当前选中对象表会使用这个隐藏输入执行兜底面积缩放。")
self.resize_boss_button = QPushButton("调整圆柱凸台直径")
help_tip(self.resize_boss_button, "修改完整圆柱凸台直径。变大会加料,变小会重建凸台区域。")
self.resize_boss_button.clicked.connect(self.resize_boss)
edit_layout.addWidget(self.resize_boss_button, 7, 0, 1, 2)
edit_layout.addWidget(self.resize_boss_button, 9, 0, 1, 2)
self.suppress_button = QPushButton("封堵圆柱孔")
help_tip(self.suppress_button, "用补料体填住完整圆柱孔。适合通孔/盲孔,不适合半孔或槽。")
self.suppress_button.clicked.connect(self.suppress_hole)
edit_layout.addWidget(self.suppress_button, 8, 0, 1, 2)
edit_layout.addWidget(self.suppress_button, 10, 0, 1, 2)
edit_layout.addWidget(QLabel("孔深度"), 9, 0)
edit_layout.addWidget(QLabel("孔深度"), 11, 0)
depth_inputs = QWidget()
depth_layout = QHBoxLayout(depth_inputs)
depth_layout.setContentsMargins(0, 0, 0, 0)
@@ -810,41 +953,72 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.hole_bottom_face_input.textChanged.connect(lambda _text: self._update_action_states())
depth_layout.addWidget(self.hole_depth_input, stretch=2)
depth_layout.addWidget(self.hole_bottom_face_input, stretch=1)
edit_layout.addWidget(depth_inputs, 9, 1)
self.resize_depth_button = QPushButton("调整盲孔深度")
edit_layout.addWidget(depth_inputs, 11, 1)
self.resize_depth_button = QPushButton("调整盲孔/盲槽深度")
help_tip(self.resize_depth_button, "加深或变浅盲孔/盲槽。需要识别到底面,或手动填写底面 Face ID。")
self.resize_depth_button.clicked.connect(self.resize_hole_depth)
edit_layout.addWidget(self.resize_depth_button, 10, 0, 1, 2)
edit_layout.addWidget(self.resize_depth_button, 12, 0, 1, 2)
edit_layout.addWidget(QLabel("圆角半径"), 11, 0)
edit_layout.addWidget(QLabel("圆角半径"), 13, 0)
self.edge_fillet_radius_input = QLineEdit("")
help_tip(self.edge_fillet_radius_input, "新圆角或已有圆角的目标半径。选中Edge/圆角候选后会自动填参考值。")
edit_layout.addWidget(self.edge_fillet_radius_input, 11, 1)
edit_layout.addWidget(self.edge_fillet_radius_input, 13, 1)
self.fillet_edge_button = QPushButton("给Edge添加圆角")
help_tip(self.fillet_edge_button, "给当前直线Edge新增圆角。不是修改已有圆角;已有圆角请用下面那个按钮。")
self.fillet_edge_button.clicked.connect(self.fillet_edge)
edit_layout.addWidget(self.fillet_edge_button, 12, 0, 1, 2)
edit_layout.addWidget(self.fillet_edge_button, 14, 0, 1, 2)
self.resize_existing_fillet_button = QPushButton("修改已有圆角半径")
help_tip(self.resize_existing_fillet_button, "尝试修改已识别圆角面的半径。会先移除原圆角再重建,复杂圆角可能失败并回滚。")
self.resize_existing_fillet_button.clicked.connect(self.resize_existing_fillet)
edit_layout.addWidget(self.resize_existing_fillet_button, 13, 0, 1, 2)
edit_layout.addWidget(self.resize_existing_fillet_button, 15, 0, 1, 2)
edit_layout.addWidget(QLabel("倒角距离"), 14, 0)
edit_layout.addWidget(QLabel("倒角距离"), 16, 0)
self.edge_chamfer_distance_input = QLineEdit("")
help_tip(self.edge_chamfer_distance_input, "给Edge添加倒角时使用的距离。选中直线Edge后会填一个较小参考值。")
edit_layout.addWidget(self.edge_chamfer_distance_input, 14, 1)
self.edge_chamfer_distance1_input = QLineEdit("", edit_box)
self.edge_chamfer_distance2_input = QLineEdit("", edit_box)
self.edge_chamfer_angle_distance_input = QLineEdit("", edit_box)
self.edge_chamfer_angle_degrees_input = QLineEdit("", edit_box)
self.edge_chamfer_reference_face_input = QLineEdit("", edit_box)
self.edge_chamfer_distance1_input.setVisible(False)
self.edge_chamfer_distance2_input.setVisible(False)
self.edge_chamfer_angle_distance_input.setVisible(False)
self.edge_chamfer_angle_degrees_input.setVisible(False)
self.edge_chamfer_reference_face_input.setVisible(False)
edit_layout.addWidget(self.edge_chamfer_distance_input, 16, 1)
self.chamfer_edge_button = QPushButton("给Edge添加倒角")
help_tip(self.chamfer_edge_button, "给当前直线Edge新增对称倒角。会先预览,再后台执行,失败会回滚。")
self.chamfer_edge_button.clicked.connect(self.chamfer_edge)
edit_layout.addWidget(self.chamfer_edge_button, 15, 0, 1, 2)
edit_layout.addWidget(self.chamfer_edge_button, 17, 0, 1, 2)
edit_layout.addWidget(QLabel("Edge目标长度"), 16, 0)
edit_layout.addWidget(QLabel("Edge目标长度"), 18, 0)
edge_length_inputs = QWidget()
edge_length_layout = QHBoxLayout(edge_length_inputs)
edge_length_layout.setContentsMargins(0, 0, 0, 0)
self.edge_target_length_input = QLineEdit("")
help_tip(self.edge_target_length_input, "当前Edge的目标长度。选中Edge后会自动填当前长度,改成新长度再执行。")
self.edge_start_x_input = QLineEdit("", edit_box)
self.edge_start_y_input = QLineEdit("", edit_box)
self.edge_start_z_input = QLineEdit("", edit_box)
self.edge_center_x_input = QLineEdit("", edit_box)
self.edge_center_y_input = QLineEdit("", edit_box)
self.edge_center_z_input = QLineEdit("", edit_box)
self.edge_end_x_input = QLineEdit("", edit_box)
self.edge_end_y_input = QLineEdit("", edit_box)
self.edge_end_z_input = QLineEdit("", edit_box)
for endpoint_input in (
self.edge_start_x_input,
self.edge_start_y_input,
self.edge_start_z_input,
self.edge_center_x_input,
self.edge_center_y_input,
self.edge_center_z_input,
self.edge_end_x_input,
self.edge_end_y_input,
self.edge_end_z_input,
):
endpoint_input.setVisible(False)
self.edge_length_anchor_combo = NoWheelComboBox()
self.edge_length_anchor_combo.addItem("自动", "auto")
self.edge_length_anchor_combo.addItem("中心", "center")
@@ -858,31 +1032,31 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
)
edge_length_layout.addWidget(self.edge_target_length_input, stretch=2)
edge_length_layout.addWidget(self.edge_length_anchor_combo, stretch=1)
edit_layout.addWidget(edge_length_inputs, 16, 1)
edit_layout.addWidget(edge_length_inputs, 18, 1)
self.resize_edge_length_button = QPushButton("修改Edge长度")
help_tip(
self.resize_edge_length_button,
"修改当前Edge长度。直线Edge优先局部形变;圆、椭圆和平面曲线会优先用局部/径向策略,必要时再用后备缩放。",
)
self.resize_edge_length_button.clicked.connect(self.resize_any_edge_length)
edit_layout.addWidget(self.resize_edge_length_button, 17, 0, 1, 2)
edit_layout.addWidget(self.resize_edge_length_button, 19, 0, 1, 2)
edit_layout.addWidget(QLabel("薄壁厚度"), 18, 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, 18, 1)
edit_layout.addWidget(self.shell_thickness_input, 20, 1)
self.resize_shell_thickness_button = QPushButton("调整薄壁厚度")
help_tip(
self.resize_shell_thickness_button,
"移动当前平面区域来达到目标厚度。当前版本基于相对平面估算,执行前会预览并可回滚。",
)
self.resize_shell_thickness_button.clicked.connect(self.resize_shell_thickness)
edit_layout.addWidget(self.resize_shell_thickness_button, 19, 0, 1, 2)
edit_layout.addWidget(self.resize_shell_thickness_button, 21, 0, 1, 2)
edit_layout.addWidget(QLabel("平移 X/Y/Z"), 20, 0)
edit_layout.addWidget(QLabel("平移 X/Y/Z"), 22, 0)
translate_inputs = QWidget()
translate_layout = QHBoxLayout(translate_inputs)
translate_layout.setContentsMargins(0, 0, 0, 0)
@@ -898,17 +1072,17 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
translate_layout.addWidget(self.translate_x_input)
translate_layout.addWidget(self.translate_y_input)
translate_layout.addWidget(self.translate_z_input)
edit_layout.addWidget(translate_inputs, 20, 1)
edit_layout.addWidget(translate_inputs, 22, 1)
self.translate_part_button = QPushButton("平移选中零件")
help_tip(self.translate_part_button, "按上面的 X/Y/Z 距离移动当前零件。会写入历史,可撤销。")
self.translate_part_button.clicked.connect(self.translate_selected_part)
self.translate_solid_button = QPushButton("平移选中Solid")
help_tip(self.translate_solid_button, "按上面的 X/Y/Z 距离移动当前Solid。单Solid零件中相当于移动整个零件。")
self.translate_solid_button.clicked.connect(self.translate_selected_solid)
edit_layout.addWidget(self.translate_part_button, 21, 0, 1, 2)
edit_layout.addWidget(self.translate_solid_button, 22, 0, 1, 2)
edit_layout.addWidget(self.translate_part_button, 23, 0, 1, 2)
edit_layout.addWidget(self.translate_solid_button, 24, 0, 1, 2)
edit_layout.addWidget(QLabel("旋转轴/角度"), 23, 0)
edit_layout.addWidget(QLabel("旋转轴/角度"), 25, 0)
rotate_inputs = QWidget()
rotate_layout = QHBoxLayout(rotate_inputs)
rotate_layout.setContentsMargins(0, 0, 0, 0)
@@ -921,15 +1095,27 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
help_tip(self.rotate_angle_input, "旋转角度,单位是度。正负号决定旋转方向。")
rotate_layout.addWidget(self.rotate_axis_combo)
rotate_layout.addWidget(self.rotate_angle_input)
edit_layout.addWidget(rotate_inputs, 23, 1)
edit_layout.addWidget(rotate_inputs, 25, 1)
self.scale_target_diagonal_input = QLineEdit("", edit_box)
self.scale_target_diagonal_input.setVisible(False)
help_tip(self.scale_target_diagonal_input, "零件或Solid的目标包围盒对角线。当前选中对象表会使用这个隐藏输入执行整体等比缩放。")
self.scale_x_size_input = QLineEdit("", edit_box)
self.scale_x_size_input.setVisible(False)
help_tip(self.scale_x_size_input, "零件或Solid的目标 X 向包围盒尺寸。")
self.scale_y_size_input = QLineEdit("", edit_box)
self.scale_y_size_input.setVisible(False)
help_tip(self.scale_y_size_input, "零件或Solid的目标 Y 向包围盒尺寸。")
self.scale_z_size_input = QLineEdit("", edit_box)
self.scale_z_size_input.setVisible(False)
help_tip(self.scale_z_size_input, "零件或Solid的目标 Z 向包围盒尺寸。")
self.rotate_part_button = QPushButton("旋转选中零件")
help_tip(self.rotate_part_button, "绕所选轴旋转当前零件。会写入历史,可撤销。")
self.rotate_part_button.clicked.connect(self.rotate_selected_part)
self.rotate_solid_button = QPushButton("旋转选中Solid")
help_tip(self.rotate_solid_button, "绕所选轴旋转当前Solid。单Solid零件中相当于旋转整个零件。")
self.rotate_solid_button.clicked.connect(self.rotate_selected_solid)
edit_layout.addWidget(self.rotate_part_button, 24, 0, 1, 2)
edit_layout.addWidget(self.rotate_solid_button, 25, 0, 1, 2)
edit_layout.addWidget(self.rotate_part_button, 26, 0, 1, 2)
edit_layout.addWidget(self.rotate_solid_button, 27, 0, 1, 2)
panel_layout.addWidget(self.object_edit_box)
if ENABLE_EXPORT_PANEL:
+1362 -13
View File
File diff suppressed because it is too large Load Diff
+68 -3
View File
@@ -8,7 +8,7 @@ from OCC.Core.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Defeaturing
from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.BOPAlgo import BOPAlgo_GlueFull
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_GTransform, BRepBuilderAPI_Transform
from OCC.Core.BRepCheck import BRepCheck_Analyzer
from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier
from OCC.Core.BRepGProp import brepgprop
@@ -34,7 +34,7 @@ from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopLoc import TopLoc_Location
from OCC.Core.TopoDS import TopoDS_Compound, TopoDS_Shape, topods
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
from OCC.Core.gp import gp_Ax1, gp_Dir, gp_Pnt, gp_Trsf, gp_Vec
from OCC.Core.gp import gp_Ax1, gp_Dir, gp_GTrsf, gp_Pnt, gp_Trsf, gp_Vec, gp_XYZ
from OCC.Extend.TopologyUtils import TopologyExplorer
from .constants import CURVE_TYPES, ORIENTATION_TYPES
@@ -341,6 +341,58 @@ def _rotated_shape(
return BRepBuilderAPI_Transform(shape, trsf, True).Shape()
def _scaled_shape(shape: TopoDS_Shape, scale: float, center: tuple[float, float, float]) -> TopoDS_Shape:
if abs(scale - 1.0) <= 1e-12:
return shape
if scale <= 0:
raise ValueError("Scale factor must be greater than 0.")
trsf = gp_Trsf()
trsf.SetScale(gp_Pnt(*center), float(scale))
return BRepBuilderAPI_Transform(shape, trsf, True).Shape()
def _axis_scaled_shape(
shape: TopoDS_Shape,
axis_name: str,
scale: float,
center: tuple[float, float, float],
) -> TopoDS_Shape:
if abs(scale - 1.0) <= 1e-12:
return shape
if scale <= 0:
raise ValueError("Scale factor must be greater than 0.")
axis = axis_name.upper()
axis_index = {"X": 0, "Y": 1, "Z": 2}.get(axis)
if axis_index is None:
raise ValueError("Scale axis must be X, Y or Z.")
matrix = [
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
]
matrix[axis_index][axis_index] = float(scale)
cx, cy, cz = center
moved_center = (
matrix[0][0] * cx + matrix[0][1] * cy + matrix[0][2] * cz,
matrix[1][0] * cx + matrix[1][1] * cy + matrix[1][2] * cz,
matrix[2][0] * cx + matrix[2][1] * cy + matrix[2][2] * cz,
)
translation = (cx - moved_center[0], cy - moved_center[1], cz - moved_center[2])
transform = gp_GTrsf()
for row in range(3):
for column in range(3):
transform.SetValue(row + 1, column + 1, matrix[row][column])
transform.SetTranslationPart(gp_XYZ(*translation))
builder = BRepBuilderAPI_GTransform(shape, transform, True)
builder.Build()
if not builder.IsDone():
raise RuntimeError("Axis scale transform failed.")
result = builder.Shape()
if result.IsNull():
raise RuntimeError("Axis scale transform produced an empty shape.")
return result
def _axis_dir_from_name(axis_name: str) -> gp_Dir:
axis = axis_name.upper()
if axis == "X":
@@ -365,10 +417,23 @@ def _tuple_or_none(value: object) -> tuple[float, float, float] | None:
return None
def _float_or_none(value: object) -> float | None:
if value is None or value == "":
return None
try:
return float(value)
except (TypeError, ValueError):
return None
def _tuple_sub(left: tuple[float, float, float], right: tuple[float, float, float]) -> tuple[float, float, float]:
return (left[0] - right[0], left[1] - right[1], left[2] - right[2])
def _tuple_add(left: tuple[float, float, float], right: tuple[float, float, float]) -> tuple[float, float, float]:
return (left[0] + right[0], left[1] + right[1], left[2] + right[2])
def _tuple_scale(values: tuple[float, float, float], scale: float) -> tuple[float, float, float]:
return (values[0] * scale, values[1] * scale, values[2] * scale)
@@ -937,7 +1002,7 @@ def _cylinder_depth_readiness(
status = "caution"
if not warnings and not blockers:
note = "可以尝试调整盲孔深度。"
note = "可以尝试调整盲孔/盲槽深度。"
else:
note = " ".join(blockers + warnings)
return {
+2
View File
@@ -29,6 +29,8 @@ class InfoPanelMixin:
self.info_tabs.setCurrentWidget(self.info_tree)
if hasattr(self, "_refresh_property_editor"):
self._refresh_property_editor()
if hasattr(self, "_update_selected_object_title"):
self._update_selected_object_title()
if hasattr(self, "mode_combo"):
self._update_action_states()
+175 -25
View File
@@ -98,6 +98,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
self.edge_solid_ids: list[int] = []
self.solids: list[tuple[int, TopoDS_Shape]] = []
self._face_info_cache: dict[int, dict[str, object]] = {}
self._feature_info_cache: dict[int, dict[str, object]] = {}
self._edge_info_cache: dict[int, dict[str, object]] = {}
self._face_edge_ids_cache: dict[int, list[int]] = {}
self._edge_face_ids_cache: dict[int, list[int]] = {}
@@ -176,6 +177,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
self.edge_solid_ids.clear()
self.solids.clear()
self._face_info_cache.clear()
self._feature_info_cache.clear()
self._edge_info_cache.clear()
self._face_edge_ids_cache.clear()
self._edge_face_ids_cache.clear()
@@ -268,6 +270,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
if isinstance(logical_ids, (list, tuple)) and len(logical_ids) == len(self.faces):
self.face_logical_ids = [int(item) for item in logical_ids]
self._face_info_cache.clear()
self._feature_info_cache.clear()
self._same_domain_face_ids_cache.clear()
def face_info(self, face_id: int) -> dict[str, object]:
@@ -356,27 +359,140 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
self._face_info_cache[face_id] = dict(info)
return dict(info)
def face_surface_kind(self, face_id: int) -> str:
if face_id < 0 or face_id >= len(self.faces):
return ""
cached = self._face_info_cache.get(face_id)
if cached is not None and cached.get("surface") is not None:
return str(cached.get("surface"))
try:
surf = BRepAdaptor_Surface(self.faces[face_id])
surface_type = surf.GetType()
except Exception:
return ""
return SURFACE_TYPES.get(surface_type, f"type {surface_type}")
def face_cylinder_diameter(self, face_id: int) -> float | None:
if face_id < 0 or face_id >= len(self.faces):
return None
cached = self._face_info_cache.get(face_id)
if cached is not None and cached.get("diameter") is not None and cached.get("surface") == "cylinder":
try:
return float(cached["diameter"])
except (TypeError, ValueError):
pass
try:
surf = BRepAdaptor_Surface(self.faces[face_id])
if surf.GetType() != GeomAbs_Cylinder:
return None
return float(surf.Cylinder().Radius()) * 2.0
except Exception:
return None
def feature_info(self, face_id: int) -> dict[str, object]:
if face_id < 0 or face_id >= len(self.faces):
raise ValueError(f"Unknown face id {face_id}")
if face_id in self._feature_info_cache:
return dict(self._feature_info_cache[face_id])
info = self.face_info(face_id)
surface = str(info.get("surface", ""))
if surface == "cylinder":
return self._cylindrical_feature_info(face_id, info)
if surface == "plane":
return self._planar_feature_info(face_id, info)
result = self._cylindrical_feature_info(face_id, info)
elif surface == "plane":
result = self._planar_feature_info(face_id, info)
elif surface == "cone":
result = self._conical_feature_info(face_id, info)
elif surface == "sphere":
result = self._spherical_feature_info(face_id, info)
elif surface == "torus":
result = self._toroidal_feature_info(face_id, info)
else:
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
result = dict(info)
result.update(
{
"kind": "feature",
"feature_type": "暂不支持的局部曲面候选",
"feature_source_face_id": face_id,
"feature_face_ids": (face_id,),
"feature_highlight_face_ids": (face_id,),
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
"feature_edit_actions": "当前只读;复杂曲面局部面积修改暂未开放",
"feature_mode": (
"Feature 模式会把选中的Face解释为局部几何特征候选;"
"复杂曲面局部面积修改需要更明确的边界/约束重建,当前不会把面积伪装成直接可改参数。"
),
}
)
self._feature_info_cache[face_id] = dict(result)
return dict(result)
def _toroidal_feature_info(self, face_id: int, info: dict[str, object]) -> dict[str, object]:
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
major_radius = float(info.get("major_radius", 0.0) or 0.0)
minor_radius = float(info.get("minor_radius", 0.0) or 0.0)
result = dict(info)
result.update(
{
"kind": "feature",
"feature_type": "暂不支持的局部曲面候选",
"feature_type": "面候选",
"feature_source_face_id": face_id,
"feature_face_ids": (face_id,),
"feature_highlight_face_ids": (face_id,),
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
"feature_edit_actions": "当前版本只能查看该局部曲面,暂不支持直接编辑。",
"feature_mode": "Feature 模式会把选中的Face解释为局部几何特征候选。",
"feature_edit_actions": "修改环面主半径/小半径",
"feature_mode": (
"这是从 STEP/B-Rep 环面直接识别出的几何候选;当前修改会围绕环面中心缩放所属零件/Solid,"
"主半径和小半径会等比例变化,不是 CAD 历史里的管径或圆角参数。"
),
"feature_torus_major_radius": major_radius,
"feature_torus_minor_radius": minor_radius,
}
)
return result
def _spherical_feature_info(self, face_id: int, info: dict[str, object]) -> dict[str, object]:
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
radius = float(info.get("radius", 0.0) or 0.0)
result = dict(info)
result.update(
{
"kind": "feature",
"feature_type": "球面候选",
"feature_source_face_id": face_id,
"feature_face_ids": (face_id,),
"feature_highlight_face_ids": (face_id,),
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
"feature_edit_actions": "修改球面半径/直径",
"feature_mode": (
"这是从 STEP/B-Rep 球面直接识别出的几何候选;修改会围绕球心缩放所属零件/Solid,"
"不是 CAD 历史里的球面或圆角参数。"
),
"feature_sphere_radius": radius,
"feature_sphere_diameter": radius * 2.0 if radius > 0 else "",
}
)
return result
def _conical_feature_info(self, face_id: int, info: dict[str, object]) -> dict[str, object]:
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
reference_radius = float(info.get("reference_radius", 0.0) or 0.0)
result = dict(info)
result.update(
{
"kind": "feature",
"feature_type": "圆锥面候选",
"feature_source_face_id": face_id,
"feature_face_ids": (face_id,),
"feature_highlight_face_ids": (face_id,),
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
"feature_reference_radius": reference_radius,
"feature_reference_diameter": reference_radius * 2.0 if reference_radius > 0 else "",
"feature_edit_actions": "修改圆锥参考半径/直径",
"feature_mode": (
"这是从 STEP/B-Rep 圆锥面直接识别出的几何候选;修改会围绕圆锥轴做径向缩放,"
"不是 CAD 历史里的锥孔或倒角参数。"
),
}
)
return result
@@ -549,12 +665,14 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
if guess == "hole/groove candidate":
if angular_span < math.tau * 0.92:
feature_type = "槽/半孔候选"
edit_actions = "调整圆柱孔径;调整槽/半孔宽度;调整槽/半孔深度;调整槽/半孔圆弧长度;调整槽/半孔圆弧角度;调整槽孔总长度"
else:
feature_type = "圆柱孔候选"
edit_actions = "调整圆柱孔径"
if info.get("cylinder_end_type") == "blind" and bottom_face_ids:
edit_actions = "调整圆柱孔径;调整盲孔深度"
edit_actions += ";调整盲孔/盲槽深度"
else:
edit_actions = "调整圆柱孔径;孔深调整需要明确盲孔底面"
edit_actions += ";孔深调整需要明确盲孔底面"
if angular_span >= math.tau * 0.92:
edit_actions += ";封堵圆柱孔"
elif guess == "round/fillet candidate":
@@ -562,7 +680,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
edit_actions = "可尝试修改已有圆角半径;当前版本会先移除圆角面,再在恢复出的锐边上重新倒圆。"
elif guess == "boss/outer-round candidate":
feature_type = "凸台/外圆候选"
edit_actions = "调整圆柱凸台直径。"
edit_actions = "调整圆柱凸台直径;调整圆柱凸台高度;修改圆柱凸台轴心坐标"
else:
feature_type = "未明确圆柱特征"
edit_actions = "可尝试调整圆柱孔径,但风险较高。"
@@ -704,6 +822,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
start_end_face_ids: list[int] = []
end_end_face_ids: list[int] = []
cap_scan_skipped = False
for adjacent_id in adjacent_face_ids:
match = self._axis_end_match_for_planar_face(
adjacent_id,
@@ -720,20 +839,23 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
end_end_face_ids.append(adjacent_id)
if info.get("cylinder_end_type") == "blind" and (not start_end_face_ids or not end_end_face_ids):
scanned = self._axis_cap_face_candidates(
face_id,
axis_point,
axis_dir,
v_min,
v_max,
radius,
span,
set(adjacent_face_ids),
)
if not start_end_face_ids:
start_end_face_ids.extend(scanned["start"])
if not end_end_face_ids:
end_end_face_ids.extend(scanned["end"])
if len(self.faces) <= 1000:
scanned = self._axis_cap_face_candidates(
face_id,
axis_point,
axis_dir,
v_min,
v_max,
radius,
span,
set(adjacent_face_ids),
)
if not start_end_face_ids:
start_end_face_ids.extend(scanned["start"])
if not end_end_face_ids:
end_end_face_ids.extend(scanned["end"])
else:
cap_scan_skipped = True
bottom_face_ids: list[int] = []
opening_face_ids: list[int] = []
@@ -755,6 +877,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
bottom_confidence = "medium" if bottom_detection == "axis-cap-scan" else "high"
if not end_face_ids:
note = "没有在圆柱边界附近找到平面端面。"
if cap_scan_skipped:
note += " 当前模型 Face 数较多,已跳过全模型底面扫描以避免选择时卡顿;如需改盲孔/盲槽深度,可以手动填写底面 Face ID。"
elif bottom_face_ids:
if bottom_detection == "axis-cap-scan":
note = "已通过轴线端部采样和轴线附近圆盘面扫描标记疑似底面;这是几何推断,不是 CAD 历史孔深。"
@@ -762,6 +886,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
note = "已根据圆柱轴线端部 inside/outside 采样标记疑似底面;这是几何推断,不是 CAD 历史孔深。"
else:
note = "已找到端面候选,但端部采样显示这些端面更像开口附近的相邻面。"
if cap_scan_skipped:
note += " 当前模型 Face 数较多,已跳过全模型底面扫描以避免选择时卡顿。"
return {
"end_face_ids": end_face_ids,
"start_end_face_ids": sorted(set(start_end_face_ids)),
@@ -905,6 +1031,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
for face_id in valid_face_ids:
self.face_logical_ids[face_id] = int(logical_id)
self._face_info_cache.pop(face_id, None)
self._feature_info_cache.pop(face_id, None)
def nearest_edge_id_to_point(
self,
@@ -961,9 +1088,17 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
return valid_face_ids[0]
point_vertex = BRepBuilderAPI_MakeVertex(gp_Pnt(float(point[0]), float(point[1]), float(point[2]))).Vertex()
exact_face_ids = valid_face_ids
if len(valid_face_ids) > 48:
scored_face_ids: list[tuple[float, int]] = []
for face_id in valid_face_ids:
scored_face_ids.append((self._face_bounds_distance_sq(face_id, point), face_id))
scored_face_ids.sort(key=lambda item: item[0])
exact_face_ids = [face_id for _distance, face_id in scored_face_ids[:48]]
best_face_id: int | None = None
best_distance = math.inf
for face_id in valid_face_ids:
for face_id in exact_face_ids:
try:
extrema = BRepExtrema_DistShapeShape(point_vertex, self.faces[face_id])
extrema.Perform()
@@ -981,7 +1116,22 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
if distance < best_distance:
best_distance = distance
best_face_id = face_id
return best_face_id if best_face_id is not None else valid_face_ids[0]
return best_face_id if best_face_id is not None else exact_face_ids[0]
def _face_bounds_distance_sq(self, face_id: int, point: tuple[float, float, float]) -> float:
px, py, pz = (float(point[0]), float(point[1]), float(point[2]))
try:
xmin, ymin, zmin, xmax, ymax, zmax = _shape_bounds(self.faces[face_id])
dx = max(xmin - px, 0.0, px - xmax)
dy = max(ymin - py, 0.0, py - ymax)
dz = max(zmin - pz, 0.0, pz - zmax)
return dx * dx + dy * dy + dz * dz
except Exception:
try:
center = _surface_center(self.faces[face_id])
return _point_distance_sq((px, py, pz), _point_tuple(center))
except Exception:
return math.inf
def _adjacent_face_ids_for_edges(self, edge_ids: list[int], face_id: int) -> list[int]:
if not edge_ids:
+1917 -2
View File
File diff suppressed because it is too large Load Diff
+297
View File
@@ -262,3 +262,300 @@ class TransformMixin:
f"risk={plan['risk']}."
)
def scale_part_plan(self, part_id: int, target_diagonal: float) -> dict[str, object]:
part = self.part_by_id(part_id)
if part is None:
raise ValueError(f"Unknown part id {part_id}")
return self._scale_shape_plan(
target_kind="part",
part_id=part.id,
solid_id=-1,
shape=part.shape,
target_diagonal=target_diagonal,
part_solid_count=len(_explore(part.shape, TopAbs_SOLID)),
)
def scale_solid_plan(self, solid_id: int, target_diagonal: float) -> dict[str, object]:
if solid_id < 0 or solid_id >= len(self.solids):
raise ValueError(f"Unknown solid id {solid_id}")
part_id, solid = self.solids[solid_id]
part = self.part_by_id(part_id)
part_solid_count = len(_explore(part.shape, TopAbs_SOLID)) if part is not None else 0
plan = self._scale_shape_plan(
target_kind="solid",
part_id=part_id,
solid_id=solid_id,
shape=solid,
target_diagonal=target_diagonal,
part_solid_count=part_solid_count,
)
if part_solid_count <= 1 and plan["status"] != "blocked":
plan["warnings"] = _join_nonempty(
plan.get("warnings", ""),
"当前零件只有一个Solid,缩放Solid实际会缩放整个零件 shape。",
)
if plan["risk"] == "low":
plan["risk"] = "medium"
plan["status"] = "caution"
plan["message"] = _join_nonempty(plan.get("message", ""), plan.get("warnings", ""))
return plan
def scale_part_axis_plan(self, part_id: int, axis: str, target_size: float) -> dict[str, object]:
part = self.part_by_id(part_id)
if part is None:
raise ValueError(f"Unknown part id {part_id}")
return self._axis_scale_shape_plan(
target_kind="part",
part_id=part.id,
solid_id=-1,
shape=part.shape,
axis=axis,
target_size=target_size,
part_solid_count=len(_explore(part.shape, TopAbs_SOLID)),
)
def scale_solid_axis_plan(self, solid_id: int, axis: str, target_size: float) -> dict[str, object]:
if solid_id < 0 or solid_id >= len(self.solids):
raise ValueError(f"Unknown solid id {solid_id}")
part_id, solid = self.solids[solid_id]
part = self.part_by_id(part_id)
part_solid_count = len(_explore(part.shape, TopAbs_SOLID)) if part is not None else 0
plan = self._axis_scale_shape_plan(
target_kind="solid",
part_id=part_id,
solid_id=solid_id,
shape=solid,
axis=axis,
target_size=target_size,
part_solid_count=part_solid_count,
)
if part_solid_count <= 1 and plan["status"] != "blocked":
plan["warnings"] = _join_nonempty(
plan.get("warnings", ""),
"当前零件只有一个Solid,按轴缩放Solid实际会缩放整个零件shape。",
)
if plan["risk"] == "low":
plan["risk"] = "medium"
plan["status"] = "caution"
plan["message"] = _join_nonempty(plan.get("message", ""), plan.get("warnings", ""))
return plan
def _scale_shape_plan(
self,
*,
target_kind: str,
part_id: int,
solid_id: int,
shape: TopoDS_Shape,
target_diagonal: float,
part_solid_count: int,
) -> dict[str, object]:
blockers: list[str] = []
warnings: list[str] = []
try:
target_diagonal = float(target_diagonal)
except (TypeError, ValueError):
target_diagonal = 0.0
blockers.append("目标包围盒对角线必须是数字。")
current_diagonal = _shape_diagonal(shape)
center = _shape_center(shape)
if current_diagonal <= 1e-9:
blockers.append("当前对象包围盒对角线无效,不能缩放。")
if target_diagonal <= 1e-9:
blockers.append("目标包围盒对角线必须大于 0。")
scale = target_diagonal / max(current_diagonal, 1e-9)
delta_ratio = abs(target_diagonal - current_diagonal) / max(current_diagonal, 1e-9)
risk = "low"
status = "ready"
if abs(target_diagonal - current_diagonal) <= max(current_diagonal * 1e-6, 1e-6):
blockers.append("目标包围盒对角线与当前值几乎相同,不需要缩放。")
elif delta_ratio > 1.0:
risk = "high"
status = "caution"
warnings.append("目标尺寸变化超过当前包围盒对角线的 100%,请确认单位和目标。")
elif delta_ratio > 0.35:
risk = "medium"
status = "caution"
warnings.append("目标尺寸变化超过当前包围盒对角线的 35%,请确认缩放结果。")
if blockers:
risk = "blocked"
status = "blocked"
return {
"status": status,
"risk": risk,
"message": _join_nonempty(" ".join(blockers), " ".join(warnings)) or "可以按目标包围盒对角线等比缩放当前对象。",
"warnings": "".join(warnings),
"blockers": "".join(blockers),
"target_kind": target_kind,
"part_id": part_id,
"solid_id": solid_id,
"part_solid_count": part_solid_count,
"current_bbox_diagonal": current_diagonal,
"target_bbox_diagonal": target_diagonal,
"scale_factor": scale,
"scale_delta_ratio": delta_ratio,
"scale_center": center,
"resize_strategy": "uniform-scale-selected-shape-to-bbox-diagonal",
}
def _axis_scale_shape_plan(
self,
*,
target_kind: str,
part_id: int,
solid_id: int,
shape: TopoDS_Shape,
axis: str,
target_size: float,
part_solid_count: int,
) -> dict[str, object]:
axis = str(axis or "X").strip().upper()
axis_index = {"X": 0, "Y": 1, "Z": 2}.get(axis)
blockers: list[str] = []
warnings: list[str] = [
"按单轴尺寸缩放是 B-Rep 仿射编辑,可能把解析几何转换成 B-spline 或影响同一对象上的其它特征。"
]
try:
target_size = float(target_size)
except (TypeError, ValueError):
target_size = 0.0
blockers.append("目标轴向尺寸必须是数字。")
bounds = _shape_bounds_info(shape)
bbox_size = tuple(bounds.get("bbox_size", (0.0, 0.0, 0.0)))
current_size = float(bbox_size[axis_index]) if axis_index is not None else 0.0
center = _shape_center(shape)
if axis_index is None:
blockers.append("缩放轴必须是 X、Y 或 Z。")
if current_size <= 1e-9:
blockers.append("当前对象在该轴向的包围盒尺寸无效,不能按轴缩放。")
if target_size <= 1e-9:
blockers.append("目标轴向尺寸必须大于 0。")
scale = target_size / max(current_size, 1e-9)
delta_ratio = abs(target_size - current_size) / max(current_size, 1e-9)
risk = "medium"
status = "caution"
if abs(target_size - current_size) <= max(current_size * 1e-6, 1e-6):
blockers.append("目标轴向尺寸与当前值几乎相同,不需要缩放。")
elif delta_ratio > 1.0:
risk = "high"
warnings.append("目标轴向尺寸变化超过当前尺寸的 100%,请确认单位和目标。")
elif delta_ratio <= 0.2:
risk = "medium"
if blockers:
risk = "blocked"
status = "blocked"
return {
"status": status,
"risk": risk,
"message": _join_nonempty(" ".join(blockers), " ".join(warnings)) or "可以按目标轴向尺寸缩放当前对象。",
"warnings": "".join(warnings),
"blockers": "".join(blockers),
"target_kind": target_kind,
"part_id": part_id,
"solid_id": solid_id,
"part_solid_count": part_solid_count,
"scale_axis": axis,
"current_axis_size": current_size,
"target_axis_size": target_size,
"axis_scale_factor": scale,
"axis_scale_delta_ratio": delta_ratio,
"scale_center": center,
"bbox_size": bbox_size,
"resize_strategy": "axis-scale-selected-shape-to-bbox-size",
}
def scale_part(self, part_id: int, target_diagonal: float) -> str:
plan = self.scale_part_plan(part_id, target_diagonal)
if plan["status"] == "blocked":
raise ValueError(str(plan["message"]))
part = self.part_by_id(part_id)
if part is None:
raise ValueError(f"Unknown part id {part_id}")
part.shape = _scaled_shape(part.shape, float(plan["scale_factor"]), plan["scale_center"])
_ensure_valid_shape(part.shape)
self.refresh_topology()
return (
f"Part scaled: part {part_id}, "
f"bbox_diagonal={float(plan['current_bbox_diagonal']):g}->{float(plan['target_bbox_diagonal']):g}, "
f"scale={float(plan['scale_factor']):g}, risk={plan['risk']}."
)
def scale_part_axis(self, part_id: int, axis: str, target_size: float) -> str:
plan = self.scale_part_axis_plan(part_id, axis, target_size)
if plan["status"] == "blocked":
raise ValueError(str(plan["message"]))
part = self.part_by_id(part_id)
if part is None:
raise ValueError(f"Unknown part id {part_id}")
part.shape = _axis_scaled_shape(part.shape, str(plan["scale_axis"]), float(plan["axis_scale_factor"]), plan["scale_center"])
_ensure_valid_shape(part.shape)
self.refresh_topology()
return (
f"Part axis-scaled: part {part_id}, axis={plan['scale_axis']}, "
f"size={float(plan['current_axis_size']):g}->{float(plan['target_axis_size']):g}, "
f"scale={float(plan['axis_scale_factor']):g}, risk={plan['risk']}."
)
def scale_solid(self, solid_id: int, target_diagonal: float) -> str:
plan = self.scale_solid_plan(solid_id, target_diagonal)
if plan["status"] == "blocked":
raise ValueError(str(plan["message"]))
part_id, solid = self.solids[solid_id]
part = self.part_by_id(part_id)
if part is None:
raise ValueError(f"Unknown part id {part_id}")
part_solids = _explore(part.shape, TopAbs_SOLID)
if len(part_solids) <= 1:
part.shape = _scaled_shape(part.shape, float(plan["scale_factor"]), plan["scale_center"])
else:
scaled = _scaled_shape(solid, float(plan["scale_factor"]), plan["scale_center"])
replaced = False
shapes: list[TopoDS_Shape] = []
for item in part_solids:
if not replaced and _same_shape(item, solid):
shapes.append(scaled)
replaced = True
else:
shapes.append(item)
if not replaced:
raise RuntimeError(f"Could not locate solid {solid_id} inside part {part_id}.")
part.shape = _compound_from_shapes(shapes)
_ensure_valid_shape(part.shape)
self.refresh_topology()
return (
f"Solid scaled: solid {solid_id}, part {part_id}, "
f"bbox_diagonal={float(plan['current_bbox_diagonal']):g}->{float(plan['target_bbox_diagonal']):g}, "
f"scale={float(plan['scale_factor']):g}, risk={plan['risk']}."
)
def scale_solid_axis(self, solid_id: int, axis: str, target_size: float) -> str:
plan = self.scale_solid_axis_plan(solid_id, axis, target_size)
if plan["status"] == "blocked":
raise ValueError(str(plan["message"]))
part_id, solid = self.solids[solid_id]
part = self.part_by_id(part_id)
if part is None:
raise ValueError(f"Unknown part id {part_id}")
part_solids = _explore(part.shape, TopAbs_SOLID)
if len(part_solids) <= 1:
part.shape = _axis_scaled_shape(part.shape, str(plan["scale_axis"]), float(plan["axis_scale_factor"]), plan["scale_center"])
else:
scaled = _axis_scaled_shape(solid, str(plan["scale_axis"]), float(plan["axis_scale_factor"]), plan["scale_center"])
replaced = False
shapes: list[TopoDS_Shape] = []
for item in part_solids:
if not replaced and _same_shape(item, solid):
shapes.append(scaled)
replaced = True
else:
shapes.append(item)
if not replaced:
raise RuntimeError(f"Could not locate solid {solid_id} inside part {part_id}.")
part.shape = _compound_from_shapes(shapes)
_ensure_valid_shape(part.shape)
self.refresh_topology()
return (
f"Solid axis-scaled: solid {solid_id}, part {part_id}, axis={plan['scale_axis']}, "
f"size={float(plan['current_axis_size']):g}->{float(plan['target_axis_size']):g}, "
f"scale={float(plan['axis_scale_factor']):g}, risk={plan['risk']}."
)
+24
View File
@@ -459,6 +459,8 @@ INFO_LABELS = {
"target_to_height_ratio": "目标直径/估算高度",
"major_radius": "主半径",
"minor_radius": "小半径",
"ellipse_edge_major_radius": "椭圆Edge主半径",
"ellipse_edge_minor_radius": "椭圆Edge小半径",
"reference_radius": "参考半径",
"semi_angle": "半角",
"angular_span": "角度跨度",
@@ -583,6 +585,28 @@ INFO_LABELS = {
"existing_fillet_kind": "已有圆角类型",
"existing_fillet_status": "已有圆角识别状态",
"existing_fillet_radius_estimate": "已有圆角半径估算",
"feature_reference_radius": "特征参考半径",
"feature_reference_diameter": "特征参考直径",
"feature_sphere_radius": "球面半径",
"feature_sphere_diameter": "球面直径",
"feature_torus_major_radius": "环面主半径",
"feature_torus_minor_radius": "环面小半径",
"current_major_radius": "当前主半径",
"target_major_radius": "目标主半径",
"current_minor_radius": "当前小半径",
"target_minor_radius": "目标小半径",
"torus_radius_mode": "环面半径模式",
"current_area": "当前面积",
"target_area": "目标面积",
"area_delta": "面积变化量",
"area_delta_ratio": "面积变化比例",
"area_center": "面积中心",
"current_reference_radius": "当前参考半径",
"target_reference_radius": "目标参考半径",
"current_reference_diameter": "当前参考直径",
"target_reference_diameter": "目标参考直径",
"delta_reference_radius": "参考半径变化量",
"reference_radius_delta_ratio": "参考半径变化比例",
"existing_fillet_angular_span": "已有圆角圆弧角度",
"existing_fillet_arc_length_estimate": "已有圆角圆弧长度估算",
"existing_fillet_note": "已有圆角识别说明",
File diff suppressed because it is too large Load Diff
+261 -23
View File
@@ -43,15 +43,35 @@ class WindowCoreMixin:
if watched is getattr(self, "vtk_widget", None):
event_type = event.type()
if event_type == QEvent.Type.MouseButtonRelease:
if event.button() == Qt.MouseButton.LeftButton:
x, y = self._vtk_position_from_qt_event(event)
self.skip_next_vtk_left_release = True
self._handle_left_button_release(x, y)
self.pointer_button_down = bool(event.buttons() != Qt.MouseButton.NoButton)
elif event_type == QEvent.Type.MouseButtonPress:
if event.button() == Qt.MouseButton.LeftButton:
x, y = self._vtk_position_from_qt_event(event)
self.skip_next_vtk_left_press = True
self._handle_left_button_press(x, y)
self.pointer_button_down = True
elif event_type == QEvent.Type.MouseMove:
self.pointer_button_down = bool(event.buttons() != Qt.MouseButton.NoButton)
if event.buttons() & Qt.MouseButton.LeftButton:
x, y = self._vtk_position_from_qt_event(event)
self._update_left_button_drag_state(x, y)
if not self.pointer_button_down:
self._queue_hover_from_qt_event(event)
elif event_type == QEvent.Type.Leave:
self._cancel_hover_tracking(render=True)
return super().eventFilter(watched, event)
def _vtk_position_from_qt_event(self, event) -> tuple[int, int]:
position = event.position() if hasattr(event, "position") else event.pos()
scale = self.vtk_widget._getPixelRatio() if hasattr(self.vtk_widget, "_getPixelRatio") else 1.0
x = int(round(float(position.x()) * scale))
y = int(round((float(self.vtk_widget.height()) - float(position.y()) - 1.0) * scale))
return x, y
def _build_vtk(self) -> None:
self.renderer = vtk.vtkRenderer()
self.renderer.SetBackground(*VIEW_BACKGROUND_COLOR)
@@ -62,13 +82,19 @@ class WindowCoreMixin:
self.interactor.SetInteractorStyle(vtk.vtkInteractorStyleTrackballCamera())
self.picker = vtk.vtkCellPicker()
self.picker.SetTolerance(0.003)
self.interactor.AddObserver("LeftButtonPressEvent", self.on_left_click)
self.interactor.AddObserver("LeftButtonReleaseEvent", self.on_pointer_button_release)
self.interactor.AddObserver("LeftButtonPressEvent", self.on_left_button_press)
self.interactor.AddObserver("LeftButtonReleaseEvent", self.on_left_button_release)
self.interactor.AddObserver("MiddleButtonPressEvent", self.on_pointer_button_press)
self.interactor.AddObserver("MiddleButtonReleaseEvent", self.on_pointer_button_release)
self.interactor.AddObserver("RightButtonPressEvent", self.on_pointer_button_press)
self.interactor.AddObserver("RightButtonReleaseEvent", self.on_pointer_button_release)
self.interactor.AddObserver("MouseMoveEvent", self.on_mouse_move)
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)
if hasattr(self.interactor, "SetStillUpdateRate"):
self.interactor.SetStillUpdateRate(0.2)
light = vtk.vtkLight()
light.SetLightTypeToHeadlight()
@@ -78,6 +104,55 @@ class WindowCoreMixin:
self.interactor.Initialize()
def on_camera_interaction_start(self, _obj, _event) -> None:
if not self._is_ui_thread():
self._invoke_on_ui_thread(self._begin_camera_interaction)
return
self._begin_camera_interaction()
def on_camera_interaction_end(self, _obj, _event) -> None:
if not self._is_ui_thread():
self._invoke_on_ui_thread(self._end_camera_interaction)
return
self._end_camera_interaction()
def _begin_camera_interaction(self) -> None:
if getattr(self, "camera_interaction_active", False):
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(
(self.load_thread is not None and self.load_thread.isRunning())
@@ -839,18 +914,73 @@ class WindowCoreMixin:
if hasattr(self, "_update_id_select_title"):
self._update_id_select_title(mode)
def on_left_click(self, _obj, _event) -> None:
def _cancel_hover_tracking(self, render: bool = True) -> None:
self.pending_hover_position = None
self.last_hover_pick_position = None
if hasattr(self, "hover_timer"):
self.hover_timer.stop()
self._clear_hover(render=render)
def on_left_button_press(self, _obj, _event) -> None:
if getattr(self, "skip_next_vtk_left_press", False):
self.skip_next_vtk_left_press = False
return
try:
x, y = self.interactor.GetEventPosition()
except Exception:
x, y = 0, 0
if not self._is_ui_thread():
self._invoke_on_ui_thread(lambda x=int(x), y=int(y): self._handle_left_click(x, y))
self._invoke_on_ui_thread(lambda x=int(x), y=int(y): self._handle_left_button_press(x, y))
return
self._handle_left_click(int(x), int(y))
self._handle_left_button_press(int(x), int(y))
def _handle_left_button_press(self, x: int, y: int) -> None:
self.pointer_button_down = True
self.left_button_press_position = (int(x), int(y))
self.left_button_dragged = False
self.pending_hover_position = None
self.last_hover_pick_position = None
self.hover_timer.stop()
self._clear_hover(render=True)
def on_left_button_release(self, _obj, _event) -> None:
if getattr(self, "skip_next_vtk_left_release", False):
self.skip_next_vtk_left_release = False
return
try:
x, y = self.interactor.GetEventPosition()
except Exception:
x, y = 0, 0
if not self._is_ui_thread():
self._invoke_on_ui_thread(lambda x=int(x), y=int(y): self._handle_left_button_release(x, y))
return
self._handle_left_button_release(int(x), int(y))
def _handle_left_button_release(self, x: int, y: int) -> None:
self._update_left_button_drag_state(x, y)
was_dragged = bool(getattr(self, "left_button_dragged", False))
self.pointer_button_down = False
self.left_button_press_position = None
self.left_button_dragged = 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:
return
self._handle_left_click(x, y)
def _update_left_button_drag_state(self, x: int, y: int) -> None:
press_position = getattr(self, "left_button_press_position", None)
if press_position is None:
return
threshold = int(getattr(self, "left_click_drag_threshold_px", 6) or 6)
dx = abs(int(x) - int(press_position[0]))
dy = abs(int(y) - int(press_position[1]))
if max(dx, dy) > threshold:
self.left_button_dragged = True
def _handle_left_click(self, x: int, y: int) -> None:
self.pointer_button_down = True
self.pending_hover_position = None
self.last_hover_pick_position = None
self.hover_timer.stop()
@@ -868,7 +998,11 @@ class WindowCoreMixin:
mode = self._current_selection_mode()
target = self._pick_selection_target(mode, x, y)
if target is None:
self.statusBar().showMessage("未选中对象")
self._cancel_hover_tracking(render=False)
self._reset_selection(clear_highlight=True)
if hasattr(self, "render_window"):
self.render_window.Render()
self.statusBar().showMessage("已取消选择")
return
self._clear_hover(render=False)
@@ -897,6 +1031,7 @@ class WindowCoreMixin:
self.pointer_button_down = False
self.pending_hover_position = None
self.last_hover_pick_position = None
self._end_camera_interaction()
def on_mouse_move(self, _obj, _event) -> None:
try:
@@ -909,8 +1044,15 @@ class WindowCoreMixin:
self._handle_mouse_move(int(x), int(y))
def _handle_mouse_move(self, x: int, y: int) -> None:
if QApplication.mouseButtons() != Qt.MouseButton.NoButton:
buttons = QApplication.mouseButtons()
if buttons != Qt.MouseButton.NoButton:
self.pointer_button_down = True
if buttons & Qt.MouseButton.LeftButton:
self._update_left_button_drag_state(x, y)
if getattr(self, "left_button_dragged", False):
self._begin_camera_interaction()
else:
self._begin_camera_interaction()
return
self.pointer_button_down = False
if (
@@ -934,10 +1076,7 @@ class WindowCoreMixin:
):
self._clear_hover(render=True)
return
position = event.position() if hasattr(event, "position") else event.pos()
scale = self.vtk_widget._getPixelRatio() if hasattr(self.vtk_widget, "_getPixelRatio") else 1.0
x = int(round(float(position.x()) * scale))
y = int(round((float(self.vtk_widget.height()) - float(position.y()) - 1.0) * scale))
x, y = self._vtk_position_from_qt_event(event)
self._queue_hover_position(x, y)
def _queue_hover_position(self, x: int, y: int) -> None:
@@ -1213,10 +1352,26 @@ class WindowCoreMixin:
self._set_selection_mode("Feature")
self.select_feature(int(target_id))
self.statusBar().showMessage(f"已选择可调整凸台直径候选Face {target_id}")
elif action == "resize_boss_height":
self._set_selection_mode("Feature")
self.select_feature(int(target_id))
self.statusBar().showMessage(f"已选择可调整凸台高度候选Face {target_id}")
elif action == "move_boss_axis":
self._set_selection_mode("Feature")
self.select_feature(int(target_id))
self.statusBar().showMessage(f"已选择可移动凸台轴心候选Face {target_id}")
elif action == "resize_slot_width":
self._set_selection_mode("Feature")
self.select_feature(int(target_id))
self.statusBar().showMessage(f"已选择可调整槽/半孔宽度候选Face {target_id}")
elif action == "resize_slot_depth":
self._set_selection_mode("Feature")
self.select_feature(int(target_id))
self.statusBar().showMessage(f"已选择可调整槽/半孔深度候选Face {target_id}")
elif action == "resize_slot_arc_length":
self._set_selection_mode("Feature")
self.select_feature(int(target_id))
self.statusBar().showMessage(f"已选择可调整槽/半孔圆弧长度候选Face {target_id}")
elif action == "resize_shell_thickness":
self._set_selection_mode("Feature")
self.select_feature(int(target_id))
@@ -1228,7 +1383,7 @@ class WindowCoreMixin:
elif action == "resize_depth":
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 == "inspect_existing_fillet":
self._set_selection_mode("Feature")
self.select_feature(int(target_id))
@@ -1305,7 +1460,7 @@ class WindowCoreMixin:
if self._edit_busy("编辑计算中,暂时不能选择零件。"):
return
info = self.model.part_info(part_id)
self._reset_selection(clear_highlight=False)
self._reset_selection(clear_highlight=False, clear_info=False)
self.selected_kind = "part"
self.selected_part_id = part_id
self.selected_pick_position = pick_position
@@ -1328,7 +1483,7 @@ class WindowCoreMixin:
if self._edit_busy("编辑计算中,暂时不能选择Solid。"):
return
info = self.model.solid_info(solid_id)
self._reset_selection(clear_highlight=False)
self._reset_selection(clear_highlight=False, clear_info=False)
self.selected_kind = "solid"
self.selected_part_id = part_id
self.selected_solid_id = solid_id
@@ -1351,7 +1506,7 @@ class WindowCoreMixin:
return
if self._edit_busy("编辑计算中,暂时不能选择Face。"):
return
self._reset_selection(clear_highlight=False)
self._reset_selection(clear_highlight=False, clear_info=False)
self.selected_kind = "feature" if feature_mode else "face"
self.selected_face_id = face_id
self.selected_pick_position = pick_position
@@ -1386,7 +1541,7 @@ class WindowCoreMixin:
if self._edit_busy("编辑计算中,暂时不能选择特征。"):
return
info = self.model.feature_info(face_id)
self._reset_selection(clear_highlight=False)
self._reset_selection(clear_highlight=False, clear_info=False)
self.selected_kind = "feature"
self.selected_face_id = face_id
self.selected_pick_position = pick_position
@@ -1417,7 +1572,21 @@ class WindowCoreMixin:
self.hole_diameter_input.clear()
if hasattr(self, "slot_width_input"):
self.slot_width_input.clear()
if hasattr(self, "slot_depth_input"):
self.slot_depth_input.clear()
if hasattr(self, "slot_arc_length_input"):
self.slot_arc_length_input.clear()
if hasattr(self, "slot_total_length_input"):
self.slot_total_length_input.clear()
self.boss_diameter_input.setText(suggested_diameter)
if hasattr(self, "boss_height_input"):
boss_height = _float_or_none(info.get("same_domain_height_estimate"))
if boss_height is None:
boss_height = _float_or_none(info.get("height_estimate"))
if boss_height is not None and boss_height > 0:
self.boss_height_input.setText(_format_float(boss_height * 1.2))
else:
self.boss_height_input.clear()
elif feature_guess == "round/fillet candidate":
radius = _float_or_none(info.get("existing_fillet_radius_estimate"))
if radius is None:
@@ -1427,22 +1596,54 @@ class WindowCoreMixin:
self.hole_diameter_input.clear()
if hasattr(self, "slot_width_input"):
self.slot_width_input.clear()
if hasattr(self, "slot_depth_input"):
self.slot_depth_input.clear()
if hasattr(self, "slot_arc_length_input"):
self.slot_arc_length_input.clear()
if hasattr(self, "slot_total_length_input"):
self.slot_total_length_input.clear()
self.boss_diameter_input.clear()
if hasattr(self, "boss_height_input"):
self.boss_height_input.clear()
else:
suggested_diameter = _format_float(float(info["diameter"]) * 1.2)
self.hole_diameter_input.setText(suggested_diameter)
self.boss_diameter_input.clear()
if hasattr(self, "boss_height_input"):
self.boss_height_input.clear()
if hasattr(self, "slot_width_input"):
slot_width = _float_or_none(info.get("slot_chord_width_estimate"))
if slot_width is not None and slot_width > 0:
self.slot_width_input.setText(_format_float(slot_width * 1.2))
else:
self.slot_width_input.clear()
if hasattr(self, "slot_depth_input"):
slot_depth = _float_or_none(info.get("slot_sagitta_depth_estimate"))
if slot_depth is not None and slot_depth > 0:
self.slot_depth_input.setText(_format_float(slot_depth * 1.2))
else:
self.slot_depth_input.clear()
if hasattr(self, "slot_arc_length_input"):
slot_arc = _float_or_none(info.get("slot_arc_length_estimate"))
if slot_arc is not None and slot_arc > 0:
self.slot_arc_length_input.setText(_format_float(slot_arc * 1.2))
else:
self.slot_arc_length_input.clear()
if hasattr(self, "slot_total_length_input"):
self.slot_total_length_input.clear()
else:
self.hole_diameter_input.clear()
if hasattr(self, "slot_width_input"):
self.slot_width_input.clear()
if hasattr(self, "slot_depth_input"):
self.slot_depth_input.clear()
if hasattr(self, "slot_arc_length_input"):
self.slot_arc_length_input.clear()
if hasattr(self, "slot_total_length_input"):
self.slot_total_length_input.clear()
self.boss_diameter_input.clear()
if hasattr(self, "boss_height_input"):
self.boss_height_input.clear()
if info.get("cylinder_end_type") == "blind" and "hole_depth_estimate" in info:
self.hole_depth_input.setText(_format_float(float(info["hole_depth_estimate"]) * 1.2))
else:
@@ -1469,7 +1670,15 @@ class WindowCoreMixin:
self.shell_thickness_input.clear()
if hasattr(self, "slot_width_input"):
self.slot_width_input.clear()
if hasattr(self, "slot_depth_input"):
self.slot_depth_input.clear()
if hasattr(self, "slot_arc_length_input"):
self.slot_arc_length_input.clear()
if hasattr(self, "slot_total_length_input"):
self.slot_total_length_input.clear()
self.boss_diameter_input.clear()
if hasattr(self, "boss_height_input"):
self.boss_height_input.clear()
self.hole_depth_input.clear()
if hasattr(self, "hole_bottom_face_input"):
self.hole_bottom_face_input.clear()
@@ -1491,7 +1700,7 @@ class WindowCoreMixin:
return
if self._edit_busy("编辑计算中,暂时不能选择Edge。"):
return
self._reset_selection(clear_highlight=False)
self._reset_selection(clear_highlight=False, clear_info=False)
self.selected_kind = "edge"
self.selected_edge_id = edge_id
self.selected_pick_position = pick_position
@@ -1583,8 +1792,7 @@ 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":
info = self.model.feature_info(target_id)
face_ids = _int_values(info.get("feature_highlight_face_ids")) or [target_id]
face_ids = self.model.connected_same_domain_face_ids(target_id) or [target_id]
self._highlight_hover_faces(face_ids=face_ids)
elif kind == "edge":
self._highlight_hover_edge(target_id)
@@ -1699,6 +1907,7 @@ class WindowCoreMixin:
polydata,
color: tuple[float, float, float],
opacity: float | None = None,
position: tuple[float, float, float] | None = None,
) -> None:
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(polydata)
@@ -1708,16 +1917,24 @@ class WindowCoreMixin:
actor.GetProperty().SetOpacity(opacity if opacity is not None else self.edit_preview_base_opacity)
actor.GetProperty().SetSpecular(0.28)
actor.GetProperty().SetLineWidth(1)
if position is not None:
actor.SetPosition(float(position[0]), float(position[1]), float(position[2]))
self.edit_preview_actors.append(actor)
self.edit_preview_actor = self.edit_preview_actor or actor
self.renderer.AddActor(actor)
def _show_push_pull_preview(self, face_id: int, distance: float) -> None:
def _show_push_pull_preview(self, face_id: int, distance: float, plan: dict[str, object] | None = None) -> None:
if self.model is None:
return
self.clear_edit_preview(render=False)
try:
polydata = self.model.push_pull_preview_polydata(face_id, distance)
plan = dict(plan or self.model.push_pull_plan(face_id, distance))
scope_face_ids = _int_values(plan.get("push_pull_scope_face_ids")) or [face_id]
polydata = self._cached_face_overlay_polydata(face_ids=scope_face_ids, smooth=False)
if polydata is None:
polydata = self._cached_face_overlay_polydata(face_ids=[face_id], smooth=False)
if polydata is None:
raise RuntimeError("当前显示网格里没有可复用的选中 Face 预览数据")
except Exception as exc:
self.statusBar().showMessage(f"推拉预览不可用:{exc}")
return
@@ -1725,7 +1942,15 @@ class WindowCoreMixin:
color = (0.0, 0.86, 0.34)
else:
color = (1.0, 0.18, 0.06)
self._add_edit_preview_actor(polydata, color)
outward = plan.get("outward_direction") if isinstance(plan, dict) else None
if not isinstance(outward, (tuple, list)) or len(outward) != 3:
outward = (0.0, 0.0, 0.0)
position = (
float(outward[0]) * float(distance),
float(outward[1]) * float(distance),
float(outward[2]) * float(distance),
)
self._add_edit_preview_actor(polydata, color, opacity=0.38, position=position)
self._start_edit_preview_pulse()
self.render_window.Render()
@@ -1776,6 +2001,19 @@ class WindowCoreMixin:
self._start_edit_preview_pulse()
self.render_window.Render()
def _show_cylinder_boss_height_preview(self, face_id: int, target_height: float) -> None:
if self.model is None:
return
self.clear_edit_preview(render=False)
try:
polydata = self.model.cylindrical_boss_height_preview_polydata(face_id, target_height)
except Exception as exc:
self.statusBar().showMessage(f"凸台高度调整预览不可用:{exc}")
return
self._add_edit_preview_actor(polydata, (0.0, 0.72, 0.78), opacity=0.34)
self._start_edit_preview_pulse()
self.render_window.Render()
def _show_cylinder_suppress_preview(self, face_id: int) -> None:
if self.model is None:
return
File diff suppressed because it is too large Load Diff