f1ow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/dist/assets/elbowWorker-CUBh1uET.js +1 -0
  4. package/dist/assets/exportWorker-hU9uNZXJ.js +8 -0
  5. package/dist/assets/syncWorker.worker-CnlpNZ3k.js +6 -0
  6. package/dist/components/Canvas/CanvasElement.d.ts +42 -0
  7. package/dist/components/Canvas/ConnectionPoints.d.ts +13 -0
  8. package/dist/components/Canvas/GridLayer.d.ts +13 -0
  9. package/dist/components/Canvas/LinearElementHandles.d.ts +17 -0
  10. package/dist/components/Canvas/SelectionBox.d.ts +12 -0
  11. package/dist/components/Canvas/SelectionTransformer.d.ts +7 -0
  12. package/dist/components/ContextMenu/ContextMenu.d.ts +23 -0
  13. package/dist/components/StylePanel/StylePanel.d.ts +7 -0
  14. package/dist/components/Toolbar/Toolbar.d.ts +9 -0
  15. package/dist/components/shapes/ArrowShape.d.ts +18 -0
  16. package/dist/components/shapes/DiamondShape.d.ts +24 -0
  17. package/dist/components/shapes/EllipseShape.d.ts +24 -0
  18. package/dist/components/shapes/FreeDrawShape.d.ts +24 -0
  19. package/dist/components/shapes/ImageShape.d.ts +24 -0
  20. package/dist/components/shapes/LineShape.d.ts +18 -0
  21. package/dist/components/shapes/RectangleShape.d.ts +24 -0
  22. package/dist/components/shapes/TextShape.d.ts +23 -0
  23. package/dist/constants/index.d.ts +44 -0
  24. package/dist/f1ow-canvas.js +37725 -0
  25. package/dist/f1ow-canvas.umd.cjs +371 -0
  26. package/dist/hooks/useEfficientZoom.d.ts +14 -0
  27. package/dist/hooks/useElbowShapeFingerprint.d.ts +21 -0
  28. package/dist/hooks/useElbowWorker.d.ts +23 -0
  29. package/dist/hooks/useKeyboardShortcuts.d.ts +7 -0
  30. package/dist/hooks/useProgressiveRender.d.ts +52 -0
  31. package/dist/hooks/useSpatialIndex.d.ts +9 -0
  32. package/dist/hooks/useViewportCulling.d.ts +13 -0
  33. package/dist/lib/FlowCanvas.d.ts +4 -0
  34. package/dist/lib/FlowCanvasProps.d.ts +154 -0
  35. package/dist/lib/index.d.ts +57 -0
  36. package/dist/store/useCanvasStore.d.ts +127 -0
  37. package/dist/store/useLinearEditStore.d.ts +25 -0
  38. package/dist/types/index.d.ts +257 -0
  39. package/dist/utils/alignment.d.ts +34 -0
  40. package/dist/utils/arrowheads.d.ts +20 -0
  41. package/dist/utils/camera.d.ts +63 -0
  42. package/dist/utils/clipboard.d.ts +4 -0
  43. package/dist/utils/clone.d.ts +29 -0
  44. package/dist/utils/connection.d.ts +122 -0
  45. package/dist/utils/crdtPrep.d.ts +97 -0
  46. package/dist/utils/curve.d.ts +51 -0
  47. package/dist/utils/elbow.d.ts +110 -0
  48. package/dist/utils/elbowWorkerManager.d.ts +53 -0
  49. package/dist/utils/export.d.ts +17 -0
  50. package/dist/utils/exportWorkerManager.d.ts +25 -0
  51. package/dist/utils/fractionalIndex.d.ts +51 -0
  52. package/dist/utils/geometry.d.ts +49 -0
  53. package/dist/utils/id.d.ts +1 -0
  54. package/dist/utils/image.d.ts +52 -0
  55. package/dist/utils/performance.d.ts +65 -0
  56. package/dist/utils/roughness.d.ts +104 -0
  57. package/dist/utils/spatialIndex.d.ts +109 -0
  58. package/dist/utils/spatialSoA.d.ts +82 -0
  59. package/dist/workers/elbowWorker.d.ts +18 -0
  60. package/dist/workers/exportWorker.d.ts +18 -0
  61. package/package.json +80 -0
@@ -0,0 +1,257 @@
1
+ export type ToolType = 'select' | 'hand' | 'rectangle' | 'ellipse' | 'diamond' | 'line' | 'arrow' | 'freedraw' | 'text' | 'image' | 'eraser';
2
+ export type ElementType = 'rectangle' | 'ellipse' | 'diamond' | 'line' | 'arrow' | 'freedraw' | 'text' | 'image';
3
+ export type Arrowhead = 'arrow' | 'triangle' | 'triangle_outline' | 'circle' | 'circle_outline' | 'diamond' | 'diamond_outline' | 'bar' | 'crowfoot_one' | 'crowfoot_many' | 'crowfoot_one_or_many';
4
+ export type LineType = 'sharp' | 'curved' | 'elbow';
5
+ export interface ElementStyle {
6
+ strokeColor: string;
7
+ fillColor: string;
8
+ strokeWidth: number;
9
+ opacity: number;
10
+ strokeStyle: 'solid' | 'dashed' | 'dotted';
11
+ roughness: number;
12
+ fontSize: number;
13
+ fontFamily: string;
14
+ }
15
+ export interface BaseElement {
16
+ id: string;
17
+ type: ElementType;
18
+ x: number;
19
+ y: number;
20
+ width: number;
21
+ height: number;
22
+ rotation: number;
23
+ style: ElementStyle;
24
+ isLocked: boolean;
25
+ isVisible: boolean;
26
+ /** Bidirectional refs: arrows/text bound to this element */
27
+ boundElements: BoundElement[] | null;
28
+ /** Group hierarchy: element belongs to these groups (innermost first, outermost last) */
29
+ groupIds?: string[];
30
+ /**
31
+ * Fractional index for CRDT-compatible z-ordering.
32
+ * When present, elements are sorted by this field instead of array position.
33
+ * Uses lexicographic string comparison (e.g., "a0" < "a1" < "a2").
34
+ * Generated via `generateKeyBetween()` from `utils/fractionalIndex.ts`.
35
+ * @see {@link ../utils/fractionalIndex.ts}
36
+ */
37
+ sortOrder?: string;
38
+ /**
39
+ * Collaboration metadata for CRDT sync.
40
+ * Optional — only populated when collaboration features are active.
41
+ */
42
+ _meta?: ElementMeta;
43
+ }
44
+ /**
45
+ * Metadata for CRDT-based collaborative editing.
46
+ * Stored alongside each element but not rendered — used for
47
+ * conflict resolution, presence tracking, and version history.
48
+ */
49
+ export interface ElementMeta {
50
+ /** Unique ID of the user who last modified this element */
51
+ lastModifiedBy?: string;
52
+ /** Timestamp (ms since epoch) of the last modification */
53
+ lastModifiedAt?: number;
54
+ /** Monotonically increasing version counter */
55
+ version?: number;
56
+ }
57
+ /**
58
+ * Intent-based operations for CRDT-compatible history.
59
+ * Stores the *intent* of a change rather than before/after snapshots.
60
+ * Each operation is designed to be commutative and associative,
61
+ * making it suitable for conflict-free replication.
62
+ */
63
+ export type CanvasOperation = {
64
+ type: 'add';
65
+ element: CanvasElement;
66
+ } | {
67
+ type: 'delete';
68
+ elementId: string;
69
+ } | {
70
+ type: 'move';
71
+ elementId: string;
72
+ dx: number;
73
+ dy: number;
74
+ } | {
75
+ type: 'resize';
76
+ elementId: string;
77
+ width: number;
78
+ height: number;
79
+ x?: number;
80
+ y?: number;
81
+ } | {
82
+ type: 'style';
83
+ elementId: string;
84
+ changes: Partial<ElementStyle>;
85
+ } | {
86
+ type: 'rotate';
87
+ elementId: string;
88
+ rotation: number;
89
+ } | {
90
+ type: 'reorder';
91
+ elementId: string;
92
+ sortOrder: string;
93
+ } | {
94
+ type: 'updatePoints';
95
+ elementId: string;
96
+ points: number[];
97
+ } | {
98
+ type: 'setText';
99
+ elementId: string;
100
+ text: string;
101
+ } | {
102
+ type: 'batch';
103
+ operations: CanvasOperation[];
104
+ };
105
+ export interface RectangleElement extends BaseElement {
106
+ type: 'rectangle';
107
+ cornerRadius: number;
108
+ }
109
+ export interface EllipseElement extends BaseElement {
110
+ type: 'ellipse';
111
+ }
112
+ export interface DiamondElement extends BaseElement {
113
+ type: 'diamond';
114
+ }
115
+ export interface LineElement extends BaseElement {
116
+ type: 'line';
117
+ points: number[];
118
+ /** Routing type: sharp = straight segments, curved = smooth */
119
+ lineType: LineType;
120
+ /** Curvature ratio for curved mode (perpendicular offset / distance). Default 0.2. */
121
+ curvature?: number;
122
+ startBinding: Binding | null;
123
+ endBinding: Binding | null;
124
+ }
125
+ export interface ArrowElement extends BaseElement {
126
+ type: 'arrow';
127
+ points: number[];
128
+ /** Arrowhead at the start of the arrow (null = none) */
129
+ startArrowhead: Arrowhead | null;
130
+ /** Arrowhead at the end of the arrow (null = none) */
131
+ endArrowhead: Arrowhead | null;
132
+ /** @deprecated Use startArrowhead / endArrowhead. Kept for backward compat. */
133
+ startArrow?: boolean;
134
+ /** @deprecated Use startArrowhead / endArrowhead. Kept for backward compat. */
135
+ endArrow?: boolean;
136
+ /** Routing type: sharp = straight segments, curved = smooth Bézier */
137
+ lineType: LineType;
138
+ /** Curvature ratio for curved mode (perpendicular offset / distance). Default 0.2. */
139
+ curvature?: number;
140
+ startBinding: Binding | null;
141
+ endBinding: Binding | null;
142
+ }
143
+ export interface FreeDrawElement extends BaseElement {
144
+ type: 'freedraw';
145
+ points: number[];
146
+ }
147
+ export type TextAlign = 'left' | 'center' | 'right';
148
+ export type VerticalAlign = 'top' | 'middle' | 'bottom';
149
+ export type ImageScaleMode = 'fit' | 'fill' | 'stretch';
150
+ /** Optional crop region for images (values in pixels on the original image) */
151
+ export interface ImageCrop {
152
+ x: number;
153
+ y: number;
154
+ width: number;
155
+ height: number;
156
+ }
157
+ export interface TextElement extends BaseElement {
158
+ type: 'text';
159
+ text: string;
160
+ /** ID of the container shape this text is bound to (null = standalone) */
161
+ containerId: string | null;
162
+ /** Horizontal text alignment inside container */
163
+ textAlign: TextAlign;
164
+ /** Vertical text alignment inside container */
165
+ verticalAlign: VerticalAlign;
166
+ }
167
+ export interface ImageElement extends BaseElement {
168
+ type: 'image';
169
+ /** Base64 data URL or external URL of the image */
170
+ src: string;
171
+ /** Original intrinsic width of the loaded image (px) */
172
+ naturalWidth: number;
173
+ /** Original intrinsic height of the loaded image (px) */
174
+ naturalHeight: number;
175
+ /** How the image fits inside its bounding box */
176
+ scaleMode: ImageScaleMode;
177
+ /** Optional crop region on the original image */
178
+ crop: ImageCrop | null;
179
+ /** Border radius (px) for rounded image corners */
180
+ cornerRadius: number;
181
+ /** Alt / label text used in SVG export and accessibility */
182
+ alt: string;
183
+ }
184
+ export type CanvasElement = RectangleElement | EllipseElement | DiamondElement | LineElement | ArrowElement | FreeDrawElement | TextElement | ImageElement;
185
+ export interface Point {
186
+ x: number;
187
+ y: number;
188
+ }
189
+ export interface ViewportState {
190
+ x: number;
191
+ y: number;
192
+ scale: number;
193
+ }
194
+ export interface SelectionBox {
195
+ x: number;
196
+ y: number;
197
+ width: number;
198
+ height: number;
199
+ }
200
+ /**
201
+ * @deprecated Use fixedPoint binding instead. Kept for backward-compat exports.
202
+ */
203
+ export type ConnectionAnchor = 'top' | 'bottom' | 'left' | 'right' | 'center';
204
+ /** Bidirectional reference stored on shapes to track bound connectors/text */
205
+ export type BoundElement = {
206
+ id: string;
207
+ type: 'arrow' | 'line' | 'text';
208
+ };
209
+ /** Describes one end of a connector (arrow/line) bound to a shape */
210
+ export interface Binding {
211
+ /** The element this end is connected to */
212
+ elementId: string;
213
+ /**
214
+ * Continuous attachment ratio [0-1, 0-1] on target's bounding box.
215
+ * e.g. [0.5, 0] = top center, [1, 0.5] = right center, [0.3, 0.7] = arbitrary.
216
+ */
217
+ fixedPoint: [number, number];
218
+ /** Gap between the edge of the shape and the arrow tip (px) */
219
+ gap: number;
220
+ /**
221
+ * Whether to bind to the exact fixedPoint position, or to the shape center.
222
+ * When false (default), the arrow connects from/to the shape's center,
223
+ * producing cleaner visuals with auto-routed connections.
224
+ * When true, the arrow connects to the exact fixedPoint on the shape.
225
+ */
226
+ isPrecise?: boolean;
227
+ }
228
+ /** Snap candidate returned while drawing */
229
+ export interface SnapTarget {
230
+ elementId: string;
231
+ /** Continuous attachment point ratio on target bbox */
232
+ fixedPoint: [number, number];
233
+ /** Computed edge-point position in world coordinates */
234
+ position: Point;
235
+ /**
236
+ * Whether this snap uses precise (edge) binding or center binding.
237
+ * Automatically determined by cursor proximity to shape edge vs center.
238
+ * - `true`: cursor is near the edge → attaches to that specific edge point
239
+ * - `false`: cursor is in the center zone → attaches to center for auto-routing
240
+ */
241
+ isPrecise: boolean;
242
+ }
243
+ /** State for editing individual points of a line/arrow element */
244
+ export interface LinearEditState {
245
+ /** ID of the element currently being edited */
246
+ elementId: string;
247
+ /** Whether the editor is in full editing mode (point manipulation) */
248
+ isEditing: boolean;
249
+ /** Indices of currently selected points */
250
+ selectedPointIndices: number[];
251
+ /** Index of the point the cursor is hovering over (-1 = none) */
252
+ hoveredPointIndex: number;
253
+ /** Index of the midpoint the cursor is hovering over (null = none) */
254
+ hoveredMidpointIndex: number | null;
255
+ /** Whether a point is currently being dragged */
256
+ isDraggingPoint: boolean;
257
+ }
@@ -0,0 +1,34 @@
1
+ import { CanvasElement } from '../types';
2
+ /** A single alignment guide line */
3
+ export interface AlignGuide {
4
+ /** 'h' = horizontal line (y aligned), 'v' = vertical line (x aligned) */
5
+ orientation: 'h' | 'v';
6
+ /** Position on the align axis (x for vertical, y for horizontal) */
7
+ position: number;
8
+ /** Guide extent — start coordinate on cross axis */
9
+ start: number;
10
+ /** Guide extent — end coordinate on cross axis */
11
+ end: number;
12
+ }
13
+ export interface AlignResult {
14
+ /** Snapped x (undefined = no x snap) */
15
+ x?: number;
16
+ /** Snapped y (undefined = no y snap) */
17
+ y?: number;
18
+ /** Guides to render */
19
+ guides: AlignGuide[];
20
+ }
21
+ /**
22
+ * Compute alignment guides for a moving element against all other elements.
23
+ *
24
+ * @param moving - The element being dragged (with current position)
25
+ * @param others - All other canvas elements to compare against
26
+ * @param threshold - Pixel distance for snapping (default 5)
27
+ * @returns Snapped x/y offsets and guide lines to render
28
+ */
29
+ export declare function computeAlignGuides(movingBounds: {
30
+ x: number;
31
+ y: number;
32
+ width: number;
33
+ height: number;
34
+ }, others: CanvasElement[], excludeIds: Set<string>, threshold?: number): AlignResult;
@@ -0,0 +1,20 @@
1
+ import { Arrowhead, Point } from '../types';
2
+ import { Context } from 'konva/lib/Context';
3
+ export declare function arrowheadSize(strokeWidth: number): number;
4
+ /**
5
+ * Draw an arrowhead of the given type at `tip`, with the arrow
6
+ * body arriving from direction `from`.
7
+ *
8
+ * @param ctx Konva canvas context
9
+ * @param type Arrowhead variant
10
+ * @param tip Position of the arrowhead tip (world-local coords)
11
+ * @param from Position the arrow body is coming *from*
12
+ * @param size Size of the arrowhead (pixel length)
13
+ * @param stroke Stroke color
14
+ * @param strokeW Stroke width
15
+ */
16
+ export declare function drawArrowhead(ctx: Context, type: Arrowhead, tip: Point, from: Point, size: number, stroke: string, strokeW: number): void;
17
+ /**
18
+ * Get pairs of { x, y } from flat points array.
19
+ */
20
+ export declare function flatToPoints(pts: number[]): Point[];
@@ -0,0 +1,63 @@
1
+ import { ViewportState, CanvasElement } from '../types';
2
+ import { AABB } from './performance';
3
+ /** Predefined zoom steps for smooth discrete zooming */
4
+ export declare const ZOOM_STEPS: readonly [0.1, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4, 5];
5
+ /** Default animation duration in ms */
6
+ export declare const DEFAULT_ANIMATION_DURATION = 280;
7
+ export interface ZoomAtPointOptions {
8
+ /** Current viewport state */
9
+ viewport: ViewportState;
10
+ /** Screen-space point to zoom toward (e.g. cursor position) */
11
+ point: {
12
+ x: number;
13
+ y: number;
14
+ };
15
+ /** Target zoom scale */
16
+ targetScale: number;
17
+ }
18
+ /**
19
+ * Compute new viewport state that zooms to `targetScale` while keeping
20
+ * `point` (in screen coordinates) stationary — the standard
21
+ * "zoom toward cursor" algorithm.
22
+ */
23
+ export declare function zoomAtPoint({ viewport, point, targetScale }: ZoomAtPointOptions): ViewportState;
24
+ /**
25
+ * Get the next predefined zoom step above the current scale.
26
+ * Falls back to MAX_ZOOM if already beyond the last step.
27
+ */
28
+ export declare function getNextZoomStep(currentScale: number): number;
29
+ /**
30
+ * Get the next predefined zoom step below the current scale.
31
+ * Falls back to MIN_ZOOM if already below the first step.
32
+ */
33
+ export declare function getPrevZoomStep(currentScale: number): number;
34
+ /**
35
+ * Compute the combined bounding box for a set of elements.
36
+ * Returns null if the array is empty.
37
+ */
38
+ export declare function getElementsBounds(elements: CanvasElement[]): AABB | null;
39
+ export interface ZoomToFitOptions {
40
+ /** Pixel padding around the content */
41
+ padding?: number;
42
+ /** Maximum zoom level (avoid zooming too far in on tiny content) */
43
+ maxZoom?: number;
44
+ }
45
+ /**
46
+ * Compute viewport state that fits the given bounding box within
47
+ * a stage of the given pixel dimensions.
48
+ */
49
+ export declare function computeZoomToFit(bounds: AABB, stageWidth: number, stageHeight: number, options?: ZoomToFitOptions): ViewportState;
50
+ /**
51
+ * Smoothly animate the viewport from its current state to a target state.
52
+ *
53
+ * @param from - Start viewport
54
+ * @param to - Target viewport
55
+ * @param setViewport - Store setter to apply intermediate states
56
+ * @param duration - Animation duration in ms (default 280)
57
+ * @returns A cancel function
58
+ */
59
+ export declare function animateViewport(from: ViewportState, to: ViewportState, setViewport: (v: Partial<ViewportState>) => void, duration?: number): () => void;
60
+ /**
61
+ * Cancel any in-progress viewport animation.
62
+ */
63
+ export declare function cancelViewportAnimation(): void;
@@ -0,0 +1,4 @@
1
+ import { CanvasElement } from '../types';
2
+ export declare function setClipboard(elements: CanvasElement[]): void;
3
+ export declare function getClipboard(): CanvasElement[];
4
+ export declare function hasClipboardContent(): boolean;
@@ -0,0 +1,29 @@
1
+ import { CanvasElement } from '../types';
2
+ /**
3
+ * Clone a set of elements with proper id remapping for all cross-references.
4
+ *
5
+ * Handles:
6
+ * - Auto-including bound text of containers
7
+ * - Generating new ids and building old→new id map
8
+ * - Remapping containerId, boundElements, startBinding, endBinding
9
+ * - Remapping groupIds (generates new groupIds so clones form independent groups)
10
+ * - Deep-cloning nested objects (style, points, bindings) to prevent shared references
11
+ * - Nullifying dangling refs (when referenced element not in clone set)
12
+ *
13
+ * @param originals - Elements to clone (e.g., selected or clipboard)
14
+ * @param allElements - Full element list for looking up missing bound text
15
+ * (pass `originals` itself if source is clipboard)
16
+ * @param offset - Positional offset for the clones (default 20)
17
+ * @returns { clones, idMap, selectedCloneIds }
18
+ */
19
+ export declare function cloneAndRemapElements(originals: CanvasElement[], allElements: CanvasElement[], offset?: number): {
20
+ clones: CanvasElement[];
21
+ idMap: Map<string, string>;
22
+ selectedCloneIds: string[];
23
+ };
24
+ /**
25
+ * Gather elements for copy, auto-including:
26
+ * - Bound text of containers
27
+ * - All group members of selected elements' groups
28
+ */
29
+ export declare function gatherElementsForCopy(selectedIds: string[], allElements: CanvasElement[]): CanvasElement[];
@@ -0,0 +1,122 @@
1
+ import { CanvasElement, Point, ConnectionAnchor, Binding, SnapTarget, ArrowElement, LineElement, BoundElement } from '../types';
2
+ /** Whether an element can be a connection target */
3
+ export declare function isConnectable(el: CanvasElement): boolean;
4
+ /** Get the 5 named anchor positions for a bounding-box shape */
5
+ export declare function getConnectionPoints(el: CanvasElement): Record<ConnectionAnchor, Point>;
6
+ /**
7
+ * @deprecated Use fixedPoint-based getEdgePointFromFixedPoint() instead.
8
+ */
9
+ export declare function getAnchorPosition(el: CanvasElement, anchor: ConnectionAnchor): Point;
10
+ /**
11
+ * Compute the appropriate gap between a connector and a shape edge,
12
+ * based on both the connector's stroke width and a base offset.
13
+ * This provides a small gap between the arrow tip and the shape edge.
14
+ */
15
+ export declare function computeBindingGap(connectorStrokeWidth: number): number;
16
+ /**
17
+ * Compute a fixedPoint [0-1, 0-1] ratio from a world-space point
18
+ * relative to a shape's bounding box, accounting for rotation.
19
+ */
20
+ export declare function computeFixedPoint(el: CanvasElement, worldPt: Point): [number, number];
21
+ /**
22
+ * Convert a fixedPoint ratio back to a world-space target point,
23
+ * then compute the edge intersection from center to that target.
24
+ */
25
+ export declare function getEdgePointFromFixedPoint(el: CanvasElement, fixedPoint: [number, number], gap?: number): Point;
26
+ /**
27
+ * Given a shape and an external world-space point, compute the point on
28
+ * the shape's perimeter closest to `toward`.
29
+ *
30
+ * Handles shape rotation: transforms `toward` into the shape's local
31
+ * coordinate system, computes the local edge intersection, then
32
+ * transforms back to world space.
33
+ */
34
+ export declare function getEdgePoint(el: CanvasElement, toward: Point, gap?: number): Point;
35
+ /**
36
+ * Find the nearest connectable shape using **shape-aware** hit detection.
37
+ * The shape perimeter (+ padding) acts as a drop zone.
38
+ *
39
+ * Uses two separate distance thresholds for edge vs center binding:
40
+ *
41
+ * 1. **Edge zone** — cursor is OUTSIDE the shape but within
42
+ * `edgeOuterThreshold` px of the perimeter, OR cursor is INSIDE
43
+ * the shape but within `EDGE_INNER_BAND` px of the nearest edge.
44
+ * → Produces precise (edge) binding at the exact cursor position.
45
+ *
46
+ * 2. **Center zone** — cursor is INSIDE the shape and farther than
47
+ * `EDGE_INNER_BAND` px from any edge.
48
+ * → Produces center binding (fixedPoint [0.5, 0.5]) for clean
49
+ * auto-routing.
50
+ *
51
+ * The pixel-based inner band ensures consistent edge/center zones
52
+ * regardless of shape size.
53
+ *
54
+ * @param pos Current pointer position (canvas coords)
55
+ * @param elements All elements on canvas
56
+ * @param edgeOuterThreshold How far outside the shape perimeter to detect (px).
57
+ * Defaults to `EDGE_SNAP_THRESHOLD`.
58
+ * @param excludeIds Element IDs to skip (e.g. the connector itself)
59
+ * @param toward Direction for edge-point calculation; defaults to `pos`
60
+ * @param forcePrecise Force precise mode (skip auto-detection).
61
+ * Use `undefined` for auto-detection (recommended).
62
+ * @param gap Gap between connector endpoint and shape edge (px).
63
+ * Defaults to BOUND_ARROW_OFFSET. Pass `computeBindingGap(strokeWidth)`
64
+ * for a preview that matches the final binding gap.
65
+ * @param prevIsPrecise Previous snap's `isPrecise` value for hysteresis.
66
+ * Pass `undefined` for first call (no hysteresis).
67
+ * Pass the previous result's `isPrecise` during drag
68
+ * to prevent edge/center mode flickering at boundary.
69
+ */
70
+ export declare function findNearestSnapTarget(pos: Point, elements: CanvasElement[], edgeOuterThreshold?: number, excludeIds?: Set<string>, toward?: Point, forcePrecise?: boolean, gap?: number, prevIsPrecise?: boolean): SnapTarget | null;
71
+ /** Check if a line/arrow element has any bindings */
72
+ export declare function hasBinding(el: LineElement | ArrowElement): boolean;
73
+ /**
74
+ * Recompute start/end points for a bound line/arrow based on current
75
+ * positions of the connected shapes. Called when a shape is dragged.
76
+ *
77
+ * Supports N-point connectors: only the first and last point pairs are
78
+ * adjusted to follow bindings; intermediate waypoints are preserved.
79
+ *
80
+ * Uses a two-pass approach for double-bound connectors to resolve the
81
+ * edge-point interdependency:
82
+ * 1. First pass: compute rough edge points using shape centers as direction.
83
+ * 2. Second pass: refine using the result from pass 1 as direction.
84
+ *
85
+ * Also respects `isPrecise`: when false, uses center as anchor direction;
86
+ * when true, uses the fixedPoint on the shape's bbox.
87
+ *
88
+ * Returns the updated `x`, `y`, `points` values to merge into the element.
89
+ */
90
+ export declare function recomputeBoundPoints(connector: LineElement | ArrowElement, allElements: CanvasElement[]): Partial<LineElement | ArrowElement> | null;
91
+ /**
92
+ * Find all connector (line/arrow) elements that reference `elementId`
93
+ * in their startBinding or endBinding.
94
+ */
95
+ export declare function findConnectorsForElement(elementId: string, elements: CanvasElement[]): (LineElement | ArrowElement)[];
96
+ /**
97
+ * Clear any binding that references a deleted element.
98
+ * Also clears boundElements references on remaining shapes.
99
+ * Returns updated elements array (only changed elements are cloned).
100
+ */
101
+ export declare function clearBindingsForDeletedElements(deletedIds: Set<string>, elements: CanvasElement[]): CanvasElement[];
102
+ /**
103
+ * Add a bound-element reference to a shape's boundElements array.
104
+ * Returns the updated shape — does NOT mutate in place.
105
+ */
106
+ export declare function addBoundElement(shape: CanvasElement, ref: BoundElement): CanvasElement;
107
+ /**
108
+ * Remove a bound-element reference from a shape's boundElements array.
109
+ * Returns the updated shape — does NOT mutate in place.
110
+ */
111
+ export declare function removeBoundElement(shape: CanvasElement, refId: string): CanvasElement;
112
+ /**
113
+ * Synchronize bidirectional boundElements when a connector's bindings change.
114
+ * Call this after updating a connector's startBinding / endBinding.
115
+ *
116
+ * @param connectorId The connector (arrow/line) id
117
+ * @param connectorType 'arrow' | 'line'
118
+ * @param oldBinding Previous binding (or null)
119
+ * @param newBinding New binding (or null)
120
+ * @param updateElement Callback to persist the shape update
121
+ */
122
+ export declare function syncBoundElements(connectorId: string, connectorType: 'arrow' | 'line', oldBinding: Binding | null, newBinding: Binding | null, elements: CanvasElement[], updateElement: (id: string, updates: Partial<CanvasElement>) => void): void;
@@ -0,0 +1,97 @@
1
+ import { CanvasElement, CanvasOperation, ElementStyle } from '../types';
2
+ /**
3
+ * Timestamped operation entry for the operation log.
4
+ * The operation log is an append-only list that can be shared
5
+ * across replicas for CRDT synchronization.
6
+ */
7
+ export interface OperationEntry {
8
+ /** Unique operation ID (for deduplication across replicas) */
9
+ id: string;
10
+ /** The operation itself */
11
+ operation: CanvasOperation;
12
+ /** Unix timestamp (ms) when the operation was created */
13
+ timestamp: number;
14
+ /** ID of the user/client that created this operation */
15
+ clientId: string;
16
+ }
17
+ /**
18
+ * Operation log — append-only list of operations.
19
+ * In a full CRDT implementation, this would be synced via Yjs or similar.
20
+ */
21
+ export declare class OperationLog {
22
+ private _entries;
23
+ private _maxEntries;
24
+ constructor(maxEntries?: number);
25
+ /** Append an operation to the log */
26
+ push(entry: OperationEntry): void;
27
+ /** Get all entries (read-only) */
28
+ get entries(): readonly OperationEntry[];
29
+ /** Get entries since a specific timestamp */
30
+ entriesSince(timestamp: number): OperationEntry[];
31
+ /** Get entries by client ID */
32
+ entriesByClient(clientId: string): OperationEntry[];
33
+ /** Clear all entries */
34
+ clear(): void;
35
+ /** Number of entries */
36
+ get length(): number;
37
+ }
38
+ /**
39
+ * Create an 'add' operation for a new element.
40
+ */
41
+ export declare function opAdd(element: CanvasElement): CanvasOperation;
42
+ /**
43
+ * Create a 'delete' operation.
44
+ */
45
+ export declare function opDelete(elementId: string): CanvasOperation;
46
+ /**
47
+ * Create a 'move' operation (delta-based for commutativity).
48
+ */
49
+ export declare function opMove(elementId: string, dx: number, dy: number): CanvasOperation;
50
+ /**
51
+ * Create a 'resize' operation.
52
+ */
53
+ export declare function opResize(elementId: string, width: number, height: number, x?: number, y?: number): CanvasOperation;
54
+ /**
55
+ * Create a 'style' operation (partial style update).
56
+ */
57
+ export declare function opStyle(elementId: string, changes: Partial<ElementStyle>): CanvasOperation;
58
+ /**
59
+ * Create a 'rotate' operation.
60
+ */
61
+ export declare function opRotate(elementId: string, rotation: number): CanvasOperation;
62
+ /**
63
+ * Create a 'reorder' operation (z-order change via fractional index).
64
+ */
65
+ export declare function opReorder(elementId: string, sortOrder: string): CanvasOperation;
66
+ /**
67
+ * Create an 'updatePoints' operation (for line/arrow/freedraw).
68
+ */
69
+ export declare function opUpdatePoints(elementId: string, points: number[]): CanvasOperation;
70
+ /**
71
+ * Create a 'setText' operation (for text elements).
72
+ */
73
+ export declare function opSetText(elementId: string, text: string): CanvasOperation;
74
+ /**
75
+ * Create a batch operation (group multiple ops into one transaction).
76
+ */
77
+ export declare function opBatch(...operations: CanvasOperation[]): CanvasOperation;
78
+ /**
79
+ * Apply a single operation to an elements array.
80
+ * Returns a new array (immutable — does not mutate input).
81
+ *
82
+ * This is the foundation for:
83
+ * - Replaying operation logs
84
+ * - Applying remote operations in CRDT sync
85
+ * - Undo/redo via inverse operations
86
+ */
87
+ export declare function applyOperation(elements: CanvasElement[], op: CanvasOperation): CanvasElement[];
88
+ /**
89
+ * Detect what operations occurred between two element states.
90
+ * Useful for converting imperative store mutations into operations
91
+ * (bridge between current Zustand pattern and CRDT operations).
92
+ *
93
+ * @param before - Elements before the change
94
+ * @param after - Elements after the change
95
+ * @returns Array of operations that transform `before` into `after`
96
+ */
97
+ export declare function detectOperations(before: CanvasElement[], after: CanvasElement[]): CanvasOperation[];
@@ -0,0 +1,51 @@
1
+ import { Point } from '../types';
2
+ /** Perpendicular offset as a fraction of the start→end distance */
3
+ export declare const CURVE_RATIO = 0.2;
4
+ /**
5
+ * Compute the quadratic Bézier control point for a curved line
6
+ * defined by start and end points.
7
+ *
8
+ * The control point sits at the midpoint of start→end, offset
9
+ * perpendicularly by `ratio × distance(start, end)`.
10
+ * Positive ratio = offset to the left (looking from start to end).
11
+ */
12
+ export declare function computeCurveControlPoint(start: Point, end: Point, ratio?: number): Point;
13
+ /**
14
+ * Evaluate a quadratic Bézier at parameter t ∈ [0, 1].
15
+ * B(t) = (1-t)² P₀ + 2(1-t)t P₁ + t² P₂
16
+ */
17
+ export declare function quadBezierAt(p0: Point, cp: Point, p2: Point, t: number): Point;
18
+ /**
19
+ * Compute the tangent (derivative) of a quadratic Bézier at parameter t.
20
+ * B'(t) = 2(1-t)(P₁ - P₀) + 2t(P₂ - P₁)
21
+ * Returns a non-normalized direction vector.
22
+ */
23
+ export declare function quadBezierTangent(p0: Point, cp: Point, p2: Point, t: number): Point;
24
+ /**
25
+ * Draw a quadratic Bézier curve path on a canvas 2D context.
26
+ * Does NOT call stroke() — caller decides stroke/fill.
27
+ */
28
+ export declare function drawQuadBezierPath(ctx: CanvasRenderingContext2D | {
29
+ beginPath: () => void;
30
+ moveTo: (x: number, y: number) => void;
31
+ quadraticCurveTo: (cpx: number, cpy: number, x: number, y: number) => void;
32
+ }, start: Point, cp: Point, end: Point): void;
33
+ /**
34
+ * Compute the "arriving direction" point for an arrowhead.
35
+ * For start arrowhead: direction is tangent at t=0 pointing inward (from curve toward start).
36
+ * For end arrowhead: direction is tangent at t=1 pointing inward (from curve toward end).
37
+ *
38
+ * Returns the "previous" point that drawArrowhead expects — i.e.
39
+ * a point offset along the tangent away from the tip.
40
+ */
41
+ export declare function curveArrowPrev(start: Point, cp: Point, end: Point, atEnd: boolean): Point;
42
+ /**
43
+ * Compute the curvature ratio from a dragged control point position.
44
+ *
45
+ * Given the start/end endpoints of the line and the world position where
46
+ * the user has dragged the curve handle, computes the signed perpendicular
47
+ * offset ratio (curvature) that reproduces that position.
48
+ *
49
+ * Positive = left side (looking from start → end), negative = right side.
50
+ */
51
+ export declare function curvatureFromDragPoint(start: Point, end: Point, dragWorld: Point): number;