bc9@production:~$ ./system --statusoperational // principal-led // secure

One Model, Two Renderers: Keeping 2D and 3D in Sync_

Building an architectural design tool where a 2D floor plan editor and a real-time 3D view stay consistent, and why the answer is to stop treating them as two applications.

engineering[2026.07.04]By David Beltran4 min read

Drag a wall in the floor plan and the 3D view updates instantly. That is the entire pitch for LastPlaan, and it is deceptively hard.

The naive version of this is two applications that message each other. A 2D editor holds its state, a 3D viewer holds its state, and something in the middle tries to keep them agreeing. It works for about a week, then you find the case where a door was moved in one and deleted in the other, and you spend the rest of the project writing reconciliation code.

Stop having two states

The fix is to have one model that neither renderer owns.

Walls, doors, windows, furniture, and labels live in a single store built on Zustand. The Konva canvas is a 2D projection of that model. The Babylon scene is a 3D projection of the same model. Neither is authoritative. Neither talks to the other.

Now “keeping them in sync” is not a feature you build. It is a property you get, because there was never more than one truth to begin with. Split view, where 2D and 3D sit side by side and both update as you drag, stops being a hard mode and becomes the natural consequence of the architecture.

The design pressure this creates is worth naming: the model has to be geometric truth, not drawing instructions. A wall is a pair of points and a thickness, not a line on a canvas. A door is a position along a wall with a hinge side and a swing direction, not a sprite. The moment you let rendering concerns leak into the model, one renderer starts winning and you are back to two states wearing a trench coat.

The interesting problem is openings

A door in 2D is a symbol. A door in 3D is an absence: a hole in a wall with a frame around it, plus a panel that behaves differently depending on whether it is single, double, sliding, or pocket.

Both are derived from the same record. The 2D renderer draws the symbol with its hinge and swing. The 3D renderer computes the opening geometry and subtracts it from the wall mesh. Change the door’s position in either view and both recompute from the same source.

Room detection has the same shape. Rooms are not something a user draws. They are closed regions found by walking the wall graph, and their area falls out of the geometry. There is no room object to keep in sync because rooms are a function of walls.

That is the general rule I would extract: anything derivable should be derived, not stored. Stored duplicates of computed facts are exactly the things that drift.

Undo, and why it came almost free

Because the entire document is one store, history is a stack of states rather than a log of hand-written inverse operations.

This is the part where the architecture pays you back. In the two-application version, undo means implementing an inverse for every operation in both applications and keeping them consistent. Here, undo is a middleware layer over the store. Adding a new tool does not mean writing new undo logic for it.

Collaboration follows the same logic. Yjs provides CRDT-backed shared state over a websocket connection, and because there is one model, there is one thing to synchronize between users rather than a negotiation between two views on each client.

Getting out of the tool

A design tool that cannot export is a demo. Projects serialize to JSON for save and load, floor plans export to PNG, and drawings export to DXF so they can open in real CAD software.

DXF in particular is a discipline. It forces the model to be genuinely geometric, because you cannot export a canvas drawing to a vector interchange format. If the internal model is honest, export is a translation. If it is not, export is where you find out.

The takeaway

If you are building anything with multiple synchronized views, the question to answer first is not “how do I sync these.” It is “why do I have two states.”

Usually you do not need to. One model, several renderers, everything else derived. Sync stops being a problem you solve and becomes a problem you never had.