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,248 @@
|
|
|
1
|
+
# `component`
|
|
2
|
+
|
|
3
|
+
A reusable, parameterised piece of geometry, callable exactly like a builtin block.
|
|
4
|
+
|
|
5
|
+
```hcl
|
|
6
|
+
component "rounded_box" {
|
|
7
|
+
param size = [20, 20, 10]
|
|
8
|
+
param radius = 3
|
|
9
|
+
|
|
10
|
+
extrude {
|
|
11
|
+
height = size.z
|
|
12
|
+
rounded_rect { size = [size.x, size.y] radius = radius center = true }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
model "m" {
|
|
17
|
+
rounded_box { size = [40, 30, 12] radius = 4 }
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Reference
|
|
24
|
+
|
|
25
|
+
### `component "NAME" { ... }`
|
|
26
|
+
|
|
27
|
+
Declares a block named `NAME`. Its body is ordinary geometry; its `param` declarations are
|
|
28
|
+
the attributes a call site may pass.
|
|
29
|
+
|
|
30
|
+
Once declared, it is called like any other block — the call site cannot tell a component
|
|
31
|
+
from a builtin, which is the point.
|
|
32
|
+
|
|
33
|
+
#### Body
|
|
34
|
+
|
|
35
|
+
| Entry | Meaning |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `param NAME = expr` or `param NAME { default = expr }` | An input. Without a default, the call site must supply it |
|
|
38
|
+
| `local NAME = expr` | A working value, scoped to the component |
|
|
39
|
+
| geometry blocks | The component's output. Several are implicitly unioned |
|
|
40
|
+
| `for` / `if` | Ordinary control flow |
|
|
41
|
+
|
|
42
|
+
#### Returns
|
|
43
|
+
|
|
44
|
+
One geometry node — everything in the body, unioned. The dimensionality is whatever the body
|
|
45
|
+
produced, so a component may return 2D or 3D geometry and be used wherever that fits.
|
|
46
|
+
|
|
47
|
+
#### Caveats
|
|
48
|
+
|
|
49
|
+
- **A component cannot declare [`part`](part.md)s.** Parts are pieces of the finished scene,
|
|
50
|
+
not operands: `a component cannot declare parts`. Call the component *inside* a part
|
|
51
|
+
instead.
|
|
52
|
+
- **A component cannot shadow a builtin.** `component "box"` is an error:
|
|
53
|
+
`component "box" shadows a builtin block`. Components and builtins share one namespace.
|
|
54
|
+
- **A component body cannot see the call site.** It is evaluated in a fresh scope rooted at
|
|
55
|
+
the document, so a local where it is used does not leak in. See
|
|
56
|
+
[Scope → Component isolation](../language/scope.md#component-isolation).
|
|
57
|
+
- **Only declared params may be passed.** An extra attribute is an error:
|
|
58
|
+
`c: unknown param "t"`. There is no catch-all.
|
|
59
|
+
- **A component must produce geometry.** An empty body, or one whose only `if` took no
|
|
60
|
+
branch, fails with `component "c" produced no geometry`.
|
|
61
|
+
- **Recursion is not supported.** A component that calls itself has no base case the
|
|
62
|
+
evaluator can see, and hits the nesting limit:
|
|
63
|
+
`nesting deeper than 64 blocks — is a component using itself?`
|
|
64
|
+
- A label on a call — `my_component "label" { }` — is accepted and silently ignored.
|
|
65
|
+
Do not rely on it.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
### Declaring inputs
|
|
72
|
+
|
|
73
|
+
Both `param` forms work, and a param with no default is required at every call site:
|
|
74
|
+
|
|
75
|
+
```hcl
|
|
76
|
+
component "tray" {
|
|
77
|
+
param size = [40, 30, 12] // has a default; optional at the call site
|
|
78
|
+
param wall = 2
|
|
79
|
+
param lid { } // no default; every call must pass it
|
|
80
|
+
|
|
81
|
+
difference {
|
|
82
|
+
rounded_box { size = size radius = 4 }
|
|
83
|
+
translate {
|
|
84
|
+
offset = [0, 0, wall]
|
|
85
|
+
rounded_box { size = [size.x - wall * 2, size.y - wall * 2, size.z] radius = 3 }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Note the empty braces on `lid`. `param lid` on its own is a **syntax error** — a param needs
|
|
92
|
+
either `= expression` or a braced body, even an empty one.
|
|
93
|
+
|
|
94
|
+
Inside the body, a param is read by its bare name. There is no `var.` for component params;
|
|
95
|
+
`var` holds document params only, and those are in scope too.
|
|
96
|
+
|
|
97
|
+
### Composing components
|
|
98
|
+
|
|
99
|
+
Components can call other components, which is how a model is built up in layers:
|
|
100
|
+
|
|
101
|
+
```hcl
|
|
102
|
+
component "rounded_box" {
|
|
103
|
+
param size = [20, 20, 10]
|
|
104
|
+
param radius = 3
|
|
105
|
+
extrude { height = size.z rounded_rect { size = [size.x, size.y] radius = radius center = true } }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
component "tray" {
|
|
109
|
+
param size = [40, 30, 12]
|
|
110
|
+
param wall = 2
|
|
111
|
+
difference {
|
|
112
|
+
rounded_box { size = size radius = 4 }
|
|
113
|
+
translate { offset = [0, 0, wall] rounded_box { size = size - [wall * 2, wall * 2, 0] radius = 3 } }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
model "stacked_trays" {
|
|
118
|
+
for i in range(0, var.rows) {
|
|
119
|
+
part "tray_${i}" {
|
|
120
|
+
translate { offset = [0, 0, i * 13] tray { size = [40, 30, 12] wall = 2 } }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Nesting may go 64 levels deep, which is far more than any real model needs.
|
|
127
|
+
|
|
128
|
+
### Naming a repeated cut
|
|
129
|
+
|
|
130
|
+
A component does not have to be a whole object. Naming the *negative* of a feature is often
|
|
131
|
+
what makes a model readable:
|
|
132
|
+
|
|
133
|
+
```hcl
|
|
134
|
+
component "counterbore" {
|
|
135
|
+
param depth = 4
|
|
136
|
+
param bolt = 3
|
|
137
|
+
param head = 5.5
|
|
138
|
+
|
|
139
|
+
union {
|
|
140
|
+
cylinder { radius = bolt height = 40 center = true }
|
|
141
|
+
translate { offset = [0, 0, -depth] cylinder { radius = head height = depth + 40 } }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
model "plate" {
|
|
146
|
+
difference {
|
|
147
|
+
box { size = [60, 60, 8] }
|
|
148
|
+
for p in [[12, 12], [48, 12], [12, 48], [48, 48]] {
|
|
149
|
+
translate { offset = [p.x, p.y, 8] counterbore { depth = 3 } }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 2D components
|
|
156
|
+
|
|
157
|
+
A component that produces a 2D shape is used wherever a 2D shape is:
|
|
158
|
+
|
|
159
|
+
```hcl
|
|
160
|
+
component "slot_profile" {
|
|
161
|
+
param length = 30
|
|
162
|
+
param width = 6
|
|
163
|
+
|
|
164
|
+
stadium { size = [length, width] center = true }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
model "m" {
|
|
168
|
+
extrude { height = 5 slot_profile { length = 40 } }
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Reuse is free
|
|
173
|
+
|
|
174
|
+
Identity in the geometry tree is content identity, so a component called twenty times with
|
|
175
|
+
the same arguments is **evaluated once** — its digest is the same each time, and the kernel
|
|
176
|
+
sees one node. Calling it twenty times with twenty different sizes does cost twenty
|
|
177
|
+
evaluations, because the content genuinely differs.
|
|
178
|
+
|
|
179
|
+
That makes it worth parameterising a component on what varies and letting position be a
|
|
180
|
+
`translate` on the outside: twenty copies of one shape in twenty places cost one shape and
|
|
181
|
+
twenty transforms.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Troubleshooting
|
|
186
|
+
|
|
187
|
+
### `expected {, found "box"`
|
|
188
|
+
|
|
189
|
+
A `param` with no default and no body:
|
|
190
|
+
|
|
191
|
+
```hcl
|
|
192
|
+
component "c" {
|
|
193
|
+
param s // ✗ syntax error — the parser is looking for `=` or `{`
|
|
194
|
+
box { size = [s, s, s] }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
component "c" {
|
|
198
|
+
param s { } // ✓ declared, required at the call site
|
|
199
|
+
box { size = [s, s, s] }
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### `c: "s" has no default and was not given`
|
|
204
|
+
|
|
205
|
+
The component declares `s` with no default, and the call passed nothing. Either supply it at
|
|
206
|
+
the call site, or give the declaration a default.
|
|
207
|
+
|
|
208
|
+
### `c: unknown param "t"`
|
|
209
|
+
|
|
210
|
+
The call passed an attribute the component does not declare — usually a typo, or an
|
|
211
|
+
attribute meant for a block inside the component. Components have no pass-through: every
|
|
212
|
+
value they need must be declared.
|
|
213
|
+
|
|
214
|
+
### `component "box" shadows a builtin block`
|
|
215
|
+
|
|
216
|
+
The name is already a builtin. Pick another — `rounded_box`, `my_box`, `case_body`. See the
|
|
217
|
+
[full block list](../README.md#shapes) for what is taken.
|
|
218
|
+
|
|
219
|
+
### `a component cannot declare parts`
|
|
220
|
+
|
|
221
|
+
A [`part`](part.md) is a piece of the scene, not geometry an enclosing block can operate on,
|
|
222
|
+
so it cannot come out of a component. Invert it — put the part in the model and call the
|
|
223
|
+
component inside it:
|
|
224
|
+
|
|
225
|
+
```hcl
|
|
226
|
+
// ✗
|
|
227
|
+
component "c" { part "p" { box { size = [1, 1, 1] } } }
|
|
228
|
+
|
|
229
|
+
// ✓
|
|
230
|
+
component "c" { box { size = [1, 1, 1] } }
|
|
231
|
+
model "m" { part "p" { c { } } }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### `nesting deeper than 64 blocks — is a component using itself?`
|
|
235
|
+
|
|
236
|
+
Almost always direct or mutual recursion. There is no base case the evaluator can detect,
|
|
237
|
+
because a component's body is expanded before any condition inside it is known. Rewrite the
|
|
238
|
+
recursion as a [`for`](../language/control-flow.md#for) loop.
|
|
239
|
+
|
|
240
|
+
### `component "c" produced no geometry`
|
|
241
|
+
|
|
242
|
+
The body evaluated to nothing — an empty body, a loop over an empty list, or an `if` with no
|
|
243
|
+
`else` whose condition was false. A component must return something; guard at the call site
|
|
244
|
+
instead:
|
|
245
|
+
|
|
246
|
+
```hcl
|
|
247
|
+
if var.show_ribs { ribs { } }
|
|
248
|
+
```
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# 2D ↔ 3D
|
|
2
|
+
|
|
3
|
+
Four operations that change dimensionality. Two give a profile thickness; two take a
|
|
4
|
+
cross-section of a solid.
|
|
5
|
+
|
|
6
|
+
| Block | Direction | Description |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| [`extrude`](#extrude) | 2D → 3D | Sweeps a profile along Z |
|
|
9
|
+
| [`revolve`](#revolve) | 2D → 3D | Spins a profile about the Y axis |
|
|
10
|
+
| [`project`](#project) | 3D → 2D | The shadow of a solid on the XY plane |
|
|
11
|
+
| [`slice`](#slice) | 3D → 2D | The cross-section at one Z |
|
|
12
|
+
|
|
13
|
+
Each unions its children before acting on them, and each insists on the dimensionality it
|
|
14
|
+
takes: `"extrude" takes 2D geometry, got 3D`.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## `extrude`
|
|
19
|
+
|
|
20
|
+
Sweeps a 2D profile along Z, optionally twisting and tapering as it goes.
|
|
21
|
+
|
|
22
|
+
```hcl
|
|
23
|
+
extrude { height = 20 circle { radius = 10 } }
|
|
24
|
+
|
|
25
|
+
extrude {
|
|
26
|
+
height = 40
|
|
27
|
+
twist = 90
|
|
28
|
+
scale_top = [0.5, 0.5]
|
|
29
|
+
divisions = 24
|
|
30
|
+
rect { size = [20, 20] center = true }
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
#### Attributes
|
|
35
|
+
|
|
36
|
+
| Attribute | Type | Default | Description |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| `height` | number | **required** | How far to sweep, along +Z |
|
|
39
|
+
| `twist` | angle | `0` | Degrees of rotation from bottom to top |
|
|
40
|
+
| `scale_top` | 2-vector | `1` | Size of the top face relative to the bottom. A single number scales both axes |
|
|
41
|
+
| `divisions` | integer | `0` | Intermediate layers. Needed for a smooth twist or taper |
|
|
42
|
+
| `center` | bool | `false` | `true` centres the result on Z = 0; otherwise it stands on the XY plane |
|
|
43
|
+
|
|
44
|
+
#### Caveats
|
|
45
|
+
|
|
46
|
+
- **`divisions` matters whenever `twist` or `scale_top` is set.** With the default `0` the
|
|
47
|
+
sides are straight lines between the two end faces, so a twist becomes a single sheared
|
|
48
|
+
step rather than a helix. Set it to the number of intermediate layers you want.
|
|
49
|
+
- A `height` of `0` or less yields empty geometry rather than an error, so a parameter
|
|
50
|
+
sweeping to zero does not break the model.
|
|
51
|
+
- `scale_top = [0, 0]` gives a point, which is one way to build a pyramid from any profile.
|
|
52
|
+
- The profile is swept as drawn — `extrude` does not centre it. A `rect` that is not centred
|
|
53
|
+
produces a box in the positive quadrant.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## `revolve`
|
|
58
|
+
|
|
59
|
+
Spins a 2D profile about the **Y axis** into a solid of revolution.
|
|
60
|
+
|
|
61
|
+
```hcl
|
|
62
|
+
revolve { segments = 96 polygon { points = profile } }
|
|
63
|
+
revolve { angle = 180 segments = 64 polygon { points = profile } }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Attributes
|
|
67
|
+
|
|
68
|
+
| Attribute | Type | Default | Description |
|
|
69
|
+
| --- | --- | --- | --- |
|
|
70
|
+
| `angle` | angle | `360` | How far round to sweep, in degrees |
|
|
71
|
+
| `segments` | integer | `0` | Segments around; `0` lets the kernel choose |
|
|
72
|
+
|
|
73
|
+
#### Caveats
|
|
74
|
+
|
|
75
|
+
- **The profile is written in the XY plane, and its Y becomes the result's Z.** X is the
|
|
76
|
+
radius. A profile drawn from `[0, 0]` upwards produces a solid standing on the XY plane.
|
|
77
|
+
- **Every X must be ≥ 0.** A profile that crosses the axis revolves through itself and the
|
|
78
|
+
result is not what you drew.
|
|
79
|
+
- The profile does not have to touch the axis. One that does not produces a tube — which is
|
|
80
|
+
how [`torus`](shapes-3d.md#torus) is built internally.
|
|
81
|
+
- An `angle` of `0` or less yields empty geometry rather than an error.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## `project`
|
|
86
|
+
|
|
87
|
+
The shadow a solid casts on the XY plane — its full outline, from any Z.
|
|
88
|
+
|
|
89
|
+
```hcl
|
|
90
|
+
project { bracket { } }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Attributes
|
|
94
|
+
|
|
95
|
+
None.
|
|
96
|
+
|
|
97
|
+
#### Caveats
|
|
98
|
+
|
|
99
|
+
- Takes 3D geometry and produces 2D: `"project" takes 3D geometry, got 2D`.
|
|
100
|
+
- The result is the union of everything, seen from above. Internal voids that are covered
|
|
101
|
+
from above do not appear.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## `slice`
|
|
106
|
+
|
|
107
|
+
The cross-section of a solid at one height.
|
|
108
|
+
|
|
109
|
+
```hcl
|
|
110
|
+
slice { z = 12 bracket { } }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Attributes
|
|
114
|
+
|
|
115
|
+
| Attribute | Type | Default | Description |
|
|
116
|
+
| --- | --- | --- | --- |
|
|
117
|
+
| `z` | number | `0` | The plane to cut at |
|
|
118
|
+
|
|
119
|
+
#### Caveats
|
|
120
|
+
|
|
121
|
+
- Takes 3D geometry and produces 2D.
|
|
122
|
+
- A plane that misses the solid gives an empty cross-section, not an error.
|
|
123
|
+
- A plane exactly on a flat face is ambiguous — offset it slightly.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Usage
|
|
128
|
+
|
|
129
|
+
### Profiles are where the modelling happens
|
|
130
|
+
|
|
131
|
+
Most shapes are easier to describe in 2D and then given thickness. A rounded box, for
|
|
132
|
+
instance, has no primitive — it is a rounded rectangle with a height:
|
|
133
|
+
|
|
134
|
+
```hcl
|
|
135
|
+
extrude {
|
|
136
|
+
height = 12
|
|
137
|
+
rounded_rect { size = [40, 30] radius = 4 center = true }
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Turning a bottle, a vase, a knob
|
|
142
|
+
|
|
143
|
+
`revolve` on a hand-written profile is how anything with rotational symmetry is built. Read
|
|
144
|
+
the points as (radius, height):
|
|
145
|
+
|
|
146
|
+
```hcl
|
|
147
|
+
local profile = [
|
|
148
|
+
[0, 0], [26, 0], [24, 6], [16, 26],
|
|
149
|
+
[14, 48], [18, 66], [22, 74], [0, 74],
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
// The same silhouette walked inwards by one wall, so the difference leaves a shell.
|
|
153
|
+
local cavity = [
|
|
154
|
+
[0, 3], [21, 3], [20, 8], [13, 26],
|
|
155
|
+
[11, 48], [15, 66], [19, 72], [0, 72],
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
model "vase" {
|
|
159
|
+
part "vase" {
|
|
160
|
+
difference {
|
|
161
|
+
revolve { segments = 96 polygon { points = profile } }
|
|
162
|
+
translate {
|
|
163
|
+
offset = [0, 0, 3]
|
|
164
|
+
revolve { segments = 96 polygon { points = cavity } }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The hollowing is the same trick as anywhere else: the solid minus a slightly smaller copy of
|
|
172
|
+
itself.
|
|
173
|
+
|
|
174
|
+
### Twisted extrusions
|
|
175
|
+
|
|
176
|
+
A twist needs divisions, or there is nothing to twist between:
|
|
177
|
+
|
|
178
|
+
```hcl
|
|
179
|
+
extrude {
|
|
180
|
+
height = 60
|
|
181
|
+
twist = 180
|
|
182
|
+
divisions = 40 // without this the sides are two straight lines
|
|
183
|
+
regular_polygon { sides = 6 radius = 12 }
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Tapers and pyramids
|
|
188
|
+
|
|
189
|
+
```hcl
|
|
190
|
+
extrude { height = 30 scale_top = 0.4 rect { size = [40, 40] center = true } } // frustum
|
|
191
|
+
extrude { height = 30 scale_top = 0 rect { size = [40, 40] center = true } } // pyramid
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Deriving a base plate from a part
|
|
195
|
+
|
|
196
|
+
`project` turns a finished solid back into an outline, which is useful for a footprint, a
|
|
197
|
+
gasket or a support pad:
|
|
198
|
+
|
|
199
|
+
```hcl
|
|
200
|
+
model "m" {
|
|
201
|
+
part "body" { bracket { } }
|
|
202
|
+
|
|
203
|
+
part "pad" {
|
|
204
|
+
color = "#c8842f"
|
|
205
|
+
translate {
|
|
206
|
+
offset = [0, 0, -3]
|
|
207
|
+
extrude {
|
|
208
|
+
height = 3
|
|
209
|
+
offset { amount = 2 project { bracket { } } } // footprint, grown 2 mm
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Inspecting a cross-section
|
|
217
|
+
|
|
218
|
+
`slice` is a debugging tool as much as a modelling one — extrude the slice thinly to see
|
|
219
|
+
where the walls actually fell:
|
|
220
|
+
|
|
221
|
+
```hcl
|
|
222
|
+
part "section" {
|
|
223
|
+
color = "#ff6b7f"
|
|
224
|
+
extrude { height = 0.5 slice { z = 40 bottle { } } }
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Troubleshooting
|
|
231
|
+
|
|
232
|
+
### `"extrude" takes 2D geometry, got 3D`
|
|
233
|
+
|
|
234
|
+
The child is already a solid. Extrusion is for profiles:
|
|
235
|
+
|
|
236
|
+
```hcl
|
|
237
|
+
extrude { height = 10 box { size = [1, 1, 1] } } // ✗
|
|
238
|
+
extrude { height = 10 rect { size = [1, 1] } } // ✓
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### `"extrude" cannot mix 2D and 3D shapes`
|
|
242
|
+
|
|
243
|
+
Two children of different dimensionality. Extrude each profile separately, or union the
|
|
244
|
+
profiles before extruding.
|
|
245
|
+
|
|
246
|
+
### `part "body" is 2D — extrude or revolve it before rendering`
|
|
247
|
+
|
|
248
|
+
A profile reached the renderer. Somewhere the `extrude` or `revolve` is missing, or is
|
|
249
|
+
wrapped around the wrong block.
|
|
250
|
+
|
|
251
|
+
### My twist looks like a single shear
|
|
252
|
+
|
|
253
|
+
`divisions` defaults to `0`, which means the extrusion has no intermediate layers. Set it to
|
|
254
|
+
20–60 for a smooth twist.
|
|
255
|
+
|
|
256
|
+
### My revolved solid is inside out, or has a spike through the middle
|
|
257
|
+
|
|
258
|
+
Part of the profile has a negative X, so it crossed the axis of revolution. Every point must
|
|
259
|
+
have X ≥ 0.
|
|
260
|
+
|
|
261
|
+
### My revolved solid is lying down
|
|
262
|
+
|
|
263
|
+
`revolve` maps the profile's **Y** to the result's **Z**. A profile written as (x, z) with
|
|
264
|
+
the height in the first component produces a solid on its side — swap the components.
|
|
265
|
+
|
|
266
|
+
### `revolve` produced nothing
|
|
267
|
+
|
|
268
|
+
Either the `angle` reached `0`, or the profile has zero area. Both yield empty geometry
|
|
269
|
+
silently, by design, so that a parameter at the end of its range does not break a model.
|