@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.
@@ -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 };