A piece of Hong Kong street furniture modelled as source code instead of in CAD: 25 named constants, no installed dependencies, and a printable mesh written out by the standard library alone.

Solo build2026Prototype, never printed

01The problem

The mobile KMB bus-stop sign — a red disc on a grey mast rising out of a conical funnel base — is one of the most recognisable pieces of street furniture in Hong Kong, and almost nobody has ever looked at one up close. The starting point was a round cased ESP32-C3 display module, 45.5 mm across and 11 mm thick, already running and quoting live arrival times for the routes near me. A bare board on a desk is a gadget; the same board inside a faithful miniature of the sign it is quoting is an object.

The design question was whether a piece of street furniture could be reproduced accurately enough to read as itself at desk size, without ever opening a CAD program. Not as a stunt — as a test of whether geometry is better kept as source than as a binary file nobody can diff.

02What I built

A pure-Python parametric model of the sign. Its main dimensions are named constants — 25 of them, with a number of plate, logo and foot dimensions still written as literals inside the calls that build them — and the script emits a complete mesh plus a matching material file. A second script inlines a full three-dimensional viewer and the model into one self-contained page that opens offline with no server and no external request, styled to look like a slicer build plate. Changing a dimension was meant to be a one-line edit and a re-run, with every dependent surface regenerating; that loop does not work on the files as they stand, for the reason in section 07.

  • The whole sign generated in one pass: the round screen disc, the name plate, the vertical timetable plate, a hollow mast, the conical funnel base and three ground-clearance feet.
  • A recessed cavity sized to the module plus half a millimetre of print tolerance.
  • Fully internal cable routing: the lead exits through the back of the disc, runs down the hollow mast, out a drain hole in the base centre, then sideways under 3 mm feet.
  • Five named materials written into the material file, so the model previews in colour rather than in grey.
  • A self-contained offline preview: viewer, controls, loaders and the mesh itself all inlined into a single HTML file.
  • A standard-library-only software renderer with its own depth buffer, flat shading and hand-rolled image encoder — no imaging library involved.
Schematic elevation of the generated object A drawn side elevation of the sign: a round disc holding the display module in a recessed cavity, a name plate and a vertical timetable plate below it, a hollow mast, a conical base and three feet. A line traces the cable from the back of the disc, down the inside of the mast and out under the feet. KMB BUS STOP ELEVATION, PLACEHOLDER SCREEN SCREEN PLATE BASE CABLE

Drawn, not captured. This is a schematic elevation, not a render and not a photograph — the part has never been printed. The disc holds the display module in a recessed cavity, the cable runs down the inside of the mast and escapes sideways under the feet, and the band across the screen is a placeholder. Proportions are indicative.

03As a product

Who buys it
Nobody, by design — it is a desk object for one person. The nearest real audience is Hong Kong transit enthusiasts and the local maker scene, where themed desk pieces have a small but genuine following.
Value
It demonstrates the working method rather than the artefact. The client-facing version of the same argument is that a product enclosure can be specified in code and regenerated on demand, so a hardware change becomes a parameter change instead of a re-modelling job.
Positioning
Hand-modelling in a commercial CAD package gives better surfacing and real fillets, and locks the geometry inside a proprietary file behind a GUI. Code-CAD tools are the obvious alternative; this goes one step further down by emitting a raw mesh from standard-library Python, which means no toolchain at all. That constraint was not aesthetic — the packages everyone reaches for would not install on the build machine, so the pipeline was written to need neither.
Status
Prototype, roughly half-built, and not commercialised in any form. The geometry and the preview are finished; the part has never been printed and no firmware work has been done.

04How it works

Stack
Python 3 standard library only, with no array library, no plotting library and no CAD kernel; Wavefront OBJ and MTL as the interchange format; a three-dimensional web viewer vendored locally for the preview; and a hand-written image encoder built on the zlib and struct modules for offline renders.
Shape
One generator, one artefact, two consumers. Named constants and a handful of primitives accumulate a single vertex list and a face list, each face tagged with a material; revolved shapes are swept as trigonometric loops at fixed segment counts. Everything downstream re-reads that one mesh file rather than sharing state with the generator.
Generation pipeline, five levels, one branch that stops A vertical flow chart in five levels. Twenty-five named dimension constants drive a generator that sweeps and boxes the whole part in one pass and writes a single mesh file with a companion material file. That one file is then read back by two consumers, drawn as two lanes of equal width: an offline preview page, which continues to the shipped artefact, and a still renderer, whose lane ends where it is drawn because it raises on the current material names and has not produced an image of this mesh. Every band is hollow and equal in width — nothing in this pipeline has been counted as a quantity flowing through it, so no band makes a claim about volume. CONSTANTS 25 named dimensions ONE EDIT, ONE RE-RUN GENERATE Sweeps and boxes, no CAD 1,339 VERTICES, 1,196 FACES MESH OBJ plus MTL, five materials ONE MESH FILE, READ BACK TWICE READ BACK Two consumers, one file PREVIEW STILLS SHIPPED One offline page, 719 KB

Width uniform throughout — nothing in this pipeline has ever been counted as a quantity flowing through it, so every band is drawn at equal width and no band claims a volume. The vertex and face counts beside the rail describe the artefact, not the flow.

Nothing here reaches for a CAD kernel, an installed package or the network: the mesh and its viewer both build with the standard library alone. The branch that stops is honest rather than decorative — the still renderer raises on the current material names and has produced no image of this mesh. What the pipeline does not survive is being moved: every script still names the folder the project used to live in.

The decision I spent longest on

Routing the cable. Earlier revisions cut a notch into the outer rim of the disc to let it out, which immediately stopped looking like the real sign. The fix was to commit to a completely intact disc face and push the whole cable path inside the model: a hole through the back of the disc, a hollow mast, a hole through the base, and three small feet to lift the object 3 mm so the cable can escape sideways.

That is four coupled geometry changes across three sub-assemblies — precisely the change that is painful by hand and cheap in a script. A close second was making all the lettering flat reserved panels for stickers rather than embossed geometry, because small embossed type prints badly at this scale.

05Retrieval architecture

Nothing in this project retrieves anything. It is a geometry generator: twenty-five named numbers go in, one mesh comes out, and that same file is read back by two different consumers. Five of the six layers below are empty, which makes this the thinnest architecture in the chapter and, read honestly, one of the clearest.

The only fetch anywhere in the finished object sits upstream of this repository. The display module arrived with its own firmware, which pulls live arrival times from a public feed on an interval that is not mine to quote, because no firmware source lives here. The enclosure is deliberately indifferent to it — the cavity fits the module, and what the module talks to is its own business.

ModelNo model anywhere. The pipeline is trigonometry and arithmetic in the Python standard library, and the only judgement in the whole build was a person holding a render up against a photograph of a real sign.

Corpus
None. There is no body of knowledge to hold. The inputs are twenty-five named dimension constants in one source file and a reference photograph that a person looked at, and neither is stored, searched or consulted while the script runs — the numbers are simply read as code. Nothing accumulates between runs either: every run rewrites the same mesh from the same constants, so there is no history to keep and nothing that could be retrieved out of one. A corpus here would be nothing more than a second copy of numbers that already live in the source file, kept in step by hand.
Ingestion
The only layer here that exists, and it is a person on demand. Someone edits one named constant and re-runs the script; the generator sweeps and boxes the whole part in one pass, tags every face with a material name, and writes a mesh with a companion material file. Nothing arrives on a schedule and nothing arrives over a network — the pipeline needs no installed package at all, because the usual array and plotting libraries would not install on the build machine. A missing input raises and stops, and nothing partial is written.
Index
None. Nothing is ever looked up, so there is nothing to structure in advance. The mesh is a flat vertex list and a face list written once per run, and both consumers re-read it from the top rather than seeking into it. At one thousand three hundred and thirty-nine vertices, a lookup structure would cost a build step and a way of going stale in exchange for skipping a scan that finishes instantly — and it would be a second representation of geometry that currently has exactly one, which is the property the whole method depends on.
Query
None. Nothing is ever asked of this system. There is no request path, no run-time input and not even a command-line argument: changing the object means editing a number in the source and running the script again, which is closer to a recompile than to a question. The object is fixed the moment the file is saved, the same constants always produce the same mesh, and every intermediate revision stays diffable and reversible in a way a binary model file never is. There is no run time here in which a question could be asked, because the run is over before anyone could ask one.
Selection
None. Nothing is chosen, because nothing is ever a candidate. Every face the generator emits is written; there is no pool to filter, no threshold, no score, and therefore nothing that could be ranked, trimmed or reranked after the fact. The one branch in the whole pipeline sits downstream of the mesh, where two consumers read the same finished file for different purposes — and that is fan-out rather than selection, because neither of them is choosing between alternatives and both are handed the whole file. With no candidates there is no ordering, and with no ordering there is nothing that a post-retrieval pass could reshape.
Grounding
None. There is no model anywhere, so nothing can invent a dimension that then has to be caught. What does the job a grounding layer would do is arithmetic and a photograph: the cavity is 46.0 mm across because the module measures 45.5 mm plus half a millimetre of print tolerance, and the red was corrected against a photograph of a real mobile sign after an earlier revision used the maroon that belongs to the fixed platform sign. Errors here surface as a script that raises and writes nothing, never as a plausible sentence nobody checks.

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

Measure Figure Basis
Named geometry parameters exposed 25 Verified
Mesh output, vertices and faces 1,339 / 1,196 Verified
Finished object size 86 × 86 × 249 mm Verified
Electronics bill of materials HK$98.42 Verified
Offline preview, one self-contained file 719 KB Verified
Dependencies installed 0 Verified
Filament cost per printed part HK$6–12 Projected

Verified — computed from the repository's own mesh file, its scripts and its file sizes; the parameter count is 24 independent constants plus one derived from them. Projected — the filament figure assumes roughly 40 to 60 grams of PLA or PETG for a 249 mm part printed as a hollow shell with modest infill, at roughly HK$150 to HK$200 per kilogram. No slicer output exists anywhere in the repository, which is also why print time is not claimed at all.

07Timeline

  1. 2026-05 Round cased display module bought for HK$98.42. It arrived with working arrival-time firmware; no firmware source lives in this project.
  2. 2026-06 The design locked against a reference photograph of a real mobile sign. Generator, renderer and preview builder all written and run the same day.
  3. 2026-06 The self-contained preview page debugged and working offline, after an inlining bug that produced a completely blank page.
  4. 2026-07 Project moved out of the downloads folder into iCloud — which is what left all three scripts pointing at a directory that no longer exists.

08Looking back

What broke

The design took several wrong turns before a reference photograph settled it: a tilted disc, and a red borrowed from the fixed platform sign rather than the brighter one the mobile sign actually wears. Then the nastiest bug, in the preview builder. Inlining a minified viewer produced a completely blank page, because a minified file ends with a single-line source-map comment and no trailing newline, so that comment swallowed the closing script tag and killed every script after it. Neither fix I reasoned my way to is actually present in the shipped builder — it still assembles the page by placeholder substitution and still emits a script tag on one line. The page works because the vendored files happen to end with a newline and carry no source-map comment. The bug is dodged by its inputs, not closed in the code.

The larger failure is quieter and is the current state of the project. All three scripts hardcode the folder the project used to live in, which stopped existing when it moved into iCloud, so the generator fails outright today. The README still quotes an earlier revision's dimensions and documents parameters that no longer exist, and the renderer has no entry for the current material names, so it raises an error on the current mesh and cannot run at all. Only the preview page is current. A parametric pipeline that cannot be re-run is a claim, not a tool, and repointing those paths is the price of getting the claim back.

What it changed

It settled a working method. Code-generated geometry meant the cable-routing redesign — four coupled changes across the disc, mast, base and feet — was a handful of constant edits rather than a re-model, and every intermediate revision stayed diffable and reversible in a way a binary CAD file never is. That argument survives the path rot; it is the reason the repointing is worth doing rather than starting again in a GUI.

Where it stands

Half-built and honest about it: the geometry is finished, the preview still opens, the scripts need their paths fixed, and nothing has been printed.

Back to Top