@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,3456 @@
1
+ import { p as proxyCustomElement, t as transformTag, H, h, d as Host } from './p-CfSx6HU_.js';
2
+
3
+ class ObjectHelper {
4
+ static generateUUID(length = 16) {
5
+ const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
6
+ const bytes = crypto.getRandomValues(new Uint8Array(length));
7
+ return Array.from(bytes)
8
+ .map(byte => alphabet[byte % alphabet.length])
9
+ .join('');
10
+ }
11
+ static isEmpty(obj) {
12
+ if (obj === null || obj === undefined) {
13
+ return true;
14
+ }
15
+ return Object?.keys(obj).length === 0 && obj?.constructor === Object;
16
+ }
17
+ static isClass(object, className) {
18
+ return !!object && object.__class__ === className;
19
+ }
20
+ static isKritzelExportable(object) {
21
+ if (!object || typeof object !== 'object') {
22
+ return false;
23
+ }
24
+ const candidate = object;
25
+ if (typeof candidate.getSupportedExportFormats !== 'function' || typeof candidate.exportAs !== 'function') {
26
+ return false;
27
+ }
28
+ const formats = candidate.getSupportedExportFormats();
29
+ return Array.isArray(formats) && formats.length > 0;
30
+ }
31
+ }
32
+
33
+ class KritzelGeometryHelper {
34
+ static doPolygonsIntersect(polygon1, polygon2) {
35
+ // 1. Convert polygons to array of points for easier processing
36
+ const points1 = [polygon1.bottomLeft, polygon1.bottomRight, polygon1.topRight, polygon1.topLeft];
37
+ const points2 = [polygon2.bottomLeft, polygon2.bottomRight, polygon2.topRight, polygon2.topLeft];
38
+ // 2. Check if any point of polygon1 is inside polygon2
39
+ for (const point of points1) {
40
+ if (this.isPointInPolygon(point, points2)) {
41
+ return true;
42
+ }
43
+ }
44
+ // 3. Check if any point of polygon2 is inside polygon1
45
+ for (const point of points2) {
46
+ if (this.isPointInPolygon(point, points1)) {
47
+ return true;
48
+ }
49
+ }
50
+ // 4. Check for edge intersections (more complex)
51
+ for (let i = 0; i < points1.length; i++) {
52
+ const p1a = points1[i];
53
+ const p1b = points1[(i + 1) % points1.length]; // Wrap around to the first point
54
+ for (let j = 0; j < points2.length; j++) {
55
+ const p2a = points2[j];
56
+ const p2b = points2[(j + 1) % points2.length];
57
+ if (this.intersectLines(p1a, p1b, p2a, p2b)) {
58
+ return true;
59
+ }
60
+ }
61
+ }
62
+ return false; // No intersection found
63
+ }
64
+ static isPointInPolygon(point, polygon) {
65
+ let inside = false;
66
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
67
+ const xi = polygon[i].x, yi = polygon[i].y;
68
+ const xj = polygon[j].x, yj = polygon[j].y;
69
+ const intersect = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
70
+ if (intersect)
71
+ inside = !inside;
72
+ }
73
+ return inside;
74
+ }
75
+ static intersectLines(p1a, p1b, p2a, p2b) {
76
+ const det = (p1b.x - p1a.x) * (p2b.y - p2a.y) - (p1b.y - p1a.y) * (p2b.x - p2a.x);
77
+ if (det === 0) {
78
+ return false; // Lines are parallel
79
+ }
80
+ const t = ((p2a.x - p1a.x) * (p2b.y - p2a.y) - (p2a.y - p1a.y) * (p2b.x - p2a.x)) / det;
81
+ const u = -((p1a.x - p2a.x) * (p1b.y - p1a.y) - (p1a.y - p2a.y) * (p1b.x - p1a.x)) / det;
82
+ return t >= 0 && t <= 1 && u >= 0 && u <= 1;
83
+ }
84
+ /**
85
+ * Finds the intersection point between a line segment and a line segment.
86
+ * Returns the intersection point or null if no intersection.
87
+ */
88
+ static getLineIntersectionPoint(p1a, p1b, p2a, p2b) {
89
+ const det = (p1b.x - p1a.x) * (p2b.y - p2a.y) - (p1b.y - p1a.y) * (p2b.x - p2a.x);
90
+ if (det === 0) {
91
+ return null; // Lines are parallel
92
+ }
93
+ const t = ((p2a.x - p1a.x) * (p2b.y - p2a.y) - (p2a.y - p1a.y) * (p2b.x - p2a.x)) / det;
94
+ const u = -((p1a.x - p2a.x) * (p1b.y - p1a.y) - (p1a.y - p2a.y) * (p1b.x - p1a.x)) / det;
95
+ if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
96
+ return {
97
+ x: p1a.x + t * (p1b.x - p1a.x),
98
+ y: p1a.y + t * (p1b.y - p1a.y)
99
+ };
100
+ }
101
+ return null;
102
+ }
103
+ /**
104
+ * Finds the closest intersection point between a line segment (from lineStart to lineEnd)
105
+ * and a polygon. Returns the intersection point closest to lineStart, or null if no intersection.
106
+ */
107
+ static getLinePolygonIntersection(lineStart, lineEnd, polygon) {
108
+ const points = [polygon.topLeft, polygon.topRight, polygon.bottomRight, polygon.bottomLeft];
109
+ let closestIntersection = null;
110
+ let closestDistance = Infinity;
111
+ for (let i = 0; i < points.length; i++) {
112
+ const edgeStart = points[i];
113
+ const edgeEnd = points[(i + 1) % points.length];
114
+ const intersection = this.getLineIntersectionPoint(lineStart, lineEnd, edgeStart, edgeEnd);
115
+ if (intersection) {
116
+ const distance = Math.sqrt(Math.pow(intersection.x - lineStart.x, 2) +
117
+ Math.pow(intersection.y - lineStart.y, 2));
118
+ if (distance < closestDistance) {
119
+ closestDistance = distance;
120
+ closestIntersection = intersection;
121
+ }
122
+ }
123
+ }
124
+ return closestIntersection;
125
+ }
126
+ /**
127
+ * Generates a polygon approximation of an ellipse.
128
+ * @param centerX - X coordinate of ellipse center
129
+ * @param centerY - Y coordinate of ellipse center
130
+ * @param rx - Horizontal radius
131
+ * @param ry - Vertical radius
132
+ * @param segments - Number of segments (more = smoother approximation)
133
+ * @param rotation - Optional rotation angle in radians
134
+ */
135
+ static getEllipsePolygonApproximation(centerX, centerY, rx, ry, segments = 32, rotation = 0) {
136
+ const points = [];
137
+ const cos = Math.cos(rotation);
138
+ const sin = Math.sin(rotation);
139
+ for (let i = 0; i < segments; i++) {
140
+ const angle = (2 * Math.PI * i) / segments;
141
+ // Point on unrotated ellipse
142
+ const px = rx * Math.cos(angle);
143
+ const py = ry * Math.sin(angle);
144
+ // Apply rotation around center
145
+ const rotatedX = centerX + px * cos - py * sin;
146
+ const rotatedY = centerY + px * sin + py * cos;
147
+ points.push({ x: rotatedX, y: rotatedY });
148
+ }
149
+ return points;
150
+ }
151
+ /**
152
+ * Finds the closest intersection point between a line segment and a polygon
153
+ * defined as an array of points. Returns the intersection closest to lineStart,
154
+ * or null if no intersection.
155
+ */
156
+ static getLinePointsArrayIntersection(lineStart, lineEnd, polygonPoints) {
157
+ let closestIntersection = null;
158
+ let closestDistance = Infinity;
159
+ for (let i = 0; i < polygonPoints.length; i++) {
160
+ const edgeStart = polygonPoints[i];
161
+ const edgeEnd = polygonPoints[(i + 1) % polygonPoints.length];
162
+ const intersection = this.getLineIntersectionPoint(lineStart, lineEnd, edgeStart, edgeEnd);
163
+ if (intersection) {
164
+ const distance = Math.sqrt(Math.pow(intersection.x - lineStart.x, 2) +
165
+ Math.pow(intersection.y - lineStart.y, 2));
166
+ if (distance < closestDistance) {
167
+ closestDistance = distance;
168
+ closestIntersection = intersection;
169
+ }
170
+ }
171
+ }
172
+ return closestIntersection;
173
+ }
174
+ /**
175
+ * Checks if a point is inside a polygon defined as an array of points.
176
+ * This is a convenience wrapper that works with arbitrary polygon point arrays.
177
+ */
178
+ static isPointInPolygonPoints(point, polygonPoints) {
179
+ return this.isPointInPolygon(point, polygonPoints);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Base class for all objects that can be rendered on the Kritzel canvas.
185
+ * Provides common functionality for positioning, transformation, hit testing,
186
+ * serialization, and lifecycle management.
187
+ * @template T - The type of the underlying DOM element (HTMLElement or SVGElement)
188
+ */
189
+ class KritzelBaseObject {
190
+ __class__ = 'KritzelBaseObject';
191
+ _core;
192
+ _elementRef;
193
+ id;
194
+ workspaceId;
195
+ x;
196
+ y;
197
+ translateX;
198
+ translateY;
199
+ height;
200
+ width;
201
+ backgroundColor;
202
+ borderColor;
203
+ borderWidth = 0;
204
+ opacity = 1;
205
+ padding = 0;
206
+ scale;
207
+ resizing = false;
208
+ rotation = 0;
209
+ markedForRemoval = false;
210
+ zIndex = 0;
211
+ userId;
212
+ isVisible = true;
213
+ isSelected = false;
214
+ isHovered = false;
215
+ isMounted = false;
216
+ isEditable = false;
217
+ isInteractive = false;
218
+ isResizable = true;
219
+ isRotatable = true;
220
+ /**
221
+ * Gets the total width of the object including padding.
222
+ * @returns The width plus double the padding value
223
+ */
224
+ get totalWidth() {
225
+ return this.width + this.padding * 2;
226
+ }
227
+ /**
228
+ * Gets the total height of the object including padding.
229
+ * @returns The height plus double the padding value
230
+ */
231
+ get totalHeight() {
232
+ return this.height + this.padding * 2;
233
+ }
234
+ /**
235
+ * Sets the underlying DOM element reference for this object.
236
+ * @param element - The DOM element to associate with this object
237
+ */
238
+ set elementRef(element) {
239
+ this._elementRef = element;
240
+ }
241
+ /**
242
+ * Gets the underlying DOM element reference for this object.
243
+ * @returns The associated DOM element
244
+ */
245
+ get elementRef() {
246
+ return this._elementRef;
247
+ }
248
+ /**
249
+ * Gets the axis-aligned bounding box of the object without rotation.
250
+ * @returns The bounding box with position, scale, and dimensions
251
+ */
252
+ get boundingBox() {
253
+ return {
254
+ x: this.translateX,
255
+ y: this.translateY,
256
+ z: this.scale,
257
+ width: this.totalWidth / this.scale,
258
+ height: this.totalHeight / this.scale,
259
+ };
260
+ }
261
+ /**
262
+ * Gets the axis-aligned bounding box that encompasses the rotated object.
263
+ * Calculates the smallest rectangle that contains all four rotated corners.
264
+ * @returns The bounding box accounting for rotation
265
+ */
266
+ get rotatedBoundingBox() {
267
+ const polygon = this.rotatedPolygon;
268
+ const xValues = [polygon.topLeft.x, polygon.topRight.x, polygon.bottomRight.x, polygon.bottomLeft.x];
269
+ const yValues = [polygon.topLeft.y, polygon.topRight.y, polygon.bottomRight.y, polygon.bottomLeft.y];
270
+ const minX = Math.min(...xValues);
271
+ const maxX = Math.max(...xValues);
272
+ const minY = Math.min(...yValues);
273
+ const maxY = Math.max(...yValues);
274
+ return {
275
+ x: minX,
276
+ y: minY,
277
+ z: this.scale,
278
+ width: maxX - minX,
279
+ height: maxY - minY,
280
+ };
281
+ }
282
+ /**
283
+ * Gets the four corners of the object as a polygon, accounting for rotation.
284
+ * Uses optimized trigonometric calculations with precomputed cos/sin values.
285
+ * @returns A polygon with topLeft, topRight, bottomRight, and bottomLeft coordinates
286
+ */
287
+ get rotatedPolygon() {
288
+ const cx = this.translateX + this.totalWidth / 2 / this.scale;
289
+ const cy = this.translateY + this.totalHeight / 2 / this.scale;
290
+ const angle = this.rotation;
291
+ // Optimization: Precomputes cos/sin once
292
+ const cos = Math.cos(angle);
293
+ const sin = Math.sin(angle);
294
+ const adjustedWidth = this.totalWidth / this.scale;
295
+ const adjustedHeight = this.totalHeight / this.scale;
296
+ const x1 = this.translateX;
297
+ const y1 = this.translateY;
298
+ const x2 = x1 + adjustedWidth;
299
+ const y2 = y1 + adjustedHeight;
300
+ // Helper to rotate a point around center
301
+ const rotate = (x, y) => ({
302
+ x: cos * (x - cx) - sin * (y - cy) + cx,
303
+ y: sin * (x - cx) + cos * (y - cy) + cy,
304
+ });
305
+ return {
306
+ topLeft: rotate(x1, y1),
307
+ topRight: rotate(x2, y1),
308
+ bottomRight: rotate(x2, y2),
309
+ bottomLeft: rotate(x1, y2),
310
+ };
311
+ }
312
+ /**
313
+ * Gets the minimum X coordinate among all rotated corners.
314
+ * @returns The leftmost X coordinate of the rotated object
315
+ */
316
+ get minXRotated() {
317
+ const corners = [this.rotatedPolygon.topLeft.x, this.rotatedPolygon.topRight.x, this.rotatedPolygon.bottomRight.x, this.rotatedPolygon.bottomLeft.x];
318
+ return Math.min(...corners);
319
+ }
320
+ /**
321
+ * Gets the minimum Y coordinate among all rotated corners.
322
+ * @returns The topmost Y coordinate of the rotated object
323
+ */
324
+ get minYRotated() {
325
+ const corners = [this.rotatedPolygon.topLeft.y, this.rotatedPolygon.topRight.y, this.rotatedPolygon.bottomRight.y, this.rotatedPolygon.bottomLeft.y];
326
+ return Math.min(...corners);
327
+ }
328
+ /**
329
+ * Gets the maximum X coordinate among all rotated corners.
330
+ * @returns The rightmost X coordinate of the rotated object
331
+ */
332
+ get maxXRotated() {
333
+ const corners = [this.rotatedPolygon.topLeft.x, this.rotatedPolygon.topRight.x, this.rotatedPolygon.bottomRight.x, this.rotatedPolygon.bottomLeft.x];
334
+ return Math.max(...corners);
335
+ }
336
+ /**
337
+ * Gets the maximum Y coordinate among all rotated corners.
338
+ * @returns The bottommost Y coordinate of the rotated object
339
+ */
340
+ get maxYRotated() {
341
+ const corners = [this.rotatedPolygon.topLeft.y, this.rotatedPolygon.topRight.y, this.rotatedPolygon.bottomRight.y, this.rotatedPolygon.bottomLeft.y];
342
+ return Math.max(...corners);
343
+ }
344
+ /**
345
+ * Gets the CSS transformation matrix string for positioning and scaling.
346
+ * @returns A CSS matrix() function string combining scale and translation
347
+ */
348
+ get transformationMatrix() {
349
+ const scale = 1 / this.scale;
350
+ const translateX = this.translateX;
351
+ const translateY = this.translateY;
352
+ return `matrix(${scale}, 0, 0, ${scale}, ${translateX}, ${translateY})`;
353
+ }
354
+ /**
355
+ * Gets the rotation angle converted from radians to degrees.
356
+ * @returns The rotation angle in degrees
357
+ */
358
+ get rotationDegrees() {
359
+ return this.rotation * (180 / Math.PI);
360
+ }
361
+ /**
362
+ * Gets the X coordinate of the object's center point.
363
+ * @returns The center X position in world coordinates
364
+ */
365
+ get centerX() {
366
+ return this.translateX + this.totalWidth / 2 / this.scale;
367
+ }
368
+ /**
369
+ * Gets the Y coordinate of the object's center point.
370
+ * @returns The center Y position in world coordinates
371
+ */
372
+ get centerY() {
373
+ return this.translateY + this.totalHeight / 2 / this.scale;
374
+ }
375
+ /**
376
+ * Creates a new KritzelBaseObject instance with a generated unique ID.
377
+ */
378
+ constructor() {
379
+ this.id = this.generateId();
380
+ }
381
+ /**
382
+ * Factory method to create a new KritzelBaseObject with core context.
383
+ * Automatically assigns the current z-index and active workspace ID.
384
+ * @param core - The KritzelCore instance providing context and state
385
+ * @returns A new KritzelBaseObject instance configured with core context
386
+ */
387
+ static create(core) {
388
+ const object = new KritzelBaseObject();
389
+ object._core = core;
390
+ object.zIndex = core.store.currentZIndex;
391
+ object.workspaceId = core.store.state.activeWorkspace.id;
392
+ object.userId = core.user?.id;
393
+ return object;
394
+ }
395
+ /**
396
+ * Mounts the object to a DOM element. Only executes once; subsequent calls are ignored.
397
+ * Sets the element reference and marks the object as mounted.
398
+ * @param element - The DOM element to mount this object to
399
+ */
400
+ mount(element) {
401
+ if (this.isMounted) {
402
+ return;
403
+ }
404
+ this.elementRef = element;
405
+ this.isMounted = true;
406
+ }
407
+ /**
408
+ * Generates a unique identifier for the object.
409
+ * @returns A UUID string
410
+ */
411
+ generateId() {
412
+ return ObjectHelper.generateUUID();
413
+ }
414
+ /**
415
+ * Checks whether the object is currently visible within the viewport.
416
+ * Objects smaller than 0.5 square pixels (when scaled) are considered invisible.
417
+ * Uses the rotated bounding box for accurate intersection testing.
418
+ * @returns True if the object intersects with the visible viewport area
419
+ */
420
+ isInViewport() {
421
+ const viewportScale = this._core.store.state.scale;
422
+ const scaledWidth = this.boundingBox.width * viewportScale;
423
+ const scaledHeight = this.boundingBox.height * viewportScale;
424
+ if (scaledWidth * scaledHeight < 0.5) {
425
+ return false;
426
+ }
427
+ const viewportBounds = {
428
+ x: -this._core.store.state.translateX / this._core.store.state.scale,
429
+ y: -this._core.store.state.translateY / this._core.store.state.scale,
430
+ width: this._core.store.state.viewportWidth / this._core.store.state.scale,
431
+ height: this._core.store.state.viewportHeight / this._core.store.state.scale,
432
+ };
433
+ return (this.rotatedBoundingBox.x < viewportBounds.x + viewportBounds.width &&
434
+ this.rotatedBoundingBox.x + this.rotatedBoundingBox.width > viewportBounds.x &&
435
+ this.rotatedBoundingBox.y < viewportBounds.y + viewportBounds.height &&
436
+ this.rotatedBoundingBox.y + this.rotatedBoundingBox.height > viewportBounds.y);
437
+ }
438
+ /**
439
+ * Moves the object to the center of the current viewport.
440
+ * Calculates the offset needed to align the object's center with the viewport's center.
441
+ */
442
+ centerInViewport() {
443
+ const { viewportWidth, viewportHeight, translateX: viewportTranslateX, translateY: viewportTranslateY, scale: viewportScale } = this._core.store.state;
444
+ const { x, y, width, height } = this.rotatedBoundingBox;
445
+ const objectCenterX = x + width / 2;
446
+ const objectCenterY = y + height / 2;
447
+ const targetCenterX = (viewportWidth / 2 - viewportTranslateX) / viewportScale;
448
+ const targetCenterY = (viewportHeight / 2 - viewportTranslateY) / viewportScale;
449
+ const deltaX = targetCenterX - objectCenterX;
450
+ const deltaY = targetCenterY - objectCenterY;
451
+ this.updatePosition(this.translateX + deltaX, this.translateY + deltaY);
452
+ }
453
+ /**
454
+ * Notifies the store that this object has been updated.
455
+ * Triggers a re-render of the object in the canvas.
456
+ */
457
+ update() {
458
+ this._core.store.objects.update(this);
459
+ }
460
+ /**
461
+ * Moves the object based on the delta between start and end coordinates.
462
+ * The movement is scaled according to the current viewport scale.
463
+ * @param startX - The starting X coordinate in screen space
464
+ * @param startY - The starting Y coordinate in screen space
465
+ * @param endX - The ending X coordinate in screen space
466
+ * @param endY - The ending Y coordinate in screen space
467
+ */
468
+ move(startX, startY, endX, endY) {
469
+ const deltaX = (startX - endX) / this._core.store.state.scale;
470
+ const deltaY = (startY - endY) / this._core.store.state.scale;
471
+ this.translateX += deltaX;
472
+ this.translateY += deltaY;
473
+ this._core.store.objects.update(this);
474
+ }
475
+ /**
476
+ * Resizes the object to new dimensions and position.
477
+ * Ignores resize requests that would make the object smaller than 1x1 pixels.
478
+ * Also updates any lines anchored to this object after resizing.
479
+ * @param x - The new X position (translateX)
480
+ * @param y - The new Y position (translateY)
481
+ * @param width - The new width (must be > 1)
482
+ * @param height - The new height (must be > 1)
483
+ */
484
+ resize(x, y, width, height) {
485
+ if (width <= 1 || height <= 1) {
486
+ return;
487
+ }
488
+ this.width = width;
489
+ this.height = height;
490
+ this.translateX = x;
491
+ this.translateY = y;
492
+ this._core.store.objects.update(this);
493
+ // Update any lines that are anchored to this object (after position is updated)
494
+ this._core.anchorManager.updateAnchorsForObject(this.id);
495
+ }
496
+ /**
497
+ * Sets the rotation angle of the object.
498
+ * @param value - The rotation angle in radians
499
+ */
500
+ rotate(value) {
501
+ this.rotation = value;
502
+ this._core.store.objects.update(this);
503
+ }
504
+ /**
505
+ * Creates a shallow clone of this object with the same ID.
506
+ * The clone shares the same prototype and copies all properties.
507
+ * @returns A new instance with identical properties including the same ID
508
+ */
509
+ clone() {
510
+ const clone = Object.create(Object.getPrototypeOf(this));
511
+ Object.assign(clone, this);
512
+ clone.id = this.id;
513
+ return clone;
514
+ }
515
+ /**
516
+ * Creates a copy of this object with a new unique ID.
517
+ * Unlike clone(), this creates an independent object suitable for duplication.
518
+ * The copied object is not mounted by default.
519
+ * @returns A new instance with copied properties and a new unique ID
520
+ */
521
+ copy() {
522
+ const copiedObject = Object.create(Object.getPrototypeOf(this));
523
+ Object.assign(copiedObject, this);
524
+ copiedObject.id = this.generateId();
525
+ copiedObject.isMounted = false;
526
+ copiedObject.isSelected = false;
527
+ copiedObject.isHovered = false;
528
+ copiedObject.isInteractive = false;
529
+ copiedObject.resizing = false;
530
+ copiedObject.markedForRemoval = false;
531
+ copiedObject.isVisible = true;
532
+ return copiedObject;
533
+ }
534
+ /**
535
+ * Serializes the object to a plain JavaScript object for persistence.
536
+ * Excludes internal references (_core, _elementRef) and computed properties.
537
+ * Subclasses should override to handle domain-specific serialization (e.g., HTML elements).
538
+ * @returns A serializable object containing all persistent properties
539
+ */
540
+ serialize() {
541
+ const { _core, _elementRef, element, totalWidth, totalHeight, isSelected, isHovered, isMounted, isInteractive, resizing, markedForRemoval, isVisible, ...remainingProps } = this;
542
+ // No longer clone raw elements—let subclasses handle serializable payloads
543
+ return structuredClone(remainingProps);
544
+ }
545
+ /**
546
+ * Restores the object's state from a serialized plain object.
547
+ * Merges all properties from the input object into this instance.
548
+ * @template T - The expected return type
549
+ * @param object - The serialized object data to restore from
550
+ * @returns This instance cast to type T
551
+ */
552
+ deserialize(object) {
553
+ Object.assign(this, object);
554
+ // Selection/interactivity state is transient UI state and must not survive revival.
555
+ this.isSelected = false;
556
+ this.isHovered = false;
557
+ this.isMounted = false;
558
+ this.isInteractive = false;
559
+ this.resizing = false;
560
+ this.markedForRemoval = false;
561
+ this.isVisible = true;
562
+ // Reset known transient geometry caches if present on subclasses.
563
+ if ('_adjustedPoints' in this) {
564
+ this._adjustedPoints = null;
565
+ }
566
+ if ('_clipInfo' in this) {
567
+ this._clipInfo = null;
568
+ }
569
+ return this;
570
+ }
571
+ /**
572
+ * Copies transient (non-persisted) state from a previous local instance
573
+ * onto this freshly-revived instance. Called when a remote Yjs update
574
+ * replaces an existing object so that local-only fields (e.g. asset
575
+ * load state, resolved blob URLs) survive across remote position
576
+ * updates and don't visually flicker.
577
+ *
578
+ * Default implementation is a no-op; subclasses with transient fields
579
+ * should override.
580
+ * @param previous - The previous local instance being replaced.
581
+ */
582
+ adoptTransientStateFrom(_previous) {
583
+ // no-op by default
584
+ }
585
+ /**
586
+ * Type guard to check if this object is of a specific class type.
587
+ * Compares against the __class__ property.
588
+ * @template T - The expected class type extending KritzelBaseObject
589
+ * @param className - The class name to check against
590
+ * @returns True if this object's __class__ matches the given class name
591
+ */
592
+ isClass(className) {
593
+ return this.__class__ === className;
594
+ }
595
+ /**
596
+ * Handles edit actions on the object. Override in subclasses to implement
597
+ * custom edit behavior (e.g., entering text edit mode).
598
+ * @param _event - Optional pointer event that triggered the edit
599
+ */
600
+ edit(_event) {
601
+ // This method can be overridden by subclasses to handle edit actions.
602
+ }
603
+ /**
604
+ * Lifecycle hook called after properties are updated via updateObject.
605
+ * Override in subclasses to perform custom logic when specific properties change.
606
+ * @param _changedProperties - Array of property names that were changed
607
+ */
608
+ onAfterUpdate(_changedProperties) {
609
+ // Default implementation does nothing
610
+ }
611
+ /**
612
+ * Tests if a point intersects with this object.
613
+ * Override in subclasses for precise hit detection (e.g., path-based objects).
614
+ * @param _x - The X coordinate to test in world space
615
+ * @param _y - The Y coordinate to test in world space
616
+ * @returns True if the point hits the object (default always returns true)
617
+ */
618
+ hitTest(_x, _y) {
619
+ return true; // Default implementation, can be overridden by subclasses
620
+ }
621
+ /**
622
+ * Tests if this object's rotated polygon intersects with another polygon.
623
+ * Useful for lasso selection and collision detection.
624
+ * @param polygon - The polygon to test intersection against
625
+ * @returns True if the polygons intersect
626
+ */
627
+ hitTestPolygon(polygon) {
628
+ const objectPolygon = this.rotatedPolygon;
629
+ return KritzelGeometryHelper.doPolygonsIntersect(objectPolygon, polygon);
630
+ }
631
+ /**
632
+ * Updates the object's position and notifies the store.
633
+ * @param x - The new X position (translateX)
634
+ * @param y - The new Y position (translateY)
635
+ */
636
+ updatePosition(x, y) {
637
+ this.translateX = x;
638
+ this.translateY = y;
639
+ this._core.store.objects.update(this);
640
+ }
641
+ }
642
+
643
+ /**
644
+ * Helper function to resolve a color value for a specific theme
645
+ */
646
+ function resolveColor(color, theme) {
647
+ if (theme in color && color[theme]) {
648
+ return color[theme];
649
+ }
650
+ return color[theme === 'dark' ? 'dark' : 'light'];
651
+ }
652
+ /**
653
+ * Default color palette shared across all tool configurations.
654
+ * This ensures consistency in color options throughout the application.
655
+ * All colors support theme-specific variations.
656
+ */
657
+ const DEFAULT_COLOR_PALETTE = [
658
+ { light: '#000000', dark: '#ffffff', label: 'Primary' },
659
+ { light: '#ff5252', dark: '#ff5252' },
660
+ { light: '#ffbc00', dark: '#ffbc00' },
661
+ { light: '#00c853', dark: '#00c853' },
662
+ { light: '#0000FF', dark: '#0000FF' },
663
+ { light: '#d500f9', dark: '#d500f9' },
664
+ { light: '#fafafa', dark: '#212121', label: 'Background' },
665
+ { light: '#a52714', dark: '#a52714' },
666
+ { light: '#ee8100', dark: '#ee8100' },
667
+ { light: '#558b2f', dark: '#558b2f' },
668
+ { light: '#01579b', dark: '#01579b' },
669
+ { light: '#8e24aa', dark: '#8e24aa' },
670
+ { light: '#90a4ae', dark: '#607d8b', label: 'Neutral' },
671
+ { light: '#ff4081', dark: '#ff4081' },
672
+ { light: '#ff6e40', dark: '#ff6e40' },
673
+ { light: '#aeea00', dark: '#aeea00' },
674
+ { light: '#304ffe', dark: '#304ffe' },
675
+ { light: '#7c4dff', dark: '#7c4dff' },
676
+ { light: '#cfd8dc', dark: '#455a64' },
677
+ { light: '#f8bbd0', dark: '#ec407a' },
678
+ { light: '#ffccbc', dark: '#ff7043' },
679
+ { light: '#f0f4c3', dark: '#c0ca33' },
680
+ { light: '#9fa8da', dark: '#5c6bc0' },
681
+ { light: '#d1c4e9', dark: '#9575cd' },
682
+ ];
683
+
684
+ /**
685
+ * Light theme preset - the default theme
686
+ */
687
+ const lightTheme = {
688
+ name: 'light',
689
+ global: {
690
+ borderColor: '#ebebeb',
691
+ dividerColor: '#e0e0e0',
692
+ focusColor: '#333333',
693
+ focusRingColor: '#333333',
694
+ iconColor: 'currentColor',
695
+ scrollbarThumbColor: '#ebebeb',
696
+ textPrimary: '#000000',
697
+ textSecondary: '#333333',
698
+ },
699
+ pillTabs: {
700
+ background: '#f0f0f0',
701
+ tabBackground: 'transparent',
702
+ tabBackgroundHover: 'rgba(0, 0, 0, 0.05)',
703
+ tabBackgroundSelected: '#ffffff',
704
+ tabLabelMaxWidth: '10rem',
705
+ tabShadowSelected: '0 1px 3px rgba(0, 0, 0, 0.1)',
706
+ tabTextColor: '#666666',
707
+ tabTextColorSelected: '#000000',
708
+ },
709
+ textInput: {
710
+ background: '#ffffff',
711
+ borderColor: '#dbdbdb',
712
+ borderRadius: '6px',
713
+ focusBorderColor: '#333333',
714
+ hoverBorderColor: '#cccccc',
715
+ labelColor: '#333333',
716
+ placeholderColor: '#999999',
717
+ selectionBackground: '#007AFF',
718
+ selectionColor: '#ffffff',
719
+ suffixBackground: '#f5f5f5',
720
+ suffixColor: '#666666',
721
+ textColor: '#333333',
722
+ },
723
+ selection: {
724
+ borderColor: '#007AFF',
725
+ boxBackgroundColor: 'rgba(0, 122, 255, 0.2)',
726
+ boxBorderColor: 'rgba(0, 122, 255, 0.5)',
727
+ handleColor: '#ffffff',
728
+ handleStrokeColor: '#007AFF'
729
+ },
730
+ checkerboard: {
731
+ colorDark: '#cccccc',
732
+ colorLight: '#ffffff',
733
+ },
734
+ backToContent: {
735
+ activeBackgroundColor: 'hsl(0, 0%, 0%, 8.6%)',
736
+ backgroundColor: '#ffffff',
737
+ border: '1px solid #ebebeb',
738
+ boxShadow: '0 0 3px rgba(0, 0, 0, 0.08)',
739
+ color: '#000000',
740
+ hoverBackgroundColor: 'hsl(0, 0%, 0%, 4.3%)',
741
+ },
742
+ colorPalette: {
743
+ circleBorderColor: '#dddcdc',
744
+ hoverBackgroundColor: '#ebebeb',
745
+ selectedBackgroundColor: '#ebebeb',
746
+ },
747
+ contextMenu: {
748
+ backgroundColor: '#ffffff',
749
+ border: '1px solid #ebebeb',
750
+ boxShadow: '0 1px 6px rgba(0, 0, 0, 0.12)',
751
+ dividerColor: 'rgba(0, 0, 0, 0.1)',
752
+ itemActiveBackgroundColor: 'hsl(0, 0%, 0%, 8.6%)',
753
+ itemColor: '#333333',
754
+ itemDisabledColor: '#aaaaaa',
755
+ itemHoverBackgroundColor: 'hsl(0, 0%, 0%, 4.3%)',
756
+ },
757
+ toolbar: {
758
+ backgroundColor: '#ffffff',
759
+ border: '1px solid #ebebeb',
760
+ boxShadow: '0 0 3px rgba(0, 0, 0, 0.08)',
761
+ controlActiveBackgroundColor: 'hsl(0, 0%, 0%, 8.6%)',
762
+ controlColor: '#000000',
763
+ controlHoverBackgroundColor: 'hsl(0, 0%, 0%, 4.3%)',
764
+ controlSelectedBackgroundColor: '#007AFF',
765
+ controlSelectedColor: '#ffffff',
766
+ separatorColor: '#ebebeb',
767
+ },
768
+ currentUserDialog: {
769
+ emailColor: '#666666',
770
+ nameColor: '#333333',
771
+ },
772
+ dialog: {
773
+ backdropColor: 'rgba(0, 0, 0, 0.4)',
774
+ backgroundColor: '#ffffff',
775
+ border: '1px solid #ebebeb',
776
+ boxShadow: '0 4px 24px rgba(0, 0, 0, 0.15)',
777
+ closeButtonActiveBackground: 'hsl(0, 0%, 0%, 8.6%)',
778
+ closeButtonBackground: 'transparent',
779
+ closeButtonColor: '#333333',
780
+ closeButtonHoverBackground: 'hsl(0, 0%, 0%, 4.3%)',
781
+ closeButtonHoverColor: '#000000',
782
+ fontFamily: 'inherit',
783
+ footerBorder: '1px solid #ebebeb',
784
+ headerBorder: '1px solid #ebebeb',
785
+ titleColor: '#000000',
786
+ },
787
+ loginDialog: {
788
+ buttonActiveBackground: '#ebebeb',
789
+ buttonBackground: '#ffffff',
790
+ buttonBorderColor: '#e0e0e0',
791
+ buttonHoverBackground: '#f5f5f5',
792
+ buttonHoverBorderColor: '#cccccc',
793
+ buttonTextColor: '#333333',
794
+ spinnerActiveColor: '#333333',
795
+ spinnerColor: '#cccccc',
796
+ subtitleColor: '#666666',
797
+ },
798
+ dropdown: {
799
+ accentColor: '#007bff',
800
+ background: '#ffffff',
801
+ borderColor: '#dbdbdb',
802
+ borderRadius: '6px',
803
+ hoverBorderColor: '#cccccc',
804
+ hoverBackgroundColor: '#f0f0f0',
805
+ menuBorderRadius: '6px',
806
+ selectedBackgroundColor: '#007bff1a',
807
+ textColor: '#333333',
808
+ },
809
+ engine: {
810
+ backgroundColor: '#ffffff',
811
+ },
812
+ editor: {
813
+ loadingOverlayBackground: 'rgba(255, 255, 255, 0.85)',
814
+ loadingOverlayColor: '#333333',
815
+ loadingOverlaySpinnerActiveColor: '#333333',
816
+ loadingOverlaySpinnerColor: '#cccccc',
817
+ syncIndicatorBackground: 'rgba(255, 255, 255, 0.92)',
818
+ syncIndicatorColor: '#334155',
819
+ syncIndicatorSpinnerColor: '#007AFF',
820
+ syncIndicatorSpinnerTrack: 'rgba(148, 163, 184, 0.35)',
821
+ syncIndicatorZIndex: '10001',
822
+ },
823
+ snap: {
824
+ indicatorFill: 'rgba(59, 130, 246, 0.3)',
825
+ indicatorFillInactive: 'rgba(59, 130, 246, 0.18)',
826
+ indicatorStroke: '#007bff',
827
+ indicatorStrokeInactive: 'rgba(59, 130, 246, 0.5)',
828
+ lineStroke: 'rgba(0, 0, 0, 0.2)',
829
+ },
830
+ fontSize: {
831
+ hoverBackgroundColor: '#ebebeb',
832
+ selectedBackgroundColor: '#ebebeb',
833
+ textColor: '#333333',
834
+ },
835
+ lineEndings: {
836
+ hoverBackgroundColor: '#ebebeb',
837
+ labelColor: '#666666',
838
+ optionBackground: '#ffffff',
839
+ selectedBackgroundColor: '#ebebeb',
840
+ },
841
+ masterDetail: {
842
+ backButtonColor: '#333333',
843
+ backgroundColor: '#ffffff',
844
+ detailBackgroundColor: '#ffffff',
845
+ detailFocusOutline: '2px solid #333333',
846
+ menuBackgroundColor: '#ffffff',
847
+ menuBorderRight: '1px solid #ebebeb',
848
+ menuItemActiveBackgroundColor: 'hsl(0, 0%, 0%, 8.6%)',
849
+ menuItemBackgroundColor: 'transparent',
850
+ menuItemChevronColor: '#aaaaaa',
851
+ menuItemColor: '#333333',
852
+ menuItemDisabledColor: '#aaaaaa',
853
+ menuItemFocusOutline: '2px solid #333333',
854
+ menuItemHoverBackgroundColor: 'hsl(0, 0%, 0%, 4.3%)',
855
+ menuItemSelectedBackgroundColor: '#007AFF',
856
+ menuItemSelectedColor: '#ffffff',
857
+ menuItemSelectedHoverBackgroundColor: '#007AFF',
858
+ },
859
+ menu: {
860
+ backgroundColor: '#ffffff',
861
+ border: '1px solid #ebebeb',
862
+ boxShadow: '0 0 3px rgba(0, 0, 0, 0.08)',
863
+ itemButtonHoverBackgroundColor: 'hsl(0, 0%, 0%, 4.3%)',
864
+ itemChildOpenBackgroundColor: 'hsl(0, 0%, 0%, 3%)',
865
+ itemColor: '#333333',
866
+ itemEditingBackgroundColor: '#f0f0f0',
867
+ itemInputBorder: '1px solid #333333',
868
+ itemInputBorderColorOnSelected: '#ffffff',
869
+ itemInputCaretColor: '#333333',
870
+ itemInputCaretColorOnSelected: '#ffffff',
871
+ itemInputSelectionColor: '#007aff',
872
+ itemInputSelectionColorOnSelected: 'rgba(255, 255, 255, 0.55)',
873
+ itemInputSelectionTextColor: '#ffffff',
874
+ itemInputSelectionTextColorOnSelected: '#ffffff',
875
+ itemOverlayBackgroundColor: 'hsl(0, 0%, 0%, 4.3%)',
876
+ itemSelectedBackgroundColor: '#007aff',
877
+ itemSelectedColor: '#ffffff',
878
+ },
879
+ moreMenu: {
880
+ backgroundColor: '#ffffff',
881
+ border: '1px solid #ebebeb',
882
+ borderRadius: '12px',
883
+ boxShadow: '0 0 3px rgba(0, 0, 0, 0.08)',
884
+ buttonActiveBackgroundColor: 'hsl(0, 0%, 0%, 8.6%)',
885
+ buttonColor: '#000000',
886
+ buttonHoverBackgroundColor: 'hsl(0, 0%, 0%, 4.3%)',
887
+ innerBorderRadius: '12px',
888
+ },
889
+ notificationCard: {
890
+ backgroundColor: '#ffffff',
891
+ border: '1px solid #e5e7eb',
892
+ borderRadius: '12px',
893
+ boxShadow: '0 8px 20px rgba(15, 23, 42, 0.08)',
894
+ color: '#111827',
895
+ dismissButtonActiveBackgroundColor: '#e5e7eb',
896
+ dismissButtonBackgroundColor: 'transparent',
897
+ dismissButtonBorderRadius: '6px',
898
+ dismissButtonColor: '#4b5563',
899
+ dismissButtonHoverBackgroundColor: '#f3f4f6',
900
+ dismissButtonSize: '24px',
901
+ errorPillBackgroundColor: '#fef2f2',
902
+ errorPillColor: '#b91c1c',
903
+ headerGap: '8px',
904
+ headerMarginBottom: '8px',
905
+ headerRightGap: '6px',
906
+ infoPillBackgroundColor: '#eff6ff',
907
+ infoPillColor: '#1d4ed8',
908
+ maxWidth: '360px',
909
+ messageColor: 'inherit',
910
+ messageFontSize: '14px',
911
+ messageLineHeight: '1.45',
912
+ padding: '12px 14px',
913
+ pillBackgroundColor: '#eff6ff',
914
+ pillBorderRadius: '999px',
915
+ pillColor: '#1d4ed8',
916
+ pillFontSize: '12px',
917
+ pillFontWeight: '600',
918
+ pillHeight: '22px',
919
+ pillPadding: '0 8px',
920
+ timestampColor: '#6b7280',
921
+ timestampFontSize: '12px',
922
+ warningPillBackgroundColor: '#fff7ed',
923
+ warningPillColor: '#b45309',
924
+ },
925
+ numericInput: {
926
+ focusBorderColor: '#333333',
927
+ borderColor: '#dbdbdb',
928
+ borderRadius: '6px',
929
+ hoverBorderColor: '#cccccc',
930
+ inputBackground: '#ffffff',
931
+ labelColor: '#666666',
932
+ selectionBackground: '#007AFF',
933
+ selectionColor: '#ffffff',
934
+ spinnerActiveBackground: 'hsl(0, 0%, 0%, 8.6%)',
935
+ spinnerBackground: 'transparent',
936
+ spinnerBorderRadius: '5px',
937
+ spinnerColor: '#333333',
938
+ spinnerHoverBackground: 'hsl(0, 0%, 0%, 4.3%)',
939
+ textColor: '#333333',
940
+ },
941
+ opacitySlider: {
942
+ activeColor: '#007AFF',
943
+ thumbBorderColor: '#007AFF',
944
+ thumbColor: '#ffffff',
945
+ trackColor: '#e0e0e0',
946
+ },
947
+ settings: {
948
+ contentHeadingColor: '#000000',
949
+ contentTextColor: '#333333',
950
+ descriptionColor: '#666666',
951
+ labelColor: '#333333',
952
+ shortcutItemBg: '#f8f8f8',
953
+ shortcutKeyBg: '#ffffff',
954
+ shortcutKeyBorder: '#e0e0e0',
955
+ shortcutKeyColor: '#555555',
956
+ },
957
+ shapeFill: {
958
+ hoverBackgroundColor: '#ebebeb',
959
+ optionBackground: '#ffffff',
960
+ selectedBackgroundColor: '#ebebeb',
961
+ },
962
+ shareDialog: {
963
+ borderColor: '#e5e5e5',
964
+ copyButtonBackground: '#ffffff',
965
+ copyButtonColor: '#666666',
966
+ copyButtonHoverBackground: '#e8e8e8',
967
+ copyButtonHoverColor: '#333333',
968
+ copySuccessBackground: '#d4edda',
969
+ copySuccessColor: '#28a745',
970
+ descriptionColor: '#666666',
971
+ inputBackground: '#f5f5f5',
972
+ inputBorderColor: '#e0e0e0',
973
+ inputTextColor: '#333333',
974
+ labelColor: '#333333',
975
+ revokeButtonBorderColor: '#dc3545',
976
+ revokeButtonColor: '#dc3545',
977
+ revokeButtonHoverBackground: '#dc3545',
978
+ revokeButtonHoverColor: '#ffffff',
979
+ selectionColor: '#cce5ff',
980
+ },
981
+ slideToggle: {
982
+ thumbColor: '#fff',
983
+ thumbSize: '18px',
984
+ trackCheckedColor: '#007AFF',
985
+ trackColor: '#ccc',
986
+ },
987
+ splitButton: {
988
+ backgroundColor: '#ffffff',
989
+ border: '1px solid #ebebeb',
990
+ boxShadow: '0 0 3px rgba(0, 0, 0, 0.08)',
991
+ color: '#000000',
992
+ dividerBackgroundColor: '#ebebeb',
993
+ hoverBackgroundColor: 'hsl(0, 0%, 0%, 4.3%)',
994
+ },
995
+ strokeSize: {
996
+ hoverBackgroundColor: '#ebebeb',
997
+ selectedBackgroundColor: '#ebebeb',
998
+ },
999
+ tooltip: {
1000
+ backgroundColor: '#ffffff',
1001
+ border: '1px solid #ebebeb',
1002
+ boxShadow: '0 1px 6px rgba(0, 0, 0, 0.12)',
1003
+ color: '#000000',
1004
+ },
1005
+ utilityPanel: {
1006
+ backgroundColor: '#e2e2e2',
1007
+ buttonColor: '#333333',
1008
+ buttonHoverBackgroundColor: 'hsl(0, 0%, 0%, 4.3%)',
1009
+ separatorColor: 'hsl(0, 0%, 0%, 8%)',
1010
+ },
1011
+ watermark: {
1012
+ background: 'rgba(255, 255, 255, 0.6)',
1013
+ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.12)',
1014
+ color: 'rgba(60, 60, 60, 0.75)',
1015
+ },
1016
+ zoomPanel: {
1017
+ backgroundColor: '#ffffff',
1018
+ border: '1px solid #ebebeb',
1019
+ borderRadius: '12px',
1020
+ boxShadow: '0 0 3px rgba(0, 0, 0, 0.08)',
1021
+ buttonActiveBackgroundColor: 'hsl(0, 0%, 0%, 8.6%)',
1022
+ buttonBorderRadius: '8px',
1023
+ buttonHoverBackgroundColor: 'hsl(0, 0%, 0%, 4.3%)',
1024
+ buttonSize: '32px',
1025
+ gap: '4px',
1026
+ iconColor: '#000000',
1027
+ padding: '4px',
1028
+ },
1029
+ };
1030
+
1031
+ /**
1032
+ * Dark theme preset
1033
+ */
1034
+ const darkTheme = {
1035
+ name: 'dark',
1036
+ global: {
1037
+ borderColor: '#3a3a3a',
1038
+ dividerColor: '#3a3a3a',
1039
+ focusColor: '#ffffff',
1040
+ focusRingColor: '#ffffff',
1041
+ iconColor: 'currentColor',
1042
+ scrollbarThumbColor: '#555555',
1043
+ textPrimary: '#ffffff',
1044
+ textSecondary: '#e0e0e0',
1045
+ },
1046
+ pillTabs: {
1047
+ background: '#3a3a3a',
1048
+ tabBackground: 'transparent',
1049
+ tabBackgroundHover: 'rgba(255, 255, 255, 0.08)',
1050
+ tabBackgroundSelected: '#2a2a2a',
1051
+ tabLabelMaxWidth: '10rem',
1052
+ tabShadowSelected: '0 1px 3px rgba(0, 0, 0, 0.3)',
1053
+ tabTextColor: '#999999',
1054
+ tabTextColorSelected: '#ffffff',
1055
+ },
1056
+ textInput: {
1057
+ background: '#1a1a1a',
1058
+ borderColor: '#4a4a4a',
1059
+ borderRadius: '6px',
1060
+ focusBorderColor: '#ffffff',
1061
+ hoverBorderColor: '#5a5a5a',
1062
+ labelColor: '#e0e0e0',
1063
+ placeholderColor: '#777777',
1064
+ selectionBackground: '#0A84FF',
1065
+ selectionColor: '#ffffff',
1066
+ suffixBackground: '#3a3a3a',
1067
+ suffixColor: '#aaaaaa',
1068
+ textColor: '#e0e0e0',
1069
+ },
1070
+ selection: {
1071
+ borderColor: '#0A84FF',
1072
+ boxBackgroundColor: 'rgba(10, 132, 255, 0.2)',
1073
+ boxBorderColor: 'rgba(10, 132, 255, 0.5)',
1074
+ handleColor: '#1a1a1a',
1075
+ handleStrokeColor: '#0A84FF'
1076
+ },
1077
+ checkerboard: {
1078
+ colorDark: '#4a4a4a',
1079
+ colorLight: '#3a3a3a',
1080
+ },
1081
+ backToContent: {
1082
+ activeBackgroundColor: 'hsl(0, 0%, 100%, 12%)',
1083
+ backgroundColor: '#2a2a2a',
1084
+ border: '1px solid #3a3a3a',
1085
+ boxShadow: '0 0 6px rgba(0, 0, 0, 0.3)',
1086
+ color: '#ffffff',
1087
+ hoverBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
1088
+ },
1089
+ colorPalette: {
1090
+ circleBorderColor: '#4a4a4a',
1091
+ hoverBackgroundColor: '#3a3a3a',
1092
+ selectedBackgroundColor: '#3a3a3a',
1093
+ },
1094
+ contextMenu: {
1095
+ backgroundColor: '#2a2a2a',
1096
+ border: '1px solid #3a3a3a',
1097
+ boxShadow: '0 1px 8px rgba(0, 0, 0, 0.4)',
1098
+ dividerColor: 'rgba(255, 255, 255, 0.1)',
1099
+ itemActiveBackgroundColor: 'hsl(0, 0%, 100%, 12%)',
1100
+ itemColor: '#e0e0e0',
1101
+ itemDisabledColor: '#666666',
1102
+ itemHoverBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
1103
+ },
1104
+ toolbar: {
1105
+ backgroundColor: '#2a2a2a',
1106
+ border: '1px solid #3a3a3a',
1107
+ boxShadow: '0 0 6px rgba(0, 0, 0, 0.3)',
1108
+ controlActiveBackgroundColor: 'hsl(0, 0%, 100%, 12%)',
1109
+ controlColor: '#ffffff',
1110
+ controlHoverBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
1111
+ controlSelectedBackgroundColor: '#0A84FF',
1112
+ controlSelectedColor: '#ffffff',
1113
+ separatorColor: '#3a3a3a',
1114
+ },
1115
+ currentUserDialog: {
1116
+ emailColor: '#999999',
1117
+ nameColor: '#ffffff',
1118
+ },
1119
+ dialog: {
1120
+ backdropColor: 'rgba(0, 0, 0, 0.6)',
1121
+ backgroundColor: '#2a2a2a',
1122
+ border: '1px solid #3a3a3a',
1123
+ boxShadow: '0 4px 24px rgba(0, 0, 0, 0.5)',
1124
+ closeButtonActiveBackground: 'hsl(0, 0%, 100%, 12%)',
1125
+ closeButtonBackground: 'transparent',
1126
+ closeButtonColor: '#e0e0e0',
1127
+ closeButtonHoverBackground: 'hsl(0, 0%, 100%, 8%)',
1128
+ closeButtonHoverColor: '#ffffff',
1129
+ footerBorder: '1px solid #3a3a3a',
1130
+ headerBorder: '1px solid #3a3a3a',
1131
+ titleColor: '#ffffff',
1132
+ },
1133
+ loginDialog: {
1134
+ buttonActiveBackground: '#444444',
1135
+ buttonBackground: '#2a2a2a',
1136
+ buttonBorderColor: '#4a4a4a',
1137
+ buttonHoverBackground: '#3a3a3a',
1138
+ buttonHoverBorderColor: '#5a5a5a',
1139
+ buttonTextColor: '#e0e0e0',
1140
+ spinnerActiveColor: '#ffffff',
1141
+ spinnerColor: '#555555',
1142
+ subtitleColor: '#999999',
1143
+ },
1144
+ dropdown: {
1145
+ accentColor: '#0A84FF',
1146
+ borderColor: '#4a4a4a',
1147
+ borderRadius: '6px',
1148
+ hoverBorderColor: '#5a5a5a',
1149
+ background: '#1a1a1a',
1150
+ hoverBackgroundColor: '#3a3a3a',
1151
+ menuBorderRadius: '6px',
1152
+ selectedBackgroundColor: 'rgba(10, 132, 255, 0.2)',
1153
+ textColor: '#e0e0e0',
1154
+ },
1155
+ engine: {
1156
+ backgroundColor: '#1a1a1a',
1157
+ },
1158
+ editor: {
1159
+ loadingOverlayBackground: 'rgba(26, 26, 26, 0.85)',
1160
+ loadingOverlayColor: '#e0e0e0',
1161
+ loadingOverlaySpinnerActiveColor: '#e0e0e0',
1162
+ loadingOverlaySpinnerColor: '#555555',
1163
+ syncIndicatorBackground: 'rgba(30, 30, 30, 0.95)',
1164
+ syncIndicatorColor: '#e0e0e0',
1165
+ syncIndicatorSpinnerColor: '#0A84FF',
1166
+ syncIndicatorSpinnerTrack: 'rgba(160, 160, 160, 0.3)',
1167
+ syncIndicatorZIndex: '10001',
1168
+ },
1169
+ snap: {
1170
+ indicatorFill: 'rgba(10, 132, 255, 0.35)',
1171
+ indicatorFillInactive: 'rgba(10, 132, 255, 0.2)',
1172
+ indicatorStroke: '#0A84FF',
1173
+ indicatorStrokeInactive: 'rgba(255, 255, 255, 0.45)',
1174
+ lineStroke: 'rgba(255, 255, 255, 0.35)',
1175
+ },
1176
+ fontSize: {
1177
+ hoverBackgroundColor: '#3a3a3a',
1178
+ selectedBackgroundColor: '#3a3a3a',
1179
+ textColor: '#e0e0e0',
1180
+ },
1181
+ lineEndings: {
1182
+ hoverBackgroundColor: '#3a3a3a',
1183
+ labelColor: '#999999',
1184
+ optionBackground: '#2a2a2a',
1185
+ selectedBackgroundColor: '#3a3a3a',
1186
+ },
1187
+ masterDetail: {
1188
+ backButtonColor: '#e0e0e0',
1189
+ backgroundColor: '#2a2a2a',
1190
+ detailBackgroundColor: '#2a2a2a',
1191
+ detailFocusOutline: '2px solid #ffffff',
1192
+ menuBackgroundColor: '#2a2a2a',
1193
+ menuBorderRight: '1px solid #3a3a3a',
1194
+ menuItemActiveBackgroundColor: 'hsl(0, 0%, 100%, 12%)',
1195
+ menuItemBackgroundColor: 'transparent',
1196
+ menuItemChevronColor: '#666666',
1197
+ menuItemColor: '#e0e0e0',
1198
+ menuItemDisabledColor: '#666666',
1199
+ menuItemFocusOutline: '2px solid #ffffff',
1200
+ menuItemHoverBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
1201
+ menuItemSelectedBackgroundColor: '#0A84FF',
1202
+ menuItemSelectedColor: '#ffffff',
1203
+ menuItemSelectedHoverBackgroundColor: '#0A84FF',
1204
+ },
1205
+ menu: {
1206
+ backgroundColor: '#2a2a2a',
1207
+ border: '1px solid #3a3a3a',
1208
+ boxShadow: '0 0 6px rgba(0, 0, 0, 0.3)',
1209
+ itemButtonHoverBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
1210
+ itemChildOpenBackgroundColor: 'hsl(0, 0%, 100%, 6%)',
1211
+ itemColor: '#e0e0e0',
1212
+ itemEditingBackgroundColor: '#3a3a3a',
1213
+ itemInputBorder: '1px solid #ffffff',
1214
+ itemInputBorderColorOnSelected: '#ffffff',
1215
+ itemInputCaretColor: '#e0e0e0',
1216
+ itemInputCaretColorOnSelected: '#ffffff',
1217
+ itemInputSelectionColor: '#b0b0b0',
1218
+ itemInputSelectionColorOnSelected: 'rgba(255, 255, 255, 0.35)',
1219
+ itemInputSelectionTextColor: '#ffffff',
1220
+ itemInputSelectionTextColorOnSelected: '#ffffff',
1221
+ itemOverlayBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
1222
+ itemSelectedBackgroundColor: '#0A84FF',
1223
+ itemSelectedColor: '#ffffff',
1224
+ },
1225
+ moreMenu: {
1226
+ backgroundColor: '#2a2a2a',
1227
+ border: '1px solid #3a3a3a',
1228
+ borderRadius: '12px',
1229
+ boxShadow: '0 0 6px rgba(0, 0, 0, 0.3)',
1230
+ buttonActiveBackgroundColor: 'hsl(0, 0%, 100%, 12%)',
1231
+ buttonColor: '#ffffff',
1232
+ buttonHoverBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
1233
+ innerBorderRadius: '12px',
1234
+ },
1235
+ notificationCard: {
1236
+ backgroundColor: '#2a2a2a',
1237
+ border: '1px solid #3a3a3a',
1238
+ borderRadius: '12px',
1239
+ boxShadow: '0 8px 20px rgba(0, 0, 0, 0.35)',
1240
+ color: '#e0e0e0',
1241
+ dismissButtonActiveBackgroundColor: 'hsl(0, 0%, 100%, 12%)',
1242
+ dismissButtonBackgroundColor: 'transparent',
1243
+ dismissButtonBorderRadius: '6px',
1244
+ dismissButtonColor: '#c7c7c7',
1245
+ dismissButtonHoverBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
1246
+ dismissButtonSize: '24px',
1247
+ errorPillBackgroundColor: '#4d1f24',
1248
+ errorPillColor: '#ff9aa2',
1249
+ headerGap: '8px',
1250
+ headerMarginBottom: '8px',
1251
+ headerRightGap: '6px',
1252
+ infoPillBackgroundColor: '#1f2f4a',
1253
+ infoPillColor: '#8bb7ff',
1254
+ maxWidth: '360px',
1255
+ messageColor: 'inherit',
1256
+ messageFontSize: '14px',
1257
+ messageLineHeight: '1.45',
1258
+ padding: '12px 14px',
1259
+ pillBackgroundColor: '#1f2f4a',
1260
+ pillBorderRadius: '999px',
1261
+ pillColor: '#8bb7ff',
1262
+ pillFontSize: '12px',
1263
+ pillFontWeight: '600',
1264
+ pillHeight: '22px',
1265
+ pillPadding: '0 8px',
1266
+ timestampColor: '#a0a0a0',
1267
+ timestampFontSize: '12px',
1268
+ warningPillBackgroundColor: '#4a341f',
1269
+ warningPillColor: '#ffd29a',
1270
+ },
1271
+ numericInput: {
1272
+ borderColor: '#4a4a4a',
1273
+ borderRadius: '6px',
1274
+ focusBorderColor: '#ffffff',
1275
+ hoverBorderColor: '#5a5a5a',
1276
+ inputBackground: '#1a1a1a',
1277
+ labelColor: '#999999',
1278
+ selectionBackground: '#0A84FF',
1279
+ selectionColor: '#ffffff',
1280
+ spinnerActiveBackground: 'hsl(0, 0%, 100%, 12%)',
1281
+ spinnerBackground: 'transparent',
1282
+ spinnerBorderRadius: '5px',
1283
+ spinnerColor: '#e0e0e0',
1284
+ spinnerHoverBackground: 'hsl(0, 0%, 100%, 8%)',
1285
+ textColor: '#e0e0e0',
1286
+ },
1287
+ opacitySlider: {
1288
+ activeColor: '#0A84FF',
1289
+ thumbBorderColor: '#0A84FF',
1290
+ thumbColor: '#ffffff',
1291
+ trackColor: '#4a4a4a',
1292
+ },
1293
+ settings: {
1294
+ contentHeadingColor: '#ffffff',
1295
+ contentTextColor: '#e0e0e0',
1296
+ descriptionColor: '#999999',
1297
+ labelColor: '#e0e0e0',
1298
+ shortcutItemBg: '#3a3a3a',
1299
+ shortcutKeyBg: '#2a2a2a',
1300
+ shortcutKeyBorder: '#4a4a4a',
1301
+ shortcutKeyColor: '#e0e0e0',
1302
+ },
1303
+ shapeFill: {
1304
+ hoverBackgroundColor: '#3a3a3a',
1305
+ optionBackground: '#2a2a2a',
1306
+ selectedBackgroundColor: '#3a3a3a',
1307
+ },
1308
+ shareDialog: {
1309
+ labelColor: '#e0e0e0',
1310
+ descriptionColor: '#e0e0e0',
1311
+ inputBackground: '#1a1a1a',
1312
+ inputBorderColor: '#4a4a4a',
1313
+ inputTextColor: '#e0e0e0',
1314
+ selectionColor: '#0A84FF',
1315
+ copyButtonBackground: '#2a2a2a',
1316
+ copyButtonColor: '#e0e0e0',
1317
+ copyButtonHoverBackground: '#3a3a3a',
1318
+ copyButtonHoverColor: '#ffffff',
1319
+ copySuccessBackground: '#28a745',
1320
+ copySuccessColor: '#ffffff',
1321
+ },
1322
+ slideToggle: {
1323
+ thumbColor: '#ffffff',
1324
+ trackCheckedColor: '#0A84FF',
1325
+ trackColor: '#4a4a4a',
1326
+ transitionDuration: '0.2s',
1327
+ },
1328
+ splitButton: {
1329
+ backgroundColor: '#2a2a2a',
1330
+ border: '1px solid #3a3a3a',
1331
+ boxShadow: '0 0 6px rgba(0, 0, 0, 0.3)',
1332
+ color: '#ffffff',
1333
+ dividerBackgroundColor: '#3a3a3a',
1334
+ hoverBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
1335
+ },
1336
+ strokeSize: {
1337
+ hoverBackgroundColor: '#3a3a3a',
1338
+ selectedBackgroundColor: '#3a3a3a',
1339
+ },
1340
+ tooltip: {
1341
+ backgroundColor: '#2a2a2a',
1342
+ border: '1px solid #3a3a3a',
1343
+ boxShadow: '0 1px 8px rgba(0, 0, 0, 0.4)',
1344
+ color: '#ffffff',
1345
+ },
1346
+ utilityPanel: {
1347
+ backgroundColor: '#3a3a3a',
1348
+ buttonColor: '#e0e0e0',
1349
+ buttonHoverBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
1350
+ separatorColor: 'hsl(0, 0%, 100%, 12%)',
1351
+ },
1352
+ watermark: {
1353
+ background: 'rgba(40, 40, 40, 0.6)',
1354
+ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.35)',
1355
+ color: 'rgba(220, 220, 220, 0.8)',
1356
+ },
1357
+ zoomPanel: {
1358
+ backgroundColor: '#2a2a2a',
1359
+ border: '1px solid #3a3a3a',
1360
+ borderRadius: '12px',
1361
+ boxShadow: '0 0 3px rgba(0, 0, 0, 0.4)',
1362
+ buttonActiveBackgroundColor: 'hsl(0, 0%, 100%, 12%)',
1363
+ buttonBorderRadius: '8px',
1364
+ buttonHoverBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
1365
+ buttonSize: '32px',
1366
+ gap: '4px',
1367
+ iconColor: '#e0e0e0',
1368
+ padding: '4px',
1369
+ },
1370
+ };
1371
+
1372
+ class ThemeHelper {
1373
+ static appliedVariableNames = new WeakMap();
1374
+ /**
1375
+ * Converts a camelCase string to kebab-case.
1376
+ * @example ThemeHelper.camelToKebab('controlHoverBackgroundColor') → 'control-hover-background-color'
1377
+ * @example ThemeHelper.camelToKebab('backgroundColor') → 'background-color'
1378
+ * @example ThemeHelper.camelToKebab('gap') → 'gap'
1379
+ */
1380
+ static camelToKebab(str) {
1381
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
1382
+ }
1383
+ /**
1384
+ * Recursively flattens a theme object into a map of CSS variable names to values.
1385
+ * Skips the 'name' property at the root level since it's the theme identifier, not a CSS value.
1386
+ *
1387
+ * @param theme - The theme object to flatten
1388
+ * @param prefix - The CSS variable prefix (default: '--kritzel')
1389
+ * @returns A Map of CSS variable names to their values
1390
+ *
1391
+ * @example
1392
+ * ThemeHelper.flattenThemeToVariables(lightTheme)
1393
+ * // Returns Map {
1394
+ * // '--kritzel-global-pointer-cursor' => 'pointer',
1395
+ * // '--kritzel-toolbar-gap' => '8px',
1396
+ * // ...
1397
+ * // }
1398
+ */
1399
+ static flattenThemeToVariables(theme, prefix = '--kritzel') {
1400
+ const variables = new Map();
1401
+ const recurse = (obj, currentPrefix, isRoot = false) => {
1402
+ for (const [key, value] of Object.entries(obj)) {
1403
+ // Skip the 'name' property at the root level
1404
+ if (isRoot && key === 'name') {
1405
+ continue;
1406
+ }
1407
+ const kebabKey = ThemeHelper.camelToKebab(key);
1408
+ const variableName = `${currentPrefix}-${kebabKey}`;
1409
+ if (typeof value === 'object' && value !== null) {
1410
+ recurse(value, variableName);
1411
+ }
1412
+ else if (typeof value === 'string') {
1413
+ variables.set(variableName, value);
1414
+ }
1415
+ }
1416
+ };
1417
+ recurse(theme, prefix, true);
1418
+ return variables;
1419
+ }
1420
+ /**
1421
+ * Applies a map of CSS variables to an HTML element.
1422
+ *
1423
+ * @param element - The target HTML element
1424
+ * @param variables - A Map of CSS variable names to values
1425
+ */
1426
+ static applyVariablesToElement(element, variables) {
1427
+ for (const [name, value] of variables) {
1428
+ element.style.setProperty(name, value);
1429
+ }
1430
+ }
1431
+ /**
1432
+ * Convenience method that flattens a theme and applies it to an element in one call.
1433
+ *
1434
+ * @param element - The target HTML element
1435
+ * @param theme - The theme object to apply
1436
+ */
1437
+ static applyThemeToElement(element, theme) {
1438
+ const variables = ThemeHelper.flattenThemeToVariables(theme);
1439
+ const previousVariableNames = ThemeHelper.appliedVariableNames.get(element);
1440
+ for (const name of previousVariableNames ?? []) {
1441
+ if (!variables.has(name)) {
1442
+ element.style.removeProperty(name);
1443
+ }
1444
+ }
1445
+ ThemeHelper.applyVariablesToElement(element, variables);
1446
+ ThemeHelper.appliedVariableNames.set(element, new Set(variables.keys()));
1447
+ }
1448
+ }
1449
+
1450
+ /** Key used to store the settings object in localStorage. */
1451
+ const SETTINGS_STORAGE_KEY = 'kritzel-settings';
1452
+ /** Default theme used when no stored preference exists. */
1453
+ const DEFAULT_THEME = 'light';
1454
+ /**
1455
+ * Manages theme state and application across the Kritzel editor.
1456
+ * Reads the persisted theme from the settings object in localStorage but does not write to it.
1457
+ * Persistence is handled exclusively by the KritzelSettings component.
1458
+ *
1459
+ * The theme manager is responsible for:
1460
+ * - Reading the stored theme preference from the settings object
1461
+ * - Applying CSS custom properties to target elements
1462
+ * - Switching between light and dark themes at runtime
1463
+ */
1464
+ class KritzelThemeManager {
1465
+ _core;
1466
+ _currentTheme = DEFAULT_THEME;
1467
+ _targetElement = null;
1468
+ _storageKey;
1469
+ _themeRegistry = new Map();
1470
+ /**
1471
+ * Creates a new KritzelThemeManager instance.
1472
+ * Initializes the current theme from the settings object in localStorage or uses the default theme.
1473
+ *
1474
+ * @param core - The KritzelCore instance this manager belongs to
1475
+ */
1476
+ constructor(core) {
1477
+ this._core = core;
1478
+ this._storageKey = core.editorId ? `${SETTINGS_STORAGE_KEY}-${core.editorId}` : SETTINGS_STORAGE_KEY;
1479
+ this._themeRegistry.set('light', lightTheme);
1480
+ this._themeRegistry.set('dark', darkTheme);
1481
+ this._currentTheme = this.getStoredTheme();
1482
+ }
1483
+ /**
1484
+ * Gets the current active theme name.
1485
+ *
1486
+ * @returns The name of the currently active theme ('light' or 'dark')
1487
+ */
1488
+ get currentTheme() {
1489
+ return this._currentTheme;
1490
+ }
1491
+ /**
1492
+ * Sets the target element where theme CSS variables will be applied.
1493
+ * If an element is provided, the current theme is immediately applied to it.
1494
+ *
1495
+ * @param element - The HTML element to apply theme styles to, or null to clear
1496
+ */
1497
+ setTargetElement(element) {
1498
+ this._targetElement = element;
1499
+ if (this._targetElement) {
1500
+ this.applyTheme(this._currentTheme);
1501
+ }
1502
+ }
1503
+ /**
1504
+ * Gets the current target element where theme CSS variables are applied.
1505
+ *
1506
+ * @returns The current target HTML element, or null if none is set
1507
+ */
1508
+ getTargetElement() {
1509
+ return this._targetElement;
1510
+ }
1511
+ /**
1512
+ * Gets the theme configuration object by its name.
1513
+ * Falls back to the first registered theme if the requested name is not found.
1514
+ *
1515
+ * @param themeName - The name of the theme to retrieve
1516
+ * @returns The KritzelTheme object containing all theme CSS variable values
1517
+ */
1518
+ getThemeByName(themeName) {
1519
+ return this._themeRegistry.get(themeName) ?? this._themeRegistry.values().next().value;
1520
+ }
1521
+ /**
1522
+ * Registers the provided themes to this theme manager instance.
1523
+ *
1524
+ * @param themes - The complete list of themes to make available
1525
+ */
1526
+ registerThemes(themes) {
1527
+ if (themes.length === 0) {
1528
+ console.warn('[KritzelThemeManager] registerThemes called with an empty array. The theme registry will be empty.');
1529
+ }
1530
+ this._themeRegistry.clear();
1531
+ for (const theme of themes) {
1532
+ this._themeRegistry.set(theme.name, theme);
1533
+ }
1534
+ }
1535
+ /**
1536
+ * Pre-emptively applies the stored theme's CSS variables to the closest editor or document.
1537
+ * This prevents a "Flash of Unstyled Content" (FOUC) while Stencil web components are hydrating.
1538
+ *
1539
+ * @param element - The HTML element to apply the theme to
1540
+ */
1541
+ injectThemeEarly(element) {
1542
+ let targetElement = element;
1543
+ if (!targetElement && typeof document !== 'undefined') {
1544
+ targetElement = document.documentElement;
1545
+ }
1546
+ if (!targetElement)
1547
+ return;
1548
+ const themeName = this.getStoredTheme();
1549
+ const theme = this.getThemeByName(themeName);
1550
+ ThemeHelper.applyThemeToElement(targetElement, theme);
1551
+ }
1552
+ /**
1553
+ * Gets all registered themes.
1554
+ *
1555
+ * @returns An array of all registered theme objects
1556
+ */
1557
+ getAllThemes() {
1558
+ return Array.from(this._themeRegistry.values());
1559
+ }
1560
+ /**
1561
+ * Gets the stored theme name from the settings object in localStorage using this instance's namespaced key.
1562
+ *
1563
+ * @returns The stored theme name if valid, or 'light' as the default
1564
+ */
1565
+ getStoredTheme() {
1566
+ if (typeof localStorage === 'undefined') {
1567
+ return DEFAULT_THEME;
1568
+ }
1569
+ const stored = localStorage.getItem(this._storageKey);
1570
+ if (!stored) {
1571
+ return DEFAULT_THEME;
1572
+ }
1573
+ try {
1574
+ const parsed = JSON.parse(stored);
1575
+ if (typeof parsed?.theme === 'string') {
1576
+ return parsed.theme;
1577
+ }
1578
+ }
1579
+ catch {
1580
+ // Invalid JSON, use default
1581
+ }
1582
+ return DEFAULT_THEME;
1583
+ }
1584
+ /**
1585
+ * Gets the stored theme from the default (non-namespaced) settings object in localStorage.
1586
+ * Used by static utility helpers that don't have access to a core instance.
1587
+ *
1588
+ * @returns The stored theme name if valid, or 'light' as the default
1589
+ */
1590
+ static getStoredTheme() {
1591
+ if (typeof localStorage === 'undefined') {
1592
+ return DEFAULT_THEME;
1593
+ }
1594
+ const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
1595
+ if (!stored) {
1596
+ return DEFAULT_THEME;
1597
+ }
1598
+ try {
1599
+ const parsed = JSON.parse(stored);
1600
+ if (typeof parsed?.theme === 'string') {
1601
+ return parsed.theme;
1602
+ }
1603
+ }
1604
+ catch {
1605
+ // Invalid JSON, use default
1606
+ }
1607
+ return DEFAULT_THEME;
1608
+ }
1609
+ /**
1610
+ * Sets the current theme and applies it to the target element.
1611
+ * Does not persist the theme — persistence is handled by the KritzelSettings component.
1612
+ *
1613
+ * @param themeName - The theme name to set ('light' or 'dark')
1614
+ */
1615
+ setTheme(themeName) {
1616
+ this._currentTheme = themeName;
1617
+ if (this._targetElement) {
1618
+ this.applyTheme(themeName);
1619
+ }
1620
+ }
1621
+ /**
1622
+ * Checks if the current theme is the dark theme.
1623
+ *
1624
+ * @returns True if the current theme is 'dark', false otherwise
1625
+ */
1626
+ isDarkTheme() {
1627
+ return this._currentTheme === 'dark';
1628
+ }
1629
+ /**
1630
+ * Applies a theme to the target element by setting CSS custom properties.
1631
+ * Uses the ThemeHelper utility to automatically derive CSS variable names
1632
+ * from the theme interface structure and apply them to the element's style.
1633
+ * Does nothing if no target element is set.
1634
+ *
1635
+ * @param themeName - The name of the theme to apply ('light' or 'dark')
1636
+ */
1637
+ applyTheme(themeName) {
1638
+ if (!this._targetElement) {
1639
+ return;
1640
+ }
1641
+ const theme = this.getThemeByName(themeName);
1642
+ ThemeHelper.applyThemeToElement(this._targetElement, theme);
1643
+ }
1644
+ /**
1645
+ * Cleans up the theme manager state by clearing the target element reference.
1646
+ * Should be called when the manager is no longer needed to prevent memory leaks.
1647
+ */
1648
+ cleanup() {
1649
+ this._targetElement = null;
1650
+ }
1651
+ }
1652
+
1653
+ class KritzelColorHelper {
1654
+ /**
1655
+ * Resolves a color value based on the current theme.
1656
+ * Handles both simple string colors and theme-aware color objects.
1657
+ * @param color - The color to resolve (string or ThemeAwareColor)
1658
+ * @param theme - Optional theme to use. If not provided, uses current theme from ThemeManager
1659
+ * @returns The resolved hex color string, or empty string if color is undefined
1660
+ */
1661
+ static resolveThemeColor(color, theme) {
1662
+ if (!color) {
1663
+ return '';
1664
+ }
1665
+ const currentTheme = theme ?? KritzelThemeManager.getStoredTheme();
1666
+ return resolveColor(color, currentTheme);
1667
+ }
1668
+ /**
1669
+ * Applies opacity to a theme-aware color and returns an rgba string.
1670
+ * @param color - The theme-aware color object or a hex color string
1671
+ * @param opacity - The opacity value between 0 and 1
1672
+ * @param theme - Optional theme to use. If not provided, uses current theme from ThemeManager
1673
+ * @returns The color as an rgba string, or the resolved hex if opacity is 1
1674
+ */
1675
+ static applyOpacity(color, opacity, theme) {
1676
+ const hexColor = typeof color === 'string' ? color : this.resolveThemeColor(color, theme);
1677
+ if (!hexColor || opacity >= 1)
1678
+ return hexColor;
1679
+ const sanitizedHex = hexColor.startsWith('#') ? hexColor.slice(1) : hexColor;
1680
+ let r, g, b;
1681
+ if (sanitizedHex.length === 3) {
1682
+ r = parseInt(sanitizedHex[0] + sanitizedHex[0], 16);
1683
+ g = parseInt(sanitizedHex[1] + sanitizedHex[1], 16);
1684
+ b = parseInt(sanitizedHex[2] + sanitizedHex[2], 16);
1685
+ }
1686
+ else if (sanitizedHex.length === 6) {
1687
+ r = parseInt(sanitizedHex.substring(0, 2), 16);
1688
+ g = parseInt(sanitizedHex.substring(2, 4), 16);
1689
+ b = parseInt(sanitizedHex.substring(4, 6), 16);
1690
+ }
1691
+ else {
1692
+ return hexColor;
1693
+ }
1694
+ if (isNaN(r) || isNaN(g) || isNaN(b)) {
1695
+ return hexColor;
1696
+ }
1697
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
1698
+ }
1699
+ /**
1700
+ * Determines the appropriate contrast color (black or white) based on the luminance of the given hex color.
1701
+ * Uses the relative luminance formula: 0.299*R + 0.587*G + 0.114*B
1702
+ * @param hexColor - The hex color string (with or without #)
1703
+ * @returns '#000000' for light backgrounds, '#ffffff' for dark backgrounds
1704
+ */
1705
+ static getContrastColor(hexColor) {
1706
+ const sanitizedHex = hexColor.startsWith('#') ? hexColor.slice(1) : hexColor;
1707
+ let r, g, b;
1708
+ if (sanitizedHex.length === 3) {
1709
+ r = parseInt(sanitizedHex[0] + sanitizedHex[0], 16);
1710
+ g = parseInt(sanitizedHex[1] + sanitizedHex[1], 16);
1711
+ b = parseInt(sanitizedHex[2] + sanitizedHex[2], 16);
1712
+ }
1713
+ else if (sanitizedHex.length === 6) {
1714
+ r = parseInt(sanitizedHex.substring(0, 2), 16);
1715
+ g = parseInt(sanitizedHex.substring(2, 4), 16);
1716
+ b = parseInt(sanitizedHex.substring(4, 6), 16);
1717
+ }
1718
+ else {
1719
+ // Default to black for invalid colors
1720
+ return '#000000';
1721
+ }
1722
+ if (isNaN(r) || isNaN(g) || isNaN(b)) {
1723
+ return '#000000';
1724
+ }
1725
+ // Calculate relative luminance
1726
+ const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
1727
+ // Use threshold of 150 (midpoint) for balanced contrast
1728
+ return luminance > 150 ? '#000000' : '#ffffff';
1729
+ }
1730
+ /**
1731
+ * Determines the appropriate text color for both light and dark themes based on the fill color.
1732
+ * Returns default theme-appropriate colors if the fill is transparent.
1733
+ * @param fillColor - The theme-aware fill color
1734
+ * @returns A theme-aware color object with appropriate text colors for both themes
1735
+ */
1736
+ static determineTextColor(fillColor) {
1737
+ // Check if fillColor is transparent for both themes
1738
+ const isTransparentLight = fillColor.light === 'transparent';
1739
+ const isTransparentDark = fillColor.dark === 'transparent';
1740
+ if (isTransparentLight && isTransparentDark) {
1741
+ // Return default theme-appropriate colors for transparent fills
1742
+ return { light: '#000000', dark: '#ffffff' };
1743
+ }
1744
+ // Determine text color for each theme based on fill brightness
1745
+ const lightThemeTextColor = isTransparentLight
1746
+ ? '#000000'
1747
+ : this.getContrastColor(fillColor.light);
1748
+ const darkThemeTextColor = isTransparentDark
1749
+ ? '#ffffff'
1750
+ : this.getContrastColor(fillColor.dark);
1751
+ return {
1752
+ light: lightThemeTextColor,
1753
+ dark: darkThemeTextColor,
1754
+ };
1755
+ }
1756
+ }
1757
+
1758
+ class KritzelMathHelper {
1759
+ static average(a, b) {
1760
+ return (a + b) / 2;
1761
+ }
1762
+ static degreesToRadians(degrees) {
1763
+ return degrees * (Math.PI / 180);
1764
+ }
1765
+ }
1766
+
1767
+ var cjs = {};
1768
+
1769
+ var hasRequiredCjs;
1770
+
1771
+ function requireCjs () {
1772
+ if (hasRequiredCjs) return cjs;
1773
+ hasRequiredCjs = 1;
1774
+ (function (exports) {
1775
+ Object.defineProperties(exports,{__esModule:{value:true},[Symbol.toStringTag]:{value:`Module`}});const{PI:e}=Math,t=e+1e-4,n=.5,r=[1,1];function i(e,t,n,r=e=>e){return e*r(.5-t*(.5-n))}const{min:a}=Math;function o(e,t,n){let r=a(1,t/n);return a(1,e+(a(1,1-r)-e)*(r*.275))}function s(e){return [-e[0],-e[1]]}function c(e,t){return [e[0]+t[0],e[1]+t[1]]}function l(e,t,n){return e[0]=t[0]+n[0],e[1]=t[1]+n[1],e}function u(e,t){return [e[0]-t[0],e[1]-t[1]]}function d(e,t,n){return e[0]=t[0]-n[0],e[1]=t[1]-n[1],e}function f(e,t){return [e[0]*t,e[1]*t]}function p(e,t,n){return e[0]=t[0]*n,e[1]=t[1]*n,e}function m(e,t){return [e[0]/t,e[1]/t]}function h(e){return [e[1],-e[0]]}function g(e,t){let n=t[0];return e[0]=t[1],e[1]=-n,e}function ee(e,t){return e[0]*t[0]+e[1]*t[1]}function _(e,t){return e[0]===t[0]&&e[1]===t[1]}function v(e){return Math.hypot(e[0],e[1])}function y(e,t){let n=e[0]-t[0],r=e[1]-t[1];return n*n+r*r}function b(e){return m(e,v(e))}function x(e,t){return Math.hypot(e[1]-t[1],e[0]-t[0])}function S(e,t,n){let r=Math.sin(n),i=Math.cos(n),a=e[0]-t[0],o=e[1]-t[1],s=a*i-o*r,c=a*r+o*i;return [s+t[0],c+t[1]]}function C(e,t,n,r){let i=Math.sin(r),a=Math.cos(r),o=t[0]-n[0],s=t[1]-n[1],c=o*a-s*i,l=o*i+s*a;return e[0]=c+n[0],e[1]=l+n[1],e}function w(e,t,n){return c(e,f(u(t,e),n))}function te(e,t,n,r){let i=n[0]-t[0],a=n[1]-t[1];return e[0]=t[0]+i*r,e[1]=t[1]+a*r,e}function T(e,t,n){return c(e,f(t,n))}const E=[0,0],D=[0,0],O=[0,0];function k(e,n){let r=T(e,b(h(u(e,c(e,[1,1])))),-n),i=[],a=1/13;for(let n=a;n<=1;n+=a)i.push(S(r,e,t*2*n));return i}function A(e,n,r){let i=[],a=1/r;for(let r=a;r<=1;r+=a)i.push(S(n,e,t*r));return i}function j(e,t,n){let r=u(t,n),i=f(r,.5),a=f(r,.51);return [u(e,i),u(e,a),c(e,a),c(e,i)]}function M(e,n,r,i){let a=[],o=T(e,n,r),s=1/i;for(let n=s;n<1;n+=s)a.push(S(o,e,t*3*n));return a}function ne(e,t,n){return [c(e,f(t,n)),c(e,f(t,n*.99)),u(e,f(t,n*.99)),u(e,f(t,n))]}function N(e,t,n){return e===false||e===void 0?0:e===true?Math.max(t,n):e}function re(e,t,n){return e.slice(0,10).reduce((e,r)=>{let i=r.pressure;return t&&(i=o(e,r.distance,n)),(e+i)/2},e[0].pressure)}function P(e,n={}){let{size:r=16,smoothing:a=.5,thinning:f=.5,simulatePressure:m=true,easing:_=e=>e,start:v={},end:b={},last:x=false}=n,{cap:S=true,easing:w=e=>e*(2-e)}=v,{cap:T=true,easing:P=e=>--e*e*e+1}=b;if(e.length===0||r<=0)return [];let F=e[e.length-1].runningLength,I=N(v.taper,r,F),L=N(b.taper,r,F),R=(r*a)**2,z=[],B=[],V=re(e,m,r),H=i(r,f,e[e.length-1].pressure,_),U,W=e[0].vector,G=e[0].point,K=G,q=G,J=K,Y=false;for(let n=0;n<e.length;n++){let{pressure:a}=e[n],{point:s,vector:h,distance:v,runningLength:b}=e[n],x=n===e.length-1;if(!x&&F-b<3)continue;f?(m&&(a=o(V,v,r)),H=i(r,f,a,_)):H=r/2,U===void 0&&(U=H);let S=b<I?w(b/I):1,T=F-b<L?P((F-b)/L):1;H=Math.max(.01,H*Math.min(S,T));let k=(x?e[n]:e[n+1]).vector,A=x?1:ee(h,k),j=ee(h,W)<0&&!Y,M=A!==null&&A<0;if(j||M){g(E,W),p(E,E,H);for(let e=0;e<=1;e+=.07692307692307693)d(D,s,E),C(D,D,s,t*e),q=[D[0],D[1]],z.push(q),l(O,s,E),C(O,O,s,t*-e),J=[O[0],O[1]],B.push(J);G=q,K=J,M&&(Y=true);continue}if(Y=false,x){g(E,h),p(E,E,H),z.push(u(s,E)),B.push(c(s,E));continue}te(E,k,h,A),g(E,E),p(E,E,H),d(D,s,E),q=[D[0],D[1]],(n<=1||y(G,q)>R)&&(z.push(q),G=q),l(O,s,E),J=[O[0],O[1]],(n<=1||y(K,J)>R)&&(B.push(J),K=J),V=a,W=h;}let X=[e[0].point[0],e[0].point[1]],Z=e.length>1?[e[e.length-1].point[0],e[e.length-1].point[1]]:c(e[0].point,[1,1]),Q=[],$=[];if(e.length===1){if(!(I||L)||x)return k(X,U||H)}else {I||L&&e.length===1||(S?Q.push(...A(X,B[0],13)):Q.push(...j(X,z[0],B[0])));let t=h(s(e[e.length-1].vector));L||I&&e.length===1?$.push(Z):T?$.push(...M(Z,t,H,29)):$.push(...ne(Z,t,H));}return z.concat($,B.reverse(),Q)}const F=[0,0];function I(e){return e!=null&&e>=0}function L(e,t={}){let{streamline:i=.5,size:a=16,last:o=false}=t;if(e.length===0)return [];let s=.15+(1-i)*.85,l=Array.isArray(e[0])?e:e.map(({x:e,y:t,pressure:r=n})=>[e,t,r]);if(l.length===2){let e=l[1];l=l.slice(0,-1);for(let t=1;t<5;t++)l.push(w(l[0],e,t/4));}l.length===1&&(l=[...l,[...c(l[0],r),...l[0].slice(2)]]);let u=[{point:[l[0][0],l[0][1]],pressure:I(l[0][2])?l[0][2]:.25,vector:[...r],distance:0,runningLength:0}],f=false,p=0,m=u[0],h=l.length-1;for(let e=1;e<l.length;e++){let t=o&&e===h?[l[e][0],l[e][1]]:w(m.point,l[e],s);if(_(m.point,t))continue;let r=x(t,m.point);if(p+=r,e<h&&!f){if(p<a)continue;f=true;}d(F,m.point,t),m={point:t,pressure:I(l[e][2])?l[e][2]:n,vector:b(F),distance:r,runningLength:p},u.push(m);}return u[0].vector=u[1]?.vector||[0,0],u}function R(e,t={}){return P(L(e,t),t)}var z=R;exports.default=z,exports.getStroke=R,exports.getStrokeOutlinePoints=P,exports.getStrokePoints=L;
1776
+
1777
+ } (cjs));
1778
+ return cjs;
1779
+ }
1780
+
1781
+ var cjsExports = requireCjs();
1782
+
1783
+ class KritzelPath extends KritzelBaseObject {
1784
+ __class__ = 'KritzelPath';
1785
+ points;
1786
+ d;
1787
+ stroke = { light: 'none', dark: 'none' };
1788
+ strokeWidth;
1789
+ lineSlack = 0.5;
1790
+ fill;
1791
+ x = 0;
1792
+ y = 0;
1793
+ height = 0;
1794
+ width = 0;
1795
+ scale = 1;
1796
+ options;
1797
+ isVisible = true;
1798
+ isDebugInfoVisible = true;
1799
+ isCompleted = false;
1800
+ _adjustedPoints = null;
1801
+ /**
1802
+ * Gets the SVG viewBox string for local (object-relative) rendering.
1803
+ *
1804
+ * The path's `d` string is generated in local coordinates (offset so the
1805
+ * object's top-left origin is at 0,0), so the viewBox always starts at 0,0.
1806
+ * Keeping the SVG's internal coordinates small — independent of the object's
1807
+ * world position — avoids float32 precision loss in the browser's SVG
1808
+ * rasterizer (Skia), which otherwise fails to paint paths whose absolute
1809
+ * world coordinates exceed ~2^24 (e.g. far from the origin at extreme zoom).
1810
+ * @returns The viewBox attribute value in the format "0 0 width height"
1811
+ */
1812
+ get viewBox() {
1813
+ return `0 0 ${this.width} ${this.height}`;
1814
+ }
1815
+ /**
1816
+ * Creates a new KritzelPath instance.
1817
+ * @param config - Optional configuration object for the path
1818
+ * @param config.points - Array of [x, y] coordinate pairs defining the path
1819
+ * @param config.translateX - Horizontal translation offset (default: 0)
1820
+ * @param config.translateY - Vertical translation offset (default: 0)
1821
+ * @param config.scale - Scale factor for the path (default: 1)
1822
+ * @param config.strokeWidth - Width of the stroke in pixels (default: 8)
1823
+ * @param config.fill - Theme-aware fill color with light and dark variants
1824
+ * @param config.lineSlack - Additional padding for bounding box calculations (default: 0.5)
1825
+ */
1826
+ constructor(config) {
1827
+ super();
1828
+ this.options = config;
1829
+ this.points = config?.points ?? [];
1830
+ this.translateX = config?.translateX ?? 0;
1831
+ this.translateY = config?.translateY ?? 0;
1832
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
1833
+ this.scale = config?.scale ?? 1;
1834
+ this.strokeWidth = config?.strokeWidth ?? 8;
1835
+ this.fill = config?.fill ?? { light: '#000000', dark: '#ffffff' };
1836
+ this.d = this.generateSvgPath();
1837
+ this.updateDimensions();
1838
+ }
1839
+ /**
1840
+ * Factory method to create a new KritzelPath instance with a core reference.
1841
+ * This is the preferred way to create paths that will be managed by the canvas.
1842
+ * @param core - The KritzelCore instance that will manage this path
1843
+ * @param options - Optional configuration options for the path
1844
+ * @returns A new KritzelPath instance configured with the provided options
1845
+ */
1846
+ static create(core, options) {
1847
+ const object = new KritzelPath();
1848
+ object._core = core;
1849
+ object.id = object.generateId();
1850
+ object.workspaceId = core.store.state.activeWorkspace.id;
1851
+ object.userId = core.user?.id;
1852
+ object.options = options;
1853
+ object.points = options?.points ?? [];
1854
+ object.translateX = options?.translateX ?? 0;
1855
+ object.translateY = options?.translateY ?? 0;
1856
+ object.rotation = KritzelMathHelper.degreesToRadians(options?.rotation ?? 0);
1857
+ object.scale = options?.scale ?? 1;
1858
+ object.strokeWidth = options?.strokeWidth ?? 8;
1859
+ object.fill = options?.fill ?? { light: '#000000', dark: '#ffffff' };
1860
+ object.opacity = options?.opacity ?? 1;
1861
+ object.zIndex = core.store.currentZIndex;
1862
+ object.d = object.generateSvgPath();
1863
+ object.updateDimensions();
1864
+ return object;
1865
+ }
1866
+ /**
1867
+ * Creates a deep copy of this path object.
1868
+ * The points array is cloned to ensure the copy is independent.
1869
+ * @returns A new KritzelPath instance with copied properties
1870
+ */
1871
+ copy() {
1872
+ const copiedObject = super.copy();
1873
+ if (this.points) {
1874
+ copiedObject.points = this.points.map(p => [...p]);
1875
+ }
1876
+ return copiedObject;
1877
+ }
1878
+ /**
1879
+ * Restores the path from a serialized object, then regenerates the SVG `d`
1880
+ * string from the restored points.
1881
+ *
1882
+ * Regeneration is required because the `d` string is now emitted in local
1883
+ * (object-relative) coordinates paired with a "0 0 width height" viewBox.
1884
+ * Older persisted paths stored `d` in absolute world coordinates, which
1885
+ * would be inconsistent with the local-coordinate viewBox and fail to
1886
+ * rasterize far from the origin at extreme zoom. Recomputing `d` here makes
1887
+ * both freshly created and previously persisted paths render identically.
1888
+ * @param object - The serialized path data to restore from
1889
+ * @returns This instance cast to type T
1890
+ */
1891
+ deserialize(object) {
1892
+ const result = super.deserialize(object);
1893
+ this.d = this.generateSvgPath();
1894
+ return result;
1895
+ }
1896
+ /**
1897
+ * Resizes the path to fit within the specified dimensions.
1898
+ * Scales all points proportionally and updates the SVG path.
1899
+ * @param x - New X position for the path, or null to keep current
1900
+ * @param y - New Y position for the path, or null to keep current
1901
+ * @param width - New width for the path (must be > 1)
1902
+ * @param height - New height for the path (must be > 1)
1903
+ */
1904
+ resize(x, y, width, height) {
1905
+ if (width <= 1 || height <= 1) {
1906
+ return;
1907
+ }
1908
+ const scaleX = width / this.width;
1909
+ const scaleY = height / this.height;
1910
+ this.width = width;
1911
+ this.height = height;
1912
+ if (this.points.length === 1) {
1913
+ const p = this.points[0];
1914
+ const spreadX = Math.max(0, width - this.strokeWidth) / scaleX;
1915
+ const spreadY = Math.max(0, height - this.strokeWidth) / scaleY;
1916
+ this.points.push([p[0] + spreadX, p[1] + spreadY]);
1917
+ }
1918
+ this.points = this.points.map(([x, y]) => [x * scaleX, y * scaleY]);
1919
+ this.d = this.generateSvgPath();
1920
+ this.width = Math.max(...this.points.map(p => p[0])) - Math.min(...this.points.map(p => p[0])) + this.strokeWidth;
1921
+ this.height = Math.max(...this.points.map(p => p[1])) - Math.min(...this.points.map(p => p[1])) + this.strokeWidth;
1922
+ this.x = Math.min(...this.points.map(p => p[0])) - this.strokeWidth / 2;
1923
+ this.y = Math.min(...this.points.map(p => p[1])) - this.strokeWidth / 2;
1924
+ if (x !== null) {
1925
+ this.translateX = x;
1926
+ }
1927
+ if (y !== null) {
1928
+ this.translateY = y;
1929
+ }
1930
+ this._adjustedPoints = null;
1931
+ this._core.store.objects.update(this);
1932
+ }
1933
+ /**
1934
+ * Rotates the path by the specified angle.
1935
+ * Invalidates cached adjusted points to force recalculation.
1936
+ * @param value - Rotation angle in radians
1937
+ */
1938
+ rotate(value) {
1939
+ this.rotation = value;
1940
+ this._adjustedPoints = null;
1941
+ this._core.store.objects.update(this);
1942
+ }
1943
+ /**
1944
+ * Moves the path by calculating the delta between start and end positions.
1945
+ * The delta is adjusted for the current viewport scale.
1946
+ * @param startX - Starting X coordinate in screen space
1947
+ * @param startY - Starting Y coordinate in screen space
1948
+ * @param endX - Ending X coordinate in screen space
1949
+ * @param endY - Ending Y coordinate in screen space
1950
+ */
1951
+ move(startX, startY, endX, endY) {
1952
+ const deltaX = (startX - endX) / this._core.store.state.scale;
1953
+ const deltaY = (startY - endY) / this._core.store.state.scale;
1954
+ this.translateX += deltaX;
1955
+ this.translateY += deltaY;
1956
+ this._adjustedPoints = null;
1957
+ this._core.store.objects.update(this);
1958
+ }
1959
+ /**
1960
+ * Tests whether a point intersects with the path's stroke.
1961
+ * Uses distance-to-line-segment calculations for accurate hit detection.
1962
+ * @param x - X coordinate to test in world space
1963
+ * @param y - Y coordinate to test in world space
1964
+ * @returns True if the point is within the stroke width of the path
1965
+ */
1966
+ hitTest(x, y) {
1967
+ const halfStroke = this.strokeWidth / this.scale / 2;
1968
+ if (this._adjustedPoints === null) {
1969
+ this._adjustedPoints = this.computeAdjustedPoints();
1970
+ }
1971
+ if (this._adjustedPoints.length === 1) {
1972
+ const p1 = this._adjustedPoints[0];
1973
+ const distance = this.pointToLineSegmentDistance(x, y, p1[0], p1[1], p1[0], p1[1]);
1974
+ return distance <= halfStroke;
1975
+ }
1976
+ for (let i = 0; i < this._adjustedPoints.length - 1; i++) {
1977
+ const p1 = this._adjustedPoints[i];
1978
+ const p2 = this._adjustedPoints[i + 1];
1979
+ const distance = this.pointToLineSegmentDistance(x, y, p1[0], p1[1], p2[0], p2[1]);
1980
+ if (distance <= halfStroke) {
1981
+ return true;
1982
+ }
1983
+ }
1984
+ return false;
1985
+ }
1986
+ /**
1987
+ * Tests whether a polygon intersects with the path.
1988
+ * Checks for point containment, edge intersections, and proximity to stroke.
1989
+ * @param polygon - The polygon to test against, defined by its corner points
1990
+ * @returns True if any part of the polygon overlaps with the path's stroke
1991
+ */
1992
+ hitTestPolygon(polygon) {
1993
+ const halfStroke = this.strokeWidth / this.scale / 2;
1994
+ if (this._adjustedPoints === null) {
1995
+ this._adjustedPoints = this.computeAdjustedPoints();
1996
+ }
1997
+ const polyPoints = [
1998
+ { x: polygon.bottomLeft.x, y: polygon.bottomLeft.y },
1999
+ { x: polygon.bottomRight.x, y: polygon.bottomRight.y },
2000
+ { x: polygon.topRight.x, y: polygon.topRight.y },
2001
+ { x: polygon.topLeft.x, y: polygon.topLeft.y },
2002
+ ];
2003
+ for (const [px, py] of this._adjustedPoints) {
2004
+ if (KritzelGeometryHelper.isPointInPolygon({ x: px, y: py }, polyPoints)) {
2005
+ return true;
2006
+ }
2007
+ }
2008
+ for (const pt of polyPoints) {
2009
+ if (this.hitTest(pt.x, pt.y)) {
2010
+ return true;
2011
+ }
2012
+ }
2013
+ for (let i = 0; i < this._adjustedPoints.length - 1; i++) {
2014
+ const p1 = { x: this._adjustedPoints[i][0], y: this._adjustedPoints[i][1] };
2015
+ const p2 = { x: this._adjustedPoints[i + 1][0], y: this._adjustedPoints[i + 1][1] };
2016
+ for (let j = 0; j < polyPoints.length; j++) {
2017
+ const q1 = polyPoints[j];
2018
+ const q2 = polyPoints[(j + 1) % polyPoints.length];
2019
+ if (KritzelGeometryHelper.intersectLines(p1, p2, q1, q2)) {
2020
+ return true;
2021
+ }
2022
+ }
2023
+ }
2024
+ for (let i = 0; i < this._adjustedPoints.length - 1; i++) {
2025
+ const p1 = this._adjustedPoints[i];
2026
+ const p2 = this._adjustedPoints[i + 1];
2027
+ for (let j = 0; j < polyPoints.length; j++) {
2028
+ const q1 = polyPoints[j];
2029
+ const q2 = polyPoints[(j + 1) % polyPoints.length];
2030
+ const d1 = this.pointToLineSegmentDistance(q1.x, q1.y, p1[0], p1[1], p2[0], p2[1]);
2031
+ const d2 = this.pointToLineSegmentDistance(q2.x, q2.y, p1[0], p1[1], p2[0], p2[1]);
2032
+ const d3 = this.pointToLineSegmentDistance(p1[0], p1[1], q1.x, q1.y, q2.x, q2.y);
2033
+ const d4 = this.pointToLineSegmentDistance(p2[0], p2[1], q1.x, q1.y, q2.x, q2.y);
2034
+ const minD = Math.min(d1, d2, d3, d4);
2035
+ if (minD <= halfStroke) {
2036
+ return true;
2037
+ }
2038
+ }
2039
+ }
2040
+ return false;
2041
+ }
2042
+ /**
2043
+ * Updates the path's position to the specified coordinates.
2044
+ * Invalidates cached adjusted points and triggers a store update.
2045
+ * @param x - New X position in world space
2046
+ * @param y - New Y position in world space
2047
+ */
2048
+ updatePosition(x, y) {
2049
+ this.translateX = x;
2050
+ this.translateY = y;
2051
+ this._adjustedPoints = null;
2052
+ this._core.store.objects.update(this);
2053
+ }
2054
+ /**
2055
+ * Lifecycle hook called after properties have been updated.
2056
+ * Regenerates the SVG path and bounding box when strokeWidth changes.
2057
+ * @param changedProperties - Array of property names that were modified
2058
+ */
2059
+ onAfterUpdate(changedProperties) {
2060
+ if (changedProperties.includes('strokeWidth')) {
2061
+ this.d = this.generateSvgPath();
2062
+ this.updateBoundingBox();
2063
+ this._adjustedPoints = null;
2064
+ }
2065
+ }
2066
+ /**
2067
+ * Computes the world-space coordinates of all path points.
2068
+ * Applies rotation around the path's center and adjusts for translation and scale.
2069
+ * Results are cached in _adjustedPoints for performance.
2070
+ * @returns Array of [x, y] coordinate pairs in world space
2071
+ */
2072
+ computeAdjustedPoints() {
2073
+ if (!this.points?.length) {
2074
+ return [];
2075
+ }
2076
+ const angle = this.rotation;
2077
+ const cos = Math.cos(angle);
2078
+ const sin = Math.sin(angle);
2079
+ const xs = this.points.map(p => p[0]);
2080
+ const ys = this.points.map(p => p[1]);
2081
+ const pivot = {
2082
+ x: (Math.min(...xs) + Math.max(...xs)) / 2,
2083
+ y: (Math.min(...ys) + Math.max(...ys)) / 2,
2084
+ };
2085
+ const { x: cx, y: cy } = pivot;
2086
+ const rotatedPoints = this.points.map(([x, y]) => {
2087
+ const dx = x - cx;
2088
+ const dy = y - cy;
2089
+ return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
2090
+ });
2091
+ return rotatedPoints.map(([px, py]) => [Math.abs(px - this.x) / this.scale + this.translateX, Math.abs(py - this.y) / this.scale + this.translateY]);
2092
+ }
2093
+ /**
2094
+ * Calculates the shortest distance from a point to a line segment.
2095
+ * Uses projection to find the closest point on the segment.
2096
+ * @param x - X coordinate of the test point
2097
+ * @param y - Y coordinate of the test point
2098
+ * @param x1 - X coordinate of the line segment start
2099
+ * @param y1 - Y coordinate of the line segment start
2100
+ * @param x2 - X coordinate of the line segment end
2101
+ * @param y2 - Y coordinate of the line segment end
2102
+ * @returns The shortest distance from the point to the line segment
2103
+ */
2104
+ pointToLineSegmentDistance(x, y, x1, y1, x2, y2) {
2105
+ const A = x - x1;
2106
+ const B = y - y1;
2107
+ const C = x2 - x1;
2108
+ const D = y2 - y1;
2109
+ const dot = A * C + B * D;
2110
+ const len_sq = C * C + D * D;
2111
+ let param = -1;
2112
+ if (len_sq !== 0) {
2113
+ // in case of 0 length line
2114
+ param = dot / len_sq;
2115
+ }
2116
+ let xx, yy;
2117
+ if (param < 0) {
2118
+ xx = x1;
2119
+ yy = y1;
2120
+ }
2121
+ else if (param > 1) {
2122
+ xx = x2;
2123
+ yy = y2;
2124
+ }
2125
+ else {
2126
+ xx = x1 + param * C;
2127
+ yy = y1 + param * D;
2128
+ }
2129
+ const dx = x - xx;
2130
+ const dy = y - yy;
2131
+ return Math.sqrt(dx * dx + dy * dy);
2132
+ }
2133
+ /**
2134
+ * Updates width, height, x, y, translateX, and translateY based on current points.
2135
+ * Uses the unrotated local points and stroke width for base dimensions.
2136
+ * Called during initial setup to establish the path's dimensions and position.
2137
+ */
2138
+ updateDimensions() {
2139
+ const minX = Math.min(...this.points.map(p => p[0])) - this.strokeWidth / 2;
2140
+ const minY = Math.min(...this.points.map(p => p[1])) - this.strokeWidth / 2;
2141
+ const maxX = Math.max(...this.points.map(p => p[0])) + this.strokeWidth / 2;
2142
+ const maxY = Math.max(...this.points.map(p => p[1])) + this.strokeWidth / 2;
2143
+ this.width = maxX - minX + this.lineSlack;
2144
+ this.height = maxY - minY + this.lineSlack;
2145
+ this.x = minX;
2146
+ this.y = minY;
2147
+ this.translateX = (this.x + this.translateX) / this.scale;
2148
+ this.translateY = (this.y + this.translateY) / this.scale;
2149
+ }
2150
+ /**
2151
+ * Updates width, height, x, y based on current points and strokeWidth.
2152
+ * Does NOT modify translateX/translateY (use updateDimensions for initial setup).
2153
+ */
2154
+ updateBoundingBox() {
2155
+ const minX = Math.min(...this.points.map(p => p[0])) - this.strokeWidth / 2;
2156
+ const minY = Math.min(...this.points.map(p => p[1])) - this.strokeWidth / 2;
2157
+ const maxX = Math.max(...this.points.map(p => p[0])) + this.strokeWidth / 2;
2158
+ const maxY = Math.max(...this.points.map(p => p[1])) + this.strokeWidth / 2;
2159
+ this.width = maxX - minX + this.lineSlack;
2160
+ this.height = maxY - minY + this.lineSlack;
2161
+ this.x = minX;
2162
+ this.y = minY;
2163
+ }
2164
+ /**
2165
+ * Generates the SVG path data string from the current points.
2166
+ * Uses perfect-freehand to create a smooth, pressure-sensitive stroke.
2167
+ * @returns SVG path data string (d attribute value)
2168
+ */
2169
+ generateSvgPath() {
2170
+ const stroke = this.getStrokeFromPoints(this.points, this.strokeWidth);
2171
+ const offsetX = this.points.length ? Math.min(...this.points.map(p => p[0])) - this.strokeWidth / 2 : 0;
2172
+ const offsetY = this.points.length ? Math.min(...this.points.map(p => p[1])) - this.strokeWidth / 2 : 0;
2173
+ return this.getSvgPathFromStroke(stroke, offsetX, offsetY);
2174
+ }
2175
+ /**
2176
+ * Converts raw input points into a smooth stroke outline using perfect-freehand.
2177
+ * Applies pressure simulation and smoothing for a natural pen-like appearance.
2178
+ * @param points - Array of [x, y] coordinate pairs representing the input path
2179
+ * @param strokeWidth - The desired stroke width in pixels
2180
+ * @returns Array of [x, y] coordinate pairs forming the stroke outline polygon
2181
+ */
2182
+ getStrokeFromPoints(points, strokeWidth) {
2183
+ return cjsExports.getStroke(points, {
2184
+ size: strokeWidth,
2185
+ thinning: 0.5,
2186
+ smoothing: 0.5,
2187
+ streamline: 0.5,
2188
+ easing: t => t,
2189
+ simulatePressure: true,
2190
+ last: true,
2191
+ start: {
2192
+ cap: true,
2193
+ taper: 0,
2194
+ easing: t => t,
2195
+ },
2196
+ end: {
2197
+ cap: true,
2198
+ taper: 0,
2199
+ easing: t => t,
2200
+ },
2201
+ });
2202
+ }
2203
+ /**
2204
+ * Converts stroke outline points into an SVG path data string.
2205
+ * Uses quadratic bezier curves for smooth rendering.
2206
+ * Coordinates are emitted relative to (offsetX, offsetY) so the resulting
2207
+ * path lives in the object's local coordinate space (paired with a
2208
+ * "0 0 width height" viewBox). This keeps SVG-internal coordinates small
2209
+ * regardless of the object's world position, avoiding float32 rasterizer
2210
+ * precision failures far from the origin.
2211
+ * @param points - Array of [x, y] coordinate pairs forming the stroke outline
2212
+ * @param offsetX - X origin to subtract from every coordinate (default 0)
2213
+ * @param offsetY - Y origin to subtract from every coordinate (default 0)
2214
+ * @param closed - Whether to close the path with 'Z' command (default: true)
2215
+ * @returns SVG path data string with M, Q, and T commands
2216
+ */
2217
+ getSvgPathFromStroke(points, offsetX = 0, offsetY = 0, closed = true) {
2218
+ const len = points.length;
2219
+ if (len < 4) {
2220
+ return ``;
2221
+ }
2222
+ const a = points[0];
2223
+ const b = points[1];
2224
+ const c = points[2];
2225
+ const ax = a[0] - offsetX;
2226
+ const ay = a[1] - offsetY;
2227
+ const bx = b[0] - offsetX;
2228
+ const by = b[1] - offsetY;
2229
+ const cx = c[0] - offsetX;
2230
+ const cy = c[1] - offsetY;
2231
+ let result = `M${ax.toFixed(2)},${ay.toFixed(2)} Q${bx.toFixed(2)},${by.toFixed(2)} ${KritzelMathHelper.average(bx, cx).toFixed(2)},${KritzelMathHelper.average(by, cy).toFixed(2)} T`;
2232
+ for (let i = 2, max = len - 1; i < max; i++) {
2233
+ const p = points[i];
2234
+ const q = points[i + 1];
2235
+ const px = p[0] - offsetX;
2236
+ const py = p[1] - offsetY;
2237
+ const qx = q[0] - offsetX;
2238
+ const qy = q[1] - offsetY;
2239
+ result += `${KritzelMathHelper.average(px, qx).toFixed(2)},${KritzelMathHelper.average(py, qy).toFixed(2)} `;
2240
+ }
2241
+ if (closed) {
2242
+ result += 'Z';
2243
+ }
2244
+ return result;
2245
+ }
2246
+ /**
2247
+ * Determines if the path appears too small on screen for detailed rendering.
2248
+ * Used for level-of-detail optimizations.
2249
+ * @returns True if the path's screen area is less than 500 square pixels
2250
+ */
2251
+ isLowRes() {
2252
+ if (!this._core) {
2253
+ return false;
2254
+ }
2255
+ const viewportScale = this._core.store.state.scale;
2256
+ const scaledWidth = this.boundingBox.width * viewportScale;
2257
+ const scaledHeight = this.boundingBox.height * viewportScale;
2258
+ return (scaledWidth * scaledHeight) < 500;
2259
+ }
2260
+ /**
2261
+ * Finds the point where a line from outsidePoint to the path's center
2262
+ * intersects the stroke edge. Used for arrow clipping at the actual stroke edge.
2263
+ * Uses binary search refinement for accurate intersection detection.
2264
+ * @param outsidePoint - A point outside the path to trace from
2265
+ * @returns The intersection point on the stroke edge, or null if no intersection found
2266
+ */
2267
+ getClipPoint(outsidePoint) {
2268
+ const centerX = this.centerX;
2269
+ const centerY = this.centerY;
2270
+ const halfStroke = this.strokeWidth / this.scale / 2;
2271
+ // Ensure adjusted points are computed
2272
+ if (this._adjustedPoints === null) {
2273
+ this._adjustedPoints = this.computeAdjustedPoints();
2274
+ }
2275
+ if (this._adjustedPoints.length < 1) {
2276
+ return null;
2277
+ }
2278
+ // Sample along the line from outsidePoint to center to find stroke edge
2279
+ const steps = 32;
2280
+ let prevOutside = true;
2281
+ for (let i = 1; i <= steps; i++) {
2282
+ const t = i / steps;
2283
+ const sampleX = outsidePoint.x + (centerX - outsidePoint.x) * t;
2284
+ const sampleY = outsidePoint.y + (centerY - outsidePoint.y) * t;
2285
+ // Check distance to path stroke
2286
+ let minDistance = Infinity;
2287
+ if (this._adjustedPoints.length === 1) {
2288
+ const p = this._adjustedPoints[0];
2289
+ const dx = sampleX - p[0];
2290
+ const dy = sampleY - p[1];
2291
+ minDistance = Math.sqrt(dx * dx + dy * dy);
2292
+ }
2293
+ else {
2294
+ for (let j = 0; j < this._adjustedPoints.length - 1; j++) {
2295
+ const p1 = this._adjustedPoints[j];
2296
+ const p2 = this._adjustedPoints[j + 1];
2297
+ const dist = this.pointToLineSegmentDistance(sampleX, sampleY, p1[0], p1[1], p2[0], p2[1]);
2298
+ if (dist < minDistance) {
2299
+ minDistance = dist;
2300
+ }
2301
+ }
2302
+ }
2303
+ const isInside = minDistance <= halfStroke;
2304
+ // Found the edge: transition from outside to inside
2305
+ if (prevOutside && isInside) {
2306
+ // Refine the intersection point using binary search
2307
+ let tLow = (i - 1) / steps;
2308
+ let tHigh = t;
2309
+ for (let k = 0; k < 8; k++) {
2310
+ const tMid = (tLow + tHigh) / 2;
2311
+ const midX = outsidePoint.x + (centerX - outsidePoint.x) * tMid;
2312
+ const midY = outsidePoint.y + (centerY - outsidePoint.y) * tMid;
2313
+ let midDist = Infinity;
2314
+ if (this._adjustedPoints.length === 1) {
2315
+ const p = this._adjustedPoints[0];
2316
+ const dx = midX - p[0];
2317
+ const dy = midY - p[1];
2318
+ midDist = Math.sqrt(dx * dx + dy * dy);
2319
+ }
2320
+ else {
2321
+ for (let j = 0; j < this._adjustedPoints.length - 1; j++) {
2322
+ const p1 = this._adjustedPoints[j];
2323
+ const p2 = this._adjustedPoints[j + 1];
2324
+ const dist = this.pointToLineSegmentDistance(midX, midY, p1[0], p1[1], p2[0], p2[1]);
2325
+ if (dist < midDist) {
2326
+ midDist = dist;
2327
+ }
2328
+ }
2329
+ }
2330
+ if (midDist <= halfStroke) {
2331
+ tHigh = tMid;
2332
+ }
2333
+ else {
2334
+ tLow = tMid;
2335
+ }
2336
+ }
2337
+ const finalT = (tLow + tHigh) / 2;
2338
+ return {
2339
+ x: outsidePoint.x + (centerX - outsidePoint.x) * finalT,
2340
+ y: outsidePoint.y + (centerY - outsidePoint.y) * finalT,
2341
+ };
2342
+ }
2343
+ prevOutside = !isInside;
2344
+ }
2345
+ return null;
2346
+ }
2347
+ getSupportedExportFormats() {
2348
+ return ['svg'];
2349
+ }
2350
+ async exportAs(format, context) {
2351
+ if (format !== 'svg') {
2352
+ return null;
2353
+ }
2354
+ return context.getObjectAsSvgDataUrl(this);
2355
+ }
2356
+ }
2357
+
2358
+ class KritzelLine extends KritzelBaseObject {
2359
+ __class__ = 'KritzelLine';
2360
+ startX;
2361
+ startY;
2362
+ endX;
2363
+ endY;
2364
+ controlX;
2365
+ controlY;
2366
+ stroke;
2367
+ strokeWidth;
2368
+ scale = 1;
2369
+ options;
2370
+ /** Anchor connection for the start point of the line */
2371
+ startAnchor;
2372
+ /** Anchor connection for the end point of the line */
2373
+ endAnchor;
2374
+ /** Arrow head configuration for start and end of line */
2375
+ arrows;
2376
+ isVisible = true;
2377
+ isDebugInfoVisible = true;
2378
+ isCompleted = false;
2379
+ /** Cached adjusted points for hit testing, accounting for rotation and translation */
2380
+ _adjustedPoints = null;
2381
+ /** Cached clip information for anchored line endpoints */
2382
+ _clipInfo = null;
2383
+ /**
2384
+ * Generates the SVG path data string for the line.
2385
+ * Returns a quadratic Bezier curve path if control point exists, otherwise a straight line.
2386
+ * @returns SVG path data string in the format "M x y Q cx cy ex ey" or "M x y L ex ey"
2387
+ */
2388
+ get d() {
2389
+ if (this.controlX !== undefined && this.controlY !== undefined) {
2390
+ return `M ${this.startX} ${this.startY} Q ${this.controlX} ${this.controlY} ${this.endX} ${this.endY}`;
2391
+ }
2392
+ return `M ${this.startX} ${this.startY} L ${this.endX} ${this.endY}`;
2393
+ }
2394
+ /**
2395
+ * Generates the SVG viewBox attribute string for rendering the line.
2396
+ * @returns ViewBox string in the format "x y width height"
2397
+ */
2398
+ get viewBox() {
2399
+ return `${this.x} ${this.y} ${this.width} ${this.height}`;
2400
+ }
2401
+ /**
2402
+ * Creates a new KritzelLine instance.
2403
+ * @param config - Optional configuration options for the line including start/end points,
2404
+ * control point for curves, stroke settings, anchors, and arrow configuration
2405
+ */
2406
+ constructor(config) {
2407
+ super();
2408
+ this.options = config;
2409
+ this.startX = config?.startX ?? 0;
2410
+ this.startY = config?.startY ?? 0;
2411
+ this.endX = config?.endX ?? 0;
2412
+ this.endY = config?.endY ?? 0;
2413
+ this.controlX = config?.controlX;
2414
+ this.controlY = config?.controlY;
2415
+ this.translateX = config?.translateX ?? 0;
2416
+ this.translateY = config?.translateY ?? 0;
2417
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
2418
+ this.scale = config?.scale ?? 1;
2419
+ this.strokeWidth = config?.strokeWidth ?? 4;
2420
+ this.stroke = config?.stroke ?? { light: '#000000', dark: '#ffffff' };
2421
+ this.startAnchor = config?.startAnchor;
2422
+ this.endAnchor = config?.endAnchor;
2423
+ this.arrows = config?.arrows;
2424
+ this.updateDimensions();
2425
+ }
2426
+ /**
2427
+ * Factory method to create a new KritzelLine with a core reference and unique ID.
2428
+ * This is the preferred way to create lines that will be managed by the core.
2429
+ * @param core - The KritzelCore instance that will manage this line
2430
+ * @param options - Optional configuration options for the line
2431
+ * @returns A fully initialized KritzelLine instance with generated ID and workspace assignment
2432
+ */
2433
+ static create(core, options) {
2434
+ const object = new KritzelLine();
2435
+ object._core = core;
2436
+ object.id = object.generateId();
2437
+ object.workspaceId = core.store.state.activeWorkspace.id;
2438
+ object.userId = core.user?.id;
2439
+ object.options = options;
2440
+ object.startX = options?.startX ?? 0;
2441
+ object.startY = options?.startY ?? 0;
2442
+ object.endX = options?.endX ?? 0;
2443
+ object.endY = options?.endY ?? 0;
2444
+ object.controlX = options?.controlX;
2445
+ object.controlY = options?.controlY;
2446
+ object.translateX = options?.translateX ?? 0;
2447
+ object.translateY = options?.translateY ?? 0;
2448
+ object.rotation = KritzelMathHelper.degreesToRadians(options?.rotation ?? 0);
2449
+ object.scale = options?.scale ?? 1;
2450
+ object.strokeWidth = options?.strokeWidth ?? 4;
2451
+ object.stroke = options?.stroke ?? { light: '#000000', dark: '#ffffff' };
2452
+ object.opacity = options?.opacity ?? 1;
2453
+ object.startAnchor = options?.startAnchor;
2454
+ object.endAnchor = options?.endAnchor;
2455
+ object.arrows = options?.arrows;
2456
+ object.zIndex = core.store.currentZIndex;
2457
+ object.updateDimensions();
2458
+ return object;
2459
+ }
2460
+ /**
2461
+ * Resizes the line to fit within the specified dimensions.
2462
+ * Scales both endpoints and control point proportionally, then repositions the line.
2463
+ * @param x - New X position for the line (or null to keep current)
2464
+ * @param y - New Y position for the line (or null to keep current)
2465
+ * @param width - New width to scale the line to (must be > 1)
2466
+ * @param height - New height to scale the line to (must be > 1)
2467
+ */
2468
+ resize(x, y, width, height) {
2469
+ if (width <= 1 || height <= 1) {
2470
+ return;
2471
+ }
2472
+ const scaleX = width / this.width;
2473
+ const scaleY = height / this.height;
2474
+ // Scale the line endpoints
2475
+ this.startX = this.startX * scaleX;
2476
+ this.startY = this.startY * scaleY;
2477
+ this.endX = this.endX * scaleX;
2478
+ this.endY = this.endY * scaleY;
2479
+ if (this.controlX !== undefined && this.controlY !== undefined) {
2480
+ this.controlX = this.controlX * scaleX;
2481
+ this.controlY = this.controlY * scaleY;
2482
+ }
2483
+ // Recalculate dimensions from scaled endpoints
2484
+ const { minX, minY, maxX, maxY } = this.calculateBoundingBox();
2485
+ this.width = maxX - minX;
2486
+ this.height = maxY - minY;
2487
+ this.x = minX;
2488
+ this.y = minY;
2489
+ // Set the new position
2490
+ this.translateX = x;
2491
+ this.translateY = y;
2492
+ this._adjustedPoints = null;
2493
+ this._clipInfo = null;
2494
+ this._core.store.objects.update(this);
2495
+ // Update anchors after the line is updated
2496
+ this._core.anchorManager.updateAnchorsForObject(this.id);
2497
+ if (this.startAnchor) {
2498
+ this._core.anchorManager.updateAnchorsForObject(this.startAnchor.objectId);
2499
+ }
2500
+ if (this.endAnchor) {
2501
+ this._core.anchorManager.updateAnchorsForObject(this.endAnchor.objectId);
2502
+ }
2503
+ }
2504
+ /**
2505
+ * Sets the rotation angle of the line around its center.
2506
+ * Invalidates cached adjusted points and clip info.
2507
+ * @param value - Rotation angle in radians
2508
+ */
2509
+ rotate(value) {
2510
+ this.rotation = value;
2511
+ this._adjustedPoints = null;
2512
+ this._clipInfo = null;
2513
+ this._core.store.objects.update(this);
2514
+ }
2515
+ /**
2516
+ * Moves the line by calculating the delta between start and end positions.
2517
+ * Updates anchored objects if the line has anchor connections.
2518
+ * @param startX - Starting X position of the drag in screen coordinates
2519
+ * @param startY - Starting Y position of the drag in screen coordinates
2520
+ * @param endX - Ending X position of the drag in screen coordinates
2521
+ * @param endY - Ending Y position of the drag in screen coordinates
2522
+ */
2523
+ move(startX, startY, endX, endY) {
2524
+ const deltaX = (startX - endX) / this._core.store.state.scale;
2525
+ const deltaY = (startY - endY) / this._core.store.state.scale;
2526
+ this.translateX += deltaX;
2527
+ this.translateY += deltaY;
2528
+ // If the line is anchored, we need to ensure the anchored endpoints stay at the target
2529
+ if (this.startAnchor) {
2530
+ this._core.anchorManager.updateAnchorsForObject(this.startAnchor.objectId);
2531
+ }
2532
+ if (this.endAnchor) {
2533
+ this._core.anchorManager.updateAnchorsForObject(this.endAnchor.objectId);
2534
+ }
2535
+ this._adjustedPoints = null;
2536
+ this._clipInfo = null;
2537
+ this._core.store.objects.update(this);
2538
+ }
2539
+ /**
2540
+ * Tests if a point intersects with the line within the stroke tolerance.
2541
+ * Uses distance to line segment for straight lines, or distance to Bezier curve for curved lines.
2542
+ * @param x - X coordinate of the point to test in world coordinates
2543
+ * @param y - Y coordinate of the point to test in world coordinates
2544
+ * @returns True if the point is within the stroke width tolerance of the line
2545
+ */
2546
+ hitTest(x, y) {
2547
+ const minHitThreshold = 20;
2548
+ const strokeWidth = Math.max(this.strokeWidth, minHitThreshold);
2549
+ const halfStroke = strokeWidth / this.scale / 2;
2550
+ if (this._adjustedPoints === null) {
2551
+ this._adjustedPoints = this.computeAdjustedPoints();
2552
+ }
2553
+ // For curved lines, use distance to the Bezier curve
2554
+ if (this.controlX !== undefined && this.controlY !== undefined) {
2555
+ const clip = this.getClipInfo();
2556
+ const startT = clip.start?.t ?? 0;
2557
+ const endT = clip.end?.t ?? 1;
2558
+ const distance = this.pointToBezierDistance(x, y, startT, endT);
2559
+ return distance <= halfStroke;
2560
+ }
2561
+ // For straight lines, use distance to line segment
2562
+ const clip = this.getClipInfo();
2563
+ const p1 = clip.start ? [clip.start.x, clip.start.y] : this._adjustedPoints[0];
2564
+ const p2 = clip.end ? [clip.end.x, clip.end.y] : this._adjustedPoints[1];
2565
+ const distance = this.pointToLineSegmentDistance(x, y, p1[0], p1[1], p2[0], p2[1]);
2566
+ return distance <= halfStroke;
2567
+ }
2568
+ /**
2569
+ * Tests if the line intersects with or is contained within a polygon.
2570
+ * Checks endpoint containment, polygon vertex proximity, curve sampling for Bezier curves,
2571
+ * and line-edge intersections for straight lines.
2572
+ * @param polygon - The polygon to test intersection with, defined by four corner points
2573
+ * @returns True if any part of the line intersects with the polygon
2574
+ */
2575
+ hitTestPolygon(polygon) {
2576
+ const halfStroke = this.strokeWidth / this.scale / 2;
2577
+ if (this._adjustedPoints === null) {
2578
+ this._adjustedPoints = this.computeAdjustedPoints();
2579
+ }
2580
+ const clip = this.getClipInfo();
2581
+ const polyPoints = [
2582
+ { x: polygon.bottomLeft.x, y: polygon.bottomLeft.y },
2583
+ { x: polygon.bottomRight.x, y: polygon.bottomRight.y },
2584
+ { x: polygon.topRight.x, y: polygon.topRight.y },
2585
+ { x: polygon.topLeft.x, y: polygon.topLeft.y },
2586
+ ];
2587
+ // Check if any endpoint is inside the polygon
2588
+ // Use clipped endpoints
2589
+ const p1 = clip.start ? [clip.start.x, clip.start.y] : this._adjustedPoints[0];
2590
+ const p2 = clip.end ? [clip.end.x, clip.end.y] : this._adjustedPoints[1];
2591
+ const endpoints = [p1, p2];
2592
+ for (const [px, py] of endpoints) {
2593
+ if (KritzelGeometryHelper.isPointInPolygon({ x: px, y: py }, polyPoints)) {
2594
+ return true;
2595
+ }
2596
+ }
2597
+ // Check if any polygon vertex is on the line/curve
2598
+ for (const pt of polyPoints) {
2599
+ if (this.hitTest(pt.x, pt.y)) {
2600
+ return true;
2601
+ }
2602
+ }
2603
+ // For curved lines, sample points along the curve and check if any are inside the polygon
2604
+ if (this.controlX !== undefined && this.controlY !== undefined) {
2605
+ const p0 = this._adjustedPoints[0];
2606
+ const p2 = this._adjustedPoints[1];
2607
+ const controlAdjusted = this.computeAdjustedControlPoint();
2608
+ const startT = clip.start?.t ?? 0;
2609
+ const endT = clip.end?.t ?? 1;
2610
+ const samples = 20;
2611
+ for (let i = 0; i <= samples; i++) {
2612
+ const fraction = i / samples;
2613
+ const t = startT + fraction * (endT - startT);
2614
+ const oneMinusT = 1 - t;
2615
+ const bx = oneMinusT * oneMinusT * p0[0] + 2 * oneMinusT * t * controlAdjusted[0] + t * t * p2[0];
2616
+ const by = oneMinusT * oneMinusT * p0[1] + 2 * oneMinusT * t * controlAdjusted[1] + t * t * p2[1];
2617
+ if (KritzelGeometryHelper.isPointInPolygon({ x: bx, y: by }, polyPoints)) {
2618
+ return true;
2619
+ }
2620
+ // Also check if curve point is within stroke distance of polygon edges
2621
+ for (let j = 0; j < polyPoints.length; j++) {
2622
+ const q1 = polyPoints[j];
2623
+ const q2 = polyPoints[(j + 1) % polyPoints.length];
2624
+ const d = this.pointToLineSegmentDistance(bx, by, q1.x, q1.y, q2.x, q2.y);
2625
+ if (d <= halfStroke) {
2626
+ return true;
2627
+ }
2628
+ }
2629
+ }
2630
+ return false;
2631
+ }
2632
+ // For straight lines, check if line intersects any polygon edge
2633
+ const checkP1 = { x: p1[0], y: p1[1] };
2634
+ const checkP2 = { x: p2[0], y: p2[1] };
2635
+ for (let j = 0; j < polyPoints.length; j++) {
2636
+ const q1 = polyPoints[j];
2637
+ const q2 = polyPoints[(j + 1) % polyPoints.length];
2638
+ if (KritzelGeometryHelper.intersectLines(checkP1, checkP2, q1, q2)) {
2639
+ return true;
2640
+ }
2641
+ // Check distance from polygon edges to line segment
2642
+ const d1 = this.pointToLineSegmentDistance(q1.x, q1.y, checkP1.x, checkP1.y, checkP2.x, checkP2.y);
2643
+ const d2 = this.pointToLineSegmentDistance(q2.x, q2.y, checkP1.x, checkP1.y, checkP2.x, checkP2.y);
2644
+ const d3 = this.pointToLineSegmentDistance(checkP1.x, checkP1.y, q1.x, q1.y, q2.x, q2.y);
2645
+ const d4 = this.pointToLineSegmentDistance(checkP2.x, checkP2.y, q1.x, q1.y, q2.x, q2.y);
2646
+ const minD = Math.min(d1, d2, d3, d4);
2647
+ if (minD <= halfStroke) {
2648
+ return true;
2649
+ }
2650
+ }
2651
+ return false;
2652
+ }
2653
+ /**
2654
+ * Updates the translation position of the line without recalculating dimensions.
2655
+ * Invalidates cached adjusted points and clip info.
2656
+ * @param x - New X translation in world coordinates
2657
+ * @param y - New Y translation in world coordinates
2658
+ */
2659
+ updatePosition(x, y) {
2660
+ this.translateX = x;
2661
+ this.translateY = y;
2662
+ this._adjustedPoints = null;
2663
+ this._clipInfo = null;
2664
+ this._core.store.objects.update(this);
2665
+ }
2666
+ /**
2667
+ * Updates a specific endpoint of the line (start or end).
2668
+ * Recalculates the bounding box dimensions and adjusts translation to maintain
2669
+ * the visual position after the origin shift, accounting for rotation.
2670
+ * @param handleType - Which endpoint to update: 'start' or 'end'
2671
+ * @param newX - New X coordinate in viewBox-local space (relative to x, y origin)
2672
+ * @param newY - New Y coordinate in viewBox-local space (relative to x, y origin)
2673
+ */
2674
+ updateEndpoint(handleType, newX, newY) {
2675
+ // Update the appropriate endpoint
2676
+ if (handleType === 'start') {
2677
+ this.startX = newX;
2678
+ this.startY = newY;
2679
+ }
2680
+ else {
2681
+ this.endX = newX;
2682
+ this.endY = newY;
2683
+ }
2684
+ const oldWidth = this.width;
2685
+ const oldHeight = this.height;
2686
+ // Recalculate the bounding box (viewBox dimensions)
2687
+ const { minX, minY, maxX, maxY } = this.calculateBoundingBox();
2688
+ // Calculate the change in origin (in local coordinates)
2689
+ const deltaX = minX - this.x;
2690
+ const deltaY = minY - this.y;
2691
+ const newWidth = maxX - minX;
2692
+ const newHeight = maxY - minY;
2693
+ const deltaWidth = newWidth - oldWidth;
2694
+ const deltaHeight = newHeight - oldHeight;
2695
+ const deltaCx = deltaWidth / 2;
2696
+ const deltaCy = deltaHeight / 2;
2697
+ // Update viewBox dimensions
2698
+ this.x = minX;
2699
+ this.y = minY;
2700
+ this.width = newWidth;
2701
+ this.height = newHeight;
2702
+ // Rotate the delta to world coordinates before applying to translate
2703
+ const cos = Math.cos(this.rotation);
2704
+ const sin = Math.sin(this.rotation);
2705
+ // We need to compensate for:
2706
+ // 1. The shift of the top-left corner (deltaX, deltaY)
2707
+ // 2. The shift of the center point (deltaCx, deltaCy) which affects rotation pivot
2708
+ // Formula: DeltaT = R(DeltaPos + DeltaCenter) - DeltaCenter
2709
+ const vx = deltaX + deltaCx;
2710
+ const vy = deltaY + deltaCy;
2711
+ const rotatedVx = vx * cos - vy * sin;
2712
+ const rotatedVy = vx * sin + vy * cos;
2713
+ const correctionX = rotatedVx - deltaCx;
2714
+ const correctionY = rotatedVy - deltaCy;
2715
+ // Adjust translateX/Y to compensate for the origin shift
2716
+ // so the line stays visually in the same position
2717
+ this.translateX += correctionX / this.scale;
2718
+ this.translateY += correctionY / this.scale;
2719
+ // Clear cached adjusted points
2720
+ this._adjustedPoints = null;
2721
+ this._clipInfo = null;
2722
+ this._core.store.objects.update(this);
2723
+ }
2724
+ /**
2725
+ * Updates the control point for a quadratic Bezier curve.
2726
+ * Recalculates bounding box and adjusts translation to maintain visual position.
2727
+ * Pass undefined for both parameters to convert the line back to a straight line.
2728
+ * @param newX - New X coordinate for the control point, or undefined to remove
2729
+ * @param newY - New Y coordinate for the control point, or undefined to remove
2730
+ */
2731
+ updateControlPoint(newX, newY) {
2732
+ this.controlX = newX;
2733
+ this.controlY = newY;
2734
+ const oldWidth = this.width;
2735
+ const oldHeight = this.height;
2736
+ // Recalculate the bounding box (viewBox dimensions)
2737
+ const { minX, minY, maxX, maxY } = this.calculateBoundingBox();
2738
+ // Calculate the change in origin (in local coordinates)
2739
+ const deltaX = minX - this.x;
2740
+ const deltaY = minY - this.y;
2741
+ const newWidth = maxX - minX;
2742
+ const newHeight = maxY - minY;
2743
+ const deltaWidth = newWidth - oldWidth;
2744
+ const deltaHeight = newHeight - oldHeight;
2745
+ const deltaCx = deltaWidth / 2;
2746
+ const deltaCy = deltaHeight / 2;
2747
+ // Update viewBox dimensions
2748
+ this.x = minX;
2749
+ this.y = minY;
2750
+ this.width = newWidth;
2751
+ this.height = newHeight;
2752
+ // Rotate the delta to world coordinates before applying to translate
2753
+ const cos = Math.cos(this.rotation);
2754
+ const sin = Math.sin(this.rotation);
2755
+ // We need to compensate for:
2756
+ // 1. The shift of the top-left corner (deltaX, deltaY)
2757
+ // 2. The shift of the center point (deltaCx, deltaCy) which affects rotation pivot
2758
+ // Formula: DeltaT = R(DeltaPos + DeltaCenter) - DeltaCenter
2759
+ const vx = deltaX + deltaCx;
2760
+ const vy = deltaY + deltaCy;
2761
+ const rotatedVx = vx * cos - vy * sin;
2762
+ const rotatedVy = vx * sin + vy * cos;
2763
+ const correctionX = rotatedVx - deltaCx;
2764
+ const correctionY = rotatedVy - deltaCy;
2765
+ // Adjust translateX/Y to compensate for the origin shift
2766
+ // so the line stays visually in the same position
2767
+ this.translateX += correctionX / this.scale;
2768
+ this.translateY += correctionY / this.scale;
2769
+ // Clear cached adjusted points
2770
+ this._adjustedPoints = null;
2771
+ this._clipInfo = null;
2772
+ this._core.store.objects.update(this);
2773
+ }
2774
+ /**
2775
+ * Computes the line endpoints in world coordinates, accounting for rotation,
2776
+ * scale, and translation transformations.
2777
+ * @returns Array of two points [[startX, startY], [endX, endY]] in world coordinates
2778
+ */
2779
+ computeAdjustedPoints() {
2780
+ const points = [
2781
+ [this.startX, this.startY],
2782
+ [this.endX, this.endY],
2783
+ ];
2784
+ const angle = this.rotation;
2785
+ const cos = Math.cos(angle);
2786
+ const sin = Math.sin(angle);
2787
+ const xs = points.map(p => p[0]);
2788
+ const ys = points.map(p => p[1]);
2789
+ const pivot = {
2790
+ x: (Math.min(...xs) + Math.max(...xs)) / 2,
2791
+ y: (Math.min(...ys) + Math.max(...ys)) / 2,
2792
+ };
2793
+ const { x: cx, y: cy } = pivot;
2794
+ const rotatedPoints = points.map(([x, y]) => {
2795
+ const dx = x - cx;
2796
+ const dy = y - cy;
2797
+ return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
2798
+ });
2799
+ return rotatedPoints.map(([px, py]) => [(px - this.x) / this.scale + this.translateX, (py - this.y) / this.scale + this.translateY]);
2800
+ }
2801
+ /**
2802
+ * Calculates the minimum distance from a point to a line segment.
2803
+ * Uses parametric projection to find the closest point on the segment.
2804
+ * @param x - X coordinate of the test point
2805
+ * @param y - Y coordinate of the test point
2806
+ * @param x1 - X coordinate of the segment start
2807
+ * @param y1 - Y coordinate of the segment start
2808
+ * @param x2 - X coordinate of the segment end
2809
+ * @param y2 - Y coordinate of the segment end
2810
+ * @returns The minimum distance from the point to the line segment
2811
+ */
2812
+ pointToLineSegmentDistance(x, y, x1, y1, x2, y2) {
2813
+ const A = x - x1;
2814
+ const B = y - y1;
2815
+ const C = x2 - x1;
2816
+ const D = y2 - y1;
2817
+ const dot = A * C + B * D;
2818
+ const len_sq = C * C + D * D;
2819
+ let param = -1;
2820
+ if (len_sq !== 0) {
2821
+ param = dot / len_sq;
2822
+ }
2823
+ let xx, yy;
2824
+ if (param < 0) {
2825
+ xx = x1;
2826
+ yy = y1;
2827
+ }
2828
+ else if (param > 1) {
2829
+ xx = x2;
2830
+ yy = y2;
2831
+ }
2832
+ else {
2833
+ xx = x1 + param * C;
2834
+ yy = y1 + param * D;
2835
+ }
2836
+ const dx = x - xx;
2837
+ const dy = y - yy;
2838
+ return Math.sqrt(dx * dx + dy * dy);
2839
+ }
2840
+ /**
2841
+ * Calculates the minimum distance from a point to a quadratic Bezier curve.
2842
+ * Uses sampling along the curve (20 samples) to approximate the closest point.
2843
+ * @param x - X coordinate of the test point in world coordinates
2844
+ * @param y - Y coordinate of the test point in world coordinates
2845
+ * @param startT - Start parameter for curve sampling (0-1), used for clipped curves
2846
+ * @param endT - End parameter for curve sampling (0-1), used for clipped curves
2847
+ * @returns The minimum distance from the point to the sampled curve
2848
+ */
2849
+ pointToBezierDistance(x, y, startT = 0, endT = 1) {
2850
+ if (this._adjustedPoints === null) {
2851
+ this._adjustedPoints = this.computeAdjustedPoints();
2852
+ }
2853
+ const p0 = this._adjustedPoints[0];
2854
+ const p2 = this._adjustedPoints[1];
2855
+ // Calculate the adjusted control point
2856
+ const controlAdjusted = this.computeAdjustedControlPoint();
2857
+ let minDistance = Infinity;
2858
+ const samples = 20; // Number of samples along the curve
2859
+ for (let i = 0; i <= samples; i++) {
2860
+ const fraction = i / samples;
2861
+ const t = startT + fraction * (endT - startT);
2862
+ const oneMinusT = 1 - t;
2863
+ // Quadratic Bezier: B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂
2864
+ const bx = oneMinusT * oneMinusT * p0[0] + 2 * oneMinusT * t * controlAdjusted[0] + t * t * p2[0];
2865
+ const by = oneMinusT * oneMinusT * p0[1] + 2 * oneMinusT * t * controlAdjusted[1] + t * t * p2[1];
2866
+ const dx = x - bx;
2867
+ const dy = y - by;
2868
+ const distance = Math.sqrt(dx * dx + dy * dy);
2869
+ if (distance < minDistance) {
2870
+ minDistance = distance;
2871
+ }
2872
+ }
2873
+ return minDistance;
2874
+ }
2875
+ /**
2876
+ * Retrieves or computes the clip information for anchored line endpoints.
2877
+ * Caches the result to avoid recalculation on subsequent calls.
2878
+ * @returns Object containing optional start and end clip points with world coordinates
2879
+ * and parametric t values for curve clipping
2880
+ */
2881
+ getClipInfo() {
2882
+ if (this._clipInfo)
2883
+ return this._clipInfo;
2884
+ const startAnchor = this.startAnchor;
2885
+ const endAnchor = this.endAnchor;
2886
+ const result = {};
2887
+ if (startAnchor) {
2888
+ const target = this._core.anchorManager.findAnchorTarget(this, 'start');
2889
+ if (target) {
2890
+ const clip = this._core.anchorManager.computeAnchorClipInfo(this, 'start', target);
2891
+ if (clip) {
2892
+ result.start = { x: clip.worldX, y: clip.worldY, t: clip.t };
2893
+ }
2894
+ }
2895
+ }
2896
+ if (endAnchor) {
2897
+ const target = this._core.anchorManager.findAnchorTarget(this, 'end');
2898
+ if (target) {
2899
+ const clip = this._core.anchorManager.computeAnchorClipInfo(this, 'end', target);
2900
+ if (clip) {
2901
+ result.end = { x: clip.worldX, y: clip.worldY, t: clip.t };
2902
+ }
2903
+ }
2904
+ }
2905
+ this._clipInfo = result;
2906
+ return result;
2907
+ }
2908
+ /**
2909
+ * Computes the adjusted control point in world coordinates.
2910
+ * Applies rotation around the bounding box center, then scale and translation transforms.
2911
+ * @returns Array [x, y] representing the control point in world coordinates,
2912
+ * or the midpoint of the line if no control point is defined
2913
+ */
2914
+ computeAdjustedControlPoint() {
2915
+ if (this.controlX === undefined || this.controlY === undefined) {
2916
+ // Return midpoint if no control point
2917
+ return [(this.startX + this.endX) / 2, (this.startY + this.endY) / 2];
2918
+ }
2919
+ const angle = this.rotation;
2920
+ const cos = Math.cos(angle);
2921
+ const sin = Math.sin(angle);
2922
+ // Calculate pivot (center of bounding box for endpoints)
2923
+ const xs = [this.startX, this.endX];
2924
+ const ys = [this.startY, this.endY];
2925
+ const pivot = {
2926
+ x: (Math.min(...xs) + Math.max(...xs)) / 2,
2927
+ y: (Math.min(...ys) + Math.max(...ys)) / 2,
2928
+ };
2929
+ const { x: cx, y: cy } = pivot;
2930
+ // Rotate control point around pivot
2931
+ const dx = this.controlX - cx;
2932
+ const dy = this.controlY - cy;
2933
+ const rotatedX = cx + dx * cos - dy * sin;
2934
+ const rotatedY = cy + dx * sin + dy * cos;
2935
+ // Transform to world coordinates
2936
+ return [(rotatedX - this.x) / this.scale + this.translateX, (rotatedY - this.y) / this.scale + this.translateY];
2937
+ }
2938
+ /**
2939
+ * Gets the bounding polygon of the line in world coordinates, accounting for rotation.
2940
+ * Used for selection bounds and intersection testing.
2941
+ * @returns A polygon with four corner points (topLeft, topRight, bottomRight, bottomLeft)
2942
+ * transformed to world coordinates with rotation applied
2943
+ */
2944
+ get rotatedPolygon() {
2945
+ const padding = this.padding;
2946
+ // Use the bounding box which accounts for curve extrema
2947
+ const { minX, minY, maxX, maxY } = KritzelLine.calculateBoundingBox(this.startX, this.startY, this.endX, this.endY, this.controlX, this.controlY, this.strokeWidth);
2948
+ // Convert to local coordinates (relative to this.x, this.y)
2949
+ const localMinX = minX - this.x + padding;
2950
+ const localMinY = minY - this.y + padding;
2951
+ const localMaxX = maxX - this.x + padding;
2952
+ const localMaxY = maxY - this.y + padding;
2953
+ const c1 = { x: localMinX, y: localMinY }; // top-left
2954
+ const c2 = { x: localMaxX, y: localMinY }; // top-right
2955
+ const c3 = { x: localMaxX, y: localMaxY }; // bottom-right
2956
+ const c4 = { x: localMinX, y: localMaxY }; // bottom-left
2957
+ const cx = this.totalWidth / 2;
2958
+ const cy = this.totalHeight / 2;
2959
+ const angle = this.rotation;
2960
+ const cos = Math.cos(angle);
2961
+ const sin = Math.sin(angle);
2962
+ const transform = (p) => {
2963
+ const rx = (p.x - cx) * cos - (p.y - cy) * sin + cx;
2964
+ const ry = (p.x - cx) * sin + (p.y - cy) * cos + cy;
2965
+ return {
2966
+ x: rx / this.scale + this.translateX,
2967
+ y: ry / this.scale + this.translateY,
2968
+ };
2969
+ };
2970
+ return {
2971
+ topLeft: transform(c1),
2972
+ topRight: transform(c2),
2973
+ bottomRight: transform(c3),
2974
+ bottomLeft: transform(c4),
2975
+ };
2976
+ }
2977
+ /**
2978
+ * Calculates the tight bounding box for a quadratic Bezier curve or straight line.
2979
+ * For curves, finds extrema points where the derivative equals zero to ensure
2980
+ * the bounding box encompasses the entire curve arc.
2981
+ * @param startX - X coordinate of the line start point
2982
+ * @param startY - Y coordinate of the line start point
2983
+ * @param endX - X coordinate of the line end point
2984
+ * @param endY - Y coordinate of the line end point
2985
+ * @param controlX - X coordinate of the control point (undefined for straight lines)
2986
+ * @param controlY - Y coordinate of the control point (undefined for straight lines)
2987
+ * @param strokeWidth - Width of the line stroke for padding calculation
2988
+ * @returns Object with minX, minY, maxX, maxY including stroke width padding
2989
+ */
2990
+ static calculateBoundingBox(startX, startY, endX, endY, controlX, controlY, strokeWidth) {
2991
+ let minX = Math.min(startX, endX);
2992
+ let minY = Math.min(startY, endY);
2993
+ let maxX = Math.max(startX, endX);
2994
+ let maxY = Math.max(startY, endY);
2995
+ if (controlX !== undefined && controlY !== undefined) {
2996
+ // For a quadratic Bezier: B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂
2997
+ // Derivative: B'(t) = 2(1-t)(P₁-P₀) + 2t(P₂-P₁) = 0
2998
+ // Solving for t: t = (P₀ - P₁) / (P₀ - 2P₁ + P₂)
2999
+ // Find extrema for X
3000
+ const aX = startX - 2 * controlX + endX;
3001
+ if (aX !== 0) {
3002
+ const tX = (startX - controlX) / aX;
3003
+ if (tX > 0 && tX < 1) {
3004
+ const extremaX = (1 - tX) * (1 - tX) * startX + 2 * (1 - tX) * tX * controlX + tX * tX * endX;
3005
+ minX = Math.min(minX, extremaX);
3006
+ maxX = Math.max(maxX, extremaX);
3007
+ }
3008
+ }
3009
+ // Find extrema for Y
3010
+ const aY = startY - 2 * controlY + endY;
3011
+ if (aY !== 0) {
3012
+ const tY = (startY - controlY) / aY;
3013
+ if (tY > 0 && tY < 1) {
3014
+ const extremaY = (1 - tY) * (1 - tY) * startY + 2 * (1 - tY) * tY * controlY + tY * tY * endY;
3015
+ minY = Math.min(minY, extremaY);
3016
+ maxY = Math.max(maxY, extremaY);
3017
+ }
3018
+ }
3019
+ }
3020
+ const halfStroke = strokeWidth / 2;
3021
+ return {
3022
+ minX: minX - halfStroke,
3023
+ minY: minY - halfStroke,
3024
+ maxX: maxX + halfStroke,
3025
+ maxY: maxY + halfStroke,
3026
+ };
3027
+ }
3028
+ /**
3029
+ * Calculates the bounding box for this line instance using the current endpoint,
3030
+ * control point, and stroke width values.
3031
+ * @returns Object with minX, minY, maxX, maxY bounds including stroke width padding
3032
+ */
3033
+ calculateBoundingBox() {
3034
+ return KritzelLine.calculateBoundingBox(this.startX, this.startY, this.endX, this.endY, this.controlX, this.controlY, this.strokeWidth);
3035
+ }
3036
+ /**
3037
+ * Serializes the line while excluding transient cache fields.
3038
+ * @returns A serializable line object without runtime caches
3039
+ */
3040
+ serialize() {
3041
+ const serialized = super.serialize();
3042
+ delete serialized._adjustedPoints;
3043
+ delete serialized._clipInfo;
3044
+ return serialized;
3045
+ }
3046
+ /**
3047
+ * Deserializes and resets transient caches so geometry is recomputed lazily.
3048
+ * @param object - The serialized line object
3049
+ * @returns This instance cast to type T
3050
+ */
3051
+ deserialize(object) {
3052
+ const result = super.deserialize(object);
3053
+ this._adjustedPoints = null;
3054
+ this._clipInfo = null;
3055
+ return result;
3056
+ }
3057
+ /**
3058
+ * Recalculates and updates the width, height, x, y, and translation properties
3059
+ * based on the current endpoint positions and stroke width.
3060
+ * Called during construction and after endpoint changes.
3061
+ */
3062
+ updateDimensions() {
3063
+ const { minX, minY, maxX, maxY } = this.calculateBoundingBox();
3064
+ this.width = maxX - minX;
3065
+ this.height = maxY - minY;
3066
+ this.x = minX;
3067
+ this.y = minY;
3068
+ this.translateX = (this.x + this.translateX) / this.scale;
3069
+ this.translateY = (this.y + this.translateY) / this.scale;
3070
+ }
3071
+ /**
3072
+ * Gets the unique SVG marker ID for the arrow head at the start of the line.
3073
+ * Used in SVG defs for marker-start attribute.
3074
+ * @returns A unique marker ID string in the format "arrow-start-{lineId}"
3075
+ */
3076
+ get startMarkerId() {
3077
+ return `arrow-start-${this.id}`;
3078
+ }
3079
+ /**
3080
+ * Gets the unique SVG marker ID for the arrow head at the end of the line.
3081
+ * Used in SVG defs for marker-end attribute.
3082
+ * @returns A unique marker ID string in the format "arrow-end-{lineId}"
3083
+ */
3084
+ get endMarkerId() {
3085
+ return `arrow-end-${this.id}`;
3086
+ }
3087
+ /**
3088
+ * Gets the arrow head size for the specified end of the line.
3089
+ * @param end - Which end to get the arrow size for: 'start' or 'end'
3090
+ * @returns The configured arrow size, or strokeWidth * 3 as default
3091
+ */
3092
+ getArrowSize(end) {
3093
+ const config = end === 'start' ? this.arrows?.start : this.arrows?.end;
3094
+ return config?.size ?? this.strokeWidth * 3;
3095
+ }
3096
+ /**
3097
+ * Gets the resolved fill color for the arrow head at the specified end.
3098
+ * Resolves theme-aware colors to the appropriate value for the current theme.
3099
+ * @param end - Which end to get the arrow fill for: 'start' or 'end'
3100
+ * @returns The resolved CSS color string for the arrow fill
3101
+ */
3102
+ getArrowFill(end) {
3103
+ const config = end === 'start' ? this.arrows?.start : this.arrows?.end;
3104
+ const color = config?.fill ?? this.stroke;
3105
+ return KritzelColorHelper.resolveThemeColor(color, this._core?.themeManager?.currentTheme);
3106
+ }
3107
+ /**
3108
+ * Generates SVG path data for an arrow head based on the given style.
3109
+ * All paths are designed to fit in a 10x10 viewBox with the tip at (10, 5).
3110
+ * @param style - The arrow head style: 'triangle' (filled), 'open' (outline),
3111
+ * 'diamond' (rhombus), or 'circle'. Defaults to 'triangle'.
3112
+ * @returns SVG path data string for use in a marker element
3113
+ */
3114
+ getArrowPath(style = 'triangle') {
3115
+ switch (style) {
3116
+ case 'triangle':
3117
+ return 'M 0 0 L 10 5 L 0 10 z';
3118
+ case 'open':
3119
+ return 'M 0 0 L 10 5 L 0 10';
3120
+ case 'diamond':
3121
+ return 'M 0 5 L 5 0 L 10 5 L 5 10 z';
3122
+ case 'circle':
3123
+ return 'M 5,0 A 5,5 0 1,1 5,10 A 5,5 0 1,1 5,0';
3124
+ default:
3125
+ return 'M 0 0 L 10 5 L 0 10 z';
3126
+ }
3127
+ }
3128
+ /**
3129
+ * Checks if an arrow head is enabled at the start of the line.
3130
+ * @returns True if the start arrow configuration exists and is enabled
3131
+ */
3132
+ get hasStartArrow() {
3133
+ return this.arrows?.start?.enabled === true;
3134
+ }
3135
+ /**
3136
+ * Checks if an arrow head is enabled at the end of the line.
3137
+ * @returns True if the end arrow configuration exists and is enabled
3138
+ */
3139
+ get hasEndArrow() {
3140
+ return this.arrows?.end?.enabled === true;
3141
+ }
3142
+ }
3143
+
3144
+ const kritzelAwarenessCursorsCss = () => `:host{display:block;position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:1}.awareness-cursor{position:absolute;top:0;left:0;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, opacity 300ms ease;will-change:transform}.awareness-cursor.stale{opacity:0}.awareness-cursor.tracking-object{transition-duration:0ms}.cursor-arrow{filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))}.cursor-label{position:absolute;left:16px;top:16px;white-space:nowrap;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:var(--kritzel-awareness-cursor-label-font-size, 12px);color:var(--kritzel-awareness-cursor-label-text-color, #ffffff);padding:2px 8px;border-radius:4px;line-height:1.4;font-weight:500;pointer-events:none;user-select:none}.edge-indicator{position:absolute;top:-12px;left:-12px;width:24px;height:24px;display:flex;align-items:center;justify-content:center;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, opacity 300ms ease;will-change:transform;pointer-events:auto;user-select:none;cursor:pointer}.edge-indicator.stale{opacity:0}.edge-indicator.tracking-object{transition-duration:0ms}.edge-arrow{position:absolute;filter:drop-shadow(0 1px 3px rgba(0, 0, 0, 0.3));transition:opacity 300ms ease}.edge-arrow.stale{opacity:0}.edge-label{position:absolute;white-space:nowrap;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:var(--kritzel-awareness-cursor-label-font-size, 12px);color:var(--kritzel-awareness-cursor-label-text-color, #ffffff);padding:2px 8px;border-radius:4px;line-height:1.4;font-weight:500;pointer-events:none;opacity:0;transform-origin:center;transition:opacity 150ms ease}.edge-indicator:hover .edge-label{opacity:1}.remote-selection-box{position:absolute;top:0;left:0;border-width:2px;border-style:solid;pointer-events:none;will-change:transform, width, height;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, width var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, height var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out}`;
3145
+
3146
+ const STALE_THRESHOLD_MS = 10_000;
3147
+ const REMOVE_THRESHOLD_MS = 30_000;
3148
+ const CLEANUP_INTERVAL_MS = 3_000;
3149
+ const KritzelAwarenessCursors = /*@__PURE__*/ proxyCustomElement(class KritzelAwarenessCursors extends H {
3150
+ constructor(registerHost) {
3151
+ super();
3152
+ if (registerHost !== false) {
3153
+ this.__registerHost();
3154
+ }
3155
+ this.__attachShadow();
3156
+ }
3157
+ core;
3158
+ showEdgeIndicators = true;
3159
+ edgeIndicatorPadding = 8;
3160
+ remoteCursors = new Map();
3161
+ objectVersion = 0;
3162
+ cleanupIntervalId;
3163
+ objectChangeRafId = null;
3164
+ removeAwarenessChangeListener;
3165
+ removeObjectsChangeListener;
3166
+ componentDidLoad() {
3167
+ this.removeAwarenessChangeListener = this.core.store.objects?.onAwarenessChange(states => {
3168
+ this.handleAwarenessChange(states);
3169
+ });
3170
+ this.removeObjectsChangeListener = this.core.store.objects?.onObjectsChange(() => {
3171
+ this.handleRemoteObjectChange();
3172
+ });
3173
+ this.cleanupIntervalId = setInterval(() => {
3174
+ this.cleanupStaleCursors();
3175
+ }, CLEANUP_INTERVAL_MS);
3176
+ if (this.cleanupIntervalId && typeof this.cleanupIntervalId.unref === 'function') {
3177
+ this.cleanupIntervalId.unref();
3178
+ }
3179
+ }
3180
+ disconnectedCallback() {
3181
+ if (this.removeAwarenessChangeListener) {
3182
+ this.removeAwarenessChangeListener();
3183
+ this.removeAwarenessChangeListener = undefined;
3184
+ }
3185
+ if (this.removeObjectsChangeListener) {
3186
+ this.removeObjectsChangeListener();
3187
+ this.removeObjectsChangeListener = undefined;
3188
+ }
3189
+ if (this.cleanupIntervalId && typeof globalThis.clearInterval === 'function') {
3190
+ globalThis.clearInterval(this.cleanupIntervalId);
3191
+ this.cleanupIntervalId = undefined;
3192
+ }
3193
+ if (this.objectChangeRafId !== null && typeof globalThis.cancelAnimationFrame === 'function') {
3194
+ globalThis.cancelAnimationFrame(this.objectChangeRafId);
3195
+ this.objectChangeRafId = null;
3196
+ }
3197
+ }
3198
+ handleAwarenessChange(states) {
3199
+ const localClientId = this.core.store.objects?.localClientId;
3200
+ const now = Date.now();
3201
+ const updated = new Map(this.remoteCursors);
3202
+ // Track which clientIds are still present
3203
+ const activeClientIds = new Set();
3204
+ states.forEach((state, clientId) => {
3205
+ if (clientId === localClientId)
3206
+ return;
3207
+ if (!state.user)
3208
+ return;
3209
+ activeClientIds.add(clientId);
3210
+ const user = state.user;
3211
+ const cursor = state.cursor;
3212
+ const activeObjectId = state.activeObjectId || null;
3213
+ const selectionBox = state.selectionBox || null;
3214
+ const existing = updated.get(clientId);
3215
+ const cursorMoved = !existing ||
3216
+ !existing.cursor !== !cursor ||
3217
+ (cursor && existing.cursor && (cursor.x !== existing.cursor.x || cursor.y !== existing.cursor.y));
3218
+ updated.set(clientId, {
3219
+ clientId,
3220
+ user,
3221
+ cursor,
3222
+ activeObjectId,
3223
+ selectionBox,
3224
+ lastUpdated: now,
3225
+ lastCursorMove: cursorMoved ? now : (existing?.lastCursorMove ?? now),
3226
+ });
3227
+ });
3228
+ // Remove cursors for disconnected clients
3229
+ for (const clientId of updated.keys()) {
3230
+ if (!activeClientIds.has(clientId)) {
3231
+ updated.delete(clientId);
3232
+ }
3233
+ }
3234
+ this.remoteCursors = updated;
3235
+ }
3236
+ cleanupStaleCursors() {
3237
+ const now = Date.now();
3238
+ let changed = false;
3239
+ const updated = new Map(this.remoteCursors);
3240
+ for (const [clientId, cursor] of updated) {
3241
+ if (now - cursor.lastUpdated > REMOVE_THRESHOLD_MS) {
3242
+ updated.delete(clientId);
3243
+ changed = true;
3244
+ }
3245
+ else if (!changed && now - cursor.lastCursorMove > STALE_THRESHOLD_MS) {
3246
+ changed = true;
3247
+ }
3248
+ }
3249
+ if (changed) {
3250
+ this.remoteCursors = updated;
3251
+ }
3252
+ }
3253
+ isStale(cursor) {
3254
+ return Date.now() - cursor.lastCursorMove > STALE_THRESHOLD_MS;
3255
+ }
3256
+ hasActiveDrawingCursors() {
3257
+ for (const cursor of this.remoteCursors.values()) {
3258
+ if (cursor.activeObjectId)
3259
+ return true;
3260
+ }
3261
+ return false;
3262
+ }
3263
+ handleRemoteObjectChange() {
3264
+ if (!this.hasActiveDrawingCursors())
3265
+ return;
3266
+ // Debounce via rAF to batch multiple rapid Yjs updates into a single re-render
3267
+ if (this.objectChangeRafId !== null)
3268
+ return;
3269
+ this.objectChangeRafId = requestAnimationFrame(() => {
3270
+ this.objectChangeRafId = null;
3271
+ this.objectVersion++;
3272
+ });
3273
+ }
3274
+ getActiveObjectTip(objectId) {
3275
+ const obj = this.core.store.objects?.findById(objectId);
3276
+ if (!obj)
3277
+ return null;
3278
+ if (obj instanceof KritzelPath && !obj.isCompleted) {
3279
+ const lastPoint = obj.points[obj.points.length - 1];
3280
+ if (!lastPoint)
3281
+ return null;
3282
+ return {
3283
+ x: (lastPoint[0] - obj.x) / obj.scale + obj.translateX,
3284
+ y: (lastPoint[1] - obj.y) / obj.scale + obj.translateY,
3285
+ };
3286
+ }
3287
+ if (obj instanceof KritzelLine && !obj.isCompleted) {
3288
+ return {
3289
+ x: (obj.endX - obj.x) / obj.scale + obj.translateX,
3290
+ y: (obj.endY - obj.y) / obj.scale + obj.translateY,
3291
+ };
3292
+ }
3293
+ // Shapes normalize their bounding box (x/y always top-left, width/height
3294
+ // always positive), so we can't determine which corner the user is actively
3295
+ // dragging. Fall back to the throttled awareness cursor position instead.
3296
+ return null;
3297
+ }
3298
+ worldToScreen(worldX, worldY) {
3299
+ const { scale, translateX, translateY } = this.core.store.state;
3300
+ return {
3301
+ x: worldX * scale + translateX,
3302
+ y: worldY * scale + translateY,
3303
+ };
3304
+ }
3305
+ isInViewport(screenX, screenY) {
3306
+ const { viewportWidth, viewportHeight } = this.core.store.state;
3307
+ return screenX >= 0 && screenX <= viewportWidth && screenY >= 0 && screenY <= viewportHeight;
3308
+ }
3309
+ clampToEdge(screenX, screenY) {
3310
+ const { viewportWidth, viewportHeight } = this.core.store.state;
3311
+ const padding = this.edgeIndicatorPadding; // 8px
3312
+ // Clamp symmetrically
3313
+ const clampedX = Math.max(padding, Math.min(viewportWidth - padding, screenX));
3314
+ const clampedY = Math.max(padding, Math.min(viewportHeight - padding, screenY));
3315
+ // Determine nearest edge to position avatar inside
3316
+ const distLeft = clampedX - padding;
3317
+ const distRight = (viewportWidth - padding) - clampedX;
3318
+ const distTop = clampedY - padding;
3319
+ const distBottom = (viewportHeight - padding) - clampedY;
3320
+ let edge = 'top';
3321
+ const minDist = Math.min(distLeft, distRight, distTop, distBottom);
3322
+ if (minDist === distLeft)
3323
+ edge = 'left';
3324
+ else if (minDist === distRight)
3325
+ edge = 'right';
3326
+ else if (minDist === distTop)
3327
+ edge = 'top';
3328
+ else
3329
+ edge = 'bottom';
3330
+ const angle = Math.atan2(screenY - clampedY, screenX - clampedX);
3331
+ return { x: clampedX, y: clampedY, angle, edge };
3332
+ }
3333
+ getUserDisplayName(user) {
3334
+ if (user.displayName)
3335
+ return user.displayName;
3336
+ if (user.firstName || user.lastName) {
3337
+ return [user.firstName, user.lastName].filter(Boolean).join(' ');
3338
+ }
3339
+ return 'Unknown';
3340
+ }
3341
+ getInitials(name) {
3342
+ const parts = name.trim().split(/\s+/);
3343
+ if (parts.length === 0 || (parts.length === 1 && parts[0].length <= 1)) {
3344
+ return name;
3345
+ }
3346
+ if (parts.length === 1) {
3347
+ return parts[0][0].toUpperCase();
3348
+ }
3349
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
3350
+ }
3351
+ render() {
3352
+ const cursors = Array.from(this.remoteCursors.values());
3353
+ return (h(Host, { key: 'cf00004efc0c60e81bd6b2f19ecb7ca066396980' }, cursors.map(remoteCursor => {
3354
+ if (!remoteCursor.cursor)
3355
+ return null;
3356
+ // When a remote user is actively drawing, derive cursor position from
3357
+ // the object's latest coordinates (synced via Yjs) instead of the
3358
+ // throttled awareness cursor position
3359
+ let screen;
3360
+ let trackingObject = false;
3361
+ if (remoteCursor.activeObjectId) {
3362
+ const tip = this.getActiveObjectTip(remoteCursor.activeObjectId);
3363
+ if (tip) {
3364
+ trackingObject = true;
3365
+ screen = this.worldToScreen(tip.x, tip.y);
3366
+ }
3367
+ else {
3368
+ screen = this.worldToScreen(remoteCursor.cursor.x, remoteCursor.cursor.y);
3369
+ }
3370
+ }
3371
+ else {
3372
+ screen = this.worldToScreen(remoteCursor.cursor.x, remoteCursor.cursor.y);
3373
+ }
3374
+ const inViewport = this.isInViewport(screen.x, screen.y);
3375
+ const stale = this.isStale(remoteCursor);
3376
+ const color = remoteCursor.user.color || '#6B7280';
3377
+ if (inViewport) {
3378
+ return this.renderCursor(remoteCursor, screen.x, screen.y, color, stale, trackingObject);
3379
+ }
3380
+ if (this.showEdgeIndicators) {
3381
+ return this.renderEdgeIndicator(remoteCursor, screen.x, screen.y, color, stale, trackingObject);
3382
+ }
3383
+ return null;
3384
+ }), cursors.map(remoteCursor => {
3385
+ if (!remoteCursor.selectionBox)
3386
+ return null;
3387
+ const color = remoteCursor.user.color || '#6B7280';
3388
+ const box = remoteCursor.selectionBox;
3389
+ const topLeft = this.worldToScreen(box.x, box.y);
3390
+ const { scale } = this.core.store.state;
3391
+ const screenWidth = box.width * scale;
3392
+ const screenHeight = box.height * scale;
3393
+ return (h("div", { key: `selection-box-${remoteCursor.clientId}`, class: "remote-selection-box", style: {
3394
+ transform: `translate(${topLeft.x}px, ${topLeft.y}px)`,
3395
+ width: `${screenWidth}px`,
3396
+ height: `${screenHeight}px`,
3397
+ backgroundColor: `color-mix(in srgb, ${color} 20%, transparent)`,
3398
+ borderColor: `color-mix(in srgb, ${color} 50%, transparent)`,
3399
+ } }));
3400
+ })));
3401
+ }
3402
+ renderCursor(cursor, screenX, screenY, color, stale, trackingObject) {
3403
+ return (h("div", { key: `cursor-${cursor.clientId}`, class: { 'awareness-cursor': true, stale, 'tracking-object': trackingObject }, style: { transform: `translate(${screenX}px, ${screenY}px)` } }, h("svg", { class: "cursor-arrow", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, h("path", { d: "M5 3L19 12L12 13L9 20L5 3Z", fill: color, stroke: "#ffffff", "stroke-width": "1.5", "stroke-linejoin": "round" })), h("span", { class: "cursor-label", style: { backgroundColor: color } }, this.getUserDisplayName(cursor.user))));
3404
+ }
3405
+ renderEdgeIndicator(cursor, screenX, screenY, color, stale, trackingObject) {
3406
+ const clamped = this.clampToEdge(screenX, screenY);
3407
+ const arrowDeg = (clamped.angle * 180) / Math.PI + 90;
3408
+ // Position the username label opposite to the edge
3409
+ const labelOffset = 20;
3410
+ let labelX = 0;
3411
+ let labelY = 0;
3412
+ // Default transform origin is somewhat arbitrary without explicit layout
3413
+ // We'll calculate simple translations relative to center point
3414
+ if (clamped.edge === 'left') {
3415
+ labelX = labelOffset;
3416
+ }
3417
+ else if (clamped.edge === 'right') {
3418
+ labelX = -labelOffset;
3419
+ }
3420
+ else if (clamped.edge === 'top') {
3421
+ labelY = labelOffset;
3422
+ }
3423
+ else if (clamped.edge === 'bottom') {
3424
+ labelY = -labelOffset;
3425
+ }
3426
+ const displayName = this.getUserDisplayName(cursor.user);
3427
+ return (h("div", { key: `edge-${cursor.clientId}`, class: { 'edge-indicator': true, stale, 'tracking-object': trackingObject }, style: {
3428
+ transform: `translate(${clamped.x}px, ${clamped.y}px)`,
3429
+ } }, h("svg", { class: { 'edge-arrow': true, stale }, width: "16", height: "16", viewBox: "0 0 16 16", style: { transform: `rotate(${arrowDeg}deg)` } }, h("path", { d: "M8 1L14 13H2L8 1Z", fill: color, stroke: "#ffffff", "stroke-width": "1.5", "stroke-linejoin": "round" })), h("span", { class: "edge-label", style: {
3430
+ backgroundColor: color,
3431
+ transform: `translate(${labelX}px, ${labelY}px)`,
3432
+ } }, this.getInitials(displayName))));
3433
+ }
3434
+ static get style() { return kritzelAwarenessCursorsCss(); }
3435
+ }, [513, "kritzel-awareness-cursors", {
3436
+ "core": [16],
3437
+ "showEdgeIndicators": [4, "show-edge-indicators"],
3438
+ "edgeIndicatorPadding": [2, "edge-indicator-padding"],
3439
+ "remoteCursors": [32],
3440
+ "objectVersion": [32]
3441
+ }]);
3442
+ function defineCustomElement() {
3443
+ if (typeof customElements === "undefined") {
3444
+ return;
3445
+ }
3446
+ const components = ["kritzel-awareness-cursors"];
3447
+ components.forEach(tagName => { switch (tagName) {
3448
+ case "kritzel-awareness-cursors":
3449
+ if (!customElements.get(transformTag(tagName))) {
3450
+ customElements.define(transformTag(tagName), KritzelAwarenessCursors);
3451
+ }
3452
+ break;
3453
+ } });
3454
+ }
3455
+
3456
+ export { DEFAULT_COLOR_PALETTE as D, KritzelBaseObject as K, ObjectHelper as O, ThemeHelper as T, KritzelPath as a, KritzelLine as b, KritzelColorHelper as c, KritzelThemeManager as d, darkTheme as e, KritzelAwarenessCursors as f, defineCustomElement as g, KritzelMathHelper as h, KritzelGeometryHelper as i, lightTheme as l, resolveColor as r };