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

This commit is contained in:
2026-07-28 14:05:14 +08:00
parent 26216064e0
commit 7c263d1e96
19 changed files with 3173 additions and 931 deletions
+288 -151
View File
@@ -49,6 +49,8 @@ from .window_state import WindowStateMixin
_CRASH_LOG_HANDLE = None
PROJECT_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "geom_extract.step"
def _enable_crash_log() -> None:
@@ -85,8 +87,8 @@ 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 = 45
self.hover_move_threshold_px = 0
self.hover_interval_ms = 85
self.hover_move_threshold_px = 5
self.pending_hover_position: tuple[int, int] | None = None
self.last_hover_pick_position: tuple[int, int] | None = None
self.pointer_button_down = False
@@ -132,13 +134,14 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.scan_thread: QThread | None = None
self.scan_worker: ScanWorker | None = None
self.pending_scan_kind: str | None = None
self.pending_scan_context: dict[str, object] | None = None
self.load_in_progress = False
self.load_thread: QThread | None = None
self.load_worker: LoadWorker | None = None
self.load_refine_thread: QThread | None = None
self.load_refine_worker: LoadWorker | None = None
self.pending_load_path: Path | None = None
self.initial_load_deflection = 0.8
self.initial_load_deflection = 2.0
self.last_id_kind = "Face"
self._build_ui()
@@ -169,56 +172,154 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
panel_scroll.setStyleSheet(
"""
QScrollArea {
background: #f4f7fb;
background: #edf2f8;
border: none;
}
QScrollArea > QWidget > QWidget {
background: #f4f7fb;
background: #edf2f8;
}
QScrollBar:vertical {
background: #e3eaf2;
border: none;
border-radius: 5px;
margin: 0;
width: 10px;
}
QScrollBar::handle:vertical {
background: #aab8c8;
border-radius: 5px;
min-height: 36px;
}
QScrollBar::handle:vertical:hover {
background: #8798ad;
}
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
height: 0;
width: 0;
}
QGroupBox {
background: #ffffff;
border: 2px solid #6b7cff;
border: 1px solid #d4dde8;
border-left: 5px solid #6b7cff;
border-radius: 8px;
color: #1f2937;
margin-top: 12px;
padding: 12px 10px 10px 10px;
margin-top: 14px;
padding: 15px 10px 10px 12px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 12px;
padding: 0 6px;
left: 14px;
padding: 2px 8px;
color: #172033;
background: #f4f7fb;
background: #ffffff;
border: 1px solid #d4dde8;
border-radius: 7px;
font-weight: 700;
}
QGroupBox#fileSection {
border-color: #3478f6;
background: #f7fbff;
border: 1px solid #bdd3ff;
border-left: 5px solid #3478f6;
}
QGroupBox#fileSection::title {
background: #eaf2ff;
border-color: #bdd3ff;
color: #1d4ed8;
}
QGroupBox#treeSection {
border-color: #0f9f8f;
background: #f2fbf8;
border: 1px solid #b8dfd6;
border-left: 5px solid #0f9f8f;
}
QGroupBox#treeSection::title {
background: #e2f5ef;
border-color: #b8dfd6;
color: #0f766e;
}
QGroupBox#modeSection,
QGroupBox#idSection {
border-color: #6b5cff;
background: #f7f5ff;
border: 1px solid #cbc4ff;
border-left: 5px solid #6b5cff;
}
QGroupBox#modeSection::title,
QGroupBox#idSection::title {
background: #eeeaff;
border-color: #cbc4ff;
color: #5642c7;
}
QGroupBox#viewSection {
border-color: #0e9bd8;
background: #f1faff;
border: 1px solid #b6dff2;
border-left: 5px solid #0e9bd8;
}
QGroupBox#viewSection::title {
background: #e3f4fd;
border-color: #b6dff2;
color: #0369a1;
}
QGroupBox#measureSection {
background: #f1fbfb;
border: 1px solid #b6e0e1;
border-left: 5px solid #14a3a8;
}
QGroupBox#measureSection::title {
background: #e0f5f6;
border-color: #b6e0e1;
color: #0f777b;
}
QGroupBox#exportSection {
border-color: #1f9d55;
background: #f4fbf3;
border: 1px solid #bfdcbd;
border-left: 5px solid #1f9d55;
}
QGroupBox#exportSection::title {
background: #e8f5e7;
border-color: #bfdcbd;
color: #1f7a3d;
}
QGroupBox#editSection {
border-color: #d97706;
background: #fff8ef;
border: 1px solid #edc98e;
border-left: 5px solid #d97706;
}
QGroupBox#editSection::title {
background: #fff0d8;
border-color: #edc98e;
color: #a45303;
}
QGroupBox#editableSection,
QGroupBox#candidateSection {
border-color: #b45309;
background: #fff7ed;
border: 1px solid #e6c297;
border-left: 5px solid #b45309;
}
QGroupBox#editableSection::title,
QGroupBox#candidateSection::title {
background: #ffefd7;
border-color: #e6c297;
color: #92400e;
}
QGroupBox#historySection {
border-color: #be3b6b;
background: #fff5f8;
border: 1px solid #edb8cb;
border-left: 5px solid #be3b6b;
}
QGroupBox#historySection::title {
background: #fde7ef;
border-color: #edb8cb;
color: #9d2854;
}
QGroupBox#infoSection {
border-color: #64748b;
background: #f8fafc;
border: 1px solid #cbd5e1;
border-left: 5px solid #64748b;
}
QGroupBox#infoSection::title {
background: #eef2f7;
border-color: #cbd5e1;
color: #475569;
}
QLabel {
color: #2f3846;
@@ -243,7 +344,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
font-weight: 700;
}
QPushButton {
background: #eef3f8;
background: #f8fafc;
border: 1px solid #c8d2df;
border-radius: 6px;
color: #172033;
@@ -252,16 +353,31 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
padding: 5px 8px;
}
QPushButton:hover {
background: #e3edf9;
border-color: #8fb0dc;
background: #eef6ff;
border-color: #77a7dd;
}
QPushButton:pressed {
background: #d7e6f6;
}
QPushButton:disabled {
background: #eef0f3;
border-color: #dfe3ea;
color: #7a7f86;
background: #e3e8ef;
border: 1px dashed #b6c0cd;
color: #8f99a8;
font-weight: 500;
}
QLineEdit:disabled,
QComboBox:disabled,
QPlainTextEdit:disabled {
background: #eef2f6;
border: 1px dashed #bcc7d4;
color: #8f99a8;
}
QCheckBox:disabled,
QTabWidget:disabled,
QTreeWidget:disabled,
QTableWidget:disabled,
QListWidget:disabled {
color: #8f99a8;
}
QTreeWidget,
QTableWidget,
@@ -271,11 +387,12 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
border: 1px solid #d8e0eb;
border-radius: 6px;
color: #172033;
selection-background-color: #dceafe;
alternate-background-color: #f7fafd;
selection-background-color: #d7ecff;
selection-color: #0f172a;
}
QHeaderView::section {
background: #edf2f7;
background: #e8eef6;
border: 0;
border-bottom: 1px solid #d8e0eb;
color: #334155;
@@ -315,14 +432,14 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
file_layout.addWidget(self.path_label)
file_buttons = QHBoxLayout()
open_button = QPushButton("打开")
help_tip(open_button, "选择并打开一个 .step 或 .stp 文件。打开失败时会保留当前模型。")
open_button.clicked.connect(self.open_step)
reload_button = QPushButton("重新加载")
help_tip(reload_button, "从磁盘重新读取当前 STEP 文件,用于放弃本次会话里的临时查看状态。")
reload_button.clicked.connect(self.reload_step)
file_buttons.addWidget(open_button)
file_buttons.addWidget(reload_button)
self.open_button = QPushButton("打开")
help_tip(self.open_button, "选择并打开一个 .step 或 .stp 文件。打开失败时会保留当前模型。")
self.open_button.clicked.connect(self.open_step)
self.reload_button = QPushButton("重新加载")
help_tip(self.reload_button, "从磁盘重新读取当前 STEP 文件,用于放弃本次会话里的临时查看状态。")
self.reload_button.clicked.connect(self.reload_step)
file_buttons.addWidget(self.open_button)
file_buttons.addWidget(self.reload_button)
file_layout.addLayout(file_buttons)
panel_layout.addWidget(file_box)
@@ -333,21 +450,22 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.part_tree = QTreeWidget()
self.part_tree.setHeaderLabels(["对象", "内容"])
self.part_tree.setMinimumHeight(180)
help_tip(self.part_tree, "模型里的装配、零件和 solid 列表。点击一行可以选中并高亮对应对象。")
self.part_tree.setAlternatingRowColors(True)
help_tip(self.part_tree, "模型里的装配、零件和Solid列表。点击一行可以选中并高亮对应对象。")
self.part_tree.currentItemChanged.connect(self.on_part_tree_select)
tree_layout.addWidget(self.part_tree)
panel_layout.addWidget(tree_box)
mode_box = QGroupBox("鼠标选择模式")
mode_box.setObjectName("modeSection")
help_tip(mode_box, "决定鼠标点模型时选中零件、solid、面、边,还是识别局部特征。")
help_tip(mode_box, "决定鼠标点模型时选中 Part、Solid、Face、Edge,还是识别局部特征。")
mode_layout = QVBoxLayout(mode_box)
self.mode_combo = NoWheelComboBox()
self.mode_combo.addItems(["Part", "Solid", "Face", "Edge", "Feature"])
self.mode_combo.setCurrentText("Face")
help_tip(
self.mode_combo,
"选择鼠标点击模型时要选什么:零件、solid、面、边,或把面解释成孔/槽/圆角等特征候选。",
"选择鼠标点击模型时要选什么:Part、Solid、Face、Edge,或把 Face 解释成孔/槽/圆角等特征候选。",
)
self.mode_combo.currentTextChanged.connect(self._on_mode_changed)
mode_layout.addWidget(self.mode_combo)
@@ -367,14 +485,15 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.id_mode_display.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.id_mode_display.setMinimumWidth(70)
help_tip(self.id_mode_display, "当前按这个鼠标选择模式解释输入的 ID。它会跟随鼠标选择模式自动变化。")
select_button = QPushButton("选择")
help_tip(select_button, "按当前选择模式跳转到输入的 ID,并在 3D 视图中高亮它。")
select_button.clicked.connect(lambda _checked=False: self.select_by_id(self.mode_combo.currentText()))
self.select_id_button = QPushButton("选择")
help_tip(self.select_id_button, "按当前选择模式跳转到输入的 ID,并在 3D 视图中高亮它。")
self.select_id_button.clicked.connect(lambda _checked=False: self.select_by_id(self.mode_combo.currentText()))
self.id_input.textChanged.connect(lambda _text: self._update_action_states())
self.id_input.returnPressed.connect(lambda: self.select_by_id(self.mode_combo.currentText()))
select_layout.addWidget(QLabel("ID"), 0, 0)
select_layout.addWidget(self.id_input, 0, 1)
select_layout.addWidget(self.id_mode_display, 0, 2)
select_layout.addWidget(select_button, 0, 3)
select_layout.addWidget(self.select_id_button, 0, 3)
panel_layout.addWidget(select_box)
view_box = QGroupBox("显示")
@@ -382,18 +501,18 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
help_tip(view_box, "只改变视图显示方式,不会修改 STEP 几何。")
view_layout = QVBoxLayout(view_box)
view_buttons = QHBoxLayout()
isolate_button = QPushButton("只显示选中")
help_tip(isolate_button, "把视图临时隔离到当前选中的零件、solid、面、边或特征区域。不会修改模型。")
isolate_button.clicked.connect(self.isolate_selected)
fit_button = QPushButton("对准选中")
help_tip(fit_button, "把相机移动到当前选中对象附近,方便看清细节。不会修改模型。")
fit_button.clicked.connect(self.fit_selected)
show_all_button = QPushButton("显示全部")
help_tip(show_all_button, "取消隔离显示,恢复查看完整模型。不会修改模型。")
show_all_button.clicked.connect(self.show_all_geometry)
view_buttons.addWidget(isolate_button)
view_buttons.addWidget(fit_button)
view_buttons.addWidget(show_all_button)
self.isolate_button = QPushButton("只显示选中")
help_tip(self.isolate_button, "把视图临时隔离到当前选中的 Part、Solid、Face、Edge 或特征区域。不会修改模型。")
self.isolate_button.clicked.connect(self.isolate_selected)
self.fit_button = QPushButton("对准选中")
help_tip(self.fit_button, "把相机移动到当前选中对象附近,方便看清细节。不会修改模型。")
self.fit_button.clicked.connect(self.fit_selected)
self.show_all_button = QPushButton("显示全部")
help_tip(self.show_all_button, "取消隔离显示,恢复查看完整模型。不会修改模型。")
self.show_all_button.clicked.connect(self.show_all_geometry)
view_buttons.addWidget(self.isolate_button)
view_buttons.addWidget(self.fit_button)
view_buttons.addWidget(self.show_all_button)
view_layout.addLayout(view_buttons)
self.show_internal_edges_checkbox = QCheckBox("显示同域内部边")
help_tip(
@@ -402,10 +521,9 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
)
self.show_internal_edges_checkbox.toggled.connect(self._on_internal_edges_toggled)
view_layout.addWidget(self.show_internal_edges_checkbox)
panel_layout.addWidget(view_box)
measure_box = QGroupBox("测量")
measure_box.setObjectName("viewSection")
measure_box.setObjectName("measureSection")
help_tip(measure_box, "把当前选中对象的拾取点或中心设为 A/B,计算两点距离和 X/Y/Z 差值。不会修改模型。")
measure_layout = QVBoxLayout(measure_box)
self.measure_text = QPlainTextEdit()
@@ -413,25 +531,24 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.measure_text.setMaximumHeight(90)
help_tip(self.measure_text, "显示 A 点、B 点、两点距离和各方向差值。")
measure_buttons = QGridLayout()
set_measure_a_button = QPushButton("设为 A")
help_tip(set_measure_a_button, "把当前选中对象的拾取点设为测量点 A;没有拾取点时使用对象中心。")
set_measure_a_button.clicked.connect(lambda _checked=False: self.set_measure_point("A"))
set_measure_b_button = QPushButton("设为 B")
help_tip(set_measure_b_button, "把当前选中对象的拾取点设为测量点 B;没有拾取点时使用对象中心。")
set_measure_b_button.clicked.connect(lambda _checked=False: self.set_measure_point("B"))
copy_measure_button = QPushButton("复制测量")
help_tip(copy_measure_button, "复制当前测量结果文本,方便记录尺寸。")
copy_measure_button.clicked.connect(self.copy_measurement)
clear_measure_button = QPushButton("清除测量")
help_tip(clear_measure_button, "清除 A/B 测量点和 3D 测量线。不会影响模型。")
clear_measure_button.clicked.connect(self.clear_measurement)
measure_buttons.addWidget(set_measure_a_button, 0, 0)
measure_buttons.addWidget(set_measure_b_button, 0, 1)
measure_buttons.addWidget(copy_measure_button, 1, 0)
measure_buttons.addWidget(clear_measure_button, 1, 1)
self.set_measure_a_button = QPushButton("设为 A")
help_tip(self.set_measure_a_button, "把当前选中对象的拾取点设为测量点 A;没有拾取点时使用对象中心。")
self.set_measure_a_button.clicked.connect(lambda _checked=False: self.set_measure_point("A"))
self.set_measure_b_button = QPushButton("设为 B")
help_tip(self.set_measure_b_button, "把当前选中对象的拾取点设为测量点 B;没有拾取点时使用对象中心。")
self.set_measure_b_button.clicked.connect(lambda _checked=False: self.set_measure_point("B"))
self.copy_measure_button = QPushButton("复制测量")
help_tip(self.copy_measure_button, "复制当前测量结果文本,方便记录尺寸。")
self.copy_measure_button.clicked.connect(self.copy_measurement)
self.clear_measure_button = QPushButton("清除测量")
help_tip(self.clear_measure_button, "清除 A/B 测量点和 3D 测量线。不会影响模型。")
self.clear_measure_button.clicked.connect(self.clear_measurement)
measure_buttons.addWidget(self.set_measure_a_button, 0, 0)
measure_buttons.addWidget(self.set_measure_b_button, 0, 1)
measure_buttons.addWidget(self.copy_measure_button, 1, 0)
measure_buttons.addWidget(self.clear_measure_button, 1, 1)
measure_layout.addWidget(self.measure_text)
measure_layout.addLayout(measure_buttons)
panel_layout.addWidget(measure_box)
self._refresh_measurement_panel()
export_box = QGroupBox("导出")
@@ -442,10 +559,10 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
help_tip(self.export_all_button, "把当前编辑后的整个模型导出为 STEP 文件。导出前会做基础质量检查。")
self.export_all_button.clicked.connect(self.export_all)
self.export_part_button = QPushButton("导出选中零件")
help_tip(self.export_part_button, "只导出当前选中的零件。适合从装配里拆出一个 part")
help_tip(self.export_part_button, "只导出当前选中的零件。适合从装配里拆出一个单独零件")
self.export_part_button.clicked.connect(self.export_selected_part)
self.export_solid_button = QPushButton("导出选中 solid")
help_tip(self.export_solid_button, "只导出当前选中的实体 solid。适合检查或单独保存某个实体。")
self.export_solid_button = QPushButton("导出选中Solid")
help_tip(self.export_solid_button, "只导出当前选中的实体Solid。适合检查或单独保存某个实体。")
self.export_solid_button.clicked.connect(self.export_selected_solid)
self.export_face_button = QPushButton("导出选中面区域")
help_tip(self.export_face_button, "导出当前选中的面区域;同域高亮的共面/同圆柱区域也会一起导出。")
@@ -453,17 +570,17 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.export_feature_button = QPushButton("导出选中特征区域")
help_tip(self.export_feature_button, "导出 Feature 模式识别到的局部特征区域,例如孔、槽、圆角或凸台候选。")
self.export_feature_button.clicked.connect(self.export_selected_feature)
self.export_edge_button = QPushButton("导出选中 edge")
help_tip(self.export_edge_button, "导出当前选中的。主要用于调试、定位或把边界单独拿出去检查。")
self.export_edge_button = QPushButton("导出选中Edge")
help_tip(self.export_edge_button, "导出当前选中的Edge。主要用于调试、定位或把边界单独拿出去检查。")
self.export_edge_button.clicked.connect(self.export_selected_edge)
self.export_check_button = QPushButton("检查导出质量")
help_tip(self.export_check_button, "检查当前导出对象是否像有效实体:B-Rep、face/edge 数量、体积和包围盒等。")
help_tip(self.export_check_button, "检查当前导出对象是否像有效实体:B-Rep、Face/Edge 数量、体积和包围盒等。")
self.export_check_button.clicked.connect(self.check_export_quality)
self.repair_model_button = QPushButton("修复当前模型")
help_tip(self.repair_model_button, "对完整模型尝试 ShapeFix 和同域面/边合并。会写入历史,可撤销。")
self.repair_model_button.clicked.connect(self.repair_model)
self.repair_selected_button = QPushButton("修复选中零件/solid")
help_tip(self.repair_selected_button, "只修复当前选中的零件或 solid,范围比修复完整模型更小。会写入历史,可撤销。")
self.repair_selected_button = QPushButton("修复选中零件/Solid")
help_tip(self.repair_selected_button, "只修复当前选中的零件或Solid,范围比修复完整模型更小。会写入历史,可撤销。")
self.repair_selected_button.clicked.connect(self.repair_selected_shape)
export_layout.addWidget(self.export_all_button)
export_layout.addWidget(self.export_part_button)
@@ -474,13 +591,12 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
export_layout.addWidget(self.export_check_button)
export_layout.addWidget(self.repair_model_button)
export_layout.addWidget(self.repair_selected_button)
panel_layout.addWidget(export_box)
edit_box = QGroupBox("实验性编辑")
edit_box = QGroupBox("编辑")
edit_box.setObjectName("editSection")
help_tip(edit_box, "这里的按钮会真实修改当前 B-Rep 模型;执行前通常会预览,成功后可撤销。")
help_tip(edit_box, "这里的按钮会真实修改当前 B-Rep 模型;执行前通常会预览,成功后可撤销。部分能力仍是当前版本的几何近似。")
edit_layout = QGridLayout(edit_box)
edit_layout.addWidget(QLabel("偏移"), 0, 0)
edit_layout.addWidget(QLabel("Face偏移"), 0, 0)
self.offset_input = QLineEdit("5.0")
help_tip(self.offset_input, "平面推拉距离。正数通常向外加料,负数通常向内切削;单位沿用 STEP 模型单位。")
edit_layout.addWidget(self.offset_input, 0, 1)
@@ -503,7 +619,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
help_tip(self.slot_width_input, "槽或半孔的目标开口宽度。程序会把它换算成对应圆柱直径来执行。")
edit_layout.addWidget(self.slot_width_input, 4, 1)
self.resize_slot_button = QPushButton("调整槽/半孔宽度")
help_tip(self.resize_slot_button, "修改已识别槽/半孔候选的宽度。第一版是几何近似,失败会回滚。")
help_tip(self.resize_slot_button, "修改已识别槽/半孔候选的宽度。当前版本是几何近似,失败会回滚。")
self.resize_slot_button.clicked.connect(self.resize_slot_width)
edit_layout.addWidget(self.resize_slot_button, 5, 0, 1, 2)
@@ -541,10 +657,10 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
edit_layout.addWidget(QLabel("圆角半径"), 11, 0)
self.edge_fillet_radius_input = QLineEdit("")
help_tip(self.edge_fillet_radius_input, "新圆角或已有圆角的目标半径。选中/圆角候选后会自动填参考值。")
help_tip(self.edge_fillet_radius_input, "新圆角或已有圆角的目标半径。选中Edge/圆角候选后会自动填参考值。")
edit_layout.addWidget(self.edge_fillet_radius_input, 11, 1)
self.fillet_edge_button = QPushButton("添加圆角")
help_tip(self.fillet_edge_button, "给当前直线新增圆角。不是修改已有圆角;已有圆角请用下面那个按钮。")
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)
@@ -555,20 +671,20 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
edit_layout.addWidget(QLabel("倒角距离"), 14, 0)
self.edge_chamfer_distance_input = QLineEdit("")
help_tip(self.edge_chamfer_distance_input, "添加倒角时使用的距离。选中直线后会填一个较小参考值。")
help_tip(self.edge_chamfer_distance_input, "Edge添加倒角时使用的距离。选中直线Edge后会填一个较小参考值。")
edit_layout.addWidget(self.edge_chamfer_distance_input, 14, 1)
self.chamfer_edge_button = QPushButton("添加倒角")
help_tip(self.chamfer_edge_button, "给当前直线新增对称倒角。会先预览,再后台执行,失败会回滚。")
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(QLabel("目标长度"), 16, 0)
edit_layout.addWidget(QLabel("Edge目标长度"), 16, 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 的目标长度。选中后会自动填当前长度,改成新长度再执行。")
self.edge_length_anchor_combo = QComboBox()
help_tip(self.edge_target_length_input, "当前Edge的目标长度。选中Edge后会自动填当前长度,改成新长度再执行。")
self.edge_length_anchor_combo = NoWheelComboBox()
self.edge_length_anchor_combo.addItem("自动", "auto")
self.edge_length_anchor_combo.addItem("中心", "center")
self.edge_length_anchor_combo.addItem("固定起点", "keep-start")
@@ -577,20 +693,35 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.edge_length_anchor_combo.setMinimumWidth(90)
help_tip(
self.edge_length_anchor_combo,
"选择边长修改时尽量固定哪里:自动会优先找局部端面;中心/起点/终点会影响缩放 fallback 的基准点。",
"选择修改Edge长度时尽量固定哪里:自动默认固定起点并移动终点;中心会两端各动一半;固定起点/终点会明确控制局部形变端点。",
)
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)
self.resize_edge_length_button = QPushButton("直接修改边长")
self.resize_edge_length_button = QPushButton("修改Edge长度")
help_tip(
self.resize_edge_length_button,
"直接修改当前 edge 长度。直线边优先端面推拉,圆边优先换算相邻圆柱直径,必要时再用几何缩放 fallback",
"修改当前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(QLabel("平移 X/Y/Z"), 18, 0)
edit_layout.addWidget(QLabel("薄壁厚度"), 18, 0)
self.shell_thickness_input = QLineEdit("")
help_tip(
self.shell_thickness_input,
"薄壁/壳体局部区域的目标厚度。选中有相对平面的平面候选后会自动填一个参考值。",
)
edit_layout.addWidget(self.shell_thickness_input, 18, 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(QLabel("平移 X/Y/Z"), 20, 0)
translate_inputs = QWidget()
translate_layout = QHBoxLayout(translate_inputs)
translate_layout.setContentsMargins(0, 0, 0, 0)
@@ -606,21 +737,21 @@ 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, 18, 1)
edit_layout.addWidget(translate_inputs, 20, 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 = 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, 19, 0, 1, 2)
edit_layout.addWidget(self.translate_solid_button, 20, 0, 1, 2)
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(QLabel("旋转轴/角度"), 21, 0)
edit_layout.addWidget(QLabel("旋转轴/角度"), 23, 0)
rotate_inputs = QWidget()
rotate_layout = QHBoxLayout(rotate_inputs)
rotate_layout.setContentsMargins(0, 0, 0, 0)
self.rotate_axis_combo = QComboBox()
self.rotate_axis_combo = NoWheelComboBox()
self.rotate_axis_combo.addItems(["X", "Y", "Z"])
self.rotate_axis_combo.setCurrentText("Z")
help_tip(self.rotate_axis_combo, "选择旋转轴。对象会绕自身包围盒中心旋转。")
@@ -629,20 +760,20 @@ 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, 21, 1)
edit_layout.addWidget(rotate_inputs, 23, 1)
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 = 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, 22, 0, 1, 2)
edit_layout.addWidget(self.rotate_solid_button, 23, 0, 1, 2)
edit_layout.addWidget(self.rotate_part_button, 24, 0, 1, 2)
edit_layout.addWidget(self.rotate_solid_button, 25, 0, 1, 2)
self.cylinders_button = QPushButton("列出圆柱候选")
help_tip(self.cylinders_button, "扫描模型里的圆柱面,并粗略判断它们像孔、槽、圆角、凸台还是普通圆柱。")
self.cylinders_button = QPushButton("扫描圆柱面")
help_tip(self.cylinders_button, "扫描模型里的圆柱面候选,并粗略判断它们像孔、槽、圆角、凸台还是普通圆柱。")
self.cylinders_button.clicked.connect(self.list_cylinders)
edit_layout.addWidget(self.cylinders_button, 24, 0, 1, 2)
edit_layout.addWidget(self.cylinders_button, 26, 0, 1, 2)
self.undo_button = QPushButton("撤销")
help_tip(self.undo_button, "撤销上一次成功编辑,把模型恢复到编辑前快照。")
@@ -650,16 +781,19 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.redo_button = QPushButton("重做")
help_tip(self.redo_button, "重做刚刚撤销的编辑。")
self.redo_button.clicked.connect(self.redo_edit)
edit_layout.addWidget(self.undo_button, 25, 0)
edit_layout.addWidget(self.redo_button, 25, 1)
edit_layout.addWidget(self.undo_button, 27, 0)
edit_layout.addWidget(self.redo_button, 27, 1)
panel_layout.addWidget(edit_box)
panel_layout.addWidget(export_box)
panel_layout.addWidget(view_box)
panel_layout.addWidget(measure_box)
editable_box = QGroupBox("第一版可编辑对象")
editable_box = QGroupBox("可编辑对象")
editable_box.setObjectName("editableSection")
help_tip(editable_box, "自动找出一批可以尝试编辑的 face 或 edge,方便不用手动到处点。")
help_tip(editable_box, "自动找出当前版本可以尝试编辑的Face或Edge,方便不用手动到处点。")
editable_layout = QVBoxLayout(editable_box)
editable_button_row = QHBoxLayout()
self.editable_refresh_button = QPushButton("扫描可编辑对象")
self.editable_refresh_button = QPushButton("扫描对象")
help_tip(self.editable_refresh_button, "快速扫描一批可以尝试编辑的对象,并列在下面表格里。")
self.editable_refresh_button.clicked.connect(lambda _checked=False: self.refresh_editable_candidates(show_info=True))
self.editable_deep_scan_button = QPushButton("深度扫描")
@@ -678,14 +812,15 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.editable_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.editable_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.editable_table.setMinimumHeight(130)
help_tip(self.editable_table, "这里列出可尝试编辑的对象。点击一行会选中模型里的对应 face 或 edge,并自动填入相关输入框。")
self.editable_table.setAlternatingRowColors(True)
help_tip(self.editable_table, "这里列出可尝试编辑的对象。点击一行会选中模型里的对应Face或Edge,并自动填入相关输入框。")
self.editable_table.cellClicked.connect(self.on_editable_row_clicked)
editable_layout.addWidget(self.editable_table)
panel_layout.addWidget(editable_box)
candidate_box = QGroupBox("圆柱候选")
candidate_box = QGroupBox("圆柱候选")
candidate_box.setObjectName("candidateSection")
help_tip(candidate_box, "列出圆柱面并粗略判断它们像孔、槽、圆角、凸台还是普通圆柱。")
help_tip(candidate_box, "列出圆柱面候选,并粗略判断它们像孔、槽、圆角、凸台还是普通圆柱。")
candidate_layout = QVBoxLayout(candidate_box)
filter_layout = QHBoxLayout()
filter_layout.addWidget(QLabel("类型"))
@@ -701,7 +836,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
)
help_tip(
self.candidate_filter_combo,
"筛选下面的圆柱候选:孔/槽、圆角、凸台/外圆,或暂时无法明确分类的圆柱面。",
"筛选下面的圆柱候选:孔/槽、圆角、凸台/外圆,或暂时无法明确分类的圆柱面。",
)
self.candidate_filter_combo.currentTextChanged.connect(
lambda _text: self._filter_cached_cylinder_candidates(show_info=True)
@@ -710,13 +845,14 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
candidate_layout.addLayout(filter_layout)
self.cylinder_table = QTableWidget(0, 8)
self.cylinder_table.setHorizontalHeaderLabels(
["face", "guess", "diameter", "span", "height", "confidence", "risk", "part"]
["Face", "Guess", "Diameter", "Span", "Height", "Confidence", "Risk", "Part"]
)
self.cylinder_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.cylinder_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.cylinder_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.cylinder_table.setMinimumHeight(140)
help_tip(self.cylinder_table, "这里显示圆柱面候选。点击一行会选中对应 face,并在属性区显示可用操作和风险。")
self.cylinder_table.setAlternatingRowColors(True)
help_tip(self.cylinder_table, "这里显示圆柱面候选。点击一行会选中对应Face,并在属性区显示可用操作和风险。")
self.cylinder_table.cellClicked.connect(self.on_cylinder_row_clicked)
candidate_layout.addWidget(self.cylinder_table)
panel_layout.addWidget(candidate_box)
@@ -729,20 +865,21 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.history_list.setMinimumHeight(110)
help_tip(self.history_list, "成功执行过的编辑会出现在这里。点击一条记录可查看参数、差异预览和目标位置。")
self.history_list.currentRowChanged.connect(self.on_history_row_changed)
self.history_list.currentRowChanged.connect(lambda _row: self._update_action_states())
history_buttons = QHBoxLayout()
clear_diff_button = QPushButton("清除差异预览")
help_tip(clear_diff_button, "关闭历史记录产生的红/绿差异叠加和热力图显示。不会修改模型。")
clear_diff_button.clicked.connect(lambda _checked=False: self.clear_diff_preview())
export_diff_button = QPushButton("导出差异报告")
help_tip(export_diff_button, "把当前选中的历史记录导出成文本报告,包含参数、拓扑变化和几何差异统计。")
export_diff_button.clicked.connect(self.export_diff_report)
export_history_button = QPushButton("导出编辑历史")
help_tip(export_history_button, "把本次会话里的所有编辑记录导出为 JSON,方便留档或后续复盘。")
export_history_button.clicked.connect(self.export_operation_history)
self.clear_diff_button = QPushButton("清除差异预览")
help_tip(self.clear_diff_button, "关闭历史记录产生的红/绿差异叠加和热力图显示。不会修改模型。")
self.clear_diff_button.clicked.connect(lambda _checked=False: self.clear_diff_preview())
self.export_diff_button = QPushButton("导出差异报告")
help_tip(self.export_diff_button, "把当前选中的历史记录导出成文本报告,包含参数、拓扑变化和几何差异统计。")
self.export_diff_button.clicked.connect(self.export_diff_report)
self.export_history_button = QPushButton("导出编辑历史")
help_tip(self.export_history_button, "把本次会话里的所有编辑记录导出为 JSON,方便留档或后续复盘。")
self.export_history_button.clicked.connect(self.export_operation_history)
history_layout.addWidget(self.history_list)
history_buttons.addWidget(clear_diff_button)
history_buttons.addWidget(export_diff_button)
history_buttons.addWidget(export_history_button)
history_buttons.addWidget(self.clear_diff_button)
history_buttons.addWidget(self.export_diff_button)
history_buttons.addWidget(self.export_history_button)
history_layout.addLayout(history_buttons)
panel_layout.addWidget(history_box)
@@ -751,18 +888,18 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
help_tip(info_box, "显示当前选中对象的几何、拓扑、特征判断和可用操作信息。")
info_layout = QVBoxLayout(info_box)
info_buttons = QHBoxLayout()
copy_id_button = QPushButton("复制 ID")
help_tip(copy_id_button, "复制当前选中对象的 ID。Face/Feature 会优先复制逻辑 Face ID。")
copy_id_button.clicked.connect(self.copy_selected_id)
copy_pick_button = QPushButton("复制坐标")
help_tip(copy_pick_button, "复制最近一次鼠标点到模型上的三维坐标。")
copy_pick_button.clicked.connect(self.copy_pick_position)
copy_info_button = QPushButton("复制信息")
help_tip(copy_info_button, "复制当前属性区里的完整文本,方便发给别人或做问题记录。")
copy_info_button.clicked.connect(self.copy_current_info)
info_buttons.addWidget(copy_id_button)
info_buttons.addWidget(copy_pick_button)
info_buttons.addWidget(copy_info_button)
self.copy_id_button = QPushButton("复制 ID")
help_tip(self.copy_id_button, "复制当前选中对象的 ID。Face/Feature 会优先复制逻辑 Face ID。")
self.copy_id_button.clicked.connect(self.copy_selected_id)
self.copy_pick_button = QPushButton("复制坐标")
help_tip(self.copy_pick_button, "复制最近一次鼠标点到模型上的三维坐标。")
self.copy_pick_button.clicked.connect(self.copy_pick_position)
self.copy_info_button = QPushButton("复制信息")
help_tip(self.copy_info_button, "复制当前属性区里的完整文本,方便发给别人或做问题记录。")
self.copy_info_button.clicked.connect(self.copy_current_info)
info_buttons.addWidget(self.copy_id_button)
info_buttons.addWidget(self.copy_pick_button)
info_buttons.addWidget(self.copy_info_button)
info_layout.addLayout(info_buttons)
self.info_tabs = QTabWidget()
@@ -803,7 +940,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
def _parse_args(argv: list[str]) -> tuple[Path, bool]:
smoke_test = "--smoke-test" in argv
paths = [arg for arg in argv[1:] if not arg.startswith("--")]
path = Path(paths[0]) if paths else Path("geom_extract.step")
path = Path(paths[0]) if paths else DEFAULT_MODEL_PATH
return path, smoke_test