What it is
A bathroom tile planner for Tessera, a fictional tile studio. A visitor enters the room size, places a shower tray or bath (built-in or freestanding), a wall-hung toilet, a vanity, the door, a window and a niche, chooses floor and wall tiles from a catalogue (20 mm mosaic sheets up to 60 x 120 cm slabs), picks a pattern, joint, grout colour and setting-out point, and sees every surface tiled live in 3D with the cut tiles marked. The estimate counts whole and cut tiles, offcuts reused, waste, spares and boxes, then lists the setting materials. A five-step wizard (Room, Fixtures, Tiles, Layout, Estimate) whose steps open in any order. No prices: it is a planning tool, and every quantity is labelled approximate.
Engineering decisions
- All planning logic is pure TypeScript in
core/, with no Three.js or React: geometry.ts (flat polygons in millimetres, Sutherland–Hodgman clipping against rectangles, polygon minus rectangle as convex pieces), catalog.ts (tiles with sizes, thickness, finish and box contents, patterns and their spares margin), config.ts (the plan, limits, presets and a readable share code in the address hash), room.ts (wall frames, item footprints, the five surfaces with their openings, the wet zone, placement checks, drag snapping), layout.ts (pattern lattices and the clipped tiles) and estimate.ts (counts, offcut reuse, materials, CSV and text export). Vitest covers them in tests/unit/tessera.test.ts, including a Monte Carlo check that no pattern ever overlaps two tiles and that joints take the expected share of the area. - Every pattern is a lattice: one or two tile positions per repeat, two repeat vectors and a global turn. Straight, half and third offset are one tile with a shifted second vector; diagonal is the straight grid turned 45°; herringbone is two planks (one turned 90°) repeating by (W + joint, W + joint) and (L + joint, −(L + joint)), turned 45° so the V runs along the room. The setting-out point (corner, centre of a tile, centre of a joint) and the shift move the lattice origin.
- Each tile is clipped to its surface and has every opening subtracted (door, window, niche, the tray or bath footprint and rim, the untiled band above a part-height wall). A tile that keeps its full area is whole; otherwise its leftover outline, measured in the tile's own frame, says whether it is a clean straight cut (a rectangle) or an angled or notched one. Cuts narrower than a quarter of the tile or 40 mm are flagged as slivers, and "Balance cuts" tries 144 shifts and keeps the one with the fewest slivers, then the fewest cuts.
- Counting reuses offcuts the way a tiler would where it is plausible: straight cuts are served largest first from the smallest offcut that fits (guillotine cuts, 3 mm blade, offcuts under 50 mm discarded), and diagonal edge triangles pair up two to a tile. Angled or notched pieces always take a fresh tile. Spares are added per pattern (5% straight, 7% offset, 10% herringbone and diagonal, plus 3% for tiles a metre or longer), then rounded up to whole boxes. Mosaic is counted by its 300 x 300 mm sheet and drawn chip by chip.
- Materials use typical manufacturer data-sheet values, rounded and labelled approximate: C2 cement adhesive by notched trowel (4 mm 1.6 kg/m² up to 12 mm 4.5 kg/m², plus about 1 kg/m² back-buttering for tiles 60 cm and longer); grout by the standard formula (A + B) / (A x B) x tile depth x joint x 1.6 kg/dm³; a two-coat liquid membrane at 1.4 kg/m² over the whole floor with a 150 mm upstand and the shower or bath walls to 2.0 or 1.8 m; sealing tape along the floor joint, wet corners and tray edges; primer at 0.15 L/m²; levelling clips at one per 300 mm of joint for tiles from 30 cm, spacers below that; sanitary silicone at about 12 m per 310 ml cartridge; edge trim for the tiling top edge, the window reveal and the niche.
- Placement checks are typical clearances, not a building-regulation check: overlaps, a blocked doorway, the door swing, about 400 mm from the toilet centre to a side, 600 mm clear in front, a niche outside the wet zone, a window inside the shower.
Rendering
- Plain Three.js, no model files. Each surface is one merged mesh: every tile piece is fanned into triangles with UVs in the tile's own frame (randomly mirrored per tile), so a single canvas-drawn face per tile design (colour, marble veins, terrazzo chips, concrete, limestone, pooled zellige glaze, edge bevel) serves the whole surface. A grout bed under the tiled region shows through the joints. The cut view only rewrites the vertex colours.
- Fixtures are built in code from lofted rounded rectangles, lathes and tubes: a shower tray with a linear drain, walk-in glass screen, rain head on a wall arm, mixer valve and riser rail with hand shower; a built-in bath with a deck rim, taps, shower rail and hinged screen, or a freestanding bath with a floor-standing mixer; a wall-hung toilet with a raised lid, flush plate and roll holder; a floating oak vanity with a vessel basin, tall mixer, soap, a plant and a backlit mirror; a panelled door with a lever handle and architraves; a window with a tiled reveal, mullion, stone sill and a sky beyond; a niche lined with the wall tile and lit along its top.
- Walls between the camera and the room fade (dollhouse); the parts fixed to a faded wall hide, and an elevation hides whatever stands against the wall in front, like a section drawing. Views: 3D orbit, plan, and an elevation of each wall.
- Dragging: floor items follow the pointer on the floor, hug the nearest wall and snap to 50 mm and to corners; the door, window and niche slide on their wall plane; the tray or bath has two resize handles. A touch that starts on an item drags it instead of scrolling.
Performance
- Rendering is on demand and stops off screen or in a hidden tab. Only surfaces whose inputs changed are rebuilt, and only fixtures whose settings changed; the estimate is computed from a deferred copy of the plan so dragging stays responsive.
- The light tier, and software renderers, drop antialiasing and shadows and use fewer segments and a lower pixel ratio.