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,279 @@
|
|
|
1
|
+
# 3D shapes
|
|
2
|
+
|
|
3
|
+
Five leaf blocks producing solids. They take attributes and never contain other blocks.
|
|
4
|
+
|
|
5
|
+
| Block | Sized by | Centred by default |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| [`box`](#box) | `size` | no |
|
|
8
|
+
| [`sphere`](#sphere) | `radius` or `diameter` | always |
|
|
9
|
+
| [`cylinder`](#cylinder) | `height` and one or two radii | no |
|
|
10
|
+
| [`cone`](#cone) | `height`, `radius`, `top_radius` | no |
|
|
11
|
+
| [`torus`](#torus) | `radius`, `tube_radius` | always |
|
|
12
|
+
|
|
13
|
+
Anything these cannot express is usually a 2D profile plus
|
|
14
|
+
[`extrude`](conversions.md#extrude) or [`revolve`](conversions.md#revolve) — that is where
|
|
15
|
+
the real modelling happens.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## `box`
|
|
20
|
+
|
|
21
|
+
An axis-aligned cuboid.
|
|
22
|
+
|
|
23
|
+
```hcl
|
|
24
|
+
box { size = [40, 20, 10] }
|
|
25
|
+
box { size = [40, 20, 10] center = true }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
#### Attributes
|
|
29
|
+
|
|
30
|
+
| Attribute | Type | Default | Description |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| `size` | 3-vector | **required** | X, Y and Z extents |
|
|
33
|
+
| `center` | bool | `false` | `true` centres it on the origin; otherwise its minimum corner sits there |
|
|
34
|
+
|
|
35
|
+
#### Caveats
|
|
36
|
+
|
|
37
|
+
- A single number is accepted as shorthand for all three, so `size = 10` is a 10 mm cube.
|
|
38
|
+
- There is no rounded box. Round a 2D profile and extrude it —
|
|
39
|
+
see [Usage](#rounded-boxes).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## `sphere`
|
|
44
|
+
|
|
45
|
+
```hcl
|
|
46
|
+
sphere { radius = 12 }
|
|
47
|
+
sphere { diameter = 24 segments = 64 }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### Attributes
|
|
51
|
+
|
|
52
|
+
| Attribute | Type | Default | Description |
|
|
53
|
+
| --- | --- | --- | --- |
|
|
54
|
+
| `radius` | number | `1` | |
|
|
55
|
+
| `diameter` | number | — | An alternative spelling of the same thing |
|
|
56
|
+
| `segments` | integer | `0` | Segments around the equator; `0` lets the kernel choose |
|
|
57
|
+
|
|
58
|
+
#### Caveats
|
|
59
|
+
|
|
60
|
+
- Give `radius` **or** `diameter`, never both.
|
|
61
|
+
- Always centred on the origin.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## `cylinder`
|
|
66
|
+
|
|
67
|
+
A cylinder, or a frustum when the two ends differ.
|
|
68
|
+
|
|
69
|
+
```hcl
|
|
70
|
+
cylinder { radius = 5 height = 20 }
|
|
71
|
+
cylinder { diameter = 10 height = 20 center = true }
|
|
72
|
+
cylinder { bottom_radius = 12 top_radius = 8 height = 15 }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### Attributes
|
|
76
|
+
|
|
77
|
+
| Attribute | Type | Default | Description |
|
|
78
|
+
| --- | --- | --- | --- |
|
|
79
|
+
| `height` | number | **required** | Along Z |
|
|
80
|
+
| `radius` | number | — | Both ends alike |
|
|
81
|
+
| `diameter` | number | — | An alternative spelling of `radius` |
|
|
82
|
+
| `bottom_radius` | number | — | The end at Z = 0 |
|
|
83
|
+
| `top_radius` | number | — | The end at Z = `height` |
|
|
84
|
+
| `segments` | integer | `0` | Segments around; `0` lets the kernel choose |
|
|
85
|
+
| `center` | bool | `false` | `true` centres it on Z = 0; otherwise it stands on the XY plane |
|
|
86
|
+
|
|
87
|
+
#### Caveats
|
|
88
|
+
|
|
89
|
+
- Give **either** `radius`/`diameter`, **or both** `bottom_radius` and `top_radius`. Anything
|
|
90
|
+
else is `cylinder: give "radius"/"diameter", or both "bottom_radius" and "top_radius"` —
|
|
91
|
+
one radius on its own is ambiguous, and there is no default.
|
|
92
|
+
- Unlike every other shape, `radius` has no usable default here: the check runs before the
|
|
93
|
+
fallback would apply.
|
|
94
|
+
- `center` affects Z only. X and Y are always centred on the axis.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## `cone`
|
|
99
|
+
|
|
100
|
+
A cone, or a truncated cone.
|
|
101
|
+
|
|
102
|
+
```hcl
|
|
103
|
+
cone { radius = 10 height = 20 }
|
|
104
|
+
cone { radius = 10 top_radius = 4 height = 20 }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### Attributes
|
|
108
|
+
|
|
109
|
+
| Attribute | Type | Default | Description |
|
|
110
|
+
| --- | --- | --- | --- |
|
|
111
|
+
| `height` | number | **required** | Along Z |
|
|
112
|
+
| `radius` | number | `1` | The base, at Z = 0 |
|
|
113
|
+
| `diameter` | number | — | An alternative spelling of `radius` |
|
|
114
|
+
| `top_radius` | number | `0` | `0` gives a point |
|
|
115
|
+
| `segments` | integer | `0` | Segments around |
|
|
116
|
+
| `center` | bool | `false` | As for [`cylinder`](#cylinder) |
|
|
117
|
+
|
|
118
|
+
#### Caveats
|
|
119
|
+
|
|
120
|
+
- `cone` and `cylinder` build the same kind of solid; they differ only in which attributes
|
|
121
|
+
are required and what `top_radius` defaults to. Use whichever reads better — `cone` when
|
|
122
|
+
the top is a point, `cylinder` when both ends matter.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## `torus`
|
|
127
|
+
|
|
128
|
+
A ring, optionally a partial arc.
|
|
129
|
+
|
|
130
|
+
```hcl
|
|
131
|
+
torus { radius = 20 tube_radius = 4 }
|
|
132
|
+
torus { radius = 20 tube_radius = 4 angle = 180 }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Attributes
|
|
136
|
+
|
|
137
|
+
| Attribute | Type | Default | Description |
|
|
138
|
+
| --- | --- | --- | --- |
|
|
139
|
+
| `radius` | number | **required** | Centre of the ring to centre of the tube |
|
|
140
|
+
| `tube_radius` | number | **required** | Radius of the tube itself |
|
|
141
|
+
| `angle` | angle | `360` | How far round to sweep, in degrees |
|
|
142
|
+
| `segments` | integer | `0` | Segments around the ring |
|
|
143
|
+
| `tube_segments` | integer | `0` | Segments around the tube's cross-section |
|
|
144
|
+
|
|
145
|
+
#### Caveats
|
|
146
|
+
|
|
147
|
+
- Built as a circle pushed out to `radius` and revolved, which is what a torus is. The outer
|
|
148
|
+
radius of the result is `radius + tube_radius`.
|
|
149
|
+
- `tube_radius` greater than `radius` produces self-intersecting geometry. The kernel will
|
|
150
|
+
resolve it into *something*, but not anything you meant.
|
|
151
|
+
- An `angle` of `0` or less yields empty geometry rather than an error, so a parameter
|
|
152
|
+
sweeping to zero does not break the model.
|
|
153
|
+
- Always centred on the origin, lying in the XY plane.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Usage
|
|
158
|
+
|
|
159
|
+
### Rounded boxes
|
|
160
|
+
|
|
161
|
+
There is no rounded-box primitive. Round the 2D profile and extrude it — which also gives
|
|
162
|
+
control over whether the vertical edges or the horizontal ones are rounded:
|
|
163
|
+
|
|
164
|
+
```hcl
|
|
165
|
+
component "rounded_box" {
|
|
166
|
+
param size = [20, 20, 10]
|
|
167
|
+
param radius = 3
|
|
168
|
+
|
|
169
|
+
extrude {
|
|
170
|
+
height = size.z
|
|
171
|
+
rounded_rect { size = [size.x, size.y] radius = radius center = true }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
For rounding in all three axes, [`smooth`](refinement.md#smooth) or a
|
|
177
|
+
[`hull`](refinement.md#hull) of eight spheres are the usual approaches.
|
|
178
|
+
|
|
179
|
+
### Through-holes
|
|
180
|
+
|
|
181
|
+
Make the cutter longer than the thing it cuts, and start it below:
|
|
182
|
+
|
|
183
|
+
```hcl
|
|
184
|
+
difference {
|
|
185
|
+
cylinder { radius = 40 height = 8 }
|
|
186
|
+
|
|
187
|
+
translate {
|
|
188
|
+
offset = [0, 0, -1] // start below the bottom face
|
|
189
|
+
cylinder { radius = 12 height = 10 } // and finish above the top
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
A cutter that ends exactly on a face leaves a zero-thickness coincident surface, which is
|
|
195
|
+
slow to resolve and can produce artefacts. One millimetre of overshoot at each end costs
|
|
196
|
+
nothing.
|
|
197
|
+
|
|
198
|
+
### Bolt circles
|
|
199
|
+
|
|
200
|
+
```hcl
|
|
201
|
+
for i in range(0, var.bolts) {
|
|
202
|
+
rotate {
|
|
203
|
+
z = i * 360 / var.bolts
|
|
204
|
+
translate { offset = [var.plate_r - 9, 0, -1] cylinder { radius = 3 height = 10 } }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Rings and fillets from a torus
|
|
210
|
+
|
|
211
|
+
A torus is the cheapest way to get a circular fillet or a raised thread ridge:
|
|
212
|
+
|
|
213
|
+
```hcl
|
|
214
|
+
for i in range(0, 3) {
|
|
215
|
+
translate {
|
|
216
|
+
offset = [0, 0, 174 + i * 4.5]
|
|
217
|
+
torus { radius = 15.6 tube_radius = 1.1 segments = 96 tube_segments = 16 }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Controlling facet count
|
|
223
|
+
|
|
224
|
+
`segments` is per shape, and it is worth setting on anything that must fit something else:
|
|
225
|
+
|
|
226
|
+
```hcl
|
|
227
|
+
param preview { type = bool default = true }
|
|
228
|
+
local facets = var.preview ? 24 : 128
|
|
229
|
+
|
|
230
|
+
model "m" {
|
|
231
|
+
cylinder { radius = 10 height = 20 segments = facets }
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Globally, [`setQuality`](../api/kernel.md#setquality) moves the default that `segments = 0`
|
|
236
|
+
resolves to.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Troubleshooting
|
|
241
|
+
|
|
242
|
+
### `cylinder: give "radius"/"diameter", or both "bottom_radius" and "top_radius"`
|
|
243
|
+
|
|
244
|
+
Either one radius for both ends, or two for a frustum. Giving only `bottom_radius` leaves the
|
|
245
|
+
top undefined:
|
|
246
|
+
|
|
247
|
+
```hcl
|
|
248
|
+
cylinder { height = 10 bottom_radius = 5 } // ✗
|
|
249
|
+
cylinder { height = 10 bottom_radius = 5 top_radius = 5 } // ✓
|
|
250
|
+
cylinder { height = 10 radius = 5 } // ✓ simpler
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### `sphere: give either "radius" or "diameter", not both`
|
|
254
|
+
|
|
255
|
+
Pass one.
|
|
256
|
+
|
|
257
|
+
### `"box" is a shape and cannot contain other blocks`
|
|
258
|
+
|
|
259
|
+
A shape is a leaf. Usually a missing `}` on an earlier line has swallowed the next block.
|
|
260
|
+
|
|
261
|
+
### `box: "size" must be a number or an array of 3 numbers`
|
|
262
|
+
|
|
263
|
+
`size` needs three components in 3D. A 2-vector is a common slip when converting a profile:
|
|
264
|
+
|
|
265
|
+
```hcl
|
|
266
|
+
box { size = [40, 20] } // ✗
|
|
267
|
+
box { size = [40, 20, 10] } // ✓
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### My cylinder is not where I expect
|
|
271
|
+
|
|
272
|
+
By default a cylinder stands **on** the XY plane, with its base at Z = 0. `center = true`
|
|
273
|
+
puts its middle there instead. The same is true of `box`, whose minimum corner — not its
|
|
274
|
+
centre — sits at the origin by default.
|
|
275
|
+
|
|
276
|
+
### Two solids that touch exactly render with artefacts
|
|
277
|
+
|
|
278
|
+
Coincident faces are ambiguous. Overlap them by a small amount when unioning, and overshoot
|
|
279
|
+
by a small amount when subtracting.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Transforms
|
|
2
|
+
|
|
3
|
+
Four operations that move geometry without changing its shape. Each works in 2D and 3D, and
|
|
4
|
+
passes its children's dimensionality through.
|
|
5
|
+
|
|
6
|
+
All four **union their children first**, so a transform applied to several blocks moves them
|
|
7
|
+
together:
|
|
8
|
+
|
|
9
|
+
```hcl
|
|
10
|
+
translate {
|
|
11
|
+
offset = [0, 0, 10]
|
|
12
|
+
box { size = [4, 4, 4] }
|
|
13
|
+
sphere { radius = 3 } // both move
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Nested transforms collapse into a single matrix before they reach the kernel, so a deep stack
|
|
18
|
+
of `translate` and `rotate` blocks costs one operation rather than one per level.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## `translate`
|
|
23
|
+
|
|
24
|
+
Moves geometry.
|
|
25
|
+
|
|
26
|
+
```hcl
|
|
27
|
+
translate { offset = [10, 0, 5] box { size = [4, 4, 4] } }
|
|
28
|
+
translate { offset = [10, 0] rect { size = [4, 4] } } // 2D
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
#### Attributes
|
|
32
|
+
|
|
33
|
+
| Attribute | Type | Default | Description |
|
|
34
|
+
| --- | --- | --- | --- |
|
|
35
|
+
| `offset` | vector of the child's dimensionality | **required** | How far to move along each axis |
|
|
36
|
+
|
|
37
|
+
#### Caveats
|
|
38
|
+
|
|
39
|
+
- `offset` must have as many components as the child has dimensions — 2 for a cross-section,
|
|
40
|
+
3 for a solid. A mismatch is `translate: "offset" must be a number or an array of 3
|
|
41
|
+
numbers`.
|
|
42
|
+
- A single number broadcasts to every axis, which is rarely what you want here but is
|
|
43
|
+
consistent with every other vector attribute.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## `rotate`
|
|
48
|
+
|
|
49
|
+
Turns geometry about the origin. **All angles are in degrees.**
|
|
50
|
+
|
|
51
|
+
```hcl
|
|
52
|
+
rotate { z = 45 box { size = [20, 4, 4] } } // 3D, one axis
|
|
53
|
+
rotate { angles = [0, 90, 45] box { size = [20, 4, 4] } } // 3D, all three
|
|
54
|
+
rotate { angle = 30 rect { size = [20, 4] } } // 2D
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Attributes — 3D children
|
|
58
|
+
|
|
59
|
+
| Attribute | Type | Default | Description |
|
|
60
|
+
| --- | --- | --- | --- |
|
|
61
|
+
| `angles` | 3-vector | — | Degrees about X, Y and Z at once |
|
|
62
|
+
| `x`, `y`, `z` | number | `0` | Degrees about that axis, named one at a time |
|
|
63
|
+
|
|
64
|
+
#### Attributes — 2D children
|
|
65
|
+
|
|
66
|
+
| Attribute | Type | Default | Description |
|
|
67
|
+
| --- | --- | --- | --- |
|
|
68
|
+
| `angle` | angle | **required** | Degrees, counter-clockwise |
|
|
69
|
+
|
|
70
|
+
#### Caveats
|
|
71
|
+
|
|
72
|
+
- **Rotation is about the origin, not about the geometry.** To spin something in place, put
|
|
73
|
+
it at the origin, rotate, then translate — or build it centred in the first place.
|
|
74
|
+
- In 3D the axes are applied **X, then Y, then Z**, matching OpenSCAD, so a model ported from
|
|
75
|
+
there lands in the same orientation. Composing rotations in a different order requires
|
|
76
|
+
separate `rotate` blocks.
|
|
77
|
+
- `angles` and the named axes are alternatives. Using `angle` on a 3D child, or `x`/`y`/`z`
|
|
78
|
+
on a 2D one, is an unknown attribute.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## `scale`
|
|
83
|
+
|
|
84
|
+
Scales geometry about the origin.
|
|
85
|
+
|
|
86
|
+
```hcl
|
|
87
|
+
scale { factor = 2 sphere { radius = 5 } }
|
|
88
|
+
scale { factor = [1, 1, 0.5] sphere { radius = 5 } } // flattened
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Attributes
|
|
92
|
+
|
|
93
|
+
| Attribute | Type | Default | Description |
|
|
94
|
+
| --- | --- | --- | --- |
|
|
95
|
+
| `factor` | vector of the child's dimensionality | **required** | Multiplier per axis. A single number scales every axis alike |
|
|
96
|
+
|
|
97
|
+
#### Caveats
|
|
98
|
+
|
|
99
|
+
- Scaling is about the origin, so anything not centred there also moves.
|
|
100
|
+
- A negative factor mirrors as well as scales, and flips the winding. Prefer
|
|
101
|
+
[`mirror`](#mirror) when reflection is what you mean.
|
|
102
|
+
- A factor of `0` collapses the geometry to nothing.
|
|
103
|
+
- Non-uniform scaling of a curved surface changes its radii — a scaled sphere is an
|
|
104
|
+
ellipsoid, which is the cheapest way to get one.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## `mirror`
|
|
109
|
+
|
|
110
|
+
Reflects geometry in the plane through the origin with the given normal.
|
|
111
|
+
|
|
112
|
+
```hcl
|
|
113
|
+
mirror { normal = [1, 0, 0] bracket { } } // reflect across the YZ plane
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### Attributes
|
|
117
|
+
|
|
118
|
+
| Attribute | Type | Default | Description |
|
|
119
|
+
| --- | --- | --- | --- |
|
|
120
|
+
| `normal` | vector of the child's dimensionality | **required** | The normal of the mirror plane. Normalised first, so its length does not matter |
|
|
121
|
+
|
|
122
|
+
#### Caveats
|
|
123
|
+
|
|
124
|
+
- The plane always passes through the **origin**. To mirror about some other plane,
|
|
125
|
+
translate to the origin, mirror, and translate back.
|
|
126
|
+
- In 2D, `normal` describes a line rather than a plane.
|
|
127
|
+
- `mirror` replaces the geometry with its reflection; it does not keep the original. For a
|
|
128
|
+
symmetric pair, union both:
|
|
129
|
+
|
|
130
|
+
```hcl
|
|
131
|
+
union {
|
|
132
|
+
bracket { }
|
|
133
|
+
mirror { normal = [1, 0, 0] bracket { } }
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Usage
|
|
140
|
+
|
|
141
|
+
### Rotating in place
|
|
142
|
+
|
|
143
|
+
The origin is the pivot, so centre first:
|
|
144
|
+
|
|
145
|
+
```hcl
|
|
146
|
+
// ✗ the box sweeps around the origin
|
|
147
|
+
rotate { z = 30 box { size = [40, 10, 4] } }
|
|
148
|
+
|
|
149
|
+
// ✓ centred, so it turns on the spot
|
|
150
|
+
rotate { z = 30 box { size = [40, 10, 4] center = true } }
|
|
151
|
+
|
|
152
|
+
// ✓ or move it back afterwards
|
|
153
|
+
translate { offset = [20, 5, 0] rotate { z = 30 box { size = [40, 10, 4] center = true } } }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Order of operations
|
|
157
|
+
|
|
158
|
+
Blocks nest inside-out: the transform closest to the shape is applied first.
|
|
159
|
+
|
|
160
|
+
```hcl
|
|
161
|
+
translate { // …then moved 50 along X
|
|
162
|
+
offset = [50, 0, 0]
|
|
163
|
+
rotate { // the box is rotated first…
|
|
164
|
+
z = 45
|
|
165
|
+
box { size = [20, 4, 4] center = true }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Swapping the two gives a different result — rotating a translated box sweeps it around the
|
|
171
|
+
origin on an arc of radius 50.
|
|
172
|
+
|
|
173
|
+
### Radial patterns
|
|
174
|
+
|
|
175
|
+
Rotate a translated copy, once per step:
|
|
176
|
+
|
|
177
|
+
```hcl
|
|
178
|
+
for i in range(0, 8) {
|
|
179
|
+
rotate {
|
|
180
|
+
z = i * 45
|
|
181
|
+
translate { offset = [30, 0, 0] cylinder { radius = 3 height = 10 } }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Because identity is content identity, this costs **one** cylinder and eight transforms — the
|
|
187
|
+
cylinder node is the same node every time round the loop.
|
|
188
|
+
|
|
189
|
+
### Mirrored pairs
|
|
190
|
+
|
|
191
|
+
```hcl
|
|
192
|
+
component "hinge_leaf" { ... }
|
|
193
|
+
|
|
194
|
+
model "hinge" {
|
|
195
|
+
part "left" { hinge_leaf { } }
|
|
196
|
+
part "right" { mirror { normal = [1, 0, 0] hinge_leaf { } } }
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Ellipsoids and tapers
|
|
201
|
+
|
|
202
|
+
Non-uniform `scale` is the simplest route to shapes the primitives do not offer:
|
|
203
|
+
|
|
204
|
+
```hcl
|
|
205
|
+
scale { factor = [1, 0.6, 1] sphere { radius = 20 segments = 64 } }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Troubleshooting
|
|
211
|
+
|
|
212
|
+
### `translate: "offset" must be a number or an array of 3 numbers`
|
|
213
|
+
|
|
214
|
+
The offset has the wrong number of components for its child. 2D children want two, 3D
|
|
215
|
+
children want three — and a 2D shape stays 2D until it is extruded:
|
|
216
|
+
|
|
217
|
+
```hcl
|
|
218
|
+
translate { offset = [1, 2, 3] rect { size = [4, 4] } } // ✗ rect is 2D
|
|
219
|
+
translate { offset = [1, 2] rect { size = [4, 4] } } // ✓
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### `rotate: unknown attribute "angle"`
|
|
223
|
+
|
|
224
|
+
`angle` is the 2D spelling. A 3D child takes `x`, `y`, `z` or `angles`:
|
|
225
|
+
|
|
226
|
+
```hcl
|
|
227
|
+
rotate { angle = 45 box { size = [1, 1, 1] } } // ✗
|
|
228
|
+
rotate { z = 45 box { size = [1, 1, 1] } } // ✓
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The reverse — `z` on a 2D child — fails the same way.
|
|
232
|
+
|
|
233
|
+
### `"translate" needs at least one shape inside it`
|
|
234
|
+
|
|
235
|
+
Transforms need something to act on. Often the body contains only a
|
|
236
|
+
[`part`](part.md), which contributes no geometry to its parent, or an `if` that took no
|
|
237
|
+
branch.
|
|
238
|
+
|
|
239
|
+
### My rotated part flew off across the scene
|
|
240
|
+
|
|
241
|
+
Rotation is about the origin. A shape 200 mm out rotates around a 200 mm arc. Centre it,
|
|
242
|
+
rotate, then move it where it belongs.
|
|
243
|
+
|
|
244
|
+
### My mirrored copy vanished
|
|
245
|
+
|
|
246
|
+
`mirror` replaces rather than adds. Union the original with the reflection if you want both.
|
package/src/reference.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reference manual, bundled and searchable.
|
|
3
|
+
*
|
|
4
|
+
* The manual is the language's own documentation, copied into the package by
|
|
5
|
+
* `scripts/sync-reference.mjs` rather than paraphrased, so an answer from here is the same
|
|
6
|
+
* answer a person reading the published docs would get. Search is deliberately simple —
|
|
7
|
+
* substring matching over headings and body text, ranked — because the corpus is 28 pages
|
|
8
|
+
* and anything cleverer would be a dependency and a source of surprise.
|
|
9
|
+
*/
|
|
10
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
11
|
+
import { join, relative } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
/** Where the copied manual lives inside the package. */
|
|
15
|
+
const ROOT = fileURLToPath(new URL('./reference/', import.meta.url));
|
|
16
|
+
|
|
17
|
+
/** How many characters of context a search hit carries. */
|
|
18
|
+
const SNIPPET = 320;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The manual's pages, in reading order rather than directory order.
|
|
22
|
+
*
|
|
23
|
+
* Written out because a listing would put `align` before `param`, which is not the order
|
|
24
|
+
* anyone learns this in. A page on disk that is missing from here is a failure the tests
|
|
25
|
+
* catch, so the list cannot silently fall behind the manual.
|
|
26
|
+
*
|
|
27
|
+
* @type {Array<{ path: string, title: string, about: string }>}
|
|
28
|
+
*/
|
|
29
|
+
export const PAGES = [
|
|
30
|
+
{ path: 'README.md', title: 'Reference index', about: 'Every construct, listed and linked' },
|
|
31
|
+
{ path: 'language/overview.md', title: 'Document structure', about: 'The four top-level blocks, the pipeline, nesting, dimensionality' },
|
|
32
|
+
{ path: 'language/syntax.md', title: 'Syntax', about: 'Comments, numbers, strings, identifiers, labels, the newline rule, the grammar' },
|
|
33
|
+
{ path: 'language/expressions.md', title: 'Expressions', about: 'Values, operators, truthiness, interpolation, member access' },
|
|
34
|
+
{ path: 'language/scope.md', title: 'Scope and names', about: 'What is in scope where, var., shadowing, component isolation' },
|
|
35
|
+
{ path: 'language/control-flow.md', title: 'Control flow', about: 'for and if / else if / else' },
|
|
36
|
+
{ path: 'reference/param.md', title: 'param', about: 'Inputs, their metadata, and what is advisory' },
|
|
37
|
+
{ path: 'reference/local.md', title: 'local', about: 'Named values, ordering, and how they differ from params' },
|
|
38
|
+
{ path: 'reference/component.md', title: 'component', about: 'Reusable parameterised geometry' },
|
|
39
|
+
{ path: 'reference/model.md', title: 'model', about: 'Renderable scenes, model params, several models in one document' },
|
|
40
|
+
{ path: 'reference/shapes-2d.md', title: '2D shapes', about: 'rect, rounded_rect, circle, ellipse, regular_polygon, stadium, polygon' },
|
|
41
|
+
{ path: 'reference/shapes-3d.md', title: '3D shapes', about: 'box, sphere, cylinder, cone, torus' },
|
|
42
|
+
{ path: 'reference/booleans.md', title: 'Booleans', about: 'union, difference, intersection' },
|
|
43
|
+
{ path: 'reference/transforms.md', title: 'Transforms', about: 'translate, rotate, scale, mirror' },
|
|
44
|
+
{ path: 'reference/conversions.md', title: '2D to 3D and back', about: 'extrude, revolve, project, slice' },
|
|
45
|
+
{ path: 'reference/refinement.md', title: 'Refinement', about: 'offset, hull, refine, simplify, smooth, trim' },
|
|
46
|
+
{ path: 'reference/align.md', title: 'align', about: 'Placing geometry by measuring its bounding box' },
|
|
47
|
+
{ path: 'reference/part.md', title: 'part', about: 'Coloured pieces of the rendered scene' },
|
|
48
|
+
{ path: 'reference/functions.md', title: 'Functions', about: 'Every builtin function and constant' },
|
|
49
|
+
{ path: 'errors.md', title: 'Errors', about: 'Every error message, what raises it, and what to change' },
|
|
50
|
+
{ path: 'api/render.md', title: 'render', about: 'JavaScript API: compile a document and evaluate a model' },
|
|
51
|
+
{ path: 'api/describe-parameters.md', title: 'describeParameters', about: 'JavaScript API: read a document’s params without rendering' },
|
|
52
|
+
{ path: 'api/evaluation-context.md', title: 'EvaluationContext', about: 'JavaScript API: the digest cache and WASM ownership' },
|
|
53
|
+
{ path: 'api/geometry-node.md', title: 'GeometryNode', about: 'JavaScript API: the content-addressed geometry IR' },
|
|
54
|
+
{ path: 'api/kernel.md', title: 'Kernel', about: 'JavaScript API: loadKernel, setQuality, resetQuality' },
|
|
55
|
+
{ path: 'api/export.md', title: 'Export', about: 'JavaScript API: toRenderMesh and toBinarySTL' },
|
|
56
|
+
{ path: 'api/values.md', title: 'Values', about: 'JavaScript API: Vector, Angle, Transform' },
|
|
57
|
+
{ path: 'api/low-level.md', title: 'Low-level API', about: 'JavaScript API: Program, Evaluator, parse, tokenize, the registries' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/** The pages, by path, so a lookup does not scan the list. */
|
|
61
|
+
const BY_PATH = new Map(PAGES.map((page) => [page.path, page]));
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Every page on disk, whether or not {@link PAGES} names it.
|
|
65
|
+
*
|
|
66
|
+
* Used by the tests to prove the two agree; nothing at runtime reads it.
|
|
67
|
+
*
|
|
68
|
+
* @returns {Promise<string[]>} Paths relative to the manual's root, sorted.
|
|
69
|
+
*/
|
|
70
|
+
export async function discoverPages() {
|
|
71
|
+
/** @type {string[]} */
|
|
72
|
+
const found = [];
|
|
73
|
+
const walk = async (directory) => {
|
|
74
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
75
|
+
const child = join(directory, entry.name);
|
|
76
|
+
if (entry.isDirectory()) await walk(child);
|
|
77
|
+
else if (entry.name.endsWith('.md')) found.push(relative(ROOT, child));
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
await walk(ROOT);
|
|
81
|
+
return found.sort((a, b) => a.localeCompare(b));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reads one page.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} path The page, as {@link PAGES} spells it.
|
|
88
|
+
* @returns {Promise<{ path: string, title: string, markdown: string }>} The page.
|
|
89
|
+
* @throws {Error} If no such page exists.
|
|
90
|
+
*/
|
|
91
|
+
export async function readPage(path) {
|
|
92
|
+
const normalised = path.replace(/^\.?\//, '');
|
|
93
|
+
const page = BY_PATH.get(normalised);
|
|
94
|
+
if (!page) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`no reference page "${path}" — the pages are ${PAGES.map((each) => each.path).join(', ')}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return { path: page.path, title: page.title, markdown: await readFile(join(ROOT, page.path), 'utf8') };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The heading a character offset falls under, so a hit can say where in the page it is.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} markdown The page.
|
|
106
|
+
* @param {number} offset Where the match was found.
|
|
107
|
+
* @returns {string} The nearest heading above it, or the page's title.
|
|
108
|
+
*/
|
|
109
|
+
function headingAt(markdown, offset) {
|
|
110
|
+
const before = markdown.slice(0, offset);
|
|
111
|
+
const headings = before.match(/^#{1,4} .*$/gm);
|
|
112
|
+
return headings?.length ? headings.at(-1).replace(/^#+ /, '') : '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Searches the manual.
|
|
117
|
+
*
|
|
118
|
+
* Every term must appear somewhere in the page; pages are ranked by how often the terms
|
|
119
|
+
* occur, with a heading match worth more than a body one.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} query What to look for.
|
|
122
|
+
* @param {number} [limit] How many hits to return.
|
|
123
|
+
* @returns {Promise<Array<{ path: string, title: string, heading: string, snippet: string }>>}
|
|
124
|
+
* The hits, best first.
|
|
125
|
+
*/
|
|
126
|
+
export async function searchReference(query, limit = 6) {
|
|
127
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
128
|
+
if (!terms.length) return [];
|
|
129
|
+
|
|
130
|
+
/** @type {Array<{ score: number, path: string, title: string, heading: string, snippet: string }>} */
|
|
131
|
+
const hits = [];
|
|
132
|
+
|
|
133
|
+
for (const page of PAGES) {
|
|
134
|
+
const markdown = await readFile(join(ROOT, page.path), 'utf8');
|
|
135
|
+
const lower = markdown.toLowerCase();
|
|
136
|
+
if (!terms.every((term) => lower.includes(term))) continue;
|
|
137
|
+
|
|
138
|
+
const headings = (lower.match(/^#{1,4} .*$/gm) ?? []);
|
|
139
|
+
const score = terms.reduce((total, term) => {
|
|
140
|
+
const body = lower.split(term).length - 1;
|
|
141
|
+
const named = headings.filter((heading) => heading.includes(term)).length;
|
|
142
|
+
return total + body + named * 8;
|
|
143
|
+
}, 0);
|
|
144
|
+
|
|
145
|
+
const at = lower.indexOf(terms[0]);
|
|
146
|
+
const from = Math.max(0, at - SNIPPET / 4);
|
|
147
|
+
hits.push({
|
|
148
|
+
score,
|
|
149
|
+
path: page.path,
|
|
150
|
+
title: page.title,
|
|
151
|
+
heading: headingAt(markdown, at),
|
|
152
|
+
snippet: markdown.slice(from, from + SNIPPET).trim(),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return hits
|
|
157
|
+
.toSorted((a, b) => b.score - a.score)
|
|
158
|
+
.slice(0, limit)
|
|
159
|
+
.map(({ score: _score, ...hit }) => hit);
|
|
160
|
+
}
|