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
package/src/guide.js ADDED
@@ -0,0 +1,313 @@
1
+ /**
2
+ * The language, compressed to what someone writing a document has to hold in their head.
3
+ *
4
+ * This is deliberately not the reference manual — the manual is bundled too, and
5
+ * `forma_reference` serves it. This is the page that goes into a model's context before it
6
+ * writes anything: the rules that are not guessable, the mistakes that are expensive, and
7
+ * the shape of a working document.
8
+ */
9
+
10
+ /** The sections, in reading order. Each is a complete thought on its own. */
11
+ const SECTIONS = {
12
+ shape: `## The shape of a document
13
+
14
+ A document is a flat list of four kinds of top-level block. Nothing else may appear there.
15
+
16
+ | Block | What it is |
17
+ | --- | --- |
18
+ | \`param NAME = expr\` / \`param NAME { ... }\` | An input. Read it as \`var.NAME\` |
19
+ | \`local NAME = expr\` | A named value, computed once |
20
+ | \`component "NAME" { ... }\` | Reusable geometry, called like a builtin block |
21
+ | \`model "NAME" { ... }\` | Something to render. The first one is the default |
22
+
23
+ \`\`\`hcl
24
+ param height { type = number default = 20 min = 8 max = 40 }
25
+ local wall = 2.5
26
+
27
+ component "boss" {
28
+ param radius = 5
29
+ cylinder { radius = radius height = 8 }
30
+ }
31
+
32
+ model "riser" {
33
+ part "body" {
34
+ color = "#6f7d8c"
35
+ difference {
36
+ extrude {
37
+ height = var.height
38
+ rounded_rect { size = [40, 20] radius = 4 center = true }
39
+ }
40
+ translate { offset = [0, 0, 3] cylinder { radius = 5 height = var.height } }
41
+ }
42
+ }
43
+ }
44
+ \`\`\`
45
+
46
+ Order does not matter between declarations of different kinds — a model may use a component
47
+ declared below it. Order does matter among top-level \`local\`s, which resolve top to bottom.`,
48
+
49
+ nesting: `## Nesting is composition
50
+
51
+ There is no expression language for geometry. **A block's children are its operands**,
52
+ written inside its braces:
53
+
54
+ \`\`\`hcl
55
+ difference { // subtract…
56
+ box { size = [20, 20, 10] } // …this is the body
57
+ cylinder { radius = 5 height = 30 } // …and this is cut out of it
58
+ }
59
+ \`\`\`
60
+
61
+ **Several children in a row are implicitly unioned.** Every operation except the three
62
+ booleans unions its children before acting, so a transform around two shapes moves both:
63
+
64
+ \`\`\`hcl
65
+ translate {
66
+ offset = [0, 0, 10]
67
+ box { size = [4, 4, 4] } // unioned first…
68
+ sphere { radius = 3 } // …then the union is moved
69
+ }
70
+ \`\`\`
71
+
72
+ \`difference\` is the one operation where order matters: the first child is the body, every
73
+ later child is cut out of it. An operation with no children is an error; a shape with
74
+ children is an error.`,
75
+
76
+ dimensionality: `## Dimensionality
77
+
78
+ Every piece of geometry is 2D or 3D, and the language tracks which.
79
+
80
+ - **2D** — \`rect\` \`rounded_rect\` \`circle\` \`ellipse\` \`regular_polygon\` \`stadium\` \`polygon\`
81
+ - **3D** — \`box\` \`sphere\` \`cylinder\` \`cone\` \`torus\`
82
+ - **Operations** either fix what they take (\`extrude\` takes 2D, \`refine\` takes 3D) or pass
83
+ their children's dimensionality through (\`translate\` \`rotate\` \`scale\` \`mirror\` \`hull\`
84
+ \`simplify\` \`align\`, the booleans).
85
+
86
+ Three rules, each with its own error:
87
+
88
+ | Rule | Error when broken |
89
+ | --- | --- |
90
+ | Children of one block must all be the same dimensionality | \`"union" cannot mix 2D and 3D shapes\` |
91
+ | A block that fixes what it takes must get it | \`"extrude" takes 2D geometry, got 3D\` |
92
+ | Anything rendered must be 3D | \`part "body" is 2D — extrude or revolve it before rendering\` |
93
+
94
+ 2D → 3D with \`extrude\` or \`revolve\`; 3D → 2D with \`project\` or \`slice\`.`,
95
+
96
+ units: `## Units and orientation
97
+
98
+ - **Lengths are unitless.** Every consumer treats them as millimetres.
99
+ - **Angles are always degrees.** Everywhere — \`rotate { z = 90 }\`, \`revolve { angle = 180 }\`,
100
+ \`sin(30)\`, \`param a { type = angle }\`. There is no radian form.
101
+ - **Z is up.** \`extrude\` sweeps along +Z. \`revolve\` spins a profile written in the XY plane
102
+ about the Y axis, mapping the profile's Y to the result's Z — so a profile drawn upwards
103
+ from \`[0, 0]\` produces a solid standing on the XY plane.
104
+ - **3D rotations apply X, then Y, then Z**, matching OpenSCAD.
105
+ - **Transforms act about the origin.** Centre a shape (\`center = true\`) before rotating it,
106
+ or it swings away.`,
107
+
108
+ syntax: `## Syntax
109
+
110
+ **Comments** — \`// line\`, \`# line\`, \`/* block */\`. Block comments do not nest.
111
+
112
+ **Numbers** — \`12\` \`0.5\` \`.5\` \`1e3\` \`2.5e-3\`. No hex, no separators, no unit suffix.
113
+
114
+ **Strings** — double quotes only, no literal newline. Exactly six escapes: \`\\n\` \`\\t\` \`\\r\`
115
+ \`\\"\` \`\\\\\` \`\\$\`. \`\${ expr }\` splices any expression in.
116
+
117
+ **Identifiers** start with a letter or \`_\` and continue with letters, digits, \`_\` **or \`-\`**.
118
+
119
+ > **Hyphens are part of a name.** \`a-b\` is one identifier, not a subtraction. Write
120
+ > \`a - b\` with spaces. This is the single most common surprise: \`local gap = outer-inner\`
121
+ > fails with \`unknown name "outer-inner"\`.
122
+
123
+ **Keywords**, which cannot be used as a block type:
124
+ \`param local component model for if else in true false null\`.
125
+
126
+ **Blocks and attributes.** A body holds attributes and nested blocks in any mix. An attribute
127
+ is \`name = expression\`; anything else that is followed by a brace is a nested block. Setting
128
+ the same attribute twice is an error.
129
+
130
+ **The newline rule.** Outside brackets, **a line break ends an expression** — that is what
131
+ lets attributes be written one per line with no separators. Inside \`(...)\`, \`[...]\` or an
132
+ argument list the rule is suspended, so break there to continue across lines:
133
+
134
+ \`\`\`hcl
135
+ local total = (
136
+ var.wall * 2
137
+ + var.gap
138
+ )
139
+
140
+ local b = base
141
+ + extra // NOT a continuation — this is a parse error
142
+ \`\`\`
143
+
144
+ Trailing commas are allowed in lists, objects and argument lists.`,
145
+
146
+ expressions: `## Expressions
147
+
148
+ | Type | Literals |
149
+ | --- | --- |
150
+ | number | \`12\` \`0.5\` \`1e3\` |
151
+ | string | \`"text"\` \`"a \${b} c"\` |
152
+ | bool | \`true\` \`false\` |
153
+ | list | \`[1, 2, 3]\` \`[[0,0], [1,0]]\` |
154
+ | object | \`{ w = 3, h = 4 }\` or \`{ w: 3 }\`, read with \`.key\` |
155
+ | null | \`null\` |
156
+
157
+ There is no vector type: **a vector is a list of numbers**, and a block says how many
158
+ components it wants. Where a block wants a vector, **a single number stands for every
159
+ component** — \`scale { factor = 2 }\` is \`scale { factor = [2, 2, 2] }\`.
160
+
161
+ **Operators**, loosest first: \`?:\` · \`||\` · \`&&\` · \`== !=\` · \`< <= > >=\` · \`+ -\` ·
162
+ \`* / %\` · prefix \`-x !x\` · postfix \`a.b a[i] f(x)\`.
163
+
164
+ \`\`\`hcl
165
+ [10, 20] + [1, 2] // [11, 22] componentwise
166
+ [10, 20] * 2 // [20, 40] scalar broadcast, either order
167
+ [10, 20] / 2 // [5, 10] scalar only
168
+ "part_" + 3 // "part_3" + concatenates when either side is a string
169
+ \`\`\`
170
+
171
+ \`+\` and \`-\` on two lists are componentwise and reject a bare number; \`/\` accepts a scalar
172
+ only. Two lists of different lengths cannot be combined. **Division and modulo by zero are
173
+ errors**, not \`Infinity\`.
174
+
175
+ \`<\` \`<=\` \`>\` \`>=\` need both sides to be the same type. \`==\` compares structurally.
176
+
177
+ **Truthy / falsy** — falsy is \`false\`, \`0\`, \`""\`, \`[]\`, \`null\`. Everything else is truthy,
178
+ including \`{}\`.
179
+
180
+ **Members** — \`.x\` \`.y\` \`.z\` read index 0, 1, 2 off a list, so \`size.z\` works on a
181
+ 3-vector. \`a[i]\` indexes. \`var.NAME\` reads a param.`,
182
+
183
+ scope: `## Scope and names
184
+
185
+ - **\`var\` holds params only** — never locals. A local with the same name shadows the bare
186
+ name but \`var.NAME\` still means the param.
187
+ - **Component params are read by their bare name.** There is no \`var.\` for them.
188
+ - **A component body cannot see the call site.** It is evaluated in a fresh scope rooted at
189
+ the document, so a \`local\` where it is called does not leak in. Document params and other
190
+ components are in scope.
191
+ - **A param default may use earlier params by their bare name**, but never \`var\` — \`var\` is
192
+ assembled only after every param has resolved.
193
+ - **Top-level locals are ordered**; locals inside a body are not, so a block may use a local
194
+ written below it.
195
+ - **Loop variables** are bound per iteration in a fresh child scope.`,
196
+
197
+ control: `## Control flow
198
+
199
+ Both appear where a block appears, and produce geometry rather than a value.
200
+
201
+ \`\`\`hcl
202
+ for i in range(0, 6) { ... } // one name: the value
203
+ for i, item in list { ... } // two names: the index, then the value
204
+
205
+ if cond { ... } else if other { ... } else { ... }
206
+ \`\`\`
207
+
208
+ - The sequence must be a **list**. \`range(a, b)\` excludes \`b\` and is empty when it runs
209
+ backwards, so a loop can never spin forever.
210
+ - There is no \`break\`, no \`continue\` and no accumulator. Filter the list instead.
211
+ - A loop over an empty list produces nothing, which is an error where something was
212
+ required — guard it with \`if\`.
213
+ - For a *value* that depends on a condition, use \`cond ? a : b\`, not \`if\`.
214
+
215
+ The radial-array idiom, which is most of what loops are for:
216
+
217
+ \`\`\`hcl
218
+ for i in range(0, var.bolts) {
219
+ rotate {
220
+ z = i * 360 / var.bolts
221
+ translate { offset = [var.circle_r, 0, -1] cylinder { radius = 3 height = 12 } }
222
+ }
223
+ }
224
+ \`\`\``,
225
+
226
+ parts: `## Parts and scenes
227
+
228
+ A \`part\` is a separately coloured piece of the finished scene. It is **not an operand**: it
229
+ contributes no geometry to the block around it.
230
+
231
+ \`\`\`hcl
232
+ model "assembly" {
233
+ part "frame" { color = "#6f7d8c" box { size = [40, 40, 4] } }
234
+ part "pin" { color = "#c8842f" opacity = 0.4
235
+ translate { offset = [0, 0, 4] cylinder { radius = 3 height = 10 } } }
236
+ }
237
+ \`\`\`
238
+
239
+ - Declare parts **directly in a model**, or inside a \`for\`/\`if\` in one. A part nested inside
240
+ a transform or a boolean is **silently dropped** — no error, it is just gone.
241
+ - A part cannot contain another part, cannot be empty, and cannot come out of a component.
242
+ - Geometry written outside any part becomes one default part named after the model.
243
+ - The label is an expression: \`part "leg_\${i}"\`.
244
+ - To make a part optional, put the \`if\` **around** it, never inside it.`,
245
+
246
+ pitfalls: `## The mistakes that cost the most time
247
+
248
+ 1. **Hyphens bind into names.** \`outer-inner\` is one identifier. Write \`outer - inner\`.
249
+ 2. **A misspelled attribute is an error, not a silent no-op** — \`box: unknown attribute
250
+ "colour"\`. The one exception is a \`param\` block, which passes anything through.
251
+ 3. **A part inside a transform disappears** with no warning. Put the transform inside the
252
+ part.
253
+ 4. **Transforms are about the origin.** \`rotate\` a shape built in the positive quadrant and
254
+ it swings away from where you expected. Use \`center = true\`, or \`align\`.
255
+ 5. **A cutter must overshoot.** A \`difference\` whose cutter ends exactly on the body's face
256
+ leaves a zero-thickness skin. Start it below and run it past.
257
+ 6. **\`revolve\` needs a profile at X ≥ 0.** Crossing the axis puts a spike through the middle.
258
+ 7. **2D cannot be rendered.** A part holding a \`circle\` fails; extrude it.
259
+ 8. **\`param s\` alone is a syntax error.** Write \`param s { }\` for a required param.
260
+ 9. **\`min\`/\`max\`/\`step\`/\`options\` on a param are advisory** and never enforced. Clamp in
261
+ the model if the bound matters: \`local n = max(3, min(16, var.bolts))\`.
262
+ 10. **Empty geometry is silent.** \`extrude { height = 0 }\`, a \`difference\` that removes
263
+ everything and a backwards \`range\` all produce nothing rather than an error — and then
264
+ fail further out with \`model "m" produced no geometry\`.
265
+ 11. **A line break ends an expression** outside brackets. Continue inside \`(...)\` or end the
266
+ line on an operator.
267
+ 12. **\`rotate\` spells its 2D and 3D attributes differently** — \`angle\` in 2D, \`x\`/\`y\`/\`z\`
268
+ or \`angles\` in 3D.`,
269
+
270
+ workflow: `## How to write one of these
271
+
272
+ 1. **Name the inputs first.** Anything a person would want to change is a \`param\` with a
273
+ \`default\`, a \`min\` and a \`max\`. Anything derived from those is a \`local\`.
274
+ 2. **Write the profile, then give it thickness.** Most real parts are a 2D outline extruded
275
+ or revolved. Modelling in 2D and converting once is shorter and more robust than
276
+ composing solids.
277
+ 3. **Name repeated features as components** — including the *negative* of a feature. A
278
+ \`component "counterbore"\` subtracted in a loop reads far better than four inlined unions.
279
+ 4. **One \`part\` per physical piece**, each with a colour. It is what makes a render legible
280
+ and what lets a single piece be exported.
281
+ 5. **Check it.** Call \`forma_check\` with \`solve: true\` and read the bounding box and the
282
+ volume back: they are how you know the thing is the size you meant and is not empty.
283
+ Iterate until it renders, then write it with \`forma_write\`.
284
+
285
+ Keep every dimension that matters expressed in terms of params, so the document answers
286
+ "what if it were 20 mm taller" without being rewritten.`,
287
+ };
288
+
289
+ /** The order sections are served in when everything is asked for. */
290
+ const ORDER = [
291
+ 'shape', 'nesting', 'dimensionality', 'units', 'syntax', 'expressions',
292
+ 'scope', 'control', 'parts', 'pitfalls', 'workflow',
293
+ ];
294
+
295
+ /** The names a caller may ask for. */
296
+ export const GUIDE_SECTIONS = ORDER;
297
+
298
+ /**
299
+ * The guide, whole or in part.
300
+ *
301
+ * @param {string} [section] One of {@link GUIDE_SECTIONS}, or nothing for all of it.
302
+ * @returns {string} Markdown.
303
+ * @throws {Error} If the section is not one the guide has.
304
+ */
305
+ export function guide(section) {
306
+ if (!section) {
307
+ return ['# The forma language, in brief', '', ...ORDER.map((name) => SECTIONS[name])].join('\n\n');
308
+ }
309
+ if (!SECTIONS[section]) {
310
+ throw new Error(`no guide section "${section}" — it is one of ${ORDER.join(', ')}`);
311
+ }
312
+ return SECTIONS[section];
313
+ }
@@ -0,0 +1,153 @@
1
+ # forma Reference
2
+
3
+ `forma` is a declarative language for describing solids. A document is a list of top-level
4
+ blocks; geometry is written by **nesting blocks**, where a block's children are its operands.
5
+ The evaluator turns a document into a tree of immutable geometry nodes, and the
6
+ [Manifold](https://github.com/elalish/manifold) CSG kernel — compiled to WebAssembly — solves
7
+ that tree into meshes.
8
+
9
+ ```hcl
10
+ param height { type = number default = 20 min = 8 max = 40 }
11
+
12
+ model "riser" {
13
+ part "body" {
14
+ color = "#6f7d8c"
15
+
16
+ difference {
17
+ extrude {
18
+ height = var.height
19
+ rounded_rect { size = [40, 20] radius = 4 center = true }
20
+ }
21
+ translate {
22
+ offset = [0, 0, 3]
23
+ cylinder { radius = 5 height = var.height }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ This reference documents every construct in the language and every function the JavaScript
31
+ package exports. For a tour of the project itself — how to run the editor, how to consume the
32
+ library from Node — see the [top-level README](../../../README.md).
33
+
34
+ ---
35
+
36
+ ## Learn the language
37
+
38
+ Read these in order the first time.
39
+
40
+ | Page | What it covers |
41
+ | --- | --- |
42
+ | [Document structure](language/overview.md) | The four top-level blocks, the evaluation pipeline, nesting and implicit union, dimensionality |
43
+ | [Syntax](language/syntax.md) | Comments, numbers, strings, identifiers, labels, the rule that a newline ends an attribute |
44
+ | [Expressions](language/expressions.md) | Values, operators, precedence, truthiness, interpolation, member access |
45
+ | [Scope and names](language/scope.md) | What is in scope where, `var.`, shadowing, component isolation |
46
+ | [Control flow](language/control-flow.md) | `for` and `if` / `else if` / `else` |
47
+
48
+ ---
49
+
50
+ ## Declarations
51
+
52
+ The four blocks that may appear at the top level of a document.
53
+
54
+ | Declaration | Description |
55
+ | --- | --- |
56
+ | [`param`](reference/param.md) | An input, exposed in the editor as a control and readable as `var.NAME` |
57
+ | [`local`](reference/local.md) | A named value, computed once |
58
+ | [`component`](reference/component.md) | A reusable parameterised block, callable like any builtin |
59
+ | [`model`](reference/model.md) | Something to render. A document may declare several |
60
+
61
+ ---
62
+
63
+ ## Shapes
64
+
65
+ Leaf blocks. They take attributes and never contain other blocks.
66
+
67
+ | Block | Dimensionality | Description |
68
+ | --- | --- | --- |
69
+ | [`rect`](reference/shapes-2d.md#rect) | 2D | An axis-aligned rectangle |
70
+ | [`rounded_rect`](reference/shapes-2d.md#rounded_rect) | 2D | A rectangle with true arcs at the corners |
71
+ | [`circle`](reference/shapes-2d.md#circle) | 2D | A circle, from a radius or a diameter |
72
+ | [`ellipse`](reference/shapes-2d.md#ellipse) | 2D | A circle scaled to two exact radii |
73
+ | [`regular_polygon`](reference/shapes-2d.md#regular_polygon) | 2D | An *n*-gon, sized across flats, across corners, or by radius |
74
+ | [`stadium`](reference/shapes-2d.md#stadium) | 2D | A rectangle fully rounded on its short axis |
75
+ | [`polygon`](reference/shapes-2d.md#polygon) | 2D | An explicit outline, or several contours with holes |
76
+ | [`box`](reference/shapes-3d.md#box) | 3D | An axis-aligned cuboid |
77
+ | [`sphere`](reference/shapes-3d.md#sphere) | 3D | A sphere |
78
+ | [`cylinder`](reference/shapes-3d.md#cylinder) | 3D | A cylinder, or a frustum from two radii |
79
+ | [`cone`](reference/shapes-3d.md#cone) | 3D | A cone, or a truncated cone |
80
+ | [`torus`](reference/shapes-3d.md#torus) | 3D | A ring, optionally a partial arc |
81
+
82
+ 2D shapes cannot be rendered on their own — [`extrude`](reference/conversions.md#extrude) or
83
+ [`revolve`](reference/conversions.md#revolve) them first.
84
+
85
+ ---
86
+
87
+ ## Operations
88
+
89
+ Blocks that take geometry as children and produce new geometry.
90
+
91
+ | Block | Takes | Description |
92
+ | --- | --- | --- |
93
+ | [`union`](reference/booleans.md#union) | 2D or 3D | Everything added together |
94
+ | [`difference`](reference/booleans.md#difference) | 2D or 3D | The first child, with every later child cut out of it |
95
+ | [`intersection`](reference/booleans.md#intersection) | 2D or 3D | Only what every child shares |
96
+ | [`translate`](reference/transforms.md#translate) | 2D or 3D | Moves geometry |
97
+ | [`rotate`](reference/transforms.md#rotate) | 2D or 3D | Turns geometry about the origin |
98
+ | [`scale`](reference/transforms.md#scale) | 2D or 3D | Scales geometry about the origin |
99
+ | [`mirror`](reference/transforms.md#mirror) | 2D or 3D | Reflects geometry in a plane through the origin |
100
+ | [`extrude`](reference/conversions.md#extrude) | 2D → 3D | Sweeps a profile along Z, with optional twist and taper |
101
+ | [`revolve`](reference/conversions.md#revolve) | 2D → 3D | Turns a profile about the Y axis into a solid of revolution |
102
+ | [`project`](reference/conversions.md#project) | 3D → 2D | The shadow of a solid on the XY plane |
103
+ | [`slice`](reference/conversions.md#slice) | 3D → 2D | The cross-section of a solid at one Z |
104
+ | [`offset`](reference/refinement.md#offset) | 2D | Grows or shrinks an outline |
105
+ | [`hull`](reference/refinement.md#hull) | 2D or 3D | The convex hull of everything inside |
106
+ | [`refine`](reference/refinement.md#refine) | 3D | Subdivides edges longer than a limit |
107
+ | [`simplify`](reference/refinement.md#simplify) | 2D or 3D | Removes vertices within a tolerance |
108
+ | [`smooth`](reference/refinement.md#smooth) | 3D | Rounds edges below a sharpness threshold |
109
+ | [`trim`](reference/refinement.md#trim) | 3D | Cuts a solid with a half-space |
110
+ | [`align`](reference/align.md) | 2D or 3D | Moves geometry so a measured feature of its bounding box lands where you say |
111
+
112
+ ---
113
+
114
+ ## Scene
115
+
116
+ | Block | Description |
117
+ | --- | --- |
118
+ | [`part`](reference/part.md) | A separately coloured piece of the rendered scene |
119
+
120
+ ---
121
+
122
+ ## Expressions
123
+
124
+ | Page | What it covers |
125
+ | --- | --- |
126
+ | [Functions](reference/functions.md) | `abs`, `min`, `len`, `range`, `sin`, and the rest of the builtin functions |
127
+ | [Constants](reference/functions.md#constants) | `pi` and `e` |
128
+ | [Operators](language/expressions.md#operators) | Arithmetic, comparison, logic, and what each accepts |
129
+
130
+ ---
131
+
132
+ ## JavaScript API
133
+
134
+ `src/` is the library; the editor is one consumer of it. Everything below is exported from
135
+ the package root and typed by hand-written declarations.
136
+
137
+ | API | Description |
138
+ | --- | --- |
139
+ | [`render`](api/render.md) | Compile a document and evaluate one model into renderable parts |
140
+ | [`describeParameters`](api/describe-parameters.md) | Read a document's declared params without rendering |
141
+ | [`EvaluationContext`](api/evaluation-context.md) | The digest cache and the owner of every WASM object |
142
+ | [`GeometryNode`](api/geometry-node.md) | The immutable, content-addressed geometry IR |
143
+ | [`loadKernel`, `setQuality`, `resetQuality`](api/kernel.md) | Loading and tuning the Manifold module |
144
+ | [`toRenderMesh`, `toBinarySTL`](api/export.md) | Turning a solid into buffers or a file |
145
+ | [`Vector`, `Angle`, `Transform`](api/values.md) | The value types the evaluator works in |
146
+ | [`Program`, `Evaluator`, `parse`, `tokenize`, …](api/low-level.md) | The compiler stages, and the block and function registries |
147
+
148
+ ---
149
+
150
+ ## Errors
151
+
152
+ [Every error message](errors.md), what raises it, and what to change — the language's errors
153
+ in one place, grouped by the stage that raises them.
@@ -0,0 +1,167 @@
1
+ # `describeParameters`
2
+
3
+ Reads what a document declares as inputs, without rendering anything.
4
+
5
+ ```js
6
+ import { describeParameters } from 'forma-dsl';
7
+
8
+ const params = describeParameters(source);
9
+ ```
10
+
11
+ ---
12
+
13
+ ## Reference
14
+
15
+ ### `describeParameters(source)`
16
+
17
+ Parses `source` and returns one descriptor per top-level [`param`](../reference/param.md), in
18
+ source order. No geometry is evaluated and no kernel is loaded — this is the call a front end
19
+ makes to build controls before it can have any values to render with.
20
+
21
+ #### Parameters
22
+
23
+ | Parameter | Type | Description |
24
+ | --- | --- | --- |
25
+ | `source` | `string` | The document |
26
+
27
+ #### Returns
28
+
29
+ `ParameterDescriptor[]`:
30
+
31
+ | Field | Type | Description |
32
+ | --- | --- | --- |
33
+ | `name` | `string` | The param's name |
34
+ | `type` | `'number' \| 'string' \| 'bool' \| 'vector' \| 'list' \| 'angle' \| string` | The declared `type`, or one inferred from the default |
35
+ | `default` | `ParameterValue \| undefined` | The evaluated default, if there is one |
36
+ | `required` | `boolean` | `true` when the param declares no default |
37
+ | `min` | `number \| undefined` | Advisory |
38
+ | `max` | `number \| undefined` | Advisory |
39
+ | `step` | `number \| undefined` | Advisory |
40
+ | `description` | `string \| undefined` | Help text |
41
+ | `options` | `ParameterValue[] \| undefined` | A fixed set of choices |
42
+
43
+ #### Caveats
44
+
45
+ - **Params are read leniently.** A document that declares a required param must be
46
+ describable before anyone could have supplied a value for it, so nothing throws about
47
+ missing values here.
48
+ - **One unreadable attribute does not lose the rest.** If a `min` refers to something that
49
+ cannot be resolved yet, that field comes back `undefined` and every other descriptor is
50
+ still returned.
51
+ - **`min`, `max`, `step` and `options` are advisory.** Nothing enforces them, including
52
+ [`render`](render.md). Clamp in the model if a bound matters.
53
+ - **Model params are not listed.** A `param` inside a `model` is a constant, not an input.
54
+ - It still throws on a **syntax error** — the document has to parse.
55
+
56
+ #### Type inference
57
+
58
+ When a param declares no `type`, one is inferred from the default:
59
+
60
+ | Default | Inferred type |
61
+ | --- | --- |
62
+ | a number | `number` |
63
+ | a boolean | `bool` |
64
+ | a string | `string` |
65
+ | a list | `vector` |
66
+ | nothing | `number` |
67
+
68
+ ---
69
+
70
+ ## Usage
71
+
72
+ ### Building controls
73
+
74
+ ```js
75
+ const descriptors = describeParameters(source);
76
+
77
+ for (const p of descriptors) {
78
+ switch (p.type) {
79
+ case 'number':
80
+ addSlider(p.name, {
81
+ value: p.default ?? p.min ?? 0,
82
+ min: p.min, max: p.max, step: p.step,
83
+ title: p.description,
84
+ });
85
+ break;
86
+ case 'bool':
87
+ addCheckbox(p.name, { value: p.default ?? false });
88
+ break;
89
+ case 'string':
90
+ p.options ? addSelect(p.name, p.options) : addTextField(p.name);
91
+ break;
92
+ case 'vector':
93
+ addVectorField(p.name, { value: p.default ?? [0, 0, 0] });
94
+ break;
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### Collecting values to render with
100
+
101
+ ```js
102
+ const descriptors = describeParameters(source);
103
+
104
+ const params = Object.fromEntries(
105
+ descriptors
106
+ .filter((p) => p.default !== undefined)
107
+ .map((p) => [p.name, p.default]),
108
+ );
109
+
110
+ const result = await render(source, { params, context });
111
+ ```
112
+
113
+ ### Insisting on required inputs
114
+
115
+ ```js
116
+ const missing = describeParameters(source)
117
+ .filter((p) => p.required && values[p.name] === undefined)
118
+ .map((p) => p.name);
119
+
120
+ if (missing.length) {
121
+ throw new Error(`Supply a value for: ${missing.join(', ')}`);
122
+ }
123
+ ```
124
+
125
+ This is the whole reason `required` exists: a document can declare that there is no sensible
126
+ default, and a front end can act on that before rendering fails.
127
+
128
+ ### Re-reading after an edit
129
+
130
+ `describeParameters` is cheap — a parse, no kernel — so calling it on every edit is
131
+ reasonable. Keep any value the user has already set, for a param that still exists with a
132
+ compatible type:
133
+
134
+ ```js
135
+ function reconcile(source, previous) {
136
+ const next = {};
137
+ for (const p of describeParameters(source)) {
138
+ next[p.name] = p.name in previous ? previous[p.name] : p.default;
139
+ }
140
+ return next;
141
+ }
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Troubleshooting
147
+
148
+ ### A descriptor's `default` is `undefined`
149
+
150
+ Either the param declares no default — check `required` — or its default could not be
151
+ evaluated. A default that refers to a param declared **below** it cannot resolve; params are
152
+ read in source order.
153
+
154
+ ### `min` and `max` came back `undefined`
155
+
156
+ The expression could not be evaluated. That is deliberately not fatal: one unreadable
157
+ metadata attribute must not cost the caller every other descriptor.
158
+
159
+ ### My param is not in the list
160
+
161
+ It is probably declared inside a `model` rather than at the top level. Only top-level params
162
+ are inputs; see [model params](../reference/model.md#model-params).
163
+
164
+ ### The values I pass are ignored
165
+
166
+ `render` accepts values for declared top-level params only, and silently ignores anything
167
+ else. Compare the keys you pass against `describeParameters(source).map((p) => p.name)`.