@yuzhouu/canvas-kit 0.1.2

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 (263) hide show
  1. package/README.md +145 -0
  2. package/_vendor/canvas-core/bindings/BindingUtil.ts +92 -0
  3. package/_vendor/canvas-core/bindings/BindingUtilRegistry.ts +22 -0
  4. package/_vendor/canvas-core/bindings/arrow/arrowBindingHelpers.ts +52 -0
  5. package/_vendor/canvas-core/contextMenu.ts +85 -0
  6. package/_vendor/canvas-core/editor/Editor.ts +2614 -0
  7. package/_vendor/canvas-core/editor/HistoryManager.ts +83 -0
  8. package/_vendor/canvas-core/editor/StateNode.ts +162 -0
  9. package/_vendor/canvas-core/editor/camera.ts +6 -0
  10. package/_vendor/canvas-core/editor/clipboard.ts +478 -0
  11. package/_vendor/canvas-core/editor/commands.ts +833 -0
  12. package/_vendor/canvas-core/editor/edgeScroll.ts +37 -0
  13. package/_vendor/canvas-core/editor/editorComputedHelpers.ts +257 -0
  14. package/_vendor/canvas-core/editor/editorSelectionTransforms.ts +116 -0
  15. package/_vendor/canvas-core/editor/events.ts +93 -0
  16. package/_vendor/canvas-core/editor/index.ts +12 -0
  17. package/_vendor/canvas-core/editor/interactions.ts +101 -0
  18. package/_vendor/canvas-core/editor/internal.ts +22 -0
  19. package/_vendor/canvas-core/editor/managers/BindingManager.ts +262 -0
  20. package/_vendor/canvas-core/editor/managers/CameraManager.ts +147 -0
  21. package/_vendor/canvas-core/editor/managers/ClipboardManager.ts +32 -0
  22. package/_vendor/canvas-core/editor/managers/DocumentSnapshotManager.ts +256 -0
  23. package/_vendor/canvas-core/editor/managers/InputsManager.ts +319 -0
  24. package/_vendor/canvas-core/editor/managers/InteractionTransientManager.ts +218 -0
  25. package/_vendor/canvas-core/editor/managers/QueryManager.ts +837 -0
  26. package/_vendor/canvas-core/editor/managers/RenderWindowManager.ts +347 -0
  27. package/_vendor/canvas-core/editor/managers/SelectionLayoutManager.ts +196 -0
  28. package/_vendor/canvas-core/editor/managers/SelectionManager.ts +98 -0
  29. package/_vendor/canvas-core/editor/managers/SelectionOverlayManager.ts +308 -0
  30. package/_vendor/canvas-core/editor/managers/ShapeEditingManager.ts +226 -0
  31. package/_vendor/canvas-core/editor/managers/ShapeGeometryManager.ts +162 -0
  32. package/_vendor/canvas-core/editor/managers/ShapeHierarchyManager.ts +797 -0
  33. package/_vendor/canvas-core/editor/managers/SideEffectManager.ts +83 -0
  34. package/_vendor/canvas-core/editor/managers/SnapManager.ts +433 -0
  35. package/_vendor/canvas-core/editor/managers/SnapSnapshotBuilder.ts +430 -0
  36. package/_vendor/canvas-core/editor/managers/SpatialIndexManager.ts +354 -0
  37. package/_vendor/canvas-core/editor/managers/ToolManager.ts +81 -0
  38. package/_vendor/canvas-core/editor/managers/queryState.ts +583 -0
  39. package/_vendor/canvas-core/editor/managers/selectionGeometry.ts +287 -0
  40. package/_vendor/canvas-core/editor/managers/shapeOrder.ts +350 -0
  41. package/_vendor/canvas-core/editor/managers/shapeReparenting.ts +160 -0
  42. package/_vendor/canvas-core/editor/managers/snapCalculations.ts +412 -0
  43. package/_vendor/canvas-core/editor/managers/snapGeometry.ts +202 -0
  44. package/_vendor/canvas-core/editor/managers/snapIndicators.ts +403 -0
  45. package/_vendor/canvas-core/editor/managers/snapTypes.ts +122 -0
  46. package/_vendor/canvas-core/editor/queries/shapeTransforms.ts +171 -0
  47. package/_vendor/canvas-core/editor/renderWindow.ts +439 -0
  48. package/_vendor/canvas-core/editor/resizeGeometry.ts +654 -0
  49. package/_vendor/canvas-core/editor/selection.ts +489 -0
  50. package/_vendor/canvas-core/editor/selectionHandleInteractions.ts +43 -0
  51. package/_vendor/canvas-core/editor/selectionOverlay.ts +567 -0
  52. package/_vendor/canvas-core/exports/integrity.ts +174 -0
  53. package/_vendor/canvas-core/exports/svg.ts +494 -0
  54. package/_vendor/canvas-core/geometry/Box.ts +80 -0
  55. package/_vendor/canvas-core/geometry/Ellipse2d.ts +67 -0
  56. package/_vendor/canvas-core/geometry/Geometry2d.ts +52 -0
  57. package/_vendor/canvas-core/geometry/Mat.ts +92 -0
  58. package/_vendor/canvas-core/geometry/Polyline2d.ts +63 -0
  59. package/_vendor/canvas-core/geometry/Rectangle2d.ts +64 -0
  60. package/_vendor/canvas-core/geometry/Vec.ts +123 -0
  61. package/_vendor/canvas-core/geometry/index.ts +13 -0
  62. package/_vendor/canvas-core/geometry/segments.ts +65 -0
  63. package/_vendor/canvas-core/index.ts +391 -0
  64. package/_vendor/canvas-core/plugins/index.ts +5 -0
  65. package/_vendor/canvas-core/plugins/types.ts +53 -0
  66. package/_vendor/canvas-core/reactivity.ts +16 -0
  67. package/_vendor/canvas-core/schema/records.ts +221 -0
  68. package/_vendor/canvas-core/schema/snapshots.ts +527 -0
  69. package/_vendor/canvas-core/shapes/ShapeUtil.ts +516 -0
  70. package/_vendor/canvas-core/shapes/ShapeUtilRegistry.ts +26 -0
  71. package/_vendor/canvas-core/shapes/capabilities.ts +67 -0
  72. package/_vendor/canvas-core/shapes/types.ts +20 -0
  73. package/_vendor/canvas-core/toolbar.ts +148 -0
  74. package/_vendor/canvas-core/tools/hand/HandTool.ts +75 -0
  75. package/_vendor/canvas-core/tools/index.ts +2 -0
  76. package/_vendor/canvas-core/tools/select/SelectTool.ts +33 -0
  77. package/_vendor/canvas-core/tools/select/selectContainment.ts +104 -0
  78. package/_vendor/canvas-core/tools/select/selectToolHelpers.ts +801 -0
  79. package/_vendor/canvas-core/tools/select/states/BrushingState.ts +145 -0
  80. package/_vendor/canvas-core/tools/select/states/DraggingSelectionHandleInteractionState.ts +116 -0
  81. package/_vendor/canvas-core/tools/select/states/IdleState.ts +159 -0
  82. package/_vendor/canvas-core/tools/select/states/PointingCanvasState.ts +41 -0
  83. package/_vendor/canvas-core/tools/select/states/PointingResizeHandleState.ts +78 -0
  84. package/_vendor/canvas-core/tools/select/states/PointingRotateHandleState.ts +64 -0
  85. package/_vendor/canvas-core/tools/select/states/PointingSelectionHandleInteractionState.ts +34 -0
  86. package/_vendor/canvas-core/tools/select/states/PointingShapeState.ts +140 -0
  87. package/_vendor/canvas-core/tools/select/states/ResizingState.ts +171 -0
  88. package/_vendor/canvas-core/tools/select/states/RotatingState.ts +124 -0
  89. package/_vendor/canvas-core/tools/select/states/SelectStateNode.ts +128 -0
  90. package/_vendor/canvas-core/tools/select/states/TranslatingState.ts +139 -0
  91. package/_vendor/canvas-core/tools/select/states/index.ts +11 -0
  92. package/_vendor/canvas-plugin-api/feature.ts +204 -0
  93. package/_vendor/canvas-plugin-api/index.ts +142 -0
  94. package/_vendor/canvas-product-react/api/apiRegistry.ts +35 -0
  95. package/_vendor/canvas-product-react/api/canvasProductApi.ts +422 -0
  96. package/_vendor/canvas-product-react/api/index.ts +31 -0
  97. package/_vendor/canvas-product-react/capabilities/clipboard/clipboardService.ts +36 -0
  98. package/_vendor/canvas-product-react/capabilities/clipboard/systemClipboard.ts +245 -0
  99. package/_vendor/canvas-product-react/capabilities/context-menu/systemContextMenu.ts +396 -0
  100. package/_vendor/canvas-product-react/capabilities/context-menu/useContextMenuSurface.ts +59 -0
  101. package/_vendor/canvas-product-react/capabilities/export/useSvgExport.ts +156 -0
  102. package/_vendor/canvas-product-react/capabilities/history/useHistoryControls.ts +23 -0
  103. package/_vendor/canvas-product-react/capabilities/pages/usePageControls.ts +41 -0
  104. package/_vendor/canvas-product-react/capabilities/toolbar/systemToolbar.ts +490 -0
  105. package/_vendor/canvas-product-react/capabilities/toolbar/useToolbarSurfaces.ts +92 -0
  106. package/_vendor/canvas-product-react/capabilities/zoom/useZoomControls.ts +27 -0
  107. package/_vendor/canvas-product-react/index.ts +84 -0
  108. package/_vendor/canvas-product-react/persistence/index.ts +18 -0
  109. package/_vendor/canvas-product-react/persistence/localSnapshotPersistence.ts +294 -0
  110. package/_vendor/canvas-product-react/runtime/CanvasProductRoot.tsx +250 -0
  111. package/_vendor/canvas-product-react/runtime/productDefinition.ts +72 -0
  112. package/_vendor/canvas-product-react/runtime/productServices.ts +96 -0
  113. package/_vendor/canvas-react/components/CanvasView.tsx +796 -0
  114. package/_vendor/canvas-react/components/OverlayLayer.tsx +879 -0
  115. package/_vendor/canvas-react/components/PageSpace.tsx +34 -0
  116. package/_vendor/canvas-react/components/ShapeLayer.tsx +428 -0
  117. package/_vendor/canvas-react/components/index.ts +4 -0
  118. package/_vendor/canvas-react/hooks/useEditor.ts +11 -0
  119. package/_vendor/canvas-react/hooks/useValue.ts +14 -0
  120. package/_vendor/canvas-react/host/CanvasRendererRegistryContext.tsx +14 -0
  121. package/_vendor/canvas-react/host/contextMenuRegistry.ts +61 -0
  122. package/_vendor/canvas-react/host/domEventGuards.ts +61 -0
  123. package/_vendor/canvas-react/host/index.ts +38 -0
  124. package/_vendor/canvas-react/host/pluginHost.ts +635 -0
  125. package/_vendor/canvas-react/host/rendererRegistry.tsx +58 -0
  126. package/_vendor/canvas-react/host/toolbarRegistry.ts +100 -0
  127. package/_vendor/canvas-react/index.ts +3 -0
  128. package/_vendor/plugin-arrow/ArrowBindingUtil.ts +239 -0
  129. package/_vendor/plugin-arrow/ArrowShapeUtil.ts +311 -0
  130. package/_vendor/plugin-arrow/ArrowTool.ts +208 -0
  131. package/_vendor/plugin-arrow/arrowGeometry.ts +441 -0
  132. package/_vendor/plugin-arrow/arrowTerminalBindingResolver.ts +253 -0
  133. package/_vendor/plugin-arrow/arrowTerminalCommands.ts +230 -0
  134. package/_vendor/plugin-arrow/canvas.tsx +92 -0
  135. package/_vendor/plugin-arrow/core.ts +409 -0
  136. package/_vendor/plugin-arrow/feature.ts +20 -0
  137. package/_vendor/plugin-arrow/index.ts +28 -0
  138. package/_vendor/plugin-arrow/selectionHandleInteractions.ts +154 -0
  139. package/_vendor/plugin-arrow/types.ts +63 -0
  140. package/_vendor/plugin-draw/DrawShapeRenderer.tsx +60 -0
  141. package/_vendor/plugin-draw/DrawShapeUtil.ts +372 -0
  142. package/_vendor/plugin-draw/DrawTool.ts +444 -0
  143. package/_vendor/plugin-draw/canvas.tsx +24 -0
  144. package/_vendor/plugin-draw/core.ts +515 -0
  145. package/_vendor/plugin-draw/drawGeometry.ts +491 -0
  146. package/_vendor/plugin-draw/drawHitTest.ts +271 -0
  147. package/_vendor/plugin-draw/drawInterpolation.ts +250 -0
  148. package/_vendor/plugin-draw/drawPath.ts +550 -0
  149. package/_vendor/plugin-draw/drawSegments.ts +106 -0
  150. package/_vendor/plugin-draw/drawSelectionTransform.ts +67 -0
  151. package/_vendor/plugin-draw/drawShapeGeometry.ts +307 -0
  152. package/_vendor/plugin-draw/drawStrokeGeometry.ts +454 -0
  153. package/_vendor/plugin-draw/feature.ts +20 -0
  154. package/_vendor/plugin-draw/index.ts +31 -0
  155. package/_vendor/plugin-draw/types.ts +36 -0
  156. package/_vendor/plugin-eraser/EraserTool.ts +297 -0
  157. package/_vendor/plugin-eraser/canvas.tsx +616 -0
  158. package/_vendor/plugin-eraser/core.ts +49 -0
  159. package/_vendor/plugin-eraser/feature.ts +20 -0
  160. package/_vendor/plugin-eraser/index.ts +4 -0
  161. package/_vendor/plugin-frame/FrameShapeUtil.ts +173 -0
  162. package/_vendor/plugin-frame/FrameTool.ts +225 -0
  163. package/_vendor/plugin-frame/canvas.tsx +75 -0
  164. package/_vendor/plugin-frame/core.ts +534 -0
  165. package/_vendor/plugin-frame/feature.ts +20 -0
  166. package/_vendor/plugin-frame/index.ts +6 -0
  167. package/_vendor/plugin-geo/GeoShapeUtil.ts +463 -0
  168. package/_vendor/plugin-geo/GeoTools.ts +247 -0
  169. package/_vendor/plugin-geo/canvas.tsx +90 -0
  170. package/_vendor/plugin-geo/core.ts +495 -0
  171. package/_vendor/plugin-geo/feature.ts +20 -0
  172. package/_vendor/plugin-geo/index.ts +7 -0
  173. package/_vendor/plugin-geo/types.ts +27 -0
  174. package/_vendor/plugin-group/GroupShapeUtil.ts +59 -0
  175. package/_vendor/plugin-group/canvas.tsx +59 -0
  176. package/_vendor/plugin-group/core.ts +40 -0
  177. package/_vendor/plugin-group/feature.ts +20 -0
  178. package/_vendor/plugin-group/index.ts +5 -0
  179. package/_vendor/plugin-image/ImageShapeUtil.ts +95 -0
  180. package/_vendor/plugin-image/canvas.tsx +212 -0
  181. package/_vendor/plugin-image/core.ts +542 -0
  182. package/_vendor/plugin-image/crop.ts +770 -0
  183. package/_vendor/plugin-image/cropInteractions.ts +350 -0
  184. package/_vendor/plugin-image/cropSession.ts +38 -0
  185. package/_vendor/plugin-image/feature.ts +20 -0
  186. package/_vendor/plugin-image/index.ts +29 -0
  187. package/_vendor/plugin-image/types.ts +55 -0
  188. package/_vendor/plugin-image-product-react/ImageImportController.tsx +143 -0
  189. package/_vendor/plugin-image-product-react/imageAssetService.ts +52 -0
  190. package/_vendor/plugin-image-product-react/imageBlobStore.ts +315 -0
  191. package/_vendor/plugin-image-product-react/imageHostActions.ts +438 -0
  192. package/_vendor/plugin-image-product-react/imageProductApi.ts +88 -0
  193. package/_vendor/plugin-image-product-react/imageToolbarHostBridge.ts +48 -0
  194. package/_vendor/plugin-image-product-react/imageToolbarRuntime.ts +21 -0
  195. package/_vendor/plugin-image-product-react/index.ts +60 -0
  196. package/_vendor/plugin-image-product-react/snapshotGuards.ts +33 -0
  197. package/_vendor/plugin-image-product-react/svgImageAssetInliner.ts +54 -0
  198. package/_vendor/plugin-image-product-react/useImageImport.ts +64 -0
  199. package/_vendor/plugin-laser/LaserTool.ts +154 -0
  200. package/_vendor/plugin-laser/canvas.tsx +645 -0
  201. package/_vendor/plugin-laser/core.ts +49 -0
  202. package/_vendor/plugin-laser/feature.ts +20 -0
  203. package/_vendor/plugin-laser/index.ts +4 -0
  204. package/_vendor/plugin-text/ArrowLabelBindingUtil.ts +336 -0
  205. package/_vendor/plugin-text/GeoLabelBindingUtil.ts +391 -0
  206. package/_vendor/plugin-text/TextShapeSvgExporter.ts +101 -0
  207. package/_vendor/plugin-text/TextShapeUtil.ts +284 -0
  208. package/_vendor/plugin-text/TextTool.ts +228 -0
  209. package/_vendor/plugin-text/canvas.tsx +115 -0
  210. package/_vendor/plugin-text/core.ts +422 -0
  211. package/_vendor/plugin-text/feature.ts +20 -0
  212. package/_vendor/plugin-text/host/RichTextEditingBox.tsx +144 -0
  213. package/_vendor/plugin-text/host/RichTextToolbar.tsx +266 -0
  214. package/_vendor/plugin-text/host/TiptapTextEditor.tsx +191 -0
  215. package/_vendor/plugin-text/host/richTextRendering.tsx +49 -0
  216. package/_vendor/plugin-text/host/textDomSelection.ts +93 -0
  217. package/_vendor/plugin-text/host/textEditingBoxStyle.ts +36 -0
  218. package/_vendor/plugin-text/host/textEntryFocus.ts +307 -0
  219. package/_vendor/plugin-text/host/textFontLoader.ts +149 -0
  220. package/_vendor/plugin-text/host/textMeasurement.ts +258 -0
  221. package/_vendor/plugin-text/host/textMeasurementSync.ts +77 -0
  222. package/_vendor/plugin-text/host/textRangeSelection.ts +134 -0
  223. package/_vendor/plugin-text/host/textShapeRendererRuntime.tsx +89 -0
  224. package/_vendor/plugin-text/host/textShapeSync.ts +173 -0
  225. package/_vendor/plugin-text/host/tiptapTextEditorController.ts +148 -0
  226. package/_vendor/plugin-text/index.ts +18 -0
  227. package/_vendor/plugin-text/richTextUtils.ts +259 -0
  228. package/_vendor/plugin-text/textInteractions.ts +366 -0
  229. package/_vendor/plugin-text/textLabelHelpers.ts +235 -0
  230. package/_vendor/plugin-text/textShapeHelpers.ts +123 -0
  231. package/_vendor/plugin-text/textStyle.ts +96 -0
  232. package/_vendor/plugin-text/textTypes.ts +48 -0
  233. package/_vendor/signals/atom.ts +74 -0
  234. package/_vendor/signals/computed.ts +132 -0
  235. package/_vendor/signals/core.ts +130 -0
  236. package/_vendor/signals/effect.ts +117 -0
  237. package/_vendor/signals/index.ts +9 -0
  238. package/_vendor/signals/types.ts +17 -0
  239. package/_vendor/store/Store.ts +171 -0
  240. package/_vendor/store/diff.ts +229 -0
  241. package/_vendor/store/history.ts +177 -0
  242. package/_vendor/store/index.ts +32 -0
  243. package/_vendor/store/types.ts +37 -0
  244. package/api/canvasKitApi.d.ts +29 -0
  245. package/index.css +3 -0
  246. package/index.d.ts +16 -0
  247. package/index.mjs +1 -0
  248. package/internal-modules.d.ts +187 -0
  249. package/package.json +48 -0
  250. package/product/Shell.d.ts +19 -0
  251. package/product/chromeContext.d.ts +70 -0
  252. package/product/createCanvasKitEditor.d.ts +6 -0
  253. package/product/index.d.ts +7 -0
  254. package/product/persistence.d.ts +3 -0
  255. package/product/product.d.ts +22 -0
  256. package/product/useCanvasClipboardShortcuts.d.ts +6 -0
  257. package/product-iyBbEPIs.mjs +4 -0
  258. package/product.mjs +1 -0
  259. package/standard/extensions.d.ts +1 -0
  260. package/standard/index.d.ts +3 -0
  261. package/standard/standardApi.d.ts +91 -0
  262. package/standard-BmrxwqpJ.mjs +44 -0
  263. package/standard.mjs +1 -0
@@ -0,0 +1,2614 @@
1
+ import {
2
+ UNINITIALIZED,
3
+ atom,
4
+ computed,
5
+ type Atom,
6
+ type Signal,
7
+ } from "@land/signals";
8
+ import type {
9
+ ArrowTerminalBindingResolver,
10
+ ArrowTerminalBindingResolverInput,
11
+ ResolvedArrowTerminalBinding,
12
+ } from "../bindings/arrow/arrowBindingHelpers";
13
+ import type { BindingUtil } from "../bindings/BindingUtil";
14
+ import { BindingUtilRegistry } from "../bindings/BindingUtilRegistry";
15
+ import { Box } from "../geometry/Box";
16
+ import { Mat } from "../geometry/Mat";
17
+ import { Vec } from "../geometry/Vec";
18
+ import {
19
+ type BindingId,
20
+ type BindingRecord,
21
+ DEFAULT_CAMERA,
22
+ DEFAULT_SESSION_ID,
23
+ type AssetId,
24
+ type AssetRecord,
25
+ type Camera,
26
+ isAssetRecord,
27
+ isBindingRecord,
28
+ isPageRecord,
29
+ isSessionRecord,
30
+ isShapeRecord,
31
+ type PageId,
32
+ type PageRecord,
33
+ type EditorRecord,
34
+ type RecordScope,
35
+ type SessionRecord,
36
+ type ShapeId,
37
+ type ShapeParentId,
38
+ type ShapeRecord,
39
+ type ShapeEditingEntryRequest,
40
+ type ShapeEditingSession,
41
+ } from "../schema/records";
42
+ import {
43
+ type DocumentSnapshot,
44
+ type SessionSnapshot,
45
+ } from "../schema/snapshots";
46
+ import type {
47
+ ShapeSelectionFlipAxis,
48
+ ShapeUtil,
49
+ } from "../shapes/ShapeUtil";
50
+ import { ShapeUtilRegistry } from "../shapes/ShapeUtilRegistry";
51
+ import {
52
+ exportShapesToSvgString,
53
+ ShapeSvgExporterRegistry,
54
+ type ShapeSvgExporter,
55
+ type SvgExportOptions,
56
+ type SvgExportResult,
57
+ } from "../exports/svg";
58
+ import {
59
+ getExportIntegrityReport,
60
+ repairExportIntegrity,
61
+ type ExportIntegrityRepairResult,
62
+ type ExportIntegrityReport,
63
+ } from "../exports/integrity";
64
+ import {
65
+ getShapeCapabilityManifest,
66
+ type ShapeCapabilityDefinition,
67
+ type ShapeCapabilityManifest,
68
+ } from "../shapes/capabilities";
69
+ import { Store, type ChangeSource, type StoreChange } from "@land/store";
70
+ import {
71
+ createEditorCommands,
72
+ type CreateAssetInput,
73
+ type CreateBindingInput,
74
+ type CreatePageInput,
75
+ type CreateShapeInput,
76
+ type StartEditingShapeInput,
77
+ type EditorCommands,
78
+ } from "./commands";
79
+ import type {
80
+ CancelEditorEvent,
81
+ ClickEditorEvent,
82
+ EditorEvent,
83
+ PointerEditorEvent,
84
+ } from "./events";
85
+ import { HistoryManager, type HistoryState } from "./HistoryManager";
86
+ import { BindingManager } from "./managers/BindingManager";
87
+ import { CameraManager } from "./managers/CameraManager";
88
+ import { ClipboardManager } from "./managers/ClipboardManager";
89
+ import { DocumentSnapshotManager } from "./managers/DocumentSnapshotManager";
90
+ import { InputsManager } from "./managers/InputsManager";
91
+ import {
92
+ InteractionTransientManager,
93
+ type LaserTrailState,
94
+ } from "./managers/InteractionTransientManager";
95
+ import {
96
+ QueryManager,
97
+ type ShapeContainment,
98
+ } from "./managers/QueryManager";
99
+ import {
100
+ SpatialIndexManager,
101
+ type ShapeAtPointOptions,
102
+ type ShapeLineSegmentHitTestOptions,
103
+ type SpatialQueryOptions,
104
+ } from "./managers/SpatialIndexManager";
105
+ import { SnapManager } from "./managers/SnapManager";
106
+ import { SideEffectManager } from "./managers/SideEffectManager";
107
+ import { SelectionLayoutManager } from "./managers/SelectionLayoutManager";
108
+ import { SelectionManager } from "./managers/SelectionManager";
109
+ import { SelectionOverlayManager } from "./managers/SelectionOverlayManager";
110
+ import { ShapeEditingManager } from "./managers/ShapeEditingManager";
111
+ import { ShapeGeometryManager } from "./managers/ShapeGeometryManager";
112
+ import { ShapeHierarchyManager } from "./managers/ShapeHierarchyManager";
113
+ import { RenderWindowManager } from "./managers/RenderWindowManager";
114
+ import { ToolManager } from "./managers/ToolManager";
115
+ import type { ShapeGeometry } from "./queries/shapeTransforms";
116
+ import { editorStoreSymbol, getEditorStore, setEditorStore } from "./internal";
117
+ import {
118
+ getEditorSelectionFlipUpdates,
119
+ resetEditorSelectionRotation,
120
+ } from "./editorSelectionTransforms";
121
+ import type {
122
+ DuplicateSelectionInput,
123
+ EditorClipboardPayload,
124
+ PasteClipboardPayloadInput,
125
+ } from "./clipboard";
126
+ import { InteractionHandlerRegistry } from "./interactions";
127
+ import { SelectionHandleInteractionRegistry } from "./selectionHandleInteractions";
128
+ import {
129
+ RENDER_CULLING_OVERSCAN_SCREEN,
130
+ doesBoxContainBox,
131
+ } from "./renderWindow";
132
+ import {
133
+ areSelectedShapeOverlayItemsEqual,
134
+ getSelectionHandleState,
135
+ isSingleSelectionPathOverlay,
136
+ type SelectedShapeOverlayItem,
137
+ } from "./selectionOverlay";
138
+ import type {
139
+ ArrowBindingPreviewState,
140
+ SnapIndicatorState,
141
+ SelectionHandle,
142
+ SelectionFrame,
143
+ SelectionOverlayFrameState,
144
+ SelectionOverlayFrameGeometryState,
145
+ SelectionOverlayFrameStyleState,
146
+ SelectionOverlayHandleAnchorsState,
147
+ SelectionOverlayHandleCountsState,
148
+ SelectionOverlayHandlesState,
149
+ SelectionOverlayState,
150
+ } from "./selection";
151
+
152
+ const SELECTION_HANDLE_HIT_PADDING_SCREEN = 6;
153
+ const DOUBLE_CLICK_DURATION_MS = 250;
154
+ const DOUBLE_CLICK_DISTANCE = 40;
155
+ const DOUBLE_CLICK_DISTANCE_SQUARED =
156
+ DOUBLE_CLICK_DISTANCE * DOUBLE_CLICK_DISTANCE;
157
+ const CAMERA_WHEEL_MAX_ZOOM_STEP = 10;
158
+ const CAMERA_WHEEL_ZOOM_SPEED = 1;
159
+
160
+ export interface EditorOptions {
161
+ shapeUtils?: ShapeUtil[];
162
+ shapeSvgExporters?: ShapeSvgExporter[];
163
+ bindingUtils?: BindingUtil[];
164
+ shapeCapabilities?: readonly ShapeCapabilityDefinition[];
165
+ resolveAssetUrl?: (assetId: AssetId) => string | null;
166
+ documentSnapshot?: unknown;
167
+ sessionSnapshot?: unknown;
168
+ }
169
+
170
+ export interface ContainerBoundsQueryOptions {
171
+ includeIntersecting?: boolean;
172
+ }
173
+
174
+ export interface RenderItem {
175
+ id: ShapeId;
176
+ shape: ShapeRecord;
177
+ bounds: Box;
178
+ isSelected: boolean;
179
+ }
180
+
181
+ export interface EditorPageState {
182
+ pages: PageRecord[];
183
+ currentPageId: PageId;
184
+ currentPage: PageRecord | null;
185
+ canDeleteCurrentPage: boolean;
186
+ }
187
+
188
+ export interface EditorToolState {
189
+ activeToolId: string;
190
+ isToolLocked: boolean;
191
+ }
192
+
193
+ export type ShapeSiblingOrderDirection = "backward" | "forward";
194
+ export type ShapeSiblingOrderEdge = "back" | "front";
195
+
196
+ export interface ShapeSiblingOrderState {
197
+ parentId: ShapeParentId;
198
+ index: number;
199
+ count: number;
200
+ canMoveBackward: boolean;
201
+ canMoveForward: boolean;
202
+ }
203
+
204
+ export interface ShapeSelectionSiblingOrderState {
205
+ parentId: ShapeParentId;
206
+ selectedCount: number;
207
+ count: number;
208
+ canMoveBackward: boolean;
209
+ canMoveForward: boolean;
210
+ }
211
+
212
+ export type ReparentShapePlacementFailureReason =
213
+ | "notFound"
214
+ | "locked"
215
+ | "hidden"
216
+ | "invalidParent"
217
+ | "cycle"
218
+ | "invalidBeforeId";
219
+
220
+ export type ReparentShapePlacementResult =
221
+ | { ok: true; movedIds: ShapeId[] }
222
+ | { ok: false; reason: ReparentShapePlacementFailureReason };
223
+
224
+ export interface ReparentShapePlacementInput {
225
+ ids: readonly ShapeId[];
226
+ parentId: ShapeParentId;
227
+ beforeId?: ShapeId;
228
+ }
229
+
230
+ export interface ShapeGroupState {
231
+ parentId: ShapeParentId;
232
+ shapeIds: ShapeId[];
233
+ bounds: Box;
234
+ }
235
+
236
+ export interface LayerTreeNode {
237
+ id: ShapeId;
238
+ parentId: ShapeParentId;
239
+ pageId: PageId;
240
+ type: ShapeRecord["type"];
241
+ childIds: ShapeId[];
242
+ index: number;
243
+ depth: number;
244
+ isHidden: boolean;
245
+ isLocked: boolean;
246
+ isEffectivelyHidden: boolean;
247
+ isEffectivelyLocked: boolean;
248
+ }
249
+
250
+ export type LayerOrderDirection = "backward" | "forward";
251
+ export type LayerOrderEdge = "back" | "front";
252
+
253
+ export type ShapePropsPatch = Record<string, unknown>;
254
+
255
+ export interface EditorShapeEditingState {
256
+ editingShapeId: ShapeId | null;
257
+ session: ShapeEditingSession | null;
258
+ }
259
+
260
+ export interface EditorShapeEditingEntryRequestState {
261
+ request: ShapeEditingEntryRequest | null;
262
+ }
263
+
264
+ export interface EditorSelectionState {
265
+ count: number;
266
+ singleShapeType: ShapeRecord["type"] | null;
267
+ commonShapeType: ShapeRecord["type"] | null;
268
+ hasPointHandleSelection: boolean;
269
+ hasFrameSelection: boolean;
270
+ isMixedFrameAndPointHandleSelection: boolean;
271
+ canDragPointHandles: boolean;
272
+ hasSelectionRotation: boolean;
273
+ hasMixedSelectionRotation: boolean;
274
+ canResetSelectionRotation: boolean;
275
+ frameBounds: Box | null;
276
+ frameRotation: number;
277
+ frameRotationCenter: Vec | null;
278
+ toolbarAnchorPagePoint: Vec | null;
279
+ }
280
+
281
+ export interface EditorRenderHostDebugState {
282
+ culledCount: number;
283
+ visibleCount: number;
284
+ retainedCount: number;
285
+ retainedBaseCount: number;
286
+ culledRangeCount: number;
287
+ culledRangeSpan: number;
288
+ culledRangesSummary: string;
289
+ retainedBaseRangeCount: number;
290
+ retainedBaseRangeSpan: number;
291
+ retainedBaseRangesSummary: string;
292
+ retainedRangeCount: number;
293
+ retainedRangeSpan: number;
294
+ retainedRangesSummary: string;
295
+ retainedChangeCount: number;
296
+ retainedAddedCount: number;
297
+ retainedRemovedCount: number;
298
+ selectedOutsideCulledCount: number;
299
+ }
300
+
301
+ export interface EditorOverlayDebugState {
302
+ selectionFrameActive: boolean;
303
+ selectionHandlesActive: boolean;
304
+ bindingPreviewActive: boolean;
305
+ brushBoxActive: boolean;
306
+ selectionFrameChangeCount: number;
307
+ selectionHandlesChangeCount: number;
308
+ resizeHandleCount: number;
309
+ rotateHandleCount: number;
310
+ terminalHandleCount: number;
311
+ middleHandleCount: number;
312
+ customHandleCount: number;
313
+ boundsSummary: string;
314
+ rotation: number | null;
315
+ strokeWidth: number | null;
316
+ }
317
+
318
+ interface ReadonlyStore {
319
+ readonly recordsSignal: Signal<Map<string, EditorRecord>>;
320
+ get(id: string): EditorRecord | undefined;
321
+ getAll(): EditorRecord[];
322
+ getByScope(scope: RecordScope): EditorRecord[];
323
+ listen(listener: (change: StoreChange<EditorRecord>) => void): () => void;
324
+ }
325
+
326
+ import {
327
+ areBoxesEqual,
328
+ areShapeIdArraysEqual,
329
+ areShapeIdSetsEqual,
330
+ areShapePropsPatchesEqual,
331
+ areVecsEqual,
332
+ getCommonShapeType,
333
+ getShapeAssetId,
334
+ } from "./editorComputedHelpers";
335
+
336
+ export class Editor {
337
+ readonly store: ReadonlyStore;
338
+ readonly [editorStoreSymbol]!: Store<EditorRecord>;
339
+ readonly inputs = new InputsManager();
340
+ readonly snapshots = new DocumentSnapshotManager(this);
341
+ readonly clipboard = new ClipboardManager(this);
342
+ readonly tools = new ToolManager(this);
343
+ readonly camera = new CameraManager(this);
344
+ readonly selection = new SelectionManager(this);
345
+ readonly selectionLayout = new SelectionLayoutManager(this);
346
+ readonly editing = new ShapeEditingManager(this);
347
+ readonly geometry = new ShapeGeometryManager(this);
348
+ readonly hierarchy = new ShapeHierarchyManager(this);
349
+ readonly shapeUtils = new ShapeUtilRegistry();
350
+ readonly shapeSvgExporters = new ShapeSvgExporterRegistry();
351
+ readonly bindingUtils = new BindingUtilRegistry();
352
+ readonly interactions = new InteractionHandlerRegistry();
353
+ readonly selectionHandleInteractions = new SelectionHandleInteractionRegistry(
354
+ this.interactions,
355
+ );
356
+ readonly queries: QueryManager;
357
+ readonly bindings: BindingManager;
358
+ readonly spatialIndex: SpatialIndexManager;
359
+ readonly snaps: SnapManager;
360
+ readonly renderWindow: RenderWindowManager;
361
+ readonly selectionOverlay: SelectionOverlayManager;
362
+ readonly transients = new InteractionTransientManager(this);
363
+ readonly history: HistoryManager;
364
+ readonly sideEffects = new SideEffectManager();
365
+ readonly commands: EditorCommands;
366
+ private readonly shapeCapabilityDefinitions: readonly ShapeCapabilityDefinition[];
367
+ readonly brushBoxSignal: Signal<Box | null> = this.transients.brushBoxSignal;
368
+ readonly brushBoxActiveSignal: Signal<boolean> =
369
+ this.transients.brushBoxActiveSignal;
370
+ readonly erasingShapeIdsSignal: Signal<ShapeId[]> =
371
+ this.transients.erasingShapeIdsSignal;
372
+ readonly erasingShapeIdsActiveSignal: Signal<boolean> =
373
+ this.transients.erasingShapeIdsActiveSignal;
374
+ readonly eraserTrailSignal: Signal<Vec[]> =
375
+ this.transients.eraserTrailSignal;
376
+ readonly eraserTrailActiveSignal: Signal<boolean> =
377
+ this.transients.eraserTrailActiveSignal;
378
+ readonly laserTrailSignal: Signal<LaserTrailState | null> =
379
+ this.transients.laserTrailSignal;
380
+ readonly laserTrailActiveSignal: Signal<boolean> =
381
+ this.transients.laserTrailActiveSignal;
382
+ readonly bindingPreviewSignal: Signal<ArrowBindingPreviewState | null> =
383
+ this.transients.bindingPreviewSignal;
384
+ readonly bindingPreviewActiveSignal: Signal<boolean> =
385
+ this.transients.bindingPreviewActiveSignal;
386
+ readonly dropTargetShapeIdSignal: Signal<ShapeId | null> =
387
+ this.transients.dropTargetShapeIdSignal;
388
+ readonly snapIndicatorsSignal: Signal<SnapIndicatorState[]>;
389
+ readonly snapIndicatorsActiveSignal: Signal<boolean>;
390
+ get viewportScreenSizeSignal(): Signal<{ w: number; h: number }> {
391
+ return this.camera.viewportScreenSizeSignal;
392
+ }
393
+ readonly shapeEditingEntryRequestSignal: Signal<ShapeEditingEntryRequest | null> =
394
+ this.editing.entryRequestSignal;
395
+ private readonly _shapeDefaultPropsByTypeSignal: Atom<
396
+ ReadonlyMap<ShapeRecord["type"], ShapePropsPatch>
397
+ > = atom<ReadonlyMap<ShapeRecord["type"], ShapePropsPatch>>(
398
+ "editor.shapeDefaultPropsByType",
399
+ new Map(),
400
+ );
401
+ readonly shapeDefaultPropsByTypeSignal: Signal<
402
+ ReadonlyMap<ShapeRecord["type"], ShapePropsPatch>
403
+ > = this._shapeDefaultPropsByTypeSignal;
404
+ private nextShapeNumber = 1;
405
+ private nextPageNumber = 1;
406
+ private nextBindingNumber = 1;
407
+ private nextAssetNumber = 1;
408
+ private assetUrlResolver: (assetId: AssetId) => string | null = () => null;
409
+
410
+ private get _internalStore(): Store<EditorRecord> {
411
+ return getEditorStore(this);
412
+ }
413
+
414
+ readonly sessionSignal = computed<SessionRecord>("editor.session", () => {
415
+ const session = this._internalStore.recordsSignal
416
+ .get()
417
+ .get(DEFAULT_SESSION_ID);
418
+ if (!isSessionRecord(session)) {
419
+ throw new Error("Missing session record");
420
+ }
421
+ return session;
422
+ });
423
+
424
+ readonly currentPageIdSignal = computed<PageId>(
425
+ "editor.currentPageId",
426
+ () => {
427
+ return this.sessionSignal.get().currentPageId;
428
+ },
429
+ );
430
+
431
+ readonly currentPageShapeIdsSignal: Signal<ShapeId[]>;
432
+
433
+ readonly currentPageShapesSignal: Signal<ShapeRecord[]>;
434
+
435
+ readonly culledShapeIdsSignal: Signal<ShapeId[]>;
436
+ readonly renderVisibleShapeIdsSignal: Signal<ShapeId[]>;
437
+ readonly retainedShapeIdsSignal: Signal<ShapeId[]>;
438
+ readonly selectionPageBoundsSignal: Signal<Box | null>;
439
+ readonly selectionFrameBoundsSignal: Signal<Box | null>;
440
+ readonly selectionToolbarAnchorPagePointSignal: Signal<Vec | null>;
441
+
442
+ readonly pagesSignal = computed<PageRecord[]>("editor.pages", () => {
443
+ return Array.from(this._internalStore.recordsSignal.get().values())
444
+ .filter(isPageRecord)
445
+ .sort((left, right) => left.index.localeCompare(right.index));
446
+ });
447
+
448
+ readonly currentPageSignal = computed<PageRecord | null>(
449
+ "editor.currentPage",
450
+ () => {
451
+ const currentPageId = this.currentPageIdSignal.get();
452
+ return (
453
+ this.pagesSignal.get().find((page) => page.id === currentPageId) ?? null
454
+ );
455
+ },
456
+ );
457
+
458
+ readonly pageStateSignal = computed<EditorPageState>(
459
+ "editor.pageState",
460
+ () => {
461
+ const pages = this.pagesSignal.get();
462
+ const currentPageId = this.currentPageIdSignal.get();
463
+ const currentPage =
464
+ pages.find((page) => page.id === currentPageId) ?? null;
465
+ return {
466
+ pages,
467
+ currentPageId,
468
+ currentPage,
469
+ canDeleteCurrentPage: pages.length > 1 && currentPage !== null,
470
+ };
471
+ },
472
+ );
473
+
474
+ readonly toolStateSignal = computed<EditorToolState>(
475
+ "editor.toolState",
476
+ () => {
477
+ const session = this.sessionSignal.get();
478
+ return {
479
+ activeToolId: session.activeToolId,
480
+ isToolLocked: session.isToolLocked,
481
+ };
482
+ },
483
+ );
484
+
485
+ readonly editingShapeIdSignal = computed<ShapeId | null>(
486
+ "editor.editingShapeId",
487
+ () => this.sessionSignal.get().editingShapeId,
488
+ );
489
+
490
+ readonly shapeEditingSessionSignal = computed<ShapeEditingSession | null>(
491
+ "editor.shapeEditingSession",
492
+ () => this.sessionSignal.get().shapeEditingSession,
493
+ );
494
+
495
+ readonly shapeEditingStateSignal = computed<EditorShapeEditingState>(
496
+ "editor.shapeEditingState",
497
+ () => ({
498
+ editingShapeId: this.editingShapeIdSignal.get(),
499
+ session: this.shapeEditingSessionSignal.get(),
500
+ }),
501
+ );
502
+
503
+ readonly currentCameraSignal = computed<Camera>(
504
+ "editor.currentCamera",
505
+ () => {
506
+ const session = this.sessionSignal.get();
507
+ return (
508
+ session.pageCameraById[this.currentPageIdSignal.get()] ?? DEFAULT_CAMERA
509
+ );
510
+ },
511
+ );
512
+
513
+ readonly selectedShapeIdsSignal = computed<ShapeId[]>(
514
+ "editor.selectedShapeIds",
515
+ () => {
516
+ const session = this.sessionSignal.get();
517
+ return session.pageSelectedShapeIdsById[session.currentPageId] ?? [];
518
+ },
519
+ { isEqual: areShapeIdArraysEqual },
520
+ );
521
+
522
+ readonly selectedShapeIdSetSignal = computed<Set<ShapeId>>(
523
+ "editor.selectedShapeIdSet",
524
+ () => new Set(this.selectedShapeIdsSignal.get()),
525
+ { isEqual: areShapeIdSetsEqual },
526
+ );
527
+
528
+ readonly selectedShapesSignal = computed<ShapeRecord[]>(
529
+ "editor.selectedShapes",
530
+ () =>
531
+ this.selectedShapeIdsSignal
532
+ .get()
533
+ .map((id) => this.getShapeSignal(id).get())
534
+ .filter((shape): shape is ShapeRecord => !!shape),
535
+ );
536
+
537
+ readonly selectedShapeOverlayItemsSignal = computed<
538
+ SelectedShapeOverlayItem[]
539
+ >(
540
+ "editor.selectedShapeOverlayItems",
541
+ () => {
542
+ if (this.toolStateSignal.get().activeToolId !== "select") {
543
+ return [];
544
+ }
545
+ return this.selectedShapeIdsSignal
546
+ .get()
547
+ .map((id) => this.getShapeSignal(id).get())
548
+ .filter((shape): shape is ShapeRecord => !!shape)
549
+ .map((shape) => {
550
+ const overlayInfo =
551
+ this.getShapeUtil(shape).getSelectionOverlayInfo(shape);
552
+ return {
553
+ id: shape.id,
554
+ usesSelectionPathOverlay: overlayInfo.usesSelectionPathOverlay,
555
+ startPagePoint: overlayInfo.startPagePoint,
556
+ endPagePoint: overlayInfo.endPagePoint,
557
+ middlePagePoint: overlayInfo.middlePagePoint,
558
+ path: overlayInfo.path,
559
+ };
560
+ });
561
+ },
562
+ { isEqual: areSelectedShapeOverlayItemsEqual },
563
+ );
564
+
565
+ readonly selectionStateSignal = computed<EditorSelectionState>(
566
+ "editor.selectionState",
567
+ () => {
568
+ const selectedShapeIds = this.selectedShapeIdsSignal.get();
569
+ const singleShape =
570
+ selectedShapeIds.length === 1
571
+ ? (this.getShapeSignal(selectedShapeIds[0]!).get() ?? null)
572
+ : null;
573
+ const selectedShapes = this.selectedShapesSignal.get();
574
+ const commonShapeType = getCommonShapeType(selectedShapes);
575
+ const isOnlySelected = selectedShapes.length === 1;
576
+ const selectedShapeHandleAnchors = selectedShapes.map((shape) =>
577
+ this.getShapeUtil(shape).getSelectionHandleAnchors(shape, {
578
+ isOnlySelected,
579
+ editor: this,
580
+ }),
581
+ );
582
+ const hasPointHandleSelection = selectedShapeHandleAnchors.some(
583
+ (anchors) =>
584
+ anchors.terminalHandleAnchors.length > 0 ||
585
+ anchors.middleHandleAnchors.length > 0 ||
586
+ (anchors.customHandleAnchors?.length ?? 0) > 0,
587
+ );
588
+ const hasFrameSelection = selectedShapes.some((shape) =>
589
+ this.getShapeUtil(shape).contributesToSelectionFrame(shape, {
590
+ editor: this,
591
+ }),
592
+ );
593
+ const isMixedFrameAndPointHandleSelection =
594
+ hasPointHandleSelection && hasFrameSelection;
595
+ const singleShapeHandleAnchors =
596
+ singleShape ?
597
+ this.getShapeUtil(singleShape).getSelectionHandleAnchors(
598
+ singleShape,
599
+ { isOnlySelected, editor: this },
600
+ )
601
+ : null;
602
+ const canDragPointHandles =
603
+ selectedShapeIds.length === 1 &&
604
+ !!singleShapeHandleAnchors &&
605
+ (singleShapeHandleAnchors.terminalHandleAnchors.length > 0 ||
606
+ (singleShapeHandleAnchors.customHandleAnchors?.length ?? 0) > 0);
607
+ const hasSelectionRotation = this.queries.selectionHasRotationSignal.get();
608
+ return {
609
+ count: selectedShapeIds.length,
610
+ singleShapeType: singleShape?.type ?? null,
611
+ commonShapeType,
612
+ hasPointHandleSelection,
613
+ hasFrameSelection,
614
+ isMixedFrameAndPointHandleSelection,
615
+ canDragPointHandles,
616
+ hasSelectionRotation,
617
+ hasMixedSelectionRotation:
618
+ !this.queries.selectionHasCompatibleRotationSignal.get(),
619
+ canResetSelectionRotation:
620
+ hasSelectionRotation && hasFrameSelection,
621
+ frameBounds: this.selectionFrameBoundsSignal.get(),
622
+ frameRotation: this.queries.selectionRotationSignal.get(),
623
+ frameRotationCenter: this.queries.selectionRotationCenterSignal.get(),
624
+ toolbarAnchorPagePoint:
625
+ this.selectionToolbarAnchorPagePointSignal.get(),
626
+ };
627
+ },
628
+ );
629
+
630
+ readonly pageTransformSignal = computed<Mat>("editor.pageTransform", () => {
631
+ const camera = this.currentCameraSignal.get();
632
+ return Mat.Scale(camera.z, camera.z).compose(
633
+ Mat.Translate(-camera.x, -camera.y),
634
+ );
635
+ });
636
+
637
+ readonly cameraZoomSignal = computed<number>("editor.cameraZoom", () => {
638
+ return this.currentCameraSignal.get().z;
639
+ });
640
+
641
+ readonly viewportPageBoundsSignal = computed<Box | null>(
642
+ "editor.viewportPageBounds",
643
+ () => {
644
+ const { w, h } = this.viewportScreenSizeSignal.get();
645
+ if (w <= 0 || h <= 0) {
646
+ return null;
647
+ }
648
+
649
+ const camera = this.currentCameraSignal.get();
650
+ return new Box(camera.x, camera.y, w / camera.z, h / camera.z);
651
+ },
652
+ );
653
+
654
+ readonly renderViewportPageBoundsSignal = computed<Box | null>(
655
+ "editor.renderViewportPageBounds",
656
+ (previous) => {
657
+ const viewportBounds = this.viewportPageBoundsSignal.get();
658
+ if (!viewportBounds) {
659
+ return null;
660
+ }
661
+
662
+ if (
663
+ previous !== UNINITIALIZED &&
664
+ previous &&
665
+ doesBoxContainBox(previous, viewportBounds)
666
+ ) {
667
+ return previous;
668
+ }
669
+
670
+ return viewportBounds.expand(
671
+ RENDER_CULLING_OVERSCAN_SCREEN / this.cameraZoomSignal.get(),
672
+ );
673
+ },
674
+ { isEqual: areBoxesEqual },
675
+ );
676
+
677
+ readonly renderItemsSignal: Signal<RenderItem[]>;
678
+ readonly renderHostDebugStateSignal: Signal<EditorRenderHostDebugState>;
679
+ readonly historyStateSignal: Signal<HistoryState>;
680
+ readonly shapeRenderOrderByIdSignal: Signal<Map<ShapeId, number>>;
681
+ readonly culledShapeIdSetSignal: Signal<Set<ShapeId>>;
682
+ readonly selectedShapeIdsOutsideCulledSignal: Signal<ShapeId[]>;
683
+ readonly selectedShapeIdsOutsideCulledSetSignal: Signal<Set<ShapeId>>;
684
+ readonly renderVisibleShapeIdSetSignal: Signal<Set<ShapeId>>;
685
+
686
+ private readonly shapeSignals = new Map<
687
+ ShapeId,
688
+ Signal<ShapeRecord | undefined>
689
+ >();
690
+ private readonly shapeSelectedSignals = new Map<ShapeId, Signal<boolean>>();
691
+ private readonly shapeRenderOrderSignals = new Map<ShapeId, Signal<number>>();
692
+ private readonly shapeRenderVisibleSignals = new Map<
693
+ ShapeId,
694
+ Signal<boolean>
695
+ >();
696
+ private readonly shapeDocumentVisibleSignals = new Map<
697
+ ShapeId,
698
+ Signal<boolean>
699
+ >();
700
+ private readonly shapeEditingSessionSignals = new Map<
701
+ ShapeId,
702
+ Signal<ShapeEditingSession | null>
703
+ >();
704
+ private readonly shapeDropTargetSignals = new Map<ShapeId, Signal<boolean>>();
705
+ private readonly shapeErasingSignals = new Map<ShapeId, Signal<boolean>>();
706
+ private readonly shapeSelectedDescendantTransformAncestorSignals = new Map<
707
+ ShapeId,
708
+ Signal<boolean>
709
+ >();
710
+ private lastPointerDownForClickCount:
711
+ | { timestamp: number; screenPoint: Vec }
712
+ | null = null;
713
+ private lastPointerUpForClickCount:
714
+ | { timestamp: number; screenPoint: Vec }
715
+ | null = null;
716
+ private currentPointerClickCount = 1;
717
+
718
+ readonly selectionOverlayFrameGeometrySignal: Signal<SelectionOverlayFrameGeometryState | null>;
719
+ readonly selectionOverlayCompanionFrameGeometriesSignal: Signal<
720
+ SelectionOverlayFrameGeometryState[]
721
+ >;
722
+ readonly selectionOverlayArrowPathSignal: Signal<string | null>;
723
+ readonly selectionOverlayFrameStyleSignal: Signal<SelectionOverlayFrameStyleState>;
724
+ readonly selectionOverlayFrameSignal: Signal<SelectionOverlayFrameState | null>;
725
+ readonly selectionOverlayHandleAnchorsSignal: Signal<SelectionOverlayHandleAnchorsState | null>;
726
+ readonly selectionOverlayHandlesSignal: Signal<SelectionOverlayHandlesState | null>;
727
+ readonly selectionOverlayHandleCountsSignal: Signal<SelectionOverlayHandleCountsState | null>;
728
+ readonly selectionOverlaySignal: Signal<SelectionOverlayState | null>;
729
+ readonly overlayDebugStateSignal: Signal<EditorOverlayDebugState>;
730
+
731
+ constructor(options: EditorOptions = {}) {
732
+ this.shapeCapabilityDefinitions = options.shapeCapabilities ?? [];
733
+ if (options.resolveAssetUrl) {
734
+ this.assetUrlResolver = options.resolveAssetUrl;
735
+ }
736
+
737
+ for (const util of options.shapeUtils ?? []) {
738
+ this.shapeUtils.register(util);
739
+ }
740
+ for (const exporter of options.shapeSvgExporters ?? []) {
741
+ this.shapeSvgExporters.register(exporter);
742
+ }
743
+ for (const util of options.bindingUtils ?? []) {
744
+ this.bindingUtils.register(util);
745
+ }
746
+
747
+ const store = new Store<EditorRecord>(
748
+ this.snapshots.buildInitialRecords(
749
+ options.documentSnapshot ?? null,
750
+ options.sessionSnapshot ?? null,
751
+ ),
752
+ );
753
+ setEditorStore(this, store);
754
+ this.store = store;
755
+ this.snapshots.ensureSessionIntegrity();
756
+ this.queries = new QueryManager(this);
757
+ this.bindings = new BindingManager(this);
758
+ this.spatialIndex = new SpatialIndexManager(this, this.queries);
759
+ this.snaps = new SnapManager(this);
760
+ this.snapIndicatorsSignal = this.snaps.indicatorsSignal;
761
+ this.snapIndicatorsActiveSignal = this.snaps.activeSignal;
762
+ this.currentPageShapeIdsSignal = this.queries.currentPageShapeIdsSignal;
763
+ this.currentPageShapesSignal = this.queries.currentPageShapesSignal;
764
+ this.selectionPageBoundsSignal = this.queries.selectionPageBoundsSignal;
765
+ this.selectionFrameBoundsSignal = this.queries.selectionFrameBoundsSignal;
766
+ this.selectionToolbarAnchorPagePointSignal =
767
+ this.queries.selectionToolbarAnchorPagePointSignal;
768
+ this.selectionOverlay = new SelectionOverlayManager(this);
769
+ this.selectionOverlayFrameGeometrySignal =
770
+ this.selectionOverlay.frameGeometrySignal;
771
+ this.selectionOverlayCompanionFrameGeometriesSignal =
772
+ this.selectionOverlay.companionFrameGeometriesSignal;
773
+ this.selectionOverlayArrowPathSignal =
774
+ this.selectionOverlay.arrowPathSignal;
775
+ this.selectionOverlayFrameStyleSignal =
776
+ this.selectionOverlay.frameStyleSignal;
777
+ this.selectionOverlayFrameSignal = this.selectionOverlay.frameSignal;
778
+ this.selectionOverlayHandleAnchorsSignal =
779
+ this.selectionOverlay.handleAnchorsSignal;
780
+ this.selectionOverlayHandlesSignal = this.selectionOverlay.handlesSignal;
781
+ this.selectionOverlayHandleCountsSignal =
782
+ this.selectionOverlay.handleCountsSignal;
783
+ this.selectionOverlaySignal = this.selectionOverlay.overlaySignal;
784
+ this.overlayDebugStateSignal = this.selectionOverlay.debugStateSignal;
785
+ this.renderWindow = new RenderWindowManager(this);
786
+ this.shapeRenderOrderByIdSignal =
787
+ this.renderWindow.shapeRenderOrderByIdSignal;
788
+ this.culledShapeIdsSignal = this.renderWindow.culledShapeIdsSignal;
789
+ this.culledShapeIdSetSignal = this.renderWindow.culledShapeIdSetSignal;
790
+ this.selectedShapeIdsOutsideCulledSignal =
791
+ this.renderWindow.selectedShapeIdsOutsideCulledSignal;
792
+ this.selectedShapeIdsOutsideCulledSetSignal =
793
+ this.renderWindow.selectedShapeIdsOutsideCulledSetSignal;
794
+ this.renderVisibleShapeIdsSignal =
795
+ this.renderWindow.renderVisibleShapeIdsSignal;
796
+ this.retainedShapeIdsSignal = this.renderWindow.retainedShapeIdsSignal;
797
+ this.renderVisibleShapeIdSetSignal =
798
+ this.renderWindow.renderVisibleShapeIdSetSignal;
799
+ this.renderItemsSignal = this.renderWindow.renderItemsSignal;
800
+ this.renderHostDebugStateSignal =
801
+ this.renderWindow.renderHostDebugStateSignal;
802
+ this.syncShapeCounter();
803
+ this.syncPageCounter();
804
+ this.syncBindingCounter();
805
+ this.syncAssetCounter();
806
+ this.bindings.repairLoadedBindings("system");
807
+ this.history = new HistoryManager(this);
808
+ this.historyStateSignal = this.history.state;
809
+ this.commands = createEditorCommands(this);
810
+ this.registerBuiltInSideEffects();
811
+ }
812
+
813
+ private registerBuiltInSideEffects(): void {
814
+ // Binding cleanup must run before the shapes are removed so the bindings
815
+ // can still read the doomed shapes. Selection/editing-session repair runs
816
+ // after the records are gone.
817
+ this.sideEffects.registerBeforeDeleteHandler("shape", (ids, source) => {
818
+ this.bindings.cleanupBindingsForDeletedShapes(ids as ShapeId[], source);
819
+ });
820
+ this.sideEffects.registerAfterDeleteHandler("shape", (ids, source) => {
821
+ this.removeShapeIdsFromSelection(ids as ShapeId[], source);
822
+ this.removeShapeIdsFromEditingSession(ids as ShapeId[], source);
823
+ });
824
+ }
825
+
826
+ dispatch(event: EditorEvent): void {
827
+ const normalizedEvent = this.normalizeEvent(event);
828
+ this.inputs.handleEvent(normalizedEvent);
829
+
830
+ if (normalizedEvent.type === "keyboard" && normalizedEvent.name === "key_down") {
831
+ if (
832
+ normalizedEvent.altKey &&
833
+ !normalizedEvent.accelKey &&
834
+ !normalizedEvent.shiftKey &&
835
+ (normalizedEvent.key === "ArrowDown" ||
836
+ normalizedEvent.key === "ArrowUp")
837
+ ) {
838
+ const direction =
839
+ normalizedEvent.key === "ArrowDown" ? "backward" : "forward";
840
+ if (this.moveSelectionWithinParent(direction, "user")) {
841
+ return;
842
+ }
843
+ }
844
+
845
+ if (normalizedEvent.accelKey && normalizedEvent.key.toLowerCase() === "z") {
846
+ if (normalizedEvent.shiftKey) {
847
+ this.redo();
848
+ } else {
849
+ this.undo();
850
+ }
851
+ return;
852
+ }
853
+
854
+ if (!normalizedEvent.accelKey && !normalizedEvent.altKey) {
855
+ const shortcutToolId = this.tools.getToolIdForShortcut(
856
+ normalizedEvent.key,
857
+ );
858
+ if (shortcutToolId) {
859
+ this.commands.setActiveTool(shortcutToolId, { source: "system" });
860
+ return;
861
+ }
862
+
863
+ if (normalizedEvent.key === "Backspace" || normalizedEvent.key === "Delete") {
864
+ this.deleteSelection("user");
865
+ return;
866
+ }
867
+
868
+ }
869
+ }
870
+
871
+ if (
872
+ normalizedEvent.type === "keyboard" &&
873
+ normalizedEvent.name === "key_down" &&
874
+ normalizedEvent.key === "Escape"
875
+ ) {
876
+ const wasInteracting = !this.tools.isActiveToolInInitialState();
877
+ if (!wasInteracting && this.getActiveToolId() !== "select") {
878
+ this.commands.setActiveTool("select", { source: "system" });
879
+ } else {
880
+ this.cancelInteractionAndClearTransients("escape");
881
+ }
882
+ return;
883
+ }
884
+
885
+ if (normalizedEvent.type === "wheel") {
886
+ this.handleWheelEvent(normalizedEvent);
887
+ return;
888
+ }
889
+
890
+ if (normalizedEvent.type === "cancel") {
891
+ this.cancelInteractionAndClearTransients(normalizedEvent.reason);
892
+ return;
893
+ }
894
+
895
+ if (normalizedEvent.type === "pointer" && normalizedEvent.name === "pointer_cancel") {
896
+ this.cancelInteractionAndClearTransients("pointer_cancel");
897
+ return;
898
+ }
899
+
900
+ if (this.interactions.handleEvent(this, normalizedEvent) === "handled") {
901
+ return;
902
+ }
903
+
904
+ this.tools.dispatch(normalizedEvent);
905
+
906
+ if (
907
+ normalizedEvent.type === "pointer" &&
908
+ normalizedEvent.name === "pointer_up"
909
+ ) {
910
+ const doubleClickEvent = this.getDoubleClickEvent(normalizedEvent);
911
+ if (doubleClickEvent) {
912
+ if (this.interactions.handleEvent(this, doubleClickEvent) !== "handled") {
913
+ this.tools.dispatch(doubleClickEvent);
914
+ }
915
+ }
916
+ }
917
+ }
918
+
919
+ handleInteractionEvent(event: EditorEvent): boolean {
920
+ const normalizedEvent = this.normalizeEvent(event);
921
+ this.inputs.handleEvent(normalizedEvent);
922
+ return this.interactions.handleEvent(this, normalizedEvent) === "handled";
923
+ }
924
+
925
+ private normalizeEvent(event: EditorEvent): EditorEvent {
926
+ if (event.type !== "pointer") {
927
+ return event;
928
+ }
929
+
930
+ if (event.name === "pointer_up") {
931
+ // Pointer up inherits the click count computed from the preceding down/up
932
+ // sequence so tools can distinguish single-click caret placement from
933
+ // double-click select-all without relying on host DOM timing quirks.
934
+ const nextEvent =
935
+ event.clickCount === this.currentPointerClickCount
936
+ ? event
937
+ : ({
938
+ ...event,
939
+ clickCount: this.currentPointerClickCount,
940
+ } satisfies PointerEditorEvent);
941
+ this.lastPointerUpForClickCount = {
942
+ timestamp: nextEvent.timestamp,
943
+ screenPoint: nextEvent.screenPoint.clone(),
944
+ };
945
+ return nextEvent;
946
+ }
947
+
948
+ if (event.name !== "pointer_down") {
949
+ return event;
950
+ }
951
+
952
+ // Keep double-click detection in core so host layers only forward pointer
953
+ // events; select/text tools can then share one consistent click contract.
954
+ const previousDown = this.lastPointerDownForClickCount;
955
+ const previousUp = this.lastPointerUpForClickCount;
956
+ const nextClickCount =
957
+ previousDown &&
958
+ previousUp &&
959
+ previousUp.timestamp >= previousDown.timestamp &&
960
+ event.timestamp - previousUp.timestamp <= DOUBLE_CLICK_DURATION_MS &&
961
+ previousUp.screenPoint.dist2(event.screenPoint) <=
962
+ DOUBLE_CLICK_DISTANCE_SQUARED
963
+ ? 2
964
+ : 1;
965
+
966
+ this.lastPointerDownForClickCount = {
967
+ timestamp: event.timestamp,
968
+ screenPoint: event.screenPoint.clone(),
969
+ };
970
+ this.currentPointerClickCount = nextClickCount;
971
+
972
+ if (event.clickCount === nextClickCount) {
973
+ return event;
974
+ }
975
+
976
+ return {
977
+ ...event,
978
+ clickCount: nextClickCount,
979
+ } satisfies PointerEditorEvent;
980
+ }
981
+
982
+ private getDoubleClickEvent(
983
+ event: PointerEditorEvent,
984
+ ): ClickEditorEvent | null {
985
+ if ((event.clickCount ?? 1) < 2 || this.currentPointerClickCount < 2) {
986
+ return null;
987
+ }
988
+
989
+ return {
990
+ type: "click",
991
+ name: "double_click",
992
+ phase: "up",
993
+ timestamp: event.timestamp,
994
+ pointerId: event.pointerId,
995
+ button: event.button,
996
+ buttons: event.buttons,
997
+ pointerType: event.pointerType,
998
+ screenPoint: event.screenPoint.clone(),
999
+ pagePoint: event.pagePoint.clone(),
1000
+ pressure: event.pressure,
1001
+ shiftKey: event.shiftKey,
1002
+ altKey: event.altKey,
1003
+ ctrlKey: event.ctrlKey,
1004
+ metaKey: event.metaKey,
1005
+ accelKey: event.accelKey,
1006
+ };
1007
+ }
1008
+
1009
+ listen(listener: (change: StoreChange<EditorRecord>) => void): () => void {
1010
+ return this.store.listen((change) => listener(change));
1011
+ }
1012
+
1013
+ getRenderItems(): RenderItem[] {
1014
+ return this.renderItemsSignal.get();
1015
+ }
1016
+
1017
+ getPages(): PageRecord[] {
1018
+ return this.pagesSignal.get();
1019
+ }
1020
+
1021
+ getCurrentPageId(): PageId {
1022
+ return this.currentPageIdSignal.get();
1023
+ }
1024
+
1025
+ getActiveToolId(): string {
1026
+ return this.sessionSignal.get().activeToolId;
1027
+ }
1028
+
1029
+ isToolLocked(): boolean {
1030
+ return this.sessionSignal.get().isToolLocked;
1031
+ }
1032
+
1033
+ isActiveToolInInitialState(): boolean {
1034
+ return this.tools.isActiveToolInInitialState();
1035
+ }
1036
+
1037
+ getEditingShapeId(): ShapeId | null {
1038
+ return this.editingShapeIdSignal.get();
1039
+ }
1040
+
1041
+ getShapeEditingSession(): ShapeEditingSession | null {
1042
+ return this.shapeEditingSessionSignal.get();
1043
+ }
1044
+
1045
+ getShapeEditingEntryRequest(): ShapeEditingEntryRequest | null {
1046
+ return this.shapeEditingEntryRequestSignal.get();
1047
+ }
1048
+
1049
+ resetPointerClickTracking(): void {
1050
+ this.lastPointerDownForClickCount = null;
1051
+ this.lastPointerUpForClickCount = null;
1052
+ this.currentPointerClickCount = 1;
1053
+ }
1054
+
1055
+ getCurrentPageShapeIds(): ShapeId[] {
1056
+ return this.currentPageShapeIdsSignal.get();
1057
+ }
1058
+
1059
+ getPage(id: PageId): PageRecord | undefined {
1060
+ const record = this._internalStore.get(id);
1061
+ return isPageRecord(record) ? record : undefined;
1062
+ }
1063
+
1064
+ getCurrentCamera(): Camera {
1065
+ return this.camera.getCurrentCamera();
1066
+ }
1067
+
1068
+ undo(): boolean {
1069
+ this.cancelInteractionForCommand();
1070
+ return this.history.undo();
1071
+ }
1072
+
1073
+ redo(): boolean {
1074
+ this.cancelInteractionForCommand();
1075
+ return this.history.redo();
1076
+ }
1077
+
1078
+ createPageAndSelect(
1079
+ input?: CreatePageInput,
1080
+ source: ChangeSource = "user",
1081
+ ): PageRecord {
1082
+ const page = this.commands.createPage(input, { source });
1083
+ this.commands.setCurrentPageId(page.id, { source });
1084
+ return page;
1085
+ }
1086
+
1087
+ selectPage(pageId: PageId, source: ChangeSource = "user"): void {
1088
+ this.commands.setCurrentPageId(pageId, { source });
1089
+ }
1090
+
1091
+ renamePage(
1092
+ pageId: PageId,
1093
+ name: string,
1094
+ source: ChangeSource = "user",
1095
+ ): void {
1096
+ this.commands.renamePage({ id: pageId, name }, { source });
1097
+ }
1098
+
1099
+ deletePage(pageId: PageId, source: ChangeSource = "user"): boolean {
1100
+ this.cancelInteractionForCommand();
1101
+ return this.commands.deletePage(pageId, { source });
1102
+ }
1103
+
1104
+ setActiveTool(toolId: string, source: ChangeSource = "system"): void {
1105
+ this.commands.setActiveTool(toolId, { source });
1106
+ }
1107
+
1108
+ setToolLocked(locked: boolean, source: ChangeSource = "system"): void {
1109
+ this.commands.setToolLocked(locked, { source });
1110
+ }
1111
+
1112
+ toggleToolLocked(source: ChangeSource = "system"): void {
1113
+ this.setToolLocked(!this.isToolLocked(), source);
1114
+ }
1115
+
1116
+ getCamera(pageId: PageId = this.getCurrentPageId()): Camera {
1117
+ return this.camera.getCamera(pageId);
1118
+ }
1119
+
1120
+ pageToScreen(point: Vec, pageId: PageId = this.getCurrentPageId()): Vec {
1121
+ return this.camera.pageToScreen(point, pageId);
1122
+ }
1123
+
1124
+ screenToPage(point: Vec, pageId: PageId = this.getCurrentPageId()): Vec {
1125
+ return this.camera.screenToPage(point, pageId);
1126
+ }
1127
+
1128
+ getSelectedShapeIds(pageId: PageId = this.getCurrentPageId()): ShapeId[] {
1129
+ if (pageId === this.getCurrentPageId()) {
1130
+ return this.selectedShapeIdsSignal.get();
1131
+ }
1132
+ return this.sessionSignal.get().pageSelectedShapeIdsById[pageId] ?? [];
1133
+ }
1134
+
1135
+ isShapeHidden(shapeId: ShapeId): boolean {
1136
+ const seen = new Set<ShapeId>();
1137
+ let shape = this.getShape(shapeId);
1138
+ while (shape) {
1139
+ if (shape.isHidden) {
1140
+ return true;
1141
+ }
1142
+ if (!shape.parentId.startsWith("shape:")) {
1143
+ return false;
1144
+ }
1145
+
1146
+ const parentId = shape.parentId as ShapeId;
1147
+ if (seen.has(parentId)) {
1148
+ return false;
1149
+ }
1150
+ seen.add(parentId);
1151
+ shape = this.getShape(parentId);
1152
+ }
1153
+
1154
+ return false;
1155
+ }
1156
+
1157
+ getSelectedShapes(): ShapeRecord[] {
1158
+ return this.selectedShapesSignal.get();
1159
+ }
1160
+
1161
+ getDefaultShapeProps<TProps extends object = object>(
1162
+ type: ShapeRecord["type"],
1163
+ ): TProps {
1164
+ const util = this.shapeUtils.get(type);
1165
+ const override = this._shapeDefaultPropsByTypeSignal.get().get(type) ?? {};
1166
+ return util.validateProps({
1167
+ ...util.getDefaultProps(),
1168
+ ...override,
1169
+ }) as TProps;
1170
+ }
1171
+
1172
+ updateDefaultShapeProps(
1173
+ type: ShapeRecord["type"],
1174
+ props: ShapePropsPatch,
1175
+ ): void {
1176
+ const util = this.shapeUtils.get(type);
1177
+ const current = this._shapeDefaultPropsByTypeSignal.get().get(type) ?? {};
1178
+ const nextProps = util.validateProps({
1179
+ ...util.getDefaultProps(),
1180
+ ...current,
1181
+ ...props,
1182
+ });
1183
+ const fallback = util.validateProps(util.getDefaultProps());
1184
+ const nextOverride: ShapePropsPatch = {};
1185
+
1186
+ for (const [key, value] of Object.entries(nextProps)) {
1187
+ if ((fallback as Record<string, unknown>)[key] !== value) {
1188
+ nextOverride[key] = value;
1189
+ }
1190
+ }
1191
+
1192
+ if (areShapePropsPatchesEqual(current, nextOverride)) {
1193
+ return;
1194
+ }
1195
+
1196
+ const nextDefaults = new Map(this._shapeDefaultPropsByTypeSignal.get());
1197
+ if (Object.keys(nextOverride).length === 0) {
1198
+ nextDefaults.delete(type);
1199
+ } else {
1200
+ nextDefaults.set(type, nextOverride);
1201
+ }
1202
+ this._shapeDefaultPropsByTypeSignal.set(nextDefaults);
1203
+ }
1204
+
1205
+ getSelectionParentOrderState(): ShapeSelectionSiblingOrderState | null {
1206
+ return this.hierarchy.getSelectionParentOrderState();
1207
+ }
1208
+
1209
+ getSelectionGroupState(): ShapeGroupState | null {
1210
+ return this.hierarchy.getSelectionGroupState();
1211
+ }
1212
+
1213
+ canGroupSelection(): boolean {
1214
+ return this.hierarchy.canGroupSelection();
1215
+ }
1216
+
1217
+ getLayerTree(pageId: PageId = this.getCurrentPageId()): LayerTreeNode[] {
1218
+ return this.hierarchy.getLayerTree(pageId);
1219
+ }
1220
+
1221
+ getLayerNode(id: ShapeId): LayerTreeNode | null {
1222
+ return this.hierarchy.getLayerNode(id);
1223
+ }
1224
+
1225
+ canGroupShapes(ids: readonly ShapeId[]): boolean {
1226
+ return this.hierarchy.canGroupShapes(ids);
1227
+ }
1228
+
1229
+ groupShapes(
1230
+ ids: readonly ShapeId[],
1231
+ source: ChangeSource = "user",
1232
+ ): ShapeRecord | null {
1233
+ return this.hierarchy.groupShapes(ids, source);
1234
+ }
1235
+
1236
+ groupSelection(source: ChangeSource = "user"): ShapeRecord | null {
1237
+ return this.hierarchy.groupSelection(source);
1238
+ }
1239
+
1240
+ getUngroupableSelectedShapeIds(): ShapeId[] {
1241
+ return this.hierarchy.getUngroupableSelectedShapeIds();
1242
+ }
1243
+
1244
+ getUngroupableShapeIds(ids: readonly ShapeId[]): ShapeId[] {
1245
+ return this.hierarchy.getUngroupableShapeIds(ids);
1246
+ }
1247
+
1248
+ canUngroupSelection(): boolean {
1249
+ return this.hierarchy.canUngroupSelection();
1250
+ }
1251
+
1252
+ canUngroupShapes(ids: readonly ShapeId[]): boolean {
1253
+ return this.hierarchy.canUngroupShapes(ids);
1254
+ }
1255
+
1256
+ ungroupShapes(
1257
+ ids: readonly ShapeId[],
1258
+ source: ChangeSource = "user",
1259
+ ): ShapeId[] {
1260
+ return this.hierarchy.ungroupShapes(ids, source);
1261
+ }
1262
+
1263
+ ungroupSelection(source: ChangeSource = "user"): ShapeId[] {
1264
+ return this.hierarchy.ungroupSelection(source);
1265
+ }
1266
+
1267
+ getSelectionSiblingOrderState(): ShapeSiblingOrderState | null {
1268
+ return this.hierarchy.getSelectionSiblingOrderState();
1269
+ }
1270
+
1271
+ canMoveSelectionWithinParent(
1272
+ direction: ShapeSiblingOrderDirection,
1273
+ ): boolean {
1274
+ return this.hierarchy.canMoveSelectionWithinParent(direction);
1275
+ }
1276
+
1277
+ canMoveSelectionInParentOrder(
1278
+ direction: ShapeSiblingOrderDirection,
1279
+ ): boolean {
1280
+ return this.hierarchy.canMoveSelectionInParentOrder(direction);
1281
+ }
1282
+
1283
+ canMoveShapesInParentOrder(
1284
+ ids: readonly ShapeId[],
1285
+ direction: LayerOrderDirection,
1286
+ ): boolean {
1287
+ return this.hierarchy.canMoveShapesInParentOrder(ids, direction);
1288
+ }
1289
+
1290
+ moveShapesInParentOrder(
1291
+ ids: readonly ShapeId[],
1292
+ direction: LayerOrderDirection,
1293
+ source: ChangeSource = "user",
1294
+ ): boolean {
1295
+ return this.hierarchy.moveShapesInParentOrder(ids, direction, source);
1296
+ }
1297
+
1298
+ moveShapesToParentOrderEdge(
1299
+ ids: readonly ShapeId[],
1300
+ edge: LayerOrderEdge,
1301
+ source: ChangeSource = "user",
1302
+ ): boolean {
1303
+ return this.hierarchy.moveShapesToParentOrderEdge(ids, edge, source);
1304
+ }
1305
+
1306
+ moveSelectionWithinParent(
1307
+ direction: ShapeSiblingOrderDirection,
1308
+ source: ChangeSource = "user",
1309
+ ): boolean {
1310
+ return this.hierarchy.moveSelectionWithinParent(direction, source);
1311
+ }
1312
+
1313
+ /**
1314
+ * Align selected top-level shapes along an edge or center of their combined
1315
+ * page bounds. Only shapes parented directly to the current page are moved so
1316
+ * the page-space delta maps directly onto `x`/`y`. Needs at least two shapes.
1317
+ */
1318
+ alignSelection(
1319
+ mode:
1320
+ | "left"
1321
+ | "horizontal-center"
1322
+ | "right"
1323
+ | "top"
1324
+ | "vertical-center"
1325
+ | "bottom",
1326
+ source: ChangeSource = "user",
1327
+ ): boolean {
1328
+ this.cancelInteractionForCommand();
1329
+ return this.selectionLayout.align(mode, source);
1330
+ }
1331
+
1332
+ /**
1333
+ * Distribute selected top-level shapes so the gaps between them are equal
1334
+ * along the given axis. Needs at least three shapes.
1335
+ */
1336
+ distributeSelection(
1337
+ axis: "horizontal" | "vertical",
1338
+ source: ChangeSource = "user",
1339
+ ): boolean {
1340
+ this.cancelInteractionForCommand();
1341
+ return this.selectionLayout.distribute(axis, source);
1342
+ }
1343
+
1344
+ moveShapesToParentPlacement(
1345
+ input: ReparentShapePlacementInput,
1346
+ source: ChangeSource = "user",
1347
+ ): boolean {
1348
+ return this.hierarchy.moveShapesToParentPlacement(input, source);
1349
+ }
1350
+
1351
+ moveShapesToParentPlacementResult(
1352
+ input: ReparentShapePlacementInput,
1353
+ source: ChangeSource = "user",
1354
+ ): ReparentShapePlacementResult {
1355
+ return this.hierarchy.moveShapesToParentPlacementResult(input, source);
1356
+ }
1357
+
1358
+ moveSelectionInParentOrder(
1359
+ direction: ShapeSiblingOrderDirection,
1360
+ source: ChangeSource = "user",
1361
+ ): boolean {
1362
+ return this.hierarchy.moveSelectionInParentOrder(direction, source);
1363
+ }
1364
+
1365
+ moveSelectionToParentOrderEdge(
1366
+ edge: ShapeSiblingOrderEdge,
1367
+ source: ChangeSource = "user",
1368
+ ): boolean {
1369
+ return this.hierarchy.moveSelectionToParentOrderEdge(edge, source);
1370
+ }
1371
+
1372
+ canEditShape(shapeOrId: ShapeRecord | ShapeId | null | undefined): boolean {
1373
+ return this.editing.canEditShape(shapeOrId);
1374
+ }
1375
+
1376
+ startEditingShape(
1377
+ input: StartEditingShapeInput,
1378
+ source: ChangeSource = "user",
1379
+ ): ShapeEditingSession | null {
1380
+ return this.editing.start(input, source);
1381
+ }
1382
+
1383
+ completeEditing(source: ChangeSource = "user"): boolean {
1384
+ return this.editing.complete(source);
1385
+ }
1386
+
1387
+ cancelEditing(source: ChangeSource = "user"): boolean {
1388
+ return this.editing.cancel(source);
1389
+ }
1390
+
1391
+ setShapeEditingComposition(
1392
+ isComposing: boolean,
1393
+ source: ChangeSource = "system",
1394
+ ): void {
1395
+ this.editing.setComposition(isComposing, source);
1396
+ }
1397
+
1398
+ consumeShapeEditingEntryRequest(requestId: number): void {
1399
+ this.editing.consumeEntryRequest(requestId);
1400
+ }
1401
+
1402
+ deleteSelection(source: ChangeSource = "user"): void {
1403
+ this.deleteShapesById(this.getSelectedShapeIds(), source);
1404
+ }
1405
+
1406
+ deleteShapesById(
1407
+ ids: readonly ShapeId[],
1408
+ source: ChangeSource = "user",
1409
+ ): void {
1410
+ const uniqueIds = Array.from(new Set(ids));
1411
+ if (uniqueIds.length === 0) {
1412
+ return;
1413
+ }
1414
+ this.cancelInteractionForCommand();
1415
+ this.commands.deleteShapes(uniqueIds, { source });
1416
+ }
1417
+
1418
+ setShapesHidden(
1419
+ ids: readonly ShapeId[],
1420
+ isHidden: boolean,
1421
+ source: ChangeSource = "user",
1422
+ ): ShapeId[] {
1423
+ const nextIds = Array.from(
1424
+ new Set(
1425
+ ids.filter((id) => {
1426
+ const shape = this.getShape(id);
1427
+ return !!shape && shape.isHidden !== isHidden;
1428
+ }),
1429
+ ),
1430
+ );
1431
+ if (nextIds.length === 0) {
1432
+ return [];
1433
+ }
1434
+
1435
+ this.cancelInteractionForCommand();
1436
+ this.commands.updateShapes(
1437
+ nextIds.map((id) => ({
1438
+ id,
1439
+ isHidden,
1440
+ })),
1441
+ { source },
1442
+ );
1443
+
1444
+ if (isHidden) {
1445
+ this.removeShapeIdsFromSelection(
1446
+ this.getShapeIdsIncludingDescendants(nextIds),
1447
+ source,
1448
+ );
1449
+ this.removeShapeIdsFromEditingSession(
1450
+ this.getShapeIdsIncludingDescendants(nextIds),
1451
+ source,
1452
+ );
1453
+ }
1454
+
1455
+ return nextIds;
1456
+ }
1457
+
1458
+ setShapesLocked(
1459
+ ids: readonly ShapeId[],
1460
+ isLocked: boolean,
1461
+ source: ChangeSource = "user",
1462
+ ): ShapeId[] {
1463
+ const nextIds = Array.from(
1464
+ new Set(
1465
+ ids.filter((id) => {
1466
+ const shape = this.getShape(id);
1467
+ return !!shape && shape.isLocked !== isLocked;
1468
+ }),
1469
+ ),
1470
+ );
1471
+ if (nextIds.length === 0) {
1472
+ return [];
1473
+ }
1474
+
1475
+ this.cancelInteractionForCommand();
1476
+ this.commands.updateShapes(
1477
+ nextIds.map((id) => ({
1478
+ id,
1479
+ isLocked,
1480
+ })),
1481
+ { source },
1482
+ );
1483
+
1484
+ if (isLocked) {
1485
+ this.removeShapeIdsFromSelection(
1486
+ this.getShapeIdsIncludingDescendants(nextIds),
1487
+ source,
1488
+ );
1489
+ this.removeShapeIdsFromEditingSession(
1490
+ this.getShapeIdsIncludingDescendants(nextIds),
1491
+ source,
1492
+ );
1493
+ }
1494
+
1495
+ return nextIds;
1496
+ }
1497
+
1498
+ createClipboardPayloadFromSelection(): EditorClipboardPayload | null {
1499
+ return this.clipboard.createPayloadFromSelection();
1500
+ }
1501
+
1502
+ pasteClipboardPayload(input: PasteClipboardPayloadInput): ShapeId[] {
1503
+ return this.clipboard.pastePayload(input);
1504
+ }
1505
+
1506
+ duplicateSelection(input: DuplicateSelectionInput = {}): ShapeId[] {
1507
+ return this.clipboard.duplicateSelection(input);
1508
+ }
1509
+
1510
+ selectAllCurrentPage(source: ChangeSource = "user"): void {
1511
+ this.cancelInteractionForCommand();
1512
+ this.selection.selectAllCurrentPage(source);
1513
+ }
1514
+
1515
+ resetSelectionRotation(source: ChangeSource = "user"): void {
1516
+ resetEditorSelectionRotation(this, source);
1517
+ }
1518
+
1519
+ flipSelection(
1520
+ axis: ShapeSelectionFlipAxis,
1521
+ source: ChangeSource = "user",
1522
+ ): void {
1523
+ const updates = getEditorSelectionFlipUpdates(this, axis);
1524
+ if (updates.length === 0) {
1525
+ return;
1526
+ }
1527
+
1528
+ this.cancelInteractionForCommand();
1529
+ this.commands.updateShapes(updates, { source });
1530
+ }
1531
+
1532
+ getShape(id: ShapeId): ShapeRecord | undefined {
1533
+ const record = this._internalStore.get(id);
1534
+ return isShapeRecord(record) ? record : undefined;
1535
+ }
1536
+
1537
+ getAsset(id: AssetId): AssetRecord | undefined {
1538
+ const record = this._internalStore.get(id);
1539
+ return isAssetRecord(record) ? record : undefined;
1540
+ }
1541
+
1542
+ getAssets(): AssetRecord[] {
1543
+ return this._internalStore
1544
+ .getAll()
1545
+ .filter(isAssetRecord)
1546
+ .sort((left, right) => left.id.localeCompare(right.id));
1547
+ }
1548
+
1549
+ resolveAssetUrl(assetId: AssetId): string | null {
1550
+ return this.assetUrlResolver(assetId);
1551
+ }
1552
+
1553
+ getUnreferencedAssetIdsAfterShapeDelete(
1554
+ shapeIds: readonly ShapeId[],
1555
+ ): AssetId[] {
1556
+ if (shapeIds.length === 0) {
1557
+ return [];
1558
+ }
1559
+
1560
+ const deletedShapeIds = new Set(shapeIds);
1561
+ const deletedAssetIds = new Set<AssetId>();
1562
+ const retainedAssetIds = new Set<AssetId>();
1563
+
1564
+ for (const shape of this._internalStore.getAll().filter(isShapeRecord)) {
1565
+ const assetId = getShapeAssetId(shape);
1566
+ if (!assetId) {
1567
+ continue;
1568
+ }
1569
+ if (deletedShapeIds.has(shape.id)) {
1570
+ deletedAssetIds.add(assetId);
1571
+ } else {
1572
+ retainedAssetIds.add(assetId);
1573
+ }
1574
+ }
1575
+
1576
+ return this.getAssets()
1577
+ .map((asset) => asset.id)
1578
+ .filter(
1579
+ (assetId) =>
1580
+ deletedAssetIds.has(assetId) && !retainedAssetIds.has(assetId),
1581
+ );
1582
+ }
1583
+
1584
+ getShapeSignal(id: ShapeId): Signal<ShapeRecord | undefined> {
1585
+ let signal = this.shapeSignals.get(id);
1586
+ if (!signal) {
1587
+ signal = computed<ShapeRecord | undefined>(`editor.shape.${id}`, () =>
1588
+ this.getShape(id),
1589
+ );
1590
+ this.shapeSignals.set(id, signal);
1591
+ }
1592
+ return signal;
1593
+ }
1594
+
1595
+ getShapeSelectedSignal(id: ShapeId): Signal<boolean> {
1596
+ let signal = this.shapeSelectedSignals.get(id);
1597
+ if (!signal) {
1598
+ signal = computed<boolean>(`editor.shapeSelected.${id}`, () =>
1599
+ this.selectedShapeIdSetSignal.get().has(id),
1600
+ );
1601
+ this.shapeSelectedSignals.set(id, signal);
1602
+ }
1603
+ return signal;
1604
+ }
1605
+
1606
+ getShapeRenderOrderSignal(id: ShapeId): Signal<number> {
1607
+ let signal = this.shapeRenderOrderSignals.get(id);
1608
+ if (!signal) {
1609
+ signal = computed<number>(`editor.shapeRenderOrder.${id}`, () => {
1610
+ return this.shapeRenderOrderByIdSignal.get().get(id) ?? -1;
1611
+ });
1612
+ this.shapeRenderOrderSignals.set(id, signal);
1613
+ }
1614
+ return signal;
1615
+ }
1616
+
1617
+ getShapeRenderVisibleSignal(id: ShapeId): Signal<boolean> {
1618
+ let signal = this.shapeRenderVisibleSignals.get(id);
1619
+ if (!signal) {
1620
+ signal = computed<boolean>(`editor.shapeRenderVisible.${id}`, () => {
1621
+ return (
1622
+ this.renderVisibleShapeIdSetSignal.get().has(id) &&
1623
+ !this.isShapeHidden(id)
1624
+ );
1625
+ });
1626
+ this.shapeRenderVisibleSignals.set(id, signal);
1627
+ }
1628
+ return signal;
1629
+ }
1630
+
1631
+ getShapeDocumentVisibleSignal(id: ShapeId): Signal<boolean> {
1632
+ let signal = this.shapeDocumentVisibleSignals.get(id);
1633
+ if (!signal) {
1634
+ signal = computed<boolean>(`editor.shapeDocumentVisible.${id}`, () => {
1635
+ const shape = this.getShapeSignal(id).get();
1636
+ return !!shape && !this.isShapeHidden(shape.id);
1637
+ });
1638
+ this.shapeDocumentVisibleSignals.set(id, signal);
1639
+ }
1640
+ return signal;
1641
+ }
1642
+
1643
+ getShapeEditingSessionForShapeSignal(
1644
+ id: ShapeId,
1645
+ ): Signal<ShapeEditingSession | null> {
1646
+ let signal = this.shapeEditingSessionSignals.get(id);
1647
+ if (!signal) {
1648
+ signal = computed<ShapeEditingSession | null>(
1649
+ `editor.shapeEditingSessionForShape.${id}`,
1650
+ () => {
1651
+ const session = this.shapeEditingSessionSignal.get();
1652
+ return session?.shapeId === id ? session : null;
1653
+ },
1654
+ );
1655
+ this.shapeEditingSessionSignals.set(id, signal);
1656
+ }
1657
+ return signal;
1658
+ }
1659
+
1660
+ getShapeDropTargetSignal(id: ShapeId): Signal<boolean> {
1661
+ let signal = this.shapeDropTargetSignals.get(id);
1662
+ if (!signal) {
1663
+ signal = computed<boolean>(`editor.shapeDropTarget.${id}`, () => {
1664
+ return this.dropTargetShapeIdSignal.get() === id;
1665
+ });
1666
+ this.shapeDropTargetSignals.set(id, signal);
1667
+ }
1668
+ return signal;
1669
+ }
1670
+
1671
+ getShapeErasingSignal(id: ShapeId): Signal<boolean> {
1672
+ let signal = this.shapeErasingSignals.get(id);
1673
+ if (!signal) {
1674
+ signal = computed<boolean>(`editor.shapeErasing.${id}`, () => {
1675
+ const erasingShapeIds = new Set(this.erasingShapeIdsSignal.get());
1676
+ return this.hierarchy.isShapeOrAncestorInSet(id, erasingShapeIds);
1677
+ });
1678
+ this.shapeErasingSignals.set(id, signal);
1679
+ }
1680
+ return signal;
1681
+ }
1682
+
1683
+ getShapeSelectedDescendantTransformAncestorSignal(
1684
+ id: ShapeId,
1685
+ ): Signal<boolean> {
1686
+ let signal = this.shapeSelectedDescendantTransformAncestorSignals.get(id);
1687
+ if (!signal) {
1688
+ signal = computed<boolean>(
1689
+ `editor.shapeSelectedDescendantTransformAncestor.${id}`,
1690
+ () => {
1691
+ if (this.getShapeSelectedSignal(id).get()) {
1692
+ return false;
1693
+ }
1694
+ const shape = this.getShapeSignal(id).get();
1695
+ if (
1696
+ !shape ||
1697
+ !this.getShapeUtil(shape).shouldTransformDescendants(shape)
1698
+ ) {
1699
+ return false;
1700
+ }
1701
+ return this.selectedShapeIdsSignal
1702
+ .get()
1703
+ .some((selectedShapeId) =>
1704
+ this.hierarchy.isShapeDescendantOf(selectedShapeId, id),
1705
+ );
1706
+ },
1707
+ );
1708
+ this.shapeSelectedDescendantTransformAncestorSignals.set(id, signal);
1709
+ }
1710
+ return signal;
1711
+ }
1712
+
1713
+ getBinding(id: BindingId): BindingRecord | undefined {
1714
+ const record = this._internalStore.get(id);
1715
+ return isBindingRecord(record) ? record : undefined;
1716
+ }
1717
+
1718
+ getShapesForPage(pageId: PageId): ShapeRecord[] {
1719
+ return this.queries.getShapesForPage(pageId);
1720
+ }
1721
+
1722
+ getBindingsFromShape(shapeId: ShapeId): BindingRecord[] {
1723
+ return this.queries.getBindingsFromShape(shapeId);
1724
+ }
1725
+
1726
+ getBindingsToShape(shapeId: ShapeId): BindingRecord[] {
1727
+ return this.queries.getBindingsToShape(shapeId);
1728
+ }
1729
+
1730
+ getBindingsForShape(shapeId: ShapeId): BindingRecord[] {
1731
+ return this.queries.getBindingsForShape(shapeId);
1732
+ }
1733
+
1734
+ getChildShapeIds(parentId: ShapeParentId): ShapeId[] {
1735
+ return this.queries.getChildShapeIds(parentId);
1736
+ }
1737
+
1738
+ getChildShapes(parentId: ShapeParentId): ShapeRecord[] {
1739
+ return this.queries.getChildShapes(parentId);
1740
+ }
1741
+
1742
+ getDescendantShapeIds(parentId: ShapeParentId): ShapeId[] {
1743
+ return this.queries.getDescendantShapeIds(parentId);
1744
+ }
1745
+
1746
+ getDescendantPageBounds(parentId: ShapeParentId): Box | null {
1747
+ return this.queries.getDescendantPageBounds(parentId);
1748
+ }
1749
+
1750
+ getShapeSubtreePageBounds(shapeId: ShapeId): Box | null {
1751
+ return this.queries.getShapeSubtreePageBounds(shapeId);
1752
+ }
1753
+
1754
+ getShapePageId(shapeId: ShapeId): PageId | null {
1755
+ return this.queries.getPageIdForShape(shapeId);
1756
+ }
1757
+
1758
+ isShapeOrAncestorLocked(shapeId: ShapeId): boolean {
1759
+ return this.hierarchy.isShapeOrAncestorLocked(shapeId);
1760
+ }
1761
+
1762
+ hasLockedDescendant(shapeId: ShapeId): boolean {
1763
+ return this.hierarchy.hasLockedDescendant(shapeId);
1764
+ }
1765
+
1766
+ getOutermostSelectableShapeId(shapeId: ShapeId): ShapeId {
1767
+ return this.hierarchy.getOutermostSelectableShapeId(shapeId);
1768
+ }
1769
+
1770
+ getShapeIdsIncludingDescendants(shapeIds: Iterable<ShapeId>): ShapeId[] {
1771
+ return this.hierarchy.getShapeIdsIncludingDescendants(shapeIds);
1772
+ }
1773
+
1774
+ getShapeIdsForSelectionTransform(shapeIds: Iterable<ShapeId>): ShapeId[] {
1775
+ return this.hierarchy.getShapeIdsForSelectionTransform(shapeIds);
1776
+ }
1777
+
1778
+ getShapeIdsForSelectionRotation(shapeIds: Iterable<ShapeId>): ShapeId[] {
1779
+ return this.hierarchy.getShapeIdsForSelectionRotation(shapeIds);
1780
+ }
1781
+
1782
+ getSelectionTransformTargetShapes(): ShapeRecord[] {
1783
+ return this.hierarchy.getSelectionTransformTargetShapes();
1784
+ }
1785
+
1786
+ getSelectionRotationTargetShapes(): ShapeRecord[] {
1787
+ return this.hierarchy.getSelectionRotationTargetShapes();
1788
+ }
1789
+
1790
+ getShapeExportTraversalIds(shapeIds: Iterable<ShapeId>): ShapeId[] {
1791
+ return this.hierarchy.getShapeExportTraversalIds(shapeIds);
1792
+ }
1793
+
1794
+ getShapeContainment(
1795
+ containerId: ShapeId,
1796
+ shapeId: ShapeId,
1797
+ ): ShapeContainment | null {
1798
+ return this.queries.getShapeContainment(containerId, shapeId);
1799
+ }
1800
+
1801
+ getContainingShapeParentId(
1802
+ shapeIds: Iterable<ShapeId>,
1803
+ ): ShapeParentId | null {
1804
+ return this.queries.getContainingShapeParentId(shapeIds);
1805
+ }
1806
+
1807
+ getContainingParentIdForShapeRecord(shape: ShapeRecord): ShapeParentId | null {
1808
+ return this.queries.getContainingParentIdForShapeRecord(shape);
1809
+ }
1810
+
1811
+ getShapeClipAncestorIds(shapeId: ShapeId): ShapeId[] {
1812
+ return this.queries.getShapeClipAncestorIds(shapeId);
1813
+ }
1814
+
1815
+ getShapeClipAncestorIdsSignal(shapeId: ShapeId): Signal<ShapeId[]> {
1816
+ return this.queries.getShapeClipAncestorIdsSignal(shapeId);
1817
+ }
1818
+
1819
+ isPagePointInShapeClip(shapeId: ShapeId, point: Vec): boolean {
1820
+ return this.queries.isPagePointInShapeClip(shapeId, point);
1821
+ }
1822
+
1823
+ getShapeClippedPageBounds(shapeId: ShapeId): Box | null {
1824
+ return this.queries.getShapeClippedPageBounds(shapeId);
1825
+ }
1826
+
1827
+ getShapeIdsInContainerBounds(
1828
+ containerId: ShapeId,
1829
+ options: ContainerBoundsQueryOptions = {},
1830
+ ): ShapeId[] {
1831
+ return this.hierarchy.getShapeIdsInContainerBounds(containerId, options);
1832
+ }
1833
+
1834
+ canUseShapeParent(parentId: ShapeParentId, childId?: ShapeId): boolean {
1835
+ return this.hierarchy.canUseShapeParent(parentId, childId);
1836
+ }
1837
+
1838
+ getShapeUtil<S extends ShapeRecord>(shape: S): ShapeUtil<S> {
1839
+ return this.shapeUtils.get(shape.type);
1840
+ }
1841
+
1842
+ getBindingUtil<B extends BindingRecord>(binding: B): BindingUtil<B> {
1843
+ return this.bindingUtils.get(binding.type);
1844
+ }
1845
+
1846
+ // Pure geometry helpers operate only on the provided shape snapshot.
1847
+ computeShapeBounds(shape: ShapeRecord): Box {
1848
+ return this.geometry.computeShapeBounds(shape);
1849
+ }
1850
+
1851
+ computeShapeLocalBounds(shape: ShapeRecord): Box {
1852
+ return this.geometry.computeShapeLocalBounds(shape);
1853
+ }
1854
+
1855
+ computeShapeRenderBounds(shape: ShapeRecord): Box {
1856
+ return this.geometry.computeShapeRenderBounds(shape);
1857
+ }
1858
+
1859
+ computeShapeExportBounds(shape: ShapeRecord): Box {
1860
+ return this.geometry.computeShapeExportBounds(shape);
1861
+ }
1862
+
1863
+ computeShapeLocalToPageTransform(shape: ShapeRecord): Mat {
1864
+ return this.geometry.computeShapeLocalToPageTransform(shape);
1865
+ }
1866
+
1867
+ computeShapeRenderToPageTransform(shape: ShapeRecord): Mat {
1868
+ return this.geometry.computeShapeRenderToPageTransform(shape);
1869
+ }
1870
+
1871
+ computeShapeExportToPageTransform(shape: ShapeRecord): Mat {
1872
+ return this.geometry.computeShapeExportToPageTransform(shape);
1873
+ }
1874
+
1875
+ computeShapePageBounds(shape: ShapeRecord): Box {
1876
+ return this.geometry.computeShapePageBounds(shape);
1877
+ }
1878
+
1879
+ computeShapeExportPageBounds(shape: ShapeRecord): Box {
1880
+ return this.geometry.computeShapeExportPageBounds(shape);
1881
+ }
1882
+
1883
+ computeShapeGeometry(shape: ShapeRecord): ShapeGeometry {
1884
+ return this.geometry.computeShapeGeometry(shape);
1885
+ }
1886
+
1887
+ computeShapePositionedToPageTransform(shape: ShapeRecord): Mat {
1888
+ return this.geometry.computeShapePositionedToPageTransform(shape);
1889
+ }
1890
+
1891
+ computeShapePageCenter(shape: ShapeRecord): Vec {
1892
+ return this.geometry.computeShapePageCenter(shape);
1893
+ }
1894
+
1895
+ computeShapePageCorners(shape: ShapeRecord): Vec[] {
1896
+ return this.geometry.computeShapePageCorners(shape);
1897
+ }
1898
+
1899
+ computeShapePageOutlineVertices(shape: ShapeRecord): Vec[] {
1900
+ return this.geometry.computeShapePageOutlineVertices(shape);
1901
+ }
1902
+
1903
+ // Query helpers always read the current editor state for a shape id.
1904
+ getShapeBounds(shapeId: ShapeId): Box | null {
1905
+ return this.geometry.getShapeBounds(shapeId);
1906
+ }
1907
+
1908
+ getShapePageBounds(shapeId: ShapeId): Box | null {
1909
+ return this.geometry.getShapePageBounds(shapeId);
1910
+ }
1911
+
1912
+ getShapeGeometry(shapeId: ShapeId): ShapeGeometry | null {
1913
+ return this.geometry.getShapeGeometry(shapeId);
1914
+ }
1915
+
1916
+ getShapePositionedToPageTransform(shapeId: ShapeId): Mat | null {
1917
+ return this.geometry.getShapePositionedToPageTransform(shapeId);
1918
+ }
1919
+
1920
+ getShapePageCenter(shapeId: ShapeId): Vec | null {
1921
+ return this.geometry.getShapePageCenter(shapeId);
1922
+ }
1923
+
1924
+ getShapePageCorners(shapeId: ShapeId): Vec[] | null {
1925
+ return this.geometry.getShapePageCorners(shapeId);
1926
+ }
1927
+
1928
+ getShapePageOutlineVertices(shapeId: ShapeId): Vec[] {
1929
+ return this.geometry.getShapePageOutlineVertices(shapeId);
1930
+ }
1931
+
1932
+ getSelectionPageBounds(): Box | null {
1933
+ return this.geometry.getSelectionPageBounds();
1934
+ }
1935
+
1936
+ getSelectionFrame():
1937
+ | SelectionFrame
1938
+ | null {
1939
+ return this.geometry.getSelectionFrame();
1940
+ }
1941
+
1942
+ isPointInSelectionBounds(point: Vec): boolean {
1943
+ return this.geometry.isPointInSelectionBounds(point);
1944
+ }
1945
+
1946
+ getSelectionFrameLocalBoundsPoint(point: Vec): Vec | null {
1947
+ return this.geometry.getSelectionFrameLocalBoundsPoint(point);
1948
+ }
1949
+
1950
+ getSelectionPageOutlineVertices(): Vec[] {
1951
+ return this.geometry.getSelectionPageOutlineVertices();
1952
+ }
1953
+
1954
+ getSelectionToolbarAnchorPagePoint(): Vec | null {
1955
+ return this.geometry.getSelectionToolbarAnchorPagePoint();
1956
+ }
1957
+
1958
+ hitTestSelectionHandle(point: Vec): SelectionHandle | null {
1959
+ const selectedShapes = this.selectedShapesSignal.get();
1960
+ const selectionFrame = this.getSelectionFrame();
1961
+ if (selectedShapes.length === 0) {
1962
+ return null;
1963
+ }
1964
+ const isOnlySelected = selectedShapes.length === 1;
1965
+ const {
1966
+ terminalHandles,
1967
+ middleHandles,
1968
+ customHandles,
1969
+ rotateHandles,
1970
+ resizeHandles,
1971
+ } =
1972
+ getSelectionHandleState(
1973
+ selectedShapes,
1974
+ selectionFrame,
1975
+ this.getCurrentCamera().z,
1976
+ (shape) =>
1977
+ this.getShapeUtil(shape).getSelectionHandleAnchors(shape, {
1978
+ isOnlySelected,
1979
+ editor: this,
1980
+ }),
1981
+ (shape) =>
1982
+ this.getShapeUtil(shape).canUseSelectionFrameHandles(shape, {
1983
+ editor: this,
1984
+ }),
1985
+ );
1986
+
1987
+ for (const handle of middleHandles) {
1988
+ if (
1989
+ handle.bounds
1990
+ .expand(
1991
+ SELECTION_HANDLE_HIT_PADDING_SCREEN / this.getCurrentCamera().z,
1992
+ )
1993
+ .containsPoint(point)
1994
+ ) {
1995
+ return handle;
1996
+ }
1997
+ }
1998
+
1999
+ for (const handle of terminalHandles) {
2000
+ if (
2001
+ handle.bounds
2002
+ .expand(
2003
+ SELECTION_HANDLE_HIT_PADDING_SCREEN / this.getCurrentCamera().z,
2004
+ )
2005
+ .containsPoint(point)
2006
+ ) {
2007
+ return handle;
2008
+ }
2009
+ }
2010
+
2011
+ for (const handle of customHandles) {
2012
+ if (
2013
+ handle.bounds
2014
+ .expand(SELECTION_HANDLE_HIT_PADDING_SCREEN / this.getCurrentCamera().z)
2015
+ .containsPoint(point)
2016
+ ) {
2017
+ return handle;
2018
+ }
2019
+ }
2020
+
2021
+ for (const handle of resizeHandles) {
2022
+ if (handle.bounds.containsPoint(point)) {
2023
+ return handle;
2024
+ }
2025
+ }
2026
+
2027
+ for (const handle of rotateHandles) {
2028
+ if (
2029
+ handle.bounds
2030
+ .expand(SELECTION_HANDLE_HIT_PADDING_SCREEN / this.getCurrentCamera().z)
2031
+ .containsPoint(point)
2032
+ ) {
2033
+ return handle;
2034
+ }
2035
+ }
2036
+
2037
+ return null;
2038
+ }
2039
+
2040
+ setBrushBox(brushBox: Box | null): void {
2041
+ this.transients.setBrushBox(brushBox);
2042
+ }
2043
+
2044
+ getErasingShapeIds(): ShapeId[] {
2045
+ return this.transients.getErasingShapeIds();
2046
+ }
2047
+
2048
+ setErasingShapeIds(shapeIds: readonly ShapeId[]): void {
2049
+ this.transients.setErasingShapeIds(shapeIds);
2050
+ }
2051
+
2052
+ clearErasingShapeIds(): void {
2053
+ this.transients.clearErasingShapeIds();
2054
+ }
2055
+
2056
+ getEraserTrail(): Vec[] {
2057
+ return this.transients.getEraserTrail();
2058
+ }
2059
+
2060
+ setEraserTrail(points: readonly Vec[]): void {
2061
+ this.transients.setEraserTrail(points);
2062
+ }
2063
+
2064
+ clearEraserTrail(): void {
2065
+ this.transients.clearEraserTrail();
2066
+ }
2067
+
2068
+ startLaserTrail(): number {
2069
+ return this.transients.startLaserTrail();
2070
+ }
2071
+
2072
+ setLaserTrail(sessionId: number, points: readonly Vec[]): void {
2073
+ this.transients.setLaserTrail(sessionId, points);
2074
+ }
2075
+
2076
+ finishLaserTrail(sessionId: number): void {
2077
+ this.transients.finishLaserTrail(sessionId);
2078
+ }
2079
+
2080
+ clearLaserTrail(sessionId: number): void {
2081
+ this.transients.clearLaserTrail(sessionId);
2082
+ }
2083
+
2084
+ setBindingPreview(preview: ArrowBindingPreviewState | null): void {
2085
+ this.transients.setBindingPreview(preview);
2086
+ }
2087
+
2088
+ setDropTargetShapeId(shapeId: ShapeId | null): void {
2089
+ this.transients.setDropTargetShapeId(shapeId);
2090
+ }
2091
+
2092
+ setSnapIndicators(indicators: SnapIndicatorState[] | null): void {
2093
+ this.transients.setSnapIndicators(indicators);
2094
+ }
2095
+
2096
+ setArrowTerminalBindingResolver(
2097
+ resolver: ArrowTerminalBindingResolver,
2098
+ ): void {
2099
+ this.transients.setArrowTerminalBindingResolver(resolver);
2100
+ }
2101
+
2102
+ resolveArrowTerminalBinding(
2103
+ input: ArrowTerminalBindingResolverInput,
2104
+ ): ResolvedArrowTerminalBinding {
2105
+ return this.transients.resolveArrowTerminalBinding(input);
2106
+ }
2107
+
2108
+ clearSelectionTransients(): void {
2109
+ this.transients.clearSelectionTransients();
2110
+ }
2111
+
2112
+ hitTestShape(point: Vec): ShapeRecord | null {
2113
+ return this.hitTestShapesAtPoint(point)[0] ?? null;
2114
+ }
2115
+
2116
+ hitTestShapesAtPoint(
2117
+ point: Vec,
2118
+ options?: ShapeAtPointOptions,
2119
+ ): ShapeRecord[] {
2120
+ return this.spatialIndex.getShapesAtPoint(point, {
2121
+ ...options,
2122
+ filter: (shape) =>
2123
+ !this.isShapeHidden(shape.id) &&
2124
+ this.isPagePointInShapeClip(shape.id, point) &&
2125
+ (options?.filter?.(shape) ?? true),
2126
+ });
2127
+ }
2128
+
2129
+ hitTestShapesByLineSegment(
2130
+ start: Vec,
2131
+ end: Vec,
2132
+ options?: ShapeLineSegmentHitTestOptions,
2133
+ ): ShapeRecord[] {
2134
+ return this.spatialIndex.getShapesHitByLineSegment(start, end, {
2135
+ ...options,
2136
+ filter: (shape) =>
2137
+ !this.isShapeHidden(shape.id) && (options?.filter?.(shape) ?? true),
2138
+ });
2139
+ }
2140
+
2141
+ getShapeIdsInBounds(bounds: Box, options?: SpatialQueryOptions): ShapeId[] {
2142
+ return this.spatialIndex
2143
+ .getShapeIdsInBounds(bounds, options)
2144
+ .filter((shapeId) => !this.isShapeHidden(shapeId));
2145
+ }
2146
+
2147
+ getShapesInBounds(bounds: Box, options?: SpatialQueryOptions): ShapeRecord[] {
2148
+ return this.spatialIndex
2149
+ .getShapesInBounds(bounds, options)
2150
+ .filter((shape) => !this.isShapeHidden(shape.id));
2151
+ }
2152
+
2153
+ getShapesInViewport(
2154
+ bounds: Box,
2155
+ options?: SpatialQueryOptions,
2156
+ ): ShapeRecord[] {
2157
+ return this.spatialIndex
2158
+ .getShapesInViewport(bounds, options)
2159
+ .filter((shape) => !this.isShapeHidden(shape.id));
2160
+ }
2161
+
2162
+ getDocumentSnapshot(): DocumentSnapshot {
2163
+ return this.snapshots.getDocumentSnapshot();
2164
+ }
2165
+
2166
+ getSessionSnapshot(): SessionSnapshot {
2167
+ return this.snapshots.getSessionSnapshot();
2168
+ }
2169
+
2170
+ getSvgString(
2171
+ shapeIds: Iterable<ShapeId> = this.getSelectedShapeIds(),
2172
+ options?: SvgExportOptions,
2173
+ ): SvgExportResult | null {
2174
+ return exportShapesToSvgString(
2175
+ this,
2176
+ shapeIds,
2177
+ this.shapeSvgExporters,
2178
+ options,
2179
+ );
2180
+ }
2181
+
2182
+ getExportIntegrityReport(
2183
+ shapeIds: Iterable<ShapeId> = this.getSelectedShapeIds(),
2184
+ ): ExportIntegrityReport {
2185
+ return getExportIntegrityReport({
2186
+ editor: this,
2187
+ exporters: this.shapeSvgExporters,
2188
+ shapeIds,
2189
+ });
2190
+ }
2191
+
2192
+ repairExportIntegrity(
2193
+ shapeIds: Iterable<ShapeId> = this.getSelectedShapeIds(),
2194
+ ): ExportIntegrityRepairResult {
2195
+ return repairExportIntegrity({
2196
+ editor: this,
2197
+ exporters: this.shapeSvgExporters,
2198
+ shapeIds,
2199
+ });
2200
+ }
2201
+
2202
+ getShapeCapabilityManifest(): ShapeCapabilityManifest {
2203
+ return getShapeCapabilityManifest({
2204
+ definitions: this.shapeCapabilityDefinitions,
2205
+ exporters: this.shapeSvgExporters,
2206
+ shapeUtils: this.shapeUtils,
2207
+ });
2208
+ }
2209
+
2210
+ createPageRecord(input?: {
2211
+ name?: string;
2212
+ index?: string;
2213
+ meta?: Record<string, unknown>;
2214
+ }): PageRecord {
2215
+ const pageNumber = this.nextPageNumber;
2216
+ return {
2217
+ id: this.createPageId(pageNumber),
2218
+ typeName: "page",
2219
+ scope: "document",
2220
+ name: input?.name ?? `Page ${pageNumber}`,
2221
+ index: input?.index ?? this.createPageIndex(pageNumber),
2222
+ meta: input?.meta ?? {},
2223
+ };
2224
+ }
2225
+
2226
+ loadDocumentSnapshot(
2227
+ snapshot: unknown,
2228
+ options?: { source?: ChangeSource },
2229
+ ): void {
2230
+ this.snapshots.loadDocumentSnapshot(snapshot, options);
2231
+ }
2232
+
2233
+ loadSessionSnapshot(
2234
+ snapshot: unknown,
2235
+ options?: { source?: ChangeSource },
2236
+ ): void {
2237
+ this.snapshots.loadSessionSnapshot(snapshot, options);
2238
+ }
2239
+
2240
+ syncAfterDocumentRestore(): void {
2241
+ this.snapshots.syncAfterDocumentRestore();
2242
+ }
2243
+
2244
+ cancelInteractionForCommand(): void {
2245
+ this.cancelInteractionAndClearTransients("command");
2246
+ }
2247
+
2248
+ cancelInteractionAndClearTransients(
2249
+ reason: CancelEditorEvent["reason"],
2250
+ ): void {
2251
+ this.tools.cancel(reason);
2252
+ this.inputs.resetPointer();
2253
+ this.clearSelectionTransients();
2254
+ }
2255
+
2256
+ syncToolStateFromSession(source: ChangeSource): void {
2257
+ const { activeToolId, isToolLocked } = this.sessionSignal.get();
2258
+
2259
+ if (this.tools.hasTool(activeToolId)) {
2260
+ this.tools.setActiveTool(activeToolId, "restore");
2261
+ } else {
2262
+ this.tools.setActiveTool("select", "restore");
2263
+ this.updateSession({ activeToolId: "select" }, source);
2264
+ }
2265
+
2266
+ this.tools.setToolLocked(isToolLocked);
2267
+ }
2268
+
2269
+ createShapeRecord(input: CreateShapeInput): ShapeRecord {
2270
+ const parentId = input.parentId ?? this.sessionSignal.get().currentPageId;
2271
+ if (!this.canUseShapeParent(parentId)) {
2272
+ throw new Error(`Invalid parent id for shape`);
2273
+ }
2274
+ const shapeNumber = this.nextShapeNumber;
2275
+ const id = this.createShapeId(shapeNumber);
2276
+ const util = this.shapeUtils.get(input.type);
2277
+ const props = util.validateProps({
2278
+ ...this.getDefaultShapeProps(input.type),
2279
+ ...input.props,
2280
+ });
2281
+
2282
+ return {
2283
+ id,
2284
+ typeName: "shape" as const,
2285
+ scope: "document" as const,
2286
+ type: input.type,
2287
+ parentId,
2288
+ index: input.index ?? this.createShapeIndex(shapeNumber),
2289
+ x: input.x,
2290
+ y: input.y,
2291
+ rotation: input.rotation ?? 0,
2292
+ scaleX: input.scaleX ?? 1,
2293
+ scaleY: input.scaleY ?? 1,
2294
+ opacity: input.opacity ?? 1,
2295
+ isHidden: input.isHidden ?? false,
2296
+ isLocked: input.isLocked ?? false,
2297
+ props,
2298
+ meta: input.meta ?? {},
2299
+ } as ShapeRecord;
2300
+ }
2301
+
2302
+ createBindingRecord(input: CreateBindingInput): BindingRecord {
2303
+ const bindingNumber = this.nextBindingNumber;
2304
+ const id = input.id ?? this.createBindingId(bindingNumber);
2305
+ const util = this.bindingUtils.get(input.type);
2306
+ const props = util.validateProps({
2307
+ ...util.getDefaultProps(),
2308
+ ...input.props,
2309
+ });
2310
+
2311
+ return {
2312
+ id,
2313
+ typeName: "binding",
2314
+ scope: "document",
2315
+ type: input.type,
2316
+ fromId: input.fromId,
2317
+ toId: input.toId,
2318
+ props,
2319
+ meta: input.meta ?? {},
2320
+ } as BindingRecord;
2321
+ }
2322
+
2323
+ createAssetRecord(input: CreateAssetInput): AssetRecord {
2324
+ const assetNumber = this.nextAssetNumber;
2325
+ const id = input.id ?? this.createAssetId(assetNumber);
2326
+
2327
+ return {
2328
+ id,
2329
+ typeName: "asset",
2330
+ scope: "document",
2331
+ type: input.type,
2332
+ props: input.props ?? {},
2333
+ meta: input.meta ?? {},
2334
+ } as AssetRecord;
2335
+ }
2336
+
2337
+ updateSession(
2338
+ partial: Partial<SessionRecord>,
2339
+ source: ChangeSource = "system",
2340
+ ): void {
2341
+ const current = this.sessionSignal.get();
2342
+ this._internalStore.put(
2343
+ [
2344
+ {
2345
+ ...current,
2346
+ ...partial,
2347
+ },
2348
+ ],
2349
+ source,
2350
+ );
2351
+ }
2352
+
2353
+ setCurrentPage(pageId: PageId, source: ChangeSource = "system"): void {
2354
+ if (!this.getPage(pageId)) {
2355
+ return;
2356
+ }
2357
+
2358
+ if (pageId !== this.getCurrentPageId()) {
2359
+ this.cancelInteractionAndClearTransients("page_switch");
2360
+ this.completeEditing(source);
2361
+ }
2362
+
2363
+ const session = this.sessionSignal.get();
2364
+ this.updateSession(
2365
+ {
2366
+ currentPageId: pageId,
2367
+ pageCameraById: {
2368
+ ...session.pageCameraById,
2369
+ [pageId]: session.pageCameraById[pageId] ?? { ...DEFAULT_CAMERA },
2370
+ },
2371
+ pageSelectedShapeIdsById: {
2372
+ ...session.pageSelectedShapeIdsById,
2373
+ [pageId]: session.pageSelectedShapeIdsById[pageId] ?? [],
2374
+ },
2375
+ },
2376
+ source,
2377
+ );
2378
+ }
2379
+
2380
+ setSelectedShapeIds(
2381
+ ids: ShapeId[],
2382
+ options?: { pageId?: PageId; source?: ChangeSource },
2383
+ ): void {
2384
+ this.selection.setSelectedShapeIds(ids, options);
2385
+ }
2386
+
2387
+ removeShapeIdsFromSelection(
2388
+ ids: ShapeId[],
2389
+ source: ChangeSource = "system",
2390
+ ): void {
2391
+ this.selection.removeShapeIdsFromSelection(ids, source);
2392
+ }
2393
+
2394
+ removeShapeIdsFromEditingSession(
2395
+ ids: ShapeId[],
2396
+ source: ChangeSource = "system",
2397
+ ): void {
2398
+ this.editing.removeShapeIds(ids, source);
2399
+ }
2400
+
2401
+ ensurePageSessionState(
2402
+ pageId: PageId,
2403
+ source: ChangeSource = "system",
2404
+ ): void {
2405
+ const session = this.sessionSignal.get();
2406
+ const nextCamera = session.pageCameraById[pageId] ?? { ...DEFAULT_CAMERA };
2407
+ const nextSelection = session.pageSelectedShapeIdsById[pageId] ?? [];
2408
+
2409
+ this.updateSession(
2410
+ {
2411
+ pageCameraById: {
2412
+ ...session.pageCameraById,
2413
+ [pageId]: nextCamera,
2414
+ },
2415
+ pageSelectedShapeIdsById: {
2416
+ ...session.pageSelectedShapeIdsById,
2417
+ [pageId]: nextSelection,
2418
+ },
2419
+ },
2420
+ source,
2421
+ );
2422
+ }
2423
+
2424
+ removePageSessionState(
2425
+ pageId: PageId,
2426
+ nextCurrentPageId: PageId,
2427
+ source: ChangeSource = "system",
2428
+ ): void {
2429
+ const session = this.sessionSignal.get();
2430
+ const nextCameraById = { ...session.pageCameraById };
2431
+ const nextSelectionById = { ...session.pageSelectedShapeIdsById };
2432
+ delete nextCameraById[pageId];
2433
+ delete nextSelectionById[pageId];
2434
+
2435
+ this.updateSession(
2436
+ {
2437
+ currentPageId: nextCurrentPageId,
2438
+ pageCameraById: nextCameraById,
2439
+ pageSelectedShapeIdsById: nextSelectionById,
2440
+ },
2441
+ source,
2442
+ );
2443
+ }
2444
+
2445
+ setCamera(
2446
+ partial: Partial<Camera>,
2447
+ options?: { pageId?: PageId; source?: ChangeSource },
2448
+ ): void {
2449
+ this.camera.setCamera(partial, options);
2450
+ }
2451
+
2452
+ panCamera(delta: Vec, source: ChangeSource = "system"): void {
2453
+ this.camera.panCamera(delta, source);
2454
+ }
2455
+
2456
+ zoomCameraAtScreenPoint(
2457
+ screenPoint: Vec,
2458
+ deltaY: number,
2459
+ source: ChangeSource = "system",
2460
+ ): void {
2461
+ this.camera.zoomCameraAtScreenPoint(screenPoint, deltaY, source);
2462
+ }
2463
+
2464
+ zoomCameraToScreenPoint(
2465
+ screenPoint: Vec,
2466
+ zoom: number,
2467
+ source: ChangeSource = "system",
2468
+ ): void {
2469
+ this.camera.zoomCameraToScreenPoint(screenPoint, zoom, source);
2470
+ }
2471
+
2472
+ zoomAtViewportCenter(deltaY: number, source: ChangeSource = "system"): void {
2473
+ this.camera.zoomAtViewportCenter(deltaY, source);
2474
+ }
2475
+
2476
+ resetZoom(source: ChangeSource = "user"): void {
2477
+ this.camera.resetZoom(source);
2478
+ }
2479
+
2480
+ setViewportScreenSize(size: { w: number; h: number }): void {
2481
+ this.camera.setViewportScreenSize(size);
2482
+ }
2483
+
2484
+ /** Combined page bounds of every shape on a page, or null when empty. */
2485
+ getCurrentPageBounds(pageId: PageId = this.getCurrentPageId()): Box | null {
2486
+ return this.selectionLayout.getCurrentPageBounds(pageId);
2487
+ }
2488
+
2489
+ /**
2490
+ * Move and zoom the camera so `bounds` fits inside the viewport with optional
2491
+ * padding (screen px). Returns false when the viewport or bounds are empty.
2492
+ */
2493
+ zoomToBounds(
2494
+ bounds: Box,
2495
+ options?: { padding?: number; maxZoom?: number; source?: ChangeSource },
2496
+ ): boolean {
2497
+ return this.selectionLayout.zoomToBounds(bounds, options);
2498
+ }
2499
+
2500
+ /** Fit all shapes on the current page into the viewport. */
2501
+ zoomToFit(options?: { padding?: number; source?: ChangeSource }): boolean {
2502
+ return this.selectionLayout.zoomToFit(options);
2503
+ }
2504
+
2505
+ /** Fit the current selection into the viewport. */
2506
+ zoomToSelection(options?: {
2507
+ padding?: number;
2508
+ source?: ChangeSource;
2509
+ }): boolean {
2510
+ return this.selectionLayout.zoomToSelection(options);
2511
+ }
2512
+
2513
+ private createShapeId(shapeNumber: number): ShapeId {
2514
+ const id = `shape:${shapeNumber}` as ShapeId;
2515
+ this.nextShapeNumber += 1;
2516
+ return id;
2517
+ }
2518
+
2519
+ createShapeIndex(shapeNumber: number): string {
2520
+ return String(shapeNumber).padStart(6, "0");
2521
+ }
2522
+
2523
+ private createBindingId(bindingNumber: number): BindingId {
2524
+ const id = `binding:${bindingNumber}` as BindingId;
2525
+ this.nextBindingNumber += 1;
2526
+ return id;
2527
+ }
2528
+
2529
+ private createAssetId(assetNumber: number): AssetId {
2530
+ const id = `asset:${assetNumber}` as AssetId;
2531
+ this.nextAssetNumber += 1;
2532
+ return id;
2533
+ }
2534
+
2535
+ syncShapeCounter(): void {
2536
+ let maxShapeNumber = 0;
2537
+ for (const record of this._internalStore.getAll()) {
2538
+ if (!isShapeRecord(record)) continue;
2539
+ const value = Number(record.id.split(":")[1] ?? 0);
2540
+ if (Number.isFinite(value)) {
2541
+ maxShapeNumber = Math.max(maxShapeNumber, value);
2542
+ }
2543
+ }
2544
+ this.nextShapeNumber = maxShapeNumber + 1;
2545
+ }
2546
+
2547
+ syncPageCounter(): void {
2548
+ let maxPageNumber = 0;
2549
+ for (const record of this._internalStore.getAll()) {
2550
+ if (!isPageRecord(record)) continue;
2551
+ const suffix = record.id.split(":")[1] ?? "";
2552
+ const value = Number(suffix === "default" ? 1 : suffix);
2553
+ if (Number.isFinite(value)) {
2554
+ maxPageNumber = Math.max(maxPageNumber, value);
2555
+ }
2556
+ }
2557
+ this.nextPageNumber = maxPageNumber + 1;
2558
+ }
2559
+
2560
+ syncBindingCounter(): void {
2561
+ let maxBindingNumber = 0;
2562
+ for (const record of this._internalStore.getAll()) {
2563
+ if (!isBindingRecord(record)) continue;
2564
+ const value = Number(record.id.split(":")[1] ?? 0);
2565
+ if (Number.isFinite(value)) {
2566
+ maxBindingNumber = Math.max(maxBindingNumber, value);
2567
+ }
2568
+ }
2569
+ this.nextBindingNumber = maxBindingNumber + 1;
2570
+ }
2571
+
2572
+ syncAssetCounter(): void {
2573
+ let maxAssetNumber = 0;
2574
+ for (const record of this._internalStore.getAll()) {
2575
+ if (!isAssetRecord(record)) continue;
2576
+ const value = Number(record.id.split(":")[1] ?? 0);
2577
+ if (Number.isFinite(value)) {
2578
+ maxAssetNumber = Math.max(maxAssetNumber, value);
2579
+ }
2580
+ }
2581
+ this.nextAssetNumber = maxAssetNumber + 1;
2582
+ }
2583
+
2584
+ private createPageId(pageNumber: number): PageId {
2585
+ const id = `page:${pageNumber}` as PageId;
2586
+ this.nextPageNumber += 1;
2587
+ return id;
2588
+ }
2589
+
2590
+ private createPageIndex(pageNumber: number): string {
2591
+ return String(pageNumber).padStart(4, "0");
2592
+ }
2593
+
2594
+ private handleWheelEvent(
2595
+ event: Extract<EditorEvent, { type: "wheel" }>,
2596
+ ): void {
2597
+ if (event.ctrlKey) {
2598
+ const current = this.getCurrentCamera();
2599
+ const deltaY = event.delta.y;
2600
+ const zoomDelta =
2601
+ (Math.abs(deltaY) > CAMERA_WHEEL_MAX_ZOOM_STEP
2602
+ ? CAMERA_WHEEL_MAX_ZOOM_STEP * Math.sign(deltaY)
2603
+ : deltaY) / 100;
2604
+ this.zoomCameraToScreenPoint(
2605
+ event.screenPoint,
2606
+ current.z - zoomDelta * CAMERA_WHEEL_ZOOM_SPEED * current.z,
2607
+ );
2608
+ return;
2609
+ }
2610
+
2611
+ this.panCamera(event.delta);
2612
+ }
2613
+
2614
+ }