@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.
- package/LICENSE +21 -0
- package/README.md +562 -2
- package/dist/animation-B1h0Ryvj.d.ts +97 -0
- package/dist/bake/index.d.ts +369 -0
- package/dist/bake/index.js +9 -0
- package/dist/bake/index.js.map +1 -0
- package/dist/bake-DZ-CJR6f.d.ts +1364 -0
- package/dist/catalog/index.d.ts +100 -0
- package/dist/catalog/index.js +154 -0
- package/dist/catalog/index.js.map +1 -0
- package/dist/catalog.gen-BM-aNf7n.d.ts +733 -0
- package/dist/chunk-3G5QL4F4.js +145 -0
- package/dist/chunk-3G5QL4F4.js.map +1 -0
- package/dist/chunk-4E5VV4QY.js +12 -0
- package/dist/chunk-4E5VV4QY.js.map +1 -0
- package/dist/chunk-62M6XXNX.js +142 -0
- package/dist/chunk-62M6XXNX.js.map +1 -0
- package/dist/chunk-6FYI6AJM.js +1157 -0
- package/dist/chunk-6FYI6AJM.js.map +1 -0
- package/dist/chunk-6NYR73Y7.js +832 -0
- package/dist/chunk-6NYR73Y7.js.map +1 -0
- package/dist/chunk-APWUCEEB.js +118 -0
- package/dist/chunk-APWUCEEB.js.map +1 -0
- package/dist/chunk-HNMSWQU7.js +1709 -0
- package/dist/chunk-HNMSWQU7.js.map +1 -0
- package/dist/chunk-JFYUDERE.js +1412 -0
- package/dist/chunk-JFYUDERE.js.map +1 -0
- package/dist/chunk-KNUAOILG.js +551 -0
- package/dist/chunk-KNUAOILG.js.map +1 -0
- package/dist/chunk-KZLVTSBI.js +191 -0
- package/dist/chunk-KZLVTSBI.js.map +1 -0
- package/dist/chunk-LAKXC4WR.js +2028 -0
- package/dist/chunk-LAKXC4WR.js.map +1 -0
- package/dist/chunk-NXBAZGNB.js +1107 -0
- package/dist/chunk-NXBAZGNB.js.map +1 -0
- package/dist/chunk-QRCT5UGZ.js +3286 -0
- package/dist/chunk-QRCT5UGZ.js.map +1 -0
- package/dist/chunk-S4AJTLLN.js +23 -0
- package/dist/chunk-S4AJTLLN.js.map +1 -0
- package/dist/chunk-UWBP7B54.js +92 -0
- package/dist/chunk-UWBP7B54.js.map +1 -0
- package/dist/chunk-XK35ANPJ.js +346 -0
- package/dist/chunk-XK35ANPJ.js.map +1 -0
- package/dist/chunk-Z7IYVH22.js +4781 -0
- package/dist/chunk-Z7IYVH22.js.map +1 -0
- package/dist/chunk-ZEBVAIJJ.js +156 -0
- package/dist/chunk-ZEBVAIJJ.js.map +1 -0
- package/dist/devtools/index.d.ts +536 -0
- package/dist/devtools/index.js +15 -0
- package/dist/devtools/index.js.map +1 -0
- package/dist/environments/index.d.ts +93 -0
- package/dist/environments/index.js +382 -0
- package/dist/environments/index.js.map +1 -0
- package/dist/hotspots/index.d.ts +111 -0
- package/dist/hotspots/index.js +285 -0
- package/dist/hotspots/index.js.map +1 -0
- package/dist/index.d.ts +975 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/node/index.cjs +5240 -0
- package/dist/node/index.cjs.map +1 -0
- package/dist/node/index.d.cts +4927 -0
- package/dist/node/index.d.ts +597 -0
- package/dist/node/index.js +1328 -0
- package/dist/node/index.js.map +1 -0
- package/dist/prepare-BtjY4G3q.d.ts +112 -0
- package/dist/presets/index.d.ts +562 -0
- package/dist/presets/index.js +14 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/r3f/index.d.ts +159 -0
- package/dist/r3f/index.js +592 -0
- package/dist/r3f/index.js.map +1 -0
- package/dist/room-FS26KAPQ.js +9 -0
- package/dist/room-FS26KAPQ.js.map +1 -0
- package/dist/rooms.gen-DItzBR9k.d.ts +1462 -0
- package/dist/session-VCIQEO26.js +9 -0
- package/dist/session-VCIQEO26.js.map +1 -0
- package/dist/shapes/index.d.ts +222 -0
- package/dist/shapes/index.js +836 -0
- package/dist/shapes/index.js.map +1 -0
- package/dist/testRun-20OARnQr.d.ts +1139 -0
- package/dist/timeline-ChwgD7bT.d.ts +470 -0
- package/dist/tsl/index.d.ts +165 -0
- package/dist/tsl/index.js +310 -0
- package/dist/tsl/index.js.map +1 -0
- package/package.json +170 -4
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Scene, Object3D } from 'three';
|
|
2
|
+
import { Q as QualityPreset, O as OutputSpec } from '../bake-DZ-CJR6f.js';
|
|
3
|
+
import { B as BatchProgress, V as VariantsQuote, a as BatchRecord } from '../testRun-20OARnQr.js';
|
|
4
|
+
export { b as Batch } from '../testRun-20OARnQr.js';
|
|
5
|
+
import { CatalogCamera, CatalogFinish, Bakery3 } from '../index.js';
|
|
6
|
+
import { R as RoomPresetName, a as RoomPresetLighting } from '../rooms.gen-DItzBR9k.js';
|
|
7
|
+
import '../animation-B1h0Ryvj.js';
|
|
8
|
+
import '../timeline-ChwgD7bT.js';
|
|
9
|
+
import '../prepare-BtjY4G3q.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @oeave/bakery3/catalog: render a catalog of products × finishes × cameras
|
|
13
|
+
* from the browser.
|
|
14
|
+
*
|
|
15
|
+
* import { runCatalog } from '@oeave/bakery3/catalog';
|
|
16
|
+
*
|
|
17
|
+
* await runCatalog(bakery3, catalog, {
|
|
18
|
+
* scene,
|
|
19
|
+
* loadProduct: (url) => loadGltf(url),
|
|
20
|
+
* onProgress: ({ product, done, total, cost }) => setStatus(`${product}: ${done}/${total}`),
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* The work is declared as data (`Catalog`) and the recorder walks it with one
|
|
24
|
+
* product resident at a time: load, mount, capture once (the model with every
|
|
25
|
+
* finish inside it is exported and uploaded once), then one small manifest
|
|
26
|
+
* per finish × camera that differs only in `camera` and `hiddenObjectIds`,
|
|
27
|
+
* flushed as pages to the batch, then the product is removed and disposed.
|
|
28
|
+
* Peak memory is one product plus one page of manifests, whatever the
|
|
29
|
+
* catalog's size.
|
|
30
|
+
*
|
|
31
|
+
* A recorder that dies half way is resumed by running it again: the batch key
|
|
32
|
+
* re-opens the same batch and every item already `completed` is adopted at no
|
|
33
|
+
* cost.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
type CatalogProduct = {
|
|
37
|
+
/** Unique in the catalog; it is the item key and the `sku` in metadata. */
|
|
38
|
+
id: string;
|
|
39
|
+
/** The model, with every finish inside; the recorder hides the others per render. */
|
|
40
|
+
url: string;
|
|
41
|
+
/** At least one. `{ id, show: [] }` shows the product as loaded. */
|
|
42
|
+
finishes: CatalogFinish[];
|
|
43
|
+
};
|
|
44
|
+
type Catalog = {
|
|
45
|
+
/** Stable across re-runs: the batch's idempotency key. */
|
|
46
|
+
key: string;
|
|
47
|
+
/** A label for the console. */
|
|
48
|
+
name?: string;
|
|
49
|
+
quality: QualityPreset;
|
|
50
|
+
output: OutputSpec;
|
|
51
|
+
/** Ceiling for the whole run, in USD. A page that would cross it is refused, never charged. */
|
|
52
|
+
maxCost: number;
|
|
53
|
+
/** At least one; every product is shot from each. */
|
|
54
|
+
cameras: CatalogCamera[];
|
|
55
|
+
/** A preset room to photograph in, added for the run when the scene has none. */
|
|
56
|
+
room?: {
|
|
57
|
+
preset: RoomPresetName;
|
|
58
|
+
mode?: RoomPresetLighting;
|
|
59
|
+
};
|
|
60
|
+
products: CatalogProduct[];
|
|
61
|
+
};
|
|
62
|
+
type CatalogProgress = BatchProgress & {
|
|
63
|
+
/** The product just flushed. */
|
|
64
|
+
product: string;
|
|
65
|
+
};
|
|
66
|
+
type RunCatalogOptions = {
|
|
67
|
+
/** Your live scene: lights, room, environment. */
|
|
68
|
+
scene: Scene;
|
|
69
|
+
/** Your loader, with your own draco/ktx setup. */
|
|
70
|
+
loadProduct: (url: string) => Promise<Object3D> | Object3D;
|
|
71
|
+
/**
|
|
72
|
+
* Products resident at a time. `1` is the point of the recorder and the
|
|
73
|
+
* only value supported; anything else is refused rather than silently run
|
|
74
|
+
* as 1.
|
|
75
|
+
*/
|
|
76
|
+
concurrency?: 1;
|
|
77
|
+
/** Called after each product's items are flushed. */
|
|
78
|
+
onProgress?: (progress: CatalogProgress) => void;
|
|
79
|
+
/**
|
|
80
|
+
* Stops between products. What was flushed stays admitted; the batch is
|
|
81
|
+
* left open so the same run can be resumed.
|
|
82
|
+
*/
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Record and submit the whole catalog, then seal the batch. Resolves with the
|
|
87
|
+
* sealed batch record; the pictures render on from there (`batch.wait()` or
|
|
88
|
+
* a webhook says when). Run it again with the same key to resume.
|
|
89
|
+
*/
|
|
90
|
+
declare function runCatalog(bakery3: Bakery3, catalog: Catalog, options: RunCatalogOptions): Promise<BatchRecord>;
|
|
91
|
+
/**
|
|
92
|
+
* The same walk, priced instead of submitted: every product is loaded and
|
|
93
|
+
* captured, every item is quoted, and nothing is uploaded, admitted or
|
|
94
|
+
* reserved. The `maximum` it returns is the number to set `maxCost` from;
|
|
95
|
+
* pictures an earlier run already rendered are counted as adopted, at
|
|
96
|
+
* nothing, so after a change this is the price of the re-run.
|
|
97
|
+
*/
|
|
98
|
+
declare function quoteCatalog(bakery3: Bakery3, catalog: Catalog, options: RunCatalogOptions): Promise<VariantsQuote>;
|
|
99
|
+
|
|
100
|
+
export { BatchProgress, type Catalog, CatalogCamera, CatalogFinish, type CatalogProduct, type CatalogProgress, type RunCatalogOptions, VariantsQuote, quoteCatalog, runCatalog };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { record, disposeTree } from '../chunk-XK35ANPJ.js';
|
|
2
|
+
import '../chunk-ZEBVAIJJ.js';
|
|
3
|
+
import '../chunk-62M6XXNX.js';
|
|
4
|
+
import { Bakery3Error } from '../chunk-3G5QL4F4.js';
|
|
5
|
+
|
|
6
|
+
// src/catalog/index.ts
|
|
7
|
+
async function runCatalog(bakery3, catalog, options) {
|
|
8
|
+
validate(catalog, options);
|
|
9
|
+
const batch = await bakery3.batches.open({
|
|
10
|
+
key: catalog.key,
|
|
11
|
+
...catalog.name ? { name: catalog.name } : {},
|
|
12
|
+
maxCost: catalog.maxCost,
|
|
13
|
+
lane: "batch"
|
|
14
|
+
});
|
|
15
|
+
await walk(bakery3, batch, catalog, options, false);
|
|
16
|
+
return batch.close();
|
|
17
|
+
}
|
|
18
|
+
async function quoteCatalog(bakery3, catalog, options) {
|
|
19
|
+
validate(catalog, options);
|
|
20
|
+
const batch = await bakery3.batches.open({
|
|
21
|
+
key: catalog.key,
|
|
22
|
+
...catalog.name ? { name: catalog.name } : {},
|
|
23
|
+
maxCost: catalog.maxCost,
|
|
24
|
+
lane: "batch"
|
|
25
|
+
});
|
|
26
|
+
return walk(bakery3, batch, catalog, options, true);
|
|
27
|
+
}
|
|
28
|
+
async function walk(bakery3, batch, catalog, options, dryRun) {
|
|
29
|
+
const room = await mountRoom(options.scene, catalog.room);
|
|
30
|
+
try {
|
|
31
|
+
return await record({
|
|
32
|
+
host: bakery3,
|
|
33
|
+
batch,
|
|
34
|
+
scene: options.scene,
|
|
35
|
+
variants: catalog.products,
|
|
36
|
+
idOf: (product) => product.id,
|
|
37
|
+
apply: async (product) => {
|
|
38
|
+
const root = await options.loadProduct(product.url);
|
|
39
|
+
options.scene.add(root);
|
|
40
|
+
return () => {
|
|
41
|
+
options.scene.remove(root);
|
|
42
|
+
disposeTree(root);
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
finishesOf: (product) => product.finishes,
|
|
46
|
+
cameras: catalog.cameras,
|
|
47
|
+
output: catalog.output,
|
|
48
|
+
spec: (manifest) => ({
|
|
49
|
+
manifest,
|
|
50
|
+
quality: catalog.quality,
|
|
51
|
+
output: catalog.output
|
|
52
|
+
}),
|
|
53
|
+
metadataOf: (_product, ids) => ({
|
|
54
|
+
sku: ids.variant,
|
|
55
|
+
finish: ids.finish,
|
|
56
|
+
camera: ids.camera
|
|
57
|
+
}),
|
|
58
|
+
dryRun,
|
|
59
|
+
...options.signal ? { signal: options.signal } : {},
|
|
60
|
+
onVariant: ({
|
|
61
|
+
variant,
|
|
62
|
+
variants: _variants,
|
|
63
|
+
images: _images,
|
|
64
|
+
upload: _upload,
|
|
65
|
+
...progress
|
|
66
|
+
}) => options.onProgress?.({ ...progress, product: variant })
|
|
67
|
+
});
|
|
68
|
+
} finally {
|
|
69
|
+
room?.();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function validate(catalog, options) {
|
|
73
|
+
validateCatalog(catalog);
|
|
74
|
+
if (options.concurrency !== void 0 && options.concurrency !== 1) {
|
|
75
|
+
throw new Bakery3Error({
|
|
76
|
+
code: "InvalidRequest",
|
|
77
|
+
message: `concurrency: ${options.concurrency} is not supported; the recorder holds one product at a time.`,
|
|
78
|
+
why: "Bounded memory is the reason runCatalog() exists. One product resident is the bound.",
|
|
79
|
+
fix: "Remove `concurrency`, or set it to 1.",
|
|
80
|
+
docs: "https://bakery3.com/docs/api"
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function mountRoom(scene, room) {
|
|
85
|
+
if (!room) return void 0;
|
|
86
|
+
const { createPresetRoom, findPresetRooms } = await import('../room-FS26KAPQ.js');
|
|
87
|
+
if (findPresetRooms(scene).length) return void 0;
|
|
88
|
+
const group = createPresetRoom(
|
|
89
|
+
room.preset,
|
|
90
|
+
room.mode ? { lighting: room.mode } : {}
|
|
91
|
+
);
|
|
92
|
+
scene.add(group);
|
|
93
|
+
return () => {
|
|
94
|
+
scene.remove(group);
|
|
95
|
+
disposeTree(group);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function validateCatalog(catalog) {
|
|
99
|
+
const refuse = (message, fix) => new Bakery3Error({
|
|
100
|
+
code: "InvalidRequest",
|
|
101
|
+
message,
|
|
102
|
+
why: "The catalog is the recorder's whole input; a hole in it would be a hole in the batch.",
|
|
103
|
+
fix,
|
|
104
|
+
docs: "https://bakery3.com/docs/api"
|
|
105
|
+
});
|
|
106
|
+
if (!catalog.key)
|
|
107
|
+
throw refuse(
|
|
108
|
+
"The catalog has no key.",
|
|
109
|
+
"Give it a stable key: it is the batch's idempotency key."
|
|
110
|
+
);
|
|
111
|
+
if (!catalog.cameras.length)
|
|
112
|
+
throw refuse("The catalog has no cameras.", "Declare at least one camera.");
|
|
113
|
+
if (!(catalog.maxCost > 0))
|
|
114
|
+
throw refuse(
|
|
115
|
+
"The catalog has no maxCost.",
|
|
116
|
+
"Set the ceiling for the whole run, in dollars."
|
|
117
|
+
);
|
|
118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
119
|
+
for (const product of catalog.products) {
|
|
120
|
+
if (seen.has(product.id))
|
|
121
|
+
throw refuse(
|
|
122
|
+
`Product "${product.id}" is declared twice.`,
|
|
123
|
+
"Product ids are item keys; make them unique."
|
|
124
|
+
);
|
|
125
|
+
seen.add(product.id);
|
|
126
|
+
if (!product.finishes.length)
|
|
127
|
+
throw refuse(
|
|
128
|
+
`Product "${product.id}" has no finishes.`,
|
|
129
|
+
"Declare one finish: `{ id, show: [] }` shows the product as loaded."
|
|
130
|
+
);
|
|
131
|
+
const finishIds = /* @__PURE__ */ new Set();
|
|
132
|
+
for (const finish of product.finishes) {
|
|
133
|
+
if (finishIds.has(finish.id))
|
|
134
|
+
throw refuse(
|
|
135
|
+
`Finish "${finish.id}" of "${product.id}" is declared twice.`,
|
|
136
|
+
"Finish ids are part of the item key; make them unique per product."
|
|
137
|
+
);
|
|
138
|
+
finishIds.add(finish.id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const cameraIds = /* @__PURE__ */ new Set();
|
|
142
|
+
for (const camera of catalog.cameras) {
|
|
143
|
+
if (cameraIds.has(camera.id))
|
|
144
|
+
throw refuse(
|
|
145
|
+
`Camera "${camera.id}" is declared twice.`,
|
|
146
|
+
"Camera ids are part of the item key; make them unique."
|
|
147
|
+
);
|
|
148
|
+
cameraIds.add(camera.id);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { quoteCatalog, runCatalog };
|
|
153
|
+
//# sourceMappingURL=index.js.map
|
|
154
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/catalog/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @oeave/bakery3/catalog: render a catalog of products × finishes × cameras\n * from the browser.\n *\n * import { runCatalog } from '@oeave/bakery3/catalog';\n *\n * await runCatalog(bakery3, catalog, {\n * scene,\n * loadProduct: (url) => loadGltf(url),\n * onProgress: ({ product, done, total, cost }) => setStatus(`${product}: ${done}/${total}`),\n * });\n *\n * The work is declared as data (`Catalog`) and the recorder walks it with one\n * product resident at a time: load, mount, capture once (the model with every\n * finish inside it is exported and uploaded once), then one small manifest\n * per finish × camera that differs only in `camera` and `hiddenObjectIds`,\n * flushed as pages to the batch, then the product is removed and disposed.\n * Peak memory is one product plus one page of manifests, whatever the\n * catalog's size.\n *\n * A recorder that dies half way is resumed by running it again: the batch key\n * re-opens the same batch and every item already `completed` is adopted at no\n * cost.\n */\n\nimport type { Object3D, Scene } from 'three';\nimport type {\n BatchRecord,\n OutputSpec,\n QualityPreset,\n} from '../protocol/index.js';\nimport type { Bakery3 } from '../index.js';\nimport type { Batch, BatchProgress } from '../batch.js';\nimport { Bakery3Error } from '../errors.js';\nimport { record, type VariantsQuote } from '../renderSet.js';\nimport {\n disposeTree,\n type CatalogCamera,\n type CatalogFinish,\n} from '../variant.js';\n// Types only: the room module (its loaders, the meshopt decoder, the sky\n// dome) is loaded when a catalog names a room, and not before.\nimport type {\n RoomPresetLighting,\n RoomPresetName,\n} from '../presets/rooms.gen.js';\n\n// ---------------------------------------------------------------------------\n// The declaration\n// ---------------------------------------------------------------------------\n\nexport type CatalogProduct = {\n /** Unique in the catalog; it is the item key and the `sku` in metadata. */\n id: string;\n /** The model, with every finish inside; the recorder hides the others per render. */\n url: string;\n /** At least one. `{ id, show: [] }` shows the product as loaded. */\n finishes: CatalogFinish[];\n};\n\nexport type Catalog = {\n /** Stable across re-runs: the batch's idempotency key. */\n key: string;\n /** A label for the console. */\n name?: string;\n quality: QualityPreset;\n output: OutputSpec;\n /** Ceiling for the whole run, in USD. A page that would cross it is refused, never charged. */\n maxCost: number;\n /** At least one; every product is shot from each. */\n cameras: CatalogCamera[];\n /** A preset room to photograph in, added for the run when the scene has none. */\n room?: { preset: RoomPresetName; mode?: RoomPresetLighting };\n products: CatalogProduct[];\n};\n\nexport type CatalogProgress = BatchProgress & {\n /** The product just flushed. */\n product: string;\n};\n\nexport type RunCatalogOptions = {\n /** Your live scene: lights, room, environment. */\n scene: Scene;\n /** Your loader, with your own draco/ktx setup. */\n loadProduct: (url: string) => Promise<Object3D> | Object3D;\n /**\n * Products resident at a time. `1` is the point of the recorder and the\n * only value supported; anything else is refused rather than silently run\n * as 1.\n */\n concurrency?: 1;\n /** Called after each product's items are flushed. */\n onProgress?: (progress: CatalogProgress) => void;\n /**\n * Stops between products. What was flushed stays admitted; the batch is\n * left open so the same run can be resumed.\n */\n signal?: AbortSignal;\n};\n\n// ---------------------------------------------------------------------------\n// The recorder\n// ---------------------------------------------------------------------------\n\n/**\n * Record and submit the whole catalog, then seal the batch. Resolves with the\n * sealed batch record; the pictures render on from there (`batch.wait()` or\n * a webhook says when). Run it again with the same key to resume.\n */\nexport async function runCatalog(\n bakery3: Bakery3,\n catalog: Catalog,\n options: RunCatalogOptions,\n): Promise<BatchRecord> {\n validate(catalog, options);\n\n // Open, or re-open, the batch. Same key, same batch; items already\n // completed in an earlier run are adopted, not re-rendered.\n const batch: Batch = await bakery3.batches.open({\n key: catalog.key,\n ...(catalog.name ? { name: catalog.name } : {}),\n maxCost: catalog.maxCost,\n lane: 'batch',\n });\n\n await walk(bakery3, batch, catalog, options, false);\n\n // Seal: the total is known and `batch.completed` can fire.\n return batch.close();\n}\n\n/**\n * The same walk, priced instead of submitted: every product is loaded and\n * captured, every item is quoted, and nothing is uploaded, admitted or\n * reserved. The `maximum` it returns is the number to set `maxCost` from;\n * pictures an earlier run already rendered are counted as adopted, at\n * nothing, so after a change this is the price of the re-run.\n */\nexport async function quoteCatalog(\n bakery3: Bakery3,\n catalog: Catalog,\n options: RunCatalogOptions,\n): Promise<VariantsQuote> {\n validate(catalog, options);\n // The route that prices a page belongs to a batch, so one is opened under\n // the catalog's key. It holds nothing, and the real run re-opens it.\n const batch = await bakery3.batches.open({\n key: catalog.key,\n ...(catalog.name ? { name: catalog.name } : {}),\n maxCost: catalog.maxCost,\n lane: 'batch',\n });\n return walk(bakery3, batch, catalog, options, true);\n}\n\n/**\n * For every product, by the recorder every multi-picture call shares: load\n * and mount one product, capture once, upload once, one item per finish ×\n * camera, flush, let go.\n */\nasync function walk(\n bakery3: Bakery3,\n batch: Batch,\n catalog: Catalog,\n options: RunCatalogOptions,\n dryRun: boolean,\n) {\n const room = await mountRoom(options.scene, catalog.room);\n try {\n return await record<CatalogProduct>({\n host: bakery3,\n batch,\n scene: options.scene,\n variants: catalog.products,\n idOf: (product) => product.id,\n apply: async (product) => {\n const root = await options.loadProduct(product.url);\n options.scene.add(root);\n return () => {\n options.scene.remove(root);\n disposeTree(root);\n };\n },\n finishesOf: (product) => product.finishes,\n cameras: catalog.cameras,\n output: catalog.output,\n spec: (manifest) => ({\n manifest,\n quality: catalog.quality,\n output: catalog.output,\n }),\n metadataOf: (_product, ids) => ({\n sku: ids.variant,\n finish: ids.finish,\n camera: ids.camera,\n }),\n dryRun,\n ...(options.signal ? { signal: options.signal } : {}),\n onVariant: ({\n variant,\n variants: _variants,\n images: _images,\n upload: _upload,\n ...progress\n }) => options.onProgress?.({ ...progress, product: variant }),\n });\n } finally {\n room?.();\n }\n}\n\nfunction validate(catalog: Catalog, options: RunCatalogOptions): void {\n validateCatalog(catalog);\n if (options.concurrency !== undefined && options.concurrency !== 1) {\n throw new Bakery3Error({\n code: 'InvalidRequest',\n message: `concurrency: ${options.concurrency} is not supported; the recorder holds one product at a time.`,\n why: 'Bounded memory is the reason runCatalog() exists. One product resident is the bound.',\n fix: 'Remove `concurrency`, or set it to 1.',\n docs: 'https://bakery3.com/docs/api',\n });\n }\n}\n\n/**\n * Put the declared room in the scene for the run, unless the scene already\n * has one: an existing room is the one to be faithful to. Returns the undo,\n * or nothing when nothing was added.\n */\nasync function mountRoom(\n scene: Scene,\n room: Catalog['room'],\n): Promise<(() => void) | undefined> {\n if (!room) return undefined;\n const { createPresetRoom, findPresetRooms } =\n await import('../presets/room.js');\n if (findPresetRooms(scene).length) return undefined;\n const group = createPresetRoom(\n room.preset,\n room.mode ? { lighting: room.mode } : {},\n );\n scene.add(group);\n return () => {\n scene.remove(group);\n disposeTree(group);\n };\n}\n\nfunction validateCatalog(catalog: Catalog): void {\n const refuse = (message: string, fix: string) =>\n new Bakery3Error({\n code: 'InvalidRequest',\n message,\n why: \"The catalog is the recorder's whole input; a hole in it would be a hole in the batch.\",\n fix,\n docs: 'https://bakery3.com/docs/api',\n });\n if (!catalog.key)\n throw refuse(\n 'The catalog has no key.',\n \"Give it a stable key: it is the batch's idempotency key.\",\n );\n if (!catalog.cameras.length)\n throw refuse('The catalog has no cameras.', 'Declare at least one camera.');\n if (!(catalog.maxCost > 0))\n throw refuse(\n 'The catalog has no maxCost.',\n 'Set the ceiling for the whole run, in dollars.',\n );\n const seen = new Set<string>();\n for (const product of catalog.products) {\n if (seen.has(product.id))\n throw refuse(\n `Product \"${product.id}\" is declared twice.`,\n 'Product ids are item keys; make them unique.',\n );\n seen.add(product.id);\n if (!product.finishes.length)\n throw refuse(\n `Product \"${product.id}\" has no finishes.`,\n 'Declare one finish: `{ id, show: [] }` shows the product as loaded.',\n );\n const finishIds = new Set<string>();\n for (const finish of product.finishes) {\n if (finishIds.has(finish.id))\n throw refuse(\n `Finish \"${finish.id}\" of \"${product.id}\" is declared twice.`,\n 'Finish ids are part of the item key; make them unique per product.',\n );\n finishIds.add(finish.id);\n }\n }\n const cameraIds = new Set<string>();\n for (const camera of catalog.cameras) {\n if (cameraIds.has(camera.id))\n throw refuse(\n `Camera \"${camera.id}\" is declared twice.`,\n 'Camera ids are part of the item key; make them unique.',\n );\n cameraIds.add(camera.id);\n }\n}\n\nexport type { CatalogCamera, CatalogFinish } from '../variant.js';\nexport type { VariantsQuote } from '../renderSet.js';\nexport type { Batch, BatchProgress } from '../batch.js';\n"]}
|