feat: 接入 SCDM 优先编辑闭环并支持阵列局部间距
接入 SCDM probe/edit/cache/校验链路,增强孔组、阵列、关系式和参数表交互。 支持阵列相邻段间距、移动意图切换、结果回滚校验,并补充对应回归脚本。
This commit is contained in:
@@ -79,10 +79,75 @@ def validate_scdm_edit_result(
|
||||
"editResult": dict(edit_result),
|
||||
}
|
||||
|
||||
if _is_removal_capability(capability_key) and (not before_signature or not after_cache):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "removal-check-unavailable",
|
||||
"message": "Removed feature cannot be verified without the old feature signature and the new SCDM cache.",
|
||||
"summaryCheck": summary_check,
|
||||
"topologyCheck": topology_check,
|
||||
"brep": brep,
|
||||
"editResult": dict(edit_result),
|
||||
}
|
||||
|
||||
if capability_key == "pattern.segment_spacing":
|
||||
check = _check_pattern_segment_spacing_edit_result(edit_result, expected_target, tolerance=tolerance)
|
||||
if check.get("ok") is not True:
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": str(check.get("reason") or "target-check-failed"),
|
||||
"message": str(check.get("message") or "SCDM result did not reach the target segment spacing."),
|
||||
"targetCheck": check,
|
||||
"summaryCheck": summary_check,
|
||||
"topologyCheck": topology_check,
|
||||
"brep": brep,
|
||||
"editResult": dict(edit_result),
|
||||
}
|
||||
return {
|
||||
"ok": True,
|
||||
"reason": "ok",
|
||||
"message": "SCDM edit result passed the available validation checks.",
|
||||
"output_step": str(output_step),
|
||||
"matchedObject": None,
|
||||
"targetCheck": check,
|
||||
"removalCheck": {"ok": None, "reason": "not-run", "message": "Removal check is not needed for this capability."},
|
||||
"summaryCheck": summary_check,
|
||||
"topologyCheck": topology_check,
|
||||
"brep": brep,
|
||||
"editResult": dict(edit_result),
|
||||
}
|
||||
|
||||
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:
|
||||
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):
|
||||
removal_check = _check_removed_object_match(match)
|
||||
if removal_check.get("ok") is not True:
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": str(removal_check.get("reason") or "feature-still-present"),
|
||||
"message": str(removal_check.get("message") or "Removed feature is still present in the new SCDM cache."),
|
||||
"removalCheck": removal_check,
|
||||
"match": match,
|
||||
"brep": brep,
|
||||
"editResult": dict(edit_result),
|
||||
}
|
||||
check = {"ok": True, "reason": "removed", "message": "Target feature disappeared from the new SCDM cache."}
|
||||
return {
|
||||
"ok": True,
|
||||
"reason": "ok",
|
||||
"message": "SCDM edit result passed the available validation checks.",
|
||||
"output_step": str(output_step),
|
||||
"matchedObject": None,
|
||||
"targetCheck": check,
|
||||
"removalCheck": removal_check,
|
||||
"summaryCheck": summary_check,
|
||||
"topologyCheck": topology_check,
|
||||
"brep": brep,
|
||||
"editResult": dict(edit_result),
|
||||
}
|
||||
if status != "unique":
|
||||
return {
|
||||
"ok": False,
|
||||
@@ -118,6 +183,7 @@ def validate_scdm_edit_result(
|
||||
"output_step": str(output_step),
|
||||
"matchedObject": matched,
|
||||
"targetCheck": check,
|
||||
"removalCheck": removal_check,
|
||||
"summaryCheck": summary_check,
|
||||
"topologyCheck": topology_check,
|
||||
"brep": brep,
|
||||
@@ -251,6 +317,8 @@ def check_scdm_unedited_objects(
|
||||
continue
|
||||
if edited_signature and _same_signature_subject(signature, edited_signature):
|
||||
continue
|
||||
if edited_signature and _is_expected_pattern_spacing_subject(signature, edited_signature, capability_key=capability_key):
|
||||
continue
|
||||
checked += 1
|
||||
match = match_scdm_object_by_signature(signature, after_cache, capability_key=capability_key)
|
||||
status = str(match.get("status") or "")
|
||||
@@ -324,15 +392,88 @@ def check_scdm_target(
|
||||
actual_vector = _vector(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "center"))
|
||||
expected_vector = _vector(expected_target)
|
||||
return _vector_check(actual_vector, expected_vector, "hole.position", tolerance)
|
||||
if capability_key in {"slot.position", "boss.position", "pattern.instance_position"}:
|
||||
actual_vector = _vector(
|
||||
_capability_value(raw_object, capability_key)
|
||||
or _geometry_value(raw_object, "center")
|
||||
or _geometry_value(raw_object, "axisCenter")
|
||||
or _geometry_value(raw_object, "instanceCenter")
|
||||
)
|
||||
expected_vector = _vector(expected_target)
|
||||
return _vector_check(actual_vector, expected_vector, capability_key, tolerance)
|
||||
if capability_key == "slot.width":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "width"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "slot.width", tolerance)
|
||||
if capability_key == "slot.depth":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "depth"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "slot.depth", tolerance)
|
||||
if capability_key == "boss.height":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "height"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "boss.height", tolerance)
|
||||
if capability_key == "boss.diameter":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "diameter"))
|
||||
if actual is None:
|
||||
radius = _number(_geometry_value(raw_object, "radius"))
|
||||
actual = radius * 2.0 if radius is not None else None
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "boss.diameter", tolerance)
|
||||
if capability_key == "round.radius":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "radius"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "round.radius", tolerance)
|
||||
if capability_key == "chamfer.distance":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "distance") or _geometry_value(raw_object, "offset"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "chamfer.distance", tolerance)
|
||||
if capability_key == "pattern.spacing":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "spacing") or _geometry_value(raw_object, "pitch"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "pattern.spacing", tolerance)
|
||||
if capability_key == "pattern.segment_spacing":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "segmentSpacing") or _geometry_value(raw_object, "spacing") or _geometry_value(raw_object, "pitch"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "pattern.segment_spacing", tolerance)
|
||||
if capability_key == "shell.thickness":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "thickness"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "shell.thickness", tolerance)
|
||||
if capability_key == "face.offset":
|
||||
actual = _number(_capability_value(raw_object, capability_key) or _geometry_value(raw_object, "offset") or _geometry_value(raw_object, "planeOffset"))
|
||||
expected = _number(expected_target)
|
||||
return _number_check(actual, expected, "face.offset", tolerance)
|
||||
if capability_key == "feature.fill":
|
||||
return {"ok": True, "reason": "not-applicable", "message": "feature.fill is checked by object disappearance in the caller."}
|
||||
if _is_removal_capability(capability_key):
|
||||
return {"ok": True, "reason": "not-applicable", "message": f"{capability_key} is checked by object disappearance in the caller."}
|
||||
return {"ok": None, "reason": "unsupported-post-check", "message": f"No target checker is registered for {capability_key}."}
|
||||
|
||||
|
||||
def _check_pattern_segment_spacing_edit_result(
|
||||
edit_result: Mapping[str, object],
|
||||
expected_target: object,
|
||||
*,
|
||||
tolerance: float,
|
||||
) -> dict[str, object]:
|
||||
applied = edit_result.get("applied")
|
||||
nested = edit_result.get("result")
|
||||
if not isinstance(applied, Mapping) and isinstance(nested, Mapping):
|
||||
applied = nested.get("applied")
|
||||
if not isinstance(applied, Mapping):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "missing-edit-applied",
|
||||
"message": "SCDM edit result did not report the applied local segment spacing.",
|
||||
}
|
||||
actual = _number(applied.get("segmentSpacing") or applied.get("targetSpacing"))
|
||||
expected = _number(expected_target)
|
||||
check = _number_check(actual, expected, "pattern.segment_spacing", tolerance)
|
||||
if check.get("ok") is True:
|
||||
check["segmentIndex"] = applied.get("segmentIndex")
|
||||
check["spacingMode"] = applied.get("spacingMode")
|
||||
return check
|
||||
|
||||
|
||||
def check_scdm_summary_delta(
|
||||
before_cache: Mapping[str, object],
|
||||
after_cache: Mapping[str, object],
|
||||
@@ -401,6 +542,27 @@ def _signature_score(before: Mapping[str, object], after: Mapping[str, object],
|
||||
if before_surface and before_surface == after_surface:
|
||||
score += 1.0
|
||||
|
||||
before_components = _component_locator_keys(before.get("componentLocators") or before.get("bodyLocators"))
|
||||
after_components = _component_locator_keys(after.get("componentLocators") or after.get("bodyLocators"))
|
||||
if before_components and after_components:
|
||||
if before_components & after_components:
|
||||
score += 3.0
|
||||
else:
|
||||
return 0.0
|
||||
|
||||
before_body = _int_or_none(before.get("bodyIndex"))
|
||||
after_body = _int_or_none(after.get("bodyIndex"))
|
||||
if before_body is not None and after_body is not None and before_body == after_body:
|
||||
score += 2.0
|
||||
before_face_ordinal = _int_or_none(before.get("faceOrdinal"))
|
||||
after_face_ordinal = _int_or_none(after.get("faceOrdinal"))
|
||||
if before_face_ordinal is not None and before_face_ordinal == after_face_ordinal:
|
||||
score += 1.0
|
||||
before_edge_ordinal = _int_or_none(before.get("edgeOrdinal"))
|
||||
after_edge_ordinal = _int_or_none(after.get("edgeOrdinal"))
|
||||
if before_edge_ordinal is not None and before_edge_ordinal == after_edge_ordinal:
|
||||
score += 1.0
|
||||
|
||||
before_faces = set(_int_values(before.get("faceIds")))
|
||||
after_faces = set(_int_values(after.get("faceIds")))
|
||||
if before_faces and after_faces:
|
||||
@@ -413,8 +575,13 @@ def _signature_score(before: Mapping[str, object], after: Mapping[str, object],
|
||||
if before_edges and after_edges and before_edges & after_edges:
|
||||
score += 0.5
|
||||
|
||||
if capability_key != "hole.position":
|
||||
center_score = _vector_distance_score(_vector(before.get("center")), _vector(after.get("center")))
|
||||
if not _is_position_capability(capability_key):
|
||||
before_center = _vector(before.get("center"))
|
||||
after_center = _vector(after.get("center"))
|
||||
if _is_removal_capability(capability_key) and before_center and after_center:
|
||||
if _vector_error(before_center, after_center) > _vector_tolerance(before_center, after_center):
|
||||
return 0.0
|
||||
center_score = _vector_distance_score(before_center, after_center)
|
||||
score += center_score
|
||||
|
||||
axis_score = _axis_score(_vector(before.get("axis")), _vector(after.get("axis")))
|
||||
@@ -426,6 +593,98 @@ def _signature_score(before: Mapping[str, object], after: Mapping[str, object],
|
||||
return score
|
||||
|
||||
|
||||
def _is_position_capability(capability_key: str) -> bool:
|
||||
return capability_key in {"hole.position", "slot.position", "boss.position", "pattern.instance_position"} or capability_key.endswith(".position")
|
||||
|
||||
|
||||
def _is_removal_capability(capability_key: str) -> bool:
|
||||
return capability_key in {"feature.fill", "feature.delete_round_or_chamfer"} or capability_key.endswith(".remove") or capability_key.startswith("feature.delete")
|
||||
|
||||
|
||||
def _check_removed_object_match(match: Mapping[str, object]) -> dict[str, object]:
|
||||
status = str(match.get("status") or "")
|
||||
if status == "none":
|
||||
return {"ok": True, "reason": "removed", "message": "Edited feature is no longer present in the new SCDM cache."}
|
||||
if status == "unique":
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "feature-still-present",
|
||||
"message": "SCDM reported success, but the edited feature still matches an object in the new cache.",
|
||||
"match": dict(match),
|
||||
}
|
||||
if status == "multiple":
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "feature-removal-ambiguous",
|
||||
"message": "SCDM reported success, but multiple new objects still match the edited feature.",
|
||||
"match": dict(match),
|
||||
}
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": f"feature-removal-{status or 'failed'}",
|
||||
"message": str(match.get("message") or "Removed feature could not be verified."),
|
||||
"match": dict(match),
|
||||
}
|
||||
|
||||
|
||||
def _is_expected_pattern_spacing_subject(
|
||||
signature: Mapping[str, object],
|
||||
edited_signature: Mapping[str, object],
|
||||
*,
|
||||
capability_key: str,
|
||||
) -> bool:
|
||||
if capability_key not in {"pattern.spacing", "pattern.segment_spacing"}:
|
||||
return False
|
||||
if str(edited_signature.get("objectType") or "") != "linear_pattern":
|
||||
return False
|
||||
touched_faces = set(_int_values(edited_signature.get("faceIds")))
|
||||
touched_bodies = set(_int_values(edited_signature.get("bodyIndices")))
|
||||
touched_components = _component_locator_keys(edited_signature.get("componentLocators"))
|
||||
for instance in _pattern_instances(edited_signature):
|
||||
touched_faces.update(_int_values(instance.get("faceIds")))
|
||||
body_index = _int_or_none(instance.get("bodyIndex"))
|
||||
if body_index is not None:
|
||||
touched_bodies.add(body_index)
|
||||
touched_bodies.update(_int_values(instance.get("bodyIndices")))
|
||||
touched_components.update(_component_locator_keys(instance.get("componentLocators") or instance.get("bodyLocators")))
|
||||
|
||||
subject_faces = set(_int_values(signature.get("faceIds")))
|
||||
if touched_faces and subject_faces and touched_faces.intersection(subject_faces):
|
||||
return True
|
||||
subject_body = _int_or_none(signature.get("bodyIndex"))
|
||||
if subject_body is not None and subject_body in touched_bodies:
|
||||
return True
|
||||
subject_bodies = set(_int_values(signature.get("bodyIndices")))
|
||||
if touched_bodies and subject_bodies and touched_bodies.intersection(subject_bodies):
|
||||
return True
|
||||
subject_components = _component_locator_keys(signature.get("componentLocators") or signature.get("bodyLocators"))
|
||||
return bool(touched_components and subject_components and touched_components.intersection(subject_components))
|
||||
|
||||
|
||||
def _pattern_instances(signature: Mapping[str, object]) -> list[Mapping[str, object]]:
|
||||
value = signature.get("patternInstances")
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return []
|
||||
return [item for item in value if isinstance(item, Mapping)]
|
||||
|
||||
|
||||
def _component_locator_keys(value: object) -> set[str]:
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return set()
|
||||
result: set[str] = set()
|
||||
for locator in value:
|
||||
if not isinstance(locator, Mapping):
|
||||
continue
|
||||
path = _ordered_int_values(locator.get("componentPath"))
|
||||
if path:
|
||||
result.add("path:" + ".".join(str(item) for item in path))
|
||||
continue
|
||||
component_index = _int_or_none(locator.get("componentIndex"))
|
||||
if component_index is not None:
|
||||
result.add("index:" + str(component_index))
|
||||
return result
|
||||
|
||||
|
||||
def _signature_has_enough_identity(signature: Mapping[str, object]) -> bool:
|
||||
if _vector(signature.get("center")) and _vector(signature.get("axis")):
|
||||
return True
|
||||
@@ -473,12 +732,13 @@ def _unchanged_signature_still_matches(before: Mapping[str, object], after: Mapp
|
||||
if abs(float(before_diameter) - float(after_diameter)) > tolerance:
|
||||
return False
|
||||
|
||||
before_offset = _number(before.get("planeOffset"))
|
||||
after_offset = _number(after.get("planeOffset"))
|
||||
if before_offset is not None and after_offset is not None:
|
||||
tolerance = max(abs(before_offset), abs(after_offset), 1.0) * 1.0e-5
|
||||
if abs(float(before_offset) - float(after_offset)) > tolerance:
|
||||
return False
|
||||
for key in ("planeOffset", "width", "depth", "height", "distance", "spacing", "pitch", "thickness"):
|
||||
before_value = _number(before.get(key))
|
||||
after_value = _number(after.get(key))
|
||||
if before_value is not None and after_value is not None:
|
||||
tolerance = max(abs(before_value), abs(after_value), 1.0) * 1.0e-5
|
||||
if abs(float(before_value) - float(after_value)) > tolerance:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -645,6 +905,22 @@ def _int_values(value: object) -> list[int]:
|
||||
return result
|
||||
|
||||
|
||||
def _ordered_int_values(value: object) -> list[int]:
|
||||
if isinstance(value, (str, bytes)) or value is None:
|
||||
return []
|
||||
try:
|
||||
values = list(value) # type: ignore[arg-type]
|
||||
except TypeError:
|
||||
return []
|
||||
result: list[int] = []
|
||||
for item in values:
|
||||
try:
|
||||
result.append(int(item))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return result
|
||||
|
||||
|
||||
def _raw_summary(cache: Mapping[str, object]) -> Mapping[str, object]:
|
||||
diagnostics = cache.get("diagnostics")
|
||||
if not isinstance(diagnostics, Mapping):
|
||||
|
||||
Reference in New Issue
Block a user