feat: 完善特征编辑和选择稳定性
This commit is contained in:
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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']}."
|
||||
)
|
||||
|
||||
@@ -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": "已有圆角识别说明",
|
||||
|
||||
+1931
-16
File diff suppressed because it is too large
Load Diff
+261
-23
@@ -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
|
||||
|
||||
+1854
-88
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user