@neta-art/cohub 11.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.
Files changed (71) hide show
  1. package/README.md +31 -5
  2. package/dist/board/editor/creation.d.ts +2 -2
  3. package/dist/board/editor/creation.js +3 -3
  4. package/dist/board/editor/editor.d.ts +4 -4
  5. package/dist/board/editor/editor.js +1 -1
  6. package/dist/board/editor/index.d.ts +2 -2
  7. package/dist/board/editor/interaction.d.ts +8 -2
  8. package/dist/board/editor/interaction.js +34 -1
  9. package/dist/board/editor/snapshots.d.ts +0 -2
  10. package/dist/board/editor/snapshots.js +4 -20
  11. package/dist/board/export/scene.js +4 -1
  12. package/dist/board/model/image-key.js +1 -1
  13. package/dist/board/model/index.d.ts +8 -7
  14. package/dist/board/model/index.js +6 -5
  15. package/dist/board/model/items/create.d.ts +16 -3
  16. package/dist/board/model/items/create.js +125 -5
  17. package/dist/board/model/items/index.d.ts +2 -2
  18. package/dist/board/model/items/index.js +2 -2
  19. package/dist/board/model/media-playback.js +1 -1
  20. package/dist/board/model/overlay-placement.d.ts +25 -0
  21. package/dist/board/model/overlay-placement.js +35 -0
  22. package/dist/board/model/replay.d.ts +1 -1
  23. package/dist/board/model/replay.js +1 -0
  24. package/dist/board/model/script-hit.d.ts +20 -3
  25. package/dist/board/model/script-hit.js +81 -24
  26. package/dist/board/model/script-observers.d.ts +18 -0
  27. package/dist/board/model/script-observers.js +92 -0
  28. package/dist/board/model/script-scene.d.ts +1 -1
  29. package/dist/board/model/script-scene.js +35 -8
  30. package/dist/board/model/script.d.ts +51 -7
  31. package/dist/board/render/board-background.js +5 -4
  32. package/dist/board/render/index.d.ts +2 -2
  33. package/dist/board/render/index.js +2 -2
  34. package/dist/board/render/media-interaction.js +1 -1
  35. package/dist/board/render/renderers/board-renderer-registry.d.ts +17 -2
  36. package/dist/board/render/renderers/board-renderer-registry.js +8 -1
  37. package/dist/board/render/renderers/custom-card-renderer.js +2 -1
  38. package/dist/board/render/renderers/task-card-renderer.d.ts +2 -1
  39. package/dist/board/render/renderers/task-card-renderer.js +220 -71
  40. package/dist/board/render/script-host.d.ts +1 -1
  41. package/dist/board/render/script-host.js +53 -28
  42. package/dist/board/render/script-runtime.js +13 -3
  43. package/dist/board/render/script-texture.d.ts +11 -0
  44. package/dist/board/render/script-texture.js +33 -0
  45. package/dist/board/stage/assets.js +1 -1
  46. package/dist/board/stage/scene.d.ts +2 -0
  47. package/dist/board/stage/scene.js +17 -1
  48. package/dist/board/stage/script-pointer.d.ts +1 -0
  49. package/dist/board/stage/script-pointer.js +8 -2
  50. package/dist/board/stage/stage.d.ts +11 -3
  51. package/dist/board/stage/stage.js +118 -21
  52. package/dist/chunks/environment.d.ts +40 -5
  53. package/dist/chunks/environment.js +12 -4
  54. package/dist/chunks/http.d.ts +3 -1
  55. package/dist/chunks/http.js +2 -1
  56. package/dist/chunks/transport.js +1 -1
  57. package/dist/chunks/websocket.d.ts +1 -1
  58. package/dist/http.d.ts +2 -2
  59. package/dist/index.d.ts +2 -2
  60. package/dist/index.js +151 -139
  61. package/dist/protocol/dist/board-model.d.ts +36 -1
  62. package/dist/protocol/dist/board-model.js +14 -4
  63. package/dist/protocol/dist/board-task.d.ts +22 -0
  64. package/dist/{board/model/task.js → protocol/dist/board-task.js} +34 -7
  65. package/dist/protocol/dist/generation/blocks.d.ts +8 -0
  66. package/dist/{generation-blocks.js → protocol/dist/generation/blocks.js} +14 -15
  67. package/dist/protocol/dist/index.d.ts +5 -2
  68. package/dist/protocol/dist/task/index.d.ts +4 -0
  69. package/package.json +1 -1
  70. package/dist/board/model/task.d.ts +0 -11
  71. package/dist/types.d.ts +0 -36
@@ -1,11 +1,11 @@
1
- import { parseBoardItem } from "../../../protocol/dist/board-model.js";
1
+ import { BoardMediaOriginSchema, parseBoardItem } from "../../../protocol/dist/board-model.js";
2
2
  import { pointsRect } from "../../../protocol/dist/board-layout.js";
3
+ import { featuredTaskArtifact } from "../../../protocol/dist/board-task.js";
3
4
  import { quantizeDrawCoordinate } from "../draw-input.js";
4
5
  import { getMediaResourceTitle, inferBoardMediaKind } from "../media.js";
5
6
  import { fileStem } from "../file-snapshot.js";
6
7
  import { filePreviewKind } from "../file-preview.js";
7
8
  import { DEFAULT_BOARD_TOOL_STYLES } from "../tool-styles.js";
8
- import { featuredTaskArtifact } from "../task.js";
9
9
  import { createBoardItemId } from "./id.js";
10
10
  //#region src/board/model/items/create.ts
11
11
  const DEFAULT_MEDIA_SIZE = {
@@ -122,14 +122,14 @@ function createMediaBoardItem(path, x, y, snapshot = {}, id = createBoardItemId(
122
122
  };
123
123
  }
124
124
  const createFileNodeForPath = createMediaBoardItem;
125
- function createTaskBoardItem(taskRunId, snapshot, x, y, metadata, id = createBoardItemId()) {
125
+ function createTaskBoardItem(taskRunId, snapshot, x, y, metadata, id = createBoardItemId(), aspect) {
126
126
  const artifact = featuredTaskArtifact(snapshot.artifacts);
127
127
  const visual = artifact?.type === "image" || artifact?.type === "video" ? artifact : null;
128
128
  return {
129
129
  id,
130
130
  item: item({
131
131
  type: "task",
132
- ...centered(x, y, visual ? mediaFrameSize(visual.naturalWidth, visual.naturalHeight, 480, DEFAULT_TASK_MEDIA_SIZE) : artifact?.type === "audio" ? DEFAULT_TASK_MEDIA_SIZE : DEFAULT_TASK_SIZE),
132
+ ...centered(x, y, visual ? mediaFrameSize(visual.naturalWidth, visual.naturalHeight, 480, DEFAULT_TASK_MEDIA_SIZE) : artifact?.type === "audio" ? DEFAULT_TASK_MEDIA_SIZE : expectedTaskSize(aspect)),
133
133
  ...metadata ? { metadata } : {},
134
134
  props: {
135
135
  taskRunId,
@@ -138,6 +138,15 @@ function createTaskBoardItem(taskRunId, snapshot, x, y, metadata, id = createBoa
138
138
  })
139
139
  };
140
140
  }
141
+ function expectedTaskSize(aspect) {
142
+ if (!aspect || !Number.isFinite(aspect) || aspect <= 0) return { ...DEFAULT_TASK_SIZE };
143
+ const width = DEFAULT_TASK_MEDIA_SIZE.width;
144
+ const height = Math.round(width / aspect);
145
+ return height > 0 && height <= width * 4 ? {
146
+ width,
147
+ height
148
+ } : { ...DEFAULT_TASK_SIZE };
149
+ }
141
150
  function createTextBoardItem(text, x, y, color = DEFAULT_BOARD_TOOL_STYLES.text.color, id = createBoardItemId()) {
142
151
  return {
143
152
  id,
@@ -268,5 +277,116 @@ function titleForBoardItem(value) {
268
277
  default: return value.type;
269
278
  }
270
279
  }
280
+ const RESULT_GAP = 24;
281
+ const RESULT_COLUMNS = 4;
282
+ const RESULT_TEXT_WIDTH = 320;
283
+ const RESULT_MIN_HEIGHT = 120;
284
+ const RESULT_MAX_HEIGHT = 480;
285
+ function boardItemOrigin(item) {
286
+ if (item.type === "image" || item.type === "video" || item.type === "audio") return item.props.origin ?? null;
287
+ if (item.type !== "text") return null;
288
+ const parsed = BoardMediaOriginSchema.safeParse(item.metadata?.origin);
289
+ return parsed.success ? parsed.data : null;
290
+ }
291
+ const boardOriginKey = (origin) => `${origin.taskRunId}/${origin.artifactId}`;
292
+ function createTaskResultItems(input) {
293
+ const rowHeight = Math.min(RESULT_MAX_HEIGHT, Math.max(RESULT_MIN_HEIGHT, input.frame.height));
294
+ const left = input.frame.x + input.frame.width + 48;
295
+ const entries = [];
296
+ let column = 0;
297
+ let x = left;
298
+ let y = input.frame.y;
299
+ let tallest = 0;
300
+ for (const artifact of input.artifacts) {
301
+ if (input.existing?.has(boardOriginKey({
302
+ taskRunId: input.taskRunId,
303
+ artifactId: artifact.id
304
+ }))) continue;
305
+ if (column === RESULT_COLUMNS) {
306
+ column = 0;
307
+ x = left;
308
+ y += tallest + RESULT_GAP;
309
+ tallest = 0;
310
+ }
311
+ const entry = taskResultItem(input.taskRunId, artifact, x, y, rowHeight);
312
+ const size = "size" in entry.item && entry.item.size ? entry.item.size : {
313
+ width: RESULT_TEXT_WIDTH,
314
+ height: rowHeight
315
+ };
316
+ entries.push(entry);
317
+ x += size.width + RESULT_GAP;
318
+ tallest = Math.max(tallest, size.height);
319
+ column += 1;
320
+ }
321
+ return entries;
322
+ }
323
+ function taskResultItem(taskRunId, artifact, x, y, rowHeight) {
324
+ const id = createBoardItemId();
325
+ const origin = {
326
+ taskRunId,
327
+ artifactId: artifact.id
328
+ };
329
+ if (artifact.type === "text") return {
330
+ id,
331
+ item: item({
332
+ type: "text",
333
+ position: {
334
+ x,
335
+ y
336
+ },
337
+ metadata: { origin },
338
+ props: {
339
+ text: artifact.textExcerpt,
340
+ width: RESULT_TEXT_WIDTH
341
+ }
342
+ })
343
+ };
344
+ const snapshot = cleanSnapshot({
345
+ title: artifact.title,
346
+ mimeType: artifact.mimeType,
347
+ ...artifact.type !== "audio" ? {
348
+ naturalWidth: artifact.naturalWidth,
349
+ naturalHeight: artifact.naturalHeight
350
+ } : {},
351
+ ...artifact.type !== "image" ? { durationMs: artifact.durationMs } : {}
352
+ });
353
+ if (artifact.type === "audio") return {
354
+ id,
355
+ item: item({
356
+ type: "audio",
357
+ position: {
358
+ x,
359
+ y
360
+ },
361
+ size: DEFAULT_AUDIO_SIZE,
362
+ props: {
363
+ src: artifact.url,
364
+ snapshot,
365
+ origin
366
+ }
367
+ })
368
+ };
369
+ const fallback = artifact.type === "video" ? DEFAULT_VIDEO_SIZE : DEFAULT_MEDIA_SIZE;
370
+ const ratio = artifact.naturalWidth && artifact.naturalHeight ? artifact.naturalWidth / artifact.naturalHeight : fallback.width / fallback.height;
371
+ return {
372
+ id,
373
+ item: item({
374
+ type: artifact.type,
375
+ position: {
376
+ x,
377
+ y
378
+ },
379
+ size: {
380
+ width: Math.round(rowHeight * ratio),
381
+ height: rowHeight
382
+ },
383
+ props: {
384
+ src: artifact.url,
385
+ snapshot,
386
+ origin
387
+ }
388
+ })
389
+ };
390
+ }
271
391
  //#endregion
272
- export { createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTextBoardItem, mediaFrameSize, titleForBoardItem };
392
+ export { boardItemOrigin, boardOriginKey, createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, mediaFrameSize, titleForBoardItem };
@@ -1,6 +1,6 @@
1
1
  import { BoardItemCapabilities, BoardItemDefinition, BoardItemOf, BoardItemResizeMode, FULL_CAPABILITIES, defineBoardItem } from "./definition.js";
2
2
  import { builtinBoardItems } from "./builtin.js";
3
- import { BoardAppMetadata, BoardItemEntry, BoardMediaItemKind, createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTextBoardItem, mediaFrameSize, titleForBoardItem } from "./create.js";
3
+ import { BoardAppMetadata, BoardItemEntry, BoardMediaItemKind, boardItemOrigin, boardOriginKey, createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, mediaFrameSize, titleForBoardItem } from "./create.js";
4
4
  import { createBoardEntityId, createBoardItemId } from "./id.js";
5
5
  import { BoardItemInput, BoardRegistry, createBoardRegistry, defaultBoardRegistry } from "./registry.js";
6
- export { BoardAppMetadata, BoardItemCapabilities, BoardItemDefinition, BoardItemEntry, BoardItemInput, BoardItemOf, BoardItemResizeMode, BoardMediaItemKind, BoardRegistry, FULL_CAPABILITIES, builtinBoardItems, createAppBoardItem, createArrowBoardItem, createBoardEntityId, createBoardItemId, createBoardRegistry, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTextBoardItem, defaultBoardRegistry, defineBoardItem, mediaFrameSize, titleForBoardItem };
6
+ export { BoardAppMetadata, BoardItemCapabilities, BoardItemDefinition, BoardItemEntry, BoardItemInput, BoardItemOf, BoardItemResizeMode, BoardMediaItemKind, BoardRegistry, FULL_CAPABILITIES, boardItemOrigin, boardOriginKey, builtinBoardItems, createAppBoardItem, createArrowBoardItem, createBoardEntityId, createBoardItemId, createBoardRegistry, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, defaultBoardRegistry, defineBoardItem, mediaFrameSize, titleForBoardItem };
@@ -1,6 +1,6 @@
1
1
  import { FULL_CAPABILITIES, defineBoardItem } from "./definition.js";
2
2
  import { builtinBoardItems } from "./builtin.js";
3
3
  import { createBoardEntityId, createBoardItemId } from "./id.js";
4
- import { createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTextBoardItem, mediaFrameSize, titleForBoardItem } from "./create.js";
4
+ import { boardItemOrigin, boardOriginKey, createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, mediaFrameSize, titleForBoardItem } from "./create.js";
5
5
  import { createBoardRegistry, defaultBoardRegistry } from "./registry.js";
6
- export { FULL_CAPABILITIES, builtinBoardItems, createAppBoardItem, createArrowBoardItem, createBoardEntityId, createBoardItemId, createBoardRegistry, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTextBoardItem, defaultBoardRegistry, defineBoardItem, mediaFrameSize, titleForBoardItem };
6
+ export { FULL_CAPABILITIES, boardItemOrigin, boardOriginKey, builtinBoardItems, createAppBoardItem, createArrowBoardItem, createBoardEntityId, createBoardItemId, createBoardRegistry, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, defaultBoardRegistry, defineBoardItem, mediaFrameSize, titleForBoardItem };
@@ -1,6 +1,6 @@
1
1
  import { parseBoardMediaSource } from "../../protocol/dist/board-model.js";
2
+ import { rankedTaskArtifacts } from "../../protocol/dist/board-task.js";
2
3
  import { getMediaResourceTitle } from "./media.js";
3
- import { rankedTaskArtifacts } from "./task.js";
4
4
  //#region src/board/model/media-playback.ts
5
5
  function resolveBoardMediaUrl(src, source, purpose = "preview") {
6
6
  const parsed = parseBoardMediaSource(src);
@@ -0,0 +1,25 @@
1
+ import { Rect } from "./geometry.js";
2
+ //#region src/board/model/overlay-placement.d.ts
3
+ type BoardPlacement = "right" | "left" | "bottom" | "top";
4
+ declare function placeBoardOverlay(input: {
5
+ anchor: Rect;
6
+ size: {
7
+ width: number;
8
+ height: number;
9
+ };
10
+ safeRect: Rect;
11
+ placements?: readonly BoardPlacement[];
12
+ gap?: number;
13
+ }): {
14
+ placement: BoardPlacement;
15
+ position: {
16
+ x: number;
17
+ y: number;
18
+ };
19
+ availableSize: {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ };
24
+ //#endregion
25
+ export { BoardPlacement, placeBoardOverlay };
@@ -0,0 +1,35 @@
1
+ //#region src/board/model/overlay-placement.ts
2
+ function placeBoardOverlay(input) {
3
+ const { anchor: a, size, safeRect: safe, gap = 12 } = input;
4
+ const placements = input.placements?.length ? input.placements : [
5
+ "right",
6
+ "left",
7
+ "bottom",
8
+ "top"
9
+ ];
10
+ const width = Math.min(size.width, safe.width);
11
+ const height = Math.min(size.height, safe.height);
12
+ const best = placements.map((placement) => {
13
+ const x = placement === "right" ? a.x + a.width + gap : placement === "left" ? a.x - width - gap : a.x + (a.width - width) / 2;
14
+ const y = placement === "bottom" ? a.y + a.height + gap : placement === "top" ? a.y - height - gap : a.y + (a.height - height) / 2;
15
+ return {
16
+ placement,
17
+ x,
18
+ y,
19
+ overflow: Math.max(0, safe.x - x) + Math.max(0, x + width - safe.x - safe.width) + Math.max(0, safe.y - y) + Math.max(0, y + height - safe.y - safe.height)
20
+ };
21
+ }).reduce((best, next) => next.overflow < best.overflow ? next : best);
22
+ return {
23
+ placement: best.placement,
24
+ position: {
25
+ x: Math.max(safe.x, Math.min(best.x, safe.x + safe.width - width)),
26
+ y: Math.max(safe.y, Math.min(best.y, safe.y + safe.height - height))
27
+ },
28
+ availableSize: {
29
+ width,
30
+ height
31
+ }
32
+ };
33
+ }
34
+ //#endregion
35
+ export { placeBoardOverlay };
@@ -3,7 +3,7 @@ import { RequestSource } from "../../protocol/dist/provenance.js";
3
3
  import { BoardHistoryPage } from "../../protocol/dist/board.js";
4
4
  import "../../protocol/dist/index.js";
5
5
  //#region src/board/model/replay.d.ts
6
- type BoardReplayActorKind = "human" | "cli" | "agent";
6
+ type BoardReplayActorKind = "human" | "cli" | "agent" | "task";
7
7
  type BoardReplayEntry = {
8
8
  version: number;
9
9
  actorId: string;
@@ -3,6 +3,7 @@ import { applyBoardDelta } from "../../protocol/dist/board-patch.js";
3
3
  function boardReplayActorKind(source) {
4
4
  if (!source) return "human";
5
5
  if (source.toolCallId) return "agent";
6
+ if (source.via === "task") return "task";
6
7
  return source.via === "cli" ? "cli" : "human";
7
8
  }
8
9
  function entryOf(transaction) {
@@ -1,10 +1,27 @@
1
- import { BoardScriptOutput } from "./script.js";
2
- import { WorldPoint } from "./geometry.js";
1
+ import { BoardViewport, WorldPoint } from "./geometry.js";
2
+ import { BoardScriptHit, BoardScriptOutput } from "./script.js";
3
3
  import { BoardScene, BoardSceneItem } from "./scene.js";
4
4
  //#region src/board/model/script-hit.d.ts
5
+ declare function resolveBoardScriptHit(scene: BoardScene, owner: string, hit: BoardScriptHit): {
6
+ id: string;
7
+ item: BoardSceneItem;
8
+ rect: {
9
+ x: number;
10
+ y: number;
11
+ width: number;
12
+ height: number;
13
+ } & {
14
+ label?: string;
15
+ cursor?: string;
16
+ };
17
+ } | null;
18
+ declare function boardScriptHitVisible(scene: BoardScene, hit: NonNullable<ReturnType<typeof resolveBoardScriptHit>>, camera: BoardViewport, surface: {
19
+ width: number;
20
+ height: number;
21
+ }): boolean;
5
22
  declare function boardScriptTarget(scene: BoardScene, candidates: readonly string[], point: WorldPoint, outputs: ReadonlyMap<string, BoardScriptOutput>, hitTest: (item: BoardSceneItem, point: WorldPoint) => boolean): {
6
23
  owner: string;
7
24
  target: string;
8
25
  } | null;
9
26
  //#endregion
10
- export { boardScriptTarget };
27
+ export { boardScriptHitVisible, boardScriptTarget, resolveBoardScriptHit };
@@ -2,11 +2,75 @@ import { applyMatrix, invertMatrix } from "../../protocol/dist/board-layout.js";
2
2
  import { frameContainsPoint } from "./geometry.js";
3
3
  import { scriptTemporaryPrefix } from "./script-scene.js";
4
4
  //#region src/board/model/script-hit.ts
5
+ function resolveBoardScriptHit(scene, owner, hit) {
6
+ const id = "item" in hit ? `${scriptTemporaryPrefix(owner)}${hit.item}` : owner;
7
+ const item = scene.get(id);
8
+ if (!item || scene.opacity(id) <= 0) return null;
9
+ const box = scene.layout.box(id);
10
+ return {
11
+ id,
12
+ item,
13
+ rect: "x" in hit ? hit : {
14
+ x: 0,
15
+ y: 0,
16
+ width: box.width,
17
+ height: box.height
18
+ }
19
+ };
20
+ }
21
+ function boardScriptHitVisible(scene, hit, camera, surface) {
22
+ const { x, y, width, height } = hit.rect;
23
+ if (width <= 0 || height <= 0) return false;
24
+ const matrix = scene.layout.matrix(hit.id);
25
+ const corners = [
26
+ applyMatrix(matrix, {
27
+ x,
28
+ y
29
+ }),
30
+ applyMatrix(matrix, {
31
+ x: x + width,
32
+ y
33
+ }),
34
+ applyMatrix(matrix, {
35
+ x,
36
+ y: y + height
37
+ }),
38
+ applyMatrix(matrix, {
39
+ x: x + width,
40
+ y: y + height
41
+ })
42
+ ];
43
+ const xs = corners.map((point) => point.x * camera.zoom + camera.x);
44
+ const ys = corners.map((point) => point.y * camera.zoom + camera.y);
45
+ return Math.max(...xs) > 0 && Math.min(...xs) < surface.width && Math.max(...ys) > 0 && Math.min(...ys) < surface.height;
46
+ }
47
+ const bindings = /* @__PURE__ */ new WeakMap();
48
+ function bindingsFor(owner, output) {
49
+ const cached = bindings.get(output);
50
+ if (cached?.owner === owner) return cached.byItem;
51
+ const byItem = /* @__PURE__ */ new Map();
52
+ for (const [target, hit] of Object.entries(output.hits)) {
53
+ const id = "item" in hit ? `${scriptTemporaryPrefix(owner)}${hit.item}` : owner;
54
+ const list = byItem.get(id) ?? [];
55
+ list.push({
56
+ owner,
57
+ target,
58
+ hit
59
+ });
60
+ byItem.set(id, list);
61
+ }
62
+ bindings.set(output, {
63
+ owner,
64
+ byItem
65
+ });
66
+ return byItem;
67
+ }
5
68
  function boardScriptTarget(scene, candidates, point, outputs, hitTest) {
6
- const owners = new Map([...outputs.keys()].map((id) => [scriptTemporaryPrefix(id), id]));
69
+ const declared = [...outputs].map(([owner, output]) => bindingsFor(owner, output));
70
+ const prefixes = [...outputs.keys()].map(scriptTemporaryPrefix);
7
71
  for (const id of [...candidates].sort((a, b) => scene.indexOf(b) - scene.indexOf(a))) {
8
72
  const item = scene.get(id);
9
- if (!item || scene.opacity(id) <= 0 || id.endsWith("root") && owners.has(id.slice(0, -4))) continue;
73
+ if (!item || scene.opacity(id) <= 0) continue;
10
74
  let parent = item.parent;
11
75
  let clipped = false;
12
76
  for (let depth = 0; parent && depth < 64; depth++) {
@@ -19,34 +83,27 @@ function boardScriptTarget(scene, candidates, point, outputs, hitTest) {
19
83
  parent = ancestor.parent;
20
84
  }
21
85
  if (clipped) continue;
22
- const output = outputs.get(id);
23
- if (output) {
86
+ for (const byItem of declared) {
87
+ const hits = byItem.get(id);
88
+ if (!hits) continue;
24
89
  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)
90
+ for (let i = hits.length - 1; i >= 0; i--) {
91
+ const binding = hits[i];
92
+ if (!binding) continue;
93
+ const resolved = resolveBoardScriptHit(scene, binding.owner, binding.hit);
94
+ if (!resolved) continue;
95
+ const r = resolved.rect;
96
+ if (local.x >= r.x && local.y >= r.y && local.x <= r.x + r.width && local.y <= r.y + r.height && ("x" in binding.hit || hitTest(item, point))) return {
97
+ owner: binding.owner,
98
+ target: binding.target
42
99
  };
43
100
  }
44
- current = current.parent ? scene.get(current.parent) : void 0;
45
101
  }
102
+ if (prefixes.some((prefix) => id.startsWith(prefix))) continue;
46
103
  if (item.type === "frame" && item.props.decoration === "none") continue;
47
- return null;
104
+ if (hitTest(item, point)) return null;
48
105
  }
49
106
  return null;
50
107
  }
51
108
  //#endregion
52
- export { boardScriptTarget };
109
+ export { boardScriptHitVisible, boardScriptTarget, resolveBoardScriptHit };
@@ -0,0 +1,18 @@
1
+ import { BoardDocument } from "../../protocol/dist/board-model.js";
2
+ import "../../protocol/dist/index.js";
3
+ import { BoardScriptEvent, BoardScriptOutput, BoardScriptView } from "./script.js";
4
+ //#region src/board/model/script-observers.d.ts
5
+ declare function createBoardScriptObservers(send: (owner: string, event: BoardScriptEvent) => void): {
6
+ readonly size: number;
7
+ readonly transforms: boolean;
8
+ update(owner: string, output: BoardScriptOutput | null): void;
9
+ sync(document: BoardDocument, selection: readonly string[], readView: () => BoardScriptView): void;
10
+ transform(document: BoardDocument, event: {
11
+ ids: readonly string[];
12
+ operation: string;
13
+ cancelled: boolean;
14
+ }): void;
15
+ clear(): void;
16
+ };
17
+ //#endregion
18
+ export { createBoardScriptObservers };
@@ -0,0 +1,92 @@
1
+ import { boardScriptScope } from "./script-scene.js";
2
+ //#region src/board/model/script-observers.ts
3
+ const EVENTS = [
4
+ "viewchange",
5
+ "selectionchange",
6
+ "transformend"
7
+ ];
8
+ function createBoardScriptObservers(send) {
9
+ const subscribers = /* @__PURE__ */ new Map();
10
+ function scope(owner, entry, document) {
11
+ if (entry.document !== document) {
12
+ entry.scope = boardScriptScope(document, owner);
13
+ entry.document = document;
14
+ entry.selection = void 0;
15
+ }
16
+ return entry.scope;
17
+ }
18
+ return {
19
+ get size() {
20
+ return subscribers.size;
21
+ },
22
+ get transforms() {
23
+ return [...subscribers.values()].some((entry) => entry.listens.has("transformend"));
24
+ },
25
+ update(owner, output) {
26
+ const listens = new Set(EVENTS.filter((type) => output?.listens.includes(type)));
27
+ if (!listens.size) {
28
+ subscribers.delete(owner);
29
+ return;
30
+ }
31
+ const entry = subscribers.get(owner) ?? {
32
+ listens,
33
+ scope: /* @__PURE__ */ new Set()
34
+ };
35
+ if (!listens.has("viewchange")) entry.view = void 0;
36
+ if (!listens.has("selectionchange")) {
37
+ entry.selection = void 0;
38
+ entry.selected = void 0;
39
+ }
40
+ entry.listens = listens;
41
+ subscribers.set(owner, entry);
42
+ },
43
+ sync(document, selection, readView) {
44
+ let view;
45
+ let key;
46
+ for (const [owner, entry] of subscribers) {
47
+ if (entry.listens.has("viewchange")) {
48
+ view ??= readView();
49
+ key ??= JSON.stringify(view);
50
+ if (entry.view !== key) {
51
+ entry.view = key;
52
+ send(owner, {
53
+ type: "viewchange",
54
+ data: view
55
+ });
56
+ }
57
+ }
58
+ if (entry.listens.has("selectionchange")) {
59
+ const allowed = scope(owner, entry, document);
60
+ if (entry.selection === selection) continue;
61
+ entry.selection = selection;
62
+ const ids = selection.filter((id) => allowed.has(id));
63
+ if (entry.selected && ids.length === entry.selected.length && ids.every((id, i) => id === entry.selected?.[i])) continue;
64
+ entry.selected = ids;
65
+ send(owner, {
66
+ type: "selectionchange",
67
+ data: { ids }
68
+ });
69
+ }
70
+ }
71
+ },
72
+ transform(document, event) {
73
+ for (const [owner, entry] of subscribers) {
74
+ if (!entry.listens.has("transformend")) continue;
75
+ const allowed = scope(owner, entry, document);
76
+ const ids = event.ids.filter((id) => allowed.has(id));
77
+ if (ids.length) send(owner, {
78
+ type: "transformend",
79
+ data: {
80
+ ...event,
81
+ ids
82
+ }
83
+ });
84
+ }
85
+ },
86
+ clear() {
87
+ subscribers.clear();
88
+ }
89
+ };
90
+ }
91
+ //#endregion
92
+ export { createBoardScriptObservers };
@@ -19,6 +19,6 @@ type PreparedOutput = {
19
19
  declare function prepareBoardScriptOutput(document: BoardDocument, owner: string, output: BoardScriptOutput): PreparedOutput;
20
20
  declare function composeBoardScriptItems(document: BoardDocument, scene: BoardScene, outputs: ReadonlyMap<string, BoardScriptOutput>, incoming?: ReadonlyMap<string, BoardItem>, screen?: {
21
21
  zoom: number;
22
- }, onError?: (owner: string, error: unknown) => void): Map<string, BoardItem>;
22
+ }, onError?: (owner: string, error: unknown) => void, gestureLayout?: BoardScene["layout"]): Map<string, BoardItem>;
23
23
  //#endregion
24
24
  export { boardScriptScope, composeBoardScriptItems, prepareBoardScriptOutput, scriptTemporaryPrefix, validateBoardScriptCommit, validateBoardScriptOutput };
@@ -35,6 +35,10 @@ function validateBoardScriptOutput(output, scope) {
35
35
  if (!output || typeof output !== "object" || !output.patches || !output.items || !output.hits || !Array.isArray(output.listens)) throw new Error("Invalid script output.");
36
36
  if (JSON.stringify(output).length > 1e6) throw new Error("Script output exceeds the 1 MB limit.");
37
37
  if (output.space !== void 0 && output.space !== "world" && output.space !== "screen") throw new Error("Invalid script scene space.");
38
+ if (output.anchor) {
39
+ const { item, point } = output.anchor;
40
+ if (output.space !== "screen" || typeof item !== "string" || !item || point && ![point.x, point.y].every((n) => Number.isFinite(n) && n >= 0 && n <= 1)) throw new Error("Invalid screen scene anchor.");
41
+ }
38
42
  if (output.parameters) {
39
43
  if (Object.keys(output.parameters).length > 64) throw new Error("A script may declare at most 64 parameters.");
40
44
  for (const spec of Object.values(output.parameters)) {
@@ -58,6 +62,7 @@ function validateBoardScriptOutput(output, scope) {
58
62
  if (!patch || typeof patch !== "object" || Object.keys(patch).some((key) => !DISPLAY_FIELDS.has(key))) throw new Error("Scripts may only present display properties.");
59
63
  }
60
64
  for (const hit of Object.values(output.hits)) {
65
+ if (!hit || typeof hit !== "object") throw new Error("Invalid script hit region.");
61
66
  if (hit.cursor && ![
62
67
  "default",
63
68
  "pointer",
@@ -70,7 +75,11 @@ function validateBoardScriptOutput(output, scope) {
70
75
  "ns-resize",
71
76
  "not-allowed"
72
77
  ].includes(hit.cursor)) throw new Error("Invalid script cursor.");
73
- if (![
78
+ if ("target" in hit) {
79
+ if (hit.target !== "self" || "item" in hit || "x" in hit) throw new Error("Invalid script hit target.");
80
+ } else if ("item" in hit) {
81
+ if (typeof hit.item !== "string" || !Object.hasOwn(output.items, hit.item) || "x" in hit) throw new Error("Hit items must belong to the temporary scene.");
82
+ } else if (![
74
83
  hit.x,
75
84
  hit.y,
76
85
  hit.width,
@@ -100,6 +109,15 @@ function scriptTemporaryPrefix(owner) {
100
109
  }
101
110
  const preparedOutputs = /* @__PURE__ */ new WeakMap();
102
111
  const preparedScopes = /* @__PURE__ */ new WeakMap();
112
+ const itemOrders = /* @__PURE__ */ new WeakMap();
113
+ function orderedScriptOwners(document, outputs) {
114
+ let order = itemOrders.get(document.items);
115
+ if (!order) {
116
+ order = new Map(Object.keys(document.items).map((id, index) => [id, index]));
117
+ itemOrders.set(document.items, order);
118
+ }
119
+ return [...outputs.keys()].filter((id) => order.has(id)).sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
120
+ }
103
121
  function prepareBoardScriptOutput(document, owner, output) {
104
122
  const previous = preparedOutputs.get(output);
105
123
  if (previous?.document === document && previous.owner === owner) return previous;
@@ -113,6 +131,7 @@ function prepareBoardScriptOutput(document, owner, output) {
113
131
  scope = boardScriptScope(document, owner);
114
132
  scopes.set(owner, scope);
115
133
  }
134
+ if (output.anchor && document.items[output.anchor.item] && !scope.has(output.anchor.item)) throw new Error("Scene anchor is outside the script scope.");
116
135
  validateBoardScriptOutput(output, scope);
117
136
  const presented = /* @__PURE__ */ new Map();
118
137
  for (const [id, patch] of Object.entries(output.patches)) {
@@ -177,11 +196,11 @@ function prepareBoardScriptOutput(document, owner, output) {
177
196
  preparedOutputs.set(output, prepared);
178
197
  return prepared;
179
198
  }
180
- function composeBoardScriptItems(document, scene, outputs, incoming = /* @__PURE__ */ new Map(), screen, onError) {
199
+ function composeBoardScriptItems(document, scene, outputs, incoming = /* @__PURE__ */ new Map(), screen, onError, gestureLayout) {
181
200
  const result = new Map(incoming);
182
201
  const screenAnchors = /* @__PURE__ */ new Map();
183
202
  if (outputs.size === 0) return result;
184
- for (const owner of Object.keys(document.items)) {
203
+ for (const owner of orderedScriptOwners(document, outputs)) {
185
204
  const output = outputs.get(owner);
186
205
  if (!output) continue;
187
206
  try {
@@ -249,15 +268,23 @@ function composeBoardScriptItems(document, scene, outputs, incoming = /* @__PURE
249
268
  }
250
269
  }
251
270
  if (screenAnchors.size) {
252
- const layout = createBoardLayout((id) => result.get(id) ?? document.items[id]);
271
+ const layout = gestureLayout ?? createBoardLayout((id) => result.get(id) ?? document.items[id]);
253
272
  for (const [owner, rootId] of screenAnchors) {
254
273
  const root = result.get(rootId);
274
+ const anchor = outputs.get(owner)?.anchor;
275
+ const target = anchor?.item ?? owner;
276
+ const exists = Boolean(document.items[target]);
277
+ const box = exists ? layout.box(target) : {
278
+ width: 0,
279
+ height: 0
280
+ };
255
281
  if (root) result.set(rootId, {
256
282
  ...root,
257
- position: applyMatrix(layout.matrix(owner), {
258
- x: 0,
259
- y: 0
260
- })
283
+ opacity: exists ? root.opacity : 0,
284
+ position: exists ? applyMatrix(layout.matrix(target), {
285
+ x: (anchor?.point?.x ?? 0) * box.width,
286
+ y: (anchor?.point?.y ?? 0) * box.height
287
+ }) : root.position
261
288
  });
262
289
  }
263
290
  }