Files
pythonocc-step-editor/step_editor/app.py
T

1516 lines
77 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from datetime import datetime
import faulthandler
import math
import os
import sys
from pathlib import Path
import vtk
import vtkmodules.vtkInteractionStyle # noqa: F401
import vtkmodules.vtkRenderingFreeType # noqa: F401
import vtkmodules.vtkRenderingOpenGL2 # noqa: F401
from PySide6.QtCore import Qt, QThread, QTimer, Signal, Slot
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import (
QAbstractItemView,
QApplication,
QCheckBox,
QComboBox,
QFileDialog,
QFrame,
QGridLayout,
QGroupBox,
QHeaderView,
QHBoxLayout,
QLabel,
QLineEdit,
QListWidget,
QMainWindow,
QMessageBox,
QPushButton,
QPlainTextEdit,
QScrollArea,
QSizePolicy,
QTableWidget,
QTableWidgetItem,
QTabWidget,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
QWidget,
)
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
from .records import OperationRecord
from .model import StepModel
from .widgets import NoWheelComboBox
from .workers import EditWorker, LoadWorker, ScanWorker
from .info_panel import InfoPanelMixin
from .ui_helpers import * # noqa: F403
from .window_actions import WindowActionMixin
from .window_core import WindowCoreMixin
from .window_state import WindowStateMixin
_CRASH_LOG_HANDLE = None
PROJECT_ROOT = Path(__file__).resolve().parent.parent
APP_USER_MODEL_ID = "GeometryParametric.StepEditor"
ISOLATED_EDIT_WORKER_ARG = "--isolated-edit-worker"
def _resource_path(*relative_parts: str) -> Path:
frozen_root = getattr(sys, "_MEIPASS", None)
if frozen_root:
return Path(frozen_root).joinpath(*relative_parts)
return PROJECT_ROOT.joinpath(*relative_parts)
APP_ICON_PATH = _resource_path("assets", "ico", "logo_new.ico")
DEFAULT_MODEL_PATH = _resource_path("assets", "models", "geom_extract.step")
# Panel build switches. These work like a Python-side "#if 0": code in a
# disabled branch is kept for later, but the widgets are not constructed.
ENABLE_STRUCTURE_TREE_PANEL = False
ENABLE_VIEW_PANEL = False
ENABLE_MEASURE_PANEL = False
ENABLE_EXPORT_PANEL = False
ENABLE_EDITABLE_OBJECTS_PANEL = False
ENABLE_CYLINDER_CANDIDATES_PANEL = False
ENABLE_HISTORY_PANEL = False
def _enable_crash_log() -> None:
global _CRASH_LOG_HANDLE
if _CRASH_LOG_HANDLE is not None:
return
try:
log_path = Path("step_editor_crash.log")
_CRASH_LOG_HANDLE = log_path.open("a", encoding="utf-8")
faulthandler.enable(file=_CRASH_LOG_HANDLE, all_threads=True)
except Exception:
faulthandler.enable(all_threads=True)
def _crash_log_requested(argv: list[str]) -> bool:
if "--crash-log" in argv:
return True
return os.environ.get("STEP_EDITOR_CRASH_LOG", "").strip().lower() in {"1", "true", "yes", "on"}
def _application_icon() -> QIcon:
icon = QIcon(str(APP_ICON_PATH))
if icon.isNull():
icon = QIcon(str(PROJECT_ROOT / "assets" / "ico" / "logo_new.ico"))
return icon
def _set_windows_taskbar_identity() -> None:
if sys.platform != "win32":
return
try:
import ctypes
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(APP_USER_MODEL_ID)
except Exception:
pass
def _isolated_edit_worker_request(argv: list[str]) -> Path | None:
if ISOLATED_EDIT_WORKER_ARG not in argv:
return None
index = argv.index(ISOLATED_EDIT_WORKER_ARG)
if index + 1 >= len(argv):
raise ValueError(f"{ISOLATED_EDIT_WORKER_ARG} requires a request JSON path.")
return Path(argv[index + 1])
class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, InfoPanelMixin, QMainWindow):
ui_task_requested = Signal(object)
def __init__(self, step_path: str | Path, *, background_load: bool = False):
super().__init__()
self.ui_task_requested.connect(self._run_ui_task, Qt.ConnectionType.QueuedConnection)
self.setWindowTitle("几何参数化")
self.setWindowIcon(_application_icon())
self.resize(1280, 820)
self.model: StepModel | None = None
self.step_path = Path(step_path)
self.selected_kind: str | None = None
self.selected_part_id: int | None = None
self.selected_solid_id: int | None = None
self.selected_face_id: int | None = None
self.selected_edge_id: int | None = None
self.selected_pick_position: tuple[float, float, float] | None = None
self.model_actor = None
self.edge_actor = None
self.highlight_actor = None
self.edge_highlight_actor = None
self.highlight_signature: tuple[object, ...] | None = None
self.hover_face_actor = None
self.hover_edge_actor = None
self.hover_signature: tuple[str, int] | None = None
self.hover_interval_ms = 260
self.hover_move_threshold_px = 10
self.pending_hover_position: tuple[int, int] | None = None
self.last_hover_pick_position: tuple[int, int] | None = None
self.camera_interaction_active = False
self.last_camera_interaction_ended_at: datetime | None = None
self.hover_after_camera_cooldown_ms = 420
self.pointer_button_down = False
self.left_button_press_position: tuple[int, int] | None = None
self.left_button_dragged = False
self.left_click_drag_threshold_px = 6
self.skip_next_vtk_left_press = False
self.skip_next_vtk_left_release = False
self.pick_marker_actor = None
self.edit_preview_actor = None
self.edit_preview_actors: list[object] = []
self.edit_preview_timer: QTimer | None = None
self.hover_timer = QTimer(self)
self.hover_timer.setSingleShot(True)
self.hover_timer.timeout.connect(self._update_hover_target)
self.edit_preview_phase = 0.0
self.edit_preview_base_opacity = 0.35
self.diff_actors: list[object] = []
self.model_polydata = None
self.edge_polydata = None
self.model_face_id_array = None
self.model_part_id_array = None
self.model_solid_id_array = None
self.edge_id_array = None
self.model_face_cell_ids_by_face: dict[int, list[int]] = {}
self.model_face_cell_ids_by_part: dict[int, list[int]] = {}
self.model_face_cell_ids_by_solid: dict[int, list[int]] = {}
self.edge_cell_ids_by_edge: dict[int, list[int]] = {}
self.face_overlay_polydata_cache: dict[tuple[tuple[int, ...] | None, tuple[int, ...] | None, bool], object] = {}
self.edge_overlay_polydata_cache: dict[int, object] = {}
self.overlay_cache_limit = 160
self.show_internal_edges_checkbox: QCheckBox | None = None
self.scene_isolated = False
self.undo_stack: list[dict[int, object]] = []
self.redo_stack: list[dict[int, object]] = []
self.operation_history: list[OperationRecord] = []
self.redo_history: list[OperationRecord] = []
self.measure_point_a: tuple[float, float, float] | None = None
self.measure_point_b: tuple[float, float, float] | None = None
self.measure_label_a = ""
self.measure_label_b = ""
self.measure_actor = None
self.current_info_text = ""
self.current_info_values: dict[str, object] = {}
self.cylinder_candidate_cache: list[dict[str, object]] = []
self.cylinder_candidates_loaded = False
self.operation_in_progress = False
self.edit_busy_timer = QTimer(self)
self.edit_busy_timer.timeout.connect(self._pulse_edit_busy_status)
self.edit_busy_base_text = ""
self.edit_busy_started_at: datetime | None = None
self.edit_busy_phase = 0
self.edit_thread: QThread | None = None
self.edit_worker: EditWorker | None = None
self.pending_edit_context: dict[str, object] | None = None
self.active_isolated_edit_process = None
self.isolated_edit_cancel_requested = False
self.close_after_edit_cancel = False
self.scan_in_progress = False
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.scan_wait_cursor_active = False
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.auto_load_on_show = bool(background_load)
self.vtk_interactor_started = False
self.first_show_handled = False
self.preview_load_deflection = 1.0
self.initial_load_deflection = 0.035
self.edit_result_deflection = 0.035
self.last_id_kind = "Feature"
self.feature_detection_level = "current-only"
self.property_editor_updating = False
self._control_state_cache: dict[int, tuple[bool, str]] = {}
self._selected_action_info_cache_key: tuple[object, ...] | None = None
self._selected_action_info_cache_value: dict[str, object] | None = None
self.property_editor_specs: list[dict[str, object]] = []
self.property_table_expanded = False
self.property_table_collapsed_rows = 6
self._build_ui()
self._build_vtk()
def _build_ui(self) -> None:
help_tip = self._set_help_tip
central = QWidget()
root_layout = QHBoxLayout(central)
root_layout.setContentsMargins(10, 10, 10, 10)
root_layout.setSpacing(10)
self.setCentralWidget(central)
panel_scroll = QScrollArea()
panel_scroll.setWidgetResizable(True)
panel_scroll.setFixedWidth(440)
panel_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
root_layout.addWidget(panel_scroll)
panel = QWidget()
panel.setFixedWidth(420)
panel_layout = QVBoxLayout(panel)
panel_layout.setContentsMargins(0, 0, 0, 0)
panel_layout.setSpacing(10)
panel_scroll.setWidget(panel)
panel_scroll.setStyleSheet(
"""
QScrollArea {
background: #edf2f8;
border: none;
}
QScrollArea > QWidget > QWidget {
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: 1px solid #d4dde8;
border-left: 5px solid #6b7cff;
border-radius: 8px;
color: #1f2937;
margin-top: 14px;
padding: 15px 10px 10px 12px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 14px;
padding: 2px 8px;
color: #172033;
background: #ffffff;
border: 1px solid #d4dde8;
border-radius: 7px;
font-weight: 700;
}
QGroupBox#fileSection {
background: #f7fbff;
border: 1px solid #bdd3ff;
border-left: 5px solid #3478f6;
}
QGroupBox#fileSection::title {
background: #eaf2ff;
border-color: #bdd3ff;
color: #1d4ed8;
}
QGroupBox#treeSection {
background: #f2fbf8;
border: 1px solid #b8dfd6;
border-left: 5px solid #0f9f8f;
}
QGroupBox#treeSection::title {
background: #e2f5ef;
border-color: #b8dfd6;
color: #0f766e;
}
QFrame#modeSection {
background: #f6f3ff;
border: 1px solid #a99bff;
border-left: 5px solid #5b4bdb;
border-radius: 7px;
}
QLabel#modeSectionTitle {
background: #ebe7ff;
border: 1px solid #a99bff;
border-radius: 5px;
color: #4736b3;
font-weight: 700;
padding: 1px 8px;
}
QFrame#modeIdSeparator {
background: #ded8ff;
border-left: 2px solid #5b4bdb;
border-right: 1px solid #b8adff;
max-width: 8px;
min-width: 8px;
}
QLabel#mouseModeLabel,
QLabel#idSelectLabel,
QLabel#featureDetectionLabel {
color: #4736b3;
font-weight: 700;
}
QGroupBox#viewSection {
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 {
background: #f4fbf3;
border: 1px solid #bfdcbd;
border-left: 5px solid #1f9d55;
}
QGroupBox#exportSection::title {
background: #e8f5e7;
border-color: #bfdcbd;
color: #1f7a3d;
}
QGroupBox#editSection {
background: #fff8ef;
border: 1px solid #edc98e;
border-left: 5px solid #d97706;
padding-left: 2px;
padding-right: 1px;
padding-bottom: 6px;
}
QGroupBox#editSection::title {
background: #fff0d8;
border-color: #edc98e;
color: #a45303;
left: 10px;
padding-left: 6px;
padding-right: 6px;
}
QGroupBox#editableSection,
QGroupBox#candidateSection {
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 {
background: #fff5f8;
border: 1px solid #edb8cb;
border-left: 5px solid #be3b6b;
}
QGroupBox#historySection::title {
background: #fde7ef;
border-color: #edb8cb;
color: #9d2854;
}
QGroupBox#infoSection {
background: #f8fafc;
border: 1px solid #cbd5e1;
border-left: 5px solid #64748b;
}
QGroupBox#infoSection::title {
background: #eef2f7;
border-color: #cbd5e1;
color: #475569;
}
QLabel {
color: #2f3846;
}
QLineEdit,
QComboBox {
background: #ffffff;
border: 1px solid #c8d2df;
border-radius: 5px;
color: #172033;
min-height: 24px;
padding: 4px 6px;
}
QLineEdit:focus,
QComboBox:focus {
border: 1px solid #3478f6;
}
QPushButton {
background: #f8fafc;
border: 1px solid #c8d2df;
border-radius: 6px;
color: #172033;
font-weight: 600;
min-height: 24px;
padding: 5px 8px;
}
QPushButton:hover {
background: #eef6ff;
border-color: #77a7dd;
}
QPushButton:pressed {
background: #d7e6f6;
}
QPushButton:disabled {
background: #e3e8ef;
border: 1px dashed #b6c0cd;
color: #8f99a8;
font-weight: 500;
}
QPushButton#propertyRowEditButton {
background: #ea580c;
border: 1px solid #c2410c;
border-radius: 5px;
color: #ffffff;
font-weight: 700;
min-height: 18px;
padding: 1px 3px;
}
QPushButton#propertyRowEditButton:hover {
background: #f97316;
border-color: #9a3412;
}
QPushButton#propertyRowEditButton:pressed {
background: #9a3412;
border-color: #7c2d12;
color: #ffffff;
padding-top: 2px;
padding-left: 4px;
}
QPushButton#propertyRowEditButton[changed="true"] {
background: #dcfce7;
border: 1px solid #16a34a;
color: #14532d;
}
QPushButton#propertyRowEditButton[changed="true"]:hover {
background: #bbf7d0;
border-color: #15803d;
}
QPushButton#propertyRowEditButton:disabled {
background: #eef2f6;
border: 1px dashed #bcc7d4;
color: #8f99a8;
}
QPushButton#propertyRowEditButton[invalid="true"],
QPushButton#propertyRowEditButton[invalid="true"]:disabled {
background: #fee2e2;
border: 1px solid #ef4444;
color: #991b1b;
}
QComboBox#propertyScopeCombo {
background: #ffffff;
border: 1px solid #f59e0b;
border-radius: 5px;
color: #7c2d12;
min-height: 18px;
padding: 0px 2px;
font-weight: 600;
}
QComboBox#propertyScopeCombo:hover {
background: #fffbeb;
border-color: #ea580c;
}
QComboBox#propertyScopeCombo:focus {
background: #ffffff;
border: 1px solid #2563eb;
}
QComboBox#propertyScopeCombo:disabled {
background: #eef2f6;
border: 1px dashed #bcc7d4;
color: #8f99a8;
}
QLineEdit#propertyTargetEditor {
background: #fff7ed;
border: 2px solid #f97316;
border-radius: 5px;
color: #111827;
min-height: 18px;
padding: 0px 2px;
selection-background-color: #bfdbfe;
}
QLineEdit#propertyTargetEditor:hover {
background: #ffedd5;
border-color: #ea580c;
}
QLineEdit#propertyTargetEditor:focus {
background: #ffffff;
border: 2px solid #2563eb;
padding: 0px 1px;
}
QPushButton#propertyExpandBar {
background: #f8fafc;
border: 1px solid #d7dee8;
border-radius: 4px;
color: #475569;
font-weight: 600;
min-height: 14px;
max-height: 20px;
padding: 1px 6px;
}
QPushButton#propertyExpandBar:hover {
background: #eef6ff;
border-color: #9dbbe0;
color: #1e3a8a;
}
QPushButton#propertyExpandBar:pressed {
background: #dbeafe;
padding-top: 2px;
}
QLineEdit:disabled,
QComboBox:disabled,
QPlainTextEdit:disabled {
background: #eef2f6;
border: 1px dashed #bcc7d4;
color: #8f99a8;
}
QLineEdit#idSelectInput {
background: #ffffff;
border: 1px solid #8f82ea;
color: #172033;
placeholder-text-color: #64748b;
}
QLineEdit#idSelectInput:focus {
background: #ffffff;
border: 1px solid #5b4bdb;
}
QLineEdit#idSelectInput:disabled {
background: #ffffff;
border: 1px solid #c8d2df;
color: #172033;
}
QLineEdit#stepPathDisplay {
background: #ffffff;
border: 1px solid #bdd3ff;
color: #172033;
}
QCheckBox:disabled,
QTabWidget:disabled,
QTreeWidget:disabled,
QTableWidget:disabled,
QListWidget:disabled {
color: #8f99a8;
}
QTreeWidget,
QTableWidget,
QListWidget,
QPlainTextEdit {
background: #ffffff;
border: 1px solid #d8e0eb;
border-radius: 6px;
color: #172033;
alternate-background-color: #f7fafd;
selection-background-color: #d7ecff;
selection-color: #0f172a;
}
QHeaderView::section {
background: #e8eef6;
border: 0;
border-bottom: 1px solid #d8e0eb;
color: #334155;
font-weight: 700;
padding: 4px 6px;
}
QTableWidget#propertyTable {
margin: 0;
padding: 0;
}
QTableWidget#propertyTable::item {
padding-left: 1px;
padding-right: 1px;
}
QTableWidget#propertyTable QHeaderView::section {
padding-left: 1px;
padding-right: 1px;
}
QTabWidget::pane {
border: 1px solid #d8e0eb;
border-radius: 6px;
top: -1px;
}
QTabBar::tab {
background: #e9eef6;
border: 1px solid #d8e0eb;
border-bottom: none;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
color: #334155;
padding: 5px 10px;
}
QTabBar::tab:selected {
background: #ffffff;
color: #172033;
font-weight: 700;
}
"""
)
file_box = QGroupBox("STEP 文件")
file_box.setObjectName("fileSection")
help_tip(file_box, "导入、查看路径并读取当前 STEP 几何模型。")
file_layout = QVBoxLayout(file_box)
file_buttons = QHBoxLayout()
file_buttons.setSpacing(6)
self.open_button = QPushButton("导入几何模型")
self.open_button.setMinimumWidth(96)
help_tip(self.open_button, "选择并导入一个 .step 或 .stp 几何模型。打开失败时会保留当前模型。")
self.open_button.clicked.connect(self.open_step)
self.path_label = QLineEdit(str(self.step_path))
self.path_label.setObjectName("stepPathDisplay")
self.path_label.setReadOnly(True)
self.path_label.setMinimumWidth(120)
help_tip(self.path_label, "当前 STEP 文件的完整路径。可以选中文字复制路径。")
self.path_label.setToolTip(str(self.step_path))
self.path_label.setCursorPosition(0)
self.reload_button = QPushButton("读取模型")
self.reload_button.setMinimumWidth(78)
help_tip(self.reload_button, "从显示的路径重新读取 STEP 模型,用于放弃本次会话里的临时查看状态。")
self.reload_button.clicked.connect(self.reload_step)
file_buttons.addWidget(self.open_button)
file_buttons.addWidget(self.path_label, stretch=1)
file_buttons.addWidget(self.reload_button)
file_layout.addLayout(file_buttons)
panel_layout.addWidget(file_box)
if ENABLE_STRUCTURE_TREE_PANEL:
tree_box = QGroupBox("模型结构树")
tree_box.setObjectName("treeSection")
help_tip(tree_box, "查看 STEP 里的装配、零件和实体层级,并从树上直接选中对象。")
tree_layout = QVBoxLayout(tree_box)
self.part_tree = QTreeWidget()
self.part_tree.setHeaderLabels(["对象", "内容"])
self.part_tree.setMinimumHeight(180)
self.part_tree.setAlternatingRowColors(True)
help_tip(self.part_tree, "模型里的装配、零件和Solid列表。点击一行可以选中并高亮对应对象。")
self.part_tree.currentItemChanged.connect(self.on_part_tree_select)
tree_layout.addWidget(self.part_tree)
panel_layout.addWidget(tree_box)
mode_box = QWidget()
mode_box.setMinimumHeight(62)
help_tip(mode_box, "决定鼠标点模型时选中零件、Solid、Face、Edge,还是识别几何特征。")
self.mode_section_title = QLabel("选择模式", mode_box)
self.mode_section_title.setObjectName("modeSectionTitle")
self.mode_section_title.setFixedSize(74, 20)
self.mode_section_title.move(12, 0)
self.mode_section_title.raise_()
help_tip(self.mode_section_title, "选择鼠标点模型时按什么对象类型选择。")
mode_outer_layout = QVBoxLayout(mode_box)
mode_outer_layout.setContentsMargins(0, 9, 0, 0)
mode_outer_layout.setSpacing(0)
self.mode_section_frame = QFrame()
self.mode_section_frame.setObjectName("modeSection")
self.mode_section_frame.setMinimumHeight(50)
mode_outer_layout.addWidget(self.mode_section_frame)
self.mode_section_title.raise_()
mode_layout = QHBoxLayout(self.mode_section_frame)
mode_layout.setSpacing(0)
mode_layout.setContentsMargins(0, 0, 0, 0)
mode_pick_panel = QWidget()
mode_pick_layout = QHBoxLayout(mode_pick_panel)
mode_pick_layout.setContentsMargins(8, 10, 8, 6)
mode_pick_layout.setSpacing(6)
self.mouse_mode_label = QLabel("按鼠标:")
self.mouse_mode_label.setObjectName("mouseModeLabel")
self.mouse_mode_label.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignRight)
self.mouse_mode_label.setMaximumWidth(62)
help_tip(self.mouse_mode_label, "这里决定鼠标点击 3D 模型时按哪种对象类型选择。")
self.mode_combo = NoWheelComboBox()
for mode in ("Feature", "Face", "Edge", "Solid", "Part"):
self.mode_combo.addItem(_selection_mode_label(mode), mode)
self._set_selection_mode("Feature")
self.mode_combo.setMinimumWidth(94)
self.mode_combo.setMaximumWidth(112)
help_tip(
self.mode_combo,
"选择模式决定鼠标点击模型时要选什么:零件、Solid、Face、Edge,或把 Face 解释成孔/槽/圆角等几何特征候选。",
)
self.mode_combo.currentIndexChanged.connect(lambda _index: self._on_mode_changed(self._current_selection_mode()))
mode_pick_layout.addWidget(self.mouse_mode_label)
mode_pick_layout.addWidget(self.mode_combo)
mode_layout.addWidget(mode_pick_panel)
mode_separator = QFrame()
mode_separator.setFrameShape(QFrame.Shape.NoFrame)
mode_separator.setObjectName("modeIdSeparator")
mode_separator.setFixedWidth(8)
mode_separator.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
self.mode_id_separator = mode_separator
help_tip(mode_separator, "左侧是鼠标点选模式,右侧是按当前模式输入 ID 选择。")
mode_layout.addWidget(mode_separator)
help_tip(mode_separator, "左侧是鼠标点选模式,右侧是特征探测级别。")
detection_panel = QWidget()
detection_layout = QHBoxLayout(detection_panel)
detection_layout.setContentsMargins(8, 10, 8, 6)
detection_layout.setSpacing(6)
self.feature_detection_label = QLabel("特征探测级别")
self.feature_detection_label.setObjectName("featureDetectionLabel")
self.feature_detection_label.setMinimumWidth(88)
help_tip(self.feature_detection_label, "控制点击特征时探测多深。默认只探测当前特征的一层局部关联。")
self.feature_detection_combo = NoWheelComboBox()
self.feature_detection_combo.addItem("只识别当前特征", "current-only")
self.feature_detection_combo.addItem("探测相邻特征", "associated-only")
self.feature_detection_combo.addItem("探测二级特征", "secondary")
self.feature_detection_combo.setCurrentIndex(0)
self.feature_detection_combo.setMinimumWidth(128)
help_tip(
self.feature_detection_combo,
"默认只读取当前点击的特征,点击最流畅;需要查找周边孔、槽、凸台等关联时,再切换到相邻或二级探测。",
)
self.feature_detection_combo.currentIndexChanged.connect(
lambda _index: self._on_feature_detection_level_changed()
)
detection_layout.addWidget(self.feature_detection_label)
detection_layout.addWidget(self.feature_detection_combo, stretch=1)
mode_layout.addWidget(detection_panel, stretch=1)
id_select_panel = QWidget()
id_select_panel.setVisible(False)
select_layout = QHBoxLayout(id_select_panel)
select_layout.setContentsMargins(8, 10, 8, 6)
select_layout.setSpacing(6)
self.id_select_label = QLabel(f"按ID{_selection_mode_label(self._current_selection_mode())}")
self.id_select_label.setObjectName("idSelectLabel")
self.id_select_label.setMinimumWidth(78)
help_tip(self.id_select_label, "按当前选择模式解释输入的 ID。")
self.id_input = QLineEdit("")
self.id_input.setObjectName("idSelectInput")
self.id_input.setPlaceholderText("ID")
self.id_input.setMaximumWidth(70)
help_tip(self.id_input, "输入要选中的对象 ID。ID 类型显示在左侧标签中,并会跟随选择模式变化。")
self.select_id_button = QPushButton("选择")
self.select_id_button.setMaximumWidth(58)
help_tip(self.select_id_button, "按当前选择模式跳转到输入的 ID,并在 3D 视图中高亮它。")
self.select_id_button.clicked.connect(lambda _checked=False: self.select_by_id(self._current_selection_mode()))
self.id_input.textChanged.connect(lambda _text: self._update_action_states())
self.id_input.returnPressed.connect(lambda: self.select_by_id(self._current_selection_mode()))
self.id_select_label.setVisible(False)
self.id_input.setVisible(False)
self.select_id_button.setVisible(False)
select_layout.addWidget(self.id_select_label)
select_layout.addWidget(self.id_input)
select_layout.addWidget(self.select_id_button)
mode_layout.addWidget(id_select_panel, stretch=1)
panel_layout.addWidget(mode_box)
if ENABLE_VIEW_PANEL:
view_box = QGroupBox("显示")
view_box.setObjectName("viewSection")
help_tip(view_box, "只改变视图显示方式,不会修改 STEP 几何。")
view_layout = QVBoxLayout(view_box)
view_buttons = QHBoxLayout()
self.isolate_button = QPushButton("只显示选中")
help_tip(self.isolate_button, "把视图临时隔离到当前选中的零件、Solid、Face、Edge 或几何特征区域。不会修改模型。")
self.isolate_button.clicked.connect(self.isolate_selected)
self.fit_button = QPushButton("对准选中")
help_tip(self.fit_button, "把相机移动到当前选中对象附近,方便看清细节。不会修改模型。")
self.fit_button.clicked.connect(self.fit_selected)
self.show_all_button = QPushButton("显示全部")
help_tip(self.show_all_button, "取消隔离显示,恢复查看完整模型。不会修改模型。")
self.show_all_button.clicked.connect(self.show_all_geometry)
view_buttons.addWidget(self.isolate_button)
view_buttons.addWidget(self.fit_button)
view_buttons.addWidget(self.show_all_button)
view_layout.addLayout(view_buttons)
self.show_internal_edges_checkbox = QCheckBox("显示同域内部边")
help_tip(
self.show_internal_edges_checkbox,
"显示同一平面或同一圆柱面内部的拓扑分割边。关闭时会隐藏布尔拉伸/切除后常见的视觉接缝线。",
)
self.show_internal_edges_checkbox.toggled.connect(self._on_internal_edges_toggled)
view_layout.addWidget(self.show_internal_edges_checkbox)
if ENABLE_MEASURE_PANEL:
measure_box = QGroupBox("测量")
measure_box.setObjectName("measureSection")
help_tip(measure_box, "把当前选中对象的拾取点或中心设为 A/B,计算两点距离和 X/Y/Z 差值。不会修改模型。")
measure_layout = QVBoxLayout(measure_box)
self.measure_text = QPlainTextEdit()
self.measure_text.setReadOnly(True)
self.measure_text.setMaximumHeight(90)
help_tip(self.measure_text, "显示 A 点、B 点、两点距离和各方向差值。")
measure_buttons = QGridLayout()
self.set_measure_a_button = QPushButton("设为 A")
help_tip(self.set_measure_a_button, "把当前选中对象的拾取点设为测量点 A;没有拾取点时使用对象中心。")
self.set_measure_a_button.clicked.connect(lambda _checked=False: self.set_measure_point("A"))
self.set_measure_b_button = QPushButton("设为 B")
help_tip(self.set_measure_b_button, "把当前选中对象的拾取点设为测量点 B;没有拾取点时使用对象中心。")
self.set_measure_b_button.clicked.connect(lambda _checked=False: self.set_measure_point("B"))
self.copy_measure_button = QPushButton("复制测量")
help_tip(self.copy_measure_button, "复制当前测量结果文本,方便记录尺寸。")
self.copy_measure_button.clicked.connect(self.copy_measurement)
self.clear_measure_button = QPushButton("清除测量")
help_tip(self.clear_measure_button, "清除 A/B 测量点和 3D 测量线。不会影响模型。")
self.clear_measure_button.clicked.connect(self.clear_measurement)
measure_buttons.addWidget(self.set_measure_a_button, 0, 0)
measure_buttons.addWidget(self.set_measure_b_button, 0, 1)
measure_buttons.addWidget(self.copy_measure_button, 1, 0)
measure_buttons.addWidget(self.clear_measure_button, 1, 1)
measure_layout.addWidget(self.measure_text)
measure_layout.addLayout(measure_buttons)
self._refresh_measurement_panel()
if ENABLE_EXPORT_PANEL:
export_box = QGroupBox("导出")
export_box.setObjectName("exportSection")
help_tip(export_box, "把当前模型或选中对象导出为 STEP,也可以做基础质量检查和修复。")
export_layout = QVBoxLayout(export_box)
self.export_all_button = QPushButton("导出当前完整 STEP")
help_tip(self.export_all_button, "把当前编辑后的整个模型导出为 STEP 文件。导出前会做基础质量检查。")
self.export_all_button.clicked.connect(self.export_all)
self.export_part_button = QPushButton("导出选中零件")
help_tip(self.export_part_button, "只导出当前选中的零件。适合从装配里拆出一个单独零件。")
self.export_part_button.clicked.connect(self.export_selected_part)
self.export_solid_button = QPushButton("导出选中Solid")
help_tip(self.export_solid_button, "只导出当前选中的实体Solid。适合检查或单独保存某个实体。")
self.export_solid_button.clicked.connect(self.export_selected_solid)
self.export_face_button = QPushButton("导出选中面区域")
help_tip(self.export_face_button, "导出当前选中的面区域;同域高亮的共面/同圆柱区域也会一起导出。")
self.export_face_button.clicked.connect(self.export_selected_face)
self.export_feature_button = QPushButton("导出选中特征区域")
help_tip(self.export_feature_button, "导出特征模式识别到的区域,例如孔、槽、圆角或凸台候选。")
self.export_feature_button.clicked.connect(self.export_selected_feature)
self.export_edge_button = QPushButton("导出选中Edge")
help_tip(self.export_edge_button, "导出当前选中的Edge。主要用于调试、定位或把边界单独拿出去检查。")
self.export_edge_button.clicked.connect(self.export_selected_edge)
self.export_check_button = QPushButton("检查导出质量")
help_tip(self.export_check_button, "检查当前导出对象是否像有效实体:B-Rep、Face/Edge 数量、体积和包围盒等。")
self.export_check_button.clicked.connect(self.check_export_quality)
self.repair_model_button = QPushButton("修复当前模型")
help_tip(self.repair_model_button, "对完整模型尝试 ShapeFix 和同域面/边合并。会写入历史,可撤销。")
self.repair_model_button.clicked.connect(self.repair_model)
self.repair_selected_button = QPushButton("修复选中零件/Solid")
help_tip(self.repair_selected_button, "只修复当前选中的零件或Solid,范围比修复完整模型更小。会写入历史,可撤销。")
self.repair_selected_button.clicked.connect(self.repair_selected_shape)
export_layout.addWidget(self.export_all_button)
export_layout.addWidget(self.export_part_button)
export_layout.addWidget(self.export_solid_button)
export_layout.addWidget(self.export_face_button)
export_layout.addWidget(self.export_feature_button)
export_layout.addWidget(self.export_edge_button)
export_layout.addWidget(self.export_check_button)
export_layout.addWidget(self.repair_model_button)
export_layout.addWidget(self.repair_selected_button)
self.object_edit_box = QGroupBox("特征参数:未选择")
self.object_edit_box.setObjectName("editSection")
help_tip(
self.object_edit_box,
"选择特征后,这里只列出可可靠修改的语义尺寸;面积、中心和拓扑数据位于下方诊断信息。",
)
object_edit_layout = QVBoxLayout(self.object_edit_box)
object_edit_layout.setContentsMargins(0, 8, 0, 4)
object_edit_layout.setSpacing(4)
self.property_table = QTableWidget(0, 5)
self.property_table.setObjectName("propertyTable")
self.property_table.setHorizontalHeaderLabels(["尺寸参数", "当前值", "建模意图", "目标值", "操作"])
self.property_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.property_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.property_table.setAlternatingRowColors(True)
self.property_table.setMinimumHeight(180)
self.property_table.setWordWrap(False)
self.property_table.setTextElideMode(Qt.TextElideMode.ElideMiddle)
self.property_table.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.property_table.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.property_table.verticalHeader().setVisible(False)
self.property_table.verticalHeader().setDefaultSectionSize(24)
self.property_table.verticalHeader().setMinimumSectionSize(22)
property_header = self.property_table.horizontalHeader()
property_header.setMinimumSectionSize(44)
property_header.setStretchLastSection(False)
property_header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
property_header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
property_header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
property_header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed)
property_header.setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch)
self.property_table.setColumnWidth(0, 148)
self.property_table.setColumnWidth(1, 68)
self.property_table.setColumnWidth(2, 96)
self.property_table.setColumnWidth(3, 72)
self.property_table.setColumnWidth(4, 54)
help_tip(
self.property_table,
"特征模式显示当前特征及局部关联特征的可变尺寸;建模意图决定这次修改是局部重建、拉伸/切除、端面移动还是整体缩放。",
)
self.property_table.itemChanged.connect(self._on_property_table_item_changed)
object_edit_layout.addWidget(self.property_table)
self.property_expand_button = QPushButton("展开全部参数")
self.property_expand_button.setObjectName("propertyExpandBar")
help_tip(self.property_expand_button, "参数较多时默认只显示前 6 行;点击这里展开或收起完整参数列表。")
self.property_expand_button.clicked.connect(self.toggle_property_table_expanded)
self.property_expand_button.setVisible(False)
self.property_expand_button.setMaximumHeight(22)
self.property_expand_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
object_edit_layout.addWidget(self.property_expand_button)
property_action_row = QHBoxLayout()
property_action_row.setContentsMargins(0, 0, 0, 0)
self.apply_property_button = QPushButton("参数化建模")
help_tip(self.apply_property_button, "按当前属性表里修改过的目标值执行参数化建模;一次只执行一个几何修改,成功后可撤销。")
self.apply_property_button.clicked.connect(self.apply_current_property_edit)
self.quick_export_all_button = QPushButton("导出当前完整STEP")
help_tip(self.quick_export_all_button, "把当前编辑后的完整模型导出为 STEP 文件。导出前会做基础质量检查。")
self.quick_export_all_button.clicked.connect(self.export_all)
property_action_row.addWidget(self.apply_property_button)
property_action_row.addWidget(self.quick_export_all_button)
object_edit_layout.addLayout(property_action_row)
edit_box = QGroupBox(panel)
edit_box.setObjectName("editSection")
edit_box.setVisible(False)
self.hidden_edit_controls_box = edit_box
edit_layout = QGridLayout(edit_box)
edit_layout.addWidget(QLabel("移动距离"), 0, 0)
self.offset_input = QLineEdit("5.0")
help_tip(self.offset_input, "平面拉伸/切除距离。正数通常向外加料,负数通常向内切削;单位沿用 STEP 模型单位。")
edit_layout.addWidget(self.offset_input, 0, 1)
self.push_button = QPushButton("拉伸/切除平面")
help_tip(self.push_button, "移动当前选中的平面区域:正数加料,负数切削。会先显示半透明预览,再后台执行。")
self.push_button.clicked.connect(self.push_pull_face)
edit_layout.addWidget(self.push_button, 1, 0, 1, 2)
edit_layout.addWidget(QLabel("孔直径"), 2, 0)
self.hole_diameter_input = QLineEdit("")
help_tip(self.hole_diameter_input, "圆柱孔/槽的目标直径。选中候选后会自动填一个参考值,可以手动改。")
edit_layout.addWidget(self.hole_diameter_input, 2, 1)
self.hole_center_x_input = QLineEdit("", edit_box)
self.hole_center_y_input = QLineEdit("", edit_box)
self.hole_center_z_input = QLineEdit("", edit_box)
for hole_center_input in (
self.hole_center_x_input,
self.hole_center_y_input,
self.hole_center_z_input,
):
hole_center_input.setVisible(False)
help_tip(self.hole_center_x_input, "圆柱孔轴心目标 X 坐标。当前选中对象表会使用这个隐藏输入执行孔位置修改。")
help_tip(self.hole_center_y_input, "圆柱孔轴心目标 Y 坐标。当前选中对象表会使用这个隐藏输入执行孔位置修改。")
help_tip(self.hole_center_z_input, "圆柱孔轴心目标 Z 坐标。当前选中对象表会使用这个隐藏输入执行孔位置修改。")
self.resize_button = QPushButton("调整圆柱孔径")
help_tip(self.resize_button, "修改孔或圆柱槽的直径。扩大时切削,缩小时会先补料再重切。")
self.resize_button.clicked.connect(self.resize_hole)
edit_layout.addWidget(self.resize_button, 3, 0, 1, 2)
edit_layout.addWidget(QLabel("槽/半孔宽度"), 4, 0)
self.slot_width_input = QLineEdit("")
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, "修改已识别槽/半孔候选的宽度。当前版本是几何近似,失败会回滚。")
self.resize_slot_button.clicked.connect(self.resize_slot_width)
edit_layout.addWidget(self.resize_slot_button, 5, 0, 1, 2)
edit_layout.addWidget(QLabel("槽/半孔深度"), 6, 0)
self.slot_depth_input = QLineEdit("")
help_tip(self.slot_depth_input, "槽或半孔的目标凹入深度。程序会按当前圆弧角度换算成对应圆柱直径来执行。")
edit_layout.addWidget(self.slot_depth_input, 6, 1)
self.resize_slot_depth_button = QPushButton("调整槽/半孔深度")
help_tip(self.resize_slot_depth_button, "修改已识别槽/半孔候选的凹入深度。当前版本是几何近似,失败会回滚。")
self.resize_slot_depth_button.clicked.connect(self.resize_slot_depth)
edit_layout.addWidget(self.resize_slot_depth_button, 7, 0, 1, 2)
self.slot_arc_length_input = QLineEdit("", edit_box)
self.slot_arc_length_input.setVisible(False)
help_tip(self.slot_arc_length_input, "槽或半孔的目标圆弧长度。当前选中对象表会使用这个隐藏输入执行圆弧长度修改。")
self.slot_angular_span_input = QLineEdit("", edit_box)
self.slot_angular_span_input.setVisible(False)
help_tip(self.slot_angular_span_input, "槽或半孔的目标圆弧角度。当前选中对象表会使用这个隐藏输入执行角度修改。")
self.slot_pair_face_input = QLineEdit("", edit_box)
self.slot_pair_face_input.setVisible(False)
help_tip(self.slot_pair_face_input, "长圆槽/槽孔的另一个半圆端 Face ID。自动配对不准时可在当前选中对象表里手动设置。")
self.slot_total_length_input = QLineEdit("", edit_box)
self.slot_total_length_input.setVisible(False)
help_tip(self.slot_total_length_input, "长圆槽/槽孔的目标总长度。只有识别到成对半圆槽端时才会启用。")
self.slot_center_distance_input = QLineEdit("", edit_box)
self.slot_center_distance_input.setVisible(False)
help_tip(self.slot_center_distance_input, "长圆槽/槽孔两端半圆中心的目标距离。当前选中对象表会使用这个隐藏输入执行中心距修改。")
self.slot_center_x_input = QLineEdit("", edit_box)
self.slot_center_x_input.setVisible(False)
self.slot_center_y_input = QLineEdit("", edit_box)
self.slot_center_y_input.setVisible(False)
self.slot_center_z_input = QLineEdit("", edit_box)
self.slot_center_z_input.setVisible(False)
help_tip(self.slot_center_x_input, "槽/半孔目标轴心坐标 X。当前选中对象表会使用这个隐藏输入执行轴心移动。")
help_tip(self.slot_center_y_input, "槽/半孔目标轴心坐标 Y。当前选中对象表会使用这个隐藏输入执行轴心移动。")
help_tip(self.slot_center_z_input, "槽/半孔目标轴心坐标 Z。当前选中对象表会使用这个隐藏输入执行轴心移动。")
edit_layout.addWidget(QLabel("凸台直径"), 8, 0)
self.boss_diameter_input = QLineEdit("")
help_tip(self.boss_diameter_input, "圆柱凸台的目标直径。选中凸台候选后会自动填一个参考值。")
edit_layout.addWidget(self.boss_diameter_input, 8, 1)
self.boss_height_input = QLineEdit("", edit_box)
self.boss_height_input.setVisible(False)
help_tip(self.boss_height_input, "圆柱凸台的目标高度。当前选中对象表会使用这个隐藏输入执行高度修改。")
self.boss_center_x_input = QLineEdit("", edit_box)
self.boss_center_x_input.setVisible(False)
self.boss_center_y_input = QLineEdit("", edit_box)
self.boss_center_y_input.setVisible(False)
self.boss_center_z_input = QLineEdit("", edit_box)
self.boss_center_z_input.setVisible(False)
help_tip(self.boss_center_x_input, "圆柱凸台目标轴心坐标 X。当前选中对象表会使用这个隐藏输入执行轴心移动。")
help_tip(self.boss_center_y_input, "圆柱凸台目标轴心坐标 Y。当前选中对象表会使用这个隐藏输入执行轴心移动。")
help_tip(self.boss_center_z_input, "圆柱凸台目标轴心坐标 Z。当前选中对象表会使用这个隐藏输入执行轴心移动。")
self.cone_reference_radius_input = QLineEdit("", edit_box)
self.cone_reference_radius_input.setVisible(False)
help_tip(self.cone_reference_radius_input, "圆锥面的目标参考半径。当前选中对象表会使用这个隐藏输入执行圆锥面修改。")
self.sphere_radius_input = QLineEdit("", edit_box)
self.sphere_radius_input.setVisible(False)
help_tip(self.sphere_radius_input, "球面的目标半径。当前选中对象表会使用这个隐藏输入执行球面修改。")
self.torus_radius_input = QLineEdit("", edit_box)
self.torus_radius_input.setVisible(False)
help_tip(self.torus_radius_input, "环面的目标半径。当前选中对象表会使用这个隐藏输入执行环面修改。")
self.face_area_input = QLineEdit("", edit_box)
self.face_area_input.setVisible(False)
help_tip(self.face_area_input, "目标面面积。当前选中对象表会使用这个隐藏输入执行所属特征或 Solid 的均匀缩放。")
self.face_width_input = QLineEdit("", edit_box)
self.face_width_input.setVisible(False)
help_tip(self.face_width_input, "目标U向尺寸。当前选中对象表会使用这个隐藏输入修改 Face 平面内第一个方向的尺寸。")
self.face_height_input = QLineEdit("", edit_box)
self.face_height_input.setVisible(False)
help_tip(self.face_height_input, "目标V向尺寸。当前选中对象表会使用这个隐藏输入修改 Face 平面内第二个方向的尺寸。")
self.resize_boss_button = QPushButton("调整圆柱凸台直径")
help_tip(self.resize_boss_button, "修改完整圆柱凸台直径。变大会加料,变小会重建凸台区域。")
self.resize_boss_button.clicked.connect(self.resize_boss)
edit_layout.addWidget(self.resize_boss_button, 9, 0, 1, 2)
self.suppress_button = QPushButton("封堵圆柱孔")
help_tip(self.suppress_button, "用补料体填住完整圆柱孔。适合通孔/盲孔,不适合半孔或槽。")
self.suppress_button.clicked.connect(self.suppress_hole)
edit_layout.addWidget(self.suppress_button, 10, 0, 1, 2)
edit_layout.addWidget(QLabel("孔深度"), 11, 0)
depth_inputs = QWidget()
depth_layout = QHBoxLayout(depth_inputs)
depth_layout.setContentsMargins(0, 0, 0, 0)
self.hole_depth_input = QLineEdit("")
help_tip(self.hole_depth_input, "盲孔或盲槽的目标深度。选中候选后会填入参考值,深度来自几何估算。")
self.hole_bottom_face_input = QLineEdit("")
self.hole_bottom_face_input.setPlaceholderText("底面 Face ID")
help_tip(self.hole_bottom_face_input, "自动识别孔底不稳定时,可手动输入底面 Face ID,让孔深计算有明确底面。")
self.hole_bottom_face_input.textChanged.connect(lambda _text: self._update_action_states())
depth_layout.addWidget(self.hole_depth_input, stretch=2)
depth_layout.addWidget(self.hole_bottom_face_input, stretch=1)
edit_layout.addWidget(depth_inputs, 11, 1)
self.resize_depth_button = QPushButton("调整盲孔/盲槽深度")
help_tip(self.resize_depth_button, "加深或变浅盲孔/盲槽。需要识别到底面,或手动填写底面 Face ID。")
self.resize_depth_button.clicked.connect(self.resize_hole_depth)
edit_layout.addWidget(self.resize_depth_button, 12, 0, 1, 2)
edit_layout.addWidget(QLabel("圆角半径"), 13, 0)
self.edge_fillet_radius_input = QLineEdit("")
help_tip(self.edge_fillet_radius_input, "新圆角或已有圆角的目标半径。选中Edge/圆角候选后会自动填参考值。")
edit_layout.addWidget(self.edge_fillet_radius_input, 13, 1)
self.fillet_edge_button = QPushButton("给Edge添加圆角")
help_tip(self.fillet_edge_button, "给当前直线Edge新增圆角。不是修改已有圆角;已有圆角请用下面那个按钮。")
self.fillet_edge_button.clicked.connect(self.fillet_edge)
edit_layout.addWidget(self.fillet_edge_button, 14, 0, 1, 2)
self.resize_existing_fillet_button = QPushButton("修改已有圆角半径")
help_tip(self.resize_existing_fillet_button, "尝试修改已识别圆角面的半径。会先移除原圆角再重建,复杂圆角可能失败并回滚。")
self.resize_existing_fillet_button.clicked.connect(self.resize_existing_fillet)
edit_layout.addWidget(self.resize_existing_fillet_button, 15, 0, 1, 2)
edit_layout.addWidget(QLabel("倒角距离"), 16, 0)
self.edge_chamfer_distance_input = QLineEdit("")
help_tip(self.edge_chamfer_distance_input, "给Edge添加倒角时使用的距离。选中直线Edge后会填一个较小参考值。")
self.edge_chamfer_distance1_input = QLineEdit("", edit_box)
self.edge_chamfer_distance2_input = QLineEdit("", edit_box)
self.edge_chamfer_angle_distance_input = QLineEdit("", edit_box)
self.edge_chamfer_angle_degrees_input = QLineEdit("", edit_box)
self.edge_chamfer_reference_face_input = QLineEdit("", edit_box)
self.edge_chamfer_distance1_input.setVisible(False)
self.edge_chamfer_distance2_input.setVisible(False)
self.edge_chamfer_angle_distance_input.setVisible(False)
self.edge_chamfer_angle_degrees_input.setVisible(False)
self.edge_chamfer_reference_face_input.setVisible(False)
edit_layout.addWidget(self.edge_chamfer_distance_input, 16, 1)
self.chamfer_edge_button = QPushButton("给Edge添加倒角")
help_tip(self.chamfer_edge_button, "给当前直线Edge新增对称倒角。会先预览,再后台执行,失败会回滚。")
self.chamfer_edge_button.clicked.connect(self.chamfer_edge)
edit_layout.addWidget(self.chamfer_edge_button, 17, 0, 1, 2)
edit_layout.addWidget(QLabel("Edge长度"), 18, 0)
edge_length_inputs = QWidget()
edge_length_layout = QHBoxLayout(edge_length_inputs)
edge_length_layout.setContentsMargins(0, 0, 0, 0)
self.edge_target_length_input = QLineEdit("")
help_tip(self.edge_target_length_input, "当前Edge的目标长度。选中Edge后会自动填当前长度,改成新长度后,还要确认右侧建模意图。")
self.edge_start_x_input = QLineEdit("", edit_box)
self.edge_start_y_input = QLineEdit("", edit_box)
self.edge_start_z_input = QLineEdit("", edit_box)
self.edge_center_x_input = QLineEdit("", edit_box)
self.edge_center_y_input = QLineEdit("", edit_box)
self.edge_center_z_input = QLineEdit("", edit_box)
self.edge_end_x_input = QLineEdit("", edit_box)
self.edge_end_y_input = QLineEdit("", edit_box)
self.edge_end_z_input = QLineEdit("", edit_box)
self.ellipse_edge_major_radius_input = QLineEdit("", edit_box)
self.ellipse_edge_minor_radius_input = QLineEdit("", edit_box)
for endpoint_input in (
self.edge_start_x_input,
self.edge_start_y_input,
self.edge_start_z_input,
self.edge_center_x_input,
self.edge_center_y_input,
self.edge_center_z_input,
self.edge_end_x_input,
self.edge_end_y_input,
self.edge_end_z_input,
self.ellipse_edge_major_radius_input,
self.ellipse_edge_minor_radius_input,
):
endpoint_input.setVisible(False)
help_tip(self.ellipse_edge_major_radius_input, "椭圆Edge目标主半径。当前选中对象表会使用这个隐藏输入执行主轴单轴缩放。")
help_tip(self.ellipse_edge_minor_radius_input, "椭圆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")
self.edge_length_anchor_combo.addItem("固定终点", "keep-end")
self.edge_length_anchor_combo.setCurrentIndex(0)
self.edge_length_anchor_combo.setMinimumWidth(90)
help_tip(
self.edge_length_anchor_combo,
"选择Edge长度修改的固定基准:自动默认固定起点并移动终点;中心会两端各动一半;固定起点/终点会明确控制局部形变端点。",
)
self.edge_length_strategy_combo = NoWheelComboBox()
self.edge_length_strategy_combo.addItem("自动选择", "auto")
self.edge_length_strategy_combo.addItem("只改当前Edge", "local-edge-only-deform")
self.edge_length_strategy_combo.addItem("移动端面/保持垂直", "move-edge-end-plane-by-push-pull")
self.edge_length_strategy_combo.addItem("相邻圆柱直径", "resize-adjacent-cylinder-from-circular-edge-length")
self.edge_length_strategy_combo.addItem("缩放所属", "scale-owning-shape-from-edge")
self.edge_length_strategy_combo.setCurrentIndex(0)
self.edge_length_strategy_combo.setMinimumWidth(130)
help_tip(
self.edge_length_strategy_combo,
"选择Edge长度修改的建模意图:自动会按可用性选择;只改当前Edge会让相邻面自然变斜;移动端面/保持垂直会让相关端面和同向边跟随;相邻圆柱直径会把圆Edge长度换算成孔/槽/凸台直径;缩放所属会让所属对象整体跟随变化。",
)
edge_length_layout.addWidget(self.edge_target_length_input, stretch=2)
edge_length_layout.addWidget(self.edge_length_anchor_combo, stretch=1)
edge_length_layout.addWidget(self.edge_length_strategy_combo, stretch=1)
edit_layout.addWidget(edge_length_inputs, 18, 1)
self.resize_edge_length_button = QPushButton("修改Edge长度")
help_tip(
self.resize_edge_length_button,
"按已选择的建模意图修改当前Edge长度。直线Edge可只改当前边或移动端面;圆Edge可转成相邻圆柱直径;缩放所属会影响其它尺寸。",
)
self.resize_edge_length_button.clicked.connect(self.resize_any_edge_length)
edit_layout.addWidget(self.resize_edge_length_button, 19, 0, 1, 2)
edit_layout.addWidget(QLabel("壳体厚度"), 20, 0)
self.shell_thickness_input = QLineEdit("")
help_tip(
self.shell_thickness_input,
"壳体局部区域的目标厚度。选中有相对平面的平面候选后会自动填一个参考值。",
)
edit_layout.addWidget(self.shell_thickness_input, 20, 1)
self.resize_shell_thickness_button = QPushButton("调整壳体厚度")
help_tip(
self.resize_shell_thickness_button,
"移动当前平面区域来达到目标厚度。当前版本基于相对平面估算,执行前会预览并可回滚。",
)
self.resize_shell_thickness_button.clicked.connect(self.resize_shell_thickness)
edit_layout.addWidget(self.resize_shell_thickness_button, 21, 0, 1, 2)
edit_layout.addWidget(QLabel("平移 X/Y/Z"), 22, 0)
translate_inputs = QWidget()
translate_layout = QHBoxLayout(translate_inputs)
translate_layout.setContentsMargins(0, 0, 0, 0)
self.translate_x_input = QLineEdit("0")
self.translate_y_input = QLineEdit("0")
self.translate_z_input = QLineEdit("0")
self.translate_x_input.setPlaceholderText("X")
self.translate_y_input.setPlaceholderText("Y")
self.translate_z_input.setPlaceholderText("Z")
help_tip(self.translate_x_input, "沿 X 方向平移的距离。输入 0 表示 X 方向不移动。")
help_tip(self.translate_y_input, "沿 Y 方向平移的距离。输入 0 表示 Y 方向不移动。")
help_tip(self.translate_z_input, "沿 Z 方向平移的距离。输入 0 表示 Z 方向不移动。")
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, 22, 1)
self.translate_part_button = QPushButton("平移选中零件")
help_tip(self.translate_part_button, "按上面的 X/Y/Z 距离移动当前零件。会写入历史,可撤销。")
self.translate_part_button.clicked.connect(self.translate_selected_part)
self.translate_solid_button = QPushButton("平移选中Solid")
help_tip(self.translate_solid_button, "按上面的 X/Y/Z 距离移动当前Solid。单Solid零件中相当于移动整个零件。")
self.translate_solid_button.clicked.connect(self.translate_selected_solid)
edit_layout.addWidget(self.translate_part_button, 23, 0, 1, 2)
edit_layout.addWidget(self.translate_solid_button, 24, 0, 1, 2)
edit_layout.addWidget(QLabel("旋转轴/角度"), 25, 0)
rotate_inputs = QWidget()
rotate_layout = QHBoxLayout(rotate_inputs)
rotate_layout.setContentsMargins(0, 0, 0, 0)
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, "选择旋转轴。对象会绕自身包围盒中心旋转。")
self.rotate_angle_input = QLineEdit("90")
self.rotate_angle_input.setPlaceholderText("度")
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, 25, 1)
self.scale_target_diagonal_input = QLineEdit("", edit_box)
self.scale_target_diagonal_input.setVisible(False)
help_tip(self.scale_target_diagonal_input, "零件或Solid的目标包围盒对角线。当前选中对象表会使用这个隐藏输入执行整体等比缩放。")
self.scale_x_size_input = QLineEdit("", edit_box)
self.scale_x_size_input.setVisible(False)
help_tip(self.scale_x_size_input, "零件或Solid的目标 X 向包围盒尺寸。")
self.scale_y_size_input = QLineEdit("", edit_box)
self.scale_y_size_input.setVisible(False)
help_tip(self.scale_y_size_input, "零件或Solid的目标 Y 向包围盒尺寸。")
self.scale_z_size_input = QLineEdit("", edit_box)
self.scale_z_size_input.setVisible(False)
help_tip(self.scale_z_size_input, "零件或Solid的目标 Z 向包围盒尺寸。")
self.rotate_part_button = QPushButton("旋转选中零件")
help_tip(self.rotate_part_button, "绕所选轴旋转当前零件。会写入历史,可撤销。")
self.rotate_part_button.clicked.connect(self.rotate_selected_part)
self.rotate_solid_button = QPushButton("旋转选中Solid")
help_tip(self.rotate_solid_button, "绕所选轴旋转当前Solid。单Solid零件中相当于旋转整个零件。")
self.rotate_solid_button.clicked.connect(self.rotate_selected_solid)
edit_layout.addWidget(self.rotate_part_button, 26, 0, 1, 2)
edit_layout.addWidget(self.rotate_solid_button, 27, 0, 1, 2)
panel_layout.addWidget(self.object_edit_box)
if ENABLE_EXPORT_PANEL:
panel_layout.addWidget(export_box)
if ENABLE_VIEW_PANEL:
panel_layout.addWidget(view_box)
if ENABLE_MEASURE_PANEL:
panel_layout.addWidget(measure_box)
if ENABLE_EDITABLE_OBJECTS_PANEL:
editable_box = QGroupBox("可编辑对象")
editable_box.setObjectName("editableSection")
help_tip(editable_box, "自动找出当前版本可以尝试编辑的Face或Edge,方便不用手动到处点。")
editable_layout = QVBoxLayout(editable_box)
editable_button_row = QHBoxLayout()
self.editable_refresh_button = QPushButton("扫描对象")
help_tip(self.editable_refresh_button, "快速扫描一批可以尝试编辑的对象,并列在下面表格里。")
self.editable_refresh_button.clicked.connect(
lambda _checked=False: self.refresh_editable_candidates(show_info=True)
)
self.editable_deep_scan_button = QPushButton("深度扫描")
help_tip(self.editable_deep_scan_button, "扫描更多候选对象,结果更全但更慢。默认列表找不到目标时再用。")
self.editable_deep_scan_button.clicked.connect(
lambda _checked=False: self.refresh_editable_candidates(show_info=True, deep_scan=True)
)
editable_button_row.addWidget(self.editable_refresh_button)
editable_button_row.addWidget(self.editable_deep_scan_button)
editable_layout.addLayout(editable_button_row)
self.editable_table = QTableWidget(0, 8)
self.editable_table.setHorizontalHeaderLabels(
["操作", "ID", "对象", "当前值", "状态", "风险", "置信度", "说明"]
)
self.editable_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.editable_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.editable_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.editable_table.setMinimumHeight(130)
self.editable_table.setAlternatingRowColors(True)
help_tip(self.editable_table, "这里列出可尝试编辑的对象。点击一行会选中模型里的对应Face或Edge,并自动填入相关输入框。")
self.editable_table.cellClicked.connect(self.on_editable_row_clicked)
editable_layout.addWidget(self.editable_table)
panel_layout.addWidget(editable_box)
if ENABLE_CYLINDER_CANDIDATES_PANEL:
candidate_box = QGroupBox("圆柱面候选")
candidate_box.setObjectName("candidateSection")
help_tip(candidate_box, "列出圆柱面候选,并粗略判断它们像孔、槽、圆角、凸台还是普通圆柱。")
candidate_layout = QVBoxLayout(candidate_box)
filter_layout = QHBoxLayout()
self.cylinders_button = QPushButton("扫描圆柱面")
help_tip(self.cylinders_button, "扫描模型里的圆柱面候选,并粗略判断它们像孔、槽、圆角、凸台还是普通圆柱。")
self.cylinders_button.clicked.connect(self.list_cylinders)
filter_layout.addWidget(self.cylinders_button)
filter_layout.addWidget(QLabel("类型"))
self.candidate_filter_combo = NoWheelComboBox()
self.candidate_filter_combo.addItems(
[
"All",
"Hole/Groove",
"Round/Fillet",
"Boss/Outer",
"Unclear",
]
)
help_tip(
self.candidate_filter_combo,
"筛选下面的圆柱面候选:孔/槽、圆角、凸台/外圆,或暂时无法明确分类的圆柱面。",
)
self.candidate_filter_combo.currentTextChanged.connect(
lambda _text: self._filter_cached_cylinder_candidates(show_info=True)
)
filter_layout.addWidget(self.candidate_filter_combo)
candidate_layout.addLayout(filter_layout)
self.cylinder_table = QTableWidget(0, 8)
self.cylinder_table.setHorizontalHeaderLabels(
["Face", "Guess", "Diameter", "Span", "Height", "Confidence", "Risk", "零件"]
)
self.cylinder_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.cylinder_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.cylinder_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.cylinder_table.setMinimumHeight(140)
self.cylinder_table.setAlternatingRowColors(True)
help_tip(self.cylinder_table, "这里显示圆柱面候选。点击一行会选中对应Face,并在属性区显示可用操作和风险。")
self.cylinder_table.cellClicked.connect(self.on_cylinder_row_clicked)
candidate_layout.addWidget(self.cylinder_table)
panel_layout.addWidget(candidate_box)
if ENABLE_HISTORY_PANEL:
history_box = QGroupBox("操作历史")
history_box.setObjectName("historySection")
help_tip(history_box, "查看已经成功执行的编辑、差异预览,并导出报告或 JSON 历史。")
history_layout = QVBoxLayout(history_box)
self.history_list = QListWidget()
self.history_list.setMinimumHeight(110)
help_tip(self.history_list, "成功执行过的编辑会出现在这里。点击一条记录可查看参数、差异预览和目标位置。")
self.history_list.currentRowChanged.connect(self.on_history_row_changed)
self.history_list.currentRowChanged.connect(lambda _row: self._update_action_states())
history_buttons = QHBoxLayout()
self.undo_button = QPushButton("撤销")
help_tip(self.undo_button, "撤销上一次成功编辑,把模型恢复到编辑前快照。")
self.undo_button.clicked.connect(self.undo_edit)
self.redo_button = QPushButton("重做")
help_tip(self.redo_button, "重做刚刚撤销的编辑。")
self.redo_button.clicked.connect(self.redo_edit)
self.clear_diff_button = QPushButton("清除差异预览")
help_tip(self.clear_diff_button, "关闭历史记录产生的红/绿差异叠加和热力图显示。不会修改模型。")
self.clear_diff_button.clicked.connect(lambda _checked=False: self.clear_diff_preview())
self.export_diff_button = QPushButton("导出差异报告")
help_tip(self.export_diff_button, "把当前选中的历史记录导出成文本报告,包含参数、拓扑变化和几何差异统计。")
self.export_diff_button.clicked.connect(self.export_diff_report)
self.export_history_button = QPushButton("导出编辑历史")
help_tip(self.export_history_button, "把本次会话里的所有编辑记录导出为 JSON,方便留档或后续复盘。")
self.export_history_button.clicked.connect(self.export_operation_history)
history_layout.addWidget(self.history_list)
history_buttons.addWidget(self.undo_button)
history_buttons.addWidget(self.redo_button)
history_buttons.addWidget(self.clear_diff_button)
history_buttons.addWidget(self.export_diff_button)
history_buttons.addWidget(self.export_history_button)
history_layout.addLayout(history_buttons)
panel_layout.addWidget(history_box)
panel_layout.addStretch(1)
info_box = QGroupBox("诊断信息(高级)")
info_box.setObjectName("infoSection")
info_box.setCheckable(True)
info_box.setChecked(False)
help_tip(info_box, "展开查看面积、中心、Face ID、拓扑关系和识别依据;这些内容不属于主特征尺寸。")
info_layout = QVBoxLayout(info_box)
self.diagnostic_info_content = QWidget()
diagnostic_info_layout = QVBoxLayout(self.diagnostic_info_content)
diagnostic_info_layout.setContentsMargins(0, 0, 0, 0)
info_buttons = QHBoxLayout()
self.copy_id_button = QPushButton("复制 ID")
help_tip(self.copy_id_button, "复制当前选中对象的 ID。Face/特征会优先复制逻辑 Face ID。")
self.copy_id_button.clicked.connect(self.copy_selected_id)
self.copy_pick_button = QPushButton("复制坐标")
help_tip(self.copy_pick_button, "复制最近一次鼠标点到模型上的三维坐标。")
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)
diagnostic_info_layout.addLayout(info_buttons)
self.info_tabs = QTabWidget()
self.info_tree = QTreeWidget()
self.info_tree.setHeaderLabels(["属性", "值"])
self.info_tree.setAlternatingRowColors(True)
self.info_tree.setTextElideMode(Qt.TextElideMode.ElideMiddle)
self.info_tree.setUniformRowHeights(True)
help_tip(self.info_tree, "当前选中对象的结构化属性。悬停单元格可以看到完整字段和值。")
self.info_text = QPlainTextEdit()
self.info_text.setReadOnly(True)
help_tip(self.info_text, "当前选中对象信息的原始文本版本,适合复制或排查问题。")
self.info_tabs.addTab(self.info_tree, "属性表")
self.info_tabs.addTab(self.info_text, "原始文本")
help_tip(self.info_tabs, "在表格视图和原始文本视图之间切换当前选中对象信息。")
diagnostic_info_layout.addWidget(self.info_tabs)
info_layout.addWidget(self.diagnostic_info_content)
info_box.toggled.connect(self._on_diagnostic_info_toggled)
self.diagnostic_info_content.setVisible(False)
panel_layout.addWidget(info_box)
self._update_action_states()
self.vtk_widget = QVTKRenderWindowInteractor(central)
self.vtk_widget.setMouseTracking(True)
self.vtk_widget.installEventFilter(self)
root_layout.addWidget(self.vtk_widget, stretch=1)
self.statusBar().showMessage("请选择 STEP 文件,或点击“读取模型”加载到可视化区域。")
def _set_help_tip(self, widget, text: str) -> None:
widget.setToolTip(text)
widget.setStatusTip(text)
try:
widget.setToolTipDuration(14000)
except AttributeError:
pass
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 DEFAULT_MODEL_PATH
return path, smoke_test
def main() -> int:
worker_request = _isolated_edit_worker_request(sys.argv)
if worker_request is not None:
from .isolated_edit_worker import run_request
return run_request(worker_request)
if _crash_log_requested(sys.argv):
_enable_crash_log()
path, smoke_test = _parse_args(sys.argv)
_set_windows_taskbar_identity()
app = QApplication(sys.argv)
app.setApplicationName("几何参数化")
app.setApplicationDisplayName("几何参数化")
app.setOrganizationName("GeometryParametric")
app.setWindowIcon(_application_icon())
window = StepEditorWindow(path, background_load=not smoke_test)
if smoke_test:
print("smoke test ok")
window.close()
app.quit()
return 0
window.show()
window._ensure_vtk_interactor_started()
return app.exec()
if __name__ == "__main__":
raise SystemExit(main())