@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,1476 @@
1
+ import { KritzelLine } from "../objects/line.class";
2
+ import { KritzelSelectionBox } from "../objects/selection-box.class";
3
+ import { KritzelSelectionGroup } from "../objects/selection-group.class";
4
+ import { KritzelGeometryHelper } from "../../helpers/geometry.helper";
5
+ import { KritzelClassHelper } from "../../helpers/class.helper";
6
+ /**
7
+ * Manages anchor connections between line endpoints and other objects.
8
+ * Maintains a runtime index for efficient reverse lookups and handles
9
+ * snap detection during line endpoint dragging.
10
+ */
11
+ export class KritzelAnchorManager {
12
+ _core;
13
+ /**
14
+ * Runtime index mapping objectId to the lines anchored to it.
15
+ * This is derived from the anchor properties on lines and rebuilt as needed.
16
+ */
17
+ _anchorIndex = new Map();
18
+ /**
19
+ * Creates a new KritzelAnchorManager instance.
20
+ * @param core - The KritzelCore instance that provides access to the store and other managers.
21
+ */
22
+ constructor(core) {
23
+ this._core = core;
24
+ }
25
+ // ============================================
26
+ // Anchor CRUD Operations
27
+ // ============================================
28
+ /**
29
+ * Sets an anchor from a line endpoint to a target object's center.
30
+ * Updates both the line's anchor property and the internal index.
31
+ * Prevents anchoring both endpoints of the same line to the same object.
32
+ *
33
+ * @param lineId - The unique identifier of the line to anchor.
34
+ * @param endpoint - Which endpoint of the line to anchor ('start' or 'end').
35
+ * @param targetObjectId - The unique identifier of the object to anchor to.
36
+ */
37
+ setAnchor(lineId, endpoint, targetObjectId) {
38
+ const line = this.getLineById(lineId);
39
+ if (!line) {
40
+ return;
41
+ }
42
+ // Prevent anchoring both endpoints to the same object
43
+ if (endpoint === 'start' && line.endAnchor?.objectId === targetObjectId) {
44
+ return;
45
+ }
46
+ if (endpoint === 'end' && line.startAnchor?.objectId === targetObjectId) {
47
+ return;
48
+ }
49
+ // Remove existing anchor if any
50
+ this.removeAnchor(lineId, endpoint);
51
+ // Set the anchor on the line
52
+ const anchor = { objectId: targetObjectId };
53
+ if (endpoint === 'start') {
54
+ line.startAnchor = anchor;
55
+ }
56
+ else {
57
+ line.endAnchor = anchor;
58
+ }
59
+ // Update the index
60
+ this.addToIndex(targetObjectId, lineId, endpoint);
61
+ // Snap the endpoint to the target's center
62
+ this.snapEndpointToObject(line, endpoint, targetObjectId);
63
+ // Persist the change
64
+ this._core.store.objects.update(line);
65
+ }
66
+ /**
67
+ * Removes an anchor from a line endpoint.
68
+ * Updates both the line's anchor property and the internal index.
69
+ *
70
+ * @param lineId - The unique identifier of the line.
71
+ * @param endpoint - Which endpoint's anchor to remove ('start' or 'end').
72
+ */
73
+ removeAnchor(lineId, endpoint) {
74
+ const line = this.getLineById(lineId);
75
+ if (!line) {
76
+ return;
77
+ }
78
+ const anchor = endpoint === 'start' ? line.startAnchor : line.endAnchor;
79
+ if (!anchor) {
80
+ return;
81
+ }
82
+ // Remove from index
83
+ this.removeFromIndex(anchor.objectId, lineId, endpoint);
84
+ // Clear the anchor on the line
85
+ if (endpoint === 'start') {
86
+ line.startAnchor = undefined;
87
+ }
88
+ else {
89
+ line.endAnchor = undefined;
90
+ }
91
+ }
92
+ /**
93
+ * Gets the anchor for a specific line endpoint.
94
+ *
95
+ * @param lineId - The unique identifier of the line.
96
+ * @param endpoint - Which endpoint to get the anchor for ('start' or 'end').
97
+ * @returns The LineAnchor object if an anchor exists, null otherwise.
98
+ */
99
+ getAnchor(lineId, endpoint) {
100
+ const line = this.getLineById(lineId);
101
+ if (!line) {
102
+ return null;
103
+ }
104
+ const anchor = endpoint === 'start' ? line.startAnchor : line.endAnchor;
105
+ return anchor ?? null;
106
+ }
107
+ // ============================================
108
+ // Query Operations
109
+ // ============================================
110
+ /**
111
+ * Gets all lines that have an endpoint anchored to the specified object.
112
+ * Uses the internal anchor index for efficient reverse lookup.
113
+ *
114
+ * @param objectId - The unique identifier of the object to query.
115
+ * @returns An array of AnchorIndexEntry objects containing lineId and endpoint information.
116
+ */
117
+ getLinesAnchoredTo(objectId) {
118
+ const entries = this._anchorIndex.get(objectId);
119
+ return entries ? Array.from(entries) : [];
120
+ }
121
+ // ============================================
122
+ // Position Updates
123
+ // ============================================
124
+ /**
125
+ * Updates all line endpoints that are anchored to the specified object.
126
+ * Should be called when an object moves to keep anchored lines in sync.
127
+ *
128
+ * @param objectId - The unique identifier of the object that has moved.
129
+ */
130
+ updateAnchorsForObject(objectId) {
131
+ const entries = this.getLinesAnchoredTo(objectId);
132
+ if (entries.length === 0) {
133
+ return;
134
+ }
135
+ const targetObject = this.getObjectById(objectId);
136
+ if (!targetObject) {
137
+ return;
138
+ }
139
+ for (const entry of entries) {
140
+ const line = this.getLineById(entry.lineId);
141
+ if (!line) {
142
+ continue;
143
+ }
144
+ this.snapEndpointToObject(line, entry.endpoint, objectId);
145
+ }
146
+ const selectionGroup = this._core.store.selectionGroup;
147
+ if (!selectionGroup || !this._core.store.state.isDragging) {
148
+ return;
149
+ }
150
+ if (selectionGroup.hasExternalAnchoredLineConnections()) {
151
+ // Keep the local drag overlay aligned with reshaped anchored lines without spamming Yjs.
152
+ selectionGroup.refreshObjectDimensions(undefined, true);
153
+ }
154
+ }
155
+ /**
156
+ * Snaps a line endpoint to an object's center position.
157
+ * Converts world coordinates to the line's local coordinate system before updating.
158
+ *
159
+ * @param line - The KritzelLine object to update.
160
+ * @param endpoint - Which endpoint to snap ('start' or 'end').
161
+ * @param targetObjectId - The unique identifier of the target object to snap to.
162
+ */
163
+ snapEndpointToObject(line, endpoint, targetObjectId) {
164
+ const targetObject = this.getObjectById(targetObjectId);
165
+ if (!targetObject) {
166
+ return;
167
+ }
168
+ // Get target center in world coordinates
169
+ const targetCenterX = targetObject.centerX;
170
+ const targetCenterY = targetObject.centerY;
171
+ // Convert world coordinates to line's local coordinate system
172
+ const localCoords = this.worldToLineLocal(line, targetCenterX, targetCenterY);
173
+ // Update the endpoint
174
+ line.updateEndpoint(endpoint, localCoords.x, localCoords.y);
175
+ }
176
+ /**
177
+ * Converts world coordinates to a line's local coordinate system.
178
+ * Applies inverse rotation and scale transformations to translate
179
+ * from global canvas coordinates to the line's internal SVG coordinates.
180
+ *
181
+ * @param line - The KritzelLine object providing the transformation context.
182
+ * @param worldX - The X coordinate in world/canvas space.
183
+ * @param worldY - The Y coordinate in world/canvas space.
184
+ * @returns An object containing the local x and y coordinates.
185
+ */
186
+ worldToLineLocal(line, worldX, worldY) {
187
+ // Get line center in world coordinates
188
+ const cx = line.centerX;
189
+ const cy = line.centerY;
190
+ // Translate to center
191
+ const dx = worldX - cx;
192
+ const dy = worldY - cy;
193
+ // Apply inverse rotation
194
+ const cos = Math.cos(-line.rotation);
195
+ const sin = Math.sin(-line.rotation);
196
+ const rotatedX = dx * cos - dy * sin;
197
+ const rotatedY = dx * sin + dy * cos;
198
+ // Calculate local coordinates relative to the unrotated bounding box top-left
199
+ const relativeX = rotatedX + (line.totalWidth / 2) / line.scale;
200
+ const relativeY = rotatedY + (line.totalHeight / 2) / line.scale;
201
+ // Convert to internal coordinates
202
+ const localX = relativeX * line.scale + line.x;
203
+ const localY = relativeY * line.scale + line.y;
204
+ return { x: localX, y: localY };
205
+ }
206
+ // ============================================
207
+ // Snap Detection
208
+ // ============================================
209
+ /**
210
+ * Finds an object that can be snapped to when the cursor is sufficiently close to its center.
211
+ * Snap is only triggered when the cursor is inside the object's bounds AND within 1/5 of the
212
+ * shorter side from the center, so snapping activates only as the user approaches the center
213
+ * and releases quickly as they move away.
214
+ * Returns null if no suitable snap target is found.
215
+ *
216
+ * @param worldX - X coordinate in world space
217
+ * @param worldY - Y coordinate in world space
218
+ * @param excludeLineId - ID of the line being edited (to exclude from snap targets)
219
+ * @param otherEndpointAnchorId - ID of object anchored to the other endpoint (to prevent same-object anchoring)
220
+ * @param pointerType - The pointer type driving the drag interaction.
221
+ */
222
+ findSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, pointerType = 'mouse') {
223
+ return this.findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, object => {
224
+ const snapRadius = this.getSnapRadius(object, pointerType);
225
+ const dx = worldX - object.centerX;
226
+ const dy = worldY - object.centerY;
227
+ const distanceToCenter = Math.sqrt(dx * dx + dy * dy);
228
+ return distanceToCenter <= snapRadius;
229
+ });
230
+ }
231
+ /**
232
+ * Finds the topmost anchorable object currently containing the dragged endpoint.
233
+ * Used to drive preview UI before an actual snap engages.
234
+ */
235
+ findSnapPreviewTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId) {
236
+ return this.findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, () => true);
237
+ }
238
+ findBestSnapTarget(worldX, worldY, excludeLineId, otherEndpointAnchorId, predicate) {
239
+ let bestTarget = null;
240
+ let highestZIndex = -Infinity;
241
+ const objects = this._core.store.allNonSelectionObjects;
242
+ for (const object of objects) {
243
+ // Skip the line being edited
244
+ if (object.id === excludeLineId) {
245
+ continue;
246
+ }
247
+ // Skip if this is the object anchored to the other endpoint
248
+ if (otherEndpointAnchorId && object.id === otherEndpointAnchorId) {
249
+ continue;
250
+ }
251
+ // Skip non-anchorable objects
252
+ if (!this.isAnchorable(object)) {
253
+ continue;
254
+ }
255
+ // Check if point is inside the object's rotated polygon
256
+ const polygon = object.rotatedPolygon;
257
+ const points = [polygon.topLeft, polygon.topRight, polygon.bottomRight, polygon.bottomLeft];
258
+ if (!KritzelGeometryHelper.isPointInPolygon({ x: worldX, y: worldY }, points)) {
259
+ continue;
260
+ }
261
+ if (!predicate(object)) {
262
+ continue;
263
+ }
264
+ if (object.zIndex > highestZIndex) {
265
+ highestZIndex = object.zIndex;
266
+ bestTarget = {
267
+ objectId: object.id,
268
+ centerX: object.centerX,
269
+ centerY: object.centerY,
270
+ };
271
+ }
272
+ }
273
+ return bestTarget;
274
+ }
275
+ getSnapRadius(object, pointerType) {
276
+ const baseRadius = Math.min(object.width, object.height) / 5;
277
+ const safeScale = this.getSafeScale(this._core.store.state.scale);
278
+ if (pointerType === 'touch' || pointerType === 'pen') {
279
+ return Math.max(baseRadius * 1.75, 24 / safeScale);
280
+ }
281
+ return Math.max(baseRadius, 12 / safeScale);
282
+ }
283
+ /**
284
+ * Sets the current snap candidate for visual feedback during line endpoint dragging.
285
+ * Updates the store state and triggers a rerender to show the snap indicator.
286
+ *
287
+ * @param candidate - The SnapCandidate object containing snap target info, or null to clear.
288
+ */
289
+ setSnapCandidate(candidate) {
290
+ this._core.store.state.snapCandidate = candidate;
291
+ this._core.rerender();
292
+ }
293
+ /**
294
+ * Sets the current snap preview candidate for early visual feedback during line dragging.
295
+ * Preview state must remain separate from active snap state so line clipping and anchor
296
+ * commit semantics only react to actual snaps.
297
+ *
298
+ * @param candidate - The preview candidate object, or null to clear.
299
+ */
300
+ setSnapPreviewCandidate(candidate) {
301
+ this._core.store.state.snapPreviewCandidate = candidate;
302
+ this._core.rerender();
303
+ }
304
+ /**
305
+ * Gets the current snap candidate from the store state.
306
+ *
307
+ * @returns The current SnapCandidate object if one exists, null otherwise.
308
+ */
309
+ getSnapCandidate() {
310
+ return this._core.store.state.snapCandidate ?? null;
311
+ }
312
+ /**
313
+ * Gets the current snap preview candidate from the store state.
314
+ *
315
+ * @returns The current preview candidate if one exists, null otherwise.
316
+ */
317
+ getSnapPreviewCandidate() {
318
+ return this._core.store.state.snapPreviewCandidate ?? null;
319
+ }
320
+ /**
321
+ * Clears the snap candidate from the store state and triggers a rerender.
322
+ * Should be called when snapping is cancelled or completed.
323
+ */
324
+ clearSnapCandidate() {
325
+ this._core.store.state.snapCandidate = null;
326
+ this._core.rerender();
327
+ }
328
+ /**
329
+ * Clears the snap preview candidate from the store state and triggers a rerender.
330
+ */
331
+ clearSnapPreviewCandidate() {
332
+ this._core.store.state.snapPreviewCandidate = null;
333
+ this._core.rerender();
334
+ }
335
+ // ============================================
336
+ // Render Data
337
+ // ============================================
338
+ /**
339
+ * Gets render data for anchor lines visualization.
340
+ * Computes stroke widths, dash patterns, and indicator sizes scaled to the current viewport.
341
+ *
342
+ * @returns AnchorLinesRenderData object containing visualization parameters and anchor
343
+ * visualization data for both endpoints, or null if there's no selected line
344
+ * with anchors.
345
+ */
346
+ getAnchorLinesRenderData() {
347
+ const selectionGroup = this._core.store.selectionGroup;
348
+ if (!selectionGroup || selectionGroup.objects.length !== 1)
349
+ return null;
350
+ const selectedObject = selectionGroup.objects[0];
351
+ if (!KritzelClassHelper.isInstanceOf(selectedObject, 'KritzelLine'))
352
+ return null;
353
+ const line = selectedObject;
354
+ const startAnchorVizRaw = this.computeAnchorVisualization(line, 'start');
355
+ const endAnchorVizRaw = this.computeAnchorVisualization(line, 'end');
356
+ const startAnchorViz = this.isFiniteAnchorVisualization(startAnchorVizRaw) ? startAnchorVizRaw : null;
357
+ const endAnchorViz = this.isFiniteAnchorVisualization(endAnchorVizRaw) ? endAnchorVizRaw : null;
358
+ if (!startAnchorViz && !endAnchorViz)
359
+ return null;
360
+ const scale = this.getSafeScale(this._core.store.state.scale);
361
+ const lineScale = this.getSafeScale(line.scale);
362
+ const lineStrokeWidth = line.strokeWidth / lineScale;
363
+ const indicatorStrokeWidth = `${2 / scale}`;
364
+ const dashLength = Math.max(lineStrokeWidth * 2, 4 / scale);
365
+ const dashArray = `${dashLength} ${dashLength}`;
366
+ const indicatorRadius = 8 / scale;
367
+ if (!this.areFiniteNumbers(lineStrokeWidth, dashLength, indicatorRadius)) {
368
+ return null;
369
+ }
370
+ return {
371
+ lineStrokeWidth,
372
+ indicatorStrokeWidth,
373
+ dashArray,
374
+ indicatorRadius,
375
+ startAnchorViz,
376
+ endAnchorViz,
377
+ };
378
+ }
379
+ /**
380
+ * Gets render data for snap indicator visualization.
381
+ * Computes all visual parameters needed to render the snap preview including
382
+ * indicator radius, stroke widths, dash patterns, and arrow geometry.
383
+ *
384
+ * @returns SnapIndicatorRenderData object containing all visualization parameters,
385
+ * or null if there's no active snap candidate.
386
+ */
387
+ getSnapIndicatorRenderData() {
388
+ return this.getIndicatorRenderDataForCandidate(this.getSnapCandidate());
389
+ }
390
+ /**
391
+ * Gets render data for snap preview visualization.
392
+ *
393
+ * @returns SnapIndicatorRenderData for the active preview candidate, or null if none exists.
394
+ */
395
+ getSnapPreviewRenderData() {
396
+ return this.getIndicatorRenderDataForCandidate(this.getSnapPreviewCandidate());
397
+ }
398
+ getIndicatorRenderDataForCandidate(candidate) {
399
+ if (!candidate)
400
+ return null;
401
+ if (!this.areFiniteNumbers(candidate.centerX, candidate.centerY, candidate.lineEndpointX, candidate.lineEndpointY)) {
402
+ return null;
403
+ }
404
+ const scale = this.getSafeScale(this._core.store.state.scale);
405
+ const indicatorRadius = 8 / scale;
406
+ const indicatorStrokeWidth = `${2 / scale}`;
407
+ const lineStrokeWidthNum = this.areFiniteNumbers(candidate.lineStrokeWidth) && candidate.lineStrokeWidth > 0
408
+ ? candidate.lineStrokeWidth
409
+ : (4 / scale);
410
+ const lineStrokeWidth = `${lineStrokeWidthNum}`;
411
+ const dashLength = Math.max(lineStrokeWidthNum * 2, 4 / scale);
412
+ const dashArray = `${dashLength} ${dashLength}`;
413
+ const lineStroke = candidate.lineStroke || '#000000';
414
+ let edgeX = this.areFiniteNumbers(candidate.edgeX) ? candidate.edgeX : undefined;
415
+ let edgeY = this.areFiniteNumbers(candidate.edgeY) ? candidate.edgeY : undefined;
416
+ let solidLineEndX = edgeX;
417
+ let solidLineEndY = edgeY;
418
+ let arrowPoints;
419
+ const arrowOffset = this.areFiniteNumbers(candidate.arrowOffset) && candidate.arrowOffset > 0
420
+ ? candidate.arrowOffset
421
+ : undefined;
422
+ if (arrowOffset !== undefined && edgeX !== undefined && edgeY !== undefined) {
423
+ const dx = candidate.lineEndpointX - edgeX;
424
+ const dy = candidate.lineEndpointY - edgeY;
425
+ const length = Math.sqrt(dx * dx + dy * dy);
426
+ if (length > arrowOffset) {
427
+ solidLineEndX = edgeX + (dx / length) * arrowOffset;
428
+ solidLineEndY = edgeY + (dy / length) * arrowOffset;
429
+ }
430
+ // Calculate arrow head points
431
+ // Direction from line endpoint to edge (arrow direction)
432
+ const arrowDx = edgeX - candidate.lineEndpointX;
433
+ const arrowDy = edgeY - candidate.lineEndpointY;
434
+ const arrowLengthTotal = Math.sqrt(arrowDx * arrowDx + arrowDy * arrowDy);
435
+ if (arrowLengthTotal > 0) {
436
+ const ux = arrowDx / arrowLengthTotal;
437
+ const uy = arrowDy / arrowLengthTotal;
438
+ // Perpendicular vector
439
+ const px = -uy;
440
+ const py = ux;
441
+ // Arrow dimensions
442
+ const arrowLength = arrowOffset;
443
+ const arrowWidth = arrowLength; // 1:1 ratio
444
+ // Arrow tip at edge
445
+ const tipX = edgeX;
446
+ const tipY = edgeY;
447
+ // Arrow base
448
+ const baseX = tipX - ux * arrowLength;
449
+ const baseY = tipY - uy * arrowLength;
450
+ // Arrow wings
451
+ const leftX = baseX + px * arrowWidth / 2;
452
+ const leftY = baseY + py * arrowWidth / 2;
453
+ const rightX = baseX - px * arrowWidth / 2;
454
+ const rightY = baseY - py * arrowWidth / 2;
455
+ if (this.areFiniteNumbers(tipX, tipY, leftX, leftY, rightX, rightY)) {
456
+ arrowPoints = `${tipX},${tipY} ${leftX},${leftY} ${rightX},${rightY}`;
457
+ }
458
+ }
459
+ }
460
+ if (!this.areFiniteNumbers(indicatorRadius, lineStrokeWidthNum, dashLength)) {
461
+ return null;
462
+ }
463
+ const snapLinePath = (() => {
464
+ if (candidate.controlX !== undefined &&
465
+ candidate.controlY !== undefined &&
466
+ candidate.t !== undefined &&
467
+ this.areFiniteNumbers(candidate.controlX, candidate.controlY, candidate.t)) {
468
+ const startT = candidate.endpoint === 'start' ? 1 - candidate.t : candidate.t;
469
+ // Ensure meaningful range
470
+ if (startT >= 1)
471
+ return undefined;
472
+ const segment = this.extractQuadraticSegment({ x: candidate.lineEndpointX, y: candidate.lineEndpointY }, { x: candidate.controlX, y: candidate.controlY }, { x: candidate.centerX, y: candidate.centerY }, startT, 1);
473
+ if (!this.areFiniteNumbers(segment.start.x, segment.start.y, segment.control.x, segment.control.y, segment.end.x, segment.end.y)) {
474
+ return undefined;
475
+ }
476
+ return `M ${segment.start.x} ${segment.start.y} Q ${segment.control.x} ${segment.control.y} ${segment.end.x} ${segment.end.y}`;
477
+ }
478
+ return undefined;
479
+ })();
480
+ return {
481
+ indicatorRadius,
482
+ indicatorStrokeWidth,
483
+ lineStrokeWidth,
484
+ dashArray,
485
+ lineStroke,
486
+ centerX: candidate.centerX,
487
+ centerY: candidate.centerY,
488
+ lineEndpointX: candidate.lineEndpointX,
489
+ lineEndpointY: candidate.lineEndpointY,
490
+ edgeX,
491
+ edgeY,
492
+ arrowOffset,
493
+ arrowStyle: candidate.arrowStyle,
494
+ arrowFill: candidate.arrowFill,
495
+ solidLineEndX,
496
+ solidLineEndY,
497
+ arrowPoints,
498
+ snapLinePath,
499
+ };
500
+ }
501
+ // ============================================
502
+ // Cleanup Operations
503
+ // ============================================
504
+ /**
505
+ * Handles cleanup when an object is deleted from the canvas.
506
+ * Detaches all lines that were anchored to the deleted object and
507
+ * removes the object from the internal anchor index.
508
+ *
509
+ * @param objectId - The unique identifier of the deleted object.
510
+ */
511
+ handleObjectDeleted(objectId) {
512
+ const entries = this.getLinesAnchoredTo(objectId);
513
+ for (const entry of entries) {
514
+ this.removeAnchor(entry.lineId, entry.endpoint);
515
+ // Update the line to persist the change
516
+ const line = this.getLineById(entry.lineId);
517
+ if (line) {
518
+ this._core.store.objects.update(line);
519
+ }
520
+ }
521
+ // Remove the object from the index
522
+ this._anchorIndex.delete(objectId);
523
+ }
524
+ /**
525
+ * Handles cleanup when a line is deleted from the canvas.
526
+ * Removes all anchor index entries for the deleted line.
527
+ *
528
+ * @param lineId - The unique identifier of the deleted line.
529
+ */
530
+ handleLineDeleted(lineId) {
531
+ const line = this.getLineById(lineId);
532
+ if (!line) {
533
+ return;
534
+ }
535
+ if (line.startAnchor) {
536
+ this.removeFromIndex(line.startAnchor.objectId, lineId, 'start');
537
+ }
538
+ if (line.endAnchor) {
539
+ this.removeFromIndex(line.endAnchor.objectId, lineId, 'end');
540
+ }
541
+ }
542
+ // ============================================
543
+ // Index Management
544
+ // ============================================
545
+ /**
546
+ * Rebuilds the anchor index from all lines in the object map.
547
+ * Iterates through all KritzelLine objects and indexes their anchor connections.
548
+ * Should be called after loading/deserializing objects or when the index may be stale.
549
+ */
550
+ rebuildIndex() {
551
+ this._anchorIndex.clear();
552
+ const objects = this._core.store.allObjects;
553
+ for (const object of objects) {
554
+ if (object instanceof KritzelLine) {
555
+ if (object.startAnchor) {
556
+ this.addToIndex(object.startAnchor.objectId, object.id, 'start');
557
+ }
558
+ if (object.endAnchor) {
559
+ this.addToIndex(object.endAnchor.objectId, object.id, 'end');
560
+ }
561
+ }
562
+ }
563
+ }
564
+ /**
565
+ * Adds an entry to the anchor index.
566
+ * Creates a new Set for the object if it doesn't exist in the index.
567
+ * Prevents duplicate entries by checking for existing matches.
568
+ *
569
+ * @param objectId - The unique identifier of the anchored-to object.
570
+ * @param lineId - The unique identifier of the line with the anchor.
571
+ * @param endpoint - Which endpoint of the line is anchored ('start' or 'end').
572
+ */
573
+ addToIndex(objectId, lineId, endpoint) {
574
+ if (!this._anchorIndex.has(objectId)) {
575
+ this._anchorIndex.set(objectId, new Set());
576
+ }
577
+ const entries = this._anchorIndex.get(objectId);
578
+ // Check if entry already exists
579
+ for (const entry of entries) {
580
+ if (entry.lineId === lineId && entry.endpoint === endpoint) {
581
+ return;
582
+ }
583
+ }
584
+ entries.add({ lineId, endpoint });
585
+ }
586
+ /**
587
+ * Removes an entry from the anchor index.
588
+ * Cleans up empty Sets from the index to prevent memory leaks.
589
+ *
590
+ * @param objectId - The unique identifier of the anchored-to object.
591
+ * @param lineId - The unique identifier of the line with the anchor.
592
+ * @param endpoint - Which endpoint of the line is anchored ('start' or 'end').
593
+ */
594
+ removeFromIndex(objectId, lineId, endpoint) {
595
+ const entries = this._anchorIndex.get(objectId);
596
+ if (!entries) {
597
+ return;
598
+ }
599
+ for (const entry of entries) {
600
+ if (entry.lineId === lineId && entry.endpoint === endpoint) {
601
+ entries.delete(entry);
602
+ break;
603
+ }
604
+ }
605
+ // Clean up empty sets
606
+ if (entries.size === 0) {
607
+ this._anchorIndex.delete(objectId);
608
+ }
609
+ }
610
+ // ============================================
611
+ // Helper Methods
612
+ // ============================================
613
+ /**
614
+ * Gets a KritzelLine object by its unique identifier.
615
+ *
616
+ * @param lineId - The unique identifier of the line to retrieve.
617
+ * @returns The KritzelLine object if found and is a line, null otherwise.
618
+ */
619
+ getLineById(lineId) {
620
+ const objects = this._core.store.objects.filter(o => o.id === lineId);
621
+ if (objects.length === 0) {
622
+ return null;
623
+ }
624
+ const object = objects[0];
625
+ return object instanceof KritzelLine ? object : null;
626
+ }
627
+ /**
628
+ * Gets any KritzelBaseObject by its unique identifier.
629
+ *
630
+ * @param objectId - The unique identifier of the object to retrieve.
631
+ * @returns The KritzelBaseObject if found, null otherwise.
632
+ */
633
+ getObjectById(objectId) {
634
+ const objects = this._core.store.objects.filter(o => o.id === objectId);
635
+ return objects.length > 0 ? objects[0] : null;
636
+ }
637
+ /**
638
+ * Finds the target object that a line endpoint is anchored to.
639
+ * Searches among all non-selection objects in the store.
640
+ *
641
+ * @param line - The KritzelLine object to check.
642
+ * @param endpoint - Which endpoint to check ('start' or 'end').
643
+ * @returns The target KritzelBaseObject if the anchor exists and target is found, null otherwise.
644
+ */
645
+ findAnchorTarget(line, endpoint) {
646
+ const anchor = endpoint === 'start' ? line.startAnchor : line.endAnchor;
647
+ if (!anchor) {
648
+ return null;
649
+ }
650
+ return this._core.store.allNonSelectionObjects.find(obj => obj.id === anchor.objectId) ?? null;
651
+ }
652
+ // ============================================
653
+ // Visualization Helpers
654
+ // ============================================
655
+ /**
656
+ * Computes anchor visualization data for a specific endpoint of a line with anchors.
657
+ * Calculates edge intersection points for rendering dashed lines from edge to center.
658
+ *
659
+ * @param line - The KritzelLine object to compute visualization for.
660
+ * @param endpoint - Which endpoint to compute visualization for ('start' or 'end').
661
+ * @returns AnchorVisualization object containing edge and center coordinates plus
662
+ * optional path data for curved lines, or null if no anchor exists.
663
+ */
664
+ computeAnchorVisualization(line, endpoint) {
665
+ const anchor = endpoint === 'start' ? line.startAnchor : line.endAnchor;
666
+ if (!anchor)
667
+ return null;
668
+ const targetObject = this.findAnchorTarget(line, endpoint);
669
+ if (!targetObject)
670
+ return null;
671
+ const clipInfo = this.computeAnchorClipInfo(line, endpoint, targetObject);
672
+ if (!clipInfo)
673
+ return null;
674
+ const centerX = targetObject.centerX;
675
+ const centerY = targetObject.centerY;
676
+ if (!this.areFiniteNumbers(clipInfo.worldX, clipInfo.worldY, centerX, centerY)) {
677
+ return null;
678
+ }
679
+ const pathD = this.buildAnchorPath(line, endpoint, clipInfo, targetObject) ?? undefined;
680
+ if (pathD && !this.isFinitePathData(pathD)) {
681
+ return null;
682
+ }
683
+ return {
684
+ edgeX: clipInfo.worldX,
685
+ edgeY: clipInfo.worldY,
686
+ centerX,
687
+ centerY,
688
+ pathD,
689
+ };
690
+ }
691
+ /**
692
+ * Computes a clipped line path that stops at anchor edges instead of going to anchor centers.
693
+ * When arrows are present on anchored ends, the line is shortened so the arrow tip appears at the edge.
694
+ * Handles both straight lines and curved (quadratic Bezier) lines.
695
+ *
696
+ * @param line - The KritzelLine object to compute the clipped path for.
697
+ * @param offsetByViewBox - If true, subtracts line.x and line.y from coordinates (for selection UI).
698
+ * @returns The SVG path 'd' attribute string, or the original path if no clipping is needed.
699
+ */
700
+ computeClippedLinePath(line, offsetByViewBox = false) {
701
+ // Check for snap candidate first - this applies during drag operations even without anchors
702
+ const snapCandidate = this.getSnapCandidate();
703
+ const selectionGroup = this._core.store.selectionGroup;
704
+ const hasActiveSnapCandidate = snapCandidate && selectionGroup && selectionGroup.objects.length === 1 && selectionGroup.objects[0].id === line.id;
705
+ // If no anchors and no active snap candidate, return original path (with or without offset)
706
+ if (!line.startAnchor && !line.endAnchor && !hasActiveSnapCandidate) {
707
+ if (offsetByViewBox) {
708
+ if (line.controlX !== undefined && line.controlY !== undefined) {
709
+ return `M ${line.startX - line.x} ${line.startY - line.y} Q ${line.controlX - line.x} ${line.controlY - line.y} ${line.endX - line.x} ${line.endY - line.y}`;
710
+ }
711
+ return `M ${line.startX - line.x} ${line.startY - line.y} L ${line.endX - line.x} ${line.endY - line.y}`;
712
+ }
713
+ return line.d;
714
+ }
715
+ const startTarget = line.startAnchor ? this.findAnchorTarget(line, 'start') : null;
716
+ const endTarget = line.endAnchor ? this.findAnchorTarget(line, 'end') : null;
717
+ let startClip = startTarget ? this.computeAnchorClipInfo(line, 'start', startTarget) : null;
718
+ let endClip = endTarget ? this.computeAnchorClipInfo(line, 'end', endTarget) : null;
719
+ // Apply snap candidate if present and matches this line
720
+ if (hasActiveSnapCandidate) {
721
+ if (snapCandidate.edgeX !== undefined && snapCandidate.edgeY !== undefined) {
722
+ const localEdge = this.lineWorldToLocal(line, snapCandidate.edgeX, snapCandidate.edgeY);
723
+ const clipInfo = {
724
+ worldX: snapCandidate.edgeX,
725
+ worldY: snapCandidate.edgeY,
726
+ localX: localEdge.x,
727
+ localY: localEdge.y,
728
+ t: snapCandidate.t
729
+ };
730
+ if (snapCandidate.endpoint === 'start') {
731
+ startClip = clipInfo;
732
+ }
733
+ else {
734
+ endClip = clipInfo;
735
+ }
736
+ }
737
+ }
738
+ // Apply offset if requested (for selection UI which uses coordinates relative to 0,0)
739
+ const offsetX = offsetByViewBox ? line.x : 0;
740
+ const offsetY = offsetByViewBox ? line.y : 0;
741
+ // Handle curved lines by splitting the quadratic at the clipped parameters
742
+ if (line.controlX !== undefined && line.controlY !== undefined) {
743
+ let startT = startClip?.t ?? 0;
744
+ let endT = endClip?.t ?? 1;
745
+ // Adjust for start arrow
746
+ if (startClip && line.hasStartArrow) {
747
+ const arrowOffset = line.getArrowSize('start');
748
+ const speed = this.evaluateDerivativeSpeedAtT(line, startT);
749
+ if (speed > 0) {
750
+ startT += arrowOffset / speed;
751
+ }
752
+ }
753
+ // Adjust for end arrow
754
+ if (endClip && line.hasEndArrow) {
755
+ const arrowOffset = line.getArrowSize('end');
756
+ const speed = this.evaluateDerivativeSpeedAtT(line, endT);
757
+ if (speed > 0) {
758
+ endT -= arrowOffset / speed;
759
+ }
760
+ }
761
+ // Ensure valid range
762
+ if (startT < 0)
763
+ startT = 0;
764
+ if (endT > 1)
765
+ endT = 1;
766
+ // Handle overlap or invalid range
767
+ if (endT <= startT) {
768
+ // If the arrow adjustment caused them to cross, or they were already crossed/close
769
+ // We can either effectively hide the line, or just clamp them.
770
+ // If we clamp them to the midpoint, the line becomes a point.
771
+ // Let's ensure a minimal gap or just set them equal.
772
+ const mid = (startT + endT) / 2;
773
+ startT = mid;
774
+ endT = mid;
775
+ }
776
+ const segment = this.extractQuadraticSegment({ x: line.startX, y: line.startY }, { x: line.controlX, y: line.controlY }, { x: line.endX, y: line.endY }, startT, endT);
777
+ return `M ${segment.start.x - offsetX} ${segment.start.y - offsetY} Q ${segment.control.x - offsetX} ${segment.control.y - offsetY} ${segment.end.x - offsetX} ${segment.end.y - offsetY}`;
778
+ }
779
+ // Straight lines fall back to linear interpolation
780
+ let startX = startClip?.localX ?? line.startX;
781
+ let startY = startClip?.localY ?? line.startY;
782
+ let endX = endClip?.localX ?? line.endX;
783
+ let endY = endClip?.localY ?? line.endY;
784
+ if (startClip && line.hasStartArrow) {
785
+ const arrowOffset = line.getArrowSize('start');
786
+ const dx = endX - startX;
787
+ const dy = endY - startY;
788
+ const length = Math.sqrt(dx * dx + dy * dy);
789
+ if (length > arrowOffset) {
790
+ startX += (dx / length) * arrowOffset;
791
+ startY += (dy / length) * arrowOffset;
792
+ }
793
+ }
794
+ if (endClip && line.hasEndArrow) {
795
+ const arrowOffset = line.getArrowSize('end');
796
+ const dx = startX - endX;
797
+ const dy = startY - endY;
798
+ const length = Math.sqrt(dx * dx + dy * dy);
799
+ if (length > arrowOffset) {
800
+ endX += (dx / length) * arrowOffset;
801
+ endY += (dy / length) * arrowOffset;
802
+ }
803
+ }
804
+ return `M ${startX - offsetX} ${startY - offsetY} L ${endX - offsetX} ${endY - offsetY}`;
805
+ }
806
+ /**
807
+ * Computes anchor clipping information for a line endpoint connected to a target object.
808
+ * Dispatches to either curved or straight line computation based on line type.
809
+ *
810
+ * @param line - The KritzelLine object.
811
+ * @param endpoint - Which endpoint to compute clip info for ('start' or 'end').
812
+ * @param targetObject - The object the line is anchored to.
813
+ * @returns AnchorClipInfo containing local and world coordinates of the clip point,
814
+ * plus parameter t for curved lines, or null if computation fails.
815
+ */
816
+ computeAnchorClipInfo(line, endpoint, targetObject) {
817
+ if (line.controlX !== undefined && line.controlY !== undefined) {
818
+ return this.computeCurvedClipInfo(line, endpoint, targetObject);
819
+ }
820
+ return this.computeStraightClipInfo(line, endpoint, targetObject);
821
+ }
822
+ /**
823
+ * Computes clip information for a straight line anchored to a target object.
824
+ * Uses ray-casting from the opposite endpoint through the target's edge.
825
+ *
826
+ * @param line - The KritzelLine object (must be a straight line, no control point).
827
+ * @param endpoint - Which endpoint is anchored ('start' or 'end').
828
+ * @param targetObject - The object the line is anchored to.
829
+ * @returns AnchorClipInfo with intersection coordinates and parameter t, or null if no intersection.
830
+ */
831
+ computeStraightClipInfo(line, endpoint, targetObject) {
832
+ const reference = endpoint === 'start'
833
+ ? this.lineLocalToWorld(line, line.endX, line.endY)
834
+ : this.lineLocalToWorld(line, line.startX, line.startY);
835
+ // Check for path object (uses stroke edge detection)
836
+ const pathClipPoint = this.getPathClipPoint(targetObject, reference);
837
+ if (pathClipPoint) {
838
+ const localEdge = this.lineWorldToLocal(line, pathClipPoint.x, pathClipPoint.y);
839
+ const totalLength = Math.sqrt(Math.pow(line.endX - line.startX, 2) +
840
+ Math.pow(line.endY - line.startY, 2));
841
+ const distanceFromStart = Math.sqrt(Math.pow(localEdge.x - line.startX, 2) +
842
+ Math.pow(localEdge.y - line.startY, 2));
843
+ const t = totalLength > 0 ? distanceFromStart / totalLength : endpoint === 'start' ? 0 : 1;
844
+ return {
845
+ localX: localEdge.x,
846
+ localY: localEdge.y,
847
+ worldX: pathClipPoint.x,
848
+ worldY: pathClipPoint.y,
849
+ t,
850
+ };
851
+ }
852
+ // Check for custom clip polygon (ellipse, triangle shapes)
853
+ const customClipPolygon = this.getClipPolygonForObject(targetObject);
854
+ let edgeIntersection;
855
+ if (customClipPolygon) {
856
+ // Use custom polygon for non-rectangular shapes
857
+ edgeIntersection = KritzelGeometryHelper.getLinePointsArrayIntersection(reference, { x: targetObject.centerX, y: targetObject.centerY }, customClipPolygon);
858
+ }
859
+ else {
860
+ // Fall back to bounding box for rectangles and other objects
861
+ edgeIntersection = KritzelGeometryHelper.getLinePolygonIntersection(reference, { x: targetObject.centerX, y: targetObject.centerY }, targetObject.rotatedPolygon);
862
+ }
863
+ if (!edgeIntersection) {
864
+ return null;
865
+ }
866
+ const localEdge = this.lineWorldToLocal(line, edgeIntersection.x, edgeIntersection.y);
867
+ const totalLength = Math.sqrt(Math.pow(line.endX - line.startX, 2) +
868
+ Math.pow(line.endY - line.startY, 2));
869
+ const distanceFromStart = Math.sqrt(Math.pow(localEdge.x - line.startX, 2) +
870
+ Math.pow(localEdge.y - line.startY, 2));
871
+ const t = totalLength > 0 ? distanceFromStart / totalLength : endpoint === 'start' ? 0 : 1;
872
+ return {
873
+ localX: localEdge.x,
874
+ localY: localEdge.y,
875
+ worldX: edgeIntersection.x,
876
+ worldY: edgeIntersection.y,
877
+ t,
878
+ };
879
+ }
880
+ /**
881
+ * Computes clip information for a curved (quadratic Bezier) line anchored to a target object.
882
+ * Samples along the curve to find where it exits the target object's bounds.
883
+ *
884
+ * @param line - The KritzelLine object (must have controlX and controlY defined).
885
+ * @param endpoint - Which endpoint is anchored ('start' or 'end').
886
+ * @param targetObject - The object the line is anchored to.
887
+ * @returns AnchorClipInfo with intersection coordinates and parameter t, or null if no intersection.
888
+ */
889
+ computeCurvedClipInfo(line, endpoint, targetObject) {
890
+ // Check for path object (uses stroke edge detection via sampling)
891
+ if (KritzelClassHelper.isInstanceOf(targetObject, 'KritzelPath')) {
892
+ // For curved lines to paths, sample along the curve to find stroke edge
893
+ const path = targetObject;
894
+ return this.findCurveExitPointForPath(line, endpoint, path);
895
+ }
896
+ // Check for custom clip polygon (ellipse, triangle shapes)
897
+ const customClipPolygon = this.getClipPolygonForObject(targetObject);
898
+ const polygonPoints = customClipPolygon ?? this.getPolygonPoints(targetObject.rotatedPolygon);
899
+ const exitPoint = this.findCurveExitPoint(line, endpoint, polygonPoints);
900
+ if (exitPoint) {
901
+ return exitPoint;
902
+ }
903
+ const reference = endpoint === 'start'
904
+ ? this.lineLocalToWorld(line, line.endX, line.endY)
905
+ : this.lineLocalToWorld(line, line.startX, line.startY);
906
+ let fallbackIntersection;
907
+ if (customClipPolygon) {
908
+ fallbackIntersection = KritzelGeometryHelper.getLinePointsArrayIntersection(reference, { x: targetObject.centerX, y: targetObject.centerY }, customClipPolygon);
909
+ }
910
+ else {
911
+ fallbackIntersection = KritzelGeometryHelper.getLinePolygonIntersection(reference, { x: targetObject.centerX, y: targetObject.centerY }, targetObject.rotatedPolygon);
912
+ }
913
+ if (!fallbackIntersection) {
914
+ return null;
915
+ }
916
+ const localEdge = this.lineWorldToLocal(line, fallbackIntersection.x, fallbackIntersection.y);
917
+ const approxT = this.approximateParameterForWorldPoint(line, fallbackIntersection);
918
+ return {
919
+ localX: localEdge.x,
920
+ localY: localEdge.y,
921
+ worldX: fallbackIntersection.x,
922
+ worldY: fallbackIntersection.y,
923
+ t: approxT,
924
+ };
925
+ }
926
+ /**
927
+ * Finds the point where a curved line exits a polygon boundary.
928
+ * Samples along the curve from the anchored endpoint outward, detecting
929
+ * the transition from inside to outside the polygon, then refines the result.
930
+ *
931
+ * @param line - The KritzelLine object (curved line with control point).
932
+ * @param endpoint - Which endpoint is anchored inside the polygon ('start' or 'end').
933
+ * @param polygonPoints - Array of points defining the polygon boundary.
934
+ * @returns AnchorClipInfo with the exit point coordinates and parameter t, or null if no exit found.
935
+ */
936
+ findCurveExitPoint(line, endpoint, polygonPoints) {
937
+ const steps = 64;
938
+ const initialT = endpoint === 'start' ? 0 : 1;
939
+ const initialSample = this.evaluateLineAtT(line, initialT);
940
+ let prevInside = KritzelGeometryHelper.isPointInPolygon({ x: initialSample.worldX, y: initialSample.worldY }, polygonPoints);
941
+ let prevT = initialT;
942
+ for (let i = 1; i <= steps; i++) {
943
+ const t = endpoint === 'start' ? i / steps : 1 - i / steps;
944
+ const sample = this.evaluateLineAtT(line, t);
945
+ const inside = KritzelGeometryHelper.isPointInPolygon({ x: sample.worldX, y: sample.worldY }, polygonPoints);
946
+ if (prevInside && !inside) {
947
+ const refinedT = this.refineCurveExitParameter(line, polygonPoints, prevT, t);
948
+ const refinedPoint = this.evaluateLineAtT(line, refinedT);
949
+ return {
950
+ localX: refinedPoint.localX,
951
+ localY: refinedPoint.localY,
952
+ worldX: refinedPoint.worldX,
953
+ worldY: refinedPoint.worldY,
954
+ t: refinedT,
955
+ };
956
+ }
957
+ prevInside = inside;
958
+ prevT = t;
959
+ }
960
+ return null;
961
+ }
962
+ /**
963
+ * Finds the exit point for a curved line going to a Path object.
964
+ * Uses distance-to-stroke sampling instead of polygon containment
965
+ * to detect when the line exits the path's stroke width boundary.
966
+ *
967
+ * @param line - The KritzelLine object (curved line with control point).
968
+ * @param endpoint - Which endpoint is anchored to the path ('start' or 'end').
969
+ * @param path - The KritzelPath object the line is anchored to.
970
+ * @returns AnchorClipInfo with the exit point coordinates and parameter t, or null if no exit found.
971
+ */
972
+ findCurveExitPointForPath(line, endpoint, path) {
973
+ const steps = 64;
974
+ const initialT = endpoint === 'start' ? 0 : 1;
975
+ const halfStroke = path.strokeWidth / path.scale / 2;
976
+ // Ensure path has computed points
977
+ const pathPoints = this.getPathAdjustedPoints(path);
978
+ if (pathPoints.length < 1) {
979
+ return null;
980
+ }
981
+ // Check if starting point is inside the stroke
982
+ const initialSample = this.evaluateLineAtT(line, initialT);
983
+ let prevInside = this.isPointInPathStroke(initialSample.worldX, initialSample.worldY, pathPoints, halfStroke);
984
+ let prevT = initialT;
985
+ for (let i = 1; i <= steps; i++) {
986
+ const t = endpoint === 'start' ? i / steps : 1 - i / steps;
987
+ const sample = this.evaluateLineAtT(line, t);
988
+ const inside = this.isPointInPathStroke(sample.worldX, sample.worldY, pathPoints, halfStroke);
989
+ if (prevInside && !inside) {
990
+ // Refine using binary search
991
+ const refinedT = this.refineCurveExitParameterForPath(line, pathPoints, halfStroke, prevT, t);
992
+ const refinedPoint = this.evaluateLineAtT(line, refinedT);
993
+ return {
994
+ localX: refinedPoint.localX,
995
+ localY: refinedPoint.localY,
996
+ worldX: refinedPoint.worldX,
997
+ worldY: refinedPoint.worldY,
998
+ t: refinedT,
999
+ };
1000
+ }
1001
+ prevInside = inside;
1002
+ prevT = t;
1003
+ }
1004
+ return null;
1005
+ }
1006
+ /**
1007
+ * Refines the curve exit parameter for a Path object using binary search.
1008
+ * Narrows down the exact parameter t where the curve exits the path's stroke.
1009
+ *
1010
+ * @param line - The KritzelLine object being evaluated.
1011
+ * @param pathPoints - Array of adjusted path points [[x, y], ...].
1012
+ * @param halfStroke - Half of the path's stroke width for distance checks.
1013
+ * @param insideT - Parameter t known to be inside the stroke.
1014
+ * @param outsideT - Parameter t known to be outside the stroke.
1015
+ * @returns The refined parameter t at the stroke boundary.
1016
+ */
1017
+ refineCurveExitParameterForPath(line, pathPoints, halfStroke, insideT, outsideT) {
1018
+ let tInside = insideT;
1019
+ let tOutside = outsideT;
1020
+ for (let i = 0; i < 8; i++) {
1021
+ const mid = (tInside + tOutside) / 2;
1022
+ const sample = this.evaluateLineAtT(line, mid);
1023
+ const inside = this.isPointInPathStroke(sample.worldX, sample.worldY, pathPoints, halfStroke);
1024
+ if (inside) {
1025
+ tInside = mid;
1026
+ }
1027
+ else {
1028
+ tOutside = mid;
1029
+ }
1030
+ }
1031
+ return (tInside + tOutside) / 2;
1032
+ }
1033
+ /**
1034
+ * Gets the adjusted world-space points for a Path object.
1035
+ * Wrapper that delegates to computePathAdjustedPoints.
1036
+ *
1037
+ * @param path - The KritzelPath object to get points for.
1038
+ * @returns Array of [x, y] coordinate pairs in world space.
1039
+ */
1040
+ getPathAdjustedPoints(path) {
1041
+ // Access the path's adjusted points through its internal computation
1042
+ // We'll need to manually compute them since they're private
1043
+ return this.computePathAdjustedPoints(path);
1044
+ }
1045
+ /**
1046
+ * Computes world-space adjusted points for a Path object.
1047
+ * Applies rotation and scale transformations to convert the path's
1048
+ * internal points to world coordinates.
1049
+ *
1050
+ * @param path - The KritzelPath object to compute points for.
1051
+ * @returns Array of [x, y] coordinate pairs in world space, or empty array if path has no points.
1052
+ */
1053
+ computePathAdjustedPoints(path) {
1054
+ if (!path.points?.length) {
1055
+ return [];
1056
+ }
1057
+ const angle = path.rotation;
1058
+ const cos = Math.cos(angle);
1059
+ const sin = Math.sin(angle);
1060
+ const xs = path.points.map(p => p[0]);
1061
+ const ys = path.points.map(p => p[1]);
1062
+ const pivot = {
1063
+ x: (Math.min(...xs) + Math.max(...xs)) / 2,
1064
+ y: (Math.min(...ys) + Math.max(...ys)) / 2,
1065
+ };
1066
+ const { x: cx, y: cy } = pivot;
1067
+ const rotatedPoints = path.points.map(([x, y]) => {
1068
+ const dx = x - cx;
1069
+ const dy = y - cy;
1070
+ return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
1071
+ });
1072
+ return rotatedPoints.map(([px, py]) => [
1073
+ Math.abs(px - path.x) / path.scale + path.translateX,
1074
+ Math.abs(py - path.y) / path.scale + path.translateY,
1075
+ ]);
1076
+ }
1077
+ /**
1078
+ * Checks if a point is within the stroke width of a Path.
1079
+ * Tests distance from the point to each segment of the path.
1080
+ *
1081
+ * @param x - X coordinate in world space.
1082
+ * @param y - Y coordinate in world space.
1083
+ * @param pathPoints - Array of [x, y] coordinate pairs defining the path.
1084
+ * @param halfStroke - Half of the stroke width (the distance threshold).
1085
+ * @returns True if the point is within halfStroke distance of any path segment.
1086
+ */
1087
+ isPointInPathStroke(x, y, pathPoints, halfStroke) {
1088
+ if (pathPoints.length === 1) {
1089
+ const p = pathPoints[0];
1090
+ const dx = x - p[0];
1091
+ const dy = y - p[1];
1092
+ return Math.sqrt(dx * dx + dy * dy) <= halfStroke;
1093
+ }
1094
+ for (let i = 0; i < pathPoints.length - 1; i++) {
1095
+ const p1 = pathPoints[i];
1096
+ const p2 = pathPoints[i + 1];
1097
+ const dist = this.pointToSegmentDistance(x, y, p1[0], p1[1], p2[0], p2[1]);
1098
+ if (dist <= halfStroke) {
1099
+ return true;
1100
+ }
1101
+ }
1102
+ return false;
1103
+ }
1104
+ /**
1105
+ * Calculates the shortest distance from a point to a line segment.
1106
+ * Uses projection to find the nearest point on the segment, clamped to segment bounds.
1107
+ *
1108
+ * @param px - X coordinate of the point.
1109
+ * @param py - Y coordinate of the point.
1110
+ * @param x1 - X coordinate of segment start.
1111
+ * @param y1 - Y coordinate of segment start.
1112
+ * @param x2 - X coordinate of segment end.
1113
+ * @param y2 - Y coordinate of segment end.
1114
+ * @returns The distance from the point to the nearest point on the segment.
1115
+ */
1116
+ pointToSegmentDistance(px, py, x1, y1, x2, y2) {
1117
+ const dx = x2 - x1;
1118
+ const dy = y2 - y1;
1119
+ const lengthSquared = dx * dx + dy * dy;
1120
+ if (lengthSquared === 0) {
1121
+ return Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1));
1122
+ }
1123
+ let t = ((px - x1) * dx + (py - y1) * dy) / lengthSquared;
1124
+ t = Math.max(0, Math.min(1, t));
1125
+ const nearestX = x1 + t * dx;
1126
+ const nearestY = y1 + t * dy;
1127
+ return Math.sqrt((px - nearestX) * (px - nearestX) + (py - nearestY) * (py - nearestY));
1128
+ }
1129
+ /**
1130
+ * Refines the curve exit parameter using binary search.
1131
+ * Narrows down the exact parameter t where the curve exits a polygon.
1132
+ *
1133
+ * @param line - The KritzelLine object being evaluated.
1134
+ * @param polygonPoints - Array of points defining the polygon boundary.
1135
+ * @param insideT - Parameter t known to be inside the polygon.
1136
+ * @param outsideT - Parameter t known to be outside the polygon.
1137
+ * @returns The refined parameter t at the polygon boundary.
1138
+ */
1139
+ refineCurveExitParameter(line, polygonPoints, insideT, outsideT) {
1140
+ let tInside = insideT;
1141
+ let tOutside = outsideT;
1142
+ for (let i = 0; i < 8; i++) {
1143
+ const mid = (tInside + tOutside) / 2;
1144
+ const sample = this.evaluateLineAtT(line, mid);
1145
+ const inside = KritzelGeometryHelper.isPointInPolygon({ x: sample.worldX, y: sample.worldY }, polygonPoints);
1146
+ if (inside) {
1147
+ tInside = mid;
1148
+ }
1149
+ else {
1150
+ tOutside = mid;
1151
+ }
1152
+ }
1153
+ return (tInside + tOutside) / 2;
1154
+ }
1155
+ /**
1156
+ * Approximates the curve parameter t for a given world point by sampling.
1157
+ * Finds the t value that produces a point closest to the target.
1158
+ *
1159
+ * @param line - The KritzelLine object to evaluate.
1160
+ * @param target - The target point with x and y coordinates in world space.
1161
+ * @returns The parameter t (0 to 1) that produces the closest point on the line.
1162
+ */
1163
+ approximateParameterForWorldPoint(line, target) {
1164
+ const steps = 80;
1165
+ let bestT = 0;
1166
+ let bestDistance = Infinity;
1167
+ for (let i = 0; i <= steps; i++) {
1168
+ const t = i / steps;
1169
+ const sample = this.evaluateLineAtT(line, t);
1170
+ const distance = Math.hypot(sample.worldX - target.x, sample.worldY - target.y);
1171
+ if (distance < bestDistance) {
1172
+ bestDistance = distance;
1173
+ bestT = t;
1174
+ }
1175
+ }
1176
+ return bestT;
1177
+ }
1178
+ /**
1179
+ * Evaluates a line at parameter t to get both local and world coordinates.
1180
+ * For curved lines, uses quadratic Bezier interpolation.
1181
+ * For straight lines, uses linear interpolation.
1182
+ *
1183
+ * @param line - The KritzelLine object to evaluate.
1184
+ * @param t - The parameter value (0 = start, 1 = end).
1185
+ * @returns Object containing clamped t, local coordinates, and world coordinates.
1186
+ */
1187
+ evaluateLineAtT(line, t) {
1188
+ const clampedT = Math.max(0, Math.min(1, t));
1189
+ let localX;
1190
+ let localY;
1191
+ if (line.controlX !== undefined && line.controlY !== undefined) {
1192
+ const oneMinusT = 1 - clampedT;
1193
+ localX = oneMinusT * oneMinusT * line.startX + 2 * oneMinusT * clampedT * line.controlX + clampedT * clampedT * line.endX;
1194
+ localY = oneMinusT * oneMinusT * line.startY + 2 * oneMinusT * clampedT * line.controlY + clampedT * clampedT * line.endY;
1195
+ }
1196
+ else {
1197
+ localX = line.startX + (line.endX - line.startX) * clampedT;
1198
+ localY = line.startY + (line.endY - line.startY) * clampedT;
1199
+ }
1200
+ const world = this.lineLocalToWorld(line, localX, localY);
1201
+ return { t: clampedT, localX, localY, worldX: world.x, worldY: world.y };
1202
+ }
1203
+ /**
1204
+ * Calculates the derivative speed (magnitude of tangent vector) at parameter t.
1205
+ * For quadratic Bezier: |B'(t)| = |2(1-t)(P1-P0) + 2t(P2-P1)|
1206
+ * For straight lines: constant length of the line segment.
1207
+ *
1208
+ * @param line - The KritzelLine object to evaluate.
1209
+ * @param t - The parameter value (0 to 1).
1210
+ * @returns The magnitude of the derivative (units per parameter unit).
1211
+ */
1212
+ evaluateDerivativeSpeedAtT(line, t) {
1213
+ const clampedT = Math.max(0, Math.min(1, t));
1214
+ if (line.controlX !== undefined && line.controlY !== undefined) {
1215
+ // Quadratic Bezier derivative: B'(t) = 2(1-t)(P1-P0) + 2t(P2-P1)
1216
+ const p0x = line.startX;
1217
+ const p0y = line.startY;
1218
+ const p1x = line.controlX;
1219
+ const p1y = line.controlY;
1220
+ const p2x = line.endX;
1221
+ const p2y = line.endY;
1222
+ const dx = 2 * (1 - clampedT) * (p1x - p0x) + 2 * clampedT * (p2x - p1x);
1223
+ const dy = 2 * (1 - clampedT) * (p1y - p0y) + 2 * clampedT * (p2y - p1y);
1224
+ return Math.sqrt(dx * dx + dy * dy);
1225
+ }
1226
+ else {
1227
+ // Straight line speed is constant length
1228
+ const dx = line.endX - line.startX;
1229
+ const dy = line.endY - line.startY;
1230
+ return Math.sqrt(dx * dx + dy * dy);
1231
+ }
1232
+ }
1233
+ /**
1234
+ * Extracts a sub-segment from a quadratic Bezier curve.
1235
+ * Uses de Casteljau's algorithm to split the curve at the specified t values.
1236
+ *
1237
+ * @param start - The start point of the original curve.
1238
+ * @param control - The control point of the original curve.
1239
+ * @param end - The end point of the original curve.
1240
+ * @param tStart - The parameter value for the new start (0 to 1).
1241
+ * @param tEnd - The parameter value for the new end (0 to 1).
1242
+ * @returns A new QuadraticSegment representing the sub-curve from tStart to tEnd.
1243
+ */
1244
+ extractQuadraticSegment(start, control, end, tStart, tEnd) {
1245
+ let normalizedStart = Math.max(0, Math.min(1, tStart));
1246
+ let normalizedEnd = Math.max(0, Math.min(1, tEnd));
1247
+ if (normalizedEnd < normalizedStart) {
1248
+ const swap = normalizedStart;
1249
+ normalizedStart = normalizedEnd;
1250
+ normalizedEnd = swap;
1251
+ }
1252
+ let segment = { start, control, end };
1253
+ if (normalizedStart > 0) {
1254
+ const split = this.splitQuadraticSegment(segment, normalizedStart);
1255
+ segment = split.right;
1256
+ const remainingRange = 1 - normalizedStart;
1257
+ normalizedEnd = remainingRange > 0 ? (normalizedEnd - normalizedStart) / remainingRange : 1;
1258
+ }
1259
+ if (normalizedEnd < 1) {
1260
+ const split = this.splitQuadraticSegment(segment, normalizedEnd);
1261
+ segment = split.left;
1262
+ }
1263
+ return segment;
1264
+ }
1265
+ /**
1266
+ * Splits a quadratic Bezier segment at parameter t using de Casteljau's algorithm.
1267
+ *
1268
+ * @param segment - The original QuadraticSegment to split.
1269
+ * @param t - The parameter value to split at (0 to 1).
1270
+ * @returns An object containing left and right QuadraticSegments.
1271
+ */
1272
+ splitQuadraticSegment(segment, t) {
1273
+ const clampedT = Math.max(0, Math.min(1, t));
1274
+ const p0 = segment.start;
1275
+ const p1 = segment.control;
1276
+ const p2 = segment.end;
1277
+ const p01 = this.lerpPoint(p0, p1, clampedT);
1278
+ const p12 = this.lerpPoint(p1, p2, clampedT);
1279
+ const p012 = this.lerpPoint(p01, p12, clampedT);
1280
+ return {
1281
+ left: { start: p0, control: p01, end: p012 },
1282
+ right: { start: p012, control: p12, end: p2 },
1283
+ };
1284
+ }
1285
+ /**
1286
+ * Linearly interpolates between two points.
1287
+ *
1288
+ * @param a - The start point.
1289
+ * @param b - The end point.
1290
+ * @param t - The interpolation factor (0 = a, 1 = b).
1291
+ * @returns A new QuadraticPoint at the interpolated position.
1292
+ */
1293
+ lerpPoint(a, b, t) {
1294
+ return {
1295
+ x: a.x + (b.x - a.x) * t,
1296
+ y: a.y + (b.y - a.y) * t,
1297
+ };
1298
+ }
1299
+ /**
1300
+ * Builds an SVG path for the anchor visualization from clip point to target center.
1301
+ * For straight lines, returns a simple line path. For curved lines, extracts and
1302
+ * transforms the appropriate Bezier segment.
1303
+ *
1304
+ * @param line - The KritzelLine object.
1305
+ * @param endpoint - Which endpoint is anchored ('start' or 'end').
1306
+ * @param clipInfo - The computed clip information including coordinates and parameter t.
1307
+ * @param targetObject - The target object the line is anchored to.
1308
+ * @returns SVG path 'd' attribute string, or null if path cannot be built.
1309
+ */
1310
+ buildAnchorPath(line, endpoint, clipInfo, targetObject) {
1311
+ if (line.controlX === undefined || line.controlY === undefined || clipInfo.t === undefined) {
1312
+ return `M ${clipInfo.worldX} ${clipInfo.worldY} L ${targetObject.centerX} ${targetObject.centerY}`;
1313
+ }
1314
+ const startT = endpoint === 'start' ? 0 : clipInfo.t;
1315
+ const endT = endpoint === 'start' ? clipInfo.t : 1;
1316
+ if (endT <= startT) {
1317
+ return `M ${clipInfo.worldX} ${clipInfo.worldY} L ${targetObject.centerX} ${targetObject.centerY}`;
1318
+ }
1319
+ const segment = this.extractQuadraticSegment({ x: line.startX, y: line.startY }, { x: line.controlX, y: line.controlY }, { x: line.endX, y: line.endY }, startT, endT);
1320
+ const reverse = endpoint === 'start';
1321
+ return this.buildWorldQuadraticPath(line, segment, reverse);
1322
+ }
1323
+ /**
1324
+ * Builds an SVG quadratic Bezier path in world coordinates from a segment.
1325
+ * Applies line transformations to convert local segment coordinates to world space.
1326
+ *
1327
+ * @param line - The KritzelLine providing transformation context.
1328
+ * @param segment - The QuadraticSegment to convert to a path.
1329
+ * @param reverse - If true, draws the path from end to start.
1330
+ * @returns SVG path 'd' attribute string with M and Q commands.
1331
+ */
1332
+ buildWorldQuadraticPath(line, segment, reverse = false) {
1333
+ const startPoint = reverse ? segment.end : segment.start;
1334
+ const endPoint = reverse ? segment.start : segment.end;
1335
+ const controlPoint = segment.control;
1336
+ const startWorld = this.lineLocalToWorld(line, startPoint.x, startPoint.y);
1337
+ const controlWorld = this.lineLocalToWorld(line, controlPoint.x, controlPoint.y);
1338
+ const endWorld = this.lineLocalToWorld(line, endPoint.x, endPoint.y);
1339
+ return `M ${startWorld.x} ${startWorld.y} Q ${controlWorld.x} ${controlWorld.y} ${endWorld.x} ${endWorld.y}`;
1340
+ }
1341
+ /**
1342
+ * Converts a KritzelPolygon to an array of corner points.
1343
+ *
1344
+ * @param polygon - The KritzelPolygon with topLeft, topRight, bottomRight, bottomLeft corners.
1345
+ * @returns Array of KritzelPoint in clockwise order starting from topLeft.
1346
+ */
1347
+ getPolygonPoints(polygon) {
1348
+ return [polygon.topLeft, polygon.topRight, polygon.bottomRight, polygon.bottomLeft];
1349
+ }
1350
+ /**
1351
+ * Gets the custom clip polygon for an object if it has one.
1352
+ * Used for non-rectangular shapes like ellipses and triangles that need
1353
+ * special edge detection instead of using the default bounding box.
1354
+ *
1355
+ * @param object - The object to check for a custom clip polygon.
1356
+ * @returns Array of KritzelPoint defining the clip boundary, or null for default behavior.
1357
+ */
1358
+ getClipPolygonForObject(object) {
1359
+ // Check if the object is a KritzelShape with a custom clip polygon
1360
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelShape')) {
1361
+ const shape = object;
1362
+ return shape.getClipPolygon();
1363
+ }
1364
+ // Path objects use getPathClipPoint instead of polygon
1365
+ return null;
1366
+ }
1367
+ /**
1368
+ * Gets the clip point for a Path object by finding the stroke edge intersection.
1369
+ * Path objects don't use polygon-based clipping; they use distance-to-stroke calculations.
1370
+ *
1371
+ * @param object - The object to check (only processes KritzelPath objects).
1372
+ * @param outsidePoint - A reference point outside the path to cast a ray from.
1373
+ * @returns The intersection point on the path's stroke edge, or null if not a path or no intersection.
1374
+ */
1375
+ getPathClipPoint(object, outsidePoint) {
1376
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelPath')) {
1377
+ const path = object;
1378
+ return path.getClipPoint(outsidePoint);
1379
+ }
1380
+ return null;
1381
+ }
1382
+ /**
1383
+ * Converts local line coordinates to world coordinates.
1384
+ * Applies the line's rotation, scale, and translation transformations
1385
+ * to convert from internal SVG coordinates to canvas world space.
1386
+ *
1387
+ * @param line - The KritzelLine providing transformation context.
1388
+ * @param localX - X coordinate in the line's local SVG space.
1389
+ * @param localY - Y coordinate in the line's local SVG space.
1390
+ * @returns Object with x and y coordinates in world space.
1391
+ */
1392
+ lineLocalToWorld(line, localX, localY) {
1393
+ const px = localX - line.x;
1394
+ const py = localY - line.y;
1395
+ const cx = line.totalWidth / 2;
1396
+ const cy = line.totalHeight / 2;
1397
+ const cos = Math.cos(line.rotation);
1398
+ const sin = Math.sin(line.rotation);
1399
+ const rotatedX = (px - cx) * cos - (py - cy) * sin + cx;
1400
+ const rotatedY = (px - cx) * sin + (py - cy) * cos + cy;
1401
+ const safeLineScale = this.getSafeScale(line.scale);
1402
+ return {
1403
+ x: rotatedX / safeLineScale + line.translateX,
1404
+ y: rotatedY / safeLineScale + line.translateY,
1405
+ };
1406
+ }
1407
+ /**
1408
+ * Converts world coordinates to line's local SVG coordinates.
1409
+ * Applies inverse transformations (scale, rotation, translation) to convert
1410
+ * from canvas world space to the line's internal SVG coordinate system.
1411
+ *
1412
+ * @param line - The KritzelLine providing transformation context.
1413
+ * @param worldX - X coordinate in world/canvas space.
1414
+ * @param worldY - Y coordinate in world/canvas space.
1415
+ * @returns Object with x and y coordinates in the line's local SVG space.
1416
+ */
1417
+ lineWorldToLocal(line, worldX, worldY) {
1418
+ const safeLineScale = this.getSafeScale(line.scale);
1419
+ const dx = (worldX - line.translateX) * safeLineScale;
1420
+ const dy = (worldY - line.translateY) * safeLineScale;
1421
+ const cx = line.totalWidth / 2;
1422
+ const cy = line.totalHeight / 2;
1423
+ const cos = Math.cos(-line.rotation);
1424
+ const sin = Math.sin(-line.rotation);
1425
+ const rotatedX = (dx - cx) * cos - (dy - cy) * sin + cx;
1426
+ const rotatedY = (dx - cx) * sin + (dy - cy) * cos + cy;
1427
+ return {
1428
+ x: rotatedX + line.x,
1429
+ y: rotatedY + line.y,
1430
+ };
1431
+ }
1432
+ getSafeScale(scale) {
1433
+ if (Number.isFinite(scale) && Math.abs(scale) > 1e-6) {
1434
+ return scale;
1435
+ }
1436
+ return 1;
1437
+ }
1438
+ areFiniteNumbers(...values) {
1439
+ return values.every(value => value !== undefined && Number.isFinite(value));
1440
+ }
1441
+ isFiniteAnchorVisualization(visualization) {
1442
+ if (!visualization) {
1443
+ return false;
1444
+ }
1445
+ if (!this.areFiniteNumbers(visualization.edgeX, visualization.edgeY, visualization.centerX, visualization.centerY)) {
1446
+ return false;
1447
+ }
1448
+ if (visualization.pathD && !this.isFinitePathData(visualization.pathD)) {
1449
+ return false;
1450
+ }
1451
+ return true;
1452
+ }
1453
+ isFinitePathData(pathD) {
1454
+ return !pathD.includes('Infinity') && !pathD.includes('NaN');
1455
+ }
1456
+ /**
1457
+ * Checks if an object can be used as an anchor target.
1458
+ * Excludes selection-related objects (SelectionBox, SelectionGroup) and
1459
+ * line objects (lines cannot anchor to other lines).
1460
+ *
1461
+ * @param object - The object to check for anchorability.
1462
+ * @returns True if the object can be anchored to, false otherwise.
1463
+ */
1464
+ isAnchorable(object) {
1465
+ // Exclude selection-related objects
1466
+ if (object instanceof KritzelSelectionBox || object instanceof KritzelSelectionGroup) {
1467
+ return false;
1468
+ }
1469
+ // Exclude line objects - lines cannot be anchored to other lines
1470
+ if (object instanceof KritzelLine) {
1471
+ return false;
1472
+ }
1473
+ // All other visible objects can be anchored to
1474
+ return object.isVisible;
1475
+ }
1476
+ }