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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/package.json +52 -0
  4. package/src/bin.js +30 -0
  5. package/src/catalogue.js +703 -0
  6. package/src/document.js +285 -0
  7. package/src/examples.js +212 -0
  8. package/src/guide.js +313 -0
  9. package/src/reference/README.md +153 -0
  10. package/src/reference/api/describe-parameters.md +167 -0
  11. package/src/reference/api/evaluation-context.md +221 -0
  12. package/src/reference/api/export.md +185 -0
  13. package/src/reference/api/geometry-node.md +215 -0
  14. package/src/reference/api/kernel.md +182 -0
  15. package/src/reference/api/low-level.md +239 -0
  16. package/src/reference/api/render.md +217 -0
  17. package/src/reference/api/values.md +223 -0
  18. package/src/reference/errors.md +253 -0
  19. package/src/reference/language/control-flow.md +160 -0
  20. package/src/reference/language/expressions.md +206 -0
  21. package/src/reference/language/overview.md +156 -0
  22. package/src/reference/language/scope.md +184 -0
  23. package/src/reference/language/syntax.md +210 -0
  24. package/src/reference/reference/align.md +189 -0
  25. package/src/reference/reference/booleans.md +231 -0
  26. package/src/reference/reference/component.md +248 -0
  27. package/src/reference/reference/conversions.md +269 -0
  28. package/src/reference/reference/functions.md +314 -0
  29. package/src/reference/reference/local.md +197 -0
  30. package/src/reference/reference/model.md +213 -0
  31. package/src/reference/reference/param.md +230 -0
  32. package/src/reference/reference/part.md +229 -0
  33. package/src/reference/reference/refinement.md +284 -0
  34. package/src/reference/reference/shapes-2d.md +309 -0
  35. package/src/reference/reference/shapes-3d.md +279 -0
  36. package/src/reference/reference/transforms.md +246 -0
  37. package/src/reference.js +160 -0
  38. package/src/server.js +219 -0
  39. package/src/tools.js +523 -0
  40. 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
+ ```