@osmo.inc/cli-linux-arm64 0.1.19 → 0.1.20
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/dist/studio.js +920 -329
- package/package.json +1 -1
- package/release.json +32 -32
- package/runtime/apps/native/tools/compile.mjs +10 -2
- package/runtime/packages/scene/src/assets.ts +27 -0
- package/runtime/packages/scene/src/elements.ts +29 -13
- package/runtime/packages/scene/src/evaluate.ts +58 -21
- package/runtime/packages/scene/src/index.ts +4 -0
- package/runtime/packages/scene/src/jsx-runtime.ts +9 -3
- package/runtime/packages/scene/src/knob-schema.ts +5 -2
- package/runtime/packages/scene/src/knobs.ts +50 -10
- package/runtime/packages/scene/src/motion-sampling.d.mts +1 -0
- package/runtime/packages/scene/src/motion-sampling.mjs +33 -0
- package/runtime/packages/scene/src/motion.ts +5 -5
- package/runtime/packages/scene/src/text-animation.ts +239 -0
- package/studio/src/app.css +1 -1
- package/studio/src/app.js +207 -198
- package/studio/src/engine_bg.wasm +0 -0
- package/third-party/inventory.json +1061 -78
- package/third-party/npm/@phosphor-icons__react@2.1.10/LICENSE +21 -0
- package/third-party/npm/@phosphor-icons__react@2.1.10/package.json +113 -0
- package/third-party/npm/@radix-ui__number@1.1.1/package.json +62 -0
- package/third-party/npm/@radix-ui__number@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__primitive@1.1.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__primitive@1.1.3/package.json +47 -0
- package/third-party/npm/@radix-ui__react-accessible-icon@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-accessible-icon@1.1.7/package.json +68 -0
- package/third-party/npm/@radix-ui__react-accordion@1.2.12/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-accordion@1.2.12/package.json +76 -0
- package/third-party/npm/@radix-ui__react-alert-dialog@1.1.15/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-alert-dialog@1.1.15/package.json +73 -0
- package/third-party/npm/@radix-ui__react-arrow@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-arrow@1.1.7/package.json +68 -0
- package/third-party/npm/@radix-ui__react-aspect-ratio@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-aspect-ratio@1.1.7/package.json +68 -0
- package/third-party/npm/@radix-ui__react-avatar@1.1.10/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-avatar@1.1.10/package.json +72 -0
- package/third-party/npm/@radix-ui__react-checkbox@1.3.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-checkbox@1.3.3/package.json +75 -0
- package/third-party/npm/@radix-ui__react-collapsible@1.1.12/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-collapsible@1.1.12/package.json +75 -0
- package/third-party/npm/@radix-ui__react-collection@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-collection@1.1.7/package.json +71 -0
- package/third-party/npm/@radix-ui__react-compose-refs@1.1.2/package.json +75 -0
- package/third-party/npm/@radix-ui__react-compose-refs@1.1.2/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-context-menu@2.2.16/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-context-menu@2.2.16/package.json +74 -0
- package/third-party/npm/@radix-ui__react-context@1.1.2/package.json +75 -0
- package/third-party/npm/@radix-ui__react-context@1.1.2/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-dialog@1.1.15/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-dialog@1.1.15/package.json +81 -0
- package/third-party/npm/@radix-ui__react-direction@1.1.1/package.json +75 -0
- package/third-party/npm/@radix-ui__react-direction@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-dismissable-layer@1.1.11/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-dismissable-layer@1.1.11/package.json +73 -0
- package/third-party/npm/@radix-ui__react-dropdown-menu@2.1.16/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-dropdown-menu@2.1.16/package.json +75 -0
- package/third-party/npm/@radix-ui__react-focus-guards@1.1.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-focus-guards@1.1.3/package.json +60 -0
- package/third-party/npm/@radix-ui__react-focus-scope@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-focus-scope@1.1.7/package.json +70 -0
- package/third-party/npm/@radix-ui__react-form@0.1.8/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-form@0.1.8/package.json +73 -0
- package/third-party/npm/@radix-ui__react-hover-card@1.1.15/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-hover-card@1.1.15/package.json +76 -0
- package/third-party/npm/@radix-ui__react-id@1.1.1/package.json +78 -0
- package/third-party/npm/@radix-ui__react-id@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-label@2.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-label@2.1.7/package.json +68 -0
- package/third-party/npm/@radix-ui__react-menu@2.1.16/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-menu@2.1.16/package.json +86 -0
- package/third-party/npm/@radix-ui__react-menubar@1.1.16/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-menubar@1.1.16/package.json +78 -0
- package/third-party/npm/@radix-ui__react-navigation-menu@1.2.14/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-navigation-menu@1.2.14/package.json +81 -0
- package/third-party/npm/@radix-ui__react-one-time-password-field@0.1.8/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-one-time-password-field@0.1.8/package.json +80 -0
- package/third-party/npm/@radix-ui__react-password-toggle-field@0.1.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-password-toggle-field@0.1.3/package.json +76 -0
- package/third-party/npm/@radix-ui__react-popover@1.1.15/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-popover@1.1.15/package.json +82 -0
- package/third-party/npm/@radix-ui__react-popper@1.2.8/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-popper@1.2.8/package.json +77 -0
- package/third-party/npm/@radix-ui__react-portal@1.1.9/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-portal@1.1.9/package.json +69 -0
- package/third-party/npm/@radix-ui__react-presence@1.1.5/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-presence@1.1.5/package.json +69 -0
- package/third-party/npm/@radix-ui__react-primitive@2.1.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-primitive@2.1.3/package.json +68 -0
- package/third-party/npm/@radix-ui__react-progress@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-progress@1.1.7/package.json +69 -0
- package/third-party/npm/@radix-ui__react-radio-group@1.3.8/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-radio-group@1.3.8/package.json +77 -0
- package/third-party/npm/@radix-ui__react-roving-focus@1.1.11/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-roving-focus@1.1.11/package.json +76 -0
- package/third-party/npm/@radix-ui__react-scroll-area@1.2.10/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-scroll-area@1.2.10/package.json +76 -0
- package/third-party/npm/@radix-ui__react-select@2.2.6/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-select@2.2.6/package.json +89 -0
- package/third-party/npm/@radix-ui__react-separator@1.1.7/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-separator@1.1.7/package.json +68 -0
- package/third-party/npm/@radix-ui__react-slider@1.3.6/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-slider@1.3.6/package.json +78 -0
- package/third-party/npm/@radix-ui__react-slot@1.2.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-slot@1.2.3/package.json +63 -0
- package/third-party/npm/@radix-ui__react-switch@1.2.6/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-switch@1.2.6/package.json +74 -0
- package/third-party/npm/@radix-ui__react-tabs@1.1.13/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-tabs@1.1.13/package.json +75 -0
- package/third-party/npm/@radix-ui__react-toast@1.2.15/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-toast@1.2.15/package.json +79 -0
- package/third-party/npm/@radix-ui__react-toggle-group@1.1.11/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-toggle-group@1.1.11/package.json +74 -0
- package/third-party/npm/@radix-ui__react-toggle@1.1.10/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-toggle@1.1.10/package.json +70 -0
- package/third-party/npm/@radix-ui__react-toolbar@1.1.11/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-toolbar@1.1.11/package.json +74 -0
- package/third-party/npm/@radix-ui__react-tooltip@1.2.8/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-tooltip@1.2.8/package.json +79 -0
- package/third-party/npm/@radix-ui__react-use-callback-ref@1.1.1/package.json +75 -0
- package/third-party/npm/@radix-ui__react-use-callback-ref@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-controllable-state@1.2.2/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-controllable-state@1.2.2/package.json +64 -0
- package/third-party/npm/@radix-ui__react-use-effect-event@0.0.2/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-effect-event@0.0.2/package.json +65 -0
- package/third-party/npm/@radix-ui__react-use-escape-keydown@1.1.1/package.json +78 -0
- package/third-party/npm/@radix-ui__react-use-escape-keydown@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-is-hydrated@0.1.0/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-is-hydrated@0.1.0/package.json +65 -0
- package/third-party/npm/@radix-ui__react-use-layout-effect@1.1.1/package.json +75 -0
- package/third-party/npm/@radix-ui__react-use-layout-effect@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-previous@1.1.1/package.json +75 -0
- package/third-party/npm/@radix-ui__react-use-previous@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-use-size@1.1.1/package.json +79 -0
- package/third-party/npm/@radix-ui__react-use-size@1.1.1/radix-ui-primitives-ee4e3197bffe-LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-visually-hidden@1.2.3/LICENSE +21 -0
- package/third-party/npm/@radix-ui__react-visually-hidden@1.2.3/package.json +68 -0
- package/third-party/npm/color-convert@3.1.3/LICENSE +22 -0
- package/third-party/npm/color-convert@3.1.3/package.json +58 -0
- package/third-party/npm/color-name@2.1.0/LICENSE +8 -0
- package/third-party/npm/color-name@2.1.0/package.json +32 -0
- package/third-party/npm/color-string@2.1.4/LICENSE +21 -0
- package/third-party/npm/color-string@2.1.4/package.json +47 -0
- package/third-party/npm/color@5.0.3/LICENSE +21 -0
- package/third-party/npm/color@5.0.3/package.json +52 -0
- package/third-party/npm/radix-ui@1.4.3/LICENSE +21 -0
- package/third-party/npm/radix-ui@1.4.3/package.json +136 -0
- package/third-party/npm/use-sync-external-store@1.6.0/LICENSE +21 -0
- package/third-party/npm/use-sync-external-store@1.6.0/package.json +43 -0
- package/vendor/osmo-lite +0 -0
- package/vendor/osmo-scene-0.1.0.tgz +0 -0
- package/vendor/platform.json +1 -1
package/dist/studio.js
CHANGED
|
@@ -215,10 +215,170 @@ var init_labels = __esm({
|
|
|
215
215
|
}
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
// src/
|
|
219
|
-
import {
|
|
218
|
+
// src/local-attachment-picker.ts
|
|
219
|
+
import { execFile } from "node:child_process";
|
|
220
|
+
import { promisify } from "node:util";
|
|
221
|
+
import { realpath, stat } from "node:fs/promises";
|
|
220
222
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
221
223
|
import path7 from "node:path";
|
|
224
|
+
async function registerLocalAttachment(filename2) {
|
|
225
|
+
const sourcePath = await realpath(filename2), info = await stat(sourcePath);
|
|
226
|
+
if (!info.isFile()) throw new Error("Choose a file, not a folder.");
|
|
227
|
+
const localRef = randomUUID2();
|
|
228
|
+
const file = {
|
|
229
|
+
name: path7.basename(sourcePath),
|
|
230
|
+
type: types[path7.extname(sourcePath).toLowerCase()] ?? "application/octet-stream",
|
|
231
|
+
size: info.size,
|
|
232
|
+
sourcePath
|
|
233
|
+
};
|
|
234
|
+
selections.set(localRef, file);
|
|
235
|
+
return { localRef, name: file.name, type: file.type, size: file.size };
|
|
236
|
+
}
|
|
237
|
+
function selectedLocalAttachment(token) {
|
|
238
|
+
const file = selections.get(token);
|
|
239
|
+
if (!file)
|
|
240
|
+
throw new Error("Select this file again; its local selection expired.");
|
|
241
|
+
return file;
|
|
242
|
+
}
|
|
243
|
+
async function pickLocalAttachments() {
|
|
244
|
+
if (picking) throw new Error("The file picker is already open.");
|
|
245
|
+
if (process.platform !== "darwin")
|
|
246
|
+
throw new Error("Local reference selection currently requires macOS.");
|
|
247
|
+
picking = true;
|
|
248
|
+
try {
|
|
249
|
+
const script = 'var app=Application.currentApplication(); app.includeStandardAdditions=true; try {var files=app.chooseFile({withPrompt:"Attach original files (no copies)",multipleSelectionsAllowed:true}); JSON.stringify(files.map(function(f){return f.toString()}));} catch(e) {if(e.errorNumber===-128) JSON.stringify([]); else throw e;}';
|
|
250
|
+
const { stdout } = await exec(
|
|
251
|
+
"/usr/bin/osascript",
|
|
252
|
+
["-l", "JavaScript", "-e", script],
|
|
253
|
+
{ maxBuffer: 8 * 1024 * 1024 }
|
|
254
|
+
);
|
|
255
|
+
const paths = JSON.parse(stdout);
|
|
256
|
+
return Promise.all(
|
|
257
|
+
paths.map((file) => registerLocalAttachment(file))
|
|
258
|
+
);
|
|
259
|
+
} finally {
|
|
260
|
+
picking = false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
var exec, selections, types, picking;
|
|
264
|
+
var init_local_attachment_picker = __esm({
|
|
265
|
+
"src/local-attachment-picker.ts"() {
|
|
266
|
+
"use strict";
|
|
267
|
+
exec = promisify(execFile);
|
|
268
|
+
selections = /* @__PURE__ */ new Map();
|
|
269
|
+
types = {
|
|
270
|
+
".svg": "image/svg+xml",
|
|
271
|
+
".png": "image/png",
|
|
272
|
+
".jpg": "image/jpeg",
|
|
273
|
+
".jpeg": "image/jpeg",
|
|
274
|
+
".webp": "image/webp",
|
|
275
|
+
".gif": "image/gif",
|
|
276
|
+
".avif": "image/avif",
|
|
277
|
+
".mp4": "video/mp4",
|
|
278
|
+
".mov": "video/quicktime",
|
|
279
|
+
".pdf": "application/pdf",
|
|
280
|
+
".txt": "text/plain"
|
|
281
|
+
};
|
|
282
|
+
picking = false;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// src/comment-attachments.ts
|
|
287
|
+
import { mkdir as mkdir3, writeFile as writeFile2, rm as rm3, realpath as realpath2, readFile as readFile3 } from "node:fs/promises";
|
|
288
|
+
import path8 from "node:path";
|
|
289
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
290
|
+
function validateUploads(value) {
|
|
291
|
+
if (value === void 0) return [];
|
|
292
|
+
if (!Array.isArray(value)) throw new Error("Invalid attachments.");
|
|
293
|
+
if (value.some((f) => f?.localRef !== void 0 && (typeof f.localRef !== "string" || !f.localRef)))
|
|
294
|
+
throw new Error("Invalid local attachment reference.");
|
|
295
|
+
if (value.filter((f) => typeof f?.localRef !== "string").length > 5)
|
|
296
|
+
throw new Error("Choose up to 5 cloud files.");
|
|
297
|
+
let total = 0;
|
|
298
|
+
return value.map((f) => {
|
|
299
|
+
if (f && typeof f.localRef === "string")
|
|
300
|
+
return { ...selectedLocalAttachment(f.localRef) };
|
|
301
|
+
if (!f || typeof f.name !== "string" || !f.name.trim() || f.name.length > 255 || typeof f.type !== "string" || f.type.length > 255 || typeof f.data !== "string" || f.data.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(f.data))
|
|
302
|
+
throw new Error("Invalid attachment.");
|
|
303
|
+
if (f.data.length > 4 * Math.ceil(15 * 1024 * 1024 / 3))
|
|
304
|
+
throw new Error("Each file must be 15 MB or smaller.");
|
|
305
|
+
const bytes = Buffer.from(f.data, "base64");
|
|
306
|
+
if (bytes.length > 15 * 1024 * 1024)
|
|
307
|
+
throw new Error("Each file must be 15 MB or smaller.");
|
|
308
|
+
total += bytes.length;
|
|
309
|
+
if (total > 50 * 1024 * 1024)
|
|
310
|
+
throw new Error("Files must total 50 MB or less.");
|
|
311
|
+
return { name: f.name, type: f.type, bytes };
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async function storeAttachments(root, id, uploads, saved) {
|
|
315
|
+
const project = await realpath2(root);
|
|
316
|
+
for (const file of uploads) {
|
|
317
|
+
if (file.sourcePath) {
|
|
318
|
+
const { stat: stat4 } = await import("node:fs/promises");
|
|
319
|
+
const info = await stat4(file.sourcePath);
|
|
320
|
+
if (!info.isFile())
|
|
321
|
+
throw new Error("The attached file is no longer available.");
|
|
322
|
+
saved.push({
|
|
323
|
+
name: file.name,
|
|
324
|
+
path: file.sourcePath,
|
|
325
|
+
type: file.type,
|
|
326
|
+
size: info.size,
|
|
327
|
+
local: true
|
|
328
|
+
});
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const bytes = file.bytes;
|
|
332
|
+
const folder = path8.join(root, ".osmo", "attachments", id, randomUUID3());
|
|
333
|
+
await mkdir3(folder, { recursive: true });
|
|
334
|
+
const resolved = await realpath2(folder);
|
|
335
|
+
if (!resolved.startsWith(project + path8.sep))
|
|
336
|
+
throw new Error("Attachment folder is outside the project.");
|
|
337
|
+
const name = file.name.replace(/[\\/\x00-\x1f\x7f]/g, "_").replace(/^\.+$/, "file");
|
|
338
|
+
const relative = path8.relative(root, path8.join(folder, name));
|
|
339
|
+
saved.push({
|
|
340
|
+
name: file.name,
|
|
341
|
+
path: relative,
|
|
342
|
+
type: file.type,
|
|
343
|
+
size: bytes.length
|
|
344
|
+
});
|
|
345
|
+
await writeFile2(path8.join(folder, name), bytes, { flag: "wx" });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async function cleanupAttachments(root, files) {
|
|
349
|
+
for (const file of files.filter((file2) => !file2.local))
|
|
350
|
+
await rm3(path8.dirname(path8.join(root, file.path)), {
|
|
351
|
+
recursive: true,
|
|
352
|
+
force: true
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
async function attachmentPath(root, file) {
|
|
356
|
+
if (file.local) {
|
|
357
|
+
if (!path8.isAbsolute(file.path))
|
|
358
|
+
throw new Error("Invalid local attachment reference.");
|
|
359
|
+
try {
|
|
360
|
+
return await realpath2(file.path);
|
|
361
|
+
} catch {
|
|
362
|
+
throw new Error("Original file was moved or removed. Attach it again.");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const base = await realpath2(path8.join(root, ".osmo", "attachments"));
|
|
366
|
+
const resolved = await realpath2(path8.resolve(root, file.path));
|
|
367
|
+
if (!resolved.startsWith(base + path8.sep))
|
|
368
|
+
throw new Error("Invalid attachment path.");
|
|
369
|
+
return resolved;
|
|
370
|
+
}
|
|
371
|
+
var init_comment_attachments = __esm({
|
|
372
|
+
"src/comment-attachments.ts"() {
|
|
373
|
+
"use strict";
|
|
374
|
+
init_local_attachment_picker();
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// src/comments.ts
|
|
379
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile3, rename as rename2, rm as rm4 } from "node:fs/promises";
|
|
380
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
381
|
+
import path9 from "node:path";
|
|
222
382
|
function validBounds(value) {
|
|
223
383
|
return !!value && ["x", "y", "width", "height"].every((key) => Number.isFinite(value[key])) && value.width >= 0 && value.height >= 0;
|
|
224
384
|
}
|
|
@@ -242,7 +402,7 @@ function validAgentResponse(value) {
|
|
|
242
402
|
}
|
|
243
403
|
async function readComments(root) {
|
|
244
404
|
try {
|
|
245
|
-
const data = JSON.parse(await
|
|
405
|
+
const data = JSON.parse(await readFile4(filename(root), "utf8"));
|
|
246
406
|
if (data.version !== 1 || !Array.isArray(data.comments))
|
|
247
407
|
throw new Error("Unsupported comments file");
|
|
248
408
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -270,13 +430,13 @@ function commentText(value) {
|
|
|
270
430
|
return value.trim();
|
|
271
431
|
}
|
|
272
432
|
async function mutateComments(root, change) {
|
|
273
|
-
const folder =
|
|
274
|
-
await
|
|
275
|
-
const lock =
|
|
433
|
+
const folder = path9.join(root, ".osmo");
|
|
434
|
+
await mkdir4(folder, { recursive: true });
|
|
435
|
+
const lock = path9.join(folder, "comments.lock");
|
|
276
436
|
const start = Date.now();
|
|
277
437
|
while (true) {
|
|
278
438
|
try {
|
|
279
|
-
await
|
|
439
|
+
await mkdir4(lock);
|
|
280
440
|
break;
|
|
281
441
|
} catch (e) {
|
|
282
442
|
if (e.code !== "EEXIST") throw e;
|
|
@@ -287,22 +447,23 @@ async function mutateComments(root, change) {
|
|
|
287
447
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
288
448
|
}
|
|
289
449
|
}
|
|
290
|
-
const temporary =
|
|
450
|
+
const temporary = path9.join(folder, `comments.${randomUUID4()}.tmp`);
|
|
291
451
|
try {
|
|
292
452
|
const data = await readComments(root);
|
|
293
|
-
const result = change(data.comments);
|
|
294
|
-
await
|
|
453
|
+
const result = await change(data.comments);
|
|
454
|
+
await writeFile3(temporary, JSON.stringify(data, null, 2) + "\n", {
|
|
295
455
|
flag: "wx"
|
|
296
456
|
});
|
|
297
457
|
await rename2(temporary, filename(root));
|
|
298
458
|
return result;
|
|
299
459
|
} finally {
|
|
300
|
-
await
|
|
301
|
-
await
|
|
460
|
+
await rm4(temporary, { force: true });
|
|
461
|
+
await rm4(lock, { recursive: true, force: true });
|
|
302
462
|
}
|
|
303
463
|
}
|
|
304
464
|
async function addComment(root, input) {
|
|
305
|
-
const
|
|
465
|
+
const uploads = validateUploads(input.attachments);
|
|
466
|
+
const text = uploads.length && typeof input.text === "string" && !input.text.trim() ? "" : commentText(input.text);
|
|
306
467
|
if (typeof input.scene !== "string" || !input.scene || input.scene.startsWith("/") || input.scene.split(/[\\/]/).includes(".."))
|
|
307
468
|
throw new Error("Invalid comment scene");
|
|
308
469
|
if (!Number.isFinite(input.time) || input.time < 0 || input.endTime !== void 0 && (!Number.isFinite(input.endTime) || input.endTime < input.time))
|
|
@@ -313,42 +474,60 @@ async function addComment(root, input) {
|
|
|
313
474
|
throw new Error("Invalid comment target");
|
|
314
475
|
if (input.author !== void 0 && !validAuthor(input.author))
|
|
315
476
|
throw new Error("Invalid comment author");
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
477
|
+
const saved = [];
|
|
478
|
+
try {
|
|
479
|
+
return await mutateComments(root, async (comments) => {
|
|
480
|
+
const comment = {
|
|
481
|
+
id: `c_${randomUUID4().slice(0, 8)}`,
|
|
482
|
+
scene: input.scene,
|
|
483
|
+
time: input.time,
|
|
484
|
+
...input.endTime === void 0 ? {} : { endTime: input.endTime },
|
|
485
|
+
...input.target === void 0 ? {} : { target: input.target },
|
|
486
|
+
...input.author === void 0 ? {} : { author: input.author },
|
|
487
|
+
text,
|
|
488
|
+
status: "open",
|
|
489
|
+
revision: input.revision,
|
|
490
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
491
|
+
replies: []
|
|
492
|
+
};
|
|
493
|
+
await storeAttachments(root, comment.id, uploads, saved);
|
|
494
|
+
if (saved.length) comment.attachments = saved;
|
|
495
|
+
comments.push(comment);
|
|
496
|
+
return comment;
|
|
497
|
+
});
|
|
498
|
+
} catch (error) {
|
|
499
|
+
await cleanupAttachments(root, saved);
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
333
502
|
}
|
|
334
|
-
async function updateComment(root, id, action, text) {
|
|
503
|
+
async function updateComment(root, id, action, text, attachments) {
|
|
335
504
|
if (!["reply", "resolve", "reopen"].includes(action))
|
|
336
505
|
throw new Error("Unknown comment action");
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
506
|
+
const uploads = validateUploads(attachments);
|
|
507
|
+
if (action === "reply")
|
|
508
|
+
text = uploads.length && text === "" ? "" : commentText(text);
|
|
509
|
+
const saved = [];
|
|
510
|
+
try {
|
|
511
|
+
return await mutateComments(root, async (comments) => {
|
|
512
|
+
const comment = comments.find((c) => c.id === id);
|
|
513
|
+
if (!comment) throw new Error(`Comment not found: ${id}`);
|
|
514
|
+
if (action === "reply") {
|
|
515
|
+
await storeAttachments(root, id, uploads, saved);
|
|
516
|
+
comment.replies.push({
|
|
517
|
+
...saved.length ? { attachments: saved } : {},
|
|
518
|
+
id: randomUUID4(),
|
|
519
|
+
// commentText returned it above: this is a reply.
|
|
520
|
+
text,
|
|
521
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
522
|
+
});
|
|
523
|
+
} else comment.status = action === "resolve" ? "resolved" : "open";
|
|
524
|
+
comment.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
525
|
+
return comment;
|
|
526
|
+
});
|
|
527
|
+
} catch (error) {
|
|
528
|
+
await cleanupAttachments(root, saved);
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
352
531
|
}
|
|
353
532
|
async function updateCommentRange(root, id, input) {
|
|
354
533
|
if (!Number.isFinite(input.time) || input.time < 0 || !Number.isFinite(input.endTime) || input.endTime <= input.time)
|
|
@@ -397,13 +576,131 @@ var filename, AGENT_REACTION_MIN_MS, graphemes, isoTime;
|
|
|
397
576
|
var init_comments = __esm({
|
|
398
577
|
"src/comments.ts"() {
|
|
399
578
|
"use strict";
|
|
400
|
-
|
|
579
|
+
init_comment_attachments();
|
|
580
|
+
filename = (root) => path9.join(root, ".osmo/comments.json");
|
|
401
581
|
AGENT_REACTION_MIN_MS = 3e4;
|
|
402
582
|
graphemes = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
403
583
|
isoTime = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
404
584
|
}
|
|
405
585
|
});
|
|
406
586
|
|
|
587
|
+
// src/attachment-thumbnail.ts
|
|
588
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
589
|
+
import { promisify as promisify2 } from "node:util";
|
|
590
|
+
import { stat as stat2 } from "node:fs/promises";
|
|
591
|
+
async function attachmentThumbnail(filename2) {
|
|
592
|
+
const info = await stat2(filename2), key = `${filename2}:${info.mtimeMs}:${info.size}`;
|
|
593
|
+
const existing = cache.get(key);
|
|
594
|
+
if (existing) return existing;
|
|
595
|
+
const result = exec2(
|
|
596
|
+
"ffmpeg",
|
|
597
|
+
[
|
|
598
|
+
"-hide_banner",
|
|
599
|
+
"-loglevel",
|
|
600
|
+
"error",
|
|
601
|
+
"-i",
|
|
602
|
+
filename2,
|
|
603
|
+
"-frames:v",
|
|
604
|
+
"1",
|
|
605
|
+
"-vf",
|
|
606
|
+
"scale=96:96:force_original_aspect_ratio=decrease",
|
|
607
|
+
"-f",
|
|
608
|
+
"image2pipe",
|
|
609
|
+
"-vcodec",
|
|
610
|
+
"mjpeg",
|
|
611
|
+
"pipe:1"
|
|
612
|
+
],
|
|
613
|
+
{ encoding: "buffer", maxBuffer: 1024 * 1024, timeout: 15e3 }
|
|
614
|
+
).then(({ stdout }) => {
|
|
615
|
+
if (!stdout.length) throw new Error("No video preview");
|
|
616
|
+
return stdout;
|
|
617
|
+
}).catch((error) => {
|
|
618
|
+
cache.delete(key);
|
|
619
|
+
throw error;
|
|
620
|
+
});
|
|
621
|
+
if (cache.size >= 64) cache.delete(cache.keys().next().value);
|
|
622
|
+
cache.set(key, result);
|
|
623
|
+
return result;
|
|
624
|
+
}
|
|
625
|
+
var exec2, cache;
|
|
626
|
+
var init_attachment_thumbnail = __esm({
|
|
627
|
+
"src/attachment-thumbnail.ts"() {
|
|
628
|
+
"use strict";
|
|
629
|
+
exec2 = promisify2(execFile2);
|
|
630
|
+
cache = /* @__PURE__ */ new Map();
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// ../scene/src/assets.ts
|
|
635
|
+
function validAssetPath(kind, value) {
|
|
636
|
+
return Object.hasOwn(assetExtensions, kind) && typeof value === "string" && value.length > 0 && value.length <= 4096 && value === value.trim() && !/^[a-z][a-z0-9+.-]*:|^[/\\]|[\x00-\x1f?#\\]/i.test(value) && assetExtensions[kind].includes(value.split(".").at(-1).toLowerCase());
|
|
637
|
+
}
|
|
638
|
+
var assetExtensions;
|
|
639
|
+
var init_assets = __esm({
|
|
640
|
+
"../scene/src/assets.ts"() {
|
|
641
|
+
"use strict";
|
|
642
|
+
assetExtensions = {
|
|
643
|
+
image: ["png", "jpg", "jpeg", "webp", "gif", "svg"],
|
|
644
|
+
video: ["mp4", "webm", "mov", "m4v"],
|
|
645
|
+
audio: ["wav", "mp3", "m4a", "aac", "ogg", "flac"],
|
|
646
|
+
font: ["ttf", "otf"],
|
|
647
|
+
model: ["glb"]
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// src/asset-import.ts
|
|
653
|
+
import fs2 from "node:fs/promises";
|
|
654
|
+
import path13 from "node:path";
|
|
655
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
656
|
+
async function importProjectAsset(root, stream, name, kind, limit = MAX_ASSET_BYTES) {
|
|
657
|
+
if (!Object.hasOwn(assetExtensions, kind))
|
|
658
|
+
throw new Error("Unknown asset kind");
|
|
659
|
+
if (!name || name.length > 255 || /[/\\\x00-\x1f]/.test(name))
|
|
660
|
+
throw new Error("Invalid asset filename");
|
|
661
|
+
const extension = path13.extname(name).slice(1).toLowerCase();
|
|
662
|
+
if (!assetExtensions[kind].includes(extension))
|
|
663
|
+
throw new Error(
|
|
664
|
+
`Expected a ${kind} file (${assetExtensions[kind].join(", ")})`
|
|
665
|
+
);
|
|
666
|
+
const folder = path13.join(root, "assets");
|
|
667
|
+
await fs2.mkdir(folder).catch((error) => {
|
|
668
|
+
if (error.code !== "EEXIST") throw error;
|
|
669
|
+
});
|
|
670
|
+
const directory = sceneInProject(root, "assets").source;
|
|
671
|
+
const stem = path13.basename(name, path13.extname(name)).normalize("NFKC").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "asset";
|
|
672
|
+
const filename2 = `${stem}-${randomUUID5()}.${extension}`;
|
|
673
|
+
const target = path13.join(directory, filename2);
|
|
674
|
+
const temp = path13.join(directory, `.${filename2}.upload`);
|
|
675
|
+
const file = await fs2.open(temp, "wx", 384);
|
|
676
|
+
let size = 0;
|
|
677
|
+
try {
|
|
678
|
+
for await (const chunk of stream) {
|
|
679
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
680
|
+
size += bytes.length;
|
|
681
|
+
if (size > limit)
|
|
682
|
+
throw new Error(`Asset exceeds ${Math.round(limit / 1024 / 1024)} MB`);
|
|
683
|
+
await file.writeFile(bytes);
|
|
684
|
+
}
|
|
685
|
+
if (!size) throw new Error("Asset file is empty");
|
|
686
|
+
await file.close();
|
|
687
|
+
await fs2.link(temp, target);
|
|
688
|
+
return path13.relative(await fs2.realpath(root), target).split(path13.sep).join("/");
|
|
689
|
+
} finally {
|
|
690
|
+
await file.close();
|
|
691
|
+
await fs2.rm(temp, { force: true });
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
var MAX_ASSET_BYTES;
|
|
695
|
+
var init_asset_import = __esm({
|
|
696
|
+
"src/asset-import.ts"() {
|
|
697
|
+
"use strict";
|
|
698
|
+
init_assets();
|
|
699
|
+
init_runtime();
|
|
700
|
+
MAX_ASSET_BYTES = 256 * 1024 * 1024;
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
407
704
|
// ../studio/src/lib/export-settings.ts
|
|
408
705
|
function validateExportSettings(input, meta) {
|
|
409
706
|
const format2 = input.format ?? "h264";
|
|
@@ -621,9 +918,9 @@ var init_storyboard = __esm({
|
|
|
621
918
|
});
|
|
622
919
|
|
|
623
920
|
// src/storyboard.ts
|
|
624
|
-
import
|
|
625
|
-
import
|
|
626
|
-
import { createHash as createHash4, randomUUID as
|
|
921
|
+
import fs3 from "node:fs/promises";
|
|
922
|
+
import path14 from "node:path";
|
|
923
|
+
import { createHash as createHash4, randomUUID as randomUUID6 } from "node:crypto";
|
|
627
924
|
import ts4 from "typescript";
|
|
628
925
|
function literal(n) {
|
|
629
926
|
if (!n) return void 0;
|
|
@@ -640,7 +937,7 @@ function literal(n) {
|
|
|
640
937
|
);
|
|
641
938
|
}
|
|
642
939
|
async function groupInfo(source, relative) {
|
|
643
|
-
const text = await
|
|
940
|
+
const text = await fs3.readFile(source, "utf8"), ast = ts4.createSourceFile(
|
|
644
941
|
source,
|
|
645
942
|
text,
|
|
646
943
|
ts4.ScriptTarget.Latest,
|
|
@@ -675,7 +972,7 @@ async function groupInfo(source, relative) {
|
|
|
675
972
|
};
|
|
676
973
|
}
|
|
677
974
|
function createStoryboard(root) {
|
|
678
|
-
const filename2 =
|
|
975
|
+
const filename2 = path14.join(root, "storyboard.canvas.json"), cache2 = path14.join(root, ".osmo/cache/storyboard");
|
|
679
976
|
let queue = Promise.resolve(), undo = [], redo = [], knownRevision, activeRenderer, closed = false;
|
|
680
977
|
const serial = (fn) => {
|
|
681
978
|
const result = queue.then(fn);
|
|
@@ -686,7 +983,7 @@ function createStoryboard(root) {
|
|
|
686
983
|
async function read() {
|
|
687
984
|
let document;
|
|
688
985
|
try {
|
|
689
|
-
document = validateBoard(JSON.parse(await
|
|
986
|
+
document = validateBoard(JSON.parse(await fs3.readFile(filename2, "utf8")));
|
|
690
987
|
} catch (error) {
|
|
691
988
|
if (error.code !== "ENOENT") throw error;
|
|
692
989
|
document = structuredClone(empty);
|
|
@@ -703,17 +1000,17 @@ function createStoryboard(root) {
|
|
|
703
1000
|
const result = [];
|
|
704
1001
|
async function scan(dir, depth = 0) {
|
|
705
1002
|
if (depth > 8 || result.length >= 100) return;
|
|
706
|
-
for (const e of await
|
|
1003
|
+
for (const e of await fs3.readdir(dir, { withFileTypes: true })) {
|
|
707
1004
|
if (e.name.startsWith(".") || ["node_modules", "exports", "dist", "target", "assets"].includes(
|
|
708
1005
|
e.name
|
|
709
1006
|
))
|
|
710
1007
|
continue;
|
|
711
|
-
const absolute =
|
|
1008
|
+
const absolute = path14.join(dir, e.name);
|
|
712
1009
|
if (e.isDirectory()) await scan(absolute, depth + 1);
|
|
713
1010
|
else if (e.isFile() && e.name.endsWith(".tsx")) {
|
|
714
1011
|
const info2 = await groupInfo(
|
|
715
1012
|
absolute,
|
|
716
|
-
|
|
1013
|
+
path14.relative(root, absolute).split(path14.sep).join("/")
|
|
717
1014
|
);
|
|
718
1015
|
if (info2) result.push(info2);
|
|
719
1016
|
}
|
|
@@ -745,12 +1042,12 @@ function createStoryboard(root) {
|
|
|
745
1042
|
};
|
|
746
1043
|
}
|
|
747
1044
|
async function write(document) {
|
|
748
|
-
const temp = `${filename2}.${
|
|
1045
|
+
const temp = `${filename2}.${randomUUID6()}.tmp`;
|
|
749
1046
|
try {
|
|
750
|
-
await
|
|
751
|
-
await
|
|
1047
|
+
await fs3.writeFile(temp, JSON.stringify(document, null, 2) + "\n");
|
|
1048
|
+
await fs3.rename(temp, filename2);
|
|
752
1049
|
} finally {
|
|
753
|
-
await
|
|
1050
|
+
await fs3.rm(temp, { force: true });
|
|
754
1051
|
}
|
|
755
1052
|
knownRevision = hash(JSON.stringify(document));
|
|
756
1053
|
}
|
|
@@ -802,8 +1099,8 @@ function createStoryboard(root) {
|
|
|
802
1099
|
await info(card);
|
|
803
1100
|
const { source } = sceneInProject(root, card.source), validation = await check(source);
|
|
804
1101
|
if (!validation.ok) throw new Error(validation.errors.join("\n"));
|
|
805
|
-
await
|
|
806
|
-
const name = `${
|
|
1102
|
+
await fs3.mkdir(cache2, { recursive: true });
|
|
1103
|
+
const name = `${randomUUID6()}.mp4`, destination = path14.join(cache2, name);
|
|
807
1104
|
const renderer = new Renderer({
|
|
808
1105
|
preview: {
|
|
809
1106
|
width: card.width,
|
|
@@ -819,7 +1116,7 @@ function createStoryboard(root) {
|
|
|
819
1116
|
await renderer.request("export", { destination, supersample: 1 });
|
|
820
1117
|
return { name, revision: current.revision };
|
|
821
1118
|
} catch (error) {
|
|
822
|
-
await
|
|
1119
|
+
await fs3.rm(destination, { force: true });
|
|
823
1120
|
throw error;
|
|
824
1121
|
} finally {
|
|
825
1122
|
renderer.close();
|
|
@@ -829,7 +1126,7 @@ function createStoryboard(root) {
|
|
|
829
1126
|
video: async (name) => {
|
|
830
1127
|
if (!/^[0-9a-f-]{36}\.mp4$/.test(name))
|
|
831
1128
|
throw new Error("Invalid preview");
|
|
832
|
-
return
|
|
1129
|
+
return fs3.readFile(path14.join(cache2, name));
|
|
833
1130
|
},
|
|
834
1131
|
close() {
|
|
835
1132
|
closed = true;
|
|
@@ -850,6 +1147,198 @@ var init_storyboard2 = __esm({
|
|
|
850
1147
|
}
|
|
851
1148
|
});
|
|
852
1149
|
|
|
1150
|
+
// src/asset-metadata.ts
|
|
1151
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
1152
|
+
import { promisify as promisify3 } from "node:util";
|
|
1153
|
+
import path15 from "node:path";
|
|
1154
|
+
async function inspectAsset(file, size) {
|
|
1155
|
+
const metadata = { size };
|
|
1156
|
+
const extension = path15.extname(file).slice(1).toLowerCase();
|
|
1157
|
+
const image = ["png", "jpg", "jpeg", "webp", "gif"].includes(extension);
|
|
1158
|
+
const video = ["mp4", "webm", "mov", "m4v"].includes(extension);
|
|
1159
|
+
const audio = ["wav", "mp3", "m4a", "aac", "ogg", "flac"].includes(extension);
|
|
1160
|
+
if (!image && !video && !audio) return metadata;
|
|
1161
|
+
try {
|
|
1162
|
+
const { stdout } = await exec3(
|
|
1163
|
+
process.env.OSMO_FFPROBE ?? "ffprobe",
|
|
1164
|
+
[
|
|
1165
|
+
"-v",
|
|
1166
|
+
"error",
|
|
1167
|
+
"-protocol_whitelist",
|
|
1168
|
+
"file,pipe",
|
|
1169
|
+
"-show_entries",
|
|
1170
|
+
"stream=codec_type,width,height,avg_frame_rate,duration,sample_rate:stream_disposition=attached_pic:stream_tags=rotate:stream_side_data=rotation:format=duration",
|
|
1171
|
+
"-of",
|
|
1172
|
+
"json",
|
|
1173
|
+
file
|
|
1174
|
+
],
|
|
1175
|
+
{ timeout: 5e3, maxBuffer: 1024 * 1024 }
|
|
1176
|
+
);
|
|
1177
|
+
const data = JSON.parse(stdout);
|
|
1178
|
+
const stream = data.streams?.find(
|
|
1179
|
+
(s) => s.codec_type === (audio ? "audio" : "video") && !s.disposition?.attached_pic
|
|
1180
|
+
);
|
|
1181
|
+
const assign = (key, value) => {
|
|
1182
|
+
const number = Number(value);
|
|
1183
|
+
if (Number.isFinite(number) && number > 0) metadata[key] = number;
|
|
1184
|
+
};
|
|
1185
|
+
if (image || video) {
|
|
1186
|
+
const rotation = Number(
|
|
1187
|
+
stream?.side_data_list?.find(
|
|
1188
|
+
(s) => s.rotation !== void 0
|
|
1189
|
+
)?.rotation ?? stream?.tags?.rotate ?? 0
|
|
1190
|
+
);
|
|
1191
|
+
const portrait = Math.abs(rotation % 180) === 90;
|
|
1192
|
+
assign("width", portrait ? stream?.height : stream?.width);
|
|
1193
|
+
assign("height", portrait ? stream?.width : stream?.height);
|
|
1194
|
+
}
|
|
1195
|
+
if (video) {
|
|
1196
|
+
const [n, d = "1"] = String(stream?.avg_frame_rate).split("/");
|
|
1197
|
+
assign("fps", Number(n) / Number(d));
|
|
1198
|
+
}
|
|
1199
|
+
if (audio || video)
|
|
1200
|
+
assign("duration", stream?.duration ?? data.format?.duration);
|
|
1201
|
+
if (audio) assign("sampleRate", stream?.sample_rate);
|
|
1202
|
+
} catch {
|
|
1203
|
+
}
|
|
1204
|
+
return metadata;
|
|
1205
|
+
}
|
|
1206
|
+
var exec3;
|
|
1207
|
+
var init_asset_metadata = __esm({
|
|
1208
|
+
"src/asset-metadata.ts"() {
|
|
1209
|
+
"use strict";
|
|
1210
|
+
exec3 = promisify3(execFile3);
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
// ../editor/src/types.ts
|
|
1215
|
+
function nodeKnobs(node) {
|
|
1216
|
+
return node.type === "Composition" && isSceneNode(node.children[0]) ? node.children[0].props.knobs : node.props.knobs;
|
|
1217
|
+
}
|
|
1218
|
+
function nodeValues(node) {
|
|
1219
|
+
return node.type === "Composition" && isSceneNode(node.children[0]) ? { ...node.children[0].props.values, ...node.props.values } : node.props.values;
|
|
1220
|
+
}
|
|
1221
|
+
var isSceneNode;
|
|
1222
|
+
var init_types = __esm({
|
|
1223
|
+
"../editor/src/types.ts"() {
|
|
1224
|
+
"use strict";
|
|
1225
|
+
isSceneNode = (child) => !!child?.type;
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
// ../editor/src/document-assets.ts
|
|
1230
|
+
function documentAssetReferences(document) {
|
|
1231
|
+
const assets = [];
|
|
1232
|
+
function resolve(value, scope, seen = /* @__PURE__ */ new Set()) {
|
|
1233
|
+
if (!value?.$ref) return [{ value }];
|
|
1234
|
+
const key = `${value.$ref}:${value.name}`;
|
|
1235
|
+
if (seen.has(key)) throw new Error("Cyclic asset binding");
|
|
1236
|
+
const binding = document.bindings[value.$ref];
|
|
1237
|
+
if (binding?.kind === "knobs")
|
|
1238
|
+
return scope.get(value.$ref)?.get(value.name) ?? [];
|
|
1239
|
+
if (binding?.kind === "motion") {
|
|
1240
|
+
const track = binding.definition[value.name];
|
|
1241
|
+
return [
|
|
1242
|
+
track?.default,
|
|
1243
|
+
...(track?.keyframes ?? []).map((k) => k.value)
|
|
1244
|
+
].flatMap(
|
|
1245
|
+
(v) => resolve(v, scope, /* @__PURE__ */ new Set([...seen, key])).map((r) => ({
|
|
1246
|
+
...r,
|
|
1247
|
+
animated: true
|
|
1248
|
+
}))
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
return [];
|
|
1252
|
+
}
|
|
1253
|
+
function add(file, value, kind, scope) {
|
|
1254
|
+
for (const v of resolve(value, scope))
|
|
1255
|
+
if (typeof v.value === "string" && !(kind === "font" && v.value === "Inter")) {
|
|
1256
|
+
if (v.kind && v.kind !== kind)
|
|
1257
|
+
throw new Error(
|
|
1258
|
+
`Knob ${v.knob}: cannot use ${v.kind} asset as ${kind}`
|
|
1259
|
+
);
|
|
1260
|
+
assets.push({
|
|
1261
|
+
file: v.file ?? file,
|
|
1262
|
+
value: v.value,
|
|
1263
|
+
kind,
|
|
1264
|
+
...v.knob ? { knob: v.knob } : {}
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function walk(n, parent, override) {
|
|
1269
|
+
const scope = n.type === "Scene" ? /* @__PURE__ */ new Map() : new Map(parent), set = nodeKnobs(n)?.$ref;
|
|
1270
|
+
const schema = document.bindings[set];
|
|
1271
|
+
if (n.type === "Composition") {
|
|
1272
|
+
const child = n.children[0];
|
|
1273
|
+
if (isSceneNode(child))
|
|
1274
|
+
walk(child, parent, { values: n.props.values, file: n.source.file });
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (schema?.kind === "knobs") {
|
|
1278
|
+
const values = { ...n.props.values, ...override?.values };
|
|
1279
|
+
scope.set(
|
|
1280
|
+
set,
|
|
1281
|
+
new Map(
|
|
1282
|
+
Object.entries(schema.definition).map(([name, raw]) => {
|
|
1283
|
+
const knob = raw, supplied = values[name] !== void 0;
|
|
1284
|
+
const file = !supplied ? schema.source.file : override?.values?.[name] !== void 0 ? override.file : n.valueSources?.[name] ?? n.source.file;
|
|
1285
|
+
const resolved = resolve(
|
|
1286
|
+
supplied ? values[name] : knob.default,
|
|
1287
|
+
parent
|
|
1288
|
+
);
|
|
1289
|
+
if (knob.type === "asset")
|
|
1290
|
+
for (const v of resolved) {
|
|
1291
|
+
if (v.animated)
|
|
1292
|
+
throw new Error(
|
|
1293
|
+
`Knob ${name}: asset bindings must be static`
|
|
1294
|
+
);
|
|
1295
|
+
if (v.kind && v.kind !== knob.kind)
|
|
1296
|
+
throw new Error(
|
|
1297
|
+
`Knob ${name}: cannot bind ${v.kind} to ${knob.kind}`
|
|
1298
|
+
);
|
|
1299
|
+
if (!validAssetPath(knob.kind, v.value))
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
`Knob ${name}: expected a local ${knob.kind} asset path`
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
return [
|
|
1305
|
+
name,
|
|
1306
|
+
resolved.map(
|
|
1307
|
+
(v) => knob.type === "asset" ? {
|
|
1308
|
+
...v,
|
|
1309
|
+
file: v.file ?? v.origin ?? file,
|
|
1310
|
+
knob: v.knob ?? name,
|
|
1311
|
+
kind: knob.kind
|
|
1312
|
+
} : { ...v, origin: v.origin ?? file }
|
|
1313
|
+
)
|
|
1314
|
+
];
|
|
1315
|
+
})
|
|
1316
|
+
)
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
if (n.props.src)
|
|
1320
|
+
add(n.sourceFile, n.props.src, n.type.toLowerCase(), scope);
|
|
1321
|
+
if (n.props.font) add(n.sourceFile, n.props.font, "font", scope);
|
|
1322
|
+
for (const c of n.children) if (isSceneNode(c)) walk(c, scope);
|
|
1323
|
+
}
|
|
1324
|
+
walk(
|
|
1325
|
+
document.root,
|
|
1326
|
+
/* @__PURE__ */ new Map(),
|
|
1327
|
+
document.root.type === "Group" && document.preview?.values ? { values: document.preview.values, file: document.entry } : void 0
|
|
1328
|
+
);
|
|
1329
|
+
for (const b of Object.values(document.bindings))
|
|
1330
|
+
if (b.kind === "effect" && b.definition.src)
|
|
1331
|
+
add(b.source.file, b.definition.src, "shader", /* @__PURE__ */ new Map());
|
|
1332
|
+
return assets;
|
|
1333
|
+
}
|
|
1334
|
+
var init_document_assets = __esm({
|
|
1335
|
+
"../editor/src/document-assets.ts"() {
|
|
1336
|
+
"use strict";
|
|
1337
|
+
init_assets();
|
|
1338
|
+
init_types();
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
|
|
853
1342
|
// ../editor/src/source-path.ts
|
|
854
1343
|
function normalize(value) {
|
|
855
1344
|
const parts = [];
|
|
@@ -869,15 +1358,6 @@ var init_source_path = __esm({
|
|
|
869
1358
|
}
|
|
870
1359
|
});
|
|
871
1360
|
|
|
872
|
-
// ../editor/src/types.ts
|
|
873
|
-
var isSceneNode;
|
|
874
|
-
var init_types = __esm({
|
|
875
|
-
"../editor/src/types.ts"() {
|
|
876
|
-
"use strict";
|
|
877
|
-
isSceneNode = (child) => !!child?.type;
|
|
878
|
-
}
|
|
879
|
-
});
|
|
880
|
-
|
|
881
1361
|
// ../editor/src/scene-document.ts
|
|
882
1362
|
import ts5 from "typescript";
|
|
883
1363
|
function decodeJsxLiteral(raw, attribute = false) {
|
|
@@ -1053,7 +1533,8 @@ function createSceneDocument(entry, files) {
|
|
|
1053
1533
|
"opacity",
|
|
1054
1534
|
"effects",
|
|
1055
1535
|
"hidden",
|
|
1056
|
-
"solo"
|
|
1536
|
+
"solo",
|
|
1537
|
+
"values"
|
|
1057
1538
|
]);
|
|
1058
1539
|
for (const name of Object.keys(props))
|
|
1059
1540
|
if (!allowed.has(name))
|
|
@@ -1088,6 +1569,7 @@ function createSceneDocument(entry, files) {
|
|
|
1088
1569
|
...props.values ? { values: { ...base.props.values, ...props.values } } : {}
|
|
1089
1570
|
},
|
|
1090
1571
|
source: location(opening3),
|
|
1572
|
+
valueSources: { ...base.valueSources, ...Object.fromEntries(Object.keys(props.values ?? {}).map((k) => [k, file])) },
|
|
1091
1573
|
definition: base.source.file,
|
|
1092
1574
|
identity: props.id ?? null,
|
|
1093
1575
|
tag
|
|
@@ -1221,7 +1703,7 @@ function outlineDocument(document, meta) {
|
|
|
1221
1703
|
...n.props,
|
|
1222
1704
|
start: 0,
|
|
1223
1705
|
...document.preview?.values ? { values: { ...n.props.values, ...document.preview.values } } : {}
|
|
1224
|
-
} : n.props, local = new Map(scope), set =
|
|
1706
|
+
} : n.props, local = new Map(scope), set = nodeKnobs(n)?.$ref, own = document.bindings[set];
|
|
1225
1707
|
if (own)
|
|
1226
1708
|
local.set(
|
|
1227
1709
|
set,
|
|
@@ -1231,7 +1713,7 @@ function outlineDocument(document, meta) {
|
|
|
1231
1713
|
).map(([k, s]) => [
|
|
1232
1714
|
k,
|
|
1233
1715
|
resolve(
|
|
1234
|
-
p.values?.[k] ?? s.default,
|
|
1716
|
+
(n.type === "Composition" ? nodeValues(n) : p.values)?.[k] ?? s.default,
|
|
1235
1717
|
scope,
|
|
1236
1718
|
clock.offset + (isClipPrimitive2(n.type) && (p.start !== void 0 || p.duration !== void 0 || p.track !== void 0) ? p.start ?? 0 : 0),
|
|
1237
1719
|
n.id
|
|
@@ -1386,12 +1868,13 @@ var init_document_outline = __esm({
|
|
|
1386
1868
|
});
|
|
1387
1869
|
|
|
1388
1870
|
// src/scene-document.ts
|
|
1389
|
-
import
|
|
1390
|
-
import
|
|
1871
|
+
import fs4 from "node:fs/promises";
|
|
1872
|
+
import path16 from "node:path";
|
|
1391
1873
|
import { createHash as createHash5 } from "node:crypto";
|
|
1392
1874
|
import ts6 from "typescript";
|
|
1393
1875
|
function createDocumentLoader(root) {
|
|
1394
1876
|
const assets = /* @__PURE__ */ new Map();
|
|
1877
|
+
const metadata = /* @__PURE__ */ new Map();
|
|
1395
1878
|
async function load(source, text, preview = {}, inputs = /* @__PURE__ */ new Map(), includeSources = false) {
|
|
1396
1879
|
const entry = sceneInProject(root, source).scene, files = {};
|
|
1397
1880
|
async function read(file, body2, stack = []) {
|
|
@@ -1399,7 +1882,7 @@ function createDocumentLoader(root) {
|
|
|
1399
1882
|
throw new Error("Cyclic animation imports");
|
|
1400
1883
|
if (Object.hasOwn(files, file)) return;
|
|
1401
1884
|
const located = sceneInProject(root, file);
|
|
1402
|
-
files[file] = body2 ?? inputs.get(located.source) ?? await
|
|
1885
|
+
files[file] = body2 ?? inputs.get(located.source) ?? await fs4.readFile(located.source, "utf8");
|
|
1403
1886
|
const ast = ts6.createSourceFile(
|
|
1404
1887
|
file,
|
|
1405
1888
|
files[file],
|
|
@@ -1423,49 +1906,24 @@ function createDocumentLoader(root) {
|
|
|
1423
1906
|
const document = createSceneDocument(entry, files);
|
|
1424
1907
|
document.preview = preview;
|
|
1425
1908
|
document.assets = {};
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
const
|
|
1429
|
-
if (
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
return;
|
|
1436
|
-
}
|
|
1437
|
-
if (typeof value !== "string" || kind === "font" && value === "Inter")
|
|
1438
|
-
return;
|
|
1439
|
-
const relative = path12.posix.normalize(
|
|
1440
|
-
path12.posix.join(path12.posix.dirname(file), value)
|
|
1441
|
-
);
|
|
1442
|
-
if (document.assets[relative]) return;
|
|
1443
|
-
document.assets[relative] = await registerAsset(relative, kind);
|
|
1444
|
-
}
|
|
1445
|
-
async function walk(n) {
|
|
1446
|
-
try {
|
|
1447
|
-
if (n.props.src)
|
|
1448
|
-
await asset(
|
|
1449
|
-
n.sourceFile,
|
|
1450
|
-
n.props.src,
|
|
1451
|
-
n.type === "Shader" ? "shader" : n.type.toLowerCase()
|
|
1452
|
-
);
|
|
1453
|
-
if (n.props.font) await asset(n.sourceFile, n.props.font, "font");
|
|
1454
|
-
} catch (error) {
|
|
1455
|
-
document.nativeReason = `Asset unavailable for browser preview: ${error.message}`;
|
|
1909
|
+
try {
|
|
1910
|
+
for (const ref of documentAssetReferences(document)) {
|
|
1911
|
+
const relative = path16.posix.normalize(path16.posix.join(path16.posix.dirname(ref.file), ref.value));
|
|
1912
|
+
if (document.assets[relative]) continue;
|
|
1913
|
+
try {
|
|
1914
|
+
document.assets[relative] = await registerAsset(relative, ref.kind);
|
|
1915
|
+
} catch (error) {
|
|
1916
|
+
throw new Error(`${ref.knob ? `Knob ${ref.knob}: ` : ""}${relative}: ${error.message}`);
|
|
1917
|
+
}
|
|
1456
1918
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
document.nativeReason = `Asset unavailable for browser preview: ${error.message}`;
|
|
1459
1921
|
}
|
|
1460
|
-
for (const binding of Object.values(document.bindings))
|
|
1461
|
-
if (binding.kind === "effect" && binding.definition.src)
|
|
1462
|
-
await asset(binding.source.file, binding.definition.src, "shader");
|
|
1463
|
-
await walk(document.root);
|
|
1464
1922
|
while (assets.size > 1e3) assets.delete(assets.keys().next().value);
|
|
1465
1923
|
return { ...document, ...includeSources ? { sourceFiles: files } : {} };
|
|
1466
1924
|
}
|
|
1467
1925
|
async function registerAsset(relative, kind) {
|
|
1468
|
-
const located = sceneInProject(root, relative), info = await
|
|
1926
|
+
const located = sceneInProject(root, relative), info = await fs4.stat(located.source);
|
|
1469
1927
|
if (!info.isFile()) throw new Error(`Asset is not a file: ${relative}`);
|
|
1470
1928
|
const id = createHash5("sha256").update(located.source + "\0" + info.mtimeMs + "\0" + info.size).digest("hex");
|
|
1471
1929
|
assets.set(id, {
|
|
@@ -1476,7 +1934,7 @@ function createDocumentLoader(root) {
|
|
|
1476
1934
|
const asset = { id, kind };
|
|
1477
1935
|
if (kind === "shader") {
|
|
1478
1936
|
if (info.size > 1024 * 1024) throw new Error("Shader exceeds 1 MB");
|
|
1479
|
-
asset.code = await
|
|
1937
|
+
asset.code = await fs4.readFile(located.source, "utf8");
|
|
1480
1938
|
}
|
|
1481
1939
|
return asset;
|
|
1482
1940
|
}
|
|
@@ -1521,26 +1979,40 @@ function createDocumentLoader(root) {
|
|
|
1521
1979
|
}
|
|
1522
1980
|
return { resources: resources2, errors };
|
|
1523
1981
|
}
|
|
1524
|
-
async function
|
|
1982
|
+
async function locateAsset(id) {
|
|
1525
1983
|
const asset = assets.get(id);
|
|
1526
1984
|
if (!asset)
|
|
1527
1985
|
throw Object.assign(new Error("Unknown scene asset"), { status: 404 });
|
|
1528
|
-
const current = sceneInProject(root, asset.file), info = await
|
|
1986
|
+
const current = sceneInProject(root, asset.file), info = await fs4.stat(current.source);
|
|
1529
1987
|
if (current.source !== asset.file || info.size !== asset.size || info.mtimeMs !== asset.mtime)
|
|
1530
1988
|
throw Object.assign(
|
|
1531
1989
|
new Error("Scene asset changed. Refresh the scene."),
|
|
1532
1990
|
{ status: 409 }
|
|
1533
1991
|
);
|
|
1992
|
+
return { file: current.source, size: info.size };
|
|
1993
|
+
}
|
|
1994
|
+
async function readAsset(id) {
|
|
1995
|
+
const current = await locateAsset(id);
|
|
1534
1996
|
return {
|
|
1535
|
-
bytes: await
|
|
1536
|
-
extension:
|
|
1997
|
+
bytes: await fs4.readFile(current.file),
|
|
1998
|
+
extension: path16.extname(current.file).toLowerCase()
|
|
1537
1999
|
};
|
|
1538
2000
|
}
|
|
1539
|
-
|
|
2001
|
+
async function assetMetadata(id) {
|
|
2002
|
+
const current = await locateAsset(id);
|
|
2003
|
+
if (!metadata.has(id))
|
|
2004
|
+
metadata.set(id, inspectAsset(current.file, current.size));
|
|
2005
|
+
while (metadata.size > 1e3)
|
|
2006
|
+
metadata.delete(metadata.keys().next().value);
|
|
2007
|
+
return metadata.get(id);
|
|
2008
|
+
}
|
|
2009
|
+
return { load, readAsset, resources, registerAsset, assetMetadata };
|
|
1540
2010
|
}
|
|
1541
2011
|
var init_scene_document2 = __esm({
|
|
1542
2012
|
"src/scene-document.ts"() {
|
|
1543
2013
|
"use strict";
|
|
2014
|
+
init_asset_metadata();
|
|
2015
|
+
init_document_assets();
|
|
1544
2016
|
init_runtime();
|
|
1545
2017
|
init_scene_document();
|
|
1546
2018
|
init_document_outline();
|
|
@@ -1548,30 +2020,30 @@ var init_scene_document2 = __esm({
|
|
|
1548
2020
|
});
|
|
1549
2021
|
|
|
1550
2022
|
// src/project-resources.ts
|
|
1551
|
-
import
|
|
1552
|
-
import
|
|
1553
|
-
import { execFile } from "node:child_process";
|
|
1554
|
-
import { promisify } from "node:util";
|
|
2023
|
+
import fs5 from "node:fs/promises";
|
|
2024
|
+
import path17 from "node:path";
|
|
2025
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
2026
|
+
import { promisify as promisify4 } from "node:util";
|
|
1555
2027
|
import ts7 from "typescript";
|
|
1556
2028
|
async function resourceFile(root, relative) {
|
|
1557
|
-
if (typeof relative !== "string" ||
|
|
2029
|
+
if (typeof relative !== "string" || path17.isAbsolute(relative))
|
|
1558
2030
|
throw new Error("Resource paths must be relative.");
|
|
1559
|
-
const base = await
|
|
1560
|
-
if (!inside(
|
|
2031
|
+
const base = await fs5.realpath(root), file = await fs5.realpath(path17.resolve(base, relative));
|
|
2032
|
+
if (!inside(path17.join(base, "resources"), file) || (await fs5.stat(file)).isDirectory())
|
|
1561
2033
|
throw new Error("Resource files must stay inside resources/.");
|
|
1562
2034
|
return file;
|
|
1563
2035
|
}
|
|
1564
2036
|
async function resourceFolder(root) {
|
|
1565
|
-
const base = await
|
|
1566
|
-
await
|
|
1567
|
-
if ((await
|
|
2037
|
+
const base = await fs5.realpath(root), folder = path17.join(base, "resources");
|
|
2038
|
+
await fs5.mkdir(folder, { recursive: true });
|
|
2039
|
+
if ((await fs5.lstat(folder)).isSymbolicLink())
|
|
1568
2040
|
throw new Error("resources/ must be a real directory.");
|
|
1569
2041
|
return folder;
|
|
1570
2042
|
}
|
|
1571
2043
|
async function listProjectResources(root) {
|
|
1572
2044
|
let entries;
|
|
1573
2045
|
try {
|
|
1574
|
-
entries = await
|
|
2046
|
+
entries = await fs5.readdir(path17.join(root, "resources"), {
|
|
1575
2047
|
withFileTypes: true
|
|
1576
2048
|
});
|
|
1577
2049
|
} catch (e) {
|
|
@@ -1583,12 +2055,12 @@ async function listProjectResources(root) {
|
|
|
1583
2055
|
if (item.name.startsWith(".") || !item.isDirectory()) continue;
|
|
1584
2056
|
try {
|
|
1585
2057
|
const relative = `resources/${item.name}/resource.json`, manifestFile = await resourceFile(root, relative);
|
|
1586
|
-
if ((await
|
|
2058
|
+
if ((await fs5.stat(manifestFile)).size > 1024 * 1024)
|
|
1587
2059
|
throw new Error("Manifest exceeds 1 MB");
|
|
1588
|
-
const meta = JSON.parse(await
|
|
2060
|
+
const meta = JSON.parse(await fs5.readFile(manifestFile, "utf8"));
|
|
1589
2061
|
const entry = `resources/${item.name}/${meta.entry}`, file = await resourceFile(root, entry);
|
|
1590
|
-
const kind = meta.kind ?? (meta.category === "3d" ? "model" : meta.category) ?? kinds[
|
|
1591
|
-
if (!["shader", "image", "video", "model", "scene"].includes(kind) || kinds[
|
|
2062
|
+
const kind = meta.kind ?? (meta.category === "3d" ? "model" : meta.category) ?? kinds[path17.extname(file).toLowerCase()];
|
|
2063
|
+
if (!["shader", "image", "video", "model", "scene"].includes(kind) || kinds[path17.extname(file).toLowerCase()] !== kind)
|
|
1592
2064
|
throw new Error("Unsupported resource type");
|
|
1593
2065
|
let effect;
|
|
1594
2066
|
if (kind === "shader" && meta.effect) {
|
|
@@ -1606,7 +2078,7 @@ async function listProjectResources(root) {
|
|
|
1606
2078
|
const dimensions = Object.fromEntries(
|
|
1607
2079
|
["width", "height", "duration"].filter((k) => Number.isFinite(meta[k]) && meta[k] > 0).map((k) => [k, meta[k]])
|
|
1608
2080
|
);
|
|
1609
|
-
const info = await
|
|
2081
|
+
const info = await fs5.stat(file), preview = meta.preview ? `resources/${item.name}/${meta.preview}` : ["image", "video"].includes(kind) ? entry : null;
|
|
1610
2082
|
if (preview) await resourceFile(root, preview);
|
|
1611
2083
|
resources.push({
|
|
1612
2084
|
id: item.name,
|
|
@@ -1621,7 +2093,7 @@ async function listProjectResources(root) {
|
|
|
1621
2093
|
} : {},
|
|
1622
2094
|
preview: preview && (meta.preview || info.size < 12 * 1024 * 1024) ? preview : null,
|
|
1623
2095
|
previewType: meta.preview ? "image" : kind,
|
|
1624
|
-
stamp: `${info.mtimeMs}:${info.size}:${(await
|
|
2096
|
+
stamp: `${info.mtimeMs}:${info.size}:${(await fs5.stat(manifestFile)).mtimeMs}`
|
|
1625
2097
|
});
|
|
1626
2098
|
} catch (error) {
|
|
1627
2099
|
if (error.code !== "ENOENT")
|
|
@@ -1630,7 +2102,7 @@ async function listProjectResources(root) {
|
|
|
1630
2102
|
}
|
|
1631
2103
|
async function scenes(folder, depth = 0) {
|
|
1632
2104
|
if (depth > 16) return;
|
|
1633
|
-
for (const item of await
|
|
2105
|
+
for (const item of await fs5.readdir(folder, { withFileTypes: true })) {
|
|
1634
2106
|
if (item.name.startsWith(".") || [
|
|
1635
2107
|
"node_modules",
|
|
1636
2108
|
"dist",
|
|
@@ -1639,16 +2111,16 @@ async function listProjectResources(root) {
|
|
|
1639
2111
|
"video-references"
|
|
1640
2112
|
].includes(item.name) || item.isSymbolicLink())
|
|
1641
2113
|
continue;
|
|
1642
|
-
const file =
|
|
2114
|
+
const file = path17.join(folder, item.name);
|
|
1643
2115
|
if (item.isDirectory()) {
|
|
1644
2116
|
await scenes(file, depth + 1);
|
|
1645
2117
|
continue;
|
|
1646
2118
|
}
|
|
1647
|
-
if (!item.isFile() || !item.name.endsWith(".tsx") || (await
|
|
2119
|
+
if (!item.isFile() || !item.name.endsWith(".tsx") || (await fs5.stat(file)).size > 1024 * 1024)
|
|
1648
2120
|
continue;
|
|
1649
|
-
const entry =
|
|
2121
|
+
const entry = path17.relative(root, file).split(path17.sep).join("/");
|
|
1650
2122
|
try {
|
|
1651
|
-
const text = await
|
|
2123
|
+
const text = await fs5.readFile(file, "utf8"), ast = ts7.createSourceFile(
|
|
1652
2124
|
entry,
|
|
1653
2125
|
text,
|
|
1654
2126
|
ts7.ScriptTarget.Latest,
|
|
@@ -1685,12 +2157,12 @@ async function listProjectResources(root) {
|
|
|
1685
2157
|
else
|
|
1686
2158
|
resources.push({
|
|
1687
2159
|
id: `scene:${entry}`,
|
|
1688
|
-
title: item.name === "scene.tsx" ?
|
|
2160
|
+
title: item.name === "scene.tsx" ? path17.basename(folder) : item.name.slice(0, -4),
|
|
1689
2161
|
kind: "scene",
|
|
1690
2162
|
entry,
|
|
1691
2163
|
...info,
|
|
1692
2164
|
preview: null,
|
|
1693
|
-
stamp: String((await
|
|
2165
|
+
stamp: String((await fs5.stat(file)).mtimeMs)
|
|
1694
2166
|
});
|
|
1695
2167
|
} catch (error) {
|
|
1696
2168
|
errors.push(`${entry}: ${error.message}`);
|
|
@@ -1709,44 +2181,44 @@ async function getProjectResource(root, id) {
|
|
|
1709
2181
|
async function resourcePreview(root, id) {
|
|
1710
2182
|
const resource = await getProjectResource(root, id);
|
|
1711
2183
|
if (!resource.preview) throw new Error("No preview for this resource");
|
|
1712
|
-
const file = await resourceFile(root, resource.preview), type = mime[
|
|
2184
|
+
const file = await resourceFile(root, resource.preview), type = mime[path17.extname(file).toLowerCase()];
|
|
1713
2185
|
if (!type) throw new Error("Unsupported preview type");
|
|
1714
|
-
if ((await
|
|
2186
|
+
if ((await fs5.stat(file)).size > 12 * 1024 * 1024)
|
|
1715
2187
|
throw new Error("Preview is too large");
|
|
1716
|
-
return { bytes: await
|
|
2188
|
+
return { bytes: await fs5.readFile(file), type };
|
|
1717
2189
|
}
|
|
1718
2190
|
async function installResourceExamples(root) {
|
|
1719
|
-
const folder = await resourceFolder(root), source =
|
|
1720
|
-
for (const item of await
|
|
2191
|
+
const folder = await resourceFolder(root), source = path17.join(packageRoot, "src/resource-examples/");
|
|
2192
|
+
for (const item of await fs5.readdir(source, { withFileTypes: true })) {
|
|
1721
2193
|
if (!item.isDirectory()) continue;
|
|
1722
|
-
const destination =
|
|
2194
|
+
const destination = path17.join(folder, item.name);
|
|
1723
2195
|
try {
|
|
1724
|
-
await
|
|
2196
|
+
await fs5.mkdir(destination);
|
|
1725
2197
|
} catch (e) {
|
|
1726
2198
|
if (e.code === "EEXIST") continue;
|
|
1727
2199
|
throw e;
|
|
1728
2200
|
}
|
|
1729
2201
|
try {
|
|
1730
|
-
for (const file of (await
|
|
2202
|
+
for (const file of (await fs5.readdir(path17.join(source, item.name))).sort(
|
|
1731
2203
|
(a, b) => Number(a === "resource.json") - Number(b === "resource.json")
|
|
1732
2204
|
))
|
|
1733
|
-
await
|
|
1734
|
-
|
|
1735
|
-
|
|
2205
|
+
await fs5.copyFile(
|
|
2206
|
+
path17.join(source, item.name, file),
|
|
2207
|
+
path17.join(destination, file)
|
|
1736
2208
|
);
|
|
1737
2209
|
} catch (error) {
|
|
1738
|
-
await
|
|
2210
|
+
await fs5.rm(destination, { recursive: true, force: true });
|
|
1739
2211
|
throw error;
|
|
1740
2212
|
}
|
|
1741
2213
|
}
|
|
1742
2214
|
return { status: "installed", ...await listProjectResources(root) };
|
|
1743
2215
|
}
|
|
1744
|
-
var
|
|
2216
|
+
var exec4, kinds, mime, inside;
|
|
1745
2217
|
var init_project_resources = __esm({
|
|
1746
2218
|
"src/project-resources.ts"() {
|
|
1747
2219
|
"use strict";
|
|
1748
2220
|
init_runtime();
|
|
1749
|
-
|
|
2221
|
+
exec4 = promisify4(execFile4);
|
|
1750
2222
|
kinds = {
|
|
1751
2223
|
".tsx": "scene",
|
|
1752
2224
|
".svg": "image",
|
|
@@ -1771,8 +2243,8 @@ var init_project_resources = __esm({
|
|
|
1771
2243
|
".mov": "video/quicktime"
|
|
1772
2244
|
};
|
|
1773
2245
|
inside = (root, file) => {
|
|
1774
|
-
const rel =
|
|
1775
|
-
return rel !== ".." && !rel.startsWith(".." +
|
|
2246
|
+
const rel = path17.relative(root, file);
|
|
2247
|
+
return rel !== ".." && !rel.startsWith(".." + path17.sep) && !path17.isAbsolute(rel);
|
|
1776
2248
|
};
|
|
1777
2249
|
}
|
|
1778
2250
|
});
|
|
@@ -2033,7 +2505,7 @@ function validProperty(name, value, spec) {
|
|
|
2033
2505
|
if (["position", "origin"].includes(name)) return pair(value);
|
|
2034
2506
|
if (name === "rotation" && spec.kind === "json")
|
|
2035
2507
|
return Array.isArray(value) && value.length === 3 && value.every(number);
|
|
2036
|
-
if (name === "scale") return number(value)
|
|
2508
|
+
if (name === "scale") return number(value);
|
|
2037
2509
|
if (["width", "height"].includes(name))
|
|
2038
2510
|
return value === "fill" || value === "auto" || number(value) && value > 0;
|
|
2039
2511
|
if (name === "padding")
|
|
@@ -2041,6 +2513,7 @@ function validProperty(name, value, spec) {
|
|
|
2041
2513
|
return false;
|
|
2042
2514
|
}
|
|
2043
2515
|
function validKnob(schema, value) {
|
|
2516
|
+
if (schema.type === "asset") return !!schema.kind && validAssetPath(schema.kind, value);
|
|
2044
2517
|
if (schema.type === "text")
|
|
2045
2518
|
return typeof value === "string" && value.length <= 1e4;
|
|
2046
2519
|
if (schema.type === "color")
|
|
@@ -2061,6 +2534,7 @@ function validKnob(schema, value) {
|
|
|
2061
2534
|
var init_property_validation = __esm({
|
|
2062
2535
|
"../editor/src/property-validation.ts"() {
|
|
2063
2536
|
"use strict";
|
|
2537
|
+
init_assets();
|
|
2064
2538
|
init_mask();
|
|
2065
2539
|
}
|
|
2066
2540
|
});
|
|
@@ -2756,6 +3230,30 @@ function applyTimelineCommand(text, command, {
|
|
|
2756
3230
|
{ start: clip.node.getStart(model.ast), end: clip.node.end, text: "" }
|
|
2757
3231
|
]);
|
|
2758
3232
|
id = null;
|
|
3233
|
+
} else if (command.type === "split") {
|
|
3234
|
+
const at = command.time, cut = at - clip.start, value = (name, fallback) => {
|
|
3235
|
+
const attr = clip.attrs.get(name);
|
|
3236
|
+
if (!attr) return fallback;
|
|
3237
|
+
const read = literal2(attr.initializer);
|
|
3238
|
+
if (!read.known || typeof read.value !== "number")
|
|
3239
|
+
throw new Error(`This clip's ${name} is set in code; edit it there`);
|
|
3240
|
+
return read.value;
|
|
3241
|
+
};
|
|
3242
|
+
const copy = copyClip(
|
|
3243
|
+
clip,
|
|
3244
|
+
model.ast,
|
|
3245
|
+
{
|
|
3246
|
+
start: at,
|
|
3247
|
+
duration: clip.duration - cut,
|
|
3248
|
+
trimIn: value("trimIn", 0) + cut * value("playbackRate", 1)
|
|
3249
|
+
},
|
|
3250
|
+
fresh
|
|
3251
|
+
);
|
|
3252
|
+
id = copy.id;
|
|
3253
|
+
text = patches(text, [
|
|
3254
|
+
{ start: clip.node.end, end: clip.node.end, text: "\n " + copy.text }
|
|
3255
|
+
]);
|
|
3256
|
+
text = setAttrs(text, clip.node, { duration: cut }, model.ast);
|
|
2759
3257
|
} else if (command.type === "duplicate") {
|
|
2760
3258
|
let start = clip.start + clip.duration, track = clip.track;
|
|
2761
3259
|
if (!canPlace(model.clips, null, start, clip.duration, track, model.duration)) {
|
|
@@ -2871,7 +3369,7 @@ var init_timeline = __esm({
|
|
|
2871
3369
|
position: { kind: "json", default: [0, 0] },
|
|
2872
3370
|
z: { kind: "number", default: 0 },
|
|
2873
3371
|
rotation: { kind: "number", default: 0 },
|
|
2874
|
-
scale: { kind: "
|
|
3372
|
+
scale: { kind: "number", default: 1 },
|
|
2875
3373
|
origin: { kind: "json", default: [0, 0] },
|
|
2876
3374
|
width: { kind: "json", default: "fill" },
|
|
2877
3375
|
height: { kind: "json", default: "fill" },
|
|
@@ -3143,10 +3641,10 @@ var init_resource_source = __esm({
|
|
|
3143
3641
|
});
|
|
3144
3642
|
|
|
3145
3643
|
// src/timeline-editor.ts
|
|
3146
|
-
import
|
|
3644
|
+
import fs6 from "node:fs/promises";
|
|
3147
3645
|
import { realpathSync } from "node:fs";
|
|
3148
|
-
import
|
|
3149
|
-
import { createHash as createHash6, randomUUID as
|
|
3646
|
+
import path18 from "node:path";
|
|
3647
|
+
import { createHash as createHash6, randomUUID as randomUUID7 } from "node:crypto";
|
|
3150
3648
|
import ts10 from "typescript";
|
|
3151
3649
|
function createTimelineEditor(root, context, {
|
|
3152
3650
|
nativeValidation = false,
|
|
@@ -3164,8 +3662,8 @@ function createTimelineEditor(root, context, {
|
|
|
3164
3662
|
};
|
|
3165
3663
|
async function read() {
|
|
3166
3664
|
const { source: inputSource, scene } = context();
|
|
3167
|
-
const source = await
|
|
3168
|
-
const text = await
|
|
3665
|
+
const source = await fs6.realpath(inputSource);
|
|
3666
|
+
const text = await fs6.readFile(source, "utf8"), definitions = {}, inputs = /* @__PURE__ */ new Map([[source, text]]);
|
|
3169
3667
|
async function dependencies(file, body2, depth = 0) {
|
|
3170
3668
|
if (depth > 32) throw new Error("Too many imported animations");
|
|
3171
3669
|
const ast = ts10.createSourceFile(
|
|
@@ -3184,7 +3682,7 @@ function createTimelineEditor(root, context, {
|
|
|
3184
3682
|
);
|
|
3185
3683
|
let code = inputs.get(next.source);
|
|
3186
3684
|
if (code === void 0) {
|
|
3187
|
-
code = await
|
|
3685
|
+
code = await fs6.readFile(next.source, "utf8");
|
|
3188
3686
|
inputs.set(next.source, code);
|
|
3189
3687
|
await dependencies(next.source, code, depth + 1);
|
|
3190
3688
|
}
|
|
@@ -3233,7 +3731,7 @@ function createTimelineEditor(root, context, {
|
|
|
3233
3731
|
...data,
|
|
3234
3732
|
sourceFiles: Object.fromEntries(
|
|
3235
3733
|
[...inputs].map(([file, body2]) => [
|
|
3236
|
-
|
|
3734
|
+
path18.relative(root, file).split(path18.sep).join("/"),
|
|
3237
3735
|
body2
|
|
3238
3736
|
])
|
|
3239
3737
|
),
|
|
@@ -3244,16 +3742,16 @@ function createTimelineEditor(root, context, {
|
|
|
3244
3742
|
};
|
|
3245
3743
|
}
|
|
3246
3744
|
async function write(file, text) {
|
|
3247
|
-
const temp =
|
|
3248
|
-
|
|
3249
|
-
`.osmo-edit-${
|
|
3745
|
+
const temp = path18.join(
|
|
3746
|
+
path18.dirname(file),
|
|
3747
|
+
`.osmo-edit-${randomUUID7()}.tmp`
|
|
3250
3748
|
);
|
|
3251
3749
|
try {
|
|
3252
|
-
const mode = (await
|
|
3253
|
-
await
|
|
3254
|
-
await
|
|
3750
|
+
const mode = (await fs6.stat(file)).mode;
|
|
3751
|
+
await fs6.writeFile(temp, text, { mode });
|
|
3752
|
+
await fs6.rename(temp, file);
|
|
3255
3753
|
} finally {
|
|
3256
|
-
await
|
|
3754
|
+
await fs6.rm(temp, { force: true });
|
|
3257
3755
|
}
|
|
3258
3756
|
}
|
|
3259
3757
|
return {
|
|
@@ -3266,27 +3764,27 @@ function createTimelineEditor(root, context, {
|
|
|
3266
3764
|
resolveAdd: (cardId) => serial(async () => {
|
|
3267
3765
|
const board = validateBoard(
|
|
3268
3766
|
JSON.parse(
|
|
3269
|
-
await
|
|
3270
|
-
|
|
3767
|
+
await fs6.readFile(
|
|
3768
|
+
path18.join(root, "storyboard.canvas.json"),
|
|
3271
3769
|
"utf8"
|
|
3272
3770
|
)
|
|
3273
3771
|
)
|
|
3274
3772
|
);
|
|
3275
3773
|
const card = board.cards.find((c) => c.id === cardId);
|
|
3276
3774
|
if (!card) throw new Error("Storyboard card no longer exists");
|
|
3277
|
-
const located = sceneInProject(root, card.source), code = await
|
|
3775
|
+
const located = sceneInProject(root, card.source), code = await fs6.readFile(located.source, "utf8");
|
|
3278
3776
|
const document = await documentLoader.load(located.source, code);
|
|
3279
3777
|
const sourceFiles = Object.fromEntries(
|
|
3280
3778
|
await Promise.all(
|
|
3281
3779
|
document.files.map(
|
|
3282
3780
|
async (file) => [
|
|
3283
3781
|
file,
|
|
3284
|
-
await
|
|
3782
|
+
await fs6.readFile(sceneInProject(root, file).source, "utf8")
|
|
3285
3783
|
]
|
|
3286
3784
|
)
|
|
3287
3785
|
)
|
|
3288
3786
|
);
|
|
3289
|
-
let importPath =
|
|
3787
|
+
let importPath = path18.relative(path18.dirname(context().source), located.source).split(path18.sep).join("/");
|
|
3290
3788
|
if (!importPath.startsWith(".")) importPath = "./" + importPath;
|
|
3291
3789
|
return {
|
|
3292
3790
|
type: "add",
|
|
@@ -3326,7 +3824,7 @@ function createTimelineEditor(root, context, {
|
|
|
3326
3824
|
if (typeof change.file !== "string" || !change.file.endsWith(".tsx") || typeof change.after !== "string" || change.after.length > 1024 * 1024)
|
|
3327
3825
|
throw new Error("Invalid source patch");
|
|
3328
3826
|
const file = sceneInProject(root, change.file).source;
|
|
3329
|
-
const current2 = await
|
|
3827
|
+
const current2 = await fs6.readFile(file, "utf8");
|
|
3330
3828
|
if (current2 !== change.before)
|
|
3331
3829
|
throw Object.assign(new Error(`Source changed: ${change.file}`), {
|
|
3332
3830
|
status: 409
|
|
@@ -3360,7 +3858,7 @@ function createTimelineEditor(root, context, {
|
|
|
3360
3858
|
status: 409
|
|
3361
3859
|
});
|
|
3362
3860
|
for (const change of changed)
|
|
3363
|
-
if (await
|
|
3861
|
+
if (await fs6.readFile(change.file, "utf8") !== change.before)
|
|
3364
3862
|
throw Object.assign(new Error("Source changed while validating"), {
|
|
3365
3863
|
status: 409
|
|
3366
3864
|
});
|
|
@@ -3372,7 +3870,7 @@ function createTimelineEditor(root, context, {
|
|
|
3372
3870
|
}
|
|
3373
3871
|
} catch (error) {
|
|
3374
3872
|
for (const change of written.reverse())
|
|
3375
|
-
if (await
|
|
3873
|
+
if (await fs6.readFile(change.file, "utf8") === change.after)
|
|
3376
3874
|
await write(change.file, change.before);
|
|
3377
3875
|
throw error;
|
|
3378
3876
|
}
|
|
@@ -3435,15 +3933,15 @@ function createTimelineEditor(root, context, {
|
|
|
3435
3933
|
if (!before.inputs.has(file))
|
|
3436
3934
|
throw new Error("Target source is not part of this scene");
|
|
3437
3935
|
const relative = (value) => {
|
|
3438
|
-
let p =
|
|
3936
|
+
let p = path18.relative(path18.dirname(file), sceneInProject(root, value).source).split(path18.sep).join("/");
|
|
3439
3937
|
return p.startsWith(".") ? p : "./" + p;
|
|
3440
3938
|
};
|
|
3441
3939
|
result = applyResourceSource(before.inputs.get(file), plan, {
|
|
3442
|
-
filename:
|
|
3940
|
+
filename: path18.relative(root, file),
|
|
3443
3941
|
definitions: before.definitions,
|
|
3444
3942
|
src: relative(resource.entry),
|
|
3445
3943
|
effectSrc: resource.effect ? relative(resource.effect.src) : void 0,
|
|
3446
|
-
newId: () => `clip-${
|
|
3944
|
+
newId: () => `clip-${randomUUID7().slice(0, 8)}`
|
|
3447
3945
|
});
|
|
3448
3946
|
} else if (command.type === "group") {
|
|
3449
3947
|
const target = findGroup(before.document, command.nodeId);
|
|
@@ -3463,8 +3961,8 @@ function createTimelineEditor(root, context, {
|
|
|
3463
3961
|
if (command.type === "add") {
|
|
3464
3962
|
const board = validateBoard(
|
|
3465
3963
|
JSON.parse(
|
|
3466
|
-
await
|
|
3467
|
-
|
|
3964
|
+
await fs6.readFile(
|
|
3965
|
+
path18.join(root, "storyboard.canvas.json"),
|
|
3468
3966
|
"utf8"
|
|
3469
3967
|
)
|
|
3470
3968
|
)
|
|
@@ -3473,12 +3971,12 @@ function createTimelineEditor(root, context, {
|
|
|
3473
3971
|
const { source: groupSource, scene: groupScene } = sceneInProject(
|
|
3474
3972
|
root,
|
|
3475
3973
|
card.source
|
|
3476
|
-
), code = await
|
|
3974
|
+
), code = await fs6.readFile(groupSource, "utf8");
|
|
3477
3975
|
const definition = {
|
|
3478
3976
|
...describeGroup(code, groupScene),
|
|
3479
3977
|
source: groupScene
|
|
3480
3978
|
};
|
|
3481
|
-
let importPath =
|
|
3979
|
+
let importPath = path18.relative(path18.dirname(before.source), groupSource).split(path18.sep).join("/");
|
|
3482
3980
|
if (!importPath.startsWith(".")) importPath = "./" + importPath;
|
|
3483
3981
|
resolved = {
|
|
3484
3982
|
type: "add",
|
|
@@ -3491,7 +3989,7 @@ function createTimelineEditor(root, context, {
|
|
|
3491
3989
|
result = applyTimelineCommand(before.text, resolved, {
|
|
3492
3990
|
definitions: before.definitions,
|
|
3493
3991
|
filename: before.scene,
|
|
3494
|
-
newId: () => `clip-${
|
|
3992
|
+
newId: () => `clip-${randomUUID7().slice(0, 8)}`
|
|
3495
3993
|
});
|
|
3496
3994
|
}
|
|
3497
3995
|
if (result.text === before.inputs.get(file))
|
|
@@ -3590,8 +4088,8 @@ var init_timeline_editor = __esm({
|
|
|
3590
4088
|
init_resource_source();
|
|
3591
4089
|
init_timeline();
|
|
3592
4090
|
init_group_model();
|
|
3593
|
-
resolveImport = (source, spec) =>
|
|
3594
|
-
|
|
4091
|
+
resolveImport = (source, spec) => path18.resolve(
|
|
4092
|
+
path18.dirname(source),
|
|
3595
4093
|
spec.endsWith(".tsx") ? spec : spec.endsWith(".js") ? spec.slice(0, -3) + ".tsx" : spec + ".tsx"
|
|
3596
4094
|
);
|
|
3597
4095
|
hash2 = (value) => createHash6("sha256").update(value).digest("hex");
|
|
@@ -3599,17 +4097,17 @@ var init_timeline_editor = __esm({
|
|
|
3599
4097
|
});
|
|
3600
4098
|
|
|
3601
4099
|
// src/studio-settings.ts
|
|
3602
|
-
import { mkdir as
|
|
3603
|
-
import
|
|
4100
|
+
import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile7, rename as rename3, rm as rm7 } from "node:fs/promises";
|
|
4101
|
+
import path19 from "node:path";
|
|
3604
4102
|
import { randomBytes } from "node:crypto";
|
|
3605
4103
|
async function studioSettings(root) {
|
|
3606
|
-
const directory =
|
|
3607
|
-
await
|
|
3608
|
-
const lock =
|
|
4104
|
+
const directory = path19.join(root, ".osmo");
|
|
4105
|
+
await mkdir7(directory, { recursive: true });
|
|
4106
|
+
const lock = path19.join(directory, "studio.lock");
|
|
3609
4107
|
const deadline = Date.now() + 5e3;
|
|
3610
4108
|
while (true) {
|
|
3611
4109
|
try {
|
|
3612
|
-
await
|
|
4110
|
+
await mkdir7(lock);
|
|
3613
4111
|
break;
|
|
3614
4112
|
} catch (error) {
|
|
3615
4113
|
if (error.code !== "EEXIST") throw error;
|
|
@@ -3620,12 +4118,12 @@ async function studioSettings(root) {
|
|
|
3620
4118
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
3621
4119
|
}
|
|
3622
4120
|
}
|
|
3623
|
-
const release = () =>
|
|
4121
|
+
const release = () => rm7(lock, { recursive: true, force: true });
|
|
3624
4122
|
try {
|
|
3625
|
-
const file =
|
|
4123
|
+
const file = path19.join(directory, "studio.json");
|
|
3626
4124
|
let settings, existing = false;
|
|
3627
4125
|
try {
|
|
3628
|
-
settings = JSON.parse(await
|
|
4126
|
+
settings = JSON.parse(await readFile8(file, "utf8"));
|
|
3629
4127
|
existing = true;
|
|
3630
4128
|
if (settings.version !== 1 || typeof settings.id !== "string" || settings.id.length > 64 || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(settings.id) || !/^[a-f0-9]{48}$/.test(settings.token) || !Number.isInteger(settings.port) || settings.port < 1 || settings.port > 65535)
|
|
3631
4129
|
throw new Error("Invalid settings");
|
|
@@ -3634,7 +4132,7 @@ async function studioSettings(root) {
|
|
|
3634
4132
|
throw new Error(
|
|
3635
4133
|
"Cannot read .osmo/studio.json. Fix or remove it to reset the Studio address."
|
|
3636
4134
|
);
|
|
3637
|
-
const name =
|
|
4135
|
+
const name = path19.basename(root).normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/['’]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 48).replace(/-$/, "") || "project";
|
|
3638
4136
|
settings = {
|
|
3639
4137
|
version: 1,
|
|
3640
4138
|
id: `${name}-${randomBytes(2).toString("hex")}`,
|
|
@@ -3642,10 +4140,10 @@ async function studioSettings(root) {
|
|
|
3642
4140
|
token: randomBytes(24).toString("hex")
|
|
3643
4141
|
};
|
|
3644
4142
|
}
|
|
3645
|
-
const ignorePath =
|
|
4143
|
+
const ignorePath = path19.join(root, ".gitignore");
|
|
3646
4144
|
let ignore = "";
|
|
3647
4145
|
try {
|
|
3648
|
-
ignore = await
|
|
4146
|
+
ignore = await readFile8(ignorePath, "utf8");
|
|
3649
4147
|
} catch (error) {
|
|
3650
4148
|
if (error.code !== "ENOENT") throw error;
|
|
3651
4149
|
}
|
|
@@ -3655,7 +4153,7 @@ async function studioSettings(root) {
|
|
|
3655
4153
|
".osmo/studio.*.tmp"
|
|
3656
4154
|
].filter((rule) => !ignore.split("\n").includes(rule));
|
|
3657
4155
|
if (missing.length)
|
|
3658
|
-
await
|
|
4156
|
+
await writeFile7(
|
|
3659
4157
|
ignorePath,
|
|
3660
4158
|
ignore + (ignore && !ignore.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n"
|
|
3661
4159
|
);
|
|
@@ -3666,12 +4164,12 @@ async function studioSettings(root) {
|
|
|
3666
4164
|
async save() {
|
|
3667
4165
|
const temp = `${file}.${process.pid}.tmp`;
|
|
3668
4166
|
try {
|
|
3669
|
-
await
|
|
4167
|
+
await writeFile7(temp, JSON.stringify(settings, null, 2) + "\n", {
|
|
3670
4168
|
mode: 384
|
|
3671
4169
|
});
|
|
3672
4170
|
await rename3(temp, file);
|
|
3673
4171
|
} finally {
|
|
3674
|
-
await
|
|
4172
|
+
await rm7(temp, { force: true });
|
|
3675
4173
|
}
|
|
3676
4174
|
}
|
|
3677
4175
|
};
|
|
@@ -3687,14 +4185,14 @@ var init_studio_settings = __esm({
|
|
|
3687
4185
|
});
|
|
3688
4186
|
|
|
3689
4187
|
// src/comment-profile.ts
|
|
3690
|
-
import { readFile as
|
|
4188
|
+
import { readFile as readFile9, writeFile as writeFile8, rename as rename4, mkdir as mkdir8 } from "node:fs/promises";
|
|
3691
4189
|
import { userInfo } from "node:os";
|
|
3692
|
-
import { randomUUID as
|
|
3693
|
-
import
|
|
4190
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
4191
|
+
import path20 from "node:path";
|
|
3694
4192
|
async function commentProfile(root) {
|
|
3695
4193
|
try {
|
|
3696
4194
|
const profile = JSON.parse(
|
|
3697
|
-
await
|
|
4195
|
+
await readFile9(path20.join(root, ".osmo/comment-profile.json"), "utf8")
|
|
3698
4196
|
);
|
|
3699
4197
|
if (!validAuthor(profile)) throw new Error("Invalid comment profile");
|
|
3700
4198
|
return profile;
|
|
@@ -3713,11 +4211,11 @@ async function saveCommentProfile(root, input) {
|
|
|
3713
4211
|
throw new Error(
|
|
3714
4212
|
"Provide a name and a PNG, JPEG, or WebP avatar under 110 KB."
|
|
3715
4213
|
);
|
|
3716
|
-
const folder =
|
|
3717
|
-
await
|
|
3718
|
-
const temp =
|
|
3719
|
-
await
|
|
3720
|
-
await rename4(temp,
|
|
4214
|
+
const folder = path20.join(root, ".osmo");
|
|
4215
|
+
await mkdir8(folder, { recursive: true });
|
|
4216
|
+
const temp = path20.join(folder, `comment-profile.${randomUUID8()}.tmp`);
|
|
4217
|
+
await writeFile8(temp, JSON.stringify(profile) + "\n");
|
|
4218
|
+
await rename4(temp, path20.join(folder, "comment-profile.json"));
|
|
3721
4219
|
return profile;
|
|
3722
4220
|
}
|
|
3723
4221
|
var init_comment_profile = __esm({
|
|
@@ -3733,16 +4231,16 @@ __export(server_exports, {
|
|
|
3733
4231
|
startStudio: () => startStudio
|
|
3734
4232
|
});
|
|
3735
4233
|
import http from "node:http";
|
|
3736
|
-
import
|
|
3737
|
-
import { readFile as
|
|
3738
|
-
import
|
|
3739
|
-
import { createHash as createHash7, randomUUID as
|
|
4234
|
+
import fs7 from "node:fs";
|
|
4235
|
+
import { readFile as readFile10, stat as stat3, realpath as realpath3, readdir as readdir3 } from "node:fs/promises";
|
|
4236
|
+
import path21 from "node:path";
|
|
4237
|
+
import { createHash as createHash7, randomUUID as randomUUID9 } from "node:crypto";
|
|
3740
4238
|
import { spawn as spawn3 } from "node:child_process";
|
|
3741
4239
|
async function revision(paths) {
|
|
3742
4240
|
const hash3 = createHash7("sha256");
|
|
3743
4241
|
for (const file of [...new Set(paths)].sort()) {
|
|
3744
4242
|
hash3.update(file);
|
|
3745
|
-
for await (const chunk of
|
|
4243
|
+
for await (const chunk of fs7.createReadStream(file)) hash3.update(chunk);
|
|
3746
4244
|
}
|
|
3747
4245
|
return `sha256:${hash3.digest("hex")}`;
|
|
3748
4246
|
}
|
|
@@ -3750,7 +4248,7 @@ async function fingerprint(paths) {
|
|
|
3750
4248
|
return Promise.all(
|
|
3751
4249
|
paths.map(async (p) => {
|
|
3752
4250
|
try {
|
|
3753
|
-
const s = await
|
|
4251
|
+
const s = await stat3(p);
|
|
3754
4252
|
return `${p}:${s.mtimeMs}:${s.size}`;
|
|
3755
4253
|
} catch {
|
|
3756
4254
|
return p + ":missing";
|
|
@@ -3759,7 +4257,7 @@ async function fingerprint(paths) {
|
|
|
3759
4257
|
).then((v) => v.join("|"));
|
|
3760
4258
|
}
|
|
3761
4259
|
async function studioRevision() {
|
|
3762
|
-
const files = (await readdir3(studioRoot, { recursive: true })).filter((file) => /\.(html|js|css|svg|woff2|wasm)$/.test(file)).sort().map((file) =>
|
|
4260
|
+
const files = (await readdir3(studioRoot, { recursive: true })).filter((file) => /\.(html|js|css|svg|woff2|wasm)$/.test(file)).sort().map((file) => path21.join(studioRoot, file));
|
|
3763
4261
|
const stamp = await fingerprint(files);
|
|
3764
4262
|
if (stamp !== studioStamp) {
|
|
3765
4263
|
const digest = await revision(files);
|
|
@@ -3797,10 +4295,10 @@ async function startStudio({
|
|
|
3797
4295
|
preview = {},
|
|
3798
4296
|
open = true
|
|
3799
4297
|
}) {
|
|
3800
|
-
const studioBuild =
|
|
3801
|
-
if (
|
|
3802
|
-
root = await
|
|
3803
|
-
source = await
|
|
4298
|
+
const studioBuild = path21.join(runtimeRoot, "packages/studio/tools/build.mjs");
|
|
4299
|
+
if (fs7.existsSync(studioBuild)) await run(process.execPath, [studioBuild]);
|
|
4300
|
+
root = await realpath3(root);
|
|
4301
|
+
source = await realpath3(source);
|
|
3804
4302
|
const profile = await studioSettings(root);
|
|
3805
4303
|
const { settings } = profile, { token } = settings;
|
|
3806
4304
|
const projectPath = "/" + settings.id;
|
|
@@ -3832,7 +4330,7 @@ async function startStudio({
|
|
|
3832
4330
|
scene,
|
|
3833
4331
|
preview,
|
|
3834
4332
|
version,
|
|
3835
|
-
projectName:
|
|
4333
|
+
projectName: path21.basename(root),
|
|
3836
4334
|
projectId: settings.id,
|
|
3837
4335
|
loading: true,
|
|
3838
4336
|
error: null,
|
|
@@ -3845,10 +4343,10 @@ async function startStudio({
|
|
|
3845
4343
|
const watchedStamps = /* @__PURE__ */ new Map();
|
|
3846
4344
|
watchedStamps.set(source, await fingerprint([source]));
|
|
3847
4345
|
const geometryCache = /* @__PURE__ */ new Map(), geometryInflight = /* @__PURE__ */ new Map();
|
|
3848
|
-
const
|
|
4346
|
+
const cache2 = /* @__PURE__ */ new Map(), inflight = /* @__PURE__ */ new Map();
|
|
3849
4347
|
let cacheBytes = 0;
|
|
3850
4348
|
function clearFrames() {
|
|
3851
|
-
|
|
4349
|
+
cache2.clear();
|
|
3852
4350
|
cacheBytes = 0;
|
|
3853
4351
|
geometryCache.clear();
|
|
3854
4352
|
}
|
|
@@ -4000,16 +4498,68 @@ async function startStudio({
|
|
|
4000
4498
|
return json(res, 403, {
|
|
4001
4499
|
error: "Open the Studio URL printed by the CLI."
|
|
4002
4500
|
});
|
|
4501
|
+
if (req.method === "POST" && url2.pathname === "/api/attachments/pick")
|
|
4502
|
+
return json(res, 200, await pickLocalAttachments());
|
|
4503
|
+
const selection = url2.pathname.match(
|
|
4504
|
+
/^\/api\/attachments\/selected\/([a-f0-9-]+)$/
|
|
4505
|
+
);
|
|
4506
|
+
if (req.method === "GET" && selection) {
|
|
4507
|
+
const file2 = selectedLocalAttachment(selection[1]);
|
|
4508
|
+
if (url2.searchParams.get("preview") === "1") {
|
|
4509
|
+
const bytes2 = await attachmentThumbnail(file2.sourcePath);
|
|
4510
|
+
res.writeHead(200, {
|
|
4511
|
+
"Content-Type": "image/jpeg",
|
|
4512
|
+
"Cache-Control": "private, max-age=60"
|
|
4513
|
+
});
|
|
4514
|
+
return res.end(bytes2);
|
|
4515
|
+
}
|
|
4516
|
+
res.writeHead(200, {
|
|
4517
|
+
"Content-Type": "application/octet-stream",
|
|
4518
|
+
"X-Content-Type-Options": "nosniff"
|
|
4519
|
+
});
|
|
4520
|
+
const stream = fs7.createReadStream(file2.sourcePath);
|
|
4521
|
+
res.once("close", () => stream.destroy());
|
|
4522
|
+
stream.on("error", () => res.destroy()).pipe(res);
|
|
4523
|
+
return;
|
|
4524
|
+
}
|
|
4525
|
+
if (req.method === "POST" && url2.pathname === "/api/assets/import") {
|
|
4526
|
+
const kind = url2.searchParams.get("kind") ?? "";
|
|
4527
|
+
const relative = await importProjectAsset(
|
|
4528
|
+
root,
|
|
4529
|
+
req,
|
|
4530
|
+
url2.searchParams.get("name") ?? "",
|
|
4531
|
+
kind
|
|
4532
|
+
);
|
|
4533
|
+
return json(res, 200, {
|
|
4534
|
+
path: relative,
|
|
4535
|
+
asset: await documentLoader.registerAsset(relative, kind)
|
|
4536
|
+
});
|
|
4537
|
+
}
|
|
4538
|
+
if (req.method === "GET" && url2.pathname === "/api/assets/metadata")
|
|
4539
|
+
return json(
|
|
4540
|
+
res,
|
|
4541
|
+
200,
|
|
4542
|
+
await documentLoader.assetMetadata(url2.searchParams.get("id"))
|
|
4543
|
+
);
|
|
4003
4544
|
if (req.method === "GET" && url2.pathname === "/api/document/asset") {
|
|
4004
4545
|
const asset = await documentLoader.readAsset(
|
|
4005
4546
|
url2.searchParams.get("id")
|
|
4006
4547
|
);
|
|
4007
|
-
const
|
|
4548
|
+
const types3 = {
|
|
4008
4549
|
".svg": "image/svg+xml",
|
|
4009
4550
|
".png": "image/png",
|
|
4010
4551
|
".jpg": "image/jpeg",
|
|
4011
4552
|
".jpeg": "image/jpeg",
|
|
4012
4553
|
".webp": "image/webp",
|
|
4554
|
+
".gif": "image/gif",
|
|
4555
|
+
".wav": "audio/wav",
|
|
4556
|
+
".mp3": "audio/mpeg",
|
|
4557
|
+
".m4a": "audio/mp4",
|
|
4558
|
+
".aac": "audio/aac",
|
|
4559
|
+
".ogg": "audio/ogg",
|
|
4560
|
+
".flac": "audio/flac",
|
|
4561
|
+
".m4v": "video/mp4",
|
|
4562
|
+
".glb": "model/gltf-binary",
|
|
4013
4563
|
".mp4": "video/mp4",
|
|
4014
4564
|
".webm": "video/webm",
|
|
4015
4565
|
".mov": "video/quicktime",
|
|
@@ -4017,7 +4567,7 @@ async function startStudio({
|
|
|
4017
4567
|
".otf": "font/otf"
|
|
4018
4568
|
};
|
|
4019
4569
|
res.writeHead(200, {
|
|
4020
|
-
"Content-Type":
|
|
4570
|
+
"Content-Type": types3[asset.extension] ?? "application/octet-stream",
|
|
4021
4571
|
"Cache-Control": "private, max-age=31536000, immutable"
|
|
4022
4572
|
});
|
|
4023
4573
|
return res.end(asset.bytes);
|
|
@@ -4138,7 +4688,7 @@ async function startStudio({
|
|
|
4138
4688
|
renderer?.close();
|
|
4139
4689
|
renderer = null;
|
|
4140
4690
|
nativeRevision = null;
|
|
4141
|
-
source = await
|
|
4691
|
+
source = await realpath3(next.source);
|
|
4142
4692
|
scene = next.scene;
|
|
4143
4693
|
state.scene = scene;
|
|
4144
4694
|
dependencies = [source];
|
|
@@ -4208,14 +4758,14 @@ async function startStudio({
|
|
|
4208
4758
|
geometry = {
|
|
4209
4759
|
revision: rev,
|
|
4210
4760
|
frame,
|
|
4211
|
-
targets: result.targets.filter((t) => rows.has(t.path)).map(({ path:
|
|
4761
|
+
targets: result.targets.filter((t) => rows.has(t.path)).map(({ path: path23, ...t }, order) => ({
|
|
4212
4762
|
...t,
|
|
4213
4763
|
// Filtered to rows above.
|
|
4214
|
-
id: rows.get(
|
|
4764
|
+
id: rows.get(path23),
|
|
4215
4765
|
order
|
|
4216
4766
|
})),
|
|
4217
4767
|
quads: Object.fromEntries(
|
|
4218
|
-
Object.entries(result.quads).filter(([
|
|
4768
|
+
Object.entries(result.quads).filter(([path23]) => rows.has(path23)).map(([path23, quad]) => [rows.get(path23), quad])
|
|
4219
4769
|
)
|
|
4220
4770
|
};
|
|
4221
4771
|
geometryCache.set(key, geometry);
|
|
@@ -4234,12 +4784,12 @@ async function startStudio({
|
|
|
4234
4784
|
0,
|
|
4235
4785
|
state.meta.totalFrames - 1
|
|
4236
4786
|
);
|
|
4237
|
-
const
|
|
4238
|
-
const row =
|
|
4239
|
-
if (
|
|
4787
|
+
const selection2 = url2.searchParams.get("selection");
|
|
4788
|
+
const row = selection2 ? outline?.rows.find((r) => r.id === selection2) : null;
|
|
4789
|
+
if (selection2 && !row)
|
|
4240
4790
|
return json(res, 404, { error: "Element is not in this preview" });
|
|
4241
|
-
const rev = state.revision, key = rev + ":" + frame + ":" + (
|
|
4242
|
-
let result =
|
|
4791
|
+
const rev = state.revision, key = rev + ":" + frame + ":" + (selection2 ?? "");
|
|
4792
|
+
let result = cache2.get(key);
|
|
4243
4793
|
if (!result) {
|
|
4244
4794
|
if (!inflight.has(key)) {
|
|
4245
4795
|
const pending = withNative(
|
|
@@ -4257,14 +4807,14 @@ async function startStudio({
|
|
|
4257
4807
|
result = await inflight.get(key);
|
|
4258
4808
|
if (rev !== state.revision || state.loading)
|
|
4259
4809
|
return json(res, 409, { error: "Preview changed" });
|
|
4260
|
-
if (!
|
|
4261
|
-
|
|
4810
|
+
if (!cache2.has(key)) {
|
|
4811
|
+
cache2.set(key, result);
|
|
4262
4812
|
cacheBytes += result.bytes.length;
|
|
4263
4813
|
}
|
|
4264
|
-
while (cacheBytes > 32 * 1024 * 1024 &&
|
|
4265
|
-
const first =
|
|
4266
|
-
cacheBytes -=
|
|
4267
|
-
|
|
4814
|
+
while (cacheBytes > 32 * 1024 * 1024 && cache2.size > 1) {
|
|
4815
|
+
const first = cache2.keys().next().value;
|
|
4816
|
+
cacheBytes -= cache2.get(first).bytes.length;
|
|
4817
|
+
cache2.delete(first);
|
|
4268
4818
|
}
|
|
4269
4819
|
}
|
|
4270
4820
|
res.writeHead(200, {
|
|
@@ -4278,7 +4828,7 @@ async function startStudio({
|
|
|
4278
4828
|
if (req.method === "GET" && url2.pathname === "/api/audio") {
|
|
4279
4829
|
if (!state.meta?.audioPath || url2.searchParams.get("revision") !== state.revision)
|
|
4280
4830
|
return json(res, 404, { error: "Audio unavailable" });
|
|
4281
|
-
const bytes2 = await
|
|
4831
|
+
const bytes2 = await readFile10(state.meta.audioPath);
|
|
4282
4832
|
res.writeHead(200, {
|
|
4283
4833
|
"Content-Type": "audio/wav",
|
|
4284
4834
|
"Cache-Control": "no-store"
|
|
@@ -4306,7 +4856,7 @@ async function startStudio({
|
|
|
4306
4856
|
);
|
|
4307
4857
|
}
|
|
4308
4858
|
if (req.method === "POST" && url2.pathname === "/api/comments") {
|
|
4309
|
-
const input = await body(req);
|
|
4859
|
+
const input = await body(req, 71 * 1024 * 1024);
|
|
4310
4860
|
if (!state.meta || input.revision !== state.revision || state.loading || dirty)
|
|
4311
4861
|
return json(res, 409, {
|
|
4312
4862
|
error: "Preview changed. Review the current frame before commenting."
|
|
@@ -4332,6 +4882,37 @@ async function startStudio({
|
|
|
4332
4882
|
})
|
|
4333
4883
|
);
|
|
4334
4884
|
}
|
|
4885
|
+
const attachment = url2.pathname.match(
|
|
4886
|
+
/^\/api\/comments\/([a-zA-Z0-9_-]+)\/attachment$/
|
|
4887
|
+
);
|
|
4888
|
+
if (req.method === "GET" && attachment) {
|
|
4889
|
+
const comment = (await readComments(root)).comments.find(
|
|
4890
|
+
(c) => c.id === attachment[1] && c.scene === scene
|
|
4891
|
+
);
|
|
4892
|
+
const file2 = comment && [
|
|
4893
|
+
...comment.attachments ?? [],
|
|
4894
|
+
...comment.replies.flatMap((r) => r.attachments ?? [])
|
|
4895
|
+
].find((f) => f.path === url2.searchParams.get("path"));
|
|
4896
|
+
if (!file2) return json(res, 404, { error: "Attachment not found" });
|
|
4897
|
+
const filename2 = await attachmentPath(root, file2);
|
|
4898
|
+
if (url2.searchParams.get("preview") === "1") {
|
|
4899
|
+
const bytes2 = await attachmentThumbnail(filename2);
|
|
4900
|
+
res.writeHead(200, {
|
|
4901
|
+
"Content-Type": "image/jpeg",
|
|
4902
|
+
"Cache-Control": "private, max-age=60"
|
|
4903
|
+
});
|
|
4904
|
+
return res.end(bytes2);
|
|
4905
|
+
}
|
|
4906
|
+
res.writeHead(200, {
|
|
4907
|
+
"Content-Type": "application/octet-stream",
|
|
4908
|
+
"Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(file2.name)}`,
|
|
4909
|
+
"X-Content-Type-Options": "nosniff"
|
|
4910
|
+
});
|
|
4911
|
+
const stream = fs7.createReadStream(filename2);
|
|
4912
|
+
res.once("close", () => stream.destroy());
|
|
4913
|
+
stream.on("error", () => res.destroy()).pipe(res);
|
|
4914
|
+
return;
|
|
4915
|
+
}
|
|
4335
4916
|
const thread = url2.pathname.match(
|
|
4336
4917
|
/^\/api\/comments\/([a-zA-Z0-9_-]+)\/(reply|resolve|reopen|range|react)$/
|
|
4337
4918
|
);
|
|
@@ -4365,6 +4946,7 @@ async function startStudio({
|
|
|
4365
4946
|
(await body(req)).emoji
|
|
4366
4947
|
)
|
|
4367
4948
|
);
|
|
4949
|
+
const replyInput = await body(req, 71 * 1024 * 1024);
|
|
4368
4950
|
return json(
|
|
4369
4951
|
res,
|
|
4370
4952
|
200,
|
|
@@ -4372,7 +4954,8 @@ async function startStudio({
|
|
|
4372
4954
|
root,
|
|
4373
4955
|
thread[1],
|
|
4374
4956
|
thread[2],
|
|
4375
|
-
|
|
4957
|
+
replyInput.text,
|
|
4958
|
+
replyInput.attachments
|
|
4376
4959
|
)
|
|
4377
4960
|
);
|
|
4378
4961
|
}
|
|
@@ -4422,22 +5005,22 @@ async function startStudio({
|
|
|
4422
5005
|
if (!state.meta) throw new Error("Load a scene before exporting");
|
|
4423
5006
|
const settings2 = validateExportSettings(input, state.meta);
|
|
4424
5007
|
const format2 = exportFormats[settings2.format];
|
|
4425
|
-
const filename2 = input.filename ?? `${
|
|
5008
|
+
const filename2 = input.filename ?? `${path21.basename(scene, ".tsx")}-${Date.now()}.${format2.extension}`;
|
|
4426
5009
|
if (typeof filename2 !== "string" || !/^[\w .-]+$/.test(filename2) || !filename2.toLowerCase().endsWith(`.${format2.extension}`) || filename2.startsWith("."))
|
|
4427
5010
|
throw new Error(`Use a filename ending in .${format2.extension}`);
|
|
4428
|
-
const folder =
|
|
4429
|
-
await
|
|
4430
|
-
const canonicalFolder = await
|
|
4431
|
-
if (
|
|
5011
|
+
const folder = path21.join(root, "exports");
|
|
5012
|
+
await fs7.promises.mkdir(folder, { recursive: true });
|
|
5013
|
+
const canonicalFolder = await realpath3(folder);
|
|
5014
|
+
if (path21.relative(root, canonicalFolder).startsWith(".."))
|
|
4432
5015
|
throw new Error("Export folder must stay inside the project");
|
|
4433
|
-
const destination =
|
|
5016
|
+
const destination = path21.join(canonicalFolder, filename2);
|
|
4434
5017
|
if (closed || project !== snapshot || state.loading || editing || dirty)
|
|
4435
5018
|
throw Object.assign(
|
|
4436
5019
|
new Error("The scene changed before export. Try again."),
|
|
4437
5020
|
{ status: 409 }
|
|
4438
5021
|
);
|
|
4439
5022
|
const job = state.export = {
|
|
4440
|
-
id:
|
|
5023
|
+
id: randomUUID9(),
|
|
4441
5024
|
status: "rendering",
|
|
4442
5025
|
phase: "preparing",
|
|
4443
5026
|
filename: filename2,
|
|
@@ -4484,7 +5067,7 @@ async function startStudio({
|
|
|
4484
5067
|
}
|
|
4485
5068
|
}
|
|
4486
5069
|
if (req.method === "GET" && url2.pathname === "/api/export/download" && state.export?.status === "complete" && (!url2.searchParams.has("id") || url2.searchParams.get("id") === state.export.id)) {
|
|
4487
|
-
const bytes2 = await
|
|
5070
|
+
const bytes2 = await readFile10(state.export.path);
|
|
4488
5071
|
res.writeHead(200, {
|
|
4489
5072
|
"Content-Type": state.export.filename.toLowerCase().endsWith(".webm") ? "video/webm" : state.export.filename.toLowerCase().endsWith(".mov") ? "video/quicktime" : "video/mp4",
|
|
4490
5073
|
"Content-Disposition": `attachment; filename="${state.export.filename}"`
|
|
@@ -4510,7 +5093,7 @@ async function startStudio({
|
|
|
4510
5093
|
file
|
|
4511
5094
|
))
|
|
4512
5095
|
return json(res, 404, { error: "Not found" });
|
|
4513
|
-
const
|
|
5096
|
+
const types2 = {
|
|
4514
5097
|
".html": "text/html",
|
|
4515
5098
|
".wasm": "application/wasm",
|
|
4516
5099
|
".js": "text/javascript",
|
|
@@ -4518,13 +5101,13 @@ async function startStudio({
|
|
|
4518
5101
|
".woff2": "font/woff2",
|
|
4519
5102
|
".svg": "image/svg+xml"
|
|
4520
5103
|
};
|
|
4521
|
-
const bytes = await
|
|
5104
|
+
const bytes = await readFile10(path21.join(studioRoot, file));
|
|
4522
5105
|
res.setHeader(
|
|
4523
5106
|
"Content-Security-Policy",
|
|
4524
5107
|
"default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; media-src 'self' blob:; connect-src 'self' blob:; font-src 'self'; frame-ancestors 'none'"
|
|
4525
5108
|
);
|
|
4526
5109
|
res.writeHead(200, {
|
|
4527
|
-
"Content-Type":
|
|
5110
|
+
"Content-Type": types2[path21.extname(file)],
|
|
4528
5111
|
"Cache-Control": "no-cache"
|
|
4529
5112
|
});
|
|
4530
5113
|
res.end(bytes);
|
|
@@ -4651,16 +5234,16 @@ Ctrl+C stops the server and renderer.`
|
|
|
4651
5234
|
);
|
|
4652
5235
|
openBrowser(url);
|
|
4653
5236
|
try {
|
|
4654
|
-
watcher =
|
|
5237
|
+
watcher = fs7.watch(root, { recursive: true }, async (event, filename2) => {
|
|
4655
5238
|
const name = String(filename2 ?? "");
|
|
4656
5239
|
if (/(^|[/\\])\.osmo-edit-/.test(name)) return;
|
|
4657
5240
|
if (name === "storyboard.canvas.json" || name.startsWith("storyboard.canvas.json."))
|
|
4658
5241
|
return;
|
|
4659
5242
|
if (/(^|[/\\])(node_modules|\.git|\.osmo|exports)([/\\]|$)/.test(name))
|
|
4660
5243
|
return;
|
|
4661
|
-
if (name.startsWith("resources/") && !dependencies.includes(
|
|
5244
|
+
if ((name.startsWith("resources/") || name === "assets" || name.startsWith("assets/")) && !dependencies.includes(path21.join(root, name)))
|
|
4662
5245
|
return;
|
|
4663
|
-
const file =
|
|
5246
|
+
const file = path21.join(root, name), before = watchedStamps.get(file);
|
|
4664
5247
|
const next = await fingerprint([file]);
|
|
4665
5248
|
watchedStamps.set(file, next);
|
|
4666
5249
|
if (closed || editing) return;
|
|
@@ -4685,6 +5268,10 @@ var studioStamp, studioDigest;
|
|
|
4685
5268
|
var init_server = __esm({
|
|
4686
5269
|
"src/server.ts"() {
|
|
4687
5270
|
"use strict";
|
|
5271
|
+
init_attachment_thumbnail();
|
|
5272
|
+
init_local_attachment_picker();
|
|
5273
|
+
init_asset_import();
|
|
5274
|
+
init_comment_attachments();
|
|
4688
5275
|
init_export_settings();
|
|
4689
5276
|
init_storyboard2();
|
|
4690
5277
|
init_timeline_editor();
|
|
@@ -4702,7 +5289,7 @@ var init_server = __esm({
|
|
|
4702
5289
|
// src/studio-cli.ts
|
|
4703
5290
|
init_check();
|
|
4704
5291
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
4705
|
-
import
|
|
5292
|
+
import path22 from "node:path";
|
|
4706
5293
|
import { parseArgs } from "node:util";
|
|
4707
5294
|
|
|
4708
5295
|
// src/cloud.ts
|
|
@@ -4879,8 +5466,8 @@ async function withCredentials(origin, action) {
|
|
|
4879
5466
|
}
|
|
4880
5467
|
try {
|
|
4881
5468
|
try {
|
|
4882
|
-
const
|
|
4883
|
-
if (!
|
|
5469
|
+
const stat4 = await lstat(file);
|
|
5470
|
+
if (!stat4.isFile()) throw new Error("Not a regular file");
|
|
4884
5471
|
await chmod(file, 384);
|
|
4885
5472
|
state = JSON.parse(await readFile(file, "utf8"));
|
|
4886
5473
|
if (state.version !== 1 || state.origin !== origin || state.credentials && !validCredentials(state.credentials) || state.pending && (!validClient(state.pending.client) || typeof state.pending.sessionId !== "string" || typeof state.pending.deviceCode !== "string" || typeof state.pending.code !== "string" || !Number.isFinite(state.pending.expiresAt) || !Number.isFinite(state.pending.nextPollAt) || !Number.isFinite(state.pending.interval) || state.pending.interval < 5 || !validApprovalUrl(state.pending.url, origin)))
|
|
@@ -5417,7 +6004,7 @@ function inspectScene(text, filename2, meta, definitions = {}) {
|
|
|
5417
6004
|
);
|
|
5418
6005
|
if (parent === null && type === "Group") props.start = 0;
|
|
5419
6006
|
const id = typeof props.id === "string" ? `source:${props.id}` : `node-${rows.length}`, local = new Map(scope);
|
|
5420
|
-
const ownKnobs =
|
|
6007
|
+
const ownKnobs = ["Group", "Scene"].includes(type) && props.knobs instanceof Reference ? knobs.get(props.knobs.set) : null;
|
|
5421
6008
|
if (ownKnobs)
|
|
5422
6009
|
local.set(
|
|
5423
6010
|
props.knobs.set,
|
|
@@ -6581,22 +7168,22 @@ init_comments();
|
|
|
6581
7168
|
|
|
6582
7169
|
// src/init.ts
|
|
6583
7170
|
import {
|
|
6584
|
-
mkdir as
|
|
6585
|
-
readFile as
|
|
6586
|
-
writeFile as
|
|
7171
|
+
mkdir as mkdir6,
|
|
7172
|
+
readFile as readFile7,
|
|
7173
|
+
writeFile as writeFile6,
|
|
6587
7174
|
copyFile,
|
|
6588
7175
|
cp as cp2,
|
|
6589
7176
|
access as access2,
|
|
6590
7177
|
mkdtemp,
|
|
6591
|
-
rm as
|
|
7178
|
+
rm as rm6
|
|
6592
7179
|
} from "node:fs/promises";
|
|
6593
|
-
import
|
|
7180
|
+
import path12 from "node:path";
|
|
6594
7181
|
|
|
6595
7182
|
// src/authoring.ts
|
|
6596
7183
|
init_runtime();
|
|
6597
|
-
import { readFile as
|
|
7184
|
+
import { readFile as readFile5, writeFile as writeFile4, rm as rm5 } from "node:fs/promises";
|
|
6598
7185
|
import { createHash as createHash3 } from "node:crypto";
|
|
6599
|
-
import
|
|
7186
|
+
import path10 from "node:path";
|
|
6600
7187
|
var begin = "<!-- osmo:begin -->";
|
|
6601
7188
|
var end = "<!-- osmo:end -->";
|
|
6602
7189
|
var legacyPointer = "For Osmo animation tasks, read osmo.md before editing. Use osmo studio check, and read open Studio comments.";
|
|
@@ -6661,9 +7248,13 @@ Read relevant declarations in node_modules/@osmo/scene/dist/ (JSDoc explains usa
|
|
|
6661
7248
|
- Use explicit JSX, literal values/arrays/objects, and const defineMotion or defineKnobs declarations. Bind tracks/knobs directly to compatible props.
|
|
6662
7249
|
- No arbitrary expressions, loops, callbacks, spreads, import aliases, custom components, helper calls or computed variables. Only blur({...}) and grain({...}) effect calls are allowed in JSX; pass an explicit object, even for defaults.
|
|
6663
7250
|
- Shader.src must be a static relative local .wgsl path. Put complete uniform values inline; numeric uniforms can use motion/knob tracks. defineEffect remains unsupported.
|
|
6664
|
-
- Keyframe at values are increasing local seconds (scene unless inside a timed clip); easing belongs to the arriving key. Add a key at 0 to define a continuous track's starting value. Tracks do not loop automatically.
|
|
7251
|
+
- Keyframe at values are increasing local seconds (scene unless inside a timed clip); easing belongs to the arriving key. Custom easing uses "cubic-bezier(x1,y1,x2,y2)" with x control points in 0..1; use curves between key poses instead of sampling every frame. Add a key at 0 to define a continuous track's starting value. Tracks do not loop automatically.
|
|
6665
7252
|
- Group knobs can drive many descendant props; Group.values accepts motion tracks.
|
|
7253
|
+
- Scene also accepts knobs/values. Imported Scene instances accept values to override declared controls; forward parent refs explicitly. Each instance is independent.
|
|
7254
|
+
- Asset knobs: { type: "asset", kind: "image" | "video" | "audio" | "font" | "model", default: "./local-file" }. Bind to matching src/font. Assets are static; paths retain the file that declared/supplied them across nested bindings. Model files are GLB; surfaces are not auto-remapped.
|
|
7255
|
+
- Scale is one number applied equally to both axes; animate width/height to change a shape\u2019s proportions. Two-value scale arrays are not supported.
|
|
6666
7256
|
- Units: pixels, degrees, seconds; +z toward viewer. Asset/font paths are relative to their declaring file.
|
|
7257
|
+
- Text.animate animates a whole string by character, word, or wrapped line: {unit:"character",stagger:0.05,duration:0.4,easing:"easeOut",from:{opacity:0,position:[0,20]},to:{opacity:1,position:[0,0]}}. Per-unit properties: position, z, rotation, uniform scale, origin, opacity, color, blur. Use keyframes instead of from/to/duration for multiple stages. Keep font/layout properties on Text. Do not create separate letter labels; animate cannot combine with path.
|
|
6667
7258
|
- Text.path handles the complete string; no width/align/lineHeight or line breaks. SVG geometry is static; progress and ellipse radii can animate.
|
|
6668
7259
|
- Path draws SVG d in local pixels; use scale to resize. d accepts a literal or a defineMotion string track for baked geometry (discrete switches, not SVG interpolation). Transforms, stroke width and effects can animate. Use fill="transparent" for lines.
|
|
6669
7260
|
|
|
@@ -6676,14 +7267,14 @@ ${end}
|
|
|
6676
7267
|
`;
|
|
6677
7268
|
async function optional(file) {
|
|
6678
7269
|
try {
|
|
6679
|
-
return await
|
|
7270
|
+
return await readFile5(file, "utf8");
|
|
6680
7271
|
} catch (error) {
|
|
6681
7272
|
if (error.code === "ENOENT") return "";
|
|
6682
7273
|
throw error;
|
|
6683
7274
|
}
|
|
6684
7275
|
}
|
|
6685
7276
|
async function writeAuthoringGuide(root) {
|
|
6686
|
-
const file =
|
|
7277
|
+
const file = path10.join(root, "AGENTS.md");
|
|
6687
7278
|
const original = await optional(file);
|
|
6688
7279
|
const hasBegin = original.includes(begin), hasEnd = original.includes(end);
|
|
6689
7280
|
if (hasBegin !== hasEnd || hasBegin && original.indexOf(end) < original.indexOf(begin))
|
|
@@ -6697,10 +7288,10 @@ async function writeAuthoringGuide(root) {
|
|
|
6697
7288
|
const rest = original.split("\n").filter((line) => line !== legacyPointer).join("\n");
|
|
6698
7289
|
text = rest + (rest && !rest.endsWith("\n\n") ? rest.endsWith("\n") ? "\n" : "\n\n" : "") + authoringGuide;
|
|
6699
7290
|
}
|
|
6700
|
-
if (text !== original) await
|
|
6701
|
-
const legacy = await optional(
|
|
7291
|
+
if (text !== original) await writeFile4(file, text);
|
|
7292
|
+
const legacy = await optional(path10.join(root, "osmo.md"));
|
|
6702
7293
|
if (legacy && createHash3("sha256").update(legacy).digest("hex") === legacyGuideHash)
|
|
6703
|
-
await
|
|
7294
|
+
await rm5(path10.join(root, "osmo.md"));
|
|
6704
7295
|
else if (legacy)
|
|
6705
7296
|
console.log(
|
|
6706
7297
|
"Existing osmo.md was preserved because it differs from the original generated guide. Osmo instructions now live in AGENTS.md."
|
|
@@ -6712,38 +7303,38 @@ import os2 from "node:os";
|
|
|
6712
7303
|
|
|
6713
7304
|
// src/starter.ts
|
|
6714
7305
|
init_runtime();
|
|
6715
|
-
import { access, mkdir as
|
|
6716
|
-
import
|
|
6717
|
-
var template =
|
|
7306
|
+
import { access, mkdir as mkdir5, readFile as readFile6, readdir as readdir2, writeFile as writeFile5, cp } from "node:fs/promises";
|
|
7307
|
+
import path11 from "node:path";
|
|
7308
|
+
var template = path11.join(packageRoot, "src/starter/");
|
|
6718
7309
|
async function writeStarter(root) {
|
|
6719
|
-
const scene =
|
|
7310
|
+
const scene = path11.join(root, "scene.tsx");
|
|
6720
7311
|
try {
|
|
6721
7312
|
await access(scene);
|
|
6722
7313
|
return;
|
|
6723
7314
|
} catch (error) {
|
|
6724
7315
|
if (error.code !== "ENOENT") throw error;
|
|
6725
7316
|
}
|
|
6726
|
-
await
|
|
7317
|
+
await mkdir5(path11.join(root, "animations"), { recursive: true });
|
|
6727
7318
|
let folder = "osmo";
|
|
6728
7319
|
for (let suffix = 1; ; suffix++) {
|
|
6729
7320
|
try {
|
|
6730
|
-
await
|
|
7321
|
+
await mkdir5(path11.join(root, "animations", folder));
|
|
6731
7322
|
break;
|
|
6732
7323
|
} catch (error) {
|
|
6733
7324
|
if (error.code !== "EEXIST") throw error;
|
|
6734
7325
|
folder = `osmo-${suffix}`;
|
|
6735
7326
|
}
|
|
6736
7327
|
}
|
|
6737
|
-
for (const file of await readdir2(
|
|
6738
|
-
await cp(
|
|
7328
|
+
for (const file of await readdir2(path11.join(template, "animations/osmo"))) {
|
|
7329
|
+
await cp(path11.join(template, "animations/osmo", file), path11.join(root, "animations", folder, file), {
|
|
6739
7330
|
recursive: true,
|
|
6740
7331
|
force: false,
|
|
6741
7332
|
errorOnExist: true
|
|
6742
7333
|
});
|
|
6743
7334
|
}
|
|
6744
|
-
const source = (await
|
|
7335
|
+
const source = (await readFile6(path11.join(template, "scene.tsx"), "utf8")).replaceAll("./animations/osmo/", `./animations/${folder}/`);
|
|
6745
7336
|
try {
|
|
6746
|
-
await
|
|
7337
|
+
await writeFile5(scene, source, {
|
|
6747
7338
|
flag: "wx"
|
|
6748
7339
|
});
|
|
6749
7340
|
} catch (error) {
|
|
@@ -6764,7 +7355,7 @@ async function exists(p) {
|
|
|
6764
7355
|
}
|
|
6765
7356
|
async function create(p, value) {
|
|
6766
7357
|
try {
|
|
6767
|
-
await
|
|
7358
|
+
await writeFile6(p, value, { flag: "wx" });
|
|
6768
7359
|
return true;
|
|
6769
7360
|
} catch (e) {
|
|
6770
7361
|
if (e.code !== "EEXIST") throw e;
|
|
@@ -6773,7 +7364,7 @@ async function create(p, value) {
|
|
|
6773
7364
|
}
|
|
6774
7365
|
async function init(root = process.cwd()) {
|
|
6775
7366
|
nativeBinary();
|
|
6776
|
-
const gitProbe = await mkdtemp(
|
|
7367
|
+
const gitProbe = await mkdtemp(path12.join(os2.tmpdir(), "osmo-git-check-"));
|
|
6777
7368
|
try {
|
|
6778
7369
|
const result = spawnSync3(
|
|
6779
7370
|
"git",
|
|
@@ -6785,7 +7376,7 @@ async function init(root = process.cwd()) {
|
|
|
6785
7376
|
"Osmo projects require Git with SHA-256 support (Git 2.29+). Install or update Git, then retry osmo studio init. Sharing an existing MP4 with osmo share does not require Git."
|
|
6786
7377
|
);
|
|
6787
7378
|
} finally {
|
|
6788
|
-
await
|
|
7379
|
+
await rm6(gitProbe, { recursive: true, force: true });
|
|
6789
7380
|
}
|
|
6790
7381
|
for (const [name, variable] of [
|
|
6791
7382
|
["ffmpeg", "OSMO_FFMPEG"],
|
|
@@ -6810,15 +7401,15 @@ async function init(root = process.cwd()) {
|
|
|
6810
7401
|
);
|
|
6811
7402
|
}
|
|
6812
7403
|
}
|
|
6813
|
-
root =
|
|
6814
|
-
await
|
|
7404
|
+
root = path12.resolve(root);
|
|
7405
|
+
await mkdir6(root, { recursive: true });
|
|
6815
7406
|
if (hasRepo(root) && objectFormat(root) !== "sha256")
|
|
6816
7407
|
throw new Error(
|
|
6817
7408
|
`${root} is already a git repository with ${objectFormat(root) ?? "unknown"} objects. Osmo projects are SHA-256 repositories: run osmo studio init in a new folder.`
|
|
6818
7409
|
);
|
|
6819
|
-
const manifestPath =
|
|
6820
|
-
const manifest2 = await exists(manifestPath) ? JSON.parse(await
|
|
6821
|
-
name:
|
|
7410
|
+
const manifestPath = path12.join(root, "package.json");
|
|
7411
|
+
const manifest2 = await exists(manifestPath) ? JSON.parse(await readFile7(manifestPath, "utf8")) : {
|
|
7412
|
+
name: path12.basename(root).toLowerCase().replace(/[^a-z0-9-]/g, "-") || "osmo-project",
|
|
6822
7413
|
private: true,
|
|
6823
7414
|
type: "module"
|
|
6824
7415
|
};
|
|
@@ -6828,58 +7419,58 @@ async function init(root = process.cwd()) {
|
|
|
6828
7419
|
throw new Error(
|
|
6829
7420
|
`Existing @osmo/scene dependency (${previous}) was preserved. Initialize a new folder for this release.`
|
|
6830
7421
|
);
|
|
6831
|
-
const configFile =
|
|
7422
|
+
const configFile = path12.join(root, ".osmo/project.json");
|
|
6832
7423
|
if (await exists(configFile)) {
|
|
6833
|
-
const config = JSON.parse(await
|
|
7424
|
+
const config = JSON.parse(await readFile7(configFile, "utf8"));
|
|
6834
7425
|
assertCompatibleProject(config);
|
|
6835
7426
|
}
|
|
6836
|
-
const vendor =
|
|
6837
|
-
await
|
|
6838
|
-
await
|
|
6839
|
-
const target =
|
|
7427
|
+
const vendor = path12.join(root, ".osmo/vendor");
|
|
7428
|
+
await mkdir6(vendor, { recursive: true });
|
|
7429
|
+
await mkdir6(path12.join(root, "assets"), { recursive: true });
|
|
7430
|
+
const target = path12.join(vendor, `osmo-scene-${sceneVersion}.tgz`);
|
|
6840
7431
|
let refreshScene = false;
|
|
6841
|
-
const bundled =
|
|
7432
|
+
const bundled = path12.join(
|
|
6842
7433
|
packageRoot,
|
|
6843
7434
|
"vendor",
|
|
6844
7435
|
`osmo-scene-${sceneVersion}.tgz`
|
|
6845
7436
|
);
|
|
6846
7437
|
if (await exists(bundled)) {
|
|
6847
|
-
if (!await exists(target) || !(await
|
|
7438
|
+
if (!await exists(target) || !(await readFile7(target)).equals(await readFile7(bundled))) {
|
|
6848
7439
|
await copyFile(bundled, target);
|
|
6849
7440
|
refreshScene = true;
|
|
6850
7441
|
}
|
|
6851
7442
|
} else {
|
|
6852
|
-
const scratch = await mkdtemp(
|
|
7443
|
+
const scratch = await mkdtemp(path12.join(os2.tmpdir(), "osmo-scene-pack-"));
|
|
6853
7444
|
try {
|
|
6854
7445
|
await run("npm", ["run", "build"], {
|
|
6855
|
-
cwd:
|
|
7446
|
+
cwd: path12.join(runtimeRoot, "packages/scene")
|
|
6856
7447
|
});
|
|
6857
|
-
const sceneRoot =
|
|
7448
|
+
const sceneRoot = path12.join(runtimeRoot, "packages/scene");
|
|
6858
7449
|
const { scripts, devDependencies, ...scenePackage } = JSON.parse(
|
|
6859
|
-
await
|
|
7450
|
+
await readFile7(path12.join(sceneRoot, "package.json"), "utf8")
|
|
6860
7451
|
);
|
|
6861
|
-
await
|
|
6862
|
-
|
|
7452
|
+
await writeFile6(
|
|
7453
|
+
path12.join(scratch, "package.json"),
|
|
6863
7454
|
JSON.stringify({ ...scenePackage, files: ["dist"] }, null, 2)
|
|
6864
7455
|
);
|
|
6865
|
-
await cp2(
|
|
7456
|
+
await cp2(path12.join(sceneRoot, "dist"), path12.join(scratch, "dist"), {
|
|
6866
7457
|
recursive: true
|
|
6867
7458
|
});
|
|
6868
7459
|
await run("npm", ["pack", "--pack-destination", scratch], {
|
|
6869
7460
|
cwd: scratch
|
|
6870
7461
|
});
|
|
6871
|
-
const packed =
|
|
6872
|
-
if (!await exists(target) || !(await
|
|
7462
|
+
const packed = path12.join(scratch, `osmo-scene-${sceneVersion}.tgz`);
|
|
7463
|
+
if (!await exists(target) || !(await readFile7(target)).equals(await readFile7(packed))) {
|
|
6873
7464
|
await copyFile(packed, target);
|
|
6874
7465
|
refreshScene = true;
|
|
6875
7466
|
}
|
|
6876
7467
|
} finally {
|
|
6877
|
-
await
|
|
7468
|
+
await rm6(scratch, { recursive: true, force: true });
|
|
6878
7469
|
}
|
|
6879
7470
|
}
|
|
6880
7471
|
if (!previous) {
|
|
6881
7472
|
manifest2.dependencies = { ...manifest2.dependencies, "@osmo/scene": spec };
|
|
6882
|
-
await
|
|
7473
|
+
await writeFile6(manifestPath, JSON.stringify(manifest2, null, 2) + "\n");
|
|
6883
7474
|
}
|
|
6884
7475
|
const tsconfig = JSON.stringify(
|
|
6885
7476
|
{
|
|
@@ -6898,7 +7489,7 @@ async function init(root = process.cwd()) {
|
|
|
6898
7489
|
null,
|
|
6899
7490
|
2
|
|
6900
7491
|
) + "\n";
|
|
6901
|
-
await create(
|
|
7492
|
+
await create(path12.join(root, "tsconfig.json"), tsconfig);
|
|
6902
7493
|
await writeStarter(root);
|
|
6903
7494
|
await create(
|
|
6904
7495
|
configFile,
|
|
@@ -6909,8 +7500,8 @@ async function init(root = process.cwd()) {
|
|
|
6909
7500
|
) + "\n"
|
|
6910
7501
|
);
|
|
6911
7502
|
await writeAuthoringGuide(root);
|
|
6912
|
-
const ignorePath =
|
|
6913
|
-
const ignore = await exists(ignorePath) ? await
|
|
7503
|
+
const ignorePath = path12.join(root, ".gitignore");
|
|
7504
|
+
const ignore = await exists(ignorePath) ? await readFile7(ignorePath, "utf8") : "";
|
|
6914
7505
|
const rules = [
|
|
6915
7506
|
"node_modules/",
|
|
6916
7507
|
"exports/",
|
|
@@ -6927,7 +7518,7 @@ async function init(root = process.cwd()) {
|
|
|
6927
7518
|
];
|
|
6928
7519
|
const missing = rules.filter((rule) => !ignore.split("\n").includes(rule));
|
|
6929
7520
|
if (missing.length)
|
|
6930
|
-
await
|
|
7521
|
+
await writeFile6(
|
|
6931
7522
|
ignorePath,
|
|
6932
7523
|
ignore + (ignore && !ignore.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n"
|
|
6933
7524
|
);
|
|
@@ -6985,7 +7576,7 @@ async function studioCommand(argv, help) {
|
|
|
6985
7576
|
if (args[0] === "studio" && args[1] === "clone") {
|
|
6986
7577
|
if (args.length !== 4)
|
|
6987
7578
|
throw new Error("Usage: osmo studio clone PROJECT_ID folder");
|
|
6988
|
-
const result = await clone(args[2],
|
|
7579
|
+
const result = await clone(args[2], path22.resolve(args[3]));
|
|
6989
7580
|
console.log(
|
|
6990
7581
|
flags.json ? JSON.stringify(result) : `Cloned ${result.name} (${result.revisions} revision${result.revisions === 1 ? "" : "s"}) into ${args[3]}.
|
|
6991
7582
|
Next: cd ${args[3]} && osmo studio open scene.tsx`
|
|
@@ -7101,7 +7692,7 @@ ${c.text}${c.replies.map((r) => "\n \u21B3 " + r.text).join("")}`
|
|
|
7101
7692
|
throw new Error(
|
|
7102
7693
|
"--frame must be a non-negative integer (frame 0 is the first frame)."
|
|
7103
7694
|
);
|
|
7104
|
-
if (!flags.out ||
|
|
7695
|
+
if (!flags.out || path22.extname(flags.out).toLowerCase() !== (still ? ".png" : ".mp4"))
|
|
7105
7696
|
throw new Error(
|
|
7106
7697
|
still ? "With --frame, provide --out frame.png" : "Provide --out animation.mp4, or --frame NUMBER --out frame.png"
|
|
7107
7698
|
);
|
|
@@ -7114,7 +7705,7 @@ ${c.text}${c.replies.map((r) => "\n \u21B3 " + r.text).join("")}`
|
|
|
7114
7705
|
nativeBinary(),
|
|
7115
7706
|
[
|
|
7116
7707
|
source,
|
|
7117
|
-
...still ? ["--snapshot",
|
|
7708
|
+
...still ? ["--snapshot", path22.resolve(flags.out), "--frame", flags.frame] : ["--export", path22.resolve(flags.out)],
|
|
7118
7709
|
"--supersample",
|
|
7119
7710
|
quality
|
|
7120
7711
|
],
|