@neta-art/cohub 5.7.0 → 5.8.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 +18 -5
- package/dist/board/core/palette.d.ts +3 -2
- package/dist/board/core/shape-types.d.ts +3 -1
- package/dist/board/core/shape-types.js +3 -7
- package/dist/board/core/tool-styles.d.ts +2 -1
- package/dist/board/image-key.js +3 -5
- package/dist/board/index.d.ts +9 -4
- package/dist/board/index.js +7 -3
- package/dist/board/media-playback.d.ts +22 -0
- package/dist/board/media-playback.js +67 -0
- package/dist/board/media.d.ts +7 -0
- package/dist/board/media.js +70 -0
- package/dist/board/nodes.d.ts +113 -0
- package/dist/board/nodes.js +154 -0
- package/dist/board/render/index.d.ts +3 -1
- package/dist/board/render/index.js +4 -2
- package/dist/board/render/media-interaction.d.ts +19 -0
- package/dist/board/render/media-interaction.js +26 -0
- package/dist/board/render/renderers/audio-card-renderer.js +1 -1
- package/dist/board/render/renderers/board-renderer-registry.js +2 -2
- package/dist/board/render/renderers/draw-card-renderer.js +1 -1
- package/dist/board/render/renderers/file-card-renderer.js +1 -1
- package/dist/board/render/renderers/frame-card-renderer.js +1 -1
- package/dist/board/render/renderers/geo-card-renderer.js +1 -1
- package/dist/board/render/renderers/image-card-renderer.js +1 -1
- package/dist/board/render/renderers/task-card-renderer.js +16 -13
- package/dist/board/render/renderers/text-card-renderer.js +1 -1
- package/dist/board/render/renderers/unknown-card-renderer.js +1 -1
- package/dist/board/render/renderers/video-card-renderer.js +1 -1
- package/dist/board/render/video-thumbnail.d.ts +16 -0
- package/dist/board/render/video-thumbnail.js +86 -0
- package/dist/board/task.d.ts +10 -5
- package/dist/board/task.js +159 -103
- package/dist/chunks/environment.d.ts +6 -6
- package/dist/chunks/environment.js +6 -6
- package/dist/chunks/http.d.ts +71 -5
- package/dist/chunks/http.js +1535 -167
- package/dist/chunks/transport.js +8 -1
- package/dist/chunks/websocket.d.ts +138 -2
- package/dist/http.d.ts +3 -3
- package/dist/index.d.ts +245 -4
- package/dist/index.js +438 -788
- package/dist/protocol/dist/board-document.d.ts +155 -48
- package/dist/protocol/dist/board-document.js +40 -19
- package/dist/protocol/dist/board-node.d.ts +18 -0
- package/dist/protocol/dist/board-node.js +239 -0
- package/dist/protocol/dist/board-url.d.ts +12 -0
- package/dist/protocol/dist/board-url.js +83 -0
- package/dist/protocol/dist/board.d.ts +5 -0
- package/dist/protocol/dist/index.d.ts +2 -1
- package/dist/protocol/dist/index.js +2 -1
- package/dist/protocol/dist/provenance.js +1 -0
- package/docs/work-runtime-guide.md +7 -7
- package/package.json +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import "../protocol/dist/board-constants.js";
|
|
2
|
+
import { validateBoardNodeInput } from "../protocol/dist/board-node.js";
|
|
3
|
+
import "../protocol/dist/index.js";
|
|
4
|
+
import { computeDrawBounds } from "./core/draw-geometry.js";
|
|
5
|
+
//#region src/board/nodes.ts
|
|
6
|
+
var BoardInputError = class extends Error {
|
|
7
|
+
code = "INVALID_BOARD_NODE";
|
|
8
|
+
diagnostics;
|
|
9
|
+
body;
|
|
10
|
+
constructor(diagnostics) {
|
|
11
|
+
const message = diagnostics[0]?.message ?? "Invalid Board node";
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "BoardInputError";
|
|
14
|
+
this.diagnostics = diagnostics;
|
|
15
|
+
this.body = {
|
|
16
|
+
code: this.code,
|
|
17
|
+
message,
|
|
18
|
+
diagnostics
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
function baseNode(spec, frame) {
|
|
23
|
+
return {
|
|
24
|
+
nodeId: spec.id,
|
|
25
|
+
type: spec.type,
|
|
26
|
+
parentId: spec.parentId ?? null,
|
|
27
|
+
orderKey: spec.orderKey ?? null,
|
|
28
|
+
x: frame.x,
|
|
29
|
+
y: frame.y,
|
|
30
|
+
width: frame.width,
|
|
31
|
+
height: frame.height,
|
|
32
|
+
rotation: frame.rotation ?? 0,
|
|
33
|
+
refKind: null,
|
|
34
|
+
refPath: null,
|
|
35
|
+
refUrl: null,
|
|
36
|
+
view: {},
|
|
37
|
+
style: spec.style ?? {},
|
|
38
|
+
data: {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function arrowFrame(start, end) {
|
|
42
|
+
const padding = 16;
|
|
43
|
+
return {
|
|
44
|
+
x: Math.min(start.x, end.x) - padding,
|
|
45
|
+
y: Math.min(start.y, end.y) - padding,
|
|
46
|
+
width: Math.max(1, Math.abs(end.x - start.x) + padding * 2),
|
|
47
|
+
height: Math.max(1, Math.abs(end.y - start.y) + padding * 2)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create one validated wire node from semantic Board input.
|
|
52
|
+
*
|
|
53
|
+
* Box nodes take an explicit world-space frame. Draw samples and arrow endpoints
|
|
54
|
+
* take world coordinates; the builder derives their frame and local storage form.
|
|
55
|
+
*/
|
|
56
|
+
function createBoardNode(spec) {
|
|
57
|
+
let node;
|
|
58
|
+
if (spec.type === "draw") {
|
|
59
|
+
const size = spec.size ?? 4;
|
|
60
|
+
const worldPoints = spec.points.map((point) => ({
|
|
61
|
+
x: point.x,
|
|
62
|
+
y: point.y,
|
|
63
|
+
p: point.p ?? .5
|
|
64
|
+
}));
|
|
65
|
+
const bounds = computeDrawBounds(worldPoints, size);
|
|
66
|
+
node = baseNode(spec, bounds);
|
|
67
|
+
node.data = {
|
|
68
|
+
points: worldPoints.map((point) => ({
|
|
69
|
+
x: point.x - bounds.x,
|
|
70
|
+
y: point.y - bounds.y,
|
|
71
|
+
p: point.p
|
|
72
|
+
})),
|
|
73
|
+
color: spec.color ?? "brand",
|
|
74
|
+
size
|
|
75
|
+
};
|
|
76
|
+
} else if (spec.type === "arrow") {
|
|
77
|
+
node = baseNode(spec, arrowFrame(spec.start, spec.end));
|
|
78
|
+
node.data = {
|
|
79
|
+
start: spec.start,
|
|
80
|
+
end: spec.end,
|
|
81
|
+
bend: spec.bend ?? 0,
|
|
82
|
+
color: spec.color ?? "brand",
|
|
83
|
+
size: spec.size ?? 2.5,
|
|
84
|
+
arrowStart: spec.arrowStart ?? false,
|
|
85
|
+
arrowEnd: spec.arrowEnd ?? true,
|
|
86
|
+
label: spec.label ?? ""
|
|
87
|
+
};
|
|
88
|
+
} else {
|
|
89
|
+
node = baseNode(spec, spec.frame);
|
|
90
|
+
switch (spec.type) {
|
|
91
|
+
case "text":
|
|
92
|
+
node.data = {
|
|
93
|
+
text: spec.text ?? "",
|
|
94
|
+
color: spec.color ?? "neutral",
|
|
95
|
+
fontSize: spec.fontSize ?? 24
|
|
96
|
+
};
|
|
97
|
+
break;
|
|
98
|
+
case "geo":
|
|
99
|
+
node.data = {
|
|
100
|
+
geo: spec.geo ?? "rectangle",
|
|
101
|
+
text: spec.text ?? "",
|
|
102
|
+
color: spec.color ?? "brand",
|
|
103
|
+
fillOpacity: spec.fillOpacity ?? 0
|
|
104
|
+
};
|
|
105
|
+
break;
|
|
106
|
+
case "frame":
|
|
107
|
+
node.data = {
|
|
108
|
+
label: spec.label ?? "Frame",
|
|
109
|
+
color: spec.color ?? "neutral"
|
|
110
|
+
};
|
|
111
|
+
break;
|
|
112
|
+
case "image":
|
|
113
|
+
node.refKind = "space_file";
|
|
114
|
+
node.refPath = spec.path;
|
|
115
|
+
node.view = spec.snapshot ?? {};
|
|
116
|
+
node.data = spec.crop ? { crop: spec.crop } : {};
|
|
117
|
+
break;
|
|
118
|
+
case "video":
|
|
119
|
+
case "audio":
|
|
120
|
+
node.refKind = "space_file";
|
|
121
|
+
node.refPath = spec.path;
|
|
122
|
+
node.view = spec.snapshot ?? {};
|
|
123
|
+
break;
|
|
124
|
+
case "file":
|
|
125
|
+
node.refKind = "space_file";
|
|
126
|
+
node.refPath = spec.path;
|
|
127
|
+
node.view = spec.snapshot ?? {};
|
|
128
|
+
break;
|
|
129
|
+
case "task":
|
|
130
|
+
node.view = spec.snapshot;
|
|
131
|
+
node.data = { taskRunId: spec.taskRunId };
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
assertBoardNodes([node]);
|
|
136
|
+
return node;
|
|
137
|
+
}
|
|
138
|
+
function validateBoardNodes(nodes, path = "nodes") {
|
|
139
|
+
return nodes.flatMap((node, index) => validateBoardNodeInput(node, `${path}.${index}`));
|
|
140
|
+
}
|
|
141
|
+
function assertBoardNodes(nodes, path = "nodes") {
|
|
142
|
+
const diagnostics = validateBoardNodes(nodes, path);
|
|
143
|
+
if (diagnostics.length > 0) throw new BoardInputError(diagnostics);
|
|
144
|
+
}
|
|
145
|
+
function assertBoardTransactionNodeCreates(operations) {
|
|
146
|
+
const diagnostics = operations.flatMap((operation, index) => {
|
|
147
|
+
if (operation.type !== "node.create") return [];
|
|
148
|
+
const payload = operation.payload;
|
|
149
|
+
return payload.node ? validateBoardNodeInput(payload.node, `operations.${index}.payload.node`) : [];
|
|
150
|
+
});
|
|
151
|
+
if (diagnostics.length > 0) throw new BoardInputError(diagnostics);
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
export { BoardInputError, assertBoardNodes, assertBoardTransactionNodeCreates, createBoardNode, validateBoardNodes };
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ConnectionLayer, ConnectionRenderInput, createConnectionLayer, framesFromItems } from "./connection-layer.js";
|
|
2
|
+
import { BoardMediaAction, boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible } from "./media-interaction.js";
|
|
2
3
|
import { BoardCardRenderer, BoardRenderContext, BoardRenderPalette, boardCardRenderersForTest, getBoardCardRenderer, registerBoardCardRenderer } from "./renderers/board-renderer-registry.js";
|
|
3
4
|
import { defaultBoardPalette } from "./palette.js";
|
|
4
5
|
import { TASK_CARD_FULL_DETAIL_ZOOM } from "./renderers/task-card-renderer.js";
|
|
5
6
|
import { ensureBoardTextMeasurement, installBoardTextMeasurement } from "./text-measurement.js";
|
|
6
7
|
import { getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
|
|
7
8
|
import { BoardThemeContext, BoardThemeRenderer, getBoardThemeRenderer, registerBoardThemeRenderer } from "./themes/board-theme-registry.js";
|
|
8
|
-
|
|
9
|
+
import { VIDEO_THUMBNAIL_MAX_EDGE, VideoNaturalSize, loadVideoThumbnailTexture, videoTextureNaturalSize, videoThumbnailSize } from "./video-thumbnail.js";
|
|
10
|
+
export { BoardCardRenderer, BoardMediaAction, BoardRenderContext, BoardRenderPalette, BoardThemeContext, BoardThemeRenderer, ConnectionLayer, ConnectionRenderInput, TASK_CARD_FULL_DETAIL_ZOOM, VIDEO_THUMBNAIL_MAX_EDGE, VideoNaturalSize, boardCardRenderersForTest, boardMediaActionAt, createConnectionLayer, defaultBoardPalette, ensureBoardTextMeasurement, framesFromItems, getBoardCardRenderer, getBoardResolution, getBoardThemeRenderer, installBoardTextMeasurement, loadVideoThumbnailTexture, mediaPlayBadgeHit, mediaPlayBadgeVisible, registerBoardCardRenderer, registerBoardThemeRenderer, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, videoTextureNaturalSize, videoThumbnailSize };
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
|
|
2
2
|
import { createConnectionLayer, framesFromItems } from "./connection-layer.js";
|
|
3
|
+
import { TASK_CARD_FULL_DETAIL_ZOOM } from "./renderers/task-card-renderer.js";
|
|
4
|
+
import { boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible } from "./media-interaction.js";
|
|
3
5
|
import { defaultBoardPalette } from "./palette.js";
|
|
4
6
|
import { ensureBoardTextMeasurement, installBoardTextMeasurement } from "./text-measurement.js";
|
|
5
|
-
import { TASK_CARD_FULL_DETAIL_ZOOM } from "./renderers/task-card-renderer.js";
|
|
6
7
|
import { boardCardRenderersForTest, getBoardCardRenderer, registerBoardCardRenderer } from "./renderers/board-renderer-registry.js";
|
|
7
8
|
import { getBoardThemeRenderer, registerBoardThemeRenderer } from "./themes/board-theme-registry.js";
|
|
8
|
-
|
|
9
|
+
import { VIDEO_THUMBNAIL_MAX_EDGE, loadVideoThumbnailTexture, videoTextureNaturalSize, videoThumbnailSize } from "./video-thumbnail.js";
|
|
10
|
+
export { TASK_CARD_FULL_DETAIL_ZOOM, VIDEO_THUMBNAIL_MAX_EDGE, boardCardRenderersForTest, boardMediaActionAt, createConnectionLayer, defaultBoardPalette, ensureBoardTextMeasurement, framesFromItems, getBoardCardRenderer, getBoardResolution, getBoardThemeRenderer, installBoardTextMeasurement, loadVideoThumbnailTexture, mediaPlayBadgeHit, mediaPlayBadgeVisible, registerBoardCardRenderer, registerBoardThemeRenderer, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, videoTextureNaturalSize, videoThumbnailSize };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BoardItem } from "../../protocol/dist/board-document.js";
|
|
2
|
+
import { WorldPoint } from "../geometry.js";
|
|
3
|
+
//#region src/board/render/media-interaction.d.ts
|
|
4
|
+
type BoardMediaAction = {
|
|
5
|
+
action: "play-media";
|
|
6
|
+
itemId: string;
|
|
7
|
+
};
|
|
8
|
+
declare function mediaPlayBadgeVisible(item: BoardItem, zoom: number, options: {
|
|
9
|
+
materialized: boolean;
|
|
10
|
+
hasVideoPreview: boolean;
|
|
11
|
+
}): boolean;
|
|
12
|
+
/** The central badge is a fixed screen-space target, independent of Board zoom. */
|
|
13
|
+
declare function mediaPlayBadgeHit(item: BoardItem, point: WorldPoint, zoom: number): boolean;
|
|
14
|
+
declare function boardMediaActionAt(item: BoardItem, point: WorldPoint, zoom: number, options: {
|
|
15
|
+
materialized: boolean;
|
|
16
|
+
hasVideoPreview: boolean;
|
|
17
|
+
}): BoardMediaAction | null;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { BoardMediaAction, boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { featuredTaskArtifact } from "../task.js";
|
|
2
|
+
import "./renderers/task-card-renderer.js";
|
|
3
|
+
//#region src/board/render/media-interaction.ts
|
|
4
|
+
function mediaPlayBadgeVisible(item, zoom, options) {
|
|
5
|
+
if (!options.materialized) return false;
|
|
6
|
+
if (item.type === "video" || item.type === "audio") return true;
|
|
7
|
+
if (item.type !== "task" || zoom < .55) return false;
|
|
8
|
+
const artifact = featuredTaskArtifact(item.snapshot.artifacts);
|
|
9
|
+
if (artifact?.type === "audio") return true;
|
|
10
|
+
return artifact?.type === "video" && (Boolean(artifact.previewUrl) || options.hasVideoPreview);
|
|
11
|
+
}
|
|
12
|
+
/** The central badge is a fixed screen-space target, independent of Board zoom. */
|
|
13
|
+
function mediaPlayBadgeHit(item, point, zoom) {
|
|
14
|
+
const radius = 28 / Math.max(zoom, .05);
|
|
15
|
+
const centerX = item.frame.x + item.frame.width / 2;
|
|
16
|
+
const centerY = item.frame.y + item.frame.height / 2;
|
|
17
|
+
return Math.hypot(point.x - centerX, point.y - centerY) <= radius;
|
|
18
|
+
}
|
|
19
|
+
function boardMediaActionAt(item, point, zoom, options) {
|
|
20
|
+
return mediaPlayBadgeVisible(item, zoom, options) && mediaPlayBadgeHit(item, point, zoom) ? {
|
|
21
|
+
action: "play-media",
|
|
22
|
+
itemId: item.id
|
|
23
|
+
} : null;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
2
|
import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
|
|
3
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
4
3
|
import { drawAudioWaveform } from "../audio-waveform.js";
|
|
5
4
|
import { positionShell } from "./base-card-renderer.js";
|
|
5
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
6
6
|
import { Container, Graphics, Text } from "pixi.js";
|
|
7
7
|
//#region src/board/render/renderers/audio-card-renderer.ts
|
|
8
8
|
const RADIUS = 4;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import { fileCardRenderer } from "./file-card-renderer.js";
|
|
2
|
+
import { taskCardRenderer } from "./task-card-renderer.js";
|
|
1
3
|
import { ensureBoardTextMeasurement } from "../text-measurement.js";
|
|
2
4
|
import { arrowCardRenderer } from "./arrow-card-renderer.js";
|
|
3
5
|
import { audioCardRenderer } from "./audio-card-renderer.js";
|
|
4
6
|
import { drawCardRenderer } from "./draw-card-renderer.js";
|
|
5
|
-
import { fileCardRenderer } from "./file-card-renderer.js";
|
|
6
7
|
import { frameCardRenderer } from "./frame-card-renderer.js";
|
|
7
8
|
import { geoCardRenderer } from "./geo-card-renderer.js";
|
|
8
9
|
import { imageCardRenderer } from "./image-card-renderer.js";
|
|
9
|
-
import { taskCardRenderer } from "./task-card-renderer.js";
|
|
10
10
|
import { textCardRenderer } from "./text-card-renderer.js";
|
|
11
11
|
import { unknownCardRenderer } from "./unknown-card-renderer.js";
|
|
12
12
|
import { videoCardRenderer } from "./video-card-renderer.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildStrokeOutline, computeDrawBounds } from "../../core/draw-geometry.js";
|
|
2
2
|
import { pickBoardColor } from "../../core/palette.js";
|
|
3
|
-
import { drawFarStroke } from "./far-plate.js";
|
|
4
3
|
import { positionShell } from "./base-card-renderer.js";
|
|
4
|
+
import { drawFarStroke } from "./far-plate.js";
|
|
5
5
|
import { Container, Graphics } from "pixi.js";
|
|
6
6
|
//#region src/board/render/renderers/draw-card-renderer.ts
|
|
7
7
|
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
2
|
import { fileBaseName, filePreviewKind } from "../../core/file-preview.js";
|
|
3
3
|
import { syncTextResolution } from "../text-resolution.js";
|
|
4
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
5
4
|
import { positionShell } from "./base-card-renderer.js";
|
|
5
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
6
6
|
import { CanvasTextMetrics, Container, Graphics, Sprite, Text, Texture } from "pixi.js";
|
|
7
7
|
//#region src/board/render/renderers/file-card-renderer.ts
|
|
8
8
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
2
|
import { pickBoardColor } from "../../core/palette.js";
|
|
3
3
|
import { syncTextResolution } from "../text-resolution.js";
|
|
4
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
5
4
|
import { createLabel, positionShell } from "./base-card-renderer.js";
|
|
5
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
6
6
|
import { Container, Graphics } from "pixi.js";
|
|
7
7
|
//#region src/board/render/renderers/frame-card-renderer.ts
|
|
8
8
|
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
2
|
import { pickBoardColor } from "../../core/palette.js";
|
|
3
3
|
import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
|
|
4
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
5
4
|
import { positionShell } from "./base-card-renderer.js";
|
|
5
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
6
6
|
import { Container, Graphics, Text } from "pixi.js";
|
|
7
7
|
//#region src/board/render/renderers/geo-card-renderer.ts
|
|
8
8
|
const RADIUS = 8;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
2
1
|
import { positionShell } from "./base-card-renderer.js";
|
|
2
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
3
3
|
import { Container, Graphics, Sprite, Texture } from "pixi.js";
|
|
4
4
|
//#region src/board/render/renderers/image-card-renderer.ts
|
|
5
5
|
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { BOARD_FONT_STACK, BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
+
import { featuredTaskArtifact } from "../../task.js";
|
|
2
3
|
import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
|
|
3
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
4
4
|
import { drawAudioWaveform } from "../audio-waveform.js";
|
|
5
5
|
import { positionShell } from "./base-card-renderer.js";
|
|
6
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
6
7
|
import { fitTextToLines } from "./file-card-renderer.js";
|
|
7
8
|
import { Container, Graphics, Sprite, Text } from "pixi.js";
|
|
8
9
|
//#region src/board/render/renderers/task-card-renderer.ts
|
|
@@ -12,12 +13,11 @@ const META_HEIGHT = 28;
|
|
|
12
13
|
const TASK_CARD_FULL_DETAIL_ZOOM = .55;
|
|
13
14
|
const PLAY_BADGE_RADIUS = 15;
|
|
14
15
|
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
15
|
-
function previewKindFor(
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
return output.textExcerpt ? "text" : "empty";
|
|
16
|
+
function previewKindFor(artifact) {
|
|
17
|
+
if (!artifact) return "empty";
|
|
18
|
+
if (artifact.type === "image" || artifact.type === "video") return "texture";
|
|
19
|
+
if (artifact.type === "audio") return artifact.previewUrl ? "texture" : "waveform";
|
|
20
|
+
return artifact.textExcerpt ? "text" : "empty";
|
|
21
21
|
}
|
|
22
22
|
function stateSurfaceFor(item, kind, hasTexture, previewFailed) {
|
|
23
23
|
if (kind === "texture" && !hasTexture) {
|
|
@@ -46,7 +46,8 @@ function statusColor(item, context) {
|
|
|
46
46
|
return context.colors.neutral.stroke;
|
|
47
47
|
}
|
|
48
48
|
function metadata(item) {
|
|
49
|
-
const
|
|
49
|
+
const { artifactCount, artifacts } = item.snapshot;
|
|
50
|
+
const extra = artifactCount > artifacts.length ? `${artifacts.length}/${artifactCount}` : artifactCount > 1 ? `+${artifactCount - 1}` : null;
|
|
50
51
|
return [item.snapshot.model, extra].filter(Boolean).join(" · ");
|
|
51
52
|
}
|
|
52
53
|
/** Scale a texture into a frame without cropping or distortion. */
|
|
@@ -169,7 +170,8 @@ function sync(container, item, context) {
|
|
|
169
170
|
}
|
|
170
171
|
const texture = key ? context.getTexture(key) : null;
|
|
171
172
|
const previewFailed = Boolean(key && !texture && context.hasError(key));
|
|
172
|
-
const
|
|
173
|
+
const artifact = featuredTaskArtifact(item.snapshot.artifacts);
|
|
174
|
+
const kind = previewKindFor(artifact);
|
|
173
175
|
const surface = stateSurfaceFor(item, kind, Boolean(texture), previewFailed);
|
|
174
176
|
const frame = {
|
|
175
177
|
x: 1,
|
|
@@ -177,7 +179,7 @@ function sync(container, item, context) {
|
|
|
177
179
|
width: Math.max(1, width - 2),
|
|
178
180
|
height: Math.max(1, height - 2)
|
|
179
181
|
};
|
|
180
|
-
const metaText =
|
|
182
|
+
const metaText = artifact ? metadata(item) : "";
|
|
181
183
|
const showMeta = full && Boolean(metaText);
|
|
182
184
|
const failedWithOutput = item.snapshot.status === "failed" && kind !== "empty";
|
|
183
185
|
syncTextResolution(parts.body, parts, context.zoom);
|
|
@@ -193,7 +195,8 @@ function sync(container, item, context) {
|
|
|
193
195
|
surface,
|
|
194
196
|
showMeta,
|
|
195
197
|
failedWithOutput,
|
|
196
|
-
|
|
198
|
+
artifact?.type ?? "none",
|
|
199
|
+
artifact?.id ?? "none",
|
|
197
200
|
item.taskRunId,
|
|
198
201
|
texture ? `${texture.width}x${texture.height}` : "none",
|
|
199
202
|
context.palette.surface,
|
|
@@ -227,7 +230,7 @@ function sync(container, item, context) {
|
|
|
227
230
|
height: Math.max(1, frame.height - PADDING * 2 - bottomInset)
|
|
228
231
|
}, context.colors.brand.stroke);
|
|
229
232
|
}
|
|
230
|
-
if (full && (
|
|
233
|
+
if (full && (artifact?.type === "audio" || artifact?.type === "video" && texture)) drawPlayBadge(parts.previewArt, frame, context);
|
|
231
234
|
if (surface) {
|
|
232
235
|
const markY = frame.y + frame.height / 2 - (full && stateLabel(surface) ? 10 : 0);
|
|
233
236
|
drawStateMark(parts.previewArt, surface, frame.x + frame.width / 2, markY, surface === "failed" ? color : context.colors.neutral.stroke);
|
|
@@ -246,7 +249,7 @@ function sync(container, item, context) {
|
|
|
246
249
|
parts.preview.width = fitted.width;
|
|
247
250
|
parts.preview.height = fitted.height;
|
|
248
251
|
}
|
|
249
|
-
const bodyText = kind === "text"
|
|
252
|
+
const bodyText = kind === "text" && artifact?.type === "text" ? artifact.textExcerpt : stateLabel(surface);
|
|
250
253
|
const textSig = [
|
|
251
254
|
bodyText,
|
|
252
255
|
metaText,
|
|
@@ -2,8 +2,8 @@ import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
|
2
2
|
import { TEXT_FONT_SIZE, boardTextLineHeight, clampBoardTextFontSize, measureBoardText } from "../../core/text-metrics.js";
|
|
3
3
|
import { pickBoardColor } from "../../core/palette.js";
|
|
4
4
|
import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
|
|
5
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
6
5
|
import { positionShell } from "./base-card-renderer.js";
|
|
6
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
7
7
|
import { Container, Text } from "pixi.js";
|
|
8
8
|
//#region src/board/render/renderers/text-card-renderer.ts
|
|
9
9
|
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
2
|
import { unknownRealType } from "../../../protocol/dist/board-document.js";
|
|
3
3
|
import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
|
|
4
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
5
4
|
import { positionShell } from "./base-card-renderer.js";
|
|
5
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
6
6
|
import { Container, Graphics, Text } from "pixi.js";
|
|
7
7
|
//#region src/board/render/renderers/unknown-card-renderer.ts
|
|
8
8
|
const RADIUS = 10;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
2
|
import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
|
|
3
|
-
import { drawFarPlate } from "./far-plate.js";
|
|
4
3
|
import { positionShell } from "./base-card-renderer.js";
|
|
4
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
5
5
|
import { Container, Graphics, Sprite, Text, Texture } from "pixi.js";
|
|
6
6
|
//#region src/board/render/renderers/video-card-renderer.ts
|
|
7
7
|
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Texture } from "pixi.js";
|
|
2
|
+
//#region src/board/render/video-thumbnail.d.ts
|
|
3
|
+
declare const VIDEO_THUMBNAIL_MAX_EDGE = 960;
|
|
4
|
+
type VideoNaturalSize = {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
};
|
|
8
|
+
declare function videoThumbnailSize(width: number, height: number, maxEdge?: number): {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
declare function videoTextureNaturalSize(texture: Texture): VideoNaturalSize | null;
|
|
13
|
+
/** Decode one static, bounded preview frame without starting playback. */
|
|
14
|
+
declare function loadVideoThumbnailTexture(url: string): Promise<Texture>;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { VIDEO_THUMBNAIL_MAX_EDGE, VideoNaturalSize, loadVideoThumbnailTexture, videoTextureNaturalSize, videoThumbnailSize };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Texture } from "pixi.js";
|
|
2
|
+
//#region src/board/render/video-thumbnail.ts
|
|
3
|
+
const VIDEO_THUMBNAIL_MAX_EDGE = 960;
|
|
4
|
+
const VIDEO_LOAD_TIMEOUT_MS = 15e3;
|
|
5
|
+
const FRAME_PRESENT_TIMEOUT_MS = 250;
|
|
6
|
+
const naturalSizeByTexture = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
function videoThumbnailSize(width, height, maxEdge = 960) {
|
|
8
|
+
if (width <= 0 || height <= 0 || maxEdge <= 0) throw new Error("Video has no displayable frame");
|
|
9
|
+
const scale = Math.min(1, maxEdge / Math.max(width, height));
|
|
10
|
+
return {
|
|
11
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
12
|
+
height: Math.max(1, Math.round(height * scale))
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function waitForFirstFrame(video) {
|
|
16
|
+
if (video.readyState >= 2) return Promise.resolve();
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const timeout = setTimeout(() => finish(/* @__PURE__ */ new Error("Video preview timed out")), VIDEO_LOAD_TIMEOUT_MS);
|
|
19
|
+
const onLoaded = () => finish();
|
|
20
|
+
const onError = () => finish(/* @__PURE__ */ new Error("Video preview could not be decoded"));
|
|
21
|
+
function finish(error) {
|
|
22
|
+
clearTimeout(timeout);
|
|
23
|
+
video.removeEventListener("loadeddata", onLoaded);
|
|
24
|
+
video.removeEventListener("error", onError);
|
|
25
|
+
if (error) reject(error);
|
|
26
|
+
else resolve();
|
|
27
|
+
}
|
|
28
|
+
video.addEventListener("loadeddata", onLoaded, { once: true });
|
|
29
|
+
video.addEventListener("error", onError, { once: true });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function waitForPresentedFrame(video) {
|
|
33
|
+
if (!video.requestVideoFrameCallback) return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
let settled = false;
|
|
36
|
+
let timeout;
|
|
37
|
+
const finish = () => {
|
|
38
|
+
if (settled) return;
|
|
39
|
+
settled = true;
|
|
40
|
+
clearTimeout(timeout);
|
|
41
|
+
resolve();
|
|
42
|
+
};
|
|
43
|
+
const handle = video.requestVideoFrameCallback(finish);
|
|
44
|
+
timeout = setTimeout(() => {
|
|
45
|
+
video.cancelVideoFrameCallback?.(handle);
|
|
46
|
+
finish();
|
|
47
|
+
}, FRAME_PRESENT_TIMEOUT_MS);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function videoTextureNaturalSize(texture) {
|
|
51
|
+
return naturalSizeByTexture.get(texture) ?? null;
|
|
52
|
+
}
|
|
53
|
+
/** Decode one static, bounded preview frame without starting playback. */
|
|
54
|
+
async function loadVideoThumbnailTexture(url) {
|
|
55
|
+
const video = document.createElement("video");
|
|
56
|
+
video.muted = true;
|
|
57
|
+
video.playsInline = true;
|
|
58
|
+
video.preload = "auto";
|
|
59
|
+
if (/^https?:/i.test(url)) video.crossOrigin = "anonymous";
|
|
60
|
+
try {
|
|
61
|
+
const ready = waitForFirstFrame(video);
|
|
62
|
+
video.src = url;
|
|
63
|
+
video.load();
|
|
64
|
+
await ready;
|
|
65
|
+
await waitForPresentedFrame(video);
|
|
66
|
+
const size = videoThumbnailSize(video.videoWidth, video.videoHeight);
|
|
67
|
+
const canvas = document.createElement("canvas");
|
|
68
|
+
canvas.width = size.width;
|
|
69
|
+
canvas.height = size.height;
|
|
70
|
+
const context = canvas.getContext("2d", { alpha: false });
|
|
71
|
+
if (!context) throw new Error("Canvas is not supported");
|
|
72
|
+
context.drawImage(video, 0, 0, size.width, size.height);
|
|
73
|
+
const texture = Texture.from(canvas);
|
|
74
|
+
naturalSizeByTexture.set(texture, {
|
|
75
|
+
width: video.videoWidth,
|
|
76
|
+
height: video.videoHeight
|
|
77
|
+
});
|
|
78
|
+
return texture;
|
|
79
|
+
} finally {
|
|
80
|
+
video.pause();
|
|
81
|
+
video.removeAttribute("src");
|
|
82
|
+
video.load();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { VIDEO_THUMBNAIL_MAX_EDGE, loadVideoThumbnailTexture, videoTextureNaturalSize, videoThumbnailSize };
|
package/dist/board/task.d.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { BoardTaskSnapshot } from "../protocol/dist/board-document.js";
|
|
1
|
+
import { BoardTaskArtifact, BoardTaskSnapshot } from "../protocol/dist/board-document.js";
|
|
2
2
|
import { TaskRunRecord } from "../types.js";
|
|
3
3
|
//#region src/board/task.d.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Group provider blocks into user-facing works. A cover/poster sharing a stable
|
|
6
|
+
* provider id with playable media belongs to that work instead of becoming a
|
|
7
|
+
* competing image result.
|
|
7
8
|
*/
|
|
8
|
-
declare function
|
|
9
|
+
declare function taskArtifacts(blocks: Record<string, unknown>[]): BoardTaskArtifact[];
|
|
10
|
+
/** Highest-value artifact first, with provider order as the stable final tie. */
|
|
11
|
+
declare function rankedTaskArtifacts(artifacts: readonly BoardTaskArtifact[]): BoardTaskArtifact[];
|
|
12
|
+
declare function featuredTaskArtifact(artifacts: readonly BoardTaskArtifact[]): BoardTaskArtifact | undefined;
|
|
13
|
+
declare function taskArtifactPreviewUrl(artifact: BoardTaskArtifact | undefined): string | undefined;
|
|
9
14
|
/**
|
|
10
15
|
* Project an authoritative TaskRun into a small, replaceable Board display cache.
|
|
11
16
|
* Raw payloads, complete results and inline media are never copied into the Board.
|
|
12
17
|
*/
|
|
13
18
|
declare function taskRunToBoardTaskSnapshot(run: TaskRunRecord): BoardTaskSnapshot;
|
|
14
19
|
//#endregion
|
|
15
|
-
export {
|
|
20
|
+
export { featuredTaskArtifact, rankedTaskArtifacts, taskArtifactPreviewUrl, taskArtifacts, taskRunToBoardTaskSnapshot };
|