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