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,263 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { commandRegistry } from '../core/registries.svelte.js';
|
|
3
|
+
import { dispatch, undoLast, _resetForTesting as resetDispatcher } from '../core/dispatcher.js';
|
|
4
|
+
import { layerCommandsPlugin } from './layer-commands.js';
|
|
5
|
+
import * as tree from './layer-tree.svelte.js';
|
|
6
|
+
import type { Command } from '../core/commands.js';
|
|
7
|
+
import type { CommandType, ParamsOf } from '../core/command-params.js';
|
|
8
|
+
import { createPluginAPI } from '../core/plugin-api.js';
|
|
9
|
+
import { notificationState } from '../core/notification-state.svelte.js';
|
|
10
|
+
|
|
11
|
+
// --- Helpers ---
|
|
12
|
+
|
|
13
|
+
// Typed overload: validates params when type is a known command literal
|
|
14
|
+
function makeCommand<T extends CommandType>(type: T, params: ParamsOf<T>): Command;
|
|
15
|
+
// String fallback: for dynamic/unknown command types in tests
|
|
16
|
+
function makeCommand(type: string, params?: Record<string, unknown>): Command;
|
|
17
|
+
function makeCommand(type: string, params: Record<string, unknown> = {}): Command {
|
|
18
|
+
return {
|
|
19
|
+
type,
|
|
20
|
+
plugin: 'builtin/layers',
|
|
21
|
+
version: '1.0.0',
|
|
22
|
+
params,
|
|
23
|
+
timestamp: Date.now(),
|
|
24
|
+
id: crypto.randomUUID(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// --- Setup ---
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
tree._resetForTesting();
|
|
32
|
+
resetDispatcher();
|
|
33
|
+
notificationState.dismissAll();
|
|
34
|
+
// Register the layer commands plugin
|
|
35
|
+
const api = createPluginAPI('builtin/layers');
|
|
36
|
+
layerCommandsPlugin.register(api);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// --- Tests ---
|
|
40
|
+
|
|
41
|
+
describe('add_layer command', () => {
|
|
42
|
+
it('should add a layer to the tree', () => {
|
|
43
|
+
dispatch(makeCommand('add_layer', { name: 'Test Layer' }));
|
|
44
|
+
expect(tree.getLayers()).toHaveLength(1);
|
|
45
|
+
expect(tree.getLayers()[0]?.name).toBe('Test Layer');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should undo by removing the added layer', () => {
|
|
49
|
+
dispatch(makeCommand('add_layer', { name: 'Test Layer' }));
|
|
50
|
+
expect(tree.getLayers()).toHaveLength(1);
|
|
51
|
+
undoLast();
|
|
52
|
+
expect(tree.getLayers()).toHaveLength(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should describe the action', () => {
|
|
56
|
+
const def = commandRegistry.get('add_layer');
|
|
57
|
+
if (!def) throw new Error(`command 'add_layer' not registered`);
|
|
58
|
+
const desc = def.describe({ name: 'Background' });
|
|
59
|
+
expect(desc).toBe('Added layer "Background"');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('remove_layer command', () => {
|
|
64
|
+
it('should remove a layer from the tree', () => {
|
|
65
|
+
const layer = tree.addLayer('Victim');
|
|
66
|
+
dispatch(makeCommand('remove_layer', { id: layer.id }));
|
|
67
|
+
expect(tree.getLayers()).toHaveLength(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should undo by restoring the removed layer at its original position', () => {
|
|
71
|
+
const l1 = tree.addLayer('First');
|
|
72
|
+
const l2 = tree.addLayer('Second');
|
|
73
|
+
tree.addLayer('Third');
|
|
74
|
+
tree.setActiveLayer(l1.id);
|
|
75
|
+
|
|
76
|
+
dispatch(makeCommand('remove_layer', { id: l2.id }));
|
|
77
|
+
expect(tree.getLayers()).toHaveLength(2);
|
|
78
|
+
|
|
79
|
+
undoLast();
|
|
80
|
+
expect(tree.getLayers()).toHaveLength(3);
|
|
81
|
+
expect(tree.getLayers()[1]?.id).toBe(l2.id);
|
|
82
|
+
expect(tree.getLayers()[1]?.name).toBe('Second');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should describe the action', () => {
|
|
86
|
+
const layer = tree.addLayer('Background');
|
|
87
|
+
const cmd = makeCommand('remove_layer', { id: layer.id });
|
|
88
|
+
dispatch(cmd);
|
|
89
|
+
const def = commandRegistry.get('remove_layer');
|
|
90
|
+
if (!def) throw new Error(`command 'remove_layer' not registered`);
|
|
91
|
+
const desc = def.describe(cmd.params);
|
|
92
|
+
expect(desc).toContain('Background');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should fall back to root with a notification when the parent group is gone on undo', () => {
|
|
96
|
+
// Set up: a pixel layer inside a group, then delete the inner layer.
|
|
97
|
+
// Before undoing, remove the group from the tree directly (without going
|
|
98
|
+
// through the dispatcher) so that the layer-removal undo entry remains
|
|
99
|
+
// at the top of the stack but its snapshotted parentId now points at a
|
|
100
|
+
// group that no longer exists -- the orphaned-parent scenario.
|
|
101
|
+
const inner = tree.addLayer('Inner');
|
|
102
|
+
const group = tree.groupLayers([inner.id], 'Doomed Group');
|
|
103
|
+
expect(tree.getLayers()).toHaveLength(1);
|
|
104
|
+
expect(tree.getLayers()[0]?.id).toBe(group.id);
|
|
105
|
+
|
|
106
|
+
// Delete the inner layer through the dispatcher (snapshots parentId=group.id)
|
|
107
|
+
dispatch(makeCommand('remove_layer', { id: inner.id }));
|
|
108
|
+
expect(tree.getLayer(inner.id)).toBeUndefined();
|
|
109
|
+
|
|
110
|
+
// Directly remove the parent group, bypassing the dispatcher so the
|
|
111
|
+
// layer-removal entry stays at the top of the undo stack.
|
|
112
|
+
tree.removeLayer(group.id);
|
|
113
|
+
expect(tree.getLayer(group.id)).toBeUndefined();
|
|
114
|
+
expect(tree.getLayers()).toHaveLength(0);
|
|
115
|
+
|
|
116
|
+
// Undoing the layer removal should land it at the root, not silently drop it.
|
|
117
|
+
notificationState.dismissAll();
|
|
118
|
+
undoLast();
|
|
119
|
+
|
|
120
|
+
expect(tree.getLayer(inner.id)).toBeDefined();
|
|
121
|
+
expect(tree.getLayers().some((l) => l.id === inner.id)).toBe(true);
|
|
122
|
+
|
|
123
|
+
// And a warning notification should have been pushed.
|
|
124
|
+
const warning = notificationState.notifications.find(
|
|
125
|
+
(n) => n.id === `restore-layer-orphan-${inner.id}`,
|
|
126
|
+
);
|
|
127
|
+
expect(warning).toBeDefined();
|
|
128
|
+
expect(warning?.type).toBe('warning');
|
|
129
|
+
expect(warning?.message).toContain('Parent group for restored layer no longer exists');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('rename_layer command', () => {
|
|
134
|
+
it('should rename the layer', () => {
|
|
135
|
+
const layer = tree.addLayer('Old');
|
|
136
|
+
dispatch(makeCommand('rename_layer', { id: layer.id, name: 'New' }));
|
|
137
|
+
expect(layer.name).toBe('New');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should undo to restore old name', () => {
|
|
141
|
+
const layer = tree.addLayer('Old');
|
|
142
|
+
dispatch(makeCommand('rename_layer', { id: layer.id, name: 'New' }));
|
|
143
|
+
undoLast();
|
|
144
|
+
expect(layer.name).toBe('Old');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should describe the action', () => {
|
|
148
|
+
const def = commandRegistry.get('rename_layer');
|
|
149
|
+
if (!def) throw new Error(`command 'rename_layer' not registered`);
|
|
150
|
+
const desc = def.describe({ name: 'Hair' });
|
|
151
|
+
expect(desc).toBe('Renamed layer to "Hair"');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('move_layer command', () => {
|
|
156
|
+
it('should move a layer and undo to restore original position', () => {
|
|
157
|
+
const l1 = tree.addLayer('First');
|
|
158
|
+
tree.addLayer('Second');
|
|
159
|
+
tree.setActiveLayer(l1.id);
|
|
160
|
+
|
|
161
|
+
// Move l1 to index 1 (above l2)
|
|
162
|
+
dispatch(makeCommand('move_layer', { id: l1.id, newParentId: null, newIndex: 1 }));
|
|
163
|
+
expect(tree.getLayers()[1]?.id).toBe(l1.id);
|
|
164
|
+
|
|
165
|
+
undoLast();
|
|
166
|
+
expect(tree.getLayers()[0]?.id).toBe(l1.id);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('set_layer_visibility command', () => {
|
|
171
|
+
it('should toggle visibility and undo', () => {
|
|
172
|
+
const layer = tree.addLayer('Layer');
|
|
173
|
+
dispatch(makeCommand('set_layer_visibility', { id: layer.id, visible: false }));
|
|
174
|
+
expect(layer.visible).toBe(false);
|
|
175
|
+
|
|
176
|
+
undoLast();
|
|
177
|
+
expect(layer.visible).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('set_layer_opacity command', () => {
|
|
182
|
+
it('should change opacity and undo', () => {
|
|
183
|
+
const layer = tree.addLayer('Layer');
|
|
184
|
+
dispatch(makeCommand('set_layer_opacity', { id: layer.id, opacity: 50 }));
|
|
185
|
+
expect(layer.opacity).toBe(50);
|
|
186
|
+
|
|
187
|
+
undoLast();
|
|
188
|
+
expect(layer.opacity).toBe(100);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('set_active_layer command', () => {
|
|
193
|
+
it('should change active layer and undo', () => {
|
|
194
|
+
const l1 = tree.addLayer('A');
|
|
195
|
+
const l2 = tree.addLayer('B');
|
|
196
|
+
// l2 is active after addLayer
|
|
197
|
+
|
|
198
|
+
dispatch(makeCommand('set_active_layer', { id: l1.id }));
|
|
199
|
+
expect(tree.getActiveLayerId()).toBe(l1.id);
|
|
200
|
+
|
|
201
|
+
undoLast();
|
|
202
|
+
expect(tree.getActiveLayerId()).toBe(l2.id);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('group_layers command', () => {
|
|
207
|
+
it('should group and undo', () => {
|
|
208
|
+
const l1 = tree.addLayer('A');
|
|
209
|
+
const l2 = tree.addLayer('B');
|
|
210
|
+
|
|
211
|
+
dispatch(makeCommand('group_layers', { ids: [l1.id, l2.id], groupName: 'Group' }));
|
|
212
|
+
expect(tree.getLayers()).toHaveLength(1);
|
|
213
|
+
expect(tree.getLayers()[0]?.type).toBe('group');
|
|
214
|
+
|
|
215
|
+
undoLast();
|
|
216
|
+
expect(tree.getLayers()).toHaveLength(2);
|
|
217
|
+
expect(tree.getLayers()[0]?.id).toBe(l1.id);
|
|
218
|
+
expect(tree.getLayers()[1]?.id).toBe(l2.id);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('ungroup_layer command', () => {
|
|
223
|
+
it('should ungroup and undo', () => {
|
|
224
|
+
const l1 = tree.addLayer('A');
|
|
225
|
+
const l2 = tree.addLayer('B');
|
|
226
|
+
const group = tree.groupLayers([l1.id, l2.id], 'Group');
|
|
227
|
+
|
|
228
|
+
dispatch(makeCommand('ungroup_layer', { id: group.id }));
|
|
229
|
+
expect(tree.getLayers()).toHaveLength(2);
|
|
230
|
+
expect(tree.getLayers()[0]?.id).toBe(l1.id);
|
|
231
|
+
|
|
232
|
+
undoLast();
|
|
233
|
+
expect(tree.getLayers()).toHaveLength(1);
|
|
234
|
+
expect(tree.getLayers()[0]?.type).toBe('group');
|
|
235
|
+
expect(tree.getLayers()[0]?.children).toHaveLength(2);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('duplicate_layer command', () => {
|
|
240
|
+
it('should duplicate and undo', () => {
|
|
241
|
+
const layer = tree.addLayer('Original');
|
|
242
|
+
dispatch(makeCommand('duplicate_layer', { id: layer.id }));
|
|
243
|
+
expect(tree.getLayers()).toHaveLength(2);
|
|
244
|
+
|
|
245
|
+
undoLast();
|
|
246
|
+
expect(tree.getLayers()).toHaveLength(1);
|
|
247
|
+
expect(tree.getLayers()[0]?.id).toBe(layer.id);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('toggle_expanded command', () => {
|
|
252
|
+
it('should toggle and undo (toggle again)', () => {
|
|
253
|
+
tree.addLayer('Pixel'); // need at least one pixel layer for active
|
|
254
|
+
const group = tree.addGroup('Group');
|
|
255
|
+
expect(group.expanded).toBe(true);
|
|
256
|
+
|
|
257
|
+
dispatch(makeCommand('toggle_expanded', { id: group.id }));
|
|
258
|
+
expect(group.expanded).toBe(false);
|
|
259
|
+
|
|
260
|
+
undoLast();
|
|
261
|
+
expect(group.expanded).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer Commands Plugin -- registers layer operations as undoable commands.
|
|
3
|
+
*
|
|
4
|
+
* Each command returns a snapshot from execute() that undo() uses to restore state.
|
|
5
|
+
* The layer tree module is imported directly (module-level singleton).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PluginModule } from '../core/plugin-loader.js';
|
|
9
|
+
import type { Layer, BlendMode } from './layer-types.js';
|
|
10
|
+
import * as tree from './layer-tree.svelte.js';
|
|
11
|
+
import { notificationState } from '../core/notification-state.svelte.js';
|
|
12
|
+
|
|
13
|
+
/** Snapshot of a removed layer's position for undo restore. */
|
|
14
|
+
interface LayerPosition {
|
|
15
|
+
parentId: string | null;
|
|
16
|
+
index: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Find the parent ID and index of a layer in the tree. */
|
|
20
|
+
function getLayerPosition(id: string): LayerPosition {
|
|
21
|
+
const parent = tree.getParent(id);
|
|
22
|
+
const siblings = parent?.children ?? tree.getLayers();
|
|
23
|
+
const index = siblings.findIndex((l) => l.id === id);
|
|
24
|
+
return {
|
|
25
|
+
parentId: parent?.id ?? null,
|
|
26
|
+
index,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Deep-clone a layer for snapshot storage (plain object, no references). */
|
|
31
|
+
function snapshotLayer(layer: Layer): Layer {
|
|
32
|
+
const snap: Layer = { ...layer };
|
|
33
|
+
if (layer.children) {
|
|
34
|
+
snap.children = layer.children.map(snapshotLayer);
|
|
35
|
+
}
|
|
36
|
+
return snap;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Re-insert a snapshot layer at the given position.
|
|
40
|
+
*
|
|
41
|
+
* If the original parent group no longer exists (e.g. the user deleted the
|
|
42
|
+
* parent group after the layer was deleted, then undid the layer deletion),
|
|
43
|
+
* fall back to inserting at the root so the layer is not silently dropped.
|
|
44
|
+
* A warning notification is surfaced so the user knows the position changed.
|
|
45
|
+
*/
|
|
46
|
+
function restoreLayer(snapshot: Layer, position: LayerPosition): void {
|
|
47
|
+
let siblings: Layer[] | undefined = position.parentId
|
|
48
|
+
? tree.getLayer(position.parentId)?.children
|
|
49
|
+
: tree.getLayers();
|
|
50
|
+
let index = position.index;
|
|
51
|
+
|
|
52
|
+
if (!siblings) {
|
|
53
|
+
// Parent group was removed between delete and undo -- fall back to root
|
|
54
|
+
// instead of silently dropping the restored layer.
|
|
55
|
+
siblings = tree.getLayers();
|
|
56
|
+
index = siblings.length;
|
|
57
|
+
notificationState.push({
|
|
58
|
+
id: `restore-layer-orphan-${snapshot.id}`,
|
|
59
|
+
message: 'Parent group for restored layer no longer exists; placed at root instead.',
|
|
60
|
+
type: 'warning',
|
|
61
|
+
autoDismissMs: 5000,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
siblings.splice(index, 0, snapshot);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- Snapshot types for each command ---
|
|
69
|
+
|
|
70
|
+
type AddLayerSnapshot = { previousActiveLayerId: string | null };
|
|
71
|
+
type RemoveLayerSnapshot = {
|
|
72
|
+
layer: Layer;
|
|
73
|
+
position: LayerPosition;
|
|
74
|
+
previousActiveLayerId: string | null;
|
|
75
|
+
};
|
|
76
|
+
type RenameLayerSnapshot = { oldName: string };
|
|
77
|
+
type MoveLayerSnapshot = { oldPosition: LayerPosition };
|
|
78
|
+
type SetVisibilitySnapshot = { oldVisible: boolean };
|
|
79
|
+
type SetOpacitySnapshot = { oldOpacity: number };
|
|
80
|
+
type SetBlendModeSnapshot = { oldBlendMode: BlendMode };
|
|
81
|
+
type SetLockedSnapshot = { oldLocked: boolean };
|
|
82
|
+
type SetActiveLayerSnapshot = { oldActiveLayerId: string | null };
|
|
83
|
+
type GroupLayersSnapshot = {
|
|
84
|
+
groupId: string;
|
|
85
|
+
previousActiveLayerId: string | null;
|
|
86
|
+
};
|
|
87
|
+
type UngroupLayerSnapshot = {
|
|
88
|
+
group: Layer;
|
|
89
|
+
position: LayerPosition;
|
|
90
|
+
previousActiveLayerId: string | null;
|
|
91
|
+
};
|
|
92
|
+
type DuplicateLayerSnapshot = {
|
|
93
|
+
cloneId: string;
|
|
94
|
+
previousActiveLayerId: string | null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const layerCommandsPlugin: PluginModule = {
|
|
98
|
+
name: 'builtin/layers',
|
|
99
|
+
version: '1.0.0',
|
|
100
|
+
description: 'Layer management commands',
|
|
101
|
+
dependencies: [],
|
|
102
|
+
|
|
103
|
+
register(api) {
|
|
104
|
+
// --- add_layer ---
|
|
105
|
+
api.addCommand('add_layer', {
|
|
106
|
+
tier: 'project',
|
|
107
|
+
execute(params) {
|
|
108
|
+
const previousActiveLayerId = tree.getActiveLayerId();
|
|
109
|
+
// Build options without undefined values (exactOptionalPropertyTypes)
|
|
110
|
+
const addOptions: { parentId?: string; index?: number; id?: string } = {};
|
|
111
|
+
if (params["parentId"] !== undefined) addOptions.parentId = params["parentId"];
|
|
112
|
+
if (params["index"] !== undefined) addOptions.index = params["index"];
|
|
113
|
+
if (params["id"] !== undefined) addOptions.id = params["id"];
|
|
114
|
+
const layer = tree.addLayer(params["name"], addOptions);
|
|
115
|
+
// Store the created layer's ID for undo (and to keep params stable on redo)
|
|
116
|
+
params["id"] = layer.id;
|
|
117
|
+
const snapshot: AddLayerSnapshot = { previousActiveLayerId };
|
|
118
|
+
return snapshot;
|
|
119
|
+
},
|
|
120
|
+
undo(params, _ctx, snapshot) {
|
|
121
|
+
const typed = snapshot as AddLayerSnapshot | undefined;
|
|
122
|
+
tree.removeLayer(params["id"]!);
|
|
123
|
+
// Restore previous active layer
|
|
124
|
+
const previousActive = typed?.previousActiveLayerId;
|
|
125
|
+
if (previousActive && tree.getLayer(previousActive)) {
|
|
126
|
+
const layer = tree.getLayer(previousActive);
|
|
127
|
+
if (layer?.type === 'pixel') {
|
|
128
|
+
tree.setActiveLayer(previousActive);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
describe(params) {
|
|
133
|
+
return `Added layer "${String(params["name"])}"`;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// --- remove_layer ---
|
|
138
|
+
api.addCommand('remove_layer', {
|
|
139
|
+
tier: 'project',
|
|
140
|
+
execute(params) {
|
|
141
|
+
const layer = tree.getLayer(params["id"]);
|
|
142
|
+
if (!layer) return;
|
|
143
|
+
// Stash name on params so describe() can use it (describe only gets params)
|
|
144
|
+
params["_removedName"] = layer.name;
|
|
145
|
+
// Snapshot the layer and its position for undo
|
|
146
|
+
const snapshot: RemoveLayerSnapshot = {
|
|
147
|
+
layer: snapshotLayer(layer),
|
|
148
|
+
position: getLayerPosition(params["id"]),
|
|
149
|
+
previousActiveLayerId: tree.getActiveLayerId(),
|
|
150
|
+
};
|
|
151
|
+
tree.removeLayer(params["id"]);
|
|
152
|
+
return snapshot;
|
|
153
|
+
},
|
|
154
|
+
undo(_params, _ctx, snapshot) {
|
|
155
|
+
const typed = snapshot as RemoveLayerSnapshot | undefined;
|
|
156
|
+
if (!typed) return;
|
|
157
|
+
restoreLayer(typed.layer, typed.position);
|
|
158
|
+
// Restore previous active layer
|
|
159
|
+
const previousActive = typed.previousActiveLayerId;
|
|
160
|
+
if (previousActive && tree.getLayer(previousActive)) {
|
|
161
|
+
const layer = tree.getLayer(previousActive);
|
|
162
|
+
if (layer?.type === 'pixel') {
|
|
163
|
+
tree.setActiveLayer(previousActive);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
describe(params) {
|
|
168
|
+
return `Removed layer "${String(params["_removedName"] ?? params["id"])}"`;
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// --- rename_layer ---
|
|
173
|
+
api.addCommand('rename_layer', {
|
|
174
|
+
tier: 'project',
|
|
175
|
+
execute(params) {
|
|
176
|
+
const layer = tree.getLayer(params["id"]);
|
|
177
|
+
if (!layer) return;
|
|
178
|
+
const snapshot: RenameLayerSnapshot = { oldName: layer.name };
|
|
179
|
+
tree.renameLayer(params["id"], params["name"]);
|
|
180
|
+
return snapshot;
|
|
181
|
+
},
|
|
182
|
+
undo(params, _ctx, snapshot) {
|
|
183
|
+
const typed = snapshot as RenameLayerSnapshot | undefined;
|
|
184
|
+
if (!typed) return;
|
|
185
|
+
tree.renameLayer(params["id"], typed.oldName);
|
|
186
|
+
},
|
|
187
|
+
describe(params) {
|
|
188
|
+
return `Renamed layer to "${String(params["name"])}"`;
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// --- move_layer ---
|
|
193
|
+
api.addCommand('move_layer', {
|
|
194
|
+
tier: 'project',
|
|
195
|
+
execute(params) {
|
|
196
|
+
// Capture old position for undo
|
|
197
|
+
const snapshot: MoveLayerSnapshot = {
|
|
198
|
+
oldPosition: getLayerPosition(params["id"]),
|
|
199
|
+
};
|
|
200
|
+
tree.moveLayer(
|
|
201
|
+
params["id"],
|
|
202
|
+
params["newParentId"] ?? null,
|
|
203
|
+
params["newIndex"],
|
|
204
|
+
);
|
|
205
|
+
return snapshot;
|
|
206
|
+
},
|
|
207
|
+
undo(params, _ctx, snapshot) {
|
|
208
|
+
const typed = snapshot as MoveLayerSnapshot | undefined;
|
|
209
|
+
if (!typed) return;
|
|
210
|
+
tree.moveLayer(params["id"], typed.oldPosition.parentId, typed.oldPosition.index);
|
|
211
|
+
},
|
|
212
|
+
describe(params) {
|
|
213
|
+
return `Moved layer "${String(params["id"])}"`;
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// --- set_layer_visibility ---
|
|
218
|
+
api.addCommand('set_layer_visibility', {
|
|
219
|
+
tier: 'project',
|
|
220
|
+
execute(params) {
|
|
221
|
+
const layer = tree.getLayer(params["id"]);
|
|
222
|
+
if (!layer) return;
|
|
223
|
+
const snapshot: SetVisibilitySnapshot = { oldVisible: layer.visible };
|
|
224
|
+
tree.setVisibility(params["id"], params["visible"]);
|
|
225
|
+
return snapshot;
|
|
226
|
+
},
|
|
227
|
+
undo(params, _ctx, snapshot) {
|
|
228
|
+
const typed = snapshot as SetVisibilitySnapshot | undefined;
|
|
229
|
+
if (!typed) return;
|
|
230
|
+
tree.setVisibility(params["id"], typed.oldVisible);
|
|
231
|
+
},
|
|
232
|
+
describe(params) {
|
|
233
|
+
return params["visible"] ? `Showed layer` : `Hid layer`;
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// --- set_layer_opacity ---
|
|
238
|
+
api.addCommand('set_layer_opacity', {
|
|
239
|
+
tier: 'project',
|
|
240
|
+
execute(params) {
|
|
241
|
+
const layer = tree.getLayer(params["id"]);
|
|
242
|
+
if (!layer) return;
|
|
243
|
+
const snapshot: SetOpacitySnapshot = { oldOpacity: layer.opacity };
|
|
244
|
+
tree.setOpacity(params["id"], params["opacity"]);
|
|
245
|
+
return snapshot;
|
|
246
|
+
},
|
|
247
|
+
undo(params, _ctx, snapshot) {
|
|
248
|
+
const typed = snapshot as SetOpacitySnapshot | undefined;
|
|
249
|
+
if (!typed) return;
|
|
250
|
+
tree.setOpacity(params["id"], typed.oldOpacity);
|
|
251
|
+
},
|
|
252
|
+
describe(params) {
|
|
253
|
+
return `Set layer opacity to ${String(params["opacity"])}%`;
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// --- set_layer_blend_mode ---
|
|
258
|
+
api.addCommand('set_layer_blend_mode', {
|
|
259
|
+
tier: 'project',
|
|
260
|
+
execute(params) {
|
|
261
|
+
const layer = tree.getLayer(params["id"]);
|
|
262
|
+
if (!layer) return;
|
|
263
|
+
const snapshot: SetBlendModeSnapshot = { oldBlendMode: layer.blendMode };
|
|
264
|
+
tree.setBlendMode(params["id"], params["mode"] as BlendMode);
|
|
265
|
+
return snapshot;
|
|
266
|
+
},
|
|
267
|
+
undo(params, _ctx, snapshot) {
|
|
268
|
+
const typed = snapshot as SetBlendModeSnapshot | undefined;
|
|
269
|
+
if (!typed) return;
|
|
270
|
+
tree.setBlendMode(params["id"], typed.oldBlendMode);
|
|
271
|
+
},
|
|
272
|
+
describe(params) {
|
|
273
|
+
return `Set blend mode to "${String(params["mode"])}"`;
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// --- set_layer_locked ---
|
|
278
|
+
api.addCommand('set_layer_locked', {
|
|
279
|
+
tier: 'project',
|
|
280
|
+
execute(params) {
|
|
281
|
+
const layer = tree.getLayer(params["id"]);
|
|
282
|
+
if (!layer) return;
|
|
283
|
+
const snapshot: SetLockedSnapshot = { oldLocked: layer.locked };
|
|
284
|
+
tree.setLocked(params["id"], params["locked"]);
|
|
285
|
+
return snapshot;
|
|
286
|
+
},
|
|
287
|
+
undo(params, _ctx, snapshot) {
|
|
288
|
+
const typed = snapshot as SetLockedSnapshot | undefined;
|
|
289
|
+
if (!typed) return;
|
|
290
|
+
tree.setLocked(params["id"], typed.oldLocked);
|
|
291
|
+
},
|
|
292
|
+
describe(params) {
|
|
293
|
+
return params["locked"] ? 'Locked layer' : 'Unlocked layer';
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// --- set_active_layer ---
|
|
298
|
+
api.addCommand('set_active_layer', {
|
|
299
|
+
tier: 'project',
|
|
300
|
+
execute(params) {
|
|
301
|
+
const snapshot: SetActiveLayerSnapshot = {
|
|
302
|
+
oldActiveLayerId: tree.getActiveLayerId(),
|
|
303
|
+
};
|
|
304
|
+
tree.setActiveLayer(params["id"]);
|
|
305
|
+
return snapshot;
|
|
306
|
+
},
|
|
307
|
+
undo(_params, _ctx, snapshot) {
|
|
308
|
+
const typed = snapshot as SetActiveLayerSnapshot | undefined;
|
|
309
|
+
if (!typed) return;
|
|
310
|
+
const oldId = typed.oldActiveLayerId;
|
|
311
|
+
if (oldId && tree.getLayer(oldId)?.type === 'pixel') {
|
|
312
|
+
tree.setActiveLayer(oldId);
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
describe() {
|
|
316
|
+
return 'Changed active layer';
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// --- group_layers ---
|
|
321
|
+
api.addCommand('group_layers', {
|
|
322
|
+
tier: 'project',
|
|
323
|
+
execute(params) {
|
|
324
|
+
const ids = params["ids"];
|
|
325
|
+
const previousActiveLayerId = tree.getActiveLayerId();
|
|
326
|
+
const group = tree.groupLayers(ids, params["groupName"]);
|
|
327
|
+
const snapshot: GroupLayersSnapshot = {
|
|
328
|
+
groupId: group.id,
|
|
329
|
+
previousActiveLayerId,
|
|
330
|
+
};
|
|
331
|
+
return snapshot;
|
|
332
|
+
},
|
|
333
|
+
undo(_params, _ctx, snapshot) {
|
|
334
|
+
const typed = snapshot as GroupLayersSnapshot | undefined;
|
|
335
|
+
if (!typed) return;
|
|
336
|
+
// Ungroup, then restore original positions
|
|
337
|
+
tree.ungroupLayer(typed.groupId);
|
|
338
|
+
// Restore active layer
|
|
339
|
+
const previousActive = typed.previousActiveLayerId;
|
|
340
|
+
if (previousActive && tree.getLayer(previousActive)?.type === 'pixel') {
|
|
341
|
+
tree.setActiveLayer(previousActive);
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
describe(params) {
|
|
345
|
+
return `Grouped layers into "${String(params["groupName"])}"`;
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// --- ungroup_layer ---
|
|
350
|
+
api.addCommand('ungroup_layer', {
|
|
351
|
+
tier: 'project',
|
|
352
|
+
execute(params) {
|
|
353
|
+
const group = tree.getLayer(params["id"]);
|
|
354
|
+
if (!group || group.type !== 'group') return;
|
|
355
|
+
// Stash name on params so describe() can use it (describe only gets params)
|
|
356
|
+
params["_removedName"] = group.name;
|
|
357
|
+
// Snapshot the group for undo
|
|
358
|
+
const snapshot: UngroupLayerSnapshot = {
|
|
359
|
+
group: snapshotLayer(group),
|
|
360
|
+
position: getLayerPosition(params["id"]),
|
|
361
|
+
previousActiveLayerId: tree.getActiveLayerId(),
|
|
362
|
+
};
|
|
363
|
+
tree.ungroupLayer(params["id"]);
|
|
364
|
+
return snapshot;
|
|
365
|
+
},
|
|
366
|
+
undo(_params, _ctx, snapshot) {
|
|
367
|
+
const typed = snapshot as UngroupLayerSnapshot | undefined;
|
|
368
|
+
if (!typed) return;
|
|
369
|
+
|
|
370
|
+
// Remove the children that were flattened out
|
|
371
|
+
const childIds = typed.group.children?.map((c) => c.id) ?? [];
|
|
372
|
+
for (const childId of childIds) {
|
|
373
|
+
tree.removeLayer(childId);
|
|
374
|
+
}
|
|
375
|
+
// Restore the group
|
|
376
|
+
restoreLayer(typed.group, typed.position);
|
|
377
|
+
// Restore active layer
|
|
378
|
+
const previousActive = typed.previousActiveLayerId;
|
|
379
|
+
if (previousActive && tree.getLayer(previousActive)?.type === 'pixel') {
|
|
380
|
+
tree.setActiveLayer(previousActive);
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
describe(params) {
|
|
384
|
+
return `Ungrouped "${String(params["_removedName"] ?? params["id"])}"`;
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// --- duplicate_layer ---
|
|
389
|
+
api.addCommand('duplicate_layer', {
|
|
390
|
+
tier: 'project',
|
|
391
|
+
execute(params) {
|
|
392
|
+
const previousActiveLayerId = tree.getActiveLayerId();
|
|
393
|
+
const clone = tree.duplicateLayer(params["id"]);
|
|
394
|
+
const snapshot: DuplicateLayerSnapshot = {
|
|
395
|
+
cloneId: clone.id,
|
|
396
|
+
previousActiveLayerId,
|
|
397
|
+
};
|
|
398
|
+
return snapshot;
|
|
399
|
+
},
|
|
400
|
+
undo(_params, _ctx, snapshot) {
|
|
401
|
+
const typed = snapshot as DuplicateLayerSnapshot | undefined;
|
|
402
|
+
if (!typed) return;
|
|
403
|
+
tree.removeLayer(typed.cloneId);
|
|
404
|
+
const previousActive = typed.previousActiveLayerId;
|
|
405
|
+
if (previousActive && tree.getLayer(previousActive)?.type === 'pixel') {
|
|
406
|
+
tree.setActiveLayer(previousActive);
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
describe() {
|
|
410
|
+
return `Duplicated layer`;
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// --- toggle_expanded ---
|
|
415
|
+
api.addCommand('toggle_expanded', {
|
|
416
|
+
tier: 'project',
|
|
417
|
+
execute(params) {
|
|
418
|
+
tree.toggleExpanded(params["id"]);
|
|
419
|
+
},
|
|
420
|
+
undo(params) {
|
|
421
|
+
// Toggle is its own inverse
|
|
422
|
+
tree.toggleExpanded(params["id"]);
|
|
423
|
+
},
|
|
424
|
+
describe() {
|
|
425
|
+
return 'Toggled group expansion';
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
},
|
|
429
|
+
};
|