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
+165 -19
View File
@@ -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: