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 positions: number[] = []
const uvs: number[] = [] const uvs: number[] = []
const indices: 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) => { const pushFloorVertex = (x: number, y: number, z: number) => {
positions.push(x, y, z) 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) => { const pushWallVertex = (x: number, y: number, z: number, u: number, v: number) => {
+14
View File
@@ -80,3 +80,17 @@ The editor UI chrome is always dark (a fixed `document.body.classList.add('dark'
## Adding a theme ## Adding a theme
Append a `SceneTheme` to `SCENE_THEMES` with all required fields. `clayTints` is a `Partial` — any role you omit falls back to the active `colorPreset`. The theme pickers (toolbar + community overlay) render a 2×2 swatch from `clayTints` over `background`, so populate at least `wall`/`roof`/`floor`/`glazing` for a good swatch. Append a `SceneTheme` to `SCENE_THEMES` with all required fields. `clayTints` is a `Partial` — any role you omit falls back to the active `colorPreset`. The theme pickers (toolbar + community overlay) render a 2×2 swatch from `clayTints` over `background`, so populate at least `wall`/`roof`/`floor`/`glazing` for a good swatch.
## Texture world scale (UVs in metres)
Every procedural surface generates UVs in metres: 1 UV unit = 1 m.
This contract is shared by wall `systems/wall/wall-system.tsx` (`ExtrudeGeometry`), slab `systems/slab/slab-system.tsx` (`generatePositiveSlabGeometry`, and `generatePoolGeometry`), ceiling `systems/ceiling/ceiling-system.tsx`, roof `systems/roof/roof-system.tsx`, and chimney/dormer `nodes/src/chimney/geometry.ts`.
GLB item slots follow the same ~1 UV unit/m authoring convention, enforced by the slot validator's UV-presence check and the phase-6 Blender recipe. This is an authoring requirement, not a render-time correction.
A catalog material's `repeat` (`mapProperties.repeatX/repeatY` in `packages/core/src/material-library.ts`) is therefore a per-material world-scale setting: tiles per metre.
`repeat: 1` means 1 tile/m, `0.4` means one tile every 2.5 m, and `1.5` means 1.5 tiles/m.
Repeat is a property of the material, identical for every surface that uses it, never per-item or per-surface. Custom repeat values are intentional material scale, not per-surface hacks.