loft grammar cheat-sheet (v0.4)
One statement per line. # starts a comment — comments are kept (they ride the statement below them, or the line they end, and survive reformatting). Blank lines are ignored.
Type declarations may also take the block form: end the head with : and put one parameter per indented line beneath it (any consistent indent). Same model either way — the one-line form stays legal, and the formatter picks.
Statement order across the file never matters — references resolve across the whole file. Within a line, slot order is fixed: category → type → [id] → placement (see Line anatomy).
A model may span multiple .loft files: every file in a project directory compiles as ONE model with ONE namespace — reference names across files bare, with no import lines, and file layout never changes meaning (folders navigate, never scope).
Start here — build in this order
Everything below this section is reference. This section is method. Most broken models are correctly-spelled statements written in the wrong order and measured from the wrong thing.
# 1. datums first — the framework everything else is measured from level L1 elev 0' height 10'-0" grid A vertical at 0' grid B vertical at A + 24'-0" # the bay width, not grid B's coordinate grid C vertical at B + 30'-0" grid 1 horizontal at 0' grid 2 horizontal at 1 + 20'-0" # 2. then hosts — walls today (floors and roofs later); frame first, then the line # traveling (A,1) -> (C,1), heading east; right = south wall EXT-1 W1 from (A, 1) to (C, 1) justify left # 3. then what they host — a typed product, placed along the host off a datum door 3070 D1 in W1 at 4' past B # the 3070 doortype states the size once, below # 4. then the views — what to draw, at what scale, cut where (a query, never a # container). A view owns its file: each goes in its own views/ file. view plan L1-plan of L1 scale 1/4" cut 4'-0" name "Level 1 Floor Plan"
- Datums first. Declare levels and grids before anything measured from them. Order doesn't matter to the parser, but it matters to you: an element you can't anchor is a datum you haven't declared yet
- Types declare facts once; instances reference a type and add only what it delegates. A door's size is a fact about the *product* — state it in a
doortype, and every placement follows. One rule, every category - Then hosts, then what they host. Walls carry doors and windows (floors and roofs will carry their own). An opening names its host and a station along it, so the host must exist as a decision before the opening can be placed — this is the order the model depends in, whatever order you type
- Relative by default; absolute is the last resort. Prefer, in this order: a declared datum (
(A, 1)) → an expression off one (A + 14') → another element's datum (W1.end,D1.jamb.far) → a bare literal. A literal is the right answer only where the number has no reference — an origin, or a free choice - Say the measurement, not the coordinate.
grid C vertical at B + 30'-0"states a bay width;at 54'-0"states a position and discards why it sits there. Widen the first bay and grid C follows on its own — the coordinate form leaves every later number to be recomputed by hand - One travel frame governs everything. A wall's own from → to direction decides its left from its right — which face
justifynames, which way layers stack, which sideswinghinges on, and whereat 0'starts for its openings. Choose the direction, state it in a comment, then write the line - When travel and measurement disagree, anchor to a datum. Closing a perimeter often forces a wall to run opposite the direction you'd naturally measure along it. Rather than counting backwards from that wall's start, place its openings off a grid:
at 4' past 2 - Check your work with the plan's `dims` button — it cycles
stated→measured→off. stated draws only the facts your statements assert: a grid dimensioned from the anchor you chained it off, a carrier's offset, an opening's position. measured draws the adjacent-pair chain a paper drawing would show, from resolved positions. If the stated view comes up nearly empty, you dimensioned in coordinates — there are no relationships in the model for it to draw
Line anatomy — how a declaration reads
Every declaration is category → type → [id] → placement, with a slot dropped where the category doesn't carry it. One of the two leading name slots is always required, so the first name after the keyword is never ambiguous.
# element: the type leads and is REQUIRED; the id follows, optional wall EXT-1 W1 from (A, 1) to (C, 1) justify left wall STUD4 from 1 to 2 along A # anonymous — id omitted, type still leads # datum: no type — the REQUIRED id leads (a datum exists to be referenced) level L1 elev 0' height 10'-0" grid A vertical at 0' # door/window: the type leads here too (and is required); the id follows, optional door 3070 D1 in W1 at 4' past B # the 3070 doortype states the size once, below # opening: the type-less cutter — a hole has no product identity, so its # dimensions live on the line; id optional, like walls opening O1 in W1 at 14' width 3' height 7' sill 4'
- Read the element form as a noun phrase:
wall EXT-1 W1= "an EXT-1 wall, called W1" — the type says what it's made of, the id names it, the rest places it - Declare an element id only when something will reference it — host an opening, anchor a chain (
W1.end), take a carrier offset. Datums always carry their id - The v0.1 trailing spelling (
wall W1 from … type EXT-1) is retired — the parser recognizes it and answers with the corrected line
Lengths
Always carry units. Bare numbers are never lengths.
12' 8" 12'-6" 12'-6 1/2" 27.5' 4.5" 5/8" 3-5/8" 12ft 8in- Word suffixes (
12ft,8in) are accepted on input; canonical output prints the symbol forms - Rejected near-misses:
12'6"(the hyphen is mandatory — write 12'-6") ·1/2'(no fractional feet — write 6") ·12(bare numbers are never lengths — write 12')
Coordinates
(x, y), plan view: positive X is right (east), positive Y is up the page (north).
Each slot is a length literal or a position expression over datums:
(12', 28') (A, 1) (A + 14', 1) (B - 4', 2) (A + (B - A)/2, 1)- A bare token in a coordinate slot is always a datum reference (a grid name), never a number.
Expressions
Anywhere a position or distance goes, a static expression works: + - * /, parentheses, datum references.
grid A.5 vertical at A + (B - A)/2 # midpoint between grids A and B grid D vertical at C + 12' # chained off C door 3070 D4 in W1 at (2 - 1)/2 # centered: half the 1-to-2 span
- Declared vs projected: grids and levels are *declared* datums — they get their own statements; elements *project* the rest (
W1.end,D1.hinge). Both are named datums. An expression likeA - 5'is a position, not a datum — nothing can reference it later; where a slot demands a named datum, declare one there:grid A.1 vertical at A - 5' - A datum reference is a position; two same-axis positions subtract into a distance; positions never add, multiply, or divide
- Bare numbers only multiply or divide — a bare number is never a length
- An offset needs an anchor: write
A + (B - A)/2, not(B - A)/2 - Space the minus between names:
(B - A)/2, never(B-A)/2— hyphens are name characters (as inEXT-1) - Cycles between datum positions are errors
- Qualified names (
grid.2,level.L1) are always legal and settle any ambiguity
Project & units
project "Sample Project" # optional units imperial # optional; imperial is the v0 default
Levels
A level declares its story one way: height <len>, or top <level> [± offset] — the height is then derived (the named level's elevation minus this one's). Story height is a fact about the building, stated once.
level L1 elev 0' height 10'-0" level L2 elev 10'-0" top L3 # story derived: L3's elev minus L2's level L3 elev 21'-0" height 9'-0" level L1 # this FILE's level — the default base for its elements
- A level is declared once in the unit (any file, any position — datums beside the grids by convention). A bare
level L2line is the file's level statement: file-scoped content that sets the default base for every element in the file that has one. A default, never a requirement — at most one per file, legal anywhere in it - With exactly ONE level declared, it is every element's default and no file statement is needed. With two or more, an element needing a base must get one — from the file's statement, or its own
base— or the error on the element names both fixes - Elements default to their level's base and top: a wall runs from its level to that level's top, however the level stated it;
base L2 + 7',height 9', andtop L3override per instance (see Walls)
Grids
grid A vertical at 0' grid 3 horizontal at 28'-0" grid B.5 vertical at B + 12'-6"
vertical= line of constant Xhorizontal= line of constant Y- Dotted names are fine (
A.5)
Wall types
walltype STUD4 width 4.5" note "3-5/8 stud + gyp ea side" walltype EXT-1 layers [brick 4", air 1", core: stud 6", gyp 5/8"]
- Layers stack left → right of the wall's travel (
flipmirrors them) core:marks the structural layer
Door & window types
The category requires; the type binds. Every door/window type must satisfy width and height — by fixing a value (stated once, every placement follows) or delegating it (the word instance: each placement supplies it).
doortype 3070 width 3'-0" height 7'-0" # fixed: a product fact, stated once doortype STOREFRONT width instance height 8'-0" # width delegated to each placement windowtype W-5040 width 5'-0" height 4'-0" doortype HM-A: # block form: one parameter per indented line width 3'-0" height 7'-0" u-fire-rating 90 # minutes u-hardware instance text required
- Rhymes with
walltype: keyword, type name, parameter pairs. Type names are project-unique per category - Block form (
doortype HM-A:+ indented lines) and one-line form are the same declaration — the block reads better past two or three parameters, and each parameter line is its own error address. Onlydoortype/windowtypetake a block in v0.4;walltypestays one-line - Supplying a fixed parameter on a placement is an error naming the type and its value — duplicate the type to change it. Omitting a delegated
width/heightis an error too: the placement owes what the type left open u-<name>adds user parameters — job-specific data that rides the format (typed from the literal: length, number,true/false, or quoted text; a delegated one states its type:u-hardware instance text). One name has ONE value type within its category — u- vocabulary belongs to the component, so a windowtype may type the same name differently than a doortype- A delegated u- parameter is optional unless the type adds trailing
required— then every placement must supply the pair, same error as a missing delegated width swing,reverse, and windowsillstay on placements — per-placement facts, never product facts
Walls
A wall's line is stated one of two ways — by its endpoints, or by a carrier it rides plus two extents that cut it. Both are first-class; pick whichever states the facts you already know.
Carrier + extents is not a partition trick — an exterior wall running along a grid is the case it fits best. It is how a line-based element is placed — walls and roomlines today, every line element to come.
# by endpoints wall EXT-1 W1 from (A, 1) to (C, 1) justify left # by carrier + extents — the carrier fixes the line, two transverse datums cut it wall EXT-1 W2 from 1 to 2 along A justify left # ALONG grid A — no offset, no side wall STUD4 W7 from 1 to 2 at 4' right of W2 # 4' clear of W2's core face wall STUD4 from 1 to W5.end at 9' left of B # grid carrier; a wall's line datum cuts too # vertical facts — base and top name levels; one wall may span stories wall EXT-1 W9 from (A, 3) to (B, 3) base L1 top L3 # a shaft wall, L1 up to L3
- Order is category, type, id, placement —
wall EXT-1 W1 from …reads "an EXT-1 wall, called W1" (and rhymes with itswalltype EXT-1declaration) - id (
W1) optional — required only to host doors/windows justify:centerline(default) |left|right— which face of the wall the drawn line is, in YOUR from → to direction (you typed it; you already know left from right). A side may name its plane: bareleftis the finish face;justify left focholds the structural core face (fos/foc/fom— face of stud/concrete/masonry, one plane, three field spellings),justify left fofsays finish face out loud;justify core.centerlineholds the core's own center (differs fromcenterlineon an asymmetric assembly). Hold the core when finishes may change — it's the plane that doesn't move- Everything is in the travel frame: layers stack left → right, door arcs open right;
flipmirrors that content, never the body - Draw perimeters clockwise with
justify leftand the outside faces out - Carrier —
along <ref>rides the location line ALONG a declared reference plane andjustifyplaces the body;at <offset> <side> of <ref>holds it off one instead. A wall carrier always needs both a side and an offset (along W3is an error) - Extents — any two transverse datums: grids, crossing walls, or wall line datums (
W5.end) left/rightare the frame of the wall THIS statement declares — its own from → to travel — never the carrier's- Derive the frame in a comment before picking a side —
# traveling W4→W5 (east), right = south— and the side words stop biting - A bare wall carrier measures from the wall's core face on your side —
at 4' right of W3is 4' clear of W3's studs/block, same rule aspast W3; name another plane to measure elsewhere (right of W3.fofthe finish,right of W3.centerlinethe center). Extents (from W1 to W3) are joins — the wall runs to the crossing wall's centerline. The wall must sit clearly on the stated side (zero or straddling offsets are errors) - State each fact once: edit the offset and the wall, its openings, and its dimensions all move together
- Vertical facts: a wall's base is its level (the file's
levelstatement, or the unit's one level) and its top is that level's top.base <level> [± offset]pins the base per instance (Revit's Base Constraint + Base Offset) — a wall from L1 to L4 is one element, andbase L1 top L4says where it lives.height 9'andtop <level|W1.top> [± offset]override the top as ever (heightdefaults to the rest of the base level's story)
Element datums (anchors & accessors)
Every element projects datums you can reference — <id>.<datum>, kind-qualified when ambiguous (wall.W1.end).
wall EXT-1 W4 from W1.end to (C, 2) # chain: W4 follows W1 wall STUD4 W5 from (A + 12', 1) to (A + 12', 2) top W1.top door 3070 D5 in W1 at 6' past D1.jamb # measured from D1's jamb, not its center
- Projection table — wall:
start · end · centerline · core.centerline · fos/foc/fom · fof · top · base| door:centerline · head · jamb+hinge/strike(swinging leaf) | window:centerline · head · jamb · sill| opening:centerline · head · jamb(the base interface, nothing more) - Line datums (
W1.end) fill a whole coordinate slot; plane datums (W1.centerline,W1.core.centerline,W1.top) join expressions on their axis; opening datums are stations — reference them in directed measurements (past D1.jamb) - Wall planes, field notation:
fos/foc/fom= the structural core face (face of stud / concrete / masonry unit — ONE plane; spell it for the material),fof= finish face,cl=centerlineanywhere it appears (W6.cl,D1.cl,to cl),core.centerline= the core's center. Faces come in pairs, so they resolve where a tape can pick the side:at 4' past W6.focmeasures from the core face facing the opening (a barepast W6means the same); in an expression, use.centerline/.core.centerline.ffis held for far face — finish face isfof jamb.near/jamb.farare framed by the host wall — near toward its start, far toward its end (the originatmeasures from). Stable in any direction, soat 2' past D1.jamb.farreads as it sounds- Bare
jambis whichever jamb the measurement reaches first — the one nearer the reference you measured from. Swappastforshort ofand it changes ends, which is why barejambalways needs a direction word - Opening datums are stations (lengths from the wall's start), so they do arithmetic in that wall's own
at:door 3070 D9 in W1 at D1.jamb.far + 2' - Flip the swing and the jambs don't move; rehang the door and
hingefollows - Guardrail: anchor to declared datums (grids, levels) by default, to elements by exception — chains state genuinely relative intent, never a substitute for gridding
- Driver → driven, one direction: W2 follows W1, W1 never feels W2; cycles are errors
Doors
door 3070 D1 in W1 at 4' past B # the 3070 doortype states the size once, below door 3070 D2 in W5 at 2' past 1 swing left door 3070 D3 in W5 at 3' short of 2 door STOREFRONT D6 in W1 at 20' width 6'-8" # STOREFRONT delegates width; the placement supplies it
- The size lives on the
doortype, not the line (see Door & window types) — a parameter pair is legal on a placement only when the type delegates that parameter atmeasures from the wall's start to the door center — a plain length is a station from that start (at 4'-0"), and everything below measures the same station off a datum insteadpast <ref>measures forward from a reference;short of <ref>holds back from one — both along the wall's from→to travel. A grid line or a datum likeD1.jamb.faris a POINT: the tape runs from it to this opening's center. A bare ELEMENT reference (past D4) measures clear — from the reference's facing jamb to this opening's facing jamb — soat 0' past D4is a flush sidelite, andpast D4.centerlineis the center-to-center spelling. A bare CROSSING WALL (past W6) is its core face facing this opening — face of stud/concrete/masonry, the plane a finish change never moves — the same aspast W6.foc; name another plane to measure elsewhere:past W6.centerline(or.cl, the wall's center),past W6.fof(finish face),past W6.core.centerline. From a wall the landing stays this opening's center; addto jambfor clear- Add
to jambto measure to the opening's near jamb instead of its center — the jamb the measurement reaches first, coming from the reference:at 2' past 1 to jamb.to jamb.farmeasures across to the far side instead ("the opening ends at 10'"):at 10' to jamb.far. (to edge, the older spelling, still parses; with an element reference, the near jamb is already the default landing.)to centerlineoverrides the other way — the measurement lands on the opening's center:at 4' past D4 to centerlineputs D4's far jamb 4' from this door's centerline swingis the hinge side (wall's travel frame); doors open away from the wall's facing — addreverseto open toward it. Hardware handing verbatim:swing left= LH,swing left reverse= LHR- Defaults:
swing right; height and width come from the type, always
Windows
window W-5040 WIN1 in W2 at 6'-0" sill 3'-0" window W-5040 WIN2 in W2 at 4' past WIN1 # 4' clear of WIN1's jamb
- Same binding rule as doors: size on the
windowtype, placement facts on the line sillstays per-placement (default3'-0") — a sill height is where THIS window sits, not what the product is
Openings
opening O1 in W1 at 14' width 3' height 7' sill 4'- The type-less cutter: a hole has no product identity, so instance dimensions are *correct* here — not a workaround. Defaults:
height 7'-0",sill 0'(a floor-to-head passage) - Same placement grammar as doors/windows (
past/short of/to jamb/to centerline), and it projects the base datums (O1.jamb.far)
Rooms
room 210 name "OFFICE" at (7', 14') # number (the id — any text: 210, 210A, A-101), name, point; boundary and area are derived roomline from (C, 2) to (C, 3) # closes a boundary where no wall does — an open-plan split roomline RL1 from W1 to 2 at 12' right of A # positions like a wall: carrier + extents, or a point pair
- A room point marks an enclosure — anchor it to declared datums only (grid expressions fine:
at (A + 12', 1 + 8')). Element datums (W1.end) are errors here: a wall edit must never drag a room along. The id is the room NUMBER — unique, optional, any id text (210,210A,A-101,210-1; a hyphen inside a token is always part of a name, so subtraction is always spaced:2 - 1) — and it is what doors and finishes will reference;nameis the tag text - The boundary is derived from the walls around the point (finish faces; doorways don't break it) and the area shows on the plan tag and the takeoff. A room whose walls don't close reads NOT ENCLOSED and the plan rings the open ends — close the wall, extend it, or draw a
roomline; two room points in one enclosure each report the other roomlineis a zero-width room separation line: no type, no geometry, no 3D — a hairline on the plan. It uses the same placement as a wall (point pair, orfrom <extent> to <extent> at <offset> left|right of <ref>/along <grid>), may reference grids and walls, and nothing references it — it only closes a boundary. Id optional
Views
A view is a declared query over the model — never a container of elements. Every fact a view has is authorable on the view line itself; a viewtype is an OPTIONAL template — a named bundle of defaults — and a written fact on the view is a visible override. A view owns its file: each view statement lives in its own views/ file (its page), with only project, units, and viewtype declarations allowed beside it.
# untyped: kind → id → the level it is "of" → [crop] → [facts] → [name] view plan L1-plan of L1 scale 1/4" cut 4'-0" name "Level 1 Floor Plan" view plan L1-min of L1 # every fact optional — scale defaults to 1/8", cut to 4'-0" # or through a template: shared defaults, per-fact; the instance's own words win viewtype plan A-PLAN scale 1/4" cut 4'-0" depth -10'-0" # the view range: looks one story down view A-PLAN L1-key of L1 scale 1/8" # 1/8" here overrides the template's 1/4" # the crop: lower-left corner to upper-right — what the view shows; corners are ordinary coordinates view A-PLAN L1-kitchen of L1 from (A - 5', 1 - 5') to (C + 5', 2 + 5') name "Kitchen — Enlarged Plan" # a section: "at" the cut plane, "to" the far clip — stations on one axis; the gaze is their sign view section S1 at 1 + 4' to 3 name "Building Section" view elevation E1 at 1 - 10' to 3 top L3 + 2' bottom L1 # trims anchor to LEVELS view section S2 horizontal at 10' to 40' # no grid named — the axis word says which way the plane runs
- One view per file. A file holding a
viewholds only that view — a second view, or any element beside one (a wall, a room, even awalltype), is an error teaching the split.viewtypeis the exception: a type declaration lives anywhere, so a template may sit beside the one view that applies it, or with the other types. Declaring a view is the moment a project becomes a directory; a single file with noviewstill draws — the implicit plan of its level, at the 1/8" default - Slot order is every statement's: category → type-or-kind → id → placement. The second word is a viewtype reference or a bare kind word (
plan,section,elevation) for an untyped view; kind words can never name a viewtype. The id is what references the view (a token);name "…"is the title that prints and what the picker shows — optional, trailing everything. The placement is the kind's own: a plan isofa level (neither hostedinnor placedat;of L9with no such level is an unknown-datum error naming the levels you do have), a section or elevation is placed by its plane:at <station> to <station> - The kinds are
plan,section,elevation;rcpis a reserved word that names its era; a discipline (structural, MEP) is a filter on the query, never a kind - Sections and elevations are one machine under two words — the drawing set's vocabulary.
atis the cut plane's station,tothe far clip's, each an ordinary station expression (grid refs preserved, wall anchors refused — a wall edit must not move the drawing); the view looks fromattowardtoand sees that far, so swapping them looks the other way, andtoequal toatsees nothing. The axis comes from the grids the stations name; name none and the axis word leads:vertical/horizontalbeforeat. Walls the plane crosses draw CUT — paper mass bounded by the cut pen, no poché (cut beside a wall, not on it: a plane down a wall's own line cuts it at full length); walls beyond draw in the projection pen, nearer covering farther, openings punched from cut walls and projected on beyond faces; the mass's outline profiles at the cut pen (the drawing is read by its silhouette); every level draws as a datum line with its name and elevation. Optionaltop/bottomtrim the frame at LEVELS (top L3 + 2'— a bare length is a plan's range word, not a trim); untrimmed shows everything, ground to bulkhead - The facts —
scale,cut, and the range wordstop/bottom/depth— are each optional on both statements, any authoring order (canonical print order: scale cut top bottom depth). Resolution is per fact: the instance's word, else its viewtype's, else the language default — scale 1/8" flat, cut 4'-0". Scale is a length literal in paper inches per model foot (1/4",1/8",3/16",1"); it sizes the hatches, the text, and the line weights, so zooming the plan magnifies the drawing instead of changing its scale. Cut is a length above the level - The view range —
top,bottom,depth— are lengths relative to the level (a leading-reads below it). Defaults when unwritten: top = the level's top, bottom = the level, depth = bottom. What a plan shows is computed against them, never authored: an element crossing the cut plane draws cut; wholly between the cut and the top, dashed (above); wholly below the cut and above the depth, in the projection pen (beyond —depth -10'is how a plan shows the story below); outside the range, not at all. Openings follow their host. The relationships are errors with the fix written out — bottom above the cut, top below it, depth above bottom — checked over the effective values, so an authored word conflicts even with a defaulted one from (x, y) to (x, y)after the query is the crop — the rectangle the view shows (Revit's crop region),fromthe lower-left corner,tothe upper-right. It is a placement on the view, before the facts; the corners are ordinary coordinates, so a crop to grids follows them when a bay widens, and a corner off an unknown grid is the usual unknown-datum error. Anchor corners to grids or literals, never to a wall (from W1.endis refused like a room's point). An inside-out pair (toleft of or belowfrom) is an error with the swap written out, never silently fixed. Grid lines end on the crop, bubbles sit just past it, dimension strings stack just inside it (paper offsets from the standard, the same at every scale), and elements outside it clip — a grid the crop doesn't reach, a string to it, a string or room tag not wholly inside it, are hidden, never half-drawn. No crop: the view shows the whole model plus a margin- A view takes no indented body — annotations (dimensions, tags, notes) are reserved for a future version. New projects start two files —
model.loftand the plan's ownviews/file — no project ships with zero views
Reserved keywords
Every major building category is reserved for future versions — using one is an error (not silently ignored). Highlights:
component import export const refplane detail sheet schedule floor roof ceiling stair ramp railing column beam shaft curtainwall duct pipe conduit furniture site topo zone space floortype rooftype materialtype hatchtype layertype …
instanceis a reserved value word: the delegation marker inside type declarations, an error as a value anywhere else