@neta-art/cohub 8.21.1 → 8.23.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/dist/board/core/connections.d.ts +18 -1
- package/dist/board/core/connections.js +43 -2
- package/dist/board/export/scene.js +2 -1
- package/dist/board/geometry.d.ts +3 -1
- package/dist/board/geometry.js +16 -1
- package/dist/board/index.d.ts +3 -3
- package/dist/board/index.js +3 -3
- package/dist/board/render/connection-layer.d.ts +6 -1
- package/dist/board/render/connection-layer.js +274 -122
- package/dist/board/task.d.ts +2 -3
- package/dist/board/task.js +25 -95
- package/dist/chunks/environment.d.ts +174 -60
- package/dist/chunks/http.d.ts +78 -8
- package/dist/chunks/http.js +12 -0
- package/dist/chunks/websocket.d.ts +1 -1
- package/dist/generation-blocks.js +125 -0
- package/dist/http.d.ts +3 -3
- package/dist/index.d.ts +123 -3
- package/dist/index.js +721 -42
- package/dist/media.d.ts +81 -0
- package/dist/media.js +177 -0
- package/package.json +5 -1
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import { BOARD_FONT_STACK } from "../../protocol/dist/board-constants.js";
|
|
2
|
-
import {
|
|
2
|
+
import { rectsIntersect } from "../geometry.js";
|
|
3
|
+
import { connectionArrowheads, createConnectionGeometryCache } from "../core/connections.js";
|
|
3
4
|
import { pickBoardColor } from "../core/palette.js";
|
|
4
|
-
import { syncTextResolution, textResolutionForZoom } from "./text-resolution.js";
|
|
5
|
+
import { syncTextResolution, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
|
|
5
6
|
import { Container, Graphics, Text } from "pixi.js";
|
|
6
7
|
//#region src/board/render/connection-layer.ts
|
|
7
8
|
/**
|
|
8
9
|
* Connection drawing.
|
|
9
10
|
*
|
|
10
|
-
* Connections render into
|
|
11
|
+
* Connections render into shared layers beneath the cards rather than as a
|
|
11
12
|
* container per relation. A connection is a thin stroke with no texture and no
|
|
12
13
|
* interactive chrome of its own, so a per-connection container would add a
|
|
13
14
|
* transform, a render group and a draw call each for geometry that batches
|
|
14
15
|
* perfectly — on a densely connected board that is the difference between a few
|
|
15
16
|
* draw calls and a few thousand.
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Tessellation is the real cost, so both layers are cached: `base` holds idle
|
|
19
|
+
* relations, `live` those touching manipulated, selected or hovered nodes.
|
|
20
|
+
* Details too small to read degrade to plain strokes; the document never changes.
|
|
21
|
+
*
|
|
22
|
+
* Labels are the exception to batching: text needs its own object to rasterise,
|
|
23
|
+
* so a `Text` is materialised only for visible connections that carry one and is
|
|
24
|
+
* pooled by connection id.
|
|
20
25
|
*/
|
|
21
26
|
/** Arrowhead length relative to stroke width, and its floor in world units. */
|
|
22
27
|
const HEAD_SCALE = 5.5;
|
|
@@ -25,85 +30,203 @@ const HEAD_SPREAD = Math.PI / 6;
|
|
|
25
30
|
/** Dash pattern for `dashed` connections, in world units. */
|
|
26
31
|
const DASH_LENGTH = 10;
|
|
27
32
|
const DASH_GAP = 7;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const LABEL_FONT_SIZE = 12;
|
|
34
|
+
/** Minimum on-screen sizes before a detail is drawn. */
|
|
35
|
+
const MIN_DASH_PX = 4;
|
|
36
|
+
const MIN_HEAD_PX = 3;
|
|
37
|
+
const MIN_LABEL_PX = 5;
|
|
38
|
+
const MIN_SEGMENT_PX = 2;
|
|
39
|
+
const MIN_ROUND_PX = 2;
|
|
40
|
+
/** Dashes per frame; past it all dashed relations draw solid (with hysteresis). */
|
|
41
|
+
const DASH_BUDGET = 4e4;
|
|
42
|
+
const DASH_RESUME_RATIO = .8;
|
|
43
|
+
function detailForZoom(zoom) {
|
|
44
|
+
const bucket = 2 ** (Math.floor(Math.log2(Math.max(zoom, 1e-4)) * 2) / 2);
|
|
45
|
+
return {
|
|
46
|
+
zoom: bucket,
|
|
47
|
+
dashes: DASH_LENGTH * bucket >= MIN_DASH_PX,
|
|
48
|
+
labels: LABEL_FONT_SIZE * bucket >= MIN_LABEL_PX,
|
|
49
|
+
minWidth: 1 / bucket,
|
|
50
|
+
minStep: MIN_SEGMENT_PX / bucket
|
|
51
|
+
};
|
|
37
52
|
}
|
|
38
|
-
/**
|
|
39
|
-
function
|
|
53
|
+
/** Visit each dash piece inside `clip`; `start` marks the first piece of a dash. */
|
|
54
|
+
function walkDashes(path, clip, visit) {
|
|
40
55
|
let penDown = true;
|
|
41
56
|
let remaining = DASH_LENGTH;
|
|
42
|
-
let
|
|
43
|
-
if (!current) return;
|
|
44
|
-
graphics.moveTo(current.x, current.y);
|
|
57
|
+
let open = false;
|
|
45
58
|
for (let index = 1; index < path.length; index += 1) {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
const a = path[index - 1];
|
|
60
|
+
const b = path[index];
|
|
61
|
+
if (!a || !b) continue;
|
|
62
|
+
const dx = b.x - a.x;
|
|
63
|
+
const dy = b.y - a.y;
|
|
64
|
+
const length = Math.hypot(dx, dy);
|
|
65
|
+
let travelled = 0;
|
|
66
|
+
while (length - travelled > 1e-4) {
|
|
67
|
+
const step = Math.min(length - travelled, remaining);
|
|
68
|
+
if (penDown) {
|
|
69
|
+
const t0 = travelled / length;
|
|
70
|
+
const t1 = (travelled + step) / length;
|
|
71
|
+
const x0 = a.x + dx * t0;
|
|
72
|
+
const y0 = a.y + dy * t0;
|
|
73
|
+
const x1 = a.x + dx * t1;
|
|
74
|
+
const y1 = a.y + dy * t1;
|
|
75
|
+
if (!clip || Math.max(x0, x1) >= clip.x && Math.min(x0, x1) <= clip.x + clip.width && Math.max(y0, y1) >= clip.y && Math.min(y0, y1) <= clip.y + clip.height) {
|
|
76
|
+
visit(x0, y0, x1, y1, !open);
|
|
77
|
+
open = true;
|
|
78
|
+
} else open = false;
|
|
79
|
+
}
|
|
80
|
+
travelled += step;
|
|
59
81
|
remaining -= step;
|
|
60
|
-
segmentRemaining -= step;
|
|
61
|
-
from = to;
|
|
62
82
|
if (remaining <= 1e-4) {
|
|
63
83
|
penDown = !penDown;
|
|
64
84
|
remaining = penDown ? DASH_LENGTH : DASH_GAP;
|
|
85
|
+
open = false;
|
|
65
86
|
}
|
|
66
87
|
}
|
|
67
|
-
current = next;
|
|
68
88
|
}
|
|
69
89
|
}
|
|
90
|
+
function countDashes(entries, clip) {
|
|
91
|
+
let count = 0;
|
|
92
|
+
for (const { connection, resolved } of entries) {
|
|
93
|
+
if (connection.style.line !== "dashed") continue;
|
|
94
|
+
walkDashes(resolved.path, clip, (_x0, _y0, _x1, _y1, start) => {
|
|
95
|
+
if (start) count += 1;
|
|
96
|
+
});
|
|
97
|
+
if (count > DASH_BUDGET) break;
|
|
98
|
+
}
|
|
99
|
+
return count;
|
|
100
|
+
}
|
|
101
|
+
function traceSolid(graphics, path, minStep) {
|
|
102
|
+
const first = path[0];
|
|
103
|
+
const last = path[path.length - 1];
|
|
104
|
+
if (!first || !last) return;
|
|
105
|
+
graphics.moveTo(first.x, first.y);
|
|
106
|
+
let previous = first;
|
|
107
|
+
for (let index = 1; index < path.length - 1; index += 1) {
|
|
108
|
+
const point = path[index];
|
|
109
|
+
if (!point) continue;
|
|
110
|
+
if (Math.abs(point.x - previous.x) + Math.abs(point.y - previous.y) < minStep) continue;
|
|
111
|
+
graphics.lineTo(point.x, point.y);
|
|
112
|
+
previous = point;
|
|
113
|
+
}
|
|
114
|
+
graphics.lineTo(last.x, last.y);
|
|
115
|
+
}
|
|
116
|
+
/** Trace an open chevron arrowhead aimed along `angle`. */
|
|
117
|
+
function traceArrowhead(graphics, tip, angle, size) {
|
|
118
|
+
graphics.moveTo(tip.x - size * Math.cos(angle - HEAD_SPREAD), tip.y - size * Math.sin(angle - HEAD_SPREAD)).lineTo(tip.x, tip.y).lineTo(tip.x - size * Math.cos(angle + HEAD_SPREAD), tip.y - size * Math.sin(angle + HEAD_SPREAD));
|
|
119
|
+
}
|
|
120
|
+
/** Stroke each style once instead of once per relation. */
|
|
121
|
+
function createStrokeBatch(graphics) {
|
|
122
|
+
const groups = /* @__PURE__ */ new Map();
|
|
123
|
+
return {
|
|
124
|
+
add(style, trace) {
|
|
125
|
+
const key = `${style.color}|${style.width}|${style.alpha}|${style.cap}|${style.join}`;
|
|
126
|
+
const group = groups.get(key);
|
|
127
|
+
if (group) group.traces.push(trace);
|
|
128
|
+
else groups.set(key, {
|
|
129
|
+
style,
|
|
130
|
+
traces: [trace]
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
flush() {
|
|
134
|
+
for (const { style, traces } of groups.values()) {
|
|
135
|
+
for (const trace of traces) trace();
|
|
136
|
+
graphics.stroke(style);
|
|
137
|
+
}
|
|
138
|
+
groups.clear();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function drawConnections(graphics, entries, states, input, detail, clip, dashes) {
|
|
143
|
+
const batch = createStrokeBatch(graphics);
|
|
144
|
+
entries.forEach(({ connection, resolved }, index) => {
|
|
145
|
+
const state = states[index] ?? "idle";
|
|
146
|
+
const color = pickBoardColor(input.colors, connection.style.color, input.colorScheme);
|
|
147
|
+
const width = Math.max(connection.style.size + (state === "selected" ? 1 : 0), detail.minWidth);
|
|
148
|
+
const round = width * detail.zoom >= MIN_ROUND_PX;
|
|
149
|
+
const alpha = state === "idle" ? .9 : 1;
|
|
150
|
+
const path = resolved.path;
|
|
151
|
+
const dashed = dashes && connection.style.line === "dashed";
|
|
152
|
+
batch.add({
|
|
153
|
+
color: color.stroke,
|
|
154
|
+
width,
|
|
155
|
+
alpha,
|
|
156
|
+
cap: dashed || !round ? "butt" : "round",
|
|
157
|
+
join: round ? "round" : "miter"
|
|
158
|
+
}, dashed ? () => walkDashes(path, clip, (x0, y0, x1, y1, start) => {
|
|
159
|
+
if (start) graphics.moveTo(x0, y0);
|
|
160
|
+
graphics.lineTo(x1, y1);
|
|
161
|
+
}) : () => traceSolid(graphics, path, detail.minStep));
|
|
162
|
+
const headSize = Math.max(HEAD_MIN, connection.style.size * HEAD_SCALE);
|
|
163
|
+
if (headSize * detail.zoom < MIN_HEAD_PX) return;
|
|
164
|
+
const heads = connectionArrowheads(connection);
|
|
165
|
+
const headStyle = {
|
|
166
|
+
color: color.stroke,
|
|
167
|
+
width,
|
|
168
|
+
alpha,
|
|
169
|
+
cap: round ? "round" : "butt",
|
|
170
|
+
join: round ? "round" : "miter"
|
|
171
|
+
};
|
|
172
|
+
const tip = path[path.length - 1];
|
|
173
|
+
const beforeTip = path[path.length - 2];
|
|
174
|
+
if (heads.atTarget && tip && beforeTip) {
|
|
175
|
+
const angle = Math.atan2(tip.y - beforeTip.y, tip.x - beforeTip.x);
|
|
176
|
+
batch.add(headStyle, () => traceArrowhead(graphics, tip, angle, headSize));
|
|
177
|
+
}
|
|
178
|
+
const origin = path[0];
|
|
179
|
+
const afterOrigin = path[1];
|
|
180
|
+
if (heads.atSource && origin && afterOrigin) {
|
|
181
|
+
const angle = Math.atan2(origin.y - afterOrigin.y, origin.x - afterOrigin.x);
|
|
182
|
+
batch.add(headStyle, () => traceArrowhead(graphics, origin, angle, headSize));
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
batch.flush();
|
|
186
|
+
}
|
|
187
|
+
function isCurrent(drawn, entries, states, view, colors) {
|
|
188
|
+
return drawn !== null && drawn.view === view && drawn.colors === colors && sameItems(drawn.entries, entries) && sameItems(drawn.states, states);
|
|
189
|
+
}
|
|
190
|
+
function sameItems(a, b) {
|
|
191
|
+
if (a.length !== b.length) return false;
|
|
192
|
+
for (let index = 0; index < a.length; index += 1) if (a[index] !== b[index]) return false;
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
70
195
|
function createConnectionLayer(options) {
|
|
71
|
-
let
|
|
72
|
-
|
|
196
|
+
let layers = null;
|
|
197
|
+
const geometry = createConnectionGeometryCache();
|
|
73
198
|
const labels = /* @__PURE__ */ new Map();
|
|
74
|
-
let
|
|
199
|
+
let lastConnections = null;
|
|
200
|
+
let drawnBase = null;
|
|
201
|
+
let drawnLive = null;
|
|
202
|
+
let withinBudget = true;
|
|
75
203
|
function ensureAttached() {
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
204
|
+
if (layers) return layers;
|
|
205
|
+
const next = {
|
|
206
|
+
base: new Graphics({ label: "board-connections" }),
|
|
207
|
+
live: new Graphics({ label: "board-connections-live" }),
|
|
208
|
+
labels: new Container({ label: "board-connection-labels" })
|
|
79
209
|
};
|
|
80
|
-
const nextGraphics = new Graphics({ label: "board-connections" });
|
|
81
|
-
const nextLabels = new Container({ label: "board-connection-labels" });
|
|
82
210
|
if (options.zIndex !== void 0) {
|
|
83
|
-
|
|
84
|
-
|
|
211
|
+
next.base.zIndex = options.zIndex;
|
|
212
|
+
next.live.zIndex = options.zIndex;
|
|
213
|
+
next.labels.zIndex = options.zIndex;
|
|
85
214
|
}
|
|
86
|
-
options.parent.addChild(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
graphics: nextGraphics,
|
|
91
|
-
labelLayer: nextLabels
|
|
92
|
-
};
|
|
215
|
+
options.parent.addChild(next.base, next.live, next.labels);
|
|
216
|
+
layers = next;
|
|
217
|
+
return next;
|
|
93
218
|
}
|
|
94
219
|
function releaseLabel(connectionId) {
|
|
95
220
|
const entry = labels.get(connectionId);
|
|
96
221
|
if (!entry) return;
|
|
97
|
-
|
|
222
|
+
layers?.labels.removeChild(entry.text);
|
|
98
223
|
entry.text.destroy();
|
|
99
224
|
labels.delete(connectionId);
|
|
100
225
|
}
|
|
101
|
-
function syncLabel(connection, resolved, input,
|
|
226
|
+
function syncLabel({ connection, resolved }, input, host) {
|
|
102
227
|
const value = connection.label.trim();
|
|
103
|
-
if (!value)
|
|
104
|
-
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
228
|
+
if (!value) return false;
|
|
229
|
+
const color = pickBoardColor(input.colors, connection.style.color, input.colorScheme).label;
|
|
107
230
|
let entry = labels.get(connection.id);
|
|
108
231
|
if (!entry) {
|
|
109
232
|
const resolution = textResolutionForZoom(input.zoom);
|
|
@@ -112,7 +235,7 @@ function createConnectionLayer(options) {
|
|
|
112
235
|
style: {
|
|
113
236
|
fill: color,
|
|
114
237
|
fontFamily: BOARD_FONT_STACK,
|
|
115
|
-
fontSize:
|
|
238
|
+
fontSize: LABEL_FONT_SIZE,
|
|
116
239
|
fontWeight: "500"
|
|
117
240
|
},
|
|
118
241
|
resolution,
|
|
@@ -135,82 +258,111 @@ function createConnectionLayer(options) {
|
|
|
135
258
|
entry.text.style.fill = color;
|
|
136
259
|
}
|
|
137
260
|
entry.text.position.set(resolved.mid.x, resolved.mid.y);
|
|
261
|
+
return true;
|
|
138
262
|
}
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
263
|
+
function drawLayer(graphics, layer, input, detail, clip) {
|
|
264
|
+
graphics.clear();
|
|
265
|
+
drawConnections(graphics, layer.entries, layer.states, input, detail, clip, layer.dashes);
|
|
266
|
+
const labelIds = /* @__PURE__ */ new Set();
|
|
267
|
+
if (detail.labels && layers) {
|
|
268
|
+
for (const entry of layer.entries) if (syncLabel(entry, input, layers.labels)) labelIds.add(entry.connection.id);
|
|
143
269
|
}
|
|
270
|
+
return {
|
|
271
|
+
...layer,
|
|
272
|
+
labelIds
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function sync(input) {
|
|
276
|
+
if (input.connections.length === 0 && !layers) return;
|
|
144
277
|
const host = ensureAttached();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
278
|
+
if (input.connections !== lastConnections) {
|
|
279
|
+
geometry.retain(input.connections);
|
|
280
|
+
lastConnections = input.connections;
|
|
281
|
+
}
|
|
282
|
+
const detail = detailForZoom(input.zoom);
|
|
283
|
+
const cull = input.cullRect ?? null;
|
|
284
|
+
const clip = detail.dashes ? cull : null;
|
|
285
|
+
const selected = input.selectedIds;
|
|
286
|
+
const liveNodes = input.liveNodeIds;
|
|
287
|
+
const baseEntries = [];
|
|
288
|
+
const baseStates = [];
|
|
289
|
+
const liveEntries = [];
|
|
290
|
+
const liveStates = [];
|
|
291
|
+
let hovered = null;
|
|
150
292
|
for (const connection of input.connections) {
|
|
151
|
-
const
|
|
152
|
-
if (!
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
else {
|
|
165
|
-
const first = resolved.path[0];
|
|
166
|
-
if (!first) continue;
|
|
167
|
-
host.graphics.moveTo(first.x, first.y);
|
|
168
|
-
for (let index = 1; index < resolved.path.length; index += 1) {
|
|
169
|
-
const point = resolved.path[index];
|
|
170
|
-
if (point) host.graphics.lineTo(point.x, point.y);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
host.graphics.stroke({
|
|
174
|
-
color: color.stroke,
|
|
175
|
-
width,
|
|
176
|
-
alpha,
|
|
177
|
-
cap: "round",
|
|
178
|
-
join: "round"
|
|
179
|
-
});
|
|
180
|
-
const heads = connectionArrowheads(connection);
|
|
181
|
-
const headSize = Math.max(HEAD_MIN, connection.style.size * HEAD_SCALE);
|
|
182
|
-
if (heads.atTarget) {
|
|
183
|
-
const tip = resolved.path[resolved.path.length - 1];
|
|
184
|
-
const previous = resolved.path[resolved.path.length - 2];
|
|
185
|
-
if (tip && previous) drawArrowhead(host.graphics, tip, Math.atan2(tip.y - previous.y, tip.x - previous.x), headSize, color.stroke, width, alpha);
|
|
293
|
+
const entry = geometry.get(connection, input.getFrame);
|
|
294
|
+
if (!entry || input.skipIds?.has(connection.id)) continue;
|
|
295
|
+
if (cull && !rectsIntersect(entry.bounds, cull)) continue;
|
|
296
|
+
if (selected?.has(connection.id)) {
|
|
297
|
+
liveEntries.push(entry);
|
|
298
|
+
liveStates.push("selected");
|
|
299
|
+
} else if (liveNodes?.has(connection.source.itemId) || liveNodes?.has(connection.target.itemId)) {
|
|
300
|
+
liveEntries.push(entry);
|
|
301
|
+
liveStates.push(connection.id === input.hoveredId ? "hovered" : "idle");
|
|
302
|
+
} else {
|
|
303
|
+
baseEntries.push(entry);
|
|
304
|
+
baseStates.push("idle");
|
|
305
|
+
if (connection.id === input.hoveredId) hovered = entry;
|
|
186
306
|
}
|
|
187
|
-
if (heads.atSource) {
|
|
188
|
-
const tip = resolved.path[0];
|
|
189
|
-
const next2 = resolved.path[1];
|
|
190
|
-
if (tip && next2) drawArrowhead(host.graphics, tip, Math.atan2(tip.y - next2.y, tip.x - next2.x), headSize, color.stroke, width, alpha);
|
|
191
|
-
}
|
|
192
|
-
syncLabel(connection, resolved, input, color.label, host.labelLayer);
|
|
193
307
|
}
|
|
194
|
-
|
|
195
|
-
|
|
308
|
+
if (hovered) {
|
|
309
|
+
liveEntries.push(hovered);
|
|
310
|
+
liveStates.push("hovered");
|
|
311
|
+
}
|
|
312
|
+
const view = `${detail.zoom}|${textZoomBucket(input.zoom)}|${input.colorScheme}|${clip ? `${clip.x},${clip.y},${clip.width},${clip.height}` : ""}`;
|
|
313
|
+
const baseCurrent = isCurrent(drawnBase, baseEntries, baseStates, view, input.colors);
|
|
314
|
+
const liveCurrent = isCurrent(drawnLive, liveEntries, liveStates, view, input.colors);
|
|
315
|
+
const countFor = (current, drawn, entries) => !detail.dashes ? 0 : current && drawn ? drawn.dashCount : countDashes(entries, clip);
|
|
316
|
+
const baseDashes = countFor(baseCurrent, drawnBase, baseEntries);
|
|
317
|
+
const liveDashes = countFor(liveCurrent, drawnLive, hovered ? liveEntries.slice(0, -1) : liveEntries);
|
|
318
|
+
const total = baseDashes + liveDashes;
|
|
319
|
+
withinBudget = withinBudget ? total <= DASH_BUDGET : total <= DASH_BUDGET * DASH_RESUME_RATIO;
|
|
320
|
+
const dashes = detail.dashes && withinBudget;
|
|
321
|
+
const redraw = (graphics, entries, states, dashCount) => drawLayer(graphics, {
|
|
322
|
+
entries,
|
|
323
|
+
states,
|
|
324
|
+
view,
|
|
325
|
+
colors: input.colors,
|
|
326
|
+
dashCount,
|
|
327
|
+
dashes
|
|
328
|
+
}, input, detail, clip);
|
|
329
|
+
let labelsChanged = false;
|
|
330
|
+
if (!baseCurrent || drawnBase?.dashes !== dashes) {
|
|
331
|
+
drawnBase = redraw(host.base, baseEntries, baseStates, baseDashes);
|
|
332
|
+
labelsChanged = true;
|
|
333
|
+
}
|
|
334
|
+
if (!liveCurrent || drawnLive?.dashes !== dashes) {
|
|
335
|
+
drawnLive = redraw(host.live, liveEntries, liveStates, liveDashes);
|
|
336
|
+
labelsChanged = true;
|
|
337
|
+
}
|
|
338
|
+
if (!labelsChanged) return;
|
|
339
|
+
const wanted = /* @__PURE__ */ new Set([...drawnBase?.labelIds ?? [], ...drawnLive?.labelIds ?? []]);
|
|
340
|
+
for (const connectionId of [...labels.keys()]) if (!wanted.has(connectionId)) releaseLabel(connectionId);
|
|
196
341
|
}
|
|
197
342
|
return {
|
|
198
343
|
sync,
|
|
199
|
-
resolved: (connectionId) =>
|
|
344
|
+
resolved: (connectionId) => geometry.peek(connectionId)?.resolved ?? null,
|
|
200
345
|
get children() {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
346
|
+
return layers ? [
|
|
347
|
+
layers.base,
|
|
348
|
+
layers.live,
|
|
349
|
+
layers.labels
|
|
350
|
+
] : [];
|
|
205
351
|
},
|
|
206
352
|
destroy: () => {
|
|
207
353
|
for (const entry of labels.values()) entry.text.destroy();
|
|
208
354
|
labels.clear();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
355
|
+
if (layers) {
|
|
356
|
+
layers.base.destroy();
|
|
357
|
+
layers.live.destroy();
|
|
358
|
+
layers.labels.destroy({ children: true });
|
|
359
|
+
}
|
|
360
|
+
layers = null;
|
|
361
|
+
drawnBase = null;
|
|
362
|
+
drawnLive = null;
|
|
363
|
+
withinBudget = true;
|
|
364
|
+
lastConnections = null;
|
|
365
|
+
geometry.clear();
|
|
214
366
|
}
|
|
215
367
|
};
|
|
216
368
|
}
|
package/dist/board/task.d.ts
CHANGED
|
@@ -2,9 +2,8 @@ import { BoardTaskArtifact, BoardTaskSnapshot } from "../protocol/dist/board-doc
|
|
|
2
2
|
import { TaskRunRecord } from "../types.js";
|
|
3
3
|
//#region src/board/task.d.ts
|
|
4
4
|
/**
|
|
5
|
-
* Group provider blocks into user-facing works. A cover
|
|
6
|
-
*
|
|
7
|
-
* competing image result.
|
|
5
|
+
* Group provider blocks into user-facing works. A cover belongs to the media
|
|
6
|
+
* it depicts (see `generationCovers`) instead of becoming a competing image.
|
|
8
7
|
*/
|
|
9
8
|
declare function taskArtifacts(blocks: Record<string, unknown>[]): BoardTaskArtifact[];
|
|
10
9
|
/** Highest-value artifact first, with provider order as the stable final tie. */
|
package/dist/board/task.js
CHANGED
|
@@ -1,80 +1,6 @@
|
|
|
1
|
-
import { normalizeBoardRemoteUrl } from "../protocol/dist/board-url.js";
|
|
2
1
|
import "../protocol/dist/board-document.js";
|
|
2
|
+
import { blockDurationMs, blockIdentity, blockMimeType, blockNaturalSize, blockPreviewUrl, blockText, blockTitle, blockUrl, cleanExcerpt, generationCovers, generationOutput, generationPrompt, taskData } from "../generation-blocks.js";
|
|
3
3
|
//#region src/board/task.ts
|
|
4
|
-
const EXCERPT_LIMIT = 240;
|
|
5
|
-
function record(value) {
|
|
6
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
7
|
-
}
|
|
8
|
-
function cleanExcerpt(value, limit = EXCERPT_LIMIT) {
|
|
9
|
-
if (typeof value !== "string") return void 0;
|
|
10
|
-
const clean = value.replace(/\s+/g, " ").trim();
|
|
11
|
-
if (!clean) return void 0;
|
|
12
|
-
return clean.length > limit ? `${clean.slice(0, limit - 3).trimEnd()}...` : clean;
|
|
13
|
-
}
|
|
14
|
-
function blockText(block) {
|
|
15
|
-
return cleanExcerpt(block.text ?? block.content ?? block.value);
|
|
16
|
-
}
|
|
17
|
-
function blockUrl(block) {
|
|
18
|
-
const source = record(block.source);
|
|
19
|
-
return normalizeBoardRemoteUrl(source?.url ?? source?.src ?? block.url ?? block.src);
|
|
20
|
-
}
|
|
21
|
-
function blockMimeType(block) {
|
|
22
|
-
const source = record(block.source);
|
|
23
|
-
const value = source?.mediaType ?? source?.media_type ?? source?.mimeType ?? block.mediaType ?? block.media_type ?? block.mimeType;
|
|
24
|
-
return typeof value === "string" ? value : void 0;
|
|
25
|
-
}
|
|
26
|
-
function positiveNumber(value) {
|
|
27
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
28
|
-
}
|
|
29
|
-
function blockNaturalSize(block) {
|
|
30
|
-
const source = record(block.source);
|
|
31
|
-
const naturalWidth = positiveNumber(source?.width ?? source?.naturalWidth ?? block.width ?? block.naturalWidth);
|
|
32
|
-
const naturalHeight = positiveNumber(source?.height ?? source?.naturalHeight ?? block.height ?? block.naturalHeight);
|
|
33
|
-
return {
|
|
34
|
-
...naturalWidth ? { naturalWidth } : {},
|
|
35
|
-
...naturalHeight ? { naturalHeight } : {}
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
function contentBlocks(value) {
|
|
39
|
-
return Array.isArray(value) ? value.filter((item) => record(item)) : [];
|
|
40
|
-
}
|
|
41
|
-
function taskData(run) {
|
|
42
|
-
const payload = record(run.payload);
|
|
43
|
-
return record(payload?.data) ?? payload;
|
|
44
|
-
}
|
|
45
|
-
function generationOutput(run) {
|
|
46
|
-
return contentBlocks(record(run.result)?.output);
|
|
47
|
-
}
|
|
48
|
-
function generationPrompt(run) {
|
|
49
|
-
for (const block of contentBlocks(taskData(run)?.content)) {
|
|
50
|
-
const text = blockText(block);
|
|
51
|
-
if (text) return text;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
function blockMeta(block) {
|
|
55
|
-
return record(block.meta);
|
|
56
|
-
}
|
|
57
|
-
function blockIdentity(block) {
|
|
58
|
-
const meta = blockMeta(block);
|
|
59
|
-
const value = meta?.id ?? meta?.clip_id ?? meta?.clipId ?? block.id ?? block.clip_id ?? block.clipId;
|
|
60
|
-
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
61
|
-
return cleanExcerpt(String(value), 220);
|
|
62
|
-
}
|
|
63
|
-
function blockTitle(block) {
|
|
64
|
-
return cleanExcerpt(blockMeta(block)?.title ?? block.title ?? block.name, 240);
|
|
65
|
-
}
|
|
66
|
-
function blockDurationMs(block) {
|
|
67
|
-
const source = record(block.source);
|
|
68
|
-
const meta = blockMeta(block);
|
|
69
|
-
const milliseconds = positiveNumber(source?.durationMs ?? meta?.durationMs ?? block.durationMs);
|
|
70
|
-
if (milliseconds) return Math.round(milliseconds);
|
|
71
|
-
const seconds = positiveNumber(source?.duration ?? meta?.duration ?? block.duration);
|
|
72
|
-
return seconds ? Math.round(seconds * 1e3) : void 0;
|
|
73
|
-
}
|
|
74
|
-
function blockPreviewUrl(block) {
|
|
75
|
-
const source = record(block.source);
|
|
76
|
-
return normalizeBoardRemoteUrl(source?.poster ?? source?.thumbnail ?? source?.previewUrl ?? block.poster ?? block.thumbnail ?? block.previewUrl);
|
|
77
|
-
}
|
|
78
4
|
function artifactId(base, used, suffix) {
|
|
79
5
|
const stem = cleanExcerpt(base, 220) ?? "output";
|
|
80
6
|
let candidate = suffix ? `${stem}-${suffix}` : stem;
|
|
@@ -87,36 +13,41 @@ function artifactId(base, used, suffix) {
|
|
|
87
13
|
return candidate;
|
|
88
14
|
}
|
|
89
15
|
/**
|
|
90
|
-
* Group provider blocks into user-facing works. A cover
|
|
91
|
-
*
|
|
92
|
-
* competing image result.
|
|
16
|
+
* Group provider blocks into user-facing works. A cover belongs to the media
|
|
17
|
+
* it depicts (see `generationCovers`) instead of becoming a competing image.
|
|
93
18
|
*/
|
|
94
19
|
function taskArtifacts(blocks) {
|
|
20
|
+
const covers = new Map([...generationCovers(blocks)].filter(([media]) => Boolean(blockUrl(blocks[media] ?? {}))));
|
|
21
|
+
const coverIndexes = new Set(covers.values());
|
|
22
|
+
const coverUrl = (index) => {
|
|
23
|
+
const cover = covers.get(index);
|
|
24
|
+
return cover === void 0 ? void 0 : blockUrl(blocks[cover] ?? {});
|
|
25
|
+
};
|
|
95
26
|
const groups = /* @__PURE__ */ new Map();
|
|
96
27
|
blocks.forEach((block, index) => {
|
|
28
|
+
if (coverIndexes.has(index)) return;
|
|
97
29
|
const key = blockIdentity(block) ?? `output-${index + 1}`;
|
|
30
|
+
const entry = {
|
|
31
|
+
block,
|
|
32
|
+
index,
|
|
33
|
+
url: blockUrl(block)
|
|
34
|
+
};
|
|
98
35
|
const group = groups.get(key);
|
|
99
|
-
if (group) group.push(
|
|
100
|
-
else groups.set(key, [
|
|
36
|
+
if (group) group.push(entry);
|
|
37
|
+
else groups.set(key, [entry]);
|
|
101
38
|
});
|
|
39
|
+
const withUrl = (entry) => Boolean(entry.url);
|
|
102
40
|
const artifacts = [];
|
|
103
41
|
const usedIds = /* @__PURE__ */ new Set();
|
|
104
42
|
for (const [groupId, blocksInGroup] of groups) {
|
|
105
|
-
const images = blocksInGroup.filter((block) => block.type === "image").
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
})).filter((entry) => Boolean(entry.url));
|
|
109
|
-
const media = blocksInGroup.filter((block) => block.type === "video" || block.type === "audio").map((block) => ({
|
|
110
|
-
block,
|
|
111
|
-
url: blockUrl(block)
|
|
112
|
-
})).filter((entry) => Boolean(entry.url));
|
|
113
|
-
const pairedPreview = images[0];
|
|
114
|
-
media.forEach(({ block, url }, mediaIndex) => {
|
|
43
|
+
const images = blocksInGroup.filter(({ block }) => block.type === "image").filter(withUrl);
|
|
44
|
+
const media = blocksInGroup.filter(({ block }) => block.type === "video" || block.type === "audio").filter(withUrl);
|
|
45
|
+
media.forEach(({ block, index, url }, mediaIndex) => {
|
|
115
46
|
const type = block.type;
|
|
116
47
|
const mimeType = blockMimeType(block);
|
|
117
48
|
const title = blockTitle(block);
|
|
118
49
|
const durationMs = blockDurationMs(block);
|
|
119
|
-
const previewUrl = blockPreviewUrl(block) ??
|
|
50
|
+
const previewUrl = blockPreviewUrl(block) ?? coverUrl(index);
|
|
120
51
|
const id = artifactId(groupId, usedIds, media.length > 1 ? `${type}-${mediaIndex + 1}` : void 0);
|
|
121
52
|
if (type === "video") {
|
|
122
53
|
artifacts.push({
|
|
@@ -141,12 +72,11 @@ function taskArtifacts(blocks) {
|
|
|
141
72
|
...durationMs ? { durationMs } : {}
|
|
142
73
|
});
|
|
143
74
|
});
|
|
144
|
-
|
|
145
|
-
images.slice(firstUnpairedImage).forEach(({ block, url }, imageIndex) => {
|
|
75
|
+
images.forEach(({ block, url }, imageIndex) => {
|
|
146
76
|
const mimeType = blockMimeType(block);
|
|
147
77
|
const title = blockTitle(block);
|
|
148
78
|
artifacts.push({
|
|
149
|
-
id: artifactId(groupId, usedIds, images.length
|
|
79
|
+
id: artifactId(groupId, usedIds, images.length > 1 ? `image-${imageIndex + 1}` : void 0),
|
|
150
80
|
type: "image",
|
|
151
81
|
url,
|
|
152
82
|
...title ? { title } : {},
|
|
@@ -154,7 +84,7 @@ function taskArtifacts(blocks) {
|
|
|
154
84
|
...blockNaturalSize(block)
|
|
155
85
|
});
|
|
156
86
|
});
|
|
157
|
-
blocksInGroup.filter((block) => block.type === "text").forEach((block, textIndex, texts) => {
|
|
87
|
+
blocksInGroup.filter(({ block }) => block.type === "text").forEach(({ block }, textIndex, texts) => {
|
|
158
88
|
const textExcerpt = blockText(block);
|
|
159
89
|
if (!textExcerpt) return;
|
|
160
90
|
const title = blockTitle(block);
|