@neta-art/cohub 10.0.0 → 11.0.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 +11 -2
- 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 +5 -1
- package/dist/board/editor/creation.js +2 -1
- 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 +18 -3
- package/dist/board/editor/editor.js +11 -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/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 +20 -6
- 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 +6 -2
- package/dist/board/model/index.d.ts +13 -6
- package/dist/board/model/index.js +14 -8
- package/dist/board/model/items/builtin.js +2 -2
- package/dist/board/model/items/create.d.ts +4 -2
- package/dist/board/model/items/create.js +6 -5
- 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 +14 -4
- package/dist/board/model/media.js +7 -1
- 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 +10 -0
- package/dist/board/model/script-hit.js +52 -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-scene.d.ts +24 -0
- package/dist/board/model/script-scene.js +267 -0
- package/dist/board/model/script.d.ts +105 -0
- package/dist/board/model/script.js +1 -0
- 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 +9 -6
- 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 +10 -5
- package/dist/board/render/renderers/board-renderer-registry.js +3 -3
- 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} +16 -15
- 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/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 +421 -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 +213 -0
- package/dist/board/render/script-sandbox.d.ts +4 -0
- package/dist/board/render/script-sandbox.js +173 -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 +6 -1
- package/dist/board/stage/maintenance.d.ts +5 -0
- package/dist/board/stage/maintenance.js +11 -0
- package/dist/board/stage/scene.js +5 -2
- package/dist/board/stage/script-pointer.d.ts +19 -0
- package/dist/board/stage/script-pointer.js +35 -0
- package/dist/board/stage/stage.d.ts +10 -0
- package/dist/board/stage/stage.js +249 -43
- package/dist/board/stage/texture-pool.js +2 -3
- package/dist/chunks/environment.d.ts +266 -23
- package/dist/chunks/host-bridge.d.ts +2 -2
- package/dist/chunks/http.d.ts +88 -7
- package/dist/chunks/http.js +155 -63
- package/dist/chunks/transport.js +3 -1
- 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 +2 -2
- package/dist/index.d.ts +182 -4
- package/dist/index.js +1374 -126
- 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 +201 -11
- package/dist/protocol/dist/board-model.js +33 -14
- 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/display/index.d.ts +1 -0
- package/dist/protocol/dist/index.d.ts +4 -2
- package/dist/protocol/dist/realtime/types.d.ts +1 -0
- package/dist/protocol/dist/rtc/index.d.ts +1 -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/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
|
@@ -59,9 +59,15 @@ function inferBoardMediaKind(value, mimeType) {
|
|
|
59
59
|
return "file";
|
|
60
60
|
}
|
|
61
61
|
function getMediaResourceTitle(value) {
|
|
62
|
+
if (!/^[a-z][a-z\d+.-]*:/i.test(value)) return value.split("/").filter(Boolean).pop() || value;
|
|
62
63
|
try {
|
|
63
64
|
const url = new URL(value);
|
|
64
|
-
|
|
65
|
+
const title = url.pathname.split("/").filter(Boolean).pop() || url.hostname;
|
|
66
|
+
try {
|
|
67
|
+
return decodeURIComponent(title);
|
|
68
|
+
} catch {
|
|
69
|
+
return title;
|
|
70
|
+
}
|
|
65
71
|
} catch {
|
|
66
72
|
return value.split("/").filter(Boolean).pop() || value;
|
|
67
73
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BoardDocument } from "../../protocol/dist/board-model.js";
|
|
2
|
+
import { BoardFrame } from "../../protocol/dist/board-layout.js";
|
|
3
|
+
import "../../protocol/dist/index.js";
|
|
4
|
+
import { BoardViewport, Size } from "./geometry.js";
|
|
5
|
+
import { BoardScene } from "./scene.js";
|
|
6
|
+
import { BoardCameraOverride, BoardEvaluateOptions, BoardEvaluation } from "./animation.js";
|
|
7
|
+
//#region src/board/model/sampling.d.ts
|
|
8
|
+
declare function createBoardAnimationSampler(document: BoardDocument, animation: string | null, options?: BoardEvaluateOptions): {
|
|
9
|
+
frameAt(id: string, time: number): BoardFrame | undefined;
|
|
10
|
+
at(time: number): {
|
|
11
|
+
document: BoardDocument;
|
|
12
|
+
evaluation: BoardEvaluation;
|
|
13
|
+
scene: BoardScene;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
type BoardAnimationSampler = ReturnType<typeof createBoardAnimationSampler>;
|
|
17
|
+
declare function resolveBoardAnimationCamera(override: BoardCameraOverride, bounds: (id: string) => BoardFrame | undefined, surface: Size, time: number, base?: BoardViewport): BoardViewport;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { BoardAnimationSampler, createBoardAnimationSampler, resolveBoardAnimationCamera };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { arrowBindings } from "../../protocol/dist/board-layout.js";
|
|
2
|
+
import { cameraForFocus, cameraForState } from "./geometry.js";
|
|
3
|
+
import { applyBoardEvaluation, boardAnimationTime, compileBoardAnimations, evaluateBoardAnimations } from "./animation.js";
|
|
4
|
+
import { buildBoardScene } from "./scene.js";
|
|
5
|
+
//#region src/board/model/sampling.ts
|
|
6
|
+
function createBoardAnimationSampler(document, animation, options = {}) {
|
|
7
|
+
const selected = animation ? document.animations[animation] : void 0;
|
|
8
|
+
if (animation && !selected) throw new Error(`Animation ${animation} does not exist.`);
|
|
9
|
+
const compiled = compileBoardAnimations(document);
|
|
10
|
+
const cache = /* @__PURE__ */ new Map();
|
|
11
|
+
const geometry = /* @__PURE__ */ new Map();
|
|
12
|
+
function sample(time) {
|
|
13
|
+
const times = /* @__PURE__ */ new Map();
|
|
14
|
+
for (const [id, entry] of compiled.animations) if (entry.header.play === "always") times.set(id, boardAnimationTime(entry.header, time) ?? 0);
|
|
15
|
+
if (animation && selected) times.set(animation, Math.min(time, selected.duration));
|
|
16
|
+
const evaluation = evaluateBoardAnimations(compiled, times, options);
|
|
17
|
+
return {
|
|
18
|
+
document: applyBoardEvaluation(document, evaluation),
|
|
19
|
+
evaluation
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function stateAt(time) {
|
|
23
|
+
if (!Number.isFinite(time) || time < 0) throw new Error("Animation time must be finite and non-negative.");
|
|
24
|
+
let value = cache.get(time);
|
|
25
|
+
if (!value) {
|
|
26
|
+
value = sample(time);
|
|
27
|
+
const oldest = cache.keys().next().value;
|
|
28
|
+
if (cache.size >= 64 && oldest !== void 0) cache.delete(oldest);
|
|
29
|
+
cache.set(time, value);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
let current;
|
|
34
|
+
return {
|
|
35
|
+
frameAt(id, time) {
|
|
36
|
+
const key = `${time}:${id}`;
|
|
37
|
+
if (geometry.has(key)) return geometry.get(key);
|
|
38
|
+
const sampled = stateAt(time).document;
|
|
39
|
+
const items = {};
|
|
40
|
+
const pending = [id];
|
|
41
|
+
while (pending.length) {
|
|
42
|
+
const next = pending.pop();
|
|
43
|
+
if (!next || items[next]) continue;
|
|
44
|
+
const item = sampled.items[next];
|
|
45
|
+
if (!item) continue;
|
|
46
|
+
items[next] = item;
|
|
47
|
+
if (item.parent) pending.push(item.parent);
|
|
48
|
+
pending.push(...arrowBindings(item));
|
|
49
|
+
}
|
|
50
|
+
const frame = buildBoardScene({
|
|
51
|
+
...sampled,
|
|
52
|
+
items
|
|
53
|
+
}).get(id)?.frame;
|
|
54
|
+
if (geometry.size >= 2048) geometry.clear();
|
|
55
|
+
geometry.set(key, frame);
|
|
56
|
+
return frame;
|
|
57
|
+
},
|
|
58
|
+
at(time) {
|
|
59
|
+
if (current?.time === time) return current.value;
|
|
60
|
+
const sampled = stateAt(time);
|
|
61
|
+
const value = {
|
|
62
|
+
...sampled,
|
|
63
|
+
scene: buildBoardScene(sampled.document)
|
|
64
|
+
};
|
|
65
|
+
current = {
|
|
66
|
+
time,
|
|
67
|
+
value
|
|
68
|
+
};
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function resolveBoardAnimationCamera(override, bounds, surface, time, base = {
|
|
74
|
+
x: 0,
|
|
75
|
+
y: 0,
|
|
76
|
+
zoom: 1
|
|
77
|
+
}) {
|
|
78
|
+
let camera = override.focus === void 0 ? base : cameraForFocus(override.focus, bounds, surface, { padding: 48 }) ?? base;
|
|
79
|
+
if (override.zoom !== void 0) camera = {
|
|
80
|
+
...camera,
|
|
81
|
+
zoom: override.zoom
|
|
82
|
+
};
|
|
83
|
+
if (override.center) camera = cameraForState({
|
|
84
|
+
centerX: override.center.x,
|
|
85
|
+
centerY: override.center.y,
|
|
86
|
+
zoom: camera.zoom
|
|
87
|
+
}, surface);
|
|
88
|
+
if (override.shake) camera = {
|
|
89
|
+
...camera,
|
|
90
|
+
x: camera.x + Math.sin(time / 1e3 * 91.3) * override.shake,
|
|
91
|
+
y: camera.y + Math.cos(time / 1e3 * 77.9) * override.shake
|
|
92
|
+
};
|
|
93
|
+
return camera;
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { createBoardAnimationSampler, resolveBoardAnimationCamera };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
//#region src/board/model/script-example.d.ts
|
|
2
|
+
declare const BOARD_SCRIPT_DRAWING_EXAMPLE = "export const animated = true;\n\nexport const parameters = {\n amplitude: { type: \"number\", label: \"Amplitude\", default: 40, min: 0, max: 120 }\n};\n\nexport function draw(ctx, { t, width, height, params }) {\n ctx.strokeStyle = \"#e67845\";\n ctx.lineWidth = 3;\n ctx.beginPath();\n for (let x = 0; x <= width; x += 2) {\n const y = height / 2 + Math.sin(x / 40 - t / 800) * params.amplitude;\n if (x === 0) ctx.moveTo(x, y);\n else ctx.lineTo(x, y);\n }\n ctx.stroke();\n}\n";
|
|
3
|
+
//#endregion
|
|
4
|
+
export { BOARD_SCRIPT_DRAWING_EXAMPLE };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/board/model/script-example.ts
|
|
2
|
+
const BOARD_SCRIPT_DRAWING_EXAMPLE = `export const animated = true;
|
|
3
|
+
|
|
4
|
+
export const parameters = {
|
|
5
|
+
amplitude: { type: "number", label: "Amplitude", default: 40, min: 0, max: 120 }
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function draw(ctx, { t, width, height, params }) {
|
|
9
|
+
ctx.strokeStyle = "#e67845";
|
|
10
|
+
ctx.lineWidth = 3;
|
|
11
|
+
ctx.beginPath();
|
|
12
|
+
for (let x = 0; x <= width; x += 2) {
|
|
13
|
+
const y = height / 2 + Math.sin(x / 40 - t / 800) * params.amplitude;
|
|
14
|
+
if (x === 0) ctx.moveTo(x, y);
|
|
15
|
+
else ctx.lineTo(x, y);
|
|
16
|
+
}
|
|
17
|
+
ctx.stroke();
|
|
18
|
+
}
|
|
19
|
+
`;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { BOARD_SCRIPT_DRAWING_EXAMPLE };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BoardScriptOutput } from "./script.js";
|
|
2
|
+
import { WorldPoint } from "./geometry.js";
|
|
3
|
+
import { BoardScene, BoardSceneItem } from "./scene.js";
|
|
4
|
+
//#region src/board/model/script-hit.d.ts
|
|
5
|
+
declare function boardScriptTarget(scene: BoardScene, candidates: readonly string[], point: WorldPoint, outputs: ReadonlyMap<string, BoardScriptOutput>, hitTest: (item: BoardSceneItem, point: WorldPoint) => boolean): {
|
|
6
|
+
owner: string;
|
|
7
|
+
target: string;
|
|
8
|
+
} | null;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { boardScriptTarget };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { applyMatrix, invertMatrix } from "../../protocol/dist/board-layout.js";
|
|
2
|
+
import { frameContainsPoint } from "./geometry.js";
|
|
3
|
+
import { scriptTemporaryPrefix } from "./script-scene.js";
|
|
4
|
+
//#region src/board/model/script-hit.ts
|
|
5
|
+
function boardScriptTarget(scene, candidates, point, outputs, hitTest) {
|
|
6
|
+
const owners = new Map([...outputs.keys()].map((id) => [scriptTemporaryPrefix(id), id]));
|
|
7
|
+
for (const id of [...candidates].sort((a, b) => scene.indexOf(b) - scene.indexOf(a))) {
|
|
8
|
+
const item = scene.get(id);
|
|
9
|
+
if (!item || scene.opacity(id) <= 0 || id.endsWith("root") && owners.has(id.slice(0, -4))) continue;
|
|
10
|
+
let parent = item.parent;
|
|
11
|
+
let clipped = false;
|
|
12
|
+
for (let depth = 0; parent && depth < 64; depth++) {
|
|
13
|
+
const ancestor = scene.get(parent);
|
|
14
|
+
if (!ancestor) break;
|
|
15
|
+
if (ancestor.type === "frame" && ancestor.props.clip && !frameContainsPoint(ancestor.frame, point)) {
|
|
16
|
+
clipped = true;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
parent = ancestor.parent;
|
|
20
|
+
}
|
|
21
|
+
if (clipped) continue;
|
|
22
|
+
const output = outputs.get(id);
|
|
23
|
+
if (output) {
|
|
24
|
+
const local = applyMatrix(invertMatrix(scene.layout.matrix(id)), point);
|
|
25
|
+
for (const [target, region] of Object.entries(output.hits).reverse()) if (local.x >= region.x && local.y >= region.y && local.x <= region.x + region.width && local.y <= region.y + region.height) return {
|
|
26
|
+
owner: id,
|
|
27
|
+
target
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (!hitTest(item, point)) continue;
|
|
31
|
+
let current = item;
|
|
32
|
+
for (let depth = 0; current && depth < 64; depth++) {
|
|
33
|
+
if (outputs.has(current.id)) return {
|
|
34
|
+
owner: current.id,
|
|
35
|
+
target: id
|
|
36
|
+
};
|
|
37
|
+
for (const [prefix, owner] of owners) {
|
|
38
|
+
if (current.id === `${prefix}root`) break;
|
|
39
|
+
if (current.id.startsWith(prefix)) return {
|
|
40
|
+
owner,
|
|
41
|
+
target: current.id.slice(prefix.length)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
current = current.parent ? scene.get(current.parent) : void 0;
|
|
45
|
+
}
|
|
46
|
+
if (item.type === "frame" && item.props.decoration === "none") continue;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { boardScriptTarget };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BoardDocument, BoardItem } from "../../protocol/dist/board-model.js";
|
|
2
|
+
import "../../protocol/dist/index.js";
|
|
3
|
+
//#region src/board/model/script-index.d.ts
|
|
4
|
+
declare function boardScriptItems(document: Pick<BoardDocument, "items">): readonly (readonly [string, BoardItem])[];
|
|
5
|
+
declare function hasBoardScripts(document: Pick<BoardDocument, "items">): boolean;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { boardScriptItems, hasBoardScripts };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/board/model/script-index.ts
|
|
2
|
+
const scriptsByItems = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
function boardScriptItems(document) {
|
|
4
|
+
let scripts = scriptsByItems.get(document.items);
|
|
5
|
+
if (!scripts) {
|
|
6
|
+
scripts = Object.entries(document.items).filter(([, item]) => item.script?.enabled);
|
|
7
|
+
scriptsByItems.set(document.items, scripts);
|
|
8
|
+
}
|
|
9
|
+
return scripts;
|
|
10
|
+
}
|
|
11
|
+
function hasBoardScripts(document) {
|
|
12
|
+
return boardScriptItems(document).length > 0;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { boardScriptItems, hasBoardScripts };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { BoardDocument, BoardItem, BoardPatch } from "../../protocol/dist/board-model.js";
|
|
2
|
+
import { BoardScriptOutput } from "./script.js";
|
|
3
|
+
import { BoardScene } from "./scene.js";
|
|
4
|
+
//#region src/board/model/script-scene.d.ts
|
|
5
|
+
declare function boardScriptScope(document: Pick<BoardDocument, "items">, id: string): Set<string>;
|
|
6
|
+
declare function validateBoardScriptOutput(output: BoardScriptOutput, scope: ReadonlySet<string>): void;
|
|
7
|
+
declare function validateBoardScriptCommit(document: BoardDocument, id: string, patch: BoardPatch): void;
|
|
8
|
+
declare function scriptTemporaryPrefix(owner: string): string;
|
|
9
|
+
type PreparedOutput = {
|
|
10
|
+
document: BoardDocument;
|
|
11
|
+
owner: string;
|
|
12
|
+
rootId: string;
|
|
13
|
+
items: Map<string, BoardItem>;
|
|
14
|
+
presented: Map<string, {
|
|
15
|
+
base: BoardItem;
|
|
16
|
+
item: BoardItem;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
19
|
+
declare function prepareBoardScriptOutput(document: BoardDocument, owner: string, output: BoardScriptOutput): PreparedOutput;
|
|
20
|
+
declare function composeBoardScriptItems(document: BoardDocument, scene: BoardScene, outputs: ReadonlyMap<string, BoardScriptOutput>, incoming?: ReadonlyMap<string, BoardItem>, screen?: {
|
|
21
|
+
zoom: number;
|
|
22
|
+
}, onError?: (owner: string, error: unknown) => void): Map<string, BoardItem>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { boardScriptScope, composeBoardScriptItems, prepareBoardScriptOutput, scriptTemporaryPrefix, validateBoardScriptCommit, validateBoardScriptOutput };
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { parseBoardItem as parseBoardItem$1 } from "../../protocol/dist/board-model.js";
|
|
2
|
+
import { applyMatrix, createBoardLayout } from "../../protocol/dist/board-layout.js";
|
|
3
|
+
import { applyBoardPatchToDocument, mergePatch } from "../../protocol/dist/board-patch.js";
|
|
4
|
+
//#region src/board/model/script-scene.ts
|
|
5
|
+
function parseBoardItem(value) {
|
|
6
|
+
const parsed = parseBoardItem$1(value);
|
|
7
|
+
if (!parsed.ok) throw new Error(parsed.diagnostics[0]?.message ?? "Invalid script item.");
|
|
8
|
+
return parsed.item;
|
|
9
|
+
}
|
|
10
|
+
const DISPLAY_FIELDS = /* @__PURE__ */ new Set([
|
|
11
|
+
"position",
|
|
12
|
+
"size",
|
|
13
|
+
"rotation",
|
|
14
|
+
"scale",
|
|
15
|
+
"opacity",
|
|
16
|
+
"style",
|
|
17
|
+
"props"
|
|
18
|
+
]);
|
|
19
|
+
function boardScriptScope(document, id) {
|
|
20
|
+
const scope = /* @__PURE__ */ new Set([id, ...Object.values(document.items[id]?.script?.targets ?? {}).filter((target) => Boolean(document.items[target]))]);
|
|
21
|
+
if (document.items[id]?.type !== "frame") return scope;
|
|
22
|
+
for (const candidate of Object.keys(document.items)) {
|
|
23
|
+
let parent = document.items[candidate]?.parent;
|
|
24
|
+
for (let depth = 0; parent && depth < 64; depth++) {
|
|
25
|
+
if (parent === id) {
|
|
26
|
+
scope.add(candidate);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
parent = document.items[parent]?.parent;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return scope;
|
|
33
|
+
}
|
|
34
|
+
function validateBoardScriptOutput(output, scope) {
|
|
35
|
+
if (!output || typeof output !== "object" || !output.patches || !output.items || !output.hits || !Array.isArray(output.listens)) throw new Error("Invalid script output.");
|
|
36
|
+
if (JSON.stringify(output).length > 1e6) throw new Error("Script output exceeds the 1 MB limit.");
|
|
37
|
+
if (output.space !== void 0 && output.space !== "world" && output.space !== "screen") throw new Error("Invalid script scene space.");
|
|
38
|
+
if (output.parameters) {
|
|
39
|
+
if (Object.keys(output.parameters).length > 64) throw new Error("A script may declare at most 64 parameters.");
|
|
40
|
+
for (const spec of Object.values(output.parameters)) {
|
|
41
|
+
if (!spec || ![
|
|
42
|
+
"number",
|
|
43
|
+
"boolean",
|
|
44
|
+
"string",
|
|
45
|
+
"color"
|
|
46
|
+
].includes(spec.type)) throw new Error("Invalid script parameter type.");
|
|
47
|
+
if (spec.label !== void 0 && typeof spec.label !== "string") throw new Error("Invalid script parameter label.");
|
|
48
|
+
if ([
|
|
49
|
+
spec.min,
|
|
50
|
+
spec.max,
|
|
51
|
+
spec.step
|
|
52
|
+
].some((value) => value !== void 0 && !Number.isFinite(value))) throw new Error("Invalid script parameter range.");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (Object.keys(output.items).length > 1e3 || Object.keys(output.hits).length > 1e3) throw new Error("A script may create at most 1000 temporary items or hit regions.");
|
|
56
|
+
for (const [id, patch] of Object.entries(output.patches)) {
|
|
57
|
+
if (!scope.has(id)) throw new Error(`Item is outside the script scope: ${id}`);
|
|
58
|
+
if (!patch || typeof patch !== "object" || Object.keys(patch).some((key) => !DISPLAY_FIELDS.has(key))) throw new Error("Scripts may only present display properties.");
|
|
59
|
+
}
|
|
60
|
+
for (const hit of Object.values(output.hits)) {
|
|
61
|
+
if (hit.cursor && ![
|
|
62
|
+
"default",
|
|
63
|
+
"pointer",
|
|
64
|
+
"grab",
|
|
65
|
+
"grabbing",
|
|
66
|
+
"crosshair",
|
|
67
|
+
"text",
|
|
68
|
+
"move",
|
|
69
|
+
"ew-resize",
|
|
70
|
+
"ns-resize",
|
|
71
|
+
"not-allowed"
|
|
72
|
+
].includes(hit.cursor)) throw new Error("Invalid script cursor.");
|
|
73
|
+
if (![
|
|
74
|
+
hit.x,
|
|
75
|
+
hit.y,
|
|
76
|
+
hit.width,
|
|
77
|
+
hit.height
|
|
78
|
+
].every(Number.isFinite) || hit.width < 0 || hit.height < 0) throw new Error("Invalid script hit region.");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function validateBoardScriptCommit(document, id, patch) {
|
|
82
|
+
if (!patch || typeof patch !== "object" || Object.keys(patch).some((key) => key !== "items")) throw new Error("Scripts may only commit items in their scope.");
|
|
83
|
+
const scope = boardScriptScope(document, id);
|
|
84
|
+
for (const [target, value] of Object.entries(patch.items ?? {})) {
|
|
85
|
+
if (!scope.has(target)) {
|
|
86
|
+
if (document.items[target]) throw new Error(`Item is outside the script scope: ${target}`);
|
|
87
|
+
if (!value || typeof value !== "object" || !("parent" in value) || typeof value.parent !== "string" || !scope.has(value.parent) || document.items[value.parent]?.type !== "frame") throw new Error("New items must belong to a frame in the script scope.");
|
|
88
|
+
if ("script" in value) throw new Error("Scripts cannot start other scripts.");
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (value && typeof value === "object" && ("script" in value || "parent" in value || "type" in value)) throw new Error("A script cannot replace scripts, types, or parent relationships.");
|
|
92
|
+
}
|
|
93
|
+
const result = applyBoardPatchToDocument(document, patch);
|
|
94
|
+
if (!result.ok) throw new Error(result.diagnostics[0]?.message ?? "Invalid script commit.");
|
|
95
|
+
}
|
|
96
|
+
function scriptTemporaryPrefix(owner) {
|
|
97
|
+
let hash = 2166136261;
|
|
98
|
+
for (const char of owner) hash = Math.imul(hash ^ char.charCodeAt(0), 16777619) >>> 0;
|
|
99
|
+
return `~script-${hash.toString(36)}-`;
|
|
100
|
+
}
|
|
101
|
+
const preparedOutputs = /* @__PURE__ */ new WeakMap();
|
|
102
|
+
const preparedScopes = /* @__PURE__ */ new WeakMap();
|
|
103
|
+
function prepareBoardScriptOutput(document, owner, output) {
|
|
104
|
+
const previous = preparedOutputs.get(output);
|
|
105
|
+
if (previous?.document === document && previous.owner === owner) return previous;
|
|
106
|
+
let scopes = preparedScopes.get(document);
|
|
107
|
+
if (!scopes) {
|
|
108
|
+
scopes = /* @__PURE__ */ new Map();
|
|
109
|
+
preparedScopes.set(document, scopes);
|
|
110
|
+
}
|
|
111
|
+
let scope = scopes.get(owner);
|
|
112
|
+
if (!scope) {
|
|
113
|
+
scope = boardScriptScope(document, owner);
|
|
114
|
+
scopes.set(owner, scope);
|
|
115
|
+
}
|
|
116
|
+
validateBoardScriptOutput(output, scope);
|
|
117
|
+
const presented = /* @__PURE__ */ new Map();
|
|
118
|
+
for (const [id, patch] of Object.entries(output.patches)) {
|
|
119
|
+
const base = document.items[id];
|
|
120
|
+
if (base) presented.set(id, {
|
|
121
|
+
base,
|
|
122
|
+
item: parseBoardItem(mergePatch(base, patch))
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const prefix = scriptTemporaryPrefix(owner);
|
|
126
|
+
const rootId = `${prefix}root`;
|
|
127
|
+
if (document.items[rootId]) throw new Error("Temporary item ID conflicts with the document.");
|
|
128
|
+
const items = /* @__PURE__ */ new Map();
|
|
129
|
+
const parsed = new Map(Object.entries(output.items).map(([id, value]) => [id, parseBoardItem(value)]));
|
|
130
|
+
for (const [local, value] of parsed) {
|
|
131
|
+
if (!/^[a-zA-Z0-9_-]{1,80}$/.test(local) || local === "root") throw new Error("Temporary IDs must be 1–80 letters, digits, underscores or hyphens; root is reserved.");
|
|
132
|
+
if (value.script) throw new Error("Temporary items cannot start scripts.");
|
|
133
|
+
if (value.parent && !(value.parent in output.items)) throw new Error("Temporary parents must belong to the same instance.");
|
|
134
|
+
let cursor = value.parent;
|
|
135
|
+
const visited = /* @__PURE__ */ new Set([local]);
|
|
136
|
+
while (cursor) {
|
|
137
|
+
if (visited.has(cursor)) throw new Error("Temporary parents must not form a cycle.");
|
|
138
|
+
visited.add(cursor);
|
|
139
|
+
const parentItem = parsed.get(cursor);
|
|
140
|
+
if (!parentItem) throw new Error("Temporary parent is missing.");
|
|
141
|
+
if (parentItem.type !== "frame") throw new Error("Only frames can contain temporary items.");
|
|
142
|
+
cursor = parentItem.parent;
|
|
143
|
+
}
|
|
144
|
+
const id = `${prefix}${local}`;
|
|
145
|
+
if (document.items[id]) throw new Error("Temporary item ID conflicts with the document.");
|
|
146
|
+
const parent = value.parent ? `${prefix}${value.parent}` : rootId;
|
|
147
|
+
if (value.type === "arrow") {
|
|
148
|
+
const remap = (endpoint) => {
|
|
149
|
+
if (!("item" in endpoint)) return endpoint;
|
|
150
|
+
if (!(endpoint.item in output.items) && !scope.has(endpoint.item)) throw new Error("Arrow target is outside the script scope.");
|
|
151
|
+
return endpoint.item in output.items ? {
|
|
152
|
+
...endpoint,
|
|
153
|
+
item: `${prefix}${endpoint.item}`
|
|
154
|
+
} : endpoint;
|
|
155
|
+
};
|
|
156
|
+
items.set(id, {
|
|
157
|
+
...value,
|
|
158
|
+
parent,
|
|
159
|
+
props: {
|
|
160
|
+
...value.props,
|
|
161
|
+
start: remap(value.props.start),
|
|
162
|
+
end: remap(value.props.end)
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
} else items.set(id, {
|
|
166
|
+
...value,
|
|
167
|
+
parent
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const prepared = {
|
|
171
|
+
document,
|
|
172
|
+
owner,
|
|
173
|
+
rootId,
|
|
174
|
+
items,
|
|
175
|
+
presented
|
|
176
|
+
};
|
|
177
|
+
preparedOutputs.set(output, prepared);
|
|
178
|
+
return prepared;
|
|
179
|
+
}
|
|
180
|
+
function composeBoardScriptItems(document, scene, outputs, incoming = /* @__PURE__ */ new Map(), screen, onError) {
|
|
181
|
+
const result = new Map(incoming);
|
|
182
|
+
const screenAnchors = /* @__PURE__ */ new Map();
|
|
183
|
+
if (outputs.size === 0) return result;
|
|
184
|
+
for (const owner of Object.keys(document.items)) {
|
|
185
|
+
const output = outputs.get(owner);
|
|
186
|
+
if (!output) continue;
|
|
187
|
+
try {
|
|
188
|
+
const changes = /* @__PURE__ */ new Map();
|
|
189
|
+
const prepared = prepareBoardScriptOutput(document, owner, output);
|
|
190
|
+
for (const [id, patch] of Object.entries(output.patches)) {
|
|
191
|
+
const base = result.get(id) ?? document.items[id];
|
|
192
|
+
if (!base) continue;
|
|
193
|
+
let cached = prepared.presented.get(id);
|
|
194
|
+
if (cached?.base !== base) {
|
|
195
|
+
cached = {
|
|
196
|
+
base,
|
|
197
|
+
item: parseBoardItem(mergePatch(base, patch))
|
|
198
|
+
};
|
|
199
|
+
prepared.presented.set(id, cached);
|
|
200
|
+
}
|
|
201
|
+
changes.set(id, cached.item);
|
|
202
|
+
}
|
|
203
|
+
if (!prepared.items.size || output.space === "screen" && !screen) {
|
|
204
|
+
for (const [id, item] of changes) result.set(id, item);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const rootId = prepared.rootId;
|
|
208
|
+
const item = changes.get(owner) ?? result.get(owner) ?? document.items[owner];
|
|
209
|
+
if (!item) continue;
|
|
210
|
+
const box = scene.layout.box(owner);
|
|
211
|
+
const overlay = output.space === "screen" && screen ? {
|
|
212
|
+
parent: void 0,
|
|
213
|
+
position: {
|
|
214
|
+
x: 0,
|
|
215
|
+
y: 0
|
|
216
|
+
},
|
|
217
|
+
scale: 1 / screen.zoom,
|
|
218
|
+
rotation: 0,
|
|
219
|
+
origin: {
|
|
220
|
+
x: 0,
|
|
221
|
+
y: 0
|
|
222
|
+
}
|
|
223
|
+
} : {};
|
|
224
|
+
changes.set(rootId, parseBoardItem({
|
|
225
|
+
type: "frame",
|
|
226
|
+
parent: item.parent,
|
|
227
|
+
position: item.position,
|
|
228
|
+
rotation: item.rotation,
|
|
229
|
+
scale: item.scale,
|
|
230
|
+
origin: item.origin,
|
|
231
|
+
opacity: item.opacity,
|
|
232
|
+
size: {
|
|
233
|
+
width: Math.max(1, box.width),
|
|
234
|
+
height: Math.max(1, box.height)
|
|
235
|
+
},
|
|
236
|
+
z: (item.z ?? 0) + .001,
|
|
237
|
+
props: {
|
|
238
|
+
decoration: "none",
|
|
239
|
+
clip: false
|
|
240
|
+
},
|
|
241
|
+
...overlay
|
|
242
|
+
}));
|
|
243
|
+
for (const [id, value] of prepared.items) changes.set(id, value);
|
|
244
|
+
for (const [id, item] of changes) result.set(id, item);
|
|
245
|
+
if (output.space === "screen") screenAnchors.set(owner, rootId);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (!onError) throw error;
|
|
248
|
+
onError(owner, error);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (screenAnchors.size) {
|
|
252
|
+
const layout = createBoardLayout((id) => result.get(id) ?? document.items[id]);
|
|
253
|
+
for (const [owner, rootId] of screenAnchors) {
|
|
254
|
+
const root = result.get(rootId);
|
|
255
|
+
if (root) result.set(rootId, {
|
|
256
|
+
...root,
|
|
257
|
+
position: applyMatrix(layout.matrix(owner), {
|
|
258
|
+
x: 0,
|
|
259
|
+
y: 0
|
|
260
|
+
})
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
export { boardScriptScope, composeBoardScriptItems, prepareBoardScriptOutput, scriptTemporaryPrefix, validateBoardScriptCommit, validateBoardScriptOutput };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { BoardItem, BoardPatch, BoardScript, BoardVec2 } from "../../protocol/dist/board-model.js";
|
|
2
|
+
import "../../protocol/dist/index.js";
|
|
3
|
+
//#region src/board/model/script.d.ts
|
|
4
|
+
type BoardScriptEvent = {
|
|
5
|
+
type: string;
|
|
6
|
+
target?: string;
|
|
7
|
+
relatedTarget?: string;
|
|
8
|
+
relatedOwner?: string;
|
|
9
|
+
point?: BoardVec2;
|
|
10
|
+
pointerId?: number;
|
|
11
|
+
pointerType?: string;
|
|
12
|
+
buttons?: number;
|
|
13
|
+
key?: string;
|
|
14
|
+
shiftKey?: boolean;
|
|
15
|
+
altKey?: boolean;
|
|
16
|
+
ctrlKey?: boolean;
|
|
17
|
+
metaKey?: boolean;
|
|
18
|
+
data?: unknown;
|
|
19
|
+
};
|
|
20
|
+
type BoardScriptFrame = {
|
|
21
|
+
t: number;
|
|
22
|
+
dt: number;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
seed: string;
|
|
26
|
+
params: BoardScript["params"];
|
|
27
|
+
state: Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
type BoardScriptItemPatch = Partial<Pick<BoardItem, "position" | "scale" | "rotation" | "opacity" | "style">> & {
|
|
30
|
+
size?: {
|
|
31
|
+
width?: number;
|
|
32
|
+
height?: number;
|
|
33
|
+
};
|
|
34
|
+
props?: Record<string, unknown>;
|
|
35
|
+
};
|
|
36
|
+
type BoardScriptHit = {
|
|
37
|
+
x: number;
|
|
38
|
+
y: number;
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
label?: string;
|
|
42
|
+
cursor?: string;
|
|
43
|
+
};
|
|
44
|
+
type BoardScriptSnapshot = {
|
|
45
|
+
state: Record<string, unknown>;
|
|
46
|
+
time: number;
|
|
47
|
+
random: number;
|
|
48
|
+
configuration?: string;
|
|
49
|
+
effects?: Pick<BoardScriptOutput, "patches" | "items" | "hits" | "space">;
|
|
50
|
+
};
|
|
51
|
+
type BoardScriptParameter = {
|
|
52
|
+
type: "number" | "boolean" | "string" | "color";
|
|
53
|
+
label?: string;
|
|
54
|
+
default?: string | number | boolean;
|
|
55
|
+
min?: number;
|
|
56
|
+
max?: number;
|
|
57
|
+
step?: number;
|
|
58
|
+
};
|
|
59
|
+
type BoardScriptOutput = {
|
|
60
|
+
parameters?: Record<string, BoardScriptParameter>;
|
|
61
|
+
space?: "world" | "screen";
|
|
62
|
+
patches: Record<string, Record<string, unknown>>;
|
|
63
|
+
items: Record<string, unknown>;
|
|
64
|
+
hits: Record<string, BoardScriptHit>;
|
|
65
|
+
listens: string[];
|
|
66
|
+
animated: boolean;
|
|
67
|
+
updating?: boolean;
|
|
68
|
+
version: number;
|
|
69
|
+
};
|
|
70
|
+
type BoardScriptContext = {
|
|
71
|
+
readonly self: {
|
|
72
|
+
readonly id: string;
|
|
73
|
+
readonly base: BoardItem;
|
|
74
|
+
present(patch: BoardScriptItemPatch | null): void;
|
|
75
|
+
};
|
|
76
|
+
readonly params: BoardScript["params"];
|
|
77
|
+
readonly targets: Readonly<Record<string, string>>;
|
|
78
|
+
emit(target: string, type: string, data?: unknown): Promise<void>;
|
|
79
|
+
readonly state: Record<string, unknown>;
|
|
80
|
+
readonly frame: BoardScriptFrame;
|
|
81
|
+
random(): number;
|
|
82
|
+
on(type: string, handler: (event: BoardScriptEvent) => void): () => void;
|
|
83
|
+
onFrame(handler: (frame: BoardScriptFrame) => void): () => void;
|
|
84
|
+
onDispose(handler: () => void): void;
|
|
85
|
+
invalidate(): void;
|
|
86
|
+
get(id: string): BoardItem | undefined;
|
|
87
|
+
present(id: string, patch: BoardScriptItemPatch | null): void;
|
|
88
|
+
scene(items: Record<string, unknown>, options?: {
|
|
89
|
+
space?: "world" | "screen";
|
|
90
|
+
}): void;
|
|
91
|
+
hitRegions(hits: Record<string, BoardScriptHit>): void;
|
|
92
|
+
commit(patch: BoardPatch): Promise<void>;
|
|
93
|
+
capabilities: {
|
|
94
|
+
has(name: string): boolean;
|
|
95
|
+
call(name: string, input?: unknown): Promise<unknown>;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
type BoardScriptModule = {
|
|
99
|
+
parameters?: Record<string, BoardScriptParameter>;
|
|
100
|
+
default?: ((context: BoardScriptContext) => void) | ((context: BoardScriptContext) => () => void);
|
|
101
|
+
draw?: (context: CanvasRenderingContext2D, frame: BoardScriptFrame) => void;
|
|
102
|
+
animated?: boolean;
|
|
103
|
+
};
|
|
104
|
+
//#endregion
|
|
105
|
+
export { BoardScriptContext, BoardScriptEvent, BoardScriptFrame, BoardScriptHit, BoardScriptItemPatch, BoardScriptModule, BoardScriptOutput, BoardScriptParameter, BoardScriptSnapshot };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
//#region src/board/render/css-color.d.ts
|
|
2
|
+
type BoardCssPaint = {
|
|
3
|
+
color: number;
|
|
4
|
+
alpha: number;
|
|
5
|
+
};
|
|
6
|
+
declare function parseBoardCssPaint(value: string): BoardCssPaint | null;
|
|
2
7
|
declare function parseBoardCssColor(value: string): number | null;
|
|
3
8
|
//#endregion
|
|
4
|
-
export { parseBoardCssColor };
|
|
9
|
+
export { BoardCssPaint, parseBoardCssColor, parseBoardCssPaint };
|