---
name: room-from-photos
description: Turn a few phone photos of an existing room into a near-photoreal 3D model and plan with furniture layouts to compare and drag around, as a single Three.js HTML file. Use when the user sends photos of a room, with no floor plan, and wants to see it in 3D or work out where furniture goes: "will the new bed fit in this room", "where should the desk go", "can we try a few layouts".
---

# Room from photos

Photos in, a 3D room out, then layouts the user moves around. **Copy `starter.html` and edit only its `CONFIG` block.** The engine draws the plan (the working drawing), a dollhouse model that cuts away the near walls, live fit checks, option tabs, drag and turn in either view, and each photo with the model drawn over it. The model opens in the room's real finishes (paint, floorboards, paneling, trim, each piece's colours), and a toggle switches to a white architectural model for reading the layout alone. It works like a configurator.

The model renders close to photoreal: three.js 0.162 loaded as ES modules from jsDelivr, physically based finishes from CC0 photo scans in `textures/` (ambientCG wood cut board by board into a strip floor and plank by plank into the paneling, a linen weave, brick and carpet pile, each tiled at its real size; greyscale, so every swatch still tints them; drawn stand-ins when the scans can't load), soft sun shadows, ambient occlusion at half resolution, and a view through each window. The dollhouse is the working view, so it stays bright and clean under a studio light: every swatch reads as itself (a lit wall or floor lands within a few ΔE of its hex), each window is a soft area light, lamp shades glow with a warm bulb inside, and every piece stands in its own contact shadow. Through a photo's camera the room lights itself the way the photos show it instead: the environment is captured from the room's own centre (six small cube faces, blurred, two bounces from a studio light, so it comes out the same after every edit), so the floor's gloss streaks with the windows, the ceiling is a lit warm grey rather than a painted white, and the paneling's bounce warms the corners; exposure, a cooler sun, the view outside blown to white and a phone lens's vignette are graded to match, measured against a real room's five photos. The white model keeps its own studio light and sun, unchanged. It redraws only on change, and a slow GPU drops the occlusion pass while things move; the room-light capture and the shadow redraw run only once a change settles, never per frame. Select a piece in the model and a gizmo appears: arrows slide it and a ring turns it in 15° steps. One-click looks recolour the room, swatch chips set each surface, and a selected piece offers its style variants (a longer sofa, a bigger rug) and its colours. "Add a piece" opens a library: a catalogue of common pieces in real sizes (sofas, tables, storage, beds, rugs, lamps, plants, a TV, art) plus the room's own entries, each shown as the engine's own render of it; a piece added from it lands in the clearest spot, selected, and drags, turns, takes variants and colours and is checked like the rest. Every choice belongs to the current option. The sample room renders on its own.

## Process
1. **Ask only for what blocks you.** You need the photos, ideally one from the doorway and one from the opposite corner, and the new thing's size (a product link or W × D × H). Estimate everything else.
2. **Read each photo by perspective before drawing anything.** Work out which wall faces the camera, which floor lines recede, and what each doorway looks through to. Write the room out wall by wall (top wall left to right, then clockwise), with every door, window, closet, radiator and vent, and check that text against every photo. Skipping this is the most common way to get the room wrong.
3. **Scale from known objects** and list each estimate in `ROOM.verify`. Don't wait for a tape measure.

   | Object | Size |
   |---|---|
   | Interior door | 80″ high; 28–32″ wide (bedrooms often 30) |
   | Outlet / switch | centre 12–16″ / 48″ off the floor; cover plate 4.5″ tall |
   | Ceiling | 96″ unless the photo says otherwise |
   | Mattress | twin 38 × 75, full 54 × 75, queen 60 × 80 |
   | Window sill | 24–36″; head usually lines up with the door head |
4. **Fill `CONFIG`.** Pick any wall as the top of the plan (the one facing you from the doorway reads well); every yaw and `rot` is measured from it. The outline traces the inner wall faces clockwise from the top-left corner, in inches. Place each opening by its centre point on the wall. Existing furniture goes in as items, built-ins go in `fixed`, and the thing being placed gets `new: true`. Write 2–3 layouts that are different ideas (corner, centred, along a wall), each with a one-sentence trade-off. When the room is furnished, make the first option the room as it is.
5. **Colour it from the photos.** Sample the wall paint, flooring, trim and every piece you keep from a lit patch of the photo, away from corners and window glare. A swatch is the finish itself, not the pixel: the engine lights it again, so a sample taken in shade renders about twice as dark. Take the brightest unglared patch and lift it toward the material's own colour (one white wall can read anywhere from #625a47 to #f6edcf in a single photo). Metals go in `paint`. Name each one in `PALETTE` ("Pale sage", "Walnut") so the pickers show names, not hex. Set `ROOM.walls`, `floor`, `trim` and `boards`, and each item's `color` and `color2`. Colours for things not yet bought (the new bed's bedding, a rug) come from the user's inspiration, or else sit quietly with the room's colours. Group the swatches as `paint`, `wood`, `fabric` and `clay` so each surface offers the right ones. Add `LOOKS` for the room's big palette levers (paint the paneling, a darker wall) and `variants` for any piece whose size or kind is in question. When the palette is part of the question, give an option its own `colors` so the options differ as a design choice, and say so in its note. Otherwise keep one palette and let the layouts differ.
6. **Match every photo.** Give each photo a `PHOTOS` entry: `{src: 'photos/door.jpg', label: 'From the door', at: [x, y], h: 60, yaw: 0, pitch: 0, roll: 0, fov: 67}`. The camera stands at plan `at` with its lens `h` inches up; yaw 0 looks at the top wall and 90 at the right one; pitch is up-positive, roll clockwise-positive, and fov is the vertical angle. On a phone's 1× lens that's about 67° for a portrait shot and 53° for landscape; the 0.5× ultra-wide is about 106° and 90°. Read the lens from EXIF when it's there (a 35 mm-equivalent focal length near 13 mm is the 0.5×, 24–26 mm the 1×); when it's stripped, judge it by how much of the room one frame holds. Fix fov from the lens: left free, a solve slides along fov and distance at almost the same error. With three or more photos, solve them jointly (a background agent works well for this): click corners, jambs, sills and board lines in every photo, then least-squares the room's size, the openings and all the cameras together. A five-photo living room came back within 1–4 px per photo and corrected an eyeballed reading by 9″ in length and 3″ in height. The absolute scale rests on known objects (an 80″ door, 2¼″ floor strips). Photos that share no feature can't fix the distance between them: two shots facing opposite ends of a room give its width but not its length. Take that from a known object or a symmetry (a centred fireplace), and list it in `verify`. A second pass with the solved cameras measures each piece's footprint by back-projecting its feet onto the floor. Open the photo in the page and tune the fields until the model's corners, jambs and sill land on the photo's. **If no camera lines them all up, the room is wrong: fix the outline, not the camera.** "Copy camera" hands the numbers back.
7. **Verify.** Serve the page over localhost (`python3 -m http.server`). Judge the look on a real GPU, not headless: open the page with `#debug`, run `python3 snapsink.py <dir>` in another terminal, and call `ROOM.snap(name, w, h, zoom, [x, y, z])` in the console for views (the current view's camera; `zoom` 2 is twice as close, and `[x, y, z]` is the point it looks at, as plan x, height, plan y) or `ROOM.snapPhoto(i, name, true)` for a photo's camera at full quality (`i` counts from 0, unlike `#photo=N`); the images land in `<dir>`, and both sides use port 8794. Put each `snapPhoto` beside its photo: that comparison is the realism test, and score it rather than eyeball it: mean luminance of the ceiling, wall and floor bands and the mean difference of a 20 × 15 grid of block luminances (the living room's photo 1 went from 22 to 11 on that scale when the room lit itself; the ceiling band from 0.91 to 0.63 against the photo's 0.60). Aim a few percent brighter than the photo, never darker. Headless, `#debug&photo=N&bare` gives the same render at the window's size (`--window-size=1000,750` fills a 4:3 photo exactly). Score the dollhouse the other way: the mean colour of a lit patch of each wall and of the open floor, converted to Lab, should sit within about 8 ΔE of its swatch hex (the sample room's walls sit at 2–4 and its floor at 8, most of that the floor's own grain); a dollhouse that drifts past that has taken grading that belongs in the photo view. Run `localStorage.clear()` and reload first, or your own test drags show up. Then take headless captures and look at each one: 1440 × 900 with an item selected, the same at `#look=white`, 390 × 844 through an iframe harness (headless Chrome won't go narrower than about 500 px, so capture a page holding `<iframe src="index.html" width="390" height="844" style="border:0">` at a 500 × 900 window), and one per photo at `#photo=N`. Captures need the network: three.js and the fonts load from CDNs.
8. **Deliver** one `.html` file with the photos beside it in `photos/` and the finishes in `textures/` (`build.py` copies them in; an Artifact publishes both folders as files). Three.js loads from a CDN and everything else is inline, so it opens from any static host. Opened straight from disk (`file://`), a browser won't let the page read its textures, so it falls back to the drawn finishes: serve it. Until you have a capture from the browser the user will open, call the render unverified.
9. **Iterate.** The user drags things or says "try the bed under the window", adds pieces from the library, takes pieces out, and tries paint and finishes in the colour pickers. "Copy layout for Claude" gives `{option, name, place, colors, variants, added}`; paste `place`, `colors` and `variants` into that option in `LAYOUTS`, and each entry of `added` into `ITEMS` under its key: the spec is complete (name, type, size, colours, `new: true`, and `lib`, the catalogue entry it came from), so the piece becomes part of the room and the option's `place` already carries where it stands. A configured piece the user took out is simply missing from `place`. For a real room, keep `CONFIG` in its own `config.js` in a project folder and run `python3 build.py <project-dir>` to splice it into the engine, so engine fixes carry over. Add an option rather than overwriting one the user liked.

## CONFIG reference
- `rot` is the wall an item's back faces: 0 top, 90 right, 180 bottom, 270 left. The front is the side you use (the bed's foot, the drawers, the desk's chair side).
- `rot` takes any angle, not just the four walls. A piece given only `w` is round or square: `d` defaults to `w`.
- Door `hinge` is `'left'` or `'right'` as seen standing in the room facing that wall; `swing` is `'in'` or `'out'`. `type: 'opening'` is a closet or cased opening; `clear` sets the depth kept free in front of it (default 24″). An opening's `label` is a short noun ("Hall", "Closet"), because the checks use it in sentences: "Desk blocks the closet".
- Types: bed, crib, dresser, nightstand, desk (with chair), bookshelf, wardrobe, sofa, armchair, lounge (a bentwood cantilever chair; `throw: true` adds a sheepskin), chair (a side chair sized to its w, d and h), table (`glass: true` for a framed glass top, `shelf: true` for a lower shelf, `round: true` for a round top on splayed legs), rug, box, ottoman, petbed, stands (plant stands with pots), floorlamp (a tripod lamp; `arc: 22` makes an arc lamp that hangs its shade 22″ out in front, so aim it with `rot`, and the checks flag a shade that reaches through a wall; `shade` = radius; an arc lamp's `h` is the arc's crown, and its shade hangs about a third of the reach below it), floorplant (`w` is the canopy's spread and `h` its top, and the pot scales from `w`; `heads` for a leafy tree, `upright: true` for spears, `pole: true` for a climber on a moss pole), art (a framed picture: `mount` = its bottom edge's height, `print: 'landscape' | 'poster' | 'abstract' | 'mirror'`), media (a console; `tv` = the screen's diagonal in inches; `mount: 58` hangs just the TV on the wall at that height, with no floor footprint, drawn dashed in the plan), fireplace (a built-in: `h` is the mantel, `face` and `faceH` the firebox face, `front` the hearth kept clear, `fw` its width).
- Wall-mounted items (`mount`) have no floor footprint and hide while their wall is cut away in the dollhouse view.
- Windows take `panes` (the number of sashes; the default is one per 26″). Openings also take `type: 'niche'`: shelves recessed into the wall between `sill` and `head`. `ROOM.paneling: {walls: [edge indexes], name, color, plank: [min, max]}` panels whole walls with random-width planks; edge i runs from `outline[i]` to `outline[i+1]`, so a rectangle traced from the top-left is 0 top, 1 right, 2 bottom, 3 left. A built-in in `fixed` takes an `id`, which is the key its colours use in `colors`; its `c` is its centre and its `rot` works as for items.
- `LIBRARY` (optional) adds this room's own entries to the "Add a piece" catalogue: `[{id: 'toys', name: 'Toy chest', type: 'box', w: 36, d: 18, h: 20}]`, each an item spec with an `id` and a `name` (`variants` work as on `ITEMS`, so "the new bed in three sizes" is one entry). `group` files it under seating, tables, storage, beds, lighting, plants, rugs or walls; without one it is listed first as "For this room". `{base: false, entries: [...]}` offers only these. The built-in catalogue covers each type in common sizes: a three-seat sofa and loveseat, armchair, lounge and dining chairs, an ottoman, coffee tables (rectangular, round, glass) and a side table, a media console, bookshelf, dresser, nightstand, wardrobe and desk, twin to king beds and a crib, tripod and arc lamps, a tree, a snake plant and a climber, 5 × 8 to 9 × 12 rugs, a pet bed, a wall TV and art. An added piece takes the palette's quietest swatches for its finishes, lands in the biggest clear square (a wall piece on the nearest wall, clear of its openings), is selected with the gizmo on it, and persists per option with its place, colours and variants under `storageKey` beside the older edits. Pieces taken out of an option wait in the library under "In this room". Thumbnails are the engine's own renders, made when the library opens and cached.
- `variants: [{label: 'Yours'}, {label: 'Longer', w: 96}]` on an item; an option picks one with `variants: {sofa: 'Longer'}`. `LOOKS: [{name, colors}]` replace the option's colours in one click; `{}` is the room as configured. Default clearances: dresser and wardrobe 36″ in front, desk 30″, bookshelf 24″, sofa 14″ (the usual reach to a coffee table); a bed needs one side with 20″ free. Override per item with `front` or `side`.
- Checks: things that run into a wall, overlap, or sit in a door swing; a blocked front, closet or bed side; tall things in front of a window; an arc lamp's shade through a wall or hanging into what's under it; open floor and the biggest clear square.
- `python3 test.py <project-dir>` audits the model against those checks and fails on any finding: every part stays over its footprint or a zone a check sees (an arc lamp's reach, a desk chair's front clearance, a hearth), stands on the floor (or its wall) joined to the rest, stays under its height, and every rod or tube ends on another part without passing through one. It also fails when the photo finishes don't load, when the page's `load` event waits for a photo or scan, when the photo view's light shifts as the room is re-captured, or when a piece loses its turn in the model after the ring, a nudge and the turn buttons. An `expect.json` beside a fixture's `config.js` pins what the panel says. Run it after any builder or finish change; `tests/every-piece` holds every type and flag.
- Colour: `ROOM.walls`, `floor`, `trim`, `boards` (board width in inches, 0 = none) and `boardsDir` (`'x'` or `'y'`). On items, `color` is the body and `color2` the second finish: bedding (bed), mattress (crib), legs (desk, table), cushions (sofa, armchair), cushion over a wood frame (lounge), shade over the stand (floorlamp), the pot (floorplant), the print (art), border (rug). Mattress and pillows stay soft white, and shelf books get muted colours of their own. `LAYOUTS[i].colors` overrides any of these for one option, as `walls: 0x…` or `bed: [body, second]`. `look: 'white'` opens on the white model.
- Finishes: `TEXTURES` names the folder of photo scans (default `'textures/'`; `false` keeps the drawn finishes). `textures/SOURCES.md` lists each scan, its ambientCG source and the size one tile covers; swap in any greyscale scan under the same file name and set its size in `SCANS` in the engine. `#debug&drawn` shows the drawn finishes, for a before-and-after.
- Light: every lamp glows by default (a warm bulb inside any part a builder tags `kind: 'shade'`); `ROOM.lamps: 0` turns them all off and `2` doubles them, and an item's `glow: 0.5` scales its own. Every solid piece stands in a soft contact shadow; `contact: 0` on an item removes it, `1` darkens it. Everything that dims or grades applies only through a photo's camera, never in the dollhouse: there, floors and paneling render a little less saturated than their swatch (15 % and 12 % toward grey), painted walls and trim take 70 % of the room's own bounce, and the capture itself sees the wood half as saturated, because wood under that warm bounce goes redder than a photo of it and an off-white wall beside it would tan where the photos show it white-grey; the view outside a window is lit 1.7× and hazed toward white, the way a camera exposed for the room sees it. A room with no window has no daylight in colour: give it a window, or its lamps carry the room.
- URL hash: `#layout=2&view=sw|top&sel=bed&pane=plan&photo=1&look=white` (`layout` and `photo` count from 1); add `debug` for the `ROOM` handle, and `debug&frame=<id>&el=20` for a close-up of one piece or built-in (the visual sweep of `tests/every-piece` uses it). With `debug`, `photo=N&bare` draws that photo's camera alone, filling the window with no photo or UI, for a capture beside the photo; `bench=8` writes frame costs into `<pre id="bench">` (`ROOM.bench(n)` returns them: `still`, `moving` with the shadow map redrawn, `env` for the room-light capture, and draw `calls`); and `exp`, `sun`, `win` (window light), `sky` (the view's brightness), `lamp`, `vig`, `wenv` (how much of the captured room paint takes) and `grey` (how far the wood is greyed for the capture) override the photo view's light levels for tuning against photos (the defaults live in `LIGHT` in the engine; `win` and `lamp` also reach the dollhouse); `lib` opens the library, `libadd=sofa3,art` adds those catalogue entries, and `libtest` adds two, takes one out, and writes the stored state and the copy output to `pre#libtest` for a `--dump-dom` check.

## Common mistakes
| Mistake | Instead |
|---|---|
| Placing walls by eye from the photos | Write the wall-by-wall text, then prove it with the overlay |
| Asking for measurements before drawing | Estimate from known objects, mark them verify |
| Bending the photo camera to hide a wrong room | Fix the outline; the overlay is the test |
| Three nudges of one layout | Different ideas, each with its trade-off |
| Inventing a palette | Sample the room's colours from the photos and name them |
| Shadowed or glary samples | Sample a lit patch of wall, away from corners and glass |
| Adding a person for scale | None: the furniture gives scale, and a figure standing in someone's own room looks odd |
| Editing the engine for one room | Change `starter.html` itself and re-capture the sample |
| Judging realism from the dollhouse view | `snapPhoto` beside each photo: floor tone, wall brightness, lamps and plants show up there first |
| Guessing where the furniture stands | Back-project each piece from the solved cameras. One living room's guesses were off by up to 30″ (a dog bed), 9″ (a coffee table's length) and 6″ (a sofa's depth) |
| Fixing every flag on the "as it is" option | Nudge only within the cameras' 3–5″ tolerance (a table top over a chair arm is not an overlap). Real pinch points stay as findings, and the note names them |
| A third option that nudges the other two | A different idea (move the sofa under the window, put the TV over the mantel) |

## Gotchas
- Headless Chrome renders in software: slow, and fine for layout and the phone check but not for judging light. Use a visible browser on a real GPU for that; a hidden or background tab pauses its frame loop, and the loop is what applies a photo view's grading and captures the room's light. `ROOM.snapPhoto` applies both itself; for any other capture from a hidden tab, call `ROOM.ev('applyLight(); captureEnv(); render3d()')` first, or you'll compare studio light against the photo.
- Headless captures: `"<Chrome>" --headless=new --user-data-dir=<scratch>/st-N --disable-gpu --use-angle=swiftshader --enable-unsafe-swiftshader --window-size=1440,900 --virtual-time-budget=5000 --screenshot=<png> <url>`, one at a time with a timeout (e.g. `perl -e 'alarm 45; exec @ARGV' …`), then `pkill -f "[u]ser-data-dir=<scratch>/st-N"`. The brackets stop pkill from matching and killing your own shell. `file://` URLs work (add `--allow-file-access-from-files` or the textures fall back to drawn ones); give the phone harness's iframe an absolute `file://` URL (a relative one came back ERR_FILE_NOT_FOUND). A capture with the library open takes a minute or more: its thumbnails render in software, so allow `alarm 200`. At phone width, headless SwiftShader never delivers a screenshot of the open library beside the model canvas (it does at desktop size), so capture the phone library over the plan (`#debug&lib&pane=plan`) or with `nothumb`, which skips the thumbnails. Chrome lingers after writing the file, so poll for it and kill rather than wait for exit; on a loaded machine the screenshot sometimes lands before the first frame as a 4 KB white page, so check the size and retry with a bigger budget. Software rendering is slow (a photo view with all passes takes 20–50 s) but its light is the same maths as a GPU's, so luminance measurements from it are valid; only the look needs a real GPU.
- Hosts that wrap the page in their own skeleton (claude.ai Artifacts do) want a copy without doctype, `<html>` and `<body>`: `build.py --artifact` writes one. Such hosts often pass only a bare `#anchor`, so make the defaults (`view`, the first option) the right opening state.
- claude.ai keeps an Artifact blank until the page's `load` event, and that event waits for every image started before it: a five-photo room's thumbnails and scans held one blank for 20–40 s. The engine starts its scans and thumbnails through `afterLoad`; start any new image the same way.
- Once a layout is chosen and something has to be built, the step-by-step build guide is the `3d-assembly-manual` skill.
