%unimsg 0 spatial/arm/v0 -- A six-axis robot arm, hand-authored in @vocab/spatial/v0. -- -- The vocabulary was read off sixteen formats and no file, and docs/42 tested -- it by transcribing five specifications rather than converting five files. -- This is the other test, and the one @vocab/vocabulary/v0's landing checklist -- asks for: small, written by hand, and exercising the thing the whole design -- rests on — a frame graph acting as a kinematic chain. -- -- WHAT IT DELIBERATELY STRAINS. A joint per frame, with limits in a unit that -- is not the frame's. A measured frame beside authored ones, so :tolerance and -- :measurement are both used rather than described. A collision instance -- alongside the visual ones, which is the URDF construct docs/42 found had -- nowhere to go. A morph target driven by a declared parameter, which is the -- glTF construct that broke the motion sketch. And a body carrying mass and an -- inertia tensor, which is a fact and not a simulation. -- -- Every pose here is a translation and an identity rotation, and that is not -- laziness: a rest pose is where the mechanism sits with every joint at zero, -- and the interesting rotations belong in a take — which is the SECOND BLOCK of -- this same document, not a second file. spatial @spatial/arm/v0 { title "Six-axis arm at rest" status :draft at 2026-08-30 description "A six-axis industrial arm in its zero configuration: a base column, two link shells, a wrist ball and a gripper blade, with a coarse collision hull over the upper arm and a camera mounted on the base." -- A micrometre, on a machine tool. The number is a fact about how finely -- this document records and the transcript is rounded onto it. precision { length mm 0.001 unitless 0.000001 rounding :half-even } canonical-frame -> @frames.base measurement { instrument "Faro Vantage tracker" at 2026-08-30 method "the camera mount surveyed against the base flange; every other frame is authored from the drawing" } frames { base { unit :m handedness :right up :z } shoulder { parent -> @frames.base pose { translate [ 0, 0, 0.1625 ] } joint { kind :revolute axis [ 0, 0, 1 ] limit { lower deg -180, upper deg 180 } effort nm 150 velocity 3.15 } } upper-arm { parent -> @frames.shoulder pose { translate [ 0, 0.138, 0 ] } joint { kind :revolute axis [ 0, 1, 0 ] limit { lower deg -150, upper deg 150 } } } forearm { parent -> @frames.upper-arm pose { translate [ 0, -0.131, 0.425 ] } joint { kind :revolute axis [ 0, 1, 0 ] limit { lower deg -150, upper deg 150 } } } wrist { parent -> @frames.forearm pose { translate [ 0, 0, 0.392 ] } joint { kind :revolute axis [ 0, 1, 0 ] limit { lower deg -180, upper deg 180 } } } tool-flange { parent -> @frames.wrist pose { translate [ 0, 0.127, 0 ] } joint { kind :fixed } } -- The one frame in this document that was measured rather than drawn, and -- the reason :measurement is present at all. camera-mount { parent -> @frames.base pose { translate [ 0.3, -0.25, 0.9 ] } tolerance { position mm 2 } } } parameters { gripper-open { doc "how far the blade is spread, from closed to fully open" default 0 lower 0 upper 1 } } materials { steel { base-colour "#8a8f98", metallic 1, roughness 0.4 } warning { base-colour "#d9a441", metallic 0, roughness 0.6 } } geometry { base-column { cylinder { radius 0.075, length 0.1625 } } upper-arm-shell { box { extent [ 0.09, 0.09, 0.425 ] } } forearm-shell { box { extent [ 0.08, 0.08, 0.392 ] } } wrist-ball { sphere { radius 0.045 } } collision-hull { box { extent [ 0.1, 0.1, 0.45 ] } } -- A five-vertex blade, and the only mesh here. Its :targets entry is what -- a declared parameter is for: the tips spread and nothing else moves. gripper-blade { mesh { positions [ -0.02, -0.02, 0, 0.02, -0.02, 0, 0.02, 0.02, 0, -0.02, 0.02, 0, 0, 0, 0.06 ] indices [ 0, 1, 2, 0, 2, 3, 0, 1, 4, 1, 2, 4, 2, 3, 4, 3, 0, 4 ] attributes { face { kind :uv, values [ 0, 0, 1, 0, 1, 1, 0, 1, 0.5, 0.5 ] } } targets { open { deltas [ -0.01, -0.01, 0, 0.01, -0.01, 0, 0.01, 0.01, 0, -0.01, 0.01, 0, 0, 0, 0 ] parameter -> @parameters.gripper-open } } } } } bodies { forearm-body { doc "the forearm casting, weighed rather than modelled" frame -> @frames.forearm mass kg 3.7 centre-of-mass [ 0, 0, 0.196 ] inertia [ 0.0666, 0.0666, 0.0047, 0, 0, 0 ] } } scene [ { geometry -> @geometry.base-column, frame -> @frames.base, purpose :visual, material -> @materials.steel } { geometry -> @geometry.upper-arm-shell, frame -> @frames.upper-arm, purpose :visual, material -> @materials.steel } { geometry -> @geometry.forearm-shell, frame -> @frames.forearm, purpose :visual, material -> @materials.steel, body -> @bodies.forearm-body } { geometry -> @geometry.wrist-ball, frame -> @frames.wrist, purpose :visual, material -> @materials.steel } { geometry -> @geometry.gripper-blade, frame -> @frames.tool-flange, purpose :visual, material -> @materials.warning } { geometry -> @geometry.collision-hull, frame -> @frames.upper-arm, purpose :collision } ] -- The arm at rest, asserted rather than described. The flange sits at -- y = 0.138 - 0.131 + 0.127 and z = 0.1625 + 0.425 + 0.392, which is the -- kind of sum a reader should not have to do and a checker should. checks [ { doc "the tool flange sits where the drawing says, with every joint at zero" assert :position-of of -> @frames.tool-flange in -> @frames.base expect [ 0, 0.134, 0.9795 ] } { doc "the camera sees the base from where it was surveyed" assert :position-of of -> @frames.camera-mount in -> @frames.base expect [ 0.3, -0.25, 0.9 ] } { doc "nothing was silently dropped: five visual instances and one collision hull" assert :entry-count of -> @spatial/arm/v0 expect 6 } ] } -- The take that drives it, BESIDE it rather than in a second file. It carries -- no :binds and no digest: two blocks in the same bytes cannot drift apart, so -- there is nothing for a pin to defend against. See -- a-take-belongs-beside-its-scene in @vocab/motion/v0. motion @motion/arm-sweep/v0 { title "Arm sweep, one second" status :draft at 2026-08-30 doc "the shoulder sweeps 45 degrees while the forearm drops 30 and the blade opens, in one second at ten hertz" interval [ 0, 1 ] rate Hz 10 channels [ -- the shoulder sweeps 45 degrees about its own axis { drives -> @frames.shoulder interpolation :linear rotate [ 0.00000000, 0.00000000, 0.00000000, 1.00000000, 0.00000000, 0.00000000, 0.03925982, 0.99922904, 0.00000000, 0.00000000, 0.07845910, 0.99691733, 0.00000000, 0.00000000, 0.11753740, 0.99306846, 0.00000000, 0.00000000, 0.15643447, 0.98768834, 0.00000000, 0.00000000, 0.19509032, 0.98078528, 0.00000000, 0.00000000, 0.23344536, 0.97236992, 0.00000000, 0.00000000, 0.27144045, 0.96245524, 0.00000000, 0.00000000, 0.30901699, 0.95105652, 0.00000000, 0.00000000, 0.34611706, 0.93819134, 0.00000000, 0.00000000, 0.38268343, 0.92387953, ] } -- and the forearm drops 30 degrees while it does { drives -> @frames.forearm interpolation :linear rotate [ 0.00000000, -0.00000000, 0.00000000, 1.00000000, 0.00000000, -0.02617695, 0.00000000, 0.99965732, 0.00000000, -0.05233596, 0.00000000, 0.99862953, 0.00000000, -0.07845910, 0.00000000, 0.99691733, 0.00000000, -0.10452846, 0.00000000, 0.99452190, 0.00000000, -0.13052619, 0.00000000, 0.99144486, 0.00000000, -0.15643447, 0.00000000, 0.98768834, 0.00000000, -0.18223553, 0.00000000, 0.98325491, 0.00000000, -0.20791169, 0.00000000, 0.97814760, 0.00000000, -0.23344536, 0.00000000, 0.97236992, 0.00000000, -0.25881905, 0.00000000, 0.96592583, ] } -- the blade opens across the whole sweep. A parameter, not a frame { drives -> @parameters.gripper-open interpolation :linear values [ 0.00000000, 0.10000000, 0.20000000, 0.30000000, 0.40000000, 0.50000000, 0.60000000, 0.70000000, 0.80000000, 0.90000000, 1.00000000 ] } ] checks [ { doc "the blade is fully open at the end" assert :value-at of -> @parameters.gripper-open at 1 expect 1 } { doc "every sample of the shoulder is inside the limit the scene declares" assert :limit-holds of -> @frames.shoulder at 0 expect true } ] }