feat: 完善 STEP 一级参数化编辑识别与关系式建模

This commit is contained in:
2026-08-14 18:42:39 +08:00
parent 70b59c1de6
commit a633b5a338
26 changed files with 4676 additions and 77 deletions
+213 -2
View File
@@ -74,6 +74,13 @@ def _unit_triple_or_none(value: object) -> tuple[float, float, float] | None:
return (triple[0] / length, triple[1] / length, triple[2] / length)
def _int_or_none(value: object) -> int | None:
try:
return int(value)
except (TypeError, ValueError):
return None
def _compact_plan_value(value: object) -> str:
text = _format_value(value)
return text if len(text) <= 120 else text[:117] + "..."
@@ -1592,8 +1599,9 @@ class WindowActionMixin:
if plan.get("status") == "blocked":
self._show_blocked_plan_message(title, plan, blocked_status)
return False
supports_isolation = bool(plan.get("supports_isolation")) or self._quick_edit_title_supports_isolation(title)
if (
not self._quick_edit_title_supports_isolation(title)
not supports_isolation
and self._block_unisolated_high_risk_operation(f"已阻止高风险{title}", plan)
):
return False
@@ -1620,7 +1628,7 @@ class WindowActionMixin:
+ f"\n{plan.get('message', '')}\n\n"
+ (
"本次会在隔离子进程里执行高风险 OCC 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
if str(plan.get("risk")) == "high" and self._quick_edit_title_supports_isolation(title)
if str(plan.get("risk")) == "high" and supports_isolation
else ""
)
+ "为避免复杂 STEP 在界面线程卡死,本次不会先生成红/绿预览;"
@@ -1687,6 +1695,10 @@ class WindowActionMixin:
"resize_sphere_radius",
"resize_torus_radius",
"resize_cylindrical_hole",
"edit_cylindrical_holes_by_refs",
"resize_cylindrical_holes_by_refs",
"move_cylindrical_holes_by_offset",
"suppress_cylindrical_holes_by_refs",
"resize_cylindrical_owning_scale",
"move_cylindrical_hole_axis",
"suppress_cylindrical_hole",
@@ -2351,6 +2363,200 @@ class WindowActionMixin:
isolation=isolation,
)
def _multi_selected_hole_refs(self) -> list[dict[str, object]]:
refs: list[dict[str, object]] = []
for item in getattr(self, "multi_selected_hole_entries", []) or []:
if not isinstance(item, dict):
continue
center = _triple_or_none(item.get("axis_center"))
diameter = _float_or_none(item.get("diameter"))
face_id = _int_or_none(item.get("face_id"))
logical_id = _int_or_none(item.get("logical_id"))
if center is None or diameter is None or diameter <= 0:
continue
refs.append(
{
"face_id": face_id,
"logical_id": logical_id,
"diameter": float(diameter),
"axis_center": [float(center[0]), float(center[1]), float(center[2])],
"part_id": item.get("part_id"),
"solid_id": item.get("solid_id"),
}
)
return refs
def _run_multi_selected_hole_edit(
self,
*,
target_diameter: float | None = None,
offset: tuple[float, float, float] | None = None,
) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再批量修改孔。"):
return
refs = self._multi_selected_hole_refs()
if len(refs) < 2:
QMessageBox.information(self, "不能修改", "请先按 Ctrl 选择至少两个完整圆柱孔。")
return
if target_diameter is None and offset is None:
QMessageBox.information(self, "不能修改", "请先输入孔径目标值或位置偏移量。")
return
if target_diameter is not None and target_diameter <= 0:
QMessageBox.information(self, "不能修改", "孔径必须大于 0。")
return
if offset is not None and _vector_length(offset) <= 1e-9:
offset = None
if target_diameter is None and offset is None:
QMessageBox.information(self, "不能修改", "位置偏移量为 0,不需要修改。")
return
operation_label_parts: list[str] = []
if target_diameter is not None:
operation_label_parts.append("改孔径")
if offset is not None:
operation_label_parts.append("移动位置")
operation_name = "批量孔" + " + ".join(operation_label_parts)
logical_ids = [item.get("logical_id") for item in refs if item.get("logical_id") is not None]
refs_arg = [dict(item) for item in refs]
offset_arg = list(offset) if offset is not None else None
isolation = {
"operation": "edit_cylindrical_holes_by_refs",
"args": [refs_arg, target_diameter, offset_arg],
"timeout_seconds": 300.0,
"reason": "multi-hole-isolated-occ-edit",
}
self.clear_edit_preview(render=False)
def action():
return self.model.edit_cylindrical_holes_by_refs(
refs_arg,
target_diameter=target_diameter,
offset=offset,
)
self._run_edit_action(
action,
operation_name=operation_name,
target=f"{len(refs)} holes",
parameters={
"surface": "cylinder",
"feature_type": "multi cylindrical hole",
"feature_guess": "hole/groove candidate",
"multi_selected_count": len(refs),
"multi_selected_logical_ids": tuple(logical_ids),
"target_diameter": target_diameter,
"axis_move_vector": offset,
"resize_strategy": "multi-hole-fill-and-recut",
"edit_strategy_label": "批量圆柱孔参数化",
"edit_semantics": "按当前多选孔引用逐个重新定位孔组,统一修改孔径或按相同偏移移动位置;失败时整体回滚。",
"multi_hole_status": "ready",
"multi_hole_risk": "medium",
"quick_preflight": True,
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind=None,
target_id=None,
isolation=isolation,
)
def resize_multi_selected_holes(self) -> None:
try:
target_diameter = float(self.hole_diameter_input.text())
except (AttributeError, ValueError):
QMessageBox.information(self, "不能修改", "请输入数字形式的目标孔径。")
return
self._run_multi_selected_hole_edit(target_diameter=target_diameter)
def move_multi_selected_holes_by_offset(self) -> None:
try:
offset = (
float(self.translate_x_input.text()),
float(self.translate_y_input.text()),
float(self.translate_z_input.text()),
)
except (AttributeError, ValueError):
QMessageBox.information(self, "不能修改", "请输入 X/Y/Z 三个数字形式的位置偏移量。")
return
self._run_multi_selected_hole_edit(offset=offset)
def suppress_multi_selected_holes(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再批量封堵孔。"):
return
refs = self._multi_selected_hole_refs()
if len(refs) < 2:
QMessageBox.information(self, "不能修改", "请先按 Ctrl 选择至少两个完整圆柱孔。")
return
refs_arg = [dict(item) for item in refs]
logical_ids = [item.get("logical_id") for item in refs if item.get("logical_id") is not None]
isolation = {
"operation": "suppress_cylindrical_holes_by_refs",
"args": [refs_arg],
"timeout_seconds": 300.0,
"reason": "multi-hole-suppress-isolated-occ-edit",
}
self.clear_edit_preview(render=False)
def action():
return self.model.suppress_cylindrical_holes_by_refs(refs_arg)
self._run_edit_action(
action,
operation_name="批量封堵孔",
target=f"{len(refs)} holes",
parameters={
"surface": "cylinder",
"feature_type": "multi cylindrical hole",
"feature_guess": "hole/groove candidate",
"multi_selected_count": len(refs),
"multi_selected_logical_ids": tuple(logical_ids),
"suppress_strategy": "multi-hole-fill",
"edit_strategy_label": "批量圆柱孔封堵",
"edit_semantics": "按当前多选孔引用逐个封堵;任意孔失败时整次批量操作会回滚。",
"multi_hole_status": "ready",
"multi_hole_risk": "medium",
"quick_preflight": True,
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind=None,
target_id=None,
isolation=isolation,
)
def apply_multi_selected_hole_property_edit(self, changed: list[tuple[int, dict[str, object], str]]) -> None:
target_diameter: float | None = None
offset: tuple[float, float, float] | None = None
for _row, spec, text in changed:
validation_error = self._property_target_validation_error(spec, text)
if validation_error:
QMessageBox.information(self, "目标值无效", validation_error)
return
key = str(spec.get("key") or "")
try:
if key == "multi_hole_diameter":
diameter_value = float(text)
if target_diameter is not None and abs(target_diameter - diameter_value) > 1e-9:
QMessageBox.information(self, "目标值无效", "孔径和半径换算后的目标孔径不一致,请只修改其中一个。")
return
target_diameter = diameter_value
elif key == "multi_hole_radius":
diameter_value = float(text) * 2.0
if target_diameter is not None and abs(target_diameter - diameter_value) > 1e-9:
QMessageBox.information(self, "目标值无效", "孔径和半径换算后的目标孔径不一致,请只修改其中一个。")
return
target_diameter = diameter_value
elif key == "multi_hole_position_delta":
offset = self._parse_property_vector3(text)
except ValueError as exc:
QMessageBox.information(self, "目标值无效", str(exc))
return
self._run_multi_selected_hole_edit(target_diameter=target_diameter, offset=offset)
def move_cylindrical_slot_axis(self) -> None:
if self.model is None:
return
@@ -8979,6 +9185,11 @@ class WindowActionMixin:
if isinstance(timings, dict):
timings["finish_ui"] = time.perf_counter() - finish_started
locator_note = self._locate_operation_record(record)
relation_note = ""
if hasattr(self, "_refresh_relation_formulas_after_model_edit"):
relation_note = self._refresh_relation_formulas_after_model_edit()
if relation_note:
locator_note = f"{locator_note}\n{relation_note}" if locator_note else relation_note
except Exception as exc:
rollback_message = self._restore_failed_edit_snapshot(result.get("snapshot") if isinstance(result, dict) else None)
self._end_edit_task(clear_preview=True)