@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.
Files changed (96) hide show
  1. package/README.md +33 -2
  2. package/dist/{board.d.ts → board/animation.d.ts} +4 -2
  3. package/dist/{chunks/board.js → board/animation.js} +5 -44
  4. package/dist/board/codec.d.ts +25 -0
  5. package/dist/board/codec.js +252 -0
  6. package/dist/board/core/bindings.d.ts +45 -0
  7. package/dist/board/core/bindings.js +162 -0
  8. package/dist/board/core/draw-geometry.d.ts +31 -0
  9. package/dist/board/core/draw-geometry.js +183 -0
  10. package/dist/board/core/export-assets.d.ts +14 -0
  11. package/dist/board/core/export-assets.js +22 -0
  12. package/dist/board/core/export-plan.d.ts +84 -0
  13. package/dist/board/core/export-plan.js +128 -0
  14. package/dist/board/core/file-preview.d.ts +176 -0
  15. package/dist/board/core/file-preview.js +271 -0
  16. package/dist/board/core/palette.d.ts +42 -0
  17. package/dist/board/core/palette.js +138 -0
  18. package/dist/board/core/shape-definition.d.ts +35 -0
  19. package/dist/board/core/shape-definition.js +64 -0
  20. package/dist/board/core/shape-types.d.ts +144 -0
  21. package/dist/board/core/shape-types.js +27 -0
  22. package/dist/board/core/text-metrics.d.ts +18 -0
  23. package/dist/board/core/text-metrics.js +43 -0
  24. package/dist/board/export/index.d.ts +56 -0
  25. package/dist/board/export/index.js +86 -0
  26. package/dist/board/export/scene.d.ts +35 -0
  27. package/dist/board/export/scene.js +69 -0
  28. package/dist/board/geometry.d.ts +131 -0
  29. package/dist/board/geometry.js +468 -0
  30. package/dist/board/headless/index.d.ts +69 -0
  31. package/dist/board/headless/index.js +150 -0
  32. package/dist/board/image-key.d.ts +19 -0
  33. package/dist/board/image-key.js +34 -0
  34. package/dist/board/index.d.ts +16 -0
  35. package/dist/board/index.js +16 -0
  36. package/dist/board/render/index.d.ts +6 -0
  37. package/dist/board/render/index.js +6 -0
  38. package/dist/board/render/palette.d.ts +5 -0
  39. package/dist/board/render/palette.js +30 -0
  40. package/dist/board/render/renderers/arrow-card-renderer.d.ts +5 -0
  41. package/dist/board/render/renderers/arrow-card-renderer.js +157 -0
  42. package/dist/board/render/renderers/base-card-renderer.d.ts +46 -0
  43. package/dist/board/render/renderers/base-card-renderer.js +153 -0
  44. package/dist/board/render/renderers/board-renderer-registry.d.ts +85 -0
  45. package/dist/board/render/renderers/board-renderer-registry.js +43 -0
  46. package/dist/board/render/renderers/draw-card-renderer.d.ts +5 -0
  47. package/dist/board/render/renderers/draw-card-renderer.js +82 -0
  48. package/dist/board/render/renderers/far-plate.d.ts +39 -0
  49. package/dist/board/render/renderers/far-plate.js +92 -0
  50. package/dist/board/render/renderers/file-card-renderer.d.ts +33 -0
  51. package/dist/board/render/renderers/file-card-renderer.js +371 -0
  52. package/dist/board/render/renderers/frame-card-renderer.d.ts +5 -0
  53. package/dist/board/render/renderers/frame-card-renderer.js +99 -0
  54. package/dist/board/render/renderers/geo-card-renderer.d.ts +5 -0
  55. package/dist/board/render/renderers/geo-card-renderer.js +128 -0
  56. package/dist/board/render/renderers/image-card-renderer.d.ts +8 -0
  57. package/dist/board/render/renderers/image-card-renderer.js +138 -0
  58. package/dist/board/render/renderers/note-card-renderer.d.ts +5 -0
  59. package/dist/board/render/renderers/note-card-renderer.js +112 -0
  60. package/dist/board/render/renderers/text-card-renderer.d.ts +7 -0
  61. package/dist/board/render/renderers/text-card-renderer.js +94 -0
  62. package/dist/board/render/renderers/unknown-card-renderer.d.ts +5 -0
  63. package/dist/board/render/renderers/unknown-card-renderer.js +102 -0
  64. package/dist/board/render/renderers/video-card-renderer.d.ts +7 -0
  65. package/dist/board/render/renderers/video-card-renderer.js +146 -0
  66. package/dist/board/render/text-measurement.d.ts +30 -0
  67. package/dist/board/render/text-measurement.js +57 -0
  68. package/dist/board/render/text-resolution.d.ts +16 -0
  69. package/dist/board/render/text-resolution.js +43 -0
  70. package/dist/board/render/themes/board-theme-registry.d.ts +20 -0
  71. package/dist/board/render/themes/board-theme-registry.js +13 -0
  72. package/dist/board/render/themes/clean-theme.d.ts +5 -0
  73. package/dist/board/render/themes/clean-theme.js +102 -0
  74. package/dist/chunks/http.d.ts +10 -40
  75. package/dist/chunks/http.js +26 -4
  76. package/dist/chunks/transport.js +4 -2
  77. package/dist/chunks/websocket.d.ts +832 -21
  78. package/dist/chunks/websocket.js +19 -2
  79. package/dist/http.d.ts +2 -3
  80. package/dist/index.d.ts +100 -5
  81. package/dist/index.js +391 -1
  82. package/dist/protocol/dist/board-constants.d.ts +23 -0
  83. package/dist/protocol/dist/board-constants.js +60 -0
  84. package/dist/protocol/dist/board-document.d.ts +1124 -0
  85. package/dist/protocol/dist/board-document.js +331 -0
  86. package/dist/protocol/dist/board.d.ts +186 -0
  87. package/dist/protocol/dist/board.js +207 -0
  88. package/dist/protocol/dist/index.d.ts +5 -0
  89. package/dist/protocol/dist/index.js +5 -0
  90. package/dist/protocol/dist/provenance.js +12 -0
  91. package/dist/protocol/dist/realtime/board-awareness.d.ts +1 -0
  92. package/dist/protocol/dist/realtime/board-awareness.js +117 -0
  93. package/dist/protocol/dist/realtime/types.d.ts +2 -0
  94. package/package.json +39 -4
  95. package/dist/board.js +0 -2
  96. 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,5 @@
1
+ import { BoardThemeRenderer } from "./board-theme-registry.js";
2
+ //#region src/board/render/themes/clean-theme.d.ts
3
+ declare const cleanBoardTheme: BoardThemeRenderer;
4
+ //#endregion
5
+ export { cleanBoardTheme };
@@ -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 };