Commit graph

9 commits

Author SHA1 Message Date
CRT
0b2623755f Add the stone trough, and water to put in it
The kit could water nobody. It had a well, a bucket and a cauldron, and all
three are things you carry water IN; a trough is the thing a village keeps
water in, standing in the street outside the smithy.

Two additions to the core, both earned by measurement rather than by wanting
them:

  - `limestone`, a palette key. `stone` was measured off dressed and rubble
    masonry and is grey, because a wall is quarried and laid face out. One
    block left in the open for a century reads hue 36 and saturation 0.16
    against the wall's 44 and 0.05 — warmer, three times more saturated,
    lighter. Both are right and neither replaces the other. The rendered block
    now matches its reference to within a fifth of a degree of hue and 0.004
    of lightness.
  - `water`, a slot, and it is far less green than instinct says. Sampled
    inside the reference's basin the film reads hue 39.5 — which is the hue of
    the stone UNDERNEATH it, not a colour of its own. It darkens and slightly
    saturates whatever it lies on and does that through the material's
    opacity. Painted the pond green I was about to reach for, the composite
    would have missed the reference by a hundred degrees.

Making it read as CARVED rather than sawn took three goes, and the two failures
are opposite errors worth keeping:

  1. Each wall in two tiers, to give `roughenGeometry` more corners to move.
     The tiers' corners sit at different positions, the hash moved them by
     different amounts, and the render showed a lid on a base with a hard seam
     right round it.
  2. One box per wall, roughened harder to compensate. A box has eight corners
     and every face is two triangles, so moving those corners folded each wall
     along its own diagonal. Not stone: creased cardboard.

Both were trying to get subdivision out of the wrong thing. The walls are
`dishedSheetGeometry`, whose own docstring says the trough wants it: one
continuous body with levels along its length, so the rim undulates because
each level carries its own height, the wall thickens and thins because each
carries its own thickness, and there is no join for a seam to open in.

The z-fight checker earned its keep twice on the same plane, `0,-1,0 | 0`.
Four walls standing on the ground all have a face down there, and where the
corners overlap — which is where they are MEANT to overlap — two of those
faces share it. Lifting the basin floor only moved the pairing from
floor-against-wall to wall-against-wall. The answer was to stop there being
four things down there at all: a trough is solid below the water line, so it
is one block from the ground to the basin with the walls sunk into it, by
different amounts, because two buried faces in one plane are still two faces
in one plane.

Last, two defaults that the render corrected rather than the reference. The
walls at 0.27 of the width shut the basin so far that a three-quarter view
showed only its inner face, against the reference's opening of about three
fifths — now 0.21. And the water at 0.55 of the basin's depth sits below the
near rim and the trough reads as dry from every angle anyone will look at it
from; the reference stands a couple of centimetres under the rim, which is
0.85.

36 models, 31 748 triangles, fourteen material slots, all checks pass, 0
floating cases, GLB round-trip clean, Conformant, documentation in step.
2026-08-27 02:46:05 +07:00
CRT
7d2877c901 Add the oak
The first thing in this kit that grew rather than got made, and the first one
built entirely against reference photographs from the start rather than after
three rounds of guessing.

Two references, generated with the same Codex tool the rest of the kit now
uses: one oak in full leaf and the same tree bare. Both of them contradicted
what I was about to build, and the default lowpoly tree -- a tall pole with a
ball on it -- turns out to be wrong in every measurement that matters.

  crown width / tree height     1.32 to 1.43.  An open-grown oak is half again
                                as WIDE as it is tall. Nothing else in this kit
                                is wider than it is tall.
  widest at                     24% of the height. Not halfway up: a quarter,
                                just above the ground, because the lowest limbs
                                are the longest and leave almost horizontally.
  bole runs to                  72% of the height as one continuous stem. It
                                does not fork low into equal leaders, which is
                                what the leafy photograph alone suggested and
                                the winter one disproved.
  bole width / height           0.130 at 2% of the height, 0.073 at 10%. It
                                nearly halves in the bottom tenth and then
                                barely tapers at all for a quarter of the tree.

The taper table in the model is those rows, read off the photograph one at a
time. The limbs SWEEP -- out at a shallow angle, turning up at the end -- which
comes free from bending each limb along its own axis before rotating it into
place, and which cost a correction I had not expected: bending a limb swings
its whole chord up by half the arc, so every limb was arriving 0.21 rad high
and the crown's widest point had climbed to 38%. Aiming low by exactly what the
bend will lift is the fix, and it is a constant.

Measuring is the whole story of this one. `scripts/probe-tree.ts` prints the
four reference numbers off the built geometry the same way they came off the
photographs, plus one a photograph cannot give: how many separate pieces the
crown is in. A crown is one piece. This one was four, then one, then two, then
one again, and the flood fill was right every time my eye was vague.

Four things it caught that looking would not have:

  - Forks throwing DOWNWARD. The rise of a fork is sin(splay) * sin(spin), so
    the sign of the PRODUCT decides it. I constrained the spin and alternated
    the splay, which left every second fork pointing at the floor. The
    measurement said so plainly while the reasoning said the opposite.
  - The crown torn in two, and not where I kept looking. Locating the loose
    piece -- height 10-64%, bearing 228-313, reaching 5.7 m where the main mass
    stopped at 4.5 -- ended four rounds of widening clumps and evening out
    bearings, none of which were the gap. The gap was the MIDDLE: pushing the
    leaves outboard to open the space under the crown had taken the inside out
    with it, and nine lobes hung off a bare hub. The reference's crown is full
    from the bole outward; the clear space is underneath.
  - Occlusion, exonerated. Turning it off entirely changed the clipping by
    nothing at all, which took two minutes and saved a redesign.
  - My own metric, wrong twice. It counted every dark pixel in the crown's box
    and 87% of what it found was the cast shadow, so it read 10.8% clipping on
    a crown that was barely clipping. And it measured the reference by row
    width but our own geometry by twice its radius, which is a different
    quantity on anything that is not symmetric.

Colour is measured too, and the measurement contradicted me again: I was about
to bake a top-to-bottom gradient into the crown, and sampled in three bands the
reference reads 0.362, 0.336, 0.324 -- no gradient at all. What is there is
local, a spread of 0.45 in lightness within one band. Foliage varies leaf to
leaf, not top to bottom. Chasing that 0.45 with tint, mottle and occlusion all
at once put 2.2% of the crown through the floor into pure black against the
reference's 0.19%, which is the straw trap for the third time: a statistic taken
off fine detail cannot be added on top of the machinery that already
reproduces it.

The bark is measured off the WINTER photograph after the summer one gave a
colour 28 degrees too green -- a trunk under a full crown is lit through the
leaves. Low saturation made it worse than the hue did: a nearly neutral colour
takes its hue from its light, and this renderer's blue-grey sky dragged the
rendered bole to hue 119 on a tree whose bark is brown.

Finished: hue 79.6 against 77.9, saturation 0.386 against 0.381, lightness
0.386 against 0.373, 0.00% clipped. Crown 1.325 wide for its height, in one
piece, hanging from 8% where the reference hangs from 11%.

The honest limits. What fills a real winter silhouette is thousands of twigs
and no budget here reaches them, so `leafiness: 0` is a bare tree of LIMBS --
a good dead or winter tree, not a photograph of one. And the crown still
carries more of its width high than the reference does: widest at 50% against
24%, on a reference profile that is nearly flat from 24 to 44 so the peak is
softer than the number sounds.

2310 triangles, inside the kit's 2500 budget, which cost a side off every clump
and a level off every fork -- but not one clump, because the count is what
holds the crown together. 35 models, 31 404 triangles, thirteen material slots,
all checks pass, 0 floating cases, GLB round-trip clean, Conformant.
2026-08-23 19:47:21 +07:00
CRT
ad6aed30dd Add the stone well, and stone to the palette
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.
2026-08-23 14:18:08 +07:00
CRT
6bae10f593 Stand the bell's frame clear of the bell, and stop painting it brass
Three faults, one of them mine and one that had been there from the start.

The braces were the change this session declined and that reached the repo
anyway, in 715d6af: moved outboard of the uprights, where they connect to
nothing and read as broken sticks. Reverting them was not enough, because
the version they replaced was also wrong -- a single diagonal tilted about
Z, in the same plane as the bell, which had to be shoved in front of it and
drew a line straight across the one silhouette in this model that has to
read cleanly. Neither is what a bell frame does. A real frame is two
A-frames, one at each end of the beam, braced fore-and-aft in their own
planes, with the bell swinging in the gap. Tilting about X instead of Z
puts them there and the bell is never in the way.

The uprights stood at beamLength * 0.34, which at the default yoke is 0.165
against a bell radius of 0.18. With their half-width they spanned 0.147 to
0.183 and ran through the mouth of the bell. Nothing objected: the support
test sees one connected mass, which is precisely what a post driven through
a bell looks like. They now stand at the larger of the yoke's reach and the
bell's radius plus a margin, so a short yoke cannot close the frame onto
the bell -- and the beam's span is derived from that rather than from the
yoke alone, because decoupling the two first left the rail too short to
reach the uprights at the bottom of the slider and the two A-frames came
apart into separate objects.

The foot sat height * 0.16 below the mouth, leaving 24 mm between sill and
lip. A bell hangs clear of its sill; at that gap it read as resting on one.

Colour last. The bell was painted from the `brass` entry and rendered at
hue 39 / saturation 0.56 / value 0.56, against a reference at 33 / 0.34 /
0.33. That is not a subtle disagreement and, unlike the hay bale, the
measurement can be trusted here: a bale photographs desaturated because it
is thousands of straws shadowing each other, while a cast bell is a smooth
surface, so the number is the material rather than the texture. A separate
`bronze` palette entry, derived by inverting the renderer's own gain,
brings it to 30 / 0.31 / 0.43. It is a palette entry and not a slot -- the
mesh still resolves through `brass` -- so the coin pouch and the book's
corner fittings, where fresh yellow bronze is right, are untouched.

27 models, 18457 triangles, all checks pass, 0 floating cases, build clean.
2026-08-22 14:02:22 +07:00
CRT
f64fdfd1eb Tune the oak palette against measured references, and record what did not work
With a reference photograph for every model it became possible to ask a
question the kit could not previously answer: is the colour right? Not
"does it look nice", but how far is it, in degrees and in saturation,
from the real material.

Our oak hue turned out to be correct already -- 26 degrees against 27 in
the photographs -- but saturation ran a median 0.08 high across ten wooden
models, worst on the ones that are almost entirely bare timber: the ladder
and the fence were each 0.16 over. That is the gap between weathered oak
and new pine. After the adjustment the median difference is 0.003 and the
mean absolute error drops from 0.097 to 0.055.

The same measurement said straw and linen were too yellow and too washed
out, and acting on it made them visibly worse, so that change is reverted
and the reason is written into the palette. A bale photographs as
desaturated because it is thousands of straws each shadowing its
neighbour; pushing a flat lowpoly surface to that number does not
reproduce the texture, it just makes the surface garish. The statistic was
real and the inference from it was wrong.

Two further measurements closed off the remaining colour theories. Our
saturation spread is 93% of the references', so the surfaces do not lack
variation, and the hue was never off. Whatever still separates these
models from their references is geometry, not colour -- which is where the
next pass goes.

Also adds the remaining reference shots: all 27 models now have one.

27 models, 17393 triangles, all checks pass, 0 floating cases.
2026-08-22 08:14:16 +07:00
CRT
50c7fc60b9 Translate the whole repository to English
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.
2026-08-21 14:35:13 +07:00
CRT
8a982160ac wicker-basket ekle ve dokümanı 27 modele güncelle
Kullanıcının prop listesindeki son madde meyve/sebzeydi; sepetle birlikte
kapatıldı çünkü kitte zaten sepet eksikti.

Örgü, biçimi yapımdan türetiyor: yatay halkalar önce düz bant olarak
üretilip köşeleri açılarına bağlı olarak içeri-dışarı itiliyor
(yarıçap × (1 + genlik·cos(dikeySayısı·açı + faz))), ardışık sıralarda faz
π kaydırılıyor. Dikey çubuk başına iki segment yeterli — dalga tam olarak
her çubukta bir artı bir eksi örnekleniyor.

Halka başına iç yüz üretmek altı sırada ~800 üçgen tutuyor ve modeli
lowpoly bütçesinin dışına çıkarıyordu; yerine 44 üçgenlik tek parça iç
astar var.

Yeni `produce` materyal yuvası (mumsu kabuk, roughness 0.52). Meyve rengi
`hue` alanından geliyor: aynı modelden elma, şalgam ya da lahana sepeti.
2026-08-21 09:50:34 +07:00
CRT
a3300ff233 Kit'i 26 modele çıkar, doğrulamayı ve inceleyiciyi genişlet
Modeller (13 → 26): chest, bench, torch, lantern, bell, sign, hay bale,
sack, broom, tankard, book, phial, coin pouch. Kürek, çapa ve yaba
render'lara bakılarak yeniden yazıldı.

core:
- createKitModel artık tipli `actions` ve `update(dt)` taşıyor
- BuiltPart.extras: bir parça birden çok materyal yuvası kullanabiliyor
  (sandığın kapağı = meşe tahta + demir kayış, birlikte dönmek zorunda)
- BuiltPart.origin: parça başına dönme merkezi (menteşe, sarkaç)
- bendGeometry / roughenGeometry / mottleGeometry
- 5 yeni materyal yuvası: steel, brass, straw, cloth, leather, glass
- bandGeometry({ inner }) — serbest halkalar kapalı katı olabilsin

my-registry/meta.ts: katalog metadata'sı artık build.ts ve viewer için
tek kaynak. Doğrulama modelle metadata'nın uyuştuğunu denetliyor ve bu
eklendiği gün dört modelde gerçek bir sapma yakaladı.

Doğrulama (~500 kontrol):
- metadata uyumu, eylem semantiği, kare hızı bağımsızlığı
- scripts/render.ts: tarayıcısız yazılım rasterleyici. Bütün doğrulama
  geometrikti; "bu kürek küreğe benzemiyor" diyebilmek için modele
  bakmak gerekiyordu
- scripts/verify-glb.ts: dışa aktar → geri oku → karşılaştır

GLB dışa aktarımı: viewer düğmesi ve toplu CLI aynı kodu kullanıyor.

Viewer:
- parça paneli (solo/izole) ve materyal yuvası paneli (canlı renk override)
- kit görünümü: 26 model tek sahnede, ölçek ve ton tutarlılığı için
- configure() sonrası gölge bayraklarının düşmesi düzeltildi
2026-08-21 09:44:58 +07:00
CRT
98cfbab180 Initial commit: @medieval-kit registry and viewer
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.
2026-08-21 06:15:58 +07:00