# cad memory

## ⭐ SESSION STATE 2026-06-23 PT (READ FIRST to resume)
- **TOM generator now RUNS end-to-end** and reproduces committed `tom.urdf` **byte-identical** (9 links/8 joints/24 meshes). The long-standing `gen_urdf` AssertionError was **NOT build123d/branch** — it was **5 custom-part STEPs being unhydrated LFS stubs** (`.lfsconfig` excludes `models/**`). Fix: `git lfs pull -I models/robots/tom/v2/parts -X ""`. Build via `smith300_para_stuff/tom_build.sh <gen.py> <out.urdf>` (reuses `.venv_cad` + develop `cadpy`/`cadpy_metadata` on PYTHONPATH + inert center() shim `tom_patch/`). Validate via `validate_tom.sh`.
- **Two-servo connector URDF DONE+polished**: `smith300_para_stuff/two_servo.urdf` (servo+holder_wrap+yoke_exact ×2, 1 revolute joint, mating flush+centered). Viewer: `cad.omidlab.net/?dir=/data/cameron/repos/smith300_para_stuff&file=two_servo.urdf`.
- **ACTIVE 24h exploration loop** (cron `afcc5898`, every 20 min, deadline 2026-06-24T18:15 PT / epoch 1782350134, self-deletes). Plan+goals G1–G7 in `agents/cad/tom_loop_state.md`; progress in `agents/cad/outbox.md`.
- TOM = sheet-metal (SendCutSend) robot; our printed Boolean-shell holder + exact-hole yoke is the deliberate divergence, not a copy target.
RESUME FACTS: lab `ssh cameronsmith@100.74.71.38`; venvs `/data/cameron/repos/cad_experiments/.venv_cad` (build123d, py3.12) + `/data/cameron/repos/custom_robot_building/.venv` (mujoco, py3.10). Run build123d there, render with mujoco venv (MUJOCO_GL=egl). Servo STEP = `cad_experiments/parts/waveshare_feetech_st3215_servo.step`; holes via `smith300_para_stuff/servo_holes.py` (horn +Y @ (-25.5,0) R7, idler back R7, body corners). Parts + exact_module.urdf in `smith300_para_stuff/`. Viewer = `cad.omidlab.net` (tmux `cadviewer` on lab, `~/node20/bin/node <cad-viewer>/scripts/viewer/backend/server.mjs --host 0.0.0.0 --port 4178 --dir /data/cameron/repos/cad_experiments`). See entries below for full detail ([[fidex_notes]] for fidex).


Notes on URDF conventions, MuJoCo gotchas, print/iteration lessons, parts catalog, what's worked + what hasn't. Append-only.

## 2026-06-23 — Ops: code lives on lab; git ops over sshfs are forbidden
`/data/cameron/repos/` on `omid-fleet` is `fuse.sshfs` from lab (`100.74.71.38`) over a bandwidth-limited Tailscale link. Cloning/large-pulling directly onto it **truncates pack files** ("premature end of pack file, N bytes missing") and leaves the clone process **wedged in uninterruptible D-state** holding `index.lock` (wchan `request_wait_answer` = FUSE).

**Decision (Cameron, 2026-06-23):** all fleet code lives on lab. VPS-side workflow:
- **Small reads via sshfs** — fine.
- **Small-file writes via sshfs** (single-file atomic ops like editing a Python script, writing an MJCF/STEP file) — fine.
- **Big git ops** (clone, large pull, large checkout, large rebase) — dispatch on lab via `ssh cameronsmith@100.74.71.38 'cd /data/cameron/repos/<repo> && git <cmd>'`. Tailscale SSH was unlocked 2026-06-23; auth holds ~12h then needs re-auth (Cameron clicks the URL).

Verify mounts: `mount | grep /data/cameron` — `agents_stuff`/`vault`/`~` are local, `repos`/`para/robot`/`yukon_remote` are sshfs.

The earlier `~/cad_repos/` workspace on VPS local disk was a temporary workaround during the Tailscale-SSH-blocked window. After lab-side clones are in, those local copies should be deleted.

## 2026-06-23 — Lab render pipeline + reaching the Mac (verified working)
- **Lab env:** `uv` venv at `/data/cameron/repos/custom_robot_building/.venv` (numpy+mujoco+pillow). Lab (`phe108-yuewang-01`) has 2× RTX 6000 Ada but **no display** → use **offscreen EGL**: `MUJOCO_GL=egl .venv/bin/python ...`. Default offscreen framebuffer is **640×480**; bigger needs `<global offwidth=.. offheight=..>` in the MJCF `<visual>`.
- **Reusable renderer:** `2ourso100/render_baseline.py <model.xml> <out.png> [w h]` — loads home keyframe, free camera, saves PNG.
- **Reach the Mac from lab:** `ssh mac "<cmd>"` works **from lab** (ControlMaster alias in lab's `~/.ssh/config`; Mac = `Mac.lan` @ 100.66.139.38). **From the VPS `ssh mac` FAILS** (no key). So Mac↔fleet file moves go **Mac→lab** (`rsync -a "mac:<src>" <lab-dst>`), run on lab. The `mac` agent's sshfs mount of the Mac is at lab `~/mnt/mac/`.
- **6-DOF arm meshes:** the repo shipped only EEF meshes; the 6 `long_singlejointed` STLs (`link1_holder/clamp`, `link2_clamp_fixed`, `link3_clamp`, `link4_connector_fixed`, `link5_connector`) were pulled from Mac `~/Projects/robotics_testing/menagerie_testing/mujoco_menagerie/2ourso100/assets/` into the repo `assets/` (2026-06-23). 6-DOF arm now loads (nq=6, njnt=6) + renders. Baselines: `2ourso100/{long_6dof_baseline,long_6dof_extended,eef_baseline}.png`.

## 2026-06-23 — text-to-cad urdf skill: VERIFIED + our-arm converter built
- **urdf skill** (`plugins/cad/skills/urdf`): write a top-level `gen_urdf()` returning an `ElementTree.Element`/XML; run `python <skill>/scripts/urdf <source.py>` → generates sibling `.urdf` + validates (single-rooted tree, links-1 joints, revolute needs `<limit>`, axis nonzero, meshes resolved relative to the `.urdf` dir, optional inertials must obey triangle ineq). **Std-lib only** → runs on bare system python3; but if the source imports numpy etc., run it with the venv python.
- **Scratch workspace:** `/data/cameron/repos/cad_experiments/` (on lab, visible via sshfs; outside both git repos). `assets` there is a symlink → custom_robot_building `2ourso100/assets`.
- **Our-arm converter:** `cad_experiments/arm6dof_ours.py` imports the MJCF generator and remaps MuJoCo's nested body tree (link→shoulder(joint)→link) to a URDF joint tree. Key rule: **URDF link frame must == the MuJoCo *shoulder* (hinge) frame** (URDF child frame == joint frame), so meshes rigid in that frame get baked offsets; gives 7 links / 6 revolute joints. Produces `arm6dof_ours.urdf` (validated) using our `ours`/`link*` STLs + `sts3215_03a_v1.stl` servo. Verified by rendering in MuJoCo (`verify_urdf_render.py`, inject `<mujoco><compiler discardvisual=false meshdir=.>` + nominal inertials) — matches the MJCF baseline pose. Render PNGs in cad_experiments/.
- **Gotcha:** MuJoCo discards URDF *visual* meshes unless `discardvisual="false"`, needs nonzero inertia on moving bodies (inject nominal for render), and `meshdir` joins with the mesh `filename` (don't double the `assets/` prefix).

## 2026-06-23 — build123d (cad skill) on lab + mesh-vs-BREP finding
- **Env:** py3.12 venv `cad_experiments/.venv_cad` (build123d 0.11 + cadquery-ocp + editable cadpy@0.3.6 from `<cad skill>/scripts/packages/cadpy` + playwright). cad skill needs py≥3.11. `scripts/step <src.py>` (define `gen_step()` returning a build123d Solid/Compound) → STEP + hidden GLB/topology sidecars; `scripts/inspect`, `scripts/snapshot` for validation.
- **Two venvs now:** `.venv` (py3.10, numpy/mujoco/pillow — MJCF gen + rendering) and `.venv_cad` (py3.12, build123d — STEP gen). They're separate; export STL with `.venv_cad`, render with `.venv`.
- **KEY finding — build123d vs our mesh holder:** `build123d.import_stl(ours_base_holder.stl)` loads it but as a **non-solid `Face`** (triangulated, is_valid=False); **cannot boolean-fuse** a parametric solid to it. So: build123d = *new parametric connectors* (STEP, recommended); fusing geometry *onto* the existing Blender-mesh holder = a **mesh boolean** (Blender / trimesh+manifold3d), not build123d. Compose them as separate URDF links/assembly, or mesh-merge only for a single printed piece.
- Demo connector: `cad_experiments/connector_v1.py` → STEP+STL (parametric plate+riser+M3 holes). Bolt pattern is a placeholder until we have the holder's real hole coords/CAD.

## 2026-06-23 — Real STS3215 servo STEP via step.parts skill
- text-to-cad `step-parts` skill queries `https://api.step.parts` (16847 parts, electronics-heavy). `STS3215` returns 0; the right id is **`waveshare_feetech_st3215_servo`** ("Waveshare Feetech ST3215 Servo" = the Feetech STS3215 Cameron uses). Aliases that hit: `ST3215`, `3215`, `family=feetech` (69 servos). Also `waveshare_feetech_st3215_hs_servo` (high-speed variant).
- Download: `.venv_cad/bin/python <step-parts skill>/scripts/download_step_part.py --id waveshare_feetech_st3215_servo --download --out-dir parts` → `parts/waveshare_feetech_st3215_servo.step` (3.0MB, sha256 checksum-verified). Lab can reach api.step.parts.
- Imports in build123d as a **clean BREP solid** (unlike our mesh holder): bbox **45.2×37.8×24.7mm**, vol 42.6cm³. `export_stl(..., tolerance=0.1, ascii_format=False)` → keep <200k faces or MuJoCo's STL decoder rejects it (default ASCII fine export is 17MB/>200k faces).
- **Connector experiment:** `cad_experiments/connector_v2_joint.py` = parametric bracket (build123d) bridging servo A→servo B into a perpendicular joint module; `gen_step()` = bracket, `build_assembly()` = servo+bracket+servo. Rendered (`joint_module.png`) showing connector-on-top-of-servo→next-servo. **Geometry is approximate** — bolt pattern/cradle are placeholders; needs the servo-holder's real mounting-hole coords (or holder CAD/STEP) and confirmation of how connectors attach (servo horn bolt circle vs holder body) to be print-accurate.

## 2026-06-23 — Mounting holes ARE in the STEP + minimal 2-link robot built
- **Hole coords come from the STEP, not online.** For BREP/STEP parts, mounting holes are real cylindrical faces — extract axis+radius+position via `OCP.BRepAdaptor.BRepAdaptor_Surface` + `GeomAbs_Cylinder` (see `cad_experiments/servo_features.py`). Filter by radius for bolt holes; a detailed servo STEP has ~471 cylinders (gears/fillets) so isolate the *outer* mounting ones. STL meshes (our Blender holder) have NO hole metadata — must fit cylinders or get a STEP.
- **ST3215 real interface (from its STEP):** output horn = 4-hole bolt circle **R=7mm centered (-25.5, 6, 0), axis +Y**, central spline bore; coaxial idler bearing on the -Y face. Body mount holes (rect pattern) on the ±Y faces. So the servo's **output/joint axis = +Y through (-25.5, 0, 0)**.
- **Minimal 2-link robot (built + verified):** `cad_experiments/two_link.py` holds shared constants (`JOINT1_XYZ`, `JOINT1_AXIS`, `SERVO_B_*`) + `gen_step()` connector (horn plate w/ R7 bolt circle + riser + top pad). Outputs, all consistent:
  - `connector_min.stl` (printable connector), `two_link_urdf.py`→`two_link_urdf.urdf` (validated by urdf skill, 2 links/1 revolute joint), `two_link_mjcf.py`→`two_link.xml` (MJCF, hinge + position actuator).
  - Animated both: `animate_urdf.py` sweeps a joint→GIF; URDF and MJCF filmstrips match (base servo fixed, connector+servoB sweep ±1.4rad about the real horn axis). `two_link_anim.gif`, `two_link_mjcf.gif`.
- **Pattern:** keep ONE shared constants module; generate URDF + MJCF from it so they never drift. Emit MJCF/URDF with the build123d venv (imports the constants module), render with the mujoco venv.

## 2026-06-23 — CAD Viewer = the visualization interface (browser, interactive)
- **Tool:** text-to-cad `cad-viewer` skill — browser viewer for `.step/.stl/.urdf/.glb/.3mf/.gcode/.dxf/.srdf/.sdf`. URDF gets **movable joint sliders + reset + copy joint values** (no more offscreen-render round-trips). STEP/STL get orbit/pan/zoom, wireframe/x-ray, screenshots.
- **Runtime:** just `node backend/server.mjs --host <h> --port <p> --dir <root> --shutdown-after 12h` serving the prebuilt `dist/` — no Vite/npm install. **Lab's system node is v12 (too old)**; installed Node 20 at `~/node20/` on lab (prebuilt tarball, no root). 
- **Running on lab** in tmux session `cadviewer`: `~/node20/bin/node <cad-viewer>/scripts/viewer/backend/server.mjs --host 0.0.0.0 --port 4178 --dir /data/cameron/repos/cad_experiments --shutdown-after 12h`. Launch gotcha: tmux pane shell is `sh` → no `|&`/bashisms; use `> log 2>&1`. Catalog endpoint reads any abs dir via `?dir=`; assets fetch by ABSOLUTE path.
- **Reach it (LIVE):** public `https://cad.omidlab.net/?dir=/data/cameron/repos/cad_experiments&file=<rel>` (no tailnet needed), or tailnet `http://100.74.71.38:4178/...`. Route: `cad.omidlab.net → 100.74.71.38:4178` in VPS `~/.cloudflared/config.yml` (before `*.omidlab.net`) + DNS CNAME; manager reloaded cloudflared via **systemctl** (2026-06-23, clean ~3s drain). To re-point the viewer at a different dir, restart the lab tmux `cadviewer` with a new `--dir` (or just use `?dir=` in the URL — it reads any absolute dir).

## 2026-06-23 — MESH UNITS TRAP: sts3215_03a_v1.stl is in METERS, not mm
In custom_robot_building's MJCF, holder/clamp STLs are in **mm** (`scale="0.001"`) but the servo mesh `sts3215_03a_v1.stl` is loaded **unscaled** = it's authored in **meters**. (Same servo as `ours_base_servo.stl` which IS mm.) Trap: applying the global mm→m 0.001 scale to the servo too shrinks it 1000× (~0.045mm → invisible). Symptom: "servos missing from the URDF" — they were there but microscopic. Fix in `arm6dof_ours.py`: `SERVO_SCALE="1 1 1"` for the servo mesh, `0.001` for holder/clamp. Always check per-mesh scale in the source MJCF before reusing a mesh.

## 2026-06-24 — TOM = text-to-cad's reference servo-arm generator (THE example)
The functional printed robot Cameron asked about = **TOM**, on text-to-cad **`develop`** branch (not main), `models/robots/tom/v2/`. Parametric build123d servo arm from sts3215/sts3250, emits STEP/URDF/SRDF + sheet-metal DXF. Checked out to a worktree: **`/data/cameron/repos/ttc-develop`** (on develop); LFS meshes need `git lfs pull -I <path> -X ""` (`.lfsconfig` fetchexclude=models/**). Viewable: `cad.omidlab.net/?dir=/data/cameron/repos/ttc-develop/models/robots/tom/v2&file=tom_with_gripper.urdf` (also tom.urdf, tom_double.urdf).
- **Canonical parts = exactly what we hand-built:** `parts/servo_horn_yoke.py` (yoke), `servo_end_mount.py` (holder), `link_bracket*.py`, `base_plate.py`; `assemblies/*`; `robot_common/robot_arm.py`, `keyed_connection.py`. Top generators `tom.py`/`tom_with_gripper.py`/`tom_double.py` (gen_step/gen_urdf/gen_srdf).
- **Validates our work:** servo_horn_yoke.py uses `SERVO_HORN_AXIS_X_MM = -25.5` = the exact horn axis I extracted. Selectors `xcaf:1.4`/`1.5` for upper/lower horn faces; M3 clearance r=1.7, horn center clearance r=3.25.
- **Key fab difference:** TOM parts are **sheet metal** (ezdxf flat patterns + bends, ~2.03mm sheet), NOT printed blocks. Adapt geometry if printing.

## 2026-06-24 — Holder = single wrapping shell (Cameron feedback)
Cameron rejected the bolt-plate holder (3 floating parts, missed the servo, collided with the yoke). Fix = **`holder_wrap.py`**: ONE Boolean collar (outer box − (body bbox + 0.4mm clearance)) wrapping all 4 XZ sides of the body, in the middle-Y region (Y[-25,6]) so it's open at the ±Y output faces and never overlaps the yoke. Moved `yoke_exact` spine out to x=-45 to clear the collar. Verified collision-free through joint rotation (`exact_module_sweep.png`). `exact_module.urdf` updated to holder_wrap; live at cad.omidlab.net. Lesson: holder must wrap with print clearance as one solid, NOT bolt-plates. TODO: holder is a plain rectangular collar (could round/lighten + add the attachment to the previous yoke for chaining).

## 2026-06-24 — EXACT fit from STEP screw holes (Cameron: never approximate)
Extracted ALL exact screw holes from the ST3215 STEP (`servo_holes.py` → BREP cylinders → `hole_markers.json`). In the STEP frame: output axis **+Y through (-25.5, 0)**; **horn bolt circle** center (-25.5,6,0) R7 4 holes (front, y=9.6/holes y=6); **idler bolt circle** center (-25.5,-27.7,0) R7 (back, y=-28.2); **body mount holes** 4 corners front (-17.2/3.5, 6.4, ±10.2) + back (-17.2/7.2, -25.6, ±10.2). Viz: `servo_holes_viz.png`.
- **`yoke_exact.py`** — yoke bolts to the exact horn+idler circles (front+back plates at those holes, spine on -X past the body, link out). `yoke_exact_fit.png` shows bolts through the real holes.
- **`holder_exact.py`** — holder bolts to the exact 4 corner body-mount holes (front+back U on the +X portion, clear of the horn at -25.5).
- `module_exact.png`: holder (body, +X) + yoke (output axis, -X) side-by-side on the servo, output rotates +Y between them. ALL holes exact.
- **Lesson:** build connectors on the STEP (exact BREP holes), NOT the mesh servo (only eyeballed centers). The earlier "approximate" was using the mesh frame. NB: this is the STEP servo frame (axis +Y); the robot's `sts3215_03a_v1.stl`/`ours_base_servo` is a different frame — mapping TBD.

## 2026-06-24 — Wrap-around module: yoke connector + U-holder (Cameron's correction)
Cameron: connector/holder must WRAP the servo on BOTH sides (yoke gripping horn+idler), not a 1-sided plate on one horn. Built parametric build123d parts in `smith300_para_stuff/`:
- **`yoke.py` `make_yoke(link_len)`** — C-bracket gripping the output on both sides: top plate on horn (z=20.2), bottom plate on idler (z=-19.4), both with R7 bolt circle + Ø9 bore at the axis (12,0), spine on +X, structural link extending +X. Built in the servo mesh frame.
- **`holder.py` `make_holder()`** — U-cradle wrapping the body's ±Y faces + back wall + floor (open ±Z for the output/yoke).
- Verified: `module_param.png` (one servo: body in holder, output in yoke), `two_servo_wrap.png` (servo0+holder0 → yoke → servo1+holder1). NOTE the mesh-fit approach failed (double_servo_holder/clamp don't share an assembly frame); built parametric around the MEASURED servo geometry instead. Servo2 currently placed parallel (could be perpendicular for an arm bend); yoke-link↔holder-back mating is approximate.

## 2026-06-24 — Real module geometry (ours_base_holder + ours_base_servo)
Found empirically (renders in smith300_para_stuff/): **servo output horn = +Z top face, center (12, 0, 20.2)mm, axis +Z, ~R7 bolt circle** (idler on -Z). A connector with its horn flange placed at (12,0,20.2) bolts onto it cleanly (`conn_on_horn.png`). **ours_base_holder bbox** min[-22.4,18.6,3.9] max[22.4,72,30.5] — it's offset entirely to +Y, a side/back brace, NOT a wrap cradle. **OPEN (needs Cameron):** (1) exact servo↔holder assembly pose — identity puts holder beside servo w/ a 6mm gap; the MJCF `HOLDER_POS/HOLDER_QUAT` relative pose grips the side better (`holder_fit_mjcf.png`) but unconfirmed; (2) how a connector mounts to the holder (which face/holes). These are design facts only Cameron knows; the earlier abstract-kinematic connectors floated because they ignored this real geometry.

## 2026-06-24 — Parametric MODULAR robot generator (resembles smith300)
`smith300_para_stuff/param_robot.py`: rebuilds the smith300 arm from a **constant reusable holder** (`ours_base_holder.stl`) + **generated build123d connectors** (straight bars spanning each link's input→output joint, cross-section 30×16mm) + the **custom gripper** (eef meshes kept). Servo = real sts3215 mesh. Kinematics (joint dirs/orientations) parsed from `smith300.urdf` (avoids the cv2-heavy config import); **LINK LENGTHS are the free params** (`scale` arg or per-joint). Servo+holder are anchored to each link's OUTPUT joint so they track when lengths scale (`T_servo = T_outjoint_scaled @ inv(T_outjoint0) @ servo_pose`). Holder placed via constant `HOLDER_ON_SERVO = inv(T_servo_mjcf) @ T_holder_mjcf`.
- **Verified:** generates `smith300_modular.urdf` (8 links/7 joints, loads nq=7); link-frame positions are **0.0mm identical to smith300** at the home pose (kinematics provably correct); `x1.5` lengths = exactly 1.5× and servos/holders track. Renders: `modular_hero.png`, `resemblance.png`, `modular_scale_compare.png`. Viewable: `cad.omidlab.net/?dir=/data/cameron/repos/smith300_para_stuff&file=smith300_modular.urdf`.
- **Mature connectors (2026-06-24):** `connector_part.py` `make_connector(L)` — a real bracket: horn flange (R7 4-hole bolt circle + Ø9 spline bore), structural beam w/ lightening holes, holder flange (4 M3 holes), 2mm filleted junctions. Built canonically along +Z; param_robot orients it per link via `align_R` (URDF origin rpy). Is a proper text-to-cad `cad`-skill part — has `gen_step()`, `scripts/step connector_part.py` → validated `connector_part.step`. Gotcha: junction fillet must be ≤ flange thickness (5mm fillet on 5mm flange fails; use 2mm). Robot regenerated w/ mature connectors (`modular_mature_hero.png`); parametric x1.5 still works.
- **Next refinements:** add ArUco board pockets onto connectors (fidex); make holder flange actually mate ours_base_holder's holes; gripper parametric; per-connector flange orientation to truly face horn vs holder.

## 2026-06-23 — fidex pipeline notes → see [[fidex_notes]]
Full abstraction of FiducialExoskeletons (marker-per-link → single-image camera+joint-angle estimation) in `vault/fleet/agents/cad/fidex_notes.md`. Key for our work: each link carries a known ArUco board at a known link→board offset; parametric CAD should emit the board pocket + its pose + board spec as one source of truth per link. Also: general reusable **MJCF→URDF translator** at `cad_experiments/mjcf_to_urdf.py` (carries per-mesh scale, box half→full extents, aruco textured boxes, folds no-joint frame bodies). smith300 URDF at `smith300_para_stuff/smith300.urdf` (8 links/7 joints). Viewer can't serve `.png` textures → aruco shows as positioned boxes (textures only in MuJoCo renders); GLB-quad bake is the fix later.

## 2026-06-23 — smith300 = CURRENT robot (the long_singlejointed one I converted is OLD)
- **Modern build:** Mac `~/Projects/robotics_testing/smith300_para_stuff/` (copied core to lab `/data/cameron/repos/smith300_para_stuff/`, assets pulled from Mac's `mujoco_menagerie/2ourso100/assets`). It's the SAME MJCF generator (`generate_example_twolink_custom.py`) with config `robot_configs/medium_single_redo.py`. `example_twolink.xml` = **6-DOF arm + 2-finger gripper** (nq=7: 6 arm joints + 1 gripper; EEF = `eef_base_with_handle.stl` + `eef_thumb_with_handle.stl` on shoulder6/shoulder7). Meshes `arm_redo_thin_long_pretty_medium_size_*`. Rendered → `smith300_para_stuff/smith300_render.png`.
- **ArUco fiducials baked onto every link** (`medium_single_redo.aruco_positions`): base = 3×3 GridBoard @50mm (id 0), each shoulder = 2×2 @15mm (ids 20,24,28,32,36,40), DICT_4X4_250. In the MJCF they're textured `<geom type="box">` planes on each link/clamp; also auto-generates a true-scale printable A4 PDF sheet (`aruco_print_sheet_medium_single_redo.{png,pdf}`).
- **FiducialExoskeletons** (`github.com/cameronosmith/FiducialExoskeletons`, cloned to lab; paper "Fiducial Exoskeletons: Image-Centric Robot State Estimation", cameronosmith.github.io/fidex): single uncalibrated RGB image → detect per-link ArUco boards → per-link poses → estimate **camera pose + all joint angles via IK** → render overlay (`demo.py`, `exo_utils.py`). Base `ExoskeletonConfig` (exo_configs/exoskeleton.py): per-link `LinkConfig`(robot mesh, exo/mount mesh, aruco offset, board). The markers ARE the robot's mounts — printed onto the structure, not stuck on.
- **Implication for our cad work:** when we (re)build the robot parametrically (build123d/URDF), target **smith300** (gripper + per-link fiducial mounts), not the old long_singlejointed. The clamp/connector parts should carry the ArUco board pockets.

## 2026-06-23 — Design decisions (Cameron)
Compute+code all on lab, VPS = hosting+memory only ([[code-lives-on-lab-2026-06-23]]). Use **build123d** for parts now (follow text-to-cad examples; Cameron prefers mujoco overall but is willing to try build123d). **URDF-primary**, derive MJCF later (easy convert). Blender stays in the loop as a geometry editor — Cameron edits a generated STL and returns it; keep STL-vs-parametric source-of-truth open, don't over-build the frame-correction machinery yet. **6-DOF arm = the default `long_singlejointed` config** (6 WORLD_LINKS / 6 hinges, `DEFAULT_JOINT_QPOS=[0,-1.5,1.5,0.75,0,0]`); `just_eef_umi` is EEF-only. STL handoff stays on lab under the repo; Cameron pulls to Mac or via sshfs (no agents_stuff handoff dir).

## 2026-06-23 — custom_robot_building is MJCF-first, SO-ARM100/101 lineage
`2ourso100/generate_example_twolink_custom.py` emits **MuJoCo XML** (`model="so101"`), not URDF-primary. Robot = repeated identical module: `link(holder mesh + STS3215 servo) → shoulder(hinge ±1.92rad + clamp mesh) → next link`, child link quat = inverse(parent shoulder quat). Add a DOF = add one `WORLD_LINKS` entry. STLs authored in **mm**, scaled `0.001`. The "hacky" knobs (`USE_LINK_{HOLDER,CLAMP}_COORD_FRAMES`, `HOLDER_COORD_USE_INVERSE`, `CLAMP_MESH_*`) exist to un-bake Blender-world-frame STLs into link/shoulder-local frame. ArUco boards (textures + true-scale A4 print PDF w/ 50mm calib square) baked from config for UMI teleop tracking. Run: `cd 2ourso100 && OUT=umi_eef.xml ./gen_and_view.sh just_eef_umi`.

## 2026-06-23 — text-to-cad = build123d skills library
NL/image → build123d Python (`gen_step()`) → **STEP** (primary) → STL/3MF/GLB. Mandatory inspect+snapshot validation loop. **All 11 skills are committed on the canonical lab clone `main` (HEAD 7da1c64) under `plugins/cad/skills/`: cad, cad-viewer, dxf, bambu-labs, urdf, srdf, sdf, gcode, step-parts, sendcutsend, implicit-cad.** (Earlier "urdf only on develop" note was wrong — it came from listing an in-progress sshfs checkout; the git tree always tracked all 11.) `urdf` skill = `gen_urdf()` source-of-truth, std-lib-only generator+validator — most relevant to the arm rebuild. `step-parts` can source a real STS3215 STEP envelope. Steal: parametric-source-not-mesh, STEP-first validation loop, viewer-link handoff.
