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