Eight kinds mixed by two sliders: how many of them appear, and which one
leads. Turnip, cabbage, onion, leek, parsnip, carrot, tomato, potato.
The first six are what a medieval European kitchen had and the last two are
New World crops that only reach Europe after 1492 -- they are in because a
kit is more useful than it is a museum, and the carrot is orange rather
than the purple a medieval one would have been. The kinds are ordered so
that the period-correct set costs no thought: `lead: 0` with `kinds: 6`
gives exactly those six and never reaches the two after them.
Two parts, `bulbs` and `roots`, because those are the two ways a vegetable
rests -- on its base or on its side -- and a consumer wanting one should not
have to take the other.
Four faults on the way in, and every one of them was about placement rather
than shape.
The header claimed each vegetable is authored with its lowest point at the
origin, and I asserted that instead of measuring it: a leek's blades lean
four times its own radius away from the axis, and a cabbage's loose leaves
hang below its base. Whichever vegetable dug in deepest then defined the
model's floor and made every other one look as though it were hovering.
Reading the bounding box and setting each one down by what it actually
measures makes the claim true by construction rather than by an argument
that has to be right about eight shapes.
The leaves were flung off their vegetables outright. They were built
already in place and then rotated, and rotation turns a body about the
ORIGIN, so each one moved away by its own distance times the sine of the
angle. Origin, rotate, place -- which is the order the hoe's neck already
has a comment about.
Tightening the heap so it read as a pile rather than a display then made
the bulbs collide, twice. Standing vegetables were never turned about their
own axis, so every turnip presented its facets at the same phase and
neighbours met in parallel faces. And where the turnip's cream body meets
its purple shoulder -- two lathes, because `colourTop` interpolates across a
whole profile and bled the purple down to the root -- both pieces were
capping an end that is buried inside the vegetable. Two turnips touching
met on those hidden discs. They are not generated any more, which is the
same saving `bandGeometry` makes on the inside of a hoop.
32 models, 26 939 triangles, all checks pass, 0 floating cases, GLB
round-trip clean, Conformant, build clean.
Two models that each fill a hole the catalogue could name.
The kit had a torch and a lantern and no HEARTH, which is the wrong way
round: a cooking fire is what everyone in a medieval scene is facing and
the only light in most of them. The cauldron hangs from a tripod over a
ring of hearth stones, with embers that `setLit` puts out. Its belly is
wider than its mouth, it has three short feet because it stands in the
ashes as often as it hangs over them, and it hangs by a bail rather than by
its rim.
And there was a `cart-wheel` with nothing to put it on -- while every
container in the catalogue, crates and sacks and barrels and baskets, is a
thing you would MOVE. The handcart's axle sits aft of the bed's centre so a
load leans on the puller rather than away from him, and its shafts leave
the bed at an angle and rest on the ground, which is what lets one person
lift them to waist height without the load shifting.
The cauldron found a fault in the mill on its way in. Both build their
splayed legs the same way -- lean about X, swing about Y, translate out
along the radius -- and the lean was POSITIVE, which carries the top the
same way the translate does. The two add, so the head ends up at the full
outboard radius and the foot at the axis: the tripod stood on its apex and
the mill's quarter-bars stood on their heads, splayed over the cross-trees
and gathered at the post, which is precisely backwards for a brace whose
job is to carry a load out to the ground. Nothing had caught it in the
mill, because a bar joins post to cross-tree either way round and the
bounding box is identical; in the cauldron it separated the tripod from the
ground and the support check finally had something to say.
Lowering the pot to where it could actually cook then broke its chain, and
for a reason this session has seen before: the link radius was derived by
dividing the drop by the link COUNT while the links are laid out over
count-1 gaps, so each link came out a little smaller than half its own gap
and the chain came apart as soon as the drop grew. The tavern sign's chain
had the same slip.
31 models, 25 221 triangles, all checks pass, 0 floating cases, GLB
round-trip clean, Conformant, build clean.
A village well: a drystone kerb, a timber frame over it, a windlass with
its collars and crank, and a rope and bucket that lower down the shaft.
`setDepth` and `setWinding` drive the bucket; its part origin is the
roller's axis, so winding is a translation and costs nothing per frame.
The kit had no masonry at all, which for a medieval catalogue is a hole
rather than an omission -- a well, a trough, a millstone, a boundary wall
and a hearth are all stone before they are anything else. `stone` is the
twelfth slot. Its colour sits close to weathered oak on purpose: what
separates the two at a glance is not hue but that stone scatters light
completely flat, and roughness is not something vertex colour can carry,
which is exactly the argument the slot table makes for keeping iron and
steel apart.
Four faults on the way in, and three of them are ones this session has seen
before in other models.
The blocks were written footprint-first as [wall, arc]. `rotateY` carries a
footprint's +Z out along the radius and its +X round the circumference, so
every block sat across the ring rather than along it -- each one sticking a
block's length out of a wall a block's width thick. The kerb came out as
rubble thrown at a circle.
The rope started at the roller's axis, which put its whole surface inside
the barrel's hollow interior touching nothing. Then it started in the eye
of the wound coil, which is also a hole. It has to begin within the coil's
material, between 1.0 and 1.34 of the roller radius, so that it crosses the
coil's outer face on the way down.
The actions set `anchor.position.y = 0`, which destroys what `origin` had
just done and dropped the whole rope and bucket by the height of the
roller. Everything the actions touch is now an offset from where the origin
put the anchor.
And the masonry was over the triangle budget at 2 784, almost 1 900 of
which were chamfers on forty-four blocks -- edges the roughen pass softens
anyway, and stone is the one material here where a sharp arris is not
wrong. Plain tapered boxes bring the model to 1 376.
Two things about the wall itself, both from looking at it. Varying each
block by a tenth in arc and thickness put every stone at its own radius and
its own width; a dry wall is irregular the way a hand-cut stone is
irregular, which is a few percent. And blocks cut to exactly their share of
the arc cannot close: a flat face on a curve leaves a wedge of daylight at
every joint, so they are cut long and bite into each other -- safe here
because each is rotated to its own angle and no two adjoining faces are
parallel.
29 models, 22 043 triangles, all checks pass, 0 floating cases, GLB
round-trip clean, Conformant, build clean.
The mill and the sign both changed shape after the table was last generated,
so it was claiming 20 523 triangles against 20 667 and drawing the sign the
wrong way round.
The largest thing in the kit, and the one whose name is its whole design:
the entire body -- millstones, gearing, miller and all -- is balanced on one
massive vertical post and turns bodily on it, so the sails can be pointed
into whatever wind there is. The miller does that by walking the tail
ladder round, which is why the ladder is not a way up but a lever, and why
it reaches so far behind the mill.
Four parts, in the order the thing is built: `trestle` (two cross-trees,
the post standing on them, four quarter-bars), `body` (the boarded buck and
its pitched roof), `sails` (a windshaft leaving the front gable, with its
own origin so `setTurning` spins it alone), and `ladder`. `setTurning`,
`setAngle` and `update` drive the sails at about ten revolutions a minute
-- a working mill turns slowly, not in the blur people draw.
The sails are where the triangles went, and deliberately. A sail is not a
blade, it is a LATTICE the miller dresses with canvas according to the
wind. Drawn solid the silhouette is wrong in the one place everyone looks,
so the bars are modelled and the budget is taken from the buck's boarding,
which reads perfectly well as vertex colour. 1 516 triangles.
Four faults on the way in, all found by measuring rather than by looking:
The roof pitches were built `slope * 2` long and tilted by the complement
of the roof angle, which made two panels 2.79 m across a building 1.80 m
wide, standing up like the covers of an open book. Each pitch covers half a
roof, and the tilt is measured from the horizontal because
`chamferedBoxGeometry` takes an X-Z footprint.
The rails of the tail ladder were tilted the wrong way round. `run` is
positive -- the head sits at a larger z than the foot -- so rotating by
`-lean` laid the rails along the opposite diagonal to the one the rungs
were placed on. They crossed near the middle and diverged towards the ends,
and the bounding box looked correct the whole time because the translation
put the ladder where it belonged and only its internals were wrong. The
support audit found it as loose rungs, and found it only at the small end
of the height slider, where the voxel grid is fine enough not to bridge the
gap: the joint was never real at any size.
Fixing the ladder's foot, which sat 0.10 m below the cross-trees so the
mill hovered above whatever it was placed on, then displaced the rails
perpendicular to their own axis by almost exactly a rung's half-depth --
correcting the rails alone was worse than not correcting at all. Rails and
rungs now share one lift.
And the lowest sail tip came within 0.27 m of the ground, which reads as a
mill about to plough its own field.
28 models, 20 523 triangles, all checks pass, 0 floating cases, GLB
round-trip clean, Conformant at 30 items, build clean.
The registry README's model table was generated once and had gone stale in
exactly the way its own note warns about: it claimed 15 425 triangles
against the current 19 007, listed a barrel at 806 triangles that now has
rivets and six hoops at 1 690, and described "the barrel's 13 staves" when
there are 17. Regenerated from the models themselves.
Both READMEs now open with a contact sheet of the whole kit. It lives in a
tracked `media/` directory rather than in `renders/`, which is ignored
because it is rebuilt output -- a README cannot reference a file that is not
in the repository. The registry's copy points at the raw GitHub URL, since
the npm package ships only dist, README and LICENSE and a relative path
would resolve to nothing there.
The root README gains a Showcase section -- the 60 s default, why it is the
default rather than 30 s, and what makes the variation visible -- and the
support check as a fifth note among the checks worth explaining. It is the
most distinctive thing in the suite and the only one that catches a whole
class of fault the others are blind to by construction.
Also: the check count in that section said ~500 and the suite now runs 818.
The kit is going public, so everything a reader sees is now English:
27 model sources, the core vocabulary, meta.ts (titles, descriptions and
every slider label), the viewer UI, all script output, REFERENCE.md and
the READMEs.
Rules applied: only comments, doc comments and human-facing strings were
touched. Identifiers, numbers, logic, registry item ids, part names and
material slot names are unchanged — those are a published contract and
were already English.
Verified rather than assumed: for all 53 changed .ts files the code
skeleton (source with comments stripped and string contents replaced by a
placeholder) is byte-identical before and after. Full gate is green —
typecheck, registry conformance, ~510 model checks, GLB round trip and a
production build.
Also fixed here:
- index.html and viewer.html were missed by the translation pass because
the file list only covered .ts/.md/.css. Both had lang="tr" and Turkish
titles.
- The viewer formatted triangle counts with toLocaleString('tr-TR'), which
prints 15.425 rather than 15,425.
- REFERENCE.md's model table header did not match what
scripts/catalog-table.ts emits, so the next regeneration would have
silently changed it. Aligned to the generator.
- Removed scripts/probe.ts and scripts/zdiag.ts; they were throwaway
diagnostics that got committed by accident.
A lowpoly medieval procedural model library for Vibe3D, plus the model
inspector used to build it.
Registry (@medieval-kit, published as @medieval-kit/registry):
- core: deterministic mulberry32 RNG, slot-based vertex-coloured materials,
a geometry vocabulary (box, prism, stave, band, head, flip), and the
stable-anchor part slots the Vibe3D protocol requires
- wooden-barrel: 806 tris, 13 separate staves, iron hoops, recessed heads
- wooden-crate: 360 tris, plank rows on corner posts, forged straps
- iron-brazier: 423 tris, typed actions, per-frame flame flicker, own light
The kit declares no renderer capabilities, so it runs on WebGLRenderer as
well as WebGPURenderer. Colour variation lives in vertex attributes rather
than in materials, so each model keeps one draw call per material.
Verification (bun run verify) covers 64 checks without a browser: geometry
validity, winding by two measures (radial alignment for bodies of
revolution, signed volume for closed solids), coplanar-face detection for
z-fighting, stable root and anchor identity across configure(),
deterministic seeding, material ownership, and idempotent disposal.