feat: 完善 SCDM-first 参数化编辑交付版

This commit is contained in:
2026-08-20 17:12:01 +08:00
parent 4e7877e05c
commit b4feab24d2
21 changed files with 3015 additions and 1729 deletions
+429 -17
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from datetime import datetime
import json
import math
from pathlib import Path
import re
@@ -482,7 +483,7 @@ class WindowStateMixin:
self.renderer.RemoveActor(self.pick_marker_actor)
self.pick_marker_actor = None
if getattr(self, "render_window", None) is not None:
self.render_window.Render()
self._render_window_safely()
def _with_pick_info(
self,
@@ -1911,7 +1912,7 @@ class WindowStateMixin:
headline = "当前支持:常见 Face、孔、槽、凸台、圆角/倒角、壳体、Edge"
detail = ""
tooltip = (
"能改:Face 偏移,孔径/位置,槽位置,凸台位置/尺寸,部分槽宽/槽深,简单圆角/倒角,Edge 长度等。\n"
"能改:Face 偏移,孔径/位置,证据完整的简单槽/长圆槽,凸台位置/尺寸,简单圆角/倒角,Edge 长度等。\n"
"能识别:基础拓扑、曲面类型、一级关系、孔组、槽、凸台、圆角/倒角候选。\n"
"暂不能:原 CAD 历史树、复杂装配约束、二级/三级拓扑自动传播。"
)
@@ -2064,7 +2065,7 @@ class WindowStateMixin:
"## 已能修改",
"- **Face**:偏移/推拉,部分稳定矩形平面的长度、宽度,局部壳体厚度。",
"- **孔**:直径、半径、位置、盲孔深度、封堵;拆成多片的圆柱孔会按同一孔组处理。",
"- **槽**:简单槽/长圆槽位置、槽宽、槽深、弧长、总长、中心距。",
"- **槽**证据完整的简单槽/长圆槽可改位置、槽宽、槽深、弧长、总长、中心距;中置信度“圆柱孔/槽候选”只表示已识别到槽可能性,不等于已经可改",
"- **凸台**:圆柱凸台位置、直径、高度;部分矩形凸台/口袋的长度、宽度、高度或深度。",
"- **圆角/倒角**:简单已有圆角半径、已有等距倒角,直线 Edge 新增圆角/倒角。",
"- **Edge**:直线 Edge 长度/端点,圆或椭圆 Edge 的半径类参数。",
@@ -2081,6 +2082,7 @@ class WindowStateMixin:
"- 任意复杂自由曲面的直接变形。",
"- 二级/三级拓扑自动传播。",
"- 阵列间距联动、跨特征强约束、复杂公式组求解。",
"- 只有槽候选、但缺少槽宽/槽深/槽底面/方向/成对端面等证据的复杂槽或多槽组。",
"- 复杂圆角链/倒角链批量重建。",
"- 无法通过 B-Rep 校验或目标回测的修改会被阻止并回滚。",
"",
@@ -2119,6 +2121,21 @@ class WindowStateMixin:
if selection_status:
info["scdm_selection_status"] = selection_status
scdm_specs = self._scdm_property_specs_for_selection() if hasattr(self, "_scdm_property_specs_for_selection") else []
filter_message = ""
if scdm_specs and hasattr(self, "_filter_scdm_specs_for_local_recognition"):
try:
action_info = self._selected_action_info() if hasattr(self, "_selected_action_info") else info
except Exception:
action_info = info
try:
scdm_specs, filter_message = self._filter_scdm_specs_for_local_recognition(
list(scdm_specs),
dict(action_info or {}),
)
except Exception:
filter_message = ""
if filter_message and not selection_status:
info["scdm_selection_status"] = filter_message
enabled_labels = [
str(spec.get("label") or spec.get("scdm_capability_key") or "").strip()
for spec in scdm_specs
@@ -3081,15 +3098,47 @@ class WindowStateMixin:
"请先导入模型,并输入类似 Face87.直径 = Face85.直径 的关系式。",
)
remove_button = getattr(self, "remove_relation_formula_button", None)
toggle_button = getattr(self, "toggle_relation_formula_button", None)
formula_list = getattr(self, "relation_formula_list", None)
selected_ids = self._selected_relation_formula_ids()
selected_items = [
dict(item)
for item in (getattr(self, "relation_formula_items", []) or [])
if int(item.get("id", -1)) in selected_ids
]
selected_has_enabled = any(bool(item.get("enabled", True)) for item in selected_items)
if isinstance(toggle_button, QPushButton):
toggle_button.setText("停用公式" if selected_has_enabled else "启用公式")
self._set_control_state(
toggle_button,
bool(can_modify_formula_set and selected_items),
"临时停用或重新启用选中的关系式,并按当前启用公式重新计算模型。",
"请先在已有关系式列表里选择一条公式;公式计算中暂不能切换。",
)
if isinstance(remove_button, QPushButton):
has_selected = bool(formula_list is not None and formula_list.selectedItems())
self._set_control_state(
remove_button,
bool(can_modify_formula_set and has_selected),
bool(can_modify_formula_set and selected_items),
"删除选中的关系式,并按剩余公式重新计算模型。",
"请先在已有关系式列表里选择一条公式;公式计算中暂不能删除。",
)
import_button = getattr(self, "import_relation_formula_button", None)
if isinstance(import_button, QPushButton):
self._set_control_state(
import_button,
bool(can_modify_formula_set),
"从 JSON 导入一组关系式,替换当前公式组,并按启用公式重新计算模型。",
"请先导入模型;公式计算中暂不能导入。",
)
export_button = getattr(self, "export_relation_formula_button", None)
if isinstance(export_button, QPushButton):
has_formulas = bool(getattr(self, "relation_formula_items", []) or [])
self._set_control_state(
export_button,
bool(has_formulas),
"把当前关系式导出为 JSON,便于复用或交给外部流程。",
"当前还没有可导出的关系式。",
)
def add_relation_formula(self) -> None:
if self.model is None:
@@ -3143,11 +3192,7 @@ class WindowStateMixin:
return
if self._edit_busy("请等待当前几何计算完成后再删除公式。"):
return
ids = {
int(item.data(Qt.ItemDataRole.UserRole))
for item in formula_list.selectedItems()
if item.data(Qt.ItemDataRole.UserRole) is not None
}
ids = self._selected_relation_formula_ids()
if not ids:
return
self.relation_formula_items = [
@@ -3157,15 +3202,218 @@ class WindowStateMixin:
]
self._refresh_relation_formula_list()
self._update_property_apply_state()
if self.relation_formula_items:
if self._enabled_relation_formula_items():
self.statusBar().showMessage(f"已删除 {len(ids)} 条关系式,正在按剩余公式更新模型...")
self._start_relation_formula_reapply(reason="remove")
elif self.relation_formula_items:
restored = self._restore_relation_formula_base_snapshot()
message = "当前没有启用公式,模型已恢复到公式基准状态。" if restored else "当前没有启用公式。"
self.statusBar().showMessage(f"已删除 {len(ids)} 条关系式,{message}")
self._refresh_relation_formula_list()
self._update_property_apply_state()
else:
restored = self._restore_relation_formula_base_snapshot()
self._clear_relation_formula_runtime_state(clear_items=False)
message = "模型已恢复到添加公式前状态。" if restored else "已删除关系式。"
self.statusBar().showMessage(f"已删除 {len(ids)} 条关系式,{message}")
def toggle_selected_relation_formula(self) -> None:
formula_list = getattr(self, "relation_formula_list", None)
if formula_list is None:
return
if self._edit_busy("请等待当前几何计算完成后再切换公式。"):
return
ids = self._selected_relation_formula_ids()
if not ids:
return
selected_items = [
dict(item)
for item in (getattr(self, "relation_formula_items", []) or [])
if int(item.get("id", -1)) in ids
]
enable_selected = not any(bool(item.get("enabled", True)) for item in selected_items)
changed: list[dict[str, object]] = []
for raw_item in getattr(self, "relation_formula_items", []) or []:
item = dict(raw_item)
if int(item.get("id", -1)) in ids:
item["enabled"] = enable_selected
if enable_selected:
item["status"] = "ready"
item["message"] = "已启用,等待重新计算。"
else:
item["status"] = "disabled"
item["message"] = "已停用,不参与计算。"
changed.append(item)
self.relation_formula_items = changed
self._refresh_relation_formula_list()
self._update_property_apply_state()
action = "启用" if enable_selected else "停用"
if self._enabled_relation_formula_items():
self.statusBar().showMessage(f"{action} {len(ids)} 条关系式,正在按启用公式更新模型...")
self._start_relation_formula_reapply(reason="toggle")
else:
restored = self._restore_relation_formula_base_snapshot()
message = "模型已恢复到公式基准状态。" if restored else "当前没有启用公式。"
self.statusBar().showMessage(f"{action} {len(ids)} 条关系式,{message}")
self._refresh_relation_formula_list()
self._update_property_apply_state()
def export_relation_formulas(self, path: str | Path | bool | None = None) -> bool:
interactive = path is None or isinstance(path, bool)
if isinstance(path, bool):
path = None
formulas = list(getattr(self, "relation_formula_items", []) or [])
if not formulas:
self.statusBar().showMessage("当前没有可导出的关系式。")
if interactive:
QMessageBox.information(self, "导出公式", "当前没有可导出的关系式。")
return False
if path is None:
start_dir = Path(getattr(self, "step_path", "") or Path.cwd()).parent
selected, _ = QFileDialog.getSaveFileName(
self,
"导出关系式",
str(start_dir / "relation_formulas.json"),
"JSON (*.json);;所有文件 (*.*)",
)
if not selected:
return False
path = selected
output_path = Path(path)
output_path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"schema": "python-occt.relation-formulas.v1",
"exportedAt": datetime.now().isoformat(timespec="seconds"),
"sourceStep": str(getattr(self, "step_path", "") or ""),
"formulas": self._relation_formula_export_rows(),
}
output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
self.statusBar().showMessage(f"已导出 {len(payload['formulas'])} 条关系式:{output_path}")
return True
def import_relation_formulas(self, path: str | Path | bool | None = None) -> bool:
interactive = path is None or isinstance(path, bool)
if isinstance(path, bool):
path = None
if self.model is None:
self.statusBar().showMessage("请先导入模型后再导入关系式。")
if interactive:
QMessageBox.information(self, "导入公式", "请先导入模型后再导入关系式。")
return False
if self._edit_busy("请等待当前几何计算完成后再导入公式。"):
return False
if path is None:
start_dir = Path(getattr(self, "step_path", "") or Path.cwd()).parent
selected, _ = QFileDialog.getOpenFileName(
self,
"导入关系式",
str(start_dir),
"JSON (*.json);;所有文件 (*.*)",
)
if not selected:
return False
path = selected
input_path = Path(path)
try:
payload = json.loads(input_path.read_text(encoding="utf-8"))
imported_items = self._relation_formula_items_from_import_payload(payload)
except Exception as exc:
message = f"导入公式失败:{exc}"
self.statusBar().showMessage(message)
if interactive:
QMessageBox.information(self, "导入公式失败", message)
return False
self._ensure_relation_formula_base_snapshot()
self.relation_formula_items = imported_items
next_ids = [int(item.get("id", 0) or 0) for item in imported_items]
self.relation_formula_next_id = (max(next_ids) + 1) if next_ids else 1
self._refresh_relation_formula_list()
self._update_property_apply_state()
enabled_count = len(self._enabled_relation_formula_items())
if enabled_count:
self.statusBar().showMessage(f"已导入 {len(imported_items)} 条关系式,正在按 {enabled_count} 条启用公式更新模型...")
self._start_relation_formula_reapply(reason="import")
else:
self.statusBar().showMessage(f"已导入 {len(imported_items)} 条关系式;当前没有启用公式。")
return True
def _relation_formula_export_rows(self) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
for index, raw_item in enumerate(getattr(self, "relation_formula_items", []) or [], start=1):
item = dict(raw_item)
rows.append(
{
"index": index,
"text": str(item.get("text") or "").strip(),
"enabled": bool(item.get("enabled", True)),
"status": str(item.get("status") or "").strip(),
"message": str(item.get("message") or "").strip(),
}
)
return rows
def _relation_formula_items_from_import_payload(self, payload: object) -> list[dict[str, object]]:
raw_items = payload.get("formulas") if isinstance(payload, dict) else payload
if not isinstance(raw_items, list):
raise RelationFormulaError("JSON 必须是公式数组,或包含 formulas 数组。")
formulas = []
items: list[dict[str, object]] = []
for index, raw_item in enumerate(raw_items, start=1):
if isinstance(raw_item, str):
text = raw_item
enabled = True
elif isinstance(raw_item, dict):
text = str(raw_item.get("text") or raw_item.get("formula") or "").strip()
enabled = bool(raw_item.get("enabled", True))
else:
raise RelationFormulaError(f"{index} 条公式格式不正确。")
if not text:
raise RelationFormulaError(f"{index} 条公式为空。")
try:
formula = parse_relation_formula(text)
except RelationFormulaError as exc:
raise RelationFormulaError(f"{index} 条公式无效:{exc}") from exc
formulas.append(formula)
items.append(
{
"id": index,
"text": formula.text,
"enabled": enabled,
"status": "ready" if enabled else "disabled",
"message": "导入后等待计算。" if enabled else "已停用,不参与计算。",
"signatures": {},
}
)
if not items:
raise RelationFormulaError("JSON 里没有关系式。")
try:
validate_relation_formula_graph(formulas)
except RelationFormulaError as exc:
raise RelationFormulaError(f"导入的公式组无效:{exc}") from exc
return items
def _selected_relation_formula_ids(self) -> set[int]:
formula_list = getattr(self, "relation_formula_list", None)
if formula_list is None:
return set()
ids: set[int] = set()
for item in formula_list.selectedItems():
value = item.data(Qt.ItemDataRole.UserRole)
if value is None:
continue
try:
ids.add(int(value))
except (TypeError, ValueError):
continue
return ids
def _enabled_relation_formula_items(self) -> list[dict[str, object]]:
return [
dict(item)
for item in (getattr(self, "relation_formula_items", []) or [])
if bool(item.get("enabled", True))
]
def _clear_relation_formula_runtime_state(self, *, clear_items: bool) -> None:
if clear_items:
self.relation_formula_items = []
@@ -3264,6 +3512,10 @@ class WindowStateMixin:
label = "添加"
elif reason == "dependency":
label = "依赖参数变化"
elif reason == "toggle":
label = "切换"
elif reason == "import":
label = "导入"
else:
label = "删除"
self.statusBar().showMessage(f"{label}公式后正在重新计算:共 {self.relation_formula_replay_total} 条。")
@@ -3424,7 +3676,10 @@ class WindowStateMixin:
message = str(item.get("message") or "").strip()
text = str(item.get("text") or "")
suffix = ""
if status == "invalid":
enabled = bool(item.get("enabled", True))
if not enabled:
suffix = f" [停用:{message or '不参与计算'}]"
elif status == "invalid":
suffix = f" [失效:{message or '需要重新选择'}]"
elif status == "pending":
suffix = f" [待选择:{message or '目标不在当前参数表'}]"
@@ -3435,7 +3690,9 @@ class WindowStateMixin:
row_item = QListWidgetItem(f"{text}{suffix}")
row_item.setData(Qt.ItemDataRole.UserRole, int(item.get("id", -1)))
row_item.setToolTip(message or text)
if status == "invalid":
if not enabled:
row_item.setForeground(QColor("#6b7280"))
elif status == "invalid":
row_item.setForeground(QColor("#b91c1c"))
elif status == "pending":
row_item.setForeground(QColor("#92400e"))
@@ -4527,6 +4784,7 @@ class WindowStateMixin:
if self.selected_kind == "multi_feature" and str(action_info.get("multi_selection_kind") or "") == "holes":
return self._multi_hole_property_specs(action_info)
scdm_specs = self._scdm_property_specs_for_selection()
scdm_specs, scdm_filter_message = self._filter_scdm_specs_for_local_recognition(scdm_specs, action_info)
if self._prefer_local_face_offset_over_scdm(scdm_specs, action_info):
scdm_specs = [
spec
@@ -4538,9 +4796,17 @@ class WindowStateMixin:
"避免 SCDM 长时间直接建模计算。"
)
else:
self.scdm_selection_status_message = self._scdm_selection_status_message(scdm_specs)
self.scdm_selection_status_message = scdm_filter_message or self._scdm_selection_status_message(scdm_specs)
if scdm_specs and any(bool(spec.get("enabled")) for spec in scdm_specs):
return [spec for spec in scdm_specs if bool(spec.get("enabled"))]
if scdm_filter_message:
return []
if self._should_hold_local_cylindrical_specs_for_scdm(action_info):
self.scdm_selection_status_message = (
"SCDM 正在按需识别当前大模型对象;圆柱、槽、圆角这类容易误分的对象,"
"识别完成前不开放本地兜底孔/槽参数。"
)
return []
editable_specs, _used_keys = self._editable_property_specs(action_info)
if self.selected_kind == "feature":
return self._feature_context_property_specs(editable_specs, action_info)
@@ -4554,6 +4820,46 @@ class WindowStateMixin:
and _property_parameter_is_visible(spec)
]
def _filter_scdm_specs_for_local_recognition(
self,
scdm_specs: list[dict[str, object]],
action_info: dict[str, object],
) -> tuple[list[dict[str, object]], str]:
if not scdm_specs or self.selected_kind not in {"feature", "face"}:
return scdm_specs, ""
conflicting_keys = {"round.radius", "chamfer.distance", "feature.delete_round_or_chamfer"}
if not any(str(spec.get("scdm_capability_key") or "") in conflicting_keys for spec in scdm_specs):
return scdm_specs, ""
feature_guess = str(action_info.get("feature_guess") or "").strip()
feature_type = str(action_info.get("feature_type") or "").strip()
if feature_guess in {"round/fillet candidate", "chamfer candidate"} or any(
token in feature_type for token in ("圆角", "倒圆", "倒角")
):
return scdm_specs, ""
local_slot_or_pocket = bool(
action_info.get("blind_split_cylindrical_pocket")
or action_info.get("blind_split_cylindrical_groove")
or "复杂槽" in feature_type
or ("" in feature_type and "圆柱孔" not in feature_type)
)
if not local_slot_or_pocket:
return scdm_specs, ""
filtered = [
spec
for spec in scdm_specs
if str(spec.get("scdm_capability_key") or "") not in conflicting_keys
]
if len(filtered) == len(scdm_specs):
return scdm_specs, ""
return (
filtered,
"SCDM 返回了圆角/倒角删除能力,但当前对象本地识别为盲孔/槽凹坑或复杂槽;"
"为避免把槽误删成圆角,已隐藏该冲突操作。",
)
def _prefer_local_face_offset_over_scdm(
self,
scdm_specs: list[dict[str, object]],
@@ -4578,6 +4884,35 @@ class WindowStateMixin:
) or 0
return inner_wires > 0 or boundary_edges >= 16
def _should_hold_local_cylindrical_specs_for_scdm(self, action_info: dict[str, object]) -> bool:
if self.selected_kind not in {"feature", "face"} or self.model is None:
return False
if not bool(getattr(self, "_large_model_interaction_mode", lambda: False)()):
return False
state = str(getattr(self, "scdm_feature_cache_state", "") or "")
if state not in {"deferred", "running", "empty", "stale"}:
return False
surface = str(action_info.get("surface") or "").strip().lower()
if surface == "cylinder":
return True
feature_guess = str(action_info.get("feature_guess") or "").strip().lower()
feature_type = str(action_info.get("feature_type") or "").strip()
cylindrical_markers = (
"cylindrical_feature_side_face_count",
"diameter",
"radius",
"axis",
"axis_point",
"slot_chord_width_estimate",
"slot_sagitta_depth_estimate",
"existing_fillet_radius_estimate",
)
if any(action_info.get(key) is not None and str(action_info.get(key)) != "" for key in cylindrical_markers):
return True
return any(token in feature_guess for token in ("hole", "groove", "round", "fillet", "boss")) or any(
token in feature_type for token in ("", "", "圆角", "倒圆", "凸台", "外圆")
)
def _scdm_selection_status_message(self, scdm_specs: list[dict[str, object]]) -> str:
if self.selected_kind not in {"feature", "face", "edge", "solid", "part"}:
return ""
@@ -4617,16 +4952,20 @@ class WindowStateMixin:
return []
face_ids: set[int] = set()
edge_ids: set[int] = set()
solid_ids: set[int] = set()
if self.selected_kind in {"feature", "face"} and self.selected_face_id is not None:
face_ids.update(self._scdm_selection_face_match_ids(int(self.selected_face_id)))
if self.selected_kind == "edge" and self.selected_edge_id is not None:
edge_ids.add(int(self.selected_edge_id))
if not face_ids and not edge_ids:
if self.selected_kind == "solid" and self.selected_solid_id is not None:
solid_ids.add(int(self.selected_solid_id))
if not face_ids and not edge_ids and not solid_ids:
return []
return property_specs_from_scdm_cache(
cache,
selected_face_ids=face_ids,
selected_edge_ids=edge_ids,
selected_solid_ids=solid_ids,
execution_ready=getattr(self, "scdm_edit_runner_ready", False),
)
@@ -4917,7 +5256,8 @@ class WindowStateMixin:
) -> list[dict[str, object]]:
def no_editable_feature_dimensions_spec() -> dict[str, object]:
reason = str(
action_info.get("freeform_face_blockers")
self._no_editable_feature_dimension_reason(action_info)
or action_info.get("freeform_face_blockers")
or action_info.get("recognition_blockers")
or action_info.get("local_face_deform_blocker")
or action_info.get("feature_edit_actions")
@@ -4925,6 +5265,9 @@ class WindowStateMixin:
).strip()
if not reason:
reason = "当前识别结果没有稳定可修改参数;详细原因请看诊断信息。"
existing_message = str(getattr(self, "scdm_selection_status_message", "") or "").strip()
if not existing_message or existing_message.startswith("SCDM 未在当前对象上识别到"):
self.scdm_selection_status_message = reason
return {
"key": "no_editable_feature_dimensions",
"label": "可修改参数",
@@ -4968,6 +5311,51 @@ class WindowStateMixin:
rows = root_dimensions + related_rows
return rows or [no_editable_feature_dimensions_spec()]
def _no_editable_feature_dimension_reason(self, action_info: dict[str, object]) -> str:
feature_type = str(action_info.get("feature_type") or "").strip()
feature_guess = str(action_info.get("feature_guess") or "").strip()
surface = str(action_info.get("surface") or "").strip()
slot_status = str(action_info.get("slot_status") or "").strip()
slot_blockers = str(action_info.get("slot_blockers") or action_info.get("recognition_blockers") or "").strip()
slotish = bool(
"" in feature_type
or bool(action_info.get("blind_split_cylindrical_groove"))
or bool(action_info.get("blind_split_cylindrical_pocket"))
or (
surface == "cylinder"
and feature_guess == "hole/groove candidate"
and not _is_effectively_full_cylinder(action_info)
)
)
if not slotish:
return ""
if slot_status == "blocked":
return slot_blockers or "当前槽属于复杂槽/多槽组,暂时不能稳定修改。"
if slot_status != "candidate":
state = str(getattr(self, "scdm_feature_cache_state", "") or "").strip()
if state in {"deferred", "running", "empty", "stale"}:
return (
"当前对象只识别为圆柱孔/槽候选,还没有确认成可编辑的简单槽/长圆槽;"
"需要等 SCDM 识别或本地计划拿到稳定的槽宽、槽深、槽底面/方向或成对端面证据后,才会显示槽参数。"
)
return (
"当前对象只识别为圆柱孔/槽候选,SCDM/cache 没有返回可执行的槽宽、槽深或槽位置能力;"
"软件进度里的槽编辑只针对证据完整的简单槽/长圆槽。"
)
missing: list[str] = []
if _float_or_none(action_info.get("slot_chord_width_estimate")) is None:
missing.append("槽宽")
if _float_or_none(action_info.get("slot_sagitta_depth_estimate")) is None:
missing.append("槽深")
if not _int_values(action_info.get("feature_bottom_face_ids")) and not _int_values(action_info.get("feature_slot_face_ids")):
missing.append("槽底面/槽组")
if missing:
return (
f"当前槽候选缺少可稳定执行的{''.join(missing)}证据;"
"为避免把复杂槽误改坏,暂不在参数表开放槽参数。"
)
return ""
def _ordered_property_info_items(self, info: dict[str, object]) -> list[tuple[str, object]]:
items = self._ordered_info_items(info)
if not self._is_feature_like_info(info):
@@ -8673,6 +9061,8 @@ class WindowStateMixin:
return
item = queue.pop(0)
self.property_batch_queue = queue
# 每改完一个参数都会重新加载/识别模型,原来的表格行可能已经失效;
# 所以下一个批量项必须用 key/能力信息重新定位当前行。
row = self._property_batch_row_for_item(item)
if row is None:
self._clear_property_batch_state()
@@ -8895,6 +9285,8 @@ class WindowStateMixin:
thread = QThread(self)
def action() -> dict[str, object]:
# Worker 线程只做外部 SCDM 调用和文件等待,不碰 Qt 控件和当前 StepModel。
# 完成后的模型替换、弹窗、cache 刷新都在 queued slot 里回 UI 线程处理。
return run_scdm_edit_job(
source_step,
capability_key=capability_key,
@@ -9018,7 +9410,26 @@ class WindowStateMixin:
}
self._end_edit_task(clear_preview=True)
self.statusBar().showMessage("SCDM 修改完成,正在重新读取结果模型...")
self.load_step(output_step, background=True)
# 先把 result.step 加载到当前模型,再进入 _finish_pending_scdm_edit_reload。
# 只有加载后的 OCCT B-Rep 和新 SCDM cache 都通过校验,才真正记录历史。
loaded = False
if hasattr(self, "_load_step_sync"):
loaded = bool(
self._load_step_sync(
output_step,
deflection=float(getattr(self, "preview_load_deflection", 0.35) or 0.35),
show_internal_edges=bool(self._show_same_domain_internal_edges()) if hasattr(self, "_show_same_domain_internal_edges") else False,
status_prefix="读取 SCDM 修改结果",
defer_edges=True,
reset_camera=False,
)
)
else:
self.load_step(output_step, background=False)
loaded = True
if not loaded and isinstance(getattr(self, "pending_scdm_edit_reload", None), dict):
self.pending_scdm_edit_reload = None
self._after_property_edit_finished(success=False)
@Slot(str)
def _fail_scdm_edit_action(self, message: str) -> None:
@@ -9052,6 +9463,7 @@ class WindowStateMixin:
return
if isinstance(before_cache, dict) and isinstance(after_cache, dict):
signature = pending.get("object_signature")
# 结果模型已经显示出来了,但还没承诺成功;验证失败会立即恢复修改前快照。
validation = validate_scdm_edit_result(
pending.get("edit_result") if isinstance(pending.get("edit_result"), dict) else {},
before_signature=signature if isinstance(signature, dict) else None,