@oh-just-another/state 0.59.0 → 0.61.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 (163) hide show
  1. package/CHANGELOG.md +189 -0
  2. package/README.md +6 -4
  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 +79 -11
  11. package/dist/actions/actionKeyboard.js.map +1 -1
  12. package/dist/actions/actionMode.d.ts +2 -0
  13. package/dist/actions/actionMode.d.ts.map +1 -1
  14. package/dist/actions/actionMode.js +81 -27
  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 +10 -1
  34. package/dist/actions/types.d.ts.map +1 -1
  35. package/dist/actions/types.js.map +1 -1
  36. package/dist/built-in-handlers.d.ts +8 -0
  37. package/dist/built-in-handlers.d.ts.map +1 -1
  38. package/dist/built-in-handlers.js +31 -16
  39. package/dist/built-in-handlers.js.map +1 -1
  40. package/dist/clipboard.d.ts +0 -14
  41. package/dist/clipboard.d.ts.map +1 -1
  42. package/dist/clipboard.js +1 -1
  43. package/dist/clipboard.js.map +1 -1
  44. package/dist/constants.d.ts +253 -18
  45. package/dist/constants.d.ts.map +1 -1
  46. package/dist/constants.js +255 -2
  47. package/dist/constants.js.map +1 -1
  48. package/dist/dom-events.d.ts +1 -2
  49. package/dist/dom-events.d.ts.map +1 -1
  50. package/dist/dom-events.js.map +1 -1
  51. package/dist/editor/animation-scene.d.ts +15 -0
  52. package/dist/editor/animation-scene.d.ts.map +1 -1
  53. package/dist/editor/animation-scene.js +159 -0
  54. package/dist/editor/animation-scene.js.map +1 -1
  55. package/dist/editor/applies/create.d.ts +0 -6
  56. package/dist/editor/applies/create.d.ts.map +1 -1
  57. package/dist/editor/applies/create.js +1 -1
  58. package/dist/editor/applies/create.js.map +1 -1
  59. package/dist/editor/applies/link-move.d.ts +0 -8
  60. package/dist/editor/applies/link-move.d.ts.map +1 -1
  61. package/dist/editor/applies/link-move.js +1 -1
  62. package/dist/editor/applies/link-move.js.map +1 -1
  63. package/dist/editor/event-fanout.d.ts +6 -6
  64. package/dist/editor/event-fanout.d.ts.map +1 -1
  65. package/dist/editor/event-fanout.js +8 -8
  66. package/dist/editor/event-fanout.js.map +1 -1
  67. package/dist/editor/hit-test.d.ts +8 -0
  68. package/dist/editor/hit-test.d.ts.map +1 -1
  69. package/dist/editor/hit-test.js +31 -16
  70. package/dist/editor/hit-test.js.map +1 -1
  71. package/dist/editor/interaction-state.d.ts +184 -0
  72. package/dist/editor/interaction-state.d.ts.map +1 -0
  73. package/dist/editor/interaction-state.js +139 -0
  74. package/dist/editor/interaction-state.js.map +1 -0
  75. package/dist/editor/link-handle-drag.d.ts +172 -0
  76. package/dist/editor/link-handle-drag.d.ts.map +1 -0
  77. package/dist/editor/link-handle-drag.js +369 -0
  78. package/dist/editor/link-handle-drag.js.map +1 -0
  79. package/dist/editor/pointer-binding.d.ts +9 -11
  80. package/dist/editor/pointer-binding.d.ts.map +1 -1
  81. package/dist/editor/pointer-binding.js +1247 -909
  82. package/dist/editor/pointer-binding.js.map +1 -1
  83. package/dist/editor/public/brush.d.ts +93 -13
  84. package/dist/editor/public/brush.d.ts.map +1 -1
  85. package/dist/editor/public/brush.js +240 -22
  86. package/dist/editor/public/brush.js.map +1 -1
  87. package/dist/editor/public/cursor.d.ts +1 -15
  88. package/dist/editor/public/cursor.d.ts.map +1 -1
  89. package/dist/editor/public/cursor.js +17 -7
  90. package/dist/editor/public/cursor.js.map +1 -1
  91. package/dist/editor/public/eraser.d.ts +55 -0
  92. package/dist/editor/public/eraser.d.ts.map +1 -0
  93. package/dist/editor/public/eraser.js +65 -0
  94. package/dist/editor/public/eraser.js.map +1 -0
  95. package/dist/editor/public/laser.d.ts +38 -0
  96. package/dist/editor/public/laser.d.ts.map +1 -0
  97. package/dist/editor/public/laser.js +42 -0
  98. package/dist/editor/public/laser.js.map +1 -0
  99. package/dist/editor/public/placement.d.ts.map +1 -1
  100. package/dist/editor/public/placement.js +9 -1
  101. package/dist/editor/public/placement.js.map +1 -1
  102. package/dist/editor/public/selection-ops.d.ts +12 -0
  103. package/dist/editor/public/selection-ops.d.ts.map +1 -1
  104. package/dist/editor/public/selection-ops.js +19 -1
  105. package/dist/editor/public/selection-ops.js.map +1 -1
  106. package/dist/editor/public/stroke-eraser-coverage.d.ts +37 -0
  107. package/dist/editor/public/stroke-eraser-coverage.d.ts.map +1 -0
  108. package/dist/editor/public/stroke-eraser-coverage.js +168 -0
  109. package/dist/editor/public/stroke-eraser-coverage.js.map +1 -0
  110. package/dist/editor/public/stroke-eraser.d.ts +50 -0
  111. package/dist/editor/public/stroke-eraser.d.ts.map +1 -0
  112. package/dist/editor/public/stroke-eraser.js +176 -0
  113. package/dist/editor/public/stroke-eraser.js.map +1 -0
  114. package/dist/editor/public/stroke-smoothing.d.ts +21 -0
  115. package/dist/editor/public/stroke-smoothing.d.ts.map +1 -0
  116. package/dist/editor/public/stroke-smoothing.js +37 -0
  117. package/dist/editor/public/stroke-smoothing.js.map +1 -0
  118. package/dist/editor/public/tool-ops.d.ts +130 -0
  119. package/dist/editor/public/tool-ops.d.ts.map +1 -0
  120. package/dist/editor/public/tool-ops.js +392 -0
  121. package/dist/editor/public/tool-ops.js.map +1 -0
  122. package/dist/editor/public/z-order.d.ts +1 -12
  123. package/dist/editor/public/z-order.d.ts.map +1 -1
  124. package/dist/editor/public/z-order.js +1 -1
  125. package/dist/editor/public/z-order.js.map +1 -1
  126. package/dist/editor/public/zoom-pan.d.ts +9 -0
  127. package/dist/editor/public/zoom-pan.d.ts.map +1 -1
  128. package/dist/editor/public/zoom-pan.js +31 -0
  129. package/dist/editor/public/zoom-pan.js.map +1 -1
  130. package/dist/editor/render-orchestrator.d.ts +180 -5
  131. package/dist/editor/render-orchestrator.d.ts.map +1 -1
  132. package/dist/editor/render-orchestrator.js +516 -353
  133. package/dist/editor/render-orchestrator.js.map +1 -1
  134. package/dist/editor/text-edit.d.ts +136 -0
  135. package/dist/editor/text-edit.d.ts.map +1 -0
  136. package/dist/editor/text-edit.js +328 -0
  137. package/dist/editor/text-edit.js.map +1 -0
  138. package/dist/editor-events.d.ts +5 -5
  139. package/dist/editor-events.d.ts.map +1 -1
  140. package/dist/editor.d.ts +560 -250
  141. package/dist/editor.d.ts.map +1 -1
  142. package/dist/editor.js +1650 -743
  143. package/dist/editor.js.map +1 -1
  144. package/dist/handle.d.ts +0 -2
  145. package/dist/handle.d.ts.map +1 -1
  146. package/dist/handle.js +0 -6
  147. package/dist/handle.js.map +1 -1
  148. package/dist/index.d.ts +7 -2
  149. package/dist/index.d.ts.map +1 -1
  150. package/dist/index.js +7 -1
  151. package/dist/index.js.map +1 -1
  152. package/dist/modes.d.ts +34 -1
  153. package/dist/modes.d.ts.map +1 -1
  154. package/dist/modes.js.map +1 -1
  155. package/dist/overlay.d.ts +98 -21
  156. package/dist/overlay.d.ts.map +1 -1
  157. package/dist/overlay.js +442 -116
  158. package/dist/overlay.js.map +1 -1
  159. package/dist/search.d.ts +37 -0
  160. package/dist/search.d.ts.map +1 -0
  161. package/dist/search.js +45 -0
  162. package/dist/search.js.map +1 -0
  163. package/package.json +5 -4
package/dist/editor.d.ts CHANGED
@@ -1,19 +1,23 @@
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
- import type { Mode } from "./modes.js";
10
+ import type { ActiveTool, Mode } from "./modes.js";
11
11
  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";
@@ -52,7 +56,14 @@ export interface EditorOptions {
52
56
  */
53
57
  readonly onAfterRender?: () => void;
54
58
  readonly initialScene: Scene;
55
- readonly initialMode?: Mode;
59
+ readonly initialTool?: 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,21 @@ export type TileComposeFn = (scene: Scene, mainTarget: RenderTarget, options: {
127
138
  after: Bounds | null;
128
139
  }>;
129
140
  readonly zoomBucket: number;
141
+ /**
142
+ * Elements omitted from tile rasterisation (stroke-eraser preview and
143
+ * per-element hide). The compositor invalidates the tiles an element
144
+ * touches when it enters/leaves the set.
145
+ */
146
+ readonly hideElements?: ReadonlySet<ElementId>;
147
+ /**
148
+ * Persistent spatial index over the scene's current element world-AABBs,
149
+ * when the editor maintains one (large scenes, shared with the hit-test
150
+ * path). A tile compositor that supports it (`renderViaTiles`) queries the
151
+ * index for per-tile element selection instead of scanning every shape in
152
+ * every layer. Omitted for small scenes; compositors must fall back to a
153
+ * full scan when absent.
154
+ */
155
+ readonly index?: SpatialGrid;
130
156
  }) => void;
131
157
  /**
132
158
  * Top-level interaction controller. Owns the scene + selection state, wires
@@ -155,7 +181,15 @@ export declare class Editor {
155
181
  * View-only — never persisted or recorded in history.
156
182
  */
157
183
  debugHitZones: boolean;
158
- readonly actor: Actor<typeof interactionMachine>;
184
+ /**
185
+ * Read-only / view mode. When true the pointer paths that create,
186
+ * move, resize, rotate or delete are gated (pan / zoom / select stay
187
+ * live) and the action registry only runs actions flagged
188
+ * `viewMode`. View-only — never persisted or recorded in history.
189
+ * Read via {@link readOnly}; flip via {@link setReadOnly}.
190
+ */
191
+ private _readOnly;
192
+ actor: Actor<typeof interactionMachine>;
159
193
  private readonly listeners;
160
194
  /**
161
195
  * Typed event surface. Specific events (`mode`, `selection`,
@@ -178,74 +212,31 @@ export declare class Editor {
178
212
  _scene: Scene;
179
213
  _selection: Selection.Selection;
180
214
  /**
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.
215
+ * Ephemeral interaction / gesture state (previews, gesture origins,
216
+ * transient modifiers). Single source of truth for the short-lived fields
217
+ * the pointer handlers, render orchestrator and container-ops read/write
218
+ * while a gesture is in flight. The public fields below delegate to it so
219
+ * external writers keep referencing `editor.<field>` unchanged.
185
220
  */
186
- annotationDrag: {
187
- id: AnnotationId;
188
- originPosition: Vec2;
189
- originWorldPoint: Vec2;
190
- moved: boolean;
191
- } | null;
221
+ readonly interaction: InteractionState;
222
+ /** Snapshot of an in-progress annotation-pin drag. */
223
+ get annotationDrag(): AnnotationDrag | null;
224
+ set annotationDrag(v: AnnotationDrag | null);
192
225
  /** 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;
226
+ get drawingPreview(): Bounds | null;
227
+ set drawingPreview(v: Bounds | null);
228
+ /** Live preview of an edge being drawn. */
229
+ get edgePreview(): EdgePreview | null;
230
+ set edgePreview(v: EdgePreview | null);
231
+ /** Active "drag a link from a start-anchor" gesture. */
232
+ get linkDragFromAnchor(): LinkDragFromAnchor | null;
233
+ set linkDragFromAnchor(v: LinkDragFromAnchor | null);
234
+ /** Element hovered while draw-edge mode is active (drives the port overlay). */
235
+ get hoveredLinkTarget(): HoveredLinkTarget | null;
236
+ set hoveredLinkTarget(v: HoveredLinkTarget | null);
237
+ /** Last idle cursor position (world) in select mode grows the nearest dot. */
238
+ get hoverCursorWorld(): Vec2 | null;
239
+ set hoverCursorWorld(v: Vec2 | null);
249
240
  /**
250
241
  * Currently selected links (connectors). Links are first-class members
251
242
  * of the selection: they coexist with selected elements, join Cmd+A and
@@ -271,8 +262,9 @@ export declare class Editor {
271
262
  /**
272
263
  * Mid-drag preview state when the user is dragging an edge endpoint.
273
264
  * Drawn as an overlay line + handle dot so the user sees the target.
265
+ * State lives in `LinkHandleDragController`; this is a delegate.
274
266
  */
275
- linkEndpointDrag: {
267
+ get linkEndpointDrag(): {
276
268
  linkId: LinkId;
277
269
  side: "from" | "to";
278
270
  toPoint: Vec2;
@@ -283,83 +275,40 @@ export declare class Editor {
283
275
  * began on a segment midpoint and will splice a new waypoint on the
284
276
  * first move (so a no-move click adds nothing). Live-mutated through the
285
277
  * gesture transaction → one undo step per drag.
278
+ * State lives in `LinkHandleDragController`; this is a delegate.
286
279
  */
287
- linkWaypointDrag: {
280
+ get linkWaypointDrag(): {
288
281
  linkId: LinkId;
289
282
  index: number;
290
283
  pendingInsert: boolean;
291
284
  } | null;
292
285
  /**
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.
286
+ * Host-managed elbow segment drag. `axis` is the segment's orientation.
287
+ * Dragging pins the segment's perpendicular coordinate into
288
+ * `Link.fixedSegments`; the reroute pass re-flows the rest. One undo
289
+ * step via the gesture tx.
290
+ * State lives in `LinkHandleDragController`; this is a delegate.
297
291
  */
298
- linkSegmentDrag: {
292
+ get linkSegmentDrag(): {
299
293
  linkId: LinkId;
300
294
  axis: "h" | "v";
301
295
  at: number;
302
296
  } | null;
303
297
  /** 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;
298
+ get lassoPreview(): Bounds | null;
299
+ set lassoPreview(v: Bounds | null);
300
+ /** Snapshot of every selected shape's `position` at press-down (multi-drag). */
301
+ get groupMoveOrigin(): ReadonlyMap<ElementId, Vec2> | null;
302
+ set groupMoveOrigin(v: ReadonlyMap<ElementId, Vec2> | null);
303
+ /** Press-time snapshot of connectors that follow a multi-element drag rigidly. */
304
+ get groupLinkMoveOrigin(): ReadonlyMap<LinkId, Link> | null;
305
+ set groupLinkMoveOrigin(v: ReadonlyMap<LinkId, Link> | null);
306
+ /** Per-shape snapshot for a group-resize gesture. */
307
+ get groupResizeOrigin(): GroupResizeOrigin | null;
308
+ set groupResizeOrigin(v: GroupResizeOrigin | null);
309
+ /** Press-time snapshot for a rotate gesture. */
310
+ get rotateGestureOrigin(): RotateGestureOrigin | null;
311
+ set rotateGestureOrigin(v: RotateGestureOrigin | null);
363
312
  /**
364
313
  * Active layer — new shapes created via `addElement` / `applyCreate` land
365
314
  * here when their input doesn't specify a `layerId`. Defaults to the
@@ -375,21 +324,6 @@ export declare class Editor {
375
324
  private readonly snapEngine;
376
325
  /** Snap threshold in world units. */
377
326
  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
327
  /**
394
328
  * In-editor style memory for copy-style / paste-style. Holds the visual
395
329
  * `style` (fill / stroke / dash / …) captured from a shape; `null` until a
@@ -417,36 +351,37 @@ export declare class Editor {
417
351
  * escape, click outside the group's descendants, or `cancelInteraction`.
418
352
  */
419
353
  _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
354
  /**
438
355
  * In-progress brush stroke. Hosts push points via
439
356
  * `extendBrushStroke`; the overlay reads it through
440
357
  * `pendingBrushStroke` to draw a live preview.
441
358
  */
442
- brushStroke: BrushStrokeState | null;
359
+ get brushStroke(): BrushStrokeState | null;
360
+ set brushStroke(v: BrushStrokeState | null);
361
+ /**
362
+ * The in-progress brush stroke run through the SAME commit pipeline
363
+ * `commitBrushStroke` applies on release (see {@link brushCommitPoints}:
364
+ * raw catch-up point + Catmull-Rom resample), so the stroke reads exactly
365
+ * as it will land in the scene instead of snapping on release. A fresh
366
+ * object each call (points diverge from `brushStroke.points`), so the
367
+ * overlay memo repaints every move that grows the stroke.
368
+ */
369
+ private get brushPreviewStroke();
370
+ /** In-progress eraser stroke (pending-delete set), or null between strokes. */
371
+ get eraseStroke(): EraseStrokeState | null;
372
+ set eraseStroke(v: EraseStrokeState | null);
373
+ /** Ids swept by the current eraser stroke — previewed dimmed, deleted on release. */
374
+ get pendingErase(): ReadonlySet<ElementId>;
375
+ /** Live laser-pointer trails (ephemeral, fading). Empty when none active. */
376
+ get laserStrokes(): readonly LaserStroke[];
443
377
  /**
444
378
  * Last world-space pointer position observed by the host's onMove
445
379
  * handler. `paste()` uses it as the default drop target so a fresh
446
380
  * paste lands under the cursor instead of overlapping the originals.
447
381
  * `null` until the pointer first enters the host.
448
382
  */
449
- lastPointerWorld: Vec2 | null;
383
+ get lastPointerWorld(): Vec2 | null;
384
+ set lastPointerWorld(v: Vec2 | null);
450
385
  /** Host-registered custom cursor images per role (see `setCursorOverride`). */
451
386
  private readonly cursorOverrides;
452
387
  /**
@@ -465,6 +400,23 @@ export declare class Editor {
465
400
  * pass would never visibly apply.
466
401
  */
467
402
  lastRenderedEnteredGroup: ElementId | null;
403
+ /**
404
+ * Whether the last paint had eraser-dim active — paired with
405
+ * `lastRenderedScene` like {@link lastRenderedEnteredGroup}. When an eraser
406
+ * stroke ENDS by cancel (Esc), the marked shapes un-dim without a scene
407
+ * change, so the dirty-rect diff is empty and the dim would linger on screen;
408
+ * this lets that active→inactive transition force one full repaint.
409
+ */
410
+ lastRenderedEraseActive: boolean;
411
+ /**
412
+ * Set whenever an eraser move actually CHANGES the marked / cut set (a new
413
+ * shape marked, un-marked, or a brush point cut). Gates the forced full
414
+ * repaint during erasing: only the frames that change the preview repaint the
415
+ * whole scene; a slowly-moving or stopped cursor over already-covered area
416
+ * skips the expensive main pass (only the overlay cursor / trail refresh).
417
+ * Cleared after each paint.
418
+ */
419
+ private eraseDirty;
468
420
  /**
469
421
  * Fractional-order compaction scheduler (microtask-coalesced).
470
422
  * Triggered from every `notify()`; only does real work when at
@@ -483,7 +435,8 @@ export declare class Editor {
483
435
  * gestures, set in onDown when press lands on a shape and cleared
484
436
  * in onUp / cancel.
485
437
  */
486
- dragElementId: ElementId | null;
438
+ get dragElementId(): ElementId | null;
439
+ set dragElementId(v: ElementId | null);
487
440
  /**
488
441
  * Element that the current press added to the selection additively
489
442
  * (shift / meta click on an unselected shape). The press promotes it
@@ -491,17 +444,16 @@ export declare class Editor {
491
444
  * otherwise `SELECT_TOGGLE` it straight back off, so it consults this
492
445
  * to skip that redundant toggle. Reset at every press-down.
493
446
  */
494
- additivePressAdded: ElementId | null;
447
+ get additivePressAdded(): ElementId | null;
448
+ set additivePressAdded(v: ElementId | null);
495
449
  /**
496
450
  * Live container highlight: the container shape the dragged item is
497
451
  * currently hovering over. Drawn by the overlay as a dashed
498
452
  * accent rect on the container's drop-zone so the user sees where the
499
453
  * shape will land after release.
500
454
  */
501
- containerHover: {
502
- id: ElementId;
503
- dropZone: Bounds;
504
- } | null;
455
+ get containerHover(): ContainerHover | null;
456
+ set containerHover(v: ContainerHover | null);
505
457
  /**
506
458
  * Remote peer cursors / selections, pushed in by the host (typically
507
459
  * a `bindAwareness(editor, awareness)` helper in `@collab`). The
@@ -521,7 +473,7 @@ export declare class Editor {
521
473
  * two or more entries we enter a pinch / pan gesture and bypass the
522
474
  * interaction machine — `pinchOrigin` holds the baseline.
523
475
  */
524
- readonly activePointers: Map<number, Vec2>;
476
+ get activePointers(): Map<number, Vec2>;
525
477
  /**
526
478
  * One-finger-pan candidate: set at pointer-down when a TOUCH press lands
527
479
  * on empty canvas in select mode. A tap (no movement) still falls through
@@ -529,7 +481,8 @@ export declare class Editor {
529
481
  * this to a real pan instead of a marquee lasso (mobile convention).
530
482
  * Screen-space origin point.
531
483
  */
532
- touchPanCandidate: Vec2 | null;
484
+ get touchPanCandidate(): Vec2 | null;
485
+ set touchPanCandidate(v: Vec2 | null);
533
486
  pinch: PinchController;
534
487
  /** Bridge for the container-ops helpers. Built lazily in constructor. */
535
488
  private containerOpsRef;
@@ -539,7 +492,8 @@ export declare class Editor {
539
492
  * "grab" / "grabbing". Wires a window-level keydown/keyup listener
540
493
  * in `bindPointerEvents`.
541
494
  */
542
- spaceHeld: boolean;
495
+ get spaceHeld(): boolean;
496
+ set spaceHeld(v: boolean);
543
497
  /**
544
498
  * Host-supplied tile compositor — when set (via
545
499
  * `EditorOptions.useTileCache` + `tileCompose`), the per-frame
@@ -570,6 +524,10 @@ export declare class Editor {
570
524
  * toolbar as a lock affordance next to the active tool.
571
525
  */
572
526
  private _toolLocked;
527
+ /** The tool active before the current one — `activeTool.lastActiveTool`. */
528
+ private _lastActiveTool;
529
+ /** Cached `activeTool` value object; rebuilt only when a component changes. */
530
+ private _activeToolCache;
573
531
  /**
574
532
  * Host-extensible file-drop dispatch. Built-ins (image / scene
575
533
  * JSON) register themselves at editor construction; hosts add
@@ -589,20 +547,16 @@ export declare class Editor {
589
547
  * only treat right-click releases as potential context-menu
590
548
  * triggers (Space + left-drag never opens a menu).
591
549
  */
592
- panGesture: {
593
- pointerId: number;
594
- button: number;
595
- startPoint: Vec2;
596
- lastPoint: Vec2;
597
- moved: boolean;
598
- } | null;
550
+ get panGesture(): PanGesture | null;
551
+ set panGesture(v: PanGesture | null);
599
552
  /**
600
553
  * Set on a right-click pointerdown so the upcoming native
601
554
  * `contextmenu` event can be unconditionally preventDefault'ed
602
555
  * (the gesture decides whether to fire the menu manually on
603
556
  * pointerup based on whether the user dragged).
604
557
  */
605
- suppressNextContextMenu: boolean;
558
+ get suppressNextContextMenu(): boolean;
559
+ set suppressNextContextMenu(v: boolean);
606
560
  /**
607
561
  * Long-press tracking. Starts on `pointerdown`; cancelled on
608
562
  * `pointermove > LONG_PRESS_MAX_MOVEMENT_PX` or `pointerup` before
@@ -622,13 +576,13 @@ export declare class Editor {
622
576
  * in the constructor from `EditorOptions.inputMode` (default `"auto"`
623
577
  * uses `matchMedia('(pointer: coarse)')`).
624
578
  */
625
- private readonly inputMode;
626
- private readonly handleHitSlop;
627
- private readonly edgeHandleHitSlop;
628
- private readonly edgeHitThreshold;
579
+ private inputMode;
580
+ private handleHitSlop;
581
+ private edgeHandleHitSlop;
582
+ private edgeHitThreshold;
629
583
  /** Link-start anchor-dot grab/click hit radii — touch-enlarged in touch mode. */
630
- readonly anchorStartHitSlop: number;
631
- readonly anchorClickRadius: number;
584
+ anchorStartHitSlop: number;
585
+ anchorClickRadius: number;
632
586
  readonly _history: HistoryProvider;
633
587
  /** Open transaction during a single drag/resize gesture. */
634
588
  gestureTx: TransactionHandle | null;
@@ -646,13 +600,72 @@ export declare class Editor {
646
600
  * The controller calls back through the narrow `GestureRef` bridge
647
601
  * built lazily below.
648
602
  */
649
- private readonly gestures;
603
+ private gestures;
604
+ /**
605
+ * Owns the inline text-edit session (edited shape, pending creation,
606
+ * origin snapshot, live selection, drag anchor and caret blink).
607
+ * Editor keeps thin delegate wrappers so the public API is unchanged.
608
+ */
609
+ private textEdit;
610
+ /**
611
+ * Owns the link edit-handle drags (waypoint / segment / endpoint) and
612
+ * the handle double-click detector. Editor keeps thin delegate
613
+ * wrappers so the public API is unchanged.
614
+ */
615
+ private linkHandles;
650
616
  constructor(options: EditorOptions);
651
- /** Snapshot used by event-fanout. Kept private — internal API. */
617
+ /**
618
+ * Build the interaction controllers (gestures, text edit, link-handle
619
+ * drag, long-press, pinch) and the container-ops bridge. Each wires to a
620
+ * narrow getter/setter surface over the editor's mutable fields, so the
621
+ * controllers live in their own modules without importing Editor. The
622
+ * getters/setters in the object literals rebind `this`, so a single
623
+ * `self` alias captures the Editor reference for all of them.
624
+ */
625
+ private initControllers;
626
+ /**
627
+ * Install process-global hooks the host opted into: a custom text shaper
628
+ * and rasterizer for the WebGL2 backend, plus the scene text measurer that
629
+ * routes through the renderer's own metrics so selection boxes hug text.
630
+ */
631
+ private initGlobalHooks;
632
+ /**
633
+ * Resolve the input mode (`touch` vs `mouse`, `auto` reads the coarse-
634
+ * pointer media query) and the derived hit slops / thresholds once.
635
+ */
636
+ private initInputMode;
637
+ /**
638
+ * Create and start the interaction state-machine actor, wire its render /
639
+ * emit subscriptions, register the built-in file-drop handlers, and apply
640
+ * the initial mode.
641
+ */
642
+ private initActor;
643
+ /**
644
+ * Last {@link observableSnapshot} object, reused while none of its slices
645
+ * have flipped. `null` until the first snapshot is built.
646
+ */
647
+ private snapshotCache;
648
+ /**
649
+ * Snapshot used by event-fanout. Kept private — internal API.
650
+ *
651
+ * Memoized by slice identity: `notify()` fires on many mutations that touch
652
+ * no observable slice (annotation focus, cursor pushes, viewport-only re-arm),
653
+ * so rebuilding the object every call is pure churn on the hot drag path.
654
+ * We reuse the cached object whenever all six slices compare equal (refs for
655
+ * mode/selection/selectedLinks/scene — scene uses structural sharing so a new
656
+ * ref iff something changed — plus the two history booleans), and only
657
+ * allocate a fresh one on a real flip. `fanOutEvents` sees identical values
658
+ * either way, so emitted events are unchanged.
659
+ */
652
660
  private observableSnapshot;
653
661
  get scene(): Scene;
654
662
  get selection(): Selection.Selection;
655
- get mode(): Mode;
663
+ /**
664
+ * The active tool as a single value object (`{ type, locked,
665
+ * lastActiveTool }`) — the one source of truth for the current tool.
666
+ * The reference is stable between changes (safe for React deps).
667
+ */
668
+ get activeTool(): ActiveTool;
656
669
  get history(): HistoryProvider;
657
670
  get canUndo(): boolean;
658
671
  get canRedo(): boolean;
@@ -740,14 +753,25 @@ export declare class Editor {
740
753
  * values can be eyeballed. View-only — not recorded in history.
741
754
  */
742
755
  setDebugHitZones(on: boolean): void;
756
+ /**
757
+ * Read-only / view mode flag. `true` gates pointer edits and
758
+ * non-`viewMode` actions while leaving pan / zoom / select live.
759
+ */
760
+ get readOnly(): boolean;
761
+ /**
762
+ * Enter / leave read-only (view) mode. Notifies subscribers so the UI
763
+ * can re-render disabled chrome, and repaints (no visual diff today, but
764
+ * keeps the contract symmetric with other view toggles). Idempotent.
765
+ */
766
+ setReadOnly(on: boolean): void;
767
+ /** Toggle read-only (view) mode. */
768
+ toggleReadOnly(): void;
743
769
  /** Whether the background grid is enabled for the scene. */
744
770
  get gridEnabled(): boolean;
745
771
  /** Show/hide the background grid (`g`). Persists in the viewport, not in history. */
746
772
  setGridVisible(on: boolean): void;
747
773
  /** Toggle the background grid on/off. */
748
774
  toggleGrid(): void;
749
- /** Whether the active draw-mode sticks after a create (toolbar lock). */
750
- get toolLocked(): boolean;
751
775
  /** All currently-selected link (connector) ids. */
752
776
  get selectedLinks(): LinkSelection.LinkSelection;
753
777
  /**
@@ -783,14 +807,19 @@ export declare class Editor {
783
807
  */
784
808
  dispatchFileDrop(file: File, worldPoint: Vec2): Promise<boolean>;
785
809
  /**
786
- * Toggle the tool-lock affordance. With `true`, draw-modes persist
787
- * after each successful shape create — the user keeps drawing
810
+ * Toggle the tool lock (`activeTool.locked`). With `true`, draw tools
811
+ * persist after each successful shape create — the user keeps drawing
788
812
  * rectangles without re-pressing R. With `false` (default), the
789
813
  * editor reverts to `select` after each create.
790
814
  */
791
815
  setToolLocked(locked: boolean): void;
792
- private maybeRevertModeAfterCreate;
793
- setMode(mode: Mode): void;
816
+ private maybeRevertToolAfterCreate;
817
+ /**
818
+ * Switch the active tool. The single entry point for tool changes —
819
+ * toolbar buttons and hotkeys reach it through the action registry.
820
+ * Records the outgoing tool in `activeTool.lastActiveTool`.
821
+ */
822
+ setActiveTool(mode: Mode): void;
794
823
  /** Undo the latest record. No-op if there is nothing to undo. */
795
824
  undo(): boolean;
796
825
  /** Redo the undone record. */
@@ -902,11 +931,10 @@ export declare class Editor {
902
931
  * cleared by `commitTextEdit` / `cancelTextEdit`. The host overlay
903
932
  * (`<TextEditorOverlay>` in `@react-ui`) subscribes via `editor`
904
933
  * and renders a `<textarea>` positioned over the shape.
934
+ * State lives in `TextEditController`; this is a delegate.
905
935
  */
906
- private _editingTextElement;
907
936
  get editingTextElement(): ElementId | null;
908
937
  /** Link whose caption is being edited inline (double-click), or null. */
909
- private _editingLinkCaption;
910
938
  get editingLinkCaption(): LinkId | null;
911
939
  /**
912
940
  * Frame whose NAME (header label) is being edited inline (double-click
@@ -915,29 +943,6 @@ export declare class Editor {
915
943
  */
916
944
  private _editingFrameName;
917
945
  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
946
  get editingTextSelection(): {
942
947
  start: number;
943
948
  end: number;
@@ -963,7 +968,11 @@ export declare class Editor {
963
968
  commitLinkCaptionEdit(text: string): void;
964
969
  /** Cancel link caption editing without changing the label. */
965
970
  cancelLinkCaptionEdit(): void;
966
- /** World-space anchor point for a link's caption (midpoint of its path). */
971
+ /**
972
+ * World-space anchor point for a link's caption — the same shared geometry
973
+ * the renderer places the pill at (`linkLabelAnchor` over the drawn
974
+ * polyline), so the inline editor opens exactly over the label.
975
+ */
967
976
  linkLabelWorld(id: LinkId): Vec2 | null;
968
977
  beginTextEdit(id: ElementId): void;
969
978
  /**
@@ -1013,15 +1022,6 @@ export declare class Editor {
1013
1022
  extendTextSelectionToPoint(worldPoint: Vec2): void;
1014
1023
  /** End a canvas drag-select (clears the drag anchor). */
1015
1024
  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
1025
  /**
1026
1026
  * World-space caret + selection geometry for the overlay pass.
1027
1027
  * Returns `null` when not editing. The caret is `null` while blinked
@@ -1064,7 +1064,22 @@ export declare class Editor {
1064
1064
  * the tool is locked.
1065
1065
  */
1066
1066
  createTextAt(worldPoint: Vec2): ElementId;
1067
- beginBrushStroke(world: Vec2, pressure?: number): void;
1067
+ /** Current brush paint settings (line colour, fill, opacity, width). */
1068
+ private _brushSettings;
1069
+ get brushSettings(): BrushSettings;
1070
+ /**
1071
+ * Update one or more brush paint settings (e.g. from the drawing panel). New
1072
+ * strokes pick them up on commit; the width also drives the pressure curve and
1073
+ * the eraser radius. Merges over the current settings.
1074
+ */
1075
+ setBrushSettings(patch: Partial<BrushSettings>): void;
1076
+ /**
1077
+ * Start a brush stroke. `pointerType` (a `PointerEvent.pointerType`) decides
1078
+ * the pressure source: a pen has a real pressure channel and is honoured
1079
+ * verbatim (the default, which also keeps programmatic callers exact); mouse
1080
+ * and touch have none, so pressure is simulated from pointer speed.
1081
+ */
1082
+ beginBrushStroke(world: Vec2, pressure?: number, pointerType?: string): void;
1068
1083
  extendBrushStroke(world: Vec2, pressure?: number): void;
1069
1084
  commitBrushStroke(): ElementId | null;
1070
1085
  cancelBrushStroke(): void;
@@ -1072,7 +1087,59 @@ export declare class Editor {
1072
1087
  get pendingBrushStroke(): {
1073
1088
  readonly origin: Vec2;
1074
1089
  readonly points: readonly BrushPoint[];
1090
+ readonly pressures: readonly number[];
1091
+ readonly baseWidth: number;
1092
+ readonly lastRaw: BrushPoint & {
1093
+ readonly pressure: number;
1094
+ };
1075
1095
  } | null;
1096
+ /**
1097
+ * Start an eraser stroke at `world`, seeding it with the shape under it so a
1098
+ * plain click erases. With `restore` (Alt held at press) it seeds nothing —
1099
+ * the gesture is in un-mark mode, and there's nothing marked yet to rescue.
1100
+ */
1101
+ beginEraseStroke(world: Vec2, restore?: boolean, strokeErase?: boolean): void;
1102
+ /**
1103
+ * Incrementally mark the brush points erased by the eraser segment `a → b`
1104
+ * (world). Iterates the current brushes and grows `stroke.erased` in place —
1105
+ * O(points) per move (each already-erased point is skipped), so a long drag
1106
+ * no longer costs O(points × path length) per frame. Radius is the on-screen
1107
+ * eraser ring converted to world units.
1108
+ */
1109
+ private markStrokeEraseSegment;
1110
+ /**
1111
+ * Extend the eraser stroke to `world`, sweeping shapes along the segment.
1112
+ * `restore` (Alt held) un-marks swept shapes instead of marking them.
1113
+ */
1114
+ extendEraseStroke(world: Vec2, restore?: boolean): void;
1115
+ /**
1116
+ * Commit the eraser stroke — delete every swept shape in ONE undo step (with
1117
+ * their attached links). No-op delete when nothing was swept. Returns the
1118
+ * count removed.
1119
+ */
1120
+ commitEraseStroke(): number;
1121
+ /** Abort the eraser stroke without deleting anything. */
1122
+ cancelEraseStroke(): void;
1123
+ /** True only while the pointer is down in laser mode (a trail is being laid). */
1124
+ get laserDrawing(): boolean;
1125
+ /** Start a laser trail at `world` (ephemeral — never enters the scene). */
1126
+ beginLaserStroke(world: Vec2): void;
1127
+ /** Append a point to the active laser trail (no-op unless drawing). */
1128
+ extendLaserStroke(world: Vec2): void;
1129
+ /** End the active laser trail — it keeps fading via the animation tick. */
1130
+ endLaserStroke(): void;
1131
+ /**
1132
+ * True while any laser trail OR eraser trail still has visible points (drives
1133
+ * the fade tick — so the trail keeps melting after the pointer stops).
1134
+ */
1135
+ hasActiveLaser(): boolean;
1136
+ /** Live eraser drag trail (ephemeral, fading). Empty when none active. */
1137
+ get eraserTrail(): readonly LaserStroke[];
1138
+ /**
1139
+ * Drop expired laser/eraser trail points (called once per frame before paint).
1140
+ * Self-terminating: once both arrays empty the animation tick stops.
1141
+ */
1142
+ private pruneLaser;
1076
1143
  arrangeAsGrid(opts?: {
1077
1144
  cols?: number;
1078
1145
  gap?: number;
@@ -1106,6 +1173,14 @@ export declare class Editor {
1106
1173
  */
1107
1174
  duplicateSelectedInPlace(anchorId?: ElementId | null): ElementId | null;
1108
1175
  setSelection(ids: Iterable<ElementId>): void;
1176
+ /**
1177
+ * Programmatically select a single link by id (or clear the link
1178
+ * selection with `null`), clearing the element selection so the link
1179
+ * becomes the sole selection. Used by host navigation (search / jump-to)
1180
+ * to frame an edge with {@link zoomToSelection}. No-op when nothing
1181
+ * would change.
1182
+ */
1183
+ selectLink(id: LinkId | null): void;
1109
1184
  selectAll(): void;
1110
1185
  /**
1111
1186
  * Internal clipboard. Stored as deep-cloned snapshots so subsequent
@@ -1145,6 +1220,186 @@ export declare class Editor {
1145
1220
  /** Whether a style has been copied and can be pasted. */
1146
1221
  get hasStyleClipboard(): boolean;
1147
1222
  updateStyle(ids: Iterable<ElementId>, partial: Partial<TextStyle>): void;
1223
+ /**
1224
+ * Re-base the width of committed brush strokes (`style.strokeWidth` has no
1225
+ * effect on brushes — their widths are baked per point). Scales every baked
1226
+ * point width proportionally and records the new `baseWidth`, keeping the
1227
+ * stroke's pressure profile. One undo step. Read-only editors ignore it.
1228
+ */
1229
+ setBrushWidth(ids: Iterable<ElementId>, width: number): void;
1230
+ /**
1231
+ * Apply a partial text style (bold / italic / colour / decoration) to the
1232
+ * character range `[from, to)` of a single text element, producing styled
1233
+ * runs (rich text). One undo step. No-op when the id isn't a text shape or
1234
+ * the range is empty. Read-only editors ignore it. Use this — rather than
1235
+ * `updateStyle` — to style only PART of a text block (e.g. the current
1236
+ * inline-edit selection).
1237
+ */
1238
+ applyTextStyleToRange(id: ElementId, from: number, to: number, partial: Partial<TextStyle>): void;
1239
+ /**
1240
+ * The fill (or stroke, per `role`) colour of the top-most shape under the
1241
+ * world point, or `null` on empty canvas. Pure read — no mutation.
1242
+ */
1243
+ pickColorAt(worldPoint: Vec2, role?: "fill" | "stroke"): Color | null;
1244
+ /** One-shot callback armed by {@link beginEyedropperPick}; consumes the next canvas click. */
1245
+ private pendingEyedropperPick;
1246
+ /** `true` while a colour-picker pipette is armed and waiting for a canvas click. */
1247
+ get isEyedropperArmed(): boolean;
1248
+ /**
1249
+ * Arm the eyedropper for a one-shot canvas pick that routes the sampled colour
1250
+ * to `onPick` (e.g. a colour-picker swatch) instead of the selection fill. Does
1251
+ * NOT change the tool mode — the next canvas press is intercepted by
1252
+ * {@link applyEyedropperAt}. Cancelled by a mode switch or an empty-canvas click.
1253
+ */
1254
+ beginEyedropperPick(onPick: (color: Color) => void): void;
1255
+ /**
1256
+ * Sample the colour under `worldPoint`. When a pipette pick is armed (see
1257
+ * {@link beginEyedropperPick}), route the colour to that callback and disarm.
1258
+ * Otherwise apply it as the current selection's fill (programmatic path).
1259
+ * Returns the sampled colour, or `null` on empty canvas. Read-only editors
1260
+ * sample but don't mutate.
1261
+ */
1262
+ applyEyedropperAt(worldPoint: Vec2): Color | null;
1263
+ /**
1264
+ * Convert every convertible selected shape (rectangle / ellipse / diamond)
1265
+ * to `target`, preserving position, size and style. One undo step; no-op
1266
+ * when nothing applies. See {@link ConvertTarget}.
1267
+ */
1268
+ convertSelection(target: ConvertTarget): void;
1269
+ /**
1270
+ * Flowchart auto-generate: clone the single selected node offset in
1271
+ * `direction` and connect the two with a fresh link. Selects the new node.
1272
+ * No-op unless exactly one element is selected. One undo step.
1273
+ */
1274
+ spawnConnectedNode(direction: SpawnDirection): void;
1275
+ /**
1276
+ * Pending flowchart-create session, or `null` when idle. Holds the ORIGINAL
1277
+ * source id + direction, the current sibling `count`, and the pending
1278
+ * `elements` + `links` (a PREVIEW — not yet in the scene / history). Grown by
1279
+ * {@link growFlowchart}, committed by {@link commitFlowchart}, discarded by
1280
+ * {@link cancelFlowchart}.
1281
+ */
1282
+ private flowchartSession;
1283
+ /**
1284
+ * Grow the flowchart-create preview one step in `direction`. Starts a session
1285
+ * (count = 1) when idle or when the direction changes; otherwise bumps the
1286
+ * sibling count up to {@link FLOWCHART_MAX_SIBLINGS}. Recomputes the pending
1287
+ * nodes/links from the ORIGINAL source each call. PREVIEW ONLY — never
1288
+ * touches the scene or history until {@link commitFlowchart}. No-op in
1289
+ * read-only mode or unless exactly one element is selected.
1290
+ */
1291
+ growFlowchart(direction: SpawnDirection): void;
1292
+ /**
1293
+ * Commit the pending flowchart-create preview: add every pending node + link
1294
+ * to the scene as ONE undo step, select the first new node, clear the
1295
+ * session. Returns the first new node's id, or `null` when no session is
1296
+ * active.
1297
+ */
1298
+ commitFlowchart(): ElementId | null;
1299
+ /** Discard the pending flowchart-create preview without committing. */
1300
+ cancelFlowchart(): void;
1301
+ /**
1302
+ * The pending flowchart-create preview (nodes + links), or `null` when no
1303
+ * session is active. Read by the render snapshot to paint the preview on the
1304
+ * overlay. Reference-stable between renders (only changes on grow / commit /
1305
+ * cancel) so the overlay memo doesn't thrash.
1306
+ */
1307
+ get flowchartPreview(): {
1308
+ readonly elements: readonly Element[];
1309
+ readonly links: readonly Link[];
1310
+ } | null;
1311
+ /**
1312
+ * Move the selection to an adjacent node. With exactly one element selected,
1313
+ * prefers a graph neighbour (linked node) best aligned with `direction`;
1314
+ * falls back to the spatial {@link selectClosest} when no neighbour lies that
1315
+ * way. No-op unless exactly one element is selected.
1316
+ */
1317
+ navigateFlowchart(direction: "left" | "right" | "up" | "down"): void;
1318
+ /**
1319
+ * Live image-crop session, or `null` when not cropping. Excalidraw-style:
1320
+ * the crop frame IS the element's visible box, and the user drags edge /
1321
+ * corner handles inward (hides pixels) or the image body (pans the source).
1322
+ *
1323
+ * - `id` — the image being cropped.
1324
+ * - `crop` — pending normalised source rect.
1325
+ * - `position` / `width` / `height` — the pending element box (world position
1326
+ * + local size); a handle drag moves them, a body pan leaves them fixed.
1327
+ * - `drag` — the active gesture, or `null` when only hovering.
1328
+ * - `dragStartWorld` — pointer world position at drag start (body pan basis).
1329
+ *
1330
+ * Seeded on {@link beginImageCrop}; committed by {@link commitImageCrop} (one
1331
+ * undo step), abandoned by {@link cancelImageCrop}.
1332
+ */
1333
+ cropSession: {
1334
+ id: ElementId;
1335
+ crop: ImageCrop;
1336
+ position: Vec2;
1337
+ width: number;
1338
+ height: number;
1339
+ drag: {
1340
+ kind: "handle";
1341
+ handle: CropHandle;
1342
+ } | {
1343
+ kind: "body";
1344
+ } | null;
1345
+ dragStartWorld: Vec2 | null;
1346
+ } | null;
1347
+ /** The image-crop session (read-only accessor for UI / overlay). */
1348
+ get imageCropSession(): {
1349
+ readonly id: ElementId;
1350
+ readonly crop: ImageCrop;
1351
+ readonly position: Vec2;
1352
+ readonly width: number;
1353
+ readonly height: number;
1354
+ } | null;
1355
+ /**
1356
+ * Enter crop mode for the image `id`, seeding the pending crop / box from its
1357
+ * current state (or the full image). No-op for non-image shapes or in
1358
+ * read-only. Typically triggered by a double-click on an image.
1359
+ */
1360
+ beginImageCrop(id: ElementId): void;
1361
+ /**
1362
+ * Hit-test `worldPoint` against the pending crop chrome: a crop handle when
1363
+ * within {@link CROP_HANDLE_HIT_RADIUS} (screen px, zoom-compensated) of one,
1364
+ * `"body"` when inside the window, else `null`. Returns `null` when not
1365
+ * cropping.
1366
+ */
1367
+ cropHandleAtWorld(worldPoint: Vec2): CropHandle | "body" | null;
1368
+ /** Begin dragging crop handle `handle` from `worldPoint`. */
1369
+ beginImageCropHandle(handle: CropHandle, worldPoint: Vec2): void;
1370
+ /** Begin panning the image body under the fixed window from `worldPoint`. */
1371
+ beginImageCropBody(worldPoint: Vec2): void;
1372
+ /**
1373
+ * Update the active crop drag to `worldPoint` — resize the window (handle) or
1374
+ * pan the source (body). Geometry is recomputed from the ORIGINAL element so
1375
+ * it stays stable across many moves. No-op when no drag is active.
1376
+ */
1377
+ updateImageCropDrag(worldPoint: Vec2): void;
1378
+ /** Finish the current crop drag (keeps the pending crop / box). */
1379
+ endImageCropDrag(): void;
1380
+ /** Apply the pending crop + box and leave crop mode. One undo step. */
1381
+ commitImageCrop(): void;
1382
+ /** Abandon the crop session without changing the image. */
1383
+ cancelImageCrop(): void;
1384
+ /**
1385
+ * Synthetic element carrying the PENDING crop box (position / size) over the
1386
+ * original element's rotation / scale — the frame the user currently sees.
1387
+ * Used to project the crop frame and handles.
1388
+ */
1389
+ private pendingCropElement;
1390
+ /**
1391
+ * World-space corners (clockwise) of the pending crop frame, or `null` when
1392
+ * not cropping. The frame is the pending element box mapped through its
1393
+ * local→world transform (so rotation / scale are honoured).
1394
+ */
1395
+ private cropFrameCorners;
1396
+ /**
1397
+ * Ghost-image overlay descriptor for the crop session: the ORIGINAL element
1398
+ * (its transform + live bitmap handle) and the virtual full-image LOCAL rect
1399
+ * the whole bitmap occupies. `null` when not cropping. The overlay paints the
1400
+ * full bitmap faintly over this rect so hidden parts stay visible.
1401
+ */
1402
+ private cropGhost;
1148
1403
  /**
1149
1404
  * Update non-style text properties (`fontSize`, `fontFamily`,
1150
1405
  * `maxWidth`) on every selected text shape. Non-text shapes are
@@ -1253,6 +1508,13 @@ export declare class Editor {
1253
1508
  zoomToFit(padding?: number): void;
1254
1509
  /** Fit the camera to the current selection (standard `⌥2`). No-op when empty. */
1255
1510
  zoomToSelection(padding?: number): void;
1511
+ /**
1512
+ * Center the camera on the current selection for a reveal / jump-to (search
1513
+ * navigation). Unlike {@link zoomToSelection}, it does NOT fill the screen —
1514
+ * a small match keeps its size and is merely centered; the zoom only drops to
1515
+ * fit an oversized match. No-op when the selection is empty.
1516
+ */
1517
+ revealSelection(padding?: number): void;
1256
1518
  /**
1257
1519
  * Select the nearest interactable top-level element in `direction` from the
1258
1520
  * current selection's centre (or the viewport centre when nothing is
@@ -1429,7 +1691,22 @@ export declare class Editor {
1429
1691
  * selected, you can see".
1430
1692
  */
1431
1693
  computeHiddenElements(): ReadonlySet<ElementId> | undefined;
1694
+ /**
1695
+ * Live stroke-erase preview: while a Shift-held eraser gesture drags, the
1696
+ * fragments each touched brush WOULD become, plus the set of touched
1697
+ * originals to hide in the main pass. `null` outside a stroke-erase gesture
1698
+ * or when the path touches no brush. Recomputed each frame from the
1699
+ * path-so-far — never mutates the scene or history.
1700
+ */
1701
+ private computeStrokeErasePreview;
1432
1702
  computeDimElements(enteredGroupId: ElementId): ReadonlySet<ElementId>;
1703
+ /**
1704
+ * Dim set fed to the renderer: group-isolation dim UNION the eraser's
1705
+ * pending-delete set (shapes swept by the current eraser stroke are shown
1706
+ * dimmed so the user sees what release will delete). `undefined` when neither
1707
+ * is active, keeping the fast tile-cache render path.
1708
+ */
1709
+ private computeDimSet;
1433
1710
  /**
1434
1711
  * Enter a group — subsequent hits inside this group return children
1435
1712
  * directly instead of the group root. `null` exits group-edit mode.
@@ -1671,6 +1948,11 @@ export declare class Editor {
1671
1948
  * transaction so the whole drag is one undo step.
1672
1949
  */
1673
1950
  beginWaypointDrag(linkId: LinkId, index: number, insert: boolean): void;
1951
+ /** Caption (label pill) drag along the selected link's path. */
1952
+ get isDraggingLabel(): boolean;
1953
+ beginLabelDrag(linkId: LinkId): void;
1954
+ updateLabelDrag(world: Vec2): void;
1955
+ endLabelDrag(): void;
1674
1956
  /** Live update of the dragged waypoint to `world`. */
1675
1957
  updateWaypointDrag(world: Vec2): void;
1676
1958
  /**
@@ -1817,19 +2099,47 @@ export declare class Editor {
1817
2099
  */
1818
2100
  runLayout(parentId: ElementId): Patch | null;
1819
2101
  /**
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`.
2102
+ * Derived elbow-route cache, keyed by link the source of truth for
2103
+ * A*-routed corners, living OUTSIDE the immutable `Scene`. Each entry holds
2104
+ * the routed interior `points` (between from/to) plus the `sig` of the
2105
+ * inputs it was computed from (endpoint refs + bound-shape bounds +
2106
+ * fixedSegments), so an unchanged link short-circuits the reroute.
2107
+ *
2108
+ * `rerouteElbows` still MIRRORS `points` onto `Link.routedPoints` in
2109
+ * `_scene` because three readers still consume the baked field: the render
2110
+ * path (`getLinkPath` in `renderer-core`), the headless `getLinkPath`, and
2111
+ * serialization (`schema.ts`). The mirror is compat-only derived state — no
2112
+ * history push / notify.
2113
+ *
2114
+ * TODO(fable R7a): drop the `_scene` mirror once (a) the render path reads
2115
+ * routes from this cache via the `RenderSnapshot`, and (b) headless
2116
+ * `getLinkPath` / serialization stop depending on baked `routedPoints`.
2117
+ * That eviction changes headless geometry, serialized output, and
2118
+ * collab-synced fields, so it spans the scene / serialization / renderer /
2119
+ * headless goldens and must land as its own cross-package change — out of
2120
+ * scope for this state-only pass.
2121
+ */
2122
+ private readonly elbowRoutes;
2123
+ /**
2124
+ * Choke-point reroute (standard model): recompute the route for every
2125
+ * orthogonal link whose inputs changed since the last pass, store it in the
2126
+ * derived {@link elbowRoutes} cache, and mirror it onto `_scene`. Runs once
2127
+ * per frame before paint — derived state, so no history push / notify (would
2128
+ * loop). Cheap when nothing moved (signature short-circuit).
1823
2129
  */
1824
- private readonly elbowRouteSig;
2130
+ private rerouteElbows;
1825
2131
  /**
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).
2132
+ * Collect everything {@link renderEditor} paints from into a flat
2133
+ * {@link RenderSnapshot}. Resolves the derived viewport / dirty-rect / dim /
2134
+ * hide inputs and the shared spatial index up front (same order the
2135
+ * orchestrator used to call them in), so the paint pass stays side-effect
2136
+ * free and the orchestrator stays decoupled from this class.
2137
+ *
2138
+ * `computeDirtyWorld` is order-sensitive (it diffs against
2139
+ * `lastRenderedScene` and populates `tileDirtyElements`); it runs here and
2140
+ * the `lastRendered*` bookkeeping is applied in `render()` after the paint.
1831
2141
  */
1832
- private rerouteElbows;
2142
+ private buildRenderSnapshot;
1833
2143
  private render;
1834
2144
  }
1835
2145
  /** Distance from point `p` to the finite segment `a`–`b` (world space). */