feat: 完善一级关系参数化建模与参数导出
This commit is contained in:
+52
-17
@@ -54,7 +54,7 @@ from .info_panel import InfoPanelMixin
|
||||
from .ui_helpers import * # noqa: F403
|
||||
from .window_actions import WindowActionMixin
|
||||
from .window_core import WindowCoreMixin
|
||||
from .window_state import WindowStateMixin
|
||||
from .window_state import PROPERTY_TABLE_HEADERS, WindowStateMixin
|
||||
|
||||
|
||||
_CRASH_LOG_HANDLE = None
|
||||
@@ -145,7 +145,7 @@ def _isolated_edit_worker_request(argv: list[str]) -> Path | None:
|
||||
class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, InfoPanelMixin, QMainWindow):
|
||||
ui_task_requested = Signal(object)
|
||||
|
||||
def __init__(self, step_path: str | Path, *, background_load: bool = False):
|
||||
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)
|
||||
@@ -154,7 +154,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.resize(1280, 820)
|
||||
|
||||
self.model: StepModel | None = None
|
||||
self.step_path = Path(step_path)
|
||||
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
|
||||
@@ -585,6 +585,32 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
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;
|
||||
@@ -885,12 +911,13 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.open_button.setMinimumWidth(96)
|
||||
help_tip(self.open_button, "选择并导入一个 .step 或 .stp 几何模型。打开失败时会保留当前模型。")
|
||||
self.open_button.clicked.connect(self.open_step)
|
||||
self.path_label = QLineEdit(str(self.step_path))
|
||||
self.path_label = 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.setToolTip(str(self.step_path))
|
||||
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)
|
||||
@@ -1086,7 +1113,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
export_box.setObjectName("exportSection")
|
||||
help_tip(export_box, "把当前模型或选中对象导出为 STEP,也可以做基础质量检查和修复。")
|
||||
export_layout = QVBoxLayout(export_box)
|
||||
self.export_all_button = QPushButton("导出当前完整 STEP")
|
||||
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("导出选中零件")
|
||||
@@ -1132,9 +1159,9 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
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, 4, self.object_edit_box)
|
||||
self.property_table = QTableWidget(0, len(PROPERTY_TABLE_HEADERS), self.object_edit_box)
|
||||
self.property_table.setObjectName("propertyTable")
|
||||
self.property_table.setHorizontalHeaderLabels(["尺寸参数", "当前值", "建模意图", "目标值"])
|
||||
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)
|
||||
@@ -1153,11 +1180,12 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
property_header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||
property_header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
|
||||
property_header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed)
|
||||
property_header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
|
||||
self.property_table.setColumnWidth(0, 148)
|
||||
self.property_table.setColumnWidth(1, 92)
|
||||
self.property_table.setColumnWidth(2, 112)
|
||||
self.property_table.setColumnWidth(3, 96)
|
||||
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,
|
||||
@@ -1217,14 +1245,21 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
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("导出当前完整STEP")
|
||||
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。")
|
||||
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)
|
||||
@@ -1741,10 +1776,10 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
|
||||
|
||||
|
||||
def _parse_args(argv: list[str]) -> tuple[Path, bool]:
|
||||
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 DEFAULT_MODEL_PATH
|
||||
path = Path(paths[0]) if paths else None
|
||||
return path, smoke_test
|
||||
|
||||
|
||||
@@ -1764,7 +1799,7 @@ def main() -> int:
|
||||
app.setApplicationDisplayName("几何参数化")
|
||||
app.setOrganizationName("GeometryParametric")
|
||||
app.setWindowIcon(_application_icon())
|
||||
window = StepEditorWindow(path, background_load=not smoke_test)
|
||||
window = StepEditorWindow(path, background_load=bool(path) and not smoke_test)
|
||||
if smoke_test:
|
||||
print("smoke test ok")
|
||||
window.close()
|
||||
|
||||
@@ -61,7 +61,7 @@ from OCC.Extend.TopologyUtils import TopologyExplorer, discretize_edge
|
||||
|
||||
from .constants import CURVE_TYPES, SNAPSHOT_FACE_LOGICAL_IDS_KEY, SURFACE_TYPES
|
||||
from .geometry_utils import * # noqa: F403
|
||||
from .step_io import _prepare_shape_for_step_export, _write_step
|
||||
from .step_io import _prepare_shape_for_step_export, _write_brep, _write_step
|
||||
|
||||
|
||||
class ExportMixin:
|
||||
@@ -71,6 +71,9 @@ class ExportMixin:
|
||||
)
|
||||
_write_step(export_shape, Path(filename))
|
||||
|
||||
def export_internal_brep(self, filename: str | Path) -> None:
|
||||
_write_brep(self.shape, Path(filename))
|
||||
|
||||
def export_quality_info(self, scope: str, target_id: int | None = None) -> dict[str, object]:
|
||||
if scope == "all":
|
||||
return _shape_quality_info("当前完整模型", self.shape, expect_solid=False)
|
||||
|
||||
+36
-6
@@ -288,6 +288,19 @@ class FeatureMixin:
|
||||
max_scan_edges: int | None = None,
|
||||
progress_callback: Callable[[], None] | None = None,
|
||||
) -> list[dict[str, object]]:
|
||||
limit = max(1, int(limit))
|
||||
normalized_max_faces = None if max_scan_faces is None else max(0, int(max_scan_faces))
|
||||
normalized_max_edges = None if max_scan_edges is None else max(0, int(max_scan_edges))
|
||||
cache_key = (
|
||||
"editable",
|
||||
limit,
|
||||
bool(detailed),
|
||||
normalized_max_faces,
|
||||
normalized_max_edges,
|
||||
)
|
||||
cache = getattr(self, "_editable_feature_candidates_cache", None)
|
||||
if isinstance(cache, dict) and cache_key in cache:
|
||||
return [dict(item) for item in cache[cache_key]]
|
||||
per_type_limit = max(1, limit // 5)
|
||||
candidates: list[dict[str, object]] = []
|
||||
|
||||
@@ -315,7 +328,7 @@ class FeatureMixin:
|
||||
for item in self.cylindrical_feature_candidates(
|
||||
limit=cylinder_scan_limit,
|
||||
include_end_info=True,
|
||||
max_scan_faces=max_scan_faces,
|
||||
max_scan_faces=normalized_max_faces,
|
||||
progress_callback=progress_callback,
|
||||
):
|
||||
feature_guess = str(item["feature_guess"])
|
||||
@@ -680,7 +693,7 @@ class FeatureMixin:
|
||||
shell_thickness_limit = max(2, min(per_type_limit, limit // 10))
|
||||
shell_candidate_rows: list[dict[str, object]] = []
|
||||
shell_scan_pool_limit = max(shell_thickness_limit * 4, 12)
|
||||
face_scan_limit = len(self.faces) if max_scan_faces is None else min(len(self.faces), max(0, int(max_scan_faces)))
|
||||
face_scan_limit = len(self.faces) if normalized_max_faces is None else min(len(self.faces), normalized_max_faces)
|
||||
for face_id, face in enumerate(self.faces[:face_scan_limit]):
|
||||
if progress_callback is not None and face_id % 30 == 0:
|
||||
progress_callback()
|
||||
@@ -832,7 +845,7 @@ class FeatureMixin:
|
||||
ellipse_edge_major_radius_count = 0
|
||||
ellipse_edge_minor_radius_count = 0
|
||||
edge_type_limit = max(1, per_type_limit // 3)
|
||||
edge_scan_limit = len(self.edges) if max_scan_edges is None else min(len(self.edges), max(0, int(max_scan_edges)))
|
||||
edge_scan_limit = len(self.edges) if normalized_max_edges is None else min(len(self.edges), normalized_max_edges)
|
||||
for edge_id, edge in enumerate(self.edges[:edge_scan_limit]):
|
||||
if progress_callback is not None and edge_id % 80 == 0:
|
||||
progress_callback()
|
||||
@@ -995,7 +1008,10 @@ class FeatureMixin:
|
||||
for candidate in candidates:
|
||||
candidate["recognition_user_priority"] = feature_recognition_sort_key(candidate)[0]
|
||||
candidates.sort(key=feature_recognition_sort_key)
|
||||
return candidates[:limit]
|
||||
result = [dict(item) for item in candidates[:limit]]
|
||||
if isinstance(cache, dict):
|
||||
cache[cache_key] = [dict(item) for item in result]
|
||||
return [dict(item) for item in result]
|
||||
|
||||
def cylindrical_feature_candidates(
|
||||
self,
|
||||
@@ -1004,8 +1020,19 @@ class FeatureMixin:
|
||||
max_scan_faces: int | None = None,
|
||||
progress_callback: Callable[[], None] | None = None,
|
||||
) -> list[dict[str, object]]:
|
||||
limit = max(1, int(limit))
|
||||
normalized_max_faces = None if max_scan_faces is None else max(0, int(max_scan_faces))
|
||||
cache_key = (
|
||||
"cylinder",
|
||||
limit,
|
||||
bool(include_end_info),
|
||||
normalized_max_faces,
|
||||
)
|
||||
cache = getattr(self, "_cylindrical_feature_candidates_cache", None)
|
||||
if isinstance(cache, dict) and cache_key in cache:
|
||||
return [dict(item) for item in cache[cache_key]]
|
||||
candidates: list[dict[str, object]] = []
|
||||
face_scan_limit = len(self.faces) if max_scan_faces is None else min(len(self.faces), max(0, int(max_scan_faces)))
|
||||
face_scan_limit = len(self.faces) if normalized_max_faces is None else min(len(self.faces), normalized_max_faces)
|
||||
for face_id, face in enumerate(self.faces[:face_scan_limit]):
|
||||
if progress_callback is not None and face_id % 30 == 0:
|
||||
progress_callback()
|
||||
@@ -1075,7 +1102,10 @@ class FeatureMixin:
|
||||
candidates.append(candidate)
|
||||
if len(candidates) >= limit:
|
||||
break
|
||||
return candidates
|
||||
result = [dict(item) for item in candidates]
|
||||
if isinstance(cache, dict):
|
||||
cache[cache_key] = [dict(item) for item in result]
|
||||
return [dict(item) for item in result]
|
||||
|
||||
def cylindrical_resize_plan(self, face_id: int, new_diameter: float) -> dict[str, object]:
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
|
||||
@@ -625,7 +625,16 @@ def _axis_aligned_edge_candidates(
|
||||
return [edge for _score, edge in candidates]
|
||||
|
||||
|
||||
def _enable_occt_builder_parallel(builder) -> None:
|
||||
if hasattr(builder, "SetRunParallel"):
|
||||
try:
|
||||
builder.SetRunParallel(True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _finalize_boolean_result(op, operation_name: str, *, use_glue: bool | None = None) -> TopoDS_Shape:
|
||||
_enable_occt_builder_parallel(op)
|
||||
op.SetNonDestructive(True)
|
||||
glue_enabled = "cut" not in operation_name.lower() if use_glue is None else bool(use_glue)
|
||||
if glue_enabled and hasattr(op, "SetGlue"):
|
||||
@@ -668,6 +677,7 @@ def _simplify_boolean_builder(builder) -> None:
|
||||
|
||||
|
||||
def _finalize_builder_result(builder, operation_name: str) -> TopoDS_Shape:
|
||||
_enable_occt_builder_parallel(builder)
|
||||
builder.Build()
|
||||
if hasattr(builder, "IsDone") and not builder.IsDone():
|
||||
raise RuntimeError(f"{operation_name} operation failed.")
|
||||
|
||||
@@ -144,6 +144,24 @@ def _execute(model: StepModel, operation: str, args: list[object]) -> str:
|
||||
raise ValueError(f"Unsupported isolated edit operation: {operation}")
|
||||
|
||||
|
||||
def _load_request_model(path: Path, file_format: str) -> StepModel:
|
||||
if file_format == "brep":
|
||||
return StepModel.load_internal_brep(path)
|
||||
if file_format == "step":
|
||||
return StepModel.load(path)
|
||||
raise ValueError(f"Unsupported isolated edit input format: {file_format}")
|
||||
|
||||
|
||||
def _export_response_model(model: StepModel, path: Path, file_format: str) -> None:
|
||||
if file_format == "brep":
|
||||
model.export_internal_brep(path)
|
||||
return
|
||||
if file_format == "step":
|
||||
model.export_all(path)
|
||||
return
|
||||
raise ValueError(f"Unsupported isolated edit output format: {file_format}")
|
||||
|
||||
|
||||
def run_request(request: str | Path) -> int:
|
||||
request_path = Path(request)
|
||||
response_path = request_path.with_suffix(".response.json")
|
||||
@@ -153,10 +171,12 @@ def run_request(request: str | Path) -> int:
|
||||
output_path = Path(str(request["output_path"]))
|
||||
operation = str(request["operation"])
|
||||
args = list(request.get("args") or [])
|
||||
input_format = str(request.get("input_format") or "step").strip().lower()
|
||||
output_format = str(request.get("output_format") or input_format).strip().lower()
|
||||
|
||||
model = StepModel.load(input_path)
|
||||
model = _load_request_model(input_path, input_format)
|
||||
message = _execute(model, operation, args)
|
||||
model.export_all(output_path)
|
||||
_export_response_model(model, output_path, output_format)
|
||||
response_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
@@ -164,6 +184,7 @@ def run_request(request: str | Path) -> int:
|
||||
"message": message,
|
||||
"stats": model.stats().__dict__,
|
||||
"output_path": str(output_path),
|
||||
"output_format": output_format,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
|
||||
@@ -90,6 +90,7 @@ from .transforms import TransformMixin
|
||||
from .geometry_utils import * # noqa: F403
|
||||
from .model_types import PartNode, TopologyStats
|
||||
from .step_io import (
|
||||
_load_brep_shape,
|
||||
_load_plain_step,
|
||||
_load_with_xcaf,
|
||||
_parse_product_names,
|
||||
@@ -130,6 +131,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._edge_duplicate_key_ids_cache: dict[tuple[object, ...], list[int]] | None = None
|
||||
self._same_domain_internal_edge_ids_cache: set[int] | None = None
|
||||
self._same_domain_duplicate_edge_ids_cache: set[int] | None = None
|
||||
self._editable_feature_candidates_cache: dict[tuple[object, ...], list[dict[str, object]]] = {}
|
||||
self._cylindrical_feature_candidates_cache: dict[tuple[object, ...], list[dict[str, object]]] = {}
|
||||
self._face_polydata_cache: dict[tuple[object, ...], object] = {}
|
||||
self._edge_polydata_cache: dict[tuple[object, ...], object] = {}
|
||||
self._polydata_cache_limit = 96
|
||||
@@ -137,6 +140,13 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._face_mesh_deflections: dict[int, float] = {}
|
||||
self.refresh_topology()
|
||||
|
||||
@classmethod
|
||||
def from_shape(cls, filename: str | Path, shape: TopoDS_Shape, *, name: str | None = None) -> "StepModel":
|
||||
path = Path(filename)
|
||||
part_name = name or path.stem or "model"
|
||||
parts = [PartNode(1, part_name, "part", shape, path=part_name)]
|
||||
return cls(path, parts, shape)
|
||||
|
||||
@classmethod
|
||||
def load(cls, filename: str | Path) -> "StepModel":
|
||||
path = Path(filename)
|
||||
@@ -151,6 +161,13 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
parts = [PartNode(1, fallback_name, "part", whole_shape, path=fallback_name)]
|
||||
return cls(path, parts, whole_shape)
|
||||
|
||||
@classmethod
|
||||
def load_internal_brep(cls, filename: str | Path) -> "StepModel":
|
||||
path = Path(filename)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(path)
|
||||
return cls.from_shape(path, _load_brep_shape(path), name=path.stem)
|
||||
|
||||
def display_parts(self) -> list[PartNode]:
|
||||
leaf_parts = [p for p in self.parts if p.kind == "part" and not p.shape.IsNull()]
|
||||
if leaf_parts:
|
||||
@@ -220,6 +237,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._edge_duplicate_key_ids_cache = None
|
||||
self._same_domain_internal_edge_ids_cache = None
|
||||
self._same_domain_duplicate_edge_ids_cache = None
|
||||
self._editable_feature_candidates_cache.clear()
|
||||
self._cylindrical_feature_candidates_cache.clear()
|
||||
self._face_polydata_cache.clear()
|
||||
self._edge_polydata_cache.clear()
|
||||
self._mesh_deflection = None
|
||||
|
||||
@@ -4,6 +4,8 @@ import re
|
||||
from pathlib import Path
|
||||
|
||||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform
|
||||
from OCC.Core.BRep import BRep_Builder
|
||||
from OCC.Core.BRepTools import breptools
|
||||
from OCC.Core.IFSelect import IFSelect_RetDone
|
||||
from OCC.Core.Interface import Interface_Static
|
||||
from OCC.Core.STEPCAFControl import STEPCAFControl_Reader
|
||||
@@ -126,6 +128,24 @@ def _write_step(shape: TopoDS_Shape, filename: Path) -> None:
|
||||
raise IOError(f"Could not write STEP file: {filename}")
|
||||
|
||||
|
||||
def _load_brep_shape(filename: str | Path) -> TopoDS_Shape:
|
||||
path = Path(filename)
|
||||
shape = TopoDS_Shape()
|
||||
builder = BRep_Builder()
|
||||
if not breptools.Read(shape, str(path), builder) or shape.IsNull():
|
||||
raise IOError(f"Could not read BREP file: {path}")
|
||||
return shape
|
||||
|
||||
|
||||
def _write_brep(shape: TopoDS_Shape, filename: str | Path) -> None:
|
||||
if shape.IsNull():
|
||||
raise ValueError("Cannot export a null shape.")
|
||||
path = Path(filename)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not breptools.Write(shape, str(path)):
|
||||
raise IOError(f"Could not write BREP file: {path}")
|
||||
|
||||
|
||||
def _prepare_shape_for_step_export(shape: TopoDS_Shape) -> TopoDS_Shape:
|
||||
if shape.IsNull():
|
||||
return shape
|
||||
|
||||
+165
-19
@@ -7,9 +7,10 @@ from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import vtk
|
||||
from PySide6.QtCore import Qt, QThread, Slot
|
||||
from PySide6.QtCore import Qt, QThread, QTimer, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QFileDialog,
|
||||
@@ -84,7 +85,78 @@ def _compact_user_message(value: object, limit: int = 360) -> str:
|
||||
return f"{text[: max(0, limit - 1)].rstrip()}..."
|
||||
|
||||
|
||||
def _edit_timing_summary(timings: object, *, limit: int = 5) -> str:
|
||||
if not isinstance(timings, dict) or not timings:
|
||||
return ""
|
||||
labels = {
|
||||
"snapshot": "快照",
|
||||
"precheck": "预检",
|
||||
"edit_geometry": "几何计算",
|
||||
"isolated_export": "内部传模",
|
||||
"isolated_worker": "子进程计算",
|
||||
"isolated_result_load": "读取结果",
|
||||
"validate": "结果校验",
|
||||
"display_faces": "面显示",
|
||||
"display_edges": "边线",
|
||||
"finish_ui": "界面刷新",
|
||||
"total": "总计",
|
||||
}
|
||||
rows: list[tuple[str, float]] = []
|
||||
for key, value in timings.items():
|
||||
if key == "total":
|
||||
continue
|
||||
try:
|
||||
seconds = float(value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if seconds >= 0.05:
|
||||
rows.append((labels.get(str(key), str(key)), seconds))
|
||||
rows.sort(key=lambda item: item[1], reverse=True)
|
||||
parts = [f"{label} {seconds:.1f}s" for label, seconds in rows[:limit]]
|
||||
try:
|
||||
total = float(timings.get("total"))
|
||||
except (TypeError, ValueError):
|
||||
total = 0.0
|
||||
if total >= 0.05:
|
||||
parts.append(f"总计 {total:.1f}s")
|
||||
return ",".join(parts)
|
||||
|
||||
|
||||
class WindowActionMixin:
|
||||
def _empty_edge_polydata(self):
|
||||
polydata = vtk.vtkPolyData()
|
||||
polydata.SetPoints(vtk.vtkPoints())
|
||||
polydata.SetLines(vtk.vtkCellArray())
|
||||
edge_arr = vtk.vtkIntArray()
|
||||
edge_arr.SetName("edge_id")
|
||||
part_arr = vtk.vtkIntArray()
|
||||
part_arr.SetName("part_id")
|
||||
polydata.GetCellData().AddArray(edge_arr)
|
||||
polydata.GetCellData().AddArray(part_arr)
|
||||
return polydata
|
||||
|
||||
def _parameter_export_output_path(self) -> Path:
|
||||
return Path(__file__).resolve().parent.parent / "data.json"
|
||||
|
||||
def export_selected_parameters(self) -> None:
|
||||
rows = self._selected_parameter_export_rows() if hasattr(self, "_selected_parameter_export_rows") else []
|
||||
if not rows:
|
||||
if hasattr(self, "_update_parameter_export_state"):
|
||||
self._update_parameter_export_state()
|
||||
self.statusBar().showMessage("请先在“输入参数”列勾选至少一个尺寸参数。")
|
||||
return
|
||||
output_path = self._parameter_export_output_path()
|
||||
try:
|
||||
output_path.write_text(json.dumps(rows, ensure_ascii=False, indent=4), encoding="utf-8")
|
||||
except OSError as exc:
|
||||
QMessageBox.warning(self, "导出参数失败", f"无法写入 {output_path.name}:{exc}")
|
||||
return
|
||||
self.statusBar().showMessage(f"已导出 {len(rows)} 个输入参数到 {output_path.name}")
|
||||
if hasattr(self, "set_plain_info"):
|
||||
names = "、".join(str(row.get("displayName", "")) for row in rows[:8] if row.get("displayName"))
|
||||
suffix = "……" if len(rows) > 8 else ""
|
||||
self.set_plain_info(f"已导出参数文件:{output_path}\n参数数量:{len(rows)}\n参数:{names}{suffix}")
|
||||
|
||||
def export_all(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
@@ -92,10 +164,11 @@ class WindowActionMixin:
|
||||
return
|
||||
if not self._confirm_export_quality("all"):
|
||||
return
|
||||
source_path = self.step_path if isinstance(getattr(self, "step_path", None), Path) else Path.cwd() / "model.step"
|
||||
target, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"导出当前完整 STEP",
|
||||
str(self.step_path.parent / f"{self.step_path.stem}_edited.step"),
|
||||
"导出模型",
|
||||
str(source_path.parent / f"{source_path.stem}_edited.step"),
|
||||
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
|
||||
)
|
||||
if not target:
|
||||
@@ -7651,6 +7724,7 @@ class WindowActionMixin:
|
||||
"pick_position": self.selected_pick_position,
|
||||
"show_same_domain_internal_edges": self._show_same_domain_internal_edges(),
|
||||
"edit_result_deflection": result_deflection,
|
||||
"defer_edge_polydata": True,
|
||||
"isolation": dict(isolation or {}),
|
||||
}
|
||||
blocker = self._edit_preflight_blocker(context)
|
||||
@@ -7691,12 +7765,16 @@ class WindowActionMixin:
|
||||
def job():
|
||||
if self.model is None:
|
||||
raise RuntimeError("Model is not loaded.")
|
||||
timings: dict[str, float] = {}
|
||||
total_started = time.perf_counter()
|
||||
started = time.perf_counter()
|
||||
snapshot = self.model.snapshot()
|
||||
target_part_id = self._edit_context_part_id(context)
|
||||
before_stats = self.model.stats()
|
||||
before_part_stats = self._part_stats_or_none(target_part_id)
|
||||
before_quality = self._edit_quality_info_or_none(self.model, context, target_part_id)
|
||||
before_geometry = {}
|
||||
timings["snapshot"] = time.perf_counter() - started
|
||||
isolation = context.get("isolation")
|
||||
if isinstance(isolation, dict) and isolation:
|
||||
return self._run_isolated_edit_job(
|
||||
@@ -7707,9 +7785,14 @@ class WindowActionMixin:
|
||||
before_part_stats=before_part_stats,
|
||||
before_quality=before_quality,
|
||||
before_geometry=before_geometry,
|
||||
base_timings=timings,
|
||||
total_started=total_started,
|
||||
)
|
||||
try:
|
||||
started = time.perf_counter()
|
||||
result = action()
|
||||
timings["edit_geometry"] = time.perf_counter() - started
|
||||
started = time.perf_counter()
|
||||
after_snapshot = self.model.snapshot()
|
||||
after_stats = self.model.stats()
|
||||
after_part_stats = self._part_stats_or_none(target_part_id)
|
||||
@@ -7724,6 +7807,7 @@ class WindowActionMixin:
|
||||
after_quality,
|
||||
after_model=self.model,
|
||||
)
|
||||
timings["validate"] = time.perf_counter() - started
|
||||
after_geometry = {}
|
||||
except Exception as exc:
|
||||
try:
|
||||
@@ -7738,17 +7822,27 @@ class WindowActionMixin:
|
||||
) from exc
|
||||
model_polydata = None
|
||||
edge_polydata = None
|
||||
edge_deferred = bool(context.get("defer_edge_polydata", True))
|
||||
try:
|
||||
deflection = float(context.get("edit_result_deflection", 1.6))
|
||||
started = time.perf_counter()
|
||||
face_polydata = self.model.build_face_polydata(deflection=deflection)
|
||||
model_polydata = _smooth_surface_polydata(face_polydata)
|
||||
edge_polydata = self.model.build_edge_polydata(
|
||||
deflection=deflection,
|
||||
show_same_domain_internal_edges=bool(context.get("show_same_domain_internal_edges", False))
|
||||
)
|
||||
timings["display_faces"] = time.perf_counter() - started
|
||||
if edge_deferred:
|
||||
edge_polydata = self._empty_edge_polydata()
|
||||
timings["display_edges"] = 0.0
|
||||
else:
|
||||
started = time.perf_counter()
|
||||
edge_polydata = self.model.build_edge_polydata(
|
||||
deflection=deflection,
|
||||
show_same_domain_internal_edges=bool(context.get("show_same_domain_internal_edges", False)),
|
||||
)
|
||||
timings["display_edges"] = time.perf_counter() - started
|
||||
except Exception:
|
||||
model_polydata = None
|
||||
edge_polydata = None
|
||||
timings["total"] = time.perf_counter() - total_started
|
||||
return {
|
||||
"message": str(result),
|
||||
"snapshot": snapshot,
|
||||
@@ -7762,6 +7856,8 @@ class WindowActionMixin:
|
||||
"after_geometry": after_geometry,
|
||||
"model_polydata": model_polydata,
|
||||
"edge_polydata": edge_polydata,
|
||||
"edge_polydata_deferred": edge_deferred,
|
||||
"timings": timings,
|
||||
}
|
||||
|
||||
return job
|
||||
@@ -7776,9 +7872,14 @@ class WindowActionMixin:
|
||||
before_part_stats,
|
||||
before_quality: dict[str, object] | None,
|
||||
before_geometry: dict[str, object],
|
||||
base_timings: dict[str, float] | None = None,
|
||||
total_started: float | None = None,
|
||||
) -> dict[str, object]:
|
||||
if self.model is None:
|
||||
raise RuntimeError("Model is not loaded.")
|
||||
timings = dict(base_timings or {})
|
||||
if total_started is None:
|
||||
total_started = time.perf_counter()
|
||||
target_part_id = self._edit_context_part_id(context)
|
||||
timeout_seconds = float(isolation.get("timeout_seconds") or 180.0)
|
||||
operation = str(isolation.get("operation") or "").strip()
|
||||
@@ -7789,15 +7890,26 @@ class WindowActionMixin:
|
||||
project_root = Path(__file__).resolve().parent.parent
|
||||
with tempfile.TemporaryDirectory(prefix="geom_param_isolated_edit_") as temp_dir:
|
||||
temp_root = Path(temp_dir)
|
||||
input_path = temp_root / "input.step"
|
||||
output_path = temp_root / "output.step"
|
||||
exchange_format = str(isolation.get("exchange_format") or "brep").strip().lower()
|
||||
if exchange_format not in {"brep", "step"}:
|
||||
exchange_format = "brep"
|
||||
suffix = ".brep" if exchange_format == "brep" else ".step"
|
||||
input_path = temp_root / f"input{suffix}"
|
||||
output_path = temp_root / f"output{suffix}"
|
||||
request_path = temp_root / "request.json"
|
||||
self.model.export_all(input_path)
|
||||
started = time.perf_counter()
|
||||
if exchange_format == "brep":
|
||||
self.model.export_internal_brep(input_path)
|
||||
else:
|
||||
self.model.export_all(input_path)
|
||||
timings["isolated_export"] = time.perf_counter() - started
|
||||
request_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"input_path": str(input_path),
|
||||
"output_path": str(output_path),
|
||||
"input_format": exchange_format,
|
||||
"output_format": exchange_format,
|
||||
"operation": operation,
|
||||
"args": args,
|
||||
},
|
||||
@@ -7811,6 +7923,7 @@ class WindowActionMixin:
|
||||
self.isolated_edit_cancel_requested = False
|
||||
process: subprocess.Popen[str] | None = None
|
||||
try:
|
||||
started = time.perf_counter()
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
cwd=project_root,
|
||||
@@ -7823,6 +7936,7 @@ class WindowActionMixin:
|
||||
self.active_isolated_edit_process = process
|
||||
stdout, stderr = process.communicate(timeout=timeout_seconds)
|
||||
completed = subprocess.CompletedProcess(command, process.returncode, stdout, stderr)
|
||||
timings["isolated_worker"] = time.perf_counter() - started
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
self._terminate_isolated_edit_process(process)
|
||||
try:
|
||||
@@ -7856,15 +7970,21 @@ class WindowActionMixin:
|
||||
f"{self._edit_failure_diagnostics(context)}"
|
||||
)
|
||||
if not output_path.exists():
|
||||
raise RuntimeError("隔离子进程报告成功,但没有生成结果 STEP;原模型保持不变。")
|
||||
raise RuntimeError("隔离子进程报告成功,但没有生成结果文件;原模型保持不变。")
|
||||
|
||||
new_model = StepModel.load(output_path)
|
||||
started = time.perf_counter()
|
||||
if exchange_format == "brep":
|
||||
new_model = StepModel.load_internal_brep(output_path)
|
||||
else:
|
||||
new_model = StepModel.load(output_path)
|
||||
timings["isolated_result_load"] = time.perf_counter() - started
|
||||
try:
|
||||
new_model.filename = self.step_path
|
||||
except Exception:
|
||||
pass
|
||||
child_message = str(response.get("message") or "隔离子进程编辑完成。")
|
||||
self._preserve_isolated_face_logical_id(new_model, context, child_message)
|
||||
started = time.perf_counter()
|
||||
after_snapshot = new_model.snapshot()
|
||||
after_stats = new_model.stats()
|
||||
after_part_stats = self._part_stats_or_none_for_model(new_model, target_part_id)
|
||||
@@ -7879,20 +7999,31 @@ class WindowActionMixin:
|
||||
after_quality,
|
||||
after_model=new_model,
|
||||
)
|
||||
timings["validate"] = time.perf_counter() - started
|
||||
after_geometry: dict[str, object] = {}
|
||||
model_polydata = None
|
||||
edge_polydata = None
|
||||
edge_deferred = bool(context.get("defer_edge_polydata", True))
|
||||
try:
|
||||
deflection = float(context.get("edit_result_deflection", 1.6))
|
||||
started = time.perf_counter()
|
||||
face_polydata = new_model.build_face_polydata(deflection=deflection)
|
||||
model_polydata = _smooth_surface_polydata(face_polydata)
|
||||
edge_polydata = new_model.build_edge_polydata(
|
||||
deflection=deflection,
|
||||
show_same_domain_internal_edges=bool(context.get("show_same_domain_internal_edges", False)),
|
||||
)
|
||||
timings["display_faces"] = time.perf_counter() - started
|
||||
if edge_deferred:
|
||||
edge_polydata = self._empty_edge_polydata()
|
||||
timings["display_edges"] = 0.0
|
||||
else:
|
||||
started = time.perf_counter()
|
||||
edge_polydata = new_model.build_edge_polydata(
|
||||
deflection=deflection,
|
||||
show_same_domain_internal_edges=bool(context.get("show_same_domain_internal_edges", False)),
|
||||
)
|
||||
timings["display_edges"] = time.perf_counter() - started
|
||||
except Exception:
|
||||
model_polydata = None
|
||||
edge_polydata = None
|
||||
timings["total"] = time.perf_counter() - total_started
|
||||
|
||||
return {
|
||||
"message": f"{child_message} 已通过隔离子进程完成;如果 OCC 崩溃,主程序不会被带崩。",
|
||||
@@ -7908,6 +8039,8 @@ class WindowActionMixin:
|
||||
"after_geometry": after_geometry,
|
||||
"model_polydata": model_polydata,
|
||||
"edge_polydata": edge_polydata,
|
||||
"edge_polydata_deferred": edge_deferred,
|
||||
"timings": timings,
|
||||
}
|
||||
|
||||
def _isolated_edit_command(self, request_path: Path) -> list[str]:
|
||||
@@ -8710,6 +8843,7 @@ class WindowActionMixin:
|
||||
if self.model is None:
|
||||
self._end_edit_task(clear_preview=True)
|
||||
return
|
||||
finish_started = time.perf_counter()
|
||||
self._set_edit_status_text("布尔计算已完成,正在刷新模型显示和历史记录...")
|
||||
if not isinstance(result, dict):
|
||||
self._end_edit_task(clear_preview=True)
|
||||
@@ -8742,16 +8876,22 @@ class WindowActionMixin:
|
||||
)
|
||||
model_polydata = result.get("model_polydata")
|
||||
edge_polydata = result.get("edge_polydata")
|
||||
if model_polydata is None or edge_polydata is None:
|
||||
edge_deferred = bool(result.get("edge_polydata_deferred"))
|
||||
if model_polydata is None or (edge_polydata is None and not edge_deferred):
|
||||
deflection = float(context.get("edit_result_deflection", 1.6))
|
||||
model_polydata = self.model.build_face_polydata(deflection=deflection)
|
||||
edge_polydata = self.model.build_edge_polydata(
|
||||
deflection=deflection,
|
||||
show_same_domain_internal_edges=bool(context.get("show_same_domain_internal_edges", False))
|
||||
)
|
||||
elif edge_polydata is None:
|
||||
edge_polydata = self._empty_edge_polydata()
|
||||
self.clear_edit_preview(render=False)
|
||||
self._populate_part_tree()
|
||||
self._rebuild_scene_from_polydata(model_polydata, edge_polydata, reset_camera=False)
|
||||
timings = result.get("timings")
|
||||
if isinstance(timings, dict):
|
||||
timings["finish_ui"] = time.perf_counter() - finish_started
|
||||
locator_note = self._locate_operation_record(record)
|
||||
except Exception as exc:
|
||||
rollback_message = self._restore_failed_edit_snapshot(result.get("snapshot") if isinstance(result, dict) else None)
|
||||
@@ -8768,13 +8908,19 @@ class WindowActionMixin:
|
||||
self._clear_cylinder_candidates()
|
||||
self._refresh_history_list()
|
||||
self._end_edit_task(clear_preview=False)
|
||||
timing_text = _edit_timing_summary(result.get("timings"))
|
||||
if bool(result.get("edge_polydata_deferred")):
|
||||
QTimer.singleShot(80, self._rebuild_deferred_edge_display)
|
||||
if result.get("quality_warnings"):
|
||||
self.statusBar().showMessage("编辑完成,但有质量警告,请查看操作历史详情")
|
||||
else:
|
||||
selection_note = ";已保持当前选择" if self.selected_kind is not None else ""
|
||||
self.statusBar().showMessage(f"{message}{selection_note}")
|
||||
timing_note = f";耗时 {timing_text}" if timing_text else ""
|
||||
edge_note = ";边线稍后补充" if bool(result.get("edge_polydata_deferred")) else ""
|
||||
self.statusBar().showMessage(f"{message}{selection_note}{timing_note}{edge_note}")
|
||||
if self.selected_kind is None:
|
||||
self.set_plain_info(f"{record.detail}\n\n{locator_note}")
|
||||
timing_detail = f"\n\n性能耗时:{timing_text}" if timing_text else ""
|
||||
self.set_plain_info(f"{record.detail}{timing_detail}\n\n{locator_note}")
|
||||
|
||||
@Slot(str)
|
||||
def _fail_edit_action(self, message: str) -> None:
|
||||
|
||||
+155
-31
@@ -47,11 +47,59 @@ def _safe_int_or_none(value: object) -> int | None:
|
||||
return None
|
||||
|
||||
|
||||
def _empty_edge_polydata():
|
||||
polydata = vtk.vtkPolyData()
|
||||
polydata.SetPoints(vtk.vtkPoints())
|
||||
polydata.SetLines(vtk.vtkCellArray())
|
||||
edge_arr = vtk.vtkIntArray()
|
||||
edge_arr.SetName("edge_id")
|
||||
part_arr = vtk.vtkIntArray()
|
||||
part_arr.SetName("part_id")
|
||||
polydata.GetCellData().AddArray(edge_arr)
|
||||
polydata.GetCellData().AddArray(part_arr)
|
||||
return polydata
|
||||
|
||||
|
||||
def _timing_summary(timings: dict[str, object] | None, *, limit: int = 4) -> str:
|
||||
if not isinstance(timings, dict) or not timings:
|
||||
return ""
|
||||
labels = {
|
||||
"step_load": "STEP读取",
|
||||
"stats": "统计",
|
||||
"display_faces": "面显示",
|
||||
"display_edges": "边线",
|
||||
"apply_loaded": "界面刷新",
|
||||
"total": "总计",
|
||||
}
|
||||
rows: list[tuple[str, float]] = []
|
||||
for key, value in timings.items():
|
||||
if key == "total":
|
||||
continue
|
||||
try:
|
||||
seconds = float(value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if seconds >= 0.05:
|
||||
rows.append((labels.get(str(key), str(key)), seconds))
|
||||
rows.sort(key=lambda item: item[1], reverse=True)
|
||||
parts = [f"{label} {seconds:.1f}s" for label, seconds in rows[:limit]]
|
||||
try:
|
||||
total = float(timings.get("total"))
|
||||
except (TypeError, ValueError):
|
||||
total = 0.0
|
||||
if total >= 0.05:
|
||||
parts.append(f"总计 {total:.1f}s")
|
||||
return ",".join(parts)
|
||||
|
||||
|
||||
class WindowCoreMixin:
|
||||
@Slot(object)
|
||||
def _run_ui_task(self, callback) -> None:
|
||||
callback()
|
||||
|
||||
def _empty_edge_polydata(self):
|
||||
return _empty_edge_polydata()
|
||||
|
||||
def showEvent(self, event) -> None:
|
||||
super().showEvent(event)
|
||||
if getattr(self, "first_show_handled", False):
|
||||
@@ -62,7 +110,7 @@ class WindowCoreMixin:
|
||||
@Slot()
|
||||
def _after_first_show(self) -> None:
|
||||
self._ensure_vtk_interactor_started()
|
||||
if getattr(self, "auto_load_on_show", False):
|
||||
if getattr(self, "auto_load_on_show", False) and getattr(self, "step_path", None) is not None:
|
||||
self.auto_load_on_show = False
|
||||
self.load_step(self.step_path, background=True)
|
||||
|
||||
@@ -927,23 +975,41 @@ class WindowCoreMixin:
|
||||
deflection: float,
|
||||
show_internal_edges: bool,
|
||||
build_polydata: bool = True,
|
||||
build_edges: bool = True,
|
||||
) -> dict[str, object]:
|
||||
timings: dict[str, float] = {}
|
||||
total_started = time.perf_counter()
|
||||
started = time.perf_counter()
|
||||
new_model = StepModel.load(path)
|
||||
timings["step_load"] = time.perf_counter() - started
|
||||
started = time.perf_counter()
|
||||
stats = new_model.stats()
|
||||
timings["stats"] = time.perf_counter() - started
|
||||
result = {
|
||||
"path": path,
|
||||
"model": new_model,
|
||||
"stats": stats,
|
||||
"deflection": deflection,
|
||||
"show_internal_edges": show_internal_edges,
|
||||
"timings": timings,
|
||||
}
|
||||
if build_polydata:
|
||||
started = time.perf_counter()
|
||||
face_polydata = new_model.build_face_polydata(deflection=deflection)
|
||||
result["model_polydata"] = _smooth_surface_polydata(face_polydata)
|
||||
result["edge_polydata"] = new_model.build_edge_polydata(
|
||||
deflection=deflection,
|
||||
show_same_domain_internal_edges=show_internal_edges,
|
||||
)
|
||||
timings["display_faces"] = time.perf_counter() - started
|
||||
if build_edges:
|
||||
started = time.perf_counter()
|
||||
result["edge_polydata"] = new_model.build_edge_polydata(
|
||||
deflection=deflection,
|
||||
show_same_domain_internal_edges=show_internal_edges,
|
||||
)
|
||||
timings["display_edges"] = time.perf_counter() - started
|
||||
else:
|
||||
result["edge_polydata"] = _empty_edge_polydata()
|
||||
result["edge_polydata_deferred"] = True
|
||||
timings["display_edges"] = 0.0
|
||||
timings["total"] = time.perf_counter() - total_started
|
||||
return result
|
||||
|
||||
def _load_step_background_or_sync(self, path: str | Path, *, background: bool) -> None:
|
||||
@@ -995,6 +1061,7 @@ class WindowCoreMixin:
|
||||
deflection: float,
|
||||
show_internal_edges: bool,
|
||||
status_prefix: str,
|
||||
defer_edges: bool = True,
|
||||
) -> bool:
|
||||
self.statusBar().showMessage(f"{status_prefix} {new_path.name}...")
|
||||
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
||||
@@ -1004,6 +1071,7 @@ class WindowCoreMixin:
|
||||
new_path,
|
||||
deflection=deflection,
|
||||
show_internal_edges=show_internal_edges,
|
||||
build_edges=not defer_edges,
|
||||
)
|
||||
result["display"] = "ready"
|
||||
except Exception as exc:
|
||||
@@ -1013,7 +1081,9 @@ class WindowCoreMixin:
|
||||
finally:
|
||||
QApplication.restoreOverrideCursor()
|
||||
self._apply_loaded_model_result(result, reset_camera=True)
|
||||
self.statusBar().showMessage(f"Loaded {self.step_path.name}")
|
||||
timing_text = _timing_summary(result.get("timings") if isinstance(result, dict) else None)
|
||||
suffix = f";{timing_text}" if timing_text else ""
|
||||
self.statusBar().showMessage(f"Loaded {self.step_path.name}{suffix}")
|
||||
return True
|
||||
|
||||
@Slot(object)
|
||||
@@ -1026,6 +1096,7 @@ class WindowCoreMixin:
|
||||
deflection=self.preview_load_deflection,
|
||||
show_internal_edges=self._show_same_domain_internal_edges(),
|
||||
status_prefix="读取 STEP 可视化网格",
|
||||
defer_edges=True,
|
||||
)
|
||||
finally:
|
||||
self._end_load_task()
|
||||
@@ -1047,6 +1118,7 @@ class WindowCoreMixin:
|
||||
return detached
|
||||
|
||||
def _apply_loaded_model_result(self, result: dict[str, object], *, reset_camera: bool) -> None:
|
||||
apply_started = time.perf_counter()
|
||||
new_path = Path(result["path"])
|
||||
stats = result["stats"]
|
||||
self.model = result["model"]
|
||||
@@ -1062,7 +1134,8 @@ class WindowCoreMixin:
|
||||
self._reset_selection()
|
||||
model_polydata = result.get("model_polydata")
|
||||
edge_polydata = result.get("edge_polydata")
|
||||
if model_polydata is None or edge_polydata is None:
|
||||
edge_deferred = bool(result.get("edge_polydata_deferred"))
|
||||
if model_polydata is None or (edge_polydata is None and not edge_deferred):
|
||||
deflection = float(result.get("deflection", 0.8))
|
||||
show_internal_edges = bool(result.get("show_internal_edges", self._show_same_domain_internal_edges()))
|
||||
model_polydata = self.model.build_face_polydata(deflection=deflection)
|
||||
@@ -1070,6 +1143,8 @@ class WindowCoreMixin:
|
||||
deflection=deflection,
|
||||
show_same_domain_internal_edges=show_internal_edges,
|
||||
)
|
||||
elif edge_polydata is None:
|
||||
edge_polydata = _empty_edge_polydata()
|
||||
self._rebuild_scene_from_polydata(
|
||||
model_polydata,
|
||||
edge_polydata,
|
||||
@@ -1077,18 +1152,28 @@ class WindowCoreMixin:
|
||||
)
|
||||
self._clear_editable_candidates()
|
||||
self._clear_cylinder_candidates()
|
||||
self.set_info(
|
||||
{
|
||||
"file": str(self.step_path),
|
||||
"parts": stats.parts,
|
||||
"solids": stats.solids,
|
||||
"faces": stats.faces,
|
||||
"edges": stats.edges,
|
||||
"vertices": stats.vertices,
|
||||
"display": result.get("display", "quick preview" if self.load_in_progress else "ready"),
|
||||
}
|
||||
)
|
||||
timings = result.get("timings")
|
||||
if isinstance(timings, dict):
|
||||
timings["apply_loaded"] = time.perf_counter() - apply_started
|
||||
display_state = result.get("display", "quick preview" if self.load_in_progress else "ready")
|
||||
if edge_deferred:
|
||||
display_state = f"{display_state}; edge display pending"
|
||||
info_payload = {
|
||||
"file": str(self.step_path),
|
||||
"parts": stats.parts,
|
||||
"solids": stats.solids,
|
||||
"faces": stats.faces,
|
||||
"edges": stats.edges,
|
||||
"vertices": stats.vertices,
|
||||
"display": display_state,
|
||||
}
|
||||
timing_text = _timing_summary(timings if isinstance(timings, dict) else None)
|
||||
if timing_text:
|
||||
info_payload["load_performance"] = timing_text
|
||||
self.set_info(info_payload)
|
||||
self._update_action_states()
|
||||
if edge_deferred:
|
||||
QTimer.singleShot(80, self._rebuild_deferred_edge_display)
|
||||
|
||||
@Slot(object)
|
||||
def _finish_initial_load(self, result: object) -> None:
|
||||
@@ -1177,10 +1262,12 @@ class WindowCoreMixin:
|
||||
def open_step(self) -> None:
|
||||
if self._edit_busy("请等待当前编辑完成后再打开文件。"):
|
||||
return
|
||||
current_path = getattr(self, "step_path", None)
|
||||
start_dir = current_path.parent if isinstance(current_path, Path) else Path.cwd()
|
||||
path, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"打开 STEP 文件",
|
||||
str(self.step_path.parent if self.step_path else Path.cwd()),
|
||||
str(start_dir),
|
||||
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
|
||||
)
|
||||
if path:
|
||||
@@ -1200,6 +1287,9 @@ class WindowCoreMixin:
|
||||
return
|
||||
path_text = self.path_label.text().strip() if hasattr(self, "path_label") else ""
|
||||
path = Path(path_text) if path_text else self.step_path
|
||||
if path is None:
|
||||
self.statusBar().showMessage("请先点击“导入几何模型”选择 STEP 文件。")
|
||||
return
|
||||
self._ensure_vtk_interactor_started()
|
||||
self.load_step(path)
|
||||
|
||||
@@ -1620,6 +1710,50 @@ class WindowCoreMixin:
|
||||
edge_id = int(self.edge_id_array.GetValue(cell_id))
|
||||
self.edge_cell_ids_by_edge.setdefault(edge_id, []).append(cell_id)
|
||||
|
||||
def _install_edge_polydata(self, edge_polydata, *, render: bool = True) -> None:
|
||||
if edge_polydata is None:
|
||||
edge_polydata = _empty_edge_polydata()
|
||||
old_actor = getattr(self, "edge_actor", None)
|
||||
if old_actor is not None:
|
||||
try:
|
||||
self.renderer.RemoveActor(old_actor)
|
||||
except Exception:
|
||||
pass
|
||||
self.edge_polydata = edge_polydata
|
||||
self.edge_id_array = self.edge_polydata.GetCellData().GetArray("edge_id")
|
||||
self._rebuild_edge_polydata_cell_index()
|
||||
edge_mapper = vtk.vtkPolyDataMapper()
|
||||
edge_mapper.SetInputData(self.edge_polydata)
|
||||
_prepare_static_mapper(edge_mapper)
|
||||
self.edge_actor = vtk.vtkActor()
|
||||
self.edge_actor.SetMapper(edge_mapper)
|
||||
self.edge_actor.GetProperty().SetColor(0.08, 0.09, 0.1)
|
||||
self.edge_actor.GetProperty().SetLineWidth(1.0)
|
||||
self.edge_actor.GetProperty().LightingOff()
|
||||
self.renderer.AddActor(self.edge_actor)
|
||||
self.edge_overlay_polydata_cache.clear()
|
||||
if render:
|
||||
self.render_window.Render()
|
||||
|
||||
@Slot()
|
||||
def _rebuild_deferred_edge_display(self) -> None:
|
||||
if self.model is None or self.operation_in_progress or self.load_in_progress:
|
||||
return
|
||||
started = time.perf_counter()
|
||||
self.statusBar().showMessage("模型已显示,正在补充边线...")
|
||||
QApplication.processEvents()
|
||||
try:
|
||||
edge_polydata = self.model.build_edge_polydata(
|
||||
deflection=float(getattr(self, "preview_load_deflection", 0.35) or 0.35),
|
||||
show_same_domain_internal_edges=self._show_same_domain_internal_edges(),
|
||||
)
|
||||
self._install_edge_polydata(edge_polydata, render=True)
|
||||
except Exception as exc:
|
||||
self.statusBar().showMessage(f"模型已显示;边线补充失败:{exc}")
|
||||
return
|
||||
elapsed = time.perf_counter() - started
|
||||
self.statusBar().showMessage(f"模型边线已补充,用时 {elapsed:.1f}s")
|
||||
|
||||
def _remember_overlay_cache_item(self, cache: dict, key: object, value: object) -> object:
|
||||
if len(cache) >= self.overlay_cache_limit:
|
||||
try:
|
||||
@@ -1744,18 +1878,8 @@ class WindowCoreMixin:
|
||||
self.model_actor.SetBackfaceProperty(backface_property)
|
||||
self.renderer.AddActor(self.model_actor)
|
||||
|
||||
self.edge_polydata = edge_polydata
|
||||
self.edge_id_array = self.edge_polydata.GetCellData().GetArray("edge_id")
|
||||
self._rebuild_edge_polydata_cell_index()
|
||||
edge_mapper = vtk.vtkPolyDataMapper()
|
||||
edge_mapper.SetInputData(self.edge_polydata)
|
||||
_prepare_static_mapper(edge_mapper)
|
||||
self.edge_actor = vtk.vtkActor()
|
||||
self.edge_actor.SetMapper(edge_mapper)
|
||||
self.edge_actor.GetProperty().SetColor(0.08, 0.09, 0.1)
|
||||
self.edge_actor.GetProperty().SetLineWidth(1.0)
|
||||
self.edge_actor.GetProperty().LightingOff()
|
||||
self.renderer.AddActor(self.edge_actor)
|
||||
self.edge_actor = None
|
||||
self._install_edge_polydata(edge_polydata, render=False)
|
||||
|
||||
self.highlight_actor = None
|
||||
self.edge_highlight_actor = None
|
||||
|
||||
+176
-27
@@ -7,6 +7,7 @@ import time
|
||||
from PySide6.QtCore import Qt, QThread, QTimer, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QFileDialog,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
@@ -36,8 +37,15 @@ PROPERTY_LABEL_COLUMN = 0
|
||||
PROPERTY_CURRENT_COLUMN = 1
|
||||
PROPERTY_SCOPE_COLUMN = 2
|
||||
PROPERTY_TARGET_COLUMN = 3
|
||||
PROPERTY_TABLE_MIN_COLUMN_WIDTHS = (72, 118, 72, 82)
|
||||
PROPERTY_TABLE_PREFERRED_COLUMN_WIDTHS = (118, 168, 108, 104)
|
||||
PROPERTY_INPUT_COLUMN = 4
|
||||
PROPERTY_TABLE_HEADERS = ("尺寸参数", "当前值", "建模意图", "目标值", "输入参数")
|
||||
PROPERTY_TABLE_MIN_COLUMN_WIDTHS = (70, 72, 66, 62, 50)
|
||||
PROPERTY_TABLE_PREFERRED_COLUMN_WIDTHS = (108, 104, 88, 82, 66)
|
||||
PROPERTY_TEMPORARILY_HIDDEN_PARAMETER_KEYS = {
|
||||
"face_center_position",
|
||||
"edge_center_point",
|
||||
}
|
||||
PROPERTY_TEMPORARILY_HIDDEN_PARAMETER_LABELS = {"中心"}
|
||||
PROPERTY_COMMAND_ORDER = ("offset", "move", "scale", "rotate", "feature", "diagnostics")
|
||||
PROPERTY_COMMAND_LABELS = {
|
||||
"offset": "偏移",
|
||||
@@ -98,7 +106,7 @@ PROPERTY_EXPLANATION_TOOLTIPS = {
|
||||
}
|
||||
|
||||
|
||||
def _property_table_column_widths(available_width: int) -> tuple[int, int, int, int]:
|
||||
def _property_table_column_widths(available_width: int) -> tuple[int, ...]:
|
||||
"""Prefer current value visibility while keeping modeling intent and target usable."""
|
||||
column_count = len(PROPERTY_TABLE_MIN_COLUMN_WIDTHS)
|
||||
available = max(int(available_width or 0), column_count * 44)
|
||||
@@ -110,7 +118,7 @@ def _property_table_column_widths(available_width: int) -> tuple[int, int, int,
|
||||
if available >= preferred_total:
|
||||
widths = [int(value) for value in preferred]
|
||||
extra = available - preferred_total
|
||||
weights = (0.26, 0.38, 0.18, 0.18)
|
||||
weights = (0.30, 0.24, 0.18, 0.16, 0.12)
|
||||
for index, weight in enumerate(weights):
|
||||
addition = int(extra * weight)
|
||||
widths[index] += addition
|
||||
@@ -126,10 +134,10 @@ def _property_table_column_widths(available_width: int) -> tuple[int, int, int,
|
||||
widths[1] += available - sum(widths)
|
||||
return tuple(widths) # type: ignore[return-value]
|
||||
|
||||
floors = (48, 96, 44, 56)
|
||||
floors = (44, 44, 44, 44, 44)
|
||||
widths = [int(value) for value in minimum]
|
||||
deficit = min_total - available
|
||||
for index in (0, 2, 3, 1):
|
||||
for index in (0, 2, 3, 1, 4):
|
||||
if deficit <= 0:
|
||||
break
|
||||
reducible = max(0, widths[index] - floors[index])
|
||||
@@ -149,6 +157,16 @@ def _compact_property_card_text(text: str, limit: int = 220) -> str:
|
||||
return f"{compact[: max(0, limit - 1)].rstrip()}…"
|
||||
|
||||
|
||||
def _property_parameter_is_visible(spec: dict[str, object]) -> bool:
|
||||
key = str(spec.get("key", "") or "")
|
||||
label = str(spec.get("label", "") or "").strip()
|
||||
if key in PROPERTY_TEMPORARILY_HIDDEN_PARAMETER_KEYS:
|
||||
return False
|
||||
if label in PROPERTY_TEMPORARILY_HIDDEN_PARAMETER_LABELS:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _feature_dimension_keys(action_info: dict[str, object]) -> tuple[str, ...]:
|
||||
"""Return the independent, user-facing dimensions for a feature candidate."""
|
||||
surface = str(action_info.get("surface", "") or "")
|
||||
@@ -817,7 +835,7 @@ class WindowStateMixin:
|
||||
self._set_control_state(
|
||||
self.quick_export_all_button,
|
||||
has_model,
|
||||
"导出当前完整 STEP 模型。",
|
||||
"导出当前模型。",
|
||||
wait_or_load_tip,
|
||||
)
|
||||
if hasattr(self, "part_tree"):
|
||||
@@ -852,7 +870,7 @@ class WindowStateMixin:
|
||||
self._set_control_state(
|
||||
self.export_all_button,
|
||||
has_model,
|
||||
"导出当前完整 STEP 模型。",
|
||||
"导出当前模型。",
|
||||
wait_or_load_tip,
|
||||
)
|
||||
self._set_control_state(
|
||||
@@ -1548,11 +1566,11 @@ class WindowStateMixin:
|
||||
headline = "当前支持:Face、孔、槽、凸台、圆角/倒角、壳体、Edge"
|
||||
detail = ""
|
||||
tooltip = (
|
||||
"Face:面内长度/宽度、中心、偏移、壳体厚度。\n"
|
||||
"Face:面内长度/宽度、偏移、壳体厚度。\n"
|
||||
"孔/槽:孔径、轴心、封堵、盲孔/盲槽深度、槽宽/槽深/弧长/总长。\n"
|
||||
"凸台:圆柱凸台直径/高度/轴心;矩形凸台/矩形槽口袋长宽、中心、高度/深度;多台阶矩形凸台顶层规则台阶。\n"
|
||||
"凸台:圆柱凸台直径/高度/轴心;矩形凸台/矩形槽口袋长宽、高度/深度;多台阶矩形凸台顶层规则台阶。\n"
|
||||
"圆角/倒角:简单已有圆角半径/弧长、简单等半径圆角链半径/弧长、已有等距倒角距离,直线 Edge 新增圆角/倒角。\n"
|
||||
"Edge/解析曲面:直线 Edge 长度/端点/中心、圆/椭圆 Edge、简单圆锥/球/环面。\n"
|
||||
"Edge/解析曲面:直线 Edge 长度/端点、圆/椭圆 Edge、简单圆锥/球/环面。\n"
|
||||
"一级关系:Face/Edge 平行垂直事实、Face 共面/同域碎片、一级同轴圆柱事实;部分 Face/Edge 操作可选择保持关系。\n"
|
||||
"受限:复杂链式特征、二级/三级拓扑传播和原 CAD 历史恢复。"
|
||||
)
|
||||
@@ -1985,7 +2003,9 @@ class WindowStateMixin:
|
||||
editable=False,
|
||||
)
|
||||
target_item.setToolTip(self._property_target_tooltip(effective_spec, editable=editable))
|
||||
row_items = (label_item, current_item, scope_item, target_item)
|
||||
input_item = self._property_table_item("", editable=False)
|
||||
input_item.setToolTip("勾选后点击“导出参数”,会把这一行作为输入参数写入 data.json。")
|
||||
row_items = (label_item, current_item, scope_item, target_item, input_item)
|
||||
self._style_property_row_items(row_items, editable=editable, spec=effective_spec)
|
||||
for column, item in enumerate(row_items):
|
||||
item.setToolTip(item.toolTip() or item.text())
|
||||
@@ -2002,6 +2022,10 @@ class WindowStateMixin:
|
||||
self.property_table.removeCellWidget(row, PROPERTY_TARGET_COLUMN)
|
||||
else:
|
||||
self.property_table.removeCellWidget(row, PROPERTY_TARGET_COLUMN)
|
||||
self._set_property_input_checkbox(
|
||||
row,
|
||||
exportable=bool(input_editable and effective_spec.get("label")),
|
||||
)
|
||||
self._clear_property_command_bar()
|
||||
self._clear_property_cards()
|
||||
self._update_current_capability_panel()
|
||||
@@ -2032,12 +2056,13 @@ class WindowStateMixin:
|
||||
|
||||
def _style_property_row_items(
|
||||
self,
|
||||
items: tuple[QTableWidgetItem, QTableWidgetItem, QTableWidgetItem, QTableWidgetItem],
|
||||
items: tuple[QTableWidgetItem, ...],
|
||||
*,
|
||||
editable: bool,
|
||||
spec: dict[str, object] | None = None,
|
||||
) -> None:
|
||||
label_item, current_item, scope_item, target_item = items
|
||||
label_item, current_item, scope_item, target_item = items[:4]
|
||||
input_item = items[4] if len(items) > 4 else None
|
||||
if spec is not None and bool(spec.get("pin_top")):
|
||||
for item in items:
|
||||
item.setBackground(QColor("#eef6ff"))
|
||||
@@ -2045,6 +2070,8 @@ class WindowStateMixin:
|
||||
current_item.setForeground(QColor("#0f172a"))
|
||||
scope_item.setForeground(QColor("#0369a1"))
|
||||
target_item.setForeground(QColor("#64748b"))
|
||||
if input_item is not None:
|
||||
input_item.setForeground(QColor("#64748b"))
|
||||
label_font = label_item.font()
|
||||
label_font.setBold(True)
|
||||
label_item.setFont(label_font)
|
||||
@@ -2053,13 +2080,15 @@ class WindowStateMixin:
|
||||
current_item.setFont(current_font)
|
||||
return
|
||||
if editable:
|
||||
row_backgrounds = ("#fff7ed", "#fffbeb", "#fff7ed", "#fff7ed")
|
||||
row_backgrounds = ("#fff7ed", "#fffbeb", "#fff7ed", "#fff7ed", "#fff7ed")
|
||||
for item, color in zip(items, row_backgrounds):
|
||||
item.setBackground(QColor(color))
|
||||
label_item.setForeground(QColor("#7c2d12"))
|
||||
current_item.setForeground(QColor("#431407"))
|
||||
scope_item.setForeground(QColor("#9a3412"))
|
||||
target_item.setForeground(QColor("#111827"))
|
||||
if input_item is not None:
|
||||
input_item.setForeground(QColor("#7c2d12"))
|
||||
label_font = label_item.font()
|
||||
label_font.setBold(True)
|
||||
label_item.setFont(label_font)
|
||||
@@ -2070,6 +2099,9 @@ class WindowStateMixin:
|
||||
scope_item.setForeground(QColor("#64748b"))
|
||||
target_item.setBackground(QColor("#eef2f6"))
|
||||
target_item.setForeground(QColor("#8f99a8"))
|
||||
if input_item is not None:
|
||||
input_item.setBackground(QColor("#eef2f6"))
|
||||
input_item.setForeground(QColor("#8f99a8"))
|
||||
|
||||
def _property_scope_default(self, spec: dict[str, object]) -> str:
|
||||
modes = spec.get("scope_modes")
|
||||
@@ -2145,6 +2177,39 @@ class WindowStateMixin:
|
||||
editor.returnPressed.connect(self.apply_current_property_edit)
|
||||
self.property_table.setCellWidget(row, PROPERTY_TARGET_COLUMN, editor)
|
||||
|
||||
def _set_property_input_checkbox(self, row: int, *, exportable: bool) -> None:
|
||||
if not hasattr(self, "property_table"):
|
||||
return
|
||||
container = QWidget()
|
||||
container.setObjectName("propertyInputParameterCell")
|
||||
layout = QHBoxLayout(container)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
checkbox = QCheckBox(container)
|
||||
checkbox.setObjectName("propertyInputParameterCheckbox")
|
||||
checkbox.setChecked(False)
|
||||
checkbox.setEnabled(exportable)
|
||||
checkbox.setCursor(Qt.CursorShape.PointingHandCursor if exportable else Qt.CursorShape.ArrowCursor)
|
||||
tip = "勾选后点击“导出参数”,会把这一行作为输入参数写入 data.json。"
|
||||
if not exportable:
|
||||
tip = "当前行不是可导出的尺寸输入参数。"
|
||||
checkbox.setToolTip(tip)
|
||||
container.setToolTip(tip)
|
||||
checkbox.toggled.connect(lambda _checked=False: self._update_parameter_export_state())
|
||||
layout.addWidget(checkbox)
|
||||
self.property_table.setCellWidget(row, PROPERTY_INPUT_COLUMN, container)
|
||||
|
||||
def _property_input_checkbox(self, row: int) -> QCheckBox | None:
|
||||
if not hasattr(self, "property_table"):
|
||||
return None
|
||||
widget = self.property_table.cellWidget(row, PROPERTY_INPUT_COLUMN)
|
||||
if isinstance(widget, QCheckBox):
|
||||
return widget
|
||||
if isinstance(widget, QWidget):
|
||||
return widget.findChild(QCheckBox)
|
||||
return None
|
||||
|
||||
def _set_property_scope_editor(self, row: int, spec: dict[str, object]) -> None:
|
||||
modes = spec.get("scope_modes")
|
||||
if not isinstance(modes, dict) or not modes:
|
||||
@@ -2213,6 +2278,23 @@ class WindowStateMixin:
|
||||
target_item = self.property_table.item(row, PROPERTY_TARGET_COLUMN)
|
||||
if target_item is not None:
|
||||
target_item.setToolTip(self._property_target_tooltip(effective_spec, editable=editable))
|
||||
checkbox = self._property_input_checkbox(row)
|
||||
if checkbox is not None:
|
||||
exportable = bool(input_editable and effective_spec.get("label"))
|
||||
if not exportable and checkbox.isChecked():
|
||||
was_blocked = checkbox.blockSignals(True)
|
||||
try:
|
||||
checkbox.setChecked(False)
|
||||
finally:
|
||||
checkbox.blockSignals(was_blocked)
|
||||
tip = (
|
||||
"勾选后点击“导出参数”,会把这一行作为输入参数写入 data.json。"
|
||||
if exportable
|
||||
else "当前行不是可导出的尺寸输入参数。"
|
||||
)
|
||||
checkbox.setEnabled(exportable)
|
||||
checkbox.setCursor(Qt.CursorShape.PointingHandCursor if exportable else Qt.CursorShape.ArrowCursor)
|
||||
checkbox.setToolTip(tip)
|
||||
self._update_current_capability_panel()
|
||||
self._update_property_apply_state()
|
||||
|
||||
@@ -2345,6 +2427,7 @@ class WindowStateMixin:
|
||||
and bool(spec.get("enabled"))
|
||||
and bool(spec.get("action"))
|
||||
and str(spec.get("value_type", "number")) != "command"
|
||||
and _property_parameter_is_visible(spec)
|
||||
]
|
||||
|
||||
def _feature_property_specs(
|
||||
@@ -2381,6 +2464,7 @@ class WindowStateMixin:
|
||||
or not bool(spec.get("editable"))
|
||||
or (not bool(spec.get("enabled")) and not prismatic_size_key and not prismatic_center_key)
|
||||
or str(spec.get("value_type", "number")) == "command"
|
||||
or not _property_parameter_is_visible(spec)
|
||||
):
|
||||
continue
|
||||
dimension = dict(spec)
|
||||
@@ -4546,7 +4630,7 @@ class WindowStateMixin:
|
||||
"action": "resize_boss_height",
|
||||
"target_attr": "boss_height_input",
|
||||
"enabled": boss_height_can_local,
|
||||
"enabled_tip": "输入完整圆柱凸台的目标高度;点击修改时程序会再计算并确认可拉伸/切除的凸台端盖 Face。",
|
||||
"enabled_tip": "输入完整圆柱凸台的目标高度;点击修改时程序会再计算并校验可拉伸/切除的凸台端盖 Face。",
|
||||
"disabled_tip": "当前凸台候选缺少稳定高度或端盖信息,暂不放行高度修改。",
|
||||
"range_hint": relative_range_hint(current_boss_height, 0.3, 0.8),
|
||||
},
|
||||
@@ -4663,7 +4747,7 @@ class WindowStateMixin:
|
||||
"action": "resize_cylinder_height",
|
||||
"target_attr": "boss_height_input",
|
||||
"enabled": bool(is_full_cylinder and current_cylinder_height is not None),
|
||||
"enabled_tip": "输入完整圆柱面的目标高度;点击修改时程序会再计算并确认可拉伸/切除的圆柱端盖 Face。",
|
||||
"enabled_tip": "输入完整圆柱面的目标高度;点击修改时程序会再计算并校验可拉伸/切除的圆柱端盖 Face。",
|
||||
"disabled_tip": "当前圆柱面不是完整圆柱,或缺少稳定高度信息。",
|
||||
"range_hint": relative_range_hint(current_cylinder_height, 0.3, 0.8),
|
||||
},
|
||||
@@ -5775,17 +5859,70 @@ class WindowStateMixin:
|
||||
card.style().unpolish(card)
|
||||
card.style().polish(card)
|
||||
setattr(widget, "_geom_param_button_state", button_state)
|
||||
if not hasattr(self, "apply_property_button"):
|
||||
if hasattr(self, "apply_property_button"):
|
||||
changed = self._changed_property_rows()
|
||||
enabled = bool(has_model and changed)
|
||||
disabled_tip = "请先选择对象,并在属性表中修改一个可编辑目标值。"
|
||||
if changed and len(changed) > 1:
|
||||
disabled_tip = "当前一次只执行一个几何修改;请只保留一行目标值不同,再点击参数化建模。"
|
||||
self._set_control_state(
|
||||
self.apply_property_button,
|
||||
enabled and len(changed) == 1,
|
||||
"应用当前被修改的参数。",
|
||||
disabled_tip,
|
||||
)
|
||||
self._update_parameter_export_state(has_model)
|
||||
|
||||
def _selected_parameter_export_rows(self) -> list[dict[str, str]]:
|
||||
if not hasattr(self, "property_table"):
|
||||
return []
|
||||
rows: list[dict[str, str]] = []
|
||||
specs = getattr(self, "property_editor_specs", [])
|
||||
row_count = min(self.property_table.rowCount(), len(specs))
|
||||
for row in range(row_count):
|
||||
checkbox = self._property_input_checkbox(row)
|
||||
if checkbox is None or not checkbox.isEnabled() or not checkbox.isChecked():
|
||||
continue
|
||||
spec = self._effective_property_spec(specs[row], row=row)
|
||||
if str(spec.get("value_type", "number")) == "command":
|
||||
continue
|
||||
label_item = self.property_table.item(row, PROPERTY_LABEL_COLUMN)
|
||||
current_item = self.property_table.item(row, PROPERTY_CURRENT_COLUMN)
|
||||
label = (label_item.text() if label_item is not None else str(spec.get("label", ""))).strip()
|
||||
current = (
|
||||
current_item.text()
|
||||
if current_item is not None
|
||||
else str(spec.get("current_text", spec.get("current_raw", "")))
|
||||
).strip()
|
||||
if not label:
|
||||
continue
|
||||
rows.append(
|
||||
{
|
||||
"name": label,
|
||||
"displayName": label,
|
||||
"type": "number",
|
||||
"ioRole": "input",
|
||||
"default": current,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
def _update_parameter_export_state(self, has_model: bool | None = None) -> None:
|
||||
if not hasattr(self, "export_parameters_button"):
|
||||
return
|
||||
changed = self._changed_property_rows()
|
||||
enabled = bool(has_model and changed)
|
||||
disabled_tip = "请先选择对象,并在属性表中修改一个可编辑目标值。"
|
||||
if changed and len(changed) > 1:
|
||||
disabled_tip = "当前一次只执行一个几何修改;请只保留一行目标值不同,再点击参数化建模。"
|
||||
if has_model is None:
|
||||
has_model = self.model is not None and not (
|
||||
self.operation_in_progress or self.scan_in_progress or self.load_in_progress
|
||||
)
|
||||
selected_rows = self._selected_parameter_export_rows()
|
||||
if selected_rows:
|
||||
disabled_tip = "请等待当前后台任务完成后再导出参数。"
|
||||
else:
|
||||
disabled_tip = "请先在“输入参数”列勾选至少一个尺寸参数。"
|
||||
self._set_control_state(
|
||||
self.apply_property_button,
|
||||
enabled and len(changed) == 1,
|
||||
"应用当前被修改的参数。",
|
||||
self.export_parameters_button,
|
||||
bool(has_model and selected_rows),
|
||||
f"导出已勾选的 {len(selected_rows)} 个输入参数到 data.json。",
|
||||
disabled_tip,
|
||||
)
|
||||
|
||||
@@ -6060,7 +6197,19 @@ class WindowStateMixin:
|
||||
if action is None:
|
||||
QMessageBox.information(self, "暂不支持", f"当前属性没有可用的执行入口:{action_name}")
|
||||
return
|
||||
action()
|
||||
self._run_parametric_property_action_directly(action)
|
||||
|
||||
def _run_parametric_property_action_directly(self, action) -> None:
|
||||
original_question = QMessageBox.question
|
||||
|
||||
def auto_yes(*_args, **_kwargs):
|
||||
return QMessageBox.StandardButton.Yes
|
||||
|
||||
try:
|
||||
QMessageBox.question = auto_yes
|
||||
action()
|
||||
finally:
|
||||
QMessageBox.question = original_question
|
||||
|
||||
def set_manual_hole_bottom_face(self) -> None:
|
||||
self._update_action_states()
|
||||
|
||||
Reference in New Issue
Block a user