@canvas-harness/core 0.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/dist/index.cjs +5767 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3024 -0
- package/dist/index.d.ts +3024 -0
- package/dist/index.js +5599 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,3024 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branded ID types so node/edge/group ids never accidentally cross.
|
|
3
|
+
*/
|
|
4
|
+
type NodeId = string & {
|
|
5
|
+
readonly __brand: 'NodeId';
|
|
6
|
+
};
|
|
7
|
+
type EdgeId = string & {
|
|
8
|
+
readonly __brand: 'EdgeId';
|
|
9
|
+
};
|
|
10
|
+
type GroupId = string & {
|
|
11
|
+
readonly __brand: 'GroupId';
|
|
12
|
+
};
|
|
13
|
+
type ClientId = string & {
|
|
14
|
+
readonly __brand: 'ClientId';
|
|
15
|
+
};
|
|
16
|
+
type BatchId = string & {
|
|
17
|
+
readonly __brand: 'BatchId';
|
|
18
|
+
};
|
|
19
|
+
declare const asNodeId: (s: string) => NodeId;
|
|
20
|
+
declare const asEdgeId: (s: string) => EdgeId;
|
|
21
|
+
declare const asGroupId: (s: string) => GroupId;
|
|
22
|
+
declare const asClientId: (s: string) => ClientId;
|
|
23
|
+
declare const asBatchId: (s: string) => BatchId;
|
|
24
|
+
/**
|
|
25
|
+
* A point in world coordinates.
|
|
26
|
+
*/
|
|
27
|
+
type Vec2 = {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* An axis-aligned rectangle in world coordinates.
|
|
33
|
+
* x/y is the top-left; w/h extend toward +x/+y.
|
|
34
|
+
*/
|
|
35
|
+
type WorldRect = {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
w: number;
|
|
39
|
+
h: number;
|
|
40
|
+
};
|
|
41
|
+
declare const SCHEMA_VERSION: 1;
|
|
42
|
+
type SchemaVersion = typeof SCHEMA_VERSION;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Style tokens — see ARCHITECTURE.md §3.4. All fields optional;
|
|
46
|
+
* missing values fall back to theme resolver, then built-in defaults.
|
|
47
|
+
*/
|
|
48
|
+
type StrokeStyle = 'solid' | 'dashed' | 'dotted';
|
|
49
|
+
type FontFamily = 'handwriting' | 'sans-serif' | 'serif' | 'monospace' | 'informal';
|
|
50
|
+
type FontSize = 'S' | 'M' | 'L' | 'XL';
|
|
51
|
+
type TextAlign = 'left' | 'center' | 'right';
|
|
52
|
+
type TextStyle = 'normal' | 'bold' | 'italic';
|
|
53
|
+
type Arrowhead = 'none' | 'arrow' | 'barb' | 'arrow-filled';
|
|
54
|
+
type Style = {
|
|
55
|
+
strokeColor?: string;
|
|
56
|
+
strokeWidth?: number;
|
|
57
|
+
strokeStyle?: StrokeStyle;
|
|
58
|
+
backgroundColor?: string;
|
|
59
|
+
roughness?: number;
|
|
60
|
+
roundness?: number;
|
|
61
|
+
opacity?: number;
|
|
62
|
+
fontFamily?: FontFamily;
|
|
63
|
+
fontSize?: FontSize;
|
|
64
|
+
textAlign?: TextAlign;
|
|
65
|
+
textColor?: string;
|
|
66
|
+
textStyle?: TextStyle;
|
|
67
|
+
/**
|
|
68
|
+
* When true, the node's height auto-adjusts to fit `content` on add /
|
|
69
|
+
* edit-commit / resize-commit. Defaults to true for any node type.
|
|
70
|
+
* See ARCHITECTURE.md §8 (autofit lives on commit boundaries, never
|
|
71
|
+
* per-keystroke).
|
|
72
|
+
*/
|
|
73
|
+
autoFit?: boolean;
|
|
74
|
+
};
|
|
75
|
+
type EdgeStyle = Style & {
|
|
76
|
+
sourceArrowhead?: Arrowhead;
|
|
77
|
+
targetArrowhead?: Arrowhead;
|
|
78
|
+
/**
|
|
79
|
+
* Position of `edge.content` (the edge label) along the polyline,
|
|
80
|
+
* expressed as arc-length `[0..1]`. Default `0.5` (midpoint).
|
|
81
|
+
*/
|
|
82
|
+
labelArcLength?: number;
|
|
83
|
+
/**
|
|
84
|
+
* When true, the label rotates to follow the tangent of the edge at
|
|
85
|
+
* its anchor. Default false — labels stay upright. See
|
|
86
|
+
* ARCHITECTURE.md §6.11.
|
|
87
|
+
*/
|
|
88
|
+
labelFollowsTangent?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Background color of the edge-label chip. `'transparent'` or
|
|
91
|
+
* `'none'` skips the chip entirely (text renders directly on the
|
|
92
|
+
* edge). Falls through to `theme('edge.label.background')` if unset,
|
|
93
|
+
* then to white. Per-edge override of the theme default.
|
|
94
|
+
*/
|
|
95
|
+
labelBackground?: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Built-in node types — see ARCHITECTURE.md §3.5.
|
|
100
|
+
* Custom node types are arbitrary strings registered via defineNode.
|
|
101
|
+
*/
|
|
102
|
+
type BuiltInNodeType = 'rect' | 'ellipse' | 'diamond' | 'tag' | 'capsule' | 'thought-cloud' | 'layered-rect' | 'layered-ellipse' | 'layered-diamond' | 'text' | 'image' | 'icon' | 'frame';
|
|
103
|
+
type NodeType = BuiltInNodeType | (string & {
|
|
104
|
+
readonly __nodeType?: never;
|
|
105
|
+
});
|
|
106
|
+
/**
|
|
107
|
+
* Scene node — see ARCHITECTURE.md §3.2.
|
|
108
|
+
*
|
|
109
|
+
* Coordinates are top-left world coords pre-rotation; `angle` is radians
|
|
110
|
+
* around node center. `content` is lite-markdown for text-bearing built-in
|
|
111
|
+
* shapes; `data` is type-specific payload for everything else.
|
|
112
|
+
*/
|
|
113
|
+
type Node = {
|
|
114
|
+
id: NodeId;
|
|
115
|
+
type: NodeType;
|
|
116
|
+
x: number;
|
|
117
|
+
y: number;
|
|
118
|
+
w: number;
|
|
119
|
+
h: number;
|
|
120
|
+
angle: number;
|
|
121
|
+
z: number;
|
|
122
|
+
groups: GroupId[];
|
|
123
|
+
locked?: boolean;
|
|
124
|
+
hidden?: boolean;
|
|
125
|
+
content?: string;
|
|
126
|
+
style?: Style;
|
|
127
|
+
data?: unknown;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
type PathStyle = 'straight' | 'bezier' | 'polyline';
|
|
131
|
+
/**
|
|
132
|
+
* Edge endpoint — see ARCHITECTURE.md §6.1.
|
|
133
|
+
*
|
|
134
|
+
* Attached: `localOffset` is in the node's pre-rotation local frame,
|
|
135
|
+
* top-left origin, absolute pixels. Endpoint follows the node
|
|
136
|
+
* automatically via projection at render time.
|
|
137
|
+
* Free-floating: `worldPoint` is in world coordinates.
|
|
138
|
+
*/
|
|
139
|
+
type EdgeEnd = {
|
|
140
|
+
nodeId: NodeId;
|
|
141
|
+
localOffset: Vec2;
|
|
142
|
+
} | {
|
|
143
|
+
worldPoint: Vec2;
|
|
144
|
+
};
|
|
145
|
+
declare const isAttached: (e: EdgeEnd) => e is {
|
|
146
|
+
nodeId: NodeId;
|
|
147
|
+
localOffset: Vec2;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Scene edge — see ARCHITECTURE.md §3.3.
|
|
151
|
+
*/
|
|
152
|
+
type Edge = {
|
|
153
|
+
id: EdgeId;
|
|
154
|
+
source: EdgeEnd;
|
|
155
|
+
target: EdgeEnd;
|
|
156
|
+
pathStyle: PathStyle;
|
|
157
|
+
control?: Vec2[];
|
|
158
|
+
z: number;
|
|
159
|
+
groups: GroupId[];
|
|
160
|
+
locked?: boolean;
|
|
161
|
+
hidden?: boolean;
|
|
162
|
+
content?: string;
|
|
163
|
+
style?: EdgeStyle;
|
|
164
|
+
data?: unknown;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Group metadata — see ARCHITECTURE.md §3.6.
|
|
169
|
+
*
|
|
170
|
+
* Membership is NOT stored here; each node/edge has its own `groups: GroupId[]`.
|
|
171
|
+
* A Group is just optional name/color metadata for the id.
|
|
172
|
+
*/
|
|
173
|
+
type Group = {
|
|
174
|
+
id: GroupId;
|
|
175
|
+
name?: string;
|
|
176
|
+
color?: string;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Camera state — see ARCHITECTURE.md §4 and §13.4 camera API.
|
|
181
|
+
*
|
|
182
|
+
* x/y is the world-space coord that maps to the top-left of the viewport.
|
|
183
|
+
* z is the zoom factor (1 = 1px world per 1px screen).
|
|
184
|
+
*/
|
|
185
|
+
type CameraState = {
|
|
186
|
+
x: number;
|
|
187
|
+
y: number;
|
|
188
|
+
z: number;
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* In-memory scene shape. Records keyed by id for O(1) lookup.
|
|
192
|
+
* Wire format swaps records for arrays at the codec boundary (see §3.8).
|
|
193
|
+
*/
|
|
194
|
+
type Scene = {
|
|
195
|
+
schemaVersion: SchemaVersion;
|
|
196
|
+
nodes: Record<NodeId, Node>;
|
|
197
|
+
edges: Record<EdgeId, Edge>;
|
|
198
|
+
groups: Record<GroupId, Group>;
|
|
199
|
+
camera: CameraState;
|
|
200
|
+
selection: (NodeId | EdgeId)[];
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* On-the-wire serialized form. Arrays gzip smaller and have predictable iteration order.
|
|
204
|
+
* Camera + selection are unchanged.
|
|
205
|
+
*/
|
|
206
|
+
type SerializedScene = {
|
|
207
|
+
schemaVersion: SchemaVersion;
|
|
208
|
+
nodes: Node[];
|
|
209
|
+
edges: Edge[];
|
|
210
|
+
groups: Group[];
|
|
211
|
+
camera: CameraState;
|
|
212
|
+
selection: (NodeId | EdgeId)[];
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Operations — see ARCHITECTURE.md §10.2.
|
|
217
|
+
*
|
|
218
|
+
* Every committed scene mutation is one Op. `prev` slices capture the
|
|
219
|
+
* fields touched by an update so undo can apply the inverse without a diff.
|
|
220
|
+
*/
|
|
221
|
+
type Op = {
|
|
222
|
+
type: 'node.add';
|
|
223
|
+
node: Node;
|
|
224
|
+
} | {
|
|
225
|
+
type: 'node.update';
|
|
226
|
+
id: NodeId;
|
|
227
|
+
patch: Partial<Node>;
|
|
228
|
+
prev: Partial<Node>;
|
|
229
|
+
} | {
|
|
230
|
+
type: 'node.remove';
|
|
231
|
+
node: Node;
|
|
232
|
+
} | {
|
|
233
|
+
type: 'edge.add';
|
|
234
|
+
edge: Edge;
|
|
235
|
+
} | {
|
|
236
|
+
type: 'edge.update';
|
|
237
|
+
id: EdgeId;
|
|
238
|
+
patch: Partial<Edge>;
|
|
239
|
+
prev: Partial<Edge>;
|
|
240
|
+
} | {
|
|
241
|
+
type: 'edge.remove';
|
|
242
|
+
edge: Edge;
|
|
243
|
+
} | {
|
|
244
|
+
type: 'group.upsert';
|
|
245
|
+
group: Group;
|
|
246
|
+
prev?: Group;
|
|
247
|
+
} | {
|
|
248
|
+
type: 'group.remove';
|
|
249
|
+
group: Group;
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* A batch is the atomic unit of mutation (also the unit of undo/redo and sync).
|
|
253
|
+
*/
|
|
254
|
+
type OpBatch = {
|
|
255
|
+
id: BatchId;
|
|
256
|
+
clientId: ClientId;
|
|
257
|
+
ts: number;
|
|
258
|
+
origin: 'local' | 'remote' | 'history';
|
|
259
|
+
ops: Op[];
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Canvas background — see ARCHITECTURE.md §4 (rendering pipeline).
|
|
264
|
+
*
|
|
265
|
+
* Local-only render-time config (not part of the synced scene). Drives
|
|
266
|
+
* the page color plus an optional infinite dot / grid pattern that
|
|
267
|
+
* helps spatial orientation while panning.
|
|
268
|
+
*
|
|
269
|
+
* Patterns are world-space: dots/lines are anchored to the world
|
|
270
|
+
* origin, so panning moves *through* them rather than dragging them
|
|
271
|
+
* along.
|
|
272
|
+
*/
|
|
273
|
+
type CanvasBackgroundPattern = 'none' | 'dots' | 'grid';
|
|
274
|
+
type CanvasBackground = {
|
|
275
|
+
/** Page background color. Default `'#f8fafc'`. */
|
|
276
|
+
color?: string;
|
|
277
|
+
/** Pattern overlay on top of the color. Default `'none'`. */
|
|
278
|
+
pattern?: CanvasBackgroundPattern;
|
|
279
|
+
/** World units between adjacent dots / grid lines. Default `20`. */
|
|
280
|
+
gap?: number;
|
|
281
|
+
/** Color of the dots / grid lines. Default `'#cbd5e1'`. */
|
|
282
|
+
patternColor?: string;
|
|
283
|
+
/**
|
|
284
|
+
* Hide the pattern when `camera.z < minZoom`. Useful to declutter
|
|
285
|
+
* zoomed-out views and skip the per-frame pattern paint cost. Default
|
|
286
|
+
* `0` (no minimum — pattern shows at any zoom, subject to the LOD
|
|
287
|
+
* skip when individual cells would be sub-2px).
|
|
288
|
+
*/
|
|
289
|
+
minZoom?: number;
|
|
290
|
+
/**
|
|
291
|
+
* Hide the pattern when `camera.z > maxZoom`. Default `Infinity` (no
|
|
292
|
+
* maximum). Most consumers won't need this; useful if you want the
|
|
293
|
+
* pattern to disappear when zoomed in past a "detail" threshold.
|
|
294
|
+
*/
|
|
295
|
+
maxZoom?: number;
|
|
296
|
+
};
|
|
297
|
+
declare const DEFAULT_BACKGROUND: Required<CanvasBackground>;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* ID generation — see ARCHITECTURE.md §10.8.
|
|
301
|
+
*
|
|
302
|
+
* Default scheme: `${clientId}-${counter}`. Collision-free across clients
|
|
303
|
+
* without coordination, human-readable in dev tools, monotonic per client.
|
|
304
|
+
*
|
|
305
|
+
* Consumers may override via `createCanvasStore({ idGenerator })`.
|
|
306
|
+
*/
|
|
307
|
+
|
|
308
|
+
type IdGenerator = () => string;
|
|
309
|
+
/**
|
|
310
|
+
* Generates a random short client id like "u-7f3a".
|
|
311
|
+
* Used when no `clientId` is passed and no `sync` adapter is attached.
|
|
312
|
+
*/
|
|
313
|
+
declare const randomClientId: () => ClientId;
|
|
314
|
+
/**
|
|
315
|
+
* Builds an id generator that prefixes a stable client id with an
|
|
316
|
+
* incrementing counter. Each call returns a fresh, never-recycled id.
|
|
317
|
+
*/
|
|
318
|
+
declare const makeIdGenerator: (clientId: ClientId) => IdGenerator;
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Camera math — see ARCHITECTURE.md §4.3 and §13.4.
|
|
322
|
+
*
|
|
323
|
+
* The camera maps world coordinates to screen coordinates via:
|
|
324
|
+
* screen = (world - camera.{x,y}) * camera.z
|
|
325
|
+
* world = screen / camera.z + camera.{x,y}
|
|
326
|
+
*
|
|
327
|
+
* camera.{x,y} is the world-space point shown at screen (0,0).
|
|
328
|
+
* camera.z is the zoom factor (1 = identity).
|
|
329
|
+
*/
|
|
330
|
+
|
|
331
|
+
declare const DEFAULT_CAMERA: CameraState;
|
|
332
|
+
declare const MIN_ZOOM = 0.05;
|
|
333
|
+
declare const MAX_ZOOM = 16;
|
|
334
|
+
/**
|
|
335
|
+
* Converts a screen-space point to world coords given the current camera.
|
|
336
|
+
*/
|
|
337
|
+
declare const screenToWorld: (screen: Vec2, camera: CameraState) => Vec2;
|
|
338
|
+
/**
|
|
339
|
+
* Converts a world-space point to screen coords given the current camera.
|
|
340
|
+
*/
|
|
341
|
+
declare const worldToScreen: (world: Vec2, camera: CameraState) => Vec2;
|
|
342
|
+
/**
|
|
343
|
+
* Computes the world-space rect currently visible inside a viewport of the
|
|
344
|
+
* given screen-space size. Used for viewport culling queries.
|
|
345
|
+
*/
|
|
346
|
+
declare const viewportWorldRect: (camera: CameraState, viewportW: number, viewportH: number) => WorldRect;
|
|
347
|
+
/**
|
|
348
|
+
* Clamps a zoom factor to the supported range.
|
|
349
|
+
*/
|
|
350
|
+
declare const clampZoom: (z: number) => number;
|
|
351
|
+
/**
|
|
352
|
+
* Applies a zoom delta keeping the world point under `screenAnchor` stationary.
|
|
353
|
+
* Useful for wheel-zoom and pinch-zoom — keeps focus where the user looks.
|
|
354
|
+
*/
|
|
355
|
+
declare const zoomAtScreenPoint: (camera: CameraState, newZoom: number, screenAnchor: Vec2) => CameraState;
|
|
356
|
+
/**
|
|
357
|
+
* Pans the camera by a screen-space delta (e.g. from a drag gesture).
|
|
358
|
+
*/
|
|
359
|
+
declare const panByScreen: (camera: CameraState, deltaScreen: Vec2) => CameraState;
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* AABB utilities. Rectangles are { x, y, w, h } in world space.
|
|
363
|
+
*/
|
|
364
|
+
declare const rectContainsPoint: (r: WorldRect, p: Vec2) => boolean;
|
|
365
|
+
declare const rectsIntersect: (a: WorldRect, b: WorldRect) => boolean;
|
|
366
|
+
/**
|
|
367
|
+
* Inflate rect by a uniform world-space amount on all sides.
|
|
368
|
+
*/
|
|
369
|
+
declare const inflateRect: (r: WorldRect, amount: number) => WorldRect;
|
|
370
|
+
/**
|
|
371
|
+
* Smallest AABB containing two points.
|
|
372
|
+
*/
|
|
373
|
+
declare const rectFromPoints: (a: Vec2, b: Vec2) => WorldRect;
|
|
374
|
+
/**
|
|
375
|
+
* Smallest AABB containing all given rects. Returns null for empty input.
|
|
376
|
+
*/
|
|
377
|
+
declare const unionRects: (rects: WorldRect[]) => WorldRect | null;
|
|
378
|
+
|
|
379
|
+
type SpatialId = string;
|
|
380
|
+
declare class UniformGrid {
|
|
381
|
+
private readonly cellSize;
|
|
382
|
+
private readonly cells;
|
|
383
|
+
private readonly bounds;
|
|
384
|
+
constructor(cellSize?: number);
|
|
385
|
+
get size(): number;
|
|
386
|
+
/**
|
|
387
|
+
* Inserts or replaces an entry. Removes previous cell membership if the id existed.
|
|
388
|
+
*/
|
|
389
|
+
insert(id: SpatialId, aabb: WorldRect): void;
|
|
390
|
+
/**
|
|
391
|
+
* Removes an entry. No-op if id is unknown.
|
|
392
|
+
*/
|
|
393
|
+
remove(id: SpatialId): void;
|
|
394
|
+
/**
|
|
395
|
+
* Returns the stored AABB for an id, if any.
|
|
396
|
+
*/
|
|
397
|
+
getAABB(id: SpatialId): WorldRect | undefined;
|
|
398
|
+
/**
|
|
399
|
+
* Returns ids whose AABB intersects the query rect.
|
|
400
|
+
* Broad-phase only — callers do narrow-phase per id.
|
|
401
|
+
*/
|
|
402
|
+
queryRect(rect: WorldRect): SpatialId[];
|
|
403
|
+
/**
|
|
404
|
+
* Returns ids whose AABB contains the point.
|
|
405
|
+
*/
|
|
406
|
+
queryPoint(p: Vec2): SpatialId[];
|
|
407
|
+
/**
|
|
408
|
+
* Empties the index. O(1) on the bookkeeping; the GC handles the rest.
|
|
409
|
+
*/
|
|
410
|
+
clear(): void;
|
|
411
|
+
/**
|
|
412
|
+
* Yields the cell keys that an AABB covers.
|
|
413
|
+
*/
|
|
414
|
+
private cellKeysFor;
|
|
415
|
+
private removeFromCells;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Compute world-space AABB for a (possibly rotated) node.
|
|
420
|
+
*
|
|
421
|
+
* For axis-aligned nodes (angle === 0): the AABB is the node rect itself.
|
|
422
|
+
* For rotated nodes: enclose the 4 rotated corners.
|
|
423
|
+
*/
|
|
424
|
+
|
|
425
|
+
declare const nodeAABB: (node: Node) => WorldRect;
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Auto-routing for bezier edges — see ARCHITECTURE.md §6.6.
|
|
429
|
+
*
|
|
430
|
+
* Computes cubic-bezier control points by projecting outward from each
|
|
431
|
+
* endpoint along the attachment-side normal. Rotation-aware: a node
|
|
432
|
+
* rotated 30° gets a normal rotated 30° too, so edges leave perpendicular
|
|
433
|
+
* to the rotated side.
|
|
434
|
+
*/
|
|
435
|
+
|
|
436
|
+
type Side = 'n' | 's' | 'e' | 'w';
|
|
437
|
+
/**
|
|
438
|
+
* Picks the side of a node's local rect closest to the given local offset.
|
|
439
|
+
* Used to determine which way a bezier should leave the node.
|
|
440
|
+
*/
|
|
441
|
+
declare const sideOf: (node: Node, localX: number, localY: number) => Side;
|
|
442
|
+
/**
|
|
443
|
+
* Outward-pointing unit vector for a given side, in the node's pre-rotation
|
|
444
|
+
* local frame.
|
|
445
|
+
*/
|
|
446
|
+
declare const sideNormalLocal: (side: Side) => Vec2;
|
|
447
|
+
/**
|
|
448
|
+
* Rotates a local-frame vector into world coordinates by the node's angle.
|
|
449
|
+
*/
|
|
450
|
+
declare const rotateVecByAngle: (v: Vec2, angle: number) => Vec2;
|
|
451
|
+
/**
|
|
452
|
+
* Computes auto-routed control points for a cubic bezier between
|
|
453
|
+
* sourceWorld and targetWorld. Each control point is offset along the
|
|
454
|
+
* outward normal of its endpoint's attached node (if any).
|
|
455
|
+
*
|
|
456
|
+
* For free-floating endpoints (no node), the control aligns with the
|
|
457
|
+
* source→target direction so the curve degenerates gracefully toward a
|
|
458
|
+
* straight line.
|
|
459
|
+
*/
|
|
460
|
+
declare const autoRouteControls: (sourceWorld: Vec2, targetWorld: Vec2, sourceNormalWorld: Vec2 | null, targetNormalWorld: Vec2 | null) => {
|
|
461
|
+
c1: Vec2;
|
|
462
|
+
c2: Vec2;
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Result of clipping the polyline against the two attached-node rects.
|
|
467
|
+
* `startIndex` / `endIndex` are sample indices; the visible polyline is
|
|
468
|
+
* `[startPoint, samples[startIndex+1..endIndex-1], endPoint]`.
|
|
469
|
+
* If both ends are free-floating (no attached node), returns the full range.
|
|
470
|
+
*/
|
|
471
|
+
type ClipResult = {
|
|
472
|
+
startIndex: number;
|
|
473
|
+
endIndex: number;
|
|
474
|
+
startPoint: Vec2;
|
|
475
|
+
endPoint: Vec2;
|
|
476
|
+
visible: boolean;
|
|
477
|
+
};
|
|
478
|
+
declare const fullVisibleClipResult: (samples: Vec2[]) => ClipResult;
|
|
479
|
+
/**
|
|
480
|
+
* Clips a polyline against (up to) two attached-node rects.
|
|
481
|
+
* `sourceNode` / `targetNode` are the nodes the source/target endpoints
|
|
482
|
+
* attach to (null for free-floating endpoints).
|
|
483
|
+
*/
|
|
484
|
+
declare const clipSamples: (samples: Vec2[], sourceNode: Node | null, targetNode: Node | null) => ClipResult;
|
|
485
|
+
|
|
486
|
+
type EdgeGeometry = {
|
|
487
|
+
/** Endpoint world positions (post-projection, pre-clip). */
|
|
488
|
+
source: Vec2;
|
|
489
|
+
target: Vec2;
|
|
490
|
+
/** Polyline samples for paint / hit-test / clip. */
|
|
491
|
+
samples: Vec2[];
|
|
492
|
+
/** AABB enclosing samples (padded for arrowheads). */
|
|
493
|
+
aabb: WorldRect;
|
|
494
|
+
/** Whether the edge is a self-loop (source.nodeId === target.nodeId). */
|
|
495
|
+
isSelfLoop: boolean;
|
|
496
|
+
/** Source/target attached node IDs (or null). Used by paint to clip. */
|
|
497
|
+
sourceNodeId: NodeId | null;
|
|
498
|
+
targetNodeId: NodeId | null;
|
|
499
|
+
};
|
|
500
|
+
/**
|
|
501
|
+
* Computes edge geometry from current node state. No caching — callers
|
|
502
|
+
* memoize on a version key (see makeEdgeVersion).
|
|
503
|
+
*/
|
|
504
|
+
declare const computeEdgeGeometry: (edge: Edge, getNode: (id: NodeId) => Node | undefined) => EdgeGeometry | null;
|
|
505
|
+
/**
|
|
506
|
+
* Cache wrapper: stores last-computed geometry per edge id, keyed by an
|
|
507
|
+
* integer version supplied by the caller. The store maintains the version
|
|
508
|
+
* counter and bumps it on geometry-affecting mutations (edge.update or
|
|
509
|
+
* incident node moves), so this cache becomes a pure integer-compare.
|
|
510
|
+
*
|
|
511
|
+
* Earlier versions of this file used a `toFixed(2)`-built string version
|
|
512
|
+
* to detect changes implicitly. That allocated ~14 strings per edge per
|
|
513
|
+
* paint and cost ~5-8ms at 2k visible edges. Explicit integer versioning
|
|
514
|
+
* eliminates that entirely.
|
|
515
|
+
*/
|
|
516
|
+
declare class EdgeGeometryCache {
|
|
517
|
+
private readonly entries;
|
|
518
|
+
/**
|
|
519
|
+
* Returns the cached geometry if `version` matches the cache entry;
|
|
520
|
+
* otherwise recomputes via `computeEdgeGeometry`, stores, and returns.
|
|
521
|
+
* Caller is responsible for passing the current store-managed version.
|
|
522
|
+
*/
|
|
523
|
+
get(edge: Edge, version: number, getNode: (id: NodeId) => Node | undefined): EdgeGeometry | null;
|
|
524
|
+
delete(id: EdgeId): void;
|
|
525
|
+
clear(): void;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Edge AABB from samples — see ARCHITECTURE.md §6.12.
|
|
530
|
+
*
|
|
531
|
+
* Phase-1 had a crude AABB based on raw endpoint positions. Phase 4
|
|
532
|
+
* computes the actual sample bounds + padding for arrowheads and labels,
|
|
533
|
+
* which is what the spatial index needs for correct hit-testing.
|
|
534
|
+
*/
|
|
535
|
+
|
|
536
|
+
declare const edgeAABBFromSamples: (samples: Vec2[]) => WorldRect;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Returns `{ point, tangent }` at fractional arc-length `t ∈ [0..1]`
|
|
540
|
+
* along a sample polyline.
|
|
541
|
+
*
|
|
542
|
+
* Walks the polyline summing segment lengths until the target is
|
|
543
|
+
* reached; linearly interpolates inside the last segment. The tangent
|
|
544
|
+
* is the unit direction of that segment.
|
|
545
|
+
*
|
|
546
|
+
* Used by edge-label placement (see ARCHITECTURE.md §6.11) — labels at
|
|
547
|
+
* arc-length 0.5 sit at the geometric midpoint regardless of curve
|
|
548
|
+
* shape, and the tangent lets a label rotate to follow the edge.
|
|
549
|
+
*
|
|
550
|
+
* @example
|
|
551
|
+
* const { point, tangent } = getPointAndTangentAtArcLength(geom.samples, 0.5)
|
|
552
|
+
* ctx.translate(point.x, point.y)
|
|
553
|
+
*/
|
|
554
|
+
declare const getPointAndTangentAtArcLength: (samples: readonly Vec2[], t: number) => {
|
|
555
|
+
point: Vec2;
|
|
556
|
+
tangent: Vec2;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Converts a single midpoint `P` (the point the user dragged the
|
|
561
|
+
* bezier midpoint handle to) into a pair of cubic control points
|
|
562
|
+
* `(c1, c2)` such that the resulting cubic **passes through `P` at
|
|
563
|
+
* t = 0.5**.
|
|
564
|
+
*
|
|
565
|
+
* Math: a cubic Bezier with endpoints S, T and controls c1, c2 has
|
|
566
|
+
*
|
|
567
|
+
* B(0.5) = (1/8) · S + (3/8) · c1 + (3/8) · c2 + (1/8) · T
|
|
568
|
+
*
|
|
569
|
+
* Solving for c1 + c2 when B(0.5) = P:
|
|
570
|
+
*
|
|
571
|
+
* c1 + c2 = (8P − S − T) / 3
|
|
572
|
+
*
|
|
573
|
+
* One equation, two unknowns. The convention here: split symmetrically
|
|
574
|
+
* (c1 = c2). That keeps the curve smooth and predictable for the user.
|
|
575
|
+
* A two-handle form (c1 / c2 draggable independently) is the v1.x
|
|
576
|
+
* follow-up.
|
|
577
|
+
*/
|
|
578
|
+
declare const midpointToCubicControls: (source: Vec2, midpoint: Vec2, target: Vec2) => {
|
|
579
|
+
c1: Vec2;
|
|
580
|
+
c2: Vec2;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Arrowheads — see ARCHITECTURE.md §6.5 + §3.4 (4 styles).
|
|
585
|
+
*
|
|
586
|
+
* The arrowhead sits at the clipped endpoint of the edge; its direction
|
|
587
|
+
* comes from the curve's tangent at that arc-length position. We draw
|
|
588
|
+
* in world coords so the strokeWidth follows the camera transform —
|
|
589
|
+
* scale of strokeWidth/scale gives a constant-screen-px width.
|
|
590
|
+
*/
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Draws an arrowhead at the given world tip, pointing toward `tipDir`
|
|
594
|
+
* (the unit tangent direction at that point — pointing FROM the curve
|
|
595
|
+
* INTO the tip).
|
|
596
|
+
*/
|
|
597
|
+
declare const drawArrowhead: (ctx: CanvasRenderingContext2D, kind: Arrowhead, tip: Vec2, tipDir: Vec2, strokeColor: string, strokeWidth: number) => void;
|
|
598
|
+
/**
|
|
599
|
+
* World-space length added to the visible arrowhead at a given stroke
|
|
600
|
+
* width. Used to know how much to shorten the curve's visible portion
|
|
601
|
+
* so the line tail doesn't poke through the arrowhead tip.
|
|
602
|
+
*/
|
|
603
|
+
declare const arrowheadLength: (kind: Arrowhead, strokeWidth: number) => number;
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Built-in style defaults — see ARCHITECTURE.md §3.4.
|
|
607
|
+
*
|
|
608
|
+
* Each render call resolves style via:
|
|
609
|
+
* 1. node.style[token] if set
|
|
610
|
+
* 2. theme(token) if a theme resolver is provided
|
|
611
|
+
* 3. the value here
|
|
612
|
+
*/
|
|
613
|
+
|
|
614
|
+
declare const DEFAULT_STYLE: Required<Pick<Style, 'strokeColor' | 'strokeWidth' | 'strokeStyle' | 'backgroundColor' | 'opacity' | 'roundness'>>;
|
|
615
|
+
/**
|
|
616
|
+
* Resolves a style field with the precedence above.
|
|
617
|
+
* `theme` lookup is optional and returns `undefined` if no override.
|
|
618
|
+
*
|
|
619
|
+
* Stable token catalog (consumer maps these to its design system):
|
|
620
|
+
* - `strokeColor` edge + shape stroke
|
|
621
|
+
* - `strokeWidth` shape + edge stroke width
|
|
622
|
+
* - `backgroundColor` shape fill
|
|
623
|
+
* - `textColor` shape text color
|
|
624
|
+
* - `opacity` shape opacity (0-100)
|
|
625
|
+
* - `selection.outline` selection outline color (overlay)
|
|
626
|
+
* - `handle.fill` resize/rotate handle fill
|
|
627
|
+
* - `handle.stroke` resize/rotate handle stroke
|
|
628
|
+
* - `text.highlight` markdown ==highlight== chip color
|
|
629
|
+
* - `text.codeBackground` markdown `code` chip color
|
|
630
|
+
*
|
|
631
|
+
* Tokens not in this list are passed through unchanged; consumers can
|
|
632
|
+
* extend with their own (e.g. `node.shadow.color`).
|
|
633
|
+
*/
|
|
634
|
+
type ThemeResolver = (token: string) => string | number | undefined;
|
|
635
|
+
declare const resolveColor: (style: Style | undefined, key: "strokeColor" | "backgroundColor" | "textColor", fallback: string, theme?: ThemeResolver) => string;
|
|
636
|
+
declare const resolveStrokeWidth: (style: Style | undefined, theme?: ThemeResolver) => number;
|
|
637
|
+
declare const resolveOpacity: (style: Style | undefined, theme?: ThemeResolver) => number;
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Edge paint entry. `scale` is the world → device factor (camera.z ×
|
|
641
|
+
* dpr) — kept for back-compat with PNG export and tests. `opts.zoom`,
|
|
642
|
+
* `opts.dpr`, `opts.isMoving` control the label bitmap-cache LOD; if
|
|
643
|
+
* omitted, derived from `scale` (zoom = scale, dpr = 1, idle).
|
|
644
|
+
*/
|
|
645
|
+
declare const drawEdge: (ctx: CanvasRenderingContext2D, edge: Edge, geom: EdgeGeometry, sourceNode: Node | null, targetNode: Node | null, scale: number, theme?: ThemeResolver, opts?: {
|
|
646
|
+
roughEnabled?: boolean;
|
|
647
|
+
zoom?: number;
|
|
648
|
+
dpr?: number;
|
|
649
|
+
isMoving?: boolean;
|
|
650
|
+
}) => void;
|
|
651
|
+
/**
|
|
652
|
+
* World-space label bounding box. Used by the hit-test (see
|
|
653
|
+
* `hitTestEdge`). Returns `null` when the edge has no content.
|
|
654
|
+
*/
|
|
655
|
+
declare const edgeLabelBoundsWorld: (edge: Edge, geom: EdgeGeometry) => {
|
|
656
|
+
x: number;
|
|
657
|
+
y: number;
|
|
658
|
+
w: number;
|
|
659
|
+
h: number;
|
|
660
|
+
} | null;
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Resolves an EdgeEnd to its current world coordinates.
|
|
664
|
+
* Returns null when the endpoint is attached to a node that no longer exists.
|
|
665
|
+
*/
|
|
666
|
+
declare const projectEndToWorld: (end: EdgeEnd, getNode: (id: NodeId) => Node | undefined) => Vec2 | null;
|
|
667
|
+
/**
|
|
668
|
+
* Transforms a point in a node's pre-rotation local frame (top-left origin)
|
|
669
|
+
* into world coordinates.
|
|
670
|
+
*/
|
|
671
|
+
declare const nodeLocalToWorld: (local: Vec2, node: Node) => Vec2;
|
|
672
|
+
/**
|
|
673
|
+
* Transforms a world point into the node's pre-rotation local frame.
|
|
674
|
+
* Used by auto-clip and by edge-creation snap-to-boundary logic.
|
|
675
|
+
*/
|
|
676
|
+
declare const worldToNodeLocal: (world: Vec2, node: Node) => Vec2;
|
|
677
|
+
/**
|
|
678
|
+
* Given a world point and a node, returns the local-frame coords of the
|
|
679
|
+
* nearest point on the node's rect boundary. If the world point is inside
|
|
680
|
+
* the rect, projects to the nearest edge; if outside, clamps to the
|
|
681
|
+
* containing edge / corner.
|
|
682
|
+
*
|
|
683
|
+
* Used by the edge-creation gesture to snap endpoints to the node boundary.
|
|
684
|
+
*/
|
|
685
|
+
declare const projectToNodeBoundary: (world: Vec2, node: Node) => Vec2;
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Curve sampling — see ARCHITECTURE.md §6.6 / §6.9.
|
|
689
|
+
*
|
|
690
|
+
* The polyline samples are the load-bearing data for everything edge-related:
|
|
691
|
+
* paint, auto-clip, hit testing all walk the same array. Caching is in
|
|
692
|
+
* cache.ts; this module is pure geometry.
|
|
693
|
+
*/
|
|
694
|
+
|
|
695
|
+
/** Default number of intermediate samples for a bezier (cubic). 32 is
|
|
696
|
+
* indistinguishable from 64 at typical zoom; halve the array size. */
|
|
697
|
+
declare const BEZIER_SEGMENTS = 32;
|
|
698
|
+
/**
|
|
699
|
+
* Evaluates a cubic bezier at parameter t ∈ [0, 1].
|
|
700
|
+
*/
|
|
701
|
+
declare const cubicBezier: (p0: Vec2, c1: Vec2, c2: Vec2, p1: Vec2, t: number) => Vec2;
|
|
702
|
+
/**
|
|
703
|
+
* Tangent (unit vector) to a cubic bezier at parameter t.
|
|
704
|
+
* Used for arrowhead orientation.
|
|
705
|
+
*/
|
|
706
|
+
declare const cubicBezierTangent: (p0: Vec2, c1: Vec2, c2: Vec2, p1: Vec2, t: number) => Vec2;
|
|
707
|
+
/**
|
|
708
|
+
* Samples a cubic bezier into BEZIER_SEGMENTS+1 evenly-spaced points
|
|
709
|
+
* (in parameter space — not arc-length).
|
|
710
|
+
*/
|
|
711
|
+
declare const sampleBezier: (p0: Vec2, c1: Vec2, c2: Vec2, p1: Vec2, segments?: number) => Vec2[];
|
|
712
|
+
/**
|
|
713
|
+
* Returns the polyline sample list for an edge given its path style,
|
|
714
|
+
* world-projected endpoints, and (for bezier) control points or (for
|
|
715
|
+
* polyline) midpoints. Straight = 2-point polyline.
|
|
716
|
+
*/
|
|
717
|
+
declare const samplesFor: (pathStyle: PathStyle, source: Vec2, target: Vec2, controls: Vec2[] | undefined) => Vec2[];
|
|
718
|
+
/**
|
|
719
|
+
* Tangent at parameter t along the sampled polyline (arc-length-ish).
|
|
720
|
+
* Used for arrowhead orientation when we don't have analytic curve info.
|
|
721
|
+
* For straight/polyline this returns the segment direction; for bezier
|
|
722
|
+
* we approximate by the direction between adjacent samples around the
|
|
723
|
+
* target t.
|
|
724
|
+
*/
|
|
725
|
+
declare const tangentAtArcLength: (samples: Vec2[], t: number) => Vec2;
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Returns world-space (source, target, control1, control2) for a self-loop
|
|
729
|
+
* on the given node. The loop exits from the top edge and re-enters via
|
|
730
|
+
* the right edge.
|
|
731
|
+
*/
|
|
732
|
+
declare const selfLoopGeometry: (node: Node) => {
|
|
733
|
+
source: Vec2;
|
|
734
|
+
target: Vec2;
|
|
735
|
+
controls: [Vec2, Vec2];
|
|
736
|
+
};
|
|
737
|
+
/**
|
|
738
|
+
* Samples a self-loop given its node. Convenience wrapper that produces
|
|
739
|
+
* the polyline samples auto-clip/hit-test/paint all consume.
|
|
740
|
+
*/
|
|
741
|
+
declare const sampleSelfLoop: (node: Node) => Vec2[];
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Custom node type definitions — see ARCHITECTURE.md §5.
|
|
745
|
+
*
|
|
746
|
+
* `defineNode` creates a registerable NodeTypeDef. The core doesn't know
|
|
747
|
+
* about React; the `view` field is typed as `unknown` so the React layer
|
|
748
|
+
* (or the playground for phase 5) can stash a component reference there
|
|
749
|
+
* without coupling the core package to React.
|
|
750
|
+
*
|
|
751
|
+
* The library reads:
|
|
752
|
+
* - kind: 'canvas' | 'react' — which render path applies
|
|
753
|
+
* - renderCanvas / drawPlaceholder — canvas paint functions
|
|
754
|
+
* - getSnapshot — optional bitmap fallback for LOD / motion
|
|
755
|
+
* - hitTest / getOutline — interaction hooks
|
|
756
|
+
* - lod — zoom thresholds
|
|
757
|
+
* - lifecycle hooks
|
|
758
|
+
*
|
|
759
|
+
* Consumers register types via `createCanvasStore({ nodeTypes: [...] })`.
|
|
760
|
+
*/
|
|
761
|
+
|
|
762
|
+
type RenderEnv = {
|
|
763
|
+
zoom: number;
|
|
764
|
+
isMoving: boolean;
|
|
765
|
+
isSelected: boolean;
|
|
766
|
+
isHovered: boolean;
|
|
767
|
+
isEditing: boolean;
|
|
768
|
+
theme(token: string): string | number | undefined;
|
|
769
|
+
};
|
|
770
|
+
type SnapshotEnv = {
|
|
771
|
+
width: number;
|
|
772
|
+
height: number;
|
|
773
|
+
dpr: number;
|
|
774
|
+
};
|
|
775
|
+
type NodeTypeDefOptions = {
|
|
776
|
+
/** Unique type id, e.g. 'chart-card'. */
|
|
777
|
+
type: string;
|
|
778
|
+
renderCanvas?: (ctx: CanvasRenderingContext2D, node: Node, env: RenderEnv) => void;
|
|
779
|
+
drawPlaceholder?: (ctx: CanvasRenderingContext2D, node: Node, env: RenderEnv) => void;
|
|
780
|
+
/**
|
|
781
|
+
* The React view component reference. Stored as `unknown` here because the
|
|
782
|
+
* core package is framework-agnostic. The React layer reads this field
|
|
783
|
+
* when it needs to mount a custom node in the DOM overlay.
|
|
784
|
+
*/
|
|
785
|
+
view?: unknown;
|
|
786
|
+
lod?: {
|
|
787
|
+
/** Below this zoom, prefer drawPlaceholder over the React view. Default 0.7. */
|
|
788
|
+
minZoomForReact?: number;
|
|
789
|
+
/** Below this zoom, skip the node entirely. Default 0.3. */
|
|
790
|
+
minZoomForPlaceholder?: number;
|
|
791
|
+
/** ms; default Infinity. After this age, the snapshot is regenerated. */
|
|
792
|
+
snapshotMaxAge?: number;
|
|
793
|
+
};
|
|
794
|
+
/**
|
|
795
|
+
* Author-provided rasterized fallback. Library calls this when it needs a
|
|
796
|
+
* fast paint (motion, low zoom) and uses `drawImage` to blit. Returns null
|
|
797
|
+
* to fall back to `drawPlaceholder`.
|
|
798
|
+
*/
|
|
799
|
+
getSnapshot?: (node: Node, env: SnapshotEnv) => CanvasImageSource | null | Promise<CanvasImageSource | null>;
|
|
800
|
+
/**
|
|
801
|
+
* Custom hit-test. Receives the world point pre-transformed into the node's
|
|
802
|
+
* pre-rotation local frame, origin top-left. Default: AABB.
|
|
803
|
+
*/
|
|
804
|
+
hitTest?: (node: Node, localPoint: Vec2) => boolean;
|
|
805
|
+
/**
|
|
806
|
+
* Custom outline polygon (in node-local coords). Default: rect AABB.
|
|
807
|
+
* Used by the edge auto-clip system when an edge attaches to this node.
|
|
808
|
+
*/
|
|
809
|
+
getOutline?: (node: Node) => Vec2[] | null;
|
|
810
|
+
/** Called when the node enters the viewport / mounts a live React view. */
|
|
811
|
+
onEnter?: (node: Node) => void;
|
|
812
|
+
/** Called when the node exits the viewport / unmounts. */
|
|
813
|
+
onExit?: (node: Node) => void;
|
|
814
|
+
/**
|
|
815
|
+
* If true, the React view stays mounted (hidden via visibility:hidden) when
|
|
816
|
+
* off-screen instead of unmounting. Use sparingly — defeats viewport culling.
|
|
817
|
+
* Default false.
|
|
818
|
+
*/
|
|
819
|
+
keepMounted?: boolean;
|
|
820
|
+
/** Validation / migration on scene load. */
|
|
821
|
+
parse?: (raw: unknown) => Node['data'];
|
|
822
|
+
migrate?: (data: unknown, fromVersion: number) => Node['data'];
|
|
823
|
+
};
|
|
824
|
+
/**
|
|
825
|
+
* Normalized form of a node type definition. The `kind` field is derived
|
|
826
|
+
* from which render paths are provided so the renderer dispatch is one
|
|
827
|
+
* `switch` away.
|
|
828
|
+
*/
|
|
829
|
+
type NodeTypeDef = NodeTypeDefOptions & {
|
|
830
|
+
kind: 'canvas-only' | 'react-only' | 'mixed' | 'invalid';
|
|
831
|
+
lod: {
|
|
832
|
+
minZoomForReact: number;
|
|
833
|
+
minZoomForPlaceholder: number;
|
|
834
|
+
snapshotMaxAge: number;
|
|
835
|
+
};
|
|
836
|
+
};
|
|
837
|
+
/**
|
|
838
|
+
* Defines a custom node type. Register the returned def via
|
|
839
|
+
* `createCanvasStore({ nodeTypes: [myDef, ...] })`; then any `Node`
|
|
840
|
+
* with `type === opts.type` will be dispatched to your renderers + hit
|
|
841
|
+
* test + lifecycle hooks.
|
|
842
|
+
*
|
|
843
|
+
* A type must supply at least one render path: `renderCanvas` (paints
|
|
844
|
+
* via the 2D context), `view` (React component reference — used by
|
|
845
|
+
* `<Canvas renderCustomNodeView>`), or both (`mixed` — canvas at low
|
|
846
|
+
* zoom, React at high zoom).
|
|
847
|
+
*
|
|
848
|
+
* @example
|
|
849
|
+
* // Canvas-only — fastest path, paints with the 2D context.
|
|
850
|
+
* export const sparklineDef = defineNode({
|
|
851
|
+
* type: 'sparkline',
|
|
852
|
+
* renderCanvas(ctx, node, env) {
|
|
853
|
+
* ctx.strokeStyle = env.theme('node.stroke') as string ?? '#000'
|
|
854
|
+
* // ...draw a sparkline...
|
|
855
|
+
* },
|
|
856
|
+
* hitTest: (node, p) => p.x >= 0 && p.x <= node.w && p.y >= 0 && p.y <= node.h,
|
|
857
|
+
* })
|
|
858
|
+
*
|
|
859
|
+
* @example
|
|
860
|
+
* // React view — full UI; library mounts it in the DOM overlay above
|
|
861
|
+
* // the canvas at high zoom, falls back to drawPlaceholder below.
|
|
862
|
+
* export const chartCardDef = defineNode({
|
|
863
|
+
* type: 'chart-card',
|
|
864
|
+
* view: ChartCardComponent,
|
|
865
|
+
* drawPlaceholder(ctx, node) {
|
|
866
|
+
* ctx.fillStyle = '#e0e7ff'
|
|
867
|
+
* ctx.fillRect(0, 0, node.w, node.h)
|
|
868
|
+
* },
|
|
869
|
+
* lod: { minZoomForReact: 0.7, minZoomForPlaceholder: 0.3 },
|
|
870
|
+
* })
|
|
871
|
+
*/
|
|
872
|
+
declare const defineNode: (opts: NodeTypeDefOptions) => NodeTypeDef;
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Lite-markdown tokenizer — ported verbatim from
|
|
876
|
+
* `dim0/webui/src/components/markdown/canvas-lite-markdown.tsx`.
|
|
877
|
+
*
|
|
878
|
+
* The vocabulary is deliberately small: bold, italic, underline, strike,
|
|
879
|
+
* highlight, inline code, links, fenced code blocks, hr lines. No
|
|
880
|
+
* nesting, no escapes — single-pass regex tokenization keeps layout
|
|
881
|
+
* fast at scale. See ARCHITECTURE.md §8.
|
|
882
|
+
*/
|
|
883
|
+
type InlineType = 'text' | 'bold' | 'italic' | 'underline' | 'strike' | 'highlight' | 'code' | 'link';
|
|
884
|
+
type Token = {
|
|
885
|
+
type: InlineType;
|
|
886
|
+
content: string;
|
|
887
|
+
} | {
|
|
888
|
+
type: 'code-block';
|
|
889
|
+
content: string;
|
|
890
|
+
} | {
|
|
891
|
+
type: 'br';
|
|
892
|
+
} | {
|
|
893
|
+
type: 'hr';
|
|
894
|
+
} | {
|
|
895
|
+
type: 'hr-double';
|
|
896
|
+
};
|
|
897
|
+
/**
|
|
898
|
+
* Full markdown tokenizer with fenced code-block support. Code blocks
|
|
899
|
+
* are display-only (no language badge / syntax highlighting).
|
|
900
|
+
*/
|
|
901
|
+
declare const tokenize: (input: string) => Token[];
|
|
902
|
+
|
|
903
|
+
type StyledRun = {
|
|
904
|
+
text: string;
|
|
905
|
+
type: InlineType;
|
|
906
|
+
};
|
|
907
|
+
type LayoutLine = {
|
|
908
|
+
kind: 'text';
|
|
909
|
+
runs: StyledRun[];
|
|
910
|
+
} | {
|
|
911
|
+
kind: 'code-block';
|
|
912
|
+
runs: StyledRun[];
|
|
913
|
+
isFirst: boolean;
|
|
914
|
+
isLast: boolean;
|
|
915
|
+
} | {
|
|
916
|
+
kind: 'rule';
|
|
917
|
+
double: boolean;
|
|
918
|
+
};
|
|
919
|
+
type LayoutOptions = {
|
|
920
|
+
width: number;
|
|
921
|
+
fontFamily: FontFamily;
|
|
922
|
+
fontSize: FontSize;
|
|
923
|
+
textStyle: TextStyle;
|
|
924
|
+
};
|
|
925
|
+
/**
|
|
926
|
+
* Turns tokens into drawable lines. Output consumed by the canvas paint pass.
|
|
927
|
+
*/
|
|
928
|
+
declare const layoutTokens: (tokens: Token[], opts: LayoutOptions) => LayoutLine[];
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Font + line-height defaults — matches `dim0/webui/.../canvas-lite-markdown.tsx`
|
|
932
|
+
* and `dim0/backend/topix/datatypes/note/style.py`.
|
|
933
|
+
*
|
|
934
|
+
* The maps below are the canonical contract between consumer style tokens
|
|
935
|
+
* and concrete typography. Custom fonts live in the consumer's @font-face;
|
|
936
|
+
* the library only renames them.
|
|
937
|
+
*/
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Mirrors the font stacks defined in dim0's index.css so canvas measurement
|
|
941
|
+
* matches DOM text. Custom fonts must be loaded by the consumer (via
|
|
942
|
+
* @font-face / Google Fonts); the font-epoch reactivity (see font-epoch.ts)
|
|
943
|
+
* invalidates the bitmap cache when fonts finish loading.
|
|
944
|
+
*/
|
|
945
|
+
declare const FONT_FAMILY_MAP: Record<FontFamily, string>;
|
|
946
|
+
declare const FONT_SIZE_MAP: Record<FontSize, number>;
|
|
947
|
+
declare const LINE_HEIGHT_MAP: Record<FontSize, number>;
|
|
948
|
+
declare const CODE_BLOCK_PADDING_X = 6;
|
|
949
|
+
declare const CODE_BLOCK_MARGIN_Y = 4;
|
|
950
|
+
declare const CONTENT_HEIGHT_BUFFER = 4;
|
|
951
|
+
declare const CONTENT_PADDING = 6;
|
|
952
|
+
declare const DEFAULT_TEXT_COLOR = "#1f2937";
|
|
953
|
+
declare const DEFAULT_HIGHLIGHT_COLOR = "#fde047";
|
|
954
|
+
declare const DEFAULT_HIGHLIGHT_COLOR_DARK = "#6b5a23";
|
|
955
|
+
declare const LINK_COLOR = "#2563eb";
|
|
956
|
+
declare const CODE_BG_COLOR = "rgba(148, 163, 184, 0.18)";
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Returns the canvas `font` string for a given run.
|
|
960
|
+
*/
|
|
961
|
+
declare const getCanvasFont: (opts: {
|
|
962
|
+
type: InlineType;
|
|
963
|
+
fontFamily: FontFamily;
|
|
964
|
+
fontSize: FontSize;
|
|
965
|
+
textStyle: TextStyle;
|
|
966
|
+
}) => string;
|
|
967
|
+
/**
|
|
968
|
+
* Memoized width measurement. Cache key includes font (so different
|
|
969
|
+
* fonts/sizes/styles get separate entries).
|
|
970
|
+
*
|
|
971
|
+
* Falls back to a heuristic when no canvas is available (SSR / Node tests).
|
|
972
|
+
*/
|
|
973
|
+
declare const measureText: (opts: {
|
|
974
|
+
text: string;
|
|
975
|
+
type: InlineType;
|
|
976
|
+
fontFamily: FontFamily;
|
|
977
|
+
fontSize: FontSize;
|
|
978
|
+
textStyle: TextStyle;
|
|
979
|
+
}) => number;
|
|
980
|
+
/**
|
|
981
|
+
* Clears all cached measurements. Called by the font-epoch system on
|
|
982
|
+
* font load — fallback metrics returned before fonts settle would be
|
|
983
|
+
* wrong, so we re-measure everything once they're ready.
|
|
984
|
+
*/
|
|
985
|
+
declare const clearMeasureCache: () => void;
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Subscribe to font-epoch bumps. Lazy-initializes the document.fonts
|
|
989
|
+
* listeners on first call. Returns an unsubscribe.
|
|
990
|
+
*/
|
|
991
|
+
declare const subscribeFontEpoch: (listener: (epoch: number) => void) => (() => void);
|
|
992
|
+
/**
|
|
993
|
+
* Current epoch — included in bitmap-cache keys so they invalidate when
|
|
994
|
+
* custom fonts settle.
|
|
995
|
+
*/
|
|
996
|
+
declare const getFontEpoch: () => number;
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Buckets zoom to avoid cache churn from sub-1%-zoom changes.
|
|
1000
|
+
*/
|
|
1001
|
+
declare const quantizeZoom: (value: number) => number;
|
|
1002
|
+
/**
|
|
1003
|
+
* Buckets DPR to keep cache keys stable across tiny devicePixelRatio
|
|
1004
|
+
* variance (e.g. when a window crosses a mixed-DPR monitor boundary).
|
|
1005
|
+
*/
|
|
1006
|
+
declare const quantizeDpr: (value: number) => number;
|
|
1007
|
+
/**
|
|
1008
|
+
* Chooses a render scale from a base scale, the current zoom bucket,
|
|
1009
|
+
* and whether the camera (or shape) is in motion. While moving, drop
|
|
1010
|
+
* quality for throughput; on idle, snap back to full quality.
|
|
1011
|
+
*/
|
|
1012
|
+
declare const resolveRenderScale: (baseScale: number, zoom: number, isMoving: boolean) => number;
|
|
1013
|
+
/**
|
|
1014
|
+
* Applies the absolute backing-store size cap so very-wide / very-tall
|
|
1015
|
+
* shapes don't blow up memory at high zoom.
|
|
1016
|
+
*/
|
|
1017
|
+
declare const clampEffectiveScale: (baseScale: number, width: number, height: number) => number;
|
|
1018
|
+
|
|
1019
|
+
type DrawTextOptions = {
|
|
1020
|
+
text: string;
|
|
1021
|
+
width: number;
|
|
1022
|
+
height: number;
|
|
1023
|
+
align: TextAlign;
|
|
1024
|
+
fontFamily: FontFamily;
|
|
1025
|
+
fontSize: FontSize;
|
|
1026
|
+
textStyle: TextStyle;
|
|
1027
|
+
textColor: string;
|
|
1028
|
+
highlightColor: string;
|
|
1029
|
+
};
|
|
1030
|
+
/**
|
|
1031
|
+
* Paints `text` (markdown) into the canvas at (0..width, 0..height) using
|
|
1032
|
+
* the laid-out lines and styled runs. Background chips for inline code +
|
|
1033
|
+
* highlight, vertical centering when content shorter than height, dashed
|
|
1034
|
+
* decorations for underline/strike/link.
|
|
1035
|
+
*/
|
|
1036
|
+
declare const drawTextToCanvas: (ctx: CanvasRenderingContext2D, opts: DrawTextOptions) => void;
|
|
1037
|
+
|
|
1038
|
+
type EstimateOptions = {
|
|
1039
|
+
text: string;
|
|
1040
|
+
width: number;
|
|
1041
|
+
fontFamily?: FontFamily;
|
|
1042
|
+
fontSize?: FontSize;
|
|
1043
|
+
textStyle?: TextStyle;
|
|
1044
|
+
};
|
|
1045
|
+
declare const getContentHeight: (lines: LayoutLine[], lineHeight: number) => number;
|
|
1046
|
+
declare const estimateMarkdownContentHeight: ({ text, width, fontFamily, fontSize, textStyle, }: EstimateOptions) => number;
|
|
1047
|
+
declare const getMarkdownLineHeightPx: (fontSize: FontSize) => number;
|
|
1048
|
+
|
|
1049
|
+
type BitmapCacheRequest = {
|
|
1050
|
+
/** Stable id for the source — typically the node id. */
|
|
1051
|
+
id: string;
|
|
1052
|
+
text: string;
|
|
1053
|
+
/** Logical CSS pixels of the destination rect. */
|
|
1054
|
+
width: number;
|
|
1055
|
+
height: number;
|
|
1056
|
+
zoom: number;
|
|
1057
|
+
dpr: number;
|
|
1058
|
+
isMoving: boolean;
|
|
1059
|
+
align: TextAlign;
|
|
1060
|
+
fontFamily: FontFamily;
|
|
1061
|
+
fontSize: FontSize;
|
|
1062
|
+
textStyle: TextStyle;
|
|
1063
|
+
textColor: string;
|
|
1064
|
+
highlightColor: string;
|
|
1065
|
+
};
|
|
1066
|
+
type BitmapCacheEntry = {
|
|
1067
|
+
/** Backing-store canvas — pass to ctx.drawImage. */
|
|
1068
|
+
canvas: HTMLCanvasElement;
|
|
1069
|
+
/** Logical (CSS) target width — what the caller should draw at. */
|
|
1070
|
+
width: number;
|
|
1071
|
+
/** Logical (CSS) target height. */
|
|
1072
|
+
height: number;
|
|
1073
|
+
};
|
|
1074
|
+
/**
|
|
1075
|
+
* Lookup-or-build. Always returns a non-null entry as long as `text` is
|
|
1076
|
+
* non-empty — on miss it draws synchronously and stores. Same call site
|
|
1077
|
+
* for hit and miss.
|
|
1078
|
+
*
|
|
1079
|
+
* Returns null only when the input is empty/whitespace.
|
|
1080
|
+
*/
|
|
1081
|
+
declare const getOrRenderTextBitmap: (req: BitmapCacheRequest) => BitmapCacheEntry | null;
|
|
1082
|
+
/** Test / debug aid. */
|
|
1083
|
+
declare const clearTextBitmapCache: () => void;
|
|
1084
|
+
/** Test / debug aid. */
|
|
1085
|
+
declare const getTextBitmapCacheSize: () => number;
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Auto-fit policy — see ARCHITECTURE.md §8 and IMPLEMENTATION.md Phase 7.
|
|
1089
|
+
*
|
|
1090
|
+
* Height of a content-bearing node is recomputed on **commit boundaries**:
|
|
1091
|
+
* - `store.addNode` (when creating a node with content)
|
|
1092
|
+
* - `store.commitEdit` (when the user finishes editing)
|
|
1093
|
+
* - resize-commit (when a width-resize ends; new wrap → new height)
|
|
1094
|
+
*
|
|
1095
|
+
* NEVER per-keystroke. The textarea-editor grows its own DOM textarea
|
|
1096
|
+
* during typing; the canvas catches up once on commit.
|
|
1097
|
+
*/
|
|
1098
|
+
/**
|
|
1099
|
+
* Should this node auto-fit its height to its content?
|
|
1100
|
+
*
|
|
1101
|
+
* Default: true for all node types — matches tldraw/excalidraw behavior
|
|
1102
|
+
* where a sticky / shape grows to fit whatever you type. Set
|
|
1103
|
+
* `style.autoFit: false` to opt out.
|
|
1104
|
+
*/
|
|
1105
|
+
declare const shouldAutoFit: (node: Node) => boolean;
|
|
1106
|
+
/**
|
|
1107
|
+
* Returns the height this node *would* have if its content laid out at its
|
|
1108
|
+
* current width and style. For empty content, returns one line-height so a
|
|
1109
|
+
* shape with the empty-content placeholder isn't zero-sized.
|
|
1110
|
+
*/
|
|
1111
|
+
declare const computeAutoFitHeight: (node: Node) => number;
|
|
1112
|
+
/**
|
|
1113
|
+
* Pure: returns a copy of `node` with `h` adjusted to fit `content`, if
|
|
1114
|
+
* the node opts into autofit. Otherwise returns the input unchanged.
|
|
1115
|
+
*
|
|
1116
|
+
* Pure-by-design so it can run inside `addNode` before the op is enqueued
|
|
1117
|
+
* (avoids a double op: add + update-height).
|
|
1118
|
+
*/
|
|
1119
|
+
declare const withAutoFitHeight: (node: Node) => Node;
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Editor markdown shortcuts — see IMPLEMENTATION.md Phase 7.
|
|
1123
|
+
*
|
|
1124
|
+
* Pure string-transform helpers, framework-agnostic. The default DOM
|
|
1125
|
+
* textarea editor wires these to keyboard events; consumers using a
|
|
1126
|
+
* custom adapter (Lexical / ProseMirror) can ignore this module.
|
|
1127
|
+
*
|
|
1128
|
+
* Each transform takes `(value, selStart, selEnd)` and returns the new
|
|
1129
|
+
* value + new selection so the editor can update its DOM textarea in
|
|
1130
|
+
* one pass.
|
|
1131
|
+
*/
|
|
1132
|
+
type Transform = {
|
|
1133
|
+
value: string;
|
|
1134
|
+
selStart: number;
|
|
1135
|
+
selEnd: number;
|
|
1136
|
+
};
|
|
1137
|
+
declare const toggleBold: (v: string, s: number, e: number) => Transform;
|
|
1138
|
+
declare const toggleItalic: (v: string, s: number, e: number) => Transform;
|
|
1139
|
+
declare const toggleUnderline: (v: string, s: number, e: number) => Transform;
|
|
1140
|
+
declare const toggleStrike: (v: string, s: number, e: number) => Transform;
|
|
1141
|
+
declare const toggleCode: (v: string, s: number, e: number) => Transform;
|
|
1142
|
+
/**
|
|
1143
|
+
* Inserts `[selection](url)`. If `url` is empty the cursor lands inside
|
|
1144
|
+
* the parens so the user can type the URL.
|
|
1145
|
+
*/
|
|
1146
|
+
declare const insertLink: (value: string, selStart: number, selEnd: number, url: string) => Transform;
|
|
1147
|
+
/**
|
|
1148
|
+
* Auto-list — when the user presses Enter inside a `- ` (or `* `, or
|
|
1149
|
+
* `1. ` etc.) line, the next line gets the same prefix prepended.
|
|
1150
|
+
* Two consecutive Enters on an empty list line exit the list.
|
|
1151
|
+
*
|
|
1152
|
+
* Returns a transform when an auto-list rule fires, else null. The
|
|
1153
|
+
* editor should fall back to the textarea's default Enter behavior in
|
|
1154
|
+
* the null case.
|
|
1155
|
+
*/
|
|
1156
|
+
declare const handleEnter: (value: string, selStart: number, selEnd: number) => Transform | null;
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* EditorAdapter — see ARCHITECTURE.md §8 (edit mode pluggability).
|
|
1160
|
+
*
|
|
1161
|
+
* Pluggable contract for the in-place editor. The default is a plain
|
|
1162
|
+
* `<textarea>` overlay (see `default-textarea-editor.ts`). Consumers can
|
|
1163
|
+
* swap in a Lexical / ProseMirror / TipTap subtree by implementing this
|
|
1164
|
+
* interface and passing it to the renderer.
|
|
1165
|
+
*
|
|
1166
|
+
* The adapter is created **per edit session** — `mount` runs on
|
|
1167
|
+
* `beginEdit`, `destroy` runs on `commit`/`cancel`.
|
|
1168
|
+
*/
|
|
1169
|
+
type EditorAdapterMountOptions = {
|
|
1170
|
+
/** The node being edited. Use its style for font-family / size / align. */
|
|
1171
|
+
node: Node;
|
|
1172
|
+
/** The host DOM container the adapter should append its element into. */
|
|
1173
|
+
container: HTMLElement;
|
|
1174
|
+
/**
|
|
1175
|
+
* Current camera. The adapter is responsible for positioning its
|
|
1176
|
+
* element so it overlaps the node in screen-space.
|
|
1177
|
+
*/
|
|
1178
|
+
camera: CameraState;
|
|
1179
|
+
/** Device pixel ratio at edit time. Used for crisp positioning. */
|
|
1180
|
+
dpr: number;
|
|
1181
|
+
/** Called when the user commits (Esc / blur / Cmd+Enter). */
|
|
1182
|
+
onCommit: (text: string) => void;
|
|
1183
|
+
/** Called when the user cancels (no content change). */
|
|
1184
|
+
onCancel: () => void;
|
|
1185
|
+
};
|
|
1186
|
+
type EditorAdapter = {
|
|
1187
|
+
/** Focus the editor (after mount). */
|
|
1188
|
+
focus(): void;
|
|
1189
|
+
/** Read the current draft text. */
|
|
1190
|
+
getValue(): string;
|
|
1191
|
+
/** Replace the draft text (rare; mostly used by undo or programmatic). */
|
|
1192
|
+
setValue(text: string): void;
|
|
1193
|
+
/** Tear down the editor and remove its DOM. */
|
|
1194
|
+
destroy(): void;
|
|
1195
|
+
};
|
|
1196
|
+
type EditorAdapterFactory = (opts: EditorAdapterMountOptions) => EditorAdapter;
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Default in-place editor — a plain `<textarea>` positioned over the
|
|
1200
|
+
* editing node. See Phase 7 plan.
|
|
1201
|
+
*
|
|
1202
|
+
* - Auto-sizes its height to its scrollHeight (DOM-native).
|
|
1203
|
+
* - Font matches the node's style so what you type roughly matches what
|
|
1204
|
+
* you'll see on commit.
|
|
1205
|
+
* - Cmd+B/I/U/Shift+X/E/K: bold/italic/underline/strike/code/link.
|
|
1206
|
+
* - Enter inside a `- ` line continues the bullet; double-Enter exits.
|
|
1207
|
+
* - Esc / Cmd+Enter / blur: commit. (cancel is wired by the renderer.)
|
|
1208
|
+
*/
|
|
1209
|
+
declare const createDefaultTextareaEditor: EditorAdapterFactory;
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Resize-handle hit testing — see ARCHITECTURE.md §11.6.
|
|
1213
|
+
*
|
|
1214
|
+
* Handles are drawn at constant screen size (e.g. 8px), so their world-space
|
|
1215
|
+
* bounds change with camera zoom. They sit at the 8 cardinal points of the
|
|
1216
|
+
* node's bounding rect; for rotated nodes the handle positions rotate with
|
|
1217
|
+
* the node so a "north-east" handle is actually at the rotated NE corner.
|
|
1218
|
+
*/
|
|
1219
|
+
|
|
1220
|
+
type ResizeHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w';
|
|
1221
|
+
declare const RESIZE_HANDLES: ResizeHandle[];
|
|
1222
|
+
/**
|
|
1223
|
+
* Screen-pixel size of a resize-handle hit target. Visual size matches.
|
|
1224
|
+
* Sized for touch reach (~14px) without being intrusive on desktop;
|
|
1225
|
+
* tldraw uses a similar value.
|
|
1226
|
+
*/
|
|
1227
|
+
declare const RESIZE_HANDLE_SIZE_PX = 14;
|
|
1228
|
+
/**
|
|
1229
|
+
* Screen-pixel distance from the top edge of the node to the rotation
|
|
1230
|
+
* handle center. Visual + hit-target match.
|
|
1231
|
+
*/
|
|
1232
|
+
declare const ROTATE_HANDLE_OFFSET_PX = 24;
|
|
1233
|
+
/** Screen-pixel radius of the rotation-handle hit target. */
|
|
1234
|
+
declare const ROTATE_HANDLE_RADIUS_PX = 9;
|
|
1235
|
+
/**
|
|
1236
|
+
* World-space centers of all 8 resize handles for the given node.
|
|
1237
|
+
* Rotation-aware: handles rotate with the node so they sit on the corners
|
|
1238
|
+
* and edge midpoints of the rotated rect (not the rotated AABB).
|
|
1239
|
+
*/
|
|
1240
|
+
declare const handleWorldPositions: (node: Node) => Record<ResizeHandle, Vec2>;
|
|
1241
|
+
/**
|
|
1242
|
+
* Returns the handle hit by a world point, or null. `cameraZ` lets us map
|
|
1243
|
+
* the constant screen-size handle box to its current world-space footprint.
|
|
1244
|
+
*/
|
|
1245
|
+
declare const hitTestHandles: (node: Node, worldPoint: Vec2, cameraZ: number) => ResizeHandle | null;
|
|
1246
|
+
/**
|
|
1247
|
+
* World-space position of the rotation handle — sits a constant 22px
|
|
1248
|
+
* (screen) above the node's top edge midpoint, perpendicular to the
|
|
1249
|
+
* rotated top edge.
|
|
1250
|
+
*/
|
|
1251
|
+
declare const rotateHandleWorldPosition: (node: Node, cameraZ: number) => Vec2;
|
|
1252
|
+
/**
|
|
1253
|
+
* Returns true if `worldPoint` is over the rotation handle for the node.
|
|
1254
|
+
*/
|
|
1255
|
+
declare const hitTestRotateHandle: (node: Node, worldPoint: Vec2, cameraZ: number) => boolean;
|
|
1256
|
+
|
|
1257
|
+
type InteractionMode = 'idle' | 'panning' | 'zooming' | 'dragging' | 'resizing' | 'rotating' | 'marqueeing' | 'creating-shape' | 'creating-edge' | 'reconnecting-edge' | 'editing';
|
|
1258
|
+
type PointerInfo = {
|
|
1259
|
+
worldX: number;
|
|
1260
|
+
worldY: number;
|
|
1261
|
+
screenX: number;
|
|
1262
|
+
screenY: number;
|
|
1263
|
+
pointerType: 'mouse' | 'touch' | 'pen';
|
|
1264
|
+
pressure?: number;
|
|
1265
|
+
};
|
|
1266
|
+
/**
|
|
1267
|
+
* The frozen geometry of a node at drag-start, used to compute the
|
|
1268
|
+
* uncommitted display position during drag (= original + delta).
|
|
1269
|
+
*/
|
|
1270
|
+
type DragOriginal = {
|
|
1271
|
+
id: NodeId;
|
|
1272
|
+
x: number;
|
|
1273
|
+
y: number;
|
|
1274
|
+
w: number;
|
|
1275
|
+
h: number;
|
|
1276
|
+
angle: number;
|
|
1277
|
+
};
|
|
1278
|
+
type InteractionState = {
|
|
1279
|
+
mode: InteractionMode;
|
|
1280
|
+
pointer: PointerInfo | null;
|
|
1281
|
+
draggedIds: NodeId[];
|
|
1282
|
+
dragOriginals: DragOriginal[];
|
|
1283
|
+
/** World-space delta from drag start; renderer applies this to draw the dragged set. */
|
|
1284
|
+
dragDelta: Vec2;
|
|
1285
|
+
resizeHandle: ResizeHandle | null;
|
|
1286
|
+
/** Whether the user is holding Shift during a resize (aspect-lock). */
|
|
1287
|
+
resizeLockAspect: boolean;
|
|
1288
|
+
/** Whether the user is holding Alt during a resize (resize from center). */
|
|
1289
|
+
resizeFromCenter: boolean;
|
|
1290
|
+
marqueeRect: WorldRect | null;
|
|
1291
|
+
/** Whether the marquee should add to (true, shift held) or replace selection. */
|
|
1292
|
+
marqueeAdditive: boolean;
|
|
1293
|
+
draftEdge: {
|
|
1294
|
+
source: EdgeEnd;
|
|
1295
|
+
target: EdgeEnd;
|
|
1296
|
+
/** When reconnecting an existing edge, the id; null for new edges. */
|
|
1297
|
+
reconnectingId: EdgeId | null;
|
|
1298
|
+
/** Snap candidate (a node id the target endpoint is hovering over). */
|
|
1299
|
+
snapTargetNodeId: NodeId | null;
|
|
1300
|
+
} | null;
|
|
1301
|
+
editingTarget: EditTarget | null;
|
|
1302
|
+
createDraftRect: WorldRect | null;
|
|
1303
|
+
createTool: string | null;
|
|
1304
|
+
};
|
|
1305
|
+
/** Identifies what's currently being edited — a node (text content) or
|
|
1306
|
+
* an edge (label content). See `store.beginEdit`. */
|
|
1307
|
+
type EditTarget = {
|
|
1308
|
+
kind: 'node';
|
|
1309
|
+
id: NodeId;
|
|
1310
|
+
} | {
|
|
1311
|
+
kind: 'edge';
|
|
1312
|
+
id: EdgeId;
|
|
1313
|
+
};
|
|
1314
|
+
declare const idleInteractionState: () => InteractionState;
|
|
1315
|
+
/**
|
|
1316
|
+
* Convenience: any of panning/zooming/dragging/resizing/rotating is "moving".
|
|
1317
|
+
*/
|
|
1318
|
+
declare const isMoving: (state: InteractionState) => boolean;
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Per-client awareness state that other clients see in real time.
|
|
1322
|
+
* Synced over the {@link SyncAdapter}; never in the op log; never
|
|
1323
|
+
* persisted by `toJSON`.
|
|
1324
|
+
*
|
|
1325
|
+
* Set the local copy via `store.presence.setLocal({...})`. Read the
|
|
1326
|
+
* remote copy via `usePresence()` / `usePresence(clientId)` (React) or
|
|
1327
|
+
* `store.presence.get(...)` / `store.presence.getAll()`.
|
|
1328
|
+
*/
|
|
1329
|
+
type PresenceState = {
|
|
1330
|
+
/** Stable id of the owning client. */
|
|
1331
|
+
clientId: ClientId;
|
|
1332
|
+
/** Cursor world position; null when the cursor has left the surface. */
|
|
1333
|
+
cursor: Vec2 | null;
|
|
1334
|
+
/** Ids the remote client has selected — for shared-awareness highlights. */
|
|
1335
|
+
selection: (NodeId | EdgeId)[];
|
|
1336
|
+
/** Node id the remote client is currently editing (or null). */
|
|
1337
|
+
editing: NodeId | null;
|
|
1338
|
+
/** Display color (hex). Used for remote cursors + selection outlines. */
|
|
1339
|
+
color: string;
|
|
1340
|
+
/** Display name. */
|
|
1341
|
+
name: string;
|
|
1342
|
+
};
|
|
1343
|
+
declare const emptyPresenceState: (clientId: ClientId) => PresenceState;
|
|
1344
|
+
type PresencePatch = Partial<Omit<PresenceState, 'clientId'>>;
|
|
1345
|
+
/**
|
|
1346
|
+
* Returns true if any remote presence has this node currently open in
|
|
1347
|
+
* edit mode. Used to enforce the exclusive edit-lock when a SyncAdapter
|
|
1348
|
+
* is attached (see ARCHITECTURE.md §9 collab edit semantics).
|
|
1349
|
+
*/
|
|
1350
|
+
declare const isNodeRemoteEditing: (remote: ReadonlyMap<ClientId, PresenceState>, nodeId: NodeId) => boolean;
|
|
1351
|
+
|
|
1352
|
+
type StoreOptions = {
|
|
1353
|
+
initial?: Scene;
|
|
1354
|
+
clientId?: ClientId;
|
|
1355
|
+
idGenerator?: IdGenerator;
|
|
1356
|
+
/**
|
|
1357
|
+
* Custom node type registry. Each entry created via `defineNode`. The
|
|
1358
|
+
* renderer consults this to decide between built-in shapes, canvas
|
|
1359
|
+
* custom paint, and React-overlay views. See ARCHITECTURE.md §5.
|
|
1360
|
+
*/
|
|
1361
|
+
nodeTypes?: NodeTypeDef[];
|
|
1362
|
+
};
|
|
1363
|
+
/**
|
|
1364
|
+
* Origin of an applied op — drives sync + undo behavior.
|
|
1365
|
+
*
|
|
1366
|
+
* Phase 1 only uses 'local'; remote/history wire up in phase 8.
|
|
1367
|
+
*/
|
|
1368
|
+
type OpOrigin = 'local' | 'remote' | 'history';
|
|
1369
|
+
/**
|
|
1370
|
+
* Presence change event payload. `removed: true` signals a remote client
|
|
1371
|
+
* has left. Local-presence changes carry the full new state.
|
|
1372
|
+
*/
|
|
1373
|
+
type PresenceEvent = {
|
|
1374
|
+
state: PresenceState;
|
|
1375
|
+
removed?: false;
|
|
1376
|
+
} | {
|
|
1377
|
+
clientId: ClientId;
|
|
1378
|
+
removed: true;
|
|
1379
|
+
};
|
|
1380
|
+
type StoreEvents = {
|
|
1381
|
+
/** Fires once per committed OpBatch (one batch per mutation or per `batch()` call). */
|
|
1382
|
+
change: OpBatch;
|
|
1383
|
+
/** Camera state changed (any field). */
|
|
1384
|
+
camera: CameraState;
|
|
1385
|
+
/** Selection changed. */
|
|
1386
|
+
selection: (NodeId | EdgeId)[];
|
|
1387
|
+
/** Interaction state changed (mode / pointer / drag / marquee / resize / edit). */
|
|
1388
|
+
interaction: InteractionState;
|
|
1389
|
+
/** Local or remote presence changed. Subscribers compare clientId to filter. */
|
|
1390
|
+
presence: PresenceEvent;
|
|
1391
|
+
/**
|
|
1392
|
+
* LWW conflict detected when applying a remote batch — `prev` slice
|
|
1393
|
+
* didn't match local current value. The op was still applied (last
|
|
1394
|
+
* writer wins); the event fires for telemetry / consumer UX.
|
|
1395
|
+
*/
|
|
1396
|
+
conflict: {
|
|
1397
|
+
batch: OpBatch;
|
|
1398
|
+
conflicts: {
|
|
1399
|
+
op: Op;
|
|
1400
|
+
field: string;
|
|
1401
|
+
}[];
|
|
1402
|
+
};
|
|
1403
|
+
};
|
|
1404
|
+
/** Public presence slice on the store. */
|
|
1405
|
+
interface PresenceSlice {
|
|
1406
|
+
/** Patch this client's presence; emits a 'presence' event + forwards via SyncAdapter. */
|
|
1407
|
+
setLocal(patch: PresencePatch): void;
|
|
1408
|
+
/** Current local presence. */
|
|
1409
|
+
getLocal(): PresenceState;
|
|
1410
|
+
/** Snapshot of a remote client's presence, or undefined. */
|
|
1411
|
+
get(clientId: ClientId): PresenceState | undefined;
|
|
1412
|
+
/** Snapshot of all remote presences. */
|
|
1413
|
+
getAll(): ReadonlyMap<ClientId, PresenceState>;
|
|
1414
|
+
/**
|
|
1415
|
+
* Adapter-facing: apply a remote client's presence patch. `state === null`
|
|
1416
|
+
* removes the remote client (they've left). Emits a 'presence' event so
|
|
1417
|
+
* consumers can update overlay UI.
|
|
1418
|
+
*
|
|
1419
|
+
* @internal — used by `attachSync`. Not meant for app code.
|
|
1420
|
+
*/
|
|
1421
|
+
applyRemote(clientId: ClientId, state: PresenceState | null): void;
|
|
1422
|
+
}
|
|
1423
|
+
type StoreEventName = keyof StoreEvents;
|
|
1424
|
+
type StoreEventHandler<E extends StoreEventName> = (payload: StoreEvents[E]) => void;
|
|
1425
|
+
type Unsubscribe = () => void;
|
|
1426
|
+
type SpatialQuery = {
|
|
1427
|
+
rect?: WorldRect;
|
|
1428
|
+
point?: Vec2;
|
|
1429
|
+
};
|
|
1430
|
+
type SpatialResult = {
|
|
1431
|
+
nodes: NodeId[];
|
|
1432
|
+
edges: EdgeId[];
|
|
1433
|
+
};
|
|
1434
|
+
/**
|
|
1435
|
+
* The single source of truth for one canvas. Mutations go through
|
|
1436
|
+
* typed ops (collab-ready + undoable), reads are imperative, and
|
|
1437
|
+
* change events drive React hooks + sync adapters.
|
|
1438
|
+
*
|
|
1439
|
+
* Created via `createCanvasStore(opts)`. See ARCHITECTURE.md §10.
|
|
1440
|
+
*/
|
|
1441
|
+
interface CanvasStore {
|
|
1442
|
+
/** Stable id for *this* client. Generated ids embed it. */
|
|
1443
|
+
readonly clientId: ClientId;
|
|
1444
|
+
/**
|
|
1445
|
+
* Mints a new globally-unique id. Embeds `clientId` so ids generated
|
|
1446
|
+
* concurrently across peers don't collide.
|
|
1447
|
+
*
|
|
1448
|
+
* @example
|
|
1449
|
+
* const id = asNodeId(store.generateId())
|
|
1450
|
+
*/
|
|
1451
|
+
generateId(): string;
|
|
1452
|
+
/**
|
|
1453
|
+
* Adds a node. Returns its id. If `node.style.autoFit !== false` and
|
|
1454
|
+
* `node.content` is set, height is grown to fit.
|
|
1455
|
+
*
|
|
1456
|
+
* @example
|
|
1457
|
+
* const id = store.addNode({
|
|
1458
|
+
* id: asNodeId(store.generateId()),
|
|
1459
|
+
* type: 'rect', x: 0, y: 0, w: 200, h: 100,
|
|
1460
|
+
* angle: 0, z: 0, groups: [],
|
|
1461
|
+
* })
|
|
1462
|
+
*/
|
|
1463
|
+
addNode(node: Node): NodeId;
|
|
1464
|
+
/**
|
|
1465
|
+
* Patches fields on an existing node. Captures the previous slice on
|
|
1466
|
+
* the op so undo is free. Autofit re-runs when `content` or font
|
|
1467
|
+
* style fields change.
|
|
1468
|
+
*
|
|
1469
|
+
* @example
|
|
1470
|
+
* store.updateNode(id, { x: 100, style: { backgroundColor: '#fef9c3' } })
|
|
1471
|
+
*/
|
|
1472
|
+
updateNode(id: NodeId, patch: Partial<Node>): void;
|
|
1473
|
+
/**
|
|
1474
|
+
* Removes a node and cascade-removes its incident edges in the same
|
|
1475
|
+
* batch (so one undo restores the node + every edge that pointed to it).
|
|
1476
|
+
*/
|
|
1477
|
+
removeNode(id: NodeId): void;
|
|
1478
|
+
/** Adds an edge. Returns its id. */
|
|
1479
|
+
addEdge(edge: Edge): EdgeId;
|
|
1480
|
+
/** Patches fields on an existing edge. */
|
|
1481
|
+
updateEdge(id: EdgeId, patch: Partial<Edge>): void;
|
|
1482
|
+
/** Removes an edge. */
|
|
1483
|
+
removeEdge(id: EdgeId): void;
|
|
1484
|
+
upsertGroup(group: Group): void;
|
|
1485
|
+
removeGroup(id: GroupId): void;
|
|
1486
|
+
/**
|
|
1487
|
+
* Z-order: bring the given nodes/edges to the top of the paint
|
|
1488
|
+
* order (and the top of the hit-test priority for overlapping
|
|
1489
|
+
* shapes). Mixed node/edge selections are supported. Each entity
|
|
1490
|
+
* gets a fresh top-of-stack z; relative order among the targets
|
|
1491
|
+
* is preserved (first-passed stays on top).
|
|
1492
|
+
*
|
|
1493
|
+
* Batched as one OpBatch — single undo step.
|
|
1494
|
+
*/
|
|
1495
|
+
bringToFront(ids: (NodeId | EdgeId)[]): void;
|
|
1496
|
+
/** Symmetric: send to the bottom of the stack. */
|
|
1497
|
+
sendToBack(ids: (NodeId | EdgeId)[]): void;
|
|
1498
|
+
/** Step each target above the next-higher non-target sibling. */
|
|
1499
|
+
bringForward(ids: (NodeId | EdgeId)[]): void;
|
|
1500
|
+
/** Step each target below the next-lower non-target sibling. */
|
|
1501
|
+
sendBackward(ids: (NodeId | EdgeId)[]): void;
|
|
1502
|
+
/**
|
|
1503
|
+
* Collapses every mutation inside `fn` into a single `OpBatch` — one
|
|
1504
|
+
* undo step, one change event, one sync send.
|
|
1505
|
+
*
|
|
1506
|
+
* @example
|
|
1507
|
+
* store.batch(() => {
|
|
1508
|
+
* for (const id of selection) store.removeNode(id as NodeId)
|
|
1509
|
+
* })
|
|
1510
|
+
*/
|
|
1511
|
+
batch(fn: () => void): void;
|
|
1512
|
+
/**
|
|
1513
|
+
* Low-level op application — usually called by sync adapters or
|
|
1514
|
+
* tool-use AI agents that generate ops directly.
|
|
1515
|
+
* `opts.origin` defaults to `'local'`. Remote/history origins skip
|
|
1516
|
+
* the undo stack.
|
|
1517
|
+
*
|
|
1518
|
+
* @example
|
|
1519
|
+
* // Apply an AI-generated op:
|
|
1520
|
+
* store.applyOp({ type: 'node.add', node: aiNode })
|
|
1521
|
+
*/
|
|
1522
|
+
applyOp(op: Op, opts?: {
|
|
1523
|
+
origin?: OpOrigin;
|
|
1524
|
+
}): void;
|
|
1525
|
+
/** Apply an entire batch (origin lives on the batch). */
|
|
1526
|
+
applyBatch(batch: OpBatch): void;
|
|
1527
|
+
/** True when there's something to undo. */
|
|
1528
|
+
canUndo(): boolean;
|
|
1529
|
+
/** True when there's something to redo. */
|
|
1530
|
+
canRedo(): boolean;
|
|
1531
|
+
/**
|
|
1532
|
+
* Pops the most recent local batch and applies its inverse. Returns
|
|
1533
|
+
* true if anything was undone.
|
|
1534
|
+
*
|
|
1535
|
+
* @example
|
|
1536
|
+
* if (e.metaKey && e.key === 'z') store.undo()
|
|
1537
|
+
*/
|
|
1538
|
+
undo(): boolean;
|
|
1539
|
+
/** Re-applies a previously-undone batch. */
|
|
1540
|
+
redo(): boolean;
|
|
1541
|
+
/** Drops both undo and redo stacks. Call after `fromJSON` / scene reset. */
|
|
1542
|
+
clearHistory(): void;
|
|
1543
|
+
/**
|
|
1544
|
+
* Per-client *synced* state — cursor / selection / editing / color /
|
|
1545
|
+
* name. Distinct from `getInteractionState()` (which is local-only).
|
|
1546
|
+
*
|
|
1547
|
+
* @example
|
|
1548
|
+
* store.presence.setLocal({ name: 'Alice', color: '#ef4444' })
|
|
1549
|
+
*/
|
|
1550
|
+
presence: PresenceSlice;
|
|
1551
|
+
/** O(1) lookup; undefined if not found or removed. */
|
|
1552
|
+
getNode(id: NodeId): Node | undefined;
|
|
1553
|
+
/** O(1) lookup. */
|
|
1554
|
+
getEdge(id: EdgeId): Edge | undefined;
|
|
1555
|
+
/** O(1) lookup. */
|
|
1556
|
+
getGroup(id: GroupId): Group | undefined;
|
|
1557
|
+
/** O(n) — materializes the full list. Use sparingly. */
|
|
1558
|
+
getAllNodes(): Node[];
|
|
1559
|
+
/** O(n) — materializes the full list. */
|
|
1560
|
+
getAllEdges(): Edge[];
|
|
1561
|
+
/** O(n) — materializes the full list. */
|
|
1562
|
+
getAllGroups(): Group[];
|
|
1563
|
+
/** O(1) count without materializing the list. */
|
|
1564
|
+
getNodeCount(): number;
|
|
1565
|
+
/** O(1) count. */
|
|
1566
|
+
getEdgeCount(): number;
|
|
1567
|
+
/** O(1) count. */
|
|
1568
|
+
getGroupCount(): number;
|
|
1569
|
+
/**
|
|
1570
|
+
* Spatial query — ids of nodes + edges that intersect a rect or
|
|
1571
|
+
* contain a point. Backed by a uniform grid for sub-millisecond
|
|
1572
|
+
* queries at 10k+ entities.
|
|
1573
|
+
*
|
|
1574
|
+
* @example
|
|
1575
|
+
* const visible = store.querySpatial({ rect: viewport })
|
|
1576
|
+
*/
|
|
1577
|
+
querySpatial(q: SpatialQuery): SpatialResult;
|
|
1578
|
+
/**
|
|
1579
|
+
* Cached edge geometry — sample polyline, AABB, attached-node ids,
|
|
1580
|
+
* self-loop flag. Lazily recomputed when any input changes.
|
|
1581
|
+
*/
|
|
1582
|
+
getEdgeGeometry(id: EdgeId): EdgeGeometry | undefined;
|
|
1583
|
+
/**
|
|
1584
|
+
* Ids of every edge attached to this node (either endpoint). O(1) —
|
|
1585
|
+
* maintained as an inverted index internally.
|
|
1586
|
+
*/
|
|
1587
|
+
getIncidentEdges(id: NodeId): EdgeId[];
|
|
1588
|
+
/**
|
|
1589
|
+
* The registered `NodeTypeDef` for a type id, or `undefined` for
|
|
1590
|
+
* built-in shapes. See `defineNode`.
|
|
1591
|
+
*/
|
|
1592
|
+
getNodeTypeDef(type: string): NodeTypeDef | undefined;
|
|
1593
|
+
getCamera(): CameraState;
|
|
1594
|
+
/**
|
|
1595
|
+
* Set camera fields (partial patch). Clamped to legal zoom range.
|
|
1596
|
+
*
|
|
1597
|
+
* @example
|
|
1598
|
+
* store.setCamera({ z: 1.5 })
|
|
1599
|
+
*/
|
|
1600
|
+
setCamera(patch: Partial<CameraState>): void;
|
|
1601
|
+
getSelection(): (NodeId | EdgeId)[];
|
|
1602
|
+
/** Replace the selection. Pass `[]` to deselect everything. */
|
|
1603
|
+
setSelection(ids: (NodeId | EdgeId)[]): void;
|
|
1604
|
+
/** Current interaction state — mode, drag delta, marquee rect, etc. */
|
|
1605
|
+
getInteractionState(): InteractionState;
|
|
1606
|
+
/** Patch fields on interaction state. Emits a 'interaction' event. */
|
|
1607
|
+
setInteractionState(patch: Partial<InteractionState>): void;
|
|
1608
|
+
/** Reset to idle (clears drag / marquee / draft edge / edit mode). */
|
|
1609
|
+
resetInteractionState(): void;
|
|
1610
|
+
/**
|
|
1611
|
+
* Enter edit mode for `id`. Polymorphic — `id` may be a {@link NodeId}
|
|
1612
|
+
* (to edit `node.content`) or an {@link EdgeId} (to edit
|
|
1613
|
+
* `edge.content`, the edge label). Flips interaction mode to
|
|
1614
|
+
* `'editing'`; the library's `EditorMount` picks up the change and
|
|
1615
|
+
* mounts the configured editor adapter at the right anchor.
|
|
1616
|
+
*
|
|
1617
|
+
* @example
|
|
1618
|
+
* // Double-click a node body or an edge label to edit:
|
|
1619
|
+
* onDoubleClick={e => {
|
|
1620
|
+
* const hit = hitTestAny(store, e.world, camera.z)
|
|
1621
|
+
* if (hit?.kind === 'body' && 'nodeId' in hit) store.beginEdit(hit.nodeId)
|
|
1622
|
+
* if (hit?.kind === 'label') store.beginEdit(hit.edgeId)
|
|
1623
|
+
* }}
|
|
1624
|
+
*/
|
|
1625
|
+
beginEdit(id: NodeId | EdgeId): void;
|
|
1626
|
+
/**
|
|
1627
|
+
* Write the new content + apply autofit (if opted in) + exit edit
|
|
1628
|
+
* mode. No-op when not editing.
|
|
1629
|
+
*/
|
|
1630
|
+
commitEdit(content: string): void;
|
|
1631
|
+
/** Exit edit mode without writing content. */
|
|
1632
|
+
cancelEdit(): void;
|
|
1633
|
+
/**
|
|
1634
|
+
* Subscribe to a store event. Returns an unsubscribe function.
|
|
1635
|
+
* Events fire synchronously; subscribers must not throw.
|
|
1636
|
+
*
|
|
1637
|
+
* Events:
|
|
1638
|
+
* - `'change'` — any committed batch (local / remote / history)
|
|
1639
|
+
* - `'camera'` — pan or zoom
|
|
1640
|
+
* - `'selection'` — selection change
|
|
1641
|
+
* - `'interaction'` — interaction state change (frequent during drag)
|
|
1642
|
+
* - `'presence'` — local or remote presence update
|
|
1643
|
+
* - `'conflict'` — LWW conflict detected when applying a remote batch
|
|
1644
|
+
*
|
|
1645
|
+
* @example
|
|
1646
|
+
* const unsub = store.subscribe('change', batch => save(batch))
|
|
1647
|
+
*/
|
|
1648
|
+
subscribe<E extends StoreEventName>(event: E, cb: StoreEventHandler<E>): Unsubscribe;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
declare const createCanvasStore: (opts?: StoreOptions) => CanvasStore;
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* Op inversion — see ARCHITECTURE.md §10.2.
|
|
1655
|
+
*
|
|
1656
|
+
* Every committed `Op` carries enough state (full snapshot for add/remove,
|
|
1657
|
+
* `prev` slice for updates) to derive its inverse with no diffing. Undo
|
|
1658
|
+
* applies `inverseBatch(batch)` with `origin: 'history'`; redo re-applies
|
|
1659
|
+
* the original batch.
|
|
1660
|
+
*
|
|
1661
|
+
* Inverse rules:
|
|
1662
|
+
* - add ↔ remove (full snapshot retained)
|
|
1663
|
+
* - update.patch ↔ update.prev
|
|
1664
|
+
* - group.upsert with `prev` ↔ group.upsert with prev fields swapped; if
|
|
1665
|
+
* `prev` is absent, the upsert was a fresh insert → invert to remove.
|
|
1666
|
+
*/
|
|
1667
|
+
|
|
1668
|
+
declare const inverseOp: (op: Op) => Op;
|
|
1669
|
+
/**
|
|
1670
|
+
* Inverse batch: reverse op order, invert each op. Reversing preserves
|
|
1671
|
+
* "later ops depended on earlier ones" semantics — e.g. a batch that
|
|
1672
|
+
* (1) adds a node and (2) updates it must undo (2) first, then (1).
|
|
1673
|
+
*/
|
|
1674
|
+
declare const inverseBatch: (batch: OpBatch) => Op[];
|
|
1675
|
+
|
|
1676
|
+
/**
|
|
1677
|
+
* LWW conflict detection — see ARCHITECTURE.md §10.6.
|
|
1678
|
+
*
|
|
1679
|
+
* For remote `node.update` / `edge.update` ops, the `prev` slice
|
|
1680
|
+
* captures what the *remote* client thought the value was before its
|
|
1681
|
+
* change. If our local current value differs, two clients touched the
|
|
1682
|
+
* same field concurrently: LWW says higher `batch.ts` wins (we still
|
|
1683
|
+
* apply the remote op), but we surface a 'conflict' event for
|
|
1684
|
+
* telemetry / consumer UX (e.g. "your background color was just
|
|
1685
|
+
* overwritten by Alice").
|
|
1686
|
+
*/
|
|
1687
|
+
type ConflictRecord = {
|
|
1688
|
+
op: Op;
|
|
1689
|
+
field: string;
|
|
1690
|
+
};
|
|
1691
|
+
type GetCurrentNode = (id: Node['id']) => Node | undefined;
|
|
1692
|
+
type GetCurrentEdge = (id: Edge['id']) => Edge | undefined;
|
|
1693
|
+
/**
|
|
1694
|
+
* Walks a remote OpBatch and returns the set of `(op, field)` pairs
|
|
1695
|
+
* where the local current value disagrees with the op's `prev` slice.
|
|
1696
|
+
* No state is mutated.
|
|
1697
|
+
*/
|
|
1698
|
+
declare const detectConflicts: (batch: OpBatch, getNode: GetCurrentNode, getEdge: GetCurrentEdge) => ConflictRecord[];
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* SyncAdapter — see ARCHITECTURE.md §10.6.
|
|
1702
|
+
*
|
|
1703
|
+
* Pluggable transport contract for collab. The library never ships a
|
|
1704
|
+
* concrete adapter (transport is consumer territory: WebSocket, Yjs,
|
|
1705
|
+
* Automerge, BroadcastChannel, …). v1 ships:
|
|
1706
|
+
*
|
|
1707
|
+
* - This interface
|
|
1708
|
+
* - `attachSync(store, adapter)` — wires local commits → adapter and
|
|
1709
|
+
* remote batches → store with `origin: 'remote'`
|
|
1710
|
+
* - A separate package `@canvas-harness/sync-broadcast` providing a
|
|
1711
|
+
* BroadcastChannel-backed adapter for single-machine demos
|
|
1712
|
+
*
|
|
1713
|
+
* **v1 sync is experimental.** Conflict semantics assume causally-ordered
|
|
1714
|
+
* op delivery from the adapter. Adapters without causal ordering must
|
|
1715
|
+
* advertise `capabilities.crdt: true` and own merge themselves.
|
|
1716
|
+
*/
|
|
1717
|
+
type SyncAdapterCapabilities = {
|
|
1718
|
+
/**
|
|
1719
|
+
* Adapter guarantees ops arrive in the same causal order all clients
|
|
1720
|
+
* see. Required for the default LWW path.
|
|
1721
|
+
*/
|
|
1722
|
+
causalOrdering?: boolean;
|
|
1723
|
+
/**
|
|
1724
|
+
* Adapter merges via CRDT (Yjs / Automerge / ...). Skips library-side
|
|
1725
|
+
* LWW because the adapter has already resolved conflicts.
|
|
1726
|
+
*/
|
|
1727
|
+
crdt?: boolean;
|
|
1728
|
+
};
|
|
1729
|
+
/**
|
|
1730
|
+
* Pluggable collab transport. Implementations forward op batches +
|
|
1731
|
+
* presence patches between peers. The library is transport-agnostic;
|
|
1732
|
+
* see `@canvas-harness/sync-broadcast` for a reference adapter using
|
|
1733
|
+
* `BroadcastChannel`.
|
|
1734
|
+
*
|
|
1735
|
+
* Authors typically wrap a WebSocket / Yjs / Automerge instance.
|
|
1736
|
+
*
|
|
1737
|
+
* @example
|
|
1738
|
+
* const myAdapter: SyncAdapter = {
|
|
1739
|
+
* capabilities: { causalOrdering: true },
|
|
1740
|
+
* sendBatch: b => ws.send(JSON.stringify({ kind: 'op', batch: b })),
|
|
1741
|
+
* sendPresence: p => ws.send(JSON.stringify({ kind: 'presence', patch: p })),
|
|
1742
|
+
* onBatch(cb) {
|
|
1743
|
+
* const h = (e: MessageEvent) => { const m = JSON.parse(e.data); if (m.kind === 'op') cb(m.batch) }
|
|
1744
|
+
* ws.addEventListener('message', h)
|
|
1745
|
+
* return () => ws.removeEventListener('message', h)
|
|
1746
|
+
* },
|
|
1747
|
+
* onPresence(cb) { … },
|
|
1748
|
+
* destroy() { ws.close() },
|
|
1749
|
+
* }
|
|
1750
|
+
* const detach = attachSync(store, myAdapter)
|
|
1751
|
+
*/
|
|
1752
|
+
type SyncAdapter = {
|
|
1753
|
+
capabilities: SyncAdapterCapabilities;
|
|
1754
|
+
/** Send a locally-committed (or history) batch to peers. */
|
|
1755
|
+
sendBatch(batch: OpBatch): void;
|
|
1756
|
+
/** Send a local presence patch to peers. */
|
|
1757
|
+
sendPresence(patch: PresencePatch): void;
|
|
1758
|
+
/** Receive remote batches. Subscription persists until `destroy()`. */
|
|
1759
|
+
onBatch(cb: (batch: OpBatch) => void): Unsubscribe;
|
|
1760
|
+
/**
|
|
1761
|
+
* Receive remote presence patches. `state === null` means the remote
|
|
1762
|
+
* client has left and should be removed from the presence map.
|
|
1763
|
+
*/
|
|
1764
|
+
onPresence(cb: (clientId: ClientId, state: PresenceState | null) => void): Unsubscribe;
|
|
1765
|
+
/** Optional teardown — closes sockets, clears buffers, etc. */
|
|
1766
|
+
destroy?(): void;
|
|
1767
|
+
};
|
|
1768
|
+
/**
|
|
1769
|
+
* Wires a {@link SyncAdapter} to a {@link CanvasStore}. Returns a
|
|
1770
|
+
* `detach()` function that disconnects everything (including the
|
|
1771
|
+
* adapter's own `destroy()`).
|
|
1772
|
+
*
|
|
1773
|
+
* Throws if the adapter advertises neither `causalOrdering` nor
|
|
1774
|
+
* `crdt` — the default LWW path requires causal order.
|
|
1775
|
+
*
|
|
1776
|
+
* After attach:
|
|
1777
|
+
* - Local + history batches forward to peers via `adapter.sendBatch`.
|
|
1778
|
+
* - Local presence updates forward via `adapter.sendPresence`.
|
|
1779
|
+
* - Remote batches apply to the store with `origin: 'remote'`
|
|
1780
|
+
* (don't enter undo stack; conflict event fires on `prev` mismatch).
|
|
1781
|
+
* - Remote presence updates merge into `store.presence`.
|
|
1782
|
+
*
|
|
1783
|
+
* @example
|
|
1784
|
+
* import { createBroadcastSyncAdapter } from '@canvas-harness/sync-broadcast'
|
|
1785
|
+
* const adapter = createBroadcastSyncAdapter({
|
|
1786
|
+
* channelName: 'my-board',
|
|
1787
|
+
* clientId: store.clientId,
|
|
1788
|
+
* })
|
|
1789
|
+
* const detach = attachSync(store, adapter)
|
|
1790
|
+
* // ...later, on unmount:
|
|
1791
|
+
* detach()
|
|
1792
|
+
*/
|
|
1793
|
+
declare const attachSync: (store: CanvasStore, adapter: SyncAdapter) => Unsubscribe;
|
|
1794
|
+
|
|
1795
|
+
/**
|
|
1796
|
+
* Palm-rejection helper — see IMPLEMENTATION.md Phase 11.
|
|
1797
|
+
*
|
|
1798
|
+
* When a stylus is actively touching the surface, mobile/tablet OSes
|
|
1799
|
+
* sometimes mis-route palm contacts as `pointerType: 'touch'`. The
|
|
1800
|
+
* heuristic: when a pen pointer is down (or has just lifted), drop all
|
|
1801
|
+
* incoming `touch` pointers for a short grace period.
|
|
1802
|
+
*
|
|
1803
|
+
* Pure state holder + helpers. The `<Canvas>` gesture hooks call
|
|
1804
|
+
* `notePenActive` / `notePenInactive` on pen pointer events, and
|
|
1805
|
+
* `shouldRejectTouch` before processing a touch event.
|
|
1806
|
+
*/
|
|
1807
|
+
type PalmRejectionState = {
|
|
1808
|
+
/** True while at least one pen pointer is currently down. */
|
|
1809
|
+
penActive: boolean;
|
|
1810
|
+
/** Timestamp (ms since epoch) at which the most recent pen pointer lifted. */
|
|
1811
|
+
lastPenUpAt: number;
|
|
1812
|
+
};
|
|
1813
|
+
declare const PALM_REJECTION_GRACE_MS = 300;
|
|
1814
|
+
declare const createPalmRejectionState: () => PalmRejectionState;
|
|
1815
|
+
declare const notePenActive: (state: PalmRejectionState) => void;
|
|
1816
|
+
declare const notePenInactive: (state: PalmRejectionState, now: number) => void;
|
|
1817
|
+
/**
|
|
1818
|
+
* Returns true if this touch event should be ignored because a pen is
|
|
1819
|
+
* active (or just lifted within the grace window).
|
|
1820
|
+
*/
|
|
1821
|
+
declare const shouldRejectTouch: (state: PalmRejectionState, now: number) => boolean;
|
|
1822
|
+
|
|
1823
|
+
type Migrator = (raw: unknown) => unknown;
|
|
1824
|
+
/**
|
|
1825
|
+
* Register a migrator that runs when loading data at version `fromVersion`.
|
|
1826
|
+
* The migrator should return data shaped for `fromVersion + 1`.
|
|
1827
|
+
*/
|
|
1828
|
+
declare const registerMigrator: (fromVersion: number, fn: Migrator) => void;
|
|
1829
|
+
/**
|
|
1830
|
+
* Serializes a scene to its wire form.
|
|
1831
|
+
*/
|
|
1832
|
+
declare const toSerialized: (scene: Scene) => SerializedScene;
|
|
1833
|
+
/**
|
|
1834
|
+
* Deserializes from wire form into the in-memory Scene shape.
|
|
1835
|
+
* Runs migrators if the version is older than current.
|
|
1836
|
+
*/
|
|
1837
|
+
declare const fromSerialized: (raw: SerializedScene | unknown) => Scene;
|
|
1838
|
+
/**
|
|
1839
|
+
* Convenience: dump a store's current state to wire form.
|
|
1840
|
+
*/
|
|
1841
|
+
declare const storeToJSON: (store: CanvasStore) => SerializedScene;
|
|
1842
|
+
|
|
1843
|
+
type CanvasSurface = {
|
|
1844
|
+
canvas: HTMLCanvasElement;
|
|
1845
|
+
ctx: CanvasRenderingContext2D;
|
|
1846
|
+
/** Logical CSS pixels (the size you set in JS / read with getBoundingClientRect). */
|
|
1847
|
+
cssWidth: number;
|
|
1848
|
+
cssHeight: number;
|
|
1849
|
+
/** Device pixels — backing-store size. */
|
|
1850
|
+
dpr: number;
|
|
1851
|
+
};
|
|
1852
|
+
declare const getDpr: () => number;
|
|
1853
|
+
/**
|
|
1854
|
+
* Builds a managed canvas surface. Caller pins the canvas element; we size
|
|
1855
|
+
* it and reset the 2d context's transform to logical-pixel space.
|
|
1856
|
+
*
|
|
1857
|
+
* Subsequent calls to `setSize` re-allocate the backing store if the new
|
|
1858
|
+
* `cssW × cssH × DPR` differs from the current.
|
|
1859
|
+
*/
|
|
1860
|
+
declare const setupSurface: (canvas: HTMLCanvasElement) => CanvasSurface;
|
|
1861
|
+
/**
|
|
1862
|
+
* Resizes the surface to a new CSS-pixel size, picking up the current DPR.
|
|
1863
|
+
* Returns true if anything changed (caller should redraw).
|
|
1864
|
+
*/
|
|
1865
|
+
declare const sizeSurface: (surface: CanvasSurface, cssW: number, cssH: number) => boolean;
|
|
1866
|
+
/**
|
|
1867
|
+
* Clears the entire backing store. Call before setting the camera transform.
|
|
1868
|
+
*/
|
|
1869
|
+
declare const clearSurface: (surface: CanvasSurface) => void;
|
|
1870
|
+
|
|
1871
|
+
/**
|
|
1872
|
+
* rAF-driven frame loop — see ARCHITECTURE.md §4.3.
|
|
1873
|
+
*
|
|
1874
|
+
* The renderer marks layers dirty via `requestFrame()`; the loop coalesces
|
|
1875
|
+
* multiple requests into one paint per rAF tick. Idle frames cost nothing
|
|
1876
|
+
* because we only schedule when something is dirty.
|
|
1877
|
+
*
|
|
1878
|
+
* Per-frame timing is captured so consumers can drive perf overlays.
|
|
1879
|
+
*/
|
|
1880
|
+
type FrameStats = {
|
|
1881
|
+
/** Most recent frame duration in ms (drawFn + overhead). */
|
|
1882
|
+
lastMs: number;
|
|
1883
|
+
/** Running average over the last `historySize` frames. */
|
|
1884
|
+
avgMs: number;
|
|
1885
|
+
/** Number of frames painted since start. */
|
|
1886
|
+
frames: number;
|
|
1887
|
+
/** Frames drawn in the last 1000ms (FPS measurement). */
|
|
1888
|
+
fps: number;
|
|
1889
|
+
};
|
|
1890
|
+
type FrameLoop = {
|
|
1891
|
+
start(): void;
|
|
1892
|
+
stop(): void;
|
|
1893
|
+
requestFrame(): void;
|
|
1894
|
+
stats(): FrameStats;
|
|
1895
|
+
};
|
|
1896
|
+
type Opts = {
|
|
1897
|
+
draw: () => void;
|
|
1898
|
+
historySize?: number;
|
|
1899
|
+
};
|
|
1900
|
+
declare const createFrameLoop: ({ draw, historySize }: Opts) => FrameLoop;
|
|
1901
|
+
|
|
1902
|
+
/**
|
|
1903
|
+
* Generic primitive paint pass.
|
|
1904
|
+
*
|
|
1905
|
+
* The renderer applies camera + node transforms (translate to node center,
|
|
1906
|
+
* rotate, translate back to top-left) before calling this. The drawer
|
|
1907
|
+
* just builds a local-space path and fills/strokes it with resolved style.
|
|
1908
|
+
*
|
|
1909
|
+
* `scale` is the current camera × DPR factor (world-units → device-pixels).
|
|
1910
|
+
* Callers compute it once per frame and pass it in so each drawShape call
|
|
1911
|
+
* doesn't allocate a DOMMatrix via ctx.getTransform().
|
|
1912
|
+
*/
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* Atomic single-path primitives. Composites paint multiple of these.
|
|
1916
|
+
* Thought-cloud is atomic — its union geometry is one continuous path
|
|
1917
|
+
* so the rough wobble unifies the silhouette.
|
|
1918
|
+
*/
|
|
1919
|
+
type AtomicPrimitive = 'rect' | 'ellipse' | 'diamond' | 'tag' | 'thought-cloud';
|
|
1920
|
+
/**
|
|
1921
|
+
* Composite primitives built from multiple atomic sub-shapes. Capsule
|
|
1922
|
+
* is intentionally composite — the visible seam between the accent
|
|
1923
|
+
* circle and the rect body reads as two stacked hand-drawn shapes
|
|
1924
|
+
* (medicine-pill aesthetic), which we want to keep.
|
|
1925
|
+
*/
|
|
1926
|
+
type CompositePrimitive = 'capsule' | 'layered-rect' | 'layered-ellipse' | 'layered-diamond';
|
|
1927
|
+
type PrimitiveType = AtomicPrimitive | CompositePrimitive;
|
|
1928
|
+
/** Returns true if `node.type` is one of the built-ins drawShape can render. */
|
|
1929
|
+
declare const isDrawablePrimitive: (type: string) => type is PrimitiveType;
|
|
1930
|
+
declare const drawShape: (ctx: CanvasRenderingContext2D, node: Node, scale: number, theme?: ThemeResolver, opts?: {
|
|
1931
|
+
skipStroke?: boolean;
|
|
1932
|
+
}) => void;
|
|
1933
|
+
|
|
1934
|
+
type RendererOptions = {
|
|
1935
|
+
store: CanvasStore;
|
|
1936
|
+
staticCanvas: HTMLCanvasElement;
|
|
1937
|
+
interactiveCanvas: HTMLCanvasElement;
|
|
1938
|
+
theme?: ThemeResolver;
|
|
1939
|
+
/** Initial CSS-pixel size. Use `setSize()` to update on resize. */
|
|
1940
|
+
width: number;
|
|
1941
|
+
height: number;
|
|
1942
|
+
/**
|
|
1943
|
+
* Optional page background + dot/grid pattern. Local-only (not in
|
|
1944
|
+
* the synced scene). Update at runtime via `Renderer.setBackground`.
|
|
1945
|
+
*/
|
|
1946
|
+
background?: CanvasBackground;
|
|
1947
|
+
/**
|
|
1948
|
+
* Fires when the set of custom nodes that should be rendered in the DOM
|
|
1949
|
+
* overlay changes. Consumers use this to mount/unmount React subtrees
|
|
1950
|
+
* (or whatever framework). See ARCHITECTURE.md §5.2 lifecycle.
|
|
1951
|
+
*
|
|
1952
|
+
* The callback receives the FULL current set, not a delta — consumers
|
|
1953
|
+
* compute the mount/unmount diff themselves.
|
|
1954
|
+
*/
|
|
1955
|
+
onOverlayChange?: (mountedIds: NodeId[]) => void;
|
|
1956
|
+
};
|
|
1957
|
+
type Renderer = {
|
|
1958
|
+
/** Begin the rAF loop. Idempotent. */
|
|
1959
|
+
start(): void;
|
|
1960
|
+
/** Stop the rAF loop. Idempotent. */
|
|
1961
|
+
stop(): void;
|
|
1962
|
+
/** Force a static repaint on the next rAF tick. */
|
|
1963
|
+
invalidate(): void;
|
|
1964
|
+
/** Resize both canvases to a new CSS-pixel viewport. */
|
|
1965
|
+
setSize(cssW: number, cssH: number): void;
|
|
1966
|
+
/** Update the page background / pattern. Triggers a static repaint. */
|
|
1967
|
+
setBackground(bg: CanvasBackground | undefined): void;
|
|
1968
|
+
/** Per-frame timing (FPS, lastMs, avgMs, frames). */
|
|
1969
|
+
stats(): FrameStats;
|
|
1970
|
+
/** Number of items the most recent paint actually drew. */
|
|
1971
|
+
lastDrawCount(): number;
|
|
1972
|
+
/** Current overlay-mounted custom-node ids. */
|
|
1973
|
+
getOverlaySet(): NodeId[];
|
|
1974
|
+
/** Detach event listeners. The store is left untouched. */
|
|
1975
|
+
dispose(): void;
|
|
1976
|
+
};
|
|
1977
|
+
declare const createRenderer: (opts: RendererOptions) => Renderer;
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
* Page background + optional infinite dot / grid pattern.
|
|
1981
|
+
*
|
|
1982
|
+
* Called inside `paintStatic` after the camera transform is applied,
|
|
1983
|
+
* before nodes. Dots / grid lines are drawn in world coordinates so
|
|
1984
|
+
* they anchor to the world origin — panning moves the user *through*
|
|
1985
|
+
* the pattern, zooming changes visual density.
|
|
1986
|
+
*
|
|
1987
|
+
* LOD: when `gap × zoom` drops below `MIN_PATTERN_SCREEN_PX`, the
|
|
1988
|
+
* effective gap doubles in octaves so the pattern stays roughly the
|
|
1989
|
+
* same on-screen density. Below `MIN_VISIBLE_PATTERN_PX` the pattern
|
|
1990
|
+
* is omitted entirely (sub-pixel would be unreadable mush + waste).
|
|
1991
|
+
*/
|
|
1992
|
+
type PaintBackgroundOptions = {
|
|
1993
|
+
/** Visible world rect (after viewport-overscan). */
|
|
1994
|
+
viewport: WorldRect;
|
|
1995
|
+
/** camera.z — used for LOD octave selection + screen-px conversion. */
|
|
1996
|
+
zoom: number;
|
|
1997
|
+
background?: CanvasBackground;
|
|
1998
|
+
};
|
|
1999
|
+
declare const paintBackground: (ctx: CanvasRenderingContext2D, opts: PaintBackgroundOptions) => void;
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* Minimap rendering — see IMPROVEMENTS.md (UX) and the React layer's
|
|
2003
|
+
* `<Minimap />` component.
|
|
2004
|
+
*
|
|
2005
|
+
* Splits into two paths consumers can compose independently:
|
|
2006
|
+
*
|
|
2007
|
+
* - `renderMinimapContent(ctx, store, ...)` — paints every node as a
|
|
2008
|
+
* plain `fillRect` at its scaled AABB. Edge bodies are skipped
|
|
2009
|
+
* (not useful at this scale and would multiply cost). Run on
|
|
2010
|
+
* committed scene change ONLY; cache the result as an
|
|
2011
|
+
* OffscreenCanvas/HTMLCanvasElement and blit.
|
|
2012
|
+
*
|
|
2013
|
+
* - `drawMinimapViewport(ctx, camera, sceneBounds, mapSize)` — paints
|
|
2014
|
+
* a tiny rectangle showing the visible viewport. Cheap; run on
|
|
2015
|
+
* every camera change.
|
|
2016
|
+
*
|
|
2017
|
+
* Cost model:
|
|
2018
|
+
* - content render: O(N) — only fires on committed mutations.
|
|
2019
|
+
* - viewport overlay: O(1) per frame.
|
|
2020
|
+
*
|
|
2021
|
+
* Hard cap (`maxNodes`) — above which the content render skips
|
|
2022
|
+
* entirely and the consumer is expected to show a "minimap disabled"
|
|
2023
|
+
* placeholder. Default 5000.
|
|
2024
|
+
*/
|
|
2025
|
+
declare const DEFAULT_MINIMAP_MAX_NODES = 5000;
|
|
2026
|
+
type MinimapContentOptions = {
|
|
2027
|
+
/** Hard upper bound on node count; above this, content is skipped. */
|
|
2028
|
+
maxNodes?: number;
|
|
2029
|
+
/** Override fill color for nodes (used when node has no style.backgroundColor). */
|
|
2030
|
+
defaultNodeColor?: string;
|
|
2031
|
+
/** Background color drawn first inside the minimap rect. */
|
|
2032
|
+
backgroundColor?: string;
|
|
2033
|
+
};
|
|
2034
|
+
/**
|
|
2035
|
+
* Returns the world-space bounding rect that encloses every visible
|
|
2036
|
+
* node, or `null` if the scene is empty. Used to scale the minimap so
|
|
2037
|
+
* the entire scene fits inside it.
|
|
2038
|
+
*/
|
|
2039
|
+
declare const sceneBounds: (store: CanvasStore) => WorldRect | null;
|
|
2040
|
+
/**
|
|
2041
|
+
* Paints the scene's nodes into the minimap canvas's logical pixel
|
|
2042
|
+
* space. Caller has already cleared the target. Returns true on
|
|
2043
|
+
* success, false when skipped (empty scene or over the node cap).
|
|
2044
|
+
*/
|
|
2045
|
+
declare const renderMinimapContent: (ctx: CanvasRenderingContext2D, store: CanvasStore, mapWidth: number, mapHeight: number, opts?: MinimapContentOptions) => boolean;
|
|
2046
|
+
/**
|
|
2047
|
+
* Paints the camera viewport rectangle on top of the cached minimap
|
|
2048
|
+
* content. Cheap; consumers call this on every camera tick.
|
|
2049
|
+
*
|
|
2050
|
+
* `sceneRect` should be the same bounds used by `renderMinimapContent`
|
|
2051
|
+
* for the current cache. `viewportWorld` is the visible world rect
|
|
2052
|
+
* (caller derives from camera + screen size).
|
|
2053
|
+
*/
|
|
2054
|
+
declare const drawMinimapViewport: (ctx: CanvasRenderingContext2D, viewportWorld: WorldRect, sceneRect: WorldRect, mapWidth: number, mapHeight: number, color?: string) => void;
|
|
2055
|
+
/**
|
|
2056
|
+
* Inverse mapping for click-to-pan: a screen point inside the minimap
|
|
2057
|
+
* rect → the world point it corresponds to. Returns null when the
|
|
2058
|
+
* scene is empty (no bounds to scale against).
|
|
2059
|
+
*/
|
|
2060
|
+
declare const minimapScreenToWorld: (store: CanvasStore, screenX: number, screenY: number, mapWidth: number, mapHeight: number) => {
|
|
2061
|
+
x: number;
|
|
2062
|
+
y: number;
|
|
2063
|
+
} | null;
|
|
2064
|
+
/**
|
|
2065
|
+
* World-space viewport rect from the camera + a screen size. Pass to
|
|
2066
|
+
* `drawMinimapViewport`. Caller's responsibility to supply the
|
|
2067
|
+
* canvas/CSS pixel dimensions.
|
|
2068
|
+
*/
|
|
2069
|
+
declare const worldViewportFromCamera: (camera: CameraState, screenW: number, screenH: number) => WorldRect;
|
|
2070
|
+
|
|
2071
|
+
/**
|
|
2072
|
+
* Sets the 2d transform so subsequent draw calls take world coords.
|
|
2073
|
+
*
|
|
2074
|
+
* screen.x = (world.x - camera.x) * camera.z * dpr
|
|
2075
|
+
* screen.y = (world.y - camera.y) * camera.z * dpr
|
|
2076
|
+
*/
|
|
2077
|
+
declare const applyCameraTransform: (surface: CanvasSurface, camera: CameraState) => void;
|
|
2078
|
+
/**
|
|
2079
|
+
* The world rect currently visible inside the surface.
|
|
2080
|
+
*/
|
|
2081
|
+
declare const worldViewport: (surface: CanvasSurface, camera: CameraState) => WorldRect;
|
|
2082
|
+
/**
|
|
2083
|
+
* Wraps a draw callback in the local-frame transform for one node:
|
|
2084
|
+
* translates to the node's center, rotates by node.angle, then translates
|
|
2085
|
+
* back to the node's top-left so the drawer can build paths in (0..w, 0..h).
|
|
2086
|
+
*/
|
|
2087
|
+
declare const drawWithNodeTransform: (ctx: CanvasRenderingContext2D, node: Node, fn: () => void) => void;
|
|
2088
|
+
|
|
2089
|
+
/**
|
|
2090
|
+
* Node hit testing — see ARCHITECTURE.md §6.9 (parallels edge hit testing
|
|
2091
|
+
* structure for phase 4).
|
|
2092
|
+
*
|
|
2093
|
+
* For axis-aligned nodes: a fast AABB check.
|
|
2094
|
+
* For rotated nodes: transform the world point into node-local pre-rotation
|
|
2095
|
+
* coords (collapsing the rotation problem to AABB).
|
|
2096
|
+
*/
|
|
2097
|
+
|
|
2098
|
+
/**
|
|
2099
|
+
* Returns true if the world-space point is inside the (possibly rotated) node.
|
|
2100
|
+
*/
|
|
2101
|
+
declare const pointInNode: (point: Vec2, node: Node) => boolean;
|
|
2102
|
+
/**
|
|
2103
|
+
* Returns true if the node's rotated rect intersects the given AABB.
|
|
2104
|
+
* For axis-aligned nodes this collapses to two AABB ranges; for rotated
|
|
2105
|
+
* nodes we test all 4 corners + 4 axis projections (SAT).
|
|
2106
|
+
*/
|
|
2107
|
+
declare const nodeIntersectsRect: (node: Node, rect: WorldRect) => boolean;
|
|
2108
|
+
|
|
2109
|
+
/** Hit-slop in screen pixels for the edge body. */
|
|
2110
|
+
declare const EDGE_HIT_SLOP_PX = 8;
|
|
2111
|
+
/** Hit-slop in screen pixels for endpoint / arrowhead handles. */
|
|
2112
|
+
declare const EDGE_HANDLE_SLOP_PX = 12;
|
|
2113
|
+
type EdgeHit = {
|
|
2114
|
+
kind: 'body';
|
|
2115
|
+
edgeId: EdgeId;
|
|
2116
|
+
distance: number;
|
|
2117
|
+
arcLength: number;
|
|
2118
|
+
} | {
|
|
2119
|
+
kind: 'source-handle';
|
|
2120
|
+
edgeId: EdgeId;
|
|
2121
|
+
distance: number;
|
|
2122
|
+
} | {
|
|
2123
|
+
kind: 'target-handle';
|
|
2124
|
+
edgeId: EdgeId;
|
|
2125
|
+
distance: number;
|
|
2126
|
+
} | {
|
|
2127
|
+
kind: 'midpoint-handle';
|
|
2128
|
+
edgeId: EdgeId;
|
|
2129
|
+
} | {
|
|
2130
|
+
kind: 'label';
|
|
2131
|
+
edgeId: EdgeId;
|
|
2132
|
+
};
|
|
2133
|
+
/**
|
|
2134
|
+
* Returns the topmost edge hit by a world point, or null.
|
|
2135
|
+
* `selectedEdges` enables endpoint-handle detection (handles only show
|
|
2136
|
+
* when the edge is selected).
|
|
2137
|
+
*/
|
|
2138
|
+
declare const hitTestEdge: (store: CanvasStore, worldPoint: Vec2, cameraZ: number, selectedEdges?: ReadonlySet<EdgeId>) => EdgeHit | null;
|
|
2139
|
+
|
|
2140
|
+
/**
|
|
2141
|
+
* Higher-level hit queries that combine the spatial index with per-shape
|
|
2142
|
+
* narrow-phase tests. Returns the topmost hit by z, or all hits inside a
|
|
2143
|
+
* marquee rect.
|
|
2144
|
+
*/
|
|
2145
|
+
|
|
2146
|
+
type NodeHit = {
|
|
2147
|
+
kind: 'body';
|
|
2148
|
+
nodeId: NodeId;
|
|
2149
|
+
} | {
|
|
2150
|
+
kind: 'resize-handle';
|
|
2151
|
+
nodeId: NodeId;
|
|
2152
|
+
handle: ResizeHandle;
|
|
2153
|
+
} | {
|
|
2154
|
+
kind: 'rotate-handle';
|
|
2155
|
+
nodeId: NodeId;
|
|
2156
|
+
};
|
|
2157
|
+
/** A hit covers either a node or an edge sub-region. */
|
|
2158
|
+
type Hit = NodeHit | EdgeHit;
|
|
2159
|
+
/**
|
|
2160
|
+
* Returns the topmost node hit by a world-space point, plus the part hit
|
|
2161
|
+
* (body or resize handle). Handles are tested before bodies (interactive
|
|
2162
|
+
* elements always win over background — see ARCHITECTURE.md §7).
|
|
2163
|
+
*
|
|
2164
|
+
* If `selectedIds` is provided, only those nodes' handles are considered
|
|
2165
|
+
* — handles only display when the node is selected.
|
|
2166
|
+
*/
|
|
2167
|
+
declare const hitTestPoint: (store: CanvasStore, worldPoint: Vec2, cameraZ: number, selectedIds?: ReadonlySet<NodeId>) => NodeHit | null;
|
|
2168
|
+
/**
|
|
2169
|
+
* Combined node + edge hit testing. Order: node handles > edge endpoint
|
|
2170
|
+
* handles > node bodies > edge bodies.
|
|
2171
|
+
*
|
|
2172
|
+
* Node bodies take priority over edge bodies because clicking ON a node
|
|
2173
|
+
* shouldn't accidentally select the edge passing behind it.
|
|
2174
|
+
*/
|
|
2175
|
+
declare const hitTestAny: (store: CanvasStore, worldPoint: Vec2, cameraZ: number, selectedNodes?: ReadonlySet<NodeId>, selectedEdges?: ReadonlySet<EdgeId>) => Hit | null;
|
|
2176
|
+
/**
|
|
2177
|
+
* Returns ids of all nodes whose (rotated) rect intersects the given rect.
|
|
2178
|
+
* Used for marquee selection.
|
|
2179
|
+
*/
|
|
2180
|
+
declare const marqueeNodes: (store: CanvasStore, rect: WorldRect) => NodeId[];
|
|
2181
|
+
|
|
2182
|
+
/**
|
|
2183
|
+
* Clipboard serialization — see ARCHITECTURE.md §13 (copy/paste).
|
|
2184
|
+
*
|
|
2185
|
+
* Captures the selected nodes plus the edges *between* them (edges
|
|
2186
|
+
* crossing the selection are dropped — same rule as tldraw/excalidraw).
|
|
2187
|
+
* Pure functions; no clipboard-API I/O. The store wraps these with
|
|
2188
|
+
* `navigator.clipboard.{write,read}` calls.
|
|
2189
|
+
*/
|
|
2190
|
+
type SerializedClipboard = {
|
|
2191
|
+
/** Schema version stamped at copy time. */
|
|
2192
|
+
v: number;
|
|
2193
|
+
/** Source clientId — diagnostic only; not used for paste. */
|
|
2194
|
+
clientId: string;
|
|
2195
|
+
/** Tagged so we can tell our payload apart from arbitrary JSON. */
|
|
2196
|
+
kind: 'canvas-harness/clipboard';
|
|
2197
|
+
nodes: Node[];
|
|
2198
|
+
edges: Edge[];
|
|
2199
|
+
};
|
|
2200
|
+
/**
|
|
2201
|
+
* Builds a clipboard payload from the store's current selection. Pure
|
|
2202
|
+
* — no I/O, no clipboard API. Useful for programmatic copy-paste
|
|
2203
|
+
* (snapshots, AI-driven duplication, drag-from-sidebar, ...).
|
|
2204
|
+
*
|
|
2205
|
+
* Edges crossing the selection boundary (only one endpoint in the
|
|
2206
|
+
* selection) are dropped. Edges with `worldPoint` endpoints are kept.
|
|
2207
|
+
*
|
|
2208
|
+
* @example
|
|
2209
|
+
* const clip = serializeSelection(store)
|
|
2210
|
+
* localStorage.setItem('clipboard', JSON.stringify(clip))
|
|
2211
|
+
*/
|
|
2212
|
+
declare const serializeSelection: (store: CanvasStore) => SerializedClipboard;
|
|
2213
|
+
type DeserializeOptions = {
|
|
2214
|
+
/** World-space offset applied to all pasted nodes. Default (20, 20). */
|
|
2215
|
+
offset?: Vec2;
|
|
2216
|
+
/** Override the selection on the store after applying. Default true. */
|
|
2217
|
+
select?: boolean;
|
|
2218
|
+
};
|
|
2219
|
+
/**
|
|
2220
|
+
* Applies a clipboard payload to the store. New ids are minted; edge
|
|
2221
|
+
* endpoints are rewired; offset defaults to `(20, 20)` world units;
|
|
2222
|
+
* the resulting nodes + edges become the new selection by default.
|
|
2223
|
+
*
|
|
2224
|
+
* One `store.batch` — one undo step.
|
|
2225
|
+
*
|
|
2226
|
+
* @example
|
|
2227
|
+
* // Restore from localStorage:
|
|
2228
|
+
* const clip = JSON.parse(localStorage.getItem('clipboard')!)
|
|
2229
|
+
* if (isCanvasHarnessClipboard(clip)) deserializeClipboard(store, clip)
|
|
2230
|
+
*/
|
|
2231
|
+
declare const deserializeClipboard: (store: CanvasStore, clip: SerializedClipboard, opts?: DeserializeOptions) => NodeId[];
|
|
2232
|
+
/**
|
|
2233
|
+
* Type guard — verifies a parsed JSON blob is a clipboard payload from
|
|
2234
|
+
* this library, not arbitrary JSON pasted from elsewhere.
|
|
2235
|
+
*/
|
|
2236
|
+
declare const isCanvasHarnessClipboard: (raw: unknown) => raw is SerializedClipboard;
|
|
2237
|
+
|
|
2238
|
+
/**
|
|
2239
|
+
* Copies the current selection to the system clipboard. Writes both a
|
|
2240
|
+
* native MIME (`application/x-canvas-harness+json`) and a `text/plain`
|
|
2241
|
+
* fallback (concatenated node contents) so paste works in non-canvas
|
|
2242
|
+
* destinations too.
|
|
2243
|
+
*
|
|
2244
|
+
* The `<Canvas>` component already wires this to Cmd/Ctrl+C — call
|
|
2245
|
+
* directly only if you're building a custom copy button.
|
|
2246
|
+
*
|
|
2247
|
+
* @example
|
|
2248
|
+
* <button onClick={() => copy(store)}>Copy</button>
|
|
2249
|
+
*/
|
|
2250
|
+
declare const copy: (store: CanvasStore) => Promise<SerializedClipboard>;
|
|
2251
|
+
/**
|
|
2252
|
+
* Copy + remove the selection in one undoable batch. Same as
|
|
2253
|
+
* Cmd/Ctrl+X.
|
|
2254
|
+
*
|
|
2255
|
+
* @example
|
|
2256
|
+
* <button onClick={() => cut(store)}>Cut</button>
|
|
2257
|
+
*/
|
|
2258
|
+
declare const cut: (store: CanvasStore) => Promise<SerializedClipboard>;
|
|
2259
|
+
/**
|
|
2260
|
+
* Paste from the system clipboard (or a supplied payload). Every node
|
|
2261
|
+
* + edge gets a fresh id; edge endpoints rewire to the new ids; the
|
|
2262
|
+
* paste is offset by `(+20, +20)` world units so it doesn't overlay
|
|
2263
|
+
* the original. Wrapped in one undoable batch.
|
|
2264
|
+
*
|
|
2265
|
+
* Returns the new node ids on success, or `null` if the clipboard
|
|
2266
|
+
* didn't contain a canvas-harness payload.
|
|
2267
|
+
*
|
|
2268
|
+
* @example
|
|
2269
|
+
* <button onClick={() => paste(store)}>Paste</button>
|
|
2270
|
+
*
|
|
2271
|
+
* @example
|
|
2272
|
+
* // Programmatic paste from a saved JSON snippet:
|
|
2273
|
+
* paste(store, savedClip, { offset: { x: 0, y: 0 }, select: false })
|
|
2274
|
+
*/
|
|
2275
|
+
declare const paste: (store: CanvasStore, payload?: SerializedClipboard, opts?: DeserializeOptions) => Promise<(NodeId | EdgeId)[] | null>;
|
|
2276
|
+
|
|
2277
|
+
/**
|
|
2278
|
+
* PNG export — see ARCHITECTURE.md §13. Paints the requested set of
|
|
2279
|
+
* nodes + edges into an offscreen canvas at logical coords; returns a
|
|
2280
|
+
* Blob (image/png).
|
|
2281
|
+
*/
|
|
2282
|
+
type ExportOptions = {
|
|
2283
|
+
/** Bitmap scale multiplier — defaults to 2 for retina-ish output. */
|
|
2284
|
+
scale?: number;
|
|
2285
|
+
/** Padding (logical px) around the bounding rect. Default 16. */
|
|
2286
|
+
padding?: number;
|
|
2287
|
+
/** Skip the background fill. Default false. */
|
|
2288
|
+
transparentBackground?: boolean;
|
|
2289
|
+
/** Background color when not transparent. Default white. */
|
|
2290
|
+
backgroundColor?: string;
|
|
2291
|
+
/** Theme resolver, same one passed to the live renderer. */
|
|
2292
|
+
theme?: ThemeResolver;
|
|
2293
|
+
};
|
|
2294
|
+
/**
|
|
2295
|
+
* Renders the current selection to a PNG. Returns a `Blob` you can
|
|
2296
|
+
* download, upload, or paste somewhere.
|
|
2297
|
+
*
|
|
2298
|
+
* Bounding rect is computed from the selected nodes; edges between
|
|
2299
|
+
* selected nodes are included, edges crossing the boundary are
|
|
2300
|
+
* dropped.
|
|
2301
|
+
*
|
|
2302
|
+
* @example
|
|
2303
|
+
* // Download the selection as a PNG file.
|
|
2304
|
+
* const blob = await exportSelection(store, { scale: 2 })
|
|
2305
|
+
* const url = URL.createObjectURL(blob)
|
|
2306
|
+
* Object.assign(document.createElement('a'), {
|
|
2307
|
+
* href: url, download: 'scene.png',
|
|
2308
|
+
* }).click()
|
|
2309
|
+
* URL.revokeObjectURL(url)
|
|
2310
|
+
*
|
|
2311
|
+
* @example
|
|
2312
|
+
* // Transparent PNG for slide-deck overlays.
|
|
2313
|
+
* const blob = await exportSelection(store, { transparentBackground: true })
|
|
2314
|
+
*/
|
|
2315
|
+
declare const exportSelection: (store: CanvasStore, opts?: ExportOptions) => Promise<Blob>;
|
|
2316
|
+
/**
|
|
2317
|
+
* Renders an arbitrary world-space viewport to a PNG. Use to capture
|
|
2318
|
+
* the current screen, a minimap, or a specific region.
|
|
2319
|
+
*
|
|
2320
|
+
* @example
|
|
2321
|
+
* const viewport = worldViewport(staticSurface, store.getCamera())
|
|
2322
|
+
* const blob = await exportViewport(store, viewport)
|
|
2323
|
+
*/
|
|
2324
|
+
declare const exportViewport: (store: CanvasStore, viewport: {
|
|
2325
|
+
x: number;
|
|
2326
|
+
y: number;
|
|
2327
|
+
w: number;
|
|
2328
|
+
h: number;
|
|
2329
|
+
}, opts?: ExportOptions) => Promise<Blob>;
|
|
2330
|
+
|
|
2331
|
+
/**
|
|
2332
|
+
* SVG export — see ARCHITECTURE.md §13.
|
|
2333
|
+
*
|
|
2334
|
+
* **Scope**: matches PNG export for shape geometry + edge geometry, but
|
|
2335
|
+
* markdown content is emitted as **plain text** (no inline bold /
|
|
2336
|
+
* italic / highlight). SVG `<text>` doesn't support our markdown
|
|
2337
|
+
* dialect without tspan positioning math; deferred to v2. PNG export
|
|
2338
|
+
* preserves all markdown styling via the bitmap pipeline.
|
|
2339
|
+
*/
|
|
2340
|
+
type SvgExportOptions = {
|
|
2341
|
+
padding?: number;
|
|
2342
|
+
transparentBackground?: boolean;
|
|
2343
|
+
backgroundColor?: string;
|
|
2344
|
+
};
|
|
2345
|
+
/**
|
|
2346
|
+
* Renders the current selection to an SVG string. Synchronous —
|
|
2347
|
+
* unlike PNG, no canvas roundtrip needed.
|
|
2348
|
+
*
|
|
2349
|
+
* **Caveat:** SVG `<text>` doesn't support our markdown dialect, so
|
|
2350
|
+
* `**bold**` / `==hl==` etc. render as plain text with the syntax
|
|
2351
|
+
* stripped. PNG export preserves all markdown via the bitmap pipeline.
|
|
2352
|
+
*
|
|
2353
|
+
* @example
|
|
2354
|
+
* const svg = exportSelectionSvg(store)
|
|
2355
|
+
* const blob = new Blob([svg], { type: 'image/svg+xml' })
|
|
2356
|
+
*/
|
|
2357
|
+
declare const exportSelectionSvg: (store: CanvasStore, opts?: SvgExportOptions) => string;
|
|
2358
|
+
|
|
2359
|
+
/**
|
|
2360
|
+
* AI scene context — see ARCHITECTURE.md §13.
|
|
2361
|
+
*
|
|
2362
|
+
* Returns a human- or machine-readable snapshot of the scene for use
|
|
2363
|
+
* as a system-prompt payload or AI tool-call argument. **Markdown is
|
|
2364
|
+
* the prose form** (better for LLM comprehension token-per-token);
|
|
2365
|
+
* JSON is the structured form for downstream automation.
|
|
2366
|
+
*
|
|
2367
|
+
* Output keeps it tight: each node + edge becomes one line, with
|
|
2368
|
+
* truncation when the scene is large.
|
|
2369
|
+
*/
|
|
2370
|
+
type GetContextOptions = {
|
|
2371
|
+
format?: 'markdown' | 'json';
|
|
2372
|
+
/** Restrict to the current selection. Default: include the whole scene. */
|
|
2373
|
+
selectionOnly?: boolean;
|
|
2374
|
+
/** Truncate node list at this count. Default 500. */
|
|
2375
|
+
maxNodes?: number;
|
|
2376
|
+
};
|
|
2377
|
+
/**
|
|
2378
|
+
* Returns a snapshot of the scene suitable for an LLM system prompt or
|
|
2379
|
+
* a tool-call argument.
|
|
2380
|
+
*
|
|
2381
|
+
* - `format: 'markdown'` (default) — full-text prose summary. Better
|
|
2382
|
+
* for LLM token economy.
|
|
2383
|
+
* - `format: 'json'` — structured `SceneContextJson` shape; lighter
|
|
2384
|
+
* than `SerializedScene` (no internal fields).
|
|
2385
|
+
*
|
|
2386
|
+
* @example
|
|
2387
|
+
* // System prompt
|
|
2388
|
+
* const ctx = getContext(store) as string
|
|
2389
|
+
* await anthropic.messages.create({
|
|
2390
|
+
* system: `Current scene:\n${ctx}`,
|
|
2391
|
+
* ...
|
|
2392
|
+
* })
|
|
2393
|
+
*
|
|
2394
|
+
* @example
|
|
2395
|
+
* // JSON for downstream automation
|
|
2396
|
+
* const ctx = getContext(store, { format: 'json', selectionOnly: true })
|
|
2397
|
+
*/
|
|
2398
|
+
declare const getContext: (store: CanvasStore, opts?: GetContextOptions) => string | SceneContextJson;
|
|
2399
|
+
type SceneContextJson = {
|
|
2400
|
+
camera: {
|
|
2401
|
+
x: number;
|
|
2402
|
+
y: number;
|
|
2403
|
+
z: number;
|
|
2404
|
+
};
|
|
2405
|
+
nodes: ContextNode[];
|
|
2406
|
+
edges: ContextEdge[];
|
|
2407
|
+
truncated: boolean;
|
|
2408
|
+
};
|
|
2409
|
+
type ContextNode = {
|
|
2410
|
+
id: string;
|
|
2411
|
+
type: string;
|
|
2412
|
+
x: number;
|
|
2413
|
+
y: number;
|
|
2414
|
+
w: number;
|
|
2415
|
+
h: number;
|
|
2416
|
+
angle?: number;
|
|
2417
|
+
content?: string;
|
|
2418
|
+
style?: Record<string, unknown>;
|
|
2419
|
+
};
|
|
2420
|
+
type ContextEdge = {
|
|
2421
|
+
id: string;
|
|
2422
|
+
source: string | {
|
|
2423
|
+
x: number;
|
|
2424
|
+
y: number;
|
|
2425
|
+
};
|
|
2426
|
+
target: string | {
|
|
2427
|
+
x: number;
|
|
2428
|
+
y: number;
|
|
2429
|
+
};
|
|
2430
|
+
pathStyle?: string;
|
|
2431
|
+
};
|
|
2432
|
+
|
|
2433
|
+
/**
|
|
2434
|
+
* Op schemas — see ARCHITECTURE.md §13.
|
|
2435
|
+
*
|
|
2436
|
+
* Hand-written JSON-Schema definitions for the `Op` discriminated
|
|
2437
|
+
* union. AI agents use these to validate generated ops before calling
|
|
2438
|
+
* `store.applyOp`; tool-use frameworks (Anthropic, OpenAI, Vertex)
|
|
2439
|
+
* advertise the schemas as callable tool definitions.
|
|
2440
|
+
*
|
|
2441
|
+
* Hand-written (not derived from TS) so the schemas survive when the
|
|
2442
|
+
* runtime shape evolves without an explicit schema update.
|
|
2443
|
+
*/
|
|
2444
|
+
/**
|
|
2445
|
+
* JSON-Schema definitions for every `Op` variant. Use to validate
|
|
2446
|
+
* agent-generated ops before calling `store.applyOp`, or to feed into
|
|
2447
|
+
* an LLM tool-use loop.
|
|
2448
|
+
*
|
|
2449
|
+
* @example
|
|
2450
|
+
* import Ajv from 'ajv'
|
|
2451
|
+
* const ajv = new Ajv()
|
|
2452
|
+
* const validate = ajv.compile(opSchemas.nodeAdd)
|
|
2453
|
+
* if (validate(generatedOp)) store.applyOp(generatedOp)
|
|
2454
|
+
*/
|
|
2455
|
+
declare const opSchemas: {
|
|
2456
|
+
readonly nodeAdd: {
|
|
2457
|
+
readonly type: "object";
|
|
2458
|
+
readonly required: readonly ["type", "node"];
|
|
2459
|
+
readonly properties: {
|
|
2460
|
+
readonly type: {
|
|
2461
|
+
readonly const: "node.add";
|
|
2462
|
+
};
|
|
2463
|
+
readonly node: {
|
|
2464
|
+
readonly type: "object";
|
|
2465
|
+
readonly required: readonly ["id", "type", "x", "y", "w", "h", "angle", "z", "groups"];
|
|
2466
|
+
readonly properties: {
|
|
2467
|
+
readonly id: {
|
|
2468
|
+
readonly type: "string";
|
|
2469
|
+
readonly description: "Stable id (typically generated via `store.generateId()`).";
|
|
2470
|
+
};
|
|
2471
|
+
readonly type: {
|
|
2472
|
+
readonly type: "string";
|
|
2473
|
+
readonly description: "Node type — rect / ellipse / diamond / tag / capsule / thought-cloud / layered-rect / layered-ellipse / layered-diamond / text / a registered custom type.";
|
|
2474
|
+
};
|
|
2475
|
+
readonly x: {
|
|
2476
|
+
readonly type: "number";
|
|
2477
|
+
};
|
|
2478
|
+
readonly y: {
|
|
2479
|
+
readonly type: "number";
|
|
2480
|
+
};
|
|
2481
|
+
readonly w: {
|
|
2482
|
+
readonly type: "number";
|
|
2483
|
+
readonly minimum: 0;
|
|
2484
|
+
};
|
|
2485
|
+
readonly h: {
|
|
2486
|
+
readonly type: "number";
|
|
2487
|
+
readonly minimum: 0;
|
|
2488
|
+
};
|
|
2489
|
+
readonly angle: {
|
|
2490
|
+
readonly type: "number";
|
|
2491
|
+
readonly description: "Rotation in radians (clockwise).";
|
|
2492
|
+
};
|
|
2493
|
+
readonly z: {
|
|
2494
|
+
readonly type: "number";
|
|
2495
|
+
};
|
|
2496
|
+
readonly groups: {
|
|
2497
|
+
readonly type: "array";
|
|
2498
|
+
readonly items: {
|
|
2499
|
+
readonly type: "string";
|
|
2500
|
+
};
|
|
2501
|
+
};
|
|
2502
|
+
readonly content: {
|
|
2503
|
+
readonly type: "string";
|
|
2504
|
+
readonly description: "Markdown content (for text-bearing shapes).";
|
|
2505
|
+
};
|
|
2506
|
+
readonly style: {
|
|
2507
|
+
readonly type: "object";
|
|
2508
|
+
readonly description: "Style bag — see ARCHITECTURE.md §3.4 (Style type).";
|
|
2509
|
+
};
|
|
2510
|
+
readonly hidden: {
|
|
2511
|
+
readonly type: "boolean";
|
|
2512
|
+
};
|
|
2513
|
+
};
|
|
2514
|
+
};
|
|
2515
|
+
};
|
|
2516
|
+
};
|
|
2517
|
+
readonly nodeUpdate: {
|
|
2518
|
+
readonly type: "object";
|
|
2519
|
+
readonly required: readonly ["type", "id", "patch", "prev"];
|
|
2520
|
+
readonly properties: {
|
|
2521
|
+
readonly type: {
|
|
2522
|
+
readonly const: "node.update";
|
|
2523
|
+
};
|
|
2524
|
+
readonly id: {
|
|
2525
|
+
readonly type: "string";
|
|
2526
|
+
};
|
|
2527
|
+
readonly patch: {
|
|
2528
|
+
readonly type: "object";
|
|
2529
|
+
};
|
|
2530
|
+
readonly prev: {
|
|
2531
|
+
readonly type: "object";
|
|
2532
|
+
};
|
|
2533
|
+
};
|
|
2534
|
+
};
|
|
2535
|
+
readonly nodeRemove: {
|
|
2536
|
+
readonly type: "object";
|
|
2537
|
+
readonly required: readonly ["type", "node"];
|
|
2538
|
+
readonly properties: {
|
|
2539
|
+
readonly type: {
|
|
2540
|
+
readonly const: "node.remove";
|
|
2541
|
+
};
|
|
2542
|
+
readonly node: {
|
|
2543
|
+
readonly type: "object";
|
|
2544
|
+
readonly required: readonly ["id", "type", "x", "y", "w", "h", "angle", "z", "groups"];
|
|
2545
|
+
readonly properties: {
|
|
2546
|
+
readonly id: {
|
|
2547
|
+
readonly type: "string";
|
|
2548
|
+
readonly description: "Stable id (typically generated via `store.generateId()`).";
|
|
2549
|
+
};
|
|
2550
|
+
readonly type: {
|
|
2551
|
+
readonly type: "string";
|
|
2552
|
+
readonly description: "Node type — rect / ellipse / diamond / tag / capsule / thought-cloud / layered-rect / layered-ellipse / layered-diamond / text / a registered custom type.";
|
|
2553
|
+
};
|
|
2554
|
+
readonly x: {
|
|
2555
|
+
readonly type: "number";
|
|
2556
|
+
};
|
|
2557
|
+
readonly y: {
|
|
2558
|
+
readonly type: "number";
|
|
2559
|
+
};
|
|
2560
|
+
readonly w: {
|
|
2561
|
+
readonly type: "number";
|
|
2562
|
+
readonly minimum: 0;
|
|
2563
|
+
};
|
|
2564
|
+
readonly h: {
|
|
2565
|
+
readonly type: "number";
|
|
2566
|
+
readonly minimum: 0;
|
|
2567
|
+
};
|
|
2568
|
+
readonly angle: {
|
|
2569
|
+
readonly type: "number";
|
|
2570
|
+
readonly description: "Rotation in radians (clockwise).";
|
|
2571
|
+
};
|
|
2572
|
+
readonly z: {
|
|
2573
|
+
readonly type: "number";
|
|
2574
|
+
};
|
|
2575
|
+
readonly groups: {
|
|
2576
|
+
readonly type: "array";
|
|
2577
|
+
readonly items: {
|
|
2578
|
+
readonly type: "string";
|
|
2579
|
+
};
|
|
2580
|
+
};
|
|
2581
|
+
readonly content: {
|
|
2582
|
+
readonly type: "string";
|
|
2583
|
+
readonly description: "Markdown content (for text-bearing shapes).";
|
|
2584
|
+
};
|
|
2585
|
+
readonly style: {
|
|
2586
|
+
readonly type: "object";
|
|
2587
|
+
readonly description: "Style bag — see ARCHITECTURE.md §3.4 (Style type).";
|
|
2588
|
+
};
|
|
2589
|
+
readonly hidden: {
|
|
2590
|
+
readonly type: "boolean";
|
|
2591
|
+
};
|
|
2592
|
+
};
|
|
2593
|
+
};
|
|
2594
|
+
};
|
|
2595
|
+
};
|
|
2596
|
+
readonly edgeAdd: {
|
|
2597
|
+
readonly type: "object";
|
|
2598
|
+
readonly required: readonly ["type", "edge"];
|
|
2599
|
+
readonly properties: {
|
|
2600
|
+
readonly type: {
|
|
2601
|
+
readonly const: "edge.add";
|
|
2602
|
+
};
|
|
2603
|
+
readonly edge: {
|
|
2604
|
+
readonly type: "object";
|
|
2605
|
+
readonly required: readonly ["id", "source", "target", "pathStyle", "z", "groups"];
|
|
2606
|
+
readonly properties: {
|
|
2607
|
+
readonly id: {
|
|
2608
|
+
readonly type: "string";
|
|
2609
|
+
};
|
|
2610
|
+
readonly source: {
|
|
2611
|
+
readonly oneOf: readonly [{
|
|
2612
|
+
readonly type: "object";
|
|
2613
|
+
readonly required: readonly ["nodeId", "localOffset"];
|
|
2614
|
+
readonly properties: {
|
|
2615
|
+
readonly nodeId: {
|
|
2616
|
+
readonly type: "string";
|
|
2617
|
+
};
|
|
2618
|
+
readonly localOffset: {
|
|
2619
|
+
readonly type: "object";
|
|
2620
|
+
readonly required: readonly ["x", "y"];
|
|
2621
|
+
readonly properties: {
|
|
2622
|
+
readonly x: {
|
|
2623
|
+
readonly type: "number";
|
|
2624
|
+
};
|
|
2625
|
+
readonly y: {
|
|
2626
|
+
readonly type: "number";
|
|
2627
|
+
};
|
|
2628
|
+
};
|
|
2629
|
+
};
|
|
2630
|
+
};
|
|
2631
|
+
}, {
|
|
2632
|
+
readonly type: "object";
|
|
2633
|
+
readonly required: readonly ["worldPoint"];
|
|
2634
|
+
readonly properties: {
|
|
2635
|
+
readonly worldPoint: {
|
|
2636
|
+
readonly type: "object";
|
|
2637
|
+
readonly required: readonly ["x", "y"];
|
|
2638
|
+
readonly properties: {
|
|
2639
|
+
readonly x: {
|
|
2640
|
+
readonly type: "number";
|
|
2641
|
+
};
|
|
2642
|
+
readonly y: {
|
|
2643
|
+
readonly type: "number";
|
|
2644
|
+
};
|
|
2645
|
+
};
|
|
2646
|
+
};
|
|
2647
|
+
};
|
|
2648
|
+
}];
|
|
2649
|
+
};
|
|
2650
|
+
readonly target: {
|
|
2651
|
+
readonly oneOf: readonly [{
|
|
2652
|
+
readonly type: "object";
|
|
2653
|
+
readonly required: readonly ["nodeId", "localOffset"];
|
|
2654
|
+
readonly properties: {
|
|
2655
|
+
readonly nodeId: {
|
|
2656
|
+
readonly type: "string";
|
|
2657
|
+
};
|
|
2658
|
+
readonly localOffset: {
|
|
2659
|
+
readonly type: "object";
|
|
2660
|
+
readonly required: readonly ["x", "y"];
|
|
2661
|
+
readonly properties: {
|
|
2662
|
+
readonly x: {
|
|
2663
|
+
readonly type: "number";
|
|
2664
|
+
};
|
|
2665
|
+
readonly y: {
|
|
2666
|
+
readonly type: "number";
|
|
2667
|
+
};
|
|
2668
|
+
};
|
|
2669
|
+
};
|
|
2670
|
+
};
|
|
2671
|
+
}, {
|
|
2672
|
+
readonly type: "object";
|
|
2673
|
+
readonly required: readonly ["worldPoint"];
|
|
2674
|
+
readonly properties: {
|
|
2675
|
+
readonly worldPoint: {
|
|
2676
|
+
readonly type: "object";
|
|
2677
|
+
readonly required: readonly ["x", "y"];
|
|
2678
|
+
readonly properties: {
|
|
2679
|
+
readonly x: {
|
|
2680
|
+
readonly type: "number";
|
|
2681
|
+
};
|
|
2682
|
+
readonly y: {
|
|
2683
|
+
readonly type: "number";
|
|
2684
|
+
};
|
|
2685
|
+
};
|
|
2686
|
+
};
|
|
2687
|
+
};
|
|
2688
|
+
}];
|
|
2689
|
+
};
|
|
2690
|
+
readonly pathStyle: {
|
|
2691
|
+
readonly type: "string";
|
|
2692
|
+
readonly enum: readonly ["bezier", "straight", "polyline"];
|
|
2693
|
+
};
|
|
2694
|
+
readonly z: {
|
|
2695
|
+
readonly type: "number";
|
|
2696
|
+
};
|
|
2697
|
+
readonly groups: {
|
|
2698
|
+
readonly type: "array";
|
|
2699
|
+
readonly items: {
|
|
2700
|
+
readonly type: "string";
|
|
2701
|
+
};
|
|
2702
|
+
};
|
|
2703
|
+
readonly style: {
|
|
2704
|
+
readonly type: "object";
|
|
2705
|
+
};
|
|
2706
|
+
readonly hidden: {
|
|
2707
|
+
readonly type: "boolean";
|
|
2708
|
+
};
|
|
2709
|
+
};
|
|
2710
|
+
};
|
|
2711
|
+
};
|
|
2712
|
+
};
|
|
2713
|
+
readonly edgeUpdate: {
|
|
2714
|
+
readonly type: "object";
|
|
2715
|
+
readonly required: readonly ["type", "id", "patch", "prev"];
|
|
2716
|
+
readonly properties: {
|
|
2717
|
+
readonly type: {
|
|
2718
|
+
readonly const: "edge.update";
|
|
2719
|
+
};
|
|
2720
|
+
readonly id: {
|
|
2721
|
+
readonly type: "string";
|
|
2722
|
+
};
|
|
2723
|
+
readonly patch: {
|
|
2724
|
+
readonly type: "object";
|
|
2725
|
+
};
|
|
2726
|
+
readonly prev: {
|
|
2727
|
+
readonly type: "object";
|
|
2728
|
+
};
|
|
2729
|
+
};
|
|
2730
|
+
};
|
|
2731
|
+
readonly edgeRemove: {
|
|
2732
|
+
readonly type: "object";
|
|
2733
|
+
readonly required: readonly ["type", "edge"];
|
|
2734
|
+
readonly properties: {
|
|
2735
|
+
readonly type: {
|
|
2736
|
+
readonly const: "edge.remove";
|
|
2737
|
+
};
|
|
2738
|
+
readonly edge: {
|
|
2739
|
+
readonly type: "object";
|
|
2740
|
+
readonly required: readonly ["id", "source", "target", "pathStyle", "z", "groups"];
|
|
2741
|
+
readonly properties: {
|
|
2742
|
+
readonly id: {
|
|
2743
|
+
readonly type: "string";
|
|
2744
|
+
};
|
|
2745
|
+
readonly source: {
|
|
2746
|
+
readonly oneOf: readonly [{
|
|
2747
|
+
readonly type: "object";
|
|
2748
|
+
readonly required: readonly ["nodeId", "localOffset"];
|
|
2749
|
+
readonly properties: {
|
|
2750
|
+
readonly nodeId: {
|
|
2751
|
+
readonly type: "string";
|
|
2752
|
+
};
|
|
2753
|
+
readonly localOffset: {
|
|
2754
|
+
readonly type: "object";
|
|
2755
|
+
readonly required: readonly ["x", "y"];
|
|
2756
|
+
readonly properties: {
|
|
2757
|
+
readonly x: {
|
|
2758
|
+
readonly type: "number";
|
|
2759
|
+
};
|
|
2760
|
+
readonly y: {
|
|
2761
|
+
readonly type: "number";
|
|
2762
|
+
};
|
|
2763
|
+
};
|
|
2764
|
+
};
|
|
2765
|
+
};
|
|
2766
|
+
}, {
|
|
2767
|
+
readonly type: "object";
|
|
2768
|
+
readonly required: readonly ["worldPoint"];
|
|
2769
|
+
readonly properties: {
|
|
2770
|
+
readonly worldPoint: {
|
|
2771
|
+
readonly type: "object";
|
|
2772
|
+
readonly required: readonly ["x", "y"];
|
|
2773
|
+
readonly properties: {
|
|
2774
|
+
readonly x: {
|
|
2775
|
+
readonly type: "number";
|
|
2776
|
+
};
|
|
2777
|
+
readonly y: {
|
|
2778
|
+
readonly type: "number";
|
|
2779
|
+
};
|
|
2780
|
+
};
|
|
2781
|
+
};
|
|
2782
|
+
};
|
|
2783
|
+
}];
|
|
2784
|
+
};
|
|
2785
|
+
readonly target: {
|
|
2786
|
+
readonly oneOf: readonly [{
|
|
2787
|
+
readonly type: "object";
|
|
2788
|
+
readonly required: readonly ["nodeId", "localOffset"];
|
|
2789
|
+
readonly properties: {
|
|
2790
|
+
readonly nodeId: {
|
|
2791
|
+
readonly type: "string";
|
|
2792
|
+
};
|
|
2793
|
+
readonly localOffset: {
|
|
2794
|
+
readonly type: "object";
|
|
2795
|
+
readonly required: readonly ["x", "y"];
|
|
2796
|
+
readonly properties: {
|
|
2797
|
+
readonly x: {
|
|
2798
|
+
readonly type: "number";
|
|
2799
|
+
};
|
|
2800
|
+
readonly y: {
|
|
2801
|
+
readonly type: "number";
|
|
2802
|
+
};
|
|
2803
|
+
};
|
|
2804
|
+
};
|
|
2805
|
+
};
|
|
2806
|
+
}, {
|
|
2807
|
+
readonly type: "object";
|
|
2808
|
+
readonly required: readonly ["worldPoint"];
|
|
2809
|
+
readonly properties: {
|
|
2810
|
+
readonly worldPoint: {
|
|
2811
|
+
readonly type: "object";
|
|
2812
|
+
readonly required: readonly ["x", "y"];
|
|
2813
|
+
readonly properties: {
|
|
2814
|
+
readonly x: {
|
|
2815
|
+
readonly type: "number";
|
|
2816
|
+
};
|
|
2817
|
+
readonly y: {
|
|
2818
|
+
readonly type: "number";
|
|
2819
|
+
};
|
|
2820
|
+
};
|
|
2821
|
+
};
|
|
2822
|
+
};
|
|
2823
|
+
}];
|
|
2824
|
+
};
|
|
2825
|
+
readonly pathStyle: {
|
|
2826
|
+
readonly type: "string";
|
|
2827
|
+
readonly enum: readonly ["bezier", "straight", "polyline"];
|
|
2828
|
+
};
|
|
2829
|
+
readonly z: {
|
|
2830
|
+
readonly type: "number";
|
|
2831
|
+
};
|
|
2832
|
+
readonly groups: {
|
|
2833
|
+
readonly type: "array";
|
|
2834
|
+
readonly items: {
|
|
2835
|
+
readonly type: "string";
|
|
2836
|
+
};
|
|
2837
|
+
};
|
|
2838
|
+
readonly style: {
|
|
2839
|
+
readonly type: "object";
|
|
2840
|
+
};
|
|
2841
|
+
readonly hidden: {
|
|
2842
|
+
readonly type: "boolean";
|
|
2843
|
+
};
|
|
2844
|
+
};
|
|
2845
|
+
};
|
|
2846
|
+
};
|
|
2847
|
+
};
|
|
2848
|
+
readonly groupUpsert: {
|
|
2849
|
+
readonly type: "object";
|
|
2850
|
+
readonly required: readonly ["type", "group"];
|
|
2851
|
+
readonly properties: {
|
|
2852
|
+
readonly type: {
|
|
2853
|
+
readonly const: "group.upsert";
|
|
2854
|
+
};
|
|
2855
|
+
readonly group: {
|
|
2856
|
+
readonly type: "object";
|
|
2857
|
+
readonly required: readonly ["id", "memberIds"];
|
|
2858
|
+
readonly properties: {
|
|
2859
|
+
readonly id: {
|
|
2860
|
+
readonly type: "string";
|
|
2861
|
+
};
|
|
2862
|
+
readonly memberIds: {
|
|
2863
|
+
readonly type: "array";
|
|
2864
|
+
readonly items: {
|
|
2865
|
+
readonly type: "string";
|
|
2866
|
+
};
|
|
2867
|
+
};
|
|
2868
|
+
readonly name: {
|
|
2869
|
+
readonly type: "string";
|
|
2870
|
+
};
|
|
2871
|
+
};
|
|
2872
|
+
};
|
|
2873
|
+
readonly prev: {
|
|
2874
|
+
readonly type: "object";
|
|
2875
|
+
readonly required: readonly ["id", "memberIds"];
|
|
2876
|
+
readonly properties: {
|
|
2877
|
+
readonly id: {
|
|
2878
|
+
readonly type: "string";
|
|
2879
|
+
};
|
|
2880
|
+
readonly memberIds: {
|
|
2881
|
+
readonly type: "array";
|
|
2882
|
+
readonly items: {
|
|
2883
|
+
readonly type: "string";
|
|
2884
|
+
};
|
|
2885
|
+
};
|
|
2886
|
+
readonly name: {
|
|
2887
|
+
readonly type: "string";
|
|
2888
|
+
};
|
|
2889
|
+
};
|
|
2890
|
+
};
|
|
2891
|
+
};
|
|
2892
|
+
};
|
|
2893
|
+
readonly groupRemove: {
|
|
2894
|
+
readonly type: "object";
|
|
2895
|
+
readonly required: readonly ["type", "group"];
|
|
2896
|
+
readonly properties: {
|
|
2897
|
+
readonly type: {
|
|
2898
|
+
readonly const: "group.remove";
|
|
2899
|
+
};
|
|
2900
|
+
readonly group: {
|
|
2901
|
+
readonly type: "object";
|
|
2902
|
+
readonly required: readonly ["id", "memberIds"];
|
|
2903
|
+
readonly properties: {
|
|
2904
|
+
readonly id: {
|
|
2905
|
+
readonly type: "string";
|
|
2906
|
+
};
|
|
2907
|
+
readonly memberIds: {
|
|
2908
|
+
readonly type: "array";
|
|
2909
|
+
readonly items: {
|
|
2910
|
+
readonly type: "string";
|
|
2911
|
+
};
|
|
2912
|
+
};
|
|
2913
|
+
readonly name: {
|
|
2914
|
+
readonly type: "string";
|
|
2915
|
+
};
|
|
2916
|
+
};
|
|
2917
|
+
};
|
|
2918
|
+
};
|
|
2919
|
+
};
|
|
2920
|
+
};
|
|
2921
|
+
/**
|
|
2922
|
+
* Tool definition in the Anthropic Messages API shape.
|
|
2923
|
+
*/
|
|
2924
|
+
type AnthropicToolDef = {
|
|
2925
|
+
name: string;
|
|
2926
|
+
description: string;
|
|
2927
|
+
input_schema: object;
|
|
2928
|
+
};
|
|
2929
|
+
/**
|
|
2930
|
+
* Returns op schemas wrapped as Anthropic Messages-API tool
|
|
2931
|
+
* definitions. Drop into the `tools` field of a `messages.create`
|
|
2932
|
+
* request to let an agent mutate the canvas directly.
|
|
2933
|
+
*
|
|
2934
|
+
* @example
|
|
2935
|
+
* const response = await anthropic.messages.create({
|
|
2936
|
+
* model: 'claude-opus-4-7',
|
|
2937
|
+
* tools: opSchemasAsAnthropicTools(),
|
|
2938
|
+
* messages: [{ role: 'user', content: 'Add a red sticky note' }],
|
|
2939
|
+
* })
|
|
2940
|
+
* for (const block of response.content) {
|
|
2941
|
+
* if (block.type === 'tool_use' && block.name.startsWith('canvas_')) {
|
|
2942
|
+
* const op = toOp(block.name, block.input)
|
|
2943
|
+
* store.applyOp(op)
|
|
2944
|
+
* }
|
|
2945
|
+
* }
|
|
2946
|
+
*/
|
|
2947
|
+
declare const opSchemasAsAnthropicTools: () => AnthropicToolDef[];
|
|
2948
|
+
|
|
2949
|
+
/**
|
|
2950
|
+
* Extension system — see ARCHITECTURE.md §13.9.
|
|
2951
|
+
*
|
|
2952
|
+
* The escape hatch for features the core won't ship: snap-to-grid,
|
|
2953
|
+
* alignment guides, minimap, autosave, AI plugins. Extensions get a
|
|
2954
|
+
* store handle + event subscription helper; they can mutate the store
|
|
2955
|
+
* (via the regular API), subscribe to events, and clean up on uninstall.
|
|
2956
|
+
*
|
|
2957
|
+
* Bare-bones by design — anything more (paint hooks, custom handles,
|
|
2958
|
+
* shortcut registration) is a v2 concern. Authors who need those today
|
|
2959
|
+
* can compose them inside `onInstall` against the store directly.
|
|
2960
|
+
*/
|
|
2961
|
+
type ExtensionApi = {
|
|
2962
|
+
/** The store the extension is attached to. */
|
|
2963
|
+
store: CanvasStore;
|
|
2964
|
+
/**
|
|
2965
|
+
* Subscribe to a store event with automatic cleanup on uninstall —
|
|
2966
|
+
* authors don't have to thread their own teardown.
|
|
2967
|
+
*/
|
|
2968
|
+
on<E extends StoreEventName>(event: E, cb: StoreEventHandler<E>): Unsubscribe;
|
|
2969
|
+
};
|
|
2970
|
+
type Extension = {
|
|
2971
|
+
/** Unique name; one extension per name per store. */
|
|
2972
|
+
name: string;
|
|
2973
|
+
/**
|
|
2974
|
+
* Called when the extension is installed. May return a cleanup
|
|
2975
|
+
* function that runs on uninstall (in addition to auto-unsubscribed
|
|
2976
|
+
* listeners registered via `api.on`).
|
|
2977
|
+
*/
|
|
2978
|
+
onInstall(api: ExtensionApi): undefined | (() => void);
|
|
2979
|
+
};
|
|
2980
|
+
/**
|
|
2981
|
+
* Defines an extension. Pure identity — exists for symmetry with
|
|
2982
|
+
* `defineNode` and to make call sites read nicely.
|
|
2983
|
+
*
|
|
2984
|
+
* @example
|
|
2985
|
+
* export const snapToGrid = defineExtension({
|
|
2986
|
+
* name: 'snap-to-grid',
|
|
2987
|
+
* onInstall: api => {
|
|
2988
|
+
* api.on('interaction', state => {
|
|
2989
|
+
* if (state.mode !== 'dragging') return
|
|
2990
|
+
* const snapped = {
|
|
2991
|
+
* x: Math.round(state.dragDelta.x / 20) * 20,
|
|
2992
|
+
* y: Math.round(state.dragDelta.y / 20) * 20,
|
|
2993
|
+
* }
|
|
2994
|
+
* api.store.setInteractionState({ dragDelta: snapped })
|
|
2995
|
+
* })
|
|
2996
|
+
* },
|
|
2997
|
+
* })
|
|
2998
|
+
*/
|
|
2999
|
+
declare const defineExtension: (ext: Extension) => Extension;
|
|
3000
|
+
/**
|
|
3001
|
+
* Installs an extension against a store. Returns an `uninstall()`
|
|
3002
|
+
* function. Re-installing the same name replaces the previous
|
|
3003
|
+
* instance.
|
|
3004
|
+
*
|
|
3005
|
+
* @example
|
|
3006
|
+
* useEffect(() => {
|
|
3007
|
+
* if (snapEnabled) return installExtension(store, snapToGrid)
|
|
3008
|
+
* }, [store, snapEnabled])
|
|
3009
|
+
*/
|
|
3010
|
+
declare const installExtension: (store: CanvasStore, ext: Extension) => Unsubscribe;
|
|
3011
|
+
/** Test / debug aid: list installed extension names for a store. */
|
|
3012
|
+
declare const installedExtensions: (store: CanvasStore) => string[];
|
|
3013
|
+
|
|
3014
|
+
/**
|
|
3015
|
+
* @canvas-harness/core
|
|
3016
|
+
*
|
|
3017
|
+
* Framework-agnostic core for the canvas-harness library.
|
|
3018
|
+
* See ARCHITECTURE.md and IMPLEMENTATION.md at the repo root.
|
|
3019
|
+
*
|
|
3020
|
+
* Phase 1 ships: types, ids, camera, spatial index, store skeleton, codec.
|
|
3021
|
+
*/
|
|
3022
|
+
declare const VERSION = "0.0.0";
|
|
3023
|
+
|
|
3024
|
+
export { type AnthropicToolDef, type Arrowhead, BEZIER_SEGMENTS, type BatchId, type BitmapCacheEntry, type BitmapCacheRequest, type BuiltInNodeType, CODE_BG_COLOR, CODE_BLOCK_MARGIN_Y, CODE_BLOCK_PADDING_X, CONTENT_HEIGHT_BUFFER, CONTENT_PADDING, type CameraState, type CanvasBackground, type CanvasBackgroundPattern, type CanvasStore, type CanvasSurface, type ClientId, type ClipResult, type ConflictRecord, type ContextEdge, type ContextNode, DEFAULT_BACKGROUND, DEFAULT_CAMERA, DEFAULT_HIGHLIGHT_COLOR, DEFAULT_HIGHLIGHT_COLOR_DARK, DEFAULT_MINIMAP_MAX_NODES, DEFAULT_STYLE, DEFAULT_TEXT_COLOR, type DeserializeOptions, type DragOriginal, type DrawTextOptions, EDGE_HANDLE_SLOP_PX, EDGE_HIT_SLOP_PX, type Edge, type EdgeEnd, type EdgeGeometry, EdgeGeometryCache, type EdgeHit, type EdgeId, type EdgeStyle, type EditorAdapter, type EditorAdapterFactory, type EditorAdapterMountOptions, type EstimateOptions, type ExportOptions, type Extension, type ExtensionApi, FONT_FAMILY_MAP, FONT_SIZE_MAP, type FontFamily, type FontSize, type FrameLoop, type FrameStats, type GetContextOptions, type Group, type GroupId, type Hit, type IdGenerator, type InlineType, type InteractionMode, type InteractionState, LINE_HEIGHT_MAP, LINK_COLOR, type LayoutLine, type LayoutOptions, MAX_ZOOM, MIN_ZOOM, type Migrator, type MinimapContentOptions, type Node, type NodeHit, type NodeId, type NodeType, type NodeTypeDef, type NodeTypeDefOptions, type Op, type OpBatch, type OpOrigin, PALM_REJECTION_GRACE_MS, type PalmRejectionState, type PathStyle, type PointerInfo, type PresenceEvent, type PresencePatch, type PresenceSlice, type PresenceState, type PrimitiveType, RESIZE_HANDLES, RESIZE_HANDLE_SIZE_PX, ROTATE_HANDLE_OFFSET_PX, ROTATE_HANDLE_RADIUS_PX, type RenderEnv, type Renderer, type RendererOptions, type ResizeHandle, SCHEMA_VERSION, type Scene, type SceneContextJson, type SchemaVersion, type SerializedClipboard, type SerializedScene, type Side, type SnapshotEnv, type SpatialId, type SpatialQuery, type SpatialResult, type StoreEventHandler, type StoreEventName, type StoreEvents, type StoreOptions, type StrokeStyle, type Style, type StyledRun, type SvgExportOptions, type SyncAdapter, type SyncAdapterCapabilities, type TextAlign, type TextStyle, type ThemeResolver, type Token, type Transform, UniformGrid, type Unsubscribe, VERSION, type Vec2, type WorldRect, applyCameraTransform, arrowheadLength, asBatchId, asClientId, asEdgeId, asGroupId, asNodeId, attachSync, autoRouteControls, clampEffectiveScale, clampZoom, clearMeasureCache, clearSurface, clearTextBitmapCache, clipSamples, computeAutoFitHeight, computeEdgeGeometry, copy, createCanvasStore, createDefaultTextareaEditor, createFrameLoop, createPalmRejectionState, createRenderer, cubicBezier, cubicBezierTangent, cut, defineExtension, defineNode, deserializeClipboard, detectConflicts, drawArrowhead, drawEdge, drawMinimapViewport, drawShape, drawTextToCanvas, drawWithNodeTransform, edgeAABBFromSamples, edgeLabelBoundsWorld, emptyPresenceState, estimateMarkdownContentHeight, exportSelection, exportSelectionSvg, exportViewport, fromSerialized, fullVisibleClipResult, getCanvasFont, getContentHeight, getContext, getDpr, getFontEpoch, getMarkdownLineHeightPx, getOrRenderTextBitmap, getPointAndTangentAtArcLength, getTextBitmapCacheSize, handleEnter, handleWorldPositions, hitTestAny, hitTestEdge, hitTestHandles, hitTestPoint, hitTestRotateHandle, idleInteractionState, inflateRect, insertLink, installExtension, installedExtensions, inverseBatch, inverseOp, isAttached, isCanvasHarnessClipboard, isDrawablePrimitive, isMoving, isNodeRemoteEditing, layoutTokens, makeIdGenerator, marqueeNodes, measureText, midpointToCubicControls, minimapScreenToWorld, nodeAABB, nodeIntersectsRect, nodeLocalToWorld, notePenActive, notePenInactive, opSchemas, opSchemasAsAnthropicTools, paintBackground, panByScreen, paste, pointInNode, projectEndToWorld, projectToNodeBoundary, quantizeDpr, quantizeZoom, randomClientId, rectContainsPoint, rectFromPoints, rectsIntersect, registerMigrator, renderMinimapContent, resolveColor, resolveOpacity, resolveRenderScale, resolveStrokeWidth, rotateHandleWorldPosition, rotateVecByAngle, sampleBezier, sampleSelfLoop, samplesFor, sceneBounds, screenToWorld, selfLoopGeometry, serializeSelection, setupSurface, shouldAutoFit, shouldRejectTouch, sideNormalLocal, sideOf, sizeSurface, storeToJSON, subscribeFontEpoch, tangentAtArcLength, toSerialized, toggleBold, toggleCode, toggleItalic, toggleStrike, toggleUnderline, tokenize, unionRects, viewportWorldRect, withAutoFitHeight, worldToNodeLocal, worldToScreen, worldViewport, worldViewportFromCamera, zoomAtScreenPoint };
|