@kritzel/engine 0.4.31

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 (373) hide show
  1. package/LICENSE.md +50 -0
  2. package/dist/cjs/app-globals-V2Kpy_OQ.js +5 -0
  3. package/dist/cjs/engine.constants-CakmdKMW.js +28206 -0
  4. package/dist/cjs/index-BbbWU2aN.js +1742 -0
  5. package/dist/cjs/index.cjs.js +4232 -0
  6. package/dist/cjs/kritzel-awareness-cursors_5.cjs.entry.js +28004 -0
  7. package/dist/cjs/loader.cjs.js +13 -0
  8. package/dist/cjs/stencil.cjs.js +25 -0
  9. package/dist/collection/classes/core/core.class.js +1389 -0
  10. package/dist/collection/classes/core/reviver.class.js +124 -0
  11. package/dist/collection/classes/core/store.class.js +337 -0
  12. package/dist/collection/classes/core/viewport.class.js +794 -0
  13. package/dist/collection/classes/core/workspace.class.js +112 -0
  14. package/dist/collection/classes/handlers/base.handler.js +20 -0
  15. package/dist/collection/classes/handlers/context-menu.handler.js +170 -0
  16. package/dist/collection/classes/handlers/hover.handler.js +35 -0
  17. package/dist/collection/classes/handlers/key.handler.js +162 -0
  18. package/dist/collection/classes/handlers/line-handle.handler.js +475 -0
  19. package/dist/collection/classes/handlers/move.handler.js +295 -0
  20. package/dist/collection/classes/handlers/resize.handler.js +327 -0
  21. package/dist/collection/classes/handlers/rotation.handler.js +174 -0
  22. package/dist/collection/classes/handlers/selection.handler.js +526 -0
  23. package/dist/collection/classes/managers/anchor.manager.js +1476 -0
  24. package/dist/collection/classes/managers/browser-permission.manager.js +64 -0
  25. package/dist/collection/classes/managers/clipboard.manager.js +346 -0
  26. package/dist/collection/classes/managers/clipboard.types.js +1 -0
  27. package/dist/collection/classes/managers/cursor.manager.js +139 -0
  28. package/dist/collection/classes/managers/license.manager.js +341 -0
  29. package/dist/collection/classes/managers/localization.manager.js +227 -0
  30. package/dist/collection/classes/managers/theme.manager.js +206 -0
  31. package/dist/collection/classes/objects/base-object.class.js +462 -0
  32. package/dist/collection/classes/objects/dynamic-object.class.js +437 -0
  33. package/dist/collection/classes/objects/group.class.js +590 -0
  34. package/dist/collection/classes/objects/image.class.js +500 -0
  35. package/dist/collection/classes/objects/line.class.js +789 -0
  36. package/dist/collection/classes/objects/path.class.js +578 -0
  37. package/dist/collection/classes/objects/selection-box.class.js +32 -0
  38. package/dist/collection/classes/objects/selection-group.class.js +752 -0
  39. package/dist/collection/classes/objects/shape.class.js +628 -0
  40. package/dist/collection/classes/objects/text.class.js +461 -0
  41. package/dist/collection/classes/providers/assets/asset-resolver.class.js +536 -0
  42. package/dist/collection/classes/providers/assets/http-asset-provider.class.js +111 -0
  43. package/dist/collection/classes/providers/assets/indexeddb-asset-provider.class.js +241 -0
  44. package/dist/collection/classes/providers/assets/presigned-asset-provider.class.js +86 -0
  45. package/dist/collection/classes/providers/paste-intent/event-paste-intent.provider.js +48 -0
  46. package/dist/collection/classes/providers/paste-intent/ios-paste-intent.provider.js +16 -0
  47. package/dist/collection/classes/providers/paste-intent/navigator-paste-intent.provider.js +86 -0
  48. package/dist/collection/classes/providers/sync/broadcast-sync-provider.class.js +100 -0
  49. package/dist/collection/classes/providers/sync/hocuspocus-sync-provider.class.js +333 -0
  50. package/dist/collection/classes/providers/sync/in-memory-sync-provider.class.js +69 -0
  51. package/dist/collection/classes/providers/sync/indexeddb-sync-provider.class.js +39 -0
  52. package/dist/collection/classes/providers/sync/websocket-sync-provider.class.js +110 -0
  53. package/dist/collection/classes/registries/dynamic-object-renderer.registry.js +52 -0
  54. package/dist/collection/classes/registries/font-registry.class.js +161 -0
  55. package/dist/collection/classes/registries/icon-registry.class.js +118 -0
  56. package/dist/collection/classes/registries/paste-intent-provider.registry.js +26 -0
  57. package/dist/collection/classes/registries/tool.registry.js +69 -0
  58. package/dist/collection/classes/structures/app-state-map.structure.js +461 -0
  59. package/dist/collection/classes/structures/object-map.structure.js +1103 -0
  60. package/dist/collection/classes/structures/quadtree.structure.js +251 -0
  61. package/dist/collection/classes/tools/base-tool.class.js +88 -0
  62. package/dist/collection/classes/tools/brush-tool.class.js +236 -0
  63. package/dist/collection/classes/tools/eraser-tool.class.js +133 -0
  64. package/dist/collection/classes/tools/image-tool.class.js +152 -0
  65. package/dist/collection/classes/tools/line-tool.class.js +249 -0
  66. package/dist/collection/classes/tools/selection-tool.class.js +611 -0
  67. package/dist/collection/classes/tools/shape-tool.class.js +251 -0
  68. package/dist/collection/classes/tools/text-tool.class.js +179 -0
  69. package/dist/collection/collection-manifest.json +17 -0
  70. package/dist/collection/components/kritzel-awareness-cursors/kritzel-awareness-cursors.css +115 -0
  71. package/dist/collection/components/kritzel-awareness-cursors/kritzel-awareness-cursors.js +380 -0
  72. package/dist/collection/components/kritzel-cursor-trail/kritzel-cursor-trail.css +10 -0
  73. package/dist/collection/components/kritzel-cursor-trail/kritzel-cursor-trail.js +153 -0
  74. package/dist/collection/components/kritzel-engine/kritzel-engine.css +81 -0
  75. package/dist/collection/components/kritzel-engine/kritzel-engine.js +6345 -0
  76. package/dist/collection/components/kritzel-icon/kritzel-icon.css +18 -0
  77. package/dist/collection/components/kritzel-icon/kritzel-icon.js +93 -0
  78. package/dist/collection/components/kritzel-watermark/kritzel-watermark.css +29 -0
  79. package/dist/collection/components/kritzel-watermark/kritzel-watermark.js +83 -0
  80. package/dist/collection/configs/default-asset-storage.config.js +9 -0
  81. package/dist/collection/configs/default-brush-tool.config.js +9 -0
  82. package/dist/collection/configs/default-engine-config.js +73 -0
  83. package/dist/collection/configs/default-line-tool.config.js +12 -0
  84. package/dist/collection/configs/default-shape-tool.config.js +15 -0
  85. package/dist/collection/configs/default-sync.config.js +6 -0
  86. package/dist/collection/configs/default-text-tool.config.js +12 -0
  87. package/dist/collection/constants/color-palette.constants.js +40 -0
  88. package/dist/collection/constants/core.constants.js +1 -0
  89. package/dist/collection/constants/engine.constants.js +4 -0
  90. package/dist/collection/constants/license.constants.js +25 -0
  91. package/dist/collection/constants/schema.constants.js +4 -0
  92. package/dist/collection/constants/storage.constants.js +1 -0
  93. package/dist/collection/constants/stroke-size.constants.js +2 -0
  94. package/dist/collection/constants/text-tool-fonts.constants.js +18 -0
  95. package/dist/collection/constants/version.js +6 -0
  96. package/dist/collection/enums/alignment.enum.js +9 -0
  97. package/dist/collection/enums/event-button.enum.js +6 -0
  98. package/dist/collection/enums/handle-type.enum.js +7 -0
  99. package/dist/collection/enums/shape-type.enum.js +6 -0
  100. package/dist/collection/events/active-workspace-change.event.js +1 -0
  101. package/dist/collection/events/editor-is-ready.event.js +1 -0
  102. package/dist/collection/events/login.event.js +1 -0
  103. package/dist/collection/helpers/alignment.helper.js +73 -0
  104. package/dist/collection/helpers/class.helper.js +5 -0
  105. package/dist/collection/helpers/color.helper.js +106 -0
  106. package/dist/collection/helpers/cursor.helper.js +57 -0
  107. package/dist/collection/helpers/devices.helper.js +28 -0
  108. package/dist/collection/helpers/event.helper.js +58 -0
  109. package/dist/collection/helpers/geometry.helper.js +149 -0
  110. package/dist/collection/helpers/html.helper.js +116 -0
  111. package/dist/collection/helpers/image-file.helper.js +6 -0
  112. package/dist/collection/helpers/keyboard.helper.js +55 -0
  113. package/dist/collection/helpers/localization.helper.js +25 -0
  114. package/dist/collection/helpers/math.helper.js +8 -0
  115. package/dist/collection/helpers/migration.helper.js +45 -0
  116. package/dist/collection/helpers/object.helper.js +29 -0
  117. package/dist/collection/helpers/svg-export.helper.js +655 -0
  118. package/dist/collection/helpers/theme.helper.js +77 -0
  119. package/dist/collection/helpers/tool-config.helper.js +63 -0
  120. package/dist/collection/helpers/wheel.helper.js +113 -0
  121. package/dist/collection/index.js +107 -0
  122. package/dist/collection/interfaces/anchor.interface.js +1 -0
  123. package/dist/collection/interfaces/arrow-head.interface.js +1 -0
  124. package/dist/collection/interfaces/asset-provider.interface.js +1 -0
  125. package/dist/collection/interfaces/asset-storage-config.interface.js +1 -0
  126. package/dist/collection/interfaces/asset.interface.js +1 -0
  127. package/dist/collection/interfaces/bounding-box.interface.js +1 -0
  128. package/dist/collection/interfaces/clonable.interface.js +1 -0
  129. package/dist/collection/interfaces/context-menu-item.interface.js +1 -0
  130. package/dist/collection/interfaces/context-menu-state.interface.js +1 -0
  131. package/dist/collection/interfaces/debug-info.interface.js +1 -0
  132. package/dist/collection/interfaces/dialog.interface.js +1 -0
  133. package/dist/collection/interfaces/document-metadata.interface.js +1 -0
  134. package/dist/collection/interfaces/dynamic-object-renderer.interface.js +1 -0
  135. package/dist/collection/interfaces/engine-state.interface.js +1 -0
  136. package/dist/collection/interfaces/font.interface.js +1 -0
  137. package/dist/collection/interfaces/localization.interface.js +1 -0
  138. package/dist/collection/interfaces/login-config.interface.js +1 -0
  139. package/dist/collection/interfaces/master-detail.interface.js +1 -0
  140. package/dist/collection/interfaces/menu-item.interface.js +1 -0
  141. package/dist/collection/interfaces/migration.interface.js +1 -0
  142. package/dist/collection/interfaces/notification.interface.js +1 -0
  143. package/dist/collection/interfaces/object-change-event.interface.js +1 -0
  144. package/dist/collection/interfaces/object-export.interface.js +1 -0
  145. package/dist/collection/interfaces/object.interface.js +1 -0
  146. package/dist/collection/interfaces/paste-intent-provider.interface.js +1 -0
  147. package/dist/collection/interfaces/point.interface.js +1 -0
  148. package/dist/collection/interfaces/polygon.interface.js +1 -0
  149. package/dist/collection/interfaces/remote-cursor.interface.js +1 -0
  150. package/dist/collection/interfaces/selection-state.interface.js +1 -0
  151. package/dist/collection/interfaces/serializable.interface.js +1 -0
  152. package/dist/collection/interfaces/settings.interface.js +1 -0
  153. package/dist/collection/interfaces/share.interface.js +1 -0
  154. package/dist/collection/interfaces/shortcut.interface.js +1 -0
  155. package/dist/collection/interfaces/sync-config.interface.js +21 -0
  156. package/dist/collection/interfaces/sync-provider.interface.js +1 -0
  157. package/dist/collection/interfaces/theme.interface.js +1 -0
  158. package/dist/collection/interfaces/tool-config.interface.js +1 -0
  159. package/dist/collection/interfaces/tool-type.interface.js +1 -0
  160. package/dist/collection/interfaces/tool.interface.js +1 -0
  161. package/dist/collection/interfaces/toolbar-item.interface.js +1 -0
  162. package/dist/collection/interfaces/undo-state.interface.js +1 -0
  163. package/dist/collection/interfaces/user.interface.js +1 -0
  164. package/dist/collection/interfaces/viewport-state.interface.js +1 -0
  165. package/dist/collection/locales/de-locale.js +119 -0
  166. package/dist/collection/locales/en-locale.js +120 -0
  167. package/dist/collection/locales/fr-locale.js +119 -0
  168. package/dist/collection/migrations/app-state.migrations.js +47 -0
  169. package/dist/collection/migrations/workspace.migrations.js +67 -0
  170. package/dist/collection/themes/dark-theme.js +340 -0
  171. package/dist/collection/themes/light-theme.js +346 -0
  172. package/dist/collection/types/deep-readonly.type.js +1 -0
  173. package/dist/collection/types/state.types.js +1 -0
  174. package/dist/components/index.d.ts +47 -0
  175. package/dist/components/index.js +4143 -0
  176. package/dist/components/kritzel-awareness-cursors.d.ts +11 -0
  177. package/dist/components/kritzel-awareness-cursors.js +6 -0
  178. package/dist/components/kritzel-cursor-trail.d.ts +11 -0
  179. package/dist/components/kritzel-cursor-trail.js +6 -0
  180. package/dist/components/kritzel-engine.d.ts +11 -0
  181. package/dist/components/kritzel-engine.js +52641 -0
  182. package/dist/components/kritzel-icon.d.ts +11 -0
  183. package/dist/components/kritzel-icon.js +6 -0
  184. package/dist/components/kritzel-watermark.d.ts +11 -0
  185. package/dist/components/kritzel-watermark.js +6 -0
  186. package/dist/components/p-B2Htv9ou.js +49 -0
  187. package/dist/components/p-BvRGQvVI.js +3456 -0
  188. package/dist/components/p-CfSx6HU_.js +1469 -0
  189. package/dist/components/p-YP8wAcF4.js +122 -0
  190. package/dist/components/p-fx6msYHJ.js +167 -0
  191. package/dist/esm/app-globals-DQuL1Twl.js +3 -0
  192. package/dist/esm/engine.constants-Bl5-pbq-.js +28143 -0
  193. package/dist/esm/index-BU9Mv7y0.js +1715 -0
  194. package/dist/esm/index.js +4134 -0
  195. package/dist/esm/kritzel-awareness-cursors_5.entry.js +27978 -0
  196. package/dist/esm/loader.js +11 -0
  197. package/dist/esm/stencil.js +21 -0
  198. package/dist/index.cjs.js +1 -0
  199. package/dist/index.js +1 -0
  200. package/dist/stencil/index.esm.js +4134 -0
  201. package/dist/stencil/p-BU9Mv7y0.js +1702 -0
  202. package/dist/stencil/p-Bl5-pbq-.js +28143 -0
  203. package/dist/stencil/p-DQuL1Twl.js +3 -0
  204. package/dist/stencil/p-f0a8b7af.entry.js +27978 -0
  205. package/dist/stencil/stencil.esm.js +21 -0
  206. package/dist/types/classes/core/core.class.d.ts +532 -0
  207. package/dist/types/classes/core/reviver.class.d.ts +32 -0
  208. package/dist/types/classes/core/store.class.d.ts +198 -0
  209. package/dist/types/classes/core/viewport.class.d.ts +256 -0
  210. package/dist/types/classes/core/workspace.class.d.ts +95 -0
  211. package/dist/types/classes/handlers/base.handler.d.ts +18 -0
  212. package/dist/types/classes/handlers/context-menu.handler.d.ts +67 -0
  213. package/dist/types/classes/handlers/hover.handler.d.ts +23 -0
  214. package/dist/types/classes/handlers/key.handler.d.ts +44 -0
  215. package/dist/types/classes/handlers/line-handle.handler.d.ts +115 -0
  216. package/dist/types/classes/handlers/move.handler.d.ts +94 -0
  217. package/dist/types/classes/handlers/resize.handler.d.ts +95 -0
  218. package/dist/types/classes/handlers/rotation.handler.d.ts +50 -0
  219. package/dist/types/classes/handlers/selection.handler.d.ts +153 -0
  220. package/dist/types/classes/managers/anchor.manager.d.ts +513 -0
  221. package/dist/types/classes/managers/browser-permission.manager.d.ts +41 -0
  222. package/dist/types/classes/managers/clipboard.manager.d.ts +71 -0
  223. package/dist/types/classes/managers/clipboard.types.d.ts +23 -0
  224. package/dist/types/classes/managers/cursor.manager.d.ts +66 -0
  225. package/dist/types/classes/managers/license.manager.d.ts +147 -0
  226. package/dist/types/classes/managers/localization.manager.d.ts +145 -0
  227. package/dist/types/classes/managers/theme.manager.d.ts +112 -0
  228. package/dist/types/classes/objects/base-object.class.d.ts +266 -0
  229. package/dist/types/classes/objects/dynamic-object.class.d.ts +148 -0
  230. package/dist/types/classes/objects/group.class.d.ts +231 -0
  231. package/dist/types/classes/objects/image.class.d.ts +246 -0
  232. package/dist/types/classes/objects/line.class.d.ts +285 -0
  233. package/dist/types/classes/objects/path.class.d.ts +220 -0
  234. package/dist/types/classes/objects/selection-box.class.d.ts +19 -0
  235. package/dist/types/classes/objects/selection-group.class.d.ts +267 -0
  236. package/dist/types/classes/objects/shape.class.d.ts +322 -0
  237. package/dist/types/classes/objects/text.class.d.ts +197 -0
  238. package/dist/types/classes/providers/assets/asset-resolver.class.d.ts +141 -0
  239. package/dist/types/classes/providers/assets/http-asset-provider.class.d.ts +79 -0
  240. package/dist/types/classes/providers/assets/indexeddb-asset-provider.class.d.ts +69 -0
  241. package/dist/types/classes/providers/assets/presigned-asset-provider.class.d.ts +69 -0
  242. package/dist/types/classes/providers/paste-intent/event-paste-intent.provider.d.ts +8 -0
  243. package/dist/types/classes/providers/paste-intent/ios-paste-intent.provider.d.ts +6 -0
  244. package/dist/types/classes/providers/paste-intent/navigator-paste-intent.provider.d.ts +9 -0
  245. package/dist/types/classes/providers/sync/broadcast-sync-provider.class.d.ts +23 -0
  246. package/dist/types/classes/providers/sync/hocuspocus-sync-provider.class.d.ts +163 -0
  247. package/dist/types/classes/providers/sync/in-memory-sync-provider.class.d.ts +39 -0
  248. package/dist/types/classes/providers/sync/indexeddb-sync-provider.class.d.ts +25 -0
  249. package/dist/types/classes/providers/sync/websocket-sync-provider.class.d.ts +63 -0
  250. package/dist/types/classes/registries/dynamic-object-renderer.registry.d.ts +32 -0
  251. package/dist/types/classes/registries/font-registry.class.d.ts +19 -0
  252. package/dist/types/classes/registries/icon-registry.class.d.ts +39 -0
  253. package/dist/types/classes/registries/paste-intent-provider.registry.d.ts +10 -0
  254. package/dist/types/classes/registries/tool.registry.d.ts +53 -0
  255. package/dist/types/classes/structures/app-state-map.structure.d.ts +171 -0
  256. package/dist/types/classes/structures/object-map.structure.d.ts +403 -0
  257. package/dist/types/classes/structures/quadtree.structure.d.ts +128 -0
  258. package/dist/types/classes/tools/base-tool.class.d.ts +72 -0
  259. package/dist/types/classes/tools/brush-tool.class.d.ts +68 -0
  260. package/dist/types/classes/tools/eraser-tool.class.d.ts +38 -0
  261. package/dist/types/classes/tools/image-tool.class.d.ts +74 -0
  262. package/dist/types/classes/tools/line-tool.class.d.ts +63 -0
  263. package/dist/types/classes/tools/selection-tool.class.d.ts +188 -0
  264. package/dist/types/classes/tools/shape-tool.class.d.ts +90 -0
  265. package/dist/types/classes/tools/text-tool.class.d.ts +66 -0
  266. package/dist/types/components/kritzel-awareness-cursors/kritzel-awareness-cursors.d.ts +29 -0
  267. package/dist/types/components/kritzel-cursor-trail/kritzel-cursor-trail.d.ts +19 -0
  268. package/dist/types/components/kritzel-engine/kritzel-engine.d.ts +695 -0
  269. package/dist/types/components/kritzel-icon/kritzel-icon.d.ts +6 -0
  270. package/dist/types/components/kritzel-watermark/kritzel-watermark.d.ts +20 -0
  271. package/dist/types/components.d.ts +981 -0
  272. package/dist/types/configs/default-asset-storage.config.d.ts +8 -0
  273. package/dist/types/configs/default-brush-tool.config.d.ts +2 -0
  274. package/dist/types/configs/default-engine-config.d.ts +2 -0
  275. package/dist/types/configs/default-line-tool.config.d.ts +2 -0
  276. package/dist/types/configs/default-shape-tool.config.d.ts +2 -0
  277. package/dist/types/configs/default-sync.config.d.ts +5 -0
  278. package/dist/types/configs/default-text-tool.config.d.ts +2 -0
  279. package/dist/types/constants/color-palette.constants.d.ts +31 -0
  280. package/dist/types/constants/core.constants.d.ts +0 -0
  281. package/dist/types/constants/engine.constants.d.ts +4 -0
  282. package/dist/types/constants/license.constants.d.ts +25 -0
  283. package/dist/types/constants/schema.constants.d.ts +4 -0
  284. package/dist/types/constants/storage.constants.d.ts +1 -0
  285. package/dist/types/constants/stroke-size.constants.d.ts +2 -0
  286. package/dist/types/constants/text-tool-fonts.constants.d.ts +3 -0
  287. package/dist/types/constants/version.d.ts +6 -0
  288. package/dist/types/enums/alignment.enum.d.ts +8 -0
  289. package/dist/types/enums/event-button.enum.d.ts +5 -0
  290. package/dist/types/enums/handle-type.enum.d.ts +6 -0
  291. package/dist/types/enums/shape-type.enum.d.ts +5 -0
  292. package/dist/types/events/active-workspace-change.event.d.ts +7 -0
  293. package/dist/types/events/editor-is-ready.event.d.ts +18 -0
  294. package/dist/types/events/login.event.d.ts +3 -0
  295. package/dist/types/helpers/alignment.helper.d.ts +21 -0
  296. package/dist/types/helpers/class.helper.d.ts +3 -0
  297. package/dist/types/helpers/color.helper.d.ts +34 -0
  298. package/dist/types/helpers/cursor.helper.d.ts +22 -0
  299. package/dist/types/helpers/devices.helper.d.ts +8 -0
  300. package/dist/types/helpers/event.helper.d.ts +6 -0
  301. package/dist/types/helpers/geometry.helper.d.ts +38 -0
  302. package/dist/types/helpers/html.helper.d.ts +8 -0
  303. package/dist/types/helpers/image-file.helper.d.ts +3 -0
  304. package/dist/types/helpers/keyboard.helper.d.ts +6 -0
  305. package/dist/types/helpers/localization.helper.d.ts +18 -0
  306. package/dist/types/helpers/math.helper.d.ts +4 -0
  307. package/dist/types/helpers/migration.helper.d.ts +30 -0
  308. package/dist/types/helpers/object.helper.d.ts +9 -0
  309. package/dist/types/helpers/svg-export.helper.d.ts +231 -0
  310. package/dist/types/helpers/theme.helper.d.ts +42 -0
  311. package/dist/types/helpers/tool-config.helper.d.ts +4 -0
  312. package/dist/types/helpers/wheel.helper.d.ts +9 -0
  313. package/dist/types/index.d.ts +109 -0
  314. package/dist/types/interfaces/anchor.interface.d.ts +147 -0
  315. package/dist/types/interfaces/arrow-head.interface.d.ts +27 -0
  316. package/dist/types/interfaces/asset-provider.interface.d.ts +88 -0
  317. package/dist/types/interfaces/asset-storage-config.interface.d.ts +40 -0
  318. package/dist/types/interfaces/asset.interface.d.ts +43 -0
  319. package/dist/types/interfaces/bounding-box.interface.d.ts +8 -0
  320. package/dist/types/interfaces/clonable.interface.d.ts +3 -0
  321. package/dist/types/interfaces/context-menu-item.interface.d.ts +25 -0
  322. package/dist/types/interfaces/context-menu-state.interface.d.ts +16 -0
  323. package/dist/types/interfaces/debug-info.interface.d.ts +7 -0
  324. package/dist/types/interfaces/dialog.interface.d.ts +4 -0
  325. package/dist/types/interfaces/document-metadata.interface.d.ts +6 -0
  326. package/dist/types/interfaces/dynamic-object-renderer.interface.d.ts +40 -0
  327. package/dist/types/interfaces/engine-state.interface.d.ts +82 -0
  328. package/dist/types/interfaces/font.interface.d.ts +15 -0
  329. package/dist/types/interfaces/localization.interface.d.ts +143 -0
  330. package/dist/types/interfaces/login-config.interface.d.ts +24 -0
  331. package/dist/types/interfaces/master-detail.interface.d.ts +14 -0
  332. package/dist/types/interfaces/menu-item.interface.d.ts +27 -0
  333. package/dist/types/interfaces/migration.interface.d.ts +16 -0
  334. package/dist/types/interfaces/notification.interface.d.ts +10 -0
  335. package/dist/types/interfaces/object-change-event.interface.d.ts +31 -0
  336. package/dist/types/interfaces/object-export.interface.d.ts +20 -0
  337. package/dist/types/interfaces/object.interface.d.ts +53 -0
  338. package/dist/types/interfaces/paste-intent-provider.interface.d.ts +10 -0
  339. package/dist/types/interfaces/point.interface.d.ts +4 -0
  340. package/dist/types/interfaces/polygon.interface.d.ts +7 -0
  341. package/dist/types/interfaces/remote-cursor.interface.d.ts +18 -0
  342. package/dist/types/interfaces/selection-state.interface.d.ts +7 -0
  343. package/dist/types/interfaces/serializable.interface.d.ts +5 -0
  344. package/dist/types/interfaces/settings.interface.d.ts +25 -0
  345. package/dist/types/interfaces/share.interface.d.ts +9 -0
  346. package/dist/types/interfaces/shortcut.interface.d.ts +10 -0
  347. package/dist/types/interfaces/sync-config.interface.d.ts +41 -0
  348. package/dist/types/interfaces/sync-provider.interface.d.ts +66 -0
  349. package/dist/types/interfaces/theme.interface.d.ts +553 -0
  350. package/dist/types/interfaces/tool-config.interface.d.ts +24 -0
  351. package/dist/types/interfaces/tool-type.interface.d.ts +1 -0
  352. package/dist/types/interfaces/tool.interface.d.ts +8 -0
  353. package/dist/types/interfaces/toolbar-item.interface.d.ts +66 -0
  354. package/dist/types/interfaces/undo-state.interface.d.ts +6 -0
  355. package/dist/types/interfaces/user.interface.d.ts +22 -0
  356. package/dist/types/interfaces/viewport-state.interface.d.ts +7 -0
  357. package/dist/types/locales/de-locale.d.ts +5 -0
  358. package/dist/types/locales/en-locale.d.ts +6 -0
  359. package/dist/types/locales/fr-locale.d.ts +5 -0
  360. package/dist/types/migrations/app-state.migrations.d.ts +11 -0
  361. package/dist/types/migrations/workspace.migrations.d.ts +11 -0
  362. package/dist/types/stencil-public-runtime.d.ts +1873 -0
  363. package/dist/types/themes/dark-theme.d.ts +5 -0
  364. package/dist/types/themes/light-theme.d.ts +5 -0
  365. package/dist/types/types/deep-readonly.type.d.ts +7 -0
  366. package/dist/types/types/state.types.d.ts +3 -0
  367. package/loader/cdn.js +1 -0
  368. package/loader/index.cjs.js +1 -0
  369. package/loader/index.d.ts +24 -0
  370. package/loader/index.es2017.js +1 -0
  371. package/loader/index.js +2 -0
  372. package/package.json +101 -0
  373. package/readme.md +11 -0
@@ -0,0 +1,1389 @@
1
+ import { DEFAULT_ENGINE_CONFIG } from "../../configs/default-engine-config";
2
+ import { AlignmentHelper } from "../../helpers/alignment.helper";
3
+ import { KritzelSelectionBox } from "../objects/selection-box.class";
4
+ import { KritzelSelectionGroup } from "../objects/selection-group.class";
5
+ import { KritzelGroup } from "../objects/group.class";
6
+ import { KritzelLine } from "../objects/line.class";
7
+ import { KritzelToolRegistry } from "../registries/tool.registry";
8
+ import { KritzelStore } from "./store.class";
9
+ import { KritzelWorkspace } from "./workspace.class";
10
+ import { KritzelObjectMap } from "../structures/object-map.structure";
11
+ import { KritzelAssetResolver } from "../providers/assets/asset-resolver.class";
12
+ import { KritzelAppStateMap } from "../structures/app-state-map.structure";
13
+ import { KritzelAnchorManager } from "../managers/anchor.manager";
14
+ import { KritzelCursorManager } from "../managers/cursor.manager";
15
+ import { KritzelThemeManager } from "../managers/theme.manager";
16
+ import { KritzelLocalizationManager } from "../managers/localization.manager";
17
+ import { KritzelLicenseManager } from "../managers/license.manager";
18
+ import { KritzelBrowserPermissionManager } from "../managers/browser-permission.manager";
19
+ import { KritzelClipboardManager } from "../managers/clipboard.manager";
20
+ import { KritzelClassHelper } from "../../helpers/class.helper";
21
+ /**
22
+ * Central orchestrator class for the Kritzel canvas system.
23
+ * Manages workspaces, objects, selection, clipboard operations, and coordinates
24
+ * between the engine, store, and various managers.
25
+ */
26
+ export class KritzelCore {
27
+ /** The underlying Kritzel engine component */
28
+ _kritzelEngine;
29
+ /** State management store */
30
+ _store;
31
+ /** Optional sync configuration for collaborative features */
32
+ _syncConfig;
33
+ /** Optional asset storage configuration for image / binary payloads */
34
+ _assetStorageConfig;
35
+ /** Resolves and stores binary assets referenced by canvas objects */
36
+ _assetResolver;
37
+ /** App state map for persistent workspace storage */
38
+ _appStateMap;
39
+ /** Manager for line anchor points and connections */
40
+ _anchorManager;
41
+ /** Manager for cursor states and appearance */
42
+ _cursorManager;
43
+ /** Manager for theme styling */
44
+ _themeManager;
45
+ /** Manager for localization / translated UI strings */
46
+ _localizationManager;
47
+ /** Manager for license validation and watermark gating */
48
+ _licenseManager;
49
+ /** Manager for browser permission checks and permission prompts */
50
+ _permissionManager;
51
+ /** Manager for clipboard copy/cut/paste orchestration */
52
+ _clipboardManager;
53
+ /** Per-core registry of drawing tools (one instance per editor). */
54
+ _toolRegistry;
55
+ /** Optional unique identifier for namespacing storage keys across multiple editor instances */
56
+ _editorId;
57
+ /** Current user for awareness broadcasting */
58
+ _user;
59
+ /**
60
+ * Gets the Kritzel engine instance.
61
+ * @returns The KritzelEngine managing rendering and events
62
+ */
63
+ get engine() {
64
+ return this._kritzelEngine;
65
+ }
66
+ /**
67
+ * Gets the state store instance.
68
+ * @returns The KritzelStore managing application state
69
+ */
70
+ get store() {
71
+ return this._store;
72
+ }
73
+ /**
74
+ * Gets the app state map for workspace persistence.
75
+ * @returns The KritzelAppStateMap for storing workspace data
76
+ */
77
+ get appStateMap() {
78
+ return this._appStateMap;
79
+ }
80
+ /**
81
+ * Gets the anchor manager for line connections.
82
+ * @returns The KritzelAnchorManager for managing line anchors
83
+ */
84
+ get anchorManager() {
85
+ return this._anchorManager;
86
+ }
87
+ /**
88
+ * Gets the cursor manager.
89
+ * @returns The KritzelCursorManager for managing cursor states
90
+ */
91
+ get cursorManager() {
92
+ return this._cursorManager;
93
+ }
94
+ /**
95
+ * Gets the theme manager.
96
+ * @returns The KritzelThemeManager for managing visual themes
97
+ */
98
+ get themeManager() {
99
+ return this._themeManager;
100
+ }
101
+ /**
102
+ * Gets the localization manager.
103
+ * @returns The KritzelLocalizationManager for resolving translated UI strings
104
+ */
105
+ get localizationManager() {
106
+ return this._localizationManager;
107
+ }
108
+ /**
109
+ * Gets the license manager.
110
+ * @returns The KritzelLicenseManager for validating licenses and gating the watermark
111
+ */
112
+ get licenseManager() {
113
+ return this._licenseManager;
114
+ }
115
+ /**
116
+ * Gets the browser permission manager.
117
+ * @returns The KritzelBrowserPermissionManager for browser permission checks and requests
118
+ */
119
+ get permissionManager() {
120
+ return this._permissionManager;
121
+ }
122
+ /**
123
+ * Gets the tool registry scoped to this core instance.
124
+ * @returns The KritzelToolRegistry owned by this core
125
+ */
126
+ get toolRegistry() {
127
+ return this._toolRegistry;
128
+ }
129
+ /**
130
+ * Gets the clipboard manager scoped to this core instance.
131
+ * @returns The KritzelClipboardManager owned by this core
132
+ */
133
+ get clipboardManager() {
134
+ return this._clipboardManager;
135
+ }
136
+ /**
137
+ * Gets the editor ID used for namespacing storage keys.
138
+ * @returns The editor ID or undefined if not set
139
+ */
140
+ get editorId() {
141
+ return this._editorId;
142
+ }
143
+ /**
144
+ * Builds a namespaced storage key.
145
+ * When editorId is set, appends it after the base prefix:
146
+ * e.g. 'kritzel-settings' → 'kritzel-settings-homepage'
147
+ * @param base - The base key name (e.g. 'kritzel-settings')
148
+ * @returns The namespaced key
149
+ */
150
+ getStorageKey(base) {
151
+ return this._editorId ? `${base}-${this._editorId}` : base;
152
+ }
153
+ /**
154
+ * Gets the current user.
155
+ */
156
+ get user() {
157
+ return this._user;
158
+ }
159
+ /**
160
+ * Sets the current user and propagates to the active object map's awareness.
161
+ */
162
+ setUser(user) {
163
+ this._user = user;
164
+ this._store.setLocalUserId(user?.id ?? null);
165
+ this._store.objects?.setLocalUser(user);
166
+ }
167
+ /**
168
+ * Creates a new KritzelCore instance.
169
+ * Initializes the store with default configuration and sets up all managers.
170
+ * @param kritzelEngine - The KritzelEngine component to orchestrate
171
+ * @param editorId - Optional unique identifier for namespacing storage keys
172
+ */
173
+ constructor(kritzelEngine, editorId) {
174
+ this._kritzelEngine = kritzelEngine;
175
+ this._editorId = editorId;
176
+ this._store = new KritzelStore(DEFAULT_ENGINE_CONFIG);
177
+ this._appStateMap = new KritzelAppStateMap();
178
+ this._anchorManager = new KritzelAnchorManager(this);
179
+ this._cursorManager = new KritzelCursorManager(this);
180
+ this._themeManager = new KritzelThemeManager(this);
181
+ this._localizationManager = new KritzelLocalizationManager(this);
182
+ this._licenseManager = new KritzelLicenseManager(this);
183
+ this._permissionManager = new KritzelBrowserPermissionManager(this);
184
+ this._clipboardManager = new KritzelClipboardManager(this);
185
+ this._toolRegistry = new KritzelToolRegistry(this);
186
+ this._assetResolver = new KritzelAssetResolver();
187
+ this._assetResolver.setShowInfo(this._store.state.debugInfo?.showAssetResolverInfo ?? false);
188
+ }
189
+ /**
190
+ * Gets the asset resolver responsible for storing and resolving
191
+ * binary payloads (images, etc.) referenced by canvas objects.
192
+ */
193
+ get assetResolver() {
194
+ return this._assetResolver;
195
+ }
196
+ /**
197
+ * Sets the synchronization configuration for collaborative features.
198
+ * @param config - The sync configuration or undefined to disable sync
199
+ */
200
+ setSyncConfig(config) {
201
+ this._syncConfig = config;
202
+ }
203
+ /**
204
+ * Sets the asset storage configuration used for image / binary payloads.
205
+ * Takes effect on the next `initializeYjs` call (or on explicit
206
+ * `initializeAssets` if invoked directly).
207
+ */
208
+ setAssetStorageConfig(config) {
209
+ this._assetStorageConfig = config;
210
+ }
211
+ /**
212
+ * Sets the editor ID for namespacing storage keys.
213
+ * Must be called before any storage initialization (e.g., in componentWillLoad).
214
+ * Re-initializes the theme manager to pick up the new storage key.
215
+ * @param editorId - The unique identifier for this editor instance
216
+ */
217
+ setEditorId(editorId) {
218
+ this._editorId = editorId;
219
+ // Re-create theme manager so it picks up the new storage key
220
+ this._themeManager = new KritzelThemeManager(this);
221
+ // Re-create localization manager so it picks up the new storage key
222
+ this._localizationManager = new KritzelLocalizationManager(this);
223
+ }
224
+ /**
225
+ * Initializes the Yjs document for collaborative editing.
226
+ * Sets up the app state map with the current sync configuration.
227
+ *
228
+ * Asset storage is initialized separately via {@link initializeAssetStorage}
229
+ * so that it can be deferred until the `assetStorageConfig` prop arrives,
230
+ * mirroring the lazy-init pattern used for sync config.
231
+ */
232
+ async initializeYjs() {
233
+ await this._appStateMap.initialize(this, this._syncConfig);
234
+ }
235
+ /**
236
+ * Initializes the asset storage layer with the current asset storage
237
+ * configuration. Safe to call multiple times: subsequent calls are
238
+ * no-ops because {@link KritzelAssetResolver.init} is idempotent.
239
+ * To apply a new configuration after initialization, destroy the
240
+ * resolver first or use {@link KritzelEngine.reinitSync}.
241
+ */
242
+ async initializeAssetStorage() {
243
+ await this._assetResolver.init(this._assetStorageConfig);
244
+ this.warnIfAssetStorageMismatched();
245
+ }
246
+ /**
247
+ * Emits a single warning when the sync config declares at least one
248
+ * network provider but the asset storage config has none. In that
249
+ * scenario images added on this device will not be available to
250
+ * other devices, yielding a broken cross-device experience.
251
+ */
252
+ warnIfAssetStorageMismatched() {
253
+ if (!this._appStateMap.hasNetworkSyncProvider)
254
+ return;
255
+ if (this._assetResolver.hasRemoteProvider)
256
+ return;
257
+ console.warn('[Kritzel] A network sync provider is configured, but no remote asset storage provider ' +
258
+ 'was found. Images added on this device will only be available locally and will appear ' +
259
+ 'broken on other devices. Configure a remote asset provider (e.g. HttpAssetProvider) ' +
260
+ 'via the `assetStorage` prop to enable cross-device image sync.');
261
+ }
262
+ /**
263
+ * Load all workspaces from app state map
264
+ */
265
+ loadWorkspacesFromAppState() {
266
+ return this._appStateMap.allWorkspaces();
267
+ }
268
+ /**
269
+ * Save a workspace to app state map
270
+ */
271
+ saveWorkspaceToAppState(workspace) {
272
+ if (this._appStateMap.has(workspace.id)) {
273
+ this._appStateMap.update(workspace);
274
+ }
275
+ else {
276
+ this._appStateMap.insert(workspace);
277
+ }
278
+ }
279
+ /**
280
+ * Delete a workspace from app state map
281
+ */
282
+ deleteWorkspaceFromAppState(workspaceId) {
283
+ this._appStateMap.remove(w => w.id === workspaceId);
284
+ }
285
+ /**
286
+ * Initializes or switches to a workspace.
287
+ * Handles workspace loading from app state, localStorage preferences,
288
+ * viewport state restoration, and ObjectMap initialization.
289
+ * @param workspace - Optional workspace to switch to; if not provided, uses last active or most recent
290
+ * @param options - Optional initialization behavior flags
291
+ */
292
+ async initializeWorkspace(workspace, options) {
293
+ // Load all workspaces from app state map
294
+ const workspaces = this.loadWorkspacesFromAppState();
295
+ const shouldCreateEditorScopedFallback = !workspace && !!this._editorId;
296
+ // Find newest created workspace (tie-break by most recently updated)
297
+ const newestCreatedWorkspace = workspaces.length > 0
298
+ ? [...workspaces].sort((a, b) => {
299
+ const diff = b.createdAt.getTime() - a.createdAt.getTime();
300
+ return diff !== 0 ? diff : b.updatedAt.getTime() - a.updatedAt.getTime();
301
+ })[0]
302
+ : null;
303
+ // Check localStorage for last active workspace
304
+ const lastActiveWorkspaceId = localStorage.getItem(this.getStorageKey('kritzel-active-workspace-id'));
305
+ const lastActiveWorkspace = lastActiveWorkspaceId ? workspaces.find(w => w.id === lastActiveWorkspaceId) : null;
306
+ let activeWorkspace;
307
+ if (workspace) {
308
+ // Use provided workspace
309
+ activeWorkspace = workspace;
310
+ const existing = workspaces.find(w => w.id === workspace.id);
311
+ if (!existing) {
312
+ // New workspace, save it
313
+ activeWorkspace._core = this;
314
+ this.saveWorkspaceToAppState(activeWorkspace);
315
+ }
316
+ }
317
+ else if (lastActiveWorkspace) {
318
+ // Use last active workspace from localStorage
319
+ activeWorkspace = lastActiveWorkspace;
320
+ }
321
+ else if (newestCreatedWorkspace && !shouldCreateEditorScopedFallback) {
322
+ // Use newest created workspace
323
+ activeWorkspace = newestCreatedWorkspace;
324
+ }
325
+ else if (options?.skipFallbackCreation) {
326
+ this._store.state.workspaces = workspaces;
327
+ this._store.state.activeWorkspace = null;
328
+ this.rerender();
329
+ return;
330
+ }
331
+ else {
332
+ // Create fallback workspace
333
+ activeWorkspace = new KritzelWorkspace({ name: 'New Workspace' });
334
+ activeWorkspace._core = this;
335
+ this.saveWorkspaceToAppState(activeWorkspace);
336
+ }
337
+ // Save active workspace ID to localStorage
338
+ localStorage.setItem(this.getStorageKey('kritzel-active-workspace-id'), activeWorkspace.id);
339
+ // Destroy old ObjectMap if switching workspaces
340
+ if (this._store.objects && this._store.objects.isReady) {
341
+ this._store.objects.destroy();
342
+ }
343
+ // Create new ObjectMap with its own Y.Doc for this workspace
344
+ const objectsMap = new KritzelObjectMap();
345
+ // Assign immediately so the UI shows an empty state while loading,
346
+ // instead of showing the old workspace's objects with the new workspace's viewport
347
+ this._store.state.objects = objectsMap;
348
+ // Set active workspace
349
+ this._store.state.activeWorkspace = activeWorkspace;
350
+ this._store.state.workspaces = this.loadWorkspacesFromAppState();
351
+ // Set viewport from workspace (convert world-space center to screen-space translate)
352
+ const viewport = activeWorkspace.viewport ?? { centerWorldX: 0, centerWorldY: 0, scale: 1 };
353
+ const scale = viewport.scale ?? 1;
354
+ const viewportWidth = this._store.state.viewportWidth;
355
+ const viewportHeight = this._store.state.viewportHeight;
356
+ this._store.state.translateX = viewportWidth / 2 - (viewport.centerWorldX ?? 0) * scale;
357
+ this._store.state.translateY = viewportHeight / 2 - (viewport.centerWorldY ?? 0) * scale;
358
+ this._store.state.scale = scale;
359
+ await objectsMap.initialize(this, activeWorkspace.id, this._syncConfig);
360
+ this.seedWorkspaceObjects(activeWorkspace, objectsMap);
361
+ // Set local user in awareness after object map is initialized
362
+ if (this._user) {
363
+ objectsMap.setLocalUser(this._user);
364
+ }
365
+ // Forward awareness changes to the engine as events
366
+ objectsMap.onAwarenessChange(states => {
367
+ this.engine.emitAwarenessChange(states);
368
+ });
369
+ // Rebuild anchor index after loading objects
370
+ this._anchorManager.rebuildIndex();
371
+ this.engine.emitObjectsChange();
372
+ this.rerender();
373
+ }
374
+ seedWorkspaceObjects(workspace, objectsMap) {
375
+ if (!workspace.objects?.length) {
376
+ return;
377
+ }
378
+ objectsMap.transaction(() => {
379
+ for (const object of workspace.objects) {
380
+ if (objectsMap.findById(object.id)) {
381
+ continue;
382
+ }
383
+ this.prepareObjectForWorkspace(object, workspace.id, this._store.currentZIndex, this._store.state.scale);
384
+ objectsMap.insert(object);
385
+ }
386
+ });
387
+ }
388
+ /**
389
+ * Mirrors engine.addObject() runtime initialization for consumer-constructed objects.
390
+ */
391
+ prepareObjectForWorkspace(object, workspaceId, zIndex, scaleFallback) {
392
+ object._core = this;
393
+ object.scale = object.scale ?? scaleFallback;
394
+ object.zIndex = zIndex;
395
+ // Recreate the text editor now that _core is available for persistence callbacks
396
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelText')) {
397
+ const pendingContent = object.content;
398
+ object.editor = object.createEditor();
399
+ if (pendingContent) {
400
+ object.setContent(pendingContent);
401
+ }
402
+ }
403
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelGroup')) {
404
+ object.updateWorkspaceId(workspaceId);
405
+ }
406
+ else {
407
+ object.workspaceId = workspaceId;
408
+ }
409
+ }
410
+ /**
411
+ * Applies `desiredObjects` as the complete object set of an object map.
412
+ * Objects are matched by ID: unknown IDs are added, known IDs are updated (replacing the live
413
+ * instance when a different one is passed), and objects absent from the list are removed.
414
+ * @param objectsMap - The map to reconcile
415
+ * @param workspaceId - The workspace the objects belong to
416
+ * @param desiredObjects - The desired object set
417
+ * @param nextZIndex - Supplies the z-index for newly added objects
418
+ * @param scaleFallback - Scale applied to objects that do not define one
419
+ * @returns The objects that were added, updated and removed
420
+ */
421
+ reconcileWorkspaceObjects(objectsMap, workspaceId, desiredObjects, nextZIndex, scaleFallback) {
422
+ const existingObjects = objectsMap.allObjects().filter(o => !(o instanceof KritzelSelectionGroup) && !(o instanceof KritzelSelectionBox));
423
+ const existingById = new Map(existingObjects.map(object => [object.id, object]));
424
+ const desiredIds = new Set();
425
+ const diff = { added: [], updated: [], removed: [] };
426
+ objectsMap.transaction(() => {
427
+ for (const object of desiredObjects) {
428
+ if (!object.id) {
429
+ object.id = object.generateId();
430
+ }
431
+ desiredIds.add(object.id);
432
+ const existingObject = existingById.get(object.id);
433
+ if (!existingObject) {
434
+ this.prepareObjectForWorkspace(object, workspaceId, nextZIndex(), scaleFallback);
435
+ objectsMap.insert(object);
436
+ diff.added.push(object);
437
+ continue;
438
+ }
439
+ if (existingObject === object) {
440
+ objectsMap.update(object);
441
+ diff.updated.push(object);
442
+ continue;
443
+ }
444
+ // Same ID but a different instance: swap the live object for the incoming one
445
+ this.detachObject(existingObject);
446
+ objectsMap.remove(o => o.id === object.id);
447
+ this.prepareObjectForWorkspace(object, workspaceId, object.zIndex ?? nextZIndex(), scaleFallback);
448
+ objectsMap.insert(object);
449
+ diff.updated.push(object);
450
+ }
451
+ for (const object of existingObjects) {
452
+ if (desiredIds.has(object.id)) {
453
+ continue;
454
+ }
455
+ this.detachObject(object);
456
+ objectsMap.remove(o => o.id === object.id);
457
+ diff.removed.push(object);
458
+ }
459
+ });
460
+ return diff;
461
+ }
462
+ /**
463
+ * Releases runtime resources held by an object that is about to leave the canvas.
464
+ * Unlike removeObject() this never cascades into group children, so the reconciled
465
+ * object set stays exactly what the caller declared.
466
+ */
467
+ detachObject(object) {
468
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelDynamicObject')) {
469
+ object.unmount();
470
+ }
471
+ object.isMounted = false;
472
+ }
473
+ /**
474
+ * Returns the next free z-index within an object map.
475
+ */
476
+ nextZIndexOf(objectsMap) {
477
+ const objects = objectsMap.allObjects().filter(o => !(o instanceof KritzelSelectionGroup) && !(o instanceof KritzelSelectionBox));
478
+ return Math.max(0, ...objects.map(o => o.zIndex)) + 1;
479
+ }
480
+ /**
481
+ * Loads a shared workspace by token.
482
+ * If the workspace exists locally, it switches to it.
483
+ * If it doesn't exist, it adds it to the local workspaces list and then switches to it.
484
+ * @param token - The connection token identifying the shared workspace
485
+ */
486
+ async loadSharedWorkspace(token) {
487
+ const workspaceId = token;
488
+ const name = 'Shared Workspace';
489
+ // Token is the workspace ID
490
+ let workspace = this.getWorkspaces().find(w => w.id === workspaceId);
491
+ if (!workspace) {
492
+ // Create it if it doesn't exist locally
493
+ workspace = new KritzelWorkspace({ id: workspaceId, name });
494
+ workspace.isPublic = true;
495
+ this.createWorkspace(workspace);
496
+ }
497
+ else {
498
+ // If it exists, ensure isPublic is correct
499
+ workspace.isPublic = true;
500
+ }
501
+ // Now switch to it
502
+ await this.initializeWorkspace(workspace);
503
+ }
504
+ /**
505
+ * Gets all objects currently visible within the viewport bounds.
506
+ * Excludes selection-related objects and sorts by z-index.
507
+ * @returns Array of visible objects sorted by z-index ascending
508
+ */
509
+ getObjectsInViewport() {
510
+ const viewportBounds = {
511
+ x: -this._store.state.translateX / this._store.state.scale,
512
+ y: -this._store.state.translateY / this._store.state.scale,
513
+ z: this._store.state.scale,
514
+ width: this._store.state.viewportWidth / this._store.state.scale,
515
+ height: this._store.state.viewportHeight / this._store.state.scale,
516
+ depth: 100,
517
+ };
518
+ if (!this._store.objects) {
519
+ return [];
520
+ }
521
+ return this._store.objects
522
+ .query(viewportBounds)
523
+ .filter(obj => !(obj instanceof KritzelSelectionGroup) && !(obj instanceof KritzelSelectionBox))
524
+ .sort((a, b) => a.zIndex - b.zIndex);
525
+ }
526
+ /**
527
+ * Forces a rerender of the engine component.
528
+ * Increments forceUpdate counter to trigger Stencil's reactivity.
529
+ */
530
+ rerender() {
531
+ if (this._kritzelEngine) {
532
+ this._kritzelEngine.forceUpdate++;
533
+ }
534
+ }
535
+ /**
536
+ * Finds an object in the store by its unique identifier.
537
+ * @param id - The unique ID of the object to find
538
+ * @returns The found object or null if not found
539
+ */
540
+ findObjectById(id) {
541
+ for (const object of this._store.allObjects) {
542
+ if (object.id === id) {
543
+ return object;
544
+ }
545
+ }
546
+ return null;
547
+ }
548
+ /**
549
+ * Gets all workspaces from the app state map.
550
+ * @returns Array of all available workspaces
551
+ */
552
+ getWorkspaces() {
553
+ return this.loadWorkspacesFromAppState();
554
+ }
555
+ /**
556
+ * Switches the active workspace in the editor by ID.
557
+ * @param workspaceId - The unique ID of the workspace to activate
558
+ */
559
+ async setActiveWorkspace(workspaceId) {
560
+ const workspaces = this.getWorkspaces();
561
+ const target = workspaces.find(w => w.id === workspaceId);
562
+ if (!target) {
563
+ console.warn(`[KritzelCore] Cannot set active workspace: No workspace found with ID '${workspaceId}'`);
564
+ return;
565
+ }
566
+ this.beforeWorkspaceChange();
567
+ await this.initializeWorkspace(target);
568
+ }
569
+ /**
570
+ * Creates a new workspace and saves it to the app state.
571
+ * @param workspace - The workspace instance to create
572
+ * @returns The created workspace instance
573
+ */
574
+ async createWorkspace(workspace) {
575
+ workspace._core = this;
576
+ workspace.createdAt = new Date();
577
+ workspace.updatedAt = new Date();
578
+ this.saveWorkspaceToAppState(workspace);
579
+ this._store.state.workspaces = this.loadWorkspacesFromAppState();
580
+ return workspace;
581
+ }
582
+ /**
583
+ * Applies a workspace as declared: metadata, viewport, and — when `workspace.objects` is set —
584
+ * the complete object set. A `undefined` object list leaves existing objects untouched, an empty
585
+ * list removes them all. Objects of non-active workspaces are written through a background
586
+ * object map and therefore require a configured sync provider.
587
+ * @param workspace - The workspace describing the desired state
588
+ * @returns The object changes that were applied, or null if objects were not touched
589
+ */
590
+ async updateWorkspace(workspace) {
591
+ const isActive = this._store.state.activeWorkspace?.id === workspace.id;
592
+ const desiredObjects = workspace.objects;
593
+ workspace._core = this;
594
+ // Never persist the declared object set into the app state entry
595
+ workspace.objects = undefined;
596
+ workspace.updatedAt = new Date();
597
+ this.saveWorkspaceToAppState(workspace);
598
+ // Keep metadata map in sync when the active workspace's name changes
599
+ if (isActive) {
600
+ this._store.objects?.setWorkspaceName(workspace.name);
601
+ }
602
+ const workspaces = this._store.state.workspaces;
603
+ const index = workspaces.findIndex(w => w.id === workspace.id);
604
+ if (index !== -1) {
605
+ workspaces[index] = workspace;
606
+ this._store.state.workspaces = workspaces;
607
+ }
608
+ if (isActive) {
609
+ this.applyWorkspaceViewport(workspace);
610
+ this._store.state.activeWorkspace = workspace;
611
+ }
612
+ if (!desiredObjects) {
613
+ return null;
614
+ }
615
+ return isActive ? this.applyObjectsToActiveWorkspace(workspace, desiredObjects) : this.applyObjectsToInactiveWorkspace(workspace, desiredObjects);
616
+ }
617
+ /**
618
+ * Moves the live viewport to the workspace's declared viewport.
619
+ * Compared against the persisted viewport rather than the live one, so a workspace that was
620
+ * read and passed back unchanged (e.g. a rename) never snaps the canvas.
621
+ */
622
+ applyWorkspaceViewport(workspace) {
623
+ const viewport = workspace.viewport;
624
+ if (!viewport) {
625
+ return;
626
+ }
627
+ const scale = viewport.scale ?? 1;
628
+ const centerWorldX = viewport.centerWorldX ?? 0;
629
+ const centerWorldY = viewport.centerWorldY ?? 0;
630
+ const persistedViewport = this._store.state.activeWorkspace?.viewport;
631
+ if (persistedViewport && persistedViewport.scale === scale && persistedViewport.centerWorldX === centerWorldX && persistedViewport.centerWorldY === centerWorldY) {
632
+ return;
633
+ }
634
+ // Drop the debounced save of the outgoing viewport so it cannot overwrite the new one
635
+ this._kritzelEngine?.viewport?.cancelPendingUpdates();
636
+ this._store.state.translateX = this._store.state.viewportWidth / 2 - centerWorldX * scale;
637
+ this._store.state.translateY = this._store.state.viewportHeight / 2 - centerWorldY * scale;
638
+ this._store.state.scale = scale;
639
+ const activeWorkspace = this._store.state.activeWorkspace;
640
+ if (activeWorkspace) {
641
+ activeWorkspace.viewport = { centerWorldX, centerWorldY, scale };
642
+ }
643
+ }
644
+ applyObjectsToActiveWorkspace(workspace, desiredObjects) {
645
+ const objectsMap = this._store.objects;
646
+ if (!objectsMap) {
647
+ return null;
648
+ }
649
+ this.clearSelection();
650
+ const diff = this.reconcileWorkspaceObjects(objectsMap, workspace.id, desiredObjects, () => this._store.currentZIndex, this._store.state.scale);
651
+ this._anchorManager.rebuildIndex();
652
+ this.rerender();
653
+ return diff;
654
+ }
655
+ /**
656
+ * Applies objects to a workspace that is not currently open by loading its document into a
657
+ * background object map, reconciling, and disposing it again.
658
+ */
659
+ async applyObjectsToInactiveWorkspace(workspace, desiredObjects) {
660
+ if (!this._syncConfig?.providers?.length) {
661
+ console.warn(`[KritzelCore] Cannot update the objects of workspace '${workspace.id}' because it is not active and no sync provider is configured. ` +
662
+ 'Configure a sync provider, or activate the workspace before updating its objects.');
663
+ return null;
664
+ }
665
+ const objectsMap = new KritzelObjectMap();
666
+ try {
667
+ await objectsMap.initialize(this, workspace.id, this._syncConfig, { headless: true, workspaceName: workspace.name });
668
+ await objectsMap.whenNetworkSynced();
669
+ return this.reconcileWorkspaceObjects(objectsMap, workspace.id, desiredObjects, () => this.nextZIndexOf(objectsMap), this._store.state.scale);
670
+ }
671
+ finally {
672
+ objectsMap.destroy();
673
+ }
674
+ }
675
+ /**
676
+ * Deletes a workspace and its associated data.
677
+ * Handles cleanup of active workspace ObjectMap if deleting the current workspace.
678
+ * Also deletes the workspace's IndexedDB database.
679
+ * @param workspace - The workspace to delete
680
+ */
681
+ deleteWorkspace(workspace) {
682
+ // If deleting the active workspace, need to handle ObjectMap cleanup
683
+ if (this._store.state.activeWorkspace?.id === workspace.id) {
684
+ this.engine.viewport?.cancelPendingUpdates();
685
+ this._store.objects?.destroy();
686
+ }
687
+ this.deleteWorkspaceFromAppState(workspace.id);
688
+ this._store.state.workspaces = this.loadWorkspacesFromAppState();
689
+ // Also delete the workspace's IndexedDB database
690
+ const dbName = this._editorId
691
+ ? `kritzel-workspace-${this._editorId}-${workspace.id}`
692
+ : `kritzel-workspace-${workspace.id}`;
693
+ window.indexedDB.deleteDatabase(dbName);
694
+ }
695
+ /**
696
+ * Persists the current viewport state to the active workspace.
697
+ * Converts screen-space translate values to device-independent world-space center coordinates.
698
+ * Validates that the workspace still exists before saving.
699
+ * @param translateX - The X translation of the viewport
700
+ * @param translateY - The Y translation of the viewport
701
+ * @param scale - The zoom scale of the viewport
702
+ */
703
+ updateWorkspaceViewport(translateX, translateY, scale) {
704
+ const activeWorkspace = this._store.state.activeWorkspace;
705
+ if (!activeWorkspace) {
706
+ return;
707
+ }
708
+ // Check if the workspace is still valid (not deleted)
709
+ const isValid = this._store.state.workspaces.some(w => w.id === activeWorkspace.id);
710
+ if (!isValid) {
711
+ return;
712
+ }
713
+ // Convert screen-space translate to device-independent world-space center
714
+ const viewportWidth = this._store.state.viewportWidth;
715
+ const viewportHeight = this._store.state.viewportHeight;
716
+ const centerWorldX = (viewportWidth / 2 - translateX) / scale;
717
+ const centerWorldY = (viewportHeight / 2 - translateY) / scale;
718
+ activeWorkspace.viewport = { centerWorldX, centerWorldY, scale };
719
+ activeWorkspace.updatedAt = new Date();
720
+ this.saveWorkspaceToAppState(activeWorkspace);
721
+ }
722
+ /**
723
+ * Gets whether the active workspace is publicly accessible.
724
+ * @returns True if the workspace is public, false otherwise
725
+ */
726
+ getIsPublic() {
727
+ return this._store.state.activeWorkspace?.isPublic ?? false;
728
+ }
729
+ // ═══════════════════════════════════════════════════════════════════════════
730
+ // Object Management Methods
731
+ // ═══════════════════════════════════════════════════════════════════════════
732
+ /**
733
+ * Adds an object to the current workspace's object map.
734
+ * @param object - The object to add to the canvas
735
+ */
736
+ addObject(object) {
737
+ this._store.objects.insert(object);
738
+ }
739
+ /**
740
+ * Removes an object from the workspace.
741
+ * Handles recursive deletion of group children (unless preserved),
742
+ * cleans up line anchors, and removes from the object map.
743
+ * @param object - The object to remove
744
+ * @param preserveChildren - If true and object is a group, children are kept; defaults to false
745
+ */
746
+ removeObject(object, preserveChildren = false) {
747
+ // Handle KritzelGroup: recursively delete all children first (unless preserveChildren is true)
748
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelGroup') && !preserveChildren) {
749
+ // Get children before removing the group
750
+ const children = [...object.children];
751
+ children.forEach(child => {
752
+ this.removeObject(child);
753
+ });
754
+ }
755
+ // Unmount dynamic objects before removal
756
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelDynamicObject')) {
757
+ object.unmount();
758
+ }
759
+ // Handle anchor cleanup
760
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelLine')) {
761
+ // If removing a line, clean up its anchor index entries
762
+ this._anchorManager.handleLineDeleted(object.id);
763
+ }
764
+ else {
765
+ // If removing a non-line object, detach any lines anchored to it
766
+ this._anchorManager.handleObjectDeleted(object.id);
767
+ }
768
+ object.isMounted = false;
769
+ this._store.objects.remove(o => o.id === object.id);
770
+ }
771
+ /**
772
+ * Updates properties on an object and persists the changes.
773
+ * Calls the object's onAfterUpdate hook with changed property keys.
774
+ * @typeParam T - The type of the object being updated
775
+ * @param object - The object to update
776
+ * @param updatedProperties - Partial object containing properties to update
777
+ */
778
+ updateObject(object, updatedProperties) {
779
+ const changedKeys = [];
780
+ for (const key in updatedProperties) {
781
+ if (updatedProperties.hasOwnProperty(key)) {
782
+ const value = updatedProperties[key];
783
+ if (value !== undefined) {
784
+ object[key] = value;
785
+ changedKeys.push(key);
786
+ }
787
+ }
788
+ }
789
+ object.onAfterUpdate(changedKeys);
790
+ this._store.objects.update(object);
791
+ }
792
+ /**
793
+ * Adds a selection group to the canvas.
794
+ * Removes any existing selection group and selection box first.
795
+ * Triggers the selection change event.
796
+ * @param selectionGroup - The selection group to add
797
+ */
798
+ addSelectionGroup(selectionGroup) {
799
+ this.removeLocalSelectionGroups();
800
+ this.removeSelectionBox();
801
+ for (const object of selectionGroup.objects ?? []) {
802
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelDynamicObject')) {
803
+ const dynamicObject = object;
804
+ if (typeof dynamicObject.setIsInteractive === 'function') {
805
+ dynamicObject.setIsInteractive(true);
806
+ }
807
+ else {
808
+ dynamicObject.isInteractive = true;
809
+ }
810
+ }
811
+ }
812
+ this._store.objects.insert(selectionGroup);
813
+ this._store.setSelectionGroup(selectionGroup);
814
+ this._kritzelEngine.triggerSelectionChange();
815
+ }
816
+ /**
817
+ * Returns whether the provided selection group belongs to the local user context.
818
+ * Local groups are either explicitly owned by the current user or have no user ID.
819
+ */
820
+ isLocalSelectionGroup(selectionGroup) {
821
+ const localUserId = this._user?.id;
822
+ return localUserId == null || selectionGroup.userId === localUserId || selectionGroup.userId == null;
823
+ }
824
+ /**
825
+ * Applies deselection teardown side effects to a selection group before removal.
826
+ */
827
+ teardownSelectionGroup(selectionGroup) {
828
+ selectionGroup.deselectAllChildren?.();
829
+ for (const object of selectionGroup.objects ?? []) {
830
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelDynamicObject')) {
831
+ const dynamicObject = object;
832
+ if (typeof dynamicObject.setIsInteractive === 'function') {
833
+ dynamicObject.setIsInteractive(false);
834
+ }
835
+ else {
836
+ dynamicObject.isInteractive = false;
837
+ }
838
+ }
839
+ }
840
+ }
841
+ /**
842
+ * Removes all local selection groups from the store, including orphaned groups
843
+ * that are no longer referenced by store.selectionGroup.
844
+ * @returns True when at least one local selection group was removed
845
+ */
846
+ removeLocalSelectionGroups() {
847
+ const allSelectionGroupsInStore = this._store.objects
848
+ .allObjects()
849
+ .filter((object) => {
850
+ return object instanceof KritzelSelectionGroup || KritzelClassHelper.isInstanceOf(object, 'KritzelSelectionGroup');
851
+ });
852
+ const allLocalSelectionGroups = allSelectionGroupsInStore.filter(selectionGroup => this.isLocalSelectionGroup(selectionGroup));
853
+ const currentSelectionGroup = this._store.selectionGroup;
854
+ if (currentSelectionGroup &&
855
+ this.isLocalSelectionGroup(currentSelectionGroup) &&
856
+ !allLocalSelectionGroups.some(group => group.id === currentSelectionGroup.id)) {
857
+ allLocalSelectionGroups.push(currentSelectionGroup);
858
+ }
859
+ if (allLocalSelectionGroups.length === 0) {
860
+ return false;
861
+ }
862
+ for (const selectionGroup of allLocalSelectionGroups) {
863
+ this.teardownSelectionGroup(selectionGroup);
864
+ }
865
+ const localSelectionGroupIds = new Set(allLocalSelectionGroups.map(group => group.id));
866
+ this._store.objects.remove(object => localSelectionGroupIds.has(object.id));
867
+ this._store.setSelectionGroup(null);
868
+ return true;
869
+ }
870
+ /**
871
+ * Removes the current selection group from the canvas.
872
+ * Clears the cached reference and triggers the selection change event.
873
+ */
874
+ removeSelectionGroup() {
875
+ const removedAnySelectionGroup = this.removeLocalSelectionGroups();
876
+ if (removedAnySelectionGroup) {
877
+ this._kritzelEngine.triggerSelectionChange();
878
+ }
879
+ }
880
+ /**
881
+ * Removes the current selection box from the canvas.
882
+ * Clears the cached reference.
883
+ */
884
+ removeSelectionBox() {
885
+ const selectionBox = this._store.selectionBox;
886
+ if (selectionBox) {
887
+ this._store.objects.remove(object => object.id === selectionBox.id);
888
+ this._store.setSelectionBox(null);
889
+ this._store.objects.clearLocalSelectionBox();
890
+ }
891
+ }
892
+ /**
893
+ * Deselects all objects by removing the selection group.
894
+ * Triggers a rerender.
895
+ */
896
+ deselectAllObjects() {
897
+ this.removeSelectionGroup();
898
+ this.rerender();
899
+ }
900
+ /**
901
+ * Deletes all objects in the current selection group.
902
+ * Removes the selection group and emits viewport change event.
903
+ */
904
+ delete() {
905
+ const selectionGroup = this._store.selectionGroup;
906
+ if (!selectionGroup) {
907
+ return;
908
+ }
909
+ // Seal the selection group's creation into its own undo step so Yjs
910
+ // doesn't collapse create+delete of the SG into a no-op on undo.
911
+ this._store.objects.stopUndoCapturing();
912
+ this._store.objects.transaction(() => {
913
+ selectionGroup.objects.forEach(obj => this.removeObject(obj));
914
+ this.removeSelectionGroup();
915
+ });
916
+ // Close the deletion step so subsequent unrelated changes start a new step.
917
+ this._store.objects.stopUndoCapturing();
918
+ this.engine.emitObjectsInViewportChange();
919
+ this.rerender();
920
+ }
921
+ /**
922
+ * Undoes the last action in the history stack.
923
+ */
924
+ undo() {
925
+ this._store.objects?.undo();
926
+ }
927
+ /**
928
+ * Redoes the last undone action from the history stack.
929
+ */
930
+ redo() {
931
+ this._store.objects?.redo();
932
+ }
933
+ /**
934
+ * Deletes a specific object by its ID.
935
+ * Emits viewport change event and triggers rerender.
936
+ * @param id - The unique ID of the object to delete
937
+ */
938
+ deleteObject(id) {
939
+ const object = this.findObjectById(id);
940
+ if (object) {
941
+ this.removeObject(object);
942
+ this.engine.emitObjectsInViewportChange();
943
+ this.rerender();
944
+ }
945
+ }
946
+ /**
947
+ * Moves objects one step forward in the z-order stack.
948
+ * @param object - Optional specific object to move; defaults to all selected objects
949
+ */
950
+ bringForward(object) {
951
+ const selectionGroup = this._store.selectionGroup;
952
+ const objects = object ? [object] : selectionGroup?.objects || [];
953
+ const allNonSelectionObjectsWithoutCurrent = this._store.allNonSelectionObjects.filter(o => objects.findIndex(obj => obj.id === o.id) === -1);
954
+ const max = allNonSelectionObjectsWithoutCurrent.length > 0 ? Math.max(...allNonSelectionObjectsWithoutCurrent.map(obj => obj.zIndex)) + 1 : 0;
955
+ objects.forEach(obj => {
956
+ if (obj.zIndex !== max) {
957
+ this.updateObject(obj, { zIndex: obj.zIndex + 1 });
958
+ }
959
+ });
960
+ this.rerender();
961
+ }
962
+ /**
963
+ * Moves objects one step backward in the z-order stack.
964
+ * @param object - Optional specific object to move; defaults to all selected objects
965
+ */
966
+ sendBackward(object) {
967
+ const selectionGroup = this._store.selectionGroup;
968
+ const objects = object ? [object] : selectionGroup?.objects || [];
969
+ const allNonSelectionObjectsWithoutCurrent = this._store.allNonSelectionObjects.filter(o => objects.findIndex(obj => obj.id === o.id) === -1);
970
+ const min = allNonSelectionObjectsWithoutCurrent.length > 0 ? Math.min(...allNonSelectionObjectsWithoutCurrent.map(obj => obj.zIndex)) - 1 : 0;
971
+ objects.forEach(obj => {
972
+ if (obj.zIndex !== min) {
973
+ this.updateObject(obj, { zIndex: obj.zIndex - 1 });
974
+ }
975
+ });
976
+ this.rerender();
977
+ }
978
+ /**
979
+ * Moves objects to the very front of the z-order stack.
980
+ * @param object - Optional specific object to move; defaults to all selected objects
981
+ */
982
+ bringToFront(object) {
983
+ const selectionGroup = this._store.selectionGroup;
984
+ const objects = object ? [object] : selectionGroup?.objects || [];
985
+ const allNonSelectionObjectsWithoutCurrent = this._store.allNonSelectionObjects.filter(o => objects.findIndex(obj => obj.id === o.id) === -1);
986
+ const max = Math.max(...allNonSelectionObjectsWithoutCurrent.map(obj => obj.zIndex)) + 1;
987
+ objects.forEach(obj => {
988
+ this.updateObject(obj, { zIndex: max });
989
+ });
990
+ this.rerender();
991
+ }
992
+ /**
993
+ * Moves objects to the very back of the z-order stack.
994
+ * @param object - Optional specific object to move; defaults to all selected objects
995
+ */
996
+ sendToBack(object) {
997
+ const selectionGroup = this._store.selectionGroup;
998
+ const objects = object ? [object] : selectionGroup?.objects || [];
999
+ const allNonSelectionObjectsWithoutCurrent = this._store.allNonSelectionObjects.filter(o => objects.findIndex(obj => obj.id === o.id) === -1);
1000
+ const min = Math.min(...allNonSelectionObjectsWithoutCurrent.map(obj => obj.zIndex)) - 1;
1001
+ objects.forEach(obj => {
1002
+ this.updateObject(obj, { zIndex: min });
1003
+ });
1004
+ this.rerender();
1005
+ }
1006
+ /**
1007
+ * Aligns objects within the current selection group based on the specified alignment type.
1008
+ * Requires at least 2 objects to be selected.
1009
+ * @param alignment The alignment type to apply
1010
+ */
1011
+ alignObjects(alignment) {
1012
+ const selectionGroup = this._store.selectionGroup;
1013
+ if (!selectionGroup || selectionGroup.objects.length < 2) {
1014
+ return;
1015
+ }
1016
+ const objects = selectionGroup.objects;
1017
+ const alignedPositions = AlignmentHelper.calculateAlignedPositions(objects, alignment);
1018
+ this._store.objects.transaction(() => {
1019
+ for (const obj of objects) {
1020
+ const newPosition = alignedPositions.get(obj.id);
1021
+ if (newPosition) {
1022
+ this.updateObject(obj, {
1023
+ translateX: newPosition.translateX,
1024
+ translateY: newPosition.translateY,
1025
+ });
1026
+ }
1027
+ }
1028
+ });
1029
+ selectionGroup.refreshObjectDimensions();
1030
+ this.rerender();
1031
+ }
1032
+ /**
1033
+ * Groups the currently selected objects into a KritzelGroup.
1034
+ * Requires at least 2 objects to be selected.
1035
+ */
1036
+ group() {
1037
+ const selectionGroup = this._store.selectionGroup;
1038
+ if (!selectionGroup || selectionGroup.objects.length < 2) {
1039
+ return;
1040
+ }
1041
+ // Create a group from the selected objects
1042
+ const group = KritzelGroup.createFromObjects(this, selectionGroup.objects);
1043
+ // Add the group to the object store
1044
+ this.addObject(group);
1045
+ // Remove the selection group and create a new one with just the group
1046
+ this.removeSelectionGroup();
1047
+ const newSelectionGroup = KritzelSelectionGroup.create(this);
1048
+ newSelectionGroup.addOrRemove(group);
1049
+ newSelectionGroup.isSelected = true;
1050
+ newSelectionGroup.rotation = group.rotation;
1051
+ this.addSelectionGroup(newSelectionGroup);
1052
+ this.engine.emitObjectsChange();
1053
+ this.rerender();
1054
+ }
1055
+ /**
1056
+ * Ungroups any KritzelGroup objects in the current selection.
1057
+ * Children are extracted and become top-level objects.
1058
+ */
1059
+ ungroup() {
1060
+ const selectionGroup = this._store.selectionGroup;
1061
+ if (!selectionGroup) {
1062
+ return;
1063
+ }
1064
+ const groups = selectionGroup.objects.filter(obj => KritzelClassHelper.isInstanceOf(obj, 'KritzelGroup'));
1065
+ if (groups.length === 0) {
1066
+ return;
1067
+ }
1068
+ // Collect all children from all groups
1069
+ const allChildren = [];
1070
+ const nonGroupObjects = selectionGroup.objects.filter(obj => !KritzelClassHelper.isInstanceOf(obj, 'KritzelGroup'));
1071
+ groups.forEach(group => {
1072
+ // Get children before removing the group
1073
+ const children = group.ungroup();
1074
+ allChildren.push(...children);
1075
+ // Remove the group from the store (but keep children)
1076
+ this.removeObject(group, true);
1077
+ });
1078
+ // Create a new selection group with all the ungrouped children and non-group objects
1079
+ this.removeSelectionGroup();
1080
+ const newSelectionGroup = KritzelSelectionGroup.create(this);
1081
+ [...allChildren, ...nonGroupObjects].forEach(child => {
1082
+ newSelectionGroup.addOrRemove(child);
1083
+ });
1084
+ newSelectionGroup.isSelected = true;
1085
+ this.addSelectionGroup(newSelectionGroup);
1086
+ this.engine.emitObjectsChange();
1087
+ this.rerender();
1088
+ }
1089
+ /**
1090
+ * Creates a selection from the given objects.
1091
+ * Clears existing selection, creates a selection group, and marks objects as selected.
1092
+ * @param objects - Array of objects to select; no-op if empty
1093
+ */
1094
+ selectObjects(objects) {
1095
+ if (objects.length === 0) {
1096
+ return;
1097
+ }
1098
+ const selectionGroup = KritzelSelectionGroup.create(this);
1099
+ objects.forEach(obj => {
1100
+ obj.isSelected = false;
1101
+ selectionGroup.addOrRemove(obj);
1102
+ });
1103
+ selectionGroup.isSelected = true;
1104
+ if (objects.length === 1) {
1105
+ selectionGroup.rotation = selectionGroup.objects[0].rotation;
1106
+ }
1107
+ this.addSelectionGroup(selectionGroup);
1108
+ this.rerender();
1109
+ }
1110
+ /**
1111
+ * Selects all objects in the workspace.
1112
+ * Excludes selection-related objects.
1113
+ * Switches to selection tool.
1114
+ */
1115
+ selectAllObjects() {
1116
+ const allObjects = this._store.objects
1117
+ .allObjects()
1118
+ .filter(o => !(o instanceof KritzelSelectionGroup) && !(o instanceof KritzelSelectionBox));
1119
+ if (allObjects.length > 0) {
1120
+ const selectionGroup = KritzelSelectionGroup.create(this);
1121
+ allObjects.forEach(obj => {
1122
+ obj.isSelected = false;
1123
+ selectionGroup.addOrRemove(obj);
1124
+ });
1125
+ selectionGroup.isSelected = true;
1126
+ this._store.state.isSelecting = false;
1127
+ if (allObjects.length === 1) {
1128
+ selectionGroup.rotation = selectionGroup.objects[0].rotation;
1129
+ }
1130
+ this.addSelectionGroup(selectionGroup);
1131
+ this._store.setState('activeTool', this._toolRegistry.getTool('selection'));
1132
+ this.rerender();
1133
+ }
1134
+ }
1135
+ /**
1136
+ * Selects all objects currently visible in the viewport.
1137
+ * Excludes selection-related objects.
1138
+ * Switches to selection tool.
1139
+ */
1140
+ selectAllObjectsInViewport() {
1141
+ const viewportBounds = {
1142
+ x: -this._store.state.translateX / this._store.state.scale,
1143
+ y: -this._store.state.translateY / this._store.state.scale,
1144
+ z: this._store.state.scale,
1145
+ width: this._store.state.viewportWidth / this._store.state.scale,
1146
+ height: this._store.state.viewportHeight / this._store.state.scale,
1147
+ depth: 100,
1148
+ };
1149
+ const objectsInViewport = this._store.objects
1150
+ .query(viewportBounds)
1151
+ .filter(o => o.isInViewport())
1152
+ .filter(o => !(o instanceof KritzelSelectionGroup) && !(o instanceof KritzelSelectionBox));
1153
+ if (objectsInViewport.length > 0) {
1154
+ const selectionGroup = KritzelSelectionGroup.create(this);
1155
+ objectsInViewport.forEach(obj => {
1156
+ obj.isSelected = false;
1157
+ selectionGroup.addOrRemove(obj);
1158
+ });
1159
+ selectionGroup.isSelected = true;
1160
+ this._store.state.isSelecting = false;
1161
+ if (objectsInViewport.length === 1) {
1162
+ selectionGroup.rotation = selectionGroup.objects[0].rotation;
1163
+ }
1164
+ this.addSelectionGroup(selectionGroup);
1165
+ this._store.setState('activeTool', this._toolRegistry.getTool('selection'));
1166
+ this.rerender();
1167
+ }
1168
+ }
1169
+ /**
1170
+ * Clears all selection state.
1171
+ * Removes selection group and box, resets selection-related flags.
1172
+ */
1173
+ clearSelection() {
1174
+ this.removeSelectionGroup();
1175
+ const localUserId = this._user?.id;
1176
+ this._store.objects.remove(o => o instanceof KritzelSelectionBox ||
1177
+ (o instanceof KritzelSelectionGroup && (localUserId == null || o.userId === localUserId || o.userId == null)));
1178
+ this._store.setSelectionBox(null);
1179
+ this._store.setSelectionGroup(null);
1180
+ for (const object of this._store.allNonSelectionObjects) {
1181
+ if (object.isSelected) {
1182
+ object.isSelected = false;
1183
+ }
1184
+ }
1185
+ this._store.state.isSelecting = false;
1186
+ this._store.state.isResizeHandleSelected = false;
1187
+ this._store.state.isRotationHandleSelected = false;
1188
+ this._store.state.isLineHandleSelected = false;
1189
+ this._store.state.isLineHandleDragging = false;
1190
+ this._store.state.lineHandleType = undefined;
1191
+ this.rerender();
1192
+ }
1193
+ /**
1194
+ * Resets the active text object.
1195
+ * Deletes empty text objects or saves non-empty ones.
1196
+ */
1197
+ resetActiveText() {
1198
+ const activeText = this._store.activeText;
1199
+ if (activeText) {
1200
+ if (activeText.isEmpty) {
1201
+ this.deleteObject(activeText.id);
1202
+ }
1203
+ else {
1204
+ activeText.save();
1205
+ }
1206
+ }
1207
+ }
1208
+ /**
1209
+ * Resets the active shape object by saving it.
1210
+ */
1211
+ resetActiveShape() {
1212
+ const activeShape = this._store.activeShape;
1213
+ if (activeShape) {
1214
+ activeShape.save();
1215
+ }
1216
+ }
1217
+ /**
1218
+ * Gets the topmost object at a pointer event location that matches a selector.
1219
+ * Performs hit testing to verify the object is actually under the pointer.
1220
+ * @param event - The pointer event with client coordinates
1221
+ * @param selector - CSS selector to match elements; defaults to '.object'
1222
+ * @returns The matched object or null if none found or hit test fails
1223
+ */
1224
+ getObjectFromPointerEvent(event, selector = '.object') {
1225
+ const shadowRoot = this._store.state.host?.shadowRoot;
1226
+ if (!shadowRoot)
1227
+ return null;
1228
+ const clientX = event.clientX;
1229
+ const clientY = event.clientY;
1230
+ const elementAtPoint = shadowRoot.elementFromPoint(clientX, clientY);
1231
+ if (!elementAtPoint)
1232
+ return null;
1233
+ const selectedObject = elementAtPoint.closest(selector);
1234
+ if (selectedObject) {
1235
+ const foundObject = this._store.allObjects.find(object => selectedObject.id === object.id);
1236
+ if (!foundObject)
1237
+ return null;
1238
+ const isHit = foundObject.hitTest(clientX, clientY);
1239
+ return isHit ? foundObject : null;
1240
+ }
1241
+ return null;
1242
+ }
1243
+ /**
1244
+ * Gets all objects at a pointer event location that match a selector.
1245
+ * Returns objects sorted by z-index (highest first) for proper layering.
1246
+ * @param event - The pointer event with client coordinates
1247
+ * @param selector - CSS selector to match elements; defaults to '.object'
1248
+ * @returns Array of matched objects sorted by z-index descending
1249
+ */
1250
+ getObjectsFromPointerEvent(event, selector = '.object') {
1251
+ const shadowRoot = this._store.state.host?.shadowRoot;
1252
+ if (!shadowRoot)
1253
+ return [];
1254
+ const clientX = event.clientX;
1255
+ const clientY = event.clientY;
1256
+ const elementsAtPoint = shadowRoot.elementsFromPoint(clientX, clientY);
1257
+ if (!elementsAtPoint || elementsAtPoint.length === 0)
1258
+ return [];
1259
+ const objectIds = new Set();
1260
+ elementsAtPoint.forEach(element => {
1261
+ const selectedObject = element.closest(selector);
1262
+ if (selectedObject && selectedObject.id) {
1263
+ objectIds.add(selectedObject.id);
1264
+ }
1265
+ });
1266
+ if (objectIds.size > 0) {
1267
+ return this._store.allObjects.filter(object => objectIds.has(object.id)).sort((a, b) => b.zIndex - a.zIndex);
1268
+ }
1269
+ return [];
1270
+ }
1271
+ /**
1272
+ * Get all elements at a pointer position that match a given selector.
1273
+ * Uses elementsFromPoint to find overlapped elements.
1274
+ */
1275
+ getElementsAtPoint(event, selector) {
1276
+ const shadowRoot = this._store.state.host?.shadowRoot;
1277
+ if (!shadowRoot)
1278
+ return [];
1279
+ const elementsAtPoint = shadowRoot.elementsFromPoint(event.clientX, event.clientY);
1280
+ if (!elementsAtPoint || elementsAtPoint.length === 0)
1281
+ return [];
1282
+ return elementsAtPoint.filter(el => el.matches(selector));
1283
+ }
1284
+ /**
1285
+ * Check if any element at the pointer position matches the given selector.
1286
+ * Useful for detecting hover states on overlapped elements.
1287
+ */
1288
+ isPointerOverElement(event, selector) {
1289
+ return this.getElementsAtPoint(event, selector).length > 0;
1290
+ }
1291
+ /**
1292
+ * Converts a pointer event to canvas (world) coordinates.
1293
+ * Accounts for host element offset and viewport transform.
1294
+ * @param event - The pointer event with client coordinates
1295
+ * @returns Object with x and y in canvas coordinates
1296
+ */
1297
+ getCanvasPoint(event) {
1298
+ if (!this._store.state.host) {
1299
+ return { x: 0, y: 0 };
1300
+ }
1301
+ const hostRect = this._store.state.host.getBoundingClientRect();
1302
+ const xRelativeToHost = event.clientX - hostRect.left;
1303
+ const yRelativeToHost = event.clientY - hostRect.top;
1304
+ const xWithoutTranslate = xRelativeToHost - this._store.state.translateX;
1305
+ const yWithoutTranslate = yRelativeToHost - this._store.state.translateY;
1306
+ const worldX = xWithoutTranslate / this._store.state.scale;
1307
+ const worldY = yWithoutTranslate / this._store.state.scale;
1308
+ return { x: worldX, y: worldY };
1309
+ }
1310
+ /**
1311
+ * Prepares the engine for a workspace change.
1312
+ * Cancels pending viewport updates, saves current viewport state,
1313
+ * resets active text, clears selection, and switches to selection tool.
1314
+ */
1315
+ beforeWorkspaceChange() {
1316
+ // Cancel any pending debounced viewport updates to prevent them from
1317
+ // saving the old workspace's viewport to the new workspace
1318
+ this._kritzelEngine.viewport?.cancelPendingUpdates();
1319
+ // Immediately save the current workspace's viewport before switching
1320
+ const currentWorkspace = this._store.state.activeWorkspace;
1321
+ if (currentWorkspace) {
1322
+ this.updateWorkspaceViewport(this._store.state.translateX, this._store.state.translateY, this._store.state.scale);
1323
+ }
1324
+ this.resetActiveText();
1325
+ this.clearSelection();
1326
+ this._store.setState('activeTool', this._toolRegistry.getTool('selection'));
1327
+ }
1328
+ /**
1329
+ * Determines whether to display the selection group UI for an object.
1330
+ * Hides UI for single KritzelLine selections (they have their own UI).
1331
+ * @param object - The object to check
1332
+ * @returns True if selection group UI should be displayed
1333
+ */
1334
+ displaySelectionGroupUI(object) {
1335
+ if (!object.isSelected) {
1336
+ return false;
1337
+ }
1338
+ // Remote selection groups always show group UI (border only, no handles)
1339
+ if (object instanceof KritzelSelectionGroup && this._user?.id != null && object.userId != null && object.userId !== this._user.id) {
1340
+ return true;
1341
+ }
1342
+ const selectionGroup = this._store.selectionGroup;
1343
+ if (!selectionGroup) {
1344
+ // During selection phase (no group yet), hide UI for KritzelLine objects
1345
+ return !(object instanceof KritzelLine);
1346
+ }
1347
+ // Once a local selection group exists, only the selection-group object
1348
+ // should render group UI (children remain selected but don't render handles).
1349
+ if (!(object instanceof KritzelSelectionGroup) || object.id !== selectionGroup.id) {
1350
+ return false;
1351
+ }
1352
+ // Show UI if selection contains more than one object
1353
+ if (selectionGroup.objects.length > 1) {
1354
+ return true;
1355
+ }
1356
+ // Hide UI if selection contains a single KritzelLine
1357
+ if (selectionGroup.objects.length === 1) {
1358
+ const selectedObject = selectionGroup.objects[0];
1359
+ return !(selectedObject instanceof KritzelLine);
1360
+ }
1361
+ return true;
1362
+ }
1363
+ /**
1364
+ * Determines whether to display the line-specific selection UI for an object.
1365
+ * Shows line UI only for single KritzelLine selections.
1366
+ * @param object - The object to check
1367
+ * @returns True if line selection UI should be displayed
1368
+ */
1369
+ displaySelectionLineUI(object) {
1370
+ // Only show line UI on KritzelLine objects, not on selection groups
1371
+ if (!(object instanceof KritzelLine)) {
1372
+ return false;
1373
+ }
1374
+ // During selection phase (no group yet), show line UI if line is selected
1375
+ const selectionGroup = this._store.selectionGroup;
1376
+ if (!selectionGroup) {
1377
+ return object.isSelected;
1378
+ }
1379
+ if (!selectionGroup.isSelected) {
1380
+ return false;
1381
+ }
1382
+ // Show UI only if selection contains exactly one KritzelLine and it's this object
1383
+ if (selectionGroup.objects.length === 1) {
1384
+ const selectedObject = selectionGroup.objects[0];
1385
+ return selectedObject instanceof KritzelLine && selectedObject.id === object.id;
1386
+ }
1387
+ return false;
1388
+ }
1389
+ }