The Wadi DSL (.wdl) — authoring reference
You author houses in the Wadi DSL — a small, formal language (.wdl) that
compiles to a resolved .wadi (house_config.json). The DSL is complete:
every object type in the model has first-class syntax, so you rarely need the
raw escape. Authoring the DSL is more direct and less error-prone than writing
JSON — the grammar enforces structure, and check.sh reports parse errors with
line:col.
This file is the SYNTAX reference. The semantics live in the other references and apply unchanged — read them:
coordinate-system.md— X→right, Y→DOWN, Z→up; 10 units = 1 ft; the centreline convention. The #1 source of mistakes.conventions.md— the structural coding conventions (check.shenforces them): plinth-floor height must match the plinth block, rooms must wall every exterior side, a no-slab floor must setslab_thickness 0.parametric-conventions.md— the grid-first recipe for reusable templates.roof-v2-guide.md— roof segments, hip vs gable, trusses, joints.data-model.md— the underlying.wadischema (what the DSL compiles to; also the field reference for therawescape).
The loop
- Write / edit
house.wdl— the single shared source (you and the human co-edit it; the app's DSL previewer renders it live). You never produce a.wadi. wadi-skill/architect/scripts/check.sh house.wdl— runs the DSL compiler + validator (schema + wall/roof geometry) against a throwaway temp just for feedback; fix any reported error and re-run.preview.sh house.wdl→ read the PNGs (plans / elevations / roof) to check your work. (It also compiles to a throwaway temp — no persistent.wadi.)
Skeleton
house MyHouse {
convention center // ALWAYS use center (wall-centreline coords)
units feet_inches per_unit 10 // 10 project units = 1 ft
site { plot (WIDTH, LENGTH) ref (0, 0) }
defaults { floor_height 120 wall_height 108 slab_thickness 8 wall_thickness 8 }
// parametric core (optional): var, point, grid, configurator
// component / layer declarations (optional)
floor 0 "Plinth" { … } // floors stack in source order (0 = plinth)
floor 1 "Ground Floor" { … }
floor 2 "Loft" { roof … } // roof lives ALONE on its own top floor
}
Numbers are project units (feet × 10 by default). Names after house,
room, pillar, var, point, grid, and use/component are bare
identifiers (no spaces); names introduced with the name keyword are quoted
strings.
Parametric core (domain-neutral)
var wallT = 8 // a knob; may reference other vars
var pilInset = (pillarW - wallT) / 2
point House { x = 420, y = 470 } // reference as House.x / House.W / House.L
// (.W = x, .L = y — a point doubles as a size)
grid main { // named wall centrelines; publishes main.x1 / main.yA
x: 1 @ wallT / 2, 2 @ House.W / 2, 3 @ House.W - wallT / 2
y: A @ wallT / 2, B @ House.L / 2, C @ House.L - wallT / 2
}
// each line may add: … @ <expr> thick <expr> role structural|planning
configurator { // the owner-facing template you author
title "Configure your home" // panel heading (optional)
note "Everything re-flows to fit." // panel subtitle (optional)
slider pillarW "Column size" ft [8 .. 14 step 1] note "help text" // trailing note optional
number ceiling "Ceiling height" ft
toggle has_loft "Add a loft"
select roof_style "Roof style" { Flat = 0, Shed = 1, Gable = 2, Hip = 3 }
select floorH "Ceiling height" { "9 ft" = 90, "10 ft (std)" = 100 } // labels can be quoted strings
group "Plot" note "about this section" { // sections the panel; label only
slider W "Plot width" ft [340 .. 520 step 10]
}
}
Every knob binds to a var by name (target). To expose a plot dimension, model
it as a var and reference it from the point (point House { x = W }), then bind
the knob to W — knobs target vars, never a point field like House.W.
Formulas are automatic. Any geometry number can be a formula — just write the
expression instead of a literal (at (main.x1, main.yA), size (House.W/2, 200)).
Operators: + - * /, unary -, parentheses, and the functions
min max clamp round floor ceil abs. References: a var, a point
(House.W), or a grid line (main.x3 - main.x1). No comparison operators — gate
things with the min/abs idiom (see enabled below).
Common attribute tail (every object)
After an object's geometry, in THIS order, any of:
… z_offset <expr> enabled <expr> layer "id" [material "id"]
enabled <expr>— the on/off switch. A0/falsevalue hides the object. To gate on a configurator variable, use a 0/1 formula:enabled 1 - min(1, abs(roof_style - 3))renders the object only whenroof_style == 3. (This is how one template carries several roofs and shows only the chosen one.)z_offset <expr>— lift above the floor base (split levels).material "id"— only on plinth / ground / room / wall / staircase / kitchen / roof.
Objects — structure & envelope
slab [name "N"] at (x,y) size (w,l) [thickness <t>] // floor_slab
beam [name "N"] at (x,y) size (w,l) [height <h>]
plinth [name "N"] at (x,y) size (w,l) height <h> // raised base (Plinth floor)
ground [name "N"] at (x,y) size (w,l) [height <h>] // terrain plane
pillar Name at (x,y) size (w,l) [height <h>] // (x,y) = TOP-LEFT corner
at (x,y) is the TOP-LEFT CORNER — not the centre — for every one of these
(same as rooms/slabs/beams); size (w,l) is width × length. All accept the common tail.
Pillars catch people out here. A column reads as "placed at a point," but at is
still its corner. To centre a column on a point (cx, cy) — a grid node, a room
corner — place it at at (cx - w/2, cy - l/2), never at (cx, cy). On a grid, the
pilInset idiom (see parametric-conventions.md) does exactly this so columns sit flush.
Objects — rooms, walls & openings
A room shows exactly the walls you declare. A bare room (no wall lines) is
enclosed on all four sides. List plain walls compactly; give a wall its own line
only when it carries a door/window; omit a side to leave it open (verandah).
room Name at (x,y) size (w,l) [height <h>] [material "…"] {
wall east west north // plain walls — several in one statement
wall south { door Main at <offset> [from start|center|end] size (w,h) [open] } // wall WITH openings
wall west { window W at <offset> [from start|center|end] size (w,h) [sill <s>] [open] }
item asset { … } anchor center [gap (gx,gy)] // furniture anchored inside the room
}
wall <side>…sides arenorth|south|east|west. Awall <side>line may also addheight <h>/height_end <h>(sloped).door/windowat <offset>is measured along the wall;size (width, height);window … sill <s>sets the sill height;open= a bare hole (no leaf/glazing).from start|center|end(defaultstart) picks which end<offset>is measured from, so the opening keeps its place when the wall or room scales — no formula needed.start: offset from the wall start to the near edge (the legacy default).end: offset from the wall end to the far edge (at 0 from end= flush to the end).center: signed shift of the opening centre from the wall midpoint (at 0 from center= centred; the offset may be negative).
A free-standing wall (not a room side):
wall Name from (x1,y1) to (x2,y2) [height <h>] [height_end <h>] [facing north|…] {
… door/window openings …
}
from/toare the wall's centreline endpoints; the wall is drawn as a rectanglewall_thicknesswide, centred on that line.Overlap walls at corners — they do NOT auto-mitre. Two free-standing walls that merely touch at a shared endpoint leave an unfilled square notch (½·
wall_thickness) at the corner, because each is just a rectangle capped at its endpoint. To fill the corner, extend the endpoints so the wall bodies OVERLAP — run at least one wall's end half the wall thickness past the shared point (overlapping by the full thickness is fine and simplest). For an L of thickness 8 meeting at(160,40):wall H from (40, 40) to (164, 40) height 108 // ends 4 (½·8) PAST the corner wall V from (160, 40) to (160, 160) height 108 // butts into H's overlapped body(Room walls handle their own corners; this only applies to
wall … from … to ….)
Objects — circulation & fittings
staircase [name "N"] at (start_x, start_y) step (rise, tread, width)
direction north|south|east|west [climb up|down]
[total_height <h>] [max_run <r>] [landing_depth <d>]
[landing_thickness <t>] [turn clockwise|anticlockwise] [flight_gap <g>]
kitchen [name "N"] path ((x,y), (x,y), …) side left|right
depth <d> height <h> [base_z <z>] // path points are literal numbers
climb picks the anchor + z direction. Prefer climb up — the intuitive way:
- Put the stair on the LOWER floor it rises FROM.
at (x,y)is the bottom step's near corner on that floor;directionis the ascent direction; the flight climbs UP. total_heightis the rise to the next level (omit → this floor's own height).max_runcaps a flight's run; exceed it and the stair auto-splits into switchback flights with turn landings (landing_depth/turn/flight_gaptune the switchback).
floor 1 "Ground Floor" height 116 {
slab at (…) size (…)
staircase name "Stair" at (212, 64) step (7, 11, 44) // bottom = this floor
direction south climb up // ascends south to the floor above
}
climb down is the legacy mode (DEFAULT for older configs): put the stair on the
UPPER destination floor; at (x,y) is the top connection, direction is the
descent, and total_height defaults to the floor-below's height. New designs should use
climb up.
// three ways to name the GLB, in order of preference:
item [name "N"] f."sofa" // 1. from an imported module (see Imports)
item [name "N"] "sofa" // 2. a same-file / bare-imported asset id
item [name "N"] asset { id "sofa" src "…/sofa.glb" dims (w,h,d) [category "…"] } // 3. inline one-off
at (x,y) [rotation ]
[anchor_to "RoomName" anchor center gap (gx,gy)]
Prefer the module form (`item f."bed_double"`) — `import "std-furniture" as f`
once and every piece is a short id, no URLs. The bare form (`item "sofa"`) needs
a matching top-level `asset "sofa" …` in the file (or a bare `import`). The inline
`asset { … }` block is only for a one-off GLB not in any pack. All three produce
the identical `{id,src,dims}` downstream. Furniture `dims` are the real-world size
in **metres** `(width, height, depth)`; `src` is a GLB URL (an unreachable GLB
shows a placeholder box, never a blank). `anchor` is one of `top-left top-center
top-right center-left center center-right bottom-left bottom-center bottom-right`.
**Orientation — this is how you point furniture the right way.** A piece's FRONT
(the side you sit at / the doors / the open side) faces a known compass direction
per its `rotation` (degrees):
| `rotation` | front faces |
|---|---|
| `0` | **South** (the plot front / entrance side, +Y) |
| `90` | East |
| `180` | North |
| `270` | West |
So a sofa against the NORTH wall (facing into the room, i.e. south) is `rotation
0`; against the SOUTH wall (facing north) it's `rotation 180`; against the WEST
wall (facing east) `rotation 90`. The floor plan (`wadi_preview plans`) draws a
small triangle on each piece's front edge so you can verify the way it points;
for a definitive 3D check use `wadi_capture_3d({ room: "…" })` (first-person from
inside the room).
**Anchoring auto-orients.** When you `anchor` a piece to a wall and DON'T give a
`rotation`, it automatically faces away from that wall, into the room — `anchor
top-center` → faces south, `bottom-center` → north, `center-left` → east,
`center-right` → west (a corner uses its north/south edge). So `item f."bed_double"
anchor top-center` needs no rotation. An explicit `rotation` always overrides,
and the derived value is written into the resolved model, so the plan notch and
the 3D view show it — anchoring never changes facing silently.
## Objects — GLB models (`model`)
A `model` places a **GLB at real metre scale** and manipulates it through a **rig**
of named-node operations. It is distinct from `item` (catalog furniture): `model`
targets the GLB's internal node graph, so you can hide, move, recolour, or array
sub-parts of one asset.
```wdl
model [name "N"] <asset> at (x,y) [rotation <deg>] [scale <s>] {
translate "nodeName" (x,y,z) // move a named node
rotate "nodeName" (x,y,z) // rotate it (degrees)
scale "nodeName" (x,y,z) // scale it
visible "nodeName" false // hide a node
material "nodeName" color "#rrggbb" // recolour a node
array "nodeName" count N step (dx,dy,dz) { … nested rig ops … }
}
<asset> names the GLB the same three ways as item (module ns."id", bare
"id", or an inline asset { … } block). The rig ops run against the GLB's named
nodes; array replicates a node count times along step, and its nested block
applies further ops per copy. Use model for a rigged mechanism (a fan, a louvre
bank, a spiral of balusters); use item for a plain piece of furniture.
Imports & modules (reusable .wdl libraries)
A .wdl file can be a module — top-level declarations (no house needed) —
that another file imports. Two bundled ones: std-furniture (asset pack →
item ns."id") and konkan/base (goal-tagged component pack → use ns.Comp;
Stairwell, Verandah, Otla, Bathroom, Kitchen, TulsiVrindavan, Parapet). The
konkan_cottage example (wadi_examples) assembles a whole house from both.
house Home {
import "std-furniture" as f // aliased: refer to its assets as f."<id>"
// import "std-furniture" // bare: its ids drop into scope for item "<id>"
floor 1 "G" slab_thickness 0 {
room Bed at (20,20) size (160,200) { wall north east south west
item f."bed_double" anchor center }
}
}
A module file itself is just top-level asset and component decls:
// my-furniture.wdl — a house-less module (a reusable library)
asset "daybed" src "https://…/daybed.glb" dims (1.8, 0.4, 0.9) name "Daybed" category "Living"
Over MCP, wadi_modules lists importable modules and wadi_module "<name>"
shows a module's asset ids + dimensions (filter with a query). Import refs
resolve by name against the bundled std-* packs (a local modules/ search
path and git refs come later).
Objects — roof (one object; flat / shed / gable / hip)
The roof lives ALONE on its own top floor and you never set its Z (see
roof-v2-guide.md). endpoint: closed = hip triangle, open = gable end-wall.
roof [name "N"] pitched|shed|flat
[endpoint open|closed]
[slope angle <deg> | slope height <ridge_h>] // symmetric pitch (one value)
[slope angle (<left>, <right>)] // asymmetric (saltbox) gable — angle pair
[overhang <o>] [slab_thickness <t>] [parapet <h> x <t>] [gable_wall_thickness <t>] {
segment "id" from (x,y) to (x,y) width <w>
[high_side left|right] // shed only
[start_endpoint open|closed] [end_endpoint open|closed]
[hip_setback (a,b)] [gable_overhang (a,b)] [hip_ridge_extension (a,b)]
[overhang <o>] // uniform eave, all four sides
[overhang_start <o>] [overhang_end <o>] // per-side along the axis (shed;
// on a gable end = gable_overhang)
[overhang_low <o>] [overhang_high <o>] // SHED eaves (down-slope / up-slope)
[overhang_left <o>] [overhang_right <o>] // PITCHED eaves (left / right of ridge)
[tie_beams N]
truss "segId" fink|mono_pitch at (pos, pos, …)
}
Segment from/to/width and the hip_setback/… values accept formulas, so a
roof scales with the plot (e.g. width House.W, hip_setback (Verandah.L, Padvi.L)).
Per-side overhang (cantilever one edge). overhang <o> sets a uniform eave on
all four sides. Any sloping roof can override a side independently — each defaults to
overhang. Along the axis: overhang_start / overhang_end (on a shed, or a
gable open end — there they're the same as gable_overhang; a hip end is geometric,
tuned via hip_setback). Eaves: overhang_low / overhang_high on a shed
(down-slope / up-slope); overhang_left / overhang_right on a pitched roof
(the two eaves either side of the ridge). A bigger eave overhang also drops that
eave's edge along the same pitch, so the slope stays planar. (Per-eave on a pitched
roof is single-segment only — on a multi-segment roof the eaves share one height so
joints line up.)
Asymmetric gable (saltbox) — an angle pair. A pitched roof's slope gives both
faces the same pitch (a symmetric gable). To make the two sides different, give
slope angle a pair instead of one value — slope angle (45, 25). A single value
is symmetric; a pair is asymmetric, and that's the whole distinction (no separate
keywords, nothing half-settable). The two eaves stay put (footprint unchanged) and the
ridge shifts across the width so each face takes its angle; the gable-end triangle and
trusses follow. The pair is (left, right) by the segment's left normal — the same
sides as overhang_left/overhang_right. Angles are measured to the wall-top eave
line. height is single-value only (both faces share one ridge line). Intended for a
single-segment gable (endpoint open); on a hip end the ridge shift skews the hip.
roof pitched endpoint open slope angle (45, 25) {
segment "s0" from (House.W/2, 0) to (House.W/2, House.L) width House.W
}
Idiom: keep the roof FOOTPRINT (its supported edges) on the main room, then cantilever
one eave to cover an entry landing / stair — end the axis on the room wall and set a big
overhang_end:
// footprint ends on the main room's east wall (x204 centreline → x208 outer);
// the east eave reaches 258, covering a landing that sticks out to x256.
segment "seg0" from (4,124) to (204,124) width 240 high_side right overhang 25 overhang_end 50
Roof coordinates are wall centrelines (under convention center), same as
rooms. from/to is the segment's ridge/axis and width its span centred on
that axis. Author them on the same centreline grid as the walls — a segment
whose axis + width match the rooms' centrelines auto-grows to the outer wall
face on every side (the compiler extends the axis by ½·wall_thickness at each end
and widens by wall_thickness, exactly the grow a room gets). overhang then
extends beyond the outer face. So to cover a footprint spanning wall centrelines
x1..x2 (E–W) and yA..yB (N–S), write from (x1, (yA+yB)/2) to (x2, (yA+yB)/2) width (yB - yA) — do not add ½-wall fudge factors; the convention handles it.
(Before this, a roof drawn on the grid sat half a wall-thickness inside the walls.)
Components & layers
component Bench { // a reusable mini-house in LOCAL coords (origin 0,0)
param blen = 60 label "Bench length"
beam name "Top" at (0,0) size (blen, 18) height 6
}
use Bench as "B1" at (x,y) [rotation <deg>] with { blen = 80 } // stamp onto a floor
layer "structure" "Structure" [color "#rrggbb"] [group "Frame"] // per-house layer registry
A component may carry a goal — a short description of what it accomplishes,
the discovery key for module lookup (wadi_module / a wadi_modules query):
component Stairwell goal "climb to the next floor" {
param rise = 116
staircase name "Stair" at (0,0) step (7,11,44) direction south total_height rise
}
Components can also come from an imported module (see Imports & modules),
stamped with a namespaced use ns.Comp:
house Home {
import "konkan/base" as kb // Stairwell, Verandah, Otla (goal-tagged)
floor 1 "G" slab_thickness 0 {
room Hall at (20,20) size (200,200) { wall north east south west }
use kb.Stairwell at (60,60) with { rise = 116 } // param args use `=`, not `:`
}
}
use ns.Comp expands byte-identical to an inline component. Components nest
freely: a library component may use a sibling, use a component from a
library it itself imports, and place item ns."id" furniture from its own
imports — imports resolve transitively (cycles are a compile error).
Un-overridden params fall back to their declared defaults.
rotation <deg> (optional, yaw°: 0=south, 90=east) turns the whole stamped
assembly about its origin. Right angles (0/90/180/270) are exact for any
component (rooms/pillars/beams/slabs swap dims + remap wall sides; furniture
turns with them). A non-right angle is allowed only for a furniture-only
component (items/free walls rotate to any angle); a free angle on a component
that contains a room/pillar/beam/slab/staircase is a compile error (arbitrary
structural rotation is a future feature).
Promote a component to a primitive (expose as)
A component can be promoted to a runtime typed primitive with expose as, so it
reads and behaves like a built-in object type (pack.type) rather than a use
instance:
component Bench goal "a place to sit" expose as garden.bench [layer "id"] [label "…"] {
param length = 60
beam name "Seat" at (0,0) size (length, 18) height 6
}
expose as <pack>.<type> names the promoted primitive (a dotted pack.type id);
optional layer "id" and label "…" set its default layer and menu label. Once
exposed, the component is available as a first-class object type named pack.type
throughout the model, its params becoming that type's fields.
Template metadata (template)
A template { … } block self-describes a template on its .wdl / .wadi, so a
gallery can index the file without a separate catalog. It carries display metadata
only (no geometry):
template {
title "Coastal Cottage"
description "A compact single-storey Konkan home."
style "konkan"
roof "gable"
tags ("coastal", "1BHK", "compact")
thumbnails ("thumb-iso.png", "thumb-plan.png")
min_plot (340, 400)
}
Fields: title, description, style, roof, tags (a list), thumbnails (a
list of image paths), min_plot (the minimum plot the template needs). The folder
of self-describing files IS the catalog.
The raw escape (rarely needed)
Anything the first-class syntax doesn't cover can be written as literal JSON per
the .wadi schema (data-model.md):
raw "type" { "field": 1, "formulas": { "field": "= expr" } }
DSL-specific pitfalls
convention centerandunits … per_unit 10belong at the top of everyhouse— same as the JSON path. All thecoordinate-system.mdrules (Y-down, units, centreline abutment) apply identically; the DSL just writes them shorter.- Formulas are bare expressions, not
"= …"strings — the compiler emits the= …form for you. Writeat (main.x1, main.yA), notat ("= main.x1", …). name "…"is quoted;room/pillar/var/grid-line names are bare ids (no spaces, and not a reserved word likewidth,height,size,at).- Roof alone on the top floor; segment widths/positions come from the walls
they sit on. See
roof-v2-guide.md. - A pillar's
atis its TOP-LEFT corner, not its centre. To centre a column on(cx,cy), authorat (cx - w/2, cy - l/2). - Free-standing walls don't auto-mitre at corners — extend endpoints so the wall
bodies overlap (≥ ½·
wall_thicknesspast the shared point), or the corner is left as a gap. - Prefer
climb up— it's bottom-anchored. Put the stair on the LOWER floor it rises FROM;atis the bottom step and the flight ascends intodirection. Top-anchored is the legacyclimb down(default only for older configs): the stair sits on the UPPER floor and descends.check.shC5 flags a stair that lands below ground. See the staircase note. - Structural conventions are enforced —
check.shfails on floating floors (plinth-floorheight≠ plinth block height; a no-slab floor with nonzeroslab_thickness) and warns on exterior room sides left open. Seeconventions.md; the DSL editor shows the same findings in its status pill. - Compile after every edit; a parse error means the
.wadiwasn't updated, so the live model just won't change — never silently wrong.