feat: 完善 Face 一级关系编辑和稳定性
This commit is contained in:
+742
-26
@@ -375,7 +375,11 @@ class WindowActionMixin:
|
||||
if result != QMessageBox.StandardButton.Yes:
|
||||
self.statusBar().showMessage("已取消推拉平面")
|
||||
return
|
||||
self._show_push_pull_preview(face_id, distance, plan=plan)
|
||||
isolation = self._isolation_for_plan(plan, "push_pull_face", [face_id, distance])
|
||||
if isolation is None:
|
||||
self._show_push_pull_preview(face_id, distance, plan=plan)
|
||||
else:
|
||||
self.clear_edit_preview(render=False)
|
||||
|
||||
def action():
|
||||
return self.model.push_pull_face(face_id, distance)
|
||||
@@ -410,7 +414,7 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=self._isolation_for_plan(plan, "push_pull_face", [face_id, distance]),
|
||||
isolation=isolation,
|
||||
)
|
||||
|
||||
def _quick_push_pull_plan(self, face_id: int, distance: float) -> dict[str, object]:
|
||||
@@ -467,10 +471,9 @@ class WindowActionMixin:
|
||||
warnings.append("面移动距离相对当前面尺寸偏大,请确认预览范围。")
|
||||
|
||||
outward_tuple = tuple(float(item) for item in outward)
|
||||
inward_material_depth = None
|
||||
inward_material_depth = _float_or_none(info.get("push_pull_inward_material_depth"))
|
||||
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 distance < 0:
|
||||
if inward_material_depth is not None and inward_material_depth > 1e-9:
|
||||
inward_cut_ratio = distance_abs / inward_material_depth
|
||||
depth_tolerance = max(
|
||||
@@ -488,6 +491,10 @@ class WindowActionMixin:
|
||||
elif inward_cut_ratio >= 0.6 and risk != "high":
|
||||
risk = "medium"
|
||||
warnings.append("向内切削距离超过当前面背后材料厚度的 60%,请确认不会切穿。")
|
||||
else:
|
||||
if risk == "low":
|
||||
risk = "medium"
|
||||
warnings.append("向内推拉的材料厚度尚未缓存;完整切穿检查会放到后台计算。")
|
||||
if status != "blocked" and risk in {"medium", "high"}:
|
||||
status = "caution"
|
||||
|
||||
@@ -605,10 +612,11 @@ class WindowActionMixin:
|
||||
self.statusBar().showMessage("已取消薄壁厚度调整")
|
||||
return
|
||||
|
||||
if str(plan.get("risk")) == "high":
|
||||
isolation = self._isolation_for_plan(plan, "resize_shell_thickness", [face_id, target_thickness])
|
||||
if isolation is not None:
|
||||
self.clear_edit_preview(render=False)
|
||||
else:
|
||||
self._show_shell_thickness_preview(face_id, target_thickness)
|
||||
self._show_shell_thickness_preview(face_id, target_thickness, plan=plan)
|
||||
|
||||
def action():
|
||||
return self.model.resize_shell_thickness(face_id, target_thickness)
|
||||
@@ -647,7 +655,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]),
|
||||
isolation=isolation,
|
||||
)
|
||||
|
||||
def resize_shell_thickness_owning_scale(self) -> None:
|
||||
@@ -984,13 +992,31 @@ class WindowActionMixin:
|
||||
*,
|
||||
timeout_seconds: float = 180.0,
|
||||
) -> dict[str, object] | None:
|
||||
if str(plan.get("risk", "")) != "high":
|
||||
risk = str(plan.get("risk", ""))
|
||||
isolated_face_operations = {
|
||||
"push_pull_face",
|
||||
"move_face_plane_offset_local",
|
||||
"resize_face_area_local",
|
||||
"resize_face_area",
|
||||
"resize_face_size_local",
|
||||
"resize_face_size_owning_scale",
|
||||
"move_face_center_local",
|
||||
"resize_shell_thickness",
|
||||
"resize_shell_thickness_owning_scale",
|
||||
"resize_cone_reference_radius",
|
||||
"resize_cone_semi_angle",
|
||||
"resize_sphere_radius",
|
||||
"resize_torus_radius",
|
||||
}
|
||||
if operation not in isolated_face_operations:
|
||||
return None
|
||||
if risk not in {"low", "medium", "high"}:
|
||||
return None
|
||||
return {
|
||||
"operation": operation,
|
||||
"args": args,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
"reason": "high-risk-occ-edit",
|
||||
"reason": f"{risk}-risk-face-occ-edit",
|
||||
}
|
||||
|
||||
def resize_hole(self) -> None:
|
||||
@@ -1067,6 +1093,7 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=isolation,
|
||||
)
|
||||
|
||||
def resize_cylindrical_owning_scale(self) -> None:
|
||||
@@ -1130,6 +1157,12 @@ 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"),
|
||||
"embedded_cone_recut_mode": plan.get("embedded_cone_recut_mode"),
|
||||
"embedded_cone_current_small_radius": plan.get("embedded_cone_current_small_radius"),
|
||||
"embedded_cone_target_small_radius": plan.get("embedded_cone_target_small_radius"),
|
||||
"embedded_cone_current_large_radius": plan.get("embedded_cone_current_large_radius"),
|
||||
"embedded_cone_target_large_radius": plan.get("embedded_cone_target_large_radius"),
|
||||
"embedded_cone_height": plan.get("embedded_cone_height"),
|
||||
"resize_strategy": plan.get("resize_strategy"),
|
||||
"edit_strategy_label": plan.get("edit_strategy_label"),
|
||||
"edit_semantics": plan.get("edit_semantics"),
|
||||
@@ -2564,6 +2597,13 @@ 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"),
|
||||
"embedded_cone_recut_mode": plan.get("embedded_cone_recut_mode"),
|
||||
"embedded_cone_fixed_small_radius": plan.get("embedded_cone_fixed_small_radius"),
|
||||
"embedded_cone_current_small_radius": plan.get("embedded_cone_current_small_radius"),
|
||||
"embedded_cone_target_small_radius": plan.get("embedded_cone_target_small_radius"),
|
||||
"embedded_cone_current_large_radius": plan.get("embedded_cone_current_large_radius"),
|
||||
"embedded_cone_target_large_radius": plan.get("embedded_cone_target_large_radius"),
|
||||
"embedded_cone_height": plan.get("embedded_cone_height"),
|
||||
"resize_strategy": plan.get("resize_strategy"),
|
||||
"edit_strategy_label": plan.get("edit_strategy_label"),
|
||||
"edit_semantics": plan.get("edit_semantics"),
|
||||
@@ -4112,7 +4152,7 @@ class WindowActionMixin:
|
||||
def resize_cone_reference_radius(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
if self._edit_busy("请等待当前编辑完成后再修改圆锥面(整体)。"):
|
||||
if self._edit_busy("请等待当前编辑完成后再修改圆锥面参数。"):
|
||||
return
|
||||
if self.selected_face_id is None:
|
||||
QMessageBox.information(self, "未选择圆锥面", "请先选择一个圆锥面 Face 或圆锥面特征。")
|
||||
@@ -4127,16 +4167,22 @@ class WindowActionMixin:
|
||||
|
||||
face_id = self.selected_face_id
|
||||
plan = self.model.conical_reference_radius_plan(face_id, target_radius)
|
||||
isolation = self._isolation_for_plan(plan, "resize_cone_reference_radius", [face_id, target_radius])
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能修改圆锥面(整体)", str(plan["message"]))
|
||||
self.statusBar().showMessage("圆锥面(整体)修改已阻止")
|
||||
QMessageBox.information(self, "不能修改圆锥面参数", str(plan["message"]))
|
||||
self.statusBar().showMessage("圆锥面参数修改已阻止")
|
||||
return
|
||||
if plan["risk"] != "low":
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||||
isolation_line = (
|
||||
"本次会在隔离子进程里执行高风险 OCCT 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||||
if isolation is not None
|
||||
else ""
|
||||
)
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"确认修改圆锥面(整体)",
|
||||
"确认修改圆锥面参数",
|
||||
(
|
||||
f"Face: {face_id}\n"
|
||||
f"当前参考半径: {_format_value(plan.get('current_reference_radius'))}\n"
|
||||
@@ -4150,13 +4196,14 @@ 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,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
if result != QMessageBox.StandardButton.Yes:
|
||||
self.statusBar().showMessage("已取消圆锥面(整体)修改")
|
||||
self.statusBar().showMessage("已取消圆锥面参数修改")
|
||||
return
|
||||
self.clear_edit_preview(render=False)
|
||||
|
||||
@@ -4165,7 +4212,7 @@ class WindowActionMixin:
|
||||
|
||||
self._run_edit_action(
|
||||
action,
|
||||
operation_name="修改圆锥面参数(整体)",
|
||||
operation_name="修改圆锥面参数",
|
||||
target=f"Face {face_id}",
|
||||
parameters={
|
||||
"part_id": plan.get("part_id"),
|
||||
@@ -4198,6 +4245,104 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=isolation,
|
||||
)
|
||||
|
||||
def resize_cone_semi_angle(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
if self._edit_busy("请等待当前编辑完成后再修改圆锥半角。"):
|
||||
return
|
||||
if self.selected_face_id is None:
|
||||
QMessageBox.information(self, "未选择圆锥面", "请先选择一个圆锥 Face 或圆锥特征。")
|
||||
return
|
||||
if not hasattr(self, "cone_reference_radius_input"):
|
||||
return
|
||||
try:
|
||||
target_angle_degrees = float(self.cone_reference_radius_input.text())
|
||||
except ValueError:
|
||||
QMessageBox.critical(self, "圆锥半角无效", "请输入数字形式的目标半角,单位是度。")
|
||||
return
|
||||
|
||||
face_id = self.selected_face_id
|
||||
plan = self.model.conical_semi_angle_plan(face_id, target_angle_degrees)
|
||||
isolation = self._isolation_for_plan(plan, "resize_cone_semi_angle", [face_id, target_angle_degrees])
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能修改圆锥半角", str(plan["message"]))
|
||||
self.statusBar().showMessage("圆锥半角修改已阻止")
|
||||
return
|
||||
if plan["risk"] != "low":
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||||
isolation_line = (
|
||||
"本次会在隔离子进程里执行高风险 OCCT 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||||
if isolation is not None
|
||||
else ""
|
||||
)
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"确认修改圆锥半角",
|
||||
(
|
||||
f"Face: {face_id}\n"
|
||||
f"当前半角: {_format_value(plan.get('semi_angle_degrees'))} 度\n"
|
||||
f"目标半角: {_format_value(plan.get('target_semi_angle_degrees'))} 度\n"
|
||||
f"目标参考半径: {_format_value(plan.get('target_reference_radius'))}\n"
|
||||
f"变化比例: {_format_percent(plan.get('reference_radius_delta_ratio'))}\n"
|
||||
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"影响范围: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
f"{isolation_line}"
|
||||
"继续操作会修改当前 B-Rep 结果几何,并支持失败回滚。确定继续吗?"
|
||||
),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
if result != QMessageBox.StandardButton.Yes:
|
||||
self.statusBar().showMessage("已取消圆锥半角修改")
|
||||
return
|
||||
self.clear_edit_preview(render=False)
|
||||
|
||||
def action():
|
||||
return self.model.resize_conical_semi_angle(face_id, target_angle_degrees)
|
||||
|
||||
self._run_edit_action(
|
||||
action,
|
||||
operation_name="修改圆锥半角(整体)",
|
||||
target=f"Face {face_id}",
|
||||
parameters={
|
||||
"part_id": plan.get("part_id"),
|
||||
"solid_id": plan.get("solid_id"),
|
||||
"surface": "cone",
|
||||
"current_reference_radius": plan.get("current_reference_radius"),
|
||||
"target_reference_radius": plan.get("target_reference_radius"),
|
||||
"current_reference_diameter": plan.get("current_reference_diameter"),
|
||||
"target_reference_diameter": plan.get("target_reference_diameter"),
|
||||
"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"),
|
||||
"affine_axis_point": plan.get("affine_axis_point"),
|
||||
"affine_axis_direction": plan.get("affine_axis_direction"),
|
||||
"affine_target_kind": plan.get("affine_target_kind"),
|
||||
"resize_strategy": plan.get("resize_strategy"),
|
||||
"edit_strategy_label": plan.get("edit_strategy_label"),
|
||||
"edit_semantics": plan.get("edit_semantics"),
|
||||
"resize_status": plan.get("status"),
|
||||
"resize_risk": plan.get("risk"),
|
||||
"resize_message": plan.get("message"),
|
||||
"resize_warnings": plan.get("warnings"),
|
||||
"resize_blockers": plan.get("blockers"),
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=isolation,
|
||||
)
|
||||
|
||||
def resize_sphere_radius(self) -> None:
|
||||
@@ -4218,6 +4363,7 @@ class WindowActionMixin:
|
||||
|
||||
face_id = self.selected_face_id
|
||||
plan = self.model.spherical_radius_plan(face_id, target_radius)
|
||||
isolation = self._isolation_for_plan(plan, "resize_sphere_radius", [face_id, target_radius])
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能修改球面(整体)", str(plan["message"]))
|
||||
self.statusBar().showMessage("球面(整体)修改已阻止")
|
||||
@@ -4225,6 +4371,11 @@ class WindowActionMixin:
|
||||
if plan["risk"] != "low":
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||||
isolation_line = (
|
||||
"本次会在隔离子进程里执行高风险 OCCT 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||||
if isolation is not None
|
||||
else ""
|
||||
)
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"确认修改球面(整体)",
|
||||
@@ -4241,6 +4392,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,
|
||||
@@ -4285,6 +4437,7 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=isolation,
|
||||
)
|
||||
|
||||
def resize_torus_major_radius(self) -> None:
|
||||
@@ -4313,6 +4466,7 @@ class WindowActionMixin:
|
||||
mode_label = "主半径(整体)" if mode_key == "major" else "小半径(整体)"
|
||||
face_id = self.selected_face_id
|
||||
plan = self.model.toroidal_radius_plan(face_id, target_radius, mode_key)
|
||||
isolation = self._isolation_for_plan(plan, "resize_torus_radius", [face_id, target_radius, mode_key])
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能修改环面(整体)", str(plan["message"]))
|
||||
self.statusBar().showMessage("环面(整体)修改已阻止")
|
||||
@@ -4320,6 +4474,11 @@ class WindowActionMixin:
|
||||
if plan["risk"] != "low":
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||||
isolation_line = (
|
||||
"本次会在隔离子进程里执行高风险 OCCT 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||||
if isolation is not None
|
||||
else ""
|
||||
)
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
f"确认修改环面{mode_label}",
|
||||
@@ -4338,6 +4497,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,
|
||||
@@ -4386,6 +4546,7 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=isolation,
|
||||
)
|
||||
|
||||
def resize_face_area(self) -> None:
|
||||
@@ -6252,6 +6413,7 @@ class WindowActionMixin:
|
||||
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 = {}
|
||||
isolation = context.get("isolation")
|
||||
if isinstance(isolation, dict) and isolation:
|
||||
@@ -6261,6 +6423,7 @@ class WindowActionMixin:
|
||||
snapshot=snapshot,
|
||||
before_stats=before_stats,
|
||||
before_part_stats=before_part_stats,
|
||||
before_quality=before_quality,
|
||||
before_geometry=before_geometry,
|
||||
)
|
||||
try:
|
||||
@@ -6268,6 +6431,17 @@ class WindowActionMixin:
|
||||
after_snapshot = self.model.snapshot()
|
||||
after_stats = self.model.stats()
|
||||
after_part_stats = self._part_stats_or_none(target_part_id)
|
||||
after_quality = self._edit_quality_info_or_none(self.model, context, target_part_id)
|
||||
quality_warnings = self._verified_edit_quality_warnings(
|
||||
context,
|
||||
before_stats,
|
||||
after_stats,
|
||||
before_part_stats,
|
||||
after_part_stats,
|
||||
before_quality,
|
||||
after_quality,
|
||||
after_model=self.model,
|
||||
)
|
||||
after_geometry = {}
|
||||
except Exception as exc:
|
||||
try:
|
||||
@@ -6298,7 +6472,7 @@ class WindowActionMixin:
|
||||
"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),
|
||||
"quality_warnings": quality_warnings,
|
||||
"after_geometry": after_geometry,
|
||||
"model_polydata": model_polydata,
|
||||
"edge_polydata": edge_polydata,
|
||||
@@ -6314,6 +6488,7 @@ class WindowActionMixin:
|
||||
snapshot: dict[object, object],
|
||||
before_stats,
|
||||
before_part_stats,
|
||||
before_quality: dict[str, object] | None,
|
||||
before_geometry: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
if self.model is None:
|
||||
@@ -6346,7 +6521,7 @@ class WindowActionMixin:
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
command = [sys.executable, "-m", "step_editor.isolated_edit_worker", str(request_path)]
|
||||
command = self._isolated_edit_command(request_path)
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
@@ -6385,11 +6560,23 @@ class WindowActionMixin:
|
||||
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)
|
||||
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)
|
||||
after_quality = self._edit_quality_info_or_none(new_model, context, target_part_id)
|
||||
quality_warnings = self._verified_edit_quality_warnings(
|
||||
context,
|
||||
before_stats,
|
||||
after_stats,
|
||||
before_part_stats,
|
||||
after_part_stats,
|
||||
before_quality,
|
||||
after_quality,
|
||||
after_model=new_model,
|
||||
)
|
||||
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
|
||||
@@ -6404,7 +6591,6 @@ class WindowActionMixin:
|
||||
model_polydata = None
|
||||
edge_polydata = None
|
||||
|
||||
child_message = str(response.get("message") or "隔离子进程编辑完成。")
|
||||
return {
|
||||
"message": f"{child_message} 已通过隔离子进程完成;如果 OCC 崩溃,主程序不会被带崩。",
|
||||
"snapshot": snapshot,
|
||||
@@ -6414,12 +6600,47 @@ class WindowActionMixin:
|
||||
"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),
|
||||
"quality_warnings": quality_warnings,
|
||||
"after_geometry": after_geometry,
|
||||
"model_polydata": model_polydata,
|
||||
"edge_polydata": edge_polydata,
|
||||
}
|
||||
|
||||
def _isolated_edit_command(self, request_path: Path) -> list[str]:
|
||||
if getattr(sys, "frozen", False):
|
||||
return [sys.executable, "--isolated-edit-worker", str(request_path)]
|
||||
return [sys.executable, "-m", "step_editor.isolated_edit_worker", str(request_path)]
|
||||
|
||||
def _preserve_isolated_face_logical_id(
|
||||
self,
|
||||
model: StepModel,
|
||||
context: dict[str, object],
|
||||
result_message: str,
|
||||
) -> None:
|
||||
if not self._context_is_face_parameter_edit(context):
|
||||
return
|
||||
target_logical_id = context.get("target_logical_id")
|
||||
if target_logical_id is None:
|
||||
return
|
||||
located_text = _message_field(result_message, "nearest_face") or _message_field(result_message, "verified_face")
|
||||
if located_text is None:
|
||||
return
|
||||
try:
|
||||
face_id = int(float(located_text))
|
||||
logical_id = int(target_logical_id)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
if not (0 <= face_id < len(model.faces)):
|
||||
return
|
||||
try:
|
||||
face_ids = model.face_region_ids(face_id) or [face_id]
|
||||
except Exception:
|
||||
face_ids = [face_id]
|
||||
try:
|
||||
model.assign_logical_face_region_exclusive(logical_id, face_ids)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def _edit_context_part_id(self, context: dict[str, object]) -> int | None:
|
||||
if self.model is None:
|
||||
return None
|
||||
@@ -6452,13 +6673,508 @@ class WindowActionMixin:
|
||||
return None
|
||||
|
||||
def _part_stats_or_none(self, part_id: int | None):
|
||||
if self.model is None or part_id is None:
|
||||
return self._part_stats_or_none_for_model(self.model, part_id)
|
||||
|
||||
def _part_stats_or_none_for_model(self, model: StepModel | None, part_id: int | None):
|
||||
if model is None or part_id is None:
|
||||
return None
|
||||
try:
|
||||
return self.model.part_topology_stats(part_id)
|
||||
return model.part_topology_stats(part_id)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _edit_quality_info_or_none(
|
||||
self,
|
||||
model: StepModel | None,
|
||||
context: dict[str, object],
|
||||
target_part_id: int | None,
|
||||
) -> dict[str, object] | None:
|
||||
if model is None:
|
||||
return None
|
||||
try:
|
||||
if target_part_id is not None:
|
||||
return model.export_quality_info("part", target_part_id)
|
||||
return model.export_quality_info("all", None)
|
||||
except Exception:
|
||||
try:
|
||||
return model.export_quality_info("all", None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _verified_edit_quality_warnings(
|
||||
self,
|
||||
context: dict[str, object],
|
||||
before_stats,
|
||||
after_stats,
|
||||
before_part_stats,
|
||||
after_part_stats,
|
||||
before_quality: dict[str, object] | None,
|
||||
after_quality: dict[str, object] | None,
|
||||
*,
|
||||
after_model: StepModel | None = None,
|
||||
) -> list[str]:
|
||||
warnings = _edit_quality_warnings(before_part_stats, after_part_stats)
|
||||
blockers: list[str] = []
|
||||
|
||||
if getattr(after_stats, "solids", 0) <= 0:
|
||||
blockers.append("编辑结果里没有检测到 Solid,模型已保持在修改前状态。")
|
||||
if getattr(after_stats, "faces", 0) <= 0:
|
||||
blockers.append("编辑结果里没有检测到 Face,模型已保持在修改前状态。")
|
||||
if getattr(after_stats, "edges", 0) <= 0:
|
||||
blockers.append("编辑结果里没有检测到 Edge,模型已保持在修改前状态。")
|
||||
|
||||
if after_quality is not None:
|
||||
quality_status = str(after_quality.get("quality_status", ""))
|
||||
after_brep_valid = after_quality.get("brep_valid")
|
||||
before_brep_valid = before_quality.get("brep_valid") if before_quality is not None else None
|
||||
quality_warnings = str(after_quality.get("quality_warnings", "")).strip()
|
||||
if quality_status == "blocked":
|
||||
blockers.append(quality_warnings or "编辑结果质量检查被判定为不可用。")
|
||||
elif after_brep_valid is False and before_brep_valid is not False:
|
||||
blockers.append(quality_warnings or "编辑结果没有通过 B-Rep 校验。")
|
||||
elif quality_warnings:
|
||||
warnings.append(quality_warnings)
|
||||
|
||||
if self._context_is_face_parameter_edit(context):
|
||||
if (
|
||||
before_part_stats is not None
|
||||
and after_part_stats is not None
|
||||
and getattr(before_part_stats, "solids", 0) > 0
|
||||
and getattr(after_part_stats, "solids", 0) != getattr(before_part_stats, "solids", 0)
|
||||
):
|
||||
blockers.append(
|
||||
"Face 参数化编辑后目标特征的 Solid 数量发生变化,已阻止接受该结果。"
|
||||
f" Solid: {before_part_stats.solids} -> {after_part_stats.solids}。"
|
||||
)
|
||||
|
||||
target_blocker = self._face_target_integrity_blocker(after_model, context)
|
||||
if target_blocker:
|
||||
blockers.append(target_blocker)
|
||||
|
||||
if blockers:
|
||||
raise RuntimeError(" ".join(blockers))
|
||||
return warnings
|
||||
|
||||
def _face_target_integrity_blocker(
|
||||
self,
|
||||
model: StepModel | None,
|
||||
context: dict[str, object],
|
||||
) -> str:
|
||||
if model is None:
|
||||
return ""
|
||||
parameters = context.get("parameters")
|
||||
if not isinstance(parameters, dict):
|
||||
return ""
|
||||
surface = str(parameters.get("surface") or "")
|
||||
face_ids = self._face_target_candidate_ids(model, context)
|
||||
if not face_ids:
|
||||
if surface in {"cone", "sphere", "torus"} and any(
|
||||
parameters.get(key) not in {"", None}
|
||||
for key in (
|
||||
"target_reference_radius",
|
||||
"target_semi_angle_degrees",
|
||||
"target_radius",
|
||||
"target_major_radius",
|
||||
"target_minor_radius",
|
||||
)
|
||||
):
|
||||
return f"Face 编辑结果里没有找到目标 {surface} 面,模型已恢复到修改前状态。"
|
||||
return ""
|
||||
|
||||
checks: list[tuple[str, bool]] = []
|
||||
target_area = _float_or_none(parameters.get("target_area"))
|
||||
if target_area is not None and target_area > 0:
|
||||
checks.append(("面积", self._face_target_area_matches(model, face_ids, target_area)))
|
||||
|
||||
target_width = _float_or_none(parameters.get("target_face_width"))
|
||||
target_height = _float_or_none(parameters.get("target_face_height"))
|
||||
if target_width is not None or target_height is not None:
|
||||
checks.append(("面宽/面高", self._face_target_size_matches(model, face_ids, target_width, target_height)))
|
||||
|
||||
target_center = _triple_or_none(parameters.get("target_face_center"))
|
||||
if target_center is not None:
|
||||
checks.append(("中心", self._face_target_center_matches(model, face_ids, target_center)))
|
||||
|
||||
target_position = _float_or_none(parameters.get("target_plane_position"))
|
||||
plane_direction = (
|
||||
_unit_triple_or_none(parameters.get("plane_direction"))
|
||||
or _unit_triple_or_none(parameters.get("outward_direction"))
|
||||
)
|
||||
if target_position is not None and plane_direction is not None:
|
||||
checks.append(
|
||||
(
|
||||
"面偏移",
|
||||
self._face_target_plane_position_matches(
|
||||
model,
|
||||
face_ids,
|
||||
target_position,
|
||||
plane_direction,
|
||||
_float_or_none(parameters.get("bbox_diagonal")),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
target_thickness = _float_or_none(parameters.get("shell_target_thickness"))
|
||||
if target_thickness is not None and target_thickness > 0:
|
||||
checks.append(
|
||||
(
|
||||
"薄壁厚度",
|
||||
self._face_target_shell_thickness_matches(model, face_ids, target_thickness),
|
||||
)
|
||||
)
|
||||
|
||||
if surface == "sphere":
|
||||
target_radius = _float_or_none(parameters.get("target_radius"))
|
||||
if target_radius is not None and target_radius > 0:
|
||||
checks.append(("半径", self._face_target_numeric_info_matches(model, face_ids, "radius", target_radius)))
|
||||
elif surface == "cone":
|
||||
resize_strategy = str(parameters.get("resize_strategy") or "")
|
||||
embedded_target_small_radius = _float_or_none(parameters.get("embedded_cone_target_small_radius"))
|
||||
embedded_target_large_radius = _float_or_none(parameters.get("embedded_cone_target_large_radius"))
|
||||
if (
|
||||
embedded_target_small_radius is not None
|
||||
and embedded_target_small_radius > 0
|
||||
and embedded_target_large_radius is not None
|
||||
and embedded_target_large_radius > embedded_target_small_radius
|
||||
):
|
||||
checks.append(
|
||||
(
|
||||
"锥孔边界半径",
|
||||
self._face_target_cone_boundary_radii_match(
|
||||
model,
|
||||
face_ids,
|
||||
embedded_target_small_radius,
|
||||
embedded_target_large_radius,
|
||||
),
|
||||
)
|
||||
)
|
||||
target_reference_radius = _float_or_none(parameters.get("target_reference_radius"))
|
||||
if (
|
||||
target_reference_radius is not None
|
||||
and target_reference_radius > 0
|
||||
and "semi-angle" not in resize_strategy
|
||||
and "bounded-cone-recut" not in resize_strategy
|
||||
):
|
||||
checks.append(
|
||||
(
|
||||
"参考半径",
|
||||
self._face_target_numeric_info_matches(
|
||||
model,
|
||||
face_ids,
|
||||
"reference_radius",
|
||||
target_reference_radius,
|
||||
),
|
||||
)
|
||||
)
|
||||
target_semi_angle_degrees = _float_or_none(parameters.get("target_semi_angle_degrees"))
|
||||
if target_semi_angle_degrees is not None and target_semi_angle_degrees > 0:
|
||||
checks.append(
|
||||
(
|
||||
"半角",
|
||||
self._face_target_angle_degrees_matches(model, face_ids, target_semi_angle_degrees),
|
||||
)
|
||||
)
|
||||
elif surface == "torus":
|
||||
target_major_radius = _float_or_none(parameters.get("target_major_radius"))
|
||||
if target_major_radius is not None and target_major_radius > 0:
|
||||
checks.append(
|
||||
(
|
||||
"主半径",
|
||||
self._face_target_numeric_info_matches(
|
||||
model,
|
||||
face_ids,
|
||||
"major_radius",
|
||||
target_major_radius,
|
||||
),
|
||||
)
|
||||
)
|
||||
target_minor_radius = _float_or_none(parameters.get("target_minor_radius"))
|
||||
if target_minor_radius is not None and target_minor_radius > 0:
|
||||
checks.append(
|
||||
(
|
||||
"小半径",
|
||||
self._face_target_numeric_info_matches(
|
||||
model,
|
||||
face_ids,
|
||||
"minor_radius",
|
||||
target_minor_radius,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
failed = [name for name, ok in checks if not ok]
|
||||
if not failed:
|
||||
return ""
|
||||
return (
|
||||
"Face 编辑结果没有达到目标值;"
|
||||
f"未通过的检查: {', '.join(failed)}。"
|
||||
"模型已恢复到修改前状态。"
|
||||
)
|
||||
|
||||
def _face_target_candidate_ids(self, model: StepModel, context: dict[str, object]) -> list[int]:
|
||||
parameters = context.get("parameters")
|
||||
parameters = parameters if isinstance(parameters, dict) else {}
|
||||
candidates: list[int] = []
|
||||
logical_id = context.get("target_logical_id")
|
||||
if logical_id is not None:
|
||||
try:
|
||||
candidates.extend(model.face_ids_for_logical_id(int(logical_id)))
|
||||
except Exception:
|
||||
pass
|
||||
target_id = context.get("target_id")
|
||||
try:
|
||||
numeric_target_id = int(target_id) if target_id is not None else -1
|
||||
except (TypeError, ValueError):
|
||||
numeric_target_id = -1
|
||||
if 0 <= numeric_target_id < len(model.faces):
|
||||
candidates.append(numeric_target_id)
|
||||
if not candidates:
|
||||
candidates = list(range(len(model.faces)))
|
||||
|
||||
part_id = self._edit_integrity_int_or_none(parameters.get("part_id"))
|
||||
solid_id = self._edit_integrity_int_or_none(parameters.get("solid_id"))
|
||||
surface = str(parameters.get("surface") or "")
|
||||
target_keys = (
|
||||
"target_area",
|
||||
"target_face_width",
|
||||
"target_face_height",
|
||||
"target_face_center",
|
||||
"target_plane_position",
|
||||
"shell_target_thickness",
|
||||
"target_reference_radius",
|
||||
"target_semi_angle_degrees",
|
||||
"embedded_cone_target_small_radius",
|
||||
"embedded_cone_target_large_radius",
|
||||
"target_radius",
|
||||
"target_major_radius",
|
||||
"target_minor_radius",
|
||||
)
|
||||
has_target_value = any(parameters.get(key) not in {"", None} for key in target_keys)
|
||||
|
||||
def filtered(source_ids, *, require_solid: bool = True) -> list[int]:
|
||||
result: list[int] = []
|
||||
for face_id in source_ids:
|
||||
if not (0 <= int(face_id) < len(model.faces)):
|
||||
continue
|
||||
if part_id is not None and int(model.face_part_ids[int(face_id)]) != part_id:
|
||||
continue
|
||||
if (
|
||||
require_solid
|
||||
and solid_id is not None
|
||||
and solid_id >= 0
|
||||
and int(model.face_solid_ids[int(face_id)]) != solid_id
|
||||
):
|
||||
continue
|
||||
if surface in {"plane", "cylinder", "cone", "sphere", "torus"}:
|
||||
try:
|
||||
if model.face_surface_kind(int(face_id)) != surface:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
if int(face_id) not in result:
|
||||
result.append(int(face_id))
|
||||
return result
|
||||
|
||||
result = filtered(candidates)
|
||||
if result:
|
||||
return result
|
||||
if surface in {"plane", "cylinder", "cone", "sphere", "torus"} and has_target_value:
|
||||
result = filtered(range(len(model.faces)))
|
||||
if result:
|
||||
return result
|
||||
if solid_id is not None and solid_id >= 0:
|
||||
result = filtered(range(len(model.faces)), require_solid=False)
|
||||
return result
|
||||
|
||||
def _edit_integrity_int_or_none(self, value: object) -> int | None:
|
||||
if value in {"", None}:
|
||||
return None
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
def _face_target_area_matches(self, model: StepModel, face_ids: list[int], target_area: float) -> bool:
|
||||
tolerance = max(abs(target_area) * 0.02, 1e-4)
|
||||
for face_id in face_ids:
|
||||
try:
|
||||
area = _float_or_none(model.face_info(face_id).get("area"))
|
||||
except Exception:
|
||||
area = None
|
||||
if area is not None and abs(area - target_area) <= tolerance:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _face_target_size_matches(
|
||||
self,
|
||||
model: StepModel,
|
||||
face_ids: list[int],
|
||||
target_width: float | None,
|
||||
target_height: float | None,
|
||||
) -> bool:
|
||||
for face_id in face_ids:
|
||||
try:
|
||||
info = model.face_info(face_id)
|
||||
except Exception:
|
||||
continue
|
||||
width = _float_or_none(info.get("local_face_width"))
|
||||
height = _float_or_none(info.get("local_face_height"))
|
||||
if target_width is not None:
|
||||
if width is None or abs(width - target_width) > max(abs(target_width) * 0.02, 1e-4):
|
||||
continue
|
||||
if target_height is not None:
|
||||
if height is None or abs(height - target_height) > max(abs(target_height) * 0.02, 1e-4):
|
||||
continue
|
||||
return True
|
||||
return False
|
||||
|
||||
def _face_target_center_matches(
|
||||
self,
|
||||
model: StepModel,
|
||||
face_ids: list[int],
|
||||
target_center: tuple[float, float, float],
|
||||
) -> bool:
|
||||
reference = max(max(abs(item) for item in target_center), 1.0)
|
||||
tolerance = max(reference * 1e-5, 1e-4)
|
||||
for face_id in face_ids:
|
||||
try:
|
||||
info = model.face_info(face_id)
|
||||
except Exception:
|
||||
continue
|
||||
center = _triple_or_none(info.get("area_center")) or _triple_or_none(info.get("bbox_center"))
|
||||
if center is None:
|
||||
continue
|
||||
distance = math.sqrt(sum((center[index] - target_center[index]) ** 2 for index in range(3)))
|
||||
if distance <= tolerance:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _face_target_plane_position_matches(
|
||||
self,
|
||||
model: StepModel,
|
||||
face_ids: list[int],
|
||||
target_position: float,
|
||||
direction: tuple[float, float, float],
|
||||
bbox_diagonal: float | None,
|
||||
) -> bool:
|
||||
tolerance = max((bbox_diagonal or 1.0) * 1e-4, abs(target_position) * 1e-5, 1e-4)
|
||||
for face_id in face_ids:
|
||||
try:
|
||||
info = model.face_info(face_id)
|
||||
except Exception:
|
||||
continue
|
||||
origin = _triple_or_none(info.get("plane_origin"))
|
||||
if origin is None:
|
||||
continue
|
||||
position = sum(origin[index] * direction[index] for index in range(3))
|
||||
if abs(position - target_position) <= tolerance:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _face_target_shell_thickness_matches(
|
||||
self,
|
||||
model: StepModel,
|
||||
face_ids: list[int],
|
||||
target_thickness: float,
|
||||
) -> bool:
|
||||
tolerance = max(abs(target_thickness) * 0.03, 1e-4)
|
||||
for face_id in face_ids:
|
||||
try:
|
||||
info = model.feature_info(face_id)
|
||||
except Exception:
|
||||
continue
|
||||
current = _float_or_none(info.get("shell_thickness_estimate"))
|
||||
if current is not None and abs(current - target_thickness) <= tolerance:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _face_target_numeric_info_matches(
|
||||
self,
|
||||
model: StepModel,
|
||||
face_ids: list[int],
|
||||
info_key: str,
|
||||
target_value: float,
|
||||
) -> bool:
|
||||
tolerance = max(abs(target_value) * 0.01, 1e-4)
|
||||
for face_id in face_ids:
|
||||
try:
|
||||
value = _float_or_none(model.face_info(face_id).get(info_key))
|
||||
except Exception:
|
||||
value = None
|
||||
if value is not None and abs(value - target_value) <= tolerance:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _face_target_cone_boundary_radii_match(
|
||||
self,
|
||||
model: StepModel,
|
||||
face_ids: list[int],
|
||||
target_small_radius: float,
|
||||
target_large_radius: float,
|
||||
) -> bool:
|
||||
tolerance = max(abs(target_large_radius) * 0.01, abs(target_small_radius) * 0.01, 1e-4)
|
||||
for face_id in face_ids:
|
||||
try:
|
||||
info = model.face_info(face_id)
|
||||
axis_point = _triple_or_none(info.get("axis_point"))
|
||||
axis_direction = _unit_triple_or_none(info.get("axis"))
|
||||
if axis_point is None or axis_direction is None:
|
||||
continue
|
||||
circles = model._conical_face_circle_boundaries(face_id, axis_point, axis_direction)
|
||||
except Exception:
|
||||
continue
|
||||
radii = sorted(float(circle["radius"]) for circle in circles)
|
||||
if len(radii) != 2:
|
||||
continue
|
||||
if abs(radii[0] - target_small_radius) <= tolerance and abs(radii[1] - target_large_radius) <= tolerance:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _face_target_angle_degrees_matches(
|
||||
self,
|
||||
model: StepModel,
|
||||
face_ids: list[int],
|
||||
target_degrees: float,
|
||||
) -> bool:
|
||||
tolerance = max(abs(target_degrees) * 0.01, 0.05)
|
||||
for face_id in face_ids:
|
||||
try:
|
||||
angle = _float_or_none(model.face_info(face_id).get("semi_angle"))
|
||||
except Exception:
|
||||
angle = None
|
||||
if angle is None:
|
||||
continue
|
||||
degrees = abs(math.degrees(angle))
|
||||
if abs(degrees - target_degrees) <= tolerance:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _context_is_face_parameter_edit(self, context: dict[str, object]) -> bool:
|
||||
parameters = context.get("parameters")
|
||||
if not isinstance(parameters, dict):
|
||||
return False
|
||||
strategy = str(parameters.get("resize_strategy") or parameters.get("translate_strategy") or "")
|
||||
if any(
|
||||
token in strategy
|
||||
for token in (
|
||||
"face",
|
||||
"plane-offset",
|
||||
"push-pull-planar",
|
||||
"push-pull-shell",
|
||||
"shell-thickness",
|
||||
"cone",
|
||||
"sphere",
|
||||
"torus",
|
||||
"radial-affine-scale",
|
||||
)
|
||||
):
|
||||
return True
|
||||
operation = str(context.get("operation_name") or "")
|
||||
target_kind = str(context.get("target_kind") or "")
|
||||
return target_kind in {"face", "feature"} and any(
|
||||
token in operation for token in ("Face", "面", "薄壁", "推拉", "圆锥", "球面", "环面")
|
||||
)
|
||||
|
||||
def _begin_edit_task(self, operation_name: str) -> None:
|
||||
self.operation_in_progress = True
|
||||
self._update_action_states()
|
||||
|
||||
Reference in New Issue
Block a user