@oeave/bakery3 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +562 -2
  3. package/dist/animation-B1h0Ryvj.d.ts +97 -0
  4. package/dist/bake/index.d.ts +369 -0
  5. package/dist/bake/index.js +9 -0
  6. package/dist/bake/index.js.map +1 -0
  7. package/dist/bake-DaOlenyx.d.ts +1357 -0
  8. package/dist/catalog/index.d.ts +100 -0
  9. package/dist/catalog/index.js +154 -0
  10. package/dist/catalog/index.js.map +1 -0
  11. package/dist/catalog.gen-BM-aNf7n.d.ts +733 -0
  12. package/dist/chunk-25ZXSVQ3.js +1107 -0
  13. package/dist/chunk-25ZXSVQ3.js.map +1 -0
  14. package/dist/chunk-3G5QL4F4.js +145 -0
  15. package/dist/chunk-3G5QL4F4.js.map +1 -0
  16. package/dist/chunk-4E5VV4QY.js +12 -0
  17. package/dist/chunk-4E5VV4QY.js.map +1 -0
  18. package/dist/chunk-54WJDN5R.js +3286 -0
  19. package/dist/chunk-54WJDN5R.js.map +1 -0
  20. package/dist/chunk-6NYR73Y7.js +832 -0
  21. package/dist/chunk-6NYR73Y7.js.map +1 -0
  22. package/dist/chunk-6YVQANMW.js +1463 -0
  23. package/dist/chunk-6YVQANMW.js.map +1 -0
  24. package/dist/chunk-AF5BIELI.js +70 -0
  25. package/dist/chunk-AF5BIELI.js.map +1 -0
  26. package/dist/chunk-APBOWWSV.js +92 -0
  27. package/dist/chunk-APBOWWSV.js.map +1 -0
  28. package/dist/chunk-ARMTTZXZ.js +1157 -0
  29. package/dist/chunk-ARMTTZXZ.js.map +1 -0
  30. package/dist/chunk-CX4WTVGH.js +4492 -0
  31. package/dist/chunk-CX4WTVGH.js.map +1 -0
  32. package/dist/chunk-HQTUFVMM.js +1412 -0
  33. package/dist/chunk-HQTUFVMM.js.map +1 -0
  34. package/dist/chunk-MD2U6S4N.js +1952 -0
  35. package/dist/chunk-MD2U6S4N.js.map +1 -0
  36. package/dist/chunk-S4AJTLLN.js +23 -0
  37. package/dist/chunk-S4AJTLLN.js.map +1 -0
  38. package/dist/chunk-TGFNQBKC.js +551 -0
  39. package/dist/chunk-TGFNQBKC.js.map +1 -0
  40. package/dist/chunk-V3QINEEG.js +87 -0
  41. package/dist/chunk-V3QINEEG.js.map +1 -0
  42. package/dist/chunk-W6WQ5UP7.js +91 -0
  43. package/dist/chunk-W6WQ5UP7.js.map +1 -0
  44. package/dist/chunk-WBIAFIHV.js +346 -0
  45. package/dist/chunk-WBIAFIHV.js.map +1 -0
  46. package/dist/chunk-ZEBVAIJJ.js +156 -0
  47. package/dist/chunk-ZEBVAIJJ.js.map +1 -0
  48. package/dist/devtools/index.d.ts +536 -0
  49. package/dist/devtools/index.js +15 -0
  50. package/dist/devtools/index.js.map +1 -0
  51. package/dist/environments/index.d.ts +93 -0
  52. package/dist/environments/index.js +382 -0
  53. package/dist/environments/index.js.map +1 -0
  54. package/dist/hotspots/index.d.ts +111 -0
  55. package/dist/hotspots/index.js +285 -0
  56. package/dist/hotspots/index.js.map +1 -0
  57. package/dist/index.d.ts +944 -0
  58. package/dist/index.js +15 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/node/index.cjs +5115 -0
  61. package/dist/node/index.cjs.map +1 -0
  62. package/dist/node/index.d.cts +4862 -0
  63. package/dist/node/index.d.ts +576 -0
  64. package/dist/node/index.js +1279 -0
  65. package/dist/node/index.js.map +1 -0
  66. package/dist/prepare-C5qVvaw4.d.ts +112 -0
  67. package/dist/presets/index.d.ts +526 -0
  68. package/dist/presets/index.js +14 -0
  69. package/dist/presets/index.js.map +1 -0
  70. package/dist/r3f/index.d.ts +159 -0
  71. package/dist/r3f/index.js +592 -0
  72. package/dist/r3f/index.js.map +1 -0
  73. package/dist/room-S5TLCK4F.js +9 -0
  74. package/dist/room-S5TLCK4F.js.map +1 -0
  75. package/dist/rooms.gen-vhj6sFW5.d.ts +1434 -0
  76. package/dist/session-YCQS4MRH.js +9 -0
  77. package/dist/session-YCQS4MRH.js.map +1 -0
  78. package/dist/shapes/index.d.ts +222 -0
  79. package/dist/shapes/index.js +836 -0
  80. package/dist/shapes/index.js.map +1 -0
  81. package/dist/testRun-BIPXaVPb.d.ts +1130 -0
  82. package/dist/timeline-ChwgD7bT.d.ts +470 -0
  83. package/dist/tsl/index.d.ts +165 -0
  84. package/dist/tsl/index.js +310 -0
  85. package/dist/tsl/index.js.map +1 -0
  86. package/package.json +170 -4
@@ -0,0 +1,4492 @@
1
+ import { captureCamera } from './chunk-V3QINEEG.js';
2
+ import { protocolVersionFor, SDK_VERSION, QUALITY } from './chunk-APBOWWSV.js';
3
+ import { BAKERY_NODE_MARKER, builtinFor, tslRuntimeRegistered, toNodeObject } from './chunk-S4AJTLLN.js';
4
+ import { findPresetRooms, sha256, environmentSourceFor, isPresetEnvironment, roomPresetSpecOf, roomPresetFactsOf, roomPresetAssetsOf } from './chunk-AF5BIELI.js';
5
+ import { idOf, shouldExclude, captureFog, readUserData, scenePath, captureLights, captureRuntime, stampIds, captureEnvironmentSettings, excludedItself, bakeCandidates, resolveProbe, defaultIncludeIds, quantizeBakeView, bakeBounds, isBakeWrittenUv } from './chunk-HQTUFVMM.js';
6
+ import { MAX_BAKE_OBJECTS, MAX_BAKE_TEXELS, roomIsPrebaked, roomPrebakeChanges, MAX_BAKE_SIMPLIFY, BAKE_TEXTURE_SIZES, MAX_BAKE_SIMPLIFY_TRIANGLES, BAKE_ATLAS_SIZES } from './chunk-W6WQ5UP7.js';
7
+ import { Bakery3Error } from './chunk-3G5QL4F4.js';
8
+ import { Vector3, Vector2, Matrix4, Quaternion } from 'three';
9
+ import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
10
+
11
+ // src/timeline/easing.ts
12
+ var NAMED = {
13
+ "ease-in": [0.42, 0, 1, 1],
14
+ "ease-out": [0, 0, 0.58, 1],
15
+ "ease-in-out": [0.42, 0, 0.58, 1]
16
+ };
17
+ function ease(easing, t) {
18
+ const clamped = t <= 0 ? 0 : t >= 1 ? 1 : t;
19
+ if (!easing || easing === "linear") return clamped;
20
+ const points = Array.isArray(easing) ? easing : NAMED[easing];
21
+ if (!points) return clamped;
22
+ return cubicBezier(points[0], points[1], points[2], points[3], clamped);
23
+ }
24
+ function cubicBezier(x1, y1, x2, y2, t) {
25
+ if (x1 === y1 && x2 === y2) return t;
26
+ const s = solveForX(x1, x2, t);
27
+ return bezier(y1, y2, s);
28
+ }
29
+ function bezier(a, b, s) {
30
+ const inv = 1 - s;
31
+ return 3 * inv * inv * s * a + 3 * inv * s * s * b + s * s * s;
32
+ }
33
+ function slope(a, b, s) {
34
+ const inv = 1 - s;
35
+ return 3 * inv * inv * a + 6 * inv * s * (b - a) + 3 * s * s * (1 - b);
36
+ }
37
+ function solveForX(x1, x2, target) {
38
+ let s = target;
39
+ for (let i = 0; i < 8; i++) {
40
+ const error = bezier(x1, x2, s) - target;
41
+ if (Math.abs(error) < 1e-6) return s;
42
+ const derivative = slope(x1, x2, s);
43
+ if (Math.abs(derivative) < 1e-6) break;
44
+ s -= error / derivative;
45
+ }
46
+ let low = 0;
47
+ let high = 1;
48
+ s = target;
49
+ for (let i = 0; i < 24; i++) {
50
+ const x = bezier(x1, x2, s);
51
+ if (Math.abs(x - target) < 1e-6) return s;
52
+ if (x > target) high = s;
53
+ else low = s;
54
+ s = (low + high) / 2;
55
+ }
56
+ return s;
57
+ }
58
+
59
+ // src/timeline/orbit.ts
60
+ function toSpherical(position, center) {
61
+ const dx = position[0] - center[0];
62
+ const dy = position[1] - center[1];
63
+ const dz = position[2] - center[2];
64
+ const radius = Math.hypot(dx, dy, dz);
65
+ if (radius < 1e-9) return { radius: 0, yaw: 0, pitch: 0 };
66
+ return {
67
+ radius,
68
+ yaw: Math.atan2(dx, dz),
69
+ // Clamped before asin: a normalized component can land at 1.0000000002.
70
+ pitch: Math.asin(Math.max(-1, Math.min(1, dy / radius)))
71
+ };
72
+ }
73
+ function fromSpherical(spherical, center) {
74
+ const { radius, yaw, pitch } = spherical;
75
+ const horizontal = radius * Math.cos(pitch);
76
+ return [
77
+ center[0] + horizontal * Math.sin(yaw),
78
+ center[1] + radius * Math.sin(pitch),
79
+ center[2] + horizontal * Math.cos(yaw)
80
+ ];
81
+ }
82
+ function orbitAt(from, to, center, t) {
83
+ return fromSpherical(
84
+ {
85
+ radius: from.radius + (to.radius - from.radius) * t,
86
+ yaw: from.yaw + (to.yaw - from.yaw) * t,
87
+ pitch: from.pitch + (to.pitch - from.pitch) * t
88
+ },
89
+ center
90
+ );
91
+ }
92
+ function lookAtQuaternion(eye, target, up = [0, 1, 0]) {
93
+ let z = normalize([
94
+ eye[0] - target[0],
95
+ eye[1] - target[1],
96
+ eye[2] - target[2]
97
+ ]);
98
+ if (lengthOf(z) === 0) z = [0, 0, 1];
99
+ let x = cross(up, z);
100
+ if (lengthOf(x) === 0) {
101
+ if (Math.abs(up[2]) === 1) z[0] += 1e-4;
102
+ else z[2] += 1e-4;
103
+ z = normalize(z);
104
+ x = cross(up, z);
105
+ }
106
+ x = normalize(x);
107
+ const y = cross(z, x);
108
+ return matrixToQuaternion(x, y, z);
109
+ }
110
+ function matrixToQuaternion(x, y, z) {
111
+ const m00 = x[0];
112
+ const m10 = x[1];
113
+ const m20 = x[2];
114
+ const m01 = y[0];
115
+ const m11 = y[1];
116
+ const m21 = y[2];
117
+ const m02 = z[0];
118
+ const m12 = z[1];
119
+ const m22 = z[2];
120
+ const trace = m00 + m11 + m22;
121
+ if (trace > 0) {
122
+ const s2 = 0.5 / Math.sqrt(trace + 1);
123
+ return [(m21 - m12) * s2, (m02 - m20) * s2, (m10 - m01) * s2, 0.25 / s2];
124
+ }
125
+ if (m00 > m11 && m00 > m22) {
126
+ const s2 = 2 * Math.sqrt(1 + m00 - m11 - m22);
127
+ return [0.25 * s2, (m01 + m10) / s2, (m02 + m20) / s2, (m21 - m12) / s2];
128
+ }
129
+ if (m11 > m22) {
130
+ const s2 = 2 * Math.sqrt(1 + m11 - m00 - m22);
131
+ return [(m01 + m10) / s2, 0.25 * s2, (m12 + m21) / s2, (m02 - m20) / s2];
132
+ }
133
+ const s = 2 * Math.sqrt(1 + m22 - m00 - m11);
134
+ return [(m02 + m20) / s, (m12 + m21) / s, 0.25 * s, (m10 - m01) / s];
135
+ }
136
+ function eulerToQuaternion(euler) {
137
+ const [x, y, z] = euler;
138
+ const c1 = Math.cos(x / 2);
139
+ const c2 = Math.cos(y / 2);
140
+ const c3 = Math.cos(z / 2);
141
+ const s1 = Math.sin(x / 2);
142
+ const s2 = Math.sin(y / 2);
143
+ const s3 = Math.sin(z / 2);
144
+ return [
145
+ s1 * c2 * c3 + c1 * s2 * s3,
146
+ c1 * s2 * c3 - s1 * c2 * s3,
147
+ c1 * c2 * s3 + s1 * s2 * c3,
148
+ c1 * c2 * c3 - s1 * s2 * s3
149
+ ];
150
+ }
151
+ function cross(a, b) {
152
+ return [
153
+ a[1] * b[2] - a[2] * b[1],
154
+ a[2] * b[0] - a[0] * b[2],
155
+ a[0] * b[1] - a[1] * b[0]
156
+ ];
157
+ }
158
+ function lengthOf(v) {
159
+ return Math.hypot(v[0], v[1], v[2]);
160
+ }
161
+ function normalize(v) {
162
+ const length = lengthOf(v);
163
+ return length === 0 ? [0, 0, 0] : [v[0] / length, v[1] / length, v[2] / length];
164
+ }
165
+
166
+ // src/timeline/properties.ts
167
+ function hemisphereOf(object) {
168
+ const light = object;
169
+ return light.isHemisphereLight ? light : void 0;
170
+ }
171
+ function sceneOf(object) {
172
+ const scene = object;
173
+ return scene.isScene ? scene : void 0;
174
+ }
175
+ function aimedLight(object) {
176
+ const light = object;
177
+ return (light.isDirectionalLight || light.isSpotLight) && light.target ? light : void 0;
178
+ }
179
+ function materialOf(object) {
180
+ const material = object.material;
181
+ if (!material) return void 0;
182
+ return Array.isArray(material) ? material[0] : material;
183
+ }
184
+ function readVector(v) {
185
+ return v ? [v.x, v.y, v.z] : void 0;
186
+ }
187
+ function scalar(get, set) {
188
+ return {
189
+ size: 1,
190
+ read(object) {
191
+ const material = materialOf(object);
192
+ const value = material ? get(material) : void 0;
193
+ return typeof value === "number" ? [value] : void 0;
194
+ },
195
+ write(object, value) {
196
+ const material = materialOf(object);
197
+ if (material) set(material, value[0]);
198
+ }
199
+ };
200
+ }
201
+ function colorProperty(pick) {
202
+ return {
203
+ size: 3,
204
+ color: true,
205
+ read(object) {
206
+ const target = pick(object);
207
+ return target ? [target.r, target.g, target.b] : void 0;
208
+ },
209
+ write(object, value) {
210
+ const target = pick(object);
211
+ if (!target) return;
212
+ if (target.setRGB) target.setRGB(value[0], value[1], value[2]);
213
+ else Object.assign(target, { r: value[0], g: value[1], b: value[2] });
214
+ }
215
+ };
216
+ }
217
+ var OBJECT_PROPERTIES = {
218
+ position: {
219
+ size: 3,
220
+ read: (object) => readVector(object.position),
221
+ write: (object, value) => object.position.set(value[0], value[1], value[2])
222
+ },
223
+ rotation: {
224
+ size: 3,
225
+ channel: "quaternion",
226
+ read: (object) => readVector(
227
+ object.rotation
228
+ ),
229
+ write: (object, value) => object.rotation.set(value[0], value[1], value[2])
230
+ },
231
+ scale: {
232
+ size: 3,
233
+ read: (object) => readVector(object.scale),
234
+ write: (object, value) => object.scale.set(value[0], value[1], value[2])
235
+ },
236
+ // A light's own color (the worker drives the lamp, or for an ambient or
237
+ // hemisphere light the dome), and a mesh's material's.
238
+ color: colorProperty(
239
+ (object) => object.isLight ? object.color : materialOf(object)?.color
240
+ ),
241
+ emissive: colorProperty((object) => materialOf(object)?.emissive),
242
+ emissiveIntensity: scalar(
243
+ (material) => material.emissiveIntensity,
244
+ (material, value) => {
245
+ material.emissiveIntensity = value;
246
+ }
247
+ ),
248
+ roughness: scalar(
249
+ (material) => material.roughness,
250
+ (material, value) => {
251
+ material.roughness = value;
252
+ }
253
+ ),
254
+ metalness: scalar(
255
+ (material) => material.metalness,
256
+ (material, value) => {
257
+ material.metalness = value;
258
+ }
259
+ ),
260
+ opacity: scalar(
261
+ (material) => material.opacity,
262
+ (material, value) => {
263
+ material.opacity = value;
264
+ if (value < 1 && !material.transparent) {
265
+ material.transparent = true;
266
+ material.needsUpdate = true;
267
+ }
268
+ }
269
+ ),
270
+ intensity: {
271
+ size: 1,
272
+ read(object) {
273
+ const light = object;
274
+ return light.isLight && typeof light.intensity === "number" ? [light.intensity] : void 0;
275
+ },
276
+ write(object, value) {
277
+ const light = object;
278
+ if (light.isLight) light.intensity = value[0];
279
+ }
280
+ },
281
+ /** A HemisphereLight's color from below. Its `color` is the sky. */
282
+ groundColor: {
283
+ ...colorProperty((object) => hemisphereOf(object)?.groundColor),
284
+ appliesTo: (object) => hemisphereOf(object) !== void 0
285
+ },
286
+ /**
287
+ * The scene's own `environmentIntensity`, on the Scene as a target: the
288
+ * environment's `intensity` channel. It scales the HDRI only, as in three.
289
+ */
290
+ environmentIntensity: {
291
+ size: 1,
292
+ appliesTo: (object) => sceneOf(object) !== void 0,
293
+ read(object) {
294
+ const value = sceneOf(object)?.environmentIntensity;
295
+ return typeof value === "number" ? [value] : void 0;
296
+ },
297
+ write(object, value) {
298
+ const scene = sceneOf(object);
299
+ if (scene) scene.environmentIntensity = value[0];
300
+ }
301
+ },
302
+ /**
303
+ * The scene's `environmentRotation`, radians, XYZ, as three holds it: the
304
+ * environment's `rotation` channel. Unlike an object's rotation it stays
305
+ * an euler on the wire, because that is what the environment carries.
306
+ */
307
+ environmentRotation: {
308
+ size: 3,
309
+ appliesTo: (object) => sceneOf(object) !== void 0,
310
+ read: (object) => readVector(sceneOf(object)?.environmentRotation),
311
+ write(object, value) {
312
+ sceneOf(object)?.environmentRotation?.set(
313
+ value[0],
314
+ value[1],
315
+ value[2]
316
+ );
317
+ }
318
+ },
319
+ /**
320
+ * Where a directional or spot light aims: its `target`'s WORLD position,
321
+ * which is what three aims it at and what the channel carries. Written
322
+ * back through the target's parent, if it has one, so the light points at
323
+ * the same place in the viewport as in the render.
324
+ */
325
+ target: {
326
+ size: 3,
327
+ appliesTo: (object) => aimedLight(object) !== void 0,
328
+ read(object) {
329
+ const target = aimedLight(object)?.target;
330
+ if (!target) return void 0;
331
+ target.updateMatrixWorld();
332
+ const e = target.matrixWorld.elements;
333
+ return [e[12], e[13], e[14]];
334
+ },
335
+ write(object, value) {
336
+ const target = aimedLight(object)?.target;
337
+ if (!target) return;
338
+ target.position.set(value[0], value[1], value[2]);
339
+ target.parent?.worldToLocal(target.position);
340
+ target.updateMatrixWorld();
341
+ }
342
+ }
343
+ };
344
+ var CAMERA_PROPERTIES = {
345
+ position: OBJECT_PROPERTIES.position,
346
+ lookAt: {
347
+ size: 3,
348
+ // There is no live "lookAt" to read; the timeline supplies its own center
349
+ // when a keyframe is captured. Returning undefined keeps the panel from
350
+ // offering a capture button that would silently record the wrong thing.
351
+ read: () => void 0,
352
+ // Applied together with position by the timeline, never alone: aiming at a
353
+ // point before moving to the position it was computed for produces one
354
+ // frame of visible swing.
355
+ write: () => void 0
356
+ },
357
+ fov: {
358
+ size: 1,
359
+ read(object) {
360
+ const camera = object;
361
+ return typeof camera.fov === "number" ? [camera.fov] : void 0;
362
+ },
363
+ write(object, value) {
364
+ const camera = object;
365
+ if (typeof camera.fov !== "number") return;
366
+ camera.fov = value[0];
367
+ camera.updateProjectionMatrix?.();
368
+ }
369
+ }
370
+ };
371
+ function propertyDefinition(target, property) {
372
+ return target === "camera" ? CAMERA_PROPERTIES[property] : OBJECT_PROPERTIES[property];
373
+ }
374
+ function readCameraLookAt(camera, center) {
375
+ camera.updateMatrixWorld();
376
+ const e = camera.matrixWorld.elements;
377
+ const position = [e[12], e[13], e[14]];
378
+ const forward = normalize2([-e[8], -e[9], -e[10]]);
379
+ const distance = Math.hypot(
380
+ position[0] - center[0],
381
+ position[1] - center[1],
382
+ position[2] - center[2]
383
+ ) || 1;
384
+ return [
385
+ position[0] + forward[0] * distance,
386
+ position[1] + forward[1] * distance,
387
+ position[2] + forward[2] * distance
388
+ ];
389
+ }
390
+ function normalize2(v) {
391
+ const length = Math.hypot(v[0], v[1], v[2]);
392
+ return length === 0 ? [0, 0, -1] : [v[0] / length, v[1] / length, v[2] / length];
393
+ }
394
+ function hexToLinear(hex) {
395
+ const value = hex.trim().replace(/^#/, "");
396
+ const full = value.length === 3 ? value.split("").map((c) => c + c).join("") : value;
397
+ const int = Number.parseInt(full, 16);
398
+ if (!Number.isFinite(int)) return [1, 1, 1];
399
+ return [
400
+ srgbToLinear((int >> 16 & 255) / 255),
401
+ srgbToLinear((int >> 8 & 255) / 255),
402
+ srgbToLinear((int & 255) / 255)
403
+ ];
404
+ }
405
+ function srgbToLinear(c) {
406
+ return c < 0.04045 ? c * 0.0773993808 : Math.pow(c * 0.9478672986 + 0.0521327014, 2.4);
407
+ }
408
+ function linearToSrgbHex(value) {
409
+ const channel = (v) => {
410
+ const srgb = v < 31308e-7 ? v * 12.92 : 1.055 * Math.pow(Math.max(v, 0), 0.41666) - 0.055;
411
+ return Math.round(Math.max(0, Math.min(1, srgb)) * 255).toString(16).padStart(2, "0");
412
+ };
413
+ return `#${channel(value[0] ?? 0)}${channel(value[1] ?? 0)}${channel(value[2] ?? 0)}`;
414
+ }
415
+
416
+ // src/protocol/animation.ts
417
+ var ENVIRONMENT_CHANNEL_SIZES = {
418
+ intensity: 1,
419
+ rotation: 3,
420
+ ambientIntensity: 1,
421
+ ambientColor: 3
422
+ };
423
+ var HEMISPHERE_CHANNEL_PROPERTIES = [
424
+ "intensity",
425
+ "color",
426
+ "groundColor"
427
+ ];
428
+ var WORLD_CHECK_FRAMES = 5;
429
+ var ANIMATION_CHANNEL_SIZES = {
430
+ position: 3,
431
+ quaternion: 4,
432
+ scale: 3,
433
+ color: 3,
434
+ emissive: 3,
435
+ fov: 1,
436
+ emissiveIntensity: 1,
437
+ roughness: 1,
438
+ metalness: 1,
439
+ opacity: 1,
440
+ intensity: 1,
441
+ target: 3,
442
+ groundColor: 3
443
+ };
444
+ var MAX_ANIMATION_FRAMES = 1800;
445
+
446
+ // src/protocol/shader.ts
447
+ var SHADER_GRAPH_VERSION = 1;
448
+ var SHADER_OPS = {
449
+ // ---- values ------------------------------------------------------------
450
+ /** params: { value: number | number[], type } */
451
+ const: { tier: "exact" },
452
+ /**
453
+ * A uniform, baked to the value it held at capture. A still render is a
454
+ * snapshot; an animated uniform renders the moment the button was pressed.
455
+ * params: { value, type, name? }
456
+ */
457
+ uniform: { tier: "exact" },
458
+ // ---- geometry inputs ---------------------------------------------------
459
+ /** params: { index }. TEXCOORD_<index> of the mesh being shaded. */
460
+ uv: { tier: "exact" },
461
+ /** COLOR_0, as the mesh carries it. */
462
+ vertex_color: { tier: "exact" },
463
+ /** params: { space: 'local' | 'world' | 'view' } */
464
+ position: {
465
+ tier: "exact"
466
+ },
467
+ /**
468
+ * params: { space: 'world' | 'view' | 'local' }. View- and local-space
469
+ * normals are rebuilt from the renderer's world-space normal;
470
+ * rotation-invariant uses (dot products, fresnel) match exactly, raw
471
+ * components differ.
472
+ */
473
+ normal: { tier: "exact" },
474
+ /**
475
+ * Direction from the shaded point toward the camera, in view space. The
476
+ * worker rebuilds three's exact frame.
477
+ */
478
+ view_direction: { tier: "exact" },
479
+ /** The world-space camera location. */
480
+ camera_position: { tier: "exact" },
481
+ // ---- textures ----------------------------------------------------------
482
+ /**
483
+ * params: { image: 'sha256:…', colorSpace: 'srgb' | 'linear',
484
+ * wrap: 'repeat' | 'clamp' | 'mirror', filter: 'linear' | 'nearest' }
485
+ * inputs: [uv?]. Sampled at TEXCOORD_0 when the input is omitted.
486
+ */
487
+ texture: { tier: "exact" },
488
+ /**
489
+ * Tangent-space normal mapping. inputs: [textureColor, scale?].
490
+ */
491
+ normal_map: { tier: "exact" },
492
+ /** Bump from a height value. inputs: [height, scale?]. */
493
+ bump_map: {
494
+ tier: "approximate",
495
+ note: "three.js bump-maps with screen-space derivatives, the renderer with surface derivatives. The relief reads the same; the exact pixels differ."
496
+ },
497
+ // ---- arithmetic --------------------------------------------------------
498
+ add: { tier: "exact" },
499
+ sub: { tier: "exact" },
500
+ mul: { tier: "exact" },
501
+ div: { tier: "exact" },
502
+ mod: { tier: "exact" },
503
+ pow: { tier: "exact" },
504
+ min: { tier: "exact" },
505
+ max: { tier: "exact" },
506
+ abs: { tier: "exact" },
507
+ sign: { tier: "exact" },
508
+ floor: { tier: "exact" },
509
+ ceil: { tier: "exact" },
510
+ round: { tier: "exact" },
511
+ trunc: { tier: "exact" },
512
+ fract: { tier: "exact" },
513
+ sqrt: { tier: "exact" },
514
+ inverse_sqrt: { tier: "exact" },
515
+ exp: { tier: "exact" },
516
+ exp2: { tier: "exact" },
517
+ log: { tier: "exact" },
518
+ log2: { tier: "exact" },
519
+ sin: { tier: "exact" },
520
+ cos: { tier: "exact" },
521
+ tan: { tier: "exact" },
522
+ asin: { tier: "exact" },
523
+ acos: { tier: "exact" },
524
+ atan: { tier: "exact" },
525
+ /** inputs: [y, x], like GLSL's two-argument atan. */
526
+ atan2: { tier: "exact" },
527
+ radians: { tier: "exact" },
528
+ degrees: { tier: "exact" },
529
+ negate: { tier: "exact" },
530
+ one_minus: { tier: "exact" },
531
+ reciprocal: { tier: "exact" },
532
+ saturate: { tier: "exact" },
533
+ clamp: { tier: "exact" },
534
+ /** inputs: [a, b, t]. three's `mix`, the same lerp on both sides. */
535
+ mix: { tier: "exact" },
536
+ step: { tier: "exact" },
537
+ smoothstep: { tier: "exact" },
538
+ /** inputs: [value, inLow, inHigh, outLow, outHigh]; params: { clamp } */
539
+ remap: { tier: "exact" },
540
+ // ---- vectors -----------------------------------------------------------
541
+ dot: { tier: "exact" },
542
+ cross: { tier: "exact" },
543
+ length: { tier: "exact" },
544
+ distance: { tier: "exact" },
545
+ normalize: { tier: "exact" },
546
+ reflect: { tier: "exact" },
547
+ refract: { tier: "exact" },
548
+ faceforward: { tier: "exact" },
549
+ /** params: { components: 'x' | 'xy' | 'zyx' | … } */
550
+ swizzle: { tier: "exact" },
551
+ /** inputs: the components, in order. Two to four of them. */
552
+ join: { tier: "exact" },
553
+ /** params: { to: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int' | 'uint' | 'bool' } */
554
+ convert: { tier: "exact" },
555
+ /** Rotate a vector about an axis. inputs: [vector, axis, angle]. */
556
+ rotate: { tier: "exact" },
557
+ // ---- comparison and choice --------------------------------------------
558
+ less_than: { tier: "exact" },
559
+ greater_than: { tier: "exact" },
560
+ less_equal: { tier: "exact" },
561
+ greater_equal: { tier: "exact" },
562
+ equal: {
563
+ tier: "approximate",
564
+ note: "Float equality is compared exactly on both sides, but GPU rounding differs between the browser and the renderer, so equality on computed values may flip near the boundary."
565
+ },
566
+ not_equal: {
567
+ tier: "approximate",
568
+ note: "Float equality is compared exactly on both sides, but GPU rounding differs between the browser and the renderer, so equality on computed values may flip near the boundary."
569
+ },
570
+ and: { tier: "exact" },
571
+ or: { tier: "exact" },
572
+ not: { tier: "exact" },
573
+ /** inputs: [condition, ifTrue, ifFalse]. */
574
+ select: { tier: "exact" },
575
+ // ---- color ------------------------------------------------------------
576
+ /**
577
+ * Luminance and color-space ops use Rec.709 weights on both sides.
578
+ */
579
+ luminance: { tier: "exact" },
580
+ srgb_to_linear: {
581
+ tier: "approximate",
582
+ note: "The renderer applies a gamma-2.2 curve rather than the piecewise sRGB transfer function; the toe below 0.04 differs slightly."
583
+ },
584
+ linear_to_srgb: {
585
+ tier: "approximate",
586
+ note: "The renderer applies a gamma-2.2 curve rather than the piecewise sRGB transfer function; the toe below 0.04 differs slightly."
587
+ },
588
+ // ---- procedural (three.js implementations, native equivalents) ---------
589
+ /**
590
+ * three's `checker()`: a 2×2 tile per UV unit.
591
+ */
592
+ checker: {
593
+ tier: "exact"
594
+ },
595
+ /**
596
+ * MaterialX Perlin noise, rendered through the renderer's own
597
+ * Perlin-derived noise. The lattices differ: the look matches, the exact
598
+ * pattern does not. params: { dimensions: 2 | 3 }
599
+ */
600
+ mx_noise: {
601
+ tier: "approximate",
602
+ note: "MaterialX Perlin noise renders through the renderer's own Perlin-based noise. The character matches; the exact pattern placement does not."
603
+ },
604
+ /** params: { octaves, lacunarity, diminish }. Fractal (fBm) noise. */
605
+ mx_fractal_noise: {
606
+ tier: "approximate",
607
+ note: "MaterialX fractal noise renders through the renderer's own noise with matching octaves. The character matches; the exact pattern placement does not."
608
+ },
609
+ /**
610
+ * Worley/cellular noise, as three calls it: SQUARED distances, to the
611
+ * nearest point, or with `vector` to the nearest `features` (2 or 3).
612
+ * inputs: [coordinate, jitter]; params: { vector?, features?, dimensions? },
613
+ * `dimensions: 2` when the coordinate is 2D, which three runs as 2D noise.
614
+ */
615
+ mx_worley_noise: {
616
+ tier: "approximate",
617
+ note: "MaterialX Worley noise renders through the renderer's cellular noise. Cell shapes match in character, not placement."
618
+ },
619
+ /** Constant pseudo-random value per integer cell. */
620
+ mx_cell_noise: {
621
+ tier: "approximate",
622
+ note: "MaterialX cell noise renders as a hash over the floored coordinate. Statistically identical, per-cell values differ."
623
+ },
624
+ // ---- Bakery-only (from @oeave/bakery3/tsl) -------------------------------
625
+ /**
626
+ * These exist because the renderer has procedural textures and shading
627
+ * inputs the browser cannot compute. The SDK exposes them as TSL nodes
628
+ * that preview approximately in WebGPU and serialize exactly here: the
629
+ * render is the real one, the viewport is the sketch.
630
+ */
631
+ /** params: { dimensions, scale, detail, roughness, lacunarity, distortion } */
632
+ bk_noise_texture: { tier: "bakery" },
633
+ /** params: { dimensions, feature, metric, scale, randomness, exponent } */
634
+ bk_voronoi_texture: { tier: "bakery" },
635
+ /** params: { type, bands_direction, rings_direction, profile, scale,
636
+ * distortion, detail, detail_scale, detail_roughness, phase } */
637
+ bk_wave_texture: { tier: "bakery" },
638
+ /** params: { depth, scale, distortion } */
639
+ bk_magic_texture: { tier: "bakery" },
640
+ /** params: { gradient_type } */
641
+ bk_gradient_texture: { tier: "bakery" },
642
+ /** params: { offset, frequency, squash, …brick geometry } */
643
+ bk_brick_texture: { tier: "bakery" },
644
+ /** params: { dimensions }. A hash of the input coordinate. */
645
+ bk_white_noise: { tier: "bakery" },
646
+ /** Traced ambient occlusion. params: { distance, samples, only_local } */
647
+ bk_ambient_occlusion: { tier: "bakery" },
648
+ /** Rounds geometry edges at shading time. params: { radius, samples } */
649
+ bk_bevel: { tier: "bakery" },
650
+ /** Curvature of the surface: concave 0, flat 0.5, convex 1. */
651
+ bk_pointiness: { tier: "bakery" },
652
+ /** 1 on triangle edges, 0 elsewhere. params: { size, pixel_size } */
653
+ bk_wireframe: { tier: "bakery" },
654
+ /** Physically correct fresnel. inputs: [ior]. */
655
+ bk_fresnel: { tier: "bakery" },
656
+ /** Facing/fresnel layer weight. inputs: [blend]; params: { output } */
657
+ bk_layer_weight: { tier: "bakery" },
658
+ /**
659
+ * A color ramp. inputs: [fac];
660
+ * params: { interpolation, stops: [pos, r, g, b, a, …] flat }
661
+ */
662
+ bk_color_ramp: { tier: "bakery" },
663
+ /** Blackbody temperature to color. inputs: [kelvin]. */
664
+ bk_blackbody: { tier: "bakery" },
665
+ /** params: { hue, saturation, value, fac } as defaults; inputs override. */
666
+ bk_hsv: { tier: "bakery" },
667
+ /** Brightness/contrast. inputs: [color, bright, contrast]. */
668
+ bk_brightness_contrast: { tier: "bakery" },
669
+ /** A gamma curve. inputs: [color, gamma]. */
670
+ bk_gamma: { tier: "bakery" }
671
+ };
672
+
673
+ // src/protocol/complexity.ts
674
+ var ENCLOSURE_RAYS = 128;
675
+ var ENCLOSURE_REACH = 60;
676
+ function skyDirections(count) {
677
+ const golden = Math.PI * (3 - Math.sqrt(5));
678
+ const lowest = Math.sin(2 * Math.PI / 180);
679
+ const out = [];
680
+ for (let i = 0; i < count; i++) {
681
+ const y = lowest + (1 - lowest) * ((i + 0.5) / count);
682
+ const r = Math.sqrt(1 - y * y);
683
+ const a = i * golden;
684
+ out.push([Math.cos(a) * r, y, Math.sin(a) * r]);
685
+ }
686
+ return out;
687
+ }
688
+ function vertex(mesh, v, out, at) {
689
+ const p = mesh.positions;
690
+ const m = mesh.matrix;
691
+ const x = p[v * 3], y = p[v * 3 + 1], z = p[v * 3 + 2];
692
+ if (!m) {
693
+ out[at] = x;
694
+ out[at + 1] = y;
695
+ out[at + 2] = z;
696
+ return;
697
+ }
698
+ out[at] = m[0] * x + m[4] * y + m[8] * z + m[12];
699
+ out[at + 1] = m[1] * x + m[5] * y + m[9] * z + m[13];
700
+ out[at + 2] = m[2] * x + m[6] * y + m[10] * z + m[14];
701
+ }
702
+ function prepare(mesh) {
703
+ const p = mesh.positions;
704
+ if (p.length < 9) return null;
705
+ const lo = [Infinity, Infinity, Infinity];
706
+ const hi = [-Infinity, -Infinity, -Infinity];
707
+ for (let i = 0; i < p.length; i += 3) {
708
+ for (let k = 0; k < 3; k++) {
709
+ const value = p[i + k];
710
+ if (value < lo[k]) lo[k] = value;
711
+ if (value > hi[k]) hi[k] = value;
712
+ }
713
+ }
714
+ const min = [Infinity, Infinity, Infinity];
715
+ const max = [-Infinity, -Infinity, -Infinity];
716
+ const corner = [0, 0, 0];
717
+ const probe = {
718
+ positions: [0, 0, 0],
719
+ matrix: mesh.matrix ?? null
720
+ };
721
+ for (let c = 0; c < 8; c++) {
722
+ probe.positions[0] = c & 1 ? hi[0] : lo[0];
723
+ probe.positions[1] = c & 2 ? hi[1] : lo[1];
724
+ probe.positions[2] = c & 4 ? hi[2] : lo[2];
725
+ vertex(probe, 0, corner, 0);
726
+ for (let k = 0; k < 3; k++) {
727
+ if (corner[k] < min[k]) min[k] = corner[k];
728
+ if (corner[k] > max[k]) max[k] = corner[k];
729
+ }
730
+ }
731
+ return { mesh, min, max };
732
+ }
733
+ function boxEntry(o, d, min, max) {
734
+ let near = 0;
735
+ let far = ENCLOSURE_REACH;
736
+ for (let k = 0; k < 3; k++) {
737
+ const lo = min[k] - 1e-4;
738
+ const hi = max[k] + 1e-4;
739
+ if (Math.abs(d[k]) < 1e-12) {
740
+ if (o[k] < lo || o[k] > hi) return Infinity;
741
+ continue;
742
+ }
743
+ let t1 = (lo - o[k]) / d[k];
744
+ let t2 = (hi - o[k]) / d[k];
745
+ if (t1 > t2) [t1, t2] = [t2, t1];
746
+ if (t1 > near) near = t1;
747
+ if (t2 < far) far = t2;
748
+ if (near > far) return Infinity;
749
+ }
750
+ return near;
751
+ }
752
+ function hitsMesh(o, d, mesh) {
753
+ const index = mesh.indices;
754
+ const count = index ? index.length : mesh.positions.length / 3;
755
+ const t = [0, 0, 0, 0, 0, 0, 0, 0, 0];
756
+ for (let i = 0; i + 2 < count; i += 3) {
757
+ vertex(mesh, index ? index[i] : i, t, 0);
758
+ vertex(mesh, index ? index[i + 1] : i + 1, t, 3);
759
+ vertex(mesh, index ? index[i + 2] : i + 2, t, 6);
760
+ const e1x = t[3] - t[0], e1y = t[4] - t[1], e1z = t[5] - t[2];
761
+ const e2x = t[6] - t[0], e2y = t[7] - t[1], e2z = t[8] - t[2];
762
+ const px = d[1] * e2z - d[2] * e2y, py = d[2] * e2x - d[0] * e2z, pz = d[0] * e2y - d[1] * e2x;
763
+ const det = e1x * px + e1y * py + e1z * pz;
764
+ if (Math.abs(det) < 1e-12) continue;
765
+ const inv = 1 / det;
766
+ const sx = o[0] - t[0], sy = o[1] - t[1], sz = o[2] - t[2];
767
+ const u = (sx * px + sy * py + sz * pz) * inv;
768
+ if (u < 0 || u > 1) continue;
769
+ const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
770
+ const v = (d[0] * qx + d[1] * qy + d[2] * qz) * inv;
771
+ if (v < 0 || u + v > 1) continue;
772
+ const distance = (e2x * qx + e2y * qy + e2z * qz) * inv;
773
+ if (distance > 1e-4 && distance < ENCLOSURE_REACH) return true;
774
+ }
775
+ return false;
776
+ }
777
+ function enclosureOf(meshes, eye, rays = ENCLOSURE_RAYS) {
778
+ const prepared = meshes.filter((mesh) => !mesh.emitter).map(prepare).filter((mesh) => mesh !== null);
779
+ if (!prepared.length) return 0;
780
+ let hidden = 0;
781
+ for (const d of skyDirections(rays)) {
782
+ const order = prepared.map((candidate) => ({
783
+ mesh: candidate.mesh,
784
+ entry: boxEntry(eye, d, candidate.min, candidate.max)
785
+ })).filter((candidate) => candidate.entry < ENCLOSURE_REACH).sort((a, b) => a.entry - b.entry);
786
+ if (order.some(({ mesh }) => hitsMesh(eye, d, mesh))) hidden++;
787
+ }
788
+ return hidden / rays;
789
+ }
790
+ function complexityOf(meshes, eye, objects = meshes.length) {
791
+ return { enclosure: round2(enclosureOf(meshes, eye)), objects };
792
+ }
793
+ function round2(value) {
794
+ return Math.round(value * 100) / 100;
795
+ }
796
+
797
+ // src/capture/ambient.ts
798
+ function ambientLightsIn(root) {
799
+ const lights = [];
800
+ root.traverse((object) => {
801
+ const light = object;
802
+ if (!light.isAmbientLight || !light.visible && !readUserData(object).renderOnly)
803
+ return;
804
+ lights.push(light);
805
+ });
806
+ return lights;
807
+ }
808
+ function combineAmbient(terms) {
809
+ if (terms.length === 1) {
810
+ const [only] = terms;
811
+ return only.intensity > 0 ? { color: [...only.color], intensity: only.intensity } : void 0;
812
+ }
813
+ const sum = [0, 0, 0];
814
+ for (const term of terms) {
815
+ for (let c = 0; c < 3; c++) sum[c] += term.color[c] * term.intensity;
816
+ }
817
+ const intensity = Math.max(...sum);
818
+ if (!(intensity > 0)) return void 0;
819
+ return {
820
+ color: [sum[0] / intensity, sum[1] / intensity, sum[2] / intensity],
821
+ intensity
822
+ };
823
+ }
824
+ function ambientTermOf(light) {
825
+ const color = light.color;
826
+ return {
827
+ color: color ? [color.r, color.g, color.b] : [1, 1, 1],
828
+ intensity: light.intensity ?? 0
829
+ };
830
+ }
831
+ function capturedAmbient(root) {
832
+ const lights = ambientLightsIn(root);
833
+ const term = combineAmbient(lights.map(ambientTermOf));
834
+ if (!term) return void 0;
835
+ if (lights.length === 1) {
836
+ const color2 = lights[0].color;
837
+ return {
838
+ color: color2 ? `#${color2.getHexString()}` : "#ffffff",
839
+ intensity: term.intensity
840
+ };
841
+ }
842
+ const color = lights.find((light) => light.color)?.color;
843
+ const hex = color ? color.clone().setRGB(term.color[0], term.color[1], term.color[2]).getHexString() : "ffffff";
844
+ return { color: `#${hex}`, intensity: term.intensity };
845
+ }
846
+
847
+ // src/timeline/bake.ts
848
+ function bakeTimeline(timeline, options = {}) {
849
+ const fps = Math.max(1, Math.round(options.fps ?? timeline.fps));
850
+ const duration = Math.max(1 / fps, options.duration ?? timeline.duration);
851
+ const frameCount = Math.max(1, Math.round(duration * fps));
852
+ if (frameCount > MAX_ANIMATION_FRAMES) {
853
+ throw new Bakery3Error({
854
+ code: "InvalidRequest",
855
+ message: `${frameCount} frames is above the ${MAX_ANIMATION_FRAMES}-frame ceiling.`,
856
+ why: `${duration}s at ${fps} fps is more render time than any single request is allowed to buy. A video is where one wrong number turns minutes into hours.`,
857
+ fix: "Shorten the timeline, lower the fps, or render it in sections.",
858
+ docs: "https://bakery3.com/docs/animation"
859
+ });
860
+ }
861
+ const scene = options.scene ?? timeline.scene;
862
+ const channels = [];
863
+ channels.push(...cameraChannels(timeline, fps, frameCount));
864
+ channels.push(...objectChannels(timeline, fps, frameCount, scene));
865
+ channels.push(...environmentChannels(timeline, fps, frameCount, scene));
866
+ if (!channels.length) {
867
+ throw new Bakery3Error({
868
+ code: "InvalidRequest",
869
+ message: "The timeline has nothing to animate.",
870
+ why: "A video of an unchanging scene costs one render per frame and looks exactly like the still.",
871
+ fix: "Add a move, such as tl.push({ object: 'camera', orbitY: 90 }), or render a still with render().",
872
+ docs: "https://bakery3.com/docs/animation"
873
+ });
874
+ }
875
+ const checks = worldChecks(timeline, fps, frameCount, channels);
876
+ return { fps, frameCount, channels, ...checks.length ? { checks } : {} };
877
+ }
878
+ function checkFrames(frameCount) {
879
+ const last = Math.max(0, frameCount - 1);
880
+ const frames = /* @__PURE__ */ new Set();
881
+ for (let i = 0; i < WORLD_CHECK_FRAMES; i++)
882
+ frames.add(Math.round(last * i / (WORLD_CHECK_FRAMES - 1)));
883
+ return [...frames].sort((a, b) => a - b);
884
+ }
885
+ function worldChecks(timeline, fps, frameCount, channels) {
886
+ const frames = checkFrames(frameCount);
887
+ const objects = /* @__PURE__ */ new Map();
888
+ for (const track of timeline.tracks) {
889
+ if (track.target !== "camera" && !isDome(track.target) && !isScene(track.target))
890
+ objects.set(idOf(track.target), track.target);
891
+ }
892
+ const cameraMoves = channels.some(
893
+ (c) => c.target.type === "camera" && c.property === "quaternion"
894
+ );
895
+ if (!objects.size && !cameraMoves) return [];
896
+ const perObject = new Map(
897
+ [...objects.keys()].map((id) => [id, []])
898
+ );
899
+ const cameraMatrices = [];
900
+ const matrix = new Matrix4();
901
+ const position = new Vector3();
902
+ const quaternion = new Quaternion();
903
+ const unit = new Vector3(1, 1, 1);
904
+ const was = timeline.time;
905
+ try {
906
+ for (const frame of frames) {
907
+ const time = frame / fps;
908
+ if (objects.size) {
909
+ timeline.seek(time);
910
+ for (const [id, object] of objects) {
911
+ object.updateWorldMatrix(true, false);
912
+ perObject.get(id).push(...object.matrixWorld.elements);
913
+ }
914
+ }
915
+ if (cameraMoves) {
916
+ const pose = timeline.cameraPoseAt(time);
917
+ if (pose) {
918
+ matrix.compose(
919
+ position.set(...pose.position),
920
+ quaternion.set(...pose.quaternion),
921
+ unit
922
+ );
923
+ cameraMatrices.push(...matrix.elements);
924
+ }
925
+ }
926
+ }
927
+ } finally {
928
+ if (objects.size) timeline.seek(was);
929
+ }
930
+ const checks = [];
931
+ for (const [objectId, matrices] of perObject)
932
+ checks.push({ target: { type: "object", objectId }, frames, matrices });
933
+ if (cameraMatrices.length === frames.length * 16)
934
+ checks.push({
935
+ target: { type: "camera" },
936
+ frames,
937
+ matrices: cameraMatrices
938
+ });
939
+ return checks;
940
+ }
941
+ function cameraChannels(timeline, fps, frameCount) {
942
+ const tracks = timeline.tracks.filter((track) => track.target === "camera");
943
+ const moves = tracks.some(
944
+ (track) => track.property === "position" || track.property === "lookAt"
945
+ );
946
+ const fovTrack = tracks.find((track) => track.property === "fov");
947
+ const channels = [];
948
+ if (!moves && !fovTrack) return channels;
949
+ if (moves) {
950
+ const positions = [];
951
+ const quaternions = [];
952
+ for (let frame = 0; frame < frameCount; frame++) {
953
+ const pose = timeline.cameraPoseAt(frame / fps);
954
+ if (!pose) break;
955
+ positions.push(...pose.position);
956
+ quaternions.push(...pose.quaternion);
957
+ }
958
+ const animatesPosition = tracks.some(
959
+ (track) => track.property === "position"
960
+ );
961
+ if (animatesPosition) {
962
+ channels.push({
963
+ target: { type: "camera" },
964
+ property: "position",
965
+ size: 3,
966
+ values: positions
967
+ });
968
+ }
969
+ channels.push({
970
+ target: { type: "camera" },
971
+ property: "quaternion",
972
+ size: 4,
973
+ values: quaternions
974
+ });
975
+ }
976
+ if (fovTrack) {
977
+ channels.push({
978
+ target: { type: "camera" },
979
+ property: "fov",
980
+ size: 1,
981
+ values: sample(timeline, fovTrack, fps, frameCount, 1)
982
+ });
983
+ }
984
+ return channels;
985
+ }
986
+ function objectChannels(timeline, fps, frameCount, scene) {
987
+ const channels = [];
988
+ for (const track of timeline.tracks) {
989
+ if (track.target === "camera") continue;
990
+ const object = track.target;
991
+ if (isAmbient(object) || isScene(object)) continue;
992
+ requireInScene(object, scene, track.property);
993
+ const definition = propertyDefinition(object, track.property);
994
+ const property = definition?.channel ?? track.property;
995
+ const size = ANIMATION_CHANNEL_SIZES[property];
996
+ if (size === void 0) {
997
+ throw new Bakery3Error({
998
+ code: "InvalidRequest",
999
+ message: `"${property}" has no channel in this protocol version.`,
1000
+ why: "The SDK can author it but the wire format cannot carry it, so the renderer would never see it.",
1001
+ fix: "This is a bug in the SDK. Please report it to hello@bakery3.com with the property name."
1002
+ });
1003
+ }
1004
+ const values = track.property === "rotation" ? sampleAsQuaternion(timeline, track, fps, frameCount) : sample(timeline, track, fps, frameCount, size);
1005
+ channels.push({
1006
+ target: { type: "object", objectId: idOf(object) },
1007
+ property,
1008
+ size,
1009
+ values
1010
+ });
1011
+ }
1012
+ return channels;
1013
+ }
1014
+ function environmentChannels(timeline, fps, frameCount, scene) {
1015
+ const channels = [];
1016
+ const environment = { type: "environment" };
1017
+ for (const track of timeline.tracks) {
1018
+ if (track.target === "camera" || !isScene(track.target)) continue;
1019
+ requireInScene(track.target, scene, track.property);
1020
+ const property = track.property === "environmentIntensity" ? "intensity" : "rotation";
1021
+ const size = ENVIRONMENT_CHANNEL_SIZES[property];
1022
+ channels.push({
1023
+ target: environment,
1024
+ property,
1025
+ size,
1026
+ values: sample(timeline, track, fps, frameCount, size)
1027
+ });
1028
+ }
1029
+ const ambient = timeline.tracks.filter(
1030
+ (track) => track.target !== "camera" && isAmbient(track.target)
1031
+ );
1032
+ if (!ambient.length) return channels;
1033
+ for (const track of ambient)
1034
+ requireInScene(track.target, scene, track.property);
1035
+ const lights = ambientLightsIn(scene ?? rootOf(ambient[0].target));
1036
+ const intensities = [];
1037
+ const colors = [];
1038
+ for (let frame = 0; frame < frameCount; frame++) {
1039
+ const terms = lights.map((light) => {
1040
+ const term = ambientTermOf(light);
1041
+ for (const track of ambient) {
1042
+ if (track.target !== light) continue;
1043
+ const value = timeline.valueAt(track, frame / fps);
1044
+ if (!value) continue;
1045
+ if (track.property === "intensity") term.intensity = value[0];
1046
+ else term.color = [value[0], value[1], value[2]];
1047
+ }
1048
+ return term;
1049
+ });
1050
+ const sum = combineAmbient(terms) ?? { color: [1, 1, 1], intensity: 0 };
1051
+ intensities.push(sum.intensity);
1052
+ colors.push(...sum.color);
1053
+ }
1054
+ channels.push(
1055
+ {
1056
+ target: environment,
1057
+ property: "ambientIntensity",
1058
+ size: ENVIRONMENT_CHANNEL_SIZES.ambientIntensity,
1059
+ values: intensities
1060
+ },
1061
+ {
1062
+ target: environment,
1063
+ property: "ambientColor",
1064
+ size: ENVIRONMENT_CHANNEL_SIZES.ambientColor,
1065
+ values: colors
1066
+ }
1067
+ );
1068
+ return channels;
1069
+ }
1070
+ function isAmbient(object) {
1071
+ return Boolean(
1072
+ object.isAmbientLight
1073
+ );
1074
+ }
1075
+ function isHemisphere(object) {
1076
+ return Boolean(
1077
+ object.isHemisphereLight
1078
+ );
1079
+ }
1080
+ function isDome(object) {
1081
+ return isAmbient(object) || isHemisphere(object);
1082
+ }
1083
+ function isScene(object) {
1084
+ return Boolean(object.isScene);
1085
+ }
1086
+ function rootOf(object) {
1087
+ let root = object;
1088
+ while (root.parent) root = root.parent;
1089
+ return root;
1090
+ }
1091
+ function sample(timeline, track, fps, frameCount, size) {
1092
+ const values = [];
1093
+ for (let frame = 0; frame < frameCount; frame++) {
1094
+ const value = timeline.valueAt(track, frame / fps);
1095
+ for (let i = 0; i < size; i++) values.push(value?.[i] ?? 0);
1096
+ }
1097
+ return values;
1098
+ }
1099
+ function sampleAsQuaternion(timeline, track, fps, frameCount) {
1100
+ const values = [];
1101
+ for (let frame = 0; frame < frameCount; frame++) {
1102
+ const euler = timeline.valueAt(track, frame / fps) ?? [0, 0, 0];
1103
+ values.push(...eulerToQuaternion(euler));
1104
+ }
1105
+ return values;
1106
+ }
1107
+ function unexportableReason(object, property) {
1108
+ const name = () => object.name || object.type;
1109
+ for (let node = object; node; node = node.parent) {
1110
+ if (!excludedItself(node)) continue;
1111
+ const where = node === object ? `"${name()}"` : `"${name()}" (inside "${node.name || node.type}")`;
1112
+ const because = readExclusionKind(node);
1113
+ return {
1114
+ code: "InvalidRequest",
1115
+ message: `The timeline animates "${property}" on ${where}, which never reaches a render: ${because}.`,
1116
+ why: "Excluded objects are detached from the GLB before export, so this channel would arrive at the renderer with an id that matches nothing.",
1117
+ fix: because === EXCLUDED_BY_USERDATA ? "Remove userData.bakery3.exclude from it, or remove the track with timeline.removeTrack()." : "Animate the object you meant instead: helpers and gizmos never render.",
1118
+ docs: "https://bakery3.com/docs/animation"
1119
+ };
1120
+ }
1121
+ const domeProperties = isAmbient(object) ? AMBIENT_PROPERTIES : isHemisphere(object) ? HEMISPHERE_CHANNEL_PROPERTIES : void 0;
1122
+ if (domeProperties && !domeProperties.includes(property)) {
1123
+ const kind = isAmbient(object) ? "an AmbientLight" : "a HemisphereLight";
1124
+ return {
1125
+ code: "InvalidRequest",
1126
+ message: `The timeline animates "${property}" on "${name()}", ${kind}, which has no "${property}" in a render.`,
1127
+ why: isHemisphere(object) ? "A hemisphere light is a sky above and a ground below, split on its up axis. The renderer builds it into the world, where it has nothing to move, and turning its axis over a clip is not something it reproduces." : "An ambient light is the same light from every direction, so it has nothing to move or turn.",
1128
+ fix: `Animate ${domeProperties.join(", ")} on it instead.`,
1129
+ docs: "https://bakery3.com/docs/animation"
1130
+ };
1131
+ }
1132
+ return null;
1133
+ }
1134
+ var AMBIENT_PROPERTIES = ["intensity", "color"];
1135
+ var EXCLUDED_BY_USERDATA = "it is excluded with userData.bakery3.exclude";
1136
+ function readExclusionKind(node) {
1137
+ const data = node.userData?.bakery3;
1138
+ if (data?.exclude) return EXCLUDED_BY_USERDATA;
1139
+ if (node.type === "TransformControls")
1140
+ return "it is part of a TransformControls gizmo";
1141
+ if (node.userData?.bakery3Preset?.roomPreset) {
1142
+ return "it is part of a room preset's stand-in, which the renderer rebuilds rather than imports";
1143
+ }
1144
+ return `${node.type} is debug furniture the exporter always skips`;
1145
+ }
1146
+ function requireInScene(object, scene, property) {
1147
+ const reason = unexportableReason(object, property);
1148
+ if (reason) throw new Bakery3Error(reason);
1149
+ if (object.isLight && !object.visible) {
1150
+ throw new Bakery3Error({
1151
+ code: "InvalidRequest",
1152
+ message: `The timeline animates "${property}" on "${object.name || object.type}", a light that is currently invisible.`,
1153
+ why: "Invisible lights are left out of the manifest, so this channel would arrive with an id that matches nothing.",
1154
+ fix: "Make the light visible before rendering, or remove the track.",
1155
+ docs: "https://bakery3.com/docs/animation"
1156
+ });
1157
+ }
1158
+ if (!scene) return;
1159
+ let found = false;
1160
+ scene.traverse((candidate) => {
1161
+ if (candidate === object) found = true;
1162
+ });
1163
+ if (found) return;
1164
+ throw new Bakery3Error({
1165
+ code: "InvalidRequest",
1166
+ message: `The timeline animates "${property}" on "${object.name || object.type}", which is not in the scene being rendered.`,
1167
+ why: "Only objects the exporter walks reach the renderer, so this channel would arrive with an id that matches nothing and be silently ignored.",
1168
+ fix: "Add the object to the scene before rendering, or remove the track with timeline.removeTrack().",
1169
+ docs: "https://bakery3.com/docs/animation"
1170
+ });
1171
+ }
1172
+
1173
+ // src/timeline/timeline.ts
1174
+ var PUSH_OPTION_KEYS = /* @__PURE__ */ new Set([
1175
+ "object",
1176
+ "at",
1177
+ "duration",
1178
+ "easing",
1179
+ "orbitY",
1180
+ "orbitX",
1181
+ "dolly",
1182
+ "lookAt"
1183
+ ]);
1184
+ var contexts = /* @__PURE__ */ new WeakMap();
1185
+ function bindTimelineContext(timeline, context) {
1186
+ contexts.set(timeline, context);
1187
+ return timeline;
1188
+ }
1189
+ var Timeline = class _Timeline {
1190
+ _duration;
1191
+ _fps;
1192
+ _time = 0;
1193
+ _tracks = [];
1194
+ _playing = false;
1195
+ _raf = null;
1196
+ _lastTick = 0;
1197
+ _listeners = /* @__PURE__ */ new Map();
1198
+ _center;
1199
+ _nextId = 0;
1200
+ /** Playback only. A bake is always exactly one pass of the duration, so
1201
+ * looping in the panel never changes what a video contains. */
1202
+ loop;
1203
+ _camera;
1204
+ _scene;
1205
+ /** The host's orbit-controls target, when it has one. */
1206
+ controlsTarget;
1207
+ constructor(options = {}) {
1208
+ this._duration = options.duration ?? 5;
1209
+ this._fps = options.fps ?? 30;
1210
+ this._center = options.lookAt ?? null;
1211
+ this.loop = options.loop ?? true;
1212
+ if (options.camera) this.camera = options.camera;
1213
+ if (options.scene) this.scene = options.scene;
1214
+ if (options.controlsTarget) this.controlsTarget = options.controlsTarget;
1215
+ }
1216
+ /**
1217
+ * What `seek()` drives and `captureCameraKeyframe()` reads. Settable after
1218
+ * construction because an R3F camera is frequently swapped out from under
1219
+ * the application. A timeline from `bakery3.timeline()` that was given no
1220
+ * camera reads the client's attached one each time it needs it.
1221
+ */
1222
+ get camera() {
1223
+ return this._camera ?? contexts.get(this)?.()?.camera ?? void 0;
1224
+ }
1225
+ set camera(camera) {
1226
+ this._camera = camera;
1227
+ }
1228
+ /** Used to find the orbit center, and to check at bake time that every
1229
+ * animated object is actually in what gets exported. Read from the
1230
+ * client's attachment, like `camera`, when it was not given. */
1231
+ get scene() {
1232
+ return this._scene ?? contexts.get(this)?.()?.scene ?? void 0;
1233
+ }
1234
+ set scene(scene) {
1235
+ this._scene = scene;
1236
+ }
1237
+ // -------------------------------------------------------------------------
1238
+ // Shape
1239
+ // -------------------------------------------------------------------------
1240
+ get duration() {
1241
+ return this._duration;
1242
+ }
1243
+ /**
1244
+ * Setting this moves the end, not the keyframes.
1245
+ *
1246
+ * Existing keyframes keep their absolute times, because the common reason to
1247
+ * change the duration is "I need two more seconds at the end", and silently
1248
+ * rescaling every ease you already tuned would be the opposite of that.
1249
+ * `scaleToFit()` is the other intent, spelled out.
1250
+ */
1251
+ set duration(seconds) {
1252
+ this._duration = Math.max(1 / this._fps, seconds);
1253
+ if (this._time > this._duration) this.seek(this._duration);
1254
+ this.emit("change");
1255
+ }
1256
+ get fps() {
1257
+ return this._fps;
1258
+ }
1259
+ set fps(value) {
1260
+ this._fps = Math.max(1, Math.round(value));
1261
+ this.emit("change");
1262
+ }
1263
+ get time() {
1264
+ return this._time;
1265
+ }
1266
+ get playing() {
1267
+ return this._playing;
1268
+ }
1269
+ get tracks() {
1270
+ return this._tracks;
1271
+ }
1272
+ /** Frames a bake at the current settings will produce. */
1273
+ get frameCount() {
1274
+ return Math.max(1, Math.round(this._duration * this._fps));
1275
+ }
1276
+ /** Proportionally rescale every keyframe into a new duration. */
1277
+ scaleToFit(seconds) {
1278
+ const factor = seconds / this._duration;
1279
+ for (const track of this._tracks) {
1280
+ for (const keyframe of track.keyframes) keyframe.time *= factor;
1281
+ }
1282
+ this._duration = seconds;
1283
+ this.emit("change");
1284
+ return this;
1285
+ }
1286
+ /**
1287
+ * The point orbits turn around.
1288
+ *
1289
+ * Resolved lazily so a timeline created before the model finished loading
1290
+ * still centers on it. `createTimeline()` at mount is the normal case, and
1291
+ * an empty scene's bounding box is the origin, which would be wrong for
1292
+ * every model that is not at the origin.
1293
+ */
1294
+ get center() {
1295
+ if (this._center) return this._center;
1296
+ if (this.scene) {
1297
+ const box = boundingCenter(this.scene);
1298
+ if (box) return box;
1299
+ }
1300
+ return [0, 0, 0];
1301
+ }
1302
+ set center(value) {
1303
+ this._center = value;
1304
+ this.emit("change");
1305
+ }
1306
+ // -------------------------------------------------------------------------
1307
+ // Authoring
1308
+ // -------------------------------------------------------------------------
1309
+ /**
1310
+ * Tween to a value from wherever the target is at `at`.
1311
+ *
1312
+ * "Wherever it is at `at`" means the timeline's own value if something else
1313
+ * already animates that property, and the live object's value if not, so
1314
+ * chaining two `.to()` calls on one property does what it looks like.
1315
+ */
1316
+ to(target, props, options = {}) {
1317
+ const { at, end } = this.span(options);
1318
+ for (const [property, raw] of Object.entries(props)) {
1319
+ const track = this.track(target, property);
1320
+ const value = normalize3(raw, track.size, property);
1321
+ this.anchor(track, at);
1322
+ this.insert(track, {
1323
+ time: end,
1324
+ value,
1325
+ ...options.easing ? { easing: options.easing } : {}
1326
+ });
1327
+ }
1328
+ this.emit("change");
1329
+ return this;
1330
+ }
1331
+ /** Both ends stated. Nothing is read off the live object. */
1332
+ fromTo(target, from, to, options = {}) {
1333
+ const { at, end } = this.span(options);
1334
+ for (const [property, raw] of Object.entries(to)) {
1335
+ const track = this.track(target, property);
1336
+ const start = from[property];
1337
+ if (start !== void 0) {
1338
+ this.insert(track, {
1339
+ time: at,
1340
+ value: normalize3(start, track.size, property)
1341
+ });
1342
+ } else {
1343
+ this.anchor(track, at);
1344
+ }
1345
+ this.insert(track, {
1346
+ time: end,
1347
+ value: normalize3(raw, track.size, property),
1348
+ ...options.easing ? { easing: options.easing } : {}
1349
+ });
1350
+ }
1351
+ this.emit("change");
1352
+ return this;
1353
+ }
1354
+ /**
1355
+ * Swing the camera around what it is looking at.
1356
+ *
1357
+ * The lookAt point is pinned for the whole segment. That is the difference
1358
+ * between an orbit and a camera drifting sideways past the product, and it
1359
+ * is why this writes a `lookAt` track as well as a `position` one.
1360
+ */
1361
+ orbit(camera, options = {}) {
1362
+ const target = camera === "camera" || camera === this.camera ? "camera" : camera;
1363
+ if (target !== "camera") {
1364
+ throw new Bakery3Error({
1365
+ code: "InvalidRequest",
1366
+ message: "orbit() only drives the camera.",
1367
+ why: "An orbit is defined by a camera and the point it is aimed at; an ordinary object has no lookAt to hold.",
1368
+ fix: "Pass 'camera' (or the timeline's camera), or animate the object's position with to()."
1369
+ });
1370
+ }
1371
+ const { at, end } = this.span(options);
1372
+ const center = resolveCenter(options.lookAt, this.center);
1373
+ const positions = this.track("camera", "position");
1374
+ const looks = this.track("camera", "lookAt");
1375
+ const start = this.valueAt(positions, at) ?? this.live("camera", "position") ?? [
1376
+ center[0],
1377
+ center[1],
1378
+ center[2] + 1
1379
+ ];
1380
+ const from = toSpherical(start, center);
1381
+ const radius = options.radius !== void 0 ? options.radius : from.radius + (options.dolly ?? 0);
1382
+ const to = {
1383
+ radius,
1384
+ yaw: from.yaw + degrees(options.yaw ?? 0),
1385
+ pitch: from.pitch + degrees(options.pitch ?? 0)
1386
+ };
1387
+ if (!positions.keyframes.some((k) => Math.abs(k.time - at) < 1e-6))
1388
+ this.insert(positions, { time: at, value: [...start] });
1389
+ this.insert(positions, {
1390
+ time: end,
1391
+ value: [...fromSpherical(to, center)],
1392
+ path: "orbit",
1393
+ orbit: { center, ...to },
1394
+ ...options.easing ? { easing: options.easing } : {}
1395
+ });
1396
+ this.insert(looks, { time: at, value: [...center] });
1397
+ this.insert(looks, { time: end, value: [...center] });
1398
+ this.emit("change");
1399
+ return this;
1400
+ }
1401
+ /**
1402
+ * One-call shorthand:
1403
+ *
1404
+ * tl.push({ object: 'camera', orbitY: +15 });
1405
+ *
1406
+ * The camera starts where it is now and ends fifteen degrees round its
1407
+ * current lookAt, holding that lookAt, over the whole timeline. Any other
1408
+ * key is treated as if it had been passed to `.to()`.
1409
+ */
1410
+ push(spec) {
1411
+ const { object, ...rest } = spec;
1412
+ const options = {
1413
+ ...rest.at !== void 0 ? { at: rest.at } : {},
1414
+ ...rest.duration !== void 0 ? { duration: rest.duration } : {},
1415
+ ...rest.easing !== void 0 ? { easing: rest.easing } : {}
1416
+ };
1417
+ const orbits = rest.orbitY !== void 0 || rest.orbitX !== void 0 || rest.dolly !== void 0;
1418
+ if (orbits) {
1419
+ this.orbit(object, {
1420
+ ...options,
1421
+ ...rest.orbitY !== void 0 ? { yaw: rest.orbitY } : {},
1422
+ ...rest.orbitX !== void 0 ? { pitch: rest.orbitX } : {},
1423
+ ...rest.dolly !== void 0 ? { dolly: rest.dolly } : {},
1424
+ ...rest.lookAt !== void 0 ? { lookAt: rest.lookAt } : {}
1425
+ });
1426
+ }
1427
+ const props = {};
1428
+ for (const [key, value] of Object.entries(rest)) {
1429
+ if (PUSH_OPTION_KEYS.has(key)) continue;
1430
+ props[key] = value;
1431
+ }
1432
+ if (Object.keys(props).length) this.to(object, props, options);
1433
+ else if (!orbits) {
1434
+ throw new Bakery3Error({
1435
+ code: "InvalidRequest",
1436
+ message: "push() was given nothing to animate.",
1437
+ why: "Every key was an option (at, duration, easing, \u2026); no property and no orbit remained.",
1438
+ fix: "Add a property: push({ object: 'camera', orbitY: 15 }) or push({ object: mesh, position: [0, 1, 0] })."
1439
+ });
1440
+ }
1441
+ return this;
1442
+ }
1443
+ // -------------------------------------------------------------------------
1444
+ // Keyframe editing (what the panel drives)
1445
+ // -------------------------------------------------------------------------
1446
+ /**
1447
+ * Save a property where it is right now.
1448
+ *
1449
+ * The After Effects flow: put the playhead somewhere, pose the scene, press
1450
+ * the button. Both arguments default to exactly that: the playhead, and the
1451
+ * live value.
1452
+ */
1453
+ keyframe(target, property, time = this._time, value) {
1454
+ const track = this.track(target, property);
1455
+ const resolved = value !== void 0 ? normalize3(value, track.size, property) : this.live(target, property) ?? this.valueAt(track, time);
1456
+ if (!resolved) {
1457
+ throw new Bakery3Error({
1458
+ code: "InvalidRequest",
1459
+ message: `Nothing to read for "${property}" on ${track.label}.`,
1460
+ why: "The property is not present on that object (a mesh has no intensity, a light has no roughness) and no value was passed.",
1461
+ fix: `Pass a value explicitly, or animate a property the object has.`
1462
+ });
1463
+ }
1464
+ const keyframe = this.insert(track, { time, value: resolved });
1465
+ this.emit("change");
1466
+ return keyframe;
1467
+ }
1468
+ /**
1469
+ * The camera, as the developer framed it.
1470
+ *
1471
+ * Position and the point it is aimed at, because a camera keyframe that
1472
+ * records only position leaves the render free to aim it somewhere else.
1473
+ * The aim comes from the host's orbit controls when there are any (that is
1474
+ * the point you were dragging around), and is otherwise projected along the
1475
+ * camera's own forward axis, never assumed to be the origin.
1476
+ */
1477
+ captureCameraKeyframe(time = this._time, lookAt) {
1478
+ const camera = this.requireCamera("captureCameraKeyframe");
1479
+ camera.updateMatrixWorld();
1480
+ const elements = camera.matrixWorld.elements;
1481
+ const position = [elements[12], elements[13], elements[14]];
1482
+ const target = lookAt ?? this.controlsTarget?.() ?? readCameraLookAt(camera, this.center);
1483
+ this.insert(this.track("camera", "position"), {
1484
+ time,
1485
+ value: [...position]
1486
+ });
1487
+ this.insert(this.track("camera", "lookAt"), { time, value: [...target] });
1488
+ this.emit("change");
1489
+ }
1490
+ removeKeyframe(trackId, index) {
1491
+ const track = this._tracks.find((t) => t.id === trackId);
1492
+ if (!track) return;
1493
+ track.keyframes.splice(index, 1);
1494
+ if (!track.keyframes.length) this.removeTrack(trackId);
1495
+ this.emit("change");
1496
+ }
1497
+ /** Retime one keyframe, clamped into the timeline. A keyframe dragged past
1498
+ * the end would still be baked, at a frame nobody renders, so it is pinned
1499
+ * to the duration rather than allowed to leave the shot. */
1500
+ moveKeyframe(trackId, index, time) {
1501
+ const track = this._tracks.find((t) => t.id === trackId);
1502
+ const keyframe = track?.keyframes[index];
1503
+ if (!track || !keyframe) return;
1504
+ keyframe.time = Math.max(0, Math.min(this._duration, time));
1505
+ track.keyframes.sort((a, b) => a.time - b.time);
1506
+ this.emit("change");
1507
+ }
1508
+ /** Re-curve the segment ending at this keyframe, the same convention `.to()`
1509
+ * writes, so the panel's dropdown and the authoring API mean the same thing
1510
+ * by "the ease on this diamond". */
1511
+ setEasing(trackId, index, easing) {
1512
+ const keyframe = this._tracks.find((t) => t.id === trackId)?.keyframes[index];
1513
+ if (!keyframe) return;
1514
+ keyframe.easing = easing;
1515
+ this.emit("change");
1516
+ }
1517
+ /** Drop a lane and everything on it. The live object keeps whatever value it
1518
+ * last had: removing a track stops driving a property, it does not restore
1519
+ * what the property was before the timeline touched it. */
1520
+ removeTrack(trackId) {
1521
+ this._tracks = this._tracks.filter((track) => track.id !== trackId);
1522
+ this.emit("change");
1523
+ }
1524
+ /**
1525
+ * Re-attach tracks to the scene as it is now, and report what could not be.
1526
+ *
1527
+ * An application that rebuilds part of its graph (a configurator swapping a
1528
+ * variant, R3F remounting a subtree) leaves tracks pointing at Object3Ds
1529
+ * that are no longer in the scene, even though an object that is for every
1530
+ * purpose "the same one" is standing right there. Rather than fail at
1531
+ * export, look for that object: first by the identity you pinned
1532
+ * (`userData.bakery3Id`, which is exactly what survives a rebuild), then by
1533
+ * an unambiguous name match.
1534
+ *
1535
+ * Nothing is dropped silently. Every track that could not be healed is
1536
+ * returned, so the caller can say what happened; that is the difference
1537
+ * between repairing a timeline and quietly losing a motion you authored.
1538
+ */
1539
+ reconcile(scene) {
1540
+ const healed = [];
1541
+ const stale = [];
1542
+ const live = /* @__PURE__ */ new Set();
1543
+ const byId = /* @__PURE__ */ new Map();
1544
+ const byName = /* @__PURE__ */ new Map();
1545
+ scene.traverse((object) => {
1546
+ live.add(object);
1547
+ const declared = object.userData?.bakery3Id;
1548
+ if (typeof declared === "string" && declared) byId.set(declared, object);
1549
+ if (object.name) {
1550
+ const bucket = byName.get(object.name);
1551
+ if (bucket) bucket.push(object);
1552
+ else byName.set(object.name, [object]);
1553
+ }
1554
+ });
1555
+ for (const track of this._tracks) {
1556
+ if (track.target === "camera" || live.has(track.target)) continue;
1557
+ const previous = track.target;
1558
+ const declared = previous.userData?.bakery3Id;
1559
+ const replacement = (typeof declared === "string" ? byId.get(declared) : void 0) ?? // Only an unambiguous name match: two meshes called 'seat' and a
1560
+ // coin flip between them is worse than saying the track is stale.
1561
+ (previous.name && byName.get(previous.name)?.length === 1 ? byName.get(previous.name)[0] : void 0);
1562
+ const entry = {
1563
+ trackId: track.id,
1564
+ property: track.property,
1565
+ label: track.label
1566
+ };
1567
+ if (!replacement) {
1568
+ stale.push({ ...entry, reason: "it is no longer in the scene" });
1569
+ continue;
1570
+ }
1571
+ const blocked = unexportableReason(replacement, track.property);
1572
+ if (blocked) {
1573
+ stale.push({ ...entry, reason: blocked.message });
1574
+ continue;
1575
+ }
1576
+ if (!propertyDefinition(replacement, track.property)) {
1577
+ stale.push({
1578
+ ...entry,
1579
+ reason: `the object that replaced it has no "${track.property}"`
1580
+ });
1581
+ continue;
1582
+ }
1583
+ track.target = replacement;
1584
+ track.label = labelFor(replacement);
1585
+ healed.push(entry);
1586
+ }
1587
+ if (healed.length) this.emit("change");
1588
+ return { healed, stale };
1589
+ }
1590
+ /** Find or create a lane. Public because the panel's "add track" picker
1591
+ * needs to create one without also writing a keyframe. */
1592
+ track(target, property) {
1593
+ const resolved = this.normalizeTarget(target);
1594
+ const existing = this._tracks.find(
1595
+ (track2) => track2.target === resolved && track2.property === property
1596
+ );
1597
+ if (existing) return existing;
1598
+ if (resolved !== "camera") {
1599
+ const reason = unexportableReason(resolved, property);
1600
+ if (reason) throw new Bakery3Error(reason);
1601
+ }
1602
+ const definition = propertyDefinition(resolved, property);
1603
+ if (!definition) {
1604
+ throw new Bakery3Error({
1605
+ code: "InvalidRequest",
1606
+ message: `"${property}" is not an animatable property${resolved === "camera" ? " of the camera" : ""}.`,
1607
+ why: "Only properties the renderer can reproduce can be animated; animating one it cannot would produce a video that silently ignores it.",
1608
+ fix: `Animate one of: ${Object.keys(
1609
+ resolved === "camera" ? CAMERA_PROPERTIES : OBJECT_PROPERTIES
1610
+ ).join(", ")}.`,
1611
+ docs: "https://bakery3.com/docs/animation"
1612
+ });
1613
+ }
1614
+ if (resolved !== "camera" && definition.appliesTo && !definition.appliesTo(resolved)) {
1615
+ throw new Bakery3Error({
1616
+ code: "InvalidRequest",
1617
+ message: `"${labelFor(resolved)}" has no "${property}" to animate.`,
1618
+ why: property === "target" ? "Only a DirectionalLight or a SpotLight aims at a target." : `"${property}" exists on some kinds of object, and this is not one of them.`,
1619
+ fix: property === "target" ? "Animate the light's position, or animate a DirectionalLight or SpotLight's target." : "Animate the property on an object that has it.",
1620
+ docs: "https://bakery3.com/docs/animation"
1621
+ });
1622
+ }
1623
+ const track = {
1624
+ id: `t${this._nextId++}`,
1625
+ target: resolved,
1626
+ property,
1627
+ size: definition.size,
1628
+ label: labelFor(resolved),
1629
+ keyframes: []
1630
+ };
1631
+ this._tracks.push(track);
1632
+ return track;
1633
+ }
1634
+ // -------------------------------------------------------------------------
1635
+ // Playback
1636
+ // -------------------------------------------------------------------------
1637
+ /**
1638
+ * Evaluate every track and drive the live scene.
1639
+ *
1640
+ * The same evaluator `bake()` samples, which is the whole design: what you
1641
+ * scrub to and what the renderer traces cannot drift apart, because there is
1642
+ * only one of them.
1643
+ */
1644
+ seek(time) {
1645
+ this._time = Math.max(0, Math.min(this._duration, time));
1646
+ this.apply(this._time);
1647
+ this.emit("time", this._time);
1648
+ return this;
1649
+ }
1650
+ play() {
1651
+ if (this._playing) return this;
1652
+ this._playing = true;
1653
+ this._lastTick = now();
1654
+ this.emit("play");
1655
+ this.tick();
1656
+ return this;
1657
+ }
1658
+ pause() {
1659
+ if (!this._playing) return this;
1660
+ this._playing = false;
1661
+ cancel(this._raf);
1662
+ this._raf = null;
1663
+ this.emit("pause");
1664
+ return this;
1665
+ }
1666
+ /** Pause and rewind. */
1667
+ stop() {
1668
+ this.pause();
1669
+ return this.seek(0);
1670
+ }
1671
+ /** Stop the clock and forget every listener. The scene is left exactly where
1672
+ * the playhead left it: a timeline being torn down should not also move
1673
+ * your camera back. */
1674
+ dispose() {
1675
+ this.pause();
1676
+ this._listeners.clear();
1677
+ }
1678
+ tick() {
1679
+ if (!this._playing) return;
1680
+ const stamp = now();
1681
+ const delta = (stamp - this._lastTick) / 1e3;
1682
+ this._lastTick = stamp;
1683
+ let next = this._time + delta;
1684
+ if (next >= this._duration) {
1685
+ if (this.loop) next = next % this._duration;
1686
+ else {
1687
+ this.seek(this._duration);
1688
+ this.pause();
1689
+ return;
1690
+ }
1691
+ }
1692
+ this.seek(next);
1693
+ this._raf = schedule(() => this.tick());
1694
+ }
1695
+ // -------------------------------------------------------------------------
1696
+ // Evaluation
1697
+ // -------------------------------------------------------------------------
1698
+ /** The value of one track at a time, without touching the scene. Exposed
1699
+ * because the bake samples it. */
1700
+ valueAt(track, time) {
1701
+ const keys = track.keyframes;
1702
+ if (!keys.length) return void 0;
1703
+ if (keys.length === 1 || time <= keys[0].time) return [...keys[0].value];
1704
+ const last = keys[keys.length - 1];
1705
+ if (time >= last.time) return [...last.value];
1706
+ let index = 0;
1707
+ while (index < keys.length - 1 && keys[index + 1].time <= time) index++;
1708
+ const a = keys[index];
1709
+ const b = keys[index + 1];
1710
+ const span = b.time - a.time;
1711
+ const u = span <= 0 ? 1 : (time - a.time) / span;
1712
+ const t = ease(b.easing, u);
1713
+ if (b.path === "orbit" && b.orbit) {
1714
+ const center = b.orbit.center;
1715
+ const from = a.orbit ?? toSpherical(a.value, center);
1716
+ return orbitAt(from, b.orbit, center, t);
1717
+ }
1718
+ if (track.size === 4 && track.property === "quaternion") {
1719
+ return slerp(a.value, b.value, t);
1720
+ }
1721
+ return a.value.map(
1722
+ (value, i) => value + ((b.value[i] ?? value) - value) * t
1723
+ );
1724
+ }
1725
+ /** What the timeline says everything is at `time`, for a panel readout or a
1726
+ * test: the same numbers `seek()` applies. */
1727
+ sample(time) {
1728
+ const out = [];
1729
+ for (const track of this._tracks) {
1730
+ const value = this.valueAt(track, time);
1731
+ if (value) out.push({ track, value });
1732
+ }
1733
+ return out;
1734
+ }
1735
+ /** The camera pose at a time: position and the rotation the wire format
1736
+ * carries. Shared by `seek()` and `bake()` so the preview cannot aim
1737
+ * somewhere the render does not. */
1738
+ cameraPoseAt(time) {
1739
+ const positions = this.find("camera", "position");
1740
+ const looks = this.find("camera", "lookAt");
1741
+ if (!positions && !looks) return void 0;
1742
+ const position = (positions ? this.valueAt(positions, time) : void 0) ?? this.live("camera", "position") ?? [0, 0, 1];
1743
+ const lookAt = (looks ? this.valueAt(looks, time) : void 0) ?? [
1744
+ ...this.center
1745
+ ];
1746
+ return {
1747
+ position,
1748
+ lookAt,
1749
+ quaternion: lookAtQuaternion(position, lookAt)
1750
+ };
1751
+ }
1752
+ apply(time) {
1753
+ const pose = this.cameraPoseAt(time);
1754
+ const camera = pose ? this.cameraOrThrow("seek()") : void 0;
1755
+ if (pose && camera) {
1756
+ camera.position.set(pose.position[0], pose.position[1], pose.position[2]);
1757
+ const lookable = camera;
1758
+ lookable.lookAt?.(pose.lookAt[0], pose.lookAt[1], pose.lookAt[2]);
1759
+ camera.updateMatrixWorld();
1760
+ }
1761
+ for (const track of this._tracks) {
1762
+ if (track.target === "camera" && (track.property === "position" || track.property === "lookAt")) {
1763
+ continue;
1764
+ }
1765
+ const value = this.valueAt(track, time);
1766
+ if (!value) continue;
1767
+ const definition = propertyDefinition(track.target, track.property);
1768
+ const object = track.target === "camera" ? this.camera : track.target;
1769
+ if (definition && object) definition.write(object, value);
1770
+ }
1771
+ }
1772
+ // -------------------------------------------------------------------------
1773
+ // Baking and serialization
1774
+ // -------------------------------------------------------------------------
1775
+ /**
1776
+ * Every track, sampled at every output frame. The wire format is flat
1777
+ * samples, so the renderer needs no evaluator of its own.
1778
+ */
1779
+ bake(options = {}) {
1780
+ if (this._tracks.some((track) => track.target === "camera"))
1781
+ this.cameraOrThrow("bake()");
1782
+ return bakeTimeline(this, options);
1783
+ }
1784
+ /** The timeline as a storable document. See `TimelineJSON`, and `fromJSON`
1785
+ * for how stored ids find their live objects again. */
1786
+ toJSON() {
1787
+ return {
1788
+ version: 1,
1789
+ duration: this._duration,
1790
+ fps: this._fps,
1791
+ center: this.center,
1792
+ tracks: this._tracks.map((track) => ({
1793
+ target: track.target === "camera" ? { type: "camera" } : { type: "object", objectId: idOf(track.target) },
1794
+ property: track.property,
1795
+ label: track.label,
1796
+ keyframes: track.keyframes.map((keyframe) => ({ ...keyframe }))
1797
+ }))
1798
+ };
1799
+ }
1800
+ /**
1801
+ * Rebuild a timeline against a live scene.
1802
+ *
1803
+ * `resolve` maps a stored object id back to a live object. An id that does
1804
+ * not resolve is refused rather than skipped: a track that silently
1805
+ * disappears is a video missing a motion you authored, and you would find
1806
+ * out by watching the finished render.
1807
+ */
1808
+ static fromJSON(json, context = {}) {
1809
+ const timeline = new _Timeline({
1810
+ duration: json.duration,
1811
+ fps: json.fps,
1812
+ ...json.center ? { lookAt: json.center } : {},
1813
+ ...context.camera ? { camera: context.camera } : {},
1814
+ ...context.scene ? { scene: context.scene } : {}
1815
+ });
1816
+ for (const stored of json.tracks) {
1817
+ let target;
1818
+ if (stored.target.type === "camera") {
1819
+ target = "camera";
1820
+ } else {
1821
+ const object = context.resolve?.(stored.target.objectId) ?? findByUuid(context.scene, stored.target.objectId);
1822
+ if (!object) {
1823
+ throw new Bakery3Error({
1824
+ code: "InvalidRequest",
1825
+ message: `Timeline track "${stored.property}" refers to an object that is not in this scene.`,
1826
+ why: `Object id ${stored.target.objectId} did not resolve. three assigns a new uuid on every reload, so a timeline saved against one session does not match the next one by default.`,
1827
+ fix: "Pass { resolve } to Timeline.fromJSON to map stored ids onto your own stable names.",
1828
+ docs: "https://bakery3.com/docs/animation"
1829
+ });
1830
+ }
1831
+ target = object;
1832
+ }
1833
+ const track = timeline.track(target, stored.property);
1834
+ if (stored.label) track.label = stored.label;
1835
+ track.keyframes = stored.keyframes.map((keyframe) => ({ ...keyframe })).sort((a, b) => a.time - b.time);
1836
+ }
1837
+ return timeline;
1838
+ }
1839
+ // -------------------------------------------------------------------------
1840
+ // Events
1841
+ // -------------------------------------------------------------------------
1842
+ /** Subscribe. Returns the unsubscribe, so a panel's teardown is one call and
1843
+ * a forgotten listener cannot keep a disposed scene alive. */
1844
+ on(event, listener) {
1845
+ const set = this._listeners.get(event) ?? /* @__PURE__ */ new Set();
1846
+ set.add(listener);
1847
+ this._listeners.set(event, set);
1848
+ return () => set.delete(listener);
1849
+ }
1850
+ emit(event, payload) {
1851
+ for (const listener of this._listeners.get(event) ?? []) listener(payload);
1852
+ if (event === "change") {
1853
+ for (const listener of this._listeners.get("time") ?? [])
1854
+ listener(this._time);
1855
+ }
1856
+ }
1857
+ // -------------------------------------------------------------------------
1858
+ // Internals
1859
+ // -------------------------------------------------------------------------
1860
+ /** The live value of a property, read through the shared property table. */
1861
+ live(target, property) {
1862
+ const object = target === "camera" ? this.cameraOrThrow(`Reading the camera's ${property}`) : target;
1863
+ if (!object) return void 0;
1864
+ return propertyDefinition(target, property)?.read(object);
1865
+ }
1866
+ find(target, property) {
1867
+ const resolved = this.normalizeTarget(target);
1868
+ return this._tracks.find(
1869
+ (t) => t.target === resolved && t.property === property
1870
+ );
1871
+ }
1872
+ normalizeTarget(target) {
1873
+ return target === "camera" || this.camera && target === this.camera ? "camera" : target;
1874
+ }
1875
+ span(options) {
1876
+ const at = Math.max(0, options.at ?? 0);
1877
+ const duration = options.duration ?? Math.max(0, this._duration - at);
1878
+ return { at, end: at + duration };
1879
+ }
1880
+ /** Make sure a segment has something to start from at `at`. */
1881
+ anchor(track, at) {
1882
+ if (track.keyframes.some((keyframe) => Math.abs(keyframe.time - at) < 1e-6))
1883
+ return;
1884
+ const value = this.valueAt(track, at) ?? this.live(track.target, track.property);
1885
+ if (value) this.insert(track, { time: at, value });
1886
+ }
1887
+ insert(track, keyframe) {
1888
+ const index = track.keyframes.findIndex(
1889
+ (existing) => Math.abs(existing.time - keyframe.time) < 1e-6
1890
+ );
1891
+ if (index >= 0) {
1892
+ track.keyframes[index] = keyframe;
1893
+ } else {
1894
+ track.keyframes.push(keyframe);
1895
+ track.keyframes.sort((a, b) => a.time - b.time);
1896
+ }
1897
+ return keyframe;
1898
+ }
1899
+ requireCamera(what) {
1900
+ const camera = this.camera;
1901
+ if (!camera) throw noCamera(`${what}()`, contexts.has(this));
1902
+ return camera;
1903
+ }
1904
+ /**
1905
+ * The camera, when one is needed. A client's timeline that has none by then
1906
+ * refuses, because the move it was asked for would otherwise be authored
1907
+ * from a made-up pose, or silently drive nothing. A standalone timeline
1908
+ * with no camera is a document that can still be read and sampled.
1909
+ */
1910
+ cameraOrThrow(what) {
1911
+ const camera = this.camera;
1912
+ if (!camera && contexts.has(this)) throw noCamera(what, true);
1913
+ return camera;
1914
+ }
1915
+ };
1916
+ function noCamera(what, bound) {
1917
+ return new Bakery3Error({
1918
+ code: "InvalidRequest",
1919
+ message: bound ? "This timeline has no camera. Pass { scene, camera } to timeline(), or call bakery3.attach() first." : "This timeline has no camera. Pass { camera } to createTimeline(), or use bakery3.timeline() after bakery3.attach().",
1920
+ why: `${what} needs the camera, and the timeline was given none${bound ? " and none is attached" : ""}.`,
1921
+ fix: bound ? "Pass { scene, camera } to timeline(), or call bakery3.attach({ scene, camera, renderer }) before you author or play a camera move." : "Pass { camera } to createTimeline(), or set timeline.camera.",
1922
+ docs: "https://bakery3.com/docs/animation#move-the-camera"
1923
+ });
1924
+ }
1925
+ function createTimeline(options = {}) {
1926
+ return new Timeline(options);
1927
+ }
1928
+ function normalize3(value, size, property) {
1929
+ const flat = flatten(value, size, property);
1930
+ if (flat && flat.length === size && flat.every((n) => Number.isFinite(n)))
1931
+ return flat;
1932
+ throw new Bakery3Error({
1933
+ code: "InvalidRequest",
1934
+ message: flat ? `"${property}" takes ${size === 1 ? "one number" : `${size} numbers`}, and got ${describe(value)}.` : `Could not read a value for "${property}".`,
1935
+ why: "Expected a number, an array, a hex color, or a three Vector3/Color/Euler, of the size the property has.",
1936
+ fix: size === 1 ? `Pass a single number for "${property}".` : `Pass [x, y, z] or a Vector3 for "${property}", or "#ff2d95" for a color. A bare number is read as a uniform value for scale only.`
1937
+ });
1938
+ }
1939
+ function flatten(value, size, property) {
1940
+ if (typeof value === "number") {
1941
+ return property === "scale" ? new Array(size).fill(value) : [value];
1942
+ }
1943
+ if (typeof value === "string") return hexToLinear(value);
1944
+ if (Array.isArray(value)) return [...value];
1945
+ if ("x" in value) return [value.x, value.y, value.z];
1946
+ if ("r" in value) return [value.r, value.g, value.b];
1947
+ return null;
1948
+ }
1949
+ function describe(value) {
1950
+ if (typeof value === "number") return `the number ${value}`;
1951
+ if (typeof value === "string") return `"${value}"`;
1952
+ if (Array.isArray(value)) return `an array of ${value.length}`;
1953
+ return "x" in value ? "a vector" : "a color";
1954
+ }
1955
+ function resolveCenter(lookAt, fallback) {
1956
+ if (!lookAt) return fallback;
1957
+ if (Array.isArray(lookAt)) return [...lookAt];
1958
+ lookAt.updateMatrixWorld();
1959
+ const e = lookAt.matrixWorld.elements;
1960
+ return [e[12], e[13], e[14]];
1961
+ }
1962
+ function degrees(value) {
1963
+ return value * Math.PI / 180;
1964
+ }
1965
+ function slerp(a, b, t) {
1966
+ let dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
1967
+ let end = [...b];
1968
+ if (dot < 0) {
1969
+ end = [-b[0], -b[1], -b[2], -b[3]];
1970
+ dot = -dot;
1971
+ }
1972
+ if (dot > 0.9995) {
1973
+ const out = a.map((value, i) => value + (end[i] - value) * t);
1974
+ const length = Math.hypot(out[0], out[1], out[2], out[3]) || 1;
1975
+ return out.map((value) => value / length);
1976
+ }
1977
+ const theta = Math.acos(dot);
1978
+ const sin = Math.sin(theta);
1979
+ const wa = Math.sin((1 - t) * theta) / sin;
1980
+ const wb = Math.sin(t * theta) / sin;
1981
+ return a.map((value, i) => value * wa + end[i] * wb);
1982
+ }
1983
+ function labelFor(target) {
1984
+ if (target === "camera") return "camera";
1985
+ return target.name || target.type || "object";
1986
+ }
1987
+ function findByUuid(scene, id) {
1988
+ if (!scene) return void 0;
1989
+ let found;
1990
+ scene.traverse((object) => {
1991
+ if (!found && (idOf(object) === id || object.uuid === id)) found = object;
1992
+ });
1993
+ return found;
1994
+ }
1995
+ function boundingCenter(scene) {
1996
+ let min = null;
1997
+ let max = null;
1998
+ scene.traverse((object) => {
1999
+ const mesh = object;
2000
+ if (!mesh.isMesh) return;
2001
+ object.updateMatrixWorld();
2002
+ const e = object.matrixWorld.elements;
2003
+ const point = [e[12], e[13], e[14]];
2004
+ if (!min || !max) {
2005
+ min = [...point];
2006
+ max = [...point];
2007
+ return;
2008
+ }
2009
+ for (let i = 0; i < 3; i++) {
2010
+ min[i] = Math.min(min[i], point[i]);
2011
+ max[i] = Math.max(max[i], point[i]);
2012
+ }
2013
+ });
2014
+ if (!min || !max) return null;
2015
+ const lo = min;
2016
+ const hi = max;
2017
+ return [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2];
2018
+ }
2019
+ function now() {
2020
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
2021
+ }
2022
+ function schedule(callback) {
2023
+ if (typeof requestAnimationFrame === "function")
2024
+ return requestAnimationFrame(callback);
2025
+ return setTimeout(callback, 16);
2026
+ }
2027
+ function cancel(handle) {
2028
+ if (handle === null) return;
2029
+ if (typeof cancelAnimationFrame === "function" && typeof handle === "number") {
2030
+ cancelAnimationFrame(handle);
2031
+ return;
2032
+ }
2033
+ clearTimeout(handle);
2034
+ }
2035
+
2036
+ // src/timeline/adapters.ts
2037
+ function timelineFromGsap(gsapTimeline, options) {
2038
+ const source = gsapTimeline;
2039
+ const duration = options.duration ?? numberFrom(source.duration);
2040
+ if (!duration || !Number.isFinite(duration)) {
2041
+ throw refusal(
2042
+ "gsap",
2043
+ "the timeline did not report a duration",
2044
+ "Pass { duration }: an infinitely repeating gsap timeline has no length to bake."
2045
+ );
2046
+ }
2047
+ const wasPaused = typeof source.paused === "function" ? source.paused() : true;
2048
+ const before = typeof source.time === "function" ? source.time() : 0;
2049
+ source.pause?.();
2050
+ const seek = (time) => {
2051
+ if (typeof source.seek === "function") source.seek(time, false);
2052
+ else if (typeof source.time === "function") source.time(time, false);
2053
+ else {
2054
+ throw refusal(
2055
+ "gsap",
2056
+ "the object has neither seek() nor time()",
2057
+ "Pass the gsap.timeline() itself rather than a wrapper around it."
2058
+ );
2059
+ }
2060
+ };
2061
+ try {
2062
+ return record(seek, duration, options);
2063
+ } finally {
2064
+ seek(before);
2065
+ if (!wasPaused) source.play?.();
2066
+ }
2067
+ }
2068
+ function timelineFromTheatre(sequence, options) {
2069
+ const source = sequence;
2070
+ const duration = options.duration ?? (typeof source.length === "number" ? source.length : 0);
2071
+ if (!duration || !Number.isFinite(duration)) {
2072
+ throw refusal(
2073
+ "Theatre.js",
2074
+ "the sequence did not report a length",
2075
+ "Pass { duration }, or set the sequence length in the Theatre studio."
2076
+ );
2077
+ }
2078
+ const before = typeof source.position === "number" ? source.position : 0;
2079
+ source.pause?.();
2080
+ const seek = (time) => {
2081
+ source.position = time;
2082
+ };
2083
+ try {
2084
+ return record(seek, duration, options);
2085
+ } finally {
2086
+ seek(before);
2087
+ }
2088
+ }
2089
+ function record(seek, duration, options) {
2090
+ const fps = Math.max(1, Math.round(options.fps ?? 30));
2091
+ const frames = Math.max(1, Math.round(duration * fps));
2092
+ const timeline = new Timeline({
2093
+ duration,
2094
+ fps,
2095
+ ...options.camera ? { camera: options.camera } : {},
2096
+ ...options.scene ? { scene: options.scene } : {}
2097
+ });
2098
+ const tracks = options.tracks.map((spec) => {
2099
+ const track = timeline.track(spec.object, spec.property);
2100
+ if (spec.label) track.label = spec.label;
2101
+ return { spec, track };
2102
+ });
2103
+ for (let frame = 0; frame < frames; frame++) {
2104
+ const time = frame / fps;
2105
+ seek(time);
2106
+ for (const { spec, track } of tracks) {
2107
+ const value = readLive(timeline, spec);
2108
+ if (!value) continue;
2109
+ track.keyframes.push({ time, value });
2110
+ }
2111
+ }
2112
+ for (const { track } of tracks)
2113
+ track.keyframes.sort((a, b) => a.time - b.time);
2114
+ timeline.seek(0);
2115
+ return timeline;
2116
+ }
2117
+ function readLive(timeline, spec) {
2118
+ if (spec.object === "camera" && spec.property === "lookAt") {
2119
+ return timeline.camera ? readCameraLookAt(timeline.camera, timeline.center) : void 0;
2120
+ }
2121
+ const value = timeline.live(spec.object, spec.property);
2122
+ return value ? [...value] : void 0;
2123
+ }
2124
+ function numberFrom(value) {
2125
+ if (typeof value === "function") return value();
2126
+ return typeof value === "number" ? value : 0;
2127
+ }
2128
+ function refusal(library, why, fix) {
2129
+ return new Bakery3Error({
2130
+ code: "InvalidRequest",
2131
+ message: `Could not sample the ${library} timeline.`,
2132
+ why,
2133
+ fix,
2134
+ docs: "https://bakery3.com/docs/animation#adapters"
2135
+ });
2136
+ }
2137
+
2138
+ // src/shader/extract.ts
2139
+ var DOCS = "https://bakery3.com/docs";
2140
+ var OPERATORS = {
2141
+ "+": "add",
2142
+ "-": "sub",
2143
+ "*": "mul",
2144
+ "/": "div",
2145
+ "%": "mod",
2146
+ "<": "less_than",
2147
+ ">": "greater_than",
2148
+ "<=": "less_equal",
2149
+ ">=": "greater_equal",
2150
+ "==": "equal",
2151
+ "!=": "not_equal",
2152
+ "&&": "and",
2153
+ "||": "or"
2154
+ };
2155
+ var MATH_METHODS = {
2156
+ radians: "radians",
2157
+ degrees: "degrees",
2158
+ exp: "exp",
2159
+ exp2: "exp2",
2160
+ log: "log",
2161
+ log2: "log2",
2162
+ sqrt: "sqrt",
2163
+ inversesqrt: "inverse_sqrt",
2164
+ floor: "floor",
2165
+ ceil: "ceil",
2166
+ normalize: "normalize",
2167
+ fract: "fract",
2168
+ sin: "sin",
2169
+ cos: "cos",
2170
+ tan: "tan",
2171
+ asin: "asin",
2172
+ acos: "acos",
2173
+ abs: "abs",
2174
+ sign: "sign",
2175
+ length: "length",
2176
+ negate: "negate",
2177
+ oneMinus: "one_minus",
2178
+ round: "round",
2179
+ reciprocal: "reciprocal",
2180
+ trunc: "trunc",
2181
+ min: "min",
2182
+ max: "max",
2183
+ step: "step",
2184
+ reflect: "reflect",
2185
+ distance: "distance",
2186
+ dot: "dot",
2187
+ cross: "cross",
2188
+ pow: "pow",
2189
+ mix: "mix",
2190
+ clamp: "clamp",
2191
+ refract: "refract",
2192
+ smoothstep: "smoothstep",
2193
+ faceforward: "faceforward",
2194
+ saturate: "saturate"
2195
+ };
2196
+ var MATH_REFUSED = {
2197
+ dFdx: "Screen-space derivatives do not exist in a path tracer: there is no screen-space neighbor to difference against.",
2198
+ dFdy: "Screen-space derivatives do not exist in a path tracer: there is no screen-space neighbor to difference against.",
2199
+ fwidth: "Screen-space derivatives do not exist in a path tracer: there is no screen-space neighbor to difference against."
2200
+ };
2201
+ var NAMED_BUILTINS = {
2202
+ normalView: { op: "normal", params: { space: "view" } },
2203
+ normalWorld: { op: "normal", params: { space: "world" } },
2204
+ normalLocal: { op: "normal", params: { space: "local" } },
2205
+ positionLocal: { op: "position", params: { space: "local" } },
2206
+ positionWorld: { op: "position", params: { space: "world" } },
2207
+ positionView: { op: "position", params: { space: "view" } },
2208
+ positionViewDirection: { op: "view_direction" }
2209
+ };
2210
+ var NAMED_FUNCTIONS = {
2211
+ mx_perlin_noise_float: { op: "mx_noise", inputCount: 1 },
2212
+ mx_perlin_noise_vec3: {
2213
+ op: "mx_noise",
2214
+ params: { vector: 1 },
2215
+ inputCount: 1
2216
+ },
2217
+ mx_fractal_noise_float: { op: "mx_fractal_noise", inputCount: 4 },
2218
+ mx_fractal_noise_vec2: {
2219
+ op: "mx_fractal_noise",
2220
+ params: { vector: 1 },
2221
+ inputCount: 4
2222
+ },
2223
+ mx_fractal_noise_vec3: {
2224
+ op: "mx_fractal_noise",
2225
+ params: { vector: 1 },
2226
+ inputCount: 4
2227
+ },
2228
+ mx_fractal_noise_vec4: {
2229
+ op: "mx_fractal_noise",
2230
+ params: { vector: 1 },
2231
+ inputCount: 4
2232
+ },
2233
+ mx_worley_noise_float: { op: "mx_worley_noise", inputCount: 2, planar: true },
2234
+ // The squared distances to the nearest two, and three, points.
2235
+ mx_worley_noise_vec2: {
2236
+ op: "mx_worley_noise",
2237
+ params: { vector: 1, features: 2 },
2238
+ inputCount: 2,
2239
+ planar: true
2240
+ },
2241
+ mx_worley_noise_vec3: {
2242
+ op: "mx_worley_noise",
2243
+ params: { vector: 1, features: 3 },
2244
+ inputCount: 2,
2245
+ planar: true
2246
+ },
2247
+ mx_cell_noise_float: { op: "mx_cell_noise", inputCount: 1 }
2248
+ };
2249
+ var TYPE_WIDTH = {
2250
+ float: 1,
2251
+ int: 1,
2252
+ uint: 1,
2253
+ bool: 1,
2254
+ vec2: 2,
2255
+ vec3: 3,
2256
+ color: 3,
2257
+ vec4: 4
2258
+ };
2259
+ var widest = (widths) => widths.length && widths.every((w) => w !== void 0) ? Math.max(...widths) : void 0;
2260
+ function outputWidth(op, inputs, params) {
2261
+ switch (op) {
2262
+ case "const":
2263
+ case "uniform": {
2264
+ const type = TYPE_WIDTH[String(params?.type)];
2265
+ if (type) return type;
2266
+ return Array.isArray(params?.value) ? params.value.length : 1;
2267
+ }
2268
+ case "uv":
2269
+ return 2;
2270
+ case "position":
2271
+ case "normal":
2272
+ case "view_direction":
2273
+ case "camera_position":
2274
+ case "normal_map":
2275
+ case "bump_map":
2276
+ case "cross":
2277
+ return 3;
2278
+ case "vertex_color":
2279
+ case "texture":
2280
+ return 4;
2281
+ case "dot":
2282
+ case "length":
2283
+ case "distance":
2284
+ case "luminance":
2285
+ case "checker":
2286
+ return 1;
2287
+ case "swizzle":
2288
+ return String(params?.components ?? "x").length;
2289
+ case "convert":
2290
+ return TYPE_WIDTH[String(params?.to)];
2291
+ case "join":
2292
+ return inputs.every((w) => w !== void 0) ? inputs.reduce((sum, w) => sum + w, 0) : void 0;
2293
+ case "select":
2294
+ return widest(inputs.slice(1));
2295
+ case "reflect":
2296
+ case "refract":
2297
+ case "faceforward":
2298
+ case "rotate":
2299
+ case "remap":
2300
+ return inputs[0];
2301
+ }
2302
+ if (op.startsWith("mx_") || op.startsWith("bk_")) return void 0;
2303
+ return widest(inputs);
2304
+ }
2305
+ var Untranslatable = class extends Error {
2306
+ constructor(issue) {
2307
+ super(issue.message);
2308
+ this.issue = issue;
2309
+ }
2310
+ issue;
2311
+ };
2312
+ var GraphWriter = class {
2313
+ nodes = [];
2314
+ issues = [];
2315
+ textures = /* @__PURE__ */ new Map();
2316
+ /** Ops seen, for the once-per-material approximate/bakery notes. */
2317
+ opsUsed = /* @__PURE__ */ new Set();
2318
+ memo = /* @__PURE__ */ new Map();
2319
+ nextId = 0;
2320
+ /** Fn-expansion results, cached so a shared call node stays shared. */
2321
+ expanded = /* @__PURE__ */ new Map();
2322
+ /** Components in each emitted value, where they can be known (`outputWidth`). */
2323
+ widths = /* @__PURE__ */ new Map();
2324
+ emit(op, inputs, params) {
2325
+ const id = `n${this.nextId++}`;
2326
+ this.opsUsed.add(op);
2327
+ this.nodes.push({
2328
+ id,
2329
+ op,
2330
+ ...inputs && inputs.length ? { inputs } : {},
2331
+ ...params && Object.keys(params).length ? { params } : {}
2332
+ });
2333
+ const width = outputWidth(
2334
+ op,
2335
+ (inputs ?? []).map(
2336
+ (input) => input ? this.widths.get(input) : void 0
2337
+ ),
2338
+ params
2339
+ );
2340
+ if (width) this.widths.set(id, width);
2341
+ return id;
2342
+ }
2343
+ constant(value, type) {
2344
+ return this.emit("const", void 0, { value, type });
2345
+ }
2346
+ refuse(code, message, docsAnchor = "shader-nodes") {
2347
+ throw new Untranslatable({
2348
+ code,
2349
+ severity: "error",
2350
+ message,
2351
+ docs: `${DOCS}/${docsAnchor}`
2352
+ });
2353
+ }
2354
+ /**
2355
+ * The walk. Returns the IR id of the node's value.
2356
+ *
2357
+ * Dispatch order matters: the bakery-node marker and the uuid registry
2358
+ * come before structure, because a marked node's TSL subtree is only its
2359
+ * browser preview and must not be extracted.
2360
+ */
2361
+ visit(input) {
2362
+ const node = unwrap(input);
2363
+ if (node === null || node === void 0) {
2364
+ this.refuse("shader_node_missing", "A node input was null.");
2365
+ }
2366
+ if (typeof node === "number") return this.constant(node, "float");
2367
+ if (typeof node === "boolean") return this.constant(node ? 1 : 0, "float");
2368
+ const tsl = node;
2369
+ if (!tsl.isNode) {
2370
+ const value = serializeValue(tsl);
2371
+ if (value) return this.constant(value.value, value.type);
2372
+ this.refuse(
2373
+ "shader_node_unknown",
2374
+ `A shader input was not a node (${describe2(tsl)}).`
2375
+ );
2376
+ }
2377
+ const seen = this.memo.get(tsl.uuid);
2378
+ if (seen) return seen;
2379
+ const id = this.visitFresh(tsl);
2380
+ this.memo.set(tsl.uuid, id);
2381
+ return id;
2382
+ }
2383
+ visitFresh(node) {
2384
+ const marker = node[BAKERY_NODE_MARKER];
2385
+ if (marker) {
2386
+ const inputs = (marker.inputs ?? []).map((input) => this.visit(input));
2387
+ return this.emit(marker.op, inputs, marker.params);
2388
+ }
2389
+ const builtin = builtinFor(node.uuid);
2390
+ if (builtin) {
2391
+ return this.emit(builtin.op, void 0, builtin.params?.());
2392
+ }
2393
+ const type = node.type ?? "Node";
2394
+ switch (type) {
2395
+ case "ConstNode":
2396
+ return this.emitValue(node, "const");
2397
+ case "UniformNode": {
2398
+ return this.emitValue(node, "uniform");
2399
+ }
2400
+ case "OperatorNode": {
2401
+ const op = OPERATORS[node.op ?? ""];
2402
+ if (!op) {
2403
+ this.refuse(
2404
+ "shader_node_unsupported",
2405
+ `The "${node.op}" operator cannot be translated: bitwise operations do not exist as shader nodes.`
2406
+ );
2407
+ }
2408
+ return this.emit(op, [this.visit(node.aNode), this.visit(node.bNode)]);
2409
+ }
2410
+ case "MathNode": {
2411
+ const method = node.method ?? "";
2412
+ const refused = MATH_REFUSED[method];
2413
+ if (refused) {
2414
+ this.refuse(
2415
+ "shader_node_unsupported",
2416
+ `\`${method}()\` cannot be translated. ${refused}`
2417
+ );
2418
+ }
2419
+ if (method === "atan" && node.bNode) {
2420
+ return this.emit("atan2", [
2421
+ this.visit(node.aNode),
2422
+ this.visit(node.bNode)
2423
+ ]);
2424
+ }
2425
+ const op = MATH_METHODS[method];
2426
+ if (!op) {
2427
+ this.refuse(
2428
+ "shader_node_unsupported",
2429
+ `The \`${method}()\` math function has no render-engine equivalent.`
2430
+ );
2431
+ }
2432
+ const inputs = [node.aNode, node.bNode, node.cNode].filter((n) => n !== null && n !== void 0).map((n) => this.visit(n));
2433
+ return this.emit(op, inputs);
2434
+ }
2435
+ case "ConditionalNode":
2436
+ return this.emit("select", [
2437
+ this.visit(node.condNode),
2438
+ this.visit(node.ifNode),
2439
+ node.elseNode ? this.visit(node.elseNode) : this.constant(0, "float")
2440
+ ]);
2441
+ case "RemapNode":
2442
+ return this.emit(
2443
+ "remap",
2444
+ [
2445
+ this.visit(node.node),
2446
+ this.visit(node.inLowNode),
2447
+ this.visit(node.inHighNode),
2448
+ this.visit(node.outLowNode),
2449
+ this.visit(node.outHighNode)
2450
+ ],
2451
+ { clamp: node.doClamp ?? false }
2452
+ );
2453
+ case "SplitNode": {
2454
+ const components = node.components ?? "x";
2455
+ if (!/^[xyzwrgba]{1,4}$/.test(components)) {
2456
+ this.refuse(
2457
+ "shader_node_unsupported",
2458
+ `Swizzle ".${components}" is not translatable.`
2459
+ );
2460
+ }
2461
+ return this.emit("swizzle", [this.visit(node.node)], { components });
2462
+ }
2463
+ case "JoinNode": {
2464
+ const parts = (node.nodes ?? []).map((n) => this.visit(n));
2465
+ if (parts.length < 2 || parts.length > 4) {
2466
+ this.refuse(
2467
+ "shader_node_unsupported",
2468
+ `join() of ${parts.length} components is not translatable.`
2469
+ );
2470
+ }
2471
+ return this.emit("join", parts);
2472
+ }
2473
+ case "ConvertNode": {
2474
+ const inner = this.visit(node.node);
2475
+ const width = this.widths.get(inner);
2476
+ const options = (node.convertTo ?? "").split("|");
2477
+ let to = options[0];
2478
+ for (const option of options)
2479
+ if (width !== void 0 && TYPE_WIDTH[option] === width) to = option;
2480
+ const id = this.emit("convert", [inner], { to });
2481
+ if (options.length > 1 && width === void 0) this.widths.delete(id);
2482
+ return id;
2483
+ }
2484
+ case "FlipNode":
2485
+ return this.emit("one_minus", [this.visit(node.node)]);
2486
+ case "RotateNode":
2487
+ return this.emit("rotate", [
2488
+ this.visit(node.positionNode),
2489
+ this.constant([0, 0, 1], "vec3"),
2490
+ this.visit(node.rotationNode)
2491
+ ]);
2492
+ case "NormalMapNode":
2493
+ return this.emit("normal_map", [
2494
+ this.visit(node.node),
2495
+ node.scaleNode ? this.visit(node.scaleNode) : this.constant(1, "float")
2496
+ ]);
2497
+ case "BumpMapNode":
2498
+ return this.emit("bump_map", [
2499
+ this.visit(node.textureNode),
2500
+ node.scaleNode ? this.visit(node.scaleNode) : this.constant(1, "float")
2501
+ ]);
2502
+ case "AttributeNode":
2503
+ return this.emitAttribute(node);
2504
+ case "VertexColorNode":
2505
+ return this.emit("vertex_color");
2506
+ case "TextureNode":
2507
+ return this.emitTexture(node);
2508
+ case "VarNode":
2509
+ case "VaryingNode": {
2510
+ const named = node.name ? NAMED_BUILTINS[node.name] : void 0;
2511
+ if (named) return this.emit(named.op, void 0, named.params);
2512
+ if (node.node) return this.visit(node.node);
2513
+ this.refuse(
2514
+ "shader_node_unsupported",
2515
+ `Variable "${node.name ?? "unnamed"}" has no source node.`
2516
+ );
2517
+ }
2518
+ // Purely organisational wrappers: the value is the wrapped node.
2519
+ case "ContextNode":
2520
+ case "CacheNode":
2521
+ case "DebugNode":
2522
+ if (node.node) return this.visit(node.node);
2523
+ this.refuse("shader_node_unsupported", `${type} has no inner node.`);
2524
+ break;
2525
+ // overloadingFn([...]): how MaterialX's noise family is published.
2526
+ // The variants share a base name ('mx_perlin_noise_float_0/_1'), which
2527
+ // is what the catalog knows them by.
2528
+ case "FunctionOverloadingNode": {
2529
+ const first = (node.functionNodes ?? [])[0];
2530
+ const layout = first?.shaderNode?.layout?.name;
2531
+ const base = layout?.replace(/_\d+$/, "");
2532
+ const named = base ? NAMED_FUNCTIONS[base] : void 0;
2533
+ if (named) return this.emitNamed(named, base, node.parametersNodes);
2534
+ this.refuse(
2535
+ "shader_fn_opaque",
2536
+ `The overloaded shader function ${layout ?? "(unnamed)"} has no render-engine equivalent.`
2537
+ );
2538
+ break;
2539
+ }
2540
+ }
2541
+ if (node.isShaderCallNodeInternal || node.shaderNode) {
2542
+ return this.emitFnCall(node);
2543
+ }
2544
+ this.refuse(
2545
+ "shader_node_unsupported",
2546
+ `${describeNodeType(type)} cannot be translated.`
2547
+ );
2548
+ }
2549
+ emitValue(node, op) {
2550
+ const serialized = serializeValue(node.value);
2551
+ if (!serialized) {
2552
+ this.refuse(
2553
+ "shader_node_unsupported",
2554
+ op === "uniform" ? `A uniform holds a ${describe2(node.value)}, which cannot travel as a shader value (matrices and textures cannot).` : `A constant holds a ${describe2(node.value)}, which cannot travel as a shader value.`
2555
+ );
2556
+ }
2557
+ const params = {
2558
+ value: serialized.value,
2559
+ type: serialized.type
2560
+ };
2561
+ if (op === "uniform" && node.name) params.name = node.name;
2562
+ return this.emit(op, void 0, params);
2563
+ }
2564
+ emitAttribute(node) {
2565
+ const name = node._attributeName ?? "";
2566
+ const uvIndex = /^uv(\d?)$/.exec(name);
2567
+ if (uvIndex) {
2568
+ return this.emit("uv", void 0, {
2569
+ index: uvIndex[1] ? Number(uvIndex[1]) : 0
2570
+ });
2571
+ }
2572
+ if (name === "position")
2573
+ return this.emit("position", void 0, { space: "local" });
2574
+ if (name === "normal")
2575
+ return this.emit("normal", void 0, { space: "local" });
2576
+ if (name === "color") return this.emit("vertex_color");
2577
+ this.refuse(
2578
+ "shader_attribute_unsupported",
2579
+ `The custom geometry attribute "${name}" does not survive glTF export, so the renderer cannot read it. Rename it to a COLOR_0/TEXCOORD channel or bake it to a texture.`
2580
+ );
2581
+ }
2582
+ emitTexture(node) {
2583
+ const texture = node.value;
2584
+ if (!texture || !texture.isTexture) {
2585
+ this.refuse(
2586
+ "shader_texture_missing",
2587
+ "A texture node has no texture assigned."
2588
+ );
2589
+ }
2590
+ const uvInput = node.uvNode ? this.visit(node.uvNode) : this.emit("uv", void 0, { index: 0 });
2591
+ return this.sampleTexture(texture, uvInput);
2592
+ }
2593
+ /** A texture sample from a known texture; also used for the static maps. */
2594
+ sampleTexture(texture, uvInput) {
2595
+ const uv = uvInput ?? this.emit("uv", void 0, { index: 0 });
2596
+ const params = {
2597
+ image: "",
2598
+ colorSpace: texture.colorSpace === "srgb" ? "srgb" : "linear",
2599
+ wrap: wrapMode(texture),
2600
+ filter: texture.magFilter === 1003 ? "nearest" : "linear",
2601
+ // three's default. A GLTFLoader texture has flipY false, and the
2602
+ // renderer compensates with a V flip so both sample the image the same
2603
+ // way up.
2604
+ flipY: texture.flipY !== false
2605
+ };
2606
+ const id = this.emit("texture", [uv], params);
2607
+ const existing = this.textures.get(texture.uuid);
2608
+ if (existing) {
2609
+ const previous = existing.apply;
2610
+ existing.apply = (image) => {
2611
+ previous(image);
2612
+ params.image = image;
2613
+ };
2614
+ } else {
2615
+ this.textures.set(texture.uuid, {
2616
+ texture,
2617
+ apply: (image) => {
2618
+ params.image = image;
2619
+ }
2620
+ });
2621
+ }
2622
+ return id;
2623
+ }
2624
+ emitFnCall(node) {
2625
+ const layoutName = node.shaderNode?.layout?.name;
2626
+ const named = layoutName ? NAMED_FUNCTIONS[layoutName] ?? NAMED_FUNCTIONS[layoutName.replace(/_\d+$/, "")] : void 0;
2627
+ if (named) {
2628
+ return this.emitNamed(
2629
+ named,
2630
+ layoutName.replace(/_\d+$/, ""),
2631
+ Array.isArray(node.rawInputs) ? node.rawInputs : [],
2632
+ // A variant called directly declares its coordinate's type.
2633
+ TYPE_WIDTH[node.shaderNode?.layout?.inputs?.[0]?.type ?? ""]
2634
+ );
2635
+ }
2636
+ const expandedNode = this.expand(node);
2637
+ if (expandedNode) return this.visit(expandedNode);
2638
+ const label = layoutName ?? "an anonymous Fn()";
2639
+ this.refuse(
2640
+ "shader_fn_opaque",
2641
+ `The shader function ${label} could not be analyzed: it depends on renderer state the SDK cannot see.` + (tslRuntimeRegistered() ? "" : ` If it is a three.js builtin, importing '@oeave/bakery3/tsl' teaches the checker to recognize it.`)
2642
+ );
2643
+ }
2644
+ /**
2645
+ * A library function by its base name ('mx_worley_noise_vec2'). The name
2646
+ * says how wide its value is; a `planar` one records a 2D coordinate.
2647
+ */
2648
+ emitNamed(named, base, raw = [], declared) {
2649
+ const inputs = raw.slice(0, named.inputCount ?? raw.length).map((input) => this.visit(input));
2650
+ const params = { ...named.params };
2651
+ const coordinate = declared ?? (inputs[0] ? this.widths.get(inputs[0]) : void 0);
2652
+ if (named.planar && coordinate === 2) params.dimensions = 2;
2653
+ const id = this.emit(named.op, inputs, params);
2654
+ const width = TYPE_WIDTH[base.slice(base.lastIndexOf("_") + 1)];
2655
+ if (width) this.widths.set(id, width);
2656
+ return id;
2657
+ }
2658
+ expand(node) {
2659
+ const cached = this.expanded.get(node.uuid);
2660
+ if (cached) return cached;
2661
+ const jsFunc = node.shaderNode?.jsFunc;
2662
+ if (typeof jsFunc !== "function") return null;
2663
+ try {
2664
+ const raw = node.rawInputs;
2665
+ const inputs = Array.isArray(raw) ? raw.map(toNodeObject) : raw;
2666
+ const result = unwrap(
2667
+ jsFunc(Array.isArray(inputs) ? inputs : inputs ?? [])
2668
+ );
2669
+ if (result && result.isNode) {
2670
+ this.expanded.set(node.uuid, result);
2671
+ return result;
2672
+ }
2673
+ return null;
2674
+ } catch {
2675
+ return null;
2676
+ }
2677
+ }
2678
+ };
2679
+ function isNodeMaterial(material) {
2680
+ return Boolean(material?.isNodeMaterial);
2681
+ }
2682
+ var SLOT_NODES = [
2683
+ ["color", "colorNode"],
2684
+ ["opacity", "opacityNode"],
2685
+ ["alphaTest", "alphaTestNode"],
2686
+ ["roughness", "roughnessNode"],
2687
+ ["metalness", "metalnessNode"],
2688
+ ["normal", "normalNode"],
2689
+ ["emissive", "emissiveNode"],
2690
+ ["ao", "aoNode"],
2691
+ ["clearcoat", "clearcoatNode"],
2692
+ ["clearcoatRoughness", "clearcoatRoughnessNode"],
2693
+ ["sheen", "sheenNode"],
2694
+ ["sheenRoughness", "sheenRoughnessNode"],
2695
+ ["iridescence", "iridescenceNode"],
2696
+ ["iridescenceIOR", "iridescenceIORNode"],
2697
+ ["iridescenceThickness", "iridescenceThicknessNode"],
2698
+ ["transmission", "transmissionNode"],
2699
+ ["thickness", "thicknessNode"],
2700
+ ["ior", "iorNode"],
2701
+ ["specularIntensity", "specularIntensityNode"],
2702
+ ["specularColor", "specularColorNode"],
2703
+ ["anisotropy", "anisotropyNode"]
2704
+ ];
2705
+ var REFUSED_SLOTS = [
2706
+ [
2707
+ "fragmentNode",
2708
+ "fragmentNode replaces the whole fragment stage; there is no BSDF left to translate."
2709
+ ],
2710
+ [
2711
+ "outputNode",
2712
+ "outputNode replaces the material output; there is no BSDF left to translate."
2713
+ ],
2714
+ [
2715
+ "positionNode",
2716
+ "positionNode displaces vertices at shade time; true displacement is not wired up yet."
2717
+ ],
2718
+ [
2719
+ "backdropNode",
2720
+ "backdropNode is a screen-space effect with no path-traced equivalent."
2721
+ ],
2722
+ [
2723
+ "depthNode",
2724
+ "depthNode overrides rasterizer depth, which a path tracer does not have."
2725
+ ],
2726
+ [
2727
+ "envNode",
2728
+ "A per-material environment override is not translated; the scene environment is used."
2729
+ ]
2730
+ ];
2731
+ function extractMaterialShader(material) {
2732
+ const writer = new GraphWriter();
2733
+ const outputs = {};
2734
+ const supportedSlots = [];
2735
+ const failedSlots = [];
2736
+ for (const [prop, reason] of REFUSED_SLOTS) {
2737
+ if (material[prop]) {
2738
+ writer.issues.push({
2739
+ code: "shader_slot_unsupported",
2740
+ severity: prop === "envNode" ? "warning" : "error",
2741
+ message: `${material.type ?? "NodeMaterial"} sets \`${prop}\`. ${reason}`,
2742
+ docs: `${DOCS}/shader-nodes#slots`
2743
+ });
2744
+ }
2745
+ }
2746
+ for (const [slot, prop] of SLOT_NODES) {
2747
+ const driver = material[prop];
2748
+ if (!driver) continue;
2749
+ try {
2750
+ outputs[slot] = writer.visit(driver);
2751
+ supportedSlots.push(slot);
2752
+ } catch (error) {
2753
+ if (!(error instanceof Untranslatable)) throw error;
2754
+ failedSlots.push(slot);
2755
+ writer.issues.push({
2756
+ ...error.issue,
2757
+ message: `In \`${prop}\`: ${error.issue.message}`
2758
+ });
2759
+ }
2760
+ }
2761
+ synthesizeStaticMaps(material, writer, outputs, supportedSlots);
2762
+ const hasGraph = Object.keys(outputs).length > 0;
2763
+ const nodes = pruneUnreachable(writer.nodes, outputs);
2764
+ reportTiers(writer);
2765
+ return {
2766
+ spec: hasGraph ? {
2767
+ materialId: idOf(material),
2768
+ ...material.name ? { name: material.name } : {},
2769
+ base: material.isMeshPhysicalNodeMaterial ? "physical" : material.isMeshBasicNodeMaterial ? "basic" : "standard",
2770
+ properties: staticProperties(material),
2771
+ graph: {
2772
+ version: SHADER_GRAPH_VERSION,
2773
+ nodes,
2774
+ outputs
2775
+ }
2776
+ } : null,
2777
+ supportedSlots,
2778
+ failedSlots,
2779
+ issues: writer.issues,
2780
+ textures: [...writer.textures.values()]
2781
+ };
2782
+ }
2783
+ function synthesizeStaticMaps(material, writer, outputs, supportedSlots) {
2784
+ const texture = (prop) => {
2785
+ const value = material[prop];
2786
+ return value && value.isTexture ? value : null;
2787
+ };
2788
+ const claim = (slot, id) => {
2789
+ outputs[slot] = id;
2790
+ supportedSlots.push(slot);
2791
+ };
2792
+ const sample2 = (map) => writer.sampleTexture(map);
2793
+ try {
2794
+ const map = texture("map");
2795
+ if (map && !outputs.color) {
2796
+ const color = serializeValue(material.color) ?? {
2797
+ value: [1, 1, 1],
2798
+ type: "color"
2799
+ };
2800
+ claim(
2801
+ "color",
2802
+ writer.emit("mul", [
2803
+ sample2(map),
2804
+ writer.constant(color.value, "color")
2805
+ ])
2806
+ );
2807
+ }
2808
+ const roughnessMap = texture("roughnessMap");
2809
+ if (roughnessMap && !outputs.roughness) {
2810
+ const channel = writer.emit("swizzle", [sample2(roughnessMap)], {
2811
+ components: "g"
2812
+ });
2813
+ claim(
2814
+ "roughness",
2815
+ writer.emit("mul", [
2816
+ channel,
2817
+ writer.constant(num(material.roughness, 1), "float")
2818
+ ])
2819
+ );
2820
+ }
2821
+ const metalnessMap = texture("metalnessMap");
2822
+ if (metalnessMap && !outputs.metalness) {
2823
+ const channel = writer.emit("swizzle", [sample2(metalnessMap)], {
2824
+ components: "b"
2825
+ });
2826
+ claim(
2827
+ "metalness",
2828
+ writer.emit("mul", [
2829
+ channel,
2830
+ writer.constant(num(material.metalness, 0), "float")
2831
+ ])
2832
+ );
2833
+ }
2834
+ const emissiveMap = texture("emissiveMap");
2835
+ if (emissiveMap && !outputs.emissive) {
2836
+ const emissive = serializeValue(material.emissive) ?? {
2837
+ value: [0, 0, 0],
2838
+ type: "color"
2839
+ };
2840
+ claim(
2841
+ "emissive",
2842
+ writer.emit("mul", [
2843
+ sample2(emissiveMap),
2844
+ writer.constant(emissive.value, "color")
2845
+ ])
2846
+ );
2847
+ }
2848
+ const alphaMap = texture("alphaMap");
2849
+ if (alphaMap && !outputs.opacity) {
2850
+ const channel = writer.emit("swizzle", [sample2(alphaMap)], {
2851
+ components: "g"
2852
+ });
2853
+ claim(
2854
+ "opacity",
2855
+ writer.emit("mul", [
2856
+ channel,
2857
+ writer.constant(num(material.opacity, 1), "float")
2858
+ ])
2859
+ );
2860
+ }
2861
+ const normalMap = texture("normalMap");
2862
+ if (normalMap && !outputs.normal) {
2863
+ const scale = material.normalScale;
2864
+ claim(
2865
+ "normal",
2866
+ writer.emit("normal_map", [
2867
+ sample2(normalMap),
2868
+ writer.constant(num(scale?.x, 1), "float")
2869
+ ])
2870
+ );
2871
+ }
2872
+ const aoMap = texture("aoMap");
2873
+ if (aoMap && !outputs.ao) {
2874
+ const channel = writer.emit("swizzle", [sample2(aoMap)], {
2875
+ components: "r"
2876
+ });
2877
+ const intensity = writer.constant(
2878
+ num(material.aoMapIntensity, 1),
2879
+ "float"
2880
+ );
2881
+ const one = writer.constant(1, "float");
2882
+ const delta = writer.emit("sub", [channel, one]);
2883
+ claim(
2884
+ "ao",
2885
+ writer.emit("add", [one, writer.emit("mul", [intensity, delta])])
2886
+ );
2887
+ }
2888
+ } catch (error) {
2889
+ if (!(error instanceof Untranslatable)) throw error;
2890
+ writer.issues.push(error.issue);
2891
+ }
2892
+ }
2893
+ function pruneUnreachable(nodes, outputs) {
2894
+ const byId = new Map(nodes.map((node) => [node.id, node]));
2895
+ const reachable = /* @__PURE__ */ new Set();
2896
+ const queue = Object.values(outputs).filter(
2897
+ (id) => Boolean(id)
2898
+ );
2899
+ while (queue.length) {
2900
+ const id = queue.pop();
2901
+ if (reachable.has(id)) continue;
2902
+ reachable.add(id);
2903
+ for (const input of byId.get(id)?.inputs ?? []) queue.push(input);
2904
+ }
2905
+ return nodes.filter((node) => reachable.has(node.id));
2906
+ }
2907
+ function reportTiers(writer) {
2908
+ const bakeryOps = [];
2909
+ for (const op of writer.opsUsed) {
2910
+ const info = SHADER_OPS[op];
2911
+ if (!info) continue;
2912
+ if (info.tier === "approximate" && info.note) {
2913
+ writer.issues.push({
2914
+ code: "shader_op_approximated",
2915
+ severity: "warning",
2916
+ message: info.note,
2917
+ docs: `${DOCS}/shader-nodes#${op}`
2918
+ });
2919
+ }
2920
+ if (info.tier === "bakery") bakeryOps.push(op);
2921
+ }
2922
+ if (bakeryOps.length) {
2923
+ writer.issues.push({
2924
+ code: "shader_bakery_nodes",
2925
+ severity: "info",
2926
+ message: `Uses ${bakeryOps.length} bakery-only node(s) (${bakeryOps.join(", ")}). The viewport shows an approximation; the render is the real one.`,
2927
+ docs: `${DOCS}/shader-nodes#bakery-only`
2928
+ });
2929
+ }
2930
+ }
2931
+ function staticProperties(material) {
2932
+ const properties = {};
2933
+ const color = (key, prop) => {
2934
+ const value = material[prop];
2935
+ if (value?.isColor && value.getHexString) {
2936
+ properties[key] = `#${value.getHexString()}`;
2937
+ }
2938
+ };
2939
+ const scalar2 = (key, prop) => {
2940
+ const value = material[prop];
2941
+ if (typeof value === "number" && Number.isFinite(value)) {
2942
+ properties[key] = value;
2943
+ }
2944
+ };
2945
+ color("color", "color");
2946
+ color("emissive", "emissive");
2947
+ color("sheenColor", "sheenColor");
2948
+ color("specularColor", "specularColor");
2949
+ color("attenuationColor", "attenuationColor");
2950
+ scalar2("roughness", "roughness");
2951
+ scalar2("metalness", "metalness");
2952
+ scalar2("emissiveIntensity", "emissiveIntensity");
2953
+ scalar2("opacity", "opacity");
2954
+ scalar2("alphaTest", "alphaTest");
2955
+ scalar2("ior", "ior");
2956
+ scalar2("transmission", "transmission");
2957
+ scalar2("thickness", "thickness");
2958
+ scalar2("attenuationDistance", "attenuationDistance");
2959
+ scalar2("clearcoat", "clearcoat");
2960
+ scalar2("clearcoatRoughness", "clearcoatRoughness");
2961
+ scalar2("sheen", "sheen");
2962
+ scalar2("sheenRoughness", "sheenRoughness");
2963
+ scalar2("specularIntensity", "specularIntensity");
2964
+ scalar2("iridescence", "iridescence");
2965
+ scalar2("iridescenceIOR", "iridescenceIOR");
2966
+ scalar2("anisotropy", "anisotropy");
2967
+ if (material.transparent === true) properties.transparent = true;
2968
+ if (material.side === 2) properties.doubleSided = true;
2969
+ return properties;
2970
+ }
2971
+ function unwrap(value) {
2972
+ const node = value;
2973
+ if (node && typeof node === "object" && typeof node.getSelf === "function") {
2974
+ try {
2975
+ return node.getSelf();
2976
+ } catch {
2977
+ return node;
2978
+ }
2979
+ }
2980
+ return value;
2981
+ }
2982
+ function serializeValue(value) {
2983
+ if (typeof value === "number") return { value, type: "float" };
2984
+ if (typeof value === "boolean")
2985
+ return { value: value ? 1 : 0, type: "float" };
2986
+ const v = value;
2987
+ if (!v || typeof v !== "object") return null;
2988
+ if (v.isColor) return { value: [v.r, v.g, v.b], type: "color" };
2989
+ if (v.isVector2) return { value: [v.x, v.y], type: "vec2" };
2990
+ if (v.isVector3) return { value: [v.x, v.y, v.z], type: "vec3" };
2991
+ if (v.isVector4) return { value: [v.x, v.y, v.z, v.w], type: "vec4" };
2992
+ return null;
2993
+ }
2994
+ function wrapMode(texture) {
2995
+ const wrap = texture.wrapS ?? 1e3;
2996
+ return wrap === 1001 ? "clamp" : wrap === 1002 ? "mirror" : "repeat";
2997
+ }
2998
+ function num(value, fallback) {
2999
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
3000
+ }
3001
+ function describe2(value) {
3002
+ if (value === null) return "null";
3003
+ if (typeof value !== "object") return typeof value;
3004
+ const v = value;
3005
+ if (v.isMatrix3) return "Matrix3";
3006
+ if (v.isMatrix4) return "Matrix4";
3007
+ if (v.isQuaternion) return "Quaternion";
3008
+ if (v.isTexture) return "Texture";
3009
+ return v.constructor?.name ?? "object";
3010
+ }
3011
+ function describeNodeType(type) {
3012
+ const KNOWN = {
3013
+ ViewportTextureNode: "Reading the viewport back",
3014
+ ScreenNode: "Screen-space coordinates",
3015
+ ViewportNode: "Screen-space coordinates",
3016
+ MatcapUVNode: "Matcap UVs (they encode the rasterizer camera)",
3017
+ ReflectorNode: "A planar reflector (it renders the scene twice)",
3018
+ RTTNode: "Render-to-texture",
3019
+ LoopNode: "A shader loop",
3020
+ ArrayNode: "A shader array",
3021
+ StorageBufferNode: "A storage buffer",
3022
+ FunctionOverloadingNode: "An overloaded shader function",
3023
+ ComputeNode: "A compute pass"
3024
+ };
3025
+ return KNOWN[type] ?? `A "${type}"`;
3026
+ }
3027
+
3028
+ // src/compat/analyze.ts
3029
+ var DOCS2 = "https://bakery3.com/docs";
3030
+ var LARGE_TEXTURE_PX = 4096;
3031
+ var LARGE_TRIANGLE_COUNT = 1e7;
3032
+ var TRANSLATABLE = /* @__PURE__ */ new Set(["MeshStandardMaterial", "MeshPhysicalMaterial"]);
3033
+ var APPROXIMATED = {
3034
+ MeshBasicMaterial: "Rendered as an emissive surface, since a path tracer has no unlit shading model.",
3035
+ MeshLambertMaterial: "Rendered as a rough physically based material. Lambert has no specular term, so highlights will differ.",
3036
+ MeshPhongMaterial: "Converted to roughness/metalness. Phong shininess is not a physical parameter, so the specular falloff is an approximation.",
3037
+ MeshToonMaterial: "Rendered as flat diffuse. Cel shading is a screen-space stylization with no path-traced equivalent.",
3038
+ MeshMatcapMaterial: "Rendered as diffuse using the matcap average. A matcap encodes lighting into a texture, which is exactly what path tracing replaces.",
3039
+ MeshNormalMaterial: "Rendered as flat gray. Normal visualization is a debug view, not a surface.",
3040
+ MeshDepthMaterial: "Rendered as flat gray. Depth output is a debug view.",
3041
+ MeshDistanceMaterial: "Rendered as flat gray. Distance output is a debug view."
3042
+ };
3043
+ function emitsLight(material) {
3044
+ const m = material;
3045
+ if (m.isMeshBasicMaterial) return true;
3046
+ if (m.emissiveNode) return true;
3047
+ if ((m.emissiveIntensity ?? 1) <= 0) return false;
3048
+ if (m.emissiveMap) return true;
3049
+ const e = m.emissive;
3050
+ return Boolean(e && e.r + e.g + e.b > 0);
3051
+ }
3052
+ function analyzeScene(root) {
3053
+ const issues = [];
3054
+ const seenMaterials = /* @__PURE__ */ new Set();
3055
+ const seenTextures = /* @__PURE__ */ new Set();
3056
+ let triangles = 0;
3057
+ let faithfulTriangles = 0;
3058
+ let maxTextureDimension = 0;
3059
+ let emitters = 0;
3060
+ root.traverse((object) => {
3061
+ if (shouldExclude(object)) return;
3062
+ const mesh = object;
3063
+ if (!mesh.isMesh) return;
3064
+ const count = triangleCount(mesh);
3065
+ triangles += count;
3066
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
3067
+ let objectIsFaithful = true;
3068
+ for (const material of materials) {
3069
+ if (!material) continue;
3070
+ for (const texture of texturesOf(material)) {
3071
+ if (seenTextures.has(texture.uuid)) continue;
3072
+ seenTextures.add(texture.uuid);
3073
+ const dimension = Math.max(
3074
+ texture.image?.width ?? 0,
3075
+ texture.image?.height ?? 0
3076
+ );
3077
+ maxTextureDimension = Math.max(maxTextureDimension, dimension);
3078
+ if (dimension > LARGE_TEXTURE_PX) {
3079
+ issues.push({
3080
+ code: "texture_large",
3081
+ severity: "warning",
3082
+ message: `Texture "${texture.name || texture.uuid.slice(0, 8)}" is ${dimension}px. Above 4K a texture costs upload time and VRAM without visibly improving a product render.`,
3083
+ path: pathOf(object),
3084
+ docs: `${DOCS2}/scene-size#textures`
3085
+ });
3086
+ }
3087
+ }
3088
+ if (seenMaterials.has(material.uuid)) continue;
3089
+ seenMaterials.add(material.uuid);
3090
+ if (emitsLight(material)) emitters++;
3091
+ for (const issue of classifyMaterial(material, object)) {
3092
+ issues.push(issue);
3093
+ if (issue.severity === "error") objectIsFaithful = false;
3094
+ }
3095
+ }
3096
+ if (objectIsFaithful) faithfulTriangles += count;
3097
+ });
3098
+ if (triangles > LARGE_TRIANGLE_COUNT) {
3099
+ issues.push({
3100
+ code: "scene_large",
3101
+ severity: "warning",
3102
+ message: `Scene has ${(triangles / 1e6).toFixed(1)}M triangles.`,
3103
+ docs: `${DOCS2}/scene-size`
3104
+ });
3105
+ }
3106
+ const fog = captureFog(root).issue;
3107
+ if (fog) issues.push(fog);
3108
+ const counts = { info: 0, warning: 0, error: 0 };
3109
+ for (const issue of issues) counts[issue.severity]++;
3110
+ return {
3111
+ // An empty scene is not 0% compatible, it is a different problem, and
3112
+ // reporting 0 there would put a red 0% in the devtools panel of a
3113
+ // developer whose scene simply had not loaded yet.
3114
+ score: triangles === 0 ? 1 : faithfulTriangles / triangles,
3115
+ issues,
3116
+ counts,
3117
+ triangles,
3118
+ textures: { count: seenTextures.size, maxDimension: maxTextureDimension },
3119
+ emitters
3120
+ };
3121
+ }
3122
+ function collectFallbacks(root) {
3123
+ const fallbacks = [];
3124
+ const seen = /* @__PURE__ */ new Set();
3125
+ root.traverse((object) => {
3126
+ const mesh = object;
3127
+ if (!mesh.isMesh) return;
3128
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
3129
+ for (const material of materials) {
3130
+ if (!material || seen.has(material.uuid)) continue;
3131
+ const declared = material.userData?.bakery3?.fallback ?? readUserData(object).fallback;
3132
+ if (!declared) continue;
3133
+ seen.add(material.uuid);
3134
+ fallbacks.push({
3135
+ ...declared,
3136
+ materialId: idOf(material),
3137
+ type: "physical"
3138
+ });
3139
+ }
3140
+ });
3141
+ return fallbacks;
3142
+ }
3143
+ function classifyMaterial(material, object) {
3144
+ const hasFallback = Boolean(
3145
+ material.userData?.bakery3?.fallback ?? readUserData(object).fallback
3146
+ );
3147
+ if (isNodeMaterial(material) && !hasFallback) {
3148
+ const extracted = extractMaterialShader(material);
3149
+ return extracted.issues.map((issue) => ({
3150
+ ...issue,
3151
+ path: pathOf(object),
3152
+ materialId: idOf(material),
3153
+ materialName: material.name,
3154
+ materialType: material.type
3155
+ }));
3156
+ }
3157
+ const verdict = classify(material, object, hasFallback);
3158
+ return verdict ? [verdict.issue] : [];
3159
+ }
3160
+ function classify(material, object, hasFallback) {
3161
+ if (TRANSLATABLE.has(material.type)) return null;
3162
+ if (hasFallback) {
3163
+ return {
3164
+ issue: {
3165
+ code: "material_fallback_used",
3166
+ severity: "info",
3167
+ message: `${material.type} "${material.name || "unnamed"}" renders with your declared fallback.`,
3168
+ path: pathOf(object),
3169
+ materialId: idOf(material),
3170
+ materialName: material.name,
3171
+ materialType: material.type,
3172
+ docs: `${DOCS2}/material-fallbacks`
3173
+ }
3174
+ };
3175
+ }
3176
+ if (material.type === "ShaderMaterial" || material.type === "RawShaderMaterial") {
3177
+ return {
3178
+ issue: {
3179
+ code: "material_custom_shader",
3180
+ severity: "error",
3181
+ message: `Unsupported material: ${material.name || material.type}`,
3182
+ path: pathOf(object),
3183
+ materialId: idOf(material),
3184
+ materialName: material.name,
3185
+ materialType: material.type,
3186
+ docs: `${DOCS2}/material-fallbacks`
3187
+ }
3188
+ };
3189
+ }
3190
+ const approximation = APPROXIMATED[material.type];
3191
+ if (approximation) {
3192
+ return {
3193
+ issue: {
3194
+ code: "material_approximated",
3195
+ severity: "warning",
3196
+ message: `${material.type} "${material.name || "unnamed"}" is approximated. ${approximation}`,
3197
+ path: pathOf(object),
3198
+ materialId: idOf(material),
3199
+ materialName: material.name,
3200
+ materialType: material.type,
3201
+ docs: `${DOCS2}/materials`
3202
+ }
3203
+ };
3204
+ }
3205
+ return {
3206
+ issue: {
3207
+ code: "material_unknown",
3208
+ severity: "error",
3209
+ message: `Unsupported material: ${material.name || material.type}`,
3210
+ path: pathOf(object),
3211
+ materialId: idOf(material),
3212
+ materialName: material.name,
3213
+ materialType: material.type,
3214
+ docs: `${DOCS2}/materials`
3215
+ }
3216
+ };
3217
+ }
3218
+ var TEXTURE_SLOTS = [
3219
+ "map",
3220
+ "normalMap",
3221
+ "bumpMap",
3222
+ "displacementMap",
3223
+ "roughnessMap",
3224
+ "metalnessMap",
3225
+ "aoMap",
3226
+ "emissiveMap",
3227
+ "alphaMap",
3228
+ "clearcoatMap",
3229
+ "clearcoatNormalMap",
3230
+ "clearcoatRoughnessMap",
3231
+ "transmissionMap",
3232
+ "thicknessMap",
3233
+ "sheenColorMap",
3234
+ "sheenRoughnessMap",
3235
+ "iridescenceMap",
3236
+ "iridescenceThicknessMap",
3237
+ "anisotropyMap",
3238
+ "specularMap",
3239
+ "specularColorMap",
3240
+ "specularIntensityMap"
3241
+ ];
3242
+ function texturesOf(material) {
3243
+ const out = [];
3244
+ const record2 = material;
3245
+ for (const slot of TEXTURE_SLOTS) {
3246
+ const texture = record2[slot];
3247
+ if (texture?.isTexture) out.push(texture);
3248
+ }
3249
+ return out;
3250
+ }
3251
+ function triangleCount(mesh) {
3252
+ const geometry = mesh.geometry;
3253
+ if (!geometry) return 0;
3254
+ const indexed = geometry.index?.count;
3255
+ const positions = geometry.attributes.position?.count ?? 0;
3256
+ const vertices = indexed ?? positions;
3257
+ const instances = mesh.count;
3258
+ const perInstance = Math.floor(vertices / 3);
3259
+ return mesh.isInstancedMesh ? perInstance * (instances ?? 1) : perInstance;
3260
+ }
3261
+ var pathOf = scenePath;
3262
+ var PROBED_INSTANCES = 64;
3263
+ function sceneComplexity(scene, camera) {
3264
+ scene.updateMatrixWorld();
3265
+ camera.updateMatrixWorld();
3266
+ const rooms = new Set(findPresetRooms(scene));
3267
+ const meshes = [];
3268
+ let objects = 0;
3269
+ const visit = (object) => {
3270
+ if (!object.visible) return;
3271
+ if (excludedItself(object) && !rooms.has(object)) return;
3272
+ const mesh = object;
3273
+ if (mesh.isMesh && mesh.geometry?.attributes?.position) {
3274
+ const position = mesh.geometry.attributes.position;
3275
+ const index = mesh.geometry.index;
3276
+ const plain = !position.normalized && !position.isInterleavedBufferAttribute && position.itemSize === 3;
3277
+ const positions = plain ? position.array : Float32Array.from(
3278
+ { length: position.count * 3 },
3279
+ (_, i) => i % 3 === 0 ? position.getX(i / 3 | 0) : i % 3 === 1 ? position.getY(i / 3 | 0) : position.getZ(i / 3 | 0)
3280
+ );
3281
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
3282
+ const emitter = materials.length > 0 && materials.every((material) => material && emitsLight(material));
3283
+ const base = {
3284
+ positions,
3285
+ indices: index?.array ?? null,
3286
+ emitter
3287
+ };
3288
+ if (mesh.isInstancedMesh && mesh.instanceMatrix) {
3289
+ const count = mesh.count ?? 0;
3290
+ objects += count;
3291
+ const local = new Matrix4();
3292
+ for (let i = 0; i < Math.min(count, PROBED_INSTANCES); i++) {
3293
+ local.fromArray(mesh.instanceMatrix.array, i * 16);
3294
+ meshes.push({
3295
+ ...base,
3296
+ matrix: new Matrix4().multiplyMatrices(mesh.matrixWorld, local).elements
3297
+ });
3298
+ }
3299
+ } else {
3300
+ objects += 1;
3301
+ meshes.push({ ...base, matrix: mesh.matrixWorld.elements });
3302
+ }
3303
+ }
3304
+ for (const child of object.children) visit(child);
3305
+ };
3306
+ visit(scene);
3307
+ const eye = camera.getWorldPosition(new Vector3());
3308
+ return complexityOf(meshes, [eye.x, eye.y, eye.z], objects);
3309
+ }
3310
+
3311
+ // src/capture/color.ts
3312
+ var TONE_MAPPING = {
3313
+ 0: "none",
3314
+ 1: "linear",
3315
+ 2: "reinhard",
3316
+ 3: "cineon",
3317
+ 4: "aces-filmic",
3318
+ // AgX (r162+) and Neutral (r165+) took the next two slots, in that order.
3319
+ 6: "agx",
3320
+ 7: "neutral"
3321
+ };
3322
+ var COLOR_SPACES = {
3323
+ srgb: "srgb",
3324
+ "srgb-linear": "srgb-linear",
3325
+ "display-p3": "display-p3"
3326
+ };
3327
+ function captureColor(renderer) {
3328
+ if (!renderer) {
3329
+ return {
3330
+ outputColorSpace: "srgb",
3331
+ toneMapping: "none",
3332
+ toneMappingExposure: 1
3333
+ };
3334
+ }
3335
+ return {
3336
+ outputColorSpace: COLOR_SPACES[renderer.outputColorSpace ?? "srgb"] ?? "srgb",
3337
+ toneMapping: TONE_MAPPING[renderer.toneMapping ?? 0] ?? "none",
3338
+ toneMappingExposure: renderer.toneMappingExposure ?? 1
3339
+ };
3340
+ }
3341
+
3342
+ // src/capture/shaders.ts
3343
+ async function captureShaders(root) {
3344
+ const shaders = [];
3345
+ const assets = [];
3346
+ const issues = [];
3347
+ const seen = /* @__PURE__ */ new Set();
3348
+ const hashed = /* @__PURE__ */ new Map();
3349
+ const materials = [];
3350
+ root.traverse((object) => {
3351
+ if (shouldExclude(object)) return;
3352
+ const mesh = object;
3353
+ if (!mesh.isMesh || !mesh.material) return;
3354
+ const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
3355
+ for (const material of list) {
3356
+ if (!material || seen.has(material.uuid)) continue;
3357
+ seen.add(material.uuid);
3358
+ const hasFallback = Boolean(
3359
+ material.userData?.bakery3?.fallback ?? readUserData(object).fallback
3360
+ );
3361
+ if (isNodeMaterial(material) && !hasFallback) {
3362
+ materials.push({ material, path: pathOf2(object) });
3363
+ }
3364
+ }
3365
+ });
3366
+ for (const { material, path } of materials) {
3367
+ const extracted = extractMaterialShader(material);
3368
+ if (!extracted.spec) continue;
3369
+ let texturesOk = true;
3370
+ for (const ref of extracted.textures) {
3371
+ const hash = await resolveTexture(
3372
+ ref,
3373
+ hashed,
3374
+ assets,
3375
+ issues,
3376
+ material,
3377
+ path
3378
+ );
3379
+ if (!hash) texturesOk = false;
3380
+ }
3381
+ if (texturesOk) {
3382
+ shaders.push(extracted.spec);
3383
+ }
3384
+ }
3385
+ return { shaders, assets, issues };
3386
+ }
3387
+ async function resolveTexture(ref, hashed, assets, issues, material, path) {
3388
+ const known = hashed.get(ref.texture.uuid);
3389
+ if (known !== void 0) {
3390
+ if (known) ref.apply(known);
3391
+ return known;
3392
+ }
3393
+ const bytes = await textureBytes(ref.texture);
3394
+ if (!bytes) {
3395
+ hashed.set(ref.texture.uuid, null);
3396
+ issues.push({
3397
+ code: "shader_texture_unreadable",
3398
+ severity: "error",
3399
+ message: `The texture "${ref.texture.name || ref.texture.uuid.slice(0, 8)}" used by "${material.name || material.type}" has no reachable pixels, so the shader graph was not sent. `,
3400
+ path,
3401
+ materialId: idOf(material),
3402
+ materialName: material.name,
3403
+ materialType: material.type,
3404
+ docs: "https://bakery3.com/docs/shader-nodes#textures"
3405
+ });
3406
+ return null;
3407
+ }
3408
+ const hash = await sha256(bytes.data);
3409
+ hashed.set(ref.texture.uuid, hash);
3410
+ if (!assets.some((asset) => asset.hash === hash)) {
3411
+ assets.push({
3412
+ hash,
3413
+ kind: "texture",
3414
+ bytes: bytes.data.byteLength,
3415
+ contentType: bytes.contentType,
3416
+ name: ref.texture.name || "shader-texture",
3417
+ data: bytes.data
3418
+ });
3419
+ }
3420
+ ref.apply(hash);
3421
+ return hash;
3422
+ }
3423
+ async function textureBytes(texture) {
3424
+ const declared = texture.userData?.bakery3?.src;
3425
+ if (typeof declared === "string") {
3426
+ const fetched = await fetchBytes(declared);
3427
+ if (fetched) return fetched;
3428
+ }
3429
+ const image = texture.image;
3430
+ if (!image) return null;
3431
+ if (typeof image.src === "string" && image.src && !image.src.startsWith("blob:")) {
3432
+ const fetched = await fetchBytes(image.src);
3433
+ if (fetched) return fetched;
3434
+ }
3435
+ const raw = image.data;
3436
+ if (raw && ArrayBuffer.isView(raw)) {
3437
+ if (!(raw instanceof Uint8Array) && !(raw instanceof Uint8ClampedArray)) {
3438
+ return null;
3439
+ }
3440
+ return encodeRGBA(raw, image.width ?? 0, image.height ?? 0);
3441
+ }
3442
+ return drawToPng(
3443
+ image,
3444
+ image.width ?? 0,
3445
+ image.height ?? 0
3446
+ );
3447
+ }
3448
+ async function fetchBytes(url) {
3449
+ if (typeof fetch !== "function") return null;
3450
+ try {
3451
+ const response = await fetch(url);
3452
+ if (!response.ok) return null;
3453
+ return {
3454
+ data: await response.arrayBuffer(),
3455
+ contentType: response.headers.get("content-type") ?? "application/octet-stream"
3456
+ };
3457
+ } catch {
3458
+ return null;
3459
+ }
3460
+ }
3461
+ async function drawToPng(source, width, height) {
3462
+ if (!width || !height) return null;
3463
+ const canvas = makeCanvas(width, height);
3464
+ if (!canvas) return null;
3465
+ const context = canvas.getContext("2d");
3466
+ if (!context) return null;
3467
+ try {
3468
+ context.drawImage(source, 0, 0);
3469
+ } catch {
3470
+ return null;
3471
+ }
3472
+ return canvasToPng(canvas);
3473
+ }
3474
+ async function encodeRGBA(data, width, height) {
3475
+ if (!width || !height || data.length < width * height * 4) return null;
3476
+ const canvas = makeCanvas(width, height);
3477
+ if (!canvas) return null;
3478
+ const context = canvas.getContext("2d");
3479
+ if (!context) return null;
3480
+ const pixels = new Uint8ClampedArray(width * height * 4);
3481
+ pixels.set(data.subarray(0, pixels.length));
3482
+ context.putImageData(new ImageData(pixels, width, height), 0, 0);
3483
+ return canvasToPng(canvas);
3484
+ }
3485
+ function makeCanvas(width, height) {
3486
+ if (typeof OffscreenCanvas !== "undefined")
3487
+ return new OffscreenCanvas(width, height);
3488
+ if (typeof document !== "undefined") {
3489
+ const canvas = document.createElement("canvas");
3490
+ canvas.width = width;
3491
+ canvas.height = height;
3492
+ return canvas;
3493
+ }
3494
+ return null;
3495
+ }
3496
+ async function canvasToPng(canvas) {
3497
+ if ("convertToBlob" in canvas) {
3498
+ const blob2 = await canvas.convertToBlob({ type: "image/png" });
3499
+ return { data: await blob2.arrayBuffer(), contentType: "image/png" };
3500
+ }
3501
+ const blob = await new Promise(
3502
+ (resolve) => canvas.toBlob(resolve, "image/png")
3503
+ );
3504
+ return blob ? { data: await blob.arrayBuffer(), contentType: "image/png" } : null;
3505
+ }
3506
+ function pathOf2(object) {
3507
+ const parts = [];
3508
+ let current = object;
3509
+ while (current && current.parent) {
3510
+ parts.unshift(`<${current.name || current.type}>`);
3511
+ current = current.parent;
3512
+ }
3513
+ return parts.join("/");
3514
+ }
3515
+
3516
+ // src/capture/room.ts
3517
+ var DOCS3 = "https://bakery3.com/docs/presets#room-presets";
3518
+ function captureRoomPreset(scene, report, context) {
3519
+ const rooms = findPresetRooms(scene);
3520
+ if (!rooms.length) return null;
3521
+ if (rooms.length > 1) {
3522
+ throw new Bakery3Error({
3523
+ code: "CaptureFailed",
3524
+ message: `The scene holds ${rooms.length} room presets (${rooms.map(scenePath).join(", ")}); a render can be inside one room.`,
3525
+ why: "The renderer rebuilds a room around the product from its published definition, and two rooms would be two skies and two suns for one camera.",
3526
+ fix: "Keep one createPresetRoom() group in the scene at render time: hide the others with userData.bakery3.exclude, or remove them.",
3527
+ docs: DOCS3
3528
+ });
3529
+ }
3530
+ const group = rooms[0];
3531
+ const spec = roomPresetSpecOf(group);
3532
+ const info = roomPresetFactsOf(group);
3533
+ const note = (issue) => {
3534
+ report.issues.push({
3535
+ ...issue,
3536
+ severity: "info",
3537
+ path: scenePath(group),
3538
+ docs: DOCS3
3539
+ });
3540
+ report.counts.info++;
3541
+ };
3542
+ note({
3543
+ code: "RoomPresetReconstructed",
3544
+ message: `Room preset "${info?.label ?? spec.name}" is rebuilt by the renderer from its full-quality definition; the furniture, lights and materials in the viewport are stand-ins, and the foliage outside its windows exists only in the render.`
3545
+ });
3546
+ if (context.environment) {
3547
+ note({
3548
+ code: "RoomPresetOwnsSky",
3549
+ message: "The room preset provides the sky and the daylight; scene.environment is not used for this render."
3550
+ });
3551
+ }
3552
+ if (spec.pictures?.some(Boolean)) {
3553
+ note({
3554
+ code: "RoomPresetPicturesReplaced",
3555
+ message: `${spec.pictures.filter(Boolean).length} of the room's pictures are yours; each is cropped to fill the frame it hangs in, which does not move.`
3556
+ });
3557
+ }
3558
+ return {
3559
+ spec,
3560
+ triangles: info?.triangles ?? 0,
3561
+ assets: roomPresetAssetsOf(group)
3562
+ };
3563
+ }
3564
+ function roomTriangles(room) {
3565
+ return room && room.spec.standIn !== true ? room.triangles : 0;
3566
+ }
3567
+ function presetRoomTriangles(scene) {
3568
+ const rooms = findPresetRooms(scene);
3569
+ if (rooms.length !== 1) return 0;
3570
+ const spec = roomPresetSpecOf(rooms[0]);
3571
+ if (!spec) return 0;
3572
+ return roomTriangles({
3573
+ spec,
3574
+ triangles: roomPresetFactsOf(rooms[0])?.triangles ?? 0
3575
+ });
3576
+ }
3577
+
3578
+ // src/capture/capture.ts
3579
+ async function captureScene(options) {
3580
+ const { scene, camera } = options;
3581
+ assertCaptureRuntime();
3582
+ const color = captureColor(options.renderer);
3583
+ const compatibility = analyzeScene(scene);
3584
+ const fallbacks = collectFallbacks(scene);
3585
+ const lights = captureLights(scene);
3586
+ const runtime = captureRuntime(scene, options.productState);
3587
+ const shaded = await captureShaders(scene);
3588
+ for (const issue of shaded.issues) {
3589
+ compatibility.issues.push(issue);
3590
+ compatibility.counts[issue.severity]++;
3591
+ }
3592
+ const room = captureRoomPreset(scene, compatibility, {
3593
+ environment: Boolean(scene.environment || options.environment)
3594
+ });
3595
+ const unstamp = stampIds(scene);
3596
+ let glb;
3597
+ try {
3598
+ glb = await exportGLB(scene);
3599
+ } finally {
3600
+ unstamp();
3601
+ }
3602
+ const sceneAsset = await sha256(glb);
3603
+ const assets = [
3604
+ {
3605
+ hash: sceneAsset,
3606
+ kind: "scene",
3607
+ bytes: glb.byteLength,
3608
+ contentType: "model/gltf-binary",
3609
+ name: "scene.glb",
3610
+ data: glb
3611
+ },
3612
+ ...shaded.assets,
3613
+ // A picture the developer hung in a preset room's frame: the one case
3614
+ // where the room record names an asset the scene GLB does not carry.
3615
+ ...(room?.assets ?? []).map((asset) => ({
3616
+ hash: asset.hash,
3617
+ kind: "texture",
3618
+ bytes: asset.data.byteLength,
3619
+ contentType: asset.contentType,
3620
+ name: asset.name,
3621
+ data: asset.data
3622
+ }))
3623
+ ];
3624
+ const lit = await captureEnvironment(options, scene, assets, compatibility);
3625
+ const { fog } = captureFog(scene);
3626
+ const environment = fog ? { ...lit, fog } : lit;
3627
+ const manifest = {
3628
+ // A manifest is stamped with the lowest protocol version that can read
3629
+ // it, so an older render worker is not refused a still it understands.
3630
+ // `renderVideo()` re-stamps once it has added the animation; a preset
3631
+ // room raises the version here, because a worker that ignored the room
3632
+ // would render the product in a void. See `protocolVersionFor`.
3633
+ // Fog raises it too: an older worker would leave the fog out.
3634
+ protocolVersion: protocolVersionFor({
3635
+ ...room ? { room: room.spec } : {},
3636
+ ...environment ? { environment } : {}
3637
+ }),
3638
+ sdkVersion: SDK_VERSION,
3639
+ sceneAsset,
3640
+ ...assets.length > 1 ? { assets: assets.slice(1).map((asset) => asset.hash) } : {},
3641
+ camera: captureCamera(camera),
3642
+ ...Object.keys(runtime).length ? { runtime } : {},
3643
+ ...environment ? { environment } : {},
3644
+ ...lights.length ? { lights } : {},
3645
+ color,
3646
+ ...fallbacks.length ? { fallbacks } : {},
3647
+ ...shaded.shaders.length ? { shaders: shaded.shaders } : {},
3648
+ ...room ? { room: room.spec } : {},
3649
+ compatibility: {
3650
+ score: compatibility.score,
3651
+ issues: compatibility.issues,
3652
+ counts: compatibility.counts
3653
+ },
3654
+ // The control plane prices what it accepts with the same numbers
3655
+ // estimate() was given (see SceneManifest.stats).
3656
+ stats: {
3657
+ // A preset room's geometry is built by the renderer, not uploaded, but
3658
+ // it is traced all the same, so its published count is added. Unless
3659
+ // the room shipped its stand-in: then the upload carries it and
3660
+ // `compatibility` has already counted it.
3661
+ triangles: compatibility.triangles + roomTriangles(room),
3662
+ emitters: compatibility.emitters,
3663
+ // How enclosed and how full the scene is, from the camera: what the
3664
+ // quote reads to price an interior as one (capture/complexity.ts).
3665
+ ...sceneComplexity(scene, camera)
3666
+ },
3667
+ source: {
3668
+ three: threeVersion(),
3669
+ renderer: options.renderer ? "WebGLRenderer" : void 0,
3670
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0
3671
+ }
3672
+ };
3673
+ return { manifest, assets };
3674
+ }
3675
+ function exportGLB(scene) {
3676
+ const exporter = new GLTFExporter();
3677
+ const reattach = detachExcluded(scene);
3678
+ const unmute = muteLightWarnings();
3679
+ return new Promise((resolve, reject) => {
3680
+ exporter.parse(
3681
+ scene,
3682
+ (result) => {
3683
+ if (!(result instanceof ArrayBuffer)) {
3684
+ reject(
3685
+ new Bakery3Error({
3686
+ code: "CaptureFailed",
3687
+ message: "The glTF exporter returned JSON instead of a binary GLB.",
3688
+ why: "binary: true was set, so this should not happen. It usually means a patched or very old GLTFExporter.",
3689
+ fix: "Update three, or file an issue at github.com/oeave/bakery3 with your three version."
3690
+ })
3691
+ );
3692
+ return;
3693
+ }
3694
+ resolve(result);
3695
+ },
3696
+ (error) => reject(
3697
+ new Bakery3Error({
3698
+ code: "CaptureFailed",
3699
+ message: `Could not export the scene: ${error instanceof Error ? error.message : String(error)}`,
3700
+ why: "The glTF exporter walks the scene graph; a geometry with a malformed attribute or a texture from a tainted canvas will stop it.",
3701
+ fix: "Run bakery3.check() to find the object it stopped on.",
3702
+ docs: "https://bakery3.com/docs/troubleshooting#export"
3703
+ })
3704
+ ),
3705
+ {
3706
+ binary: true,
3707
+ onlyVisible: false,
3708
+ includeCustomExtensions: true
3709
+ }
3710
+ );
3711
+ }).finally(() => {
3712
+ unmute();
3713
+ reattach();
3714
+ });
3715
+ }
3716
+ var MUTED_EXPORTER_WARNINGS = [
3717
+ "THREE.GLTFExporter: Only directional, point, and spot lights are supported.",
3718
+ "THREE.GLTFExporter: Light direction may be lost.",
3719
+ "THREE.GLTFExporter: Light decay may be lost."
3720
+ ];
3721
+ var muted = 0;
3722
+ var mute = null;
3723
+ function muteLightWarnings() {
3724
+ if (muted++ === 0) {
3725
+ const original = console.warn;
3726
+ const filter = function(...args) {
3727
+ const first = args[0];
3728
+ if (typeof first === "string" && MUTED_EXPORTER_WARNINGS.some((prefix) => first.startsWith(prefix)))
3729
+ return;
3730
+ original.apply(this, args);
3731
+ };
3732
+ mute = { original, filter };
3733
+ console.warn = filter;
3734
+ }
3735
+ let done = false;
3736
+ return () => {
3737
+ if (done) return;
3738
+ done = true;
3739
+ if (--muted > 0 || !mute) return;
3740
+ if (console.warn === mute.filter) console.warn = mute.original;
3741
+ mute = null;
3742
+ };
3743
+ }
3744
+ function assertCaptureRuntime() {
3745
+ if (typeof globalThis.FileReader === "function")
3746
+ return;
3747
+ throw new Bakery3Error({
3748
+ code: "CaptureFailed",
3749
+ message: "render() captures a three.js scene in a browser. In Node, use @oeave/bakery3/node: renderBatch() renders models by URL.",
3750
+ why: "The capture exports your live scene with three.js, which needs the browser's FileReader, and this runtime has none.",
3751
+ fix: "Call render() from the page that shows the scene. From a server or a script, render .glb files by URL with `new Bakery3Server(key).renderBatch()` from '@oeave/bakery3/node'.",
3752
+ docs: "https://bakery3.com/docs/catalog"
3753
+ });
3754
+ }
3755
+ function detachExcluded(scene) {
3756
+ const detached = [];
3757
+ scene.traverse((object) => {
3758
+ if (!object.parent || !shouldExclude(object)) return;
3759
+ detached.push({
3760
+ object,
3761
+ parent: object.parent,
3762
+ index: object.parent.children.indexOf(object)
3763
+ });
3764
+ });
3765
+ for (const entry of detached) entry.parent.remove(entry.object);
3766
+ return () => {
3767
+ for (const entry of [...detached].reverse()) {
3768
+ entry.object.parent = entry.parent;
3769
+ entry.parent.children.splice(entry.index, 0, entry.object);
3770
+ }
3771
+ };
3772
+ }
3773
+ async function captureEnvironment(options, scene, assets, compatibility) {
3774
+ const settings = captureEnvironmentSettings(scene);
3775
+ const background = options.background ?? {
3776
+ type: "transparent"
3777
+ };
3778
+ const explicit = options.environment;
3779
+ const remembered = scene.environment ? environmentSourceFor(scene.environment) : void 0;
3780
+ const source = explicit ?? remembered;
3781
+ if (!source) {
3782
+ if (scene.environment) {
3783
+ compatibility.issues.push({
3784
+ code: "environment_unavailable",
3785
+ severity: "warning",
3786
+ message: "The scene has an environment map, but its source pixels are not reachable from the SDK.",
3787
+ docs: "https://bakery3.com/docs/environment"
3788
+ });
3789
+ compatibility.counts.warning++;
3790
+ }
3791
+ const ambient2 = capturedAmbient(scene);
3792
+ return {
3793
+ ...settings,
3794
+ ...ambient2 ? { ambient: ambient2 } : {},
3795
+ background
3796
+ };
3797
+ }
3798
+ const preset = typeof source === "string" && (source.startsWith(PRESET_LIBRARY) || !explicit && scene.environment !== null && isPresetEnvironment(scene.environment));
3799
+ const ambient = capturedAmbient(scene);
3800
+ const fill = ambient ? { ambient } : {};
3801
+ if (typeof source === "string" && preset) {
3802
+ return { ...settings, ...fill, hdri: source, background };
3803
+ }
3804
+ const pixels = typeof source === "string" ? await fetchEnvironment(source) : source;
3805
+ const hash = await sha256(pixels);
3806
+ assets.push({
3807
+ hash,
3808
+ kind: "hdri",
3809
+ bytes: pixels.byteLength,
3810
+ contentType: "image/vnd.radiance",
3811
+ name: "environment.hdr",
3812
+ data: pixels
3813
+ });
3814
+ return { ...settings, ...fill, hdri: hash, background };
3815
+ }
3816
+ var PRESET_LIBRARY = "https://cdn.bakery3.com/presets/";
3817
+ async function fetchEnvironment(url) {
3818
+ let response;
3819
+ try {
3820
+ response = await fetch(url);
3821
+ } catch (cause) {
3822
+ throw environmentUnreachable(
3823
+ url,
3824
+ cause instanceof Error ? cause.message : String(cause)
3825
+ );
3826
+ }
3827
+ if (!response.ok)
3828
+ throw environmentUnreachable(
3829
+ url,
3830
+ `${response.status} ${response.statusText}`.trim()
3831
+ );
3832
+ return response.arrayBuffer();
3833
+ }
3834
+ function environmentUnreachable(url, detail) {
3835
+ return new Bakery3Error({
3836
+ code: "CaptureFailed",
3837
+ message: `The environment at ${url} could not be fetched: ${detail}.`,
3838
+ why: "An environment given by URL is downloaded in the page and uploaded with the scene, so the page has to be able to read it.",
3839
+ fix: "Check the URL and its CORS headers, or pass the HDRI bytes as `environment` instead.",
3840
+ docs: "https://bakery3.com/docs/environment"
3841
+ });
3842
+ }
3843
+ function threeVersion() {
3844
+ const three = globalThis.__THREE__;
3845
+ return typeof three === "string" ? three : void 0;
3846
+ }
3847
+ function aspectOf(camera, renderer) {
3848
+ return cameraAspect(camera) ?? rendererAspect(renderer) ?? 1;
3849
+ }
3850
+ function outputSize(given, longEdge, aspect, options = {}) {
3851
+ const ratio = Number.isFinite(aspect) && aspect > 0 ? aspect : 1;
3852
+ const round = options.even ? (value) => Math.max(2, Math.round(value / 2) * 2) : (value) => Math.max(1, Math.round(value));
3853
+ const { width, height } = given;
3854
+ if (width !== void 0 && height !== void 0) return { width, height };
3855
+ if (width !== void 0) return { width, height: round(width / ratio) };
3856
+ if (height !== void 0) return { width: round(height * ratio), height };
3857
+ return ratio >= 1 ? { width: round(longEdge), height: round(longEdge / ratio) } : { width: round(longEdge * ratio), height: round(longEdge) };
3858
+ }
3859
+ function resolveOutput(call, defaults, context) {
3860
+ const own = call.width !== void 0 || call.height !== void 0;
3861
+ const given = own ? { width: call.width, height: call.height } : { width: defaults?.width, height: defaults?.height };
3862
+ const size = outputSize(
3863
+ {
3864
+ ...given.width !== void 0 ? { width: given.width } : {},
3865
+ ...given.height !== void 0 ? { height: given.height } : {}
3866
+ },
3867
+ QUALITY[context.quality].defaultLongEdge,
3868
+ aspectOf(context.camera, context.renderer),
3869
+ { even: context.even ?? false }
3870
+ );
3871
+ const quality = call.imageQuality ?? defaults?.quality;
3872
+ return {
3873
+ ...size,
3874
+ format: call.format ?? defaults?.format ?? "png",
3875
+ ...quality !== void 0 ? { quality } : {},
3876
+ ...defaults?.opaque !== void 0 ? { opaque: defaults.opaque } : {}
3877
+ };
3878
+ }
3879
+ function cameraAspect(camera) {
3880
+ const view = camera;
3881
+ if (!view) return void 0;
3882
+ if (view.isPerspectiveCamera) return positive(view.aspect);
3883
+ if (view.isOrthographicCamera)
3884
+ return positive(
3885
+ ((view.right ?? 0) - (view.left ?? 0)) / ((view.top ?? 0) - (view.bottom ?? 0))
3886
+ );
3887
+ return void 0;
3888
+ }
3889
+ function rendererAspect(renderer) {
3890
+ const drawing = renderer;
3891
+ if (!drawing || typeof drawing !== "object") return void 0;
3892
+ if (typeof drawing.getDrawingBufferSize === "function") {
3893
+ try {
3894
+ const size = drawing.getDrawingBufferSize(new Vector2());
3895
+ const aspect = positive(size.x / size.y);
3896
+ if (aspect) return aspect;
3897
+ } catch {
3898
+ }
3899
+ }
3900
+ const canvas = drawing.domElement;
3901
+ return canvas ? positive((canvas.width ?? 0) / (canvas.height ?? 0)) : void 0;
3902
+ }
3903
+ function positive(value) {
3904
+ return value !== void 0 && Number.isFinite(value) && value > 0 ? value : void 0;
3905
+ }
3906
+
3907
+ // src/capture/stats.ts
3908
+ function tracedTriangles(scene, uploaded = analyzeScene(scene).triangles) {
3909
+ return uploaded + presetRoomTriangles(scene);
3910
+ }
3911
+
3912
+ // src/bake/room.ts
3913
+ var ON_THE_STAND_METRES = 0.05;
3914
+ function presetRoomBake(scene, candidates, include) {
3915
+ const rooms = findPresetRooms(scene);
3916
+ if (rooms.length === 0) return null;
3917
+ if (rooms.length > 1) {
3918
+ return {
3919
+ refused: {
3920
+ code: "BakeManyRooms",
3921
+ message: `The scene has ${rooms.length} preset rooms in it, and a prebaked room has to be the only one.`
3922
+ }
3923
+ };
3924
+ }
3925
+ const group = rooms[0];
3926
+ const room = roomPresetSpecOf(group);
3927
+ if (!room) return null;
3928
+ if (!roomIsPrebaked(room.name, room.lighting)) {
3929
+ return {
3930
+ refused: {
3931
+ code: "BakeRoomNotPrebaked",
3932
+ message: `There are no prebaked lightmaps for ${room.name} under ${room.lighting} light, so this bake covers the room itself.`
3933
+ }
3934
+ };
3935
+ }
3936
+ const changed = roomPrebakeChanges(room);
3937
+ if (changed.length) {
3938
+ return {
3939
+ refused: {
3940
+ code: "BakeRoomModified",
3941
+ message: `This room has ${changed.join(" and ")} changed, so its light is not the light we baked.`
3942
+ }
3943
+ };
3944
+ }
3945
+ const mine = candidates.filter(
3946
+ (candidate) => include.includes(candidate.objectId) && !isUnder(candidate.object, group)
3947
+ );
3948
+ if (!mine.length) {
3949
+ return {
3950
+ refused: {
3951
+ code: "BakeRoomOnly",
3952
+ message: "Everything this bake covers belongs to the room, so there is nothing a prebaked one would not already have."
3953
+ }
3954
+ };
3955
+ }
3956
+ const stand = group.getObjectByName("stand");
3957
+ const standIds = stand ? standsUnder(candidates, stand, mine) : [];
3958
+ const mineIds = mine.map((candidate) => candidate.objectId);
3959
+ return {
3960
+ ok: {
3961
+ room,
3962
+ group,
3963
+ standIds,
3964
+ castIds: mineIds,
3965
+ spanIds: [...mineIds, ...standIds]
3966
+ }
3967
+ };
3968
+ }
3969
+ function standsUnder(candidates, stand, mine) {
3970
+ const top = boundsOf(stand)?.high;
3971
+ if (top === void 0) return [];
3972
+ const lowest = Math.min(
3973
+ ...mine.map((candidate) => boundsOf(candidate.object)?.low ?? Infinity)
3974
+ );
3975
+ if (!Number.isFinite(lowest) || Math.abs(lowest - top) > ON_THE_STAND_METRES)
3976
+ return [];
3977
+ const ids = [];
3978
+ for (const candidate of candidates) {
3979
+ if (candidate.bakeable && isUnder(candidate.object, stand))
3980
+ ids.push(candidate.objectId);
3981
+ }
3982
+ return ids;
3983
+ }
3984
+ function boundsOf(object) {
3985
+ object.updateWorldMatrix(true, true);
3986
+ let low = Infinity;
3987
+ let high = -Infinity;
3988
+ object.traverse((node) => {
3989
+ const mesh = node;
3990
+ if (!mesh.isMesh || !mesh.geometry) return;
3991
+ if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox();
3992
+ const box = mesh.geometry.boundingBox;
3993
+ if (!box) return;
3994
+ const matrix = node.matrixWorld.elements;
3995
+ const scale = matrix[5] ?? 1;
3996
+ const offset = matrix[13] ?? 0;
3997
+ low = Math.min(low, box.min.y * scale + offset);
3998
+ high = Math.max(high, box.max.y * scale + offset);
3999
+ });
4000
+ return Number.isFinite(low) ? { low, high } : void 0;
4001
+ }
4002
+ function isUnder(object, root) {
4003
+ for (let node = object; node; node = node.parent)
4004
+ if (node === root) return true;
4005
+ return false;
4006
+ }
4007
+
4008
+ // src/bake/prepare.ts
4009
+ function planBake(scene, camera, settings = {}) {
4010
+ const quality = settings.quality ?? "standard";
4011
+ const textureSize = settings.textureSize ?? 1024;
4012
+ const candidates = bakeCandidates({ scene });
4013
+ let include = resolveInclude(candidates, settings);
4014
+ let preset;
4015
+ if (settings.room !== "full") {
4016
+ const verdict = presetRoomBake(scene, candidates, include);
4017
+ if (verdict && "ok" in verdict) {
4018
+ preset = verdict.ok;
4019
+ const keep = /* @__PURE__ */ new Set([...preset.spanIds]);
4020
+ include = include.filter((id) => keep.has(id));
4021
+ }
4022
+ }
4023
+ const standIns = resolveSimplify(scene, candidates, settings);
4024
+ const covered = new Set(include);
4025
+ standIns.entries = standIns.entries.filter(
4026
+ (entry) => entry.objects.some((id) => covered.has(id))
4027
+ );
4028
+ const replaced = new Set(standIns.entries.flatMap((entry) => entry.objects));
4029
+ include = include.filter((id) => !replaced.has(id));
4030
+ if (!include.length && !standIns.entries.length) {
4031
+ throw new Bakery3Error({
4032
+ code: "InvalidRequest",
4033
+ message: "There is nothing to bake.",
4034
+ why: candidates.length ? "Every mesh in the scene is either unbakeable or excluded. Instanced and skinned meshes cannot take a lightmap, and glass is off by default." : "The scene has no meshes.",
4035
+ fix: "Tick at least one object in the bake panel, or pass { settings: { include: [objectId] } }.",
4036
+ docs: "https://bakery3.com/docs/bake"
4037
+ });
4038
+ }
4039
+ if (include.length > MAX_BAKE_OBJECTS) {
4040
+ throw new Bakery3Error({
4041
+ code: "InvalidRequest",
4042
+ message: `A bake can cover ${MAX_BAKE_OBJECTS} objects; this one asks for ${include.length}.`,
4043
+ why: "Past that the scene wants a shared atlas, which this version does not build. Sampling the list down for you would quietly leave objects unlit.",
4044
+ fix: "Bake the hero objects, or merge small props into fewer meshes.",
4045
+ docs: "https://bakery3.com/docs/bake"
4046
+ });
4047
+ }
4048
+ const atlas = resolveAtlas(settings.atlas);
4049
+ const texels = standIns.entries.reduce(
4050
+ (total, entry) => total + (entry.textureSize ?? textureSize) ** 2,
4051
+ 0
4052
+ ) + (atlas ? atlas.size * atlas.size : include.reduce((total, id) => {
4053
+ const size = settings.sizes?.[id] ?? textureSize;
4054
+ return total + size * size;
4055
+ }, 0));
4056
+ if (texels > MAX_BAKE_TEXELS) {
4057
+ throw new Bakery3Error({
4058
+ code: "InvalidRequest",
4059
+ message: `This bake asks for ${(texels / 1e6).toFixed(1)}M texels; the ceiling is ${(MAX_BAKE_TEXELS / 1e6).toFixed(0)}M.`,
4060
+ why: `${include.length} objects \xD7 ${textureSize}\xB2 = ${(texels / 1e6).toFixed(1)}M texels, and texels are what a bake costs the way frames are what a video costs.`,
4061
+ fix: "Lower the texture size, or bake fewer objects at a time.",
4062
+ docs: "https://bakery3.com/docs/bake"
4063
+ });
4064
+ }
4065
+ const view = camera ? resolveView(settings.view ?? "camera", camera, candidates, include) : void 0;
4066
+ const probe = resolveProbe(candidates, include);
4067
+ const bake = {
4068
+ quality,
4069
+ textureSize,
4070
+ include,
4071
+ ...settings.sizes ? { sizes: settings.sizes } : {},
4072
+ ...atlas ? { atlas } : {},
4073
+ ...settings.margin !== void 0 ? { margin: settings.margin } : {},
4074
+ ...settings.deliverGlb ? { deliverGlb: true } : {},
4075
+ ...view ? { view } : {},
4076
+ probe,
4077
+ ...standIns.entries.length ? { simplify: standIns.entries } : {},
4078
+ // This SDK can apply a lightmap to a uv set the mesh already has, so the
4079
+ // render worker may keep an object's own layout rather than unwrap it;
4080
+ // these are the objects whose `uv1` is your own (a lightmap uv you
4081
+ // exported) rather than one a bake applied here wrote.
4082
+ existingUv: { uv1: developerUv1(candidates, include) },
4083
+ ...preset ? {
4084
+ room: "preset",
4085
+ // The plane is placed and sized by the render worker, which sees what
4086
+ // the objects stand on; all it is told is whose shadow it is and
4087
+ // what it has to clear. The stand is in the second list and not
4088
+ // the first: the prebaked floor already has its shadow.
4089
+ shadow: { cast: preset.castIds, spans: preset.spanIds }
4090
+ } : {}
4091
+ };
4092
+ return {
4093
+ bake,
4094
+ // For the quote, not for the cache: see `bakedTriangles` in the protocol.
4095
+ bakedTriangles: countTriangles(candidates, include),
4096
+ candidates,
4097
+ ...preset ? { preset } : {},
4098
+ shapes: standIns.shapes
4099
+ };
4100
+ }
4101
+ function stageBake(plan, scene) {
4102
+ const restore = plan.shapes.map((shape) => {
4103
+ const parent = shape.parent;
4104
+ const visible = shape.visible;
4105
+ if (!isUnder2(shape, scene)) scene.add(shape);
4106
+ shape.visible = false;
4107
+ return () => {
4108
+ shape.visible = visible;
4109
+ if (parent !== shape.parent) {
4110
+ if (parent) parent.add(shape);
4111
+ else shape.removeFromParent();
4112
+ }
4113
+ };
4114
+ });
4115
+ if (plan.preset)
4116
+ restore.push(...holdRoomBack(plan.preset, plan.bake.include));
4117
+ return () => {
4118
+ for (const undo of restore) undo();
4119
+ };
4120
+ }
4121
+ function bakeQuoteRequest(bake, facts) {
4122
+ return {
4123
+ bake: {
4124
+ quality: bake.quality,
4125
+ textureSize: bake.textureSize,
4126
+ include: bake.include,
4127
+ ...bake.sizes ? { sizes: bake.sizes } : {},
4128
+ ...bake.atlas ? { atlas: bake.atlas } : {},
4129
+ ...bake.deliverGlb ? { deliverGlb: true } : {},
4130
+ ...bake.simplify ? { simplify: bake.simplify } : {},
4131
+ // The shadow plane: two traces of its own.
4132
+ ...bake.shadow ? { shadow: bake.shadow } : {},
4133
+ // Inside a room a ray that misses keeps going, which is most of
4134
+ // what one of these bakes costs.
4135
+ ...facts.enclosed ? { enclosed: true } : {}
4136
+ },
4137
+ scene: { triangles: facts.triangles },
4138
+ bakedTriangles: facts.bakedTriangles
4139
+ };
4140
+ }
4141
+ function bakeQuoteOf(spec) {
4142
+ return bakeQuoteRequest(spec.bake, {
4143
+ triangles: spec.manifest.stats?.triangles ?? 0,
4144
+ bakedTriangles: spec.bakedTriangles ?? 0,
4145
+ enclosed: Boolean(spec.manifest.room)
4146
+ });
4147
+ }
4148
+ function quoteBake(scene, camera, settings = {}) {
4149
+ const plan = planBake(scene, camera, settings);
4150
+ const undo = stageBake(plan, scene);
4151
+ let triangles;
4152
+ try {
4153
+ triangles = tracedTriangles(scene);
4154
+ } finally {
4155
+ undo();
4156
+ }
4157
+ return bakeQuoteRequest(plan.bake, {
4158
+ triangles,
4159
+ bakedTriangles: plan.bakedTriangles,
4160
+ enclosed: findPresetRooms(scene).length > 0
4161
+ });
4162
+ }
4163
+ async function prepareBake(options) {
4164
+ const plan = planBake(options.scene, options.camera, options.settings ?? {});
4165
+ const undo = stageBake(plan, options.scene);
4166
+ let captured;
4167
+ try {
4168
+ captured = await captureScene({
4169
+ scene: options.scene,
4170
+ camera: options.camera,
4171
+ ...options.renderer !== void 0 ? { renderer: options.renderer } : {},
4172
+ ...options.environment !== void 0 ? { environment: options.environment } : {},
4173
+ ...options.productState !== void 0 ? { productState: options.productState } : {},
4174
+ ...options.background !== void 0 ? { background: options.background } : {}
4175
+ });
4176
+ } finally {
4177
+ undo();
4178
+ }
4179
+ const { manifest, assets } = captured;
4180
+ const spec = {
4181
+ manifest,
4182
+ bake: plan.bake,
4183
+ bakedTriangles: plan.bakedTriangles,
4184
+ ...options.maxCost !== void 0 ? { maxCost: options.maxCost } : {},
4185
+ ...options.metadata ? { metadata: options.metadata } : {},
4186
+ ...options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}
4187
+ };
4188
+ const preset = plan.preset;
4189
+ return {
4190
+ spec,
4191
+ assets,
4192
+ candidates: plan.candidates,
4193
+ ...preset ? {
4194
+ presetRoom: {
4195
+ name: preset.room.name,
4196
+ lighting: preset.room.lighting,
4197
+ build: preset.room.build
4198
+ }
4199
+ } : {}
4200
+ };
4201
+ }
4202
+ function holdRoomBack(preset, include) {
4203
+ const undo = [];
4204
+ const state = preset.group.userData?.bakery3Preset?.roomPreset;
4205
+ if (!state?.standIn) return undo;
4206
+ const shipping = new Set(
4207
+ include.filter((id) => preset.standIds.includes(id))
4208
+ );
4209
+ const stand = preset.group.getObjectByName("stand");
4210
+ preset.group.traverse((object) => {
4211
+ if (!object.isMesh) return;
4212
+ if (stand && isUnder2(object, stand) && shipping.has(idOf(object))) return;
4213
+ const before = object.userData.bakery3;
4214
+ object.userData.bakery3 = { ...before ?? {}, exclude: true };
4215
+ undo.push(() => {
4216
+ if (before === void 0) delete object.userData.bakery3;
4217
+ else object.userData.bakery3 = before;
4218
+ });
4219
+ });
4220
+ const was = state.standIn;
4221
+ state.standIn = shipping.size ? "stand" : void 0;
4222
+ undo.push(() => {
4223
+ state.standIn = was;
4224
+ });
4225
+ return undo;
4226
+ }
4227
+ function developerUv1(candidates, include) {
4228
+ const wanted = new Set(include);
4229
+ return candidates.filter((candidate) => {
4230
+ if (!wanted.has(candidate.objectId)) return false;
4231
+ const uv1 = candidate.object.geometry?.getAttribute("uv1");
4232
+ return Boolean(uv1) && !isBakeWrittenUv(uv1);
4233
+ }).map((candidate) => candidate.objectId);
4234
+ }
4235
+ function countTriangles(candidates, include) {
4236
+ const wanted = new Set(include);
4237
+ let total = 0;
4238
+ for (const candidate of candidates) {
4239
+ if (!wanted.has(candidate.objectId)) continue;
4240
+ const geometry = candidate.object.geometry;
4241
+ const count = geometry?.index?.count ?? geometry?.attributes?.position?.count ?? 0;
4242
+ total += Math.floor(count / 3);
4243
+ }
4244
+ return total;
4245
+ }
4246
+ function isUnder2(object, root) {
4247
+ for (let node = object; node; node = node.parent)
4248
+ if (node === root) return true;
4249
+ return false;
4250
+ }
4251
+ function mergeGroups(inputs, docs) {
4252
+ const out = [];
4253
+ const at = /* @__PURE__ */ new Map();
4254
+ for (const input of inputs) {
4255
+ if (!input.group) {
4256
+ out.push(input);
4257
+ continue;
4258
+ }
4259
+ const index = at.get(input.group);
4260
+ if (index === void 0) {
4261
+ at.set(input.group, out.length);
4262
+ out.push({
4263
+ ...input,
4264
+ objects: [
4265
+ ...Array.isArray(input.objects) ? input.objects : [input.objects]
4266
+ ]
4267
+ });
4268
+ continue;
4269
+ }
4270
+ const first = out[index];
4271
+ const differs = (input.to ?? "lowpoly") !== (first.to ?? "lowpoly") || input.triangles !== first.triangles || input.textureSize !== first.textureSize;
4272
+ if (differs) {
4273
+ throw new Bakery3Error({
4274
+ code: "InvalidRequest",
4275
+ message: `The stand-ins grouped as "${input.group}" do not ask for the same thing.`,
4276
+ why: "A group is one stand-in, so it can only be built one way.",
4277
+ fix: "Give them the same `to`, `triangles` and `textureSize`, or take them out of the group.",
4278
+ docs
4279
+ });
4280
+ }
4281
+ first.objects.push(
4282
+ ...Array.isArray(input.objects) ? input.objects : [input.objects]
4283
+ );
4284
+ }
4285
+ return out;
4286
+ }
4287
+ function resolveSimplify(scene, candidates, settings) {
4288
+ const docs = "https://bakery3.com/docs/bake#simplify";
4289
+ const inputs = [...settings.simplify ?? []];
4290
+ scene.traverse((object) => {
4291
+ const declared = object.userData?.bakery3?.simplify;
4292
+ if (declared === void 0 || declared === null) return;
4293
+ const options = typeof declared === "object" && !declared.isMesh ? declared : { to: declared };
4294
+ inputs.push({ ...options, objects: object });
4295
+ });
4296
+ if (!inputs.length) return { entries: [], shapes: [] };
4297
+ const merged = mergeGroups(inputs, docs);
4298
+ if (merged.length > MAX_BAKE_SIMPLIFY) {
4299
+ throw new Bakery3Error({
4300
+ code: "InvalidRequest",
4301
+ message: `A bake can build ${MAX_BAKE_SIMPLIFY} stand-ins; this one asks for ${merged.length}.`,
4302
+ why: "Each stand-in is three traced passes of its own.",
4303
+ fix: "Group more objects under each entry: one stand-in can replace a whole shelf of them.",
4304
+ docs
4305
+ });
4306
+ }
4307
+ const byId = new Map(
4308
+ candidates.map((candidate) => [candidate.objectId, candidate])
4309
+ );
4310
+ const claimed = /* @__PURE__ */ new Map();
4311
+ const shapes = [];
4312
+ const entries = merged.map((input, n) => {
4313
+ const shape = typeof input.to === "object" && input.to !== null ? input.to : void 0;
4314
+ const shapeId = shape ? idOf(shape) : void 0;
4315
+ const ids = [];
4316
+ for (const target of Array.isArray(input.objects) ? input.objects : [input.objects]) {
4317
+ if (typeof target === "string") {
4318
+ if (!byId.has(target)) {
4319
+ throw new Bakery3Error({
4320
+ code: "InvalidRequest",
4321
+ message: `simplify[${n}] names "${target}", which is not a mesh in this scene.`,
4322
+ why: "Ids are matched against userData.bakery3Id, falling back to the object uuid, which three mints anew on every reload.",
4323
+ fix: "Pass the object itself, or take the id from candidates().",
4324
+ docs
4325
+ });
4326
+ }
4327
+ ids.push(target);
4328
+ continue;
4329
+ }
4330
+ for (const candidate of candidates) {
4331
+ if (candidate.objectId !== shapeId && isUnder2(candidate.object, target))
4332
+ ids.push(candidate.objectId);
4333
+ }
4334
+ }
4335
+ const objects = [...new Set(ids)];
4336
+ if (!objects.length) {
4337
+ throw new Bakery3Error({
4338
+ code: "InvalidRequest",
4339
+ message: `simplify[${n}] replaces nothing: there is no mesh under what it names.`,
4340
+ why: "A stand-in is made from the meshes it replaces.",
4341
+ fix: "Pass a mesh, or a group with meshes under it.",
4342
+ docs
4343
+ });
4344
+ }
4345
+ for (const id of objects) {
4346
+ const other = claimed.get(id);
4347
+ if (other !== void 0) {
4348
+ throw new Bakery3Error({
4349
+ code: "InvalidRequest",
4350
+ message: `"${byId.get(id)?.name ?? id}" is claimed by simplify[${other}] and simplify[${n}].`,
4351
+ why: "An object is replaced by one stand-in. A group and something inside it were probably both named.",
4352
+ fix: "Name the group or its parts, not both.",
4353
+ docs
4354
+ });
4355
+ }
4356
+ claimed.set(id, n);
4357
+ }
4358
+ if (input.textureSize !== void 0 && !BAKE_TEXTURE_SIZES.includes(input.textureSize)) {
4359
+ throw new Bakery3Error({
4360
+ code: "InvalidRequest",
4361
+ message: `simplify[${n}].textureSize must be one of ${BAKE_TEXTURE_SIZES.join(", ")}; got ${String(input.textureSize)}.`,
4362
+ fix: "Leave it out to use the bake's textureSize.",
4363
+ docs
4364
+ });
4365
+ }
4366
+ if (input.triangles !== void 0 && !(Number.isInteger(input.triangles) && input.triangles >= 12 && input.triangles <= MAX_BAKE_SIMPLIFY_TRIANGLES)) {
4367
+ throw new Bakery3Error({
4368
+ code: "InvalidRequest",
4369
+ message: `simplify[${n}].triangles must be a whole number from 12 to ${MAX_BAKE_SIMPLIFY_TRIANGLES}; got ${String(input.triangles)}.`,
4370
+ fix: "A few hundred is a piece of furniture across a room; a few thousand holds up close.",
4371
+ docs
4372
+ });
4373
+ }
4374
+ if (shape) {
4375
+ if (!shape.isMesh || !shape.geometry) {
4376
+ throw new Bakery3Error({
4377
+ code: "InvalidRequest",
4378
+ message: `simplify[${n}].to must be 'lowpoly', 'box', 'billboard', 'decimate' or a Mesh.`,
4379
+ fix: "Pass a THREE.Mesh whose geometry is the low-poly shape, placed where the objects are.",
4380
+ docs
4381
+ });
4382
+ }
4383
+ shapes.push(shape);
4384
+ }
4385
+ return {
4386
+ objects,
4387
+ to: shape ? { objectId: shapeId } : input.to ?? "lowpoly",
4388
+ ...input.triangles !== void 0 ? { triangles: input.triangles } : {},
4389
+ ...input.textureSize !== void 0 ? { textureSize: input.textureSize } : {}
4390
+ };
4391
+ });
4392
+ const expanded = entries.flatMap(
4393
+ (entry) => entry.to === "decimate" && entry.objects.length > 1 ? entry.objects.map((id) => ({ ...entry, objects: [id] })) : [entry]
4394
+ );
4395
+ if (expanded.length > MAX_BAKE_SIMPLIFY) {
4396
+ throw new Bakery3Error({
4397
+ code: "InvalidRequest",
4398
+ message: `A bake can build ${MAX_BAKE_SIMPLIFY} stand-ins; decimating every object named here asks for ${expanded.length}.`,
4399
+ why: "Each decimated object is a stand-in of its own.",
4400
+ fix: "Decimate the objects that are heavy and leave the rest as they are.",
4401
+ docs
4402
+ });
4403
+ }
4404
+ return { entries: expanded, shapes };
4405
+ }
4406
+ function resolveView(setting, camera, candidates, include) {
4407
+ if (setting === "none") return void 0;
4408
+ if (setting !== "camera") {
4409
+ const position = setting.position;
4410
+ if (!Array.isArray(position) || position.length !== 3 || position.some(
4411
+ (value) => typeof value !== "number" || !Number.isFinite(value)
4412
+ )) {
4413
+ throw new Bakery3Error({
4414
+ code: "InvalidRequest",
4415
+ message: "settings.view.position must be three finite numbers.",
4416
+ why: "The render worker places UV seams on the side of each object that faces away from this point, so it has to be a point.",
4417
+ fix: "Pass { view: { position: [x, y, z] } } in scene units, 'camera' to derive it from the camera, or 'none'.",
4418
+ docs: "https://bakery3.com/docs/bake#seams"
4419
+ });
4420
+ }
4421
+ return { position: [position[0], position[1], position[2]] };
4422
+ }
4423
+ if (typeof camera.getWorldPosition !== "function")
4424
+ return void 0;
4425
+ const wanted = new Set(include);
4426
+ const objects = candidates.filter((candidate) => wanted.has(candidate.objectId)).map((candidate) => candidate.object);
4427
+ return quantizeBakeView(camera, bakeBounds(objects));
4428
+ }
4429
+ function resolveAtlas(setting) {
4430
+ if (!setting) return void 0;
4431
+ const atlas = setting === true ? { size: 2048 } : { ...setting };
4432
+ if (!BAKE_ATLAS_SIZES.includes(atlas.size)) {
4433
+ throw new Bakery3Error({
4434
+ code: "InvalidRequest",
4435
+ message: `settings.atlas.size must be one of ${BAKE_ATLAS_SIZES.join(", ")}; got ${String(atlas.size)}.`,
4436
+ why: "The atlas is one texture shared by every baked object, and its side is what the whole bake costs in texels.",
4437
+ fix: "Pass atlas: true for 2048\xB2, or { size: 4096 } for a large room.",
4438
+ docs: "https://bakery3.com/docs/bake#one-map-for-the-room"
4439
+ });
4440
+ }
4441
+ for (const [id, weight] of Object.entries(atlas.weights ?? {})) {
4442
+ if (typeof weight !== "number" || !Number.isFinite(weight) || weight <= 0) {
4443
+ throw new Bakery3Error({
4444
+ code: "InvalidRequest",
4445
+ message: `settings.atlas.weights["${id}"] must be a positive number; got ${String(weight)}.`,
4446
+ why: "A weight multiplies the texel density an object gets in the shared map; zero or less would leave it with no texels at all.",
4447
+ fix: "Use 1 for an ordinary object and 4 for the hero product (weights round to a power of two in side).",
4448
+ docs: "https://bakery3.com/docs/bake#one-map-for-the-room"
4449
+ });
4450
+ }
4451
+ }
4452
+ return atlas;
4453
+ }
4454
+ function resolveInclude(candidates, settings) {
4455
+ const byId = new Map(
4456
+ candidates.map((candidate) => [candidate.objectId, candidate])
4457
+ );
4458
+ const excluded = new Set(settings.exclude ?? []);
4459
+ if (!settings.include) {
4460
+ return defaultIncludeIds(candidates).filter((id) => !excluded.has(id));
4461
+ }
4462
+ const resolved = [];
4463
+ for (const id of settings.include) {
4464
+ if (excluded.has(id)) continue;
4465
+ const candidate = byId.get(id);
4466
+ if (!candidate) {
4467
+ throw new Bakery3Error({
4468
+ code: "InvalidRequest",
4469
+ message: `Asked to bake "${id}", which is not a mesh in this scene.`,
4470
+ why: "The include list is matched against userData.bakery3Id, falling back to the object uuid. three mints a new uuid on every reload, so an id saved from a previous session will not match.",
4471
+ fix: "Set userData.bakery3Id on the objects you bake so their ids survive a reload, or take the ids from candidates().",
4472
+ docs: "https://bakery3.com/docs/bake"
4473
+ });
4474
+ }
4475
+ if (!candidate.bakeable) {
4476
+ throw new Bakery3Error({
4477
+ code: "InvalidRequest",
4478
+ message: candidate.reason?.message ?? `"${candidate.name}" cannot take a lightmap.`,
4479
+ ...candidate.reason?.why ? { why: candidate.reason.why } : {},
4480
+ ...candidate.reason?.fix ? { fix: candidate.reason.fix } : {},
4481
+ path: candidate.path,
4482
+ docs: "https://bakery3.com/docs/bake"
4483
+ });
4484
+ }
4485
+ if (!resolved.includes(id)) resolved.push(id);
4486
+ }
4487
+ return resolved;
4488
+ }
4489
+
4490
+ export { OBJECT_PROPERTIES, Timeline, analyzeScene, aspectOf, bakeQuoteOf, bindTimelineContext, captureScene, createTimeline, extractMaterialShader, isNodeMaterial, linearToSrgbHex, outputSize, prepareBake, quoteBake, resolveOutput, sceneComplexity, timelineFromGsap, timelineFromTheatre, tracedTriangles };
4491
+ //# sourceMappingURL=chunk-CX4WTVGH.js.map
4492
+ //# sourceMappingURL=chunk-CX4WTVGH.js.map