@neta-art/cohub 5.6.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/codec.js +15 -0
- package/dist/board/core/palette.d.ts +3 -2
- package/dist/board/core/shape-types.d.ts +8 -2
- 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 +10 -5
- 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/audio-waveform.d.ts +12 -0
- package/dist/board/render/audio-waveform.js +36 -0
- package/dist/board/render/index.d.ts +4 -1
- package/dist/board/render/index.js +4 -1
- 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.d.ts +5 -0
- package/dist/board/render/renderers/audio-card-renderer.js +120 -0
- package/dist/board/render/renderers/board-renderer-registry.js +4 -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.d.ts +2 -1
- package/dist/board/render/renderers/task-card-renderer.js +21 -53
- 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 +139 -3
- 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 +271 -53
- package/dist/protocol/dist/board-document.js +54 -22
- 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 };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Graphics } from "pixi.js";
|
|
2
|
+
//#region src/board/render/audio-waveform.d.ts
|
|
3
|
+
/** Stable placeholder amplitudes without reading or decoding the audio file. */
|
|
4
|
+
declare function audioWaveformBars(seed: string, count: number): number[];
|
|
5
|
+
declare function drawAudioWaveform(graphics: Graphics, seed: string, rect: {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}, color: number, alpha?: number): void;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { audioWaveformBars, drawAudioWaveform };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/board/render/audio-waveform.ts
|
|
2
|
+
const DEFAULT_BARS = 44;
|
|
3
|
+
const BAR_GAP = 2;
|
|
4
|
+
/** Stable placeholder amplitudes without reading or decoding the audio file. */
|
|
5
|
+
function audioWaveformBars(seed, count) {
|
|
6
|
+
let hash = 2166136261;
|
|
7
|
+
for (let index = 0; index < seed.length; index += 1) {
|
|
8
|
+
hash ^= seed.charCodeAt(index);
|
|
9
|
+
hash = Math.imul(hash, 16777619);
|
|
10
|
+
}
|
|
11
|
+
const bars = [];
|
|
12
|
+
for (let index = 0; index < count; index += 1) {
|
|
13
|
+
hash ^= hash << 13;
|
|
14
|
+
hash ^= hash >>> 17;
|
|
15
|
+
hash ^= hash << 5;
|
|
16
|
+
bars.push(.2 + (hash >>> 0) / 4294967295 * .8);
|
|
17
|
+
}
|
|
18
|
+
return bars;
|
|
19
|
+
}
|
|
20
|
+
function drawAudioWaveform(graphics, seed, rect, color, alpha = .72) {
|
|
21
|
+
const count = Math.max(8, Math.min(DEFAULT_BARS, Math.floor(rect.width / 4)));
|
|
22
|
+
const step = rect.width / count;
|
|
23
|
+
const barWidth = Math.max(1.5, step - BAR_GAP);
|
|
24
|
+
const centerY = rect.y + rect.height / 2;
|
|
25
|
+
const maxHeight = Math.max(2, rect.height * .72);
|
|
26
|
+
const bars = audioWaveformBars(seed, count);
|
|
27
|
+
for (let index = 0; index < count; index += 1) {
|
|
28
|
+
const height = Math.max(2, (bars[index] ?? .4) * maxHeight);
|
|
29
|
+
graphics.roundRect(rect.x + index * step, centerY - height / 2, barWidth, height, barWidth / 2).fill({
|
|
30
|
+
color,
|
|
31
|
+
alpha
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { audioWaveformBars, drawAudioWaveform };
|
|
@@ -1,7 +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";
|
|
5
|
+
import { TASK_CARD_FULL_DETAIL_ZOOM } from "./renderers/task-card-renderer.js";
|
|
4
6
|
import { ensureBoardTextMeasurement, installBoardTextMeasurement } from "./text-measurement.js";
|
|
5
7
|
import { getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
|
|
6
8
|
import { BoardThemeContext, BoardThemeRenderer, getBoardThemeRenderer, registerBoardThemeRenderer } from "./themes/board-theme-registry.js";
|
|
7
|
-
|
|
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,7 +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
7
|
import { boardCardRenderersForTest, getBoardCardRenderer, registerBoardCardRenderer } from "./renderers/board-renderer-registry.js";
|
|
6
8
|
import { getBoardThemeRenderer, registerBoardThemeRenderer } from "./themes/board-theme-registry.js";
|
|
7
|
-
|
|
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 };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
+
import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
|
|
3
|
+
import { drawAudioWaveform } from "../audio-waveform.js";
|
|
4
|
+
import { positionShell } from "./base-card-renderer.js";
|
|
5
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
6
|
+
import { Container, Graphics, Text } from "pixi.js";
|
|
7
|
+
//#region src/board/render/renderers/audio-card-renderer.ts
|
|
8
|
+
const RADIUS = 4;
|
|
9
|
+
const PLAY_RADIUS = 18;
|
|
10
|
+
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
function sync(container, item, context) {
|
|
12
|
+
const parts = partsByContainer.get(container);
|
|
13
|
+
if (!parts) return;
|
|
14
|
+
positionShell(parts.root, item);
|
|
15
|
+
const { width, height } = item.frame;
|
|
16
|
+
const selected = context.selectedIds.has(item.id);
|
|
17
|
+
const hovered = context.hoveredId === item.id;
|
|
18
|
+
const title = item.snapshot?.title ?? item.ref.path.split("/").pop() ?? "Audio";
|
|
19
|
+
syncTextResolution(parts.label, parts, context.zoom);
|
|
20
|
+
if (parts.label.text !== title) parts.label.text = title;
|
|
21
|
+
syncTextWrapWidth(parts.label, parts, Math.max(1, width - 24), false);
|
|
22
|
+
parts.label.position.set(12, Math.max(8, height - parts.label.height - 8));
|
|
23
|
+
const sig = [
|
|
24
|
+
width,
|
|
25
|
+
height,
|
|
26
|
+
selected,
|
|
27
|
+
hovered,
|
|
28
|
+
title,
|
|
29
|
+
parts.label.height,
|
|
30
|
+
context.palette.surface,
|
|
31
|
+
context.palette.brand,
|
|
32
|
+
context.palette.border,
|
|
33
|
+
context.palette.muted,
|
|
34
|
+
context.palette.text
|
|
35
|
+
].join("|");
|
|
36
|
+
if (sig === parts.sig) return;
|
|
37
|
+
parts.sig = sig;
|
|
38
|
+
parts.plate.clear().roundRect(0, 0, width, height, RADIUS).fill({
|
|
39
|
+
color: context.palette.surface,
|
|
40
|
+
alpha: .98
|
|
41
|
+
}).roundRect(0, 0, width, height, RADIUS).stroke({
|
|
42
|
+
color: selected ? context.palette.brand : hovered ? context.palette.muted : context.palette.border,
|
|
43
|
+
width: selected ? 2 : 1,
|
|
44
|
+
alpha: selected ? .96 : .84
|
|
45
|
+
});
|
|
46
|
+
const labelBand = Math.min(height * .38, parts.label.height + 16);
|
|
47
|
+
const waveformInset = Math.min(24, width * .08);
|
|
48
|
+
parts.waveform.clear();
|
|
49
|
+
drawAudioWaveform(parts.waveform, `${item.ref.path}:${item.snapshot?.mtimeMs ?? "unknown"}`, {
|
|
50
|
+
x: waveformInset,
|
|
51
|
+
y: 10,
|
|
52
|
+
width: Math.max(1, width - waveformInset * 2),
|
|
53
|
+
height: Math.max(8, height - labelBand - 16)
|
|
54
|
+
}, context.palette.brand);
|
|
55
|
+
const cx = width / 2;
|
|
56
|
+
const cy = Math.max(24, (height - labelBand) / 2);
|
|
57
|
+
parts.chrome.clear().circle(cx, cy, PLAY_RADIUS).fill({
|
|
58
|
+
color: selected ? context.palette.brand : context.palette.surface,
|
|
59
|
+
alpha: .92
|
|
60
|
+
});
|
|
61
|
+
const triangle = PLAY_RADIUS * .58;
|
|
62
|
+
parts.chrome.moveTo(cx - triangle * .35, cy - triangle * .62).lineTo(cx - triangle * .35, cy + triangle * .62).lineTo(cx + triangle * .72, cy).closePath().fill({
|
|
63
|
+
color: context.palette.text,
|
|
64
|
+
alpha: .96
|
|
65
|
+
});
|
|
66
|
+
parts.label.style.fill = context.palette.text;
|
|
67
|
+
}
|
|
68
|
+
const audioCardRenderer = {
|
|
69
|
+
id: "audio-card",
|
|
70
|
+
canRender: (item) => item.type === "audio",
|
|
71
|
+
create: (item, context) => {
|
|
72
|
+
const root = new Container();
|
|
73
|
+
const plate = new Graphics();
|
|
74
|
+
const waveform = new Graphics();
|
|
75
|
+
const chrome = new Graphics();
|
|
76
|
+
const resolution = textResolutionForZoom(context.zoom);
|
|
77
|
+
const label = new Text({
|
|
78
|
+
text: "",
|
|
79
|
+
style: {
|
|
80
|
+
fill: context.palette.text,
|
|
81
|
+
fontFamily: BOARD_FONT_STACK,
|
|
82
|
+
fontSize: 12,
|
|
83
|
+
fontWeight: "500",
|
|
84
|
+
wordWrap: true
|
|
85
|
+
},
|
|
86
|
+
resolution,
|
|
87
|
+
roundPixels: true
|
|
88
|
+
});
|
|
89
|
+
root.addChild(plate, waveform, chrome, label);
|
|
90
|
+
partsByContainer.set(root, {
|
|
91
|
+
root,
|
|
92
|
+
plate,
|
|
93
|
+
waveform,
|
|
94
|
+
chrome,
|
|
95
|
+
label,
|
|
96
|
+
sig: "",
|
|
97
|
+
wrapWidth: 0,
|
|
98
|
+
resolution
|
|
99
|
+
});
|
|
100
|
+
if (item.type === "audio") sync(root, item, context);
|
|
101
|
+
return root;
|
|
102
|
+
},
|
|
103
|
+
update: (container, item, context) => {
|
|
104
|
+
if (item.type === "audio") sync(container, item, context);
|
|
105
|
+
},
|
|
106
|
+
renderFar: (graphics, item, context) => {
|
|
107
|
+
drawFarPlate(graphics, item.frame, {
|
|
108
|
+
fill: context.palette.surface,
|
|
109
|
+
fillAlpha: .96,
|
|
110
|
+
accent: context.palette.brand,
|
|
111
|
+
accentAlpha: .72
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
destroy: (container) => {
|
|
115
|
+
partsByContainer.get(container)?.root.destroy({ children: true });
|
|
116
|
+
partsByContainer.delete(container);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
//#endregion
|
|
120
|
+
export { audioCardRenderer };
|
|
@@ -1,11 +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";
|
|
5
|
+
import { audioCardRenderer } from "./audio-card-renderer.js";
|
|
3
6
|
import { drawCardRenderer } from "./draw-card-renderer.js";
|
|
4
|
-
import { fileCardRenderer } from "./file-card-renderer.js";
|
|
5
7
|
import { frameCardRenderer } from "./frame-card-renderer.js";
|
|
6
8
|
import { geoCardRenderer } from "./geo-card-renderer.js";
|
|
7
9
|
import { imageCardRenderer } from "./image-card-renderer.js";
|
|
8
|
-
import { taskCardRenderer } from "./task-card-renderer.js";
|
|
9
10
|
import { textCardRenderer } from "./text-card-renderer.js";
|
|
10
11
|
import { unknownCardRenderer } from "./unknown-card-renderer.js";
|
|
11
12
|
import { videoCardRenderer } from "./video-card-renderer.js";
|
|
@@ -14,6 +15,7 @@ const boardCardRenderers = [
|
|
|
14
15
|
textCardRenderer,
|
|
15
16
|
imageCardRenderer,
|
|
16
17
|
videoCardRenderer,
|
|
18
|
+
audioCardRenderer,
|
|
17
19
|
fileCardRenderer,
|
|
18
20
|
taskCardRenderer,
|
|
19
21
|
geoCardRenderer,
|
|
@@ -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,5 +1,6 @@
|
|
|
1
1
|
import { BoardCardRenderer } from "./board-renderer-registry.js";
|
|
2
2
|
//#region src/board/render/renderers/task-card-renderer.d.ts
|
|
3
|
+
declare const TASK_CARD_FULL_DETAIL_ZOOM = 0.55;
|
|
3
4
|
/** Scale a texture into a frame without cropping or distortion. */
|
|
4
5
|
declare function containTaskPreviewRect(width: number, height: number, imageWidth: number, imageHeight: number): {
|
|
5
6
|
x: number;
|
|
@@ -9,4 +10,4 @@ declare function containTaskPreviewRect(width: number, height: number, imageWidt
|
|
|
9
10
|
};
|
|
10
11
|
declare const taskCardRenderer: BoardCardRenderer;
|
|
11
12
|
//#endregion
|
|
12
|
-
export { containTaskPreviewRect, taskCardRenderer };
|
|
13
|
+
export { TASK_CARD_FULL_DETAIL_ZOOM, containTaskPreviewRect, taskCardRenderer };
|
|
@@ -1,24 +1,23 @@
|
|
|
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 {
|
|
4
|
+
import { drawAudioWaveform } from "../audio-waveform.js";
|
|
4
5
|
import { positionShell } from "./base-card-renderer.js";
|
|
6
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
5
7
|
import { fitTextToLines } from "./file-card-renderer.js";
|
|
6
8
|
import { Container, Graphics, Sprite, Text } from "pixi.js";
|
|
7
9
|
//#region src/board/render/renderers/task-card-renderer.ts
|
|
8
10
|
const RADIUS = 4;
|
|
9
11
|
const PADDING = 12;
|
|
10
12
|
const META_HEIGHT = 28;
|
|
11
|
-
const
|
|
12
|
-
const WAVEFORM_BARS = 44;
|
|
13
|
-
const WAVEFORM_GAP = 2;
|
|
13
|
+
const TASK_CARD_FULL_DETAIL_ZOOM = .55;
|
|
14
14
|
const PLAY_BADGE_RADIUS = 15;
|
|
15
15
|
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
16
|
-
function previewKindFor(
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
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";
|
|
22
21
|
}
|
|
23
22
|
function stateSurfaceFor(item, kind, hasTexture, previewFailed) {
|
|
24
23
|
if (kind === "texture" && !hasTexture) {
|
|
@@ -47,7 +46,8 @@ function statusColor(item, context) {
|
|
|
47
46
|
return context.colors.neutral.stroke;
|
|
48
47
|
}
|
|
49
48
|
function metadata(item) {
|
|
50
|
-
const
|
|
49
|
+
const { artifactCount, artifacts } = item.snapshot;
|
|
50
|
+
const extra = artifactCount > artifacts.length ? `${artifacts.length}/${artifactCount}` : artifactCount > 1 ? `+${artifactCount - 1}` : null;
|
|
51
51
|
return [item.snapshot.model, extra].filter(Boolean).join(" · ");
|
|
52
52
|
}
|
|
53
53
|
/** Scale a texture into a frame without cropping or distortion. */
|
|
@@ -68,40 +68,6 @@ function containTaskPreviewRect(width, height, imageWidth, imageHeight) {
|
|
|
68
68
|
height: renderedHeight
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
-
function waveformBars(seed, count) {
|
|
72
|
-
let hash = 2166136261;
|
|
73
|
-
for (let index = 0; index < seed.length; index += 1) {
|
|
74
|
-
hash ^= seed.charCodeAt(index);
|
|
75
|
-
hash = Math.imul(hash, 16777619) >>> 0;
|
|
76
|
-
}
|
|
77
|
-
const bars = [];
|
|
78
|
-
for (let index = 0; index < count; index += 1) {
|
|
79
|
-
hash ^= hash << 13;
|
|
80
|
-
hash ^= hash >>> 17;
|
|
81
|
-
hash ^= hash << 5;
|
|
82
|
-
hash >>>= 0;
|
|
83
|
-
const unit = hash % 1e3 / 1e3;
|
|
84
|
-
const envelope = Math.sin(Math.PI * (index + .5) / count);
|
|
85
|
-
bars.push(.18 + unit * .82 * (.45 + envelope * .55));
|
|
86
|
-
}
|
|
87
|
-
return bars;
|
|
88
|
-
}
|
|
89
|
-
function drawWaveform(graphics, seed, rect, color) {
|
|
90
|
-
const count = Math.max(8, Math.min(WAVEFORM_BARS, Math.floor(rect.width / 4)));
|
|
91
|
-
const step = rect.width / count;
|
|
92
|
-
const barWidth = Math.max(1.5, step - WAVEFORM_GAP);
|
|
93
|
-
const centerY = rect.y + rect.height / 2;
|
|
94
|
-
const maxHeight = Math.max(2, rect.height * .68);
|
|
95
|
-
const bars = waveformBars(seed, count);
|
|
96
|
-
for (let index = 0; index < count; index += 1) {
|
|
97
|
-
const amplitude = (bars[index] ?? .4) * maxHeight;
|
|
98
|
-
graphics.roundRect(rect.x + index * step + (step - barWidth) / 2, centerY - amplitude / 2, barWidth, amplitude, barWidth / 2);
|
|
99
|
-
}
|
|
100
|
-
graphics.fill({
|
|
101
|
-
color,
|
|
102
|
-
alpha: .68
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
71
|
function drawPlayBadge(graphics, rect, context) {
|
|
106
72
|
const radius = Math.min(PLAY_BADGE_RADIUS, Math.max(8, Math.min(rect.width, rect.height) * .16));
|
|
107
73
|
const cx = rect.x + rect.width / 2;
|
|
@@ -194,7 +160,7 @@ function sync(container, item, context) {
|
|
|
194
160
|
const { width, height } = item.frame;
|
|
195
161
|
const selected = context.selectedIds.has(item.id);
|
|
196
162
|
const hovered = context.hoveredId === item.id;
|
|
197
|
-
const full = context.zoom >=
|
|
163
|
+
const full = context.zoom >= TASK_CARD_FULL_DETAIL_ZOOM;
|
|
198
164
|
const color = statusColor(item, context);
|
|
199
165
|
const key = context.assetKey(item);
|
|
200
166
|
if (key !== parts.assetKey) {
|
|
@@ -204,7 +170,8 @@ function sync(container, item, context) {
|
|
|
204
170
|
}
|
|
205
171
|
const texture = key ? context.getTexture(key) : null;
|
|
206
172
|
const previewFailed = Boolean(key && !texture && context.hasError(key));
|
|
207
|
-
const
|
|
173
|
+
const artifact = featuredTaskArtifact(item.snapshot.artifacts);
|
|
174
|
+
const kind = previewKindFor(artifact);
|
|
208
175
|
const surface = stateSurfaceFor(item, kind, Boolean(texture), previewFailed);
|
|
209
176
|
const frame = {
|
|
210
177
|
x: 1,
|
|
@@ -212,7 +179,7 @@ function sync(container, item, context) {
|
|
|
212
179
|
width: Math.max(1, width - 2),
|
|
213
180
|
height: Math.max(1, height - 2)
|
|
214
181
|
};
|
|
215
|
-
const metaText =
|
|
182
|
+
const metaText = artifact ? metadata(item) : "";
|
|
216
183
|
const showMeta = full && Boolean(metaText);
|
|
217
184
|
const failedWithOutput = item.snapshot.status === "failed" && kind !== "empty";
|
|
218
185
|
syncTextResolution(parts.body, parts, context.zoom);
|
|
@@ -228,7 +195,8 @@ function sync(container, item, context) {
|
|
|
228
195
|
surface,
|
|
229
196
|
showMeta,
|
|
230
197
|
failedWithOutput,
|
|
231
|
-
|
|
198
|
+
artifact?.type ?? "none",
|
|
199
|
+
artifact?.id ?? "none",
|
|
232
200
|
item.taskRunId,
|
|
233
201
|
texture ? `${texture.width}x${texture.height}` : "none",
|
|
234
202
|
context.palette.surface,
|
|
@@ -255,14 +223,14 @@ function sync(container, item, context) {
|
|
|
255
223
|
parts.previewArt.clear();
|
|
256
224
|
if (kind === "waveform") {
|
|
257
225
|
const bottomInset = showMeta ? META_HEIGHT : 0;
|
|
258
|
-
|
|
226
|
+
drawAudioWaveform(parts.previewArt, item.taskRunId, {
|
|
259
227
|
x: frame.x + PADDING,
|
|
260
228
|
y: frame.y + PADDING,
|
|
261
229
|
width: Math.max(1, frame.width - PADDING * 2),
|
|
262
230
|
height: Math.max(1, frame.height - PADDING * 2 - bottomInset)
|
|
263
231
|
}, context.colors.brand.stroke);
|
|
264
232
|
}
|
|
265
|
-
if (
|
|
233
|
+
if (full && (artifact?.type === "audio" || artifact?.type === "video" && texture)) drawPlayBadge(parts.previewArt, frame, context);
|
|
266
234
|
if (surface) {
|
|
267
235
|
const markY = frame.y + frame.height / 2 - (full && stateLabel(surface) ? 10 : 0);
|
|
268
236
|
drawStateMark(parts.previewArt, surface, frame.x + frame.width / 2, markY, surface === "failed" ? color : context.colors.neutral.stroke);
|
|
@@ -281,7 +249,7 @@ function sync(container, item, context) {
|
|
|
281
249
|
parts.preview.width = fitted.width;
|
|
282
250
|
parts.preview.height = fitted.height;
|
|
283
251
|
}
|
|
284
|
-
const bodyText = kind === "text"
|
|
252
|
+
const bodyText = kind === "text" && artifact?.type === "text" ? artifact.textExcerpt : stateLabel(surface);
|
|
285
253
|
const textSig = [
|
|
286
254
|
bodyText,
|
|
287
255
|
metaText,
|
|
@@ -386,4 +354,4 @@ const taskCardRenderer = {
|
|
|
386
354
|
}
|
|
387
355
|
};
|
|
388
356
|
//#endregion
|
|
389
|
-
export { containTaskPreviewRect, taskCardRenderer };
|
|
357
|
+
export { TASK_CARD_FULL_DETAIL_ZOOM, containTaskPreviewRect, taskCardRenderer };
|
|
@@ -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();
|