@neta-art/cohub 3.1.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 +1 -2
- package/dist/chunks/websocket.d.ts +798 -352
- package/dist/http.d.ts +1 -2
- package/dist/index.d.ts +99 -4
- 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
package/README.md
CHANGED
|
@@ -118,18 +118,49 @@ const stop = board.subscribe({
|
|
|
118
118
|
stop();
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
Board is split by dependency: the model runs anywhere, drawing needs PixiJS.
|
|
122
|
+
`@neta-art/cohub/board` carries the document schema, geometry, the shape layer,
|
|
123
|
+
timeline compilation and export planning, with no renderer and no PixiJS — so
|
|
124
|
+
agents, servers and edge workers can read, write and measure boards without a
|
|
125
|
+
graphics stack:
|
|
123
126
|
|
|
124
127
|
```ts
|
|
125
128
|
import {
|
|
129
|
+
BoardDocumentSchema,
|
|
126
130
|
clip,
|
|
127
131
|
compileSequence,
|
|
128
132
|
createBoardExtensionRegistry,
|
|
133
|
+
itemBounds,
|
|
134
|
+
planBoardExport,
|
|
129
135
|
timeline,
|
|
130
136
|
} from "@neta-art/cohub/board";
|
|
131
137
|
```
|
|
132
138
|
|
|
139
|
+
Drawing pixels is where PixiJS enters. The card renderers and themes the editor
|
|
140
|
+
uses live behind `@neta-art/cohub/board/render`, and turning a plan into an
|
|
141
|
+
image has dedicated browser and Node.js entries:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { getBoardCardRenderer } from "@neta-art/cohub/board/render";
|
|
145
|
+
import { renderBoardExport } from "@neta-art/cohub/board/export";
|
|
146
|
+
import {
|
|
147
|
+
createBoardHeadlessRenderer,
|
|
148
|
+
exportBoardImageBytes,
|
|
149
|
+
} from "@neta-art/cohub/board/headless";
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Install `pixi.js` to use `board/render` or `board/export`, and add
|
|
153
|
+
`@napi-rs/canvas` as well for `board/headless`. Both are optional peers, and
|
|
154
|
+
`@neta-art/cohub/board` never reaches for either, so HTTP-only installations
|
|
155
|
+
stay lightweight.
|
|
156
|
+
|
|
157
|
+
Text metrics follow the same split. The renderers measure through a real canvas
|
|
158
|
+
and set that up themselves, so nothing extra is needed to draw or export. Called
|
|
159
|
+
straight off `@neta-art/cohub/board` with no renderer in play,
|
|
160
|
+
`measureBoardText` returns a per-character estimate instead — fine for laying
|
|
161
|
+
out a board on a server, but call `installBoardTextMeasurement` from
|
|
162
|
+
`board/render` first if the numbers have to match what the editor draws.
|
|
163
|
+
|
|
133
164
|
## Session subscriptions
|
|
134
165
|
|
|
135
166
|
```ts
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { BoardCapability, BoardRenderCost } from "../protocol/dist/board-constants.js";
|
|
2
|
+
import { BoardAssetRef, BoardClip, BoardDiagnostic, BoardEffect, BoardSequence, BoardTarget, BoardValidationResult } from "../protocol/dist/board.js";
|
|
3
|
+
import "../protocol/dist/index.js";
|
|
4
|
+
//#region src/board/animation.d.ts
|
|
3
5
|
type TimelineClipInput = Omit<BoardClip, "id" | "sequenceId" | "start" | "seed"> & {
|
|
4
6
|
id?: string;
|
|
5
7
|
seed?: string;
|
|
@@ -1,44 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
particles: 2e4,
|
|
4
|
-
vertices: 5e5,
|
|
5
|
-
dynamicVertices: 15e4,
|
|
6
|
-
drawCalls: 400,
|
|
7
|
-
filterPasses: 24,
|
|
8
|
-
renderTexturePixels: 16777216,
|
|
9
|
-
textureBytes: 512 * 1024 * 1024,
|
|
10
|
-
bufferBytes: 256 * 1024 * 1024,
|
|
11
|
-
simulationSteps: 1e5
|
|
12
|
-
};
|
|
13
|
-
const BOARD_BUILTIN_CLIP_KINDS = [
|
|
14
|
-
"motion.keyframes",
|
|
15
|
-
"motion.path",
|
|
16
|
-
"draw.reveal",
|
|
17
|
-
"draw.handwrite",
|
|
18
|
-
"text.reveal",
|
|
19
|
-
"effects.particles",
|
|
20
|
-
"effects.trail",
|
|
21
|
-
"effects.impact",
|
|
22
|
-
"effects.flash",
|
|
23
|
-
"effects.color",
|
|
24
|
-
"camera.pan",
|
|
25
|
-
"camera.zoom",
|
|
26
|
-
"camera.shake"
|
|
27
|
-
];
|
|
28
|
-
const BOARD_BUILTIN_EFFECT_KINDS = ["effects.pulse", "effects.float"];
|
|
29
|
-
const BOARD_BUILTIN_CAPABILITIES = [...BOARD_BUILTIN_CLIP_KINDS.map((id) => ({
|
|
30
|
-
kind: "clip",
|
|
31
|
-
id,
|
|
32
|
-
version: 1,
|
|
33
|
-
renderers: ["webgpu", "webgl"]
|
|
34
|
-
})), ...BOARD_BUILTIN_EFFECT_KINDS.map((id) => ({
|
|
35
|
-
kind: "effect",
|
|
36
|
-
id,
|
|
37
|
-
version: 1,
|
|
38
|
-
renderers: ["webgpu", "webgl"]
|
|
39
|
-
}))];
|
|
40
|
-
//#endregion
|
|
41
|
-
//#region src/board.ts
|
|
1
|
+
import { BOARD_BUILTIN_CAPABILITIES, DEFAULT_BOARD_RENDER_LIMITS } from "../protocol/dist/board-constants.js";
|
|
2
|
+
//#region src/board/animation.ts
|
|
42
3
|
const ZERO_COST = {
|
|
43
4
|
particles: 0,
|
|
44
5
|
vertices: 0,
|
|
@@ -266,9 +227,9 @@ function flatten(input, offset, sequenceSeed, path) {
|
|
|
266
227
|
}
|
|
267
228
|
const clips = [];
|
|
268
229
|
let duration = 0;
|
|
269
|
-
for (
|
|
230
|
+
for (const [index, child] of input.children.entries()) {
|
|
270
231
|
const childOffset = input.type === "sequence" ? duration : input.type === "stagger" ? input.each * index : 0;
|
|
271
|
-
const nested = flatten(
|
|
232
|
+
const nested = flatten(child, offset + childOffset, sequenceSeed, `${path}.${index}`);
|
|
272
233
|
clips.push(...nested.clips);
|
|
273
234
|
duration = input.type === "sequence" ? duration + nested.duration : Math.max(duration, childOffset + nested.duration);
|
|
274
235
|
}
|
|
@@ -389,4 +350,4 @@ function createBoardExtensionRegistry(input = {}) {
|
|
|
389
350
|
return registry;
|
|
390
351
|
}
|
|
391
352
|
//#endregion
|
|
392
|
-
export {
|
|
353
|
+
export { BoardExtensionRegistry, DEFAULT_BOARD_LIMITS, clip, compileSequence, createBoardExtensionRegistry, timeline };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BoardManifest, BoardNodeInput, BoardNodeRecord, BoardRecord, InvalidBoardFileError, isBoardPath, parseBoardManifest, serializeBoardManifest } from "../protocol/dist/board.js";
|
|
2
|
+
import "../protocol/dist/index.js";
|
|
3
|
+
import { BoardAppearance, BoardDocument, BoardItem } from "../protocol/dist/board-document.js";
|
|
4
|
+
//#region src/board/codec.d.ts
|
|
5
|
+
declare const DEFAULT_BOARD_APPEARANCE: BoardAppearance;
|
|
6
|
+
declare const ITEM_BASE_KEYS: readonly ["id", "type", "frame", "locked", "style", "metadata"];
|
|
7
|
+
/**
|
|
8
|
+
* Original wire data follows an item through immutable object spreads while
|
|
9
|
+
* remaining invisible to JSON serialization. Codecs can then patch only the
|
|
10
|
+
* fields this client understands instead of rebuilding and truncating a node.
|
|
11
|
+
*/
|
|
12
|
+
declare const BOARD_NODE_SOURCE: unique symbol;
|
|
13
|
+
type WireBackedBoardItem = BoardItem & {
|
|
14
|
+
[BOARD_NODE_SOURCE]?: BoardNodeInput;
|
|
15
|
+
};
|
|
16
|
+
declare function nodeInputFromRecord(node: BoardNodeRecord): BoardNodeInput;
|
|
17
|
+
declare function sourceForItem(item: BoardItem): BoardNodeInput | undefined;
|
|
18
|
+
declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
19
|
+
declare function boardNodeToItem(node: BoardNodeRecord): BoardItem;
|
|
20
|
+
declare function boardBootstrapToDocument(input: {
|
|
21
|
+
board: BoardRecord;
|
|
22
|
+
nodes: BoardNodeRecord[];
|
|
23
|
+
}): BoardDocument;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { BOARD_NODE_SOURCE, type BoardManifest, type BoardNodeRecord, type BoardRecord, DEFAULT_BOARD_APPEARANCE, ITEM_BASE_KEYS, InvalidBoardFileError, WireBackedBoardItem, boardBootstrapToDocument, boardNodeToItem, isBoardPath, isRecord, nodeInputFromRecord, parseBoardManifest, serializeBoardManifest, sourceForItem };
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { BOARD_DOCUMENT_KIND, InvalidBoardFileError, isBoardPath, parseBoardManifest, serializeBoardManifest } from "../protocol/dist/board.js";
|
|
2
|
+
import { BoardAppearanceSchema, BoardDocumentSchema, BoardFileSnapshotSchema, UNKNOWN_BOARD_ITEM_TYPE } from "../protocol/dist/board-document.js";
|
|
3
|
+
import { TEXT_FONT_SIZE, clampBoardTextFontSize } from "./core/text-metrics.js";
|
|
4
|
+
import "../protocol/dist/index.js";
|
|
5
|
+
//#region src/board/codec.ts
|
|
6
|
+
const DEFAULT_BOARD_APPEARANCE = BoardAppearanceSchema.parse({
|
|
7
|
+
theme: "clean",
|
|
8
|
+
background: { kind: "solid" },
|
|
9
|
+
grid: {
|
|
10
|
+
visible: false,
|
|
11
|
+
size: 24,
|
|
12
|
+
opacity: .12
|
|
13
|
+
},
|
|
14
|
+
mood: "clean"
|
|
15
|
+
});
|
|
16
|
+
const ITEM_BASE_KEYS = [
|
|
17
|
+
"id",
|
|
18
|
+
"type",
|
|
19
|
+
"frame",
|
|
20
|
+
"locked",
|
|
21
|
+
"style",
|
|
22
|
+
"metadata"
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Original wire data follows an item through immutable object spreads while
|
|
26
|
+
* remaining invisible to JSON serialization. Codecs can then patch only the
|
|
27
|
+
* fields this client understands instead of rebuilding and truncating a node.
|
|
28
|
+
*/
|
|
29
|
+
const BOARD_NODE_SOURCE = Symbol("board-node-source");
|
|
30
|
+
function nodeInputFromRecord(node) {
|
|
31
|
+
return {
|
|
32
|
+
nodeId: node.nodeId,
|
|
33
|
+
type: node.type,
|
|
34
|
+
parentId: node.parentId ?? null,
|
|
35
|
+
orderKey: node.orderKey ?? null,
|
|
36
|
+
x: node.x,
|
|
37
|
+
y: node.y,
|
|
38
|
+
width: node.width,
|
|
39
|
+
height: node.height,
|
|
40
|
+
rotation: node.rotation,
|
|
41
|
+
refKind: node.refKind ?? null,
|
|
42
|
+
refPath: node.refPath ?? null,
|
|
43
|
+
refUrl: node.refUrl ?? null,
|
|
44
|
+
view: node.view ?? {},
|
|
45
|
+
style: node.style ?? {},
|
|
46
|
+
data: node.data ?? {}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function sourceForItem(item) {
|
|
50
|
+
return item[BOARD_NODE_SOURCE];
|
|
51
|
+
}
|
|
52
|
+
function isRecord(value) {
|
|
53
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
54
|
+
}
|
|
55
|
+
function frameFromNode(node) {
|
|
56
|
+
return {
|
|
57
|
+
x: node.x,
|
|
58
|
+
y: node.y,
|
|
59
|
+
width: node.width,
|
|
60
|
+
height: node.height,
|
|
61
|
+
rotation: node.rotation
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function lockedFromData(data) {
|
|
65
|
+
return data.locked === true ? true : void 0;
|
|
66
|
+
}
|
|
67
|
+
function styleFromNode(node) {
|
|
68
|
+
return Object.keys(node.style ?? {}).length ? node.style : void 0;
|
|
69
|
+
}
|
|
70
|
+
function spaceFilePathFromNode(node) {
|
|
71
|
+
if (node.refKind === "space_file" && node.refPath) return node.refPath;
|
|
72
|
+
if (typeof node.refPath === "string" && node.refPath) return node.refPath;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function boardNodeToItemValue(node) {
|
|
76
|
+
const frame = frameFromNode(node);
|
|
77
|
+
const style = styleFromNode(node);
|
|
78
|
+
const data = node.data ?? {};
|
|
79
|
+
const locked = lockedFromData(data);
|
|
80
|
+
switch (node.type) {
|
|
81
|
+
case "text": return {
|
|
82
|
+
id: node.nodeId,
|
|
83
|
+
type: "text",
|
|
84
|
+
text: typeof data.text === "string" ? data.text : "",
|
|
85
|
+
color: typeof data.color === "string" ? data.color : "neutral",
|
|
86
|
+
fontSize: clampBoardTextFontSize(typeof data.fontSize === "number" ? data.fontSize : TEXT_FONT_SIZE),
|
|
87
|
+
frame,
|
|
88
|
+
...locked ? { locked } : {},
|
|
89
|
+
style
|
|
90
|
+
};
|
|
91
|
+
case "note": return {
|
|
92
|
+
id: node.nodeId,
|
|
93
|
+
type: "note",
|
|
94
|
+
text: typeof data.text === "string" ? data.text : "",
|
|
95
|
+
color: typeof data.color === "string" ? data.color : "amber",
|
|
96
|
+
frame,
|
|
97
|
+
...locked ? { locked } : {},
|
|
98
|
+
style
|
|
99
|
+
};
|
|
100
|
+
case "geo": return {
|
|
101
|
+
id: node.nodeId,
|
|
102
|
+
type: "geo",
|
|
103
|
+
geo: typeof data.geo === "string" ? data.geo : "rectangle",
|
|
104
|
+
text: typeof data.text === "string" ? data.text : "",
|
|
105
|
+
color: typeof data.color === "string" ? data.color : "brand",
|
|
106
|
+
fillOpacity: typeof data.fillOpacity === "number" ? data.fillOpacity : 0,
|
|
107
|
+
frame,
|
|
108
|
+
...locked ? { locked } : {},
|
|
109
|
+
style
|
|
110
|
+
};
|
|
111
|
+
case "draw": return {
|
|
112
|
+
id: node.nodeId,
|
|
113
|
+
type: "draw",
|
|
114
|
+
points: Array.isArray(data.points) ? data.points : [],
|
|
115
|
+
color: typeof data.color === "string" ? data.color : "brand",
|
|
116
|
+
size: typeof data.size === "number" ? data.size : 4,
|
|
117
|
+
frame,
|
|
118
|
+
...locked ? { locked } : {},
|
|
119
|
+
style
|
|
120
|
+
};
|
|
121
|
+
case "arrow": return {
|
|
122
|
+
id: node.nodeId,
|
|
123
|
+
type: "arrow",
|
|
124
|
+
start: data.start ?? {
|
|
125
|
+
kind: "point",
|
|
126
|
+
x: 0,
|
|
127
|
+
y: 0
|
|
128
|
+
},
|
|
129
|
+
end: data.end ?? {
|
|
130
|
+
kind: "point",
|
|
131
|
+
x: 0,
|
|
132
|
+
y: 0
|
|
133
|
+
},
|
|
134
|
+
bend: typeof data.bend === "number" ? data.bend : 0,
|
|
135
|
+
color: typeof data.color === "string" ? data.color : "brand",
|
|
136
|
+
size: typeof data.size === "number" ? data.size : 2.5,
|
|
137
|
+
arrowStart: Boolean(data.arrowStart),
|
|
138
|
+
arrowEnd: data.arrowEnd === void 0 ? true : Boolean(data.arrowEnd),
|
|
139
|
+
label: typeof data.label === "string" ? data.label : "",
|
|
140
|
+
frame,
|
|
141
|
+
...locked ? { locked } : {},
|
|
142
|
+
style
|
|
143
|
+
};
|
|
144
|
+
case "frame": return {
|
|
145
|
+
id: node.nodeId,
|
|
146
|
+
type: "frame",
|
|
147
|
+
label: typeof data.label === "string" ? data.label : "Frame",
|
|
148
|
+
color: typeof data.color === "string" ? data.color : "neutral",
|
|
149
|
+
frame,
|
|
150
|
+
...locked ? { locked } : {},
|
|
151
|
+
style
|
|
152
|
+
};
|
|
153
|
+
case "image": {
|
|
154
|
+
const path = spaceFilePathFromNode(node) ?? "missing";
|
|
155
|
+
return {
|
|
156
|
+
id: node.nodeId,
|
|
157
|
+
type: "image",
|
|
158
|
+
ref: {
|
|
159
|
+
kind: "space-file",
|
|
160
|
+
path
|
|
161
|
+
},
|
|
162
|
+
snapshot: node.view,
|
|
163
|
+
...data.crop && typeof data.crop === "object" ? { crop: data.crop } : {},
|
|
164
|
+
frame,
|
|
165
|
+
...locked ? { locked } : {},
|
|
166
|
+
style
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
case "video": {
|
|
170
|
+
const path = spaceFilePathFromNode(node) ?? "missing";
|
|
171
|
+
return {
|
|
172
|
+
id: node.nodeId,
|
|
173
|
+
type: "video",
|
|
174
|
+
ref: {
|
|
175
|
+
kind: "space-file",
|
|
176
|
+
path
|
|
177
|
+
},
|
|
178
|
+
snapshot: node.view,
|
|
179
|
+
frame,
|
|
180
|
+
...locked ? { locked } : {},
|
|
181
|
+
style
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
case "file": {
|
|
185
|
+
const path = spaceFilePathFromNode(node) ?? "missing";
|
|
186
|
+
const parsed = BoardFileSnapshotSchema.safeParse(node.view ?? {});
|
|
187
|
+
return {
|
|
188
|
+
id: node.nodeId,
|
|
189
|
+
type: "file",
|
|
190
|
+
ref: {
|
|
191
|
+
kind: "space-file",
|
|
192
|
+
path
|
|
193
|
+
},
|
|
194
|
+
...parsed.success ? { snapshot: parsed.data } : {},
|
|
195
|
+
frame,
|
|
196
|
+
...locked ? { locked } : {},
|
|
197
|
+
style
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
default: {
|
|
201
|
+
const raw = {
|
|
202
|
+
...data,
|
|
203
|
+
id: node.nodeId,
|
|
204
|
+
type: node.type,
|
|
205
|
+
frame
|
|
206
|
+
};
|
|
207
|
+
if (style) raw.style = style;
|
|
208
|
+
if (locked) raw.locked = true;
|
|
209
|
+
return {
|
|
210
|
+
id: node.nodeId,
|
|
211
|
+
type: UNKNOWN_BOARD_ITEM_TYPE,
|
|
212
|
+
frame,
|
|
213
|
+
...locked ? { locked } : {},
|
|
214
|
+
style,
|
|
215
|
+
raw
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function boardNodeToItem(node) {
|
|
221
|
+
const item = boardNodeToItemValue(node);
|
|
222
|
+
const metadata = isRecord(node.data?.metadata) ? node.data.metadata : void 0;
|
|
223
|
+
return {
|
|
224
|
+
...item,
|
|
225
|
+
...metadata ? { metadata } : {},
|
|
226
|
+
[BOARD_NODE_SOURCE]: nodeInputFromRecord(node)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function boardBootstrapToDocument(input) {
|
|
230
|
+
const appearance = BoardAppearanceSchema.safeParse(input.board.metadata.appearance);
|
|
231
|
+
const document = BoardDocumentSchema.parse({
|
|
232
|
+
kind: BOARD_DOCUMENT_KIND,
|
|
233
|
+
version: 1,
|
|
234
|
+
appearance: appearance.success ? appearance.data : DEFAULT_BOARD_APPEARANCE,
|
|
235
|
+
viewport: {
|
|
236
|
+
x: 0,
|
|
237
|
+
y: 0,
|
|
238
|
+
zoom: 1
|
|
239
|
+
},
|
|
240
|
+
items: input.nodes.map(boardNodeToItem)
|
|
241
|
+
});
|
|
242
|
+
const sources = new Map(input.nodes.map((node) => [node.nodeId, nodeInputFromRecord(node)]));
|
|
243
|
+
return {
|
|
244
|
+
...document,
|
|
245
|
+
items: document.items.map((item) => ({
|
|
246
|
+
...item,
|
|
247
|
+
[BOARD_NODE_SOURCE]: sources.get(item.id)
|
|
248
|
+
}))
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
//#endregion
|
|
252
|
+
export { BOARD_NODE_SOURCE, DEFAULT_BOARD_APPEARANCE, ITEM_BASE_KEYS, InvalidBoardFileError, boardBootstrapToDocument, boardNodeToItem, isBoardPath, isRecord, nodeInputFromRecord, parseBoardManifest, serializeBoardManifest, sourceForItem };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ArrowEndpoint, BoardArrowItem, BoardFrame } from "../../protocol/dist/board-document.js";
|
|
2
|
+
import { Rect, WorldPoint } from "../geometry.js";
|
|
3
|
+
//#region src/board/core/bindings.d.ts
|
|
4
|
+
type FrameLookup = (id: string) => BoardFrame | undefined;
|
|
5
|
+
/** Denormalize a binding anchor to a world point on the (rotated) frame. */
|
|
6
|
+
declare function anchorToWorld(frame: BoardFrame, nx: number, ny: number): WorldPoint;
|
|
7
|
+
/** Normalize a world point to a binding anchor on the (rotated) frame, clamped. */
|
|
8
|
+
declare function worldToAnchor(frame: BoardFrame, point: WorldPoint): {
|
|
9
|
+
nx: number;
|
|
10
|
+
ny: number;
|
|
11
|
+
};
|
|
12
|
+
/** Resolve an endpoint to a world point, or null if a binding target is gone. */
|
|
13
|
+
declare function resolveEndpoint(endpoint: ArrowEndpoint, getFrame: FrameLookup): WorldPoint | null;
|
|
14
|
+
type ResolvedArrow = {
|
|
15
|
+
start: WorldPoint;
|
|
16
|
+
end: WorldPoint;
|
|
17
|
+
/** Quadratic control point (already bent); equals midpoint when bend is 0. */
|
|
18
|
+
control: WorldPoint;
|
|
19
|
+
};
|
|
20
|
+
/** Resolve both endpoints and compute the bent control point. */
|
|
21
|
+
declare function resolveArrow(item: BoardArrowItem, getFrame: FrameLookup): ResolvedArrow | null;
|
|
22
|
+
/** World-space bounds of an arrow, sampling the curve for accuracy. */
|
|
23
|
+
declare function arrowBounds(item: BoardArrowItem, getFrame: FrameLookup): Rect | null;
|
|
24
|
+
/** Sample the arrow's quadratic curve into world points (for render/hit-test). */
|
|
25
|
+
declare function sampleQuadratic(resolved: ResolvedArrow, segments: number): WorldPoint[];
|
|
26
|
+
/** Distance from a world point to the arrow curve (hit testing). */
|
|
27
|
+
declare function distanceToArrow(item: BoardArrowItem, getFrame: FrameLookup, point: WorldPoint): number;
|
|
28
|
+
/**
|
|
29
|
+
* Turn a free endpoint into a binding if a candidate target frame contains the
|
|
30
|
+
* point; otherwise keep it free. Used while dragging an arrow handle so the
|
|
31
|
+
* arrow "connects" to shapes it lands on.
|
|
32
|
+
*/
|
|
33
|
+
declare function bindEndpointAt(point: WorldPoint, target: {
|
|
34
|
+
id: string;
|
|
35
|
+
frame: BoardFrame;
|
|
36
|
+
} | null): ArrowEndpoint;
|
|
37
|
+
/**
|
|
38
|
+
* Translate an arrow by a delta: free endpoints move; bindings stay attached to
|
|
39
|
+
* their targets (so a bound end anchors while the rest of the arrow moves). The
|
|
40
|
+
* frame — a derived bounding box, not a transform — is recomputed from the new
|
|
41
|
+
* endpoint geometry.
|
|
42
|
+
*/
|
|
43
|
+
declare function translateArrow(item: BoardArrowItem, dx: number, dy: number, getFrame: FrameLookup): BoardArrowItem;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { FrameLookup, ResolvedArrow, anchorToWorld, arrowBounds, bindEndpointAt, distanceToArrow, resolveArrow, resolveEndpoint, sampleQuadratic, translateArrow, worldToAnchor };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { degToRad, rectCenter, rotatePointAround, worldPoint } from "../geometry.js";
|
|
2
|
+
//#region src/board/core/bindings.ts
|
|
3
|
+
/**
|
|
4
|
+
* Arrow bindings — pure geometry for connecting arrows to shapes.
|
|
5
|
+
*
|
|
6
|
+
* An arrow endpoint is either a free world point or a *binding*: a normalized
|
|
7
|
+
* anchor (0..1) on a target shape's frame. Because the anchor is relative, the
|
|
8
|
+
* arrow tracks the target through move and resize with no stored absolute
|
|
9
|
+
* coordinates — the binding never goes stale. Resolution to world space happens
|
|
10
|
+
* here, given a frame lookup, so it is fully testable and renderer-independent.
|
|
11
|
+
*/
|
|
12
|
+
/** Denormalize a binding anchor to a world point on the (rotated) frame. */
|
|
13
|
+
function anchorToWorld(frame, nx, ny) {
|
|
14
|
+
const unrotated = worldPoint(frame.x + nx * frame.width, frame.y + ny * frame.height);
|
|
15
|
+
if (!frame.rotation) return unrotated;
|
|
16
|
+
return rotatePointAround(unrotated, rectCenter(frame), degToRad(frame.rotation));
|
|
17
|
+
}
|
|
18
|
+
/** Normalize a world point to a binding anchor on the (rotated) frame, clamped. */
|
|
19
|
+
function worldToAnchor(frame, point) {
|
|
20
|
+
let local = point;
|
|
21
|
+
if (frame.rotation) local = rotatePointAround(point, rectCenter(frame), -degToRad(frame.rotation));
|
|
22
|
+
return {
|
|
23
|
+
nx: clamp01((local.x - frame.x) / frame.width),
|
|
24
|
+
ny: clamp01((local.y - frame.y) / frame.height)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function clamp01(value) {
|
|
28
|
+
return Math.min(1, Math.max(0, value));
|
|
29
|
+
}
|
|
30
|
+
/** Resolve an endpoint to a world point, or null if a binding target is gone. */
|
|
31
|
+
function resolveEndpoint(endpoint, getFrame) {
|
|
32
|
+
if (endpoint.kind === "point") return worldPoint(endpoint.x, endpoint.y);
|
|
33
|
+
const frame = getFrame(endpoint.target);
|
|
34
|
+
if (!frame) return null;
|
|
35
|
+
return anchorToWorld(frame, endpoint.nx, endpoint.ny);
|
|
36
|
+
}
|
|
37
|
+
/** Resolve both endpoints and compute the bent control point. */
|
|
38
|
+
function resolveArrow(item, getFrame) {
|
|
39
|
+
const start = resolveEndpoint(item.start, getFrame);
|
|
40
|
+
const end = resolveEndpoint(item.end, getFrame);
|
|
41
|
+
if (!start || !end) return null;
|
|
42
|
+
const mid = worldPoint((start.x + end.x) / 2, (start.y + end.y) / 2);
|
|
43
|
+
if (!item.bend) return {
|
|
44
|
+
start,
|
|
45
|
+
end,
|
|
46
|
+
control: mid
|
|
47
|
+
};
|
|
48
|
+
const dx = end.x - start.x;
|
|
49
|
+
const dy = end.y - start.y;
|
|
50
|
+
const length = Math.hypot(dx, dy) || 1;
|
|
51
|
+
const offset = item.bend * length;
|
|
52
|
+
return {
|
|
53
|
+
start,
|
|
54
|
+
end,
|
|
55
|
+
control: worldPoint(mid.x + -dy / length * offset, mid.y + dx / length * offset)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** World-space bounds of an arrow, sampling the curve for accuracy. */
|
|
59
|
+
function arrowBounds(item, getFrame) {
|
|
60
|
+
const resolved = resolveArrow(item, getFrame);
|
|
61
|
+
if (!resolved) return null;
|
|
62
|
+
const samples = sampleQuadratic(resolved, 12);
|
|
63
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
64
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
65
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
66
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
67
|
+
for (const p of samples) {
|
|
68
|
+
minX = Math.min(minX, p.x);
|
|
69
|
+
minY = Math.min(minY, p.y);
|
|
70
|
+
maxX = Math.max(maxX, p.x);
|
|
71
|
+
maxY = Math.max(maxY, p.y);
|
|
72
|
+
}
|
|
73
|
+
const pad = Math.max(8, item.size * 2);
|
|
74
|
+
return {
|
|
75
|
+
x: minX - pad,
|
|
76
|
+
y: minY - pad,
|
|
77
|
+
width: Math.max(1, maxX - minX + pad * 2),
|
|
78
|
+
height: Math.max(1, maxY - minY + pad * 2)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** Sample the arrow's quadratic curve into world points (for render/hit-test). */
|
|
82
|
+
function sampleQuadratic(resolved, segments) {
|
|
83
|
+
const { start, control, end } = resolved;
|
|
84
|
+
const out = [];
|
|
85
|
+
for (let i = 0; i <= segments; i += 1) {
|
|
86
|
+
const t = i / segments;
|
|
87
|
+
const mt = 1 - t;
|
|
88
|
+
out.push(worldPoint(mt * mt * start.x + 2 * mt * t * control.x + t * t * end.x, mt * mt * start.y + 2 * mt * t * control.y + t * t * end.y));
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/** Distance from a world point to the arrow curve (hit testing). */
|
|
93
|
+
function distanceToArrow(item, getFrame, point) {
|
|
94
|
+
const resolved = resolveArrow(item, getFrame);
|
|
95
|
+
if (!resolved) return Number.POSITIVE_INFINITY;
|
|
96
|
+
const samples = sampleQuadratic(resolved, 16);
|
|
97
|
+
let min = Number.POSITIVE_INFINITY;
|
|
98
|
+
for (let i = 0; i < samples.length - 1; i += 1) {
|
|
99
|
+
const from = samples[i];
|
|
100
|
+
const to = samples[i + 1];
|
|
101
|
+
if (!from || !to) continue;
|
|
102
|
+
const d = segmentDistance(point, from, to);
|
|
103
|
+
if (d < min) min = d;
|
|
104
|
+
}
|
|
105
|
+
return min;
|
|
106
|
+
}
|
|
107
|
+
function segmentDistance(p, a, b) {
|
|
108
|
+
const dx = b.x - a.x;
|
|
109
|
+
const dy = b.y - a.y;
|
|
110
|
+
const lengthSq = dx * dx + dy * dy;
|
|
111
|
+
if (lengthSq === 0) return Math.hypot(p.x - a.x, p.y - a.y);
|
|
112
|
+
const t = Math.min(1, Math.max(0, ((p.x - a.x) * dx + (p.y - a.y) * dy) / lengthSq));
|
|
113
|
+
return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Turn a free endpoint into a binding if a candidate target frame contains the
|
|
117
|
+
* point; otherwise keep it free. Used while dragging an arrow handle so the
|
|
118
|
+
* arrow "connects" to shapes it lands on.
|
|
119
|
+
*/
|
|
120
|
+
function bindEndpointAt(point, target) {
|
|
121
|
+
if (!target) return {
|
|
122
|
+
kind: "point",
|
|
123
|
+
x: point.x,
|
|
124
|
+
y: point.y
|
|
125
|
+
};
|
|
126
|
+
const { nx, ny } = worldToAnchor(target.frame, point);
|
|
127
|
+
return {
|
|
128
|
+
kind: "binding",
|
|
129
|
+
target: target.id,
|
|
130
|
+
nx,
|
|
131
|
+
ny,
|
|
132
|
+
precise: true
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Translate an arrow by a delta: free endpoints move; bindings stay attached to
|
|
137
|
+
* their targets (so a bound end anchors while the rest of the arrow moves). The
|
|
138
|
+
* frame — a derived bounding box, not a transform — is recomputed from the new
|
|
139
|
+
* endpoint geometry.
|
|
140
|
+
*/
|
|
141
|
+
function translateArrow(item, dx, dy, getFrame) {
|
|
142
|
+
const move = (endpoint) => endpoint.kind === "point" ? {
|
|
143
|
+
kind: "point",
|
|
144
|
+
x: endpoint.x + dx,
|
|
145
|
+
y: endpoint.y + dy
|
|
146
|
+
} : endpoint;
|
|
147
|
+
const next = {
|
|
148
|
+
...item,
|
|
149
|
+
start: move(item.start),
|
|
150
|
+
end: move(item.end)
|
|
151
|
+
};
|
|
152
|
+
const bounds = arrowBounds(next, getFrame);
|
|
153
|
+
return bounds ? {
|
|
154
|
+
...next,
|
|
155
|
+
frame: {
|
|
156
|
+
...bounds,
|
|
157
|
+
rotation: 0
|
|
158
|
+
}
|
|
159
|
+
} : next;
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
export { anchorToWorld, arrowBounds, bindEndpointAt, distanceToArrow, resolveArrow, resolveEndpoint, sampleQuadratic, translateArrow, worldToAnchor };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { DrawPoint } from "../../protocol/dist/board-document.js";
|
|
2
|
+
import { Rect, WorldPoint } from "../geometry.js";
|
|
3
|
+
//#region src/board/core/draw-geometry.d.ts
|
|
4
|
+
/** Radius of a sample in world units given the stroke size and pressure. */
|
|
5
|
+
declare function sampleRadius(size: number, pressure: number): number;
|
|
6
|
+
/** Axis-aligned bounds of a stroke in its local space, padded by stroke width. */
|
|
7
|
+
declare function computeDrawBounds(points: DrawPoint[], size: number): Rect;
|
|
8
|
+
/**
|
|
9
|
+
* Ramer–Douglas–Peucker simplification. Reduces point count for low-zoom
|
|
10
|
+
* rendering without touching the persisted raw samples. Returns indices into
|
|
11
|
+
* the input so callers can keep pressure alongside the simplified path.
|
|
12
|
+
*/
|
|
13
|
+
declare function simplifyDrawIndices(points: DrawPoint[], tolerance: number): number[];
|
|
14
|
+
/**
|
|
15
|
+
* Build a closed outline polygon for a variable-width stroke. The left side is
|
|
16
|
+
* offset outward along each segment normal by the sample radius, the right side
|
|
17
|
+
* mirrored, producing a smooth ribbon that widens with pressure. Suitable for a
|
|
18
|
+
* filled Pixi polygon. Returns an empty array for degenerate input.
|
|
19
|
+
*/
|
|
20
|
+
declare function buildStrokeOutline(points: DrawPoint[], size: number): Array<{
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Distance from a world point to the stroke's polyline, in the shape's local
|
|
26
|
+
* space. Used for hit testing: a hit registers within half the stroke width plus
|
|
27
|
+
* a small tolerance. `local` is the point expressed in the draw item's frame.
|
|
28
|
+
*/
|
|
29
|
+
declare function distanceToStroke(points: DrawPoint[], local: WorldPoint): number;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { buildStrokeOutline, computeDrawBounds, distanceToStroke, sampleRadius, simplifyDrawIndices };
|