@neta-art/cohub 3.0.0 → 3.2.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 +252 -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 +31 -0
- package/dist/board/core/draw-geometry.js +183 -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 +176 -0
- package/dist/board/core/file-preview.js +271 -0
- package/dist/board/core/palette.d.ts +42 -0
- package/dist/board/core/palette.js +138 -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 +144 -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/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 +131 -0
- package/dist/board/geometry.js +468 -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 +16 -0
- package/dist/board/index.js +16 -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 +43 -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 +33 -0
- package/dist/board/render/renderers/file-card-renderer.js +371 -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/note-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/note-card-renderer.js +112 -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 +20 -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 +102 -0
- package/dist/chunks/http.d.ts +10 -40
- package/dist/chunks/http.js +26 -4
- package/dist/chunks/transport.js +4 -2
- package/dist/chunks/websocket.d.ts +832 -21
- package/dist/chunks/websocket.js +19 -2
- package/dist/http.d.ts +2 -3
- 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 +60 -0
- package/dist/protocol/dist/board-document.d.ts +1124 -0
- package/dist/protocol/dist/board-document.js +331 -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 +117 -0
- package/dist/protocol/dist/realtime/types.d.ts +2 -0
- package/package.json +39 -4
- package/dist/board.js +0 -2
- package/dist/chunks/board.d.ts +0 -2589
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
+
import { unknownRealType } from "../../../protocol/dist/board-document.js";
|
|
3
|
+
import { syncTextResolution, 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/unknown-card-renderer.ts
|
|
8
|
+
const RADIUS = 10;
|
|
9
|
+
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
10
|
+
/**
|
|
11
|
+
* Neutral placeholder for shape types this client does not recognise. The shape
|
|
12
|
+
* stays fully interactive (select/move/resize via the generic box definition) and
|
|
13
|
+
* its data is preserved elsewhere — this is only its display.
|
|
14
|
+
*/
|
|
15
|
+
function sync(container, item, context) {
|
|
16
|
+
const parts = partsByContainer.get(container);
|
|
17
|
+
if (!parts) return;
|
|
18
|
+
positionShell(parts.root, item);
|
|
19
|
+
const { width, height } = item.frame;
|
|
20
|
+
const selected = context.selectedIds.has(item.id);
|
|
21
|
+
const realType = unknownRealType(item);
|
|
22
|
+
syncTextResolution(parts.label, parts, context.zoom);
|
|
23
|
+
const visualSig = [
|
|
24
|
+
width,
|
|
25
|
+
height,
|
|
26
|
+
selected,
|
|
27
|
+
context.palette.surface,
|
|
28
|
+
context.palette.brand,
|
|
29
|
+
context.palette.border,
|
|
30
|
+
context.palette.muted
|
|
31
|
+
].join("|");
|
|
32
|
+
if (visualSig !== parts.visualSig) {
|
|
33
|
+
parts.visualSig = visualSig;
|
|
34
|
+
parts.box.clear();
|
|
35
|
+
parts.box.roundRect(0, 0, width, height, RADIUS).fill({
|
|
36
|
+
color: context.palette.surface,
|
|
37
|
+
alpha: .7
|
|
38
|
+
}).roundRect(0, 0, width, height, RADIUS).stroke({
|
|
39
|
+
color: selected ? context.palette.brand : context.palette.border,
|
|
40
|
+
width: selected ? 2 : 1,
|
|
41
|
+
alpha: .8
|
|
42
|
+
});
|
|
43
|
+
parts.box.roundRect(6, 6, width - 12, height - 12, RADIUS - 4).stroke({
|
|
44
|
+
color: context.palette.muted,
|
|
45
|
+
width: 1,
|
|
46
|
+
alpha: .4
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const textSig = [realType, context.palette.muted].join("|");
|
|
50
|
+
if (textSig !== parts.textSig) {
|
|
51
|
+
parts.textSig = textSig;
|
|
52
|
+
parts.label.text = realType;
|
|
53
|
+
parts.label.style.fill = context.palette.muted;
|
|
54
|
+
}
|
|
55
|
+
parts.label.position.set(width / 2, height / 2);
|
|
56
|
+
}
|
|
57
|
+
const unknownCardRenderer = {
|
|
58
|
+
id: "unknown-card",
|
|
59
|
+
canRender: (item) => "raw" in item,
|
|
60
|
+
create: (item, context) => {
|
|
61
|
+
const root = new Container();
|
|
62
|
+
const box = new Graphics();
|
|
63
|
+
const resolution = textResolutionForZoom(context.zoom);
|
|
64
|
+
const label = new Text({
|
|
65
|
+
text: "",
|
|
66
|
+
style: {
|
|
67
|
+
fontFamily: BOARD_MONO_FONT_STACK,
|
|
68
|
+
fontSize: 11,
|
|
69
|
+
fontWeight: "600"
|
|
70
|
+
},
|
|
71
|
+
resolution,
|
|
72
|
+
roundPixels: true
|
|
73
|
+
});
|
|
74
|
+
label.anchor.set(.5);
|
|
75
|
+
root.addChild(box, label);
|
|
76
|
+
partsByContainer.set(root, {
|
|
77
|
+
root,
|
|
78
|
+
box,
|
|
79
|
+
label,
|
|
80
|
+
visualSig: "",
|
|
81
|
+
textSig: "",
|
|
82
|
+
resolution
|
|
83
|
+
});
|
|
84
|
+
if ("raw" in item) sync(root, item, context);
|
|
85
|
+
return root;
|
|
86
|
+
},
|
|
87
|
+
update: (container, item, context) => {
|
|
88
|
+
if ("raw" in item) sync(container, item, context);
|
|
89
|
+
},
|
|
90
|
+
renderFar: (graphics, item, context) => {
|
|
91
|
+
drawFarPlate(graphics, item.frame, {
|
|
92
|
+
fill: context.palette.muted,
|
|
93
|
+
fillAlpha: .2
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
destroy: (container) => {
|
|
97
|
+
partsByContainer.get(container)?.root.destroy({ children: true });
|
|
98
|
+
partsByContainer.delete(container);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
102
|
+
export { unknownCardRenderer };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BoardItem, BoardVideoItem } from "../../../protocol/dist/board-document.js";
|
|
2
|
+
import { BoardCardRenderer } from "./board-renderer-registry.js";
|
|
3
|
+
//#region src/board/render/renderers/video-card-renderer.d.ts
|
|
4
|
+
declare const videoCardRenderer: BoardCardRenderer;
|
|
5
|
+
declare function isVideoItem(item: BoardItem): item is BoardVideoItem;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { isVideoItem, videoCardRenderer };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
|
+
import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
|
|
3
|
+
import { drawFarPlate } from "./far-plate.js";
|
|
4
|
+
import { positionShell } from "./base-card-renderer.js";
|
|
5
|
+
import { Container, Graphics, Sprite, Text, Texture } from "pixi.js";
|
|
6
|
+
//#region src/board/render/renderers/video-card-renderer.ts
|
|
7
|
+
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
const RADIUS = 4;
|
|
9
|
+
function layoutContain(sprite, width, height) {
|
|
10
|
+
const texture = sprite.texture;
|
|
11
|
+
if (!texture.width || !texture.height) return;
|
|
12
|
+
const scale = Math.min(width / texture.width, height / texture.height);
|
|
13
|
+
sprite.width = texture.width * scale;
|
|
14
|
+
sprite.height = texture.height * scale;
|
|
15
|
+
sprite.x = (width - sprite.width) / 2;
|
|
16
|
+
sprite.y = (height - sprite.height) / 2;
|
|
17
|
+
}
|
|
18
|
+
function sync(container, item, context) {
|
|
19
|
+
const parts = partsByContainer.get(container);
|
|
20
|
+
if (!parts) return;
|
|
21
|
+
positionShell(parts.root, item);
|
|
22
|
+
const { width, height } = item.frame;
|
|
23
|
+
const selected = context.selectedIds.has(item.id);
|
|
24
|
+
const hovered = context.hoveredId === item.id;
|
|
25
|
+
const resizing = context.resizingIds.has(item.id);
|
|
26
|
+
const title = item.snapshot?.title ?? item.ref.path.split("/").pop() ?? "Video";
|
|
27
|
+
const key = context.assetKey(item);
|
|
28
|
+
const texture = key ? context.getTexture(key) : null;
|
|
29
|
+
const failed = Boolean(key && !texture && context.hasError(key));
|
|
30
|
+
syncTextResolution(parts.label, parts, context.zoom);
|
|
31
|
+
if (parts.label.text !== title) parts.label.text = title;
|
|
32
|
+
syncTextWrapWidth(parts.label, parts, Math.max(1, width - 16), resizing);
|
|
33
|
+
parts.label.position.set(8, Math.max(8, height - parts.label.height - 8));
|
|
34
|
+
const sig = [
|
|
35
|
+
key ?? "",
|
|
36
|
+
texture ? `${texture.width}x${texture.height}` : "none",
|
|
37
|
+
width,
|
|
38
|
+
height,
|
|
39
|
+
selected,
|
|
40
|
+
hovered,
|
|
41
|
+
failed,
|
|
42
|
+
title,
|
|
43
|
+
parts.label.height,
|
|
44
|
+
context.palette.surface,
|
|
45
|
+
context.palette.brand,
|
|
46
|
+
context.palette.muted,
|
|
47
|
+
context.palette.border,
|
|
48
|
+
context.palette.hover,
|
|
49
|
+
context.palette.text
|
|
50
|
+
].join("|");
|
|
51
|
+
if (sig === parts.sig) return;
|
|
52
|
+
parts.sig = sig;
|
|
53
|
+
if (texture && parts.sprite.texture !== texture) parts.sprite.texture = texture;
|
|
54
|
+
parts.sprite.visible = Boolean(texture);
|
|
55
|
+
if (texture) layoutContain(parts.sprite, width, height);
|
|
56
|
+
parts.mask.clear().roundRect(0, 0, width, height, RADIUS).fill({ color: 16777215 });
|
|
57
|
+
parts.plate.clear().roundRect(0, 0, width, height, RADIUS).fill({
|
|
58
|
+
color: context.palette.surface,
|
|
59
|
+
alpha: .96
|
|
60
|
+
});
|
|
61
|
+
parts.chrome.clear();
|
|
62
|
+
if (texture) {
|
|
63
|
+
const bandHeight = Math.min(height, parts.label.height + 16);
|
|
64
|
+
parts.chrome.rect(0, height - bandHeight, width, bandHeight).fill({
|
|
65
|
+
color: context.palette.surface,
|
|
66
|
+
alpha: .78
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const cx = width / 2;
|
|
70
|
+
const cy = height / 2;
|
|
71
|
+
const radius = Math.min(22, Math.min(width, height) * .18);
|
|
72
|
+
parts.chrome.circle(cx, cy, radius).fill({
|
|
73
|
+
color: selected ? context.palette.brand : context.palette.surface,
|
|
74
|
+
alpha: texture ? .88 : .96
|
|
75
|
+
});
|
|
76
|
+
const triangle = radius * .55;
|
|
77
|
+
parts.chrome.moveTo(cx - triangle * .35, cy - triangle * .6).lineTo(cx - triangle * .35, cy + triangle * .6).lineTo(cx + triangle * .7, cy).closePath().fill({
|
|
78
|
+
color: context.palette.text,
|
|
79
|
+
alpha: .94
|
|
80
|
+
});
|
|
81
|
+
parts.chrome.roundRect(0, 0, width, height, RADIUS).stroke({
|
|
82
|
+
color: selected ? context.palette.brand : hovered ? context.palette.muted : context.palette.border,
|
|
83
|
+
width: selected ? 2 : 1,
|
|
84
|
+
alpha: selected ? .95 : .85
|
|
85
|
+
});
|
|
86
|
+
parts.label.style.fill = texture ? context.palette.text : context.palette.muted;
|
|
87
|
+
}
|
|
88
|
+
const videoCardRenderer = {
|
|
89
|
+
id: "video-card",
|
|
90
|
+
canRender: (item) => item.type === "video",
|
|
91
|
+
create: (item, context) => {
|
|
92
|
+
const root = new Container();
|
|
93
|
+
const plate = new Graphics();
|
|
94
|
+
const sprite = new Sprite(Texture.EMPTY);
|
|
95
|
+
const mask = new Graphics();
|
|
96
|
+
const chrome = new Graphics();
|
|
97
|
+
const resolution = textResolutionForZoom(context.zoom);
|
|
98
|
+
const label = new Text({
|
|
99
|
+
text: "",
|
|
100
|
+
style: {
|
|
101
|
+
fill: context.palette.muted,
|
|
102
|
+
fontFamily: BOARD_FONT_STACK,
|
|
103
|
+
fontSize: 12,
|
|
104
|
+
fontWeight: "500",
|
|
105
|
+
wordWrap: true
|
|
106
|
+
},
|
|
107
|
+
resolution,
|
|
108
|
+
roundPixels: true
|
|
109
|
+
});
|
|
110
|
+
root.addChild(plate, sprite, mask, chrome, label);
|
|
111
|
+
sprite.mask = mask;
|
|
112
|
+
partsByContainer.set(root, {
|
|
113
|
+
root,
|
|
114
|
+
plate,
|
|
115
|
+
sprite,
|
|
116
|
+
mask,
|
|
117
|
+
chrome,
|
|
118
|
+
label,
|
|
119
|
+
sig: "",
|
|
120
|
+
wrapWidth: 0,
|
|
121
|
+
resolution
|
|
122
|
+
});
|
|
123
|
+
if (item.type === "video") sync(root, item, context);
|
|
124
|
+
return root;
|
|
125
|
+
},
|
|
126
|
+
update: (container, item, context) => {
|
|
127
|
+
if (item.type === "video") sync(container, item, context);
|
|
128
|
+
},
|
|
129
|
+
renderFar: (graphics, item, context) => {
|
|
130
|
+
drawFarPlate(graphics, item.frame, {
|
|
131
|
+
fill: context.palette.surface,
|
|
132
|
+
fillAlpha: .96,
|
|
133
|
+
accent: context.palette.brand,
|
|
134
|
+
accentAlpha: .75
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
destroy: (container) => {
|
|
138
|
+
partsByContainer.get(container)?.root.destroy({ children: true });
|
|
139
|
+
partsByContainer.delete(container);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
function isVideoItem(item) {
|
|
143
|
+
return item.type === "video";
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
export { isVideoItem, videoCardRenderer };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/board/render/text-measurement.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Canvas-backed text measurement.
|
|
4
|
+
*
|
|
5
|
+
* Measurement goes through Pixi's `DOMAdapter` rather than `document`, so the
|
|
6
|
+
* browser and a headless export produce the same numbers. The core metrics
|
|
7
|
+
* module owns the layout maths and only borrows this measurer, which is what
|
|
8
|
+
* lets `@neta-art/cohub/board` lay text out with no PixiJS installed.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Route board text measurement through the active Pixi DOM adapter.
|
|
12
|
+
*
|
|
13
|
+
* Call this after swapping the adapter (see `../headless`), which drops the
|
|
14
|
+
* cached measuring context. Anything that goes through `getBoardCardRenderer`
|
|
15
|
+
* is already covered by `ensureBoardTextMeasurement`.
|
|
16
|
+
*/
|
|
17
|
+
declare function installBoardTextMeasurement(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Install the canvas measurer once, unless it already is.
|
|
20
|
+
*
|
|
21
|
+
* The core metrics module falls back to a per-character estimate when no
|
|
22
|
+
* measurer is set, which is off by ~90% for real text — wrong enough to
|
|
23
|
+
* misplace every label. Rather than make that a step consumers can forget, the
|
|
24
|
+
* renderer registry calls this before handing a renderer out. It cannot be an
|
|
25
|
+
* import-time side effect: the package is marked side-effect free, so a bundler
|
|
26
|
+
* is entitled to drop a module whose only job happens at load time.
|
|
27
|
+
*/
|
|
28
|
+
declare function ensureBoardTextMeasurement(): void;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { ensureBoardTextMeasurement, installBoardTextMeasurement };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { BOARD_FONT_STACK } from "../../protocol/dist/board-constants.js";
|
|
2
|
+
import { setBoardTextMeasurer } from "../core/text-metrics.js";
|
|
3
|
+
import { DOMAdapter } from "pixi.js";
|
|
4
|
+
//#region src/board/render/text-measurement.ts
|
|
5
|
+
/**
|
|
6
|
+
* Canvas-backed text measurement.
|
|
7
|
+
*
|
|
8
|
+
* Measurement goes through Pixi's `DOMAdapter` rather than `document`, so the
|
|
9
|
+
* browser and a headless export produce the same numbers. The core metrics
|
|
10
|
+
* module owns the layout maths and only borrows this measurer, which is what
|
|
11
|
+
* lets `@neta-art/cohub/board` lay text out with no PixiJS installed.
|
|
12
|
+
*/
|
|
13
|
+
let measureContext;
|
|
14
|
+
let installed = false;
|
|
15
|
+
function getMeasureContext() {
|
|
16
|
+
if (measureContext !== void 0) return measureContext;
|
|
17
|
+
try {
|
|
18
|
+
measureContext = DOMAdapter.get().createCanvas(1, 1).getContext("2d") ?? null;
|
|
19
|
+
} catch {
|
|
20
|
+
measureContext = null;
|
|
21
|
+
}
|
|
22
|
+
return measureContext;
|
|
23
|
+
}
|
|
24
|
+
function measureText(text, fontSize) {
|
|
25
|
+
const context = getMeasureContext();
|
|
26
|
+
if (!context) return null;
|
|
27
|
+
context.font = `500 ${fontSize}px ${BOARD_FONT_STACK}`;
|
|
28
|
+
return context.measureText(text).width;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Route board text measurement through the active Pixi DOM adapter.
|
|
32
|
+
*
|
|
33
|
+
* Call this after swapping the adapter (see `../headless`), which drops the
|
|
34
|
+
* cached measuring context. Anything that goes through `getBoardCardRenderer`
|
|
35
|
+
* is already covered by `ensureBoardTextMeasurement`.
|
|
36
|
+
*/
|
|
37
|
+
function installBoardTextMeasurement() {
|
|
38
|
+
measureContext = void 0;
|
|
39
|
+
installed = true;
|
|
40
|
+
setBoardTextMeasurer(measureText);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Install the canvas measurer once, unless it already is.
|
|
44
|
+
*
|
|
45
|
+
* The core metrics module falls back to a per-character estimate when no
|
|
46
|
+
* measurer is set, which is off by ~90% for real text — wrong enough to
|
|
47
|
+
* misplace every label. Rather than make that a step consumers can forget, the
|
|
48
|
+
* renderer registry calls this before handing a renderer out. It cannot be an
|
|
49
|
+
* import-time side effect: the package is marked side-effect free, so a bundler
|
|
50
|
+
* is entitled to drop a module whose only job happens at load time.
|
|
51
|
+
*/
|
|
52
|
+
function ensureBoardTextMeasurement() {
|
|
53
|
+
if (installed) return;
|
|
54
|
+
installBoardTextMeasurement();
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { ensureBoardTextMeasurement, installBoardTextMeasurement };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Text } from "pixi.js";
|
|
2
|
+
//#region src/board/render/text-resolution.d.ts
|
|
3
|
+
declare function getBoardResolution(): number;
|
|
4
|
+
declare function textZoomBucket(zoom: number): number;
|
|
5
|
+
/** Effective text resolution for a given camera zoom. */
|
|
6
|
+
declare function textResolutionForZoom(zoom: number): number;
|
|
7
|
+
/** Update a Pixi text texture only when zoom crosses a resolution bucket. */
|
|
8
|
+
declare function syncTextResolution(text: Text, state: {
|
|
9
|
+
resolution: number;
|
|
10
|
+
}, zoom: number): void;
|
|
11
|
+
/** Defer expensive Pixi word-wrap rasterisation during a live resize. */
|
|
12
|
+
declare function syncTextWrapWidth(text: Text, state: {
|
|
13
|
+
wrapWidth: number;
|
|
14
|
+
}, width: number, defer: boolean): void;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/board/render/text-resolution.ts
|
|
2
|
+
const MAX_BOARD_RESOLUTION = 2;
|
|
3
|
+
function getBoardResolution() {
|
|
4
|
+
return Math.min(globalThis.devicePixelRatio || 1, MAX_BOARD_RESOLUTION);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Zoom buckets for text re-rasterisation. Pixi Text is a bitmap: at high zoom it
|
|
8
|
+
* looks soft unless we raise its resolution. We quantise zoom into a few
|
|
9
|
+
* buckets so small zoom changes don't thrash texture regeneration.
|
|
10
|
+
*/
|
|
11
|
+
const TEXT_ZOOM_BUCKETS = [
|
|
12
|
+
.5,
|
|
13
|
+
1,
|
|
14
|
+
1.5,
|
|
15
|
+
2,
|
|
16
|
+
3,
|
|
17
|
+
4
|
|
18
|
+
];
|
|
19
|
+
function textZoomBucket(zoom) {
|
|
20
|
+
const clamped = Math.max(.1, Math.min(4, zoom));
|
|
21
|
+
for (const bucket of TEXT_ZOOM_BUCKETS) if (clamped <= bucket) return bucket;
|
|
22
|
+
return TEXT_ZOOM_BUCKETS[TEXT_ZOOM_BUCKETS.length - 1] ?? 4;
|
|
23
|
+
}
|
|
24
|
+
/** Effective text resolution for a given camera zoom. */
|
|
25
|
+
function textResolutionForZoom(zoom) {
|
|
26
|
+
const bucket = textZoomBucket(zoom);
|
|
27
|
+
return Math.min(getBoardResolution() * Math.max(1, bucket), MAX_BOARD_RESOLUTION * 3);
|
|
28
|
+
}
|
|
29
|
+
/** Update a Pixi text texture only when zoom crosses a resolution bucket. */
|
|
30
|
+
function syncTextResolution(text, state, zoom) {
|
|
31
|
+
const resolution = textResolutionForZoom(zoom);
|
|
32
|
+
if (resolution === state.resolution) return;
|
|
33
|
+
text.resolution = resolution;
|
|
34
|
+
state.resolution = resolution;
|
|
35
|
+
}
|
|
36
|
+
/** Defer expensive Pixi word-wrap rasterisation during a live resize. */
|
|
37
|
+
function syncTextWrapWidth(text, state, width, defer) {
|
|
38
|
+
if (defer || width === state.wrapWidth) return;
|
|
39
|
+
text.style.wordWrapWidth = width;
|
|
40
|
+
state.wrapWidth = width;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BoardDocument, BoardViewport } from "../../../protocol/dist/board-document.js";
|
|
2
|
+
import { BoardRenderPalette } from "../renderers/board-renderer-registry.js";
|
|
3
|
+
import { Application, Container } from "pixi.js";
|
|
4
|
+
//#region src/board/render/themes/board-theme-registry.d.ts
|
|
5
|
+
type BoardThemeContext = {
|
|
6
|
+
app: Application;
|
|
7
|
+
document: BoardDocument;
|
|
8
|
+
viewport: BoardViewport;
|
|
9
|
+
palette: BoardRenderPalette;
|
|
10
|
+
};
|
|
11
|
+
type BoardThemeRenderer = {
|
|
12
|
+
id: string;
|
|
13
|
+
canRender: (document: BoardDocument) => boolean;
|
|
14
|
+
createBackground: (context: BoardThemeContext) => Container;
|
|
15
|
+
updateBackground?: (container: Container, context: BoardThemeContext) => void;
|
|
16
|
+
};
|
|
17
|
+
declare function getBoardThemeRenderer(document: BoardDocument): BoardThemeRenderer;
|
|
18
|
+
declare function registerBoardThemeRenderer(renderer: BoardThemeRenderer): void;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { BoardThemeContext, BoardThemeRenderer, getBoardThemeRenderer, registerBoardThemeRenderer };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cleanBoardTheme } from "./clean-theme.js";
|
|
2
|
+
//#region src/board/render/themes/board-theme-registry.ts
|
|
3
|
+
const boardThemeRenderers = [cleanBoardTheme];
|
|
4
|
+
function getBoardThemeRenderer(document) {
|
|
5
|
+
return boardThemeRenderers.find((renderer) => renderer.canRender(document)) ?? cleanBoardTheme;
|
|
6
|
+
}
|
|
7
|
+
function registerBoardThemeRenderer(renderer) {
|
|
8
|
+
const existingIndex = boardThemeRenderers.findIndex((candidate) => candidate.id === renderer.id);
|
|
9
|
+
if (existingIndex >= 0) boardThemeRenderers.splice(existingIndex, 1, renderer);
|
|
10
|
+
else boardThemeRenderers.unshift(renderer);
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { getBoardThemeRenderer, registerBoardThemeRenderer };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Container, Graphics, RenderTexture, TilingSprite } from "pixi.js";
|
|
2
|
+
//#region src/board/render/themes/clean-theme.ts
|
|
3
|
+
const partsByContainer = /* @__PURE__ */ new WeakMap();
|
|
4
|
+
/** Positive modulo so tile offsets stay valid for negative viewport offsets. */
|
|
5
|
+
function wrap(value, period) {
|
|
6
|
+
return (value % period + period) % period;
|
|
7
|
+
}
|
|
8
|
+
function buildGridTexture(context, size, color, opacity, kind = "dots") {
|
|
9
|
+
const graphics = new Graphics();
|
|
10
|
+
if (kind === "grid") graphics.moveTo(0, .5).lineTo(size, .5).moveTo(.5, 0).lineTo(.5, size).stroke({
|
|
11
|
+
color,
|
|
12
|
+
width: 1,
|
|
13
|
+
alpha: opacity
|
|
14
|
+
});
|
|
15
|
+
else graphics.circle(.5, .5, .9).fill({
|
|
16
|
+
color,
|
|
17
|
+
alpha: opacity
|
|
18
|
+
});
|
|
19
|
+
const target = RenderTexture.create({
|
|
20
|
+
width: size,
|
|
21
|
+
height: size
|
|
22
|
+
});
|
|
23
|
+
context.app.renderer.render({
|
|
24
|
+
container: graphics,
|
|
25
|
+
target
|
|
26
|
+
});
|
|
27
|
+
graphics.destroy();
|
|
28
|
+
return target;
|
|
29
|
+
}
|
|
30
|
+
function sync(parts, context) {
|
|
31
|
+
const { app, document, viewport, palette } = context;
|
|
32
|
+
const width = app.screen.width;
|
|
33
|
+
const height = app.screen.height;
|
|
34
|
+
if (parts.lastWidth !== width || parts.lastHeight !== height || parts.lastBg !== palette.bg) {
|
|
35
|
+
parts.fill.clear();
|
|
36
|
+
parts.fill.rect(0, 0, width, height).fill({
|
|
37
|
+
color: palette.bg,
|
|
38
|
+
alpha: 1
|
|
39
|
+
});
|
|
40
|
+
parts.lastWidth = width;
|
|
41
|
+
parts.lastHeight = height;
|
|
42
|
+
parts.lastBg = palette.bg;
|
|
43
|
+
}
|
|
44
|
+
const appearance = document.appearance;
|
|
45
|
+
const visible = appearance.grid?.visible === true;
|
|
46
|
+
const size = Math.max(4, appearance.grid?.size ?? 24);
|
|
47
|
+
const opacity = appearance.grid?.opacity ?? .12;
|
|
48
|
+
const kind = appearance.background?.kind === "grid" ? "grid" : "dots";
|
|
49
|
+
const key = `${kind}|${size}|${palette.border}|${opacity}`;
|
|
50
|
+
if (!visible) {
|
|
51
|
+
if (parts.sprite) parts.sprite.visible = false;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (parts.textureKey !== key || !parts.sprite) {
|
|
55
|
+
if (parts.sprite) {
|
|
56
|
+
const previousTexture = parts.sprite.texture;
|
|
57
|
+
parts.sprite.destroy();
|
|
58
|
+
previousTexture.destroy(true);
|
|
59
|
+
}
|
|
60
|
+
parts.sprite = new TilingSprite({
|
|
61
|
+
texture: buildGridTexture(context, size, palette.border, opacity, kind),
|
|
62
|
+
width,
|
|
63
|
+
height
|
|
64
|
+
});
|
|
65
|
+
parts.textureKey = key;
|
|
66
|
+
parts.fill.parent?.addChild(parts.sprite);
|
|
67
|
+
}
|
|
68
|
+
const sprite = parts.sprite;
|
|
69
|
+
if (!sprite) return;
|
|
70
|
+
sprite.visible = true;
|
|
71
|
+
sprite.width = width;
|
|
72
|
+
sprite.height = height;
|
|
73
|
+
const step = size * viewport.zoom;
|
|
74
|
+
sprite.tileScale.set(viewport.zoom);
|
|
75
|
+
sprite.tilePosition.set(wrap(viewport.x, step), wrap(viewport.y, step));
|
|
76
|
+
}
|
|
77
|
+
const cleanBoardTheme = {
|
|
78
|
+
id: "clean",
|
|
79
|
+
canRender: () => true,
|
|
80
|
+
createBackground: (context) => {
|
|
81
|
+
const container = new Container();
|
|
82
|
+
const fill = new Graphics();
|
|
83
|
+
container.addChild(fill);
|
|
84
|
+
const parts = {
|
|
85
|
+
fill,
|
|
86
|
+
sprite: null,
|
|
87
|
+
textureKey: "",
|
|
88
|
+
lastWidth: -1,
|
|
89
|
+
lastHeight: -1,
|
|
90
|
+
lastBg: NaN
|
|
91
|
+
};
|
|
92
|
+
partsByContainer.set(container, parts);
|
|
93
|
+
sync(parts, context);
|
|
94
|
+
return container;
|
|
95
|
+
},
|
|
96
|
+
updateBackground: (container, context) => {
|
|
97
|
+
const parts = partsByContainer.get(container);
|
|
98
|
+
if (parts) sync(parts, context);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
102
|
+
export { cleanBoardTheme };
|