@oh-just-another/state 0.57.0 → 0.59.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 (233) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +76 -27
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/actions/actionArrange.d.ts +13 -0
  5. package/dist/actions/actionArrange.d.ts.map +1 -0
  6. package/dist/actions/actionArrange.js +130 -0
  7. package/dist/actions/actionArrange.js.map +1 -0
  8. package/dist/actions/actionClipboard.d.ts +2 -0
  9. package/dist/actions/actionClipboard.d.ts.map +1 -1
  10. package/dist/actions/actionClipboard.js +36 -4
  11. package/dist/actions/actionClipboard.js.map +1 -1
  12. package/dist/actions/actionGrouping.d.ts.map +1 -1
  13. package/dist/actions/actionGrouping.js +3 -1
  14. package/dist/actions/actionGrouping.js.map +1 -1
  15. package/dist/actions/actionKeyboard.d.ts.map +1 -1
  16. package/dist/actions/actionKeyboard.js +6 -2
  17. package/dist/actions/actionKeyboard.js.map +1 -1
  18. package/dist/actions/actionLayout.d.ts.map +1 -1
  19. package/dist/actions/actionLayout.js +12 -4
  20. package/dist/actions/actionLayout.js.map +1 -1
  21. package/dist/actions/actionMode.d.ts.map +1 -1
  22. package/dist/actions/actionMode.js +30 -10
  23. package/dist/actions/actionMode.js.map +1 -1
  24. package/dist/actions/actionSelection.d.ts.map +1 -1
  25. package/dist/actions/actionSelection.js +18 -6
  26. package/dist/actions/actionSelection.js.map +1 -1
  27. package/dist/actions/actionText.d.ts +5 -0
  28. package/dist/actions/actionText.d.ts.map +1 -0
  29. package/dist/actions/actionText.js +37 -0
  30. package/dist/actions/actionText.js.map +1 -0
  31. package/dist/actions/actionView.d.ts.map +1 -1
  32. package/dist/actions/actionView.js +4 -2
  33. package/dist/actions/actionView.js.map +1 -1
  34. package/dist/actions/actionZOrder.d.ts.map +1 -1
  35. package/dist/actions/actionZOrder.js +12 -4
  36. package/dist/actions/actionZOrder.js.map +1 -1
  37. package/dist/actions/actionZoom.d.ts.map +1 -1
  38. package/dist/actions/actionZoom.js +17 -17
  39. package/dist/actions/actionZoom.js.map +1 -1
  40. package/dist/actions/index.d.ts +3 -1
  41. package/dist/actions/index.d.ts.map +1 -1
  42. package/dist/actions/index.js +3 -1
  43. package/dist/actions/index.js.map +1 -1
  44. package/dist/actions/registry.d.ts.map +1 -1
  45. package/dist/actions/registry.js +5 -3
  46. package/dist/actions/registry.js.map +1 -1
  47. package/dist/actions/types.d.ts +1 -1
  48. package/dist/actions/types.d.ts.map +1 -1
  49. package/dist/actions/types.js.map +1 -1
  50. package/dist/auto-layout-scheduler.d.ts +1 -3
  51. package/dist/auto-layout-scheduler.d.ts.map +1 -1
  52. package/dist/auto-layout-scheduler.js +1 -2
  53. package/dist/auto-layout-scheduler.js.map +1 -1
  54. package/dist/built-in-handlers.d.ts.map +1 -1
  55. package/dist/built-in-handlers.js +30 -7
  56. package/dist/built-in-handlers.js.map +1 -1
  57. package/dist/clipboard.d.ts.map +1 -1
  58. package/dist/clipboard.js +6 -7
  59. package/dist/clipboard.js.map +1 -1
  60. package/dist/constants.d.ts +48 -23
  61. package/dist/constants.d.ts.map +1 -1
  62. package/dist/constants.js +48 -23
  63. package/dist/constants.js.map +1 -1
  64. package/dist/dom-events.d.ts +7 -0
  65. package/dist/dom-events.d.ts.map +1 -1
  66. package/dist/dom-events.js +12 -0
  67. package/dist/dom-events.js.map +1 -1
  68. package/dist/editor/animation-scene.d.ts +21 -0
  69. package/dist/editor/animation-scene.d.ts.map +1 -0
  70. package/dist/editor/animation-scene.js +71 -0
  71. package/dist/editor/animation-scene.js.map +1 -0
  72. package/dist/editor/animation.d.ts +43 -0
  73. package/dist/editor/animation.d.ts.map +1 -0
  74. package/dist/editor/animation.js +80 -0
  75. package/dist/editor/animation.js.map +1 -0
  76. package/dist/editor/applies/arrange.d.ts +46 -0
  77. package/dist/editor/applies/arrange.d.ts.map +1 -0
  78. package/dist/editor/applies/arrange.js +162 -0
  79. package/dist/editor/applies/arrange.js.map +1 -0
  80. package/dist/editor/applies/create.d.ts.map +1 -1
  81. package/dist/editor/applies/create.js +1 -1
  82. package/dist/editor/applies/create.js.map +1 -1
  83. package/dist/editor/applies/edge.d.ts +8 -1
  84. package/dist/editor/applies/edge.d.ts.map +1 -1
  85. package/dist/editor/applies/edge.js +31 -1
  86. package/dist/editor/applies/edge.js.map +1 -1
  87. package/dist/editor/applies/link-move.d.ts.map +1 -1
  88. package/dist/editor/applies/link-move.js +5 -2
  89. package/dist/editor/applies/link-move.js.map +1 -1
  90. package/dist/editor/applies/move.d.ts +6 -0
  91. package/dist/editor/applies/move.d.ts.map +1 -1
  92. package/dist/editor/applies/move.js +6 -0
  93. package/dist/editor/applies/move.js.map +1 -1
  94. package/dist/editor/applies/resize.d.ts +26 -5
  95. package/dist/editor/applies/resize.d.ts.map +1 -1
  96. package/dist/editor/applies/resize.js +92 -14
  97. package/dist/editor/applies/resize.js.map +1 -1
  98. package/dist/editor/applies/snap-grid.d.ts.map +1 -1
  99. package/dist/editor/applies/snap-grid.js.map +1 -1
  100. package/dist/editor/caret-blink.d.ts +18 -0
  101. package/dist/editor/caret-blink.d.ts.map +1 -0
  102. package/dist/editor/caret-blink.js +42 -0
  103. package/dist/editor/caret-blink.js.map +1 -0
  104. package/dist/editor/container-ops.d.ts.map +1 -1
  105. package/dist/editor/container-ops.js +2 -2
  106. package/dist/editor/container-ops.js.map +1 -1
  107. package/dist/editor/dirty-rect.d.ts +23 -0
  108. package/dist/editor/dirty-rect.d.ts.map +1 -0
  109. package/dist/editor/dirty-rect.js +115 -0
  110. package/dist/editor/dirty-rect.js.map +1 -0
  111. package/dist/editor/gif-playback.d.ts +45 -0
  112. package/dist/editor/gif-playback.d.ts.map +1 -0
  113. package/dist/editor/gif-playback.js +125 -0
  114. package/dist/editor/gif-playback.js.map +1 -0
  115. package/dist/editor/hit-test.d.ts +3 -3
  116. package/dist/editor/hit-test.d.ts.map +1 -1
  117. package/dist/editor/hit-test.js +40 -15
  118. package/dist/editor/hit-test.js.map +1 -1
  119. package/dist/editor/long-press.d.ts.map +1 -1
  120. package/dist/editor/long-press.js +2 -2
  121. package/dist/editor/long-press.js.map +1 -1
  122. package/dist/editor/pinch.d.ts.map +1 -1
  123. package/dist/editor/pinch.js +6 -7
  124. package/dist/editor/pinch.js.map +1 -1
  125. package/dist/editor/pointer-binding.d.ts.map +1 -1
  126. package/dist/editor/pointer-binding.js +42 -25
  127. package/dist/editor/pointer-binding.js.map +1 -1
  128. package/dist/editor/public/annotations.d.ts.map +1 -1
  129. package/dist/editor/public/annotations.js.map +1 -1
  130. package/dist/editor/public/arrange-group.d.ts +3 -9
  131. package/dist/editor/public/arrange-group.d.ts.map +1 -1
  132. package/dist/editor/public/arrange-group.js +4 -6
  133. package/dist/editor/public/arrange-group.js.map +1 -1
  134. package/dist/editor/public/brush.d.ts.map +1 -1
  135. package/dist/editor/public/brush.js +1 -3
  136. package/dist/editor/public/brush.js.map +1 -1
  137. package/dist/editor/public/clipboard.d.ts +2 -4
  138. package/dist/editor/public/clipboard.d.ts.map +1 -1
  139. package/dist/editor/public/clipboard.js +2 -4
  140. package/dist/editor/public/clipboard.js.map +1 -1
  141. package/dist/editor/public/cursor.d.ts +43 -0
  142. package/dist/editor/public/cursor.d.ts.map +1 -0
  143. package/dist/editor/public/cursor.js +135 -0
  144. package/dist/editor/public/cursor.js.map +1 -0
  145. package/dist/editor/public/frame-name.d.ts +20 -0
  146. package/dist/editor/public/frame-name.d.ts.map +1 -0
  147. package/dist/editor/public/frame-name.js +54 -0
  148. package/dist/editor/public/frame-name.js.map +1 -0
  149. package/dist/editor/public/image-insert.d.ts +1 -1
  150. package/dist/editor/public/image-insert.d.ts.map +1 -1
  151. package/dist/editor/public/layers.d.ts.map +1 -1
  152. package/dist/editor/public/layers.js +2 -2
  153. package/dist/editor/public/layers.js.map +1 -1
  154. package/dist/editor/public/link.d.ts +11 -2
  155. package/dist/editor/public/link.d.ts.map +1 -1
  156. package/dist/editor/public/link.js +71 -5
  157. package/dist/editor/public/link.js.map +1 -1
  158. package/dist/editor/public/placement.d.ts +60 -2
  159. package/dist/editor/public/placement.d.ts.map +1 -1
  160. package/dist/editor/public/placement.js +185 -2
  161. package/dist/editor/public/placement.js.map +1 -1
  162. package/dist/editor/public/selection-ops.d.ts +23 -1
  163. package/dist/editor/public/selection-ops.d.ts.map +1 -1
  164. package/dist/editor/public/selection-ops.js +81 -8
  165. package/dist/editor/public/selection-ops.js.map +1 -1
  166. package/dist/editor/public/text-edit.d.ts +1 -10
  167. package/dist/editor/public/text-edit.d.ts.map +1 -1
  168. package/dist/editor/public/text-edit.js +2 -16
  169. package/dist/editor/public/text-edit.js.map +1 -1
  170. package/dist/editor/public/z-order.d.ts.map +1 -1
  171. package/dist/editor/public/z-order.js +5 -5
  172. package/dist/editor/public/z-order.js.map +1 -1
  173. package/dist/editor/public/zoom-pan.d.ts +6 -4
  174. package/dist/editor/public/zoom-pan.d.ts.map +1 -1
  175. package/dist/editor/public/zoom-pan.js +8 -7
  176. package/dist/editor/public/zoom-pan.js.map +1 -1
  177. package/dist/editor/render-orchestrator.d.ts.map +1 -1
  178. package/dist/editor/render-orchestrator.js +5 -11
  179. package/dist/editor/render-orchestrator.js.map +1 -1
  180. package/dist/editor/resize-helpers.d.ts +21 -5
  181. package/dist/editor/resize-helpers.d.ts.map +1 -1
  182. package/dist/editor/resize-helpers.js +59 -19
  183. package/dist/editor/resize-helpers.js.map +1 -1
  184. package/dist/editor/viewport-helpers.d.ts.map +1 -1
  185. package/dist/editor/viewport-helpers.js +3 -3
  186. package/dist/editor/viewport-helpers.js.map +1 -1
  187. package/dist/editor-events.d.ts +1 -1
  188. package/dist/editor-events.d.ts.map +1 -1
  189. package/dist/editor.d.ts +125 -130
  190. package/dist/editor.d.ts.map +1 -1
  191. package/dist/editor.js +352 -985
  192. package/dist/editor.js.map +1 -1
  193. package/dist/file-drop.d.ts.map +1 -1
  194. package/dist/file-drop.js +7 -4
  195. package/dist/file-drop.js.map +1 -1
  196. package/dist/frame-helpers.d.ts.map +1 -1
  197. package/dist/frame-helpers.js +5 -5
  198. package/dist/frame-helpers.js.map +1 -1
  199. package/dist/group-helpers.d.ts.map +1 -1
  200. package/dist/group-helpers.js +4 -4
  201. package/dist/group-helpers.js.map +1 -1
  202. package/dist/handle.d.ts +61 -6
  203. package/dist/handle.d.ts.map +1 -1
  204. package/dist/handle.js +105 -7
  205. package/dist/handle.js.map +1 -1
  206. package/dist/hotkeys-binding.d.ts +26 -0
  207. package/dist/hotkeys-binding.d.ts.map +1 -0
  208. package/dist/hotkeys-binding.js +35 -0
  209. package/dist/hotkeys-binding.js.map +1 -0
  210. package/dist/index.d.ts +3 -2
  211. package/dist/index.d.ts.map +1 -1
  212. package/dist/index.js +3 -2
  213. package/dist/index.js.map +1 -1
  214. package/dist/link-selection.d.ts +3 -5
  215. package/dist/link-selection.d.ts.map +1 -1
  216. package/dist/link-selection.js +3 -12
  217. package/dist/link-selection.js.map +1 -1
  218. package/dist/machine.d.ts +27 -1
  219. package/dist/machine.d.ts.map +1 -1
  220. package/dist/machine.js +38 -0
  221. package/dist/machine.js.map +1 -1
  222. package/dist/overlay.d.ts +1 -4
  223. package/dist/overlay.d.ts.map +1 -1
  224. package/dist/overlay.js +142 -38
  225. package/dist/overlay.js.map +1 -1
  226. package/dist/selection.d.ts.map +1 -1
  227. package/dist/selection.js +2 -10
  228. package/dist/selection.js.map +1 -1
  229. package/dist/util.d.ts +8 -0
  230. package/dist/util.d.ts.map +1 -0
  231. package/dist/util.js +17 -0
  232. package/dist/util.js.map +1 -0
  233. package/package.json +9 -9
package/CHANGELOG.md CHANGED
@@ -1,5 +1,95 @@
1
1
  # @oh-just-another/state
2
2
 
3
+ ## 0.59.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b4b252b: Arrange operations for the selection. **Flip** mirrors the selection about its bounding-box centre — horizontal (`Shift+H`) and vertical (`Shift+V`); a single shape flips about its own centre. **Align** flushes two or more shapes to the left / right / top / bottom edge or the horizontal / vertical centre of their bounding box (`Alt+←/→/↑/↓` for the four edges; centres via the panel / menu). **Distribute** evenly spaces three or more shapes so the gaps between them are equal, on the horizontal (`Alt+H`) or vertical (`Alt+V`) axis, keeping the outermost shapes fixed. All three are available from the selection property panel and the right-click menu. New engine API: `Editor.flipSelection(axis)`, `Editor.alignSelection(edge)`, and `Editor.distributeSelection(axis)`.
8
+ - d20d50a: Copy and paste a shape's visual style. `Cmd/Ctrl+Alt+C` captures the fill / stroke / dash / opacity of the selected shape into an in-editor buffer; `Cmd/Ctrl+Alt+V` applies it to the current selection (one undo step). Also available from the right-click menu. New engine API: `Editor.copySelectionStyle()` / `Editor.pasteSelectionStyle()` and the `hasStyleClipboard` flag.
9
+ - 938e7c8: Increase / decrease the font size of the selected text with `Cmd/Ctrl+Shift+>` and `Cmd/Ctrl+Shift+<`. Each shape steps by a gentle ~10 % (at least 1 px) from its own size, so a mixed selection keeps its relative sizing, clamped to the usable range. New engine API: `Editor.adjustSelectionFontSize(direction)`.
10
+ - 9673846: Grid model rework. The viewport's `gridSize` (spacing that doubled as a hidden/
11
+ shown toggle) is replaced by an explicit `gridEnabled` boolean; spacing is fixed
12
+ at `DEFAULT_GRID_SPACING`. The runtime `gridVisible` flag is removed — grid
13
+ on/off now lives on the scene viewport and persists with it. Scene documents
14
+ migrate v1 → v2 automatically (`gridSize > 0` → `gridEnabled: true`). `<Editor>`
15
+ ships gridless by default; hosts enable the grid per scene.
16
+ - 8f00738: Images (static and animated GIF) now render on the OffscreenCanvas worker backend, matching the Canvas2D / WebGL2 backends. The offscreen command stream now carries `drawImage` as an `ImageBitmap`, and static images are loaded as `ImageBitmap` so they cross the worker boundary. `insertImage` now accepts an `ImageBitmap` handle in addition to `HTMLImageElement`.
17
+ - 3152317: The single-shape selection box now turns with the element: its outline, resize
18
+ handles and rotate grip are drawn on an oriented frame that hugs the rotated
19
+ body instead of its axis-aligned bounding box, and handle hit-testing inverse-
20
+ rotates the cursor into the frame so grabs stay precise. The rotate grip moved
21
+ from above the top edge to the bottom-left corner, just outside the shape.
22
+
23
+ Its placement is now defined per element type as an `AnchorRef` — the same
24
+ vocabulary that positions a shape's custom connection points — via the new
25
+ `registerRotateAnchor(type, anchor)` / `getRotateAnchor(type)` API (default:
26
+ the bottom-left corner). Groups and multi-selections keep their axis-aligned
27
+ box, with the grip likewise at the bottom-left corner.
28
+
29
+ New math helper `vec2.rotateAround(point, pivot, radians)`.
30
+
31
+ - fc47ecc: Resizing a rotated shape now works correctly. Dragging a handle on a rotated
32
+ element resizes it in the element's own (un-rotated) frame and keeps the corner
33
+ opposite the dragged handle fixed in world — the same "the other side stays put"
34
+ feel as for an unrotated shape. Aspect-lock (Shift) and resize-from-centre (Alt)
35
+ are honoured in the rotated frame too. Previously a rotated shape jumped because
36
+ the resize math assumed an axis-aligned box.
37
+ - 8fc6b69: Rotate shapes interactively. A rotate grip floats above the selection (single shape or group); dragging it turns the selection about its bounding-box centre, and holding **Shift** snaps the angle to 15° steps. The engine API `Editor.rotateSelection(angle)` drives the same maths programmatically. Element rotation was already modelled and rendered — this adds the handle, the gesture, and the hit-testing (the grip takes priority over the link-start anchors it overlaps).
38
+ - edde5d0: Add `bindEditorHotkeys(editor, options?)` — a reusable, framework-agnostic keyboard-shortcut binding driven by the action registry. Returns an unbind function, leaves text fields alone (except `Escape`), and reads `composedPath()[0]` so the editable-target check stays correct across a shadow-root boundary. Re-exported from `@oh-just-another/editor`.
39
+ - c5be6e5: Transform modifier keys during pointer gestures: hold **Alt** to resize symmetrically about the element's centre, **Shift** to lock the aspect ratio while resizing, and **Shift** to constrain a move to a single axis (Cmd/Ctrl already pulls a shape off the grid for one gesture). `<Editor>` mirrors the modifiers from keyboard events automatically; headless hosts can drive them via `Editor.setTransformModifiers({ alt, shift })`. Applies to single shapes, multi-selection / group resizes, and text.
40
+
41
+ ### Patch Changes
42
+
43
+ - 0152ed6: The canvas surface now takes keyboard focus on pointer-down. The press handler
44
+ calls `preventDefault()` (to suppress text selection / native scroll), which also
45
+ suppressed the browser's default focus-on-click — so clicking the canvas left it
46
+ unfocused and keyboard shortcuts (or a clean blur of a previously-focused panel
47
+ input) only worked after tabbing to it, reading as "the first click did nothing".
48
+ The handler now focuses the host explicitly, skipping the case where the press
49
+ lands on an in-canvas text field so editing keeps its own focus.
50
+ - f370dba: `normalizeHref` no longer backtracks polynomially on a crafted email-like input:
51
+ the bare-email check matches domain labels linearly. As a side effect it is
52
+ stricter about what counts as an email — a domain with empty labels (consecutive
53
+ dots, e.g. `a@b..c`) is treated as a URL and gets `https://`, not `mailto:`.
54
+ - da91d59: Polish the rotate grip: it now renders as a clockwise circular-arrow glyph (a
55
+ `rotate-cw` icon) instead of a plain circle, and the connector line back to the
56
+ shape is gone. Hovering the grip shows a `grab` cursor; the cursor switches to
57
+ `grabbing` while a rotate gesture is in flight (overridable via the new
58
+ `rotate` cursor role).
59
+ - 1c7cc6c: Fix inline text editing on a scaled text element: the caret and selection highlight now apply the element's `scale`, so they line up with the rendered text instead of trailing behind it. Clicking to place the caret divides the point back through `scale` to hit the right glyph.
60
+ - Updated dependencies [9673846]
61
+ - Updated dependencies [ff90a95]
62
+ - Updated dependencies [3152317]
63
+ - Updated dependencies [f98730f]
64
+ - Updated dependencies [904cc09]
65
+ - @oh-just-another/scene@0.59.0
66
+ - @oh-just-another/renderer-core@0.58.0
67
+ - @oh-just-another/math@0.58.0
68
+ - @oh-just-another/history@0.57.2
69
+
70
+ ## 0.58.0
71
+
72
+ ### Minor Changes
73
+
74
+ - d1b96d9: Couple snap-to-grid to grid visibility, and turn the grid on by default.
75
+
76
+ Snapping is now active only while a grid is actually displayed — the toggle is
77
+ on (`gridVisible`) AND the scene has a positive `gridSize`, the same condition
78
+ `renderGrid` paints under. Snapping to an invisible grid is gone: no grid → no
79
+ snap, always.
80
+
81
+ `DEFAULT_VIEWPORT` now ships `gridSize: DEFAULT_GRID_SPACING` (tune it in scene
82
+ `constants.ts`), so a fresh scene has a visible grid and snapping on. Pass a
83
+ scene with `gridSize: 0` (or omit it on a custom viewport) for a gridless,
84
+ snap-free canvas.
85
+
86
+ ### Patch Changes
87
+
88
+ - Updated dependencies [d1b96d9]
89
+ - @oh-just-another/scene@0.58.0
90
+ - @oh-just-another/history@0.57.1
91
+ - @oh-just-another/renderer-core@0.57.1
92
+
3
93
  ## 0.57.0
4
94
 
5
95
  ### Minor Changes
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # @oh-just-another/state
2
2
 
3
- Level 2 interaction layer. Owns the xstate interaction machine, normalizes DOM events, draws selection/handles on the overlay layer, and ships a high-level `Editor` class that wires a scene + main/overlay render targets + DOM events together.
3
+ Interactive editor engine: tools, modes, selection, history, and hit-testing over a Scene.
4
4
 
5
- Browser-only relies on the DOM `PointerEvent` API. Depends on `@types`, `@math`, `@scene`, `@renderer-core`, and `xstate` (v5).
5
+ 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.
6
6
 
7
7
  ## Quick start
8
8
 
9
9
  ```ts
10
10
  import { LayeredCanvas, installBuiltinRenderers } from "@oh-just-another/renderer-canvas";
11
- import { Editor, type Mode } from "@oh-just-another/state";
11
+ import { Editor } from "@oh-just-another/state";
12
12
  import { emptyScene } from "@oh-just-another/scene";
13
13
 
14
14
  installBuiltinRenderers();
@@ -24,37 +24,86 @@ const editor = new Editor({
24
24
  initialMode: "select",
25
25
  });
26
26
 
27
- document.querySelector("#rect-button")!.addEventListener("click", () => {
28
- editor.setMode("draw-rect");
29
- });
27
+ editor.subscribe(() => render());
28
+ editor.setMode("draw-rect");
30
29
  ```
31
30
 
32
- ## Architecture
31
+ ## Concepts
32
+
33
+ ### `Editor`
34
+
35
+ The centerpiece controller (`Editor`, `EditorOptions`, `LoadSceneOptions`). It owns the scene, selection, and viewport, and exposes a high-level API spanning:
36
+
37
+ - **Tools / modes** — `mode`, `setMode`, `toolLocked` / `setToolLocked` (sticky tool).
38
+ - **Selection** — `selection`, `selectAll`, `deleteSelected`, `moveSelectionBy`, `focusCycle`, plus link selection (`selectedLinks`, `selectedLink`).
39
+ - **History** — `undo`, `redo`, `canUndo`, `canRedo`, `history` (backed by `@oh-just-another/history`).
40
+ - **Hit-testing** — `hitTest(worldPoint)` returns a `PressTarget`; `hitAnnotation`.
41
+ - **Viewport / camera** — `panBy`, `zoomIn`, `zoomOut`, `zoomAt`, `zoomToFit`, `zoomToSelection`, `setViewportSize`, `screenToWorld`; grid via `gridEnabled`, `setGridVisible`, `toggleGrid`.
42
+ - **Z-order** — `bringToFront`, `sendToBack`.
43
+ - **Grouping / arrange** — `groupSelected`, `ungroup`, `expandSelectionWithDescendants`, `arrangeAsGrid`, `arrangeAsStack`.
44
+ - **Containers / frames** — frame membership reconciliation; `beginFrameNameEdit`, `commitFrameNameEdit`, `cancelFrameNameEdit`.
45
+ - **Link / edge editing** — link routing, endpoint anchors, `linkPreviewPath`, `linkAttachTarget`, `beginLinkCaptionEdit`, `commitLinkCaptionEdit`, `cancelLinkCaptionEdit`, `linkLabelWorld`.
46
+ - **Text editing** — inline caret/selection model: `beginTextEdit`, `commitTextEdit`, `cancelTextEdit`, `setEditingText`, `setTextCaretFromPoint`, `extendTextSelectionToPoint`, `caretIndexAtWorldPoint`, `editingTextOverlay`, plus `createTextAt`.
47
+ - **Image / file insert** — `insertImage`, `addElement`, `beginPlacement`, and a `FileDropRegistry` (see below).
48
+ - **GIF / animation playback** — `togglePlayback`, `hoverAnimatedElement`, `isPlaybackPaused`.
49
+ - **Brush strokes** — `beginBrushStroke`, `extendBrushStroke`, `commitBrushStroke`, `cancelBrushStroke`, `pendingBrushStroke`.
50
+ - **Annotations / comments** — `addAnnotation`, `removeAnnotation`, `toggleAnnotationResolved`, `addComment`, `removeComment`, `setSelectedAnnotation`, `setCommentAuthor`.
51
+ - **Scene lifecycle** — `scene`, `loadScene`.
52
+
53
+ Subscribe via `editor.subscribe(fn)` (coarse-grained) or the typed `EditorEvents` surface (`change`, `mode`, `selection`, `scene`, `history`, `viewport`).
54
+
55
+ ### Modes
56
+
57
+ `Mode` and `DEFAULT_MODE` (`select`). The active mode dictates how a pointer-down is interpreted:
58
+
59
+ `select` · `hand` (pan) · `draw-rect` · `draw-ellipse` · `draw-text` · `draw-edge` · `draw-frame` · `brush`.
60
+
61
+ Pan and zoom remain available as gestures (middle-mouse / Space+drag / wheel) regardless of mode.
62
+
63
+ ### Actions
64
+
65
+ A command registry for editor operations and their hotkeys:
66
+
67
+ - `ActionRegistry`, `defaultActionRegistry`, `registerBuiltinActions`, and types `Action`, `ActionCategory`, `ActionContext`, `HotkeyMatcher`.
68
+ - Predicates `hasSelection`, `hasSelectionOrLink`.
69
+ - Built-ins are exported both as bundles and individually so hosts can replace or compose them: history (`actionUndo`, `actionRedo`, `historyActions`), selection (`actionSelectAll`, `actionDeleteSelection`, `actionDuplicateSelection`, `selectionActions`), clipboard (`actionCopy`, `actionCut`, `actionPaste`, `clipboardActions`), z-order (`actionBringToFront`, `actionSendToBack`, `zOrderActions`), grouping (`actionGroupSelection`, `actionUngroupSelection`, `groupingActions`), zoom (`actionZoomIn`, `actionZoomOut`, `actionZoomReset`, `actionZoomToFit`, `zoomActions`), and modes (`actionModeSelect` … `actionModeFrame`, `actionToggleToolLock`, `actionCancel`, `modeActions`).
70
+
71
+ ### Interaction machine
72
+
73
+ A pure xstate machine (`interactionMachine`) tracks gesture state and emits intent: `InteractionContext`, `InteractionEvent`, `InteractionEmit`, `PressTarget`, the pointer/mode event types, `interpretPressEnd`, `boundsFromPoints`, and `DRAG_THRESHOLD`. It never mutates a scene — emit events describe what the host should apply.
74
+
75
+ ### Selection & handles
76
+
77
+ - `Selection` type plus the `selection.*` namespace of immutable-set helpers.
78
+ - Resize handles: `HandleId`, `ALL_HANDLES`, `HANDLE_SIZE`, `handlePosition`, `hitHandle`, `resizeBounds` (8 zoom-aware corner/edge handles).
79
+
80
+ ### Overlay & peers
81
+
82
+ `renderOverlay`, `OverlayStyle`, `DEFAULT_OVERLAY_STYLE` draw selection outlines, handles, and drawing previews. Collaborative cursors/selections via `PeerCursor`, `PeerSelection`, `Editor.setPeerCursors` / `setPeerSelections`, and `PEER_CURSOR_BROADCAST_INTERVAL_MS`.
83
+
84
+ ### Interactive hit-test registry
85
+
86
+ `registerInteractiveHitTester` / `getInteractiveHitTester` (`InteractiveHitTester`) — an extension point for hit-testing interactive element kinds without the engine knowing their internals.
87
+
88
+ ### File-drop registry
89
+
90
+ `FileDropRegistry` with `FileDropHandler`, `FileDropContext`, `WalkOptions`, plus helpers: `IMAGE_MIME_TYPES`, `VIDEO_MIME_TYPES`, `isImageFile`, `isVideoFile`, `isSceneJsonFile`, `readFileAsDataURL`, `readFileAsText`, `walkDataTransfer`. Hosts register handlers for images, scene JSON, or custom payloads.
91
+
92
+ ### DOM event normalizers
33
93
 
34
- The interaction layer is divided into:
94
+ `fromPointerEvent`, `fromKeyboardEvent`, `fromWheelEvent`, `isEditableTarget` translate raw DOM events into host-side CSS-pixel domain events. Optional: the engine accepts the normalized shapes regardless of source.
35
95
 
36
- - **A pure xstate machine** (`interactionMachine`) that manages the _gesture_ state — what is the user doing right now? It has 5 states: `idle`, `pressing`, `draggingShape`, `draggingHandle`, `drawing`. It never modifies a scene.
37
- - **Emit events** (`InteractionEmit`) describe what the host should do: `SELECT_REPLACE`, `SELECT_CLEAR`, `MOVE_SHAPE`, `RESIZE_SHAPE`, `CREATE_SHAPE`.
38
- - **`Editor`** acts as the glue: it manages the scene + selection, listens for DOM pointer events, performs hit-tests, sends machine events, applies emit events to the scene using `@scene` operations, and re-renders.
96
+ ### Platform detection
39
97
 
40
- This division keeps the state machine completely pure and easy to test, and allows advanced hosts to replace `Editor` if they need more control.
98
+ `isMac`, `isWindows`, `isAndroid`, `isIOS`, `isLinux`, `isFirefox`, `isSafari`, `CTRL_OR_CMD_KEY`, `getDevicePixelRatio`, and the hotkey pretty-printers `formatHotkey`, `formatHotkeyParts` (`PrettyHotkeyDesc`).
41
99
 
42
- ## API
100
+ ### Annotation re-exports
43
101
 
44
- | Name | Purpose |
45
- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
46
- | `Editor` / `EditorOptions` | Top-level controller. Mounts on a host element with two render targets and an initial scene. |
47
- | `Mode` (`select` / `draw-rect` / `draw-ellipse`), `DEFAULT_MODE` | Editor modes. |
48
- | `Selection`, `selection.*` | Immutable `ReadonlySet<ElementId>` + helpers (`single`, `add`, `remove`, `toggle`, `equals`). |
49
- | `HandleId`, `handlePosition`, `hitHandle`, `resizeBounds`, `HANDLE_SIZE` | Resize-handle geometry (8 corner/edge handles, zoom-aware hit-test). |
50
- | `interactionMachine`, `InteractionContext`, `InteractionEvent`, `InteractionEmit`, `PressTarget`, `DRAG_THRESHOLD`, `interpretPressEnd` | Pure xstate machine + types + helpers. |
51
- | `renderOverlay(scene, selection, target, options?)`, `OverlayStyle` | Draws selection outlines, handles, and drawing previews on the overlay layer. |
52
- | `fromPointerEvent` / `fromKeyboardEvent` / `fromWheelEvent` | DOM → domain event normalizers (CSS-pixel coords relative to host element). |
102
+ `Annotation` and `Comment` types are re-exported from `@oh-just-another/scene` so hosts wiring `addAnnotation` / `addComment` don't need a direct scene dependency for the data shapes. `normalizeHref` / `safeHref` sanitize link hrefs.
53
103
 
54
104
  ## Design notes
55
105
 
56
- - **The machine owns gesture state, not scene state.** Selection and elements live in `Editor` (or your own equivalent). Emit events describe intent; the host applies it. This makes the machine snapshot-testable and replayable.
57
- - **Clicks vs drags via threshold.** A press becomes a drag once the pointer travels `DRAG_THRESHOLD` (4 px) from the press origin. Below the threshold, `POINTER_UP` yields a click effect derived via `interpretPressEnd` (SELECT_REPLACE / SELECT_CLEAR).
58
- - **Hit-test order:** handles of currently-selected elements win over element body hits. This matches how every editor feels once an element is selected, clicking its handle resizes rather than re-selects.
59
- - **Handles are screen-sized.** `hitHandle` divides the tolerance by viewport zoom so handles stay 8 × 8 CSS px regardless of zoom level.
60
- - **DOM listeners are pointer-events** (`pointerdown` / `move` / `up` / `cancel`) with `setPointerCapture`. Touch and mouse share a single path; no separate touch handlers.
106
+ - **The machine owns gesture state, not scene state.** Selection and elements live in `Editor`; emit events describe intent and the host applies it, keeping the machine snapshot-testable.
107
+ - **Clicks vs drags via threshold.** A press becomes a drag once the pointer travels `DRAG_THRESHOLD` from the press origin; below it, the press resolves to a click effect via `interpretPressEnd`.
108
+ - **Handles are screen-sized.** `hitHandle` divides tolerance by viewport zoom so handles stay a fixed CSS size at any zoom.
109
+ - **Framework-agnostic.** No React, and no direct DOM access in the engine hosts feed normalized events and own mounting. See [ohjustanother.site](https://ohjustanother.site).