@oeave/bakery3 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-DaOlenyx.d.ts +1357 -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-25ZXSVQ3.js +1107 -0
- package/dist/chunk-25ZXSVQ3.js.map +1 -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-54WJDN5R.js +3286 -0
- package/dist/chunk-54WJDN5R.js.map +1 -0
- package/dist/chunk-6NYR73Y7.js +832 -0
- package/dist/chunk-6NYR73Y7.js.map +1 -0
- package/dist/chunk-6YVQANMW.js +1463 -0
- package/dist/chunk-6YVQANMW.js.map +1 -0
- package/dist/chunk-AF5BIELI.js +70 -0
- package/dist/chunk-AF5BIELI.js.map +1 -0
- package/dist/chunk-APBOWWSV.js +92 -0
- package/dist/chunk-APBOWWSV.js.map +1 -0
- package/dist/chunk-ARMTTZXZ.js +1157 -0
- package/dist/chunk-ARMTTZXZ.js.map +1 -0
- package/dist/chunk-CX4WTVGH.js +4492 -0
- package/dist/chunk-CX4WTVGH.js.map +1 -0
- package/dist/chunk-HQTUFVMM.js +1412 -0
- package/dist/chunk-HQTUFVMM.js.map +1 -0
- package/dist/chunk-MD2U6S4N.js +1952 -0
- package/dist/chunk-MD2U6S4N.js.map +1 -0
- package/dist/chunk-S4AJTLLN.js +23 -0
- package/dist/chunk-S4AJTLLN.js.map +1 -0
- package/dist/chunk-TGFNQBKC.js +551 -0
- package/dist/chunk-TGFNQBKC.js.map +1 -0
- package/dist/chunk-V3QINEEG.js +87 -0
- package/dist/chunk-V3QINEEG.js.map +1 -0
- package/dist/chunk-W6WQ5UP7.js +91 -0
- package/dist/chunk-W6WQ5UP7.js.map +1 -0
- package/dist/chunk-WBIAFIHV.js +346 -0
- package/dist/chunk-WBIAFIHV.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 +944 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/node/index.cjs +5115 -0
- package/dist/node/index.cjs.map +1 -0
- package/dist/node/index.d.cts +4862 -0
- package/dist/node/index.d.ts +576 -0
- package/dist/node/index.js +1279 -0
- package/dist/node/index.js.map +1 -0
- package/dist/prepare-C5qVvaw4.d.ts +112 -0
- package/dist/presets/index.d.ts +526 -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-S5TLCK4F.js +9 -0
- package/dist/room-S5TLCK4F.js.map +1 -0
- package/dist/rooms.gen-vhj6sFW5.d.ts +1434 -0
- package/dist/session-YCQS4MRH.js +9 -0
- package/dist/session-YCQS4MRH.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-BIPXaVPb.d.ts +1130 -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,1157 @@
|
|
|
1
|
+
import { record, defaultVariantId } from './chunk-WBIAFIHV.js';
|
|
2
|
+
import { Bakery3Client, Batch, quoteKey, sampleOf, testSettings, PollFailures } from './chunk-25ZXSVQ3.js';
|
|
3
|
+
import { RenderSet } from './chunk-ZEBVAIJJ.js';
|
|
4
|
+
import { captureScene, bindTimelineContext, createTimeline, prepareBake, bakeQuoteOf, quoteBake, analyzeScene, resolveOutput, sceneComplexity, tracedTriangles } from './chunk-CX4WTVGH.js';
|
|
5
|
+
import { SDK_VERSION, protocolVersionFor } from './chunk-APBOWWSV.js';
|
|
6
|
+
import { findPresetRooms } from './chunk-AF5BIELI.js';
|
|
7
|
+
import { shouldExclude, idOf, captureFog } from './chunk-HQTUFVMM.js';
|
|
8
|
+
import { Bakery3Error, RenderBudgetExceeded } from './chunk-3G5QL4F4.js';
|
|
9
|
+
|
|
10
|
+
// src/protocol/job.ts
|
|
11
|
+
var TERMINAL_STATES = [
|
|
12
|
+
"completed",
|
|
13
|
+
"failed",
|
|
14
|
+
"cancelled",
|
|
15
|
+
"expired"
|
|
16
|
+
];
|
|
17
|
+
function isTerminal(state) {
|
|
18
|
+
return TERMINAL_STATES.includes(state);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/job.ts
|
|
22
|
+
var POLL_SCHEDULE = [400, 400, 600, 800, 1e3, 1500, 2e3, 3e3];
|
|
23
|
+
var PolledJob = class {
|
|
24
|
+
id;
|
|
25
|
+
record;
|
|
26
|
+
client;
|
|
27
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
28
|
+
/** The last payload of each replayed event, for a listener added late. */
|
|
29
|
+
lastPayloads = /* @__PURE__ */ new Map();
|
|
30
|
+
pending = null;
|
|
31
|
+
/** `result()` calls still waiting. */
|
|
32
|
+
waiting = 0;
|
|
33
|
+
/** The running poll loop's switch, or null when no loop runs. */
|
|
34
|
+
poller = null;
|
|
35
|
+
/** Set once the job has settled, for good: a terminal state or a verdict. */
|
|
36
|
+
hasEnded = false;
|
|
37
|
+
constructor(client, record2) {
|
|
38
|
+
this.client = client;
|
|
39
|
+
this.record = record2;
|
|
40
|
+
this.id = record2.id;
|
|
41
|
+
}
|
|
42
|
+
get status() {
|
|
43
|
+
return this.record.state;
|
|
44
|
+
}
|
|
45
|
+
get progress() {
|
|
46
|
+
return this.record.progress;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Listen for an event. Starts polling if nothing has yet.
|
|
50
|
+
*
|
|
51
|
+
* A listener added after an event already fired is called with the latest
|
|
52
|
+
* one, on the next microtask: the latest `preview` and `progress`, and
|
|
53
|
+
* `complete`, `failed` or `cancelled` once the job has ended. Returns the
|
|
54
|
+
* unsubscribe function, so a React effect can clean up without a second
|
|
55
|
+
* API.
|
|
56
|
+
*/
|
|
57
|
+
on(event, listener) {
|
|
58
|
+
const set = this.subscribers.get(event) ?? /* @__PURE__ */ new Set();
|
|
59
|
+
this.subscribers.set(event, set);
|
|
60
|
+
const entry = {
|
|
61
|
+
fn: listener,
|
|
62
|
+
heard: false
|
|
63
|
+
};
|
|
64
|
+
set.add(entry);
|
|
65
|
+
if (this.lastPayloads.has(event)) {
|
|
66
|
+
queueMicrotask(() => {
|
|
67
|
+
if (!set.has(entry) || entry.heard) return;
|
|
68
|
+
entry.heard = true;
|
|
69
|
+
call(event, entry, this.lastPayloads.get(event));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
this.startPolling();
|
|
73
|
+
return () => {
|
|
74
|
+
set.delete(entry);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The finished output. Starts polling if nothing has yet.
|
|
79
|
+
*
|
|
80
|
+
* Several callers may await the same job (a component, a devtools panel,
|
|
81
|
+
* an analytics hook) and share one polling loop. Rejects with a
|
|
82
|
+
* `Bakery3Error` when the job fails (after `failed` is emitted), with code
|
|
83
|
+
* `Cancelled` when it was cancelled, and with an `AbortError` when the wait
|
|
84
|
+
* was stopped by `stop()` or by `signal`.
|
|
85
|
+
*/
|
|
86
|
+
result(options = {}) {
|
|
87
|
+
const outcome = this.pendingOutcome();
|
|
88
|
+
if (this.hasEnded) return outcome.promise;
|
|
89
|
+
const { signal } = options;
|
|
90
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
91
|
+
this.waiting++;
|
|
92
|
+
this.startPolling();
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
let waiting = true;
|
|
95
|
+
const done = () => {
|
|
96
|
+
if (!waiting) return false;
|
|
97
|
+
waiting = false;
|
|
98
|
+
this.waiting--;
|
|
99
|
+
signal?.removeEventListener("abort", onAbort);
|
|
100
|
+
return true;
|
|
101
|
+
};
|
|
102
|
+
const onAbort = () => {
|
|
103
|
+
if (done()) reject(signal.reason);
|
|
104
|
+
};
|
|
105
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
106
|
+
outcome.promise.then(
|
|
107
|
+
(value) => {
|
|
108
|
+
if (done()) resolve(value);
|
|
109
|
+
},
|
|
110
|
+
(error) => {
|
|
111
|
+
if (done()) reject(error);
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Stop polling, here, now. The job is NOT cancelled on the server: it
|
|
118
|
+
* keeps running and is still charged, and `getRender(id)` or `getBake(id)`
|
|
119
|
+
* reads it later. A pending `result()` rejects with an `AbortError`, and a
|
|
120
|
+
* later `on()` or `result()` starts polling again.
|
|
121
|
+
*
|
|
122
|
+
* `cancel()` is the other one: it asks the server to stop the job, which
|
|
123
|
+
* then ends as `cancelled`.
|
|
124
|
+
*/
|
|
125
|
+
stop() {
|
|
126
|
+
this.poller?.abort();
|
|
127
|
+
this.poller = null;
|
|
128
|
+
if (this.hasEnded || !this.pending) return;
|
|
129
|
+
const stopped = this.pending;
|
|
130
|
+
this.pending = null;
|
|
131
|
+
stopped.reject(
|
|
132
|
+
abortError(
|
|
133
|
+
`Stopped watching ${this.jobNoun} ${this.id}. It still runs on the server.`
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
/** Apply a record read from the API, and settle if it is the last one. */
|
|
138
|
+
applyRecord(record2) {
|
|
139
|
+
this.record = record2;
|
|
140
|
+
this.observeRecord(record2);
|
|
141
|
+
if (isTerminal(record2.state)) void this.settleEnd();
|
|
142
|
+
}
|
|
143
|
+
emitEvent(event, payload) {
|
|
144
|
+
if (this.replayedEvents.includes(event))
|
|
145
|
+
this.lastPayloads.set(event, payload);
|
|
146
|
+
for (const entry of this.subscribers.get(event) ?? []) {
|
|
147
|
+
entry.heard = true;
|
|
148
|
+
call(event, entry, payload);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
pendingOutcome() {
|
|
152
|
+
if (this.pending) return this.pending;
|
|
153
|
+
let resolve;
|
|
154
|
+
let reject;
|
|
155
|
+
const promise = new Promise((res, rej) => {
|
|
156
|
+
resolve = res;
|
|
157
|
+
reject = rej;
|
|
158
|
+
});
|
|
159
|
+
promise.catch(() => {
|
|
160
|
+
});
|
|
161
|
+
this.pending = { promise, resolve, reject };
|
|
162
|
+
return this.pending;
|
|
163
|
+
}
|
|
164
|
+
isWatched() {
|
|
165
|
+
if (this.waiting > 0) return true;
|
|
166
|
+
for (const set of this.subscribers.values()) if (set.size > 0) return true;
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
startPolling() {
|
|
170
|
+
if (this.poller || this.hasEnded) return;
|
|
171
|
+
const loop = new AbortController();
|
|
172
|
+
this.poller = loop;
|
|
173
|
+
queueMicrotask(() => void this.pollLoop(loop));
|
|
174
|
+
}
|
|
175
|
+
async pollLoop(loop) {
|
|
176
|
+
const { signal } = loop;
|
|
177
|
+
const failures = new PollFailures();
|
|
178
|
+
try {
|
|
179
|
+
for (let step = 0; ; step++) {
|
|
180
|
+
if (signal.aborted || this.hasEnded) return;
|
|
181
|
+
if (isTerminal(this.record.state)) return void await this.settleEnd();
|
|
182
|
+
if (!this.isWatched()) return;
|
|
183
|
+
const wait = this.pollSchedule[Math.min(step, this.pollSchedule.length - 1)];
|
|
184
|
+
if (!await pause(wait, signal)) return;
|
|
185
|
+
try {
|
|
186
|
+
const record2 = await this.readRecord(signal);
|
|
187
|
+
if (signal.aborted) return;
|
|
188
|
+
this.applyRecord(record2);
|
|
189
|
+
failures.succeeded();
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (signal.aborted) return;
|
|
192
|
+
if (error instanceof Bakery3Error && !error.retryable)
|
|
193
|
+
return this.settleFailure(error);
|
|
194
|
+
if (failures.exhausted()) return this.settleFailure(error);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
if (this.poller === loop) this.poller = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Settle on the record's terminal state. Once. */
|
|
202
|
+
async settleEnd() {
|
|
203
|
+
if (this.hasEnded) return;
|
|
204
|
+
this.hasEnded = true;
|
|
205
|
+
this.poller?.abort();
|
|
206
|
+
this.poller = null;
|
|
207
|
+
const record2 = this.record;
|
|
208
|
+
const outcome = this.pendingOutcome();
|
|
209
|
+
if (record2.state === "cancelled") {
|
|
210
|
+
this.emitEvent("cancelled", this.recordPayload(record2));
|
|
211
|
+
outcome.reject(
|
|
212
|
+
new Bakery3Error({
|
|
213
|
+
code: "Cancelled",
|
|
214
|
+
message: `${capitalize(this.jobNoun)} ${this.id} was cancelled.`,
|
|
215
|
+
retryable: false
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (record2.state === "completed" && record2.result) {
|
|
221
|
+
try {
|
|
222
|
+
outcome.resolve(await this.completeRecord(record2));
|
|
223
|
+
} catch (error) {
|
|
224
|
+
this.settleFailure(error);
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (record2.state === "expired")
|
|
229
|
+
return this.settleFailure(this.expiredError());
|
|
230
|
+
this.settleFailure(
|
|
231
|
+
record2.error ? Bakery3Error.from(record2.error) : new Bakery3Error({
|
|
232
|
+
code: "WorkerFailure",
|
|
233
|
+
message: `${capitalize(this.jobNoun)} ended in state "${record2.state}" with no result.`,
|
|
234
|
+
retryable: true
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
/** Emit `failed`, then reject. Every failure goes through here. */
|
|
239
|
+
settleFailure(cause) {
|
|
240
|
+
this.hasEnded = true;
|
|
241
|
+
this.poller?.abort();
|
|
242
|
+
this.poller = null;
|
|
243
|
+
const error = cause instanceof Bakery3Error ? cause : new Bakery3Error({
|
|
244
|
+
code: "NetworkError",
|
|
245
|
+
message: `Could not read ${this.jobNoun} ${this.id}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
246
|
+
retryable: true
|
|
247
|
+
});
|
|
248
|
+
this.emitEvent("failed", {
|
|
249
|
+
error,
|
|
250
|
+
...this.recordPayload(this.record)
|
|
251
|
+
});
|
|
252
|
+
this.pendingOutcome().reject(error);
|
|
253
|
+
}
|
|
254
|
+
expiredError() {
|
|
255
|
+
const files = this.jobNoun === "bake" ? "lightmaps" : "image";
|
|
256
|
+
return new Bakery3Error({
|
|
257
|
+
code: "Expired",
|
|
258
|
+
message: `${capitalize(this.jobNoun)} ${this.id} has expired, so its ${files} can no longer be downloaded.`,
|
|
259
|
+
why: `A ${this.jobNoun} expires when the project's retention ends or someone deletes it. The record stays, with its cost, but the files are gone.`,
|
|
260
|
+
fix: `${this.jobNoun === "bake" ? "Bake" : "Render"} again. To keep a result, copy its files into your own storage when the ${this.jobNoun} completes.`,
|
|
261
|
+
docs: "https://bakery3.com/docs/limits#retention",
|
|
262
|
+
retryable: false
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
var RenderJob = class extends PolledJob {
|
|
267
|
+
jobNoun = "render";
|
|
268
|
+
pollSchedule = POLL_SCHEDULE;
|
|
269
|
+
replayedEvents = [
|
|
270
|
+
"preview",
|
|
271
|
+
"progress",
|
|
272
|
+
"complete",
|
|
273
|
+
"failed",
|
|
274
|
+
"cancelled"
|
|
275
|
+
];
|
|
276
|
+
previewSeen = false;
|
|
277
|
+
lastState = null;
|
|
278
|
+
constructor(client, record2) {
|
|
279
|
+
super(client, record2);
|
|
280
|
+
}
|
|
281
|
+
get previewUrl() {
|
|
282
|
+
return this.record.previewUrl;
|
|
283
|
+
}
|
|
284
|
+
/** The estimate this render was accepted under. */
|
|
285
|
+
get estimate() {
|
|
286
|
+
return this.record.estimate;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Ask the server to stop the render. It ends as `cancelled`: `cancelled`
|
|
290
|
+
* is emitted and `result()` rejects with code `Cancelled`. To stop
|
|
291
|
+
* watching without stopping the render, call `stop()`.
|
|
292
|
+
*/
|
|
293
|
+
async cancel() {
|
|
294
|
+
this.applyRecord(await this.client.cancelRender(this.id));
|
|
295
|
+
}
|
|
296
|
+
readRecord(signal) {
|
|
297
|
+
return this.client.getRender(this.id, { signal });
|
|
298
|
+
}
|
|
299
|
+
observeRecord(record2) {
|
|
300
|
+
if (record2.previewUrl && !this.previewSeen) {
|
|
301
|
+
this.previewSeen = true;
|
|
302
|
+
this.emitEvent("preview", { url: record2.previewUrl, render: record2 });
|
|
303
|
+
}
|
|
304
|
+
if (record2.state !== this.lastState) {
|
|
305
|
+
this.lastState = record2.state;
|
|
306
|
+
if (record2.state === "queued")
|
|
307
|
+
this.emitEvent("queued", { render: record2 });
|
|
308
|
+
if (record2.state === "preparing")
|
|
309
|
+
this.emitEvent("preparing", { render: record2 });
|
|
310
|
+
}
|
|
311
|
+
this.emitEvent("progress", {
|
|
312
|
+
percent: Math.round(record2.progress * 100),
|
|
313
|
+
state: record2.state,
|
|
314
|
+
render: record2
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
async completeRecord(record2) {
|
|
318
|
+
const result = record2.result;
|
|
319
|
+
this.emitEvent("complete", { result, render: record2 });
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
recordPayload(record2) {
|
|
323
|
+
return { render: record2 };
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
function call(event, entry, payload) {
|
|
327
|
+
try {
|
|
328
|
+
entry.fn(payload);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error(`[bakery3] listener for "${String(event)}" threw:`, error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function pause(ms, signal) {
|
|
334
|
+
return new Promise((resolve) => {
|
|
335
|
+
if (signal.aborted) return resolve(false);
|
|
336
|
+
const timer = setTimeout(() => {
|
|
337
|
+
signal.removeEventListener("abort", onAbort);
|
|
338
|
+
resolve(true);
|
|
339
|
+
}, ms);
|
|
340
|
+
function onAbort() {
|
|
341
|
+
clearTimeout(timer);
|
|
342
|
+
resolve(false);
|
|
343
|
+
}
|
|
344
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function abortError(message) {
|
|
348
|
+
if (typeof DOMException === "function")
|
|
349
|
+
return new DOMException(message, "AbortError");
|
|
350
|
+
const error = new Error(message);
|
|
351
|
+
error.name = "AbortError";
|
|
352
|
+
return error;
|
|
353
|
+
}
|
|
354
|
+
function capitalize(word) {
|
|
355
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/capture/exported.ts
|
|
359
|
+
var GLB_MAGIC = 1179937895;
|
|
360
|
+
var CHUNK_JSON = 1313821514;
|
|
361
|
+
function exportedObjectIds(glb) {
|
|
362
|
+
const ids = /* @__PURE__ */ new Set();
|
|
363
|
+
const view = new DataView(glb);
|
|
364
|
+
if (view.byteLength < 20 || view.getUint32(0, true) !== GLB_MAGIC) return ids;
|
|
365
|
+
let offset = 12;
|
|
366
|
+
while (offset + 8 <= view.byteLength) {
|
|
367
|
+
const length = view.getUint32(offset, true);
|
|
368
|
+
const type = view.getUint32(offset + 4, true);
|
|
369
|
+
const start = offset + 8;
|
|
370
|
+
if (type === CHUNK_JSON) {
|
|
371
|
+
const text = new TextDecoder().decode(new Uint8Array(glb, start, length));
|
|
372
|
+
let document2;
|
|
373
|
+
try {
|
|
374
|
+
document2 = JSON.parse(text);
|
|
375
|
+
} catch {
|
|
376
|
+
return ids;
|
|
377
|
+
}
|
|
378
|
+
for (const node of document2.nodes ?? []) {
|
|
379
|
+
const id = node.extras?.bakery3Id;
|
|
380
|
+
if (typeof id === "string" && id) ids.add(id);
|
|
381
|
+
}
|
|
382
|
+
return ids;
|
|
383
|
+
}
|
|
384
|
+
offset = start + length + (4 - length % 4) % 4;
|
|
385
|
+
}
|
|
386
|
+
return ids;
|
|
387
|
+
}
|
|
388
|
+
function addressableIds(manifest, glb) {
|
|
389
|
+
const ids = exportedObjectIds(glb);
|
|
390
|
+
for (const light of manifest.lights ?? []) ids.add(light.objectId);
|
|
391
|
+
return ids;
|
|
392
|
+
}
|
|
393
|
+
function assertAnimationTargets(animation, manifest, glb, names) {
|
|
394
|
+
const addressable = addressableIds(manifest, glb);
|
|
395
|
+
if (addressable.size === 0) return;
|
|
396
|
+
for (const channel of animation.channels) {
|
|
397
|
+
if (channel.target.type !== "object") continue;
|
|
398
|
+
const id = channel.target.objectId;
|
|
399
|
+
if (addressable.has(id)) continue;
|
|
400
|
+
const name = names?.get(id);
|
|
401
|
+
throw new Bakery3Error({
|
|
402
|
+
code: "InvalidRequest",
|
|
403
|
+
message: `The timeline animates "${channel.property}" on ${name ? `"${name}"` : `an object (${id.slice(0, 8)}\u2026)`}, which did not make it into the exported scene.`,
|
|
404
|
+
why: "The channel would arrive at the renderer with an id that matches nothing in the GLB, and the render would refuse after the upload rather than here.",
|
|
405
|
+
fix: `Check that the object is still in the scene and is exportable: it must not be a helper, a gizmo, or marked userData.bakery3.exclude. Otherwise drop the track with timeline.removeTrack().`,
|
|
406
|
+
docs: "https://bakery3.com/docs/animation"
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/bake/job.ts
|
|
412
|
+
var POLL_SCHEDULE2 = [800, 800, 1e3, 1500, 2e3, 3e3];
|
|
413
|
+
var BakeJob = class extends PolledJob {
|
|
414
|
+
jobNoun = "bake";
|
|
415
|
+
pollSchedule = POLL_SCHEDULE2;
|
|
416
|
+
replayedEvents = [
|
|
417
|
+
"progress",
|
|
418
|
+
"complete",
|
|
419
|
+
"failed",
|
|
420
|
+
"cancelled"
|
|
421
|
+
];
|
|
422
|
+
fetchImpl;
|
|
423
|
+
lastState = null;
|
|
424
|
+
constructor(client, record2, fetchImpl) {
|
|
425
|
+
super(client, record2);
|
|
426
|
+
this.fetchImpl = fetchImpl ?? client.fetch ?? globalThis.fetch.bind(globalThis);
|
|
427
|
+
}
|
|
428
|
+
/** The estimate this bake was accepted under. */
|
|
429
|
+
get estimate() {
|
|
430
|
+
return this.record.estimate;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Ask the server to stop the bake. It ends as `cancelled`: `cancelled` is
|
|
434
|
+
* emitted and `result()` rejects with code `Cancelled`. To stop watching
|
|
435
|
+
* without stopping the bake, call `stop()`.
|
|
436
|
+
*/
|
|
437
|
+
async cancel() {
|
|
438
|
+
this.applyRecord(await this.client.cancelBake(this.id));
|
|
439
|
+
}
|
|
440
|
+
readRecord(signal) {
|
|
441
|
+
return this.client.getBake(this.id, { signal });
|
|
442
|
+
}
|
|
443
|
+
observeRecord(record2) {
|
|
444
|
+
if (record2.state !== this.lastState) {
|
|
445
|
+
this.lastState = record2.state;
|
|
446
|
+
if (record2.state === "queued") this.emitEvent("queued", { bake: record2 });
|
|
447
|
+
}
|
|
448
|
+
this.emitEvent("progress", {
|
|
449
|
+
percent: Math.round(record2.progress * 100),
|
|
450
|
+
state: record2.state,
|
|
451
|
+
bake: record2
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async completeRecord(record2) {
|
|
455
|
+
const output = await this.resolve(record2.result);
|
|
456
|
+
this.emitEvent("complete", { output, bake: record2 });
|
|
457
|
+
return output;
|
|
458
|
+
}
|
|
459
|
+
recordPayload(record2) {
|
|
460
|
+
return { bake: record2 };
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* bundle.json from storage, every uri replaced by its signed URL.
|
|
464
|
+
*
|
|
465
|
+
* A name the bundle uses that the control plane signed nothing for is a
|
|
466
|
+
* refusal here, with the name: never a bundle that quietly points at a
|
|
467
|
+
* texture the browser will 403 on ten seconds later.
|
|
468
|
+
*/
|
|
469
|
+
async resolve(result) {
|
|
470
|
+
const response = await this.fetchImpl(result.bundleUrl).catch(
|
|
471
|
+
(cause) => {
|
|
472
|
+
throw new Bakery3Error({
|
|
473
|
+
code: "NetworkError",
|
|
474
|
+
message: `Could not fetch the bake bundle: ${cause.message}`,
|
|
475
|
+
retryable: true
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
if (!response.ok) {
|
|
480
|
+
throw new Bakery3Error({
|
|
481
|
+
code: "WorkerFailure",
|
|
482
|
+
message: `The bake bundle could not be read from storage (${response.status}).`,
|
|
483
|
+
why: "The signed URL was refused. These expire after 24 hours.",
|
|
484
|
+
fix: "Fetch the bake again with bakery3.getBake(id) for fresh URLs.",
|
|
485
|
+
docs: "https://bakery3.com/docs/bake",
|
|
486
|
+
retryable: true
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
const bundle = await response.json();
|
|
490
|
+
const url = (uri, what) => {
|
|
491
|
+
const signed = result.files[uri];
|
|
492
|
+
if (!signed) {
|
|
493
|
+
throw new Bakery3Error({
|
|
494
|
+
code: "WorkerFailure",
|
|
495
|
+
message: `The bake bundle names ${what} "${uri}" that storage has no signed URL for.`,
|
|
496
|
+
why: "The render worker wrote a file the control plane did not sign, or the upload of that file failed.",
|
|
497
|
+
fix: "Bake again. If it repeats, this is a bug on our side: write to hello@bakery3.com with the bake id.",
|
|
498
|
+
docs: "https://bakery3.com/docs/bake",
|
|
499
|
+
retryable: true
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return signed;
|
|
503
|
+
};
|
|
504
|
+
const resolved = {
|
|
505
|
+
...bundle,
|
|
506
|
+
objects: bundle.objects.map((object) => ({
|
|
507
|
+
...object,
|
|
508
|
+
lightmap: {
|
|
509
|
+
...object.lightmap,
|
|
510
|
+
uri: url(object.lightmap.uri, "a lightmap")
|
|
511
|
+
},
|
|
512
|
+
...object.preview ? { preview: { uri: url(object.preview.uri, "a preview") } } : {}
|
|
513
|
+
})),
|
|
514
|
+
...bundle.atlas ? {
|
|
515
|
+
atlas: {
|
|
516
|
+
...bundle.atlas,
|
|
517
|
+
uri: url(bundle.atlas.uri, "the atlas"),
|
|
518
|
+
...bundle.atlas.preview ? {
|
|
519
|
+
preview: {
|
|
520
|
+
uri: url(bundle.atlas.preview.uri, "the atlas preview")
|
|
521
|
+
}
|
|
522
|
+
} : {}
|
|
523
|
+
}
|
|
524
|
+
} : {},
|
|
525
|
+
...bundle.proxies ? {
|
|
526
|
+
proxies: bundle.proxies.map(
|
|
527
|
+
(proxy) => proxy.kind === "billboard" ? {
|
|
528
|
+
...proxy,
|
|
529
|
+
map: {
|
|
530
|
+
...proxy.map,
|
|
531
|
+
uri: url(proxy.map.uri, "a billboard")
|
|
532
|
+
}
|
|
533
|
+
} : proxy.kind === "decimated" ? {
|
|
534
|
+
...proxy,
|
|
535
|
+
lightmap: {
|
|
536
|
+
...proxy.lightmap,
|
|
537
|
+
uri: url(
|
|
538
|
+
proxy.lightmap.uri,
|
|
539
|
+
"a decimated object's lightmap"
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
} : {
|
|
543
|
+
...proxy,
|
|
544
|
+
color: {
|
|
545
|
+
uri: url(proxy.color.uri, "a stand-in's color map")
|
|
546
|
+
},
|
|
547
|
+
normal: {
|
|
548
|
+
uri: url(proxy.normal.uri, "a stand-in's normal map")
|
|
549
|
+
},
|
|
550
|
+
lightmap: {
|
|
551
|
+
...proxy.lightmap,
|
|
552
|
+
uri: url(proxy.lightmap.uri, "a stand-in's lightmap")
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
)
|
|
556
|
+
} : {},
|
|
557
|
+
...bundle.shadow ? {
|
|
558
|
+
shadow: {
|
|
559
|
+
...bundle.shadow,
|
|
560
|
+
uri: url(bundle.shadow.uri, "the shadow plane")
|
|
561
|
+
}
|
|
562
|
+
} : {},
|
|
563
|
+
...bundle.glb ? { glb: { uri: url(bundle.glb.uri, "the GLB") } } : {},
|
|
564
|
+
...bundle.probe ? {
|
|
565
|
+
probe: {
|
|
566
|
+
...bundle.probe,
|
|
567
|
+
uri: url(bundle.probe.uri, "the reflection probe"),
|
|
568
|
+
...bundle.probe.preview ? {
|
|
569
|
+
preview: {
|
|
570
|
+
...bundle.probe.preview,
|
|
571
|
+
uri: url(bundle.probe.preview.uri, "the probe preview")
|
|
572
|
+
}
|
|
573
|
+
} : {}
|
|
574
|
+
}
|
|
575
|
+
} : {}
|
|
576
|
+
};
|
|
577
|
+
const { summary: _summary, ...rest } = result;
|
|
578
|
+
return { ...rest, bundle: resolved };
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// src/upload.ts
|
|
583
|
+
var CONCURRENCY = 4;
|
|
584
|
+
var ATTEMPTS = 3;
|
|
585
|
+
async function uploadMissingAssets(client, assets, onProgress) {
|
|
586
|
+
const check = await client.checkAssets({
|
|
587
|
+
assets: assets.map(({ data: _data, ...descriptor }) => descriptor)
|
|
588
|
+
});
|
|
589
|
+
const fetchImpl = client.fetch ?? globalThis.fetch.bind(globalThis);
|
|
590
|
+
const targets = new Map(check.uploads.map((upload) => [upload.hash, upload]));
|
|
591
|
+
const missing = assets.filter((asset) => targets.has(asset.hash));
|
|
592
|
+
const progress = {
|
|
593
|
+
cached: assets.length - missing.length,
|
|
594
|
+
uploaded: 0,
|
|
595
|
+
bytesUploaded: 0,
|
|
596
|
+
bytesTotal: missing.reduce((sum, asset) => sum + asset.bytes, 0)
|
|
597
|
+
};
|
|
598
|
+
onProgress?.(progress);
|
|
599
|
+
const queue = [...missing];
|
|
600
|
+
const workers = Array.from(
|
|
601
|
+
{ length: Math.min(CONCURRENCY, queue.length) },
|
|
602
|
+
async () => {
|
|
603
|
+
for (; ; ) {
|
|
604
|
+
const asset = queue.shift();
|
|
605
|
+
if (!asset) return;
|
|
606
|
+
const target = targets.get(asset.hash);
|
|
607
|
+
await put(asset, target, fetchImpl);
|
|
608
|
+
progress.uploaded++;
|
|
609
|
+
progress.bytesUploaded += asset.bytes;
|
|
610
|
+
onProgress?.({ ...progress });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
);
|
|
614
|
+
await Promise.all(workers);
|
|
615
|
+
return progress;
|
|
616
|
+
}
|
|
617
|
+
async function put(asset, target, fetchImpl) {
|
|
618
|
+
let last = "";
|
|
619
|
+
for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
|
620
|
+
try {
|
|
621
|
+
const response = await fetchImpl(target.url, {
|
|
622
|
+
method: "PUT",
|
|
623
|
+
// Verbatim. A presigned PUT is signed over an exact header set, so an
|
|
624
|
+
// extra or missing header is a 403 that reads like an auth bug.
|
|
625
|
+
headers: { ...target.headers },
|
|
626
|
+
body: asset.data
|
|
627
|
+
});
|
|
628
|
+
if (response.ok) return;
|
|
629
|
+
last = `${response.status} ${response.statusText}`;
|
|
630
|
+
if (response.status < 500 && response.status !== 429) break;
|
|
631
|
+
} catch (error) {
|
|
632
|
+
last = error instanceof Error ? error.message : String(error);
|
|
633
|
+
}
|
|
634
|
+
if (attempt < ATTEMPTS) {
|
|
635
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * 500));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
throw new Bakery3Error({
|
|
639
|
+
code: "AssetUploadIncomplete",
|
|
640
|
+
message: `Could not upload ${asset.name ?? asset.kind} (${formatBytes(asset.bytes)}): ${last}`,
|
|
641
|
+
why: "The presigned upload URL was rejected. These expire, and they are signed over an exact set of headers.",
|
|
642
|
+
fix: "Retry the render. If it keeps failing, check for a service worker or proxy that rewrites request headers.",
|
|
643
|
+
docs: "https://bakery3.com/docs/uploads",
|
|
644
|
+
retryable: true
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
function formatBytes(bytes) {
|
|
648
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
649
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
650
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/index.ts
|
|
654
|
+
function createBakery3(options = {}) {
|
|
655
|
+
const client = new Bakery3Client(options);
|
|
656
|
+
const defaults = options.defaults ?? {};
|
|
657
|
+
const clientFetch = options.fetch;
|
|
658
|
+
let attached = null;
|
|
659
|
+
function resolveScene(explicit, method) {
|
|
660
|
+
const scene = explicit ?? attached?.scene;
|
|
661
|
+
if (!scene) throw missingContext(method, "scene");
|
|
662
|
+
return scene;
|
|
663
|
+
}
|
|
664
|
+
function resolveCamera(explicit, method) {
|
|
665
|
+
const camera = explicit ?? attached?.camera;
|
|
666
|
+
if (!camera) throw missingContext(method, "camera");
|
|
667
|
+
return camera;
|
|
668
|
+
}
|
|
669
|
+
let warnedRenderer = false;
|
|
670
|
+
function rendererFor(explicit) {
|
|
671
|
+
const renderer = explicit ?? attached?.renderer;
|
|
672
|
+
if (!renderer && !warnedRenderer && inBrowser()) {
|
|
673
|
+
warnedRenderer = true;
|
|
674
|
+
console.warn(
|
|
675
|
+
"Bakery3: no renderer was given, so this render uses no tone mapping and an exposure of 1, and can look darker or flatter than your viewport. Pass renderer to render(), or call bakery3.attach({ scene, camera, renderer }) once. https://bakery3.com/docs/troubleshooting#the-render-is-darker-or-flatter-than-my-viewport"
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
return renderer;
|
|
679
|
+
}
|
|
680
|
+
function outputFor(call2, quality, camera, renderer, even = false) {
|
|
681
|
+
return resolveOutput(call2, defaults.output, {
|
|
682
|
+
quality,
|
|
683
|
+
camera: camera ?? null,
|
|
684
|
+
renderer,
|
|
685
|
+
even
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
function renderQuote(input) {
|
|
689
|
+
return {
|
|
690
|
+
quality: input.quality,
|
|
691
|
+
// Only the size is priced. The format is left out of the quote so that
|
|
692
|
+
// `estimate({ frames })` and the clip it precedes send the same body.
|
|
693
|
+
output: {
|
|
694
|
+
width: input.output.width,
|
|
695
|
+
height: input.output.height,
|
|
696
|
+
format: "png"
|
|
697
|
+
},
|
|
698
|
+
...input.advanced ? { advanced: input.advanced } : {},
|
|
699
|
+
scene: estimateScene(input.scene, input.camera),
|
|
700
|
+
...input.frames ? { frames: input.frames } : {}
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
async function capture(options2, method) {
|
|
704
|
+
return captureScene({
|
|
705
|
+
scene: resolveScene(options2.scene, method),
|
|
706
|
+
camera: resolveCamera(options2.camera, method),
|
|
707
|
+
renderer: rendererFor(options2.renderer),
|
|
708
|
+
environment: options2.environment,
|
|
709
|
+
productState: options2.productState,
|
|
710
|
+
background: options2.background
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
function recorderHost(options2, method) {
|
|
714
|
+
return {
|
|
715
|
+
async capture({ scene, camera }) {
|
|
716
|
+
const captured = await capture({ ...options2, scene, camera }, method);
|
|
717
|
+
return { ...captured, objects: objectIdsByName(scene) };
|
|
718
|
+
},
|
|
719
|
+
uploadAssets: (captured, onProgress) => uploadMissingAssets(client, captured.assets, onProgress)
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function setOutput(options2, cameras, scene) {
|
|
723
|
+
const quality = options2.quality ?? defaults.quality ?? "studio";
|
|
724
|
+
return outputFor(
|
|
725
|
+
options2,
|
|
726
|
+
quality,
|
|
727
|
+
firstCamera(cameras, scene) ?? attached?.camera,
|
|
728
|
+
options2.renderer ?? attached?.renderer
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
function pictures(options2, cameras, method) {
|
|
732
|
+
const quality = options2.quality ?? defaults.quality ?? "studio";
|
|
733
|
+
const scene = resolveScene(options2.scene, method);
|
|
734
|
+
const output = setOutput(options2, cameras, scene);
|
|
735
|
+
return {
|
|
736
|
+
host: recorderHost(options2, method),
|
|
737
|
+
scene,
|
|
738
|
+
output,
|
|
739
|
+
spec: (manifest) => ({
|
|
740
|
+
manifest,
|
|
741
|
+
quality,
|
|
742
|
+
output,
|
|
743
|
+
...options2.advanced ? { advanced: options2.advanced } : {},
|
|
744
|
+
...options2.preview !== void 0 ? { preview: options2.preview } : {}
|
|
745
|
+
})
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
async function renderCameras(options2) {
|
|
749
|
+
const maxCost = options2.maxCost ?? defaults.maxCost;
|
|
750
|
+
const key = options2.idempotencyKey ?? `set_${randomId()}`;
|
|
751
|
+
const plan = pictures(options2, options2.cameras, "render");
|
|
752
|
+
const batch = new Batch(
|
|
753
|
+
client,
|
|
754
|
+
await client.openBatch({
|
|
755
|
+
key,
|
|
756
|
+
lane: "normal",
|
|
757
|
+
...maxCost !== void 0 ? { maxCost } : {}
|
|
758
|
+
})
|
|
759
|
+
);
|
|
760
|
+
const keys = [];
|
|
761
|
+
const recorded = await record({
|
|
762
|
+
...plan,
|
|
763
|
+
batch,
|
|
764
|
+
variants: [key],
|
|
765
|
+
idOf: () => key,
|
|
766
|
+
cameras: options2.cameras,
|
|
767
|
+
metadataOf: (_set, ids) => ({ ...options2.metadata, camera: ids.camera }),
|
|
768
|
+
keys,
|
|
769
|
+
onVariant: ({ upload }) => upload && options2.onUpload?.(upload)
|
|
770
|
+
});
|
|
771
|
+
await batch.close();
|
|
772
|
+
return new RenderSet(batch, recorded).ordered(keys);
|
|
773
|
+
}
|
|
774
|
+
function variantsRecording(options2, batch, method) {
|
|
775
|
+
const cameras = options2.cameras ?? [resolveCamera(void 0, method)];
|
|
776
|
+
return {
|
|
777
|
+
...pictures(options2, cameras, method),
|
|
778
|
+
batch,
|
|
779
|
+
variants: options2.variants,
|
|
780
|
+
idOf: options2.id ?? defaultVariantId,
|
|
781
|
+
...options2.apply ? { apply: options2.apply } : {},
|
|
782
|
+
...options2.finishes ? { finishesOf: options2.finishes } : {},
|
|
783
|
+
cameras,
|
|
784
|
+
metadataOf: (variant, ids) => ({
|
|
785
|
+
...options2.metadata?.(variant),
|
|
786
|
+
...ids
|
|
787
|
+
}),
|
|
788
|
+
...options2.version ? { version: options2.version } : {},
|
|
789
|
+
...options2.maxCostPerImage !== void 0 ? { maxCostPerImage: options2.maxCostPerImage } : {},
|
|
790
|
+
...options2.signal ? { signal: options2.signal } : {},
|
|
791
|
+
...options2.onProgress ? { onVariant: options2.onProgress } : {}
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
function openVariants(options2, method) {
|
|
795
|
+
requireRunKey(options2.key, method);
|
|
796
|
+
return client.openBatch({
|
|
797
|
+
key: options2.key,
|
|
798
|
+
...options2.name ? { name: options2.name } : {},
|
|
799
|
+
...options2.maxCost !== void 0 ? { maxCost: options2.maxCost } : {},
|
|
800
|
+
lane: options2.lane ?? "batch",
|
|
801
|
+
...options2.maxParallel !== void 0 ? { maxParallel: options2.maxParallel } : {}
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
async function renderVariants(options2) {
|
|
805
|
+
if (!(options2.maxCost > 0)) {
|
|
806
|
+
throw new Bakery3Error({
|
|
807
|
+
code: "InvalidRequest",
|
|
808
|
+
message: "renderVariants() needs a maxCost.",
|
|
809
|
+
why: "A run is as many renders as the variants multiply out to, and a ceiling is what makes that number safe to be wrong about: a page that would cross it is refused, never charged.",
|
|
810
|
+
fix: "Call quoteVariants() with the same options for the real number, then set `maxCost` a little above its `maximum`.",
|
|
811
|
+
docs: "https://bakery3.com/docs/batches#estimating-the-whole-catalog"
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
const batch = new Batch(
|
|
815
|
+
client,
|
|
816
|
+
await openVariants(options2, "renderVariants")
|
|
817
|
+
);
|
|
818
|
+
const recorded = await record(
|
|
819
|
+
variantsRecording(options2, batch, "renderVariants")
|
|
820
|
+
);
|
|
821
|
+
await batch.close();
|
|
822
|
+
return new RenderSet(batch, recorded);
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
version: SDK_VERSION,
|
|
826
|
+
attach(context) {
|
|
827
|
+
attached = context;
|
|
828
|
+
},
|
|
829
|
+
check(scene) {
|
|
830
|
+
const { score, issues, counts } = analyzeScene(
|
|
831
|
+
resolveScene(scene, "check")
|
|
832
|
+
);
|
|
833
|
+
return { score, issues, counts };
|
|
834
|
+
},
|
|
835
|
+
async inspect(options2 = {}) {
|
|
836
|
+
return (await capture(options2, "inspect")).manifest;
|
|
837
|
+
},
|
|
838
|
+
async capture(options2 = {}) {
|
|
839
|
+
const captured = await capture(options2, "capture");
|
|
840
|
+
return {
|
|
841
|
+
...captured,
|
|
842
|
+
objects: objectIdsByName(resolveScene(options2.scene, "capture"))
|
|
843
|
+
};
|
|
844
|
+
},
|
|
845
|
+
uploadAssets(captured, onProgress) {
|
|
846
|
+
return uploadMissingAssets(client, captured.assets, onProgress);
|
|
847
|
+
},
|
|
848
|
+
batches: {
|
|
849
|
+
async open(request) {
|
|
850
|
+
return new Batch(client, await client.openBatch(request));
|
|
851
|
+
},
|
|
852
|
+
async get(id) {
|
|
853
|
+
return new Batch(client, await client.getBatch(id));
|
|
854
|
+
}
|
|
855
|
+
},
|
|
856
|
+
async estimate(options2 = {}) {
|
|
857
|
+
const scene = resolveScene(options2.scene, "estimate");
|
|
858
|
+
if (options2.bake) {
|
|
859
|
+
return client.estimate(
|
|
860
|
+
quoteBake(scene, options2.camera ?? attached?.camera, options2.bake)
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
const camera = resolveCamera(options2.camera, "estimate");
|
|
864
|
+
const quality = options2.quality ?? defaults.quality ?? "studio";
|
|
865
|
+
return client.estimate(
|
|
866
|
+
renderQuote({
|
|
867
|
+
quality,
|
|
868
|
+
// A clip's derived side is even, as `renderVideo()` rounds it.
|
|
869
|
+
output: outputFor(
|
|
870
|
+
options2,
|
|
871
|
+
quality,
|
|
872
|
+
camera,
|
|
873
|
+
attached?.renderer,
|
|
874
|
+
Boolean(options2.frames)
|
|
875
|
+
),
|
|
876
|
+
advanced: options2.advanced,
|
|
877
|
+
scene,
|
|
878
|
+
camera,
|
|
879
|
+
...options2.frames ? { frames: options2.frames } : {}
|
|
880
|
+
})
|
|
881
|
+
);
|
|
882
|
+
},
|
|
883
|
+
renderVariants,
|
|
884
|
+
async testVariants(options2, test = {}) {
|
|
885
|
+
const scene = resolveScene(options2.scene, "testVariants");
|
|
886
|
+
const output = setOutput(
|
|
887
|
+
options2,
|
|
888
|
+
options2.cameras ?? [resolveCamera(void 0, "testVariants")],
|
|
889
|
+
scene
|
|
890
|
+
);
|
|
891
|
+
return renderVariants({
|
|
892
|
+
...options2,
|
|
893
|
+
...testSettings(
|
|
894
|
+
{ key: options2.key, width: output.width, height: output.height },
|
|
895
|
+
test
|
|
896
|
+
),
|
|
897
|
+
name: `${options2.name ?? options2.key} (test)`,
|
|
898
|
+
variants: sampleOf(options2.variants, test.products)
|
|
899
|
+
});
|
|
900
|
+
},
|
|
901
|
+
async quoteVariants(options2) {
|
|
902
|
+
requireRunKey(options2.key, "quoteVariants");
|
|
903
|
+
const batch = new Batch(
|
|
904
|
+
client,
|
|
905
|
+
await openVariants(
|
|
906
|
+
{
|
|
907
|
+
key: quoteKey(options2.key),
|
|
908
|
+
name: `${options2.name ?? options2.key} (quote)`,
|
|
909
|
+
lane: options2.lane ?? "batch"
|
|
910
|
+
},
|
|
911
|
+
"quoteVariants"
|
|
912
|
+
)
|
|
913
|
+
);
|
|
914
|
+
try {
|
|
915
|
+
return await record({
|
|
916
|
+
...variantsRecording(options2, batch, "quoteVariants"),
|
|
917
|
+
dryRun: true
|
|
918
|
+
});
|
|
919
|
+
} finally {
|
|
920
|
+
await batch.cancel().catch(() => {
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
},
|
|
924
|
+
async getRenderSet(id) {
|
|
925
|
+
return new RenderSet(new Batch(client, await client.getBatch(id)));
|
|
926
|
+
},
|
|
927
|
+
async getRender(id) {
|
|
928
|
+
return new RenderJob(client, await client.getRender(id));
|
|
929
|
+
},
|
|
930
|
+
render: (async (options2 = {}) => {
|
|
931
|
+
if (hasCameras(options2)) return renderCameras(options2);
|
|
932
|
+
const scene = resolveScene(options2.scene, "render");
|
|
933
|
+
const camera = resolveCamera(options2.camera, "render");
|
|
934
|
+
const renderer = rendererFor(options2.renderer);
|
|
935
|
+
const quality = options2.quality ?? defaults.quality ?? "studio";
|
|
936
|
+
const output = outputFor(options2, quality, camera, renderer);
|
|
937
|
+
const maxCost = options2.maxCost ?? defaults.maxCost;
|
|
938
|
+
const { manifest, assets } = await captureScene({
|
|
939
|
+
scene,
|
|
940
|
+
camera,
|
|
941
|
+
renderer,
|
|
942
|
+
environment: options2.environment,
|
|
943
|
+
productState: options2.productState,
|
|
944
|
+
background: options2.background
|
|
945
|
+
});
|
|
946
|
+
if (maxCost !== void 0) {
|
|
947
|
+
const estimate = await client.estimate(
|
|
948
|
+
renderQuote({
|
|
949
|
+
quality,
|
|
950
|
+
output,
|
|
951
|
+
advanced: options2.advanced,
|
|
952
|
+
scene,
|
|
953
|
+
camera
|
|
954
|
+
})
|
|
955
|
+
);
|
|
956
|
+
if (estimate.maximumCost > maxCost) {
|
|
957
|
+
throw new RenderBudgetExceeded(maxCost, estimate, "render");
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
await uploadMissingAssets(client, assets, options2.onUpload);
|
|
961
|
+
const created = await client.createRender({
|
|
962
|
+
manifest,
|
|
963
|
+
quality,
|
|
964
|
+
output,
|
|
965
|
+
...options2.advanced ? { advanced: options2.advanced } : {},
|
|
966
|
+
...maxCost !== void 0 ? { maxCost } : {},
|
|
967
|
+
...options2.preview !== void 0 ? { preview: options2.preview } : {},
|
|
968
|
+
...options2.metadata ? { metadata: options2.metadata } : {},
|
|
969
|
+
...options2.idempotencyKey ? { idempotencyKey: options2.idempotencyKey } : {},
|
|
970
|
+
...options2.priority ? { priority: options2.priority } : {}
|
|
971
|
+
});
|
|
972
|
+
return new RenderJob(client, created);
|
|
973
|
+
}),
|
|
974
|
+
async bake(options2 = {}) {
|
|
975
|
+
const scene = resolveScene(options2.scene, "bake");
|
|
976
|
+
const camera = resolveCamera(options2.camera, "bake");
|
|
977
|
+
const maxCost = options2.maxCost ?? defaults.maxCost;
|
|
978
|
+
const { spec, assets } = await prepareBake({
|
|
979
|
+
scene,
|
|
980
|
+
camera,
|
|
981
|
+
renderer: rendererFor(options2.renderer),
|
|
982
|
+
environment: options2.environment,
|
|
983
|
+
productState: options2.productState,
|
|
984
|
+
background: options2.background,
|
|
985
|
+
...options2.settings ? { settings: options2.settings } : {},
|
|
986
|
+
...maxCost !== void 0 ? { maxCost } : {},
|
|
987
|
+
...options2.metadata ? { metadata: options2.metadata } : {},
|
|
988
|
+
...options2.idempotencyKey ? { idempotencyKey: options2.idempotencyKey } : {}
|
|
989
|
+
});
|
|
990
|
+
if (maxCost !== void 0) {
|
|
991
|
+
const quote = await client.estimate(bakeQuoteOf(spec));
|
|
992
|
+
if (quote.maximumCost > maxCost) {
|
|
993
|
+
throw new RenderBudgetExceeded(maxCost, quote, "bake");
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
await uploadMissingAssets(client, assets, options2.onUpload);
|
|
997
|
+
const record2 = await client.createBake(spec);
|
|
998
|
+
return new BakeJob(client, record2, clientFetch);
|
|
999
|
+
},
|
|
1000
|
+
async getBake(id) {
|
|
1001
|
+
return new BakeJob(client, await client.getBake(id), clientFetch);
|
|
1002
|
+
},
|
|
1003
|
+
timeline(options2 = {}) {
|
|
1004
|
+
return bindTimelineContext(createTimeline(options2), () => attached);
|
|
1005
|
+
},
|
|
1006
|
+
async renderVideo(options2) {
|
|
1007
|
+
const scene = resolveScene(options2.scene, "renderVideo");
|
|
1008
|
+
const camera = resolveCamera(options2.camera, "renderVideo");
|
|
1009
|
+
const renderer = rendererFor(options2.renderer);
|
|
1010
|
+
const quality = options2.quality ?? defaults.quality ?? "studio";
|
|
1011
|
+
const preferred = defaults.output?.format;
|
|
1012
|
+
const format = options2.format ?? (preferred === "mp4" || preferred === "webm" || preferred === "png-sequence" ? preferred : "mp4");
|
|
1013
|
+
const sequence = format === "png-sequence";
|
|
1014
|
+
const { quality: _imageQuality, ...size } = outputFor(
|
|
1015
|
+
{ ...options2, format },
|
|
1016
|
+
quality,
|
|
1017
|
+
camera,
|
|
1018
|
+
renderer,
|
|
1019
|
+
!sequence
|
|
1020
|
+
);
|
|
1021
|
+
const output = {
|
|
1022
|
+
...size,
|
|
1023
|
+
format,
|
|
1024
|
+
...sequence && options2.bitDepth !== void 0 ? { bitDepth: options2.bitDepth } : {}
|
|
1025
|
+
};
|
|
1026
|
+
const maxCost = options2.maxCost ?? defaults.maxCost;
|
|
1027
|
+
const animation = options2.timeline.bake({
|
|
1028
|
+
scene,
|
|
1029
|
+
...options2.fps !== void 0 ? { fps: options2.fps } : {}
|
|
1030
|
+
});
|
|
1031
|
+
const { manifest, assets } = await captureScene({
|
|
1032
|
+
scene,
|
|
1033
|
+
camera,
|
|
1034
|
+
renderer,
|
|
1035
|
+
environment: options2.environment,
|
|
1036
|
+
productState: options2.productState,
|
|
1037
|
+
background: options2.background
|
|
1038
|
+
});
|
|
1039
|
+
const animated = { ...manifest, animation };
|
|
1040
|
+
animated.protocolVersion = protocolVersionFor(animated);
|
|
1041
|
+
assertAnimationTargets(
|
|
1042
|
+
animation,
|
|
1043
|
+
animated,
|
|
1044
|
+
assets[0].data,
|
|
1045
|
+
namesByObjectId(options2.timeline)
|
|
1046
|
+
);
|
|
1047
|
+
if (maxCost !== void 0) {
|
|
1048
|
+
const quote = await client.estimate(
|
|
1049
|
+
renderQuote({
|
|
1050
|
+
quality,
|
|
1051
|
+
output,
|
|
1052
|
+
advanced: options2.advanced,
|
|
1053
|
+
scene,
|
|
1054
|
+
camera,
|
|
1055
|
+
frames: animation.frameCount
|
|
1056
|
+
})
|
|
1057
|
+
);
|
|
1058
|
+
if (quote.maximumCost > maxCost) {
|
|
1059
|
+
throw new RenderBudgetExceeded(maxCost, quote, "video");
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
await uploadMissingAssets(client, assets, options2.onUpload);
|
|
1063
|
+
const record2 = await client.createRender({
|
|
1064
|
+
manifest: animated,
|
|
1065
|
+
quality,
|
|
1066
|
+
output,
|
|
1067
|
+
...options2.advanced ? { advanced: options2.advanced } : {},
|
|
1068
|
+
...maxCost !== void 0 ? { maxCost } : {},
|
|
1069
|
+
// No mid-render preview for a video: a low-sample still of frame one
|
|
1070
|
+
// answers none of the questions somebody waiting on a clip is asking.
|
|
1071
|
+
preview: false,
|
|
1072
|
+
...options2.metadata ? { metadata: options2.metadata } : {},
|
|
1073
|
+
...options2.idempotencyKey ? { idempotencyKey: options2.idempotencyKey } : {},
|
|
1074
|
+
...options2.priority ? { priority: options2.priority } : {}
|
|
1075
|
+
});
|
|
1076
|
+
return new RenderJob(client, record2);
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
function estimateScene(scene, camera) {
|
|
1081
|
+
return {
|
|
1082
|
+
// Every triangle traced, a preset room's published count included, as
|
|
1083
|
+
// `manifest.stats.triangles` counts them for the price it is accepted at.
|
|
1084
|
+
triangles: tracedTriangles(scene),
|
|
1085
|
+
...captureFog(scene).fog ? { fog: true } : {},
|
|
1086
|
+
...findPresetRooms(scene).length ? { room: true } : {},
|
|
1087
|
+
...sceneComplexity(scene, camera)
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
function hasCameras(options) {
|
|
1091
|
+
return Array.isArray(options.cameras);
|
|
1092
|
+
}
|
|
1093
|
+
function randomId() {
|
|
1094
|
+
const bytes = new Uint8Array(9);
|
|
1095
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
1096
|
+
return Array.from(bytes, (byte) => byte.toString(36).padStart(2, "0")).join(
|
|
1097
|
+
""
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
function objectIdsByName(scene) {
|
|
1101
|
+
const ids = {};
|
|
1102
|
+
scene.traverse((object) => {
|
|
1103
|
+
if (!object.name || object.name in ids || shouldExclude(object)) return;
|
|
1104
|
+
ids[object.name] = idOf(object);
|
|
1105
|
+
});
|
|
1106
|
+
return ids;
|
|
1107
|
+
}
|
|
1108
|
+
function namesByObjectId(timeline) {
|
|
1109
|
+
const names = /* @__PURE__ */ new Map();
|
|
1110
|
+
for (const track of timeline.tracks) {
|
|
1111
|
+
if (track.target === "camera") continue;
|
|
1112
|
+
names.set(idOf(track.target), track.target.name || track.target.type);
|
|
1113
|
+
}
|
|
1114
|
+
return names;
|
|
1115
|
+
}
|
|
1116
|
+
function missingContext(method, what) {
|
|
1117
|
+
const pass = method === "check" ? "pass your scene to check(scene)" : `pass { scene, camera } to ${method}()`;
|
|
1118
|
+
return new Bakery3Error({
|
|
1119
|
+
code: "InvalidRequest",
|
|
1120
|
+
message: `No ${what} for ${method}().`,
|
|
1121
|
+
why: `${method}() was called with no ${what}, and none has been attached.`,
|
|
1122
|
+
fix: `In plain three.js, ${pass}, or call bakery3.attach({ scene, camera, renderer }) once. In React Three Fiber, use useBakery3() inside or beside <Canvas>.`,
|
|
1123
|
+
docs: "https://bakery3.com/docs/troubleshooting#no-scene-to-render"
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
function requireRunKey(key, method) {
|
|
1127
|
+
if (key) return;
|
|
1128
|
+
throw new Bakery3Error({
|
|
1129
|
+
code: "InvalidRequest",
|
|
1130
|
+
message: `${method}() needs a key.`,
|
|
1131
|
+
why: "The key names the run. It is what lets the same call resume a recording that stopped, instead of starting a second one.",
|
|
1132
|
+
fix: "Give it something stable that names this body of work: `key: 'fall-2026'`.",
|
|
1133
|
+
docs: "https://bakery3.com/docs/batches"
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
function firstCamera(cameras, scene) {
|
|
1137
|
+
for (const camera of cameras ?? []) {
|
|
1138
|
+
if (typeof camera === "string") {
|
|
1139
|
+
let found;
|
|
1140
|
+
scene.traverse((object) => {
|
|
1141
|
+
if (!found && object.isCamera && object.name === camera)
|
|
1142
|
+
found = object;
|
|
1143
|
+
});
|
|
1144
|
+
if (found) return found;
|
|
1145
|
+
} else if (camera.isCamera) {
|
|
1146
|
+
return camera;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return void 0;
|
|
1150
|
+
}
|
|
1151
|
+
function inBrowser() {
|
|
1152
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
export { BakeJob, RenderJob, createBakery3 };
|
|
1156
|
+
//# sourceMappingURL=chunk-ARMTTZXZ.js.map
|
|
1157
|
+
//# sourceMappingURL=chunk-ARMTTZXZ.js.map
|