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,223 @@
|
|
|
1
|
+
# Values
|
|
2
|
+
|
|
3
|
+
Three small immutable classes the evaluator works in: [`Vector`](#vector),
|
|
4
|
+
[`Angle`](#angle) and [`Transform`](#transform). Documents never see them directly — a
|
|
5
|
+
document writes lists and bare numbers — but they are exported for anyone building geometry
|
|
6
|
+
through the [`GeometryNode`](geometry-node.md) API.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import { Vector, Angle, Transform } from 'forma-dsl';
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
All three are frozen on construction. Every method returns a new value.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## `Vector`
|
|
17
|
+
|
|
18
|
+
A 2D or 3D vector. One class covers both, because every operation is componentwise and a
|
|
19
|
+
separate 3D class would be the same code with one more letter.
|
|
20
|
+
|
|
21
|
+
### Construction
|
|
22
|
+
|
|
23
|
+
| | |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `new Vector([x, y])` | From components |
|
|
26
|
+
| `Vector.of(value, dimensions)` | From anything vector-like |
|
|
27
|
+
| `Vector.zero(dimensions)` | The origin |
|
|
28
|
+
|
|
29
|
+
`Vector.of` accepts a `Vector`, a **number** — broadcast to every component — an array, or an
|
|
30
|
+
`{x, y, z}` object whose missing keys read as zero. This is what makes
|
|
31
|
+
`scale { factor = 2 }` work the same as `factor = [2, 2, 2]`.
|
|
32
|
+
|
|
33
|
+
### Properties
|
|
34
|
+
|
|
35
|
+
| Property | Description |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `components` | The components, frozen |
|
|
38
|
+
| `size` | How many there are |
|
|
39
|
+
| `x`, `y`, `z` | The first, second and third. `z` is `undefined` in 2D |
|
|
40
|
+
| `magnitude` | Length |
|
|
41
|
+
|
|
42
|
+
### Methods
|
|
43
|
+
|
|
44
|
+
| Method | Description |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `plus(other)` `minus(other)` | Componentwise |
|
|
47
|
+
| `times(other)` `dividedBy(other)` | Componentwise; a number scales every component |
|
|
48
|
+
| `negated()` | The other way round |
|
|
49
|
+
| `dot(other)` | Dot product |
|
|
50
|
+
| `cross(other)` | Cross product. 3D only |
|
|
51
|
+
| `normalized()` | Scaled to unit length |
|
|
52
|
+
| `toArray()` | A copy, safe to keep |
|
|
53
|
+
|
|
54
|
+
#### Caveats
|
|
55
|
+
|
|
56
|
+
- **A zero vector normalises to itself**, not to NaN, so a degenerate normal cannot quietly
|
|
57
|
+
poison a transform downstream.
|
|
58
|
+
- `Vector.of` throws if the size does not match what was asked for.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## `Angle`
|
|
63
|
+
|
|
64
|
+
An angle, stored in radians and read in whichever unit the caller wants.
|
|
65
|
+
|
|
66
|
+
The kernel takes degrees and JavaScript's trigonometry takes radians, so a bare number would
|
|
67
|
+
be ambiguous at exactly the call sites where getting it wrong produces a model that is subtly
|
|
68
|
+
wrong rather than an error.
|
|
69
|
+
|
|
70
|
+
**Every angle the language reads or writes is in degrees.** Radians appear only here.
|
|
71
|
+
|
|
72
|
+
### Construction
|
|
73
|
+
|
|
74
|
+
| | |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `Angle.degrees(v)` | From degrees |
|
|
77
|
+
| `Angle.radians(v)` | From radians |
|
|
78
|
+
| `Angle.turns(v)` | From full turns, so `0.25` is a right angle |
|
|
79
|
+
| `Angle.of(v)` | From an `Angle`, or a **number read as degrees** |
|
|
80
|
+
| `new Angle(radians)` | Rarely the unit at hand; prefer the factories |
|
|
81
|
+
|
|
82
|
+
### Properties and methods
|
|
83
|
+
|
|
84
|
+
| | Description |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `radians` | The angle in radians |
|
|
87
|
+
| `degrees` | The angle in degrees |
|
|
88
|
+
| `sin` `cos` `tan` | Trigonometry, as properties |
|
|
89
|
+
| `plus(other)` `minus(other)` | Other may be an `Angle` or a number of degrees |
|
|
90
|
+
| `times(factor)` | A plain multiplier, not an angle |
|
|
91
|
+
|
|
92
|
+
The unit names appear twice — `Angle.degrees(90)` builds one, `angle.degrees` reads one back.
|
|
93
|
+
They are different namespaces and do not collide.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## `Transform`
|
|
98
|
+
|
|
99
|
+
An affine transform, held column-major at the size the kernel wants: 3×3 for 2D and 4×4 for
|
|
100
|
+
3D.
|
|
101
|
+
|
|
102
|
+
Column-major throughout, because that is what Manifold reads. Storing row-major and
|
|
103
|
+
transposing at the boundary would put a silent transpose in the one place a wrong model still
|
|
104
|
+
renders.
|
|
105
|
+
|
|
106
|
+
### Construction
|
|
107
|
+
|
|
108
|
+
| | |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| `Transform.identity(dim)` | Changes nothing |
|
|
111
|
+
| `Transform.translation(offset, dim)` | Moves |
|
|
112
|
+
| `Transform.scaling(factor, dim)` | Scales about the origin |
|
|
113
|
+
| `Transform.rotation2D(angle)` | Rotates about the origin. A bare number is degrees |
|
|
114
|
+
| `Transform.rotation3D(angles)` | Degrees about X, Y and Z |
|
|
115
|
+
| `Transform.rotation(angles, dim)` | Dispatches to whichever of the two fits |
|
|
116
|
+
| `Transform.mirroring(normal, dim)` | Reflects in the plane through the origin |
|
|
117
|
+
|
|
118
|
+
### Properties and methods
|
|
119
|
+
|
|
120
|
+
| | Description |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| `dim` | 2 or 3 |
|
|
123
|
+
| `m` | The matrix, column-major and frozen |
|
|
124
|
+
| `order` | Side length — one more than `dim` |
|
|
125
|
+
| `at(row, col)` | One entry, read out of column-major storage |
|
|
126
|
+
| `concat(other)` | `this ∘ other` — **`other` runs first** |
|
|
127
|
+
| `toArray()` | A copy, safe to keep |
|
|
128
|
+
|
|
129
|
+
#### Caveats
|
|
130
|
+
|
|
131
|
+
- **3D rotation applies X, then Y, then Z**, matching OpenSCAD, so a model ported from there
|
|
132
|
+
lands in the same orientation.
|
|
133
|
+
- **`concat` applies its argument first.** That order matches how blocks nest: the transform
|
|
134
|
+
closest to the shape is applied to it first.
|
|
135
|
+
- `mirroring` normalises the normal, so its length does not change the result.
|
|
136
|
+
- Every transform is about the **origin**. To act about some other point, translate there,
|
|
137
|
+
act, and translate back.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Usage
|
|
142
|
+
|
|
143
|
+
### Composing a placement
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
import { Transform, GeometryNode } from 'forma-dsl';
|
|
147
|
+
|
|
148
|
+
// Rotate, then move — the argument to concat runs first.
|
|
149
|
+
const placement = Transform.translation([30, 0, 0], 3)
|
|
150
|
+
.concat(Transform.rotation3D([0, 0, 45]));
|
|
151
|
+
|
|
152
|
+
const placed = GeometryNode.transformed(shape, placement);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Reading it as blocks, that is:
|
|
156
|
+
|
|
157
|
+
```hcl
|
|
158
|
+
translate { offset = [30, 0, 0] rotate { z = 45 shape { } } }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Rotating about a point
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
const about = (point, rotation) => Transform.translation(point, 3)
|
|
165
|
+
.concat(rotation)
|
|
166
|
+
.concat(Transform.translation(new Vector(point).negated().toArray(), 3));
|
|
167
|
+
|
|
168
|
+
const spun = GeometryNode.transformed(shape, about([20, 20, 0], Transform.rotation3D([0, 0, 30])));
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Working in degrees
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
import { Angle } from 'forma-dsl';
|
|
175
|
+
|
|
176
|
+
const a = Angle.degrees(30);
|
|
177
|
+
a.sin; // 0.5
|
|
178
|
+
a.radians; // 0.5235987755982988
|
|
179
|
+
a.plus(60).degrees; // 90 — a bare number is read as degrees
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Vector arithmetic
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
import { Vector } from 'forma-dsl';
|
|
186
|
+
|
|
187
|
+
const size = new Vector([40, 30, 12]);
|
|
188
|
+
const wall = 2;
|
|
189
|
+
|
|
190
|
+
const inner = size.minus(new Vector([wall * 2, wall * 2, 0]));
|
|
191
|
+
inner.toArray(); // [36, 26, 12]
|
|
192
|
+
|
|
193
|
+
Vector.of(3, 3).toArray(); // [3, 3, 3] — a scalar broadcasts
|
|
194
|
+
Vector.of({ x: 1, z: 5 }, 3).toArray(); // [1, 0, 5] — missing keys are zero
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Troubleshooting
|
|
200
|
+
|
|
201
|
+
### `expected 3 components, got 2`
|
|
202
|
+
|
|
203
|
+
`Vector.of` was asked for a dimensionality the value does not have. In a document this
|
|
204
|
+
surfaces more helpfully as `translate: "offset" must be a number or an array of 3 numbers`.
|
|
205
|
+
|
|
206
|
+
### `cannot read an angle from string`
|
|
207
|
+
|
|
208
|
+
`Angle.of` takes an `Angle` or a number. A string is not coerced — that would let `"90deg"`
|
|
209
|
+
silently become `NaN`.
|
|
210
|
+
|
|
211
|
+
### My composed transform is applied in the wrong order
|
|
212
|
+
|
|
213
|
+
`concat` runs its **argument** first. `a.concat(b)` means "do `b`, then `a`", matching how
|
|
214
|
+
the equivalent nested blocks read.
|
|
215
|
+
|
|
216
|
+
### My rotation moved the shape instead of turning it
|
|
217
|
+
|
|
218
|
+
Every transform is about the origin. Centre the geometry, rotate, then translate.
|
|
219
|
+
|
|
220
|
+
### Non-uniform scaling made my circle an ellipse
|
|
221
|
+
|
|
222
|
+
Expected — that is how [`ellipse`](../reference/shapes-2d.md#ellipse) is built internally, and
|
|
223
|
+
it is the cheapest route to an ellipsoid from a sphere.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Errors
|
|
2
|
+
|
|
3
|
+
Every error the language raises, grouped by the stage that raises it. Knowing the stage
|
|
4
|
+
narrows the cause: a lexer error is a typo, an evaluator error is a mistake about meaning.
|
|
5
|
+
|
|
6
|
+
All of them carry a source position where one is known, appended to the message and available
|
|
7
|
+
as `error.loc`:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
try {
|
|
11
|
+
await render(source, { context });
|
|
12
|
+
} catch (error) {
|
|
13
|
+
error.loc?.line; // undefined when the error has no position
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Most are a [`FormaError`](api/low-level.md#formaerror). A few come from the kernel or from
|
|
18
|
+
`render` itself and are plain `Error`s, so test `error.loc` rather than the class.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Lexer
|
|
23
|
+
|
|
24
|
+
Raised while turning characters into tokens. Always a typo.
|
|
25
|
+
|
|
26
|
+
| Message | Cause |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `unexpected character "…"` | A character that cannot begin a token — a smart quote, a stray backtick, a non-ASCII dash |
|
|
29
|
+
| `unterminated string` | A `"` with no closing `"`, or a literal newline inside one |
|
|
30
|
+
| `unterminated block comment` | A `/*` with no `*/`. Block comments do not nest |
|
|
31
|
+
| `unknown escape \q` | Only `\n` `\t` `\r` `\"` `\\` `\$` exist |
|
|
32
|
+
| `unterminated ${ } in string` | A splice with no closing brace |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Parser
|
|
37
|
+
|
|
38
|
+
Raised while turning tokens into a tree. Always a grammar mistake.
|
|
39
|
+
|
|
40
|
+
| Message | Cause |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `expected a block, found "…"` | Something at the top level that is not a block |
|
|
43
|
+
| `unknown top-level block "shape"` | Only `param`, `local`, `component` and `model` may appear at the top level |
|
|
44
|
+
| `expected an attribute or block, found "…"` | Inside a body, an entry that does not start with a name |
|
|
45
|
+
| `expected {, found "…"` | A `param` with no `=` and no body — write `param s { }`. Also a `component` or `model` with no body |
|
|
46
|
+
| `expected =, found "…"` | A `local` with no value |
|
|
47
|
+
| `expected ident, found "…"` | A keyword where a name was required |
|
|
48
|
+
| `"for" cannot be used as a block type` | One of the eleven [keywords](language/syntax.md#keywords) used as a block name |
|
|
49
|
+
| `a param block holds attributes, not nested blocks` | Usually a missing `=`: `param h { default 20 }` |
|
|
50
|
+
| `a label cannot contain ${ }` | `component` and `model` names must be static. [`part`](reference/part.md) labels may be expressions |
|
|
51
|
+
| `expected = or : in an object` | An object literal entry with no separator |
|
|
52
|
+
| `unexpected "…"` | A token that cannot begin an expression |
|
|
53
|
+
| `trailing input in ${ }` | A splice holding more than one expression |
|
|
54
|
+
|
|
55
|
+
### `expected {, found "box"`
|
|
56
|
+
|
|
57
|
+
The most common parser error, and the least obvious. A component param needs `=` or a body,
|
|
58
|
+
even an empty one:
|
|
59
|
+
|
|
60
|
+
```hcl
|
|
61
|
+
component "c" {
|
|
62
|
+
param s // ✗ the parser is looking for `=` or `{`
|
|
63
|
+
box { size = [s, s, s] }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
component "c" {
|
|
67
|
+
param s { } // ✓ declared, and required at the call site
|
|
68
|
+
box { size = [s, s, s] }
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Declarations
|
|
75
|
+
|
|
76
|
+
Raised when the parsed document is indexed.
|
|
77
|
+
|
|
78
|
+
| Message | Cause |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `param "a" is declared twice` | Two top-level params share a name |
|
|
81
|
+
| `component "c" is declared twice` | Two components share a name |
|
|
82
|
+
| `model "m" is declared twice` | Two models share a name |
|
|
83
|
+
| `component "box" shadows a builtin block` | Components and builtins share one namespace |
|
|
84
|
+
|
|
85
|
+
Locals are the exception: a duplicate `local` is accepted and silently replaces the first.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Names and expressions
|
|
90
|
+
|
|
91
|
+
Raised while evaluating.
|
|
92
|
+
|
|
93
|
+
| Message | Cause |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `unknown name "x"` | Nothing in scope binds it. See [Scope](language/scope.md) |
|
|
96
|
+
| `no attribute "x"` | A missing key on an object — including `var.x` for something that is not a param |
|
|
97
|
+
| `cannot read "x" of nothing` | A property read on `null` |
|
|
98
|
+
| `cannot index this value` | Indexing something that is not a list, string or object |
|
|
99
|
+
| `cannot negate a non-number` | Unary `-` on a string, list or bool |
|
|
100
|
+
| `cannot apply + to a list and a number` | Operands the operator does not accept. See the [operator table](language/expressions.md#arithmetic) |
|
|
101
|
+
| `cannot compare a number with a string` | `<` `<=` `>` `>=` require both sides to be the same type |
|
|
102
|
+
| `cannot combine vectors of length 2 and 3` | Componentwise arithmetic on lists of different lengths |
|
|
103
|
+
| `division by zero` · `modulo by zero` | Errors rather than `Infinity` or `NaN`, so a degenerate value cannot reach the kernel |
|
|
104
|
+
| `unknown function "clamp"` | No such builtin, and there are no user-defined functions |
|
|
105
|
+
| `len takes 1 argument(s), got 2` | Wrong argument count for a fixed-arity function |
|
|
106
|
+
| `pow: functions take positional arguments` | Named arguments are not supported |
|
|
107
|
+
| `range: step cannot be 0` | A zero step would never terminate |
|
|
108
|
+
| `range: more than 100000 values` | A guard against a live editor allocating until the tab dies |
|
|
109
|
+
|
|
110
|
+
### `unknown name "outer-inner"`
|
|
111
|
+
|
|
112
|
+
Hyphens are part of an identifier, so `outer-inner` is one name. Put spaces around the
|
|
113
|
+
operator: `outer - inner`. See [Identifiers](language/syntax.md#identifiers).
|
|
114
|
+
|
|
115
|
+
### `unknown name "var"`
|
|
116
|
+
|
|
117
|
+
A param default referred to `var`, which is assembled only after every param has resolved.
|
|
118
|
+
Use the earlier param's bare name instead — see
|
|
119
|
+
[the root scope](language/scope.md#the-root-scope).
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Params
|
|
124
|
+
|
|
125
|
+
| Message | Cause |
|
|
126
|
+
| --- | --- |
|
|
127
|
+
| `param "h" has no default and no value was supplied` | A required param with no value. Pass one, or give it a default |
|
|
128
|
+
| `model "m": param "s" needs a default` | Model params cannot be set from outside, so a default is the only way they get a value |
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Blocks
|
|
133
|
+
|
|
134
|
+
Raised while building geometry.
|
|
135
|
+
|
|
136
|
+
| Message | Cause |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| `unknown block "bloop"` | Not a builtin and not a declared component. Check the spelling against the [block list](README.md#shapes) |
|
|
139
|
+
| `"box" does not take a label` | Only [`part`](reference/part.md) takes a meaningful label |
|
|
140
|
+
| `"box" is a shape and cannot contain other blocks` | A shape is a leaf. Usually a missing `}` swallowed the next block |
|
|
141
|
+
| `"translate" needs at least one shape inside it` | An operation with nothing to act on |
|
|
142
|
+
| `box: unknown attribute "colour"` | Every block rejects attributes it did not read, so a typo fails loudly |
|
|
143
|
+
| `attribute "size" is set twice` | The same attribute written twice in one block |
|
|
144
|
+
| `box: "size" must be a number or an array of 3 numbers` | Wrong shape of value |
|
|
145
|
+
| `circle: "radius" must be a finite number` | A `NaN` or `Infinity` reached the block |
|
|
146
|
+
| `regular_polygon: "sides" must be at least 3` | |
|
|
147
|
+
| `polygon: "points" must be an array of at least 3 points` | |
|
|
148
|
+
| `circle: give either "radius" or "diameter", not both` | They mean the same thing |
|
|
149
|
+
| `cylinder: give "radius"/"diameter", or both "bottom_radius" and "top_radius"` | One end radius on its own is ambiguous |
|
|
150
|
+
| `offset: "join" must be one of "Square", "Round", "Miter"` | An enum attribute with an unrecognised value |
|
|
151
|
+
|
|
152
|
+
### `box: unknown attribute "colour"`
|
|
153
|
+
|
|
154
|
+
Worth dwelling on, because it is the error that saves the most time. A misspelled attribute
|
|
155
|
+
would otherwise be silent, and the model would render subtly wrong with nothing to point at.
|
|
156
|
+
The one exception is a [`param`](reference/param.md) block, which is metadata and passes
|
|
157
|
+
anything through.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Dimensionality
|
|
162
|
+
|
|
163
|
+
| Message | Cause |
|
|
164
|
+
| --- | --- |
|
|
165
|
+
| `"union" cannot mix 2D and 3D shapes` | Children of different dimensionality in one block |
|
|
166
|
+
| `"extrude" takes 2D geometry, got 3D` | A block that fixes what it takes got the other thing |
|
|
167
|
+
| `part "body" is 2D — extrude or revolve it before rendering` | A cross-section reached the renderer |
|
|
168
|
+
| `cannot combine 2D and 3D geometry in a union` | The same mistake, from the [node API](api/geometry-node.md) rather than a document |
|
|
169
|
+
|
|
170
|
+
See [Dimensionality](language/overview.md#dimensionality) for the three rules, and
|
|
171
|
+
[2D ↔ 3D](reference/conversions.md) for the way across.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Components
|
|
176
|
+
|
|
177
|
+
| Message | Cause |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| `c: "s" has no default and was not given` | A required component param the call site did not pass |
|
|
180
|
+
| `c: unknown param "t"` | Components have no pass-through: every value must be declared |
|
|
181
|
+
| `component "c" produced no geometry` | An empty body, a loop over an empty list, or an `if` that took no branch |
|
|
182
|
+
| `a component cannot declare parts` | Scene structure belongs in the model |
|
|
183
|
+
| `nesting deeper than 64 blocks — is a component using itself?` | Direct or mutual recursion. Rewrite as a `for` loop |
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Parts and scenes
|
|
188
|
+
|
|
189
|
+
| Message | Cause |
|
|
190
|
+
| --- | --- |
|
|
191
|
+
| `a part cannot contain another part` | Parts are a flat list |
|
|
192
|
+
| `a part needs at least one shape inside it` | Put the `if` around the part, not inside it |
|
|
193
|
+
| `model "m" produced no geometry` | Nothing was produced. Often a `for` over an empty range |
|
|
194
|
+
| `the document declares no model` | Add a `model` block |
|
|
195
|
+
| `no model named "x"` | `render(source, { model })` named one that does not exist |
|
|
196
|
+
|
|
197
|
+
A [`part`](reference/part.md) nested inside an operation raises **nothing** — it is silently
|
|
198
|
+
dropped. If a part is missing from a render, check it is written directly in the model.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Control flow
|
|
203
|
+
|
|
204
|
+
| Message | Cause |
|
|
205
|
+
| --- | --- |
|
|
206
|
+
| `for: expected a list to iterate` | The sequence is not a list. Strings are not iterable here |
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## `align`
|
|
211
|
+
|
|
212
|
+
| Message | Cause |
|
|
213
|
+
| --- | --- |
|
|
214
|
+
| `align needs a kernel context — render the model rather than building it` | An `Evaluator` built with no context. [`render`](api/render.md) always supplies one |
|
|
215
|
+
| `align needs at least one shape inside it` | Nothing to measure |
|
|
216
|
+
| `align: "x" must be "min", "center", "max" or a number` | The spelling is `"center"` |
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Runtime
|
|
221
|
+
|
|
222
|
+
Raised by the JavaScript API rather than by a document.
|
|
223
|
+
|
|
224
|
+
| Message | Cause |
|
|
225
|
+
| --- | --- |
|
|
226
|
+
| `EvaluationContext has been disposed` | A solid or a render used a context after `dispose()` |
|
|
227
|
+
| `EvaluationContext used before create() resolved` | The constructor was called directly. Use `EvaluationContext.create()` |
|
|
228
|
+
| `bad dimensionality 4` | `new GeometryNode` with something other than 2 or 3 |
|
|
229
|
+
| `expected 3 components, got 2` | `Vector.of` asked for a size the value does not have |
|
|
230
|
+
| `cannot read an angle from string` | `Angle.of` takes an `Angle` or a number |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Silent behaviours
|
|
235
|
+
|
|
236
|
+
Not errors, and each is deliberate — but each has surprised someone. A parameter reaching the
|
|
237
|
+
end of its range should not break a model, so these degenerate quietly:
|
|
238
|
+
|
|
239
|
+
| What | Result |
|
|
240
|
+
| --- | --- |
|
|
241
|
+
| `extrude { height = 0 }` | Empty geometry |
|
|
242
|
+
| `revolve { angle = 0 }` | Empty geometry |
|
|
243
|
+
| A `difference` that removes everything | Empty geometry |
|
|
244
|
+
| A negative `offset` larger than the shape | Empty geometry |
|
|
245
|
+
| `range` running backwards for its step | An empty list |
|
|
246
|
+
| A label on `align` or a component call | Ignored |
|
|
247
|
+
| A supplied param the document does not declare | Ignored |
|
|
248
|
+
| `min`, `max`, `step`, `options` on a param | Advisory; never enforced |
|
|
249
|
+
| A duplicate `local` | Replaces the first, everywhere |
|
|
250
|
+
| A `part` inside an operation | Dropped from the scene |
|
|
251
|
+
| An index past the end of a list | Nothing, which fails later where it is used |
|
|
252
|
+
|
|
253
|
+
If a model renders but is not what you meant, this table is the place to start.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Control flow
|
|
2
|
+
|
|
3
|
+
Two constructs, both of which appear where a block appears and produce geometry rather than
|
|
4
|
+
a value. Neither is an expression — for a value that depends on a condition, use
|
|
5
|
+
[`cond ? a : b`](expressions.md#conditional).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## `for`
|
|
10
|
+
|
|
11
|
+
Repeats a body once per item in a list.
|
|
12
|
+
|
|
13
|
+
```hcl
|
|
14
|
+
for i in range(0, 6) { ... } // one name: the value
|
|
15
|
+
for i, item in list { ... } // two names: the index, then the value
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Everything every iteration produces is collected and handed to the enclosing block, so a
|
|
19
|
+
loop inside an operation contributes several children to it:
|
|
20
|
+
|
|
21
|
+
```hcl
|
|
22
|
+
difference {
|
|
23
|
+
extrude { height = 20 rounded_rect { size = [62, 13] radius = 6.5 center = true } }
|
|
24
|
+
|
|
25
|
+
for i, size in sizes { // each iteration adds one more thing to subtract
|
|
26
|
+
translate {
|
|
27
|
+
offset = [(i - (len(sizes) - 1) / 2) * 8, 0, 2.5]
|
|
28
|
+
extrude { height = 20 regular_polygon { sides = 6 width_across_flats = size } }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Reference
|
|
35
|
+
|
|
36
|
+
| | |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| **Sequence** | Any expression that evaluates to a list. Iterating anything else is an error: `for: expected a list to iterate` |
|
|
39
|
+
| **Names** | One name binds the value. Two names bind the index and then the value |
|
|
40
|
+
| **Scope** | Each iteration gets a fresh child scope holding just those names |
|
|
41
|
+
| **Result** | The concatenation of what every iteration produced — geometry, and any `part`s |
|
|
42
|
+
|
|
43
|
+
#### Caveats
|
|
44
|
+
|
|
45
|
+
- A loop over an empty list produces nothing. If that is all a model contains, the model
|
|
46
|
+
fails with `model "m" produced no geometry`. Guard the empty case with
|
|
47
|
+
[`if`](#if) when a parameter can legitimately reach zero.
|
|
48
|
+
- `range(a, b)` **excludes** `b`, and produces an empty list rather than looping forever when
|
|
49
|
+
the range runs backwards. See [`range`](../reference/functions.md#range).
|
|
50
|
+
- Iterating a string does not work — strings are not lists here. Use
|
|
51
|
+
[`len`](../reference/functions.md#len) and index into it if you need the characters.
|
|
52
|
+
- There is no `break`, no `continue` and no accumulator. A loop that needs to skip items
|
|
53
|
+
should filter the list it iterates, or wrap its body in an `if`.
|
|
54
|
+
|
|
55
|
+
### Building parts in a loop
|
|
56
|
+
|
|
57
|
+
A [`part`](../reference/part.md) inside a loop needs a distinct name, and the loop variable
|
|
58
|
+
is how it gets one:
|
|
59
|
+
|
|
60
|
+
```hcl
|
|
61
|
+
model "stacked_trays" {
|
|
62
|
+
for i in range(0, var.rows) {
|
|
63
|
+
part "tray_${i}" {
|
|
64
|
+
color = i % 2 == 0 ? "#3f7fbf" : "#bf6b3f"
|
|
65
|
+
translate { offset = [0, 0, i * 13] tray { size = [40, 30, 12] } }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Part labels are expressions, so `"tray_${i}"` is evaluated once per iteration. Two parts with
|
|
72
|
+
the same name are not rejected, but the viewer will show two entries called the same thing.
|
|
73
|
+
|
|
74
|
+
### Radial arrays
|
|
75
|
+
|
|
76
|
+
The common pattern for anything on a bolt circle:
|
|
77
|
+
|
|
78
|
+
```hcl
|
|
79
|
+
for i in range(0, var.bolts) {
|
|
80
|
+
rotate {
|
|
81
|
+
z = i * 360 / var.bolts
|
|
82
|
+
translate { offset = [var.plate_r - 9, 0, -1] cylinder { radius = 3 height = 10 } }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## `if`
|
|
90
|
+
|
|
91
|
+
Includes a body only when a condition holds.
|
|
92
|
+
|
|
93
|
+
```hcl
|
|
94
|
+
if var.reinforced {
|
|
95
|
+
box { size = [40, 40, 4] }
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
With an alternative, and chained:
|
|
100
|
+
|
|
101
|
+
```hcl
|
|
102
|
+
if var.style == "round" {
|
|
103
|
+
cylinder { radius = 10 height = 4 }
|
|
104
|
+
} else if var.style == "hex" {
|
|
105
|
+
extrude { height = 4 regular_polygon { sides = 6 radius = 10 } }
|
|
106
|
+
} else {
|
|
107
|
+
box { size = [20, 20, 4] center = true }
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Reference
|
|
112
|
+
|
|
113
|
+
| | |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| **Condition** | Any expression. Judged by [truthiness](expressions.md#truthiness), so `if var.name { }` is true for a non-empty string |
|
|
116
|
+
| **Branches** | `else` takes a body; `else if` chains, nested rather than flattened |
|
|
117
|
+
| **Scope** | Each branch is its own body scope |
|
|
118
|
+
| **Result** | What the taken branch produced, or nothing when no branch is taken |
|
|
119
|
+
|
|
120
|
+
#### Caveats
|
|
121
|
+
|
|
122
|
+
- A condition is an ordinary expression, so both branches are *parsed* but only the taken one
|
|
123
|
+
is evaluated. An error inside the untaken branch will not be reported.
|
|
124
|
+
- An `if` that takes no branch contributes nothing. Inside an operation that requires
|
|
125
|
+
children, that can turn into `"translate" needs at least one shape inside it`.
|
|
126
|
+
- There is no `if` for attributes. Use the conditional operator:
|
|
127
|
+
`color = var.hot ? "#e2593c" : "#3f7fbf"`.
|
|
128
|
+
|
|
129
|
+
### Making a part optional
|
|
130
|
+
|
|
131
|
+
Put the `if` **outside** the `part`, not inside it. A part with no geometry is an error
|
|
132
|
+
(`a part needs at least one shape inside it`), so guarding the contents is not enough:
|
|
133
|
+
|
|
134
|
+
```hcl
|
|
135
|
+
// ✓ the part exists only when it is wanted
|
|
136
|
+
if var.show_label {
|
|
137
|
+
part "label" { color = "#f4f1e4" ... }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ✗ fails with "a part needs at least one shape inside it" when show_label is false
|
|
141
|
+
part "label" {
|
|
142
|
+
if var.show_label { ... }
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Choosing between shapes
|
|
147
|
+
|
|
148
|
+
Because `if` produces geometry, it composes inside any operation:
|
|
149
|
+
|
|
150
|
+
```hcl
|
|
151
|
+
difference {
|
|
152
|
+
box { size = [40, 40, 10] }
|
|
153
|
+
|
|
154
|
+
if var.through {
|
|
155
|
+
translate { offset = [20, 20, -1] cylinder { radius = 4 height = 12 } }
|
|
156
|
+
} else {
|
|
157
|
+
translate { offset = [20, 20, 4] cylinder { radius = 4 height = 7 } }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|