A browser tool that prices an exhibition stand while you draw it, so the drawing and the quote can never disagree.

Built for my employer2026Live, feature-frozen since April 2026

01The problem

An exhibition production company gets the same request repeatedly through a show season: a client books a shell scheme and wants to see what fits inside it before signing off. Historically that meant a designer opening CAD software, rebuilding the same rental furniture from scratch, and then a separate person pricing the layout by hand from the contractor’s rate card.

It is a slow manual round trip, and it has a structural flaw: the drawing and the quote drift apart the moment the client asks for one more chair. Meanwhile the sales team cannot answer what does that cost while the client is still on the call. The people who actually needed the answer — account handlers and project managers — were not CAD users at all.

02What I built

A single-page web app, gated behind company email login, in which a non-designer picks a booth footprint, drags catalogue items onto a real-scale floor plan, toggles wall panels segment by segment, and flips into a live three-dimensional view of the same stand. Every catalogue item carries its real dimensions, its height and, where applicable, its rental price, so the cost summary updates as the plan changes. The deliverable is a two-page landscape document: the dimensioned plan with a line-item cost summary, then a grid of renders shot from four fixed camera angles.

  • A catalogue of 54 pre-modelled items across four categories — furniture, fixtures, lighting and audio-visual — 30 of them carrying a rental price.
  • A real-scale floor plan on a 5 cm snap grid, with three preset booth footprints.
  • A per-segment wall builder: outer shell walls are generated in one-metre panels and each can be switched on or off individually.
  • A live 3D view in which each catalogue code is hand-modelled from primitives — tables get legs, display cabinets get shelves and light bars, and lighting items emit actual point lights.
  • Custom item import: upload a mesh file, which is parsed, normalised, previewed as a spinning thumbnail and stored per user; or drop in a photo with dimensions typed by hand.
  • Two-page document export with the cost summary, a one-click image of the plan, and keyboard shortcuts for delete, rotate, copy, paste, duplicate and save.
Schematic of the plan editor A drawn, abstracted representation of the editor: a left catalogue rail with four category headings and three item chips, a square floor plan on a light grid with five placed rectangles and a row of wall panel marks along the top edge where one panel is switched off, a right-hand cost column whose amounts are drawn as bars rather than numbers, and a strip of four empty frames standing for the four scripted camera angles in the export. No price, plan name, client or venue appears anywhere. BOOTH PLANNER FURNITURE FIXTURES LIGHTING AV PLAN COST AMOUNTS ARE PLACEHOLDER

Drawn, not captured. The rail is the catalogue by category, the square is the floor plan on its snap grid with wall panels marked along the top edge and one panel switched off, the right column is the running cost with every amount shown as a bar rather than a figure, and the four empty frames are the scripted camera angles that become page two of the export. No price, rate, plan name, client or venue is shown, and every bar is illustrative.

03As a product

Who buys it
Exhibition and event production contractors who rent out shell-scheme furniture, and trade-show organisers who publish an official furniture rate card to their exhibitors. The secondary buyer is the sales team that needs a costed visual during the call rather than after it.
Value
It collapses two jobs, the layout drawing and the quotation, into one artefact. Because the price lives on the catalogue item and not in a separate spreadsheet, the drawing and the quote can never disagree. It also moves booth layout from a CAD specialist to anyone with a browser.
Positioning
General CAD is more powerful but needs a trained operator and knows nothing about your rate card. Generic online floor planners are interior-oriented and have no exhibition vocabulary at all — shell walls, service counters, socket loads, spotlight types. This is deliberately narrow: it only knows one contractor’s actual rental catalogue, which is precisely why it can produce a quotable number.
Status
Internal tool, live but feature-frozen since April 2026. Login is restricted by email-domain allowlist, so it has never been sold or opened to external users. The design is licensable in principle — swapping the catalogue constant would re-target it at another contractor’s rate card — but nothing of the sort has been attempted.

04How it works

Stack
Next.js with React, Supabase for authentication, Postgres and object storage, and Vercel for hosting. The 3D library and the document exporter are loaded on demand from a CDN rather than bundled, which is why there are only four runtime dependencies in total.
Shape
One editable scene in component state, two renderers over it, three sinks: a JSON row, an object store for thumbnails and meshes, and a client-side document assembler. Row-level security scopes every plan, thumbnail and mesh to its owning user.
One scene, two renderers, three outputs A vertical flow in five levels, every band drawn at uniform width because nothing along this path has ever been counted. A catalogue held as a constant in the source feeds one editable scene of placed items and wall segments, kept in metres and snapped on every drag. Two renderers read that same array, so the plan and the three-dimensional view cannot drift apart. The level below fans into three: the dimensioned plan and four scripted camera frames continue into a two-page document assembled in the browser, while the saved row and its thumbnail end there and feed nothing. CATALOGUE 54 items, four category tabs A CONSTANT IN SOURCE, NOT A QUERY SCENE Items and walls, in metres ITEM ARRAY, REDRAWN ON EVERY DRAG VIEWS One array, two renderers SCENE PLUS THUMBNAIL ON SAVE; MESH APART OUTPUTS Two pages, one row saved PLAN FRAMES ROW DOCUMENT Two pages, built in browser

Width uniform throughout — no path here has ever been instrumented and no per-item quantity was counted, so every band is drawn at equal width and no quantity is claimed.

The drawing and the quote are the same object. A cost line exists only because a catalogue item was placed, so the plan cannot contain something the summary omits, or the other way round. The saved row is a sink rather than a stage: reopening a plan restores the scene, never the document.

The decision I spent longest on

Getting usable 3D frames into the exported document at all. A WebGL canvas normally discards its drawing buffer after each frame, so the obvious capture call returns an empty image, and the screenshot library cannot see into a WebGL context in the first place. The fix was to create the renderer with the drawing buffer preserved and expose an imperative capture handle from the 3D component, so the export routine can move the camera, render synchronously and grab the pixels four times in a row. The cost is a permanent performance penalty on every frame, which I accepted because the document is the actual product.

05Retrieval architecture

There is no retrieval layer here, and the product does not want one. The whole catalogue is 54 items written into the source, so the interesting question was never how to find one; it was how to make sure the drawing and the price can never describe different objects.

That is worth stating as a decision rather than an omission. A catalogue you can read in one screen is a catalogue you should not index, and every structure this system declined to build is a structure that could not later fall out of step with the list it describes.

ModelNo model anywhere. The setup notes describe an optional key for estimating dimensions from a photograph, but no such call exists anywhere in the shipped source, and the photo import path takes dimensions typed by the user.

Corpus
A catalogue of 54 pre-modelled items across four categories, held as a constant in the source rather than in a table — each carrying real dimensions, a height for the three-dimensional view and, where one applies, a rental price. Beside it sits a small per-user body of material: saved plans as documents, plus thumbnails and uploaded meshes in a private store, every one of them scoped to its owner.
Ingestion
The catalogue does not ingest; it ships. It changes only when the source changes, so a deploy is the entire update path and there is no feed, no sync and nothing that runs unattended. The only material arriving at run time is a user’s own: a mesh or a photograph uploaded under caps of a few megabytes for a thumbnail and a few tens of megabytes for a mesh, with an imported mesh subsampled to a face ceiling before preview so that a heavy export cannot stall the animation loop.
Index
None. Nothing was built here, because 54 constants in one array cannot repay a structure. Category is a field on the item and a tab reads it directly; the per-user rows are reached by owner and primary key, which the database already indexes without being asked to. Anything added here would amount to maintaining a second description of a list short enough to read in a single screen — and the list only ever changes when somebody edits the source, which is also when everything else about the app changes.
Query
None. Nothing arrives as a query, and the interface has no field in which one could be typed. A user picks a category tab, which is a predicate already, and then drags an item onto the floor; the only other lookup is a plan or a mesh fetched by its own stored identifier when a saved layout is reopened. There is no phrase to parse, expand or disambiguate anywhere in the product.
Selection
Picking a tab filters the catalogue constant, and that is the whole mechanism — a comparison on one field, evaluated in the browser, over a list that never grows at run time. Saved plans and meshes are selected by owner under row-level security, and a mesh is re-fetched by the exact path stored with the plan rather than searched for. Nothing that comes back is scored, so there is nothing to rerank: the catalogue appears in the order it is written in the source, deliberately, so that the same item is always in the same place on the panel.
Grounding
There is no model to keep honest, so the constraint is structural instead. A price is a property of a catalogue item, which means a cost line can exist only because the corresponding object was placed in the plan — the drawing and the summary are two readings of one array and cannot disagree. The one place a model was considered, estimating a stand’s dimensions from a photograph, was left out: a dimension is either read from the catalogue or typed by the user, so nothing in the exported document is inferred.

Present — this layer exists and runs.Absent by decision — the layer is not there, and the sentence beside it is the reason. Every one of the six is answered on every system in this chapter, so the rows can be read across pages.

06Numbers

MeasureFigureBasis
Catalogue items modelled, across four categories54Verified
Catalogue items carrying a rental price30Verified
Application code1,707 linesVerified
Of which in the single planner component1,345 linesVerified
Runtime dependencies4Verified
Layout precision, at real metric scale5 cm gridVerified
Face ceiling applied to an imported mesh preview2,500Verified
Upload caps, thumbnail and mesh5 / 50 MBVerified
Empty booth to costed documentunder 10 minProjected

Verified — counted directly in the source, the schema and the storage bucket definitions on 2026-08-12, when the deployment also answered normally. These are engineering figures only — no rate-card amount, per-item price or usage number appears anywhere on this page. Projected — an estimate from the interaction cost of the flow — pick a footprint, drag roughly ten to twenty items with snapping, toggle wall segments, export. The app carries no analytics or timing instrumentation, so nothing here was measured.

07Timeline

  1. 2026-04Project starts with its own database project; configuration and schema reach their final state within the week.
  2. 2026-04Initial commit lands effectively the whole application in one drop.
  3. 2026-07Repository moved into cloud storage and a baseline backup pushed. No functional commits since.
  4. 2026-08Still deployed and reachable, and still feature-frozen. Presented here as a working internal tool rather than an actively developed product.

08Looking back

What broke

Three things, and one of them was documentation rather than code. The mesh round trip was fragile: the uploaded storage path and the placed item’s code have to be generated exactly once and shared, or the model silently fails to reload the next time the plan is opened — a bug the source now carries an explicit comment about. Rendering performance in the mesh preview collapsed on large uploads until the original per-frame depth sort was removed and replaced with face subsampling. And the repository carries honest drift: the README documents two social login providers and an optional AI-powered photo dimension estimator, but the shipped code signs users in with email and password behind a domain allowlist, and there is no such model call anywhere in the source. The photo import path exists; it takes dimensions typed by the user.

What it changed

It put a costed, visual answer in the hands of people who cannot use CAD, which was the whole point. The structural idea worth stealing is smaller and more general: the price is a property of the catalogue item, not of a downstream spreadsheet. That single choice is what makes the drawing and the quote a single object rather than two documents someone has to keep in sync, and it is the pattern I reach for whenever a deliverable and its number live in different places.

Where it stands

Live and reachable, feature-frozen since April 2026. An internal tool for a small team, never sold, never opened beyond the company domains — and deliberately left alone rather than quietly rewritten.

Back to Top