@oh-just-another/state 0.59.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 (149) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +2 -0
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/actions/actionArrange.js +4 -4
  5. package/dist/actions/actionArrange.js.map +1 -1
  6. package/dist/actions/actionClipboard.d.ts.map +1 -1
  7. package/dist/actions/actionClipboard.js +5 -0
  8. package/dist/actions/actionClipboard.js.map +1 -1
  9. package/dist/actions/actionKeyboard.d.ts.map +1 -1
  10. package/dist/actions/actionKeyboard.js +77 -9
  11. package/dist/actions/actionKeyboard.js.map +1 -1
  12. package/dist/actions/actionMode.d.ts +4 -0
  13. package/dist/actions/actionMode.d.ts.map +1 -1
  14. package/dist/actions/actionMode.js +54 -0
  15. package/dist/actions/actionMode.js.map +1 -1
  16. package/dist/actions/actionSelection.d.ts.map +1 -1
  17. package/dist/actions/actionSelection.js +1 -0
  18. package/dist/actions/actionSelection.js.map +1 -1
  19. package/dist/actions/actionView.d.ts +6 -0
  20. package/dist/actions/actionView.d.ts.map +1 -1
  21. package/dist/actions/actionView.js +19 -1
  22. package/dist/actions/actionView.js.map +1 -1
  23. package/dist/actions/actionZoom.d.ts.map +1 -1
  24. package/dist/actions/actionZoom.js +5 -0
  25. package/dist/actions/actionZoom.js.map +1 -1
  26. package/dist/actions/index.d.ts +3 -3
  27. package/dist/actions/index.d.ts.map +1 -1
  28. package/dist/actions/index.js +3 -3
  29. package/dist/actions/index.js.map +1 -1
  30. package/dist/actions/registry.d.ts.map +1 -1
  31. package/dist/actions/registry.js +13 -0
  32. package/dist/actions/registry.js.map +1 -1
  33. package/dist/actions/types.d.ts +9 -0
  34. package/dist/actions/types.d.ts.map +1 -1
  35. package/dist/actions/types.js.map +1 -1
  36. package/dist/clipboard.d.ts +0 -14
  37. package/dist/clipboard.d.ts.map +1 -1
  38. package/dist/clipboard.js +1 -1
  39. package/dist/clipboard.js.map +1 -1
  40. package/dist/constants.d.ts +160 -18
  41. package/dist/constants.d.ts.map +1 -1
  42. package/dist/constants.js +162 -2
  43. package/dist/constants.js.map +1 -1
  44. package/dist/dom-events.d.ts +1 -2
  45. package/dist/dom-events.d.ts.map +1 -1
  46. package/dist/dom-events.js.map +1 -1
  47. package/dist/editor/animation-scene.d.ts +15 -0
  48. package/dist/editor/animation-scene.d.ts.map +1 -1
  49. package/dist/editor/animation-scene.js +123 -0
  50. package/dist/editor/animation-scene.js.map +1 -1
  51. package/dist/editor/applies/create.d.ts +0 -6
  52. package/dist/editor/applies/create.d.ts.map +1 -1
  53. package/dist/editor/applies/create.js +1 -1
  54. package/dist/editor/applies/create.js.map +1 -1
  55. package/dist/editor/applies/link-move.d.ts +0 -8
  56. package/dist/editor/applies/link-move.d.ts.map +1 -1
  57. package/dist/editor/applies/link-move.js +1 -1
  58. package/dist/editor/applies/link-move.js.map +1 -1
  59. package/dist/editor/interaction-state.d.ts +184 -0
  60. package/dist/editor/interaction-state.d.ts.map +1 -0
  61. package/dist/editor/interaction-state.js +139 -0
  62. package/dist/editor/interaction-state.js.map +1 -0
  63. package/dist/editor/link-handle-drag.d.ts +152 -0
  64. package/dist/editor/link-handle-drag.d.ts.map +1 -0
  65. package/dist/editor/link-handle-drag.js +300 -0
  66. package/dist/editor/link-handle-drag.js.map +1 -0
  67. package/dist/editor/pointer-binding.d.ts +9 -11
  68. package/dist/editor/pointer-binding.d.ts.map +1 -1
  69. package/dist/editor/pointer-binding.js +1185 -901
  70. package/dist/editor/pointer-binding.js.map +1 -1
  71. package/dist/editor/public/brush.d.ts +31 -12
  72. package/dist/editor/public/brush.d.ts.map +1 -1
  73. package/dist/editor/public/brush.js +66 -17
  74. package/dist/editor/public/brush.js.map +1 -1
  75. package/dist/editor/public/cursor.d.ts +1 -15
  76. package/dist/editor/public/cursor.d.ts.map +1 -1
  77. package/dist/editor/public/cursor.js +14 -3
  78. package/dist/editor/public/cursor.js.map +1 -1
  79. package/dist/editor/public/eraser.d.ts +55 -0
  80. package/dist/editor/public/eraser.d.ts.map +1 -0
  81. package/dist/editor/public/eraser.js +65 -0
  82. package/dist/editor/public/eraser.js.map +1 -0
  83. package/dist/editor/public/laser.d.ts +38 -0
  84. package/dist/editor/public/laser.d.ts.map +1 -0
  85. package/dist/editor/public/laser.js +42 -0
  86. package/dist/editor/public/laser.js.map +1 -0
  87. package/dist/editor/public/placement.d.ts.map +1 -1
  88. package/dist/editor/public/placement.js +9 -1
  89. package/dist/editor/public/placement.js.map +1 -1
  90. package/dist/editor/public/selection-ops.d.ts +12 -0
  91. package/dist/editor/public/selection-ops.d.ts.map +1 -1
  92. package/dist/editor/public/selection-ops.js +19 -1
  93. package/dist/editor/public/selection-ops.js.map +1 -1
  94. package/dist/editor/public/stroke-eraser-coverage.d.ts +37 -0
  95. package/dist/editor/public/stroke-eraser-coverage.d.ts.map +1 -0
  96. package/dist/editor/public/stroke-eraser-coverage.js +168 -0
  97. package/dist/editor/public/stroke-eraser-coverage.js.map +1 -0
  98. package/dist/editor/public/stroke-eraser.d.ts +50 -0
  99. package/dist/editor/public/stroke-eraser.d.ts.map +1 -0
  100. package/dist/editor/public/stroke-eraser.js +176 -0
  101. package/dist/editor/public/stroke-eraser.js.map +1 -0
  102. package/dist/editor/public/stroke-smoothing.d.ts +21 -0
  103. package/dist/editor/public/stroke-smoothing.d.ts.map +1 -0
  104. package/dist/editor/public/stroke-smoothing.js +37 -0
  105. package/dist/editor/public/stroke-smoothing.js.map +1 -0
  106. package/dist/editor/public/tool-ops.d.ts +130 -0
  107. package/dist/editor/public/tool-ops.d.ts.map +1 -0
  108. package/dist/editor/public/tool-ops.js +392 -0
  109. package/dist/editor/public/tool-ops.js.map +1 -0
  110. package/dist/editor/public/z-order.d.ts +1 -12
  111. package/dist/editor/public/z-order.d.ts.map +1 -1
  112. package/dist/editor/public/z-order.js +1 -1
  113. package/dist/editor/public/z-order.js.map +1 -1
  114. package/dist/editor/public/zoom-pan.d.ts +9 -0
  115. package/dist/editor/public/zoom-pan.d.ts.map +1 -1
  116. package/dist/editor/public/zoom-pan.js +31 -0
  117. package/dist/editor/public/zoom-pan.js.map +1 -1
  118. package/dist/editor/render-orchestrator.d.ts +177 -5
  119. package/dist/editor/render-orchestrator.d.ts.map +1 -1
  120. package/dist/editor/render-orchestrator.js +517 -351
  121. package/dist/editor/render-orchestrator.js.map +1 -1
  122. package/dist/editor/text-edit.d.ts +136 -0
  123. package/dist/editor/text-edit.d.ts.map +1 -0
  124. package/dist/editor/text-edit.js +328 -0
  125. package/dist/editor/text-edit.js.map +1 -0
  126. package/dist/editor.d.ts +504 -239
  127. package/dist/editor.d.ts.map +1 -1
  128. package/dist/editor.js +1557 -706
  129. package/dist/editor.js.map +1 -1
  130. package/dist/handle.d.ts +0 -2
  131. package/dist/handle.d.ts.map +1 -1
  132. package/dist/handle.js +0 -6
  133. package/dist/handle.js.map +1 -1
  134. package/dist/index.d.ts +6 -1
  135. package/dist/index.d.ts.map +1 -1
  136. package/dist/index.js +7 -1
  137. package/dist/index.js.map +1 -1
  138. package/dist/modes.d.ts +15 -1
  139. package/dist/modes.d.ts.map +1 -1
  140. package/dist/modes.js.map +1 -1
  141. package/dist/overlay.d.ts +91 -2
  142. package/dist/overlay.d.ts.map +1 -1
  143. package/dist/overlay.js +450 -93
  144. package/dist/overlay.js.map +1 -1
  145. package/dist/search.d.ts +37 -0
  146. package/dist/search.d.ts.map +1 -0
  147. package/dist/search.js +45 -0
  148. package/dist/search.js.map +1 -0
  149. package/package.json +4 -4
package/dist/editor.d.ts CHANGED
@@ -1,8 +1,8 @@
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";
@@ -12,8 +12,12 @@ import type { EditorEvents } from "./editor-events.js";
12
12
  import { GifPlaybackController } from "./editor/gif-playback.js";
13
13
  import { LongPressController } from "./editor/long-press.js";
14
14
  import { PinchController } from "./editor/pinch.js";
15
- 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";
16
19
  import type { CursorRole, CursorSpec } from "./editor/public/cursor.js";
20
+ import { type ConvertTarget, type CropHandle, type SpawnDirection } from "./editor/public/tool-ops.js";
17
21
  import { type AlignEdge, type DistributeAxis, type FlipAxis } from "./editor/applies/arrange.js";
18
22
  import { type PeerCursor, type PeerSelection } from "./overlay.js";
19
23
  import * as Selection from "./selection.js";
@@ -53,6 +57,13 @@ export interface EditorOptions {
53
57
  readonly onAfterRender?: () => void;
54
58
  readonly initialScene: Scene;
55
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;
56
67
  /**
57
68
  * Pre-existing history backend, or options for the default
58
69
  * `History` (linear stack). Any `HistoryProvider` implementation
@@ -127,6 +138,15 @@ export type TileComposeFn = (scene: Scene, mainTarget: RenderTarget, options: {
127
138
  after: Bounds | null;
128
139
  }>;
129
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;
130
150
  }) => void;
131
151
  /**
132
152
  * Top-level interaction controller. Owns the scene + selection state, wires
@@ -155,7 +175,15 @@ export declare class Editor {
155
175
  * View-only — never persisted or recorded in history.
156
176
  */
157
177
  debugHitZones: boolean;
158
- readonly actor: Actor<typeof interactionMachine>;
178
+ /**
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}.
184
+ */
185
+ private _readOnly;
186
+ actor: Actor<typeof interactionMachine>;
159
187
  private readonly listeners;
160
188
  /**
161
189
  * Typed event surface. Specific events (`mode`, `selection`,
@@ -178,74 +206,31 @@ export declare class Editor {
178
206
  _scene: Scene;
179
207
  _selection: Selection.Selection;
180
208
  /**
181
- * Snapshot of an in-progress annotation drag (press on pin → move
182
- * pointer → release). `originPosition` is the annotation's stored
183
- * position at press time; per-move handler computes a delta from
184
- * 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.
185
214
  */
186
- annotationDrag: {
187
- id: AnnotationId;
188
- originPosition: Vec2;
189
- originWorldPoint: Vec2;
190
- moved: boolean;
191
- } | 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);
192
219
  /** Live preview while drawing a new shape; null when not drawing. */
193
- drawingPreview: Bounds | null;
194
- edgePreview: {
195
- from: Vec2;
196
- to: Vec2;
197
- points?: readonly Vec2[];
198
- } | null;
199
- /**
200
- * Active "drag a link from a start-anchor" gesture. Set when a
201
- * press lands on one of the selected element's link-start dots; lets
202
- * the user draw a link straight from the dot without switching to the
203
- * draw-edge tool. `fromWorld` is the true anchor world point (the link
204
- * origin, un-offset); `origin` is the press point (for the drag
205
- * threshold). Read by the pointer handlers (drive preview / commit on
206
- * up) and the render orchestrator (keep the source's start dots visible
207
- * during the drag). Null when no such drag is in flight. */
208
- linkDragFromAnchor: {
209
- fromElement: ElementId;
210
- fromWorld: Vec2;
211
- /** Named anchor the gesture started on — drives the click-to-create
212
- * direction (outward normal) and the source link endpoint. */
213
- anchorName: string;
214
- origin: Vec2;
215
- moved: boolean;
216
- } | null;
217
- /**
218
- * Element being hovered while draw-edge mode is active. Drives the port-
219
- * overlay render so the user sees attachment points. `null` outside
220
- * draw-edge mode or when the pointer is over empty canvas.
221
- */
222
- hoveredLinkTarget: {
223
- elementId: ElementId;
224
- activeAnchor: string | null;
225
- outlinePoint?: Vec2 | undefined;
226
- /**
227
- * What the drop will produce, for clear pre-drop feedback (standard):
228
- * - `"point"` → fixed attach to a specific dot (highlight the dot);
229
- * - `"element"` → floating attach to the whole shape (highlight the
230
- * element). Mirrors `snapLinkEndpoint`: an anchor within threshold →
231
- * point, otherwise floating.
232
- */
233
- mode: "point" | "element";
234
- } | null;
235
- /**
236
- * Last idle cursor position (world) in select mode — the overlay grows the
237
- * SINGLE selected element's link-start dot nearest it
238
- * (`ANCHOR_DOT_HOVER_GROW_RADIUS`). Reset to null on press / gesture.
239
- */
240
- hoverCursorWorld: Vec2 | null;
241
- /**
242
- * When a link is dropped on empty canvas, the edge is created with a
243
- * free `point` end and this records where, so the host can pop a
244
- * mini shape-picker at that spot (standard). Picking a shape re-points the
245
- * end to the new element; dismissing (Esc / click-away) leaves the free
246
- * end on the canvas. `null` when no menu is pending.
247
- */
248
- 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);
249
234
  /**
250
235
  * Currently selected links (connectors). Links are first-class members
251
236
  * of the selection: they coexist with selected elements, join Cmd+A and
@@ -271,8 +256,9 @@ export declare class Editor {
271
256
  /**
272
257
  * Mid-drag preview state when the user is dragging an edge endpoint.
273
258
  * Drawn as an overlay line + handle dot so the user sees the target.
259
+ * State lives in `LinkHandleDragController`; this is a delegate.
274
260
  */
275
- linkEndpointDrag: {
261
+ get linkEndpointDrag(): {
276
262
  linkId: LinkId;
277
263
  side: "from" | "to";
278
264
  toPoint: Vec2;
@@ -283,83 +269,40 @@ export declare class Editor {
283
269
  * began on a segment midpoint and will splice a new waypoint on the
284
270
  * first move (so a no-move click adds nothing). Live-mutated through the
285
271
  * gesture transaction → one undo step per drag.
272
+ * State lives in `LinkHandleDragController`; this is a delegate.
286
273
  */
287
- linkWaypointDrag: {
274
+ get linkWaypointDrag(): {
288
275
  linkId: LinkId;
289
276
  index: number;
290
277
  pendingInsert: boolean;
291
278
  } | null;
292
279
  /**
293
- * Host-managed elbow segment drag. `index` is the segment in the routed
294
- * chain `[from, ...routedPoints, to]`; `axis` is its orientation. Dragging
295
- * pins the segment's perpendicular coordinate into `Link.fixedSegments`;
296
- * 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.
297
285
  */
298
- linkSegmentDrag: {
286
+ get linkSegmentDrag(): {
299
287
  linkId: LinkId;
300
288
  axis: "h" | "v";
301
289
  at: number;
302
290
  } | null;
303
291
  /** Live lasso bounds during a rubber-band select gesture. */
304
- lassoPreview: Bounds | null;
305
- /**
306
- * Selection captured at lasso-press time. Used to compute the live
307
- * preview correctly: in `replace` mode the lasso starts from empty
308
- * each frame; in `add` mode it starts from this snapshot so shapes
309
- * the user already had selected don't blink out and back.
310
- */
311
- private lassoBaseSelection;
312
- /** Link-selection counterpart of `lassoBaseSelection` for the marquee. */
313
- private lassoBaseLinks;
314
- /**
315
- * Snapshot of every selected shape's `position` at press-down. Used to
316
- * translate the whole group additively during a multi-shape drag. The
317
- * machine still emits per-shape MOVE_SHAPE — the editor intercepts and
318
- * fans out when this map is populated.
319
- */
320
- groupMoveOrigin: ReadonlyMap<ElementId, Vec2> | null;
321
- /**
322
- * Press-time snapshot of connectors that must follow a multi-element
323
- * drag rigidly — both endpoints bound to moved elements, carrying
324
- * absolute geometry (waypoints / fixedSegments / routedPoints). Each
325
- * frame translates from these originals so the shift never compounds.
326
- * Cleared on gesture commit / cancel alongside `groupMoveOrigin`.
327
- */
328
- groupLinkMoveOrigin: ReadonlyMap<LinkId, Link> | null;
329
- /**
330
- * Per-shape snapshot for a group-resize gesture — `bounds` is the
331
- * shape's world AABB at press-down. Editor scales the relative
332
- * position / size against the combined bounds delta each frame.
333
- */
334
- groupResizeOrigin: {
335
- readonly combined: Bounds;
336
- readonly elements: ReadonlyMap<ElementId, {
337
- readonly position: Vec2;
338
- readonly bounds: Bounds;
339
- readonly scale: Vec2;
340
- }>;
341
- readonly links: ReadonlyMap<LinkId, Link>;
342
- } | null;
343
- /**
344
- * Press-time snapshot for a rotate gesture: the pivot (selection bbox centre)
345
- * and every member's pristine `position` / `rotation`. Each frame rotates
346
- * from this baseline so the cumulative angle never drifts. Cleared on gesture
347
- * end (commit / cancel).
348
- */
349
- rotateGestureOrigin: {
350
- readonly pivot: Vec2;
351
- readonly origin: ReadonlyMap<ElementId, {
352
- readonly position: Vec2;
353
- readonly rotation: number;
354
- }>;
355
- } | null;
356
- /**
357
- * Pristine shape snapshot for a single-shape text resize, captured on
358
- * the gesture's first tick. Font scaling is computed against this base
359
- * so it never compounds across pointermove ticks. Cleared on gesture
360
- * end (commit / cancel).
361
- */
362
- 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);
363
306
  /**
364
307
  * Active layer — new shapes created via `addElement` / `applyCreate` land
365
308
  * here when their input doesn't specify a `layerId`. Defaults to the
@@ -375,21 +318,6 @@ export declare class Editor {
375
318
  private readonly snapEngine;
376
319
  /** Snap threshold in world units. */
377
320
  private readonly snapThreshold;
378
- /**
379
- * Transient flag set by the host while a snap-suppress modifier
380
- * (Cmd / Ctrl) is held during a drag — lets the user pull a shape off
381
- * the grid for one gesture without toggling snap off. Read by the
382
- * move / resize / create wrappers; never persisted.
383
- */
384
- private snapSuppressed;
385
- /**
386
- * Transient transform-modifier state mirrored from the host while a drag is
387
- * in flight. `alt` resizes symmetrically about the centre; `shift` locks the
388
- * resize aspect ratio or constrains a move to a single axis. Read by the
389
- * move / resize wrappers; never persisted.
390
- */
391
- private transformAltKey;
392
- private transformShiftKey;
393
321
  /**
394
322
  * In-editor style memory for copy-style / paste-style. Holds the visual
395
323
  * `style` (fill / stroke / dash / …) captured from a shape; `null` until a
@@ -417,36 +345,37 @@ export declare class Editor {
417
345
  * escape, click outside the group's descendants, or `cancelInteraction`.
418
346
  */
419
347
  _enteredGroup: ElementId | null;
420
- /**
421
- * Double-click detection state. Updated on every non-drag pointer
422
- * up; the next pointer-up within `DOUBLE_CLICK_MS` and within
423
- * `DOUBLE_CLICK_TOLERANCE_PX` of `lastClickWorldPoint` counts as a
424
- * double-click. Used to trigger group drill-down (enter isolation).
425
- */
426
- private lastClickAt;
427
- private lastClickWorldPoint;
428
- /**
429
- * Separate double-click tracker for link edit handles (waypoint /
430
- * segment). Kept apart from `lastClickAt` because a handle press
431
- * returns early in `onDown` (begin-drag) and never reaches the up-side
432
- * double-click path that updates `lastClickAt`. Updated by
433
- * `isHandleDoubleClick` on each handle press.
434
- */
435
- private lastHandleClickAt;
436
- private lastHandleClickWorld;
437
348
  /**
438
349
  * In-progress brush stroke. Hosts push points via
439
350
  * `extendBrushStroke`; the overlay reads it through
440
351
  * `pendingBrushStroke` to draw a live preview.
441
352
  */
442
- 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[];
443
371
  /**
444
372
  * Last world-space pointer position observed by the host's onMove
445
373
  * handler. `paste()` uses it as the default drop target so a fresh
446
374
  * paste lands under the cursor instead of overlapping the originals.
447
375
  * `null` until the pointer first enters the host.
448
376
  */
449
- lastPointerWorld: Vec2 | null;
377
+ get lastPointerWorld(): Vec2 | null;
378
+ set lastPointerWorld(v: Vec2 | null);
450
379
  /** Host-registered custom cursor images per role (see `setCursorOverride`). */
451
380
  private readonly cursorOverrides;
452
381
  /**
@@ -465,6 +394,23 @@ export declare class Editor {
465
394
  * pass would never visibly apply.
466
395
  */
467
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;
468
414
  /**
469
415
  * Fractional-order compaction scheduler (microtask-coalesced).
470
416
  * Triggered from every `notify()`; only does real work when at
@@ -483,7 +429,8 @@ export declare class Editor {
483
429
  * gestures, set in onDown when press lands on a shape and cleared
484
430
  * in onUp / cancel.
485
431
  */
486
- dragElementId: ElementId | null;
432
+ get dragElementId(): ElementId | null;
433
+ set dragElementId(v: ElementId | null);
487
434
  /**
488
435
  * Element that the current press added to the selection additively
489
436
  * (shift / meta click on an unselected shape). The press promotes it
@@ -491,17 +438,16 @@ export declare class Editor {
491
438
  * otherwise `SELECT_TOGGLE` it straight back off, so it consults this
492
439
  * to skip that redundant toggle. Reset at every press-down.
493
440
  */
494
- additivePressAdded: ElementId | null;
441
+ get additivePressAdded(): ElementId | null;
442
+ set additivePressAdded(v: ElementId | null);
495
443
  /**
496
444
  * Live container highlight: the container shape the dragged item is
497
445
  * currently hovering over. Drawn by the overlay as a dashed
498
446
  * accent rect on the container's drop-zone so the user sees where the
499
447
  * shape will land after release.
500
448
  */
501
- containerHover: {
502
- id: ElementId;
503
- dropZone: Bounds;
504
- } | null;
449
+ get containerHover(): ContainerHover | null;
450
+ set containerHover(v: ContainerHover | null);
505
451
  /**
506
452
  * Remote peer cursors / selections, pushed in by the host (typically
507
453
  * a `bindAwareness(editor, awareness)` helper in `@collab`). The
@@ -521,7 +467,7 @@ export declare class Editor {
521
467
  * two or more entries we enter a pinch / pan gesture and bypass the
522
468
  * interaction machine — `pinchOrigin` holds the baseline.
523
469
  */
524
- readonly activePointers: Map<number, Vec2>;
470
+ get activePointers(): Map<number, Vec2>;
525
471
  /**
526
472
  * One-finger-pan candidate: set at pointer-down when a TOUCH press lands
527
473
  * on empty canvas in select mode. A tap (no movement) still falls through
@@ -529,7 +475,8 @@ export declare class Editor {
529
475
  * this to a real pan instead of a marquee lasso (mobile convention).
530
476
  * Screen-space origin point.
531
477
  */
532
- touchPanCandidate: Vec2 | null;
478
+ get touchPanCandidate(): Vec2 | null;
479
+ set touchPanCandidate(v: Vec2 | null);
533
480
  pinch: PinchController;
534
481
  /** Bridge for the container-ops helpers. Built lazily in constructor. */
535
482
  private containerOpsRef;
@@ -539,7 +486,8 @@ export declare class Editor {
539
486
  * "grab" / "grabbing". Wires a window-level keydown/keyup listener
540
487
  * in `bindPointerEvents`.
541
488
  */
542
- spaceHeld: boolean;
489
+ get spaceHeld(): boolean;
490
+ set spaceHeld(v: boolean);
543
491
  /**
544
492
  * Host-supplied tile compositor — when set (via
545
493
  * `EditorOptions.useTileCache` + `tileCompose`), the per-frame
@@ -589,20 +537,16 @@ export declare class Editor {
589
537
  * only treat right-click releases as potential context-menu
590
538
  * triggers (Space + left-drag never opens a menu).
591
539
  */
592
- panGesture: {
593
- pointerId: number;
594
- button: number;
595
- startPoint: Vec2;
596
- lastPoint: Vec2;
597
- moved: boolean;
598
- } | null;
540
+ get panGesture(): PanGesture | null;
541
+ set panGesture(v: PanGesture | null);
599
542
  /**
600
543
  * Set on a right-click pointerdown so the upcoming native
601
544
  * `contextmenu` event can be unconditionally preventDefault'ed
602
545
  * (the gesture decides whether to fire the menu manually on
603
546
  * pointerup based on whether the user dragged).
604
547
  */
605
- suppressNextContextMenu: boolean;
548
+ get suppressNextContextMenu(): boolean;
549
+ set suppressNextContextMenu(v: boolean);
606
550
  /**
607
551
  * Long-press tracking. Starts on `pointerdown`; cancelled on
608
552
  * `pointermove > LONG_PRESS_MAX_MOVEMENT_PX` or `pointerup` before
@@ -622,13 +566,13 @@ export declare class Editor {
622
566
  * in the constructor from `EditorOptions.inputMode` (default `"auto"`
623
567
  * uses `matchMedia('(pointer: coarse)')`).
624
568
  */
625
- private readonly inputMode;
626
- private readonly handleHitSlop;
627
- private readonly edgeHandleHitSlop;
628
- private readonly edgeHitThreshold;
569
+ private inputMode;
570
+ private handleHitSlop;
571
+ private edgeHandleHitSlop;
572
+ private edgeHitThreshold;
629
573
  /** Link-start anchor-dot grab/click hit radii — touch-enlarged in touch mode. */
630
- readonly anchorStartHitSlop: number;
631
- readonly anchorClickRadius: number;
574
+ anchorStartHitSlop: number;
575
+ anchorClickRadius: number;
632
576
  readonly _history: HistoryProvider;
633
577
  /** Open transaction during a single drag/resize gesture. */
634
578
  gestureTx: TransactionHandle | null;
@@ -646,9 +590,63 @@ export declare class Editor {
646
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,6 +738,19 @@ export declare class Editor {
740
738
  * values can be eyeballed. View-only — not recorded in history.
741
739
  */
742
740
  setDebugHitZones(on: boolean): void;
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;
743
754
  /** Whether the background grid is enabled for the scene. */
744
755
  get gridEnabled(): boolean;
745
756
  /** Show/hide the background grid (`g`). Persists in the viewport, not in history. */
@@ -902,11 +913,10 @@ export declare class Editor {
902
913
  * cleared by `commitTextEdit` / `cancelTextEdit`. The host overlay
903
914
  * (`<TextEditorOverlay>` in `@react-ui`) subscribes via `editor`
904
915
  * and renders a `<textarea>` positioned over the shape.
916
+ * State lives in `TextEditController`; this is a delegate.
905
917
  */
906
- private _editingTextElement;
907
918
  get editingTextElement(): ElementId | null;
908
919
  /** Link whose caption is being edited inline (double-click), or null. */
909
- private _editingLinkCaption;
910
920
  get editingLinkCaption(): LinkId | null;
911
921
  /**
912
922
  * Frame whose NAME (header label) is being edited inline (double-click
@@ -915,29 +925,6 @@ export declare class Editor {
915
925
  */
916
926
  private _editingFrameName;
917
927
  get editingFrameName(): ElementId | null;
918
- /**
919
- * When the `draw-text` tool just placed a shape and opened its
920
- * editor, this holds that shape's id until the first commit. A
921
- * pending creation isn't in history yet: committing non-empty text
922
- * records a single add patch (whole shape = one undo); committing
923
- * empty / cancelling removes it with no history entry at all.
924
- */
925
- private _pendingTextCreate;
926
- /**
927
- * Snapshot of the shape at edit start. Used to revert on cancel and
928
- * as the `before` of the single commit patch. `null` for a pending
929
- * creation (the shape didn't exist yet).
930
- */
931
- private _textEditOrigin;
932
- /**
933
- * Live selection inside the edited text, mirrored from the hidden
934
- * `<textarea>` (`start`/`end` are source offsets, `dir` is the
935
- * anchored end). The caret is `dir === "backward" ? start : end`.
936
- */
937
- private _textSel;
938
- /** Anchor offset for a canvas drag-select inside the edited text. */
939
- private _textDragAnchor;
940
- private readonly caretBlink;
941
928
  get editingTextSelection(): {
942
929
  start: number;
943
930
  end: number;
@@ -1013,15 +1000,6 @@ export declare class Editor {
1013
1000
  extendTextSelectionToPoint(worldPoint: Vec2): void;
1014
1001
  /** End a canvas drag-select (clears the drag anchor). */
1015
1002
  endTextDragSelect(): void;
1016
- /** Build the editable layout for a text shape using the main target's metrics. */
1017
- private editingTextLayout;
1018
- /**
1019
- * A measure callback bound to a shape's font, using the main target's
1020
- * `measureText` — the SAME source the renderer draws with (WebGL2
1021
- * reports MSDF advances) and the bounder measures with. Caret /
1022
- * selection geometry therefore lines up exactly with the glyphs.
1023
- */
1024
- private measureFor;
1025
1003
  /**
1026
1004
  * World-space caret + selection geometry for the overlay pass.
1027
1005
  * Returns `null` when not editing. The caret is `null` while blinked
@@ -1064,6 +1042,15 @@ export declare class Editor {
1064
1042
  * the tool is locked.
1065
1043
  */
1066
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;
1067
1054
  beginBrushStroke(world: Vec2, pressure?: number): void;
1068
1055
  extendBrushStroke(world: Vec2, pressure?: number): void;
1069
1056
  commitBrushStroke(): ElementId | null;
@@ -1073,6 +1060,53 @@ export declare class Editor {
1073
1060
  readonly origin: Vec2;
1074
1061
  readonly points: readonly BrushPoint[];
1075
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;
1076
1110
  arrangeAsGrid(opts?: {
1077
1111
  cols?: number;
1078
1112
  gap?: number;
@@ -1106,6 +1140,14 @@ export declare class Editor {
1106
1140
  */
1107
1141
  duplicateSelectedInPlace(anchorId?: ElementId | null): ElementId | null;
1108
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;
1109
1151
  selectAll(): void;
1110
1152
  /**
1111
1153
  * Internal clipboard. Stored as deep-cloned snapshots so subsequent
@@ -1145,6 +1187,179 @@ export declare class Editor {
1145
1187
  /** Whether a style has been copied and can be pasted. */
1146
1188
  get hasStyleClipboard(): boolean;
1147
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;
1148
1363
  /**
1149
1364
  * Update non-style text properties (`fontSize`, `fontFamily`,
1150
1365
  * `maxWidth`) on every selected text shape. Non-text shapes are
@@ -1253,6 +1468,13 @@ export declare class Editor {
1253
1468
  zoomToFit(padding?: number): void;
1254
1469
  /** Fit the camera to the current selection (standard `⌥2`). No-op when empty. */
1255
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;
1256
1478
  /**
1257
1479
  * Select the nearest interactable top-level element in `direction` from the
1258
1480
  * current selection's centre (or the viewport centre when nothing is
@@ -1429,7 +1651,22 @@ export declare class Editor {
1429
1651
  * selected, you can see".
1430
1652
  */
1431
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;
1432
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;
1433
1670
  /**
1434
1671
  * Enter a group — subsequent hits inside this group return children
1435
1672
  * directly instead of the group root. `null` exits group-edit mode.
@@ -1817,19 +2054,47 @@ export declare class Editor {
1817
2054
  */
1818
2055
  runLayout(parentId: ElementId): Patch | null;
1819
2056
  /**
1820
- * Per-link signature of the inputs that determine an elbow route
1821
- * (endpoint refs + bound-shape bounds + fixedSegments). When unchanged
1822
- * 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).
1823
2084
  */
1824
- private readonly elbowRouteSig;
2085
+ private rerouteElbows;
1825
2086
  /**
1826
- * Choke-point reroute (standard model): recompute `routedPoints` for
1827
- * every orthogonal link whose inputs changed since the last pass, and
1828
- * bake the result into `_scene`. Runs once per frame before paint
1829
- * derived state, so no history push / notify (would loop). Cheap when
1830
- * 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.
1831
2096
  */
1832
- private rerouteElbows;
2097
+ private buildRenderSnapshot;
1833
2098
  private render;
1834
2099
  }
1835
2100
  /** Distance from point `p` to the finite segment `a`–`b` (world space). */