feat: 完善一级关系参数化建模与参数导出

This commit is contained in:
2026-08-11 18:28:16 +08:00
parent 6cb99a1273
commit 19364d81b5
20 changed files with 917 additions and 167 deletions
+155 -31
View File
@@ -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