@neta-art/cohub 10.0.0 → 11.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -7
- package/dist/board/editor/animation.d.ts +3 -0
- package/dist/board/editor/animation.js +16 -1
- package/dist/board/editor/clipboard.js +7 -0
- package/dist/board/editor/copy.js +23 -18
- package/dist/board/editor/creation.d.ts +7 -3
- package/dist/board/editor/creation.js +5 -4
- package/dist/board/editor/deletion.js +3 -3
- package/dist/board/editor/document.d.ts +8 -2
- package/dist/board/editor/document.js +54 -7
- package/dist/board/editor/editor.d.ts +22 -7
- package/dist/board/editor/editor.js +12 -2
- package/dist/board/editor/index.d.ts +2 -2
- package/dist/board/editor/interaction.d.ts +8 -2
- package/dist/board/editor/interaction.js +34 -1
- package/dist/board/editor/query.d.ts +5 -5
- package/dist/board/editor/query.js +14 -12
- package/dist/board/editor/selection.js +2 -1
- package/dist/board/editor/snapshots.d.ts +0 -2
- package/dist/board/editor/snapshots.js +4 -20
- package/dist/board/export/index.d.ts +25 -4
- package/dist/board/export/index.js +45 -12
- package/dist/board/export/node/index.d.ts +5 -4
- package/dist/board/export/node/index.js +16 -11
- package/dist/board/export/node/scripts.d.ts +16 -0
- package/dist/board/export/node/scripts.js +258 -0
- package/dist/board/export/scene.d.ts +2 -1
- package/dist/board/export/scene.js +24 -7
- package/dist/board/model/animation.d.ts +4 -1
- package/dist/board/model/animation.js +34 -7
- package/dist/board/model/authoring.d.ts +20 -0
- package/dist/board/model/authoring.js +185 -0
- package/dist/board/model/export-plan.js +1 -0
- package/dist/board/model/file-preview.js +1 -1
- package/dist/board/model/file-snapshot.js +3 -2
- package/dist/board/model/image-key.js +7 -3
- package/dist/board/model/index.d.ts +15 -7
- package/dist/board/model/index.js +16 -9
- package/dist/board/model/items/builtin.js +2 -2
- package/dist/board/model/items/create.d.ts +19 -4
- package/dist/board/model/items/create.js +130 -9
- package/dist/board/model/items/index.d.ts +2 -2
- package/dist/board/model/items/index.js +2 -2
- package/dist/board/model/media-playback.d.ts +2 -1
- package/dist/board/model/media-playback.js +15 -5
- package/dist/board/model/media.js +7 -1
- package/dist/board/model/overlay-placement.d.ts +25 -0
- package/dist/board/model/overlay-placement.js +35 -0
- package/dist/board/model/replay.d.ts +1 -1
- package/dist/board/model/replay.js +1 -0
- package/dist/board/model/sampling.d.ts +19 -0
- package/dist/board/model/sampling.js +96 -0
- package/dist/board/model/script-example.d.ts +4 -0
- package/dist/board/model/script-example.js +21 -0
- package/dist/board/model/script-hit.d.ts +27 -0
- package/dist/board/model/script-hit.js +109 -0
- package/dist/board/model/script-index.d.ts +7 -0
- package/dist/board/model/script-index.js +15 -0
- package/dist/board/model/script-observers.d.ts +18 -0
- package/dist/board/model/script-observers.js +92 -0
- package/dist/board/model/script-scene.d.ts +24 -0
- package/dist/board/model/script-scene.js +294 -0
- package/dist/board/model/script.d.ts +149 -0
- package/dist/board/model/script.js +1 -0
- package/dist/board/render/board-background.js +5 -4
- package/dist/board/render/css-color.d.ts +6 -1
- package/dist/board/render/css-color.js +34 -20
- package/dist/board/render/diagnostics.d.ts +20 -0
- package/dist/board/render/diagnostics.js +117 -0
- package/dist/board/render/index.d.ts +9 -6
- package/dist/board/render/index.js +10 -7
- package/dist/board/render/media-interaction.js +1 -1
- package/dist/board/render/palette.d.ts +2 -1
- package/dist/board/render/palette.js +6 -2
- package/dist/board/render/renderers/arrow-card-renderer.js +3 -2
- package/dist/board/render/renderers/audio-card-renderer.js +2 -1
- package/dist/board/render/renderers/board-renderer-registry.d.ts +25 -5
- package/dist/board/render/renderers/board-renderer-registry.js +11 -4
- package/dist/board/render/renderers/custom-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/{sketch-card-renderer.js → custom-card-renderer.js} +18 -16
- package/dist/board/render/renderers/draw-card-renderer.js +3 -2
- package/dist/board/render/renderers/effect-card-renderer.js +22 -5
- package/dist/board/render/renderers/frame-card-renderer.js +30 -14
- package/dist/board/render/renderers/image-card-renderer.js +43 -4
- package/dist/board/render/renderers/shape-card-renderer.js +10 -27
- package/dist/board/render/renderers/task-card-renderer.d.ts +2 -1
- package/dist/board/render/renderers/task-card-renderer.js +220 -71
- package/dist/board/render/renderers/text-card-renderer.js +5 -3
- package/dist/board/render/renderers/unknown-card-renderer.js +1 -0
- package/dist/board/render/renderers/video-card-renderer.js +2 -1
- package/dist/board/render/sampling.d.ts +22 -0
- package/dist/board/render/sampling.js +21 -0
- package/dist/board/render/script-export.d.ts +11 -0
- package/dist/board/render/script-export.js +86 -0
- package/dist/board/render/script-host.d.ts +31 -0
- package/dist/board/render/script-host.js +446 -0
- package/dist/board/render/script-modules.d.ts +2 -0
- package/dist/board/render/script-modules.js +2 -0
- package/dist/board/render/script-runtime.d.ts +29 -0
- package/dist/board/render/script-runtime.js +223 -0
- package/dist/board/render/script-sandbox.d.ts +4 -0
- package/dist/board/render/script-sandbox.js +173 -0
- package/dist/board/render/script-texture.d.ts +11 -0
- package/dist/board/render/script-texture.js +33 -0
- package/dist/board/render/stroke.d.ts +2 -1
- package/dist/board/render/stroke.js +22 -1
- package/dist/board/render/text-measurement.js +1 -1
- package/dist/board/stage/assets.js +7 -2
- package/dist/board/stage/maintenance.d.ts +5 -0
- package/dist/board/stage/maintenance.js +11 -0
- package/dist/board/stage/scene.d.ts +2 -0
- package/dist/board/stage/scene.js +21 -2
- package/dist/board/stage/script-pointer.d.ts +20 -0
- package/dist/board/stage/script-pointer.js +41 -0
- package/dist/board/stage/stage.d.ts +21 -3
- package/dist/board/stage/stage.js +350 -47
- package/dist/board/stage/texture-pool.js +2 -3
- package/dist/chunks/environment.d.ts +305 -27
- package/dist/chunks/environment.js +12 -4
- package/dist/chunks/host-bridge.d.ts +2 -2
- package/dist/chunks/http.d.ts +90 -7
- package/dist/chunks/http.js +157 -64
- package/dist/chunks/transport.js +4 -2
- package/dist/chunks/websocket.d.ts +9 -1
- package/dist/chunks/websocket.js +16 -2
- package/dist/host-bridge.js +2 -1
- package/dist/http.d.ts +3 -3
- package/dist/index.d.ts +182 -4
- package/dist/index.js +1559 -299
- package/dist/protocol/dist/board-assets.d.ts +1 -0
- package/dist/protocol/dist/board-constants.js +2 -2
- package/dist/protocol/dist/board-model.d.ts +236 -11
- package/dist/protocol/dist/board-model.js +46 -17
- package/dist/protocol/dist/board-patch.js +2 -1
- package/dist/protocol/dist/board-schema.d.ts +4 -2
- package/dist/protocol/dist/board-schema.js +18 -4
- package/dist/protocol/dist/board-script-modules.d.ts +25 -0
- package/dist/protocol/dist/board-script-modules.js +127 -0
- package/dist/protocol/dist/board-task.d.ts +22 -0
- package/dist/{board/model/task.js → protocol/dist/board-task.js} +34 -7
- package/dist/protocol/dist/display/index.d.ts +1 -0
- package/dist/protocol/dist/generation/blocks.d.ts +8 -0
- package/dist/{generation-blocks.js → protocol/dist/generation/blocks.js} +14 -15
- package/dist/protocol/dist/index.d.ts +7 -2
- package/dist/protocol/dist/realtime/types.d.ts +1 -0
- package/dist/protocol/dist/rtc/index.d.ts +1 -0
- package/dist/protocol/dist/task/index.d.ts +4 -0
- package/package.json +2 -1
- package/dist/board/export/node/sketch.d.ts +0 -19
- package/dist/board/export/node/sketch.js +0 -146
- package/dist/board/model/task.d.ts +0 -11
- package/dist/board/render/renderers/sketch-card-renderer.d.ts +0 -5
- package/dist/board/render/sketch-host.d.ts +0 -14
- package/dist/board/render/sketch-host.js +0 -216
- package/dist/types.d.ts +0 -36
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { boardScriptScope, composeBoardScriptItems } from "../../model/script-scene.js";
|
|
2
|
+
import { buildBoardScene } from "../../model/scene.js";
|
|
3
|
+
import { createBoardScriptModuleReader, loadBoardScriptSources } from "../../../protocol/dist/board-script-modules.js";
|
|
4
|
+
import { createScriptRuntime } from "../../render/script-runtime.js";
|
|
5
|
+
import { Worker } from "node:worker_threads";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
//#region src/board/export/node/scripts.ts
|
|
8
|
+
const WORKER = `
|
|
9
|
+
const {parentPort, workerData} = require('node:worker_threads');
|
|
10
|
+
const {createContext, Script, SourceTextModule} = require('node:vm');
|
|
11
|
+
const {createCanvas} = require(workerData.canvasModule);
|
|
12
|
+
const instances = new Map();
|
|
13
|
+
const graphs = new Map();
|
|
14
|
+
const update = new Script('__runtime.update(JSON.parse(__inputJson)); __runtime.step(__time)');
|
|
15
|
+
const draw = new Script('__runtime.draw(__drawing)');
|
|
16
|
+
const output = new Script('JSON.stringify(__runtime.output())');
|
|
17
|
+
const dispose = new Script('__runtime.destroy()');
|
|
18
|
+
function release(id) {
|
|
19
|
+
const entry=instances.get(id);instances.delete(id);
|
|
20
|
+
if(entry) {try {dispose.runInContext(entry.context,{timeout:workerData.timeout});} catch {}}
|
|
21
|
+
}
|
|
22
|
+
async function create(data) {
|
|
23
|
+
const context = createContext({console:{log(){},warn(){},error(){}},structuredClone},{codeGeneration:{strings:false,wasm:false}});
|
|
24
|
+
new Script("const __name=(value,name)=>Object.defineProperty(value,'name',{value:name,configurable:true})").runInContext(context);
|
|
25
|
+
const sources = graphs.get(data.src);
|
|
26
|
+
if(!sources) throw new Error('Module graph is unavailable.');
|
|
27
|
+
const modules = new Map();
|
|
28
|
+
const resolve = (from,name) => {
|
|
29
|
+
const parts=from.split('/').slice(0,-1);
|
|
30
|
+
for(const part of name.split('/')) { if(part==='..') parts.pop(); else if(part!=='.') parts.push(part); }
|
|
31
|
+
return parts.join('/');
|
|
32
|
+
};
|
|
33
|
+
const link = (name,ref) => load(resolve(ref.identifier,name));
|
|
34
|
+
function load(path) {
|
|
35
|
+
if(modules.has(path)) return modules.get(path);
|
|
36
|
+
const source=sources[path];
|
|
37
|
+
if(!source) throw new Error('Module is unavailable: '+path);
|
|
38
|
+
const module=new SourceTextModule(source.code,{context,identifier:path,importModuleDynamically:async name=>{
|
|
39
|
+
const child=load(resolve(path,name));
|
|
40
|
+
if(child.status==='unlinked') await child.link(link);
|
|
41
|
+
if(child.status==='linked') await child.evaluate({timeout:workerData.timeout});
|
|
42
|
+
return child;
|
|
43
|
+
}});
|
|
44
|
+
modules.set(path,module);return module;
|
|
45
|
+
}
|
|
46
|
+
const root=load(data.src);
|
|
47
|
+
await root.link(link);await root.evaluate({timeout:workerData.timeout});
|
|
48
|
+
context.__module=root.namespace;
|
|
49
|
+
context.__inputJson=JSON.stringify(data.input);
|
|
50
|
+
new Script('globalThis.__runtime=('+workerData.runtime+')(__module,JSON.parse(__inputJson),{call:async()=>{throw new Error("Capabilities are unavailable during export.")},invalidate(){}})').runInContext(context,{timeout:workerData.timeout});
|
|
51
|
+
return {context,signature:data.signature,time:-1,draw:!!root.namespace.draw,canvas:null};
|
|
52
|
+
}
|
|
53
|
+
parentPort.on('message',async data=>{
|
|
54
|
+
try {
|
|
55
|
+
if(data.release) {for(const id of data.release) release(id);return;}
|
|
56
|
+
if(data.sources) graphs.set(data.src,data.sources);
|
|
57
|
+
let entry=instances.get(data.input.id);
|
|
58
|
+
if(!entry || entry.signature!==data.signature || data.time<entry.time) {
|
|
59
|
+
release(data.input.id);
|
|
60
|
+
entry=await create(data);instances.set(data.input.id,entry);
|
|
61
|
+
}
|
|
62
|
+
const context=entry.context;
|
|
63
|
+
context.__inputJson=JSON.stringify(data.input);context.__time=data.time;
|
|
64
|
+
update.runInContext(context,{timeout:workerData.timeout});entry.time=data.time;
|
|
65
|
+
let png;
|
|
66
|
+
if(entry.draw) {
|
|
67
|
+
const width=Math.max(1,Math.round(data.input.width*data.scale));
|
|
68
|
+
const height=Math.max(1,Math.round(data.input.height*data.scale));
|
|
69
|
+
const canvas=entry.canvas??=createCanvas(width,height);
|
|
70
|
+
canvas.width=width;canvas.height=height;
|
|
71
|
+
const drawing=canvas.getContext('2d');drawing.scale(data.scale,data.scale);
|
|
72
|
+
context.__drawing=drawing;draw.runInContext(context,{timeout:workerData.timeout});
|
|
73
|
+
png=canvas.toBuffer('image/png');delete context.__drawing;
|
|
74
|
+
}
|
|
75
|
+
parentPort.postMessage({png,output:JSON.parse(output.runInContext(context,{timeout:workerData.timeout}))});
|
|
76
|
+
} catch(error) { instances.delete(data.input?.id);parentPort.postMessage({error:String(error.stack||error)}); }
|
|
77
|
+
});
|
|
78
|
+
`;
|
|
79
|
+
function createNodeBoardScriptHost(renderer, options) {
|
|
80
|
+
const textures = /* @__PURE__ */ new Map();
|
|
81
|
+
const outputs = /* @__PURE__ */ new Map();
|
|
82
|
+
const errors = /* @__PURE__ */ new Map();
|
|
83
|
+
const sources = /* @__PURE__ */ new Map();
|
|
84
|
+
const modules = createBoardScriptModuleReader(options.readModule, options.readTimeoutMs);
|
|
85
|
+
const lanes = Array.from({ length: 4 }, () => ({
|
|
86
|
+
worker: null,
|
|
87
|
+
tail: Promise.resolve(),
|
|
88
|
+
graphs: /* @__PURE__ */ new Set()
|
|
89
|
+
}));
|
|
90
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
91
|
+
let disposed = false;
|
|
92
|
+
let nextLane = 0;
|
|
93
|
+
const timeout = options.timeoutMs ?? 2e3;
|
|
94
|
+
const key = (id, time) => `${id}|${time}`;
|
|
95
|
+
function stop(lane) {
|
|
96
|
+
const worker = lane.worker;
|
|
97
|
+
lane.worker = null;
|
|
98
|
+
lane.graphs.clear();
|
|
99
|
+
worker?.terminate();
|
|
100
|
+
}
|
|
101
|
+
function sample(input, graph, time, scale) {
|
|
102
|
+
let lane = assignments.get(input.id);
|
|
103
|
+
if (!lane) {
|
|
104
|
+
lane = lanes[nextLane++ % lanes.length];
|
|
105
|
+
assignments.set(input.id, lane);
|
|
106
|
+
}
|
|
107
|
+
const selected = lane;
|
|
108
|
+
const task = selected.tail.then(() => new Promise((resolve, reject) => {
|
|
109
|
+
if (disposed) {
|
|
110
|
+
reject(/* @__PURE__ */ new Error("Script host is disposed."));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!Number.isFinite(time) || time < 0 || time > 6e5) {
|
|
114
|
+
reject(/* @__PURE__ */ new Error("Script export time must be between 0 and 600000 ms."));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const freshWorker = !selected.worker;
|
|
118
|
+
selected.worker ??= new Worker(WORKER, {
|
|
119
|
+
eval: true,
|
|
120
|
+
execArgv: ["--experimental-vm-modules", "--disable-warning=ExperimentalWarning"],
|
|
121
|
+
env: {},
|
|
122
|
+
resourceLimits: { maxOldGenerationSizeMb: 128 },
|
|
123
|
+
workerData: {
|
|
124
|
+
runtime: createScriptRuntime.toString(),
|
|
125
|
+
timeout,
|
|
126
|
+
canvasModule: fileURLToPath(import.meta.resolve("@napi-rs/canvas"))
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
const worker = selected.worker;
|
|
130
|
+
if (freshWorker) {
|
|
131
|
+
const stopped = (error) => {
|
|
132
|
+
if (selected.worker !== worker) return;
|
|
133
|
+
if (selected.reject) selected.reject(error);
|
|
134
|
+
else stop(selected);
|
|
135
|
+
};
|
|
136
|
+
worker.on("error", stopped);
|
|
137
|
+
worker.on("exit", () => stopped(/* @__PURE__ */ new Error("Script worker stopped.")));
|
|
138
|
+
}
|
|
139
|
+
const cleanup = () => {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
worker.off("message", message);
|
|
142
|
+
selected.reject = void 0;
|
|
143
|
+
};
|
|
144
|
+
const failure = (error) => {
|
|
145
|
+
cleanup();
|
|
146
|
+
stop(selected);
|
|
147
|
+
reject(error);
|
|
148
|
+
};
|
|
149
|
+
const message = (value) => {
|
|
150
|
+
cleanup();
|
|
151
|
+
if (value.error) reject(new Error(value.error));
|
|
152
|
+
else resolve(value);
|
|
153
|
+
};
|
|
154
|
+
const timer = setTimeout(() => failure(/* @__PURE__ */ new Error("Script export timed out.")), Math.max(timeout, 15e3));
|
|
155
|
+
selected.reject = failure;
|
|
156
|
+
worker.once("message", message);
|
|
157
|
+
const src = input.item.script?.src ?? "";
|
|
158
|
+
worker.postMessage({
|
|
159
|
+
input,
|
|
160
|
+
src,
|
|
161
|
+
signature: JSON.stringify(input.item.script),
|
|
162
|
+
sources: selected.graphs.has(src) ? void 0 : graph,
|
|
163
|
+
time,
|
|
164
|
+
scale
|
|
165
|
+
});
|
|
166
|
+
selected.graphs.add(src);
|
|
167
|
+
}));
|
|
168
|
+
selected.tail = task.catch(() => void 0);
|
|
169
|
+
return task;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
ready: (id) => [...outputs.values()].some((at) => at.has(id)),
|
|
173
|
+
frame: (item, time) => textures.get(key(item.id, time)) ?? null,
|
|
174
|
+
error: (id) => errors.get(id) ?? null,
|
|
175
|
+
release() {},
|
|
176
|
+
document(document, time) {
|
|
177
|
+
const at = outputs.get(time);
|
|
178
|
+
if (!at?.size) return document;
|
|
179
|
+
const items = { ...document.items };
|
|
180
|
+
for (const [id, item] of composeBoardScriptItems(document, buildBoardScene(document), at)) items[id] = item;
|
|
181
|
+
return {
|
|
182
|
+
...document,
|
|
183
|
+
items
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
clearFrames() {
|
|
187
|
+
for (const texture of textures.values()) texture.destroy(true);
|
|
188
|
+
textures.clear();
|
|
189
|
+
outputs.clear();
|
|
190
|
+
errors.clear();
|
|
191
|
+
},
|
|
192
|
+
async prepare(items, times, zoom) {
|
|
193
|
+
const scripted = items.filter((item) => item.script?.enabled);
|
|
194
|
+
const alive = new Set(scripted.map((item) => item.id));
|
|
195
|
+
for (const [id, lane] of assignments) if (!alive.has(id)) {
|
|
196
|
+
lane.worker?.postMessage({ release: [id] });
|
|
197
|
+
assignments.delete(id);
|
|
198
|
+
}
|
|
199
|
+
if (!scripted.length) return;
|
|
200
|
+
const all = Object.fromEntries(items.map(({ id, frame: _frame, ...item }) => [id, item]));
|
|
201
|
+
await Promise.all(scripted.map(async (item) => {
|
|
202
|
+
const base = all[item.id];
|
|
203
|
+
const src = item.script?.src;
|
|
204
|
+
if (!base || !src) return;
|
|
205
|
+
try {
|
|
206
|
+
let graph = sources.get(src);
|
|
207
|
+
if (!graph) {
|
|
208
|
+
graph = loadBoardScriptSources(src, modules.read, options.readTimeoutMs);
|
|
209
|
+
sources.set(src, graph);
|
|
210
|
+
}
|
|
211
|
+
const resolved = await graph;
|
|
212
|
+
const width = "size" in item ? item.size.width : item.frame.width;
|
|
213
|
+
const height = "size" in item ? item.size.height : item.frame.height;
|
|
214
|
+
const scale = Math.min(4, Math.max(.25, zoom), 2048 / Math.max(1, width, height));
|
|
215
|
+
const scoped = Object.fromEntries([...boardScriptScope({ items: all }, item.id)].flatMap((id) => all[id] ? [[id, all[id]]] : []));
|
|
216
|
+
for (const time of times) {
|
|
217
|
+
const result = await sample({
|
|
218
|
+
id: item.id,
|
|
219
|
+
item: base,
|
|
220
|
+
items: scoped,
|
|
221
|
+
width,
|
|
222
|
+
height
|
|
223
|
+
}, resolved, time, scale);
|
|
224
|
+
if (disposed) return;
|
|
225
|
+
if (result.png) {
|
|
226
|
+
const frameKey = key(item.id, time);
|
|
227
|
+
textures.get(frameKey)?.destroy(true);
|
|
228
|
+
textures.set(frameKey, await renderer.decodeImage(result.png));
|
|
229
|
+
}
|
|
230
|
+
let at = outputs.get(time);
|
|
231
|
+
if (!at) {
|
|
232
|
+
at = /* @__PURE__ */ new Map();
|
|
233
|
+
outputs.set(time, at);
|
|
234
|
+
}
|
|
235
|
+
at.set(item.id, result.output);
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
errors.set(item.id, error instanceof Error ? error.message : String(error));
|
|
239
|
+
}
|
|
240
|
+
}));
|
|
241
|
+
},
|
|
242
|
+
destroy() {
|
|
243
|
+
disposed = true;
|
|
244
|
+
for (const lane of lanes) {
|
|
245
|
+
lane.reject?.(/* @__PURE__ */ new Error("Script host is disposed."));
|
|
246
|
+
stop(lane);
|
|
247
|
+
}
|
|
248
|
+
for (const texture of textures.values()) texture.destroy(true);
|
|
249
|
+
textures.clear();
|
|
250
|
+
outputs.clear();
|
|
251
|
+
sources.clear();
|
|
252
|
+
modules.destroy();
|
|
253
|
+
assignments.clear();
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
export { createNodeBoardScriptHost };
|
|
@@ -12,13 +12,14 @@ type BoardExportSceneInput = {
|
|
|
12
12
|
scene: BoardScene;
|
|
13
13
|
items: readonly BoardSceneItem[];
|
|
14
14
|
time?: number;
|
|
15
|
+
frameAt?: BoardRenderContext["frameAt"];
|
|
15
16
|
world: Rect;
|
|
16
17
|
scale: number;
|
|
17
18
|
colorScheme: "dark" | "light";
|
|
18
19
|
palette?: Partial<BoardRenderPalette>;
|
|
19
20
|
colors?: BoardShapeColors;
|
|
20
21
|
textures?: Map<string, Texture>;
|
|
21
|
-
|
|
22
|
+
scripts?: BoardRenderContext["scripts"];
|
|
22
23
|
assetKey?: (item: BoardSceneItem) => string | null;
|
|
23
24
|
/** Renderers for item types beyond the built-ins. */
|
|
24
25
|
renderers?: BoardCardRendererResolver;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { buildFallbackShapeColors } from "../model/palette.js";
|
|
2
2
|
import { imageAssetKey } from "../model/image-key.js";
|
|
3
|
-
import { clippingAncestor, isClippingFrame, syncClipGroup } from "../render/clip.js";
|
|
4
3
|
import { defaultBoardPalette } from "../render/palette.js";
|
|
5
|
-
import {
|
|
4
|
+
import { clippingAncestor, isClippingFrame, syncClipGroup } from "../render/clip.js";
|
|
5
|
+
import { DEFAULT_BOARD_RENDER_LABELS, getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
|
|
6
6
|
import { Container, Graphics, Sprite, TilingSprite } from "pixi.js";
|
|
7
7
|
//#region src/board/export/scene.ts
|
|
8
8
|
function buildContext(input) {
|
|
@@ -13,6 +13,7 @@ function buildContext(input) {
|
|
|
13
13
|
settings: input.settings,
|
|
14
14
|
scene: input.scene,
|
|
15
15
|
time: input.time ?? 0,
|
|
16
|
+
frameAt: input.frameAt,
|
|
16
17
|
selectedIds: /* @__PURE__ */ new Set(),
|
|
17
18
|
hoveredId: null,
|
|
18
19
|
resizingIds: /* @__PURE__ */ new Set(),
|
|
@@ -24,7 +25,10 @@ function buildContext(input) {
|
|
|
24
25
|
colorScheme: input.colorScheme,
|
|
25
26
|
rendererType: "canvas",
|
|
26
27
|
zoom: input.scale,
|
|
27
|
-
|
|
28
|
+
reducedMotion: true,
|
|
29
|
+
labels: DEFAULT_BOARD_RENDER_LABELS,
|
|
30
|
+
taskRun: () => null,
|
|
31
|
+
scripts: input.scripts,
|
|
28
32
|
assetKey: input.assetKey ?? imageAssetKey,
|
|
29
33
|
getTexture: (key) => textures.get(key) ?? null,
|
|
30
34
|
hasError: (key) => {
|
|
@@ -77,13 +81,26 @@ function createBoardExportScene(input) {
|
|
|
77
81
|
world.position.set(-input.world.x * input.scale, -input.world.y * input.scale);
|
|
78
82
|
const resolve = input.renderers ?? getBoardCardRenderer;
|
|
79
83
|
const groups = /* @__PURE__ */ new Map();
|
|
80
|
-
|
|
84
|
+
function hostFor(item) {
|
|
81
85
|
const clip = clippingAncestor(input.scene, item);
|
|
82
|
-
const
|
|
86
|
+
const ancestor = clip ? input.scene.get(clip) : void 0;
|
|
87
|
+
if (!ancestor) return world;
|
|
88
|
+
let group = groups.get(ancestor.id);
|
|
89
|
+
if (!group) {
|
|
90
|
+
group = syncClipGroup(void 0, ancestor).group;
|
|
91
|
+
hostFor(ancestor).addChild(group);
|
|
92
|
+
groups.set(ancestor.id, group);
|
|
93
|
+
}
|
|
94
|
+
return group;
|
|
95
|
+
}
|
|
96
|
+
for (const item of input.items) {
|
|
97
|
+
const opacity = input.scene.opacity(item.id);
|
|
98
|
+
if (opacity <= 0) continue;
|
|
99
|
+
const host = hostFor(item);
|
|
83
100
|
const container = resolve(item, context).create(item, context);
|
|
84
|
-
container.alpha =
|
|
101
|
+
container.alpha = opacity;
|
|
85
102
|
host.addChild(container);
|
|
86
|
-
if (isClippingFrame(item)) {
|
|
103
|
+
if (isClippingFrame(item) && !groups.has(item.id)) {
|
|
87
104
|
const { group } = syncClipGroup(void 0, item);
|
|
88
105
|
host.addChild(group);
|
|
89
106
|
groups.set(item.id, group);
|
|
@@ -35,7 +35,10 @@ type CompiledBoardAnimations = {
|
|
|
35
35
|
readonly document: BoardDocument;
|
|
36
36
|
};
|
|
37
37
|
declare function compileBoardAnimations(document: BoardDocument): CompiledBoardAnimations;
|
|
38
|
-
type BoardColorResolver = (color: BoardColor) => number |
|
|
38
|
+
type BoardColorResolver = (color: BoardColor) => number | {
|
|
39
|
+
color: number;
|
|
40
|
+
alpha: number;
|
|
41
|
+
} | null;
|
|
39
42
|
declare function parseHexColor(color: BoardColor): number | null;
|
|
40
43
|
declare function sampleBoardTrack(track: CompiledTrack, time: number, resolveColor?: BoardColorResolver): unknown;
|
|
41
44
|
type BoardCameraOverride = {
|
|
@@ -183,11 +183,21 @@ function hex(color) {
|
|
|
183
183
|
return `#${Math.max(0, Math.min(16777215, Math.round(color))).toString(16).padStart(6, "0")}`;
|
|
184
184
|
}
|
|
185
185
|
function mixColor(a, b, t, resolve) {
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
189
|
-
const
|
|
190
|
-
|
|
186
|
+
const left = resolve(a);
|
|
187
|
+
const right = resolve(b);
|
|
188
|
+
if (left === null || right === null) return t < 1 ? a : b;
|
|
189
|
+
const from = typeof left === "number" ? {
|
|
190
|
+
color: left,
|
|
191
|
+
alpha: 1
|
|
192
|
+
} : left;
|
|
193
|
+
const to = typeof right === "number" ? {
|
|
194
|
+
color: right,
|
|
195
|
+
alpha: 1
|
|
196
|
+
} : right;
|
|
197
|
+
const alpha = lerp(from.alpha, to.alpha, t);
|
|
198
|
+
const channel = (shift) => alpha > 0 ? Math.round(lerp((from.color >> shift & 255) * from.alpha, (to.color >> shift & 255) * to.alpha, t) / alpha) : 0;
|
|
199
|
+
const rgb = hex(channel(16) << 16 | channel(8) << 8 | channel(0));
|
|
200
|
+
return alpha >= 1 ? rgb : `${rgb}${Math.round(Math.max(0, alpha) * 255).toString(16).padStart(2, "0")}`;
|
|
191
201
|
}
|
|
192
202
|
function parseHexColor(color) {
|
|
193
203
|
if (typeof color !== "string") return null;
|
|
@@ -196,6 +206,23 @@ function parseHexColor(color) {
|
|
|
196
206
|
const digits = match[1].length === 3 ? [...match[1]].map((digit) => digit + digit).join("") : match[1];
|
|
197
207
|
return Number.parseInt(digits, 16);
|
|
198
208
|
}
|
|
209
|
+
function parseAnimationColor(color) {
|
|
210
|
+
if (typeof color === "string") {
|
|
211
|
+
if (color.trim().toLowerCase() === "transparent") return {
|
|
212
|
+
color: 0,
|
|
213
|
+
alpha: 0
|
|
214
|
+
};
|
|
215
|
+
const match = /^#([0-9a-f]{4}|[0-9a-f]{8})$/i.exec(color.trim());
|
|
216
|
+
if (match?.[1]) {
|
|
217
|
+
const digits = match[1].length === 4 ? [...match[1]].map((digit) => digit + digit).join("") : match[1];
|
|
218
|
+
return {
|
|
219
|
+
color: Number.parseInt(digits.slice(0, 6), 16),
|
|
220
|
+
alpha: Number.parseInt(digits.slice(6), 16) / 255
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return parseHexColor(color);
|
|
225
|
+
}
|
|
199
226
|
function blend(track, index, time, resolveColor) {
|
|
200
227
|
const { times, values } = track;
|
|
201
228
|
const from = values[index];
|
|
@@ -229,7 +256,7 @@ function blend(track, index, time, resolveColor) {
|
|
|
229
256
|
default: return progress >= 1 ? to : from;
|
|
230
257
|
}
|
|
231
258
|
}
|
|
232
|
-
function sampleBoardTrack(track, time, resolveColor =
|
|
259
|
+
function sampleBoardTrack(track, time, resolveColor = parseAnimationColor) {
|
|
233
260
|
const index = keyframeIndex(track.times, time);
|
|
234
261
|
if (index < 0) return track.values[0];
|
|
235
262
|
return blend(track, index, time, resolveColor);
|
|
@@ -302,7 +329,7 @@ function outranks(track, time, other, otherTime) {
|
|
|
302
329
|
return track.id > other.id;
|
|
303
330
|
}
|
|
304
331
|
function evaluateBoardAnimations(compiled, times, options = {}) {
|
|
305
|
-
const resolveColor = options.resolveColor ??
|
|
332
|
+
const resolveColor = options.resolveColor ?? parseAnimationColor;
|
|
306
333
|
const local = new Map(times instanceof Map ? times : Object.entries(times));
|
|
307
334
|
for (const id of compiled.order) {
|
|
308
335
|
const animation = compiled.animations.get(id);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BoardAnimation, BoardDocument, BoardPatch, BoardTrack, BoardVec2 } from "../../protocol/dist/board-model.js";
|
|
2
|
+
import "../../protocol/dist/index.js";
|
|
3
|
+
//#region src/board/model/authoring.d.ts
|
|
4
|
+
type BoardChapter = {
|
|
5
|
+
animation: string;
|
|
6
|
+
frame?: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
gap?: number;
|
|
9
|
+
pause?: boolean;
|
|
10
|
+
};
|
|
11
|
+
declare function boardSequence(document: BoardDocument, chapters: readonly BoardChapter[]): BoardAnimation;
|
|
12
|
+
declare function boardSequencePatch(document: BoardDocument, id: string, chapters: readonly BoardChapter[]): BoardPatch;
|
|
13
|
+
declare function boardPathTrack(target: string, points: readonly BoardVec2[], options: {
|
|
14
|
+
duration: number;
|
|
15
|
+
at?: number;
|
|
16
|
+
samples?: number;
|
|
17
|
+
orient?: boolean;
|
|
18
|
+
}): BoardTrack;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { BoardChapter, boardPathTrack, boardSequence, boardSequencePatch };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { BoardIdSchema } from "../../protocol/dist/board-model.js";
|
|
2
|
+
import { applyBoardPatchToDocument } from "../../protocol/dist/board-patch.js";
|
|
3
|
+
//#region src/board/model/authoring.ts
|
|
4
|
+
function validateChapterComposition(document, chapters) {
|
|
5
|
+
const uses = /* @__PURE__ */ new Map();
|
|
6
|
+
for (const [index, chapter] of chapters.entries()) for (const track of Object.values(document.animations[chapter.animation]?.tracks ?? {})) {
|
|
7
|
+
let properties = uses.get(track.target);
|
|
8
|
+
if (!properties) {
|
|
9
|
+
properties = /* @__PURE__ */ new Map();
|
|
10
|
+
uses.set(track.target, properties);
|
|
11
|
+
}
|
|
12
|
+
for (const [property, usage] of properties) if ((property === track.property || property.startsWith(`${track.property}.`) || track.property.startsWith(`${property}.`)) && (usage.additive || track.composite === "add") && [...usage.chapters].some((other) => other !== index)) throw new Error(`Chapters share additive tracks on ${track.target}.${track.property}; use distinct items or combine the tracks before sequencing.`);
|
|
13
|
+
const usage = properties.get(track.property) ?? {
|
|
14
|
+
chapters: /* @__PURE__ */ new Set(),
|
|
15
|
+
additive: false
|
|
16
|
+
};
|
|
17
|
+
usage.chapters.add(index);
|
|
18
|
+
usage.additive ||= track.composite === "add";
|
|
19
|
+
properties.set(track.property, usage);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function boardSequence(document, chapters) {
|
|
23
|
+
if (!chapters.length) throw new Error("A sequence needs at least one chapter.");
|
|
24
|
+
validateChapterComposition(document, chapters);
|
|
25
|
+
const tracks = {};
|
|
26
|
+
const markers = [];
|
|
27
|
+
let cursor = 0;
|
|
28
|
+
const seen = /* @__PURE__ */ new Set();
|
|
29
|
+
const seenFrames = /* @__PURE__ */ new Set();
|
|
30
|
+
for (const [index, chapter] of chapters.entries()) {
|
|
31
|
+
const animation = document.animations[chapter.animation];
|
|
32
|
+
if (!animation) throw new Error(`Animation ${chapter.animation} does not exist.`);
|
|
33
|
+
if (animation.play !== "manual") throw new Error(`Chapter ${chapter.animation} must use manual playback.`);
|
|
34
|
+
if (!Number.isFinite(animation.duration) || animation.duration <= 0) throw new Error("Chapter duration must be positive and finite.");
|
|
35
|
+
if (seen.has(chapter.animation)) throw new Error(`Animation ${chapter.animation} appears more than once; use separate animation IDs.`);
|
|
36
|
+
seen.add(chapter.animation);
|
|
37
|
+
if (chapter.frame && document.items[chapter.frame]?.type !== "frame") throw new Error(`Chapter ${chapter.animation} needs a valid frame.`);
|
|
38
|
+
if (chapter.frame && seenFrames.has(chapter.frame)) throw new Error("Each chapter needs a separate frame, or omit frame visibility control.");
|
|
39
|
+
if (chapter.frame) seenFrames.add(chapter.frame);
|
|
40
|
+
const gap = chapter.gap ?? 0;
|
|
41
|
+
if (!Number.isFinite(gap) || gap < 0) throw new Error("Chapter gap must be finite and non-negative.");
|
|
42
|
+
cursor += gap;
|
|
43
|
+
const start = cursor;
|
|
44
|
+
const end = start + animation.duration;
|
|
45
|
+
const rankWidth = String(Object.keys(animation.tracks).length).length;
|
|
46
|
+
const ranks = new Map(Object.keys(animation.tracks).sort().map((id, rank) => [id, String(rank).padStart(rankWidth, "0")]));
|
|
47
|
+
const chapterIndex = String(index).padStart(String(chapters.length).length, "0");
|
|
48
|
+
for (const [id, track] of Object.entries(animation.tracks)) {
|
|
49
|
+
if (document.animations[track.target]) throw new Error(`Chapter ${chapter.animation} contains nested clocks; compose its leaf animations instead.`);
|
|
50
|
+
if (chapter.frame && track.target === "camera" && track.property === "focus") continue;
|
|
51
|
+
tracks[`chapter-${chapterIndex}-${ranks.get(id)}`] = {
|
|
52
|
+
...track,
|
|
53
|
+
keyframes: track.keyframes.map((keyframe) => ({
|
|
54
|
+
...keyframe,
|
|
55
|
+
at: start + keyframe.at
|
|
56
|
+
}))
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const boundary = end + (chapter.pause && index < chapters.length - 1 ? 1 : 0);
|
|
60
|
+
if (chapter.frame) {
|
|
61
|
+
tracks[`visibility-${index}`] = {
|
|
62
|
+
target: chapter.frame,
|
|
63
|
+
property: "opacity",
|
|
64
|
+
composite: "add",
|
|
65
|
+
interpolation: "step",
|
|
66
|
+
keyframes: [
|
|
67
|
+
...start > 0 ? [{
|
|
68
|
+
at: 0,
|
|
69
|
+
value: 0
|
|
70
|
+
}] : [],
|
|
71
|
+
{
|
|
72
|
+
at: start,
|
|
73
|
+
value: 1
|
|
74
|
+
},
|
|
75
|
+
...index < chapters.length - 1 ? [{
|
|
76
|
+
at: boundary + (chapters[index + 1]?.gap ?? 0),
|
|
77
|
+
value: 0
|
|
78
|
+
}] : []
|
|
79
|
+
]
|
|
80
|
+
};
|
|
81
|
+
tracks[`camera-${index}`] = {
|
|
82
|
+
target: "camera",
|
|
83
|
+
property: "focus",
|
|
84
|
+
composite: "replace",
|
|
85
|
+
interpolation: "step",
|
|
86
|
+
keyframes: [{
|
|
87
|
+
at: start,
|
|
88
|
+
value: chapter.frame
|
|
89
|
+
}]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
markers.push({
|
|
93
|
+
at: start,
|
|
94
|
+
label: chapter.label ?? animation.name ?? chapter.animation
|
|
95
|
+
});
|
|
96
|
+
for (const marker of animation.markers) markers.push({
|
|
97
|
+
...marker,
|
|
98
|
+
at: start + marker.at
|
|
99
|
+
});
|
|
100
|
+
if (chapter.pause) markers.push({
|
|
101
|
+
at: end,
|
|
102
|
+
pause: true
|
|
103
|
+
});
|
|
104
|
+
cursor = boundary;
|
|
105
|
+
}
|
|
106
|
+
const byTime = /* @__PURE__ */ new Map();
|
|
107
|
+
for (const marker of markers) {
|
|
108
|
+
const previous = byTime.get(marker.at);
|
|
109
|
+
byTime.set(marker.at, {
|
|
110
|
+
...previous,
|
|
111
|
+
...marker,
|
|
112
|
+
pause: Boolean(previous?.pause || marker.pause)
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
duration: cursor,
|
|
117
|
+
play: "manual",
|
|
118
|
+
end: "hold",
|
|
119
|
+
delay: 0,
|
|
120
|
+
loop: false,
|
|
121
|
+
tracks,
|
|
122
|
+
markers: [...byTime.values()].sort((a, b) => a.at - b.at)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function boardSequencePatch(document, id, chapters) {
|
|
126
|
+
BoardIdSchema.parse(id);
|
|
127
|
+
if (chapters.some((chapter) => chapter.animation === id)) throw new Error("A sequence cannot overwrite one of its source chapters.");
|
|
128
|
+
const animation = boardSequence(document, chapters);
|
|
129
|
+
const tracks = { ...animation.tracks };
|
|
130
|
+
for (const key of Object.keys(document.animations[id]?.tracks ?? {})) if (!(key in tracks)) tracks[key] = null;
|
|
131
|
+
const patch = { animations: { [id]: {
|
|
132
|
+
...animation,
|
|
133
|
+
tracks
|
|
134
|
+
} } };
|
|
135
|
+
const result = applyBoardPatchToDocument(document, patch);
|
|
136
|
+
if (!result.ok) throw new Error(result.diagnostics.map((entry) => `${entry.path}: ${entry.message}`).join("; "));
|
|
137
|
+
return patch;
|
|
138
|
+
}
|
|
139
|
+
function boardPathTrack(target, points, options) {
|
|
140
|
+
const { duration, at = 0, samples = 64, orient = false } = options;
|
|
141
|
+
if (points.length < 2 || points.length > 1e4 || points.some((p) => !Number.isFinite(p.x) || !Number.isFinite(p.y))) throw new Error("A path needs 2 to 10000 finite points.");
|
|
142
|
+
if (!Number.isFinite(duration) || duration <= 0 || !Number.isFinite(at) || at < 0) throw new Error("Path duration must be positive and start time non-negative.");
|
|
143
|
+
if (!Number.isInteger(samples) || samples < 2 || samples > 1e4) throw new Error("Path samples must be an integer from 2 to 10000.");
|
|
144
|
+
const segments = [];
|
|
145
|
+
let total = 0;
|
|
146
|
+
let previous = points[0];
|
|
147
|
+
for (const point of points.slice(1)) {
|
|
148
|
+
if (!previous) break;
|
|
149
|
+
const start = total;
|
|
150
|
+
total += Math.hypot(point.x - previous.x, point.y - previous.y);
|
|
151
|
+
segments.push({
|
|
152
|
+
a: previous,
|
|
153
|
+
b: point,
|
|
154
|
+
start,
|
|
155
|
+
end: total
|
|
156
|
+
});
|
|
157
|
+
previous = point;
|
|
158
|
+
}
|
|
159
|
+
const distances = [.../* @__PURE__ */ new Set([...segments.map((segment) => segment.end), ...Array.from({ length: samples }, (_, index) => total * index / (samples - 1))])].sort((a, b) => a - b);
|
|
160
|
+
let segmentIndex = 0;
|
|
161
|
+
return {
|
|
162
|
+
target,
|
|
163
|
+
property: "position",
|
|
164
|
+
composite: "replace",
|
|
165
|
+
interpolation: "auto",
|
|
166
|
+
orient,
|
|
167
|
+
keyframes: distances.map((distance) => {
|
|
168
|
+
const progress = total > 0 ? distance / total : 0;
|
|
169
|
+
while (segmentIndex < segments.length - 1 && (segments[segmentIndex]?.end ?? 0) < distance) segmentIndex++;
|
|
170
|
+
const segment = segments[segmentIndex];
|
|
171
|
+
if (!segment) throw new Error("Path has no segments.");
|
|
172
|
+
const { a, b, start, end } = segment;
|
|
173
|
+
const fraction = end > start ? (distance - start) / (end - start) : 0;
|
|
174
|
+
return {
|
|
175
|
+
at: at + duration * progress,
|
|
176
|
+
value: {
|
|
177
|
+
x: a.x + (b.x - a.x) * fraction,
|
|
178
|
+
y: a.y + (b.y - a.y) * fraction
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
})
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
export { boardPathTrack, boardSequence, boardSequencePatch };
|
|
@@ -43,6 +43,7 @@ function resolveRegion(scene, region) {
|
|
|
43
43
|
return boundsOf.get(item.id) ?? null;
|
|
44
44
|
};
|
|
45
45
|
const visible = (item, rect) => {
|
|
46
|
+
if (item.type === "effect" && item.props.kind === "trail" && item.props.follow) return true;
|
|
46
47
|
const box = bounds(item);
|
|
47
48
|
return box !== null && rectsIntersect(box, rect);
|
|
48
49
|
};
|
|
@@ -13,7 +13,7 @@ function filePreviewKind(snapshot) {
|
|
|
13
13
|
}
|
|
14
14
|
/** Short uppercase type label for a file card (`MD`, `JSON`, `FILE`). */
|
|
15
15
|
function fileTypeLabel(path) {
|
|
16
|
-
const name = path
|
|
16
|
+
const name = fileBaseName(path);
|
|
17
17
|
const dot = name.lastIndexOf(".");
|
|
18
18
|
if (dot <= 0) return (name.replace(/^\./, "") || "file").toUpperCase();
|
|
19
19
|
return name.slice(dot + 1).toUpperCase();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getMediaResourceTitle } from "./media.js";
|
|
1
2
|
//#region src/board/model/file-snapshot.ts
|
|
2
3
|
/**
|
|
3
4
|
* File-card snapshot derivation — pure, renderer-agnostic, dependency-free.
|
|
@@ -134,7 +135,7 @@ const MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
134
135
|
]);
|
|
135
136
|
const IMAGE_REF_RE = /\.(png|jpe?g|gif|webp|avif|svg|bmp|ico)(\?|#|$)/i;
|
|
136
137
|
function extensionOf(path) {
|
|
137
|
-
const name = path
|
|
138
|
+
const name = fileBaseName(path);
|
|
138
139
|
const dot = name.lastIndexOf(".");
|
|
139
140
|
if (dot <= 0) return "";
|
|
140
141
|
return name.slice(dot + 1).toLowerCase();
|
|
@@ -162,7 +163,7 @@ function fileCategory(path, mimeType) {
|
|
|
162
163
|
}
|
|
163
164
|
/** Basename of a path, used as a last-resort card title. */
|
|
164
165
|
function fileBaseName(path) {
|
|
165
|
-
return path
|
|
166
|
+
return getMediaResourceTitle(path);
|
|
166
167
|
}
|
|
167
168
|
/** Basename with a trailing extension stripped. Dotfiles keep their full name. */
|
|
168
169
|
function fileStem(path) {
|