@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,140 @@
|
|
|
1
|
+
import { BoardFrame } from "../../protocol/dist/board-document.js";
|
|
2
|
+
import { Rect, WorldPoint } from "../geometry.js";
|
|
3
|
+
//#region src/board/core/shape-types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A shape's geometry, expressed in the shape's local space (origin at the
|
|
6
|
+
* frame's top-left, unrotated). Hit testing and handles are derived from this,
|
|
7
|
+
* so a shape only declares its outline once.
|
|
8
|
+
*/
|
|
9
|
+
type ShapeGeometry = {
|
|
10
|
+
/** Axis-aligned bounds in local space (usually the full frame). */
|
|
11
|
+
bounds: Rect;
|
|
12
|
+
/**
|
|
13
|
+
* Exact point containment test in *world* space. Receives the shape frame so
|
|
14
|
+
* it can account for rotation. Defaults to the rotated-rect test when absent.
|
|
15
|
+
*/
|
|
16
|
+
containsWorldPoint?: (frame: BoardFrame, point: WorldPoint) => boolean;
|
|
17
|
+
};
|
|
18
|
+
type ShapeHandleId = string;
|
|
19
|
+
type ShapeHandle = {
|
|
20
|
+
id: ShapeHandleId;
|
|
21
|
+
/** Position in the shape's local (unrotated, frame-origin) space. */
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
/** Visual + hit radius hint in screen px; the stage scales by zoom. */
|
|
25
|
+
radius?: number;
|
|
26
|
+
};
|
|
27
|
+
/** Result of dragging a handle: a patched frame and/or shape props. */
|
|
28
|
+
type HandleDragResult = {
|
|
29
|
+
frame?: BoardFrame;
|
|
30
|
+
props?: Record<string, unknown>;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* What a shape supports. The editor, selection toolbar and tools read these so
|
|
34
|
+
* behaviour is data-driven: adding a shape never requires editing the editor.
|
|
35
|
+
*/
|
|
36
|
+
type ShapeCapabilities = {
|
|
37
|
+
/** Can be moved by dragging. */
|
|
38
|
+
canMove: boolean;
|
|
39
|
+
/** Shows resize handles. */
|
|
40
|
+
canResize: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Resize always preserves the shape's aspect ratio, regardless of Shift.
|
|
43
|
+
* True for shapes whose content has a single intrinsic scale (text font size,
|
|
44
|
+
* media pixels, stroke geometry) — distorting the frame would either letterbox
|
|
45
|
+
* the content or have no representation in the data model.
|
|
46
|
+
*/
|
|
47
|
+
aspectLocked: boolean;
|
|
48
|
+
/** Shows the rotation handle. */
|
|
49
|
+
canRotate: boolean;
|
|
50
|
+
/** Supports double-click inline editing. */
|
|
51
|
+
canEdit: boolean;
|
|
52
|
+
/** Can be a binding target for arrows. */
|
|
53
|
+
canBind: boolean;
|
|
54
|
+
/** Participates in snapping as a target. */
|
|
55
|
+
canSnap: boolean;
|
|
56
|
+
/** Can be locked against accidental edits. */
|
|
57
|
+
canLock: boolean;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Effective resize behaviour derived from the backwards-compatible capability
|
|
61
|
+
* flags. Consumers should use this mode instead of repeating the flag matrix.
|
|
62
|
+
*/
|
|
63
|
+
type ShapeResizeMode = "none" | "uniform" | "free";
|
|
64
|
+
declare function resizeModeForCapabilities(capabilities: ShapeCapabilities): ShapeResizeMode;
|
|
65
|
+
declare const FULL_CAPABILITIES: ShapeCapabilities;
|
|
66
|
+
/**
|
|
67
|
+
* A single raw input sample of a freehand stroke. We persist the *raw* samples
|
|
68
|
+
* (not just a simplified path) so strokes can be re-smoothed, re-simplified or
|
|
69
|
+
* re-rendered at any LOD later without losing information — data first.
|
|
70
|
+
*/
|
|
71
|
+
type DrawPoint = {
|
|
72
|
+
x: number;
|
|
73
|
+
y: number;
|
|
74
|
+
/** Pen pressure 0..1; 0.5 when unavailable (mouse). */
|
|
75
|
+
p: number;
|
|
76
|
+
};
|
|
77
|
+
type DrawShapeProps = {
|
|
78
|
+
/** Raw samples in the shape's local space. */
|
|
79
|
+
points: DrawPoint[];
|
|
80
|
+
color: string;
|
|
81
|
+
/** Stroke width in world units. */
|
|
82
|
+
size: number;
|
|
83
|
+
};
|
|
84
|
+
type GeoKind = "rectangle" | "ellipse" | "diamond" | "triangle" | "rounded";
|
|
85
|
+
declare const GEO_KINDS: readonly GeoKind[];
|
|
86
|
+
declare function isGeoKind(value: unknown): value is GeoKind;
|
|
87
|
+
type GeoShapeProps = {
|
|
88
|
+
geo: GeoKind;
|
|
89
|
+
/** Optional label rendered centered inside the shape. */
|
|
90
|
+
text: string;
|
|
91
|
+
color: string;
|
|
92
|
+
/** Fill opacity 0..1 (0 = outline only). */
|
|
93
|
+
fillOpacity: number;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* An arrow endpoint: either a free point (local space) or a binding to another
|
|
97
|
+
* shape. Bindings use a normalized anchor (0..1 of the target's frame) so the
|
|
98
|
+
* arrow tracks the target through move/resize without storing absolute coords.
|
|
99
|
+
*/
|
|
100
|
+
type ArrowEndpoint = {
|
|
101
|
+
kind: "point";
|
|
102
|
+
x: number;
|
|
103
|
+
y: number;
|
|
104
|
+
} | {
|
|
105
|
+
kind: "binding";
|
|
106
|
+
/** Target shape id. */
|
|
107
|
+
target: string;
|
|
108
|
+
/** Normalized anchor on the target frame (0..1). */
|
|
109
|
+
nx: number;
|
|
110
|
+
ny: number;
|
|
111
|
+
/** Whether the binding snaps to the nearest edge/center. */
|
|
112
|
+
precise: boolean;
|
|
113
|
+
};
|
|
114
|
+
type ArrowShapeProps = {
|
|
115
|
+
start: ArrowEndpoint;
|
|
116
|
+
end: ArrowEndpoint;
|
|
117
|
+
/** Bend of a curved arrow, as a fraction of the length (-0.5..0.5). */
|
|
118
|
+
bend: number;
|
|
119
|
+
color: string;
|
|
120
|
+
size: number;
|
|
121
|
+
/** Arrowhead placement. */
|
|
122
|
+
arrowStart: boolean;
|
|
123
|
+
arrowEnd: boolean;
|
|
124
|
+
label: string;
|
|
125
|
+
};
|
|
126
|
+
type TextShapeProps = {
|
|
127
|
+
text: string;
|
|
128
|
+
color: string;
|
|
129
|
+
fontSize: number;
|
|
130
|
+
};
|
|
131
|
+
type ImageShapeProps = {
|
|
132
|
+
path: string;
|
|
133
|
+
mimeType?: string;
|
|
134
|
+
};
|
|
135
|
+
type VideoShapeProps = {
|
|
136
|
+
path: string;
|
|
137
|
+
mimeType?: string;
|
|
138
|
+
};
|
|
139
|
+
//#endregion
|
|
140
|
+
export { ArrowEndpoint, ArrowShapeProps, DrawPoint, DrawShapeProps, FULL_CAPABILITIES, GEO_KINDS, GeoKind, GeoShapeProps, HandleDragResult, ImageShapeProps, ShapeCapabilities, ShapeGeometry, ShapeHandle, ShapeHandleId, ShapeResizeMode, TextShapeProps, VideoShapeProps, isGeoKind, resizeModeForCapabilities };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/board/core/shape-types.ts
|
|
2
|
+
function resizeModeForCapabilities(capabilities) {
|
|
3
|
+
if (!capabilities.canResize) return "none";
|
|
4
|
+
return capabilities.aspectLocked ? "uniform" : "free";
|
|
5
|
+
}
|
|
6
|
+
const FULL_CAPABILITIES = {
|
|
7
|
+
canMove: true,
|
|
8
|
+
canResize: true,
|
|
9
|
+
aspectLocked: false,
|
|
10
|
+
canRotate: true,
|
|
11
|
+
canEdit: false,
|
|
12
|
+
canBind: true,
|
|
13
|
+
canSnap: true,
|
|
14
|
+
canLock: true
|
|
15
|
+
};
|
|
16
|
+
const GEO_KINDS = [
|
|
17
|
+
"rectangle",
|
|
18
|
+
"rounded",
|
|
19
|
+
"ellipse",
|
|
20
|
+
"diamond",
|
|
21
|
+
"triangle"
|
|
22
|
+
];
|
|
23
|
+
function isGeoKind(value) {
|
|
24
|
+
return typeof value === "string" && GEO_KINDS.includes(value);
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { FULL_CAPABILITIES, GEO_KINDS, isGeoKind, resizeModeForCapabilities };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/board/core/text-metrics.d.ts
|
|
2
|
+
declare const TEXT_FONT_FAMILY = "Geist";
|
|
3
|
+
declare const TEXT_FONT_SIZE = 24;
|
|
4
|
+
declare const TEXT_LINE_HEIGHT = 32;
|
|
5
|
+
declare const TEXT_MIN_FONT_SIZE = 2;
|
|
6
|
+
declare const TEXT_MAX_FONT_SIZE = 512;
|
|
7
|
+
type BoardTextMeasurer = (text: string, fontSize: number) => number | null;
|
|
8
|
+
/** Install the renderer's text measurer, or clear it for environment-free fallback metrics. */
|
|
9
|
+
declare function setBoardTextMeasurer(measurer?: BoardTextMeasurer): void;
|
|
10
|
+
declare function clampBoardTextFontSize(fontSize: number): number;
|
|
11
|
+
declare function boardTextLineHeight(fontSize: number): number;
|
|
12
|
+
/** Measure unwrapped plain text into scalable world-space bounds. */
|
|
13
|
+
declare function measureBoardText(text: string, fontSize?: number): {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { TEXT_FONT_FAMILY, TEXT_FONT_SIZE, TEXT_LINE_HEIGHT, TEXT_MAX_FONT_SIZE, TEXT_MIN_FONT_SIZE, boardTextLineHeight, clampBoardTextFontSize, measureBoardText, setBoardTextMeasurer };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BOARD_TEXT_FONT_FAMILY } from "../../protocol/dist/board-constants.js";
|
|
2
|
+
//#region src/board/core/text-metrics.ts
|
|
3
|
+
const TEXT_FONT_FAMILY = BOARD_TEXT_FONT_FAMILY;
|
|
4
|
+
const TEXT_FONT_SIZE = 24;
|
|
5
|
+
const TEXT_LINE_HEIGHT = 32;
|
|
6
|
+
const TEXT_MIN_FONT_SIZE = 2;
|
|
7
|
+
const TEXT_MAX_FONT_SIZE = 512;
|
|
8
|
+
const TEXT_LINE_HEIGHT_RATIO = TEXT_LINE_HEIGHT / TEXT_FONT_SIZE;
|
|
9
|
+
const TEXT_MIN_WIDTH_RATIO = 16 / TEXT_FONT_SIZE;
|
|
10
|
+
const TEXT_HORIZONTAL_PADDING_RATIO = 2 / TEXT_FONT_SIZE;
|
|
11
|
+
const FALLBACK_CHARACTER_RATIO = .52;
|
|
12
|
+
let textMeasurer;
|
|
13
|
+
/** Install the renderer's text measurer, or clear it for environment-free fallback metrics. */
|
|
14
|
+
function setBoardTextMeasurer(measurer) {
|
|
15
|
+
textMeasurer = measurer;
|
|
16
|
+
}
|
|
17
|
+
function clampBoardTextFontSize(fontSize) {
|
|
18
|
+
return Math.min(TEXT_MAX_FONT_SIZE, Math.max(TEXT_MIN_FONT_SIZE, fontSize));
|
|
19
|
+
}
|
|
20
|
+
function boardTextLineHeight(fontSize) {
|
|
21
|
+
return clampBoardTextFontSize(fontSize) * TEXT_LINE_HEIGHT_RATIO;
|
|
22
|
+
}
|
|
23
|
+
/** Measure unwrapped plain text into scalable world-space bounds. */
|
|
24
|
+
function measureBoardText(text, fontSize = TEXT_FONT_SIZE) {
|
|
25
|
+
const size = clampBoardTextFontSize(fontSize);
|
|
26
|
+
const lines = (text || " ").split("\n");
|
|
27
|
+
const minWidth = size * TEXT_MIN_WIDTH_RATIO;
|
|
28
|
+
const horizontalPadding = size * TEXT_HORIZONTAL_PADDING_RATIO;
|
|
29
|
+
const lineHeight = boardTextLineHeight(size);
|
|
30
|
+
const characterWidth = size * FALLBACK_CHARACTER_RATIO;
|
|
31
|
+
let width = minWidth;
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const value = line || " ";
|
|
34
|
+
const measured = textMeasurer?.(value, size);
|
|
35
|
+
width = Math.max(width, (measured ?? Math.max(1, value.length) * characterWidth) + horizontalPadding);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
width,
|
|
39
|
+
height: Math.max(lineHeight, lines.length * lineHeight)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { TEXT_FONT_FAMILY, TEXT_FONT_SIZE, TEXT_LINE_HEIGHT, TEXT_MAX_FONT_SIZE, TEXT_MIN_FONT_SIZE, boardTextLineHeight, clampBoardTextFontSize, measureBoardText, setBoardTextMeasurer };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { BoardColorId } from "./palette.js";
|
|
2
|
+
import { GeoKind } from "./shape-types.js";
|
|
3
|
+
//#region src/board/core/tool-styles.d.ts
|
|
4
|
+
declare const BOARD_STROKE_MIN_SIZE = 1;
|
|
5
|
+
declare const BOARD_STROKE_MAX_SIZE = 64;
|
|
6
|
+
type BoardToolStyleMap = {
|
|
7
|
+
text: {
|
|
8
|
+
color: BoardColorId;
|
|
9
|
+
};
|
|
10
|
+
geo: {
|
|
11
|
+
color: BoardColorId;
|
|
12
|
+
geo: GeoKind;
|
|
13
|
+
};
|
|
14
|
+
draw: {
|
|
15
|
+
color: BoardColorId;
|
|
16
|
+
size: number;
|
|
17
|
+
};
|
|
18
|
+
arrow: {
|
|
19
|
+
color: BoardColorId;
|
|
20
|
+
size: number;
|
|
21
|
+
};
|
|
22
|
+
frame: {
|
|
23
|
+
color: BoardColorId;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
type BoardStyledToolId = keyof BoardToolStyleMap;
|
|
27
|
+
type BoardToolStylePatch = { [K in BoardStyledToolId]?: Partial<BoardToolStyleMap[K]>; };
|
|
28
|
+
declare const DEFAULT_BOARD_TOOL_STYLES: {
|
|
29
|
+
readonly text: {
|
|
30
|
+
readonly color: "neutral";
|
|
31
|
+
};
|
|
32
|
+
readonly geo: {
|
|
33
|
+
readonly color: "brand";
|
|
34
|
+
readonly geo: "rectangle";
|
|
35
|
+
};
|
|
36
|
+
readonly draw: {
|
|
37
|
+
readonly color: "brand";
|
|
38
|
+
readonly size: 4;
|
|
39
|
+
};
|
|
40
|
+
readonly arrow: {
|
|
41
|
+
readonly color: "brand";
|
|
42
|
+
readonly size: 2.5;
|
|
43
|
+
};
|
|
44
|
+
readonly frame: {
|
|
45
|
+
readonly color: "neutral";
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
declare function clampBoardStrokeSize(size: number): number;
|
|
49
|
+
/** Return a mutable, validated style map for an editor or another Board client. */
|
|
50
|
+
declare function createBoardToolStyles(patch?: BoardToolStylePatch): BoardToolStyleMap;
|
|
51
|
+
//#endregion
|
|
52
|
+
export { BOARD_STROKE_MAX_SIZE, BOARD_STROKE_MIN_SIZE, BoardStyledToolId, BoardToolStyleMap, BoardToolStylePatch, DEFAULT_BOARD_TOOL_STYLES, clampBoardStrokeSize, createBoardToolStyles };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { BOARD_ARROW_STROKE_SIZE } from "../../protocol/dist/board-constants.js";
|
|
2
|
+
import { isBoardColorId } from "./palette.js";
|
|
3
|
+
import { isGeoKind } from "./shape-types.js";
|
|
4
|
+
//#region src/board/core/tool-styles.ts
|
|
5
|
+
const BOARD_STROKE_MIN_SIZE = 1;
|
|
6
|
+
const BOARD_STROKE_MAX_SIZE = 64;
|
|
7
|
+
const DEFAULT_BOARD_TOOL_STYLES = {
|
|
8
|
+
text: { color: "neutral" },
|
|
9
|
+
geo: {
|
|
10
|
+
color: "brand",
|
|
11
|
+
geo: "rectangle"
|
|
12
|
+
},
|
|
13
|
+
draw: {
|
|
14
|
+
color: "brand",
|
|
15
|
+
size: 4
|
|
16
|
+
},
|
|
17
|
+
arrow: {
|
|
18
|
+
color: "brand",
|
|
19
|
+
size: BOARD_ARROW_STROKE_SIZE
|
|
20
|
+
},
|
|
21
|
+
frame: { color: "neutral" }
|
|
22
|
+
};
|
|
23
|
+
function finiteOr(value, fallback) {
|
|
24
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
25
|
+
}
|
|
26
|
+
function clampBoardStrokeSize(size) {
|
|
27
|
+
return Math.min(64, Math.max(1, size));
|
|
28
|
+
}
|
|
29
|
+
/** Return a mutable, validated style map for an editor or another Board client. */
|
|
30
|
+
function createBoardToolStyles(patch = {}) {
|
|
31
|
+
const drawSize = clampBoardStrokeSize(finiteOr(patch.draw?.size, DEFAULT_BOARD_TOOL_STYLES.draw.size));
|
|
32
|
+
const arrowSize = clampBoardStrokeSize(finiteOr(patch.arrow?.size, DEFAULT_BOARD_TOOL_STYLES.arrow.size));
|
|
33
|
+
return {
|
|
34
|
+
text: { color: isBoardColorId(patch.text?.color) ? patch.text.color : DEFAULT_BOARD_TOOL_STYLES.text.color },
|
|
35
|
+
geo: {
|
|
36
|
+
color: isBoardColorId(patch.geo?.color) ? patch.geo.color : DEFAULT_BOARD_TOOL_STYLES.geo.color,
|
|
37
|
+
geo: isGeoKind(patch.geo?.geo) ? patch.geo.geo : DEFAULT_BOARD_TOOL_STYLES.geo.geo
|
|
38
|
+
},
|
|
39
|
+
draw: {
|
|
40
|
+
color: isBoardColorId(patch.draw?.color) ? patch.draw.color : DEFAULT_BOARD_TOOL_STYLES.draw.color,
|
|
41
|
+
size: drawSize
|
|
42
|
+
},
|
|
43
|
+
arrow: {
|
|
44
|
+
color: isBoardColorId(patch.arrow?.color) ? patch.arrow.color : DEFAULT_BOARD_TOOL_STYLES.arrow.color,
|
|
45
|
+
size: arrowSize
|
|
46
|
+
},
|
|
47
|
+
frame: { color: isBoardColorId(patch.frame?.color) ? patch.frame.color : DEFAULT_BOARD_TOOL_STYLES.frame.color }
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { BOARD_STROKE_MAX_SIZE, BOARD_STROKE_MIN_SIZE, DEFAULT_BOARD_TOOL_STYLES, clampBoardStrokeSize, createBoardToolStyles };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { BoardDocument, BoardItem } from "../../protocol/dist/board-document.js";
|
|
2
|
+
import { BoardExportPlan, BoardExportPlanInput, BoardExportRegion } from "../core/export-plan.js";
|
|
3
|
+
import { BoardShapeColors } from "../core/palette.js";
|
|
4
|
+
import { BoardRenderPalette } from "../render/renderers/board-renderer-registry.js";
|
|
5
|
+
import "../render/index.js";
|
|
6
|
+
import { BoardExportScene, BoardExportSceneInput, createBoardExportScene } from "./scene.js";
|
|
7
|
+
import { ICanvas, Renderer, Texture } from "pixi.js";
|
|
8
|
+
//#region src/board/export/index.d.ts
|
|
9
|
+
/** Paper behind the content: the theme's page color, or none at all. */
|
|
10
|
+
type BoardExportBackground = "paper" | "transparent" | number;
|
|
11
|
+
type BoardExportOptions = {
|
|
12
|
+
region?: BoardExportRegion;
|
|
13
|
+
/** Output pixels per world unit. Defaults to 2. */
|
|
14
|
+
scale?: number;
|
|
15
|
+
/** World-space padding. Frame and rect regions default to 0, others to 32. */
|
|
16
|
+
padding?: number;
|
|
17
|
+
colorScheme?: "dark" | "light";
|
|
18
|
+
/** Defaults to "paper" — a transparent PNG surprises people who paste it. */
|
|
19
|
+
background?: BoardExportBackground;
|
|
20
|
+
palette?: Partial<BoardRenderPalette>;
|
|
21
|
+
colors?: BoardShapeColors;
|
|
22
|
+
/** Resolved textures by preview key; unresolved keys become placeholders. */
|
|
23
|
+
textures?: Map<string, Texture>;
|
|
24
|
+
/** Preview-key strategy supplied by the host; defaults to still images only. */
|
|
25
|
+
assetKey?: (item: BoardItem) => string | null;
|
|
26
|
+
maxEdge?: number;
|
|
27
|
+
maxPixels?: number;
|
|
28
|
+
};
|
|
29
|
+
type BoardExportWarning = {
|
|
30
|
+
kind: "scale-clamped";
|
|
31
|
+
requested: number;
|
|
32
|
+
applied: number;
|
|
33
|
+
} | {
|
|
34
|
+
kind: "images-missing";
|
|
35
|
+
keys: string[];
|
|
36
|
+
} | {
|
|
37
|
+
kind: "many-items";
|
|
38
|
+
count: number;
|
|
39
|
+
};
|
|
40
|
+
type BoardExportResult = {
|
|
41
|
+
canvas: ICanvas;
|
|
42
|
+
plan: BoardExportPlan;
|
|
43
|
+
warnings: BoardExportWarning[];
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Capture a board region as a canvas.
|
|
47
|
+
*
|
|
48
|
+
* Returns null when the region is empty — an empty selection is a no-op, not a
|
|
49
|
+
* failure. The scene is always destroyed, so a throwing renderer cannot leak
|
|
50
|
+
* GPU resources.
|
|
51
|
+
*/
|
|
52
|
+
declare function renderBoardExport(renderer: Renderer, input: BoardDocument, options?: BoardExportOptions): BoardExportResult | null;
|
|
53
|
+
/** Human-readable form of a warning, shared by the CLI and the web UI. */
|
|
54
|
+
declare function describeBoardExportWarning(warning: BoardExportWarning): string;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { BoardExportBackground, BoardExportOptions, type BoardExportPlan, type BoardExportPlanInput, type BoardExportRegion, BoardExportResult, type BoardExportScene, type BoardExportSceneInput, BoardExportWarning, createBoardExportScene, describeBoardExportWarning, renderBoardExport };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { normalizeBoardDocument, planBoardExport } from "../core/export-plan.js";
|
|
2
|
+
import { defaultBoardPalette } from "../render/palette.js";
|
|
3
|
+
import { createBoardExportScene } from "./scene.js";
|
|
4
|
+
import { Rectangle } from "pixi.js";
|
|
5
|
+
//#region src/board/export/index.ts
|
|
6
|
+
function resolveBackground(background, palette) {
|
|
7
|
+
if (background === "transparent") return null;
|
|
8
|
+
if (typeof background === "number") return background;
|
|
9
|
+
return palette.bg;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Capture a board region as a canvas.
|
|
13
|
+
*
|
|
14
|
+
* Returns null when the region is empty — an empty selection is a no-op, not a
|
|
15
|
+
* failure. The scene is always destroyed, so a throwing renderer cannot leak
|
|
16
|
+
* GPU resources.
|
|
17
|
+
*/
|
|
18
|
+
function renderBoardExport(renderer, input, options = {}) {
|
|
19
|
+
const document = normalizeBoardDocument(input);
|
|
20
|
+
const plan = planBoardExport({
|
|
21
|
+
document,
|
|
22
|
+
region: options.region ?? { kind: "all" },
|
|
23
|
+
scale: options.scale,
|
|
24
|
+
padding: options.padding,
|
|
25
|
+
maxEdge: options.maxEdge,
|
|
26
|
+
maxPixels: options.maxPixels
|
|
27
|
+
});
|
|
28
|
+
if (!plan) return null;
|
|
29
|
+
const colorScheme = options.colorScheme ?? "dark";
|
|
30
|
+
const palette = {
|
|
31
|
+
...defaultBoardPalette(colorScheme),
|
|
32
|
+
...options.palette
|
|
33
|
+
};
|
|
34
|
+
const scene = createBoardExportScene({
|
|
35
|
+
document,
|
|
36
|
+
items: plan.items,
|
|
37
|
+
world: plan.world,
|
|
38
|
+
scale: plan.scale,
|
|
39
|
+
colorScheme,
|
|
40
|
+
palette,
|
|
41
|
+
colors: options.colors,
|
|
42
|
+
textures: options.textures,
|
|
43
|
+
assetKey: options.assetKey,
|
|
44
|
+
background: resolveBackground(options.background, palette)
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
const canvas = renderer.extract.canvas({
|
|
48
|
+
target: scene.root,
|
|
49
|
+
frame: new Rectangle(0, 0, plan.width, plan.height),
|
|
50
|
+
resolution: 1,
|
|
51
|
+
antialias: true,
|
|
52
|
+
clearColor: options.background === "transparent" ? void 0 : palette.bg
|
|
53
|
+
});
|
|
54
|
+
const warnings = [];
|
|
55
|
+
if (plan.clamped) warnings.push({
|
|
56
|
+
kind: "scale-clamped",
|
|
57
|
+
requested: plan.requestedScale,
|
|
58
|
+
applied: plan.scale
|
|
59
|
+
});
|
|
60
|
+
if (scene.missingImageKeys.length > 0) warnings.push({
|
|
61
|
+
kind: "images-missing",
|
|
62
|
+
keys: scene.missingImageKeys
|
|
63
|
+
});
|
|
64
|
+
if (plan.items.length > 2e3) warnings.push({
|
|
65
|
+
kind: "many-items",
|
|
66
|
+
count: plan.items.length
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
canvas,
|
|
70
|
+
plan,
|
|
71
|
+
warnings
|
|
72
|
+
};
|
|
73
|
+
} finally {
|
|
74
|
+
scene.destroy();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Human-readable form of a warning, shared by the CLI and the web UI. */
|
|
78
|
+
function describeBoardExportWarning(warning) {
|
|
79
|
+
switch (warning.kind) {
|
|
80
|
+
case "scale-clamped": return `Scale reduced from ${warning.requested}x to ${warning.applied.toFixed(2)}x to stay within the size limit.`;
|
|
81
|
+
case "images-missing": return warning.keys.length === 1 ? "1 image could not be loaded and was drawn as a placeholder." : `${warning.keys.length} images could not be loaded and were drawn as placeholders.`;
|
|
82
|
+
case "many-items": return `${warning.count} items exported; this may take a moment.`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { createBoardExportScene, describeBoardExportWarning, renderBoardExport };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BoardDocument, BoardItem } from "../../protocol/dist/board-document.js";
|
|
2
|
+
import { Rect } from "../geometry.js";
|
|
3
|
+
import { BoardShapeColors } from "../core/palette.js";
|
|
4
|
+
import { BoardRenderPalette } from "../render/renderers/board-renderer-registry.js";
|
|
5
|
+
import "../render/index.js";
|
|
6
|
+
import { Container, Texture } from "pixi.js";
|
|
7
|
+
//#region src/board/export/scene.d.ts
|
|
8
|
+
type BoardExportSceneInput = {
|
|
9
|
+
document: BoardDocument;
|
|
10
|
+
/** Items to draw, in document (z) order. */
|
|
11
|
+
items: BoardItem[];
|
|
12
|
+
/** World rect being captured; content is translated so it starts at 0,0. */
|
|
13
|
+
world: Rect;
|
|
14
|
+
/** Output pixels per world unit. Drives text rasterisation resolution. */
|
|
15
|
+
scale: number;
|
|
16
|
+
colorScheme: "dark" | "light";
|
|
17
|
+
palette?: Partial<BoardRenderPalette>;
|
|
18
|
+
colors?: BoardShapeColors;
|
|
19
|
+
/** Resolved textures by preview key. Missing keys render as placeholders. */
|
|
20
|
+
textures?: Map<string, Texture>;
|
|
21
|
+
/** Preview-key strategy supplied by the host; defaults to still images only. */
|
|
22
|
+
assetKey?: (item: BoardItem) => string | null;
|
|
23
|
+
/** Opaque paper behind the content, or null for transparency. */
|
|
24
|
+
background?: number | null;
|
|
25
|
+
};
|
|
26
|
+
type BoardExportScene = {
|
|
27
|
+
/** Root to hand to the renderer; already positioned and scaled. */
|
|
28
|
+
root: Container;
|
|
29
|
+
/** Image keys the scene wanted but could not resolve. */
|
|
30
|
+
missingImageKeys: string[];
|
|
31
|
+
destroy: () => void;
|
|
32
|
+
};
|
|
33
|
+
declare function createBoardExportScene(input: BoardExportSceneInput): BoardExportScene;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { BoardExportScene, BoardExportSceneInput, createBoardExportScene };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { imageAssetKey } from "../image-key.js";
|
|
2
|
+
import { buildFallbackShapeColors } from "../core/palette.js";
|
|
3
|
+
import { defaultBoardPalette } from "../render/palette.js";
|
|
4
|
+
import { getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
|
|
5
|
+
import { Container, Graphics } from "pixi.js";
|
|
6
|
+
//#region src/board/export/scene.ts
|
|
7
|
+
/**
|
|
8
|
+
* Render context for an export.
|
|
9
|
+
*
|
|
10
|
+
* Interaction state is empty by construction: nothing is selected, hovered or
|
|
11
|
+
* resizing, so no editor chrome (outlines, handles) can leak into the image.
|
|
12
|
+
*/
|
|
13
|
+
function buildContext(input) {
|
|
14
|
+
const missing = /* @__PURE__ */ new Set();
|
|
15
|
+
const byId = new Map(input.document.items.map((item) => [item.id, item]));
|
|
16
|
+
const textures = input.textures ?? /* @__PURE__ */ new Map();
|
|
17
|
+
return {
|
|
18
|
+
context: {
|
|
19
|
+
document: input.document,
|
|
20
|
+
getItem: (id) => byId.get(id) ?? null,
|
|
21
|
+
selectedIds: /* @__PURE__ */ new Set(),
|
|
22
|
+
hoveredId: null,
|
|
23
|
+
resizingIds: /* @__PURE__ */ new Set(),
|
|
24
|
+
palette: {
|
|
25
|
+
...defaultBoardPalette(input.colorScheme),
|
|
26
|
+
...input.palette
|
|
27
|
+
},
|
|
28
|
+
colors: input.colors ?? buildFallbackShapeColors(input.colorScheme),
|
|
29
|
+
colorScheme: input.colorScheme,
|
|
30
|
+
zoom: input.scale,
|
|
31
|
+
assetKey: input.assetKey ?? imageAssetKey,
|
|
32
|
+
getTexture: (key) => textures.get(key) ?? null,
|
|
33
|
+
hasError: (key) => {
|
|
34
|
+
if (!textures.has(key)) missing.add(key);
|
|
35
|
+
return false;
|
|
36
|
+
},
|
|
37
|
+
fileState: () => "ok",
|
|
38
|
+
acquireTexture: () => {},
|
|
39
|
+
releaseTexture: () => {}
|
|
40
|
+
},
|
|
41
|
+
missing
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function createBoardExportScene(input) {
|
|
45
|
+
const { context, missing } = buildContext(input);
|
|
46
|
+
const root = new Container({ label: "board-export-root" });
|
|
47
|
+
if (input.background != null) root.addChild(new Graphics().rect(0, 0, input.world.width * input.scale, input.world.height * input.scale).fill({
|
|
48
|
+
color: input.background,
|
|
49
|
+
alpha: 1
|
|
50
|
+
}));
|
|
51
|
+
const world = new Container({
|
|
52
|
+
isRenderGroup: true,
|
|
53
|
+
label: "board-export-world"
|
|
54
|
+
});
|
|
55
|
+
world.scale.set(input.scale);
|
|
56
|
+
world.position.set(-input.world.x * input.scale, -input.world.y * input.scale);
|
|
57
|
+
for (const item of input.items) {
|
|
58
|
+
const renderer = getBoardCardRenderer(item, context);
|
|
59
|
+
world.addChild(renderer.create(item, context));
|
|
60
|
+
}
|
|
61
|
+
root.addChild(world);
|
|
62
|
+
return {
|
|
63
|
+
root,
|
|
64
|
+
missingImageKeys: [...missing],
|
|
65
|
+
destroy: () => root.destroy({ children: true })
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { createBoardExportScene };
|