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
+238 -13
View File
@@ -175,10 +175,15 @@ def main() -> int:
"change_boss_diameter",
"change_round_radius",
"change_chamfer_distance",
"change_shell_thickness",
"change_pattern_spacing",
"change_pattern_segment_spacing",
"move_pattern_instance",
"segment_before",
"segment_split",
"segment_single_left",
"segment_single_right",
"wallFaceLocators",
"pull_face_offset",
"fill_feature",
"delete_round_or_chamfer",
@@ -207,6 +212,11 @@ def main() -> int:
):
_assert(token in script, f"generated edit script missing {token}")
_assert("Pull.Execute" not in script, "generated edit script should use documented OffsetFaces instead of Pull.Execute")
_assert("_delete_selection(selection, fill_errors)" in script, "generated edit script should fall back from Fill to Delete")
_assert(
"if fill is None:\n raise Exception('capability_not_implemented: Fill command not available')" not in script,
"Delete-capable SCDM environments must not fail merely because Fill is missing",
)
generated = generate_scdm_edit_script(job_path)
_assert("JOB_PATH =" in generated and job_path.name in generated, "generated edit script should embed the job path")
@@ -253,6 +263,19 @@ def main() -> int:
_assert(slot_width_job.get("target", {}).get("backendOperation") == "change_slot_width", f"slot.width should route to change_slot_width: {slot_width_job}")
_assert(slot_width_job.get("target", {}).get("value") == 2.5, f"slot.width target should be numeric: {slot_width_job}")
_assert(slot_width_job.get("object", {}).get("geometrySignature", {}).get("width") == 2.0, f"slot width signature should be preserved: {slot_width_job}")
slot_width_noop = prepare_scdm_edit_job(
step_path,
output_dir=root / "slot-width-noop",
backend=backend,
capability_key="slot.width",
target_value="2.0",
object_id="slot:30-31-32",
object_signature=slot_signature,
)
_assert(
slot_width_noop.get("ok") is False and slot_width_noop.get("reason") == "target-already-current",
f"slot.width no-op should be blocked before launching SCDM: {slot_width_noop}",
)
slot_depth_prepared = prepare_scdm_edit_job(
step_path,
output_dir=root / "slot-depth-prepared",
@@ -270,6 +293,22 @@ def main() -> int:
_assert(slot_depth_signature.get("depth") == 1.5, f"slot depth signature should be preserved: {slot_depth_job}")
_assert(slot_depth_signature.get("depthFaceLocators"), f"slot depth bottom face locator should be preserved: {slot_depth_job}")
_assert(slot_depth_signature.get("depthAxis") == [0.0, 0.0, -1.0], f"slot depth axis should be preserved: {slot_depth_job}")
slot_depth_missing_locator_signature = dict(slot_signature)
slot_depth_missing_locator_signature.pop("depthFaceLocators", None)
slot_depth_missing_locator = prepare_scdm_edit_job(
step_path,
output_dir=root / "slot-depth-missing-locator",
backend=backend,
capability_key="slot.depth",
target_value="2.0",
object_id="slot:30-31-32",
object_signature=slot_depth_missing_locator_signature,
)
_assert(
slot_depth_missing_locator.get("ok") is False
and slot_depth_missing_locator.get("reason") == "object-signature-missing-depth-face-locator",
f"slot.depth should be blocked before SCDM when bottom face locator is missing: {slot_depth_missing_locator}",
)
boss_signature = {
"objectType": "cylindrical_boss",
@@ -316,6 +355,22 @@ def main() -> int:
boss_height_signature = boss_height_job.get("object", {}).get("geometrySignature", {})
_assert(boss_height_signature.get("height") == 4.0, f"boss height signature should be preserved: {boss_height_job}")
_assert(boss_height_signature.get("heightFaceLocators"), f"boss height top face locator should be preserved: {boss_height_job}")
boss_height_missing_locator_signature = dict(boss_signature)
boss_height_missing_locator_signature.pop("heightFaceLocators", None)
boss_height_missing_locator = prepare_scdm_edit_job(
step_path,
output_dir=root / "boss-height-missing-locator",
backend=backend,
capability_key="boss.height",
target_value="5.5",
object_id="boss:50-51-52",
object_signature=boss_height_missing_locator_signature,
)
_assert(
boss_height_missing_locator.get("ok") is False
and boss_height_missing_locator.get("reason") == "object-signature-missing-height-face-locator",
f"boss.height should be blocked before SCDM when top face locator is missing: {boss_height_missing_locator}",
)
boss_diameter_prepared = prepare_scdm_edit_job(
step_path,
output_dir=root / "boss-diameter-prepared",
@@ -332,6 +387,48 @@ def main() -> int:
boss_diameter_signature = boss_diameter_job.get("object", {}).get("geometrySignature", {})
_assert(boss_diameter_signature.get("diameter") == 3.0, f"boss diameter signature should be preserved: {boss_diameter_job}")
_assert(boss_diameter_signature.get("diameterFaceLocators"), f"boss diameter side face locator should be preserved: {boss_diameter_job}")
boss_diameter_missing_locator_signature = dict(boss_signature)
boss_diameter_missing_locator_signature.pop("diameterFaceLocators", None)
boss_diameter_missing_locator = prepare_scdm_edit_job(
step_path,
output_dir=root / "boss-diameter-missing-locator",
backend=backend,
capability_key="boss.diameter",
target_value="4.5",
object_id="boss:50-51-52",
object_signature=boss_diameter_missing_locator_signature,
)
_assert(
boss_diameter_missing_locator.get("ok") is False
and boss_diameter_missing_locator.get("reason") == "object-signature-missing-diameter-face-locator",
f"boss.diameter should be blocked before SCDM when side face locator is missing: {boss_diameter_missing_locator}",
)
hole_position_noop = prepare_scdm_edit_job(
step_path,
output_dir=root / "hole-position-noop",
backend=backend,
capability_key="hole.position",
target_value=[0.5, 1.0, 9.5],
object_id="hole:85-94",
object_signature=signature,
)
_assert(
hole_position_noop.get("ok") is False and hole_position_noop.get("reason") == "target-already-current",
f"hole.position no-op should be blocked before launching SCDM: {hole_position_noop}",
)
boss_height_zero = prepare_scdm_edit_job(
step_path,
output_dir=root / "boss-height-zero",
backend=backend,
capability_key="boss.height",
target_value="0",
object_id="boss:50-51-52",
object_signature=boss_signature,
)
_assert(
boss_height_zero.get("ok") is False and boss_height_zero.get("reason") == "target-value-illegal",
f"positive dimensions should reject zero before launching SCDM: {boss_height_zero}",
)
round_signature = {
"objectType": "round",
@@ -404,6 +501,55 @@ def main() -> int:
_assert(chamfer_job_signature.get("isEqualDistanceChamfer") is True, f"chamfer.distance should preserve equal-distance evidence: {chamfer_distance_job}")
_assert(chamfer_job_signature.get("distance") == 0.8, f"chamfer.distance should preserve current distance: {chamfer_distance_job}")
shell_signature = {
"objectType": "thin_wall",
"faceIds": [101, 102],
"bodyIndex": 0,
"faceOrdinals": [101, 102],
"globalFaceOrdinals": [201, 202],
"thickness": 1.2,
"thicknessAxis": [0.0, 0.0, 1.0],
"wallFaceCenters": [[0.0, 0.0, 0.0], [0.0, 0.0, 1.2]],
"wallFaceLocators": [
{"bodyIndex": 0, "faceOrdinal": 101, "globalFaceOrdinal": 201},
{"bodyIndex": 0, "faceOrdinal": 102, "globalFaceOrdinal": 202},
],
}
shell_thickness_prepared = prepare_scdm_edit_job(
step_path,
output_dir=root / "shell-thickness-prepared",
backend=backend,
capability_key="shell.thickness",
target_value="1.6",
object_id="thin_wall:101-102",
object_signature=shell_signature,
)
_assert(shell_thickness_prepared.get("ok") is True, f"shell.thickness should be productized and prepare an edit job: {shell_thickness_prepared}")
shell_thickness_job = read_json(shell_thickness_prepared["job_path"])
_assert(shell_thickness_job.get("target", {}).get("backendOperation") == "change_shell_thickness", f"shell.thickness should route to change_shell_thickness: {shell_thickness_job}")
_assert(shell_thickness_job.get("target", {}).get("value") == 1.6, f"shell.thickness target should be numeric: {shell_thickness_job}")
shell_job_signature = shell_thickness_job.get("object", {}).get("geometrySignature", {})
_assert(shell_job_signature.get("thickness") == 1.2, f"shell thickness signature should be preserved: {shell_thickness_job}")
_assert(shell_job_signature.get("wallFaceLocators"), f"shell wall face locators should be preserved: {shell_thickness_job}")
shell_missing_locator_signature = dict(shell_signature)
shell_missing_locator_signature.pop("wallFaceLocators", None)
shell_missing_locator_signature.pop("faceOrdinals", None)
shell_missing_locator_signature.pop("globalFaceOrdinals", None)
shell_missing_locator = prepare_scdm_edit_job(
step_path,
output_dir=root / "shell-thickness-missing-locator",
backend=backend,
capability_key="shell.thickness",
target_value="1.6",
object_id="thin_wall:101-102",
object_signature=shell_missing_locator_signature,
)
_assert(
shell_missing_locator.get("ok") is False
and shell_missing_locator.get("reason") == "object-signature-missing-wall-face-locator",
f"shell.thickness should be blocked before SCDM when both wall locators are missing: {shell_missing_locator}",
)
pattern_signature = {
"objectType": "linear_pattern",
"faceIds": [85, 94, 87, 96, 89, 98],
@@ -514,6 +660,73 @@ def main() -> int:
and read_json(pattern_segment_split_prepared["job_path"]).get("object", {}).get("geometrySignature", {}).get("movingSide") == "split",
f"pattern.segment_spacing should preserve split/keep-center semantics: {pattern_segment_split_prepared}",
)
pattern_segment_single_left_signature = dict(pattern_segment_signature)
pattern_segment_single_left_signature["movingSide"] = "single_left"
pattern_segment_single_left_prepared = prepare_scdm_edit_job(
step_path,
output_dir=root / "pattern-segment-spacing-single-left-prepared",
backend=backend,
capability_key="pattern.segment_spacing",
target_value="6.5",
object_id="pattern:holes-a-b-c",
object_signature=pattern_segment_single_left_signature,
)
_assert(
pattern_segment_single_left_prepared.get("ok") is True
and read_json(pattern_segment_single_left_prepared["job_path"]).get("object", {}).get("geometrySignature", {}).get("movingSide") == "single_left",
f"pattern.segment_spacing should preserve move-only-left-instance semantics: {pattern_segment_single_left_prepared}",
)
pattern_segment_single_right_signature = dict(pattern_segment_signature)
pattern_segment_single_right_signature["movingSide"] = "single_right"
pattern_segment_single_right_prepared = prepare_scdm_edit_job(
step_path,
output_dir=root / "pattern-segment-spacing-single-right-prepared",
backend=backend,
capability_key="pattern.segment_spacing",
target_value="6.5",
object_id="pattern:holes-a-b-c",
object_signature=pattern_segment_single_right_signature,
)
_assert(
pattern_segment_single_right_prepared.get("ok") is True
and read_json(pattern_segment_single_right_prepared["job_path"]).get("object", {}).get("geometrySignature", {}).get("movingSide") == "single_right",
f"pattern.segment_spacing should preserve move-only-right-instance semantics: {pattern_segment_single_right_prepared}",
)
pattern_instance_signature = dict(pattern_signature["patternInstances"][1])
pattern_instance_prepared = prepare_scdm_edit_job(
step_path,
output_dir=root / "pattern-instance-position-prepared",
backend=backend,
capability_key="pattern.instance_position",
target_value=[6.0, 0.0, 0.0],
object_id="pattern:holes-a-b-c:instance-b",
object_signature=pattern_instance_signature,
)
_assert(pattern_instance_prepared.get("ok") is True, f"pattern.instance_position should prepare an edit job when instance locators exist: {pattern_instance_prepared}")
pattern_instance_job = read_json(pattern_instance_prepared["job_path"])
_assert(pattern_instance_job.get("target", {}).get("backendOperation") == "move_pattern_instance", f"pattern.instance_position should route to move_pattern_instance: {pattern_instance_job}")
_assert(pattern_instance_job.get("target", {}).get("value") == [6.0, 0.0, 0.0], f"pattern.instance_position target should be vector3: {pattern_instance_job}")
pattern_instance_missing_locator = dict(pattern_instance_signature)
pattern_instance_missing_locator.pop("scdmFaceLocators", None)
pattern_instance_missing_locator.pop("faceOrdinals", None)
pattern_instance_missing_locator.pop("globalFaceOrdinals", None)
pattern_instance_missing_locator.pop("bodyLocators", None)
pattern_instance_missing_locator.pop("componentLocators", None)
pattern_instance_missing_locator.pop("bodyIndex", None)
blocked_pattern_instance = prepare_scdm_edit_job(
step_path,
output_dir=root / "pattern-instance-position-missing-locator",
backend=backend,
capability_key="pattern.instance_position",
target_value=[6.0, 0.0, 0.0],
object_id="pattern:holes-a-b-c:instance-b",
object_signature=pattern_instance_missing_locator,
)
_assert(
blocked_pattern_instance.get("ok") is False
and blocked_pattern_instance.get("reason") == "object-signature-missing-pattern-instance-locator",
f"pattern.instance_position should be blocked before SCDM when instance locator is missing: {blocked_pattern_instance}",
)
body_pattern_signature = {
"objectType": "linear_pattern",
@@ -658,19 +871,6 @@ def main() -> int:
f"pattern segment spacing should be blocked before SCDM when it exceeds support Face limits: {blocked_segment_prepared}",
)
planned = prepare_scdm_edit_job(
step_path,
output_dir=root / "unsupported",
backend=backend,
capability_key="pattern.instance_position",
target_value="1",
object_signature=signature,
)
_assert(
planned.get("ok") is False and planned.get("reason") == "capability-not-productized",
f"planned capability should fail early with a roadmap reason: {planned}",
)
unsupported = prepare_scdm_edit_job(
step_path,
output_dir=root / "unsupported-unknown",
@@ -823,6 +1023,19 @@ def main() -> int:
_assert(chamfer_distance_success.get("ok") is True, f"fake chamfer.distance edit should succeed: {chamfer_distance_success}")
_assert(chamfer_distance_success.get("backend_operation") == "change_chamfer_distance", f"chamfer.distance should report change_chamfer_distance: {chamfer_distance_success}")
shell_thickness_success = run_scdm_edit_job(
step_path,
output_dir=root / "shell-thickness-success",
backend=backend,
capability_key="shell.thickness",
target_value=1.6,
object_id="thin_wall:101-102",
object_signature=shell_signature,
runner=_successful_runner,
)
_assert(shell_thickness_success.get("ok") is True, f"fake shell.thickness edit should succeed: {shell_thickness_success}")
_assert(shell_thickness_success.get("backend_operation") == "change_shell_thickness", f"shell.thickness should report change_shell_thickness: {shell_thickness_success}")
pattern_spacing_success = run_scdm_edit_job(
step_path,
output_dir=root / "pattern-spacing-success",
@@ -850,6 +1063,18 @@ def main() -> int:
pattern_segment_success.get("backend_operation") == "change_pattern_segment_spacing",
f"pattern.segment_spacing should report change_pattern_segment_spacing: {pattern_segment_success}",
)
pattern_instance_success = run_scdm_edit_job(
step_path,
output_dir=root / "pattern-instance-position-success",
backend=backend,
capability_key="pattern.instance_position",
target_value=[6.0, 0.0, 0.0],
object_id="pattern:holes-a-b-c:instance-b",
object_signature=pattern_instance_signature,
runner=_successful_runner,
)
_assert(pattern_instance_success.get("ok") is True, f"fake pattern.instance_position edit should succeed: {pattern_instance_success}")
_assert(pattern_instance_success.get("backend_operation") == "move_pattern_instance", f"pattern.instance_position should report move_pattern_instance: {pattern_instance_success}")
missing_output = run_scdm_edit_job(
step_path,