feat: 完善 STEP 编辑器最小系统和稳定性
This commit is contained in:
+288
-151
@@ -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
|
||||
|
||||
|
||||
|
||||
+83
-51
@@ -271,48 +271,79 @@ class FeatureMixin:
|
||||
break
|
||||
|
||||
plane_count = 0
|
||||
shell_thickness_count = 0
|
||||
shell_thickness_limit = max(2, min(per_type_limit, limit // 10))
|
||||
face_scan_limit = len(self.faces) if max_scan_faces is None else min(len(self.faces), max(0, int(max_scan_faces)))
|
||||
for face_id, face in enumerate(self.faces[:face_scan_limit]):
|
||||
if progress_callback is not None and face_id % 30 == 0:
|
||||
progress_callback()
|
||||
if plane_count >= per_type_limit:
|
||||
if plane_count >= per_type_limit and shell_thickness_count >= shell_thickness_limit:
|
||||
break
|
||||
surf = BRepAdaptor_Surface(face)
|
||||
if surf.GetType() != GeomAbs_Plane:
|
||||
continue
|
||||
props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(face, props)
|
||||
if detailed:
|
||||
direction_info = self._plane_push_pull_direction(face_id, surf)
|
||||
confidence = str(direction_info["confidence"])
|
||||
risk = "low" if confidence == "high" else "medium"
|
||||
status = "ready" if confidence == "high" else "caution"
|
||||
note = str(direction_info["note"])
|
||||
else:
|
||||
confidence = "pending"
|
||||
risk = "medium"
|
||||
status = "caution"
|
||||
note = "快速扫描:推拉方向会在选中 face 或执行编辑前再详细判断。"
|
||||
candidates.append(
|
||||
{
|
||||
"operation_key": "push_pull_plane",
|
||||
"operation": "推拉平面",
|
||||
"target_kind": "face",
|
||||
"target_id": face_id,
|
||||
"face_id": face_id,
|
||||
"part_id": self.face_part_ids[face_id],
|
||||
"solid_id": self.face_solid_ids[face_id],
|
||||
"surface": "plane",
|
||||
"feature_guess": "planar push/pull candidate",
|
||||
"current_value": props.Mass(),
|
||||
"current_value_label": "area",
|
||||
"status": status,
|
||||
"risk": risk,
|
||||
"confidence": confidence,
|
||||
"note": note,
|
||||
}
|
||||
)
|
||||
plane_count += 1
|
||||
if plane_count < per_type_limit:
|
||||
if detailed:
|
||||
direction_info = self._plane_push_pull_direction(face_id, surf)
|
||||
confidence = str(direction_info["confidence"])
|
||||
risk = "low" if confidence == "high" else "medium"
|
||||
status = "ready" if confidence == "high" else "caution"
|
||||
note = str(direction_info["note"])
|
||||
else:
|
||||
confidence = "pending"
|
||||
risk = "medium"
|
||||
status = "caution"
|
||||
note = "快速扫描:推拉方向会在选中Face或执行编辑前再详细判断。"
|
||||
candidates.append(
|
||||
{
|
||||
"operation_key": "push_pull_plane",
|
||||
"operation": "推拉平面",
|
||||
"target_kind": "face",
|
||||
"target_id": face_id,
|
||||
"face_id": face_id,
|
||||
"part_id": self.face_part_ids[face_id],
|
||||
"solid_id": self.face_solid_ids[face_id],
|
||||
"surface": "plane",
|
||||
"feature_guess": "planar push/pull candidate",
|
||||
"current_value": props.Mass(),
|
||||
"current_value_label": "area",
|
||||
"status": status,
|
||||
"risk": risk,
|
||||
"confidence": confidence,
|
||||
"note": note,
|
||||
}
|
||||
)
|
||||
plane_count += 1
|
||||
if shell_thickness_count < shell_thickness_limit:
|
||||
feature = self.feature_info(face_id)
|
||||
if feature.get("shell_region_status") == "candidate":
|
||||
shell_confidence = str(feature.get("shell_confidence", "low"))
|
||||
shell_risk = "low" if shell_confidence == "high" else "medium" if shell_confidence == "medium" else "high"
|
||||
candidates.append(
|
||||
{
|
||||
"operation_key": "resize_shell_thickness",
|
||||
"operation": "调整薄壁厚度",
|
||||
"target_kind": "face",
|
||||
"target_id": face_id,
|
||||
"face_id": face_id,
|
||||
"part_id": self.face_part_ids[face_id],
|
||||
"solid_id": self.face_solid_ids[face_id],
|
||||
"surface": "plane",
|
||||
"feature_guess": "thin-wall opposite plane candidate",
|
||||
"current_value": feature.get("shell_thickness_estimate"),
|
||||
"current_value_label": "shell_thickness",
|
||||
"status": "ready" if shell_confidence == "high" else "caution",
|
||||
"risk": shell_risk,
|
||||
"confidence": shell_confidence,
|
||||
"note": (
|
||||
"快速扫描:已找到投影重叠的相对平面;点击后会填入参考目标厚度,"
|
||||
"执行时会移动当前平面区域来改变局部薄壁/壳体厚度。"
|
||||
),
|
||||
}
|
||||
)
|
||||
shell_thickness_count += 1
|
||||
|
||||
fillet_edge_count = 0
|
||||
chamfer_edge_count = 0
|
||||
@@ -342,7 +373,7 @@ class FeatureMixin:
|
||||
candidates.append(
|
||||
{
|
||||
"operation_key": "fillet_edge",
|
||||
"operation": "给边添加圆角",
|
||||
"operation": "给Edge添加圆角",
|
||||
"target_kind": "edge",
|
||||
"target_id": edge_id,
|
||||
"edge_id": edge_id,
|
||||
@@ -363,7 +394,7 @@ class FeatureMixin:
|
||||
candidates.append(
|
||||
{
|
||||
"operation_key": "chamfer_edge",
|
||||
"operation": "给边添加倒角",
|
||||
"operation": "给Edge添加倒角",
|
||||
"target_kind": "edge",
|
||||
"target_id": edge_id,
|
||||
"edge_id": edge_id,
|
||||
@@ -384,7 +415,7 @@ class FeatureMixin:
|
||||
candidates.append(
|
||||
{
|
||||
"operation_key": "resize_edge_length",
|
||||
"operation": "直接修改边长",
|
||||
"operation": "修改Edge长度",
|
||||
"target_kind": "edge",
|
||||
"target_id": edge_id,
|
||||
"edge_id": edge_id,
|
||||
@@ -397,7 +428,7 @@ class FeatureMixin:
|
||||
"status": "caution",
|
||||
"risk": "medium" if is_line_edge or is_circle_edge else "high",
|
||||
"confidence": "pending",
|
||||
"note": "快速扫描:直线边会优先尝试端面推拉;圆边会尝试换算相邻圆柱直径;其他边会使用几何缩放 fallback。",
|
||||
"note": "快速扫描:直线边优先局部形变;圆边优先换算相邻圆柱直径;椭圆/平面曲线会尝试径向缩放;其他边使用后备策略。",
|
||||
}
|
||||
)
|
||||
edge_length_count += 1
|
||||
@@ -411,10 +442,11 @@ class FeatureMixin:
|
||||
"suppress_cylinder": 3,
|
||||
"resize_depth": 4,
|
||||
"inspect_existing_fillet": 5,
|
||||
"push_pull_plane": 6,
|
||||
"fillet_edge": 7,
|
||||
"chamfer_edge": 8,
|
||||
"resize_edge_length": 9,
|
||||
"resize_shell_thickness": 6,
|
||||
"push_pull_plane": 7,
|
||||
"fillet_edge": 8,
|
||||
"chamfer_edge": 9,
|
||||
"resize_edge_length": 10,
|
||||
}
|
||||
candidates.sort(
|
||||
key=lambda item: (
|
||||
@@ -488,7 +520,7 @@ class FeatureMixin:
|
||||
return {
|
||||
"status": "blocked",
|
||||
"risk": "blocked",
|
||||
"message": "当前选中的 face 不是圆柱面,不能执行圆柱切削。",
|
||||
"message": "当前选中的 Face 不是圆柱面,不能执行圆柱切削。",
|
||||
}
|
||||
current_diameter = float(info["diameter"])
|
||||
feature = self.feature_info(face_id)
|
||||
@@ -550,7 +582,7 @@ class FeatureMixin:
|
||||
return {
|
||||
"status": "blocked",
|
||||
"risk": "blocked",
|
||||
"message": "当前选中的 face 不是圆柱面,不能调整圆柱凸台直径。",
|
||||
"message": "当前选中的 Face 不是圆柱面,不能调整圆柱凸台直径。",
|
||||
}
|
||||
|
||||
current_diameter = float(info["diameter"])
|
||||
@@ -609,7 +641,7 @@ class FeatureMixin:
|
||||
return {
|
||||
"status": "blocked",
|
||||
"risk": "blocked",
|
||||
"message": "当前选中的 face 不是圆柱面,不能封堵圆柱孔。",
|
||||
"message": "当前选中的 Face 不是圆柱面,不能封堵圆柱孔。",
|
||||
}
|
||||
feature = self.feature_info(face_id)
|
||||
axis_range = self._cylindrical_axis_range(
|
||||
@@ -664,7 +696,7 @@ class FeatureMixin:
|
||||
return {
|
||||
"status": "blocked",
|
||||
"risk": "blocked",
|
||||
"message": "当前选中的 face 不是圆柱面,不能调整盲孔深度。",
|
||||
"message": "当前选中的 Face 不是圆柱面,不能调整盲孔深度。",
|
||||
}
|
||||
|
||||
feature = self.feature_info(face_id)
|
||||
@@ -742,7 +774,7 @@ class FeatureMixin:
|
||||
if surf.GetType() != GeomAbs_Cylinder:
|
||||
return {
|
||||
"context_status": "blocked",
|
||||
"context_message": "当前选中的 face 不是圆柱面。",
|
||||
"context_message": "当前选中的 Face 不是圆柱面。",
|
||||
}
|
||||
|
||||
manual_bottom_face_id = None
|
||||
@@ -775,7 +807,7 @@ class FeatureMixin:
|
||||
if not bottom_face_ids:
|
||||
return {
|
||||
"context_status": "blocked",
|
||||
"context_message": "第一版孔深调整需要疑似底面;如果自动识别失败,请手动填写底面 Face ID。",
|
||||
"context_message": "当前版本的孔深调整需要疑似底面;如果自动识别失败,请手动填写底面Face ID。",
|
||||
}
|
||||
|
||||
cyl = surf.Cylinder()
|
||||
@@ -912,7 +944,7 @@ class FeatureMixin:
|
||||
if surf.GetType() != GeomAbs_Cylinder:
|
||||
return {
|
||||
"cutter_strategy": "unavailable",
|
||||
"cutter_note": "selected face is not cylindrical",
|
||||
"cutter_note": "选中Face不是圆柱面",
|
||||
}
|
||||
|
||||
cyl = surf.Cylinder()
|
||||
@@ -954,7 +986,7 @@ class FeatureMixin:
|
||||
"cutter_strategy": "bounded-to-selected-cylinder-v-range",
|
||||
"cutter_note": (
|
||||
"有限长度切削:优先按同域圆柱侧壁整体 V 范围生成 cutter,"
|
||||
"如果没有同域拆分则退回选中 face 范围,减少贯穿整个零件的误切风险。"
|
||||
"如果没有同域拆分则退回选中Face范围,减少贯穿整个零件的误切风险。"
|
||||
),
|
||||
"cutter_scope_face_ids": axis_range["same_domain_face_ids"],
|
||||
"cutter_scope_face_count": axis_range["same_domain_face_count"],
|
||||
@@ -982,7 +1014,7 @@ class FeatureMixin:
|
||||
if surf.GetType() != GeomAbs_Cylinder:
|
||||
return {
|
||||
"fill_strategy": "unavailable",
|
||||
"fill_note": "selected face is not cylindrical",
|
||||
"fill_note": "选中Face不是圆柱面",
|
||||
}
|
||||
|
||||
cyl = surf.Cylinder()
|
||||
@@ -1019,7 +1051,7 @@ class FeatureMixin:
|
||||
if surf.GetType() != GeomAbs_Cylinder:
|
||||
return {
|
||||
"boss_tool_strategy": "unavailable",
|
||||
"boss_tool_note": "selected face is not cylindrical",
|
||||
"boss_tool_note": "选中Face不是圆柱面",
|
||||
}
|
||||
|
||||
cyl = surf.Cylinder()
|
||||
@@ -1055,7 +1087,7 @@ class FeatureMixin:
|
||||
"boss_tool_note": (
|
||||
"扩大凸台会在同域圆柱侧壁整体 V 范围内生成目标半径圆柱并 Fuse;"
|
||||
"缩小凸台会先用旧半径包络体移除原凸台范围,再 Fuse 目标半径圆柱重建。"
|
||||
"第一版会给轴向两端保留少量重叠,让布尔结果更容易和原实体合并。"
|
||||
"当前版本会给轴向两端保留少量重叠,让布尔结果更容易和原实体合并。"
|
||||
),
|
||||
"boss_tool_scope_face_ids": axis_range["same_domain_face_ids"],
|
||||
"boss_tool_scope_face_count": axis_range["same_domain_face_count"],
|
||||
|
||||
@@ -191,12 +191,12 @@ def _shape_quality_info(label: str, shape: TopoDS_Shape, expect_solid: bool) ->
|
||||
if not brep_valid:
|
||||
warnings.append("B-Rep 校验未通过,导出后其他 CAD 软件可能无法正常识别。")
|
||||
if faces == 0:
|
||||
warnings.append("没有检测到 face,导出结果可能不可用。")
|
||||
warnings.append("没有检测到Face,导出结果可能不可用。")
|
||||
if expect_solid and solids == 0:
|
||||
warnings.append("没有检测到 solid,导出后可能不是实体。")
|
||||
warnings.append("没有检测到Solid,导出后可能不是实体。")
|
||||
elif expect_solid and solids > 1:
|
||||
warnings.append(
|
||||
f"检测到 {solids} 个 solid。"
|
||||
f"检测到 {solids} 个Solid。"
|
||||
"如果这不是有意的多实体零件,导出后可能看起来像多个体叠在一起或彼此分离。"
|
||||
)
|
||||
|
||||
@@ -782,7 +782,7 @@ def _cylinder_resize_readiness(
|
||||
if guess != "hole/groove candidate":
|
||||
status = "blocked"
|
||||
risk = "blocked"
|
||||
blockers.append("缩小孔径第一版只支持孔/槽候选,不支持圆角、凸柱或未明确圆柱面。")
|
||||
blockers.append("缩小孔径当前版本只支持孔/槽候选,不支持圆角、凸柱或未明确圆柱面。")
|
||||
else:
|
||||
risk = _max_risk(risk, "high")
|
||||
warnings.append("缩小孔径会先补料再重切,属于高风险实验功能。")
|
||||
@@ -817,9 +817,9 @@ def _cylinder_boss_resize_readiness(
|
||||
height_estimate = float(info.get("height_estimate", 0.0))
|
||||
|
||||
if guess != "boss/outer-round candidate":
|
||||
blockers.append("凸台直径调整第一版只支持明确的凸台/外圆柱候选。")
|
||||
blockers.append("凸台直径调整当前版本只支持明确的凸台/外圆柱候选。")
|
||||
if angular_span < math.tau * 0.92:
|
||||
blockers.append("凸台直径调整第一版只支持接近完整圆柱的凸台,不处理局部外圆角或圆角面。")
|
||||
blockers.append("凸台直径调整当前版本只支持接近完整圆柱的凸台,不处理局部外圆角或圆角面。")
|
||||
if current_diameter <= 1e-9:
|
||||
blockers.append("当前圆柱面的直径估算无效。")
|
||||
|
||||
@@ -885,9 +885,9 @@ def _cylinder_depth_readiness(
|
||||
manual_bottom_face_used = bool(info.get("manual_bottom_face_used"))
|
||||
|
||||
if guess != "hole/groove candidate":
|
||||
blockers.append("孔深调整第一版只支持孔/槽候选,不支持圆角、凸柱或未明确圆柱面。")
|
||||
blockers.append("孔深调整当前版本只支持孔/槽候选,不支持圆角、凸柱或未明确圆柱面。")
|
||||
if end_type != "blind" and not manual_bottom_face_used:
|
||||
blockers.append("孔深调整第一版只支持端部类型为 blind 的盲孔/盲槽。")
|
||||
blockers.append("孔深调整当前版本只支持端部类型为 blind 的盲孔/盲槽。")
|
||||
elif end_type != "blind" and manual_bottom_face_used:
|
||||
risk = _max_risk(risk, "medium")
|
||||
warnings.append("端部类型不是明确 blind,当前按手动底面 Face ID 推断孔深方向。")
|
||||
@@ -951,9 +951,9 @@ def _cylinder_suppress_readiness(info: dict[str, object]) -> dict[str, object]:
|
||||
diameter = float(info.get("diameter", 0.0))
|
||||
|
||||
if guess != "hole/groove candidate":
|
||||
blockers.append("封堵圆柱孔第一版只支持孔候选,不支持圆角、凸柱或未明确圆柱面。")
|
||||
blockers.append("封堵圆柱孔当前版本只支持孔候选,不支持圆角、凸柱或未明确圆柱面。")
|
||||
if angular_span < math.tau * 0.92:
|
||||
blockers.append("封堵圆柱孔第一版只支持接近完整圆柱的孔,不支持半孔/槽。")
|
||||
blockers.append("封堵圆柱孔当前版本只支持接近完整圆柱的孔,不支持半孔/槽。")
|
||||
if end_type == "closed/internal":
|
||||
blockers.append("当前圆柱两端都像在材料内部,不像可封堵的外部孔。")
|
||||
if height <= 1e-9 or diameter <= 1e-9:
|
||||
@@ -998,14 +998,14 @@ def _edge_fillet_readiness(
|
||||
adjacent_count = int(info.get("adjacent_face_count", 0))
|
||||
|
||||
if curve != "line":
|
||||
blockers.append("添加圆角第一版只支持直线 edge。")
|
||||
blockers.append("添加圆角当前版本只支持直线Edge。")
|
||||
if length <= 1e-9:
|
||||
blockers.append("当前 edge 长度无效。")
|
||||
blockers.append("当前Edge长度无效。")
|
||||
if adjacent_count < 2:
|
||||
blockers.append("当前 edge 没有检测到至少两个相邻 face,不能可靠添加圆角。")
|
||||
blockers.append("当前 Edge 没有检测到至少两个相邻 Face,不能可靠添加圆角。")
|
||||
elif adjacent_count > 2:
|
||||
risk = _max_risk(risk, "medium")
|
||||
warnings.append(f"当前 edge 相邻 face 数为 {adjacent_count},可能是复杂交汇边。")
|
||||
warnings.append(f"当前 Edge 相邻 Face 数为 {adjacent_count},可能是复杂交汇边。")
|
||||
|
||||
if radius is not None:
|
||||
if radius <= 0:
|
||||
@@ -1013,13 +1013,13 @@ def _edge_fillet_readiness(
|
||||
elif length > 1e-9:
|
||||
ratio = radius / length
|
||||
if ratio >= 0.45:
|
||||
blockers.append("圆角半径接近或超过 edge 长度的一半,第一版直接阻止。")
|
||||
blockers.append("圆角半径接近或超过Edge长度的一半,当前版本会直接阻止。")
|
||||
elif ratio > 0.25:
|
||||
risk = _max_risk(risk, "high")
|
||||
warnings.append("圆角半径超过 edge 长度的 25%,很容易导致倒圆失败。")
|
||||
warnings.append("圆角半径超过Edge长度的 25%,很容易导致倒圆失败。")
|
||||
elif ratio > 0.12:
|
||||
risk = _max_risk(risk, "medium")
|
||||
warnings.append("圆角半径相对 edge 长度偏大,请确认预览范围。")
|
||||
warnings.append("圆角半径相对Edge长度偏大,请确认预览范围。")
|
||||
|
||||
if blockers:
|
||||
status = "blocked"
|
||||
@@ -1053,14 +1053,14 @@ def _edge_chamfer_readiness(
|
||||
adjacent_count = int(info.get("adjacent_face_count", 0))
|
||||
|
||||
if curve != "line":
|
||||
blockers.append("添加倒角第一版只支持直线 edge。")
|
||||
blockers.append("添加倒角当前版本只支持直线Edge。")
|
||||
if length <= 1e-9:
|
||||
blockers.append("当前 edge 长度无效。")
|
||||
blockers.append("当前Edge长度无效。")
|
||||
if adjacent_count < 2:
|
||||
blockers.append("当前 edge 没有检测到至少两个相邻 face,不能可靠添加倒角。")
|
||||
blockers.append("当前 Edge 没有检测到至少两个相邻 Face,不能可靠添加倒角。")
|
||||
elif adjacent_count > 2:
|
||||
risk = _max_risk(risk, "medium")
|
||||
warnings.append(f"当前 edge 相邻 face 数为 {adjacent_count},可能是复杂交汇边。")
|
||||
warnings.append(f"当前 Edge 相邻 Face 数为 {adjacent_count},可能是复杂交汇边。")
|
||||
|
||||
if distance is not None:
|
||||
if distance <= 0:
|
||||
@@ -1068,13 +1068,13 @@ def _edge_chamfer_readiness(
|
||||
elif length > 1e-9:
|
||||
ratio = distance / length
|
||||
if ratio >= 0.45:
|
||||
blockers.append("倒角距离接近或超过 edge 长度的一半,第一版直接阻止。")
|
||||
blockers.append("倒角距离接近或超过Edge长度的一半,当前版本会直接阻止。")
|
||||
elif ratio > 0.25:
|
||||
risk = _max_risk(risk, "high")
|
||||
warnings.append("倒角距离超过 edge 长度的 25%,很容易导致倒角失败。")
|
||||
warnings.append("倒角距离超过Edge长度的 25%,很容易导致倒角失败。")
|
||||
elif ratio > 0.12:
|
||||
risk = _max_risk(risk, "medium")
|
||||
warnings.append("倒角距离相对 edge 长度偏大,请确认预览范围。")
|
||||
warnings.append("倒角距离相对Edge长度偏大,请确认预览范围。")
|
||||
|
||||
if blockers:
|
||||
status = "blocked"
|
||||
|
||||
@@ -27,6 +27,8 @@ class InfoPanelMixin:
|
||||
self.info_text.setPlainText(self.current_info_text)
|
||||
self._populate_info_tree(info)
|
||||
self.info_tabs.setCurrentWidget(self.info_tree)
|
||||
if hasattr(self, "export_all_button"):
|
||||
self._update_action_states()
|
||||
|
||||
def set_plain_info(self, text: str) -> None:
|
||||
self.current_info_values = {}
|
||||
@@ -34,6 +36,8 @@ class InfoPanelMixin:
|
||||
self.info_tree.clear()
|
||||
self.info_text.setPlainText(text)
|
||||
self.info_tabs.setCurrentWidget(self.info_text)
|
||||
if hasattr(self, "export_all_button"):
|
||||
self._update_action_states()
|
||||
|
||||
def _populate_info_tree(self, info: dict[str, object]) -> None:
|
||||
self.info_tree.clear()
|
||||
@@ -106,6 +110,7 @@ class InfoPanelMixin:
|
||||
self.measure_label_b = source
|
||||
self.statusBar().showMessage(f"已设置测量点 B: {_format_value(point)}")
|
||||
self._refresh_measurement_panel()
|
||||
self._update_action_states()
|
||||
|
||||
def clear_measurement(self) -> None:
|
||||
self.measure_point_a = None
|
||||
@@ -115,6 +120,7 @@ class InfoPanelMixin:
|
||||
self._clear_measure_actor()
|
||||
self._refresh_measurement_panel()
|
||||
self.statusBar().showMessage("已清除测量")
|
||||
self._update_action_states()
|
||||
|
||||
def copy_measurement(self) -> None:
|
||||
text = self.measure_text.toPlainText() if hasattr(self, "measure_text") else ""
|
||||
|
||||
+12
-5
@@ -374,8 +374,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"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": "Feature 模式会把选中的Face解释为局部几何特征候选。",
|
||||
}
|
||||
)
|
||||
return result
|
||||
@@ -390,7 +390,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
scope_note = "当前 face 没有检测到可一起推拉的共面相接/重叠邻居。"
|
||||
edit_actions = "推拉平面"
|
||||
if shell_info.get("shell_region_status") == "candidate":
|
||||
edit_actions += ";查看薄壁/壳体厚度估算"
|
||||
edit_actions += ";调整薄壁/壳体厚度"
|
||||
result = dict(info)
|
||||
result.update(
|
||||
{
|
||||
@@ -493,6 +493,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
best = {
|
||||
"shell_opposite_face_id": candidate_id,
|
||||
"shell_thickness_estimate": thickness,
|
||||
"shell_signed_thickness": _axis_parameter(plane.Location(), normal, candidate_plane.Location()),
|
||||
"shell_overlap_ratio_estimate": overlap_ratio,
|
||||
"shell_opposite_normal_dot": normal_dot,
|
||||
}
|
||||
@@ -557,7 +558,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
edit_actions += ";封堵圆柱孔"
|
||||
elif guess == "round/fillet candidate":
|
||||
feature_type = "圆角/倒圆候选"
|
||||
edit_actions = "可尝试修改已有圆角半径;第一版会先移除圆角面,再在恢复出的锐边上重新倒圆。"
|
||||
edit_actions = "可尝试修改已有圆角半径;当前版本会先移除圆角面,再在恢复出的锐边上重新倒圆。"
|
||||
elif guess == "boss/outer-round candidate":
|
||||
feature_type = "凸台/外圆候选"
|
||||
edit_actions = "调整圆柱凸台直径。"
|
||||
@@ -668,7 +669,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"feature_existing_fillet_support_face_ids": tuple(support_face_ids),
|
||||
"existing_fillet_note": (
|
||||
"这是由局部小半径圆柱面推断出的已有圆角/倒圆候选;"
|
||||
"第一版可尝试使用 defeature + 重新倒圆修改半径;"
|
||||
"当前版本可尝试使用 defeature + 重新倒圆修改半径;"
|
||||
"复杂 blend 或支撑面不明确时可能失败并回滚。"
|
||||
),
|
||||
}
|
||||
@@ -1302,6 +1303,12 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
info["axis"] = _dir_tuple(circle.Axis().Direction())
|
||||
info["radius"] = circle.Radius()
|
||||
info["diameter"] = circle.Radius() * 2.0
|
||||
if curve_type == GeomAbs_Ellipse:
|
||||
ellipse = curve.Ellipse()
|
||||
info["center"] = _point_tuple(ellipse.Location())
|
||||
info["axis"] = _dir_tuple(ellipse.Axis().Direction())
|
||||
info["major_radius"] = ellipse.MajorRadius()
|
||||
info["minor_radius"] = ellipse.MinorRadius()
|
||||
adjacent_face_ids = self._edge_adjacent_face_ids(edge_id)
|
||||
info["adjacent_face_ids"] = tuple(adjacent_face_ids)
|
||||
info["adjacent_face_count"] = len(adjacent_face_ids)
|
||||
|
||||
+996
-138
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,10 @@ from dataclasses import dataclass
|
||||
class OperationRecord:
|
||||
summary: str
|
||||
detail: str
|
||||
operation_name: str | None = None
|
||||
target: str | None = None
|
||||
parameters: dict[str, object] | None = None
|
||||
result_message: str | None = None
|
||||
target_kind: str | None = None
|
||||
target_id: int | None = None
|
||||
target_logical_id: int | None = None
|
||||
|
||||
@@ -94,7 +94,7 @@ class TransformMixin:
|
||||
risk = readiness["translate_risk"]
|
||||
status = readiness["translate_status"]
|
||||
if part_solid_count <= 1 and status != "blocked":
|
||||
warnings = _join_nonempty(warnings, "当前 part 只有一个 solid,平移 solid 实际会移动整个 part shape。")
|
||||
warnings = _join_nonempty(warnings, "当前Part只有一个Solid,平移Solid实际会移动整个Part shape。")
|
||||
if risk == "low":
|
||||
risk = "medium"
|
||||
status = "caution"
|
||||
@@ -192,7 +192,7 @@ class TransformMixin:
|
||||
risk = readiness["rotate_risk"]
|
||||
status = readiness["rotate_status"]
|
||||
if part_solid_count <= 1 and status != "blocked":
|
||||
warnings = _join_nonempty(warnings, "当前 part 只有一个 solid,旋转 solid 实际会旋转整个 part shape。")
|
||||
warnings = _join_nonempty(warnings, "当前Part只有一个Solid,旋转Solid实际会旋转整个Part shape。")
|
||||
if risk == "low":
|
||||
risk = "medium"
|
||||
status = "caution"
|
||||
|
||||
+58
-10
@@ -65,6 +65,13 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
|
||||
"length_change_ratio",
|
||||
"edge_length_anchor_mode",
|
||||
"edge_length_anchor_label",
|
||||
"affine_initial_scale",
|
||||
"affine_scale",
|
||||
"affine_scale_correction",
|
||||
"affine_predicted_edge_length",
|
||||
"affine_scale_refine_iterations",
|
||||
"planar_edge_scale_sample_count",
|
||||
"planar_edge_scale_plane_deviation",
|
||||
"circular_edge_current_radius",
|
||||
"circular_edge_target_radius",
|
||||
"circular_edge_length_scale",
|
||||
@@ -160,8 +167,15 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
|
||||
"shell_source_face_ids",
|
||||
"shell_opposite_face_id",
|
||||
"shell_thickness_estimate",
|
||||
"shell_current_thickness",
|
||||
"shell_target_thickness",
|
||||
"shell_delta_thickness",
|
||||
"shell_delta_ratio",
|
||||
"shell_signed_thickness",
|
||||
"shell_overlap_ratio_estimate",
|
||||
"shell_opposite_normal_dot",
|
||||
"shell_desired_movement_vector",
|
||||
"shell_movement_alignment",
|
||||
"shell_note",
|
||||
"shell_region_note",
|
||||
],
|
||||
@@ -379,13 +393,21 @@ INFO_LABELS = {
|
||||
"surface_area": "表面积",
|
||||
"area": "面积",
|
||||
"length": "长度",
|
||||
"edge_length": "边长",
|
||||
"current_length": "当前边长",
|
||||
"target_length": "目标边长",
|
||||
"delta_length": "边长变化量",
|
||||
"length_change_ratio": "边长变化比例",
|
||||
"edge_length_anchor_mode": "边长基准模式",
|
||||
"edge_length_anchor_label": "边长基准",
|
||||
"edge_length": "Edge长度",
|
||||
"current_length": "当前Edge长度",
|
||||
"target_length": "目标Edge长度",
|
||||
"delta_length": "Edge长度变化量",
|
||||
"length_change_ratio": "Edge长度变化比例",
|
||||
"edge_length_anchor_mode": "Edge长度基准模式",
|
||||
"edge_length_anchor_label": "Edge长度基准",
|
||||
"local_edge_deform_target_kind": "局部边形变目标",
|
||||
"local_edge_deform_anchor": "局部边形变基准",
|
||||
"local_edge_deform_face_count": "局部边形变重建面数",
|
||||
"local_edge_deform_start_move": "局部边形变起点移动",
|
||||
"local_edge_deform_end_move": "局部边形变终点移动",
|
||||
"local_edge_deform_moved_endpoint_count": "局部边形变移动端点数",
|
||||
"local_edge_deform_risk": "局部边形变风险",
|
||||
"local_edge_deform_note": "局部边形变说明",
|
||||
"circular_edge_current_radius": "圆边当前半径",
|
||||
"circular_edge_target_radius": "圆边目标半径",
|
||||
"circular_edge_length_scale": "圆边长度比例",
|
||||
@@ -400,10 +422,28 @@ INFO_LABELS = {
|
||||
"cylinder_resize_delta_ratio": "圆柱直径变化比例",
|
||||
"cylinder_resize_status": "圆柱编辑状态",
|
||||
"cylinder_resize_risk": "圆柱编辑风险",
|
||||
"cylinder_resize_feature_guess": "圆柱候选判断",
|
||||
"cylinder_resize_feature_guess": "圆柱面候选判断",
|
||||
"cylinder_resize_confidence": "圆柱判断置信度",
|
||||
"cylinder_resize_same_domain_face_ids": "圆柱同域 Face",
|
||||
"cylinder_resize_same_domain_face_count": "圆柱同域 Face 数",
|
||||
"affine_scale": "几何缩放比例",
|
||||
"affine_initial_scale": "几何缩放初始比例",
|
||||
"affine_scale_correction": "几何缩放校正比例",
|
||||
"affine_predicted_edge_length": "几何缩放预估Edge长度",
|
||||
"affine_scale_refine_iterations": "几何缩放校正次数",
|
||||
"affine_scale_refine_note": "几何缩放校正说明",
|
||||
"affine_transform_kind": "几何缩放类型",
|
||||
"affine_transform_label": "几何缩放类型",
|
||||
"affine_transform_note": "几何缩放说明",
|
||||
"affine_transform_warning": "几何缩放警告",
|
||||
"affine_transform_risk": "几何缩放风险",
|
||||
"affine_axis_point": "几何缩放基准点",
|
||||
"affine_axis_direction": "几何缩放轴线方向",
|
||||
"affine_axis_source": "几何缩放轴线来源",
|
||||
"affine_anchor_source": "几何缩放基准来源",
|
||||
"affine_target_kind": "几何缩放目标",
|
||||
"planar_edge_scale_sample_count": "平面曲线采样点数",
|
||||
"planar_edge_scale_plane_deviation": "平面曲线采样偏差",
|
||||
"radius": "半径",
|
||||
"diameter": "直径",
|
||||
"target_radius": "目标圆角半径",
|
||||
@@ -476,11 +516,19 @@ INFO_LABELS = {
|
||||
"shell_region_kind": "壳体/薄壁候选类型",
|
||||
"shell_region_status": "壳体/薄壁识别状态",
|
||||
"shell_confidence": "壳体/薄壁置信度",
|
||||
"shell_thickness": "薄壁厚度",
|
||||
"shell_source_face_ids": "壳体/薄壁源平面 Face",
|
||||
"shell_opposite_face_id": "相对平面 Face",
|
||||
"shell_thickness_estimate": "薄壁厚度估算",
|
||||
"shell_current_thickness": "当前薄壁厚度",
|
||||
"shell_target_thickness": "目标薄壁厚度",
|
||||
"shell_delta_thickness": "薄壁厚度变化量",
|
||||
"shell_delta_ratio": "薄壁厚度变化比例",
|
||||
"shell_signed_thickness": "有符号薄壁厚度",
|
||||
"shell_overlap_ratio_estimate": "相对平面重叠率估算",
|
||||
"shell_opposite_normal_dot": "相对平面法向点积",
|
||||
"shell_desired_movement_vector": "薄壁目标移动向量",
|
||||
"shell_movement_alignment": "薄壁移动方向匹配度",
|
||||
"shell_note": "壳体/薄壁识别说明",
|
||||
"shell_region_note": "壳体/薄壁识别说明",
|
||||
"u_range": "U 参数范围",
|
||||
@@ -730,10 +778,10 @@ def _edit_quality_warnings(before_part_stats, after_part_stats) -> list[str]:
|
||||
return []
|
||||
warnings: list[str] = []
|
||||
if after_part_stats.solids == 0:
|
||||
warnings.append("目标零件编辑后没有检测到 solid,导出前请确认模型是否有效。")
|
||||
warnings.append("目标零件编辑后没有检测到Solid,导出前请确认模型是否有效。")
|
||||
elif before_part_stats.solids > 0 and after_part_stats.solids != before_part_stats.solids:
|
||||
warnings.append(
|
||||
"目标零件 solid 数发生变化:"
|
||||
"目标零件Solid数发生变化:"
|
||||
f"{before_part_stats.solids} -> {after_part_stats.solids}。"
|
||||
"如果这是一次局部推拉/孔径修改,请重点检查导出后是否仍是一体实体。"
|
||||
)
|
||||
|
||||
@@ -5,4 +5,7 @@ from PySide6.QtWidgets import QComboBox
|
||||
|
||||
class NoWheelComboBox(QComboBox):
|
||||
def wheelEvent(self, event) -> None:
|
||||
event.accept()
|
||||
if self.view().isVisible():
|
||||
super().wheelEvent(event)
|
||||
return
|
||||
event.ignore()
|
||||
|
||||
+370
-147
File diff suppressed because it is too large
Load Diff
+157
-112
@@ -54,7 +54,7 @@ class WindowCoreMixin:
|
||||
self.interactor.AddObserver("MouseMoveEvent", self.on_mouse_move)
|
||||
|
||||
light = vtk.vtkLight()
|
||||
light.SetLightTypeToSceneLight()
|
||||
light.SetLightTypeToHeadlight()
|
||||
light.SetPosition(1, 1, 1)
|
||||
light.SetIntensity(0.9)
|
||||
self.renderer.AddLight(light)
|
||||
@@ -62,20 +62,30 @@ class WindowCoreMixin:
|
||||
self.interactor.Initialize()
|
||||
|
||||
def closeEvent(self, event) -> None:
|
||||
if self.load_in_progress:
|
||||
load_thread_running = bool(
|
||||
(self.load_thread is not None and self.load_thread.isRunning())
|
||||
or (self.load_refine_thread is not None and self.load_refine_thread.isRunning())
|
||||
)
|
||||
if self.load_in_progress or load_thread_running:
|
||||
self.statusBar().showMessage("STEP background loading is still running.")
|
||||
event.ignore()
|
||||
return
|
||||
if self.operation_in_progress:
|
||||
edit_thread_running = bool(self.edit_thread is not None and self.edit_thread.isRunning())
|
||||
if self.operation_in_progress or edit_thread_running:
|
||||
self.statusBar().showMessage("后台编辑正在计算,请等待当前操作完成后再关闭窗口。")
|
||||
event.ignore()
|
||||
return
|
||||
if self.scan_in_progress:
|
||||
self.statusBar().showMessage("后台扫描正在进行,请等待扫描完成后再关闭窗口。")
|
||||
scan_thread_running = bool(self.scan_thread is not None and self.scan_thread.isRunning())
|
||||
if self.scan_in_progress or scan_thread_running:
|
||||
self.statusBar().showMessage("扫描正在进行,请等待扫描完成后再关闭窗口。")
|
||||
event.ignore()
|
||||
return
|
||||
super().closeEvent(event)
|
||||
|
||||
def _request_thread_quit(self, thread: QThread | None) -> None:
|
||||
if thread is not None and thread.isRunning():
|
||||
thread.quit()
|
||||
|
||||
def load_step(self, path: str | Path, *, background: bool = True) -> None:
|
||||
self._load_step_background_or_sync(path, background=background)
|
||||
return
|
||||
@@ -110,53 +120,62 @@ class WindowCoreMixin:
|
||||
return
|
||||
new_path = Path(path)
|
||||
if not background:
|
||||
self.statusBar().showMessage(f"Loading {new_path.name}...")
|
||||
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
||||
QApplication.processEvents()
|
||||
try:
|
||||
result = self._load_step_result(
|
||||
new_path,
|
||||
deflection=0.8,
|
||||
show_internal_edges=self._show_same_domain_internal_edges(),
|
||||
)
|
||||
except Exception as exc:
|
||||
QMessageBox.critical(self, "Load failed", str(exc))
|
||||
self.statusBar().showMessage("STEP load failed; current model was left unchanged.")
|
||||
return
|
||||
finally:
|
||||
QApplication.restoreOverrideCursor()
|
||||
self._apply_loaded_model_result(result, reset_camera=True)
|
||||
self.statusBar().showMessage(f"Loaded {self.step_path.name}")
|
||||
self._load_step_sync(
|
||||
new_path,
|
||||
deflection=0.8,
|
||||
show_internal_edges=self._show_same_domain_internal_edges(),
|
||||
status_prefix="Loading",
|
||||
)
|
||||
return
|
||||
|
||||
self.load_in_progress = True
|
||||
self.pending_load_path = new_path
|
||||
self.statusBar().showMessage(f"Loading {new_path.name} in background...")
|
||||
self.statusBar().showMessage(f"Preparing fast load for {new_path.name}...")
|
||||
self._clear_hover(render=True)
|
||||
self._update_action_states()
|
||||
QTimer.singleShot(0, lambda path=new_path: self._run_deferred_initial_load(path))
|
||||
|
||||
def action():
|
||||
return self._load_step_result(
|
||||
def _load_step_sync(
|
||||
self,
|
||||
new_path: Path,
|
||||
*,
|
||||
deflection: float,
|
||||
show_internal_edges: bool,
|
||||
status_prefix: str,
|
||||
) -> bool:
|
||||
self.statusBar().showMessage(f"{status_prefix} {new_path.name}...")
|
||||
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
||||
QApplication.processEvents()
|
||||
try:
|
||||
result = self._load_step_result(
|
||||
new_path,
|
||||
deflection=deflection,
|
||||
show_internal_edges=show_internal_edges,
|
||||
)
|
||||
result["display"] = "ready"
|
||||
except Exception as exc:
|
||||
QMessageBox.critical(self, "Load failed", str(exc))
|
||||
self.statusBar().showMessage("STEP load failed; current model was left unchanged.")
|
||||
return False
|
||||
finally:
|
||||
QApplication.restoreOverrideCursor()
|
||||
self._apply_loaded_model_result(result, reset_camera=True)
|
||||
self.statusBar().showMessage(f"Loaded {self.step_path.name}")
|
||||
return True
|
||||
|
||||
@Slot(object)
|
||||
def _run_deferred_initial_load(self, expected_path: Path) -> None:
|
||||
if self.pending_load_path != expected_path:
|
||||
return
|
||||
try:
|
||||
self._load_step_sync(
|
||||
expected_path,
|
||||
deflection=self.initial_load_deflection,
|
||||
show_internal_edges=True,
|
||||
build_polydata=False,
|
||||
status_prefix="Fast loading",
|
||||
)
|
||||
|
||||
thread = QThread(self)
|
||||
worker = LoadWorker(action)
|
||||
worker.moveToThread(thread)
|
||||
thread.started.connect(worker.run)
|
||||
worker.finished.connect(self._finish_initial_load)
|
||||
worker.failed.connect(self._fail_initial_load)
|
||||
worker.finished.connect(thread.quit)
|
||||
worker.failed.connect(thread.quit)
|
||||
thread.finished.connect(worker.deleteLater)
|
||||
thread.finished.connect(thread.deleteLater)
|
||||
thread.finished.connect(self._forget_load_thread)
|
||||
self.load_thread = thread
|
||||
self.load_worker = worker
|
||||
thread.start()
|
||||
finally:
|
||||
self._end_load_task()
|
||||
|
||||
def _apply_loaded_model_result(self, result: dict[str, object], *, reset_camera: bool) -> None:
|
||||
new_path = Path(result["path"])
|
||||
@@ -194,11 +213,12 @@ class WindowCoreMixin:
|
||||
"faces": stats.faces,
|
||||
"edges": stats.edges,
|
||||
"vertices": stats.vertices,
|
||||
"display": "quick preview" if self.load_in_progress else "ready",
|
||||
"display": result.get("display", "quick preview" if self.load_in_progress else "ready"),
|
||||
}
|
||||
)
|
||||
self._update_action_states()
|
||||
|
||||
@Slot(object)
|
||||
def _finish_initial_load(self, result: object) -> None:
|
||||
try:
|
||||
if not isinstance(result, dict):
|
||||
@@ -210,49 +230,21 @@ class WindowCoreMixin:
|
||||
self._end_load_task()
|
||||
QMessageBox.critical(self, "Load failed", str(exc))
|
||||
self.statusBar().showMessage("STEP load failed; current model was left unchanged.")
|
||||
finally:
|
||||
self._request_thread_quit(self.load_thread)
|
||||
|
||||
@Slot(str)
|
||||
def _fail_initial_load(self, message: str) -> None:
|
||||
self._end_load_task()
|
||||
QMessageBox.critical(self, "Load failed", message)
|
||||
self.statusBar().showMessage("STEP load failed; current model was left unchanged.")
|
||||
|
||||
def _start_load_refine(self, initial_result: dict[str, object]) -> None:
|
||||
model = initial_result.get("model")
|
||||
if model is None or model is not self.model:
|
||||
self._end_load_task()
|
||||
return
|
||||
path = Path(initial_result["path"])
|
||||
stats = initial_result["stats"]
|
||||
show_internal_edges = self._show_same_domain_internal_edges()
|
||||
|
||||
def action():
|
||||
return {
|
||||
"path": path,
|
||||
"model": model,
|
||||
"stats": stats,
|
||||
"model_polydata": model.build_face_polydata(deflection=0.8),
|
||||
"edge_polydata": model.build_edge_polydata(
|
||||
deflection=0.8,
|
||||
show_same_domain_internal_edges=show_internal_edges,
|
||||
),
|
||||
"show_internal_edges": show_internal_edges,
|
||||
}
|
||||
|
||||
thread = QThread(self)
|
||||
worker = LoadWorker(action)
|
||||
worker.moveToThread(thread)
|
||||
thread.started.connect(worker.run)
|
||||
worker.finished.connect(self._finish_load_refine)
|
||||
worker.failed.connect(self._fail_load_refine)
|
||||
worker.finished.connect(thread.quit)
|
||||
worker.failed.connect(thread.quit)
|
||||
thread.finished.connect(worker.deleteLater)
|
||||
thread.finished.connect(thread.deleteLater)
|
||||
thread.finished.connect(self._forget_load_refine_thread)
|
||||
self.load_refine_thread = thread
|
||||
self.load_refine_worker = worker
|
||||
thread.start()
|
||||
self._end_load_task()
|
||||
if self.model is not None:
|
||||
self.statusBar().showMessage(f"Loaded {self.step_path.name}")
|
||||
|
||||
@Slot(object)
|
||||
def _finish_load_refine(self, result: object) -> None:
|
||||
try:
|
||||
if isinstance(result, dict) and result.get("model") is self.model:
|
||||
@@ -277,6 +269,7 @@ class WindowCoreMixin:
|
||||
finally:
|
||||
self._end_load_task()
|
||||
|
||||
@Slot(str)
|
||||
def _fail_load_refine(self, message: str) -> None:
|
||||
self._end_load_task()
|
||||
self.statusBar().showMessage(f"Quick preview is available; display refinement failed: {message}")
|
||||
@@ -285,6 +278,8 @@ class WindowCoreMixin:
|
||||
self.load_in_progress = False
|
||||
self.pending_load_path = None
|
||||
self._update_action_states()
|
||||
self._request_thread_quit(self.load_thread)
|
||||
self._request_thread_quit(self.load_refine_thread)
|
||||
|
||||
def _forget_load_thread(self) -> None:
|
||||
self.load_thread = None
|
||||
@@ -319,6 +314,8 @@ class WindowCoreMixin:
|
||||
self.clear_diff_preview(render=False)
|
||||
if hasattr(self, "history_list"):
|
||||
self.history_list.clear()
|
||||
if hasattr(self, "export_all_button"):
|
||||
self._update_action_states()
|
||||
|
||||
def _refresh_history_list(self) -> None:
|
||||
was_blocked = self.history_list.blockSignals(True)
|
||||
@@ -328,6 +325,7 @@ class WindowCoreMixin:
|
||||
self.history_list.addItem(f"{index}. {entry.summary}")
|
||||
finally:
|
||||
self.history_list.blockSignals(was_blocked)
|
||||
self._update_action_states()
|
||||
|
||||
def on_history_row_changed(self, row: int) -> None:
|
||||
if self._edit_busy("编辑计算中,暂时不能查看历史记录详情。"):
|
||||
@@ -407,6 +405,10 @@ class WindowCoreMixin:
|
||||
"index": index,
|
||||
"summary": record.summary,
|
||||
"detail": record.detail,
|
||||
"operation_name": record.operation_name,
|
||||
"target": record.target,
|
||||
"parameters": safe_value(record.parameters or {}),
|
||||
"result_message": record.result_message,
|
||||
"target_kind": record.target_kind,
|
||||
"target_id": record.target_id,
|
||||
"target_logical_id": record.target_logical_id,
|
||||
@@ -417,7 +419,7 @@ class WindowCoreMixin:
|
||||
}
|
||||
)
|
||||
payload = {
|
||||
"format": "step-editor-operation-history-v1",
|
||||
"format": "step-editor-operation-history-v2",
|
||||
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"source_file": str(self.step_path),
|
||||
"record_count": len(records),
|
||||
@@ -558,27 +560,27 @@ class WindowCoreMixin:
|
||||
|
||||
if self.selected_kind == "part" and self.selected_part_id is not None:
|
||||
part_ids = [self.selected_part_id]
|
||||
label = f"part {self.selected_part_id}"
|
||||
label = f"Part {self.selected_part_id}"
|
||||
elif self.selected_kind == "solid" and self.selected_solid_id is not None:
|
||||
face_ids = [index for index, solid_id in enumerate(self.model.face_solid_ids) if solid_id == self.selected_solid_id]
|
||||
edge_ids = self.model.edge_ids_for_solid(self.selected_solid_id)
|
||||
label = f"solid {self.selected_solid_id}"
|
||||
label = f"Solid {self.selected_solid_id}"
|
||||
elif self.selected_kind == "feature" and self.selected_face_id is not None:
|
||||
info = self.model.feature_info(self.selected_face_id)
|
||||
face_ids = _int_values(info.get("feature_highlight_face_ids")) or [self.selected_face_id]
|
||||
edge_ids = _int_values(info.get("feature_boundary_edge_ids"))
|
||||
label = f"feature face {self.selected_face_id}"
|
||||
label = f"Feature Face {self.selected_face_id}"
|
||||
elif self.selected_kind == "face" and self.selected_face_id is not None:
|
||||
face_ids = self.model.connected_same_domain_face_ids(self.selected_face_id) or [self.selected_face_id]
|
||||
edge_ids = self.model.face_region_boundary_edge_ids(self.selected_face_id)
|
||||
label = f"face {self.selected_face_id}"
|
||||
label = f"Face {self.selected_face_id}"
|
||||
elif self.selected_kind == "edge" and self.selected_edge_id is not None:
|
||||
info = self.model.edge_info(self.selected_edge_id)
|
||||
face_ids = _int_values(info.get("adjacent_face_ids"))
|
||||
edge_ids = [self.selected_edge_id]
|
||||
label = f"edge {self.selected_edge_id}"
|
||||
label = f"Edge {self.selected_edge_id}"
|
||||
else:
|
||||
QMessageBox.information(self, "未选择对象", "请先选择 part、solid、face、edge 或 feature。")
|
||||
QMessageBox.information(self, "未选择对象", "请先选择 Part、Solid、Face、Edge 或 Feature。")
|
||||
return
|
||||
|
||||
model_polydata = self.model.build_face_polydata(face_ids=face_ids, part_ids=part_ids)
|
||||
@@ -608,7 +610,7 @@ class WindowCoreMixin:
|
||||
return
|
||||
bounds = self._selected_focus_bounds()
|
||||
if bounds is None:
|
||||
QMessageBox.information(self, "未选择对象", "请先选择 part、solid、face、edge 或 feature。")
|
||||
QMessageBox.information(self, "未选择对象", "请先选择 Part、Solid、Face、Edge 或 Feature。")
|
||||
return
|
||||
self._fit_camera_to_bounds(bounds)
|
||||
self.render_window.Render()
|
||||
@@ -772,6 +774,11 @@ class WindowCoreMixin:
|
||||
self.model_actor.GetProperty().SetSpecular(0.25)
|
||||
self.model_actor.GetProperty().SetSpecularPower(18)
|
||||
self.model_actor.GetProperty().SetInterpolationToPhong()
|
||||
backface_property = vtk.vtkProperty()
|
||||
backface_property.SetColor(0.58, 0.62, 0.64)
|
||||
backface_property.SetDiffuse(0.9)
|
||||
backface_property.SetAmbient(0.28)
|
||||
self.model_actor.SetBackfaceProperty(backface_property)
|
||||
self.renderer.AddActor(self.model_actor)
|
||||
|
||||
self.edge_polydata = edge_polydata
|
||||
@@ -816,7 +823,7 @@ class WindowCoreMixin:
|
||||
self.statusBar().showMessage("编辑计算中,请等待当前操作完成")
|
||||
return
|
||||
if self.scan_in_progress:
|
||||
self.statusBar().showMessage("后台扫描中,请等待扫描完成后再选择对象。")
|
||||
self.statusBar().showMessage("扫描中,请等待扫描完成后再选择对象。")
|
||||
return
|
||||
if self.model is None:
|
||||
return
|
||||
@@ -876,7 +883,16 @@ class WindowCoreMixin:
|
||||
self._queue_hover_position(x, y)
|
||||
|
||||
def _queue_hover_position(self, x: int, y: int) -> None:
|
||||
self.pending_hover_position = (int(x), int(y))
|
||||
position = (int(x), int(y))
|
||||
threshold = int(getattr(self, "hover_move_threshold_px", 0) or 0)
|
||||
if threshold > 0:
|
||||
previous = self.pending_hover_position or self.last_hover_pick_position
|
||||
if previous is not None:
|
||||
dx = abs(position[0] - int(previous[0]))
|
||||
dy = abs(position[1] - int(previous[1]))
|
||||
if max(dx, dy) < threshold:
|
||||
return
|
||||
self.pending_hover_position = position
|
||||
if not self.hover_timer.isActive():
|
||||
self.hover_timer.start(self.hover_interval_ms)
|
||||
|
||||
@@ -1102,7 +1118,7 @@ class WindowCoreMixin:
|
||||
self.select_part(int(target_id))
|
||||
|
||||
def on_cylinder_row_clicked(self, row: int, _column: int) -> None:
|
||||
if self._edit_busy("编辑计算中,暂时不能切换圆柱候选。"):
|
||||
if self._edit_busy("编辑计算中,暂时不能切换圆柱面候选。"):
|
||||
return
|
||||
item = self.cylinder_table.item(row, 0)
|
||||
if item is None:
|
||||
@@ -1127,47 +1143,51 @@ class WindowCoreMixin:
|
||||
if action == "resize_cylinder":
|
||||
self.mode_combo.setCurrentText("Feature")
|
||||
self.select_feature(int(target_id))
|
||||
self.statusBar().showMessage(f"已选择可调整孔径候选 face {target_id}")
|
||||
self.statusBar().showMessage(f"已选择可调整孔径候选Face {target_id}")
|
||||
elif action == "resize_boss":
|
||||
self.mode_combo.setCurrentText("Feature")
|
||||
self.select_feature(int(target_id))
|
||||
self.statusBar().showMessage(f"已选择可调整凸台直径候选 face {target_id}")
|
||||
self.statusBar().showMessage(f"已选择可调整凸台直径候选Face {target_id}")
|
||||
elif action == "resize_slot_width":
|
||||
self.mode_combo.setCurrentText("Feature")
|
||||
self.select_feature(int(target_id))
|
||||
self.statusBar().showMessage(f"已选择可调整槽/半孔宽度候选 face {target_id}")
|
||||
self.statusBar().showMessage(f"已选择可调整槽/半孔宽度候选Face {target_id}")
|
||||
elif action == "resize_shell_thickness":
|
||||
self.mode_combo.setCurrentText("Feature")
|
||||
self.select_feature(int(target_id))
|
||||
self.statusBar().showMessage(f"已选择可调整薄壁厚度候选Face {target_id}")
|
||||
elif action == "suppress_cylinder":
|
||||
self.mode_combo.setCurrentText("Feature")
|
||||
self.select_feature(int(target_id))
|
||||
self.statusBar().showMessage(f"已选择可封堵圆柱孔 face {target_id}")
|
||||
self.statusBar().showMessage(f"已选择可封堵圆柱孔Face {target_id}")
|
||||
elif action == "resize_depth":
|
||||
self.mode_combo.setCurrentText("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.mode_combo.setCurrentText("Feature")
|
||||
self.select_feature(int(target_id))
|
||||
self.statusBar().showMessage(f"已选择已有圆角/倒圆候选 face {target_id}")
|
||||
self.statusBar().showMessage(f"已选择已有圆角/倒圆候选Face {target_id}")
|
||||
elif action == "fillet_edge":
|
||||
self.mode_combo.setCurrentText("Edge")
|
||||
self.select_edge(int(target_id))
|
||||
self.statusBar().showMessage(f"已选择可添加圆角 edge {target_id}")
|
||||
self.statusBar().showMessage(f"已选择可添加圆角Edge {target_id}")
|
||||
elif action == "chamfer_edge":
|
||||
self.mode_combo.setCurrentText("Edge")
|
||||
self.select_edge(int(target_id))
|
||||
self.statusBar().showMessage(f"已选择可添加倒角 edge {target_id}")
|
||||
self.statusBar().showMessage(f"已选择可添加倒角Edge {target_id}")
|
||||
elif action == "resize_edge_length":
|
||||
self.mode_combo.setCurrentText("Edge")
|
||||
self.select_edge(int(target_id))
|
||||
self.statusBar().showMessage(f"已选择可尝试调整长度的直线 edge {target_id}")
|
||||
self.statusBar().showMessage(f"已选择可尝试调整长度的Edge {target_id}")
|
||||
elif target_kind == "edge":
|
||||
self.mode_combo.setCurrentText("Edge")
|
||||
self.select_edge(int(target_id))
|
||||
self.statusBar().showMessage(f"已选择 edge {target_id}")
|
||||
self.statusBar().showMessage(f"已选择Edge {target_id}")
|
||||
else:
|
||||
self.mode_combo.setCurrentText("Face")
|
||||
self.select_face(int(target_id))
|
||||
self.statusBar().showMessage(f"已选择可推拉平面 face {target_id}")
|
||||
self.statusBar().showMessage(f"已选择可推拉平面Face {target_id}")
|
||||
|
||||
def select_by_id(self, kind: str | None = None) -> None:
|
||||
if self.model is None:
|
||||
@@ -1240,7 +1260,7 @@ class WindowCoreMixin:
|
||||
) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
if self._edit_busy("编辑计算中,暂时不能选择 solid。"):
|
||||
if self._edit_busy("编辑计算中,暂时不能选择Solid。"):
|
||||
return
|
||||
info = self.model.solid_info(solid_id)
|
||||
self._reset_selection(clear_highlight=False)
|
||||
@@ -1254,7 +1274,7 @@ class WindowCoreMixin:
|
||||
self._sync_id_picker("Solid", solid_id)
|
||||
self.set_info(self._with_pick_info(info, pick_position))
|
||||
self._update_action_states()
|
||||
self.statusBar().showMessage(self._selection_status(f"已选择 solid {solid_id}", pick_position))
|
||||
self.statusBar().showMessage(self._selection_status(f"已选择Solid {solid_id}", pick_position))
|
||||
|
||||
def select_face(
|
||||
self,
|
||||
@@ -1264,7 +1284,7 @@ class WindowCoreMixin:
|
||||
) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
if self._edit_busy("编辑计算中,暂时不能选择 face。"):
|
||||
if self._edit_busy("编辑计算中,暂时不能选择Face。"):
|
||||
return
|
||||
self._reset_selection(clear_highlight=False)
|
||||
self.selected_kind = "feature" if feature_mode else "face"
|
||||
@@ -1279,9 +1299,10 @@ class WindowCoreMixin:
|
||||
if len(highlight_face_ids) > 1:
|
||||
info["same_domain_face_ids"] = tuple(highlight_face_ids)
|
||||
info["same_domain_face_count"] = len(highlight_face_ids)
|
||||
info["same_domain_note"] = "已高亮属于同一几何面且范围相接/重叠的连续 face 区域。"
|
||||
info["same_domain_note"] = "已高亮属于同一几何面且范围相接/重叠的连续Face区域。"
|
||||
logical_id = self.model.face_region_logical_id(face_id)
|
||||
self._sync_cylindrical_edit_inputs(info)
|
||||
input_info = self.model.feature_info(face_id) if info.get("surface") == "plane" else info
|
||||
self._sync_cylindrical_edit_inputs(input_info)
|
||||
self.selected_part_id = int(info["part_id"])
|
||||
self.selected_solid_id = int(info["solid_id"]) if int(info["solid_id"]) >= 0 else None
|
||||
self._highlight_faces(face_ids=highlight_face_ids or [face_id])
|
||||
@@ -1289,9 +1310,9 @@ class WindowCoreMixin:
|
||||
self._sync_id_picker("Feature" if feature_mode else "Face", face_id if feature_mode else logical_id)
|
||||
self.set_info(self._with_pick_info(info, pick_position))
|
||||
self._update_action_states()
|
||||
suffix = f",同域区域 {len(highlight_face_ids)} 个 face" if not feature_mode and len(highlight_face_ids) > 1 else ""
|
||||
raw_note = f"(拓扑 face {face_id})" if not feature_mode and logical_id != face_id else ""
|
||||
message = f"已选择逻辑面区域 {logical_id}{raw_note}{suffix}" if not feature_mode else f"已选择 face {face_id}"
|
||||
suffix = f",同域区域 {len(highlight_face_ids)} 个Face" if not feature_mode and len(highlight_face_ids) > 1 else ""
|
||||
raw_note = f"(拓扑Face {face_id})" if not feature_mode and logical_id != face_id else ""
|
||||
message = f"已选择逻辑面区域 {logical_id}{raw_note}{suffix}" if not feature_mode else f"已选择Face {face_id}"
|
||||
self.statusBar().showMessage(self._selection_status(message, pick_position))
|
||||
|
||||
def select_feature(self, face_id: int, pick_position: tuple[float, float, float] | None = None) -> None:
|
||||
@@ -1314,10 +1335,16 @@ class WindowCoreMixin:
|
||||
self.set_info(self._with_pick_info(info, pick_position))
|
||||
feature_type = str(info.get("feature_type", "局部特征候选"))
|
||||
self._update_action_states()
|
||||
self.statusBar().showMessage(self._selection_status(f"已选择 {feature_type},来源 face {face_id}", pick_position))
|
||||
self.statusBar().showMessage(self._selection_status(f"已选择 {feature_type},来源Face {face_id}", pick_position))
|
||||
|
||||
def _sync_cylindrical_edit_inputs(self, info: dict[str, object]) -> None:
|
||||
fillet_radius_suggestion: str | None = None
|
||||
shell_thickness = _float_or_none(info.get("shell_thickness_estimate"))
|
||||
if hasattr(self, "shell_thickness_input"):
|
||||
if info.get("shell_region_status") == "candidate" and shell_thickness is not None and shell_thickness > 0:
|
||||
self.shell_thickness_input.setText(_format_float(shell_thickness * 1.2))
|
||||
else:
|
||||
self.shell_thickness_input.clear()
|
||||
if "diameter" in info:
|
||||
feature_guess = str(info.get("feature_guess", ""))
|
||||
if feature_guess == "boss/outer-round candidate":
|
||||
@@ -1373,6 +1400,8 @@ class WindowCoreMixin:
|
||||
|
||||
def _sync_edge_edit_inputs(self, info: dict[str, object]) -> None:
|
||||
self.hole_diameter_input.clear()
|
||||
if hasattr(self, "shell_thickness_input"):
|
||||
self.shell_thickness_input.clear()
|
||||
if hasattr(self, "slot_width_input"):
|
||||
self.slot_width_input.clear()
|
||||
self.boss_diameter_input.clear()
|
||||
@@ -1395,7 +1424,7 @@ class WindowCoreMixin:
|
||||
def select_edge(self, edge_id: int, pick_position: tuple[float, float, float] | None = None) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
if self._edit_busy("编辑计算中,暂时不能选择 edge。"):
|
||||
if self._edit_busy("编辑计算中,暂时不能选择Edge。"):
|
||||
return
|
||||
self._reset_selection(clear_highlight=False)
|
||||
self.selected_kind = "edge"
|
||||
@@ -1410,7 +1439,7 @@ class WindowCoreMixin:
|
||||
self._sync_id_picker("Edge", edge_id)
|
||||
self.set_info(self._with_pick_info(info, pick_position))
|
||||
self._update_action_states()
|
||||
self.statusBar().showMessage(self._selection_status(f"已选择 edge {edge_id}", pick_position))
|
||||
self.statusBar().showMessage(self._selection_status(f"已选择Edge {edge_id}", pick_position))
|
||||
|
||||
def _highlight_faces(self, face_ids=None, part_ids=None) -> None:
|
||||
if self.model is None:
|
||||
@@ -1635,6 +1664,19 @@ class WindowCoreMixin:
|
||||
self._start_edit_preview_pulse()
|
||||
self.render_window.Render()
|
||||
|
||||
def _show_shell_thickness_preview(self, face_id: int, target_thickness: float) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
self.clear_edit_preview(render=False)
|
||||
try:
|
||||
polydata = self.model.shell_thickness_preview_polydata(face_id, target_thickness)
|
||||
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_resize_preview(self, face_id: int, diameter: float) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
@@ -1740,7 +1782,7 @@ class WindowCoreMixin:
|
||||
try:
|
||||
polydata = self.model.straight_edge_length_preview_polydata(edge_id, target_length, anchor_mode=anchor_mode)
|
||||
except Exception as exc:
|
||||
self.statusBar().showMessage(f"边长直接修改预览不可用:{exc}")
|
||||
self.statusBar().showMessage(f"Edge长度修改预览不可用:{exc}")
|
||||
return
|
||||
if isinstance(polydata, list):
|
||||
for preview in polydata:
|
||||
@@ -1771,7 +1813,7 @@ class WindowCoreMixin:
|
||||
ids.InsertNextId(cell_id)
|
||||
break
|
||||
if ids.GetNumberOfIds() == 0:
|
||||
self.statusBar().showMessage(f"{label}不可用:没有找到选中的 edge。")
|
||||
self.statusBar().showMessage(f"{label}不可用:没有找到选中的Edge。")
|
||||
return
|
||||
extract = vtk.vtkExtractCells()
|
||||
extract.SetInputData(polydata)
|
||||
@@ -1817,6 +1859,8 @@ class WindowCoreMixin:
|
||||
if render and hasattr(self, "render_window"):
|
||||
self.render_window.Render()
|
||||
self.statusBar().showMessage("已清除差异预览")
|
||||
if hasattr(self, "export_all_button"):
|
||||
self._update_action_states()
|
||||
|
||||
def _show_operation_diff(self, record: OperationRecord) -> str:
|
||||
if self.model is None:
|
||||
@@ -1838,6 +1882,7 @@ class WindowCoreMixin:
|
||||
for actor in self.diff_actors:
|
||||
self.renderer.AddViewProp(actor)
|
||||
self.render_window.Render()
|
||||
self._update_action_states()
|
||||
if heatmap_stats:
|
||||
record.diff_stats = heatmap_stats
|
||||
return (
|
||||
|
||||
+384
-61
@@ -87,7 +87,7 @@ class WindowStateMixin:
|
||||
self.statusBar().showMessage(message)
|
||||
return True
|
||||
if self.scan_in_progress:
|
||||
self.statusBar().showMessage("后台扫描中,请等待扫描完成后再执行该操作。")
|
||||
self.statusBar().showMessage("扫描中,请等待扫描完成后再执行该操作。")
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -98,33 +98,229 @@ class WindowStateMixin:
|
||||
def _update_action_states(self) -> None:
|
||||
if not hasattr(self, "export_all_button"):
|
||||
return
|
||||
has_model = (
|
||||
self.model is not None
|
||||
and not self.operation_in_progress
|
||||
and not self.scan_in_progress
|
||||
and not self.load_in_progress
|
||||
)
|
||||
has_loaded_model = self.model is not None
|
||||
busy = self.operation_in_progress or self.scan_in_progress or self.load_in_progress
|
||||
has_model = has_loaded_model and not busy
|
||||
selected_kind = self.selected_kind
|
||||
self.export_all_button.setEnabled(has_model)
|
||||
self.export_check_button.setEnabled(has_model)
|
||||
has_selection = any(
|
||||
item is not None
|
||||
for item in (
|
||||
self.selected_part_id,
|
||||
self.selected_solid_id,
|
||||
self.selected_face_id,
|
||||
self.selected_edge_id,
|
||||
)
|
||||
)
|
||||
has_part = self.selected_part_id is not None
|
||||
has_solid = self.selected_solid_id is not None
|
||||
has_face = self.selected_face_id is not None and selected_kind in {"face", "feature"}
|
||||
has_edge = self.selected_edge_id is not None and selected_kind == "edge"
|
||||
id_text = self.id_input.text().strip() if hasattr(self, "id_input") else ""
|
||||
history_row = self.history_list.currentRow() if hasattr(self, "history_list") else -1
|
||||
has_history = has_model and bool(getattr(self, "operation_history", []))
|
||||
has_history_selection = has_history and 0 <= history_row < len(self.operation_history)
|
||||
has_diff_preview = has_model and bool(getattr(self, "diff_actors", []))
|
||||
has_measure_points = bool(getattr(self, "measure_point_a", None) or getattr(self, "measure_point_b", None))
|
||||
has_measure_result = bool(getattr(self, "measure_point_a", None) and getattr(self, "measure_point_b", None))
|
||||
pick_value = getattr(self, "current_info_values", {}).get("pick_position")
|
||||
has_pick_position = self.selected_pick_position is not None or (
|
||||
isinstance(pick_value, (tuple, list)) and len(pick_value) == 3
|
||||
)
|
||||
selected_id_text = self._selected_id_text() if hasattr(self, "_selected_id_text") else ""
|
||||
has_current_info = bool(getattr(self, "current_info_text", ""))
|
||||
wait_or_load_tip = "请先加载 STEP 文件,或等待当前后台任务完成。"
|
||||
|
||||
if hasattr(self, "open_button"):
|
||||
self._set_control_state(
|
||||
self.open_button,
|
||||
not busy,
|
||||
"选择并打开一个 .step 或 .stp 文件。",
|
||||
"请等待当前后台任务完成后再打开其他 STEP 文件。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.reload_button,
|
||||
has_loaded_model and not busy,
|
||||
"从磁盘重新读取当前 STEP 文件。",
|
||||
wait_or_load_tip,
|
||||
)
|
||||
if hasattr(self, "part_tree"):
|
||||
self._set_control_state(
|
||||
self.part_tree,
|
||||
has_model,
|
||||
"从结构树中选择 Part、Solid 或装配节点。",
|
||||
wait_or_load_tip,
|
||||
)
|
||||
if hasattr(self, "mode_combo"):
|
||||
self._set_control_state(
|
||||
self.mode_combo,
|
||||
not busy,
|
||||
"选择鼠标点击模型时要选什么对象。",
|
||||
"请等待当前后台任务完成后再切换鼠标选择模式。",
|
||||
)
|
||||
if hasattr(self, "id_input"):
|
||||
self._set_control_state(
|
||||
self.id_input,
|
||||
has_model,
|
||||
"输入要选中的对象 ID。",
|
||||
wait_or_load_tip,
|
||||
)
|
||||
self._set_control_state(
|
||||
self.select_id_button,
|
||||
has_model and bool(id_text),
|
||||
"按当前选择模式跳转到输入的 ID。",
|
||||
"请先加载模型并输入整数 ID。",
|
||||
)
|
||||
|
||||
self._set_control_state(
|
||||
self.export_all_button,
|
||||
has_model,
|
||||
"导出当前完整 STEP 模型。",
|
||||
wait_or_load_tip,
|
||||
)
|
||||
self._set_control_state(
|
||||
self.export_check_button,
|
||||
has_model,
|
||||
"检查当前导出对象的 B-Rep、数量、体积和包围盒等质量信息。",
|
||||
wait_or_load_tip,
|
||||
)
|
||||
if hasattr(self, "repair_model_button"):
|
||||
self._set_control_state(
|
||||
self.repair_model_button,
|
||||
has_model,
|
||||
"对当前完整模型执行 ShapeFix 和同域面/边合并,并写入撤销历史。",
|
||||
"请先加载 STEP 文件,或等待当前后台任务完成。",
|
||||
wait_or_load_tip,
|
||||
)
|
||||
self._set_control_state(
|
||||
self.repair_selected_button,
|
||||
has_model and (self.selected_solid_id is not None or self.selected_part_id is not None),
|
||||
"优先修复当前选中对象所属 solid;没有 solid 时修复所属零件,并写入撤销历史。",
|
||||
"请先选择 part、solid、face 或 edge,或等待当前后台任务完成。",
|
||||
has_model and (has_solid or has_part),
|
||||
"优先修复当前选中对象所属Solid;没有Solid时修复所属零件,并写入撤销历史。",
|
||||
"请先选择 Part、Solid、Face 或 Edge,或等待当前后台任务完成。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.export_part_button,
|
||||
has_model and has_part,
|
||||
"导出当前选中对象所属零件。",
|
||||
"请先选择 Part,或选择属于某个 Part 的 Solid、Face 或 Edge。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.export_solid_button,
|
||||
has_model and has_solid,
|
||||
"导出当前选中对象所属 Solid。",
|
||||
"请先选择 Solid,或选择属于某个 Solid 的 Face 或 Edge。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.export_face_button,
|
||||
has_model and has_face,
|
||||
"导出当前选中的 Face 或同域面区域。",
|
||||
"请先在 Face 或 Feature 模式下选择一个 Face。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.export_feature_button,
|
||||
has_model and selected_kind == "feature" and self.selected_face_id is not None,
|
||||
"导出 Feature 模式识别到的局部特征区域。",
|
||||
"请先切换到 Feature 模式并选择孔、槽、圆角或凸台候选。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.export_edge_button,
|
||||
has_model and has_edge,
|
||||
"导出当前选中的 Edge。",
|
||||
"请先切换到 Edge 模式并选择一条 Edge。",
|
||||
)
|
||||
if hasattr(self, "isolate_button"):
|
||||
self._set_control_state(
|
||||
self.isolate_button,
|
||||
has_model and has_selection,
|
||||
"只显示当前选中的对象或区域。",
|
||||
"请先加载模型并选择 Part、Solid、Face、Edge 或 Feature。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.fit_button,
|
||||
has_model and has_selection,
|
||||
"把相机对准当前选中对象。",
|
||||
"请先加载模型并选择一个对象。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.show_all_button,
|
||||
has_model,
|
||||
"恢复显示完整模型。",
|
||||
wait_or_load_tip,
|
||||
)
|
||||
self._set_control_state(
|
||||
self.show_internal_edges_checkbox,
|
||||
has_model,
|
||||
"显示或隐藏同域内部拓扑边。",
|
||||
wait_or_load_tip,
|
||||
)
|
||||
if hasattr(self, "set_measure_a_button"):
|
||||
measure_point_tip = "把当前选中对象的拾取点或中心设为测量点。"
|
||||
self._set_control_state(
|
||||
self.set_measure_a_button,
|
||||
has_model and has_selection,
|
||||
measure_point_tip,
|
||||
"请先加载模型并选择一个对象。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.set_measure_b_button,
|
||||
has_model and has_selection,
|
||||
measure_point_tip,
|
||||
"请先加载模型并选择一个对象。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.copy_measure_button,
|
||||
has_measure_result,
|
||||
"复制 A/B 两点距离和 X/Y/Z 差值。",
|
||||
"请先设置测量点 A 和 B。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.clear_measure_button,
|
||||
has_measure_points,
|
||||
"清除 A/B 测量点和 3D 测量线。",
|
||||
"当前没有可清除的测量点。",
|
||||
)
|
||||
if hasattr(self, "history_list"):
|
||||
self._set_control_state(
|
||||
self.history_list,
|
||||
has_history,
|
||||
"点击历史记录可查看参数、定位目标并显示差异预览。",
|
||||
"当前还没有编辑历史。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.clear_diff_button,
|
||||
has_diff_preview,
|
||||
"清除当前显示的差异预览。",
|
||||
"当前没有正在显示的差异预览。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.export_diff_button,
|
||||
has_history_selection,
|
||||
"导出当前选中历史记录的差异报告。",
|
||||
"请先在操作历史中选择一条记录。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.export_history_button,
|
||||
has_history,
|
||||
"导出本次会话的完整编辑历史 JSON。",
|
||||
"当前还没有可导出的编辑历史。",
|
||||
)
|
||||
if hasattr(self, "copy_id_button"):
|
||||
self._set_control_state(
|
||||
self.copy_id_button,
|
||||
bool(selected_id_text),
|
||||
"复制当前选中对象的 ID。",
|
||||
"请先选择一个对象。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.copy_pick_button,
|
||||
has_pick_position,
|
||||
"复制最近一次鼠标拾取到的三维坐标。",
|
||||
"当前没有可复制的拾取坐标。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.copy_info_button,
|
||||
has_current_info,
|
||||
"复制当前属性区里的完整文本。",
|
||||
"当前属性区还没有可复制的信息。",
|
||||
)
|
||||
self.export_part_button.setEnabled(has_model and selected_kind == "part" and self.selected_part_id is not None)
|
||||
self.export_solid_button.setEnabled(has_model and selected_kind == "solid" and self.selected_solid_id is not None)
|
||||
self.export_face_button.setEnabled(has_model and selected_kind == "face" and self.selected_face_id is not None)
|
||||
self.export_feature_button.setEnabled(has_model and selected_kind == "feature" and self.selected_face_id is not None)
|
||||
self.export_edge_button.setEnabled(has_model and selected_kind == "edge" and self.selected_edge_id is not None)
|
||||
self._update_edit_action_states(has_model)
|
||||
|
||||
def _update_edit_action_states(self, has_model: bool) -> None:
|
||||
@@ -139,6 +335,7 @@ class WindowStateMixin:
|
||||
has_face = self.selected_face_id is not None and self.selected_kind in {"face", "feature"}
|
||||
has_edge = self.selected_edge_id is not None and self.selected_kind == "edge"
|
||||
is_plane = has_face and surface == "plane"
|
||||
is_shell_candidate = is_plane and action_info.get("shell_region_status") == "candidate"
|
||||
is_cylinder = has_face and surface == "cylinder" and "diameter" in action_info
|
||||
is_hole_or_groove = is_cylinder and feature_guess == "hole/groove candidate"
|
||||
is_boss = is_cylinder and feature_guess == "boss/outer-round candidate"
|
||||
@@ -158,111 +355,237 @@ class WindowStateMixin:
|
||||
)
|
||||
has_fillet_support = len(_int_values(action_info.get("feature_existing_fillet_support_face_ids"))) >= 2
|
||||
is_line_edge = has_edge and curve == "line"
|
||||
can_push = has_model and is_plane
|
||||
can_resize_shell = has_model and is_shell_candidate
|
||||
can_resize_hole = has_model and is_hole_or_groove
|
||||
can_resize_slot = has_model and is_slot_or_half_hole
|
||||
can_resize_boss = has_model and is_boss and is_full_cylinder
|
||||
can_suppress_hole = has_model and is_hole_or_groove and is_full_cylinder
|
||||
can_resize_depth = has_model and is_hole_or_groove and ((is_blind and has_bottom) or has_manual_bottom)
|
||||
can_fillet_edge = has_model and is_line_edge
|
||||
can_resize_existing_fillet = has_model and is_existing_fillet and has_fillet_support
|
||||
can_chamfer_edge = has_model and is_line_edge
|
||||
can_resize_edge_length = has_model and has_edge
|
||||
can_transform_part = has_model and self.selected_part_id is not None
|
||||
can_transform_solid = has_model and self.selected_solid_id is not None
|
||||
|
||||
self._set_control_state(
|
||||
self.offset_input,
|
||||
can_push,
|
||||
"输入平面 Face 的推拉距离。",
|
||||
"请先选择一个可推拉的平面 Face。",
|
||||
)
|
||||
|
||||
self._set_control_state(
|
||||
self.push_button,
|
||||
has_model and is_plane,
|
||||
"对当前平面 face 执行推拉。",
|
||||
"请先选择一个平面 face,或在 Feature 模式下选择可推拉平面候选。",
|
||||
can_push,
|
||||
"对当前平面 Face 执行推拉。",
|
||||
"请先选择一个平面 Face,或在 Feature 模式下选择可推拉平面候选。",
|
||||
)
|
||||
if hasattr(self, "resize_shell_thickness_button"):
|
||||
self._set_control_state(
|
||||
self.shell_thickness_input,
|
||||
can_resize_shell,
|
||||
"输入薄壁/壳体局部区域的目标厚度。",
|
||||
"请先选择一个已识别到相对平面的薄壁/壳体平面候选。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.resize_shell_thickness_button,
|
||||
can_resize_shell,
|
||||
"按目标厚度推拉当前薄壁/壳体平面区域。",
|
||||
"请先选择一个已识别到相对平面的薄壁/壳体平面候选。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.hole_diameter_input,
|
||||
can_resize_hole,
|
||||
"输入圆柱孔/槽的目标直径。",
|
||||
"请先选择被识别为孔/槽候选的圆柱 Face。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.resize_button,
|
||||
has_model and is_hole_or_groove,
|
||||
can_resize_hole,
|
||||
"调整当前圆柱孔/槽候选的直径。",
|
||||
"请先选择被识别为孔/槽候选的圆柱 face。",
|
||||
"请先选择被识别为孔/槽候选的圆柱 Face。",
|
||||
)
|
||||
if hasattr(self, "resize_slot_button"):
|
||||
self._set_control_state(
|
||||
self.slot_width_input,
|
||||
can_resize_slot,
|
||||
"输入槽/半孔候选的目标宽度。",
|
||||
"请先选择被识别为槽/半孔的部分圆柱 Face。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.resize_slot_button,
|
||||
has_model and is_slot_or_half_hole,
|
||||
"按目标槽宽调整当前槽/半孔候选;第一版会换算为对应圆柱直径后执行。",
|
||||
"请先选择被识别为槽/半孔的部分圆柱 face。",
|
||||
can_resize_slot,
|
||||
"按目标槽宽调整当前槽/半孔候选;当前版本会换算为对应圆柱直径后执行。",
|
||||
"请先选择被识别为槽/半孔的部分圆柱 Face。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.boss_diameter_input,
|
||||
can_resize_boss,
|
||||
"输入完整圆柱凸台候选的目标直径。",
|
||||
"请先选择被识别为完整凸台/外圆候选的圆柱 Face。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.resize_boss_button,
|
||||
has_model and is_boss and is_full_cylinder,
|
||||
can_resize_boss,
|
||||
"调整当前完整圆柱凸台候选的直径。",
|
||||
"请先选择被识别为完整凸台/外圆候选的圆柱 face。",
|
||||
"请先选择被识别为完整凸台/外圆候选的圆柱 Face。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.suppress_button,
|
||||
has_model and is_hole_or_groove and is_full_cylinder,
|
||||
can_suppress_hole,
|
||||
"封堵当前完整圆柱孔候选。",
|
||||
"请先选择接近完整圆柱的孔候选;半孔/槽不会放行。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.hole_depth_input,
|
||||
can_resize_hole,
|
||||
"输入盲孔/盲槽的目标深度。",
|
||||
"请先选择孔/槽圆柱面。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.hole_bottom_face_input,
|
||||
can_resize_hole,
|
||||
"必要时输入底面 Face ID,用来明确盲孔/盲槽深度。",
|
||||
"请先选择孔/槽圆柱面。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.resize_depth_button,
|
||||
has_model and is_hole_or_groove and ((is_blind and has_bottom) or has_manual_bottom),
|
||||
can_resize_depth,
|
||||
"调整当前盲孔/盲槽深度;自动底面不稳定时可手动填写底面 Face ID。",
|
||||
"请先选择孔/槽圆柱面;如果没有自动识别到底面,请填写底面 Face ID。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.edge_fillet_radius_input,
|
||||
can_fillet_edge or can_resize_existing_fillet,
|
||||
"输入新圆角或已有圆角的目标半径。",
|
||||
"请先选择直线Edge,或选择可修改的已有圆角 Face。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.fillet_edge_button,
|
||||
has_model and is_line_edge,
|
||||
"给当前直线 edge 添加新圆角。",
|
||||
"请先选择一条直线 edge。",
|
||||
can_fillet_edge,
|
||||
"给当前直线Edge添加新圆角。",
|
||||
"请先选择一条直线Edge。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.resize_existing_fillet_button,
|
||||
has_model and is_existing_fillet and has_fillet_support,
|
||||
can_resize_existing_fillet,
|
||||
"尝试修改当前已有圆角/倒圆候选的半径。",
|
||||
"请先选择一个已有圆角/倒圆候选 face;第一版需要识别到至少两个支撑 face。",
|
||||
"请先选择一个已有圆角/倒圆候选 Face;当前版本需要识别到至少两个支撑 Face。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.edge_chamfer_distance_input,
|
||||
can_chamfer_edge,
|
||||
"输入当前直线Edge的倒角距离。",
|
||||
"请先选择一条直线Edge。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.chamfer_edge_button,
|
||||
has_model and is_line_edge,
|
||||
"给当前直线 edge 添加倒角。",
|
||||
"请先选择一条直线 edge。",
|
||||
can_chamfer_edge,
|
||||
"给当前直线Edge添加倒角。",
|
||||
"请先选择一条直线Edge。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.edge_target_length_input,
|
||||
can_resize_edge_length,
|
||||
"输入当前Edge的目标长度。",
|
||||
"请先选择一条Edge。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.edge_length_anchor_combo,
|
||||
can_resize_edge_length,
|
||||
"选择修改Edge长度时尽量固定哪一端。",
|
||||
"请先选择一条Edge。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.resize_edge_length_button,
|
||||
has_model and has_edge,
|
||||
"直接修改当前 edge 的长度;直线优先端面推拉,其他 edge 使用高风险几何 fallback。",
|
||||
"请先选择一条 edge。",
|
||||
can_resize_edge_length,
|
||||
"修改当前Edge的长度;直线Edge优先局部形变,圆/椭圆/平面曲线优先用局部或径向策略。",
|
||||
"请先选择一条Edge。",
|
||||
)
|
||||
for widget in (self.translate_x_input, self.translate_y_input, self.translate_z_input):
|
||||
self._set_control_state(
|
||||
widget,
|
||||
can_transform_part or can_transform_solid,
|
||||
"输入选中零件或 Solid 的平移距离。",
|
||||
"请先选择一个零件、Solid,或其所属 Face/Edge。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.translate_part_button,
|
||||
has_model and self.selected_part_id is not None,
|
||||
can_transform_part,
|
||||
"平移当前选中对象所属零件。",
|
||||
"请先选择一个零件,或选择属于某个零件的对象。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.rotate_part_button,
|
||||
has_model and self.selected_part_id is not None,
|
||||
can_transform_part,
|
||||
"旋转当前选中对象所属零件。",
|
||||
"请先选择一个零件,或选择属于某个零件的对象。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.rotate_axis_combo,
|
||||
can_transform_part or can_transform_solid,
|
||||
"选择选中零件或 Solid 的旋转轴。",
|
||||
"请先选择一个零件、Solid,或其所属 Face/Edge。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.rotate_angle_input,
|
||||
can_transform_part or can_transform_solid,
|
||||
"输入选中零件或 Solid 的旋转角度。",
|
||||
"请先选择一个零件、Solid,或其所属 Face/Edge。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.translate_solid_button,
|
||||
has_model and self.selected_solid_id is not None,
|
||||
"平移当前选中对象所属 solid。",
|
||||
"请先选择一个 solid,或选择属于某个 solid 的 face/edge。",
|
||||
can_transform_solid,
|
||||
"平移当前选中对象所属 Solid。",
|
||||
"请先选择一个 Solid,或选择属于某个 Solid 的 Face/Edge。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.rotate_solid_button,
|
||||
has_model and self.selected_solid_id is not None,
|
||||
"旋转当前选中对象所属 solid。",
|
||||
"请先选择一个 solid,或选择属于某个 solid 的 face/edge。",
|
||||
can_transform_solid,
|
||||
"旋转当前选中对象所属 Solid。",
|
||||
"请先选择一个 Solid,或选择属于某个 Solid 的 Face/Edge。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.cylinders_button,
|
||||
has_model,
|
||||
"扫描当前模型中的圆柱候选。",
|
||||
"扫描当前模型中的圆柱面候选。",
|
||||
"请先加载 STEP 文件。",
|
||||
)
|
||||
if hasattr(self, "editable_refresh_button"):
|
||||
self._set_control_state(
|
||||
self.editable_refresh_button,
|
||||
has_model,
|
||||
"扫描第一版可编辑对象。",
|
||||
"扫描可编辑对象。",
|
||||
"请先加载 STEP 文件,或等待当前后台任务完成。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.editable_deep_scan_button,
|
||||
has_model,
|
||||
"深度扫描更多第一版可编辑对象。",
|
||||
"深度扫描更多可编辑对象。",
|
||||
"请先加载 STEP 文件,或等待当前后台任务完成。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.editable_table,
|
||||
has_model and self.editable_table.rowCount() > 0,
|
||||
"点击一行可选中对应对象并填入相关编辑参数。",
|
||||
"当前没有可编辑对象列表;请先点击扫描对象。",
|
||||
)
|
||||
if hasattr(self, "candidate_filter_combo"):
|
||||
has_cylinder_candidates = has_model and bool(getattr(self, "cylinder_candidates_loaded", False))
|
||||
self._set_control_state(
|
||||
self.candidate_filter_combo,
|
||||
has_cylinder_candidates,
|
||||
"筛选已扫描的圆柱面候选。",
|
||||
"请先点击扫描圆柱面。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.cylinder_table,
|
||||
has_model and self.cylinder_table.rowCount() > 0,
|
||||
"点击一行可选中对应圆柱 Face。",
|
||||
"当前没有圆柱面候选列表;请先点击扫描圆柱面。",
|
||||
)
|
||||
self._set_control_state(
|
||||
self.undo_button,
|
||||
has_model and bool(self.undo_stack),
|
||||
@@ -292,7 +615,7 @@ class WindowStateMixin:
|
||||
return self.model.feature_info(self.selected_face_id)
|
||||
if self.selected_kind == "face" and self.selected_face_id is not None:
|
||||
info = self.model.face_info(self.selected_face_id)
|
||||
if info.get("surface") == "cylinder":
|
||||
if info.get("surface") in {"cylinder", "plane"}:
|
||||
return self.model.feature_info(self.selected_face_id)
|
||||
return info
|
||||
if self.selected_kind == "edge" and self.selected_edge_id is not None:
|
||||
@@ -330,23 +653,23 @@ class WindowStateMixin:
|
||||
face_ids = self.model.face_region_ids(resolved_face_id)
|
||||
self._highlight_faces(face_ids=face_ids)
|
||||
located = True
|
||||
region_note = f";同域面区域 {len(face_ids)} 个 face" if len(face_ids) > 1 else ""
|
||||
region_note = f";同域面区域 {len(face_ids)} 个Face" if len(face_ids) > 1 else ""
|
||||
id_note = (
|
||||
f"逻辑 Face ID {current_logical_id}"
|
||||
if record.target_logical_id is not None
|
||||
else f"face {record.target_id}"
|
||||
else f"Face {record.target_id}"
|
||||
)
|
||||
if resolved_face_id != record.target_id:
|
||||
id_note += f"(当前拓扑 face {resolved_face_id})"
|
||||
id_note += f"(当前拓扑Face {resolved_face_id})"
|
||||
locator_note = (
|
||||
f"定位: 已尝试高亮当前模型中的 {id_note}{region_note}。"
|
||||
"布尔编辑后 face ID 可能发生语义变化,请结合拾取点确认。"
|
||||
"布尔编辑后Face ID可能发生语义变化,请结合拾取点确认。"
|
||||
)
|
||||
else:
|
||||
if record.target_logical_id is not None:
|
||||
locator_note = f"定位: 原目标逻辑 Face ID {record.target_logical_id} 在当前模型索引中已经不存在。"
|
||||
else:
|
||||
locator_note = f"定位: 原目标 face {record.target_id} 在当前模型索引中已经不存在。"
|
||||
locator_note = f"定位: 原目标Face {record.target_id} 在当前模型索引中已经不存在。"
|
||||
|
||||
elif record.target_kind == "edge" and record.target_id is not None:
|
||||
if 0 <= record.target_id < len(self.model.edges):
|
||||
@@ -361,11 +684,11 @@ class WindowStateMixin:
|
||||
self._highlight_edge(record.target_id)
|
||||
located = True
|
||||
locator_note = (
|
||||
f"定位: 已尝试高亮当前模型中的 edge {record.target_id}。"
|
||||
"布尔/倒圆编辑后 edge ID 可能发生语义变化,请结合拾取点确认。"
|
||||
f"定位: 已尝试高亮当前模型中的Edge {record.target_id}。"
|
||||
"布尔/倒圆编辑后Edge ID可能发生语义变化,请结合拾取点确认。"
|
||||
)
|
||||
else:
|
||||
locator_note = f"定位: 原目标 edge {record.target_id} 在当前模型索引中已经不存在。"
|
||||
locator_note = f"定位: 原目标Edge {record.target_id} 在当前模型索引中已经不存在。"
|
||||
|
||||
if record.pick_position is not None:
|
||||
self._show_pick_marker(record.pick_position)
|
||||
|
||||
Reference in New Issue
Block a user