feat: 完善 SCDM-first 参数化编辑交付版

This commit is contained in:
2026-08-20 17:12:01 +08:00
parent 4e7877e05c
commit b4feab24d2
21 changed files with 3015 additions and 1729 deletions
+181 -13
View File
@@ -326,6 +326,42 @@ def main() -> int:
],
"rawLimitations": [],
},
{
"backendId": "body:0/face:1722",
"objectType": "round",
"geometry": {
"surfaceType": "cylinder",
"radius": 0.0013,
"diameter": 0.0026,
"center": [-0.083, -0.031, -0.11635],
"axis": [0.0, 1.0, 0.0],
"roundInfo": {"radius": 0.0, "available": True, "type": "RoundInfoResult"},
},
"topologyHint": {"faceIds": [1722], "bodyIndex": 0, "faceOrdinal": 1722, "globalFaceOrdinal": 1722},
"backendCommandCandidates": [
{"operation": "change_round_radius", "enabled": True},
{"operation": "delete_round_or_chamfer", "enabled": True},
],
"rawLimitations": [],
},
{
"backendId": "body:0/face:1723",
"objectType": "round",
"geometry": {
"surfaceType": "cylinder",
"radius": 0.0013,
"diameter": 0.0026,
"center": [-0.083, -0.031, -0.11635],
"axis": [0.0, 1.0, 0.0],
"roundInfo": {"radius": 0.0, "available": True, "type": "RoundInfoResult"},
},
"topologyHint": {"faceIds": [1723], "bodyIndex": 0, "faceOrdinal": 1723, "globalFaceOrdinal": 1723},
"backendCommandCandidates": [
{"operation": "change_round_radius", "enabled": True},
{"operation": "delete_round_or_chamfer", "enabled": True},
],
"rawLimitations": [],
},
{
"backendId": "slot:1",
"objectType": "slot",
@@ -549,6 +585,36 @@ def main() -> int:
_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_split_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(1722,), execution_ready=True)
round_split_keys = {str(spec.get("scdm_capability_key")) for spec in round_split_specs}
round_split_group = next(
(
item
for item in objects
if isinstance(item, dict)
and item.get("objectType") == "cylindrical_face_group"
and {1722, 1723} <= set(item.get("geometrySignature", {}).get("faceIds", []))
),
None,
)
_assert(
round_split_group is None,
f"SCDM round cylinder fragments must not be derived into a cylindrical hole group: {round_split_group}",
)
round_split_radius_spec = next((spec for spec in round_split_specs if spec.get("scdm_capability_key") == "round.radius"), None)
round_split_radius_value = float(str(round_split_radius_spec.get("current_raw"))) if round_split_radius_spec else None
_assert(
round_split_radius_value is not None and abs(round_split_radius_value - 0.0013) < 1.0e-9,
f"SCDM roundInfo radius=0 should fall back to geometric radius: {round_split_specs}",
)
_assert(
"hole.diameter" not in round_split_keys and "hole.position" not in round_split_keys,
f"SCDM round cylinder faces must not be derived into editable hole groups: {round_split_specs}",
)
_assert(
"feature.delete_round_or_chamfer" in round_split_keys,
f"SCDM round cylinder faces should keep their own round/delete capabilities: {round_split_specs}",
)
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"},
@@ -812,6 +878,56 @@ 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}")
delete_only_raw = {
"schemaVersion": 1,
"backend": {"name": "SCDM", "version": "v222"},
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
"diagnostics": {"availableCommands": [{"name": "Delete", "available": True}, {"name": "Fill", "available": False}]},
"objects": [
{
"backendId": "round:delete-only",
"objectType": "round",
"geometry": {"roundInfo": {"radius": 1.0, "isConstant": True, "isRound": True}},
"topologyHint": {"faceIds": [48], "bodyIndex": 0, "faceOrdinal": 48, "globalFaceOrdinal": 48},
"backendCommandCandidates": [{"operation": "delete_round_or_chamfer", "enabled": True}],
"rawLimitations": [],
}
],
}
delete_only_specs = property_specs_from_scdm_cache(
map_scdm_raw_features(delete_only_raw),
selected_face_ids=(48,),
execution_ready={"feature.delete_round_or_chamfer"},
)
delete_only_spec = next((spec for spec in delete_only_specs if spec.get("scdm_capability_key") == "feature.delete_round_or_chamfer"), None)
_assert(delete_only_spec and delete_only_spec.get("enabled") is True, f"Delete-only SCDM should enable round/chamfer removal: {delete_only_specs}")
shell_without_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": "shell:local-face-ids-only",
"objectType": "shell",
"geometry": {"thickness": 1.2, "thicknessAxis": [0.0, 0.0, 1.0]},
"topologyHint": {"faceIds": [149, 150]},
"backendCommandCandidates": [{"operation": "change_shell_thickness", "enabled": True}],
"rawLimitations": [],
}
],
}
shell_without_locator_specs = property_specs_from_scdm_cache(
map_scdm_raw_features(shell_without_locator_raw),
selected_face_ids=(149,),
execution_ready={"shell.thickness"},
)
shell_without_locator_spec = next((spec for spec in shell_without_locator_specs if spec.get("scdm_capability_key") == "shell.thickness"), None)
_assert(
shell_without_locator_spec and shell_without_locator_spec.get("enabled") is False,
f"shell.thickness should not execute when only local Face IDs are known: {shell_without_locator_specs}",
)
_assert("两侧墙面" in str(shell_without_locator_spec.get("disabled_tip") or ""), f"shell.thickness disabled reason should explain missing wall locators: {shell_without_locator_specs}")
missing_current_raw = {
"schemaVersion": 1,
"backend": {"name": "SCDM", "version": "v222"},
@@ -1052,10 +1168,10 @@ def main() -> int:
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}")
_assert(isinstance(geometry_hints, list), f"SCDM structural candidate hints should be preserved as a list: {cache}")
hint_keys = {str(item.get("capabilityKey")) for item in geometry_hints if isinstance(item, dict)}
_assert(
{"pattern.instance_position", "shell.thickness"} <= hint_keys and "slot.depth" not in hint_keys and "pattern.spacing" not in hint_keys,
"pattern.instance_position" not in hint_keys and "shell.thickness" not in 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")
@@ -1077,6 +1193,11 @@ def main() -> int:
_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}")
_assert({"shell.thickness"} <= _capability_keys(cache, "derived:thin_wall"), f"derived thin wall should expose productized shell thickness: {cache}")
thin_wall_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(101,), execution_ready={"shell.thickness"})
thin_wall_thickness_spec = next((spec for spec in thin_wall_specs if spec.get("scdm_capability_key") == "shell.thickness"), None)
_assert(thin_wall_thickness_spec and thin_wall_thickness_spec.get("enabled") is True, f"thin-wall thickness should enable when both wall faces are locatable: {thin_wall_specs}")
_assert(thin_wall_thickness_spec.get("scope_text") == "固定一侧,移动另一侧", f"thin-wall thickness intent should be explicit: {thin_wall_specs}")
body_pattern_candidate = next(
(
item
@@ -1097,7 +1218,7 @@ def main() -> int:
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_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(301,), execution_ready={"pattern.spacing", "pattern.segment_spacing", "pattern.instance_position"})
body_pattern_spacing = [
spec for spec in body_pattern_specs if spec.get("scdm_capability_key") == "pattern.spacing"
]
@@ -1111,7 +1232,7 @@ def main() -> int:
]
_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 "")
"Solid" 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}",
@@ -1119,15 +1240,16 @@ def main() -> int:
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),
and {"fix_left_move_right", "fix_right_move_left", "split_keep_center", "move_single_left", "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}",
and body_segment_modes["move_single_left"].get("enabled") is True
and body_segment_modes["move_single_right"].get("enabled") is True,
f"implemented local spacing intents should be explicit: {body_segment_modes}",
)
for mode in body_segment_modes.values():
if not isinstance(mode, dict):
@@ -1135,6 +1257,11 @@ def main() -> int:
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_modes["move_single_right"].get("range_hint") or "")
and "不用于保持整列等距" in str(body_segment_modes["move_single_left"].get("range_hint") or ""),
f"single-instance local spacing intents should warn that they break equal spacing: {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 ""),
@@ -1147,7 +1274,44 @@ def main() -> int:
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"})
body_instance_position = next((spec for spec in body_pattern_specs if spec.get("scdm_capability_key") == "pattern.instance_position"), None)
_assert(
body_instance_position
and body_instance_position.get("enabled") is True
and body_instance_position.get("value_type") == "vector3"
and body_instance_position.get("scdm_backend_operation") == "move_pattern_instance",
f"body pattern member should expose a single-instance position edit: {body_pattern_specs}",
)
_assert(
"只移动该实例" in str(body_instance_position.get("scope_text") or "")
and "不自动保持整体阵列等距" in str(body_instance_position.get("range_hint") or ""),
f"pattern instance position should explain that it is not a spacing edit: {body_instance_position}",
)
solid_body_pattern_specs = property_specs_from_scdm_cache(
cache,
selected_solid_ids=(21,),
execution_ready={"pattern.spacing", "pattern.segment_spacing", "pattern.instance_position", "face.offset"},
)
solid_pattern_keys = [str(spec.get("scdm_capability_key") or "") for spec in solid_body_pattern_specs]
_assert(
"pattern.spacing" in solid_pattern_keys
and "pattern.segment_spacing" in solid_pattern_keys
and "pattern.instance_position" in solid_pattern_keys,
f"selecting a Solid member should expose body-level pattern capabilities: {solid_body_pattern_specs}",
)
_assert(
"face.offset" not in solid_pattern_keys,
f"selecting a Solid should not expand child Face offsets from the SCDM cache: {solid_body_pattern_specs}",
)
solid_instance_specs = [
spec for spec in solid_body_pattern_specs if spec.get("scdm_capability_key") == "pattern.instance_position"
]
_assert(
len(solid_instance_specs) == 1
and str(solid_instance_specs[0].get("label") or "").startswith("Solid21"),
f"Solid selection should expose only the selected member position row: {solid_body_pattern_specs}",
)
pattern_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(201,), execution_ready={"pattern.spacing", "pattern.segment_spacing", "pattern.instance_position"})
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}")
@@ -1159,16 +1323,20 @@ def main() -> int:
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}",
)
face_instance_position = next((spec for spec in pattern_specs if spec.get("scdm_capability_key") == "pattern.instance_position"), None)
_assert(
face_instance_position
and face_instance_position.get("enabled") is True
and str(face_instance_position.get("label") or "").startswith("Face"),
f"face pattern member should expose its own position edit with visible Face ID: {pattern_specs}",
)
not_productized = diagnostics.get("discovered_not_productized")
_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 = {
"pattern.instance_position",
"shell.thickness",
}
_assert(expected_planned <= planned_keys, f"S7 planned capabilities should be diagnosed but not productized: {planned_keys}")
_assert("pattern.instance_position" not in planned_keys, f"pattern.instance_position should now be productized with member-level specs: {planned_keys}")
_assert("shell.thickness" not in planned_keys, f"shell.thickness should now be productized with strict geometry gates: {planned_keys}")
raw_file = root / "raw.json"
cache_file = root / "cache.json"