@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,1279 @@
|
|
|
1
|
+
import { warnIfKeyIsExposed, Bakery3Client, quoteKey, testSettings, sampleOf, Batch } from '../chunk-25ZXSVQ3.js';
|
|
2
|
+
import { RenderSet, emptyRecorded } from '../chunk-ZEBVAIJJ.js';
|
|
3
|
+
import { QUALITY, isClipFormat, isVideoFormat, protocolVersionFor, SDK_VERSION } from '../chunk-APBOWWSV.js';
|
|
4
|
+
import { HDRI_PRESETS, PRESETS_VERSION, PEDESTAL_PRESETS } from '../chunk-6NYR73Y7.js';
|
|
5
|
+
import { ROOM_PRESETS } from '../chunk-MD2U6S4N.js';
|
|
6
|
+
import { Bakery3Error } from '../chunk-3G5QL4F4.js';
|
|
7
|
+
export { Bakery3Error, BatchPageRefused } from '../chunk-3G5QL4F4.js';
|
|
8
|
+
|
|
9
|
+
// src/frame.ts
|
|
10
|
+
var VIEW_WORDS = {
|
|
11
|
+
front: { azimuthDeg: 0, elevationDeg: 6 },
|
|
12
|
+
"three-quarter": { azimuthDeg: 35, elevationDeg: 14 },
|
|
13
|
+
side: { azimuthDeg: 90, elevationDeg: 6 },
|
|
14
|
+
back: { azimuthDeg: 180, elevationDeg: 6 },
|
|
15
|
+
"three-quarter-back": { azimuthDeg: 145, elevationDeg: 14 },
|
|
16
|
+
top: { azimuthDeg: 0, elevationDeg: 88 }
|
|
17
|
+
};
|
|
18
|
+
function angleOf(view) {
|
|
19
|
+
if (typeof view === "string")
|
|
20
|
+
return view in VIEW_WORDS ? { ...VIEW_WORDS[view] } : null;
|
|
21
|
+
if (view && typeof view === "object" && typeof view.azimuthDeg === "number") {
|
|
22
|
+
const { azimuthDeg, elevationDeg } = view;
|
|
23
|
+
return {
|
|
24
|
+
azimuthDeg,
|
|
25
|
+
...elevationDeg !== void 0 ? { elevationDeg } : {}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function angleBetween(from, to, t) {
|
|
31
|
+
const fromEl = from.elevationDeg ?? 12;
|
|
32
|
+
const toEl = to.elevationDeg ?? 12;
|
|
33
|
+
return {
|
|
34
|
+
azimuthDeg: from.azimuthDeg + (to.azimuthDeg - from.azimuthDeg) * t,
|
|
35
|
+
elevationDeg: fromEl + (toEl - fromEl) * t
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function directionOf(angle) {
|
|
39
|
+
const azimuth = angle.azimuthDeg * Math.PI / 180;
|
|
40
|
+
const elevation = (angle.elevationDeg ?? 12) * Math.PI / 180;
|
|
41
|
+
return [
|
|
42
|
+
Math.sin(azimuth) * Math.cos(elevation),
|
|
43
|
+
Math.sin(elevation),
|
|
44
|
+
Math.cos(azimuth) * Math.cos(elevation)
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
function framedDistance(radius, fov, aspect, margin) {
|
|
48
|
+
const vertical = fov * Math.PI / 180;
|
|
49
|
+
const horizontal = 2 * Math.atan(Math.tan(vertical / 2) * aspect);
|
|
50
|
+
return radius * (1 + margin) / Math.sin(Math.min(vertical, horizontal) / 2);
|
|
51
|
+
}
|
|
52
|
+
function radiusOf(bounds) {
|
|
53
|
+
return Math.hypot(
|
|
54
|
+
bounds.max[0] - bounds.min[0],
|
|
55
|
+
bounds.max[1] - bounds.min[1],
|
|
56
|
+
bounds.max[2] - bounds.min[2]
|
|
57
|
+
) / 2 || 0.5;
|
|
58
|
+
}
|
|
59
|
+
function centerOf(bounds) {
|
|
60
|
+
return [
|
|
61
|
+
(bounds.min[0] + bounds.max[0]) / 2,
|
|
62
|
+
(bounds.min[1] + bounds.max[1]) / 2,
|
|
63
|
+
(bounds.min[2] + bounds.max[2]) / 2
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
function framedCamera(bounds, view, aspect) {
|
|
67
|
+
const fov = view.fov ?? 30;
|
|
68
|
+
if (!(fov > 1 && fov < 120))
|
|
69
|
+
throw badView(view.id, `fov ${fov} is outside 1 to 120 degrees`);
|
|
70
|
+
const center = centerOf(bounds);
|
|
71
|
+
const radius = radiusOf(bounds);
|
|
72
|
+
const distance = framedDistance(radius, fov, aspect, view.margin ?? 0.12);
|
|
73
|
+
return cameraAt(
|
|
74
|
+
center,
|
|
75
|
+
radius,
|
|
76
|
+
directionOf(view),
|
|
77
|
+
distance,
|
|
78
|
+
fov,
|
|
79
|
+
aspect,
|
|
80
|
+
view.id
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
function cameraAt(center, radius, direction, distance, fov, aspect, name) {
|
|
84
|
+
const position = [
|
|
85
|
+
center[0] + direction[0] * distance,
|
|
86
|
+
center[1] + direction[1] * distance,
|
|
87
|
+
center[2] + direction[2] * distance
|
|
88
|
+
];
|
|
89
|
+
return {
|
|
90
|
+
type: "perspective",
|
|
91
|
+
fov,
|
|
92
|
+
aspect,
|
|
93
|
+
near: Math.max(1e-3, (distance - radius) / 10),
|
|
94
|
+
far: (distance + radius) * 10,
|
|
95
|
+
position,
|
|
96
|
+
quaternion: lookAt(position, center),
|
|
97
|
+
name
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function lookAt(position, target) {
|
|
101
|
+
let z = normalize([
|
|
102
|
+
position[0] - target[0],
|
|
103
|
+
position[1] - target[1],
|
|
104
|
+
position[2] - target[2]
|
|
105
|
+
]);
|
|
106
|
+
if (!z) z = [0, 0, 1];
|
|
107
|
+
const up = Math.abs(z[0]) < 1e-9 && Math.abs(z[2]) < 1e-9 ? [0, 0, -1] : [0, 1, 0];
|
|
108
|
+
const x = normalize(cross(up, z)) ?? [1, 0, 0];
|
|
109
|
+
const y = cross(z, x);
|
|
110
|
+
return quaternionOf(x, y, z);
|
|
111
|
+
}
|
|
112
|
+
function quaternionOf(x, y, z) {
|
|
113
|
+
const m00 = x[0], m10 = x[1], m20 = x[2];
|
|
114
|
+
const m01 = y[0], m11 = y[1], m21 = y[2];
|
|
115
|
+
const m02 = z[0], m12 = z[1], m22 = z[2];
|
|
116
|
+
const trace = m00 + m11 + m22;
|
|
117
|
+
if (trace > 0) {
|
|
118
|
+
const s2 = 0.5 / Math.sqrt(trace + 1);
|
|
119
|
+
return [(m21 - m12) * s2, (m02 - m20) * s2, (m10 - m01) * s2, 0.25 / s2];
|
|
120
|
+
}
|
|
121
|
+
if (m00 > m11 && m00 > m22) {
|
|
122
|
+
const s2 = 2 * Math.sqrt(1 + m00 - m11 - m22);
|
|
123
|
+
return [0.25 * s2, (m01 + m10) / s2, (m02 + m20) / s2, (m21 - m12) / s2];
|
|
124
|
+
}
|
|
125
|
+
if (m11 > m22) {
|
|
126
|
+
const s2 = 2 * Math.sqrt(1 + m11 - m00 - m22);
|
|
127
|
+
return [(m01 + m10) / s2, 0.25 * s2, (m12 + m21) / s2, (m02 - m20) / s2];
|
|
128
|
+
}
|
|
129
|
+
const s = 2 * Math.sqrt(1 + m22 - m00 - m11);
|
|
130
|
+
return [(m02 + m20) / s, (m12 + m21) / s, 0.25 * s, (m10 - m01) / s];
|
|
131
|
+
}
|
|
132
|
+
function cross(a, b) {
|
|
133
|
+
return [
|
|
134
|
+
a[1] * b[2] - a[2] * b[1],
|
|
135
|
+
a[2] * b[0] - a[0] * b[2],
|
|
136
|
+
a[0] * b[1] - a[1] * b[0]
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
function normalize(v) {
|
|
140
|
+
const length = Math.hypot(v[0], v[1], v[2]);
|
|
141
|
+
return length > 1e-12 ? [v[0] / length, v[1] / length, v[2] / length] : null;
|
|
142
|
+
}
|
|
143
|
+
function badView(id, what) {
|
|
144
|
+
return new Bakery3Error({
|
|
145
|
+
code: "InvalidRequest",
|
|
146
|
+
message: `Camera "${id}": ${what}.`,
|
|
147
|
+
docs: "https://bakery3.com/docs/catalog#angles"
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/node/renderBatch.ts
|
|
152
|
+
var DOCS = "https://bakery3.com/docs/catalog";
|
|
153
|
+
var PRESET_LIBRARY = "https://cdn.bakery3.com/presets";
|
|
154
|
+
var SKIPS_KEPT = 50;
|
|
155
|
+
var BREAKER_AFTER = 8;
|
|
156
|
+
var BREAKER_SHARE = 0.5;
|
|
157
|
+
var MAX_SECONDS = 30;
|
|
158
|
+
var MAX_FPS = 60;
|
|
159
|
+
async function recordBatch(host, batch, options, { dryRun = false, force } = {}) {
|
|
160
|
+
validateShots(options.shots ?? [], "the run");
|
|
161
|
+
const environments = new EnvironmentCache(host);
|
|
162
|
+
const tally = {
|
|
163
|
+
...emptyRecorded(),
|
|
164
|
+
skipped: 0,
|
|
165
|
+
skips: [],
|
|
166
|
+
estimated: 0,
|
|
167
|
+
maximum: 0
|
|
168
|
+
};
|
|
169
|
+
const seen = /* @__PURE__ */ new Set();
|
|
170
|
+
let attempted = 0;
|
|
171
|
+
const one = async (product) => {
|
|
172
|
+
let items;
|
|
173
|
+
try {
|
|
174
|
+
if (product.shots)
|
|
175
|
+
validateShots(product.shots, `product "${product.id}"`);
|
|
176
|
+
const imported = await host.importAsset({
|
|
177
|
+
url: product.url,
|
|
178
|
+
kind: "scene"
|
|
179
|
+
});
|
|
180
|
+
items = await itemsOf(product, imported, options, environments, force);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (!(error instanceof Bakery3Error) || error.code !== "ImportFailed" && error.code !== "ImportRefused")
|
|
183
|
+
throw error;
|
|
184
|
+
attempted++;
|
|
185
|
+
tally.skipped++;
|
|
186
|
+
if (tally.skips.length < SKIPS_KEPT)
|
|
187
|
+
tally.skips.push({ id: product.id, url: product.url, error });
|
|
188
|
+
if (attempted >= BREAKER_AFTER && tally.skipped / attempted > BREAKER_SHARE)
|
|
189
|
+
throw mostlyFailing(tally, attempted);
|
|
190
|
+
report(product.id);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
attempted++;
|
|
194
|
+
let outcomes;
|
|
195
|
+
if (dryRun) {
|
|
196
|
+
outcomes = await batch.quote(items);
|
|
197
|
+
} else {
|
|
198
|
+
for (const item of items) batch.add(item);
|
|
199
|
+
outcomes = await batch.flush();
|
|
200
|
+
}
|
|
201
|
+
tally.variants++;
|
|
202
|
+
tally.images += items.length;
|
|
203
|
+
for (const outcome of outcomes) {
|
|
204
|
+
if (outcome.error) {
|
|
205
|
+
tally.refused++;
|
|
206
|
+
if (tally.refusals.length < SKIPS_KEPT)
|
|
207
|
+
tally.refusals.push({ key: outcome.key, error: outcome.error });
|
|
208
|
+
} else if (outcome.adopted) tally.adopted++;
|
|
209
|
+
else {
|
|
210
|
+
tally.estimated += outcome.estimatedCost;
|
|
211
|
+
tally.maximum += outcome.maximumCost;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
report(product.id);
|
|
215
|
+
};
|
|
216
|
+
const report = (product) => options.onProgress?.({
|
|
217
|
+
...batch.progress(),
|
|
218
|
+
product,
|
|
219
|
+
products: tally.variants,
|
|
220
|
+
images: tally.images,
|
|
221
|
+
skipped: tally.skipped
|
|
222
|
+
});
|
|
223
|
+
const limit = Math.max(1, Math.min(16, Math.floor(options.concurrency ?? 4)));
|
|
224
|
+
const flying = /* @__PURE__ */ new Set();
|
|
225
|
+
let failure;
|
|
226
|
+
for await (const product of options.products) {
|
|
227
|
+
if (failure) break;
|
|
228
|
+
if (options.signal?.aborted) {
|
|
229
|
+
failure = options.signal.reason ?? abortError();
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
validateProduct(product, seen);
|
|
233
|
+
const task = one(product).catch((error) => {
|
|
234
|
+
failure ??= error;
|
|
235
|
+
}).finally(() => flying.delete(task));
|
|
236
|
+
flying.add(task);
|
|
237
|
+
if (flying.size >= limit) await Promise.race(flying);
|
|
238
|
+
}
|
|
239
|
+
await Promise.all(flying);
|
|
240
|
+
if (failure) throw failure;
|
|
241
|
+
tally.estimated = Math.round(tally.estimated * 1e4) / 1e4;
|
|
242
|
+
tally.maximum = Math.round(tally.maximum * 1e4) / 1e4;
|
|
243
|
+
return tally;
|
|
244
|
+
}
|
|
245
|
+
async function itemsOf(product, imported, run, environments, force) {
|
|
246
|
+
const facts = imported.facts;
|
|
247
|
+
if (!facts)
|
|
248
|
+
throw unreadable(product, "The import returned no facts about the model.");
|
|
249
|
+
const blocking = facts.issues.find((issue) => issue.severity === "error");
|
|
250
|
+
if (blocking) throw unreadable(product, blocking.message);
|
|
251
|
+
if (!facts.bounds)
|
|
252
|
+
throw unreadable(
|
|
253
|
+
product,
|
|
254
|
+
"The model has no geometry with bounds, so there is nowhere to point a camera."
|
|
255
|
+
);
|
|
256
|
+
const bounds = facts.bounds;
|
|
257
|
+
const runSettings = settingsOf(run);
|
|
258
|
+
const productSettings = settingsOf(product);
|
|
259
|
+
const shots = (product.shots ?? run.shots ?? []).map(
|
|
260
|
+
(shot) => resolveShot(shot, runSettings, productSettings, force)
|
|
261
|
+
);
|
|
262
|
+
if (!shots.length) {
|
|
263
|
+
const inRoom = layered("room", runSettings, productSettings, force) != null;
|
|
264
|
+
shots.push(
|
|
265
|
+
resolveShot(
|
|
266
|
+
inRoom ? "room" : "three-quarter",
|
|
267
|
+
runSettings,
|
|
268
|
+
productSettings,
|
|
269
|
+
force
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const counts = { info: 0, warning: 0, error: 0 };
|
|
274
|
+
for (const issue of facts.issues) counts[issue.severity]++;
|
|
275
|
+
const version = force?.version ?? run.version;
|
|
276
|
+
const items = [];
|
|
277
|
+
for (const shot of shots) {
|
|
278
|
+
const quality = shot.quality ?? "studio";
|
|
279
|
+
const edge = QUALITY[quality].defaultLongEdge;
|
|
280
|
+
const video = shot.kind === "video";
|
|
281
|
+
const output = {
|
|
282
|
+
width: shot.width ?? edge,
|
|
283
|
+
height: shot.height ?? edge,
|
|
284
|
+
format: shot.format ?? (video ? "mp4" : "png"),
|
|
285
|
+
...shot.imageQuality !== void 0 ? { quality: shot.imageQuality } : {}
|
|
286
|
+
};
|
|
287
|
+
if (video !== isClipFormat(output.format)) {
|
|
288
|
+
throw new Bakery3Error({
|
|
289
|
+
code: "InvalidRequest",
|
|
290
|
+
message: `Shot "${shot.id}" of ${product.id} is ${video ? "a video in a still" : "an image in a video"} format (${output.format}).`,
|
|
291
|
+
fix: "A video is mp4, webm or png-sequence; an image is png, jpeg or webp. Leave `format` out for the default.",
|
|
292
|
+
path: `${product.id}/${shot.id}`,
|
|
293
|
+
docs: `${DOCS}#shots`
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
const aspect = output.width / output.height;
|
|
297
|
+
const room = shot.room ? roomFor(shot.room, bounds) : void 0;
|
|
298
|
+
const background = isVideoFormat(output.format) && (shot.background ?? "transparent") === "transparent" ? CLIP_BACKDROP : shot.background;
|
|
299
|
+
const environment = room ? roomBackground() : await environments.get(shot.environment, background);
|
|
300
|
+
const color = {
|
|
301
|
+
outputColorSpace: "srgb",
|
|
302
|
+
toneMapping: "aces-filmic",
|
|
303
|
+
toneMappingExposure: room ? room.exposure : 1,
|
|
304
|
+
...shot.color
|
|
305
|
+
};
|
|
306
|
+
const lens = { fov: shot.fov, margin: shot.margin };
|
|
307
|
+
let camera;
|
|
308
|
+
let animation;
|
|
309
|
+
if (video) {
|
|
310
|
+
const from = angleFor(shot.from, room, product, shot.id);
|
|
311
|
+
const to = angleFor(shot.to, room, product, shot.id);
|
|
312
|
+
const fps = Math.min(MAX_FPS, Math.max(1, Math.round(shot.fps ?? 30)));
|
|
313
|
+
const seconds = Math.min(MAX_SECONDS, shot.seconds ?? 4);
|
|
314
|
+
const frameCount = Math.max(2, Math.round(seconds * fps));
|
|
315
|
+
const positions = [];
|
|
316
|
+
const quaternions = [];
|
|
317
|
+
let first;
|
|
318
|
+
for (let frame = 0; frame < frameCount; frame++) {
|
|
319
|
+
const at = angleBetween(from, to, frame / (frameCount - 1));
|
|
320
|
+
const spec = cameraForAngle(at, bounds, aspect, lens, room, shot.id);
|
|
321
|
+
first ??= spec;
|
|
322
|
+
positions.push(...spec.position);
|
|
323
|
+
quaternions.push(...spec.quaternion);
|
|
324
|
+
}
|
|
325
|
+
camera = first;
|
|
326
|
+
animation = {
|
|
327
|
+
fps,
|
|
328
|
+
frameCount,
|
|
329
|
+
channels: [
|
|
330
|
+
{
|
|
331
|
+
target: { type: "camera" },
|
|
332
|
+
property: "position",
|
|
333
|
+
size: 3,
|
|
334
|
+
values: positions
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
target: { type: "camera" },
|
|
338
|
+
property: "quaternion",
|
|
339
|
+
size: 4,
|
|
340
|
+
values: quaternions
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
};
|
|
344
|
+
} else if (shot.camera) {
|
|
345
|
+
camera = ownCamera(shot.camera, facts, aspect, product, shot.id);
|
|
346
|
+
} else {
|
|
347
|
+
camera = cameraForAngle(
|
|
348
|
+
angleFor(
|
|
349
|
+
shot.view ?? (room ? "room" : "three-quarter"),
|
|
350
|
+
room,
|
|
351
|
+
product,
|
|
352
|
+
shot.id
|
|
353
|
+
),
|
|
354
|
+
bounds,
|
|
355
|
+
aspect,
|
|
356
|
+
lens,
|
|
357
|
+
room,
|
|
358
|
+
shot.id
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const manifest = {
|
|
362
|
+
protocolVersion: protocolVersionFor({
|
|
363
|
+
...room ? { room: room.spec } : {},
|
|
364
|
+
...animation ? { animation } : {}
|
|
365
|
+
}),
|
|
366
|
+
sdkVersion: SDK_VERSION,
|
|
367
|
+
sceneAsset: imported.hash,
|
|
368
|
+
camera,
|
|
369
|
+
environment,
|
|
370
|
+
color,
|
|
371
|
+
...room ? { room: room.spec } : {},
|
|
372
|
+
...animation ? { animation } : {},
|
|
373
|
+
compatibility: { score: 1, issues: facts.issues, counts },
|
|
374
|
+
stats: {
|
|
375
|
+
triangles: facts.triangles + (room?.triangles ?? 0),
|
|
376
|
+
// A room is priced by how enclosed and how full it is, measured when
|
|
377
|
+
// it was published; the product adds one object to it. Without a room
|
|
378
|
+
// the shot is an open set, which needs saying nothing.
|
|
379
|
+
...room?.complexity ? {
|
|
380
|
+
enclosure: room.complexity.enclosure,
|
|
381
|
+
objects: room.complexity.objects + 1
|
|
382
|
+
} : {}
|
|
383
|
+
},
|
|
384
|
+
source: { renderer: "catalog" }
|
|
385
|
+
};
|
|
386
|
+
const key = `${product.id}/${shot.id}${version ? `@${version}` : ""}`;
|
|
387
|
+
if (key.length > 200)
|
|
388
|
+
throw unreadable(
|
|
389
|
+
product,
|
|
390
|
+
`The key "${key.slice(0, 40)}\u2026" is over 200 characters. Use shorter ids.`
|
|
391
|
+
);
|
|
392
|
+
items.push({
|
|
393
|
+
key,
|
|
394
|
+
spec: {
|
|
395
|
+
manifest,
|
|
396
|
+
quality,
|
|
397
|
+
output,
|
|
398
|
+
...shot.advanced ? { advanced: shot.advanced } : {},
|
|
399
|
+
...video ? { preview: false } : {}
|
|
400
|
+
},
|
|
401
|
+
metadata: {
|
|
402
|
+
...product.metadata,
|
|
403
|
+
variant: product.id,
|
|
404
|
+
shot: shot.id,
|
|
405
|
+
kind: shot.kind
|
|
406
|
+
},
|
|
407
|
+
...run.maxCostPerImage !== void 0 ? { maxCost: run.maxCostPerImage } : {}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return items;
|
|
411
|
+
}
|
|
412
|
+
var SETTING_KEYS = [
|
|
413
|
+
"room",
|
|
414
|
+
"environment",
|
|
415
|
+
"background",
|
|
416
|
+
"quality",
|
|
417
|
+
"width",
|
|
418
|
+
"height",
|
|
419
|
+
"format",
|
|
420
|
+
"imageQuality",
|
|
421
|
+
"fov",
|
|
422
|
+
"margin",
|
|
423
|
+
"fps",
|
|
424
|
+
"advanced",
|
|
425
|
+
"color"
|
|
426
|
+
];
|
|
427
|
+
function settingsOf(source) {
|
|
428
|
+
const out = {};
|
|
429
|
+
for (const key of SETTING_KEYS)
|
|
430
|
+
if (source[key] !== void 0)
|
|
431
|
+
out[key] = source[key];
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
function layered(key, ...layers) {
|
|
435
|
+
for (let i = layers.length - 1; i >= 0; i--) {
|
|
436
|
+
const value = layers[i]?.[key];
|
|
437
|
+
if (value !== void 0) return value;
|
|
438
|
+
}
|
|
439
|
+
return void 0;
|
|
440
|
+
}
|
|
441
|
+
function shotOf(shot) {
|
|
442
|
+
if (typeof shot === "string") return { view: shot };
|
|
443
|
+
if (shot && typeof shot === "object" && "azimuthDeg" in shot && !("view" in shot) && !("kind" in shot) && !("camera" in shot)) {
|
|
444
|
+
const { azimuthDeg, elevationDeg, ...rest } = shot;
|
|
445
|
+
return {
|
|
446
|
+
...rest,
|
|
447
|
+
view: {
|
|
448
|
+
azimuthDeg,
|
|
449
|
+
...elevationDeg !== void 0 ? { elevationDeg } : {}
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
return shot;
|
|
454
|
+
}
|
|
455
|
+
function resolveShot(shot, run, product, force) {
|
|
456
|
+
const own = shotOf(shot);
|
|
457
|
+
const settings = {};
|
|
458
|
+
for (const key of SETTING_KEYS) {
|
|
459
|
+
const value = layered(key, run, product, settingsOf(own), force);
|
|
460
|
+
if (value !== void 0) settings[key] = value;
|
|
461
|
+
}
|
|
462
|
+
if (own.kind === "video") {
|
|
463
|
+
return {
|
|
464
|
+
...settings,
|
|
465
|
+
kind: "video",
|
|
466
|
+
id: own.id ?? `orbit-${viewName(own.from)}-${viewName(own.to)}`,
|
|
467
|
+
from: own.from,
|
|
468
|
+
to: own.to,
|
|
469
|
+
seconds: force?.seconds !== void 0 ? Math.min(force.seconds, own.seconds ?? 4) : own.seconds
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
const image = own;
|
|
473
|
+
return {
|
|
474
|
+
...settings,
|
|
475
|
+
kind: "image",
|
|
476
|
+
id: image.id ?? (image.camera ? image.camera : viewName(image.view ?? (settings.room ? "room" : "three-quarter"))),
|
|
477
|
+
...image.view !== void 0 ? { view: image.view } : {},
|
|
478
|
+
...image.camera !== void 0 ? { camera: image.camera } : {}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function viewName(view) {
|
|
482
|
+
if (typeof view === "string") return view;
|
|
483
|
+
const el = view.elevationDeg;
|
|
484
|
+
return `az${Math.round(view.azimuthDeg)}${el !== void 0 ? `-el${Math.round(el)}` : ""}`.replace(
|
|
485
|
+
"--",
|
|
486
|
+
"-neg"
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
function angleFor(view, room, product, shot) {
|
|
490
|
+
if (view === "room") {
|
|
491
|
+
if (!room) {
|
|
492
|
+
throw new Bakery3Error({
|
|
493
|
+
code: "InvalidRequest",
|
|
494
|
+
message: `Shot "${shot}" of ${product.id} looks from 'room', but has no room.`,
|
|
495
|
+
fix: "Set `room: { preset: \u2026 }` on the run, the product or the shot, or use a view such as 'three-quarter'.",
|
|
496
|
+
path: `${product.id}/${shot}`,
|
|
497
|
+
docs: `${DOCS}#angles`
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
return room.published;
|
|
501
|
+
}
|
|
502
|
+
const angle = angleOf(view);
|
|
503
|
+
if (angle) return angle;
|
|
504
|
+
throw new Bakery3Error({
|
|
505
|
+
code: "InvalidRequest",
|
|
506
|
+
message: `Shot "${shot}" of ${product.id}: "${typeof view === "string" ? view : JSON.stringify(view)}" is not a view.`,
|
|
507
|
+
fix: `A view is one of ${Object.keys(VIEW_WORDS).join(", ")}, room, or \`{ azimuthDeg, elevationDeg? }\`. For a camera inside the model, use \`camera: '<node name>'\`.`,
|
|
508
|
+
path: `${product.id}/${shot}`,
|
|
509
|
+
docs: `${DOCS}#angles`
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function cameraForAngle(angle, bounds, aspect, lens, room, name) {
|
|
513
|
+
if (room) return room.camera(angle, aspect, lens, name);
|
|
514
|
+
return framedCamera(
|
|
515
|
+
bounds,
|
|
516
|
+
{
|
|
517
|
+
id: name,
|
|
518
|
+
...angle,
|
|
519
|
+
...lens.fov !== void 0 ? { fov: lens.fov } : {},
|
|
520
|
+
...lens.margin !== void 0 ? { margin: lens.margin } : {}
|
|
521
|
+
},
|
|
522
|
+
aspect
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
function ownCamera(name, facts, aspect, product, shot) {
|
|
526
|
+
const own = facts.cameras.find((camera) => camera.name === name);
|
|
527
|
+
if (own)
|
|
528
|
+
return {
|
|
529
|
+
...own.camera,
|
|
530
|
+
...own.camera.type === "perspective" ? { aspect } : {},
|
|
531
|
+
name: shot
|
|
532
|
+
};
|
|
533
|
+
const names = facts.cameras.map((camera) => camera.name);
|
|
534
|
+
throw new Bakery3Error({
|
|
535
|
+
code: "ImportFailed",
|
|
536
|
+
message: `Shot "${shot}": ${product.id}'s model has no camera named "${name}".`,
|
|
537
|
+
fix: names.length ? `Its cameras are: ${names.slice(0, 8).join(", ")}.` : "It carries no cameras. Use a view instead: an angle is framed from the model itself.",
|
|
538
|
+
path: `${product.id}/${shot}`,
|
|
539
|
+
docs: `${DOCS}#angles`
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
var DEFAULT_ENVIRONMENT = {
|
|
543
|
+
preset: "studio_small_03",
|
|
544
|
+
intensity: 0.6
|
|
545
|
+
};
|
|
546
|
+
var CLIP_BACKDROP = "#f4f4f5";
|
|
547
|
+
var EnvironmentCache = class {
|
|
548
|
+
constructor(host) {
|
|
549
|
+
this.host = host;
|
|
550
|
+
}
|
|
551
|
+
host;
|
|
552
|
+
hashes = /* @__PURE__ */ new Map();
|
|
553
|
+
async get(chosen, background) {
|
|
554
|
+
const environment = chosen ?? DEFAULT_ENVIRONMENT;
|
|
555
|
+
if (environment.preset && environment.url) {
|
|
556
|
+
throw new Bakery3Error({
|
|
557
|
+
code: "InvalidRequest",
|
|
558
|
+
message: "environment takes a `preset` or a `url`, not both.",
|
|
559
|
+
docs: `${DOCS}#light`
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
let hdri;
|
|
563
|
+
if (environment.url) {
|
|
564
|
+
const url = environment.url;
|
|
565
|
+
let pending = this.hashes.get(url);
|
|
566
|
+
if (!pending) {
|
|
567
|
+
pending = this.host.importAsset({ url, kind: "hdri" }).then((imported) => imported.hash);
|
|
568
|
+
this.hashes.set(url, pending);
|
|
569
|
+
pending.catch(() => this.hashes.delete(url));
|
|
570
|
+
}
|
|
571
|
+
hdri = await pending;
|
|
572
|
+
} else {
|
|
573
|
+
const name = environment.preset ?? "studio_small_03";
|
|
574
|
+
if (!(name in HDRI_PRESETS)) {
|
|
575
|
+
throw new Bakery3Error({
|
|
576
|
+
code: "InvalidRequest",
|
|
577
|
+
message: `There is no environment preset "${name}".`,
|
|
578
|
+
fix: `The presets are: ${Object.keys(HDRI_PRESETS).join(", ")}.`,
|
|
579
|
+
docs: `${DOCS}#light`
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
hdri = `${PRESET_LIBRARY}/v${PRESETS_VERSION}/hdri/${name}/render.exr`;
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
hdri,
|
|
586
|
+
...environment.intensity !== void 0 ? { intensity: environment.intensity } : {},
|
|
587
|
+
...environment.rotationDeg ? {
|
|
588
|
+
rotation: [0, environment.rotationDeg * Math.PI / 180, 0]
|
|
589
|
+
} : {},
|
|
590
|
+
background: backgroundOf(background)
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
function backgroundOf(background) {
|
|
595
|
+
const chosen = background ?? "transparent";
|
|
596
|
+
if (chosen === "transparent") return { type: "transparent" };
|
|
597
|
+
if (chosen === "environment") return { type: "environment" };
|
|
598
|
+
if (!/^#[0-9a-f]{6}$/i.test(chosen)) {
|
|
599
|
+
throw new Bakery3Error({
|
|
600
|
+
code: "InvalidRequest",
|
|
601
|
+
message: `background "${chosen}" is not 'transparent', 'environment' or a #rrggbb color.`,
|
|
602
|
+
docs: `${DOCS}#light`
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
return { type: "color", color: chosen };
|
|
606
|
+
}
|
|
607
|
+
function roomBackground() {
|
|
608
|
+
return { background: { type: "transparent" } };
|
|
609
|
+
}
|
|
610
|
+
var STAND_RULES = {
|
|
611
|
+
fit: 0.85,
|
|
612
|
+
tallFrom: 0.6,
|
|
613
|
+
smallHeight: 0.6,
|
|
614
|
+
plinthHeight: 1
|
|
615
|
+
};
|
|
616
|
+
var PLINTH = "pedestal_plinth";
|
|
617
|
+
var ROOM_MARGIN = 0.9;
|
|
618
|
+
var WALL_CLEARANCE = 0.3;
|
|
619
|
+
var FIT_MARGIN = 0.12;
|
|
620
|
+
var MAX_ROOM_FOV = 100;
|
|
621
|
+
var ROOM_MIN_ELEVATION_DEG = 0;
|
|
622
|
+
function fovToFit(radius, distance, aspect) {
|
|
623
|
+
const half = Math.asin(Math.min(1, radius / distance));
|
|
624
|
+
const vertical = aspect >= 1 ? half : Math.atan(Math.tan(half) / aspect);
|
|
625
|
+
return 2 * vertical * 180 / Math.PI;
|
|
626
|
+
}
|
|
627
|
+
function standFor(room, info, bounds) {
|
|
628
|
+
const footprint = Math.max(
|
|
629
|
+
bounds.max[0] - bounds.min[0],
|
|
630
|
+
bounds.max[2] - bounds.min[2]
|
|
631
|
+
);
|
|
632
|
+
const height = bounds.max[1] - bounds.min[1];
|
|
633
|
+
const fits = (size) => {
|
|
634
|
+
if (!size) return false;
|
|
635
|
+
if (footprint > STAND_RULES.fit * Math.min(size.x, size.z)) return false;
|
|
636
|
+
return height <= (size.y >= STAND_RULES.tallFrom ? STAND_RULES.smallHeight : STAND_RULES.plinthHeight);
|
|
637
|
+
};
|
|
638
|
+
const choice = room.stand ?? "auto";
|
|
639
|
+
if (choice === "own" || choice === "auto" && fits(info.stand.size))
|
|
640
|
+
return { top: info.stand.top };
|
|
641
|
+
if (choice === "auto" && fits(PEDESTAL_PRESETS[PLINTH].size))
|
|
642
|
+
return { pedestal: { type: PLINTH }, top: PEDESTAL_PRESETS[PLINTH].size.y };
|
|
643
|
+
return { pedestal: null, top: 0 };
|
|
644
|
+
}
|
|
645
|
+
function roomFor(room, bounds) {
|
|
646
|
+
const info = ROOM_PRESETS[room.preset];
|
|
647
|
+
if (!info) {
|
|
648
|
+
throw new Bakery3Error({
|
|
649
|
+
code: "InvalidRequest",
|
|
650
|
+
message: `There is no room preset "${room.preset}".`,
|
|
651
|
+
fix: `The rooms are: ${Object.keys(ROOM_PRESETS).join(", ")}.`,
|
|
652
|
+
docs: `${DOCS}#light`
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
const lighting = room.mode ?? info.lighting;
|
|
656
|
+
const standing = standFor(room, info, bounds);
|
|
657
|
+
const stand = [info.stand.x, standing.top, info.stand.z];
|
|
658
|
+
const base = [
|
|
659
|
+
(bounds.min[0] + bounds.max[0]) / 2,
|
|
660
|
+
bounds.min[1],
|
|
661
|
+
(bounds.min[2] + bounds.max[2]) / 2
|
|
662
|
+
];
|
|
663
|
+
const size = [
|
|
664
|
+
bounds.max[0] - bounds.min[0],
|
|
665
|
+
bounds.max[1] - bounds.min[1],
|
|
666
|
+
bounds.max[2] - bounds.min[2]
|
|
667
|
+
];
|
|
668
|
+
const radius = radiusOf(bounds);
|
|
669
|
+
const line = [
|
|
670
|
+
info.camera.position[0] - info.camera.target[0],
|
|
671
|
+
info.camera.position[1] - info.camera.target[1],
|
|
672
|
+
info.camera.position[2] - info.camera.target[2]
|
|
673
|
+
];
|
|
674
|
+
const designed = Math.hypot(line[0], line[1], line[2]);
|
|
675
|
+
const publishedAzimuth = Math.atan2(line[0], line[2]);
|
|
676
|
+
const viewFrom = (room.viewFromDeg ?? 30) * Math.PI / 180;
|
|
677
|
+
const theta = viewFrom - publishedAzimuth;
|
|
678
|
+
const c = Math.cos(theta);
|
|
679
|
+
const s = Math.sin(theta);
|
|
680
|
+
const place = ([x, y, z]) => {
|
|
681
|
+
const dx = x - stand[0];
|
|
682
|
+
const dz = z - stand[2];
|
|
683
|
+
return [
|
|
684
|
+
base[0] + dx * c + dz * s,
|
|
685
|
+
base[1] + (y - stand[1]),
|
|
686
|
+
base[2] - dx * s + dz * c
|
|
687
|
+
];
|
|
688
|
+
};
|
|
689
|
+
const origin = place([0, 0, 0]);
|
|
690
|
+
const matrix = [
|
|
691
|
+
c,
|
|
692
|
+
0,
|
|
693
|
+
-s,
|
|
694
|
+
0,
|
|
695
|
+
0,
|
|
696
|
+
1,
|
|
697
|
+
0,
|
|
698
|
+
0,
|
|
699
|
+
s,
|
|
700
|
+
0,
|
|
701
|
+
c,
|
|
702
|
+
0,
|
|
703
|
+
origin[0],
|
|
704
|
+
origin[1],
|
|
705
|
+
origin[2],
|
|
706
|
+
1
|
|
707
|
+
];
|
|
708
|
+
const spec = {
|
|
709
|
+
name: room.preset,
|
|
710
|
+
build: info.build,
|
|
711
|
+
lighting,
|
|
712
|
+
...standing.pedestal !== void 0 ? { pedestal: standing.pedestal } : {},
|
|
713
|
+
matrix
|
|
714
|
+
};
|
|
715
|
+
const published = {
|
|
716
|
+
azimuthDeg: viewFrom * 180 / Math.PI,
|
|
717
|
+
elevationDeg: Math.max(
|
|
718
|
+
ROOM_MIN_ELEVATION_DEG,
|
|
719
|
+
Math.atan2(line[1], Math.hypot(line[0], line[2])) * 180 / Math.PI
|
|
720
|
+
)
|
|
721
|
+
};
|
|
722
|
+
const lensFov = (aspect) => 2 * Math.atan((aspect >= 1 ? 36 / aspect : 36) / (2 * info.camera.lensMm)) * 180 / Math.PI;
|
|
723
|
+
return {
|
|
724
|
+
spec,
|
|
725
|
+
triangles: info.preview.triangles,
|
|
726
|
+
complexity: info.complexity,
|
|
727
|
+
exposure: info.exposure[lighting],
|
|
728
|
+
published,
|
|
729
|
+
/**
|
|
730
|
+
* A camera in the room from any angle: framed as in the open, from as
|
|
731
|
+
* far as the published camera stood when that is the view, and never
|
|
732
|
+
* behind a wall, the floor or the ceiling.
|
|
733
|
+
*/
|
|
734
|
+
camera(angle, aspect, lens, name) {
|
|
735
|
+
const fov = lens.fov ?? lensFov(aspect);
|
|
736
|
+
const center = centerOf(bounds);
|
|
737
|
+
const direction = directionOf(angle);
|
|
738
|
+
const framed = framedDistance(
|
|
739
|
+
radius,
|
|
740
|
+
fov,
|
|
741
|
+
aspect,
|
|
742
|
+
lens.margin ?? ROOM_MARGIN
|
|
743
|
+
);
|
|
744
|
+
const isPublished = Math.abs(angle.azimuthDeg - published.azimuthDeg) < 1e-6 && Math.abs((angle.elevationDeg ?? 12) - published.elevationDeg) < 1e-6;
|
|
745
|
+
const rx = direction[0] * c - direction[2] * s;
|
|
746
|
+
const rz = direction[0] * s + direction[2] * c;
|
|
747
|
+
const ry = direction[1];
|
|
748
|
+
const from = [stand[0], stand[1] + size[1] / 2, stand[2]];
|
|
749
|
+
const half = [
|
|
750
|
+
info.size.width / 2 - WALL_CLEARANCE,
|
|
751
|
+
info.size.depth / 2 - WALL_CLEARANCE
|
|
752
|
+
];
|
|
753
|
+
let reach = Infinity;
|
|
754
|
+
if (rx !== 0)
|
|
755
|
+
reach = Math.min(
|
|
756
|
+
reach,
|
|
757
|
+
((rx > 0 ? half[0] : -half[0]) - from[0]) / rx
|
|
758
|
+
);
|
|
759
|
+
if (rz !== 0)
|
|
760
|
+
reach = Math.min(
|
|
761
|
+
reach,
|
|
762
|
+
((rz > 0 ? half[1] : -half[1]) - from[2]) / rz
|
|
763
|
+
);
|
|
764
|
+
if (ry > 0)
|
|
765
|
+
reach = Math.min(
|
|
766
|
+
reach,
|
|
767
|
+
(info.size.height - WALL_CLEARANCE - from[1]) / ry
|
|
768
|
+
);
|
|
769
|
+
if (ry < 0) reach = Math.min(reach, (WALL_CLEARANCE - from[1]) / ry);
|
|
770
|
+
const wanted = Math.max(isPublished ? designed : 0, framed);
|
|
771
|
+
const distance = Math.max(radius * 1.2, Math.min(wanted, reach));
|
|
772
|
+
const lensFit = distance < framed ? fovToFit(radius * (1 + FIT_MARGIN), distance, aspect) : fov;
|
|
773
|
+
return cameraAt(
|
|
774
|
+
center,
|
|
775
|
+
radius,
|
|
776
|
+
direction,
|
|
777
|
+
distance,
|
|
778
|
+
Math.min(MAX_ROOM_FOV, Math.max(fov, lensFit)),
|
|
779
|
+
aspect,
|
|
780
|
+
name
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function validateShots(shots, where) {
|
|
786
|
+
const ids = /* @__PURE__ */ new Set();
|
|
787
|
+
shots.forEach((shot, index) => {
|
|
788
|
+
if (!shot || typeof shot !== "string" && typeof shot !== "object")
|
|
789
|
+
throw badShot(where, index, "is not a shot");
|
|
790
|
+
const own = shotOf(shot);
|
|
791
|
+
if (own.kind === "video") {
|
|
792
|
+
if (angleOf(own.from) === null && own.from !== "room")
|
|
793
|
+
throw badShot(where, index, "`from` is not a view");
|
|
794
|
+
if (angleOf(own.to) === null && own.to !== "room")
|
|
795
|
+
throw badShot(where, index, "`to` is not a view");
|
|
796
|
+
if (own.seconds !== void 0 && !(own.seconds > 0 && own.seconds <= MAX_SECONDS))
|
|
797
|
+
throw badShot(
|
|
798
|
+
where,
|
|
799
|
+
index,
|
|
800
|
+
`\`seconds\` is outside 0 to ${MAX_SECONDS}`
|
|
801
|
+
);
|
|
802
|
+
} else if (own.kind !== void 0 && own.kind !== "image") {
|
|
803
|
+
throw badShot(
|
|
804
|
+
where,
|
|
805
|
+
index,
|
|
806
|
+
`kind "${String(own.kind)}" is not 'image' or 'video'`
|
|
807
|
+
);
|
|
808
|
+
} else if (own.view !== void 0 && own.camera !== void 0) {
|
|
809
|
+
throw badShot(where, index, "has both a `view` and a `camera`");
|
|
810
|
+
}
|
|
811
|
+
const id = own.id ?? (own.kind === "video" ? `orbit-${viewName(own.from)}-${viewName(own.to)}` : own.camera ?? (own.view !== void 0 ? viewName(own.view) : void 0));
|
|
812
|
+
if (id !== void 0) {
|
|
813
|
+
if (!/^[A-Za-z0-9_.-]{1,60}$/.test(id))
|
|
814
|
+
throw badShot(
|
|
815
|
+
where,
|
|
816
|
+
index,
|
|
817
|
+
`id "${id}" is not 1 to 60 letters, digits, dots, dashes or underscores`
|
|
818
|
+
);
|
|
819
|
+
if (ids.has(id)) throw badShot(where, index, `id "${id}" is used twice`);
|
|
820
|
+
ids.add(id);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
function badShot(where, index, what) {
|
|
825
|
+
return new Bakery3Error({
|
|
826
|
+
code: "InvalidRequest",
|
|
827
|
+
message: `Shot ${index + 1} of ${where} ${what}.`,
|
|
828
|
+
why: 'A shot is a word, `{ view }`, or `{ kind: "video", from, to }`, plus settings, and its id ends the picture\u2019s key.',
|
|
829
|
+
docs: `${DOCS}#shots`
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
function validateProduct(product, seen) {
|
|
833
|
+
if (!product || typeof product.id !== "string" || !product.id || typeof product.url !== "string" || !product.url) {
|
|
834
|
+
throw new Bakery3Error({
|
|
835
|
+
code: "InvalidRequest",
|
|
836
|
+
message: "Every product needs an `id` and a `url`.",
|
|
837
|
+
why: "The id starts every shot\u2019s key: it is what a re-run adopts by and what a failure is reported under.",
|
|
838
|
+
docs: DOCS
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
if (seen.has(product.id)) {
|
|
842
|
+
throw new Bakery3Error({
|
|
843
|
+
code: "InvalidRequest",
|
|
844
|
+
message: `Two products are called "${product.id}".`,
|
|
845
|
+
why: "The second would be adopted as the first: same key, same picture.",
|
|
846
|
+
fix: "Make the ids unique.",
|
|
847
|
+
path: product.id,
|
|
848
|
+
docs: DOCS
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
seen.add(product.id);
|
|
852
|
+
}
|
|
853
|
+
function unreadable(product, message) {
|
|
854
|
+
return new Bakery3Error({
|
|
855
|
+
code: "ImportFailed",
|
|
856
|
+
message,
|
|
857
|
+
path: product.id,
|
|
858
|
+
docs: `${DOCS}#what-a-model-can-use`
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
function mostlyFailing(tally, attempted) {
|
|
862
|
+
const first = tally.skips[0];
|
|
863
|
+
return new Bakery3Error({
|
|
864
|
+
code: "ImportFailed",
|
|
865
|
+
message: `${tally.skipped} of the first ${attempted} models could not be imported. The first: ${first.id}: ${first.error.message}`,
|
|
866
|
+
why: "One bad model is skipped and listed. Most of them failing is something wrong with all of them: a base URL, a bucket that is not public, a format.",
|
|
867
|
+
fix: "Open one of the URLs in a private browser window. It has to download a .glb there. What was recorded stays admitted; call again with the same key to go on.",
|
|
868
|
+
docs: `${DOCS}#importing-by-url`
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
function abortError() {
|
|
872
|
+
const error = new Error("The recording was aborted.");
|
|
873
|
+
error.name = "AbortError";
|
|
874
|
+
return error;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/node/webhook.ts
|
|
878
|
+
var TOLERANCE_MS = 5 * 60 * 1e3;
|
|
879
|
+
async function verifyWebhook(input) {
|
|
880
|
+
const { body, signature, secret } = input;
|
|
881
|
+
const now = input.now ?? Date.now();
|
|
882
|
+
if (typeof secret !== "string" || !secret) {
|
|
883
|
+
throw new Bakery3Error({
|
|
884
|
+
code: "InvalidRequest",
|
|
885
|
+
message: "verifyWebhook() was given no secret.",
|
|
886
|
+
fix: "Pass the signing secret from your webhook settings, from a server-only environment variable: secret: process.env.BAKERY3_WEBHOOK_SECRET!",
|
|
887
|
+
docs: "https://bakery3.com/docs/webhooks"
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
if (typeof signature !== "string" || !signature) {
|
|
891
|
+
throw invalid(
|
|
892
|
+
'The request has no bakery3-signature header. Pass request.headers.get("bakery3-signature").'
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
const parts = Object.fromEntries(
|
|
896
|
+
signature.split(",").map((piece) => {
|
|
897
|
+
const index = piece.indexOf("=");
|
|
898
|
+
return [piece.slice(0, index).trim(), piece.slice(index + 1).trim()];
|
|
899
|
+
})
|
|
900
|
+
);
|
|
901
|
+
const timestamp = Number(parts.t);
|
|
902
|
+
const provided = parts.v1;
|
|
903
|
+
if (!timestamp || !provided) {
|
|
904
|
+
throw invalid('Signature header is malformed. Expected "t=\u2026,v1=\u2026".');
|
|
905
|
+
}
|
|
906
|
+
if (Math.abs(now - timestamp) > TOLERANCE_MS) {
|
|
907
|
+
throw invalid("Signature timestamp is outside the five-minute tolerance.");
|
|
908
|
+
}
|
|
909
|
+
const key = await crypto.subtle.importKey(
|
|
910
|
+
"raw",
|
|
911
|
+
new TextEncoder().encode(secret),
|
|
912
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
913
|
+
false,
|
|
914
|
+
["sign"]
|
|
915
|
+
);
|
|
916
|
+
const mac = await crypto.subtle.sign(
|
|
917
|
+
"HMAC",
|
|
918
|
+
key,
|
|
919
|
+
new TextEncoder().encode(`${timestamp}.${body}`)
|
|
920
|
+
);
|
|
921
|
+
if (!timingSafeEqual(toHex(new Uint8Array(mac)), provided)) {
|
|
922
|
+
throw invalid("Signature does not match the body.");
|
|
923
|
+
}
|
|
924
|
+
return JSON.parse(body);
|
|
925
|
+
}
|
|
926
|
+
function invalid(message) {
|
|
927
|
+
return new Bakery3Error({
|
|
928
|
+
code: "Unauthorized",
|
|
929
|
+
message,
|
|
930
|
+
why: "The request did not carry a valid Bakery3 signature, so it may not have come from us.",
|
|
931
|
+
fix: "Pass the raw request body (not a re-serialized object) and the secret from your webhook settings.",
|
|
932
|
+
docs: "https://bakery3.com/docs/webhooks"
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
function timingSafeEqual(a, b) {
|
|
936
|
+
if (a.length !== b.length) return false;
|
|
937
|
+
let diff = 0;
|
|
938
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
939
|
+
return diff === 0;
|
|
940
|
+
}
|
|
941
|
+
function toHex(bytes) {
|
|
942
|
+
let out = "";
|
|
943
|
+
for (const byte of bytes) out += byte.toString(16).padStart(2, "0");
|
|
944
|
+
return out;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/node/index.ts
|
|
948
|
+
var Bakery3Server = class {
|
|
949
|
+
tokens;
|
|
950
|
+
renders;
|
|
951
|
+
batches;
|
|
952
|
+
assets;
|
|
953
|
+
secretKey;
|
|
954
|
+
options;
|
|
955
|
+
transport;
|
|
956
|
+
/**
|
|
957
|
+
* @param secretKey Your `bk_sk_` key, as a string, from a server-only
|
|
958
|
+
* environment variable. Not the browser SDK's `{ apiKey }` object.
|
|
959
|
+
*
|
|
960
|
+
* The key is checked on the first request, not here. A route module that
|
|
961
|
+
* builds its client at the top level is evaluated by `next build`, often
|
|
962
|
+
* where the server's secrets are not set, and a constructor that threw
|
|
963
|
+
* there would fail the build over a key the build never uses.
|
|
964
|
+
*/
|
|
965
|
+
constructor(secretKey, options = {}) {
|
|
966
|
+
if (secretKey != null && typeof secretKey !== "string") {
|
|
967
|
+
throw new Bakery3Error({
|
|
968
|
+
code: "InvalidRequest",
|
|
969
|
+
message: "new Bakery3Server() takes the secret key as a string, not an options object.",
|
|
970
|
+
fix: "new Bakery3Server(process.env.BAKERY3_SECRET_KEY!, options)",
|
|
971
|
+
docs: "https://bakery3.com/docs/auth"
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
this.secretKey = secretKey || void 0;
|
|
975
|
+
this.options = options;
|
|
976
|
+
if (this.secretKey) warnIfKeyIsExposed(this.secretKey, { source: "node" });
|
|
977
|
+
const client = () => this.client();
|
|
978
|
+
const request = async (method, path, body) => client().request(method, path, body);
|
|
979
|
+
this.tokens = new TokensApi(request);
|
|
980
|
+
this.renders = new RendersApi(request, client);
|
|
981
|
+
this.batches = new BatchesApi(client);
|
|
982
|
+
this.assets = new AssetsApi(client);
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* The same transport the browser SDK uses (one error shape, the same
|
|
986
|
+
* 429/503 backoff) with the secret key as the bearer. It has nothing to
|
|
987
|
+
* re-mint, so a 401 here is a 401. Made on first use, after the key is
|
|
988
|
+
* checked.
|
|
989
|
+
*/
|
|
990
|
+
client() {
|
|
991
|
+
if (this.transport) return this.transport;
|
|
992
|
+
const secretKey = this.secretKey;
|
|
993
|
+
if (!secretKey) {
|
|
994
|
+
throw new Bakery3Error({
|
|
995
|
+
code: "Unauthorized",
|
|
996
|
+
message: "A secret key is required. Create one at https://bakery3.com/keys, then pass it: new Bakery3Server(process.env.BAKERY3_SECRET_KEY!).",
|
|
997
|
+
docs: "https://bakery3.com/docs/auth"
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
if (secretKey.startsWith("bk_pt_")) {
|
|
1001
|
+
throw new Bakery3Error({
|
|
1002
|
+
code: "Unauthorized",
|
|
1003
|
+
message: "That is a browser token, not a secret key.",
|
|
1004
|
+
why: "Browser tokens (bk_pt_\u2026) are minted by this SDK. Secret keys (bk_sk_\u2026) create them.",
|
|
1005
|
+
fix: "Use your bk_sk_ key from https://bakery3.com/keys, kept in a server-only environment variable.",
|
|
1006
|
+
docs: "https://bakery3.com/docs/auth"
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
if (!secretKey.startsWith("bk_sk_")) {
|
|
1010
|
+
throw new Bakery3Error({
|
|
1011
|
+
code: "Unauthorized",
|
|
1012
|
+
message: "That is not a secret key. Secret keys start with bk_sk_. Create one at https://bakery3.com/keys.",
|
|
1013
|
+
docs: "https://bakery3.com/docs/auth"
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
this.transport = new Bakery3Client({
|
|
1017
|
+
apiKey: secretKey,
|
|
1018
|
+
...this.options.baseUrl ? { baseUrl: this.options.baseUrl } : {},
|
|
1019
|
+
...this.options.fetch ? { fetch: this.options.fetch } : {},
|
|
1020
|
+
...this.options.retry ? { retry: this.options.retry } : {}
|
|
1021
|
+
});
|
|
1022
|
+
return this.transport;
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Render a catalog of models that already sit on a CDN: one call, no
|
|
1026
|
+
* browser, and none of the models' bytes through this process. Needs a
|
|
1027
|
+
* plan that includes imports (see https://bakery3.com/docs/catalog#plans).
|
|
1028
|
+
*
|
|
1029
|
+
* const run = await api.renderBatch({
|
|
1030
|
+
* key: 'fall-2026',
|
|
1031
|
+
* products: [{ id: 'chair-aria', url: 'https://cdn.shop.com/models/chair-aria.glb' }],
|
|
1032
|
+
* shots: ['three-quarter', 'front', { kind: 'video', from: 'front', to: 'back', seconds: 4 }],
|
|
1033
|
+
* room: { preset: 'loft_golden' },
|
|
1034
|
+
* maxCost: 600,
|
|
1035
|
+
* });
|
|
1036
|
+
* await run.wait(); // or let a webhook tell you, and exit now
|
|
1037
|
+
*
|
|
1038
|
+
* Resolves once every product is recorded and the run is sealed. The
|
|
1039
|
+
* shots render on from there; this process does not have to stay up.
|
|
1040
|
+
* A product whose model cannot be imported is skipped and listed in
|
|
1041
|
+
* `run.recorded.skips`, and the run goes on. Calling it again with the
|
|
1042
|
+
* same `key` resumes a recording that stopped, re-downloads no model that
|
|
1043
|
+
* did not change, and adopts every shot that already rendered.
|
|
1044
|
+
*
|
|
1045
|
+
* What a model by URL cannot carry (node materials, a shader-lit set) is
|
|
1046
|
+
* recorded where that code runs; see `renderVariants()` in `@oeave/bakery3`.
|
|
1047
|
+
*/
|
|
1048
|
+
async renderBatch(options) {
|
|
1049
|
+
if (!(options.maxCost > 0)) {
|
|
1050
|
+
throw new Bakery3Error({
|
|
1051
|
+
code: "InvalidRequest",
|
|
1052
|
+
message: "renderBatch() needs a maxCost.",
|
|
1053
|
+
why: "A run is as many renders as products times shots, and a ceiling is what makes that number safe to be wrong about: a page that would cross it is refused, never charged.",
|
|
1054
|
+
fix: "Call quoteBatch() with the same options for the real number, or testBatch() to look at three products first.",
|
|
1055
|
+
docs: "https://bakery3.com/docs/catalog#what-it-will-cost"
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
const batch = await this.openRun(options);
|
|
1059
|
+
const recorded = await recordBatch(this.client(), batch, options);
|
|
1060
|
+
await batch.close();
|
|
1061
|
+
return new RenderSet(batch, recorded);
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* What `renderBatch()` would cost. The models are imported (that is how
|
|
1065
|
+
* they are priced); nothing is rendered or charged.
|
|
1066
|
+
*/
|
|
1067
|
+
async quoteBatch(options) {
|
|
1068
|
+
const batch = await this.openRun({
|
|
1069
|
+
key: options.key && quoteKey(options.key),
|
|
1070
|
+
name: `${options.name ?? options.key} (quote)`,
|
|
1071
|
+
...options.lane ? { lane: options.lane } : {}
|
|
1072
|
+
});
|
|
1073
|
+
try {
|
|
1074
|
+
return await recordBatch(this.client(), batch, options, {
|
|
1075
|
+
dryRun: true
|
|
1076
|
+
});
|
|
1077
|
+
} finally {
|
|
1078
|
+
await batch.cancel().catch(() => {
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Look before paying: three products (the first, the middle and the last
|
|
1084
|
+
* of an array; the first three of anything else), every shot, at preview
|
|
1085
|
+
* quality and a small size, in the ordinary lane so they come back in
|
|
1086
|
+
* about a minute. A clip is cut to two seconds at twelve frames. Same
|
|
1087
|
+
* options as `renderBatch()`. The test run has keys of its own, so the
|
|
1088
|
+
* real run never adopts these previews as finished shots.
|
|
1089
|
+
*
|
|
1090
|
+
* const test = await api.testBatch(options);
|
|
1091
|
+
* for (const shot of await test.result()) console.log(shot.variant, shot.shot, shot.url);
|
|
1092
|
+
*/
|
|
1093
|
+
async testBatch(options, test = {}) {
|
|
1094
|
+
const edge = QUALITY[options.quality ?? "studio"].defaultLongEdge;
|
|
1095
|
+
const settings = testSettings(
|
|
1096
|
+
{
|
|
1097
|
+
key: options.key,
|
|
1098
|
+
width: options.width ?? edge,
|
|
1099
|
+
height: options.height ?? edge
|
|
1100
|
+
},
|
|
1101
|
+
test
|
|
1102
|
+
);
|
|
1103
|
+
const run = {
|
|
1104
|
+
...options,
|
|
1105
|
+
key: settings.key,
|
|
1106
|
+
lane: settings.lane,
|
|
1107
|
+
maxCost: settings.maxCost,
|
|
1108
|
+
products: sampleOf(options.products, test.products),
|
|
1109
|
+
name: `${options.name ?? options.key} (test)`
|
|
1110
|
+
};
|
|
1111
|
+
const batch = await this.openRun(run);
|
|
1112
|
+
const recorded = await recordBatch(this.client(), batch, run, {
|
|
1113
|
+
force: {
|
|
1114
|
+
version: settings.version,
|
|
1115
|
+
quality: settings.quality,
|
|
1116
|
+
width: settings.width,
|
|
1117
|
+
height: settings.height,
|
|
1118
|
+
fps: 12,
|
|
1119
|
+
seconds: 2
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
await batch.close();
|
|
1123
|
+
return new RenderSet(batch, recorded);
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* A run by id, from another process, a webhook handler, or tomorrow
|
|
1127
|
+
* morning.
|
|
1128
|
+
*/
|
|
1129
|
+
async getRenderSet(id) {
|
|
1130
|
+
const client = this.client();
|
|
1131
|
+
return new RenderSet(new Batch(client, await client.getBatch(id)));
|
|
1132
|
+
}
|
|
1133
|
+
async openRun(options) {
|
|
1134
|
+
if (!options.key) {
|
|
1135
|
+
throw new Bakery3Error({
|
|
1136
|
+
code: "InvalidRequest",
|
|
1137
|
+
message: "A run needs a key.",
|
|
1138
|
+
why: "The key names the run. It is what lets the same call resume a recording that stopped, instead of starting a second one.",
|
|
1139
|
+
fix: "Give it something stable that names this body of work: `key: 'fall-2026'`.",
|
|
1140
|
+
docs: "https://bakery3.com/docs/catalog"
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
const client = this.client();
|
|
1144
|
+
return new Batch(
|
|
1145
|
+
client,
|
|
1146
|
+
await client.openBatch({
|
|
1147
|
+
key: options.key,
|
|
1148
|
+
...options.name ? { name: options.name } : {},
|
|
1149
|
+
...options.maxCost !== void 0 ? { maxCost: options.maxCost } : {},
|
|
1150
|
+
lane: options.lane ?? "batch",
|
|
1151
|
+
...options.maxParallel !== void 0 ? { maxParallel: options.maxParallel } : {}
|
|
1152
|
+
})
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
var AssetsApi = class {
|
|
1157
|
+
constructor(client) {
|
|
1158
|
+
this.client = client;
|
|
1159
|
+
}
|
|
1160
|
+
client;
|
|
1161
|
+
/**
|
|
1162
|
+
* Have the service fetch a public https URL into the project's assets,
|
|
1163
|
+
* and say what it found: the hash to put in a manifest, and for a model
|
|
1164
|
+
* its triangles, bounds, cameras and anything the renderer cannot read.
|
|
1165
|
+
*
|
|
1166
|
+
* Secret key only, which is why it is here and not in the browser SDK.
|
|
1167
|
+
* What is fetched is fenced on the service's side: https, public
|
|
1168
|
+
* addresses only, no credentials, a size limit, and the file must be a
|
|
1169
|
+
* model or an HDRI. An unchanged URL is answered without a download
|
|
1170
|
+
* (`unchanged: true`).
|
|
1171
|
+
*/
|
|
1172
|
+
async import(url, options = {}) {
|
|
1173
|
+
return this.client().importAsset({
|
|
1174
|
+
url,
|
|
1175
|
+
...options.kind ? { kind: options.kind } : {}
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
var TokensApi = class {
|
|
1180
|
+
constructor(request) {
|
|
1181
|
+
this.request = request;
|
|
1182
|
+
}
|
|
1183
|
+
request;
|
|
1184
|
+
/**
|
|
1185
|
+
* Mint a browser token.
|
|
1186
|
+
*
|
|
1187
|
+
* Everything about it is a ceiling rather than a grant: it cannot outlive
|
|
1188
|
+
* its expiry, cannot start more renders than `maxRenders`, cannot spend past
|
|
1189
|
+
* `maxCost`, cannot be used from an origin outside `allowedOrigins`, and
|
|
1190
|
+
* cannot perform a single billing or admin operation. That last one is not
|
|
1191
|
+
* a policy: those routes require a `bk_sk_` key and this is not one.
|
|
1192
|
+
*/
|
|
1193
|
+
create(options = {}) {
|
|
1194
|
+
return this.request("POST", "/v1/tokens", {
|
|
1195
|
+
expiresInSeconds: options.expiresInSeconds ?? 300,
|
|
1196
|
+
maxRenders: options.maxRenders ?? 5,
|
|
1197
|
+
maxCost: options.maxCost ?? 2,
|
|
1198
|
+
...options.allowedOrigins ? { allowedOrigins: options.allowedOrigins } : {},
|
|
1199
|
+
...options.subject ? { subject: options.subject } : {}
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
/** Revoke a token before its expiry. */
|
|
1203
|
+
revoke(token) {
|
|
1204
|
+
return this.request("POST", "/v1/tokens/revoke", { token });
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
var RendersApi = class {
|
|
1208
|
+
constructor(request, client) {
|
|
1209
|
+
this.request = request;
|
|
1210
|
+
this.client = client;
|
|
1211
|
+
}
|
|
1212
|
+
request;
|
|
1213
|
+
client;
|
|
1214
|
+
/**
|
|
1215
|
+
* Render from a manifest you already have: a batch job, a re-render, a
|
|
1216
|
+
* queued webhook. The browser flow is `@oeave/bakery3`; this is the API.
|
|
1217
|
+
* Repeated on a 429/503 only when `spec.idempotencyKey` makes that safe.
|
|
1218
|
+
*/
|
|
1219
|
+
async create(spec) {
|
|
1220
|
+
return this.client().createRender(spec);
|
|
1221
|
+
}
|
|
1222
|
+
/** A render's record, with its state, cost and result URLs. */
|
|
1223
|
+
get(id) {
|
|
1224
|
+
return this.request("GET", `/v1/renders/${id}`);
|
|
1225
|
+
}
|
|
1226
|
+
/** Cancel a render that has not finished. Already terminal is a no-op. */
|
|
1227
|
+
cancel(id) {
|
|
1228
|
+
return this.request("POST", `/v1/renders/${id}/cancel`, {});
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* One more attempt for a render that failed with `attemptsExhausted`.
|
|
1232
|
+
* Returns the new render, linked to the old one; a render that is not in
|
|
1233
|
+
* that state answers 409.
|
|
1234
|
+
*/
|
|
1235
|
+
async retry(id) {
|
|
1236
|
+
return this.client().retryRender(id);
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Delete a render now rather than when retention would: the result, the
|
|
1240
|
+
* preview, and every signed URL that pointed at them. The record survives
|
|
1241
|
+
* as `expired`, so the receipt is still readable. A live render is
|
|
1242
|
+
* cancelled first.
|
|
1243
|
+
*/
|
|
1244
|
+
delete(id) {
|
|
1245
|
+
return this.request("DELETE", `/v1/renders/${id}`);
|
|
1246
|
+
}
|
|
1247
|
+
/** The project's renders, newest first, a page at a time. */
|
|
1248
|
+
list(options = {}) {
|
|
1249
|
+
const query = new URLSearchParams();
|
|
1250
|
+
if (options.limit) query.set("limit", String(options.limit));
|
|
1251
|
+
if (options.cursor) query.set("cursor", options.cursor);
|
|
1252
|
+
const suffix = query.toString() ? `?${query}` : "";
|
|
1253
|
+
return this.request("GET", `/v1/renders${suffix}`);
|
|
1254
|
+
}
|
|
1255
|
+
/** The quote for a spec: a likely cost and a maximum, in USD. */
|
|
1256
|
+
estimate(spec) {
|
|
1257
|
+
return this.request("POST", "/v1/estimate", spec);
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
var BatchesApi = class {
|
|
1261
|
+
constructor(client) {
|
|
1262
|
+
this.client = client;
|
|
1263
|
+
}
|
|
1264
|
+
client;
|
|
1265
|
+
/** Open, or re-open by key. */
|
|
1266
|
+
async open(request) {
|
|
1267
|
+
const client = this.client();
|
|
1268
|
+
return new Batch(client, await client.openBatch(request));
|
|
1269
|
+
}
|
|
1270
|
+
/** A batch by id, with its latest counts. */
|
|
1271
|
+
async get(id) {
|
|
1272
|
+
const client = this.client();
|
|
1273
|
+
return new Batch(client, await client.getBatch(id));
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
export { Bakery3Server, verifyWebhook };
|
|
1278
|
+
//# sourceMappingURL=index.js.map
|
|
1279
|
+
//# sourceMappingURL=index.js.map
|