@neta-art/cohub 3.1.0 → 4.0.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 +33 -2
- package/dist/{board.d.ts → board/animation.d.ts} +4 -2
- package/dist/{chunks/board.js → board/animation.js} +5 -44
- package/dist/board/codec.d.ts +25 -0
- package/dist/board/codec.js +243 -0
- package/dist/board/core/bindings.d.ts +45 -0
- package/dist/board/core/bindings.js +162 -0
- package/dist/board/core/draw-geometry.d.ts +30 -0
- package/dist/board/core/draw-geometry.js +175 -0
- package/dist/board/core/export-assets.d.ts +14 -0
- package/dist/board/core/export-assets.js +22 -0
- package/dist/board/core/export-plan.d.ts +84 -0
- package/dist/board/core/export-plan.js +128 -0
- package/dist/board/core/file-preview.d.ts +178 -0
- package/dist/board/core/file-preview.js +282 -0
- package/dist/board/core/palette.d.ts +42 -0
- package/dist/board/core/palette.js +166 -0
- package/dist/board/core/shape-definition.d.ts +35 -0
- package/dist/board/core/shape-definition.js +64 -0
- package/dist/board/core/shape-types.d.ts +140 -0
- package/dist/board/core/shape-types.js +27 -0
- package/dist/board/core/text-metrics.d.ts +18 -0
- package/dist/board/core/text-metrics.js +43 -0
- package/dist/board/core/tool-styles.d.ts +52 -0
- package/dist/board/core/tool-styles.js +51 -0
- package/dist/board/export/index.d.ts +56 -0
- package/dist/board/export/index.js +86 -0
- package/dist/board/export/scene.d.ts +35 -0
- package/dist/board/export/scene.js +69 -0
- package/dist/board/geometry.d.ts +139 -0
- package/dist/board/geometry.js +492 -0
- package/dist/board/headless/index.d.ts +69 -0
- package/dist/board/headless/index.js +150 -0
- package/dist/board/image-key.d.ts +19 -0
- package/dist/board/image-key.js +34 -0
- package/dist/board/index.d.ts +17 -0
- package/dist/board/index.js +17 -0
- package/dist/board/render/index.d.ts +6 -0
- package/dist/board/render/index.js +6 -0
- package/dist/board/render/palette.d.ts +5 -0
- package/dist/board/render/palette.js +30 -0
- package/dist/board/render/renderers/arrow-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/arrow-card-renderer.js +157 -0
- package/dist/board/render/renderers/base-card-renderer.d.ts +46 -0
- package/dist/board/render/renderers/base-card-renderer.js +153 -0
- package/dist/board/render/renderers/board-renderer-registry.d.ts +85 -0
- package/dist/board/render/renderers/board-renderer-registry.js +41 -0
- package/dist/board/render/renderers/draw-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/draw-card-renderer.js +82 -0
- package/dist/board/render/renderers/far-plate.d.ts +39 -0
- package/dist/board/render/renderers/far-plate.js +92 -0
- package/dist/board/render/renderers/file-card-renderer.d.ts +40 -0
- package/dist/board/render/renderers/file-card-renderer.js +350 -0
- package/dist/board/render/renderers/frame-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/frame-card-renderer.js +99 -0
- package/dist/board/render/renderers/geo-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/geo-card-renderer.js +128 -0
- package/dist/board/render/renderers/image-card-renderer.d.ts +8 -0
- package/dist/board/render/renderers/image-card-renderer.js +138 -0
- package/dist/board/render/renderers/text-card-renderer.d.ts +7 -0
- package/dist/board/render/renderers/text-card-renderer.js +94 -0
- package/dist/board/render/renderers/unknown-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/unknown-card-renderer.js +102 -0
- package/dist/board/render/renderers/video-card-renderer.d.ts +7 -0
- package/dist/board/render/renderers/video-card-renderer.js +146 -0
- package/dist/board/render/text-measurement.d.ts +30 -0
- package/dist/board/render/text-measurement.js +57 -0
- package/dist/board/render/text-resolution.d.ts +16 -0
- package/dist/board/render/text-resolution.js +43 -0
- package/dist/board/render/themes/board-theme-registry.d.ts +22 -0
- package/dist/board/render/themes/board-theme-registry.js +13 -0
- package/dist/board/render/themes/clean-theme.d.ts +5 -0
- package/dist/board/render/themes/clean-theme.js +105 -0
- package/dist/chunks/http.d.ts +2 -3
- package/dist/chunks/http.js +4 -0
- package/dist/chunks/websocket.d.ts +851 -353
- package/dist/http.d.ts +1 -2
- package/dist/index.d.ts +100 -5
- package/dist/index.js +391 -1
- package/dist/protocol/dist/board-constants.d.ts +23 -0
- package/dist/protocol/dist/board-constants.js +61 -0
- package/dist/protocol/dist/board-document.d.ts +1047 -0
- package/dist/protocol/dist/board-document.js +321 -0
- package/dist/protocol/dist/board.d.ts +186 -0
- package/dist/protocol/dist/board.js +207 -0
- package/dist/protocol/dist/index.d.ts +5 -0
- package/dist/protocol/dist/index.js +5 -0
- package/dist/protocol/dist/provenance.js +12 -0
- package/dist/protocol/dist/realtime/board-awareness.d.ts +1 -0
- package/dist/protocol/dist/realtime/board-awareness.js +119 -0
- package/dist/protocol/dist/realtime/types.d.ts +2 -0
- package/package.json +40 -4
- package/dist/board.js +0 -2
- package/dist/chunks/board.d.ts +0 -2589
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
+
import { fileBaseName, filePreviewKind } from "../../core/file-preview.js";
|
|
3
|
+
import { syncTextResolution } from "../text-resolution.js";
|
|
4
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
5
|
+
import { positionShell } from "./base-card-renderer.js";
|
|
6
|
+
import { CanvasTextMetrics, Container, Graphics, Sprite, Text, Texture } from "pixi.js";
|
|
7
|
+
//#region src/board/render/renderers/file-card-renderer.ts
|
|
8
|
+
/**
|
|
9
|
+
* File card renderer.
|
|
10
|
+
*
|
|
11
|
+
* The card is an *entry point* to a workspace file, so it shows only what helps
|
|
12
|
+
* you recognise and pick it: an optional cover, the name, a couple of lines of
|
|
13
|
+
* excerpt. Everything else belongs to the file preview that opens
|
|
14
|
+
* when the card is activated.
|
|
15
|
+
*
|
|
16
|
+
* Cost is managed through three levels of detail keyed on zoom, because a board
|
|
17
|
+
* is expected to hold thousands of these. Pixi `Text` is a rasterised texture,
|
|
18
|
+
* so below roughly one CSS pixel per glyph it can only ever resolve to a smudge;
|
|
19
|
+
* the tiers below simply stop paying for what cannot be read anyway.
|
|
20
|
+
*
|
|
21
|
+
* Absolute floor: content never paints outside the node frame. A rounded clip
|
|
22
|
+
* mask is the hard boundary; line-clamped title/excerpt keep the interior tidy.
|
|
23
|
+
*/
|
|
24
|
+
const RADIUS = 4;
|
|
25
|
+
const PADDING = 10;
|
|
26
|
+
const COVER_RATIO = .56;
|
|
27
|
+
/** Minimum body height reserved under a cover so the title stays readable. */
|
|
28
|
+
const MIN_BODY_FOR_COVER = 38;
|
|
29
|
+
const TITLE_SIZE = 13;
|
|
30
|
+
const TITLE_LINE = TITLE_SIZE * 1.35;
|
|
31
|
+
const TITLE_MAX_LINES = 2;
|
|
32
|
+
const EXCERPT_SIZE = 11;
|
|
33
|
+
const EXCERPT_LINE = EXCERPT_SIZE * 1.45;
|
|
34
|
+
const EXCERPT_MAX_LINES = 4;
|
|
35
|
+
const GAP = 4;
|
|
36
|
+
/** Zoom below which the title is dropped (glyphs are sub-pixel). */
|
|
37
|
+
const LOD_TITLE_ZOOM = .35;
|
|
38
|
+
/** Zoom below which the excerpt is dropped. */
|
|
39
|
+
const LOD_BODY_ZOOM = .6;
|
|
40
|
+
/** Upper bound on dashes in the unavailable-state edge, regardless of width. */
|
|
41
|
+
const MAX_DASHES = 40;
|
|
42
|
+
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
43
|
+
/** Detail tier for a zoom level. */
|
|
44
|
+
function detailFor(zoom) {
|
|
45
|
+
if (zoom < LOD_TITLE_ZOOM) return "plate";
|
|
46
|
+
if (zoom < LOD_BODY_ZOOM) return "title";
|
|
47
|
+
return "full";
|
|
48
|
+
}
|
|
49
|
+
/** Cover band height for a card, or 0 when it has no cover. */
|
|
50
|
+
function coverHeight(item, height) {
|
|
51
|
+
if (filePreviewKind(item.snapshot) !== "cover") return 0;
|
|
52
|
+
const ideal = Math.round(height * COVER_RATIO);
|
|
53
|
+
return Math.max(0, Math.min(ideal, height - MIN_BODY_FOR_COVER));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Join already-wrapped lines and append an ellipsis to the last one.
|
|
57
|
+
* Pure helper so the clamp can be unit-tested without a Pixi canvas.
|
|
58
|
+
*/
|
|
59
|
+
function ellipsizeWrappedLines(lines, maxLines) {
|
|
60
|
+
if (maxLines <= 0 || lines.length === 0) return "";
|
|
61
|
+
if (lines.length <= maxLines) return lines.join("\n");
|
|
62
|
+
const kept = lines.slice(0, maxLines);
|
|
63
|
+
const lastIndex = kept.length - 1;
|
|
64
|
+
const last = (kept[lastIndex] ?? "").replace(/\s+$/u, "");
|
|
65
|
+
kept[lastIndex] = last ? `${last}…` : "…";
|
|
66
|
+
return kept.join("\n");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Shorten `line` so `line + "…"` fits in `wrapWidth`.
|
|
70
|
+
*
|
|
71
|
+
* Binary search over the prefix: O(log n) single-line measures. Callers pass a
|
|
72
|
+
* measure callback so this stays free of Pixi types and easy to unit-test.
|
|
73
|
+
*/
|
|
74
|
+
function fitLineWithEllipsis(line, wrapWidth, measureWidth) {
|
|
75
|
+
const trimmed = line.replace(/\s+$/u, "");
|
|
76
|
+
if (!trimmed) return "…";
|
|
77
|
+
const full = `${trimmed}…`;
|
|
78
|
+
if (measureWidth(full) <= wrapWidth) return full;
|
|
79
|
+
let lo = 0;
|
|
80
|
+
let hi = trimmed.length;
|
|
81
|
+
let best = "…";
|
|
82
|
+
while (lo <= hi) {
|
|
83
|
+
const mid = lo + hi >> 1;
|
|
84
|
+
const candidate = mid > 0 ? `${trimmed.slice(0, mid)}…` : "…";
|
|
85
|
+
if (measureWidth(candidate) <= wrapWidth) {
|
|
86
|
+
best = candidate;
|
|
87
|
+
lo = mid + 1;
|
|
88
|
+
} else hi = mid - 1;
|
|
89
|
+
}
|
|
90
|
+
return best;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Fit a Pixi text node into a fixed line budget.
|
|
94
|
+
*
|
|
95
|
+
* Pixi Text has no built-in max-lines / ellipsis, so we measure with the same
|
|
96
|
+
* style the node will render, keep the leading wrapped lines, and mark a cut
|
|
97
|
+
* with an ellipsis. Returning early when everything fits avoids an extra
|
|
98
|
+
* texture rebuild on the common path.
|
|
99
|
+
*
|
|
100
|
+
* Cost is one wrap of a length-capped sample, plus at most a binary search on
|
|
101
|
+
* the last kept line. No character-by-character multi-line remeasure.
|
|
102
|
+
*/
|
|
103
|
+
function fitTextToLines(text, value, maxLines, wrapWidth) {
|
|
104
|
+
const width = Math.max(1, wrapWidth);
|
|
105
|
+
text.style.wordWrapWidth = width;
|
|
106
|
+
if (!value || maxLines <= 0) {
|
|
107
|
+
if (text.text !== "") text.text = "";
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const charCap = Math.max(maxLines * Math.ceil(width), maxLines * 4);
|
|
111
|
+
const sample = value.length > charCap ? value.slice(0, charCap) : value;
|
|
112
|
+
const metrics = CanvasTextMetrics.measureText(sample, text.style);
|
|
113
|
+
if (!(metrics.lines.length > maxLines || sample !== value)) {
|
|
114
|
+
if (text.text !== value) text.text = value;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const keptCount = Math.min(maxLines, metrics.lines.length);
|
|
118
|
+
const kept = metrics.lines.slice(0, keptCount);
|
|
119
|
+
const lastIndex = kept.length - 1;
|
|
120
|
+
const probe = text.style.clone();
|
|
121
|
+
probe.wordWrap = false;
|
|
122
|
+
kept[lastIndex] = fitLineWithEllipsis(kept[lastIndex] ?? "", width, (candidate) => CanvasTextMetrics.measureText(candidate, probe).width);
|
|
123
|
+
const fitted = kept.join("\n");
|
|
124
|
+
if (text.text !== fitted) text.text = fitted;
|
|
125
|
+
}
|
|
126
|
+
/** How many full lines of `lineHeight` fit in `room`, capped at `max`. */
|
|
127
|
+
function linesInRoom(room, lineHeight, max) {
|
|
128
|
+
if (room < lineHeight * .85) return 0;
|
|
129
|
+
return Math.max(0, Math.min(max, Math.floor((room + .5) / lineHeight)));
|
|
130
|
+
}
|
|
131
|
+
/** Scale an image into a fixed cover band without cropping or distortion. */
|
|
132
|
+
function containCoverRect(width, height, imageWidth, imageHeight) {
|
|
133
|
+
if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return {
|
|
134
|
+
x: 0,
|
|
135
|
+
y: 0,
|
|
136
|
+
width: 0,
|
|
137
|
+
height: 0
|
|
138
|
+
};
|
|
139
|
+
const scale = Math.min(width / imageWidth, height / imageHeight);
|
|
140
|
+
const renderedWidth = imageWidth * scale;
|
|
141
|
+
const renderedHeight = imageHeight * scale;
|
|
142
|
+
return {
|
|
143
|
+
x: (width - renderedWidth) / 2,
|
|
144
|
+
y: (height - renderedHeight) / 2,
|
|
145
|
+
width: renderedWidth,
|
|
146
|
+
height: renderedHeight
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function layoutCover(sprite, width, height, texture) {
|
|
150
|
+
const rect = containCoverRect(width, height, texture.width, texture.height);
|
|
151
|
+
sprite.position.set(rect.x, rect.y);
|
|
152
|
+
sprite.width = rect.width;
|
|
153
|
+
sprite.height = rect.height;
|
|
154
|
+
}
|
|
155
|
+
function syncClip(parts, width, height) {
|
|
156
|
+
parts.clip.clear().roundRect(0, 0, Math.max(1, width), Math.max(1, height), RADIUS).fill({ color: 16777215 });
|
|
157
|
+
}
|
|
158
|
+
function sync(container, item, context) {
|
|
159
|
+
const parts = partsByContainer.get(container);
|
|
160
|
+
if (!parts) return;
|
|
161
|
+
positionShell(parts.root, item);
|
|
162
|
+
const { width, height } = item.frame;
|
|
163
|
+
const selected = context.selectedIds.has(item.id);
|
|
164
|
+
const hovered = context.hoveredId === item.id;
|
|
165
|
+
const detail = detailFor(context.zoom);
|
|
166
|
+
const key = context.assetKey(item);
|
|
167
|
+
const texture = key ? context.getTexture(key) : null;
|
|
168
|
+
const coverFailed = Boolean(key && !texture && context.hasError(key));
|
|
169
|
+
const fileState = context.fileState(item.ref.path);
|
|
170
|
+
const band = coverHeight(item, height);
|
|
171
|
+
syncTextResolution(parts.title, parts.titleRes, context.zoom);
|
|
172
|
+
syncTextResolution(parts.excerpt, parts.excerptRes, context.zoom);
|
|
173
|
+
const visualSig = [
|
|
174
|
+
key ?? "",
|
|
175
|
+
width,
|
|
176
|
+
height,
|
|
177
|
+
selected,
|
|
178
|
+
hovered,
|
|
179
|
+
detail,
|
|
180
|
+
texture ? `${texture.width}x${texture.height}` : "none",
|
|
181
|
+
coverFailed,
|
|
182
|
+
fileState,
|
|
183
|
+
context.colorScheme,
|
|
184
|
+
context.palette.surface,
|
|
185
|
+
context.palette.hover
|
|
186
|
+
].join("|");
|
|
187
|
+
if (visualSig !== parts.visualSig) {
|
|
188
|
+
parts.visualSig = visualSig;
|
|
189
|
+
syncClip(parts, width, height);
|
|
190
|
+
parts.plate.clear();
|
|
191
|
+
parts.plate.roundRect(0, 0, width, height, RADIUS).fill({
|
|
192
|
+
color: context.palette.surface,
|
|
193
|
+
alpha: .98
|
|
194
|
+
}).roundRect(0, 0, width, height, RADIUS).stroke({
|
|
195
|
+
color: selected ? context.palette.brand : hovered ? context.palette.muted : context.palette.border,
|
|
196
|
+
width: selected ? 2 : 1,
|
|
197
|
+
alpha: selected ? .95 : .85
|
|
198
|
+
});
|
|
199
|
+
if (fileState !== "ok") {
|
|
200
|
+
const dash = Math.max(6, width / MAX_DASHES / 2);
|
|
201
|
+
for (let x = 0; x < width; x += dash * 2) parts.plate.moveTo(x, .5).lineTo(Math.min(x + dash, width), .5);
|
|
202
|
+
parts.plate.stroke({
|
|
203
|
+
color: context.palette.muted,
|
|
204
|
+
width: 2,
|
|
205
|
+
alpha: fileState === "missing" ? .9 : .5
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const showCover = band > 0 && Boolean(texture);
|
|
209
|
+
if (band > 0) parts.plate.rect(1, 1, width - 2, band - 1).fill({
|
|
210
|
+
color: context.palette.hover,
|
|
211
|
+
alpha: coverFailed ? .35 : .6
|
|
212
|
+
});
|
|
213
|
+
if (showCover && texture) {
|
|
214
|
+
if (parts.cover.texture !== texture) parts.cover.texture = texture;
|
|
215
|
+
layoutCover(parts.cover, width, band, texture);
|
|
216
|
+
parts.coverMask.clear().roundRect(0, 0, width, band, RADIUS).fill({ color: 16777215 });
|
|
217
|
+
parts.coverMask.rect(0, band - RADIUS, width, RADIUS).fill({ color: 16777215 });
|
|
218
|
+
}
|
|
219
|
+
parts.cover.visible = showCover;
|
|
220
|
+
parts.coverMask.visible = showCover;
|
|
221
|
+
if (!band) parts.plate.rect(1, 1, 2, height - 2).fill({
|
|
222
|
+
color: context.palette.muted,
|
|
223
|
+
alpha: .35
|
|
224
|
+
});
|
|
225
|
+
parts.title.visible = detail !== "plate";
|
|
226
|
+
parts.excerpt.visible = detail === "full";
|
|
227
|
+
}
|
|
228
|
+
if (detail === "plate") return;
|
|
229
|
+
const title = item.snapshot?.title || fileBaseName(item.ref.path);
|
|
230
|
+
const excerpt = item.snapshot?.excerpt ?? "";
|
|
231
|
+
const innerWidth = Math.max(1, width - PADDING * 2);
|
|
232
|
+
const textSig = [
|
|
233
|
+
title,
|
|
234
|
+
excerpt,
|
|
235
|
+
detail,
|
|
236
|
+
innerWidth,
|
|
237
|
+
band,
|
|
238
|
+
height,
|
|
239
|
+
context.palette.text,
|
|
240
|
+
context.palette.muted
|
|
241
|
+
].join("|");
|
|
242
|
+
if (textSig === parts.textSig) return;
|
|
243
|
+
parts.textSig = textSig;
|
|
244
|
+
const top = band > 0 ? band + PADDING * .8 : PADDING;
|
|
245
|
+
const contentBottom = height - PADDING;
|
|
246
|
+
const titleLines = linesInRoom(Math.max(0, contentBottom - top), TITLE_LINE, TITLE_MAX_LINES);
|
|
247
|
+
parts.title.style.fill = context.palette.text;
|
|
248
|
+
fitTextToLines(parts.title, title, titleLines, innerWidth);
|
|
249
|
+
parts.title.position.set(PADDING, top);
|
|
250
|
+
parts.title.visible = titleLines > 0;
|
|
251
|
+
if (detail !== "full") {
|
|
252
|
+
parts.excerpt.visible = false;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const excerptTop = top + (titleLines > 0 ? parts.title.height + GAP : 0);
|
|
256
|
+
const excerptLines = linesInRoom(contentBottom - excerptTop, EXCERPT_LINE, EXCERPT_MAX_LINES);
|
|
257
|
+
const showExcerpt = Boolean(excerpt) && excerptLines > 0;
|
|
258
|
+
if (showExcerpt) {
|
|
259
|
+
parts.excerpt.style.fill = context.palette.muted;
|
|
260
|
+
fitTextToLines(parts.excerpt, excerpt, excerptLines, innerWidth);
|
|
261
|
+
parts.excerpt.position.set(PADDING, excerptTop);
|
|
262
|
+
}
|
|
263
|
+
parts.excerpt.visible = showExcerpt;
|
|
264
|
+
}
|
|
265
|
+
const fileCardRenderer = {
|
|
266
|
+
id: "file-card",
|
|
267
|
+
canRender: (item) => item.type === "file",
|
|
268
|
+
create: (item, context) => {
|
|
269
|
+
const root = new Container();
|
|
270
|
+
const plate = new Graphics();
|
|
271
|
+
const body = new Container();
|
|
272
|
+
const clip = new Graphics();
|
|
273
|
+
const cover = new Sprite(Texture.EMPTY);
|
|
274
|
+
const coverMask = new Graphics();
|
|
275
|
+
cover.mask = coverMask;
|
|
276
|
+
const resolution = 1;
|
|
277
|
+
const title = new Text({
|
|
278
|
+
text: "",
|
|
279
|
+
style: {
|
|
280
|
+
fill: context.palette.text,
|
|
281
|
+
fontFamily: BOARD_FONT_STACK,
|
|
282
|
+
fontSize: TITLE_SIZE,
|
|
283
|
+
fontWeight: "600",
|
|
284
|
+
wordWrap: true,
|
|
285
|
+
breakWords: true,
|
|
286
|
+
lineHeight: TITLE_LINE
|
|
287
|
+
},
|
|
288
|
+
resolution,
|
|
289
|
+
roundPixels: true
|
|
290
|
+
});
|
|
291
|
+
const excerpt = new Text({
|
|
292
|
+
text: "",
|
|
293
|
+
style: {
|
|
294
|
+
fill: context.palette.muted,
|
|
295
|
+
fontFamily: BOARD_FONT_STACK,
|
|
296
|
+
fontSize: EXCERPT_SIZE,
|
|
297
|
+
fontWeight: "400",
|
|
298
|
+
wordWrap: true,
|
|
299
|
+
breakWords: true,
|
|
300
|
+
lineHeight: EXCERPT_LINE
|
|
301
|
+
},
|
|
302
|
+
resolution,
|
|
303
|
+
roundPixels: true
|
|
304
|
+
});
|
|
305
|
+
body.mask = clip;
|
|
306
|
+
body.addChild(cover, coverMask, title, excerpt);
|
|
307
|
+
root.addChild(plate, body, clip);
|
|
308
|
+
partsByContainer.set(root, {
|
|
309
|
+
root,
|
|
310
|
+
plate,
|
|
311
|
+
body,
|
|
312
|
+
clip,
|
|
313
|
+
cover,
|
|
314
|
+
coverMask,
|
|
315
|
+
title,
|
|
316
|
+
excerpt,
|
|
317
|
+
visualSig: "",
|
|
318
|
+
textSig: "",
|
|
319
|
+
titleRes: { resolution },
|
|
320
|
+
excerptRes: { resolution }
|
|
321
|
+
});
|
|
322
|
+
if (item.type === "file") sync(root, item, context);
|
|
323
|
+
return root;
|
|
324
|
+
},
|
|
325
|
+
update: (container, item, context) => {
|
|
326
|
+
if (item.type === "file") sync(container, item, context);
|
|
327
|
+
},
|
|
328
|
+
/**
|
|
329
|
+
* Far LOD: a plate with a muted accent band. Sampling the real cover here
|
|
330
|
+
* would mean one draw call per distinct image and defeat the batch.
|
|
331
|
+
*/
|
|
332
|
+
renderFar: (graphics, item, context) => {
|
|
333
|
+
drawFarPlate(graphics, item.frame, {
|
|
334
|
+
fill: context.palette.surface,
|
|
335
|
+
fillAlpha: .96,
|
|
336
|
+
accent: context.palette.muted,
|
|
337
|
+
accentAlpha: .4
|
|
338
|
+
});
|
|
339
|
+
},
|
|
340
|
+
destroy: (container) => {
|
|
341
|
+
partsByContainer.get(container)?.root.destroy({ children: true });
|
|
342
|
+
partsByContainer.delete(container);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
/** Helper kept for type narrowing in tests. */
|
|
346
|
+
function isFileItem(item) {
|
|
347
|
+
return item.type === "file";
|
|
348
|
+
}
|
|
349
|
+
//#endregion
|
|
350
|
+
export { containCoverRect, ellipsizeWrappedLines, fileCardRenderer, fitLineWithEllipsis, fitTextToLines, isFileItem };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
+
import { pickBoardColor } from "../../core/palette.js";
|
|
3
|
+
import { syncTextResolution } from "../text-resolution.js";
|
|
4
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
5
|
+
import { createLabel, positionShell } from "./base-card-renderer.js";
|
|
6
|
+
import { Container, Graphics } from "pixi.js";
|
|
7
|
+
//#region src/board/render/renderers/frame-card-renderer.ts
|
|
8
|
+
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
function sync(container, item, context) {
|
|
10
|
+
const parts = partsByContainer.get(container);
|
|
11
|
+
if (!parts || item.type !== "frame") return;
|
|
12
|
+
const { frame, label, color, locked } = item;
|
|
13
|
+
positionShell(parts.root, item);
|
|
14
|
+
syncTextResolution(parts.label, parts, context.zoom);
|
|
15
|
+
const selected = context.selectedIds.has(item.id);
|
|
16
|
+
const hovered = context.hoveredId === item.id;
|
|
17
|
+
const palette = pickBoardColor(context.colors, color, context.colorScheme);
|
|
18
|
+
const stroke = selected ? context.palette.brand : palette.stroke;
|
|
19
|
+
const alpha = selected ? 1 : hovered ? .85 : .55;
|
|
20
|
+
const visualSig = [
|
|
21
|
+
frame.width,
|
|
22
|
+
frame.height,
|
|
23
|
+
selected,
|
|
24
|
+
hovered,
|
|
25
|
+
palette.fill,
|
|
26
|
+
stroke
|
|
27
|
+
].join("|");
|
|
28
|
+
if (visualSig !== parts.visualSig) {
|
|
29
|
+
parts.visualSig = visualSig;
|
|
30
|
+
parts.box.clear();
|
|
31
|
+
parts.box.roundRect(0, 0, frame.width, frame.height, 4).fill({
|
|
32
|
+
color: palette.fill,
|
|
33
|
+
alpha: .04
|
|
34
|
+
});
|
|
35
|
+
parts.box.roundRect(0, 0, frame.width, frame.height, 4).stroke({
|
|
36
|
+
color: stroke,
|
|
37
|
+
width: selected ? 2 : 1.5,
|
|
38
|
+
alpha
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const title = locked ? `🔒 ${label || "Frame"}` : label || "Frame";
|
|
42
|
+
const textSig = [title, palette.stroke].join("|");
|
|
43
|
+
if (textSig !== parts.textSig) {
|
|
44
|
+
parts.textSig = textSig;
|
|
45
|
+
parts.label.text = title;
|
|
46
|
+
parts.label.style.fill = palette.stroke;
|
|
47
|
+
}
|
|
48
|
+
parts.label.position.set(4, -18);
|
|
49
|
+
}
|
|
50
|
+
const frameCardRenderer = {
|
|
51
|
+
id: "frame-card",
|
|
52
|
+
canRender: (item) => item.type === "frame",
|
|
53
|
+
create: (item, context) => {
|
|
54
|
+
const root = new Container();
|
|
55
|
+
const box = new Graphics();
|
|
56
|
+
const label = createLabel("", {
|
|
57
|
+
fill: context.palette.muted,
|
|
58
|
+
fontFamily: BOARD_FONT_STACK,
|
|
59
|
+
fontSize: 12,
|
|
60
|
+
fontWeight: "600"
|
|
61
|
+
});
|
|
62
|
+
root.addChild(box, label);
|
|
63
|
+
partsByContainer.set(root, {
|
|
64
|
+
root,
|
|
65
|
+
box,
|
|
66
|
+
label,
|
|
67
|
+
visualSig: "",
|
|
68
|
+
textSig: "",
|
|
69
|
+
resolution: label.resolution
|
|
70
|
+
});
|
|
71
|
+
sync(root, item, context);
|
|
72
|
+
return root;
|
|
73
|
+
},
|
|
74
|
+
update: (container, item, context) => {
|
|
75
|
+
sync(container, item, context);
|
|
76
|
+
},
|
|
77
|
+
/**
|
|
78
|
+
* Far LOD: a frame is a background region, so it draws as a faint wash with no
|
|
79
|
+
* label.
|
|
80
|
+
*
|
|
81
|
+
* Batching it is what keeps it *behind* its contents. As a live container it
|
|
82
|
+
* would be drawn above the whole far layer no matter where the document places
|
|
83
|
+
* it, so a frame would hide every card inside it.
|
|
84
|
+
*/
|
|
85
|
+
renderFar: (graphics, item, context) => {
|
|
86
|
+
if (item.type !== "frame") return;
|
|
87
|
+
const palette = pickBoardColor(context.colors, item.color, context.colorScheme);
|
|
88
|
+
drawFarPlate(graphics, item.frame, {
|
|
89
|
+
fill: palette.fill,
|
|
90
|
+
fillAlpha: .07
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
destroy: (container) => {
|
|
94
|
+
partsByContainer.delete(container);
|
|
95
|
+
container.destroy({ children: true });
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
//#endregion
|
|
99
|
+
export { frameCardRenderer };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
+
import { pickBoardColor } from "../../core/palette.js";
|
|
3
|
+
import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
|
|
4
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
5
|
+
import { positionShell } from "./base-card-renderer.js";
|
|
6
|
+
import { Container, Graphics, Text } from "pixi.js";
|
|
7
|
+
//#region src/board/render/renderers/geo-card-renderer.ts
|
|
8
|
+
const RADIUS = 8;
|
|
9
|
+
const LABEL_PADDING = 8;
|
|
10
|
+
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
function traceOutline(graphics, geo, width, height) {
|
|
12
|
+
switch (geo) {
|
|
13
|
+
case "ellipse":
|
|
14
|
+
graphics.ellipse(width / 2, height / 2, width / 2, height / 2);
|
|
15
|
+
return;
|
|
16
|
+
case "diamond":
|
|
17
|
+
graphics.moveTo(width / 2, 0).lineTo(width, height / 2).lineTo(width / 2, height).lineTo(0, height / 2).closePath();
|
|
18
|
+
return;
|
|
19
|
+
case "triangle":
|
|
20
|
+
graphics.moveTo(width / 2, 0).lineTo(width, height).lineTo(0, height).closePath();
|
|
21
|
+
return;
|
|
22
|
+
case "rounded":
|
|
23
|
+
graphics.roundRect(0, 0, width, height, RADIUS);
|
|
24
|
+
return;
|
|
25
|
+
default: graphics.rect(0, 0, width, height);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function sync(container, item, context) {
|
|
29
|
+
const parts = partsByContainer.get(container);
|
|
30
|
+
if (!parts) return;
|
|
31
|
+
positionShell(parts.root, item);
|
|
32
|
+
const { width, height } = item.frame;
|
|
33
|
+
const selected = context.selectedIds.has(item.id);
|
|
34
|
+
const hovered = context.hoveredId === item.id;
|
|
35
|
+
const color = pickBoardColor(context.colors, item.color, context.colorScheme);
|
|
36
|
+
syncTextResolution(parts.label, parts, context.zoom);
|
|
37
|
+
const resizing = context.resizingIds.has(item.id);
|
|
38
|
+
const visualSig = [
|
|
39
|
+
width,
|
|
40
|
+
height,
|
|
41
|
+
selected,
|
|
42
|
+
hovered,
|
|
43
|
+
item.geo,
|
|
44
|
+
item.fillOpacity,
|
|
45
|
+
color.fill,
|
|
46
|
+
color.stroke,
|
|
47
|
+
context.palette.muted
|
|
48
|
+
].join("|");
|
|
49
|
+
if (visualSig !== parts.visualSig) {
|
|
50
|
+
parts.visualSig = visualSig;
|
|
51
|
+
parts.shape.clear();
|
|
52
|
+
traceOutline(parts.shape, item.geo, width, height);
|
|
53
|
+
if (item.fillOpacity > 0) parts.shape.fill({
|
|
54
|
+
color: color.fill,
|
|
55
|
+
alpha: item.fillOpacity
|
|
56
|
+
});
|
|
57
|
+
traceOutline(parts.shape, item.geo, width, height);
|
|
58
|
+
parts.shape.stroke({
|
|
59
|
+
color: selected ? color.stroke : hovered ? context.palette.muted : color.stroke,
|
|
60
|
+
width: selected ? 2.5 : 1.75,
|
|
61
|
+
alpha: selected ? 1 : hovered ? .95 : .85
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const textSig = [item.text, color.label].join("|");
|
|
65
|
+
if (textSig !== parts.textSig) {
|
|
66
|
+
parts.textSig = textSig;
|
|
67
|
+
parts.label.text = item.text;
|
|
68
|
+
parts.label.visible = item.text.length > 0;
|
|
69
|
+
parts.label.style.fill = color.label;
|
|
70
|
+
}
|
|
71
|
+
syncTextWrapWidth(parts.label, parts, Math.max(1, width - LABEL_PADDING * 2), resizing);
|
|
72
|
+
parts.label.position.set(width / 2, height / 2);
|
|
73
|
+
}
|
|
74
|
+
const geoCardRenderer = {
|
|
75
|
+
id: "geo-card",
|
|
76
|
+
canRender: (item) => item.type === "geo",
|
|
77
|
+
create: (item, context) => {
|
|
78
|
+
const root = new Container();
|
|
79
|
+
const shape = new Graphics();
|
|
80
|
+
const resolution = textResolutionForZoom(context.zoom);
|
|
81
|
+
const label = new Text({
|
|
82
|
+
text: "",
|
|
83
|
+
style: {
|
|
84
|
+
fill: 16777215,
|
|
85
|
+
fontFamily: BOARD_FONT_STACK,
|
|
86
|
+
fontSize: 14,
|
|
87
|
+
fontWeight: "500",
|
|
88
|
+
align: "center",
|
|
89
|
+
wordWrap: true,
|
|
90
|
+
lineHeight: 20
|
|
91
|
+
},
|
|
92
|
+
resolution,
|
|
93
|
+
roundPixels: true
|
|
94
|
+
});
|
|
95
|
+
label.anchor.set(.5);
|
|
96
|
+
root.addChild(shape, label);
|
|
97
|
+
partsByContainer.set(root, {
|
|
98
|
+
root,
|
|
99
|
+
shape,
|
|
100
|
+
label,
|
|
101
|
+
visualSig: "",
|
|
102
|
+
textSig: "",
|
|
103
|
+
wrapWidth: 0,
|
|
104
|
+
resolution
|
|
105
|
+
});
|
|
106
|
+
if (item.type === "geo") sync(root, item, context);
|
|
107
|
+
return root;
|
|
108
|
+
},
|
|
109
|
+
update: (container, item, context) => {
|
|
110
|
+
if (item.type === "geo") sync(container, item, context);
|
|
111
|
+
},
|
|
112
|
+
renderFar: (graphics, item, context) => {
|
|
113
|
+
if (item.type !== "geo") return;
|
|
114
|
+
const color = pickBoardColor(context.colors, item.color, context.colorScheme);
|
|
115
|
+
drawFarPlate(graphics, item.frame, {
|
|
116
|
+
fill: color.fill,
|
|
117
|
+
fillAlpha: Math.max(.18, item.fillOpacity),
|
|
118
|
+
accent: color.stroke,
|
|
119
|
+
accentAlpha: .9
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
destroy: (container) => {
|
|
123
|
+
partsByContainer.get(container)?.root.destroy({ children: true });
|
|
124
|
+
partsByContainer.delete(container);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
//#endregion
|
|
128
|
+
export { geoCardRenderer };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { BoardImageItem, BoardItem } from "../../../protocol/dist/board-document.js";
|
|
2
|
+
import { BoardCardRenderer } from "./board-renderer-registry.js";
|
|
3
|
+
//#region src/board/render/renderers/image-card-renderer.d.ts
|
|
4
|
+
declare const imageCardRenderer: BoardCardRenderer;
|
|
5
|
+
/** Helper kept for type narrowing in tests. */
|
|
6
|
+
declare function isImageItem(item: BoardItem): item is BoardImageItem;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { imageCardRenderer, isImageItem };
|