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
+147 -14
View File
@@ -51,10 +51,13 @@ def validate_scdm_edit_result(
summary_check: dict[str, object] = {"ok": None, "reason": "not-run", "message": "SCDM summary check needs the old and new caches."}
topology_check: dict[str, object] = {"ok": None, "reason": "not-run", "message": "Object drift check needs the old and new SCDM caches."}
context_failure: dict[str, object] | None = None
if before_cache and after_cache:
# B-Rep 有效只说明 STEP 能读回来;还要比较 SCDM cache,防止一次局部编辑
# 顺手让其它可识别对象消失、漂移或变得歧义。
summary_check = check_scdm_summary_delta(before_cache, after_cache, capability_key=capability_key)
if summary_check.get("ok") is False:
return {
context_failure = {
"ok": False,
"reason": str(summary_check.get("reason") or "summary-drift"),
"message": str(summary_check.get("message") or "SCDM result changed the model summary too much."),
@@ -70,14 +73,15 @@ def validate_scdm_edit_result(
capability_key=capability_key,
)
if topology_check.get("ok") is not True:
return {
"ok": False,
"reason": str(topology_check.get("reason") or "unexpected-object-drift"),
"message": str(topology_check.get("message") or "SCDM result changed unrelated recognized objects."),
"topologyCheck": topology_check,
"brep": brep,
"editResult": dict(edit_result),
}
if context_failure is None:
context_failure = {
"ok": False,
"reason": str(topology_check.get("reason") or "unexpected-object-drift"),
"message": str(topology_check.get("message") or "SCDM result changed unrelated recognized objects."),
"topologyCheck": topology_check,
"brep": brep,
"editResult": dict(edit_result),
}
if _is_removal_capability(capability_key) and (not before_signature or not after_cache):
return {
@@ -91,7 +95,12 @@ def validate_scdm_edit_result(
}
if capability_key == "pattern.segment_spacing":
check = _check_pattern_segment_spacing_edit_result(edit_result, expected_target, tolerance=tolerance)
check = _check_pattern_segment_spacing_edit_result(
edit_result,
expected_target,
before_signature=before_signature,
tolerance=tolerance,
)
if check.get("ok") is not True:
return {
"ok": False,
@@ -103,6 +112,15 @@ def validate_scdm_edit_result(
"brep": brep,
"editResult": dict(edit_result),
}
warning = _context_guard_warning_if_target_verified(
context_failure,
capability_key=capability_key,
target_check=check,
summary_check=summary_check,
brep=brep,
)
if context_failure is not None and warning is None:
return context_failure
return {
"ok": True,
"reason": "ok",
@@ -115,11 +133,14 @@ def validate_scdm_edit_result(
"topologyCheck": topology_check,
"brep": brep,
"editResult": dict(edit_result),
"validationWarnings": [warning] if warning else [],
}
matched: dict[str, object] | None = None
removal_check: dict[str, object] = {"ok": None, "reason": "not-run", "message": "Removal check is not needed for this capability."}
if before_signature and after_cache:
# 修改后的拓扑 ID 可能变化,不能按旧 Face ID 验证;这里用几何签名在新 cache
# 里找唯一对象,再用它做目标值回测和关系式 ID 续接。
match = match_scdm_object_by_signature(before_signature, after_cache, capability_key=capability_key)
status = str(match.get("status") or "")
if _is_removal_capability(capability_key):
@@ -176,6 +197,16 @@ def validate_scdm_edit_result(
else:
check = {"ok": None, "reason": "not-run", "message": "Target check needs a matched object and an expected target."}
warning = _context_guard_warning_if_target_verified(
context_failure,
capability_key=capability_key,
target_check=check,
summary_check=summary_check,
brep=brep,
)
if context_failure is not None and warning is None:
return context_failure
return {
"ok": True,
"reason": "ok",
@@ -188,6 +219,62 @@ def validate_scdm_edit_result(
"topologyCheck": topology_check,
"brep": brep,
"editResult": dict(edit_result),
"validationWarnings": [warning] if warning else [],
}
def _context_guard_warning_if_target_verified(
context_failure: Mapping[str, object] | None,
*,
capability_key: str,
target_check: Mapping[str, object],
summary_check: Mapping[str, object],
brep: Mapping[str, object],
) -> dict[str, object] | None:
# SCDM 重新导出 STEP 时有时会重新划分 body count,但目标对象和 B-Rep 都正确。
# 对已能目标回测的几何能力,将这种情况降级为警告,避免把成功修改误回滚。
if brep.get("ok") is not True:
return None
if target_check.get("ok") is not True:
return None
if str(summary_check.get("reason") or "") != "body-count-repartitioned":
return None
if not _is_target_verified_scdm_geometry_capability(capability_key):
return None
if not isinstance(context_failure, Mapping):
return {
"reason": "body-count-repartitioned",
"message": str(summary_check.get("message") or "SCDM changed body count while re-exporting the STEP result."),
"summaryCheck": dict(summary_check),
}
warning = {
"reason": str(context_failure.get("reason") or "context-guard-warning"),
"message": str(context_failure.get("message") or "SCDM cache context changed after edit."),
"summaryCheck": dict(summary_check),
}
topology_check = context_failure.get("topologyCheck")
if isinstance(topology_check, Mapping):
warning["topologyCheck"] = dict(topology_check)
return warning
def _is_target_verified_scdm_geometry_capability(capability_key: str) -> bool:
return capability_key in {
"face.offset",
"hole.diameter",
"hole.position",
"slot.width",
"slot.depth",
"slot.position",
"boss.height",
"boss.diameter",
"boss.position",
"round.radius",
"chamfer.distance",
"pattern.spacing",
"pattern.segment_spacing",
"pattern.instance_position",
"shell.thickness",
}
@@ -453,6 +540,7 @@ def _check_pattern_segment_spacing_edit_result(
edit_result: Mapping[str, object],
expected_target: object,
*,
before_signature: Mapping[str, object] | None,
tolerance: float,
) -> dict[str, object]:
applied = edit_result.get("applied")
@@ -469,11 +557,53 @@ def _check_pattern_segment_spacing_edit_result(
expected = _number(expected_target)
check = _number_check(actual, expected, "pattern.segment_spacing", tolerance)
if check.get("ok") is True:
spacing_mode = str(applied.get("spacingMode") or "").strip()
known_modes = {
"segment_after",
"segment_before",
"segment_split",
"segment_single_left",
"segment_single_right",
}
if spacing_mode not in known_modes:
return {
"ok": False,
"reason": "pattern-segment-spacing-mode-missing",
"message": "SCDM edit result did not report a known local spacing mode.",
"spacingMode": spacing_mode,
"knownModes": sorted(known_modes),
}
expected_modes = _expected_pattern_segment_spacing_modes(before_signature)
if expected_modes and spacing_mode not in expected_modes:
return {
"ok": False,
"reason": "pattern-segment-spacing-mode-mismatch",
"message": f"SCDM edit used {spacing_mode}, but the selected modeling intent expected one of {sorted(expected_modes)}.",
"spacingMode": spacing_mode,
"expectedModes": sorted(expected_modes),
}
check["segmentIndex"] = applied.get("segmentIndex")
check["spacingMode"] = applied.get("spacingMode")
check["spacingMode"] = spacing_mode
return check
def _expected_pattern_segment_spacing_modes(signature: Mapping[str, object] | None) -> set[str]:
if not isinstance(signature, Mapping):
return set()
moving_side = str(signature.get("movingSide") or "").strip().lower()
if moving_side in {"after", "right"}:
return {"segment_after"}
if moving_side in {"before", "left"}:
return {"segment_before"}
if moving_side in {"split", "both", "center"}:
return {"segment_split"}
if moving_side in {"single_left", "only_left"}:
return {"segment_single_left"}
if moving_side in {"single_right", "only_right"}:
return {"segment_single_right"}
return set()
def check_scdm_summary_delta(
before_cache: Mapping[str, object],
after_cache: Mapping[str, object],
@@ -493,11 +623,14 @@ def check_scdm_summary_delta(
body_after = _int_or_none(after_summary.get("bodyCount"))
if body_before is not None and body_after is not None and body_before != body_after:
return {
"ok": False,
"reason": "summary-drift",
"message": f"SCDM result changed body count unexpectedly: {body_before} -> {body_after}.",
"ok": None,
"reason": "body-count-repartitioned",
"message": f"SCDM result changed body count during STEP re-export: {body_before} -> {body_after}.",
"before": dict(before_summary),
"after": dict(after_summary),
"metric": "bodyCount",
"beforeValue": body_before,
"afterValue": body_after,
}
for key, label in (("faceCount", "Face"), ("edgeCount", "Edge"), ("objectCount", "对象")):