Skip to content

Report: taking over round-3/excavator.aix

Files: dogfood/excavator.aix (the rewrite, 154 lines), dogfood/excavator_reimport.aix, and in dogfood/renders/: excavator.png, excavator_poses.png, excavator_anim_cycle.png, excavator_beauty.png, excavator_reimport.png. Outputs in out/excavator/ (model.glb 7.3 MB, model.stl 9.8 MB, viewer). Nothing is committed. 14 renders of the model (baseline, 2 quick, 1 focus, 6 full transport/rest/reach, 4 with --beauty), plus two round-trip renders and one 3-line probe render.

Note first: the worktree I was given was two commits behind the branch the main checkout was on (no dogfood/round-3, no angle(), no STL); I fast-forwarded it (git merge --ff-only) before starting.

1. Reading the inherited program

What it does, section by section. Lines 12–23 lay out a 2D skeleton in the side (y, z) plane: the boom foot pin P1, boom length/elevation L1/E1, from which P2 (stick pin) and P3 (bucket pin) are derived with sin/cos; xb = 0.35 is the x plane the whole arm lives in (the cab is on −x). wy/wz (26–27) convert a member-local point (n above the axis, s along it) to world y/z. Lines 30–41 place the six cylinder pins, twelve scalars named A1y…B3z (A = barrel base on the parent member, B = rod eye on the child). rby/rbz (44–45) rotate the rod eye about the child's pin by the pose angle; swing (47) is the angle the cylinder must turn to keep pointing at the moved eye; stroke (48) the pin-to-pin distance. barrel/rod/eye/boss (53–60) draw a cylinder as a black flat-capped tube plus an iron pin boss, and a chrome tube from the rod eye back. Then undercarriage, house (with a decal nameplate and decal glazing), a boxy hollow bucket built at its pin and rotated to E3, the stick (a box), the boom (a rect swept along a bezier bowed by ±15° control points), each hydraulic barrel and rod wrapped in its own joint (91, 98–99, 111–114), and epose (121) which emits a pose() with the derived cylinder angles; the twelve *_stroke steps at the end are a hand-check table.

What was clear: the header comment, the member/pin scheme, epose's comment on line 120 (the relative-angle rule), and that every part is built at the origin, rotated, then moved.

What I had to guess or derive: (a) the sign convention of rotate(x=-E3) vs elevation (had to work out that rotate(x=+t) decreases elevation, so -E puts a +z-built part at elevation E); (b) why swing is old − new elevation (same sign question); (c) why the rod's joint gets swing − t (because it lives inside the child's joint); (d) which scalar belonged to which cylinder among A1y A1z B1y … B3z; (e) whether barrel+rod lengths were valid in every pose: the stroke table is printed but nothing states the constraint (barrel ≤ stroke ≤ barrel + rod), and in reach the stick stroke was 1.26 vs a 1.25 barrel, i.e. the barrel already touching the rod eye; (f) why the boom sits at x 0.35 (no comment). I had to render to understand the shape of the bucket and where the cylinder bases really sat on the beams.

What made it hard: twelve single-letter pin scalars instead of six points; seven-argument positional swing(Ay, Az, Py, Pz, By, Bz, t) calls repeated six times in epose; ten joints of which six exist only to fake a telescoping cylinder; a variable named house whose joint is named cab; the trig for the cylinders spread over five defs with no worked example; the stroke table with no stated pass/fail rule; and the check output printing bounds in a pose that are useless for knowing where a posed part is.

Did the report match? Yes, closely. Every friction point it lists I could confirm from the program (relative angles, loose posed bounds, the joint-per-cylinder workaround, wrong-step attribution). Its 14 unlocatable non-manifold edges at rest in stick_beam/boom_beam/boom_bosses I can now place: the stick cylinder's barrel (base at n 0.38) runs level over the bowed boom's top (bow 0.21 + half-depth 0.31 = 0.52 vs barrel underside 0.51), a one-cell tangency along the barrel — the same class of fault I then hit twice myself. One claim did not hold: "Stands: yes and Pieces: 1 in every pose" is fine, but the model was not watertight in the transport pose (6 edges where the twin boom barrels grazed the pedestal's sides, 0.06 apart).

2. Print-readiness, transport pose (set pose transport, full renders at grid 200)

Before (round-3 program)After (dogfood/excavator.aix)
Pieces11
Watertightno: 6 edges, pedestal/boom_cyl_j (barrels 0.06 from the pedestal sides)no: 1 edge at (−0.19, 3.137, 1.552) — see below
Standsyes, 1.212 inside the footprintyes, 1.261 inside
Overhangs14%10% (9% before the last pin boss was added)

What I changed for it: a 45°+ lofted skirt (loft(rect(1.4,1.7), rect(2.25,2.55), 0.5), line 75) under the house so its 2.3 × 2.6 underside no longer floats over the frame and tracks; the counterweight and boom pedestal rebuilt from side profiles with chamfered undersides steeper than 45° (lines 79, 87); stadium-profile tracks (extrude(rect(3.6, 0.72, round=0.36))) instead of a rounded box, halving the bottom fillet that overhung; the bucket curled to −150° world in transport so its inside faces the stick instead of the floor, and the stick opened to −67° so the curled bucket clears the pedestal (it hit it at −85°); boom raised to 65° so the bowed tip's underside stays above 45°. For watertightness: pedestal widened to 1.1 so the twin barrels overlap it by 2 cells instead of grazing it, cylinders at xb ± 0.6, teeth rooted into a solid lip_bar, the mouth cutter shortened, skirt bottom moved inside the frame, and both upper cylinders raised onto brackets/bosses so they neither graze the tapered stick nor the bowed boom (verified at rest: 17 edges → those fixed; reach: 34 → same fix, not re-rendered).

The one remaining edge: in the boom frame (65°) it is at s −0.16, n −0.06 from the boom-cylinder pin B1, on the left rod's +x silhouette — the rod eye (r 0.12) sits 0.03 (one cell) inside the lug's rear face (at ±0.15), and the rod exits through both. Fix: boom_lug 0.4 deep along the boom, or the rod eye r 0.1. I ran out of renders to confirm it. The reports of every pose also confirm every pin-to-pin length (boom_ram, stick_ram, bucket_ram, printed by check --pose) stays above its barrel.

3. Improvements (beyond print-readiness)

  1. The rig itself: six cylinder joints replaced by angle() (lines 47–50, 58–63, 111–112, 120–122, 135–140). Each barrel is a tube in the parent's frame aimed at turn(B, P, angle), each rod a tube in the child's frame aimed at turn(A, P, -angle), and the rod's exposed length is ram − barrel + 0.3, so it can never gap or overshoot. swing, rby/rbz, stroke, the twelve *_stroke steps and the six derived pose angles are gone; arm_pose is one line; pins are [y, z] lists with local()/turn()/toward() helpers; the joint is named house like its variable. 10 joints → 4. The trade-off: the GLB animation no longer telescopes the cylinders (exports are at rest, and only joints animate) — the renders do.
  2. Proportions and detail of the working end: a real bucket profile (lip, floor, round heel and back from a 15° arc loop, offset-hollowed, mouth opened, side plates, lip bar, wedge-lofted teeth, taller lug) instead of a hollow box, and a tapered stick from a side-profile polygon; brackets and bosses under every cylinder base.
  3. House detail and materials: an L-shaped handrail with a mid stanchion on the rear deck (tube(0.035, ...)), an amber glow beacon on the cab roof, a warmer body yellow (material("#e9b125", rough=0.55)), and the hazard stripes as a decal on the counterweight's rear face only (the x-stacked stripes painted the whole side face black).
  4. Beauty composition: camera moved to the cab side (set azimuth -35, elevation 22, zoom 1.25) with the key light swung to -45/50; the round-3 picture showed the machine from behind the boom with the cab hidden and the cab side in shade.

4. Friction

  • dogfood/excavator.aix:48 b = angle("boom")[0] — used 90 lines before joint(..., "boom", ...) is declared. Expected either an error or a doc statement; it works (returns the pose's angle) but nothing says whether order matters. Probed with a 14-line file before relying on it.
  • dogfood/excavator.aix:100–102 bucket_side = polygon(heel) / mouth = rect(...) / bucket_wall = ...check prints nothing for 2D profile steps: they are absent from the step list, so I could not read the profile's box or confirm the mouth cutter's placement without extruding it in a probe file and rendering.
  • Same lines: in the X=0 slice of the transport render the open bucket showed a thin dashed line across the mouth, which I read as "the mouth is still walled"; a probe render showed the mouth open. The line is the section drawing the outline of the side plate one cell behind the plane. Cost one probe and one focus render.
  • npx aixle render ... --focus bucket_shape (transport pose): the focus frame used the step's rest-position bounds (check shows y 0.94..2.58, z 3.1..4.4; the posed bucket is at y ≈ 1.9..3.4, z ≈ 1.6..3.0), so the sheet showed half a bucket, the Y and Z slices were empty and the report said 1.36 × 1.9 × 1.53. Worked around by reasoning from the main sheet.
  • check --pose transport bounds are still loose to uselessness (boom y 0.46..5.88 while the surface extent is 5.2; grid 200: cell 0.04 from that box while the mesh reports cell 0.028). Same as the round-3 complaint; still the only numbers available for a posed part.
  • Report attribution: every non-manifold edge in a pose was named 'boom', 'house' or 'stick', 'boom' — whole assemblies. Each of the four edge sets I fixed (teeth vs inner floor, mouth-cutter nick, barrel over bracket top, skirt corner on frame corner) had to be located by hand-computing frames. Worked around with arithmetic; three of four guesses were right on the first try, one (the lug) was not and I had no render left.
  • dogfood/excavator.aix:75 loft(rect(1.5, 1.9, round=0.2), rect(2.25, 2.55, round=0.1), 0.45) — the docs do not say which way a lofted rect's second dimension goes; probed: h along z, like extrude.
  • dogfood/excavator.aix:54 def side(profile, w) = extrude(profile, w, "x") | flip("z")extrude(..., "x") puts the profile's x along −z, so a side profile drawn "+z to the right" comes out backwards; every side profile needs this flip. The language doc states it, but it is the surprising default for the one axis meant for side profiles.
  • --quick renders drop the rails (r 0.035) and warn, and every quick sheet showed the bucket as a blob; every judgement of the working end needed a full 17–70 s render.
  • Sandbox: a for p in ...; do npx tsx ... loop was refused ("cannot be shown not to be git" for a variable used as an argument); ran poses as separate commands. sed -i ... $f with a variable was refused for the same reason.
  • Time: full render 17 s without exports, 70 s with exports + 1024 beauty; the round trip 15 s.

5. Re-import (dogfood/excavator_reimport.aix, import("../out/excavator/model.glb"))

Size 2.601 × 5.132 × 6.666 vs the original at rest 2.6 × 5.13 × 6.67 (matches to 0.01). Triangles 75 372 at the import's default cell 0.052 vs 196 240 in the source mesh (240 476 in the GLB). Survives: the whole silhouette, tracks, house, cab, hood, stack, skirt, boom bow, stick taper, bucket shape, cylinder barrels. Lost: all materials and decals (one clay colour, no nameplate, no glazing, no tread stripes), the rails and beacon (under the import cell), the rod tubes (r 0.06, broken into beads — one bead is the reported second "piece", a speck at (−0.251, 2.308, 1.741)), the teeth become nubs; 146 non-manifold edges. The GLB lists 5 meshes in 5 nodes with the cycle animation; the viewer page could not be screenshotted here (same CDN block the round-3 agent hit; not attempted twice).

6. Three changes that would make an inherited program easier to read and change

  1. Make check (and the report) name posed steps by their posed placement: print each step's true posed box (or at least each joint's posed pivot and the posed pin positions of every tube), make --focus frame the posed part, and attribute non-manifold edges and loose pieces to the innermost step whose posed surface passes there, with the offending distance ("bucket_barrel side 0.03 from stick_bracket top"). Every hard minute of this round was hand-computing frames the tool already had.
  2. Print 2D profile steps in check (their box and, for a polygon, the point count and winding) and draw them in steps.png; a profile-built part is currently invisible until extruded. Add loft's orientation and a "side profile" idiom (extrude(p, w, "x") reads its x along −z; suggest | flip("z") or an axis="-x") to the docs.
  3. Ship the angle() cylinder as a documented pattern with helpers: a two-point turn/toward vector idiom on [y, z] lists, and either a builtin ram(barrel_pin, rod_pin, pivot, "joint", r_barrel, len, r_rod) or a worked example in language.md/the skill showing barrel-in-parent, rod-in-child with the rod length following the pin-to-pin distance — and state that it costs GLB animation of the cylinders, so an author can choose the joint-per-cylinder form when the export matters.