feat: 完善参数化编辑和孔修改稳定性

This commit is contained in:
2026-07-28 18:30:58 +08:00
parent 7c263d1e96
commit 720c2dc970
13 changed files with 2190 additions and 690 deletions
+447 -279
View File
@@ -7,15 +7,17 @@ import sys
from pathlib import Path
import vtk
from PySide6.QtCore import Qt, QThread, QTimer, Slot
from PySide6.QtCore import Qt, QThread, QTimer, Signal, Slot
from PySide6.QtWidgets import (
QAbstractItemView,
QApplication,
QCheckBox,
QComboBox,
QFileDialog,
QFrame,
QGridLayout,
QGroupBox,
QHeaderView,
QHBoxLayout,
QLabel,
QLineEdit,
@@ -25,6 +27,7 @@ from PySide6.QtWidgets import (
QPushButton,
QPlainTextEdit,
QScrollArea,
QSizePolicy,
QTableWidget,
QTableWidgetItem,
QTabWidget,
@@ -52,6 +55,16 @@ _CRASH_LOG_HANDLE = None
PROJECT_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "geom_extract.step"
# Panel build switches. These work like a Python-side "#if 0": code in a
# disabled branch is kept for later, but the widgets are not constructed.
ENABLE_STRUCTURE_TREE_PANEL = False
ENABLE_VIEW_PANEL = False
ENABLE_MEASURE_PANEL = False
ENABLE_EXPORT_PANEL = False
ENABLE_EDITABLE_OBJECTS_PANEL = False
ENABLE_CYLINDER_CANDIDATES_PANEL = False
ENABLE_HISTORY_PANEL = False
def _enable_crash_log() -> None:
global _CRASH_LOG_HANDLE
@@ -66,9 +79,12 @@ def _enable_crash_log() -> None:
class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, InfoPanelMixin, QMainWindow):
ui_task_requested = Signal(object)
def __init__(self, step_path: str | Path, *, background_load: bool = True):
super().__init__()
self.setWindowTitle("STEP 零件查看与编辑原型")
self.ui_task_requested.connect(self._run_ui_task, Qt.ConnectionType.QueuedConnection)
self.setWindowTitle("几何参数化")
self.resize(1280, 820)
self.model: StepModel | None = None
@@ -143,6 +159,10 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.pending_load_path: Path | None = None
self.initial_load_deflection = 2.0
self.last_id_kind = "Face"
self.property_editor_updating = False
self.property_editor_specs: list[dict[str, object]] = []
self.property_table_expanded = False
self.property_table_collapsed_rows = 6
self._build_ui()
self._build_vtk()
@@ -237,17 +257,31 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
border-color: #b8dfd6;
color: #0f766e;
}
QGroupBox#modeSection,
QGroupBox#idSection {
background: #f7f5ff;
border: 1px solid #cbc4ff;
border-left: 5px solid #6b5cff;
QFrame#modeSection {
background: #f6f3ff;
border: 1px solid #a99bff;
border-left: 5px solid #5b4bdb;
border-radius: 7px;
}
QGroupBox#modeSection::title,
QGroupBox#idSection::title {
background: #eeeaff;
border-color: #cbc4ff;
color: #5642c7;
QLabel#modeSectionTitle {
background: #ebe7ff;
border: 1px solid #a99bff;
border-radius: 5px;
color: #4736b3;
font-weight: 700;
padding: 1px 8px;
}
QFrame#modeIdSeparator {
background: #ded8ff;
border-left: 2px solid #5b4bdb;
border-right: 1px solid #b8adff;
max-width: 8px;
min-width: 8px;
}
QLabel#mouseModeLabel,
QLabel#idSelectLabel {
color: #4736b3;
font-weight: 700;
}
QGroupBox#viewSection {
background: #f1faff;
@@ -337,12 +371,6 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
QComboBox:focus {
border: 1px solid #3478f6;
}
QLineEdit#idModeDisplay {
background: #eef2ff;
border-color: #93a5e8;
color: #2d3a8c;
font-weight: 700;
}
QPushButton {
background: #f8fafc;
border: 1px solid #c8d2df;
@@ -365,6 +393,24 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
color: #8f99a8;
font-weight: 500;
}
QPushButton#propertyRowEditButton {
background: #fff3d9;
border: 1px solid #d97706;
border-radius: 5px;
color: #92400e;
font-weight: 700;
min-height: 18px;
padding: 1px 6px;
}
QPushButton#propertyRowEditButton:hover {
background: #ffe6b8;
border-color: #b45309;
}
QPushButton#propertyRowEditButton:disabled {
background: #eef2f6;
border: 1px dashed #bcc7d4;
color: #8f99a8;
}
QLineEdit:disabled,
QComboBox:disabled,
QPlainTextEdit:disabled {
@@ -372,6 +418,26 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
border: 1px dashed #bcc7d4;
color: #8f99a8;
}
QLineEdit#idSelectInput {
background: #ffffff;
border: 1px solid #8f82ea;
color: #172033;
placeholder-text-color: #64748b;
}
QLineEdit#idSelectInput:focus {
background: #ffffff;
border: 1px solid #5b4bdb;
}
QLineEdit#idSelectInput:disabled {
background: #ffffff;
border: 1px solid #c8d2df;
color: #172033;
}
QLineEdit#stepPathDisplay {
background: #ffffff;
border: 1px solid #bdd3ff;
color: #172033;
}
QCheckBox:disabled,
QTabWidget:disabled,
QTreeWidget:disabled,
@@ -423,178 +489,273 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
file_box = QGroupBox("STEP 文件")
file_box.setObjectName("fileSection")
help_tip(file_box, "打开、重新加载和查看当前 STEP 文件路径")
help_tip(file_box, "导入、查看路径并读取当前 STEP 几何模型")
file_layout = QVBoxLayout(file_box)
self.path_label = QLabel("")
self.path_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
self.path_label.setWordWrap(True)
help_tip(self.path_label, "当前打开的 STEP 文件路径。可以选中文字复制路径。")
file_layout.addWidget(self.path_label)
file_buttons = QHBoxLayout()
self.open_button = QPushButton("打开")
help_tip(self.open_button, "选择并打开一个 .step 或 .stp 文件。打开失败时会保留当前模型")
file_buttons.setSpacing(6)
self.open_button = QPushButton("导入几何模型")
self.open_button.setMinimumWidth(96)
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.path_label = QLineEdit(str(self.step_path))
self.path_label.setObjectName("stepPathDisplay")
self.path_label.setReadOnly(True)
self.path_label.setMinimumWidth(120)
help_tip(self.path_label, "当前 STEP 文件的完整路径。可以选中文字复制路径。")
self.path_label.setToolTip(str(self.step_path))
self.path_label.setCursorPosition(0)
self.reload_button = QPushButton("读取模型")
self.reload_button.setMinimumWidth(78)
help_tip(self.reload_button, "从显示的路径重新读取 STEP 模型,用于放弃本次会话里的临时查看状态。")
self.reload_button.clicked.connect(self.reload_step)
file_buttons.addWidget(self.open_button)
file_buttons.addWidget(self.path_label, stretch=1)
file_buttons.addWidget(self.reload_button)
file_layout.addLayout(file_buttons)
panel_layout.addWidget(file_box)
tree_box = QGroupBox("模型结构树")
tree_box.setObjectName("treeSection")
help_tip(tree_box, "查看 STEP 里的装配、零件和实体层级,并从树上直接选中对象。")
tree_layout = QVBoxLayout(tree_box)
self.part_tree = QTreeWidget()
self.part_tree.setHeaderLabels(["对象", "内容"])
self.part_tree.setMinimumHeight(180)
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)
if ENABLE_STRUCTURE_TREE_PANEL:
tree_box = QGroupBox("模型结构树")
tree_box.setObjectName("treeSection")
help_tip(tree_box, "查看 STEP 里的装配、零件和实体层级,并从树上直接选中对象。")
tree_layout = QVBoxLayout(tree_box)
self.part_tree = QTreeWidget()
self.part_tree.setHeaderLabels(["对象", "内容"])
self.part_tree.setMinimumHeight(180)
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, "决定鼠标点模型时选中 Part、Solid、Face、Edge,还是识别局部特征。")
mode_layout = QVBoxLayout(mode_box)
mode_box = QWidget()
mode_box.setMinimumHeight(62)
help_tip(mode_box, "决定鼠标点模型时选中零件、Solid、Face、Edge,还是识别几何特征。")
self.mode_section_title = QLabel("选择模式", mode_box)
self.mode_section_title.setObjectName("modeSectionTitle")
self.mode_section_title.setFixedSize(74, 20)
self.mode_section_title.move(12, 0)
self.mode_section_title.raise_()
help_tip(self.mode_section_title, "选择鼠标点模型时按什么对象类型选择。")
mode_outer_layout = QVBoxLayout(mode_box)
mode_outer_layout.setContentsMargins(0, 9, 0, 0)
mode_outer_layout.setSpacing(0)
self.mode_section_frame = QFrame()
self.mode_section_frame.setObjectName("modeSection")
self.mode_section_frame.setMinimumHeight(50)
mode_outer_layout.addWidget(self.mode_section_frame)
self.mode_section_title.raise_()
mode_layout = QHBoxLayout(self.mode_section_frame)
mode_layout.setSpacing(0)
mode_layout.setContentsMargins(0, 0, 0, 0)
mode_pick_panel = QWidget()
mode_pick_layout = QHBoxLayout(mode_pick_panel)
mode_pick_layout.setContentsMargins(8, 10, 8, 6)
mode_pick_layout.setSpacing(6)
self.mouse_mode_label = QLabel("按鼠标:")
self.mouse_mode_label.setObjectName("mouseModeLabel")
self.mouse_mode_label.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignRight)
self.mouse_mode_label.setMaximumWidth(62)
help_tip(self.mouse_mode_label, "这里决定鼠标点击 3D 模型时按哪种对象类型选择。")
self.mode_combo = NoWheelComboBox()
self.mode_combo.addItems(["Part", "Solid", "Face", "Edge", "Feature"])
self.mode_combo.setCurrentText("Face")
for mode in ("Part", "Solid", "Face", "Edge", "Feature"):
self.mode_combo.addItem(_selection_mode_label(mode), mode)
self._set_selection_mode("Face")
self.mode_combo.setMinimumWidth(94)
self.mode_combo.setMaximumWidth(112)
help_tip(
self.mode_combo,
"选择鼠标点击模型时要选什么:Part、Solid、Face、Edge,或把 Face 解释成孔/槽/圆角等特征候选。",
"选择模式决定鼠标点击模型时要选什么:零件、Solid、Face、Edge,或把 Face 解释成孔/槽/圆角等几何特征候选。",
)
self.mode_combo.currentTextChanged.connect(self._on_mode_changed)
mode_layout.addWidget(self.mode_combo)
self.mode_combo.currentIndexChanged.connect(lambda _index: self._on_mode_changed(self._current_selection_mode()))
mode_pick_layout.addWidget(self.mouse_mode_label)
mode_pick_layout.addWidget(self.mode_combo)
mode_layout.addWidget(mode_pick_panel)
mode_separator = QFrame()
mode_separator.setFrameShape(QFrame.Shape.NoFrame)
mode_separator.setObjectName("modeIdSeparator")
mode_separator.setFixedWidth(8)
mode_separator.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
self.mode_id_separator = mode_separator
help_tip(mode_separator, "左侧是鼠标点选模式,右侧是按当前模式输入 ID 选择。")
mode_layout.addWidget(mode_separator)
id_select_panel = QWidget()
select_layout = QHBoxLayout(id_select_panel)
select_layout.setContentsMargins(8, 10, 8, 6)
select_layout.setSpacing(6)
self.id_select_label = QLabel(f"按ID{_selection_mode_label(self._current_selection_mode())}")
self.id_select_label.setObjectName("idSelectLabel")
self.id_select_label.setMinimumWidth(78)
help_tip(self.id_select_label, "按当前选择模式解释输入的 ID。")
self.id_input = QLineEdit("")
self.id_input.setObjectName("idSelectInput")
self.id_input.setPlaceholderText("ID")
self.id_input.setMaximumWidth(70)
help_tip(self.id_input, "输入要选中的对象 ID。ID 类型显示在左侧标签中,并会跟随选择模式变化。")
self.select_id_button = QPushButton("选择")
self.select_id_button.setMaximumWidth(58)
help_tip(self.select_id_button, "按当前选择模式跳转到输入的 ID,并在 3D 视图中高亮它。")
self.select_id_button.clicked.connect(lambda _checked=False: self.select_by_id(self._current_selection_mode()))
self.id_input.textChanged.connect(lambda _text: self._update_action_states())
self.id_input.returnPressed.connect(lambda: self.select_by_id(self._current_selection_mode()))
select_layout.addWidget(self.id_select_label)
select_layout.addWidget(self.id_input)
select_layout.addWidget(self.select_id_button)
mode_layout.addWidget(id_select_panel, stretch=1)
panel_layout.addWidget(mode_box)
select_box = QGroupBox("按 ID 选择")
select_box.setObjectName("idSection")
help_tip(select_box, "知道对象 ID 时,可以直接输入 ID 跳转选择。")
select_layout = QGridLayout(select_box)
self.id_input = QLineEdit("")
self.id_input.setPlaceholderText("输入 ID")
help_tip(self.id_input, "输入要选中的对象 ID。ID 的类型由右侧显示的选择模式决定。")
self.id_mode_display = QLineEdit(self.mode_combo.currentText())
self.id_mode_display.setObjectName("idModeDisplay")
self.id_mode_display.setReadOnly(True)
self.id_mode_display.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.id_mode_display.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.id_mode_display.setMinimumWidth(70)
help_tip(self.id_mode_display, "当前按这个鼠标选择模式解释输入的 ID。它会跟随鼠标选择模式自动变化")
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(self.select_id_button, 0, 3)
panel_layout.addWidget(select_box)
if ENABLE_VIEW_PANEL:
view_box = QGroupBox("显示")
view_box.setObjectName("viewSection")
help_tip(view_box, "只改变视图显示方式,不会修改 STEP 几何。")
view_layout = QVBoxLayout(view_box)
view_buttons = QHBoxLayout()
self.isolate_button = QPushButton("只显示选中")
help_tip(self.isolate_button, "把视图临时隔离到当前选中的零件、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(
self.show_internal_edges_checkbox,
"显示同一平面或同一圆柱面内部的拓扑分割边。关闭时会隐藏布尔推拉后常见的视觉接缝线。",
)
self.show_internal_edges_checkbox.toggled.connect(self._on_internal_edges_toggled)
view_layout.addWidget(self.show_internal_edges_checkbox)
view_box = QGroupBox("显示")
view_box.setObjectName("viewSection")
help_tip(view_box, "只改变视图显示方式,不会修改 STEP 几何。")
view_layout = QVBoxLayout(view_box)
view_buttons = QHBoxLayout()
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("显示同域内部边")
if ENABLE_MEASURE_PANEL:
measure_box = QGroupBox("测量")
measure_box.setObjectName("measureSection")
help_tip(measure_box, "把当前选中对象的拾取点或中心设为 A/B,计算两点距离和 X/Y/Z 差值。不会修改模型。")
measure_layout = QVBoxLayout(measure_box)
self.measure_text = QPlainTextEdit()
self.measure_text.setReadOnly(True)
self.measure_text.setMaximumHeight(90)
help_tip(self.measure_text, "显示 A 点、B 点、两点距离和各方向差值。")
measure_buttons = QGridLayout()
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)
self._refresh_measurement_panel()
if ENABLE_EXPORT_PANEL:
export_box = QGroupBox("导出")
export_box.setObjectName("exportSection")
help_tip(export_box, "把当前模型或选中对象导出为 STEP,也可以做基础质量检查和修复。")
export_layout = QVBoxLayout(export_box)
self.export_all_button = QPushButton("导出当前完整 STEP")
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, "只导出当前选中的零件。适合从装配里拆出一个单独零件。")
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.clicked.connect(self.export_selected_solid)
self.export_face_button = QPushButton("导出选中面区域")
help_tip(self.export_face_button, "导出当前选中的面区域;同域高亮的共面/同圆柱区域也会一起导出。")
self.export_face_button.clicked.connect(self.export_selected_face)
self.export_feature_button = QPushButton("导出选中特征区域")
help_tip(self.export_feature_button, "导出特征模式识别到的区域,例如孔、槽、圆角或凸台候选。")
self.export_feature_button.clicked.connect(self.export_selected_feature)
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 数量、体积和包围盒等。")
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.clicked.connect(self.repair_selected_shape)
export_layout.addWidget(self.export_all_button)
export_layout.addWidget(self.export_part_button)
export_layout.addWidget(self.export_solid_button)
export_layout.addWidget(self.export_face_button)
export_layout.addWidget(self.export_feature_button)
export_layout.addWidget(self.export_edge_button)
export_layout.addWidget(self.export_check_button)
export_layout.addWidget(self.repair_model_button)
export_layout.addWidget(self.repair_selected_button)
self.object_edit_box = QGroupBox("当前选中对象:未选择")
self.object_edit_box.setObjectName("editSection")
help_tip(
self.show_internal_edges_checkbox,
"显示同一平面或同一圆柱面内部的拓扑分割边。关闭时会隐藏布尔推拉后常见的视觉接缝线",
self.object_edit_box,
"选择模型对象后,这里会列出当前对象的属性和值;能改的行可以输入目标值,不能改的行只读",
)
self.show_internal_edges_checkbox.toggled.connect(self._on_internal_edges_toggled)
view_layout.addWidget(self.show_internal_edges_checkbox)
object_edit_layout = QVBoxLayout(self.object_edit_box)
self.property_table = QTableWidget(0, 4)
self.property_table.setHorizontalHeaderLabels(["属性", "当前值", "目标值", "状态"])
self.property_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.property_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.property_table.setAlternatingRowColors(True)
self.property_table.setMinimumHeight(180)
self.property_table.setWordWrap(False)
self.property_table.setTextElideMode(Qt.TextElideMode.ElideMiddle)
self.property_table.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.property_table.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.property_table.verticalHeader().setVisible(False)
self.property_table.verticalHeader().setDefaultSectionSize(24)
self.property_table.verticalHeader().setMinimumSectionSize(22)
property_header = self.property_table.horizontalHeader()
property_header.setMinimumSectionSize(44)
property_header.setStretchLastSection(False)
property_header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
property_header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
property_header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
property_header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
self.property_table.setColumnWidth(0, 106)
self.property_table.setColumnWidth(1, 92)
self.property_table.setColumnWidth(2, 92)
self.property_table.setColumnWidth(3, 62)
help_tip(
self.property_table,
"当前选中对象的属性表。可修改的行可以在目标值列输入新值,并点击状态列里的修改按钮只应用这一行。",
)
self.property_table.itemChanged.connect(self._on_property_table_item_changed)
object_edit_layout.addWidget(self.property_table)
self.property_expand_button = QPushButton("展开全部属性")
help_tip(self.property_expand_button, "当前选中对象属性较多时,默认只显示前 6 行;点击这里展开或收起属性表。")
self.property_expand_button.clicked.connect(self.toggle_property_table_expanded)
self.property_expand_button.setVisible(False)
object_edit_layout.addWidget(self.property_expand_button)
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)
measure_box = QGroupBox("测量")
measure_box.setObjectName("measureSection")
help_tip(measure_box, "把当前选中对象的拾取点或中心设为 A/B,计算两点距离和 X/Y/Z 差值。不会修改模型。")
measure_layout = QVBoxLayout(measure_box)
self.measure_text = QPlainTextEdit()
self.measure_text.setReadOnly(True)
self.measure_text.setMaximumHeight(90)
help_tip(self.measure_text, "显示 A 点、B 点、两点距离和各方向差值。")
measure_buttons = QGridLayout()
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)
self._refresh_measurement_panel()
export_box = QGroupBox("导出")
export_box.setObjectName("exportSection")
help_tip(export_box, "把当前模型或选中对象导出为 STEP,也可以做基础质量检查和修复。")
export_layout = QVBoxLayout(export_box)
self.export_all_button = QPushButton("导出当前完整 STEP")
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, "只导出当前选中的零件。适合从装配里拆出一个单独零件。")
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.clicked.connect(self.export_selected_solid)
self.export_face_button = QPushButton("导出选中面区域")
help_tip(self.export_face_button, "导出当前选中的面区域;同域高亮的共面/同圆柱区域也会一起导出。")
self.export_face_button.clicked.connect(self.export_selected_face)
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, "导出当前选中的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 数量、体积和包围盒等。")
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.clicked.connect(self.repair_selected_shape)
export_layout.addWidget(self.export_all_button)
export_layout.addWidget(self.export_part_button)
export_layout.addWidget(self.export_solid_button)
export_layout.addWidget(self.export_face_button)
export_layout.addWidget(self.export_feature_button)
export_layout.addWidget(self.export_edge_button)
export_layout.addWidget(self.export_check_button)
export_layout.addWidget(self.repair_model_button)
export_layout.addWidget(self.repair_selected_button)
edit_box = QGroupBox("编辑")
edit_box = QGroupBox(panel)
edit_box.setObjectName("editSection")
help_tip(edit_box, "这里的按钮会真实修改当前 B-Rep 模型;执行前通常会预览,成功后可撤销。部分能力仍是当前版本的几何近似。")
edit_box.setVisible(False)
self.hidden_edit_controls_box = edit_box
edit_layout = QGridLayout(edit_box)
edit_layout.addWidget(QLabel("Face偏移"), 0, 0)
self.offset_input = QLineEdit("5.0")
@@ -770,118 +931,125 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
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.clicked.connect(self.list_cylinders)
edit_layout.addWidget(self.cylinders_button, 26, 0, 1, 2)
panel_layout.addWidget(self.object_edit_box)
if ENABLE_EXPORT_PANEL:
panel_layout.addWidget(export_box)
if ENABLE_VIEW_PANEL:
panel_layout.addWidget(view_box)
if ENABLE_MEASURE_PANEL:
panel_layout.addWidget(measure_box)
self.undo_button = QPushButton("撤销")
help_tip(self.undo_button, "撤销上一次成功编辑,把模型恢复到编辑前快照。")
self.undo_button.clicked.connect(self.undo_edit)
self.redo_button = QPushButton("重做")
help_tip(self.redo_button, "重做刚刚撤销的编辑。")
self.redo_button.clicked.connect(self.redo_edit)
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)
if ENABLE_EDITABLE_OBJECTS_PANEL:
editable_box = QGroupBox("可编辑对象")
editable_box.setObjectName("editableSection")
help_tip(editable_box, "自动找出当前版本可以尝试编辑的Face或Edge,方便不用手动到处点。")
editable_layout = QVBoxLayout(editable_box)
editable_button_row = QHBoxLayout()
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("深度扫描")
help_tip(self.editable_deep_scan_button, "扫描更多候选对象,结果更全但更慢。默认列表找不到目标时再用。")
self.editable_deep_scan_button.clicked.connect(
lambda _checked=False: self.refresh_editable_candidates(show_info=True, deep_scan=True)
)
editable_button_row.addWidget(self.editable_refresh_button)
editable_button_row.addWidget(self.editable_deep_scan_button)
editable_layout.addLayout(editable_button_row)
self.editable_table = QTableWidget(0, 8)
self.editable_table.setHorizontalHeaderLabels(
["操作", "ID", "对象", "当前值", "状态", "风险", "置信度", "说明"]
)
self.editable_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.editable_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.editable_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.editable_table.setMinimumHeight(130)
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)
editable_box = QGroupBox("可编辑对象")
editable_box.setObjectName("editableSection")
help_tip(editable_box, "自动找出当前版本可以尝试编辑的Face或Edge,方便不用手动到处点。")
editable_layout = QVBoxLayout(editable_box)
editable_button_row = QHBoxLayout()
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("深度扫描")
help_tip(self.editable_deep_scan_button, "扫描更多候选对象,结果更全但更慢。默认列表找不到目标时再用。")
self.editable_deep_scan_button.clicked.connect(
lambda _checked=False: self.refresh_editable_candidates(show_info=True, deep_scan=True)
)
editable_button_row.addWidget(self.editable_refresh_button)
editable_button_row.addWidget(self.editable_deep_scan_button)
editable_layout.addLayout(editable_button_row)
self.editable_table = QTableWidget(0, 8)
self.editable_table.setHorizontalHeaderLabels(
["操作", "ID", "对象", "当前值", "状态", "风险", "置信度", "说明"]
)
self.editable_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.editable_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.editable_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.editable_table.setMinimumHeight(130)
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)
if ENABLE_CYLINDER_CANDIDATES_PANEL:
candidate_box = QGroupBox("圆柱面候选")
candidate_box.setObjectName("candidateSection")
help_tip(candidate_box, "列出圆柱面候选,并粗略判断它们像孔、槽、圆角、凸台还是普通圆柱。")
candidate_layout = QVBoxLayout(candidate_box)
filter_layout = QHBoxLayout()
self.cylinders_button = QPushButton("扫描圆柱面")
help_tip(self.cylinders_button, "扫描模型里的圆柱面候选,并粗略判断它们像孔、槽、圆角、凸台还是普通圆柱。")
self.cylinders_button.clicked.connect(self.list_cylinders)
filter_layout.addWidget(self.cylinders_button)
filter_layout.addWidget(QLabel("类型"))
self.candidate_filter_combo = NoWheelComboBox()
self.candidate_filter_combo.addItems(
[
"All",
"Hole/Groove",
"Round/Fillet",
"Boss/Outer",
"Unclear",
]
)
help_tip(
self.candidate_filter_combo,
"筛选下面的圆柱面候选:孔/槽、圆角、凸台/外圆,或暂时无法明确分类的圆柱面。",
)
self.candidate_filter_combo.currentTextChanged.connect(
lambda _text: self._filter_cached_cylinder_candidates(show_info=True)
)
filter_layout.addWidget(self.candidate_filter_combo)
candidate_layout.addLayout(filter_layout)
self.cylinder_table = QTableWidget(0, 8)
self.cylinder_table.setHorizontalHeaderLabels(
["Face", "Guess", "Diameter", "Span", "Height", "Confidence", "Risk", "零件"]
)
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)
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)
candidate_box = QGroupBox("圆柱面候选")
candidate_box.setObjectName("candidateSection")
help_tip(candidate_box, "列出圆柱面候选,并粗略判断它们像孔、槽、圆角、凸台还是普通圆柱。")
candidate_layout = QVBoxLayout(candidate_box)
filter_layout = QHBoxLayout()
filter_layout.addWidget(QLabel("类型"))
self.candidate_filter_combo = NoWheelComboBox()
self.candidate_filter_combo.addItems(
[
"All",
"Hole/Groove",
"Round/Fillet",
"Boss/Outer",
"Unclear",
]
)
help_tip(
self.candidate_filter_combo,
"筛选下面的圆柱面候选:孔/槽、圆角、凸台/外圆,或暂时无法明确分类的圆柱面",
)
self.candidate_filter_combo.currentTextChanged.connect(
lambda _text: self._filter_cached_cylinder_candidates(show_info=True)
)
filter_layout.addWidget(self.candidate_filter_combo)
candidate_layout.addLayout(filter_layout)
self.cylinder_table = QTableWidget(0, 8)
self.cylinder_table.setHorizontalHeaderLabels(
["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)
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)
history_box = QGroupBox("操作历史")
history_box.setObjectName("historySection")
help_tip(history_box, "查看已经成功执行的编辑、差异预览,并导出报告或 JSON 历史。")
history_layout = QVBoxLayout(history_box)
self.history_list = QListWidget()
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()
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(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)
if ENABLE_HISTORY_PANEL:
history_box = QGroupBox("操作历史")
history_box.setObjectName("historySection")
help_tip(history_box, "查看已经成功执行的编辑、差异预览,并导出报告或 JSON 历史。")
history_layout = QVBoxLayout(history_box)
self.history_list = QListWidget()
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()
self.undo_button = QPushButton("撤销")
help_tip(self.undo_button, "撤销上一次成功编辑,把模型恢复到编辑前快照。")
self.undo_button.clicked.connect(self.undo_edit)
self.redo_button = QPushButton("重做")
help_tip(self.redo_button, "重做刚刚撤销的编辑。")
self.redo_button.clicked.connect(self.redo_edit)
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(self.undo_button)
history_buttons.addWidget(self.redo_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)
info_box = QGroupBox("选中对象信息")
info_box.setObjectName("infoSection")
@@ -889,7 +1057,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
info_layout = QVBoxLayout(info_box)
info_buttons = QHBoxLayout()
self.copy_id_button = QPushButton("复制 ID")
help_tip(self.copy_id_button, "复制当前选中对象的 ID。Face/Feature 会优先复制逻辑 Face ID。")
help_tip(self.copy_id_button, "复制当前选中对象的 ID。Face/特征会优先复制逻辑 Face ID。")
self.copy_id_button.clicked.connect(self.copy_selected_id)
self.copy_pick_button = QPushButton("复制坐标")
help_tip(self.copy_pick_button, "复制最近一次鼠标点到模型上的三维坐标。")