@oeave/bakery3 0.0.0-stage → 0.2.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-DZ-CJR6f.d.ts +1364 -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-3G5QL4F4.js +145 -0
  13. package/dist/chunk-3G5QL4F4.js.map +1 -0
  14. package/dist/chunk-4E5VV4QY.js +12 -0
  15. package/dist/chunk-4E5VV4QY.js.map +1 -0
  16. package/dist/chunk-62M6XXNX.js +142 -0
  17. package/dist/chunk-62M6XXNX.js.map +1 -0
  18. package/dist/chunk-6FYI6AJM.js +1157 -0
  19. package/dist/chunk-6FYI6AJM.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-APWUCEEB.js +118 -0
  23. package/dist/chunk-APWUCEEB.js.map +1 -0
  24. package/dist/chunk-HNMSWQU7.js +1709 -0
  25. package/dist/chunk-HNMSWQU7.js.map +1 -0
  26. package/dist/chunk-JFYUDERE.js +1412 -0
  27. package/dist/chunk-JFYUDERE.js.map +1 -0
  28. package/dist/chunk-KNUAOILG.js +551 -0
  29. package/dist/chunk-KNUAOILG.js.map +1 -0
  30. package/dist/chunk-KZLVTSBI.js +191 -0
  31. package/dist/chunk-KZLVTSBI.js.map +1 -0
  32. package/dist/chunk-LAKXC4WR.js +2028 -0
  33. package/dist/chunk-LAKXC4WR.js.map +1 -0
  34. package/dist/chunk-NXBAZGNB.js +1107 -0
  35. package/dist/chunk-NXBAZGNB.js.map +1 -0
  36. package/dist/chunk-QRCT5UGZ.js +3286 -0
  37. package/dist/chunk-QRCT5UGZ.js.map +1 -0
  38. package/dist/chunk-S4AJTLLN.js +23 -0
  39. package/dist/chunk-S4AJTLLN.js.map +1 -0
  40. package/dist/chunk-UWBP7B54.js +92 -0
  41. package/dist/chunk-UWBP7B54.js.map +1 -0
  42. package/dist/chunk-XK35ANPJ.js +346 -0
  43. package/dist/chunk-XK35ANPJ.js.map +1 -0
  44. package/dist/chunk-Z7IYVH22.js +4781 -0
  45. package/dist/chunk-Z7IYVH22.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 +975 -0
  58. package/dist/index.js +15 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/node/index.cjs +5240 -0
  61. package/dist/node/index.cjs.map +1 -0
  62. package/dist/node/index.d.cts +4927 -0
  63. package/dist/node/index.d.ts +597 -0
  64. package/dist/node/index.js +1328 -0
  65. package/dist/node/index.js.map +1 -0
  66. package/dist/prepare-BtjY4G3q.d.ts +112 -0
  67. package/dist/presets/index.d.ts +562 -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-FS26KAPQ.js +9 -0
  74. package/dist/room-FS26KAPQ.js.map +1 -0
  75. package/dist/rooms.gen-DItzBR9k.d.ts +1462 -0
  76. package/dist/session-VCIQEO26.js +9 -0
  77. package/dist/session-VCIQEO26.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-20OARnQr.d.ts +1139 -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,23 @@
1
+ // src/shader/registry.ts
2
+ var BUILTINS_BY_UUID = /* @__PURE__ */ new Map();
3
+ var NODE_OBJECT = null;
4
+ function registerBuiltin(node, entry) {
5
+ if (node?.uuid) BUILTINS_BY_UUID.set(node.uuid, entry);
6
+ }
7
+ function builtinFor(uuid) {
8
+ return BUILTINS_BY_UUID.get(uuid);
9
+ }
10
+ function registerNodeObject(fn) {
11
+ NODE_OBJECT = fn;
12
+ }
13
+ function toNodeObject(value) {
14
+ return NODE_OBJECT ? NODE_OBJECT(value) : value;
15
+ }
16
+ function tslRuntimeRegistered() {
17
+ return NODE_OBJECT !== null;
18
+ }
19
+ var BAKERY_NODE_MARKER = "__bakery3EngineNode";
20
+
21
+ export { BAKERY_NODE_MARKER, builtinFor, registerBuiltin, registerNodeObject, toNodeObject, tslRuntimeRegistered };
22
+ //# sourceMappingURL=chunk-S4AJTLLN.js.map
23
+ //# sourceMappingURL=chunk-S4AJTLLN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shader/registry.ts"],"names":[],"mappings":";AAwBA,IAAM,gBAAA,uBAAuB,GAAA,EAA0B;AAOvD,IAAI,WAAA,GAAoD,IAAA;AAEjD,SAAS,eAAA,CACd,MACA,KAAA,EACM;AACN,EAAA,IAAI,MAAM,IAAA,EAAM,gBAAA,CAAiB,GAAA,CAAI,IAAA,CAAK,MAAM,KAAK,CAAA;AACvD;AAEO,SAAS,WAAW,IAAA,EAAwC;AACjE,EAAA,OAAO,gBAAA,CAAiB,IAAI,IAAI,CAAA;AAClC;AAEO,SAAS,mBAAmB,EAAA,EAAuC;AACxE,EAAA,WAAA,GAAc,EAAA;AAChB;AAEO,SAAS,aAAa,KAAA,EAAyB;AACpD,EAAA,OAAO,WAAA,GAAc,WAAA,CAAY,KAAK,CAAA,GAAI,KAAA;AAC5C;AAEO,SAAS,oBAAA,GAAgC;AAC9C,EAAA,OAAO,WAAA,KAAgB,IAAA;AACzB;AAOO,IAAM,kBAAA,GAAqB","file":"chunk-S4AJTLLN.js","sourcesContent":["/**\n * The extractor's runtime registry: how it recognizes TSL's singletons.\n *\n * The extractor itself (extract.ts) imports nothing from three, on purpose:\n * `check()` must work for every SDK user, and `three/tsl` only exists from\n * r167. But some TSL builtins (`positionWorld`, `cameraPosition`, `time`) are\n * anonymous singleton objects with no recognizable structure, so the only\n * reliable way to know one is to have been introduced.\n *\n * `@oeave/bakery3/tsl` does the introducing: importing it registers every\n * builtin singleton here by uuid. Without it, the structurally recognizable\n * builtins (`normalWorld` and friends carry their names) still extract, and\n * the anonymous ones produce a compatibility issue whose fix line says to\n * import the entry.\n */\n\nexport type BuiltinEntry = {\n op: string;\n /** A function so `time` can read its live value at capture, not at import. */\n params?: () => Record<string, number | string | boolean | number[]>;\n};\n\n/** Uuid to op. Uuid rather than object identity: TSL wraps nodes in proxies,\n * and two proxies of one node are equal by uuid and nothing else. */\nconst BUILTINS_BY_UUID = new Map<string, BuiltinEntry>();\n\n/**\n * `nodeObject` from three/tsl, once something has provided it. The extractor\n * uses it to expand `Fn` calls whose arguments were plain numbers; without it\n * those calls are flagged instead of expanded.\n */\nlet NODE_OBJECT: ((value: unknown) => unknown) | null = null;\n\nexport function registerBuiltin(\n node: { uuid?: string } | null | undefined,\n entry: BuiltinEntry,\n): void {\n if (node?.uuid) BUILTINS_BY_UUID.set(node.uuid, entry);\n}\n\nexport function builtinFor(uuid: string): BuiltinEntry | undefined {\n return BUILTINS_BY_UUID.get(uuid);\n}\n\nexport function registerNodeObject(fn: (value: unknown) => unknown): void {\n NODE_OBJECT = fn;\n}\n\nexport function toNodeObject(value: unknown): unknown {\n return NODE_OBJECT ? NODE_OBJECT(value) : value;\n}\n\nexport function tslRuntimeRegistered(): boolean {\n return NODE_OBJECT !== null;\n}\n\n/**\n * The marker `@oeave/bakery3/tsl` stamps onto a bakery-only node. The\n * extractor checks for it before anything else: the node's TSL subtree is the\n * browser preview, and the marker is the real material.\n */\nexport const BAKERY_NODE_MARKER = '__bakery3EngineNode';\n\nexport type BakeryNodeMarker = {\n op: string;\n params?: Record<string, number | string | boolean | number[]>;\n /** Logical inputs, extracted instead of the preview subtree. */\n inputs?: unknown[];\n};\n"]}
@@ -0,0 +1,92 @@
1
+ // src/protocol/manifest.ts
2
+ var PROTOCOL_VERSION = "5.0";
3
+ var PROTOCOL_VERSION_ROOM = "4.0";
4
+ var PROTOCOL_VERSION_ANIMATED = "3.0";
5
+ var PROTOCOL_VERSION_STILL = "2.0";
6
+ function protocolVersionFor(manifest) {
7
+ if (manifest.environment?.fog) return PROTOCOL_VERSION;
8
+ if (manifest.room) return PROTOCOL_VERSION_ROOM;
9
+ return manifest.animation ? PROTOCOL_VERSION_ANIMATED : PROTOCOL_VERSION_STILL;
10
+ }
11
+ var VIDEO_FORMATS = ["mp4", "webm"];
12
+ var SEQUENCE_FORMATS = ["png-sequence"];
13
+ function isVideoFormat(format) {
14
+ return VIDEO_FORMATS.includes(format);
15
+ }
16
+ function isSequenceFormat(format) {
17
+ return SEQUENCE_FORMATS.includes(format);
18
+ }
19
+ function isClipFormat(format) {
20
+ return format !== void 0 && (isVideoFormat(format) || isSequenceFormat(format));
21
+ }
22
+
23
+ // src/protocol/quality.ts
24
+ var QUALITY = {
25
+ /**
26
+ * "Show me roughly what this will look like." Cheap enough to sit inside a
27
+ * click, noisy enough that nobody will ship it.
28
+ *
29
+ * No denoiser, since version 3 (2026-10-03): with it, 32 samples of a
30
+ * product came out close to a studio render, so the cheapest tier was a
31
+ * finished image at half the price. The grain says draft, every choice a
32
+ * preview is for (framing, light, materials, color) still reads through
33
+ * it, and most of a preview's time was the denoiser.
34
+ */
35
+ preview: {
36
+ version: 3,
37
+ samples: 32,
38
+ adaptiveThreshold: 0.08,
39
+ maxBounces: 4,
40
+ diffuseBounces: 2,
41
+ glossyBounces: 4,
42
+ transmissionBounces: 4,
43
+ denoise: false,
44
+ defaultLongEdge: 1024,
45
+ preview: false,
46
+ previewSamples: 16
47
+ },
48
+ /**
49
+ * The default. Deep enough on glossy and transmission to carry glaze,
50
+ * metal and glass, which is where a product shot lives, and shallow on
51
+ * diffuse, which buys almost nothing on a product against a transparent
52
+ * background. The threshold ends most pixels well before the ceiling; the
53
+ * denoiser carries the noisiest ones.
54
+ */
55
+ studio: {
56
+ version: 2,
57
+ samples: 256,
58
+ adaptiveThreshold: 0.02,
59
+ maxBounces: 8,
60
+ diffuseBounces: 3,
61
+ glossyBounces: 8,
62
+ transmissionBounces: 8,
63
+ denoise: true,
64
+ defaultLongEdge: 2048,
65
+ preview: true,
66
+ previewSamples: 24
67
+ },
68
+ /**
69
+ * For the hero image that goes on the homepage. Roughly 8x studio's sample
70
+ * ceiling and a much stricter threshold.
71
+ */
72
+ ultra: {
73
+ version: 1,
74
+ samples: 2048,
75
+ adaptiveThreshold: 5e-3,
76
+ maxBounces: 16,
77
+ diffuseBounces: 6,
78
+ glossyBounces: 16,
79
+ transmissionBounces: 16,
80
+ denoise: true,
81
+ defaultLongEdge: 4096,
82
+ preview: true,
83
+ previewSamples: 32
84
+ }
85
+ };
86
+
87
+ // src/version.ts
88
+ var SDK_VERSION = "0.2.0";
89
+
90
+ export { QUALITY, SDK_VERSION, isClipFormat, isVideoFormat, protocolVersionFor };
91
+ //# sourceMappingURL=chunk-UWBP7B54.js.map
92
+ //# sourceMappingURL=chunk-UWBP7B54.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/protocol/manifest.ts","../src/protocol/quality.ts","../src/version.ts"],"names":[],"mappings":";AAwCO,IAAM,gBAAA,GAAmB,KAAA;AAGzB,IAAM,qBAAA,GAAwB,KAAA;AAG9B,IAAM,yBAAA,GAA4B,KAAA;AAQlC,IAAM,sBAAA,GAAyB,KAAA;AAW/B,SAAS,mBAAmB,QAAA,EAIxB;AACT,EAAA,IAAI,QAAA,CAAS,WAAA,EAAa,GAAA,EAAK,OAAO,gBAAA;AACtC,EAAA,IAAI,QAAA,CAAS,MAAM,OAAO,qBAAA;AAC1B,EAAA,OAAO,QAAA,CAAS,YACZ,yBAAA,GACA,sBAAA;AACN;AAwTO,IAAM,aAAA,GAAgC,CAAC,KAAA,EAAO,MAAM,CAAA;AAUpD,IAAM,gBAAA,GAAmC,CAAC,cAAc,CAAA;AAGxD,SAAS,cAAc,MAAA,EAA+B;AAC3D,EAAA,OAAO,aAAA,CAAc,SAAS,MAAM,CAAA;AACtC;AAGO,SAAS,iBAAiB,MAAA,EAA+B;AAC9D,EAAA,OAAO,gBAAA,CAAiB,SAAS,MAAM,CAAA;AACzC;AAQO,SAAS,aAAa,MAAA,EAAqC;AAChE,EAAA,OACE,WAAW,MAAA,KACV,aAAA,CAAc,MAAsB,CAAA,IACnC,iBAAiB,MAAsB,CAAA,CAAA;AAE7C;;;ACjYO,IAAM,OAAA,GAAkD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAW7D,OAAA,EAAS;AAAA,IACP,OAAA,EAAS,CAAA;AAAA,IACT,OAAA,EAAS,EAAA;AAAA,IACT,iBAAA,EAAmB,IAAA;AAAA,IACnB,UAAA,EAAY,CAAA;AAAA,IACZ,cAAA,EAAgB,CAAA;AAAA,IAChB,aAAA,EAAe,CAAA;AAAA,IACf,mBAAA,EAAqB,CAAA;AAAA,IACrB,OAAA,EAAS,KAAA;AAAA,IACT,eAAA,EAAiB,IAAA;AAAA,IACjB,OAAA,EAAS,KAAA;AAAA,IACT,cAAA,EAAgB;AAAA,GAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAA,EAAQ;AAAA,IACN,OAAA,EAAS,CAAA;AAAA,IACT,OAAA,EAAS,GAAA;AAAA,IACT,iBAAA,EAAmB,IAAA;AAAA,IACnB,UAAA,EAAY,CAAA;AAAA,IACZ,cAAA,EAAgB,CAAA;AAAA,IAChB,aAAA,EAAe,CAAA;AAAA,IACf,mBAAA,EAAqB,CAAA;AAAA,IACrB,OAAA,EAAS,IAAA;AAAA,IACT,eAAA,EAAiB,IAAA;AAAA,IACjB,OAAA,EAAS,IAAA;AAAA,IACT,cAAA,EAAgB;AAAA,GAClB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,KAAA,EAAO;AAAA,IACL,OAAA,EAAS,CAAA;AAAA,IACT,OAAA,EAAS,IAAA;AAAA,IACT,iBAAA,EAAmB,IAAA;AAAA,IACnB,UAAA,EAAY,EAAA;AAAA,IACZ,cAAA,EAAgB,CAAA;AAAA,IAChB,aAAA,EAAe,EAAA;AAAA,IACf,mBAAA,EAAqB,EAAA;AAAA,IACrB,OAAA,EAAS,IAAA;AAAA,IACT,eAAA,EAAiB,IAAA;AAAA,IACjB,OAAA,EAAS,IAAA;AAAA,IACT,cAAA,EAAgB;AAAA;AAEpB;;;AClGO,IAAM,WAAA,GAAc","file":"chunk-UWBP7B54.js","sourcesContent":["/**\n * The scene manifest: a GLB plus a versioned sidecar.\n *\n * glTF carries geometry, materials, textures, cameras, lights, skins and\n * morph targets well. What it does not carry is the state the running\n * application was in at the moment somebody pressed Render: which objects\n * were hidden, which of four cameras was live, what the renderer's tone\n * mapping was set to, which materials the SDK could not translate and what\n * the developer wants done about them. So the GLB is an asset and this is\n * the contract. `protocolVersion` is versioned independently of the SDK, of\n * the renderer and of the material translator, which is what makes a render\n * from six months ago reproducible.\n *\n * Everything here is JSON (no class instances, no functions) because it\n * travels from the browser through the control plane to the render worker\n * and is stored verbatim for replay.\n */\n\nimport type { AnimationSpec } from './animation.js';\nimport type { RoomPresetSpec } from './room.js';\nimport type { MaterialShaderSpec } from './shader.js';\n\n/**\n * Bumped only for a change a worker cannot read straight through. A worker\n * refuses a manifest whose major it does not implement rather than guessing;\n * a plausible wrong image is worse than a refusal.\n *\n * 2.0: `shaders`, node-material graphs. Without them a worker would render\n * the exporter's gray stand-in as if it were the material.\n *\n * 3.0: `animation`, baked per-frame channels. Without them a worker would\n * hand back one still of frame zero for a five-second orbit.\n *\n * 4.0: `room`, a room preset the worker rebuilds itself (see room.ts). The\n * room's geometry is deliberately not in the GLB, so a worker that ignored\n * the field would render the product floating in nothing.\n *\n * 5.0: `environment.fog`, the scene's fog traced as a volume. A worker that\n * ignored the field would hand back the scene in clear air.\n */\nexport const PROTOCOL_VERSION = '5.0';\n\n/** What a manifest with a room preset and no fog is stamped with. */\nexport const PROTOCOL_VERSION_ROOM = '4.0';\n\n/** What an animated manifest without a room preset is stamped with. */\nexport const PROTOCOL_VERSION_ANIMATED = '3.0';\n\n/**\n * What a manifest carrying no animation is stamped with. A still is still a\n * 2.0 document (every field in it means what it meant before), so stamping\n * it higher would only strand it at a worker not yet updated. See\n * `protocolVersionFor`.\n */\nexport const PROTOCOL_VERSION_STILL = '2.0';\n\n/** Every major the current worker can render faithfully. */\nexport const SUPPORTED_PROTOCOL_MAJORS = ['1', '2', '3', '4', '5'];\n\n/**\n * The version to stamp on a manifest: the lowest major that reads every\n * field in it faithfully (5.0 with fog, 4.0 with a room preset, 3.0 if\n * animated, 2.0 otherwise), so a manifest never strands itself at an older\n * worker for a field it does not carry.\n */\nexport function protocolVersionFor(manifest: {\n animation?: AnimationSpec;\n room?: RoomPresetSpec;\n environment?: { fog?: FogSpec };\n}): string {\n if (manifest.environment?.fog) return PROTOCOL_VERSION;\n if (manifest.room) return PROTOCOL_VERSION_ROOM;\n return manifest.animation\n ? PROTOCOL_VERSION_ANIMATED\n : PROTOCOL_VERSION_STILL;\n}\n\n/** `sha256:<64 hex>`: how every immutable blob is named. See asset.ts. */\nexport type AssetHash = string;\n\nexport type Vec3 = [number, number, number];\n/** glTF order: x, y, z, w. Any conversion to another order happens in the worker. */\nexport type Quat = [number, number, number, number];\nexport type Matrix4 = number[];\n\n// ---------------------------------------------------------------------------\n// Camera\n// ---------------------------------------------------------------------------\n\n/**\n * The active camera.\n *\n * Written out in full rather than referenced into the GLB by name: a live\n * R3F camera is frequently not in the scene graph the exporter walked (it\n * is held by `useThree`, or by drei's controls), so it travels as data.\n */\nexport type CameraSpec =\n | {\n type: 'perspective';\n /** Vertical FOV in degrees, matching three's `PerspectiveCamera.fov`. */\n fov: number;\n /**\n * The viewport's aspect ratio at capture time.\n *\n * The render matches vertical fov, so a wider output shows more at the\n * sides rather than cropping the top and bottom. Recorded so the\n * control plane can default the output to the viewport's own shape.\n */\n aspect?: number;\n near: number;\n far: number;\n position: Vec3;\n quaternion: Quat;\n /** three's `.zoom`, which scales the projection and is easy to forget. */\n zoom?: number;\n /** Present when the developer named a camera; only ever a label. */\n name?: string;\n /**\n * Depth of field, off unless present. `fStop` is a full-frame f-number\n * at this `fov`; `focalDistance` is in metres along the view axis.\n * `target` is the world point it was measured to: a worker that reads\n * it keeps that point sharp on every frame of a clip, and one that\n * does not still focuses the first frame right.\n */\n focus?: { fStop: number; focalDistance: number; target?: Vec3 };\n }\n | {\n type: 'orthographic';\n left: number;\n right: number;\n top: number;\n bottom: number;\n near: number;\n far: number;\n position: Vec3;\n quaternion: Quat;\n zoom?: number;\n name?: string;\n };\n\n// ---------------------------------------------------------------------------\n// Runtime state: the part glTF cannot express\n// ---------------------------------------------------------------------------\n\n/**\n * What the application had done to the scene by render time.\n *\n * Visibility is an explicit hidden list rather than \"export only what is\n * visible\" on purpose: the GLB is content-addressed and cached, and a\n * configurator that toggles one part between renders would otherwise miss\n * the cache every time. Ship one GLB with every variant in it, then hide\n * per render: the first render uploads the model, the next one uploads a\n * few KB.\n */\nexport type RuntimeState = {\n /** Object ids (see `objectId` in capture) the SDK found with `visible === false`. */\n hiddenObjectIds?: string[];\n /**\n * Transform overrides, for objects the application moved after export or\n * that were exported from a shared cached GLB.\n */\n transforms?: Array<{\n objectId: string;\n position?: Vec3;\n quaternion?: Quat;\n scale?: Vec3;\n }>;\n /** Morph target influences, by object. */\n morphs?: Array<{ objectId: string; influences: number[] }>;\n /** Current skeletal pose as bone-local matrices. */\n poses?: Array<{ objectId: string; boneMatrices: Matrix4[] }>;\n /**\n * `InstancedMesh` transforms. Written here rather than baked into the GLB\n * so changing a layout does not re-upload the geometry.\n *\n * `matrices` is flat: `count * 16` numbers, column-major, exactly as three\n * stores `instanceMatrix.array`. Nested arrays would add two bytes of JSON\n * per instance, which for a large scatter is a measurable part of the\n * manifest.\n */\n instances?: Array<{ objectId: string; count: number; matrices: number[] }>;\n /**\n * Opaque application state, echoed back on the render record. Never read\n * by the renderer; it exists so a stored render can say which sofa.\n */\n productState?: Record<string, unknown>;\n};\n\n// ---------------------------------------------------------------------------\n// Environment and lighting\n// ---------------------------------------------------------------------------\n\nexport type EnvironmentSpec = {\n /** An HDRI/EXR the SDK found on `scene.environment`, content-addressed. */\n hdri?: AssetHash;\n /** three's `Scene.environmentIntensity`. */\n intensity?: number;\n /** three's `Scene.environmentRotation`, radians, XYZ euler. */\n rotation?: Vec3;\n /**\n * The scene's `AmbientLight`s as one flat term. It ADDS to the HDRI, as it\n * does in three, and like three's it lights diffuse surfaces only: a\n * mirror reflects the environment and nothing of this.\n */\n ambient?: { color: string; intensity: number };\n /**\n * Background. `transparent` is the default: a still that composites is\n * worth more to a catalog pipeline than one with a baked backdrop.\n */\n background?:\n | { type: 'transparent' }\n | { type: 'color'; color: string }\n | { type: 'environment'; blurriness?: number };\n /** The scene's fog, when it has one the renderer can trace. */\n fog?: FogSpec;\n};\n\n/**\n * three's `FogExp2`, which the renderer traces as a volume filling the scene:\n * air that scatters the light passing through it, so a lamp or an emissive\n * mesh sits in a glow and throws visible beams.\n *\n * `density` is three's own number. three fades a surface by\n * `exp(-(density · d)²)`; a real volume thins light by `exp(-σ · d)`. The\n * two cannot agree at every distance, so the renderer uses σ = `density`,\n * which makes them agree where three's fog has taken all but 1/e of the\n * surface: nearer than that the render is a little foggier than the\n * viewport, further a little clearer.\n *\n * `color` is the fog's TINT (`#rrggbb`, as three holds it): its hue colors\n * the scattered light, its brightness is ignored. How bright traced fog\n * looks is decided by the light in it, which a rasterizer's fog color has to\n * guess. A linear `Fog` has no volume it corresponds to and is not carried;\n * the compatibility report says so.\n */\nexport type FogSpec = {\n color: string;\n density: number;\n};\n\n/**\n * Lights the SDK lifted out of the scene graph.\n *\n * Exported here as well as in the GLB because glTF's punctual-lights\n * extension has no notion of three's intensity-unit drift, no area lights\n * and no IES. The sidecar is where the good version lives and the GLB copy\n * is the fallback.\n */\nexport type LightSpec = {\n objectId: string;\n type: 'directional' | 'point' | 'spot' | 'area' | 'hemisphere';\n color: string;\n /**\n * three's own value, unconverted. The worker owns the unit conversion so\n * the physics is written down in one place.\n */\n intensity: number;\n /**\n * World position. A hemisphere light's is its up axis instead (three\n * normalizes it; absent is `[0, 1, 0]`).\n */\n position?: Vec3;\n /**\n * World point a directional, spot or area light aims at, as three's\n * `light.target` (or `lookAt`) does. Absent aims a directional or spot\n * light at the origin and leaves an area light facing straight down.\n */\n target?: Vec3;\n /**\n * Hemisphere lights only: the color from below (`groundColor`). `color` is\n * the sky. Absent is black, a sky with no ground bounce.\n */\n groundColor?: string;\n distance?: number;\n decay?: number;\n angle?: number;\n penumbra?: number;\n /** Area lights only, in world units. */\n size?: { width: number; height: number };\n /** Angular diameter in degrees: the shadow-softness dial for a sun. */\n angleDeg?: number;\n /** An IES profile, content-addressed. */\n ies?: AssetHash;\n castShadow?: boolean;\n};\n\n// ---------------------------------------------------------------------------\n// Color management\n// ---------------------------------------------------------------------------\n\n/**\n * The renderer's own color settings, copied verbatim.\n *\n * This is the difference between a render that looks like the developer's\n * app and one that is subtly, unfixably wrong. three's defaults changed in\n * r152 and again in r155; reading the live renderer beats assuming any of\n * them.\n */\nexport type ColorSpec = {\n outputColorSpace: 'srgb' | 'srgb-linear' | 'display-p3';\n toneMapping:\n | 'none'\n | 'linear'\n | 'reinhard'\n | 'cineon'\n | 'aces-filmic'\n | 'agx'\n | 'neutral';\n toneMappingExposure: number;\n};\n\n// ---------------------------------------------------------------------------\n// Material fallbacks and compatibility\n// ---------------------------------------------------------------------------\n\n/**\n * What to do with a material the renderer cannot be given honestly.\n *\n * Arbitrary GLSL does not translate, and the answer is to say so rather\n * than quietly render gray. A fallback is the developer's explicit\n * override, from `<Bakery3Material render={...}>` or from\n * `material.userData.bakery3.fallback`.\n */\nexport type MaterialFallback = {\n /** The material's uuid in the exported GLB. */\n materialId: string;\n type: 'physical';\n color?: string;\n metalness?: number;\n roughness?: number;\n clearcoat?: number;\n clearcoatRoughness?: number;\n transmission?: number;\n thickness?: number;\n ior?: number;\n emissive?: string;\n emissiveIntensity?: number;\n opacity?: number;\n sheen?: number;\n sheenColor?: string;\n iridescence?: number;\n anisotropy?: number;\n};\n\nexport type CompatibilitySeverity = 'info' | 'warning' | 'error';\n\n/**\n * One thing the SDK could not promise to reproduce: what it is, where it\n * lives in the tree, why, and the fix, without opening a debugger.\n */\nexport type CompatibilityIssue = {\n code: string;\n severity: CompatibilitySeverity;\n message: string;\n /** `<Car>/<Body>/<Paint>`: the path a developer can actually find. */\n path?: string;\n objectId?: string;\n materialId?: string;\n materialName?: string;\n materialType?: string;\n /** Deep link into the docs. */\n docs?: string;\n};\n\nexport type CompatibilityReport = {\n /** 0 to 1. Objects that will render faithfully, weighted by triangle count. */\n score: number;\n issues: CompatibilityIssue[];\n /** Rolled up for the devtools panel and the render record. */\n counts: { info: number; warning: number; error: number };\n};\n\n// ---------------------------------------------------------------------------\n// Output and quality\n// ---------------------------------------------------------------------------\n\n/** Presets, not raw renderer settings. Most scenes need only these. */\nexport type QualityPreset = 'preview' | 'studio' | 'ultra';\n\nexport type OutputFormat =\n 'png' | 'jpeg' | 'webp' | 'exr' | 'mp4' | 'webm' | 'png-sequence';\n\n/**\n * H.264 in mp4, VP9 in webm: an encoded video, opaque, written by the\n * renderer's own muxer. Both require `manifest.animation`: there is no\n * sensible video of a still, and guessing at one is not the worker's job.\n */\nexport const VIDEO_FORMATS: OutputFormat[] = ['mp4', 'webm'];\n\n/**\n * Every frame of a clip as its own PNG, alpha kept, delivered as one\n * uncompressed tar (`render.tar`) with a `frames.json` index first. Each\n * frame is exactly what a `png` still of that pose would be: the same color\n * pipeline, the background and the shadow catcher in alpha as a still has\n * them, straight (unassociated) alpha. For a compositor that puts the\n * product between layers of its own, which an mp4 cannot feed.\n */\nexport const SEQUENCE_FORMATS: OutputFormat[] = ['png-sequence'];\n\n/** An encoded video container: opaque, one file, no per-frame alpha. */\nexport function isVideoFormat(format: OutputFormat): boolean {\n return VIDEO_FORMATS.includes(format);\n}\n\n/** One file per frame, packed in a tar. */\nexport function isSequenceFormat(format: OutputFormat): boolean {\n return SEQUENCE_FORMATS.includes(format);\n}\n\n/**\n * A format that renders every frame of `manifest.animation` rather than one\n * still: a video or a sequence. It needs an animation, it is priced at the\n * clip rate, and the format is part of what the trace produces (so it is in\n * the cache key, where a still's format is not).\n */\nexport function isClipFormat(format: string | undefined): boolean {\n return (\n format !== undefined &&\n (isVideoFormat(format as OutputFormat) ||\n isSequenceFormat(format as OutputFormat))\n );\n}\n\n/**\n * The result's file name, which is also its name in storage: the worker\n * writes it and the control plane signs a PUT for it before the render runs,\n * so both sides read it from here.\n */\nexport function outputFileName(format: OutputFormat): string {\n if (isSequenceFormat(format)) return 'render.tar';\n return `render.${format === 'jpeg' ? 'jpg' : format}`;\n}\n\n/**\n * Bits per channel of a PNG output, a still's or a sequence's frames, unless\n * `bitDepth` says otherwise. 16, so a sequence's frame is the very PNG a\n * still of that pose is, and a compositor grading it has the headroom a\n * still gives. 8 is for a pipeline that ends in 8 bits anyway: measured\n * 2026-10-03 at 1400 × 1504, it is 0.98 MB a frame against 2.24 MB, and the\n * same time to trace and write.\n */\nexport const PNG_BIT_DEPTH_DEFAULT = 16;\n\n/** The first file in a `png-sequence` tar: what the rest of it is. */\nexport const FRAME_SEQUENCE_INDEX = 'frames.json';\n\n/**\n * `frame_00000.png`, zero-based like `animation.frameCount`'s frames, so file\n * `k` is the pose a channel's `k`-th sample describes.\n */\nexport function frameFileName(frame: number): string {\n return `frame_${String(frame).padStart(5, '0')}.png`;\n}\n\n/** `frames.json`, at the head of a `png-sequence` tar. */\nexport type FrameSequenceIndex = {\n fps: number;\n frameCount: number;\n width: number;\n height: number;\n bitDepth: 8 | 16;\n /** In frame order, one per frame: `frameFileName(0)` onwards. */\n files: string[];\n};\n\nexport type OutputSpec = {\n width: number;\n height: number;\n format: OutputFormat;\n /** JPEG/WebP only, 1 to 100. */\n quality?: number;\n /**\n * PNG and `png-sequence` only: bits per channel, 16 unless 8 is asked\n * for (`PNG_BIT_DEPTH_DEFAULT`).\n */\n bitDepth?: 8 | 16;\n /** Force an opaque background even when the environment is transparent. */\n opaque?: boolean;\n};\n\n/**\n * Escape hatch under the presets. Present so an expert is never blocked,\n * absent from the quickstart so a beginner is never asked to become a\n * rendering expert to get one image.\n */\nexport type AdvancedRenderSettings = {\n samples?: number;\n maxBounces?: number;\n diffuseBounces?: number;\n glossyBounces?: number;\n transmissionBounces?: number;\n /** Adaptive sampling noise floor: the real cost dial once denoising is on. */\n adaptiveThreshold?: number;\n denoise?: boolean;\n denoiser?: 'OPTIX' | 'OPENIMAGEDENOISE';\n /** Pays for itself with many emitters, costs overhead with few. */\n lightTree?: boolean;\n /**\n * Light that reaches a surface through glass: the bright pool under a\n * tumbler, the focus line beneath a lens. Off by default: with no caustic\n * paths a transmissive object casts a solid shadow, which is also what the\n * viewport's shadow maps draw, and tracing them costs samples a product\n * shot rarely needs.\n *\n * `'soft'` lets the path tracer find caustic paths itself, with the glossy\n * filter and the indirect clamp (both of which exist to hide caustics)\n * switched off. Unbiased, and slow to converge: a hard light through\n * curved glass stays grainy at studio samples.\n *\n * `'sharp'` adds a dedicated shadow-caustic solver on top: every scene\n * lamp casts caustics, every transmissive mesh is a caster, and every\n * other mesh (the shadow catcher included) receives them. This is what\n * makes a glass on a table converge at normal sample counts. It handles a\n * single refractive object between a lamp and a surface; light from an\n * HDRI or an emissive mesh still goes the `'soft'` way.\n *\n * The solver takes over lamp light through a caster rather than adding to\n * the traced paths, and it only follows light that goes straight through.\n * A faceted stone, whose light leaves after internal reflections, comes\n * back under `'sharp'` with no caustic at all. Cut gems want `'soft'`.\n *\n * Absent is off. There is no boolean on purpose: `caustics: true` would\n * have to mean one of the two. Changes pixels, so it is in the cache key.\n */\n caustics?: 'soft' | 'sharp';\n /**\n * A ceiling on how bright one indirect sample may be, in scene radiance.\n * Absent is the renderer's own default, which `caustics` switches off: a\n * caustic's focus is a very bright indirect sample, and capping it dims\n * the focus. That is right for a still and wrong for a clip, because the\n * rare uncapped hit lands on different pixels every frame, the denoiser\n * smears each one into a soft patch, and the patches boil. For caustics\n * in motion, start at 30 (the pool under a lens is still its full\n * brightness there) and trace larger than you publish.\n *\n * Set, it wins over what `caustics` would have chosen. Changes pixels, so\n * it is in the cache key, present only when set.\n */\n clampIndirect?: number;\n /**\n * Multiplies the converted energy of every light lifted from the scene\n * graph (directional, point, spot, area and the ambient dome) and leaves\n * the HDRI environment alone. The worker's unit table is physics and stays\n * fixed; this dial is for the residual gap, where a viewport (legacy-lights\n * intensities, usually) reads brighter than the physically converted lamps\n * do. One scalar, so the ratio between the scene's lights is preserved.\n * Default 1. Changes pixels, so it is in the cache key.\n */\n lightScale?: number;\n /**\n * Ground the product with a traced shadow catcher. Defaults on for\n * transparent/solid output and off for a visible environment background.\n */\n shadowCatcher?: boolean;\n /** Catcher size, as a multiple of the model's largest dimension. */\n shadowPad?: number;\n /**\n * The renderer's view transform override. Changes pixels, so it is in the\n * cache key. Pinning one also switches off `toneCurveEmulation`: an\n * explicit name is a statement about what the render should look like.\n */\n viewTransform?: string;\n /**\n * Reproduce three's tone mapping exactly instead of substituting the\n * nearest native view transform. On by default, and it is what makes a\n * traced frame the same color as the viewport it came from.\n *\n * Turn it off to get the substitution: a wider, more forgiving curve for a\n * scene whose viewport was never the reference. Changes pixels, so it is\n * in the cache key.\n */\n toneCurveEmulation?: boolean;\n /** Seconds. The worker stops the render rather than billing an unbounded job. */\n timeoutSeconds?: number;\n};\n\n// ---------------------------------------------------------------------------\n// The manifest\n// ---------------------------------------------------------------------------\n\n/**\n * Everything one render needs, minus the bytes.\n *\n * Typically a few KB. The heavy things (the GLB, textures, the HDRI) are\n * `sha256:` references that the control plane resolves against storage the\n * SDK already filled (see asset.ts).\n */\nexport type SceneManifest = {\n protocolVersion: string;\n sdkVersion: string;\n /** The exported GLB. */\n sceneAsset: AssetHash;\n /**\n * Extra blobs this scene needs that are not inside the GLB: an HDRI, an\n * IES profile, a texture the SDK stripped out to keep the GLB cacheable.\n */\n assets?: AssetHash[];\n camera: CameraSpec;\n runtime?: RuntimeState;\n environment?: EnvironmentSpec;\n lights?: LightSpec[];\n color: ColorSpec;\n fallbacks?: MaterialFallback[];\n /**\n * Node materials, as portable shader graphs (see shader.ts). The GLB still\n * carries the mesh; the material the exporter wrote for it is a stand-in\n * that the worker replaces wholesale with the rebuilt graph.\n */\n shaders?: MaterialShaderSpec[];\n /**\n * Baked per-frame motion (see animation.ts). Present only for a video,\n * and its presence is what makes this a 3.0 manifest.\n *\n * Not inside `runtime` on purpose: runtime is the one state the\n * application was in when Render was pressed, and every field in it is a\n * single value. A sequence is a different kind of thing.\n */\n animation?: AnimationSpec;\n /**\n * A room preset around the product (see room.ts). Its geometry, lights\n * and sky are not in the GLB or in `lights`: the worker rebuilds all of it\n * from the published definition, so a product in a room uploads only the\n * product. Presence makes this a 4.0 manifest.\n */\n room?: RoomPresetSpec;\n compatibility?: CompatibilityReport;\n /**\n * What the scene weighs, measured by the SDK at capture. The control plane\n * prices with it (a dense scene traces slower per pixel and unwraps slower\n * per object), so the quote a job is accepted under is the same one\n * `estimate()` gave with the same numbers. Operational, not visual: it is\n * outside every cache key.\n */\n stats?: {\n triangles: number;\n /** Bytes of texture the manifest's shader graphs sample, when known. */\n textureBytes?: number;\n /**\n * Distinct materials that emit light: a standard material with a\n * non-black emissive color, an unlit material (rendered as emission), or\n * a node material with an emission graph. The worker turns its light\n * tree on from this. Only the SDK can count them, since the GLB's\n * materials are opaque to the control plane.\n */\n emitters?: number;\n /**\n * How much of the sky above the camera the scene's geometry hides, 0 for\n * an open set and 1 inside a closed room (`SceneComplexity`). Walls keep\n * light bouncing, which is most of what makes an interior slow to render,\n * so the quote reads it. A preset room's stand-in counts, though it is not\n * uploaded: it stands for the room the renderer builds.\n */\n enclosure?: number;\n /** Meshes the scene draws, instances counted: how full it is. */\n objects?: number;\n };\n /**\n * Where the scene came from. Diagnostics only, but \"which three version\"\n * is the first question every support conversation starts with.\n */\n source?: {\n three?: string;\n r3f?: string;\n renderer?: string;\n userAgent?: string;\n };\n};\n\n/** A manifest plus what to make from it. This is the render request body. */\nexport type RenderSpec = {\n manifest: SceneManifest;\n quality: QualityPreset;\n output: OutputSpec;\n advanced?: AdvancedRenderSettings;\n /**\n * Hard ceiling in USD. Exceeded at estimate time means the job is refused\n * before it is admitted: `RenderBudgetExceeded`, not a surprise bill.\n */\n maxCost?: number;\n /** Emit a low-sample preview partway through. On by default for `studio`. */\n preview?: boolean;\n /** Echoed on the render record and every webhook. */\n metadata?: Record<string, unknown>;\n /**\n * A resubmit with the same key returns the existing render while it is\n * running or once it has completed, never a second charge. This is what\n * makes the SDK's own retries safe. A key whose render failed, was\n * cancelled or expired is free again: nothing was charged, and the\n * resubmit renders.\n */\n idempotencyKey?: string;\n /**\n * `high` puts this render ahead of the account's other waiting work in\n * the same lane: the hero shot before the rest of the queue. It never\n * jumps a lane; a person waiting still goes first.\n */\n priority?: 'normal' | 'high';\n};\n","/**\n * Quality presets: three words instead of a renderer manual.\n *\n * The three names are the public surface. The numbers below are read by the\n * worker, the estimator and the cache key from this one table, so they\n * cannot drift apart. `version` is here because changing any number changes\n * pixels: it travels in the render record and in the render cache key, so a\n * catalog that re-renders a SKU never silently gets a different image.\n */\n\nimport type { QualityPreset } from './manifest.js';\n\nexport type QualitySettings = {\n /** Bumped whenever any number in this preset changes. */\n version: number;\n /** A ceiling, not a target: adaptive sampling decides when a pixel stops. */\n samples: number;\n /**\n * The real cost dial. With adaptive sampling on, this noise floor is what\n * ends a pixel; the renderer's own default is strict enough that most\n * pixels run to the ceiling anyway.\n */\n adaptiveThreshold: number;\n maxBounces: number;\n diffuseBounces: number;\n glossyBounces: number;\n transmissionBounces: number;\n denoise: boolean;\n /** Long edge, in px, when the caller gives no explicit output size. */\n defaultLongEdge: number;\n /** Emit a low-sample preview partway through. */\n preview: boolean;\n /** Samples for that preview. Small enough to arrive in about a second. */\n previewSamples: number;\n};\n\nexport const QUALITY: Record<QualityPreset, QualitySettings> = {\n /**\n * \"Show me roughly what this will look like.\" Cheap enough to sit inside a\n * click, noisy enough that nobody will ship it.\n *\n * No denoiser, since version 3 (2026-10-03): with it, 32 samples of a\n * product came out close to a studio render, so the cheapest tier was a\n * finished image at half the price. The grain says draft, every choice a\n * preview is for (framing, light, materials, color) still reads through\n * it, and most of a preview's time was the denoiser.\n */\n preview: {\n version: 3,\n samples: 32,\n adaptiveThreshold: 0.08,\n maxBounces: 4,\n diffuseBounces: 2,\n glossyBounces: 4,\n transmissionBounces: 4,\n denoise: false,\n defaultLongEdge: 1024,\n preview: false,\n previewSamples: 16,\n },\n\n /**\n * The default. Deep enough on glossy and transmission to carry glaze,\n * metal and glass, which is where a product shot lives, and shallow on\n * diffuse, which buys almost nothing on a product against a transparent\n * background. The threshold ends most pixels well before the ceiling; the\n * denoiser carries the noisiest ones.\n */\n studio: {\n version: 2,\n samples: 256,\n adaptiveThreshold: 0.02,\n maxBounces: 8,\n diffuseBounces: 3,\n glossyBounces: 8,\n transmissionBounces: 8,\n denoise: true,\n defaultLongEdge: 2048,\n preview: true,\n previewSamples: 24,\n },\n\n /**\n * For the hero image that goes on the homepage. Roughly 8x studio's sample\n * ceiling and a much stricter threshold.\n */\n ultra: {\n version: 1,\n samples: 2048,\n adaptiveThreshold: 0.005,\n maxBounces: 16,\n diffuseBounces: 6,\n glossyBounces: 16,\n transmissionBounces: 16,\n denoise: true,\n defaultLongEdge: 4096,\n preview: true,\n previewSamples: 32,\n },\n};\n\n/** The preset with any `advanced` overrides folded in. */\nexport function resolveQuality(\n preset: QualityPreset,\n advanced?: Partial<QualitySettings> & {\n samples?: number;\n maxBounces?: number;\n },\n): QualitySettings {\n const base = QUALITY[preset];\n if (!advanced) return base;\n return { ...base, ...stripUndefined(advanced) };\n}\n\nfunction stripUndefined<T extends object>(value: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(value).filter(([, v]) => v !== undefined),\n ) as Partial<T>;\n}\n","/** The published version. Must equal the version in package.json. */\nexport const SDK_VERSION = '0.2.0';\n"]}
@@ -0,0 +1,346 @@
1
+ import { emptyRecorded, who } from './chunk-ZEBVAIJJ.js';
2
+ import { captureCamera } from './chunk-62M6XXNX.js';
3
+ import { Bakery3Error } from './chunk-3G5QL4F4.js';
4
+ import { PerspectiveCamera, Vector3, Matrix4, Quaternion } from 'three';
5
+
6
+ function variantHiddenIds(objects, finishes, finish) {
7
+ const shown = new Set(finish.show);
8
+ const hidden = /* @__PURE__ */ new Set();
9
+ for (const other of finishes) {
10
+ for (const name of other.show) {
11
+ if (shown.has(name)) continue;
12
+ const id = objects[name];
13
+ if (id === void 0) throw unknownObject(name, other.id, objects);
14
+ hidden.add(id);
15
+ }
16
+ }
17
+ for (const name of finish.show) {
18
+ if (objects[name] === void 0)
19
+ throw unknownObject(name, finish.id, objects);
20
+ }
21
+ return [...hidden];
22
+ }
23
+ function cameraSpec(camera, output) {
24
+ const position = new Vector3(...camera.position);
25
+ const target = new Vector3(...camera.target);
26
+ const distance = position.distanceTo(target);
27
+ if (!(distance > 0)) {
28
+ throw new Bakery3Error({
29
+ code: "InvalidRequest",
30
+ message: `Camera "${camera.id}" has its position on its target.`,
31
+ why: "A camera needs a direction to look in, and position === target gives it none.",
32
+ fix: "Move `position` or `target` so they differ.",
33
+ docs: "https://bakery3.com/docs/cameras"
34
+ });
35
+ }
36
+ const up = Math.abs(position.x - target.x) < 1e-9 && Math.abs(position.z - target.z) < 1e-9 ? (
37
+ // Straight down (or up): +Y up is degenerate, so face -Z as three does.
38
+ new Vector3(0, 0, -1)
39
+ ) : new Vector3(0, 1, 0);
40
+ const rotation = new Matrix4().lookAt(position, target, up);
41
+ const q = new Quaternion().setFromRotationMatrix(rotation);
42
+ const quaternion = [q.x, q.y, q.z, q.w];
43
+ return {
44
+ type: "perspective",
45
+ fov: camera.fov,
46
+ ...output ? { aspect: output.width / output.height } : {},
47
+ // Clip planes scaled to the shot: a watch a hand-span away and a sofa
48
+ // across a room both keep their subject well inside them.
49
+ near: Math.max(0.01, distance / 100),
50
+ far: Math.max(100, distance * 100),
51
+ position: [position.x, position.y, position.z],
52
+ quaternion,
53
+ name: camera.id
54
+ };
55
+ }
56
+ function variantManifest(manifest, camera, hiddenObjectIds) {
57
+ const runtime = { ...manifest.runtime };
58
+ if (hiddenObjectIds.length) runtime.hiddenObjectIds = hiddenObjectIds;
59
+ else delete runtime.hiddenObjectIds;
60
+ return {
61
+ ...manifest,
62
+ camera,
63
+ ...Object.keys(runtime).length ? { runtime } : {}
64
+ };
65
+ }
66
+ function disposeTree(root) {
67
+ const textures = /* @__PURE__ */ new Set();
68
+ const materials = /* @__PURE__ */ new Set();
69
+ root.traverse((object) => {
70
+ const mesh = object;
71
+ mesh.geometry?.dispose();
72
+ const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
73
+ for (const material of list) {
74
+ if (!material || typeof material !== "object") continue;
75
+ materials.add(material);
76
+ for (const value of Object.values(material)) {
77
+ if (value && typeof value === "object" && value.isTexture) {
78
+ textures.add(value);
79
+ }
80
+ }
81
+ }
82
+ });
83
+ for (const texture of textures) texture.dispose();
84
+ for (const material of materials) material.dispose();
85
+ }
86
+ function claimedIds(objects, finishes) {
87
+ const ids = /* @__PURE__ */ new Set();
88
+ for (const finish of finishes) {
89
+ for (const name of finish.show) {
90
+ const id = objects[name];
91
+ if (id !== void 0) ids.add(id);
92
+ }
93
+ }
94
+ return ids;
95
+ }
96
+ function unknownObject(name, finish, objects) {
97
+ const known = Object.keys(objects);
98
+ const sample = known.slice(0, 8).join(", ") + (known.length > 8 ? ", \u2026" : "");
99
+ return new Bakery3Error({
100
+ code: "InvalidRequest",
101
+ message: `Finish "${finish}" shows "${name}", but no object in the product has that name.`,
102
+ why: "The finish would render without the part it says it has, and the missing part would only be noticed in the catalog.",
103
+ fix: `Match \`show\` to the object names in the model${known.length ? ` (it has: ${sample})` : ""}.`,
104
+ path: name,
105
+ docs: "https://bakery3.com/docs/api"
106
+ });
107
+ }
108
+ var REFUSALS_KEPT = 50;
109
+ var KEY_LIMIT = 200;
110
+ var DOCS = "https://bakery3.com/docs/batches";
111
+ async function record(plan) {
112
+ validateCameras(plan.cameras);
113
+ const tally = {
114
+ ...emptyRecorded(),
115
+ estimated: 0,
116
+ maximum: 0
117
+ };
118
+ const sent = /* @__PURE__ */ new Set();
119
+ const seen = /* @__PURE__ */ new Set();
120
+ let index = 0;
121
+ for await (const variant of plan.variants) {
122
+ if (plan.signal?.aborted) throw plan.signal.reason ?? abortError();
123
+ const id = plan.idOf(variant, index++);
124
+ if (seen.has(id)) throw duplicateVariant(id);
125
+ seen.add(id);
126
+ const cleanup = await plan.apply?.(variant);
127
+ try {
128
+ const views = resolveCameras(plan.cameras, plan.scene, plan.output);
129
+ const capture = await plan.host.capture({
130
+ scene: plan.scene,
131
+ camera: views.find((view) => view.object)?.object ?? new PerspectiveCamera()
132
+ });
133
+ let upload;
134
+ if (!plan.dryRun && capture.assets.some((asset) => !sent.has(asset.hash))) {
135
+ upload = await plan.host.uploadAssets(capture);
136
+ for (const asset of capture.assets) sent.add(asset.hash);
137
+ }
138
+ const items = itemsOf(plan, variant, id, capture, views);
139
+ if (plan.keys) plan.keys.push(...items.map((item) => item.key));
140
+ let outcomes;
141
+ if (plan.dryRun) {
142
+ outcomes = await plan.batch.quote(items);
143
+ } else {
144
+ for (const item of items) plan.batch.add(item);
145
+ outcomes = await plan.batch.flush();
146
+ }
147
+ tally.variants++;
148
+ tally.images += items.length;
149
+ const metadata = new Map(items.map((item) => [item.key, item.metadata]));
150
+ for (const outcome of outcomes) {
151
+ if (outcome.error) {
152
+ tally.refused++;
153
+ if (tally.refusals.length < REFUSALS_KEPT) {
154
+ const { kind: _kind, ...ids } = who(metadata.get(outcome.key));
155
+ tally.refusals.push({
156
+ key: outcome.key,
157
+ ...ids,
158
+ error: outcome.error
159
+ });
160
+ }
161
+ } else if (outcome.adopted) {
162
+ tally.adopted++;
163
+ } else {
164
+ tally.estimated += outcome.estimatedCost;
165
+ tally.maximum += outcome.maximumCost;
166
+ }
167
+ }
168
+ plan.onVariant?.({
169
+ ...plan.batch.progress(),
170
+ variant: id,
171
+ variants: tally.variants,
172
+ images: tally.images,
173
+ ...upload ? { upload } : {}
174
+ });
175
+ } finally {
176
+ await cleanup?.();
177
+ }
178
+ }
179
+ tally.estimated = Math.round(tally.estimated * 1e4) / 1e4;
180
+ tally.maximum = Math.round(tally.maximum * 1e4) / 1e4;
181
+ return tally;
182
+ }
183
+ function itemsOf(plan, variant, id, capture, views) {
184
+ const finishes = plan.finishesOf?.(variant);
185
+ const items = [];
186
+ const push = (finish, hidden) => {
187
+ for (const view of views) {
188
+ const parts = [plan.prefix, id, finish?.id, view.id].filter(
189
+ (part) => Boolean(part)
190
+ );
191
+ const key = parts.join("/") + (plan.version ? `@${plan.version}` : "");
192
+ if (key.length > KEY_LIMIT) throw keyTooLong(key);
193
+ const manifest = hidden ? variantManifest(capture.manifest, view.spec, hidden) : { ...capture.manifest, camera: view.spec };
194
+ items.push({
195
+ key,
196
+ spec: plan.spec(manifest),
197
+ metadata: plan.metadataOf(variant, {
198
+ variant: id,
199
+ ...finish ? { finish: finish.id } : {},
200
+ camera: view.id
201
+ }),
202
+ ...plan.maxCostPerImage !== void 0 ? { maxCost: plan.maxCostPerImage } : {}
203
+ });
204
+ }
205
+ };
206
+ if (!finishes?.length) {
207
+ push(void 0, void 0);
208
+ return items;
209
+ }
210
+ const finishIds = /* @__PURE__ */ new Set();
211
+ const claimed = claimedIds(capture.objects, finishes);
212
+ const sceneHidden = (capture.manifest.runtime?.hiddenObjectIds ?? []).filter(
213
+ (hiddenId) => !claimed.has(hiddenId)
214
+ );
215
+ for (const finish of finishes) {
216
+ if (finishIds.has(finish.id)) throw duplicateFinish(id, finish.id);
217
+ finishIds.add(finish.id);
218
+ push(finish, [
219
+ ...sceneHidden,
220
+ ...variantHiddenIds(capture.objects, finishes, finish)
221
+ ]);
222
+ }
223
+ return items;
224
+ }
225
+ function idOfCamera(camera, index) {
226
+ if (typeof camera === "string") return camera;
227
+ if (isCameraObject(camera)) return camera.name || `camera-${index + 1}`;
228
+ return camera.id;
229
+ }
230
+ function isCameraObject(camera) {
231
+ return typeof camera === "object" && camera.isCamera === true;
232
+ }
233
+ function validateCameras(cameras) {
234
+ if (!cameras.length) {
235
+ throw new Bakery3Error({
236
+ code: "InvalidRequest",
237
+ message: "`cameras` is empty.",
238
+ why: "Every picture is taken from a camera, and the camera is part of its key.",
239
+ fix: "Name the cameras in your scene (`cameras: ['hero', 'front']`), pass Camera objects, or declare `{ id, position, target, fov }`. For the one live camera, leave `cameras` out.",
240
+ docs: "https://bakery3.com/docs/cameras"
241
+ });
242
+ }
243
+ const ids = /* @__PURE__ */ new Set();
244
+ cameras.forEach((camera, index) => {
245
+ const id = idOfCamera(camera, index);
246
+ if (ids.has(id)) {
247
+ throw new Bakery3Error({
248
+ code: "InvalidRequest",
249
+ message: `Two cameras are called "${id}".`,
250
+ why: "The camera id ends every picture\u2019s key, so two cameras under one id would be one picture.",
251
+ fix: "Give each camera its own `name` (or `id`).",
252
+ docs: "https://bakery3.com/docs/cameras"
253
+ });
254
+ }
255
+ ids.add(id);
256
+ });
257
+ }
258
+ function resolveCameras(cameras, scene, output) {
259
+ return cameras.map((camera, index) => {
260
+ const id = idOfCamera(camera, index);
261
+ if (typeof camera === "string") {
262
+ const object = findCamera(scene, camera);
263
+ return { id, spec: captureCamera(object), object };
264
+ }
265
+ if (isCameraObject(camera))
266
+ return {
267
+ id,
268
+ spec: { ...captureCamera(camera), name: id },
269
+ object: camera
270
+ };
271
+ return { id, spec: cameraSpec(camera, output) };
272
+ });
273
+ }
274
+ function findCamera(scene, name) {
275
+ let found;
276
+ const cameras = [];
277
+ scene.traverse((object) => {
278
+ if (!object.isCamera) return;
279
+ if (object.name) cameras.push(object.name);
280
+ if (!found && object.name === name) found = object;
281
+ });
282
+ if (found) return found;
283
+ throw new Bakery3Error({
284
+ code: "InvalidRequest",
285
+ message: `No camera named "${name}" in the scene.`,
286
+ why: "A camera given by name is looked up in the scene for every variant, so a rig the variant moves is read where it stands.",
287
+ fix: cameras.length ? `The scene's cameras are: ${cameras.slice(0, 8).join(", ")}${cameras.length > 8 ? ", \u2026" : ""}. Match one, or pass the Camera object itself.` : "The scene has no named cameras. Add the camera to the scene and set its `name`, pass the Camera object, or declare `{ id, position, target, fov }`.",
288
+ path: name,
289
+ docs: "https://bakery3.com/docs/cameras"
290
+ });
291
+ }
292
+ function defaultVariantId(variant, index) {
293
+ if (typeof variant === "string" && variant) return variant;
294
+ if (variant && typeof variant === "object") {
295
+ for (const field of ["id", "sku", "key"]) {
296
+ const value = variant[field];
297
+ if (typeof value === "string" && value) return value;
298
+ if (typeof value === "number") return String(value);
299
+ }
300
+ }
301
+ throw new Bakery3Error({
302
+ code: "InvalidRequest",
303
+ message: `Variant ${index + 1} has no id.`,
304
+ why: "The id starts every picture\u2019s key. It is what lets a re-run adopt the pictures that already rendered, and what a failure is reported under. A position in a list is not stable enough for either.",
305
+ fix: "Give each variant an `id` (or `sku`), or pass `id: (variant) => \u2026`.",
306
+ docs: DOCS
307
+ });
308
+ }
309
+ function duplicateVariant(id) {
310
+ return new Bakery3Error({
311
+ code: "InvalidRequest",
312
+ message: `Two variants are called "${id}".`,
313
+ why: "The second would be adopted as the first: same key, same picture, and the variant it was meant to show never rendered.",
314
+ fix: "Make the ids unique. When two variants share a SKU, put what differs in the id: `${sku}-${color}`.",
315
+ path: id,
316
+ docs: DOCS
317
+ });
318
+ }
319
+ function duplicateFinish(variant, finish) {
320
+ return new Bakery3Error({
321
+ code: "InvalidRequest",
322
+ message: `Finish "${finish}" of "${variant}" is declared twice.`,
323
+ why: "Finish ids are part of the item key.",
324
+ fix: "Make them unique per variant.",
325
+ path: `${variant}/${finish}`,
326
+ docs: DOCS
327
+ });
328
+ }
329
+ function keyTooLong(key) {
330
+ return new Bakery3Error({
331
+ code: "InvalidRequest",
332
+ message: `The key "${key.slice(0, 60)}\u2026" is ${key.length} characters; a key holds at most ${KEY_LIMIT}.`,
333
+ why: 'A picture\u2019s key is its variant, finish and camera ids joined with "/".',
334
+ fix: "Use shorter ids: a SKU rather than a product title.",
335
+ docs: DOCS
336
+ });
337
+ }
338
+ function abortError() {
339
+ const error = new Error("The recording was aborted.");
340
+ error.name = "AbortError";
341
+ return error;
342
+ }
343
+
344
+ export { defaultVariantId, disposeTree, record };
345
+ //# sourceMappingURL=chunk-XK35ANPJ.js.map
346
+ //# sourceMappingURL=chunk-XK35ANPJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/variant.ts","../src/renderSet.ts"],"names":[],"mappings":"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","file":"chunk-XK35ANPJ.js","sourcesContent":["/**\n * One capture, many pictures: the pure pieces.\n *\n * A capture is one GLB with every finish inside it and a few KB of manifest.\n * A picture of it is that manifest with another camera and another hidden\n * list, and nothing else. `render({ cameras })`, `renderVariants()` and the\n * catalog recorder all build their items from these, and they are exported\n * from `@oeave/bakery3/catalog` so a recorder that writes `manifests.jsonl`\n * instead of submitting builds exactly the same ones.\n */\n\nimport { Matrix4, Quaternion, Vector3, type Object3D } from 'three';\nimport type {\n CameraSpec,\n OutputSpec,\n Quat,\n RuntimeState,\n SceneManifest,\n Vec3,\n} from './protocol/index.js';\nimport { Bakery3Error } from './errors.js';\n\nexport type CatalogCamera = {\n id: string;\n position: Vec3;\n target: Vec3;\n /** Vertical field of view in degrees, as `PerspectiveCamera.fov`. */\n fov: number;\n};\n\nexport type CatalogFinish = {\n id: string;\n /** Object names this finish shows. Anything another finish claims and this\n * one does not is hidden; anything no finish claims stays as it is. */\n show: string[];\n};\n\n/**\n * Which object ids this finish hides: everything any finish claims that this\n * finish does not show. What no finish claims is not touched: the frame a\n * chair's finishes share stays visible, and so does whatever the developer\n * had hidden themselves.\n */\nexport function variantHiddenIds(\n objects: Record<string, string>,\n finishes: CatalogFinish[],\n finish: CatalogFinish,\n): string[] {\n const shown = new Set(finish.show);\n const hidden = new Set<string>();\n for (const other of finishes) {\n for (const name of other.show) {\n if (shown.has(name)) continue;\n const id = objects[name];\n if (id === undefined) throw unknownObject(name, other.id, objects);\n hidden.add(id);\n }\n }\n // A shown name that names nothing is the same typo from the other side:\n // the finish would render silently without the part it says it has.\n for (const name of finish.show) {\n if (objects[name] === undefined)\n throw unknownObject(name, finish.id, objects);\n }\n return [...hidden];\n}\n\n/**\n * A `CameraSpec` for a catalog camera: perspective, at `position`, looking\n * at `target` with +Y up. `output` sets the aspect so the render frames what\n * the declaration meant, not what some viewport happened to be.\n */\nexport function cameraSpec(\n camera: CatalogCamera,\n output?: Pick<OutputSpec, 'width' | 'height'>,\n): CameraSpec {\n const position = new Vector3(...camera.position);\n const target = new Vector3(...camera.target);\n const distance = position.distanceTo(target);\n if (!(distance > 0)) {\n throw new Bakery3Error({\n code: 'InvalidRequest',\n message: `Camera \"${camera.id}\" has its position on its target.`,\n why: 'A camera needs a direction to look in, and position === target gives it none.',\n fix: 'Move `position` or `target` so they differ.',\n docs: 'https://bakery3.com/docs/cameras',\n });\n }\n const up =\n Math.abs(position.x - target.x) < 1e-9 &&\n Math.abs(position.z - target.z) < 1e-9\n ? // Straight down (or up): +Y up is degenerate, so face -Z as three does.\n new Vector3(0, 0, -1)\n : new Vector3(0, 1, 0);\n // three's own convention: a camera looks down its local -Z.\n const rotation = new Matrix4().lookAt(position, target, up);\n const q = new Quaternion().setFromRotationMatrix(rotation);\n const quaternion: Quat = [q.x, q.y, q.z, q.w];\n\n return {\n type: 'perspective',\n fov: camera.fov,\n ...(output ? { aspect: output.width / output.height } : {}),\n // Clip planes scaled to the shot: a watch a hand-span away and a sofa\n // across a room both keep their subject well inside them.\n near: Math.max(0.01, distance / 100),\n far: Math.max(100, distance * 100),\n position: [position.x, position.y, position.z],\n quaternion,\n name: camera.id,\n };\n}\n\n/** One capture, one variant: a new camera and a new hidden list, nothing else. */\nexport function variantManifest(\n manifest: SceneManifest,\n camera: CameraSpec,\n hiddenObjectIds: string[],\n): SceneManifest {\n const runtime: RuntimeState = { ...manifest.runtime };\n if (hiddenObjectIds.length) runtime.hiddenObjectIds = hiddenObjectIds;\n else delete runtime.hiddenObjectIds;\n return {\n ...manifest,\n camera,\n ...(Object.keys(runtime).length ? { runtime } : {}),\n };\n}\n\n/**\n * Release everything a product holds on the GPU: geometries, materials and\n * every texture a material references. The tree is detached from the scene\n * by the caller; this is what lets the browser actually free it.\n */\nexport function disposeTree(root: Object3D): void {\n const textures = new Set<{ dispose(): void }>();\n const materials = new Set<{ dispose(): void }>();\n root.traverse((object) => {\n const mesh = object as Object3D & {\n geometry?: { dispose(): void };\n material?: unknown;\n };\n mesh.geometry?.dispose();\n const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];\n for (const material of list) {\n if (!material || typeof material !== 'object') continue;\n materials.add(material as { dispose(): void });\n for (const value of Object.values(material as Record<string, unknown>)) {\n if (\n value &&\n typeof value === 'object' &&\n (value as { isTexture?: boolean }).isTexture\n ) {\n textures.add(value as { dispose(): void });\n }\n }\n }\n });\n for (const texture of textures) texture.dispose();\n for (const material of materials) material.dispose();\n}\n\n/** Every id any finish of the product claims. */\nexport function claimedIds(\n objects: Record<string, string>,\n finishes: CatalogFinish[],\n): Set<string> {\n const ids = new Set<string>();\n for (const finish of finishes) {\n for (const name of finish.show) {\n const id = objects[name];\n if (id !== undefined) ids.add(id);\n }\n }\n return ids;\n}\n\nfunction unknownObject(\n name: string,\n finish: string,\n objects: Record<string, string>,\n): Bakery3Error {\n const known = Object.keys(objects);\n const sample = known.slice(0, 8).join(', ') + (known.length > 8 ? ', …' : '');\n return new Bakery3Error({\n code: 'InvalidRequest',\n message: `Finish \"${finish}\" shows \"${name}\", but no object in the product has that name.`,\n why: 'The finish would render without the part it says it has, and the missing part would only be noticed in the catalog.',\n fix: `Match \\`show\\` to the object names in the model${known.length ? ` (it has: ${sample})` : ''}.`,\n path: name,\n docs: 'https://bakery3.com/docs/api',\n });\n}\n","/**\n * Many pictures from one call.\n *\n * // every camera of the scene you have\n * const shots = await bakery3.render({ cameras: ['hero', 'front', 'detail'] });\n * const images = await shots.result();\n *\n * // every variant, from every camera\n * const run = await bakery3.renderVariants({\n * key: 'fall-2026',\n * variants: products,\n * apply: (product) => configurator.show(product),\n * cameras: ['hero', 'front', 'detail'],\n * maxCost: 600,\n * });\n * await run.wait();\n * for await (const image of run.images()) save(image.variant, image.camera, image.url);\n *\n * Both record into a batch and hand back a `RenderSet`, so a set of three\n * and a catalog of six thousand are the same object with the same\n * guarantees: one ceiling for all of it, one poll for all of it, and a\n * re-run that adopts what already rendered.\n *\n * What this file is careful about is the customer's machine. A catalog is\n * recorded in a browser tab, and a tab that holds two hundred products, or\n * two hundred exported GLBs, or six thousand result records, is a tab that\n * dies at product 140 with nothing to show for it. So:\n *\n * - One variant resident. A variant is applied, captured, uploaded, turned\n * into items, flushed and let go of before the next is touched. The\n * exported GLB is the largest thing a run holds, and it holds one.\n * - The list need not exist. `variants` is any iterable or async iterable: a\n * generator, a database cursor, a paged fetch. It is pulled one at a time\n * and never collected.\n * - One capture, many pictures. Cameras and finishes multiply items, not\n * exports: each is the same few-KB manifest with another camera and\n * another hidden list (`variant.ts`).\n * - Nothing uploads twice. Assets are content addressed; a variant that\n * changed no geometry uploads nothing, and one whose hashes this run has\n * already sent does not even ask.\n * - Results are walked, not held. `images()` and `failures()` read the\n * ledger a page at a time by cursor. `result()` collects, and says so: it\n * is for a handful of cameras.\n */\n\nimport { PerspectiveCamera, type Camera, type Scene } from 'three';\nimport type {\n BatchItemOutcome,\n BatchItemRequest,\n CameraSpec,\n OutputSpec,\n RenderError,\n RenderResult,\n RenderSpec,\n SceneManifest,\n} from './protocol/index.js';\nimport type { Batch, BatchProgress, WaitOptions } from './batch.js';\nimport type { Capture } from './index.js';\nimport type { UploadProgress } from './upload.js';\nimport { captureCamera } from './capture/camera.js';\nimport { Bakery3Error } from './errors.js';\nimport { emptyRecorded, who, type Recorded } from './set.js';\nimport {\n cameraSpec,\n claimedIds,\n variantHiddenIds,\n variantManifest,\n type CatalogCamera,\n type CatalogFinish,\n} from './variant.js';\n\n// ---------------------------------------------------------------------------\n// What the developer types\n// ---------------------------------------------------------------------------\n\n/**\n * A camera to photograph from:\n *\n * - a **name**: a camera in the scene with that `name`. Looked up again for\n * every variant, after `apply`, so a camera that ships inside a product's\n * own GLB, or rides a rig the variant moves, is read where it then stands.\n * - a **Camera**: read as it stands at capture. Its `name` is its id.\n * - a **declaration**: `{ id, position, target, fov }`, for a shot no camera\n * object exists for. Its aspect is the output's.\n */\nexport type ViewCamera = string | Camera | CatalogCamera;\n\n/** What `apply` may hand back: how to undo it. Called before the next variant, and when a run stops. */\nexport type VariantCleanup = () => void | Promise<void>;\n\nexport type VariantProgress = BatchProgress & {\n /** The variant just recorded. */\n variant: string;\n /** Variants recorded so far, this one included. */\n variants: number;\n /** Pictures recorded so far: variants × finishes × cameras. */\n images: number;\n /** What this variant's capture uploaded. Usually nothing after the first. */\n upload?: UploadProgress;\n};\n\nexport { RenderSet, RenderSetRefused } from './set.js';\nexport type {\n Recorded,\n RenderSetFailure,\n RenderSetImage,\n RenderSetRefusal,\n VariantsQuote,\n} from './set.js';\n\nconst REFUSALS_KEPT = 50;\nconst KEY_LIMIT = 200;\nconst DOCS = 'https://bakery3.com/docs/batches';\n\n// ---------------------------------------------------------------------------\n// The recorder\n// ---------------------------------------------------------------------------\n\n/** The slice of the SDK a recording drives. `Bakery3` satisfies it; so does a fake. */\nexport type RecorderHost = {\n capture(options: { scene: Scene; camera: Camera }): Promise<Capture>;\n uploadAssets(\n capture: Pick<Capture, 'assets'>,\n onProgress?: (progress: UploadProgress) => void,\n ): Promise<UploadProgress>;\n};\n\nexport type Recording<V> = {\n host: RecorderHost;\n batch: Batch;\n scene: Scene;\n variants: Iterable<V> | AsyncIterable<V>;\n idOf: (variant: V, index: number) => string;\n apply?: (\n variant: V,\n ) => void | VariantCleanup | Promise<void | VariantCleanup>;\n finishesOf?: (variant: V) => CatalogFinish[] | undefined;\n cameras: ViewCamera[];\n output: OutputSpec;\n /** The manifest of one picture, wrapped in everything else its render needs. */\n spec: (manifest: SceneManifest) => RenderSpec;\n metadataOf: (\n variant: V,\n ids: { variant: string; finish?: string; camera: string },\n ) => Record<string, unknown>;\n /** Appended to every item key as `@version`. */\n version?: string;\n /** Prefixed to every item key, for a set whose variant ids are not unique in the project on their own. */\n prefix?: string;\n /** A ceiling for each picture, under the set's. */\n maxCostPerImage?: number;\n /** Price, admit nothing, upload nothing. */\n dryRun?: boolean;\n signal?: AbortSignal;\n onVariant?: (progress: VariantProgress) => void;\n /** Collect the item keys, in order. Only for a set small enough to want one. */\n keys?: string[];\n};\n\nexport type RecordingResult = Recorded & { estimated: number; maximum: number };\n\n/**\n * Walk the variants, one resident at a time.\n *\n * Every `await` in the loop is a point where the previous variant's capture\n * is already unreachable: it lives in a `const` inside the iteration, the\n * items built from it hold its manifest (a few KB) and never its blobs, and\n * the flush at the end of the iteration hands those to the server.\n */\nexport async function record<V>(plan: Recording<V>): Promise<RecordingResult> {\n validateCameras(plan.cameras);\n const tally: RecordingResult = {\n ...emptyRecorded(),\n estimated: 0,\n maximum: 0,\n };\n /** Asset hashes this run has already put in storage. Strings, not bytes. */\n const sent = new Set<string>();\n const seen = new Set<string>();\n let index = 0;\n\n for await (const variant of plan.variants) {\n if (plan.signal?.aborted) throw plan.signal.reason ?? abortError();\n\n const id = plan.idOf(variant, index++);\n if (seen.has(id)) throw duplicateVariant(id);\n seen.add(id);\n\n const cleanup = await plan.apply?.(variant);\n try {\n const views = resolveCameras(plan.cameras, plan.scene, plan.output);\n const capture = await plan.host.capture({\n scene: plan.scene,\n camera:\n views.find((view) => view.object)?.object ?? new PerspectiveCamera(),\n });\n\n let upload: UploadProgress | undefined;\n if (\n !plan.dryRun &&\n capture.assets.some((asset) => !sent.has(asset.hash))\n ) {\n upload = await plan.host.uploadAssets(capture);\n for (const asset of capture.assets) sent.add(asset.hash);\n }\n\n const items = itemsOf(plan, variant, id, capture, views);\n if (plan.keys) plan.keys.push(...items.map((item) => item.key));\n\n let outcomes: BatchItemOutcome[];\n if (plan.dryRun) {\n outcomes = await plan.batch.quote(items);\n } else {\n for (const item of items) plan.batch.add(item);\n // A refusal here is honest and touches nothing already admitted: the\n // page stays in the batch's buffer and the error says what to do.\n outcomes = await plan.batch.flush();\n }\n\n tally.variants++;\n tally.images += items.length;\n const metadata = new Map(items.map((item) => [item.key, item.metadata]));\n for (const outcome of outcomes) {\n if (outcome.error) {\n tally.refused++;\n if (tally.refusals.length < REFUSALS_KEPT) {\n // Named the way the set's images are: by variant, finish and\n // camera, read from the same metadata.\n const { kind: _kind, ...ids } = who(metadata.get(outcome.key));\n tally.refusals.push({\n key: outcome.key,\n ...ids,\n error: outcome.error,\n });\n }\n } else if (outcome.adopted) {\n tally.adopted++;\n } else {\n tally.estimated += outcome.estimatedCost;\n tally.maximum += outcome.maximumCost;\n }\n }\n\n plan.onVariant?.({\n ...plan.batch.progress(),\n variant: id,\n variants: tally.variants,\n images: tally.images,\n ...(upload ? { upload } : {}),\n });\n } finally {\n // Even when the capture or the flush threw: the caller's scene must not\n // be left holding a variant it did not ask to keep.\n await cleanup?.();\n }\n }\n\n // Sums of cents, kept as cents: 0.1 + 0.2 is not a number to show anybody.\n tally.estimated = Math.round(tally.estimated * 1e4) / 1e4;\n tally.maximum = Math.round(tally.maximum * 1e4) / 1e4;\n return tally;\n}\n\ntype ResolvedView = { id: string; spec: CameraSpec; object?: Camera };\n\nfunction itemsOf<V>(\n plan: Recording<V>,\n variant: V,\n id: string,\n capture: Capture,\n views: ResolvedView[],\n): BatchItemRequest[] {\n const finishes = plan.finishesOf?.(variant);\n const items: BatchItemRequest[] = [];\n\n const push = (\n finish: CatalogFinish | undefined,\n hidden: string[] | undefined,\n ) => {\n for (const view of views) {\n const parts = [plan.prefix, id, finish?.id, view.id].filter(\n (part): part is string => Boolean(part),\n );\n const key = parts.join('/') + (plan.version ? `@${plan.version}` : '');\n if (key.length > KEY_LIMIT) throw keyTooLong(key);\n const manifest = hidden\n ? variantManifest(capture.manifest, view.spec, hidden)\n : { ...capture.manifest, camera: view.spec };\n items.push({\n key,\n spec: plan.spec(manifest),\n metadata: plan.metadataOf(variant, {\n variant: id,\n ...(finish ? { finish: finish.id } : {}),\n camera: view.id,\n }),\n ...(plan.maxCostPerImage !== undefined\n ? { maxCost: plan.maxCostPerImage }\n : {}),\n });\n }\n };\n\n if (!finishes?.length) {\n push(undefined, undefined);\n return items;\n }\n\n const finishIds = new Set<string>();\n // What the scene itself hides stays hidden, unless a finish claims it:\n // a loader that starts on oak has not decided anything about walnut.\n const claimed = claimedIds(capture.objects, finishes);\n const sceneHidden = (capture.manifest.runtime?.hiddenObjectIds ?? []).filter(\n (hiddenId) => !claimed.has(hiddenId),\n );\n for (const finish of finishes) {\n if (finishIds.has(finish.id)) throw duplicateFinish(id, finish.id);\n finishIds.add(finish.id);\n push(finish, [\n ...sceneHidden,\n ...variantHiddenIds(capture.objects, finishes, finish),\n ]);\n }\n return items;\n}\n\n// ---------------------------------------------------------------------------\n// Cameras\n// ---------------------------------------------------------------------------\n\nfunction idOfCamera(camera: ViewCamera, index: number): string {\n if (typeof camera === 'string') return camera;\n if (isCameraObject(camera)) return camera.name || `camera-${index + 1}`;\n return camera.id;\n}\n\nfunction isCameraObject(camera: ViewCamera): camera is Camera {\n return typeof camera === 'object' && (camera as Camera).isCamera === true;\n}\n\nfunction validateCameras(cameras: ViewCamera[]): void {\n if (!cameras.length) {\n throw new Bakery3Error({\n code: 'InvalidRequest',\n message: '`cameras` is empty.',\n why: 'Every picture is taken from a camera, and the camera is part of its key.',\n fix: \"Name the cameras in your scene (`cameras: ['hero', 'front']`), pass Camera objects, or declare `{ id, position, target, fov }`. For the one live camera, leave `cameras` out.\",\n docs: 'https://bakery3.com/docs/cameras',\n });\n }\n const ids = new Set<string>();\n cameras.forEach((camera, index) => {\n const id = idOfCamera(camera, index);\n if (ids.has(id)) {\n throw new Bakery3Error({\n code: 'InvalidRequest',\n message: `Two cameras are called \"${id}\".`,\n why: 'The camera id ends every picture’s key, so two cameras under one id would be one picture.',\n fix: 'Give each camera its own `name` (or `id`).',\n docs: 'https://bakery3.com/docs/cameras',\n });\n }\n ids.add(id);\n });\n}\n\n/** Read every camera as it stands now: after `apply`, before the capture. */\nfunction resolveCameras(\n cameras: ViewCamera[],\n scene: Scene,\n output: OutputSpec,\n): ResolvedView[] {\n return cameras.map((camera, index) => {\n const id = idOfCamera(camera, index);\n if (typeof camera === 'string') {\n const object = findCamera(scene, camera);\n return { id, spec: captureCamera(object), object };\n }\n if (isCameraObject(camera))\n return {\n id,\n spec: { ...captureCamera(camera), name: id },\n object: camera,\n };\n return { id, spec: cameraSpec(camera, output) };\n });\n}\n\nfunction findCamera(scene: Scene, name: string): Camera {\n let found: Camera | undefined;\n const cameras: string[] = [];\n scene.traverse((object) => {\n if (!(object as Camera).isCamera) return;\n if (object.name) cameras.push(object.name);\n if (!found && object.name === name) found = object as Camera;\n });\n if (found) return found;\n throw new Bakery3Error({\n code: 'InvalidRequest',\n message: `No camera named \"${name}\" in the scene.`,\n why: 'A camera given by name is looked up in the scene for every variant, so a rig the variant moves is read where it stands.',\n fix: cameras.length\n ? `The scene's cameras are: ${cameras.slice(0, 8).join(', ')}${cameras.length > 8 ? ', …' : ''}. Match one, or pass the Camera object itself.`\n : 'The scene has no named cameras. Add the camera to the scene and set its `name`, pass the Camera object, or declare `{ id, position, target, fov }`.',\n path: name,\n docs: 'https://bakery3.com/docs/cameras',\n });\n}\n\n// ---------------------------------------------------------------------------\n\n/** `variant.id`, `.sku` or `.key`, or the variant itself when it is a string. */\nexport function defaultVariantId(variant: unknown, index: number): string {\n if (typeof variant === 'string' && variant) return variant;\n if (variant && typeof variant === 'object') {\n for (const field of ['id', 'sku', 'key'] as const) {\n const value = (variant as Record<string, unknown>)[field];\n if (typeof value === 'string' && value) return value;\n if (typeof value === 'number') return String(value);\n }\n }\n throw new Bakery3Error({\n code: 'InvalidRequest',\n message: `Variant ${index + 1} has no id.`,\n why: 'The id starts every picture’s key. It is what lets a re-run adopt the pictures that already rendered, and what a failure is reported under. A position in a list is not stable enough for either.',\n fix: 'Give each variant an `id` (or `sku`), or pass `id: (variant) => …`.',\n docs: DOCS,\n });\n}\n\nfunction duplicateVariant(id: string): Bakery3Error {\n return new Bakery3Error({\n code: 'InvalidRequest',\n message: `Two variants are called \"${id}\".`,\n why: 'The second would be adopted as the first: same key, same picture, and the variant it was meant to show never rendered.',\n fix: 'Make the ids unique. When two variants share a SKU, put what differs in the id: `${sku}-${color}`.',\n path: id,\n docs: DOCS,\n });\n}\n\nfunction duplicateFinish(variant: string, finish: string): Bakery3Error {\n return new Bakery3Error({\n code: 'InvalidRequest',\n message: `Finish \"${finish}\" of \"${variant}\" is declared twice.`,\n why: 'Finish ids are part of the item key.',\n fix: 'Make them unique per variant.',\n path: `${variant}/${finish}`,\n docs: DOCS,\n });\n}\n\nfunction keyTooLong(key: string): Bakery3Error {\n return new Bakery3Error({\n code: 'InvalidRequest',\n message: `The key \"${key.slice(0, 60)}…\" is ${key.length} characters; a key holds at most ${KEY_LIMIT}.`,\n why: 'A picture’s key is its variant, finish and camera ids joined with \"/\".',\n fix: 'Use shorter ids: a SKU rather than a product title.',\n docs: DOCS,\n });\n}\n\nfunction abortError(): Error {\n const error = new Error('The recording was aborted.');\n error.name = 'AbortError';\n return error;\n}\n"]}