forma-dsl-mcp 0.3.8 → 0.3.9

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 (41) hide show
  1. package/README.md +18 -18
  2. package/package.json +2 -2
  3. package/src/bin.js +1 -1
  4. package/src/catalogue.js +15 -15
  5. package/src/document.js +8 -38
  6. package/src/examples.js +1 -1
  7. package/src/guide.js +24 -24
  8. package/src/reference/README.md +15 -5
  9. package/src/reference/api/describe-parameters.md +4 -4
  10. package/src/reference/api/evaluation-context.md +13 -13
  11. package/src/reference/api/export.md +7 -7
  12. package/src/reference/api/geometry-node.md +9 -9
  13. package/src/reference/api/kernel.md +8 -8
  14. package/src/reference/api/low-level.md +9 -9
  15. package/src/reference/api/render.md +8 -8
  16. package/src/reference/api/values.md +12 -12
  17. package/src/reference/cli.md +146 -0
  18. package/src/reference/errors.md +14 -14
  19. package/src/reference/language/control-flow.md +3 -3
  20. package/src/reference/language/expressions.md +10 -10
  21. package/src/reference/language/overview.md +11 -11
  22. package/src/reference/language/scope.md +13 -13
  23. package/src/reference/language/syntax.md +4 -4
  24. package/src/reference/reference/align.md +9 -9
  25. package/src/reference/reference/booleans.md +5 -5
  26. package/src/reference/reference/component.md +12 -12
  27. package/src/reference/reference/conversions.md +8 -8
  28. package/src/reference/reference/functions.md +8 -8
  29. package/src/reference/reference/local.md +4 -4
  30. package/src/reference/reference/model.md +10 -10
  31. package/src/reference/reference/param.md +8 -8
  32. package/src/reference/reference/part.md +12 -12
  33. package/src/reference/reference/refinement.md +10 -10
  34. package/src/reference/reference/shapes-2d.md +15 -15
  35. package/src/reference/reference/shapes-3d.md +13 -13
  36. package/src/reference/reference/transforms.md +10 -10
  37. package/src/reference.js +4 -3
  38. package/src/roots.js +2 -2
  39. package/src/server.js +2 -2
  40. package/src/tools.js +15 -15
  41. package/src/workspace.js +7 -7
@@ -29,7 +29,7 @@ the node's digest.
29
29
  | | |
30
30
  | --- | --- |
31
31
  | **Parameter** | `node: GeometryNode` |
32
- | **Returns** | `Promise<Solid \| Shape>` — a `Manifold` for a 3D node, a `CrossSection` for a 2D one |
32
+ | **Returns** | `Promise<Solid \| Shape>` - a `Manifold` for a 3D node, a `CrossSection` for a 2D one |
33
33
  | **Throws** | If the context has been disposed |
34
34
 
35
35
  Concurrent requests for the same node share one evaluation, so a shape used twice in a tree
@@ -41,12 +41,12 @@ Frees every cached object the given roots can no longer reach.
41
41
 
42
42
  | | |
43
43
  | --- | --- |
44
- | **`roots`** | `readonly GeometryNode[]` — the trees still in use |
44
+ | **`roots`** | `readonly GeometryNode[]` - the trees still in use |
45
45
  | **`options.keep`** | `number`, default `512`. How many unreachable entries to keep as a most-recently-used tail |
46
- | **Returns** | `number` — how many objects were freed |
46
+ | **Returns** | `number` - how many objects were freed |
47
47
 
48
48
  The retained tail matters more than it looks: editing churns the tree, so the entries that
49
- just fell out of it are the ones most likely to come straight back — on an undo, or on the
49
+ just fell out of it are the ones most likely to come straight back - on an undo, or on the
50
50
  next character of a number being typed.
51
51
 
52
52
  ### `context.dispose()`
@@ -68,7 +68,7 @@ evaluations still in flight free their own results as they land. Disposing twice
68
68
  ## Why a context exists
69
69
 
70
70
  WebAssembly objects are not garbage-collected. Something has to own them, and something has
71
- to decide when they are no longer needed — that is this class.
71
+ to decide when they are no longer needed - that is this class.
72
72
 
73
73
  It is also the cache. Nothing is freed as evaluation proceeds, because an intermediate result
74
74
  is also a cache entry and a later node may still need it. The two ways out are `collect`,
@@ -115,7 +115,7 @@ async function rerender(source, params) {
115
115
  }
116
116
  ```
117
117
 
118
- There is no `dispose` here, and that is correct — the page unloading frees the whole heap.
118
+ There is no `dispose` here, and that is correct - the page unloading frees the whole heap.
119
119
 
120
120
  ### Tuning what is kept
121
121
 
@@ -141,7 +141,7 @@ console.log(`${evaluated} solved, ${hits} reused`);
141
141
  ```
142
142
 
143
143
  On a document where one dimension changed, `evaluated` should be a handful and `hits` most of
144
- the tree. If `evaluated` is the whole document every time, something is defeating the cache —
144
+ the tree. If `evaluated` is the whole document every time, something is defeating the cache -
145
145
  see [Troubleshooting](#every-render-re-evaluates-everything).
146
146
 
147
147
  ### Evaluating a node directly
@@ -167,7 +167,7 @@ context.dispose();
167
167
  1. **Everything a context returns belongs to the context.** Never call `delete()` on a
168
168
  `Solid`, a `Shape` or anything hanging off `part.concrete`.
169
169
  2. **A result is valid only while its context is alive.** After `dispose()`, every object it
170
- produced is gone. Copy out what you need first — `toRenderMesh` and `toBinarySTL` both
170
+ produced is gone. Copy out what you need first - `toRenderMesh` and `toBinarySTL` both
171
171
  produce plain typed arrays that outlive the context.
172
172
  3. **`render` does not dispose a context you supplied.** It disposes only one it created
173
173
  itself, and only on failure.
@@ -179,7 +179,7 @@ context.dispose();
179
179
  ### `EvaluationContext has been disposed`
180
180
 
181
181
  An `evaluate` or a `render` used a context after `dispose()`. Usually an async render that
182
- was still in flight when a page or a test tore the context down — await it first.
182
+ was still in flight when a page or a test tore the context down - await it first.
183
183
 
184
184
  ### `EvaluationContext used before create() resolved`
185
185
 
@@ -198,19 +198,19 @@ Add a `collect` after each render, passing the roots that are still in use:
198
198
  context.collect(result.parts.map((part) => part.node));
199
199
  ```
200
200
 
201
- Without it, every intermediate solid from every render is retained — which is the correct
201
+ Without it, every intermediate solid from every render is retained - which is the correct
202
202
  default, because the context cannot know whether you still hold a reference.
203
203
 
204
204
  ### `collect` freed nothing
205
205
 
206
- It returns `0` when the number of unreachable entries is at or below `keep` — the default 512
206
+ It returns `0` when the number of unreachable entries is at or below `keep` - the default 512
207
207
  tail. That is expected on a small document. Pass `{ keep: 0 }` to see whether anything is
208
208
  genuinely unreachable.
209
209
 
210
210
  ### Every render re-evaluates everything
211
211
 
212
- Either a fresh context is being created per render — check that the same one is passed back
213
- in — or the document's content genuinely changes on every render. A param that feeds a
212
+ Either a fresh context is being created per render - check that the same one is passed back
213
+ in - or the document's content genuinely changes on every render. A param that feeds a
214
214
  dimension changes the digest of everything downstream of it, which is correct; a param that
215
215
  feeds only one wall should not.
216
216
 
@@ -6,8 +6,8 @@ Turning a solved solid into buffers a renderer can upload, or a file a slicer ca
6
6
  import { toRenderMesh, toBinarySTL } from 'forma-dsl';
7
7
  ```
8
8
 
9
- Both take a `Solid` — the `concrete` field of a
10
- [rendered part](render.md#returns) — and both return plain typed arrays that **outlive the
9
+ Both take a `Solid` - the `concrete` field of a
10
+ [rendered part](render.md#returns) - and both return plain typed arrays that **outlive the
11
11
  context**, so they are what to copy out before disposing.
12
12
 
13
13
  ---
@@ -25,7 +25,7 @@ Converts a solid into flat arrays for a renderer.
25
25
 
26
26
  | Field | Type | Description |
27
27
  | --- | --- | --- |
28
- | `positions` | `Float32Array` | Three vertices per triangle, expanded — `triangleCount * 9` floats |
28
+ | `positions` | `Float32Array` | Three vertices per triangle, expanded - `triangleCount * 9` floats |
29
29
  | `normals` | `Float32Array` | One face normal, repeated for all three corners |
30
30
  | `triangleCount` | `number` | Triangles |
31
31
  | `vertexCount` | `number` | Vertices **before** expansion, as the kernel counted them |
@@ -44,7 +44,7 @@ The cost is that `positions.length / 3` is `triangleCount * 3` rather than `vert
44
44
 
45
45
  - A degenerate triangle keeps a zero normal rather than dividing by zero and poisoning the
46
46
  buffer with NaN.
47
- - [`render`](render.md) already calls this for every part — `part.mesh` is the result. Call
47
+ - [`render`](render.md) already calls this for every part - `part.mesh` is the result. Call
48
48
  it yourself only for a solid you built directly.
49
49
 
50
50
  ---
@@ -59,7 +59,7 @@ Encodes a solid as binary STL.
59
59
  | --- | --- |
60
60
  | **`solid`** | `Solid` |
61
61
  | **`header`** | `string`, default `'forma-dsl'`. Free text for the 80-byte header, truncated to fit |
62
- | **Returns** | `Uint8Array` — the file |
62
+ | **Returns** | `Uint8Array` - the file |
63
63
 
64
64
  Binary rather than ASCII: a model of any size is several times smaller and every slicer reads
65
65
  it.
@@ -141,7 +141,7 @@ function toThree(part) {
141
141
 
142
142
  ### Keeping geometry past the context
143
143
 
144
- Typed arrays are plain memory, so they survive disposal — unlike `part.concrete`:
144
+ Typed arrays are plain memory, so they survive disposal - unlike `part.concrete`:
145
145
 
146
146
  ```js
147
147
  const result = await render(source);
@@ -159,7 +159,7 @@ result.context.dispose(); // …so this is safe
159
159
 
160
160
  ### `EvaluationContext has been disposed`
161
161
 
162
- The solid was freed before it was encoded. Export first, dispose after — or copy the arrays
162
+ The solid was freed before it was encoded. Export first, dispose after - or copy the arrays
163
163
  out, which is what the example above does.
164
164
 
165
165
  ### The STL is empty, or has zero triangles
@@ -7,7 +7,7 @@ the kernel consumes them.
7
7
  import { GeometryNode } from 'forma-dsl';
8
8
 
9
9
  const box = GeometryNode.shape(3, 'box', { size: [10, 10, 10], center: true });
10
- box.digest; // '3f2a…' — 32 hex characters
10
+ box.digest; // '3f2a…' - 32 hex characters
11
11
  box.subtreeSize; // 1
12
12
  ```
13
13
 
@@ -36,7 +36,7 @@ process, so it is identical in every browser and every run.
36
36
  | Property | Type | Description |
37
37
  | --- | --- | --- |
38
38
  | `dim` | `2 \| 3` | Whether this node is a cross-section or a solid |
39
- | `kind` | `string` | What the node does — `shape3d`, `boolean`, `transform`, `extrude`, … |
39
+ | `kind` | `string` | What the node does - `shape3d`, `boolean`, `transform`, `extrude`, … |
40
40
  | `props` | `Record<string, unknown>` | The node's own parameters |
41
41
  | `children` | `readonly GeometryNode[]` | The operands |
42
42
  | `digest` | `string` | 128-bit content digest, 32 lowercase hex characters |
@@ -47,13 +47,13 @@ Nodes are frozen. Every factory returns a new node rather than mutating one.
47
47
 
48
48
  ### Factories
49
49
 
50
- Prefer these to the constructor — they apply the algebraic simplifications that keep the tree
50
+ Prefer these to the constructor - they apply the algebraic simplifications that keep the tree
51
51
  small.
52
52
 
53
53
  | Factory | Description |
54
54
  | --- | --- |
55
55
  | `GeometryNode.empty(dim)` | A node standing for no geometry |
56
- | `GeometryNode.shape(dim, shape, params)` | A leaf primitive — `'box'`, `'circle'`, `'cylinder'`, … |
56
+ | `GeometryNode.shape(dim, shape, params)` | A leaf primitive - `'box'`, `'circle'`, `'cylinder'`, … |
57
57
  | `GeometryNode.union(children)` | Everything added together |
58
58
  | `GeometryNode.difference(children)` | The first child with the rest removed |
59
59
  | `GeometryNode.intersection(children)` | Only what every child shares |
@@ -82,7 +82,7 @@ call and a cache entry:
82
82
  - **`dim` must be 2 or 3.** Anything else throws `bad dimensionality`.
83
83
  - **Booleans cannot mix dimensionality**: `cannot combine 2D and 3D geometry in a union`.
84
84
  - `GeometryNode.unary` defaults its result dimensionality to the child's, which is wrong for
85
- the operations that change it — pass `dim` explicitly for anything extrude-like.
85
+ the operations that change it - pass `dim` explicitly for anything extrude-like.
86
86
  - Two nodes with the same digest are interchangeable. Nothing stops you building the same
87
87
  node twice; the cache will treat both as one.
88
88
 
@@ -108,7 +108,7 @@ for (const node of walk(result.parts[0].node)) {
108
108
  }
109
109
  ```
110
110
 
111
- Deduplicating on `digest` is what makes the walk reflect what the kernel will actually do —
111
+ Deduplicating on `digest` is what makes the walk reflect what the kernel will actually do -
112
112
  the tree is a DAG, not a tree, once sharing is taken into account.
113
113
 
114
114
  ### Measuring how much a model shares
@@ -123,7 +123,7 @@ for (const node of walk(root)) unique.add(node.digest);
123
123
  console.log(`${root.subtreeSize} nodes written, ${unique.size} distinct`);
124
124
  ```
125
125
 
126
- A large gap is the digest cache doing its job — eight spheres in a loop are one sphere node
126
+ A large gap is the digest cache doing its job - eight spheres in a loop are one sphere node
127
127
  and eight transforms.
128
128
 
129
129
  ### Building geometry without the language
@@ -151,7 +151,7 @@ await writeFile('ring.stl', toBinarySTL(solid));
151
151
  context.dispose();
152
152
  ```
153
153
 
154
- Note that `pin` is one node used six times — the kernel builds one cylinder.
154
+ Note that `pin` is one node used six times - the kernel builds one cylinder.
155
155
 
156
156
  ### Checking whether an edit changed anything
157
157
 
@@ -211,5 +211,5 @@ GeometryNode.unary('extrude', profile, { height: 10, /* … */ }, 3);
211
211
 
212
212
  Digests cover **all** props. A `segments` of `0` and a `segments` of `32` are different
213
213
  shapes even when they look alike, and a float that differs in the last bit is a different
214
- number. Digest equality is exact, by design — the alternative is a cache that occasionally
214
+ number. Digest equality is exact, by design - the alternative is a cache that occasionally
215
215
  returns the wrong solid.
@@ -21,8 +21,8 @@ Loads and sets up the Manifold module, once per page or process.
21
21
 
22
22
  | | |
23
23
  | --- | --- |
24
- | **`options.locateFile`** | `() => string` — returns the URL of the `.wasm` file |
25
- | **Returns** | `Promise<Kernel>` — the `manifold-3d` toplevel module |
24
+ | **`options.locateFile`** | `() => string` - returns the URL of the `.wasm` file |
25
+ | **Returns** | `Promise<Kernel>` - the `manifold-3d` toplevel module |
26
26
 
27
27
  #### Caveats
28
28
 
@@ -30,7 +30,7 @@ Loads and sets up the Manifold module, once per page or process.
30
30
  share one instantiation rather than each starting their own.
31
31
  - **Only the first call's options take effect.** Later calls get the memoized promise, so
32
32
  `locateFile` has to be right the first time.
33
- - A rejected load is *not* memoized — a WASM fetch that lost the network can be retried
33
+ - A rejected load is *not* memoized - a WASM fetch that lost the network can be retried
34
34
  rather than being permanently broken for the page.
35
35
 
36
36
  ### Bundlers and `locateFile`
@@ -48,7 +48,7 @@ await loadKernel({ locateFile: () => wasmUrl });
48
48
  Do this **before** the first `EvaluationContext.create()`, or the default path is what gets
49
49
  memoized.
50
50
 
51
- In Node, nothing is needed — the module resolves the file itself.
51
+ In Node, nothing is needed - the module resolves the file itself.
52
52
 
53
53
  ---
54
54
 
@@ -75,7 +75,7 @@ left alone when omitted.
75
75
  solid built at draft quality will be *reused* at final quality unless the cache is cleared.
76
76
  See [Usage](#a-draft-pass-and-a-final-pass).
77
77
  - A shape's own `segments` attribute overrides all of this for that shape. Prefer it whenever
78
- a specific facet count matters — a hole that must clear a printed bolt should not depend on
78
+ a specific facet count matters - a hole that must clear a printed bolt should not depend on
79
79
  a global.
80
80
 
81
81
  ## `resetQuality`
@@ -91,7 +91,7 @@ Restores the kernel's own defaults. Returns `Promise<void>`.
91
91
  ### A draft pass and a final pass
92
92
 
93
93
  Because quality is global and digests do not include it, the cache has to be dropped between
94
- passes — otherwise the final render happily reuses the draft's geometry:
94
+ passes - otherwise the final render happily reuses the draft's geometry:
95
95
 
96
96
  ```js
97
97
  import { setQuality, EvaluationContext, render } from 'forma-dsl';
@@ -149,7 +149,7 @@ const context = await EvaluationContext.create();
149
149
  const { Manifold, CrossSection } = context.wasm;
150
150
  ```
151
151
 
152
- Anything you construct this way is **yours** to delete — the context only owns what it built
152
+ Anything you construct this way is **yours** to delete - the context only owns what it built
153
153
  itself.
154
154
 
155
155
  ---
@@ -167,7 +167,7 @@ await loadKernel({ locateFile: () => wasmUrl });
167
167
 
168
168
  ### `locateFile` is being ignored
169
169
 
170
- The kernel was already loaded — the promise is memoized, so only the first call's options
170
+ The kernel was already loaded - the promise is memoized, so only the first call's options
171
171
  count. Move the `loadKernel` call earlier, before anything creates a context.
172
172
 
173
173
  ### `setQuality` had no effect on an existing model
@@ -1,7 +1,7 @@
1
1
  # Compiler stages
2
2
 
3
3
  The pipeline, stage by stage, plus the registries the evaluator reads. These are exported so
4
- that editors, linters and tools can reach into the compiler without re-implementing it — the
4
+ that editors, linters and tools can reach into the compiler without re-implementing it - the
5
5
  [editor](../../../editor) uses them for syntax
6
6
  highlighting and inline diagnostics.
7
7
 
@@ -63,7 +63,7 @@ duplicate names and a component that shadows a builtin.
63
63
  ### `program.parameterDescriptors(evaluator?)`
64
64
 
65
65
  The declared params as [`ParameterDescriptor`](describe-parameters.md#returns)s. Without an
66
- evaluator, every field but the name comes back `undefined` —
66
+ evaluator, every field but the name comes back `undefined` -
67
67
  [`describeParameters`](describe-parameters.md) is this with one supplied.
68
68
 
69
69
  #### Listing a document's models without rendering
@@ -104,10 +104,10 @@ everything unioned.
104
104
 
105
105
  #### Caveats
106
106
 
107
- - **The evaluator does not call the kernel** — except for `align`. Its output is a node tree;
107
+ - **The evaluator does not call the kernel** - except for `align`. Its output is a node tree;
108
108
  solving it is [`EvaluationContext`](evaluation-context.md)'s job.
109
109
  - Without a `context`, a document containing `align` fails with
110
- `align needs a kernel context — render the model rather than building it`.
110
+ `align needs a kernel context - render the model rather than building it`.
111
111
  - `requireParams: false` is for inspection only. A param with no value is simply absent from
112
112
  scope, so evaluating geometry that reads it will fail.
113
113
 
@@ -152,7 +152,7 @@ try {
152
152
  }
153
153
  ```
154
154
 
155
- Not every failure is a `FormaError` — a few come from the kernel, and `render` raises a plain
155
+ Not every failure is a `FormaError` - a few come from the kernel, and `render` raises a plain
156
156
  `Error` for a 2D part. Check `error.loc` rather than the class when all you need is a
157
157
  position. See [Errors](../errors.md) for the full catalogue.
158
158
 
@@ -172,7 +172,7 @@ Every block the language understands, by name, as `Record<string, BlockDefinitio
172
172
  | `combine` | `'list'` receives every child; anything else receives them unioned |
173
173
  | `build(args, children?)` | Produces the node |
174
174
 
175
- **[`part`](../reference/part.md) and [`align`](../reference/align.md) are not in it** — the
175
+ **[`part`](../reference/part.md) and [`align`](../reference/align.md) are not in it** - the
176
176
  evaluator handles both itself, because neither is an ordinary geometry operation.
177
177
 
178
178
  ```js
@@ -184,8 +184,8 @@ Object.keys(BLOCKS).filter((k) => BLOCKS[k].leaf); // just the shapes
184
184
 
185
185
  ### `FUNCTIONS`
186
186
 
187
- Every builtin function, as `Record<string, FunctionDefinition>` with `arity` — `null` for
188
- variadic — and `call(args)`.
187
+ Every builtin function, as `Record<string, FunctionDefinition>` with `arity` - `null` for
188
+ variadic - and `call(args)`.
189
189
 
190
190
  ### `CONSTANTS`
191
191
 
@@ -213,7 +213,7 @@ cannot be added without the documentation's code samples learning about it.
213
213
 
214
214
  ## Troubleshooting
215
215
 
216
- ### `align needs a kernel context — render the model rather than building it`
216
+ ### `align needs a kernel context - render the model rather than building it`
217
217
 
218
218
  Pass a context, or avoid `align` in documents you intend to evaluate without one:
219
219
 
@@ -33,7 +33,7 @@ A `Promise<RenderResult>`:
33
33
  | Field | Type | Description |
34
34
  | --- | --- | --- |
35
35
  | `name` | `string` | The model's name |
36
- | `parts` | `RenderedPart[]` | One per [`part`](../reference/part.md) — see below |
36
+ | `parts` | `RenderedPart[]` | One per [`part`](../reference/part.md) - see below |
37
37
  | `program` | [`Program`](low-level.md#program) | The parsed document, for inspecting params |
38
38
  | `context` | `EvaluationContext` | The context that owns every solid in `parts` |
39
39
  | `stats` | `RenderStats` | `nodes`, `evaluated`, `cacheHits`, `milliseconds` |
@@ -60,7 +60,7 @@ Each `RenderedPart`:
60
60
  - Params are resolved before any geometry is built, so a missing required param fails
61
61
  immediately rather than halfway through a model.
62
62
  - `options.params` sets **top-level** params only. A param declared inside a `model` is a
63
- constant — see [model params](../reference/model.md#model-params).
63
+ constant - see [model params](../reference/model.md#model-params).
64
64
 
65
65
  ---
66
66
 
@@ -129,7 +129,7 @@ async function rerender(source, params) {
129
129
  }
130
130
  ```
131
131
 
132
- Typing a digit into a dimension re-solves that wall and nothing else — `stats.cacheHits`
132
+ Typing a digit into a dimension re-solves that wall and nothing else - `stats.cacheHits`
133
133
  against `stats.evaluated` shows how much was skipped.
134
134
 
135
135
  ### Choosing a model
@@ -165,7 +165,7 @@ result.context.dispose();
165
165
  ```js
166
166
  const { stats } = await render(source, { context });
167
167
 
168
- stats.nodes; // nodes in the tree — a measure of the document's size
168
+ stats.nodes; // nodes in the tree - a measure of the document's size
169
169
  stats.evaluated; // kernel calls actually made, across the context's life
170
170
  stats.cacheHits; // how many were avoided
171
171
  stats.milliseconds; // wall clock for this render
@@ -181,7 +181,7 @@ them across calls rather than reading one in isolation.
181
181
  ### `EvaluationContext has been disposed`
182
182
 
183
183
  Something held on to a result after disposing the context that owns it. `part.concrete` and
184
- `part.mesh` are only valid while the context is alive — copy the mesh arrays out first if you
184
+ `part.mesh` are only valid while the context is alive - copy the mesh arrays out first if you
185
185
  need them later.
186
186
 
187
187
  ### Memory grows across renders
@@ -193,18 +193,18 @@ Add the `collect` call. A reused context caches every solid it has ever built, b
193
193
  context.collect(result.parts.map((part) => part.node));
194
194
  ```
195
195
 
196
- ### `part "body" is 2D — extrude or revolve it before rendering`
196
+ ### `part "body" is 2D - extrude or revolve it before rendering`
197
197
 
198
198
  The model produced a cross-section. See [2D ↔ 3D](../reference/conversions.md).
199
199
 
200
- ### `align needs a kernel context — render the model rather than building it`
200
+ ### `align needs a kernel context - render the model rather than building it`
201
201
 
202
202
  Only happens when driving [`Evaluator`](low-level.md#evaluator) directly without a context.
203
203
  `render` always supplies one.
204
204
 
205
205
  ### The error has no line number
206
206
 
207
- Most do — `FormaError` carries a `loc` with `line`, `column` and `offset`, and appends it to
207
+ Most do - `FormaError` carries a `loc` with `line`, `column` and `offset`, and appends it to
208
208
  the message. A few errors are raised before any position is known (`the document declares no
209
209
  model`) or inside a value helper that has none (`no attribute "a"`).
210
210
 
@@ -1,8 +1,8 @@
1
1
  # Values
2
2
 
3
3
  Three small immutable classes the evaluator works in: [`Vector`](#vector),
4
- [`Angle`](#angle) and [`Transform`](#transform). Documents never see them directly — a
5
- document writes lists and bare numbers — but they are exported for anyone building geometry
4
+ [`Angle`](#angle) and [`Transform`](#transform). Documents never see them directly - a
5
+ document writes lists and bare numbers - but they are exported for anyone building geometry
6
6
  through the [`GeometryNode`](geometry-node.md) API.
7
7
 
8
8
  ```js
@@ -26,7 +26,7 @@ separate 3D class would be the same code with one more letter.
26
26
  | `Vector.of(value, dimensions)` | From anything vector-like |
27
27
  | `Vector.zero(dimensions)` | The origin |
28
28
 
29
- `Vector.of` accepts a `Vector`, a **number** — broadcast to every component — an array, or an
29
+ `Vector.of` accepts a `Vector`, a **number** - broadcast to every component - an array, or an
30
30
  `{x, y, z}` object whose missing keys read as zero. This is what makes
31
31
  `scale { factor = 2 }` work the same as `factor = [2, 2, 2]`.
32
32
 
@@ -89,7 +89,7 @@ wrong rather than an error.
89
89
  | `plus(other)` `minus(other)` | Other may be an `Angle` or a number of degrees |
90
90
  | `times(factor)` | A plain multiplier, not an angle |
91
91
 
92
- The unit names appear twice — `Angle.degrees(90)` builds one, `angle.degrees` reads one back.
92
+ The unit names appear twice - `Angle.degrees(90)` builds one, `angle.degrees` reads one back.
93
93
  They are different namespaces and do not collide.
94
94
 
95
95
  ---
@@ -121,9 +121,9 @@ renders.
121
121
  | --- | --- |
122
122
  | `dim` | 2 or 3 |
123
123
  | `m` | The matrix, column-major and frozen |
124
- | `order` | Side length — one more than `dim` |
124
+ | `order` | Side length - one more than `dim` |
125
125
  | `at(row, col)` | One entry, read out of column-major storage |
126
- | `concat(other)` | `this ∘ other` — **`other` runs first** |
126
+ | `concat(other)` | `this ∘ other` - **`other` runs first** |
127
127
  | `toArray()` | A copy, safe to keep |
128
128
 
129
129
  #### Caveats
@@ -145,7 +145,7 @@ renders.
145
145
  ```js
146
146
  import { Transform, GeometryNode } from 'forma-dsl';
147
147
 
148
- // Rotate, then move — the argument to concat runs first.
148
+ // Rotate, then move - the argument to concat runs first.
149
149
  const placement = Transform.translation([30, 0, 0], 3)
150
150
  .concat(Transform.rotation3D([0, 0, 45]));
151
151
 
@@ -176,7 +176,7 @@ import { Angle } from 'forma-dsl';
176
176
  const a = Angle.degrees(30);
177
177
  a.sin; // 0.5
178
178
  a.radians; // 0.5235987755982988
179
- a.plus(60).degrees; // 90 — a bare number is read as degrees
179
+ a.plus(60).degrees; // 90 - a bare number is read as degrees
180
180
  ```
181
181
 
182
182
  ### Vector arithmetic
@@ -190,8 +190,8 @@ const wall = 2;
190
190
  const inner = size.minus(new Vector([wall * 2, wall * 2, 0]));
191
191
  inner.toArray(); // [36, 26, 12]
192
192
 
193
- Vector.of(3, 3).toArray(); // [3, 3, 3] — a scalar broadcasts
194
- Vector.of({ x: 1, z: 5 }, 3).toArray(); // [1, 0, 5] — missing keys are zero
193
+ Vector.of(3, 3).toArray(); // [3, 3, 3] - a scalar broadcasts
194
+ Vector.of({ x: 1, z: 5 }, 3).toArray(); // [1, 0, 5] - missing keys are zero
195
195
  ```
196
196
 
197
197
  ---
@@ -205,7 +205,7 @@ surfaces more helpfully as `translate: "offset" must be a number or an array of
205
205
 
206
206
  ### `cannot read an angle from string`
207
207
 
208
- `Angle.of` takes an `Angle` or a number. A string is not coerced — that would let `"90deg"`
208
+ `Angle.of` takes an `Angle` or a number. A string is not coerced - that would let `"90deg"`
209
209
  silently become `NaN`.
210
210
 
211
211
  ### My composed transform is applied in the wrong order
@@ -219,5 +219,5 @@ Every transform is about the origin. Centre the geometry, rotate, then translate
219
219
 
220
220
  ### Non-uniform scaling made my circle an ellipse
221
221
 
222
- Expected — that is how [`ellipse`](../reference/shapes-2d.md#ellipse) is built internally, and
222
+ Expected - that is how [`ellipse`](../reference/shapes-2d.md#ellipse) is built internally, and
223
223
  it is the cheapest route to an ellipsoid from a sphere.
@@ -0,0 +1,146 @@
1
+ # Command line
2
+
3
+ [`forma-dsl-cli`](https://www.npmjs.com/package/forma-dsl-cli) solves a document and writes a
4
+ binary STL, with no browser and no build step. It is what puts a `.forma` file in a Makefile,
5
+ a CI job or a release pipeline.
6
+
7
+ ```bash
8
+ npm install -g forma-dsl-cli
9
+ forma render riser.forma -p height=32 -o build/riser.stl
10
+ ```
11
+
12
+ Or, without installing it, `npx forma-dsl-cli render riser.forma -o riser.stl`.
13
+
14
+ It knows nothing about the language itself. Every answer comes from `forma-dsl` - the same
15
+ package the editor and the MCP server import - so a block added to the language is one it
16
+ renders without being told, and an error it reports is the evaluator's own, with the
17
+ evaluator's own position.
18
+
19
+ ---
20
+
21
+ ## Commands
22
+
23
+ | Command | What it does |
24
+ | --- | --- |
25
+ | `forma render <file>` | solve a document and write a binary STL |
26
+ | `forma params <file>` | list the parameters the document declares |
27
+ | `forma models <file>` | list the models the document declares |
28
+
29
+ A `<file>` of `-` reads the document from stdin. `params` and `models` take `--json`.
30
+
31
+ ### `render`
32
+
33
+ | Option | Default | What it does |
34
+ | --- | --- | --- |
35
+ | `-o, --out <file>` | `<file>.stl` | where to write. `-` is stdout |
36
+ | `-m, --model <name>` | the first declared | which [model](reference/model.md) to build |
37
+ | `--part <name>` | see below | one [part](reference/part.md) by name |
38
+ | `-p, --param <name=val>` | each param's own default | a value for one declared [param](reference/param.md). Repeatable |
39
+ | `--segments <n>` | the kernel's own | a fixed segment count for every curve |
40
+ | `--min-angle <deg>` | the kernel's own | smallest angle between adjacent segments of a curve |
41
+ | `--min-edge <n>` | the kernel's own | smallest length a curve segment may be shortened to |
42
+ | `--max-nodes <n>` | no limit | refuse a document that would build more blocks than this |
43
+ | `-q, --quiet` | off | do not print the summary |
44
+
45
+ The three resolution options are [`setQuality`](api/kernel.md) under another name, and are
46
+ global to the kernel exactly as they are there. `--max-nodes` is
47
+ [`render`](api/render.md)'s own budget.
48
+
49
+ Without `--part`, a model with one part exports that part, and a model with several exports
50
+ them **unioned**. STL has no notion of a part, and a slicer reading two overlapping shells
51
+ makes a mess of them.
52
+
53
+ ---
54
+
55
+ ## Parameter values
56
+
57
+ A value is JSON where it parses as JSON, and the string as written otherwise. That is what
58
+ makes all four of these arrive as the type the document expects, with no second layer of
59
+ quoting:
60
+
61
+ ```bash
62
+ forma render part.forma -p count=6 -p center=true -p size=[10,20] -p label=M6
63
+ ```
64
+
65
+ ---
66
+
67
+ ## In a pipeline
68
+
69
+ Only what was asked for goes to stdout - the STL under `-o -`, and the listings. The summary
70
+ and every diagnostic go to stderr, so a pipe stays clean:
71
+
72
+ ```bash
73
+ forma render riser.forma --segments 64 -o - > riser.stl
74
+ ```
75
+
76
+ Exit codes are the three a script needs to tell apart:
77
+
78
+ | Code | What happened |
79
+ | --- | --- |
80
+ | `0` | it worked |
81
+ | `1` | the document did not build - a syntax error, an unknown name, a degenerate solid |
82
+ | `2` | the invocation was wrong - an unknown flag, a missing file, a part that does not exist |
83
+
84
+ The summary names the cache hits, because that is the number explaining why the second render
85
+ of the same document in a loop is faster, and it is otherwise invisible:
86
+
87
+ ```
88
+ build/riser.stl - 252 triangles · model "riser", part "body" · 6 nodes, 6 solved, 0 cached · 9 ms
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Failures
94
+
95
+ A failure is reported where an editor and a CI annotation can both find it: the position as a
96
+ `file:line:column:` prefix, then the lines around it with a caret under the column.
97
+
98
+ ```
99
+ riser.forma:7:23: size must be positive
100
+ 5 | part "body" {
101
+ 6 | extrude {
102
+ 7 | rect { size = [-40, 20] }
103
+ | ^
104
+ 8 | }
105
+ 9 | }
106
+ ```
107
+
108
+ The position comes from the error itself - see [Errors](errors.md) for what each stage raises
109
+ and what it means.
110
+
111
+ ---
112
+
113
+ ## Building a matrix
114
+
115
+ `params --json` is the document describing itself, which is what lets a script build every
116
+ variant without being told what the variants are:
117
+
118
+ ```bash
119
+ forma params riser.forma --json
120
+ ```
121
+
122
+ ```json
123
+ [
124
+ {
125
+ "name": "height",
126
+ "type": "number",
127
+ "default": 20,
128
+ "required": false,
129
+ "min": 8,
130
+ "max": 40
131
+ }
132
+ ]
133
+ ```
134
+
135
+ ```bash
136
+ for h in 8 20 40; do
137
+ forma render riser.forma -p "height=$h" -o "build/riser-$h.stl"
138
+ done
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Units
144
+
145
+ Lengths are unitless in forma, and STL has no unit either. Every consumer treats them as
146
+ millimetres.