@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,1103 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import { KritzelReviver } from "../core/reviver.class";
|
|
3
|
+
import { KritzelLine } from "../objects/line.class";
|
|
4
|
+
import { KritzelSelectionBox } from "../objects/selection-box.class";
|
|
5
|
+
import { KritzelSelectionGroup } from "../objects/selection-group.class";
|
|
6
|
+
import { DEFAULT_SYNC_CONFIG } from "../../configs/default-sync.config";
|
|
7
|
+
import { CURRENT_WORKSPACE_SCHEMA_VERSION } from "../../constants/schema.constants";
|
|
8
|
+
import { KritzelQuadtree } from "./quadtree.structure";
|
|
9
|
+
import { runMigrations } from "../../helpers/migration.helper";
|
|
10
|
+
import { WORKSPACE_MIGRATIONS } from "../../migrations/workspace.migrations";
|
|
11
|
+
/**
|
|
12
|
+
* A synchronized object map that stores and manages drawable objects on the canvas.
|
|
13
|
+
* This structure integrates with Yjs for real-time collaboration and persistence,
|
|
14
|
+
* uses a quadtree for efficient spatial queries, and provides undo/redo functionality.
|
|
15
|
+
*
|
|
16
|
+
* @template T - The type of objects stored, must extend KritzelBaseObject
|
|
17
|
+
*/
|
|
18
|
+
export class KritzelObjectMap {
|
|
19
|
+
quadtree;
|
|
20
|
+
_ydoc = null;
|
|
21
|
+
_objectsMap = null;
|
|
22
|
+
_metadataMap = null;
|
|
23
|
+
_providers = [];
|
|
24
|
+
_undoManager = null;
|
|
25
|
+
_reviver = null;
|
|
26
|
+
_core = null;
|
|
27
|
+
_workspaceId = null;
|
|
28
|
+
_isReady = false;
|
|
29
|
+
_temporaryItemsCount = 0;
|
|
30
|
+
_idMap = new Map();
|
|
31
|
+
_awareness = null;
|
|
32
|
+
_networkSyncPromise = null;
|
|
33
|
+
/** When true this map is a background worker: it never renders, emits, or wires awareness. */
|
|
34
|
+
_headless = false;
|
|
35
|
+
// Store observer/listener references for cleanup
|
|
36
|
+
_objectsObserver = null;
|
|
37
|
+
_stackItemAddedHandler = null;
|
|
38
|
+
_stackItemPoppedHandler = null;
|
|
39
|
+
_awarenessChangeHandler = null;
|
|
40
|
+
_awarenessChangeCallbacks = [];
|
|
41
|
+
_objectsChangeCallbacks = [];
|
|
42
|
+
_lastAwarenessEmitTime = 0;
|
|
43
|
+
_awarenessEmitTimeout = null;
|
|
44
|
+
AWARENESS_THROTTLE_INTERVAL = 100; // milliseconds
|
|
45
|
+
/**
|
|
46
|
+
* When true, update() only modifies local state (quadtree + idMap)
|
|
47
|
+
* without writing to Yjs. Used during drag operations to avoid
|
|
48
|
+
* redundant full serializations of every child object per frame.
|
|
49
|
+
*/
|
|
50
|
+
_localOnlyMode = false;
|
|
51
|
+
/**
|
|
52
|
+
* Tracks whether we're currently executing inside a `transaction()`
|
|
53
|
+
* callback. While true, `markUndoBoundary()` is a no-op so nested
|
|
54
|
+
* insert/remove calls don't split the in-progress undo step.
|
|
55
|
+
*/
|
|
56
|
+
_inTransaction = false;
|
|
57
|
+
/**
|
|
58
|
+
* Indicates whether the object map has been initialized and is ready for use.
|
|
59
|
+
* @returns `true` if providers are connected and the map is operational
|
|
60
|
+
*/
|
|
61
|
+
get isReady() {
|
|
62
|
+
return this._isReady;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Promise that settles when network providers finish connecting.
|
|
66
|
+
* Resolves immediately if no network providers. Awaited by the loading overlay.
|
|
67
|
+
*/
|
|
68
|
+
whenNetworkSynced() {
|
|
69
|
+
return this._networkSyncPromise ?? Promise.resolve();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether this map runs detached from the rendered canvas.
|
|
73
|
+
*/
|
|
74
|
+
get isHeadless() {
|
|
75
|
+
return this._headless;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Returns the Yjs Awareness instance, if a network provider is available.
|
|
79
|
+
*/
|
|
80
|
+
get awareness() {
|
|
81
|
+
return this._awareness;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Whether a network awareness instance is available.
|
|
85
|
+
*/
|
|
86
|
+
get hasAwareness() {
|
|
87
|
+
return !!this._awareness;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Returns the local client ID from the awareness instance.
|
|
91
|
+
* Used to filter out the local user when rendering remote cursors.
|
|
92
|
+
*/
|
|
93
|
+
get localClientId() {
|
|
94
|
+
return this._awareness?.clientID ?? null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Sets the local user identity in the awareness state.
|
|
98
|
+
* This broadcasts the user's identity to all connected peers.
|
|
99
|
+
*/
|
|
100
|
+
setLocalUser(user) {
|
|
101
|
+
if (!this._awareness || !user) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const displayName = user.displayName || user.firstName || 'Anonymous';
|
|
105
|
+
this._awareness.setLocalStateField('user', {
|
|
106
|
+
id: user.id,
|
|
107
|
+
displayName,
|
|
108
|
+
color: user.color || this.generateColorFromName(displayName),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
generateColorFromName(name) {
|
|
112
|
+
let hash = 0;
|
|
113
|
+
for (let i = 0; i < name.length; i++) {
|
|
114
|
+
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
115
|
+
}
|
|
116
|
+
const hue = Math.abs(hash % 360);
|
|
117
|
+
return `hsl(${hue}, 45%, 55%)`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Updates the local cursor position in the awareness state.
|
|
121
|
+
* This broadcasts the cursor position to all connected peers.
|
|
122
|
+
*/
|
|
123
|
+
updateCursorPosition(x, y) {
|
|
124
|
+
if (!this._awareness) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this._awareness.setLocalStateField('cursor', { x, y });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Clears the local cursor position (e.g., when the pointer leaves the canvas).
|
|
131
|
+
*/
|
|
132
|
+
clearCursorPosition() {
|
|
133
|
+
if (!this._awareness) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this._awareness.setLocalStateField('cursor', null);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Sets the ID of the object currently being drawn by the local user.
|
|
140
|
+
* Remote clients use this to derive cursor position from the object's latest
|
|
141
|
+
* coordinates instead of the throttled awareness cursor, reducing desync.
|
|
142
|
+
* Pass `null` when drawing ends.
|
|
143
|
+
*/
|
|
144
|
+
setActiveDrawingObject(objectId) {
|
|
145
|
+
if (!this._awareness) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this._awareness.setLocalStateField('activeObjectId', objectId);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Broadcasts the local user's selection box bounds via awareness.
|
|
152
|
+
* Remote clients use this to render the selection rectangle in the user's color.
|
|
153
|
+
*/
|
|
154
|
+
setLocalSelectionBox(box) {
|
|
155
|
+
if (!this._awareness) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
this._awareness.setLocalStateField('selectionBox', box);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Clears the local selection box from awareness (e.g., when selection completes or is cancelled).
|
|
162
|
+
*/
|
|
163
|
+
clearLocalSelectionBox() {
|
|
164
|
+
if (!this._awareness) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this._awareness.setLocalStateField('selectionBox', null);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Removes selection groups whose owner is no longer present in awareness.
|
|
171
|
+
* Called when remote clients disconnect to prevent orphaned selection groups
|
|
172
|
+
* from persisting in the workspace state.
|
|
173
|
+
*/
|
|
174
|
+
removeOrphanedSelectionGroups() {
|
|
175
|
+
if (!this._awareness) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const states = this._awareness.getStates();
|
|
179
|
+
const activeUserIds = new Set();
|
|
180
|
+
states.forEach(state => {
|
|
181
|
+
const userId = state.user?.id;
|
|
182
|
+
if (userId) {
|
|
183
|
+
activeUserIds.add(userId);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
const localUserId = this._core?.user?.id;
|
|
187
|
+
const orphanedGroups = this.quadtree.filter(o => o instanceof KritzelSelectionGroup
|
|
188
|
+
&& o.userId != null
|
|
189
|
+
&& o.userId !== localUserId
|
|
190
|
+
&& !activeUserIds.has(o.userId));
|
|
191
|
+
for (const group of orphanedGroups) {
|
|
192
|
+
this.quadtree.remove(o => o.id === group.id);
|
|
193
|
+
this._idMap.delete(group.id);
|
|
194
|
+
if (this._objectsMap) {
|
|
195
|
+
this._ydoc.transact(() => {
|
|
196
|
+
this._objectsMap.delete(group.id);
|
|
197
|
+
}, 'local');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (orphanedGroups.length > 0) {
|
|
201
|
+
this._core?.store.invalidateSelectionCache();
|
|
202
|
+
this.requestRerender();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** Rerenders the canvas unless this map is running headless. */
|
|
206
|
+
requestRerender() {
|
|
207
|
+
if (this._headless) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this._core?.rerender();
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Registers a callback to be invoked when the awareness state changes.
|
|
214
|
+
* The callback receives the full awareness states map.
|
|
215
|
+
*/
|
|
216
|
+
onAwarenessChange(callback) {
|
|
217
|
+
this._awarenessChangeCallbacks.push(callback);
|
|
218
|
+
return () => {
|
|
219
|
+
this._awarenessChangeCallbacks = this._awarenessChangeCallbacks.filter(existingCallback => existingCallback !== callback);
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Registers a callback to be invoked when remote object changes are received.
|
|
224
|
+
* Used by the awareness cursors component to re-derive cursor positions
|
|
225
|
+
* from the latest object data when a remote user is actively drawing.
|
|
226
|
+
*/
|
|
227
|
+
onObjectsChange(callback) {
|
|
228
|
+
this._objectsChangeCallbacks.push(callback);
|
|
229
|
+
return () => {
|
|
230
|
+
this._objectsChangeCallbacks = this._objectsChangeCallbacks.filter(existingCallback => existingCallback !== callback);
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Returns the Yjs UndoManager instance for managing undo/redo operations.
|
|
235
|
+
* @returns The UndoManager instance, or `null` if not initialized
|
|
236
|
+
*/
|
|
237
|
+
get undoManager() {
|
|
238
|
+
return this._undoManager;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Returns the unique identifier of the workspace this object map belongs to.
|
|
242
|
+
* @returns The workspace ID, or `null` if not initialized
|
|
243
|
+
*/
|
|
244
|
+
get workspaceId() {
|
|
245
|
+
return this._workspaceId;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Returns the current state of the undo/redo system.
|
|
249
|
+
* @returns An object containing undo/redo availability and stack sizes
|
|
250
|
+
*/
|
|
251
|
+
get undoState() {
|
|
252
|
+
return {
|
|
253
|
+
canUndo: this._undoManager ? this._undoManager.canUndo() : false,
|
|
254
|
+
canRedo: this._undoManager ? this._undoManager.canRedo() : false,
|
|
255
|
+
undoStackSize: this._undoManager ? this._undoManager.undoStack.length : 0,
|
|
256
|
+
redoStackSize: this._undoManager ? this._undoManager.redoStack.length : 0,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Returns the total number of objects stored in the map.
|
|
261
|
+
* @returns The count of all objects in the quadtree
|
|
262
|
+
*/
|
|
263
|
+
get totalCount() {
|
|
264
|
+
return this.quadtree.size;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Retrieves an object from the map by its ID in O(1) time.
|
|
268
|
+
* @param id - The ID of the object to find
|
|
269
|
+
* @returns The object if found, or undefined
|
|
270
|
+
*/
|
|
271
|
+
findById(id) {
|
|
272
|
+
return this._idMap.get(id);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Creates a new KritzelObjectMap instance with an empty quadtree.
|
|
276
|
+
* The map must be initialized with `initialize()` before use.
|
|
277
|
+
*/
|
|
278
|
+
constructor() {
|
|
279
|
+
this.quadtree = new KritzelQuadtree({
|
|
280
|
+
x: -Infinity,
|
|
281
|
+
y: -Infinity,
|
|
282
|
+
z: 0,
|
|
283
|
+
width: Infinity,
|
|
284
|
+
height: Infinity,
|
|
285
|
+
}, 8);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Initializes the object map with Yjs document, sync providers, and undo manager.
|
|
289
|
+
* This must be called before using any other methods on the object map.
|
|
290
|
+
*
|
|
291
|
+
* @param core - The KritzelCore instance that owns this object map
|
|
292
|
+
* @param workspaceId - A unique identifier for the workspace, used to namespace the Yjs document
|
|
293
|
+
* @param config - Optional sync configuration; defaults to DEFAULT_SYNC_CONFIG if not provided
|
|
294
|
+
* @param options - Optional behavior flags
|
|
295
|
+
* @param options.headless - Run detached from the rendered canvas (no rerenders, events, or awareness)
|
|
296
|
+
* @param options.workspaceName - Workspace name to write into document metadata; defaults to the active workspace's name
|
|
297
|
+
* @returns A promise that resolves when all providers are connected and the map is ready
|
|
298
|
+
*/
|
|
299
|
+
async initialize(core, workspaceId, config, options) {
|
|
300
|
+
this._core = core;
|
|
301
|
+
this._workspaceId = workspaceId;
|
|
302
|
+
this._headless = options?.headless ?? false;
|
|
303
|
+
this._reviver = new KritzelReviver(core);
|
|
304
|
+
// Create a dedicated Y.Doc for this workspace
|
|
305
|
+
this._ydoc = new Y.Doc();
|
|
306
|
+
this._objectsMap = this._ydoc.getMap('objects');
|
|
307
|
+
this._metadataMap = this._ydoc.getMap('metadata');
|
|
308
|
+
const docName = core.editorId ? `kritzel-workspace-${core.editorId}-${workspaceId}` : `kritzel-workspace-${workspaceId}`;
|
|
309
|
+
const finalConfig = config ?? DEFAULT_SYNC_CONFIG;
|
|
310
|
+
// Instantiate providers from configuration
|
|
311
|
+
const showSyncProviderInfo = !!core.store?.state?.debugInfo?.showSyncProviderInfo;
|
|
312
|
+
const quiet = !showSyncProviderInfo;
|
|
313
|
+
for (const providerConfig of finalConfig.providers) {
|
|
314
|
+
let provider;
|
|
315
|
+
// Check if it's a class constructor or a factory
|
|
316
|
+
if (typeof providerConfig === 'function') {
|
|
317
|
+
// It's a class constructor
|
|
318
|
+
provider = new providerConfig(docName, this._ydoc, { quiet });
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
// It's a factory with a create method
|
|
322
|
+
provider = providerConfig.create(docName, this._ydoc, { quiet });
|
|
323
|
+
}
|
|
324
|
+
this._providers.push(provider);
|
|
325
|
+
}
|
|
326
|
+
if (showSyncProviderInfo) {
|
|
327
|
+
const providerList = this._providers.map(p => `${p.constructor.name} (${p.type})`).join(', ');
|
|
328
|
+
console.info(`[Kritzel] Workspace sync providers initialized for ${docName}: ${providerList || 'none'}`);
|
|
329
|
+
}
|
|
330
|
+
// captureTimeout is effectively infinite — undo-step boundaries are
|
|
331
|
+
// marked explicitly via markUndoBoundary() in insert/remove/reset and
|
|
332
|
+
// at stroke/gesture boundaries in the tools. This prevents a single
|
|
333
|
+
// brush stroke from being split when the user pauses after pointerdown.
|
|
334
|
+
this._undoManager = new Y.UndoManager([this._objectsMap], {
|
|
335
|
+
captureTimeout: Number.MAX_SAFE_INTEGER,
|
|
336
|
+
trackedOrigins: new Set(['local', 'temporary']),
|
|
337
|
+
ignoreRemoteMapChanges: true,
|
|
338
|
+
});
|
|
339
|
+
// Store handler references for cleanup
|
|
340
|
+
this._stackItemAddedHandler = event => {
|
|
341
|
+
if (event.type === 'undo') {
|
|
342
|
+
// Track if this was a temporary item
|
|
343
|
+
if (event.origin === 'temporary') {
|
|
344
|
+
this._temporaryItemsCount++;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
this._stackItemPoppedHandler = event => {
|
|
349
|
+
if (event.type === 'undo') {
|
|
350
|
+
// Reduce temporary count when items are undone
|
|
351
|
+
if (event.origin === 'temporary' && this._temporaryItemsCount > 0) {
|
|
352
|
+
this._temporaryItemsCount--;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
this._undoManager.on('stack-item-added', this._stackItemAddedHandler);
|
|
357
|
+
this._undoManager.on('stack-item-popped', this._stackItemPoppedHandler);
|
|
358
|
+
// Observe changes to objects and sync with application state
|
|
359
|
+
this._objectsObserver = event => {
|
|
360
|
+
this.handleObjectsChange(event);
|
|
361
|
+
};
|
|
362
|
+
this._objectsMap.observe(this._objectsObserver);
|
|
363
|
+
// Separate local providers (IndexedDB, BroadcastChannel) from network providers (Hocuspocus, WebSocket)
|
|
364
|
+
// Local providers are awaited so data is available immediately; network providers sync in the background
|
|
365
|
+
// via Yjs observers that are already registered above.
|
|
366
|
+
const localProviders = this._providers.filter(p => p.type === 'local');
|
|
367
|
+
const networkProviders = this._providers.filter(p => p.type === 'network');
|
|
368
|
+
// Await local providers for immediate data availability
|
|
369
|
+
const localResults = await Promise.allSettled(localProviders.map(p => p.connect()));
|
|
370
|
+
if (showSyncProviderInfo) {
|
|
371
|
+
const connectedLocalCount = localResults.filter(result => result.status === 'fulfilled').length;
|
|
372
|
+
console.info(`[Kritzel] Workspace local providers connected: ${connectedLocalCount}/${localProviders.length}`);
|
|
373
|
+
}
|
|
374
|
+
localResults.forEach((result, i) => {
|
|
375
|
+
if (showSyncProviderInfo && result.status === 'rejected') {
|
|
376
|
+
console.error(`[Kritzel] Sync provider "${localProviders[i]?.constructor.name}" failed to connect:`, result.reason);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
// Connect network providers in the background (remote data arrives via Yjs observers)
|
|
380
|
+
if (showSyncProviderInfo && networkProviders.length > 0) {
|
|
381
|
+
console.info(`[Kritzel] Workspace network providers connecting in background: ${networkProviders.length}`);
|
|
382
|
+
}
|
|
383
|
+
const networkConnectPromises = networkProviders.map(provider => provider.connect().catch(err => {
|
|
384
|
+
if (showSyncProviderInfo) {
|
|
385
|
+
console.error(`[Kritzel] Network sync provider "${provider.constructor.name}" failed to connect:`, err);
|
|
386
|
+
}
|
|
387
|
+
}));
|
|
388
|
+
// Network sync awaited by loading overlay to show remote workspace hydration
|
|
389
|
+
this._networkSyncPromise = networkConnectPromises.length > 0 ? Promise.allSettled(networkConnectPromises).then(() => undefined) : null;
|
|
390
|
+
this._isReady = true;
|
|
391
|
+
// Find the first provider that exposes awareness (network providers)
|
|
392
|
+
// Awareness is available immediately after provider construction, before connect() resolves
|
|
393
|
+
for (const provider of networkProviders) {
|
|
394
|
+
if (provider.awareness) {
|
|
395
|
+
this._awareness = provider.awareness;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Subscribe to awareness changes
|
|
400
|
+
if (this._awareness && !this._headless) {
|
|
401
|
+
this._awarenessChangeHandler = (change) => {
|
|
402
|
+
// Clean up selection groups belonging to disconnected users
|
|
403
|
+
if (change.removed.length > 0) {
|
|
404
|
+
this.removeOrphanedSelectionGroups();
|
|
405
|
+
}
|
|
406
|
+
const now = Date.now();
|
|
407
|
+
const timeSinceLastEmit = now - this._lastAwarenessEmitTime;
|
|
408
|
+
// Clear any pending timeout since we have a new event
|
|
409
|
+
if (this._awarenessEmitTimeout !== null) {
|
|
410
|
+
clearTimeout(this._awarenessEmitTimeout);
|
|
411
|
+
this._awarenessEmitTimeout = null;
|
|
412
|
+
}
|
|
413
|
+
if (timeSinceLastEmit >= this.AWARENESS_THROTTLE_INTERVAL) {
|
|
414
|
+
// Enough time has passed, emit immediately
|
|
415
|
+
this._lastAwarenessEmitTime = now;
|
|
416
|
+
const states = this._awareness.getStates();
|
|
417
|
+
for (const callback of this._awarenessChangeCallbacks) {
|
|
418
|
+
callback(states);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
// Schedule emission for the remaining time
|
|
423
|
+
const delayMs = this.AWARENESS_THROTTLE_INTERVAL - timeSinceLastEmit;
|
|
424
|
+
this._awarenessEmitTimeout = setTimeout(() => {
|
|
425
|
+
this._lastAwarenessEmitTime = Date.now();
|
|
426
|
+
this._awarenessEmitTimeout = null;
|
|
427
|
+
const states = this._awareness.getStates();
|
|
428
|
+
for (const callback of this._awarenessChangeCallbacks) {
|
|
429
|
+
callback(states);
|
|
430
|
+
}
|
|
431
|
+
}, delayMs);
|
|
432
|
+
if (typeof this._awarenessEmitTimeout.unref === 'function') {
|
|
433
|
+
this._awarenessEmitTimeout.unref();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
this._awareness.on('change', this._awarenessChangeHandler);
|
|
438
|
+
}
|
|
439
|
+
// Initialize document metadata (only if not already set by a previous client)
|
|
440
|
+
const workspaceName = options?.workspaceName ?? core.store?.state?.activeWorkspace?.name ?? '';
|
|
441
|
+
this.initializeMetadata('workspace', workspaceId, workspaceName);
|
|
442
|
+
// Run any pending schema migrations before loading data
|
|
443
|
+
const quietMigrations = !core.store?.state?.debugInfo?.showMigrationInfo;
|
|
444
|
+
runMigrations(this._ydoc, this._metadataMap, { objects: this._objectsMap }, WORKSPACE_MIGRATIONS, CURRENT_WORKSPACE_SCHEMA_VERSION, { quiet: quietMigrations });
|
|
445
|
+
// Load objects from Yjs
|
|
446
|
+
this.loadFromYjs();
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Handles changes to the Yjs objects map, synchronizing remote changes with local state.
|
|
450
|
+
* For local changes, only triggers a re-render. For remote changes, deserializes objects
|
|
451
|
+
* and updates the quadtree, handling SelectionGroups specially to maintain correct dimensions.
|
|
452
|
+
*
|
|
453
|
+
* @param event - The Yjs map event containing information about changed keys and their actions
|
|
454
|
+
*/
|
|
455
|
+
handleObjectsChange(event) {
|
|
456
|
+
// Skip Map updates for local changes (already done), but still trigger re-render
|
|
457
|
+
// 'temporary' is also a local change that shouldn't be re-deserialized
|
|
458
|
+
if (event.transaction.origin === 'local' || event.transaction.origin === 'temporary') {
|
|
459
|
+
this.requestRerender();
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
// Skip migration transactions — data will be loaded fresh via loadFromYjs()
|
|
463
|
+
if (event.transaction.origin === 'migration') {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const changedKeys = Array.from(event.keysChanged);
|
|
467
|
+
const objectsToUpdate = [];
|
|
468
|
+
const selectionGroupsToUpdate = [];
|
|
469
|
+
const objectsToDelete = [];
|
|
470
|
+
// The reverse anchor index (targetObjectId -> anchored lines) is derived runtime
|
|
471
|
+
// state that is NOT synced via Yjs. When line anchor connections arrive through a
|
|
472
|
+
// remote change it must be rebuilt, otherwise getLinesAnchoredTo/updateAnchorsForObject
|
|
473
|
+
// return stale/empty results on this client (e.g. anchored lines fail to follow a
|
|
474
|
+
// locally-moved target, leaving selection UI and anchors visually offset).
|
|
475
|
+
let anchorIndexNeedsRebuild = false;
|
|
476
|
+
changedKeys.forEach(key => {
|
|
477
|
+
const change = event.changes.keys.get(key);
|
|
478
|
+
if (change && change.action === 'delete') {
|
|
479
|
+
objectsToDelete.push(key);
|
|
480
|
+
if (this._idMap.get(key) instanceof KritzelLine) {
|
|
481
|
+
anchorIndexNeedsRebuild = true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
const serialized = this._objectsMap?.get(key);
|
|
486
|
+
if (serialized) {
|
|
487
|
+
const object = this._reviver?.revive(serialized);
|
|
488
|
+
// Separate SelectionGroups to process them after regular objects
|
|
489
|
+
if (object instanceof KritzelSelectionGroup) {
|
|
490
|
+
selectionGroupsToUpdate.push(object);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
objectsToUpdate.push(object);
|
|
494
|
+
if (object instanceof KritzelLine) {
|
|
495
|
+
anchorIndexNeedsRebuild = true;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
// Delete objects from local map
|
|
502
|
+
objectsToDelete.forEach(objectId => {
|
|
503
|
+
const existingObject = this._idMap.get(objectId);
|
|
504
|
+
if (existingObject instanceof KritzelSelectionGroup) {
|
|
505
|
+
// Remote deletion of a selection group must also clear the transient
|
|
506
|
+
// selection flags it had applied to its children locally.
|
|
507
|
+
existingObject.deselectAllChildren?.();
|
|
508
|
+
}
|
|
509
|
+
this.quadtree.remove(o => o.id === objectId);
|
|
510
|
+
this._idMap.delete(objectId);
|
|
511
|
+
});
|
|
512
|
+
// Track which objects are added vs updated for granular events
|
|
513
|
+
const addedObjects = [];
|
|
514
|
+
const updatedObjects = [];
|
|
515
|
+
// First, update or insert regular objects
|
|
516
|
+
objectsToUpdate.forEach(object => {
|
|
517
|
+
const existed = this._idMap.has(object.id);
|
|
518
|
+
if (existed) {
|
|
519
|
+
const previous = this._idMap.get(object.id);
|
|
520
|
+
if (previous && typeof object.adoptTransientStateFrom === 'function') {
|
|
521
|
+
object.adoptTransientStateFrom(previous);
|
|
522
|
+
}
|
|
523
|
+
this.quadtree.reindex(object);
|
|
524
|
+
this._idMap.set(object.id, object);
|
|
525
|
+
updatedObjects.push(object);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
this.quadtree.insert(object);
|
|
529
|
+
this._idMap.set(object.id, object);
|
|
530
|
+
addedObjects.push(object);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
// Then, update or insert SelectionGroups
|
|
534
|
+
const updatedSelectionGroupIds = new Set();
|
|
535
|
+
selectionGroupsToUpdate.forEach(object => {
|
|
536
|
+
const existed = this._idMap.has(object.id);
|
|
537
|
+
if (existed) {
|
|
538
|
+
const previous = this._idMap.get(object.id);
|
|
539
|
+
if (previous && typeof object.adoptTransientStateFrom === 'function') {
|
|
540
|
+
object.adoptTransientStateFrom(previous);
|
|
541
|
+
}
|
|
542
|
+
this.quadtree.reindex(object);
|
|
543
|
+
this._idMap.set(object.id, object);
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
this.quadtree.insert(object);
|
|
547
|
+
this._idMap.set(object.id, object);
|
|
548
|
+
}
|
|
549
|
+
updatedSelectionGroupIds.add(object.id);
|
|
550
|
+
// After undo/redo, the SelectionGroup's dimensions may be stale because:
|
|
551
|
+
// 1. The serialized state contains the old bounding box
|
|
552
|
+
// 2. The child objects have been reverted to their previous positions
|
|
553
|
+
// We need to invalidate the cache and recalculate dimensions
|
|
554
|
+
// skipPersist=true prevents creating new undo entries
|
|
555
|
+
object.invalidateObjectsCache();
|
|
556
|
+
object.refreshObjectDimensions(undefined, true);
|
|
557
|
+
object.captureUnchangedSnapshots();
|
|
558
|
+
// Update quadtree again with corrected dimensions
|
|
559
|
+
this.quadtree.reindex(object);
|
|
560
|
+
});
|
|
561
|
+
// If regular objects were changed but their SelectionGroup wasn't explicitly updated,
|
|
562
|
+
// we still need to refresh that SelectionGroup's dimensions
|
|
563
|
+
if (objectsToUpdate.length > 0 || objectsToDelete.length > 0) {
|
|
564
|
+
const changedObjectIds = new Set([
|
|
565
|
+
...objectsToUpdate.map(o => o.id),
|
|
566
|
+
...objectsToDelete
|
|
567
|
+
]);
|
|
568
|
+
// Find any existing SelectionGroups that contain changed objects
|
|
569
|
+
const existingSelectionGroups = this.quadtree.filter(o => o instanceof KritzelSelectionGroup && !updatedSelectionGroupIds.has(o.id));
|
|
570
|
+
for (const selectionGroup of existingSelectionGroups) {
|
|
571
|
+
const hasChangedChildren = selectionGroup.objectIds.some(id => changedObjectIds.has(id));
|
|
572
|
+
if (hasChangedChildren) {
|
|
573
|
+
selectionGroup.invalidateObjectsCache();
|
|
574
|
+
selectionGroup.refreshObjectDimensions(undefined, true);
|
|
575
|
+
selectionGroup.captureUnchangedSnapshots();
|
|
576
|
+
// Update quadtree with corrected dimensions
|
|
577
|
+
this.quadtree.reindex(selectionGroup);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (this._headless) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
// Rebuild the anchor index if any line anchor connections may have changed, so that
|
|
585
|
+
// subsequent local operations (moving a target, refreshing selection UI) resolve the
|
|
586
|
+
// correct anchored lines on this client.
|
|
587
|
+
if (anchorIndexNeedsRebuild) {
|
|
588
|
+
this._core?.anchorManager.rebuildIndex();
|
|
589
|
+
}
|
|
590
|
+
// Invalidate the store's selection cache so it fetches the updated SelectionGroup
|
|
591
|
+
this._core?.store.invalidateSelectionCache();
|
|
592
|
+
this._core?.rerender();
|
|
593
|
+
// Emit granular lifecycle events
|
|
594
|
+
if (addedObjects.length > 0) {
|
|
595
|
+
this._core?.engine.emitObjectsAdded(addedObjects);
|
|
596
|
+
}
|
|
597
|
+
if (objectsToDelete.length > 0) {
|
|
598
|
+
// For deleted objects we only have IDs, not the full objects
|
|
599
|
+
// Create minimal object references with the IDs
|
|
600
|
+
const removedObjects = objectsToDelete.map(id => ({ id }));
|
|
601
|
+
this._core?.engine.emitObjectsRemoved(removedObjects);
|
|
602
|
+
}
|
|
603
|
+
if (updatedObjects.length > 0) {
|
|
604
|
+
this._core?.engine.emitObjectsUpdated(updatedObjects.map(obj => ({ object: obj, changedProperties: [] })));
|
|
605
|
+
}
|
|
606
|
+
// Notify subscribers of remote object changes (e.g., awareness cursors)
|
|
607
|
+
for (const callback of this._objectsChangeCallbacks) {
|
|
608
|
+
callback();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Initializes document metadata if not already set.
|
|
613
|
+
* Uses 'metadata' transaction origin which is NOT tracked by the UndoManager.
|
|
614
|
+
*/
|
|
615
|
+
initializeMetadata(type, workspaceId, workspaceName) {
|
|
616
|
+
if (!this._metadataMap || !this._ydoc) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
this._ydoc.transact(() => {
|
|
620
|
+
if (!this._metadataMap?.get('type')) {
|
|
621
|
+
this._metadataMap?.set('type', type);
|
|
622
|
+
}
|
|
623
|
+
if (this._metadataMap?.get('isPublic') === undefined) {
|
|
624
|
+
this._metadataMap?.set('isPublic', false);
|
|
625
|
+
}
|
|
626
|
+
if (this._metadataMap?.get('schemaVersion') === undefined) {
|
|
627
|
+
this._metadataMap?.set('schemaVersion', CURRENT_WORKSPACE_SCHEMA_VERSION);
|
|
628
|
+
}
|
|
629
|
+
this._metadataMap?.set('workspaceId', workspaceId);
|
|
630
|
+
this._metadataMap?.set('workspaceName', workspaceName);
|
|
631
|
+
}, 'metadata');
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Returns whether the document is publicly accessible.
|
|
635
|
+
*/
|
|
636
|
+
getIsPublic() {
|
|
637
|
+
return this._metadataMap?.get('isPublic') ?? false;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Sets whether the document is publicly accessible.
|
|
641
|
+
* This change is synced to all connected clients and the server,
|
|
642
|
+
* but is NOT tracked by the UndoManager.
|
|
643
|
+
*/
|
|
644
|
+
setIsPublic(isPublic) {
|
|
645
|
+
if (!this._metadataMap || !this._ydoc) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
this._ydoc?.transact(() => {
|
|
649
|
+
this._metadataMap?.set('isPublic', isPublic);
|
|
650
|
+
}, 'metadata');
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Returns the document type stored in metadata.
|
|
654
|
+
*/
|
|
655
|
+
getDocumentType() {
|
|
656
|
+
return this._metadataMap?.get('type') ?? 'workspace';
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Returns the workspace name stored in metadata.
|
|
660
|
+
*/
|
|
661
|
+
getWorkspaceName() {
|
|
662
|
+
return this._metadataMap?.get('workspaceName') ?? '';
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Sets the workspace name in metadata.
|
|
666
|
+
* This change is synced to all connected clients and the server,
|
|
667
|
+
* but is NOT tracked by the UndoManager.
|
|
668
|
+
*/
|
|
669
|
+
setWorkspaceName(name) {
|
|
670
|
+
if (!this._metadataMap || !this._ydoc) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
this._ydoc?.transact(() => {
|
|
674
|
+
this._metadataMap?.set('workspaceName', name);
|
|
675
|
+
}, 'metadata');
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Returns the workspace ID stored in metadata.
|
|
679
|
+
*/
|
|
680
|
+
getMetadataWorkspaceId() {
|
|
681
|
+
return this._metadataMap?.get('workspaceId') ?? '';
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Executes a callback within a Yjs transaction, grouping multiple operations
|
|
685
|
+
* into a single undo step. All changes made within the callback will be
|
|
686
|
+
* tracked as a single 'local' origin transaction.
|
|
687
|
+
*
|
|
688
|
+
* @param callback - The function containing operations to execute within the transaction
|
|
689
|
+
*/
|
|
690
|
+
transaction(callback) {
|
|
691
|
+
if (this._ydoc) {
|
|
692
|
+
this._inTransaction = true;
|
|
693
|
+
try {
|
|
694
|
+
this._ydoc.transact(callback, 'local');
|
|
695
|
+
}
|
|
696
|
+
finally {
|
|
697
|
+
this._inTransaction = false;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Closes the current undo stack item so the next tracked change starts
|
|
703
|
+
* a new one. Call at gesture/stroke boundaries (e.g. drag/resize/rotate
|
|
704
|
+
* end, brush/line/shape stroke end).
|
|
705
|
+
*
|
|
706
|
+
* No-op while a transaction() is open.
|
|
707
|
+
*/
|
|
708
|
+
stopUndoCapturing() {
|
|
709
|
+
this.markUndoBoundary();
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Internal: closes the current undo stack item. No-op while inside a
|
|
713
|
+
* transaction() so wrapped multi-op work stays a single undo step.
|
|
714
|
+
*/
|
|
715
|
+
markUndoBoundary() {
|
|
716
|
+
if (this._inTransaction) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
this._undoManager?.stopCapturing();
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Executes a callback where all update() calls only modify local state
|
|
723
|
+
* (quadtree + idMap) without writing to Yjs. This avoids expensive full
|
|
724
|
+
* serializations during continuous operations like dragging many objects.
|
|
725
|
+
*
|
|
726
|
+
* Objects updated inside this callback must be persisted later
|
|
727
|
+
* (e.g., via a normal update() call at drag end).
|
|
728
|
+
*
|
|
729
|
+
* @param callback - The function containing operations to execute locally
|
|
730
|
+
*/
|
|
731
|
+
withLocalUpdatesOnly(callback) {
|
|
732
|
+
this._localOnlyMode = true;
|
|
733
|
+
try {
|
|
734
|
+
callback();
|
|
735
|
+
}
|
|
736
|
+
finally {
|
|
737
|
+
this._localOnlyMode = false;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Loads all objects from the Yjs objects map into the local quadtree.
|
|
742
|
+
* Clears the existing quadtree and repopulates it by deserializing each
|
|
743
|
+
* object stored in the Yjs map.
|
|
744
|
+
*/
|
|
745
|
+
loadFromYjs() {
|
|
746
|
+
if (!this._objectsMap || !this._reviver) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
this.quadtree.reset();
|
|
750
|
+
this._idMap.clear();
|
|
751
|
+
const localUserId = this._core?.user?.id;
|
|
752
|
+
const staleSelectionGroupIds = [];
|
|
753
|
+
this._objectsMap.forEach((serialized, key) => {
|
|
754
|
+
const object = this._reviver?.revive(serialized);
|
|
755
|
+
// Remove remote selection groups on startup — they are transient UI state
|
|
756
|
+
// that should not survive an app restart. The owning user's session is gone.
|
|
757
|
+
if (object instanceof KritzelSelectionGroup && object.userId != null && object.userId !== localUserId) {
|
|
758
|
+
staleSelectionGroupIds.push(key);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (!object) {
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
this.quadtree.insert(object);
|
|
765
|
+
this._idMap.set(object.id, object);
|
|
766
|
+
});
|
|
767
|
+
// Clean up stale remote selection groups from Yjs
|
|
768
|
+
if (staleSelectionGroupIds.length > 0) {
|
|
769
|
+
this._ydoc.transact(() => {
|
|
770
|
+
for (const id of staleSelectionGroupIds) {
|
|
771
|
+
this._objectsMap.delete(id);
|
|
772
|
+
}
|
|
773
|
+
}, 'local');
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Resets the object map by clearing both the local quadtree and the Yjs objects map.
|
|
778
|
+
* This removes all objects from the workspace. The operation is tracked as a 'local'
|
|
779
|
+
* transaction for undo/redo purposes.
|
|
780
|
+
*/
|
|
781
|
+
reset() {
|
|
782
|
+
this.quadtree.reset();
|
|
783
|
+
this._idMap.clear();
|
|
784
|
+
this.markUndoBoundary();
|
|
785
|
+
this._ydoc?.transact(() => {
|
|
786
|
+
this._objectsMap?.clear();
|
|
787
|
+
}, 'local');
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Inserts a new object into both the local quadtree and the Yjs objects map.
|
|
791
|
+
* The object must have a valid ID. Non-persistable objects (like SelectionBox)
|
|
792
|
+
* are only added to the quadtree, not to Yjs.
|
|
793
|
+
*
|
|
794
|
+
* @param object - The object to insert; must have a valid `id` property
|
|
795
|
+
* @returns `true` if the object was successfully inserted, `false` if the object has no ID
|
|
796
|
+
*/
|
|
797
|
+
insert(object) {
|
|
798
|
+
if (!object.id) {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
this.quadtree.insert(object);
|
|
802
|
+
this._idMap.set(object.id, object);
|
|
803
|
+
if (this._objectsMap && this.isPersistable(object)) {
|
|
804
|
+
const serialized = object.serialize();
|
|
805
|
+
this.markUndoBoundary();
|
|
806
|
+
this._ydoc?.transact(() => {
|
|
807
|
+
this._objectsMap?.set(object.id, serialized);
|
|
808
|
+
}, 'local');
|
|
809
|
+
}
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Updates an existing object in both the local quadtree and the Yjs objects map.
|
|
814
|
+
* The object must already exist in the map. Supports temporary updates that can
|
|
815
|
+
* be consolidated later (e.g., for incremental text input).
|
|
816
|
+
*
|
|
817
|
+
* @param object - The object with updated properties; must have a valid `id` that exists in the map
|
|
818
|
+
* @param options - Optional settings for the update operation
|
|
819
|
+
* @param options.temporary - If `true`, marks this as a temporary change that can be consolidated
|
|
820
|
+
* with other temporary changes into a single undo step
|
|
821
|
+
* @returns `true` if the object was successfully updated, `false` if the object doesn't exist or has no ID
|
|
822
|
+
*/
|
|
823
|
+
update(object, options = {}) {
|
|
824
|
+
if (!object.id) {
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
const existed = this._idMap.has(object.id);
|
|
828
|
+
if (!existed) {
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
if (this._localOnlyMode || options.skipReindex) {
|
|
832
|
+
this.quadtree.update(object);
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
this.quadtree.reindex(object);
|
|
836
|
+
}
|
|
837
|
+
this._idMap.set(object.id, object);
|
|
838
|
+
if (!this._localOnlyMode && this._objectsMap && this.isPersistable(object)) {
|
|
839
|
+
const serialized = object.serialize();
|
|
840
|
+
const origin = options.temporary ? 'temporary' : 'local';
|
|
841
|
+
this._ydoc?.transact(() => {
|
|
842
|
+
this._objectsMap?.set(object.id, serialized);
|
|
843
|
+
}, origin);
|
|
844
|
+
}
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Removes objects matching the given predicate from both the quadtree and Yjs map.
|
|
849
|
+
* Each removal is tracked as a separate 'local' transaction for undo/redo.
|
|
850
|
+
*
|
|
851
|
+
* @param predicate - A function that returns `true` for objects that should be removed
|
|
852
|
+
*/
|
|
853
|
+
remove(predicate) {
|
|
854
|
+
const objectsToRemove = this.quadtree.filter(predicate);
|
|
855
|
+
if (objectsToRemove.length === 0) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
this.markUndoBoundary();
|
|
859
|
+
for (const object of objectsToRemove) {
|
|
860
|
+
this.quadtree.remove(o => o.id === object.id);
|
|
861
|
+
this._idMap.delete(object.id);
|
|
862
|
+
if (this._objectsMap && this.isPersistable(object)) {
|
|
863
|
+
this._ydoc?.transact(() => {
|
|
864
|
+
this._objectsMap?.delete(object.id);
|
|
865
|
+
}, 'local');
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Filters objects in the quadtree based on a predicate function.
|
|
871
|
+
*
|
|
872
|
+
* @param predicate - A function that returns `true` for objects to include in the result
|
|
873
|
+
* @returns An array of objects that match the predicate
|
|
874
|
+
*/
|
|
875
|
+
filter(predicate) {
|
|
876
|
+
return this.quadtree.filter(predicate);
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Returns all objects stored in the quadtree.
|
|
880
|
+
*
|
|
881
|
+
* @returns An array containing all objects in the map
|
|
882
|
+
*/
|
|
883
|
+
allObjects() {
|
|
884
|
+
return this.quadtree.allObjects();
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Queries the quadtree for objects within or intersecting a given 3D range.
|
|
888
|
+
* This is an efficient spatial query that avoids iterating over all objects.
|
|
889
|
+
*
|
|
890
|
+
* @param range - The bounding box to query
|
|
891
|
+
* @param range.x - The x-coordinate of the range's origin
|
|
892
|
+
* @param range.y - The y-coordinate of the range's origin
|
|
893
|
+
* @param range.z - The z-coordinate (layer) of the range's origin
|
|
894
|
+
* @param range.width - The width of the range
|
|
895
|
+
* @param range.height - The height of the range
|
|
896
|
+
* @param range.depth - Optional depth for 3D queries
|
|
897
|
+
* @returns An array of objects within or intersecting the specified range
|
|
898
|
+
*/
|
|
899
|
+
query(range) {
|
|
900
|
+
return this.quadtree.query(range);
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Determines whether an object should be persisted to the Yjs document.
|
|
904
|
+
* Certain objects like SelectionBox are transient and should not be synced.
|
|
905
|
+
*
|
|
906
|
+
* @param object - The object to check for persistability
|
|
907
|
+
* @returns `true` if the object should be persisted, `false` otherwise
|
|
908
|
+
*/
|
|
909
|
+
isPersistable(object) {
|
|
910
|
+
if (object instanceof KritzelSelectionBox) {
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Undoes the last operation if possible.
|
|
917
|
+
* Reverts the most recent change in the undo stack and emits an objects change event.
|
|
918
|
+
*/
|
|
919
|
+
undo() {
|
|
920
|
+
if (this._undoManager && this._undoManager.canUndo()) {
|
|
921
|
+
this._undoManager.undo();
|
|
922
|
+
this._core?.anchorManager.rebuildIndex();
|
|
923
|
+
this._core?.engine.emitObjectsChange();
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Redoes the last undone operation if possible.
|
|
928
|
+
* Reapplies the most recent change from the redo stack and emits an objects change event.
|
|
929
|
+
*/
|
|
930
|
+
redo() {
|
|
931
|
+
if (this._undoManager && this._undoManager.canRedo()) {
|
|
932
|
+
this._undoManager.redo();
|
|
933
|
+
this._core?.anchorManager.rebuildIndex();
|
|
934
|
+
this._core?.engine.emitObjectsChange();
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Checks whether an undo operation is available.
|
|
939
|
+
*
|
|
940
|
+
* @returns `true` if there are operations in the undo stack, `false` otherwise
|
|
941
|
+
*/
|
|
942
|
+
canUndo() {
|
|
943
|
+
return this._undoManager ? this._undoManager.canUndo() : false;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Checks whether a redo operation is available.
|
|
947
|
+
*
|
|
948
|
+
* @returns `true` if there are operations in the redo stack, `false` otherwise
|
|
949
|
+
*/
|
|
950
|
+
canRedo() {
|
|
951
|
+
return this._undoManager ? this._undoManager.canRedo() : false;
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Clears all undo and redo history.
|
|
955
|
+
* After calling this, no undo or redo operations will be available.
|
|
956
|
+
* Also resets the temporary items counter.
|
|
957
|
+
*/
|
|
958
|
+
clearHistory() {
|
|
959
|
+
if (this._undoManager) {
|
|
960
|
+
this._undoManager.clear();
|
|
961
|
+
this._temporaryItemsCount = 0;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Consolidates all temporary items in the undo stack into a single undo item.
|
|
966
|
+
* This is useful when you want to merge multiple incremental changes (e.g., individual
|
|
967
|
+
* keystrokes during text editing) into a single undoable action.
|
|
968
|
+
*
|
|
969
|
+
* The method works by undoing all temporary items and then redoing them within
|
|
970
|
+
* a single 'local' transaction, effectively merging them into one undo step.
|
|
971
|
+
* After consolidation, the temporary items counter is reset to zero.
|
|
972
|
+
*
|
|
973
|
+
* @example
|
|
974
|
+
* // After multiple temporary text updates
|
|
975
|
+
* objectMap.update(textObject, { temporary: true }); // keystroke 1
|
|
976
|
+
* objectMap.update(textObject, { temporary: true }); // keystroke 2
|
|
977
|
+
* objectMap.consolidateTemporaryItems(); // merge into single undo step
|
|
978
|
+
*/
|
|
979
|
+
consolidateTemporaryItems() {
|
|
980
|
+
if (!this._undoManager || this._temporaryItemsCount === 0) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
// Get current stack length
|
|
984
|
+
const stackLength = this._undoManager.undoStack.length;
|
|
985
|
+
if (stackLength === 0) {
|
|
986
|
+
this._temporaryItemsCount = 0;
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
// Stop tracking temporarily
|
|
990
|
+
this._undoManager.stopCapturing();
|
|
991
|
+
// The temporary items should be at the end of the stack
|
|
992
|
+
// We'll need to undo them all, then redo as a single transaction
|
|
993
|
+
const itemsToConsolidate = Math.min(this._temporaryItemsCount, stackLength);
|
|
994
|
+
if (itemsToConsolidate > 1) {
|
|
995
|
+
// Store the current state before undoing
|
|
996
|
+
const undoneItems = [];
|
|
997
|
+
// Undo all temporary items
|
|
998
|
+
for (let i = 0; i < itemsToConsolidate; i++) {
|
|
999
|
+
if (this._undoManager.canUndo()) {
|
|
1000
|
+
undoneItems.push(this._undoManager.undoStack[this._undoManager.undoStack.length - 1]);
|
|
1001
|
+
this._undoManager.undo();
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
// Now redo them all as a single 'local' transaction
|
|
1005
|
+
// This will consolidate them into one undo item
|
|
1006
|
+
this._ydoc?.transact(() => {
|
|
1007
|
+
for (let i = itemsToConsolidate - 1; i >= 0; i--) {
|
|
1008
|
+
if (this._undoManager?.canRedo()) {
|
|
1009
|
+
this._undoManager.redo();
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}, 'local');
|
|
1013
|
+
}
|
|
1014
|
+
else if (itemsToConsolidate === 1) {
|
|
1015
|
+
// If only one temporary item, just change its origin to 'local'
|
|
1016
|
+
// This is implicit - next transaction will be separate
|
|
1017
|
+
}
|
|
1018
|
+
this._temporaryItemsCount = 0;
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Removes all temporary items from the undo stack without consolidating them.
|
|
1022
|
+
* Use this to discard temporary changes completely when they should not be
|
|
1023
|
+
* preserved in the undo history (e.g., when cancelling an operation).
|
|
1024
|
+
*
|
|
1025
|
+
* Unlike `consolidateTemporaryItems`, this method does not preserve the changes
|
|
1026
|
+
* as a single undo step - it simply removes them from history entirely.
|
|
1027
|
+
*/
|
|
1028
|
+
clearTemporaryItems() {
|
|
1029
|
+
if (!this._undoManager || this._temporaryItemsCount === 0) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const stackLength = this._undoManager.undoStack.length;
|
|
1033
|
+
const itemsToRemove = Math.min(this._temporaryItemsCount, stackLength);
|
|
1034
|
+
// Remove items from the end of the stack
|
|
1035
|
+
for (let i = 0; i < itemsToRemove; i++) {
|
|
1036
|
+
if (this._undoManager.undoStack.length > 0) {
|
|
1037
|
+
this._undoManager.undoStack.pop();
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
this._temporaryItemsCount = 0;
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Hot-swaps sync providers on the existing Y.Doc without destroying the document,
|
|
1044
|
+
* objects, undo manager, or quadtree. Tears down old awareness subscriptions,
|
|
1045
|
+
/**
|
|
1046
|
+
* Destroys the object map and releases all resources.
|
|
1047
|
+
* This removes all observers, destroys the undo manager, disconnects all providers,
|
|
1048
|
+
* and destroys the Yjs document. After calling this, the object map cannot be used.
|
|
1049
|
+
*/
|
|
1050
|
+
destroy() {
|
|
1051
|
+
// Unobserve the objects map
|
|
1052
|
+
if (this._objectsMap && this._objectsObserver) {
|
|
1053
|
+
this._objectsMap.unobserve(this._objectsObserver);
|
|
1054
|
+
this._objectsObserver = null;
|
|
1055
|
+
}
|
|
1056
|
+
// Remove undo manager event listeners
|
|
1057
|
+
if (this._undoManager) {
|
|
1058
|
+
if (this._stackItemAddedHandler) {
|
|
1059
|
+
this._undoManager.off('stack-item-added', this._stackItemAddedHandler);
|
|
1060
|
+
this._stackItemAddedHandler = null;
|
|
1061
|
+
}
|
|
1062
|
+
if (this._stackItemPoppedHandler) {
|
|
1063
|
+
this._undoManager.off('stack-item-popped', this._stackItemPoppedHandler);
|
|
1064
|
+
this._stackItemPoppedHandler = null;
|
|
1065
|
+
}
|
|
1066
|
+
this._undoManager.destroy();
|
|
1067
|
+
this._undoManager = null;
|
|
1068
|
+
}
|
|
1069
|
+
// Reset quadtree
|
|
1070
|
+
this.quadtree.reset();
|
|
1071
|
+
this._idMap.clear();
|
|
1072
|
+
// Clean up awareness
|
|
1073
|
+
if (this._awareness && this._awarenessChangeHandler) {
|
|
1074
|
+
this._awareness.off('change', this._awarenessChangeHandler);
|
|
1075
|
+
this._awarenessChangeHandler = null;
|
|
1076
|
+
}
|
|
1077
|
+
if (this._awarenessEmitTimeout !== null) {
|
|
1078
|
+
clearTimeout(this._awarenessEmitTimeout);
|
|
1079
|
+
this._awarenessEmitTimeout = null;
|
|
1080
|
+
}
|
|
1081
|
+
this._awareness = null;
|
|
1082
|
+
this._awarenessChangeCallbacks = [];
|
|
1083
|
+
// Destroy providers (destroy handles disconnection internally)
|
|
1084
|
+
this._providers.forEach(p => {
|
|
1085
|
+
p.destroy();
|
|
1086
|
+
});
|
|
1087
|
+
this._providers = [];
|
|
1088
|
+
// Clear object map references
|
|
1089
|
+
this._objectsMap = null;
|
|
1090
|
+
this._metadataMap = null;
|
|
1091
|
+
// Destroy Y.Doc
|
|
1092
|
+
if (this._ydoc) {
|
|
1093
|
+
this._ydoc.destroy();
|
|
1094
|
+
this._ydoc = null;
|
|
1095
|
+
}
|
|
1096
|
+
// Clear remaining references
|
|
1097
|
+
this._core = null;
|
|
1098
|
+
this._reviver = null;
|
|
1099
|
+
this._workspaceId = null;
|
|
1100
|
+
this._isReady = false;
|
|
1101
|
+
this._temporaryItemsCount = 0;
|
|
1102
|
+
}
|
|
1103
|
+
}
|