feat: 完善 SCDM-first 参数化编辑交付版
This commit is contained in:
+429
-17
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user