@es3d/es3d 0.0.0-stage → 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 (82) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +85 -2
  3. package/docs/REFERENCE.md +581 -0
  4. package/docs/SPEC.md +417 -0
  5. package/package.json +50 -4
  6. package/scripts/es3d-cli.mjs +377 -0
  7. package/src/cache/geometry-store.js +392 -0
  8. package/src/cache/mesh-bundle.js +123 -0
  9. package/src/cache/sha256.js +73 -0
  10. package/src/format/ast.js +238 -0
  11. package/src/format/colors.js +93 -0
  12. package/src/format/compact-parser.js +171 -0
  13. package/src/format/compact-tokenizer.js +165 -0
  14. package/src/format/errors.js +247 -0
  15. package/src/format/json.js +55 -0
  16. package/src/format/limits.js +14 -0
  17. package/src/format/parse.js +92 -0
  18. package/src/format/schema.js +617 -0
  19. package/src/format/serializer.js +333 -0
  20. package/src/format/validator.js +330 -0
  21. package/src/format/value-error.js +12 -0
  22. package/src/format/value-format.js +314 -0
  23. package/src/format/values.js +459 -0
  24. package/src/format/xml-parser.js +136 -0
  25. package/src/format/xml-tokenizer.js +237 -0
  26. package/src/geometry/contour.js +86 -0
  27. package/src/geometry/extrude.js +175 -0
  28. package/src/geometry/gear.js +85 -0
  29. package/src/geometry/geometry-cache.js +159 -0
  30. package/src/geometry/lod-error.js +178 -0
  31. package/src/geometry/mesh.js +458 -0
  32. package/src/geometry/primitives.js +299 -0
  33. package/src/geometry/registry.js +645 -0
  34. package/src/geometry/revolve.js +136 -0
  35. package/src/geometry/sweep.js +441 -0
  36. package/src/index.js +47 -0
  37. package/src/io/compression.js +67 -0
  38. package/src/io/convert.js +236 -0
  39. package/src/io/download.js +13 -0
  40. package/src/io/external-scene.js +127 -0
  41. package/src/io/file-loader.js +28 -0
  42. package/src/io/gltf.js +630 -0
  43. package/src/io/mesh-import.js +104 -0
  44. package/src/io/obj.js +352 -0
  45. package/src/io/step-brep.js +439 -0
  46. package/src/io/step-exact.js +1149 -0
  47. package/src/io/step.js +1418 -0
  48. package/src/io/stl.js +193 -0
  49. package/src/io/svg-import.js +787 -0
  50. package/src/material/materials.js +154 -0
  51. package/src/math/mat4.js +475 -0
  52. package/src/math/random.js +30 -0
  53. package/src/math/trig.js +65 -0
  54. package/src/math/vec3.js +81 -0
  55. package/src/optimize/optimizer.js +579 -0
  56. package/src/path/path-flatten.js +322 -0
  57. package/src/path/path-parser.js +332 -0
  58. package/src/path/shapes.js +129 -0
  59. package/src/path/triangulate.js +517 -0
  60. package/src/pipeline.js +74 -0
  61. package/src/render/camera.js +168 -0
  62. package/src/render/frame-builder.js +362 -0
  63. package/src/render/geometry-pool.js +149 -0
  64. package/src/render/lighting.js +149 -0
  65. package/src/render/orbit-controls.js +237 -0
  66. package/src/render/renderer.js +258 -0
  67. package/src/render/scroll-timeline.js +104 -0
  68. package/src/render/shaders/glsl.js +285 -0
  69. package/src/render/shaders/wgsl.js +252 -0
  70. package/src/render/stage.js +364 -0
  71. package/src/render/texture-cache.js +40 -0
  72. package/src/render/textures.js +223 -0
  73. package/src/render/webgl2/webgl2-backend.js +586 -0
  74. package/src/render/webgpu/webgpu-backend.js +482 -0
  75. package/src/scene/animation.js +171 -0
  76. package/src/scene/camera-constraints.js +117 -0
  77. package/src/scene/flatten.js +754 -0
  78. package/src/scene/picking.js +161 -0
  79. package/src/scene/scene-graph.js +320 -0
  80. package/src/transform/transform.js +211 -0
  81. package/src/wasm/kernels-binary.js +3 -0
  82. package/src/wasm/kernels.js +385 -0
@@ -0,0 +1,154 @@
1
+ // Material system: a deliberately small metallic/roughness model.
2
+ //
3
+ // <material> elements define named parameter sets. Shapes reference them by
4
+ // id, and `material` / `color` are inherited down the tree like SVG's fill.
5
+ // The effective material of a shape is (inherited material, inherited color
6
+ // override); identical parameter sets are deduplicated into one Material, so
7
+ // the renderer binds each distinct material exactly once.
8
+
9
+ import { colorToLinear, colorToHex } from '../format/colors.js';
10
+ import { valueOf } from '../format/schema.js';
11
+
12
+ export const DEFAULT_MATERIAL_PARAMS = {
13
+ color: [0.8, 0.8, 0.8, 1],
14
+ metallic: 0,
15
+ roughness: 0.5,
16
+ opacity: 1,
17
+ emissive: [0, 0, 0, 1],
18
+ emissiveIntensity: 1,
19
+ map: null,
20
+ normalMap: null,
21
+ normalScale: 1,
22
+ occlusionMap: null,
23
+ occlusionStrength: 1,
24
+ metallicRoughnessMap: null,
25
+ emissiveMap: null,
26
+ alphaMode: 'auto',
27
+ alphaCutoff: 0.5,
28
+ uvScale: [1, 1],
29
+ doubleSided: false,
30
+ unlit: false,
31
+ };
32
+
33
+ export class Material {
34
+ constructor(params, index, name = null) {
35
+ this.index = index;
36
+ this.name = name;
37
+ this.params = params;
38
+ this.baseColor = colorToLinear(params.color);
39
+ this.baseColorSrgb = params.color;
40
+ this.metallic = params.metallic;
41
+ this.roughness = params.roughness;
42
+ this.opacity = params.opacity * params.color[3];
43
+ const e = colorToLinear(params.emissive);
44
+ this.emissive = [e[0] * params.emissiveIntensity, e[1] * params.emissiveIntensity, e[2] * params.emissiveIntensity];
45
+ this.map = params.map;
46
+ this.normalMap = params.normalMap;
47
+ this.normalScale = params.normalScale;
48
+ this.occlusionMap = params.occlusionMap;
49
+ this.occlusionStrength = params.occlusionStrength;
50
+ this.metallicRoughnessMap = params.metallicRoughnessMap;
51
+ this.emissiveMap = params.emissiveMap;
52
+ this.alphaCutoff = params.alphaCutoff;
53
+ const coverage = params.opacity * params.color[3];
54
+ const requested = params.alphaMode || 'auto';
55
+ this.alphaMode = requested === 'auto' ? (coverage < 0.999 ? 'blend' : 'opaque') : requested;
56
+ this.uvScale = params.uvScale;
57
+ this.doubleSided = params.doubleSided;
58
+ this.unlit = params.unlit;
59
+ this.key = materialKey(params);
60
+ }
61
+
62
+ get transparent() {
63
+ return this.alphaMode === 'blend' && this.opacity < 0.999;
64
+ }
65
+ }
66
+
67
+ export function materialKey(p) {
68
+ return [
69
+ colorToHex(p.color),
70
+ p.metallic,
71
+ p.roughness,
72
+ p.opacity,
73
+ colorToHex(p.emissive),
74
+ p.emissiveIntensity,
75
+ p.map ? p.map.key : '',
76
+ p.normalMap ? p.normalMap.key : '',
77
+ p.normalScale,
78
+ p.occlusionMap ? p.occlusionMap.key : '',
79
+ p.occlusionStrength,
80
+ p.metallicRoughnessMap ? p.metallicRoughnessMap.key : '',
81
+ p.emissiveMap ? p.emissiveMap.key : '',
82
+ p.alphaMode,
83
+ p.alphaCutoff,
84
+ p.uvScale.join(','),
85
+ p.doubleSided ? 1 : 0,
86
+ p.unlit ? 1 : 0,
87
+ ].join('|');
88
+ }
89
+
90
+ /** Parameters of a <material> element with schema defaults applied. */
91
+ export function paramsFromElement(el) {
92
+ const p = {};
93
+ for (const k of Object.keys(DEFAULT_MATERIAL_PARAMS)) {
94
+ const v = valueOf(el, k);
95
+ p[k] = v === undefined ? DEFAULT_MATERIAL_PARAMS[k] : v;
96
+ }
97
+ return p;
98
+ }
99
+
100
+ export class MaterialTable {
101
+ constructor() {
102
+ /** @type {Material[]} */
103
+ this.list = [];
104
+ this.byKey = new Map();
105
+ this.defs = new Map(); // material id -> params
106
+ this.resolved = new Map(); // "id|color" -> index
107
+ this.fast = new Map(); // id -> Map(color array object -> index), identity cache for hot loops
108
+ }
109
+
110
+ define(id, params) {
111
+ this.defs.set(id, params);
112
+ }
113
+
114
+ intern(params, name) {
115
+ const key = materialKey(params);
116
+ let idx = this.byKey.get(key);
117
+ if (idx === undefined) {
118
+ idx = this.list.length;
119
+ this.list.push(new Material(params, idx, name));
120
+ this.byKey.set(key, idx);
121
+ }
122
+ return idx;
123
+ }
124
+
125
+ /**
126
+ * Effective material index for an inherited (material id, color override) pair.
127
+ * @param {string|null} id
128
+ * @param {number[]|null} color sRGB rgba
129
+ */
130
+ resolve(id, color) {
131
+ let byColor = this.fast.get(id);
132
+ if (byColor === undefined) {
133
+ byColor = new Map();
134
+ this.fast.set(id, byColor);
135
+ }
136
+ let fast = byColor.get(color);
137
+ if (fast !== undefined) return fast;
138
+ fast = this.resolveSlow(id, color);
139
+ byColor.set(color, fast);
140
+ return fast;
141
+ }
142
+
143
+ resolveSlow(id, color) {
144
+ const ck = color ? colorToHex(color) : '';
145
+ const rk = (id || '') + '|' + ck;
146
+ let idx = this.resolved.get(rk);
147
+ if (idx !== undefined) return idx;
148
+ const base = (id && this.defs.get(id)) || DEFAULT_MATERIAL_PARAMS;
149
+ const params = color ? { ...base, color } : base;
150
+ idx = this.intern(params, id || (color ? ck : 'default'));
151
+ this.resolved.set(rk, idx);
152
+ return idx;
153
+ }
154
+ }
@@ -0,0 +1,475 @@
1
+ // Minimal column-major 4x4 matrix library (same memory layout as WebGL/WebGPU).
2
+ // Functions write into `out` and return it; `out` may alias an input.
3
+ // Rotations use the normative placement trig so two readers build the same matrix.
4
+
5
+ import { es3dCos, es3dSin } from './trig.js';
6
+
7
+ export const DEG = Math.PI / 180;
8
+
9
+ export function create() {
10
+ const m = new Float32Array(16);
11
+ m[0] = m[5] = m[10] = m[15] = 1;
12
+ return m;
13
+ }
14
+
15
+ export function createF64() {
16
+ const m = new Float64Array(16);
17
+ m[0] = m[5] = m[10] = m[15] = 1;
18
+ return m;
19
+ }
20
+
21
+ export function identity(out) {
22
+ out.fill(0);
23
+ out[0] = out[5] = out[10] = out[15] = 1;
24
+ return out;
25
+ }
26
+
27
+ export function copy(out, a) {
28
+ for (let i = 0; i < 16; i++) out[i] = a[i];
29
+ return out;
30
+ }
31
+
32
+ export function clone(a) {
33
+ return new Float64Array(a);
34
+ }
35
+
36
+ export function isIdentity(a, eps = 1e-12) {
37
+ for (let i = 0; i < 16; i++) {
38
+ const e = i % 5 === 0 ? 1 : 0;
39
+ if (Math.abs(a[i] - e) > eps) return false;
40
+ }
41
+ return true;
42
+ }
43
+
44
+ export function multiply(out, a, b) {
45
+ const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
46
+ const a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7];
47
+ const a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11];
48
+ const a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
49
+ let b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
50
+ out[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
51
+ out[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
52
+ out[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
53
+ out[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
54
+ b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7];
55
+ out[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
56
+ out[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
57
+ out[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
58
+ out[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
59
+ b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11];
60
+ out[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
61
+ out[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
62
+ out[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
63
+ out[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
64
+ b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15];
65
+ out[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
66
+ out[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
67
+ out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
68
+ out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
69
+ return out;
70
+ }
71
+
72
+ /** Multiply affine matrices (last row is 0,0,0,1). Faster for scene graphs. */
73
+ export function multiplyAffine(out, a, b) {
74
+ const a00 = a[0], a01 = a[1], a02 = a[2];
75
+ const a10 = a[4], a11 = a[5], a12 = a[6];
76
+ const a20 = a[8], a21 = a[9], a22 = a[10];
77
+ const a30 = a[12], a31 = a[13], a32 = a[14];
78
+ const b00 = b[0], b01 = b[1], b02 = b[2];
79
+ const b10 = b[4], b11 = b[5], b12 = b[6];
80
+ const b20 = b[8], b21 = b[9], b22 = b[10];
81
+ const b30 = b[12], b31 = b[13], b32 = b[14];
82
+ out[0] = b00 * a00 + b01 * a10 + b02 * a20;
83
+ out[1] = b00 * a01 + b01 * a11 + b02 * a21;
84
+ out[2] = b00 * a02 + b01 * a12 + b02 * a22;
85
+ out[3] = 0;
86
+ out[4] = b10 * a00 + b11 * a10 + b12 * a20;
87
+ out[5] = b10 * a01 + b11 * a11 + b12 * a21;
88
+ out[6] = b10 * a02 + b11 * a12 + b12 * a22;
89
+ out[7] = 0;
90
+ out[8] = b20 * a00 + b21 * a10 + b22 * a20;
91
+ out[9] = b20 * a01 + b21 * a11 + b22 * a21;
92
+ out[10] = b20 * a02 + b21 * a12 + b22 * a22;
93
+ out[11] = 0;
94
+ out[12] = b30 * a00 + b31 * a10 + b32 * a20 + a30;
95
+ out[13] = b30 * a01 + b31 * a11 + b32 * a21 + a31;
96
+ out[14] = b30 * a02 + b31 * a12 + b32 * a22 + a32;
97
+ out[15] = 1;
98
+ return out;
99
+ }
100
+
101
+ export function fromTranslation(out, x, y, z) {
102
+ identity(out);
103
+ out[12] = x;
104
+ out[13] = y;
105
+ out[14] = z;
106
+ return out;
107
+ }
108
+
109
+ export function fromScaling(out, x, y, z) {
110
+ identity(out);
111
+ out[0] = x;
112
+ out[5] = y;
113
+ out[10] = z;
114
+ return out;
115
+ }
116
+
117
+ export function fromRotationX(out, rad) {
118
+ const c = es3dCos(rad), s = es3dSin(rad);
119
+ identity(out);
120
+ out[5] = c;
121
+ out[6] = s;
122
+ out[9] = -s;
123
+ out[10] = c;
124
+ return out;
125
+ }
126
+
127
+ export function fromRotationY(out, rad) {
128
+ const c = es3dCos(rad), s = es3dSin(rad);
129
+ identity(out);
130
+ out[0] = c;
131
+ out[2] = -s;
132
+ out[8] = s;
133
+ out[10] = c;
134
+ return out;
135
+ }
136
+
137
+ export function fromRotationZ(out, rad) {
138
+ const c = es3dCos(rad), s = es3dSin(rad);
139
+ identity(out);
140
+ out[0] = c;
141
+ out[1] = s;
142
+ out[4] = -s;
143
+ out[5] = c;
144
+ return out;
145
+ }
146
+
147
+ /**
148
+ * ES3D Euler convention: rotation="x,y,z" (degrees) rotates about the fixed X
149
+ * axis first, then Y, then Z, i.e. R = Rz * Ry * Rx (Blender "XYZ Euler").
150
+ */
151
+ export function fromEulerDeg(out, xd, yd, zd) {
152
+ const x = xd * DEG, y = yd * DEG, z = zd * DEG;
153
+ const cx = es3dCos(x), sx = es3dSin(x);
154
+ const cy = es3dCos(y), sy = es3dSin(y);
155
+ const cz = es3dCos(z), sz = es3dSin(z);
156
+ // columns of Rz*Ry*Rx
157
+ out[0] = cz * cy;
158
+ out[1] = sz * cy;
159
+ out[2] = -sy;
160
+ out[3] = 0;
161
+ out[4] = cz * sy * sx - sz * cx;
162
+ out[5] = sz * sy * sx + cz * cx;
163
+ out[6] = cy * sx;
164
+ out[7] = 0;
165
+ out[8] = cz * sy * cx + sz * sx;
166
+ out[9] = sz * sy * cx - cz * sx;
167
+ out[10] = cy * cx;
168
+ out[11] = 0;
169
+ out[12] = 0;
170
+ out[13] = 0;
171
+ out[14] = 0;
172
+ out[15] = 1;
173
+ return out;
174
+ }
175
+
176
+ export function fromAxisAngle(out, ax, ay, az, rad) {
177
+ let len = Math.hypot(ax, ay, az);
178
+ if (len < 1e-12) return identity(out);
179
+ len = 1 / len;
180
+ const x = ax * len, y = ay * len, z = az * len;
181
+ const s = es3dSin(rad), c = es3dCos(rad), t = 1 - c;
182
+ out[0] = x * x * t + c;
183
+ out[1] = y * x * t + z * s;
184
+ out[2] = z * x * t - y * s;
185
+ out[3] = 0;
186
+ out[4] = x * y * t - z * s;
187
+ out[5] = y * y * t + c;
188
+ out[6] = z * y * t + x * s;
189
+ out[7] = 0;
190
+ out[8] = x * z * t + y * s;
191
+ out[9] = y * z * t - x * s;
192
+ out[10] = z * z * t + c;
193
+ out[11] = 0;
194
+ out[12] = 0;
195
+ out[13] = 0;
196
+ out[14] = 0;
197
+ out[15] = 1;
198
+ return out;
199
+ }
200
+
201
+ export function fromQuat(out, x, y, z, w) {
202
+ const len = Math.hypot(x, y, z, w) || 1;
203
+ x /= len; y /= len; z /= len; w /= len;
204
+ const x2 = x + x, y2 = y + y, z2 = z + z;
205
+ const xx = x * x2, yx = y * x2, yy = y * y2;
206
+ const zx = z * x2, zy = z * y2, zz = z * z2;
207
+ const wx = w * x2, wy = w * y2, wz = w * z2;
208
+ out[0] = 1 - yy - zz;
209
+ out[1] = yx + wz;
210
+ out[2] = zx - wy;
211
+ out[3] = 0;
212
+ out[4] = yx - wz;
213
+ out[5] = 1 - xx - zz;
214
+ out[6] = zy + wx;
215
+ out[7] = 0;
216
+ out[8] = zx + wy;
217
+ out[9] = zy - wx;
218
+ out[10] = 1 - xx - yy;
219
+ out[11] = 0;
220
+ out[12] = 0;
221
+ out[13] = 0;
222
+ out[14] = 0;
223
+ out[15] = 1;
224
+ return out;
225
+ }
226
+
227
+ /** M = T(position) * R(euler) * S(scale) */
228
+ export function compose(out, position, rotationDeg, scale) {
229
+ fromEulerDeg(out, rotationDeg[0], rotationDeg[1], rotationDeg[2]);
230
+ const sx = scale[0], sy = scale[1], sz = scale[2];
231
+ out[0] *= sx; out[1] *= sx; out[2] *= sx;
232
+ out[4] *= sy; out[5] *= sy; out[6] *= sy;
233
+ out[8] *= sz; out[9] *= sz; out[10] *= sz;
234
+ out[12] = position[0];
235
+ out[13] = position[1];
236
+ out[14] = position[2];
237
+ return out;
238
+ }
239
+
240
+ export function invert(out, a) {
241
+ const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
242
+ const a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7];
243
+ const a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11];
244
+ const a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
245
+ const b00 = a00 * a11 - a01 * a10;
246
+ const b01 = a00 * a12 - a02 * a10;
247
+ const b02 = a00 * a13 - a03 * a10;
248
+ const b03 = a01 * a12 - a02 * a11;
249
+ const b04 = a01 * a13 - a03 * a11;
250
+ const b05 = a02 * a13 - a03 * a12;
251
+ const b06 = a20 * a31 - a21 * a30;
252
+ const b07 = a20 * a32 - a22 * a30;
253
+ const b08 = a20 * a33 - a23 * a30;
254
+ const b09 = a21 * a32 - a22 * a31;
255
+ const b10 = a21 * a33 - a23 * a31;
256
+ const b11 = a22 * a33 - a23 * a32;
257
+ let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
258
+ if (!det) return null;
259
+ det = 1 / det;
260
+ out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
261
+ out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
262
+ out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
263
+ out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det;
264
+ out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
265
+ out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
266
+ out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
267
+ out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det;
268
+ out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
269
+ out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
270
+ out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
271
+ out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det;
272
+ out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det;
273
+ out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det;
274
+ out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
275
+ out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
276
+ return out;
277
+ }
278
+
279
+ export function transpose(out, a) {
280
+ if (out === a) {
281
+ const a01 = a[1], a02 = a[2], a03 = a[3], a12 = a[6], a13 = a[7], a23 = a[11];
282
+ out[1] = a[4]; out[2] = a[8]; out[3] = a[12];
283
+ out[4] = a01; out[6] = a[9]; out[7] = a[13];
284
+ out[8] = a02; out[9] = a12; out[11] = a[14];
285
+ out[12] = a03; out[13] = a13; out[14] = a23;
286
+ return out;
287
+ }
288
+ for (let r = 0; r < 4; r++) for (let c = 0; c < 4; c++) out[c * 4 + r] = a[r * 4 + c];
289
+ return out;
290
+ }
291
+
292
+ export function determinant3(a) {
293
+ return (
294
+ a[0] * (a[5] * a[10] - a[6] * a[9]) -
295
+ a[4] * (a[1] * a[10] - a[2] * a[9]) +
296
+ a[8] * (a[1] * a[6] - a[2] * a[5])
297
+ );
298
+ }
299
+
300
+ /** Largest axis scale factor of the upper 3x3. */
301
+ export function maxScale(a) {
302
+ const sx = a[0] * a[0] + a[1] * a[1] + a[2] * a[2];
303
+ const sy = a[4] * a[4] + a[5] * a[5] + a[6] * a[6];
304
+ const sz = a[8] * a[8] + a[9] * a[9] + a[10] * a[10];
305
+ return Math.sqrt(Math.max(sx, sy, sz));
306
+ }
307
+
308
+ export function transformPoint(out, m, x, y, z) {
309
+ const w = m[3] * x + m[7] * y + m[11] * z + m[15] || 1;
310
+ out[0] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w;
311
+ out[1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w;
312
+ out[2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w;
313
+ return out;
314
+ }
315
+
316
+ export function transformDir(out, m, x, y, z) {
317
+ out[0] = m[0] * x + m[4] * y + m[8] * z;
318
+ out[1] = m[1] * x + m[5] * y + m[9] * z;
319
+ out[2] = m[2] * x + m[6] * y + m[10] * z;
320
+ return out;
321
+ }
322
+
323
+ /**
324
+ * Perspective projection. zeroToOne selects the WebGPU clip-space depth range
325
+ * [0,1]; otherwise the OpenGL range [-1,1] is used.
326
+ */
327
+ export function perspective(out, fovyRad, aspect, near, far, zeroToOne = false) {
328
+ const f = 1 / Math.tan(fovyRad / 2);
329
+ out.fill(0);
330
+ out[0] = f / aspect;
331
+ out[5] = f;
332
+ out[11] = -1;
333
+ if (zeroToOne) {
334
+ out[10] = far / (near - far);
335
+ out[14] = (far * near) / (near - far);
336
+ } else {
337
+ out[10] = (far + near) / (near - far);
338
+ out[14] = (2 * far * near) / (near - far);
339
+ }
340
+ return out;
341
+ }
342
+
343
+ export function ortho(out, left, right, bottom, top, near, far, zeroToOne = false) {
344
+ const lr = 1 / (left - right);
345
+ const bt = 1 / (bottom - top);
346
+ const nf = 1 / (near - far);
347
+ out.fill(0);
348
+ out[0] = -2 * lr;
349
+ out[5] = -2 * bt;
350
+ out[12] = (left + right) * lr;
351
+ out[13] = (top + bottom) * bt;
352
+ if (zeroToOne) {
353
+ out[10] = nf;
354
+ out[14] = near * nf;
355
+ } else {
356
+ out[10] = 2 * nf;
357
+ out[14] = (far + near) * nf;
358
+ }
359
+ out[15] = 1;
360
+ return out;
361
+ }
362
+
363
+ export function lookAt(out, eye, target, up) {
364
+ let zx = eye[0] - target[0], zy = eye[1] - target[1], zz = eye[2] - target[2];
365
+ let len = Math.hypot(zx, zy, zz);
366
+ if (len < 1e-12) return identity(out);
367
+ zx /= len; zy /= len; zz /= len;
368
+ let xx = up[1] * zz - up[2] * zy;
369
+ let xy = up[2] * zx - up[0] * zz;
370
+ let xz = up[0] * zy - up[1] * zx;
371
+ len = Math.hypot(xx, xy, xz);
372
+ if (len < 1e-12) {
373
+ // up is parallel to the view direction: pick another up vector
374
+ const alt = Math.abs(zy) < 0.9 ? [0, 1, 0] : [0, 0, 1];
375
+ xx = alt[1] * zz - alt[2] * zy;
376
+ xy = alt[2] * zx - alt[0] * zz;
377
+ xz = alt[0] * zy - alt[1] * zx;
378
+ len = Math.hypot(xx, xy, xz);
379
+ }
380
+ xx /= len; xy /= len; xz /= len;
381
+ const yx = zy * xz - zz * xy;
382
+ const yy = zz * xx - zx * xz;
383
+ const yz = zx * xy - zy * xx;
384
+ out[0] = xx; out[1] = yx; out[2] = zx; out[3] = 0;
385
+ out[4] = xy; out[5] = yy; out[6] = zy; out[7] = 0;
386
+ out[8] = xz; out[9] = yz; out[10] = zz; out[11] = 0;
387
+ out[12] = -(xx * eye[0] + xy * eye[1] + xz * eye[2]);
388
+ out[13] = -(yx * eye[0] + yy * eye[1] + yz * eye[2]);
389
+ out[14] = -(zx * eye[0] + zy * eye[1] + zz * eye[2]);
390
+ out[15] = 1;
391
+ return out;
392
+ }
393
+
394
+ /**
395
+ * Decompose an affine matrix into translation, rotation quaternion and scale.
396
+ * Returns null when the matrix contains shear (not representable as TRS).
397
+ */
398
+ export function decompose(m, eps = 1e-5) {
399
+ const t = [m[12], m[13], m[14]];
400
+ let sx = Math.hypot(m[0], m[1], m[2]);
401
+ const sy = Math.hypot(m[4], m[5], m[6]);
402
+ const sz = Math.hypot(m[8], m[9], m[10]);
403
+ if (determinant3(m) < 0) sx = -sx;
404
+ if (Math.abs(sx) < 1e-20 || sy < 1e-20 || sz < 1e-20) return null;
405
+ const r = [m[0] / sx, m[1] / sx, m[2] / sx, m[4] / sy, m[5] / sy, m[6] / sy, m[8] / sz, m[9] / sz, m[10] / sz];
406
+ // orthogonality check (shear detection)
407
+ const d01 = r[0] * r[3] + r[1] * r[4] + r[2] * r[5];
408
+ const d02 = r[0] * r[6] + r[1] * r[7] + r[2] * r[8];
409
+ const d12 = r[3] * r[6] + r[4] * r[7] + r[5] * r[8];
410
+ if (Math.abs(d01) > eps || Math.abs(d02) > eps || Math.abs(d12) > eps) return null;
411
+ const q = quatFromRotationMatrix(r);
412
+ return { translation: t, rotation: q, scale: [sx, sy, sz] };
413
+ }
414
+
415
+ /** r: 3x3 column-major rotation matrix */
416
+ export function quatFromRotationMatrix(r) {
417
+ const m00 = r[0], m10 = r[1], m20 = r[2];
418
+ const m01 = r[3], m11 = r[4], m21 = r[5];
419
+ const m02 = r[6], m12 = r[7], m22 = r[8];
420
+ const trace = m00 + m11 + m22;
421
+ let x, y, z, w;
422
+ if (trace > 0) {
423
+ const s = 0.5 / Math.sqrt(trace + 1);
424
+ w = 0.25 / s;
425
+ x = (m21 - m12) * s;
426
+ y = (m02 - m20) * s;
427
+ z = (m10 - m01) * s;
428
+ } else if (m00 > m11 && m00 > m22) {
429
+ const s = 2 * Math.sqrt(1 + m00 - m11 - m22);
430
+ w = (m21 - m12) / s;
431
+ x = 0.25 * s;
432
+ y = (m01 + m10) / s;
433
+ z = (m02 + m20) / s;
434
+ } else if (m11 > m22) {
435
+ const s = 2 * Math.sqrt(1 + m11 - m00 - m22);
436
+ w = (m02 - m20) / s;
437
+ x = (m01 + m10) / s;
438
+ y = 0.25 * s;
439
+ z = (m12 + m21) / s;
440
+ } else {
441
+ const s = 2 * Math.sqrt(1 + m22 - m00 - m11);
442
+ w = (m10 - m01) / s;
443
+ x = (m02 + m20) / s;
444
+ y = (m12 + m21) / s;
445
+ z = 0.25 * s;
446
+ }
447
+ const len = Math.hypot(x, y, z, w) || 1;
448
+ return [x / len, y / len, z / len, w / len];
449
+ }
450
+
451
+ /** Integer power of an affine matrix by repeated squaring. */
452
+ export function power(out, m, n) {
453
+ const result = createF64();
454
+ let base = new Float64Array(m);
455
+ let k = n;
456
+ const tmp = new Float64Array(16);
457
+ while (k > 0) {
458
+ if (k & 1) {
459
+ multiplyAffine(tmp, result, base);
460
+ result.set(tmp);
461
+ }
462
+ k >>= 1;
463
+ if (k) {
464
+ multiplyAffine(tmp, base, base);
465
+ base = new Float64Array(tmp);
466
+ }
467
+ }
468
+ out.set(result);
469
+ return out;
470
+ }
471
+
472
+ export function equals(a, b, eps = 1e-9) {
473
+ for (let i = 0; i < 16; i++) if (Math.abs(a[i] - b[i]) > eps * Math.max(1, Math.abs(a[i]), Math.abs(b[i]))) return false;
474
+ return true;
475
+ }
@@ -0,0 +1,30 @@
1
+ // Deterministic pseudo random numbers.
2
+ //
3
+ // ES3D procedural elements (<scatter>) must produce the same result in every
4
+ // implementation, so the generator is part of the format specification:
5
+ // mulberry32 seeded with the 32-bit unsigned integer `seed`, returning floats in
6
+ // [0, 1) as (state >>> 0) / 2^32 exactly as implemented below.
7
+
8
+ export function mulberry32(seed) {
9
+ let a = seed >>> 0;
10
+ return function next() {
11
+ a = (a + 0x6d2b79f5) >>> 0;
12
+ let t = a;
13
+ t = Math.imul(t ^ (t >>> 15), t | 1);
14
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
15
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
16
+ };
17
+ }
18
+
19
+ /** Uniform value in [lo, hi] for a range [lo, hi]. */
20
+ export function inRange(rand, range) {
21
+ return range[0] + (range[1] - range[0]) * rand();
22
+ }
23
+
24
+ /** Integer hash -> [0,1) (used for value noise in procedural textures). */
25
+ export function hash2(x, y, seed) {
26
+ let h = Math.imul(x | 0, 0x27d4eb2d) ^ Math.imul(y | 0, 0x165667b1) ^ Math.imul(seed | 0, 0x9e3779b9);
27
+ h = Math.imul(h ^ (h >>> 15), 0x85ebca6b);
28
+ h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
29
+ return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
30
+ }