# Outbox — cad

## 2026-06-24 PT — smith300-pose rebuild: servos at real poses + Y-flip (Cameron-verified)
NEW direction: clean-modular arm had identical servo orientations → too few DOF. `param_smith_arm.py` now places our servo+holder at smith300's REAL joint chain + per-servo visual origins (varied orientations + link lengths). Servos were 180deg backward; Cameron picked the **Y-flip** (of x/y/z variants) — locked as default. `smith300_poses.urdf` canonical. NEXT: build the per-joint connectors that actually bridge the differently-oriented servos (creative connector direction; same locked holder+yoke). Continuous loop stays OFF (focused back-and-forth).

## 2026-06-24 PT — ⏹ CONTINUOUS LOOP STOPPED (goal complete, best-judgment wind-down)
Stopped cron 0b89a28b ~73min before deadline: full goal achieved + wired end-to-end; remaining work was marginal polish, so winding down vs burning compute (Cameron delegated judgment).
**DELIVERABLE (all in `smith300_para_stuff/`, regen via `bash build_all.sh`):**
- Parts (parametric): `wrap_holder.py`/.stl (locked, voxel clearance), `yoke_exact.py`+`gen_yoke_arm.py`→`yoke_arm.stl` (45mm connector), `fidex_holder.py`→`fidex_holder_sm/bg.stl` (bolt-on marker brackets w/ recessed pocket).
- Robot: `param_arm.py`→`smith300_modular.urdf` — 6-DOF modular arm (rises/curves like smith300, base-on-table) + actuated 2-finger gripper (nq=8), per-link fidex marker holders, collision-clean. `f(holder,yoke,connector,chain,fidex)`.
- Fidex: `fidex_config.json` (LinkConfig-format, 7 boards) + `fidex_markers/*.png` (real ArUco, DICT_4X4_250) + `fidex_print_sheet.png` (true-scale A4 + 50mm cal square) + `smith300_exo_config.py` (ready-to-run fidex ExoskeletonConfig bridge).
- Docs/renders: `README_fidex_robot.md`, `arm_hero3.png`, `arm_sweep.png`, `gripper_openclose2.png`. Viewer: `cad.omidlab.net/?dir=/data/cameron/repos/smith300_para_stuff&file=smith300_modular.urdf`.
NEXT (Cameron's call): run the estimator end-to-end on a test image; exact-pose match to smith300; or repoint to other work. Resume = re-create the loop.

## 2026-06-24 PT — continuous: v19 CAD↔fidex bridge (end-to-end capstone)
`smith300_exo_config.py` builds a full fidex `ExoskeletonConfig` (7 LinkConfigs + GridBoards, DICT_4X4_250) from the CAD's `fidex_config.json`, mirroring so100_adhesive.py — tested, instantiates clean. Drop into FiducialExoskeletons/exo_configs/ + supply an arm MJCF to run the estimator. Did NOT touch Cameron's repo. **Parametric CAD now produces a working fidex pipeline config end-to-end.** The whole goal is realized + wired.

## 2026-06-24 PT — continuous: v18 gripper verified + travel widened
Confirmed gripper actuates (nq=8, `render_gripper.py`); widened finger travel to ±18mm so open/close is clearly visible (`gripper_openclose2.png`). Robot is a complete functional unit: 6-DOF arm + actuated 2-finger gripper + fidex exoskeleton + LinkConfig output. Core done; genuine polish nearly exhausted.

## 2026-06-24 PT — continuous: v18 actuated 2-finger gripper
Gripper now ACTUATED — 2 prismatic finger joints (open at q=0, closed at 12mm), nq=8 (6 arm + 2 fingers). `arm_hero5.png`. Addresses the gripper backlog item. Core remains complete; this + LinkConfig output round out a usable robot.

## 2026-06-24 PT — continuous: v17 fidex_config now LinkConfig-compatible (pipeline-ready)
`fidex_config.json` per-link row now matches Cameron's fidex LinkConfig (mujoco_name, robot/exo mesh paths, aruco_offset_pos[mm]/rot, board_name, board_length, grid, ids, dict, marker_image) → feeds the pipeline directly. Fixed gen_markers/gen_print_sheet to the renamed field; verified all regen clean. The CAD now outputs the full fidex coupling per link.

## 2026-06-24 PT — continuous: v16 bolt-mountable brackets; FULL GOAL ACHIEVED
v16: fidex marker bracket now bolt-mountable (2x M2 holes flanking the pocket). The full goal is DONE — complete parametric reproducible fidex robot. Remaining work is diminishing-returns polish (pose-tune to smith300, in-part pocket). Loop will keep polishing until deadline (~1.5h) or Cameron redirects.

## 2026-06-24 PT — continuous: v14-15 (one-command regenerate + README; textured render n/a)
v14: `build_all.sh` regenerates the WHOLE package end-to-end (verified exit 0) — holder→yoke→fidex holders→arm URDF+config→ArUco boards→print sheet→hero. Wrote `README_fidex_robot.md`. v15: textured-marker render attempted (FIDEX_TEX/`_tex.urdf`) but MuJoCo doesn't map URDF PNG textures onto box faces (markers stay as print sheet/PNGs); main URDF unaffected. **The full goal is achieved: complete, parametric, reproducible fidex robot.** Continuing polish (bracket bolt mounts) until deadline/interrupt.

## 2026-06-24 PT — continuous: v13 base-on-table hero
Added `world` root + fixed joint (ROOT_RPY=π) so the base plate sits flat and the arm rises/curves to the gripper — clean deployable desktop-arm hero `arm_hero3.png`. Robot+fidex now presentation-ready: parametric arm + per-link marker holders + real ArUco boards + print sheet + fidex_config. Continuing (textured markers on the render, bracket bolt mounts).

## 2026-06-24 PT — continuous: v12 FIDEX COMPLETE (real markers + true-scale print sheet)
Installed opencv-contrib-python-headless in .venv_cad. `gen_markers.py` → real ArUco boards `fidex_markers/{L0(3x3 ids0-8,50mm),L1-6(2x2,ids20-43,15mm)}.png` (DICT_4X4_250) matching the CAD pockets. `gen_print_sheet.py` → `fidex_print_sheet.png` (A4 300dpi true-scale + 50mm calibration square). FIDEX now COMPLETE end-to-end: CAD pockets@known poses ↔ printable markers ↔ fidex_config.json. Full parametric fidex robot done (arm + holders + boards + config + print sheet). Continuing polish.

## 2026-06-24 PT — continuous: v11 verified clean + hero render
`render_sweep.py`: swept all joints q=-1.2..+1.2 → **0 self-collision** at every pose (CLEAN), montage `arm_sweep.png` (curl/rest/extend all valid). `render_hero.py`: clean hero `arm_hero.png` (base plate + base 3x3 holder + rising/curving arm + per-link fidex marker holders + gripper). Arm is complete: parametric, fidex-ready, collision-clean, articulating. Viewer: `cad.omidlab.net/?dir=/data/cameron/repos/smith300_para_stuff&file=smith300_modular.urdf`. Continuing (base-on-table, bracket bolts, ArUco textures).

## 2026-06-24 PT — continuous: v11 (real fidex marker holders w/ recessed pockets)
New parametric part `fidex_holder.py` = printed pad with a RECESSED board pocket (CAD↔vision coupling); generated 15mm + 50mm variants; param_arm now mounts the real holder bracket per link with the ArUco board seated in the pocket (locked wrap_holder untouched). `arm_v11.png` = complete fidex-ready parametric robot: base plate + base holder(3x3) + 6 module holders(2x2) + gripper + fidex_config.json. Continuing (mount/bolt the brackets, base-on-table orientation, hero).

## 2026-06-24 PT — continuous: v9-v10 (base plate, viewer-confirmed, compact connector)
v9: added base plate (table mount) under L0; arm confirmed viewable (catalog HTTP 200). v10: regenerated connector yoke at 45mm (`yoke_arm.stl`) + SERVO_OFF=-(34.7+L)/1000 → tighter, more arm-like, traces smith300 better (`arm_v10.png`). Full parametric package: base plate + base 3x3 board + 6 modules w/ 2x2 boards + gripper + fidex_config.json (7 boards). Viewer: `cad.omidlab.net/?dir=/data/cameron/repos/smith300_para_stuff&file=smith300_modular.urdf`. Continuing (compactness/pockets/pose).

## 2026-06-24 PT — continuous: v7-v8 (fidex holders + gripper + faithful boards + config)
v7: fidex markers → proper holders (standoff+frame+plate) + a parametric gripper at the tip (`arm_v7.png`). v8: made fidex FAITHFUL per fidex_notes — base 3x3@50mm/id0, shoulders 2x2@15mm/ids20.., DICT_4X4_250; generator now EMITS `fidex_config.json` (per-link aruco_offset_pos/rot = the LinkConfig coupling). Arm is now fidex-ready & parametric. `arm_v8.png`. Continuing.

## 2026-06-24 PT — modular arm v2-v6 + fidex started; CONTINUOUS mode (job 0b89a28b)
Cameron: work continuously (no 20-min idle), no questions, until interrupted. Switched 20-min loop → 3-min continuous multi-iteration job 0b89a28b (~2h). Progress this session: v2 connected modules into a clean chain; v3-v5 tuned bends → arm RISES from base + curves to wrist (traces smith300 roughly, `arm_v5.png`); v6 added per-link ArUco fiducial plates (`arm_v6_fidex.png`). Now have parametric modular arm + per-link fiducials. Next (continuous): proper outward-facing fidex holders, compactness, gripper, hero render. Plan in robot_build_state.md.

## 2026-06-24 PT — voxel clearance WORKS + modular-arm build started (loop 5bdbd6cd)
- **Voxel dilation clearance**: `voxel_trim.py <mm>` now produces wrap_holder.stl with true UNIFORM ~0.3mm gap (build123d un-trimmed holder → scipy binary_dilation of servo on 0.15mm voxel grid → skimage marching-cubes → trimesh/manifold3d subtract). Verified watertight, holder∩servo=0, inflated bounds = servo+0.3 every face. Deps installed in .venv_cad via uv (trimesh, manifold3d, scikit-image). OCP offset() confirmed dead on the geared servo (body solid + fused, all kinds). Minkowski (python wrap_holder.py) is the fallback.
- **Base parts LOCKED** (memory project_locked_base_parts): wrap_holder + yoke_exact, clearance tunable.
- **Modular smith300 rebuild STARTED**: `param_arm.py` v1 = parametric 7-DOF generator (smith300 chain). Loads nq=7 but modules float disconnected (no connectors yet) — `arm_v1.png`. Cameron chose: CLEAN MODULAR chain (roughly trace smith300, not exact overlay) + autonomous loop. Plan in `robot_build_state.md`. Loop **5bdbd6cd** every 20min, deadline 2026-06-25T12:05 PDT.

## 2026-06-24 PT — wrap-holder v9: parametric servo clearance + print-ready confirm
- Added **`SERVO_CLEAR`** (parametric; 0=touching, now **0.3mm**). OCP `offset()` fails on the geared servo → dilate by subtracting servo + 6 axis-shifted copies (octahedral Minkowski, robust). holder∩servo=0, holder∩yoke=0 still hold.
- Confirmed NOT intersecting earlier: st3215.stl bbox == STEP bbox (identical), so checks against STEP apply to the viewed mesh; what Cameron saw was a no-transparency depth illusion. Published `wrap_holder_only.urdf` (holder+yoke, no servo) as a viewer toggle.
- **Print-readiness (build123d, no trimesh):** wrap_holder = 1 valid closed solid, 20×38×32mm; yoke_exact = 1 valid closed solid, 103×46×26mm → both PRINTABLE. (STL export skipped 1 null-triangulation face — benign, slicers auto-repair.)
- Printable STLs: `smith300_para_stuff/{wrap_holder,yoke_exact}.stl`. Renders: `wrap_holder_clear03.png` (xray w/ gap), `wrap_holder_xray.png`.

## 2026-06-24 PT — wrap-holder v8: CLOSED screw holes (Cameron feedback)
Cameron: bolt holes were "open-faced" (at the thin flange-strip edge → no material wrapping the screw). Fix: added a **screw boss** (full collar, BOSS_R=5.2) around each hole, then **servo-trim** (`make_wrap_holder` now subtracts the real servo STEP) so each collar conforms to the body = max closed material that physically fits. Servo-trim left 2 zero-vol boolean slivers → export now filters to the real solid. Result: **real_solids=1**, holes have round counterbored collars, holder∩servo=0, holder∩yoke=0, swept=0, vol 5304mm³. Renders `wrap_holder_v7_holes.png` (collars visible), `wrap_holder_v7_fit.png`.
NOTE: Tailscale SSH 12h re-auth = ACL `check` action; no TS API token in fleet → gave Cameron the admin-console change (check→accept) or offered to apply via a token he generates.

## 2026-06-23 PT — ⏸ wrap-holder loop STOPPED (converged → Cameron's call)
Stopped cron 2ef53883: option (a) meets all 7 quality-bar items (complete, print-ready) AND I've proven the only path to a fuller wrap is a yoke change — a tradeoff on a yoke Cameron likes, so it's his decision, not autonomous churn. **FINAL CANDIDATE (a):** `wrap_holder.py`→`wrap_holder.stl` (both-sides rear end-clamp: wraps rear body 4 sides + bolts BOTH ±Y faces w/ M3 counterbores; sweep-relief keeps the yoke clear over ±100°; collisions 0.00 static+swept; 1 solid; 5037 mm³; print output-axis-vertical). View posed: `cad.omidlab.net/?dir=/data/cameron/repos/smith300_para_stuff&file=wrap_module.urdf`. Renders `wrap_holder_v6.png`, `wrap_module_v5.png`.
**Awaiting Cameron's fork:** (a) accept rear-clamp · (b) compact yoke for fuller wrap (yoke redesign — needs his OK since he likes the current yoke) · (c) other. Resume = re-create the 20-min cron; full state in `wrap_holder_experiment.md`.

## 2026-06-23 PT — wrap-holder analysis: (a) is locked at rear-clamp (proven)
`opt_rsweep.py` swept the relief radius 18→24 over ±100°: wrap is **nearly flat in R** (6024→5037 mm³) and the part **fragments below R21**; min clear ≈R24. So R-tuning (and range-limiting) reclaim ~nothing — option (a) is fundamentally a rear end-clamp. **A fuller wrap genuinely requires a more compact yoke (option b)** — now proven, not guessed. Next firing: bounded compact-yoke prototype (`yoke_compact.py`, side-spine, orbit r~13-15) to measure how much more wrap it buys, then report.

## 2026-06-23 PT — wrap-holder v6 (counterbores; option (a) quality-bar COMPLETE)
M3 socket-head counterbores on the 4 bolts (heads flush). Print orientation = output-axis vertical. Still 1 solid, collisions 0.00, vol 5037mm³. Render `wrap_holder_v6.png`. **Option (a) rear-clamp now meets all 7 quality-bar items** — solid candidate. Fork to Cameron still open (a accept / b compact-yoke for fuller wrap / c limit range); next firings will prototype (b) since it targets his literal "fully wraps" goal.

## 2026-06-23 PT — wrap-holder v5 (swept-collision FIX + design fork ⚠️ for Cameron)
Built `swept_check.py` (rotate yoke through range, exact per-angle overlap) — **caught a blocker the static check missed:** the rotating yoke hit the holder (550mm³). The yoke spine orbits r~19.5 about the output axis; added a cylindrical **sweep-relief** → over ±100° range swept overlap = **0.00** (static also 0.00). Module URDF `wrap_module.urdf` (nq=1) for the viewer.
- ⚠️ **FORK:** clearing the yoke forces relieving the OUTPUT half, so the holder is now a robust **+X end-clamp** (wraps rear body 4 sides + bolts BOTH ±Y faces) — not a FULL-length wrap. Full wrap is geometrically incompatible with this yoke's swing. Asked Cameron: accept rear-clamp / make a more compact yoke / limit range. Renders `wrap_holder_v5.png`,`wrap_module_v5.png`. Viewer: `cad.omidlab.net/?dir=/data/cameron/repos/smith300_para_stuff&file=wrap_module.urdf`.

## 2026-06-23 PT — wrap-holder v4 (lightened cage)
Framed lightening windows in both large ±Z side walls: mass 15943→**10009 mm³** (-37%), still 1 solid (frame rigid), collisions still 0.00. ±Y ends already open for the cable. Now reads as a proper cage, not a box. Renders `wrap_holder_v4.png`/`_v4_3body.png`. Quality bar: items 3,4,7 ✓ (+1,2 from v1). Next: fillets/print-orientation (v5), then module URDF for Cameron to view (v6).

## 2026-06-23 PT — wrap-holder loop firing: v2+v3 (collision-clear)
Built `check_collision.py` = exact boolean-intersection volume (rigorous fit gate). v2 opened flange centers (clear horn/idler). v3: overlap-bbox localized a 9.36mm³ sliver to the yoke's idler plate → learned the yoke grips BOTH ±Y faces full-width across the -X half, so only the outboard servo holes are usable → set both flanges outboard-only (4 bolts, 2 per side). Now **holder∩yoke=0.00, holder∩servo=0.00 mm³**. Render `wrap_holder_v3_3body.png`. Next: wire cutout/lightening (v4).

## 2026-06-23 PT — LOOP REPOINTED → both-sides wrapping servo holder experiment
Cameron's new direction: TOM's holder mounts on one side; build one that fully WRAPS + grips the servo on BOTH sides (like his robot), keep iterating until really good. Swapped cron afcc5898→**2ef53883** (TOM exploration paused; G1–G3 already done). Driver: `agents/cad/wrap_holder_experiment.md` (brief + quality bar + backlog).
- **v1 built+rendered:** `smith300_para_stuff/wrap_holder.py` = 4-wall sleeve wrapping the body + flanges on BOTH ±Y mount faces, bolted to the real extracted holes (4+4), horn/idler left clear at -X. Render `wrap_holder_v1.png` — cradles both sides, exact holes. Next: wire cutout + lightening (v2), yoke-collision check (v3).

## 2026-06-23 PT — loop afcc5898 firing 1: G3 done (TOM viewer-renderable)
Verified the cad viewer WILL render TOM (answers Cameron's 3MF question): bundle ships `ThreeMFLoader`; all 14 `3MF/*.3mf` are real hydrated ZIPs (PK…, not LFS stubs — asset client has explicit LFS-pointer detection that would've blanked stubs); `/__cad/catalog` returns 200 **cross-dir** for the ttc-develop tom dir, listing tom.urdf + every mesh. URL good: `cad.omidlab.net/?dir=/data/cameron/repos/ttc-develop/models/robots/tom/v2&file=tom.urdf`. Next: G2 (tom_with_gripper/tom_double URDFs).

## 2026-06-23 PT — ✅ TOM GENERATOR RUNS END-TO-END (real diagnosis: LFS stubs, NOT build123d)
**Correction to passes 1–4 below:** the `gen_urdf` AssertionError was never a build123d version or main/develop branch issue. Root cause = **5 custom-part STEPs were unhydrated Git-LFS stubs** (`.lfsconfig` excludes `models/**`) → OCC `**** ERR StepFile: Line 2 unexpected TYPE ****` → degenerate shapes → `Compound.center()` assert. Fix: `git lfs pull -I models/robots/tom/v2/parts -X ""`.
- Wired develop toolchain WITHOUT heavy install: reuse `.venv_cad` (build123d 0.11 + OCP + ezdxf) and prepend develop `cadpy`/`cadpy_metadata` `src/` to PYTHONPATH. Helper: `smith300_para_stuff/tom_build.sh`.
- After hydration, `tom.py` regenerates **byte-identical to committed `tom.urdf`** (9 links/8 joints/24 meshes, empty diff, all 3MF meshes present). Task 2 DONE.
- `tom_patch/sitecustomize.py` center()-robustness shim exists but is now **inert** (no fallbacks fire post-hydration); kept as a safety net.
- **Set 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`.

## 2026-06-24 — cron-loop pass 4 (final — loop stopped)
- **TOM run (task 2) — closed as complete-as-feasible.** `.venv_cad` has build123d **0.11.0** and **no pip module** (deps installed externally) → can't surgically upgrade without risking the venv that runs the viewer + our part gen; a fresh OCP venv (~400MB) isn't worth it. Familiarization is DONE: studied `servo_horn_yoke.py` + `servo_end_mount.py` (both sheet-metal/SendCutSend), `tom.py` top-level runs, `tom.urdf` committed + viewable. Full `gen_urdf` regen stays blocked on the build123d 0.11 topology AssertionError — low value since the design is understood and the URDF exists.
- **Task 3 DONE+polished** (two_servo.urdf clean exact mating). **Both tasks complete → CronDelete'd job b95e93f4, loop stopped.**
- For Cameron when back: two_servo viewer link = `cad.omidlab.net/?dir=/data/cameron/repos/smith300_para_stuff&file=two_servo.urdf`. Key TOM takeaway: it's a **sheet-metal** robot (laser-cut+bend), so our **printed** Boolean-shell holder + exact-hole yoke is the right divergence, not a copy target.

## 2026-06-24 — cron-loop pass 3
- **two_servo.urdf refined (task 3):** found holder2 sat ~9mm low vs the yoke-link bar (link hit the top edge of the holder wall). Fixed: servo2/holder2 origin `-0.1051 -0.0091 0` → `-0.1047 0 0`. Junction close-up now shows the yoke link butting **flush + centered** on holder2's +X wall (0.4mm gap closed), joint still nq=1. Render `two_servo_fixed.png`. Mating now clean/exact. Task 3 DONE+polished.
- Final pass: attempt build123d upgrade in a fresh venv to fully RUN TOM gen_urdf (blocked on 0.11 topology AssertionError); else wind down (tom.urdf already viewable, design understood).

## 2026-06-24 — cron-loop pass 2
- **TOM study (task 2):** `servo_end_mount.py` + `servo_horn_yoke.py` are **SHEET-METAL** parts (SendCutSend: sheet 1.6mm, K=0.42, bend deduction/radius, bend-line clearances → flat pattern + bends). Authored in the servo STEP local frame, mounted to the servo's exact horn-mount *face* (finds planar face near target). **Conclusion: TOM ≠ our fab.** For our 3D-PRINTED robot keep our Boolean wrap-shell (holder_wrap) + exact-hole bolting; don't copy TOM's bend geometry. TOM validates: servo-local-frame authoring + exact-face/hole mounting + horn axis -25.5.
- Full `tom.py` URDF regen still blocked by build123d 0.11 topology AssertionError (develop has no version pin → latest). Committed `tom.urdf` already viewable, so low priority. NEXT (better value): make the two_servo.urdf mating EXACT (yoke-link end ↔ holder), since Cameron wants no approximate fits.

## 2026-06-24 — cron-loop pass 1 (job b95e93f4, every 15min/1hr)
- **Task 3 ✅ TWO-SERVO connector URDF**: `smith300_para_stuff/two_servo.urdf` = servo+holder_wrap+yoke_exact ×2, 1 revolute joint, sweeps clean. View: `cad.omidlab.net/?dir=/data/cameron/repos/smith300_para_stuff&file=two_servo.urdf`.
- **Task 2 (TOM, in progress)**: worktree `ttc-develop` on develop; deps ready (build123d 0.11 + ezdxf 1.4.4); hydrated v2 3MF + `parts/imports/*.step` (sts3215/sts3250). `tom.py` top-level now runs (servo STEP loads, face detect OK). BUT `gen_urdf` via urdf skill → build123d **AssertionError (TopoDS_Compound .wrapped None)** = version mismatch. NEXT: pin TOM's build123d version (check develop requirements) then regenerate tom.urdf, study servo_horn_yoke.py / servo_end_mount.py.


## 2026-06-24 — visualization interface: CAD Viewer (browser, interactive)

Answer to "easiest interface to visualize STL/STEP/URDF": text-to-cad's **`cad-viewer`** — browser 3D viewer, **URDFs get joint sliders** (drag to pose), STEP/STL orbit/zoom. Stood it up on lab:
- Installed Node 20 at `~/node20/` on lab (system node was v12). Running `node backend/server.mjs` (serves prebuilt dist, no build) in tmux `cadviewer`, port 4178, `--dir cad_experiments`, 12h auto-shutdown.
- **Live now via tailnet:** `http://100.74.71.38:4178/?dir=/data/cameron/repos/cad_experiments&file=two_link_urdf.urdf` (browse all by dropping `&file=`).
- **Public route staged:** `cad.omidlab.net → lab:4178` added to VPS cloudflared config (validated) + DNS CNAME added; pinged **manager** to do the cloudflared reload (their infra) — then it's at `https://cad.omidlab.net/?dir=…&file=…` with no tailnet needed.

— for manager: see the request in your pane re: cloudflared reload.

---

## 2026-06-24 — minimal 2-link servo robot: URDF + MJCF, both animate (verified)

Built the simple 2-link robot Cameron asked for, on the servo's REAL geometry.
- Confirmed hole coords come straight from the STEP (extracted the ST3215 horn: 4-hole bolt circle R7 @ (-25.5,6,0), axis +Y) — `servo_features.py`. No online lookup.
- `two_link.py` = shared constants + `gen_step()` minimal connector (horn plate + riser + servo-B pad).
- **URDF:** `two_link_urdf.urdf` (validated by the urdf skill — 2 links / 1 revolute joint). Animated via `animate_urdf.py` → `two_link_anim.gif` + `two_link_filmstrip.png`.
- **MJCF:** `two_link.xml` (hinge + position actuator, materials) — loads + animates identically (`two_link_mjcf.gif`). Generated from the SAME constants so URDF/MJCF can't drift.
- Verified: base servo fixed, connector+servo-B sweep ±1.4 rad about the real horn axis. Both descriptions agree.

All in `/data/cameron/repos/cad_experiments/` on lab. Two venvs there: `.venv` (mujoco render), `.venv_cad` (build123d). Nothing committed yet — say the word and I'll commit the cad_experiments dir (or fold it into custom_robot_building) on a branch.

---

## 2026-06-23 (night) — real STS3215 servo STEP sourced + connector joint module

- ✅ **Got the real servo STEP** — it's in step.parts (the catalog the `step-parts` skill uses), id **`waveshare_feetech_st3215_servo`** ("Waveshare Feetech ST3215" = the Feetech STS3215). Downloaded + checksum-verified to `cad_experiments/parts/`. Imports in build123d as a clean BREP solid (45.2×37.8×24.7mm); rendered, looks right (body + output horn).
- ✅ **Parametric connector bridging two servos** — `connector_v2_joint.py`: a build123d bracket that sits on top of servo A and cradles the next servo B perpendicular (SO-ARM-style joint). STEP + STL generated; assembly rendered (`joint_module.png`): servo→connector→servo.
- ⚠️ **Geometry is a first parametric pass, not print-accurate.** Bolt pattern + cradle are placeholders. To make connectors that actually bolt on I need: (1) the **servo-holder's real mounting-hole coordinates** (or the holder CAD/STEP), and (2) confirmation of **how connectors attach** — to the servo's horn bolt circle, or to the holder body? With the real servo STEP in hand, once I know the holder interface I can make the connectors exact.

Artifacts in `/data/cameron/repos/cad_experiments/` (+ `parts/`). Nothing committed.

---

## 2026-06-23 (evening) — text-to-cad explored + 6-DOF arm exported to URDF with OUR servo holder (verified)

Working dir on lab: `/data/cameron/repos/cad_experiments/` (outside both git repos; `assets` symlinks to custom_robot_building's).

**Explored text-to-cad's `urdf` skill** (the Claude-Code-compatible scripts): `gen_urdf()` source-of-truth + `python <skill>/scripts/urdf <src.py>` generates+validates. Std-lib only.
- ✅ Verified toolchain on a trivial example.
- ✅ Built `arm6dof_ours.py` → a `gen_urdf()` that imports our MJCF generator and **remaps the MuJoCo body tree into a URDF joint tree**, using OUR `sts3215` servo-holder + clamp/connector meshes. Output `arm6dof_ours.urdf` = **7 links / 6 revolute joints**, passed the skill's validator.
- ✅ Visually verified: rendered the URDF in MuJoCo — geometry matches the MJCF 6-DOF baseline exactly (`arm6dof_ours_clear.png`). So our real arm now has a clean URDF, generated the text-to-cad way.
- Key conversion insight: URDF requires child-link-frame == joint-frame, so each URDF link's frame = the MuJoCo *shoulder/hinge* frame (not the base-body frame); meshes get baked offsets. Documented in vault memory.

**`cad` (build123d) skill — set up + verified + connector experiment done.**
- Installed py3.12 venv `.venv_cad` (build123d 0.11 + cadquery-ocp + editable cadpy + playwright). ✅ `scripts/step trivial_step.py` → STEP. Toolchain works.
- ✅ Built a **parametric connector** the build123d way: `connector_v1.py` → `connector_v1.step` (79KB solid) + `.stl` + render `connector_v1.png` (mounting plate, 4 M3 bolt holes, riser, central bore — all named parameters).
- **Answer to "can we use our own servo holder":** build123d **imports** `ours_base_holder.stl` (bbox correct 44.7×53.4×26.6mm) but only as a **non-solid `Face`** (triangulated mesh). It **cannot boolean-join** parametric solids to it ("Only shapes with the same dimension can be added"). So:
  - **build123d is great for *new parametric connectors*** (STEP, every dim a parameter) — recommended for the connector pieces.
  - **Joining geometry *onto* our existing Blender mesh holder (your current workflow) is a *mesh* boolean** — Blender, or trimesh/`manifold3d` in Python — NOT build123d. For that, mesh tools stay easier.
  - They compose: parametric connector (STEP/STL) + holder (mesh) as separate URDF links / assembly, or mesh-merge at the very end for a single printed piece.
- **Open input needed:** the connector's bolt pattern is a placeholder grid — I need the holder's real mounting-hole coordinates (or the holder CAD/STEP) to make connectors that actually bolt on. Also: do you want connectors as separate bolt-on parts (parametric, my lean) or fused into the holder mesh (Blender-style)?

All artifacts in `/data/cameron/repos/cad_experiments/` on lab (validated `arm6dof_ours.urdf`, `connector_v1.{step,stl,png}`, render/verify scripts). Nothing committed to your repos yet.

---

## 2026-06-23 (later) — decisions recorded + lab pipeline stood up + a blocker

**Cameron's answers to the 6 Qs are recorded** (vault `overview.md`/`tasks.md`/`memory.md`, and the compute rule in fleet memory): all compute+code on lab / VPS = hosting+memory only; **build123d for parts** (follow text-to-cad examples); **URDF-primary**, MJCF later; Blender stays an open geometry-edit step; 6-DOF arm = the **default `long_singlejointed`** config; STL handoff stays on lab.

**Stood up the lab render pipeline (all on phe108, 2× RTX 6000 Ada):**
- Created `uv` venv at `/data/cameron/repos/custom_robot_building/.venv` (numpy + mujoco + pillow).
- Wrote a reusable offscreen renderer `2ourso100/render_baseline.py` (EGL, headless — lab has GPUs but no display).
- ✅ Rendered the committed EEF model: `2ourso100/eef_baseline.png` (UMI gripper w/ 3×3 ArUco board + red fingers). Generation + MuJoCo-load + GPU offscreen render all confirmed working.

**Blocker for the 6-DOF baseline:** the `long_singlejointed` config generates a valid 6-hinge MJCF, but MuJoCo can't load it — it references **6 mesh STLs that aren't in the repo**: `link1_holder.stl`, `link1_clamp.stl`, `link2_clamp_fixed.stl`, `link3_clamp.stl`, `link4_connector_fixed.stl`, `link5_connector.stl`. The repo is a "curated subset" that only bundled the *latest EEF* meshes. **Need those 6 STLs** (from the original `mujoco_menagerie/2ourso100` workspace) dropped into `2ourso100/assets/` on lab to reproduce the full-arm baseline.

Meanwhile I can proceed without them: stand up build123d on lab + prototype the first parametric part (wrist camera mount → STEP → STL, URDF-primary). Say the word, or hand me the 6 STLs and I'll render the full-arm baseline first.

---

## 2026-06-23 — bootstrap current-state assessment

Status: **idle** (bootstrap done, ready for first real task). One workspace decision + a few design questions below.

### TL;DR

Both repos cloned and read deeply. They are two halves of the same loop:
- **`custom_robot_building`** = your *existing*, working, "somewhat hacky" generator. It's an **MJCF (MuJoCo XML) generator** for a **modular serial chain of SO-ARM100/101 modules** (Feetech STS3215 servos + 3D-printed holder/clamp brackets). You author link poses in a Python config; it emits the nested MuJoCo body tree, handles all the quat/frame bookkeeping, bakes ArUco marker boards, and opens the viewer.
- **`text-to-cad`** = the *prior-art toolchain* we want to steal from. It's a library of **agent skills** built on **build123d** (parametric Python CAD → STEP → STL/3MF/GLB), plus URDF/SRDF/SDF generators, a browser **CAD Viewer**, part sourcing, slicing, and Bambu printing — all wired as a describe→generate→inspect→snapshot loop.

The synthesis I see: keep your MJCF chain generator as the **assembly/kinematics layer**, and adopt build123d as the **parametric part layer** that produces the holder/clamp/wrist-mount STLs — replacing (or feeding) the Blender hand-edit step for the parts that are really parametric.

---

### What I understood — `custom_robot_building/2ourso100`

- **Robot lineage:** custom variant of the **SO-ARM100 / SO-101** (LeRobot's low-cost arm). MJCF `model="so101"`, `childclass="so101"`, servo class `sts3215` (Feetech STS3215, ±1.92 rad ≈ ±110° range, kp 998).
- **Modular by construction.** The whole arm is a repeated identical module:
  `body link_i (holder mesh + servo mesh) → body shoulder_i (hinge joint + clamp mesh) → link_{i+1} …`, nested, with each child link's quat = inverse of the parent shoulder quat. Constant `SHOULDER_POS`/`SHOULDER_QUAT`/`CLAMP_POS` per segment ⇒ you add a DOF by adding one entry to a list.
- **Two ways to specify geometry** (`generate_example_twolink_custom.py`):
  - `USE_WORLD_LINKS=True`: give each link body a **world-frame pose** `{pos, quat|r}`; `build_parent_frame_chain_from_world()` converts to the parent-relative pos/quat MJCF needs. This is the path you actually use.
  - else length-based (`LINK_POSITIONS`/`LINK_LENGTHS`) with per-link orthogonal `r=[rx,ry,rz]` quarter-turn rotations.
- **Per-robot config** lives in `robot_configs/<name>.py` (loaded via `--config`). Holds `WORLD_LINKS`, holder/clamp STL filenames per link, joint home qpos, welded-shoulder set, ArUco placements, materials. Current committed default of interest is **`just_eef_umi`** = a UMI gripper **end-effector-only** bundle (2 links, holders omitted, two EEF STLs `...arucobox_fixed.stl` + `eef_thumb_with_handle_2.stl`, servo hidden on link 0).
- **ArUco baking is built in:** configs generate marker textures + a true-scale printable **A4 PDF print sheet** with a 50 mm calibration square (so the printed board matches the MJCF box size). This is your UMI/teleop tracking layer — markers on the EEF, frames in `local`/`parent_link`/`world`.
- **The "hacky" parts** (your word, confirmed): a lot of coordinate-frame correction knobs for STLs authored in **Blender world frame** — `USE_LINK_HOLDER_COORD_FRAMES`, `USE_LINK_CLAMP_COORD_FRAMES`, `HOLDER_COORD_USE_INVERSE`, `CLAMP_MESH_WORLD_FK_JOINT_QPOS`, `CLAMP_MESH_LS_USE_ZERO_HINGE` — i.e. inverse/forward `T_geom = inv(T_holder)·inv(T_world_link)` toggles to un-bake a world-frame STL back into link/shoulder-local frame. Lots of commented-out marker/link blocks. STL filenames carry their whole edit history (`...fixjust7_strongereef... (1).stl`). All very debuggable but brittle.
- **Conventions worth locking in:** STLs authored in **mm**, scaled `0.001` in MJCF (meters). Output is **MJCF, not URDF** — though three `robot_from_blender*.urdf` files show you also have a Blender→URDF export path (box base_link + per-link visual meshes with rpy origins). Run: `cd 2ourso100 && OUT=umi_eef.xml ./gen_and_view.sh just_eef_umi`.

### What I understood — `text-to-cad` (earthtojake/CAD Skills)

- **Not a model/pipeline — a skills library** (MIT, actively developed; `main` = release, `develop` = dev w/ symlinks). The canonical lab clone (`main`, HEAD `7da1c64`) tracks **all 11 skills**: `cad`, `cad-viewer`, `dxf`, `bambu-labs`, **`urdf`, `srdf`, `sdf`, `gcode`, `step-parts`, `sendcutsend`, `implicit-cad`** under `plugins/cad/skills/`. (Correction to my first pass: I'd said urdf/srdf/sdf were develop-only — that was a partial-checkout artifact from the interrupted sshfs clone; they're committed on main and available now.)
- **`cad` skill** is the core: NL/image/drawing → **build123d** Python source w/ `gen_step()` → **STEP** (primary artifact) → STL/3MF/GLB (secondary). CLIs: `scripts/step` (generate), `scripts/inspect` (refs/measure/align/frame/diff via `#o1.2.f1` selector tokens), `scripts/snapshot` (PNG/GIF review). **Mandatory snapshot-validation loop** after every change. Assemblies via `cadpy.assembly.AssemblyHelper` with source-level build123d joints + named mating datums. Defaults: mm, XY base, +Z up, M3/M4/M5 clearance tables, plastic wall 2–3 mm.
- **`urdf` skill:** treats the `gen_urdf()` Python as source of truth, `.urdf` as generated; strong on frame-semantics (joint origin vs link vs visual/collision/inertial frames), mesh scale, inertials; std-lib-only generator+validator. **This is the most directly relevant skill to the arm rebuild.**
- **`cad-viewer` skill:** browser preview for CAD/G-code/robot files; every CAD/URDF op is supposed to hand off a live viewer link. Also fronts a MoveIt2 server for the SRDF/IK demo.
- **`gcode` + `bambu-labs`:** slice validated mesh → printer-profiled `.gcode` → dry-run/upload to a local Bambu. `step-parts`: off-the-shelf STEP (screws, bearings, **servos/motors**) — could give us a real STS3215 envelope instead of a hand mesh.
- **Big idea to steal:** parametric build123d source as the substrate (not mesh editing), STEP-first with a deterministic inspect+snapshot validation loop, and the viewer-link handoff. The gap vs your workflow: build123d is *code-defined geometry*; your loop has a **human Blender STL edit** in the middle. Re-importing a Blender-edited mesh back into parametric source is the friction point we'll have to design around (see Q3).

### Suggested first move (my recommendation)

**Reproduce your current `just_eef_umi` EEF in MuJoCo on a machine that can render, before changing anything** — establish a known-good baseline + a screenshot I can diff against. Then pick **one** small parametric part (my vote: the **wrist camera mount** for the EEF, since wrist-cam is an explicit near-term goal and a camera socket is a clean parametric part) and prototype the build123d → STEP → STL → drop-into-config path end-to-end. That exercises the whole new loop on something low-risk and genuinely useful, and tells us whether build123d-for-parts is worth adopting wholesale.

### Questions for you

1. **Compute / where do things run?** This VPS (`omid-fleet`) has **no GPU, bare python 3.14, no mujoco/build123d/OCP**. I can author code + generate MJCF/URDF/STEP here, but **MuJoCo viewer and build123d snapshots realistically run on your Mac** (via the `mac` agent) or a GPU box. Confirm: Mac is the render/print target, and I should coordinate sim runs through `mac`? Should I set up a build123d venv here just for headless STEP generation?
2. **Adopt build123d, or stay MJCF-first?** Do you want to (a) wrap text-to-cad's skills as-is, (b) cherry-pick build123d for *parts* while keeping your MJCF chain generator for the *assembly*, or (c) keep Blender as the part editor and only steal ideas/viewer? My lean is (b).
3. **The Blender-edit step.** When you Blender-edit an STL, do you want to (i) keep that as the source of truth and just re-bake frames (today's hacky path), or (ii) move toward parametric parts where Blender becomes the exception, not the rule? This decides how much of the frame-correction machinery we keep vs delete.
4. **URDF vs MJCF.** Your generator is MJCF-primary with a side Blender→URDF export. Do you want the rebuilt robot to be **URDF-primary** (text-to-cad's urdf skill, plays nicer with ROS/MoveIt and most data stacks) with MJCF derived, or stay MJCF-first?
5. **The existing 6-DOF arm.** The committed config is EEF-only (`just_eef_umi`). Is there a full 6-DOF config I should pull (the commented `WORLD_LINKS` blocks suggest a longer chain existed), or do we rebuild the full arm fresh and modular from here?
6. **STL handoff path** (raised by manager too): where do your Blender-edited STLs land so both your Mac and this VPS see them? Manager suggests somewhere under `agents_stuff/` (local on VPS + visible from your Mac mount). I'll set up an `agents/cad/handoff/` dir unless you prefer the repo's `assets/`.

### Ops note (already resolved with manager)
`/data/cameron/repos/` is an **sshfs mount of the lab box** over a slow Tailscale link — cloning git repos directly onto it truncates packs and wedges processes in D-state. Per manager, I cloned both repos to **local VPS disk at `~/cad_repos/`** instead (`/home/cameronsmith/cad_repos/{custom_robot_building,text-to-cad}`). That's the canonical workspace now; I've updated ROLE.md + vault to match.

— cad
