@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/CHANGELOG.md CHANGED
@@ -1,5 +1,194 @@
1
1
  # @oh-just-another/state
2
2
 
3
+ ## 0.61.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0548ab3: Unify the active tool into a single `editor.activeTool` value object (breaking).
8
+ - `state`: `editor.activeTool: ActiveTool` (`{ type, locked, lastActiveTool }`)
9
+ replaces `editor.mode` and `editor.toolLocked`; `setActiveTool(type)` replaces
10
+ `setMode`. `EditorOptions.initialMode` → `initialTool`. The typed `mode` event
11
+ is now `tool` and fires with the `ActiveTool` object on a type switch or a
12
+ lock flip. The action category `"mode"` is now `"tool"`. The vestigial
13
+ `"eyedropper"` mode is removed from `Mode` — colour sampling is armed from
14
+ the colour picker (`beginEyedropperPick`) and never was a toolbar tool.
15
+ - `react-ui`: `useMode()` → `useActiveTool(): ActiveTool`;
16
+ `DiagramRoot`/`DiagramCanvas` prop `initialMode` → `initialTool`.
17
+ - `editor`: `EditorAPI.getMode/setMode` → `getActiveTool/setActiveTool`;
18
+ `initialMode` prop → `initialTool`; re-exports `ActiveTool`.
19
+ - `diagram` (+ vue/svelte/angular wrappers): element methods and controller
20
+ `getMode/setMode` → `getActiveTool/setActiveTool`.
21
+
22
+ Tool ids are unchanged (`"select"`, `"draw-rect"`, …). There are no visible
23
+ behaviour changes — this is an API refactor establishing one source of truth
24
+ for "which tool is active".
25
+
26
+ - 762dd8a: Brush capture pipeline upgrade: input streamlining (low-pass with commit-time catch-up), speed-simulated pressure for mouse/touch (slow = thick, fast = thin) with rate-limited pen pressure, sample decimation with a soft point cap, and end tapering. `BrushElement` gains an optional regeneration payload (`pressures`, `simulatePressure`, `baseWidth`) carried through serialization; `Editor.beginBrushStroke` accepts a `pointerType` argument to pick the pressure source. The live preview runs the same pipeline as the commit.
27
+ - 4722388: Editable width for committed brush strokes. `style.strokeWidth` never
28
+ affected brushes (their widths are baked per point), so the property panel's
29
+ Thin/Medium/Thick control silently did nothing for them. A brush-only
30
+ selection now gets a popover range slider driving the new
31
+ `Editor.setBrushWidth(ids, width)`, which scales every baked point width by
32
+ `newWidth / baseWidth` — the stroke keeps its exact pressure profile at the
33
+ new thickness — and records the new `baseWidth`. Legacy strokes without a
34
+ recorded base fall back to their widest point. One undo step.
35
+ - 05707ed: Drag the caption pill along its link. With the link selected, dragging the
36
+ pill slides the label along the drawn path (the cursor is projected back onto
37
+ the polyline — new `projectPointToPathT` in scene); within a few pixels of
38
+ the arc-length middle it snaps back to the default placement
39
+ (`label.position` removed, so elbow links regain longest-segment
40
+ auto-placement). One undo step, Escape reverts, double-click still opens the
41
+ inline text editor, and handle dots keep pointer priority over the pill.
42
+ Tunable snap radius: `LINK_LABEL_DRAG_SNAP_PX`.
43
+ - 84450bc: Link captions: measured rounded pill, multiline, correct placement and hit-testing.
44
+ - `scene`: new shared label geometry (`linkLabelAnchor`, `estimateLinkLabelBox`,
45
+ `linkLabelBounds`, `pointAlongPath`) — one source of truth for the renderer,
46
+ hit-testing and culling. `findLinkAt` now also hits inside the caption pill.
47
+ Elbow links place an unpositioned label on the longest segment's midpoint;
48
+ explicit `label.position` is clamped away from the arrowheads. Tunables in
49
+ `constants.ts` (`LINK_LABEL_MAX_WIDTH`, paddings, clearance).
50
+ - `renderer-core`: the caption is a rounded pill sized by real `measureText`
51
+ word-wrap (multiline, `\n` breaks) instead of a square estimated box; it
52
+ rides the drawn geometry (flattened curve for bezier, not the chord), and
53
+ `computeLinkWorldBounds` unions the pill so dirty-rect / viewport culling
54
+ never clip it. `LABEL_POSITION` / `LABEL_FONT_SIZE` constants moved to
55
+ `scene` as `LINK_LABEL_DEFAULT_POSITION` / `LINK_LABEL_DEFAULT_FONT_SIZE`.
56
+ - `state`: `linkLabelWorld` uses the shared anchor, so the inline editor opens
57
+ exactly over the pill (including bezier and elbow links).
58
+ - `react-ui`: the inline caption editor is a multiline textarea — Enter
59
+ commits, Shift+Enter inserts a newline, Escape cancels.
60
+
61
+ ### Patch Changes
62
+
63
+ - 50a2bd4: Fix: with a creation tool active (draw-edge, draw-rect, …) a press on a
64
+ selected shape's edge no longer grabs the shape's resize/rotate handle — it
65
+ starts the new link / element as expected. Selection chrome (resize, rotate,
66
+ group and selected-link endpoint handles) is now pressable only under the
67
+ select tool, and the overlay stops drawing the handles while a creation tool
68
+ is active (outlines stay; the hand tool keeps the chrome visible).
69
+ - 20af638: Fix: the caption pill no longer fights the bend/segment handles. The
70
+ "add waypoint" and elbow segment handles slide out from under the label pill
71
+ along their own span (`getLinkWaypointMidpoints` is label-aware; new shared
72
+ `getElbowSegmentHandles` keeps the drawn dot and the grab point identical), so
73
+ a click on the pill selects the link and a double-click opens the inline
74
+ caption editor. Visible handle dots keep pointer priority — an existing
75
+ waypoint dot sitting inside the pill is still grabbable (dots draw above the
76
+ pill).
77
+ - 3c50ef1: Tile cache honours per-element hide (B12, hide half). `renderViaTiles` accepts
78
+ `hideElements`: tiles bake with the set applied, and an element entering or
79
+ leaving the set invalidates only the tiles it touches — so the stroke-eraser
80
+ preview and per-element visibility no longer drop very large scenes off the
81
+ tile-cache path into a full re-render every frame. Group-isolation dim still
82
+ takes the full path (dimming almost everything would re-rasterise most tiles
83
+ anyway).
84
+ - f960332: Fix: mp4 shapes survive a page reload. The video file-drop handler never
85
+ persisted the bytes into `Scene.files`, so a restored scene had only a dead
86
+ `blob:` URL and nothing to rehydrate from (the "dead-blob-url" renderer
87
+ warning) on every backend. The handler now stores the file (`fileId` on the
88
+ shape), and scene rehydration grew a video branch: it rebuilds the hidden,
89
+ muted, looping `<video>` element from the persisted bytes (shared
90
+ `createHiddenLoopingVideo` factory with the drop handler) and resumes
91
+ playback best-effort. Videos dropped BEFORE this fix have no stored bytes and
92
+ still won't restore — re-add them once.
93
+ - Updated dependencies [762dd8a]
94
+ - Updated dependencies [05707ed]
95
+ - Updated dependencies [20af638]
96
+ - Updated dependencies [84450bc]
97
+ - @oh-just-another/scene@0.61.0
98
+ - @oh-just-another/renderer-core@0.60.0
99
+ - @oh-just-another/history@0.57.4
100
+
101
+ ## 0.60.0
102
+
103
+ ### Minor Changes
104
+
105
+ - 783749e: Brush strokes can now be closed and filled. When a fill colour is set in the drawing panel and a stroke's end is drawn back near its start (within `BRUSH_CLOSE_DISTANCE`), the committed `BrushElement` gets `closed: true` and the renderer fills the enclosed area with `style.fill` under the variable-width stroke body. Open strokes and strokes drawn without a fill colour are unchanged. `BrushElement.closed` is serialized.
106
+ - 641842b: Brush strokes now carry host-controlled paint settings instead of a hard-coded colour. `editor.brushSettings` / `editor.setBrushSettings({ stroke, fill, opacity, width })` set the line colour, enclosed-fill colour (for a future closed-stroke fill), opacity, and base width; a committed stroke bakes them into its style, and the width drives the pressure curve. The brush renderer now paints the line from `style.stroke` (falling back to `style.fill` for strokes authored before the split), so old strokes are unchanged. Fixes the previously hard-coded `#222` brush colour.
107
+ - 0d3934e: Eraser gains Alt-restore. While sweeping the eraser, holding Alt un-marks shapes you drag back over — rescuing them before the delete commits on pointer-up (`extendEraseStroke(world, restore)` / `beginEraseStroke(world, restore)`). Marked-for-erase shapes now preview at a dedicated `ERASE_DIM_OPACITY` (0.2 — a clear "about to delete") instead of the gentler group-isolation dim.
108
+ - 571f13b: The eraser now shows a dedicated cursor instead of the generic crosshair: a grey ring that follows the pointer, sized to the panel's eraser width (`brushSettings.width`), plus a short fading grey trail while you drag. The OS cursor is hidden in erase mode (`cursor: none`) and the ring/trail are painted on the overlay so any radius composites cleanly. The laser and eraser trails now share one `drawFadingTrail` renderer. New `CursorRole` `"erase"`.
109
+ - ca48e8a: Add two interaction tools: an eraser (mode `erase`, hotkey `E`) and a laser pointer (mode `laser`, hotkey `K`).
110
+ - Eraser: press-and-drag sweeps shapes under the cursor into a pending set (previewed dimmed) and deletes them all in one undo step on release. Attached links are removed with their shapes, like a Delete-key delete.
111
+ - Laser pointer: press-and-drag paints an ephemeral red trail that fades over a couple of seconds. Nothing is written to the scene or history — it lives purely on the overlay. Available in read-only mode. Collab replication of trails is a follow-up.
112
+
113
+ Both tools appear in the default toolbar (`DEFAULT_TOOLBAR` / `DEFAULT_VERTICAL_TOOLBAR`) and are registered as `mode-erase` / `mode-laser` actions. TTL, colour and width of the laser trail are tunable via `state/constants.ts`.
114
+
115
+ - bdc847e: Add four editor tools:
116
+ - **Eyedropper** — new `eyedropper` mode (toolbar button, `Alt+I`). Click a
117
+ shape to sample its fill/stroke and apply it to the selection.
118
+ `Editor.pickColorAt` / `applyEyedropperAt` + pure `pickColorAt`.
119
+ - **Convert element type** — `Editor.convertSelection(target)` and pure
120
+ `computeConvertType` switch rectangle ↔ ellipse ↔ diamond (polygon) in place,
121
+ preserving position/size/style. New "Shape type" property-panel control.
122
+ - **Image crop** — optional normalised `crop` rect on `ImageElement` (additive,
123
+ serialised). New `crop` mode entered by double-clicking an image or the
124
+ property-panel Crop button: drag a frame, `Enter` to apply, `Esc` to cancel.
125
+ Canvas2D renders the cropped source region (`RenderTarget.drawImage` gains an
126
+ optional `crop` arg). `Editor.beginImageCrop` / `commitImageCrop` /
127
+ `cancelImageCrop` + pure `computeSetImageCrop` / `cropRectFromWorldDrag`.
128
+ - **Flowchart auto-generate** — `Cmd/Ctrl+Alt+Arrow` spawns a connected node
129
+ from the selected node in that direction. `Editor.spawnConnectedNode` + pure
130
+ `computeSpawnConnectedNode`.
131
+
132
+ Also exposes `worldToLocal` from `@oh-just-another/scene`.
133
+
134
+ - 511a22a: The eyedropper is no longer a standalone toolbar tool (its palette button and the `Alt+I` hotkey / `mode-eyedropper` action are removed). Instead, every colour picker (`ColorSwatchPicker`) gains an optional pipette button via a new `onEyedrop` prop: clicking it arms `Editor.beginEyedropperPick(onPick)` and the next canvas click samples the colour of the shape under the cursor straight into that swatch — without changing the current tool mode. New editor API: `beginEyedropperPick` and the `isEyedropperArmed` flag; the cursor shows a crosshair while armed.
135
+ - a9558d9: Reworked flowchart keyboard model. Arrow-key bindings are reworked and disambiguated by modifier: `Arrow` nudges (unchanged); `Cmd/Ctrl+Arrow` grows a flowchart CREATE session (each press adds a pending connected sibling, previewed on the overlay, committed as one undo step when Cmd/Ctrl is released, cancelled on Escape); `Alt+Arrow` navigates to the adjacent node (graph neighbour, else spatially nearest); `Cmd/Ctrl+Shift+Arrow` aligns (moved off the old plain `Alt+Arrow`). The old `Cmd/Ctrl+Alt+Arrow` spawn binding is retired.
136
+
137
+ New API: `editor.growFlowchart` / `commitFlowchart` / `cancelFlowchart` / `navigateFlowchart` / `flowchartPreview`, the pure `computeSpawnConnectedNodes`, and `endpointElementId` (scene). `<Diagram>` wires the keyup-commit / Escape-cancel lifecycle for you.
138
+
139
+ - 295f38b: Thread the animated-content playback clock as a per-instance render provider instead of a process-global singleton. `RenderSceneOptions` gains an optional `clock`, forwarded to each shape renderer via `ElementRenderContext.clock` (new `AnimationClock` type export). `Editor` now passes its own per-shape playback clock through the render snapshot, so two editors on one page drive independent GIF playback and no longer overwrite a shared module global every frame. The module-global `setAnimationClock` remains as a documented process-global fallback for context-less paths (headless SVG / worker / PNG export and the tile compositor); behaviour is unchanged when no per-instance clock is supplied. Additive and backwards-compatible.
140
+ - 7f69f29: Add scene text search, a stats/dimensions overlay, and zen mode.
141
+ - `state`: `searchScene(scene, query)` / `elementSearchText(element)` — a pure, case-insensitive substring index over text shapes, frame names, and edge labels; plus `Editor.selectLink(id)` to programmatically select a single connector.
142
+ - `react-ui`: `<SearchOverlay>` (⌘F) finds and frames matching text with next/prev navigation; `<StatsPanel>` (⌥/) shows the selection's x/y/w/h/angle and scene totals; `<ZenModeProvider>` / `useZenMode` (⌥Z, Esc to exit) hides chrome for focused work. All three are wired into `<Editor>` from `@oh-just-another/editor`.
143
+
144
+ - cec8f83: Add read-only / view mode. `EditorOptions.readOnly`, `editor.readOnly`, `editor.setReadOnly()` and `editor.toggleReadOnly()` gate every scene-mutating pointer path (create / move / resize / rotate / annotation / edge edits) at the `applyEmit` choke point and in the pointer-down handlers, while pan / zoom / click + marquee select stay live. The action registry now honours each action's `viewMode` flag — in read-only only `viewMode` actions dispatch (zoom, pan, grid, select-all, cancel, and the new `toggle-read-only` action bound to `⌥R`).
145
+ - 1975a9b: Stroke-eraser: holding Shift while erasing cuts brush strokes into fragments instead of deleting the whole element. Each brush point within the eraser capsule (radius = the on-screen eraser ring in world units, widened by the point's own half-width) is removed; surviving points split into fragment strokes (a lone kept point becomes a dot), links bound to a cut brush are detached, and it all lands in one undo step. A live preview shows the cut while you drag (touched originals hidden, fragments shown). Without Shift the eraser still deletes whole elements; non-brush shapes under Shift fall back to whole-element erase.
146
+ - cf8b735: Add styled text runs (rich text, phase 1): a `TextElement` can now carry an optional `runs` overlay — contiguous substrings each with a partial `TextStyle` (bold / italic / colour / decoration) over the element's base style. The flat `text` stays the source of truth (`runs.map(r => r.text).join("") === text`), so plain-text scenes render, serialise and round-trip byte-for-byte unchanged.
147
+ - `scene`: `TextRun` type + `TextElement.runs?`; pure helpers `runsToText`, `normalizeRuns`, `sliceRuns`, and `applyStyleToRange(el, from, to, partial)` (splits/merges/coalesces runs, sheds the overlay when uniform).
148
+ - `serialization`: additive optional `runs` in the text schema; legacy documents (no `runs`) round-trip unchanged.
149
+ - `renderer-core`: the text renderer draws each visual line's style segments with per-run font + fill through the shared `RenderTarget`, so Canvas2D, WebGL2 and SVG all honour runs. Line breaking still uses the element's base metrics.
150
+ - `state`: `Editor.applyTextStyleToRange(id, from, to, partial)` applies a style to a character range as one undo step.
151
+ - `react-ui`: the text formatting controls (bold / italic / underline / strikethrough / colour) target the current inline-edit selection when one is active — styling just those characters — and fall back to whole-element styling otherwise.
152
+
153
+ Full inline rich-text editing (per-run wrap metrics, caret-aware run editing) is a follow-up.
154
+
155
+ ### Patch Changes
156
+
157
+ - c189261: The live brush-stroke preview is now Catmull-Rom-smoothed with the same resampler `commitBrushStroke` applies on release, so a stroke reads smooth as it's drawn instead of snapping from an angular polyline to a curve only when the pointer is lifted.
158
+ - c189261: The live brush-stroke preview now paints in the chosen palette colour and opacity instead of a hardcoded dark-grey fill, so it matches the committed stroke. The brush body colour resolution is now a single shared `brushBodyColor(style)` helper (exported from `@oh-just-another/scene`) used by both the committed-stroke renderer and the preview, so the two can't drift.
159
+ - c189261: Brush strokes now render as a single closed outline polygon filled once, instead of a chain of per-segment quads plus a disc at every joint. The old approach overlapped itself, so at `opacity < 1` the joins double-blended into dark blotches; the single fill paints every pixel exactly once. Round joins/caps are preserved (arc points on convex corners, mitered concave corners clamped to stay a simple, non-self-intersecting polygon). The outline geometry is a new shared `brushOutline(points)` helper (exported from `@oh-just-another/scene`) used by both the committed-stroke renderer and the live preview.
160
+ - c58054b: Brush strokes are now smoothed on commit: the sparsely-captured pointer polyline is resampled through a Catmull-Rom spline (interpolating per-point width) before it enters the scene, so a freehand line reads as a fluid curve instead of a chain of angular segments. Shares one spline resampler (`smoothStrokePoints`) with the laser trail. Tunable via `BRUSH_SMOOTH_SEGMENTS`.
161
+ - b156869: Rework image cropping to a handle interaction. The crop frame is now the image's visible box: double-click an image to enter crop mode, then drag the 8 edge/corner handles to hide pixels (opposite edge stays fixed, the source is never stretched) or drag the image body to pan the source under the frame. A faint full-image ghost is drawn behind the frame so the hidden regions stay visible. Enter/click-outside commits (one undo step), Escape cancels. Replaces the previous rubber-band "draw a rectangle over the image" model.
162
+
163
+ New pure geometry in `tool-ops` (`computeCropHandleDrag`, `computeCropBodyPan`, `computeCommitImageCrop`, `cropFullImageLocalRect`, `cropHandleWorldPoints`, `CropHandle`) and Editor methods (`cropHandleAtWorld`, `beginImageCropHandle`, `beginImageCropBody`); `imageCropSession` now exposes the pending `{crop, position, width, height}`. The normalised `ImageCrop` data model is unchanged. Removed `cropRectFromWorldDrag`.
164
+
165
+ - b0a9f3b: The eraser cursor disc is now filled solid in the trail colour (fully opaque), with the ring on top — a clear, high-contrast aim target that matches the eraser wake.
166
+ - 1975a9b: The stroke-eraser no longer eats more of a line than the cursor ring shows. A brush point was erased when the eraser capsule reached the stroke's outer EDGE (`radius + point.width`); now it's erased when the ring covers the point's centre (`radius`), which equals the visible cursor ring at every zoom. The eraser removes exactly the centreline it passes over.
167
+ - 1975a9b: Fix the eraser cursor freezing when you pause mid-drag (button held) and then resume. The fading trail could empty during the pause, and the resumed move then had neither an active trail nor an object change, so it never triggered a repaint — the cursor stuck until release, when the whole cut applied at once. The eraser now always repaints on move (so the ring follows the pointer) and restarts the trail if it had faded.
168
+ - 22b90f9: The keyboard-shortcuts help dialog now lists every real binding. The `arrange` category (align / flip / distribute) was missing from the dialog's category order and is now shown, and keyTest-driven bindings (nudge arrows, Enter edit/create, plus flowchart create/navigate) surface their chips via a new display-only `Action.displayHotkey` field instead of rendering as "—". `displayHotkey` is never dispatched (only `hotkey`/`keyTest`/`sequence` are), which also closes a latent hole where a display matcher could fire a Ctrl-modified combo the `keyTest` deliberately excluded.
169
+ - f381039: Clicking a drawn line's (brush stroke's) link-start dot no longer clone-creates a connected element — duplicating a freehand line as a "node" made no sense. The start dots and dragging a real link from a brush stroke are unchanged; only the click-to-clone (and its hover ghost) is suppressed for brush sources. Other shapes keep the spawn-connected-node behaviour.
170
+ - bd2e26c: Make read-only (view) mode a true guard. Every mutating `Editor` method reachable from the UI (`updateStyle`, `updateTextProps`, `deleteSelected`, `duplicateSelected`, group/ungroup, align/flip/distribute, z-order, `moveSelectionBy`, `setLink`, `convertSelection`, `clear`, etc.) is now a no-op while `readOnly` is set, backstopping direct panel/hotkey calls that previously bypassed the pointer-level gate. The overlay keeps the selection outline (halo) but no longer paints resize/rotate/group handles or link endpoint grips in read-only, and the property panel / selection floating panel / mutating context-menu entries are hidden. `copy` / `copy-style` are flagged view-safe so they stay live.
171
+ - 97de2fd: Hide the link-creation overlay entirely in read-only mode. Hovering an element no longer shows connection anchor dots, and hovering a dot no longer previews a ghost element/connector — read-only never creates links, so the whole port/ghost overlay is now gated off. Editable behaviour is unchanged.
172
+ - 71a6c8b: Search navigation no longer blows a small match up to fill the whole canvas. Jumping to a match now centers it while preserving the current zoom, only zooming out when the match is too large to fit — a small element stays small and just lands in the center. Adds `Editor.revealSelection(padding)` and the pure `computeRevealBounds` helper (never zooms in, unlike `zoomToSelection`'s fit-to-fill).
173
+ - dde8279: Perf: memoize the overlay-options bag per overlay target and reuse it across frames whose overlay inputs are identity-unchanged (idle / animation / peer-update frames), rebuilding only on a real state change; GIF "play" badges are still recomputed every frame. Feed the persistent spatial index (shared with the hit-test path) to the tile compositor so large-scene tile rasterisation queries the index instead of scanning every shape per tile. Group isolation (dim) / per-element hide now correctly fall back to the full `renderScene` path when the tile cache is enabled, instead of silently dropping the dim/hide effect. Behaviour is unchanged when no tile cache is used and when no isolation/hide is active.
174
+ - 1975a9b: Two stroke-eraser fixes. (1) No more freeze on a slow or stopped cursor: the whole-scene repaint forced while erasing now happens only on frames that actually mark or cut something, not on every eraser move — a slow/idle cursor generates many pointer events per unit distance, each of which was re-rendering the entire scene. (2) Cutting a stroke no longer leaves isolated single-point dots: lone kept points (a survivor with both neighbours erased) are dropped instead of kept as stray discs.
175
+ - 1975a9b: The stroke-eraser (Shift + erase over a brush) now cuts the stroke's **geometry by arc length** instead of dropping whole vertices. A large eraser that merely grazes a line — or one passing between two far-apart points on a fast/short stroke — removes exactly the span it covers, with the fragment edges pinned to the eraser ring. This fixes the eraser ignoring sparsely-sampled or short strokes and eating a gap unrelated to the disc size.
176
+ - 1975a9b: Stroke-eraser no longer freezes on longer drags. The live cut was recomputed against the entire eraser path every frame (O(points × path length)), so the main thread saturated as the path grew — the cursor froze and the whole cut applied at once on release. Erased points are now accumulated incrementally (each move tests only the new segment, skipping already-erased points), making the per-move cost O(points) and the preview smooth throughout the drag.
177
+ - 571f13b: The laser and eraser trails now render as one filled comet shape per stroke instead of a stack of alpha-blended segments. The smoothed centreline is offset into a single ribbon whose half-width tapers from the head to a pointed tail, filled once at a single opacity that fades by the freshest point's age. This removes the overlapping round-cap "beads" at every joint that made the trail look like a chain of little lasers.
178
+ - Updated dependencies [783749e]
179
+ - Updated dependencies [c189261]
180
+ - Updated dependencies [c189261]
181
+ - Updated dependencies [641842b]
182
+ - Updated dependencies [c189261]
183
+ - Updated dependencies [0d3934e]
184
+ - Updated dependencies [bdc847e]
185
+ - Updated dependencies [a9558d9]
186
+ - Updated dependencies [295f38b]
187
+ - Updated dependencies [cf8b735]
188
+ - @oh-just-another/scene@0.60.0
189
+ - @oh-just-another/renderer-core@0.59.0
190
+ - @oh-just-another/history@0.57.3
191
+
3
192
  ## 0.59.0
4
193
 
5
194
  ### Minor Changes
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @oh-just-another/state
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@oh-just-another/state.svg)](https://www.npmjs.com/package/@oh-just-another/state)
4
+
3
5
  Interactive editor engine: tools, modes, selection, history, and hit-testing over a Scene.
4
6
 
5
7
  The L2 interaction layer. It wires a Scene together with render targets and high-level operations into an `Editor` controller — framework-agnostic, with no React dependency. Depends on `@oh-just-another/types`, `@oh-just-another/math`, `@oh-just-another/scene`, `@oh-just-another/renderer-core`, `@oh-just-another/history`, `@oh-just-another/events`, `@oh-just-another/tokens`, and `xstate` (v5). Hosts feed it normalized pointer/keyboard/wheel events; it never imports the DOM directly.
@@ -21,11 +23,11 @@ const editor = new Editor({
21
23
  mainTarget: layered.get("main"),
22
24
  overlayTarget: layered.get("overlay"),
23
25
  initialScene: emptyScene(),
24
- initialMode: "select",
26
+ initialTool: "select",
25
27
  });
26
28
 
27
29
  editor.subscribe(() => render());
28
- editor.setMode("draw-rect");
30
+ editor.setActiveTool("draw-rect");
29
31
  ```
30
32
 
31
33
  ## Concepts
@@ -34,7 +36,7 @@ editor.setMode("draw-rect");
34
36
 
35
37
  The centerpiece controller (`Editor`, `EditorOptions`, `LoadSceneOptions`). It owns the scene, selection, and viewport, and exposes a high-level API spanning:
36
38
 
37
- - **Tools / modes** — `mode`, `setMode`, `toolLocked` / `setToolLocked` (sticky tool).
39
+ - **Tools** — `activeTool` (`{ type, locked, lastActiveTool }`, the single source of truth), `setActiveTool`, `setToolLocked` (sticky tool).
38
40
  - **Selection** — `selection`, `selectAll`, `deleteSelected`, `moveSelectionBy`, `focusCycle`, plus link selection (`selectedLinks`, `selectedLink`).
39
41
  - **History** — `undo`, `redo`, `canUndo`, `canRedo`, `history` (backed by `@oh-just-another/history`).
40
42
  - **Hit-testing** — `hitTest(worldPoint)` returns a `PressTarget`; `hitAnnotation`.
@@ -46,7 +48,7 @@ The centerpiece controller (`Editor`, `EditorOptions`, `LoadSceneOptions`). It o
46
48
  - **Text editing** — inline caret/selection model: `beginTextEdit`, `commitTextEdit`, `cancelTextEdit`, `setEditingText`, `setTextCaretFromPoint`, `extendTextSelectionToPoint`, `caretIndexAtWorldPoint`, `editingTextOverlay`, plus `createTextAt`.
47
49
  - **Image / file insert** — `insertImage`, `addElement`, `beginPlacement`, and a `FileDropRegistry` (see below).
48
50
  - **GIF / animation playback** — `togglePlayback`, `hoverAnimatedElement`, `isPlaybackPaused`.
49
- - **Brush strokes** — `beginBrushStroke`, `extendBrushStroke`, `commitBrushStroke`, `cancelBrushStroke`, `pendingBrushStroke`.
51
+ - **Brush strokes** — `beginBrushStroke` (a `pointerType` argument picks the pressure source: real pen pressure, or speed-simulated for mouse/touch), `extendBrushStroke`, `commitBrushStroke`, `cancelBrushStroke`, `pendingBrushStroke`. Capture streamlines input, decimates samples, and tapers stroke ends on commit; the live preview matches the committed result.
50
52
  - **Annotations / comments** — `addAnnotation`, `removeAnnotation`, `toggleAnnotationResolved`, `addComment`, `removeComment`, `setSelectedAnnotation`, `setCommentAuthor`.
51
53
  - **Scene lifecycle** — `scene`, `loadScene`.
52
54