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
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
A document, and an `-o`/`--out` that names one, must resolve inside the directory the command
|
|
32
|
+
is run in: a path that leaves it - by `..`, an absolute path, or a symlink pointing out of the
|
|
33
|
+
tree - is refused with exit code 2.
|
|
34
|
+
|
|
35
|
+
### `render`
|
|
36
|
+
|
|
37
|
+
| Option | Default | What it does |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `-o, --out <file>` | `<file>.stl` | where to write. `-` is stdout |
|
|
40
|
+
| `-m, --model <name>` | the first declared | which [model](reference/model.md) to build |
|
|
41
|
+
| `--part <name>` | see below | one [part](reference/part.md) by name |
|
|
42
|
+
| `-p, --param <name=val>` | each param's own default | a value for one declared [param](reference/param.md). Repeatable |
|
|
43
|
+
| `--params <file>` | - | a JSON object of values by name. `-p` wins over it |
|
|
44
|
+
| `--segments <n>` | the kernel's own | a fixed segment count for every curve |
|
|
45
|
+
| `--min-angle <deg>` | the kernel's own | smallest angle between adjacent segments of a curve |
|
|
46
|
+
| `--min-edge <n>` | the kernel's own | smallest length a curve segment may be shortened to |
|
|
47
|
+
| `--max-nodes <n>` | no limit | refuse a document that would build more blocks than this |
|
|
48
|
+
| `-q, --quiet` | off | do not print the summary |
|
|
49
|
+
|
|
50
|
+
The three resolution options are [`setQuality`](api/kernel.md) under another name, and are
|
|
51
|
+
global to the kernel exactly as they are there. `--max-nodes` is
|
|
52
|
+
[`render`](api/render.md)'s own budget.
|
|
53
|
+
|
|
54
|
+
Without `--part`, a model with one part exports that part, and a model with several exports
|
|
55
|
+
them **unioned**. STL has no notion of a part, and a slicer reading two overlapping shells
|
|
56
|
+
makes a mess of them.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Passing parameters
|
|
61
|
+
|
|
62
|
+
Three ways, and they compose.
|
|
63
|
+
|
|
64
|
+
**One at a time, on the command line.** A value is JSON where it parses as JSON, and the
|
|
65
|
+
string as written otherwise - so there is no second layer of quoting to get wrong:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
forma render part.forma -p count=6 -p center=true -p size=[10,20] -p label=M6
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| Written | Arrives as |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `-p count=6` | the number `6` |
|
|
74
|
+
| `-p center=true` | the boolean `true` |
|
|
75
|
+
| `-p size=[10,20]` | the list `[10, 20]` |
|
|
76
|
+
| `-p label=M6` | the string `"M6"` |
|
|
77
|
+
|
|
78
|
+
**From a file.** `--params` takes a JSON object of values by name - the thing you commit and
|
|
79
|
+
diff, rather than assemble in a shell loop:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"height": 36,
|
|
84
|
+
"label": "M6",
|
|
85
|
+
"center": true
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
forma render riser.forma --params params.json -o riser.stl
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
It has to be a JSON **object**. An array or a bare number parses but names nothing, and is
|
|
94
|
+
refused with exit code 2 rather than quietly contributing nothing. Like every other path, it
|
|
95
|
+
must resolve inside the directory the command runs in.
|
|
96
|
+
|
|
97
|
+
**Both.** `-p` wins over `--params`, which is what a build matrix wants: the values every
|
|
98
|
+
variant shares live in the file, and the one that varies goes on the line.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
for h in 8 20 40; do
|
|
102
|
+
forma render riser.forma --params shared.json -p "height=$h" -o "build/riser-$h.stl"
|
|
103
|
+
done
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
And the document will tell you what it takes, so the first `params.json` need not be written
|
|
107
|
+
by hand:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
forma params riser.forma --json | jq 'map({(.name): .default}) | add' > params.json
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"height": 20,
|
|
116
|
+
"label": "A",
|
|
117
|
+
"center": false
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## In a pipeline
|
|
124
|
+
|
|
125
|
+
Only what was asked for goes to stdout - the STL under `-o -`, and the listings. The summary
|
|
126
|
+
and every diagnostic go to stderr, so a pipe stays clean:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
forma render riser.forma --segments 64 -o - > riser.stl
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Exit codes are the three a script needs to tell apart:
|
|
133
|
+
|
|
134
|
+
| Code | What happened |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `0` | it worked |
|
|
137
|
+
| `1` | the document did not build - a syntax error, an unknown name, a degenerate solid |
|
|
138
|
+
| `2` | the invocation was wrong - an unknown flag, a missing file, a part that does not exist |
|
|
139
|
+
|
|
140
|
+
The summary names the cache hits, because that is the number explaining why the second render
|
|
141
|
+
of the same document in a loop is faster, and it is otherwise invisible:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
build/riser.stl - 252 triangles · model "riser", part "body" · 6 nodes, 6 solved, 0 cached · 9 ms
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Failures
|
|
150
|
+
|
|
151
|
+
A failure is reported where an editor and a CI annotation can both find it: the position as a
|
|
152
|
+
`file:line:column:` prefix, then the lines around it with a caret under the column.
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
riser.forma:7:7: rounded_rect: unknown attribute "corner_radius" - rounded_rect reads "center", "radius", "segments", "size"
|
|
156
|
+
5 | extrude {
|
|
157
|
+
6 | height = var.height
|
|
158
|
+
7 | rounded_rect { size = [40, 20] corner_radius = 4 }
|
|
159
|
+
| ^
|
|
160
|
+
8 | }
|
|
161
|
+
9 | }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The position comes from the error itself - see [Errors](errors.md) for what each stage raises
|
|
165
|
+
and what it means.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## What a document declares
|
|
170
|
+
|
|
171
|
+
`params --json` is the document describing itself, which is what lets a script build every
|
|
172
|
+
variant without being told what the variants are:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
forma params riser.forma --json
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
[
|
|
180
|
+
{
|
|
181
|
+
"name": "height",
|
|
182
|
+
"type": "number",
|
|
183
|
+
"default": 20,
|
|
184
|
+
"required": false,
|
|
185
|
+
"min": 8,
|
|
186
|
+
"max": 40
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`required` is true for a param declared with no default, which is one that has to be supplied
|
|
192
|
+
before the document will build at all.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Units
|
|
197
|
+
|
|
198
|
+
Lengths are unitless in forma, and STL has no unit either. Every consumer treats them as
|
|
199
|
+
millimetres.
|
package/src/reference/errors.md
CHANGED
|
@@ -25,7 +25,7 @@ Raised while turning characters into tokens. Always a typo.
|
|
|
25
25
|
|
|
26
26
|
| Message | Cause |
|
|
27
27
|
| --- | --- |
|
|
28
|
-
| `unexpected character "…"` | A character that cannot begin a token
|
|
28
|
+
| `unexpected character "…"` | A character that cannot begin a token - a smart quote, a stray backtick, a non-ASCII dash |
|
|
29
29
|
| `unterminated string` | A `"` with no closing `"`, or a literal newline inside one |
|
|
30
30
|
| `unterminated block comment` | A `/*` with no `*/`. Block comments do not nest |
|
|
31
31
|
| `unknown escape \q` | Only `\n` `\t` `\r` `\"` `\\` `\$` exist |
|
|
@@ -42,7 +42,7 @@ Raised while turning tokens into a tree. Always a grammar mistake.
|
|
|
42
42
|
| `expected a block, found "…"` | Something at the top level that is not a block |
|
|
43
43
|
| `unknown top-level block "shape"` | Only `param`, `local`, `component` and `model` may appear at the top level |
|
|
44
44
|
| `expected an attribute or block, found "…"` | Inside a body, an entry that does not start with a name |
|
|
45
|
-
| `expected {, found "…"` | A `param` with no `=` and no body
|
|
45
|
+
| `expected {, found "…"` | A `param` with no `=` and no body - write `param s { }`. Also a `component` or `model` with no body |
|
|
46
46
|
| `expected =, found "…"` | A `local` with no value |
|
|
47
47
|
| `expected ident, found "…"` | A keyword where a name was required |
|
|
48
48
|
| `"for" cannot be used as a block type` | One of the eleven [keywords](language/syntax.md#keywords) used as a block name |
|
|
@@ -93,7 +93,7 @@ Raised while evaluating.
|
|
|
93
93
|
| Message | Cause |
|
|
94
94
|
| --- | --- |
|
|
95
95
|
| `unknown name "x"` | Nothing in scope binds it. See [Scope](language/scope.md) |
|
|
96
|
-
| `no attribute "x"` | A missing key on an object
|
|
96
|
+
| `no attribute "x"` | A missing key on an object - including `var.x` for something that is not a param |
|
|
97
97
|
| `cannot read "x" of nothing` | A property read on `null` |
|
|
98
98
|
| `cannot index this value` | Indexing something that is not a list, string or object |
|
|
99
99
|
| `cannot negate a non-number` | Unary `-` on a string, list or bool |
|
|
@@ -115,7 +115,7 @@ operator: `outer - inner`. See [Identifiers](language/syntax.md#identifiers).
|
|
|
115
115
|
### `unknown name "var"`
|
|
116
116
|
|
|
117
117
|
A param default referred to `var`, which is assembled only after every param has resolved.
|
|
118
|
-
Use the earlier param's bare name instead
|
|
118
|
+
Use the earlier param's bare name instead - see
|
|
119
119
|
[the root scope](language/scope.md#the-root-scope).
|
|
120
120
|
|
|
121
121
|
---
|
|
@@ -135,11 +135,11 @@ Raised while building geometry.
|
|
|
135
135
|
|
|
136
136
|
| Message | Cause |
|
|
137
137
|
| --- | --- |
|
|
138
|
-
| `unknown block "bloop"
|
|
138
|
+
| `unknown block "bloop" - the blocks are …` | Not a builtin and not a declared component. The message lists every block there is, this document's own components first |
|
|
139
139
|
| `"box" does not take a label` | Only [`part`](reference/part.md) takes a meaningful label |
|
|
140
140
|
| `"box" is a shape and cannot contain other blocks` | A shape is a leaf. Usually a missing `}` swallowed the next block |
|
|
141
141
|
| `"translate" needs at least one shape inside it` | An operation with nothing to act on |
|
|
142
|
-
| `box: unknown attribute "colour"
|
|
142
|
+
| `box: unknown attribute "colour" - box reads …` | Every block rejects attributes it did not read, so a typo fails loudly, and names what it would have read |
|
|
143
143
|
| `attribute "size" is set twice` | The same attribute written twice in one block |
|
|
144
144
|
| `box: "size" must be a number or an array of 3 numbers` | Wrong shape of value |
|
|
145
145
|
| `circle: "radius" must be a finite number` | A `NaN` or `Infinity` reached the block |
|
|
@@ -149,7 +149,7 @@ Raised while building geometry.
|
|
|
149
149
|
| `cylinder: give "radius"/"diameter", or both "bottom_radius" and "top_radius"` | One end radius on its own is ambiguous |
|
|
150
150
|
| `offset: "join" must be one of "Square", "Round", "Miter"` | An enum attribute with an unrecognised value |
|
|
151
151
|
|
|
152
|
-
### `box: unknown attribute "colour"
|
|
152
|
+
### `box: unknown attribute "colour" - box reads "center", "size"`
|
|
153
153
|
|
|
154
154
|
Worth dwelling on, because it is the error that saves the most time. A misspelled attribute
|
|
155
155
|
would otherwise be silent, and the model would render subtly wrong with nothing to point at.
|
|
@@ -158,8 +158,8 @@ anything through.
|
|
|
158
158
|
|
|
159
159
|
Both this and `unknown block` carry the alternatives, because the reader is often a program
|
|
160
160
|
with no copy of this manual: an error that says only what is wrong leaves it guessing at
|
|
161
|
-
spellings. A block that branches on which of two spellings it was given
|
|
162
|
-
`diameter`
|
|
161
|
+
spellings. A block that branches on which of two spellings it was given - `radius` against
|
|
162
|
+
`diameter` - reads only one of them, so the list is what that call would accept rather than
|
|
163
163
|
always the whole set.
|
|
164
164
|
|
|
165
165
|
---
|
|
@@ -170,7 +170,7 @@ always the whole set.
|
|
|
170
170
|
| --- | --- |
|
|
171
171
|
| `"union" cannot mix 2D and 3D shapes` | Children of different dimensionality in one block |
|
|
172
172
|
| `"extrude" takes 2D geometry, got 3D` | A block that fixes what it takes got the other thing |
|
|
173
|
-
| `part "body" is 2D
|
|
173
|
+
| `part "body" is 2D - extrude or revolve it before rendering` | A cross-section reached the renderer |
|
|
174
174
|
| `cannot combine 2D and 3D geometry in a union` | The same mistake, from the [node API](api/geometry-node.md) rather than a document |
|
|
175
175
|
|
|
176
176
|
See [Dimensionality](language/overview.md#dimensionality) for the three rules, and
|
|
@@ -186,7 +186,7 @@ See [Dimensionality](language/overview.md#dimensionality) for the three rules, a
|
|
|
186
186
|
| `c: unknown param "t"` | Components have no pass-through: every value must be declared |
|
|
187
187
|
| `component "c" produced no geometry` | An empty body, a loop over an empty list, or an `if` that took no branch |
|
|
188
188
|
| `a component cannot declare parts` | Scene structure belongs in the model |
|
|
189
|
-
| `nesting deeper than 64 blocks
|
|
189
|
+
| `nesting deeper than 64 blocks - is a component using itself?` | Direct or mutual recursion. Rewrite as a `for` loop |
|
|
190
190
|
|
|
191
191
|
---
|
|
192
192
|
|
|
@@ -200,7 +200,7 @@ See [Dimensionality](language/overview.md#dimensionality) for the three rules, a
|
|
|
200
200
|
| `the document declares no model` | Add a `model` block |
|
|
201
201
|
| `no model named "x"` | `render(source, { model })` named one that does not exist |
|
|
202
202
|
|
|
203
|
-
A [`part`](reference/part.md) nested inside an operation raises **nothing**
|
|
203
|
+
A [`part`](reference/part.md) nested inside an operation raises **nothing** - it is silently
|
|
204
204
|
dropped. If a part is missing from a render, check it is written directly in the model.
|
|
205
205
|
|
|
206
206
|
---
|
|
@@ -217,7 +217,7 @@ dropped. If a part is missing from a render, check it is written directly in the
|
|
|
217
217
|
|
|
218
218
|
| Message | Cause |
|
|
219
219
|
| --- | --- |
|
|
220
|
-
| `align needs a kernel context
|
|
220
|
+
| `align needs a kernel context - render the model rather than building it` | An `Evaluator` built with no context. [`render`](api/render.md) always supplies one |
|
|
221
221
|
| `align needs at least one shape inside it` | Nothing to measure |
|
|
222
222
|
| `align: "x" must be "min", "center", "max" or a number` | The spelling is `"center"` |
|
|
223
223
|
|
|
@@ -239,7 +239,7 @@ Raised by the JavaScript API rather than by a document.
|
|
|
239
239
|
|
|
240
240
|
## Silent behaviours
|
|
241
241
|
|
|
242
|
-
Not errors, and each is deliberate
|
|
242
|
+
Not errors, and each is deliberate - but each has surprised someone. A parameter reaching the
|
|
243
243
|
end of its range should not break a model, so these degenerate quietly:
|
|
244
244
|
|
|
245
245
|
| What | Result |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Control flow
|
|
2
2
|
|
|
3
3
|
Two constructs, both of which appear where a block appears and produce geometry rather than
|
|
4
|
-
a value. Neither is an expression
|
|
4
|
+
a value. Neither is an expression - for a value that depends on a condition, use
|
|
5
5
|
[`cond ? a : b`](expressions.md#conditional).
|
|
6
6
|
|
|
7
7
|
---
|
|
@@ -38,7 +38,7 @@ difference {
|
|
|
38
38
|
| **Sequence** | Any expression that evaluates to a list. Iterating anything else is an error: `for: expected a list to iterate` |
|
|
39
39
|
| **Names** | One name binds the value. Two names bind the index and then the value |
|
|
40
40
|
| **Scope** | Each iteration gets a fresh child scope holding just those names |
|
|
41
|
-
| **Result** | The concatenation of what every iteration produced
|
|
41
|
+
| **Result** | The concatenation of what every iteration produced - geometry, and any `part`s |
|
|
42
42
|
|
|
43
43
|
#### Caveats
|
|
44
44
|
|
|
@@ -47,7 +47,7 @@ difference {
|
|
|
47
47
|
[`if`](#if) when a parameter can legitimately reach zero.
|
|
48
48
|
- `range(a, b)` **excludes** `b`, and produces an empty list rather than looping forever when
|
|
49
49
|
the range runs backwards. See [`range`](../reference/functions.md#range).
|
|
50
|
-
- Iterating a string does not work
|
|
50
|
+
- Iterating a string does not work - strings are not lists here. Use
|
|
51
51
|
[`len`](../reference/functions.md#len) and index into it if you need the characters.
|
|
52
52
|
- There is no `break`, no `continue` and no accumulator. A loop that needs to skip items
|
|
53
53
|
should filter the list it iterates, or wrap its body in an `if`.
|
|
@@ -17,7 +17,7 @@ their type, and a mismatch is reported where it is used.
|
|
|
17
17
|
| **object** | `{ w = 3, h = 4 }`, `{ w: 3 }` | `=` and `:` are interchangeable. Read with `.key` |
|
|
18
18
|
| **null** | `null` | Falsy; renders as the empty string in interpolation |
|
|
19
19
|
|
|
20
|
-
There is no separate vector type in the language
|
|
20
|
+
There is no separate vector type in the language - a vector is a list of numbers, and blocks
|
|
21
21
|
say how many components they want. Where a block wants a vector, a **single number is
|
|
22
22
|
accepted as shorthand for every component**, so `scale { factor = 2 }` and
|
|
23
23
|
`scale { factor = [2, 2, 2] }` are the same.
|
|
@@ -69,7 +69,7 @@ What each accepts:
|
|
|
69
69
|
Two lists of different lengths cannot be combined:
|
|
70
70
|
`cannot combine vectors of length 2 and 3`.
|
|
71
71
|
|
|
72
|
-
**Division and modulo by zero are errors**, not `Infinity` or `NaN`
|
|
72
|
+
**Division and modulo by zero are errors**, not `Infinity` or `NaN` - a NaN would otherwise
|
|
73
73
|
travel all the way into the kernel and surface as a degenerate vertex a long way from its
|
|
74
74
|
cause.
|
|
75
75
|
|
|
@@ -89,7 +89,7 @@ as [interpolation](#string-interpolation):
|
|
|
89
89
|
|
|
90
90
|
```hcl
|
|
91
91
|
[1, 2] == [1, 2] // true
|
|
92
|
-
1 == "1" // false
|
|
92
|
+
1 == "1" // false - different types are never equal
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
`<`, `<=`, `>` and `>=` require both sides to be the same type, and compare numbers
|
|
@@ -99,7 +99,7 @@ numerically and strings lexicographically. Mixing them is an error:
|
|
|
99
99
|
### Logic
|
|
100
100
|
|
|
101
101
|
`&&` and `||` short-circuit, so the right side is not evaluated when the left decides the
|
|
102
|
-
answer
|
|
102
|
+
answer - `count > 0 && list[0] > 1` is safe on an empty list. Both always return a bool, even
|
|
103
103
|
when given non-bool operands: `1 || false` is `true`, not `1`.
|
|
104
104
|
|
|
105
105
|
`!x` negates the [truthiness](#truthiness) of any value. Unary `-` requires a number:
|
|
@@ -118,7 +118,7 @@ right-associative, so `a ? b : c ? d : e` chains as `a ? b : (c ? d : e)`.
|
|
|
118
118
|
|
|
119
119
|
## Truthiness
|
|
120
120
|
|
|
121
|
-
Conditions
|
|
121
|
+
Conditions - in `? :`, `&&`, `||`, `!` and [`if`](control-flow.md#if) - accept any value.
|
|
122
122
|
|
|
123
123
|
| Falsy | Truthy |
|
|
124
124
|
| --- | --- |
|
|
@@ -147,7 +147,7 @@ opts.wall // 2
|
|
|
147
147
|
opts["wall"] // 2
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
`.x`, `.y` and `.z` on a **list** are sugar for indices 0, 1 and 2
|
|
150
|
+
`.x`, `.y` and `.z` on a **list** are sugar for indices 0, 1 and 2 - they are not a vector
|
|
151
151
|
type. They are read on a list of any length, so `[1, 2].z` yields nothing rather than
|
|
152
152
|
raising; the failure surfaces later, wherever that nothing is used.
|
|
153
153
|
|
|
@@ -165,7 +165,7 @@ an error: `cannot index this value`.
|
|
|
165
165
|
```hcl
|
|
166
166
|
max(sizes) // variadic, flattens its arguments
|
|
167
167
|
pow(2, 10)
|
|
168
|
-
sin(30) // 0.5
|
|
168
|
+
sin(30) // 0.5 - every angle is in degrees
|
|
169
169
|
```
|
|
170
170
|
|
|
171
171
|
Functions take **positional arguments only**. Named arguments are an error:
|
|
@@ -194,12 +194,12 @@ How each value renders:
|
|
|
194
194
|
|
|
195
195
|
| Value | Rendered as |
|
|
196
196
|
| --- | --- |
|
|
197
|
-
| number | `3`, `0.5`
|
|
197
|
+
| number | `3`, `0.5` - JavaScript's default number formatting |
|
|
198
198
|
| string | itself |
|
|
199
199
|
| bool | `true` / `false` |
|
|
200
|
-
| list | `[1, 2, 3]`
|
|
200
|
+
| list | `[1, 2, 3]` - elements joined by `, ` inside brackets, recursively |
|
|
201
201
|
| `null` | the empty string |
|
|
202
|
-
| object | `{w: 3, h: 4}`
|
|
202
|
+
| object | `{w: 3, h: 4}` - each entry rendered by these same rules. Interpolating one is usually a mistake, but it at least names which object |
|
|
203
203
|
|
|
204
204
|
A splice must contain exactly one expression; anything left over is an error:
|
|
205
205
|
`trailing input in ${ }`. Errors raised inside a splice are reported at the position of the
|
|
@@ -21,7 +21,7 @@ model "stack" { // something to render
|
|
|
21
21
|
}
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Order does not matter between declarations of different kinds
|
|
24
|
+
Order does not matter between declarations of different kinds - a `model` may use a
|
|
25
25
|
`component` declared below it. Order does matter among `local`s, which are evaluated top to
|
|
26
26
|
bottom. See [Scope and names](scope.md).
|
|
27
27
|
|
|
@@ -41,11 +41,11 @@ mistake it was.
|
|
|
41
41
|
1. **Lexer** turns characters into tokens. It rejects unknown characters, unterminated
|
|
42
42
|
strings and unterminated block comments.
|
|
43
43
|
2. **Parser** turns tokens into an abstract syntax tree. It rejects anything that is not
|
|
44
|
-
grammatical
|
|
44
|
+
grammatical - a missing brace, a keyword used as a block type, a `param` block containing
|
|
45
45
|
nested blocks.
|
|
46
46
|
3. **Evaluator** resolves params and locals, then walks the models. It rejects unknown names,
|
|
47
47
|
bad operands, unknown blocks, unknown attributes and dimensionality mismatches. Its output
|
|
48
|
-
is a tree of [geometry nodes](../api/geometry-node.md)
|
|
48
|
+
is a tree of [geometry nodes](../api/geometry-node.md) - it never calls the kernel itself.
|
|
49
49
|
4. **Kernel** solves the node tree into concrete solids, memoized by node digest in an
|
|
50
50
|
[`EvaluationContext`](../api/evaluation-context.md).
|
|
51
51
|
|
|
@@ -95,9 +95,9 @@ An operation with no children is an error; a shape with children is an error.
|
|
|
95
95
|
|
|
96
96
|
Every piece of geometry is either 2D or 3D, and the language tracks which.
|
|
97
97
|
|
|
98
|
-
- **2D shapes**
|
|
99
|
-
`polygon`
|
|
100
|
-
- **3D shapes**
|
|
98
|
+
- **2D shapes** - `rect`, `rounded_rect`, `circle`, `ellipse`, `regular_polygon`, `stadium`,
|
|
99
|
+
`polygon` - produce cross-sections.
|
|
100
|
+
- **3D shapes** - `box`, `sphere`, `cylinder`, `cone`, `torus` - produce solids.
|
|
101
101
|
- **Operations** either fix what they accept (`extrude` takes 2D, `refine` takes 3D) or pass
|
|
102
102
|
their children's dimensionality through (`translate`, `rotate`, `hull`, the booleans).
|
|
103
103
|
|
|
@@ -107,7 +107,7 @@ Three rules follow, and each has its own error message:
|
|
|
107
107
|
| --- | --- |
|
|
108
108
|
| The children of one block must all be the same dimensionality | `"union" cannot mix 2D and 3D shapes` |
|
|
109
109
|
| A block that fixes what it takes must get it | `"extrude" takes 2D geometry, got 3D` |
|
|
110
|
-
| Anything rendered must be 3D | `part "body" is 2D
|
|
110
|
+
| Anything rendered must be 3D | `part "body" is 2D - extrude or revolve it before rendering` |
|
|
111
111
|
|
|
112
112
|
To get from 2D to 3D use [`extrude`](../reference/conversions.md#extrude) or
|
|
113
113
|
[`revolve`](../reference/conversions.md#revolve); to go back use
|
|
@@ -118,8 +118,8 @@ To get from 2D to 3D use [`extrude`](../reference/conversions.md#extrude) or
|
|
|
118
118
|
|
|
119
119
|
## Units and orientation
|
|
120
120
|
|
|
121
|
-
- **Lengths are unitless.** Every consumer treats them as millimetres
|
|
122
|
-
either
|
|
121
|
+
- **Lengths are unitless.** Every consumer treats them as millimetres - STL has no unit
|
|
122
|
+
either - but nothing in the language enforces that.
|
|
123
123
|
- **Angles are always in degrees.** Everywhere: `rotate { z = 90 }`, `revolve { angle = 180 }`,
|
|
124
124
|
`sin(30)`, `param a { type = angle }`. There is no radian form in the language. The
|
|
125
125
|
JavaScript [`Angle`](../api/values.md#angle) class stores radians internally, and that is
|
|
@@ -134,7 +134,7 @@ To get from 2D to 3D use [`extrude`](../reference/conversions.md#extrude) or
|
|
|
134
134
|
|
|
135
135
|
## Identity is content
|
|
136
136
|
|
|
137
|
-
The evaluator does not produce a call graph
|
|
137
|
+
The evaluator does not produce a call graph - it produces a tree of immutable nodes, each
|
|
138
138
|
carrying a 128-bit digest of its own content and its children's digests. Two structurally
|
|
139
139
|
identical subtrees have the same digest and therefore *are* the same node.
|
|
140
140
|
|
|
@@ -142,7 +142,7 @@ That has consequences worth designing around:
|
|
|
142
142
|
|
|
143
143
|
- A component used twenty times is evaluated once. Eight spheres in a loop cost one sphere
|
|
144
144
|
and eight transforms.
|
|
145
|
-
- Two shapes written with their attributes in a different order still share a cache entry
|
|
145
|
+
- Two shapes written with their attributes in a different order still share a cache entry -
|
|
146
146
|
object keys are sorted before they are digested.
|
|
147
147
|
- The members of a `union` are sorted by subtree size, so two unions written in a different
|
|
148
148
|
order also share a digest. `difference` is never reordered, because its first child means
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Scope and names
|
|
2
2
|
|
|
3
3
|
Every name in a document resolves through a chain of scopes. This page says exactly what is
|
|
4
|
-
in each one, and in what order they are built
|
|
4
|
+
in each one, and in what order they are built - which is what determines whether a
|
|
5
5
|
declaration may refer to another.
|
|
6
6
|
|
|
7
7
|
---
|
|
@@ -10,12 +10,12 @@ declaration may refer to another.
|
|
|
10
10
|
|
|
11
11
|
Built once, before any geometry, in this order:
|
|
12
12
|
|
|
13
|
-
1. **Constants**
|
|
14
|
-
2. **Type names**
|
|
13
|
+
1. **Constants** - `pi` and `e`.
|
|
14
|
+
2. **Type names** - `number`, `string`, `bool`, `vector`, `list`, `angle`, each bound to
|
|
15
15
|
itself as a string, so `type = number` in a param block resolves with no quoting.
|
|
16
16
|
3. **Params**, in source order. Each is bound under its bare name as it is resolved, so a
|
|
17
17
|
later param's default may refer to an earlier one.
|
|
18
|
-
4. **`var`**
|
|
18
|
+
4. **`var`** - an object holding every resolved param.
|
|
19
19
|
5. **Locals**, in source order. Each is bound as it is resolved, so a later local may refer
|
|
20
20
|
to an earlier one.
|
|
21
21
|
|
|
@@ -24,10 +24,10 @@ The order has two consequences worth knowing:
|
|
|
24
24
|
```hcl
|
|
25
25
|
param a = 3
|
|
26
26
|
param b = a * 2 // ✓ an earlier param, by its bare name
|
|
27
|
-
param c = var.a * 2 // ✗ unknown name "var"
|
|
27
|
+
param c = var.a * 2 // ✗ unknown name "var" - var does not exist yet
|
|
28
28
|
|
|
29
29
|
local d = var.a + 1 // ✓ locals are built after var
|
|
30
|
-
local e = f // ✗ unknown name "f"
|
|
30
|
+
local e = f // ✗ unknown name "f" - a later local
|
|
31
31
|
local f = 2
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -45,8 +45,8 @@ model "m" {
|
|
|
45
45
|
}
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
They differ when something shadows the bare name. `var` holds **params only**
|
|
49
|
-
locals, never component params, never loop variables
|
|
48
|
+
They differ when something shadows the bare name. `var` holds **params only** - never
|
|
49
|
+
locals, never component params, never loop variables - so `var.NAME` is the unambiguous way
|
|
50
50
|
to reach an input:
|
|
51
51
|
|
|
52
52
|
```hcl
|
|
@@ -68,8 +68,8 @@ of use.
|
|
|
68
68
|
|
|
69
69
|
## Body scopes
|
|
70
70
|
|
|
71
|
-
Every block body
|
|
72
|
-
branch
|
|
71
|
+
Every block body - a `model`, a `component`, a builtin block, a `for` iteration, an `if`
|
|
72
|
+
branch - evaluates in a fresh scope nested inside its parent.
|
|
73
73
|
|
|
74
74
|
**Locals declared in a body are visible to the whole body**, not only to what follows them,
|
|
75
75
|
because every local in a body is resolved before any of its blocks are built:
|
|
@@ -82,7 +82,7 @@ model "m" {
|
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
That is the opposite of the top-level rule, where locals are strictly ordered. Within one
|
|
85
|
-
body, ordering between locals still applies
|
|
85
|
+
body, ordering between locals still applies - a body local may not refer to a later body
|
|
86
86
|
local.
|
|
87
87
|
|
|
88
88
|
---
|
|
@@ -110,7 +110,7 @@ model "m" {
|
|
|
110
110
|
Inside a component body, these are in scope:
|
|
111
111
|
|
|
112
112
|
- its own `param`s, bound from the attributes at the call site
|
|
113
|
-
- everything in the root scope
|
|
113
|
+
- everything in the root scope - constants, type names, document params (bare and via `var.`)
|
|
114
114
|
and document locals
|
|
115
115
|
- its own body locals
|
|
116
116
|
|
|
@@ -123,7 +123,7 @@ Nothing from the caller. Values travel in through params and no other way.
|
|
|
123
123
|
A `model` may declare its own `param` blocks. They behave like locals with a declaration
|
|
124
124
|
syntax, not like document params:
|
|
125
125
|
|
|
126
|
-
- Each **must** have a default
|
|
126
|
+
- Each **must** have a default - `model "m": param "s" needs a default`.
|
|
127
127
|
- They are **not** settable from outside. `render(source, { params: { s: 9 } })` sets
|
|
128
128
|
document params only; a model param keeps its default.
|
|
129
129
|
- They are bound in the model's scope under the bare name, and are not added to `var`.
|
|
@@ -31,7 +31,7 @@ Decimal, with an optional fraction and an optional exponent.
|
|
|
31
31
|
There is no hexadecimal, no separator, and no unit suffix. A leading `-` is the unary minus
|
|
32
32
|
operator, not part of the literal.
|
|
33
33
|
|
|
34
|
-
`2e` and `2em` are **not** numbers with exponents
|
|
34
|
+
`2e` and `2em` are **not** numbers with exponents - the lexer backs out of a bare `e` and
|
|
35
35
|
lets it start an identifier, so `2e` lexes as the number `2` followed by the name `e`.
|
|
36
36
|
|
|
37
37
|
---
|
|
@@ -46,7 +46,7 @@ Double quotes only. A string may not contain a literal newline.
|
|
|
46
46
|
"a tab\there, a quote\", a literal \${ not a splice"
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
**Escapes.** Exactly six: `\n`, `\t`, `\r`, `\"`, `\\` and `\$`. Anything else is an error
|
|
49
|
+
**Escapes.** Exactly six: `\n`, `\t`, `\r`, `\"`, `\\` and `\$`. Anything else is an error -
|
|
50
50
|
`unknown escape \q`. `\$` exists so a literal `${` can be written.
|
|
51
51
|
|
|
52
52
|
**Interpolation.** `${ ... }` splices in any expression. Braces inside the splice are
|
|
@@ -65,7 +65,7 @@ height wall_thickness _private min-width part2
|
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
> **Hyphens are part of a name.** `a-b` is one identifier, not `a` minus `b`. Write
|
|
68
|
-
> subtraction with spaces: `a - b`. This is the single most common surprise in the lexer
|
|
68
|
+
> subtraction with spaces: `a - b`. This is the single most common surprise in the lexer -
|
|
69
69
|
> `local gap = outer-inner` fails with `unknown name "outer-inner"`.
|
|
70
70
|
|
|
71
71
|
### Keywords
|
|
@@ -111,7 +111,7 @@ Labels come in two flavours:
|
|
|
111
111
|
| `component "name"`, `model "name"` | Static | The name must be a plain string or identifier. `${ }` is rejected: `a label cannot contain ${ }` |
|
|
112
112
|
| `part "name"` | An expression | Evaluated at build time, so a part in a loop can name itself: `part "leg_${i}"` |
|
|
113
113
|
|
|
114
|
-
Blocks that take no label reject one
|
|
114
|
+
Blocks that take no label reject one - `"box" does not take a label`. Two blocks accept a
|
|
115
115
|
label and quietly ignore it: [`align`](../reference/align.md) and
|
|
116
116
|
[component calls](../reference/component.md).
|
|
117
117
|
|