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,210 @@
|
|
|
1
|
+
# Syntax
|
|
2
|
+
|
|
3
|
+
The lexical rules, the grammar, and the one whitespace rule that is easy to trip over.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Comments
|
|
8
|
+
|
|
9
|
+
Three forms, all ignored:
|
|
10
|
+
|
|
11
|
+
```hcl
|
|
12
|
+
// a line comment
|
|
13
|
+
# also a line comment
|
|
14
|
+
/* a block comment,
|
|
15
|
+
which may span lines */
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
A block comment that is never closed is an error: `unterminated block comment`. Block
|
|
19
|
+
comments do not nest.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Numbers
|
|
24
|
+
|
|
25
|
+
Decimal, with an optional fraction and an optional exponent.
|
|
26
|
+
|
|
27
|
+
```hcl
|
|
28
|
+
12 0.5 .5 1e3 2.5e-3
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
There is no hexadecimal, no separator, and no unit suffix. A leading `-` is the unary minus
|
|
32
|
+
operator, not part of the literal.
|
|
33
|
+
|
|
34
|
+
`2e` and `2em` are **not** numbers with exponents — the lexer backs out of a bare `e` and
|
|
35
|
+
lets it start an identifier, so `2e` lexes as the number `2` followed by the name `e`.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Strings
|
|
40
|
+
|
|
41
|
+
Double quotes only. A string may not contain a literal newline.
|
|
42
|
+
|
|
43
|
+
```hcl
|
|
44
|
+
"plain"
|
|
45
|
+
"with a ${var.count * 2} value spliced in"
|
|
46
|
+
"a tab\there, a quote\", a literal \${ not a splice"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Escapes.** Exactly six: `\n`, `\t`, `\r`, `\"`, `\\` and `\$`. Anything else is an error —
|
|
50
|
+
`unknown escape \q`. `\$` exists so a literal `${` can be written.
|
|
51
|
+
|
|
52
|
+
**Interpolation.** `${ ... }` splices in any expression. Braces inside the splice are
|
|
53
|
+
counted, so an object literal does not end it early. See
|
|
54
|
+
[Expressions → Interpolation](expressions.md#string-interpolation) for how each value type is
|
|
55
|
+
rendered.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Identifiers
|
|
60
|
+
|
|
61
|
+
An identifier starts with a letter or `_` and continues with letters, digits, `_` **or `-`**.
|
|
62
|
+
|
|
63
|
+
```hcl
|
|
64
|
+
height wall_thickness _private min-width part2
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
> **Hyphens are part of a name.** `a-b` is one identifier, not `a` minus `b`. Write
|
|
68
|
+
> subtraction with spaces: `a - b`. This is the single most common surprise in the lexer —
|
|
69
|
+
> `local gap = outer-inner` fails with `unknown name "outer-inner"`.
|
|
70
|
+
|
|
71
|
+
### Keywords
|
|
72
|
+
|
|
73
|
+
These eleven names introduce a construct and cannot be used as a block type:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
param local component model for if else in true false null
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
They can still be used as attribute names and as identifiers in expressions where the
|
|
80
|
+
grammar is unambiguous, but avoid it.
|
|
81
|
+
|
|
82
|
+
### Type names
|
|
83
|
+
|
|
84
|
+
`number`, `string`, `bool`, `vector`, `list` and `angle` are ordinary identifiers bound to
|
|
85
|
+
themselves, so `type = number` in a [`param`](../reference/param.md) block needs no quoting
|
|
86
|
+
and no keyword.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Blocks, attributes and labels
|
|
91
|
+
|
|
92
|
+
A body contains attributes and nested blocks, in any order and in any mix.
|
|
93
|
+
|
|
94
|
+
```hcl
|
|
95
|
+
block_type "label" {
|
|
96
|
+
attribute = expression
|
|
97
|
+
nested_block { ... }
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- **An attribute** is `name = expression`. Setting the same attribute twice in one block is
|
|
102
|
+
an error.
|
|
103
|
+
- **A nested block** is an identifier that is *not* followed by `=`, then optional labels,
|
|
104
|
+
then a braced body.
|
|
105
|
+
- **A label** is a string or an identifier written between the block type and the brace.
|
|
106
|
+
|
|
107
|
+
Labels come in two flavours:
|
|
108
|
+
|
|
109
|
+
| Where | Form | Notes |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| `component "name"`, `model "name"` | Static | The name must be a plain string or identifier. `${ }` is rejected: `a label cannot contain ${ }` |
|
|
112
|
+
| `part "name"` | An expression | Evaluated at build time, so a part in a loop can name itself: `part "leg_${i}"` |
|
|
113
|
+
|
|
114
|
+
Blocks that take no label reject one — `"box" does not take a label`. Two blocks accept a
|
|
115
|
+
label and quietly ignore it: [`align`](../reference/align.md) and
|
|
116
|
+
[component calls](../reference/component.md).
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## The newline rule
|
|
121
|
+
|
|
122
|
+
**Outside brackets, a line break ends an expression.** This is what lets attributes be
|
|
123
|
+
written one per line with no separators:
|
|
124
|
+
|
|
125
|
+
```hcl
|
|
126
|
+
rect {
|
|
127
|
+
size = [62, 13] // ends here
|
|
128
|
+
center = true // a separate attribute
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Without the rule, `size = [62, 13]` followed by `-4` on the next line would parse as a
|
|
133
|
+
subtraction rather than as the start of something new.
|
|
134
|
+
|
|
135
|
+
Inside `(...)`, `[...]` or a call's argument list, the rule is suspended and an expression may
|
|
136
|
+
span as many lines as it likes:
|
|
137
|
+
|
|
138
|
+
```hcl
|
|
139
|
+
polygon {
|
|
140
|
+
points = [
|
|
141
|
+
[0, 0], [26, 0],
|
|
142
|
+
[24, 6], [0, 74],
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
local total = (
|
|
147
|
+
var.wall * 2
|
|
148
|
+
+ var.gap
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
To continue an expression across lines at the top level, put the break **inside** brackets,
|
|
153
|
+
or end the line with an operator so the parser is still mid-expression:
|
|
154
|
+
|
|
155
|
+
```hcl
|
|
156
|
+
local a = base + // fine: the line ends mid-expression
|
|
157
|
+
extra
|
|
158
|
+
|
|
159
|
+
local b = base
|
|
160
|
+
+ extra // NOT a continuation: `local b = base`, then a parse error
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Trailing commas are allowed in lists, objects and argument lists.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Grammar
|
|
168
|
+
|
|
169
|
+
An informal EBNF of the whole language. `IDENT`, `NUMBER` and `STRING` are the lexical forms
|
|
170
|
+
above.
|
|
171
|
+
|
|
172
|
+
```ebnf
|
|
173
|
+
document = { declaration } ;
|
|
174
|
+
|
|
175
|
+
declaration = param | local | component | model ;
|
|
176
|
+
param = "param" label ( "=" expression | body ) ;
|
|
177
|
+
local = "local" label "=" expression ;
|
|
178
|
+
component = "component" label body ;
|
|
179
|
+
model = "model" label body ;
|
|
180
|
+
|
|
181
|
+
body = "{" { entry } "}" ;
|
|
182
|
+
entry = param | local | for | if | attribute | block ;
|
|
183
|
+
attribute = IDENT "=" expression ;
|
|
184
|
+
block = IDENT { label } body ;
|
|
185
|
+
label = IDENT | STRING ;
|
|
186
|
+
|
|
187
|
+
for = "for" IDENT [ "," IDENT ] "in" expression body ;
|
|
188
|
+
if = "if" expression body [ "else" ( if | body ) ] ;
|
|
189
|
+
|
|
190
|
+
expression = conditional ;
|
|
191
|
+
conditional = binary [ "?" expression ":" expression ] ;
|
|
192
|
+
binary = unary { binop unary } ; (* precedence climbing *)
|
|
193
|
+
unary = [ "-" | "!" ] unary | postfix ;
|
|
194
|
+
postfix = primary { "." IDENT | "[" expression "]" | arguments } ;
|
|
195
|
+
arguments = "(" [ expression { "," expression } [ "," ] ] ")" ;
|
|
196
|
+
primary = NUMBER | STRING | "true" | "false" | "null"
|
|
197
|
+
| IDENT | "(" expression ")" | array | object ;
|
|
198
|
+
array = "[" [ expression { "," expression } [ "," ] ] "]" ;
|
|
199
|
+
object = "{" { IDENT ( "=" | ":" ) expression [ "," ] } "}" ;
|
|
200
|
+
|
|
201
|
+
binop = "||" | "&&" | "==" | "!=" | "<" | "<=" | ">" | ">="
|
|
202
|
+
| "+" | "-" | "*" | "/" | "%" ;
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Two things the grammar cannot express:
|
|
206
|
+
|
|
207
|
+
- A `param` written in braced form may contain attributes only. Nested blocks inside it are
|
|
208
|
+
an error: `a param block holds attributes, not nested blocks`.
|
|
209
|
+
- Whether `IDENT` begins an attribute or a block is decided by one token of lookahead: `=`
|
|
210
|
+
means attribute, anything else means block.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# `align`
|
|
2
|
+
|
|
3
|
+
Moves geometry so a measured feature of its bounding box lands where you say.
|
|
4
|
+
|
|
5
|
+
```hcl
|
|
6
|
+
align {
|
|
7
|
+
x = "center"
|
|
8
|
+
y = "center"
|
|
9
|
+
extrude { height = 20 rounded_rect { size = [62, 13] radius = 6.5 } }
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`align` is the only block that **measures**. Every other operation builds its node from its
|
|
14
|
+
attributes alone; this one has to solve its child first, because where the geometry actually
|
|
15
|
+
is cannot be read off the tree.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Reference
|
|
20
|
+
|
|
21
|
+
### `align { ... }`
|
|
22
|
+
|
|
23
|
+
Unions its children, measures the bounding box of the result, and translates it.
|
|
24
|
+
|
|
25
|
+
#### Attributes
|
|
26
|
+
|
|
27
|
+
| Attribute | Type | Default | Description |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `x` | `"min"` `"center"` `"max"` or a number | untouched | What to do along X |
|
|
30
|
+
| `y` | same | untouched | Along Y |
|
|
31
|
+
| `z` | same | untouched | Along Z. 3D children only |
|
|
32
|
+
|
|
33
|
+
An axis that is not given is left alone.
|
|
34
|
+
|
|
35
|
+
#### What each value means
|
|
36
|
+
|
|
37
|
+
| Value | Effect |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `"min"` | The **lowest** face on that axis moves to 0 |
|
|
40
|
+
| `"center"` | The **centre** of the bounding box moves to 0 |
|
|
41
|
+
| `"max"` | The **highest** face moves to 0 |
|
|
42
|
+
| a number *n* | The **lowest** face moves to *n* |
|
|
43
|
+
|
|
44
|
+
Note the asymmetry: the keywords place a named feature at the origin, while a number places
|
|
45
|
+
the *minimum* face at that coordinate. `z = 5` means "sit this on a surface at height 5".
|
|
46
|
+
|
|
47
|
+
#### Returns
|
|
48
|
+
|
|
49
|
+
The children, unioned and translated. Dimensionality passes through — a 2D child accepts
|
|
50
|
+
`x` and `y` only.
|
|
51
|
+
|
|
52
|
+
#### Caveats
|
|
53
|
+
|
|
54
|
+
- **`align` needs a kernel context.** It is the one block that cannot be evaluated without
|
|
55
|
+
one: `align needs a kernel context — render the model rather than building it`. Building a
|
|
56
|
+
node tree with a bare `Evaluator` and no context will fail on it.
|
|
57
|
+
- **It costs a kernel round-trip.** The child has to be solved before the alignment can be
|
|
58
|
+
computed, so the subtree is evaluated at build time rather than lazily. The digest cache
|
|
59
|
+
means it is solved only once, but an `align` around something expensive is not free.
|
|
60
|
+
- It measures the **bounding box**, not the geometry. For anything that is not
|
|
61
|
+
axis-aligned, the box is larger than the shape.
|
|
62
|
+
- Children are unioned first, so several blocks inside one `align` move together.
|
|
63
|
+
- A label — `align "name" { }` — is accepted and silently ignored.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
### Centring something that was not built centred
|
|
70
|
+
|
|
71
|
+
The common case. Many profiles are easier to write from a corner, and then need centring:
|
|
72
|
+
|
|
73
|
+
```hcl
|
|
74
|
+
align {
|
|
75
|
+
x = "center"
|
|
76
|
+
y = "center"
|
|
77
|
+
extrude {
|
|
78
|
+
height = 20
|
|
79
|
+
rounded_rect { size = [62, 13] radius = 6.5 } // built in the positive quadrant
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Most shapes take a `center = true` attribute, which is cheaper — it changes how the shape is
|
|
85
|
+
built rather than measuring it afterwards. Reach for `align` when the thing being centred is
|
|
86
|
+
a **composition** rather than a single shape, and there is no one attribute to set.
|
|
87
|
+
|
|
88
|
+
### Sitting something on a surface
|
|
89
|
+
|
|
90
|
+
`z = n` puts the bottom face at *n*:
|
|
91
|
+
|
|
92
|
+
```hcl
|
|
93
|
+
align { z = 0 imported_or_composed_thing { } } // stand it on the build plate
|
|
94
|
+
align { z = 12 pin { } } // sit it on top of a 12 mm plate
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This is how to stack parts without knowing their heights:
|
|
98
|
+
|
|
99
|
+
```hcl
|
|
100
|
+
model "stack" {
|
|
101
|
+
part "base" { align { z = 0 base { } } }
|
|
102
|
+
part "mid" { align { z = 10 mid { } } }
|
|
103
|
+
part "top" { align { z = 26 top { } } }
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Aligning faces
|
|
108
|
+
|
|
109
|
+
`"max"` and `"min"` put a face on the origin, which is what you want for flush edges:
|
|
110
|
+
|
|
111
|
+
```hcl
|
|
112
|
+
align { x = "max" bracket { } } // right-hand face on the YZ plane
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Mixing axes
|
|
116
|
+
|
|
117
|
+
Each axis is independent, and an axis you do not mention is untouched:
|
|
118
|
+
|
|
119
|
+
```hcl
|
|
120
|
+
align {
|
|
121
|
+
x = "center" // centred left-to-right
|
|
122
|
+
z = 0 // standing on the plate
|
|
123
|
+
// y is left exactly where it was
|
|
124
|
+
composed_thing { }
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## `align` versus `center` versus `translate`
|
|
131
|
+
|
|
132
|
+
| Approach | When |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| `center = true` on the shape | A single primitive. Free — it changes how the shape is built |
|
|
135
|
+
| `translate` by a known amount | You already know the dimensions, usually because they are params |
|
|
136
|
+
| `align` | The extent is **computed** — a hull, a composition, an offset outline — and writing the arithmetic would mean duplicating it |
|
|
137
|
+
|
|
138
|
+
A model that knows its own dimensions should use them:
|
|
139
|
+
|
|
140
|
+
```hcl
|
|
141
|
+
param width = 62
|
|
142
|
+
|
|
143
|
+
// ✓ no measurement needed
|
|
144
|
+
translate { offset = [-var.width / 2, 0, 0] extrude { height = 20 rect { size = [var.width, 13] } } }
|
|
145
|
+
|
|
146
|
+
// also fine, and does not need to know the width
|
|
147
|
+
align { x = "center" extrude { height = 20 rect { size = [var.width, 13] } } }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The second reads better and survives a change to how the profile is built; the first avoids a
|
|
151
|
+
kernel round-trip. For anything small, prefer clarity.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Troubleshooting
|
|
156
|
+
|
|
157
|
+
### `align needs a kernel context — render the model rather than building it`
|
|
158
|
+
|
|
159
|
+
An `Evaluator` was constructed without a `context`, and the document contains an `align`.
|
|
160
|
+
[`render`](../api/render.md) always supplies one, so this only comes up when driving the
|
|
161
|
+
evaluator directly:
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
const evaluator = new Evaluator(program); // ✗ no context
|
|
165
|
+
const evaluator = new Evaluator(program, { context }); // ✓
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
[`describeParameters`](../api/describe-parameters.md) does not evaluate geometry, so it is
|
|
169
|
+
unaffected.
|
|
170
|
+
|
|
171
|
+
### `align: "x" must be "min", "center", "max" or a number`
|
|
172
|
+
|
|
173
|
+
The value is a string that is not one of the three keywords — `"middle"` and `"centre"` are
|
|
174
|
+
the usual culprits. The spelling is `"center"`.
|
|
175
|
+
|
|
176
|
+
### `align needs at least one shape inside it`
|
|
177
|
+
|
|
178
|
+
The body produced nothing — an `if` that took no branch, or a `for` over an empty list.
|
|
179
|
+
|
|
180
|
+
### `align: unknown attribute "z"` on a 2D shape
|
|
181
|
+
|
|
182
|
+
A cross-section has no Z. Align it before extruding using `x` and `y`, or after extruding
|
|
183
|
+
using all three.
|
|
184
|
+
|
|
185
|
+
### My model got slower after adding `align`
|
|
186
|
+
|
|
187
|
+
Each `align` solves its child at evaluation time. Around a cheap shape that is nothing;
|
|
188
|
+
around a hull of a hundred spheres it is the whole cost of the model, paid before anything
|
|
189
|
+
else starts. Align the small thing, or translate by a known amount instead.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Booleans
|
|
2
|
+
|
|
3
|
+
The three constructive solid geometry operations. Each takes its children as a **list**
|
|
4
|
+
rather than unioning them first, which is what makes `difference` meaningful.
|
|
5
|
+
|
|
6
|
+
All three work in 2D and 3D, but every child must be the same dimensionality:
|
|
7
|
+
`"union" cannot mix 2D and 3D shapes`.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## `union`
|
|
12
|
+
|
|
13
|
+
Everything added together.
|
|
14
|
+
|
|
15
|
+
```hcl
|
|
16
|
+
union {
|
|
17
|
+
box { size = [40, 20, 10] }
|
|
18
|
+
translate { offset = [0, 0, 10] cylinder { radius = 8 height = 6 } }
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
#### Attributes
|
|
23
|
+
|
|
24
|
+
None.
|
|
25
|
+
|
|
26
|
+
#### Caveats
|
|
27
|
+
|
|
28
|
+
- **Usually unnecessary.** Sibling blocks are already implicitly unioned, so
|
|
29
|
+
`part "p" { a b }` and `part "p" { union { a b } }` are the same. Write `union`
|
|
30
|
+
explicitly when it makes the grouping clearer — typically as one operand of a
|
|
31
|
+
`difference`.
|
|
32
|
+
- The order of the children does not affect the result, and does not affect the digest
|
|
33
|
+
either: union members are sorted by subtree size before hashing, so two unions written in a
|
|
34
|
+
different order share a cache entry.
|
|
35
|
+
- Empty children are dropped. A union of nothing is empty geometry rather than an error.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## `difference`
|
|
40
|
+
|
|
41
|
+
The first child, with every later child cut out of it.
|
|
42
|
+
|
|
43
|
+
```hcl
|
|
44
|
+
difference {
|
|
45
|
+
cylinder { radius = 40 height = 8 } // the body
|
|
46
|
+
|
|
47
|
+
translate { offset = [0, 0, -1] // everything after it is removed
|
|
48
|
+
cylinder { radius = 12 height = 10 } }
|
|
49
|
+
|
|
50
|
+
for i in range(0, 6) {
|
|
51
|
+
rotate { z = i * 60
|
|
52
|
+
translate { offset = [30, 0, -1] cylinder { radius = 3 height = 10 } } }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Attributes
|
|
58
|
+
|
|
59
|
+
None.
|
|
60
|
+
|
|
61
|
+
#### Caveats
|
|
62
|
+
|
|
63
|
+
- **Order matters, and only here.** The first child is the body; every later one is a cutter.
|
|
64
|
+
This is the one boolean whose children are never reordered, for exactly that reason.
|
|
65
|
+
- A `for` loop inside a difference contributes one cutter per iteration, which is the normal
|
|
66
|
+
way to make an array of holes.
|
|
67
|
+
- Subtracting everything leaves empty geometry rather than an error. A part that ends up
|
|
68
|
+
empty renders as nothing.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## `intersection`
|
|
73
|
+
|
|
74
|
+
Only what every child shares.
|
|
75
|
+
|
|
76
|
+
```hcl
|
|
77
|
+
intersection {
|
|
78
|
+
bottle_void { }
|
|
79
|
+
translate { offset = [0, 0, -1] cylinder { radius = 40 height = 100 } }
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### Attributes
|
|
84
|
+
|
|
85
|
+
None.
|
|
86
|
+
|
|
87
|
+
#### Caveats
|
|
88
|
+
|
|
89
|
+
- With one child, it is that child. With none, it is an error —
|
|
90
|
+
`"intersection" needs at least one shape inside it`.
|
|
91
|
+
- Order does not affect the result.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Usage
|
|
96
|
+
|
|
97
|
+
### Hollowing a shell
|
|
98
|
+
|
|
99
|
+
A solid minus a slightly smaller copy of itself. Naming both as components keeps the two
|
|
100
|
+
profiles beside each other and lets the digest cache solve each once even when several parts
|
|
101
|
+
need them:
|
|
102
|
+
|
|
103
|
+
```hcl
|
|
104
|
+
component "bottle_solid" { revolve { polygon { points = silhouette } } }
|
|
105
|
+
component "bottle_void" { revolve { polygon { points = cavity } } }
|
|
106
|
+
|
|
107
|
+
model "bottle" {
|
|
108
|
+
part "bottle" {
|
|
109
|
+
difference {
|
|
110
|
+
bottle_solid { }
|
|
111
|
+
bottle_void { }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
part "water" {
|
|
116
|
+
intersection { // the same void, reused for free
|
|
117
|
+
bottle_void { }
|
|
118
|
+
translate { offset = [0, 0, -1] cylinder { radius = 40 height = 130 } }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Arrays of holes
|
|
125
|
+
|
|
126
|
+
A loop inside a `difference` is the standard bolt pattern:
|
|
127
|
+
|
|
128
|
+
```hcl
|
|
129
|
+
difference {
|
|
130
|
+
extrude { height = 8 circle { radius = 40 } }
|
|
131
|
+
|
|
132
|
+
for i in range(0, var.bolts) {
|
|
133
|
+
rotate {
|
|
134
|
+
z = i * 360 / var.bolts
|
|
135
|
+
translate { offset = [31, 0, -1] cylinder { radius = 3 height = 10 } }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Trimming to a region
|
|
142
|
+
|
|
143
|
+
`intersection` with a large primitive is how a shape is cropped:
|
|
144
|
+
|
|
145
|
+
```hcl
|
|
146
|
+
intersection {
|
|
147
|
+
complicated_thing { }
|
|
148
|
+
translate { offset = [-50, -50, 0] box { size = [100, 100, 12] } } // keep 0 ≤ z ≤ 12
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
For a single flat cut, [`trim`](refinement.md#trim) is cheaper — it cuts with a half-space
|
|
153
|
+
and needs no second solid.
|
|
154
|
+
|
|
155
|
+
### Grouping so a transform applies to several shapes
|
|
156
|
+
|
|
157
|
+
Every operation *except* these three unions its children first, so this already works:
|
|
158
|
+
|
|
159
|
+
```hcl
|
|
160
|
+
translate {
|
|
161
|
+
offset = [0, 0, 10]
|
|
162
|
+
box { size = [4, 4, 4] }
|
|
163
|
+
sphere { radius = 3 } // both move together
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Inside a `difference`, though, each child is separate — so to subtract two shapes *as one
|
|
168
|
+
body*, group them explicitly:
|
|
169
|
+
|
|
170
|
+
```hcl
|
|
171
|
+
difference {
|
|
172
|
+
plate { }
|
|
173
|
+
union { // one cutter made of two shapes
|
|
174
|
+
cylinder { radius = 4 height = 20 }
|
|
175
|
+
translate { offset = [0, 0, 6] cylinder { radius = 7 height = 20 } }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Here the `union` is not decorative: without it the two cylinders would be two separate
|
|
181
|
+
cutters, which happens to give the same result for a difference — but not for an
|
|
182
|
+
`intersection`, where it changes the answer entirely.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Troubleshooting
|
|
187
|
+
|
|
188
|
+
### `"difference" needs at least one shape inside it`
|
|
189
|
+
|
|
190
|
+
The body is empty, or everything in it was produced by an `if` that took no branch or a `for`
|
|
191
|
+
over an empty list.
|
|
192
|
+
|
|
193
|
+
### `"union" cannot mix 2D and 3D shapes`
|
|
194
|
+
|
|
195
|
+
One child is a cross-section and another is a solid. Usually a profile that was meant to be
|
|
196
|
+
extruded:
|
|
197
|
+
|
|
198
|
+
```hcl
|
|
199
|
+
union {
|
|
200
|
+
box { size = [10, 10, 10] }
|
|
201
|
+
circle { radius = 3 } // ✗ still 2D
|
|
202
|
+
extrude { height = 10 circle { radius = 3 } } // ✓
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### My difference removed nothing
|
|
207
|
+
|
|
208
|
+
The cutter is not where you think, or does not overlap the body. Check by rendering the
|
|
209
|
+
cutter as its own [`part`](part.md) with an opacity, which shows exactly where it sits:
|
|
210
|
+
|
|
211
|
+
```hcl
|
|
212
|
+
part "cutter" { color = "#ff6b7f" opacity = 0.4 the_cutter { } }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### The result has stray faces where two solids meet
|
|
216
|
+
|
|
217
|
+
Coincident surfaces. Overlap unioned solids slightly, and overshoot cutters past both faces
|
|
218
|
+
they pass through.
|
|
219
|
+
|
|
220
|
+
### My difference is slow
|
|
221
|
+
|
|
222
|
+
Each cutter is a boolean. A hundred of them in one `difference` is a hundred kernel
|
|
223
|
+
operations; unioning the cutters first is often faster, because the union of many small
|
|
224
|
+
disjoint solids is cheap and the subtraction then happens once:
|
|
225
|
+
|
|
226
|
+
```hcl
|
|
227
|
+
difference {
|
|
228
|
+
body { }
|
|
229
|
+
union { for i in range(0, 100) { ... } }
|
|
230
|
+
}
|
|
231
|
+
```
|