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,182 @@
|
|
|
1
|
+
# Kernel
|
|
2
|
+
|
|
3
|
+
Loading the Manifold WebAssembly module, and tuning how finely it approximates curves.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { loadKernel, setQuality, resetQuality } from 'forma-dsl';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Most callers never touch these. [`EvaluationContext.create()`](evaluation-context.md#evaluationcontextcreate)
|
|
10
|
+
loads the kernel for you, and `segments` on a shape handles resolution locally. Reach for
|
|
11
|
+
this page when bundling needs help finding the `.wasm`, or when a draft pass and a final pass
|
|
12
|
+
want different quality.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## `loadKernel`
|
|
17
|
+
|
|
18
|
+
### `loadKernel(options?)`
|
|
19
|
+
|
|
20
|
+
Loads and sets up the Manifold module, once per page or process.
|
|
21
|
+
|
|
22
|
+
| | |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| **`options.locateFile`** | `() => string` — returns the URL of the `.wasm` file |
|
|
25
|
+
| **Returns** | `Promise<Kernel>` — the `manifold-3d` toplevel module |
|
|
26
|
+
|
|
27
|
+
#### Caveats
|
|
28
|
+
|
|
29
|
+
- **Memoized on the promise, not the result.** Two models evaluating concurrently at startup
|
|
30
|
+
share one instantiation rather than each starting their own.
|
|
31
|
+
- **Only the first call's options take effect.** Later calls get the memoized promise, so
|
|
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
|
|
34
|
+
rather than being permanently broken for the page.
|
|
35
|
+
|
|
36
|
+
### Bundlers and `locateFile`
|
|
37
|
+
|
|
38
|
+
A bundler that fingerprints assets renames `manifold.wasm`, and the module cannot then find
|
|
39
|
+
it by its own relative path. Point at the built URL:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
import wasmUrl from 'manifold-3d/manifold.wasm?url';
|
|
43
|
+
import { loadKernel } from 'forma-dsl';
|
|
44
|
+
|
|
45
|
+
await loadKernel({ locateFile: () => wasmUrl });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Do this **before** the first `EvaluationContext.create()`, or the default path is what gets
|
|
49
|
+
memoized.
|
|
50
|
+
|
|
51
|
+
In Node, nothing is needed — the module resolves the file itself.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## `setQuality`
|
|
56
|
+
|
|
57
|
+
### `setQuality(options?)`
|
|
58
|
+
|
|
59
|
+
Sets the resolution used for curved surfaces, globally.
|
|
60
|
+
|
|
61
|
+
| Option | Type | Description |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| `minAngle` | `number` | Smallest angle, in degrees, between adjacent segments of a curve |
|
|
64
|
+
| `minEdgeLength` | `number` | Smallest length a curve segment may be shortened to |
|
|
65
|
+
| `segments` | `number` | A fixed segment count, overriding both limits above |
|
|
66
|
+
|
|
67
|
+
Returns `Promise<void>`, resolving once the kernel is loaded and configured. Each option is
|
|
68
|
+
left alone when omitted.
|
|
69
|
+
|
|
70
|
+
#### Caveats
|
|
71
|
+
|
|
72
|
+
- **These are global to the kernel, not per model.** A viewer that renders a draft and then a
|
|
73
|
+
final pass has to set them around each evaluation, not once at load.
|
|
74
|
+
- **Quality settings are not part of a node's digest.** The cache is keyed on content, so a
|
|
75
|
+
solid built at draft quality will be *reused* at final quality unless the cache is cleared.
|
|
76
|
+
See [Usage](#a-draft-pass-and-a-final-pass).
|
|
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
|
|
79
|
+
a global.
|
|
80
|
+
|
|
81
|
+
## `resetQuality`
|
|
82
|
+
|
|
83
|
+
### `resetQuality()`
|
|
84
|
+
|
|
85
|
+
Restores the kernel's own defaults. Returns `Promise<void>`.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
### A draft pass and a final pass
|
|
92
|
+
|
|
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:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
import { setQuality, EvaluationContext, render } from 'forma-dsl';
|
|
98
|
+
|
|
99
|
+
// Draft: fast, coarse.
|
|
100
|
+
await setQuality({ segments: 16 });
|
|
101
|
+
let context = await EvaluationContext.create();
|
|
102
|
+
const draft = await render(source, { params, context });
|
|
103
|
+
draw(draft.parts);
|
|
104
|
+
context.dispose();
|
|
105
|
+
|
|
106
|
+
// Final: a fresh context, so nothing coarse survives.
|
|
107
|
+
await setQuality({ minAngle: 1, minEdgeLength: 0.2 });
|
|
108
|
+
context = await EvaluationContext.create();
|
|
109
|
+
const final = await render(source, { params, context });
|
|
110
|
+
draw(final.parts);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
If instead you want one context throughout, put the resolution in the document so it *is*
|
|
114
|
+
part of the digest:
|
|
115
|
+
|
|
116
|
+
```hcl
|
|
117
|
+
param facets { type = number default = 32 min = 8 max = 160 step = 8 }
|
|
118
|
+
|
|
119
|
+
model "m" {
|
|
120
|
+
cylinder { radius = 10 height = 20 segments = var.facets }
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
That is the better answer for an editor: changing `facets` changes the content, so the cache
|
|
125
|
+
does the right thing automatically.
|
|
126
|
+
|
|
127
|
+
### Global quality for an export
|
|
128
|
+
|
|
129
|
+
A one-off high-quality export, then back to normal:
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
await setQuality({ minEdgeLength: 0.1 });
|
|
133
|
+
try {
|
|
134
|
+
const context = await EvaluationContext.create();
|
|
135
|
+
const result = await render(source, { params, context });
|
|
136
|
+
await writeFile('final.stl', toBinarySTL(result.parts[0].concrete));
|
|
137
|
+
context.dispose();
|
|
138
|
+
} finally {
|
|
139
|
+
await resetQuality();
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Reaching the kernel directly
|
|
144
|
+
|
|
145
|
+
`context.wasm` is the loaded module, and every Manifold API is on it:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
const context = await EvaluationContext.create();
|
|
149
|
+
const { Manifold, CrossSection } = context.wasm;
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Anything you construct this way is **yours** to delete — the context only owns what it built
|
|
153
|
+
itself.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Troubleshooting
|
|
158
|
+
|
|
159
|
+
### The page 404s on `manifold.wasm`
|
|
160
|
+
|
|
161
|
+
The bundler renamed it. Pass `locateFile`, before the first context is created:
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
import wasmUrl from 'manifold-3d/manifold.wasm?url';
|
|
165
|
+
await loadKernel({ locateFile: () => wasmUrl });
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### `locateFile` is being ignored
|
|
169
|
+
|
|
170
|
+
The kernel was already loaded — the promise is memoized, so only the first call's options
|
|
171
|
+
count. Move the `loadKernel` call earlier, before anything creates a context.
|
|
172
|
+
|
|
173
|
+
### `setQuality` had no effect on an existing model
|
|
174
|
+
|
|
175
|
+
The geometry was already cached. Quality is not part of the digest, so the cached solid is
|
|
176
|
+
returned unchanged. Use a fresh context, or put the segment count in the document where it
|
|
177
|
+
becomes part of the content.
|
|
178
|
+
|
|
179
|
+
### Curves are too coarse in one place only
|
|
180
|
+
|
|
181
|
+
Set `segments` on that shape rather than moving the global. It is per-shape, it is part of the
|
|
182
|
+
digest, and it is visible to whoever reads the model.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Compiler stages
|
|
2
|
+
|
|
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
|
|
5
|
+
[editor](../../../editor) uses them for syntax
|
|
6
|
+
highlighting and inline diagnostics.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import {
|
|
10
|
+
tokenize, parse, Program, Evaluator, FormaError,
|
|
11
|
+
BLOCKS, FUNCTIONS, CONSTANTS,
|
|
12
|
+
} from 'forma-dsl';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For ordinary use, [`render`](render.md) and
|
|
16
|
+
[`describeParameters`](describe-parameters.md) are the front door.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## `tokenize`
|
|
21
|
+
|
|
22
|
+
### `tokenize(source)`
|
|
23
|
+
|
|
24
|
+
Turns source into tokens. Returns `Token[]`, ending with an `eof`.
|
|
25
|
+
|
|
26
|
+
| Field | Type | Description |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `type` | `'ident' \| 'number' \| 'string' \| 'punct' \| 'eof'` | |
|
|
29
|
+
| `value` | `any` | A string for `ident` and `punct`, a number for `number`, the parsed runs and splices for `string`, `null` for `eof` |
|
|
30
|
+
| `loc` | `SourceLocation` | `{ line, column, offset }` |
|
|
31
|
+
| `nlBefore` | `boolean` | Whether a line break preceded this token |
|
|
32
|
+
|
|
33
|
+
`nlBefore` is what the parser uses to know an attribute has ended. See
|
|
34
|
+
[the newline rule](../language/syntax.md#the-newline-rule).
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## `parse`
|
|
39
|
+
|
|
40
|
+
### `parse(source)`
|
|
41
|
+
|
|
42
|
+
Turns source into an abstract syntax tree. Returns a `FormaDocument`.
|
|
43
|
+
|
|
44
|
+
The AST's shape is an implementation detail of the evaluator and is typed loosely on purpose.
|
|
45
|
+
Use [`Program`](#program) to get at declarations in a stable form.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## `Program`
|
|
50
|
+
|
|
51
|
+
### `Program.parse(source)` · `new Program(document)`
|
|
52
|
+
|
|
53
|
+
A parsed document, indexed by what each declaration is for. Constructing one is what catches
|
|
54
|
+
duplicate names and a component that shadows a builtin.
|
|
55
|
+
|
|
56
|
+
| Property | Type | Description |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `params` | `Map<string, unknown>` | Declared params, in source order |
|
|
59
|
+
| `locals` | `unknown[]` | Declared locals, in source order |
|
|
60
|
+
| `components` | `Map<string, unknown>` | Declared components |
|
|
61
|
+
| `models` | `Map<string, unknown>` | Declared models |
|
|
62
|
+
|
|
63
|
+
### `program.parameterDescriptors(evaluator?)`
|
|
64
|
+
|
|
65
|
+
The declared params as [`ParameterDescriptor`](describe-parameters.md#returns)s. Without an
|
|
66
|
+
evaluator, every field but the name comes back `undefined` —
|
|
67
|
+
[`describeParameters`](describe-parameters.md) is this with one supplied.
|
|
68
|
+
|
|
69
|
+
#### Listing a document's models without rendering
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import { Program } from 'forma-dsl';
|
|
73
|
+
|
|
74
|
+
const program = Program.parse(source);
|
|
75
|
+
[...program.models.keys()]; // ['printable', 'assembly']
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## `Evaluator`
|
|
81
|
+
|
|
82
|
+
### `new Evaluator(program, options?)`
|
|
83
|
+
|
|
84
|
+
Turns a program into geometry nodes. Resolving params and locals happens during
|
|
85
|
+
**construction**, so every error about a missing or unreadable input surfaces before any
|
|
86
|
+
geometry is built.
|
|
87
|
+
|
|
88
|
+
| Option | Type | Description |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| `params` | `Record<string, ParameterValue>` | Values for the document's params |
|
|
91
|
+
| `context` | [`EvaluationContext`](evaluation-context.md) `\| null` | Needed only by [`align`](../reference/align.md), which measures its child |
|
|
92
|
+
| `requireParams` | `boolean`, default `true` | Off lets a document declaring a required param still be inspected |
|
|
93
|
+
|
|
94
|
+
| Member | Description |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `program` | The program being evaluated |
|
|
97
|
+
| `context` | The kernel context, if any |
|
|
98
|
+
| `root` | The document-level [scope](../language/scope.md#the-root-scope) |
|
|
99
|
+
| `expression(node, scope)` | Evaluates one expression |
|
|
100
|
+
| `model(name?)` | Builds one model into a `Scene`. Defaults to the first declared |
|
|
101
|
+
|
|
102
|
+
A `Scene` is `{ name, parts, node }`, where `parts` carry `GeometryNode`s and `node` is
|
|
103
|
+
everything unioned.
|
|
104
|
+
|
|
105
|
+
#### Caveats
|
|
106
|
+
|
|
107
|
+
- **The evaluator does not call the kernel** — except for `align`. Its output is a node tree;
|
|
108
|
+
solving it is [`EvaluationContext`](evaluation-context.md)'s job.
|
|
109
|
+
- Without a `context`, a document containing `align` fails with
|
|
110
|
+
`align needs a kernel context — render the model rather than building it`.
|
|
111
|
+
- `requireParams: false` is for inspection only. A param with no value is simply absent from
|
|
112
|
+
scope, so evaluating geometry that reads it will fail.
|
|
113
|
+
|
|
114
|
+
#### Building a node tree without solving it
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import { Program, Evaluator } from 'forma-dsl';
|
|
118
|
+
|
|
119
|
+
const program = Program.parse(source);
|
|
120
|
+
const scene = await new Evaluator(program, { params: { height: 20 } }).model();
|
|
121
|
+
|
|
122
|
+
scene.node.digest; // identity of the whole model
|
|
123
|
+
scene.node.subtreeSize; // how many nodes it took
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Useful for diffing two documents, or for caching geometry identity without loading WASM at
|
|
127
|
+
all.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## `FormaError`
|
|
132
|
+
|
|
133
|
+
Every error the language raises. Extends `Error`.
|
|
134
|
+
|
|
135
|
+
| Property | Type | Description |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| `name` | `'FormaError'` | |
|
|
138
|
+
| `loc` | `SourceLocation \| null` | Where it was raised, when known |
|
|
139
|
+
|
|
140
|
+
The position is appended to the message as well as kept on the error, so a caller that only
|
|
141
|
+
prints `error.message` still tells the reader where to look.
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
try {
|
|
145
|
+
await render(source, { context });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error.loc) {
|
|
148
|
+
markProblem(error.loc.line, error.loc.column, error.message);
|
|
149
|
+
} else {
|
|
150
|
+
showBanner(error.message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Not every failure is a `FormaError` — a few come from the kernel, and `render` raises a plain
|
|
156
|
+
`Error` for a 2D part. Check `error.loc` rather than the class when all you need is a
|
|
157
|
+
position. See [Errors](../errors.md) for the full catalogue.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Registries
|
|
162
|
+
|
|
163
|
+
### `BLOCKS`
|
|
164
|
+
|
|
165
|
+
Every block the language understands, by name, as `Record<string, BlockDefinition>`.
|
|
166
|
+
|
|
167
|
+
| Field | Description |
|
|
168
|
+
| --- | --- |
|
|
169
|
+
| `dim` | Output dimensionality, where it is fixed regardless of the children |
|
|
170
|
+
| `leaf` | True for a shape, which takes no children |
|
|
171
|
+
| `takes` | What the children must be; `'same'` passes their dimensionality through |
|
|
172
|
+
| `combine` | `'list'` receives every child; anything else receives them unioned |
|
|
173
|
+
| `build(args, children?)` | Produces the node |
|
|
174
|
+
|
|
175
|
+
**[`part`](../reference/part.md) and [`align`](../reference/align.md) are not in it** — the
|
|
176
|
+
evaluator handles both itself, because neither is an ordinary geometry operation.
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
import { BLOCKS } from 'forma-dsl';
|
|
180
|
+
|
|
181
|
+
Object.keys(BLOCKS).length; // the builtin blocks
|
|
182
|
+
Object.keys(BLOCKS).filter((k) => BLOCKS[k].leaf); // just the shapes
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `FUNCTIONS`
|
|
186
|
+
|
|
187
|
+
Every builtin function, as `Record<string, FunctionDefinition>` with `arity` — `null` for
|
|
188
|
+
variadic — and `call(args)`.
|
|
189
|
+
|
|
190
|
+
### `CONSTANTS`
|
|
191
|
+
|
|
192
|
+
`{ pi, e }`.
|
|
193
|
+
|
|
194
|
+
#### Driving an editor from the registries
|
|
195
|
+
|
|
196
|
+
Keeping completion and highlighting honest means reading the registries rather than a
|
|
197
|
+
hand-kept list:
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
import { BLOCKS, FUNCTIONS, CONSTANTS } from 'forma-dsl';
|
|
201
|
+
|
|
202
|
+
const completions = [
|
|
203
|
+
...Object.keys(BLOCKS).map((name) => ({ label: name, type: 'class' })),
|
|
204
|
+
...Object.keys(FUNCTIONS).map((name) => ({ label: name, type: 'function' })),
|
|
205
|
+
...Object.keys(CONSTANTS).map((name) => ({ label: name, type: 'constant' })),
|
|
206
|
+
];
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The docs site does the same thing for its syntax highlighting, which is why a new block
|
|
210
|
+
cannot be added without the documentation's code samples learning about it.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Troubleshooting
|
|
215
|
+
|
|
216
|
+
### `align needs a kernel context — render the model rather than building it`
|
|
217
|
+
|
|
218
|
+
Pass a context, or avoid `align` in documents you intend to evaluate without one:
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
new Evaluator(program, { context: await EvaluationContext.create() });
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### `param "x" has no default and no value was supplied`
|
|
225
|
+
|
|
226
|
+
Thrown from the `Evaluator` **constructor**, not from `model()`. Pass `requireParams: false`
|
|
227
|
+
to inspect such a document without values.
|
|
228
|
+
|
|
229
|
+
### My hand-driven evaluation returns nodes, not solids
|
|
230
|
+
|
|
231
|
+
That is the design. Pass each `part.node` to
|
|
232
|
+
[`context.evaluate`](evaluation-context.md#contextevaluatenode), or use
|
|
233
|
+
[`render`](render.md), which does both.
|
|
234
|
+
|
|
235
|
+
### `part` and `align` are missing from `BLOCKS`
|
|
236
|
+
|
|
237
|
+
Neither is an ordinary block: a `part` is a piece of the scene rather than an operand, and
|
|
238
|
+
`align` has to measure its child. Both are handled directly by the evaluator. Add them to any
|
|
239
|
+
list you build from `BLOCKS` for an editor.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# `render`
|
|
2
|
+
|
|
3
|
+
Compiles a document and evaluates one model into renderable parts.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { render } from 'forma-dsl';
|
|
7
|
+
|
|
8
|
+
const result = await render(source, { params: { height: 20 } });
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Reference
|
|
14
|
+
|
|
15
|
+
### `render(source, options?)`
|
|
16
|
+
|
|
17
|
+
Parses `source`, resolves its params, evaluates one model into a geometry tree, and solves
|
|
18
|
+
every part into a concrete solid and a mesh.
|
|
19
|
+
|
|
20
|
+
#### Parameters
|
|
21
|
+
|
|
22
|
+
| Parameter | Type | Description |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| `source` | `string` | The document |
|
|
25
|
+
| `options.model` | `string \| null` | Which model to build. Defaults to the **first** the document declares |
|
|
26
|
+
| `options.params` | `Record<string, ParameterValue>` | Values for the document's top-level params. Anything not declared is ignored |
|
|
27
|
+
| `options.context` | [`EvaluationContext`](evaluation-context.md) `\| null` | A context to reuse. One is created if you do not pass one |
|
|
28
|
+
|
|
29
|
+
#### Returns
|
|
30
|
+
|
|
31
|
+
A `Promise<RenderResult>`:
|
|
32
|
+
|
|
33
|
+
| Field | Type | Description |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `name` | `string` | The model's name |
|
|
36
|
+
| `parts` | `RenderedPart[]` | One per [`part`](../reference/part.md) — see below |
|
|
37
|
+
| `program` | [`Program`](low-level.md#program) | The parsed document, for inspecting params |
|
|
38
|
+
| `context` | `EvaluationContext` | The context that owns every solid in `parts` |
|
|
39
|
+
| `stats` | `RenderStats` | `nodes`, `evaluated`, `cacheHits`, `milliseconds` |
|
|
40
|
+
|
|
41
|
+
Each `RenderedPart`:
|
|
42
|
+
|
|
43
|
+
| Field | Type | Description |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| `name` | `string` | From the part's label |
|
|
46
|
+
| `color` | `string` | From the part's `color` attribute |
|
|
47
|
+
| `opacity` | `number` | From the part's `opacity` attribute |
|
|
48
|
+
| `node` | [`GeometryNode`](geometry-node.md) | The part's tree. This is what `collect` wants |
|
|
49
|
+
| `concrete` | `Solid` | The solved Manifold object, **owned by the context** |
|
|
50
|
+
| `mesh` | `RenderMesh` | `positions`, `normals`, `triangleCount`, `vertexCount` |
|
|
51
|
+
|
|
52
|
+
#### Caveats
|
|
53
|
+
|
|
54
|
+
- **The caller owns `result.context` and must dispose it.** WebAssembly objects are not
|
|
55
|
+
garbage-collected; nothing is freed until you say so.
|
|
56
|
+
- **Never call `delete()` on `part.concrete` yourself.** It belongs to the context, which
|
|
57
|
+
may still have it cached under its digest.
|
|
58
|
+
- If `render` throws and it created the context itself, that context is disposed before the
|
|
59
|
+
error propagates. A context you supplied is left alone, since you may still want it.
|
|
60
|
+
- Params are resolved before any geometry is built, so a missing required param fails
|
|
61
|
+
immediately rather than halfway through a model.
|
|
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).
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
### Rendering once
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import { render, toBinarySTL } from 'forma-dsl';
|
|
73
|
+
import { writeFile } from 'node:fs/promises';
|
|
74
|
+
|
|
75
|
+
const result = await render(source);
|
|
76
|
+
|
|
77
|
+
await writeFile('part.stl', toBinarySTL(result.parts[0].concrete));
|
|
78
|
+
|
|
79
|
+
result.context.dispose();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
A single render can afford to let `render` create the context and dispose it immediately
|
|
83
|
+
afterwards. Anything more than that should reuse one.
|
|
84
|
+
|
|
85
|
+
### Rendering many variants
|
|
86
|
+
|
|
87
|
+
Reusing a context is the whole point of the digest cache: only the subtrees whose content
|
|
88
|
+
actually changed are re-evaluated.
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
import { render, toBinarySTL, EvaluationContext } from 'forma-dsl';
|
|
92
|
+
import { writeFile } from 'node:fs/promises';
|
|
93
|
+
|
|
94
|
+
const context = await EvaluationContext.create();
|
|
95
|
+
|
|
96
|
+
for (const height of [10, 20, 30]) {
|
|
97
|
+
const result = await render(source, { params: { height }, context });
|
|
98
|
+
await writeFile(`part-${height}.stl`, toBinarySTL(result.parts[0].concrete));
|
|
99
|
+
|
|
100
|
+
// Free what this tree can no longer reach, so a long run stays bounded.
|
|
101
|
+
context.collect(result.parts.map((part) => part.node));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
context.dispose();
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The `collect` call is what keeps memory flat across a long run. Without it the context
|
|
108
|
+
accumulates every intermediate solid from every variant.
|
|
109
|
+
|
|
110
|
+
### Driving a live editor
|
|
111
|
+
|
|
112
|
+
The same pattern, with one context for the life of the page:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
const context = await EvaluationContext.create();
|
|
116
|
+
let current = null;
|
|
117
|
+
|
|
118
|
+
async function rerender(source, params) {
|
|
119
|
+
try {
|
|
120
|
+
const result = await render(source, { params, context });
|
|
121
|
+
draw(result.parts);
|
|
122
|
+
current = result;
|
|
123
|
+
context.collect(result.parts.map((part) => part.node));
|
|
124
|
+
return result.stats;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
showProblem(error.message, error.loc); // FormaError carries a position
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Typing a digit into a dimension re-solves that wall and nothing else — `stats.cacheHits`
|
|
133
|
+
against `stats.evaluated` shows how much was skipped.
|
|
134
|
+
|
|
135
|
+
### Choosing a model
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
await render(source); // the first model declared
|
|
139
|
+
await render(source, { model: 'assembly' }); // by name
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
To list what a document offers:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
const { program } = await render(source);
|
|
146
|
+
[...program.models.keys()]; // ['printable', 'assembly']
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Or without rendering: `Program.parse(source).models.keys()`.
|
|
150
|
+
|
|
151
|
+
### Exporting each part separately
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
const result = await render(source);
|
|
155
|
+
|
|
156
|
+
for (const part of result.parts) {
|
|
157
|
+
await writeFile(`${part.name}.stl`, toBinarySTL(part.concrete, part.name));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
result.context.dispose();
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Reading the statistics
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
const { stats } = await render(source, { context });
|
|
167
|
+
|
|
168
|
+
stats.nodes; // nodes in the tree — a measure of the document's size
|
|
169
|
+
stats.evaluated; // kernel calls actually made, across the context's life
|
|
170
|
+
stats.cacheHits; // how many were avoided
|
|
171
|
+
stats.milliseconds; // wall clock for this render
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`evaluated` and `cacheHits` are running totals on the **context**, not per render, so compare
|
|
175
|
+
them across calls rather than reading one in isolation.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Troubleshooting
|
|
180
|
+
|
|
181
|
+
### `EvaluationContext has been disposed`
|
|
182
|
+
|
|
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
|
|
185
|
+
need them later.
|
|
186
|
+
|
|
187
|
+
### Memory grows across renders
|
|
188
|
+
|
|
189
|
+
Add the `collect` call. A reused context caches every solid it has ever built, by design;
|
|
190
|
+
`collect(roots)` frees what the current tree can no longer reach.
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
context.collect(result.parts.map((part) => part.node));
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `part "body" is 2D — extrude or revolve it before rendering`
|
|
197
|
+
|
|
198
|
+
The model produced a cross-section. See [2D ↔ 3D](../reference/conversions.md).
|
|
199
|
+
|
|
200
|
+
### `align needs a kernel context — render the model rather than building it`
|
|
201
|
+
|
|
202
|
+
Only happens when driving [`Evaluator`](low-level.md#evaluator) directly without a context.
|
|
203
|
+
`render` always supplies one.
|
|
204
|
+
|
|
205
|
+
### The error has no line number
|
|
206
|
+
|
|
207
|
+
Most do — `FormaError` carries a `loc` with `line`, `column` and `offset`, and appends it to
|
|
208
|
+
the message. A few errors are raised before any position is known (`the document declares no
|
|
209
|
+
model`) or inside a value helper that has none (`no attribute "a"`).
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
try {
|
|
213
|
+
await render(source);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
error.loc?.line; // undefined when the error has no position
|
|
216
|
+
}
|
|
217
|
+
```
|