1109 lines
56 KiB
Python
1109 lines
56 KiB
Python
from __future__ import annotations
|
|
|
|
import ast
|
|
import re
|
|
import subprocess
|
|
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_backend import ScdmBackendInfo # noqa: E402
|
|
from step_editor.scdm_edit_runner import generate_scdm_edit_script, prepare_scdm_edit_job, run_scdm_edit_job # noqa: E402
|
|
from step_editor.scdm_schema import read_json, write_json # noqa: E402
|
|
|
|
|
|
def _assert(condition: bool, message: str) -> None:
|
|
if not condition:
|
|
raise AssertionError(message)
|
|
|
|
|
|
def _fake_spaceclaim(path: Path) -> Path:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text("fake", encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def _script_path_from_command(command: list[str] | tuple[str, ...]) -> Path:
|
|
for item in command:
|
|
if item.startswith("/RunScript="):
|
|
return Path(item.split("=", 1)[1])
|
|
raise AssertionError(f"missing /RunScript argument: {command}")
|
|
|
|
|
|
def _job_path_from_script(script_path: Path) -> Path:
|
|
text = script_path.read_text(encoding="utf-8")
|
|
match = re.search(r"^JOB_PATH = (.+)$", text, flags=re.MULTILINE)
|
|
_assert(match is not None, f"generated script should embed JOB_PATH: {script_path}")
|
|
return Path(ast.literal_eval(match.group(1)))
|
|
|
|
|
|
def _successful_runner(command, **_kwargs): # type: ignore[no-untyped-def]
|
|
script_path = _script_path_from_command(command)
|
|
job_path = _job_path_from_script(script_path)
|
|
job = read_json(job_path)
|
|
outputs = job.get("outputs")
|
|
_assert(isinstance(outputs, dict), f"job outputs should be an object: {job}")
|
|
output_step = Path(str(outputs["outputStep"]))
|
|
output_step.write_text("ISO-10303-21;\n/* fake SCDM output */\nEND-ISO-10303-21;\n", encoding="utf-8")
|
|
write_json(
|
|
outputs["result"],
|
|
{
|
|
"ok": True,
|
|
"reason": "ok",
|
|
"message": "fake edit finished",
|
|
"outputStep": str(output_step),
|
|
"backendOperation": job.get("target", {}).get("backendOperation"),
|
|
},
|
|
)
|
|
return subprocess.CompletedProcess(command, 0, stdout="ok", stderr="")
|
|
|
|
|
|
def _failure_runner(command, **_kwargs): # type: ignore[no-untyped-def]
|
|
script_path = _script_path_from_command(command)
|
|
job_path = _job_path_from_script(script_path)
|
|
job = read_json(job_path)
|
|
outputs = job.get("outputs")
|
|
_assert(isinstance(outputs, dict), f"job outputs should be an object: {job}")
|
|
write_json(
|
|
outputs["error"],
|
|
{
|
|
"ok": False,
|
|
"reason": "fake-failed",
|
|
"message": "fake SCDM command failed",
|
|
"backendOperation": job.get("target", {}).get("backendOperation"),
|
|
},
|
|
)
|
|
return subprocess.CompletedProcess(command, 7, stdout="", stderr="fake failure")
|
|
|
|
|
|
def _missing_output_runner(command, **_kwargs): # type: ignore[no-untyped-def]
|
|
script_path = _script_path_from_command(command)
|
|
job_path = _job_path_from_script(script_path)
|
|
job = read_json(job_path)
|
|
outputs = job.get("outputs")
|
|
_assert(isinstance(outputs, dict), f"job outputs should be an object: {job}")
|
|
write_json(
|
|
outputs["result"],
|
|
{
|
|
"ok": True,
|
|
"reason": "ok",
|
|
"message": "fake success without STEP",
|
|
"outputStep": str(outputs["outputStep"]),
|
|
},
|
|
)
|
|
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
|
|
|
|
|
|
def _empty_output_runner(command, **_kwargs): # type: ignore[no-untyped-def]
|
|
script_path = _script_path_from_command(command)
|
|
job_path = _job_path_from_script(script_path)
|
|
job = read_json(job_path)
|
|
outputs = job.get("outputs")
|
|
_assert(isinstance(outputs, dict), f"job outputs should be an object: {job}")
|
|
Path(str(outputs["outputStep"])).write_text("", encoding="utf-8")
|
|
write_json(
|
|
outputs["result"],
|
|
{
|
|
"ok": True,
|
|
"reason": "ok",
|
|
"message": "fake success with empty STEP",
|
|
"outputStep": str(outputs["outputStep"]),
|
|
},
|
|
)
|
|
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
|
|
|
|
|
|
def main() -> int:
|
|
with tempfile.TemporaryDirectory(prefix="step_editor_scdm_edit_") as temp:
|
|
root = Path(temp)
|
|
step_path = root / "sample.step"
|
|
step_path.write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8")
|
|
backend = ScdmBackendInfo(path=_fake_spaceclaim(root / "SpaceClaim.exe"), source="test", version="v222")
|
|
signature = {
|
|
"objectType": "hole",
|
|
"faceIds": [85, 94],
|
|
"bodyIndex": 0,
|
|
"faceOrdinal": 12,
|
|
"faceOrdinals": [12, 19],
|
|
"scdmFaceLocators": [
|
|
{"bodyIndex": 0, "faceOrdinal": 12, "globalFaceOrdinal": 85},
|
|
{"bodyIndex": 0, "faceOrdinal": 19, "globalFaceOrdinal": 96},
|
|
],
|
|
"center": [0.5, 1.0, 9.5],
|
|
"axis": [0.0, 0.0, 1.0],
|
|
}
|
|
|
|
prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "prepared",
|
|
backend=backend,
|
|
capability_key="hole.diameter",
|
|
target_value="0.75",
|
|
object_id="hole:85-94",
|
|
object_signature=signature,
|
|
timeout_seconds=45.0,
|
|
)
|
|
_assert(prepared.get("ok") is True, f"edit job should be prepared: {prepared}")
|
|
job_path = Path(str(prepared["job_path"]))
|
|
script_path = Path(str(prepared["script_path"]))
|
|
_assert(job_path.is_file(), "scdm_edit_job.json should be written")
|
|
_assert(script_path.is_file(), "scdm_edit.py should be written")
|
|
job = read_json(job_path)
|
|
_assert(job.get("adapter") == "spaceclaim-v1", f"bad adapter: {job}")
|
|
_assert(job.get("schemaVersion") == 1, f"bad schema version: {job}")
|
|
_assert(job.get("model", {}).get("sourceStep") == str(step_path.resolve(strict=False)), f"bad source path: {job}")
|
|
_assert(job.get("model", {}).get("rollbackStep") == str(step_path.resolve(strict=False)), f"bad rollback path: {job}")
|
|
_assert(job.get("target", {}).get("capabilityKey") == "hole.diameter", f"bad capability: {job}")
|
|
_assert(job.get("target", {}).get("backendOperation") == "change_hole_diameter", f"bad operation: {job}")
|
|
_assert(job.get("target", {}).get("value") == 0.75, f"target should be numeric: {job}")
|
|
_assert(job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [85, 94], f"bad signature: {job}")
|
|
_assert(job.get("execution", {}).get("isolatedProcess") is True, f"job should record isolated execution: {job}")
|
|
script = script_path.read_text(encoding="utf-8")
|
|
for token in (
|
|
"change_hole_diameter",
|
|
"move_hole_axis",
|
|
"move_slot",
|
|
"change_slot_width",
|
|
"change_slot_depth",
|
|
"move_boss",
|
|
"change_boss_height",
|
|
"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",
|
|
"StandardHoles.ModifyHoleRadius",
|
|
"ConstantRound.ModifyRadius",
|
|
"Chamfer.ModifyDistance",
|
|
"OffsetFaces.Execute",
|
|
"Move.Translate",
|
|
"MoveOptions",
|
|
"ConstantRoundOptions",
|
|
"ChamferOptions",
|
|
"OffsetFaceOptions",
|
|
"FillOptions",
|
|
"FillMode",
|
|
"Delete.Execute",
|
|
"DocumentSave",
|
|
"scdmFaceLocators",
|
|
"heightFaceLocators",
|
|
"depthFaceLocators",
|
|
"diameterFaceLocators",
|
|
"patternInstances",
|
|
"bodyLocators",
|
|
"instanceKind",
|
|
"result.json",
|
|
"error.json",
|
|
):
|
|
_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")
|
|
|
|
slot_signature = {
|
|
"objectType": "slot",
|
|
"faceIds": [30, 31, 32],
|
|
"bodyIndex": 0,
|
|
"faceOrdinal": 30,
|
|
"faceOrdinals": [30, 31, 32],
|
|
"globalFaceOrdinal": 40,
|
|
"globalFaceOrdinals": [40, 41, 42],
|
|
"width": 2.0,
|
|
"depth": 1.5,
|
|
"depthAxis": [0.0, 0.0, -1.0],
|
|
"depthFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 32, "globalFaceOrdinal": 42}],
|
|
"center": [1.0, 2.0, 3.0],
|
|
"axis": [1.0, 0.0, 0.0],
|
|
}
|
|
slot_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "slot-position-prepared",
|
|
backend=backend,
|
|
capability_key="slot.position",
|
|
target_value=[1.0, 2.0, 5.0],
|
|
object_id="slot:30-31-32",
|
|
object_signature=slot_signature,
|
|
)
|
|
_assert(slot_prepared.get("ok") is True, f"slot.position should be productized and prepare an edit job: {slot_prepared}")
|
|
slot_job = read_json(slot_prepared["job_path"])
|
|
_assert(slot_job.get("target", {}).get("backendOperation") == "move_slot", f"slot.position should route to move_slot: {slot_job}")
|
|
_assert(slot_job.get("target", {}).get("value") == [1.0, 2.0, 5.0], f"slot.position target should be vector3: {slot_job}")
|
|
_assert(slot_job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [30, 31, 32], f"slot faces should be preserved: {slot_job}")
|
|
slot_width_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "slot-width-prepared",
|
|
backend=backend,
|
|
capability_key="slot.width",
|
|
target_value="2.5",
|
|
object_id="slot:30-31-32",
|
|
object_signature=slot_signature,
|
|
)
|
|
_assert(slot_width_prepared.get("ok") is True, f"slot.width should be productized and prepare an edit job: {slot_width_prepared}")
|
|
slot_width_job = read_json(slot_width_prepared["job_path"])
|
|
_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",
|
|
backend=backend,
|
|
capability_key="slot.depth",
|
|
target_value="2.0",
|
|
object_id="slot:30-31-32",
|
|
object_signature=slot_signature,
|
|
)
|
|
_assert(slot_depth_prepared.get("ok") is True, f"slot.depth should be productized and prepare an edit job: {slot_depth_prepared}")
|
|
slot_depth_job = read_json(slot_depth_prepared["job_path"])
|
|
_assert(slot_depth_job.get("target", {}).get("backendOperation") == "change_slot_depth", f"slot.depth should route to change_slot_depth: {slot_depth_job}")
|
|
_assert(slot_depth_job.get("target", {}).get("value") == 2.0, f"slot.depth target should be numeric: {slot_depth_job}")
|
|
slot_depth_signature = slot_depth_job.get("object", {}).get("geometrySignature", {})
|
|
_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",
|
|
"faceIds": [50, 51, 52],
|
|
"bodyIndex": 0,
|
|
"faceOrdinal": 50,
|
|
"faceOrdinals": [50, 51, 52],
|
|
"globalFaceOrdinal": 70,
|
|
"globalFaceOrdinals": [70, 71, 72],
|
|
"height": 4.0,
|
|
"diameter": 3.0,
|
|
"heightFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 52, "globalFaceOrdinal": 72}],
|
|
"diameterFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 50, "globalFaceOrdinal": 70}],
|
|
"center": [0.0, 0.0, 2.0],
|
|
"axis": [0.0, 0.0, 1.0],
|
|
}
|
|
boss_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "boss-position-prepared",
|
|
backend=backend,
|
|
capability_key="boss.position",
|
|
target_value=[2.0, 0.0, 2.0],
|
|
object_id="boss:50-51-52",
|
|
object_signature=boss_signature,
|
|
)
|
|
_assert(boss_prepared.get("ok") is True, f"boss.position should be productized and prepare an edit job: {boss_prepared}")
|
|
boss_job = read_json(boss_prepared["job_path"])
|
|
_assert(boss_job.get("target", {}).get("backendOperation") == "move_boss", f"boss.position should route to move_boss: {boss_job}")
|
|
_assert(boss_job.get("target", {}).get("value") == [2.0, 0.0, 2.0], f"boss.position target should be vector3: {boss_job}")
|
|
_assert(boss_job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [50, 51, 52], f"boss faces should be preserved: {boss_job}")
|
|
boss_height_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "boss-height-prepared",
|
|
backend=backend,
|
|
capability_key="boss.height",
|
|
target_value="5.5",
|
|
object_id="boss:50-51-52",
|
|
object_signature=boss_signature,
|
|
)
|
|
_assert(boss_height_prepared.get("ok") is True, f"boss.height should be productized and prepare an edit job: {boss_height_prepared}")
|
|
boss_height_job = read_json(boss_height_prepared["job_path"])
|
|
_assert(boss_height_job.get("target", {}).get("backendOperation") == "change_boss_height", f"boss.height should route to change_boss_height: {boss_height_job}")
|
|
_assert(boss_height_job.get("target", {}).get("value") == 5.5, f"boss.height target should be numeric: {boss_height_job}")
|
|
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",
|
|
backend=backend,
|
|
capability_key="boss.diameter",
|
|
target_value="4.5",
|
|
object_id="boss:50-51-52",
|
|
object_signature=boss_signature,
|
|
)
|
|
_assert(boss_diameter_prepared.get("ok") is True, f"boss.diameter should be productized and prepare an edit job: {boss_diameter_prepared}")
|
|
boss_diameter_job = read_json(boss_diameter_prepared["job_path"])
|
|
_assert(boss_diameter_job.get("target", {}).get("backendOperation") == "change_boss_diameter", f"boss.diameter should route to change_boss_diameter: {boss_diameter_job}")
|
|
_assert(boss_diameter_job.get("target", {}).get("value") == 4.5, f"boss.diameter target should be numeric: {boss_diameter_job}")
|
|
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",
|
|
"faceIds": [60],
|
|
"bodyIndex": 0,
|
|
"faceOrdinal": 60,
|
|
"globalFaceOrdinal": 80,
|
|
"center": [1.0, 0.0, 2.0],
|
|
"axis": [0.0, 0.0, 1.0],
|
|
"radius": 0.5,
|
|
"roundType": "ConstantRound",
|
|
"isConstantRound": True,
|
|
}
|
|
round_radius_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "round-radius-prepared",
|
|
backend=backend,
|
|
capability_key="round.radius",
|
|
target_value="0.75",
|
|
object_id="round:60",
|
|
object_signature=round_signature,
|
|
)
|
|
_assert(round_radius_prepared.get("ok") is True, f"round.radius should be productized and prepare an edit job: {round_radius_prepared}")
|
|
round_radius_job = read_json(round_radius_prepared["job_path"])
|
|
_assert(round_radius_job.get("target", {}).get("backendOperation") == "change_round_radius", f"round.radius should route to change_round_radius: {round_radius_job}")
|
|
_assert(round_radius_job.get("target", {}).get("value") == 0.75, f"round.radius target should be numeric: {round_radius_job}")
|
|
round_radius_signature = round_radius_job.get("object", {}).get("geometrySignature", {})
|
|
_assert(round_radius_signature.get("isConstantRound") is True, f"round.radius should preserve constant round evidence: {round_radius_job}")
|
|
_assert(round_radius_signature.get("radius") == 0.5, f"round.radius should preserve current radius: {round_radius_job}")
|
|
round_delete_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "round-delete-prepared",
|
|
backend=backend,
|
|
capability_key="feature.delete_round_or_chamfer",
|
|
target_value="",
|
|
object_id="round:60",
|
|
object_signature=round_signature,
|
|
)
|
|
_assert(round_delete_prepared.get("ok") is True, f"round/chamfer delete should be productized and prepare an edit job: {round_delete_prepared}")
|
|
round_delete_job = read_json(round_delete_prepared["job_path"])
|
|
_assert(round_delete_job.get("target", {}).get("backendOperation") == "delete_round_or_chamfer", f"round delete should route to delete_round_or_chamfer: {round_delete_job}")
|
|
_assert(round_delete_job.get("target", {}).get("value") is True, f"command target should be true: {round_delete_job}")
|
|
|
|
chamfer_signature = {
|
|
"objectType": "chamfer",
|
|
"faceIds": [63],
|
|
"bodyIndex": 0,
|
|
"faceOrdinal": 63,
|
|
"globalFaceOrdinal": 83,
|
|
"distance": 0.8,
|
|
"distance1": 0.8,
|
|
"distance2": 0.8,
|
|
"chamferType": "EqualDistanceChamfer",
|
|
"isEqualDistanceChamfer": True,
|
|
}
|
|
chamfer_distance_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "chamfer-distance-prepared",
|
|
backend=backend,
|
|
capability_key="chamfer.distance",
|
|
target_value="1.2",
|
|
object_id="chamfer:63",
|
|
object_signature=chamfer_signature,
|
|
)
|
|
_assert(chamfer_distance_prepared.get("ok") is True, f"chamfer.distance should be productized and prepare an edit job: {chamfer_distance_prepared}")
|
|
chamfer_distance_job = read_json(chamfer_distance_prepared["job_path"])
|
|
_assert(chamfer_distance_job.get("target", {}).get("backendOperation") == "change_chamfer_distance", f"chamfer.distance should route to change_chamfer_distance: {chamfer_distance_job}")
|
|
_assert(chamfer_distance_job.get("target", {}).get("value") == 1.2, f"chamfer.distance target should be numeric: {chamfer_distance_job}")
|
|
chamfer_job_signature = chamfer_distance_job.get("object", {}).get("geometrySignature", {})
|
|
_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],
|
|
"bodyIndex": 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]],
|
|
"patternInstances": [
|
|
{
|
|
"sourceObjectId": "hole:a",
|
|
"center": [0.0, 0.0, 0.0],
|
|
"faceIds": [85, 94],
|
|
"scdmFaceLocators": [
|
|
{"bodyIndex": 0, "faceOrdinal": 12, "globalFaceOrdinal": 85},
|
|
{"bodyIndex": 0, "faceOrdinal": 19, "globalFaceOrdinal": 94},
|
|
],
|
|
},
|
|
{
|
|
"sourceObjectId": "hole:b",
|
|
"center": [5.0, 0.0, 0.0],
|
|
"faceIds": [87, 96],
|
|
"scdmFaceLocators": [
|
|
{"bodyIndex": 0, "faceOrdinal": 22, "globalFaceOrdinal": 87},
|
|
{"bodyIndex": 0, "faceOrdinal": 29, "globalFaceOrdinal": 96},
|
|
],
|
|
},
|
|
{
|
|
"sourceObjectId": "hole:c",
|
|
"center": [10.0, 0.0, 0.0],
|
|
"faceIds": [89, 98],
|
|
"scdmFaceLocators": [
|
|
{"bodyIndex": 0, "faceOrdinal": 32, "globalFaceOrdinal": 89},
|
|
{"bodyIndex": 0, "faceOrdinal": 39, "globalFaceOrdinal": 98},
|
|
],
|
|
},
|
|
],
|
|
}
|
|
pattern_spacing_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "pattern-spacing-prepared",
|
|
backend=backend,
|
|
capability_key="pattern.spacing",
|
|
target_value="7.5",
|
|
object_id="pattern:holes-a-b-c",
|
|
object_signature=pattern_signature,
|
|
)
|
|
_assert(pattern_spacing_prepared.get("ok") is True, f"pattern.spacing should be productized and prepare an edit job: {pattern_spacing_prepared}")
|
|
pattern_spacing_job = read_json(pattern_spacing_prepared["job_path"])
|
|
_assert(pattern_spacing_job.get("target", {}).get("backendOperation") == "change_pattern_spacing", f"pattern.spacing should route to change_pattern_spacing: {pattern_spacing_job}")
|
|
_assert(pattern_spacing_job.get("target", {}).get("value") == 7.5, f"pattern.spacing target should be numeric: {pattern_spacing_job}")
|
|
pattern_job_signature = pattern_spacing_job.get("object", {}).get("geometrySignature", {})
|
|
_assert(pattern_job_signature.get("spacing") == 5.0, f"pattern spacing signature should be preserved: {pattern_spacing_job}")
|
|
_assert(len(pattern_job_signature.get("patternInstances") or []) == 3, f"pattern instance locators should be preserved: {pattern_spacing_job}")
|
|
pattern_segment_signature = dict(pattern_signature)
|
|
pattern_segment_signature["segmentIndex"] = 1
|
|
pattern_segment_signature["movingSide"] = "after"
|
|
pattern_segment_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "pattern-segment-spacing-prepared",
|
|
backend=backend,
|
|
capability_key="pattern.segment_spacing",
|
|
target_value="6.5",
|
|
object_id="pattern:holes-a-b-c",
|
|
object_signature=pattern_segment_signature,
|
|
)
|
|
_assert(pattern_segment_prepared.get("ok") is True, f"pattern.segment_spacing should prepare an edit job: {pattern_segment_prepared}")
|
|
pattern_segment_job = read_json(pattern_segment_prepared["job_path"])
|
|
_assert(
|
|
pattern_segment_job.get("target", {}).get("backendOperation") == "change_pattern_segment_spacing",
|
|
f"pattern.segment_spacing should route to change_pattern_segment_spacing: {pattern_segment_job}",
|
|
)
|
|
_assert(pattern_segment_job.get("target", {}).get("value") == 6.5, f"pattern.segment_spacing target should be numeric: {pattern_segment_job}")
|
|
_assert(
|
|
pattern_segment_job.get("object", {}).get("geometrySignature", {}).get("segmentIndex") == 1,
|
|
f"pattern.segment_spacing should preserve the selected adjacent segment: {pattern_segment_job}",
|
|
)
|
|
pattern_segment_before_signature = dict(pattern_segment_signature)
|
|
pattern_segment_before_signature["movingSide"] = "before"
|
|
pattern_segment_before_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "pattern-segment-spacing-before-prepared",
|
|
backend=backend,
|
|
capability_key="pattern.segment_spacing",
|
|
target_value="6.5",
|
|
object_id="pattern:holes-a-b-c",
|
|
object_signature=pattern_segment_before_signature,
|
|
)
|
|
_assert(
|
|
pattern_segment_before_prepared.get("ok") is True
|
|
and read_json(pattern_segment_before_prepared["job_path"]).get("object", {}).get("geometrySignature", {}).get("movingSide") == "before",
|
|
f"pattern.segment_spacing should preserve fix-right/move-left semantics: {pattern_segment_before_prepared}",
|
|
)
|
|
pattern_segment_split_signature = dict(pattern_segment_signature)
|
|
pattern_segment_split_signature["movingSide"] = "split"
|
|
pattern_segment_split_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "pattern-segment-spacing-split-prepared",
|
|
backend=backend,
|
|
capability_key="pattern.segment_spacing",
|
|
target_value="6.5",
|
|
object_id="pattern:holes-a-b-c",
|
|
object_signature=pattern_segment_split_signature,
|
|
)
|
|
_assert(
|
|
pattern_segment_split_prepared.get("ok") is True
|
|
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",
|
|
"patternKind": "body",
|
|
"instanceKind": "body",
|
|
"faceIds": [300, 301, 302],
|
|
"bodyIndices": [20, 21, 22],
|
|
"axis": [0.0, 1.0, 0.0],
|
|
"spacing": 5.0,
|
|
"pitch": 5.0,
|
|
"instanceCount": 3,
|
|
"instanceCenters": [[0.0, 20.0, 0.0], [0.0, 25.0, 0.0], [0.0, 30.0, 0.0]],
|
|
"patternInstances": [
|
|
{"sourceObjectId": "body:20", "instanceKind": "body", "center": [0.0, 20.0, 0.0], "bodyIndex": 20, "bodyLocators": [{"bodyIndex": 20}], "faceIds": [300]},
|
|
{"sourceObjectId": "body:21", "instanceKind": "body", "center": [0.0, 25.0, 0.0], "bodyIndex": 21, "bodyLocators": [{"bodyIndex": 21}], "faceIds": [301]},
|
|
{"sourceObjectId": "body:22", "instanceKind": "body", "center": [0.0, 30.0, 0.0], "bodyIndex": 22, "bodyLocators": [{"bodyIndex": 22}], "faceIds": [302]},
|
|
],
|
|
}
|
|
body_pattern_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "body-pattern-spacing-prepared",
|
|
backend=backend,
|
|
capability_key="pattern.spacing",
|
|
target_value="8",
|
|
object_id="pattern:parts-20-22",
|
|
object_signature=body_pattern_signature,
|
|
)
|
|
_assert(
|
|
body_pattern_prepared.get("ok") is False
|
|
and body_pattern_prepared.get("reason") == "body-pattern-spacing-missing-component-locators",
|
|
f"body pattern spacing should be blocked until component occurrence locators are available: {body_pattern_prepared}",
|
|
)
|
|
body_segment_pattern_signature = dict(body_pattern_signature)
|
|
body_segment_pattern_signature["segmentIndex"] = 0
|
|
body_segment_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "body-pattern-segment-spacing-prepared",
|
|
backend=backend,
|
|
capability_key="pattern.segment_spacing",
|
|
target_value="6",
|
|
object_id="pattern:parts-20-22",
|
|
object_signature=body_segment_pattern_signature,
|
|
)
|
|
_assert(
|
|
body_segment_prepared.get("ok") is False
|
|
and body_segment_prepared.get("reason") == "body-pattern-spacing-missing-component-locators",
|
|
f"body pattern local segment spacing should also require component occurrence locators: {body_segment_prepared}",
|
|
)
|
|
component_body_pattern_signature = dict(body_pattern_signature)
|
|
component_body_pattern_signature["componentInstanceCount"] = 3
|
|
component_body_pattern_signature["patternInstances"] = [
|
|
{
|
|
**dict(item),
|
|
"componentLocators": [
|
|
{
|
|
"componentIndex": index,
|
|
"componentPath": [index],
|
|
"componentBodyIndex": 0,
|
|
"bodyIndex": item.get("bodyIndex"),
|
|
"componentName": f"Part {index + 1}",
|
|
}
|
|
],
|
|
}
|
|
for index, item in enumerate(body_pattern_signature["patternInstances"])
|
|
]
|
|
component_body_pattern_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "component-body-pattern-spacing-prepared",
|
|
backend=backend,
|
|
capability_key="pattern.spacing",
|
|
target_value="8",
|
|
object_id="pattern:components-20-22",
|
|
object_signature=component_body_pattern_signature,
|
|
)
|
|
_assert(
|
|
component_body_pattern_prepared.get("ok") is True,
|
|
f"body pattern spacing should prepare when every member has a component occurrence locator: {component_body_pattern_prepared}",
|
|
)
|
|
component_body_pattern_job = read_json(component_body_pattern_prepared["job_path"])
|
|
component_instances = component_body_pattern_job.get("object", {}).get("geometrySignature", {}).get("patternInstances") or []
|
|
_assert(
|
|
all(item.get("componentLocators") for item in component_instances if isinstance(item, dict)),
|
|
f"component occurrence locators should be preserved in pattern.spacing job: {component_body_pattern_job}",
|
|
)
|
|
component_body_segment_signature = dict(component_body_pattern_signature)
|
|
component_body_segment_signature["segmentIndex"] = 0
|
|
component_body_segment_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "component-body-pattern-segment-spacing-prepared",
|
|
backend=backend,
|
|
capability_key="pattern.segment_spacing",
|
|
target_value="6",
|
|
object_id="pattern:components-20-22",
|
|
object_signature=component_body_segment_signature,
|
|
)
|
|
_assert(
|
|
component_body_segment_prepared.get("ok") is True,
|
|
f"body pattern local segment spacing should prepare with component occurrence locators: {component_body_segment_prepared}",
|
|
)
|
|
blocked_pattern_signature = dict(pattern_signature)
|
|
blocked_pattern_signature["supportPatternFit"] = {
|
|
"supportFaceIds": [92],
|
|
"localUnitScale": 0.001,
|
|
"maxSpacing": 0.0012,
|
|
"maxSpacingLocal": 1.2,
|
|
"instanceCount": 3,
|
|
}
|
|
blocked_pattern_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "body-pattern-spacing-blocked",
|
|
backend=backend,
|
|
capability_key="pattern.spacing",
|
|
target_value=0.002,
|
|
object_id="pattern:holes-a-b-c",
|
|
object_signature=blocked_pattern_signature,
|
|
)
|
|
_assert(
|
|
blocked_pattern_prepared.get("ok") is False
|
|
and blocked_pattern_prepared.get("reason") == "pattern-spacing-exceeds-support",
|
|
f"pattern spacing should be blocked before SCDM when it exceeds support Face limits: {blocked_pattern_prepared}",
|
|
)
|
|
blocked_segment_signature = dict(pattern_segment_signature)
|
|
blocked_segment_signature["supportPatternFit"] = {
|
|
"supportFaceIds": [92],
|
|
"localUnitScale": 0.001,
|
|
"maxSegmentSpacing": 0.0013,
|
|
"maxSegmentSpacingLocal": 1.3,
|
|
"instanceCount": 3,
|
|
}
|
|
blocked_segment_prepared = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "pattern-segment-spacing-blocked",
|
|
backend=backend,
|
|
capability_key="pattern.segment_spacing",
|
|
target_value=0.002,
|
|
object_id="pattern:holes-a-b-c",
|
|
object_signature=blocked_segment_signature,
|
|
)
|
|
_assert(
|
|
blocked_segment_prepared.get("ok") is False
|
|
and blocked_segment_prepared.get("reason") == "pattern-spacing-exceeds-support",
|
|
f"pattern segment spacing should be blocked before SCDM when it exceeds support Face limits: {blocked_segment_prepared}",
|
|
)
|
|
|
|
unsupported = prepare_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "unsupported-unknown",
|
|
backend=backend,
|
|
capability_key="not.real",
|
|
target_value="1",
|
|
object_signature=signature,
|
|
)
|
|
_assert(unsupported.get("ok") is False and unsupported.get("reason") == "unsupported-capability", f"unknown capability should fail early: {unsupported}")
|
|
|
|
success = run_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "success",
|
|
backend=backend,
|
|
capability_key="hole.diameter",
|
|
target_value=0.9,
|
|
object_id="hole:85-94",
|
|
object_signature=signature,
|
|
runner=_successful_runner,
|
|
)
|
|
_assert(success.get("ok") is True, f"fake edit should succeed: {success}")
|
|
_assert(Path(str(success["output_step"])).is_file(), f"output STEP should exist: {success}")
|
|
success_backend = success.get("backend")
|
|
_assert(isinstance(success_backend, dict), f"successful edit should carry backend status: {success}")
|
|
_assert(success_backend.get("runScriptOk") is True, f"successful edit should mark /RunScript usable: {success_backend}")
|
|
_assert(success_backend.get("licenseOk") is True, f"successful edit should mark license usable: {success_backend}")
|
|
|
|
failed = run_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "failed",
|
|
backend=backend,
|
|
capability_key="hole.position",
|
|
target_value=[0.5, 1.0, 6.0],
|
|
object_id="hole:85-94",
|
|
object_signature=signature,
|
|
runner=_failure_runner,
|
|
)
|
|
_assert(failed.get("ok") is False and failed.get("reason") == "fake-failed", f"error.json should drive failure reason: {failed}")
|
|
|
|
slot_success = run_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "slot-success",
|
|
backend=backend,
|
|
capability_key="slot.position",
|
|
target_value=[1.0, 2.0, 6.0],
|
|
object_id="slot:30-31-32",
|
|
object_signature=slot_signature,
|
|
runner=_successful_runner,
|
|
)
|
|
_assert(slot_success.get("ok") is True, f"fake slot.position edit should succeed: {slot_success}")
|
|
_assert(slot_success.get("backend_operation") == "move_slot", f"slot.position should report move_slot: {slot_success}")
|
|
slot_width_success = run_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "slot-width-success",
|
|
backend=backend,
|
|
capability_key="slot.width",
|
|
target_value=2.5,
|
|
object_id="slot:30-31-32",
|
|
object_signature=slot_signature,
|
|
runner=_successful_runner,
|
|
)
|
|
_assert(slot_width_success.get("ok") is True, f"fake slot.width edit should succeed: {slot_width_success}")
|
|
_assert(slot_width_success.get("backend_operation") == "change_slot_width", f"slot.width should report change_slot_width: {slot_width_success}")
|
|
slot_depth_success = run_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "slot-depth-success",
|
|
backend=backend,
|
|
capability_key="slot.depth",
|
|
target_value=2.0,
|
|
object_id="slot:30-31-32",
|
|
object_signature=slot_signature,
|
|
runner=_successful_runner,
|
|
)
|
|
_assert(slot_depth_success.get("ok") is True, f"fake slot.depth edit should succeed: {slot_depth_success}")
|
|
_assert(slot_depth_success.get("backend_operation") == "change_slot_depth", f"slot.depth should report change_slot_depth: {slot_depth_success}")
|
|
|
|
boss_success = run_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "boss-success",
|
|
backend=backend,
|
|
capability_key="boss.position",
|
|
target_value=[3.0, 0.0, 2.0],
|
|
object_id="boss:50-51-52",
|
|
object_signature=boss_signature,
|
|
runner=_successful_runner,
|
|
)
|
|
_assert(boss_success.get("ok") is True, f"fake boss.position edit should succeed: {boss_success}")
|
|
_assert(boss_success.get("backend_operation") == "move_boss", f"boss.position should report move_boss: {boss_success}")
|
|
boss_height_success = run_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "boss-height-success",
|
|
backend=backend,
|
|
capability_key="boss.height",
|
|
target_value=5.5,
|
|
object_id="boss:50-51-52",
|
|
object_signature=boss_signature,
|
|
runner=_successful_runner,
|
|
)
|
|
_assert(boss_height_success.get("ok") is True, f"fake boss.height edit should succeed: {boss_height_success}")
|
|
_assert(boss_height_success.get("backend_operation") == "change_boss_height", f"boss.height should report change_boss_height: {boss_height_success}")
|
|
boss_diameter_success = run_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "boss-diameter-success",
|
|
backend=backend,
|
|
capability_key="boss.diameter",
|
|
target_value=4.5,
|
|
object_id="boss:50-51-52",
|
|
object_signature=boss_signature,
|
|
runner=_successful_runner,
|
|
)
|
|
_assert(boss_diameter_success.get("ok") is True, f"fake boss.diameter edit should succeed: {boss_diameter_success}")
|
|
_assert(boss_diameter_success.get("backend_operation") == "change_boss_diameter", f"boss.diameter should report change_boss_diameter: {boss_diameter_success}")
|
|
|
|
round_radius_success = run_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "round-radius-success",
|
|
backend=backend,
|
|
capability_key="round.radius",
|
|
target_value=0.75,
|
|
object_id="round:60",
|
|
object_signature=round_signature,
|
|
runner=_successful_runner,
|
|
)
|
|
_assert(round_radius_success.get("ok") is True, f"fake round.radius edit should succeed: {round_radius_success}")
|
|
_assert(round_radius_success.get("backend_operation") == "change_round_radius", f"round.radius should report change_round_radius: {round_radius_success}")
|
|
|
|
round_delete_success = run_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "round-delete-success",
|
|
backend=backend,
|
|
capability_key="feature.delete_round_or_chamfer",
|
|
target_value="",
|
|
object_id="round:60",
|
|
object_signature=round_signature,
|
|
runner=_successful_runner,
|
|
)
|
|
_assert(round_delete_success.get("ok") is True, f"fake round/chamfer delete edit should succeed: {round_delete_success}")
|
|
_assert(round_delete_success.get("backend_operation") == "delete_round_or_chamfer", f"round/chamfer delete should report delete_round_or_chamfer: {round_delete_success}")
|
|
|
|
chamfer_distance_success = run_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "chamfer-distance-success",
|
|
backend=backend,
|
|
capability_key="chamfer.distance",
|
|
target_value=1.2,
|
|
object_id="chamfer:63",
|
|
object_signature=chamfer_signature,
|
|
runner=_successful_runner,
|
|
)
|
|
_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",
|
|
backend=backend,
|
|
capability_key="pattern.spacing",
|
|
target_value=7.5,
|
|
object_id="pattern:holes-a-b-c",
|
|
object_signature=pattern_signature,
|
|
runner=_successful_runner,
|
|
)
|
|
_assert(pattern_spacing_success.get("ok") is True, f"fake pattern.spacing edit should succeed: {pattern_spacing_success}")
|
|
_assert(pattern_spacing_success.get("backend_operation") == "change_pattern_spacing", f"pattern.spacing should report change_pattern_spacing: {pattern_spacing_success}")
|
|
pattern_segment_success = run_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "pattern-segment-spacing-success",
|
|
backend=backend,
|
|
capability_key="pattern.segment_spacing",
|
|
target_value=6.5,
|
|
object_id="pattern:holes-a-b-c",
|
|
object_signature=pattern_segment_signature,
|
|
runner=_successful_runner,
|
|
)
|
|
_assert(pattern_segment_success.get("ok") is True, f"fake pattern.segment_spacing edit should succeed: {pattern_segment_success}")
|
|
_assert(
|
|
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,
|
|
output_dir=root / "missing-output",
|
|
backend=backend,
|
|
capability_key="face.offset",
|
|
target_value=5,
|
|
object_id="face:9",
|
|
object_signature={"objectType": "face", "faceIds": [9], "bodyIndex": 0, "faceOrdinal": 3},
|
|
runner=_missing_output_runner,
|
|
)
|
|
_assert(missing_output.get("ok") is False and missing_output.get("reason") == "missing-output-step", f"missing output STEP should be rejected: {missing_output}")
|
|
|
|
empty_output = run_scdm_edit_job(
|
|
step_path,
|
|
output_dir=root / "empty-output",
|
|
backend=backend,
|
|
capability_key="face.offset",
|
|
target_value=5,
|
|
object_id="face:9",
|
|
object_signature={"objectType": "face", "faceIds": [9], "bodyIndex": 0, "faceOrdinal": 3},
|
|
runner=_empty_output_runner,
|
|
)
|
|
_assert(empty_output.get("ok") is False and empty_output.get("reason") == "empty-output-step", f"empty output STEP should be rejected: {empty_output}")
|
|
|
|
print("scdm edit runner ok")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|