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,230 @@
|
|
|
1
|
+
# `param`
|
|
2
|
+
|
|
3
|
+
A declared input. Params are what the editor turns into controls and what
|
|
4
|
+
[`render`](../api/render.md) accepts values for.
|
|
5
|
+
|
|
6
|
+
```hcl
|
|
7
|
+
param height { type = number default = 20 min = 8 max = 40 step = 1 }
|
|
8
|
+
param wall = 2.5
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Reference
|
|
14
|
+
|
|
15
|
+
### `param NAME { ... }`
|
|
16
|
+
|
|
17
|
+
Declares an input at the top level of a document. Read it as `var.NAME`, or by its bare name.
|
|
18
|
+
|
|
19
|
+
```hcl
|
|
20
|
+
param bolts {
|
|
21
|
+
type = number
|
|
22
|
+
default = 6
|
|
23
|
+
min = 3
|
|
24
|
+
max = 16
|
|
25
|
+
step = 1
|
|
26
|
+
description = "How many holes on the bolt circle"
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
#### Attributes
|
|
31
|
+
|
|
32
|
+
Every attribute is optional. **Only `default` affects geometry** — the rest describe the
|
|
33
|
+
control the editor should build, and are returned verbatim by
|
|
34
|
+
[`describeParameters`](../api/describe-parameters.md).
|
|
35
|
+
|
|
36
|
+
| Attribute | Type | Description |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `default` | any | The value used when the caller supplies none. Without it the param is *required* |
|
|
39
|
+
| `type` | `number` `string` `bool` `vector` `list` `angle` | Which control to build. Written bare, without quotes. Inferred from `default` when omitted |
|
|
40
|
+
| `min` | number | Lower bound of the control |
|
|
41
|
+
| `max` | number | Upper bound of the control |
|
|
42
|
+
| `step` | number | Granularity of the control |
|
|
43
|
+
| `description` | string | Help text |
|
|
44
|
+
| `options` | list | A fixed set of choices, for a dropdown rather than a free field |
|
|
45
|
+
|
|
46
|
+
Unknown attributes are **not** rejected here — a param block is metadata, and anything in it
|
|
47
|
+
is passed through to the descriptor. This is the one block in the language that does not
|
|
48
|
+
reject attributes it does not understand.
|
|
49
|
+
|
|
50
|
+
#### Returns
|
|
51
|
+
|
|
52
|
+
Nothing. A param binds a name; it produces no geometry.
|
|
53
|
+
|
|
54
|
+
#### Caveats
|
|
55
|
+
|
|
56
|
+
- **`min`, `max`, `step` and `options` are not enforced.** They shape the editor's control
|
|
57
|
+
and nothing else. `render(source, { params: { bolts: 9999 } })` succeeds on a param
|
|
58
|
+
declared `max = 16`. Validate in the model if a bound matters —
|
|
59
|
+
`local n = min(max(var.bolts, 3), 16)`.
|
|
60
|
+
- A param block may contain **attributes only**. Nested blocks inside it are an error:
|
|
61
|
+
`a param block holds attributes, not nested blocks`.
|
|
62
|
+
- Declaring the same param twice is an error: `param "a" is declared twice`.
|
|
63
|
+
- A param's `default` may refer to params declared **above** it by their bare name, but not
|
|
64
|
+
to `var` — `var` is assembled after every param has resolved. See
|
|
65
|
+
[Scope](../language/scope.md#the-root-scope).
|
|
66
|
+
- A param declared inside a `model` is a different thing: it must have a default and cannot
|
|
67
|
+
be set from outside. See [`model`](model.md#model-params).
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### `param NAME = expression`
|
|
72
|
+
|
|
73
|
+
Shorthand for a param whose only attribute is `default`.
|
|
74
|
+
|
|
75
|
+
```hcl
|
|
76
|
+
param thickness = 8
|
|
77
|
+
param sizes = [1.5, 2, 2.5, 3]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Identical to `param thickness { default = 8 }`. Use it when the editor needs no bounds and
|
|
81
|
+
no description.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
### Reading a param
|
|
88
|
+
|
|
89
|
+
Both spellings work; `var.` is the convention because it marks the value as an input at the
|
|
90
|
+
point of use.
|
|
91
|
+
|
|
92
|
+
```hcl
|
|
93
|
+
param height = 20
|
|
94
|
+
|
|
95
|
+
model "m" {
|
|
96
|
+
extrude { height = var.height circle { radius = 10 } }
|
|
97
|
+
// ^^^^^^^^^^ or just `height`, same value
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`var` holds **params only**. A [`local`](local.md) with the same name shadows the bare name
|
|
102
|
+
but never appears under `var`, which is what makes `var.` unambiguous:
|
|
103
|
+
|
|
104
|
+
```hcl
|
|
105
|
+
param a = 1
|
|
106
|
+
local a = 2
|
|
107
|
+
|
|
108
|
+
model "m" { box { size = [a, var.a, 1] } } // [2, 1, 1]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Choosing a type
|
|
112
|
+
|
|
113
|
+
`type` is what the editor reads to decide which control to draw.
|
|
114
|
+
|
|
115
|
+
```hcl
|
|
116
|
+
param count { type = number default = 6 min = 1 max = 20 step = 1 } // slider
|
|
117
|
+
param label { type = string default = "A" } // text field
|
|
118
|
+
param hollow { type = bool default = true } // checkbox
|
|
119
|
+
param size { type = vector default = [40, 30, 12] } // three fields
|
|
120
|
+
param sweep { type = angle default = 45 min = 0 max = 360 } // degrees
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
When `type` is omitted it is inferred from the default: a number gives `number`, a boolean
|
|
124
|
+
`bool`, a string `string`, a list `vector`. A param with neither a `type` nor a `default`
|
|
125
|
+
falls back to `number`.
|
|
126
|
+
|
|
127
|
+
An `angle` param is an ordinary number as far as the language is concerned — every angle in
|
|
128
|
+
forma is in degrees, and the type only tells the editor to label the control that way.
|
|
129
|
+
|
|
130
|
+
### Declaring a required input
|
|
131
|
+
|
|
132
|
+
A param with no `default` has no value until one is supplied. Rendering without it fails:
|
|
133
|
+
|
|
134
|
+
```hcl
|
|
135
|
+
param serial { type = string }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
param "serial" has no default and no value was supplied
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
[`describeParameters`](../api/describe-parameters.md) reports it as `required: true`, so a
|
|
143
|
+
front end can insist on a value before rendering. This is how a document says "there is no
|
|
144
|
+
sensible default for this".
|
|
145
|
+
|
|
146
|
+
### Deriving one input from another
|
|
147
|
+
|
|
148
|
+
Params are resolved in source order, so a later default may be written in terms of an
|
|
149
|
+
earlier one:
|
|
150
|
+
|
|
151
|
+
```hcl
|
|
152
|
+
param diameter = 72
|
|
153
|
+
param wall = 1.6
|
|
154
|
+
param bore = diameter / 2 - wall * 4 // ✓ bare names of earlier params
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
For a value that is always derived and should never be a control, use a
|
|
158
|
+
[`local`](local.md) instead — locals are computed after every param and may use `var.`.
|
|
159
|
+
|
|
160
|
+
### Offering a fixed set of choices
|
|
161
|
+
|
|
162
|
+
```hcl
|
|
163
|
+
param finish {
|
|
164
|
+
type = string
|
|
165
|
+
default = "matte"
|
|
166
|
+
options = ["matte", "gloss", "raw"]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
model "m" {
|
|
170
|
+
part "body" {
|
|
171
|
+
color = var.finish == "gloss" ? "#e8eef4" : "#8f9ba6"
|
|
172
|
+
box { size = [20, 20, 20] }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`options` is carried through to the descriptor for the editor to render as a dropdown.
|
|
178
|
+
Nothing checks that a supplied value is one of them, so branch with a final `else` that is
|
|
179
|
+
safe.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Troubleshooting
|
|
184
|
+
|
|
185
|
+
### `param "height" has no default and no value was supplied`
|
|
186
|
+
|
|
187
|
+
The document declares the param with no `default`, and the caller passed no value. Either
|
|
188
|
+
give it a default, or supply one:
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
await render(source, { params: { height: 20 } });
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
[`describeParameters`](../api/describe-parameters.md) works on such a document — it is built
|
|
195
|
+
to be callable before any value exists, so a front end can discover what to ask for.
|
|
196
|
+
|
|
197
|
+
### `param "a" is declared twice`
|
|
198
|
+
|
|
199
|
+
Two top-level `param` blocks share a name. Unlike locals, params are rejected rather than
|
|
200
|
+
overwritten, because a duplicated input would make the editor's control ambiguous.
|
|
201
|
+
|
|
202
|
+
### `a param block holds attributes, not nested blocks`
|
|
203
|
+
|
|
204
|
+
Something inside the braces was parsed as a block rather than an attribute — usually a
|
|
205
|
+
missing `=`:
|
|
206
|
+
|
|
207
|
+
```hcl
|
|
208
|
+
param height { default 20 } // ✗ parsed as a block named `default`
|
|
209
|
+
param height { default = 20 } // ✓
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `unknown name "var"`
|
|
213
|
+
|
|
214
|
+
A param default referred to `var`. `var` is assembled only after every param has been
|
|
215
|
+
resolved, so it does not exist yet:
|
|
216
|
+
|
|
217
|
+
```hcl
|
|
218
|
+
param a = 3
|
|
219
|
+
param b = var.a * 2 // ✗ unknown name "var"
|
|
220
|
+
param b = a * 2 // ✓ earlier params are in scope by their bare name
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### The editor ignores my `min` and `max`
|
|
224
|
+
|
|
225
|
+
They are advisory. The control respects them, but a value supplied through the JavaScript
|
|
226
|
+
API does not, and the geometry never sees them. Clamp in the model if it matters:
|
|
227
|
+
|
|
228
|
+
```hcl
|
|
229
|
+
local bolts = max(3, min(16, var.bolts))
|
|
230
|
+
```
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# `part`
|
|
2
|
+
|
|
3
|
+
A separately coloured piece of the rendered scene.
|
|
4
|
+
|
|
5
|
+
```hcl
|
|
6
|
+
model "assembly" {
|
|
7
|
+
part "frame" { color = "#6f7d8c" box { size = [40, 40, 4] } }
|
|
8
|
+
part "pin" { color = "#c8842f" translate { offset = [0, 0, 4] cylinder { radius = 3 height = 10 } } }
|
|
9
|
+
}
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Reference
|
|
15
|
+
|
|
16
|
+
### `part "NAME" { ... }`
|
|
17
|
+
|
|
18
|
+
Groups geometry into one named, coloured piece of the scene. Parts are solved independently
|
|
19
|
+
and drawn in their own colour.
|
|
20
|
+
|
|
21
|
+
A part is **not an operand**. It does not contribute geometry to the block around it, and an
|
|
22
|
+
enclosing `translate` will not move it — which is why a part belongs directly in a
|
|
23
|
+
[`model`](model.md), or inside a `for` or `if` in one.
|
|
24
|
+
|
|
25
|
+
#### Label
|
|
26
|
+
|
|
27
|
+
Optional, and an **expression** rather than a fixed string, so a part built in a loop can
|
|
28
|
+
name itself:
|
|
29
|
+
|
|
30
|
+
```hcl
|
|
31
|
+
part "tray_${i}" { ... }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Without a label, the part is named `part_<line>` after the line it was written on.
|
|
35
|
+
|
|
36
|
+
#### Attributes
|
|
37
|
+
|
|
38
|
+
| Attribute | Type | Default | Description |
|
|
39
|
+
| --- | --- | --- | --- |
|
|
40
|
+
| `color` | string | `"#b8c4d0"` | Any CSS colour the viewer understands. Usually `#rrggbb` |
|
|
41
|
+
| `opacity` | number | `1` | `0` is invisible, `1` is opaque. Useful for showing contents through a shell |
|
|
42
|
+
|
|
43
|
+
#### Returns
|
|
44
|
+
|
|
45
|
+
Nothing, to the enclosing block. The part travels separately and lands in the scene.
|
|
46
|
+
|
|
47
|
+
#### Caveats
|
|
48
|
+
|
|
49
|
+
- **A part cannot contain another part**: `a part cannot contain another part`. Parts are a
|
|
50
|
+
flat list, not a tree.
|
|
51
|
+
- **A part nested inside an operation is silently dropped.** `translate { part "p" { … } }`
|
|
52
|
+
discards the part with no error, because an operation collects geometry from its children
|
|
53
|
+
and a part contributes none. Declare parts directly in the model. See
|
|
54
|
+
[Troubleshooting](#a-part-inside-an-operation-disappears).
|
|
55
|
+
- **A part cannot be empty**: `a part needs at least one shape inside it`. To make a part
|
|
56
|
+
conditional, put the [`if`](../language/control-flow.md#if) around it, not inside it.
|
|
57
|
+
- **A part cannot come out of a [`component`](component.md)**:
|
|
58
|
+
`a component cannot declare parts`. Call the component inside a part instead.
|
|
59
|
+
- **Every part must be 3D** by the time it renders:
|
|
60
|
+
`part "body" is 2D — extrude or revolve it before rendering`.
|
|
61
|
+
- Several blocks inside a part are implicitly unioned, exactly as anywhere else.
|
|
62
|
+
- Nothing rejects two parts with the same name; the viewer will simply list two entries
|
|
63
|
+
called the same thing.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
### Colouring an assembly
|
|
70
|
+
|
|
71
|
+
The common case. Each physical piece gets a part, and the colours make the render readable:
|
|
72
|
+
|
|
73
|
+
```hcl
|
|
74
|
+
model "water_bottle" {
|
|
75
|
+
part "bottle" { color = "#8fd3e8" bottle_shell { } }
|
|
76
|
+
part "cap" { color = "#e2593c" translate { offset = [0, 0, 170] cap { } } }
|
|
77
|
+
part "label" { color = "#f4f1e4" label_band { } }
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Showing what is inside
|
|
82
|
+
|
|
83
|
+
`opacity` makes a shell see-through without changing the geometry:
|
|
84
|
+
|
|
85
|
+
```hcl
|
|
86
|
+
model "assembly" {
|
|
87
|
+
part "case" {
|
|
88
|
+
color = "#8fa8bd"
|
|
89
|
+
opacity = 0.35 // the board below shows through
|
|
90
|
+
case_shell { }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
part "board" { color = "#2f7d4f" translate { offset = [0, 0, 3] board { } } }
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Opacity is a display property. It does not affect the solid, the digest, or an exported STL.
|
|
98
|
+
|
|
99
|
+
### Parts in a loop
|
|
100
|
+
|
|
101
|
+
A part label is an expression, so the loop variable can name each one:
|
|
102
|
+
|
|
103
|
+
```hcl
|
|
104
|
+
model "stack" {
|
|
105
|
+
for i in range(0, var.rows) {
|
|
106
|
+
part "tray_${i}" {
|
|
107
|
+
color = i % 2 == 0 ? "#3f7fbf" : "#bf6b3f"
|
|
108
|
+
translate { offset = [0, 0, i * 13] tray { } }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Making a part optional
|
|
115
|
+
|
|
116
|
+
Put the condition **outside** the part:
|
|
117
|
+
|
|
118
|
+
```hcl
|
|
119
|
+
// ✓
|
|
120
|
+
if var.show_label {
|
|
121
|
+
part "label" { color = "#f4f1e4" label_band { } }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ✗ — when show_label is false this is an empty part, which is an error
|
|
125
|
+
part "label" {
|
|
126
|
+
if var.show_label { label_band { } }
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### One part, several pieces of geometry
|
|
131
|
+
|
|
132
|
+
Everything in a part is unioned, so a part may hold as many blocks as it needs:
|
|
133
|
+
|
|
134
|
+
```hcl
|
|
135
|
+
part "bottle" {
|
|
136
|
+
color = "#8fd3e8"
|
|
137
|
+
|
|
138
|
+
difference { // the shell…
|
|
139
|
+
bottle_solid { }
|
|
140
|
+
bottle_void { }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for i in range(0, 3) { // …plus the thread ridges, same part
|
|
144
|
+
translate { offset = [0, 0, 174 + i * 4.5] torus { radius = 15.6 tube_radius = 1.1 } }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## How parts reach the renderer
|
|
152
|
+
|
|
153
|
+
[`render`](../api/render.md) returns one entry per part:
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
const result = await render(source);
|
|
157
|
+
|
|
158
|
+
for (const part of result.parts) {
|
|
159
|
+
part.name; // "bottle"
|
|
160
|
+
part.color; // "#8fd3e8"
|
|
161
|
+
part.opacity; // 1
|
|
162
|
+
part.node; // the GeometryNode — what context.collect() wants
|
|
163
|
+
part.concrete; // the solved Manifold solid, owned by the context
|
|
164
|
+
part.mesh; // positions and normals, ready to upload
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Parts are solved independently, so two parts that share a subtree still solve it once — the
|
|
169
|
+
digest cache is keyed on content, not on which part asked.
|
|
170
|
+
|
|
171
|
+
To export one part, pass its `concrete` to [`toBinarySTL`](../api/export.md#tobinarystl).
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Troubleshooting
|
|
176
|
+
|
|
177
|
+
### `a part needs at least one shape inside it`
|
|
178
|
+
|
|
179
|
+
The part's body produced nothing — usually an `if` inside it that took no branch, or a `for`
|
|
180
|
+
over an empty list. Move the condition outside the part.
|
|
181
|
+
|
|
182
|
+
### `a part cannot contain another part`
|
|
183
|
+
|
|
184
|
+
Parts are flat. To group several coloured pieces, declare them as siblings in the model. If
|
|
185
|
+
the grouping matters for positioning, apply the same `translate` inside each one, or hoist
|
|
186
|
+
the shared placement into a [`component`](component.md).
|
|
187
|
+
|
|
188
|
+
### `a component cannot declare parts`
|
|
189
|
+
|
|
190
|
+
Geometry comes out of a component; scene structure does not. Invert it:
|
|
191
|
+
|
|
192
|
+
```hcl
|
|
193
|
+
component "shell" { ... } // geometry only
|
|
194
|
+
model "m" { part "shell" { shell { } } } // the part lives here
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `part "body" is 2D — extrude or revolve it before rendering`
|
|
198
|
+
|
|
199
|
+
The part holds a cross-section. Extrude or revolve it first — see
|
|
200
|
+
[2D ↔ 3D](conversions.md).
|
|
201
|
+
|
|
202
|
+
### A part inside an operation disappears
|
|
203
|
+
|
|
204
|
+
A part is not an operand, so an operation collects no geometry from it. What happens next
|
|
205
|
+
depends on whether the operation had anything else to work with:
|
|
206
|
+
|
|
207
|
+
```hcl
|
|
208
|
+
// ✗ fails: "translate" needs at least one shape inside it
|
|
209
|
+
translate { offset = [0, 0, 10] part "p" { box { size = [1, 1, 1] } } }
|
|
210
|
+
|
|
211
|
+
// ✗ worse — renders, but part "p" is silently gone from the scene
|
|
212
|
+
translate {
|
|
213
|
+
offset = [0, 0, 10]
|
|
214
|
+
box { size = [1, 1, 1] }
|
|
215
|
+
part "p" { sphere { radius = 1 } }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ✓ the transform belongs inside the part
|
|
219
|
+
part "p" { translate { offset = [0, 0, 10] box { size = [1, 1, 1] } } }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Nothing warns about the second case. If a part you declared is missing from the render,
|
|
223
|
+
check that it is written directly in the [`model`](model.md) — or inside a `for` or `if` in
|
|
224
|
+
one — and not nested inside a transform or a boolean.
|
|
225
|
+
|
|
226
|
+
### Two parts have the same name
|
|
227
|
+
|
|
228
|
+
Nothing rejects it. Part names come from an expression, so a loop that computes the same
|
|
229
|
+
label twice produces duplicates — include the loop index: `part "leg_${i}"`.
|