feat: 接入 SCDM 优先编辑闭环并支持阵列局部间距
接入 SCDM probe/edit/cache/校验链路,增强孔组、阵列、关系式和参数表交互。 支持阵列相邻段间距、移动意图切换、结果回滚校验,并补充对应回归脚本。
This commit is contained in:
@@ -95,11 +95,15 @@ def main() -> int:
|
||||
_assert("StandardHoles" in script and "FindStandardHoleOptions" in script and "getattr(standard_holes, 'Find'" in script, "probe script should try SCDM StandardHoles.Find before geometric fallback")
|
||||
_assert("availableCommands" in script and "ConstantRound" in script and "Chamfer" in script, "probe script should report SCDM command availability")
|
||||
_assert("RoundInfo" in script and "_round_info_from_face" in script and "change_round_radius" in script, "probe script should collect SCDM round diagnostics")
|
||||
_assert("ChamferInfo" in script and "_chamfer_info_from_face" in script and "change_chamfer_distance" in script, "probe script should collect SCDM chamfer diagnostics")
|
||||
_assert("SlotInfo" in script and "_slot_info_from_face" in script and "change_slot_depth" in script, "probe script should collect SCDM slot diagnostics")
|
||||
_assert("backendCommandCandidates" in script, "probe script should write raw command candidates")
|
||||
for token in ("_geometry_from_edge", "Length", "StartPoint", "EndPoint", "adjacentFaceOrdinals"):
|
||||
_assert(token in script, f"probe script should enrich Edge raw geometry with {token}")
|
||||
for token in ("_record_face_adjacency", "_face_adjacency_rows", "edgeGeometrySummary", "_final_edge_geometry_summary", "_feature_inventory"):
|
||||
_assert(token in script, f"probe script should summarize SCDM topology evidence with {token}")
|
||||
for token in ("_component_entries", "_component_body_locator_map", "componentInstances", "componentLocators"):
|
||||
_assert(token in script, f"probe script should preserve SCDM component occurrence evidence with {token}")
|
||||
generated = generate_scdm_probe_script(job_path)
|
||||
_assert("JOB_PATH =" in generated and job_path.name in generated, "generated probe script should embed the job path")
|
||||
|
||||
@@ -123,7 +127,10 @@ def main() -> int:
|
||||
{"name": "Move", "available": True},
|
||||
{"name": "Fill", "available": True},
|
||||
{"name": "Delete", "available": True},
|
||||
{"name": "Chamfer", "available": True},
|
||||
{"name": "ConstantRound", "available": True},
|
||||
{"name": "ChamferInfo", "available": True},
|
||||
{"name": "SlotInfo", "available": True},
|
||||
{"name": "SomeFutureCommand", "available": False},
|
||||
],
|
||||
"faceAdjacency": [
|
||||
@@ -144,18 +151,30 @@ def main() -> int:
|
||||
"maxEdgeLength": 8.0,
|
||||
},
|
||||
"featureInventory": {
|
||||
"objectTypeCounts": {"face": 2, "hole": 1, "edge": 1, "slot": 1, "round": 1},
|
||||
"objectTypeCounts": {"face": 2, "hole": 1, "edge": 1, "slot": 1, "round": 1, "chamfer": 1},
|
||||
"surfaceTypeCounts": {"plane": 1, "cylinder": 3},
|
||||
"curveTypeCounts": {"Line": 5, "Circle": 4},
|
||||
"operationCounts": {
|
||||
"pull_face_offset": 1,
|
||||
"change_hole_diameter": 1,
|
||||
"change_slot_width": 1,
|
||||
"change_slot_depth": 1,
|
||||
"move_slot": 1,
|
||||
"change_boss_height": 1,
|
||||
"move_boss": 1,
|
||||
"change_chamfer_distance": 1,
|
||||
},
|
||||
},
|
||||
"componentInstances": [
|
||||
{
|
||||
"backendId": "component:0",
|
||||
"componentIndex": 0,
|
||||
"componentPath": [0],
|
||||
"componentName": "零件 3",
|
||||
"contentBodyCount": 1,
|
||||
"placementTranslation": [0.0, 20.0, 0.0],
|
||||
}
|
||||
],
|
||||
},
|
||||
"summary": {"bodyCount": 1, "objectCount": 10, "faceCount": 6, "edgeCount": 1, "holeFaceCount": 0},
|
||||
"objects": [
|
||||
@@ -233,6 +252,34 @@ def main() -> int:
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:2/face:100",
|
||||
"objectType": "face",
|
||||
"geometry": {
|
||||
"surfaceType": "plane",
|
||||
"center": [0.0, 0.0, 0.0],
|
||||
"normal": [0.0, 0.0, 1.0],
|
||||
},
|
||||
"topologyHint": {"faceIds": [100], "bodyIndex": 2, "faceOrdinal": 100, "globalFaceOrdinal": 100},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "pull_face_offset", "enabled": True, "parameterFields": {"distance": 0.0}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:2/face:101",
|
||||
"objectType": "face",
|
||||
"geometry": {
|
||||
"surfaceType": "plane",
|
||||
"center": [0.0, 0.0, 1.2],
|
||||
"normal": [0.0, 0.0, -1.0],
|
||||
},
|
||||
"topologyHint": {"faceIds": [101], "bodyIndex": 2, "faceOrdinal": 101, "globalFaceOrdinal": 101},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "pull_face_offset", "enabled": True, "parameterFields": {"distance": 0.0}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:8/face:5",
|
||||
"objectType": "face",
|
||||
@@ -262,7 +309,17 @@ def main() -> int:
|
||||
{
|
||||
"backendId": "round:1",
|
||||
"objectType": "round",
|
||||
"geometry": {"radius": 1.0},
|
||||
"geometry": {
|
||||
"radius": 1.0,
|
||||
"roundInfo": {
|
||||
"radius": 1.0,
|
||||
"diameter": 2.0,
|
||||
"isConstant": True,
|
||||
"isRound": True,
|
||||
"type": "ConstantRound",
|
||||
},
|
||||
},
|
||||
"topologyHint": {"faceIds": [60], "bodyIndex": 0, "faceOrdinal": 60, "globalFaceOrdinal": 60},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_round_radius", "enabled": True},
|
||||
{"operation": "delete_round_or_chamfer", "enabled": True},
|
||||
@@ -272,10 +329,16 @@ def main() -> int:
|
||||
{
|
||||
"backendId": "slot:1",
|
||||
"objectType": "slot",
|
||||
"geometry": {"width": 2.0, "depth": 1.5, "center": [1.0, 2.0, 3.0]},
|
||||
"topologyHint": {"faceIds": [30, 31, 32], "bodyIndex": 0, "faceOrdinal": 30},
|
||||
"geometry": {"width": 2.0, "depth": 1.5, "depthAxis": [0.0, 0.0, -1.0], "center": [1.0, 2.0, 3.0]},
|
||||
"topologyHint": {
|
||||
"faceIds": [30, 31, 32],
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 30,
|
||||
"depthFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 32, "globalFaceOrdinal": 42}],
|
||||
},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_slot_width", "enabled": True},
|
||||
{"operation": "change_slot_depth", "enabled": True},
|
||||
{"operation": "move_slot", "enabled": True, "parameterFields": {"center": [1.0, 2.0, 3.0]}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
@@ -284,9 +347,16 @@ def main() -> int:
|
||||
"backendId": "boss:1",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometry": {"diameter": 3.0, "height": 4.0, "center": [0.0, 0.0, 2.0]},
|
||||
"topologyHint": {"faceIds": [50, 51, 52], "bodyIndex": 0, "faceOrdinal": 50},
|
||||
"topologyHint": {
|
||||
"faceIds": [50, 51, 52],
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 50,
|
||||
"heightFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 52, "globalFaceOrdinal": 72}],
|
||||
"diameterFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 50, "globalFaceOrdinal": 70}],
|
||||
},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_boss_height", "enabled": True},
|
||||
{"operation": "change_boss_diameter", "enabled": True},
|
||||
{"operation": "move_boss", "enabled": True, "parameterFields": {"center": [0.0, 0.0, 2.0]}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
@@ -294,7 +364,18 @@ def main() -> int:
|
||||
{
|
||||
"backendId": "chamfer:1",
|
||||
"objectType": "chamfer",
|
||||
"geometry": {"distance": 0.8},
|
||||
"geometry": {
|
||||
"distance": 0.8,
|
||||
"chamferInfo": {
|
||||
"distance": 0.8,
|
||||
"distance1": 0.8,
|
||||
"distance2": 0.8,
|
||||
"isEqualDistance": True,
|
||||
"isChamfer": True,
|
||||
"type": "EqualDistanceChamfer",
|
||||
},
|
||||
},
|
||||
"topologyHint": {"faceIds": [63], "bodyIndex": 0, "faceOrdinal": 63, "globalFaceOrdinal": 63},
|
||||
"backendCommandCandidates": [{"operation": "change_chamfer_distance", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
@@ -305,6 +386,48 @@ def main() -> int:
|
||||
"backendCommandCandidates": [{"operation": "change_pattern_spacing", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:20",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 20.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {
|
||||
"bodyIndex": 20,
|
||||
"faceIds": [300],
|
||||
"faceOrdinals": [0],
|
||||
"globalFaceOrdinals": [300],
|
||||
"componentLocators": [{"componentIndex": 0, "componentPath": [0], "componentBodyIndex": 0, "bodyIndex": 20}],
|
||||
},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:21",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 25.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {
|
||||
"bodyIndex": 21,
|
||||
"faceIds": [301],
|
||||
"faceOrdinals": [0],
|
||||
"globalFaceOrdinals": [301],
|
||||
"componentLocators": [{"componentIndex": 1, "componentPath": [1], "componentBodyIndex": 0, "bodyIndex": 21}],
|
||||
},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:22",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 30.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {
|
||||
"bodyIndex": 22,
|
||||
"faceIds": [302],
|
||||
"faceOrdinals": [0],
|
||||
"globalFaceOrdinals": [302],
|
||||
"componentLocators": [{"componentIndex": 2, "componentPath": [2], "componentBodyIndex": 0, "bodyIndex": 22}],
|
||||
},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "shell:1",
|
||||
"objectType": "shell",
|
||||
@@ -342,7 +465,21 @@ def main() -> int:
|
||||
_assert(edge_signature.get("length") == 3.14, f"Edge length should be preserved: {edge_signature}")
|
||||
_assert(edge_signature.get("startPoint") == [0.0, 0.0, 0.0], f"Edge start point should be preserved: {edge_signature}")
|
||||
_assert(edge_signature.get("adjacentFaceOrdinals") == [3, 4], f"Edge adjacent faces should be preserved: {edge_signature}")
|
||||
component_signature = geometry_signature(
|
||||
{
|
||||
"backendId": "body:20",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 20.0, 0.0]},
|
||||
"topologyHint": {
|
||||
"bodyIndex": 20,
|
||||
"bodyLocators": [{"bodyIndex": 20, "componentIndex": 0, "componentPath": [0], "componentBodyIndex": 0}],
|
||||
"componentLocators": [{"componentIndex": 0, "componentPath": [0], "componentBodyIndex": 0, "bodyIndex": 20}],
|
||||
},
|
||||
}
|
||||
)
|
||||
_assert(component_signature.get("componentLocators"), f"component locators should be preserved in geometry signatures: {component_signature}")
|
||||
_assert(cache.get("modelFingerprint") == "abc123", f"model fingerprint should be copied: {cache}")
|
||||
_assert(int(cache.get("mapperRevision") or 0) >= 2, f"SCDM cache should carry the mapper revision for disk reuse: {cache}")
|
||||
_assert(cache.get("backendVersion") == "v222", f"backend version should be copied: {cache}")
|
||||
_assert({"hole.diameter", "hole.position"} <= _capability_keys(cache, "hole:1"), f"hole caps missing: {cache}")
|
||||
objects = cache.get("objects")
|
||||
@@ -375,24 +512,63 @@ def main() -> int:
|
||||
_assert({str(spec.get("scdm_capability_key")) for spec in face_specs} == {"face.offset"}, f"Face cache should map to offset only: {face_specs}")
|
||||
gated_face_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(9,), execution_ready={"face.offset"})
|
||||
_assert(gated_face_specs and all(spec.get("enabled") is True for spec in gated_face_specs), f"capability gate should enable verified face offset: {gated_face_specs}")
|
||||
_assert({"slot.position"} <= _capability_keys(cache, "slot:1"), f"slot.position should now be productized: {cache}")
|
||||
slot_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(31,), execution_ready={"slot.position"})
|
||||
_assert({"slot.width", "slot.depth", "slot.position"} <= _capability_keys(cache, "slot:1"), f"slot width/depth/position should now be productized: {cache}")
|
||||
slot_object = next((item for item in objects if isinstance(item, dict) and item.get("objectId") == "slot:slot:1"), None)
|
||||
_assert(isinstance(slot_object, dict), f"slot object should be normalized: {cache}")
|
||||
slot_signature = slot_object.get("geometrySignature")
|
||||
_assert(isinstance(slot_signature, dict) and slot_signature.get("depthFaceLocators"), f"slot depth locator should be preserved: {slot_signature}")
|
||||
_assert(isinstance(slot_signature, dict) and slot_signature.get("depthAxis") == [0.0, 0.0, -1.0], f"slot depth axis should be preserved: {slot_signature}")
|
||||
slot_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(31,), execution_ready={"slot.width", "slot.depth", "slot.position"})
|
||||
_assert(
|
||||
{str(spec.get("scdm_capability_key")) for spec in slot_specs} == {"slot.position"},
|
||||
f"slot cache should expose only productized slot.position: {slot_specs}",
|
||||
{str(spec.get("scdm_capability_key")) for spec in slot_specs} == {"slot.width", "slot.depth", "slot.position"},
|
||||
f"slot cache should expose productized slot.width, slot.depth and slot.position: {slot_specs}",
|
||||
)
|
||||
_assert(slot_specs and slot_specs[0].get("enabled") is True, f"slot.position should enable when runner gate is ready: {slot_specs}")
|
||||
_assert(slot_specs and all(spec.get("enabled") is True for spec in slot_specs), f"slot width/depth/position should enable when runner gate is ready: {slot_specs}")
|
||||
slot_width_spec = next((spec for spec in slot_specs if spec.get("scdm_capability_key") == "slot.width"), None)
|
||||
_assert(slot_width_spec and slot_width_spec.get("value_type") == "positive", f"slot.width should use positive numeric input: {slot_specs}")
|
||||
slot_depth_spec = next((spec for spec in slot_specs if spec.get("scdm_capability_key") == "slot.depth"), None)
|
||||
_assert(slot_depth_spec and slot_depth_spec.get("value_type") == "positive", f"slot.depth should use positive numeric input: {slot_specs}")
|
||||
blocked_slot_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(31,), execution_ready={"face.offset"})
|
||||
_assert(blocked_slot_specs and all(spec.get("enabled") is False for spec in blocked_slot_specs), f"slot.position should honor runner gate: {blocked_slot_specs}")
|
||||
_assert({"boss.position"} <= _capability_keys(cache, "boss:1"), f"boss.position should now be productized: {cache}")
|
||||
boss_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(51,), execution_ready={"boss.position"})
|
||||
_assert(blocked_slot_specs and all(spec.get("enabled") is False for spec in blocked_slot_specs), f"slot capabilities should honor runner gate: {blocked_slot_specs}")
|
||||
_assert({"boss.diameter", "boss.height", "boss.position"} <= _capability_keys(cache, "boss:1"), f"boss diameter/height/position should now be productized: {cache}")
|
||||
boss_object = next((item for item in objects if isinstance(item, dict) and item.get("objectType") == "cylindrical_boss"), None)
|
||||
_assert(isinstance(boss_object, dict), f"boss object should be normalized: {cache}")
|
||||
boss_signature = boss_object.get("geometrySignature")
|
||||
_assert(isinstance(boss_signature, dict) and boss_signature.get("heightFaceLocators"), f"boss height locator should be preserved: {boss_signature}")
|
||||
_assert(isinstance(boss_signature, dict) and boss_signature.get("diameterFaceLocators"), f"boss diameter locator should be preserved: {boss_signature}")
|
||||
boss_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(51,), execution_ready={"boss.diameter", "boss.height", "boss.position"})
|
||||
_assert(
|
||||
{str(spec.get("scdm_capability_key")) for spec in boss_specs} == {"boss.position"},
|
||||
f"boss cache should expose only productized boss.position: {boss_specs}",
|
||||
{str(spec.get("scdm_capability_key")) for spec in boss_specs} == {"boss.diameter", "boss.height", "boss.position"},
|
||||
f"boss cache should expose productized boss.diameter, boss.height and boss.position: {boss_specs}",
|
||||
)
|
||||
_assert(boss_specs and boss_specs[0].get("enabled") is True, f"boss.position should enable when runner gate is ready: {boss_specs}")
|
||||
_assert(boss_specs and all(spec.get("enabled") is True for spec in boss_specs), f"boss diameter/height/position should enable when runner gate is ready: {boss_specs}")
|
||||
blocked_boss_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(51,), execution_ready={"slot.position"})
|
||||
_assert(blocked_boss_specs and all(spec.get("enabled") is False for spec in blocked_boss_specs), f"boss.position should honor runner gate: {blocked_boss_specs}")
|
||||
_assert(blocked_boss_specs and all(spec.get("enabled") is False for spec in blocked_boss_specs), f"boss capabilities should honor runner gate: {blocked_boss_specs}")
|
||||
_assert({"round.radius", "feature.delete_round_or_chamfer"} <= _capability_keys(cache, "round:1"), f"round radius/delete should now be productized: {cache}")
|
||||
round_object = next((item for item in objects if isinstance(item, dict) and item.get("objectId") == "round:round:1"), None)
|
||||
_assert(isinstance(round_object, dict), f"round object should be normalized: {cache}")
|
||||
round_signature = round_object.get("geometrySignature")
|
||||
_assert(isinstance(round_signature, dict) and round_signature.get("isConstantRound") is True, f"round constant evidence should be preserved: {round_signature}")
|
||||
round_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(60,), execution_ready={"round.radius", "feature.delete_round_or_chamfer"})
|
||||
_assert(
|
||||
{str(spec.get("scdm_capability_key")) for spec in round_specs} == {"round.radius", "feature.delete_round_or_chamfer"},
|
||||
f"round cache should expose productized radius and delete command: {round_specs}",
|
||||
)
|
||||
round_radius_spec = next((spec for spec in round_specs if spec.get("scdm_capability_key") == "round.radius"), None)
|
||||
round_delete_spec = next((spec for spec in round_specs if spec.get("scdm_capability_key") == "feature.delete_round_or_chamfer"), None)
|
||||
_assert(round_radius_spec and round_radius_spec.get("value_type") == "positive" and round_radius_spec.get("enabled") is True, f"round.radius should be executable when runner gate is ready: {round_specs}")
|
||||
_assert(round_delete_spec and round_delete_spec.get("value_type") == "command" and round_delete_spec.get("enabled") is True, f"round delete command should be executable when runner gate is ready: {round_specs}")
|
||||
_assert({"chamfer.distance"} <= _capability_keys(cache, "chamfer:1"), f"chamfer distance should now be productized: {cache}")
|
||||
chamfer_object = next((item for item in objects if isinstance(item, dict) and item.get("objectId") == "chamfer:chamfer:1"), None)
|
||||
_assert(isinstance(chamfer_object, dict), f"chamfer object should be normalized: {cache}")
|
||||
chamfer_signature = chamfer_object.get("geometrySignature")
|
||||
_assert(isinstance(chamfer_signature, dict) and chamfer_signature.get("isEqualDistanceChamfer") is True, f"chamfer equal-distance evidence should be preserved: {chamfer_signature}")
|
||||
chamfer_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(63,), execution_ready={"chamfer.distance"})
|
||||
_assert(
|
||||
{str(spec.get("scdm_capability_key")) for spec in chamfer_specs} == {"chamfer.distance"},
|
||||
f"chamfer cache should expose productized chamfer.distance: {chamfer_specs}",
|
||||
)
|
||||
_assert(chamfer_specs and chamfer_specs[0].get("value_type") == "positive" and chamfer_specs[0].get("enabled") is True, f"chamfer.distance should be executable when runner gate is ready: {chamfer_specs}")
|
||||
|
||||
raw_without_local_ids = {
|
||||
"schemaVersion": 1,
|
||||
@@ -452,6 +628,163 @@ def main() -> int:
|
||||
grouped_specs = property_specs_from_scdm_cache(enriched_group, selected_face_ids=(96,), execution_ready=True)
|
||||
grouped_keys = {str(spec.get("scdm_capability_key")) for spec in grouped_specs}
|
||||
_assert({"hole.diameter", "hole.position", "feature.fill"} <= grouped_keys, f"local Face IDs should attach to SCDM cylinder groups: {enriched_group}")
|
||||
body_pattern_without_face_ids = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "body:20",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 20.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 20, "faceOrdinals": [0], "globalFaceOrdinals": [300]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:21",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 25.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 21, "faceOrdinals": [0], "globalFaceOrdinals": [301]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:22",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 30.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 22, "faceOrdinals": [0], "globalFaceOrdinals": [302]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
enriched_body_pattern = attach_local_face_ids_to_scdm_cache(
|
||||
map_scdm_raw_features(body_pattern_without_face_ids),
|
||||
[
|
||||
{"faceId": 300, "bodyIndex": 20, "faceOrdinal": 0, "globalFaceOrdinal": 300, "surfaceType": "plane"},
|
||||
{"faceId": 301, "bodyIndex": 21, "faceOrdinal": 0, "globalFaceOrdinal": 301, "surfaceType": "plane"},
|
||||
{"faceId": 302, "bodyIndex": 22, "faceOrdinal": 0, "globalFaceOrdinal": 302, "surfaceType": "plane"},
|
||||
],
|
||||
)
|
||||
body_pattern_specs_from_ordinals = property_specs_from_scdm_cache(enriched_body_pattern, selected_face_ids=(301,), execution_ready={"pattern.spacing"})
|
||||
_assert(
|
||||
any(spec.get("scdm_capability_key") == "pattern.spacing" for spec in body_pattern_specs_from_ordinals),
|
||||
f"body pattern spacing should attach local Face IDs from SCDM ordinals: {enriched_body_pattern}",
|
||||
)
|
||||
unit_scaled_face_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "m"},
|
||||
"diagnostics": {"availableCommands": [{"name": "OffsetFaces", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "body:0/face:9",
|
||||
"objectType": "face",
|
||||
"geometry": {"surfaceType": "plane", "axis": [-1.0, 0.0, 0.0], "planeOffset": -0.00125},
|
||||
"topologyHint": {"bodyIndex": 0, "faceOrdinal": 9, "globalFaceOrdinal": 9},
|
||||
"backendCommandCandidates": [{"operation": "pull_face_offset", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
unit_scaled_face_cache = attach_local_face_ids_to_scdm_cache(
|
||||
map_scdm_raw_features(unit_scaled_face_raw),
|
||||
[
|
||||
{
|
||||
"faceId": 9,
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 9,
|
||||
"globalFaceOrdinal": 9,
|
||||
"surfaceType": "plane",
|
||||
"axis": [-1.0, 0.0, 0.0],
|
||||
"planeOffset": -1.25,
|
||||
}
|
||||
],
|
||||
)
|
||||
unit_scaled_face_signature = unit_scaled_face_cache["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
_assert(abs(float(unit_scaled_face_signature.get("localUnitScale") or 0.0) - 0.001) <= 1.0e-12, f"ordinal-matched Face should infer SCDM/local unit scale: {unit_scaled_face_signature}")
|
||||
unit_scaled_face_specs = property_specs_from_scdm_cache(unit_scaled_face_cache, selected_face_ids=(9,), execution_ready=True)
|
||||
_assert(
|
||||
unit_scaled_face_specs and unit_scaled_face_specs[0].get("current_text") == "-1.25",
|
||||
f"SCDM length values should display in local model units: {unit_scaled_face_specs}",
|
||||
)
|
||||
unit_scaled_body_pattern_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "m"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "body:20",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 0.020, 0.0], "bboxSize": [0.001, 0.002, 0.003], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 20, "faceOrdinals": [0], "globalFaceOrdinals": [300]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:21",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 0.025, 0.0], "bboxSize": [0.001, 0.002, 0.003], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 21, "faceOrdinals": [0], "globalFaceOrdinals": [301]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:22",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 0.030, 0.0], "bboxSize": [0.001, 0.002, 0.003], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 22, "faceOrdinals": [0], "globalFaceOrdinals": [302]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
unit_scaled_body_pattern_cache = attach_local_face_ids_to_scdm_cache(
|
||||
map_scdm_raw_features(unit_scaled_body_pattern_raw),
|
||||
[
|
||||
{"faceId": 300, "bodyIndex": 20, "faceOrdinal": 0, "globalFaceOrdinal": 300, "surfaceType": "plane", "bboxMin": [-0.5, 19.0, 0.0], "bboxMax": [0.5, 21.0, 1.0]},
|
||||
{"faceId": 301, "bodyIndex": 21, "faceOrdinal": 0, "globalFaceOrdinal": 301, "surfaceType": "plane", "bboxMin": [-0.5, 24.0, 0.0], "bboxMax": [0.5, 26.0, 1.0]},
|
||||
{"faceId": 302, "bodyIndex": 22, "faceOrdinal": 0, "globalFaceOrdinal": 302, "surfaceType": "plane", "bboxMin": [-0.5, 29.0, 0.0], "bboxMax": [0.5, 31.0, 1.0]},
|
||||
{"faceId": 399, "bodyIndex": 99, "faceOrdinal": 0, "globalFaceOrdinal": 399, "surfaceType": "plane", "axis": [0.0, 0.0, 1.0], "planeOffset": 0.0, "bboxMin": [-1.0, 18.0, 0.0], "bboxMax": [1.0, 32.0, 0.0], "area": 28.0},
|
||||
],
|
||||
)
|
||||
unit_scaled_body_pattern = next(
|
||||
item for item in unit_scaled_body_pattern_cache["objects"] if item.get("objectType") == "linear_pattern" # type: ignore[index]
|
||||
)
|
||||
unit_scaled_body_signature = unit_scaled_body_pattern["geometrySignature"] # type: ignore[index]
|
||||
_assert(abs(float(unit_scaled_body_signature.get("localUnitScale") or 0.0) - 0.001) <= 1.0e-12, f"body pattern should infer unit scale from local body centers: {unit_scaled_body_signature}")
|
||||
_assert(unit_scaled_body_signature.get("supportFaceIds") == [399], f"body pattern should expose its support Face: {unit_scaled_body_signature}")
|
||||
fit = unit_scaled_body_signature.get("supportPatternFit")
|
||||
_assert(isinstance(fit, dict) and fit.get("supportFaceIds") == [399], f"body pattern should record support fit limits: {unit_scaled_body_signature}")
|
||||
_assert(abs(float(fit.get("maxSpacingLocal") or 0.0) - 6.0) <= 1.0e-9, f"support fit should limit centered spacing: {fit}")
|
||||
unit_scaled_body_specs = property_specs_from_scdm_cache(
|
||||
{"objects": [unit_scaled_body_pattern]},
|
||||
selected_face_ids=(399,),
|
||||
execution_ready={"pattern.spacing", "pattern.segment_spacing"},
|
||||
)
|
||||
_assert(
|
||||
unit_scaled_body_specs and unit_scaled_body_specs[0].get("current_text") == "5",
|
||||
f"support Face should expose pattern spacing in local units: {unit_scaled_body_specs}",
|
||||
)
|
||||
_assert(
|
||||
unit_scaled_body_specs[0].get("max_value") == 6.0,
|
||||
f"support Face pattern spacing should expose a local-unit hard limit: {unit_scaled_body_specs}",
|
||||
)
|
||||
local_segment_spec = next(
|
||||
(spec for spec in unit_scaled_body_specs if spec.get("scdm_capability_key") == "pattern.segment_spacing"),
|
||||
None,
|
||||
)
|
||||
_assert(
|
||||
isinstance(local_segment_spec, dict)
|
||||
and str(local_segment_spec.get("label") or "").startswith("Solid20-Solid21")
|
||||
and str(local_segment_spec.get("range_hint") or "").startswith("固定前项,移动后侧:")
|
||||
and "固定 Solid20" in str(local_segment_spec.get("range_hint") or "")
|
||||
and "平移 Solid21" in str(local_segment_spec.get("range_hint") or ""),
|
||||
f"local mapped pattern segment labels should use visible Solid IDs and explain motion semantics: {unit_scaled_body_specs}",
|
||||
)
|
||||
|
||||
missing_command_raw = {
|
||||
"schemaVersion": 1,
|
||||
@@ -479,6 +812,218 @@ def main() -> int:
|
||||
missing_command_specs = property_specs_from_scdm_cache(map_scdm_raw_features(missing_command_raw), selected_face_ids=(12,), execution_ready=True)
|
||||
_assert(missing_command_specs and missing_command_specs[0].get("enabled") is False, f"missing SCDM command should disable capability: {missing_command_specs}")
|
||||
_assert("OffsetFaces" in str(missing_command_specs[0].get("disabled_tip") or ""), f"disabled reason should name the missing SCDM command: {missing_command_specs}")
|
||||
missing_current_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "slot:missing-center",
|
||||
"objectType": "slot",
|
||||
"geometry": {"width": 2.0},
|
||||
"topologyHint": {"faceIds": [44]},
|
||||
"backendCommandCandidates": [{"operation": "move_slot", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
missing_current_specs = property_specs_from_scdm_cache(map_scdm_raw_features(missing_current_raw), selected_face_ids=(44,), execution_ready={"slot.width", "slot.position"})
|
||||
slot_position_missing_current = next((spec for spec in missing_current_specs if spec.get("scdm_capability_key") == "slot.position"), None)
|
||||
_assert(slot_position_missing_current and slot_position_missing_current.get("enabled") is False, f"missing current value should disable slot.position: {missing_current_specs}")
|
||||
_assert("没有返回可用于编辑的当前值" in str(slot_position_missing_current.get("disabled_tip") or ""), f"missing current value should be explained: {missing_current_specs}")
|
||||
slot_missing_depth_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "slot:missing-depth-locator",
|
||||
"objectType": "slot",
|
||||
"geometry": {"width": 2.0, "depth": 1.5, "depthAxis": [0.0, 0.0, -1.0], "center": [1.0, 2.0, 3.0]},
|
||||
"topologyHint": {"faceIds": [45], "bodyIndex": 0, "faceOrdinal": 45},
|
||||
"backendCommandCandidates": [{"operation": "change_slot_depth", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
slot_missing_depth_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(slot_missing_depth_locator_raw),
|
||||
selected_face_ids=(45,),
|
||||
execution_ready={"slot.depth"},
|
||||
)
|
||||
slot_depth_without_locator = next((spec for spec in slot_missing_depth_locator_specs if spec.get("scdm_capability_key") == "slot.depth"), None)
|
||||
_assert(slot_depth_without_locator and slot_depth_without_locator.get("enabled") is False, f"slot.depth should require a bottom face locator: {slot_missing_depth_locator_specs}")
|
||||
_assert("槽底面定位信息" in str(slot_depth_without_locator.get("disabled_tip") or ""), f"slot.depth missing locator should be explained: {slot_missing_depth_locator_specs}")
|
||||
slot_missing_depth_axis_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "slot:missing-depth-axis",
|
||||
"objectType": "slot",
|
||||
"geometry": {"width": 2.0, "depth": 1.5, "center": [1.0, 2.0, 3.0]},
|
||||
"topologyHint": {"faceIds": [46], "bodyIndex": 0, "faceOrdinal": 46, "depthFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 46}]},
|
||||
"backendCommandCandidates": [{"operation": "change_slot_depth", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
slot_missing_depth_axis_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(slot_missing_depth_axis_raw),
|
||||
selected_face_ids=(46,),
|
||||
execution_ready={"slot.depth"},
|
||||
)
|
||||
slot_depth_without_axis = next((spec for spec in slot_missing_depth_axis_specs if spec.get("scdm_capability_key") == "slot.depth"), None)
|
||||
_assert(slot_depth_without_axis and slot_depth_without_axis.get("enabled") is False, f"slot.depth should require a depth axis: {slot_missing_depth_axis_specs}")
|
||||
_assert("槽深方向" in str(slot_depth_without_axis.get("disabled_tip") or ""), f"slot.depth missing axis should be explained: {slot_missing_depth_axis_specs}")
|
||||
boss_missing_height_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "boss:missing-height-locator",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometry": {"diameter": 3.0, "height": 4.0, "center": [0.0, 0.0, 2.0]},
|
||||
"topologyHint": {"faceIds": [55], "bodyIndex": 0, "faceOrdinal": 55},
|
||||
"backendCommandCandidates": [{"operation": "change_boss_height", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
boss_missing_height_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(boss_missing_height_locator_raw),
|
||||
selected_face_ids=(55,),
|
||||
execution_ready={"boss.height"},
|
||||
)
|
||||
boss_height_without_locator = next((spec for spec in boss_missing_height_locator_specs if spec.get("scdm_capability_key") == "boss.height"), None)
|
||||
_assert(boss_height_without_locator and boss_height_without_locator.get("enabled") is False, f"boss.height should require a top face locator: {boss_missing_height_locator_specs}")
|
||||
_assert("顶面定位信息" in str(boss_height_without_locator.get("disabled_tip") or ""), f"boss.height missing locator should be explained: {boss_missing_height_locator_specs}")
|
||||
boss_missing_diameter_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "OffsetFaces", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "boss:missing-diameter-locator",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometry": {"diameter": 3.0, "height": 4.0, "center": [0.0, 0.0, 2.0]},
|
||||
"topologyHint": {"faceIds": [56], "bodyIndex": 0, "faceOrdinal": 56},
|
||||
"backendCommandCandidates": [{"operation": "change_boss_diameter", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
boss_missing_diameter_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(boss_missing_diameter_locator_raw),
|
||||
selected_face_ids=(56,),
|
||||
execution_ready={"boss.diameter"},
|
||||
)
|
||||
boss_diameter_without_locator = next((spec for spec in boss_missing_diameter_locator_specs if spec.get("scdm_capability_key") == "boss.diameter"), None)
|
||||
_assert(boss_diameter_without_locator and boss_diameter_without_locator.get("enabled") is False, f"boss.diameter should require a side face locator: {boss_missing_diameter_locator_specs}")
|
||||
_assert("侧壁定位信息" in str(boss_diameter_without_locator.get("disabled_tip") or ""), f"boss.diameter missing locator should be explained: {boss_missing_diameter_locator_specs}")
|
||||
round_missing_constant_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "ConstantRound", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "round:missing-constant",
|
||||
"objectType": "round",
|
||||
"geometry": {"roundInfo": {"radius": 1.0, "isRound": True}},
|
||||
"topologyHint": {"faceIds": [61], "bodyIndex": 0, "faceOrdinal": 61},
|
||||
"backendCommandCandidates": [{"operation": "change_round_radius", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
round_missing_constant_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(round_missing_constant_raw),
|
||||
selected_face_ids=(61,),
|
||||
execution_ready={"round.radius"},
|
||||
)
|
||||
round_without_constant = next((spec for spec in round_missing_constant_specs if spec.get("scdm_capability_key") == "round.radius"), None)
|
||||
_assert(round_without_constant and round_without_constant.get("enabled") is False, f"round.radius should require constant-round evidence: {round_missing_constant_specs}")
|
||||
_assert("等半径圆角证据" in str(round_without_constant.get("disabled_tip") or ""), f"round.radius missing constant evidence should be explained: {round_missing_constant_specs}")
|
||||
round_missing_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "ConstantRound", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "round:missing-locator",
|
||||
"objectType": "round",
|
||||
"geometry": {"roundInfo": {"radius": 1.0, "isConstant": True, "isRound": True}},
|
||||
"topologyHint": {"faceIds": [62]},
|
||||
"backendCommandCandidates": [{"operation": "change_round_radius", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
round_missing_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(round_missing_locator_raw),
|
||||
selected_face_ids=(62,),
|
||||
execution_ready={"round.radius"},
|
||||
)
|
||||
round_without_locator = next((spec for spec in round_missing_locator_specs if spec.get("scdm_capability_key") == "round.radius"), None)
|
||||
_assert(round_without_locator and round_without_locator.get("enabled") is False, f"round.radius should require SCDM face locator evidence: {round_missing_locator_specs}")
|
||||
_assert("可定位的圆角面" in str(round_without_locator.get("disabled_tip") or ""), f"round.radius missing locator should be explained: {round_missing_locator_specs}")
|
||||
chamfer_missing_equal_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Chamfer", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "chamfer:missing-equal",
|
||||
"objectType": "chamfer",
|
||||
"geometry": {"chamferInfo": {"distance": 0.8, "isChamfer": True}},
|
||||
"topologyHint": {"faceIds": [64], "bodyIndex": 0, "faceOrdinal": 64},
|
||||
"backendCommandCandidates": [{"operation": "change_chamfer_distance", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
chamfer_missing_equal_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(chamfer_missing_equal_raw),
|
||||
selected_face_ids=(64,),
|
||||
execution_ready={"chamfer.distance"},
|
||||
)
|
||||
chamfer_without_equal = next((spec for spec in chamfer_missing_equal_specs if spec.get("scdm_capability_key") == "chamfer.distance"), None)
|
||||
_assert(chamfer_without_equal and chamfer_without_equal.get("enabled") is False, f"chamfer.distance should require equal-distance evidence: {chamfer_missing_equal_specs}")
|
||||
_assert("等距倒角证据" in str(chamfer_without_equal.get("disabled_tip") or ""), f"chamfer.distance missing equal evidence should be explained: {chamfer_missing_equal_specs}")
|
||||
chamfer_missing_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Chamfer", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "chamfer:missing-locator",
|
||||
"objectType": "chamfer",
|
||||
"geometry": {"chamferInfo": {"distance": 0.8, "isEqualDistance": True, "isChamfer": True}},
|
||||
"topologyHint": {"faceIds": [65]},
|
||||
"backendCommandCandidates": [{"operation": "change_chamfer_distance", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
chamfer_missing_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(chamfer_missing_locator_raw),
|
||||
selected_face_ids=(65,),
|
||||
execution_ready={"chamfer.distance"},
|
||||
)
|
||||
chamfer_without_locator = next((spec for spec in chamfer_missing_locator_specs if spec.get("scdm_capability_key") == "chamfer.distance"), None)
|
||||
_assert(chamfer_without_locator and chamfer_without_locator.get("enabled") is False, f"chamfer.distance should require SCDM face locator evidence: {chamfer_missing_locator_specs}")
|
||||
_assert("可定位的倒角面" in str(chamfer_without_locator.get("disabled_tip") or ""), f"chamfer.distance missing locator should be explained: {chamfer_missing_locator_specs}")
|
||||
diagnostics = cache.get("diagnostics")
|
||||
_assert(isinstance(diagnostics, dict), f"cache diagnostics should be present: {cache}")
|
||||
raw_summary = diagnostics.get("raw_summary")
|
||||
@@ -499,11 +1044,18 @@ def main() -> int:
|
||||
and feature_inventory.get("operationCounts", {}).get("change_slot_width") == 1,
|
||||
f"SCDM feature inventory should be preserved: {cache}",
|
||||
)
|
||||
component_instances = diagnostics.get("component_instances")
|
||||
_assert(
|
||||
isinstance(component_instances, list)
|
||||
and component_instances
|
||||
and component_instances[0].get("componentPath") == [0],
|
||||
f"SCDM component occurrence diagnostics should be preserved: {cache}",
|
||||
)
|
||||
geometry_hints = diagnostics.get("geometry_candidate_hints")
|
||||
_assert(isinstance(geometry_hints, list) and geometry_hints, f"SCDM structural candidate hints should be produced: {cache}")
|
||||
hint_keys = {str(item.get("capabilityKey")) for item in geometry_hints if isinstance(item, dict)}
|
||||
_assert(
|
||||
{"slot.width", "boss.height", "round.radius", "chamfer.distance", "pattern.spacing", "shell.thickness"} <= hint_keys,
|
||||
{"pattern.instance_position", "shell.thickness"} <= hint_keys and "slot.depth" not in hint_keys and "pattern.spacing" not in hint_keys,
|
||||
f"S7 geometry hints should cover planned feature families: {hint_keys}",
|
||||
)
|
||||
derived_candidates = diagnostics.get("derived_feature_candidates")
|
||||
@@ -516,20 +1068,103 @@ def main() -> int:
|
||||
_assert(pattern_signature.get("instanceCount") == 3, f"linear pattern count should be preserved: {pattern_signature}")
|
||||
_assert(pattern_signature.get("axis") == [1.0, 0.0, 0.0], f"linear pattern axis should be preserved: {pattern_signature}")
|
||||
_assert(len(pattern_signature.get("instanceCenters") or []) == 3, f"linear pattern centers should be preserved: {pattern_signature}")
|
||||
_assert(len(pattern_signature.get("patternInstances") or []) == 3, f"linear pattern instance locators should be preserved: {pattern_signature}")
|
||||
_assert({"pattern.spacing"} <= _capability_keys(cache, "derived:linear_pattern"), f"derived linear pattern should expose productized spacing: {cache}")
|
||||
thin_wall_candidate = next((item for item in derived_candidates if isinstance(item, dict) and item.get("objectType") == "thin_wall"), None)
|
||||
_assert(isinstance(thin_wall_candidate, dict), f"paired planar faces should produce a thin-wall candidate: {derived_candidates}")
|
||||
thin_wall_signature = thin_wall_candidate.get("geometrySignature")
|
||||
_assert(isinstance(thin_wall_signature, dict), f"thin-wall candidate should keep a geometry signature: {thin_wall_candidate}")
|
||||
_assert(thin_wall_signature.get("thickness") == 1.2, f"thin-wall thickness should be preserved: {thin_wall_signature}")
|
||||
_assert(thin_wall_signature.get("thicknessAxis") == [0.0, 0.0, 1.0], f"thin-wall axis should be preserved: {thin_wall_signature}")
|
||||
_assert(len(thin_wall_signature.get("wallFaceLocators") or []) == 2, f"thin-wall face locators should be preserved: {thin_wall_signature}")
|
||||
body_pattern_candidate = next(
|
||||
(
|
||||
item
|
||||
for item in derived_candidates
|
||||
if isinstance(item, dict)
|
||||
and item.get("objectType") == "linear_pattern"
|
||||
and isinstance(item.get("geometrySignature"), dict)
|
||||
and item.get("geometrySignature", {}).get("patternKind") == "body"
|
||||
),
|
||||
None,
|
||||
)
|
||||
_assert(isinstance(body_pattern_candidate, dict), f"repeated bodies should produce a body linear-pattern candidate: {derived_candidates}")
|
||||
body_pattern_signature = body_pattern_candidate.get("geometrySignature")
|
||||
_assert(isinstance(body_pattern_signature, dict), f"body pattern should keep a geometry signature: {body_pattern_candidate}")
|
||||
_assert(body_pattern_signature.get("spacing") == 5.0, f"body pattern spacing should be preserved: {body_pattern_signature}")
|
||||
_assert(body_pattern_signature.get("axis") == [0.0, 1.0, 0.0], f"body pattern axis should be preserved: {body_pattern_signature}")
|
||||
_assert(body_pattern_signature.get("bodyIndices") == [20, 21, 22], f"body pattern body indices should be preserved: {body_pattern_signature}")
|
||||
body_instances = body_pattern_signature.get("patternInstances") or []
|
||||
_assert(len(body_instances) == 3 and all(item.get("instanceKind") == "body" for item in body_instances), f"body pattern instances should stay body-level: {body_pattern_signature}")
|
||||
_assert(all(item.get("componentLocators") for item in body_instances if isinstance(item, dict)), f"body pattern instances should preserve component occurrence locators: {body_pattern_signature}")
|
||||
body_pattern_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(301,), execution_ready={"pattern.spacing", "pattern.segment_spacing"})
|
||||
body_pattern_spacing = [
|
||||
spec for spec in body_pattern_specs if spec.get("scdm_capability_key") == "pattern.spacing"
|
||||
]
|
||||
_assert(body_pattern_spacing, f"selecting a body member face should keep body pattern spacing as a recognized capability: {body_pattern_specs}")
|
||||
_assert(
|
||||
body_pattern_spacing[0].get("enabled") is True,
|
||||
f"body pattern spacing should be executable when every member has a component occurrence locator: {body_pattern_spacing}",
|
||||
)
|
||||
body_segment_specs = [
|
||||
spec for spec in body_pattern_specs if spec.get("scdm_capability_key") == "pattern.segment_spacing"
|
||||
]
|
||||
_assert(len(body_segment_specs) == 2, f"three body pattern members should expose two local spacing segments: {body_pattern_specs}")
|
||||
_assert(
|
||||
"零件" in str(body_segment_specs[0].get("label") or "")
|
||||
and body_segment_specs[0].get("enabled") is True
|
||||
and body_segment_specs[0].get("scdm_backend_operation") == "change_pattern_segment_spacing",
|
||||
f"local body pattern spacing should be executable and name the adjacent members: {body_segment_specs}",
|
||||
)
|
||||
body_segment_modes = body_segment_specs[0].get("scope_modes")
|
||||
_assert(
|
||||
isinstance(body_segment_modes, dict)
|
||||
and {"fix_left_move_right", "fix_right_move_left", "split_keep_center", "move_single_right"} <= set(body_segment_modes),
|
||||
f"local pattern spacing should expose several explicit modeling intents: {body_segment_specs}",
|
||||
)
|
||||
_assert(
|
||||
body_segment_modes["fix_left_move_right"].get("enabled") is True
|
||||
and body_segment_modes["fix_right_move_left"].get("enabled") is True
|
||||
and body_segment_modes["split_keep_center"].get("enabled") is True
|
||||
and body_segment_modes["move_single_right"].get("enabled") is False,
|
||||
f"implemented and blocked local spacing intents should be explicit: {body_segment_modes}",
|
||||
)
|
||||
for mode in body_segment_modes.values():
|
||||
if not isinstance(mode, dict):
|
||||
continue
|
||||
label = str(mode.get("label") or "").strip()
|
||||
tip = str(mode.get("enabled_tip") or mode.get("disabled_tip") or mode.get("range_hint") or "").strip()
|
||||
_assert(label and tip.startswith(label), f"modeling-intent tooltip should start with its intent label: {body_segment_modes}")
|
||||
_assert(
|
||||
"固定前项" in str(body_segment_specs[0].get("scope_text") or "")
|
||||
and "移动后侧" in str(body_segment_specs[0].get("scope_text") or ""),
|
||||
f"local pattern spacing should expose motion semantics in the intent column: {body_segment_specs}",
|
||||
)
|
||||
body_segment_signature = body_segment_specs[0].get("scdm_geometry_signature")
|
||||
_assert(
|
||||
isinstance(body_segment_signature, dict)
|
||||
and body_segment_signature.get("segmentIndex") == 0
|
||||
and body_segment_signature.get("movingSide") == "after",
|
||||
f"local segment specs should carry segment execution metadata: {body_segment_specs}",
|
||||
)
|
||||
pattern_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(201,), execution_ready={"pattern.spacing", "pattern.segment_spacing"})
|
||||
pattern_spacing_spec = next((spec for spec in pattern_specs if spec.get("scdm_capability_key") == "pattern.spacing"), None)
|
||||
_assert(pattern_spacing_spec and pattern_spacing_spec.get("enabled") is True, f"pattern.spacing should enable for selected pattern member: {pattern_specs}")
|
||||
_assert(pattern_spacing_spec.get("value_type") == "positive", f"pattern.spacing should use positive numeric input: {pattern_specs}")
|
||||
face_segment_specs = [
|
||||
spec for spec in pattern_specs if spec.get("scdm_capability_key") == "pattern.segment_spacing"
|
||||
]
|
||||
_assert(len(face_segment_specs) == 2, f"three face pattern members should expose two local spacing segments: {pattern_specs}")
|
||||
_assert(
|
||||
str(face_segment_specs[0].get("label") or "").startswith("Face"),
|
||||
f"face pattern local spacing should name the adjacent Face members instead of raw ordinal numbers: {face_segment_specs}",
|
||||
)
|
||||
not_productized = diagnostics.get("discovered_not_productized")
|
||||
_assert(isinstance(not_productized, list) and not_productized, f"round candidate should stay diagnostic only: {cache}")
|
||||
_assert(isinstance(not_productized, list) and not_productized, f"non-productized candidates should stay diagnostic only: {cache}")
|
||||
planned = diagnostics.get("planned_not_productized")
|
||||
_assert(isinstance(planned, list), f"planned diagnostics should be present: {cache}")
|
||||
planned_keys = {str(item.get("capabilityKey")) for item in planned if isinstance(item, dict)}
|
||||
expected_planned = {
|
||||
"slot.width",
|
||||
"slot.depth",
|
||||
"boss.height",
|
||||
"boss.diameter",
|
||||
"round.radius",
|
||||
"feature.delete_round_or_chamfer",
|
||||
"chamfer.distance",
|
||||
"pattern.spacing",
|
||||
"pattern.instance_position",
|
||||
"shell.thickness",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user