From 937cf02aff687fc86498b243b2c84315b4bfb94c Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Tue, 16 Jun 2026 09:51:54 -0400 Subject: [PATCH] feat(paint-slots): world-scale pool slab floor UVs + document the metre UV contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../viewer/src/systems/slab/slab-system.tsx | 17 +++-------------- wiki/architecture/materials-and-themes.md | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/viewer/src/systems/slab/slab-system.tsx b/packages/viewer/src/systems/slab/slab-system.tsx index 5732a671..78addcf3 100644 --- a/packages/viewer/src/systems/slab/slab-system.tsx +++ b/packages/viewer/src/systems/slab/slab-system.tsx @@ -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) => { diff --git a/wiki/architecture/materials-and-themes.md b/wiki/architecture/materials-and-themes.md index c96c418a..47de0ccd 100644 --- a/wiki/architecture/materials-and-themes.md +++ b/wiki/architecture/materials-and-themes.md @@ -80,3 +80,17 @@ The editor UI chrome is always dark (a fixed `document.body.classList.add('dark' ## 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. + +## 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.