Christopher Anderson

Room Layout

A retail-style 3D planner for selecting and arranging catalog furniture

The Room Layout editor: a furnished 3D living room with a leather couch selected, a floating toolbar above the selection, camera presets on the right, the furniture list bottom-left, and the selected item's position and rotation fields bottom-right

Room Layout is a browser-based 3D planner for exploring catalog furniture in context by selecting different pieces and arranging them in a room. Users can enter their room dimensions, choose finishes and lighting, add furniture from a visual catalog, and arrange it with dragging, keyboard controls, or numeric fields. Layouts are autosaved in the browser for recovery after a reload and can be shared in a URL.

The interface is designed around consumer room planning rather than 3D modeling software. Controls use distances from the room’s walls and clockwise degrees. Furniture can be selected in the 3D scene or conventional UI panels.

Editing the room

Furniture placement

Furniture can be selected directly in the room or from a text list on desktop. Dragging provides direct placement, keyboard controls make incremental changes, and numeric fields set an exact distance from the left and back walls or a clockwise rotation.

Furniture cannot be moved through other items or beyond the room. Collision and edge-snapping calculations account for each item’s rotation, and one continuous drag is recorded as a single undoable change rather than a series of steps.

An armchair is selected, dragged, and rotated while the floating toolbar follows it.

Selected-item controls

On larger screens, the selected-item toolbar floats beside the furniture it controls and follows its position as the furniture or camera moves. It stays in place while someone uses it, so repeated rotation does not move a button out from under the pointer. On a phone, the same toolbar has a fixed position above the details panel instead of covering the smaller room view.

Desktop placement projects the furniture’s outline into screen space and tests several positions around it against both the object and the surrounding UI. Catalog entries can optionally reference a low-poly mesh for toolbar placement. Projecting this simplified geometry keeps the per-frame calculation inexpensive while preserving enough of the furniture’s shape to position the controls clearly. A stability margin keeps the toolbar from switching between nearly equal positions, while a clamped fallback keeps the controls on screen when no candidate fits cleanly.

On mobile, the selected-item toolbar docks above the details panel instead of floating over the room.

Decision record: one placement model per form factor · Decision record: the placement engine stays

Environment and room size

Room controls set its width, depth, and height, as well as wall colors, flooring materials, and lighting moods. These settings are included in autosave and shared URLs along with the furniture.

Furniture stays within the walls during normal editing, but the room can be resized around an existing arrangement. Shrinking it never silently rewrites the furniture positions. Items stranded outside the new walls receive warning outlines, and the application offers an explicit, undoable fix that pulls only those items inside. Furniture larger than the room remains flagged, while any overlap created by the fix remains visible for the user to resolve rather than causing previously valid furniture to move.

After the room is narrowed, stranded items are marked and can be moved inside as one undoable step.

Decision record: shrinking the room never moves furniture

Keyboard, focus, and feedback

Keyboard navigation in the 3D scene

In addition to the surrounding panels, the 3D scene is a focusable region. With nothing selected, the Arrow keys cycle through visible furniture in an order based on its projected position in the current camera view. Enter or Space selects the previewed item. With an item selected, the keyboard can move and rotate it. Additional keys control the camera.

Desktop also provides a stably sorted text list for selecting and managing furniture both on and off screen. Keyboard shortcuts move focus between the scene, list, details panel, and selected-item toolbar.

Focus and accessible feedback

Adding or deleting furniture, closing a surface, undoing a change, and changing responsive layouts can all invalidate an earlier focus target. Instead of passing DOM references between components, each action describes where focus should return. A shared policy resolves that request against the updated application state and hands it to the chosen surface, such as the selected-item toolbar or furniture list. That surface decides which of its elements should receive focus, keeping local decisions within the component that owns them.

Editor operations also produce feedback suited to the input. A blocked keyboard move announces whether the furniture reached a wall or another item, while an item selected from the scene explains how to reach its actions. Repeated movement announcements wait until the keys settle, and newer feedback cancels any stale pending message.

Keyboard controls move and rotate furniture and shift focus between the room, furniture list, and selected-item toolbar.

Decision record: focus decisions happen at the moment of change

Architecture and loading

A shared document and command model

The headless core owns the room document, undo history, selection, persistence, and the interfaces to engine capabilities. Pure domain modules own collision, bounds, snapping, and spawn placement. The Three.js scene renders that document and translates pointer input into world-space intent. Furniture state changes only through the core.

Temporary session state such as selection remains separate from the room document. ESLint enforces the layer boundaries, including preventing features from importing one another or reaching into scene internals.

Decision record: core owns the engine ports

Autosave and shareable scenes

The editor automatically mirrors the latest layout to a recovery draft in the browser. The Share control serializes the same scene model into a URL, using the native share sheet when available and falling back to the clipboard.

Values are normalized so an unchanged layout serializes consistently, and default room settings are omitted so existing version-one URLs remain valid as optional fields are added.

A valid shared URL takes precedence when the editor opens. If it cannot be restored, the editor can recover the autosaved draft and report what happened rather than silently replacing local work.

First paint before the 3D engine

The initial branded loading view and its theme styles are part of the static HTML, so the browser can paint them before the application or 3D engine chunks are downloaded. When the application loads, a matching React progress view takes over without changing the loading layout. The application shell and larger 3D engine load independently, and the shell can begin downloading furniture for a restored layout in parallel with the engine.

An empty room becomes interactive without waiting for catalog models. A restored layout waits only for the furniture collections it references, and later catalog additions load their collections on demand.

The shell streams those downloads so it can report byte progress and detect a stalled transfer without imposing a total time limit on a slow connection. Connection failures remain retryable, while missing or invalid model data is marked unavailable. Each explicit retry starts a new loading cycle. If work from the previous cycle finishes late, its result is discarded instead of being written into the restarted editor.

Internationalization foundations

Application strings and accessible names are routed through Lingui, while numbers and units use the browser’s internationalization APIs. English is the only production locale today, but translation catalogs load independently, language and text direction are applied at the document level, and linting guards visible JSX text and accessible names from bypassing the translation system.

A generated en-XA pseudo-locale expands the English strings to catch untranslated application text and broad overflow problems in browser tests. Supporting another production locale would still require translations and manual layout review.

Verification

Idle-rendering protection

The 3D scene renders on demand instead of running a permanent animation loop, avoiding unnecessary processor and battery use while the room is unchanged. A React bug such as an unstable dependency or state-writing effect can still keep the application updating and repeatedly trigger new renders, so a dedicated end-to-end browser test protects against this class of regression. After camera movement and toolbar placement settle, it verifies that the React application and Scene components do not render again and that the toolbar store receives no further writes.

Browser and accessibility checks

Unit and integration tests cover geometry, document operations, focus policy, persistence, loading, and toolbar placement. Playwright exercises the production build through pointer and keyboard input.

A test-only bridge exposes semantic scene state and a projected pointer target for each visible object. Tests use the editor’s DOM controls and keyboard interface when those are the behaviors under review. Tests that specifically cover canvas selection or dragging dispatch real pointer input at those targets and then assert the resulting document state. The production build omits the bridge.

Browser checks run axe across the main editor and transient states such as errors and dialogs. The repository also includes a short manual assistive-technology checklist for focus changes and live announcements, which cannot be verified from DOM state alone.

Decision records

Architecture decisions are recorded in the repo as they’re made (docs/decisions) and rendered here: