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
package/src/document.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checking a document, and measuring what it builds.
|
|
3
|
+
*
|
|
4
|
+
* This is the part of the server that makes the difference between a model that has written
|
|
5
|
+
* plausible-looking forma and a model that has written forma which renders. Nothing here
|
|
6
|
+
* re-implements the language: every answer comes from the published package, so a block that
|
|
7
|
+
* changes its attributes changes this server's answers with it.
|
|
8
|
+
*/
|
|
9
|
+
/** @import { ParameterValue, RenderResult, Solid } from 'forma-dsl' */
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
EvaluationContext,
|
|
13
|
+
GeometryNode,
|
|
14
|
+
Program,
|
|
15
|
+
describeParameters,
|
|
16
|
+
render,
|
|
17
|
+
toBinarySTL,
|
|
18
|
+
} from 'forma-dsl';
|
|
19
|
+
|
|
20
|
+
/** How many lines of source to show either side of an error. */
|
|
21
|
+
const CONTEXT_LINES = 2;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The context reused across calls, so an edit-and-recheck loop pays only for the subtrees
|
|
25
|
+
* that changed.
|
|
26
|
+
*
|
|
27
|
+
* Kept at module scope rather than per-request because that is the whole point of the digest
|
|
28
|
+
* cache — a context that lives for one call caches nothing.
|
|
29
|
+
*
|
|
30
|
+
* @type {EvaluationContext | null}
|
|
31
|
+
*/
|
|
32
|
+
let shared = null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Drops the shared context, freeing every WASM object it holds.
|
|
36
|
+
*
|
|
37
|
+
* Only tests need this; a server process keeps its context for as long as it runs.
|
|
38
|
+
*
|
|
39
|
+
* @returns {void}
|
|
40
|
+
*/
|
|
41
|
+
export function disposeSharedContext() {
|
|
42
|
+
shared?.dispose();
|
|
43
|
+
shared = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Renders a source excerpt with a caret under the column an error points at.
|
|
48
|
+
*
|
|
49
|
+
* An error message with a line number still makes a model count lines. An excerpt does not.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} source The document.
|
|
52
|
+
* @param {{ line: number, column: number } | null | undefined} loc Where the error was raised.
|
|
53
|
+
* @returns {string | undefined} The excerpt, or undefined when there is no position.
|
|
54
|
+
*/
|
|
55
|
+
export function excerpt(source, loc) {
|
|
56
|
+
if (!loc || !Number.isInteger(loc.line)) return undefined;
|
|
57
|
+
|
|
58
|
+
const lines = source.split('\n');
|
|
59
|
+
const first = Math.max(1, loc.line - CONTEXT_LINES);
|
|
60
|
+
const last = Math.min(lines.length, loc.line + CONTEXT_LINES);
|
|
61
|
+
const width = String(last).length;
|
|
62
|
+
|
|
63
|
+
/** @type {string[]} */
|
|
64
|
+
const out = [];
|
|
65
|
+
for (let n = first; n <= last; n++) {
|
|
66
|
+
out.push(`${String(n).padStart(width)} | ${lines[n - 1]}`);
|
|
67
|
+
if (n === loc.line) {
|
|
68
|
+
out.push(`${' '.repeat(width)} | ${' '.repeat(Math.max(0, loc.column - 1))}^`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out.join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Turns a thrown error into the shape a tool result reports.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} stage Which pass raised it — the stage is what tells a caller whether the
|
|
78
|
+
* mistake was a typo, a misunderstanding of meaning, or a degenerate solid.
|
|
79
|
+
* @param {unknown} error What was thrown.
|
|
80
|
+
* @param {string} source The document, for the excerpt.
|
|
81
|
+
* @returns {{ stage: string, message: string, line?: number, column?: number, excerpt?: string }}
|
|
82
|
+
* The diagnostic.
|
|
83
|
+
*/
|
|
84
|
+
function diagnose(stage, error, source) {
|
|
85
|
+
const thrown = /** @type {Error & { loc?: { line: number, column: number } }} */ (error);
|
|
86
|
+
const loc = thrown?.loc ?? null;
|
|
87
|
+
return {
|
|
88
|
+
stage,
|
|
89
|
+
message: thrown?.message ?? 'a failure with no message',
|
|
90
|
+
...(loc ? { line: loc.line, column: loc.column, excerpt: excerpt(source, loc) } : {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Rounds a number to a fixed number of places, dropping the trailing zeros.
|
|
96
|
+
*
|
|
97
|
+
* Measurements go to a language model, and `19.999999999999996` reads as a bug in the model
|
|
98
|
+
* rather than as floating point.
|
|
99
|
+
*
|
|
100
|
+
* @param {number} value The number.
|
|
101
|
+
* @returns {number} The rounded value.
|
|
102
|
+
*/
|
|
103
|
+
const tidy = (value) => Number.parseFloat(value.toFixed(4));
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* What a solved part is, measured.
|
|
107
|
+
*
|
|
108
|
+
* The bounding box and the volume are what let a caller check a model against a requirement
|
|
109
|
+
* it was given — "it has to fit in 100 x 60" is answerable from here without a viewer.
|
|
110
|
+
*
|
|
111
|
+
* @param {Solid} solid The solved part.
|
|
112
|
+
* @returns {object} Its measurements.
|
|
113
|
+
*/
|
|
114
|
+
function measure(solid) {
|
|
115
|
+
const box = solid.boundingBox();
|
|
116
|
+
const size = box.max.map((max, i) => tidy(max - box.min[i]));
|
|
117
|
+
return {
|
|
118
|
+
boundingBox: { min: box.min.map(tidy), max: box.max.map(tidy), size },
|
|
119
|
+
volume: tidy(solid.volume()),
|
|
120
|
+
surfaceArea: tidy(solid.surfaceArea()),
|
|
121
|
+
genus: solid.genus(),
|
|
122
|
+
triangles: solid.numTri(),
|
|
123
|
+
vertices: solid.numVert(),
|
|
124
|
+
empty: solid.isEmpty(),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The declarations a document makes, without building anything.
|
|
130
|
+
*
|
|
131
|
+
* @param {Program} program The parsed document.
|
|
132
|
+
* @param {string} source The document, so a param default that will not evaluate can still
|
|
133
|
+
* be reported against its position.
|
|
134
|
+
* @returns {object} The outline, and any parameter failure.
|
|
135
|
+
*/
|
|
136
|
+
function outline(program, source) {
|
|
137
|
+
/** @type {object} */
|
|
138
|
+
const result = {
|
|
139
|
+
models: [...program.models.keys()],
|
|
140
|
+
components: [...program.components.keys()],
|
|
141
|
+
};
|
|
142
|
+
try {
|
|
143
|
+
result.parameters = describeParameters(source);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
result.parameterError = diagnose('parameters', error, source);
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parses a document, and optionally solves one of its models.
|
|
152
|
+
*
|
|
153
|
+
* Returns rather than throws: every failure here is a fact about the document that the
|
|
154
|
+
* caller asked for, not an error in the call.
|
|
155
|
+
*
|
|
156
|
+
* @param {string} source The document.
|
|
157
|
+
* @param {object} [options] What to build.
|
|
158
|
+
* @param {string | null} [options.model] Which model, or the first one.
|
|
159
|
+
* @param {Record<string, ParameterValue>} [options.params] Values for the document's params.
|
|
160
|
+
* @param {boolean} [options.solve] Whether to run the geometry kernel. Off answers only what
|
|
161
|
+
* parsing can answer, which is enough while a document is still being drafted.
|
|
162
|
+
* @returns {Promise<object>} The report.
|
|
163
|
+
*/
|
|
164
|
+
export async function checkDocument(source, options = {}) {
|
|
165
|
+
const { model = null, params = {}, solve = true } = options;
|
|
166
|
+
|
|
167
|
+
/** @type {Program} */
|
|
168
|
+
let program;
|
|
169
|
+
try {
|
|
170
|
+
program = Program.parse(source);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return { ok: false, stage: 'parse', error: diagnose('parse', error, source) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const declared = outline(program, source);
|
|
176
|
+
|
|
177
|
+
if (!solve) {
|
|
178
|
+
return { ok: !declared.parameterError, stage: 'parsed', ...declared };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!declared.models.length) {
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
stage: 'parsed',
|
|
185
|
+
...declared,
|
|
186
|
+
error: {
|
|
187
|
+
stage: 'render',
|
|
188
|
+
message: 'the document declares no model — add a `model "name" { ... }` block',
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
shared ??= await EvaluationContext.create();
|
|
194
|
+
|
|
195
|
+
/** @type {RenderResult} */
|
|
196
|
+
let result;
|
|
197
|
+
try {
|
|
198
|
+
result = await render(source, { model, params, context: shared });
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return { ok: false, stage: 'render', ...declared, error: diagnose('render', error, source) };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const parts = result.parts.map((part) => ({
|
|
204
|
+
name: part.name,
|
|
205
|
+
color: part.color,
|
|
206
|
+
opacity: part.opacity,
|
|
207
|
+
...measure(part.concrete),
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
// Bounds what a long-lived context holds: without it, every intermediate solid from every
|
|
211
|
+
// document ever checked stays resident for the life of the process.
|
|
212
|
+
shared.collect(result.parts.map((part) => part.node));
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
ok: true,
|
|
216
|
+
stage: 'rendered',
|
|
217
|
+
...declared,
|
|
218
|
+
scene: {
|
|
219
|
+
model: result.name,
|
|
220
|
+
parts,
|
|
221
|
+
boundingBox: unionOf(parts.map((part) => part.boundingBox)),
|
|
222
|
+
triangles: parts.reduce((total, part) => total + part.triangles, 0),
|
|
223
|
+
},
|
|
224
|
+
stats: result.stats,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The box that contains every part's box.
|
|
230
|
+
*
|
|
231
|
+
* @param {Array<{ min: number[], max: number[] }>} boxes One box per part.
|
|
232
|
+
* @returns {{ min: number[], max: number[], size: number[] } | null} The union, or null when
|
|
233
|
+
* there are no parts.
|
|
234
|
+
*/
|
|
235
|
+
function unionOf(boxes) {
|
|
236
|
+
if (!boxes.length) return null;
|
|
237
|
+
const min = boxes[0].min.map((_, i) => Math.min(...boxes.map((box) => box.min[i])));
|
|
238
|
+
const max = boxes[0].max.map((_, i) => Math.max(...boxes.map((box) => box.max[i])));
|
|
239
|
+
return { min: min.map(tidy), max: max.map(tidy), size: max.map((v, i) => tidy(v - min[i])) };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Renders a document and writes one part, or every part unioned, as binary STL.
|
|
244
|
+
*
|
|
245
|
+
* @param {string} source The document.
|
|
246
|
+
* @param {object} [options] What to export.
|
|
247
|
+
* @param {string | null} [options.model] Which model, or the first one.
|
|
248
|
+
* @param {Record<string, ParameterValue>} [options.params] Values for the document's params.
|
|
249
|
+
* @param {string | null} [options.part] Which part, or every part together.
|
|
250
|
+
* @returns {Promise<{ bytes: Uint8Array, model: string, part: string, triangles: number }>}
|
|
251
|
+
* The file, and what is in it.
|
|
252
|
+
* @throws {Error} If the document does not render, or names no such part.
|
|
253
|
+
*/
|
|
254
|
+
export async function exportStl(source, options = {}) {
|
|
255
|
+
const { model = null, params = {}, part = null } = options;
|
|
256
|
+
|
|
257
|
+
shared ??= await EvaluationContext.create();
|
|
258
|
+
const result = await render(source, { model, params, context: shared });
|
|
259
|
+
|
|
260
|
+
let solid;
|
|
261
|
+
let name;
|
|
262
|
+
if (part) {
|
|
263
|
+
const wanted = result.parts.find((candidate) => candidate.name === part);
|
|
264
|
+
if (!wanted) {
|
|
265
|
+
const names = result.parts.map((candidate) => `"${candidate.name}"`).join(', ');
|
|
266
|
+
throw new Error(`no part named "${part}" in model "${result.name}" — it has ${names}`);
|
|
267
|
+
}
|
|
268
|
+
solid = wanted.concrete;
|
|
269
|
+
name = wanted.name;
|
|
270
|
+
} else if (result.parts.length === 1) {
|
|
271
|
+
solid = result.parts[0].concrete;
|
|
272
|
+
name = result.parts[0].name;
|
|
273
|
+
} else {
|
|
274
|
+
// Several parts in one file: STL has no notion of a part, so they are unioned rather
|
|
275
|
+
// than concatenated — a slicer reading two overlapping shells makes a mess of them.
|
|
276
|
+
solid = await shared.evaluate(GeometryNode.union(result.parts.map((each) => each.node)));
|
|
277
|
+
name = 'all';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const bytes = toBinarySTL(/** @type {Solid} */ (solid), `forma ${result.name} ${name}`);
|
|
281
|
+
const triangles = /** @type {Solid} */ (solid).numTri();
|
|
282
|
+
|
|
283
|
+
shared.collect(result.parts.map((each) => each.node));
|
|
284
|
+
return { bytes, model: result.name, part: name, triangles };
|
|
285
|
+
}
|
package/src/examples.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complete documents that render, each showing one way of building a real part.
|
|
3
|
+
*
|
|
4
|
+
* Examples are the highest-value thing this server carries: a model that has read one
|
|
5
|
+
* working document writes better forma than one that has read every attribute table. Each is
|
|
6
|
+
* rendered by `test/examples.test.js`, so an example that stops working fails the build
|
|
7
|
+
* rather than teaching a mistake.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} Example
|
|
12
|
+
* @property {string} name How it is asked for.
|
|
13
|
+
* @property {string} summary What it demonstrates.
|
|
14
|
+
* @property {string[]} shows The techniques it is worth reading it for.
|
|
15
|
+
* @property {string} source The document.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** @type {Example[]} */
|
|
19
|
+
export const EXAMPLES = [
|
|
20
|
+
{
|
|
21
|
+
name: 'minimal',
|
|
22
|
+
summary: 'The smallest document that renders.',
|
|
23
|
+
shows: ['model', 'part', 'a 3D shape'],
|
|
24
|
+
source: `model "block" {
|
|
25
|
+
part "body" {
|
|
26
|
+
color = "#6f7d8c"
|
|
27
|
+
box { size = [40, 20, 10] }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
`,
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
name: 'parametric-plate',
|
|
35
|
+
summary: 'A bolted plate: params, a rounded profile extruded, and a radial array of holes.',
|
|
36
|
+
shows: ['param with bounds', 'local', 'extrude a 2D profile', 'difference', 'for over range', 'rotate + translate'],
|
|
37
|
+
source: `// A plate with a central bore and a bolt circle.
|
|
38
|
+
param plate_diameter { type = number default = 80 min = 40 max = 160 step = 1 }
|
|
39
|
+
param bore_diameter { type = number default = 24 min = 6 max = 60 }
|
|
40
|
+
param thickness { type = number default = 8 min = 3 max = 20 step = 0.5 }
|
|
41
|
+
param bolts { type = number default = 6 min = 3 max = 16 step = 1 }
|
|
42
|
+
param bolt_diameter { type = number default = 6 min = 3 max = 12 }
|
|
43
|
+
|
|
44
|
+
local plate_r = var.plate_diameter / 2
|
|
45
|
+
local circle_r = plate_r - var.bolt_diameter - 3
|
|
46
|
+
|
|
47
|
+
model "flange" {
|
|
48
|
+
part "plate" {
|
|
49
|
+
color = "#c8842f"
|
|
50
|
+
|
|
51
|
+
difference {
|
|
52
|
+
extrude {
|
|
53
|
+
height = var.thickness
|
|
54
|
+
circle { radius = plate_r segments = 96 }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The bore, overshooting both faces so no zero-thickness skin is left behind.
|
|
58
|
+
translate {
|
|
59
|
+
offset = [0, 0, -1]
|
|
60
|
+
cylinder { radius = var.bore_diameter / 2 height = var.thickness + 2 segments = 64 }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for i in range(0, var.bolts) {
|
|
64
|
+
rotate {
|
|
65
|
+
z = i * 360 / var.bolts
|
|
66
|
+
translate {
|
|
67
|
+
offset = [circle_r, 0, -1]
|
|
68
|
+
cylinder { radius = var.bolt_diameter / 2 height = var.thickness + 2 segments = 32 }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
`,
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
name: 'components',
|
|
80
|
+
summary: 'A stacking tray: a component for the shell, another for the cut, and a loop of parts.',
|
|
81
|
+
shows: ['component with params', 'naming the negative of a feature', 'part in a loop', 'conditional colour'],
|
|
82
|
+
source: `param rows { type = number default = 3 min = 1 max = 8 step = 1 }
|
|
83
|
+
param wall { type = number default = 2 min = 1 max = 5 step = 0.5 }
|
|
84
|
+
|
|
85
|
+
local tray_size = [60, 40, 14]
|
|
86
|
+
|
|
87
|
+
component "rounded_box" {
|
|
88
|
+
param size = [20, 20, 10]
|
|
89
|
+
param radius = 3
|
|
90
|
+
|
|
91
|
+
extrude {
|
|
92
|
+
height = size.z
|
|
93
|
+
rounded_rect { size = [size.x, size.y] radius = radius center = true }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
component "tray" {
|
|
98
|
+
param size = [60, 40, 14]
|
|
99
|
+
param wall = 2
|
|
100
|
+
|
|
101
|
+
difference {
|
|
102
|
+
rounded_box { size = size radius = 4 }
|
|
103
|
+
translate {
|
|
104
|
+
offset = [0, 0, wall]
|
|
105
|
+
rounded_box {
|
|
106
|
+
size = [size.x - wall * 2, size.y - wall * 2, size.z]
|
|
107
|
+
radius = 3
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
model "stacked_trays" {
|
|
114
|
+
for i in range(0, var.rows) {
|
|
115
|
+
part "tray_\${i}" {
|
|
116
|
+
color = i % 2 == 0 ? "#3f7fbf" : "#bf6b3f"
|
|
117
|
+
translate {
|
|
118
|
+
offset = [0, 0, i * (tray_size.z + 1)]
|
|
119
|
+
tray { size = tray_size wall = var.wall }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
`,
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
name: 'revolved-profile',
|
|
129
|
+
summary: 'A vase turned from a profile — the shortest route from a drawing to a solid.',
|
|
130
|
+
shows: ['polygon from a table of points', 'revolve', 'a second profile derived from the first', 'segments'],
|
|
131
|
+
source: `param wall { type = number default = 2.4 min = 1.2 max = 5 step = 0.2 }
|
|
132
|
+
param base { type = number default = 3.5 min = 2 max = 8 step = 0.5 }
|
|
133
|
+
param facets { type = number default = 96 min = 12 max = 192 step = 12 }
|
|
134
|
+
|
|
135
|
+
// The outside, drawn from the axis at the base upwards. X is the radius and Y the height:
|
|
136
|
+
// revolve spins the profile about the Y axis and maps its Y onto the result's Z, so a
|
|
137
|
+
// profile drawn upwards from [0, 0] comes out standing on the XY plane.
|
|
138
|
+
local outside = [
|
|
139
|
+
[0, 0], [34, 0], [36, 12], [30, 46],
|
|
140
|
+
[20, 78], [17, 100], [21, 118], [24, 124], [0, 124],
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
// The cavity: the same profile pulled in by the wall thickness, lifted by the base, and run
|
|
144
|
+
// out past the top so the mouth is open rather than capped. Both ends touch X = 0, which is
|
|
145
|
+
// what keeps a pillar from being left down the middle when it is revolved.
|
|
146
|
+
local inside = [
|
|
147
|
+
[0, var.base], [34 - var.wall, var.base], [36 - var.wall, 12], [30 - var.wall, 46],
|
|
148
|
+
[20 - var.wall, 78], [17 - var.wall, 100], [21 - var.wall, 118], [24 - var.wall, 130],
|
|
149
|
+
[0, 130],
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
model "vase" {
|
|
153
|
+
part "body" {
|
|
154
|
+
color = "#8fd3e8"
|
|
155
|
+
|
|
156
|
+
difference {
|
|
157
|
+
revolve { segments = var.facets polygon { points = outside } }
|
|
158
|
+
revolve { segments = var.facets polygon { points = inside } }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
`,
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
{
|
|
166
|
+
name: 'assembly',
|
|
167
|
+
summary: 'Two coloured parts placed by measurement rather than arithmetic.',
|
|
168
|
+
shows: ['several parts', 'align', 'opacity', 'a param that explodes the view'],
|
|
169
|
+
source: `param lift { type = number default = 0 min = 0 max = 60 step = 1
|
|
170
|
+
description = "Raises the cap so the assembly can be inspected" }
|
|
171
|
+
|
|
172
|
+
component "case_shell" {
|
|
173
|
+
difference {
|
|
174
|
+
extrude {
|
|
175
|
+
height = 30
|
|
176
|
+
rounded_rect { size = [70, 45] radius = 6 center = true }
|
|
177
|
+
}
|
|
178
|
+
translate {
|
|
179
|
+
offset = [0, 0, 2]
|
|
180
|
+
extrude {
|
|
181
|
+
height = 30
|
|
182
|
+
rounded_rect { size = [64, 39] radius = 4 center = true }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
component "cap" {
|
|
189
|
+
extrude {
|
|
190
|
+
height = 4
|
|
191
|
+
rounded_rect { size = [70, 45] radius = 6 center = true }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
model "enclosure" {
|
|
196
|
+
part "case" {
|
|
197
|
+
color = "#8fa8bd"
|
|
198
|
+
opacity = 0.4
|
|
199
|
+
align { z = 0 case_shell { } }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
part "cap" {
|
|
203
|
+
color = "#e2593c"
|
|
204
|
+
align { z = 30 + var.lift cap { } }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
`,
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
/** Every example, by name. */
|
|
212
|
+
export const EXAMPLES_BY_NAME = new Map(EXAMPLES.map((example) => [example.name, example]));
|