forma-dsl-mcp 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/package.json +52 -0
  4. package/src/bin.js +30 -0
  5. package/src/catalogue.js +703 -0
  6. package/src/document.js +285 -0
  7. package/src/examples.js +212 -0
  8. package/src/guide.js +313 -0
  9. package/src/reference/README.md +153 -0
  10. package/src/reference/api/describe-parameters.md +167 -0
  11. package/src/reference/api/evaluation-context.md +221 -0
  12. package/src/reference/api/export.md +185 -0
  13. package/src/reference/api/geometry-node.md +215 -0
  14. package/src/reference/api/kernel.md +182 -0
  15. package/src/reference/api/low-level.md +239 -0
  16. package/src/reference/api/render.md +217 -0
  17. package/src/reference/api/values.md +223 -0
  18. package/src/reference/errors.md +253 -0
  19. package/src/reference/language/control-flow.md +160 -0
  20. package/src/reference/language/expressions.md +206 -0
  21. package/src/reference/language/overview.md +156 -0
  22. package/src/reference/language/scope.md +184 -0
  23. package/src/reference/language/syntax.md +210 -0
  24. package/src/reference/reference/align.md +189 -0
  25. package/src/reference/reference/booleans.md +231 -0
  26. package/src/reference/reference/component.md +248 -0
  27. package/src/reference/reference/conversions.md +269 -0
  28. package/src/reference/reference/functions.md +314 -0
  29. package/src/reference/reference/local.md +197 -0
  30. package/src/reference/reference/model.md +213 -0
  31. package/src/reference/reference/param.md +230 -0
  32. package/src/reference/reference/part.md +229 -0
  33. package/src/reference/reference/refinement.md +284 -0
  34. package/src/reference/reference/shapes-2d.md +309 -0
  35. package/src/reference/reference/shapes-3d.md +279 -0
  36. package/src/reference/reference/transforms.md +246 -0
  37. package/src/reference.js +160 -0
  38. package/src/server.js +219 -0
  39. package/src/tools.js +523 -0
  40. package/src/workspace.js +204 -0
@@ -0,0 +1,314 @@
1
+ # Functions
2
+
3
+ Thirty builtin functions, callable in any [expression](../language/expressions.md). They take
4
+ **positional arguments only** — named arguments are an error — and there are no user-defined
5
+ functions: to name a value declare a [`local`](local.md), and to name geometry declare a
6
+ [`component`](component.md).
7
+
8
+ **Every angle is in degrees**, in both directions: `sin` takes degrees, `atan` returns them.
9
+
10
+ | | |
11
+ | --- | --- |
12
+ | [Arithmetic](#arithmetic) | `abs` `floor` `ceil` `round` `sqrt` `sign` `pow` `log` `exp` |
13
+ | [Trigonometry](#trigonometry) | `sin` `cos` `tan` `asin` `acos` `atan` `atan2` |
14
+ | [Aggregates](#aggregates) | `min` `max` `sum` |
15
+ | [Lists](#lists) | `len` `concat` `reverse` `contains` `join` `range` |
16
+ | [Conversion](#conversion) | `str` `num` |
17
+ | [Constants](#constants) | `pi` `e` |
18
+
19
+ ---
20
+
21
+ ## Arithmetic
22
+
23
+ | Function | Arity | Description |
24
+ | --- | --- | --- |
25
+ | `abs(x)` | 1 | Magnitude, discarding the sign |
26
+ | `floor(x)` | 1 | Rounds towards −∞ |
27
+ | `ceil(x)` | 1 | Rounds towards +∞ |
28
+ | `round(x)` | 1 | Rounds to the nearest integer, halves upwards |
29
+ | `sqrt(x)` | 1 | Square root. Negative input gives NaN, which fails where it is used |
30
+ | `sign(x)` | 1 | `-1`, `0` or `1` |
31
+ | `pow(x, y)` | 2 | `x` raised to `y` |
32
+ | `log(x)` | 1 | Natural logarithm |
33
+ | `exp(x)` | 1 | `e` raised to `x` |
34
+
35
+ ```hcl
36
+ local rows = ceil(len(items) / 4)
37
+ local step = pow(2, var.doublings)
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Trigonometry
43
+
44
+ | Function | Arity | Takes | Returns |
45
+ | --- | --- | --- | --- |
46
+ | `sin(a)` | 1 | degrees | ratio |
47
+ | `cos(a)` | 1 | degrees | ratio |
48
+ | `tan(a)` | 1 | degrees | ratio |
49
+ | `asin(v)` | 1 | ratio | degrees |
50
+ | `acos(v)` | 1 | ratio | degrees |
51
+ | `atan(v)` | 1 | ratio | degrees |
52
+ | `atan2(y, x)` | 2 | two lengths | degrees, in the correct quadrant |
53
+
54
+ Degrees throughout is deliberate: every angle a model writes is in degrees, and a stray
55
+ radian conversion is invisible until the part is printed.
56
+
57
+ ```hcl
58
+ // A point on a bolt circle
59
+ local angle = i * 360 / var.bolts
60
+ local point = [var.r * cos(angle), var.r * sin(angle)]
61
+
62
+ // The angle of a slope
63
+ local rake = atan2(var.rise, var.run)
64
+ ```
65
+
66
+ Note the argument order of `atan2`: **y first**, then x.
67
+
68
+ ---
69
+
70
+ ## Aggregates
71
+
72
+ | Function | Arity | Description |
73
+ | --- | --- | --- |
74
+ | `min(...)` | variadic | The smallest value |
75
+ | `max(...)` | variadic | The largest value |
76
+ | `sum(...)` | variadic | Everything added together; `0` for no arguments |
77
+
78
+ All three **flatten their arguments**, so a list and a spread of numbers both work:
79
+
80
+ ```hcl
81
+ min(3, 7, 2) // 2
82
+ min([3, 7, 2]) // 2
83
+ max(sizes) // the largest entry
84
+ sum(thicknesses) // total stack height
85
+ ```
86
+
87
+ This is how a value gets clamped, since the language has no `clamp`:
88
+
89
+ ```hcl
90
+ local bolts = max(3, min(16, var.bolts))
91
+ ```
92
+
93
+ #### Caveats
94
+
95
+ - `min()` and `max()` with no arguments return `Infinity` and `-Infinity`, which fail with
96
+ `must be a finite number` wherever a block reads them. Guard an empty list.
97
+
98
+ ---
99
+
100
+ ## Lists
101
+
102
+ ### `len`
103
+
104
+ `len(value)` → number
105
+
106
+ The length of a list or a string. Anything else gives `0` — it does not raise.
107
+
108
+ ```hcl
109
+ len([1, 2, 3]) // 3
110
+ len("abcd") // 4
111
+ len(42) // 0
112
+ ```
113
+
114
+ ### `concat`
115
+
116
+ `concat(...)` → list
117
+
118
+ Joins lists and values into one flat list.
119
+
120
+ ```hcl
121
+ concat([1, 2], [3]) // [1, 2, 3]
122
+ concat(sizes, [10, 12]) // sizes with two more entries
123
+ ```
124
+
125
+ Flattens one level, like the aggregates.
126
+
127
+ ### `reverse`
128
+
129
+ `reverse(list)` → list
130
+
131
+ A reversed copy. The original is untouched.
132
+
133
+ ```hcl
134
+ reverse([1, 2, 3]) // [3, 2, 1]
135
+ concat(profile, reverse(profile))
136
+ ```
137
+
138
+ ### `contains`
139
+
140
+ `contains(list, value)` → bool
141
+
142
+ Whether the list holds the value. Comparison is by identity for numbers and strings, so it
143
+ does not match nested lists structurally.
144
+
145
+ ```hcl
146
+ contains(["a", "b"], var.mode)
147
+ ```
148
+
149
+ ### `join`
150
+
151
+ `join(separator, list)` → string
152
+
153
+ Joins a list into a string. **The separator comes first.**
154
+
155
+ ```hcl
156
+ join("-", ["M3", "x", "12"]) // "M3-x-12"
157
+ join(", ", sizes)
158
+ ```
159
+
160
+ ### `range`
161
+
162
+ `range(end)` · `range(start, end)` · `range(start, end, step)` → list
163
+
164
+ A list of numbers. The end is **excluded**.
165
+
166
+ ```hcl
167
+ range(4) // [0, 1, 2, 3]
168
+ range(2, 6) // [2, 3, 4, 5]
169
+ range(0, 10, 2.5) // [0, 2.5, 5, 7.5]
170
+ range(5, 0, -1) // [5, 4, 3, 2, 1]
171
+ range(0, -5) // [] — backwards with a positive step is empty, not infinite
172
+ ```
173
+
174
+ This is what drives most [`for`](../language/control-flow.md#for) loops.
175
+
176
+ #### Caveats
177
+
178
+ - **The end is excluded**, so `range(0, var.bolts)` gives exactly `bolts` values.
179
+ - A `step` of `0` is an error: `range: step cannot be 0`.
180
+ - A range running the wrong way for its step is **empty** rather than infinite, which is what
181
+ a loop written around a parameter needs when the parameter crosses zero.
182
+ - More than 100,000 values is an error: `range: more than 100000 values`. The guard exists
183
+ because in a live editor `range(0, 1e9)` is a state the document passes through while the
184
+ number is being typed.
185
+ - Floating-point accumulation applies. `range(0, 1, 0.1)` may not end where you expect;
186
+ prefer integer counts and multiply: `for i in range(0, 10) { local t = i / 10 ... }`.
187
+
188
+ ---
189
+
190
+ ## Conversion
191
+
192
+ | Function | Arity | Description |
193
+ | --- | --- | --- |
194
+ | `str(v)` | 1 | The value as a string |
195
+ | `num(v)` | 1 | The value as a number; non-numeric text gives NaN |
196
+
197
+ ```hcl
198
+ part "slot_${str(i)}" { ... } // though "${i}" already does this
199
+ local n = num(var.text_input)
200
+ ```
201
+
202
+ String interpolation already converts, so `str` is mostly useful when building a string with
203
+ `+`.
204
+
205
+ ---
206
+
207
+ ## Constants
208
+
209
+ | Name | Value |
210
+ | --- | --- |
211
+ | `pi` | 3.141592653589793 |
212
+ | `e` | 2.718281828459045 |
213
+
214
+ They are ordinary names in the root scope, so they can be shadowed by a param or a local —
215
+ avoid doing so.
216
+
217
+ ```hcl
218
+ local circumference = 2 * pi * var.radius
219
+ ```
220
+
221
+ Since every angle in the language is in degrees, `pi` comes up less than it would elsewhere —
222
+ mostly in arc lengths and areas rather than in rotations.
223
+
224
+ ---
225
+
226
+ ## Usage
227
+
228
+ ### Clamping a parameter
229
+
230
+ `min`/`max` bounds are advisory in a [`param`](param.md) block. Enforce them where it matters:
231
+
232
+ ```hcl
233
+ param bolts { type = number default = 6 min = 3 max = 16 step = 1 }
234
+
235
+ local bolts = max(3, min(16, round(var.bolts)))
236
+ ```
237
+
238
+ ### Laying out an array from a list
239
+
240
+ `len` gives the count, and the index does the rest:
241
+
242
+ ```hcl
243
+ local sizes = [1.5, 2, 2.5, 3, 4, 5, 6]
244
+
245
+ for i, size in sizes {
246
+ translate {
247
+ offset = [(i - (len(sizes) - 1) / 2) * var.spacing, 0, 0] // centred on the origin
248
+ ...
249
+ }
250
+ }
251
+ ```
252
+
253
+ ### Polar coordinates
254
+
255
+ ```hcl
256
+ for i in range(0, var.count) {
257
+ local a = i * 360 / var.count
258
+ translate {
259
+ offset = [var.r * cos(a), var.r * sin(a), 0]
260
+ cylinder { radius = 2 height = 10 }
261
+ }
262
+ }
263
+ ```
264
+
265
+ Equivalent to rotating a translated copy, and sometimes easier to read when each item also
266
+ needs its own orientation.
267
+
268
+ ### Building a label
269
+
270
+ ```hcl
271
+ param sizes = [3, 4, 5]
272
+
273
+ model "m" {
274
+ part "set_${join("_", var.sizes)}" { ... } // "set_3_4_5"
275
+ }
276
+ ```
277
+
278
+ ---
279
+
280
+ ## Troubleshooting
281
+
282
+ ### `len takes 1 argument(s), got 2`
283
+
284
+ A fixed-arity function got the wrong count. The variadic ones — `min`, `max`, `sum`,
285
+ `concat`, `range` — accept any number; the rest do not.
286
+
287
+ ### `pow: functions take positional arguments`
288
+
289
+ Named arguments are not supported: `pow(2, 10)`, never `pow(x = 2, y = 10)`.
290
+
291
+ ### `unknown function "clamp"`
292
+
293
+ There is no `clamp`, no `abs` on lists, no `map` and no `filter`. Compose what you need from
294
+ `min`/`max`, and use a [`for`](../language/control-flow.md#for) loop where you would reach for
295
+ `map`.
296
+
297
+ ### `range: step cannot be 0`
298
+
299
+ A step of zero would never terminate. If the step is computed, guard it.
300
+
301
+ ### `range: more than 100000 values`
302
+
303
+ Usually a range whose end is a mistyped parameter. The limit is a guard against a live editor
304
+ allocating until the tab dies.
305
+
306
+ ### `... must be a finite number`
307
+
308
+ Something upstream produced `NaN` or `Infinity` — `sqrt` of a negative, `min()` of nothing,
309
+ `num("abc")`. The error names the block and attribute that read it, not where it was
310
+ produced, so work backwards from there.
311
+
312
+ ### My `join` produced the arguments the wrong way round
313
+
314
+ The separator is first: `join(", ", list)`.
@@ -0,0 +1,197 @@
1
+ # `local`
2
+
3
+ A named value, computed once and reused.
4
+
5
+ ```hcl
6
+ local sizes = [1.5, 2, 2.5, 3, 4, 5, 6]
7
+ local radius = var.diameter / 2
8
+ ```
9
+
10
+ ---
11
+
12
+ ## Reference
13
+
14
+ ### `local NAME = expression`
15
+
16
+ Binds a name to the result of an expression. Locals hold values, never geometry — to name a
17
+ piece of geometry, declare a [`component`](component.md).
18
+
19
+ #### Parameters
20
+
21
+ | | |
22
+ | --- | --- |
23
+ | `NAME` | A plain identifier or a quoted string. It may not contain `${ }` |
24
+ | `expression` | Any [expression](../language/expressions.md). Evaluated once, when the local is declared |
25
+
26
+ #### Returns
27
+
28
+ Nothing. A local binds a name; it produces no geometry.
29
+
30
+ #### Caveats
31
+
32
+ - **Locals are ordered at the top level.** A local may refer to params, to constants and to
33
+ locals declared *above* it — never below. `local a = b` before `local b = 2` fails with
34
+ `unknown name "b"`.
35
+ - **Locals inside a body are not ordered.** Every local in one block body is resolved before
36
+ any of that body's blocks are built, so a block may use a local declared after it. See
37
+ [Scope → Body scopes](../language/scope.md#body-scopes).
38
+ - **Locals are not in `var`.** `var` holds params only. A local named the same as a param
39
+ shadows the bare name but leaves `var.NAME` pointing at the param.
40
+ - **A duplicate local is not an error.** A second `local a` silently replaces the first
41
+ everywhere, including in code written above it. Nothing warns, so treat it as a mistake to
42
+ avoid.
43
+ - A local is evaluated once, not per use. It cannot depend on a loop variable declared
44
+ elsewhere, because it is not inside that loop.
45
+
46
+ ---
47
+
48
+ ## Usage
49
+
50
+ ### Naming a derived dimension
51
+
52
+ The main use: give a computed number a name so the model reads in the vocabulary of the
53
+ thing being modelled rather than in arithmetic.
54
+
55
+ ```hcl
56
+ param diameter = 72
57
+ param neck_diameter = 28
58
+ param wall = 1.6
59
+
60
+ local r = var.diameter / 2
61
+ local nr = var.neck_diameter / 2
62
+ local neck_or = nr + var.wall // outer radius of the neck
63
+
64
+ model "bottle" {
65
+ cylinder { radius = neck_or height = 18 }
66
+ }
67
+ ```
68
+
69
+ Each local may use the ones above it, so a chain of derived measurements stays readable.
70
+
71
+ ### Holding a table of values
72
+
73
+ A list local is the usual way to drive a `for` loop over irregular data:
74
+
75
+ ```hcl
76
+ local sizes = [1.5, 2, 2.5, 3, 4, 5, 6]
77
+
78
+ model "hex_key_holder" {
79
+ for i, size in sizes {
80
+ translate {
81
+ offset = [(i - (len(sizes) - 1) / 2) * 8, 0, 0]
82
+ extrude { height = 20 regular_polygon { sides = 6 width_across_flats = size } }
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ A list of lists works the same way and is how a profile is written:
89
+
90
+ ```hcl
91
+ local profile = [
92
+ [0, 0], [26, 0], [24, 6], [16, 26], [0, 74],
93
+ ]
94
+
95
+ model "vase" { revolve { polygon { points = profile } } }
96
+ ```
97
+
98
+ ### Grouping related settings
99
+
100
+ An object local keeps a set of numbers that belong together from spreading out:
101
+
102
+ ```hcl
103
+ local fit = { clearance = 0.3, wall = 1.6, chamfer = 0.6 }
104
+
105
+ model "m" {
106
+ cylinder { radius = 10 + fit.clearance height = 4 }
107
+ }
108
+ ```
109
+
110
+ ### Locals inside a block
111
+
112
+ A local declared inside a body is scoped to that body, which keeps a working value out of
113
+ the document's namespace:
114
+
115
+ ```hcl
116
+ param width = 120
117
+ param wall = 4
118
+
119
+ model "frame" {
120
+ part "rail" {
121
+ local span = var.width - var.wall * 2 // only meaningful here
122
+ box { size = [span, 10, 4] }
123
+ }
124
+ }
125
+ ```
126
+
127
+ ---
128
+
129
+ ## `local` versus `param` versus `component`
130
+
131
+ | Use | When |
132
+ | --- | --- |
133
+ | [`param`](param.md) | The value is an **input** — someone outside the document should be able to change it |
134
+ | `local` | The value is **derived** from params or is a fixed table, and should never be a control |
135
+ | [`component`](component.md) | You are naming **geometry**, not a value |
136
+
137
+ A common shape is a handful of params and a run of locals that turn them into the dimensions
138
+ the geometry actually uses:
139
+
140
+ ```hcl
141
+ param body_height = 170
142
+ param wall = 1.6
143
+
144
+ local h = var.body_height
145
+ local top = h + 18
146
+ local cavity_top = top + 2 // runs past the opening so the neck is not capped
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Troubleshooting
152
+
153
+ ### `unknown name "b"`
154
+
155
+ A top-level local referred to something declared below it. Move the declaration up — locals
156
+ are resolved strictly in source order:
157
+
158
+ ```hcl
159
+ local a = b * 2 // ✗ b does not exist yet
160
+ local b = 2
161
+
162
+ local b = 2 // ✓
163
+ local a = b * 2
164
+ ```
165
+
166
+ Inside a block body the rule is relaxed, so this only bites at the top level.
167
+
168
+ ### `unknown name "outer-inner"`
169
+
170
+ Hyphens are part of an identifier, so `outer-inner` is one name rather than a subtraction.
171
+ Put spaces around the operator:
172
+
173
+ ```hcl
174
+ local gap = outer - inner
175
+ ```
176
+
177
+ ### `no attribute "a"` when reading `var.a`
178
+
179
+ `var` holds params only. A local is reached by its bare name:
180
+
181
+ ```hcl
182
+ local a = 2
183
+ box { size = [a, 1, 1] } // ✓
184
+ box { size = [var.a, 1, 1] } // ✗ no attribute "a"
185
+ ```
186
+
187
+ ### My local is not picking up the loop variable
188
+
189
+ A local is evaluated where it is written, once. It cannot see a loop variable from a loop it
190
+ is not inside. Move the local inside the loop body, or compute the value inline:
191
+
192
+ ```hcl
193
+ for i in range(0, 6) {
194
+ local angle = i * 60 // ✓ inside the loop, re-evaluated each iteration
195
+ rotate { z = angle ... }
196
+ }
197
+ ```
@@ -0,0 +1,213 @@
1
+ # `model`
2
+
3
+ Something to render. A document may declare several; [`render`](../api/render.md) builds one
4
+ of them.
5
+
6
+ ```hcl
7
+ model "hex_key_holder" {
8
+ part "body" {
9
+ color = "#6f7d8c"
10
+ extrude { height = 20 rounded_rect { size = [62, 13] radius = 6.5 center = true } }
11
+ }
12
+ }
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Reference
18
+
19
+ ### `model "NAME" { ... }`
20
+
21
+ Declares a renderable scene. The body is geometry, optionally organised into
22
+ [`part`](part.md)s.
23
+
24
+ #### Body
25
+
26
+ | Entry | Meaning |
27
+ | --- | --- |
28
+ | [`part "name" { ... }`](part.md) | A separately coloured piece of the scene |
29
+ | geometry blocks | Rendered as one default part named after the model |
30
+ | `param NAME { default = expr }` | A constant for this model. Must have a default; not settable from outside |
31
+ | `local NAME = expr` | A working value, scoped to the model |
32
+ | `for` / `if` | Ordinary control flow, which may produce parts as well as geometry |
33
+
34
+ #### Returns
35
+
36
+ A scene: a list of parts, each with a name, a colour, an opacity and a geometry node.
37
+
38
+ #### Caveats
39
+
40
+ - **Every part must be 3D.** A 2D part is rejected before it is solved:
41
+ `part "body" is 2D — extrude or revolve it before rendering`.
42
+ - **A model must produce something.** An empty body, or one whose only loop ran zero times,
43
+ fails with `model "m" produced no geometry`.
44
+ - **Declaring the same model twice is an error**: `model "m" is declared twice`.
45
+ - **Model params are not inputs.** They must have defaults and cannot be set by the caller.
46
+ See [Model params](#model-params).
47
+ - A document with no model at all fails at render time with `the document declares no model`.
48
+
49
+ ---
50
+
51
+ ## Usage
52
+
53
+ ### Choosing which model renders
54
+
55
+ With no name, the **first** model in the document is built:
56
+
57
+ ```js
58
+ await render(source); // the first model
59
+ await render(source, { model: 'exploded' }); // by name
60
+ ```
61
+
62
+ Naming one that does not exist fails with `no model named "exploded"`. In the editor, the
63
+ first model is the one shown.
64
+
65
+ Several models in one document is the idiomatic way to keep variants of the same object
66
+ together — a printable part and the assembly it belongs to, say:
67
+
68
+ ```hcl
69
+ component "bracket" { ... }
70
+ component "pin" { ... }
71
+
72
+ model "printable" { // what goes to the slicer
73
+ part "bracket" { bracket { } }
74
+ }
75
+
76
+ model "assembly" { // what it looks like assembled
77
+ part "bracket" { color = "#6f7d8c" bracket { } }
78
+ part "pin" { color = "#c8842f" translate { offset = [0, 0, 12] pin { } } }
79
+ }
80
+ ```
81
+
82
+ ### Parts, and geometry without them
83
+
84
+ Geometry written straight into a model — outside any `part` — becomes **one default part
85
+ named after the model**, coloured `#b8c4d0`:
86
+
87
+ ```hcl
88
+ model "riser" {
89
+ box { size = [40, 20, 10] } // → one part called "riser"
90
+ }
91
+ ```
92
+
93
+ Mixing the two is allowed. The loose geometry still collapses into a single part, which is
94
+ listed after the declared ones:
95
+
96
+ ```hcl
97
+ model "m" {
98
+ box { size = [10, 10, 10] } // → part "m"
99
+ part "pin" { cylinder { radius = 2 height = 20 } }
100
+ }
101
+ // parts: "pin", then "m"
102
+ ```
103
+
104
+ Prefer explicit parts once there is more than one piece — they are what the viewer colours
105
+ and lists, and what an exporter can write out separately.
106
+
107
+ ### Model params
108
+
109
+ A `model` may declare `param` blocks. They are **constants for that model**, not inputs:
110
+
111
+ ```hcl
112
+ model "bracket" {
113
+ param thickness { default = 4 }
114
+ param slots { default = 3 }
115
+
116
+ extrude { height = thickness rect { size = [20, slots * 10] } }
117
+ }
118
+ ```
119
+
120
+ | | Document `param` | Model `param` |
121
+ | --- | --- | --- |
122
+ | Default required | No — may be required instead | **Yes**: `model "m": param "s" needs a default` |
123
+ | Settable by the caller | Yes, via `render(source, { params })` | No |
124
+ | Appears in `describeParameters` | Yes | No |
125
+ | Readable as `var.NAME` | Yes | No — bare name only |
126
+
127
+ Use them to give one model's fixed numbers a name without adding a control to the editor.
128
+ For anything the caller should change, declare the param at the top level.
129
+
130
+ ### Building parts in a loop
131
+
132
+ `for` and `if` may produce parts, not just geometry, so a scene can be generated:
133
+
134
+ ```hcl
135
+ model "stacked_trays" {
136
+ for i in range(0, var.rows) {
137
+ part "tray_${i}" {
138
+ color = i % 2 == 0 ? "#3f7fbf" : "#bf6b3f"
139
+ translate { offset = [0, 0, i * 13] tray { size = [40, 30, 12] wall = 2 } }
140
+ }
141
+ }
142
+ }
143
+ ```
144
+
145
+ An optional part goes behind an `if` **around** the part, never inside it — a part with no
146
+ geometry is an error:
147
+
148
+ ```hcl
149
+ if var.show_label {
150
+ part "label" { color = "#f4f1e4" ... }
151
+ }
152
+ ```
153
+
154
+ ### Exploded views
155
+
156
+ A param that displaces one part is the cheapest way to make an assembly inspectable, and
157
+ costs nothing when it is zero:
158
+
159
+ ```hcl
160
+ param lift { type = number default = 0 min = 0 max = 90 }
161
+
162
+ model "assembly" {
163
+ part "body" { body { } }
164
+ part "cap" { translate { offset = [0, 0, 170 + var.lift] cap { } } }
165
+ }
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Troubleshooting
171
+
172
+ ### `model "m" produced no geometry`
173
+
174
+ The body evaluated to nothing. Usual causes:
175
+
176
+ - an empty body;
177
+ - a `for` over an empty list — `range(0, var.n)` where `n` is `0`;
178
+ - an `if` with no `else` whose condition was false.
179
+
180
+ Guard the degenerate case so something is always produced, or make the parameter's minimum
181
+ exclude it.
182
+
183
+ ### `part "body" is 2D — extrude or revolve it before rendering`
184
+
185
+ A part ended up holding a cross-section rather than a solid. 2D shapes cannot be rendered
186
+ directly:
187
+
188
+ ```hcl
189
+ part "body" { circle { radius = 5 } } // ✗
190
+ part "body" { extrude { height = 2 circle { radius = 5 } } } // ✓
191
+ ```
192
+
193
+ See [`extrude`](conversions.md#extrude) and [`revolve`](conversions.md#revolve).
194
+
195
+ ### `the document declares no model`
196
+
197
+ The document has params, locals and components but nothing to render. Add a `model` block.
198
+
199
+ ### `model "m": param "s" needs a default`
200
+
201
+ A param declared inside a model has no default. Model params cannot be supplied from
202
+ outside, so a default is the only way they can ever have a value. Either give it one, or move
203
+ the param to the top level where the caller can set it.
204
+
205
+ ### `model "m" is declared twice`
206
+
207
+ Two models share a name, so `render(source, { model: 'm' })` would be ambiguous. Rename one.
208
+
209
+ ### My `render(source, { params })` value is ignored
210
+
211
+ The param is probably declared inside the model rather than at the top level. Only top-level
212
+ params are settable. Check with [`describeParameters`](../api/describe-parameters.md) — it
213
+ lists exactly the params a caller can set.