feat: 完善 Face 一级关系编辑和稳定性
This commit is contained in:
+130
-29
@@ -8,7 +8,11 @@ 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,
|
||||
@@ -54,7 +58,19 @@ from .window_state import WindowStateMixin
|
||||
|
||||
_CRASH_LOG_HANDLE = None
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
DEFAULT_MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "geom_extract.step"
|
||||
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.
|
||||
@@ -85,13 +101,41 @@ def _crash_log_requested(argv: list[str]) -> bool:
|
||||
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 = True):
|
||||
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
|
||||
@@ -110,11 +154,13 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.hover_face_actor = None
|
||||
self.hover_edge_actor = None
|
||||
self.hover_signature: tuple[str, int] | None = None
|
||||
self.hover_interval_ms = 140
|
||||
self.hover_move_threshold_px = 8
|
||||
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
|
||||
@@ -180,9 +226,14 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.load_refine_thread: QThread | None = None
|
||||
self.load_refine_worker: LoadWorker | None = None
|
||||
self.pending_load_path: Path | None = None
|
||||
self.initial_load_deflection = 2.0
|
||||
self.edit_result_deflection = 1.6
|
||||
self.last_id_kind = "Face"
|
||||
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.property_editor_specs: list[dict[str, object]] = []
|
||||
self.property_table_expanded = False
|
||||
@@ -190,7 +241,6 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
|
||||
self._build_ui()
|
||||
self._build_vtk()
|
||||
self.load_step(self.step_path, background=background_load)
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
help_tip = self._set_help_tip
|
||||
@@ -303,7 +353,8 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
min-width: 8px;
|
||||
}
|
||||
QLabel#mouseModeLabel,
|
||||
QLabel#idSelectLabel {
|
||||
QLabel#idSelectLabel,
|
||||
QLabel#featureDetectionLabel {
|
||||
color: #4736b3;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -681,9 +732,9 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.mouse_mode_label.setMaximumWidth(62)
|
||||
help_tip(self.mouse_mode_label, "这里决定鼠标点击 3D 模型时按哪种对象类型选择。")
|
||||
self.mode_combo = NoWheelComboBox()
|
||||
for mode in ("Part", "Solid", "Face", "Edge", "Feature"):
|
||||
for mode in ("Feature", "Face", "Edge", "Solid", "Part"):
|
||||
self.mode_combo.addItem(_selection_mode_label(mode), mode)
|
||||
self._set_selection_mode("Face")
|
||||
self._set_selection_mode("Feature")
|
||||
self.mode_combo.setMinimumWidth(94)
|
||||
self.mode_combo.setMaximumWidth(112)
|
||||
help_tip(
|
||||
@@ -703,8 +754,35 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
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)
|
||||
@@ -723,6 +801,9 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
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)
|
||||
@@ -828,18 +909,18 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
export_layout.addWidget(self.repair_model_button)
|
||||
export_layout.addWidget(self.repair_selected_button)
|
||||
|
||||
self.object_edit_box = QGroupBox("当前选中对象:未选择")
|
||||
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.setHorizontalHeaderLabels(["尺寸参数", "当前值", "修改方式", "目标值", "操作"])
|
||||
self.property_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.property_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self.property_table.setAlternatingRowColors(True)
|
||||
@@ -859,20 +940,20 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
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, 82)
|
||||
self.property_table.setColumnWidth(1, 78)
|
||||
self.property_table.setColumnWidth(2, 104)
|
||||
self.property_table.setColumnWidth(3, 82)
|
||||
self.property_table.setColumnWidth(4, 58)
|
||||
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 = QPushButton("展开全部参数")
|
||||
self.property_expand_button.setObjectName("propertyExpandBar")
|
||||
help_tip(self.property_expand_button, "当前选中对象属性较多时,默认只显示前 6 行;点击这里展开或收起属性表。")
|
||||
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)
|
||||
@@ -1326,10 +1407,17 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
history_layout.addLayout(history_buttons)
|
||||
panel_layout.addWidget(history_box)
|
||||
|
||||
info_box = QGroupBox("选中对象信息")
|
||||
panel_layout.addStretch(1)
|
||||
|
||||
info_box = QGroupBox("诊断信息(高级)")
|
||||
info_box.setObjectName("infoSection")
|
||||
help_tip(info_box, "显示当前选中对象的几何、拓扑、特征判断和可用操作信息。")
|
||||
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。")
|
||||
@@ -1343,7 +1431,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
info_buttons.addWidget(self.copy_id_button)
|
||||
info_buttons.addWidget(self.copy_pick_button)
|
||||
info_buttons.addWidget(self.copy_info_button)
|
||||
info_layout.addLayout(info_buttons)
|
||||
diagnostic_info_layout.addLayout(info_buttons)
|
||||
|
||||
self.info_tabs = QTabWidget()
|
||||
self.info_tree = QTreeWidget()
|
||||
@@ -1358,8 +1446,11 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.info_tabs.addTab(self.info_tree, "属性表")
|
||||
self.info_tabs.addTab(self.info_text, "原始文本")
|
||||
help_tip(self.info_tabs, "在表格视图和原始文本视图之间切换当前选中对象信息。")
|
||||
info_layout.addWidget(self.info_tabs)
|
||||
panel_layout.addWidget(info_box, stretch=1)
|
||||
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()
|
||||
|
||||
@@ -1368,7 +1459,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.vtk_widget.installEventFilter(self)
|
||||
root_layout.addWidget(self.vtk_widget, stretch=1)
|
||||
|
||||
self.statusBar().showMessage("Ready")
|
||||
self.statusBar().showMessage("请选择 STEP 文件,或点击“读取模型”加载到可视化区域。")
|
||||
|
||||
def _set_help_tip(self, widget, text: str) -> None:
|
||||
widget.setToolTip(text)
|
||||
@@ -1388,10 +1479,20 @@ def _parse_args(argv: list[str]) -> tuple[Path, bool]:
|
||||
|
||||
|
||||
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")
|
||||
@@ -1399,7 +1500,7 @@ def main() -> int:
|
||||
app.quit()
|
||||
return 0
|
||||
window.show()
|
||||
window.vtk_widget.Start()
|
||||
window._ensure_vtk_interactor_started()
|
||||
return app.exec()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user