@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.
Files changed (129) hide show
  1. package/README.md +11 -2
  2. package/dist/board/editor/animation.d.ts +3 -0
  3. package/dist/board/editor/animation.js +16 -1
  4. package/dist/board/editor/clipboard.js +7 -0
  5. package/dist/board/editor/copy.js +23 -18
  6. package/dist/board/editor/creation.d.ts +5 -1
  7. package/dist/board/editor/creation.js +2 -1
  8. package/dist/board/editor/deletion.js +3 -3
  9. package/dist/board/editor/document.d.ts +8 -2
  10. package/dist/board/editor/document.js +54 -7
  11. package/dist/board/editor/editor.d.ts +18 -3
  12. package/dist/board/editor/editor.js +11 -1
  13. package/dist/board/editor/query.d.ts +5 -5
  14. package/dist/board/editor/query.js +14 -12
  15. package/dist/board/editor/selection.js +2 -1
  16. package/dist/board/export/index.d.ts +25 -4
  17. package/dist/board/export/index.js +45 -12
  18. package/dist/board/export/node/index.d.ts +5 -4
  19. package/dist/board/export/node/index.js +16 -11
  20. package/dist/board/export/node/scripts.d.ts +16 -0
  21. package/dist/board/export/node/scripts.js +258 -0
  22. package/dist/board/export/scene.d.ts +2 -1
  23. package/dist/board/export/scene.js +20 -6
  24. package/dist/board/model/animation.d.ts +4 -1
  25. package/dist/board/model/animation.js +34 -7
  26. package/dist/board/model/authoring.d.ts +20 -0
  27. package/dist/board/model/authoring.js +185 -0
  28. package/dist/board/model/export-plan.js +1 -0
  29. package/dist/board/model/file-preview.js +1 -1
  30. package/dist/board/model/file-snapshot.js +3 -2
  31. package/dist/board/model/image-key.js +6 -2
  32. package/dist/board/model/index.d.ts +13 -6
  33. package/dist/board/model/index.js +14 -8
  34. package/dist/board/model/items/builtin.js +2 -2
  35. package/dist/board/model/items/create.d.ts +4 -2
  36. package/dist/board/model/items/create.js +6 -5
  37. package/dist/board/model/items/index.d.ts +2 -2
  38. package/dist/board/model/items/index.js +2 -2
  39. package/dist/board/model/media-playback.d.ts +2 -1
  40. package/dist/board/model/media-playback.js +14 -4
  41. package/dist/board/model/media.js +7 -1
  42. package/dist/board/model/sampling.d.ts +19 -0
  43. package/dist/board/model/sampling.js +96 -0
  44. package/dist/board/model/script-example.d.ts +4 -0
  45. package/dist/board/model/script-example.js +21 -0
  46. package/dist/board/model/script-hit.d.ts +10 -0
  47. package/dist/board/model/script-hit.js +52 -0
  48. package/dist/board/model/script-index.d.ts +7 -0
  49. package/dist/board/model/script-index.js +15 -0
  50. package/dist/board/model/script-scene.d.ts +24 -0
  51. package/dist/board/model/script-scene.js +267 -0
  52. package/dist/board/model/script.d.ts +105 -0
  53. package/dist/board/model/script.js +1 -0
  54. package/dist/board/render/css-color.d.ts +6 -1
  55. package/dist/board/render/css-color.js +34 -20
  56. package/dist/board/render/diagnostics.d.ts +20 -0
  57. package/dist/board/render/diagnostics.js +117 -0
  58. package/dist/board/render/index.d.ts +9 -6
  59. package/dist/board/render/index.js +9 -6
  60. package/dist/board/render/palette.d.ts +2 -1
  61. package/dist/board/render/palette.js +6 -2
  62. package/dist/board/render/renderers/arrow-card-renderer.js +3 -2
  63. package/dist/board/render/renderers/audio-card-renderer.js +2 -1
  64. package/dist/board/render/renderers/board-renderer-registry.d.ts +10 -5
  65. package/dist/board/render/renderers/board-renderer-registry.js +3 -3
  66. package/dist/board/render/renderers/custom-card-renderer.d.ts +5 -0
  67. package/dist/board/render/renderers/{sketch-card-renderer.js → custom-card-renderer.js} +16 -15
  68. package/dist/board/render/renderers/draw-card-renderer.js +3 -2
  69. package/dist/board/render/renderers/effect-card-renderer.js +22 -5
  70. package/dist/board/render/renderers/frame-card-renderer.js +30 -14
  71. package/dist/board/render/renderers/image-card-renderer.js +43 -4
  72. package/dist/board/render/renderers/shape-card-renderer.js +10 -27
  73. package/dist/board/render/renderers/text-card-renderer.js +5 -3
  74. package/dist/board/render/renderers/unknown-card-renderer.js +1 -0
  75. package/dist/board/render/renderers/video-card-renderer.js +2 -1
  76. package/dist/board/render/sampling.d.ts +22 -0
  77. package/dist/board/render/sampling.js +21 -0
  78. package/dist/board/render/script-export.d.ts +11 -0
  79. package/dist/board/render/script-export.js +86 -0
  80. package/dist/board/render/script-host.d.ts +31 -0
  81. package/dist/board/render/script-host.js +421 -0
  82. package/dist/board/render/script-modules.d.ts +2 -0
  83. package/dist/board/render/script-modules.js +2 -0
  84. package/dist/board/render/script-runtime.d.ts +29 -0
  85. package/dist/board/render/script-runtime.js +213 -0
  86. package/dist/board/render/script-sandbox.d.ts +4 -0
  87. package/dist/board/render/script-sandbox.js +173 -0
  88. package/dist/board/render/stroke.d.ts +2 -1
  89. package/dist/board/render/stroke.js +22 -1
  90. package/dist/board/render/text-measurement.js +1 -1
  91. package/dist/board/stage/assets.js +6 -1
  92. package/dist/board/stage/maintenance.d.ts +5 -0
  93. package/dist/board/stage/maintenance.js +11 -0
  94. package/dist/board/stage/scene.js +5 -2
  95. package/dist/board/stage/script-pointer.d.ts +19 -0
  96. package/dist/board/stage/script-pointer.js +35 -0
  97. package/dist/board/stage/stage.d.ts +10 -0
  98. package/dist/board/stage/stage.js +249 -43
  99. package/dist/board/stage/texture-pool.js +2 -3
  100. package/dist/chunks/environment.d.ts +266 -23
  101. package/dist/chunks/host-bridge.d.ts +2 -2
  102. package/dist/chunks/http.d.ts +88 -7
  103. package/dist/chunks/http.js +155 -63
  104. package/dist/chunks/transport.js +3 -1
  105. package/dist/chunks/websocket.d.ts +9 -1
  106. package/dist/chunks/websocket.js +16 -2
  107. package/dist/host-bridge.js +2 -1
  108. package/dist/http.d.ts +2 -2
  109. package/dist/index.d.ts +182 -4
  110. package/dist/index.js +1374 -126
  111. package/dist/protocol/dist/board-assets.d.ts +1 -0
  112. package/dist/protocol/dist/board-constants.js +2 -2
  113. package/dist/protocol/dist/board-model.d.ts +201 -11
  114. package/dist/protocol/dist/board-model.js +33 -14
  115. package/dist/protocol/dist/board-patch.js +2 -1
  116. package/dist/protocol/dist/board-schema.d.ts +4 -2
  117. package/dist/protocol/dist/board-schema.js +18 -4
  118. package/dist/protocol/dist/board-script-modules.d.ts +25 -0
  119. package/dist/protocol/dist/board-script-modules.js +127 -0
  120. package/dist/protocol/dist/display/index.d.ts +1 -0
  121. package/dist/protocol/dist/index.d.ts +4 -2
  122. package/dist/protocol/dist/realtime/types.d.ts +1 -0
  123. package/dist/protocol/dist/rtc/index.d.ts +1 -0
  124. package/package.json +2 -1
  125. package/dist/board/export/node/sketch.d.ts +0 -19
  126. package/dist/board/export/node/sketch.js +0 -146
  127. package/dist/board/render/renderers/sketch-card-renderer.d.ts +0 -5
  128. package/dist/board/render/sketch-host.d.ts +0 -14
  129. 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
- return decodeURIComponent(url.pathname.split("/").filter(Boolean).pop() || url.hostname);
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 };