@oeave/bakery3 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +562 -2
- package/dist/animation-B1h0Ryvj.d.ts +97 -0
- package/dist/bake/index.d.ts +369 -0
- package/dist/bake/index.js +9 -0
- package/dist/bake/index.js.map +1 -0
- package/dist/bake-DZ-CJR6f.d.ts +1364 -0
- package/dist/catalog/index.d.ts +100 -0
- package/dist/catalog/index.js +154 -0
- package/dist/catalog/index.js.map +1 -0
- package/dist/catalog.gen-BM-aNf7n.d.ts +733 -0
- package/dist/chunk-3G5QL4F4.js +145 -0
- package/dist/chunk-3G5QL4F4.js.map +1 -0
- package/dist/chunk-4E5VV4QY.js +12 -0
- package/dist/chunk-4E5VV4QY.js.map +1 -0
- package/dist/chunk-62M6XXNX.js +142 -0
- package/dist/chunk-62M6XXNX.js.map +1 -0
- package/dist/chunk-6FYI6AJM.js +1157 -0
- package/dist/chunk-6FYI6AJM.js.map +1 -0
- package/dist/chunk-6NYR73Y7.js +832 -0
- package/dist/chunk-6NYR73Y7.js.map +1 -0
- package/dist/chunk-APWUCEEB.js +118 -0
- package/dist/chunk-APWUCEEB.js.map +1 -0
- package/dist/chunk-HNMSWQU7.js +1709 -0
- package/dist/chunk-HNMSWQU7.js.map +1 -0
- package/dist/chunk-JFYUDERE.js +1412 -0
- package/dist/chunk-JFYUDERE.js.map +1 -0
- package/dist/chunk-KNUAOILG.js +551 -0
- package/dist/chunk-KNUAOILG.js.map +1 -0
- package/dist/chunk-KZLVTSBI.js +191 -0
- package/dist/chunk-KZLVTSBI.js.map +1 -0
- package/dist/chunk-LAKXC4WR.js +2028 -0
- package/dist/chunk-LAKXC4WR.js.map +1 -0
- package/dist/chunk-NXBAZGNB.js +1107 -0
- package/dist/chunk-NXBAZGNB.js.map +1 -0
- package/dist/chunk-QRCT5UGZ.js +3286 -0
- package/dist/chunk-QRCT5UGZ.js.map +1 -0
- package/dist/chunk-S4AJTLLN.js +23 -0
- package/dist/chunk-S4AJTLLN.js.map +1 -0
- package/dist/chunk-UWBP7B54.js +92 -0
- package/dist/chunk-UWBP7B54.js.map +1 -0
- package/dist/chunk-XK35ANPJ.js +346 -0
- package/dist/chunk-XK35ANPJ.js.map +1 -0
- package/dist/chunk-Z7IYVH22.js +4781 -0
- package/dist/chunk-Z7IYVH22.js.map +1 -0
- package/dist/chunk-ZEBVAIJJ.js +156 -0
- package/dist/chunk-ZEBVAIJJ.js.map +1 -0
- package/dist/devtools/index.d.ts +536 -0
- package/dist/devtools/index.js +15 -0
- package/dist/devtools/index.js.map +1 -0
- package/dist/environments/index.d.ts +93 -0
- package/dist/environments/index.js +382 -0
- package/dist/environments/index.js.map +1 -0
- package/dist/hotspots/index.d.ts +111 -0
- package/dist/hotspots/index.js +285 -0
- package/dist/hotspots/index.js.map +1 -0
- package/dist/index.d.ts +975 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/node/index.cjs +5240 -0
- package/dist/node/index.cjs.map +1 -0
- package/dist/node/index.d.cts +4927 -0
- package/dist/node/index.d.ts +597 -0
- package/dist/node/index.js +1328 -0
- package/dist/node/index.js.map +1 -0
- package/dist/prepare-BtjY4G3q.d.ts +112 -0
- package/dist/presets/index.d.ts +562 -0
- package/dist/presets/index.js +14 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/r3f/index.d.ts +159 -0
- package/dist/r3f/index.js +592 -0
- package/dist/r3f/index.js.map +1 -0
- package/dist/room-FS26KAPQ.js +9 -0
- package/dist/room-FS26KAPQ.js.map +1 -0
- package/dist/rooms.gen-DItzBR9k.d.ts +1462 -0
- package/dist/session-VCIQEO26.js +9 -0
- package/dist/session-VCIQEO26.js.map +1 -0
- package/dist/shapes/index.d.ts +222 -0
- package/dist/shapes/index.js +836 -0
- package/dist/shapes/index.js.map +1 -0
- package/dist/testRun-20OARnQr.d.ts +1139 -0
- package/dist/timeline-ChwgD7bT.d.ts +470 -0
- package/dist/tsl/index.d.ts +165 -0
- package/dist/tsl/index.js +310 -0
- package/dist/tsl/index.js.map +1 -0
- package/package.json +170 -4
|
@@ -0,0 +1,3286 @@
|
|
|
1
|
+
import { linearToSrgbHex, OBJECT_PROPERTIES, prepareBake, aspectOf, createTimeline, outputSize } from './chunk-Z7IYVH22.js';
|
|
2
|
+
import { createBakeSession } from './chunk-KNUAOILG.js';
|
|
3
|
+
import { Bakery3Error } from './chunk-3G5QL4F4.js';
|
|
4
|
+
|
|
5
|
+
// src/devtools/dom.ts
|
|
6
|
+
var INK = "#06070d";
|
|
7
|
+
var BLUE = "#46b8ed";
|
|
8
|
+
var PEACH = "#fdc385";
|
|
9
|
+
var ORANGE = "#ec9d3c";
|
|
10
|
+
var BLUE_RGB = "70,184,237";
|
|
11
|
+
var SWEEP = `linear-gradient(90deg, ${BLUE}, ${PEACH}, ${ORANGE})`;
|
|
12
|
+
var CORNERS = {
|
|
13
|
+
"bottom-right": { right: "16px", bottom: "16px" },
|
|
14
|
+
"bottom-left": { left: "16px", bottom: "16px" },
|
|
15
|
+
"top-right": { right: "16px", top: "16px" },
|
|
16
|
+
"top-left": { left: "16px", top: "16px" }
|
|
17
|
+
};
|
|
18
|
+
var SHELL = {
|
|
19
|
+
position: "fixed",
|
|
20
|
+
zIndex: "2147483000",
|
|
21
|
+
width: "380px",
|
|
22
|
+
background: "rgba(18,18,20,0.92)",
|
|
23
|
+
backdropFilter: "blur(12px)",
|
|
24
|
+
color: "#f4f4f5",
|
|
25
|
+
borderRadius: "10px",
|
|
26
|
+
border: "1px solid rgba(255,255,255,0.08)",
|
|
27
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.4)",
|
|
28
|
+
font: "12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
29
|
+
overflow: "hidden"
|
|
30
|
+
};
|
|
31
|
+
var HEADER = {
|
|
32
|
+
display: "flex",
|
|
33
|
+
justifyContent: "space-between",
|
|
34
|
+
alignItems: "center",
|
|
35
|
+
width: "100%",
|
|
36
|
+
padding: "10px 12px",
|
|
37
|
+
background: "transparent",
|
|
38
|
+
border: "0",
|
|
39
|
+
color: "inherit",
|
|
40
|
+
font: "inherit",
|
|
41
|
+
fontWeight: "600",
|
|
42
|
+
letterSpacing: "0.02em",
|
|
43
|
+
cursor: "pointer"
|
|
44
|
+
};
|
|
45
|
+
var BODY = { padding: "0 12px 12px" };
|
|
46
|
+
var PILL = {
|
|
47
|
+
flex: "1 1 auto",
|
|
48
|
+
padding: "4px 8px",
|
|
49
|
+
background: "rgba(255,255,255,0.06)",
|
|
50
|
+
border: "1px solid transparent",
|
|
51
|
+
borderRadius: "6px",
|
|
52
|
+
color: "inherit",
|
|
53
|
+
font: "inherit",
|
|
54
|
+
cursor: "pointer"
|
|
55
|
+
};
|
|
56
|
+
var CTA = {
|
|
57
|
+
flex: "1 1 auto",
|
|
58
|
+
padding: "6px 8px",
|
|
59
|
+
background: "#f4f4f5",
|
|
60
|
+
color: "#18181b",
|
|
61
|
+
border: "0",
|
|
62
|
+
borderRadius: "6px",
|
|
63
|
+
font: "inherit",
|
|
64
|
+
fontWeight: "600",
|
|
65
|
+
cursor: "pointer"
|
|
66
|
+
};
|
|
67
|
+
function div(style) {
|
|
68
|
+
const element = document.createElement("div");
|
|
69
|
+
Object.assign(element.style, style);
|
|
70
|
+
return element;
|
|
71
|
+
}
|
|
72
|
+
function span(text, style) {
|
|
73
|
+
const element = document.createElement("span");
|
|
74
|
+
element.textContent = text;
|
|
75
|
+
Object.assign(element.style, style);
|
|
76
|
+
return element;
|
|
77
|
+
}
|
|
78
|
+
function button(text, style) {
|
|
79
|
+
const element = document.createElement("button");
|
|
80
|
+
element.textContent = text;
|
|
81
|
+
Object.assign(element.style, style);
|
|
82
|
+
return element;
|
|
83
|
+
}
|
|
84
|
+
function numberInput(value, min, step) {
|
|
85
|
+
const element = document.createElement("input");
|
|
86
|
+
element.type = "number";
|
|
87
|
+
element.value = String(value);
|
|
88
|
+
element.min = String(min);
|
|
89
|
+
element.step = String(step);
|
|
90
|
+
Object.assign(element.style, {
|
|
91
|
+
...PILL,
|
|
92
|
+
width: "100%",
|
|
93
|
+
cursor: "text"
|
|
94
|
+
});
|
|
95
|
+
return element;
|
|
96
|
+
}
|
|
97
|
+
function select(entries) {
|
|
98
|
+
const element = document.createElement("select");
|
|
99
|
+
Object.assign(element.style, PILL);
|
|
100
|
+
for (const [value, label] of entries)
|
|
101
|
+
element.appendChild(optionEl(value, label));
|
|
102
|
+
return element;
|
|
103
|
+
}
|
|
104
|
+
function optionEl(value, label) {
|
|
105
|
+
const option = document.createElement("option");
|
|
106
|
+
option.value = value;
|
|
107
|
+
option.textContent = label;
|
|
108
|
+
return option;
|
|
109
|
+
}
|
|
110
|
+
function labeled(text, control) {
|
|
111
|
+
const wrapper = div({ flex: "1 1 auto" });
|
|
112
|
+
wrapper.appendChild(
|
|
113
|
+
span(text, { display: "block", opacity: "0.5", fontSize: "10px" })
|
|
114
|
+
);
|
|
115
|
+
wrapper.appendChild(control);
|
|
116
|
+
return wrapper;
|
|
117
|
+
}
|
|
118
|
+
function describe(error) {
|
|
119
|
+
if (error && typeof error === "object" && "toDisplayString" in error) {
|
|
120
|
+
return error.toDisplayString();
|
|
121
|
+
}
|
|
122
|
+
return error instanceof Error ? error.message : String(error);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/devtools/timelinePanel.ts
|
|
126
|
+
var EASINGS = ["linear", "ease-in", "ease-out", "ease-in-out"];
|
|
127
|
+
var OBJECT_TRACKS = [
|
|
128
|
+
"position",
|
|
129
|
+
"rotation",
|
|
130
|
+
"scale",
|
|
131
|
+
"color",
|
|
132
|
+
"emissive",
|
|
133
|
+
"emissiveIntensity",
|
|
134
|
+
"roughness",
|
|
135
|
+
"metalness",
|
|
136
|
+
"opacity",
|
|
137
|
+
"intensity",
|
|
138
|
+
"target",
|
|
139
|
+
"groundColor",
|
|
140
|
+
"environmentIntensity",
|
|
141
|
+
"environmentRotation"
|
|
142
|
+
];
|
|
143
|
+
var GUTTER_PX = 104;
|
|
144
|
+
var END_PX = 22;
|
|
145
|
+
var GAP_PX = 6;
|
|
146
|
+
var INSET_PX = 6;
|
|
147
|
+
var AXIS_PX = 6;
|
|
148
|
+
function axis() {
|
|
149
|
+
return div({
|
|
150
|
+
position: "absolute",
|
|
151
|
+
top: "0",
|
|
152
|
+
bottom: "0",
|
|
153
|
+
left: `${AXIS_PX}px`,
|
|
154
|
+
right: `${AXIS_PX}px`
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
var ROW = {
|
|
158
|
+
display: "flex",
|
|
159
|
+
alignItems: "center",
|
|
160
|
+
gap: `${GAP_PX}px`,
|
|
161
|
+
padding: `0 ${INSET_PX}px`
|
|
162
|
+
};
|
|
163
|
+
var ROUND = {
|
|
164
|
+
flex: "0 0 auto",
|
|
165
|
+
width: "28px",
|
|
166
|
+
height: "28px",
|
|
167
|
+
padding: "0",
|
|
168
|
+
display: "flex",
|
|
169
|
+
alignItems: "center",
|
|
170
|
+
justifyContent: "center",
|
|
171
|
+
background: "rgba(255,255,255,0.08)",
|
|
172
|
+
border: "0",
|
|
173
|
+
borderRadius: "50%",
|
|
174
|
+
color: "inherit",
|
|
175
|
+
font: "inherit",
|
|
176
|
+
fontSize: "11px",
|
|
177
|
+
cursor: "pointer"
|
|
178
|
+
};
|
|
179
|
+
var DIAMOND = {
|
|
180
|
+
position: "absolute",
|
|
181
|
+
top: "50%",
|
|
182
|
+
width: "10px",
|
|
183
|
+
height: "10px",
|
|
184
|
+
marginTop: "-5px",
|
|
185
|
+
transform: "rotate(45deg)",
|
|
186
|
+
borderRadius: "2px"
|
|
187
|
+
};
|
|
188
|
+
var INLINE_NUMBER = {
|
|
189
|
+
flex: "0 0 auto",
|
|
190
|
+
padding: "3px 5px",
|
|
191
|
+
background: "rgba(255,255,255,0.06)",
|
|
192
|
+
border: "1px solid transparent",
|
|
193
|
+
borderRadius: "4px",
|
|
194
|
+
textAlign: "right",
|
|
195
|
+
fontVariantNumeric: "tabular-nums"
|
|
196
|
+
};
|
|
197
|
+
var MUTED = { opacity: "0.55" };
|
|
198
|
+
function mountTimelinePanel(options) {
|
|
199
|
+
const host = options.container ?? document.body;
|
|
200
|
+
let open = options.defaultOpen ?? true;
|
|
201
|
+
const shell = div(SHELL);
|
|
202
|
+
Object.assign(shell.style, CORNERS[options.position ?? "bottom-left"]);
|
|
203
|
+
const header = button("Timeline", HEADER);
|
|
204
|
+
const caret = span(open ? "\u25BE" : "\u25B4", { opacity: "0.5" });
|
|
205
|
+
header.appendChild(caret);
|
|
206
|
+
const timelineBody = buildTimelineBody({
|
|
207
|
+
timeline: options.timeline,
|
|
208
|
+
...options.targets ? { targets: options.targets } : {},
|
|
209
|
+
...options.onRenderVideo ? { onRenderVideo: options.onRenderVideo } : {},
|
|
210
|
+
...options.onPlaybackChange ? { onPlaybackChange: options.onPlaybackChange } : {},
|
|
211
|
+
style: BODY,
|
|
212
|
+
showRenderButton: true
|
|
213
|
+
});
|
|
214
|
+
const body = timelineBody.element;
|
|
215
|
+
if (!open) body.style.display = "none";
|
|
216
|
+
header.onclick = () => {
|
|
217
|
+
open = !open;
|
|
218
|
+
caret.textContent = open ? "\u25BE" : "\u25B4";
|
|
219
|
+
body.style.display = open ? "block" : "none";
|
|
220
|
+
};
|
|
221
|
+
shell.append(header, body);
|
|
222
|
+
host.appendChild(shell);
|
|
223
|
+
return {
|
|
224
|
+
refresh() {
|
|
225
|
+
timelineBody.refresh(options.targets ?? []);
|
|
226
|
+
},
|
|
227
|
+
setStatus: timelineBody.setStatus,
|
|
228
|
+
dispose() {
|
|
229
|
+
timelineBody.dispose();
|
|
230
|
+
shell.remove();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function buildTimelineBody(options) {
|
|
235
|
+
const { timeline } = options;
|
|
236
|
+
let targets = options.targets ?? [];
|
|
237
|
+
let selected = null;
|
|
238
|
+
let holding = false;
|
|
239
|
+
let scrubbing = false;
|
|
240
|
+
let busy = false;
|
|
241
|
+
const body = div(options.style ?? {});
|
|
242
|
+
const transport = div({ display: "flex", alignItems: "center", gap: "8px" });
|
|
243
|
+
const playButton = button("\u25B6", ROUND);
|
|
244
|
+
playButton.title = "play";
|
|
245
|
+
const loopButton = button("\u27F2", {
|
|
246
|
+
...ROUND,
|
|
247
|
+
borderRadius: "6px",
|
|
248
|
+
background: "transparent",
|
|
249
|
+
fontSize: "15px"
|
|
250
|
+
});
|
|
251
|
+
loopButton.title = "loop";
|
|
252
|
+
const timeNow = span("0.00", {
|
|
253
|
+
fontSize: "13px",
|
|
254
|
+
fontWeight: "600",
|
|
255
|
+
fontVariantNumeric: "tabular-nums"
|
|
256
|
+
});
|
|
257
|
+
const timeTotal = span(" / 0.00 s", { opacity: "0.5" });
|
|
258
|
+
const readout = span("", { flex: "1 1 auto", whiteSpace: "nowrap" });
|
|
259
|
+
readout.append(timeNow, timeTotal);
|
|
260
|
+
const cameraButton = button("\u25C6 Keyframe camera", {
|
|
261
|
+
flex: "0 0 auto",
|
|
262
|
+
padding: "6px 10px",
|
|
263
|
+
background: `rgba(${BLUE_RGB},0.12)`,
|
|
264
|
+
color: BLUE,
|
|
265
|
+
border: "0",
|
|
266
|
+
borderRadius: "6px",
|
|
267
|
+
font: "inherit",
|
|
268
|
+
fontSize: "11px",
|
|
269
|
+
cursor: "pointer"
|
|
270
|
+
});
|
|
271
|
+
cameraButton.title = "Record where the camera is and what it looks at, here";
|
|
272
|
+
transport.append(playButton, loopButton, readout, cameraButton);
|
|
273
|
+
const view = div({
|
|
274
|
+
marginTop: "10px",
|
|
275
|
+
padding: "6px 0",
|
|
276
|
+
borderRadius: "8px",
|
|
277
|
+
background: "rgba(255,255,255,0.025)",
|
|
278
|
+
border: "1px solid rgba(255,255,255,0.06)",
|
|
279
|
+
userSelect: "none"
|
|
280
|
+
});
|
|
281
|
+
const stack = div({ position: "relative" });
|
|
282
|
+
const rulerRow = div(ROW);
|
|
283
|
+
const ruler = div({
|
|
284
|
+
position: "relative",
|
|
285
|
+
flex: "1 1 auto",
|
|
286
|
+
height: "22px",
|
|
287
|
+
cursor: "ew-resize"
|
|
288
|
+
});
|
|
289
|
+
const rulerBase = div({
|
|
290
|
+
position: "absolute",
|
|
291
|
+
left: "0",
|
|
292
|
+
right: "0",
|
|
293
|
+
bottom: "0",
|
|
294
|
+
height: "2px",
|
|
295
|
+
borderRadius: "1px",
|
|
296
|
+
background: "rgba(255,255,255,0.08)"
|
|
297
|
+
});
|
|
298
|
+
const played = div({
|
|
299
|
+
position: "absolute",
|
|
300
|
+
left: "0",
|
|
301
|
+
right: "0",
|
|
302
|
+
bottom: "0",
|
|
303
|
+
height: "2px",
|
|
304
|
+
borderRadius: "1px",
|
|
305
|
+
background: SWEEP,
|
|
306
|
+
clipPath: "inset(0 100% 0 0)"
|
|
307
|
+
});
|
|
308
|
+
const ticks = div({ position: "absolute", inset: "0" });
|
|
309
|
+
const rulerAxis = axis();
|
|
310
|
+
rulerAxis.append(ticks, rulerBase, played);
|
|
311
|
+
ruler.appendChild(rulerAxis);
|
|
312
|
+
rulerRow.append(gutter(), ruler, end());
|
|
313
|
+
const lanes = div({ display: "flex", flexDirection: "column", gap: "2px" });
|
|
314
|
+
const head = div({
|
|
315
|
+
position: "absolute",
|
|
316
|
+
top: "0",
|
|
317
|
+
bottom: "0",
|
|
318
|
+
left: `${INSET_PX + GUTTER_PX + GAP_PX + AXIS_PX}px`,
|
|
319
|
+
right: `${INSET_PX + END_PX + GAP_PX + AXIS_PX}px`,
|
|
320
|
+
pointerEvents: "none"
|
|
321
|
+
});
|
|
322
|
+
const playhead = div({
|
|
323
|
+
position: "absolute",
|
|
324
|
+
top: "0",
|
|
325
|
+
bottom: "0",
|
|
326
|
+
left: "0%",
|
|
327
|
+
width: "2px",
|
|
328
|
+
marginLeft: "-1px",
|
|
329
|
+
background: BLUE,
|
|
330
|
+
boxShadow: `0 0 6px rgba(${BLUE_RGB},0.6)`
|
|
331
|
+
});
|
|
332
|
+
const knob = div({
|
|
333
|
+
position: "absolute",
|
|
334
|
+
top: "12px",
|
|
335
|
+
left: "-4px",
|
|
336
|
+
width: "10px",
|
|
337
|
+
height: "8px",
|
|
338
|
+
borderRadius: "2px 2px 5px 5px",
|
|
339
|
+
background: BLUE
|
|
340
|
+
});
|
|
341
|
+
playhead.appendChild(knob);
|
|
342
|
+
head.appendChild(playhead);
|
|
343
|
+
stack.append(rulerRow, lanes, head);
|
|
344
|
+
const addRow = div({ ...ROW, marginTop: "4px" });
|
|
345
|
+
const trackPicker = select(addOptions(targets));
|
|
346
|
+
Object.assign(trackPicker.style, {
|
|
347
|
+
flex: "0 1 auto",
|
|
348
|
+
padding: "3px 6px",
|
|
349
|
+
background: "transparent",
|
|
350
|
+
border: "1px dashed rgba(255,255,255,0.14)",
|
|
351
|
+
fontSize: "11px",
|
|
352
|
+
opacity: "0.75"
|
|
353
|
+
});
|
|
354
|
+
trackPicker.title = "Add a lane for another property";
|
|
355
|
+
addRow.appendChild(trackPicker);
|
|
356
|
+
const emptyNote = span(
|
|
357
|
+
"Frame the shot and press Keyframe camera. Move the playhead, frame the next one, and press it again.",
|
|
358
|
+
{
|
|
359
|
+
display: "none",
|
|
360
|
+
padding: `8px ${INSET_PX}px 4px`,
|
|
361
|
+
fontSize: "11px",
|
|
362
|
+
opacity: "0.5"
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
view.append(stack, emptyNote, addRow);
|
|
366
|
+
const inspector = div({
|
|
367
|
+
display: "none",
|
|
368
|
+
alignItems: "center",
|
|
369
|
+
gap: "8px",
|
|
370
|
+
marginTop: "8px",
|
|
371
|
+
fontSize: "11px"
|
|
372
|
+
});
|
|
373
|
+
const inspectorMark = div({
|
|
374
|
+
...DIAMOND,
|
|
375
|
+
position: "static",
|
|
376
|
+
flex: "0 0 auto",
|
|
377
|
+
width: "8px",
|
|
378
|
+
height: "8px",
|
|
379
|
+
margin: "0 2px",
|
|
380
|
+
background: ORANGE
|
|
381
|
+
});
|
|
382
|
+
const inspectorText = span("", {
|
|
383
|
+
flex: "1 1 auto",
|
|
384
|
+
minWidth: "0",
|
|
385
|
+
overflow: "hidden",
|
|
386
|
+
textOverflow: "ellipsis",
|
|
387
|
+
whiteSpace: "nowrap"
|
|
388
|
+
});
|
|
389
|
+
const easingSelect = select(
|
|
390
|
+
EASINGS.map((value) => [String(value), String(value)])
|
|
391
|
+
);
|
|
392
|
+
Object.assign(easingSelect.style, { flex: "0 0 auto", fontSize: "11px" });
|
|
393
|
+
easingSelect.title = "how it eases into this keyframe";
|
|
394
|
+
const deleteButton = button("Delete", { ...PILL, flex: "0 0 auto" });
|
|
395
|
+
const deselect = button("\xD7", { ...PILL, flex: "0 0 auto", width: "24px" });
|
|
396
|
+
deselect.title = "done";
|
|
397
|
+
inspector.append(
|
|
398
|
+
inspectorMark,
|
|
399
|
+
inspectorText,
|
|
400
|
+
easingSelect,
|
|
401
|
+
deleteButton,
|
|
402
|
+
deselect
|
|
403
|
+
);
|
|
404
|
+
const clip = div({
|
|
405
|
+
display: "flex",
|
|
406
|
+
alignItems: "center",
|
|
407
|
+
gap: "6px",
|
|
408
|
+
marginTop: "10px",
|
|
409
|
+
fontSize: "11px"
|
|
410
|
+
});
|
|
411
|
+
const durationInput = numberInput(timeline.duration, 0.25, 0.25);
|
|
412
|
+
Object.assign(durationInput.style, INLINE_NUMBER, { width: "52px" });
|
|
413
|
+
durationInput.title = "length in seconds";
|
|
414
|
+
const fpsInput = numberInput(timeline.fps, 1, 1);
|
|
415
|
+
Object.assign(fpsInput.style, INLINE_NUMBER, { width: "40px" });
|
|
416
|
+
fpsInput.title = "frames per second";
|
|
417
|
+
const frames = span(String(timeline.frameCount), {
|
|
418
|
+
fontWeight: "600",
|
|
419
|
+
fontVariantNumeric: "tabular-nums"
|
|
420
|
+
});
|
|
421
|
+
const copyButton = button("Copy JSON", {
|
|
422
|
+
...PILL,
|
|
423
|
+
flex: "0 0 auto",
|
|
424
|
+
marginLeft: "auto",
|
|
425
|
+
fontSize: "11px"
|
|
426
|
+
});
|
|
427
|
+
copyButton.title = "the timeline as JSON, for `createTimeline()`";
|
|
428
|
+
clip.append(
|
|
429
|
+
durationInput,
|
|
430
|
+
span("s \xD7", MUTED),
|
|
431
|
+
fpsInput,
|
|
432
|
+
span("fps =", MUTED),
|
|
433
|
+
frames,
|
|
434
|
+
span("frames", MUTED),
|
|
435
|
+
copyButton
|
|
436
|
+
);
|
|
437
|
+
const actions = div({ display: "none", marginTop: "10px" });
|
|
438
|
+
const renderButton = button("Render video", { ...CTA, width: "100%" });
|
|
439
|
+
if (options.onRenderVideo && options.showRenderButton !== false) {
|
|
440
|
+
actions.style.display = "flex";
|
|
441
|
+
actions.appendChild(renderButton);
|
|
442
|
+
}
|
|
443
|
+
const status = div({
|
|
444
|
+
display: "none",
|
|
445
|
+
marginTop: "8px",
|
|
446
|
+
fontSize: "11px",
|
|
447
|
+
opacity: "0.7",
|
|
448
|
+
whiteSpace: "pre-wrap"
|
|
449
|
+
});
|
|
450
|
+
body.append(transport, view, inspector, clip, actions, status);
|
|
451
|
+
function hold(active) {
|
|
452
|
+
if (holding === active) return;
|
|
453
|
+
holding = active;
|
|
454
|
+
options.onPlaybackChange?.(active);
|
|
455
|
+
}
|
|
456
|
+
playButton.onclick = () => {
|
|
457
|
+
if (timeline.playing) timeline.pause();
|
|
458
|
+
else timeline.play();
|
|
459
|
+
};
|
|
460
|
+
loopButton.onclick = () => {
|
|
461
|
+
timeline.loop = !timeline.loop;
|
|
462
|
+
paintLoop();
|
|
463
|
+
};
|
|
464
|
+
cameraButton.onclick = () => {
|
|
465
|
+
try {
|
|
466
|
+
timeline.captureCameraKeyframe();
|
|
467
|
+
setStatus(`camera keyframed at ${timeline.time.toFixed(2)} s`);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
setStatus(describe(error), "error");
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
function scrubbable(element, time) {
|
|
473
|
+
const seek = (event) => {
|
|
474
|
+
const rect = time.getBoundingClientRect();
|
|
475
|
+
const fraction = Math.max(
|
|
476
|
+
0,
|
|
477
|
+
Math.min(1, (event.clientX - rect.left) / rect.width)
|
|
478
|
+
);
|
|
479
|
+
timeline.seek(fraction * timeline.duration);
|
|
480
|
+
};
|
|
481
|
+
element.addEventListener("pointerdown", (event) => {
|
|
482
|
+
scrubbing = true;
|
|
483
|
+
timeline.pause();
|
|
484
|
+
hold(true);
|
|
485
|
+
element.setPointerCapture(event.pointerId);
|
|
486
|
+
seek(event);
|
|
487
|
+
});
|
|
488
|
+
element.addEventListener("pointermove", (event) => {
|
|
489
|
+
if (scrubbing) seek(event);
|
|
490
|
+
});
|
|
491
|
+
const endScrub = (event) => {
|
|
492
|
+
if (!scrubbing) return;
|
|
493
|
+
scrubbing = false;
|
|
494
|
+
element.releasePointerCapture?.(event.pointerId);
|
|
495
|
+
if (!timeline.playing) hold(false);
|
|
496
|
+
};
|
|
497
|
+
element.addEventListener("pointerup", endScrub);
|
|
498
|
+
element.addEventListener("pointercancel", endScrub);
|
|
499
|
+
}
|
|
500
|
+
scrubbable(ruler, rulerAxis);
|
|
501
|
+
durationInput.onchange = () => {
|
|
502
|
+
timeline.duration = Number(durationInput.value) || timeline.duration;
|
|
503
|
+
durationInput.value = String(timeline.duration);
|
|
504
|
+
};
|
|
505
|
+
fpsInput.onchange = () => {
|
|
506
|
+
timeline.fps = Number(fpsInput.value) || timeline.fps;
|
|
507
|
+
fpsInput.value = String(timeline.fps);
|
|
508
|
+
};
|
|
509
|
+
trackPicker.onchange = () => {
|
|
510
|
+
const [kind, property] = trackPicker.value.split("|");
|
|
511
|
+
trackPicker.value = "";
|
|
512
|
+
try {
|
|
513
|
+
const target = kind === "camera" ? "camera" : targets[Number(kind)]?.object;
|
|
514
|
+
if (!target || !property) return;
|
|
515
|
+
timeline.keyframe(target, property, timeline.time);
|
|
516
|
+
setStatus(`${property} keyframed at ${timeline.time.toFixed(2)} s`);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
setStatus(describe(error), "error");
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
copyButton.onclick = () => {
|
|
522
|
+
const json = JSON.stringify(timeline.toJSON(), null, 2);
|
|
523
|
+
void navigator.clipboard?.writeText(json);
|
|
524
|
+
setStatus(`${json.length} bytes of timeline JSON copied`);
|
|
525
|
+
};
|
|
526
|
+
async function renderVideo() {
|
|
527
|
+
if (!options.onRenderVideo) return;
|
|
528
|
+
timeline.pause();
|
|
529
|
+
hold(false);
|
|
530
|
+
busy = true;
|
|
531
|
+
renderButton.disabled = true;
|
|
532
|
+
renderButton.style.opacity = "0.6";
|
|
533
|
+
try {
|
|
534
|
+
await options.onRenderVideo(timeline);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
setStatus(describe(error), "error");
|
|
537
|
+
} finally {
|
|
538
|
+
busy = false;
|
|
539
|
+
renderButton.disabled = false;
|
|
540
|
+
renderButton.style.opacity = "1";
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
renderButton.onclick = () => void renderVideo();
|
|
544
|
+
easingSelect.onchange = () => {
|
|
545
|
+
if (!selected) return;
|
|
546
|
+
timeline.setEasing(
|
|
547
|
+
selected.trackId,
|
|
548
|
+
selected.index,
|
|
549
|
+
easingSelect.value
|
|
550
|
+
);
|
|
551
|
+
};
|
|
552
|
+
deleteButton.onclick = () => {
|
|
553
|
+
if (!selected) return;
|
|
554
|
+
const { trackId, index } = selected;
|
|
555
|
+
pick(null);
|
|
556
|
+
timeline.removeKeyframe(trackId, index);
|
|
557
|
+
};
|
|
558
|
+
deselect.onclick = () => {
|
|
559
|
+
pick(null);
|
|
560
|
+
paintLanes();
|
|
561
|
+
};
|
|
562
|
+
function pick(next) {
|
|
563
|
+
selected = next;
|
|
564
|
+
inspector.style.display = next ? "flex" : "none";
|
|
565
|
+
}
|
|
566
|
+
function paintTime() {
|
|
567
|
+
const fraction = timeline.duration ? timeline.time / timeline.duration : 0;
|
|
568
|
+
const percent = Math.max(0, Math.min(1, fraction)) * 100;
|
|
569
|
+
playhead.style.left = `${percent}%`;
|
|
570
|
+
played.style.clipPath = `inset(0 ${100 - percent}% 0 0)`;
|
|
571
|
+
timeNow.textContent = timeline.time.toFixed(2);
|
|
572
|
+
timeTotal.textContent = ` / ${timeline.duration.toFixed(2)} s`;
|
|
573
|
+
playButton.textContent = timeline.playing ? "\u275A\u275A" : "\u25B6";
|
|
574
|
+
playButton.title = timeline.playing ? "pause" : "play";
|
|
575
|
+
playButton.style.background = timeline.playing ? BLUE : "rgba(255,255,255,0.08)";
|
|
576
|
+
playButton.style.color = timeline.playing ? INK : "inherit";
|
|
577
|
+
if (timeline.playing) hold(true);
|
|
578
|
+
else if (!scrubbing) hold(false);
|
|
579
|
+
}
|
|
580
|
+
function paintLoop() {
|
|
581
|
+
loopButton.style.color = timeline.loop ? BLUE : "inherit";
|
|
582
|
+
loopButton.style.opacity = timeline.loop ? "1" : "0.45";
|
|
583
|
+
}
|
|
584
|
+
function paintRuler() {
|
|
585
|
+
const step = rulerStep(timeline.duration);
|
|
586
|
+
const marks = [];
|
|
587
|
+
if (step > 0) {
|
|
588
|
+
for (let i = 0; i * step / 2 <= timeline.duration + 1e-9; i++) {
|
|
589
|
+
const at = i * step / 2;
|
|
590
|
+
const major = i % 2 === 0;
|
|
591
|
+
const fraction = at / timeline.duration;
|
|
592
|
+
marks.push(
|
|
593
|
+
div({
|
|
594
|
+
position: "absolute",
|
|
595
|
+
bottom: "2px",
|
|
596
|
+
left: `${fraction * 100}%`,
|
|
597
|
+
width: "1px",
|
|
598
|
+
height: major ? "5px" : "3px",
|
|
599
|
+
background: "rgba(255,255,255,0.22)"
|
|
600
|
+
})
|
|
601
|
+
);
|
|
602
|
+
if (!major) continue;
|
|
603
|
+
const shift = fraction <= 0 ? "0" : fraction >= 0.999 ? "-100%" : "-50%";
|
|
604
|
+
marks.push(
|
|
605
|
+
span(`${formatSeconds(at)}s`, {
|
|
606
|
+
position: "absolute",
|
|
607
|
+
top: "0",
|
|
608
|
+
left: `${fraction * 100}%`,
|
|
609
|
+
transform: `translateX(${shift})`,
|
|
610
|
+
fontSize: "10px",
|
|
611
|
+
opacity: "0.45",
|
|
612
|
+
fontVariantNumeric: "tabular-nums"
|
|
613
|
+
})
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
ticks.replaceChildren(...marks);
|
|
618
|
+
}
|
|
619
|
+
function paintLanes() {
|
|
620
|
+
paintRuler();
|
|
621
|
+
durationInput.value = String(timeline.duration);
|
|
622
|
+
fpsInput.value = String(timeline.fps);
|
|
623
|
+
frames.textContent = String(timeline.frameCount);
|
|
624
|
+
emptyNote.style.display = timeline.tracks.length ? "none" : "block";
|
|
625
|
+
if (!timeline.tracks.length) {
|
|
626
|
+
pick(null);
|
|
627
|
+
lanes.replaceChildren();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (selected) {
|
|
631
|
+
const track = timeline.tracks.find(
|
|
632
|
+
(each) => each.id === selected.trackId
|
|
633
|
+
);
|
|
634
|
+
const keyframe = track?.keyframes[selected.index];
|
|
635
|
+
if (!track || !keyframe) pick(null);
|
|
636
|
+
else {
|
|
637
|
+
inspectorText.textContent = `${laneName(track)} at ${keyframe.time.toFixed(2)} s`;
|
|
638
|
+
easingSelect.value = String(keyframe.easing ?? "linear");
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
lanes.replaceChildren(...timeline.tracks.map(lane));
|
|
642
|
+
}
|
|
643
|
+
function lane(track) {
|
|
644
|
+
const row = div({ ...ROW, height: "24px" });
|
|
645
|
+
const name = div({
|
|
646
|
+
flex: `0 0 ${GUTTER_PX}px`,
|
|
647
|
+
display: "flex",
|
|
648
|
+
alignItems: "center",
|
|
649
|
+
gap: "5px",
|
|
650
|
+
minWidth: "0",
|
|
651
|
+
fontSize: "11px"
|
|
652
|
+
});
|
|
653
|
+
name.title = laneName(track);
|
|
654
|
+
if (track.property === "color" || track.property === "emissive") {
|
|
655
|
+
const value = timeline.valueAt(track, timeline.time);
|
|
656
|
+
if (value) {
|
|
657
|
+
name.appendChild(
|
|
658
|
+
span("", {
|
|
659
|
+
flex: "0 0 auto",
|
|
660
|
+
width: "8px",
|
|
661
|
+
height: "8px",
|
|
662
|
+
borderRadius: "50%",
|
|
663
|
+
background: linearToSrgbHex(value)
|
|
664
|
+
})
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
name.appendChild(
|
|
669
|
+
span(laneName(track), {
|
|
670
|
+
minWidth: "0",
|
|
671
|
+
overflow: "hidden",
|
|
672
|
+
textOverflow: "ellipsis",
|
|
673
|
+
whiteSpace: "nowrap",
|
|
674
|
+
opacity: "0.75"
|
|
675
|
+
})
|
|
676
|
+
);
|
|
677
|
+
const strip = div({
|
|
678
|
+
position: "relative",
|
|
679
|
+
flex: "1 1 auto",
|
|
680
|
+
height: "18px",
|
|
681
|
+
borderRadius: "4px",
|
|
682
|
+
background: "rgba(255,255,255,0.04)",
|
|
683
|
+
cursor: "ew-resize"
|
|
684
|
+
});
|
|
685
|
+
const time = axis();
|
|
686
|
+
strip.appendChild(time);
|
|
687
|
+
scrubbable(strip, time);
|
|
688
|
+
const at = (time2) => timeline.duration ? time2 / timeline.duration * 100 : 0;
|
|
689
|
+
track.keyframes.forEach((keyframe, index) => {
|
|
690
|
+
if (index === 0) return;
|
|
691
|
+
const from = at(track.keyframes[index - 1].time);
|
|
692
|
+
time.appendChild(
|
|
693
|
+
div({
|
|
694
|
+
position: "absolute",
|
|
695
|
+
top: "50%",
|
|
696
|
+
left: `${from}%`,
|
|
697
|
+
width: `${at(keyframe.time) - from}%`,
|
|
698
|
+
height: "2px",
|
|
699
|
+
marginTop: "-1px",
|
|
700
|
+
background: keyframe.path === "orbit" ? `rgba(${BLUE_RGB},0.55)` : "rgba(244,244,245,0.22)"
|
|
701
|
+
})
|
|
702
|
+
);
|
|
703
|
+
});
|
|
704
|
+
track.keyframes.forEach((keyframe, index) => {
|
|
705
|
+
const isSelected = selected?.trackId === track.id && selected.index === index;
|
|
706
|
+
const diamond = div({
|
|
707
|
+
...DIAMOND,
|
|
708
|
+
left: `calc(${at(keyframe.time)}% - 5px)`,
|
|
709
|
+
background: isSelected ? ORANGE : keyframe.path === "orbit" ? BLUE : "#e4e4e7",
|
|
710
|
+
boxShadow: isSelected ? `0 0 0 2px rgb(24,24,27), 0 0 10px rgba(236,157,60,0.7)` : "0 0 0 2px rgb(24,24,27)",
|
|
711
|
+
cursor: "pointer"
|
|
712
|
+
});
|
|
713
|
+
diamond.title = `${keyframe.time.toFixed(2)} s${keyframe.path === "orbit" ? ", orbiting" : ""}`;
|
|
714
|
+
diamond.addEventListener(
|
|
715
|
+
"pointerdown",
|
|
716
|
+
(event) => event.stopPropagation()
|
|
717
|
+
);
|
|
718
|
+
diamond.onclick = (event) => {
|
|
719
|
+
event.stopPropagation();
|
|
720
|
+
pick({ trackId: track.id, index });
|
|
721
|
+
timeline.seek(keyframe.time);
|
|
722
|
+
paintLanes();
|
|
723
|
+
};
|
|
724
|
+
time.appendChild(diamond);
|
|
725
|
+
});
|
|
726
|
+
const add = button("\uFF0B", {
|
|
727
|
+
...PILL,
|
|
728
|
+
flex: `0 0 ${END_PX}px`,
|
|
729
|
+
height: "18px",
|
|
730
|
+
padding: "0",
|
|
731
|
+
fontSize: "11px",
|
|
732
|
+
opacity: "0.7"
|
|
733
|
+
});
|
|
734
|
+
add.title = "keyframe the live value here";
|
|
735
|
+
add.onclick = () => {
|
|
736
|
+
try {
|
|
737
|
+
if (track.target === "camera" && track.property === "lookAt") {
|
|
738
|
+
timeline.captureCameraKeyframe();
|
|
739
|
+
} else {
|
|
740
|
+
timeline.keyframe(track.target, track.property, timeline.time);
|
|
741
|
+
}
|
|
742
|
+
} catch (error) {
|
|
743
|
+
setStatus(describe(error), "error");
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
row.append(name, strip, add);
|
|
747
|
+
return row;
|
|
748
|
+
}
|
|
749
|
+
function setStatus(message, tone = "info") {
|
|
750
|
+
status.textContent = message;
|
|
751
|
+
status.style.display = message ? "block" : "none";
|
|
752
|
+
status.style.color = tone === "error" ? "#ff6b6b" : "inherit";
|
|
753
|
+
}
|
|
754
|
+
const offChange = timeline.on("change", () => {
|
|
755
|
+
paintLanes();
|
|
756
|
+
paintTime();
|
|
757
|
+
});
|
|
758
|
+
const offTime = timeline.on("time", () => paintTime());
|
|
759
|
+
const offPlay = timeline.on("play", () => paintTime());
|
|
760
|
+
const offPause = timeline.on("pause", () => paintTime());
|
|
761
|
+
paintLoop();
|
|
762
|
+
paintLanes();
|
|
763
|
+
paintTime();
|
|
764
|
+
return {
|
|
765
|
+
element: body,
|
|
766
|
+
get busy() {
|
|
767
|
+
return busy;
|
|
768
|
+
},
|
|
769
|
+
refresh(next) {
|
|
770
|
+
if (next) targets = next;
|
|
771
|
+
trackPicker.replaceChildren();
|
|
772
|
+
for (const [value, label] of addOptions(targets)) {
|
|
773
|
+
trackPicker.appendChild(optionEl(value, label));
|
|
774
|
+
}
|
|
775
|
+
trackPicker.value = "";
|
|
776
|
+
paintLanes();
|
|
777
|
+
paintTime();
|
|
778
|
+
},
|
|
779
|
+
setStatus,
|
|
780
|
+
renderVideo,
|
|
781
|
+
dispose() {
|
|
782
|
+
offChange();
|
|
783
|
+
offTime();
|
|
784
|
+
offPlay();
|
|
785
|
+
offPause();
|
|
786
|
+
timeline.pause();
|
|
787
|
+
hold(false);
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function gutter() {
|
|
792
|
+
return div({ flex: `0 0 ${GUTTER_PX}px` });
|
|
793
|
+
}
|
|
794
|
+
function end() {
|
|
795
|
+
return div({ flex: `0 0 ${END_PX}px` });
|
|
796
|
+
}
|
|
797
|
+
function laneName(track) {
|
|
798
|
+
return `${track.label}.${track.property}`;
|
|
799
|
+
}
|
|
800
|
+
function rulerStep(duration) {
|
|
801
|
+
if (!(duration > 0)) return 0;
|
|
802
|
+
for (const step of [0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300]) {
|
|
803
|
+
if (duration / step <= 5) return step;
|
|
804
|
+
}
|
|
805
|
+
return Math.ceil(duration / 5);
|
|
806
|
+
}
|
|
807
|
+
function formatSeconds(seconds) {
|
|
808
|
+
return Number.isInteger(seconds) ? String(seconds) : seconds.toFixed(2).replace(/0$/, "");
|
|
809
|
+
}
|
|
810
|
+
function addOptions(targets) {
|
|
811
|
+
return [["", "\uFF0B Add a track"], ...trackOptions(targets)];
|
|
812
|
+
}
|
|
813
|
+
function trackOptions(targets) {
|
|
814
|
+
const entries = [
|
|
815
|
+
["camera|position", "camera \xB7 position"],
|
|
816
|
+
["camera|lookAt", "camera \xB7 lookAt"],
|
|
817
|
+
["camera|fov", "camera \xB7 fov"]
|
|
818
|
+
];
|
|
819
|
+
targets.forEach((target, index) => {
|
|
820
|
+
for (const property of OBJECT_TRACKS) {
|
|
821
|
+
const applies = OBJECT_PROPERTIES[property]?.appliesTo;
|
|
822
|
+
if (applies && !applies(target.object)) continue;
|
|
823
|
+
entries.push([`${index}|${property}`, `${target.label} \xB7 ${property}`]);
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
return entries;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// src/devtools/devPanel.ts
|
|
830
|
+
var CARD = {
|
|
831
|
+
...SHELL,
|
|
832
|
+
/**
|
|
833
|
+
* In-flow, unlike the standalone timeline shell: the corner-anchored root is
|
|
834
|
+
* the one fixed element, so a `bottom`-anchored panel grows upward from the
|
|
835
|
+
* FAB's corner. A fixed card here would sit at its static position (the
|
|
836
|
+
* corner point) and run its full height off the bottom of the screen.
|
|
837
|
+
*/
|
|
838
|
+
position: "static",
|
|
839
|
+
width: "380px",
|
|
840
|
+
display: "flex",
|
|
841
|
+
flexDirection: "column",
|
|
842
|
+
maxHeight: "min(78vh, 720px)"
|
|
843
|
+
};
|
|
844
|
+
var TAB = {
|
|
845
|
+
flex: "1 1 0",
|
|
846
|
+
minWidth: "0",
|
|
847
|
+
padding: "12px 8px 10px",
|
|
848
|
+
background: "transparent",
|
|
849
|
+
border: "0",
|
|
850
|
+
borderBottom: "2px solid transparent",
|
|
851
|
+
color: "inherit",
|
|
852
|
+
font: "inherit",
|
|
853
|
+
opacity: "0.55",
|
|
854
|
+
cursor: "pointer"
|
|
855
|
+
};
|
|
856
|
+
var META = {
|
|
857
|
+
flex: "1 1 0",
|
|
858
|
+
minWidth: "0",
|
|
859
|
+
display: "flex",
|
|
860
|
+
alignItems: "center",
|
|
861
|
+
justifyContent: "flex-end",
|
|
862
|
+
gap: "12px",
|
|
863
|
+
fontSize: "11px",
|
|
864
|
+
whiteSpace: "nowrap",
|
|
865
|
+
position: "relative"
|
|
866
|
+
};
|
|
867
|
+
var COMPAT = {
|
|
868
|
+
display: "flex",
|
|
869
|
+
alignItems: "center",
|
|
870
|
+
gap: "6px",
|
|
871
|
+
minWidth: "0",
|
|
872
|
+
padding: "0",
|
|
873
|
+
background: "transparent",
|
|
874
|
+
border: "0",
|
|
875
|
+
color: "inherit",
|
|
876
|
+
font: "inherit",
|
|
877
|
+
opacity: "0.65",
|
|
878
|
+
cursor: "default",
|
|
879
|
+
transition: "opacity 120ms ease"
|
|
880
|
+
};
|
|
881
|
+
var PRICE = {
|
|
882
|
+
flex: "0 0 auto",
|
|
883
|
+
color: BLUE,
|
|
884
|
+
cursor: "default",
|
|
885
|
+
transition: "opacity 120ms ease"
|
|
886
|
+
};
|
|
887
|
+
var BREAKDOWN = {
|
|
888
|
+
display: "none",
|
|
889
|
+
position: "absolute",
|
|
890
|
+
right: "0",
|
|
891
|
+
bottom: "calc(100% + 8px)",
|
|
892
|
+
zIndex: "1",
|
|
893
|
+
width: "max-content",
|
|
894
|
+
maxWidth: "330px",
|
|
895
|
+
padding: "8px 10px",
|
|
896
|
+
borderRadius: "8px",
|
|
897
|
+
background: "rgb(28,29,34)",
|
|
898
|
+
border: "1px solid rgba(255,255,255,0.10)",
|
|
899
|
+
boxShadow: "0 8px 24px rgba(0,0,0,0.45)",
|
|
900
|
+
whiteSpace: "normal",
|
|
901
|
+
pointerEvents: "none"
|
|
902
|
+
};
|
|
903
|
+
var WARNING = "#ffc107";
|
|
904
|
+
var ERROR = "#ff6b6b";
|
|
905
|
+
var GHOST = {
|
|
906
|
+
...PILL,
|
|
907
|
+
flex: "0 0 auto",
|
|
908
|
+
padding: "3px 8px",
|
|
909
|
+
fontSize: "11px"
|
|
910
|
+
};
|
|
911
|
+
var HISTORY_ROW_PX = 40;
|
|
912
|
+
var HISTORY_VISIBLE = 5;
|
|
913
|
+
var JOBS_MAX_PX = 172;
|
|
914
|
+
var HISTORY_LIST = {
|
|
915
|
+
maxHeight: `${HISTORY_ROW_PX * HISTORY_VISIBLE}px`,
|
|
916
|
+
overflowY: "auto"
|
|
917
|
+
};
|
|
918
|
+
var SECTION_LABEL = {
|
|
919
|
+
display: "block",
|
|
920
|
+
marginTop: "10px",
|
|
921
|
+
marginBottom: "4px",
|
|
922
|
+
fontSize: "10px",
|
|
923
|
+
letterSpacing: "0.08em",
|
|
924
|
+
textTransform: "uppercase",
|
|
925
|
+
opacity: "0.4"
|
|
926
|
+
};
|
|
927
|
+
var OUTPUT_READOUT = {
|
|
928
|
+
opacity: "0.6",
|
|
929
|
+
fontSize: "11px",
|
|
930
|
+
display: "block",
|
|
931
|
+
padding: "4px 0",
|
|
932
|
+
whiteSpace: "nowrap"
|
|
933
|
+
};
|
|
934
|
+
var WATCH_MS = 250;
|
|
935
|
+
var SCENE_SETTLE_MS = 250;
|
|
936
|
+
var SCENE_MAX_WAIT_MS = 1500;
|
|
937
|
+
var VIEW_SETTLE_MS = 600;
|
|
938
|
+
var RETRY_MS = 1e4;
|
|
939
|
+
var FOCUS_FRESH_MS = 5e3;
|
|
940
|
+
var LOUD_ESTIMATE_ERRORS = {
|
|
941
|
+
Unauthorized: { label: "Check your key:", url: "https://bakery3.com/keys" },
|
|
942
|
+
TokenExpired: { label: "Check your key:", url: "https://bakery3.com/keys" },
|
|
943
|
+
OriginNotAllowed: {
|
|
944
|
+
label: "Check your key:",
|
|
945
|
+
url: "https://bakery3.com/keys"
|
|
946
|
+
},
|
|
947
|
+
InsufficientBalance: {
|
|
948
|
+
label: "Add credit:",
|
|
949
|
+
url: "https://bakery3.com/billing"
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
function mountDevPanel(options) {
|
|
953
|
+
const host = options.container ?? document.body;
|
|
954
|
+
const tabs = availableTabs(options);
|
|
955
|
+
if (!tabs.length) {
|
|
956
|
+
return inertPanel();
|
|
957
|
+
}
|
|
958
|
+
let open = options.defaultOpen ?? false;
|
|
959
|
+
let tab = options.defaultTab && tabs.includes(options.defaultTab) ? options.defaultTab : tabs[0];
|
|
960
|
+
let disposed = false;
|
|
961
|
+
const ownHistory = !options.history;
|
|
962
|
+
const root = div({ position: "fixed", zIndex: "2147483000" });
|
|
963
|
+
Object.assign(root.style, CORNERS[options.position ?? "bottom-right"]);
|
|
964
|
+
const fab = button("", {
|
|
965
|
+
width: "48px",
|
|
966
|
+
height: "48px",
|
|
967
|
+
padding: "0",
|
|
968
|
+
borderRadius: "50%",
|
|
969
|
+
background: INK,
|
|
970
|
+
border: "1px solid rgba(255,255,255,0.10)",
|
|
971
|
+
boxShadow: "0 6px 20px rgba(0,0,0,0.45)",
|
|
972
|
+
cursor: "pointer",
|
|
973
|
+
display: "flex",
|
|
974
|
+
alignItems: "center",
|
|
975
|
+
justifyContent: "center",
|
|
976
|
+
transition: "box-shadow 120ms ease, border-color 120ms ease"
|
|
977
|
+
});
|
|
978
|
+
fab.title = "Bakery3: render, video, bake";
|
|
979
|
+
fab.appendChild(brandMark(46));
|
|
980
|
+
fab.onmouseenter = () => {
|
|
981
|
+
fab.style.boxShadow = `0 0 0 3px rgba(${BLUE_RGB},0.28), 0 6px 20px rgba(0,0,0,0.45)`;
|
|
982
|
+
fab.style.borderColor = `rgba(${BLUE_RGB},0.6)`;
|
|
983
|
+
};
|
|
984
|
+
fab.onmouseleave = () => {
|
|
985
|
+
fab.style.boxShadow = "0 6px 20px rgba(0,0,0,0.45)";
|
|
986
|
+
fab.style.borderColor = "rgba(255,255,255,0.10)";
|
|
987
|
+
};
|
|
988
|
+
fab.onclick = () => panel.open();
|
|
989
|
+
const card = div(CARD);
|
|
990
|
+
const header = div({
|
|
991
|
+
display: "flex",
|
|
992
|
+
alignItems: "center",
|
|
993
|
+
gap: "2px",
|
|
994
|
+
padding: "0 12px",
|
|
995
|
+
borderBottom: "1px solid rgba(255,255,255,0.08)",
|
|
996
|
+
flex: "0 0 auto"
|
|
997
|
+
});
|
|
998
|
+
const headerMark = brandMark(40);
|
|
999
|
+
headerMark.style.marginRight = "6px";
|
|
1000
|
+
header.appendChild(headerMark);
|
|
1001
|
+
if (options.title) {
|
|
1002
|
+
header.appendChild(
|
|
1003
|
+
span(options.title, {
|
|
1004
|
+
fontWeight: "600",
|
|
1005
|
+
letterSpacing: "0.02em",
|
|
1006
|
+
marginRight: "8px"
|
|
1007
|
+
})
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
const tabButtons = /* @__PURE__ */ new Map();
|
|
1011
|
+
for (const name of tabs) {
|
|
1012
|
+
const element = button(
|
|
1013
|
+
name === "bake" ? "Bake" : name === "video" ? "Video" : "Render",
|
|
1014
|
+
TAB
|
|
1015
|
+
);
|
|
1016
|
+
element.onclick = () => panel.setTab(name);
|
|
1017
|
+
tabButtons.set(name, element);
|
|
1018
|
+
header.appendChild(element);
|
|
1019
|
+
}
|
|
1020
|
+
const collapse = button("\u25BE", {
|
|
1021
|
+
...GHOST,
|
|
1022
|
+
marginLeft: "auto",
|
|
1023
|
+
width: "24px"
|
|
1024
|
+
});
|
|
1025
|
+
collapse.title = "collapse";
|
|
1026
|
+
collapse.onclick = () => panel.close();
|
|
1027
|
+
header.appendChild(collapse);
|
|
1028
|
+
const bodies = div({
|
|
1029
|
+
flex: "1 1 auto",
|
|
1030
|
+
overflowY: "auto",
|
|
1031
|
+
padding: "0 12px 4px",
|
|
1032
|
+
borderTop: "1px solid rgba(255,255,255,0.08)"
|
|
1033
|
+
});
|
|
1034
|
+
const progressRow = div({
|
|
1035
|
+
display: "none",
|
|
1036
|
+
padding: "6px 12px 0",
|
|
1037
|
+
flex: "0 0 auto"
|
|
1038
|
+
});
|
|
1039
|
+
const progressText = span("", {
|
|
1040
|
+
fontSize: "11px",
|
|
1041
|
+
opacity: "0.8",
|
|
1042
|
+
display: "block"
|
|
1043
|
+
});
|
|
1044
|
+
const progressTrack = div({
|
|
1045
|
+
height: "3px",
|
|
1046
|
+
marginTop: "4px",
|
|
1047
|
+
borderRadius: "2px",
|
|
1048
|
+
background: "rgba(255,255,255,0.08)",
|
|
1049
|
+
overflow: "hidden"
|
|
1050
|
+
});
|
|
1051
|
+
const progressFill = div({
|
|
1052
|
+
height: "100%",
|
|
1053
|
+
width: "0%",
|
|
1054
|
+
background: SWEEP,
|
|
1055
|
+
transition: "width 160ms ease"
|
|
1056
|
+
});
|
|
1057
|
+
progressTrack.appendChild(progressFill);
|
|
1058
|
+
progressRow.append(progressText, progressTrack);
|
|
1059
|
+
const jobsRow = div({
|
|
1060
|
+
display: "none",
|
|
1061
|
+
margin: "14px 12px 0",
|
|
1062
|
+
padding: "10px 0 0",
|
|
1063
|
+
borderTop: "1px solid rgba(255,255,255,0.12)",
|
|
1064
|
+
flex: "0 0 auto"
|
|
1065
|
+
});
|
|
1066
|
+
const jobsList = div({ maxHeight: `${JOBS_MAX_PX}px`, overflowY: "auto" });
|
|
1067
|
+
jobsRow.append(span("jobs", { ...SECTION_LABEL, marginTop: "0" }), jobsList);
|
|
1068
|
+
const statusRow = div({
|
|
1069
|
+
display: "none",
|
|
1070
|
+
padding: "6px 12px 0",
|
|
1071
|
+
fontSize: "11px",
|
|
1072
|
+
whiteSpace: "pre-wrap",
|
|
1073
|
+
flex: "0 0 auto"
|
|
1074
|
+
});
|
|
1075
|
+
const actionBar = div({
|
|
1076
|
+
display: "flex",
|
|
1077
|
+
alignItems: "center",
|
|
1078
|
+
gap: "8px",
|
|
1079
|
+
padding: "10px 12px 0",
|
|
1080
|
+
flex: "0 0 auto"
|
|
1081
|
+
});
|
|
1082
|
+
const action = button("Render", { ...CTA, flex: "1 1 0", minWidth: "0" });
|
|
1083
|
+
const meta = div(META);
|
|
1084
|
+
const compat = button("", COMPAT);
|
|
1085
|
+
const compatDot = span("", {
|
|
1086
|
+
flex: "0 0 auto",
|
|
1087
|
+
width: "6px",
|
|
1088
|
+
height: "6px",
|
|
1089
|
+
borderRadius: "50%",
|
|
1090
|
+
background: BLUE
|
|
1091
|
+
});
|
|
1092
|
+
const compatLabel = span("", {
|
|
1093
|
+
minWidth: "0",
|
|
1094
|
+
overflow: "hidden",
|
|
1095
|
+
textOverflow: "ellipsis",
|
|
1096
|
+
textUnderlineOffset: "2px"
|
|
1097
|
+
});
|
|
1098
|
+
compat.append(compatDot, compatLabel);
|
|
1099
|
+
compat.style.display = "none";
|
|
1100
|
+
const price = span("", PRICE);
|
|
1101
|
+
const breakdown = div(BREAKDOWN);
|
|
1102
|
+
meta.append(compat, price, breakdown);
|
|
1103
|
+
actionBar.append(action, meta);
|
|
1104
|
+
const issuesRow = div({
|
|
1105
|
+
display: "none",
|
|
1106
|
+
margin: "10px 12px 0",
|
|
1107
|
+
padding: "8px 10px",
|
|
1108
|
+
borderRadius: "8px",
|
|
1109
|
+
background: "rgba(255,255,255,0.04)",
|
|
1110
|
+
flex: "0 0 auto"
|
|
1111
|
+
});
|
|
1112
|
+
const issuesClose = button("\xD7", { ...GHOST, width: "22px" });
|
|
1113
|
+
issuesClose.title = "close";
|
|
1114
|
+
const issuesHead = div({
|
|
1115
|
+
display: "flex",
|
|
1116
|
+
alignItems: "center",
|
|
1117
|
+
justifyContent: "space-between",
|
|
1118
|
+
marginBottom: "6px"
|
|
1119
|
+
});
|
|
1120
|
+
const issuesTitle = span("", { fontSize: "11px", opacity: "0.8" });
|
|
1121
|
+
issuesHead.append(issuesTitle, issuesClose);
|
|
1122
|
+
const issuesList = div({ maxHeight: "180px", overflowY: "auto" });
|
|
1123
|
+
issuesRow.append(issuesHead, issuesList);
|
|
1124
|
+
const jobsFoot = div({ flex: "0 0 auto", height: "10px" });
|
|
1125
|
+
const tabSettings = div({ flex: "0 0 auto", padding: "0 12px" });
|
|
1126
|
+
card.append(
|
|
1127
|
+
header,
|
|
1128
|
+
tabSettings,
|
|
1129
|
+
actionBar,
|
|
1130
|
+
issuesRow,
|
|
1131
|
+
progressRow,
|
|
1132
|
+
statusRow,
|
|
1133
|
+
jobsRow,
|
|
1134
|
+
jobsFoot,
|
|
1135
|
+
bodies
|
|
1136
|
+
);
|
|
1137
|
+
root.append(fab, card);
|
|
1138
|
+
host.appendChild(root);
|
|
1139
|
+
const renderTab = options.render ? buildRenderTab(options.render) : null;
|
|
1140
|
+
const videoTab = options.video ? buildVideoTab(options.video) : null;
|
|
1141
|
+
const bakeTab = options.bake ? buildBakeTab(options.bake) : null;
|
|
1142
|
+
for (const body of [renderTab, videoTab, bakeTab]) {
|
|
1143
|
+
if (!body) continue;
|
|
1144
|
+
tabSettings.appendChild(body.settings);
|
|
1145
|
+
bodies.appendChild(body.element);
|
|
1146
|
+
}
|
|
1147
|
+
let costToken = 0;
|
|
1148
|
+
let lastCostAt = 0;
|
|
1149
|
+
let pricedKey;
|
|
1150
|
+
let retryAt;
|
|
1151
|
+
function estimateRequest() {
|
|
1152
|
+
if (tab === "render" && renderTab) return renderTab.estimateRequest();
|
|
1153
|
+
if (tab === "video" && videoTab) return videoTab.estimateRequest();
|
|
1154
|
+
if (tab === "bake" && bakeTab) return bakeTab.estimateRequest();
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
let loudEstimateError = null;
|
|
1158
|
+
let statusFromEstimate = false;
|
|
1159
|
+
async function refreshCost(force = false) {
|
|
1160
|
+
if (!options.estimate || disposed) return;
|
|
1161
|
+
renderTab?.paintOutput();
|
|
1162
|
+
videoTab?.paintOutput();
|
|
1163
|
+
const request = estimateRequest();
|
|
1164
|
+
if (!request) return;
|
|
1165
|
+
const key = [JSON.stringify(request), seenScene, seenView].join("\n");
|
|
1166
|
+
if (!force && key === pricedKey) return;
|
|
1167
|
+
pricedKey = key;
|
|
1168
|
+
retryAt = void 0;
|
|
1169
|
+
const token = ++costToken;
|
|
1170
|
+
lastCostAt = Date.now();
|
|
1171
|
+
if (!price.textContent) price.textContent = "\u2026";
|
|
1172
|
+
price.style.opacity = "0.55";
|
|
1173
|
+
try {
|
|
1174
|
+
const estimate = await options.estimate(request);
|
|
1175
|
+
if (token !== costToken || disposed) return;
|
|
1176
|
+
price.style.opacity = "1";
|
|
1177
|
+
settleLoudEstimateError();
|
|
1178
|
+
if (!estimate) {
|
|
1179
|
+
price.textContent = "";
|
|
1180
|
+
breakdown.replaceChildren();
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
price.textContent = (estimate.approximate === false ? "" : "\u2248 ") + usd(estimate.usd);
|
|
1184
|
+
breakdown.replaceChildren(...breakdownLines(request, estimate));
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
if (token !== costToken || disposed) return;
|
|
1187
|
+
price.style.opacity = "1";
|
|
1188
|
+
price.textContent = "";
|
|
1189
|
+
breakdown.replaceChildren();
|
|
1190
|
+
if (!sayLoudEstimateError(error)) retryAt = Date.now() + RETRY_MS;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
function sayLoudEstimateError(error) {
|
|
1194
|
+
const code = errorCode(error);
|
|
1195
|
+
const link = code ? LOUD_ESTIMATE_ERRORS[code] : void 0;
|
|
1196
|
+
if (!link) return false;
|
|
1197
|
+
const text = describe(error);
|
|
1198
|
+
const key = `${code}
|
|
1199
|
+
${text}`;
|
|
1200
|
+
if (key === loudEstimateError) return true;
|
|
1201
|
+
loudEstimateError = key;
|
|
1202
|
+
panel.setStatus(
|
|
1203
|
+
text.includes(link.url) ? text : `${text}
|
|
1204
|
+
|
|
1205
|
+
${link.label} ${link.url}`,
|
|
1206
|
+
"error"
|
|
1207
|
+
);
|
|
1208
|
+
statusFromEstimate = true;
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
function settleLoudEstimateError() {
|
|
1212
|
+
if (loudEstimateError === null) return;
|
|
1213
|
+
loudEstimateError = null;
|
|
1214
|
+
if (statusFromEstimate) panel.setStatus("");
|
|
1215
|
+
}
|
|
1216
|
+
price.onmouseenter = () => {
|
|
1217
|
+
if (breakdown.childNodes.length) breakdown.style.display = "block";
|
|
1218
|
+
};
|
|
1219
|
+
price.onmouseleave = () => {
|
|
1220
|
+
breakdown.style.display = "none";
|
|
1221
|
+
};
|
|
1222
|
+
let report = null;
|
|
1223
|
+
let issuesOpen = false;
|
|
1224
|
+
function paintCompatibility() {
|
|
1225
|
+
report = options.compatibility?.() ?? null;
|
|
1226
|
+
if (!report) {
|
|
1227
|
+
compat.style.display = "none";
|
|
1228
|
+
closeIssues();
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
const counts = { error: 0, warning: 0, info: 0 };
|
|
1232
|
+
for (const issue of report.issues) {
|
|
1233
|
+
counts[issue.severity] = (counts[issue.severity] ?? 0) + 1;
|
|
1234
|
+
}
|
|
1235
|
+
compat.style.display = "flex";
|
|
1236
|
+
compatLabel.textContent = `${Math.round(report.score * 100)}% compatible`;
|
|
1237
|
+
compatDot.style.background = counts.error ? ERROR : counts.warning ? WARNING : BLUE;
|
|
1238
|
+
const openable = report.issues.length > 0;
|
|
1239
|
+
compat.style.cursor = openable ? "pointer" : "default";
|
|
1240
|
+
compat.title = openable ? `${issueCount(report.issues.length)} in this scene. Click to see which objects.` : "Everything in this scene renders as you see it.";
|
|
1241
|
+
if (!issuesOpen) return;
|
|
1242
|
+
if (openable) paintIssues();
|
|
1243
|
+
else closeIssues();
|
|
1244
|
+
}
|
|
1245
|
+
function paintIssues() {
|
|
1246
|
+
const issues = [...report?.issues ?? []].sort(
|
|
1247
|
+
(a, b) => severityRank(a.severity) - severityRank(b.severity)
|
|
1248
|
+
);
|
|
1249
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1250
|
+
for (const issue of issues) {
|
|
1251
|
+
const where = issue.path ?? "";
|
|
1252
|
+
groups.set(where, [...groups.get(where) ?? [], issue]);
|
|
1253
|
+
}
|
|
1254
|
+
issuesTitle.textContent = "What renders differently";
|
|
1255
|
+
issuesList.replaceChildren(
|
|
1256
|
+
...[...groups].map(([path, list]) => issueGroup(path, list))
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
function openIssues() {
|
|
1260
|
+
issuesOpen = true;
|
|
1261
|
+
paintIssues();
|
|
1262
|
+
issuesRow.style.display = "block";
|
|
1263
|
+
compat.style.opacity = "1";
|
|
1264
|
+
}
|
|
1265
|
+
function closeIssues() {
|
|
1266
|
+
issuesOpen = false;
|
|
1267
|
+
issuesRow.style.display = "none";
|
|
1268
|
+
compat.style.opacity = "0.65";
|
|
1269
|
+
}
|
|
1270
|
+
compat.onclick = () => {
|
|
1271
|
+
if (!report?.issues.length) return;
|
|
1272
|
+
if (issuesOpen) closeIssues();
|
|
1273
|
+
else openIssues();
|
|
1274
|
+
};
|
|
1275
|
+
compat.onmouseenter = () => {
|
|
1276
|
+
if (!report?.issues.length) return;
|
|
1277
|
+
compat.style.opacity = "1";
|
|
1278
|
+
compatLabel.style.textDecoration = "underline";
|
|
1279
|
+
};
|
|
1280
|
+
compat.onmouseleave = () => {
|
|
1281
|
+
compat.style.opacity = issuesOpen ? "1" : "0.65";
|
|
1282
|
+
compatLabel.style.textDecoration = "none";
|
|
1283
|
+
};
|
|
1284
|
+
issuesClose.onclick = () => closeIssues();
|
|
1285
|
+
let seenScene;
|
|
1286
|
+
let seenView;
|
|
1287
|
+
let seenRequest = "";
|
|
1288
|
+
let sceneDirtySince;
|
|
1289
|
+
let sceneMovedAt = 0;
|
|
1290
|
+
let viewMovedAt;
|
|
1291
|
+
let watching = false;
|
|
1292
|
+
let cancelLook;
|
|
1293
|
+
const page = typeof window === "undefined" ? void 0 : window;
|
|
1294
|
+
function startWatching() {
|
|
1295
|
+
if (watching || disposed) return;
|
|
1296
|
+
watching = true;
|
|
1297
|
+
seenScene = options.sceneVersion?.();
|
|
1298
|
+
seenView = options.viewVersion?.();
|
|
1299
|
+
seenRequest = requestShape();
|
|
1300
|
+
sceneDirtySince = viewMovedAt = void 0;
|
|
1301
|
+
const next = () => {
|
|
1302
|
+
cancelLook = afterAWhile(() => {
|
|
1303
|
+
look(Date.now());
|
|
1304
|
+
if (watching) next();
|
|
1305
|
+
});
|
|
1306
|
+
};
|
|
1307
|
+
next();
|
|
1308
|
+
page?.addEventListener("focus", onFocus);
|
|
1309
|
+
}
|
|
1310
|
+
function stopWatching() {
|
|
1311
|
+
if (!watching) return;
|
|
1312
|
+
watching = false;
|
|
1313
|
+
cancelLook?.();
|
|
1314
|
+
cancelLook = void 0;
|
|
1315
|
+
page?.removeEventListener("focus", onFocus);
|
|
1316
|
+
}
|
|
1317
|
+
function requestShape() {
|
|
1318
|
+
return tab === "bake" ? "" : JSON.stringify(estimateRequest());
|
|
1319
|
+
}
|
|
1320
|
+
function look(now) {
|
|
1321
|
+
renderTab?.paintOutput();
|
|
1322
|
+
videoTab?.paintOutput();
|
|
1323
|
+
const request = requestShape();
|
|
1324
|
+
const scene = options.sceneVersion?.();
|
|
1325
|
+
const view = options.viewVersion?.();
|
|
1326
|
+
if (scene !== seenScene) {
|
|
1327
|
+
seenScene = scene;
|
|
1328
|
+
sceneMovedAt = now;
|
|
1329
|
+
sceneDirtySince ??= now;
|
|
1330
|
+
}
|
|
1331
|
+
if (view !== seenView || request !== seenRequest) {
|
|
1332
|
+
seenView = view;
|
|
1333
|
+
seenRequest = request;
|
|
1334
|
+
viewMovedAt = now;
|
|
1335
|
+
}
|
|
1336
|
+
if (sceneDirtySince !== void 0) {
|
|
1337
|
+
if (now - sceneMovedAt < SCENE_SETTLE_MS && now - sceneDirtySince < SCENE_MAX_WAIT_MS) {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
sceneDirtySince = void 0;
|
|
1341
|
+
bakeTab?.paintCandidates();
|
|
1342
|
+
paintCompatibility();
|
|
1343
|
+
videoTab?.reconcile();
|
|
1344
|
+
void refreshCost();
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
if (viewMovedAt !== void 0) {
|
|
1348
|
+
if (now - viewMovedAt < VIEW_SETTLE_MS) return;
|
|
1349
|
+
viewMovedAt = void 0;
|
|
1350
|
+
void refreshCost();
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
if (retryAt !== void 0 && now >= retryAt) void refreshCost(true);
|
|
1354
|
+
}
|
|
1355
|
+
function onFocus() {
|
|
1356
|
+
if (open && Date.now() - lastCostAt > FOCUS_FRESH_MS) {
|
|
1357
|
+
void refreshCost(true);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
let historyEntries = [];
|
|
1361
|
+
let historyToken = 0;
|
|
1362
|
+
let openedAt = open ? Date.now() : Infinity;
|
|
1363
|
+
const seenHistory = /* @__PURE__ */ new Set();
|
|
1364
|
+
let sessionEntries = ownHistory ? readSessionHistory() : [];
|
|
1365
|
+
function record(entry) {
|
|
1366
|
+
if (!ownHistory || disposed) return;
|
|
1367
|
+
sessionEntries = [
|
|
1368
|
+
entry,
|
|
1369
|
+
...sessionEntries.filter((each) => each.id !== entry.id)
|
|
1370
|
+
].slice(0, SESSION_HISTORY_MAX);
|
|
1371
|
+
writeSessionHistory(sessionEntries);
|
|
1372
|
+
historyEntries = liveEntries(sessionEntries);
|
|
1373
|
+
paintHistory();
|
|
1374
|
+
}
|
|
1375
|
+
function recordResult(id, result, kind) {
|
|
1376
|
+
const isVideo = kind === "video" || /^(mp4|webm|mov)$/.test(result.format ?? "");
|
|
1377
|
+
const createdAt = Date.now();
|
|
1378
|
+
record({
|
|
1379
|
+
id: id ?? `${isVideo ? "video" : "render"}-${createdAt}`,
|
|
1380
|
+
kind: isVideo ? "video" : "render",
|
|
1381
|
+
createdAt: new Date(createdAt).toISOString(),
|
|
1382
|
+
expiresAt: new Date(
|
|
1383
|
+
result.expiresAt ?? createdAt + URL_LIFETIME_MS
|
|
1384
|
+
).toISOString(),
|
|
1385
|
+
meta: {
|
|
1386
|
+
...result.width ? { width: result.width } : {},
|
|
1387
|
+
...result.height ? { height: result.height } : {},
|
|
1388
|
+
...result.format ? { format: result.format } : {},
|
|
1389
|
+
...result.frames ? { frames: result.frames } : {},
|
|
1390
|
+
...result.quality ? { quality: result.quality } : {},
|
|
1391
|
+
...result.cost !== void 0 ? { cost: result.cost } : {}
|
|
1392
|
+
},
|
|
1393
|
+
...isVideo ? {} : { thumbnail: result.url },
|
|
1394
|
+
artifact: result.url
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
async function refreshHistory() {
|
|
1398
|
+
if (disposed) return;
|
|
1399
|
+
if (ownHistory) {
|
|
1400
|
+
historyEntries = liveEntries(sessionEntries);
|
|
1401
|
+
paintHistory();
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
if (!options.history) return;
|
|
1405
|
+
const token = ++historyToken;
|
|
1406
|
+
try {
|
|
1407
|
+
const entries = await options.history.list();
|
|
1408
|
+
if (token !== historyToken || disposed) return;
|
|
1409
|
+
historyEntries = entries;
|
|
1410
|
+
showFreshEntries(entries);
|
|
1411
|
+
} catch {
|
|
1412
|
+
historyEntries = [];
|
|
1413
|
+
}
|
|
1414
|
+
paintHistory();
|
|
1415
|
+
}
|
|
1416
|
+
function showFreshEntries(entries) {
|
|
1417
|
+
if (!open) return;
|
|
1418
|
+
const fresh = (kind) => entries.find(
|
|
1419
|
+
(entry) => entry.kind === kind && !seenHistory.has(entry.id) && Date.parse(entry.createdAt) >= openedAt
|
|
1420
|
+
);
|
|
1421
|
+
const render = fresh("render");
|
|
1422
|
+
const video = fresh("video");
|
|
1423
|
+
if (render) renderTab?.showEntry(render);
|
|
1424
|
+
if (video) videoTab?.showEntry(video);
|
|
1425
|
+
for (const entry of entries) seenHistory.add(entry.id);
|
|
1426
|
+
}
|
|
1427
|
+
function paintHistory() {
|
|
1428
|
+
renderTab?.paintHistory(
|
|
1429
|
+
historyEntries.filter((entry) => entry.kind === "render")
|
|
1430
|
+
);
|
|
1431
|
+
videoTab?.paintHistory(
|
|
1432
|
+
historyEntries.filter((entry) => entry.kind === "video")
|
|
1433
|
+
);
|
|
1434
|
+
bakeTab?.paintHistory(
|
|
1435
|
+
historyEntries.filter((entry) => entry.kind === "bake")
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
let hostJobs = [];
|
|
1439
|
+
const trackedJobs = /* @__PURE__ */ new Map();
|
|
1440
|
+
const lingering = /* @__PURE__ */ new Set();
|
|
1441
|
+
let actionsInFlight = 0;
|
|
1442
|
+
let pendingRows = 0;
|
|
1443
|
+
let activeJobIds = /* @__PURE__ */ new Set();
|
|
1444
|
+
let rowsShown = 0;
|
|
1445
|
+
function paintJobs() {
|
|
1446
|
+
const jobs = [
|
|
1447
|
+
...[...trackedJobs].filter(
|
|
1448
|
+
([key, job]) => lingering.has(key) || !TERMINAL_JOB_STATES.has(job.state)
|
|
1449
|
+
).map(([, job]) => job),
|
|
1450
|
+
...hostJobs.filter((job) => !TERMINAL_JOB_STATES.has(job.state))
|
|
1451
|
+
];
|
|
1452
|
+
const ids = new Set(jobs.map((job) => job.id));
|
|
1453
|
+
const ended = [...activeJobIds].some((id) => !ids.has(id));
|
|
1454
|
+
activeJobIds = ids;
|
|
1455
|
+
rowsShown = jobs.length;
|
|
1456
|
+
jobsList.replaceChildren(...jobs.map(jobRow));
|
|
1457
|
+
jobsRow.style.display = jobs.length ? "block" : "none";
|
|
1458
|
+
paintProgress();
|
|
1459
|
+
if (ended) void refreshHistory();
|
|
1460
|
+
}
|
|
1461
|
+
function retire(key, entry) {
|
|
1462
|
+
if (entry.state === "completed" && actionsInFlight > 0) lingering.add(key);
|
|
1463
|
+
else trackedJobs.delete(key);
|
|
1464
|
+
paintJobs();
|
|
1465
|
+
}
|
|
1466
|
+
function releaseLingering() {
|
|
1467
|
+
if (!lingering.size) return;
|
|
1468
|
+
for (const key of lingering) trackedJobs.delete(key);
|
|
1469
|
+
lingering.clear();
|
|
1470
|
+
paintJobs();
|
|
1471
|
+
}
|
|
1472
|
+
function attach(entry, job, recordOnComplete) {
|
|
1473
|
+
entry.id = job.id;
|
|
1474
|
+
entry.state = job.status;
|
|
1475
|
+
entry.progress = job.progress;
|
|
1476
|
+
delete entry.detail;
|
|
1477
|
+
if (job.previewUrl) entry.previewUrl = job.previewUrl;
|
|
1478
|
+
if (job.cancel) {
|
|
1479
|
+
entry.onCancel = () => {
|
|
1480
|
+
const failed = (error) => panel.setStatus(describe(error), "error");
|
|
1481
|
+
try {
|
|
1482
|
+
void Promise.resolve(job.cancel()).catch(failed);
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
failed(error);
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
return [
|
|
1489
|
+
job.on("progress", (payload) => {
|
|
1490
|
+
if (payload.state) entry.state = payload.state;
|
|
1491
|
+
if (payload.percent !== void 0)
|
|
1492
|
+
entry.progress = payload.percent / 100;
|
|
1493
|
+
paintJobs();
|
|
1494
|
+
}),
|
|
1495
|
+
job.on("preview", (payload) => {
|
|
1496
|
+
if (payload.url) entry.previewUrl = payload.url;
|
|
1497
|
+
paintJobs();
|
|
1498
|
+
}),
|
|
1499
|
+
job.on("complete", (payload) => {
|
|
1500
|
+
entry.state = "completed";
|
|
1501
|
+
entry.progress = 1;
|
|
1502
|
+
const result = payload.result;
|
|
1503
|
+
if (result?.url) {
|
|
1504
|
+
entry.resultUrl = result.url;
|
|
1505
|
+
if (recordOnComplete) recordResult(job.id, result);
|
|
1506
|
+
}
|
|
1507
|
+
paintJobs();
|
|
1508
|
+
}),
|
|
1509
|
+
job.on("failed", (payload) => {
|
|
1510
|
+
entry.state = "failed";
|
|
1511
|
+
if (payload.error?.message) entry.detail = payload.error.message;
|
|
1512
|
+
panel.setStatus(
|
|
1513
|
+
`${entry.label} failed${entry.detail ? `: ${entry.detail}` : ""}`,
|
|
1514
|
+
"error"
|
|
1515
|
+
);
|
|
1516
|
+
paintJobs();
|
|
1517
|
+
}),
|
|
1518
|
+
job.on("cancelled", () => {
|
|
1519
|
+
entry.state = "cancelled";
|
|
1520
|
+
paintJobs();
|
|
1521
|
+
})
|
|
1522
|
+
];
|
|
1523
|
+
}
|
|
1524
|
+
let progressLabel = null;
|
|
1525
|
+
let progressFraction;
|
|
1526
|
+
function paintProgress() {
|
|
1527
|
+
if (progressLabel === null || rowsShown > 0) {
|
|
1528
|
+
progressRow.style.display = "none";
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
progressRow.style.display = "block";
|
|
1532
|
+
progressText.textContent = progressLabel;
|
|
1533
|
+
progressFill.style.width = progressFraction === void 0 ? "100%" : `${Math.round(Math.max(0, Math.min(1, progressFraction)) * 100)}%`;
|
|
1534
|
+
progressFill.style.opacity = progressFraction === void 0 ? "0.35" : "1";
|
|
1535
|
+
}
|
|
1536
|
+
function historySection(list) {
|
|
1537
|
+
const section = div({});
|
|
1538
|
+
section.append(span("recent", SECTION_LABEL), list);
|
|
1539
|
+
section.style.display = ownHistory ? "none" : "block";
|
|
1540
|
+
return section;
|
|
1541
|
+
}
|
|
1542
|
+
function paintSection(section, rows) {
|
|
1543
|
+
section.style.display = !ownHistory || rows ? "block" : "none";
|
|
1544
|
+
}
|
|
1545
|
+
function downloadButton(entry, part) {
|
|
1546
|
+
const download = options.history?.download;
|
|
1547
|
+
if (!download) return null;
|
|
1548
|
+
const element = button(part === "glb" ? "GLB" : "Download", GHOST);
|
|
1549
|
+
element.title = `download ${part === "glb" ? "the portable GLB" : "this artifact"}`;
|
|
1550
|
+
element.onclick = async () => {
|
|
1551
|
+
try {
|
|
1552
|
+
await download(entry, part);
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
panel.setStatus(describe(error), "error");
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
return element;
|
|
1558
|
+
}
|
|
1559
|
+
let baking = false;
|
|
1560
|
+
function paintAction() {
|
|
1561
|
+
const busy = tab === "bake" && baking;
|
|
1562
|
+
action.disabled = busy;
|
|
1563
|
+
action.style.opacity = busy ? "0.6" : "1";
|
|
1564
|
+
}
|
|
1565
|
+
async function runAction() {
|
|
1566
|
+
const kind = tab;
|
|
1567
|
+
if (kind === "bake") {
|
|
1568
|
+
if (baking) return;
|
|
1569
|
+
baking = true;
|
|
1570
|
+
paintAction();
|
|
1571
|
+
}
|
|
1572
|
+
panel.setStatus("");
|
|
1573
|
+
actionsInFlight += 1;
|
|
1574
|
+
try {
|
|
1575
|
+
try {
|
|
1576
|
+
if (kind === "render" && renderTab) await renderTab.run();
|
|
1577
|
+
else if (kind === "video" && videoTab) await videoTab.run();
|
|
1578
|
+
else if (kind === "bake" && bakeTab) await bakeTab.run();
|
|
1579
|
+
} finally {
|
|
1580
|
+
actionsInFlight -= 1;
|
|
1581
|
+
releaseLingering();
|
|
1582
|
+
}
|
|
1583
|
+
await refreshHistory();
|
|
1584
|
+
void refreshCost(true);
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
panel.setStatus(describe(error), "error");
|
|
1587
|
+
} finally {
|
|
1588
|
+
if (kind === "bake") {
|
|
1589
|
+
baking = false;
|
|
1590
|
+
paintAction();
|
|
1591
|
+
}
|
|
1592
|
+
if (actionsInFlight === 0) panel.setProgress(null);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
action.onclick = (event) => {
|
|
1596
|
+
if (event?.detail > 1) return;
|
|
1597
|
+
void runAction();
|
|
1598
|
+
};
|
|
1599
|
+
function paintTab() {
|
|
1600
|
+
for (const [name, element] of tabButtons) {
|
|
1601
|
+
const active = name === tab;
|
|
1602
|
+
element.style.opacity = active ? "1" : "0.55";
|
|
1603
|
+
element.style.borderBottomColor = active ? BLUE : "transparent";
|
|
1604
|
+
element.style.fontWeight = active ? "600" : "400";
|
|
1605
|
+
}
|
|
1606
|
+
for (const [name, body] of [
|
|
1607
|
+
["render", renderTab],
|
|
1608
|
+
["video", videoTab],
|
|
1609
|
+
["bake", bakeTab]
|
|
1610
|
+
]) {
|
|
1611
|
+
if (!body) continue;
|
|
1612
|
+
body.element.style.display = tab === name ? "block" : "none";
|
|
1613
|
+
body.settings.style.display = tab === name ? "flex" : "none";
|
|
1614
|
+
}
|
|
1615
|
+
action.textContent = tab === "render" ? "Render" : tab === "video" ? "Render video" : "Bake";
|
|
1616
|
+
paintAction();
|
|
1617
|
+
}
|
|
1618
|
+
function paintOpen() {
|
|
1619
|
+
fab.style.display = open ? "none" : "flex";
|
|
1620
|
+
card.style.display = open ? "flex" : "none";
|
|
1621
|
+
}
|
|
1622
|
+
const panel = {
|
|
1623
|
+
open() {
|
|
1624
|
+
if (disposed || open) return;
|
|
1625
|
+
open = true;
|
|
1626
|
+
openedAt = Date.now();
|
|
1627
|
+
paintOpen();
|
|
1628
|
+
options.onOpenChange?.(true);
|
|
1629
|
+
panel.refresh();
|
|
1630
|
+
startWatching();
|
|
1631
|
+
void refreshCost(true);
|
|
1632
|
+
},
|
|
1633
|
+
close() {
|
|
1634
|
+
if (disposed || !open) return;
|
|
1635
|
+
open = false;
|
|
1636
|
+
openedAt = Infinity;
|
|
1637
|
+
paintOpen();
|
|
1638
|
+
stopWatching();
|
|
1639
|
+
options.onOpenChange?.(false);
|
|
1640
|
+
},
|
|
1641
|
+
setTab(next) {
|
|
1642
|
+
if (disposed || !tabs.includes(next)) return;
|
|
1643
|
+
tab = next;
|
|
1644
|
+
paintTab();
|
|
1645
|
+
void refreshCost();
|
|
1646
|
+
},
|
|
1647
|
+
refresh() {
|
|
1648
|
+
if (disposed) return;
|
|
1649
|
+
paintCompatibility();
|
|
1650
|
+
bakeTab?.paintCandidates();
|
|
1651
|
+
bakeTab?.paintGenerations();
|
|
1652
|
+
videoTab?.refreshTimeline();
|
|
1653
|
+
void refreshHistory();
|
|
1654
|
+
},
|
|
1655
|
+
refreshCost() {
|
|
1656
|
+
void refreshCost(true);
|
|
1657
|
+
},
|
|
1658
|
+
setStatus(message, tone = "info") {
|
|
1659
|
+
statusFromEstimate = false;
|
|
1660
|
+
statusRow.replaceChildren(...linkified(message));
|
|
1661
|
+
statusRow.style.display = message ? "block" : "none";
|
|
1662
|
+
statusRow.style.color = tone === "error" ? ERROR : tone === "ok" ? BLUE : "rgba(244,244,245,0.75)";
|
|
1663
|
+
},
|
|
1664
|
+
setJobs(jobs) {
|
|
1665
|
+
hostJobs = jobs;
|
|
1666
|
+
paintJobs();
|
|
1667
|
+
},
|
|
1668
|
+
trackJob(job, label) {
|
|
1669
|
+
const key = job.id;
|
|
1670
|
+
const entry = {
|
|
1671
|
+
id: job.id,
|
|
1672
|
+
label: label ?? `render ${job.id.slice(0, 8)}`,
|
|
1673
|
+
state: job.status
|
|
1674
|
+
};
|
|
1675
|
+
trackedJobs.set(key, entry);
|
|
1676
|
+
const offs = attach(entry, job, true);
|
|
1677
|
+
paintJobs();
|
|
1678
|
+
let ended = false;
|
|
1679
|
+
return () => {
|
|
1680
|
+
if (ended) return;
|
|
1681
|
+
ended = true;
|
|
1682
|
+
for (const off of offs) off();
|
|
1683
|
+
retire(key, entry);
|
|
1684
|
+
};
|
|
1685
|
+
},
|
|
1686
|
+
startJob(label) {
|
|
1687
|
+
pendingRows += 1;
|
|
1688
|
+
const key = `pending:${pendingRows}`;
|
|
1689
|
+
const entry = {
|
|
1690
|
+
id: key,
|
|
1691
|
+
label,
|
|
1692
|
+
state: "preparing",
|
|
1693
|
+
detail: "capturing the scene"
|
|
1694
|
+
};
|
|
1695
|
+
trackedJobs.set(key, entry);
|
|
1696
|
+
paintJobs();
|
|
1697
|
+
let offs = [];
|
|
1698
|
+
let ended = false;
|
|
1699
|
+
return {
|
|
1700
|
+
update(detail, progress, state) {
|
|
1701
|
+
if (ended || disposed) return;
|
|
1702
|
+
if (state) entry.state = state;
|
|
1703
|
+
entry.detail = detail;
|
|
1704
|
+
if (progress === void 0) delete entry.progress;
|
|
1705
|
+
else entry.progress = progress;
|
|
1706
|
+
paintJobs();
|
|
1707
|
+
},
|
|
1708
|
+
follow(job) {
|
|
1709
|
+
if (ended || disposed) return;
|
|
1710
|
+
for (const off of offs) off();
|
|
1711
|
+
offs = attach(entry, job, false);
|
|
1712
|
+
paintJobs();
|
|
1713
|
+
},
|
|
1714
|
+
end() {
|
|
1715
|
+
if (ended) return;
|
|
1716
|
+
ended = true;
|
|
1717
|
+
for (const off of offs) off();
|
|
1718
|
+
retire(key, entry);
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
},
|
|
1722
|
+
setProgress(text, fraction) {
|
|
1723
|
+
progressLabel = text;
|
|
1724
|
+
progressFraction = fraction;
|
|
1725
|
+
if (text === null) progressFill.style.width = "0%";
|
|
1726
|
+
paintProgress();
|
|
1727
|
+
},
|
|
1728
|
+
dispose() {
|
|
1729
|
+
if (disposed) return;
|
|
1730
|
+
disposed = true;
|
|
1731
|
+
stopWatching();
|
|
1732
|
+
videoTab?.dispose();
|
|
1733
|
+
bakeTab?.dispose();
|
|
1734
|
+
root.remove();
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
paintTab();
|
|
1738
|
+
paintOpen();
|
|
1739
|
+
if (open) {
|
|
1740
|
+
panel.refresh();
|
|
1741
|
+
startWatching();
|
|
1742
|
+
void refreshCost(true);
|
|
1743
|
+
} else {
|
|
1744
|
+
bakeTab?.paintCandidates();
|
|
1745
|
+
bakeTab?.paintGenerations();
|
|
1746
|
+
}
|
|
1747
|
+
return panel;
|
|
1748
|
+
function buildRenderTab(config) {
|
|
1749
|
+
const element = div({ display: "none" });
|
|
1750
|
+
const qualities = config.qualities ?? ["preview", "studio", "ultra"];
|
|
1751
|
+
const sizes = config.sizes ?? [512, 768, 1024, 2048];
|
|
1752
|
+
const qualitySelect = select(qualities.map((value) => [value, value]));
|
|
1753
|
+
qualitySelect.value = config.defaultQuality ?? qualities[0];
|
|
1754
|
+
const sizeSelect = select(
|
|
1755
|
+
sizes.map((value) => [String(value), `${value}`])
|
|
1756
|
+
);
|
|
1757
|
+
sizeSelect.value = String(config.defaultSize ?? sizes[0]);
|
|
1758
|
+
const readout = span("", OUTPUT_READOUT);
|
|
1759
|
+
const output = () => outputSize({}, Number(sizeSelect.value), readAspect(config.aspect));
|
|
1760
|
+
const settings = div({ display: "flex", gap: "6px", marginTop: "10px" });
|
|
1761
|
+
settings.append(
|
|
1762
|
+
labeled("quality", qualitySelect),
|
|
1763
|
+
labeled("long edge", sizeSelect),
|
|
1764
|
+
labeled("output", readout)
|
|
1765
|
+
);
|
|
1766
|
+
const paintOutput = () => {
|
|
1767
|
+
const { width, height } = output();
|
|
1768
|
+
readout.textContent = `${width} \xD7 ${height} png`;
|
|
1769
|
+
};
|
|
1770
|
+
paintOutput();
|
|
1771
|
+
qualitySelect.onchange = () => void refreshCost();
|
|
1772
|
+
sizeSelect.onchange = () => {
|
|
1773
|
+
paintOutput();
|
|
1774
|
+
void refreshCost();
|
|
1775
|
+
};
|
|
1776
|
+
const result = div({ marginTop: "10px" });
|
|
1777
|
+
let shownId;
|
|
1778
|
+
const showOutcome = async (outcome) => {
|
|
1779
|
+
if (!outcome?.url) return;
|
|
1780
|
+
shownId = outcome.entry?.id ?? outcome.id;
|
|
1781
|
+
if (shownId) seenHistory.add(shownId);
|
|
1782
|
+
await paintResult(result, outcome, downloadButton);
|
|
1783
|
+
recordResult(outcome.id, { ...outcome, url: outcome.url }, "render");
|
|
1784
|
+
paintList();
|
|
1785
|
+
};
|
|
1786
|
+
const historyList = div(HISTORY_LIST);
|
|
1787
|
+
const history = historySection(historyList);
|
|
1788
|
+
let listed = [];
|
|
1789
|
+
const paintList = () => {
|
|
1790
|
+
paintHistoryList(historyList, listed, downloadButton, showEntry, shownId);
|
|
1791
|
+
paintSection(history, listed.length);
|
|
1792
|
+
};
|
|
1793
|
+
element.append(result, history);
|
|
1794
|
+
return {
|
|
1795
|
+
element,
|
|
1796
|
+
settings,
|
|
1797
|
+
paintOutput,
|
|
1798
|
+
estimateRequest() {
|
|
1799
|
+
return { kind: "render", quality: qualitySelect.value, ...output() };
|
|
1800
|
+
},
|
|
1801
|
+
async run() {
|
|
1802
|
+
panel.setProgress("Rendering\u2026");
|
|
1803
|
+
const outcome = await config.onRender({
|
|
1804
|
+
quality: qualitySelect.value,
|
|
1805
|
+
...output()
|
|
1806
|
+
});
|
|
1807
|
+
await showOutcome(outcome ?? void 0);
|
|
1808
|
+
},
|
|
1809
|
+
paintHistory(entries) {
|
|
1810
|
+
listed = entries;
|
|
1811
|
+
paintList();
|
|
1812
|
+
},
|
|
1813
|
+
showEntry
|
|
1814
|
+
};
|
|
1815
|
+
function showEntry(entry) {
|
|
1816
|
+
if (entry.id === shownId) return;
|
|
1817
|
+
const url = entry.artifact ?? entry.thumbnail;
|
|
1818
|
+
if (!url) return;
|
|
1819
|
+
shownId = entry.id;
|
|
1820
|
+
paintList();
|
|
1821
|
+
void paintResult(
|
|
1822
|
+
result,
|
|
1823
|
+
{
|
|
1824
|
+
url,
|
|
1825
|
+
meta: describeEntry(entry),
|
|
1826
|
+
isVideo: false,
|
|
1827
|
+
entry,
|
|
1828
|
+
...entry.meta?.width ? { width: entry.meta.width } : {},
|
|
1829
|
+
...entry.meta?.height ? { height: entry.meta.height } : {}
|
|
1830
|
+
},
|
|
1831
|
+
downloadButton
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
function buildVideoTab(config) {
|
|
1836
|
+
const element = div({ display: "none" });
|
|
1837
|
+
const qualities = config.qualities ?? ["preview", "studio", "ultra"];
|
|
1838
|
+
const sizes = config.sizes ?? [256, 512, 768, 1024, 1280, 1920, 2048];
|
|
1839
|
+
const formats = config.formats ?? ["mp4", "webm"];
|
|
1840
|
+
const qualitySelect = select(qualities.map((value) => [value, value]));
|
|
1841
|
+
qualitySelect.value = config.defaultQuality ?? qualities[0];
|
|
1842
|
+
const sizeSelect = select(
|
|
1843
|
+
sizes.map((value) => [String(value), `${value}`])
|
|
1844
|
+
);
|
|
1845
|
+
sizeSelect.value = String(config.defaultSize ?? sizes[0]);
|
|
1846
|
+
const formatSelect = select(formats.map((value) => [value, value]));
|
|
1847
|
+
formatSelect.value = config.defaultFormat ?? formats[0];
|
|
1848
|
+
const readout = span("", OUTPUT_READOUT);
|
|
1849
|
+
const output = () => outputSize({}, Number(sizeSelect.value), readAspect(config.aspect), {
|
|
1850
|
+
even: true
|
|
1851
|
+
});
|
|
1852
|
+
const paintOutput = () => {
|
|
1853
|
+
const { width, height } = output();
|
|
1854
|
+
readout.textContent = `${width} \xD7 ${height}`;
|
|
1855
|
+
};
|
|
1856
|
+
paintOutput();
|
|
1857
|
+
const settings = div({ display: "flex", gap: "6px", marginTop: "10px" });
|
|
1858
|
+
settings.append(
|
|
1859
|
+
labeled("quality", qualitySelect),
|
|
1860
|
+
labeled("long edge", sizeSelect),
|
|
1861
|
+
labeled("format", formatSelect),
|
|
1862
|
+
labeled("output", readout)
|
|
1863
|
+
);
|
|
1864
|
+
for (const control of [qualitySelect, sizeSelect, formatSelect]) {
|
|
1865
|
+
control.onchange = () => {
|
|
1866
|
+
paintOutput();
|
|
1867
|
+
void refreshCost();
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
const body = buildTimelineBody({
|
|
1871
|
+
timeline: config.timeline,
|
|
1872
|
+
...config.targets ? { targets: config.targets } : {},
|
|
1873
|
+
...config.onPlaybackChange ? { onPlaybackChange: config.onPlaybackChange } : {},
|
|
1874
|
+
onRenderVideo: async () => {
|
|
1875
|
+
const outcome = await config.onRenderVideo({
|
|
1876
|
+
quality: qualitySelect.value,
|
|
1877
|
+
...output(),
|
|
1878
|
+
format: formatSelect.value,
|
|
1879
|
+
timeline: config.timeline
|
|
1880
|
+
});
|
|
1881
|
+
await showOutcome(outcome ?? void 0);
|
|
1882
|
+
},
|
|
1883
|
+
// The action lives in the shared action bar next to the price.
|
|
1884
|
+
showRenderButton: false,
|
|
1885
|
+
style: { marginTop: "4px" }
|
|
1886
|
+
});
|
|
1887
|
+
const offChange = config.timeline.on("change", () => void refreshCost());
|
|
1888
|
+
const result = div({ marginTop: "4px" });
|
|
1889
|
+
let shownId;
|
|
1890
|
+
const showOutcome = async (outcome) => {
|
|
1891
|
+
if (!outcome?.url) return;
|
|
1892
|
+
shownId = outcome.entry?.id ?? outcome.id;
|
|
1893
|
+
if (shownId) seenHistory.add(shownId);
|
|
1894
|
+
await paintResult(result, outcome, downloadButton);
|
|
1895
|
+
recordResult(
|
|
1896
|
+
outcome.id,
|
|
1897
|
+
{
|
|
1898
|
+
...outcome,
|
|
1899
|
+
url: outcome.url,
|
|
1900
|
+
frames: outcome.frames ?? config.timeline.frameCount
|
|
1901
|
+
},
|
|
1902
|
+
"video"
|
|
1903
|
+
);
|
|
1904
|
+
paintList();
|
|
1905
|
+
};
|
|
1906
|
+
const historyList = div(HISTORY_LIST);
|
|
1907
|
+
const history = historySection(historyList);
|
|
1908
|
+
let listed = [];
|
|
1909
|
+
const paintList = () => {
|
|
1910
|
+
paintHistoryList(historyList, listed, downloadButton, showEntry, shownId);
|
|
1911
|
+
paintSection(history, listed.length);
|
|
1912
|
+
};
|
|
1913
|
+
const repairLine = span("", {
|
|
1914
|
+
fontSize: "11px",
|
|
1915
|
+
marginTop: "6px",
|
|
1916
|
+
display: "none",
|
|
1917
|
+
whiteSpace: "pre-wrap"
|
|
1918
|
+
});
|
|
1919
|
+
element.append(body.element, repairLine, result, history);
|
|
1920
|
+
return {
|
|
1921
|
+
element,
|
|
1922
|
+
settings,
|
|
1923
|
+
paintOutput,
|
|
1924
|
+
estimateRequest() {
|
|
1925
|
+
return {
|
|
1926
|
+
kind: "video",
|
|
1927
|
+
quality: qualitySelect.value,
|
|
1928
|
+
...output(),
|
|
1929
|
+
frames: config.timeline.frameCount,
|
|
1930
|
+
fps: config.timeline.fps,
|
|
1931
|
+
format: formatSelect.value
|
|
1932
|
+
};
|
|
1933
|
+
},
|
|
1934
|
+
async run() {
|
|
1935
|
+
if (!config.timeline.tracks.length) {
|
|
1936
|
+
panel.setStatus(
|
|
1937
|
+
"The timeline has no keyframes yet. Frame the shot and press Keyframe camera, move the playhead, frame the next shot and press it again. Then press Render video.",
|
|
1938
|
+
"error"
|
|
1939
|
+
);
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
panel.setProgress(`Rendering ${config.timeline.frameCount} frames\u2026`);
|
|
1943
|
+
await body.renderVideo();
|
|
1944
|
+
},
|
|
1945
|
+
refreshTimeline() {
|
|
1946
|
+
body.refresh(config.targets);
|
|
1947
|
+
},
|
|
1948
|
+
/**
|
|
1949
|
+
* Re-attach tracks to a rebuilt scene, and say what happened.
|
|
1950
|
+
*
|
|
1951
|
+
* A stale track is repaired where possible and removed where not: a lane
|
|
1952
|
+
* that can only refuse at export is not a lane, and leaving it there
|
|
1953
|
+
* means the developer finds out by pressing Render.
|
|
1954
|
+
*/
|
|
1955
|
+
reconcile() {
|
|
1956
|
+
const scene = config.scene?.();
|
|
1957
|
+
if (!scene) return;
|
|
1958
|
+
const { healed, stale } = config.timeline.reconcile(scene);
|
|
1959
|
+
for (const track of stale) config.timeline.removeTrack(track.trackId);
|
|
1960
|
+
if (healed.length || stale.length) body.refresh(config.targets);
|
|
1961
|
+
const notes = [];
|
|
1962
|
+
if (healed.length) {
|
|
1963
|
+
notes.push(
|
|
1964
|
+
`re-attached ${healed.length} track${healed.length === 1 ? "" : "s"} to the rebuilt scene`
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
for (const track of stale) {
|
|
1968
|
+
notes.push(
|
|
1969
|
+
`dropped "${track.label} \xB7 ${track.property}": ${track.reason}`
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
repairLine.textContent = notes.join("\n");
|
|
1973
|
+
repairLine.style.color = stale.length ? WARNING : "rgba(244,244,245,0.55)";
|
|
1974
|
+
repairLine.style.display = notes.length ? "block" : "none";
|
|
1975
|
+
},
|
|
1976
|
+
paintHistory(entries) {
|
|
1977
|
+
listed = entries;
|
|
1978
|
+
paintList();
|
|
1979
|
+
},
|
|
1980
|
+
showEntry,
|
|
1981
|
+
dispose() {
|
|
1982
|
+
offChange();
|
|
1983
|
+
body.dispose();
|
|
1984
|
+
}
|
|
1985
|
+
};
|
|
1986
|
+
function showEntry(entry) {
|
|
1987
|
+
if (entry.id === shownId) return;
|
|
1988
|
+
const url = entry.artifact ?? entry.thumbnail;
|
|
1989
|
+
if (!url) return;
|
|
1990
|
+
shownId = entry.id;
|
|
1991
|
+
paintList();
|
|
1992
|
+
void paintResult(
|
|
1993
|
+
result,
|
|
1994
|
+
{
|
|
1995
|
+
url,
|
|
1996
|
+
meta: describeEntry(entry),
|
|
1997
|
+
isVideo: true,
|
|
1998
|
+
entry,
|
|
1999
|
+
...entry.meta?.width ? { width: entry.meta.width } : {},
|
|
2000
|
+
...entry.meta?.height ? { height: entry.meta.height } : {}
|
|
2001
|
+
},
|
|
2002
|
+
downloadButton
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
function buildBakeTab(config) {
|
|
2007
|
+
const element = div({ display: "none" });
|
|
2008
|
+
const { session } = config;
|
|
2009
|
+
const qualities = config.qualities ?? ["preview", "standard", "high"];
|
|
2010
|
+
const sizes = config.sizes ?? [256, 512, 1024, 2048];
|
|
2011
|
+
const qualitySelect = select(qualities.map((value) => [value, value]));
|
|
2012
|
+
qualitySelect.value = config.defaultQuality ?? "standard";
|
|
2013
|
+
const sizeSelect = select(
|
|
2014
|
+
sizes.map((value) => [String(value), `${value}\xB2`])
|
|
2015
|
+
);
|
|
2016
|
+
sizeSelect.value = String(config.defaultSize ?? 1024);
|
|
2017
|
+
const settings = div({ display: "flex", gap: "6px", marginTop: "10px" });
|
|
2018
|
+
settings.append(
|
|
2019
|
+
labeled("texture size", sizeSelect),
|
|
2020
|
+
labeled("quality", qualitySelect)
|
|
2021
|
+
);
|
|
2022
|
+
for (const control of [qualitySelect, sizeSelect]) {
|
|
2023
|
+
control.onchange = () => void refreshCost();
|
|
2024
|
+
}
|
|
2025
|
+
const ticked = /* @__PURE__ */ new Map();
|
|
2026
|
+
const objectList = div({
|
|
2027
|
+
maxHeight: "168px",
|
|
2028
|
+
overflowY: "auto",
|
|
2029
|
+
border: "1px solid rgba(255,255,255,0.06)",
|
|
2030
|
+
borderRadius: "6px",
|
|
2031
|
+
padding: "4px 6px"
|
|
2032
|
+
});
|
|
2033
|
+
const appliedRow = div({
|
|
2034
|
+
display: "none",
|
|
2035
|
+
alignItems: "center",
|
|
2036
|
+
gap: "12px",
|
|
2037
|
+
marginTop: "8px"
|
|
2038
|
+
});
|
|
2039
|
+
const compareButton = button("Hold to compare", GHOST);
|
|
2040
|
+
compareButton.title = "hold to see the scene without the bake";
|
|
2041
|
+
const muteToggle = document.createElement("input");
|
|
2042
|
+
muteToggle.type = "checkbox";
|
|
2043
|
+
muteToggle.style.accentColor = BLUE;
|
|
2044
|
+
const muteLabel = span("Mute realtime lights", {
|
|
2045
|
+
fontSize: "11px",
|
|
2046
|
+
opacity: "0.75"
|
|
2047
|
+
});
|
|
2048
|
+
muteLabel.title = "The lightmap already replaces the diffuse light from this scene's lamps; they only add highlights now. Muting them shows the bake on its own. Reflections from an environment map keep running.";
|
|
2049
|
+
const mute = div({ display: "flex", alignItems: "center", gap: "6px" });
|
|
2050
|
+
mute.append(muteToggle, muteLabel);
|
|
2051
|
+
appliedRow.append(compareButton, mute);
|
|
2052
|
+
const reportLine = span("", {
|
|
2053
|
+
display: "none",
|
|
2054
|
+
marginTop: "8px",
|
|
2055
|
+
fontSize: "11px",
|
|
2056
|
+
color: WARNING,
|
|
2057
|
+
whiteSpace: "pre-wrap"
|
|
2058
|
+
});
|
|
2059
|
+
const historyList = div(HISTORY_LIST);
|
|
2060
|
+
const history = historySection(historyList);
|
|
2061
|
+
let hostBakes = [];
|
|
2062
|
+
element.append(
|
|
2063
|
+
span("objects", SECTION_LABEL),
|
|
2064
|
+
objectList,
|
|
2065
|
+
history,
|
|
2066
|
+
appliedRow,
|
|
2067
|
+
reportLine
|
|
2068
|
+
);
|
|
2069
|
+
const entryByGeneration = /* @__PURE__ */ new Map();
|
|
2070
|
+
function include() {
|
|
2071
|
+
return session.candidates().filter(
|
|
2072
|
+
(candidate) => candidate.bakeable && ticked.get(candidate.objectId)
|
|
2073
|
+
).map((candidate) => candidate.objectId);
|
|
2074
|
+
}
|
|
2075
|
+
function texels() {
|
|
2076
|
+
const size = Number(sizeSelect.value);
|
|
2077
|
+
return include().length * size * size;
|
|
2078
|
+
}
|
|
2079
|
+
function paintCandidates() {
|
|
2080
|
+
const candidates = session.candidates();
|
|
2081
|
+
const report2 = session.current >= 0 && session.lastReport?.generation === session.current ? session.lastReport : null;
|
|
2082
|
+
const applied = new Set(report2?.applied ?? []);
|
|
2083
|
+
const applySkips = new Map(
|
|
2084
|
+
(report2?.skipped ?? []).map((skip) => [skip.objectId, skip])
|
|
2085
|
+
);
|
|
2086
|
+
const workerSkips = new Map(
|
|
2087
|
+
(report2 ? session.generations[session.current]?.bundle.skipped ?? [] : []).map((skip) => [skip.objectId, skip])
|
|
2088
|
+
);
|
|
2089
|
+
objectList.replaceChildren();
|
|
2090
|
+
if (!candidates.length) {
|
|
2091
|
+
objectList.appendChild(
|
|
2092
|
+
span("no meshes in this scene yet", {
|
|
2093
|
+
opacity: "0.45",
|
|
2094
|
+
fontSize: "11px"
|
|
2095
|
+
})
|
|
2096
|
+
);
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
for (const candidate of candidates) {
|
|
2100
|
+
if (!ticked.has(candidate.objectId)) {
|
|
2101
|
+
ticked.set(
|
|
2102
|
+
candidate.objectId,
|
|
2103
|
+
candidate.bakeable && candidate.defaultInclude
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
const row = div({
|
|
2107
|
+
display: "flex",
|
|
2108
|
+
alignItems: "center",
|
|
2109
|
+
gap: "6px",
|
|
2110
|
+
padding: "2px 0"
|
|
2111
|
+
});
|
|
2112
|
+
const box = document.createElement("input");
|
|
2113
|
+
box.type = "checkbox";
|
|
2114
|
+
box.style.accentColor = BLUE;
|
|
2115
|
+
box.checked = Boolean(ticked.get(candidate.objectId)) && candidate.bakeable;
|
|
2116
|
+
box.disabled = !candidate.bakeable;
|
|
2117
|
+
box.onchange = () => {
|
|
2118
|
+
ticked.set(candidate.objectId, box.checked);
|
|
2119
|
+
void refreshCost();
|
|
2120
|
+
};
|
|
2121
|
+
const name = span(candidate.name, {
|
|
2122
|
+
flex: "1 1 auto",
|
|
2123
|
+
overflow: "hidden",
|
|
2124
|
+
textOverflow: "ellipsis",
|
|
2125
|
+
whiteSpace: "nowrap",
|
|
2126
|
+
fontSize: "11px",
|
|
2127
|
+
opacity: candidate.bakeable ? "0.9" : "0.5"
|
|
2128
|
+
});
|
|
2129
|
+
name.title = candidate.path;
|
|
2130
|
+
row.append(box, name);
|
|
2131
|
+
if (report2) {
|
|
2132
|
+
const skip = applySkips.get(candidate.objectId) ?? workerSkips.get(candidate.objectId);
|
|
2133
|
+
if (applied.has(candidate.objectId)) {
|
|
2134
|
+
const lit = span("\u2713", {
|
|
2135
|
+
flex: "0 0 auto",
|
|
2136
|
+
color: BLUE,
|
|
2137
|
+
cursor: "help"
|
|
2138
|
+
});
|
|
2139
|
+
lit.title = "lit by the applied bake";
|
|
2140
|
+
row.appendChild(lit);
|
|
2141
|
+
} else if (skip) {
|
|
2142
|
+
const missed = span("\u2717", {
|
|
2143
|
+
flex: "0 0 auto",
|
|
2144
|
+
color: WARNING,
|
|
2145
|
+
cursor: "help"
|
|
2146
|
+
});
|
|
2147
|
+
missed.title = [skip.message, skip.why, skip.fix].filter(Boolean).join("\n\n");
|
|
2148
|
+
row.appendChild(missed);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
if (candidate.reason) {
|
|
2152
|
+
const warn = span(candidate.bakeable ? "\xB7" : "\u26A0", {
|
|
2153
|
+
flex: "0 0 auto",
|
|
2154
|
+
color: candidate.bakeable ? "rgba(244,244,245,0.4)" : WARNING,
|
|
2155
|
+
cursor: "help"
|
|
2156
|
+
});
|
|
2157
|
+
warn.title = [
|
|
2158
|
+
candidate.reason.message,
|
|
2159
|
+
candidate.reason.why,
|
|
2160
|
+
candidate.reason.fix
|
|
2161
|
+
].filter(Boolean).join("\n\n");
|
|
2162
|
+
row.appendChild(warn);
|
|
2163
|
+
}
|
|
2164
|
+
objectList.appendChild(row);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
function paintGenerations() {
|
|
2168
|
+
const rows = [];
|
|
2169
|
+
for (let index = session.generations.length - 1; index >= 0; index--) {
|
|
2170
|
+
rows.push(generationRow(index));
|
|
2171
|
+
}
|
|
2172
|
+
const linked = new Set(
|
|
2173
|
+
[...entryByGeneration.values()].map((entry) => entry.id)
|
|
2174
|
+
);
|
|
2175
|
+
for (const entry of hostBakes) {
|
|
2176
|
+
const download = linked.has(entry.id) ? null : downloadButton(entry);
|
|
2177
|
+
if (!download) continue;
|
|
2178
|
+
rows.push(
|
|
2179
|
+
recentRow({
|
|
2180
|
+
tile: bakeTile(false),
|
|
2181
|
+
title: entryTitle(entry),
|
|
2182
|
+
meta: entryMeta(entry),
|
|
2183
|
+
selected: false,
|
|
2184
|
+
actions: [download]
|
|
2185
|
+
})
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
historyList.replaceChildren(...rows);
|
|
2189
|
+
if (!rows.length) {
|
|
2190
|
+
historyList.appendChild(
|
|
2191
|
+
span("nothing yet", { opacity: "0.4", fontSize: "11px" })
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
paintSection(history, rows.length);
|
|
2195
|
+
const applied = session.current >= 0;
|
|
2196
|
+
appliedRow.style.display = applied ? "flex" : "none";
|
|
2197
|
+
muteToggle.checked = session.lightsMuted;
|
|
2198
|
+
const notes = [];
|
|
2199
|
+
const report2 = session.lastReport;
|
|
2200
|
+
if (applied && report2 && report2.generation >= 0) {
|
|
2201
|
+
if (report2.unmatched.length) {
|
|
2202
|
+
notes.push(
|
|
2203
|
+
`${report2.unmatched.length} baked object${report2.unmatched.length === 1 ? "" : "s"} no longer exist${report2.unmatched.length === 1 ? "s" : ""}`
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
if (report2.unbaked.length) {
|
|
2207
|
+
notes.push(
|
|
2208
|
+
`${report2.unbaked.length} new object${report2.unbaked.length === 1 ? " is" : "s are"} unbaked`
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
for (const skip of report2.skipped) notes.push(`${skip.message}`);
|
|
2212
|
+
}
|
|
2213
|
+
const skipped = applied ? session.generations[session.current]?.bundle.skipped ?? [] : [];
|
|
2214
|
+
if (skipped.length) {
|
|
2215
|
+
notes.push(
|
|
2216
|
+
`${skipped.length} object${skipped.length === 1 ? "" : "s"} skipped by the bake, hover the list`
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
reportLine.textContent = notes.join(" \xB7 ");
|
|
2220
|
+
reportLine.style.display = notes.length ? "block" : "none";
|
|
2221
|
+
}
|
|
2222
|
+
function generationRow(index) {
|
|
2223
|
+
const generation = session.generations[index];
|
|
2224
|
+
const { settings: settings2, objects, skipped } = generation.bundle;
|
|
2225
|
+
const applied = session.current === index;
|
|
2226
|
+
const entry = entryByGeneration.get(index);
|
|
2227
|
+
const actions = [];
|
|
2228
|
+
if (entry) {
|
|
2229
|
+
const bundle = downloadButton(entry);
|
|
2230
|
+
if (bundle) {
|
|
2231
|
+
bundle.textContent = "Bundle";
|
|
2232
|
+
actions.push(bundle);
|
|
2233
|
+
}
|
|
2234
|
+
if (generation.bundle.glb) {
|
|
2235
|
+
const glb = downloadButton(entry, "glb");
|
|
2236
|
+
if (glb) actions.push(glb);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
const cost = entry?.meta?.cost;
|
|
2240
|
+
return recentRow({
|
|
2241
|
+
tile: bakeTile(applied),
|
|
2242
|
+
title: `${objects.length} object${objects.length === 1 ? "" : "s"} \xB7 ${settings2.textureSize}\xB2`,
|
|
2243
|
+
meta: [
|
|
2244
|
+
ago(generation.createdAt),
|
|
2245
|
+
settings2.quality,
|
|
2246
|
+
skipped.length ? `${skipped.length} skipped` : void 0,
|
|
2247
|
+
cost !== void 0 ? `$${cost.toFixed(2)}` : void 0
|
|
2248
|
+
].filter(Boolean).join(" \xB7 "),
|
|
2249
|
+
selected: applied,
|
|
2250
|
+
pick: applied ? {
|
|
2251
|
+
hint: "Take it off: back to the live scene",
|
|
2252
|
+
run: () => {
|
|
2253
|
+
session.revert();
|
|
2254
|
+
paintGenerations();
|
|
2255
|
+
}
|
|
2256
|
+
} : {
|
|
2257
|
+
hint: "Put this bake on the scene",
|
|
2258
|
+
run: () => void applyGeneration(index)
|
|
2259
|
+
},
|
|
2260
|
+
actions
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
async function applyGeneration(index) {
|
|
2264
|
+
try {
|
|
2265
|
+
panel.setProgress("Applying\u2026");
|
|
2266
|
+
await session.apply(index);
|
|
2267
|
+
panel.setStatus("");
|
|
2268
|
+
} catch (error) {
|
|
2269
|
+
panel.setStatus(describe(error), "error");
|
|
2270
|
+
} finally {
|
|
2271
|
+
panel.setProgress(null);
|
|
2272
|
+
paintGenerations();
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
compareButton.addEventListener("pointerdown", () => session.compare(true));
|
|
2276
|
+
for (const event of [
|
|
2277
|
+
"pointerup",
|
|
2278
|
+
"pointercancel",
|
|
2279
|
+
"pointerleave"
|
|
2280
|
+
]) {
|
|
2281
|
+
compareButton.addEventListener(event, () => session.compare(false));
|
|
2282
|
+
}
|
|
2283
|
+
muteToggle.onchange = () => session.muteLights(muteToggle.checked);
|
|
2284
|
+
const offSession = session.on("change", () => {
|
|
2285
|
+
paintCandidates();
|
|
2286
|
+
paintGenerations();
|
|
2287
|
+
});
|
|
2288
|
+
paintCandidates();
|
|
2289
|
+
paintGenerations();
|
|
2290
|
+
return {
|
|
2291
|
+
element,
|
|
2292
|
+
settings,
|
|
2293
|
+
estimateRequest() {
|
|
2294
|
+
const chosen = include();
|
|
2295
|
+
return {
|
|
2296
|
+
kind: "bake",
|
|
2297
|
+
quality: qualitySelect.value,
|
|
2298
|
+
textureSize: Number(sizeSelect.value),
|
|
2299
|
+
objects: chosen.length,
|
|
2300
|
+
texels: texels(),
|
|
2301
|
+
include: chosen
|
|
2302
|
+
};
|
|
2303
|
+
},
|
|
2304
|
+
async run() {
|
|
2305
|
+
const chosen = include();
|
|
2306
|
+
if (!chosen.length) {
|
|
2307
|
+
panel.setStatus(
|
|
2308
|
+
"Nothing is ticked. Choose at least one object to bake.",
|
|
2309
|
+
"error"
|
|
2310
|
+
);
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
panel.setProgress(
|
|
2314
|
+
`Baking\u2026 (${chosen.length} object${chosen.length === 1 ? "" : "s"})`
|
|
2315
|
+
);
|
|
2316
|
+
const outcome = await config.onBake({
|
|
2317
|
+
quality: qualitySelect.value,
|
|
2318
|
+
textureSize: Number(sizeSelect.value),
|
|
2319
|
+
include: chosen
|
|
2320
|
+
});
|
|
2321
|
+
if (outcome?.entry !== void 0 && outcome.generation !== void 0) {
|
|
2322
|
+
entryByGeneration.set(outcome.generation, outcome.entry);
|
|
2323
|
+
}
|
|
2324
|
+
paintCandidates();
|
|
2325
|
+
paintGenerations();
|
|
2326
|
+
},
|
|
2327
|
+
paintCandidates,
|
|
2328
|
+
paintGenerations,
|
|
2329
|
+
paintHistory(entries) {
|
|
2330
|
+
hostBakes = ownHistory ? [] : entries;
|
|
2331
|
+
paintGenerations();
|
|
2332
|
+
},
|
|
2333
|
+
dispose() {
|
|
2334
|
+
offSession();
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
function afterAWhile(callback) {
|
|
2340
|
+
let frame;
|
|
2341
|
+
const timer = setTimeout(() => {
|
|
2342
|
+
if (typeof requestAnimationFrame === "function") {
|
|
2343
|
+
frame = requestAnimationFrame(() => callback());
|
|
2344
|
+
} else {
|
|
2345
|
+
callback();
|
|
2346
|
+
}
|
|
2347
|
+
}, WATCH_MS);
|
|
2348
|
+
return () => {
|
|
2349
|
+
clearTimeout(timer);
|
|
2350
|
+
if (frame !== void 0) cancelAnimationFrame(frame);
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
function readAspect(aspect) {
|
|
2354
|
+
const value = typeof aspect === "function" ? aspect() : aspect ?? 1;
|
|
2355
|
+
return Number.isFinite(value) && value > 0 ? value : 1;
|
|
2356
|
+
}
|
|
2357
|
+
function describeRequest(request) {
|
|
2358
|
+
if (request.kind === "bake") {
|
|
2359
|
+
return `${request.quality} \xB7 ${request.textureSize}\xB2 per object`;
|
|
2360
|
+
}
|
|
2361
|
+
const size = `${request.width} \xD7 ${request.height}`;
|
|
2362
|
+
if (request.kind === "video") {
|
|
2363
|
+
return `${request.quality} \xB7 ${size} \xB7 ${request.fps} fps \xB7 ${request.format}`;
|
|
2364
|
+
}
|
|
2365
|
+
return `${request.quality} \xB7 ${size}`;
|
|
2366
|
+
}
|
|
2367
|
+
function breakdownLines(request, estimate) {
|
|
2368
|
+
const muted = { display: "block", fontSize: "11px", opacity: "0.6" };
|
|
2369
|
+
const count = request.kind === "video" ? { n: request.frames, unit: "frame" } : request.kind === "bake" ? { n: request.objects, unit: "object" } : null;
|
|
2370
|
+
const approximate = estimate.approximate !== false;
|
|
2371
|
+
const lines = [
|
|
2372
|
+
span(describeRequest(request), { ...muted, marginBottom: "4px" }),
|
|
2373
|
+
breakdownRow(
|
|
2374
|
+
count && count.n > 0 ? `${count.n} ${count.unit}${count.n === 1 ? "" : "s"} \xD7 ${usd(estimate.usd / count.n, true)}` : approximate ? "estimate" : "price",
|
|
2375
|
+
`${approximate ? "\u2248 " : ""}${usd(estimate.usd)}`
|
|
2376
|
+
)
|
|
2377
|
+
];
|
|
2378
|
+
if (estimate.maximum !== void 0) {
|
|
2379
|
+
lines.push(breakdownRow("at most", usd(estimate.maximum)));
|
|
2380
|
+
}
|
|
2381
|
+
if (estimate.detail) {
|
|
2382
|
+
lines.push(span(estimate.detail, { ...muted, marginTop: "4px" }));
|
|
2383
|
+
}
|
|
2384
|
+
if (approximate) {
|
|
2385
|
+
lines.push(
|
|
2386
|
+
span("You pay for the GPU time used.", { ...muted, marginTop: "4px" })
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
return lines;
|
|
2390
|
+
}
|
|
2391
|
+
function breakdownRow(label, value) {
|
|
2392
|
+
const row = div({
|
|
2393
|
+
display: "flex",
|
|
2394
|
+
justifyContent: "space-between",
|
|
2395
|
+
gap: "24px",
|
|
2396
|
+
fontSize: "11px"
|
|
2397
|
+
});
|
|
2398
|
+
row.append(span(label, { opacity: "0.85" }), span(value, { color: BLUE }));
|
|
2399
|
+
return row;
|
|
2400
|
+
}
|
|
2401
|
+
function usd(value, unit = false) {
|
|
2402
|
+
const digits = unit ? value < 0.01 ? 4 : value < 0.1 ? 3 : 2 : 2;
|
|
2403
|
+
return `$${value.toFixed(digits)}`;
|
|
2404
|
+
}
|
|
2405
|
+
function issueGroup(path, issues) {
|
|
2406
|
+
const group = div({ padding: "4px 0" });
|
|
2407
|
+
const name = span(path ? objectName(path) : "Scene", {
|
|
2408
|
+
display: "block",
|
|
2409
|
+
fontSize: "11px",
|
|
2410
|
+
fontWeight: "600",
|
|
2411
|
+
overflow: "hidden",
|
|
2412
|
+
textOverflow: "ellipsis",
|
|
2413
|
+
whiteSpace: "nowrap"
|
|
2414
|
+
});
|
|
2415
|
+
if (path) name.title = path;
|
|
2416
|
+
group.appendChild(name);
|
|
2417
|
+
for (const issue of issues) {
|
|
2418
|
+
const row = div({
|
|
2419
|
+
display: "flex",
|
|
2420
|
+
alignItems: "baseline",
|
|
2421
|
+
gap: "6px",
|
|
2422
|
+
marginTop: "2px",
|
|
2423
|
+
fontSize: "11px"
|
|
2424
|
+
});
|
|
2425
|
+
const [glyph, color] = issue.severity === "error" ? ["\u2715", ERROR] : issue.severity === "warning" ? ["\u26A0", WARNING] : ["i", "rgba(244,244,245,0.5)"];
|
|
2426
|
+
row.append(
|
|
2427
|
+
span(glyph, { flex: "0 0 auto", width: "12px", color }),
|
|
2428
|
+
span(issue.message, {
|
|
2429
|
+
flex: "1 1 auto",
|
|
2430
|
+
minWidth: "0",
|
|
2431
|
+
opacity: "0.8",
|
|
2432
|
+
whiteSpace: "normal"
|
|
2433
|
+
})
|
|
2434
|
+
);
|
|
2435
|
+
if (issue.docs) {
|
|
2436
|
+
const docs = document.createElement("a");
|
|
2437
|
+
docs.textContent = "docs";
|
|
2438
|
+
docs.href = issue.docs;
|
|
2439
|
+
docs.target = "_blank";
|
|
2440
|
+
docs.rel = "noreferrer";
|
|
2441
|
+
Object.assign(docs.style, {
|
|
2442
|
+
flex: "0 0 auto",
|
|
2443
|
+
color: BLUE,
|
|
2444
|
+
textDecoration: "none"
|
|
2445
|
+
});
|
|
2446
|
+
row.appendChild(docs);
|
|
2447
|
+
}
|
|
2448
|
+
group.appendChild(row);
|
|
2449
|
+
}
|
|
2450
|
+
return group;
|
|
2451
|
+
}
|
|
2452
|
+
function objectName(path) {
|
|
2453
|
+
return /<([^>]*)>$/.exec(path)?.[1] || path;
|
|
2454
|
+
}
|
|
2455
|
+
function severityRank(severity) {
|
|
2456
|
+
return severity === "error" ? 0 : severity === "warning" ? 1 : 2;
|
|
2457
|
+
}
|
|
2458
|
+
function issueCount(count) {
|
|
2459
|
+
return `${count} thing${count === 1 ? "" : "s"} render${count === 1 ? "s" : ""} differently`;
|
|
2460
|
+
}
|
|
2461
|
+
function errorCode(error) {
|
|
2462
|
+
const code = error?.code;
|
|
2463
|
+
return typeof code === "string" ? code : void 0;
|
|
2464
|
+
}
|
|
2465
|
+
var URL_PATTERN = /https:\/\/[^\s<>"')]*[^\s<>"').,;:]/g;
|
|
2466
|
+
function linkified(message) {
|
|
2467
|
+
const parts = [];
|
|
2468
|
+
let last = 0;
|
|
2469
|
+
for (const match of message.matchAll(URL_PATTERN)) {
|
|
2470
|
+
const at = match.index ?? 0;
|
|
2471
|
+
if (at > last) parts.push(span(message.slice(last, at), {}));
|
|
2472
|
+
const link = document.createElement("a");
|
|
2473
|
+
link.textContent = match[0];
|
|
2474
|
+
link.href = match[0];
|
|
2475
|
+
link.target = "_blank";
|
|
2476
|
+
link.rel = "noreferrer";
|
|
2477
|
+
Object.assign(link.style, {
|
|
2478
|
+
color: "inherit",
|
|
2479
|
+
textDecoration: "underline",
|
|
2480
|
+
textUnderlineOffset: "2px"
|
|
2481
|
+
});
|
|
2482
|
+
parts.push(link);
|
|
2483
|
+
last = at + match[0].length;
|
|
2484
|
+
}
|
|
2485
|
+
if (last < message.length) parts.push(span(message.slice(last), {}));
|
|
2486
|
+
return parts;
|
|
2487
|
+
}
|
|
2488
|
+
var SESSION_HISTORY_KEY = "bakery3.devtools.history";
|
|
2489
|
+
var SESSION_HISTORY_MAX = 40;
|
|
2490
|
+
var URL_LIFETIME_MS = 24 * 60 * 60 * 1e3;
|
|
2491
|
+
function readSessionHistory() {
|
|
2492
|
+
try {
|
|
2493
|
+
const raw = globalThis.localStorage?.getItem(SESSION_HISTORY_KEY);
|
|
2494
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
2495
|
+
if (!Array.isArray(parsed)) return [];
|
|
2496
|
+
return liveEntries(
|
|
2497
|
+
parsed.filter(
|
|
2498
|
+
(entry) => !!entry && typeof entry === "object" && typeof entry.id === "string" && typeof entry.createdAt === "string"
|
|
2499
|
+
)
|
|
2500
|
+
);
|
|
2501
|
+
} catch {
|
|
2502
|
+
return [];
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
function writeSessionHistory(entries) {
|
|
2506
|
+
try {
|
|
2507
|
+
globalThis.localStorage?.setItem(
|
|
2508
|
+
SESSION_HISTORY_KEY,
|
|
2509
|
+
JSON.stringify(entries)
|
|
2510
|
+
);
|
|
2511
|
+
} catch {
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
function liveEntries(entries) {
|
|
2515
|
+
const soon = Date.now() + 6e4;
|
|
2516
|
+
return entries.filter(
|
|
2517
|
+
(entry) => !entry.expiresAt || Date.parse(entry.expiresAt) > soon
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
var paintTokens = /* @__PURE__ */ new WeakMap();
|
|
2521
|
+
var RESULT_LOAD_TIMEOUT_MS = 8e3;
|
|
2522
|
+
async function paintResult(host, outcome, downloadButton) {
|
|
2523
|
+
if (!outcome?.url) return;
|
|
2524
|
+
const token = (paintTokens.get(host) ?? 0) + 1;
|
|
2525
|
+
paintTokens.set(host, token);
|
|
2526
|
+
const shape = outcome.width && outcome.height ? { aspectRatio: `${outcome.width} / ${outcome.height}` } : {};
|
|
2527
|
+
let media;
|
|
2528
|
+
let loaded;
|
|
2529
|
+
if (outcome.isVideo) {
|
|
2530
|
+
const video = document.createElement("video");
|
|
2531
|
+
video.controls = true;
|
|
2532
|
+
video.loop = true;
|
|
2533
|
+
video.muted = true;
|
|
2534
|
+
video.autoplay = true;
|
|
2535
|
+
video.preload = "auto";
|
|
2536
|
+
Object.assign(video.style, {
|
|
2537
|
+
width: "100%",
|
|
2538
|
+
borderRadius: "6px",
|
|
2539
|
+
display: "block",
|
|
2540
|
+
...shape
|
|
2541
|
+
});
|
|
2542
|
+
loaded = typeof video.load === "function" ? loadedOrTimeout(video, ["loadeddata", "error"]) : Promise.resolve();
|
|
2543
|
+
video.src = outcome.url;
|
|
2544
|
+
media = video;
|
|
2545
|
+
} else {
|
|
2546
|
+
const image = document.createElement("img");
|
|
2547
|
+
image.alt = "render";
|
|
2548
|
+
Object.assign(image.style, {
|
|
2549
|
+
width: "100%",
|
|
2550
|
+
borderRadius: "6px",
|
|
2551
|
+
display: "block",
|
|
2552
|
+
background: "repeating-conic-gradient(#2a2a2e 0% 25%, #232327 0% 50%) 50% / 12px 12px",
|
|
2553
|
+
...shape
|
|
2554
|
+
});
|
|
2555
|
+
image.src = outcome.url;
|
|
2556
|
+
loaded = typeof image.decode === "function" ? Promise.race([
|
|
2557
|
+
image.decode().catch(() => void 0),
|
|
2558
|
+
delay(RESULT_LOAD_TIMEOUT_MS)
|
|
2559
|
+
]) : Promise.resolve();
|
|
2560
|
+
media = image;
|
|
2561
|
+
}
|
|
2562
|
+
const parts = [media];
|
|
2563
|
+
if (outcome.meta) {
|
|
2564
|
+
parts.push(
|
|
2565
|
+
span(outcome.meta, {
|
|
2566
|
+
display: "block",
|
|
2567
|
+
marginTop: "6px",
|
|
2568
|
+
fontSize: "11px",
|
|
2569
|
+
opacity: "0.7"
|
|
2570
|
+
})
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
const actions = div({ display: "flex", gap: "6px", marginTop: "6px" });
|
|
2574
|
+
appendLinks(actions, outcome.url);
|
|
2575
|
+
if (outcome.entry) {
|
|
2576
|
+
const download = downloadButton(outcome.entry);
|
|
2577
|
+
if (download) actions.appendChild(download);
|
|
2578
|
+
}
|
|
2579
|
+
if (actions.childNodes.length) parts.push(actions);
|
|
2580
|
+
await loaded;
|
|
2581
|
+
if (paintTokens.get(host) !== token) return;
|
|
2582
|
+
host.replaceChildren(...parts);
|
|
2583
|
+
}
|
|
2584
|
+
function loadedOrTimeout(target, events) {
|
|
2585
|
+
return new Promise((resolve) => {
|
|
2586
|
+
const done = () => {
|
|
2587
|
+
for (const event of events) target.removeEventListener(event, done);
|
|
2588
|
+
resolve();
|
|
2589
|
+
};
|
|
2590
|
+
for (const event of events) target.addEventListener(event, done);
|
|
2591
|
+
void delay(RESULT_LOAD_TIMEOUT_MS).then(done);
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
function delay(ms) {
|
|
2595
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2596
|
+
}
|
|
2597
|
+
function appendLinks(actions, url) {
|
|
2598
|
+
const openLink = document.createElement("a");
|
|
2599
|
+
openLink.textContent = "Open";
|
|
2600
|
+
openLink.href = url;
|
|
2601
|
+
openLink.target = "_blank";
|
|
2602
|
+
openLink.rel = "noreferrer";
|
|
2603
|
+
Object.assign(openLink.style, {
|
|
2604
|
+
...GHOST,
|
|
2605
|
+
textAlign: "center",
|
|
2606
|
+
textDecoration: "none"
|
|
2607
|
+
});
|
|
2608
|
+
const copy = button("Copy URL", GHOST);
|
|
2609
|
+
copy.onclick = () => void navigator.clipboard?.writeText(url);
|
|
2610
|
+
actions.append(openLink, copy);
|
|
2611
|
+
}
|
|
2612
|
+
var TERMINAL_JOB_STATES = /* @__PURE__ */ new Set([
|
|
2613
|
+
"completed",
|
|
2614
|
+
"failed",
|
|
2615
|
+
"cancelled",
|
|
2616
|
+
"expired"
|
|
2617
|
+
]);
|
|
2618
|
+
function jobRow(job) {
|
|
2619
|
+
const terminal = TERMINAL_JOB_STATES.has(job.state);
|
|
2620
|
+
const row = div({
|
|
2621
|
+
display: "flex",
|
|
2622
|
+
alignItems: "center",
|
|
2623
|
+
gap: "8px",
|
|
2624
|
+
padding: "4px 0"
|
|
2625
|
+
});
|
|
2626
|
+
const thumb = div({
|
|
2627
|
+
width: "44px",
|
|
2628
|
+
height: "33px",
|
|
2629
|
+
flex: "0 0 auto",
|
|
2630
|
+
borderRadius: "4px",
|
|
2631
|
+
background: "rgba(255,255,255,0.06)",
|
|
2632
|
+
overflow: "hidden",
|
|
2633
|
+
display: "flex",
|
|
2634
|
+
alignItems: "center",
|
|
2635
|
+
justifyContent: "center"
|
|
2636
|
+
});
|
|
2637
|
+
const image = job.resultUrl ?? job.previewUrl;
|
|
2638
|
+
if (image) {
|
|
2639
|
+
const img = document.createElement("img");
|
|
2640
|
+
img.src = image;
|
|
2641
|
+
img.alt = job.resultUrl ? "result" : "preview";
|
|
2642
|
+
Object.assign(img.style, {
|
|
2643
|
+
width: "100%",
|
|
2644
|
+
height: "100%",
|
|
2645
|
+
objectFit: "cover",
|
|
2646
|
+
display: "block"
|
|
2647
|
+
});
|
|
2648
|
+
thumb.appendChild(img);
|
|
2649
|
+
} else {
|
|
2650
|
+
thumb.appendChild(
|
|
2651
|
+
span(job.state === "queued" ? "\u2026" : "", {
|
|
2652
|
+
opacity: "0.5",
|
|
2653
|
+
fontSize: "11px"
|
|
2654
|
+
})
|
|
2655
|
+
);
|
|
2656
|
+
}
|
|
2657
|
+
const text = div({ flex: "1 1 auto", minWidth: "0" });
|
|
2658
|
+
const line = [
|
|
2659
|
+
job.state.replace(/_/g, " "),
|
|
2660
|
+
job.detail,
|
|
2661
|
+
job.seconds !== void 0 ? `${Math.round(job.seconds)}s` : void 0,
|
|
2662
|
+
job.previewUrl && !job.resultUrl ? "preview" : void 0
|
|
2663
|
+
].filter(Boolean).join(" \xB7 ");
|
|
2664
|
+
text.append(
|
|
2665
|
+
span(job.label, {
|
|
2666
|
+
display: "block",
|
|
2667
|
+
fontSize: "11px",
|
|
2668
|
+
overflow: "hidden",
|
|
2669
|
+
textOverflow: "ellipsis",
|
|
2670
|
+
whiteSpace: "nowrap"
|
|
2671
|
+
}),
|
|
2672
|
+
span(line, {
|
|
2673
|
+
display: "block",
|
|
2674
|
+
fontSize: "10px",
|
|
2675
|
+
opacity: "0.7",
|
|
2676
|
+
color: job.state === "failed" ? ERROR : "inherit"
|
|
2677
|
+
})
|
|
2678
|
+
);
|
|
2679
|
+
const track = div({
|
|
2680
|
+
height: "2px",
|
|
2681
|
+
marginTop: "3px",
|
|
2682
|
+
borderRadius: "1px",
|
|
2683
|
+
background: "rgba(255,255,255,0.08)",
|
|
2684
|
+
overflow: "hidden"
|
|
2685
|
+
});
|
|
2686
|
+
const indeterminate = job.progress === void 0 && !terminal;
|
|
2687
|
+
track.appendChild(
|
|
2688
|
+
div({
|
|
2689
|
+
height: "100%",
|
|
2690
|
+
width: terminal || indeterminate ? "100%" : `${Math.round(Math.max(0, Math.min(1, job.progress)) * 100)}%`,
|
|
2691
|
+
opacity: indeterminate ? "0.35" : "1",
|
|
2692
|
+
background: terminal ? "rgba(255,255,255,0.25)" : SWEEP
|
|
2693
|
+
})
|
|
2694
|
+
);
|
|
2695
|
+
text.appendChild(track);
|
|
2696
|
+
row.append(thumb, text);
|
|
2697
|
+
if (job.onCancel && !terminal) {
|
|
2698
|
+
const cancel = button("\xD7", { ...GHOST, width: "24px" });
|
|
2699
|
+
cancel.title = "cancel";
|
|
2700
|
+
cancel.onclick = () => job.onCancel?.();
|
|
2701
|
+
row.appendChild(cancel);
|
|
2702
|
+
}
|
|
2703
|
+
return row;
|
|
2704
|
+
}
|
|
2705
|
+
function recentRow(item) {
|
|
2706
|
+
const row = div({
|
|
2707
|
+
display: "flex",
|
|
2708
|
+
alignItems: "center",
|
|
2709
|
+
gap: "8px",
|
|
2710
|
+
height: `${HISTORY_ROW_PX}px`,
|
|
2711
|
+
padding: "0 6px",
|
|
2712
|
+
boxSizing: "border-box",
|
|
2713
|
+
borderRadius: "6px",
|
|
2714
|
+
flex: "0 0 auto",
|
|
2715
|
+
background: item.selected ? `rgba(${BLUE_RGB},0.10)` : "transparent",
|
|
2716
|
+
boxShadow: item.selected ? `inset 2px 0 0 ${BLUE}` : "none"
|
|
2717
|
+
});
|
|
2718
|
+
const text = div({ flex: "1 1 auto", minWidth: "0" });
|
|
2719
|
+
const line = {
|
|
2720
|
+
display: "block",
|
|
2721
|
+
overflow: "hidden",
|
|
2722
|
+
textOverflow: "ellipsis",
|
|
2723
|
+
whiteSpace: "nowrap"
|
|
2724
|
+
};
|
|
2725
|
+
text.append(
|
|
2726
|
+
span(item.title, {
|
|
2727
|
+
...line,
|
|
2728
|
+
fontSize: "11px",
|
|
2729
|
+
opacity: item.selected ? "1" : "0.85"
|
|
2730
|
+
}),
|
|
2731
|
+
span(item.meta, { ...line, fontSize: "10px", opacity: "0.5" })
|
|
2732
|
+
);
|
|
2733
|
+
if (item.pick) {
|
|
2734
|
+
const { hint, run } = item.pick;
|
|
2735
|
+
row.style.cursor = "pointer";
|
|
2736
|
+
row.title = hint;
|
|
2737
|
+
row.onclick = (event) => {
|
|
2738
|
+
const target = event.target?.tagName;
|
|
2739
|
+
if (target?.toUpperCase() === "BUTTON") return;
|
|
2740
|
+
run();
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
row.append(item.tile, text, ...item.actions);
|
|
2744
|
+
return row;
|
|
2745
|
+
}
|
|
2746
|
+
var TILE = {
|
|
2747
|
+
flex: "0 0 auto",
|
|
2748
|
+
width: "32px",
|
|
2749
|
+
height: "32px",
|
|
2750
|
+
borderRadius: "5px",
|
|
2751
|
+
background: "rgba(255,255,255,0.06)",
|
|
2752
|
+
display: "flex",
|
|
2753
|
+
alignItems: "center",
|
|
2754
|
+
justifyContent: "center",
|
|
2755
|
+
fontSize: "10px",
|
|
2756
|
+
overflow: "hidden"
|
|
2757
|
+
};
|
|
2758
|
+
function imageTile(url) {
|
|
2759
|
+
const image = document.createElement("img");
|
|
2760
|
+
image.loading = "lazy";
|
|
2761
|
+
image.src = url;
|
|
2762
|
+
image.alt = "";
|
|
2763
|
+
Object.assign(image.style, { ...TILE, objectFit: "cover" });
|
|
2764
|
+
return image;
|
|
2765
|
+
}
|
|
2766
|
+
function bakeTile(applied) {
|
|
2767
|
+
const glow = applied ? 1 : 0.5;
|
|
2768
|
+
return div({
|
|
2769
|
+
...TILE,
|
|
2770
|
+
background: `radial-gradient(circle at 32% 30%, rgba(253,195,133,${glow}) 0%, rgba(236,157,60,${glow * 0.6}) 30%, rgba(${BLUE_RGB},${glow * 0.35}) 70%, rgba(255,255,255,0.04) 100%)`
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
function paintHistoryList(host, entries, downloadButton, select2, shownId) {
|
|
2774
|
+
host.replaceChildren();
|
|
2775
|
+
if (!entries.length) {
|
|
2776
|
+
host.appendChild(span("nothing yet", { opacity: "0.4", fontSize: "11px" }));
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
for (const entry of entries) {
|
|
2780
|
+
const selected = entry.id === shownId;
|
|
2781
|
+
const download = downloadButton(entry);
|
|
2782
|
+
host.appendChild(
|
|
2783
|
+
recentRow({
|
|
2784
|
+
// A clip without a still: its tile says what it is.
|
|
2785
|
+
tile: entry.thumbnail ? imageTile(entry.thumbnail) : span(entry.kind === "video" ? "\u25B6" : "", {
|
|
2786
|
+
...TILE,
|
|
2787
|
+
opacity: "0.7"
|
|
2788
|
+
}),
|
|
2789
|
+
title: entryTitle(entry),
|
|
2790
|
+
meta: entryMeta(entry),
|
|
2791
|
+
selected,
|
|
2792
|
+
...select2 && !selected && (entry.artifact ?? entry.thumbnail) ? { pick: { hint: "Show it above", run: () => select2(entry) } } : {},
|
|
2793
|
+
actions: download ? [download] : []
|
|
2794
|
+
})
|
|
2795
|
+
);
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
function entryTitle(entry) {
|
|
2799
|
+
const meta = entry.meta ?? {};
|
|
2800
|
+
const size = meta.width && meta.height ? `${meta.width} \xD7 ${meta.height}` : void 0;
|
|
2801
|
+
if (entry.kind === "bake") {
|
|
2802
|
+
return [
|
|
2803
|
+
meta.objects !== void 0 ? `${meta.objects} object${meta.objects === 1 ? "" : "s"}` : void 0,
|
|
2804
|
+
meta.textureSize !== void 0 ? `${meta.textureSize}\xB2` : void 0
|
|
2805
|
+
].filter(Boolean).join(" \xB7 ") || "bake";
|
|
2806
|
+
}
|
|
2807
|
+
if (entry.kind === "video") {
|
|
2808
|
+
return [meta.frames !== void 0 ? `${meta.frames} frames` : void 0, size].filter(Boolean).join(" \xB7 ") || "video";
|
|
2809
|
+
}
|
|
2810
|
+
return size ?? "render";
|
|
2811
|
+
}
|
|
2812
|
+
function entryMeta(entry) {
|
|
2813
|
+
const meta = entry.meta ?? {};
|
|
2814
|
+
return [
|
|
2815
|
+
ago(Date.parse(entry.createdAt)),
|
|
2816
|
+
meta.quality,
|
|
2817
|
+
entry.kind === "video" ? meta.format : void 0,
|
|
2818
|
+
meta.skipped ? `${meta.skipped} skipped` : void 0,
|
|
2819
|
+
meta.cost !== void 0 ? `$${meta.cost.toFixed(2)}` : void 0,
|
|
2820
|
+
entry.cached ? "cached" : void 0
|
|
2821
|
+
].filter(Boolean).join(" \xB7 ");
|
|
2822
|
+
}
|
|
2823
|
+
function describeEntry(entry) {
|
|
2824
|
+
const meta = entry.meta ?? {};
|
|
2825
|
+
const parts = [ago(Date.parse(entry.createdAt))];
|
|
2826
|
+
if (entry.kind === "bake") {
|
|
2827
|
+
if (meta.objects !== void 0) parts.push(`${meta.objects} objects`);
|
|
2828
|
+
if (meta.textureSize !== void 0) parts.push(`${meta.textureSize}\xB2`);
|
|
2829
|
+
if (meta.skipped) parts.push(`${meta.skipped} skipped`);
|
|
2830
|
+
} else {
|
|
2831
|
+
if (meta.width && meta.height) parts.push(`${meta.width}\xD7${meta.height}`);
|
|
2832
|
+
if (meta.frames !== void 0) parts.push(`${meta.frames} frames`);
|
|
2833
|
+
if (meta.format) parts.push(meta.format);
|
|
2834
|
+
}
|
|
2835
|
+
if (meta.quality) parts.push(meta.quality);
|
|
2836
|
+
if (meta.cost !== void 0) parts.push(`$${meta.cost.toFixed(2)}`);
|
|
2837
|
+
if (entry.cached) parts.push("cached");
|
|
2838
|
+
return parts.join(" \xB7 ");
|
|
2839
|
+
}
|
|
2840
|
+
function ago(timestamp) {
|
|
2841
|
+
if (!Number.isFinite(timestamp)) return "just now";
|
|
2842
|
+
const seconds = Math.max(0, Math.round((Date.now() - timestamp) / 1e3));
|
|
2843
|
+
if (seconds < 45) return "just now";
|
|
2844
|
+
const minutes = Math.round(seconds / 60);
|
|
2845
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
2846
|
+
const hours = Math.round(minutes / 60);
|
|
2847
|
+
if (hours < 24) return `${hours}h ago`;
|
|
2848
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
2849
|
+
}
|
|
2850
|
+
function availableTabs(options) {
|
|
2851
|
+
const tabs = [];
|
|
2852
|
+
if (options.render) tabs.push("render");
|
|
2853
|
+
if (options.video) tabs.push("video");
|
|
2854
|
+
if (options.bake) tabs.push("bake");
|
|
2855
|
+
return tabs;
|
|
2856
|
+
}
|
|
2857
|
+
function inertPanel() {
|
|
2858
|
+
const noop = () => {
|
|
2859
|
+
};
|
|
2860
|
+
return {
|
|
2861
|
+
open: noop,
|
|
2862
|
+
close: noop,
|
|
2863
|
+
setTab: noop,
|
|
2864
|
+
refresh: noop,
|
|
2865
|
+
refreshCost: noop,
|
|
2866
|
+
setStatus: noop,
|
|
2867
|
+
setProgress: noop,
|
|
2868
|
+
setJobs: noop,
|
|
2869
|
+
trackJob: () => noop,
|
|
2870
|
+
startJob: () => ({ update: noop, follow: noop, end: noop }),
|
|
2871
|
+
dispose: noop
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
var AVATAR_URL = "https://cdn.bakery3.com/sdk/brand/avatar-6fffdec6ef.png";
|
|
2875
|
+
function brandMark(size) {
|
|
2876
|
+
const img = document.createElement("img");
|
|
2877
|
+
img.src = AVATAR_URL;
|
|
2878
|
+
img.alt = "";
|
|
2879
|
+
img.width = size;
|
|
2880
|
+
img.height = size;
|
|
2881
|
+
img.decoding = "async";
|
|
2882
|
+
img.draggable = false;
|
|
2883
|
+
Object.assign(img.style, {
|
|
2884
|
+
display: "block",
|
|
2885
|
+
flex: "0 0 auto",
|
|
2886
|
+
width: `${size}px`,
|
|
2887
|
+
height: `${size}px`,
|
|
2888
|
+
borderRadius: "50%",
|
|
2889
|
+
background: INK
|
|
2890
|
+
});
|
|
2891
|
+
img.onerror = () => img.replaceWith(convergeMark(Math.round(size * 0.65)));
|
|
2892
|
+
return img;
|
|
2893
|
+
}
|
|
2894
|
+
var MARK_CELLS = [
|
|
2895
|
+
[23, 3, "#2d5daf"],
|
|
2896
|
+
[43, 3, "#46b8ed"],
|
|
2897
|
+
[3, 23, "#2d5daf"],
|
|
2898
|
+
[23, 23, "#46b8ed"],
|
|
2899
|
+
[63, 23, "#fdc385"],
|
|
2900
|
+
[3, 43, "#46b8ed"],
|
|
2901
|
+
[23, 43, "#ffd4ae"],
|
|
2902
|
+
[43, 43, "#fdc385"],
|
|
2903
|
+
[63, 43, "#ec9d3c"],
|
|
2904
|
+
[83, 43, "#be5c20"],
|
|
2905
|
+
[23, 63, "#fdc385"],
|
|
2906
|
+
[63, 63, "#be5c20"],
|
|
2907
|
+
[43, 83, "#be5c20"]
|
|
2908
|
+
];
|
|
2909
|
+
function convergeMark(size) {
|
|
2910
|
+
const NS = "http://www.w3.org/2000/svg";
|
|
2911
|
+
const svg = document.createElementNS(NS, "svg");
|
|
2912
|
+
svg.setAttribute("viewBox", "0 0 100 100");
|
|
2913
|
+
svg.setAttribute("width", String(size));
|
|
2914
|
+
svg.setAttribute("height", String(size));
|
|
2915
|
+
svg.setAttribute("aria-hidden", "true");
|
|
2916
|
+
Object.assign(svg.style, { display: "block", flex: "0 0 auto" });
|
|
2917
|
+
for (const [x, y, fill] of MARK_CELLS) {
|
|
2918
|
+
const rect = document.createElementNS(NS, "rect");
|
|
2919
|
+
rect.setAttribute("x", String(x));
|
|
2920
|
+
rect.setAttribute("y", String(y));
|
|
2921
|
+
rect.setAttribute("width", "14");
|
|
2922
|
+
rect.setAttribute("height", "14");
|
|
2923
|
+
rect.setAttribute("rx", "3");
|
|
2924
|
+
rect.setAttribute("fill", fill);
|
|
2925
|
+
svg.appendChild(rect);
|
|
2926
|
+
}
|
|
2927
|
+
return svg;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// src/devtools/sceneVersion.ts
|
|
2931
|
+
function sceneFingerprint(scene) {
|
|
2932
|
+
let count = 0;
|
|
2933
|
+
let triangles = 0;
|
|
2934
|
+
let hash = 0;
|
|
2935
|
+
const mix = (value) => {
|
|
2936
|
+
hash = Math.imul(hash, 31) + value | 0;
|
|
2937
|
+
};
|
|
2938
|
+
scene.traverse((object) => {
|
|
2939
|
+
count += 1;
|
|
2940
|
+
if (!object.visible) mix(count);
|
|
2941
|
+
const mesh = object;
|
|
2942
|
+
const geometry = mesh.geometry;
|
|
2943
|
+
if (geometry) {
|
|
2944
|
+
mix(geometry.id);
|
|
2945
|
+
triangles += (geometry.index?.count ?? geometry.attributes?.position?.count ?? 0) / 3;
|
|
2946
|
+
}
|
|
2947
|
+
const materials = mesh.material;
|
|
2948
|
+
if (Array.isArray(materials)) {
|
|
2949
|
+
for (const material of materials) mix(material.id ?? 0);
|
|
2950
|
+
} else if (materials) {
|
|
2951
|
+
mix(materials.id ?? 0);
|
|
2952
|
+
}
|
|
2953
|
+
if (typeof mesh.count === "number") mix(mesh.count);
|
|
2954
|
+
});
|
|
2955
|
+
return `${count}:${Math.round(triangles)}:${hash}:${scene.fog ? 1 : 0}`;
|
|
2956
|
+
}
|
|
2957
|
+
function viewFingerprint(scene, camera) {
|
|
2958
|
+
let sum = 0;
|
|
2959
|
+
scene.traverse((object) => {
|
|
2960
|
+
const elements = object.matrix?.elements;
|
|
2961
|
+
if (elements) for (let i = 12; i < 15; i++) sum += elements[i] ?? 0;
|
|
2962
|
+
});
|
|
2963
|
+
const eye = camera.matrixWorld.elements;
|
|
2964
|
+
return `${sum.toFixed(3)}:${eye[12].toFixed(2)},${eye[13].toFixed(2)},${eye[14].toFixed(2)}`;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// src/devtools/bakery3Devtools.ts
|
|
2968
|
+
function mountBakery3Devtools(options) {
|
|
2969
|
+
const { bakery3 } = options;
|
|
2970
|
+
const bakeSession = createBakeSession({ scene: options.scene });
|
|
2971
|
+
const timeline = options.timeline ?? liveTimeline(options);
|
|
2972
|
+
const ownTimeline = options.timeline === void 0;
|
|
2973
|
+
let disposed = false;
|
|
2974
|
+
const running = /* @__PURE__ */ new Set();
|
|
2975
|
+
const capture = () => ({
|
|
2976
|
+
scene: options.scene,
|
|
2977
|
+
camera: options.camera,
|
|
2978
|
+
...options.renderer ? { renderer: options.renderer } : {}
|
|
2979
|
+
});
|
|
2980
|
+
function startRow(label) {
|
|
2981
|
+
const row = panel.startJob(label);
|
|
2982
|
+
let job;
|
|
2983
|
+
const stop = () => {
|
|
2984
|
+
row.end();
|
|
2985
|
+
job?.stop?.();
|
|
2986
|
+
};
|
|
2987
|
+
running.add(stop);
|
|
2988
|
+
return {
|
|
2989
|
+
/** For `onUpload`: the bytes on their way, or the cache hit. */
|
|
2990
|
+
upload(progress) {
|
|
2991
|
+
if (!progress.bytesTotal) {
|
|
2992
|
+
row.update("already uploaded");
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
row.update(
|
|
2996
|
+
`uploading ${megabytes(progress.bytesUploaded)} of ${megabytes(progress.bytesTotal)}`,
|
|
2997
|
+
progress.bytesUploaded / progress.bytesTotal
|
|
2998
|
+
);
|
|
2999
|
+
},
|
|
3000
|
+
update: (detail, state) => row.update(detail, void 0, state),
|
|
3001
|
+
follow(next) {
|
|
3002
|
+
job = next;
|
|
3003
|
+
row.follow(next);
|
|
3004
|
+
},
|
|
3005
|
+
end() {
|
|
3006
|
+
running.delete(stop);
|
|
3007
|
+
row.end();
|
|
3008
|
+
}
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
const panel = mountDevPanel({
|
|
3012
|
+
...options.container ? { container: options.container } : {},
|
|
3013
|
+
...options.position ? { position: options.position } : {},
|
|
3014
|
+
...options.defaultOpen !== void 0 ? { defaultOpen: options.defaultOpen } : {},
|
|
3015
|
+
...options.defaultTab ? { defaultTab: options.defaultTab } : {},
|
|
3016
|
+
...options.title ? { title: options.title } : {},
|
|
3017
|
+
...options.onOpenChange ? { onOpenChange: options.onOpenChange } : {},
|
|
3018
|
+
compatibility: () => {
|
|
3019
|
+
try {
|
|
3020
|
+
return bakery3.check(options.scene);
|
|
3021
|
+
} catch {
|
|
3022
|
+
return null;
|
|
3023
|
+
}
|
|
3024
|
+
},
|
|
3025
|
+
sceneVersion: () => sceneFingerprint(options.scene),
|
|
3026
|
+
viewVersion: () => viewFingerprint(options.scene, options.camera),
|
|
3027
|
+
estimate: async (request) => {
|
|
3028
|
+
if (request.kind === "bake") {
|
|
3029
|
+
if (!request.include.length) return null;
|
|
3030
|
+
const estimate2 = await bakery3.estimate({
|
|
3031
|
+
scene: options.scene,
|
|
3032
|
+
bake: {
|
|
3033
|
+
quality: request.quality,
|
|
3034
|
+
textureSize: request.textureSize,
|
|
3035
|
+
include: request.include
|
|
3036
|
+
}
|
|
3037
|
+
});
|
|
3038
|
+
return { usd: estimate2.estimatedCost, maximum: estimate2.maximumCost };
|
|
3039
|
+
}
|
|
3040
|
+
const estimate = await bakery3.estimate({
|
|
3041
|
+
scene: options.scene,
|
|
3042
|
+
// An interior is priced from where the camera stands, and a vanilla
|
|
3043
|
+
// client has nothing attached to fall back on: without it every
|
|
3044
|
+
// estimate threw and the chip stayed blank.
|
|
3045
|
+
camera: options.camera,
|
|
3046
|
+
quality: request.quality,
|
|
3047
|
+
width: request.width,
|
|
3048
|
+
height: request.height,
|
|
3049
|
+
// The route prices a clip itself (a frame in a clip is cheaper than
|
|
3050
|
+
// a still), so the chip quotes what the render will be accepted under.
|
|
3051
|
+
...request.kind === "video" ? { frames: request.frames } : {}
|
|
3052
|
+
});
|
|
3053
|
+
return { usd: estimate.estimatedCost, maximum: estimate.maximumCost };
|
|
3054
|
+
},
|
|
3055
|
+
...options.history ? { history: options.history } : {},
|
|
3056
|
+
render: {
|
|
3057
|
+
defaultQuality: options.defaultQuality ?? "studio",
|
|
3058
|
+
// The camera's own aspect, by the rule render() uses when it is given
|
|
3059
|
+
// no size, so a wide viewport renders wide.
|
|
3060
|
+
aspect: () => aspectOf(options.camera, options.renderer),
|
|
3061
|
+
onRender: async (settings) => {
|
|
3062
|
+
const startedAt = Date.now();
|
|
3063
|
+
const row = startRow(
|
|
3064
|
+
`render \xB7 ${settings.width} \xD7 ${settings.height} \xB7 ${settings.quality}`
|
|
3065
|
+
);
|
|
3066
|
+
let job;
|
|
3067
|
+
try {
|
|
3068
|
+
job = await bakery3.render({
|
|
3069
|
+
...capture(),
|
|
3070
|
+
quality: settings.quality,
|
|
3071
|
+
width: settings.width,
|
|
3072
|
+
height: settings.height,
|
|
3073
|
+
onUpload: row.upload
|
|
3074
|
+
});
|
|
3075
|
+
if (disposed) return void 0;
|
|
3076
|
+
row.follow(job);
|
|
3077
|
+
const result = await job.result();
|
|
3078
|
+
return {
|
|
3079
|
+
id: job.id,
|
|
3080
|
+
url: result.url,
|
|
3081
|
+
width: result.width,
|
|
3082
|
+
height: result.height,
|
|
3083
|
+
format: result.format,
|
|
3084
|
+
quality: settings.quality,
|
|
3085
|
+
cost: result.cost,
|
|
3086
|
+
...result.expiresAt ? { expiresAt: result.expiresAt } : {},
|
|
3087
|
+
meta: `$${result.cost.toFixed(2)} \xB7 ${result.width} \xD7 ${result.height} \xB7 ${elapsed(startedAt)}`
|
|
3088
|
+
};
|
|
3089
|
+
} catch (error) {
|
|
3090
|
+
if (disposed) return void 0;
|
|
3091
|
+
if (job && wasCancelled(job, error)) {
|
|
3092
|
+
panel.setStatus("Render cancelled.");
|
|
3093
|
+
return void 0;
|
|
3094
|
+
}
|
|
3095
|
+
throw error;
|
|
3096
|
+
} finally {
|
|
3097
|
+
row.end();
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
},
|
|
3101
|
+
video: {
|
|
3102
|
+
timeline,
|
|
3103
|
+
...options.timelineTargets ? { targets: options.timelineTargets } : {},
|
|
3104
|
+
// The scene is handed over so the panel can re-attach tracks whose
|
|
3105
|
+
// objects were rebuilt (a variant swap, a reload) instead of letting a
|
|
3106
|
+
// stale lane survive to refuse at export.
|
|
3107
|
+
scene: () => options.scene,
|
|
3108
|
+
aspect: () => aspectOf(options.camera, options.renderer),
|
|
3109
|
+
onPlaybackChange: (active) => options.onPlaybackChange?.(active),
|
|
3110
|
+
defaultQuality: options.defaultQuality ?? "studio",
|
|
3111
|
+
onRenderVideo: async (settings) => {
|
|
3112
|
+
const startedAt = Date.now();
|
|
3113
|
+
const row = startRow(
|
|
3114
|
+
`video \xB7 ${settings.timeline.frameCount} frames \xB7 ${settings.width} \xD7 ${settings.height}`
|
|
3115
|
+
);
|
|
3116
|
+
let job;
|
|
3117
|
+
try {
|
|
3118
|
+
job = await bakery3.renderVideo({
|
|
3119
|
+
...capture(),
|
|
3120
|
+
timeline: settings.timeline,
|
|
3121
|
+
quality: settings.quality,
|
|
3122
|
+
width: settings.width,
|
|
3123
|
+
height: settings.height,
|
|
3124
|
+
format: settings.format,
|
|
3125
|
+
onUpload: row.upload
|
|
3126
|
+
});
|
|
3127
|
+
if (disposed) return void 0;
|
|
3128
|
+
row.follow(job);
|
|
3129
|
+
const result = await job.result();
|
|
3130
|
+
return {
|
|
3131
|
+
id: job.id,
|
|
3132
|
+
url: result.url,
|
|
3133
|
+
isVideo: true,
|
|
3134
|
+
width: result.width,
|
|
3135
|
+
height: result.height,
|
|
3136
|
+
format: result.format,
|
|
3137
|
+
frames: settings.timeline.frameCount,
|
|
3138
|
+
quality: settings.quality,
|
|
3139
|
+
cost: result.cost,
|
|
3140
|
+
...result.expiresAt ? { expiresAt: result.expiresAt } : {},
|
|
3141
|
+
meta: `$${result.cost.toFixed(2)} \xB7 ${elapsed(startedAt)}`
|
|
3142
|
+
};
|
|
3143
|
+
} catch (error) {
|
|
3144
|
+
if (disposed) return void 0;
|
|
3145
|
+
if (job && wasCancelled(job, error)) {
|
|
3146
|
+
panel.setStatus("Video cancelled.");
|
|
3147
|
+
return void 0;
|
|
3148
|
+
}
|
|
3149
|
+
throw error;
|
|
3150
|
+
} finally {
|
|
3151
|
+
row.end();
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
},
|
|
3155
|
+
bake: {
|
|
3156
|
+
session: bakeSession,
|
|
3157
|
+
onBake: async (settings) => {
|
|
3158
|
+
const transport = options.bakeTransport;
|
|
3159
|
+
const count = settings.include.length;
|
|
3160
|
+
const row = startRow(
|
|
3161
|
+
`bake \xB7 ${count} object${count === 1 ? "" : "s"} at ${settings.textureSize}\xB2`
|
|
3162
|
+
);
|
|
3163
|
+
try {
|
|
3164
|
+
const bakeSettings = {
|
|
3165
|
+
quality: settings.quality,
|
|
3166
|
+
textureSize: settings.textureSize,
|
|
3167
|
+
include: settings.include
|
|
3168
|
+
};
|
|
3169
|
+
let bundle;
|
|
3170
|
+
if (transport) {
|
|
3171
|
+
const { spec, assets } = await prepareBake({
|
|
3172
|
+
scene: options.scene,
|
|
3173
|
+
camera: options.camera,
|
|
3174
|
+
settings: bakeSettings
|
|
3175
|
+
});
|
|
3176
|
+
row.update("baking", "baking");
|
|
3177
|
+
bundle = await transport(spec, assets);
|
|
3178
|
+
} else {
|
|
3179
|
+
if (!bakery3.bake) {
|
|
3180
|
+
throw new Bakery3Error({
|
|
3181
|
+
code: "InvalidRequest",
|
|
3182
|
+
message: "This client cannot bake.",
|
|
3183
|
+
why: "The devtools were mounted with a client that has no bake() and no bakeTransport was given.",
|
|
3184
|
+
fix: "Mount them with the object createBakery3() returns, or pass a bakeTransport.",
|
|
3185
|
+
docs: "https://bakery3.com/docs/devtools"
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
const job = await bakery3.bake({
|
|
3189
|
+
...capture(),
|
|
3190
|
+
settings: bakeSettings,
|
|
3191
|
+
onUpload: row.upload
|
|
3192
|
+
});
|
|
3193
|
+
if (disposed) return void 0;
|
|
3194
|
+
row.follow(trackableBake(job));
|
|
3195
|
+
try {
|
|
3196
|
+
bundle = (await job.result()).bundle;
|
|
3197
|
+
} catch (error) {
|
|
3198
|
+
if (disposed) return void 0;
|
|
3199
|
+
if (wasCancelled(job, error)) {
|
|
3200
|
+
panel.setStatus("Bake cancelled.");
|
|
3201
|
+
return void 0;
|
|
3202
|
+
}
|
|
3203
|
+
throw error;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
if (disposed) return void 0;
|
|
3207
|
+
row.update("applying the lightmaps", "applying");
|
|
3208
|
+
const generation = bakeSession.addGeneration(bundle, {
|
|
3209
|
+
...transport && options.bakeBaseUrl ? { baseUrl: options.bakeBaseUrl } : {}
|
|
3210
|
+
});
|
|
3211
|
+
const report = await bakeSession.apply(generation);
|
|
3212
|
+
if (disposed) return void 0;
|
|
3213
|
+
if (options.muteLightsOnApply) bakeSession.muteLights(true);
|
|
3214
|
+
panel.setStatus(
|
|
3215
|
+
`Applied to ${report.applied.length} object${report.applied.length === 1 ? "" : "s"}.`,
|
|
3216
|
+
"ok"
|
|
3217
|
+
);
|
|
3218
|
+
return { generation };
|
|
3219
|
+
} finally {
|
|
3220
|
+
row.end();
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
});
|
|
3225
|
+
return {
|
|
3226
|
+
...panel,
|
|
3227
|
+
dispose() {
|
|
3228
|
+
if (disposed) return;
|
|
3229
|
+
disposed = true;
|
|
3230
|
+
for (const stop of [...running]) stop();
|
|
3231
|
+
running.clear();
|
|
3232
|
+
panel.dispose();
|
|
3233
|
+
if (ownTimeline) timeline.dispose();
|
|
3234
|
+
bakeSession.dispose();
|
|
3235
|
+
}
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
function liveTimeline(options) {
|
|
3239
|
+
const timeline = createTimeline({ duration: 4, fps: 24 });
|
|
3240
|
+
for (const key of ["scene", "camera"]) {
|
|
3241
|
+
Object.defineProperty(timeline, key, {
|
|
3242
|
+
configurable: true,
|
|
3243
|
+
enumerable: true,
|
|
3244
|
+
get: () => options[key],
|
|
3245
|
+
// The timeline never reassigns these itself; a host that does wants the
|
|
3246
|
+
// live one anyway.
|
|
3247
|
+
set: () => {
|
|
3248
|
+
}
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
return timeline;
|
|
3252
|
+
}
|
|
3253
|
+
function wasCancelled(job, error) {
|
|
3254
|
+
return job.status === "cancelled" || error?.code === "Cancelled";
|
|
3255
|
+
}
|
|
3256
|
+
function trackableBake(job) {
|
|
3257
|
+
return {
|
|
3258
|
+
id: job.id,
|
|
3259
|
+
get status() {
|
|
3260
|
+
return job.status;
|
|
3261
|
+
},
|
|
3262
|
+
get progress() {
|
|
3263
|
+
return job.progress;
|
|
3264
|
+
},
|
|
3265
|
+
on(event, listener) {
|
|
3266
|
+
if (event === "preview") return () => {
|
|
3267
|
+
};
|
|
3268
|
+
return job.on(
|
|
3269
|
+
event,
|
|
3270
|
+
(payload) => listener(payload)
|
|
3271
|
+
);
|
|
3272
|
+
},
|
|
3273
|
+
cancel: () => job.cancel(),
|
|
3274
|
+
stop: () => job.stop()
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
function megabytes(bytes) {
|
|
3278
|
+
return `${(bytes / 1e6).toFixed(1)} MB`;
|
|
3279
|
+
}
|
|
3280
|
+
function elapsed(startedAt) {
|
|
3281
|
+
return `${Math.max(0, (Date.now() - startedAt) / 1e3).toFixed(1)}s`;
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
export { mountBakery3Devtools, mountDevPanel, mountTimelinePanel };
|
|
3285
|
+
//# sourceMappingURL=chunk-QRCT5UGZ.js.map
|
|
3286
|
+
//# sourceMappingURL=chunk-QRCT5UGZ.js.map
|