@oh-just-another/state 0.58.0 → 0.60.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 (272) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.md +78 -27
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/actions/actionArrange.d.ts +13 -0
  5. package/dist/actions/actionArrange.d.ts.map +1 -0
  6. package/dist/actions/actionArrange.js +130 -0
  7. package/dist/actions/actionArrange.js.map +1 -0
  8. package/dist/actions/actionClipboard.d.ts +2 -0
  9. package/dist/actions/actionClipboard.d.ts.map +1 -1
  10. package/dist/actions/actionClipboard.js +32 -1
  11. package/dist/actions/actionClipboard.js.map +1 -1
  12. package/dist/actions/actionKeyboard.d.ts.map +1 -1
  13. package/dist/actions/actionKeyboard.js +77 -9
  14. package/dist/actions/actionKeyboard.js.map +1 -1
  15. package/dist/actions/actionMode.d.ts +4 -0
  16. package/dist/actions/actionMode.d.ts.map +1 -1
  17. package/dist/actions/actionMode.js +54 -0
  18. package/dist/actions/actionMode.js.map +1 -1
  19. package/dist/actions/actionSelection.d.ts.map +1 -1
  20. package/dist/actions/actionSelection.js +1 -0
  21. package/dist/actions/actionSelection.js.map +1 -1
  22. package/dist/actions/actionText.d.ts +5 -0
  23. package/dist/actions/actionText.d.ts.map +1 -0
  24. package/dist/actions/actionText.js +37 -0
  25. package/dist/actions/actionText.js.map +1 -0
  26. package/dist/actions/actionView.d.ts +6 -0
  27. package/dist/actions/actionView.d.ts.map +1 -1
  28. package/dist/actions/actionView.js +20 -2
  29. package/dist/actions/actionView.js.map +1 -1
  30. package/dist/actions/actionZoom.d.ts.map +1 -1
  31. package/dist/actions/actionZoom.js +5 -0
  32. package/dist/actions/actionZoom.js.map +1 -1
  33. package/dist/actions/index.d.ts +6 -4
  34. package/dist/actions/index.d.ts.map +1 -1
  35. package/dist/actions/index.js +6 -4
  36. package/dist/actions/index.js.map +1 -1
  37. package/dist/actions/registry.d.ts.map +1 -1
  38. package/dist/actions/registry.js +17 -0
  39. package/dist/actions/registry.js.map +1 -1
  40. package/dist/actions/types.d.ts +10 -1
  41. package/dist/actions/types.d.ts.map +1 -1
  42. package/dist/actions/types.js.map +1 -1
  43. package/dist/auto-layout-scheduler.d.ts +1 -3
  44. package/dist/auto-layout-scheduler.d.ts.map +1 -1
  45. package/dist/auto-layout-scheduler.js +1 -2
  46. package/dist/auto-layout-scheduler.js.map +1 -1
  47. package/dist/built-in-handlers.d.ts.map +1 -1
  48. package/dist/built-in-handlers.js +14 -1
  49. package/dist/built-in-handlers.js.map +1 -1
  50. package/dist/clipboard.d.ts +0 -14
  51. package/dist/clipboard.d.ts.map +1 -1
  52. package/dist/clipboard.js +1 -1
  53. package/dist/clipboard.js.map +1 -1
  54. package/dist/constants.d.ts +208 -41
  55. package/dist/constants.d.ts.map +1 -1
  56. package/dist/constants.js +210 -25
  57. package/dist/constants.js.map +1 -1
  58. package/dist/dom-events.d.ts +8 -2
  59. package/dist/dom-events.d.ts.map +1 -1
  60. package/dist/dom-events.js +12 -0
  61. package/dist/dom-events.js.map +1 -1
  62. package/dist/editor/animation-scene.d.ts +36 -0
  63. package/dist/editor/animation-scene.d.ts.map +1 -0
  64. package/dist/editor/animation-scene.js +194 -0
  65. package/dist/editor/animation-scene.js.map +1 -0
  66. package/dist/editor/animation.d.ts +43 -0
  67. package/dist/editor/animation.d.ts.map +1 -0
  68. package/dist/editor/animation.js +80 -0
  69. package/dist/editor/animation.js.map +1 -0
  70. package/dist/editor/applies/arrange.d.ts +46 -0
  71. package/dist/editor/applies/arrange.d.ts.map +1 -0
  72. package/dist/editor/applies/arrange.js +162 -0
  73. package/dist/editor/applies/arrange.js.map +1 -0
  74. package/dist/editor/applies/create.d.ts +0 -6
  75. package/dist/editor/applies/create.d.ts.map +1 -1
  76. package/dist/editor/applies/create.js +1 -1
  77. package/dist/editor/applies/create.js.map +1 -1
  78. package/dist/editor/applies/edge.d.ts +8 -1
  79. package/dist/editor/applies/edge.d.ts.map +1 -1
  80. package/dist/editor/applies/edge.js +31 -1
  81. package/dist/editor/applies/edge.js.map +1 -1
  82. package/dist/editor/applies/link-move.d.ts +0 -8
  83. package/dist/editor/applies/link-move.d.ts.map +1 -1
  84. package/dist/editor/applies/link-move.js +1 -1
  85. package/dist/editor/applies/link-move.js.map +1 -1
  86. package/dist/editor/applies/move.d.ts +6 -0
  87. package/dist/editor/applies/move.d.ts.map +1 -1
  88. package/dist/editor/applies/move.js +6 -0
  89. package/dist/editor/applies/move.js.map +1 -1
  90. package/dist/editor/applies/resize.d.ts +26 -5
  91. package/dist/editor/applies/resize.d.ts.map +1 -1
  92. package/dist/editor/applies/resize.js +91 -13
  93. package/dist/editor/applies/resize.js.map +1 -1
  94. package/dist/editor/caret-blink.d.ts +18 -0
  95. package/dist/editor/caret-blink.d.ts.map +1 -0
  96. package/dist/editor/caret-blink.js +42 -0
  97. package/dist/editor/caret-blink.js.map +1 -0
  98. package/dist/editor/container-ops.d.ts.map +1 -1
  99. package/dist/editor/container-ops.js +2 -2
  100. package/dist/editor/container-ops.js.map +1 -1
  101. package/dist/editor/dirty-rect.d.ts +23 -0
  102. package/dist/editor/dirty-rect.d.ts.map +1 -0
  103. package/dist/editor/dirty-rect.js +115 -0
  104. package/dist/editor/dirty-rect.js.map +1 -0
  105. package/dist/editor/gif-playback.d.ts +45 -0
  106. package/dist/editor/gif-playback.d.ts.map +1 -0
  107. package/dist/editor/gif-playback.js +125 -0
  108. package/dist/editor/gif-playback.js.map +1 -0
  109. package/dist/editor/hit-test.d.ts +3 -3
  110. package/dist/editor/hit-test.d.ts.map +1 -1
  111. package/dist/editor/hit-test.js +40 -15
  112. package/dist/editor/hit-test.js.map +1 -1
  113. package/dist/editor/interaction-state.d.ts +184 -0
  114. package/dist/editor/interaction-state.d.ts.map +1 -0
  115. package/dist/editor/interaction-state.js +139 -0
  116. package/dist/editor/interaction-state.js.map +1 -0
  117. package/dist/editor/link-handle-drag.d.ts +152 -0
  118. package/dist/editor/link-handle-drag.d.ts.map +1 -0
  119. package/dist/editor/link-handle-drag.js +300 -0
  120. package/dist/editor/link-handle-drag.js.map +1 -0
  121. package/dist/editor/long-press.d.ts.map +1 -1
  122. package/dist/editor/long-press.js +2 -2
  123. package/dist/editor/long-press.js.map +1 -1
  124. package/dist/editor/pinch.d.ts.map +1 -1
  125. package/dist/editor/pinch.js +6 -6
  126. package/dist/editor/pinch.js.map +1 -1
  127. package/dist/editor/pointer-binding.d.ts +9 -11
  128. package/dist/editor/pointer-binding.d.ts.map +1 -1
  129. package/dist/editor/pointer-binding.js +1191 -890
  130. package/dist/editor/pointer-binding.js.map +1 -1
  131. package/dist/editor/public/arrange-group.d.ts +3 -9
  132. package/dist/editor/public/arrange-group.d.ts.map +1 -1
  133. package/dist/editor/public/arrange-group.js +4 -6
  134. package/dist/editor/public/arrange-group.js.map +1 -1
  135. package/dist/editor/public/brush.d.ts +31 -12
  136. package/dist/editor/public/brush.d.ts.map +1 -1
  137. package/dist/editor/public/brush.js +66 -17
  138. package/dist/editor/public/brush.js.map +1 -1
  139. package/dist/editor/public/clipboard.d.ts +2 -4
  140. package/dist/editor/public/clipboard.d.ts.map +1 -1
  141. package/dist/editor/public/clipboard.js +2 -4
  142. package/dist/editor/public/clipboard.js.map +1 -1
  143. package/dist/editor/public/cursor.d.ts +29 -0
  144. package/dist/editor/public/cursor.d.ts.map +1 -0
  145. package/dist/editor/public/cursor.js +146 -0
  146. package/dist/editor/public/cursor.js.map +1 -0
  147. package/dist/editor/public/eraser.d.ts +55 -0
  148. package/dist/editor/public/eraser.d.ts.map +1 -0
  149. package/dist/editor/public/eraser.js +65 -0
  150. package/dist/editor/public/eraser.js.map +1 -0
  151. package/dist/editor/public/frame-name.d.ts +20 -0
  152. package/dist/editor/public/frame-name.d.ts.map +1 -0
  153. package/dist/editor/public/frame-name.js +54 -0
  154. package/dist/editor/public/frame-name.js.map +1 -0
  155. package/dist/editor/public/image-insert.d.ts +1 -1
  156. package/dist/editor/public/image-insert.d.ts.map +1 -1
  157. package/dist/editor/public/laser.d.ts +38 -0
  158. package/dist/editor/public/laser.d.ts.map +1 -0
  159. package/dist/editor/public/laser.js +42 -0
  160. package/dist/editor/public/laser.js.map +1 -0
  161. package/dist/editor/public/layers.d.ts.map +1 -1
  162. package/dist/editor/public/layers.js +2 -2
  163. package/dist/editor/public/layers.js.map +1 -1
  164. package/dist/editor/public/link.d.ts +11 -2
  165. package/dist/editor/public/link.d.ts.map +1 -1
  166. package/dist/editor/public/link.js +70 -2
  167. package/dist/editor/public/link.js.map +1 -1
  168. package/dist/editor/public/placement.d.ts +60 -2
  169. package/dist/editor/public/placement.d.ts.map +1 -1
  170. package/dist/editor/public/placement.js +193 -2
  171. package/dist/editor/public/placement.js.map +1 -1
  172. package/dist/editor/public/selection-ops.d.ts +35 -1
  173. package/dist/editor/public/selection-ops.d.ts.map +1 -1
  174. package/dist/editor/public/selection-ops.js +97 -2
  175. package/dist/editor/public/selection-ops.js.map +1 -1
  176. package/dist/editor/public/stroke-eraser-coverage.d.ts +37 -0
  177. package/dist/editor/public/stroke-eraser-coverage.d.ts.map +1 -0
  178. package/dist/editor/public/stroke-eraser-coverage.js +168 -0
  179. package/dist/editor/public/stroke-eraser-coverage.js.map +1 -0
  180. package/dist/editor/public/stroke-eraser.d.ts +50 -0
  181. package/dist/editor/public/stroke-eraser.d.ts.map +1 -0
  182. package/dist/editor/public/stroke-eraser.js +176 -0
  183. package/dist/editor/public/stroke-eraser.js.map +1 -0
  184. package/dist/editor/public/stroke-smoothing.d.ts +21 -0
  185. package/dist/editor/public/stroke-smoothing.d.ts.map +1 -0
  186. package/dist/editor/public/stroke-smoothing.js +37 -0
  187. package/dist/editor/public/stroke-smoothing.js.map +1 -0
  188. package/dist/editor/public/text-edit.d.ts +1 -10
  189. package/dist/editor/public/text-edit.d.ts.map +1 -1
  190. package/dist/editor/public/text-edit.js +2 -16
  191. package/dist/editor/public/text-edit.js.map +1 -1
  192. package/dist/editor/public/tool-ops.d.ts +130 -0
  193. package/dist/editor/public/tool-ops.d.ts.map +1 -0
  194. package/dist/editor/public/tool-ops.js +392 -0
  195. package/dist/editor/public/tool-ops.js.map +1 -0
  196. package/dist/editor/public/z-order.d.ts +1 -12
  197. package/dist/editor/public/z-order.d.ts.map +1 -1
  198. package/dist/editor/public/z-order.js +5 -5
  199. package/dist/editor/public/z-order.js.map +1 -1
  200. package/dist/editor/public/zoom-pan.d.ts +15 -4
  201. package/dist/editor/public/zoom-pan.d.ts.map +1 -1
  202. package/dist/editor/public/zoom-pan.js +39 -7
  203. package/dist/editor/public/zoom-pan.js.map +1 -1
  204. package/dist/editor/render-orchestrator.d.ts +177 -5
  205. package/dist/editor/render-orchestrator.d.ts.map +1 -1
  206. package/dist/editor/render-orchestrator.js +519 -358
  207. package/dist/editor/render-orchestrator.js.map +1 -1
  208. package/dist/editor/resize-helpers.d.ts +21 -5
  209. package/dist/editor/resize-helpers.d.ts.map +1 -1
  210. package/dist/editor/resize-helpers.js +59 -19
  211. package/dist/editor/resize-helpers.js.map +1 -1
  212. package/dist/editor/text-edit.d.ts +136 -0
  213. package/dist/editor/text-edit.d.ts.map +1 -0
  214. package/dist/editor/text-edit.js +328 -0
  215. package/dist/editor/text-edit.js.map +1 -0
  216. package/dist/editor/viewport-helpers.d.ts.map +1 -1
  217. package/dist/editor/viewport-helpers.js +3 -3
  218. package/dist/editor/viewport-helpers.js.map +1 -1
  219. package/dist/editor-events.d.ts +1 -1
  220. package/dist/editor-events.d.ts.map +1 -1
  221. package/dist/editor.d.ts +603 -346
  222. package/dist/editor.d.ts.map +1 -1
  223. package/dist/editor.js +1843 -1630
  224. package/dist/editor.js.map +1 -1
  225. package/dist/file-drop.d.ts.map +1 -1
  226. package/dist/file-drop.js +1 -2
  227. package/dist/file-drop.js.map +1 -1
  228. package/dist/frame-helpers.d.ts.map +1 -1
  229. package/dist/frame-helpers.js +5 -5
  230. package/dist/frame-helpers.js.map +1 -1
  231. package/dist/group-helpers.d.ts.map +1 -1
  232. package/dist/group-helpers.js +4 -4
  233. package/dist/group-helpers.js.map +1 -1
  234. package/dist/handle.d.ts +59 -6
  235. package/dist/handle.d.ts.map +1 -1
  236. package/dist/handle.js +99 -7
  237. package/dist/handle.js.map +1 -1
  238. package/dist/hotkeys-binding.d.ts +26 -0
  239. package/dist/hotkeys-binding.d.ts.map +1 -0
  240. package/dist/hotkeys-binding.js +35 -0
  241. package/dist/hotkeys-binding.js.map +1 -0
  242. package/dist/index.d.ts +9 -3
  243. package/dist/index.d.ts.map +1 -1
  244. package/dist/index.js +10 -3
  245. package/dist/index.js.map +1 -1
  246. package/dist/link-selection.d.ts +3 -5
  247. package/dist/link-selection.d.ts.map +1 -1
  248. package/dist/link-selection.js +3 -12
  249. package/dist/link-selection.js.map +1 -1
  250. package/dist/machine.d.ts +27 -1
  251. package/dist/machine.d.ts.map +1 -1
  252. package/dist/machine.js +38 -0
  253. package/dist/machine.js.map +1 -1
  254. package/dist/modes.d.ts +15 -1
  255. package/dist/modes.d.ts.map +1 -1
  256. package/dist/modes.js.map +1 -1
  257. package/dist/overlay.d.ts +92 -6
  258. package/dist/overlay.d.ts.map +1 -1
  259. package/dist/overlay.js +582 -125
  260. package/dist/overlay.js.map +1 -1
  261. package/dist/search.d.ts +37 -0
  262. package/dist/search.d.ts.map +1 -0
  263. package/dist/search.js +45 -0
  264. package/dist/search.js.map +1 -0
  265. package/dist/selection.d.ts.map +1 -1
  266. package/dist/selection.js +2 -10
  267. package/dist/selection.js.map +1 -1
  268. package/dist/util.d.ts +8 -0
  269. package/dist/util.d.ts.map +1 -0
  270. package/dist/util.js +17 -0
  271. package/dist/util.js.map +1 -0
  272. package/package.json +7 -7
package/dist/editor.d.ts CHANGED
@@ -1,17 +1,24 @@
1
1
  import { type Actor } from "xstate";
2
2
  import type { Bounds, FileId, ElementId, Vec2 } from "@oh-just-another/types";
3
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";
4
+ import { type FractionalIndex, type BrushPoint, type Link, type Patch, type Scene, type Element, type GridStyle, type ImageCrop, type TextStyle } from "@oh-just-another/scene";
5
+ import { type AnnotationId, type Color, type CommentId, type LinkId, type LayerId } from "@oh-just-another/types";
6
6
  import { ElementCache, type RenderTarget, type TextShaper, type Rasterizer } from "@oh-just-another/renderer-core";
7
7
  import { type HistoryOptions, type HistoryProvider, type TransactionHandle } from "@oh-just-another/history";
8
8
  import { type FileDropHandler } from "./file-drop.js";
9
9
  import { interactionMachine, type InteractionContext, type InteractionEmit, type PressTarget } from "./machine.js";
10
10
  import type { Mode } from "./modes.js";
11
11
  import type { EditorEvents } from "./editor-events.js";
12
+ import { GifPlaybackController } from "./editor/gif-playback.js";
12
13
  import { LongPressController } from "./editor/long-press.js";
13
14
  import { PinchController } from "./editor/pinch.js";
14
- import { type BrushStrokeState } from "./editor/public/brush.js";
15
+ import { InteractionState, type AnnotationDrag, type ContainerHover, type EdgePreview, type GroupResizeOrigin, type HoveredLinkTarget, type LinkDragFromAnchor, type PanGesture, type RotateGestureOrigin } from "./editor/interaction-state.js";
16
+ import { type BrushSettings, type BrushStrokeState } from "./editor/public/brush.js";
17
+ import { type EraseStrokeState } from "./editor/public/eraser.js";
18
+ import { type LaserStroke } from "./editor/public/laser.js";
19
+ import type { CursorRole, CursorSpec } from "./editor/public/cursor.js";
20
+ import { type ConvertTarget, type CropHandle, type SpawnDirection } from "./editor/public/tool-ops.js";
21
+ import { type AlignEdge, type DistributeAxis, type FlipAxis } from "./editor/applies/arrange.js";
15
22
  import { type PeerCursor, type PeerSelection } from "./overlay.js";
16
23
  import * as Selection from "./selection.js";
17
24
  import * as LinkSelection from "./link-selection.js";
@@ -50,10 +57,17 @@ export interface EditorOptions {
50
57
  readonly onAfterRender?: () => void;
51
58
  readonly initialScene: Scene;
52
59
  readonly initialMode?: Mode;
60
+ /**
61
+ * Start the editor in read-only / view mode. Pointer edits (create /
62
+ * move / resize / rotate / delete) and non-`viewMode` actions are gated;
63
+ * pan / zoom / select still work. Toggle at runtime via
64
+ * {@link Editor.setReadOnly}. Defaults to `false`.
65
+ */
66
+ readonly readOnly?: boolean;
53
67
  /**
54
68
  * Pre-existing history backend, or options for the default
55
69
  * `History` (linear stack). Any `HistoryProvider` implementation
56
- * works — `@oh-just-another/collab` ships `YjsHistory` that wraps
70
+ * works — `@oh-just-another/collab` ships `CollabHistory` that wraps
57
71
  * `Y.UndoManager` for CRDT-aware undo in collaborative sessions.
58
72
  */
59
73
  readonly history?: HistoryProvider | HistoryOptions;
@@ -124,6 +138,15 @@ export type TileComposeFn = (scene: Scene, mainTarget: RenderTarget, options: {
124
138
  after: Bounds | null;
125
139
  }>;
126
140
  readonly zoomBucket: number;
141
+ /**
142
+ * Persistent spatial index over the scene's current element world-AABBs,
143
+ * when the editor maintains one (large scenes, shared with the hit-test
144
+ * path). A tile compositor that supports it (`renderViaTiles`) queries the
145
+ * index for per-tile element selection instead of scanning every shape in
146
+ * every layer. Omitted for small scenes; compositors must fall back to a
147
+ * full scan when absent.
148
+ */
149
+ readonly index?: SpatialGrid;
127
150
  }) => void;
128
151
  /**
129
152
  * Top-level interaction controller. Owns the scene + selection state, wires
@@ -138,29 +161,7 @@ export type GroupSelectedResult = {
138
161
  readonly kind: "grouped";
139
162
  readonly groupId: ElementId;
140
163
  };
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 type { CursorRole, CursorSpec };
164
165
  export declare class Editor {
165
166
  readonly host: HTMLElement;
166
167
  readonly mainTarget: RenderTarget;
@@ -175,11 +176,14 @@ export declare class Editor {
175
176
  */
176
177
  debugHitZones: boolean;
177
178
  /**
178
- * When false the background grid is not painted. Toggled via `toggleGrid`
179
- * (`g` hotkey, standard parity). View-only never persisted or in history.
179
+ * Read-only / view mode. When true the pointer paths that create,
180
+ * move, resize, rotate or delete are gated (pan / zoom / select stay
181
+ * live) and the action registry only runs actions flagged
182
+ * `viewMode`. View-only — never persisted or recorded in history.
183
+ * Read via {@link readOnly}; flip via {@link setReadOnly}.
180
184
  */
181
- gridVisible: boolean;
182
- readonly actor: Actor<typeof interactionMachine>;
185
+ private _readOnly;
186
+ actor: Actor<typeof interactionMachine>;
183
187
  private readonly listeners;
184
188
  /**
185
189
  * Typed event surface. Specific events (`mode`, `selection`,
@@ -193,7 +197,7 @@ export declare class Editor {
193
197
  private readonly events;
194
198
  /**
195
199
  * Last-emitted snapshot of every observable slice. Used by
196
- * `fanOutEvents` (in `editor/event-fanout.ts`) to decide which
200
+ * `fanOutEvents` to decide which
197
201
  * typed events to fire on each `notify()` — only the slices
198
202
  * whose identity changed since the previous notify get an event.
199
203
  */
@@ -202,74 +206,31 @@ export declare class Editor {
202
206
  _scene: Scene;
203
207
  _selection: Selection.Selection;
204
208
  /**
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
+ * Ephemeral interaction / gesture state (previews, gesture origins,
210
+ * transient modifiers). Single source of truth for the short-lived fields
211
+ * the pointer handlers, render orchestrator and container-ops read/write
212
+ * while a gesture is in flight. The public fields below delegate to it so
213
+ * external writers keep referencing `editor.<field>` unchanged.
209
214
  */
210
- annotationDrag: {
211
- id: AnnotationId;
212
- originPosition: Vec2;
213
- originWorldPoint: Vec2;
214
- moved: boolean;
215
- } | null;
215
+ readonly interaction: InteractionState;
216
+ /** Snapshot of an in-progress annotation-pin drag. */
217
+ get annotationDrag(): AnnotationDrag | null;
218
+ set annotationDrag(v: AnnotationDrag | null);
216
219
  /** 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;
220
+ get drawingPreview(): Bounds | null;
221
+ set drawingPreview(v: Bounds | null);
222
+ /** Live preview of an edge being drawn. */
223
+ get edgePreview(): EdgePreview | null;
224
+ set edgePreview(v: EdgePreview | null);
225
+ /** Active "drag a link from a start-anchor" gesture. */
226
+ get linkDragFromAnchor(): LinkDragFromAnchor | null;
227
+ set linkDragFromAnchor(v: LinkDragFromAnchor | null);
228
+ /** Element hovered while draw-edge mode is active (drives the port overlay). */
229
+ get hoveredLinkTarget(): HoveredLinkTarget | null;
230
+ set hoveredLinkTarget(v: HoveredLinkTarget | null);
231
+ /** Last idle cursor position (world) in select mode grows the nearest dot. */
232
+ get hoverCursorWorld(): Vec2 | null;
233
+ set hoverCursorWorld(v: Vec2 | null);
273
234
  /**
274
235
  * Currently selected links (connectors). Links are first-class members
275
236
  * of the selection: they coexist with selected elements, join Cmd+A and
@@ -295,8 +256,9 @@ export declare class Editor {
295
256
  /**
296
257
  * Mid-drag preview state when the user is dragging an edge endpoint.
297
258
  * Drawn as an overlay line + handle dot so the user sees the target.
259
+ * State lives in `LinkHandleDragController`; this is a delegate.
298
260
  */
299
- linkEndpointDrag: {
261
+ get linkEndpointDrag(): {
300
262
  linkId: LinkId;
301
263
  side: "from" | "to";
302
264
  toPoint: Vec2;
@@ -307,70 +269,40 @@ export declare class Editor {
307
269
  * began on a segment midpoint and will splice a new waypoint on the
308
270
  * first move (so a no-move click adds nothing). Live-mutated through the
309
271
  * gesture transaction → one undo step per drag.
272
+ * State lives in `LinkHandleDragController`; this is a delegate.
310
273
  */
311
- linkWaypointDrag: {
274
+ get linkWaypointDrag(): {
312
275
  linkId: LinkId;
313
276
  index: number;
314
277
  pendingInsert: boolean;
315
278
  } | null;
316
279
  /**
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.
280
+ * Host-managed elbow segment drag. `axis` is the segment's orientation.
281
+ * Dragging pins the segment's perpendicular coordinate into
282
+ * `Link.fixedSegments`; the reroute pass re-flows the rest. One undo
283
+ * step via the gesture tx.
284
+ * State lives in `LinkHandleDragController`; this is a delegate.
321
285
  */
322
- linkSegmentDrag: {
286
+ get linkSegmentDrag(): {
323
287
  linkId: LinkId;
324
288
  axis: "h" | "v";
325
289
  at: number;
326
290
  } | null;
327
291
  /** 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;
292
+ get lassoPreview(): Bounds | null;
293
+ set lassoPreview(v: Bounds | null);
294
+ /** Snapshot of every selected shape's `position` at press-down (multi-drag). */
295
+ get groupMoveOrigin(): ReadonlyMap<ElementId, Vec2> | null;
296
+ set groupMoveOrigin(v: ReadonlyMap<ElementId, Vec2> | null);
297
+ /** Press-time snapshot of connectors that follow a multi-element drag rigidly. */
298
+ get groupLinkMoveOrigin(): ReadonlyMap<LinkId, Link> | null;
299
+ set groupLinkMoveOrigin(v: ReadonlyMap<LinkId, Link> | null);
300
+ /** Per-shape snapshot for a group-resize gesture. */
301
+ get groupResizeOrigin(): GroupResizeOrigin | null;
302
+ set groupResizeOrigin(v: GroupResizeOrigin | null);
303
+ /** Press-time snapshot for a rotate gesture. */
304
+ get rotateGestureOrigin(): RotateGestureOrigin | null;
305
+ set rotateGestureOrigin(v: RotateGestureOrigin | null);
374
306
  /**
375
307
  * Active layer — new shapes created via `addElement` / `applyCreate` land
376
308
  * here when their input doesn't specify a `layerId`. Defaults to the
@@ -387,12 +319,11 @@ export declare class Editor {
387
319
  /** Snap threshold in world units. */
388
320
  private readonly snapThreshold;
389
321
  /**
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.
322
+ * In-editor style memory for copy-style / paste-style. Holds the visual
323
+ * `style` (fill / stroke / dash / …) captured from a shape; `null` until a
324
+ * copy happens. Not the OS clipboard a lightweight per-editor buffer.
394
325
  */
395
- private snapSuppressed;
326
+ private styleClipboard;
396
327
  /**
397
328
  * Persistent world-bounds cache shared with `renderScene` for viewport
398
329
  * culling. Object-identity keyed — invalidates automatically whenever
@@ -414,36 +345,37 @@ export declare class Editor {
414
345
  * escape, click outside the group's descendants, or `cancelInteraction`.
415
346
  */
416
347
  _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
348
  /**
435
349
  * In-progress brush stroke. Hosts push points via
436
350
  * `extendBrushStroke`; the overlay reads it through
437
351
  * `pendingBrushStroke` to draw a live preview.
438
352
  */
439
- brushStroke: BrushStrokeState | null;
353
+ get brushStroke(): BrushStrokeState | null;
354
+ set brushStroke(v: BrushStrokeState | null);
355
+ /**
356
+ * The in-progress brush stroke with its captured vertices Catmull-Rom-smoothed
357
+ * for the LIVE overlay preview — the SAME resampler `commitBrushStroke` applies
358
+ * on release (see {@link smoothBrushPoints}), so the stroke reads smooth while
359
+ * drawn instead of snapping from an angular polyline to a curve on release. A
360
+ * fresh object each call (points diverge from `brushStroke.points`), so the
361
+ * overlay memo repaints every move that grows the stroke.
362
+ */
363
+ private get brushPreviewStroke();
364
+ /** In-progress eraser stroke (pending-delete set), or null between strokes. */
365
+ get eraseStroke(): EraseStrokeState | null;
366
+ set eraseStroke(v: EraseStrokeState | null);
367
+ /** Ids swept by the current eraser stroke — previewed dimmed, deleted on release. */
368
+ get pendingErase(): ReadonlySet<ElementId>;
369
+ /** Live laser-pointer trails (ephemeral, fading). Empty when none active. */
370
+ get laserStrokes(): readonly LaserStroke[];
440
371
  /**
441
372
  * Last world-space pointer position observed by the host's onMove
442
373
  * handler. `paste()` uses it as the default drop target so a fresh
443
374
  * paste lands under the cursor instead of overlapping the originals.
444
375
  * `null` until the pointer first enters the host.
445
376
  */
446
- lastPointerWorld: Vec2 | null;
377
+ get lastPointerWorld(): Vec2 | null;
378
+ set lastPointerWorld(v: Vec2 | null);
447
379
  /** Host-registered custom cursor images per role (see `setCursorOverride`). */
448
380
  private readonly cursorOverrides;
449
381
  /**
@@ -462,17 +394,32 @@ export declare class Editor {
462
394
  * pass would never visibly apply.
463
395
  */
464
396
  lastRenderedEnteredGroup: ElementId | null;
397
+ /**
398
+ * Whether the last paint had eraser-dim active — paired with
399
+ * `lastRenderedScene` like {@link lastRenderedEnteredGroup}. When an eraser
400
+ * stroke ENDS by cancel (Esc), the marked shapes un-dim without a scene
401
+ * change, so the dirty-rect diff is empty and the dim would linger on screen;
402
+ * this lets that active→inactive transition force one full repaint.
403
+ */
404
+ lastRenderedEraseActive: boolean;
405
+ /**
406
+ * Set whenever an eraser move actually CHANGES the marked / cut set (a new
407
+ * shape marked, un-marked, or a brush point cut). Gates the forced full
408
+ * repaint during erasing: only the frames that change the preview repaint the
409
+ * whole scene; a slowly-moving or stopped cursor over already-covered area
410
+ * skips the expensive main pass (only the overlay cursor / trail refresh).
411
+ * Cleared after each paint.
412
+ */
413
+ private eraseDirty;
465
414
  /**
466
415
  * Fractional-order compaction scheduler (microtask-coalesced).
467
416
  * Triggered from every `notify()`; only does real work when at
468
417
  * least one shape/edge order string crossed AUTO_COMPACT_THRESHOLD.
469
- * See `./auto-compact.ts` for the extracted logic.
470
418
  */
471
419
  private readonly autoCompactScheduler;
472
420
  /**
473
421
  * Auto-layout scheduler — microtask-coalesced re-run of every
474
- * shape carrying `metadata.autoLayout`. See
475
- * `./auto-layout-scheduler.ts` for the extracted logic.
422
+ * shape carrying `metadata.autoLayout`.
476
423
  */
477
424
  private readonly autoLayoutScheduler;
478
425
  /**
@@ -482,7 +429,8 @@ export declare class Editor {
482
429
  * gestures, set in onDown when press lands on a shape and cleared
483
430
  * in onUp / cancel.
484
431
  */
485
- dragElementId: ElementId | null;
432
+ get dragElementId(): ElementId | null;
433
+ set dragElementId(v: ElementId | null);
486
434
  /**
487
435
  * Element that the current press added to the selection additively
488
436
  * (shift / meta click on an unselected shape). The press promotes it
@@ -490,17 +438,16 @@ export declare class Editor {
490
438
  * otherwise `SELECT_TOGGLE` it straight back off, so it consults this
491
439
  * to skip that redundant toggle. Reset at every press-down.
492
440
  */
493
- additivePressAdded: ElementId | null;
441
+ get additivePressAdded(): ElementId | null;
442
+ set additivePressAdded(v: ElementId | null);
494
443
  /**
495
444
  * Live container highlight: the container shape the dragged item is
496
445
  * currently hovering over. Drawn by the overlay as a dashed
497
446
  * accent rect on the container's drop-zone so the user sees where the
498
447
  * shape will land after release.
499
448
  */
500
- containerHover: {
501
- id: ElementId;
502
- dropZone: Bounds;
503
- } | null;
449
+ get containerHover(): ContainerHover | null;
450
+ set containerHover(v: ContainerHover | null);
504
451
  /**
505
452
  * Remote peer cursors / selections, pushed in by the host (typically
506
453
  * a `bindAwareness(editor, awareness)` helper in `@collab`). The
@@ -520,7 +467,7 @@ export declare class Editor {
520
467
  * two or more entries we enter a pinch / pan gesture and bypass the
521
468
  * interaction machine — `pinchOrigin` holds the baseline.
522
469
  */
523
- readonly activePointers: Map<number, Vec2>;
470
+ get activePointers(): Map<number, Vec2>;
524
471
  /**
525
472
  * One-finger-pan candidate: set at pointer-down when a TOUCH press lands
526
473
  * on empty canvas in select mode. A tap (no movement) still falls through
@@ -528,9 +475,10 @@ export declare class Editor {
528
475
  * this to a real pan instead of a marquee lasso (mobile convention).
529
476
  * Screen-space origin point.
530
477
  */
531
- touchPanCandidate: Vec2 | null;
478
+ get touchPanCandidate(): Vec2 | null;
479
+ set touchPanCandidate(v: Vec2 | null);
532
480
  pinch: PinchController;
533
- /** Bridge for `editor/container-ops.ts`. Built lazily in constructor. */
481
+ /** Bridge for the container-ops helpers. Built lazily in constructor. */
534
482
  private containerOpsRef;
535
483
  /**
536
484
  * Space-bar held → next pointer drag pans the canvas instead of
@@ -538,7 +486,8 @@ export declare class Editor {
538
486
  * "grab" / "grabbing". Wires a window-level keydown/keyup listener
539
487
  * in `bindPointerEvents`.
540
488
  */
541
- spaceHeld: boolean;
489
+ get spaceHeld(): boolean;
490
+ set spaceHeld(v: boolean);
542
491
  /**
543
492
  * Host-supplied tile compositor — when set (via
544
493
  * `EditorOptions.useTileCache` + `tileCompose`), the per-frame
@@ -588,20 +537,16 @@ export declare class Editor {
588
537
  * only treat right-click releases as potential context-menu
589
538
  * triggers (Space + left-drag never opens a menu).
590
539
  */
591
- panGesture: {
592
- pointerId: number;
593
- button: number;
594
- startPoint: Vec2;
595
- lastPoint: Vec2;
596
- moved: boolean;
597
- } | null;
540
+ get panGesture(): PanGesture | null;
541
+ set panGesture(v: PanGesture | null);
598
542
  /**
599
543
  * Set on a right-click pointerdown so the upcoming native
600
544
  * `contextmenu` event can be unconditionally preventDefault'ed
601
545
  * (the gesture decides whether to fire the menu manually on
602
546
  * pointerup based on whether the user dragged).
603
547
  */
604
- suppressNextContextMenu: boolean;
548
+ get suppressNextContextMenu(): boolean;
549
+ set suppressNextContextMenu(v: boolean);
605
550
  /**
606
551
  * Long-press tracking. Starts on `pointerdown`; cancelled on
607
552
  * `pointermove > LONG_PRESS_MAX_MOVEMENT_PX` or `pointerup` before
@@ -621,13 +566,13 @@ export declare class Editor {
621
566
  * in the constructor from `EditorOptions.inputMode` (default `"auto"`
622
567
  * uses `matchMedia('(pointer: coarse)')`).
623
568
  */
624
- private readonly inputMode;
625
- private readonly handleHitSlop;
626
- private readonly edgeHandleHitSlop;
627
- private readonly edgeHitThreshold;
569
+ private inputMode;
570
+ private handleHitSlop;
571
+ private edgeHandleHitSlop;
572
+ private edgeHitThreshold;
628
573
  /** Link-start anchor-dot grab/click hit radii — touch-enlarged in touch mode. */
629
- readonly anchorStartHitSlop: number;
630
- readonly anchorClickRadius: number;
574
+ anchorStartHitSlop: number;
575
+ anchorClickRadius: number;
631
576
  readonly _history: HistoryProvider;
632
577
  /** Open transaction during a single drag/resize gesture. */
633
578
  gestureTx: TransactionHandle | null;
@@ -642,13 +587,66 @@ export declare class Editor {
642
587
  /**
643
588
  * Wraps gesture lifecycle (transaction open/commit/cancel +
644
589
  * 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
590
+ * The controller calls back through the narrow `GestureRef` bridge
647
591
  * built lazily below.
648
592
  */
649
- private readonly gestures;
593
+ private gestures;
594
+ /**
595
+ * Owns the inline text-edit session (edited shape, pending creation,
596
+ * origin snapshot, live selection, drag anchor and caret blink).
597
+ * Editor keeps thin delegate wrappers so the public API is unchanged.
598
+ */
599
+ private textEdit;
600
+ /**
601
+ * Owns the link edit-handle drags (waypoint / segment / endpoint) and
602
+ * the handle double-click detector. Editor keeps thin delegate
603
+ * wrappers so the public API is unchanged.
604
+ */
605
+ private linkHandles;
650
606
  constructor(options: EditorOptions);
651
- /** Snapshot used by event-fanout. Kept private — internal API. */
607
+ /**
608
+ * Build the interaction controllers (gestures, text edit, link-handle
609
+ * drag, long-press, pinch) and the container-ops bridge. Each wires to a
610
+ * narrow getter/setter surface over the editor's mutable fields, so the
611
+ * controllers live in their own modules without importing Editor. The
612
+ * getters/setters in the object literals rebind `this`, so a single
613
+ * `self` alias captures the Editor reference for all of them.
614
+ */
615
+ private initControllers;
616
+ /**
617
+ * Install process-global hooks the host opted into: a custom text shaper
618
+ * and rasterizer for the WebGL2 backend, plus the scene text measurer that
619
+ * routes through the renderer's own metrics so selection boxes hug text.
620
+ */
621
+ private initGlobalHooks;
622
+ /**
623
+ * Resolve the input mode (`touch` vs `mouse`, `auto` reads the coarse-
624
+ * pointer media query) and the derived hit slops / thresholds once.
625
+ */
626
+ private initInputMode;
627
+ /**
628
+ * Create and start the interaction state-machine actor, wire its render /
629
+ * emit subscriptions, register the built-in file-drop handlers, and apply
630
+ * the initial mode.
631
+ */
632
+ private initActor;
633
+ /**
634
+ * Last {@link observableSnapshot} object, reused while none of its slices
635
+ * have flipped. `null` until the first snapshot is built.
636
+ */
637
+ private snapshotCache;
638
+ /**
639
+ * Snapshot used by event-fanout. Kept private — internal API.
640
+ *
641
+ * Memoized by slice identity: `notify()` fires on many mutations that touch
642
+ * no observable slice (annotation focus, cursor pushes, viewport-only re-arm),
643
+ * so rebuilding the object every call is pure churn on the hot drag path.
644
+ * We reuse the cached object whenever all six slices compare equal (refs for
645
+ * mode/selection/selectedLinks/scene — scene uses structural sharing so a new
646
+ * ref iff something changed — plus the two history booleans), and only
647
+ * allocate a fresh one on a real flip. `fanOutEvents` sees identical values
648
+ * either way, so emitted events are unchanged.
649
+ */
652
650
  private observableSnapshot;
653
651
  get scene(): Scene;
654
652
  get selection(): Selection.Selection;
@@ -740,9 +738,24 @@ export declare class Editor {
740
738
  * values can be eyeballed. View-only — not recorded in history.
741
739
  */
742
740
  setDebugHitZones(on: boolean): void;
743
- /** Show/hide the background grid (standard `g`). View-only — not in history. */
741
+ /**
742
+ * Read-only / view mode flag. `true` gates pointer edits and
743
+ * non-`viewMode` actions while leaving pan / zoom / select live.
744
+ */
745
+ get readOnly(): boolean;
746
+ /**
747
+ * Enter / leave read-only (view) mode. Notifies subscribers so the UI
748
+ * can re-render disabled chrome, and repaints (no visual diff today, but
749
+ * keeps the contract symmetric with other view toggles). Idempotent.
750
+ */
751
+ setReadOnly(on: boolean): void;
752
+ /** Toggle read-only (view) mode. */
753
+ toggleReadOnly(): void;
754
+ /** Whether the background grid is enabled for the scene. */
755
+ get gridEnabled(): boolean;
756
+ /** Show/hide the background grid (`g`). Persists in the viewport, not in history. */
744
757
  setGridVisible(on: boolean): void;
745
- /** Toggle background grid visibility. */
758
+ /** Toggle the background grid on/off. */
746
759
  toggleGrid(): void;
747
760
  /** Whether the active draw-mode sticks after a create (toolbar lock). */
748
761
  get toolLocked(): boolean;
@@ -817,7 +830,7 @@ export declare class Editor {
817
830
  width: number;
818
831
  height: number;
819
832
  position: Vec2;
820
- image?: HTMLImageElement;
833
+ image?: ImageBitmap | HTMLImageElement;
821
834
  animated?: boolean;
822
835
  fileId?: FileId;
823
836
  animationKind?: string;
@@ -831,24 +844,12 @@ export declare class Editor {
831
844
  * current frame of natively-animated elements. Self-terminates
832
845
  * when no animated shapes remain.
833
846
  *
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.
847
+ * Lifecycle managed by the `AnimationTick` helper.
848
+ * `insertImage({animated:true})` and `loadScene` start the tick;
849
+ * `dispose()` stops it.
850
850
  */
851
- private hasVisibleAnimatedElement;
851
+ private readonly animation;
852
+ private hasAnimatedElement;
852
853
  /**
853
854
  * Re-arm the animation tick after a change that may have brought an
854
855
  * animated shape into (or out of) view — pan / zoom / scene edit.
@@ -856,29 +857,12 @@ export declare class Editor {
856
857
  * `isAnimated()` is false, so this is cheap to call from `notify()`.
857
858
  */
858
859
  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;
860
+ /**
861
+ * Per-shape GIF playback state (auto-stop + reduced-motion). Extracted into
862
+ * a controller; the Editor keeps the orchestration (scene iteration,
863
+ * animation tick, render scheduling) and delegates state ops here.
864
+ */
865
+ readonly gifPlayback: GifPlaybackController;
882
866
  /**
883
867
  * Toggle GIF playback for a shape — wired to a click on an animated
884
868
  * image (resume after auto-stop, play after reduced-motion). Resuming
@@ -908,25 +892,6 @@ export declare class Editor {
908
892
  elementId: ElementId;
909
893
  mode: "point" | "element";
910
894
  } | 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
895
  /**
931
896
  * Drag-to-place flow for palette templates. Adds the shape to the
932
897
  * scene immediately so the user sees it dragging under the cursor,
@@ -948,11 +913,10 @@ export declare class Editor {
948
913
  * cleared by `commitTextEdit` / `cancelTextEdit`. The host overlay
949
914
  * (`<TextEditorOverlay>` in `@react-ui`) subscribes via `editor`
950
915
  * and renders a `<textarea>` positioned over the shape.
916
+ * State lives in `TextEditController`; this is a delegate.
951
917
  */
952
- private _editingTextElement;
953
918
  get editingTextElement(): ElementId | null;
954
919
  /** Link whose caption is being edited inline (double-click), or null. */
955
- private _editingLinkCaption;
956
920
  get editingLinkCaption(): LinkId | null;
957
921
  /**
958
922
  * Frame whose NAME (header label) is being edited inline (double-click
@@ -961,30 +925,6 @@ export declare class Editor {
961
925
  */
962
926
  private _editingFrameName;
963
927
  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
928
  get editingTextSelection(): {
989
929
  start: number;
990
930
  end: number;
@@ -995,10 +935,6 @@ export declare class Editor {
995
935
  get caretBlinkOn(): boolean;
996
936
  /** `true` while a canvas drag-select inside the edited text is active. */
997
937
  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
938
  /**
1003
939
  * Begin editing a text shape's body. No-op when the shape doesn't
1004
940
  * exist or isn't a text shape. Concurrent edits commit themselves
@@ -1064,15 +1000,6 @@ export declare class Editor {
1064
1000
  extendTextSelectionToPoint(worldPoint: Vec2): void;
1065
1001
  /** End a canvas drag-select (clears the drag anchor). */
1066
1002
  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
1003
  /**
1077
1004
  * World-space caret + selection geometry for the overlay pass.
1078
1005
  * Returns `null` when not editing. The caret is `null` while blinked
@@ -1115,6 +1042,15 @@ export declare class Editor {
1115
1042
  * the tool is locked.
1116
1043
  */
1117
1044
  createTextAt(worldPoint: Vec2): ElementId;
1045
+ /** Current brush paint settings (line colour, fill, opacity, width). */
1046
+ private _brushSettings;
1047
+ get brushSettings(): BrushSettings;
1048
+ /**
1049
+ * Update one or more brush paint settings (e.g. from the drawing panel). New
1050
+ * strokes pick them up on commit; the width also drives the pressure curve and
1051
+ * the eraser radius. Merges over the current settings.
1052
+ */
1053
+ setBrushSettings(patch: Partial<BrushSettings>): void;
1118
1054
  beginBrushStroke(world: Vec2, pressure?: number): void;
1119
1055
  extendBrushStroke(world: Vec2, pressure?: number): void;
1120
1056
  commitBrushStroke(): ElementId | null;
@@ -1124,6 +1060,53 @@ export declare class Editor {
1124
1060
  readonly origin: Vec2;
1125
1061
  readonly points: readonly BrushPoint[];
1126
1062
  } | null;
1063
+ /**
1064
+ * Start an eraser stroke at `world`, seeding it with the shape under it so a
1065
+ * plain click erases. With `restore` (Alt held at press) it seeds nothing —
1066
+ * the gesture is in un-mark mode, and there's nothing marked yet to rescue.
1067
+ */
1068
+ beginEraseStroke(world: Vec2, restore?: boolean, strokeErase?: boolean): void;
1069
+ /**
1070
+ * Incrementally mark the brush points erased by the eraser segment `a → b`
1071
+ * (world). Iterates the current brushes and grows `stroke.erased` in place —
1072
+ * O(points) per move (each already-erased point is skipped), so a long drag
1073
+ * no longer costs O(points × path length) per frame. Radius is the on-screen
1074
+ * eraser ring converted to world units.
1075
+ */
1076
+ private markStrokeEraseSegment;
1077
+ /**
1078
+ * Extend the eraser stroke to `world`, sweeping shapes along the segment.
1079
+ * `restore` (Alt held) un-marks swept shapes instead of marking them.
1080
+ */
1081
+ extendEraseStroke(world: Vec2, restore?: boolean): void;
1082
+ /**
1083
+ * Commit the eraser stroke — delete every swept shape in ONE undo step (with
1084
+ * their attached links). No-op delete when nothing was swept. Returns the
1085
+ * count removed.
1086
+ */
1087
+ commitEraseStroke(): number;
1088
+ /** Abort the eraser stroke without deleting anything. */
1089
+ cancelEraseStroke(): void;
1090
+ /** True only while the pointer is down in laser mode (a trail is being laid). */
1091
+ get laserDrawing(): boolean;
1092
+ /** Start a laser trail at `world` (ephemeral — never enters the scene). */
1093
+ beginLaserStroke(world: Vec2): void;
1094
+ /** Append a point to the active laser trail (no-op unless drawing). */
1095
+ extendLaserStroke(world: Vec2): void;
1096
+ /** End the active laser trail — it keeps fading via the animation tick. */
1097
+ endLaserStroke(): void;
1098
+ /**
1099
+ * True while any laser trail OR eraser trail still has visible points (drives
1100
+ * the fade tick — so the trail keeps melting after the pointer stops).
1101
+ */
1102
+ hasActiveLaser(): boolean;
1103
+ /** Live eraser drag trail (ephemeral, fading). Empty when none active. */
1104
+ get eraserTrail(): readonly LaserStroke[];
1105
+ /**
1106
+ * Drop expired laser/eraser trail points (called once per frame before paint).
1107
+ * Self-terminating: once both arrays empty the animation tick stops.
1108
+ */
1109
+ private pruneLaser;
1127
1110
  arrangeAsGrid(opts?: {
1128
1111
  cols?: number;
1129
1112
  gap?: number;
@@ -1157,6 +1140,14 @@ export declare class Editor {
1157
1140
  */
1158
1141
  duplicateSelectedInPlace(anchorId?: ElementId | null): ElementId | null;
1159
1142
  setSelection(ids: Iterable<ElementId>): void;
1143
+ /**
1144
+ * Programmatically select a single link by id (or clear the link
1145
+ * selection with `null`), clearing the element selection so the link
1146
+ * becomes the sole selection. Used by host navigation (search / jump-to)
1147
+ * to frame an edge with {@link zoomToSelection}. No-op when nothing
1148
+ * would change.
1149
+ */
1150
+ selectLink(id: LinkId | null): void;
1160
1151
  selectAll(): void;
1161
1152
  /**
1162
1153
  * Internal clipboard. Stored as deep-cloned snapshots so subsequent
@@ -1185,7 +1176,190 @@ export declare class Editor {
1185
1176
  *
1186
1177
  * No-op when `ids` is empty or none of the targeted shapes exist.
1187
1178
  */
1179
+ /**
1180
+ * Capture the visual style of the first selected element into the style
1181
+ * buffer, for a later {@link pasteSelectionStyle}. No-op / clears nothing
1182
+ * when the selection is empty.
1183
+ */
1184
+ copySelectionStyle(): void;
1185
+ /** Apply the copied style (if any) to every selected element. One undo step. */
1186
+ pasteSelectionStyle(): void;
1187
+ /** Whether a style has been copied and can be pasted. */
1188
+ get hasStyleClipboard(): boolean;
1188
1189
  updateStyle(ids: Iterable<ElementId>, partial: Partial<TextStyle>): void;
1190
+ /**
1191
+ * Apply a partial text style (bold / italic / colour / decoration) to the
1192
+ * character range `[from, to)` of a single text element, producing styled
1193
+ * runs (rich text). One undo step. No-op when the id isn't a text shape or
1194
+ * the range is empty. Read-only editors ignore it. Use this — rather than
1195
+ * `updateStyle` — to style only PART of a text block (e.g. the current
1196
+ * inline-edit selection).
1197
+ */
1198
+ applyTextStyleToRange(id: ElementId, from: number, to: number, partial: Partial<TextStyle>): void;
1199
+ /**
1200
+ * The fill (or stroke, per `role`) colour of the top-most shape under the
1201
+ * world point, or `null` on empty canvas. Pure read — no mutation.
1202
+ */
1203
+ pickColorAt(worldPoint: Vec2, role?: "fill" | "stroke"): Color | null;
1204
+ /** One-shot callback armed by {@link beginEyedropperPick}; consumes the next canvas click. */
1205
+ private pendingEyedropperPick;
1206
+ /** `true` while a colour-picker pipette is armed and waiting for a canvas click. */
1207
+ get isEyedropperArmed(): boolean;
1208
+ /**
1209
+ * Arm the eyedropper for a one-shot canvas pick that routes the sampled colour
1210
+ * to `onPick` (e.g. a colour-picker swatch) instead of the selection fill. Does
1211
+ * NOT change the tool mode — the next canvas press is intercepted by
1212
+ * {@link applyEyedropperAt}. Cancelled by a mode switch or an empty-canvas click.
1213
+ */
1214
+ beginEyedropperPick(onPick: (color: Color) => void): void;
1215
+ /**
1216
+ * Sample the colour under `worldPoint`. When a pipette pick is armed (see
1217
+ * {@link beginEyedropperPick}), route the colour to that callback and disarm.
1218
+ * Otherwise (legacy tool path) apply it as the current selection's fill and
1219
+ * revert to `select` mode. Returns the sampled colour, or `null` on empty
1220
+ * canvas. Read-only editors sample but don't mutate.
1221
+ */
1222
+ applyEyedropperAt(worldPoint: Vec2): Color | null;
1223
+ /**
1224
+ * Convert every convertible selected shape (rectangle / ellipse / diamond)
1225
+ * to `target`, preserving position, size and style. One undo step; no-op
1226
+ * when nothing applies. See {@link ConvertTarget}.
1227
+ */
1228
+ convertSelection(target: ConvertTarget): void;
1229
+ /**
1230
+ * Flowchart auto-generate: clone the single selected node offset in
1231
+ * `direction` and connect the two with a fresh link. Selects the new node.
1232
+ * No-op unless exactly one element is selected. One undo step.
1233
+ */
1234
+ spawnConnectedNode(direction: SpawnDirection): void;
1235
+ /**
1236
+ * Pending flowchart-create session, or `null` when idle. Holds the ORIGINAL
1237
+ * source id + direction, the current sibling `count`, and the pending
1238
+ * `elements` + `links` (a PREVIEW — not yet in the scene / history). Grown by
1239
+ * {@link growFlowchart}, committed by {@link commitFlowchart}, discarded by
1240
+ * {@link cancelFlowchart}.
1241
+ */
1242
+ private flowchartSession;
1243
+ /**
1244
+ * Grow the flowchart-create preview one step in `direction`. Starts a session
1245
+ * (count = 1) when idle or when the direction changes; otherwise bumps the
1246
+ * sibling count up to {@link FLOWCHART_MAX_SIBLINGS}. Recomputes the pending
1247
+ * nodes/links from the ORIGINAL source each call. PREVIEW ONLY — never
1248
+ * touches the scene or history until {@link commitFlowchart}. No-op in
1249
+ * read-only mode or unless exactly one element is selected.
1250
+ */
1251
+ growFlowchart(direction: SpawnDirection): void;
1252
+ /**
1253
+ * Commit the pending flowchart-create preview: add every pending node + link
1254
+ * to the scene as ONE undo step, select the first new node, clear the
1255
+ * session. Returns the first new node's id, or `null` when no session is
1256
+ * active.
1257
+ */
1258
+ commitFlowchart(): ElementId | null;
1259
+ /** Discard the pending flowchart-create preview without committing. */
1260
+ cancelFlowchart(): void;
1261
+ /**
1262
+ * The pending flowchart-create preview (nodes + links), or `null` when no
1263
+ * session is active. Read by the render snapshot to paint the preview on the
1264
+ * overlay. Reference-stable between renders (only changes on grow / commit /
1265
+ * cancel) so the overlay memo doesn't thrash.
1266
+ */
1267
+ get flowchartPreview(): {
1268
+ readonly elements: readonly Element[];
1269
+ readonly links: readonly Link[];
1270
+ } | null;
1271
+ /**
1272
+ * Move the selection to an adjacent node. With exactly one element selected,
1273
+ * prefers a graph neighbour (linked node) best aligned with `direction`;
1274
+ * falls back to the spatial {@link selectClosest} when no neighbour lies that
1275
+ * way. No-op unless exactly one element is selected.
1276
+ */
1277
+ navigateFlowchart(direction: "left" | "right" | "up" | "down"): void;
1278
+ /**
1279
+ * Live image-crop session, or `null` when not cropping. Excalidraw-style:
1280
+ * the crop frame IS the element's visible box, and the user drags edge /
1281
+ * corner handles inward (hides pixels) or the image body (pans the source).
1282
+ *
1283
+ * - `id` — the image being cropped.
1284
+ * - `crop` — pending normalised source rect.
1285
+ * - `position` / `width` / `height` — the pending element box (world position
1286
+ * + local size); a handle drag moves them, a body pan leaves them fixed.
1287
+ * - `drag` — the active gesture, or `null` when only hovering.
1288
+ * - `dragStartWorld` — pointer world position at drag start (body pan basis).
1289
+ *
1290
+ * Seeded on {@link beginImageCrop}; committed by {@link commitImageCrop} (one
1291
+ * undo step), abandoned by {@link cancelImageCrop}.
1292
+ */
1293
+ cropSession: {
1294
+ id: ElementId;
1295
+ crop: ImageCrop;
1296
+ position: Vec2;
1297
+ width: number;
1298
+ height: number;
1299
+ drag: {
1300
+ kind: "handle";
1301
+ handle: CropHandle;
1302
+ } | {
1303
+ kind: "body";
1304
+ } | null;
1305
+ dragStartWorld: Vec2 | null;
1306
+ } | null;
1307
+ /** The image-crop session (read-only accessor for UI / overlay). */
1308
+ get imageCropSession(): {
1309
+ readonly id: ElementId;
1310
+ readonly crop: ImageCrop;
1311
+ readonly position: Vec2;
1312
+ readonly width: number;
1313
+ readonly height: number;
1314
+ } | null;
1315
+ /**
1316
+ * Enter crop mode for the image `id`, seeding the pending crop / box from its
1317
+ * current state (or the full image). No-op for non-image shapes or in
1318
+ * read-only. Typically triggered by a double-click on an image.
1319
+ */
1320
+ beginImageCrop(id: ElementId): void;
1321
+ /**
1322
+ * Hit-test `worldPoint` against the pending crop chrome: a crop handle when
1323
+ * within {@link CROP_HANDLE_HIT_RADIUS} (screen px, zoom-compensated) of one,
1324
+ * `"body"` when inside the window, else `null`. Returns `null` when not
1325
+ * cropping.
1326
+ */
1327
+ cropHandleAtWorld(worldPoint: Vec2): CropHandle | "body" | null;
1328
+ /** Begin dragging crop handle `handle` from `worldPoint`. */
1329
+ beginImageCropHandle(handle: CropHandle, worldPoint: Vec2): void;
1330
+ /** Begin panning the image body under the fixed window from `worldPoint`. */
1331
+ beginImageCropBody(worldPoint: Vec2): void;
1332
+ /**
1333
+ * Update the active crop drag to `worldPoint` — resize the window (handle) or
1334
+ * pan the source (body). Geometry is recomputed from the ORIGINAL element so
1335
+ * it stays stable across many moves. No-op when no drag is active.
1336
+ */
1337
+ updateImageCropDrag(worldPoint: Vec2): void;
1338
+ /** Finish the current crop drag (keeps the pending crop / box). */
1339
+ endImageCropDrag(): void;
1340
+ /** Apply the pending crop + box and leave crop mode. One undo step. */
1341
+ commitImageCrop(): void;
1342
+ /** Abandon the crop session without changing the image. */
1343
+ cancelImageCrop(): void;
1344
+ /**
1345
+ * Synthetic element carrying the PENDING crop box (position / size) over the
1346
+ * original element's rotation / scale — the frame the user currently sees.
1347
+ * Used to project the crop frame and handles.
1348
+ */
1349
+ private pendingCropElement;
1350
+ /**
1351
+ * World-space corners (clockwise) of the pending crop frame, or `null` when
1352
+ * not cropping. The frame is the pending element box mapped through its
1353
+ * local→world transform (so rotation / scale are honoured).
1354
+ */
1355
+ private cropFrameCorners;
1356
+ /**
1357
+ * Ghost-image overlay descriptor for the crop session: the ORIGINAL element
1358
+ * (its transform + live bitmap handle) and the virtual full-image LOCAL rect
1359
+ * the whole bitmap occupies. `null` when not cropping. The overlay paints the
1360
+ * full bitmap faintly over this rect so hidden parts stay visible.
1361
+ */
1362
+ private cropGhost;
1189
1363
  /**
1190
1364
  * Update non-style text properties (`fontSize`, `fontFamily`,
1191
1365
  * `maxWidth`) on every selected text shape. Non-text shapes are
@@ -1196,6 +1370,12 @@ export declare class Editor {
1196
1370
  fontFamily?: string;
1197
1371
  maxWidth?: number;
1198
1372
  }): void;
1373
+ /**
1374
+ * Step the font size of every selected text shape up or down by one gentle
1375
+ * multiplicative increment (each shape relative to its own size). One
1376
+ * undoable step; no-op when no text is selected.
1377
+ */
1378
+ adjustSelectionFontSize(direction: "increase" | "decrease"): void;
1199
1379
  /**
1200
1380
  * Set (or clear, with `null`) the element-level hyperlink (`href`) on
1201
1381
  * every shape in `ids`. Single undo step. Pass a raw user string — it
@@ -1229,6 +1409,32 @@ export declare class Editor {
1229
1409
  bringForward(id?: ElementId): void;
1230
1410
  /** Move the target shape one step toward the bottom of its layer. */
1231
1411
  sendBackward(id?: ElementId): void;
1412
+ /**
1413
+ * Mirror the current selection about its bounding-box centre on the given
1414
+ * axis — `horizontal` flips left↔right, `vertical` flips top↔bottom. A single
1415
+ * element flips about its own centre. One undoable step.
1416
+ */
1417
+ flipSelection(axis: FlipAxis): void;
1418
+ /**
1419
+ * Align the selection to the given edge / centre line of its bounding box
1420
+ * (e.g. `left` flushes every shape's left edge; `h-center` lines up
1421
+ * horizontal centres). Needs two or more elements. One undoable step.
1422
+ */
1423
+ alignSelection(edge: AlignEdge): void;
1424
+ /**
1425
+ * Evenly space the selection along the given axis so the gaps between
1426
+ * adjacent shapes are equal; the outermost shapes stay put. Needs three or
1427
+ * more elements. One undoable step.
1428
+ */
1429
+ distributeSelection(axis: DistributeAxis): void;
1430
+ /**
1431
+ * Rotate the whole selection by `delta` radians about its bounding-box centre
1432
+ * (a single shape turns about its own centre). One undoable step. The live
1433
+ * rotate gesture drives the same maths from a press-time snapshot.
1434
+ */
1435
+ rotateSelection(delta: number): void;
1436
+ /** Apply a batch of arrange patches as a single undoable step. */
1437
+ private commitArrange;
1232
1438
  compactLayerZOrder(layerId?: LayerId, options?: {
1233
1439
  recordHistory?: boolean;
1234
1440
  }): void;
@@ -1262,6 +1468,13 @@ export declare class Editor {
1262
1468
  zoomToFit(padding?: number): void;
1263
1469
  /** Fit the camera to the current selection (standard `⌥2`). No-op when empty. */
1264
1470
  zoomToSelection(padding?: number): void;
1471
+ /**
1472
+ * Center the camera on the current selection for a reveal / jump-to (search
1473
+ * navigation). Unlike {@link zoomToSelection}, it does NOT fill the screen —
1474
+ * a small match keeps its size and is merely centered; the zoom only drops to
1475
+ * fit an oversized match. No-op when the selection is empty.
1476
+ */
1477
+ revealSelection(padding?: number): void;
1265
1478
  /**
1266
1479
  * Select the nearest interactable top-level element in `direction` from the
1267
1480
  * current selection's centre (or the viewport centre when nothing is
@@ -1273,7 +1486,7 @@ export declare class Editor {
1273
1486
  zoomAt(factor: number, anchorWorld: Vec2): void;
1274
1487
  setViewportSize(width: number, height: number): void;
1275
1488
  setGrid(patch: {
1276
- size?: number;
1489
+ enabled?: boolean;
1277
1490
  style?: GridStyle;
1278
1491
  snap?: boolean;
1279
1492
  }): void;
@@ -1288,13 +1501,20 @@ export declare class Editor {
1288
1501
  */
1289
1502
  setSnapSuppressed(suppressed: boolean): void;
1290
1503
  /**
1291
- * True when a gesture should snap. Snapping is coupled to grid *display*:
1292
- * it is active only while a grid is actually shown the toggle is on
1293
- * (`gridVisible`, `g` hotkey) AND the scene has a positive `gridSize` (the
1294
- * same condition `renderGrid` paints under). Snapping to an invisible grid
1295
- * is confusing, so no grid → no snap, always. `snapToGrid` is an extra
1296
- * programmatic opt-out; the suppress modifier (Cmd/Ctrl) bypasses snapping
1297
- * for the current gesture.
1504
+ * Host hook: mirror the Alt / Shift modifier state so an in-flight resize or
1505
+ * move reacts to it `alt` resizes about the centre, `shift` locks the
1506
+ * resize aspect ratio (or constrains a move to one axis). The app wires
1507
+ * keydown/keyup of the modifiers to this. Idempotent; never touches history.
1508
+ */
1509
+ setTransformModifiers(mods: {
1510
+ readonly alt: boolean;
1511
+ readonly shift: boolean;
1512
+ }): void;
1513
+ /**
1514
+ * True when a gesture should snap. Snapping is coupled to grid display:
1515
+ * it is active only while the grid is enabled (`gridEnabled`) — snapping to
1516
+ * a hidden grid is confusing. `snapToGrid` is an extra programmatic opt-out;
1517
+ * the suppress modifier (Cmd/Ctrl) bypasses snapping for the current gesture.
1298
1518
  */
1299
1519
  private snapActive;
1300
1520
  /** World-unit spacing the current gesture snaps to. */
@@ -1350,16 +1570,6 @@ export declare class Editor {
1350
1570
  * out of sync. `worldPoint` defaults to the last known pointer position.
1351
1571
  */
1352
1572
  refreshCursor(worldPoint?: Vec2): void;
1353
- /**
1354
- * The CSS cursor for the current state. Priority: active gesture → text edit → pan affordance →
1355
- * draw tool → idle hover hit-test. Pure read of editor state; no side effects.
1356
- */
1357
- private computeCursor;
1358
- /**
1359
- * Resolve a cursor role to a CSS `cursor` value: a host-registered custom
1360
- * image (via {@link setCursorOverride}) if present, else `fallbackKeyword`.
1361
- */
1362
- private resolveCursor;
1363
1573
  /**
1364
1574
  * Register (or clear, with `null`) a custom cursor image for a state role.
1365
1575
  * The image is shown wherever `computeCursor` resolves that role; pass a
@@ -1369,12 +1579,11 @@ export declare class Editor {
1369
1579
  */
1370
1580
  setCursorOverride(role: CursorRole, spec: CursorSpec | null): void;
1371
1581
  /**
1372
- * True when `p` is within the grab radius of one of the single selected
1373
- * element's link-start dots used to show a `crosshair` (start a link).
1374
- * Mirrors the anchor-drag hit-test in pointer-binding so the cursor matches
1375
- * exactly where a press would begin a link.
1582
+ * Read-only lookup of a host-registered cursor override. Used by the cursor
1583
+ * module to resolve a role without exposing the mutable override map (mutate
1584
+ * only via {@link setCursorOverride}, so `refreshCursor` stays in sync).
1376
1585
  */
1377
- private isOverLinkStartDot;
1586
+ getCursorOverride(role: CursorRole): CursorSpec | undefined;
1378
1587
  /** True when the given layer exists and is marked `locked`. */
1379
1588
  private isLayerLocked;
1380
1589
  /**
@@ -1418,8 +1627,7 @@ export declare class Editor {
1418
1627
  * Topmost group ancestor of `shape` (walks parentId chain, returns
1419
1628
  * the highest `type === "group"` parent). `null` if `shape` has no
1420
1629
  * group ancestor. Used by drill-down: a double-click on a shape
1421
- * with a group ancestor enters that group. Body extracted to
1422
- * `./group-helpers.ts`.
1630
+ * with a group ancestor enters that group.
1423
1631
  */
1424
1632
  private topGroupAncestor;
1425
1633
  /**
@@ -1443,7 +1651,22 @@ export declare class Editor {
1443
1651
  * selected, you can see".
1444
1652
  */
1445
1653
  computeHiddenElements(): ReadonlySet<ElementId> | undefined;
1654
+ /**
1655
+ * Live stroke-erase preview: while a Shift-held eraser gesture drags, the
1656
+ * fragments each touched brush WOULD become, plus the set of touched
1657
+ * originals to hide in the main pass. `null` outside a stroke-erase gesture
1658
+ * or when the path touches no brush. Recomputed each frame from the
1659
+ * path-so-far — never mutates the scene or history.
1660
+ */
1661
+ private computeStrokeErasePreview;
1446
1662
  computeDimElements(enteredGroupId: ElementId): ReadonlySet<ElementId>;
1663
+ /**
1664
+ * Dim set fed to the renderer: group-isolation dim UNION the eraser's
1665
+ * pending-delete set (shapes swept by the current eraser stroke are shown
1666
+ * dimmed so the user sees what release will delete). `undefined` when neither
1667
+ * is active, keeping the fast tile-cache render path.
1668
+ */
1669
+ private computeDimSet;
1447
1670
  /**
1448
1671
  * Enter a group — subsequent hits inside this group return children
1449
1672
  * directly instead of the group root. `null` exits group-edit mode.
@@ -1583,6 +1806,13 @@ export declare class Editor {
1583
1806
  * grouping is the explicit "lock the ratio" gesture).
1584
1807
  */
1585
1808
  selectionIsAspectLocked(): boolean;
1809
+ /**
1810
+ * Per-frame rotate during a grip drag: turn the press-time snapshot by
1811
+ * `deltaAngle` about its pivot. Shift snaps the swept angle to
1812
+ * {@link ROTATE_SNAP_RADIANS} steps. Recorded as gesture patches (one undo
1813
+ * step on commit).
1814
+ */
1815
+ private applyRotate;
1586
1816
  private applyGroupResize;
1587
1817
  private applyResize;
1588
1818
  private applyCreate;
@@ -1808,9 +2038,8 @@ export declare class Editor {
1808
2038
  /**
1809
2039
  * Typed event surface — subscribe to a specific slice (`mode`,
1810
2040
  * `selection`, `scene`, `history`, `viewport`) or the umbrella
1811
- * `change`. Replaces ad-hoc selectors over the coarse `subscribe()`
1812
- * for callers that only care about one dimension. The legacy
1813
- * `subscribe()` still works and fires in lock-step.
2041
+ * `change`. For callers that only care about one dimension;
2042
+ * `subscribe()` fires in lock-step with these.
1814
2043
  */
1815
2044
  on<K extends keyof EditorEvents>(event: K, fn: EditorEvents[K]): () => void;
1816
2045
  off<K extends keyof EditorEvents>(event: K, fn: EditorEvents[K]): void;
@@ -1825,20 +2054,48 @@ export declare class Editor {
1825
2054
  */
1826
2055
  runLayout(parentId: ElementId): Patch | null;
1827
2056
  /**
1828
- * Per-link signature of the inputs that determine an elbow route
1829
- * (endpoint refs + bound-shape bounds + fixedSegments). When unchanged
1830
- * between frames the A* route is reused see `rerouteElbows`.
2057
+ * Derived elbow-route cache, keyed by link the source of truth for
2058
+ * A*-routed corners, living OUTSIDE the immutable `Scene`. Each entry holds
2059
+ * the routed interior `points` (between from/to) plus the `sig` of the
2060
+ * inputs it was computed from (endpoint refs + bound-shape bounds +
2061
+ * fixedSegments), so an unchanged link short-circuits the reroute.
2062
+ *
2063
+ * `rerouteElbows` still MIRRORS `points` onto `Link.routedPoints` in
2064
+ * `_scene` because three readers still consume the baked field: the render
2065
+ * path (`getLinkPath` in `renderer-core`), the headless `getLinkPath`, and
2066
+ * serialization (`schema.ts`). The mirror is compat-only derived state — no
2067
+ * history push / notify.
2068
+ *
2069
+ * TODO(fable R7a): drop the `_scene` mirror once (a) the render path reads
2070
+ * routes from this cache via the `RenderSnapshot`, and (b) headless
2071
+ * `getLinkPath` / serialization stop depending on baked `routedPoints`.
2072
+ * That eviction changes headless geometry, serialized output, and
2073
+ * collab-synced fields, so it spans the scene / serialization / renderer /
2074
+ * headless goldens and must land as its own cross-package change — out of
2075
+ * scope for this state-only pass.
2076
+ */
2077
+ private readonly elbowRoutes;
2078
+ /**
2079
+ * Choke-point reroute (standard model): recompute the route for every
2080
+ * orthogonal link whose inputs changed since the last pass, store it in the
2081
+ * derived {@link elbowRoutes} cache, and mirror it onto `_scene`. Runs once
2082
+ * per frame before paint — derived state, so no history push / notify (would
2083
+ * loop). Cheap when nothing moved (signature short-circuit).
1831
2084
  */
1832
- private readonly elbowRouteSig;
1833
- private elbowSignature;
2085
+ private rerouteElbows;
1834
2086
  /**
1835
- * Choke-point reroute (standard model): recompute `routedPoints` for
1836
- * every orthogonal link whose inputs changed since the last pass, and
1837
- * bake the result into `_scene`. Runs once per frame before paint
1838
- * derived state, so no history push / notify (would loop). Cheap when
1839
- * nothing moved (signature short-circuit).
2087
+ * Collect everything {@link renderEditor} paints from into a flat
2088
+ * {@link RenderSnapshot}. Resolves the derived viewport / dirty-rect / dim /
2089
+ * hide inputs and the shared spatial index up front (same order the
2090
+ * orchestrator used to call them in), so the paint pass stays side-effect
2091
+ * free and the orchestrator stays decoupled from this class.
2092
+ *
2093
+ * `computeDirtyWorld` is order-sensitive (it diffs against
2094
+ * `lastRenderedScene` and populates `tileDirtyElements`); it runs here and
2095
+ * the `lastRendered*` bookkeeping is applied in `render()` after the paint.
1840
2096
  */
1841
- private rerouteElbows;
2097
+ private buildRenderSnapshot;
1842
2098
  private render;
1843
2099
  }
2100
+ /** Distance from point `p` to the finite segment `a`–`b` (world space). */
1844
2101
  //# sourceMappingURL=editor.d.ts.map