@es3d/es3d 1.0.0 → 1.0.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.
Files changed (79) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +86 -86
  3. package/docs/SPEC.md +417 -417
  4. package/package.json +1 -1
  5. package/scripts/es3d-cli.mjs +377 -377
  6. package/src/cache/geometry-store.js +392 -392
  7. package/src/cache/mesh-bundle.js +123 -123
  8. package/src/cache/sha256.js +73 -73
  9. package/src/format/ast.js +238 -238
  10. package/src/format/colors.js +93 -93
  11. package/src/format/compact-parser.js +171 -171
  12. package/src/format/compact-tokenizer.js +165 -165
  13. package/src/format/errors.js +247 -247
  14. package/src/format/json.js +55 -55
  15. package/src/format/limits.js +14 -14
  16. package/src/format/parse.js +92 -92
  17. package/src/format/schema.js +617 -617
  18. package/src/format/serializer.js +333 -333
  19. package/src/format/validator.js +330 -330
  20. package/src/format/value-error.js +12 -12
  21. package/src/format/value-format.js +314 -314
  22. package/src/format/values.js +459 -459
  23. package/src/format/xml-parser.js +136 -136
  24. package/src/format/xml-tokenizer.js +237 -237
  25. package/src/geometry/contour.js +86 -86
  26. package/src/geometry/extrude.js +175 -175
  27. package/src/geometry/gear.js +85 -85
  28. package/src/geometry/geometry-cache.js +159 -159
  29. package/src/geometry/lod-error.js +178 -178
  30. package/src/geometry/mesh.js +458 -458
  31. package/src/geometry/primitives.js +299 -299
  32. package/src/geometry/registry.js +645 -645
  33. package/src/geometry/revolve.js +136 -136
  34. package/src/geometry/sweep.js +441 -441
  35. package/src/index.js +47 -47
  36. package/src/io/compression.js +67 -67
  37. package/src/io/convert.js +236 -236
  38. package/src/io/download.js +13 -13
  39. package/src/io/external-scene.js +127 -127
  40. package/src/io/file-loader.js +28 -28
  41. package/src/io/gltf.js +630 -630
  42. package/src/io/mesh-import.js +104 -104
  43. package/src/io/obj.js +352 -352
  44. package/src/io/step-brep.js +439 -439
  45. package/src/io/step-exact.js +1149 -1149
  46. package/src/io/step.js +1418 -1418
  47. package/src/io/stl.js +193 -193
  48. package/src/io/svg-import.js +787 -787
  49. package/src/material/materials.js +154 -154
  50. package/src/math/mat4.js +475 -475
  51. package/src/math/random.js +30 -30
  52. package/src/math/trig.js +65 -65
  53. package/src/math/vec3.js +81 -81
  54. package/src/optimize/optimizer.js +579 -579
  55. package/src/path/path-flatten.js +322 -322
  56. package/src/path/path-parser.js +332 -332
  57. package/src/path/shapes.js +129 -129
  58. package/src/path/triangulate.js +517 -517
  59. package/src/pipeline.js +74 -74
  60. package/src/render/camera.js +168 -168
  61. package/src/render/frame-builder.js +362 -362
  62. package/src/render/geometry-pool.js +149 -149
  63. package/src/render/lighting.js +149 -149
  64. package/src/render/orbit-controls.js +237 -237
  65. package/src/render/renderer.js +258 -249
  66. package/src/render/scroll-timeline.js +104 -104
  67. package/src/render/shaders/glsl.js +285 -285
  68. package/src/render/shaders/wgsl.js +252 -252
  69. package/src/render/stage.js +364 -352
  70. package/src/render/texture-cache.js +40 -40
  71. package/src/render/textures.js +223 -223
  72. package/src/render/webgl2/webgl2-backend.js +586 -532
  73. package/src/render/webgpu/webgpu-backend.js +482 -480
  74. package/src/scene/flatten.js +754 -754
  75. package/src/scene/picking.js +161 -161
  76. package/src/scene/scene-graph.js +320 -320
  77. package/src/transform/transform.js +211 -211
  78. package/src/wasm/kernels-binary.js +3 -3
  79. package/src/wasm/kernels.js +385 -385
@@ -1,385 +1,385 @@
1
- // WebAssembly versions of the reader's hottest loops: number lists, normals,
2
- // sweeps, extrusions, triangulation, primitives and the instance rows of
3
- // <repeat> and <scatter>. They are built from wasm/ by `npm run wasm` and
4
- // compute exactly what the JavaScript versions compute (the same IEEE
5
- // operations in the same order; sin, cos, atan2 and hypot come from this
6
- // page's Math). Where WebAssembly is missing or blocked, for example by a
7
- // content security policy without 'wasm-unsafe-eval', every caller keeps
8
- // using its JavaScript version.
9
-
10
- import BINARY, { INSTANCES } from './kernels-binary.js';
11
-
12
- let text = ''; // the number list being read, for the rare numbers the kernel hands back to Number()
13
- let material = null; // resolves an instance row's material code (see instancesWasm)
14
-
15
- const imports = {
16
- env: {
17
- sin: Math.sin,
18
- cos: Math.cos,
19
- atan2: Math.atan2,
20
- tan: Math.tan,
21
- acos: Math.acos,
22
- hypot2: Math.hypot,
23
- hypot3: Math.hypot,
24
- number: (start, end) => Number(text.slice(start, end)),
25
- material: (code) => material(code),
26
- },
27
- };
28
-
29
- function decode(base64) {
30
- // native decoders where there is one (newer browsers, Node); a loop otherwise
31
- if (typeof Uint8Array.fromBase64 === 'function') return Uint8Array.fromBase64(base64);
32
- if (typeof Buffer === 'function') return Buffer.from(base64, 'base64');
33
- const s = atob(base64);
34
- const bytes = new Uint8Array(s.length);
35
- for (let i = 0; i < s.length; i++) bytes[i] = s.charCodeAt(i);
36
- return bytes;
37
- }
38
-
39
- async function instantiate(base64) {
40
- try {
41
- if (typeof WebAssembly !== 'object') return null;
42
- const { instance } = await WebAssembly.instantiate(decode(base64), imports);
43
- return instance.exports;
44
- } catch {
45
- return null;
46
- }
47
- }
48
-
49
- // the instance kernel is a module of its own: it needs WebAssembly SIMD
50
- const [wasm, simd] = await Promise.all([instantiate(BINARY), instantiate(INSTANCES)]);
51
- let enabled = wasm !== null;
52
-
53
- /**
54
- * Whether the module's hypot (V8's algorithm) gives this engine's Math.hypot
55
- * results. The special cases (infinities, NaN, zeros) are fixed by the
56
- * language; the rounding of finite values is the engine's, so values over
57
- * many magnitudes are compared. Where they differ the kernels keep calling
58
- * Math.hypot, which is slower (every call leaves WebAssembly and allocates).
59
- */
60
- function hypotMatches() {
61
- const edge = [0, -0, 3, -4, 5e-324, 1e308, Infinity, NaN];
62
- for (const x of edge) for (const y of edge) if (!Object.is(wasm.hypot2(x, y), Math.hypot(x, y))) return false;
63
- let seed = 12345;
64
- const next = () => {
65
- seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
66
- return (seed / 4294967296 - 0.5) * 10 ** ((seed % 41) - 20);
67
- };
68
- for (let i = 0; i < 100; i++) {
69
- const x = next(), y = next(), z = next();
70
- if (!Object.is(wasm.hypot3(x, y, z), Math.hypot(x, y, z)) || !Object.is(wasm.hypot2(x, y), Math.hypot(x, y))) return false;
71
- }
72
- return true;
73
- }
74
- if (wasm) wasm.set_native_hypot(hypotMatches() ? 1 : 0);
75
-
76
- /** Whether the kernels are in use. */
77
- export function wasmEnabled() {
78
- return enabled;
79
- }
80
-
81
- /** Whether the instance kernel is in use (it needs WebAssembly SIMD as well). */
82
- export function instanceKernelEnabled() {
83
- return enabled && simd !== null;
84
- }
85
-
86
- /** Turn the kernels off (or back on, when they loaded) to compare with the JavaScript versions. */
87
- export function setWasmEnabled(on) {
88
- enabled = Boolean(on) && wasm !== null;
89
- }
90
-
91
- const heap = () => wasm.memory.buffer;
92
-
93
- /** Copy a typed array into a module's memory; returns its address (0 when memory ran out). */
94
- function put(a, module = wasm) {
95
- const p = module.alloc(Math.max(1, a.byteLength));
96
- if (p) new Uint8Array(module.memory.buffer, p, a.byteLength).set(new Uint8Array(a.buffer, a.byteOffset, a.byteLength));
97
- return p;
98
- }
99
-
100
- const encoder = new TextEncoder();
101
- const AS = { f64: [0, Float64Array], f32: [1, Float32Array], index: [2, Uint32Array] };
102
-
103
- /**
104
- * A number list read into a typed array (`as`: 'f64', 'f32' or 'index'), or
105
- * null when the JavaScript reader must handle it (the kernels are off, the
106
- * text is not ASCII, or it has an error, which JavaScript then reports).
107
- */
108
- export function readNumbersWasm(raw, as) {
109
- if (!enabled) return null;
110
- const [kind, Type] = AS[as];
111
- const n = raw.length;
112
- wasm.reset();
113
- const sp = wasm.alloc(Math.max(1, n));
114
- const op = sp && wasm.alloc((Math.floor(n / 2) + 1) * Type.BYTES_PER_ELEMENT);
115
- if (!op) return null;
116
- const r = encoder.encodeInto(raw, new Uint8Array(heap(), sp, n));
117
- if (r.read !== n || r.written !== n) return null;
118
- text = raw;
119
- const count = wasm.numbers(sp, n, op, kind);
120
- text = '';
121
- if (count < 0) return null;
122
- return new Type(heap(), op, count).slice();
123
- }
124
-
125
- /**
126
- * computeNormals for Float32Array positions and uvs and Uint32Array indices,
127
- * as {positions, normals, uvs, indices}, or null to use the JavaScript version.
128
- */
129
- export function computeNormalsWasm(positions, indices, crease, cosCrease, uvs) {
130
- if (!enabled || !(positions instanceof Float32Array) || !(indices instanceof Uint32Array) || (uvs && !(uvs instanceof Float32Array))) return null;
131
- wasm.reset();
132
- const pp = put(positions), ip = put(indices), tp = uvs ? put(uvs) : 0;
133
- if (!pp || !ip || (uvs && !tp)) return null;
134
- const res = wasm.normals(pp, positions.length / 3, ip, indices.length, tp, crease, cosCrease);
135
- if (!res) return null;
136
- const [vc, ic, p, nrm, t, i] = new Uint32Array(heap(), res, 6);
137
- return {
138
- positions: new Float32Array(heap(), p, vc * 3).slice(),
139
- normals: new Float32Array(heap(), nrm, vc * 3).slice(),
140
- uvs: new Float32Array(heap(), t, vc * 2).slice(),
141
- indices: new Uint32Array(heap(), i, ic).slice(),
142
- };
143
- }
144
-
145
- const f64 = (a) => (a instanceof Float64Array ? a : Float64Array.from(a));
146
- const u32 = (a) => (a instanceof Uint32Array ? a : Uint32Array.from(a));
147
- const u8 = (a) => (a instanceof Uint8Array ? a : Uint8Array.from(a));
148
-
149
- /**
150
- * Writes contours ({points, smooth, isWall}) as {n, points, smooth, isWall}
151
- * records; returns the table's address, or 0 when memory ran out.
152
- */
153
- function contourTable(list) {
154
- const rows = new Int32Array(list.length * 4);
155
- for (let k = 0; k < list.length; k++) {
156
- const c = list[k];
157
- const p = put(f64(c.points));
158
- const sm = c.smooth ? put(u8(c.smooth)) : 0;
159
- if (!p || (c.smooth && !sm)) return 0;
160
- rows[k * 4] = c.points.length / 2;
161
- rows[k * 4 + 1] = p;
162
- rows[k * 4 + 2] = sm;
163
- rows[k * 4 + 3] = c.isWall === false ? 0 : 1;
164
- }
165
- return put(rows);
166
- }
167
-
168
- /** {first contour, contour count} per region, appending each region's contours to `list`. */
169
- function regionRows(regions, list) {
170
- const rows = new Int32Array(Math.max(1, regions.length * 2));
171
- regions.forEach((r, k) => {
172
- rows[k * 2] = list.length;
173
- rows[k * 2 + 1] = 1 + r.holes.length;
174
- list.push(r.outer, ...r.holes);
175
- });
176
- return rows;
177
- }
178
-
179
- /** A kernel's mesh, copied out of kernel memory into one buffer (one allocation instead of four). */
180
- const meshOut = (res, extra) => {
181
- const [vc, ic, p, nrm, t, i] = new Uint32Array(heap(), res, 6);
182
- const out = new ArrayBuffer((vc * 8 + ic) * 4);
183
- const positions = new Float32Array(out, 0, vc * 3);
184
- const normals = new Float32Array(out, vc * 12, vc * 3);
185
- const uvs = new Float32Array(out, vc * 24, vc * 2);
186
- const indices = new Uint32Array(out, vc * 32, ic);
187
- positions.set(new Float32Array(heap(), p, vc * 3));
188
- normals.set(new Float32Array(heap(), nrm, vc * 3));
189
- uvs.set(new Float32Array(heap(), t, vc * 2));
190
- indices.set(new Uint32Array(heap(), i, ic));
191
- return { positions, normals, uvs, indices, ...extra };
192
- };
193
-
194
- /**
195
- * sweepMesh after its profile is known: spines [{points, closed}], the
196
- * profile contours, the circle's points (circle profiles) or the regions to
197
- * cap (shaped profiles). Returns {positions, normals, uvs, indices, open,
198
- * degenerate: one flag per spine, errors: triangulation error per cap
199
- * region}, or null to use the JavaScript version.
200
- */
201
- export function sweepWasm(spines, contours, circle, regions, twist, taper, caps) {
202
- if (!enabled) return null;
203
- wasm.reset();
204
- const list = [...contours];
205
- const regionTab = circle ? null : regionRows(regions, list);
206
- const table = contourTable(list);
207
- const spineRows = new Int32Array(Math.max(1, spines.length * 3));
208
- for (let k = 0; k < spines.length; k++) {
209
- spineRows[k * 3] = put(f64(spines[k].points));
210
- spineRows[k * 3 + 1] = spines[k].points.length / 3;
211
- spineRows[k * 3 + 2] = spines[k].closed ? 1 : 0;
212
- if (!spineRows[k * 3]) return null;
213
- }
214
- const profile = put(Int32Array.from(contours, (_, k) => k));
215
- const spineTab = put(spineRows);
216
- const capData = circle ? put(f64(circle)) : put(regionTab);
217
- const header = put(
218
- Int32Array.of(spines.length, spineTab, list.length, table, contours.length, profile, circle ? 1 : 2, circle ? circle.length / 2 : regions.length, capData, caps ? 1 : 0),
219
- );
220
- if (!table || !profile || !spineTab || !capData || !header) return null;
221
- const res = wasm.sweep(header, twist, taper);
222
- if (!res) return null;
223
- const [open, deg, errs] = new Uint32Array(heap(), res + 24, 3);
224
- return meshOut(res, {
225
- open: open !== 0,
226
- degenerate: Array.from(new Uint8Array(heap(), deg, spines.length), (d) => d !== 0),
227
- errors: circle ? [] : Array.from(new Float64Array(heap(), errs, regions.length)),
228
- });
229
- }
230
-
231
- /**
232
- * triangulate(data, holeIndices) as an array of vertex indices, or null to
233
- * use the JavaScript version (also when two holes tie for the sort order).
234
- */
235
- export function triangulateWasm(data, holeIndices) {
236
- if (!enabled) return null;
237
- wasm.reset();
238
- const dp = put(f64(data));
239
- const holes = holeIndices && holeIndices.length ? Int32Array.from(holeIndices) : null;
240
- const hp = holes ? put(holes) : 0;
241
- if (!dp || (holes && !hp)) return null;
242
- let res;
243
- try {
244
- res = wasm.triangulate(dp, data.length, hp, holes ? holes.length : 0);
245
- } catch {
246
- return null; // stack exhausted on pathological input: JavaScript has a deeper stack
247
- }
248
- if (!res) return null;
249
- const [count, p] = new Uint32Array(heap(), res, 2);
250
- return count ? Array.from(new Uint32Array(heap(), p, count)) : [];
251
- }
252
-
253
- /**
254
- * extrudeMesh from a shape ({regions, walls}) with the bevel profile rings
255
- * {inset, z, nr, nz} and bounds from JavaScript. Returns {positions,
256
- * normals, uvs, indices, errors: triangulation error per region}, or null
257
- * to use the JavaScript version.
258
- */
259
- export function extrudeWasm(shape, bevel, rings, z0, zTop, minX, minY, S) {
260
- if (!enabled) return null;
261
- wasm.reset();
262
- const list = [];
263
- const regionTab = put(regionRows(shape.regions, list));
264
- const walls = put(Int32Array.from(shape.walls, (_, k) => list.length + k));
265
- list.push(...shape.walls);
266
- const table = contourTable(list);
267
- const ringData = new Float64Array(Math.max(1, rings.length * 4));
268
- rings.forEach((r, k) => ringData.set([r.inset, r.z, r.nr, r.nz], k * 4));
269
- const ringPtr = put(ringData);
270
- const header = put(Int32Array.of(list.length, table, shape.regions.length, regionTab, shape.walls.length, walls, rings.length, ringPtr));
271
- if (!regionTab || !walls || !table || !ringPtr || !header) return null;
272
- const res = wasm.extrude(header, bevel, z0, zTop, minX, minY, S);
273
- if (!res) return null;
274
- const errs = new Uint32Array(heap(), res + 24, 1)[0];
275
- return meshOut(res, { errors: Array.from(new Float64Array(heap(), errs, shape.regions.length)) });
276
- }
277
-
278
- /** measureSphereDeviation(w) for w >= 3, or null to use the JavaScript version. */
279
- export function sphereDeviationWasm(w) {
280
- if (!enabled) return null;
281
- wasm.reset();
282
- const v = wasm.sphere_deviation(w);
283
- return v < 0 ? null : v;
284
- }
285
-
286
- const whole = (...v) => v.every((x) => Number.isInteger(x) && x >= 0 && x < 2 ** 30);
287
- const prim = (res) => (res ? meshOut(res) : null);
288
-
289
- /** unitSphere, unitCylinder, unitCapsule, unitTorus and roundedBox ({positions, normals, uvs, indices}), or null. */
290
- export const primitivesWasm = {
291
- sphere: (segments) => (enabled && whole(segments) ? (wasm.reset(), prim(wasm.sphere(segments))) : null),
292
- cylinder: (top, segments, caps) => (enabled && whole(segments) ? (wasm.reset(), prim(wasm.cylinder(top, segments, caps ? 1 : 0))) : null),
293
- capsule: (L, segments) => (enabled && whole(segments) ? (wasm.reset(), prim(wasm.capsule(L, segments))) : null),
294
- torus: (t, arcDeg, segments, tubeSegments) => (enabled && whole(segments, tubeSegments) ? (wasm.reset(), prim(wasm.torus(t, arcDeg, segments, tubeSegments))) : null),
295
- roundedBox: (w, h, d, r, seg) => (enabled && whole(seg) && seg >= 1 ? (wasm.reset(), prim(wasm.rounded_box(w, h, d, r, seg))) : null),
296
- };
297
-
298
- /** catmullRom for two or more points (a Float64Array), or null. */
299
- export function catmullRomWasm(points, closed, subdivisions) {
300
- if (!enabled || !whole(subdivisions) || points.length < 6 || points.length % 3) return null;
301
- wasm.reset();
302
- const p = put(f64(points));
303
- const res = p && wasm.catmull_rom(p, points.length / 3, closed ? 1 : 0, subdivisions);
304
- if (!res) return null;
305
- const [count, out] = new Uint32Array(heap(), res, 2);
306
- return new Float64Array(heap(), out, count * 3).slice();
307
- }
308
-
309
- /** gearOutline ({outline: {points, smooth, closed}, outerRadius, rootRadius}), or null. */
310
- export function gearOutlineWasm(teeth, pressureAngleDeg, flankSamples, arcSamples) {
311
- if (!enabled || !whole(teeth, flankSamples, arcSamples) || teeth < 1) return null;
312
- wasm.reset();
313
- const res = wasm.gear_outline(teeth, pressureAngleDeg, flankSamples, arcSamples);
314
- if (!res) return null;
315
- const [count, pts, smooth, radii] = new Uint32Array(heap(), res, 4);
316
- const [outerRadius, rootRadius] = new Float64Array(heap(), radii, 2);
317
- return {
318
- outline: { points: new Float64Array(heap(), pts, count * 2).slice(), smooth: new Uint8Array(heap(), smooth, count).slice(), closed: true },
319
- outerRadius,
320
- rootRadius,
321
- };
322
- }
323
-
324
- // Header layouts of the instance kernel (the enums in wasm/simd/instances.c).
325
- export const INSTANCE_HEADER = {
326
- KIND: 0, FIRST: 1, COUNT: 2, STATE: 3, MODE: 4, NX: 5, NZ: 6, RANDOM: 7, CHOICES: 8,
327
- OPS: 9, NOPS: 10, MATS: 11, ROWS: 12, NROWS: 13, BOUNDS: 14, MATERIALS: 15, LODS: 16, OUTER_LOD: 17, SWITCH: 18, SLOTS: 19,
328
- OUT: 20, SIZE: 29,
329
- };
330
- export const INSTANCE_PARAMS = { WORLD: 0, ACC: 16, STEP: 32, SPACING: 48, JITTER: 50, RADIUS: 51, AREA: 52, ROTATION: 54, SCALE: 60, BOUNDS: 66, SIZE: 72 };
331
-
332
- /**
333
- * Instance rows of placements [FIRST, FIRST + COUNT) of a <repeat> or
334
- * <scatter> whose subtree was recorded as `template` ({ops, mats, table,
335
- * bounds}, see recordTemplate in src/scene/flatten.js). The headers `h`
336
- * (Int32Array) and `f` (Float64Array) and the table `materials` (material
337
- * index per material code, -1 until resolveMaterial(code) has given it)
338
- * carry the generator state, the accumulated repeat step, the scene bounds
339
- * and the materials from one call to the next. Returns views of kernel
340
- * memory, valid until the next kernel call, or null.
341
- */
342
- export function instancesWasm(h, f, template, materials, resolveMaterial) {
343
- if (!enabled || !simd) return null;
344
- simd.reset();
345
- const H = INSTANCE_HEADER;
346
- h[H.OPS] = put(template.ops, simd);
347
- h[H.MATS] = put(template.mats, simd);
348
- h[H.ROWS] = put(template.table, simd);
349
- h[H.BOUNDS] = put(template.bounds, simd);
350
- h[H.MATERIALS] = put(materials, simd);
351
- const hp = put(h, simd), fp = put(f, simd);
352
- if (!h[H.OPS] || !h[H.MATS] || !h[H.ROWS] || !h[H.BOUNDS] || !h[H.MATERIALS] || !hp || !fp) return null;
353
- material = resolveMaterial;
354
- try {
355
- if (!simd.instances(hp, fp)) return null;
356
- } finally {
357
- material = null;
358
- }
359
- const buf = simd.memory.buffer;
360
- h.set(new Int32Array(buf, hp, H.SIZE));
361
- f.set(new Float64Array(buf, fp, INSTANCE_PARAMS.SIZE));
362
- materials.set(new Int32Array(buf, h[H.MATERIALS], materials.length));
363
- const n = h[H.COUNT] * h[H.NROWS];
364
- const o = H.OUT;
365
- return {
366
- matrices: new Float32Array(buf, h[o], n * 16),
367
- spheres: new Float32Array(buf, h[o + 1], n * 4),
368
- geom: new Uint32Array(buf, h[o + 2], n),
369
- mat: new Uint32Array(buf, h[o + 3], n),
370
- uid: new Uint32Array(buf, h[o + 4], n),
371
- lodNode: new Int32Array(buf, h[o + 5], n),
372
- lodChild: new Uint16Array(buf, h[o + 6], n),
373
- flags: new Uint8Array(buf, h[o + 7], n),
374
- switches: new Float64Array(buf, h[o + 8], h[H.COUNT] * h[H.LODS] * 4),
375
- };
376
- }
377
-
378
- /** es3dCos(x) and es3dSin(x) as the instance kernel computes them (for the tests), or null. */
379
- export function cosSinWasm(x) {
380
- if (!enabled || !simd) return null;
381
- simd.reset();
382
- const p = simd.alloc(16);
383
- simd.cos_sin(x, p);
384
- return Array.from(new Float64Array(simd.memory.buffer, p, 2));
385
- }
1
+ // WebAssembly versions of the reader's hottest loops: number lists, normals,
2
+ // sweeps, extrusions, triangulation, primitives and the instance rows of
3
+ // <repeat> and <scatter>. They are built from wasm/ by `npm run wasm` and
4
+ // compute exactly what the JavaScript versions compute (the same IEEE
5
+ // operations in the same order; sin, cos, atan2 and hypot come from this
6
+ // page's Math). Where WebAssembly is missing or blocked, for example by a
7
+ // content security policy without 'wasm-unsafe-eval', every caller keeps
8
+ // using its JavaScript version.
9
+
10
+ import BINARY, { INSTANCES } from './kernels-binary.js';
11
+
12
+ let text = ''; // the number list being read, for the rare numbers the kernel hands back to Number()
13
+ let material = null; // resolves an instance row's material code (see instancesWasm)
14
+
15
+ const imports = {
16
+ env: {
17
+ sin: Math.sin,
18
+ cos: Math.cos,
19
+ atan2: Math.atan2,
20
+ tan: Math.tan,
21
+ acos: Math.acos,
22
+ hypot2: Math.hypot,
23
+ hypot3: Math.hypot,
24
+ number: (start, end) => Number(text.slice(start, end)),
25
+ material: (code) => material(code),
26
+ },
27
+ };
28
+
29
+ function decode(base64) {
30
+ // native decoders where there is one (newer browsers, Node); a loop otherwise
31
+ if (typeof Uint8Array.fromBase64 === 'function') return Uint8Array.fromBase64(base64);
32
+ if (typeof Buffer === 'function') return Buffer.from(base64, 'base64');
33
+ const s = atob(base64);
34
+ const bytes = new Uint8Array(s.length);
35
+ for (let i = 0; i < s.length; i++) bytes[i] = s.charCodeAt(i);
36
+ return bytes;
37
+ }
38
+
39
+ async function instantiate(base64) {
40
+ try {
41
+ if (typeof WebAssembly !== 'object') return null;
42
+ const { instance } = await WebAssembly.instantiate(decode(base64), imports);
43
+ return instance.exports;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ // the instance kernel is a module of its own: it needs WebAssembly SIMD
50
+ const [wasm, simd] = await Promise.all([instantiate(BINARY), instantiate(INSTANCES)]);
51
+ let enabled = wasm !== null;
52
+
53
+ /**
54
+ * Whether the module's hypot (V8's algorithm) gives this engine's Math.hypot
55
+ * results. The special cases (infinities, NaN, zeros) are fixed by the
56
+ * language; the rounding of finite values is the engine's, so values over
57
+ * many magnitudes are compared. Where they differ the kernels keep calling
58
+ * Math.hypot, which is slower (every call leaves WebAssembly and allocates).
59
+ */
60
+ function hypotMatches() {
61
+ const edge = [0, -0, 3, -4, 5e-324, 1e308, Infinity, NaN];
62
+ for (const x of edge) for (const y of edge) if (!Object.is(wasm.hypot2(x, y), Math.hypot(x, y))) return false;
63
+ let seed = 12345;
64
+ const next = () => {
65
+ seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
66
+ return (seed / 4294967296 - 0.5) * 10 ** ((seed % 41) - 20);
67
+ };
68
+ for (let i = 0; i < 100; i++) {
69
+ const x = next(), y = next(), z = next();
70
+ if (!Object.is(wasm.hypot3(x, y, z), Math.hypot(x, y, z)) || !Object.is(wasm.hypot2(x, y), Math.hypot(x, y))) return false;
71
+ }
72
+ return true;
73
+ }
74
+ if (wasm) wasm.set_native_hypot(hypotMatches() ? 1 : 0);
75
+
76
+ /** Whether the kernels are in use. */
77
+ export function wasmEnabled() {
78
+ return enabled;
79
+ }
80
+
81
+ /** Whether the instance kernel is in use (it needs WebAssembly SIMD as well). */
82
+ export function instanceKernelEnabled() {
83
+ return enabled && simd !== null;
84
+ }
85
+
86
+ /** Turn the kernels off (or back on, when they loaded) to compare with the JavaScript versions. */
87
+ export function setWasmEnabled(on) {
88
+ enabled = Boolean(on) && wasm !== null;
89
+ }
90
+
91
+ const heap = () => wasm.memory.buffer;
92
+
93
+ /** Copy a typed array into a module's memory; returns its address (0 when memory ran out). */
94
+ function put(a, module = wasm) {
95
+ const p = module.alloc(Math.max(1, a.byteLength));
96
+ if (p) new Uint8Array(module.memory.buffer, p, a.byteLength).set(new Uint8Array(a.buffer, a.byteOffset, a.byteLength));
97
+ return p;
98
+ }
99
+
100
+ const encoder = new TextEncoder();
101
+ const AS = { f64: [0, Float64Array], f32: [1, Float32Array], index: [2, Uint32Array] };
102
+
103
+ /**
104
+ * A number list read into a typed array (`as`: 'f64', 'f32' or 'index'), or
105
+ * null when the JavaScript reader must handle it (the kernels are off, the
106
+ * text is not ASCII, or it has an error, which JavaScript then reports).
107
+ */
108
+ export function readNumbersWasm(raw, as) {
109
+ if (!enabled) return null;
110
+ const [kind, Type] = AS[as];
111
+ const n = raw.length;
112
+ wasm.reset();
113
+ const sp = wasm.alloc(Math.max(1, n));
114
+ const op = sp && wasm.alloc((Math.floor(n / 2) + 1) * Type.BYTES_PER_ELEMENT);
115
+ if (!op) return null;
116
+ const r = encoder.encodeInto(raw, new Uint8Array(heap(), sp, n));
117
+ if (r.read !== n || r.written !== n) return null;
118
+ text = raw;
119
+ const count = wasm.numbers(sp, n, op, kind);
120
+ text = '';
121
+ if (count < 0) return null;
122
+ return new Type(heap(), op, count).slice();
123
+ }
124
+
125
+ /**
126
+ * computeNormals for Float32Array positions and uvs and Uint32Array indices,
127
+ * as {positions, normals, uvs, indices}, or null to use the JavaScript version.
128
+ */
129
+ export function computeNormalsWasm(positions, indices, crease, cosCrease, uvs) {
130
+ if (!enabled || !(positions instanceof Float32Array) || !(indices instanceof Uint32Array) || (uvs && !(uvs instanceof Float32Array))) return null;
131
+ wasm.reset();
132
+ const pp = put(positions), ip = put(indices), tp = uvs ? put(uvs) : 0;
133
+ if (!pp || !ip || (uvs && !tp)) return null;
134
+ const res = wasm.normals(pp, positions.length / 3, ip, indices.length, tp, crease, cosCrease);
135
+ if (!res) return null;
136
+ const [vc, ic, p, nrm, t, i] = new Uint32Array(heap(), res, 6);
137
+ return {
138
+ positions: new Float32Array(heap(), p, vc * 3).slice(),
139
+ normals: new Float32Array(heap(), nrm, vc * 3).slice(),
140
+ uvs: new Float32Array(heap(), t, vc * 2).slice(),
141
+ indices: new Uint32Array(heap(), i, ic).slice(),
142
+ };
143
+ }
144
+
145
+ const f64 = (a) => (a instanceof Float64Array ? a : Float64Array.from(a));
146
+ const u32 = (a) => (a instanceof Uint32Array ? a : Uint32Array.from(a));
147
+ const u8 = (a) => (a instanceof Uint8Array ? a : Uint8Array.from(a));
148
+
149
+ /**
150
+ * Writes contours ({points, smooth, isWall}) as {n, points, smooth, isWall}
151
+ * records; returns the table's address, or 0 when memory ran out.
152
+ */
153
+ function contourTable(list) {
154
+ const rows = new Int32Array(list.length * 4);
155
+ for (let k = 0; k < list.length; k++) {
156
+ const c = list[k];
157
+ const p = put(f64(c.points));
158
+ const sm = c.smooth ? put(u8(c.smooth)) : 0;
159
+ if (!p || (c.smooth && !sm)) return 0;
160
+ rows[k * 4] = c.points.length / 2;
161
+ rows[k * 4 + 1] = p;
162
+ rows[k * 4 + 2] = sm;
163
+ rows[k * 4 + 3] = c.isWall === false ? 0 : 1;
164
+ }
165
+ return put(rows);
166
+ }
167
+
168
+ /** {first contour, contour count} per region, appending each region's contours to `list`. */
169
+ function regionRows(regions, list) {
170
+ const rows = new Int32Array(Math.max(1, regions.length * 2));
171
+ regions.forEach((r, k) => {
172
+ rows[k * 2] = list.length;
173
+ rows[k * 2 + 1] = 1 + r.holes.length;
174
+ list.push(r.outer, ...r.holes);
175
+ });
176
+ return rows;
177
+ }
178
+
179
+ /** A kernel's mesh, copied out of kernel memory into one buffer (one allocation instead of four). */
180
+ const meshOut = (res, extra) => {
181
+ const [vc, ic, p, nrm, t, i] = new Uint32Array(heap(), res, 6);
182
+ const out = new ArrayBuffer((vc * 8 + ic) * 4);
183
+ const positions = new Float32Array(out, 0, vc * 3);
184
+ const normals = new Float32Array(out, vc * 12, vc * 3);
185
+ const uvs = new Float32Array(out, vc * 24, vc * 2);
186
+ const indices = new Uint32Array(out, vc * 32, ic);
187
+ positions.set(new Float32Array(heap(), p, vc * 3));
188
+ normals.set(new Float32Array(heap(), nrm, vc * 3));
189
+ uvs.set(new Float32Array(heap(), t, vc * 2));
190
+ indices.set(new Uint32Array(heap(), i, ic));
191
+ return { positions, normals, uvs, indices, ...extra };
192
+ };
193
+
194
+ /**
195
+ * sweepMesh after its profile is known: spines [{points, closed}], the
196
+ * profile contours, the circle's points (circle profiles) or the regions to
197
+ * cap (shaped profiles). Returns {positions, normals, uvs, indices, open,
198
+ * degenerate: one flag per spine, errors: triangulation error per cap
199
+ * region}, or null to use the JavaScript version.
200
+ */
201
+ export function sweepWasm(spines, contours, circle, regions, twist, taper, caps) {
202
+ if (!enabled) return null;
203
+ wasm.reset();
204
+ const list = [...contours];
205
+ const regionTab = circle ? null : regionRows(regions, list);
206
+ const table = contourTable(list);
207
+ const spineRows = new Int32Array(Math.max(1, spines.length * 3));
208
+ for (let k = 0; k < spines.length; k++) {
209
+ spineRows[k * 3] = put(f64(spines[k].points));
210
+ spineRows[k * 3 + 1] = spines[k].points.length / 3;
211
+ spineRows[k * 3 + 2] = spines[k].closed ? 1 : 0;
212
+ if (!spineRows[k * 3]) return null;
213
+ }
214
+ const profile = put(Int32Array.from(contours, (_, k) => k));
215
+ const spineTab = put(spineRows);
216
+ const capData = circle ? put(f64(circle)) : put(regionTab);
217
+ const header = put(
218
+ Int32Array.of(spines.length, spineTab, list.length, table, contours.length, profile, circle ? 1 : 2, circle ? circle.length / 2 : regions.length, capData, caps ? 1 : 0),
219
+ );
220
+ if (!table || !profile || !spineTab || !capData || !header) return null;
221
+ const res = wasm.sweep(header, twist, taper);
222
+ if (!res) return null;
223
+ const [open, deg, errs] = new Uint32Array(heap(), res + 24, 3);
224
+ return meshOut(res, {
225
+ open: open !== 0,
226
+ degenerate: Array.from(new Uint8Array(heap(), deg, spines.length), (d) => d !== 0),
227
+ errors: circle ? [] : Array.from(new Float64Array(heap(), errs, regions.length)),
228
+ });
229
+ }
230
+
231
+ /**
232
+ * triangulate(data, holeIndices) as an array of vertex indices, or null to
233
+ * use the JavaScript version (also when two holes tie for the sort order).
234
+ */
235
+ export function triangulateWasm(data, holeIndices) {
236
+ if (!enabled) return null;
237
+ wasm.reset();
238
+ const dp = put(f64(data));
239
+ const holes = holeIndices && holeIndices.length ? Int32Array.from(holeIndices) : null;
240
+ const hp = holes ? put(holes) : 0;
241
+ if (!dp || (holes && !hp)) return null;
242
+ let res;
243
+ try {
244
+ res = wasm.triangulate(dp, data.length, hp, holes ? holes.length : 0);
245
+ } catch {
246
+ return null; // stack exhausted on pathological input: JavaScript has a deeper stack
247
+ }
248
+ if (!res) return null;
249
+ const [count, p] = new Uint32Array(heap(), res, 2);
250
+ return count ? Array.from(new Uint32Array(heap(), p, count)) : [];
251
+ }
252
+
253
+ /**
254
+ * extrudeMesh from a shape ({regions, walls}) with the bevel profile rings
255
+ * {inset, z, nr, nz} and bounds from JavaScript. Returns {positions,
256
+ * normals, uvs, indices, errors: triangulation error per region}, or null
257
+ * to use the JavaScript version.
258
+ */
259
+ export function extrudeWasm(shape, bevel, rings, z0, zTop, minX, minY, S) {
260
+ if (!enabled) return null;
261
+ wasm.reset();
262
+ const list = [];
263
+ const regionTab = put(regionRows(shape.regions, list));
264
+ const walls = put(Int32Array.from(shape.walls, (_, k) => list.length + k));
265
+ list.push(...shape.walls);
266
+ const table = contourTable(list);
267
+ const ringData = new Float64Array(Math.max(1, rings.length * 4));
268
+ rings.forEach((r, k) => ringData.set([r.inset, r.z, r.nr, r.nz], k * 4));
269
+ const ringPtr = put(ringData);
270
+ const header = put(Int32Array.of(list.length, table, shape.regions.length, regionTab, shape.walls.length, walls, rings.length, ringPtr));
271
+ if (!regionTab || !walls || !table || !ringPtr || !header) return null;
272
+ const res = wasm.extrude(header, bevel, z0, zTop, minX, minY, S);
273
+ if (!res) return null;
274
+ const errs = new Uint32Array(heap(), res + 24, 1)[0];
275
+ return meshOut(res, { errors: Array.from(new Float64Array(heap(), errs, shape.regions.length)) });
276
+ }
277
+
278
+ /** measureSphereDeviation(w) for w >= 3, or null to use the JavaScript version. */
279
+ export function sphereDeviationWasm(w) {
280
+ if (!enabled) return null;
281
+ wasm.reset();
282
+ const v = wasm.sphere_deviation(w);
283
+ return v < 0 ? null : v;
284
+ }
285
+
286
+ const whole = (...v) => v.every((x) => Number.isInteger(x) && x >= 0 && x < 2 ** 30);
287
+ const prim = (res) => (res ? meshOut(res) : null);
288
+
289
+ /** unitSphere, unitCylinder, unitCapsule, unitTorus and roundedBox ({positions, normals, uvs, indices}), or null. */
290
+ export const primitivesWasm = {
291
+ sphere: (segments) => (enabled && whole(segments) ? (wasm.reset(), prim(wasm.sphere(segments))) : null),
292
+ cylinder: (top, segments, caps) => (enabled && whole(segments) ? (wasm.reset(), prim(wasm.cylinder(top, segments, caps ? 1 : 0))) : null),
293
+ capsule: (L, segments) => (enabled && whole(segments) ? (wasm.reset(), prim(wasm.capsule(L, segments))) : null),
294
+ torus: (t, arcDeg, segments, tubeSegments) => (enabled && whole(segments, tubeSegments) ? (wasm.reset(), prim(wasm.torus(t, arcDeg, segments, tubeSegments))) : null),
295
+ roundedBox: (w, h, d, r, seg) => (enabled && whole(seg) && seg >= 1 ? (wasm.reset(), prim(wasm.rounded_box(w, h, d, r, seg))) : null),
296
+ };
297
+
298
+ /** catmullRom for two or more points (a Float64Array), or null. */
299
+ export function catmullRomWasm(points, closed, subdivisions) {
300
+ if (!enabled || !whole(subdivisions) || points.length < 6 || points.length % 3) return null;
301
+ wasm.reset();
302
+ const p = put(f64(points));
303
+ const res = p && wasm.catmull_rom(p, points.length / 3, closed ? 1 : 0, subdivisions);
304
+ if (!res) return null;
305
+ const [count, out] = new Uint32Array(heap(), res, 2);
306
+ return new Float64Array(heap(), out, count * 3).slice();
307
+ }
308
+
309
+ /** gearOutline ({outline: {points, smooth, closed}, outerRadius, rootRadius}), or null. */
310
+ export function gearOutlineWasm(teeth, pressureAngleDeg, flankSamples, arcSamples) {
311
+ if (!enabled || !whole(teeth, flankSamples, arcSamples) || teeth < 1) return null;
312
+ wasm.reset();
313
+ const res = wasm.gear_outline(teeth, pressureAngleDeg, flankSamples, arcSamples);
314
+ if (!res) return null;
315
+ const [count, pts, smooth, radii] = new Uint32Array(heap(), res, 4);
316
+ const [outerRadius, rootRadius] = new Float64Array(heap(), radii, 2);
317
+ return {
318
+ outline: { points: new Float64Array(heap(), pts, count * 2).slice(), smooth: new Uint8Array(heap(), smooth, count).slice(), closed: true },
319
+ outerRadius,
320
+ rootRadius,
321
+ };
322
+ }
323
+
324
+ // Header layouts of the instance kernel (the enums in wasm/simd/instances.c).
325
+ export const INSTANCE_HEADER = {
326
+ KIND: 0, FIRST: 1, COUNT: 2, STATE: 3, MODE: 4, NX: 5, NZ: 6, RANDOM: 7, CHOICES: 8,
327
+ OPS: 9, NOPS: 10, MATS: 11, ROWS: 12, NROWS: 13, BOUNDS: 14, MATERIALS: 15, LODS: 16, OUTER_LOD: 17, SWITCH: 18, SLOTS: 19,
328
+ OUT: 20, SIZE: 29,
329
+ };
330
+ export const INSTANCE_PARAMS = { WORLD: 0, ACC: 16, STEP: 32, SPACING: 48, JITTER: 50, RADIUS: 51, AREA: 52, ROTATION: 54, SCALE: 60, BOUNDS: 66, SIZE: 72 };
331
+
332
+ /**
333
+ * Instance rows of placements [FIRST, FIRST + COUNT) of a <repeat> or
334
+ * <scatter> whose subtree was recorded as `template` ({ops, mats, table,
335
+ * bounds}, see recordTemplate in src/scene/flatten.js). The headers `h`
336
+ * (Int32Array) and `f` (Float64Array) and the table `materials` (material
337
+ * index per material code, -1 until resolveMaterial(code) has given it)
338
+ * carry the generator state, the accumulated repeat step, the scene bounds
339
+ * and the materials from one call to the next. Returns views of kernel
340
+ * memory, valid until the next kernel call, or null.
341
+ */
342
+ export function instancesWasm(h, f, template, materials, resolveMaterial) {
343
+ if (!enabled || !simd) return null;
344
+ simd.reset();
345
+ const H = INSTANCE_HEADER;
346
+ h[H.OPS] = put(template.ops, simd);
347
+ h[H.MATS] = put(template.mats, simd);
348
+ h[H.ROWS] = put(template.table, simd);
349
+ h[H.BOUNDS] = put(template.bounds, simd);
350
+ h[H.MATERIALS] = put(materials, simd);
351
+ const hp = put(h, simd), fp = put(f, simd);
352
+ if (!h[H.OPS] || !h[H.MATS] || !h[H.ROWS] || !h[H.BOUNDS] || !h[H.MATERIALS] || !hp || !fp) return null;
353
+ material = resolveMaterial;
354
+ try {
355
+ if (!simd.instances(hp, fp)) return null;
356
+ } finally {
357
+ material = null;
358
+ }
359
+ const buf = simd.memory.buffer;
360
+ h.set(new Int32Array(buf, hp, H.SIZE));
361
+ f.set(new Float64Array(buf, fp, INSTANCE_PARAMS.SIZE));
362
+ materials.set(new Int32Array(buf, h[H.MATERIALS], materials.length));
363
+ const n = h[H.COUNT] * h[H.NROWS];
364
+ const o = H.OUT;
365
+ return {
366
+ matrices: new Float32Array(buf, h[o], n * 16),
367
+ spheres: new Float32Array(buf, h[o + 1], n * 4),
368
+ geom: new Uint32Array(buf, h[o + 2], n),
369
+ mat: new Uint32Array(buf, h[o + 3], n),
370
+ uid: new Uint32Array(buf, h[o + 4], n),
371
+ lodNode: new Int32Array(buf, h[o + 5], n),
372
+ lodChild: new Uint16Array(buf, h[o + 6], n),
373
+ flags: new Uint8Array(buf, h[o + 7], n),
374
+ switches: new Float64Array(buf, h[o + 8], h[H.COUNT] * h[H.LODS] * 4),
375
+ };
376
+ }
377
+
378
+ /** es3dCos(x) and es3dSin(x) as the instance kernel computes them (for the tests), or null. */
379
+ export function cosSinWasm(x) {
380
+ if (!enabled || !simd) return null;
381
+ simd.reset();
382
+ const p = simd.alloc(16);
383
+ simd.cos_sin(x, p);
384
+ return Array.from(new Float64Array(simd.memory.buffer, p, 2));
385
+ }