bimtex
Start

Build a restaurant

One model assembled in ten steps — lot, street, car park, shell, rooms, openings, fitout, roof. Every step compiles and validates on its own.

A neighbourhood restaurant on a 40 × 30 m lot, with a street along its frontage, a car park in front of it and a full interior fitout — assembled one group of lines at a time.

Every drawing below is live. Each step embeds the model exactly as it stands at that point, compiled in your browser. Edit a line and the drawing follows.

The useful unit of progress is not "finish the model". It is one valid thought at a time — which is why a three-line model already draws a legal site plan, and why step 07 is allowed to fail.

01 · The lot

Every model opens with a project header. After that the first real decision is what the building sits on, because a parcel is what setbacks, coverage and frontage are measured against.

parcel is a rectangle with an address. grade is the ground datum — the zero that every level is later reported against. There may be at most one.

{"t":"project","id":"bistro","name":"Neighbourhood Bistro","unit":"m","typology":"restaurant","frontSide":"south"}
{"t":"parcel","id":"lot","x":0,"y":0,"w":40,"d":30,"address":"120 Market Street","frontage":"south"}
{"t":"grade","id":"grade","elev":0}
Compiling drawing set…

3 entities → 1 solid. The validator is clean.

There is no building yet and the site sheet still draws: the parcel outline, the address, the north point and the lot area. A model is legal at every stage of its own construction, which is what makes it writable one thought at a time.

02 · The street, and a gap in the kerb

street is a centreline plus a width and a lane count, not a drawn rectangle. Its class changes both how it draws — an arterial gets a median, an alley loses its centreline — and how it validates: a driveway onto a freeway is an error, onto an arterial a warning.

curb is the kerb line. Note that the entrance is not an entity. Two kerb runs are declared with an eight-metre gap between them, and that gap is the kerb cut. This is the house style: describe what is built, and let the absence be the opening.

{"t":"street","id":"market-st","name":"MARKET STREET","from":[-6,36],"to":[46,36],"width":12,"lanes":2,"class":"collector"}
{"t":"curb","id":"kerb-west","from":[-6,30],"to":[13,30]}
{"t":"curb","id":"kerb-east","from":[21,30],"to":[46,30]}
Compiling drawing set…

6 entities → 12 solids. The validator is clean.

03 · Setbacks

setback is the first entity in this model that exists purely to be checked against. It draws as a dashed offset from the parcel, and from here on every building placed on the lot is measured against it automatically.

Nothing is being drawn for decoration. The dashed line is the visible half of a rule that will fire the moment a footprint crosses it.

{"t":"setback","id":"setback","margin":{"north":3,"south":6,"east":4,"west":4}}
Compiling drawing set…

7 entities → 12 solids. The validator is clean.

04 · Parking, in one line

paving is the surface. parking is the stall layout, and it is the clearest example in the format of one line becoming many pieces of geometry: a count, a start point and an axis expand into individually drawn stalls at the standard 2.74 × 5.49 m, with the accessible ones widened to 3.35 m and marked.

A planning reviewer reads a site plan for a small number of things, and stall count against floor area is usually the first. So the sheet prints it.

{"t":"paving","id":"lot-apron","use":"parking","x":4,"y":16,"w":32,"d":12}
{"t":"parking","id":"front-stalls","x":5,"y":17.5,"along":"x","count":10,"accessible":2}
{"t":"paving","id":"entry-walk","use":"walkway","x":17,"y":13,"w":3,"d":3}
Compiling drawing set…

10 entities → 27 solids. The validator is clean.

Use paving for a surface and road for anything a vehicle has to be shown to negotiateroad carries width, gradient and turning radius, and is checked for all three. A car park apron is a surface; the drive that reaches it from a highway is a road.

05 · Where the building goes

footprint is the building envelope, and it is the last rectangle anyone types by hand — from here on, walls, roofs and slabs are placed against it rather than beside it.

building is the site record. It points at the footprint by id instead of repeating the numbers, so the outline on the site plan and the walls on the floor plan can never disagree. level is a horizontal datum: a finished floor level and a floor-to-floor height. slab is the floor plate, and it hangs below the level rather than sitting on it.

{"t":"footprint","id":"bistro-fp","x":8,"y":3,"w":20,"d":10}
{"t":"building","id":"bistro-bldg","label":"Neighbourhood Bistro","footprint":"bistro-fp","storeys":1}
{"t":"level","id":"g","name":"Ground","elev":0,"height":3.6}
{"t":"slab","id":"slab-g","level":"g","x":0,"y":0,"w":20,"d":10,"thickness":0.25}
Compiling drawing set…

14 entities → 28 solids. The validator is clean.

The site sheet now computes lot coverage and gross floor area. Neither number was authored.

06 · Four walls, no coordinates

This is the step worth reading twice. Four exterior walls, and not one coordinate between them:

Each wall says which side of the footprint it is, what kind of wall it is and how thick. Where it starts and ends is arithmetic, and arithmetic is where mistakes live, so the compiler does it.

From this step on the drawing to watch is the floor plan — and a plan here is a real horizontal cut at 1.20 m above the floor, looking down. Nothing about it is a picture of a plan.

{"t":"wall","id":"wall-n","level":"g","side":"north","type":"exterior","thickness":0.3}
{"t":"wall","id":"wall-s","level":"g","side":"south","type":"exterior","thickness":0.3}
{"t":"wall","id":"wall-e","level":"g","side":"east","type":"exterior","thickness":0.3}
{"t":"wall","id":"wall-w","level":"g","side":"west","type":"exterior","thickness":0.3}
Compiling drawing set…

18 entities → 32 solids. The validator is clean.

07 · Rooms and partitions — two separate ideas

A room is an area, not an enclosure. It carries a name, a use and an area; it does not generate a single wall. That separation is deliberate: an open-plan kitchen and a shared party wall are both ordinary once rooms and walls are independent declarations.

So the partitions are authored separately, as free walls running from one point to another. Only walls that lie on the footprint edge get to use side.

use is the field that does the invisible work. It is optional — the compiler infers it from an English label — but writing it explicitly makes the model language-independent, so {"label":"厨房","use":"kitchen"} behaves identically to {"label":"Kitchen"}.

{"t":"room","id":"dining","label":"Dining","use":"assembly","level":"g","x":0.3,"y":0.3,"w":11.4,"d":9.4}
{"t":"room","id":"kitchen","label":"Kitchen","use":"kitchen","level":"g","x":12,"y":0.3,"w":7.7,"d":5.2}
{"t":"room","id":"lobby","label":"Service Lobby","use":"circulation","level":"g","x":12,"y":5.75,"w":7.7,"d":1.5}
{"t":"room","id":"store","label":"Store","use":"storage","level":"g","x":12,"y":7.45,"w":3.6,"d":2.25}
{"t":"room","id":"wc","label":"WC","use":"bathroom","level":"g","x":15.9,"y":7.45,"w":3.8,"d":2.25}
{"t":"wall","id":"kitchen-wall","level":"g","from":[11.85,0],"to":[11.85,10],"type":"partition","thickness":0.15}
{"t":"wall","id":"kitchen-back","level":"g","from":[11.85,5.6],"to":[20,5.6],"type":"partition","thickness":0.15}
{"t":"wall","id":"service-back","level":"g","from":[11.85,7.3],"to":[20,7.3],"type":"partition","thickness":0.15}
{"t":"wall","id":"wc-wall","level":"g","from":[15.75,7.3],"to":[15.75,10],"type":"partition","thickness":0.15}
Compiling drawing set…

27 entities → 36 solids. The validator raises 6 indicators here, and is right to.

The validator speaks up here, and it is right to. There are five rooms and not one door, so nothing can be reached from outside. This is what a useful diagnostic looks like: it names the room, states the rule, and proposes the fix — because the reader is usually the model that wrote the file.

Note the severity. It does not block: a sealed volume is buildable — a plant void, a shaft, a sealed archive — and the plan draws exactly the doorless room that was authored, so no two sheets disagree. Blocking is reserved for that contradiction. Everything merely unusual is named and released, because a checker that cries wolf teaches a writer to rearrange a building around a remark.

08 · Openings — the rule that generates the drawing

An opening is a hole punched in a named wall, and it is the single most important operation in the system. A wall run is a one-dimensional span; each opening removes [sill, head] from it, leaving up to four pieces — before, under-sill, over-head, after.

Everything visible in every drawing falls out of that being exact. Look at the plan: the doors are gaps. Nobody drew a gap. The cutting plane at 1.20 m passes through the door, so at that height those pieces of wall do not exist.

Then look at the shopfront windows: they draw dashed, because their 0.60 m sill is below the cut and their 2.60 m head is above it, so the plane passes through glass. That is the convention a drafter would use, arrived at by cutting rather than by remembering to.

Position is a word, not a number. "center", "start", "end", "third" and "two-thirds" are resolved against the host wall's own run. between is the half that matters most — it declares which two rooms the opening connects, and it is what makes the reachability graph, the egress routes and the privacy rules possible.

{"t":"opening","id":"entry","on":"wall-s","kind":"door","at":"center","width":1.8,"room":"dining"}
{"t":"opening","id":"back-door","on":"wall-e","kind":"door","at":"start","width":1,"room":"kitchen"}
{"t":"opening","id":"pass","on":"kitchen-wall","kind":"opening","at":"third","width":1.6,"between":["dining","kitchen"]}
{"t":"opening","id":"lobby-door","on":"kitchen-wall","kind":"door","at":"two-thirds","width":0.9,"between":["dining","lobby"]}
{"t":"opening","id":"kitchen-door","on":"kitchen-back","kind":"door","at":"center","width":0.9,"between":["lobby","kitchen"]}
{"t":"opening","id":"store-door","on":"service-back","kind":"door","at":"start","width":0.9,"between":["lobby","store"]}
{"t":"opening","id":"wc-door","on":"service-back","kind":"door","at":"end","width":0.9,"between":["lobby","wc"]}
{"t":"opening","id":"shopfront-s","on":"wall-s","kind":"window","at":"start","width":4.2,"sill":0.6,"head":2.6}
{"t":"opening","id":"shopfront-w","on":"wall-w","kind":"window","at":"center","width":3.6,"sill":0.6,"head":2.6}
Compiling drawing set…

36 entities → 90 solids. The validator is clean.

Reachability is proved by walking a connectivity graph built from between and room, not by looking at the picture. That is the whole reason those two fields are worth writing.

09 · The fitout — arrays, not coordinates

furniture covers everything placed rather than built. type selects a plan symbol and a set of default dimensions, so {"type":"range"} needs no size — the library knows a commercial range. Authored values always win.

The twelve covers are one line. cols and rows repeat a piece, and gapX / gapY are clear space, not centre-to-centre pitch — an author told "1.5 m aisles" writes 1.5 and does not add the table width back. The array expands into individually addressable entities (tables/r2c4), so a later edit can still move one table.

Plan symbols are functions of the size you give them, not stored pictures. That is why a 2.8 m prep counter and a 0.6 m one both draw correctly.

{"t":"furniture","id":"tables","type":"dining-table","level":"g","x":1.2,"y":1.2,"w":1.2,"d":0.8,"cols":4,"rows":3,"gapX":1.5,"gapY":1.6,"group":["dining-fitout"]}
{"t":"furniture","id":"range","type":"range","level":"g","x":12.4,"y":0.6,"group":["kitchen-equipment"]}
{"t":"furniture","id":"walk-in","type":"walk-in","level":"g","x":17.2,"y":1.6,"group":["kitchen-equipment"]}
{"t":"furniture","id":"prep","type":"counter","level":"g","x":12.4,"y":2.6,"w":2.8,"d":0.8,"group":["kitchen-equipment"]}
{"t":"furniture","id":"wc-pan","type":"toilet","level":"g","x":16.3,"y":8.4}
Compiling drawing set…

41 entities → 175 solids. The validator is clean.

10 · Roof, landscape, signage

roof completes the envelope. Ridge height is derived, never authoredeave + (halfSpan + overhang) × pitch — because a ridge that disagrees with the eaves is exactly the contradiction this format exists to prevent. A flat roof is still a roof: it gets a roof plan, and it terminates the elevations.

tree, landscape and sign are site context, and they are real model objects rather than drawing decoration: the trees appear in the site plan, the axonometrics and the 3D view from the same three lines. A sign prints its height and face area next to itself, because those are the two numbers a signage ordinance caps.

{"t":"roof","id":"roof","type":"flat","pitch":0,"overhang":0.5}
{"t":"landscape","id":"verge","x":4,"y":28,"w":32,"d":2,"planting":{"species":"street tree","count":4,"spacing":8}}
{"t":"tree","id":"tree-west","x":8,"y":29,"canopy":4}
{"t":"tree","id":"tree-east","x":32,"y":29,"canopy":4}
{"t":"sign","id":"pylon","kind":"pylon","x":15,"y":28.8,"w":0.6,"d":1.6,"height":6,"faceH":2.4,"text":"BISTRO"}
Compiling drawing set…

46 entities → 182 solids. The validator is clean.

What the finished model gives you

46 entities → 182 boxes, and from that one geometry set: a site plan, a floor plan, four elevations, two sections, axonometrics and an egress plan. Nothing further was authored.

The use on each room has been doing arithmetic all along — the dining room is 107 m², which as assembly is 77 occupants and as office would be about 12. The exit routes below are traced through the connectivity graph, not drawn.

Compiling drawing set…

The same model as a file lives at docs/tutorial/bistro.ndjson:

npx bimtex docs/tutorial/bistro.ndjson -o out/