@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,153 @@
|
|
|
1
|
+
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
+
import { getBoardResolution } from "../text-resolution.js";
|
|
3
|
+
import { Container, DOMAdapter, Graphics, Text } from "pixi.js";
|
|
4
|
+
//#region src/board/render/renderers/base-card-renderer.ts
|
|
5
|
+
const CARD_RADIUS = 10;
|
|
6
|
+
const CARD_PADDING = 12;
|
|
7
|
+
const FOOTER_HEIGHT = 34;
|
|
8
|
+
const TEXT_OPTIONS = {
|
|
9
|
+
resolution: getBoardResolution(),
|
|
10
|
+
roundPixels: true
|
|
11
|
+
};
|
|
12
|
+
function emphasisColor(item, palette) {
|
|
13
|
+
if (item.style?.accentColor) {
|
|
14
|
+
const normalized = parseCssColor(item.style.accentColor);
|
|
15
|
+
if (normalized != null) return normalized;
|
|
16
|
+
}
|
|
17
|
+
switch (item.style?.emphasis) {
|
|
18
|
+
case "rare": return palette.rare;
|
|
19
|
+
case "epic": return palette.epic;
|
|
20
|
+
case "legendary": return palette.legendary;
|
|
21
|
+
default: return palette.brand;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Normalise any CSS color to a number by letting a 2D context parse it.
|
|
26
|
+
*
|
|
27
|
+
* Goes through `DOMAdapter` rather than `document` so a headless export resolves
|
|
28
|
+
* accent colors exactly as the browser does; without it, custom accents would
|
|
29
|
+
* silently fall back to the emphasis palette only when exporting.
|
|
30
|
+
*/
|
|
31
|
+
function parseCssColor(value) {
|
|
32
|
+
try {
|
|
33
|
+
const context = DOMAdapter.get().createCanvas(1, 1).getContext("2d");
|
|
34
|
+
if (!context) return null;
|
|
35
|
+
context.fillStyle = value;
|
|
36
|
+
const match = /^#([0-9a-f]{6})$/i.exec(String(context.fillStyle));
|
|
37
|
+
return match?.[1] ? Number.parseInt(match[1], 16) : null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function createLabel(text, style) {
|
|
43
|
+
return new Text({
|
|
44
|
+
...TEXT_OPTIONS,
|
|
45
|
+
text,
|
|
46
|
+
style
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Shared card chrome: rounded background, selection/hover border, optional
|
|
51
|
+
* footer with title, and a masked content region. Tracks the last rendered
|
|
52
|
+
* state so unchanged attributes are not redrawn.
|
|
53
|
+
*/
|
|
54
|
+
function createCardShell() {
|
|
55
|
+
const root = new Container();
|
|
56
|
+
const background = new Graphics();
|
|
57
|
+
const content = new Container();
|
|
58
|
+
const mask = new Graphics();
|
|
59
|
+
const footerBg = new Graphics();
|
|
60
|
+
const title = createLabel("", {
|
|
61
|
+
fill: 16777215,
|
|
62
|
+
fontFamily: BOARD_FONT_STACK,
|
|
63
|
+
fontSize: 12,
|
|
64
|
+
fontWeight: "500",
|
|
65
|
+
wordWrap: true
|
|
66
|
+
});
|
|
67
|
+
content.mask = mask;
|
|
68
|
+
root.addChild(background, mask, content, footerBg, title);
|
|
69
|
+
let last = null;
|
|
70
|
+
function paletteKeyOf(palette) {
|
|
71
|
+
return `${palette.surface}|${palette.border}|${palette.hover}|${palette.text}|${palette.muted}`;
|
|
72
|
+
}
|
|
73
|
+
function contentRectFor(width, height, footer) {
|
|
74
|
+
const bottom = footer ? 34 : 12;
|
|
75
|
+
return {
|
|
76
|
+
x: 12,
|
|
77
|
+
y: 12,
|
|
78
|
+
width: Math.max(1, width - 24),
|
|
79
|
+
height: Math.max(1, height - 12 - bottom)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function draw(state, palette, footer) {
|
|
83
|
+
const { width, height, selected, hovered, accent } = state;
|
|
84
|
+
background.clear();
|
|
85
|
+
background.roundRect(0, 0, width, height, 10).fill({
|
|
86
|
+
color: palette.surface,
|
|
87
|
+
alpha: .98
|
|
88
|
+
});
|
|
89
|
+
const borderColor = selected ? accent : hovered ? palette.muted : palette.border;
|
|
90
|
+
background.roundRect(0, 0, width, height, 10).stroke({
|
|
91
|
+
color: borderColor,
|
|
92
|
+
width: selected ? 2 : 1,
|
|
93
|
+
alpha: selected ? .95 : hovered ? .9 : .8
|
|
94
|
+
});
|
|
95
|
+
mask.clear();
|
|
96
|
+
const rect = contentRectFor(width, height, footer);
|
|
97
|
+
mask.roundRect(rect.x, rect.y, rect.width, rect.height, 4).fill({ color: 16777215 });
|
|
98
|
+
footerBg.clear();
|
|
99
|
+
footerBg.visible = footer;
|
|
100
|
+
title.visible = footer;
|
|
101
|
+
if (footer) {
|
|
102
|
+
const fx = 1;
|
|
103
|
+
const fy = height - 34;
|
|
104
|
+
const fw = width - 2;
|
|
105
|
+
const fh = 33;
|
|
106
|
+
const r = 9;
|
|
107
|
+
footerBg.moveTo(fx, fy).lineTo(fx + fw, fy).lineTo(fx + fw, fy + fh - r).arcTo(fx + fw, fy + fh, fx + fw - r, fy + fh, r).lineTo(10, fy + fh).arcTo(fx, fy + fh, fx, fy + fh - r, r).lineTo(fx, fy).fill({
|
|
108
|
+
color: palette.hover,
|
|
109
|
+
alpha: .55
|
|
110
|
+
});
|
|
111
|
+
title.text = state.title;
|
|
112
|
+
title.style.fill = palette.text;
|
|
113
|
+
title.style.wordWrapWidth = width - 24;
|
|
114
|
+
title.x = 12;
|
|
115
|
+
title.y = height - 34 + (34 - title.height) / 2;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function update(state, palette, footer) {
|
|
119
|
+
const paletteKey = paletteKeyOf(palette);
|
|
120
|
+
if (!last || last.width !== state.width || last.height !== state.height || last.selected !== state.selected || last.hovered !== state.hovered || last.accent !== state.accent || last.title !== state.title || last.footer !== footer || last.paletteKey !== paletteKey) {
|
|
121
|
+
draw(state, palette, footer);
|
|
122
|
+
last = {
|
|
123
|
+
...state,
|
|
124
|
+
footer,
|
|
125
|
+
paletteKey
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function destroy() {
|
|
130
|
+
root.destroy({ children: true });
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
root,
|
|
134
|
+
content,
|
|
135
|
+
contentRect: () => contentRectFor(last?.width ?? 0, last?.height ?? 0, last?.footer ?? true),
|
|
136
|
+
update,
|
|
137
|
+
destroy
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Position a shell root at an item frame. The container pivots around its
|
|
142
|
+
* center so that Pixi's rotation matches the geometry model (which treats
|
|
143
|
+
* `frame.rotation` as a rotation about the frame center).
|
|
144
|
+
*/
|
|
145
|
+
function positionShell(root, item) {
|
|
146
|
+
const { x, y, width, height, rotation } = item.frame;
|
|
147
|
+
root.pivot.set(width / 2, height / 2);
|
|
148
|
+
root.x = x + width / 2;
|
|
149
|
+
root.y = y + height / 2;
|
|
150
|
+
root.rotation = (rotation || 0) * Math.PI / 180;
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
export { CARD_PADDING, CARD_RADIUS, FOOTER_HEIGHT, createCardShell, createLabel, emphasisColor, positionShell };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { BoardDocument, BoardItem } from "../../../protocol/dist/board-document.js";
|
|
2
|
+
import { BoardShapeColors } from "../../core/palette.js";
|
|
3
|
+
import { Container, Graphics, Texture } from "pixi.js";
|
|
4
|
+
//#region src/board/render/renderers/board-renderer-registry.d.ts
|
|
5
|
+
type BoardRenderPalette = {
|
|
6
|
+
bg: number;
|
|
7
|
+
surface: number;
|
|
8
|
+
hover: number;
|
|
9
|
+
border: number;
|
|
10
|
+
brand: number;
|
|
11
|
+
text: number;
|
|
12
|
+
muted: number;
|
|
13
|
+
rare: number;
|
|
14
|
+
epic: number;
|
|
15
|
+
legendary: number;
|
|
16
|
+
};
|
|
17
|
+
type BoardRenderContext = {
|
|
18
|
+
document: BoardDocument;
|
|
19
|
+
/**
|
|
20
|
+
* O(1) item lookup by id. Renderers that need to resolve references (arrow
|
|
21
|
+
* bindings) must use this rather than scanning `document.items`, which would
|
|
22
|
+
* be O(items) per card per frame.
|
|
23
|
+
*/
|
|
24
|
+
getItem: (id: string) => BoardItem | null;
|
|
25
|
+
selectedIds: Set<string>;
|
|
26
|
+
hoveredId: string | null;
|
|
27
|
+
/** Nodes currently receiving a live resize preview. */
|
|
28
|
+
resizingIds: Set<string>;
|
|
29
|
+
palette: BoardRenderPalette;
|
|
30
|
+
/**
|
|
31
|
+
* Live shape colors resolved from CSS tokens (theme + space theme.css).
|
|
32
|
+
* Prefer this over hard-coded light/dark tables at render time.
|
|
33
|
+
*/
|
|
34
|
+
colors: BoardShapeColors;
|
|
35
|
+
/** Resolved color mode, for fallback mapping when colors are unavailable. */
|
|
36
|
+
colorScheme: "dark" | "light";
|
|
37
|
+
/** Current camera zoom — used for text re-rasterisation buckets. */
|
|
38
|
+
zoom: number;
|
|
39
|
+
/** Stable preview texture key for an item, or null when it has no preview. */
|
|
40
|
+
assetKey: (item: BoardItem) => string | null;
|
|
41
|
+
/** Currently loaded texture for a key (null while loading). */
|
|
42
|
+
getTexture: (key: string) => Texture | null;
|
|
43
|
+
/** Whether the preview for a key failed to load (for a failure placeholder). */
|
|
44
|
+
hasError: (key: string) => boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Whether a referenced workspace file could not be read, and why. Transient,
|
|
47
|
+
* client-local state — never part of the document — so one client's outage is
|
|
48
|
+
* not shown to everyone.
|
|
49
|
+
*/
|
|
50
|
+
fileState: (path: string) => "ok" | "missing" | "unavailable";
|
|
51
|
+
/** Reference-counted texture acquisition and release. */
|
|
52
|
+
acquireTexture: (key: string) => void;
|
|
53
|
+
releaseTexture: (key: string) => void;
|
|
54
|
+
};
|
|
55
|
+
type BoardCardRenderer = {
|
|
56
|
+
id: string;
|
|
57
|
+
canRender: (item: BoardItem, context: BoardRenderContext) => boolean;
|
|
58
|
+
create: (item: BoardItem, context: BoardRenderContext) => Container;
|
|
59
|
+
/** Efficiently sync an existing container to a (possibly changed) item. */
|
|
60
|
+
update: (container: Container, item: BoardItem, context: BoardRenderContext) => void;
|
|
61
|
+
/**
|
|
62
|
+
* Draw the item as flat batched geometry into a shared `Graphics`, for the
|
|
63
|
+
* far LOD used when too many cards are on screen to afford a container each.
|
|
64
|
+
*
|
|
65
|
+
* Implementing this opts the shape into the far layer: past the scene's
|
|
66
|
+
* visible-card threshold it is drawn here instead of being materialised, so
|
|
67
|
+
* cost stops scaling with node count. Omit it for shapes whose whole point is
|
|
68
|
+
* their vector detail (strokes, arrows, frames) — those keep real containers.
|
|
69
|
+
*
|
|
70
|
+
* Must draw in world space and must not allocate per call beyond the path
|
|
71
|
+
* itself; this runs once per far-capable item on every far-layer rebuild.
|
|
72
|
+
*/
|
|
73
|
+
renderFar?: (graphics: Graphics, item: BoardItem, context: BoardRenderContext) => void;
|
|
74
|
+
destroy?: (container: Container, context: BoardRenderContext) => void;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Every registered renderer, for invariants that must hold across all of them —
|
|
78
|
+
* notably that each can draw itself at far LOD, which is what keeps the far
|
|
79
|
+
* layer's z-order correct (see board-scene).
|
|
80
|
+
*/
|
|
81
|
+
declare function boardCardRenderersForTest(): readonly BoardCardRenderer[];
|
|
82
|
+
declare function getBoardCardRenderer(item: BoardItem, context: BoardRenderContext): BoardCardRenderer;
|
|
83
|
+
declare function registerBoardCardRenderer(renderer: BoardCardRenderer): void;
|
|
84
|
+
//#endregion
|
|
85
|
+
export { BoardCardRenderer, BoardRenderContext, BoardRenderPalette, boardCardRenderersForTest, getBoardCardRenderer, registerBoardCardRenderer };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ensureBoardTextMeasurement } from "../text-measurement.js";
|
|
2
|
+
import { arrowCardRenderer } from "./arrow-card-renderer.js";
|
|
3
|
+
import { drawCardRenderer } from "./draw-card-renderer.js";
|
|
4
|
+
import { fileCardRenderer } from "./file-card-renderer.js";
|
|
5
|
+
import { frameCardRenderer } from "./frame-card-renderer.js";
|
|
6
|
+
import { geoCardRenderer } from "./geo-card-renderer.js";
|
|
7
|
+
import { imageCardRenderer } from "./image-card-renderer.js";
|
|
8
|
+
import { textCardRenderer } from "./text-card-renderer.js";
|
|
9
|
+
import { unknownCardRenderer } from "./unknown-card-renderer.js";
|
|
10
|
+
import { videoCardRenderer } from "./video-card-renderer.js";
|
|
11
|
+
//#region src/board/render/renderers/board-renderer-registry.ts
|
|
12
|
+
const boardCardRenderers = [
|
|
13
|
+
textCardRenderer,
|
|
14
|
+
imageCardRenderer,
|
|
15
|
+
videoCardRenderer,
|
|
16
|
+
fileCardRenderer,
|
|
17
|
+
geoCardRenderer,
|
|
18
|
+
drawCardRenderer,
|
|
19
|
+
arrowCardRenderer,
|
|
20
|
+
frameCardRenderer,
|
|
21
|
+
unknownCardRenderer
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Every registered renderer, for invariants that must hold across all of them —
|
|
25
|
+
* notably that each can draw itself at far LOD, which is what keeps the far
|
|
26
|
+
* layer's z-order correct (see board-scene).
|
|
27
|
+
*/
|
|
28
|
+
function boardCardRenderersForTest() {
|
|
29
|
+
return boardCardRenderers;
|
|
30
|
+
}
|
|
31
|
+
function getBoardCardRenderer(item, context) {
|
|
32
|
+
ensureBoardTextMeasurement();
|
|
33
|
+
return boardCardRenderers.find((renderer) => renderer.canRender(item, context)) ?? unknownCardRenderer;
|
|
34
|
+
}
|
|
35
|
+
function registerBoardCardRenderer(renderer) {
|
|
36
|
+
const existingIndex = boardCardRenderers.findIndex((candidate) => candidate.id === renderer.id);
|
|
37
|
+
if (existingIndex >= 0) boardCardRenderers.splice(existingIndex, 1, renderer);
|
|
38
|
+
else boardCardRenderers.unshift(renderer);
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { boardCardRenderersForTest, getBoardCardRenderer, registerBoardCardRenderer };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { buildStrokeOutline, computeDrawBounds } from "../../core/draw-geometry.js";
|
|
2
|
+
import { pickBoardColor } from "../../core/palette.js";
|
|
3
|
+
import { drawFarStroke } from "./far-plate.js";
|
|
4
|
+
import { positionShell } from "./base-card-renderer.js";
|
|
5
|
+
import { Container, Graphics } from "pixi.js";
|
|
6
|
+
//#region src/board/render/renderers/draw-card-renderer.ts
|
|
7
|
+
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
function sync(container, item, context) {
|
|
9
|
+
const parts = partsByContainer.get(container);
|
|
10
|
+
if (!parts) return;
|
|
11
|
+
positionShell(parts.root, item);
|
|
12
|
+
const selected = context.selectedIds.has(item.id);
|
|
13
|
+
const hovered = context.hoveredId === item.id;
|
|
14
|
+
const color = pickBoardColor(context.colors, item.color, context.colorScheme);
|
|
15
|
+
const sig = [
|
|
16
|
+
item.points.length,
|
|
17
|
+
item.size,
|
|
18
|
+
item.color,
|
|
19
|
+
selected,
|
|
20
|
+
hovered,
|
|
21
|
+
context.colorScheme,
|
|
22
|
+
color.stroke
|
|
23
|
+
].join("|");
|
|
24
|
+
if (sig !== parts.sig || item.points !== parts.points) {
|
|
25
|
+
parts.sig = sig;
|
|
26
|
+
parts.points = item.points;
|
|
27
|
+
parts.baseWidth = computeDrawBounds(item.points, item.size).width;
|
|
28
|
+
parts.stroke.clear();
|
|
29
|
+
const outline = buildStrokeOutline(item.points, item.size);
|
|
30
|
+
const origin = outline[0];
|
|
31
|
+
if (origin && outline.length >= 3) {
|
|
32
|
+
parts.stroke.moveTo(origin.x, origin.y);
|
|
33
|
+
for (let i = 1; i < outline.length; i += 1) {
|
|
34
|
+
const point = outline[i];
|
|
35
|
+
if (point) parts.stroke.lineTo(point.x, point.y);
|
|
36
|
+
}
|
|
37
|
+
parts.stroke.closePath();
|
|
38
|
+
parts.stroke.fill({
|
|
39
|
+
color: color.stroke,
|
|
40
|
+
alpha: selected || hovered ? 1 : .92
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const previewScale = item.frame.width / Math.max(1e-4, parts.baseWidth);
|
|
45
|
+
parts.stroke.scale.set(Number.isFinite(previewScale) ? previewScale : 1);
|
|
46
|
+
}
|
|
47
|
+
const drawCardRenderer = {
|
|
48
|
+
id: "draw-card",
|
|
49
|
+
canRender: (item) => item.type === "draw",
|
|
50
|
+
create: (item, context) => {
|
|
51
|
+
const root = new Container();
|
|
52
|
+
const stroke = new Graphics();
|
|
53
|
+
root.addChild(stroke);
|
|
54
|
+
partsByContainer.set(root, {
|
|
55
|
+
root,
|
|
56
|
+
stroke,
|
|
57
|
+
sig: "",
|
|
58
|
+
points: null,
|
|
59
|
+
baseWidth: 0
|
|
60
|
+
});
|
|
61
|
+
if (item.type === "draw") sync(root, item, context);
|
|
62
|
+
return root;
|
|
63
|
+
},
|
|
64
|
+
update: (container, item, context) => {
|
|
65
|
+
if (item.type === "draw") sync(container, item, context);
|
|
66
|
+
},
|
|
67
|
+
renderFar: (graphics, item, context) => {
|
|
68
|
+
if (item.type !== "draw") return;
|
|
69
|
+
const color = pickBoardColor(context.colors, item.color, context.colorScheme);
|
|
70
|
+
drawFarStroke(graphics, item.points, {
|
|
71
|
+
color: color.stroke,
|
|
72
|
+
width: item.size,
|
|
73
|
+
alpha: .92
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
destroy: (container) => {
|
|
77
|
+
partsByContainer.get(container)?.root.destroy({ children: true });
|
|
78
|
+
partsByContainer.delete(container);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
export { drawCardRenderer };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { BoardFrame } from "../../../protocol/dist/board-document.js";
|
|
2
|
+
import { Graphics } from "pixi.js";
|
|
3
|
+
//#region src/board/render/renderers/far-plate.d.ts
|
|
4
|
+
type FarPlateStyle = {
|
|
5
|
+
/** Plate body fill. */
|
|
6
|
+
fill: number;
|
|
7
|
+
fillAlpha?: number;
|
|
8
|
+
/** Optional accent band along the top edge, identifying the content kind. */
|
|
9
|
+
accent?: number;
|
|
10
|
+
accentAlpha?: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Draw one card as a far-LOD plate: a filled body plus an optional top accent
|
|
14
|
+
* band. Selection and hover are intentionally not drawn here — the interaction
|
|
15
|
+
* overlay already renders selection in world space, above this layer.
|
|
16
|
+
*/
|
|
17
|
+
declare function drawFarPlate(graphics: Graphics, frame: BoardFrame, style: FarPlateStyle): void;
|
|
18
|
+
/**
|
|
19
|
+
* Draw one card as a far-LOD polyline.
|
|
20
|
+
*
|
|
21
|
+
* The counterpart to `drawFarPlate` for shapes whose meaning is a path rather than
|
|
22
|
+
* a region — strokes and arrows. It stays in the shared `Graphics`, so these keep
|
|
23
|
+
* their place in the batch and therefore in document order: an unbatched shape
|
|
24
|
+
* would be a live container, drawn above every plate no matter where the document
|
|
25
|
+
* puts it.
|
|
26
|
+
*
|
|
27
|
+
* Points are decimated to a bound, because a freehand stroke can hold thousands of
|
|
28
|
+
* samples that are sub-pixel apart at this zoom.
|
|
29
|
+
*/
|
|
30
|
+
declare function drawFarStroke(graphics: Graphics, points: ReadonlyArray<{
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
}>, style: {
|
|
34
|
+
color: number;
|
|
35
|
+
width: number;
|
|
36
|
+
alpha?: number;
|
|
37
|
+
}): void;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { FarPlateStyle, drawFarPlate, drawFarStroke };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { degToRad } from "../../geometry.js";
|
|
2
|
+
//#region src/board/render/renderers/far-plate.ts
|
|
3
|
+
/** Upper bound on points kept when a stroke is drawn at far LOD. */
|
|
4
|
+
const FAR_STROKE_MAX_POINTS = 24;
|
|
5
|
+
/** Accent band height as a fraction of the plate, clamped to sane world units. */
|
|
6
|
+
const ACCENT_RATIO = .14;
|
|
7
|
+
const ACCENT_MIN = 2;
|
|
8
|
+
const ACCENT_MAX = 10;
|
|
9
|
+
/**
|
|
10
|
+
* Trace a frame's rectangle, honouring rotation. Unrotated frames take the
|
|
11
|
+
* fast `rect` path; rotated ones emit four explicit corners (still one path,
|
|
12
|
+
* no arcs) so the far layer never needs a per-card transform.
|
|
13
|
+
*/
|
|
14
|
+
function traceFrame(graphics, frame, inset = 0, heightOverride) {
|
|
15
|
+
const x = frame.x + inset;
|
|
16
|
+
const y = frame.y + inset;
|
|
17
|
+
const width = Math.max(0, frame.width - inset * 2);
|
|
18
|
+
const height = Math.max(0, (heightOverride ?? frame.height) - inset * 2);
|
|
19
|
+
if (!frame.rotation) {
|
|
20
|
+
graphics.rect(x, y, width, height);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const cx = frame.x + frame.width / 2;
|
|
24
|
+
const cy = frame.y + frame.height / 2;
|
|
25
|
+
const angle = degToRad(frame.rotation);
|
|
26
|
+
const cos = Math.cos(angle);
|
|
27
|
+
const sin = Math.sin(angle);
|
|
28
|
+
const corner = (px, py) => {
|
|
29
|
+
const dx = px - cx;
|
|
30
|
+
const dy = py - cy;
|
|
31
|
+
return {
|
|
32
|
+
x: cx + dx * cos - dy * sin,
|
|
33
|
+
y: cy + dx * sin + dy * cos
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
const a = corner(x, y);
|
|
37
|
+
const b = corner(x + width, y);
|
|
38
|
+
const c = corner(x + width, y + height);
|
|
39
|
+
const d = corner(x, y + height);
|
|
40
|
+
graphics.moveTo(a.x, a.y).lineTo(b.x, b.y).lineTo(c.x, c.y).lineTo(d.x, d.y).closePath();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Draw one card as a far-LOD plate: a filled body plus an optional top accent
|
|
44
|
+
* band. Selection and hover are intentionally not drawn here — the interaction
|
|
45
|
+
* overlay already renders selection in world space, above this layer.
|
|
46
|
+
*/
|
|
47
|
+
function drawFarPlate(graphics, frame, style) {
|
|
48
|
+
if (frame.width <= 0 || frame.height <= 0) return;
|
|
49
|
+
traceFrame(graphics, frame);
|
|
50
|
+
graphics.fill({
|
|
51
|
+
color: style.fill,
|
|
52
|
+
alpha: style.fillAlpha ?? 1
|
|
53
|
+
});
|
|
54
|
+
if (style.accent === void 0) return;
|
|
55
|
+
traceFrame(graphics, frame, 0, Math.min(ACCENT_MAX, Math.max(ACCENT_MIN, frame.height * ACCENT_RATIO)));
|
|
56
|
+
graphics.fill({
|
|
57
|
+
color: style.accent,
|
|
58
|
+
alpha: style.accentAlpha ?? .9
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Draw one card as a far-LOD polyline.
|
|
63
|
+
*
|
|
64
|
+
* The counterpart to `drawFarPlate` for shapes whose meaning is a path rather than
|
|
65
|
+
* a region — strokes and arrows. It stays in the shared `Graphics`, so these keep
|
|
66
|
+
* their place in the batch and therefore in document order: an unbatched shape
|
|
67
|
+
* would be a live container, drawn above every plate no matter where the document
|
|
68
|
+
* puts it.
|
|
69
|
+
*
|
|
70
|
+
* Points are decimated to a bound, because a freehand stroke can hold thousands of
|
|
71
|
+
* samples that are sub-pixel apart at this zoom.
|
|
72
|
+
*/
|
|
73
|
+
function drawFarStroke(graphics, points, style) {
|
|
74
|
+
if (points.length < 2) return;
|
|
75
|
+
const step = Math.max(1, Math.ceil(points.length / FAR_STROKE_MAX_POINTS));
|
|
76
|
+
const first = points[0];
|
|
77
|
+
if (!first) return;
|
|
78
|
+
graphics.moveTo(first.x, first.y);
|
|
79
|
+
for (let i = step; i < points.length; i += step) {
|
|
80
|
+
const point = points[i];
|
|
81
|
+
if (point) graphics.lineTo(point.x, point.y);
|
|
82
|
+
}
|
|
83
|
+
const last = points[points.length - 1];
|
|
84
|
+
if (last) graphics.lineTo(last.x, last.y);
|
|
85
|
+
graphics.stroke({
|
|
86
|
+
color: style.color,
|
|
87
|
+
width: style.width,
|
|
88
|
+
alpha: style.alpha ?? .9
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { drawFarPlate, drawFarStroke };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { BoardFileItem, BoardItem } from "../../../protocol/dist/board-document.js";
|
|
2
|
+
import { BoardCardRenderer } from "./board-renderer-registry.js";
|
|
3
|
+
import { Text } from "pixi.js";
|
|
4
|
+
//#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
|
+
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
|
+
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
|
+
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
|
+
declare function containCoverRect(width: number, height: number, imageWidth: number, imageHeight: number): {
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
};
|
|
36
|
+
declare const fileCardRenderer: BoardCardRenderer;
|
|
37
|
+
/** Helper kept for type narrowing in tests. */
|
|
38
|
+
declare function isFileItem(item: BoardItem): item is BoardFileItem;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { containCoverRect, ellipsizeWrappedLines, fileCardRenderer, fitLineWithEllipsis, fitTextToLines, isFileItem };
|