Let's talk
engineering

The 3D preview is generated from the same inputs that produced the price, and stored nowhere

A quoting system for made-to-order fabrication already knows the exact shape of the thing it just priced. It knows the length, breadth and height because the estimator typed them. It knows there are two undershelves, a splashback, four legs and a sink bowl of a particular size, because those are the features whose costs it summed.

It was drawing none of it. The customer received a price and a written description, and had to imagine the rest.

The CAD drawings could not fill that gap — they are two-dimensional, marked not to scale, drawn as raw lines with no reusable components. So the geometry had to come from the same place the price came from: the configuration on the line.

Parametric, not modelled

The rejected options are worth naming because both look attractive from a distance.

Photorealistic rendering needs an artist to build an asset per product. That is a per-item cost that recurs whenever the catalogue changes, and it produces a model that is not driven by the configuration — it shows the item as the artist built it, not as this customer ordered it. For a business whose entire proposition is that every item is different, a fixed asset per product is the wrong shape.

Generated imagery was rejected for the same reason and more sharply. It is not three-dimensional, it is not dimensionally accurate, and it is not driven by the configuration. A picture that is approximately the item is worse than no picture, because a customer will measure off it.

What was left is procedural geometry: boxes and cylinders, positioned and sized from the actual dimensions, with materials that read as brushed steel, glass or painted cast. The realism target was set deliberately at stylised rather than photographic. It is honest about what it is, it is exactly to scale, and it costs nothing per product beyond the rules.

Two pieces: a registry and a layout

The architecture is two separable things, and separating them is what makes it scale past a handful of products.

A geometry registry maps a feature to a function that returns the parts for that feature, given the dimensions, the chosen variant, and a context describing the space it has been allotted. A splashback function knows how to build a splashback. It does not know or care what it is attached to.

An archetype layout knows where things go. It walks the selected features of an item, assigns each one a position and an extent, and calls the registry. Crucially it is keyed to a family of products rather than to a product — a service counter, a work table, a rack, a cooking range, a trolley, a cabinet. Products in a family have the same spatial organisation even when their feature lists differ.

That split is the reason this does not become one authored model per product. Fifty-odd product types collapse to a handful of layouts plus one function per distinct feature, and the counter family alone — six or seven products with near-identical structure — is covered by a single layout.

The pricing engine, notably, cannot supply the layout. It knows what features an item has and what each one costs. It has no concept of where anything sits in space, and there is nothing in the pricing data that could be coaxed into providing it. That knowledge is new and has to be authored somewhere.

Key everything by code

The registry is keyed on the feature’s code, not its identifier and not its display name.

The identifier is per-tenant. Two businesses using the same system have different identifiers for the same concept, so anything keyed on it works for exactly one tenant. The display name is editable by users, and the day someone renames a feature to be clearer, geometry silently stops rendering.

Codes turned out to be fully populated — every feature and every variant in the catalogue had one — which is what made the decision available. That is worth checking before committing to it, because a nullable code column with 90 per cent coverage is not a key.

Missing geometry must be nothing, not an error

The most important behavioural decision is what happens when a feature has no entry in the registry.

It returns nothing and the model renders without it.

That single choice is what allows the feature coverage to be built up gradually rather than all-or-nothing. Of 24 features on the most complex product, around 14 render faithfully and a couple are approximated. The remainder — a curved dome cover, some burner assemblies — need either curved geometry or a hand-built asset, and they are genuinely hard.

If an unmapped feature threw, that product would show nothing at all until the last hard case was finished, which in practice means it would never ship. Because it degrades, the product shows most of itself today and gets better as functions are added, with no change to anything else.

The cost is that a preview can be quietly incomplete, so the coverage state has to be recorded somewhere honest — a per-feature tag saying whether it is modelled fully, approximately, or not at all — rather than left for the viewer to notice.

Generate it, do not store it

The model is built in the browser from the saved line configuration, every time it is shown.

That means no new endpoint, no stored geometry, no cache to invalidate, and no possibility of a preview drifting out of step with the quote it claims to represent. Change a dimension, the model changes, because the model has no independent existence. If a rendered image is ever needed for a document, it is captured from the live model rather than becoming a second source of truth.

Deterministic generation from stored inputs is nearly always preferable to stored output, and the tell for when it applies is exactly this: the inputs are already persisted for another reason.

Two costs that were not obvious

The viewer is a heavy dependency — roughly 280 KB compressed, once. It is loaded lazily as its own bundle, so the rest of the application is unaffected, and every place it is embedded has to preserve that. It is easy to accidentally import it at the top of a shared module and pull it into the main bundle for every user, including the ones who never open a preview.

And there was a typing surprise worth passing on. Importing the 3D library’s component bindings augments the framework’s global element typing with every renderer tag. On any component that was typed loosely — a dynamically chosen icon, held as a generic component type — that augmentation collapsed a styling prop to an impossible type, and three unrelated files stopped compiling. The fix was to type those holders precisely rather than generically, which was the correct thing to do anyway.

Global type augmentation from a rendering library reaches parts of a codebase that have nothing to do with rendering. Budget for it.

The rule

If the inputs to a visualisation are already stored for another purpose, generate the visualisation from them on demand and store nothing. Key the generation on stable codes rather than identifiers or names. And make an unmapped case render nothing rather than fail, because that is the difference between shipping coverage incrementally and shipping it never.

Working on something like this?

We build this kind of software, and we staff the teams that do.

Get in touch