forma-dsl 0.1.1
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 +127 -0
- package/package.json +56 -0
- package/src/core/context.js +328 -0
- package/src/core/digest.js +232 -0
- package/src/core/kernel.js +67 -0
- package/src/core/node.js +187 -0
- package/src/export/mesh.js +83 -0
- package/src/export/stl.js +49 -0
- package/src/index.d.ts +406 -0
- package/src/index.js +102 -0
- package/src/lang/ast.js +226 -0
- package/src/lang/builtins.js +562 -0
- package/src/lang/evaluate.js +937 -0
- package/src/lang/lexer.js +359 -0
- package/src/lang/parser.js +522 -0
- package/src/values/angle.js +94 -0
- package/src/values/transform.js +159 -0
- package/src/values/vector.js +169 -0
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for forma-dsl.
|
|
3
|
+
*
|
|
4
|
+
* Hand-written rather than generated: the runtime is plain JavaScript, and the shapes worth
|
|
5
|
+
* publishing are the ones a caller actually touches. Internals that are exported only
|
|
6
|
+
* because a test or the editor reaches for them are typed loosely on purpose.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// --- source positions and errors --------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export interface SourceLocation {
|
|
12
|
+
line: number;
|
|
13
|
+
column: number;
|
|
14
|
+
offset: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Every error the language raises, carrying the position it was raised at when there is one. */
|
|
18
|
+
export declare class FormaError extends Error {
|
|
19
|
+
constructor(message: string, loc?: SourceLocation | null);
|
|
20
|
+
readonly name: 'FormaError';
|
|
21
|
+
readonly loc: SourceLocation | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// --- lexer and parser -------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export type TokenType = 'ident' | 'number' | 'string' | 'punct' | 'eof';
|
|
27
|
+
|
|
28
|
+
export interface Token {
|
|
29
|
+
type: TokenType;
|
|
30
|
+
/**
|
|
31
|
+
* The payload, which differs by `type`: a string for `ident` and `punct`, a number for
|
|
32
|
+
* `number`, the parsed runs and splices for `string`, and null for `eof`.
|
|
33
|
+
*/
|
|
34
|
+
value: any;
|
|
35
|
+
loc: SourceLocation;
|
|
36
|
+
/** Whether a line break preceded this token; the parser uses it to end an attribute. */
|
|
37
|
+
nlBefore: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export declare function tokenize(source: string): Token[];
|
|
41
|
+
|
|
42
|
+
/** The untyped AST. Its shape is an implementation detail of the evaluator. */
|
|
43
|
+
export interface FormaDocument {
|
|
44
|
+
kind: 'document';
|
|
45
|
+
declarations: unknown[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export declare function parse(source: string): FormaDocument;
|
|
49
|
+
|
|
50
|
+
// --- the kernel -------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
// Re-exported from the dependency rather than restated here. A hand-written approximation
|
|
53
|
+
// of these drifts the moment manifold-3d changes, and it costs consumers the real
|
|
54
|
+
// signatures of everything hanging off `part.concrete`.
|
|
55
|
+
import type {
|
|
56
|
+
CrossSection,
|
|
57
|
+
Manifold,
|
|
58
|
+
ManifoldToplevel,
|
|
59
|
+
Mesh,
|
|
60
|
+
} from 'manifold-3d';
|
|
61
|
+
|
|
62
|
+
/** The manifold-3d module, as returned by `loadKernel`. */
|
|
63
|
+
export type Kernel = ManifoldToplevel;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A live 3D solid owned by an EvaluationContext.
|
|
67
|
+
*
|
|
68
|
+
* WebAssembly objects are not garbage-collected. Anything obtained from a context belongs
|
|
69
|
+
* to that context — do not call `delete()` on it yourself.
|
|
70
|
+
*/
|
|
71
|
+
export type Solid = Manifold;
|
|
72
|
+
|
|
73
|
+
/** A live 2D cross-section owned by an EvaluationContext, with the same ownership rule. */
|
|
74
|
+
export type Shape = CrossSection;
|
|
75
|
+
|
|
76
|
+
/** The kernel's indexed triangle mesh, as `Solid.getMesh()` returns it. */
|
|
77
|
+
export type IndexedMesh = Mesh;
|
|
78
|
+
|
|
79
|
+
export declare function loadKernel(options?: { locateFile: () => string }): Promise<Kernel>;
|
|
80
|
+
|
|
81
|
+
export interface QualityOptions {
|
|
82
|
+
/** Smallest angle, in degrees, between adjacent segments of a curve. */
|
|
83
|
+
minAngle?: number;
|
|
84
|
+
/** Smallest length a curve segment may be shortened to. */
|
|
85
|
+
minEdgeLength?: number;
|
|
86
|
+
/** A fixed segment count, overriding the two limits above. */
|
|
87
|
+
segments?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export declare function setQuality(options?: QualityOptions): Promise<void>;
|
|
91
|
+
export declare function resetQuality(): Promise<void>;
|
|
92
|
+
|
|
93
|
+
// --- values -----------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
export declare class Vector {
|
|
96
|
+
constructor(components: readonly number[]);
|
|
97
|
+
static of(value: Vector | number | readonly number[] | Record<string, number>, dimensions: number): Vector;
|
|
98
|
+
static zero(dimensions: number): Vector;
|
|
99
|
+
readonly components: readonly number[];
|
|
100
|
+
readonly size: number;
|
|
101
|
+
readonly x: number;
|
|
102
|
+
readonly y: number;
|
|
103
|
+
readonly z: number;
|
|
104
|
+
readonly magnitude: number;
|
|
105
|
+
plus(other: Vector | number | readonly number[]): Vector;
|
|
106
|
+
minus(other: Vector | number | readonly number[]): Vector;
|
|
107
|
+
times(other: Vector | number | readonly number[]): Vector;
|
|
108
|
+
dividedBy(other: Vector | number | readonly number[]): Vector;
|
|
109
|
+
negated(): Vector;
|
|
110
|
+
dot(other: Vector | number | readonly number[]): number;
|
|
111
|
+
normalized(): Vector;
|
|
112
|
+
cross(other: Vector | readonly number[]): Vector;
|
|
113
|
+
toArray(): number[];
|
|
114
|
+
toString(): string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** An angle. Stored in radians; every angle the language reads or writes is in degrees. */
|
|
118
|
+
export declare class Angle {
|
|
119
|
+
constructor(radians: number);
|
|
120
|
+
static degrees(value: number): Angle;
|
|
121
|
+
static radians(value: number): Angle;
|
|
122
|
+
static turns(value: number): Angle;
|
|
123
|
+
static of(value: Angle | number): Angle;
|
|
124
|
+
readonly radians: number;
|
|
125
|
+
readonly degrees: number;
|
|
126
|
+
readonly sin: number;
|
|
127
|
+
readonly cos: number;
|
|
128
|
+
readonly tan: number;
|
|
129
|
+
plus(other: Angle | number): Angle;
|
|
130
|
+
minus(other: Angle | number): Angle;
|
|
131
|
+
times(factor: number): Angle;
|
|
132
|
+
toString(): string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type Dimensionality = 2 | 3;
|
|
136
|
+
|
|
137
|
+
/** An affine transform, column-major, sized for the kernel: 3x3 in 2D and 4x4 in 3D. */
|
|
138
|
+
export declare class Transform {
|
|
139
|
+
constructor(dim: Dimensionality, m: readonly number[]);
|
|
140
|
+
static identity(dim: Dimensionality): Transform;
|
|
141
|
+
static translation(offset: Vector | number | readonly number[], dim: Dimensionality): Transform;
|
|
142
|
+
static scaling(factor: Vector | number | readonly number[], dim: Dimensionality): Transform;
|
|
143
|
+
static rotation2D(angle: Angle | number): Transform;
|
|
144
|
+
static rotation3D(angles: Vector | readonly number[]): Transform;
|
|
145
|
+
static rotation(angles: Vector | number | readonly number[], dim: Dimensionality): Transform;
|
|
146
|
+
static mirroring(normal: Vector | readonly number[], dim: Dimensionality): Transform;
|
|
147
|
+
readonly dim: Dimensionality;
|
|
148
|
+
readonly m: readonly number[];
|
|
149
|
+
readonly order: number;
|
|
150
|
+
at(row: number, col: number): number;
|
|
151
|
+
/** `this ∘ other` — `other` runs first. */
|
|
152
|
+
concat(other: Transform): Transform;
|
|
153
|
+
toArray(): number[];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export type BooleanOp = 'union' | 'difference' | 'intersection';
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* An immutable node in the geometry tree, identified by the digest of its content.
|
|
160
|
+
*
|
|
161
|
+
* Two structurally identical subtrees are the same node, which is what lets a component used
|
|
162
|
+
* many times be evaluated once.
|
|
163
|
+
*/
|
|
164
|
+
export declare class GeometryNode {
|
|
165
|
+
constructor(
|
|
166
|
+
dim: Dimensionality,
|
|
167
|
+
kind: string,
|
|
168
|
+
props?: Record<string, unknown>,
|
|
169
|
+
children?: readonly GeometryNode[],
|
|
170
|
+
);
|
|
171
|
+
static empty(dim: Dimensionality): GeometryNode;
|
|
172
|
+
static boolean(op: BooleanOp, children: readonly GeometryNode[]): GeometryNode;
|
|
173
|
+
static union(children: readonly GeometryNode[]): GeometryNode;
|
|
174
|
+
static difference(children: readonly GeometryNode[]): GeometryNode;
|
|
175
|
+
static intersection(children: readonly GeometryNode[]): GeometryNode;
|
|
176
|
+
static transformed(child: GeometryNode, transform: Transform): GeometryNode;
|
|
177
|
+
static unary(
|
|
178
|
+
kind: string,
|
|
179
|
+
child: GeometryNode,
|
|
180
|
+
props?: Record<string, unknown>,
|
|
181
|
+
dim?: Dimensionality,
|
|
182
|
+
): GeometryNode;
|
|
183
|
+
static shape(dim: Dimensionality, shape: string, params: Record<string, unknown>): GeometryNode;
|
|
184
|
+
readonly dim: Dimensionality;
|
|
185
|
+
readonly kind: string;
|
|
186
|
+
readonly props: Readonly<Record<string, unknown>>;
|
|
187
|
+
readonly children: readonly GeometryNode[];
|
|
188
|
+
/** 128-bit content digest, as 32 lowercase hex characters. */
|
|
189
|
+
readonly digest: string;
|
|
190
|
+
readonly subtreeSize: number;
|
|
191
|
+
readonly isEmpty: boolean;
|
|
192
|
+
toString(): string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface CollectOptions {
|
|
196
|
+
/** How many unreachable entries to keep as an LRU tail. Defaults to 512. */
|
|
197
|
+
keep?: number;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface ContextStats {
|
|
201
|
+
evaluated: number;
|
|
202
|
+
hits: number;
|
|
203
|
+
freed: number;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Evaluates a geometry tree against the Manifold kernel, memoized by node digest.
|
|
208
|
+
*
|
|
209
|
+
* A context is meant to outlive a single render — reusing one is what makes a re-render
|
|
210
|
+
* after an edit skip the subtrees that did not change. It owns every WASM object it holds,
|
|
211
|
+
* so it must eventually be `dispose()`d, and `collect()` bounds what it holds meanwhile.
|
|
212
|
+
*/
|
|
213
|
+
export declare class EvaluationContext {
|
|
214
|
+
static create(): Promise<EvaluationContext>;
|
|
215
|
+
readonly wasm: Kernel;
|
|
216
|
+
/** Number of cached objects currently held. */
|
|
217
|
+
readonly size: number;
|
|
218
|
+
readonly disposed: boolean;
|
|
219
|
+
stats: ContextStats;
|
|
220
|
+
evaluate(node: GeometryNode): Promise<Solid | Shape>;
|
|
221
|
+
/** Frees cached objects the given roots can no longer reach. Returns how many were freed. */
|
|
222
|
+
collect(roots: readonly GeometryNode[], options?: CollectOptions): number;
|
|
223
|
+
dispose(): void;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export type ParameterValue = number | string | boolean | ParameterValue[];
|
|
227
|
+
|
|
228
|
+
export type ParameterType = 'number' | 'string' | 'bool' | 'vector' | 'list' | 'angle';
|
|
229
|
+
|
|
230
|
+
export interface ParameterDescriptor {
|
|
231
|
+
name: string;
|
|
232
|
+
/** The declared `type`, or one inferred from the default. Falls back to `number`. */
|
|
233
|
+
type: ParameterType | string;
|
|
234
|
+
default: ParameterValue | undefined;
|
|
235
|
+
/** True when the param declares no default, so a value must be supplied to render. */
|
|
236
|
+
required: boolean;
|
|
237
|
+
min?: number;
|
|
238
|
+
max?: number;
|
|
239
|
+
step?: number;
|
|
240
|
+
description?: string;
|
|
241
|
+
options?: ParameterValue[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** A parsed document, indexed by what each declaration is for. */
|
|
245
|
+
export declare class Program {
|
|
246
|
+
constructor(document: FormaDocument);
|
|
247
|
+
static parse(source: string): Program;
|
|
248
|
+
readonly params: Map<string, unknown>;
|
|
249
|
+
readonly locals: unknown[];
|
|
250
|
+
readonly components: Map<string, unknown>;
|
|
251
|
+
readonly models: Map<string, unknown>;
|
|
252
|
+
parameterDescriptors(
|
|
253
|
+
evaluator?: ((expression: unknown) => unknown) | null,
|
|
254
|
+
): ParameterDescriptor[];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface Scope {
|
|
258
|
+
lookup(name: string): unknown;
|
|
259
|
+
has(name: string): boolean;
|
|
260
|
+
define(name: string, value: unknown): Scope;
|
|
261
|
+
child(values?: Record<string, unknown>): Scope;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface ScenePart {
|
|
265
|
+
name: string;
|
|
266
|
+
color: string;
|
|
267
|
+
opacity: number;
|
|
268
|
+
node: GeometryNode;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface Scene {
|
|
272
|
+
name: string;
|
|
273
|
+
parts: ScenePart[];
|
|
274
|
+
/** Every part unioned together. */
|
|
275
|
+
node: GeometryNode;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface EvaluatorOptions {
|
|
279
|
+
params?: Record<string, ParameterValue>;
|
|
280
|
+
/** Needed only by blocks that measure their child, such as `align`. */
|
|
281
|
+
context?: EvaluationContext | null;
|
|
282
|
+
/** Off lets a document declaring a param with no default still be inspected. */
|
|
283
|
+
requireParams?: boolean;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export declare class Evaluator {
|
|
287
|
+
constructor(program: Program, options?: EvaluatorOptions);
|
|
288
|
+
readonly program: Program;
|
|
289
|
+
readonly context: EvaluationContext | null;
|
|
290
|
+
readonly root: Scope;
|
|
291
|
+
expression(node: unknown, scope: Scope): unknown;
|
|
292
|
+
model(name?: string | null): Promise<Scene>;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* What a block's `build` receives: the evaluated attributes, read with the type each one is
|
|
297
|
+
* meant to be.
|
|
298
|
+
*
|
|
299
|
+
* Described structurally rather than as the `Args` class, because `Args` is an
|
|
300
|
+
* implementation detail of the block registry and not part of the package's exports.
|
|
301
|
+
*/
|
|
302
|
+
export interface AttributeReader {
|
|
303
|
+
/** The block's name, used in every error message. */
|
|
304
|
+
readonly type: string;
|
|
305
|
+
/** Where the block was written. */
|
|
306
|
+
readonly loc: SourceLocation;
|
|
307
|
+
/** Tests for an attribute without marking it used. */
|
|
308
|
+
has(name: string): boolean;
|
|
309
|
+
/** Reads an attribute without checking its type. */
|
|
310
|
+
raw(name: string, fallback?: unknown): unknown;
|
|
311
|
+
/** Rejects an attribute, naming what it should have been. */
|
|
312
|
+
fail(name: string, expected: string): never;
|
|
313
|
+
number(name: string, fallback?: number): number;
|
|
314
|
+
optionalNumber(name: string): number | undefined;
|
|
315
|
+
int(name: string, fallback?: number): number;
|
|
316
|
+
bool(name: string, fallback?: boolean): boolean;
|
|
317
|
+
string(name: string, fallback?: string): string;
|
|
318
|
+
enum(name: string, allowed: readonly string[], fallback?: string): string;
|
|
319
|
+
/** A single number is accepted as shorthand for every component. */
|
|
320
|
+
vector(name: string, dim: Dimensionality, fallback?: number | readonly number[]): number[];
|
|
321
|
+
angle(name: string, fallback?: Angle | number): Angle;
|
|
322
|
+
/** Rejects any attribute the block did not read. */
|
|
323
|
+
checkUnused(): void;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export interface BlockDefinition {
|
|
327
|
+
/** Output dimensionality, where it is fixed regardless of the children. */
|
|
328
|
+
dim?: Dimensionality;
|
|
329
|
+
/** True for a shape, which takes no children. */
|
|
330
|
+
leaf?: boolean;
|
|
331
|
+
/** What the children must be; `'same'` passes their dimensionality through. */
|
|
332
|
+
takes?: Dimensionality | 'same';
|
|
333
|
+
/** `'list'` receives every child; anything else receives them unioned. */
|
|
334
|
+
combine?: 'list';
|
|
335
|
+
build(args: AttributeReader, children?: any): GeometryNode;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export declare const BLOCKS: Record<string, BlockDefinition>;
|
|
339
|
+
|
|
340
|
+
export interface FunctionDefinition {
|
|
341
|
+
/** `null` for a variadic function. */
|
|
342
|
+
arity: number | null;
|
|
343
|
+
/**
|
|
344
|
+
* Arguments arrive as whatever the model evaluated them to. The language is dynamically
|
|
345
|
+
* typed, so each function checks what it needs and throws otherwise.
|
|
346
|
+
*/
|
|
347
|
+
call(args: any[]): unknown;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export declare const FUNCTIONS: Record<string, FunctionDefinition>;
|
|
351
|
+
export declare const CONSTANTS: Record<string, number>;
|
|
352
|
+
|
|
353
|
+
// --- export ----------------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
export interface RenderMesh {
|
|
356
|
+
/** Three vertices per triangle, expanded so hard edges stay hard. */
|
|
357
|
+
positions: Float32Array;
|
|
358
|
+
normals: Float32Array;
|
|
359
|
+
triangleCount: number;
|
|
360
|
+
/** Vertices before expansion, as the kernel counted them. */
|
|
361
|
+
vertexCount: number;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export declare function toRenderMesh(manifold: Solid): RenderMesh;
|
|
365
|
+
export declare function toBinarySTL(manifold: Solid, header?: string): Uint8Array;
|
|
366
|
+
|
|
367
|
+
// --- the front door ----------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
export interface RenderOptions {
|
|
370
|
+
/** Which model to render. Defaults to the first the document declares. */
|
|
371
|
+
model?: string | null;
|
|
372
|
+
params?: Record<string, ParameterValue>;
|
|
373
|
+
/** Reuse a context to keep its cache across renders. The caller still owns it. */
|
|
374
|
+
context?: EvaluationContext | null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export interface RenderedPart extends ScenePart {
|
|
378
|
+
concrete: Solid;
|
|
379
|
+
mesh: RenderMesh;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export interface RenderStats {
|
|
383
|
+
nodes: number;
|
|
384
|
+
evaluated: number;
|
|
385
|
+
cacheHits: number;
|
|
386
|
+
milliseconds: number;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export interface RenderResult {
|
|
390
|
+
name: string;
|
|
391
|
+
parts: RenderedPart[];
|
|
392
|
+
program: Program;
|
|
393
|
+
/** The context that owns every solid in `parts`. Dispose it when done. */
|
|
394
|
+
context: EvaluationContext;
|
|
395
|
+
stats: RenderStats;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Compiles source and evaluates one model into renderable parts.
|
|
400
|
+
*
|
|
401
|
+
* The caller owns `result.context` and must dispose it.
|
|
402
|
+
*/
|
|
403
|
+
export declare function render(source: string, options?: RenderOptions): Promise<RenderResult>;
|
|
404
|
+
|
|
405
|
+
/** The parameters a document declares, with their metadata, for building editor controls. */
|
|
406
|
+
export declare function describeParameters(source: string): ParameterDescriptor[];
|
package/src/index.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** @import { ParameterDescriptor, RenderOptions, RenderResult, RenderedPart, Solid } from './index.js' */
|
|
2
|
+
|
|
3
|
+
export { Program, Evaluator } from './lang/evaluate.js';
|
|
4
|
+
export { parse } from './lang/parser.js';
|
|
5
|
+
export { tokenize, FormaError } from './lang/lexer.js';
|
|
6
|
+
export { BLOCKS, FUNCTIONS, CONSTANTS } from './lang/builtins.js';
|
|
7
|
+
export { GeometryNode } from './core/node.js';
|
|
8
|
+
export { EvaluationContext } from './core/context.js';
|
|
9
|
+
export { loadKernel, setQuality, resetQuality } from './core/kernel.js';
|
|
10
|
+
export { toRenderMesh } from './export/mesh.js';
|
|
11
|
+
export { toBinarySTL } from './export/stl.js';
|
|
12
|
+
export { Vector } from './values/vector.js';
|
|
13
|
+
export { Angle } from './values/angle.js';
|
|
14
|
+
export { Transform } from './values/transform.js';
|
|
15
|
+
|
|
16
|
+
import { Program, Evaluator } from './lang/evaluate.js';
|
|
17
|
+
import { EvaluationContext } from './core/context.js';
|
|
18
|
+
import { toRenderMesh } from './export/mesh.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Compiles source and evaluates one model into renderable parts.
|
|
22
|
+
*
|
|
23
|
+
* The caller owns the returned `context` and must dispose it: it holds the WASM objects for
|
|
24
|
+
* every node in the tree, and those are not garbage-collected. Passing the same context back
|
|
25
|
+
* on the next call is the intended use — that is what lets the digest cache skip the subtrees
|
|
26
|
+
* an edit did not change. Call `context.collect(parts.map(p => p.node))` afterwards to bound
|
|
27
|
+
* how much a reused context holds.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} source The document.
|
|
30
|
+
* @param {RenderOptions} [options] Which model, what parameters, and a context to reuse.
|
|
31
|
+
* @returns {Promise<RenderResult>} The parts, with both kernel objects and meshes.
|
|
32
|
+
* @throws {Error} On any syntax, evaluation or geometry failure. A context created here is
|
|
33
|
+
* disposed before the error propagates; one the caller supplied is left alone, since the
|
|
34
|
+
* caller may still want it.
|
|
35
|
+
*/
|
|
36
|
+
export async function render(source, options = {}) {
|
|
37
|
+
const { model = null, params = {}, context = null } = options;
|
|
38
|
+
|
|
39
|
+
const started = Date.now();
|
|
40
|
+
const program = Program.parse(source);
|
|
41
|
+
const owned = context ?? await EvaluationContext.create();
|
|
42
|
+
const evaluator = new Evaluator(program, { params, context: owned });
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const scene = await evaluator.model(model);
|
|
46
|
+
|
|
47
|
+
/** @type {RenderedPart[]} */
|
|
48
|
+
const parts = [];
|
|
49
|
+
for (const part of scene.parts) {
|
|
50
|
+
// Checked before solving rather than after: a 2D part is going to be refused either
|
|
51
|
+
// way, and there is no reason to spend the kernel call first.
|
|
52
|
+
if (part.node.dim !== 3) {
|
|
53
|
+
throw new Error(`part "${part.name}" is 2D — extrude or revolve it before rendering`);
|
|
54
|
+
}
|
|
55
|
+
// The check above is what makes this a solid rather than a cross-section.
|
|
56
|
+
const concrete = /** @type {Solid} */ (await owned.evaluate(part.node));
|
|
57
|
+
parts.push({ ...part, concrete, mesh: toRenderMesh(concrete) });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
name: scene.name,
|
|
62
|
+
parts,
|
|
63
|
+
program,
|
|
64
|
+
context: owned,
|
|
65
|
+
stats: {
|
|
66
|
+
nodes: scene.node.subtreeSize,
|
|
67
|
+
evaluated: owned.stats.evaluated,
|
|
68
|
+
cacheHits: owned.stats.hits,
|
|
69
|
+
milliseconds: Date.now() - started,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (!context) owned.dispose();
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The parameters a document declares, with their metadata, for building editor controls.
|
|
80
|
+
*
|
|
81
|
+
* Params are read leniently: a document that declares a param with no default has to be
|
|
82
|
+
* describable before anyone could have supplied one, or there is no way to build the control
|
|
83
|
+
* that would supply it.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} source The document.
|
|
86
|
+
* @returns {ParameterDescriptor[]} One descriptor per declared param, in source order.
|
|
87
|
+
* @throws {Error} If the document does not parse, or a param default cannot be evaluated.
|
|
88
|
+
*/
|
|
89
|
+
export function describeParameters(source) {
|
|
90
|
+
const program = Program.parse(source);
|
|
91
|
+
const evaluator = new Evaluator(program, { requireParams: false });
|
|
92
|
+
|
|
93
|
+
return program.parameterDescriptors((expression) => {
|
|
94
|
+
try {
|
|
95
|
+
return evaluator.expression(expression, evaluator.root);
|
|
96
|
+
} catch {
|
|
97
|
+
// One unreadable attribute — a min that refers to a param with no value yet — must
|
|
98
|
+
// not cost the caller every other descriptor in the document.
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|