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) | |
|---|---|---|
| Pieces | 1 | 1 |
| Watertight | no: 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 |
| Stands | yes, 1.212 inside the footprint | yes, 1.261 inside |
| Overhangs | 14% | 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)
- 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 atturn(B, P, angle), each rod a tube in the child's frame aimed atturn(A, P, -angle), and the rod's exposed length isram − barrel + 0.3, so it can never gap or overshoot.swing,rby/rbz,stroke, the twelve*_strokesteps and the six derived pose angles are gone;arm_poseis one line; pins are[y, z]lists withlocal()/turn()/toward()helpers; the joint is namedhouselike 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. - 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. - House detail and materials: an L-shaped handrail with a mid stanchion on the rear deck (
tube(0.035, ...)), an amberglowbeacon on the cab roof, a warmer body yellow (material("#e9b125", rough=0.55)), and the hazard stripes as adecalon the counterweight's rear face only (the x-stacked stripes painted the whole side face black). - 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:48b = angle("boom")[0]— used 90 lines beforejoint(..., "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–102bucket_side = polygon(heel)/mouth = rect(...)/bucket_wall = ...—checkprints 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 (checkshows 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 said1.36 × 1.9 × 1.53. Worked around by reasoning from the main sheet.check --pose transportbounds are still loose to uselessness (boomy 0.46..5.88 while the surface extent is 5.2;grid 200: cell 0.04from 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:75loft(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, likeextrude.dogfood/excavator.aix:54def 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.--quickrenders 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 ... $fwith 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
- 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 everytube), make--focusframe 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_barrelside 0.03 fromstick_brackettop"). Every hard minute of this round was hand-computing frames the tool already had. - Print 2D profile steps in
check(their box and, for a polygon, the point count and winding) and draw them insteps.png; a profile-built part is currently invisible until extruded. Addloft's orientation and a "side profile" idiom (extrude(p, w, "x")reads its x along −z; suggest| flip("z")or anaxis="-x") to the docs. - Ship the
angle()cylinder as a documented pattern with helpers: a two-pointturn/towardvector idiom on[y, z]lists, and either a builtinram(barrel_pin, rod_pin, pivot, "joint", r_barrel, len, r_rod)or a worked example inlanguage.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.