Building a Parametric Domain-Specific Language
A reference method, derived from Wadi (parametric house design), for reproducing the same system in any domain
Part of the Wadi documentation, the advanced tier. This is the most abstract chapter: the general method behind Wadi, for anyone building a similar system in another domain.
Companion: extending the DSL explains the componentization framework: how a concept is declared once (as
fields) and projected onto the schema, forms, docs, and DSL, and how the domain-neutral engine is extracted into a reusable kernel. This document is the parametric layer underneath it (variables, formulas, the resolver, the configurator).
0. What this document is
This is a methodology reference. It describes how Wadi turns a single declarative document into a live parametric 3-D house, then abstracts that into a domain-agnostic recipe you can follow to build an equivalent system for a different domain (a solar farm, a data-centre floor, a garden, a PCB, a factory line, a slide layout).
A parametric design tool is not one program but a stack of separated layers, each with a single responsibility. If you reproduce the layers and the contracts between them, you get the same behaviour (choose a design and vary it, author once and reuse, one model to many outputs) regardless of what the primitives are.
The document has three parts:
- The layered architecture (§1 to §10). Each layer explained with the Wadi mechanics.
- The distilled principles (§11). The transferable rules, without the houses.
- The recipe and a worked second domain (§12 to §13). How to apply it to a new domain, shown on a solar-PV farm, with a mapping table for several more.
Throughout, "Wadi" is the concrete case and "the model" is the generic idea.
1. The core idea
A parametric design system is built from three tiers that must not be conflated:
┌─────────────────────────────────────────────┐
TIER 3│ CONTROL knobs a user turns for a │ "make THIS house
│ (configurator) specific instance │ 30ft wide, hip roof"
├─────────────────────────────────────────────┤
TIER 2│ ASSEMBLY a parametric MODEL: primitives│ "a 3-bay Konkan house
│ (the document) composed + related by formulas│ on a grid"
├─────────────────────────────────────────────┤
TIER 1│ VOCABULARY PRIMITIVES with typed │ "room, wall, pillar,
│ (the schema) parameters │ roof, staircase…"
└─────────────────────────────────────────────┘
- Tier 1, Vocabulary. A small fixed set of primitives. Each is a typed record: a
typediscriminator plus a flat set of scalar parameters, and sometimes a little nested structure. Primitives are the words. - Tier 2, Assembly. A model is a document that composes primitives and wires relationships between them with named variables and formulas. This is a sentence: a parametric design, not one frozen instance.
- Tier 3, Control. A curated set of the model's variables are surfaced as control parameters (sliders, selects, toggles). Turning a knob re-runs the relationships and the model re-flows. This is speaking the sentence for a particular occasion.
Everything else in this document is the machinery that makes those three tiers work together: a formula engine, a resolver, reusable components, an expansion step, multiple renderers, validation, and an extensibility socket.
The full data and control flow:
flowchart TD
A["Author writes a MODEL document<br/>(primitives + variables + grids + formulas)"] --> V{"validate<br/>(strict schema)"}
K["User turns CONTROL knobs<br/>(configurator)"] -->|writes a variable| V
V --> R["RESOLVE<br/>formulas → numeric fields<br/>(pure, topological, cycle-safe)"]
R --> X["EXPAND / DERIVE<br/>components, grids→walls, disabled dropped<br/>→ one concrete low-level model"]
X --> O1["3-D view"]
X --> O2["2-D plans / elevations"]
X --> O3["quantities / BOM"]
X --> O4["export / share link"]
Each box is a pure function of the box before it. That purity is what makes the system live, testable, and shareable.
2. The seven layers (map of the rest of the document)
| # | Layer | Responsibility | Wadi file(s) |
|---|---|---|---|
| 1 | Primitives | the typed vocabulary | schema/houseConfig.ts |
| 2 | Assembly model | composition, coordinate frame, conventions | the .wadi document |
| 3 | Parametric layer | variables, points, grids, per-field formulas | schema + author data |
| 4 | Resolver | formulas → numbers, one directional pass | param/resolve.ts, param/formula.ts |
| 5 | Control | curated knobs + presence switches | configurator, enabled |
| 6 | Components | reusable parametric sub-assemblies | components, component |
| 7 | Expansion + renderers | derive a concrete model; many outputs | svg2d/expand.ts + renderers |
Plus three cross-cutting concerns (§10): validation and forward-compat, units and display, and the extensibility registry.
3. Layer 1: Primitives (the vocabulary)
A primitive is a self-contained typed record. In Wadi every object is a member of a
discriminated union keyed on type:
object = discriminatedUnion("type", [
plinth, ground, floor_slab, beam, pillar, wall, room, staircase,
door, window, kitchen_platform, roof, item, component
])
Each carries:
- a
typediscriminator (the "part of speech"), - a flat set of scalar parameters, its intrinsic geometry and behaviour,
- optional nested sub-structures (e.g. a
wallhasopenings[]; aroofhassegments[],slope,trusses[]; aroomhaswalls{}anditems[]), - a few universal fields every primitive shares (see below).
3.1 The Wadi primitive catalogue
| Primitive | What it is | Key intrinsic parameters |
|---|---|---|
room |
a rectangular space that grows its own walls | x, y, width, length, walls{north,south,east,west}, wall_heights, items[] |
wall |
a free-standing wall segment | start_x, start_y, end_x, end_y, height, height_end, openings[] |
pillar |
a structural column | x, y, width, length, height (x,y = top-left corner) |
beam |
a horizontal member | x, y, …, height, height_end |
floor_slab |
an RCC deck | x, y, width, length, thickness |
staircase |
a flight (auto-splits into switchbacks) | start_x, start_y, step_rise, step_tread, step_width, direction, climb{up,down}, max_run, rise_height, turn |
spiral_staircase |
a helical stair around a central pole | x, y, radius, total_height, turns, steps, pole_radius, tread_thickness |
roof |
unified segment roof (hip/gable/shed/flat) | segments[], slope, trusses[], default_endpoint |
door / window (nested in openings[]) |
a hole cut in a wall | offset, anchor{start,center,end}, width, height, sill_height, direction, open |
kitchen_platform |
a polyline countertop | path[], side, depth, height |
plinth / ground |
the base / terrain of a floor | x, y, width, length, height |
item |
a GLB furniture / décor instance | asset{src, dimensions[w,h,d]}, x, y, rotation, scale |
model |
a GLB at real scale, posed by a named-node rig |
asset, x, y, rotation, scale, rig[translate/rotate/scale/visible/material/array] |
component |
an instance of a reusable sub-assembly | ref, params{}, x, y, z_offset |
3.2 Universal fields (the primitive envelope)
Every primitive shares a small envelope that the higher layers rely on:
type: the discriminator (how dispatchers route it).name: a human handle (also a reference target, e.g. an opening'sroom+direction).formulas: the parametric hook (§5):{ fieldName: "= expression" }.enabled: a presence switch (bool | number).false/0removes the object from every view. It can be driven by a formula, which is the basis of optional rooms and mutually-exclusive variants.layer: a visibility tag (display-only, never geometry).z_offset: placement in the stacking axis relative to the container base.
Design rule. Keep intrinsic geometry as plain numbers. Do not store a formula in a geometry field. Store the number there and the formula beside it in
formulas. The number is always a valid, renderable fallback; the formula is an overlay the resolver applies. This keeps every consumer simple (they only ever read numbers) and makes the model degrade gracefully.
4. Layer 2: The assembly model
4.1 It is a single declarative document
The entire design is one JSON document (.wadi / house_config.json), not code and
not a binary. This has several consequences:
- it is inspectable, diffable, and version-controllable;
- it can be shared in a URL (Wadi encodes a whole house into a share link);
- it can be authored by a human, a form UI, or an AI agent interchangeably;
- it is serialisable across runtimes (the same document drove a Python/Blender pipeline and now a TypeScript/Three.js one).
4.2 The composition container
Primitives do not float free. They live in containers that impose stacking and scoping. In Wadi the container hierarchy is:
HouseConfig
├─ site (plot dimensions, reference origin)
├─ defaults (house-wide fallbacks: wall_thickness, floor_height…)
├─ variables / points / grids / components (the parametric layer, §5)
├─ configurator (control metadata, §5.4)
├─ layers (visibility groups)
└─ floors[] (ordered; array order == vertical stack)
└─ objects[] (the primitives on that floor)
The floor is the assembly unit. It owns an ordered objects[] list and its own
overridable heights (height, wall_height, slab_thickness). Array order is the
physical stack: reordering floors restacks the building. Your domain's container might
be a page, a layer, a stage, or a rack row. The principle is the same: a scoping and
stacking bucket that primitives belong to.
4.3 The coordinate frame and conventions (critical, easy to get wrong)
A parametric model needs an unambiguous placement frame, and conventions that eliminate arithmetic for the author.
Wadi's frame is Inkscape-style: origin top-left, X right, Y down, and a separate
stacking axis for height. The coord_convention knob controls how positions are read:
"outer"(legacy): a room'sx,y,width,lengthdescribe the outer wall face. Adjacent rooms must overlap by a wall thickness, so the author does wall math constantly."center"(canonical): the same numbers are wall centrelines. Adjacent rooms abut on a shared line, and the expansion step grows each footprint bywall_thickness/2to the outer face. The author does no wall math.
Design rule. Push structure into the model so the author writes less. The centreline convention and the grid (§5.3) exist for one reason: to let an author say "this room spans grid line 1 to line 3" and get correct geometry without computing a single wall offset. Find your domain's equivalent arithmetic tax and design it away.
4.4 Units are a first-class, explicit concern
Geometry lives in abstract project units. A separate units block controls only how
numbers are labelled on drawings (feet_inches, per_unit: 10, and so on). Geometry
never changes with the display unit. Keeping storage units and display units separate
avoids a class of "someone changed the unit and the model resized" bugs.
5. Layer 3: The parametric layer
This is what turns a static document (Tier 1 and 2) into a parametric model. Four constructs, all optional (if absent, you get a plain, non-parametric design):
5.1 Variables (named scalars)
"variables": { "bay": 120, "depth": 300, "wallT": 8, "roof_style": 3 }
A variable is name → number | "= formula". Variables may reference each other. They
are the model's degrees of freedom, the things a knob will eventually move.
5.2 Points (named 2-D anchors that double as sizes)
"points": { "House": { "x": "= 3 * bay", "y": "= depth" } }
A point is a named coordinate pair. Each coordinate is referenceable under
case-insensitive synonyms: House.x / .X / .w / .W for the first,
House.y / .Y / .l / .L for the second. A single point therefore doubles as both a
position and a rectangle size (House.W, House.L) without a second structure.
5.3 Grids (first-class structural scaffolds)
A grid is a set of named, ordered centrelines per axis (X numbered 1,2,3…, Y
lettered A,B,C… by convention), each positioned by a formula:
"grids": {
"main": {
"x": [ {"name":"1","at":0}, {"name":"2","at":"= bay"}, {"name":"3","at":"= 2*bay"} ],
"y": [ {"name":"A","at":0}, {"name":"B","at":"= depth"} ]
}
}
The resolver publishes each grid line as a formula symbol: main.x1, main.x2,
main.yA, and so on. A room then places itself with ordinary formulas and no wall
math:
{ "type": "room", "name": "Living", "x": 0, "y": 0, "width": 1, "length": 1,
"formulas": { "x": "= main.x1", "y": "= main.yA",
"width": "= main.x3 - main.x1", "length": "= main.yB - main.yA" } }
Grids carry per-line thickness (a "tartan" grid) and a role tag
(structural | planning). Because a grid is defined in centrelines, it is
thickness-independent and reusable across templates: change the bay spacing and every
room, slab, and column bound to the grid re-flows together.
5.4 The per-field formulas map (the universal hook)
Every container (object, floor, site, defaults, and even nested openings / roof
segments / truss positions) may carry:
"formulas": { "width": "= main.x3 - main.x1", "enabled": "= 1 - min(1, abs(roof_style - 3))" }
The resolver evaluates each entry and writes the number into the named field. The authored number in the field is a fallback; the formula overlays it.
5.5 The formula language
A deliberately small, safe, side-effect-free arithmetic language. No eval, no
branching, no loops, no I/O:
expr := term (('+' | '-') term)*
term := factor (('*' | '/') factor)*
factor := '-' factor | '(' expr ')' | number | call | identifier
call := name '(' args? ')' // min, max, clamp, round, floor, ceil, abs
identifier := name ('.' name)* // dotted: point/grid symbols, e.g. main.x1, House.W
- A leading
=marks a string as a formula (the storage convention). It is stripped before parsing. - Evaluation never throws. Parse errors, unknown symbols, and divide-by-zero produce
{ value: null, error }, surfaced as a warning, never a crash. - The engine also extracts a formula's dependencies (the symbols it reads), which the resolver needs for ordering.
Why no
ifor comparisons? Branching is emulated with arithmetic.enabled = 1 - min(1, abs(roof_style - 3))is1exactly whenroof_style == 3, else0. Keeping the language total (every expression has a value) and branch-free makes the dependency graph static and the resolver simple to reason about. Add domain functions (e.g.min/max/clamp) rather than control flow.
6. Layer 4: The resolver
The resolver is a pure function config → config that evaluates every formula and
writes the results into fields. Its contract:
- Never throws. Any unexpected error returns the original config plus one warning.
- Fast path. A model with no variables, points, or formulas is returned by reference: zero cost, no spurious re-render.
- Idempotent. Resolving an already-resolved model yields identical numbers.
- Immutable and minimal. The same reference is preserved wherever nothing changed, so only the parts that moved get a new identity (cheaper re-rendering).
- Source-of-truth preservation. Only object numeric fields are written.
variablesandpointskeep their authored value (which may be a formula string).
6.1 The pipeline
flowchart TD
A["collect symbols<br/>variables + point coords (with synonyms)"] --> B["topological order<br/>(DFS) + cycle detection"]
B --> C["evaluate in order → flat numeric Scope"]
C --> D["publish grid symbols<br/>main.x1, main.yA … into Scope"]
D --> E["apply formulas into fields:<br/>objects, floors, site, defaults"]
E --> F["nested: openings, roof segments/trusses/slope"]
F --> G["config' (numbers written) + warnings[]"]
- Collect symbols. Every variable and every point coordinate (under all its synonyms) becomes a symbol with a value-or-formula and a dependency list.
- Topologically order the symbols by dependency, using a DFS that marks on-stack nodes to detect cycles (a circular reference becomes a warning, not a hang).
- Evaluate each symbol in order into a flat
Scope: { symbol → number }. - Publish scaffold symbols. Resolve each grid's line positions (and per-line
thickness) and inject
<gridId>.x<name>/.y<name>into the sameScope. - Apply formulas into fields. For every object, floor,
site, anddefaults, evaluate itsformulasmap againstScopeand write the numbers into the named fields. Recurse one level into nested structures that carry their own formulas (wall and room openings; roof segments whosestart_x/start_y/end_x/end_ymap into coordinate arrays; trusspos<i>positions; the roof slope). - Certain fields are semantically integers (
steps,tie_beam_count), and the resolver rounds them at the single write point rather than in each consumer. (A staircase's step count is itself derived:round(rise_height / step_rise).)
The dataflow is one-directional: knobs → variables → points → grids → object fields.
Objects reference the scaffolds, never each other, so objects need no ordering among
themselves and there is never a constraint-solving fixpoint. It is a dataflow
spreadsheet, not a constraint solver. That makes it predictable, fast, and debuggable.
6.2 Performance and caching
Because a config is an immutable value that changes identity on every edit, the
resolver caches the built Scope in a WeakMap keyed on the config object, so many
fields checking their own formula in one render pass share a single scope build.
7. Layer 5: Control parameters (the knobs)
The parametric layer (§5) gives many degrees of freedom. The control layer decides which of them an end user should touch, and how.
7.1 The configurator (a curated projection of variables)
"configurator": {
"inputs": [
{ "target": "bay", "label": "Room width", "control": "slider",
"unit": "ft", "min": 90, "max": 150, "step": 5 },
{ "target": "roof_style", "label": "Roof", "control": "select",
"options": [ {"value":0,"label":"Flat"}, {"value":3,"label":"Hip"} ] }
]
}
Each input targets a variable (or a point coordinate via the synonyms) and declares
its presentation (slider | number | select | toggle), range, step, and label. The
configurator is pure metadata: the resolver and every geometry consumer ignore it.
Turning a knob does one thing, write a number into a variable, and then the normal
resolve, expand, and render pass produces a new model.
Design rule. The set of knobs is an authored, curated surface, not the raw variable list. The template author (the domain expert) decides what a downstream user may vary and within what bounds. This is what lets a non-expert safely customise an expert's design.
7.2 Presence switches (enabled)
The second control primitive is the boolean/number enabled field on every object,
itself formula-drivable. Two idioms fall out of it:
- Optional parts:
"formulas": { "enabled": "= has_pooja" }. A variable toggles a whole room in or out, and the rest of the model re-flows around it. - Mutually-exclusive variants: four roof objects, each gated
"= 1 - min(1, abs(roof_style - N))", so exactly one is enabled for a givenroof_style. Aselectknob then swaps whole sub-assemblies with no new code.
Disabled objects are dropped centrally at expansion (§9), so every renderer honours them without extra code.
8. Layer 6: Components (reusable parametric sub-assemblies)
When a domain has repeated sub-assemblies, promote them to components. A component is a mini-model stored once and instantiated many times.
- A definition (
components: { id → ComponentDef }) has its ownvariables/points/objects, authored in local coordinates (origin 0,0), and declares which variables are its publicparams(label + default). - An instance (
{ "type": "component", "ref": "id", "params": {...}, "x", "y", "z_offset" }) overrides the public params (numbers, or formulas evaluated in the host's scope) and places the body at an offset.
At expansion the instance is flattened: resolve the component with the override params
and origin, recurse (components may nest), then offset every produced object into the
host frame. No renderer needs to know component exists. This is ordinary procedural
abstraction, a parameterised function call expressed in data.
9. Layer 7: Expansion and renderers (one model, many outputs)
Between the resolved parametric model and the outputs sits one derivation step
(expandRoomWalls). This is the seam that keeps every renderer simple. In one pass it:
- grows centreline footprints to outer faces (per
coord_convention); - expands components into concrete objects;
- expands multi-flight staircases into plain flights plus landings;
- trims walls where pillars overlap;
- anchors room-nested furniture to the (possibly resized) room footprint;
- drops disabled objects (
enabled=false/0) so nothing downstream sees them; - degrades on bad input by skipping a malformed object with a warning rather than producing a blank model.
The output is a concrete, low-level model that every consumer reads:
- 3-D (
react-three-fiber/ Three.js): solids, CSG openings, materials; - 2-D SVG: floor plans, elevations, roof details, a filtered "layout" sheet;
- Quantities: a wall-area / bill-of-materials estimator;
- Export and share: GLB, a URL-encoded share link, a native
.wadifile.
Design rule. Do the derivation once, centrally, and let every output consume the simplified result. Renderers should be simple mappers from the concrete model to their medium. If two renderers each re-derive geometry, they will drift.
10. Cross-cutting concerns
10.1 Validation and forward-compatibility
- The schema is a strict typed contract (Zod discriminated unions, optional fields,
.strict()objects). A bad document is rejected with field-level errors, not a silent misrender. - Additive-optional evolution: new capabilities are added as optional fields, so an old document still validates (missing fields take defaults).
- Tolerant load for forward-compat: when a document authored by a newer build carries
keys this build does not know, the loader iteratively strips exactly those
unrecognized_keys(at any depth) and retries, reporting what it dropped. A newer share link then opens on an older engine, minus the features it cannot render, instead of hard-failing.
10.2 Non-geometry metadata is quarantined
Concerns that must not affect geometry live in their own blocks that the resolver
ignores: layers (visibility groups), units (display labelling), thumbnails
(preview snapshots), and configurator (control metadata). Keeping them out of the
geometry path means they cannot perturb the model.
10.3 The extensibility registry (adding a new primitive)
New primitives plug into a registry so you do not edit N dispatchers. A
NodeDefinition bundles everything one primitive needs in one module:
interface NodeDefinition {
type: string; // discriminator it handles
label: string; // menu / tree label
addable?: boolean; // offer in "+ Add" menu
makeDefault?(cfg, existing): Object; // a sensible new instance
fields?: FieldSpec[]; // → schema / form / docs / DSL
Form?: Component; // bespoke property-panel editor (else AutoForm from fields)
layerRole?: string | (obj,floor) => string; // default layer/role
render3D?(obj, ctx): { layerId, node } | null; // 3-D output
planFootprint?(obj): { cx, cy, w, d, rot, label } | null; // 2-D footprint
expand?(obj, ctx): Object[]; // optional decomposition into simpler objects
constraints?: Constraint[]; // per-primitive structural rules (merged into the linter)
}
Dispatchers (3-D scene, 2-D plan, property panel, add-menu, layers) consult the registry first, falling back to legacy per-type switches. A new primitive is one self-contained file, not an edit spread across the renderers.
11. The principles, distilled
Strip away houses and this is the transferable core:
- Three tiers, kept separate: vocabulary (primitives), assembly (a relational model), control (curated knobs). Never merge them.
- Primitives are typed records: a
typediscriminator, a flat set of scalar parameters, optional nesting, and a shared envelope (name,formulas,enabled,layer, placement). - The model is a single declarative document, not code: inspectable, diffable, shareable, runtime-agnostic, and authorable by human, form, or AI.
- Keep intrinsic fields as plain numbers and attach formulas beside them. The number is always a valid fallback; the formula is an overlay.
- A parametric layer over the primitives: named variables, named anchors (points), structural scaffolds (grids), and a per-field formula map.
- A small, total, side-effect-free formula language: arithmetic plus a few pure
domain functions, no
eval, no control flow. Emulate branching with arithmetic and track dependencies. - A resolver that is a pure
model → modelfunction: collect symbols, topologically order (detect cycles), evaluate to a flat scope, publish scaffold symbols, write numbers into fields. One-directional dataflow, not a constraint solver. Never throws; fast-path; idempotent; immutable. - Control is a curated projection of variables into UI knobs (bounds, steps, widget), authored by the domain expert. Add presence switches for optional and mutually-exclusive parts.
- Reusable sub-assemblies as components: mini-models with public params, instantiated with overrides and placement, flattened at expansion. Procedural abstraction in data.
- One central expansion step produces a concrete low-level model for many renderers. Derive once; consumers stay simple and do not drift.
- Conventions that delete arithmetic (centrelines, grids). Push structure into the model so authors write intent, not offsets.
- Quarantine non-geometry metadata (visibility, display units, previews, control) so it cannot perturb geometry.
- Evolve additively and load tolerantly. Optional new fields plus strip-unknown on load give forward and backward compatibility for shared documents.
- Extensibility via a registry: one new primitive is one module that declares how it is created, edited, and rendered.
12. The recipe (applying the method to a new domain)
Work top-down through these steps. Each maps to a Wadi layer.
Step 1. Enumerate the primitives. List the irreducible parts of a design in your
domain. For each one: choose a type name, list its intrinsic scalar parameters, note
any nested sub-structure, and mark which fields are authored and which are derived.
Step 2. Fix the placement frame and conventions. Define the coordinate and stacking frame and the meaning of position and size. Then find the arithmetic your authors would otherwise repeat and design a convention (like centrelines) or a scaffold (like grids) to delete it.
Step 3. Define the assembly container. Choose the scoping and stacking bucket (floor, page, stage, row) and how buckets order.
Step 4. Serialise as one strict document. Pick JSON and write a strict, discriminated, optional-friendly schema. This is your wire format and source of truth.
Step 5. Add the parametric layer. Add variables, named points or anchors, any
grids or scaffolds, and a per-field formulas map on every container.
Step 6. Implement the formula engine. Arithmetic plus your domain's pure
functions, the leading-= convention, dependency extraction, and never throws.
Step 7. Implement the resolver. Collect symbols, topo-order (with cycle detection), evaluate to a flat scope, publish scaffold symbols, write fields. Fast-path, immutable, idempotent, and warning-not-exception.
Step 8. Choose control parameters. Expose a curated subset of variables as a
configurator (widget, bounds, step, label). Add enabled presence switches for
optional and variant parts.
Step 9. Add components if the domain repeats sub-assemblies.
Step 10. Implement one expansion step that produces a concrete model for your renderers and exporters. Derive once; keep outputs simple.
Step 11. Add a registry so new primitives are one module.
Step 12. Add validation, tolerant load, and quarantined metadata (visibility, units, previews).
13. Worked second domain: a parametric solar-PV farm
To show the method is domain-agnostic, here is the same architecture applied to utility-scale solar. Nothing about houses carries over except the structure.
Step 1. Primitives.
| Primitive | Intrinsic parameters |
|---|---|
panel |
w, h, wattage |
table (a rack of panels) |
panels_x, panels_y, tilt, x, y |
inverter |
x, y, rated_kw |
combiner_box |
x, y, inputs |
cable_run |
path[], gauge |
access_road |
path[], width |
block (component) |
a reusable pod of table×N + one inverter |
Each gets the same envelope: type, name, formulas, enabled, layer.
Step 2. Frame and convention. Ground plane, X east and Y north, metres. The arithmetic tax here is row pitch versus ground-coverage ratio (GCR): authors should not hand-place every row. So introduce a field grid whose lines are the row and column centrelines.
Step 3. Container. A sections[] array (fenced parcels), analogous to floors.
Steps 4 to 5. Document and parametric layer:
{
"variables": {
"plot_w": 20000, "plot_d": 12000,
"gcr": 40, // ground-coverage ratio, %
"table_len": 340, // one table, cm
"row_pitch": "= table_len * 100 / gcr",
"n_rows": "= floor(plot_d / row_pitch)",
"target_kw": 5000
},
"grids": {
"field": {
"x": [ {"name":"C1","at":0}, {"name":"C2","at":"= table_len"}, {"name":"C3","at":"= 2*table_len"} ],
"y": [ {"name":"R1","at":0}, {"name":"R2","at":"= row_pitch"}, {"name":"R3","at":"= 2*row_pitch"} ]
}
},
"configurator": {
"inputs": [
{ "target": "gcr", "label": "Ground coverage", "control": "slider", "unit": "percent", "min": 30, "max": 55, "step": 1 },
{ "target": "target_kw", "label": "Target capacity", "control": "number", "unit": "count" }
]
},
"sections": [
{ "name": "Array A", "objects": [
{ "type": "table", "name": "T-R1C1", "x": 0, "y": 0, "panels_x": 4, "panels_y": 2, "tilt": 20,
"formulas": { "x": "= field.xC1", "y": "= field.yR1" } },
{ "type": "table", "name": "T-R2C1", "x": 0, "y": 0, "panels_x": 4, "panels_y": 2, "tilt": 20,
"formulas": { "x": "= field.xC1", "y": "= field.yR2" } },
{ "type": "inverter", "name": "INV-1", "x": 0, "y": 0, "rated_kw": 1250,
"formulas": { "enabled": "= 1 - min(1, abs(0 - 0))" } }
] }
]
}
Steps 6 to 7. Formula engine and resolver: identical to Wadi's. field.yR2
resolves to the row-2 centreline. Every table binds to a grid node and re-flows when
gcr changes (which changes row_pitch, which moves every R* line).
Step 8. Control: the two knobs above. Adjusting Ground coverage re-pitches every
row live. Target capacity could gate whole block components on via enabled
formulas (add tables until n_tables * table_kw ≥ target_kw).
Step 9. Components: a block is tables plus an inverter, instantiated per parcel
with param overrides, exactly like Wadi's component.
Step 10. Expansion and outputs: one expansion (grid to table positions, disabled dropped, blocks flattened) feeds a 3-D site view, a 2-D layout drawing, a quantities output (panel count, cable length, projected kW, the domain's bill of materials), and a share link.
13.1 The mapping is mechanical
| Generic role | Wadi (houses) | Solar PV farm | Data-centre floor | Garden design |
|---|---|---|---|---|
| Primitive | room, wall, roof | table, inverter | rack, CRAC, PDU | bed, path, tree |
| Container | floor | section/parcel | room / row | zone |
| Scaffold (grid) | bay/axis grid | row × column pitch | floor-tile grid | plot grid |
| Key variables | bay, depth, roof_style | gcr, target_kw | rack_count, redundancy | sun_exposure, bed_w |
| Control knobs | plot size, roof, rooms | GCR, capacity | racks, N+1 vs 2N | sunlight, spacing |
| Component | stair pod, bay | array block | cooling pod | planting module |
| Presence switch | optional pooja room | spare string | redundant CRAC | seasonal bed |
| Expansion output | plans, 3-D, quantities | layout, kW, cable BOM | rack elevation, kW/cooling load | plan, plant list |
Every column is the same seven layers with different nouns. The architecture is the reusable asset, not the primitives.
14. Anti-patterns (things this method deliberately avoids)
- A constraint solver. Bidirectional constraints ("keep these two walls aligned") need a fixpoint engine, are slow, and fail unpredictably. One-directional dataflow (spreadsheet, not solver) is predictable and fast. If you need a constraint, model it as a formula that computes one side from the other.
- Formulas that reference other objects. Objects reference scaffolds (variables/points/grids), never each other, so there is no inter-object ordering. Shared quantities live in a variable both objects read.
evalor a Turing-complete embedded language. A total, branch-free arithmetic language keeps the dependency graph static and the model safe to share.- Re-deriving geometry in each renderer. Derive once at expansion; consumers map the concrete model. Two derivations drift.
- Letting display concerns touch geometry. Units, layers, colours, previews are quarantined metadata the resolver ignores.
- Editing N dispatchers to add a primitive. Use a registry; one primitive is one module.
15. Summary
Model a design domain as a small typed vocabulary of primitives, composed into a single declarative document, made parametric by a layer of named variables, anchors, and grids wired through a small safe formula language, and collapsed into concrete geometry by a pure, one-directional, cycle-safe resolver followed by a single expansion step that feeds many simple renderers. Expose a curated subset of the variables as control knobs, gate optional parts with presence switches, factor repetition into reusable components, keep non-geometry concerns quarantined, evolve the schema additively, and load tolerantly. Make the whole thing extensible through a registry. With that structure, "choose a design and vary it for your use case" works in any domain, because the reusable asset is the architecture, not the house.