DEV Community

Board of Project Stewardship
Board of Project Stewardship

Posted on Originally published at boardofprojectstewardship.com

How we built a plan-to-3D house walkthrough that runs entirely in the browser

Homeowners planning a remodel or a new build usually get a floor plan as a PDF and have to imagine the rooms. We wanted a free tool that lets someone trace that plan and walk through it in 3D, without uploading the plan anywhere. The result is the Plan to 3D Walkthrough on the Board of Project Stewardship site. It is one static HTML page plus one JavaScript file, served from GitHub Pages.

This post walks through how it works. The source is public in the Board's GitHub repository (plan-walkthrough/index.html and assets/js/plan-walkthrough.js).

The flow

The page has four steps: upload, scale, trace, then build and walk.

  1. Upload a builder PDF or a straight-on photo of the sheet.
  2. Scale: tap two points on a known dimension and type its length in feet and inches.
  3. Trace walls, doors, windows, room names and stairs. Each storey gets its own plan page.
  4. Build and walk: the walls rise storey by storey, the roof drops on, and you can orbit the model or walk room to room.

There is a two-storey sample house so you can try it without a plan of your own.

Reading the PDF without a server

pdf.js is loaded as an ES module only when someone actually picks a PDF, so the page stays light until then:

async function loadPdf(file) {
  const pdfjs = await import(PDF_URL);            // pdf.js 4.x from a CDN
  pdfjs.GlobalWorkerOptions.workerSrc = PDF_WORKER;
  const data = new Uint8Array(await file.arrayBuffer());
  pdfDoc = await pdfjs.getDocument({ data }).promise;
  await showPdfPage(1);
}
Enter fullscreen mode Exit fullscreen mode

The file is read with file.arrayBuffer() and rendered to a canvas in the browser. Nothing is posted anywhere, which matters when the "file" is the floor plan of someone's house.

Tracing with snapping

Drawing walls by hand on a phone is imprecise, so points snap. The snap threshold scales with zoom so it feels the same at any zoom level, and the first point of a run acts as a magnet so you can close a room:

function snapDraw(x, y) {
  const thresh = Math.max(0.4, 14 / cam.k);       // world units, scaled by zoom
  if (drawing.length >= 3) {
    const a = drawing[0];
    if (Math.hypot(x - a[0], y - a[1]) < thresh) return { x: a[0], y: a[1], close: true };
  }
  // ...then snap to existing corners and to 0, 45 and 90 degree runs
}
Enter fullscreen mode Exit fullscreen mode

Undo and redo are two stacks of JSON snapshots of the project, capped at 60 steps. It is not clever, but it is easy to reason about.

From 2D segments to 3D walls

Every wall is a 2D segment plus a height and thickness. In three.js each one becomes a box, rotated to match the segment:

function addSeg(parent, x1, z1, x2, z2, y, height, thick, mat) {
  const len = Math.hypot(x2 - x1, z2 - z1);
  if (len < 0.04 || height < 0.04) return;
  const geo = new THREE.BoxGeometry(len, height, thick);
  geo.translate(0, height / 2, 0);
  // position at the midpoint, rotate by atan2 of the segment, add to the storey group
}
Enter fullscreen mode Exit fullscreen mode

Doors and windows are stored as a position and a width along a wall segment ({ wallId, seg, t, width, kind }), and the wall is built around each opening. Storeys are built separately, which keeps "lift the roof" and "show only this storey" simple. The build animation raises the walls storey by storey, then drops the roof on.

Walking without going through walls

Walk mode uses WASD on a keyboard and an on-screen joystick on a phone. Each step is checked in 2D against the current storey's traced walls, using a player radius plus half the wall thickness. If the full step is blocked, it tries the x and y moves separately, so you slide along a wall instead of sticking to it:

let nx = player.x + dx, ny = player.y + dy;
if (blocked(nx, ny, player.level)) {
  if (!blocked(nx, player.y, player.level)) ny = player.y;
  else if (!blocked(player.x, ny, player.level)) nx = player.x;
  else return;
}
Enter fullscreen mode Exit fullscreen mode

Door openings let you through, and a stair run climbs to the next storey.

Saving and sharing with no backend

There are three ways to keep a sketch, and none of them needs a server:

  • localStorage holds the current project, so a refresh does not lose work.
  • Download project saves a JSON file you can open later.
  • Copy share link gzips the project JSON with the browser's built-in CompressionStream, base64url-encodes it, and puts it in the URL hash:
const bytes = new TextEncoder().encode(JSON.stringify(slim));
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
const buf = new Uint8Array(await new Response(stream).arrayBuffer());
// btoa(...) then swap + and / for - and _, and set location.hash = "#p=" + b64
Enter fullscreen mode Exit fullscreen mode

The slim copy drops the plan image before encoding, so a shared link carries only the walls, never the uploaded plan. Because it lives in the hash, the data never reaches the web server at all.

Limits we are upfront about

The model is an illustration for talking about layout. It is not a survey, it is not for construction, and dimensions, structure and openings should always be confirmed with the builder, architect or permit set. The page says that above the tool, not in fine print.

Try it

The Board of Project Stewardship publishes free planning guides and contractor directories for homeowners in King and Snohomish counties, Washington. Feedback on the tool is welcome in the comments.

Top comments (0)