Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
Typed reads for assembly planning (assembly_layout, ray_cast) #42
feature/planner-ops → masterTyped reads for assembly planning: assembly_layout and ray_cast
What
Two read-only mechanical operations, so an assembly planner reads the design through the bridge
instead of raw fusion_run_modeling_script scripts (Assembly Instructions' planner did both with
scripts, outside the exe's validation and the journal):
assembly_layoutlists one scope's parts: every visible occurrence directly underscope(an occurrence path, or the root), each with path, name, component, grounded, child count and an axis-aligned root-space box in mm from its bodies and its children's (breadth first, at mostmaxBodies, default 200,boxCappedwhen that stopped it). It also returns the scope component's unsuppressed joints, as-built joints and rigid groups as part paths. Journaled likeefficiency_report(sessionId,operationId): the first guarded read after the add-in loads a 4,579-occurrence assembly registers every entity (about 45 s), past the relay's 60 s.ray_castcasts batches of rays. Each ray names, by index intooccurrences, the occurrences it is cast against, in each one's own component coordinates (inverse of its root transform, hidden bodies included). Returns per ray[index, distance], nearest first, rounded to 0.01 mm, pluscast,complete,missingandfailed.
Why ray_cast is not journaled
A plan casts thousands of rays (27,000 ray-part casts on the real panel). The journal keeps every
request and result and never evicts (10,000 records, 64 MiB, and admit decodes every record), so
journaled ray batches would fill it within tens of plans and then refuse every mech write. Instead
ray_cast follows bom: it stops at budgetSeconds (default 30, max 50) or before its reply passes
1 MiB, answers the first cast rays (always at least one) and the caller sends the rest. The CLI's
128 KiB argument limit means a big batch goes in several calls anyway.
Guard
The usual one, through a new core.read_target: the active document must be the target's, and a
revision, when given, must be current. Without a revision ray_cast skips the revision digest
(about 5 s a call on the 4,579-occurrence assembly). The planner reads the layout (full target),
casts with {documentId}, and checks the revision once at the end with ray_cast and no rays.
Files
addin/AdomBridge/commands/mechanical/layout.py(new),__init__.py,core.py(read_target)schema.json: the two schemas (70 ops);contracts/describe.jsonrefrozenrust/bridge/src/mech.rs: two exe tests (ray batch validated and passed through without a journal record, malformed rays refused before Fusion; layout journaled, status reads it back)- goldens regenerated (
mech_capabilities.json,mech_validation.json: 36 new cases) tests/test_mechanical_layout.py(18),docs/MECHANICAL.md("Plan an assembly order")
Tested
Base: master ced0e0b (1.13.78). Tests and builds ran on 3b89b5d; ced0e0b only updates the release manifest.
Python:
discover571 pass (18 new intest_mechanical_layout.py: boxes, caps, joints and groups as part paths, a "+" inside a name, rays in local coordinates, rounding, missing/failed, budgets that always make progress, no digest without a revision, the layout journaled and a ray batch not, malformed rays refused).Rust:
tools/rust-ship.shgreen in a private target dir: 310 tests (2 new inmech.rs), Windows build, guards. The exe built from this tree embeds itsschema.jsonanddescribe.jsonbyte for byte.Generated files (goldens, contracts/describe.json) were regenerated on master 3b89b5d; if another of today's PRs merges first, they'll be regenerated again.
Live on Drew's laptop, dev build deployed and restored in one 139 s hold of the Fusion lock. On
tool_testbed.py(15 parts) andplanner_testbed.py(11 parts, one as-built joint),assembly_layoutreturned exactly what the planner's READ script returns (units, boxes, joints, rigid groups), andassembly-instructions plangave byte-identical specs through the ops and through the scripts (4 runs each testbed, alternating):testbed scripts typed ops ray_cast calls tool (853 distinct rays) 11.3, 11.6 s 7.1, 7.8 s 5 planner (756 distinct rays) 11.7, 11.6 s 7.8, 7.5 s 5 One-ray call:
ray_cast0.81 to 0.89 s, the script 1.86 to 1.95 s; the end-of-plan revision check 0.86 s. After restore: exe and all four add-in folders back to 1.13.61, the restored add-in session republished, readiness ready, bridge unpinned, testbeds closed.Not run: the 4,579-occurrence assembly (not authorized for this task). There, each extra digest costs about 5 s and a typed call carries about 600 rays (the CLI's 128 KiB limit), so the typed path should land near today's 207 s. The app now also sends each distinct ray to Fusion once per plan, on both paths (8% fewer rays on the planner testbed).
Note for the journal
On Drew's laptop operations-v1.sqlite3 already holds 2,150 records and 32 MB of its 64 MiB hard cap
with no eviction; this branch adds one small record per assembly_layout (7 records, 23 KB in the
whole live run).
🤖 Generated with Claude Code
Diff Skip to comments
addin/AdomBridge/commands/mechanical/__init__.py+3−2@@ -14,7 +14,7 @@ import adsk.core from .core import SESSION_ID, MechanicalError, document_identity, require from .operations import OPERATIONS, WRITES from .journal import Journal, JournalError-from . import handoff, transactions, sketches, features, assemblies, purchased, bill_of_materials, geometry, sketch_edits, regular_joints, motion_links, configurations, render_scene, exploded_view, exploded_preview, efficiency, exploded_render, exploded_stills, render_image, cam+from . import handoff, transactions, sketches, features, assemblies, purchased, bill_of_materials, geometry, sketch_edits, regular_joints, motion_links, configurations, render_scene, exploded_view, exploded_preview, efficiency, exploded_render, exploded_stills, render_image, cam, layout for _name, _handler in {**sketches.OPERATIONS, **features.OPERATIONS, **assemblies.OPERATIONS, **geometry.OPERATIONS, **sketch_edits.OPERATIONS, **regular_joints.OPERATIONS, **motion_links.OPERATIONS}.items():@@ -37,7 +37,8 @@ OPERATIONS.update(exploded_render.OPERATIONS) OPERATIONS.update(exploded_stills.OPERATIONS) OPERATIONS.update(render_image.OPERATIONS) OPERATIONS.update(cam.OPERATIONS)-WRITES.update(purchased.WRITES | bill_of_materials.WRITES | efficiency.WRITES | exploded_render.WRITES | exploded_stills.WRITES | render_image.WRITES | cam.WRITES)+OPERATIONS.update(layout.OPERATIONS)+WRITES.update(purchased.WRITES | bill_of_materials.WRITES | efficiency.WRITES | exploded_render.WRITES | exploded_stills.WRITES | render_image.WRITES | cam.WRITES | layout.WRITES) OPERATIONS["transaction_probe"] = transactions.probe WRITES.add("transaction_probe")
addin/AdomBridge/commands/mechanical/core.py+14@@ -364,6 +364,20 @@ def target(app, args, write=False, model_write=True): return doc, design, {"documentId": key, "revision": rev} +def read_target(app, args):+ """target() for a read that only needs the guard: the target's document must be the active one, and+ a revision, when the target carries one, must be current. The revision digest costs about 5 s on a+ 4,579-occurrence assembly, so a read not asked to check one does not compute it, and its target then+ carries no revision."""+ _, design, key = active(app)+ wanted = args.get("target")+ require(isinstance(wanted, dict), "target_required", "Pass target from mechanical inspect.")+ require(wanted.get("documentId") == key, "wrong_document", "Target is not the active document.", activeDocumentId=key)+ if "revision" in wanted:+ return target(app, args)+ return app.activeDocument, design, {"documentId": key}++ def current_target(app): doc, design, key = active(app) return {"documentId": key, "revision": revision(design)}
addin/AdomBridge/commands/mechanical/layout.pyadded+198@@ -0,0 +1,198 @@+"""What an assembly planner reads from the design: the parts of a scope with their boxes, the joints and+rigid groups between them, and batches of rays. Both operations change nothing.++assembly_layout lists the visible occurrences directly under a scope occurrence (or under the root), each+with its box in root space, and the scope component's joints and rigid groups between them. It is+journaled: the first guarded read after the add-in loads registers every entity of a big assembly (about+45 s on a 4,579-occurrence one) and can outlive the relay's 60 s, so the caller follows it with+fusion_mech_operation_status.++ray_cast casts rays, each against the few occurrences named for it, in each occurrence's own component+coordinates (the inverse of its root-space transform): a component holds a handful of bodies where a whole+scope can hold thousands (a ray against a 4,579-occurrence assembly's panel component took about 0.25 s,+against one part's component about a millisecond). It is not journaled: a plan casts thousands of rays,+and the journal keeps every request and result with no eviction (10,000 records, 64 MiB), so journaled ray+batches would fill it within tens of plans. It stops at budgetSeconds instead, and before its reply passes+1 MiB, and says how many rays it answered; the caller sends the rest.+"""+from collections import deque+import json+import time++import adsk.core+import adsk.fusion++from .core import active, current_target, occurrence_at, read_target, require, target++DEFAULT_BODIES = 200 # bodies read per part for its box (a body proxy's box costs about 26 ms on a big assembly)+MAX_UNITS = 4096 # visible parts in one scope+MAX_LAYOUT_BYTES = 2 * 1024 * 1024+MAX_REPLY_BYTES = 1024 * 1024+DEFAULT_BUDGET = 30.0 # seconds of ray casting per call, inside the relay's 60 s+_clock = time.perf_counter+++def part_box(occurrence, cap):+ """The root-space box (mm) of an occurrence's bodies and its children's, read breadth first from at+ most cap bodies. Returns (box or None when it has no bodies, whether the cap stopped the walk)."""+ lo = hi = None+ read, pending, stopped = 0, deque([occurrence]), False+ while pending and read < cap:+ node = pending.popleft()+ bodies = node.bRepBodies+ for i in range(bodies.count):+ if read >= cap:+ stopped = True+ break+ box = bodies.item(i).boundingBox+ read += 1+ low = [box.minPoint.x * 10, box.minPoint.y * 10, box.minPoint.z * 10]+ high = [box.maxPoint.x * 10, box.maxPoint.y * 10, box.maxPoint.z * 10]+ lo = low if lo is None else [min(u, v) for u, v in zip(lo, low)]+ hi = high if hi is None else [max(u, v) for u, v in zip(hi, high)]+ kids = node.childOccurrences+ pending.extend(kids.item(i) for i in range(kids.count))+ return ([lo, hi] if lo else None), stopped or (read >= cap and bool(pending))+++def part_path(occurrence, scope, names):+ """The scope part an occurrence belongs to, "" for the scope component's own geometry. A joint's+ occurrences are in the scope component's context, so their paths start with the part's own name;+ names (every child's, longest first) keep a name that holds "+" whole."""+ if occurrence is None:+ return ""+ path = occurrence.fullPathName+ head = next((n for n in names if path == n or path.startswith(n + "+")), path.split("+")[0])+ return scope + "+" + head if scope else head+++def assembly_layout(app, args):+ # the guard and the reply are inspect's: an optional target, and the current target returned+ if args.get("target"):+ doc, design, value = target(app, args)+ else:+ doc, design, _ = active(app)+ value = current_target(app)+ root = design.rootComponent+ scope = args.get("scope") or ""+ cap = args.get("maxBodies", DEFAULT_BODIES)+ start = None+ if scope:+ start = occurrence_at(design, scope)+ require(start is not None and start.fullPathName == scope, "occurrence_missing",+ "No occurrence at %r: pass the full path of the subassembly (\"Panel:1\", \"Cell:1+Panel:1\")." % scope[:200])+ component = start.component if start else root+ children = start.childOccurrences if start else root.occurrences+ require(children.count <= 20000, "inspection_budget", "The scope has more than 20000 occurrences directly under it.")+ kids = [children.item(i) for i in range(children.count)]+ names = sorted((k.name for k in kids), key=len, reverse=True)+ units, hidden = [], 0+ for occurrence in kids:+ if not occurrence.isLightBulbOn:+ hidden += 1+ continue+ require(len(units) < MAX_UNITS, "layout_budget", "The scope has more than %d visible parts; pass the path of a subassembly as scope." % MAX_UNITS)+ box, capped = part_box(occurrence, cap)+ unit = {"path": occurrence.fullPathName, "name": occurrence.name, "component": occurrence.component.name,+ "grounded": occurrence.isGrounded, "box": box, "children": occurrence.childOccurrences.count}+ if capped:+ unit["boxCapped"] = True+ units.append(unit)+ joints, groups, unreadable = [], [], 0+ for collection, label in ((component.joints, "joint"), (component.asBuiltJoints, "asBuilt")):+ for i in range(collection.count):+ joint = collection.item(i)+ try:+ if joint.isSuppressed:+ continue+ joints.append([part_path(joint.occurrenceOne, scope, names), part_path(joint.occurrenceTwo, scope, names), label])+ except Exception:+ unreadable += 1 # a joint to geometry Fusion cannot name here; it constrains nothing the plan reads+ for i in range(component.rigidGroups.count):+ group = component.rigidGroups.item(i)+ try:+ members = group.occurrences+ groups.append([part_path(members.item(k), scope, names) for k in range(members.count)])+ except Exception:+ unreadable += 1+ result = {"target": value, "document": doc.name, "scope": scope,+ "scopeName": start.component.name if start else doc.name,+ "units": units, "joints": joints, "rigidGroups": groups, "frameBodies": component.bRepBodies.count,+ "hiddenParts": hidden, "unreadableLinks": unreadable, "maxBodies": cap,+ "unit": "mm", "space": "root", "boxes": "axis-aligned in root space, from each part's bodies and its children's"}+ require(len(json.dumps(result)) <= MAX_LAYOUT_BYTES, "layout_budget", "The layout exceeds its 2 MiB bound; pass the path of a subassembly as scope.")+ return result+++def ray_cast(app, args):+ started = _clock()+ _, design, value = read_target(app, args)+ paths, rays = args.get("occurrences") or [], args.get("rays") or []+ budget = float(args.get("budgetSeconds", DEFAULT_BUDGET))+ for n, ray in enumerate(rays):+ require(all(0 <= i < len(paths) for i in ray["against"]), "invalid_input",+ "rays[%d].against names an index past the %d occurrences." % (n, len(paths)))+ require(any(v != 0 for v in ray["direction"]), "invalid_input", "rays[%d].direction must be nonzero." % n)+ parts, missing = {}, []++ def part(index):+ """The occurrence's component and the inverse of its root-space transform, once per call."""+ if index not in parts:+ occurrence = occurrence_at(design, paths[index])+ if occurrence is None or occurrence.fullPathName != paths[index]:+ parts[index] = None+ missing.append(index)+ else:+ inverse = occurrence.transform2.copy()+ inverse.invert()+ parts[index] = (occurrence.component, inverse)+ return parts[index]++ hits, failed, casts, size, stopped = [], 0, 0, 2, None+ for ray in rays:+ # every call answers at least one ray, so a caller sending the rest always gets further+ if hits and _clock() - started > budget:+ stopped = "budget"+ break+ x, y, z = ray["origin"]+ dx, dy, dz = ray["direction"]+ longest = ray["length"]+ row = []+ for index in ray["against"]:+ found_part = part(index)+ if found_part is None:+ continue+ component, inverse = found_part+ try:+ origin = adsk.core.Point3D.create(x / 10, y / 10, z / 10)+ direction = adsk.core.Vector3D.create(dx, dy, dz)+ origin.transformBy(inverse)+ direction.transformBy(inverse)+ points = adsk.core.ObjectCollection.create()+ # a component's own bodies report invisible (their occurrence is what shows): do not filter on it+ found = component.findBRepUsingRay(origin, direction, adsk.fusion.BRepEntityTypes.BRepBodyEntityType, -1.0, False, points)+ casts += 1+ near = [origin.distanceTo(points.item(k)) * 10 for k in range(found.count)]+ near = [d for d in near if d <= longest]+ if near:+ row.append([index, round(min(near), 2)])+ except Exception:+ failed += 1+ row.sort(key=lambda hit: hit[1])+ row_bytes = len(json.dumps(row)) + 1+ if hits and size + row_bytes > MAX_REPLY_BYTES:+ stopped = "reply_bytes"+ break+ size += row_bytes+ hits.append(row)+ result = {"target": value, "unit": "mm", "space": "root", "hits": hits, "cast": len(hits), "complete": len(hits) == len(rays),+ "missing": sorted(missing), "failed": failed, "casts": casts, "seconds": round(_clock() - started, 3)}+ if stopped:+ result["stopped"] = stopped+ result["hint"] = ("Answered the first %d of %d rays (%s); send the rest in another call."+ % (len(hits), len(rays), "budgetSeconds reached" if stopped == "budget" else "the reply reached 1 MiB"))+ return result+++OPERATIONS = {"assembly_layout": assembly_layout, "ray_cast": ray_cast}+WRITES = {"assembly_layout"} # journaled, model read-only: a big assembly's first layout outlives the relay's 60 s
addin/AdomBridge/commands/mechanical/schema.json+149@@ -8001,5 +8001,154 @@ "op", "target" ]+ },+ "assembly_layout": {+ "type": "object",+ "required": [+ "op",+ "sessionId",+ "operationId"+ ],+ "properties": {+ "sessionId": {+ "type": "string"+ },+ "operationId": {+ "type": "string"+ },+ "target": {+ "type": "object",+ "required": [+ "documentId"+ ],+ "additionalProperties": false,+ "properties": {+ "documentId": {+ "type": "string"+ },+ "revision": {+ "type": "string",+ "description": "Refuse with stale_revision unless the model is still at this revision (costs the revision digest, about 5 s on a 4,579-occurrence assembly)."+ }+ },+ "description": "Optional, as for inspect: the active document must be this one, and the model at revision when one is given. The reply carries the current target either way."+ },+ "scope": {+ "type": "string",+ "description": "Full path of the occurrence whose parts to list (\"Panel:1\", \"Cell:1+Panel:1\"). Empty or omitted: the root component's."+ },+ "maxBodies": {+ "type": "integer",+ "minimum": 1,+ "maximum": 20000,+ "description": "Bodies read per part for its box, breadth first through its children (default 200; a body proxy's box costs about 26 ms on a big assembly). A part whose walk stopped there has boxCapped:true."+ }+ },+ "description": "Read one scope's parts for planning an assembly order, without changing anything: every visible occurrence directly under the scope occurrence (or the root), each with path, name, component name, grounded, the number of its children and its box (axis-aligned, root space, mm, from its bodies and its children's; null for a part with no bodies), plus the scope component's unsuppressed joints and as-built joints ([partOne, partTwo, \"joint\"|\"asBuilt\"], \"\" for the scope's own geometry) and rigid groups as part paths, frameBodies (bodies of the scope component itself), document and scopeName. Journaled like efficiency_report: the first guarded read after the add-in loads registers every entity of a big assembly and can outlive the relay's 60 s, so follow it with fusion_mech_operation_status. Returns the current target; pass it to ray_cast."+ },+ "ray_cast": {+ "type": "object",+ "required": [+ "op",+ "target",+ "unit",+ "rays"+ ],+ "properties": {+ "target": {+ "type": "object",+ "required": [+ "documentId"+ ],+ "additionalProperties": false,+ "properties": {+ "documentId": {+ "type": "string"+ },+ "revision": {+ "type": "string",+ "description": "Refuse with stale_revision unless the model is still at this revision (costs the revision digest, about 5 s on a 4,579-occurrence assembly)."+ }+ },+ "description": "From assembly_layout or inspect. The active document must be this one; with revision the model must still be at it. Without revision no digest is computed and the reply's target carries none."+ },+ "unit": {+ "type": "string",+ "enum": [+ "mm"+ ]+ },+ "occurrences": {+ "type": "array",+ "maxItems": 4096,+ "items": {+ "type": "string"+ },+ "description": "Full root-context paths of the occurrences the rays are cast against; each ray names its own by index."+ },+ "rays": {+ "type": "array",+ "maxItems": 8192,+ "description": "Rays in root space. Each is cast against each occurrence it names, in that occurrence's own component coordinates; hidden bodies count too. An empty list only checks the target.",+ "items": {+ "type": "object",+ "required": [+ "origin",+ "direction",+ "length",+ "against"+ ],+ "additionalProperties": false,+ "properties": {+ "origin": {+ "type": "array",+ "minItems": 3,+ "maxItems": 3,+ "items": {+ "type": "number",+ "minimum": -1000000000.0,+ "maximum": 1000000000.0+ },+ "description": "Start point, mm."+ },+ "direction": {+ "type": "array",+ "minItems": 3,+ "maxItems": 3,+ "items": {+ "type": "number",+ "minimum": -1000000000.0,+ "maximum": 1000000000.0+ },+ "description": "Direction (any nonzero length)."+ },+ "length": {+ "type": "number",+ "exclusiveMinimum": 0,+ "maximum": 1000000000.0,+ "description": "Hits farther than this (mm) are dropped."+ },+ "against": {+ "type": "array",+ "minItems": 1,+ "maxItems": 256,+ "items": {+ "type": "integer",+ "minimum": 0,+ "maximum": 4095+ },+ "description": "Indexes into occurrences."+ }+ }+ }+ },+ "budgetSeconds": {+ "type": "number",+ "minimum": 1,+ "maximum": 50,+ "description": "Stop casting after this many seconds (default 30) so the reply comes back inside the relay's 60 s; the reply then has complete:false and answers the first `cast` rays. Send the rest in another call."+ }+ },+ "description": "Cast batches of rays against named occurrences, without changing anything (assembly planning: what a part seats on, what blocks its way in, whether a tool reaches a screw). Each ray is cast against each occurrence it names, in that occurrence's own component coordinates, about a millisecond on a part, where a ray against a big subassembly's component (thousands of bodies) took a quarter of a second. Returns hits: a row per ray answered, [[occurrence index, distance in mm rounded to 0.01], ...], one entry per occurrence the ray meets within length (its nearest hit), nearest first; a ray that starts inside a body meets that body's exit face. Also cast (rays answered, in order), complete, missing (indexes with no occurrence at that path), failed (casts Fusion refused) and seconds. Not journaled: it stops at budgetSeconds or before its reply passes 1 MiB (stopped: budget|reply_bytes) and the caller sends the remaining rays. Requests over the CLI's argument limit (128 KiB) go in several calls." } }
contracts/describe.json+299−1@@ -4173,6 +4173,50 @@ "output": { "idempotency": "object", "operations": {+ "assembly_layout": {+ "description": "Read one scope's parts for planning an assembly order, without changing anything: every visible occurrence directly under the scope occurrence (or the root), each with path, name, component name, grounded, the number of its children and its box (axis-aligned, root space, mm, from its bodies and its children's; null for a part with no bodies), plus the scope component's unsuppressed joints and as-built joints ([partOne, partTwo, \"joint\"|\"asBuilt\"], \"\" for the scope's own geometry) and rigid groups as part paths, frameBodies (bodies of the scope component itself), document and scopeName. Journaled like efficiency_report: the first guarded read after the add-in loads registers every entity of a big assembly and can outlive the relay's 60 s, so follow it with fusion_mech_operation_status. Returns the current target; pass it to ray_cast.",+ "properties": {+ "maxBodies": {+ "description": "Bodies read per part for its box, breadth first through its children (default 200; a body proxy's box costs about 26 ms on a big assembly). A part whose walk stopped there has boxCapped:true.",+ "maximum": 20000,+ "minimum": 1,+ "type": "integer"+ },+ "operationId": {+ "type": "string"+ },+ "scope": {+ "description": "Full path of the occurrence whose parts to list (\"Panel:1\", \"Cell:1+Panel:1\"). Empty or omitted: the root component's.",+ "type": "string"+ },+ "sessionId": {+ "type": "string"+ },+ "target": {+ "additionalProperties": false,+ "description": "Optional, as for inspect: the active document must be this one, and the model at revision when one is given. The reply carries the current target either way.",+ "properties": {+ "documentId": {+ "type": "string"+ },+ "revision": {+ "description": "Refuse with stale_revision unless the model is still at this revision (costs the revision digest, about 5 s on a 4,579-occurrence assembly).",+ "type": "string"+ }+ },+ "required": [+ "documentId"+ ],+ "type": "object"+ }+ },+ "required": [+ "op",+ "sessionId",+ "operationId"+ ],+ "type": "object"+ }, "assembly_properties": { "description": "High-accuracy physical properties of whole root assembly (default) or an exact Component/Occurrence/BRepBody. Returns mass kg, volume mm3, area mm2, center of mass mm, mean density kg/mm3 and definition/instance coordinate scope.", "properties": {@@ -10486,6 +10530,111 @@ ], "type": "object" },+ "ray_cast": {+ "description": "Cast batches of rays against named occurrences, without changing anything (assembly planning: what a part seats on, what blocks its way in, whether a tool reaches a screw). Each ray is cast against each occurrence it names, in that occurrence's own component coordinates, about a millisecond on a part, where a ray against a big subassembly's component (thousands of bodies) took a quarter of a second. Returns hits: a row per ray answered, [[occurrence index, distance in mm rounded to 0.01], ...], one entry per occurrence the ray meets within length (its nearest hit), nearest first; a ray that starts inside a body meets that body's exit face. Also cast (rays answered, in order), complete, missing (indexes with no occurrence at that path), failed (casts Fusion refused) and seconds. Not journaled: it stops at budgetSeconds or before its reply passes 1 MiB (stopped: budget|reply_bytes) and the caller sends the remaining rays. Requests over the CLI's argument limit (128 KiB) go in several calls.",+ "properties": {+ "budgetSeconds": {+ "description": "Stop casting after this many seconds (default 30) so the reply comes back inside the relay's 60 s; the reply then has complete:false and answers the first `cast` rays. Send the rest in another call.",+ "maximum": 50,+ "minimum": 1,+ "type": "number"+ },+ "occurrences": {+ "description": "Full root-context paths of the occurrences the rays are cast against; each ray names its own by index.",+ "items": {+ "type": "string"+ },+ "maxItems": 4096,+ "type": "array"+ },+ "rays": {+ "description": "Rays in root space. Each is cast against each occurrence it names, in that occurrence's own component coordinates; hidden bodies count too. An empty list only checks the target.",+ "items": {+ "additionalProperties": false,+ "properties": {+ "against": {+ "description": "Indexes into occurrences.",+ "items": {+ "maximum": 4095,+ "minimum": 0,+ "type": "integer"+ },+ "maxItems": 256,+ "minItems": 1,+ "type": "array"+ },+ "direction": {+ "description": "Direction (any nonzero length).",+ "items": {+ "maximum": 1000000000.0,+ "minimum": -1000000000.0,+ "type": "number"+ },+ "maxItems": 3,+ "minItems": 3,+ "type": "array"+ },+ "length": {+ "description": "Hits farther than this (mm) are dropped.",+ "exclusiveMinimum": 0,+ "maximum": 1000000000.0,+ "type": "number"+ },+ "origin": {+ "description": "Start point, mm.",+ "items": {+ "maximum": 1000000000.0,+ "minimum": -1000000000.0,+ "type": "number"+ },+ "maxItems": 3,+ "minItems": 3,+ "type": "array"+ }+ },+ "required": [+ "origin",+ "direction",+ "length",+ "against"+ ],+ "type": "object"+ },+ "maxItems": 8192,+ "type": "array"+ },+ "target": {+ "additionalProperties": false,+ "description": "From assembly_layout or inspect. The active document must be this one; with revision the model must still be at it. Without revision no digest is computed and the reply's target carries none.",+ "properties": {+ "documentId": {+ "type": "string"+ },+ "revision": {+ "description": "Refuse with stale_revision unless the model is still at this revision (costs the revision digest, about 5 s on a 4,579-occurrence assembly).",+ "type": "string"+ }+ },+ "required": [+ "documentId"+ ],+ "type": "object"+ },+ "unit": {+ "enum": [+ "mm"+ ],+ "type": "string"+ }+ },+ "required": [+ "op",+ "target",+ "unit",+ "rays"+ ],+ "type": "object"+ }, "regular_joint_create": { "description": "Create a geometry-solving regular joint in the root component using two distinct instance-context attachments or joint origins. All seven native motion families; offsets mm, angle and angular limits deg. Read back freshly resolved attachment graph, native parameters and root-space occurrence placement. Root/non-root sides must be unambiguous; both grounded and ancestor/descendant pairs are refused. Inferred joints are unsupported. Existing pending positions must be captured or reverted first. Before applying drive values, checks native joint-transform anchor offset magnitude/axiality and zero-angle axis collinearity. Native joint matrices have explicit provenance; physical angle/flip sense requires independent occurrence-geometry acceptance.", "properties": {@@ -12213,9 +12362,53 @@ "op": "inspect" }, "input": {- "op": "required enum: inspect|inspect_entity|new_design|parameter_set|parameter_create|health|export|transaction_probe|sketch_create|sketch_geometry|sketch_constraint|sketch_dimension|sketch_project|sketch_edit|plane_create|feature_create|feature_edit|entity_delete|parameter_batch|component_create|occurrence_insert|occurrence_update|occurrence_delete|component_metadata|joint_create|joint_edit|joint_delete|interference|bom|bom_export|cad_import|mcmaster_discover|mcmaster_open|material_list|material_assign|rigid_group_create|rigid_group_edit|rigid_group_delete|assembly_properties|geometry_query|measure|sketch_curve_edit|sketch_offset|construction_axis|sketch_diagnostics|joint_origin_create|joint_origin_edit|joint_origin_delete|regular_joint_create|regular_joint_edit|motion_link_create|motion_link_edit|motion_link_delete|configuration_inspect|render_scene_inspect|exploded_view_plan|exploded_preview_create|exploded_preview_sample|exploded_preview_show|exploded_preview_dispose|exploded_preview_inspect|efficiency_report|exploded_view_render|exploded_view_stills|render_image|cam_tools|cam_job|cam_inspect",+ "op": "required enum: inspect|inspect_entity|new_design|parameter_set|parameter_create|health|export|transaction_probe|sketch_create|sketch_geometry|sketch_constraint|sketch_dimension|sketch_project|sketch_edit|plane_create|feature_create|feature_edit|entity_delete|parameter_batch|component_create|occurrence_insert|occurrence_update|occurrence_delete|component_metadata|joint_create|joint_edit|joint_delete|interference|bom|bom_export|cad_import|mcmaster_discover|mcmaster_open|material_list|material_assign|rigid_group_create|rigid_group_edit|rigid_group_delete|assembly_properties|geometry_query|measure|sketch_curve_edit|sketch_offset|construction_axis|sketch_diagnostics|joint_origin_create|joint_origin_edit|joint_origin_delete|regular_joint_create|regular_joint_edit|motion_link_create|motion_link_edit|motion_link_delete|configuration_inspect|render_scene_inspect|exploded_view_plan|exploded_preview_create|exploded_preview_sample|exploded_preview_show|exploded_preview_dispose|exploded_preview_inspect|efficiency_report|exploded_view_render|exploded_view_stills|render_image|cam_tools|cam_job|cam_inspect|assembly_layout|ray_cast", "operationId": "required unique write ID; reuse unchanged for retry", "operationSchemas": {+ "assembly_layout": {+ "description": "Read one scope's parts for planning an assembly order, without changing anything: every visible occurrence directly under the scope occurrence (or the root), each with path, name, component name, grounded, the number of its children and its box (axis-aligned, root space, mm, from its bodies and its children's; null for a part with no bodies), plus the scope component's unsuppressed joints and as-built joints ([partOne, partTwo, \"joint\"|\"asBuilt\"], \"\" for the scope's own geometry) and rigid groups as part paths, frameBodies (bodies of the scope component itself), document and scopeName. Journaled like efficiency_report: the first guarded read after the add-in loads registers every entity of a big assembly and can outlive the relay's 60 s, so follow it with fusion_mech_operation_status. Returns the current target; pass it to ray_cast.",+ "properties": {+ "maxBodies": {+ "description": "Bodies read per part for its box, breadth first through its children (default 200; a body proxy's box costs about 26 ms on a big assembly). A part whose walk stopped there has boxCapped:true.",+ "maximum": 20000,+ "minimum": 1,+ "type": "integer"+ },+ "operationId": {+ "type": "string"+ },+ "scope": {+ "description": "Full path of the occurrence whose parts to list (\"Panel:1\", \"Cell:1+Panel:1\"). Empty or omitted: the root component's.",+ "type": "string"+ },+ "sessionId": {+ "type": "string"+ },+ "target": {+ "additionalProperties": false,+ "description": "Optional, as for inspect: the active document must be this one, and the model at revision when one is given. The reply carries the current target either way.",+ "properties": {+ "documentId": {+ "type": "string"+ },+ "revision": {+ "description": "Refuse with stale_revision unless the model is still at this revision (costs the revision digest, about 5 s on a 4,579-occurrence assembly).",+ "type": "string"+ }+ },+ "required": [+ "documentId"+ ],+ "type": "object"+ }+ },+ "required": [+ "op",+ "sessionId",+ "operationId"+ ],+ "type": "object"+ }, "assembly_properties": { "description": "High-accuracy physical properties of whole root assembly (default) or an exact Component/Occurrence/BRepBody. Returns mass kg, volume mm3, area mm2, center of mass mm, mean density kg/mm3 and definition/instance coordinate scope.", "properties": {@@ -18529,6 +18722,111 @@ ], "type": "object" },+ "ray_cast": {+ "description": "Cast batches of rays against named occurrences, without changing anything (assembly planning: what a part seats on, what blocks its way in, whether a tool reaches a screw). Each ray is cast against each occurrence it names, in that occurrence's own component coordinates, about a millisecond on a part, where a ray against a big subassembly's component (thousands of bodies) took a quarter of a second. Returns hits: a row per ray answered, [[occurrence index, distance in mm rounded to 0.01], ...], one entry per occurrence the ray meets within length (its nearest hit), nearest first; a ray that starts inside a body meets that body's exit face. Also cast (rays answered, in order), complete, missing (indexes with no occurrence at that path), failed (casts Fusion refused) and seconds. Not journaled: it stops at budgetSeconds or before its reply passes 1 MiB (stopped: budget|reply_bytes) and the caller sends the remaining rays. Requests over the CLI's argument limit (128 KiB) go in several calls.",+ "properties": {+ "budgetSeconds": {+ "description": "Stop casting after this many seconds (default 30) so the reply comes back inside the relay's 60 s; the reply then has complete:false and answers the first `cast` rays. Send the rest in another call.",+ "maximum": 50,+ "minimum": 1,+ "type": "number"+ },+ "occurrences": {+ "description": "Full root-context paths of the occurrences the rays are cast against; each ray names its own by index.",+ "items": {+ "type": "string"+ },+ "maxItems": 4096,+ "type": "array"+ },+ "rays": {+ "description": "Rays in root space. Each is cast against each occurrence it names, in that occurrence's own component coordinates; hidden bodies count too. An empty list only checks the target.",+ "items": {+ "additionalProperties": false,+ "properties": {+ "against": {+ "description": "Indexes into occurrences.",+ "items": {+ "maximum": 4095,+ "minimum": 0,+ "type": "integer"+ },+ "maxItems": 256,+ "minItems": 1,+ "type": "array"+ },+ "direction": {+ "description": "Direction (any nonzero length).",+ "items": {+ "maximum": 1000000000.0,+ "minimum": -1000000000.0,+ "type": "number"+ },+ "maxItems": 3,+ "minItems": 3,+ "type": "array"+ },+ "length": {+ "description": "Hits farther than this (mm) are dropped.",+ "exclusiveMinimum": 0,+ "maximum": 1000000000.0,+ "type": "number"+ },+ "origin": {+ "description": "Start point, mm.",+ "items": {+ "maximum": 1000000000.0,+ "minimum": -1000000000.0,+ "type": "number"+ },+ "maxItems": 3,+ "minItems": 3,+ "type": "array"+ }+ },+ "required": [+ "origin",+ "direction",+ "length",+ "against"+ ],+ "type": "object"+ },+ "maxItems": 8192,+ "type": "array"+ },+ "target": {+ "additionalProperties": false,+ "description": "From assembly_layout or inspect. The active document must be this one; with revision the model must still be at it. Without revision no digest is computed and the reply's target carries none.",+ "properties": {+ "documentId": {+ "type": "string"+ },+ "revision": {+ "description": "Refuse with stale_revision unless the model is still at this revision (costs the revision digest, about 5 s on a 4,579-occurrence assembly).",+ "type": "string"+ }+ },+ "required": [+ "documentId"+ ],+ "type": "object"+ },+ "unit": {+ "enum": [+ "mm"+ ],+ "type": "string"+ }+ },+ "required": [+ "op",+ "target",+ "unit",+ "rays"+ ],+ "type": "object"+ }, "regular_joint_create": { "description": "Create a geometry-solving regular joint in the root component using two distinct instance-context attachments or joint origins. All seven native motion families; offsets mm, angle and angular limits deg. Read back freshly resolved attachment graph, native parameters and root-space occurrence placement. Root/non-root sides must be unambiguous; both grounded and ancestor/descendant pairs are refused. Inferred joints are unsupported. Existing pending positions must be captured or reverted first. Before applying drive values, checks native joint-transform anchor offset magnitude/axiality and zero-angle axis collinearity. Native joint matrices have explicit provenance; physical angle/flip sense requires independent occurrence-geometry acceptance.", "properties": {
docs/MECHANICAL.md+14@@ -126,6 +126,20 @@ A [finite exploded-preview controller candidate](EXPLODED-PREVIEWS.md) now exist Fusion frames an orthographic image so that its shorter side spans the camera's `viewExtents`, centred on the target (measured at 1280 x 720, 960 x 960 and 720 x 1280, every point within a pixel); the projections in the reply follow that rule. It moves only the parts whose pose changes between images, about four moves per moved part in all. It takes up to 64 steps and 2048 moves (an animation takes 32 and 128) and tracks up to 4,096 moved parts with their parents. Like the animation, the images leave out joint, sketch and construction glyphs unless `glyphs:true` (see [Clean captures](#clean-captures)). Afterwards it reverts every move, restores the camera, verifies every part is home and reports `documentModified` like the animation. It is journaled. Measured on a 4,579-occurrence assembly: an electronics panel of 4 steps and 92 parts gave 9 images in 40 s with 368 moves. +## Plan an assembly order: layout and rays++Two reads give an assembly planner what it needs from the design. Neither changes anything.++`assembly_layout` lists one scope's parts: every visible occurrence directly under `scope`, a full occurrence path such as `Cell:1+Panel:1` (the root's when `scope` is omitted). Each part has its `path`, `name`, `component` name, `grounded`, the number of its `children` and its `box`: axis-aligned in root space, in mm, from the part's bodies and its children's, `null` for a part without bodies. The box is read breadth first from at most `maxBodies` bodies (default 200; a body's box costs about 26 ms on a 4,579-occurrence assembly), and `boxCapped` marks a part whose walk stopped there. The reply also lists the scope component's unsuppressed joints and as-built joints as `[partOne, partTwo, "joint" | "asBuilt"]` and its rigid groups, both as part paths (`""` is the scope's own geometry), with `frameBodies`, the bodies of the scope component itself. It is journaled like `efficiency_report` (`sessionId`, `operationId`): on a big assembly the first guarded read after the add-in loads registers every entity, about 45 s, so follow it with `fusion_mech_operation_status` once the relay's 60 s pass. `target` is optional, as for `inspect`, and the reply carries the current target.++`ray_cast` casts rays against named occurrences. `occurrences` lists full occurrence paths; each ray has an `origin` and a `direction` in root space (mm), a `length`, and `against`, indexes into `occurrences`. Each ray is cast against each occurrence it names in that occurrence's own component coordinates (the inverse of its root-space transform), hidden bodies included: about a millisecond on a part, where a ray against the component of a big subassembly (thousands of bodies, in a 4,579-occurrence assembly) took a quarter of a second. `hits` has a row per ray answered: `[index, distance]` for each occurrence the ray meets within `length` (its nearest hit), nearest first, distances in mm rounded to 0.01. A ray that starts inside a body meets that body's exit face. `missing` lists the indexes with no occurrence at their path, and `failed` counts the casts Fusion refused.++`ray_cast` is not journaled. A plan casts thousands of rays, and the journal keeps every request and result and never evicts (10,000 records, 64 MiB), so journaled batches would fill it within tens of plans. Instead a call stops at `budgetSeconds` (default 30, at most 50) or before its reply passes 1 MiB, answers the first `cast` rays and says `complete: false` and why (`stopped`); send the rest in the next call. Every call answers at least one ray. The `adom-bridge` CLI passes the arguments on its command line, which Linux caps at 128 KiB, so a big batch goes in several calls of a few hundred rays.++The guard is the usual one: the active document must be the target's, and with a `revision` the model must still be at it (`stale_revision` otherwise). Without a revision `ray_cast` computes no revision digest (about 5 s a call on a 4,579-occurrence assembly) and its reply's target carries none. A planner reads the layout, which returns the full target, casts with `{documentId}` alone, and checks the revision once at the end with a `ray_cast` that has the full target and no rays.++Measured through the relay: a `ray_cast` call answers in about 0.85 s round trip on a small design. A planner read a 15-part and an 11-part testbed (850 and 760 distinct rays) in five calls each; its whole plan took 7.1 to 7.8 s, where the same reads as modeling scripts took 11.3 to 11.7 s, with identical results.+ ## Render a picture `render_image` renders a photoreal image with Fusion's local renderer to an absolute `.png` or `.jpg` `filename`. The `view` is `iso` by default, or `iso-left`, `front`, `back`, `top`, `bottom`, `left` or `right`, each fitted to the design; `current` keeps the view as it is. It takes `width` and `height` (1920 x 1080 by default), `quality` from 25 to 100 (75 by default; Fusion refuses less than 25), `transparent` and `ground`. Fusion's renderer never draws joint, sketch or construction glyphs, but fitting a standard view counts them, so they are hidden while the view is fitted and back before the render starts (`glyphsHiddenForFit` in the reply; see [Clean captures](#clean-captures)). It is journaled, since a large high-quality render runs for minutes. A 1280 x 720 iso render at quality 60 took 28 s.
rust/bridge/src/mech.rs+50@@ -325,6 +325,56 @@ mod tests { assert_eq!(s["state"], "succeeded", "{s}"); } + #[test]+ fn a_ray_batch_is_validated_and_answered_without_a_journal_record() {+ let st = scratch("rays");+ session(&st, "S1", 7);+ let ray = json!({"origin": [30, 30, 100], "direction": [0, 0, -1], "length": 200, "against": [0]});+ let batch = |rays: Value| json!({"op": "ray_cast", "target": {"documentId": "d"}, "unit": "mm", "occurrences": ["Plate:1"], "rays": rays});+ let sent = RefCell::new(Value::Null);+ let r = mechanical(&st, &[7], &batch(json!([ray])), &|cmd, a| {+ assert_eq!(cmd, "mechanical");+ *sent.borrow_mut() = a.clone();+ json!({"success": true, "operation": "ray_cast", "data": {"hits": [[[0, 90.0]]], "cast": 1, "complete": true}})+ });+ assert_eq!(r["data"]["hits"], json!([[[0, 90.0]]]), "{r}");+ assert!(sent.borrow().get("_dispatch").is_none(), "a read carries no dispatch");+ assert!(!st.join("operations-v1.sqlite3").exists(), "a ray batch leaves no journal record");+ // a malformed ray, or one too many, is refused here and never reaches Fusion+ let mut too_many = Vec::new();+ too_many.resize(8193, ray.clone());+ for bad in [json!([{"origin": [0, 0], "direction": [0, 0, 1], "length": 1, "against": [0]}]),+ json!([{"origin": [0, 0, 0], "direction": [0, 0, 1], "length": 1, "against": ["0"]}]),+ json!([{"origin": [0, 0, 0], "direction": [0, 0, 1], "length": 0, "against": [0]}]),+ json!([{"origin": [0, 0, 0], "direction": [0, 0, 1], "against": [0]}]),+ json!([{"origin": [0, 0, 0], "direction": [0, 0, 1], "length": 1, "against": [0], "weight": 2}]),+ json!(too_many)] {+ let r = mechanical(&st, &[7], &batch(bad), &|_, _| panic!("refused before the add-in"));+ assert_eq!(r["errorCode"], "invalid_input", "{r}");+ }+ }++ #[test]+ fn an_assembly_layout_is_journaled_like_the_other_long_reads() {+ let st = scratch("layout");+ session(&st, "S1", 7);+ let args = json!({"op": "assembly_layout", "sessionId": "S1", "operationId": "l1", "scope": "Panel:1"});+ let r = mechanical(&st, &[7], &args, &|_, a| {+ let id = a["_dispatch"]["dispatchId"].as_str().unwrap().to_string();+ std::fs::create_dir_all(dispatch_dir(&st)).unwrap();+ std::fs::write(claim_path(&st, &id), json!({"state": "started", "observedDocument": {"documentId": "d"}}).to_string()).unwrap();+ let result = json!({"success": true, "sessionId": "S1", "operation": "assembly_layout", "operationId": "l1",+ "data": {"target": {"documentId": "d", "revision": "r"}, "units": [], "joints": [], "rigidGroups": []}});+ json!({"success": true, "dispatchId": id, "nativeExecutionStarted": true, "observedDocument": {"documentId": "d"}, "result": result})+ });+ assert_eq!(r["durability"]["terminalRecorded"], true, "{r}");+ assert_eq!(r["data"]["target"]["revision"], "r");+ assert_eq!(status(&st, &[7], &json!({"sessionId": "S1", "operationId": "l1"}))["state"], "succeeded");+ // journaled, so it needs the live session and an operation ID like any write+ let r = mechanical(&st, &[7], &json!({"op": "assembly_layout", "sessionId": "S0", "operationId": "l2"}), &|_, _| panic!("never dispatched"));+ assert_eq!(r["errorCode"], "session_mismatch");+ }+ #[test] fn writes_need_the_live_session_and_reads_do_not() { let st = scratch("session");
rust/core/tests/golden/mech_capabilities.json+150@@ -8016,6 +8016,155 @@ "op", "target" ]+ },+ "assembly_layout": {+ "type": "object",+ "required": [+ "op",+ "sessionId",+ "operationId"+ ],+ "properties": {+ "sessionId": {+ "type": "string"+ },+ "operationId": {+ "type": "string"+ },+ "target": {+ "type": "object",+ "required": [+ "documentId"+ ],+ "additionalProperties": false,+ "properties": {+ "documentId": {+ "type": "string"+ },+ "revision": {+ "type": "string",+ "description": "Refuse with stale_revision unless the model is still at this revision (costs the revision digest, about 5 s on a 4,579-occurrence assembly)."+ }+ },+ "description": "Optional, as for inspect: the active document must be this one, and the model at revision when one is given. The reply carries the current target either way."+ },+ "scope": {+ "type": "string",+ "description": "Full path of the occurrence whose parts to list (\"Panel:1\", \"Cell:1+Panel:1\"). Empty or omitted: the root component's."+ },+ "maxBodies": {+ "type": "integer",+ "minimum": 1,+ "maximum": 20000,+ "description": "Bodies read per part for its box, breadth first through its children (default 200; a body proxy's box costs about 26 ms on a big assembly). A part whose walk stopped there has boxCapped:true."+ }+ },+ "description": "Read one scope's parts for planning an assembly order, without changing anything: every visible occurrence directly under the scope occurrence (or the root), each with path, name, component name, grounded, the number of its children and its box (axis-aligned, root space, mm, from its bodies and its children's; null for a part with no bodies), plus the scope component's unsuppressed joints and as-built joints ([partOne, partTwo, \"joint\"|\"asBuilt\"], \"\" for the scope's own geometry) and rigid groups as part paths, frameBodies (bodies of the scope component itself), document and scopeName. Journaled like efficiency_report: the first guarded read after the add-in loads registers every entity of a big assembly and can outlive the relay's 60 s, so follow it with fusion_mech_operation_status. Returns the current target; pass it to ray_cast."+ },+ "ray_cast": {+ "type": "object",+ "required": [+ "op",+ "target",+ "unit",+ "rays"+ ],+ "properties": {+ "target": {+ "type": "object",+ "required": [+ "documentId"+ ],+ "additionalProperties": false,+ "properties": {+ "documentId": {+ "type": "string"+ },+ "revision": {+ "type": "string",+ "description": "Refuse with stale_revision unless the model is still at this revision (costs the revision digest, about 5 s on a 4,579-occurrence assembly)."+ }+ },+ "description": "From assembly_layout or inspect. The active document must be this one; with revision the model must still be at it. Without revision no digest is computed and the reply's target carries none."+ },+ "unit": {+ "type": "string",+ "enum": [+ "mm"+ ]+ },+ "occurrences": {+ "type": "array",+ "maxItems": 4096,+ "items": {+ "type": "string"+ },+ "description": "Full root-context paths of the occurrences the rays are cast against; each ray names its own by index."+ },+ "rays": {+ "type": "array",+ "maxItems": 8192,+ "description": "Rays in root space. Each is cast against each occurrence it names, in that occurrence's own component coordinates; hidden bodies count too. An empty list only checks the target.",+ "items": {+ "type": "object",+ "required": [+ "origin",+ "direction",+ "length",+ "against"+ ],+ "additionalProperties": false,+ "properties": {+ "origin": {+ "type": "array",+ "minItems": 3,+ "maxItems": 3,+ "items": {+ "type": "number",+ "minimum": -1000000000.0,+ "maximum": 1000000000.0+ },+ "description": "Start point, mm."+ },+ "direction": {+ "type": "array",+ "minItems": 3,+ "maxItems": 3,+ "items": {+ "type": "number",+ "minimum": -1000000000.0,+ "maximum": 1000000000.0+ },+ "description": "Direction (any nonzero length)."+ },+ "length": {+ "type": "number",+ "exclusiveMinimum": 0,+ "maximum": 1000000000.0,+ "description": "Hits farther than this (mm) are dropped."+ },+ "against": {+ "type": "array",+ "minItems": 1,+ "maxItems": 256,+ "items": {+ "type": "integer",+ "minimum": 0,+ "maximum": 4095+ },+ "description": "Indexes into occurrences."+ }+ }+ }+ },+ "budgetSeconds": {+ "type": "number",+ "minimum": 1,+ "maximum": 50,+ "description": "Stop casting after this many seconds (default 30) so the reply comes back inside the relay's 60 s; the reply then has complete:false and answers the first `cast` rays. Send the rest in another call."+ }+ },+ "description": "Cast batches of rays against named occurrences, without changing anything (assembly planning: what a part seats on, what blocks its way in, whether a tool reaches a screw). Each ray is cast against each occurrence it names, in that occurrence's own component coordinates, about a millisecond on a part, where a ray against a big subassembly's component (thousands of bodies) took a quarter of a second. Returns hits: a row per ray answered, [[occurrence index, distance in mm rounded to 0.01], ...], one entry per occurrence the ray meets within length (its nearest hit), nearest first; a ray that starts inside a body meets that body's exit face. Also cast (rays answered, in order), complete, missing (indexes with no occurrence at that path), failed (casts Fusion refused) and seconds. Not journaled: it stops at budgetSeconds or before its reply passes 1 MiB (stopped: budget|reply_bytes) and the caller sends the remaining rays. Requests over the CLI's argument limit (128 KiB) go in several calls." } }, "transactions": {@@ -8041,6 +8190,7 @@ } }, "writes": [+ "assembly_layout", "bom_export", "cad_import", "cam_job",
rust/core/tests/golden/mech_validation.json@@ -1 +1 @@
Reviews
Comments
Log in to comment.
No comments yet.