app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
#!/usr/bin/env python3
"""dsl-live-check.py - prove the 0.10.x modelling ops on REAL SOLIDWORKS.
Every check builds a scratch part through swx_run_script, reads the result back with
swx_inspect_bodies / swx_mass_properties, and compares it with geometry computed HERE,
independently of the bridge and of its simulator (volumes from closed-form formulas,
bounding boxes from the documented conventions). Nothing is trusted that was not measured.
python3 tools/dsl-live-check.py [--target adom_drew2] [--only T05,T12] [--keep]
It needs SOLIDWORKS running and ready on the target (swx_status: solidworks.ready). It only
ever touches documents it creates (new_part / swx_new_document), and closes them with
swx_reset_workspace after every check unless --keep is given. The report (every reply,
every expectation, pass/fail) is written to dsl-live-report.json next to where it runs.
Covers wiki issue adom/solidworks-bridge#1 (offset/face sketches, cut direction, multi-profile
sketches, appearance, conventions, fillet/chamfer/shell/patterns) and #2 (per-body mass,
run_script ownership), plus the LEGO 2x4 acceptance brick (examples/lego-2x4.json).
"""
import argparse, json, math, os, subprocess, sys, time
PI = math.pi
TARGET = "adom_drew2"
THREAD = "SolidWorks Bridge"
REUSE = {"path": None} # --reuse: one bridge-made part, cleared between checks (clear_part, 0.10.6+)
# Checks that keep their own fresh part even with --reuse: they set a material or colour that
# would survive clear_part and contaminate later checks, or they assert feature names, whose
# numbering after a clear is unmeasured.
FRESH_ALWAYS = {"T15", "T16", "T22", "T24"}
REL_TOL = 2e-3 # volumes: 0.2 % (SOLIDWORKS mass properties are far tighter than this)
MM_TOL = 0.01 # bounding boxes: 0.01 mm
def ascii_reason(args):
"""MEASURED 2026-10-13 (ab 2.1.174): a non-ASCII character in `reason` makes ab fail the call
before it reaches the bridge (adom/adom-bridge issue on header encoding). Keep reasons ASCII."""
if isinstance(args, dict) and isinstance(args.get("reason"), str):
args = dict(args, reason=args["reason"].encode("ascii", "replace").decode("ascii"))
return args
def call(verb, args, timeout=240):
"""One bridge verb through adom-bridge; returns the reply dict (unwrapped)."""
args = ascii_reason(args)
try:
r = subprocess.run(["adom-bridge", "--target", TARGET, "--ai-thread", THREAD, verb, json.dumps(args)],
capture_output=True, text=True, timeout=timeout + 60)
except subprocess.TimeoutExpired:
return {"status": "error", "errorCode": "no_answer", "error": f"{verb} did not return in {timeout + 60}s"}
t = r.stdout
i = t.find("{")
if i < 0:
return {"status": "error", "errorCode": "not_json", "error": (t + r.stderr)[:400]}
try:
d = json.loads(t[i:])
except ValueError:
return {"status": "error", "errorCode": "not_json", "error": t[i:i + 400]}
return d["result"] if isinstance(d, dict) and isinstance(d.get("result"), dict) else d
def ok(reply):
return reply.get("status") == "ok" and reply.get("success", True) is not False and not reply.get("errorCode")
# ── the geometry, computed here ──────────────────────────────────────────────
def box(x0, x1, y0, y1, z0, z1):
return {"min": [x0, y0, z0], "max": [x1, y1, z1]}
def rounded_box_volume(a, b, c, r):
"""A box a x b x c with all 12 edges filleted r (spherical corners) = the Minkowski sum
of the inner box with a sphere of radius r."""
ai, bi, ci = a - 2 * r, b - 2 * r, c - 2 * r
return ai * bi * ci + 2 * r * (ai * bi + bi * ci + ai * ci) + PI * r * r * (ai + bi + ci) + 4.0 / 3.0 * PI * r ** 3
def fillet_cylinder_volume(d, h, r):
"""A cylinder d x h with both circular edges filleted r."""
R = d / 2.0
area = (1 - PI / 4) * r * r
xbar = r * (5.0 / 6.0 - PI / 4) / (1 - PI / 4)
return PI * R * R * h - 2 * area * 2 * PI * (R - xbar)
def cyl(d, h):
return PI * d * d / 4.0 * h
# The base block every "add a feature" check starts from: 20 x 10 x 20 mm, centred on Top Plane,
# extruded +Y: X -10..10, Y 0..10, Z -10..10, volume 4000.
BASE = [
{"op": "new_part"},
{"op": "sketch_rect", "plane": "Top Plane", "widthMm": 20, "heightMm": 20, "anchor": "center"},
{"op": "extrude", "depthMm": 10},
]
BASE_BOX = box(-10, 10, 0, 10, -10, 10)
def checks():
"""(id, title, issue, script, expectations). Expectation keys:
volume (mm^3), bbox, bodyCount, faceCount, appearance [r,g,b], massKg, densityKgM3,
features [names], expectError (the run must fail at that step index, never succeed silently)."""
lego = json.load(open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "examples", "lego-2x4.json")))
lego_volume = (31.8 * 15.8 * 9.6) - (29.4 * 13.4 * 8.6) + 3 * (cyl(6.51, 8.6) - cyl(4.8, 8.6)) + 8 * cyl(4.8, 1.7)
return [
("T01", "rectangle anchor: xMm/yMm is the CORNER by default", "#1.5",
[{"op": "new_part"}, {"op": "sketch_rect", "plane": "Front Plane", "xMm": 0, "yMm": 0, "widthMm": 10, "heightMm": 20},
{"op": "extrude", "depthMm": 5}],
{"volume": 1000.0, "bbox": box(0, 10, 0, 20, 0, 5)}),
("T02", "Top Plane: sketch +Y is model -Z", "#1.5",
[{"op": "new_part"}, {"op": "sketch_rect", "plane": "Top Plane", "xMm": 0, "yMm": 0, "widthMm": 10, "heightMm": 20},
{"op": "extrude", "depthMm": 5}],
{"volume": 1000.0, "bbox": box(0, 10, 0, 5, -20, 0)}),
("T03", "anchor center", "#1.5",
[{"op": "new_part"}, {"op": "sketch_rect", "plane": "Front Plane", "widthMm": 10, "heightMm": 20, "anchor": "center"},
{"op": "extrude", "depthMm": 5}],
{"volume": 1000.0, "bbox": box(-5, 5, -10, 10, 0, 5)}),
("T04", "sketch on a plane offset +10 mm from Top Plane", "#1.1",
BASE + [{"op": "sketch", "plane": {"offsetFrom": "Top Plane", "offsetMm": 10}, "entities": [{"circle": {"centerMm": [0, 0], "diameterMm": 10}}]},
{"op": "extrude", "depthMm": 5}],
{"volume": 4000 + cyl(10, 5), "bbox": box(-10, 10, 0, 15, -10, 10)}),
("T05", "sketch on a NEGATIVE offset plane (-5 mm)", "#1.1",
BASE + [{"op": "sketch", "plane": {"offsetFrom": "Top Plane", "offsetMm": -5}, "entities": [{"rect": {"widthMm": 20, "heightMm": 20, "anchor": "center"}}]},
{"op": "extrude", "depthMm": 5}],
{"volume": 6000.0, "bbox": box(-10, 10, -5, 10, -10, 10)}),
("T06", "sketch on a FACE picked by a point (the block's top face)", "#1.1",
BASE + [{"op": "sketch", "face": {"atPointMm": [0, 10, 0]}, "entities": [{"circle": {"centerMm": [0, 0], "diameterMm": 6}}]},
{"op": "extrude", "depthMm": 4}],
{"volume": 4000 + cyl(6, 4), "bbox": box(-10, 10, 0, 14, -10, 10)}),
("T07", "Michael's case: a default cut from Top Plane with the body on +Y must FAIL LOUDLY, never succeed silently", "#1.2",
BASE + [{"op": "sketch_rect", "plane": "Top Plane", "widthMm": 10, "heightMm": 10, "anchor": "center"},
{"op": "cut", "depthMm": 5}],
{"expectError": 4, "orVolumeChanged": 4000.0}),
("T08", "cut reverse:true removes material on +Y", "#1.2",
BASE + [{"op": "sketch_rect", "plane": "Top Plane", "widthMm": 10, "heightMm": 10, "anchor": "center"},
{"op": "cut", "depthMm": 5, "reverse": True}],
{"volume": 3500.0, "bbox": BASE_BOX}),
("T09", "cut throughAll + reverse", "#1.2",
BASE + [{"op": "sketch_rect", "plane": "Top Plane", "widthMm": 10, "heightMm": 10, "anchor": "center"},
{"op": "cut", "throughAll": True, "reverse": True}],
{"volume": 3000.0, "bbox": BASE_BOX}),
("T10", "extrude direction both (5 mm one way, 3 mm the other)", "#1.2",
[{"op": "new_part"}, {"op": "sketch_rect", "plane": "Front Plane", "widthMm": 10, "heightMm": 10, "anchor": "center"},
{"op": "extrude", "depthMm": 5, "direction": "both", "depth2Mm": 3}],
{"volume": 800.0, "bboxZSpan": 8.0, "bboxXY": [[-5, 5], [-5, 5]]}),
("T11", "extrude midPlane", "#1.2",
[{"op": "new_part"}, {"op": "sketch_rect", "plane": "Front Plane", "widthMm": 10, "heightMm": 10, "anchor": "center"},
{"op": "extrude", "depthMm": 10, "midPlane": True}],
{"volume": 1000.0, "bbox": box(-5, 5, -5, 5, -5, 5)}),
("T12", "cut midPlane through the block", "#1.2",
BASE + [{"op": "sketch_rect", "plane": "Front Plane", "widthMm": 4, "heightMm": 4, "anchor": "center", "yMm": 5},
{"op": "cut", "depthMm": 6, "midPlane": True}],
{"volume": 4000 - 96.0, "bbox": BASE_BOX}),
("T13", "two concentric circles in ONE sketch extrude as a RING", "#1.3",
[{"op": "new_part"}, {"op": "sketch", "plane": "Top Plane", "entities": [{"circle": {"centerMm": [0, 0], "diameterMm": 10}}, {"circle": {"centerMm": [0, 0], "diameterMm": 6}}]},
{"op": "extrude", "depthMm": 10}],
{"volume": cyl(10, 10) - cyl(6, 10), "faceCount": 4, "bodyCount": 1}),
("T14", "two disjoint rectangles in ONE sketch: one feature, two bodies", "#1.3",
[{"op": "new_part"}, {"op": "sketch", "plane": "Front Plane", "entities": [{"rect": {"xMm": 0, "yMm": 0, "widthMm": 5, "heightMm": 5}}, {"rect": {"xMm": 10, "yMm": 0, "widthMm": 5, "heightMm": 5}}]},
{"op": "extrude", "depthMm": 5}],
{"volume": 250.0, "bodyCount": 2}),
("T15", "set_appearance on the part", "#1.4",
BASE + [{"op": "set_appearance", "rgb": [200, 0, 0]}],
{"volume": 4000.0, "appearance": [200, 0, 0]}),
("T16", "set_appearance on one face picked by a point", "#1.4",
BASE + [{"op": "set_appearance", "rgb": [0, 0, 255], "target": {"faceAtPointMm": [0, 10, 0]}}],
{"volume": 4000.0, "stepReadBack": 3}),
("T17", "fillet every edge of the block, r = 1", "#1.6",
BASE + [{"op": "fillet", "radiusMm": 1, "edges": {"allOfFeature": "Boss-Extrude1"}}],
{"volume": rounded_box_volume(20, 10, 20, 1), "bbox": BASE_BOX}),
("T18", "chamfer ONE edge picked by a point, 1 mm", "#1.6",
BASE + [{"op": "chamfer", "distanceMm": 1, "edges": {"atPointsMm": [[0, 10, 10]]}}],
{"volume": 4000 - 0.5 * 1 * 1 * 20, "bbox": BASE_BOX}),
("T19", "shell 1 mm, top face removed", "#1.6",
BASE + [{"op": "shell", "thicknessMm": 1, "removeFacesAtPointsMm": [[0, 10, 0]]}],
{"volume": 4000 - 18 * 18 * 9, "bbox": BASE_BOX}),
("T20", "linear pattern: one boss -> 3 along X, 6 mm apart", "#1.6",
BASE + [{"op": "sketch", "face": {"atPointMm": [0, 10, 0]}, "entities": [{"circle": {"centerMm": [-6, 0], "diameterMm": 4}}]},
{"op": "extrude", "depthMm": 5},
{"op": "linear_pattern", "feature": "Boss-Extrude2", "directions": [{"axis": "x", "count": 3, "spacingMm": 6}]}],
{"volume": 4000 + 3 * cyl(4, 5), "bbox": box(-10, 10, 0, 15, -10, 10)}),
("T21", "circular pattern: one boss -> 6 around Y", "#1.6",
BASE + [{"op": "sketch", "face": {"atPointMm": [0, 10, 0]}, "entities": [{"circle": {"centerMm": [7, 0], "diameterMm": 3}}]},
{"op": "extrude", "depthMm": 4},
{"op": "circular_pattern", "feature": "Boss-Extrude2", "axis": "y", "count": 6}],
{"volume": 4000 + 6 * cyl(3, 4)}),
("T22", "issue #2: body mass uses the material's density (ABS 1020 kg/m^3)", "#2.a",
BASE + [{"op": "set_material", "name": "ABS"}],
{"volume": 4000.0, "massKg": 4000e-9 * 1020, "densityKgM3": 1020.0, "massMatchesMassProperties": True}),
("T25", "audit #1/#5: sketch on the BOTTOM face (hidden in an iso view, normal -Y) extrudes OUTWARD", "#1.1",
BASE + [{"op": "sketch", "face": {"atPointMm": [0, 0, 0]}, "entities": [{"circle": {"centerMm": [0, 0], "diameterMm": 6}}]},
{"op": "extrude", "depthMm": 3}],
{"volume": 4000 + cyl(6, 3), "bbox": box(-10, 10, -3, 10, -10, 10)}),
("T26", "audit unverified #1: a NEGATIVE offset plane keeps the base plane's axes (asymmetric sketch, separate body)", "#1.1",
BASE + [{"op": "sketch", "plane": {"offsetFrom": "Top Plane", "offsetMm": -5}, "entities": [{"rect": {"xMm": 0, "yMm": 0, "widthMm": 4, "heightMm": 6}}]},
{"op": "extrude", "depthMm": 2}],
{"volume": 4000 + 48.0, "bodyCount": 2, "bodyBoxes": [BASE_BOX, box(0, 4, -5, -3, -6, 0)]}),
("T27", "audit #6: an ASYMMETRIC two-way cut: depthMm goes along the cut's own direction (-normal), depth2Mm the other way", "#1.2",
[{"op": "new_part"}, {"op": "sketch_rect", "plane": "Front Plane", "widthMm": 10, "heightMm": 10, "anchor": "center"},
{"op": "extrude", "depthMm": 10},
{"op": "sketch_rect", "plane": "Front Plane", "widthMm": 4, "heightMm": 4, "anchor": "center"},
{"op": "cut", "depthMm": 6, "direction": "both", "depth2Mm": 2}],
{"volume": 1000 - 4 * 4 * 2, "bbox": box(-5, 5, -5, 5, 0, 10)}),
("T28", "audit unverified #2: linear pattern with NEGATIVE spacing goes -X (instances stay on the block)", "#1.6",
BASE + [{"op": "sketch", "face": {"atPointMm": [0, 10, 0]}, "entities": [{"circle": {"centerMm": [6, 0], "diameterMm": 4}}]},
{"op": "extrude", "depthMm": 5},
{"op": "linear_pattern", "feature": "Boss-Extrude2", "directions": [{"axis": "x", "count": 3, "spacingMm": -6}]}],
{"volume": 4000 + 3 * cyl(4, 5), "bodyCount": 1, "bbox": box(-10, 10, 0, 15, -10, 10)}),
("T29", "audit #1: shell removing the BOTTOM face (hidden in an iso view) removes that face, not a side", "#1.6",
BASE + [{"op": "shell", "thicknessMm": 1, "removeFacesAtPointsMm": [[0, 0, 0]]}],
{"volume": 4000 - 18 * 18 * 9, "bbox": BASE_BOX}),
("T30", "audit #3: fillet ONE feature's edges by allOfFeature on a cylinder (2 circular edges)", "#1.6",
[{"op": "new_part"}, {"op": "sketch_circle", "plane": "Top Plane", "radiusMm": 5}, {"op": "extrude", "depthMm": 10},
{"op": "fillet", "radiusMm": 1, "edges": {"allOfFeature": "Boss-Extrude1"}}],
# a 10 mm-dia cylinder 10 tall with both circular edges rounded r=1. Each edge removes the
# "spandrel" (square corner minus quarter disc), area (1 - pi/4) r^2, swept round the axis;
# Pappus with its centroid r(5/6 - pi/4)/(1 - pi/4) in from the wall (Monte Carlo agrees).
{"volume": fillet_cylinder_volume(10, 10, 1)}),
("T33", "REVERSED boss: extrude reverse:true from Top Plane goes -Y (Michael's own workaround style)", "#1.2",
[{"op": "new_part"}, {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 20, "heightMm": 20, "anchor": "center"},
{"op": "extrude", "depthMm": 10, "reverse": True}],
{"volume": 4000.0, "bbox": box(-10, 10, -10, 0, -10, 10)}),
("T34", "boss throughAll (one way) from a plane below the block runs up through all of it", "#1.2",
BASE + [{"op": "sketch", "plane": {"offsetFrom": "Top Plane", "offsetMm": -5}, "entities": [{"circle": {"centerMm": [0, 0], "diameterMm": 4}}]},
{"op": "extrude", "throughAll": True}],
{"volume": 4000 + cyl(4, 5), "bodyCount": 1, "bbox": box(-10, 10, -5, 10, -10, 10)}),
("T35", "Right Plane axes: sketch +X is model -Z, +Y is model +Y, extrude +X", "#1.5",
[{"op": "new_part"}, {"op": "sketch_rect", "plane": "Right Plane", "xMm": 0, "yMm": 0, "widthMm": 10, "heightMm": 20},
{"op": "extrude", "depthMm": 5}],
{"volume": 1000.0, "bbox": box(0, 5, 0, 20, -10, 0)}),
("T36", "sketch on a SIDE face (+X) extrudes outward along +X (face sketch keeps Right Plane axes: origin projects to model Y=0)", "#1.1",
BASE + [{"op": "sketch", "face": {"atPointMm": [10, 5, 0]}, "entities": [{"circle": {"centerMm": [0, 5], "diameterMm": 4}}]},
{"op": "extrude", "depthMm": 3}],
{"volume": 4000 + cyl(4, 3), "bodyCount": 1, "bbox": box(-10, 13, 0, 10, -10, 10)}),
("T37", "sketch on the -Z face extrudes outward along -Z", "#1.1",
BASE + [{"op": "sketch", "face": {"atPointMm": [0, 5, -10]}, "entities": [{"circle": {"centerMm": [0, 5], "diameterMm": 4}}]},
{"op": "extrude", "depthMm": 3}],
{"volume": 4000 + cyl(4, 3), "bodyCount": 1, "bbox": box(-10, 10, 0, 10, -13, 10)}),
("T38", "a plane offset +12 mm from FRONT Plane (separate body at z 12..14)", "#1.1",
BASE + [{"op": "sketch", "plane": {"offsetFrom": "Front Plane", "offsetMm": 12}, "entities": [{"rect": {"widthMm": 4, "heightMm": 4, "anchor": "center"}}]},
{"op": "extrude", "depthMm": 2}],
{"volume": 4000 + 32.0, "bodyCount": 2, "bodyBoxes": [BASE_BOX, box(-2, 2, -2, 2, 12, 14)]}),
("T39", "linear pattern in TWO directions (X +12, Z -12): 4 bosses, all on the block", "#1.6",
BASE + [{"op": "sketch", "face": {"atPointMm": [0, 10, 0]}, "entities": [{"circle": {"centerMm": [-6, -6], "diameterMm": 3}}]},
{"op": "extrude", "depthMm": 3},
{"op": "linear_pattern", "feature": "Boss-Extrude2", "directions": [{"axis": "x", "count": 2, "spacingMm": 12}, {"axis": "z", "count": 2, "spacingMm": -12}]}],
{"volume": 4000 + 4 * cyl(3, 3), "bodyCount": 1, "bbox": box(-10, 10, 0, 13, -10, 10)}),
("T42", "shell OUTWARD 1 mm, top face removed: the original block becomes the cavity", "#1.6",
BASE + [{"op": "shell", "thicknessMm": 1, "outward": True, "removeFacesAtPointsMm": [[0, 10, 0]]}],
{"volume": 22 * 11 * 22 - 4000.0, "bbox": box(-11, 11, -1, 10, -11, 11)}),
("T24", "the LEGO 2x4 acceptance brick (examples/lego-2x4.json)", "#1 acceptance",
lego["script"],
{"volume": lego_volume, "bbox": box(-15.9, 15.9, 0, 11.3, -7.9, 7.9), "appearance": [200, 0, 0],
"massKg": lego_volume * 1e-9 * 1020, "densityKgM3": 1020.0, "massMatchesMassProperties": True,
"features": ["Boss-Extrude1", "Cut-Extrude1", "Boss-Extrude2", "Boss-Extrude3"]}),
]
# ── comparing ────────────────────────────────────────────────────────────────
def rel_ok(got, want):
return got is not None and abs(got - want) <= REL_TOL * max(1.0, abs(want))
def union_bbox(bodies):
mins = [b["bbox"]["minMm"] for b in bodies if b.get("bbox")]
maxs = [b["bbox"]["maxMm"] for b in bodies if b.get("bbox")]
if not mins:
return None
return {"min": [min(m[i] for m in mins) for i in range(3)], "max": [max(m[i] for m in maxs) for i in range(3)]}
def run_check(cid, title, issue, script, exp):
rec = {"id": cid, "title": title, "issue": issue, "expect": exp, "fails": []}
t0 = time.time()
req = {"script": script, "purpose": "bridge", "reason": f"dsl-live-check {cid}: {title}"}
if REUSE["path"] and cid not in FRESH_ALWAYS and script and script[0].get("op") == "new_part":
req["reuseDocument"] = REUSE["path"]
rec["reused"] = True
r = call("swx_run_script", req)
rec["runScript"] = r
if rec.get("reused"):
st0 = ((r.get("steps") or [{}])[0].get("result") or {})
rec["clearPasses"] = st0.get("passes")
steps = r.get("steps") or []
doc = r.get("documentName")
if "expectError" in exp:
if ok(r):
# Succeeding is only acceptable if the cut really removed material.
ib = call("swx_inspect_bodies", {"targetDocument": doc}) if doc else {}
vol = sum((b.get("volumeMm3") or 0) for b in ib.get("bodies") or [])
rec["inspect"] = ib
if rel_ok(vol, exp["orVolumeChanged"]):
rec["fails"].append(f"the cut reported success but removed nothing (volume still {vol}) - a silent no-op")
else:
msg = json.dumps({k: r.get(k) for k in ("errorCode", "error", "failedStep", "step")})
rec["observedError"] = msg
if str(exp["expectError"]) not in msg and "cut" not in msg.lower():
rec["fails"].append(f"failed, but the error does not name the cut step: {msg[:200]}")
rec["seconds"] = round(time.time() - t0, 1)
return rec
if not ok(r):
rec["fails"].append(f"swx_run_script failed: {r.get('errorCode')} {str(r.get('error'))[:300]}")
rec["seconds"] = round(time.time() - t0, 1)
if r.get("errorCode") in ("user_present", "low_commit_memory", "stopped_by_dialog"):
rec["stopBattery"] = r.get("errorCode")
return rec
for st in steps:
res = st.get("result") or {}
if isinstance(res, dict) and res.get("feature") is None and st.get("op") in ("extrude", "cut", "fillet", "chamfer", "shell", "linear_pattern", "circular_pattern"):
rec["fails"].append(f"step {st.get('step')} ({st.get('op')}) returned no feature")
if "features" in exp:
made = [((st.get("result") or {}).get("feature")) for st in steps if st.get("op") in ("extrude", "cut")]
if made != exp["features"]:
rec["fails"].append(f"features {made} != {exp['features']}")
if "stepReadBack" in exp:
res = (steps[exp["stepReadBack"]].get("result") or {}) if len(steps) > exp["stepReadBack"] else {}
rb = res.get("readBack") if isinstance(res, dict) else None
rec["appearanceStep"] = res
if rb is None or res.get("applied") is False:
rec["fails"].append(f"set_appearance on a face has no confirming read-back: {json.dumps(res)[:200]}")
ib = call("swx_inspect_bodies", {"targetDocument": doc} if doc else {})
rec["inspect"] = ib
bodies = ib.get("bodies") or []
if not ok(ib) or not bodies:
rec["fails"].append(f"swx_inspect_bodies: {ib.get('errorCode')} {str(ib.get('error'))[:200]} (bodies {len(bodies)})")
else:
vol = sum((b.get("volumeMm3") or 0) for b in bodies)
rec["measured"] = {"volumeMm3": vol, "bodyCount": len(bodies), "bbox": union_bbox(bodies),
"faceCount": [b.get("faceCount") for b in bodies],
"appearanceRgb": [b.get("appearanceRgb") for b in bodies],
"massKg": [b.get("massKg") for b in bodies], "densityKgM3": [b.get("densityKgM3") for b in bodies]}
if "volume" in exp and not rel_ok(vol, exp["volume"]):
rec["fails"].append(f"volume {vol:.4f} mm^3, expected {exp['volume']:.4f}")
if "bodyCount" in exp and len(bodies) != exp["bodyCount"]:
rec["fails"].append(f"{len(bodies)} bodies, expected {exp['bodyCount']}")
if "faceCount" in exp and bodies[0].get("faceCount") != exp["faceCount"]:
rec["fails"].append(f"faceCount {bodies[0].get('faceCount')}, expected {exp['faceCount']}")
bb = union_bbox(bodies)
if "bbox" in exp and bb:
for k in ("min", "max"):
for i in range(3):
if abs(bb[k][i] - exp["bbox"][k][i]) > MM_TOL:
rec["fails"].append(f"bbox {k}[{'XYZ'[i]}] = {bb[k][i]}, expected {exp['bbox'][k][i]}")
if "bboxZSpan" in exp and bb:
span = bb["max"][2] - bb["min"][2]
if abs(span - exp["bboxZSpan"]) > MM_TOL:
rec["fails"].append(f"Z span {span}, expected {exp['bboxZSpan']} (direction both)")
for i, (lo, hi) in enumerate(exp["bboxXY"]):
if abs(bb["min"][i] - lo) > MM_TOL or abs(bb["max"][i] - hi) > MM_TOL:
rec["fails"].append(f"bbox {'XY'[i]} = [{bb['min'][i]}, {bb['max'][i]}], expected [{lo}, {hi}]")
if "bodyBoxes" in exp:
got = [b.get("bbox") for b in bodies]
unmatched = []
for want in exp["bodyBoxes"]:
hit = None
for gi, g in enumerate(got):
if g and all(abs(g["minMm"][i] - want["min"][i]) <= MM_TOL and abs(g["maxMm"][i] - want["max"][i]) <= MM_TOL for i in range(3)):
hit = gi
break
if hit is None:
unmatched.append(want)
else:
got.pop(hit)
if unmatched:
rec["fails"].append(f"no body with bbox {unmatched} (bodies: {[b.get('bbox') for b in bodies]})")
if "appearance" in exp:
got = bodies[0].get("appearanceRgb")
if not got or any(abs(a - b) > 2 for a, b in zip(got, exp["appearance"])):
rec["fails"].append(f"appearanceRgb {got}, expected {exp['appearance']}")
if "massKg" in exp:
m = sum((b.get("massKg") or 0) for b in bodies)
if not rel_ok(m, exp["massKg"]):
rec["fails"].append(f"inspect_bodies massKg {m}, expected {exp['massKg']}")
d = bodies[0].get("densityKgM3")
if d is None or abs(d - exp["densityKgM3"]) > 0.5:
rec["fails"].append(f"densityKgM3 {d}, expected {exp['densityKgM3']}")
if exp.get("massMatchesMassProperties"):
mp = call("swx_mass_properties", {"targetDocument": doc} if doc else {})
rec["massProperties"] = mp
m = sum((b.get("massKg") or 0) for b in bodies)
if not rel_ok(m, mp.get("massKg") or -1):
rec["fails"].append(f"inspect_bodies mass {m} != swx_mass_properties mass {mp.get('massKg')}")
rec["seconds"] = round(time.time() - t0, 1)
return rec
def ownership_check():
"""#2.b: a part made by swx_new_document, then scripted by PATH, is the bridge's own."""
rec = {"id": "T23", "title": "issue #2: run_script on the bridge's own new part (addressed by path) says bridgeCreatedDocument:true", "issue": "#2.b", "fails": []}
nd = call("swx_new_document", {"type": "part", "purpose": "bridge", "reason": "dsl-live-check T23"})
rec["newDocument"] = nd
path = (nd.get("created") or {}).get("path") or (nd.get("documentIdentity") or {}).get("path") or nd.get("path")
title = (nd.get("created") or {}).get("title") or nd.get("title")
if not ok(nd):
rec["fails"].append(f"swx_new_document failed: {nd.get('errorCode')} {str(nd.get('error'))[:200]}")
return rec
target = {"path": path} if path else {"targetDocument": title}
r = call("swx_run_script", dict(target, script=[{"op": "sketch_rect", "plane": "Front Plane", "widthMm": 10, "heightMm": 10}, {"op": "extrude", "depthMm": 2}],
reason="dsl-live-check T23"))
rec["runScript"] = r
if not ok(r):
rec["fails"].append(f"swx_run_script failed: {r.get('errorCode')} {str(r.get('error'))[:200]}")
elif r.get("bridgeCreatedDocument") is not True:
rec["fails"].append(f"bridgeCreatedDocument = {r.get('bridgeCreatedDocument')} for the bridge's own part (addressed by {'path' if path else 'title'})")
return rec
def main():
global TARGET
ap = argparse.ArgumentParser()
ap.add_argument("--target", default=TARGET)
ap.add_argument("--only", default="")
ap.add_argument("--keep", action="store_true", help="leave the scratch parts open")
ap.add_argument("--report", default="dsl-live-report.json")
ap.add_argument("--reuse", action="store_true", help="run every check that allows it in ONE bridge-made part (clear_part between checks) instead of a fresh part each")
a = ap.parse_args()
TARGET = a.target
st = call("swx_status", {"reason": "dsl-live-check: is SOLIDWORKS ready"})
sw = st.get("solidworks") or {}
print(f"bridge {st.get('bridgeVersion')} | SOLIDWORKS running={sw.get('running')} ready={sw.get('ready')} "
f"version={sw.get('majorVersion')} edition={sw.get('edition')} | waitingOn={st.get('waitingOn')}")
if not sw.get("running"):
print("SOLIDWORKS is not running on the target: start it (swx_start) and run again. Nothing was done.")
return 2
only = {x.strip() for x in a.only.split(",") if x.strip()}
results = []
plan = checks()
if a.reuse:
nd = call("swx_new_document", {"type": "part", "purpose": "bridge", "reason": "dsl-live-check --reuse: the one part every check that allows it runs in"})
REUSE["path"] = (nd.get("documentIdentity") or {}).get("path") or (nd.get("created") or {}).get("path")
print(f"reuse part: {'created' if REUSE['path'] else 'FAILED (' + str(nd.get('errorCode')) + '), running with fresh parts'}")
for cid, title, issue, script, exp in plan:
if only and cid not in only:
continue
rec = run_check(cid, title, issue, script, exp)
results.append(rec)
print(f"{'PASS' if not rec['fails'] else 'FAIL'} {cid} {title} ({rec.get('seconds')} s)")
for f in rec["fails"]:
print(f" - {f}")
if rec.get("stopBattery"):
print(f"STOP the bridge refused to create ({rec['stopBattery']}); no more parts will be created. Re-run when the user is idle / memory is back / the dialog is answered.")
break
if not a.keep and not rec.get("reused"):
doc = (rec.get("runScript") or {}).get("documentName")
if REUSE["path"] and doc:
# MEASURED 2026-10-15: swx_reset_workspace closes EVERY bridge-made unsaved part, the shared
# reuse part included; 19 later checks then failed "clear_part needs a document". Close only
# this check's own part.
rw = call("swx_close_doc", {"targetDocument": doc, "save": False, "reason": f"dsl-live-check: close the fresh part of {cid} (reuse mode keeps the shared part)"})
if not ok(rw):
rw = call("swx_reset_workspace", {"reason": f"dsl-live-check: close the scratch part of {cid} (close_doc refused: {rw.get('errorCode')})"})
nd = call("swx_new_document", {"type": "part", "purpose": "bridge", "reason": "dsl-live-check --reuse: re-create the shared part after a workspace reset"})
REUSE["path"] = (nd.get("documentIdentity") or {}).get("path") or (nd.get("created") or {}).get("path")
else:
rw = call("swx_reset_workspace", {"reason": f"dsl-live-check: close the scratch part of {cid}"})
if not ok(rw):
# MEASURED 2026-10-14: 17 closes in a row were refused user_present_refusal while the
# user worked at the keyboard, and the battery kept creating parts: 18 windows piled up
# on his screen, SOLIDWORKS reached 15 GB. A refused close STOPS the battery.
print(f"STOP the close after {cid} was refused ({rw.get('errorCode')}: {str(rw.get('error'))[:140]}); "
f"no more parts will be created. Re-run when the user is away from the keyboard.")
results.append({"id": "STOP", "title": "battery stopped: a close was refused", "fails": [f"{rw.get('errorCode')}: {str(rw.get('error'))[:200]}"]})
break
if REUSE["path"] and not a.keep:
rw = call("swx_reset_workspace", {"reason": "dsl-live-check --reuse: close the shared part"})
if not ok(rw):
print(f"WARN the shared part could not be closed ({rw.get('errorCode')}); it is the bridge's own unsaved scratch part — swx_reset_workspace closes it once the user is idle")
if not only or "T23" in only:
rec = ownership_check()
results.append(rec)
print(f"{'PASS' if not rec['fails'] else 'FAIL'} T23 {rec['title']}")
for f in rec["fails"]:
print(f" - {f}")
if not a.keep:
call("swx_reset_workspace", {"reason": "dsl-live-check: close the scratch part of T23"})
passed = sum(1 for r in results if not r["fails"])
report = {"when": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "target": TARGET,
"bridgeVersion": st.get("bridgeVersion"), "solidworks": {k: sw.get(k) for k in ("majorVersion", "edition", "versionSupport")},
"passed": passed, "failed": len(results) - passed, "reuse": bool(REUSE["path"]),
"reusedChecks": sum(1 for r in results if r.get("reused")), "checks": results}
json.dump(report, open(a.report, "w"), indent=1)
print(f"\n{passed}/{len(results)} passed. Full evidence: {a.report}")
return 0 if passed == len(results) else 1
if __name__ == "__main__":
sys.exit(main())