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.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/package.json +52 -0
- package/src/bin.js +30 -0
- package/src/catalogue.js +703 -0
- package/src/document.js +285 -0
- package/src/examples.js +212 -0
- package/src/guide.js +313 -0
- package/src/reference/README.md +153 -0
- package/src/reference/api/describe-parameters.md +167 -0
- package/src/reference/api/evaluation-context.md +221 -0
- package/src/reference/api/export.md +185 -0
- package/src/reference/api/geometry-node.md +215 -0
- package/src/reference/api/kernel.md +182 -0
- package/src/reference/api/low-level.md +239 -0
- package/src/reference/api/render.md +217 -0
- package/src/reference/api/values.md +223 -0
- package/src/reference/errors.md +253 -0
- package/src/reference/language/control-flow.md +160 -0
- package/src/reference/language/expressions.md +206 -0
- package/src/reference/language/overview.md +156 -0
- package/src/reference/language/scope.md +184 -0
- package/src/reference/language/syntax.md +210 -0
- package/src/reference/reference/align.md +189 -0
- package/src/reference/reference/booleans.md +231 -0
- package/src/reference/reference/component.md +248 -0
- package/src/reference/reference/conversions.md +269 -0
- package/src/reference/reference/functions.md +314 -0
- package/src/reference/reference/local.md +197 -0
- package/src/reference/reference/model.md +213 -0
- package/src/reference/reference/param.md +230 -0
- package/src/reference/reference/part.md +229 -0
- package/src/reference/reference/refinement.md +284 -0
- package/src/reference/reference/shapes-2d.md +309 -0
- package/src/reference/reference/shapes-3d.md +279 -0
- package/src/reference/reference/transforms.md +246 -0
- package/src/reference.js +160 -0
- package/src/server.js +219 -0
- package/src/tools.js +523 -0
- 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.
|