Floor plans

layout: floorplan is the drawing engine under an architect’s vocabulary: the same datum, anchors and dimensions, with walls, openings and fixtures in place of parts and features.

Walls and the openings in them

{ layout: floorplan; unit: m; scale: 0.02; }

|wall#outer| {
  draw: move(0, 0) right(7.2):north down(4.8):east left(7.2):south close():west;
} [
  |window| { on: north; at: 2.7; width: 1.6; }
  |door#entry| { on: south; at: 1.05; width: 0.95; swing: right; }
  |door| { on: west; at: 1.4; width: 2.4; symbol: sliding; }
]
|partition#bathwall| {
  draw: move(4.9, 0) down(2.3):face right(2.3):side;
} [
  |door| { on: side; at: 0.15; width: 0.8; }
]

A |wall|’s draw: traces the centreline. thickness: — 200 mm by default, 100 for a |partition| — grows that line into the mitred outline that takes the paint.

unit: m makes one drawing unit a metre, and scale: 0.02 is the 1:50 ratio.

An opening rides in its wall’s [ ], stationed on: a named segment at: a distance from its start. The gap clips the wall at the jambs, and each type draws its own chrome:

  • |door| — a leaf and a quarter swing arc, or two panels for symbol: sliding.
  • |window| — a sill pair.

The entry states no hinge: because every built-in size and default is real: a door is 900 mm and hangs on its segment’s near jamb.

Mind the draw direction, twice. Both of these read from the pen, not from the picture:

  • at: counts from the segment’s start. The entry’s 1.05 is measured from the east end, because south was drawn leftward.
  • swing: names the left of the pen’s travel — which on that same leftward run is the outside. right is what opens the door into the flat.

The bathroom’s own wall ends at the shell rather than crossing it, which is what keeps a partition out of the middle of an opening.

Fixtures and room text

{
  layout: floorplan; unit: m; scale: 0.02;
  // a furniture plan tints what it is about, and leaves the shell alone
  |bed| { fill: --purple-wash; stroke: --purple-deep; }
  |sofa| { fill: --purple-wash; stroke: --purple-deep; }
  |dining| { fill: --rose-wash; stroke: --rose-deep; }
  |bath| { fill: --sky-wash; stroke: --sky-deep; }
  |appliance| { fill: --sky-wash; stroke: --sky-deep; }
}

|wall#outer| {
  draw: move(0, 0) right(7.2):north down(4.8):east left(7.2):south close():west;
} [
  |window| { on: north; at: 2.7; width: 1.6; }
  |door#entry| { on: south; at: 1.05; width: 0.95; swing: right; }
]
|partition#bathwall| {
  draw: move(4.9, 0) down(2.3):face right(2.3):side;
} [
  |door| { on: side; at: 0.15; width: 0.8; }
]

|bed|    { rotate: 90; translate: 1.15 1.05; }
|sofa|   { symbol: two; rotate: 90; translate: 0.6 3.4; }
|dining| { symbol: round; translate: 3.3 3.3; }
|rect#counter| { width: 0.6; height: 1.4; translate: 6.8 3.2;
                 fill: --bg; stroke: --stroke-dark; stroke-width: 1; }
|appliance| "F" { symbol: fridge; translate: 6.8 2.8; }
|bath| { symbol: shower; translate: 6.6 0.6; }
|bath| { symbol: toilet; rotate: 90; translate: 5.35 0.5; }
|bath| { symbol: sink;   rotate: 90; translate: 6.85 1.6; }

"STUDIO 27 m²" { translate: 2.2 2.2; }
F STUDIO 27 m²

Six symbol-bodied types carry the furniture — |bed|, |sofa|, |dining|, |bath|, |appliance|, |stairs| — and symbol: picks the variant, so one type draws the tub, the toilet and the basin alike.

Their bodies are true millimetres, which is why not one of them states a size. They place with translate: from the datum like any drawing child, and they mask what they sit on.

rotate: turns a piece to its wall, and the reference is the unturned pose: a toilet backs west at 0°, so 90° stands it against a north wall.

Two things that are not special types:

  • A room name is plain sheet text. There is no room type, because a room is what the walls already enclose.
  • A counter is a plain |rect| filled --bg, so it masks the floor exactly as a fixture does.

The tint is not part of the family either. Five type rules make this a furniture plan — sleeping and seating in purple, dining in rose, plumbing and white goods in blue — while the shell stays the black poché a plan expects.

Dimensioning to the faces

|floorplan#flat| "UNIT 3B" { unit: m; scale: 0.02; } [
  |wall#outer| {
    draw: move(0, 0) right(7):north down(4.6):east left(7):south close():west;
  } [
    |door#entry| { on: east; at: 3.4; width: 0.95; swing: right; }
  ]
  |partition#bedwall| { draw: move(2.8, 0) down(4.6):side; } [
    |door| { on: side; at: 1.9; width: 0.8; }
  ]

  outer:west-in (-) bedwall:side-out { side: top; }
  bedwall:side-in (-) outer:east-in { side: top; }
  outer:west-in (-) outer:east-in { side: top; }
  outer:north-in (-) outer:south-in { side: right; }
]
2.65 4.05 6.8 4.4 UNIT 3B

Every named wall run derives two face anchors — its offset edges:

  • name-in — the enclosed side on a closed loop, or the left of the pen’s travel on an open one.
  • name-out — the other face.

That distinction is what a plan publishes: a room’s clear span, face to face. Never a centreline, and never a span that runs through a wall.

bedwall:side was drawn southward, so its -out face is the western one — the left-hand room’s — and its -in face the eastern one. Read them the other way round and each dimension eats the partition: the two would sum past the overall instead of 2.65 + 0.1 + 4.05 = 6.8.

Dimensions sharing a side pack into rows, which is what keeps the two room clears inside the shell’s own overall. |floorplan| is the container form, and its label lowers to a title under the plan.

The whole parts library

WALLS solid · 200 |partition| · 100 thickness: 0.4 fill: --bg hatch(45) section mitred corner arc · concentric DOORS D1 start · left start · right end · left end · right symbol: double symbol: sliding |partition| · schedule tag WINDOWS · STAIRS 1200 default width: 2 in a |partition| two on one run steps: 4 steps: 8 BEDS queen king double single SOFAS three two one corner stool DINING six four round BATH tub shower toilet sink double-sink APPLIANCES F W DW stove fridge washer dishwasher
{
  density: 4; font-size: 19;
  |head::block| {
    layout: flow; align: center; font-size: 22; letter-spacing: 1;
  }
  |cap::block| { layout: flow; align: center; color: --muted; }
}

|floorplan#parts| { unit: m; scale: 0.02; } [

  // ── Walls. A `|wall|`'s `draw:` is the **centreline**; `thickness:` offsets
  // it half either way into the outline that takes the paint — mitred at the
  // corners, bevelled past miter limit 4, concentric round an `arc()`.
  |head| { translate: -3.7 0; } [ "WALLS" ]
  |wall| { draw: move(-1.2, 0) right(2.4); }
  |partition| { draw: move(1.5, 0) right(2.4); }
  |wall| { draw: move(4.2, 0) right(2.4); thickness: 0.4; }
  |wall| {
    draw: move(6.9, 0) right(2.4);
    fill: --bg; stroke: --stroke-dark; stroke-width: 1;
  }
  |wall| {
    draw: move(9.6, 0) right(2.4);
    fill: hatch(45); stroke: --stroke-dark; stroke-width: 1;
  }
  |wall| { draw: move(12.9, -0.5) right(1.2) down(1); }
  |wall| { draw: move(15.1, 0.25) arc(2.2, 0, 1.8); }
  |cap| { translate: 0 1.3; } [ "solid · 200" ]
  |cap| { translate: 2.7 1.3; } [ "|partition| · 100" ]
  |cap| { translate: 5.4 1.3; } [ "thickness: 0.4" ]
  |cap| { translate: 8.1 1.3; } [ "fill: --bg" ]
  |cap| { translate: 10.8 1.3; } [ "hatch(45) section" ]
  |cap| { translate: 13.5 1.3; } [ "mitred corner" ]
  |cap| { translate: 16.2 1.3; } [ "arc · concentric" ]

  // ── Doors. `hinge:` picks the jamb by the segment's draw direction and
  // `swing:` the side the leaf opens toward — `left` is the left of the pen's
  // travel, so on these east-running walls it opens north. The chrome is
  // generated: a leaf at 90° open plus its quarter arc, two mirrored halves for
  // `double`, and two overlapping panels for `sliding`, which hangs no leaf. An
  // opening's own text is its **schedule tag**, seated beside the gap on the
  // face the leaf never sweeps — the last cell wears one.
  |head| { translate: -3.7 2.9; } [ "DOORS" ]
  |wall| { draw: move(-1.2, 2.9) right(2.4):run; } [
    |door| { on: run; at: 0.75; hinge: start; swing: left; }
  ]
  |wall| { draw: move(1.5, 2.9) right(2.4):run; } [
    |door| { on: run; at: 0.75; hinge: start; swing: right; }
  ]
  |wall| { draw: move(4.2, 2.9) right(2.4):run; } [
    |door| { on: run; at: 0.75; hinge: end; swing: left; }
  ]
  |wall| { draw: move(6.9, 2.9) right(2.4):run; } [
    |door| { on: run; at: 0.75; hinge: end; swing: right; }
  ]
  |wall| { draw: move(9.6, 2.9) right(2.4):run; } [
    |door| { on: run; at: 0.45; width: 1.5; symbol: double; }
  ]
  |wall| { draw: move(12.3, 2.9) right(2.4):run; } [
    |door| { on: run; at: 0.3; width: 1.8; symbol: sliding; }
  ]
  |partition| { draw: move(15, 2.9) right(2.4):run; } [
    |door| "D1" { on: run; at: 0.75; }
  ]
  |cap| { translate: 0 4.25; } [ "start · left" ]
  |cap| { translate: 2.7 4.25; } [ "start · right" ]
  |cap| { translate: 5.4 4.25; } [ "end · left" ]
  |cap| { translate: 8.1 4.25; } [ "end · right" ]
  |cap| { translate: 10.8 4.25; } [ "symbol: double" ]
  |cap| { translate: 13.5 4.25; } [ "symbol: sliding" ]
  |cap| { translate: 16.2 4.25; } [ "|partition| · schedule tag" ]

  // ── A window draws its sill pair across the gap at the thickness's thirds;
  // openings share a run freely as long as they never overlap. A flight draws
  // `steps` treads at a 250 mm going, plus the up arrow.
  |head| { translate: -3.7 6; } [ "WINDOWS · STAIRS" ]
  |wall| { draw: move(-1.2, 6) right(2.4):run; } [
    |window| { on: run; at: 0.6; }
  ]
  |wall| { draw: move(1.5, 6) right(2.4):run; } [
    |window| { on: run; at: 0.2; width: 2; }
  ]
  |partition| { draw: move(4.2, 6) right(2.4):run; } [
    |window| { on: run; at: 0.6; }
  ]
  |wall| { draw: move(6.9, 6) right(2.4):run; } [
    |door| { on: run; at: 0.2; width: 0.7; }
    |window| { on: run; at: 1.2; width: 1; }
  ]
  // the two flights stand on the row's baseline, so every caption in the row
  // sits on one line however tall the piece above it is
  |stairs| { steps: 4; translate: 10.8 6.6; }
  |stairs| { steps: 8; translate: 13.5 6.1; }
  |cap| { translate: 0 7.35; } [ "1200 default" ]
  |cap| { translate: 2.7 7.35; } [ "width: 2" ]
  |cap| { translate: 5.4 7.35; } [ "in a |partition|" ]
  |cap| { translate: 8.1 7.35; } [ "two on one run" ]
  |cap| { translate: 10.8 7.35; } [ "steps: 4" ]
  |cap| { translate: 13.5 7.35; } [ "steps: 8" ]

  // ── The furniture. Every body is one path on the fixture's own node, filled
  // `--bg` so it masks the floor under it; the smart label reads **beside** the
  // body, like a discrete's value — so a row seats its pieces on one baseline
  // and their labels line up without a caption being written at all. A bed is
  // named for its mattress: `queen` is what a bare `|bed|` draws.
  |head| { translate: -3.7 9.4; } [ "BEDS" ]
  |bed| "queen" { translate: 0 9.4; }
  |bed| "king" { symbol: king; translate: 2.7 9.4; }
  |bed| "double" { symbol: double; translate: 5.4 9.45; }
  |bed| "single" { symbol: single; translate: 8.1 9.4; }

  // One anatomy at four widths — outline, then the run tracing arm → back →
  // arm; `one` is the armchair, `corner` folds the same two strokes round an L.
  // The bar `stool` is the family's one-stroke member: a plain round seat.
  |head| { translate: -3.7 12.2; } [ "SOFAS" ]
  |sofa| "three" { translate: 0 12.95; }
  |sofa| "two" { symbol: two; translate: 2.7 12.95; }
  |sofa| "one" { symbol: one; translate: 5.4 12.95; }
  |sofa| "corner" { symbol: corner; translate: 8.1 12.2; }
  |sofa| "stool" { symbol: stool; translate: 10.8 13.225; }

  // A `|dining|` is sized by its **tabletop**; the chairs stand a small
  // pull-back off its edge, and that pair of rows is what extends the bbox.
  |head| { translate: -3.7 15.4; } [ "DINING" ]
  |dining| "six" { translate: 0 15.445; }
  |dining| "four" { symbol: four; translate: 2.7 15.495; }
  |dining| "round" { symbol: round; translate: 5.4 15.395; }

  // The sanitaryware. A toilet is **one** silhouette — the cistern's shoulders
  // flowing into the rounded pan — and `double-sink` is one unit with two
  // basins, the kitchen run's, never two sinks side by side.
  |head| { translate: -3.7 17.45; } [ "BATH" ]
  |bath| "tub" { translate: 0 17.525; }
  |bath| "shower" { symbol: shower; translate: 2.7 17.45; }
  |bath| "toilet" { symbol: toilet; translate: 5.4 17.7; }
  |bath| "sink" { symbol: sink; translate: 8.1 17.7; }
  |bath| "double-sink" { symbol: double-sink; translate: 10.8 17.675; }

  // ── …and the kitchen boxes, whose label is the one that centres **in** the
  // body — the F / W / DW convention every real plan writes.
  |head| { translate: -3.7 18.9; } [ "APPLIANCES" ]
  |appliance| { symbol: stove; translate: 0 18.9; }
  |appliance| "F" { symbol: fridge; translate: 2.7 18.9; }
  |appliance| "W" { symbol: washer; translate: 5.4 18.9; }
  |appliance| "DW" { symbol: dishwasher; translate: 8.1 18.9; }
  |cap| { translate: 0 19.55; } [ "stove" ]
  |cap| { translate: 2.7 19.55; } [ "fridge" ]
  |cap| { translate: 5.4 19.55; } [ "washer" ]
  |cap| { translate: 8.1 19.55; } [ "dishwasher" ]
]

Every wall look, every door pose and symbol, the windows, the flights, and all six fixture families with every symbol: variant — drawn at 1:50, so the sizes here are the real ones.

A floorplan is a drawing scope, so there is no track grid to lay a catalogue on: every piece is datum-placed with translate: and every wall is drawn where it stands. The true-size law does the rest — a tub is 1700 × 750 mm and a door left to itself is 900 mm wide, whatever the file drafts in.

Printed as a blueprint

D1 D2 W1 D3 D4 F DW W/D LIVING / DINING 23.9 m² KITCHEN BEDROOM 12.8 m² HALL BATH 7.3 m² BALCONY N 9.4 4.45 4.85 3.85 2.65 6.6 UNIT 12A — ONE BEDROOM · 65 m²
{
  density: 3; clearance: 16; padding: 40;
  fill: #00509e; color: #edf5fb;
  --stroke-dark: white; --stroke-light: rgba(255, 255, 255, 0.72);
  --caption-color: rgba(255, 255, 255, 0.72);
  --footer-color: rgba(255, 255, 255, 0.72);
  // every body that masks the floor takes the paper too
  |slab::rect| { fill: #00509e; stroke: --stroke-dark; stroke-width: 1; }
  |bed| { fill: #00509e; }
  |sofa| { fill: #00509e; }
  |dining| { fill: #00509e; }
  |bath| { fill: #00509e; }
  |appliance| { fill: #00509e; }
  |room::block| {
    layout: flow; direction: column; align: center; gap: 2;
    font-size: 11; letter-spacing: 1;
  }
}

|floorplan#unit| "UNIT 12A — ONE BEDROOM · 65 m²" { unit: m; scale: 0.02; } [

  |wall#outer| {
    draw: move(0, 0) right(9.6):north down(6.8):east left(9.6):south
          close():west;
  } [
    |door#entry| "D1" { on: east; at: 1.2; width: 0.95; swing: right; }
    |door#slider| "D2" { on: west; at: 3.4; width: 2.4; symbol: sliding; }
    |window#w1| "W1" { on: west; at: 0.5; width: 2; }
  ]
  // north and south are party walls against the neighbouring units, so they
  // carry no openings at all — every one a condo unit gets is on the entry
  // side or the balcony side

  |partition#bedwall| { draw: move(0, 3.8) right(4.6):head down(3):side; } [
    |door#bed-door| "D3" { on: side; at: 0.4; width: 0.9; swing: right; }
  ]
  |partition#laundry| { draw: move(6, 6.8) up(0.8); }
  // a stub off the south wall: with the bathroom's own west wall it makes the
  // alcove the stacked washer/dryer is built into
  |partition#bathwall| { draw: move(9.6, 4) left(2.9):head down(2.8):side; } [
    |door#bath-door| "D4" { on: side; at: 1; width: 0.8; hinge: end; }
  ]

  // Casework is plain geometry: counters, the island, the wardrobe, the deck.
  |slab#deck| { width: 1.4; height: 6.6; translate: -0.8 3.4; }
  |slab| { width: 3.9; height: 0.6; translate: 7.55 0.4; }
  |slab| { width: 2; height: 0.9; translate: 7.2 2.05; radius: 4; }
  |slab| { width: 1.1; height: 0.55; translate: 2 1.75; }
  |slab| { width: 0.45; height: 0.45; translate: 0.6 4.075; }
  |slab| { width: 0.45; height: 0.45; translate: 3 4.075; }
  |slab| { width: 0.6; height: 1.4; translate: 4.25 6; }
  |slab| { width: 1.2; height: 0.55; translate: 7.35 4.325; }

  |appliance| { symbol: stove; translate: 6.4 0.4; }
  |appliance| "F" { symbol: fridge; translate: 8.1 0.4; }
  |appliance| "DW" { symbol: dishwasher; translate: 7.7 2.05; }
  |appliance| "W/D" { symbol: washer; translate: 6.35 6.4; }
  |bath| { symbol: double-sink; translate: 6.8 2.05; }
  |sofa| { symbol: stool; translate: 6.7 2.75; }
  |sofa| { symbol: stool; translate: 7.2 2.75; }
  |sofa| { symbol: stool; translate: 7.7 2.75; }
  |sofa| { translate: 2 0.55; }
  |dining| { symbol: four; translate: 4.3 2.2; }
  |bed| { translate: 1.8 4.85; }
  |bath| { symbol: sink; translate: 7.35 4.325; }
  |bath| { symbol: toilet; rotate: 180; translate: 9.15 5; }
  |bath| { symbol: tub; translate: 7.6 6.325; }
  |bath| { symbol: shower; translate: 9.05 6.25; }

  |room| { translate: 2.2 2.75; } [ "LIVING / DINING" "23.9 m²" ]
  |room| { translate: 7.4 3.2; } [ "KITCHEN" ]
  |room| { translate: 3.3 6.1; } [ "BEDROOM" "12.8 m²" ]
  |room| { translate: 5.65 4.6; } [ "HALL" ]
  |room| { translate: 8.35 5.55; } [ "BATH" "7.3 m²" ]
  |room| { translate: -0.8 3.4; } [ "BALCONY" ]

  // The north arrow a plan needs is a |sketch| define — the parts-library
  // escape, since the pen already draws it.
  |sketch#north| {
    draw: move(-0.8, 5.73) line(0.22, 0.72) line(-0.22, -0.2) line(-0.22, 0.2)
          close();
    fill: --stroke-dark; stroke: none;
  }
  |room| { translate: -0.8 5.46; } [ "N" ]

  outer:west-in (-) outer:east-in { side: top; }
  outer:west-in (-) bedwall:side-out { side: bottom; }
  bedwall:side-in (-) outer:east-in { side: bottom; }
  outer:north-in (-) bathwall:head-out { side: right; }
  bathwall:head-in (-) outer:south-in { side: right; }
  outer:north-in (-) outer:south-in { side: right; }
]

The plan is the one the reference implementation ships as samples/floorplan.lini, untouched below the stylesheet — what makes it a print is the handful of visual variables above it. --stroke-dark carries the poché and every dimension line, --stroke-light the thin support lines, and the two caption tones follow. The paper is stated as a literal rather than through --bg, because --bg names the host page’s backdrop and a page that owns it would hand this figure white; the fixtures take the same literal, since each one masks the floor with the paper it sits on. The whole recipe is a built-in — lini --theme blueprint prints any figure this way.

Go deeper

Floorplan — the architectural dialect covers wall junctions, hinge control, stairs and the full fixture table; the rest of section 15 is the engine underneath it.