4e7877e05c
接入 SCDM probe/edit/cache/校验链路,增强孔组、阵列、关系式和参数表交互。 支持阵列相邻段间距、移动意图切换、结果回滚校验,并补充对应回归脚本。
657 lines
31 KiB
Python
657 lines
31 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
from step_editor.scdm_result_validator import ( # noqa: E402
|
|
build_scdm_id_mapping,
|
|
check_scdm_summary_delta,
|
|
check_scdm_unedited_objects,
|
|
check_scdm_target,
|
|
match_scdm_object_by_signature,
|
|
rewrite_scdm_relation_formula_ids,
|
|
validate_scdm_edit_result,
|
|
)
|
|
|
|
|
|
def _assert(condition: bool, message: str) -> None:
|
|
if not condition:
|
|
raise AssertionError(message)
|
|
|
|
|
|
def _hole(object_id: str, face_id: int, *, diameter: float, center: tuple[float, float, float]) -> dict[str, object]:
|
|
return {
|
|
"objectId": object_id,
|
|
"objectType": "hole",
|
|
"geometrySignature": {
|
|
"objectType": "hole",
|
|
"faceIds": [face_id],
|
|
"surfaceType": "cylinder",
|
|
"center": list(center),
|
|
"axis": [0.0, 0.0, 1.0],
|
|
"diameter": diameter,
|
|
},
|
|
"capabilities": [
|
|
{"key": "hole.diameter", "currentValue": diameter},
|
|
{"key": "hole.position", "currentValue": list(center)},
|
|
],
|
|
}
|
|
|
|
|
|
def _feature(
|
|
object_id: str,
|
|
object_type: str,
|
|
face_id: int,
|
|
*,
|
|
center: tuple[float, float, float],
|
|
capability_key: str,
|
|
) -> dict[str, object]:
|
|
return {
|
|
"objectId": object_id,
|
|
"objectType": object_type,
|
|
"geometrySignature": {
|
|
"objectType": object_type,
|
|
"faceIds": [face_id],
|
|
"center": list(center),
|
|
"axis": [0.0, 0.0, 1.0],
|
|
},
|
|
"capabilities": [
|
|
{"key": capability_key, "currentValue": list(center)},
|
|
],
|
|
}
|
|
|
|
|
|
def _cache(*objects: dict[str, object]) -> dict[str, object]:
|
|
return {
|
|
"schemaVersion": 1,
|
|
"source": "SCDM",
|
|
"objects": list(objects),
|
|
"diagnostics": {},
|
|
}
|
|
|
|
|
|
def _cache_with_summary(summary: dict[str, int], *objects: dict[str, object]) -> dict[str, object]:
|
|
cache = _cache(*objects)
|
|
cache["diagnostics"] = {"raw_summary": dict(summary)}
|
|
return cache
|
|
|
|
|
|
def main() -> int:
|
|
with tempfile.TemporaryDirectory(prefix="step_editor_scdm_validate_") as temp:
|
|
root = Path(temp)
|
|
output_step = root / "result.step"
|
|
output_step.write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8")
|
|
|
|
before = _cache(
|
|
_hole("hole:85", 85, diameter=0.5, center=(0.5, 1.0, 9.5)),
|
|
_hole("hole:87", 87, diameter=0.5, center=(2.0, 1.0, 9.5)),
|
|
)
|
|
after = _cache(
|
|
_hole("hole:90", 90, diameter=0.75, center=(0.5, 1.0, 9.5)),
|
|
_hole("hole:91", 91, diameter=0.5, center=(2.0, 1.0, 9.5)),
|
|
)
|
|
before_signature = before["objects"][0]["geometrySignature"] # type: ignore[index]
|
|
|
|
match = match_scdm_object_by_signature(before_signature, after, capability_key="hole.diameter")
|
|
_assert(match.get("status") == "unique", f"changed diameter should still match by center/axis/type: {match}")
|
|
_assert(match.get("object", {}).get("objectId") == "hole:90", f"wrong match: {match}")
|
|
|
|
ok = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=before_signature,
|
|
before_cache=before,
|
|
after_cache=after,
|
|
capability_key="hole.diameter",
|
|
expected_target=0.75,
|
|
edited_object_id="hole:85",
|
|
brep_validator=lambda path: {"ok": path.is_file(), "reason": "ok"},
|
|
)
|
|
_assert(ok.get("ok") is True, f"validated edit should pass: {ok}")
|
|
_assert(ok.get("targetCheck", {}).get("ok") is True, f"target diameter should be checked: {ok}")
|
|
_assert(ok.get("topologyCheck", {}).get("ok") is True, f"unchanged objects should be checked: {ok}")
|
|
|
|
drift_after = _cache(_hole("hole:90", 90, diameter=0.75, center=(0.5, 1.0, 9.5)))
|
|
drift = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=before_signature,
|
|
before_cache=before,
|
|
after_cache=drift_after,
|
|
capability_key="hole.diameter",
|
|
expected_target=0.75,
|
|
edited_object_id="hole:85",
|
|
)
|
|
_assert(drift.get("ok") is False and drift.get("reason") == "unexpected-object-drift", f"missing unrelated hole should fail: {drift}")
|
|
direct_drift = check_scdm_unedited_objects(before, drift_after, edited_object_id="hole:85", edited_signature=before_signature)
|
|
_assert(direct_drift.get("ok") is False and direct_drift.get("checked") == 1, f"direct drift check should inspect one unedited object: {direct_drift}")
|
|
|
|
mismatch = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=before_signature,
|
|
after_cache=after,
|
|
capability_key="hole.diameter",
|
|
expected_target=0.9,
|
|
)
|
|
_assert(mismatch.get("ok") is False and mismatch.get("reason") == "target-mismatch", f"wrong target should fail: {mismatch}")
|
|
|
|
missing = validate_scdm_edit_result({"ok": True, "output_step": str(root / "missing.step")})
|
|
_assert(missing.get("ok") is False and missing.get("reason") == "missing-output-step", f"missing result STEP should fail: {missing}")
|
|
|
|
mapping = build_scdm_id_mapping(before, after, capability_key="hole.diameter")
|
|
_assert(mapping.get("faceIdMap") == {85: 90, 87: 91}, f"face IDs should remap through signatures: {mapping}")
|
|
rewritten = rewrite_scdm_relation_formula_ids("Face87.直径 = Face85.半径", mapping)
|
|
_assert(rewritten == "Face91.直径 = Face90.半径", f"formula IDs should follow SCDM remap: {rewritten}")
|
|
|
|
position_after = _cache(_hole("hole:91", 91, diameter=0.5, center=(2.0, 1.0, 6.0)))
|
|
position_check = check_scdm_target(position_after["objects"][0], capability_key="hole.position", expected_target=[2.0, 1.0, 6.0])
|
|
_assert(position_check.get("ok") is True, f"position target should pass: {position_check}")
|
|
|
|
before_slot = _cache(_feature("slot:30", "slot", 30, center=(1.0, 2.0, 3.0), capability_key="slot.position"))
|
|
after_slot = _cache(_feature("slot:40", "slot", 40, center=(1.0, 2.0, 6.0), capability_key="slot.position"))
|
|
slot_before_signature = before_slot["objects"][0]["geometrySignature"] # type: ignore[index]
|
|
slot_match = match_scdm_object_by_signature(slot_before_signature, after_slot, capability_key="slot.position")
|
|
_assert(slot_match.get("status") == "unique", f"moved slot should match without old center lock: {slot_match}")
|
|
slot_ok = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=slot_before_signature,
|
|
before_cache=before_slot,
|
|
after_cache=after_slot,
|
|
capability_key="slot.position",
|
|
expected_target=[1.0, 2.0, 6.0],
|
|
edited_object_id="slot:30",
|
|
)
|
|
_assert(slot_ok.get("ok") is True, f"slot.position result should validate target center: {slot_ok}")
|
|
_assert(slot_ok.get("targetCheck", {}).get("ok") is True, f"slot target should be checked: {slot_ok}")
|
|
before_slot_width = _cache(
|
|
{
|
|
"objectId": "slot:30",
|
|
"objectType": "slot",
|
|
"geometrySignature": {
|
|
"objectType": "slot",
|
|
"faceIds": [30],
|
|
"center": [1.0, 2.0, 3.0],
|
|
"axis": [0.0, 0.0, 1.0],
|
|
"width": 2.0,
|
|
},
|
|
"capabilities": [{"key": "slot.width", "currentValue": 2.0}],
|
|
}
|
|
)
|
|
after_slot_width = _cache(
|
|
{
|
|
"objectId": "slot:40",
|
|
"objectType": "slot",
|
|
"geometrySignature": {
|
|
"objectType": "slot",
|
|
"faceIds": [40],
|
|
"center": [1.0, 2.0, 3.0],
|
|
"axis": [0.0, 0.0, 1.0],
|
|
"width": 2.5,
|
|
},
|
|
"capabilities": [{"key": "slot.width", "currentValue": 2.5}],
|
|
}
|
|
)
|
|
slot_width_before_signature = before_slot_width["objects"][0]["geometrySignature"] # type: ignore[index]
|
|
slot_width_ok = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=slot_width_before_signature,
|
|
before_cache=before_slot_width,
|
|
after_cache=after_slot_width,
|
|
capability_key="slot.width",
|
|
expected_target=2.5,
|
|
edited_object_id="slot:30",
|
|
)
|
|
_assert(slot_width_ok.get("ok") is True, f"slot.width result should validate target width: {slot_width_ok}")
|
|
slot_width_mismatch = check_scdm_target(after_slot_width["objects"][0], capability_key="slot.width", expected_target=2.1) # type: ignore[index]
|
|
_assert(slot_width_mismatch.get("ok") is False and slot_width_mismatch.get("reason") == "target-mismatch", f"slot.width mismatch should fail: {slot_width_mismatch}")
|
|
before_slot_depth = _cache(
|
|
{
|
|
"objectId": "slot:31",
|
|
"objectType": "slot",
|
|
"geometrySignature": {
|
|
"objectType": "slot",
|
|
"faceIds": [31],
|
|
"center": [1.0, 2.0, 3.0],
|
|
"depth": 1.5,
|
|
"depthAxis": [0.0, 0.0, -1.0],
|
|
},
|
|
"capabilities": [{"key": "slot.depth", "currentValue": 1.5}],
|
|
}
|
|
)
|
|
after_slot_depth = _cache(
|
|
{
|
|
"objectId": "slot:41",
|
|
"objectType": "slot",
|
|
"geometrySignature": {
|
|
"objectType": "slot",
|
|
"faceIds": [41],
|
|
"center": [1.0, 2.0, 3.0],
|
|
"depth": 2.0,
|
|
"depthAxis": [0.0, 0.0, -1.0],
|
|
},
|
|
"capabilities": [{"key": "slot.depth", "currentValue": 2.0}],
|
|
}
|
|
)
|
|
slot_depth_before_signature = before_slot_depth["objects"][0]["geometrySignature"] # type: ignore[index]
|
|
slot_depth_ok = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=slot_depth_before_signature,
|
|
before_cache=before_slot_depth,
|
|
after_cache=after_slot_depth,
|
|
capability_key="slot.depth",
|
|
expected_target=2.0,
|
|
edited_object_id="slot:31",
|
|
)
|
|
_assert(slot_depth_ok.get("ok") is True, f"slot.depth result should validate target depth: {slot_depth_ok}")
|
|
slot_depth_mismatch = check_scdm_target(after_slot_depth["objects"][0], capability_key="slot.depth", expected_target=1.5) # type: ignore[index]
|
|
_assert(slot_depth_mismatch.get("ok") is False and slot_depth_mismatch.get("reason") == "target-mismatch", f"slot.depth mismatch should fail: {slot_depth_mismatch}")
|
|
|
|
boss_after = _feature("boss:50", "cylindrical_boss", 50, center=(3.0, 0.0, 2.0), capability_key="boss.position")
|
|
boss_check = check_scdm_target(boss_after, capability_key="boss.position", expected_target=[3.0, 0.0, 2.0])
|
|
_assert(boss_check.get("ok") is True, f"boss.position target should pass: {boss_check}")
|
|
boss_mismatch = check_scdm_target(boss_after, capability_key="boss.position", expected_target=[4.0, 0.0, 2.0])
|
|
_assert(boss_mismatch.get("ok") is False and boss_mismatch.get("reason") == "target-mismatch", f"boss.position mismatch should fail: {boss_mismatch}")
|
|
before_boss_height = _cache(
|
|
{
|
|
"objectId": "boss:50",
|
|
"objectType": "cylindrical_boss",
|
|
"geometrySignature": {
|
|
"objectType": "cylindrical_boss",
|
|
"faceIds": [50, 51, 52],
|
|
"center": [0.0, 0.0, 2.0],
|
|
"axis": [0.0, 0.0, 1.0],
|
|
"diameter": 3.0,
|
|
"height": 4.0,
|
|
},
|
|
"capabilities": [{"key": "boss.height", "currentValue": 4.0}],
|
|
}
|
|
)
|
|
after_boss_height = _cache(
|
|
{
|
|
"objectId": "boss:55",
|
|
"objectType": "cylindrical_boss",
|
|
"geometrySignature": {
|
|
"objectType": "cylindrical_boss",
|
|
"faceIds": [55, 56, 57],
|
|
"center": [0.0, 0.0, 2.75],
|
|
"axis": [0.0, 0.0, 1.0],
|
|
"diameter": 3.0,
|
|
"height": 5.5,
|
|
},
|
|
"capabilities": [{"key": "boss.height", "currentValue": 5.5}],
|
|
}
|
|
)
|
|
boss_height_signature = before_boss_height["objects"][0]["geometrySignature"] # type: ignore[index]
|
|
boss_height_ok = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=boss_height_signature,
|
|
before_cache=before_boss_height,
|
|
after_cache=after_boss_height,
|
|
capability_key="boss.height",
|
|
expected_target=5.5,
|
|
edited_object_id="boss:50",
|
|
)
|
|
_assert(boss_height_ok.get("ok") is True, f"boss.height result should validate target height: {boss_height_ok}")
|
|
boss_height_mismatch = check_scdm_target(after_boss_height["objects"][0], capability_key="boss.height", expected_target=4.5) # type: ignore[index]
|
|
_assert(boss_height_mismatch.get("ok") is False and boss_height_mismatch.get("reason") == "target-mismatch", f"boss.height mismatch should fail: {boss_height_mismatch}")
|
|
before_boss_diameter = _cache(
|
|
{
|
|
"objectId": "boss:60",
|
|
"objectType": "cylindrical_boss",
|
|
"geometrySignature": {
|
|
"objectType": "cylindrical_boss",
|
|
"faceIds": [60, 61, 62],
|
|
"center": [0.0, 0.0, 2.0],
|
|
"axis": [0.0, 0.0, 1.0],
|
|
"diameter": 3.0,
|
|
"height": 4.0,
|
|
},
|
|
"capabilities": [{"key": "boss.diameter", "currentValue": 3.0}],
|
|
}
|
|
)
|
|
after_boss_diameter = _cache(
|
|
{
|
|
"objectId": "boss:63",
|
|
"objectType": "cylindrical_boss",
|
|
"geometrySignature": {
|
|
"objectType": "cylindrical_boss",
|
|
"faceIds": [63, 64, 65],
|
|
"center": [0.0, 0.0, 2.0],
|
|
"axis": [0.0, 0.0, 1.0],
|
|
"diameter": 4.5,
|
|
"height": 4.0,
|
|
},
|
|
"capabilities": [{"key": "boss.diameter", "currentValue": 4.5}],
|
|
}
|
|
)
|
|
boss_diameter_signature = before_boss_diameter["objects"][0]["geometrySignature"] # type: ignore[index]
|
|
boss_diameter_ok = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=boss_diameter_signature,
|
|
before_cache=before_boss_diameter,
|
|
after_cache=after_boss_diameter,
|
|
capability_key="boss.diameter",
|
|
expected_target=4.5,
|
|
edited_object_id="boss:60",
|
|
)
|
|
_assert(boss_diameter_ok.get("ok") is True, f"boss.diameter result should validate target diameter: {boss_diameter_ok}")
|
|
boss_diameter_mismatch = check_scdm_target(after_boss_diameter["objects"][0], capability_key="boss.diameter", expected_target=3.5) # type: ignore[index]
|
|
_assert(boss_diameter_mismatch.get("ok") is False and boss_diameter_mismatch.get("reason") == "target-mismatch", f"boss.diameter mismatch should fail: {boss_diameter_mismatch}")
|
|
|
|
before_round_radius = _cache(
|
|
{
|
|
"objectId": "round:70",
|
|
"objectType": "round",
|
|
"geometrySignature": {
|
|
"objectType": "round",
|
|
"faceIds": [70],
|
|
"center": [1.0, 0.0, 2.0],
|
|
"axis": [0.0, 0.0, 1.0],
|
|
"radius": 0.5,
|
|
"isConstantRound": True,
|
|
},
|
|
"capabilities": [{"key": "round.radius", "currentValue": 0.5}],
|
|
}
|
|
)
|
|
after_round_radius = _cache(
|
|
{
|
|
"objectId": "round:71",
|
|
"objectType": "round",
|
|
"geometrySignature": {
|
|
"objectType": "round",
|
|
"faceIds": [71],
|
|
"center": [1.0, 0.0, 2.0],
|
|
"axis": [0.0, 0.0, 1.0],
|
|
"radius": 0.75,
|
|
"isConstantRound": True,
|
|
},
|
|
"capabilities": [{"key": "round.radius", "currentValue": 0.75}],
|
|
}
|
|
)
|
|
round_radius_signature = before_round_radius["objects"][0]["geometrySignature"] # type: ignore[index]
|
|
round_radius_ok = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=round_radius_signature,
|
|
before_cache=before_round_radius,
|
|
after_cache=after_round_radius,
|
|
capability_key="round.radius",
|
|
expected_target=0.75,
|
|
edited_object_id="round:70",
|
|
)
|
|
_assert(round_radius_ok.get("ok") is True, f"round.radius result should validate target radius: {round_radius_ok}")
|
|
round_radius_mismatch = check_scdm_target(after_round_radius["objects"][0], capability_key="round.radius", expected_target=0.5) # type: ignore[index]
|
|
_assert(round_radius_mismatch.get("ok") is False and round_radius_mismatch.get("reason") == "target-mismatch", f"round.radius mismatch should fail: {round_radius_mismatch}")
|
|
|
|
before_chamfer_distance = _cache(
|
|
{
|
|
"objectId": "chamfer:80",
|
|
"objectType": "chamfer",
|
|
"geometrySignature": {
|
|
"objectType": "chamfer",
|
|
"faceIds": [80],
|
|
"center": [2.0, 0.0, 2.0],
|
|
"axis": [0.0, 0.0, 1.0],
|
|
"distance": 0.8,
|
|
"isEqualDistanceChamfer": True,
|
|
},
|
|
"capabilities": [{"key": "chamfer.distance", "currentValue": 0.8}],
|
|
}
|
|
)
|
|
after_chamfer_distance = _cache(
|
|
{
|
|
"objectId": "chamfer:81",
|
|
"objectType": "chamfer",
|
|
"geometrySignature": {
|
|
"objectType": "chamfer",
|
|
"faceIds": [81],
|
|
"center": [2.0, 0.0, 2.0],
|
|
"axis": [0.0, 0.0, 1.0],
|
|
"distance": 1.2,
|
|
"isEqualDistanceChamfer": True,
|
|
},
|
|
"capabilities": [{"key": "chamfer.distance", "currentValue": 1.2}],
|
|
}
|
|
)
|
|
chamfer_distance_signature = before_chamfer_distance["objects"][0]["geometrySignature"] # type: ignore[index]
|
|
chamfer_distance_ok = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=chamfer_distance_signature,
|
|
before_cache=before_chamfer_distance,
|
|
after_cache=after_chamfer_distance,
|
|
capability_key="chamfer.distance",
|
|
expected_target=1.2,
|
|
edited_object_id="chamfer:80",
|
|
)
|
|
_assert(chamfer_distance_ok.get("ok") is True, f"chamfer.distance result should validate target distance: {chamfer_distance_ok}")
|
|
chamfer_distance_mismatch = check_scdm_target(after_chamfer_distance["objects"][0], capability_key="chamfer.distance", expected_target=0.8) # type: ignore[index]
|
|
_assert(chamfer_distance_mismatch.get("ok") is False and chamfer_distance_mismatch.get("reason") == "target-mismatch", f"chamfer.distance mismatch should fail: {chamfer_distance_mismatch}")
|
|
|
|
before_pattern_spacing = _cache(
|
|
{
|
|
"objectId": "pattern:holes",
|
|
"objectType": "linear_pattern",
|
|
"geometrySignature": {
|
|
"objectType": "linear_pattern",
|
|
"faceIds": [85, 87, 89],
|
|
"center": [5.0, 0.0, 0.0],
|
|
"axis": [1.0, 0.0, 0.0],
|
|
"spacing": 5.0,
|
|
"pitch": 5.0,
|
|
"instanceCount": 3,
|
|
"instanceCenters": [[0.0, 0.0, 0.0], [5.0, 0.0, 0.0], [10.0, 0.0, 0.0]],
|
|
},
|
|
"capabilities": [{"key": "pattern.spacing", "currentValue": 5.0}],
|
|
}
|
|
)
|
|
after_pattern_spacing = _cache(
|
|
{
|
|
"objectId": "pattern:holes-new",
|
|
"objectType": "linear_pattern",
|
|
"geometrySignature": {
|
|
"objectType": "linear_pattern",
|
|
"faceIds": [90, 91, 92],
|
|
"center": [7.5, 0.0, 0.0],
|
|
"axis": [1.0, 0.0, 0.0],
|
|
"spacing": 7.5,
|
|
"pitch": 7.5,
|
|
"instanceCount": 3,
|
|
"instanceCenters": [[0.0, 0.0, 0.0], [7.5, 0.0, 0.0], [15.0, 0.0, 0.0]],
|
|
},
|
|
"capabilities": [{"key": "pattern.spacing", "currentValue": 7.5}],
|
|
}
|
|
)
|
|
pattern_spacing_before_signature = before_pattern_spacing["objects"][0]["geometrySignature"] # type: ignore[index]
|
|
pattern_spacing_ok = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=pattern_spacing_before_signature,
|
|
before_cache=before_pattern_spacing,
|
|
after_cache=after_pattern_spacing,
|
|
capability_key="pattern.spacing",
|
|
expected_target=7.5,
|
|
edited_object_id="pattern:holes",
|
|
)
|
|
_assert(pattern_spacing_ok.get("ok") is True, f"pattern.spacing result should validate target spacing: {pattern_spacing_ok}")
|
|
pattern_spacing_mismatch = check_scdm_target(after_pattern_spacing["objects"][0], capability_key="pattern.spacing", expected_target=5.0) # type: ignore[index]
|
|
_assert(pattern_spacing_mismatch.get("ok") is False and pattern_spacing_mismatch.get("reason") == "target-mismatch", f"pattern.spacing mismatch should fail: {pattern_spacing_mismatch}")
|
|
before_pattern_segment = _cache(
|
|
{
|
|
"objectId": "pattern:holes",
|
|
"objectType": "linear_pattern",
|
|
"geometrySignature": {
|
|
"objectType": "linear_pattern",
|
|
"faceIds": [85, 87, 89],
|
|
"center": [5.0, 0.0, 0.0],
|
|
"axis": [1.0, 0.0, 0.0],
|
|
"spacing": 5.0,
|
|
"pitch": 5.0,
|
|
"segmentIndex": 1,
|
|
"movingSide": "after",
|
|
"patternInstances": [
|
|
{"sourceObjectId": "a", "center": [0.0, 0.0, 0.0], "faceIds": [85]},
|
|
{"sourceObjectId": "b", "center": [5.0, 0.0, 0.0], "faceIds": [87]},
|
|
{"sourceObjectId": "c", "center": [10.0, 0.0, 0.0], "faceIds": [89]},
|
|
],
|
|
},
|
|
"capabilities": [{"key": "pattern.segment_spacing", "currentValue": 5.0}],
|
|
},
|
|
_hole("hole:unrelated", 999, diameter=1.0, center=(100.0, 0.0, 0.0)),
|
|
)
|
|
after_pattern_segment = _cache(_hole("hole:unrelated-new", 999, diameter=1.0, center=(100.0, 0.0, 0.0)))
|
|
pattern_segment_ok = validate_scdm_edit_result(
|
|
{
|
|
"ok": True,
|
|
"output_step": str(output_step),
|
|
"result": {
|
|
"applied": {
|
|
"segmentSpacing": 6.5,
|
|
"targetSpacing": 6.5,
|
|
"segmentIndex": 1,
|
|
"spacingMode": "segment_after",
|
|
}
|
|
},
|
|
},
|
|
before_signature=before_pattern_segment["objects"][0]["geometrySignature"], # type: ignore[index]
|
|
before_cache=before_pattern_segment,
|
|
after_cache=after_pattern_segment,
|
|
capability_key="pattern.segment_spacing",
|
|
expected_target=6.5,
|
|
edited_object_id="pattern:holes",
|
|
)
|
|
_assert(
|
|
pattern_segment_ok.get("ok") is True
|
|
and pattern_segment_ok.get("targetCheck", {}).get("spacingMode") == "segment_after",
|
|
f"pattern.segment_spacing should validate from the applied edit result even when the old uniform pattern no longer matches: {pattern_segment_ok}",
|
|
)
|
|
pattern_segment_mismatch = validate_scdm_edit_result(
|
|
{
|
|
"ok": True,
|
|
"output_step": str(output_step),
|
|
"applied": {"segmentSpacing": 6.0, "segmentIndex": 1, "spacingMode": "segment_after"},
|
|
},
|
|
before_signature=before_pattern_segment["objects"][0]["geometrySignature"], # type: ignore[index]
|
|
before_cache=before_pattern_segment,
|
|
after_cache=after_pattern_segment,
|
|
capability_key="pattern.segment_spacing",
|
|
expected_target=6.5,
|
|
edited_object_id="pattern:holes",
|
|
)
|
|
_assert(
|
|
pattern_segment_mismatch.get("ok") is False and pattern_segment_mismatch.get("reason") == "target-mismatch",
|
|
f"pattern.segment_spacing mismatch should fail from the applied edit result: {pattern_segment_mismatch}",
|
|
)
|
|
shell_thickness_ok = check_scdm_target(
|
|
{
|
|
"objectType": "thin_wall",
|
|
"geometrySignature": {"objectType": "thin_wall", "thickness": 1.6},
|
|
"capabilities": [{"key": "shell.thickness", "currentValue": 1.6}],
|
|
},
|
|
capability_key="shell.thickness",
|
|
expected_target=1.6,
|
|
)
|
|
_assert(shell_thickness_ok.get("ok") is True, f"shell.thickness result should validate target thickness: {shell_thickness_ok}")
|
|
shell_thickness_mismatch = check_scdm_target(
|
|
{
|
|
"objectType": "thin_wall",
|
|
"geometrySignature": {"objectType": "thin_wall", "thickness": 1.6},
|
|
"capabilities": [{"key": "shell.thickness", "currentValue": 1.6}],
|
|
},
|
|
capability_key="shell.thickness",
|
|
expected_target=2.0,
|
|
)
|
|
_assert(shell_thickness_mismatch.get("ok") is False and shell_thickness_mismatch.get("reason") == "target-mismatch", f"shell.thickness mismatch should fail: {shell_thickness_mismatch}")
|
|
|
|
summary_before = _cache_with_summary(
|
|
{"bodyCount": 13, "objectCount": 554, "faceCount": 158, "edgeCount": 396},
|
|
_hole("hole:85", 85, diameter=0.5, center=(0.5, 1.0, 9.5)),
|
|
)
|
|
summary_after_ok = _cache_with_summary(
|
|
{"bodyCount": 13, "objectCount": 550, "faceCount": 157, "edgeCount": 390},
|
|
_hole("hole:90", 90, diameter=0.75, center=(0.5, 1.0, 9.5)),
|
|
)
|
|
summary_ok = check_scdm_summary_delta(summary_before, summary_after_ok, capability_key="hole.diameter")
|
|
_assert(summary_ok.get("ok") is True, f"small summary changes should pass: {summary_ok}")
|
|
summary_after_bad = _cache_with_summary(
|
|
{"bodyCount": 13, "objectCount": 80, "faceCount": 20, "edgeCount": 45},
|
|
_hole("hole:90", 90, diameter=0.75, center=(0.5, 1.0, 9.5)),
|
|
)
|
|
summary_bad = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=summary_before["objects"][0]["geometrySignature"], # type: ignore[index]
|
|
before_cache=summary_before,
|
|
after_cache=summary_after_bad,
|
|
capability_key="hole.diameter",
|
|
expected_target=0.75,
|
|
)
|
|
_assert(summary_bad.get("ok") is False and summary_bad.get("reason") == "summary-drift", f"large summary drift should fail: {summary_bad}")
|
|
summary_fill = check_scdm_summary_delta(summary_before, summary_after_bad, capability_key="feature.fill")
|
|
_assert(summary_fill.get("ok") is None and summary_fill.get("reason") == "skipped-command-feature", f"fill should skip summary count guard: {summary_fill}")
|
|
|
|
fill_before = _cache(
|
|
_hole("hole:85", 85, diameter=0.5, center=(0.5, 1.0, 9.5)),
|
|
_hole("hole:87", 87, diameter=0.5, center=(2.0, 1.0, 9.5)),
|
|
)
|
|
fill_after = _cache(_hole("hole:87", 87, diameter=0.5, center=(2.0, 1.0, 9.5)))
|
|
fill_signature = fill_before["objects"][0]["geometrySignature"] # type: ignore[index]
|
|
fill_ok = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=fill_signature,
|
|
before_cache=fill_before,
|
|
after_cache=fill_after,
|
|
capability_key="feature.fill",
|
|
edited_object_id="hole:85",
|
|
)
|
|
_assert(fill_ok.get("ok") is True, f"feature.fill should pass when the edited feature disappears: {fill_ok}")
|
|
_assert(fill_ok.get("removalCheck", {}).get("ok") is True, f"feature.fill should record removal evidence: {fill_ok}")
|
|
fill_still_present = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=fill_signature,
|
|
before_cache=fill_before,
|
|
after_cache=fill_before,
|
|
capability_key="feature.fill",
|
|
edited_object_id="hole:85",
|
|
)
|
|
_assert(
|
|
fill_still_present.get("ok") is False and fill_still_present.get("reason") == "feature-still-present",
|
|
f"feature.fill should fail when the edited feature still matches: {fill_still_present}",
|
|
)
|
|
fill_no_cache = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=fill_signature,
|
|
capability_key="feature.fill",
|
|
)
|
|
_assert(
|
|
fill_no_cache.get("ok") is False and fill_no_cache.get("reason") == "removal-check-unavailable",
|
|
f"feature.fill should require a new cache for removal verification: {fill_no_cache}",
|
|
)
|
|
|
|
round_before = _cache(_feature("round:60", "round", 60, center=(1.0, 0.0, 2.0), capability_key="feature.delete_round_or_chamfer"))
|
|
round_after = _cache(_feature("hole:85", "hole", 85, center=(5.0, 0.0, 2.0), capability_key="hole.diameter"))
|
|
round_delete_ok = validate_scdm_edit_result(
|
|
{"ok": True, "output_step": str(output_step)},
|
|
before_signature=round_before["objects"][0]["geometrySignature"], # type: ignore[index]
|
|
before_cache=round_before,
|
|
after_cache=round_after,
|
|
capability_key="feature.delete_round_or_chamfer",
|
|
edited_object_id="round:60",
|
|
)
|
|
_assert(round_delete_ok.get("ok") is True, f"round/chamfer delete should pass when the edited feature disappears: {round_delete_ok}")
|
|
_assert(round_delete_ok.get("targetCheck", {}).get("reason") == "removed", f"round/chamfer delete should use removal target check: {round_delete_ok}")
|
|
|
|
ambiguous_after = _cache(
|
|
_hole("hole:100", 100, diameter=0.75, center=(0.5, 1.0, 9.5)),
|
|
_hole("hole:101", 101, diameter=0.75, center=(0.5, 1.0, 9.5)),
|
|
)
|
|
ambiguous = match_scdm_object_by_signature(before_signature, ambiguous_after, capability_key="hole.diameter")
|
|
_assert(ambiguous.get("status") == "multiple", f"ambiguous matches should be reported: {ambiguous}")
|
|
|
|
print("scdm result validator ok")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|