pixelweaver 0.1.0
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/.env.development +1 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +18 -0
- package/.prettierignore +5 -0
- package/.prettierrc +16 -0
- package/.python-version +1 -0
- package/.rlsbl/bases/.github/workflows/ci.yml +21 -0
- package/.rlsbl/bases/.github/workflows/publish.yml +18 -0
- package/.rlsbl/bases/.rlsbl/changes/unreleased.jsonl +0 -0
- package/.rlsbl/bases/.rlsbl/hooks/post-release.sh +8 -0
- package/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +5 -0
- package/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +8 -0
- package/.rlsbl/bases/.rlsbl/lint/go.toml +17 -0
- package/.rlsbl/bases/.rlsbl/lint/npm.toml +19 -0
- package/.rlsbl/bases/.rlsbl/lint/python.toml +25 -0
- package/.rlsbl/bases/CHANGELOG.md +5 -0
- package/.rlsbl/bases/LICENSE +21 -0
- package/.rlsbl/changes/.validated +1 -0
- package/.rlsbl/changes/0.1.0.jsonl +85 -0
- package/.rlsbl/changes/0.1.0.md +13 -0
- package/.rlsbl/changes/unreleased.jsonl +0 -0
- package/.rlsbl/config.json +6 -0
- package/.rlsbl/hashes.json +14 -0
- package/.rlsbl/hooks/post-release.sh +8 -0
- package/.rlsbl/hooks/pre-checks.sh +5 -0
- package/.rlsbl/hooks/pre-release.sh +8 -0
- package/.rlsbl/lint/go.toml +17 -0
- package/.rlsbl/lint/npm.toml +19 -0
- package/.rlsbl/lint/python.toml +25 -0
- package/.rlsbl/releases/unreleased.toml +0 -0
- package/.rlsbl/releases/v0.1.0.toml +3 -0
- package/.rlsbl/version +1 -0
- package/.selfdoc/hashes/hashes.json +146 -0
- package/.strictcli/schema.json +227 -0
- package/CHANGELOG.md +17 -0
- package/CLAUDE.md +100 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/assets/icon.png +0 -0
- package/docs/_README.md +117 -0
- package/docs/cli-config.md +35 -0
- package/docs/cli-dev.md +21 -0
- package/docs/cli-diagnose.md +12 -0
- package/docs/cli-index.md +30 -0
- package/docs/cli-list.md +18 -0
- package/docs/cli-mcp.md +18 -0
- package/docs/cli-new.md +26 -0
- package/docs/cli-open.md +21 -0
- package/docs/cli-serve.md +21 -0
- package/docs/cli-stop.md +12 -0
- package/docs/gen-index.md +36 -0
- package/docs/index.md +13 -0
- package/docs/server-src-pixelweaver-__main__.md +12 -0
- package/docs/server-src-pixelweaver-autosave.md +12 -0
- package/docs/server-src-pixelweaver-bridge.md +12 -0
- package/docs/server-src-pixelweaver-cli.md +12 -0
- package/docs/server-src-pixelweaver-config.md +12 -0
- package/docs/server-src-pixelweaver-connections.md +12 -0
- package/docs/server-src-pixelweaver-main.md +12 -0
- package/docs/server-src-pixelweaver-mcp_bridge.md +12 -0
- package/docs/server-src-pixelweaver-mcp_drawing_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_export_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_frame_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_history_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_layer_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_lock.md +12 -0
- package/docs/server-src-pixelweaver-mcp_project_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_read_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_registry.md +12 -0
- package/docs/server-src-pixelweaver-mcp_resources.md +12 -0
- package/docs/server-src-pixelweaver-mcp_server.md +12 -0
- package/docs/server-src-pixelweaver-protocol.md +12 -0
- package/docs/server-src-pixelweaver-state.md +12 -0
- package/docs/server-src-pixelweaver-storage.md +12 -0
- package/docs/server-src-pixelweaver-websocket.md +12 -0
- package/docs/server-src-pixelweaver.md +12 -0
- package/e2e/app-launch.test.ts +35 -0
- package/e2e/menus.test.ts +26 -0
- package/e2e/tools.test.ts +27 -0
- package/e2e/undo-redo.test.ts +11 -0
- package/eslint.config.js +62 -0
- package/index.html +13 -0
- package/package.json +48 -0
- package/playwright.config.ts +19 -0
- package/plugins/builtin/.gitkeep +0 -0
- package/plugins/builtin/advanced-fill-tool.ts +146 -0
- package/plugins/builtin/circle-tool.ts +186 -0
- package/plugins/builtin/diamond-tool.ts +182 -0
- package/plugins/builtin/dither-tool.ts +186 -0
- package/plugins/builtin/drawing-primitives-plugin.ts +362 -0
- package/plugins/builtin/drawing-utils.test.ts +495 -0
- package/plugins/builtin/drawing-utils.ts +431 -0
- package/plugins/builtin/effects/blur.ts +97 -0
- package/plugins/builtin/effects/color-effects.ts +278 -0
- package/plugins/builtin/effects/flip.ts +83 -0
- package/plugins/builtin/effects/glow.ts +118 -0
- package/plugins/builtin/effects/outline.ts +110 -0
- package/plugins/builtin/effects/rotate.ts +357 -0
- package/plugins/builtin/effects/scale.ts +258 -0
- package/plugins/builtin/effects/shadow.ts +111 -0
- package/plugins/builtin/effects/sharpen.ts +102 -0
- package/plugins/builtin/effects.test.ts +715 -0
- package/plugins/builtin/eraser-tool.ts +23 -0
- package/plugins/builtin/eyedropper-tool.ts +83 -0
- package/plugins/builtin/fill-tool.ts +93 -0
- package/plugins/builtin/gradient-tool.ts +204 -0
- package/plugins/builtin/gradient.test.ts +142 -0
- package/plugins/builtin/importers/aseprite-importer-plugin.ts +174 -0
- package/plugins/builtin/importers/aseprite-parser.ts +497 -0
- package/plugins/builtin/importers/piskel-importer-plugin.ts +222 -0
- package/plugins/builtin/importers/sky-spec-plugin.ts +409 -0
- package/plugins/builtin/line-tool.ts +267 -0
- package/plugins/builtin/make-stroke-tool.ts +271 -0
- package/plugins/builtin/noise-dither.test.ts +151 -0
- package/plugins/builtin/noise-tool.ts +131 -0
- package/plugins/builtin/pattern-stamp-tool.ts +162 -0
- package/plugins/builtin/pencil-tool.ts +25 -0
- package/plugins/builtin/rect-tool.ts +179 -0
- package/plugins/builtin/selection-tool.ts +388 -0
- package/plugins/builtin/selection.test.ts +195 -0
- package/plugins/builtin/tool-plugins.test.ts +529 -0
- package/public/favicon.svg +7 -0
- package/public/sw.js +91 -0
- package/pyproject.toml +49 -0
- package/scripts/eslint-wave-a-list.sh +24 -0
- package/scripts/eslint-wave-a-status.sh +25 -0
- package/scripts/fix-index-signature-access.py +127 -0
- package/scripts/fix-unchecked-index.py +128 -0
- package/scripts/fix-unchecked-vars.py +127 -0
- package/scripts/fix-wave-a-bangs.py +36 -0
- package/scripts/fix-wave-a-templates.py +108 -0
- package/scripts/fix-wave-b-void-expr.py +167 -0
- package/scripts/generate-command-params.py +540 -0
- package/scripts/migrate-single-frame-to-multi.py +171 -0
- package/scripts/smoke-test.sh +77 -0
- package/selfdoc.json +10 -0
- package/server/README.md +0 -0
- package/server/src/pixelweaver/__init__.py +1 -0
- package/server/src/pixelweaver/__main__.py +4 -0
- package/server/src/pixelweaver/autosave.py +114 -0
- package/server/src/pixelweaver/bridge.py +127 -0
- package/server/src/pixelweaver/cli.py +199 -0
- package/server/src/pixelweaver/config.py +24 -0
- package/server/src/pixelweaver/connections.py +54 -0
- package/server/src/pixelweaver/main.py +271 -0
- package/server/src/pixelweaver/mcp_bridge.py +189 -0
- package/server/src/pixelweaver/mcp_drawing_tools.py +178 -0
- package/server/src/pixelweaver/mcp_export_tools.py +291 -0
- package/server/src/pixelweaver/mcp_frame_tools.py +423 -0
- package/server/src/pixelweaver/mcp_history_tools.py +106 -0
- package/server/src/pixelweaver/mcp_layer_tools.py +64 -0
- package/server/src/pixelweaver/mcp_lock.py +37 -0
- package/server/src/pixelweaver/mcp_project_tools.py +302 -0
- package/server/src/pixelweaver/mcp_read_tools.py +163 -0
- package/server/src/pixelweaver/mcp_registry.py +247 -0
- package/server/src/pixelweaver/mcp_resources.py +312 -0
- package/server/src/pixelweaver/mcp_server.py +234 -0
- package/server/src/pixelweaver/protocol.py +219 -0
- package/server/src/pixelweaver/state.py +267 -0
- package/server/src/pixelweaver/storage.py +293 -0
- package/server/src/pixelweaver/websocket.py +282 -0
- package/server/tests/__init__.py +0 -0
- package/server/tests/conftest.py +17 -0
- package/server/tests/test_api.py +96 -0
- package/server/tests/test_bridge.py +161 -0
- package/server/tests/test_health.py +9 -0
- package/server/tests/test_integration.py +86 -0
- package/server/tests/test_mcp_bridge.py +293 -0
- package/server/tests/test_mcp_lock.py +34 -0
- package/server/tests/test_mcp_registry.py +679 -0
- package/server/tests/test_mcp_resources.py +648 -0
- package/server/tests/test_protocol.py +125 -0
- package/server/tests/test_state.py +87 -0
- package/server/tests/test_storage.py +306 -0
- package/server/tests/test_websocket.py +275 -0
- package/src/App.svelte +107 -0
- package/src/app.css +215 -0
- package/src/lib/animation/AnimationPreviewPanel.svelte +667 -0
- package/src/lib/animation/animation-commands.test.ts +228 -0
- package/src/lib/animation/animation-commands.ts +540 -0
- package/src/lib/animation/animation-preview-panel-plugin.ts +25 -0
- package/src/lib/animation/animation-preview.svelte.ts +151 -0
- package/src/lib/animation/clipboard.ts +134 -0
- package/src/lib/animation/frame-model.svelte.ts +437 -0
- package/src/lib/animation/frame-model.test.ts +314 -0
- package/src/lib/animation/frame-selection.svelte.ts +77 -0
- package/src/lib/animation/frame-tags.svelte.ts +238 -0
- package/src/lib/animation/frame-tags.test.ts +136 -0
- package/src/lib/animation/import.test.ts +141 -0
- package/src/lib/animation/import.ts +112 -0
- package/src/lib/animation/spritesheet-export.test.ts +239 -0
- package/src/lib/animation/spritesheet-export.ts +314 -0
- package/src/lib/canvas/CanvasViewport.svelte +216 -0
- package/src/lib/canvas/canvas-init-plugin.ts +178 -0
- package/src/lib/canvas/canvas-renderer.ts +408 -0
- package/src/lib/canvas/canvas-state.svelte.ts +232 -0
- package/src/lib/canvas/canvas-state.test.ts +139 -0
- package/src/lib/canvas/input-handler.ts +221 -0
- package/src/lib/canvas/input-plugin.ts +150 -0
- package/src/lib/canvas/onion-skin.ts +94 -0
- package/src/lib/canvas/pixel-buffer.test.ts +249 -0
- package/src/lib/canvas/pixel-buffer.ts +151 -0
- package/src/lib/canvas/render-state.svelte.ts +18 -0
- package/src/lib/canvas/shape-preview-state.svelte.ts +36 -0
- package/src/lib/canvas/tile-mode.test.ts +53 -0
- package/src/lib/canvas/tile-mode.ts +92 -0
- package/src/lib/canvas/viewport-utils.ts +24 -0
- package/src/lib/canvas/zoom-utils.ts +31 -0
- package/src/lib/color/.gitkeep +0 -0
- package/src/lib/color/color-commands.ts +87 -0
- package/src/lib/color/color-state.svelte.ts +98 -0
- package/src/lib/color/color-state.test.ts +91 -0
- package/src/lib/color/color-utils.test.ts +220 -0
- package/src/lib/color/color-utils.ts +243 -0
- package/src/lib/color/palette-state.svelte.ts +127 -0
- package/src/lib/color/palette-state.test.ts +154 -0
- package/src/lib/color/palette.ts +79 -0
- package/src/lib/core/bootstrap.ts +66 -0
- package/src/lib/core/command-params.generated.ts +1549 -0
- package/src/lib/core/command-params.ts +20 -0
- package/src/lib/core/command-runner.ts +79 -0
- package/src/lib/core/commands.ts +134 -0
- package/src/lib/core/dispatcher.test.ts +548 -0
- package/src/lib/core/dispatcher.ts +361 -0
- package/src/lib/core/index.test.ts +7 -0
- package/src/lib/core/notification-state.svelte.ts +119 -0
- package/src/lib/core/plugin-api.ts +210 -0
- package/src/lib/core/plugin-discovery.test.ts +53 -0
- package/src/lib/core/plugin-discovery.ts +65 -0
- package/src/lib/core/plugin-loader.test.ts +159 -0
- package/src/lib/core/plugin-loader.ts +240 -0
- package/src/lib/core/plugin-types.ts +286 -0
- package/src/lib/core/registries.svelte.ts +74 -0
- package/src/lib/core/tool-options-state.svelte.ts +61 -0
- package/src/lib/dock/DockLayout.svelte +375 -0
- package/src/lib/dock/TabAddMenu.svelte +90 -0
- package/src/lib/dock/dock-persistence.ts +46 -0
- package/src/lib/dock/dock-plugin.ts +49 -0
- package/src/lib/dock/dock-presets.ts +156 -0
- package/src/lib/dock/dock-state.svelte.ts +77 -0
- package/src/lib/dock/dock-types.ts +2 -0
- package/src/lib/dock/dockview-theme.css +226 -0
- package/src/lib/dock/dockview-theme.ts +17 -0
- package/src/lib/dock/header-action-renderer.ts +77 -0
- package/src/lib/edit/clipboard-state.ts +34 -0
- package/src/lib/export/download.ts +201 -0
- package/src/lib/export/png-metadata.ts +181 -0
- package/src/lib/history/ActionLogPanel.svelte +418 -0
- package/src/lib/history/action-log-panel-plugin.ts +24 -0
- package/src/lib/history/action-log.svelte.ts +172 -0
- package/src/lib/history/action-log.test.ts +168 -0
- package/src/lib/history/history-commands.ts +403 -0
- package/src/lib/history/macros.svelte.ts +320 -0
- package/src/lib/history/macros.test.ts +224 -0
- package/src/lib/history/replay-engine.test.ts +241 -0
- package/src/lib/history/replay-engine.ts +149 -0
- package/src/lib/history/spec-format.test.ts +250 -0
- package/src/lib/history/spec-format.ts +210 -0
- package/src/lib/iso/SeamCheckerPanel.svelte +251 -0
- package/src/lib/iso/iso-math.test.ts +77 -0
- package/src/lib/iso/iso-math.ts +77 -0
- package/src/lib/iso/seam-checker-panel-plugin.ts +25 -0
- package/src/lib/iso/seam-checker.test.ts +126 -0
- package/src/lib/iso/seam-checker.ts +93 -0
- package/src/lib/iso/tessellation.ts +67 -0
- package/src/lib/layers/compositor.test.ts +193 -0
- package/src/lib/layers/compositor.ts +175 -0
- package/src/lib/layers/layer-commands.test.ts +263 -0
- package/src/lib/layers/layer-commands.ts +429 -0
- package/src/lib/layers/layer-tree.svelte.ts +516 -0
- package/src/lib/layers/layer-tree.test.ts +383 -0
- package/src/lib/layers/layer-types.ts +56 -0
- package/src/lib/leveleditor/LevelEditorViewport.svelte +1808 -0
- package/src/lib/leveleditor/MapPropertiesPanel.svelte +266 -0
- package/src/lib/leveleditor/TilePickerPanel.svelte +324 -0
- package/src/lib/leveleditor/depth-sort.test.ts +70 -0
- package/src/lib/leveleditor/depth-sort.ts +39 -0
- package/src/lib/leveleditor/level-editor-commands.ts +353 -0
- package/src/lib/leveleditor/level-editor-viewport-plugin.ts +25 -0
- package/src/lib/leveleditor/map-properties-panel-plugin.ts +25 -0
- package/src/lib/leveleditor/map-state.svelte.ts +372 -0
- package/src/lib/leveleditor/map-state.test.ts +243 -0
- package/src/lib/leveleditor/tile-picker-panel-plugin.ts +25 -0
- package/src/lib/leveleditor/tile-source-registry.svelte.ts +91 -0
- package/src/lib/leveleditor/tiled-export.test.ts +144 -0
- package/src/lib/leveleditor/tiled-export.ts +374 -0
- package/src/lib/pywebview.d.ts +15 -0
- package/src/lib/recovery/.gitkeep +0 -0
- package/src/lib/recovery/idb-store.ts +118 -0
- package/src/lib/recovery/recovery-manager.test.ts +321 -0
- package/src/lib/recovery/recovery-manager.ts +115 -0
- package/src/lib/recovery/recovery-plugin.ts +55 -0
- package/src/lib/recovery/recovery-state.svelte.ts +21 -0
- package/src/lib/recovery/sw-plugin.ts +18 -0
- package/src/lib/recovery/sw-register.ts +17 -0
- package/src/lib/save/directory-format.ts +42 -0
- package/src/lib/save/project-snapshot.ts +139 -0
- package/src/lib/save/recent-projects.ts +56 -0
- package/src/lib/save/save-state.svelte.ts +35 -0
- package/src/lib/save/storage.ts +167 -0
- package/src/lib/save/zip-format.ts +45 -0
- package/src/lib/shortcuts/.gitkeep +0 -0
- package/src/lib/shortcuts/ShortcutEditorPanel.svelte +690 -0
- package/src/lib/shortcuts/default-bindings.ts +61 -0
- package/src/lib/shortcuts/shortcut-editor-panel-plugin.ts +25 -0
- package/src/lib/shortcuts/shortcut-init.ts +380 -0
- package/src/lib/shortcuts/shortcut-manager.test.ts +466 -0
- package/src/lib/shortcuts/shortcut-manager.ts +281 -0
- package/src/lib/shortcuts/shortcut-state.svelte.ts +78 -0
- package/src/lib/shortcuts/shortcuts-plugin.ts +17 -0
- package/src/lib/sync/patch-applicator.ts +300 -0
- package/src/lib/sync/patch-types.ts +65 -0
- package/src/lib/sync/project-manager.ts +108 -0
- package/src/lib/sync/sync-init.ts +152 -0
- package/src/lib/sync/sync-plugin.ts +19 -0
- package/src/lib/sync/sync-state.svelte.ts +56 -0
- package/src/lib/sync/ws-client.test.ts +604 -0
- package/src/lib/sync/ws-client.ts +574 -0
- package/src/lib/tools/.gitkeep +0 -0
- package/src/lib/ui/.gitkeep +0 -0
- package/src/lib/ui/AboutDialog.svelte +113 -0
- package/src/lib/ui/ColorPicker.svelte +761 -0
- package/src/lib/ui/ContextMenu.svelte +216 -0
- package/src/lib/ui/ExportDialog.svelte +747 -0
- package/src/lib/ui/FrameStrip.svelte +854 -0
- package/src/lib/ui/LayerPanel.svelte +810 -0
- package/src/lib/ui/MenuBar.svelte +590 -0
- package/src/lib/ui/NewProjectDialog.svelte +803 -0
- package/src/lib/ui/PluginManagerPanel.svelte +475 -0
- package/src/lib/ui/PromptDialog.svelte +252 -0
- package/src/lib/ui/RecoveryDialog.svelte +295 -0
- package/src/lib/ui/ResizeDialog.svelte +416 -0
- package/src/lib/ui/StatusBar.svelte +145 -0
- package/src/lib/ui/ToolbarPanel.svelte +488 -0
- package/src/lib/ui/animation-menu-commands.ts +194 -0
- package/src/lib/ui/command-palette/CommandPalette.svelte +232 -0
- package/src/lib/ui/command-palette/command-palette-plugin.ts +30 -0
- package/src/lib/ui/command-palette/command-palette-state.svelte.ts +190 -0
- package/src/lib/ui/command-palette/command-palette.test.ts +129 -0
- package/src/lib/ui/dialog-state.svelte.ts +70 -0
- package/src/lib/ui/edit-commands.ts +271 -0
- package/src/lib/ui/file-commands.ts +275 -0
- package/src/lib/ui/file-open.ts +99 -0
- package/src/lib/ui/help-commands.ts +93 -0
- package/src/lib/ui/image-commands.ts +181 -0
- package/src/lib/ui/layer-menu-commands.ts +420 -0
- package/src/lib/ui/menu-builder.ts +224 -0
- package/src/lib/ui/notifications/NotificationBanner.svelte +137 -0
- package/src/lib/ui/notifications/notification-plugin.ts +29 -0
- package/src/lib/ui/notifications/notification-state.svelte.ts +9 -0
- package/src/lib/ui/plugin-manager-panel-plugin.ts +26 -0
- package/src/lib/ui/plugin-state.svelte.ts +62 -0
- package/src/lib/ui/select-commands.ts +75 -0
- package/src/lib/ui/theme-plugin.ts +18 -0
- package/src/lib/ui/theme.svelte.ts +45 -0
- package/src/lib/ui/theme.test.ts +51 -0
- package/src/lib/ui/toolbar-config.ts +90 -0
- package/src/lib/ui/toolbar-plugin.ts +39 -0
- package/src/lib/ui/view-commands.ts +629 -0
- package/src/lib/variants/BisectionExportDialog.svelte +500 -0
- package/src/lib/variants/VariantPanel.svelte +822 -0
- package/src/lib/variants/bisection-export.test.ts +113 -0
- package/src/lib/variants/bisection-export.ts +148 -0
- package/src/lib/variants/palette-extraction.test.ts +111 -0
- package/src/lib/variants/palette-extraction.ts +84 -0
- package/src/lib/variants/palette-interpolation.test.ts +113 -0
- package/src/lib/variants/palette-interpolation.ts +87 -0
- package/src/lib/variants/palette-swap.test.ts +101 -0
- package/src/lib/variants/palette-swap.ts +114 -0
- package/src/lib/variants/variant-commands.ts +594 -0
- package/src/lib/variants/variant-panel-plugin.ts +27 -0
- package/src/lib/variants/variant-randomizer.ts +101 -0
- package/src/lib/variants/variant-state.svelte.ts +166 -0
- package/src/lib/variants/variant-state.test.ts +138 -0
- package/src/main.ts +14 -0
- package/src/vite-env.d.ts +3 -0
- package/svelte.config.js +2 -0
- package/todo/.done/audit-design-decisions.md +812 -0
- package/todo/.done/audit-implementation-plan.md +1235 -0
- package/todo/.done/happy-path-polish.md +177 -0
- package/todo/.done/pixelweaver-full-build.md +937 -0
- package/todo/.done/server-multi-frame-design.md +405 -0
- package/todo/.done/typed-dispatcher-design.md +435 -0
- package/todo/.done/unified-toolbar-and-action-system.md +323 -0
- package/todo/.obsolete/comprehensive-audit-obsolete-items.md +33 -0
- package/todo/.obsolete/tauri-desktop-bundle.md +424 -0
- package/todo/comprehensive-audit.md +1085 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +20 -0
- package/uv.lock +1167 -0
- package/vite.config.ts +32 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Dispatcher -- the central routing system of PixelWeaver.
|
|
3
|
+
*
|
|
4
|
+
* When a command is dispatched:
|
|
5
|
+
* 1. Validate: ensure the command type is registered
|
|
6
|
+
* 2. Execute: call the command definition's execute()
|
|
7
|
+
* 3. Record: push onto the undo stack (with tier + parent pointer) and notify onExecute subscribers
|
|
8
|
+
* 4. Log: call describe() and append to the semantic log
|
|
9
|
+
* 5. Serialize: notify onSpecExport subscribers
|
|
10
|
+
*
|
|
11
|
+
* Two-tier undo: each command is tagged 'frame' (drawing ops) or 'project'
|
|
12
|
+
* (structural ops). Ctrl+Z always undoes the most recent command regardless
|
|
13
|
+
* of tier. The tier tag exists for future UI grouping and tier-filtered
|
|
14
|
+
* undo operations.
|
|
15
|
+
*
|
|
16
|
+
* Module-level singleton -- one dispatcher per app instance.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
Command,
|
|
21
|
+
CommandContext,
|
|
22
|
+
CommandDefinition,
|
|
23
|
+
SemanticLogEntry,
|
|
24
|
+
UndoEntry,
|
|
25
|
+
} from './commands.js';
|
|
26
|
+
import type { CommandType, ParamsOf } from './command-params.js';
|
|
27
|
+
import { commandRegistry } from './registries.svelte.js';
|
|
28
|
+
|
|
29
|
+
// --- Sync hook ---
|
|
30
|
+
// Called after every successful command dispatch so the sync layer can
|
|
31
|
+
// forward commands to the collaboration server without tight coupling.
|
|
32
|
+
|
|
33
|
+
type DispatchHook = (command: Command) => void;
|
|
34
|
+
let dispatchHook: DispatchHook | null = null;
|
|
35
|
+
|
|
36
|
+
/** Register (or clear) the post-dispatch hook used by the sync layer. */
|
|
37
|
+
export function setDispatchHook(hook: DispatchHook | null): void {
|
|
38
|
+
dispatchHook = hook;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Notify subscribers using a defensive copy so callbacks that add/remove
|
|
43
|
+
* other subscribers during iteration don't corrupt the loop.
|
|
44
|
+
*/
|
|
45
|
+
function notify<T>(callbacks: ReadonlyArray<(arg: T) => void>, arg: T): void {
|
|
46
|
+
for (const cb of [...callbacks]) cb(arg);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Consumer hook types ---
|
|
50
|
+
|
|
51
|
+
type ExecuteCallback = (command: Command) => void;
|
|
52
|
+
type UndoCallback = (command: Command) => void;
|
|
53
|
+
type LogCallback = (entry: SemanticLogEntry) => void;
|
|
54
|
+
type SpecExportCallback = (command: Command) => void;
|
|
55
|
+
|
|
56
|
+
// --- Dispatcher state ---
|
|
57
|
+
|
|
58
|
+
/** The shared command context; later phases will populate with canvas, layers, etc. */
|
|
59
|
+
let context: CommandContext = {};
|
|
60
|
+
|
|
61
|
+
/** Undo stack with tier and parent pointers (most recent entry last) */
|
|
62
|
+
const undoStack: UndoEntry[] = [];
|
|
63
|
+
|
|
64
|
+
/** Redo stack with tier and parent pointers (most recently undone entry last) */
|
|
65
|
+
const redoStack: UndoEntry[] = [];
|
|
66
|
+
|
|
67
|
+
/** Semantic log of all executed commands */
|
|
68
|
+
const semanticLog: SemanticLogEntry[] = [];
|
|
69
|
+
|
|
70
|
+
/** Maximum number of entries in the undo stack; oldest entries are dropped when exceeded */
|
|
71
|
+
let maxUndoSize = 500;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Save checkpoint: the ID of the undo stack's top entry at the time of the
|
|
75
|
+
* last save. Comparing the current top-of-stack ID against this value tells
|
|
76
|
+
* us whether the project has unsaved changes -- correctly handling undo past
|
|
77
|
+
* the save point, redo back to it, and new commands after undo.
|
|
78
|
+
*
|
|
79
|
+
* null means "saved at empty state" (fresh/new project).
|
|
80
|
+
*/
|
|
81
|
+
let saveCheckpointId: string | null = null;
|
|
82
|
+
|
|
83
|
+
// --- Consumer hook subscriptions ---
|
|
84
|
+
|
|
85
|
+
const executeCallbacks: ExecuteCallback[] = [];
|
|
86
|
+
const undoCallbacks: UndoCallback[] = [];
|
|
87
|
+
const logCallbacks: LogCallback[] = [];
|
|
88
|
+
const specExportCallbacks: SpecExportCallback[] = [];
|
|
89
|
+
|
|
90
|
+
// --- Internal helpers ---
|
|
91
|
+
|
|
92
|
+
/** Resolve the tier for a command from its definition, defaulting to 'frame'. */
|
|
93
|
+
function resolveTier(commandType: string): UndoEntry['tier'] {
|
|
94
|
+
const def = commandRegistry.get(commandType);
|
|
95
|
+
return def?.tier ?? 'frame';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Get the ID of the topmost undo entry, or null if the stack is empty. */
|
|
99
|
+
function topUndoId(): string | null {
|
|
100
|
+
return undoStack[undoStack.length - 1]?.id ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// --- Public API ---
|
|
104
|
+
|
|
105
|
+
/** Subscribe to command execution events. Returns an unsubscribe function. */
|
|
106
|
+
export function onExecute(callback: ExecuteCallback): () => void {
|
|
107
|
+
executeCallbacks.push(callback);
|
|
108
|
+
return () => {
|
|
109
|
+
const idx = executeCallbacks.indexOf(callback);
|
|
110
|
+
if (idx !== -1) executeCallbacks.splice(idx, 1);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Subscribe to command undo events. Returns an unsubscribe function. */
|
|
115
|
+
export function onUndo(callback: UndoCallback): () => void {
|
|
116
|
+
undoCallbacks.push(callback);
|
|
117
|
+
return () => {
|
|
118
|
+
const idx = undoCallbacks.indexOf(callback);
|
|
119
|
+
if (idx !== -1) undoCallbacks.splice(idx, 1);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Subscribe to semantic log events. Returns an unsubscribe function. */
|
|
124
|
+
export function onLog(callback: LogCallback): () => void {
|
|
125
|
+
logCallbacks.push(callback);
|
|
126
|
+
return () => {
|
|
127
|
+
const idx = logCallbacks.indexOf(callback);
|
|
128
|
+
if (idx !== -1) logCallbacks.splice(idx, 1);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Subscribe to spec-export events. Returns an unsubscribe function. */
|
|
133
|
+
export function onSpecExport(callback: SpecExportCallback): () => void {
|
|
134
|
+
specExportCallbacks.push(callback);
|
|
135
|
+
return () => {
|
|
136
|
+
const idx = specExportCallbacks.indexOf(callback);
|
|
137
|
+
if (idx !== -1) specExportCallbacks.splice(idx, 1);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Dispatch a command: validate, execute, record, log, and notify consumers.
|
|
143
|
+
* Throws if the command type is not registered.
|
|
144
|
+
*/
|
|
145
|
+
// Typed overload: compile-time param validation for known commands
|
|
146
|
+
export function dispatch<T extends CommandType>(command: Command & { type: T; params: ParamsOf<T> }): void;
|
|
147
|
+
// String fallback: for dynamic dispatch (import_spec replay, etc.)
|
|
148
|
+
export function dispatch(command: Command): void;
|
|
149
|
+
// Implementation
|
|
150
|
+
export function dispatch(command: Command): void {
|
|
151
|
+
// Registry stores CommandDefinition<any> as a type-erasure seam;
|
|
152
|
+
// narrow through unknown to the default-P shape so unsafe-assignment
|
|
153
|
+
// does not fire on the UndoEntry.definition assignment below.
|
|
154
|
+
const definition = commandRegistry.get(command.type) as unknown as
|
|
155
|
+
| CommandDefinition
|
|
156
|
+
| undefined;
|
|
157
|
+
if (!definition) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Unknown command type "${command.type}". ` +
|
|
160
|
+
`Has the plugin that provides it been loaded?`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Execute and capture snapshot for undo
|
|
165
|
+
const snapshot = definition.execute(command.params, context);
|
|
166
|
+
|
|
167
|
+
// Build undo entry with tier, parent pointer, definition, and snapshot
|
|
168
|
+
const entry: UndoEntry = {
|
|
169
|
+
command,
|
|
170
|
+
definition,
|
|
171
|
+
tier: resolveTier(command.type),
|
|
172
|
+
id: command.id,
|
|
173
|
+
parentId: topUndoId(),
|
|
174
|
+
snapshot,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Record for undo; new dispatch clears the redo stack
|
|
178
|
+
undoStack.push(entry);
|
|
179
|
+
redoStack.length = 0;
|
|
180
|
+
|
|
181
|
+
// Enforce max undo size by dropping oldest entries
|
|
182
|
+
while (undoStack.length > maxUndoSize) {
|
|
183
|
+
undoStack.shift();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Semantic log
|
|
187
|
+
const logEntry: SemanticLogEntry = {
|
|
188
|
+
description: definition.describe(command.params),
|
|
189
|
+
timestamp: command.timestamp,
|
|
190
|
+
commandId: command.id,
|
|
191
|
+
};
|
|
192
|
+
semanticLog.push(logEntry);
|
|
193
|
+
|
|
194
|
+
// Notify consumers
|
|
195
|
+
notify(executeCallbacks, command);
|
|
196
|
+
notify(logCallbacks, logEntry);
|
|
197
|
+
notify(specExportCallbacks, command);
|
|
198
|
+
|
|
199
|
+
// Forward to the sync layer (best-effort, won't block local dispatch)
|
|
200
|
+
dispatchHook?.(command);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Undo the most recent command. Returns the undone command, or undefined if
|
|
205
|
+
* the undo stack is empty.
|
|
206
|
+
*/
|
|
207
|
+
export function undoLast(): Command | undefined {
|
|
208
|
+
const entry = undoStack.pop();
|
|
209
|
+
if (!entry) return undefined;
|
|
210
|
+
|
|
211
|
+
entry.definition.undo(entry.command.params, context, entry.snapshot);
|
|
212
|
+
redoStack.push(entry);
|
|
213
|
+
|
|
214
|
+
notify(undoCallbacks, entry.command);
|
|
215
|
+
return entry.command;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Redo the most recently undone command. Returns the redone command, or
|
|
220
|
+
* undefined if the redo stack is empty.
|
|
221
|
+
*/
|
|
222
|
+
export function redoLast(): Command | undefined {
|
|
223
|
+
const entry = redoStack.pop();
|
|
224
|
+
if (!entry) return undefined;
|
|
225
|
+
|
|
226
|
+
// Re-execute and capture a fresh snapshot (buffer state may differ from original)
|
|
227
|
+
const newSnapshot = entry.definition.execute(entry.command.params, context);
|
|
228
|
+
entry.snapshot = newSnapshot;
|
|
229
|
+
|
|
230
|
+
// Update parent pointer to current top of undo stack before pushing
|
|
231
|
+
entry.parentId = topUndoId();
|
|
232
|
+
undoStack.push(entry);
|
|
233
|
+
|
|
234
|
+
// Log the redo as a new semantic entry
|
|
235
|
+
const logEntry: SemanticLogEntry = {
|
|
236
|
+
description: entry.definition.describe(entry.command.params),
|
|
237
|
+
timestamp: Date.now(),
|
|
238
|
+
commandId: entry.command.id,
|
|
239
|
+
};
|
|
240
|
+
semanticLog.push(logEntry);
|
|
241
|
+
|
|
242
|
+
notify(executeCallbacks, entry.command);
|
|
243
|
+
notify(logCallbacks, logEntry);
|
|
244
|
+
notify(specExportCallbacks, entry.command);
|
|
245
|
+
|
|
246
|
+
return entry.command;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- Two-tier undo query API ---
|
|
250
|
+
|
|
251
|
+
/** Read-only access to the undo stack (entries with tier + parent pointers). */
|
|
252
|
+
export function getUndoStack(): readonly UndoEntry[] {
|
|
253
|
+
return undoStack;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Read-only access to the redo stack (entries with tier + parent pointers). */
|
|
257
|
+
export function getRedoStack(): readonly UndoEntry[] {
|
|
258
|
+
return redoStack;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Whether there is at least one command to undo. */
|
|
262
|
+
export function canUndo(): boolean {
|
|
263
|
+
return undoStack.length > 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Whether there is at least one command to redo. */
|
|
267
|
+
export function canRedo(): boolean {
|
|
268
|
+
return redoStack.length > 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- Save checkpoint API ---
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Mark the current undo position as the save checkpoint. Call this after a
|
|
275
|
+
* successful save so that hasUnsavedChanges() returns false until the next
|
|
276
|
+
* dispatch/undo/redo changes the position.
|
|
277
|
+
*/
|
|
278
|
+
export function markSaveCheckpoint(): void {
|
|
279
|
+
saveCheckpointId = topUndoId();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Whether the project has been modified since the last save (or since
|
|
284
|
+
* creation, if never saved). Compares the current top-of-stack entry ID
|
|
285
|
+
* against the checkpoint recorded at save time.
|
|
286
|
+
*
|
|
287
|
+
* Correctly handles:
|
|
288
|
+
* - Undo past the save point (dirty again)
|
|
289
|
+
* - Redo back to the save point (clean again)
|
|
290
|
+
* - New commands diverging from the save point (dirty)
|
|
291
|
+
*/
|
|
292
|
+
export function hasUnsavedChanges(): boolean {
|
|
293
|
+
return topUndoId() !== saveCheckpointId;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// --- Undo stack size configuration ---
|
|
297
|
+
|
|
298
|
+
/** Set the maximum undo stack size. Oldest entries are dropped when exceeded. */
|
|
299
|
+
export function setMaxUndoSize(size: number): void {
|
|
300
|
+
if (size < 1) throw new Error('Max undo size must be at least 1');
|
|
301
|
+
maxUndoSize = size;
|
|
302
|
+
// Trim if current stack exceeds the new limit
|
|
303
|
+
while (undoStack.length > maxUndoSize) {
|
|
304
|
+
undoStack.shift();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Get the current maximum undo stack size. */
|
|
309
|
+
export function getMaxUndoSize(): number {
|
|
310
|
+
return maxUndoSize;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// --- History management ---
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Clear all undo/redo history and the semantic log.
|
|
317
|
+
* Call this when creating a new project so stale commands from the
|
|
318
|
+
* previous project cannot trigger false unsaved-changes warnings or
|
|
319
|
+
* corrupt state via undo replay.
|
|
320
|
+
*/
|
|
321
|
+
export function clearHistory(): void {
|
|
322
|
+
undoStack.length = 0;
|
|
323
|
+
redoStack.length = 0;
|
|
324
|
+
semanticLog.length = 0;
|
|
325
|
+
// Reset checkpoint so a fresh project is not considered dirty
|
|
326
|
+
saveCheckpointId = null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// --- Existing API ---
|
|
330
|
+
|
|
331
|
+
/** Read-only access to the semantic log */
|
|
332
|
+
export function getSemanticLog(): readonly SemanticLogEntry[] {
|
|
333
|
+
return semanticLog;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Set (or merge into) the shared command context */
|
|
337
|
+
export function setContext(partial: CommandContext): void {
|
|
338
|
+
Object.assign(context, partial);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Get the shared command context */
|
|
342
|
+
export function getContext(): CommandContext {
|
|
343
|
+
return context;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Reset all dispatcher state. Intended for tests only.
|
|
348
|
+
*/
|
|
349
|
+
export function _resetForTesting(): void {
|
|
350
|
+
undoStack.length = 0;
|
|
351
|
+
redoStack.length = 0;
|
|
352
|
+
semanticLog.length = 0;
|
|
353
|
+
executeCallbacks.length = 0;
|
|
354
|
+
undoCallbacks.length = 0;
|
|
355
|
+
logCallbacks.length = 0;
|
|
356
|
+
specExportCallbacks.length = 0;
|
|
357
|
+
context = {};
|
|
358
|
+
maxUndoSize = 500;
|
|
359
|
+
dispatchHook = null;
|
|
360
|
+
saveCheckpointId = null;
|
|
361
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification State -- reactive singleton for the in-app notification banner.
|
|
3
|
+
* Manages a priority-ordered queue of notifications with auto-dismiss support.
|
|
4
|
+
*
|
|
5
|
+
* Lives in core so that plugin-api.ts and plugin-types.ts can depend on it
|
|
6
|
+
* without a core-to-UI layer violation. The UI component (NotificationBanner)
|
|
7
|
+
* imports via the re-export barrel in ui/notifications/.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// --- Types ---
|
|
11
|
+
|
|
12
|
+
export interface AppNotification {
|
|
13
|
+
id: string;
|
|
14
|
+
message: string;
|
|
15
|
+
type: 'info' | 'warning' | 'success';
|
|
16
|
+
/** Priority for ordering (higher = shown first). Default: 0 */
|
|
17
|
+
priority?: number;
|
|
18
|
+
/** Optional action button displayed alongside the message */
|
|
19
|
+
action?: {
|
|
20
|
+
label: string;
|
|
21
|
+
callback: () => void;
|
|
22
|
+
};
|
|
23
|
+
/** Called when the notification is dismissed (manually or via auto-dismiss) */
|
|
24
|
+
onDismiss?: () => void;
|
|
25
|
+
/** Auto-dismiss after this many ms. If absent, stays until manually dismissed. */
|
|
26
|
+
autoDismissMs?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Union of allowed notification severity values */
|
|
30
|
+
export type NotificationLevel = AppNotification['type'];
|
|
31
|
+
|
|
32
|
+
// --- Reactive state ---
|
|
33
|
+
|
|
34
|
+
let notifications: AppNotification[] = $state([]);
|
|
35
|
+
|
|
36
|
+
/** Active auto-dismiss timers keyed by notification id (not reactive). */
|
|
37
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- internal timer bookkeeping
|
|
38
|
+
const timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
39
|
+
|
|
40
|
+
// --- Internal helpers ---
|
|
41
|
+
|
|
42
|
+
/** Sort by priority descending (higher priority first) */
|
|
43
|
+
function sorted(list: AppNotification[]): AppNotification[] {
|
|
44
|
+
return [...list].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function clearTimer(id: string): void {
|
|
48
|
+
const timer = timers.get(id);
|
|
49
|
+
if (timer !== undefined) {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
timers.delete(id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function startAutoDismiss(notification: AppNotification): void {
|
|
56
|
+
if (notification.autoDismissMs == null || notification.autoDismissMs <= 0) return;
|
|
57
|
+
clearTimer(notification.id);
|
|
58
|
+
timers.set(
|
|
59
|
+
notification.id,
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
notificationState.dismiss(notification.id);
|
|
62
|
+
}, notification.autoDismissMs),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- Public API ---
|
|
67
|
+
|
|
68
|
+
export const notificationState = {
|
|
69
|
+
/** Add a notification. If one with the same id exists, it is replaced. */
|
|
70
|
+
push(notification: AppNotification): void {
|
|
71
|
+
// Replace existing notification with same id
|
|
72
|
+
const idx = notifications.findIndex((n) => n.id === notification.id);
|
|
73
|
+
if (idx !== -1) {
|
|
74
|
+
clearTimer(notification.id);
|
|
75
|
+
notifications[idx] = notification;
|
|
76
|
+
// Trigger reactivity by reassigning the array
|
|
77
|
+
notifications = [...notifications];
|
|
78
|
+
} else {
|
|
79
|
+
notifications = [...notifications, notification];
|
|
80
|
+
}
|
|
81
|
+
startAutoDismiss(notification);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/** Remove a notification by id and invoke its onDismiss callback. */
|
|
85
|
+
dismiss(id: string): void {
|
|
86
|
+
const target = notifications.find((n) => n.id === id);
|
|
87
|
+
if (!target) return;
|
|
88
|
+
clearTimer(id);
|
|
89
|
+
notifications = notifications.filter((n) => n.id !== id);
|
|
90
|
+
target.onDismiss?.();
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/** Remove all notifications, invoking each onDismiss callback. */
|
|
94
|
+
dismissAll(): void {
|
|
95
|
+
const copy = [...notifications];
|
|
96
|
+
for (const n of copy) {
|
|
97
|
+
clearTimer(n.id);
|
|
98
|
+
}
|
|
99
|
+
notifications = [];
|
|
100
|
+
for (const n of copy) {
|
|
101
|
+
n.onDismiss?.();
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/** Sorted notification list (highest priority first). Reactive. */
|
|
106
|
+
get notifications(): AppNotification[] {
|
|
107
|
+
return sorted(notifications);
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/** The highest-priority notification, or undefined if empty. Reactive. */
|
|
111
|
+
get current(): AppNotification | undefined {
|
|
112
|
+
return sorted(notifications)[0];
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/** Total number of active notifications. Reactive. */
|
|
116
|
+
get count(): number {
|
|
117
|
+
return notifications.length;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin API -- the object passed to each plugin's register() function.
|
|
3
|
+
*
|
|
4
|
+
* Provides methods to register commands, tools, panels, exporters, importers,
|
|
5
|
+
* and shortcuts, plus helpers to dispatch commands and query app state.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Command } from './commands.js';
|
|
9
|
+
import type { PluginAPI } from './plugin-types.js';
|
|
10
|
+
import {
|
|
11
|
+
commandRegistry,
|
|
12
|
+
toolRegistry,
|
|
13
|
+
panelRegistry,
|
|
14
|
+
exporterRegistry,
|
|
15
|
+
importerRegistry,
|
|
16
|
+
shortcutRegistry,
|
|
17
|
+
menuRegistry,
|
|
18
|
+
toolbarRegistry,
|
|
19
|
+
} from './registries.svelte.js';
|
|
20
|
+
import { dispatch as dispatchCommand, onExecute } from './dispatcher.js';
|
|
21
|
+
import { notificationState } from './notification-state.svelte.js';
|
|
22
|
+
import { canvasState } from '../canvas/canvas-state.svelte.js';
|
|
23
|
+
import { PixelBuffer } from '../canvas/pixel-buffer.js';
|
|
24
|
+
import {
|
|
25
|
+
getCurrentFrame,
|
|
26
|
+
getFrames as getFramesInternal,
|
|
27
|
+
addFrame as addFrameInternal,
|
|
28
|
+
setFrameDuration as setFrameDurationInternal,
|
|
29
|
+
setCurrentFrame as setCurrentFrameInternal,
|
|
30
|
+
setGlobalFps as setGlobalFpsInternal,
|
|
31
|
+
deserialize as deserializeFramesInternal,
|
|
32
|
+
} from '../animation/frame-model.svelte.js';
|
|
33
|
+
import {
|
|
34
|
+
getLayers,
|
|
35
|
+
getActiveLayer,
|
|
36
|
+
addLayer as addLayerInternal,
|
|
37
|
+
addGroup as addGroupInternal,
|
|
38
|
+
setVisibility as setVisibilityInternal,
|
|
39
|
+
setOpacity as setOpacityInternal,
|
|
40
|
+
deserialize as deserializeLayersInternal,
|
|
41
|
+
} from '../layers/layer-tree.svelte.js';
|
|
42
|
+
import { setProjectPalette as setProjectPaletteInternal } from '../color/palette-state.svelte.js';
|
|
43
|
+
import {
|
|
44
|
+
hasSelection as selectionHasSelection,
|
|
45
|
+
getSelectedPixels as selectionGetSelectedPixels,
|
|
46
|
+
} from '../../../plugins/builtin/selection-tool.js';
|
|
47
|
+
|
|
48
|
+
// Re-export so existing consumers can import PluginAPI from either location
|
|
49
|
+
export type { PluginAPI } from './plugin-types.js';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a PluginAPI instance scoped to a specific plugin.
|
|
53
|
+
* The pluginName is used to namespace registrations and fill command metadata.
|
|
54
|
+
*/
|
|
55
|
+
export function createPluginAPI(pluginName: string): PluginAPI {
|
|
56
|
+
// Method parameter types are inferred from the PluginAPI return type
|
|
57
|
+
return {
|
|
58
|
+
addCommand(name, definition) {
|
|
59
|
+
// Stamp the plugin name onto the definition so executeOrDispatch() can
|
|
60
|
+
// construct well-formed Command objects when routing through dispatch().
|
|
61
|
+
commandRegistry.set(name, { ...definition, _plugin: pluginName });
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
addTool(name, definition) {
|
|
65
|
+
toolRegistry.set(name, definition);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
addPanel(name, definition) {
|
|
69
|
+
panelRegistry.set(name, definition);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
addExporter(name, definition) {
|
|
73
|
+
exporterRegistry.set(name, definition);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
addImporter(name, definition) {
|
|
77
|
+
importerRegistry.set(name, definition);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
addShortcut(name, definition) {
|
|
81
|
+
shortcutRegistry.set(name, definition);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
addMenuItem(name, contribution) {
|
|
85
|
+
menuRegistry.set(name, contribution);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
addToolbarItem(name, contribution) {
|
|
89
|
+
toolbarRegistry.set(name, contribution);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
notify(notification) {
|
|
93
|
+
notificationState.push(notification);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
dismissNotification(id) {
|
|
97
|
+
notificationState.dismiss(id);
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
dispatch(partial: Omit<Command, 'id' | 'timestamp'>) {
|
|
101
|
+
const command: Command = {
|
|
102
|
+
...partial,
|
|
103
|
+
id: crypto.randomUUID(),
|
|
104
|
+
timestamp: Date.now(),
|
|
105
|
+
};
|
|
106
|
+
dispatchCommand(command);
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
onCommand(callback) {
|
|
110
|
+
return onExecute(callback);
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
getCanvas() {
|
|
114
|
+
return canvasState;
|
|
115
|
+
},
|
|
116
|
+
getProject() {
|
|
117
|
+
return {
|
|
118
|
+
name: 'default',
|
|
119
|
+
width: canvasState.canvasWidth,
|
|
120
|
+
height: canvasState.canvasHeight,
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
getActiveFrame() {
|
|
124
|
+
return getCurrentFrame();
|
|
125
|
+
},
|
|
126
|
+
getActiveLayers() {
|
|
127
|
+
return { all: getLayers(), active: getActiveLayer() };
|
|
128
|
+
},
|
|
129
|
+
getSelection() {
|
|
130
|
+
// Return a stable object of bound methods so callers can capture it once
|
|
131
|
+
// at register time; each method invocation reads live selection state.
|
|
132
|
+
return {
|
|
133
|
+
hasSelection: selectionHasSelection,
|
|
134
|
+
getSelectedPixels: selectionGetSelectedPixels,
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
// --- Mutators ---
|
|
139
|
+
|
|
140
|
+
setCanvasSize(width, height) {
|
|
141
|
+
canvasState.canvasWidth = width;
|
|
142
|
+
canvasState.canvasHeight = height;
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
addLayer(name, options) {
|
|
146
|
+
return addLayerInternal(name, options).id;
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
addGroup(name, options) {
|
|
150
|
+
return addGroupInternal(name, options).id;
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
setLayerVisibility(id, visible) {
|
|
154
|
+
setVisibilityInternal(id, visible);
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
setLayerOpacity(id, opacity) {
|
|
158
|
+
setOpacityInternal(id, opacity);
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
deserializeLayers(state) {
|
|
162
|
+
deserializeLayersInternal(state);
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
addFrame(options) {
|
|
166
|
+
return addFrameInternal(options).index;
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
getFrames() {
|
|
170
|
+
return getFramesInternal();
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
setFrameDuration(index, durationMs) {
|
|
174
|
+
setFrameDurationInternal(index, durationMs);
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
setCurrentFrame(index) {
|
|
178
|
+
setCurrentFrameInternal(index);
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
setGlobalFps(fps) {
|
|
182
|
+
setGlobalFpsInternal(fps);
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
deserializeFrames(data) {
|
|
186
|
+
deserializeFramesInternal(data);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
createPixelBuffer(width, height) {
|
|
190
|
+
return new PixelBuffer(width, height);
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
setFramePixelData(frameIndex, layerId, buffer) {
|
|
194
|
+
// Write pixel data directly into the frame's per-layer map. This is the
|
|
195
|
+
// single supported way for plugins to populate raw pixels for a given
|
|
196
|
+
// (frame, layer) pair -- importers rely on this.
|
|
197
|
+
const frames = getFramesInternal();
|
|
198
|
+
if (frameIndex < 0 || frameIndex >= frames.length) {
|
|
199
|
+
throw new Error(`Frame index ${String(frameIndex)} out of range (0..${String(frames.length - 1)}).`);
|
|
200
|
+
}
|
|
201
|
+
const frame = frames[frameIndex];
|
|
202
|
+
if (!frame) throw new Error(`Frame ${String(frameIndex)} missing`);
|
|
203
|
+
frame.pixelData.set(layerId, buffer);
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
setProjectPalette(palette) {
|
|
207
|
+
setProjectPaletteInternal(palette);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|