feat: 完善 Face 参数化编辑和隔离执行

This commit is contained in:
2026-07-31 16:36:05 +08:00
parent 27e4f7236c
commit bb44e3920d
31 changed files with 5428 additions and 252 deletions
+361 -81
View File
@@ -1,8 +1,12 @@
from __future__ import annotations
from datetime import datetime
import json
import math
from pathlib import Path
import subprocess
import sys
import tempfile
import vtk
from PySide6.QtCore import Qt, QThread, Slot
@@ -336,6 +340,7 @@ class WindowActionMixin:
return
face_id = self.selected_face_id
plan = self._quick_push_pull_plan(face_id, distance)
if plan["status"] == "blocked":
QMessageBox.information(self, "不能推拉平面", str(plan["message"]))
self.statusBar().showMessage("推拉平面已阻止")
@@ -343,6 +348,11 @@ class WindowActionMixin:
if plan["risk"] != "low":
warnings = str(plan.get("warnings", ""))
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
isolation_line = (
"本次会在隔离子进程里执行高风险 OCC 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
if str(plan.get("risk")) == "high"
else ""
)
result = QMessageBox.question(
self,
"确认推拉平面",
@@ -356,6 +366,7 @@ class WindowActionMixin:
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
f"{isolation_line}"
"继续操作会修改当前 B-Rep 结果几何,并支持失败回滚/撤销。确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
@@ -393,10 +404,13 @@ class WindowActionMixin:
"push_pull_scope_face_ids": plan.get("push_pull_scope_face_ids"),
"push_pull_scope_face_count": plan.get("push_pull_scope_face_count"),
"push_pull_scope_note": plan.get("push_pull_scope_note"),
"push_pull_inward_material_depth": plan.get("push_pull_inward_material_depth"),
"push_pull_inward_cut_ratio": plan.get("push_pull_inward_cut_ratio"),
"bbox_diagonal": plan.get("bbox_diagonal"),
},
target_kind="face",
target_id=face_id,
isolation=self._isolation_for_plan(plan, "push_pull_face", [face_id, distance]),
)
def _quick_push_pull_plan(self, face_id: int, distance: float) -> dict[str, object]:
@@ -432,6 +446,7 @@ class WindowActionMixin:
risk = "low"
status = "ready"
warnings: list[str] = []
blockers: list[str] = []
if distance_abs <= 1e-9:
return {
"status": "blocked",
@@ -451,6 +466,31 @@ class WindowActionMixin:
status = "caution"
warnings.append("面移动距离相对当前面尺寸偏大,请确认预览范围。")
outward_tuple = tuple(float(item) for item in outward)
inward_material_depth = None
inward_cut_ratio = None
if distance < 0 and self.model is not None:
inward_material_depth = self.model._push_pull_inward_material_depth(face_id, outward_tuple)
if inward_material_depth is not None and inward_material_depth > 1e-9:
inward_cut_ratio = distance_abs / inward_material_depth
depth_tolerance = max(
inward_material_depth * 1e-5,
(bbox_diagonal or 0.0) * 1e-7,
1e-6,
)
if distance_abs >= inward_material_depth - depth_tolerance:
status = "blocked"
risk = "blocked"
blockers.append("向内切削距离达到或超过当前面背后的材料厚度;继续执行很可能把实体切空或生成无效几何。")
elif inward_cut_ratio >= 0.85:
risk = "high"
warnings.append("向内切削距离已经接近当前面背后的材料厚度,剩余壁厚很薄,请谨慎确认。")
elif inward_cut_ratio >= 0.6 and risk != "high":
risk = "medium"
warnings.append("向内切削距离超过当前面背后材料厚度的 60%,请确认不会切穿。")
if status != "blocked" and risk in {"medium", "high"}:
status = "caution"
scope_face_ids = (
_int_values(info.get("push_pull_scope_face_ids"))
or _int_values(info.get("feature_highlight_face_ids"))
@@ -476,12 +516,14 @@ class WindowActionMixin:
if solid_id is None and self.model is not None and 0 <= face_id < len(self.model.face_solid_ids):
solid_id = self.model.face_solid_ids[face_id]
message = " ".join(warnings) if warnings else "可以尝试偏移该平面;完整几何检查会在后台执行。"
if blockers:
message = " ".join(blockers + warnings)
return {
"status": status,
"risk": risk,
"message": message,
"warnings": "".join(warnings),
"blockers": "",
"blockers": "".join(blockers),
"face_id": face_id,
"part_id": part_id,
"solid_id": solid_id,
@@ -489,7 +531,9 @@ class WindowActionMixin:
"surface": surface,
"area": info.get("area"),
"bbox_diagonal": bbox_diagonal,
"outward_direction": tuple(float(item) for item in outward),
"push_pull_inward_material_depth": inward_material_depth,
"push_pull_inward_cut_ratio": inward_cut_ratio,
"outward_direction": outward_tuple,
"current_plane_position": current_plane_position,
"target_plane_position": target_plane_position,
"resize_strategy": "push-pull-planar-face-region",
@@ -547,7 +591,12 @@ class WindowActionMixin:
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"当前版本会移动当前平面区域,让它和相对平面的距离接近目标厚度;这不是 CAD 壳命令参数编辑。确定继续吗?"
+ (
"本次会在隔离子进程里执行高风险 OCC 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
if str(plan.get("risk")) == "high"
else ""
)
+ "当前版本会移动当前平面区域,让它和相对平面的距离接近目标厚度;这不是 CAD 壳命令参数编辑。确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
@@ -556,7 +605,10 @@ class WindowActionMixin:
self.statusBar().showMessage("已取消薄壁厚度调整")
return
self._show_shell_thickness_preview(face_id, target_thickness)
if str(plan.get("risk")) == "high":
self.clear_edit_preview(render=False)
else:
self._show_shell_thickness_preview(face_id, target_thickness)
def action():
return self.model.resize_shell_thickness(face_id, target_thickness)
@@ -595,6 +647,7 @@ class WindowActionMixin:
},
target_kind="feature" if self.selected_kind == "feature" else "face",
target_id=face_id,
isolation=self._isolation_for_plan(plan, "resize_shell_thickness", [face_id, target_thickness]),
)
def resize_shell_thickness_owning_scale(self) -> None:
@@ -627,6 +680,9 @@ class WindowActionMixin:
f"缩放比例: {_format_value(plan.get('affine_scale'))}",
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}",
f"厚度方向: {_format_value(plan.get('affine_axis_direction'))}",
f"重建模式: {_format_value(plan.get('owning_shell_thickness_rebuild_mode'))}",
f"重建Face数: {_format_value(plan.get('local_face_deform_face_count'))}",
f"移动顶点数: {_format_value(plan.get('local_face_deform_moved_point_count'))}",
f"相对平面 Face: {_format_value(plan.get('shell_opposite_face_id', ''))}",
"当前语义: 沿薄壁/壳体厚度方向整体缩放所属特征或 Solid;不是推拉当前平面区域。",
]
@@ -668,6 +724,9 @@ class WindowActionMixin:
"affine_axis_point": plan.get("affine_axis_point"),
"affine_axis_direction": plan.get("affine_axis_direction"),
"affine_target_kind": plan.get("affine_target_kind"),
"owning_shell_thickness_rebuild_mode": plan.get("owning_shell_thickness_rebuild_mode"),
"local_face_deform_face_count": plan.get("local_face_deform_face_count"),
"local_face_deform_moved_point_count": plan.get("local_face_deform_moved_point_count"),
"part_solid_count": plan.get("part_solid_count"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
@@ -681,6 +740,7 @@ class WindowActionMixin:
},
target_kind="face",
target_id=face_id,
isolation=self._isolation_for_plan(plan, "resize_shell_thickness_owning_scale", [face_id, target_thickness]),
)
@staticmethod
@@ -860,7 +920,10 @@ class WindowActionMixin:
QMessageBox.information(self, f"不能{title}", str(plan.get("message", "")))
self.statusBar().showMessage(blocked_status)
return False
if self._block_unisolated_high_risk_operation(f"已阻止高风险{title}", plan):
if (
not self._quick_edit_title_supports_isolation(title)
and self._block_unisolated_high_risk_operation(f"已阻止高风险{title}", plan)
):
return False
if plan.get("risk") == "low":
return True
@@ -883,7 +946,12 @@ class WindowActionMixin:
+ f"\n风险: {plan.get('risk')}\n"
+ warnings_line
+ f"\n{plan.get('message', '')}\n\n"
"为避免复杂 STEP 在界面线程卡死,本次不会先生成红/绿预览;"
+ (
"本次会在隔离子进程里执行高风险 OCC 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
if str(plan.get("risk")) == "high" and self._quick_edit_title_supports_isolation(title)
else ""
)
+ "为避免复杂 STEP 在界面线程卡死,本次不会先生成红/绿预览;"
"详细几何方案会进入后台计算,完成后自动刷新模型。\n\n"
"确定继续吗?"
),
@@ -895,6 +963,36 @@ class WindowActionMixin:
return False
return True
def _quick_edit_title_supports_isolation(self, title: str) -> bool:
return title in {
"薄壁厚度(整体)缩放所属对象",
"只缩放当前Face面积",
"面面积(整体)",
"面宽(当前面)",
"面高(当前面)",
"面宽(整体)",
"面高(整体)",
"面偏移(当前面)",
"只移动当前Face",
}
def _isolation_for_plan(
self,
plan: dict[str, object],
operation: str,
args: list[object],
*,
timeout_seconds: float = 180.0,
) -> dict[str, object] | None:
if str(plan.get("risk", "")) != "high":
return None
return {
"operation": operation,
"args": args,
"timeout_seconds": timeout_seconds,
"reason": "high-risk-occ-edit",
}
def resize_hole(self) -> None:
if self.model is None:
return
@@ -3776,6 +3874,90 @@ class WindowActionMixin:
def move_edge_end_point(self) -> None:
self._move_edge_endpoint("end")
def move_circular_edge_axis_center(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再移动圆Edge相邻轴心。"):
return
if self.selected_edge_id is None:
QMessageBox.information(self, "未选择圆Edge", "请先选择一条圆形或圆弧 Edge。")
return
try:
target_center = (
float(self.edge_center_x_input.text()),
float(self.edge_center_y_input.text()),
float(self.edge_center_z_input.text()),
)
except (AttributeError, ValueError):
QMessageBox.critical(self, "圆心坐标无效", "请输入 X, Y, Z 三个数字形式的目标圆心坐标。")
return
edge_id = self.selected_edge_id
plan = self.model.circular_edge_axis_move_plan(edge_id, target_center)
lines = [
f"Edge: {edge_id}",
f"当前圆心: {_format_value(plan.get('current_edge_center'))}",
f"目标圆心: {_format_value(plan.get('target_edge_center'))}",
f"圆心移动: {_format_value(plan.get('circular_edge_center_move_vector'))}",
f"相邻Face: {_format_value(plan.get('circular_edge_cylinder_face_id'))}",
f"执行路径: {_format_value(plan.get('circular_edge_cylinder_mode_label'))}",
f"当前轴心: {_format_value(plan.get('current_axis_center'))}",
f"目标轴心: {_format_value(plan.get('target_axis_center'))}",
f"圆边半径: {_format_value(plan.get('circular_edge_current_radius'))}",
"当前版本会按圆Edge圆心的移动量,移动相邻孔/槽/凸台的圆柱轴心。",
]
if not self._confirm_quick_edit_plan(
"圆Edge圆心/轴心移动",
plan,
lines,
blocked_status="圆Edge圆心/轴心移动已阻止",
cancelled_status="已取消圆Edge圆心/轴心移动",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.move_circular_edge_axis_center(edge_id, target_center)
self._run_edit_action(
action,
operation_name="移动圆Edge相邻轴心",
target=f"Edge {edge_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"curve": plan.get("curve"),
"current_edge_center": plan.get("current_edge_center"),
"target_edge_center": plan.get("target_edge_center"),
"circular_edge_center_move_vector": plan.get("circular_edge_center_move_vector"),
"circular_edge_center_move_distance": plan.get("circular_edge_center_move_distance"),
"circular_edge_current_radius": plan.get("circular_edge_current_radius"),
"circular_edge_current_diameter": plan.get("circular_edge_current_diameter"),
"circular_edge_cylinder_face_id": plan.get("circular_edge_cylinder_face_id"),
"circular_edge_cylinder_mode": plan.get("circular_edge_cylinder_mode"),
"circular_edge_cylinder_mode_label": plan.get("circular_edge_cylinder_mode_label"),
"current_axis_center": plan.get("current_axis_center"),
"target_axis_center": plan.get("target_axis_center"),
"axis_move_vector": plan.get("axis_move_vector"),
"axis_move_distance": plan.get("axis_move_distance"),
"axis_move_radial_distance": plan.get("axis_move_radial_distance"),
"axis_move_axial_delta": plan.get("axis_move_axial_delta"),
"target_diameter": plan.get("target_diameter"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"move_axis_status": plan.get("status"),
"move_axis_risk": plan.get("risk"),
"move_axis_message": plan.get("message"),
"move_axis_warnings": plan.get("warnings"),
"move_axis_blockers": plan.get("blockers"),
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="edge",
target_id=edge_id,
)
def move_edge_center_point(self) -> None:
if self.model is None:
return
@@ -3996,6 +4178,9 @@ class WindowActionMixin:
"delta_reference_radius": plan.get("delta_reference_radius"),
"reference_radius_delta_ratio": plan.get("reference_radius_delta_ratio"),
"semi_angle": plan.get("semi_angle"),
"semi_angle_degrees": plan.get("semi_angle_degrees"),
"target_semi_angle": plan.get("target_semi_angle"),
"target_semi_angle_degrees": plan.get("target_semi_angle_degrees"),
"affine_scale": plan.get("affine_scale"),
"affine_transform_kind": plan.get("affine_transform_kind"),
"affine_transform_label": plan.get("affine_transform_label"),
@@ -4245,7 +4430,12 @@ class WindowActionMixin:
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"这不是只改变一个面的 CAD 历史面积参数;继续操作会缩放所属对象并刷新 B-Rep 结果几何,支持失败回滚/撤销。确定继续吗?"
+ (
"本次会在隔离子进程里执行高风险 OCC 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
if str(plan.get("risk")) == "high"
else ""
)
+ "这不是只改变一个面的 CAD 历史面积参数;继续操作会缩放所属对象并刷新 B-Rep 结果几何,支持失败回滚/撤销。确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
@@ -4288,6 +4478,7 @@ class WindowActionMixin:
},
target_kind="face",
target_id=face_id,
isolation=self._isolation_for_plan(plan, "resize_face_area", [face_id, target_area]),
)
def resize_face_area_local(self) -> None:
@@ -4363,6 +4554,7 @@ class WindowActionMixin:
},
target_kind="face",
target_id=face_id,
isolation=self._isolation_for_plan(plan, "resize_face_area_local", [face_id, target_area]),
)
def resize_face_width_local(self) -> None:
@@ -4466,6 +4658,7 @@ class WindowActionMixin:
},
target_kind="face",
target_id=face_id,
isolation=self._isolation_for_plan(plan, "resize_face_size_local", [face_id, target_size, axis_key]),
)
def _resize_face_size_owning_scale(self, axis: str) -> None:
@@ -4569,6 +4762,7 @@ class WindowActionMixin:
},
target_kind="face",
target_id=face_id,
isolation=self._isolation_for_plan(plan, "resize_face_size_owning_scale", [face_id, target_size, axis_key]),
)
def move_selected_face_plane_position_by_translation(self) -> None:
@@ -4585,42 +4779,15 @@ class WindowActionMixin:
QMessageBox.critical(self, "面偏移无效", "请输入数字形式的目标面偏移。")
return
face_id = self.selected_face_id
frame = self._selected_plane_offset_frame(face_id)
if frame is None:
QMessageBox.information(self, "不能按面偏移平移", "当前 Face 缺少稳定平面方向或基准点。")
return
plane_origin, plane_direction, current_position = frame
target_position = current_position + distance
vector = (
plane_direction[0] * distance,
plane_direction[1] * distance,
plane_direction[2] * distance,
)
if self.selected_solid_id is not None:
moved_kind = "solid"
moved_id = self.selected_solid_id
plan = self.model.translate_solid_plan(moved_id, vector)
def action():
return self.model.translate_solid(moved_id, vector)
elif self.selected_part_id is not None:
moved_kind = "part"
moved_id = self.selected_part_id
plan = self.model.translate_part_plan(moved_id, vector)
def action():
return self.model.translate_part(moved_id, vector)
else:
QMessageBox.information(self, "不能按面偏移平移", "当前 Face 没有关联到稳定的所属对象。")
return
plan = self.model.face_plane_offset_owning_translation_plan(face_id, distance)
if plan["status"] == "blocked":
QMessageBox.information(self, "不能按面偏移平移", str(plan["message"]))
self.statusBar().showMessage("面偏移整体平移已阻止")
return
current_position = float(plan.get("current_plane_position", 0.0) or 0.0)
target_position = float(plan.get("target_plane_position", current_position) or current_position)
plane_direction = tuple(plan.get("plane_direction") or (0.0, 0.0, 0.0))
if not self._confirm_face_plane_position_translation_plan(
plan,
face_id,
@@ -4631,6 +4798,12 @@ class WindowActionMixin:
self.statusBar().showMessage("已取消面偏移整体平移")
return
self.clear_edit_preview(render=False)
def action():
return self.model.translate_face_plane_offset_owning(face_id, distance)
moved_kind = str(plan.get("target_kind", ""))
moved_id = plan.get("solid_id") if moved_kind == "solid" else plan.get("part_id")
self._run_edit_action(
action,
operation_name="面偏移(整体)",
@@ -4643,9 +4816,9 @@ class WindowActionMixin:
"moved_target_id": moved_id,
"current_plane_position": current_position,
"target_plane_position": target_position,
"plane_origin": plane_origin,
"plane_origin": plan.get("plane_origin"),
"plane_direction": plane_direction,
"translation_vector": vector,
"translation_vector": plan.get("translation_vector"),
"translation_distance": plan.get("translation_distance"),
"bbox_diagonal": plan.get("bbox_diagonal"),
"translate_status": plan.get("status"),
@@ -4655,7 +4828,7 @@ class WindowActionMixin:
"translate_blockers": plan.get("blockers"),
"resize_strategy": "translate-owning-shape-from-plane-offset",
"edit_strategy_label": "按面偏移平移所属对象",
"edit_semantics": "把目标面偏移换算成沿当前面方向的平移量,并平移所属特征或 Solid;不推拉当前面,不切削,也不补料。",
"edit_semantics": "把目标面偏移换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid;不推拉当前面,不切削,也不补料。",
},
target_kind="face",
target_id=face_id,
@@ -4676,33 +4849,12 @@ class WindowActionMixin:
return
face_id = self.selected_face_id
frame = self._selected_plane_offset_frame(face_id)
if frame is None:
QMessageBox.information(self, "不能只移动当前Face", "当前 Face 缺少稳定平面方向或基准点。")
return
plane_origin, plane_direction, current_position = frame
current_center = self._selected_face_center_for_translation(face_id)
if current_center is None:
QMessageBox.information(self, "不能只移动当前Face", "当前 Face 缺少稳定中心坐标。")
return
target_position = current_position + distance
vector = (
plane_direction[0] * distance,
plane_direction[1] * distance,
plane_direction[2] * distance,
)
target_center = (
current_center[0] + vector[0],
current_center[1] + vector[1],
current_center[2] + vector[2],
)
plan = self.model.face_center_local_move_plan(face_id, target_center)
plan = self.model.face_plane_offset_local_plan(face_id, distance)
lines = [
f"Face: {face_id}",
f"当前面偏移: {_format_value(current_position)}",
f"目标面偏移: {_format_value(target_position)}",
f"平面方向: {_format_value(plane_direction)}",
f"当前面偏移: {_format_value(plan.get('current_plane_position'))}",
f"目标面偏移: {_format_value(plan.get('target_plane_position'))}",
f"平面方向: {_format_value(plan.get('plane_direction'))}",
f"移动向量: {_format_value(plan.get('face_center_move_vector'))}",
f"移动距离: {_format_value(plan.get('face_center_move_distance'))}",
f"移动/所属对象尺寸: {_format_percent(plan.get('face_center_move_ratio'))}",
@@ -4721,7 +4873,7 @@ class WindowActionMixin:
self.clear_edit_preview(render=False)
def action():
return self.model.move_face_center_local(face_id, target_center)
return self.model.move_face_plane_offset_local(face_id, distance)
self._run_edit_action(
action,
@@ -4732,10 +4884,10 @@ class WindowActionMixin:
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"surface": plan.get("surface"),
"current_plane_position": current_position,
"target_plane_position": target_position,
"plane_origin": plane_origin,
"plane_direction": plane_direction,
"current_plane_position": plan.get("current_plane_position"),
"target_plane_position": plan.get("target_plane_position"),
"plane_origin": plan.get("plane_origin"),
"plane_direction": plan.get("plane_direction"),
"plane_offset_distance": distance,
"current_face_center": plan.get("current_face_center"),
"target_face_center": plan.get("target_face_center"),
@@ -4749,7 +4901,7 @@ class WindowActionMixin:
"resize_strategy": "local-face-plane-offset-deform",
"edit_strategy_label": "按面偏移只移动当前Face",
"edit_semantics": (
"把目标面偏移换算成沿当前面方向的移动量,只移动当前 Face 的顶点并重建相邻平面;"
"把目标面偏移换算成沿当前面垂直方向的移动量,只移动当前 Face 的顶点并重建相邻平面;"
"不推拉加料/切削,也不平移所属对象。"
),
"resize_status": plan.get("status"),
@@ -4761,6 +4913,7 @@ class WindowActionMixin:
},
target_kind="face",
target_id=face_id,
isolation=self._isolation_for_plan(plan, "move_face_plane_offset_local", [face_id, distance]),
)
def move_selected_face_center(self) -> None:
@@ -4787,18 +4940,18 @@ class WindowActionMixin:
if self.selected_solid_id is not None:
moved_kind = "solid"
moved_id = self.selected_solid_id
plan = self.model.translate_solid_plan(moved_id, vector)
plan = self.model.face_center_owning_translation_plan(face_id, target_center)
def action():
return self.model.translate_solid(moved_id, vector)
return self.model.move_face_center_owning(face_id, target_center)
elif self.selected_part_id is not None:
moved_kind = "part"
moved_id = self.selected_part_id
plan = self.model.translate_part_plan(moved_id, vector)
plan = self.model.face_center_owning_translation_plan(face_id, target_center)
def action():
return self.model.translate_part(moved_id, vector)
return self.model.move_face_center_owning(face_id, target_center)
else:
QMessageBox.information(self, "不能移动Face中心", "当前 Face 没有关联到稳定的所属对象。")
@@ -4918,6 +5071,7 @@ class WindowActionMixin:
},
target_kind="face",
target_id=face_id,
isolation=self._isolation_for_plan(plan, "move_face_center_local", [face_id, list(target_center)]),
)
def move_selected_axis_center_by_translation(self) -> None:
@@ -5181,11 +5335,11 @@ class WindowActionMixin:
f"平移向量: {_format_value(plan['translation_vector'])}\n"
f"平移距离: {_format_value(plan['translation_distance'])}\n"
f"编辑策略: 按面偏移平移所属对象\n"
f"编辑方式: 沿当前面方向平移所属特征或 Solid;不推拉当前面,不切削,也不补料。\n"
f"编辑方式: 沿当前面垂直方向平移所属特征或 Solid;不推拉当前面,不切削,也不补料。\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"如果你想改变厚度或把这个面推出/切入,请使用不带“整体”的面偏移或偏移距离行。确定继续吗?"
"如果你想改变厚度或把这个面推出/切入,请在“面偏移”这一行把影响范围选为“推拉当前面”。确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
@@ -6023,7 +6177,7 @@ class WindowActionMixin:
"该操作被稳定性保护阻止。\n\n"
"当前计划被判定为 high risk;这类 OCCT 布尔、倒圆或局部重建在复杂 STEP 上"
"可能不是普通失败,而是让进程卡死或直接退出。请先尝试更小的参数、修复模型、"
"选择更明确的面/边,或等后续子进程隔离执行通道实现后再开放"
"选择更明确的面/边;当前这类操作还没有接入隔离子进程执行通道。"
),
)
self.statusBar().showMessage("高风险操作已被稳定性保护阻止")
@@ -6037,6 +6191,7 @@ class WindowActionMixin:
parameters: dict[str, object],
target_kind: str | None = None,
target_id: int | None = None,
isolation: dict[str, object] | None = None,
) -> None:
if self.model is None:
return
@@ -6057,6 +6212,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,
"isolation": dict(isolation or {}),
}
self._begin_edit_task(operation_name)
@@ -6097,6 +6253,16 @@ class WindowActionMixin:
before_stats = self.model.stats()
before_part_stats = self._part_stats_or_none(target_part_id)
before_geometry = {}
isolation = context.get("isolation")
if isinstance(isolation, dict) and isolation:
return self._run_isolated_edit_job(
context=context,
isolation=isolation,
snapshot=snapshot,
before_stats=before_stats,
before_part_stats=before_part_stats,
before_geometry=before_geometry,
)
try:
result = action()
after_snapshot = self.model.snapshot()
@@ -6140,6 +6306,120 @@ class WindowActionMixin:
return job
def _run_isolated_edit_job(
self,
*,
context: dict[str, object],
isolation: dict[str, object],
snapshot: dict[object, object],
before_stats,
before_part_stats,
before_geometry: dict[str, object],
) -> dict[str, object]:
if self.model is None:
raise RuntimeError("Model is not loaded.")
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()
args = list(isolation.get("args") or [])
if not operation:
raise RuntimeError("隔离执行缺少操作名称。")
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"
request_path = temp_root / "request.json"
self.model.export_all(input_path)
request_path.write_text(
json.dumps(
{
"input_path": str(input_path),
"output_path": str(output_path),
"operation": operation,
"args": args,
},
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
command = [sys.executable, "-m", "step_editor.isolated_edit_worker", str(request_path)]
try:
completed = subprocess.run(
command,
cwd=project_root,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=timeout_seconds,
check=False,
)
except subprocess.TimeoutExpired as exc:
raise RuntimeError(
f"隔离子进程执行超时,已终止危险计算;主程序和原模型保持不变。"
f" 超时时间: {timeout_seconds:g}s"
) from exc
response_path = request_path.with_suffix(".response.json")
response: dict[str, object] = {}
if response_path.exists():
try:
response = json.loads(response_path.read_text(encoding="utf-8"))
except Exception:
response = {}
if completed.returncode != 0 or not bool(response.get("ok", False)):
error = str(response.get("error") or completed.stderr or completed.stdout or "未知错误").strip()
raise RuntimeError(
"隔离子进程执行失败;主程序没有崩溃,原模型保持不变。"
f" 子进程返回码: {completed.returncode}. 错误: {error}"
)
if not output_path.exists():
raise RuntimeError("隔离子进程报告成功,但没有生成结果 STEP;原模型保持不变。")
new_model = StepModel.load(output_path)
try:
new_model.filename = self.step_path
except Exception:
pass
self.model = new_model
after_snapshot = self.model.snapshot()
after_stats = self.model.stats()
after_part_stats = self._part_stats_or_none(target_part_id)
after_geometry: dict[str, object] = {}
model_polydata = None
edge_polydata = None
try:
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)),
)
except Exception:
model_polydata = None
edge_polydata = None
child_message = str(response.get("message") or "隔离子进程编辑完成。")
return {
"message": f"{child_message} 已通过隔离子进程完成;如果 OCC 崩溃,主程序不会被带崩。",
"snapshot": snapshot,
"before_stats": before_stats,
"before_part_stats": before_part_stats,
"before_geometry": before_geometry,
"after_snapshot": after_snapshot,
"after_stats": after_stats,
"after_part_stats": after_part_stats,
"quality_warnings": _edit_quality_warnings(before_part_stats, after_part_stats),
"after_geometry": after_geometry,
"model_polydata": model_polydata,
"edge_polydata": edge_polydata,
}
def _edit_context_part_id(self, context: dict[str, object]) -> int | None:
if self.model is None:
return None
@@ -6218,9 +6498,9 @@ class WindowActionMixin:
elif "面宽(当前面)" in operation_name or "面高(当前面)" in operation_name:
progress_text = f"{operation_name} 正在后台计算;当前会只沿选中 Face 的一个平面内方向缩放顶点并重建相邻平面。"
elif "面偏移(当前面)" in operation_name:
progress_text = f"{operation_name} 正在后台计算;当前会沿平面方向移动所选 Face 顶点并重建相邻平面。"
progress_text = f"{operation_name} 正在后台计算;当前会沿当前面垂直方向移动所选 Face 顶点并重建相邻平面。"
elif "面偏移(整体)" in operation_name:
progress_text = f"{operation_name} 正在后台计算;当前会沿平面方向平移所属特征或 Solid,不推拉当前面。"
progress_text = f"{operation_name} 正在后台计算;当前会沿当前面垂直方向平移所属特征或 Solid,不推拉当前面。"
elif "Face中心" in operation_name:
progress_text = f"{operation_name} 正在后台计算;当前会平移所属特征或 Solid,不做单面局部扭曲。"
elif "只移动当前Face" in operation_name: