1916 lines
95 KiB
Python
1916 lines
95 KiB
Python
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
import faulthandler
|
||
import math
|
||
import os
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import vtk
|
||
import vtkmodules.vtkInteractionWidgets # noqa: F401
|
||
import vtkmodules.vtkInteractionStyle # noqa: F401
|
||
import vtkmodules.vtkRenderingFreeType # noqa: F401
|
||
import vtkmodules.vtkRenderingOpenGL2 # noqa: F401
|
||
from PySide6.QtCore import Qt, QThread, QTimer, Signal, Slot, QStringListModel
|
||
from PySide6.QtGui import QIcon
|
||
from PySide6.QtWidgets import (
|
||
QAbstractItemView,
|
||
QApplication,
|
||
QCheckBox,
|
||
QComboBox,
|
||
QCompleter,
|
||
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 PROPERTY_TABLE_HEADERS, WindowStateMixin
|
||
|
||
|
||
_CRASH_LOG_HANDLE = None
|
||
_VTK_OUTPUT_WINDOW = 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 _suppress_vtk_output_window() -> None:
|
||
"""Keep VTK warnings from opening transient native output windows."""
|
||
global _VTK_OUTPUT_WINDOW
|
||
try:
|
||
vtk.vtkObject.GlobalWarningDisplayOff()
|
||
output_window = vtk.vtkStringOutputWindow()
|
||
vtk.vtkOutputWindow.SetInstance(output_window)
|
||
_VTK_OUTPUT_WINDOW = output_window
|
||
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 | None = None, *, background_load: bool = False):
|
||
_suppress_vtk_output_window()
|
||
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) if step_path else None
|
||
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.multi_selected_feature_face_ids: list[int] = []
|
||
self.multi_selected_hole_entries: list[dict[str, object]] = []
|
||
self.multi_selection_active = False
|
||
|
||
self.model_actor = None
|
||
self.edge_actor = None
|
||
self.scene_axes_renderer = None
|
||
self.orientation_marker_widget = None
|
||
self.orientation_marker_prop = None
|
||
self.step_coordinate_axes_actor = None
|
||
self.hide_edges_during_camera_interaction = False
|
||
self.edge_visibility_before_camera_interaction: int | None = None
|
||
self.prefer_fxaa_antialiasing = True
|
||
self.fallback_multi_samples = 2
|
||
self.interactive_multi_samples = 0
|
||
self.still_multi_samples = 0
|
||
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.camera_interaction_visual_changed = 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_press_camera_state: tuple[float, ...] | None = None
|
||
self.left_button_press_target: dict[str, object] | None = None
|
||
self.left_button_dragged = False
|
||
self.left_click_drag_threshold_px = 6
|
||
self.left_click_camera_tolerance = 1e-7
|
||
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.asitus_thread: QThread | None = None
|
||
self.asitus_worker: ScanWorker | None = None
|
||
self.pending_asitus_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.auto_load_on_show = bool(background_load)
|
||
self.vtk_interactor_started = False
|
||
self.first_show_handled = False
|
||
self.preview_load_deflection = 0.35
|
||
self.initial_load_deflection = 0.35
|
||
self.edit_result_deflection = 0.35
|
||
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 = 5
|
||
self.property_table_min_visible_rows = 5
|
||
self.property_editor_selected_row: int | None = None
|
||
self.property_command_active_key = ""
|
||
self.property_command_buttons: dict[str, QPushButton] = {}
|
||
self.relation_formula_items: list[dict[str, object]] = []
|
||
self.relation_formula_next_id = 1
|
||
|
||
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;
|
||
}
|
||
QGroupBox#capabilitySection {
|
||
background: #f0fdf4;
|
||
border: 1px solid #bbf7d0;
|
||
border-left: 5px solid #16a34a;
|
||
}
|
||
QGroupBox#capabilitySection::title {
|
||
background: #dcfce7;
|
||
border-color: #bbf7d0;
|
||
color: #14532d;
|
||
}
|
||
QLabel#capabilityHeadline {
|
||
color: #14532d;
|
||
font-weight: 800;
|
||
}
|
||
QLabel#capabilityDetail {
|
||
color: #166534;
|
||
font-size: 11px;
|
||
}
|
||
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#parametricModelButton {
|
||
background: #1f6feb;
|
||
border: 1px solid #1d4ed8;
|
||
border-bottom-color: #1e40af;
|
||
border-radius: 6px;
|
||
color: #ffffff;
|
||
font-weight: 800;
|
||
min-height: 30px;
|
||
padding: 6px 12px;
|
||
}
|
||
QPushButton#parametricModelButton:hover {
|
||
background: #2563eb;
|
||
border-color: #1e40af;
|
||
}
|
||
QPushButton#parametricModelButton:pressed {
|
||
background: #1e40af;
|
||
border-color: #1e3a8a;
|
||
padding-top: 7px;
|
||
padding-bottom: 5px;
|
||
}
|
||
QPushButton#parametricModelButton:disabled {
|
||
background: #dbe4ef;
|
||
border: 1px dashed #adb9c8;
|
||
color: #8b97a7;
|
||
font-weight: 700;
|
||
}
|
||
QPushButton#quickExportStepButton {
|
||
background: #f8fafc;
|
||
border: 1px solid #64748b;
|
||
border-bottom-color: #475569;
|
||
border-radius: 6px;
|
||
color: #1f2937;
|
||
font-weight: 700;
|
||
min-height: 30px;
|
||
padding: 6px 11px;
|
||
}
|
||
QPushButton#quickExportStepButton:hover {
|
||
background: #eef6ff;
|
||
border-color: #2563eb;
|
||
color: #1e3a8a;
|
||
}
|
||
QPushButton#quickExportStepButton:pressed {
|
||
background: #dbeafe;
|
||
border-color: #1d4ed8;
|
||
padding-top: 7px;
|
||
padding-bottom: 5px;
|
||
}
|
||
QPushButton#quickExportStepButton:disabled {
|
||
background: #eef2f6;
|
||
border: 1px dashed #bcc7d4;
|
||
color: #8f99a8;
|
||
font-weight: 650;
|
||
}
|
||
QPushButton#exportParametersButton {
|
||
background: #f0fdfa;
|
||
border: 1px solid #0f766e;
|
||
border-bottom-color: #115e59;
|
||
border-radius: 6px;
|
||
color: #134e4a;
|
||
font-weight: 750;
|
||
min-height: 30px;
|
||
padding: 6px 11px;
|
||
}
|
||
QPushButton#exportParametersButton:hover {
|
||
background: #ccfbf1;
|
||
border-color: #0d9488;
|
||
}
|
||
QPushButton#exportParametersButton:pressed {
|
||
background: #99f6e4;
|
||
border-color: #0f766e;
|
||
padding-top: 7px;
|
||
padding-bottom: 5px;
|
||
}
|
||
QPushButton#exportParametersButton:disabled {
|
||
background: #eef2f6;
|
||
border: 1px dashed #bcc7d4;
|
||
color: #8f99a8;
|
||
font-weight: 650;
|
||
}
|
||
QPushButton#propertyRowEditButton {
|
||
background: #ea580c;
|
||
border: 1px solid #c2410c;
|
||
border-radius: 5px;
|
||
color: #ffffff;
|
||
font-weight: 700;
|
||
min-height: 26px;
|
||
padding: 4px 8px;
|
||
}
|
||
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;
|
||
}
|
||
QLabel#propertyCommandSummary {
|
||
color: #334155;
|
||
font-size: 11px;
|
||
padding: 0 2px 1px 2px;
|
||
}
|
||
QLabel#propertyCommandHelp {
|
||
background: #fff7ed;
|
||
border: 1px solid #fed7aa;
|
||
border-radius: 6px;
|
||
color: #7c2d12;
|
||
font-size: 11px;
|
||
padding: 5px 7px;
|
||
}
|
||
QFrame#propertyCommandBar {
|
||
background: #eef2f7;
|
||
border: 1px solid #cbd5e1;
|
||
border-radius: 6px;
|
||
}
|
||
QPushButton#propertyCommandButton {
|
||
background: transparent;
|
||
border: 0;
|
||
border-radius: 4px;
|
||
color: #334155;
|
||
font-weight: 700;
|
||
min-height: 34px;
|
||
padding: 3px 6px;
|
||
}
|
||
QPushButton#propertyCommandButton:hover {
|
||
background: #e2e8f0;
|
||
}
|
||
QPushButton#propertyCommandButton[active="true"] {
|
||
background: #ffffff;
|
||
color: #0f172a;
|
||
border: 1px solid #f59e0b;
|
||
}
|
||
QScrollArea#propertyCardScroll {
|
||
background: transparent;
|
||
border: 0;
|
||
}
|
||
QWidget#propertyCardContainer {
|
||
background: transparent;
|
||
}
|
||
QFrame#propertyCard {
|
||
background: #ffffff;
|
||
border: 1px solid #d8e0eb;
|
||
border-radius: 6px;
|
||
}
|
||
QFrame#propertyCard[compact="true"] {
|
||
background: #ffffff;
|
||
}
|
||
QFrame#propertyCard[editable="true"] {
|
||
background: #fffaf2;
|
||
border-color: #f4c58a;
|
||
}
|
||
QFrame#propertyCard[selected="true"] {
|
||
background: #fffaf2;
|
||
border-color: #f59e0b;
|
||
}
|
||
QFrame#propertyCard[changed="true"] {
|
||
background: #f0fdf4;
|
||
border-color: #16a34a;
|
||
}
|
||
QFrame#propertyCard[invalid="true"] {
|
||
background: #fff1f2;
|
||
border-color: #ef4444;
|
||
}
|
||
QFrame#propertyCard[pinTop="true"] {
|
||
background: #eef6ff;
|
||
border-color: #b7d3f5;
|
||
}
|
||
QLabel#propertyCardTitle {
|
||
color: #172033;
|
||
font-weight: 700;
|
||
}
|
||
QLabel#propertyCardMetaLabel {
|
||
color: #64748b;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
}
|
||
QLabel#propertyCardValue {
|
||
color: #172033;
|
||
}
|
||
QLabel#propertyCardCompactValue {
|
||
color: #172033;
|
||
font-weight: 600;
|
||
}
|
||
QLabel#propertyCardHint {
|
||
color: #64748b;
|
||
font-size: 11px;
|
||
}
|
||
QLabel#propertyCardCompactMeta {
|
||
color: #475569;
|
||
font-size: 11px;
|
||
}
|
||
QLineEdit#propertyCardTargetEditor {
|
||
background: #ffffff;
|
||
border: 1px solid #c8d2df;
|
||
border-radius: 4px;
|
||
padding: 3px 5px;
|
||
}
|
||
QLineEdit#propertyCardTargetEditor:focus {
|
||
border-color: #2563eb;
|
||
}
|
||
QGroupBox#relationFormulaBox {
|
||
margin-top: 2px;
|
||
}
|
||
QLineEdit#relationFormulaInput {
|
||
background: #ffffff;
|
||
border: 1px solid #b7c6d9;
|
||
border-radius: 5px;
|
||
color: #172033;
|
||
min-height: 24px;
|
||
padding: 3px 6px;
|
||
}
|
||
QLineEdit#relationFormulaInput:focus {
|
||
border: 1px solid #2563eb;
|
||
}
|
||
QPushButton#relationFormulaAddButton,
|
||
QPushButton#relationFormulaRemoveButton {
|
||
background: #f8fafc;
|
||
border: 1px solid #94a3b8;
|
||
border-radius: 5px;
|
||
color: #1f2937;
|
||
font-weight: 700;
|
||
min-height: 26px;
|
||
padding: 3px 8px;
|
||
}
|
||
QPushButton#relationFormulaAddButton:hover,
|
||
QPushButton#relationFormulaRemoveButton:hover {
|
||
background: #eef6ff;
|
||
border-color: #2563eb;
|
||
color: #1e3a8a;
|
||
}
|
||
QPushButton#relationFormulaAddButton:disabled,
|
||
QPushButton#relationFormulaRemoveButton:disabled {
|
||
background: #eef2f6;
|
||
border: 1px dashed #bcc7d4;
|
||
color: #8f99a8;
|
||
}
|
||
QListWidget#relationFormulaList {
|
||
background: #ffffff;
|
||
border: 1px solid #d8e0eb;
|
||
border-radius: 5px;
|
||
}
|
||
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) if self.step_path is not None else "")
|
||
self.path_label.setObjectName("stepPathDisplay")
|
||
self.path_label.setReadOnly(True)
|
||
self.path_label.setMinimumWidth(120)
|
||
help_tip(self.path_label, "当前 STEP 文件的完整路径。可以选中文字复制路径。")
|
||
self.path_label.setPlaceholderText("未选择 STEP 文件")
|
||
self.path_label.setToolTip(str(self.step_path) if self.step_path is not None else "未选择 STEP 文件")
|
||
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("导出模型")
|
||
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, len(PROPERTY_TABLE_HEADERS), self.object_edit_box)
|
||
self.property_table.setObjectName("propertyTable")
|
||
self.property_table.setHorizontalHeaderLabels(list(PROPERTY_TABLE_HEADERS))
|
||
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.Fixed)
|
||
self.property_table.setColumnWidth(0, 112)
|
||
self.property_table.setColumnWidth(1, 104)
|
||
self.property_table.setColumnWidth(2, 88)
|
||
self.property_table.setColumnWidth(3, 82)
|
||
self.property_table.setColumnWidth(4, 66)
|
||
self.property_table.installEventFilter(self)
|
||
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_command_summary_label = QLabel("未选择可编辑对象")
|
||
self.property_command_summary_label.setObjectName("propertyCommandSummary")
|
||
self.property_command_summary_label.setWordWrap(True)
|
||
self.property_command_summary_label.setVisible(False)
|
||
object_edit_layout.addWidget(self.property_command_summary_label)
|
||
self.property_command_bar = QFrame()
|
||
self.property_command_bar.setObjectName("propertyCommandBar")
|
||
self.property_command_bar.setVisible(False)
|
||
self.property_command_layout = QHBoxLayout(self.property_command_bar)
|
||
self.property_command_layout.setContentsMargins(3, 3, 3, 3)
|
||
self.property_command_layout.setSpacing(2)
|
||
object_edit_layout.addWidget(self.property_command_bar)
|
||
self.property_command_help_label = QLabel("")
|
||
self.property_command_help_label.setObjectName("propertyCommandHelp")
|
||
self.property_command_help_label.setWordWrap(True)
|
||
self.property_command_help_label.setVisible(False)
|
||
object_edit_layout.addWidget(self.property_command_help_label)
|
||
self.property_card_scroll = QScrollArea()
|
||
self.property_card_scroll.setObjectName("propertyCardScroll")
|
||
self.property_card_scroll.setWidgetResizable(True)
|
||
self.property_card_scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||
self.property_card_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||
self.property_card_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||
self.property_card_container = QWidget()
|
||
self.property_card_container.setObjectName("propertyCardContainer")
|
||
self.property_card_layout = QVBoxLayout(self.property_card_container)
|
||
self.property_card_layout.setContentsMargins(0, 0, 0, 0)
|
||
self.property_card_layout.setSpacing(6)
|
||
self.property_card_scroll.setWidget(self.property_card_container)
|
||
help_tip(
|
||
self.property_card_scroll,
|
||
"旧展示层已停用;当前使用上方表格展示参数。",
|
||
)
|
||
self.property_card_scroll.setVisible(False)
|
||
self.property_card_scroll.setMaximumHeight(0)
|
||
object_edit_layout.addWidget(self.property_card_scroll)
|
||
self.property_expand_button = QPushButton("展开全部参数")
|
||
self.property_expand_button.setObjectName("propertyExpandBar")
|
||
help_tip(self.property_expand_button, "参数较多时默认只显示前几行;点击这里展开或收起完整参数列表。")
|
||
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)
|
||
|
||
self.relation_formula_box = QGroupBox("关系式")
|
||
self.relation_formula_box.setObjectName("relationFormulaBox")
|
||
help_tip(
|
||
self.relation_formula_box,
|
||
"用 FaceID.参数 = 表达式 的形式建立关系式。第一版会先计算公式并回填当前参数表目标值,再执行参数化建模。",
|
||
)
|
||
relation_layout = QVBoxLayout(self.relation_formula_box)
|
||
relation_layout.setContentsMargins(6, 8, 6, 6)
|
||
relation_layout.setSpacing(5)
|
||
relation_input_row = QHBoxLayout()
|
||
relation_input_row.setContentsMargins(0, 0, 0, 0)
|
||
relation_input_row.setSpacing(5)
|
||
self.relation_formula_input = QLineEdit("")
|
||
self.relation_formula_input.setObjectName("relationFormulaInput")
|
||
self.relation_formula_input.setPlaceholderText("Face87.直径 = Face85.直径")
|
||
help_tip(
|
||
self.relation_formula_input,
|
||
"示例:Face87.直径 = Face85.直径,或 Face87.位置 = Face85.位置 + (0, 0, -3.5)。输入 Face87. 后会提示当前可用参数。",
|
||
)
|
||
self.relation_formula_completer_model = QStringListModel(self)
|
||
self.relation_formula_completer = QCompleter(self.relation_formula_completer_model, self)
|
||
self.relation_formula_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
||
self.relation_formula_completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
|
||
self.relation_formula_input.setCompleter(self.relation_formula_completer)
|
||
self.relation_formula_input.installEventFilter(self)
|
||
self.relation_formula_input.textChanged.connect(self._on_relation_formula_input_changed)
|
||
self.relation_formula_input.returnPressed.connect(self.add_relation_formula)
|
||
self.add_relation_formula_button = QPushButton("添加公式")
|
||
self.add_relation_formula_button.setObjectName("relationFormulaAddButton")
|
||
self.add_relation_formula_button.clicked.connect(self.add_relation_formula)
|
||
relation_input_row.addWidget(self.relation_formula_input, stretch=1)
|
||
relation_input_row.addWidget(self.add_relation_formula_button)
|
||
relation_layout.addLayout(relation_input_row)
|
||
self.relation_formula_list = QListWidget()
|
||
self.relation_formula_list.setObjectName("relationFormulaList")
|
||
self.relation_formula_list.setMinimumHeight(54)
|
||
self.relation_formula_list.setMaximumHeight(86)
|
||
self.relation_formula_list.itemSelectionChanged.connect(self._update_relation_formula_buttons)
|
||
help_tip(self.relation_formula_list, "已建立的关系式。失效或暂不能映射的公式会在这里标出原因。")
|
||
relation_layout.addWidget(self.relation_formula_list)
|
||
relation_button_row = QHBoxLayout()
|
||
relation_button_row.setContentsMargins(0, 0, 0, 0)
|
||
relation_button_row.addStretch(1)
|
||
self.remove_relation_formula_button = QPushButton("删除公式")
|
||
self.remove_relation_formula_button.setObjectName("relationFormulaRemoveButton")
|
||
self.remove_relation_formula_button.clicked.connect(self.remove_selected_relation_formula)
|
||
relation_button_row.addWidget(self.remove_relation_formula_button)
|
||
relation_layout.addLayout(relation_button_row)
|
||
object_edit_layout.addWidget(self.relation_formula_box)
|
||
|
||
property_action_row = QHBoxLayout()
|
||
property_action_row.setContentsMargins(0, 0, 0, 0)
|
||
self.apply_property_button = QPushButton("参数化建模")
|
||
self.apply_property_button.setObjectName("parametricModelButton")
|
||
self.apply_property_button.setMinimumHeight(34)
|
||
self.apply_property_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||
help_tip(self.apply_property_button, "应用当前被修改的参数;多个目标值会按表格顺序依次执行,失败时停止后续修改。")
|
||
self.apply_property_button.clicked.connect(self.apply_current_property_edit)
|
||
self.quick_export_all_button = QPushButton("导出模型")
|
||
self.quick_export_all_button.setObjectName("quickExportStepButton")
|
||
self.quick_export_all_button.setMinimumHeight(34)
|
||
self.quick_export_all_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||
help_tip(self.quick_export_all_button, "把当前编辑后的完整模型导出为 STEP 文件。导出前会做基础质量检查。")
|
||
self.quick_export_all_button.clicked.connect(self.export_all)
|
||
self.export_parameters_button = QPushButton("导出参数")
|
||
self.export_parameters_button.setObjectName("exportParametersButton")
|
||
self.export_parameters_button.setMinimumHeight(34)
|
||
self.export_parameters_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||
help_tip(self.export_parameters_button, "把已勾选的尺寸输入导出为 data.json,并生成可外部调用的参数化组件 main.py。")
|
||
self.export_parameters_button.clicked.connect(self.export_selected_parameters)
|
||
property_action_row.addWidget(self.apply_property_button)
|
||
property_action_row.addWidget(self.quick_export_all_button)
|
||
property_action_row.addWidget(self.export_parameters_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, "面积是诊断结果,不作为特征参数表里的驱动变量;该隐藏输入仅保留给历史后端回归路径使用。")
|
||
self.face_width_input = QLineEdit("", edit_box)
|
||
self.face_width_input.setVisible(False)
|
||
help_tip(self.face_width_input, "目标面内长度。当前选中对象表会使用这个隐藏输入修改 Face 平面内第一个方向的尺寸。")
|
||
self.face_height_input = QLineEdit("", edit_box)
|
||
self.face_height_input.setVisible(False)
|
||
help_tip(self.face_height_input, "目标面内宽度。当前选中对象表会使用这个隐藏输入修改 Face 平面内第二个方向的尺寸。")
|
||
self.resize_boss_button = QPushButton("调整圆柱凸台直径")
|
||
help_tip(self.resize_boss_button, "修改完整圆柱凸台直径。变大会加料,变小会重建凸台区域。")
|
||
self.resize_boss_button.clicked.connect(self.resize_boss)
|
||
edit_layout.addWidget(self.resize_boss_button, 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)
|
||
self.current_capability_box = QGroupBox("软件进度")
|
||
self.current_capability_box.setObjectName("capabilitySection")
|
||
capability_layout = QVBoxLayout(self.current_capability_box)
|
||
capability_layout.setContentsMargins(8, 8, 8, 7)
|
||
capability_layout.setSpacing(2)
|
||
self.current_capability_headline = QLabel("当前支持:Face、孔/槽、Edge、凸台、圆角/倒角、壳体")
|
||
self.current_capability_headline.setObjectName("capabilityHeadline")
|
||
self.current_capability_headline.setWordWrap(True)
|
||
capability_layout.addWidget(self.current_capability_headline)
|
||
panel_layout.addWidget(self.current_capability_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)
|
||
app = QApplication.instance()
|
||
if app is not None and not getattr(self, "_global_tooltip_filter_installed", False):
|
||
app.installEventFilter(self)
|
||
self._global_tooltip_filter_installed = True
|
||
|
||
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 | None, 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 None
|
||
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()
|
||
_suppress_vtk_output_window()
|
||
app = QApplication(sys.argv)
|
||
app.setApplicationName("几何参数化")
|
||
app.setApplicationDisplayName("几何参数化")
|
||
app.setOrganizationName("GeometryParametric")
|
||
app.setWindowIcon(_application_icon())
|
||
window = StepEditorWindow(path, background_load=bool(path) and 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())
|