@neta-art/cohub 8.23.0 → 9.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/dist/board/animation.d.ts +57 -181
- package/dist/board/animation.js +374 -220
- package/dist/board/core/arrow-geometry.d.ts +33 -18
- package/dist/board/core/arrow-geometry.js +283 -52
- package/dist/board/core/draw-geometry.d.ts +22 -36
- package/dist/board/core/draw-geometry.js +148 -85
- package/dist/board/core/export-assets.d.ts +3 -6
- package/dist/board/core/export-assets.js +0 -2
- package/dist/board/core/export-plan.d.ts +12 -63
- package/dist/board/core/export-plan.js +55 -94
- package/dist/board/core/palette.d.ts +4 -21
- package/dist/board/core/palette.js +8 -13
- package/dist/board/core/scene.d.ts +33 -0
- package/dist/board/core/scene.js +302 -0
- package/dist/board/core/shape-definition.d.ts +12 -24
- package/dist/board/core/shape-definition.js +0 -19
- package/dist/board/core/shape-types.d.ts +6 -120
- package/dist/board/core/shape-types.js +5 -5
- package/dist/board/core/text-metrics.d.ts +8 -11
- package/dist/board/core/text-metrics.js +5 -34
- package/dist/board/core/tool-styles.d.ts +6 -16
- package/dist/board/core/tool-styles.js +17 -25
- package/dist/board/export/index.d.ts +10 -17
- package/dist/board/export/index.js +22 -21
- package/dist/board/export/scene.d.ts +10 -16
- package/dist/board/export/scene.js +17 -30
- package/dist/board/geometry.d.ts +27 -56
- package/dist/board/geometry.js +7 -67
- package/dist/board/headless/index.d.ts +7 -27
- package/dist/board/headless/index.js +20 -19
- package/dist/board/headless/sketch.d.ts +19 -0
- package/dist/board/headless/sketch.js +146 -0
- package/dist/board/image-key.d.ts +2 -12
- package/dist/board/image-key.js +3 -13
- package/dist/board/index.d.ts +18 -20
- package/dist/board/index.js +17 -17
- package/dist/board/media-playback.d.ts +3 -6
- package/dist/board/media-playback.js +6 -6
- package/dist/board/presets.d.ts +150 -0
- package/dist/board/presets.js +226 -0
- package/dist/board/render/board-background.d.ts +5 -3
- package/dist/board/render/board-background.js +8 -7
- package/dist/board/render/clip.d.ts +14 -0
- package/dist/board/render/clip.js +47 -0
- package/dist/board/render/index.d.ts +8 -5
- package/dist/board/render/index.js +7 -4
- package/dist/board/render/media-interaction.d.ts +4 -5
- package/dist/board/render/media-interaction.js +1 -2
- package/dist/board/render/palette.d.ts +6 -2
- package/dist/board/render/palette.js +7 -1
- package/dist/board/render/renderers/arrow-card-renderer.js +69 -71
- package/dist/board/render/renderers/audio-card-renderer.js +3 -3
- package/dist/board/render/renderers/base-card-renderer.d.ts +6 -17
- package/dist/board/render/renderers/base-card-renderer.js +1 -24
- package/dist/board/render/renderers/board-renderer-registry.d.ts +20 -50
- package/dist/board/render/renderers/board-renderer-registry.js +6 -7
- package/dist/board/render/renderers/draw-card-renderer.js +89 -56
- package/dist/board/render/renderers/effect-card-renderer.d.ts +6 -0
- package/dist/board/render/renderers/effect-card-renderer.js +197 -0
- package/dist/board/render/renderers/far-plate.d.ts +4 -21
- package/dist/board/render/renderers/far-plate.js +15 -34
- package/dist/board/render/renderers/file-card-renderer.d.ts +4 -25
- package/dist/board/render/renderers/file-card-renderer.js +8 -58
- package/dist/board/render/renderers/frame-card-renderer.js +28 -28
- package/dist/board/render/renderers/image-card-renderer.d.ts +4 -3
- package/dist/board/render/renderers/image-card-renderer.js +0 -9
- package/dist/board/render/renderers/shape-card-renderer.d.ts +9 -0
- package/dist/board/render/renderers/shape-card-renderer.js +240 -0
- package/dist/board/render/renderers/sketch-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/sketch-card-renderer.js +98 -0
- package/dist/board/render/renderers/task-card-renderer.d.ts +0 -1
- package/dist/board/render/renderers/task-card-renderer.js +14 -16
- package/dist/board/render/renderers/text-card-renderer.d.ts +1 -3
- package/dist/board/render/renderers/text-card-renderer.js +56 -46
- package/dist/board/render/renderers/unknown-card-renderer.js +4 -10
- package/dist/board/render/renderers/video-card-renderer.d.ts +4 -2
- package/dist/board/render/renderers/video-card-renderer.js +2 -1
- package/dist/board/render/sketch-host.d.ts +14 -0
- package/dist/board/render/sketch-host.js +216 -0
- package/dist/board/render/stroke.d.ts +12 -0
- package/dist/board/render/stroke.js +90 -0
- package/dist/board/render/text-measurement.d.ts +6 -26
- package/dist/board/render/text-measurement.js +11 -30
- package/dist/board/replay.d.ts +6 -31
- package/dist/board/replay.js +45 -203
- package/dist/board/runtime/board-player.d.ts +24 -0
- package/dist/board/runtime/board-player.js +161 -0
- package/dist/board/task.d.ts +2 -10
- package/dist/board/task.js +1 -10
- package/dist/chunks/environment.d.ts +2296 -3864
- package/dist/chunks/host-bridge.d.ts +253 -0
- package/dist/chunks/http.d.ts +238 -58
- package/dist/chunks/http.js +1061 -1034
- package/dist/chunks/transport.js +113 -59
- package/dist/chunks/websocket.d.ts +1 -1
- package/dist/chunks/websocket.js +1 -1
- package/dist/host-bridge.d.ts +54 -0
- package/dist/host-bridge.js +322 -0
- package/dist/http.d.ts +3 -3
- package/dist/index.d.ts +76 -193
- package/dist/index.js +2037 -1191
- package/dist/protocol/dist/board-constants.d.ts +1 -29
- package/dist/protocol/dist/board-constants.js +2 -115
- package/dist/protocol/dist/board-layout.d.ts +59 -0
- package/dist/protocol/dist/board-layout.js +482 -0
- package/dist/protocol/dist/board-model.d.ts +2052 -0
- package/dist/protocol/dist/board-model.js +592 -0
- package/dist/protocol/dist/board-patch.d.ts +86 -0
- package/dist/protocol/dist/board-patch.js +734 -0
- package/dist/protocol/dist/board-schema.d.ts +33 -0
- package/dist/protocol/dist/board-schema.js +59 -0
- package/dist/protocol/dist/board-upgrade.d.ts +1 -0
- package/dist/protocol/dist/board-url.d.ts +1 -3
- package/dist/protocol/dist/board-url.js +2 -2
- package/dist/protocol/dist/board.d.ts +37 -232
- package/dist/protocol/dist/board.js +62 -160
- package/dist/protocol/dist/display/actions.d.ts +1 -0
- package/dist/protocol/dist/display/index.d.ts +2 -0
- package/dist/protocol/dist/index.d.ts +11 -12
- package/dist/protocol/dist/model/metrics.d.ts +2 -0
- package/dist/protocol/dist/model/session.d.ts +2 -1
- package/dist/protocol/dist/realtime/types.d.ts +2 -2
- package/dist/protocol/dist/runtime/index.d.ts +1 -0
- package/dist/space-picker.d.ts +1 -0
- package/dist/space-picker.js +1 -0
- package/dist/types.d.ts +2 -5
- package/package.json +9 -1
- package/dist/board/core/connections.d.ts +0 -108
- package/dist/board/core/connections.js +0 -455
- package/dist/board/mutation.d.ts +0 -6
- package/dist/board/mutation.js +0 -14
- package/dist/board/render/connection-layer.d.ts +0 -53
- package/dist/board/render/connection-layer.js +0 -375
- package/dist/board/render/renderers/geo-card-renderer.d.ts +0 -5
- package/dist/board/render/renderers/geo-card-renderer.js +0 -127
- package/dist/board/semantic-document.d.ts +0 -37
- package/dist/board/semantic-document.js +0 -288
- package/dist/board/semantic-mutation.d.ts +0 -10
- package/dist/board/semantic-mutation.js +0 -209
- package/dist/protocol/dist/board-animation.js +0 -34
- package/dist/protocol/dist/board-authoring.d.ts +0 -1326
- package/dist/protocol/dist/board-authoring.js +0 -302
- package/dist/protocol/dist/board-capability-registry.d.ts +0 -2
- package/dist/protocol/dist/board-capability-registry.js +0 -123
- package/dist/protocol/dist/board-codec.d.ts +0 -3
- package/dist/protocol/dist/board-codec.js +0 -383
- package/dist/protocol/dist/board-composition.d.ts +0 -383
- package/dist/protocol/dist/board-composition.js +0 -310
- package/dist/protocol/dist/board-connection.d.ts +0 -317
- package/dist/protocol/dist/board-connection.js +0 -219
- package/dist/protocol/dist/board-document.d.ts +0 -1566
- package/dist/protocol/dist/board-document.js +0 -456
- package/dist/protocol/dist/board-effect.d.ts +0 -96
- package/dist/protocol/dist/board-effect.js +0 -54
- package/dist/protocol/dist/board-geometry.d.ts +0 -15
- package/dist/protocol/dist/board-geometry.js +0 -137
- package/dist/protocol/dist/board-node.d.ts +0 -8
- package/dist/protocol/dist/board-node.js +0 -252
- /package/dist/protocol/dist/{board-animation.d.ts → host-bridge.d.ts} +0 -0
|
@@ -1,32 +1,12 @@
|
|
|
1
|
-
import { BoardFileItem
|
|
1
|
+
import { BoardFileItem } from "../../../protocol/dist/board-model.js";
|
|
2
|
+
import "../../../protocol/dist/index.js";
|
|
3
|
+
import { BoardSceneItem, SceneItem } from "../../core/scene.js";
|
|
2
4
|
import { BoardCardRenderer } from "./board-renderer-registry.js";
|
|
3
5
|
import { Text } from "pixi.js";
|
|
4
6
|
//#region src/board/render/renderers/file-card-renderer.d.ts
|
|
5
|
-
/**
|
|
6
|
-
* Join already-wrapped lines and append an ellipsis to the last one.
|
|
7
|
-
* Pure helper so the clamp can be unit-tested without a Pixi canvas.
|
|
8
|
-
*/
|
|
9
7
|
declare function ellipsizeWrappedLines(lines: string[], maxLines: number): string;
|
|
10
|
-
/**
|
|
11
|
-
* Shorten `line` so `line + "…"` fits in `wrapWidth`.
|
|
12
|
-
*
|
|
13
|
-
* Binary search over the prefix: O(log n) single-line measures. Callers pass a
|
|
14
|
-
* measure callback so this stays free of Pixi types and easy to unit-test.
|
|
15
|
-
*/
|
|
16
8
|
declare function fitLineWithEllipsis(line: string, wrapWidth: number, measureWidth: (value: string) => number): string;
|
|
17
|
-
/**
|
|
18
|
-
* Fit a Pixi text node into a fixed line budget.
|
|
19
|
-
*
|
|
20
|
-
* Pixi Text has no built-in max-lines / ellipsis, so we measure with the same
|
|
21
|
-
* style the node will render, keep the leading wrapped lines, and mark a cut
|
|
22
|
-
* with an ellipsis. Returning early when everything fits avoids an extra
|
|
23
|
-
* texture rebuild on the common path.
|
|
24
|
-
*
|
|
25
|
-
* Cost is one wrap of a length-capped sample, plus at most a binary search on
|
|
26
|
-
* the last kept line. No character-by-character multi-line remeasure.
|
|
27
|
-
*/
|
|
28
9
|
declare function fitTextToLines(text: Text, value: string, maxLines: number, wrapWidth: number): void;
|
|
29
|
-
/** Scale an image into a fixed cover band without cropping or distortion. */
|
|
30
10
|
declare function containCoverRect(width: number, height: number, imageWidth: number, imageHeight: number): {
|
|
31
11
|
x: number;
|
|
32
12
|
y: number;
|
|
@@ -34,7 +14,6 @@ declare function containCoverRect(width: number, height: number, imageWidth: num
|
|
|
34
14
|
height: number;
|
|
35
15
|
};
|
|
36
16
|
declare const fileCardRenderer: BoardCardRenderer;
|
|
37
|
-
|
|
38
|
-
declare function isFileItem(item: BoardItem): item is BoardFileItem;
|
|
17
|
+
declare function isFileItem(item: BoardSceneItem): item is SceneItem<BoardFileItem>;
|
|
39
18
|
//#endregion
|
|
40
19
|
export { containCoverRect, ellipsizeWrappedLines, fileCardRenderer, fitLineWithEllipsis, fitTextToLines, isFileItem };
|
|
@@ -6,26 +6,9 @@ import { positionShell } from "./base-card-renderer.js";
|
|
|
6
6
|
import { drawFarPlate } from "./far-plate.js";
|
|
7
7
|
import { CanvasTextMetrics, Container, Graphics, Sprite, Text, Texture } from "pixi.js";
|
|
8
8
|
//#region src/board/render/renderers/file-card-renderer.ts
|
|
9
|
-
/**
|
|
10
|
-
* File card renderer.
|
|
11
|
-
*
|
|
12
|
-
* The card is an *entry point* to a workspace file, so it shows only what helps
|
|
13
|
-
* you recognise and pick it: an optional cover, the name, a couple of lines of
|
|
14
|
-
* excerpt. Everything else belongs to the file preview that opens
|
|
15
|
-
* when the card is activated.
|
|
16
|
-
*
|
|
17
|
-
* Cost is managed through three levels of detail keyed on zoom, because a board
|
|
18
|
-
* is expected to hold thousands of these. Pixi `Text` is a rasterised texture,
|
|
19
|
-
* so below roughly one CSS pixel per glyph it can only ever resolve to a smudge;
|
|
20
|
-
* the tiers below simply stop paying for what cannot be read anyway.
|
|
21
|
-
*
|
|
22
|
-
* Absolute floor: content never paints outside the node frame. A rounded clip
|
|
23
|
-
* mask is the hard boundary; line-clamped title/excerpt keep the interior tidy.
|
|
24
|
-
*/
|
|
25
9
|
const RADIUS = 4;
|
|
26
10
|
const PADDING = 10;
|
|
27
11
|
const COVER_RATIO = .56;
|
|
28
|
-
/** Minimum body height reserved under a cover so the title stays readable. */
|
|
29
12
|
const MIN_BODY_FOR_COVER = 38;
|
|
30
13
|
const TITLE_SIZE = 13;
|
|
31
14
|
const TITLE_LINE = TITLE_SIZE * 1.35;
|
|
@@ -36,29 +19,20 @@ const EXCERPT_MAX_LINES = 3;
|
|
|
36
19
|
const TYPE_MARK_SIZE = 28;
|
|
37
20
|
const GAP = 4;
|
|
38
21
|
const STRIPE = 2;
|
|
39
|
-
/** Zoom below which the title is dropped (glyphs are sub-pixel). */
|
|
40
22
|
const LOD_TITLE_ZOOM = .35;
|
|
41
|
-
/** Zoom below which the excerpt is dropped. */
|
|
42
23
|
const LOD_BODY_ZOOM = .6;
|
|
43
|
-
/** Upper bound on dashes in the unavailable-state edge, regardless of width. */
|
|
44
24
|
const MAX_DASHES = 40;
|
|
45
25
|
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
46
|
-
/** Detail tier for a zoom level. */
|
|
47
26
|
function detailFor(zoom) {
|
|
48
27
|
if (zoom < LOD_TITLE_ZOOM) return "plate";
|
|
49
28
|
if (zoom < LOD_BODY_ZOOM) return "title";
|
|
50
29
|
return "full";
|
|
51
30
|
}
|
|
52
|
-
/** Cover band height for a card, or 0 when it has no cover. */
|
|
53
31
|
function coverHeight(item, height) {
|
|
54
|
-
if (filePreviewKind(item.snapshot) !== "cover") return 0;
|
|
32
|
+
if (filePreviewKind(item.props.snapshot) !== "cover") return 0;
|
|
55
33
|
const ideal = Math.round(height * COVER_RATIO);
|
|
56
34
|
return Math.max(0, Math.min(ideal, height - MIN_BODY_FOR_COVER));
|
|
57
35
|
}
|
|
58
|
-
/**
|
|
59
|
-
* Join already-wrapped lines and append an ellipsis to the last one.
|
|
60
|
-
* Pure helper so the clamp can be unit-tested without a Pixi canvas.
|
|
61
|
-
*/
|
|
62
36
|
function ellipsizeWrappedLines(lines, maxLines) {
|
|
63
37
|
if (maxLines <= 0 || lines.length === 0) return "";
|
|
64
38
|
if (lines.length <= maxLines) return lines.join("\n");
|
|
@@ -68,12 +42,6 @@ function ellipsizeWrappedLines(lines, maxLines) {
|
|
|
68
42
|
kept[lastIndex] = last ? `${last}…` : "…";
|
|
69
43
|
return kept.join("\n");
|
|
70
44
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Shorten `line` so `line + "…"` fits in `wrapWidth`.
|
|
73
|
-
*
|
|
74
|
-
* Binary search over the prefix: O(log n) single-line measures. Callers pass a
|
|
75
|
-
* measure callback so this stays free of Pixi types and easy to unit-test.
|
|
76
|
-
*/
|
|
77
45
|
function fitLineWithEllipsis(line, wrapWidth, measureWidth) {
|
|
78
46
|
const trimmed = line.replace(/\s+$/u, "");
|
|
79
47
|
if (!trimmed) return "…";
|
|
@@ -92,17 +60,6 @@ function fitLineWithEllipsis(line, wrapWidth, measureWidth) {
|
|
|
92
60
|
}
|
|
93
61
|
return best;
|
|
94
62
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Fit a Pixi text node into a fixed line budget.
|
|
97
|
-
*
|
|
98
|
-
* Pixi Text has no built-in max-lines / ellipsis, so we measure with the same
|
|
99
|
-
* style the node will render, keep the leading wrapped lines, and mark a cut
|
|
100
|
-
* with an ellipsis. Returning early when everything fits avoids an extra
|
|
101
|
-
* texture rebuild on the common path.
|
|
102
|
-
*
|
|
103
|
-
* Cost is one wrap of a length-capped sample, plus at most a binary search on
|
|
104
|
-
* the last kept line. No character-by-character multi-line remeasure.
|
|
105
|
-
*/
|
|
106
63
|
function fitTextToLines(text, value, maxLines, wrapWidth) {
|
|
107
64
|
const width = Math.max(1, wrapWidth);
|
|
108
65
|
text.style.wordWrapWidth = width;
|
|
@@ -126,12 +83,10 @@ function fitTextToLines(text, value, maxLines, wrapWidth) {
|
|
|
126
83
|
const fitted = kept.join("\n");
|
|
127
84
|
if (text.text !== fitted) text.text = fitted;
|
|
128
85
|
}
|
|
129
|
-
/** How many full lines of `lineHeight` fit in `room`, capped at `max`. */
|
|
130
86
|
function linesInRoom(room, lineHeight, max) {
|
|
131
87
|
if (room < lineHeight * .85) return 0;
|
|
132
88
|
return Math.max(0, Math.min(max, Math.floor((room + .5) / lineHeight)));
|
|
133
89
|
}
|
|
134
|
-
/** Scale an image into a fixed cover band without cropping or distortion. */
|
|
135
90
|
function containCoverRect(width, height, imageWidth, imageHeight) {
|
|
136
91
|
if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return {
|
|
137
92
|
x: 0,
|
|
@@ -169,10 +124,10 @@ function sync(container, item, context) {
|
|
|
169
124
|
const key = context.assetKey(item);
|
|
170
125
|
const texture = key ? context.getTexture(key) : null;
|
|
171
126
|
const coverFailed = Boolean(key && !texture && context.hasError(key));
|
|
172
|
-
const fileState = context.fileState(item.
|
|
127
|
+
const fileState = context.fileState(item.props.src);
|
|
173
128
|
const band = coverHeight(item, height);
|
|
174
|
-
const kind = filePreviewKind(item.snapshot);
|
|
175
|
-
const category = fileCategory(item.
|
|
129
|
+
const kind = filePreviewKind(item.props.snapshot);
|
|
130
|
+
const category = fileCategory(item.props.src, item.props.snapshot?.mimeType);
|
|
176
131
|
const accent = fileCategoryAccent(category, context.palette);
|
|
177
132
|
syncTextResolution(parts.title, parts.titleRes, context.zoom);
|
|
178
133
|
syncTextResolution(parts.excerpt, parts.excerptRes, context.zoom);
|
|
@@ -240,9 +195,9 @@ function sync(container, item, context) {
|
|
|
240
195
|
parts.typeMark.visible = false;
|
|
241
196
|
return;
|
|
242
197
|
}
|
|
243
|
-
const title = item.snapshot?.title || fileStem(item.
|
|
244
|
-
const excerpt = item.snapshot?.excerpt ?? "";
|
|
245
|
-
const mark = fileTypeLabel(item.
|
|
198
|
+
const title = item.props.snapshot?.title || fileStem(item.props.src);
|
|
199
|
+
const excerpt = item.props.snapshot?.excerpt ?? "";
|
|
200
|
+
const mark = fileTypeLabel(item.props.src);
|
|
246
201
|
const innerWidth = Math.max(1, width - 20);
|
|
247
202
|
const showTypeMark = kind === "blank";
|
|
248
203
|
const textSig = [
|
|
@@ -369,12 +324,8 @@ const fileCardRenderer = {
|
|
|
369
324
|
update: (container, item, context) => {
|
|
370
325
|
if (item.type === "file") sync(container, item, context);
|
|
371
326
|
},
|
|
372
|
-
/**
|
|
373
|
-
* Far LOD: a plate with a muted accent band. Sampling the real cover here
|
|
374
|
-
* would mean one draw call per distinct image and defeat the batch.
|
|
375
|
-
*/
|
|
376
327
|
renderFar: (graphics, item, context) => {
|
|
377
|
-
const category = fileCategory(item.type === "file" ? item.
|
|
328
|
+
const category = fileCategory(item.type === "file" ? item.props.src : "", item.type === "file" ? item.props.snapshot?.mimeType : void 0);
|
|
378
329
|
drawFarPlate(graphics, item.frame, {
|
|
379
330
|
fill: context.palette.surface,
|
|
380
331
|
fillAlpha: .96,
|
|
@@ -387,7 +338,6 @@ const fileCardRenderer = {
|
|
|
387
338
|
partsByContainer.delete(container);
|
|
388
339
|
}
|
|
389
340
|
};
|
|
390
|
-
/** Helper kept for type narrowing in tests. */
|
|
391
341
|
function isFileItem(item) {
|
|
392
342
|
return item.type === "file";
|
|
393
343
|
}
|
|
@@ -1,49 +1,58 @@
|
|
|
1
1
|
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
-
import { pickBoardColor } from "../../core/palette.js";
|
|
3
2
|
import { syncTextResolution } from "../text-resolution.js";
|
|
4
3
|
import { createLabel, positionShell } from "./base-card-renderer.js";
|
|
5
4
|
import { drawFarPlate } from "./far-plate.js";
|
|
5
|
+
import { itemColor } from "../palette.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();
|
|
9
|
+
const DEFAULT_WASH = .04;
|
|
9
10
|
function sync(container, item, context) {
|
|
10
11
|
const parts = partsByContainer.get(container);
|
|
11
|
-
if (!parts
|
|
12
|
-
const { frame,
|
|
12
|
+
if (!parts) return;
|
|
13
|
+
const { frame, style, props, locked } = item;
|
|
13
14
|
positionShell(parts.root, item);
|
|
14
15
|
syncTextResolution(parts.label, parts, context.zoom);
|
|
15
16
|
const selected = context.selectedIds.has(item.id);
|
|
16
17
|
const hovered = context.hoveredId === item.id;
|
|
17
|
-
const
|
|
18
|
-
const
|
|
18
|
+
const fill = itemColor(context, style.fill ?? style.stroke, "neutral", "fill");
|
|
19
|
+
const fillAlpha = style.fillOpacity ?? (style.fill ? 1 : DEFAULT_WASH);
|
|
20
|
+
const baseStroke = itemColor(context, style.stroke, "neutral");
|
|
21
|
+
const stroke = selected ? context.palette.brand : baseStroke;
|
|
19
22
|
const alpha = selected ? 1 : hovered ? .85 : .55;
|
|
23
|
+
const radius = style.radius ?? 4;
|
|
24
|
+
const strokeWidth = style.strokeWidth ?? 1.5;
|
|
20
25
|
const visualSig = [
|
|
21
26
|
frame.width,
|
|
22
27
|
frame.height,
|
|
23
28
|
selected,
|
|
24
29
|
hovered,
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
fill,
|
|
31
|
+
fillAlpha,
|
|
32
|
+
stroke,
|
|
33
|
+
radius,
|
|
34
|
+
strokeWidth
|
|
27
35
|
].join("|");
|
|
28
36
|
if (visualSig !== parts.visualSig) {
|
|
29
37
|
parts.visualSig = visualSig;
|
|
30
38
|
parts.box.clear();
|
|
31
|
-
parts.box.roundRect(0, 0, frame.width, frame.height,
|
|
32
|
-
color:
|
|
33
|
-
alpha:
|
|
39
|
+
parts.box.roundRect(0, 0, frame.width, frame.height, radius).fill({
|
|
40
|
+
color: fill,
|
|
41
|
+
alpha: fillAlpha
|
|
34
42
|
});
|
|
35
|
-
parts.box.roundRect(0, 0, frame.width, frame.height,
|
|
43
|
+
if (strokeWidth > 0 || selected) parts.box.roundRect(0, 0, frame.width, frame.height, radius).stroke({
|
|
36
44
|
color: stroke,
|
|
37
|
-
width: selected ? 2 :
|
|
45
|
+
width: selected ? Math.max(2, strokeWidth) : strokeWidth,
|
|
38
46
|
alpha
|
|
39
47
|
});
|
|
40
48
|
}
|
|
41
|
-
const title = locked ? `🔒 ${label || "Frame"}` : label || "Frame";
|
|
42
|
-
const
|
|
49
|
+
const title = locked ? `🔒 ${props.label || "Frame"}` : props.label || "Frame";
|
|
50
|
+
const labelColor = style.stroke ? baseStroke : context.palette.muted;
|
|
51
|
+
const textSig = [title, labelColor].join("|");
|
|
43
52
|
if (textSig !== parts.textSig) {
|
|
44
53
|
parts.textSig = textSig;
|
|
45
54
|
parts.label.text = title;
|
|
46
|
-
parts.label.style.fill =
|
|
55
|
+
parts.label.style.fill = labelColor;
|
|
47
56
|
}
|
|
48
57
|
parts.label.position.set(4, -18);
|
|
49
58
|
}
|
|
@@ -68,26 +77,17 @@ const frameCardRenderer = {
|
|
|
68
77
|
textSig: "",
|
|
69
78
|
resolution: label.resolution
|
|
70
79
|
});
|
|
71
|
-
sync(root, item, context);
|
|
80
|
+
if (item.type === "frame") sync(root, item, context);
|
|
72
81
|
return root;
|
|
73
82
|
},
|
|
74
83
|
update: (container, item, context) => {
|
|
75
|
-
sync(container, item, context);
|
|
84
|
+
if (item.type === "frame") sync(container, item, context);
|
|
76
85
|
},
|
|
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
86
|
renderFar: (graphics, item, context) => {
|
|
86
87
|
if (item.type !== "frame") return;
|
|
87
|
-
const palette = pickBoardColor(context.colors, item.color, context.colorScheme);
|
|
88
88
|
drawFarPlate(graphics, item.frame, {
|
|
89
|
-
fill:
|
|
90
|
-
fillAlpha: .07
|
|
89
|
+
fill: itemColor(context, item.style.fill ?? item.style.stroke, "neutral", "fill"),
|
|
90
|
+
fillAlpha: Math.max(.07, item.style.fillOpacity ?? (item.style.fill ? 1 : 0))
|
|
91
91
|
});
|
|
92
92
|
},
|
|
93
93
|
destroy: (container) => {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { BoardImageItem
|
|
1
|
+
import { BoardImageItem } from "../../../protocol/dist/board-model.js";
|
|
2
|
+
import "../../../protocol/dist/index.js";
|
|
3
|
+
import { BoardSceneItem, SceneItem } from "../../core/scene.js";
|
|
2
4
|
import { BoardCardRenderer } from "./board-renderer-registry.js";
|
|
3
5
|
//#region src/board/render/renderers/image-card-renderer.d.ts
|
|
4
6
|
declare const imageCardRenderer: BoardCardRenderer;
|
|
5
|
-
|
|
6
|
-
declare function isImageItem(item: BoardItem): item is BoardImageItem;
|
|
7
|
+
declare function isImageItem(item: BoardSceneItem): item is SceneItem<BoardImageItem>;
|
|
7
8
|
//#endregion
|
|
8
9
|
export { imageCardRenderer, isImageItem };
|
|
@@ -4,14 +4,6 @@ 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();
|
|
6
6
|
const RADIUS = 2;
|
|
7
|
-
/**
|
|
8
|
-
* Lay the sprite out inside the frame.
|
|
9
|
-
*
|
|
10
|
-
* The frame normally already matches the image's pixel aspect (see the editor's
|
|
11
|
-
* media size adoption), so this fills it exactly. Until that correction lands
|
|
12
|
-
* for a freshly dropped file, `contain` keeps the whole image visible rather
|
|
13
|
-
* than cropping it — the brief letterbox is self-correcting.
|
|
14
|
-
*/
|
|
15
7
|
function layoutContain(sprite, width, height) {
|
|
16
8
|
const texture = sprite.texture;
|
|
17
9
|
const tw = texture.width;
|
|
@@ -130,7 +122,6 @@ const imageCardRenderer = {
|
|
|
130
122
|
partsByContainer.delete(container);
|
|
131
123
|
}
|
|
132
124
|
};
|
|
133
|
-
/** Helper kept for type narrowing in tests. */
|
|
134
125
|
function isImageItem(item) {
|
|
135
126
|
return item.type === "image";
|
|
136
127
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BoardShapeItem } from "../../../protocol/dist/board-model.js";
|
|
2
|
+
import "../../../protocol/dist/index.js";
|
|
3
|
+
import { BoardCardRenderer } from "./board-renderer-registry.js";
|
|
4
|
+
import { Graphics } from "pixi.js";
|
|
5
|
+
//#region src/board/render/renderers/shape-card-renderer.d.ts
|
|
6
|
+
declare function traceShapeOutline(graphics: Graphics, props: BoardShapeItem["props"], width: number, height: number, radius?: number): void;
|
|
7
|
+
declare const shapeCardRenderer: BoardCardRenderer;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { shapeCardRenderer, traceShapeOutline };
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
+
import { worldPoint } from "../../geometry.js";
|
|
3
|
+
import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
|
|
4
|
+
import { positionShell } from "./base-card-renderer.js";
|
|
5
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
6
|
+
import { itemColor } from "../palette.js";
|
|
7
|
+
import { dashPattern, tracePolyline, trimPolyline } from "../stroke.js";
|
|
8
|
+
import { Container, Graphics, GraphicsPath, Matrix, Text } from "pixi.js";
|
|
9
|
+
//#region src/board/render/renderers/shape-card-renderer.ts
|
|
10
|
+
const DEFAULT_ROUNDED_RADIUS = 8;
|
|
11
|
+
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
12
|
+
const pathCache = /* @__PURE__ */ new Map();
|
|
13
|
+
function parsedPath(d) {
|
|
14
|
+
if (pathCache.has(d)) return pathCache.get(d) ?? null;
|
|
15
|
+
let entry = null;
|
|
16
|
+
try {
|
|
17
|
+
const path = new GraphicsPath(d);
|
|
18
|
+
const probe = new Graphics().path(path).fill(0);
|
|
19
|
+
const bounds = probe.getLocalBounds();
|
|
20
|
+
probe.destroy();
|
|
21
|
+
entry = {
|
|
22
|
+
path,
|
|
23
|
+
bounds: {
|
|
24
|
+
x: bounds.x,
|
|
25
|
+
y: bounds.y,
|
|
26
|
+
width: Math.max(1e-6, bounds.width),
|
|
27
|
+
height: Math.max(1e-6, bounds.height)
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
} catch {
|
|
31
|
+
entry = null;
|
|
32
|
+
}
|
|
33
|
+
if (pathCache.size > 256) pathCache.clear();
|
|
34
|
+
pathCache.set(d, entry);
|
|
35
|
+
return entry;
|
|
36
|
+
}
|
|
37
|
+
function traceShapeOutline(graphics, props, width, height, radius) {
|
|
38
|
+
switch (props.geometry) {
|
|
39
|
+
case "ellipse":
|
|
40
|
+
graphics.ellipse(width / 2, height / 2, width / 2, height / 2);
|
|
41
|
+
return;
|
|
42
|
+
case "diamond":
|
|
43
|
+
graphics.moveTo(width / 2, 0).lineTo(width, height / 2).lineTo(width / 2, height).lineTo(0, height / 2).closePath();
|
|
44
|
+
return;
|
|
45
|
+
case "triangle":
|
|
46
|
+
graphics.moveTo(width / 2, 0).lineTo(width, height).lineTo(0, height).closePath();
|
|
47
|
+
return;
|
|
48
|
+
case "rounded":
|
|
49
|
+
graphics.roundRect(0, 0, width, height, radius ?? DEFAULT_ROUNDED_RADIUS);
|
|
50
|
+
return;
|
|
51
|
+
case "path": {
|
|
52
|
+
const parsed = props.path ? parsedPath(props.path) : null;
|
|
53
|
+
if (!parsed) {
|
|
54
|
+
graphics.rect(0, 0, width, height);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const sx = width / parsed.bounds.width;
|
|
58
|
+
const sy = height / parsed.bounds.height;
|
|
59
|
+
graphics.path(parsed.path.clone(true).transform(new Matrix(sx, 0, 0, sy, -parsed.bounds.x * sx, -parsed.bounds.y * sy)));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
default: if (radius) graphics.roundRect(0, 0, width, height, radius);
|
|
63
|
+
else graphics.rect(0, 0, width, height);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function outlinePoints(geometry, width, height, radius) {
|
|
67
|
+
const p = (x, y) => worldPoint(x, y);
|
|
68
|
+
switch (geometry) {
|
|
69
|
+
case "ellipse": {
|
|
70
|
+
const segments = 64;
|
|
71
|
+
return Array.from({ length: 65 }, (_, index) => {
|
|
72
|
+
const angle = -Math.PI / 2 + index / segments * Math.PI * 2;
|
|
73
|
+
return p(width / 2 + Math.cos(angle) * width / 2, height / 2 + Math.sin(angle) * height / 2);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
case "diamond": return [
|
|
77
|
+
p(width / 2, 0),
|
|
78
|
+
p(width, height / 2),
|
|
79
|
+
p(width / 2, height),
|
|
80
|
+
p(0, height / 2),
|
|
81
|
+
p(width / 2, 0)
|
|
82
|
+
];
|
|
83
|
+
case "triangle": return [
|
|
84
|
+
p(width / 2, 0),
|
|
85
|
+
p(width, height),
|
|
86
|
+
p(0, height),
|
|
87
|
+
p(width / 2, 0)
|
|
88
|
+
];
|
|
89
|
+
default: {
|
|
90
|
+
const r = Math.min(radius, width / 2, height / 2);
|
|
91
|
+
if (r <= 0) return [
|
|
92
|
+
p(0, 0),
|
|
93
|
+
p(width, 0),
|
|
94
|
+
p(width, height),
|
|
95
|
+
p(0, height),
|
|
96
|
+
p(0, 0)
|
|
97
|
+
];
|
|
98
|
+
const corner = (cx, cy, from) => Array.from({ length: 9 }, (_, index) => {
|
|
99
|
+
const angle = from + index / 8 * (Math.PI / 2);
|
|
100
|
+
return p(cx + Math.cos(angle) * r, cy + Math.sin(angle) * r);
|
|
101
|
+
});
|
|
102
|
+
return [
|
|
103
|
+
...corner(width - r, r, -Math.PI / 2),
|
|
104
|
+
...corner(width - r, height - r, 0),
|
|
105
|
+
...corner(r, height - r, Math.PI / 2),
|
|
106
|
+
...corner(r, r, Math.PI),
|
|
107
|
+
p(width - r, 0)
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function sync(container, item, context) {
|
|
113
|
+
const parts = partsByContainer.get(container);
|
|
114
|
+
if (!parts) return;
|
|
115
|
+
positionShell(parts.root, item);
|
|
116
|
+
const { width, height } = item.frame;
|
|
117
|
+
const { props, style } = item;
|
|
118
|
+
const selected = context.selectedIds.has(item.id);
|
|
119
|
+
const hovered = context.hoveredId === item.id;
|
|
120
|
+
const stroke = itemColor(context, style.stroke ?? style.fill, "brand");
|
|
121
|
+
const fill = itemColor(context, style.fill ?? style.stroke, "brand", "fill");
|
|
122
|
+
const fillOpacity = style.fillOpacity ?? (style.fill ? 1 : 0);
|
|
123
|
+
const strokeWidth = style.strokeWidth ?? 1.75;
|
|
124
|
+
syncTextResolution(parts.label, parts, context.zoom);
|
|
125
|
+
const visualSig = [
|
|
126
|
+
width,
|
|
127
|
+
height,
|
|
128
|
+
selected,
|
|
129
|
+
hovered,
|
|
130
|
+
props.geometry,
|
|
131
|
+
props.path,
|
|
132
|
+
fill,
|
|
133
|
+
fillOpacity,
|
|
134
|
+
stroke,
|
|
135
|
+
strokeWidth,
|
|
136
|
+
style.dash,
|
|
137
|
+
style.radius,
|
|
138
|
+
style.trim,
|
|
139
|
+
context.palette.muted
|
|
140
|
+
].join("|");
|
|
141
|
+
if (visualSig !== parts.visualSig) {
|
|
142
|
+
parts.visualSig = visualSig;
|
|
143
|
+
parts.shape.clear();
|
|
144
|
+
if (fillOpacity > 0) {
|
|
145
|
+
traceShapeOutline(parts.shape, props, width, height, style.radius);
|
|
146
|
+
parts.shape.fill({
|
|
147
|
+
color: fill,
|
|
148
|
+
alpha: fillOpacity
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const lineWidth = selected ? Math.max(2.5, strokeWidth) : strokeWidth;
|
|
152
|
+
const pattern = dashPattern(style.dash, lineWidth);
|
|
153
|
+
const partial = style.trim !== void 0 && style.trim < 1;
|
|
154
|
+
if (strokeWidth > 0 && (!partial || style.trim)) {
|
|
155
|
+
if ((pattern || partial) && props.geometry !== "path") {
|
|
156
|
+
const radius = style.radius ?? (props.geometry === "rounded" ? DEFAULT_ROUNDED_RADIUS : 0);
|
|
157
|
+
tracePolyline(parts.shape, trimPolyline(outlinePoints(props.geometry, width, height, radius), style.trim), pattern);
|
|
158
|
+
} else traceShapeOutline(parts.shape, props, width, height, style.radius);
|
|
159
|
+
parts.shape.stroke({
|
|
160
|
+
color: hovered && !selected ? context.palette.muted : stroke,
|
|
161
|
+
width: lineWidth,
|
|
162
|
+
alpha: selected ? 1 : hovered ? .95 : .85,
|
|
163
|
+
cap: partial || pattern ? "round" : "butt",
|
|
164
|
+
join: "round"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const label = itemColor(context, style.stroke, "neutral", "label");
|
|
169
|
+
const textSig = [
|
|
170
|
+
props.text,
|
|
171
|
+
label,
|
|
172
|
+
props.fontSize,
|
|
173
|
+
props.align
|
|
174
|
+
].join("|");
|
|
175
|
+
if (textSig !== parts.textSig) {
|
|
176
|
+
parts.textSig = textSig;
|
|
177
|
+
parts.label.text = props.text;
|
|
178
|
+
parts.label.visible = props.text.length > 0;
|
|
179
|
+
parts.label.style.fill = label;
|
|
180
|
+
parts.label.style.fontSize = props.fontSize;
|
|
181
|
+
parts.label.style.lineHeight = props.fontSize * 1.4;
|
|
182
|
+
parts.label.style.align = props.align;
|
|
183
|
+
}
|
|
184
|
+
syncTextWrapWidth(parts.label, parts, Math.max(1, width - 16), context.resizingIds.has(item.id));
|
|
185
|
+
parts.label.position.set(width / 2, height / 2);
|
|
186
|
+
}
|
|
187
|
+
const shapeCardRenderer = {
|
|
188
|
+
id: "shape-card",
|
|
189
|
+
canRender: (item) => item.type === "shape",
|
|
190
|
+
create: (item, context) => {
|
|
191
|
+
const root = new Container();
|
|
192
|
+
const shape = new Graphics();
|
|
193
|
+
const resolution = textResolutionForZoom(context.zoom);
|
|
194
|
+
const label = new Text({
|
|
195
|
+
text: "",
|
|
196
|
+
style: {
|
|
197
|
+
fill: 16777215,
|
|
198
|
+
fontFamily: BOARD_FONT_STACK,
|
|
199
|
+
fontSize: 20,
|
|
200
|
+
fontWeight: "500",
|
|
201
|
+
align: "center",
|
|
202
|
+
wordWrap: true,
|
|
203
|
+
lineHeight: 28
|
|
204
|
+
},
|
|
205
|
+
resolution,
|
|
206
|
+
roundPixels: true
|
|
207
|
+
});
|
|
208
|
+
label.anchor.set(.5);
|
|
209
|
+
root.addChild(shape, label);
|
|
210
|
+
partsByContainer.set(root, {
|
|
211
|
+
root,
|
|
212
|
+
shape,
|
|
213
|
+
label,
|
|
214
|
+
visualSig: "",
|
|
215
|
+
textSig: "",
|
|
216
|
+
wrapWidth: 0,
|
|
217
|
+
resolution
|
|
218
|
+
});
|
|
219
|
+
if (item.type === "shape") sync(root, item, context);
|
|
220
|
+
return root;
|
|
221
|
+
},
|
|
222
|
+
update: (container, item, context) => {
|
|
223
|
+
if (item.type === "shape") sync(container, item, context);
|
|
224
|
+
},
|
|
225
|
+
renderFar: (graphics, item, context) => {
|
|
226
|
+
if (item.type !== "shape") return;
|
|
227
|
+
drawFarPlate(graphics, item.frame, {
|
|
228
|
+
fill: itemColor(context, item.style.fill ?? item.style.stroke, "brand", "fill"),
|
|
229
|
+
fillAlpha: Math.max(.18, item.style.fillOpacity ?? 0),
|
|
230
|
+
accent: itemColor(context, item.style.stroke, "brand"),
|
|
231
|
+
accentAlpha: .9
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
destroy: (container) => {
|
|
235
|
+
partsByContainer.get(container)?.root.destroy({ children: true });
|
|
236
|
+
partsByContainer.delete(container);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
//#endregion
|
|
240
|
+
export { shapeCardRenderer, traceShapeOutline };
|