@oh-just-another/state 0.57.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 (269) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +60 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/actions/actionClipboard.d.ts +6 -0
  6. package/dist/actions/actionClipboard.d.ts.map +1 -0
  7. package/dist/actions/actionClipboard.js +26 -0
  8. package/dist/actions/actionClipboard.js.map +1 -0
  9. package/dist/actions/actionGrouping.d.ts +5 -0
  10. package/dist/actions/actionGrouping.d.ts.map +1 -0
  11. package/dist/actions/actionGrouping.js +26 -0
  12. package/dist/actions/actionGrouping.js.map +1 -0
  13. package/dist/actions/actionHistory.d.ts +5 -0
  14. package/dist/actions/actionHistory.d.ts.map +1 -0
  15. package/dist/actions/actionHistory.js +25 -0
  16. package/dist/actions/actionHistory.js.map +1 -0
  17. package/dist/actions/actionKeyboard.d.ts +3 -0
  18. package/dist/actions/actionKeyboard.d.ts.map +1 -0
  19. package/dist/actions/actionKeyboard.js +130 -0
  20. package/dist/actions/actionKeyboard.js.map +1 -0
  21. package/dist/actions/actionLayout.d.ts +3 -0
  22. package/dist/actions/actionLayout.d.ts.map +1 -0
  23. package/dist/actions/actionLayout.js +65 -0
  24. package/dist/actions/actionLayout.js.map +1 -0
  25. package/dist/actions/actionMode.d.ts +13 -0
  26. package/dist/actions/actionMode.d.ts.map +1 -0
  27. package/dist/actions/actionMode.js +116 -0
  28. package/dist/actions/actionMode.js.map +1 -0
  29. package/dist/actions/actionSelection.d.ts +9 -0
  30. package/dist/actions/actionSelection.d.ts.map +1 -0
  31. package/dist/actions/actionSelection.js +57 -0
  32. package/dist/actions/actionSelection.js.map +1 -0
  33. package/dist/actions/actionView.d.ts +7 -0
  34. package/dist/actions/actionView.d.ts.map +1 -0
  35. package/dist/actions/actionView.js +14 -0
  36. package/dist/actions/actionView.js.map +1 -0
  37. package/dist/actions/actionZOrder.d.ts +7 -0
  38. package/dist/actions/actionZOrder.d.ts.map +1 -0
  39. package/dist/actions/actionZOrder.js +42 -0
  40. package/dist/actions/actionZOrder.js.map +1 -0
  41. package/dist/actions/actionZoom.d.ts +8 -0
  42. package/dist/actions/actionZoom.d.ts.map +1 -0
  43. package/dist/actions/actionZoom.js +61 -0
  44. package/dist/actions/actionZoom.js.map +1 -0
  45. package/dist/actions/index.d.ts +12 -0
  46. package/dist/actions/index.d.ts.map +1 -0
  47. package/dist/actions/index.js +11 -0
  48. package/dist/actions/index.js.map +1 -0
  49. package/dist/actions/registry.d.ts +68 -0
  50. package/dist/actions/registry.d.ts.map +1 -0
  51. package/dist/actions/registry.js +300 -0
  52. package/dist/actions/registry.js.map +1 -0
  53. package/dist/actions/types.d.ts +103 -0
  54. package/dist/actions/types.d.ts.map +1 -0
  55. package/dist/actions/types.js +10 -0
  56. package/dist/actions/types.js.map +1 -0
  57. package/dist/animation-tick.d.ts +28 -0
  58. package/dist/animation-tick.d.ts.map +1 -0
  59. package/dist/animation-tick.js +50 -0
  60. package/dist/animation-tick.js.map +1 -0
  61. package/dist/auto-compact.d.ts +27 -0
  62. package/dist/auto-compact.d.ts.map +1 -0
  63. package/dist/auto-compact.js +46 -0
  64. package/dist/auto-compact.js.map +1 -0
  65. package/dist/auto-layout-scheduler.d.ts +50 -0
  66. package/dist/auto-layout-scheduler.d.ts.map +1 -0
  67. package/dist/auto-layout-scheduler.js +94 -0
  68. package/dist/auto-layout-scheduler.js.map +1 -0
  69. package/dist/built-in-handlers.d.ts +24 -0
  70. package/dist/built-in-handlers.d.ts.map +1 -0
  71. package/dist/built-in-handlers.js +210 -0
  72. package/dist/built-in-handlers.js.map +1 -0
  73. package/dist/clipboard.d.ts +51 -0
  74. package/dist/clipboard.d.ts.map +1 -0
  75. package/dist/clipboard.js +105 -0
  76. package/dist/clipboard.js.map +1 -0
  77. package/dist/constants.d.ts +544 -0
  78. package/dist/constants.d.ts.map +1 -0
  79. package/dist/constants.js +537 -0
  80. package/dist/constants.js.map +1 -0
  81. package/dist/dom-events.d.ts +13 -0
  82. package/dist/dom-events.d.ts.map +1 -0
  83. package/dist/dom-events.js +70 -0
  84. package/dist/dom-events.js.map +1 -0
  85. package/dist/editor/anchor-points.d.ts +22 -0
  86. package/dist/editor/anchor-points.d.ts.map +1 -0
  87. package/dist/editor/anchor-points.js +28 -0
  88. package/dist/editor/anchor-points.js.map +1 -0
  89. package/dist/editor/applies/create.d.ts +60 -0
  90. package/dist/editor/applies/create.d.ts.map +1 -0
  91. package/dist/editor/applies/create.js +107 -0
  92. package/dist/editor/applies/create.js.map +1 -0
  93. package/dist/editor/applies/edge.d.ts +32 -0
  94. package/dist/editor/applies/edge.d.ts.map +1 -0
  95. package/dist/editor/applies/edge.js +44 -0
  96. package/dist/editor/applies/edge.js.map +1 -0
  97. package/dist/editor/applies/link-move.d.ts +53 -0
  98. package/dist/editor/applies/link-move.d.ts.map +1 -0
  99. package/dist/editor/applies/link-move.js +160 -0
  100. package/dist/editor/applies/link-move.js.map +1 -0
  101. package/dist/editor/applies/move.d.ts +38 -0
  102. package/dist/editor/applies/move.d.ts.map +1 -0
  103. package/dist/editor/applies/move.js +83 -0
  104. package/dist/editor/applies/move.js.map +1 -0
  105. package/dist/editor/applies/resize.d.ts +68 -0
  106. package/dist/editor/applies/resize.d.ts.map +1 -0
  107. package/dist/editor/applies/resize.js +199 -0
  108. package/dist/editor/applies/resize.js.map +1 -0
  109. package/dist/editor/applies/selection.d.ts +34 -0
  110. package/dist/editor/applies/selection.d.ts.map +1 -0
  111. package/dist/editor/applies/selection.js +65 -0
  112. package/dist/editor/applies/selection.js.map +1 -0
  113. package/dist/editor/applies/snap-grid.d.ts +45 -0
  114. package/dist/editor/applies/snap-grid.d.ts.map +1 -0
  115. package/dist/editor/applies/snap-grid.js +90 -0
  116. package/dist/editor/applies/snap-grid.js.map +1 -0
  117. package/dist/editor/container-ops.d.ts +74 -0
  118. package/dist/editor/container-ops.d.ts.map +1 -0
  119. package/dist/editor/container-ops.js +262 -0
  120. package/dist/editor/container-ops.js.map +1 -0
  121. package/dist/editor/event-fanout.d.ts +48 -0
  122. package/dist/editor/event-fanout.d.ts.map +1 -0
  123. package/dist/editor/event-fanout.js +53 -0
  124. package/dist/editor/event-fanout.js.map +1 -0
  125. package/dist/editor/gesture-tx.d.ts +77 -0
  126. package/dist/editor/gesture-tx.d.ts.map +1 -0
  127. package/dist/editor/gesture-tx.js +93 -0
  128. package/dist/editor/gesture-tx.js.map +1 -0
  129. package/dist/editor/hit-test.d.ts +178 -0
  130. package/dist/editor/hit-test.d.ts.map +1 -0
  131. package/dist/editor/hit-test.js +460 -0
  132. package/dist/editor/hit-test.js.map +1 -0
  133. package/dist/editor/long-press.d.ts +34 -0
  134. package/dist/editor/long-press.d.ts.map +1 -0
  135. package/dist/editor/long-press.js +58 -0
  136. package/dist/editor/long-press.js.map +1 -0
  137. package/dist/editor/pinch.d.ts +37 -0
  138. package/dist/editor/pinch.d.ts.map +1 -0
  139. package/dist/editor/pinch.js +90 -0
  140. package/dist/editor/pinch.js.map +1 -0
  141. package/dist/editor/pointer-binding.d.ts +18 -0
  142. package/dist/editor/pointer-binding.d.ts.map +1 -0
  143. package/dist/editor/pointer-binding.js +1087 -0
  144. package/dist/editor/pointer-binding.js.map +1 -0
  145. package/dist/editor/public/annotations.d.ts +54 -0
  146. package/dist/editor/public/annotations.d.ts.map +1 -0
  147. package/dist/editor/public/annotations.js +106 -0
  148. package/dist/editor/public/annotations.js.map +1 -0
  149. package/dist/editor/public/arrange-group.d.ts +81 -0
  150. package/dist/editor/public/arrange-group.d.ts.map +1 -0
  151. package/dist/editor/public/arrange-group.js +193 -0
  152. package/dist/editor/public/arrange-group.js.map +1 -0
  153. package/dist/editor/public/brush.d.ts +33 -0
  154. package/dist/editor/public/brush.d.ts.map +1 -0
  155. package/dist/editor/public/brush.js +54 -0
  156. package/dist/editor/public/brush.js.map +1 -0
  157. package/dist/editor/public/clipboard.d.ts +31 -0
  158. package/dist/editor/public/clipboard.d.ts.map +1 -0
  159. package/dist/editor/public/clipboard.js +35 -0
  160. package/dist/editor/public/clipboard.js.map +1 -0
  161. package/dist/editor/public/image-insert.d.ts +30 -0
  162. package/dist/editor/public/image-insert.d.ts.map +1 -0
  163. package/dist/editor/public/image-insert.js +57 -0
  164. package/dist/editor/public/image-insert.js.map +1 -0
  165. package/dist/editor/public/layers.d.ts +56 -0
  166. package/dist/editor/public/layers.d.ts.map +1 -0
  167. package/dist/editor/public/layers.js +104 -0
  168. package/dist/editor/public/layers.js.map +1 -0
  169. package/dist/editor/public/link.d.ts +25 -0
  170. package/dist/editor/public/link.d.ts.map +1 -0
  171. package/dist/editor/public/link.js +74 -0
  172. package/dist/editor/public/link.js.map +1 -0
  173. package/dist/editor/public/placement.d.ts +60 -0
  174. package/dist/editor/public/placement.d.ts.map +1 -0
  175. package/dist/editor/public/placement.js +111 -0
  176. package/dist/editor/public/placement.js.map +1 -0
  177. package/dist/editor/public/selection-ops.d.ts +78 -0
  178. package/dist/editor/public/selection-ops.d.ts.map +1 -0
  179. package/dist/editor/public/selection-ops.js +226 -0
  180. package/dist/editor/public/selection-ops.js.map +1 -0
  181. package/dist/editor/public/text-edit.d.ts +17 -0
  182. package/dist/editor/public/text-edit.d.ts.map +1 -0
  183. package/dist/editor/public/text-edit.js +28 -0
  184. package/dist/editor/public/text-edit.js.map +1 -0
  185. package/dist/editor/public/z-order.d.ts +47 -0
  186. package/dist/editor/public/z-order.d.ts.map +1 -0
  187. package/dist/editor/public/z-order.js +151 -0
  188. package/dist/editor/public/z-order.js.map +1 -0
  189. package/dist/editor/public/zoom-pan.d.ts +44 -0
  190. package/dist/editor/public/zoom-pan.d.ts.map +1 -0
  191. package/dist/editor/public/zoom-pan.js +116 -0
  192. package/dist/editor/public/zoom-pan.js.map +1 -0
  193. package/dist/editor/render-orchestrator.d.ts +11 -0
  194. package/dist/editor/render-orchestrator.d.ts.map +1 -0
  195. package/dist/editor/render-orchestrator.js +473 -0
  196. package/dist/editor/render-orchestrator.js.map +1 -0
  197. package/dist/editor/resize-helpers.d.ts +30 -0
  198. package/dist/editor/resize-helpers.d.ts.map +1 -0
  199. package/dist/editor/resize-helpers.js +121 -0
  200. package/dist/editor/resize-helpers.js.map +1 -0
  201. package/dist/editor/shape-filters.d.ts +11 -0
  202. package/dist/editor/shape-filters.d.ts.map +1 -0
  203. package/dist/editor/shape-filters.js +19 -0
  204. package/dist/editor/shape-filters.js.map +1 -0
  205. package/dist/editor/shape-traits.d.ts +16 -0
  206. package/dist/editor/shape-traits.d.ts.map +1 -0
  207. package/dist/editor/shape-traits.js +29 -0
  208. package/dist/editor/shape-traits.js.map +1 -0
  209. package/dist/editor/viewport-helpers.d.ts +24 -0
  210. package/dist/editor/viewport-helpers.d.ts.map +1 -0
  211. package/dist/editor/viewport-helpers.js +63 -0
  212. package/dist/editor/viewport-helpers.js.map +1 -0
  213. package/dist/editor-events.d.ts +33 -0
  214. package/dist/editor-events.d.ts.map +1 -0
  215. package/dist/editor-events.js +2 -0
  216. package/dist/editor-events.js.map +1 -0
  217. package/dist/editor.d.ts +1841 -0
  218. package/dist/editor.d.ts.map +1 -0
  219. package/dist/editor.js +4803 -0
  220. package/dist/editor.js.map +1 -0
  221. package/dist/file-drop.d.ts +99 -0
  222. package/dist/file-drop.d.ts.map +1 -0
  223. package/dist/file-drop.js +198 -0
  224. package/dist/file-drop.js.map +1 -0
  225. package/dist/frame-helpers.d.ts +30 -0
  226. package/dist/frame-helpers.d.ts.map +1 -0
  227. package/dist/frame-helpers.js +105 -0
  228. package/dist/frame-helpers.js.map +1 -0
  229. package/dist/group-helpers.d.ts +39 -0
  230. package/dist/group-helpers.d.ts.map +1 -0
  231. package/dist/group-helpers.js +116 -0
  232. package/dist/group-helpers.js.map +1 -0
  233. package/dist/handle.d.ts +73 -0
  234. package/dist/handle.d.ts.map +1 -0
  235. package/dist/handle.js +184 -0
  236. package/dist/handle.js.map +1 -0
  237. package/dist/index.d.ts +26 -0
  238. package/dist/index.d.ts.map +1 -0
  239. package/dist/index.js +16 -0
  240. package/dist/index.js.map +1 -0
  241. package/dist/interactive.d.ts +21 -0
  242. package/dist/interactive.d.ts.map +1 -0
  243. package/dist/interactive.js +11 -0
  244. package/dist/interactive.js.map +1 -0
  245. package/dist/link-selection.d.ts +21 -0
  246. package/dist/link-selection.d.ts.map +1 -0
  247. package/dist/link-selection.js +37 -0
  248. package/dist/link-selection.js.map +1 -0
  249. package/dist/machine.d.ts +322 -0
  250. package/dist/machine.d.ts.map +1 -0
  251. package/dist/machine.js +568 -0
  252. package/dist/machine.js.map +1 -0
  253. package/dist/modes.d.ts +24 -0
  254. package/dist/modes.d.ts.map +1 -0
  255. package/dist/modes.js +2 -0
  256. package/dist/modes.js.map +1 -0
  257. package/dist/overlay.d.ts +262 -0
  258. package/dist/overlay.d.ts.map +1 -0
  259. package/dist/overlay.js +673 -0
  260. package/dist/overlay.js.map +1 -0
  261. package/dist/platform.d.ts +70 -0
  262. package/dist/platform.d.ts.map +1 -0
  263. package/dist/platform.js +115 -0
  264. package/dist/platform.js.map +1 -0
  265. package/dist/selection.d.ts +18 -0
  266. package/dist/selection.d.ts.map +1 -0
  267. package/dist/selection.js +31 -0
  268. package/dist/selection.js.map +1 -0
  269. package/package.json +59 -0
@@ -0,0 +1,1841 @@
1
+ import { type Actor } from "xstate";
2
+ import type { Bounds, FileId, ElementId, Vec2 } from "@oh-just-another/types";
3
+ import type { SpatialGrid } from "@oh-just-another/scene";
4
+ import { type FractionalIndex, type BrushPoint, type Link, type Patch, type Scene, type Element, type GridStyle, type TextStyle } from "@oh-just-another/scene";
5
+ import { type AnnotationId, type CommentId, type LinkId, type LayerId } from "@oh-just-another/types";
6
+ import { ElementCache, type RenderTarget, type TextShaper, type Rasterizer } from "@oh-just-another/renderer-core";
7
+ import { type HistoryOptions, type HistoryProvider, type TransactionHandle } from "@oh-just-another/history";
8
+ import { type FileDropHandler } from "./file-drop.js";
9
+ import { interactionMachine, type InteractionContext, type InteractionEmit, type PressTarget } from "./machine.js";
10
+ import type { Mode } from "./modes.js";
11
+ import type { EditorEvents } from "./editor-events.js";
12
+ import { LongPressController } from "./editor/long-press.js";
13
+ import { PinchController } from "./editor/pinch.js";
14
+ import { type BrushStrokeState } from "./editor/public/brush.js";
15
+ import { type PeerCursor, type PeerSelection } from "./overlay.js";
16
+ import * as Selection from "./selection.js";
17
+ import * as LinkSelection from "./link-selection.js";
18
+ export interface LoadSceneOptions {
19
+ /**
20
+ * Keep the existing undo/redo stack when swapping scenes. Used by
21
+ * `@collab/bindEditor` when a peer update arrives — the user's
22
+ * local history must survive remote edits. Default `false`:
23
+ * top-level callers loading a saved scene get a clean slate.
24
+ *
25
+ * When `true`, history patches that reference shapes removed by the
26
+ * remote peer become un-applicable: the local user sees an undo no-op
27
+ * or an exception on that step.
28
+ */
29
+ readonly preserveHistory?: boolean;
30
+ }
31
+ export interface EditorOptions {
32
+ readonly host: HTMLElement;
33
+ readonly mainTarget: RenderTarget;
34
+ readonly overlayTarget: RenderTarget;
35
+ /**
36
+ * Optional dedicated background target — when provided, the editor
37
+ * paints the grid (`renderGrid`) onto it. Hosts without a background
38
+ * layer can omit this; in that case the grid is drawn on `mainTarget`
39
+ * before shapes.
40
+ */
41
+ readonly backgroundTarget?: RenderTarget;
42
+ /**
43
+ * Called synchronously at the END of every render pass, right after the
44
+ * targets have been painted. Hosts whose surface defers submission
45
+ * (WebGL2 flush, OffscreenCanvas worker replay) MUST present here — not
46
+ * on `subscribe()`, which fires on `notify()` BEFORE the rAF-scheduled
47
+ * paint, leaving the surface one frame behind. No-op surfaces (Canvas2D)
48
+ * can omit it.
49
+ */
50
+ readonly onAfterRender?: () => void;
51
+ readonly initialScene: Scene;
52
+ readonly initialMode?: Mode;
53
+ /**
54
+ * Pre-existing history backend, or options for the default
55
+ * `History` (linear stack). Any `HistoryProvider` implementation
56
+ * works — `@oh-just-another/collab` ships `YjsHistory` that wraps
57
+ * `Y.UndoManager` for CRDT-aware undo in collaborative sessions.
58
+ */
59
+ readonly history?: HistoryProvider | HistoryOptions;
60
+ /**
61
+ * Primary input modality. Affects hit-test slop on handles and edges
62
+ * so a finger can grab them without precision-pointing.
63
+ *
64
+ * - `"mouse"` — pixel-accurate hit zones (default for desktop).
65
+ * - `"touch"` — 44 px+ touch targets (Apple HIG, WCAG AAA).
66
+ * - `"auto"` — pick `"touch"` if `matchMedia('(pointer: coarse)')`
67
+ * reports a coarse primary pointer, else `"mouse"`. Default.
68
+ */
69
+ readonly inputMode?: "mouse" | "touch" | "auto";
70
+ /**
71
+ * Optional text shaper. When supplied, replaces the renderer's
72
+ * default Canvas2D `measureText` path for wrap / layout. Plug
73
+ * `WasmTextShaper.loadBundled()` from `@oh-just-another/text-wasm`
74
+ * for deterministic browser-vs-Node parity (Roboto Regular
75
+ * embedded; advance widths match across environments).
76
+ */
77
+ readonly textShaper?: TextShaper;
78
+ /**
79
+ * Optional rasterizer. When supplied, hosts of `renderLinks` /
80
+ * future path-heavy code can opt in to WASM bezier / stroke-to-
81
+ * fill via `WasmRasterizer.loadBundled()` from
82
+ * `@oh-just-another/raster-wasm`. The kernel itself doesn't consume
83
+ * this directly today — exposed here so the field travels with
84
+ * `EditorOptions` and hosts have a single config surface.
85
+ */
86
+ readonly rasterizer?: Rasterizer;
87
+ /**
88
+ * When `true`, the editor routes per-frame rendering through a
89
+ * tile compositor (`renderViaTiles` in renderer-canvas) backed
90
+ * by an InMemoryTileCache. Designed for very-large scenes
91
+ * (10 K+ shapes) where re-rasterising every visible shape per
92
+ * frame dominates frame budget. Below ~5 K shapes the plain
93
+ * scene-renderer is usually faster — leave this off.
94
+ *
95
+ * Host must supply a tile compositor function via
96
+ * `tileCompose`; the kernel doesn't import renderer-canvas
97
+ * directly. Pattern:
98
+ *
99
+ * import { renderViaTiles } from "@oh-just-another/renderer-canvas";
100
+ * new Editor({ ..., useTileCache: true, tileCompose: renderViaTiles });
101
+ */
102
+ readonly useTileCache?: boolean;
103
+ /**
104
+ * Compositor function called per frame when `useTileCache` is on.
105
+ * Receives the scene, main target, and dirty bookkeeping; should
106
+ * handle caching internally.
107
+ */
108
+ readonly tileCompose?: TileComposeFn;
109
+ }
110
+ /**
111
+ * Signature of the tile compositor injected via EditorOptions. Editor
112
+ * stays decoupled from renderer-canvas; hosts wire the concrete
113
+ * implementation (`renderViaTiles`).
114
+ */
115
+ export type TileComposeFn = (scene: Scene, mainTarget: RenderTarget, options: {
116
+ readonly viewport: Bounds;
117
+ /**
118
+ * Per-shape change record (before/after world bbox) since the
119
+ * last frame. Compositors route by case (add / remove / move).
120
+ * `null` on one side = added / removed.
121
+ */
122
+ readonly changedElements: ReadonlyMap<ElementId, {
123
+ before: Bounds | null;
124
+ after: Bounds | null;
125
+ }>;
126
+ readonly zoomBucket: number;
127
+ }) => void;
128
+ /**
129
+ * Top-level interaction controller. Owns the scene + selection state, wires
130
+ * pointer events from the host element into the interaction machine, applies
131
+ * the machine's emitted effects back to the scene, and re-renders main and
132
+ * overlay on every change.
133
+ */
134
+ /** Outcome of `Editor.groupSelected`. `noop` when nothing was selected. */
135
+ export type GroupSelectedResult = {
136
+ readonly kind: "noop";
137
+ } | {
138
+ readonly kind: "grouped";
139
+ readonly groupId: ElementId;
140
+ };
141
+ /**
142
+ * Stable keys for cursor states a host can override with a custom image via
143
+ * {@link Editor.setCursorOverride}. Each maps to one outcome of `computeCursor`.
144
+ */
145
+ export type CursorRole = "default" | "pan-ready" | "pan-active" | "move" | "draw" | "text" | "link-start" | "link-handle" | "annotation" | "resize-nwse" | "resize-nesw" | "resize-ns" | "resize-ew";
146
+ /**
147
+ * A custom cursor: either a raw CSS `cursor` value, or an image with an
148
+ * optional `@2x` variant (DPR-aware via `image-set`), hotspot, and keyword
149
+ * fallback.
150
+ */
151
+ export type CursorSpec = string | {
152
+ /** 1x image URL or data-URL. */
153
+ readonly url: string;
154
+ /** Optional 2x image for hi-DPI (retina) — emitted via `image-set`. */
155
+ readonly url2x?: string;
156
+ /** Hotspot offset (px) within the image; defaults to (0, 0). */
157
+ readonly hotspot?: {
158
+ readonly x: number;
159
+ readonly y: number;
160
+ };
161
+ /** Keyword shown if the image can't load / is too large. */
162
+ readonly fallback?: string;
163
+ };
164
+ export declare class Editor {
165
+ readonly host: HTMLElement;
166
+ readonly mainTarget: RenderTarget;
167
+ readonly overlayTarget: RenderTarget;
168
+ readonly backgroundTarget: RenderTarget | null;
169
+ private readonly onAfterRender;
170
+ /**
171
+ * Debug: when true the overlay paints every element's mouse hit-zones
172
+ * (handle slop / edge endpoint / edge body). Toggled by the host
173
+ * debug panel via `setDebugHitZones`; read by the render orchestrator.
174
+ * View-only — never persisted or recorded in history.
175
+ */
176
+ debugHitZones: boolean;
177
+ /**
178
+ * When false the background grid is not painted. Toggled via `toggleGrid`
179
+ * (`g` hotkey, standard parity). View-only — never persisted or in history.
180
+ */
181
+ gridVisible: boolean;
182
+ readonly actor: Actor<typeof interactionMachine>;
183
+ private readonly listeners;
184
+ /**
185
+ * Typed event surface. Specific events (`mode`, `selection`,
186
+ * `scene`, `history`, `viewport`) fan out of `notify()` based on
187
+ * what actually changed since the last fire, so subscribers only
188
+ * wake up when their slice flips. `change` still fires once per
189
+ * `notify()` for callers that don't care which slice.
190
+ *
191
+ * The `subscribe()` set runs in lockstep with the typed events.
192
+ */
193
+ private readonly events;
194
+ /**
195
+ * Last-emitted snapshot of every observable slice. Used by
196
+ * `fanOutEvents` (in `editor/event-fanout.ts`) to decide which
197
+ * typed events to fire on each `notify()` — only the slices
198
+ * whose identity changed since the previous notify get an event.
199
+ */
200
+ private readonly eventCache;
201
+ private readonly unbind;
202
+ _scene: Scene;
203
+ _selection: Selection.Selection;
204
+ /**
205
+ * Snapshot of an in-progress annotation drag (press on pin → move
206
+ * pointer → release). `originPosition` is the annotation's stored
207
+ * position at press time; per-move handler computes a delta from
208
+ * the current pointer in world space and writes it back.
209
+ */
210
+ annotationDrag: {
211
+ id: AnnotationId;
212
+ originPosition: Vec2;
213
+ originWorldPoint: Vec2;
214
+ moved: boolean;
215
+ } | null;
216
+ /** Live preview while drawing a new shape; null when not drawing. */
217
+ drawingPreview: Bounds | null;
218
+ edgePreview: {
219
+ from: Vec2;
220
+ to: Vec2;
221
+ points?: readonly Vec2[];
222
+ } | null;
223
+ /**
224
+ * Active "drag a link from a start-anchor" gesture. Set when a
225
+ * press lands on one of the selected element's link-start dots; lets
226
+ * the user draw a link straight from the dot without switching to the
227
+ * draw-edge tool. `fromWorld` is the true anchor world point (the link
228
+ * origin, un-offset); `origin` is the press point (for the drag
229
+ * threshold). Read by the pointer handlers (drive preview / commit on
230
+ * up) and the render orchestrator (keep the source's start dots visible
231
+ * during the drag). Null when no such drag is in flight. */
232
+ linkDragFromAnchor: {
233
+ fromElement: ElementId;
234
+ fromWorld: Vec2;
235
+ /** Named anchor the gesture started on — drives the click-to-create
236
+ * direction (outward normal) and the source link endpoint. */
237
+ anchorName: string;
238
+ origin: Vec2;
239
+ moved: boolean;
240
+ } | null;
241
+ /**
242
+ * Element being hovered while draw-edge mode is active. Drives the port-
243
+ * overlay render so the user sees attachment points. `null` outside
244
+ * draw-edge mode or when the pointer is over empty canvas.
245
+ */
246
+ hoveredLinkTarget: {
247
+ elementId: ElementId;
248
+ activeAnchor: string | null;
249
+ outlinePoint?: Vec2 | undefined;
250
+ /**
251
+ * What the drop will produce, for clear pre-drop feedback (standard):
252
+ * - `"point"` → fixed attach to a specific dot (highlight the dot);
253
+ * - `"element"` → floating attach to the whole shape (highlight the
254
+ * element). Mirrors `snapLinkEndpoint`: an anchor within threshold →
255
+ * point, otherwise floating.
256
+ */
257
+ mode: "point" | "element";
258
+ } | null;
259
+ /**
260
+ * Last idle cursor position (world) in select mode — the overlay grows the
261
+ * SINGLE selected element's link-start dot nearest it
262
+ * (`ANCHOR_DOT_HOVER_GROW_RADIUS`). Reset to null on press / gesture.
263
+ */
264
+ hoverCursorWorld: Vec2 | null;
265
+ /**
266
+ * When a link is dropped on empty canvas, the edge is created with a
267
+ * free `point` end and this records where, so the host can pop a
268
+ * mini shape-picker at that spot (standard). Picking a shape re-points the
269
+ * end to the new element; dismissing (Esc / click-away) leaves the free
270
+ * end on the canvas. `null` when no menu is pending.
271
+ */
272
+ private pendingLinkDropMenu;
273
+ /**
274
+ * Currently selected links (connectors). Links are first-class members
275
+ * of the selection: they coexist with selected elements, join Cmd+A and
276
+ * marquee, and multi-select via Shift-click. Endpoint drag handles show
277
+ * only when EXACTLY one link is selected and no elements are (see
278
+ * `selectedLink`). Empty set = no link selected.
279
+ */
280
+ _selectedLinks: LinkSelection.LinkSelection;
281
+ /**
282
+ * Currently focused annotation thread — overlay highlights its pin
283
+ * with an accent ring and hosts (e.g. `<CommentsPopover>`) render
284
+ * the thread for this id. Independent of shape / edge selection so
285
+ * users can edit shapes while a comment thread is open.
286
+ */
287
+ _selectedAnnotation: AnnotationId | null;
288
+ /**
289
+ * Author identity used for comments posted via `addComment` /
290
+ * `addAnnotation` without an explicit author. Hosts set this once
291
+ * (typically from the same user object passed to `bindAwareness`).
292
+ * Defaults to a synthetic local user.
293
+ */
294
+ private commentAuthor;
295
+ /**
296
+ * Mid-drag preview state when the user is dragging an edge endpoint.
297
+ * Drawn as an overlay line + handle dot so the user sees the target.
298
+ */
299
+ linkEndpointDrag: {
300
+ linkId: LinkId;
301
+ side: "from" | "to";
302
+ toPoint: Vec2;
303
+ } | null;
304
+ /**
305
+ * Host-managed waypoint (bend-point) drag of the selected link. `index`
306
+ * is the position in `edge.waypoints`. `pendingInsert` means the gesture
307
+ * began on a segment midpoint and will splice a new waypoint on the
308
+ * first move (so a no-move click adds nothing). Live-mutated through the
309
+ * gesture transaction → one undo step per drag.
310
+ */
311
+ linkWaypointDrag: {
312
+ linkId: LinkId;
313
+ index: number;
314
+ pendingInsert: boolean;
315
+ } | null;
316
+ /**
317
+ * Host-managed elbow segment drag. `index` is the segment in the routed
318
+ * chain `[from, ...routedPoints, to]`; `axis` is its orientation. Dragging
319
+ * pins the segment's perpendicular coordinate into `Link.fixedSegments`;
320
+ * the reroute pass re-flows the rest. One undo step via the gesture tx.
321
+ */
322
+ linkSegmentDrag: {
323
+ linkId: LinkId;
324
+ axis: "h" | "v";
325
+ at: number;
326
+ } | null;
327
+ /** Live lasso bounds during a rubber-band select gesture. */
328
+ lassoPreview: Bounds | null;
329
+ /**
330
+ * Selection captured at lasso-press time. Used to compute the live
331
+ * preview correctly: in `replace` mode the lasso starts from empty
332
+ * each frame; in `add` mode it starts from this snapshot so shapes
333
+ * the user already had selected don't blink out and back.
334
+ */
335
+ private lassoBaseSelection;
336
+ /** Link-selection counterpart of `lassoBaseSelection` for the marquee. */
337
+ private lassoBaseLinks;
338
+ /**
339
+ * Snapshot of every selected shape's `position` at press-down. Used to
340
+ * translate the whole group additively during a multi-shape drag. The
341
+ * machine still emits per-shape MOVE_SHAPE — the editor intercepts and
342
+ * fans out when this map is populated.
343
+ */
344
+ groupMoveOrigin: ReadonlyMap<ElementId, Vec2> | null;
345
+ /**
346
+ * Press-time snapshot of connectors that must follow a multi-element
347
+ * drag rigidly — both endpoints bound to moved elements, carrying
348
+ * absolute geometry (waypoints / fixedSegments / routedPoints). Each
349
+ * frame translates from these originals so the shift never compounds.
350
+ * Cleared on gesture commit / cancel alongside `groupMoveOrigin`.
351
+ */
352
+ groupLinkMoveOrigin: ReadonlyMap<LinkId, Link> | null;
353
+ /**
354
+ * Per-shape snapshot for a group-resize gesture — `bounds` is the
355
+ * shape's world AABB at press-down. Editor scales the relative
356
+ * position / size against the combined bounds delta each frame.
357
+ */
358
+ groupResizeOrigin: {
359
+ readonly combined: Bounds;
360
+ readonly elements: ReadonlyMap<ElementId, {
361
+ readonly position: Vec2;
362
+ readonly bounds: Bounds;
363
+ readonly scale: Vec2;
364
+ }>;
365
+ readonly links: ReadonlyMap<LinkId, Link>;
366
+ } | null;
367
+ /**
368
+ * Pristine shape snapshot for a single-shape text resize, captured on
369
+ * the gesture's first tick. Font scaling is computed against this base
370
+ * so it never compounds across pointermove ticks. Cleared on gesture
371
+ * end (commit / cancel).
372
+ */
373
+ private _resizeOriginElement;
374
+ /**
375
+ * Active layer — new shapes created via `addElement` / `applyCreate` land
376
+ * here when their input doesn't specify a `layerId`. Defaults to the
377
+ * scene's `DEFAULT_LAYER_ID`; hosts switch via `setActiveLayer`.
378
+ */
379
+ _activeLayerId: LayerId;
380
+ private nextId;
381
+ /** Generate a short unique id with a stable prefix. */
382
+ private uniqueId;
383
+ /**
384
+ * Snap engine — defaults to grid + anchor + outline contributors.
385
+ */
386
+ private readonly snapEngine;
387
+ /** Snap threshold in world units. */
388
+ private readonly snapThreshold;
389
+ /**
390
+ * Transient flag set by the host while a snap-suppress modifier
391
+ * (Cmd / Ctrl) is held during a drag — lets the user pull a shape off
392
+ * the grid for one gesture without toggling snap off. Read by the
393
+ * move / resize / create wrappers; never persisted.
394
+ */
395
+ private snapSuppressed;
396
+ /**
397
+ * Persistent world-bounds cache shared with `renderScene` for viewport
398
+ * culling. Object-identity keyed — invalidates automatically whenever
399
+ * a scene op replaces the shape ref. Could be exposed for hit-test
400
+ * sharing in a follow-up.
401
+ */
402
+ readonly boundsCache: ElementCache<Bounds>;
403
+ /**
404
+ * Lazy SpatialGrid for hit-test acceleration in large scenes.
405
+ * Built on demand when `scene.elements.size >= LARGE_SCENE_HIT_THRESHOLD`
406
+ * and the cached index's source-scene reference is stale (any scene
407
+ * op replaces the `_scene` field, invalidating identity).
408
+ */
409
+ private spatialIndexCache;
410
+ /**
411
+ * The group the user has "entered" via double-click. While set, the
412
+ * hit-test stops promoting children of this group to the group root,
413
+ * letting the user directly manipulate inner shapes. Cleared on
414
+ * escape, click outside the group's descendants, or `cancelInteraction`.
415
+ */
416
+ _enteredGroup: ElementId | null;
417
+ /**
418
+ * Double-click detection state. Updated on every non-drag pointer
419
+ * up; the next pointer-up within `DOUBLE_CLICK_MS` and within
420
+ * `DOUBLE_CLICK_TOLERANCE_PX` of `lastClickWorldPoint` counts as a
421
+ * double-click. Used to trigger group drill-down (enter isolation).
422
+ */
423
+ private lastClickAt;
424
+ private lastClickWorldPoint;
425
+ /**
426
+ * Separate double-click tracker for link edit handles (waypoint /
427
+ * segment). Kept apart from `lastClickAt` because a handle press
428
+ * returns early in `onDown` (begin-drag) and never reaches the up-side
429
+ * double-click path that updates `lastClickAt`. Updated by
430
+ * `isHandleDoubleClick` on each handle press.
431
+ */
432
+ private lastHandleClickAt;
433
+ private lastHandleClickWorld;
434
+ /**
435
+ * In-progress brush stroke. Hosts push points via
436
+ * `extendBrushStroke`; the overlay reads it through
437
+ * `pendingBrushStroke` to draw a live preview.
438
+ */
439
+ brushStroke: BrushStrokeState | null;
440
+ /**
441
+ * Last world-space pointer position observed by the host's onMove
442
+ * handler. `paste()` uses it as the default drop target so a fresh
443
+ * paste lands under the cursor instead of overlapping the originals.
444
+ * `null` until the pointer first enters the host.
445
+ */
446
+ lastPointerWorld: Vec2 | null;
447
+ /** Host-registered custom cursor images per role (see `setCursorOverride`). */
448
+ private readonly cursorOverrides;
449
+ /**
450
+ * Scene rendered on the last frame. Used to compute a dirty rect by
451
+ * identity-diffing against the current scene — every shape / edge
452
+ * whose ref didn't change is also pixel-identical to its last paint
453
+ * and gets skipped together with the surrounding clear. `null` until
454
+ * the first render.
455
+ */
456
+ lastRenderedScene: Scene | null;
457
+ /**
458
+ * Last-painted isolation root — paired with `lastRenderedScene` so
459
+ * the dirty-rect optimization invalidates when the user enters or
460
+ * exits a group, even when the scene reference is unchanged. Without
461
+ * this, drilling into a group never triggers a redraw → the dim
462
+ * pass would never visibly apply.
463
+ */
464
+ lastRenderedEnteredGroup: ElementId | null;
465
+ /**
466
+ * Fractional-order compaction scheduler (microtask-coalesced).
467
+ * Triggered from every `notify()`; only does real work when at
468
+ * least one shape/edge order string crossed AUTO_COMPACT_THRESHOLD.
469
+ * See `./auto-compact.ts` for the extracted logic.
470
+ */
471
+ private readonly autoCompactScheduler;
472
+ /**
473
+ * Auto-layout scheduler — microtask-coalesced re-run of every
474
+ * shape carrying `metadata.autoLayout`. See
475
+ * `./auto-layout-scheduler.ts` for the extracted logic.
476
+ */
477
+ private readonly autoLayoutScheduler;
478
+ /**
479
+ * Element id that the user started dragging on press-down. Tracked
480
+ * separately from the state machine so the editor knows what to
481
+ * (re)parent / drop into a container on pointerup. `null` between
482
+ * gestures, set in onDown when press lands on a shape and cleared
483
+ * in onUp / cancel.
484
+ */
485
+ dragElementId: ElementId | null;
486
+ /**
487
+ * Element that the current press added to the selection additively
488
+ * (shift / meta click on an unselected shape). The press promotes it
489
+ * so a subsequent drag moves it; on a *tap* the up-handler would
490
+ * otherwise `SELECT_TOGGLE` it straight back off, so it consults this
491
+ * to skip that redundant toggle. Reset at every press-down.
492
+ */
493
+ additivePressAdded: ElementId | null;
494
+ /**
495
+ * Live container highlight: the container shape the dragged item is
496
+ * currently hovering over. Drawn by the overlay as a dashed
497
+ * accent rect on the container's drop-zone so the user sees where the
498
+ * shape will land after release.
499
+ */
500
+ containerHover: {
501
+ id: ElementId;
502
+ dropZone: Bounds;
503
+ } | null;
504
+ /**
505
+ * Remote peer cursors / selections, pushed in by the host (typically
506
+ * a `bindAwareness(editor, awareness)` helper in `@collab`). The
507
+ * editor only renders them; it doesn't fetch or interpret. Each
508
+ * setter triggers `render()` so the overlay updates immediately.
509
+ */
510
+ _peerCursors: readonly PeerCursor[];
511
+ _peerSelections: readonly PeerSelection[];
512
+ /**
513
+ * Subscribers notified on every host pointer move (world-space). Used
514
+ * by `@collab` to broadcast the local cursor into awareness.
515
+ */
516
+ readonly cursorListeners: Set<(point: Vec2) => void>;
517
+ /**
518
+ * Active screen-space pointer positions keyed by `pointerId`. With
519
+ * one entry the editor's normal single-pointer flow applies. With
520
+ * two or more entries we enter a pinch / pan gesture and bypass the
521
+ * interaction machine — `pinchOrigin` holds the baseline.
522
+ */
523
+ readonly activePointers: Map<number, Vec2>;
524
+ /**
525
+ * One-finger-pan candidate: set at pointer-down when a TOUCH press lands
526
+ * on empty canvas in select mode. A tap (no movement) still falls through
527
+ * to select/deselect; once the finger drags past slop, onMove promotes
528
+ * this to a real pan instead of a marquee lasso (mobile convention).
529
+ * Screen-space origin point.
530
+ */
531
+ touchPanCandidate: Vec2 | null;
532
+ pinch: PinchController;
533
+ /** Bridge for `editor/container-ops.ts`. Built lazily in constructor. */
534
+ private containerOpsRef;
535
+ /**
536
+ * Space-bar held → next pointer drag pans the canvas instead of
537
+ * doing whatever the current mode would do. Visual cursor goes to
538
+ * "grab" / "grabbing". Wires a window-level keydown/keyup listener
539
+ * in `bindPointerEvents`.
540
+ */
541
+ spaceHeld: boolean;
542
+ /**
543
+ * Host-supplied tile compositor — when set (via
544
+ * `EditorOptions.useTileCache` + `tileCompose`), the per-frame
545
+ * render path delegates to it instead of `renderScene`. Stays
546
+ * `null` for the typical small-scene case.
547
+ */
548
+ readonly tileComposeFn: TileComposeFn | null;
549
+ /**
550
+ * Per-shape change record (before/after world bbox) since the last
551
+ * tile-cache invalidation pass. Populated by `computeDirtyWorld`'s
552
+ * diff loop when `tileComposeFn` is on; forwarded to the compositor
553
+ * each frame so it can invalidate by add / remove / move correctly.
554
+ * (A plain id set lost adds — new id wasn't in the tile reverse
555
+ * index yet.)
556
+ */
557
+ tileDirtyElements: Map<ElementId, {
558
+ before: Bounds | null;
559
+ after: Bounds | null;
560
+ }>;
561
+ /**
562
+ * Tool-lock flag (standard model). When `false` (default), a
563
+ * draw-mode (`draw-rect` / `draw-ellipse` / `draw-edge` / `brush`)
564
+ * auto-reverts to `select` after a successful create. When `true`,
565
+ * the mode persists so the user can draw many shapes in a row
566
+ * without re-selecting the tool.
567
+ *
568
+ * Toggled via `Editor.setToolLocked(bool)` and surfaced in the
569
+ * toolbar as a lock affordance next to the active tool.
570
+ */
571
+ private _toolLocked;
572
+ /**
573
+ * Host-extensible file-drop dispatch. Built-ins (image / scene
574
+ * JSON) register themselves at editor construction; hosts add
575
+ * more via `registerFileDropHandler`.
576
+ */
577
+ private readonly fileDropRegistry;
578
+ /**
579
+ * Active pan gesture (right-click drag or Space + left drag).
580
+ * `pointerId` is captured by the host so move/up events keep
581
+ * arriving even after the cursor leaves the host bounds.
582
+ * `startPoint` is the press position — used to decide "click vs
583
+ * drag" at pointerup (a near-zero displacement right-click is a
584
+ * context-menu request, not a pan). `lastPoint` tracks the
585
+ * previous move so per-frame delta is correct.
586
+ *
587
+ * `button` records which mouse button started the gesture so we
588
+ * only treat right-click releases as potential context-menu
589
+ * triggers (Space + left-drag never opens a menu).
590
+ */
591
+ panGesture: {
592
+ pointerId: number;
593
+ button: number;
594
+ startPoint: Vec2;
595
+ lastPoint: Vec2;
596
+ moved: boolean;
597
+ } | null;
598
+ /**
599
+ * Set on a right-click pointerdown so the upcoming native
600
+ * `contextmenu` event can be unconditionally preventDefault'ed
601
+ * (the gesture decides whether to fire the menu manually on
602
+ * pointerup based on whether the user dragged).
603
+ */
604
+ suppressNextContextMenu: boolean;
605
+ /**
606
+ * Long-press tracking. Starts on `pointerdown`; cancelled on
607
+ * `pointermove > LONG_PRESS_MAX_MOVEMENT_PX` or `pointerup` before
608
+ * the timer fires. Hosts subscribe via `onLongPress` to surface a
609
+ * context menu (mobile alternative to right-click).
610
+ */
611
+ longPress: LongPressController;
612
+ private readonly longPressListeners;
613
+ /**
614
+ * Live-region announcements for assistive tech. The editor pushes
615
+ * short, human-readable strings ("Selected Rectangle", "Moved 5 px
616
+ * right") that hosts pipe into an `aria-live=polite` region.
617
+ */
618
+ private readonly announceListeners;
619
+ /**
620
+ * Resolved primary input modality + derived hit slops. Computed once
621
+ * in the constructor from `EditorOptions.inputMode` (default `"auto"`
622
+ * uses `matchMedia('(pointer: coarse)')`).
623
+ */
624
+ private readonly inputMode;
625
+ private readonly handleHitSlop;
626
+ private readonly edgeHandleHitSlop;
627
+ private readonly edgeHitThreshold;
628
+ /** Link-start anchor-dot grab/click hit radii — touch-enlarged in touch mode. */
629
+ readonly anchorStartHitSlop: number;
630
+ readonly anchorClickRadius: number;
631
+ readonly _history: HistoryProvider;
632
+ /** Open transaction during a single drag/resize gesture. */
633
+ gestureTx: TransactionHandle | null;
634
+ /**
635
+ * Immutable snapshot of `_scene` taken when a gesture transaction opens.
636
+ * The history transaction only records patches for undo — cancelling it does
637
+ * NOT roll back `_scene`. Keeping the pre-gesture scene lets Escape (and any
638
+ * cancel) restore it, so a drag/move/resize/endpoint-rebind aborted with Esc
639
+ * leaves the scene exactly as it was. Cleared on commit.
640
+ */
641
+ private gestureStartScene;
642
+ /**
643
+ * Wraps gesture lifecycle (transaction open/commit/cancel +
644
+ * post-create mode revert) so editor.ts doesn't carry the bodies.
645
+ * Implementation lives in `./editor/gesture-tx.ts`; the
646
+ * controller calls back through the narrow `GestureRef` bridge
647
+ * built lazily below.
648
+ */
649
+ private readonly gestures;
650
+ constructor(options: EditorOptions);
651
+ /** Snapshot used by event-fanout. Kept private — internal API. */
652
+ private observableSnapshot;
653
+ get scene(): Scene;
654
+ get selection(): Selection.Selection;
655
+ get mode(): Mode;
656
+ get history(): HistoryProvider;
657
+ get canUndo(): boolean;
658
+ get canRedo(): boolean;
659
+ /**
660
+ * The DOM element the editor was mounted onto. Read-only — external
661
+ * code reads it for screen-↔-world coordinate conversions on events
662
+ * whose coordinates are in client-space (e.g. global `contextmenu`).
663
+ */
664
+ get hostElement(): HTMLElement;
665
+ /** Subscribe to scene/selection/mode/history changes. */
666
+ subscribe(fn: () => void): () => void;
667
+ /**
668
+ * Subscribe to local world-space pointer movement. Fires on every
669
+ * `pointermove` over the host. `@collab` uses this to broadcast the
670
+ * local cursor into the awareness room.
671
+ */
672
+ onCursorMove(fn: (point: Vec2) => void): () => void;
673
+ /** Set the local user's identity for comments authored via this editor. */
674
+ setCommentAuthor(author: {
675
+ id: string;
676
+ name: string;
677
+ }): void;
678
+ /** Currently focused annotation id (or null when nothing is open). */
679
+ get selectedAnnotation(): AnnotationId | null;
680
+ /**
681
+ * Open or close an annotation thread. `null` clears the focus. The
682
+ * overlay highlights the pin; `<CommentsPopover>` reads this and
683
+ * renders the thread.
684
+ */
685
+ setSelectedAnnotation(id: AnnotationId | null): void;
686
+ addAnnotation(opts: {
687
+ position: Vec2;
688
+ elementId?: ElementId | null;
689
+ firstComment?: string;
690
+ }): AnnotationId;
691
+ removeAnnotation(id: AnnotationId): void;
692
+ toggleAnnotationResolved(id: AnnotationId): void;
693
+ addComment(annotationId: AnnotationId, body: string, author?: {
694
+ id: string;
695
+ name: string;
696
+ }): void;
697
+ removeComment(annotationId: AnnotationId, commentId: CommentId): void;
698
+ hitAnnotation(worldPoint: Vec2): AnnotationId | null;
699
+ /**
700
+ * Subscribe to long-press events — a stationary touch / mouse-press
701
+ * held longer than `LONG_PRESS_DELAY_MS`. The mobile-equivalent of
702
+ * right-click; hosts open a context menu from this. The event
703
+ * carries both screen-space (for menu positioning) and world-space
704
+ * (for target hit-test) coordinates.
705
+ */
706
+ onLongPress(fn: (payload: {
707
+ screenPoint: Vec2;
708
+ worldPoint: Vec2;
709
+ }) => void): () => void;
710
+ /**
711
+ * Subscribe to accessibility live-region announcements. The host
712
+ * pipes these strings into an `aria-live="polite"` element so a
713
+ * screen-reader user hears the editor's status changes. Strings
714
+ * are short and pre-localised by the caller of `announce`.
715
+ */
716
+ onAnnounce(fn: (message: string) => void): () => void;
717
+ /**
718
+ * Push a live-region message to all `onAnnounce` listeners. Hosts
719
+ * (and plugins) call this when something happened that an SR user
720
+ * should hear: selection changed, shape moved, mode switched, etc.
721
+ * The editor itself emits a small set of canonical messages from
722
+ * `focusCycle` / `moveSelectionBy` / `cancelInteraction`.
723
+ */
724
+ announce(message: string): void;
725
+ /**
726
+ * Replace the remote peer cursors painted by the overlay. Pass an
727
+ * empty array to clear. The host is expected to filter out the
728
+ * local user's cursor before calling.
729
+ */
730
+ setPeerCursors(cursors: readonly PeerCursor[]): void;
731
+ /**
732
+ * Replace the remote peer selections painted by the overlay. The
733
+ * host resolves a peer's `selection: ElementId[]` into world bounds
734
+ * before passing them in.
735
+ */
736
+ setPeerSelections(selections: readonly PeerSelection[]): void;
737
+ /**
738
+ * Toggle the debug hit-zone overlay (host debug panel). When on, the
739
+ * overlay paints every element's mouse hit-targets so the tuned slop
740
+ * values can be eyeballed. View-only — not recorded in history.
741
+ */
742
+ setDebugHitZones(on: boolean): void;
743
+ /** Show/hide the background grid (standard `g`). View-only — not in history. */
744
+ setGridVisible(on: boolean): void;
745
+ /** Toggle background grid visibility. */
746
+ toggleGrid(): void;
747
+ /** Whether the active draw-mode sticks after a create (toolbar lock). */
748
+ get toolLocked(): boolean;
749
+ /** All currently-selected link (connector) ids. */
750
+ get selectedLinks(): LinkSelection.LinkSelection;
751
+ /**
752
+ * The sole selected link — non-null ONLY when exactly one link and no
753
+ * elements are selected. Drives endpoint handles, the edge-style panel,
754
+ * caption edit and `updateSelectedLink`; a multi/mixed selection yields
755
+ * null so those single-link affordances stay hidden.
756
+ */
757
+ get selectedLink(): LinkId | null;
758
+ /**
759
+ * Apply an in-place mutation to the currently-selected edge as a
760
+ * single history step. The `updater` receives a clone of the edge
761
+ * and returns the next version (callers should produce a new
762
+ * object — Link is readonly). No-op when no edge is selected.
763
+ */
764
+ updateSelectedLink(updater: (edge: Link) => Link): void;
765
+ /**
766
+ * Register a file-drop handler. Handlers are tried in registration
767
+ * order; the first whose `accept(file)` returns true takes the
768
+ * file. Repeated calls with the same `id` replace the previous
769
+ * handler (idempotent for module-load wiring).
770
+ */
771
+ registerFileDropHandler(handler: FileDropHandler): void;
772
+ /** Drop a registered handler. */
773
+ unregisterFileDropHandler(id: string): void;
774
+ /**
775
+ * Dispatch a dropped file (or pasted file from clipboard) into
776
+ * the registered handlers. Returns `true` when a handler accepted
777
+ * the file, `false` otherwise — UI can show an "unsupported file"
778
+ * toast on `false`. `worldPoint` is where the file should land
779
+ * (drop-point projected to world coords; for paste, host can use
780
+ * cursor world point or viewport centre).
781
+ */
782
+ dispatchFileDrop(file: File, worldPoint: Vec2): Promise<boolean>;
783
+ /**
784
+ * Toggle the tool-lock affordance. With `true`, draw-modes persist
785
+ * after each successful shape create — the user keeps drawing
786
+ * rectangles without re-pressing R. With `false` (default), the
787
+ * editor reverts to `select` after each create.
788
+ */
789
+ setToolLocked(locked: boolean): void;
790
+ private maybeRevertModeAfterCreate;
791
+ setMode(mode: Mode): void;
792
+ /** Undo the latest record. No-op if there is nothing to undo. */
793
+ undo(): boolean;
794
+ /** Redo the undone record. */
795
+ redo(): boolean;
796
+ /**
797
+ * Add a shape to the scene and push a single record onto the history stack.
798
+ * Returns the resulting patch (useful for tests). Intended for code paths
799
+ * that create shapes outside of a pointer gesture — drag-from-palette,
800
+ * paste, programmatic insert.
801
+ */
802
+ addElement(shape: Element, options?: {
803
+ select?: boolean;
804
+ }): Patch;
805
+ /**
806
+ * Insert an image at the given world position. Wraps `addElement`
807
+ * with the image-shape boilerplate (id, layer, order, scale=1,
808
+ * rotation=0). Returns the new shape's id so callers can chain
809
+ * (e.g. immediately reparent into a container).
810
+ *
811
+ * `src` is anything the renderer accepts — a data-URL from the
812
+ * file-drop handler, a host CDN URL, an SVG string in
813
+ * `image/svg+xml;base64,...` form.
814
+ */
815
+ insertImage(input: {
816
+ src: string;
817
+ width: number;
818
+ height: number;
819
+ position: Vec2;
820
+ image?: HTMLImageElement;
821
+ animated?: boolean;
822
+ fileId?: FileId;
823
+ animationKind?: string;
824
+ animationData?: unknown;
825
+ }): ElementId;
826
+ addBinaryFile(blob: Blob, name?: string): Promise<FileId>;
827
+ /**
828
+ * Animation tick — runs while any shape carries
829
+ * `metadata.animated` (GIFs today; future video / lottie).
830
+ * Forces a full re-render every frame so drawImage picks up the
831
+ * current frame of natively-animated elements. Self-terminates
832
+ * when no animated shapes remain.
833
+ *
834
+ * Lifecycle managed by the `AnimationTick` helper (see
835
+ * `./animation-tick.ts`). `insertImage({animated:true})` and
836
+ * `loadScene` start the tick; `dispose()` stops it.
837
+ */
838
+ /** EMA of animation-tick render cost (ms) — drives the adaptive throttle. */
839
+ private gifRenderCostEma;
840
+ /** Wall-clock of the last animation-tick render — for the interval throttle. */
841
+ private lastGifTickMs;
842
+ private readonly animationTick;
843
+ private hasAnimatedElement;
844
+ /**
845
+ * True when at least one animated shape's world AABB intersects the
846
+ * current viewport. Drives viewport-culling of the animation tick —
847
+ * off-screen GIFs don't burn decode / render cost, and the wall-clock
848
+ * frame selection means they show the right frame the moment they
849
+ * scroll back in.
850
+ */
851
+ private hasVisibleAnimatedElement;
852
+ /**
853
+ * Re-arm the animation tick after a change that may have brought an
854
+ * animated shape into (or out of) view — pan / zoom / scene edit.
855
+ * `AnimationTick.start()` no-ops when already running or when
856
+ * `isAnimated()` is false, so this is cheap to call from `notify()`.
857
+ */
858
+ private maybeAnimate;
859
+ /** Bound `visibilitychange` handler — pause/resume the tick. */
860
+ private readonly onVisibilityChange;
861
+ /**
862
+ * Transient per-shape playback state for animated images. `originMs`
863
+ * is the wall-clock the current play run started; `frozenMs` is the
864
+ * playback offset a paused shape is held at. Not serialised — purely
865
+ * a runtime view, rebuilt on insert / rehydrate.
866
+ */
867
+ private readonly playbackState;
868
+ /** Element id currently hovered — a hovered heavy GIF keeps playing
869
+ * (its auto-stop timer is held off). Set by the pointer hover path. */
870
+ private hoveredAnimatedId;
871
+ private static nowMs;
872
+ private static prefersReducedMotion;
873
+ /**
874
+ * Seed playback for a freshly-animated shape. Start paused (frozen on
875
+ * frame 0) when the user prefers reduced motion; playing otherwise.
876
+ */
877
+ private initPlayback;
878
+ /** Playback timestamp fed to the renderer's animation clock for a
879
+ * shape: wall-clock when unmanaged, play offset when playing, the
880
+ * frozen frame when paused. */
881
+ private playbackClock;
882
+ /**
883
+ * Toggle GIF playback for a shape — wired to a click on an animated
884
+ * image (resume after auto-stop, play after reduced-motion). Resuming
885
+ * continues from the frozen frame.
886
+ */
887
+ togglePlayback(id: ElementId): void;
888
+ /**
889
+ * Hover entered an animated shape: resume it if paused and hold off
890
+ * its auto-stop timer while the pointer stays over it (the auto-stop
891
+ * pass keeps pushing `playStartMs` forward for the hovered shape).
892
+ * Pass `null` when the pointer leaves all shapes.
893
+ */
894
+ hoverAnimatedElement(id: ElementId | null): void;
895
+ /** True when the shape's GIF is paused (drives the overlay badge). */
896
+ isPlaybackPaused(id: ElementId): boolean;
897
+ /**
898
+ * Record the idle cursor position so the overlay can grow the SINGLE
899
+ * selected element's link-start dot nearest the cursor. Only the selected
900
+ * element shows start dots (connecting from an unselected element on hover
901
+ * was a cancelled product decision). Pass `null` to clear.
902
+ */
903
+ setHoverCursorWorld(cursor: Vec2 | null): void;
904
+ /** Live link-draw preview polyline (elbow), or null when not drawing. */
905
+ get linkPreviewPath(): readonly Vec2[] | null;
906
+ /** Current connector attach target + mode (point=fixed / element=floating). */
907
+ get linkAttachTarget(): {
908
+ elementId: ElementId;
909
+ mode: "point" | "element";
910
+ } | null;
911
+ /**
912
+ * Freeze heavy GIFs after `GIF_AUTOSTOP_MS` of continuous play.
913
+ * Light GIFs (small byte payload) loop forever. Called from the tick
914
+ * before each animation render.
915
+ */
916
+ private autoStopHeavyGifs;
917
+ /**
918
+ * Restore transient `animationData` for animated image shapes after
919
+ * a scene load. The raw GIF bytes don't survive serialisation
920
+ * (`serializeScene` strips the ArrayBuffer), but they're persisted
921
+ * in `Scene.files` via the shape's `fileId`. Here we copy the bytes
922
+ * back onto `shape.animationData` so the registered animation
923
+ * adapter (host-side, e.g. the gifuct decoder) can produce frames.
924
+ *
925
+ * Applied directly to `_scene` (no history entry — this is an
926
+ * internal rehydration, not a user edit). No-op for shapes that
927
+ * already carry live `animationData` or lack a resolvable file.
928
+ */
929
+ private rehydrateAnimatedImages;
930
+ /**
931
+ * Drag-to-place flow for palette templates. Adds the shape to the
932
+ * scene immediately so the user sees it dragging under the cursor,
933
+ * but defers the history entry until `commit()` is called. `update`
934
+ * re-positions without writing per-move patches; `cancel` removes
935
+ * the shape entirely and leaves history untouched (no undo entry).
936
+ *
937
+ * Typical wiring: HTML5 dragenter starts the placement, dragover
938
+ * updates, drop commits, dragleave / window keydown(Escape) cancel.
939
+ */
940
+ beginPlacement(shape: Element): {
941
+ update: (worldCenter: Vec2) => void;
942
+ commit: () => void;
943
+ cancel: () => void;
944
+ };
945
+ deleteSelected(): void;
946
+ /**
947
+ * Currently edited text shape (or null). Set by `beginTextEdit`;
948
+ * cleared by `commitTextEdit` / `cancelTextEdit`. The host overlay
949
+ * (`<TextEditorOverlay>` in `@react-ui`) subscribes via `editor`
950
+ * and renders a `<textarea>` positioned over the shape.
951
+ */
952
+ private _editingTextElement;
953
+ get editingTextElement(): ElementId | null;
954
+ /** Link whose caption is being edited inline (double-click), or null. */
955
+ private _editingLinkCaption;
956
+ get editingLinkCaption(): LinkId | null;
957
+ /**
958
+ * Frame whose NAME (header label) is being edited inline (double-click
959
+ * the header), or null. The host overlay (`<FrameNameEditorOverlay>` in
960
+ * `@react-ui`) renders an input over the header and commits the name.
961
+ */
962
+ private _editingFrameName;
963
+ get editingFrameName(): ElementId | null;
964
+ /**
965
+ * When the `draw-text` tool just placed a shape and opened its
966
+ * editor, this holds that shape's id until the first commit. A
967
+ * pending creation isn't in history yet: committing non-empty text
968
+ * records a single add patch (whole shape = one undo); committing
969
+ * empty / cancelling removes it with no history entry at all.
970
+ */
971
+ private _pendingTextCreate;
972
+ /**
973
+ * Snapshot of the shape at edit start. Used to revert on cancel and
974
+ * as the `before` of the single commit patch. `null` for a pending
975
+ * creation (the shape didn't exist yet).
976
+ */
977
+ private _textEditOrigin;
978
+ /**
979
+ * Live selection inside the edited text, mirrored from the hidden
980
+ * `<textarea>` (`start`/`end` are source offsets, `dir` is the
981
+ * anchored end). The caret is `dir === "backward" ? start : end`.
982
+ */
983
+ private _textSel;
984
+ /** Anchor offset for a canvas drag-select inside the edited text. */
985
+ private _textDragAnchor;
986
+ private _caretBlinkOn;
987
+ private _caretBlinkTimer;
988
+ get editingTextSelection(): {
989
+ start: number;
990
+ end: number;
991
+ dir: "forward" | "backward";
992
+ } | null;
993
+ /** Caret offset = the moving end of the selection. */
994
+ get editingTextCaret(): number | null;
995
+ get caretBlinkOn(): boolean;
996
+ /** `true` while a canvas drag-select inside the edited text is active. */
997
+ get isTextDragging(): boolean;
998
+ private startCaretBlink;
999
+ private stopCaretBlink;
1000
+ /** Reset the caret to solid (called on type / move so it never blinks off mid-action). */
1001
+ private wakeCaret;
1002
+ /**
1003
+ * Begin editing a text shape's body. No-op when the shape doesn't
1004
+ * exist or isn't a text shape. Concurrent edits commit themselves
1005
+ * (only one shape at a time). Caret defaults to the end of the text.
1006
+ */
1007
+ /** Open inline caption editing for a link (double-click). */
1008
+ beginLinkCaptionEdit(id: LinkId): void;
1009
+ /**
1010
+ * Commit the link caption. Empty / whitespace text removes the label;
1011
+ * otherwise the label text is set, preserving any existing position /
1012
+ * styling. One undo step. Clears caption-edit mode.
1013
+ */
1014
+ commitLinkCaptionEdit(text: string): void;
1015
+ /** Cancel link caption editing without changing the label. */
1016
+ cancelLinkCaptionEdit(): void;
1017
+ /** World-space anchor point for a link's caption (midpoint of its path). */
1018
+ linkLabelWorld(id: LinkId): Vec2 | null;
1019
+ beginTextEdit(id: ElementId): void;
1020
+ /**
1021
+ * Start editing a frame's header name. No-op unless `id` is a frame on
1022
+ * an unlocked layer. Commits any in-flight text edit first.
1023
+ */
1024
+ beginFrameNameEdit(id: ElementId): void;
1025
+ /**
1026
+ * Commit the edited frame name. Empty / whitespace-only clears the
1027
+ * stored name (the renderer falls back to "Frame"). One history step;
1028
+ * no-op when the name is unchanged. Always clears the editing state.
1029
+ */
1030
+ commitFrameNameEdit(name: string): void;
1031
+ /** Abandon the frame-name edit without changing the name. */
1032
+ cancelFrameNameEdit(): void;
1033
+ /**
1034
+ * Frame whose header strip (the label bar ABOVE the body) contains the
1035
+ * world point — top-most by z-order. Used to route a double-click on the
1036
+ * header to a name edit, since the header sits outside the frame's
1037
+ * hit-test bounds. Assumes unrotated frames (the common case).
1038
+ */
1039
+ private frameHeaderAt;
1040
+ /**
1041
+ * Live edit transport from the hidden `<textarea>`: replace the
1042
+ * shape's text + selection as the user types / pastes / composes.
1043
+ * Mutates the scene WITHOUT a history entry — history is recorded
1044
+ * once on commit. No-op when not editing.
1045
+ */
1046
+ setEditingText(value: string, selStart: number, selEnd: number, dir?: "forward" | "backward"): void;
1047
+ /** Selection-only update (arrows / shift-select / click) — no text change. */
1048
+ setEditingSelection(selStart: number, selEnd: number, dir?: "forward" | "backward"): void;
1049
+ /**
1050
+ * Map a world-space point to a caret offset in the edited text. Used
1051
+ * to place / extend the caret from canvas clicks. Returns `null` when
1052
+ * not editing or the shape is gone.
1053
+ */
1054
+ caretIndexAtWorldPoint(worldPoint: Vec2): number | null;
1055
+ /**
1056
+ * `true` when a point is inside the currently-edited text shape's
1057
+ * world bounds. Used by the pointer binding to decide between
1058
+ * repositioning the caret (inside) and committing (outside).
1059
+ */
1060
+ editedElementContainsPoint(worldPoint: Vec2): boolean;
1061
+ /** Place a collapsed caret at the clicked point and start a drag-select. */
1062
+ setTextCaretFromPoint(worldPoint: Vec2): void;
1063
+ /** Extend the selection from the drag anchor to the current point. */
1064
+ extendTextSelectionToPoint(worldPoint: Vec2): void;
1065
+ /** End a canvas drag-select (clears the drag anchor). */
1066
+ endTextDragSelect(): void;
1067
+ /** Build the editable layout for a text shape using the main target's metrics. */
1068
+ private editingTextLayout;
1069
+ /**
1070
+ * A measure callback bound to a shape's font, using the main target's
1071
+ * `measureText` — the SAME source the renderer draws with (WebGL2
1072
+ * reports MSDF advances) and the bounder measures with. Caret /
1073
+ * selection geometry therefore lines up exactly with the glyphs.
1074
+ */
1075
+ private measureFor;
1076
+ /**
1077
+ * World-space caret + selection geometry for the overlay pass.
1078
+ * Returns `null` when not editing. The caret is `null` while blinked
1079
+ * off so the overlay can simply skip drawing it.
1080
+ */
1081
+ editingTextOverlay(): {
1082
+ caret: {
1083
+ x: number;
1084
+ y: number;
1085
+ height: number;
1086
+ } | null;
1087
+ caretColor: string;
1088
+ selectionRects: readonly Bounds[];
1089
+ } | null;
1090
+ commitTextEdit(next?: string): void;
1091
+ cancelTextEdit(): void;
1092
+ /**
1093
+ * Translate every selected shape by the given world-space delta.
1094
+ * Single undo step. No-op when selection is empty. Used by arrow-key
1095
+ * keyboard navigation; hosts pass `{ x: 1, y: 0 }` for fine nudge
1096
+ * and `{ x: 10, y: 0 }` for shift-arrow.
1097
+ */
1098
+ moveSelectionBy(delta: Vec2): void;
1099
+ /**
1100
+ * Keyboard-friendly creation flow. Picks the shape type from the
1101
+ * current `mode` ("draw-rect" / "draw-ellipse" / fallback to
1102
+ * rectangle) and inserts a sensible default-sized shape at the
1103
+ * viewport center. Returns the new shape's id, or `null` when the
1104
+ * scene has no active layer.
1105
+ *
1106
+ * Hosts can bind this to "Enter" while in a draw mode, providing a
1107
+ * mouse-free alternative to drag-out creation.
1108
+ */
1109
+ createElementAtCursor(): ElementId | null;
1110
+ /**
1111
+ * `draw-text` tool: drop an empty text shape at `worldPoint`, select
1112
+ * it and open its inline editor immediately. The add is a single undo
1113
+ * step; the subsequent text typed in is committed (or the empty shape
1114
+ * removed) by `commitTextEdit`. Reverts to `select` afterwards unless
1115
+ * the tool is locked.
1116
+ */
1117
+ createTextAt(worldPoint: Vec2): ElementId;
1118
+ beginBrushStroke(world: Vec2, pressure?: number): void;
1119
+ extendBrushStroke(world: Vec2, pressure?: number): void;
1120
+ commitBrushStroke(): ElementId | null;
1121
+ cancelBrushStroke(): void;
1122
+ /** Current in-progress brush stroke, exposed for the overlay preview. */
1123
+ get pendingBrushStroke(): {
1124
+ readonly origin: Vec2;
1125
+ readonly points: readonly BrushPoint[];
1126
+ } | null;
1127
+ arrangeAsGrid(opts?: {
1128
+ cols?: number;
1129
+ gap?: number;
1130
+ }): void;
1131
+ arrangeAsStack(opts?: {
1132
+ direction?: "horizontal" | "vertical";
1133
+ gap?: number;
1134
+ }): void;
1135
+ groupSelected(): GroupSelectedResult;
1136
+ ungroup(): void;
1137
+ private selectionRoots;
1138
+ expandSelectionWithDescendants(): ReadonlySet<ElementId>;
1139
+ focusCycle(direction: "next" | "prev"): void;
1140
+ /**
1141
+ * Clear selection + cancel any in-progress drag / draw gesture.
1142
+ * Bound to Escape in default keyboard nav.
1143
+ */
1144
+ cancelInteraction(): void;
1145
+ /**
1146
+ * Duplicate the selected shapes 10 px down-right of the originals.
1147
+ * Links between selected shapes are NOT cloned. Single undo step.
1148
+ */
1149
+ duplicateSelected(): void;
1150
+ /**
1151
+ * Clone the selection IN PLACE (zero offset), select the clones, and return
1152
+ * the clone of `anchorId` (or null). Unlike `duplicateSelected` this also
1153
+ * clones group descendants and frame members, remapping `parentId`/`frameId`
1154
+ * among the clones so a duplicated frame keeps its contents. Used by
1155
+ * `⌥`-drag duplicate — the caller then drags the clones, leaving the
1156
+ * originals. One undo step.
1157
+ */
1158
+ duplicateSelectedInPlace(anchorId?: ElementId | null): ElementId | null;
1159
+ setSelection(ids: Iterable<ElementId>): void;
1160
+ selectAll(): void;
1161
+ /**
1162
+ * Internal clipboard. Stored as deep-cloned snapshots so subsequent
1163
+ * mutations don't affect the buffer. Survives across editor calls
1164
+ * within the same session; cross-tab paste uses host-level
1165
+ * `navigator.clipboard` (out of scope for the editor).
1166
+ */
1167
+ private clipboard;
1168
+ copySelected(): void;
1169
+ cutSelected(): void;
1170
+ /**
1171
+ * Paste clipboard contents into the scene. The cluster lands so that
1172
+ * its centroid sits at `targetWorld` (defaults to the last tracked
1173
+ * cursor position; when even that is unavailable, falls back to a
1174
+ * +10 px nudge so duplicates stay visible). Relative offsets
1175
+ * between clipboard items are preserved.
1176
+ *
1177
+ * New shapes get fresh ids and end up selected. Single undo step.
1178
+ */
1179
+ paste(targetWorld?: Vec2): void;
1180
+ /**
1181
+ * Merge `partial` into the `style` of every shape in `ids`. Useful
1182
+ * for the inspector / PropertyPanel: flipping `roundness`, swapping
1183
+ * `lineJoin`, changing `stroke` colour across a multi-selection,
1184
+ * etc. All changes go through one history record (single undo).
1185
+ *
1186
+ * No-op when `ids` is empty or none of the targeted shapes exist.
1187
+ */
1188
+ updateStyle(ids: Iterable<ElementId>, partial: Partial<TextStyle>): void;
1189
+ /**
1190
+ * Update non-style text properties (`fontSize`, `fontFamily`,
1191
+ * `maxWidth`) on every selected text shape. Non-text shapes are
1192
+ * skipped. Single undo step. Used by the text contextual panel.
1193
+ */
1194
+ updateTextProps(ids: Iterable<ElementId>, partial: {
1195
+ fontSize?: number;
1196
+ fontFamily?: string;
1197
+ maxWidth?: number;
1198
+ }): void;
1199
+ /**
1200
+ * Set (or clear, with `null`) the element-level hyperlink (`href`) on
1201
+ * every shape in `ids`. Single undo step. Pass a raw user string — it
1202
+ * is normalised here (`normalizeHref`: adds `https://`, `mailto:`,
1203
+ * rejects `javascript:`/`data:`); a string that normalises to nothing
1204
+ * clears the link. The host opens it on Cmd/Ctrl-click or the
1205
+ * hover link-popup.
1206
+ */
1207
+ setLink(ids: Iterable<ElementId>, href: string | null): void;
1208
+ /**
1209
+ * Open an element hyperlink in a new tab. Re-validates the scheme
1210
+ * (`safeHref`) before navigating — only `http`/`https`/`mailto`, never
1211
+ * `javascript:` / `data:` — and uses `noopener,noreferrer`. No-op for
1212
+ * an unsafe / empty href or outside a browser.
1213
+ */
1214
+ openLink(href: string | undefined | null): void;
1215
+ /** The element-level href of a shape, if safe to open; else `null`. */
1216
+ elementLink(id: ElementId): string | null;
1217
+ /**
1218
+ * Topmost interactable shape at `worldPoint` that carries a safe link,
1219
+ * with its world bounds — for the hover link-popup. `null` when none.
1220
+ */
1221
+ linkAt(worldPoint: Vec2): {
1222
+ id: ElementId;
1223
+ href: string;
1224
+ bounds: Bounds;
1225
+ } | null;
1226
+ bringToFront(id?: ElementId): void;
1227
+ sendToBack(id?: ElementId): void;
1228
+ /** Move the target shape one step toward the top of its layer. */
1229
+ bringForward(id?: ElementId): void;
1230
+ /** Move the target shape one step toward the bottom of its layer. */
1231
+ sendBackward(id?: ElementId): void;
1232
+ compactLayerZOrder(layerId?: LayerId, options?: {
1233
+ recordHistory?: boolean;
1234
+ }): void;
1235
+ /**
1236
+ * Wipe every shape + edge from the scene. Layers and viewport survive.
1237
+ * Clears history — restoring an empty scene through undo would be
1238
+ * surprising and the operation is rarely chained with other edits.
1239
+ */
1240
+ clear(): void;
1241
+ /** Currently active layer — new shapes default into it. */
1242
+ get activeLayerId(): LayerId;
1243
+ /** Switch the active layer. Hosts call this from a layer panel click. */
1244
+ setActiveLayer(id: LayerId): void;
1245
+ createLayer(name: string): LayerId;
1246
+ removeLayer(id: LayerId): void;
1247
+ renameLayer(id: LayerId, name: string): void;
1248
+ toggleLayerVisibility(id: LayerId): void;
1249
+ toggleLayerLock(id: LayerId): void;
1250
+ moveSelectionToLayer(targetLayer: LayerId): void;
1251
+ /**
1252
+ * Pan the camera by a screen-space delta. Drives both wheel-pan and
1253
+ * the two-finger pan gesture; deltas are in screen pixels (positive
1254
+ * x → shapes move right relative to the user). Not recorded in
1255
+ * history — viewport state is editor-local.
1256
+ */
1257
+ panBy(deltaScreen: Vec2): void;
1258
+ zoomIn(): void;
1259
+ zoomOut(): void;
1260
+ private zoomStep;
1261
+ resetZoom(): void;
1262
+ zoomToFit(padding?: number): void;
1263
+ /** Fit the camera to the current selection (standard `⌥2`). No-op when empty. */
1264
+ zoomToSelection(padding?: number): void;
1265
+ /**
1266
+ * Select the nearest interactable top-level element in `direction` from the
1267
+ * current selection's centre (or the viewport centre when nothing is
1268
+ * selected). standard `⌘`+arrows. Candidates must lie within a 45° cone of the
1269
+ * direction; the closest by along+lateral distance wins. No-op when nothing
1270
+ * qualifies.
1271
+ */
1272
+ selectClosest(direction: "left" | "right" | "up" | "down"): void;
1273
+ zoomAt(factor: number, anchorWorld: Vec2): void;
1274
+ setViewportSize(width: number, height: number): void;
1275
+ setGrid(patch: {
1276
+ size?: number;
1277
+ style?: GridStyle;
1278
+ snap?: boolean;
1279
+ }): void;
1280
+ /** Whether snap-to-grid is currently enabled (default on). */
1281
+ get snapToGridEnabled(): boolean;
1282
+ /** Toggle snap-to-grid on/off. Persists in the viewport. */
1283
+ setSnapToGrid(enabled: boolean): void;
1284
+ /**
1285
+ * Host hook: while held, the next move/resize/create gesture ignores
1286
+ * snap-to-grid (Cmd/Ctrl modifier). The app wires keydown/keyup
1287
+ * of the modifier to this. Idempotent; never touches history.
1288
+ */
1289
+ setSnapSuppressed(suppressed: boolean): void;
1290
+ /**
1291
+ * True when a gesture should snap: feature on, grid visible, AND the
1292
+ * suppress modifier not held. Hiding the grid (`toggleGrid` / `g`) also
1293
+ * disables snap-to-grid — snapping to an invisible grid is confusing.
1294
+ * Re-showing the grid restores the stored snap preference.
1295
+ */
1296
+ private snapActive;
1297
+ /** World-unit spacing the current gesture snaps to. */
1298
+ private snapSpacing;
1299
+ /**
1300
+ * Replace the entire scene (e.g. after `parseScene`). Clears history,
1301
+ * selection and any open gesture. Use to load a saved document.
1302
+ */
1303
+ loadScene(scene: Scene, options?: LoadSceneOptions): void;
1304
+ /** Detach all DOM listeners and stop the actor. */
1305
+ dispose(): void;
1306
+ private bindPointerEvents;
1307
+ /**
1308
+ * Open a pan gesture: capture the pointer so subsequent move / up
1309
+ * events arrive even outside the host bounds, cancel anything the
1310
+ * machine might have started this tick, and switch the cursor.
1311
+ */
1312
+ beginPanGesture(pointerId: number, button: number, point: Vec2): void;
1313
+ /**
1314
+ * End an in-progress pan gesture. Restores the cursor unless Space
1315
+ * is still held (then we drop back to "grab" so the user knows
1316
+ * another drag is armed). For right-click that didn't move past
1317
+ * the slop threshold, fires the long-press callback so the context
1318
+ * menu opens at the click position — that's the "right-click =
1319
+ * menu, right-drag = pan" decision rule.
1320
+ */
1321
+ endPanGesture(): void;
1322
+ isDrawingPhase(ctx: InteractionContext): boolean;
1323
+ startLongPress(screenPoint: Vec2): void;
1324
+ cancelLongPress(): void;
1325
+ beginPinch(): void;
1326
+ applyPinch(): void;
1327
+ /**
1328
+ * Convert a point in the host element's CSS-pixel coordinate space into
1329
+ * world coordinates. Public so drop handlers (drag-from-palette, paste)
1330
+ * can map pointer positions back to scene space.
1331
+ */
1332
+ screenToWorld(point: Vec2): Vec2;
1333
+ /**
1334
+ * Attach target under `worldPoint` for an endpoint-rebind drop: the topmost
1335
+ * interactable ELEMENT (group-promoted), ignoring link bodies and the dragged
1336
+ * link's own endpoint handle. Used instead of {@link hitTest} when finishing
1337
+ * an endpoint drag — `hitTest` would return the (now live, cursor-tracking)
1338
+ * endpoint handle and shadow the element beneath it, blocking re-binding.
1339
+ * `undefined` → dropped on empty space (the end stays a free point).
1340
+ */
1341
+ linkAttachTargetAt(worldPoint: Vec2): PressTarget | undefined;
1342
+ hitTest(worldPoint: Vec2): PressTarget;
1343
+ /**
1344
+ * Recompute the canvas cursor from the current interaction state and apply
1345
+ * it to the host element. Single chokepoint — called from pointer-move
1346
+ * (hover), gesture begin/end, and mode changes so the cursor never drifts
1347
+ * out of sync. `worldPoint` defaults to the last known pointer position.
1348
+ */
1349
+ refreshCursor(worldPoint?: Vec2): void;
1350
+ /**
1351
+ * The CSS cursor for the current state. Priority: active gesture → text edit → pan affordance →
1352
+ * draw tool → idle hover hit-test. Pure read of editor state; no side effects.
1353
+ */
1354
+ private computeCursor;
1355
+ /**
1356
+ * Resolve a cursor role to a CSS `cursor` value: a host-registered custom
1357
+ * image (via {@link setCursorOverride}) if present, else `fallbackKeyword`.
1358
+ */
1359
+ private resolveCursor;
1360
+ /**
1361
+ * Register (or clear, with `null`) a custom cursor image for a state role.
1362
+ * The image is shown wherever `computeCursor` resolves that role; pass a
1363
+ * `CursorSpec` object for a DPR-aware image (`image-set(1x, 2x)`) with a
1364
+ * hotspot + keyword fallback, or a raw CSS cursor string. Host-only view
1365
+ * state — not persisted.
1366
+ */
1367
+ setCursorOverride(role: CursorRole, spec: CursorSpec | null): void;
1368
+ /**
1369
+ * True when `p` is within the grab radius of one of the single selected
1370
+ * element's link-start dots — used to show a `crosshair` (start a link).
1371
+ * Mirrors the anchor-drag hit-test in pointer-binding so the cursor matches
1372
+ * exactly where a press would begin a link.
1373
+ */
1374
+ private isOverLinkStartDot;
1375
+ /** True when the given layer exists and is marked `locked`. */
1376
+ private isLayerLocked;
1377
+ /**
1378
+ * Combined interactivity check: false when the shape's layer is
1379
+ * locked, or when the shape itself or any ancestor via `parentId`
1380
+ * carries `locked: true` (group lock propagation). Hit-test treats
1381
+ * non-interactable hits as misses; render still draws them so the
1382
+ * user can see what's locked.
1383
+ */
1384
+ private isElementInteractable;
1385
+ /**
1386
+ * Can this shape be moved / resized? False when the shape (or an ancestor)
1387
+ * is `locked`, its layer is locked, or it's hidden. Distinct from
1388
+ * `isElementInteractable`, which still allows selecting a locked shape so it
1389
+ * can be unlocked.
1390
+ */
1391
+ isElementManipulable(shape: Element): boolean;
1392
+ /**
1393
+ * Toggle the `locked` flag on the selection (standard `⌘⇧L`). If any selected
1394
+ * element is currently unlocked, lock all; otherwise unlock all. One undo
1395
+ * step. A locked element stays selectable (click → select → unlock) but
1396
+ * can't be moved or resized.
1397
+ */
1398
+ toggleLockSelection(): void;
1399
+ /**
1400
+ * Promote a hit shape to the topmost ancestor whose group we have NOT
1401
+ * "entered" yet. Only **`group`**-typed parents promote — containers
1402
+ * (swim-lane, frame) intentionally let click hits land on their
1403
+ * children. Group is an abstract wrapper that has no visual identity,
1404
+ * so promoting up to it is the only way to select it; a container has
1405
+ * its own body, header, etc. and clicking inside it should let users
1406
+ * pick the actual child shape (rectangle, sticky, …) — same affordance
1407
+ * as standard.
1408
+ *
1409
+ * Stops at the first non-`group` parent. With `_enteredGroup` set,
1410
+ * the walk also stops just below that group so children can be edited
1411
+ * directly.
1412
+ */
1413
+ private promoteToGroupRoot;
1414
+ /**
1415
+ * Topmost group ancestor of `shape` (walks parentId chain, returns
1416
+ * the highest `type === "group"` parent). `null` if `shape` has no
1417
+ * group ancestor. Used by drill-down: a double-click on a shape
1418
+ * with a group ancestor enters that group. Body extracted to
1419
+ * `./group-helpers.ts`.
1420
+ */
1421
+ private topGroupAncestor;
1422
+ /**
1423
+ * True when `elementId`'s parent chain contains `groupId`. Used by the
1424
+ * isolation exit path: a click on a shape whose parent chain *does
1425
+ * not* lead through the entered group is a click "outside" the
1426
+ * group, which exits isolation.
1427
+ */
1428
+ private isDescendantOfGroup;
1429
+ /**
1430
+ * Compute the dim set for isolation rendering: every shape whose
1431
+ * parent chain does NOT pass through `enteredGroupId`. The entered
1432
+ * group itself is treated as "inside" (returns true from
1433
+ * isDescendantOfGroup) so it stays at full alpha — but groups have
1434
+ * no intrinsic geometry, so this only matters for the
1435
+ * group-bounds-outline overlay path, not the shape render.
1436
+ *
1437
+ * Defensive: shapes in the current selection are never dimmed. The
1438
+ * focus shape (drilled-into child) is always a group descendant in
1439
+ * practice, but the guard keeps the contract simple — "what you've
1440
+ * selected, you can see".
1441
+ */
1442
+ computeHiddenElements(): ReadonlySet<ElementId> | undefined;
1443
+ computeDimElements(enteredGroupId: ElementId): ReadonlySet<ElementId>;
1444
+ /**
1445
+ * Enter a group — subsequent hits inside this group return children
1446
+ * directly instead of the group root. `null` exits group-edit mode.
1447
+ * Bound to double-click on a group in the default handler.
1448
+ */
1449
+ enterGroup(groupId: ElementId | null): void;
1450
+ /**
1451
+ * Enter the single selected container — select its contents (standard `⌘⇧↓`).
1452
+ * Members are children via `parentId` (group / template container) or via
1453
+ * `frameId` (frame). For a group we also set `enteredGroup` so subsequent
1454
+ * clicks land on children. No-op unless exactly one container with members
1455
+ * is selected.
1456
+ */
1457
+ enterContainer(): void;
1458
+ /**
1459
+ * Exit to the container of the current selection — select the parent group /
1460
+ * template container (`parentId`) or frame (`frameId`) when every selected
1461
+ * element shares one (standard `⌘⇧↑`). Clears `enteredGroup`. No-op when there
1462
+ * is no single common container.
1463
+ */
1464
+ exitContainer(): void;
1465
+ /** Currently "entered" group, if any. */
1466
+ get enteredGroup(): ElementId | null;
1467
+ /**
1468
+ * SpatialGrid-accelerated topmost-shape lookup. Linear scan for small
1469
+ * scenes; for larger scenes builds a grid lazily, keyed by current
1470
+ * scene-identity. Scene operations replace `_scene` (immutable patches),
1471
+ * so reference-equality is a sufficient invalidation signal.
1472
+ */
1473
+ acceleratedElementAt(worldPoint: Vec2): Element | undefined;
1474
+ /**
1475
+ * Build (or return the cached) `SpatialGrid` for the current scene.
1476
+ * Re-built only when `_scene` reference changes — scene operations
1477
+ * always produce a fresh object, so reference equality is a
1478
+ * sufficient invalidation signal.
1479
+ *
1480
+ * Shared between the hit-test path (`acceleratedElementAt`) and the
1481
+ * renderer pass (passed to `renderScene` as `spatialIndex`), so
1482
+ * the grid build cost is amortised across both consumers.
1483
+ */
1484
+ ensureSpatialIndex(): SpatialGrid;
1485
+ /**
1486
+ * Group-isolation click routing. Returns `true` if the click was
1487
+ * handled (caller should skip the default applyEmit), `false` if the
1488
+ * normal selection emit should still run.
1489
+ *
1490
+ * Three paths fire here:
1491
+ * 1. **Double-click on a grouped shape (not yet in isolation):**
1492
+ * enter that group; select the raw inner shape (bypassing the
1493
+ * group-root promotion that ran in hitTest).
1494
+ * 2. **Inside isolation, click on a non-descendant shape OR empty
1495
+ * space:** exit isolation. Let the normal click then run so the
1496
+ * newly clicked element / empty selection takes hold.
1497
+ * 3. **Inside isolation, double-click on the entered group's own
1498
+ * child group:** drill another level deeper. (Implicit: same as
1499
+ * case 1 but topGroupAncestor here returns the inner child
1500
+ * group because the outer group is already entered.)
1501
+ *
1502
+ * Side-effect: updates `lastClickAt` / `lastClickWorldPoint`
1503
+ * regardless of result, so subsequent calls can detect a double-
1504
+ * click against this event.
1505
+ */
1506
+ routeIsolationClick(clickEffect: InteractionEmit | null, worldPoint: Vec2): boolean;
1507
+ /**
1508
+ * Given the raw shape under the cursor and its topmost group
1509
+ * ancestor, pick which group to "enter" on a drill-down.
1510
+ *
1511
+ * - No group ancestor → null (drill-down doesn't apply).
1512
+ * - Top group not yet entered → enter top.
1513
+ * - Top group already entered → walk down the chain to find the
1514
+ * next group inward (one level deeper).
1515
+ */
1516
+ private pickDrillTarget;
1517
+ applyEmit(emit: InteractionEmit): void;
1518
+ /**
1519
+ * Drag handler for annotation pins. Moves the pin to
1520
+ * `origin + delta`. Anchor semantics: for shape-anchored
1521
+ * annotations the `position` field is shape-local, so dragging
1522
+ * still updates the same field — the editor doesn't try to
1523
+ * reparent the anchor mid-drag; user wants the pin under the
1524
+ * cursor and that's exactly what `position + (delta in world)`
1525
+ * gives, regardless of which space the position is interpreted
1526
+ * in (`getAnnotationWorldPosition` already adds the shape's
1527
+ * world position when anchored).
1528
+ *
1529
+ * Wrapped in a single gestureTx so per-move updates collapse
1530
+ * into one undo step.
1531
+ */
1532
+ private applyAnnotationMove;
1533
+ private readonly templateTapListeners;
1534
+ private readonly templateDropListeners;
1535
+ /**
1536
+ * Subscribe to template button taps. Returns an unsubscribe function.
1537
+ * Hosts use this to route template button clicks to their own actions.
1538
+ */
1539
+ onTemplateTap(fn: (emit: Extract<InteractionEmit, {
1540
+ type: "TEMPLATE_TAP";
1541
+ }>) => void): () => void;
1542
+ /**
1543
+ * Subscribe to drops onto template drop-zones. Returns an unsubscribe fn.
1544
+ * Hosts decide what to do with the drop (e.g. add a child shape, link
1545
+ * templates together).
1546
+ */
1547
+ onTemplateDrop(fn: (emit: Extract<InteractionEmit, {
1548
+ type: "TEMPLATE_DROP";
1549
+ }>) => void): () => void;
1550
+ /**
1551
+ * Dispatch a TEMPLATE_DROP emit programmatically. Hosts call this from their
1552
+ * own DOM `drop` listener after looking up which drop-zone (if any) is
1553
+ * under the pointer via `findDropZoneAt`.
1554
+ */
1555
+ dispatchTemplateDrop(emit: Extract<InteractionEmit, {
1556
+ type: "TEMPLATE_DROP";
1557
+ }>): void;
1558
+ private applyMove;
1559
+ private applyGroupMove;
1560
+ computeViewportWorld(): Bounds | null;
1561
+ /**
1562
+ * Identity-diff the current scene against the last rendered one and
1563
+ * return the union AABB of every shape/edge that changed reference
1564
+ * (added, removed, or replaced). Returns `null` to force a full
1565
+ * clear when something that affects the entire surface changes —
1566
+ * viewport pan/zoom/resize, layer visibility, or the first frame.
1567
+ *
1568
+ * Empty union (returned as a zero-area bbox far off-screen) means
1569
+ * nothing changed; renderScene will cull every shape via its
1570
+ * `dirtyWorld` filter — effectively a no-op main pass.
1571
+ */
1572
+ computeDirtyWorld(): Bounds | null;
1573
+ combinedSelectionBounds(): Bounds | null;
1574
+ private groupChildrenUnion;
1575
+ /**
1576
+ * True when the current selection should be treated as aspect-
1577
+ * locked for group-handle resize. Currently: a single `group`-typed
1578
+ * shape selected. Multi-selection of free shapes keeps the default
1579
+ * 8-handle / free-aspect behaviour (matches user expectation:
1580
+ * grouping is the explicit "lock the ratio" gesture).
1581
+ */
1582
+ selectionIsAspectLocked(): boolean;
1583
+ private applyGroupResize;
1584
+ private applyResize;
1585
+ private applyCreate;
1586
+ /** Generate the next "Frame N" name based on existing frames. */
1587
+ private nextFrameName;
1588
+ /**
1589
+ * Assign frameId to every shape (except the frame itself) whose
1590
+ * centre falls inside the frame's world bounds. Runs as a single
1591
+ * undo step in the same gesture transaction as the create.
1592
+ */
1593
+ private assignFrameMembers;
1594
+ /**
1595
+ * Re-evaluate frame membership at the end of a move / resize gesture —
1596
+ * elements dropped inside a frame join it, those dragged out are
1597
+ * released (standard "membership on drop"). Runs inside the gesture
1598
+ * transaction (called from pointer-up before `commitGesture`) so the
1599
+ * frameId changes undo together with the drag. No-op when nothing
1600
+ * changed.
1601
+ */
1602
+ reconcileFrameMembership(): void;
1603
+ private applyCreateLink;
1604
+ /** Pending shape-picker after a link was dropped on empty canvas. */
1605
+ get linkDropMenu(): {
1606
+ linkId: LinkId;
1607
+ side: "from" | "to";
1608
+ world: Vec2;
1609
+ } | null;
1610
+ /**
1611
+ * Resolve a pending link-drop shape-picker by creating an element from
1612
+ * `factory` centred at the drop point and re-pointing the dropped link
1613
+ * end to float against it. Element + re-point land in one undo step; the
1614
+ * new element becomes the selection. No-op when no menu is pending.
1615
+ */
1616
+ placeShapeAtLinkDrop(factory: (ctx: {
1617
+ id: ElementId;
1618
+ layerId: LayerId;
1619
+ position: Vec2;
1620
+ order: FractionalIndex;
1621
+ }) => Element): void;
1622
+ /** Dismiss the link-drop shape-picker, leaving the free-ended link. */
1623
+ dismissLinkDropMenu(): void;
1624
+ /**
1625
+ * standard "click a link-start dot" gesture: spawn a new element in that
1626
+ * dot's outward direction and link the source to it. The clone copies
1627
+ * the source's type / style / size but NOT its text (a fresh blank of
1628
+ * the same kind). Direction is source → new; the new element becomes the
1629
+ * selection. Element + link land in one undo step.
1630
+ */
1631
+ createLinkedElementFromAnchor(fromElement: ElementId, anchorName: string): void;
1632
+ /**
1633
+ * Ghost geometry for what clicking a start dot would create (standard hover
1634
+ * preview): the would-be new element's world bounds + the connector path
1635
+ * from the dot to it. Pure — no mutation. Mirrors the placement in
1636
+ * `createLinkedElementFromAnchor`.
1637
+ */
1638
+ previewClickCreate(fromElement: ElementId, anchorName: string): {
1639
+ bounds: Bounds;
1640
+ path: readonly Vec2[];
1641
+ element: Element;
1642
+ ghostScene: Scene;
1643
+ ghostLinkId: LinkId;
1644
+ } | null;
1645
+ /**
1646
+ * Build an `LinkEndpoint` for a draw-edge / re-bind gesture. Runs the
1647
+ * scene's snap engine for the probe point, prefers anchor snap when
1648
+ * close enough, falls back to outline snap (so the user can attach
1649
+ * "anywhere on the right edge"), then `point` for the free-floating
1650
+ * case.
1651
+ *
1652
+ * `pressTargetElement` is the shape the gesture originated from or
1653
+ * landed on (used as a strong hint — we don't snap onto unrelated
1654
+ * shapes when the user clearly aimed for this one).
1655
+ */
1656
+ private snapLinkEndpoint;
1657
+ private applySelectByBounds;
1658
+ private applyLassoLiveSelection;
1659
+ /**
1660
+ * Live endpoint-rebind move: re-point the dragged end to the cursor in the
1661
+ * scene (a free `point` endpoint), recorded in the gesture transaction so the
1662
+ * WHOLE link redraws under the cursor with full fidelity — real style,
1663
+ * arrowhead, curved bow, and (via `rerouteElbows` in `render`) a live elbow
1664
+ * re-route. One undo step on commit; Escape cancels the transaction and the
1665
+ * link snaps back to where it was. The handle dot follows via `linkEndpointDrag`.
1666
+ */
1667
+ private applyLinkEndpointMove;
1668
+ private applyLinkEndpointUpdate;
1669
+ /** True while a waypoint of the selected link is being dragged. */
1670
+ get isDraggingWaypoint(): boolean;
1671
+ /**
1672
+ * Begin a host-managed waypoint drag. `insert` splices a new waypoint at
1673
+ * `index` on the first move (segment-midpoint "add" handle); otherwise an
1674
+ * existing waypoint at `index` is moved. Live-mutated through the gesture
1675
+ * transaction so the whole drag is one undo step.
1676
+ */
1677
+ beginWaypointDrag(linkId: LinkId, index: number, insert: boolean): void;
1678
+ /** Live update of the dragged waypoint to `world`. */
1679
+ updateWaypointDrag(world: Vec2): void;
1680
+ /**
1681
+ * Finish the waypoint drag. If the dragged waypoint landed within
1682
+ * `WAYPOINT_COLLAPSE_RADIUS` of an adjacent path point, it is removed
1683
+ * (drag-onto-the-line to delete). A no-move insert adds nothing.
1684
+ */
1685
+ endWaypointDrag(): void;
1686
+ /** True while an elbow segment is being dragged. */
1687
+ get isDraggingSegment(): boolean;
1688
+ /**
1689
+ * Begin a host-managed elbow segment drag. `axis` is the segment's
1690
+ * orientation; `at` is its centre along its own axis (used to re-identify it
1691
+ * across re-routes).
1692
+ */
1693
+ beginSegmentDrag(linkId: LinkId, axis: "h" | "v", at: number): void;
1694
+ /**
1695
+ * Move the dragged elbow segment perpendicular to its axis: pin its
1696
+ * perpendicular coordinate to the cursor. The reroute pass re-flows the
1697
+ * rest around the pin (one undo step via the gesture transaction).
1698
+ */
1699
+ updateSegmentDrag(world: Vec2): void;
1700
+ /** Finish the elbow segment drag (commit the gesture as one undo step). */
1701
+ endSegmentDrag(): void;
1702
+ /**
1703
+ * Double-click detector for link edit handles (waypoint / segment).
1704
+ * Returns true when this press follows the previous handle press within
1705
+ * the double-click window + tolerance. Updates state every call. Kept
1706
+ * separate from the up-side double-click path (handles return early in
1707
+ * `onDown`, so that path never sees them).
1708
+ */
1709
+ isHandleDoubleClick(world: Vec2): boolean;
1710
+ /**
1711
+ * Delete a free bend point (waypoint) from a straight / bezier link by
1712
+ * index — double-click a waypoint handle to remove it. One undo step.
1713
+ */
1714
+ deleteWaypoint(linkId: LinkId, index: number): void;
1715
+ /**
1716
+ * Remove the pinned (fixed) elbow segment that matches the given
1717
+ * geometry — double-click a segment handle to return it to the auto
1718
+ * route. Matches by axis + nearest pinned perpendicular `pos` (exact for
1719
+ * a pinned segment), `at` as tiebreak. The reroute pass re-flows on the
1720
+ * next render (fixedSegments is part of the elbow signature). One undo
1721
+ * step.
1722
+ */
1723
+ resetSegmentPin(linkId: LinkId, axis: "h" | "v", pos: number, at: number): void;
1724
+ /** Whether the selected link has obstacle-avoidance routing enabled. */
1725
+ get selectedLinkAvoidsObstacles(): boolean;
1726
+ /**
1727
+ * Toggle persistent "route around shapes" on the selected link (standard
1728
+ * model). Enabling sets `avoidObstacles` and forces `orthogonal` routing —
1729
+ * the elbow router then keeps the path clear of EVERY scene shape and
1730
+ * re-routes whenever an obstacle moves into the way (see
1731
+ * `routeElbowLink` / `elbowSignature`). Disabling drops the flag; the
1732
+ * routing type is left as-is. One undo step; the routed path itself is
1733
+ * derived (recomputed by `rerouteElbows`). No-op when no link is selected.
1734
+ */
1735
+ setSelectedLinkAvoidObstacles(enabled: boolean): void;
1736
+ updateHoveredLinkTarget(worldPoint: Vec2): void;
1737
+ applyLinkPreview(fromElement: ElementId | null, fromPoint: Vec2, toPoint: Vec2): void;
1738
+ private recordGesturePatch;
1739
+ commitGesture(): void;
1740
+ private finalizeOpenGestureTx;
1741
+ /**
1742
+ * End-of-drag container hookup. Runs after the state machine has
1743
+ * received POINTER_UP but before the gesture transaction commits,
1744
+ * so reparent + auto-grow land in one undo step with the drag itself.
1745
+ *
1746
+ * Rules:
1747
+ * - If the shape was dropped over a container and is not yet its child →
1748
+ * set `parentId`. If the shape extends past the dropZone, the zone is
1749
+ * grown (and the container's size with it).
1750
+ * - If the shape was someone's child but its final world bounds no longer
1751
+ * intersect the parent's drop-zone → clear `parentId` (drag-out).
1752
+ * - Cycles (a container inside its own descendant) are prevented by the
1753
+ * `containerHover` pipeline above — the exclude set rules them out.
1754
+ */
1755
+ applyContainerDrop(worldPoint: Vec2): void;
1756
+ private maybeGrowContainer;
1757
+ private clampContainerToChildren;
1758
+ /**
1759
+ * Return the running gesture tx, or open a new one if the drag finished
1760
+ * with an empty transaction (a move-by-zero-pixels gesture can still
1761
+ * carry a container reparent).
1762
+ */
1763
+ private beginOrAttachGesture;
1764
+ cancelGesture(): void;
1765
+ /**
1766
+ * Drop ids from the selection that no longer exist in the scene. Needed
1767
+ * after undoing a CREATE — the shape goes away and the selection becomes
1768
+ * stale.
1769
+ */
1770
+ private pruneSelection;
1771
+ notify(): void;
1772
+ /**
1773
+ * Pending `requestAnimationFrame` id for the next render, or null
1774
+ * when no render is scheduled. Used to coalesce bursts of `notify()`
1775
+ * calls (drag-pan, drag shape, multi-key, scripted batch mutations)
1776
+ * into a single render per frame.
1777
+ */
1778
+ private renderRafId;
1779
+ /** Unsubscribe for the animation-content-ready listener (decode → re-render). */
1780
+ private animationContentOff;
1781
+ /**
1782
+ * Schedule a render on the next animation frame. Idempotent —
1783
+ * multiple calls within the same frame collapse to one render.
1784
+ *
1785
+ * Falls back to a synchronous render when `requestAnimationFrame`
1786
+ * is unavailable (Node without jsdom, SSR). Browser / test environments
1787
+ * with rAF get the coalesced path.
1788
+ *
1789
+ * Use {@link forceRender} when you need the render to happen
1790
+ * immediately (PNG export, screenshot, visual-regression tests that
1791
+ * compare bitmap output after a mutation).
1792
+ */
1793
+ private scheduleRender;
1794
+ /**
1795
+ * Synchronously render the current state. Cancels any pending
1796
+ * rAF-scheduled render so the next browser frame doesn't paint
1797
+ * a stale state on top.
1798
+ *
1799
+ * Hosts only need this when they read back the rendered bitmap
1800
+ * immediately after a mutation — `editor.toPng()`, custom
1801
+ * `canvas.toDataURL()` flows, visual-regression test asserts.
1802
+ * Normal interactive flows should let `scheduleRender` do its job.
1803
+ */
1804
+ forceRender(): void;
1805
+ /**
1806
+ * Typed event surface — subscribe to a specific slice (`mode`,
1807
+ * `selection`, `scene`, `history`, `viewport`) or the umbrella
1808
+ * `change`. Replaces ad-hoc selectors over the coarse `subscribe()`
1809
+ * for callers that only care about one dimension. The legacy
1810
+ * `subscribe()` still works and fires in lock-step.
1811
+ */
1812
+ on<K extends keyof EditorEvents>(event: K, fn: EditorEvents[K]): () => void;
1813
+ off<K extends keyof EditorEvents>(event: K, fn: EditorEvents[K]): void;
1814
+ /**
1815
+ * Public command — re-run the auto-layout spec on the given
1816
+ * container shape, regardless of whether the children set actually
1817
+ * changed. Useful as an "auto-arrange" toolbar action or for hosts
1818
+ * adopting auto-layout after creating shapes programmatically.
1819
+ * Returns the patch that was applied, or `null` when nothing
1820
+ * changed (no spec, no children, or children already in position).
1821
+ * Single undo step.
1822
+ */
1823
+ runLayout(parentId: ElementId): Patch | null;
1824
+ /**
1825
+ * Per-link signature of the inputs that determine an elbow route
1826
+ * (endpoint refs + bound-shape bounds + fixedSegments). When unchanged
1827
+ * between frames the A* route is reused — see `rerouteElbows`.
1828
+ */
1829
+ private readonly elbowRouteSig;
1830
+ private elbowSignature;
1831
+ /**
1832
+ * Choke-point reroute (standard model): recompute `routedPoints` for
1833
+ * every orthogonal link whose inputs changed since the last pass, and
1834
+ * bake the result into `_scene`. Runs once per frame before paint —
1835
+ * derived state, so no history push / notify (would loop). Cheap when
1836
+ * nothing moved (signature short-circuit).
1837
+ */
1838
+ private rerouteElbows;
1839
+ private render;
1840
+ }
1841
+ //# sourceMappingURL=editor.d.ts.map