This guide covers practical patterns for AI agents writing Loon — vcad's Lisp-inspired geometry language. For the complete syntax reference, see Loon Language Docs. For a tutorial introduction, see Loon Language.
vcad has two compact formats. Loon uses S-expression syntax with square brackets ([cube 50 30 5]), let bindings, and pipe. VCode is a separate line-based opcode format (C 50 30 5, Y 5 10) used for IR serialization. The create_cad_loon MCP tool accepts Loon, not VCode. If you are seeing single-letter opcodes, you are looking at VCode.
Structure of a Loon Program
A Loon program is a sequence of expressions evaluated top to bottom. The core pattern is: define named geometry with let, then declare visible outputs with root.
; 1. Optional custom materials
[material "purple" 0.5 0.2 0.8 0 0.5]
; 2. Build geometry with let bindings
[let plate [cube 100 60 5]]
[let hole [cylinder 2.75 10]]
[let drilled [difference plate [translate 50 30 0 hole]]]
; 3. Declare scene roots
[root drilled "aluminum"]
Built-in materials (aluminum, steel, abs-black, abs-white, etc.) are available without declaration.
Pattern: Named Assembly with let
Use let bindings to give names to intermediate results. This produces a readable sequence that mirrors a machining plan and makes debugging easier — if a dimension is wrong, you can identify which binding to adjust.
; Mounting bracket
[let base [cube 80 60 8]]
[let rib [translate 0 25 8 [cube 80 10 30]]]
[let bracket [union base rib]]
; Drill pattern
[let hole [cylinder 3.0 15]]
[let h1 [translate 15 30 0 hole]]
[let h2 [translate 65 30 0 hole]]
[let drilled [difference bracket [union h1 h2]]]
[let result [fillet 2 drilled]]
[root result "aluminum"]
Guideline: Name every significant intermediate result. Avoid deeply nested expressions with more than 2-3 levels — use let to flatten them.
let names geometry for readability, and it is inlined at evaluation. For a
dimension you expect to change later, use defparam instead — see
Parameters That Survive.
Pattern: Parameters That Survive
A let binding is inlined during evaluation. The document that comes out holds
only the arithmetic result, so a value named pitch_axis_x in the source is an
anonymous 310.0 afterwards — and moving it means finding and editing every
literal that was derived from it.
defparam declares a value that survives into the document as a named
parameter. Afterwards it can be driven with
set_parameters and differentiated with
parameter_gradient, without re-authoring the source.
[defparam pitch_axis_x 310.0 :unit "mm" :min 280 :max 340]
[defparam wall "pitch_axis_x * 0.02"] ; derived — a formula over other parameters
[let leg [fn [side] [translate [* side pitch_axis_x] 0 0 [cube 20 20 wall]]]]
[root [union [leg 1] [leg -1]] "aluminum"]
Setting pitch_axis_x to 315 now moves both legs — including the mirrored one,
with its sign preserved.
Names must be identifier-safe: letters, digits and underscores. A dash reads as
subtraction inside a formula, so femur-inner is rejected with a suggestion
rather than silently meaning something else.
How intent is recovered. A parameter reaches geometry through arbitrary loon code, so vcad evaluates the program at the declared values and again with each parameter nudged, and reads the relationship off the difference. The recovered formulas are then re-checked against a fresh evaluation at a point that was not used to fit them. Anything that does not reproduce the program's own answer — a field that depends on a parameter non-linearly, a parameter that changes how many nodes exist — keeps its literal and is reported as a warning. The geometry is correct either way; the warning tells you which knob will not move which part, and why.
Pattern: Datums Instead of Shared Literals
When two parts sit against the same plane, writing that plane's coordinate into both is how they come to disagree. A datum makes it one named entity.
[datum-plane "carrier_face" y 140.0]
[root [union
[translate 0 [datum "carrier_face"] 0 [cube 20 5 5]]
[translate 0 [datum+ "carrier_face" 5] 0 [cube 20 5 5]]] "aluminum"]
[datum+ "name" d] reads "d millimetres outboard of that face". There is also
datum-axis and datum-point, whose components are read with datum-x,
datum-y, datum-z. Declaring one name twice with different geometry is an
error.
Pattern: Stacks for Lateral Packing
A run of parts butting against each other is usually written as a table in a
comment and a column of literals in the model. stack makes the table the
model:
[stack y "leg" 131.0
[lane "femur_inner" 5.0]
[gap "idler_run" 1.0] ; a running clearance, not an arbitrary number
[lane "idler_boss" 3.0]
[lane "carrier" 5.0]
[lane "actuator" 37.0]]
Each lane boundary becomes a datum plane and a derived parameter —
leg_femur_inner_lo / _hi, leg_idler_boss_lo / _hi, … through leg_end
at the outer face. The knobs are the origin, each lane thickness
(leg_carrier_t) and each gap (leg_idler_run); opening a clearance by 1 mm
slides everything outboard of it and nothing inboard.
Pattern: pipe for Sequential Operations
When a single body passes through a chain of transforms and features, pipe is more readable than nested let bindings or deep nesting.
[pipe
[cube 50 50 30]
[fillet 3]
[shell 2]
[translate -25 -25 0]]
[root _ "abs-white"]
Each step receives the previous result as its last argument. The special variable _ refers to the most recent pipe result.
Guideline: Use pipe for linear chains of 3+ operations on a single body. Use let when you need to reference intermediate results more than once (e.g., subtracting the same hole template from multiple positions).
Pattern: Reusable Tool Bodies
Define a "tool" shape once with let, then position it multiple times for repeated operations like holes or slots.
[let plate [cube 100 60 5]]
[let hole [cylinder 2.75 10]]
[let h1 [translate 10 10 0 hole]]
[let h2 [translate 90 10 0 hole]]
[let h3 [translate 10 50 0 hole]]
[let h4 [translate 90 50 0 hole]]
[let drilled [difference plate [union h1 [union h2 [union h3 h4]]]]]
[root drilled "aluminum"]
For regular arrays of holes, prefer patterns over manual placement:
[let plate [cube 100 60 5]]
[let bolt-hole [translate 40 0 -1 [cylinder 3 7]]]
[let pattern [circular-pattern 0 0 0 0 0 1 6 360 bolt-hole]]
[let result [difference plate pattern]]
[root result "steel"]
Pattern: Modules — Shared Vocabulary in Its Own File
When several files (or several hundred roots) share the same helpers —
a rounded-rect plate builder, a bore, an actuator envelope — put them in a
module: an ordinary .loon file whose top-level lets are its exports.
The file that needs them names the module; it never pastes a preamble.
; plates.loon — a module. Every top-level let is an export.
[let plate-z [fn [n0 n1 u0 u1 v0 v1 r]
[let t [- n1 n0]]
; … rounded-rect as two boxes + four corner cylinders …
]]
[let bore-z [fn [d cu cv n0 n1]
[translate cu cv [- n0 2.0] [cylinder [/ d 2.0] [+ [- n1 n0] 4.0]]]]]
; deck.loon — roots only.
[use plates [plate-z bore-z]] ; just these two, unqualified
[let deck [difference [bore-z 6.0 20.0 20.0 0.0 3.0]
[plate-z 0.0 3.0 0.0 120.0 0.0 80.0 12.0]]]
[root deck "aluminum"]
Three spellings of use:
| Form | Brings in |
|---|---|
[use plates] | everything, qualified: plates.plate-z |
[use plates [plate-z bore-z]] | the listed names, unqualified |
[use plates :as p] | everything, as p.plate-z |
Dotted names are subdirectories: [use hardware.fasteners] is
hardware/fasteners.loon. Mark pub let on some names and only those are
exported; with no pub at all, every top-level let is.
Why a module and not an include: a module is evaluated once, in its own
environment (with the vcad library in scope) and exports values — the
closures themselves — not text. It cannot see the importer's bindings, two
files that use it share one evaluation, a non-pub let stays private,
and a cycle is an error rather than a hang.
Resolution is the same in vcad info/export, vcad-render, and the MCP
server's load_document:
- next to the importing file (
<dir>/<name>.loon); - each directory in
$VCAD_LOON_PATH(:-separated) — the lib path, for parts shared across projects; - sources the host handed the kernel by value (
create_cad_loon'smodules), which win when they name the same module.
A file beside the importer shadows a lib-path module of the same name, so a
project can override one locally. See
hardware/rose-pro
for a 108-root humanoid whose plate vocabulary is one module.
Pattern: Sketch and Extrude
For profiles that go beyond box/cylinder primitives, define a 2D sketch and extrude it.
[let profile [sketch xy
[line 0 0 30 0]
[line 30 0 30 5]
[line 5 5 5 20]
[line 5 20 0 20]
[line 0 20 0 0]
end]]
[let bracket [extrude 0 0 15 profile]]
[root bracket "aluminum"]
Sketch planes: xy (horizontal), xz (front), yz (side). The profile must form a closed loop — the last point must connect back to the first.
Pattern: Multi-Part Scenes
Multiple root directives create multi-part documents. Each root gets its own material.
[let base [cube 100 100 10]]
[let pillar [translate 40 40 10 [cylinder 10 50]]]
[root base "steel"]
[root pillar "aluminum"]
Pattern: Sheet Metal That Carries Its Bends
A cut-and-bent part can be authored as a sheet chain instead of unioned
plates. The chain is subject-last like everything else, so it threads through
pipe:
[let bracket
[pipe [sheet-base-flange-rect 200.0 120.0 3.0 "al-soft"]
[sheet-edge-flange "east" 40.0 90.0]
[sheet-edge-flange "west" 40.0 90.0]
[sheet-hem "north" 8.0]
[sheet-bend-relief]]]
[root bracket "aluminum"]
This matters because the bends are kept. Modelled as unioned plates with
sharp corners, the same bracket has no bend metadata, and the flat pattern has
to be inferred back out of the solid afterwards by flat_pattern_from_solid
— which works and fails closed, but is a lossy round trip for information the
author had at design time. Authored as a chain, sheet_metal_unfold is exact,
and the bend table, DXF, nesting and cost all come off the model as written.
edge is an outline edge index, or — on the panel of a rectangular base
flange — "south" / "east" / "north" / "west" (edge 0 is the -Y edge,
counter-clockwise from there). Angles are degrees, as with rotate. A radius
or K-factor of 0.0 means "use the default": material thickness, or the shop
profile's fixed radius, or the bend table.
The full vocabulary: sheet-base-flange-rect, sheet-base-flange (arbitrary
outline plus holes, each a flat #[x0 y0 x1 y1 ...]), sheet-edge-flange,
sheet-jog, sheet-hem, sheet-bend-relief, each with an explicit -at
form that takes the panel id, radius, direction and K-factor. Add
-shop "sendcutsend" to resolve every bend through a shop's published table,
and -engraved to put part marking on the flat pattern's ENGRAVE layer.
Sheet nodes are a bend graph, not a solid: do not union, transform or fillet them. Handing a solid to a sheet op is an error, not a silent reinterpretation.
Pattern: Faceting You Chose
The plain curved primitives leave segment count to the kernel default (32). Where the facets are load-bearing — a bore that has to accept a real shaft, a diameter measured off the mesh — pin it:
[let bore [cylinder-n 10.0 40.0 128]]
A 32-segment inscribed circle sits 0.24% inside its nominal radius, which is
a 0.05 mm interference on a 20 mm bore. sphere-n, cone-n and torus-n
work the same way.
Pattern: Vendor Parts, Not Approximations
Place purchased geometry instead of approximating it:
[let actuator [translate 0 0 60 [import-step "vendor/x6-60.step"]]]
[let plate [difference [cube 100 100 20] actuator]]
Relative paths resolve against the directory of the .loon file that names
them, so a model and its vendor files move together. import-step-body picks
one body out of a multi-body STEP; import-mesh and import-mesh-scaled
place an STL.
Substituting "a cylinder of about the right size" for a purchased part means fit checks only ever test the envelope you invented — and real parts have connector bosses and cable exits that project past the body their datasheet advertises.
Common Mistakes
Wrong: VCode opcodes in Loon source. The create_cad_loon tool expects S-expression syntax, not VCode opcodes.
; WRONG — this is VCode, not Loon
C 50 30 5
Y 3 10
T 1 25 15 0
D 0 2
; CORRECT — Loon syntax
[let plate [cube 50 30 5]]
[let hole [translate 25 15 0 [cylinder 3 10]]]
[root [difference plate hole] "aluminum"]
Wrong: Open sketches. Sketch profiles must close. An open profile causes an error during extrusion.
Wrong: Forward references. let bindings are evaluated in order. A binding cannot reference a name defined later in the file.
Workflow Integration
The standard MCP workflow with Loon:
- Generate —
create_cad_loonwith the Loon source - Verify —
inspect_cadto check volume, bounding box, center of mass - Iterate — Adjust dimensions in the Loon source if inspection reveals problems
- Export —
export_cadto STL or GLB, oropen_in_browserfor a shareable link
{
"source": "[let plate [cube 50 30 5]]\n[root plate \"aluminum\"]",
"format": "vcode"
}
The format parameter controls the output representation: "vcode" (default) returns the compact VCode serialization, "json" returns the full JSON IR. Both can be passed to downstream tools.
For the complete language specification, see the Loon Language Docs. For the create_cad_loon parameter reference, see create_cad_loon.