feat: 推进一级关系参数化编辑与参数导出

This commit is contained in:
2026-08-13 17:50:20 +08:00
parent 19364d81b5
commit 7d814d2939
21 changed files with 2075 additions and 231 deletions
+98 -8
View File
@@ -30,6 +30,7 @@ from .geometry_utils import (
_tuple_sub,
_vector_length,
)
from .parametric_component import component_name_from_step, default_component_root, export_parametric_component
from .ui_helpers import * # noqa: F403
from .workers import EditWorker, LoadWorker, ScanWorker
@@ -122,6 +123,22 @@ def _edit_timing_summary(timings: object, *, limit: int = 5) -> str:
return "".join(parts)
def _start_background_thread(thread: QThread, priority: QThread.Priority = QThread.Priority.LowPriority) -> None:
try:
thread.start(priority)
except TypeError:
thread.start()
def _isolated_process_creation_flags() -> int:
if sys.platform != "win32":
return 0
flags = 0
for name in ("CREATE_NO_WINDOW", "BELOW_NORMAL_PRIORITY_CLASS"):
flags |= int(getattr(subprocess, name, 0))
return flags
class WindowActionMixin:
def _empty_edge_polydata(self):
polydata = vtk.vtkPolyData()
@@ -151,11 +168,49 @@ class WindowActionMixin:
except OSError as exc:
QMessageBox.warning(self, "导出参数失败", f"无法写入 {output_path.name}{exc}")
return
self.statusBar().showMessage(f"已导出 {len(rows)} 个输入参数到 {output_path.name}")
component_path: Path | None = None
component_warning = ""
try:
component_path = self._export_parametric_component_main(rows)
except Exception as exc:
component_warning = str(exc)
if component_path is not None:
self.statusBar().showMessage(f"已导出 {len(rows)} 个输入参数,并生成组件 {component_path.parent.name}")
else:
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}")
lines = [
f"已导出参数文件:{output_path}",
f"参数数量:{len(rows)}",
f"参数:{names}{suffix}",
]
if component_path is not None:
lines.extend(
[
f"已生成参数化组件:{component_path.parent}",
f"组件入口:{component_path.name}",
]
)
elif component_warning:
lines.append(f"组件生成未完成:{component_warning}")
self.set_plain_info("\n".join(lines))
def _export_parametric_component_main(self, rows: list[dict[str, str]]) -> Path:
edits = self._selected_parameter_component_edits(rows) if hasattr(self, "_selected_parameter_component_edits") else []
if not edits:
raise ValueError("当前勾选参数还不能映射为可执行 STEP 编辑操作。")
source_step = self.step_path if isinstance(getattr(self, "step_path", None), Path) else None
if source_step is None or not source_step.is_file():
raise ValueError("当前模型缺少可复用的 STEP 文件路径,请先导入 STEP 模型。")
return export_parametric_component(
parameters=rows,
edits=edits,
source_step=source_step,
component_root=default_component_root(Path(__file__).resolve().parent.parent),
component_name=component_name_from_step(source_step),
)
def export_all(self) -> None:
if self.model is None:
@@ -488,7 +543,7 @@ class WindowActionMixin:
def action():
if keep_relations:
return self.model.push_pull_face_keep_relations(face_id, distance)
return self.model.push_pull_face(face_id, distance)
return self.model.push_pull_face(face_id, distance, plan=dict(plan))
self._run_edit_action(
action,
@@ -501,8 +556,11 @@ class WindowActionMixin:
"distance_rule": "positive=outward fuse, negative=inward cut",
"surface": plan.get("surface"),
"outward_direction": plan.get("outward_direction"),
"plane_direction": plan.get("plane_direction"),
"current_plane_position": plan.get("current_plane_position"),
"target_plane_position": plan.get("target_plane_position"),
"current_plane_center": plan.get("current_plane_center"),
"target_plane_center": plan.get("target_plane_center"),
"direction_confidence": plan.get("direction_confidence"),
"direction_note": plan.get("direction_note"),
"resize_strategy": plan.get("resize_strategy"),
@@ -1413,7 +1471,11 @@ class WindowActionMixin:
height_estimate = _float_or_none(info.get("hole_depth_estimate"))
guess = str(info.get("feature_guess", "cylindrical face"))
confidence = str(info.get("confidence", "low"))
angular_span = _float_or_none(info.get("angular_span"))
angular_span = _float_or_none(info.get("same_domain_angular_span"))
if angular_span is None:
angular_span = _float_or_none(info.get("angular_span"))
if bool(info.get("is_full_cylinder")):
angular_span = math.tau
if surface != "cylinder" or current_diameter is None or current_diameter <= 1e-9:
blockers.append("当前选中对象不是可识别的圆柱面,不能调整孔/槽直径。")
@@ -1588,6 +1650,7 @@ class WindowActionMixin:
"中心(局部重建)",
"中心(保持关系)",
"圆柱高度调整",
"圆柱孔径调整",
"高度(缩放特征)缩放所属对象",
}
@@ -1603,6 +1666,7 @@ class WindowActionMixin:
isolated_geometry_operations = {
"push_pull_face",
"push_pull_face_keep_relations",
"translate_face_plane_offset_owning",
"move_face_plane_offset_local",
"resize_face_area_local",
"resize_face_area",
@@ -1615,6 +1679,8 @@ class WindowActionMixin:
"resize_shell_thickness_owning_scale",
"resize_cylindrical_height",
"resize_cylindrical_boss_height",
"resize_cylindrical_boss",
"move_cylindrical_boss_axis",
"resize_cylindrical_height_owning_scale",
"resize_cone_reference_radius",
"resize_cone_semi_angle",
@@ -1832,6 +1898,8 @@ class WindowActionMixin:
"does not have",
)
lower = text.lower()
if "target check failed" in lower or "没有到达目标" in text:
return "结果未达到目标", "几何内核生成了结果,但目标位置或目标尺寸校验没有通过,模型已回滚。"
if any(marker.lower() in lower for marker in unsupported_markers):
return "暂未实现", "当前版本暂未实现这类稳定修改。"
if any(marker.lower() in lower for marker in risk_markers):
@@ -7391,7 +7459,7 @@ class WindowActionMixin:
thread.finished.connect(self._forget_scan_thread)
self.scan_thread = thread
self.scan_worker = worker
thread.start()
_start_background_thread(thread)
def _run_scan_task_sync(self, action) -> None:
if not self.scan_in_progress:
@@ -7745,7 +7813,7 @@ class WindowActionMixin:
thread.finished.connect(self._forget_edit_thread)
self.edit_thread = thread
self.edit_worker = worker
thread.start()
_start_background_thread(thread)
def _edit_target_logical_id(self, target_kind: str | None, target_id: int | None) -> int | None:
if self.model is None or target_id is None or target_kind not in {"face", "feature"}:
@@ -7932,6 +8000,7 @@ class WindowActionMixin:
text=True,
encoding="utf-8",
errors="replace",
creationflags=_isolated_process_creation_flags(),
)
self.active_isolated_edit_process = process
stdout, stderr = process.communicate(timeout=timeout_seconds)
@@ -8107,16 +8176,29 @@ class WindowActionMixin:
or _unit_triple_or_none(parameters.get("outward_direction"))
)
if target_position is not None and plane_direction is not None:
part_id = self._edit_integrity_int_or_none(parameters.get("part_id"))
solid_id = self._edit_integrity_int_or_none(parameters.get("solid_id"))
candidate_ids.extend(
self._face_ids_at_plane_position(
model,
target_position,
plane_direction,
_float_or_none(parameters.get("bbox_diagonal")),
self._edit_integrity_int_or_none(parameters.get("part_id")),
self._edit_integrity_int_or_none(parameters.get("solid_id")),
part_id,
solid_id,
)
)
if solid_id is not None and solid_id >= 0:
candidate_ids.extend(
self._face_ids_at_plane_position(
model,
target_position,
plane_direction,
_float_or_none(parameters.get("bbox_diagonal")),
part_id,
None,
)
)
for candidate_id in candidate_ids:
if not (0 <= int(candidate_id) < len(model.faces)):
continue
@@ -8470,6 +8552,10 @@ class WindowActionMixin:
for face_id in filtered(range(len(model.faces))):
if face_id not in result:
result.append(face_id)
if solid_id is not None and solid_id >= 0:
for face_id in filtered(range(len(model.faces)), require_solid=False):
if face_id not in result:
result.append(face_id)
return result
if surface in {"plane", "cylinder", "cone", "sphere", "torus"} and has_target_value:
result = filtered(range(len(model.faces)))
@@ -8921,6 +9007,8 @@ class WindowActionMixin:
if self.selected_kind is None:
timing_detail = f"\n\n性能耗时:{timing_text}" if timing_text else ""
self.set_plain_info(f"{record.detail}{timing_detail}\n\n{locator_note}")
if hasattr(self, "_after_property_edit_finished"):
self._after_property_edit_finished(success=True)
@Slot(str)
def _fail_edit_action(self, message: str) -> None:
@@ -8940,6 +9028,8 @@ class WindowActionMixin:
self.statusBar().showMessage(
"后台编辑已取消,模型已保持在编辑前状态" if close_after_cancel else "不能修改,模型未修改"
)
if hasattr(self, "_after_property_edit_finished"):
self._after_property_edit_finished(success=False)
if close_after_cancel:
if self.edit_thread is not None and self.edit_thread.isRunning():
self.edit_thread.quit()