# House Viewer — project context for Claude Code

Interactive 3D models of houses by Ted Peters Construction LLC, rendered in the browser with three.js.
One shared engine draws any house from a data file. Houses can be as-builts read from plan sheets
(1351 Leighton St., exterior only) or new designs started from a brief (the 36 × 30 duplex, exterior
and interior).

## Who you're working with

Ted is a 1988 UW computer science graduate who built UI systems at Microsoft in the 90s. He's an expert in
Smalltalk, Java and C# and knows JS/TS. He has built about 20 spec houses over the last 15 years, so he
knows construction vocabulary precisely (birdsmouth, rake, frieze, sonotube, 2x10 @ 16" o.c.) — use it.
He prefers a GUI to the command line, believes good data structures beat clever algorithms, and loves
automation. He reviews by looking at the model, and often compares it against photos of the real house.

## Layout

```
engine/                    the shared viewer (plain scripts on one global, HV; no build step)
  core.js                  house-data defaults and lookups, renderer, geometry helpers
  textures.js              procedural textures and the material table, colored from the house's palette
  walls.js                 faces from the footprint, siding with openings, windows/doors/trim, corner boards, foundation
  roofs.js                 gable and shed roofs, valleys, fascia/rakes, ridge caps, brackets, vaulted ceilings
  decks.js                 deck + stair, balcony, porch platform
  fixtures.js              wall-mounted things: barn lights, address numbers, heat pump
  interior.js              floors, partitions and doors, stairs, guards, casework/furniture, room labels, section cut
  plan.js                  2D plan mode: an SVG floor plan drafted from the data, with dimensions and measuring
  site.js                  graded ground, lot, street, hardscape, beds and plants
  model.js                 HV.resolve(): the building model — named walls (per level, with openings), floors, roofs, rooms;
                           no graphics. Framing, sheets and take-offs read this, not the meshes
  framing.js               framing generated from the model ("Show framing"): studs, plates, headers, kings/jacks, joists,
                           rafters, sill plates; the rules are listed at the top of the file
  viewer.js                panel, views, sun, options, hover callouts, HV.start()
  viewer.css
houses/<slug>/house.js     the house: one object assigned to HV.house (data, plus code that generates data)
houses/<slug>/index.html   ~20 lines: three.js, the engine scripts, house.js, HV.start()
houses/<slug>/build.html   the same house in the build chrome (HV.chrome='build'): structure on, skin off, see-through
                           ground, a layer tree (foundation / framing / roof / finished house) with member counts
houses/<slug>/SPEC.md      the "why" behind the numbers, and which ones are still assumptions
houses/<slug>/plans/       source drawings, when there are any
tools/render_check.py      headless screenshots of any viewer, for checking your own changes
tools/compare_renders.py   pixel diff of two screenshot folders: proves an engine change left a house alone
tools/bundle.py            one self-contained dist/<slug>.html per house, for publishing as an artifact
tools/plan_zoom.py         close-up screenshot of a 2D plan (a view-box window)
tools/framing_check.py     turns framing on, prints the model's walls and member counts, screenshots
docs/ROADMAP.md            what's done and what's next
```

Python tools run from the project venv: `.venv/bin/python tools/render_check.py …` (Playwright, Pillow and
NumPy are installed there).

## Coordinate system and units

- Feet, as decimals: 3.5" = 3.5/12. Plan dimensions like 13'-8 1/2" convert directly.
- **x = west** (0 at the east wall, increasing west). **z = north** (0 at the rear/south wall, street side
  high). **y = up** (0 = front grade at the house).
- This matches a floor-plan sheet read left to right with the street at the bottom, which is also how the
  floor-plan views draw. Elevation sheets may be mirrored. On 1351 the A3 elevations were flipped
  left-to-right, and **the as-built floor plan always wins**.

## The house data model (see the header comment in each engine file for the full field list)

- `levels`: per floor `{n, floor, head, plate, joist}`. `head` is the default window head above the floor.
- `footprint`: `[{id, p:[x,z]}…]`, one wall face per edge, listed so the house is on the inside (outward
  normal of a direction (dx,dz) is (-dz,dx)). Wall outlines are **derived**: foundation steps along the
  bottom (`base.zones`), the roofs along the top, notches for garage doors.
- `roofs`: a list. `gable` (rectangle of wall lines, ridge axis, pitch, `hr`, overhangs, `into:` for a cross
  gable whose valleys die into another roof, `vault:` for a rafter roof with a sloped ceiling, `brackets`)
  and `shed` (ledger line, run, pitch, optional beam, posts, knee brackets).
- `openings`: the window/door schedule. Each names a face, `at` (world x or z of the unit centre), size,
  level, type (SH LS FX SC PD ED GD) and a plan code like 2860SH.
- `options`: checkboxes and selects in the panel. Parts opt in with `only:'<id>'` / `unless:'<id>'` /
  `option:'<id>'`; walls recut themselves when an option changes their openings.
- `palette`, `trim`, `decks`, `fixtures`, `site` (lot, street, terraces, items), `views`.
- `front`: which side ('n' 's' 'e' 'w', default 'n') is the front. 2D plans draw it at the bottom of the screen;
  Ted thinks of his houses front-down.
- `interior`: `wells`, `partitions` (with `doors`), `stairs`, `rails`, `fixtures`, `rooms`.
- `interior.dims`: manual dimension strings for the 2D plan. Exterior chains and the width/depth line in
  every room are automatic (`dims:false` on a room skips them).
- `framing:{ roof:'truss'|'rafter' }` and `foundation:{ footing:[w",h"], stem:[t",h"], frost:", pad, post, beam,
  beamSpan, postSpacing, outlet }` drive the generated structure (defaults = TPC typical crawl space; see framing.js).
- `extras(HV)`: an escape hatch for one-off geometry. Prefer adding a word to the data model.

A house file is JavaScript, so it can compute its data. The duplex lays out one unit in unit coordinates
and mirrors it; 1351 generates its terraced side path with a loop.

## Direction (agreed 2026-10-22)

Model first, sheets derived. Ted reviews by looking at the 3D model (and the framing), never by describing plans or
sections; 2D sheets are generated from the building model, not drawn. Order: building model (done) → framing in 3D,
iterated with Ted (first pass done) → a minimal Blaine, WA permit set (site plan, plans, elevations, sections, framing
plans, schedules), using `houses/1351-leighton/plans/` as the template. Take-offs and the link to Ted's own accounting
system (sqlite, double entry) come later. Houses stay self-contained snapshots; shared TPC standards are *extracted*
from them every few houses, not inherited.

## Conventions Ted has confirmed (apply to new houses by default, but ask)

- **Foundations (TPC typical):** crawl space; monopour footing and stem (no construction joint, so no vertical dowels).
  Always two horizontal #4s and anchor bolts. 16"×7" footing, 6" × 24" stem (IRC R404.1.4.2 allows 6" to 4'-6" tall).
  2×6 sill flush outside. Where joists hang on top-mount hangers, the stem moves out ½" instead, so the sill is flush with
  both the building line and the stem's inside face (`foundation.hungOutset`). A stem-top step for hung joists is one joist
  depth (11⅞"). Garage/house walls over a crawl are platformed: the interior stem's sill is flush with its garage edge, the rim
  sits on it, and the wall stands on the subfloor. 12" step panels ride up on the ties to ~12¼", so screed the shorter section ⅜" (high wall down, with a strip
  inside its forms; or low wall up, with a strip on top). Beams pocketed 3" (U-shaped).
  Pads dug to the exterior footing level. Grade 6"+ below the sill. Tightline on the footing ledge, risers on the corner
  boards. 16×6 block vents (75 in² net).
- **Flooring:** Ted typically uses 9" × 48" white oak LVP (`flooring:{ plank:[9,48], color }` in house.js).
- **Dimensions are to framing**, never drywall. The building line is the outside of the framing. Exterior walls
  are 5 1/2" of framing plus 1/2" drywall (siding isn't counted). Partitions: 2×4 = 3 1/2" + drywall both sides;
  2×6 = 5 1/2" + drywall. Garage/house walls are always 2×6 (PNW insulation). The plan adds the drywall back
  into room dimensions automatically (`HV.DRYWALL`), so room rectangles stay at drywall faces.
- **Left and right mean the plan as drawn, front at the bottom.** On a west-facing house, left = north.

- **Wall heights:** precut studs and a doubled top plate, so the framing tops out at 97 1/8" (8' floors) and 109 1/8"
  (9' floors). Max window head = top − 3" − header depth (an 88 1/2" head upstairs fits a 6×6). Upstairs heads are
  typically 7'-2". The upper floor has no rim to hide a header in. In house.js a level's `top` is the framing top and
  `plate` is the ceiling (½" under it); see Leighton & Dodd.
- Trim: plain 1x4 casing and a plain 1x4 head (no cap), plus a sill nose with **no apron**.
- Corner boards 5.5" per face. Their color varies by house; on 1351 they're the body color.
- The interior floor can be about 2" above grade when the joists hang from the sills (so there's no step up).
- Show the lot: lawn inside the lot lines is darker green than the lawn outside them.
- Elevation views must be orthographic so they read like plan sheets.

## Working loop that worked well

1. From plans: read the PDFs at 300 dpi and crop the dimension strings. From a brief: write the brief into
   SPEC.md first, and mark every number you had to invent as **(assumed)**.
2. Change the data first; touch the engine only when a new kind of thing appears. Then give it a word in
   the data model instead of special-casing the house.
3. **Render and look before reporting.**
   `.venv/bin/python tools/render_check.py houses/<slug>/index.html --views ne,n,plan1,open2 --plan 1,2`
   (`--plan` shoots the 2D drafted plans; `plan1`/`open2` are the 3D top-down and cutaway views)
   and read the PNGs. Use `--cam` close-ups for small details, and for standing inside a room.
4. After any engine change, re-render 1351 and run `tools/compare_renders.py` against the previous shots.
   1351 is the regression test: it matched the pre-refactor page to within 0.01% of pixels.
5. Make one change set per round of feedback, then tell Ted in plain language what changed and which
   assumptions he should check.

## Gotchas already hit

- Coplanar faces z-fight. That's why `MAT.wallSide` uses polygonOffset. Corner boards looked like they
  "toggled between 1x4 and 1x6" before this fix.
- Shimmer on orbit means two surfaces are too close. Keep stacked surfaces at least 1/4" (0.02') apart.
  **Don't turn on `logarithmicDepthBuffer`**: it silently disables polygonOffset, and the corner boards start
  shimmering again (this happened once). Don't layer a second skin on a solid: a wall's
  room side is its own extrusion lid (material 2), not a separate drywall plane. Ted saw this on the
  door lites and on the cutaway interior walls.
- Foundation segments must stop at the corners. Running them past the corners leaves little blocks under the
  corner boards.
- The handrail was once built on the inside of the wall. Check which way a face's outward normal points.
- OrbitControls `maxPolarAngle` limits how low the camera can go. The current setup allows eye height and
  clamps the camera to grade.
- The engine is plain `<script>` files, not ES modules, because modules don't load from `file://` and Ted
  opens houses by double-clicking index.html.
- Shadows ignore the renderer's global clipping planes. The section cut therefore also hides the roof and
  upper-floor groups, and the wall materials carry the cut plane themselves with `clipShadows`.
- True elevations look at the ground edge-on, so anything below grade shows. Keep `base.foundBottom`
  shallow on flat lots.
