feat(paint-slots): world-scale pool slab floor UVs + document the metre UV contract

Pool slabs (generatePoolGeometry) were the one procedural surface emitting
normalized [0..1] floor UVs, so a textured finish stretched to fit the pool
instead of tiling at real-world scale like every other surface. Switch the
floor to shape-space metres (x, -z) — the same mapping generatePositiveSlab-
Geometry already uses for its caps. Pool walls were already in metres.

Also document the contract in wiki/architecture/materials-and-themes.md: every
procedural surface generates UVs in metres (1 UV unit = 1 m), GLB slots follow
the same ~1 unit/m authoring convention, and a catalog material's `repeat` is
therefore a per-material world-scale setting (tiles per metre), identical for
every surface that uses it — never per-item.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-16 09:51:54 -04:00
co-authored by Claude Opus 4.8
parent 138543ea8c
commit 937cf02aff
2 changed files with 17 additions and 14 deletions
@@ -254,23 +254,12 @@ function generatePoolGeometry(slabNode: SlabNode): THREE.BufferGeometry {
const positions: number[] = []
const uvs: number[] = []
const indices: number[] = []
const bounds = new THREE.Box2()
for (const [x, z] of polygon) {
bounds.expandByPoint(new THREE.Vector2(x, z))
}
for (const hole of holePolygons) {
for (const [x, z] of hole) {
bounds.expandByPoint(new THREE.Vector2(x, z))
}
}
const floorWidth = Math.max(bounds.max.x - bounds.min.x, 0.001)
const floorHeight = Math.max(bounds.max.y - bounds.min.y, 0.001)
const pushFloorVertex = (x: number, y: number, z: number) => {
positions.push(x, y, z)
uvs.push((x - bounds.min.x) / floorWidth, (z - bounds.min.y) / floorHeight)
// Floor UVs in metres (shape-space x, -z), matching generatePositiveSlabGeometry's
// cap mapping so a finish tiles at the same world scale on every surface.
uvs.push(x, -z)
}
const pushWallVertex = (x: number, y: number, z: number, u: number, v: number) => {