@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.
- package/README.md +31 -5
- package/dist/board/editor/creation.d.ts +2 -2
- package/dist/board/editor/creation.js +3 -3
- package/dist/board/editor/editor.d.ts +4 -4
- package/dist/board/editor/editor.js +1 -1
- package/dist/board/editor/index.d.ts +2 -2
- package/dist/board/editor/interaction.d.ts +8 -2
- package/dist/board/editor/interaction.js +34 -1
- package/dist/board/editor/snapshots.d.ts +0 -2
- package/dist/board/editor/snapshots.js +4 -20
- package/dist/board/export/scene.js +4 -1
- package/dist/board/model/image-key.js +1 -1
- package/dist/board/model/index.d.ts +8 -7
- package/dist/board/model/index.js +6 -5
- package/dist/board/model/items/create.d.ts +16 -3
- package/dist/board/model/items/create.js +125 -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.js +1 -1
- package/dist/board/model/overlay-placement.d.ts +25 -0
- package/dist/board/model/overlay-placement.js +35 -0
- package/dist/board/model/replay.d.ts +1 -1
- package/dist/board/model/replay.js +1 -0
- package/dist/board/model/script-hit.d.ts +20 -3
- package/dist/board/model/script-hit.js +81 -24
- package/dist/board/model/script-observers.d.ts +18 -0
- package/dist/board/model/script-observers.js +92 -0
- package/dist/board/model/script-scene.d.ts +1 -1
- package/dist/board/model/script-scene.js +35 -8
- package/dist/board/model/script.d.ts +51 -7
- package/dist/board/render/board-background.js +5 -4
- package/dist/board/render/index.d.ts +2 -2
- package/dist/board/render/index.js +2 -2
- package/dist/board/render/media-interaction.js +1 -1
- package/dist/board/render/renderers/board-renderer-registry.d.ts +17 -2
- package/dist/board/render/renderers/board-renderer-registry.js +8 -1
- package/dist/board/render/renderers/custom-card-renderer.js +2 -1
- package/dist/board/render/renderers/task-card-renderer.d.ts +2 -1
- package/dist/board/render/renderers/task-card-renderer.js +220 -71
- package/dist/board/render/script-host.d.ts +1 -1
- package/dist/board/render/script-host.js +53 -28
- package/dist/board/render/script-runtime.js +13 -3
- package/dist/board/render/script-texture.d.ts +11 -0
- package/dist/board/render/script-texture.js +33 -0
- package/dist/board/stage/assets.js +1 -1
- package/dist/board/stage/scene.d.ts +2 -0
- package/dist/board/stage/scene.js +17 -1
- package/dist/board/stage/script-pointer.d.ts +1 -0
- package/dist/board/stage/script-pointer.js +8 -2
- package/dist/board/stage/stage.d.ts +11 -3
- package/dist/board/stage/stage.js +118 -21
- package/dist/chunks/environment.d.ts +40 -5
- package/dist/chunks/environment.js +12 -4
- package/dist/chunks/http.d.ts +3 -1
- package/dist/chunks/http.js +2 -1
- package/dist/chunks/transport.js +1 -1
- package/dist/chunks/websocket.d.ts +1 -1
- package/dist/http.d.ts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +151 -139
- package/dist/protocol/dist/board-model.d.ts +36 -1
- package/dist/protocol/dist/board-model.js +14 -4
- package/dist/protocol/dist/board-task.d.ts +22 -0
- package/dist/{board/model/task.js → protocol/dist/board-task.js} +34 -7
- package/dist/protocol/dist/generation/blocks.d.ts +8 -0
- package/dist/{generation-blocks.js → protocol/dist/generation/blocks.js} +14 -15
- package/dist/protocol/dist/index.d.ts +5 -2
- package/dist/protocol/dist/task/index.d.ts +4 -0
- package/package.json +1 -1
- package/dist/board/model/task.d.ts +0 -11
- 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 :
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
23
|
-
|
|
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 (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
}
|