feat: 完善一级关系参数化建模与参数导出
This commit is contained in:
+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:
|
||||
|
||||
Reference in New Issue
Block a user