forma-dsl-mcp 0.3.8 → 0.4.0
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/README.md +22 -18
- package/package.json +2 -2
- package/src/bin.js +1 -1
- package/src/catalogue.js +15 -15
- package/src/document.js +8 -38
- package/src/examples.js +1 -1
- package/src/guide.js +24 -24
- package/src/reference/README.md +15 -5
- package/src/reference/api/describe-parameters.md +4 -4
- package/src/reference/api/evaluation-context.md +13 -13
- package/src/reference/api/export.md +7 -7
- package/src/reference/api/geometry-node.md +9 -9
- package/src/reference/api/kernel.md +8 -8
- package/src/reference/api/low-level.md +9 -9
- package/src/reference/api/render.md +8 -8
- package/src/reference/api/values.md +12 -12
- package/src/reference/cli.md +199 -0
- package/src/reference/errors.md +14 -14
- package/src/reference/language/control-flow.md +3 -3
- package/src/reference/language/expressions.md +10 -10
- package/src/reference/language/overview.md +11 -11
- package/src/reference/language/scope.md +13 -13
- package/src/reference/language/syntax.md +4 -4
- package/src/reference/reference/align.md +9 -9
- package/src/reference/reference/booleans.md +5 -5
- package/src/reference/reference/component.md +12 -12
- package/src/reference/reference/conversions.md +8 -8
- package/src/reference/reference/functions.md +8 -8
- package/src/reference/reference/local.md +4 -4
- package/src/reference/reference/model.md +10 -10
- package/src/reference/reference/param.md +8 -8
- package/src/reference/reference/part.md +12 -12
- package/src/reference/reference/refinement.md +10 -10
- package/src/reference/reference/shapes-2d.md +15 -15
- package/src/reference/reference/shapes-3d.md +13 -13
- package/src/reference/reference/transforms.md +10 -10
- package/src/reference.js +4 -3
- package/src/roots.js +2 -2
- package/src/server.js +2 -2
- package/src/tools.js +15 -15
- package/src/workspace.js +7 -7
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://m8ven.ai/verified/verify?id=d3529b4756004447)
|
|
6
6
|
|
|
7
7
|
A [Model Context Protocol](https://modelcontextprotocol.io) server that knows
|
|
8
|
-
[forma](https://www.npmjs.com/package/forma-dsl)
|
|
8
|
+
[forma](https://www.npmjs.com/package/forma-dsl) - the declarative DSL for 3D solids - and
|
|
9
9
|
builds it with the real geometry kernel, so a `.forma` document is measured before anyone is
|
|
10
10
|
told it works.
|
|
11
11
|
|
|
@@ -15,6 +15,10 @@ it.
|
|
|
15
15
|
|
|
16
16
|
It speaks over stdio, and there is nothing to configure.
|
|
17
17
|
|
|
18
|
+
**[The site](https://kalpak44.github.io/forma-dsl/)** ·
|
|
19
|
+
**[Try the editor](https://kalpak44.github.io/forma-dsl/editor/)** ·
|
|
20
|
+
**[Read the reference](https://kalpak44.github.io/forma-dsl/docs/)**
|
|
21
|
+
|
|
18
22
|
## Installing it
|
|
19
23
|
|
|
20
24
|
**Claude Code**
|
|
@@ -57,7 +61,7 @@ The server does not choose. It asks the client over MCP's
|
|
|
57
61
|
writes only inside the directories the client hands over. A relative path is taken against
|
|
58
62
|
the first of them, and every write reports the directory it landed in.
|
|
59
63
|
|
|
60
|
-
**Every path is confined to those directories**
|
|
64
|
+
**Every path is confined to those directories** - an absolute path outside them, a `..` that
|
|
61
65
|
climbs out, or a symlink pointing out of the tree is refused rather than followed. When the
|
|
62
66
|
client says its roots have changed, the server re-reads them before the next call.
|
|
63
67
|
|
|
@@ -76,7 +80,7 @@ cwd = "/absolute/path/to/your/models"
|
|
|
76
80
|
A model that has read the language's documentation still writes forma that does not build.
|
|
77
81
|
The difference this server makes is that it **runs the geometry kernel**. Every tool compiles
|
|
78
82
|
the document, solves it with [Manifold](https://github.com/elalish/manifold) and reports back
|
|
79
|
-
the bounding box, the volume, the triangle count and the genus of every part
|
|
83
|
+
the bounding box, the volume, the triangle count and the genus of every part - so the
|
|
80
84
|
question "is this the size that was asked for, and is it actually a solid?" is answered
|
|
81
85
|
before the work is handed over. A part that silently solved to nothing is called out by name,
|
|
82
86
|
and an error comes back with the line, the column and an excerpt with a caret on it.
|
|
@@ -84,7 +88,7 @@ and an error comes back with the line, the column and an excerpt with a caret on
|
|
|
84
88
|
`forma_write` **refuses to write a document that does not render**.
|
|
85
89
|
|
|
86
90
|
Refusals carry the vocabulary too: an unknown block comes back with every block there is, and
|
|
87
|
-
an unknown attribute with the ones that block reads. That is the backstop, not the teacher
|
|
91
|
+
an unknown attribute with the ones that block reads. That is the backstop, not the teacher -
|
|
88
92
|
it is enough to correct a misspelling and not enough to learn a language, since it gives the
|
|
89
93
|
name of a block and nothing about what that block means or what it takes. `forma_guide`,
|
|
90
94
|
`forma_blocks` and `forma_reference` are what answer that, and they are bundled so they
|
|
@@ -120,24 +124,24 @@ is rendered by the test suite.
|
|
|
120
124
|
|
|
121
125
|
The same knowledge in the form a person can attach to a conversation by hand:
|
|
122
126
|
|
|
123
|
-
- `forma://guide`
|
|
124
|
-
- `forma://catalogue`
|
|
125
|
-
- `forma://reference/{path}`
|
|
126
|
-
- `forma://example/{name}`
|
|
127
|
+
- `forma://guide` - the language in brief
|
|
128
|
+
- `forma://catalogue` - every block and function, as JSON
|
|
129
|
+
- `forma://reference/{path}` - one page of the manual
|
|
130
|
+
- `forma://example/{name}` - one worked document
|
|
127
131
|
|
|
128
132
|
## Asking for a part
|
|
129
133
|
|
|
130
134
|
Install it in Codex as above, `cd` to wherever you keep your models, and ask:
|
|
131
135
|
|
|
132
|
-
> **you**
|
|
136
|
+
> **you** - build me a forma file with a tree in it
|
|
133
137
|
|
|
134
|
-
> **codex**
|
|
138
|
+
> **codex** - Where should I put it?
|
|
135
139
|
|
|
136
|
-
> **you**
|
|
140
|
+
> **you** - the current folder
|
|
137
141
|
|
|
138
142
|
Codex reads the language before it writes any: `forma_guide` for the shape of a document,
|
|
139
|
-
`forma_blocks` for the attributes of the blocks it is about to use
|
|
140
|
-
and `for`
|
|
143
|
+
`forma_blocks` for the attributes of the blocks it is about to use - here `cone`, `translate`
|
|
144
|
+
and `for` - and `forma_examples` for a document of a similar shape. Then it writes, and
|
|
141
145
|
`forma_write` builds what it wrote with the real kernel before anything reaches the disk.
|
|
142
146
|
|
|
143
147
|
Where the vocabulary runs out, the refusals carry it. A guess at a block that does not exist
|
|
@@ -145,17 +149,17 @@ comes back with every block that does, and a guess at an attribute with the ones
|
|
|
145
149
|
reads:
|
|
146
150
|
|
|
147
151
|
```
|
|
148
|
-
unknown block "frustum"
|
|
152
|
+
unknown block "frustum" - the blocks are "align", "for", "if", "part", "box", "circle",
|
|
149
153
|
"cone", "cylinder", "difference", … "union"
|
|
150
154
|
|
|
151
|
-
cone: unknown attribute "bottom_radius"
|
|
155
|
+
cone: unknown attribute "bottom_radius" - cone reads "center", "height", "radius",
|
|
152
156
|
"segments", "top_radius"
|
|
153
157
|
```
|
|
154
158
|
|
|
155
159
|
Then it lands, and the tool answers with the part rather than with a claim about it:
|
|
156
160
|
|
|
157
161
|
```
|
|
158
|
-
Wrote `tree.forma` under `/home/you/models`
|
|
162
|
+
Wrote `tree.forma` under `/home/you/models` - 537 bytes.
|
|
159
163
|
|
|
160
164
|
| Part | Colour | Opacity | Size (X × Y × Z) | Volume | Triangles | Genus |
|
|
161
165
|
| `trunk` | #6b4423 | 1 | 8 × 8 × 30 | 1004.682 | 108 | 0 |
|
|
@@ -243,7 +247,7 @@ this tarball, so it works with no network at all. There is no telemetry, no anal
|
|
|
243
247
|
update check and no account.
|
|
244
248
|
|
|
245
249
|
It writes only inside the directories your client declares as MCP roots, never replaces a file
|
|
246
|
-
unless told to, and leaves nothing elsewhere
|
|
250
|
+
unless told to, and leaves nothing elsewhere - no cache, no config, no logs.
|
|
247
251
|
|
|
248
252
|
Your MCP client is a separate program with its own policy. Documents you ask a model to write
|
|
249
253
|
travel wherever that client sends them, which this server neither controls nor sees.
|
|
@@ -252,4 +256,4 @@ Full text: [PRIVACY.md](https://github.com/kalpak44/forma-dsl/blob/main/PRIVACY.
|
|
|
252
256
|
|
|
253
257
|
## License
|
|
254
258
|
|
|
255
|
-
MIT
|
|
259
|
+
MIT - see [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forma-dsl-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A stdio MCP server that knows the forma DSL and writes .forma documents that render.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
49
|
-
"forma-dsl": "^0.
|
|
49
|
+
"forma-dsl": "^0.4.0",
|
|
50
50
|
"zod": "^4.1.13"
|
|
51
51
|
}
|
|
52
52
|
}
|
package/src/bin.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* The executable: one MCP server, speaking over stdin and stdout.
|
|
4
4
|
*
|
|
5
|
-
* Nothing may be written to stdout but the protocol
|
|
5
|
+
* Nothing may be written to stdout but the protocol - a stray `console.log` corrupts the
|
|
6
6
|
* JSON-RPC stream and the client disconnects with an error that names nothing. Everything
|
|
7
7
|
* this file has to say goes to stderr, which clients collect as the server's log.
|
|
8
8
|
*
|
package/src/catalogue.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What the server knows about every construct in the language.
|
|
3
3
|
*
|
|
4
|
-
* The descriptions and the attribute tables are written here; the structural facts
|
|
4
|
+
* The descriptions and the attribute tables are written here; the structural facts - which
|
|
5
5
|
* blocks exist, what dimensionality each produces and takes, which functions exist and with
|
|
6
|
-
* what arity
|
|
6
|
+
* what arity - are read from the package's own registries at load time and merged in. Where
|
|
7
7
|
* the two disagree, the registry wins and `catalogueDrift` says so, which is what
|
|
8
8
|
* `test/catalogue.test.js` asserts is empty. A block added to the language therefore cannot
|
|
9
9
|
* ship without this file learning about it.
|
|
@@ -81,7 +81,7 @@ const BLOCK_ENTRIES = [
|
|
|
81
81
|
CENTER,
|
|
82
82
|
],
|
|
83
83
|
example: 'rounded_rect { size = [62, 13] radius = 6.5 center = true }',
|
|
84
|
-
notes: ['`rounded_rect { size = [10, 10] radius = 5 }` is a circle of diameter 10
|
|
84
|
+
notes: ['`rounded_rect { size = [10, 10] radius = 5 }` is a circle of diameter 10 - `size` is the outer size.'],
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
87
|
name: 'circle', group: '2D shapes', summary: 'A circle, from a radius or a diameter.',
|
|
@@ -112,7 +112,7 @@ const BLOCK_ENTRIES = [
|
|
|
112
112
|
{ name: 'sides', type: 'integer', default: 'required', description: 'At least 3' },
|
|
113
113
|
{
|
|
114
114
|
name: 'width_across_flats', type: 'number',
|
|
115
|
-
description: 'Distance between opposite flats
|
|
115
|
+
description: 'Distance between opposite flats - how a nut or a hex key is specified',
|
|
116
116
|
},
|
|
117
117
|
{ name: 'width_across_corners', type: 'number', description: 'Distance between opposite corners' },
|
|
118
118
|
{ name: 'radius', type: 'number', default: '1', description: 'Centre to corner' },
|
|
@@ -479,17 +479,17 @@ const DECLARATIONS = [
|
|
|
479
479
|
name: 'type', type: 'enum', values: ['number', 'string', 'bool', 'vector', 'list', 'angle'],
|
|
480
480
|
description: 'Which control to build. Written bare, without quotes. Inferred from `default` when omitted',
|
|
481
481
|
},
|
|
482
|
-
{ name: 'min', type: 'number', description: 'Lower bound of the control. Advisory
|
|
483
|
-
{ name: 'max', type: 'number', description: 'Upper bound of the control. Advisory
|
|
482
|
+
{ name: 'min', type: 'number', description: 'Lower bound of the control. Advisory - never enforced' },
|
|
483
|
+
{ name: 'max', type: 'number', description: 'Upper bound of the control. Advisory - never enforced' },
|
|
484
484
|
{ name: 'step', type: 'number', description: 'Granularity of the control. Advisory' },
|
|
485
485
|
{ name: 'description', type: 'string', description: 'Help text' },
|
|
486
486
|
{ name: 'options', type: 'list', description: 'A fixed set of choices, for a dropdown. Advisory' },
|
|
487
487
|
],
|
|
488
488
|
example: 'param height { type = number default = 20 min = 8 max = 40 step = 1 }\nparam wall = 2.5',
|
|
489
489
|
notes: [
|
|
490
|
-
'This is the one block that does not reject attributes it does not understand
|
|
490
|
+
'This is the one block that does not reject attributes it does not understand - a param block is metadata.',
|
|
491
491
|
'`min`, `max`, `step` and `options` shape the editor control and nothing else. Clamp in the model if a bound matters.',
|
|
492
|
-
"A default may refer to params declared above it by their bare name, but never to `var`
|
|
492
|
+
"A default may refer to params declared above it by their bare name, but never to `var` - `var` is assembled after every param resolves.",
|
|
493
493
|
'A param declared inside a `model` must have a default and cannot be set from outside.',
|
|
494
494
|
'`param s` on its own is a syntax error; write `param s { }` for a required one.',
|
|
495
495
|
],
|
|
@@ -513,7 +513,7 @@ const DECLARATIONS = [
|
|
|
513
513
|
notes: [
|
|
514
514
|
'Params are read by their bare name inside the body; there is no `var.` for them.',
|
|
515
515
|
'Only declared params may be passed. There is no pass-through.',
|
|
516
|
-
'A component body cannot see the call site
|
|
516
|
+
'A component body cannot see the call site - it is evaluated in a fresh scope rooted at the document.',
|
|
517
517
|
'A component cannot declare parts, cannot shadow a builtin, and cannot recurse.',
|
|
518
518
|
'Reuse is free: the same call with the same arguments is one node in the tree, evaluated once.',
|
|
519
519
|
],
|
|
@@ -596,8 +596,8 @@ const FUNCTION_DESCRIPTIONS = {
|
|
|
596
596
|
round: 'Rounds to the nearest integer, halves upwards',
|
|
597
597
|
sqrt: 'Square root. A negative input gives NaN, which fails where it is used',
|
|
598
598
|
sign: '-1, 0 or 1',
|
|
599
|
-
pow: 'pow(x, y)
|
|
600
|
-
atan2: 'atan2(y, x)
|
|
599
|
+
pow: 'pow(x, y) - x raised to y',
|
|
600
|
+
atan2: 'atan2(y, x) - degrees, in the correct quadrant',
|
|
601
601
|
log: 'Natural logarithm',
|
|
602
602
|
exp: 'e raised to x',
|
|
603
603
|
sin: 'Takes degrees, returns a ratio',
|
|
@@ -612,8 +612,8 @@ const FUNCTION_DESCRIPTIONS = {
|
|
|
612
612
|
len: 'Length of a list or a string; 0 for anything else',
|
|
613
613
|
concat: 'Every argument flattened into one list',
|
|
614
614
|
reverse: 'A list, back to front',
|
|
615
|
-
contains: 'contains(list, value)
|
|
616
|
-
join: 'join(separator, list)
|
|
615
|
+
contains: 'contains(list, value) - whether the list holds it',
|
|
616
|
+
join: 'join(separator, list) - the items as one string',
|
|
617
617
|
str: 'The value as a string',
|
|
618
618
|
num: 'The value as a number; non-numeric text gives NaN',
|
|
619
619
|
range: 'range(end), range(start, end) or range(start, end, step). Excludes the end; capped at 100000 values',
|
|
@@ -679,7 +679,7 @@ export function formatConstruct(entry) {
|
|
|
679
679
|
/** @type {string[]} */
|
|
680
680
|
const facts = [];
|
|
681
681
|
if (entry.dim) facts.push(`produces ${entry.dim}D`);
|
|
682
|
-
if (entry.leaf) facts.push('a leaf
|
|
682
|
+
if (entry.leaf) facts.push('a leaf - it takes no children');
|
|
683
683
|
else if (entry.takes === 'same') facts.push("takes 2D or 3D children, passing the dimensionality through");
|
|
684
684
|
else if (entry.takes) facts.push(`takes ${entry.takes}D children`);
|
|
685
685
|
if (entry.ordered) facts.push('the order of its children matters');
|
|
@@ -691,7 +691,7 @@ export function formatConstruct(entry) {
|
|
|
691
691
|
const type = attribute.values
|
|
692
692
|
? attribute.values.map((value) => `\`"${value}"\``).join(' ')
|
|
693
693
|
: attribute.type;
|
|
694
|
-
lines.push(`| \`${attribute.name}\` | ${type} | ${attribute.default ?? '
|
|
694
|
+
lines.push(`| \`${attribute.name}\` | ${type} | ${attribute.default ?? '-'} | ${attribute.description} |`);
|
|
695
695
|
}
|
|
696
696
|
lines.push('');
|
|
697
697
|
}
|
package/src/document.js
CHANGED
|
@@ -13,19 +13,17 @@ import {
|
|
|
13
13
|
GeometryNode,
|
|
14
14
|
Program,
|
|
15
15
|
describeParameters,
|
|
16
|
+
excerpt,
|
|
16
17
|
render,
|
|
17
18
|
toBinarySTL,
|
|
18
19
|
} from 'forma-dsl';
|
|
19
20
|
|
|
20
|
-
/** How many lines of source to show either side of an error. */
|
|
21
|
-
const CONTEXT_LINES = 2;
|
|
22
|
-
|
|
23
21
|
/**
|
|
24
22
|
* How much geometry one document may build before the server refuses it.
|
|
25
23
|
*
|
|
26
24
|
* This server exists to build text a model wrote, which is the case the library leaves a
|
|
27
25
|
* budget off for. Nesting limits and `range` each bound one loop; only this bounds two of
|
|
28
|
-
* them nested, and that shape asks for more geometry than the process can finish
|
|
26
|
+
* them nested, and that shape asks for more geometry than the process can finish - on a pipe,
|
|
29
27
|
* with no request timeout of its own, that is a server that never answers again.
|
|
30
28
|
*
|
|
31
29
|
* Set far above any honest document: the worked examples build tens of blocks. A model that
|
|
@@ -38,7 +36,7 @@ const MAX_NODES = 50_000;
|
|
|
38
36
|
* that changed.
|
|
39
37
|
*
|
|
40
38
|
* Kept at module scope rather than per-request because that is the whole point of the digest
|
|
41
|
-
* cache
|
|
39
|
+
* cache - a context that lives for one call caches nothing.
|
|
42
40
|
*
|
|
43
41
|
* @type {EvaluationContext | null}
|
|
44
42
|
*/
|
|
@@ -56,38 +54,10 @@ export function disposeSharedContext() {
|
|
|
56
54
|
shared = null;
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
/**
|
|
60
|
-
* Renders a source excerpt with a caret under the column an error points at.
|
|
61
|
-
*
|
|
62
|
-
* An error message with a line number still makes a model count lines. An excerpt does not.
|
|
63
|
-
*
|
|
64
|
-
* @param {string} source The document.
|
|
65
|
-
* @param {{ line: number, column: number } | null | undefined} loc Where the error was raised.
|
|
66
|
-
* @returns {string | undefined} The excerpt, or undefined when there is no position.
|
|
67
|
-
*/
|
|
68
|
-
export function excerpt(source, loc) {
|
|
69
|
-
if (!loc || !Number.isInteger(loc.line)) return undefined;
|
|
70
|
-
|
|
71
|
-
const lines = source.split('\n');
|
|
72
|
-
const first = Math.max(1, loc.line - CONTEXT_LINES);
|
|
73
|
-
const last = Math.min(lines.length, loc.line + CONTEXT_LINES);
|
|
74
|
-
const width = String(last).length;
|
|
75
|
-
|
|
76
|
-
/** @type {string[]} */
|
|
77
|
-
const out = [];
|
|
78
|
-
for (let n = first; n <= last; n++) {
|
|
79
|
-
out.push(`${String(n).padStart(width)} | ${lines[n - 1]}`);
|
|
80
|
-
if (n === loc.line) {
|
|
81
|
-
out.push(`${' '.repeat(width)} | ${' '.repeat(Math.max(0, loc.column - 1))}^`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return out.join('\n');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
57
|
/**
|
|
88
58
|
* Turns a thrown error into the shape a tool result reports.
|
|
89
59
|
*
|
|
90
|
-
* @param {string} stage Which pass raised it
|
|
60
|
+
* @param {string} stage Which pass raised it - the stage is what tells a caller whether the
|
|
91
61
|
* mistake was a typo, a misunderstanding of meaning, or a degenerate solid.
|
|
92
62
|
* @param {unknown} error What was thrown.
|
|
93
63
|
* @param {string} source The document, for the excerpt.
|
|
@@ -119,7 +89,7 @@ const tidy = (value) => Number.parseFloat(value.toFixed(4));
|
|
|
119
89
|
* What a solved part is, measured.
|
|
120
90
|
*
|
|
121
91
|
* The bounding box and the volume are what let a caller check a model against a requirement
|
|
122
|
-
* it was given
|
|
92
|
+
* it was given - "it has to fit in 100 x 60" is answerable from here without a viewer.
|
|
123
93
|
*
|
|
124
94
|
* @param {Solid} solid The solved part.
|
|
125
95
|
* @returns {object} Its measurements.
|
|
@@ -198,7 +168,7 @@ export async function checkDocument(source, options = {}) {
|
|
|
198
168
|
...declared,
|
|
199
169
|
error: {
|
|
200
170
|
stage: 'render',
|
|
201
|
-
message: 'the document declares no model
|
|
171
|
+
message: 'the document declares no model - add a `model "name" { ... }` block',
|
|
202
172
|
},
|
|
203
173
|
};
|
|
204
174
|
}
|
|
@@ -276,7 +246,7 @@ export async function exportStl(source, options = {}) {
|
|
|
276
246
|
const wanted = result.parts.find((candidate) => candidate.name === part);
|
|
277
247
|
if (!wanted) {
|
|
278
248
|
const names = result.parts.map((candidate) => `"${candidate.name}"`).join(', ');
|
|
279
|
-
throw new Error(`no part named "${part}" in model "${result.name}"
|
|
249
|
+
throw new Error(`no part named "${part}" in model "${result.name}" - it has ${names}`);
|
|
280
250
|
}
|
|
281
251
|
solid = wanted.concrete;
|
|
282
252
|
name = wanted.name;
|
|
@@ -285,7 +255,7 @@ export async function exportStl(source, options = {}) {
|
|
|
285
255
|
name = result.parts[0].name;
|
|
286
256
|
} else {
|
|
287
257
|
// Several parts in one file: STL has no notion of a part, so they are unioned rather
|
|
288
|
-
// than concatenated
|
|
258
|
+
// than concatenated - a slicer reading two overlapping shells makes a mess of them.
|
|
289
259
|
solid = await shared.evaluate(GeometryNode.union(result.parts.map((each) => each.node)));
|
|
290
260
|
name = 'all';
|
|
291
261
|
}
|
package/src/examples.js
CHANGED
|
@@ -126,7 +126,7 @@ model "stacked_trays" {
|
|
|
126
126
|
|
|
127
127
|
{
|
|
128
128
|
name: 'revolved-profile',
|
|
129
|
-
summary: 'A vase turned from a profile
|
|
129
|
+
summary: 'A vase turned from a profile - the shortest route from a drawing to a solid.',
|
|
130
130
|
shows: ['polygon from a table of points', 'revolve', 'a second profile derived from the first', 'segments'],
|
|
131
131
|
source: `param wall { type = number default = 2.4 min = 1.2 max = 5 step = 0.2 }
|
|
132
132
|
param base { type = number default = 3.5 min = 2 max = 8 step = 0.5 }
|
package/src/guide.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The language, compressed to what someone writing a document has to hold in their head.
|
|
3
3
|
*
|
|
4
|
-
* This is deliberately not the reference manual
|
|
4
|
+
* This is deliberately not the reference manual - the manual is bundled too, and
|
|
5
5
|
* `forma_reference` serves it. This is the page that goes into a model's context before it
|
|
6
6
|
* writes anything: the rules that are not guessable, the mistakes that are expensive, and
|
|
7
7
|
* the shape of a working document.
|
|
@@ -43,7 +43,7 @@ model "riser" {
|
|
|
43
43
|
}
|
|
44
44
|
\`\`\`
|
|
45
45
|
|
|
46
|
-
Order does not matter between declarations of different kinds
|
|
46
|
+
Order does not matter between declarations of different kinds - a model may use a component
|
|
47
47
|
declared below it. Order does matter among top-level \`local\`s, which resolve top to bottom.`,
|
|
48
48
|
|
|
49
49
|
nesting: `## Nesting is composition
|
|
@@ -77,8 +77,8 @@ children is an error.`,
|
|
|
77
77
|
|
|
78
78
|
Every piece of geometry is 2D or 3D, and the language tracks which.
|
|
79
79
|
|
|
80
|
-
- **2D**
|
|
81
|
-
- **3D**
|
|
80
|
+
- **2D** - \`rect\` \`rounded_rect\` \`circle\` \`ellipse\` \`regular_polygon\` \`stadium\` \`polygon\`
|
|
81
|
+
- **3D** - \`box\` \`sphere\` \`cylinder\` \`cone\` \`torus\`
|
|
82
82
|
- **Operations** either fix what they take (\`extrude\` takes 2D, \`refine\` takes 3D) or pass
|
|
83
83
|
their children's dimensionality through (\`translate\` \`rotate\` \`scale\` \`mirror\` \`hull\`
|
|
84
84
|
\`simplify\` \`align\`, the booleans).
|
|
@@ -89,17 +89,17 @@ Three rules, each with its own error:
|
|
|
89
89
|
| --- | --- |
|
|
90
90
|
| Children of one block must all be the same dimensionality | \`"union" cannot mix 2D and 3D shapes\` |
|
|
91
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
|
|
92
|
+
| Anything rendered must be 3D | \`part "body" is 2D - extrude or revolve it before rendering\` |
|
|
93
93
|
|
|
94
94
|
2D → 3D with \`extrude\` or \`revolve\`; 3D → 2D with \`project\` or \`slice\`.`,
|
|
95
95
|
|
|
96
96
|
units: `## Units and orientation
|
|
97
97
|
|
|
98
98
|
- **Lengths are unitless.** Every consumer treats them as millimetres.
|
|
99
|
-
- **Angles are always degrees.** Everywhere
|
|
99
|
+
- **Angles are always degrees.** Everywhere - \`rotate { z = 90 }\`, \`revolve { angle = 180 }\`,
|
|
100
100
|
\`sin(30)\`, \`param a { type = angle }\`. There is no radian form.
|
|
101
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
|
|
102
|
+
about the Y axis, mapping the profile's Y to the result's Z - so a profile drawn upwards
|
|
103
103
|
from \`[0, 0]\` produces a solid standing on the XY plane.
|
|
104
104
|
- **3D rotations apply X, then Y, then Z**, matching OpenSCAD.
|
|
105
105
|
- **Transforms act about the origin.** Centre a shape (\`center = true\`) before rotating it,
|
|
@@ -107,11 +107,11 @@ Three rules, each with its own error:
|
|
|
107
107
|
|
|
108
108
|
syntax: `## Syntax
|
|
109
109
|
|
|
110
|
-
**Comments**
|
|
110
|
+
**Comments** - \`// line\`, \`# line\`, \`/* block */\`. Block comments do not nest.
|
|
111
111
|
|
|
112
|
-
**Numbers**
|
|
112
|
+
**Numbers** - \`12\` \`0.5\` \`.5\` \`1e3\` \`2.5e-3\`. No hex, no separators, no unit suffix.
|
|
113
113
|
|
|
114
|
-
**Strings**
|
|
114
|
+
**Strings** - double quotes only, no literal newline. Exactly six escapes: \`\\n\` \`\\t\` \`\\r\`
|
|
115
115
|
\`\\"\` \`\\\\\` \`\\$\`. \`\${ expr }\` splices any expression in.
|
|
116
116
|
|
|
117
117
|
**Identifiers** start with a letter or \`_\` and continue with letters, digits, \`_\` **or \`-\`**.
|
|
@@ -127,7 +127,7 @@ Three rules, each with its own error:
|
|
|
127
127
|
is \`name = expression\`; anything else that is followed by a brace is a nested block. Setting
|
|
128
128
|
the same attribute twice is an error.
|
|
129
129
|
|
|
130
|
-
**The newline rule.** Outside brackets, **a line break ends an expression**
|
|
130
|
+
**The newline rule.** Outside brackets, **a line break ends an expression** - that is what
|
|
131
131
|
lets attributes be written one per line with no separators. Inside \`(...)\`, \`[...]\` or an
|
|
132
132
|
argument list the rule is suspended, so break there to continue across lines:
|
|
133
133
|
|
|
@@ -138,7 +138,7 @@ local total = (
|
|
|
138
138
|
)
|
|
139
139
|
|
|
140
140
|
local b = base
|
|
141
|
-
+ extra // NOT a continuation
|
|
141
|
+
+ extra // NOT a continuation - this is a parse error
|
|
142
142
|
\`\`\`
|
|
143
143
|
|
|
144
144
|
Trailing commas are allowed in lists, objects and argument lists.`,
|
|
@@ -156,7 +156,7 @@ Trailing commas are allowed in lists, objects and argument lists.`,
|
|
|
156
156
|
|
|
157
157
|
There is no vector type: **a vector is a list of numbers**, and a block says how many
|
|
158
158
|
components it wants. Where a block wants a vector, **a single number stands for every
|
|
159
|
-
component**
|
|
159
|
+
component** - \`scale { factor = 2 }\` is \`scale { factor = [2, 2, 2] }\`.
|
|
160
160
|
|
|
161
161
|
**Operators**, loosest first: \`?:\` · \`||\` · \`&&\` · \`== !=\` · \`< <= > >=\` · \`+ -\` ·
|
|
162
162
|
\`* / %\` · prefix \`-x !x\` · postfix \`a.b a[i] f(x)\`.
|
|
@@ -174,21 +174,21 @@ errors**, not \`Infinity\`.
|
|
|
174
174
|
|
|
175
175
|
\`<\` \`<=\` \`>\` \`>=\` need both sides to be the same type. \`==\` compares structurally.
|
|
176
176
|
|
|
177
|
-
**Truthy / falsy**
|
|
177
|
+
**Truthy / falsy** - falsy is \`false\`, \`0\`, \`""\`, \`[]\`, \`null\`. Everything else is truthy,
|
|
178
178
|
including \`{}\`.
|
|
179
179
|
|
|
180
|
-
**Members**
|
|
180
|
+
**Members** - \`.x\` \`.y\` \`.z\` read index 0, 1, 2 off a list, so \`size.z\` works on a
|
|
181
181
|
3-vector. \`a[i]\` indexes. \`var.NAME\` reads a param.`,
|
|
182
182
|
|
|
183
183
|
scope: `## Scope and names
|
|
184
184
|
|
|
185
|
-
- **\`var\` holds params only**
|
|
185
|
+
- **\`var\` holds params only** - never locals. A local with the same name shadows the bare
|
|
186
186
|
name but \`var.NAME\` still means the param.
|
|
187
187
|
- **Component params are read by their bare name.** There is no \`var.\` for them.
|
|
188
188
|
- **A component body cannot see the call site.** It is evaluated in a fresh scope rooted at
|
|
189
189
|
the document, so a \`local\` where it is called does not leak in. Document params and other
|
|
190
190
|
components are in scope.
|
|
191
|
-
- **A param default may use earlier params by their bare name**, but never \`var\`
|
|
191
|
+
- **A param default may use earlier params by their bare name**, but never \`var\` - \`var\` is
|
|
192
192
|
assembled only after every param has resolved.
|
|
193
193
|
- **Top-level locals are ordered**; locals inside a body are not, so a block may use a local
|
|
194
194
|
written below it.
|
|
@@ -209,7 +209,7 @@ if cond { ... } else if other { ... } else { ... }
|
|
|
209
209
|
backwards, so a loop can never spin forever.
|
|
210
210
|
- There is no \`break\`, no \`continue\` and no accumulator. Filter the list instead.
|
|
211
211
|
- A loop over an empty list produces nothing, which is an error where something was
|
|
212
|
-
required
|
|
212
|
+
required - guard it with \`if\`.
|
|
213
213
|
- For a *value* that depends on a condition, use \`cond ? a : b\`, not \`if\`.
|
|
214
214
|
|
|
215
215
|
The radial-array idiom, which is most of what loops are for:
|
|
@@ -237,7 +237,7 @@ model "assembly" {
|
|
|
237
237
|
\`\`\`
|
|
238
238
|
|
|
239
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**
|
|
240
|
+
a transform or a boolean is **silently dropped** - no error, it is just gone.
|
|
241
241
|
- A part cannot contain another part, cannot be empty, and cannot come out of a component.
|
|
242
242
|
- Geometry written outside any part becomes one default part named after the model.
|
|
243
243
|
- The label is an expression: \`part "leg_\${i}"\`.
|
|
@@ -246,7 +246,7 @@ model "assembly" {
|
|
|
246
246
|
pitfalls: `## The mistakes that cost the most time
|
|
247
247
|
|
|
248
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**
|
|
249
|
+
2. **A misspelled attribute is an error, not a silent no-op** - \`box: unknown attribute
|
|
250
250
|
"colour"\`. The one exception is a \`param\` block, which passes anything through.
|
|
251
251
|
3. **A part inside a transform disappears** with no warning. Put the transform inside the
|
|
252
252
|
part.
|
|
@@ -260,11 +260,11 @@ model "assembly" {
|
|
|
260
260
|
9. **\`min\`/\`max\`/\`step\`/\`options\` on a param are advisory** and never enforced. Clamp in
|
|
261
261
|
the model if the bound matters: \`local n = max(3, min(16, var.bolts))\`.
|
|
262
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
|
|
263
|
+
everything and a backwards \`range\` all produce nothing rather than an error - and then
|
|
264
264
|
fail further out with \`model "m" produced no geometry\`.
|
|
265
265
|
11. **A line break ends an expression** outside brackets. Continue inside \`(...)\` or end the
|
|
266
266
|
line on an operator.
|
|
267
|
-
12. **\`rotate\` spells its 2D and 3D attributes differently**
|
|
267
|
+
12. **\`rotate\` spells its 2D and 3D attributes differently** - \`angle\` in 2D, \`x\`/\`y\`/\`z\`
|
|
268
268
|
or \`angles\` in 3D.`,
|
|
269
269
|
|
|
270
270
|
workflow: `## How to write one of these
|
|
@@ -274,7 +274,7 @@ model "assembly" {
|
|
|
274
274
|
2. **Write the profile, then give it thickness.** Most real parts are a 2D outline extruded
|
|
275
275
|
or revolved. Modelling in 2D and converting once is shorter and more robust than
|
|
276
276
|
composing solids.
|
|
277
|
-
3. **Name repeated features as components**
|
|
277
|
+
3. **Name repeated features as components** - including the *negative* of a feature. A
|
|
278
278
|
\`component "counterbore"\` subtracted in a loop reads far better than four inlined unions.
|
|
279
279
|
4. **One \`part\` per physical piece**, each with a colour. It is what makes a render legible
|
|
280
280
|
and what lets a single piece be exported.
|
|
@@ -307,7 +307,7 @@ export function guide(section) {
|
|
|
307
307
|
return ['# The forma language, in brief', '', ...ORDER.map((name) => SECTIONS[name])].join('\n\n');
|
|
308
308
|
}
|
|
309
309
|
if (!SECTIONS[section]) {
|
|
310
|
-
throw new Error(`no guide section "${section}"
|
|
310
|
+
throw new Error(`no guide section "${section}" - it is one of ${ORDER.join(', ')}`);
|
|
311
311
|
}
|
|
312
312
|
return SECTIONS[section];
|
|
313
313
|
}
|
package/src/reference/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
`forma` is a declarative language for describing solids. A document is a list of top-level
|
|
4
4
|
blocks; geometry is written by **nesting blocks**, where a block's children are its operands.
|
|
5
5
|
The evaluator turns a document into a tree of immutable geometry nodes, and the
|
|
6
|
-
[Manifold](https://github.com/elalish/manifold) CSG kernel
|
|
6
|
+
[Manifold](https://github.com/elalish/manifold) CSG kernel - compiled to WebAssembly - solves
|
|
7
7
|
that tree into meshes.
|
|
8
8
|
|
|
9
9
|
```hcl
|
|
@@ -28,8 +28,8 @@ model "riser" {
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
This reference documents every construct in the language and every function the JavaScript
|
|
31
|
-
package exports. For a tour of the project itself
|
|
32
|
-
library from Node
|
|
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
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
@@ -79,7 +79,7 @@ Leaf blocks. They take attributes and never contain other blocks.
|
|
|
79
79
|
| [`cone`](reference/shapes-3d.md#cone) | 3D | A cone, or a truncated cone |
|
|
80
80
|
| [`torus`](reference/shapes-3d.md#torus) | 3D | A ring, optionally a partial arc |
|
|
81
81
|
|
|
82
|
-
2D shapes cannot be rendered on their own
|
|
82
|
+
2D shapes cannot be rendered on their own - [`extrude`](reference/conversions.md#extrude) or
|
|
83
83
|
[`revolve`](reference/conversions.md#revolve) them first.
|
|
84
84
|
|
|
85
85
|
---
|
|
@@ -149,5 +149,15 @@ the package root and typed by hand-written declarations.
|
|
|
149
149
|
|
|
150
150
|
## Errors
|
|
151
151
|
|
|
152
|
-
[Every error message](errors.md), what raises it, and what to change
|
|
152
|
+
[Every error message](errors.md), what raises it, and what to change - the language's errors
|
|
153
153
|
in one place, grouped by the stage that raises them.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Running it
|
|
158
|
+
|
|
159
|
+
| Tool | What it is |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| [Command line](cli.md) | `forma render part.forma -o part.stl` - solving a document in a build, with no browser |
|
|
162
|
+
| [Editor](https://kalpak44.github.io/forma-dsl/editor/) | The same kernel in a browser, with live previews and a control per declared param |
|
|
163
|
+
| [MCP server](https://www.npmjs.com/package/forma-dsl-mcp) | The language as a tool an agent can write, with every document it produces solved before it answers |
|
|
@@ -15,7 +15,7 @@ const params = describeParameters(source);
|
|
|
15
15
|
### `describeParameters(source)`
|
|
16
16
|
|
|
17
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
|
|
18
|
+
source order. No geometry is evaluated and no kernel is loaded - this is the call a front end
|
|
19
19
|
makes to build controls before it can have any values to render with.
|
|
20
20
|
|
|
21
21
|
#### Parameters
|
|
@@ -51,7 +51,7 @@ makes to build controls before it can have any values to render with.
|
|
|
51
51
|
- **`min`, `max`, `step` and `options` are advisory.** Nothing enforces them, including
|
|
52
52
|
[`render`](render.md). Clamp in the model if a bound matters.
|
|
53
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**
|
|
54
|
+
- It still throws on a **syntax error** - the document has to parse.
|
|
55
55
|
|
|
56
56
|
#### Type inference
|
|
57
57
|
|
|
@@ -127,7 +127,7 @@ default, and a front end can act on that before rendering fails.
|
|
|
127
127
|
|
|
128
128
|
### Re-reading after an edit
|
|
129
129
|
|
|
130
|
-
`describeParameters` is cheap
|
|
130
|
+
`describeParameters` is cheap - a parse, no kernel - so calling it on every edit is
|
|
131
131
|
reasonable. Keep any value the user has already set, for a param that still exists with a
|
|
132
132
|
compatible type:
|
|
133
133
|
|
|
@@ -147,7 +147,7 @@ function reconcile(source, previous) {
|
|
|
147
147
|
|
|
148
148
|
### A descriptor's `default` is `undefined`
|
|
149
149
|
|
|
150
|
-
Either the param declares no default
|
|
150
|
+
Either the param declares no default - check `required` - or its default could not be
|
|
151
151
|
evaluated. A default that refers to a param declared **below** it cannot resolve; params are
|
|
152
152
|
read in source order.
|
|
153
153
|
|