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,594 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variant Commands Plugin -- registers variant operations as undoable commands.
|
|
3
|
+
*
|
|
4
|
+
* Provides preset-level commands (add/remove, color overrides) as well as
|
|
5
|
+
* higher-level batch operations dispatched from the VariantPanel:
|
|
6
|
+
* - generate_variants: creates N new variants (random or interpolated)
|
|
7
|
+
* - apply_variant: toggles the active preset (undoable)
|
|
8
|
+
* - extract_palette_from_layer: read-only feedback via notification
|
|
9
|
+
* - export_bisection_atlas: triggers a PNG download of the variant atlas
|
|
10
|
+
* - export_variants_batch: downloads each variant as a separate PNG file
|
|
11
|
+
*
|
|
12
|
+
* Depends on the builtin/layers plugin for layer tree access.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { PluginModule } from '../core/plugin-loader.js';
|
|
16
|
+
import type { Layer } from '../layers/layer-types.js';
|
|
17
|
+
import type { VariantPreset } from './variant-state.svelte.js';
|
|
18
|
+
import * as variantState from './variant-state.svelte.js';
|
|
19
|
+
import { getLayers, getActiveLayer } from '../layers/layer-tree.svelte.js';
|
|
20
|
+
import { getFrames } from '../animation/frame-model.svelte.js';
|
|
21
|
+
import { canvasState } from '../canvas/canvas-state.svelte.js';
|
|
22
|
+
import { notificationState } from '../core/notification-state.svelte.js';
|
|
23
|
+
import { extractGroupPalette } from './palette-extraction.js';
|
|
24
|
+
import { generateRandomVariant } from './variant-randomizer.js';
|
|
25
|
+
import { generateInterpolatedSeries } from './palette-interpolation.js';
|
|
26
|
+
import { exportGroupSpritesheet } from './bisection-export.js';
|
|
27
|
+
import { PixelBuffer } from '../canvas/pixel-buffer.js';
|
|
28
|
+
import { dialogState } from '../ui/dialog-state.svelte.js';
|
|
29
|
+
import { downloadBatch } from '../export/download.js';
|
|
30
|
+
|
|
31
|
+
/** Snapshot a preset for undo restore (deep copy of maps to plain objects). */
|
|
32
|
+
function snapshotPreset(preset: VariantPreset): {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
groupOverrides: Record<string, Record<string, string>>;
|
|
36
|
+
} {
|
|
37
|
+
const groupOverrides: Record<string, Record<string, string>> = {};
|
|
38
|
+
for (const [groupId, colorMap] of preset.groupOverrides) {
|
|
39
|
+
groupOverrides[groupId] = Object.fromEntries(colorMap);
|
|
40
|
+
}
|
|
41
|
+
return { id: preset.id, name: preset.name, groupOverrides };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Restore a preset from a snapshot. */
|
|
45
|
+
function restorePreset(snapshot: {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
groupOverrides: Record<string, Record<string, string>>;
|
|
49
|
+
}): VariantPreset {
|
|
50
|
+
const groupOverrides = new Map<string, Map<string, string>>();
|
|
51
|
+
for (const [groupId, overrides] of Object.entries(snapshot.groupOverrides)) {
|
|
52
|
+
groupOverrides.set(groupId, new Map(Object.entries(overrides)));
|
|
53
|
+
}
|
|
54
|
+
return { id: snapshot.id, name: snapshot.name, groupOverrides };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type RemovePresetSnapshot = {
|
|
58
|
+
preset: ReturnType<typeof snapshotPreset>;
|
|
59
|
+
wasActive: boolean;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type ColorOverrideSnapshot = {
|
|
63
|
+
previousColor: string | null;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Snapshot of variants created by generate_variants, used for undo. */
|
|
67
|
+
type GenerateVariantsSnapshot = {
|
|
68
|
+
/** IDs of presets that were created and need to be removed on undo. */
|
|
69
|
+
createdPresetIds: string[];
|
|
70
|
+
/** Previous active preset id (restored on undo if we changed it). */
|
|
71
|
+
previousActiveId: string | null;
|
|
72
|
+
/** How many variants were actually created (for describe on redo). */
|
|
73
|
+
count: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Snapshot of the previously active preset before apply_variant ran. */
|
|
77
|
+
type ApplyVariantSnapshot = {
|
|
78
|
+
previousActiveId: string | null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// --- Group targeting helpers ---
|
|
82
|
+
// Variant batch operations target a "group" layer. We resolve the target
|
|
83
|
+
// group by walking up from the active layer; if nothing fits, we fall back
|
|
84
|
+
// to the first group encountered in a top-level scan of the tree.
|
|
85
|
+
|
|
86
|
+
function findGroupContaining(tree: Layer[], layerId: string): Layer | null {
|
|
87
|
+
for (const layer of tree) {
|
|
88
|
+
if (layer.type === 'group' && layer.children) {
|
|
89
|
+
if (layer.children.some((c) => c.id === layerId)) return layer;
|
|
90
|
+
const nested = findGroupContaining(layer.children, layerId);
|
|
91
|
+
if (nested) return nested;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function findFirstGroup(tree: Layer[]): Layer | null {
|
|
98
|
+
for (const layer of tree) {
|
|
99
|
+
if (layer.type === 'group') return layer;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Resolve the current "target group" for a variant batch operation. */
|
|
105
|
+
function findTargetGroup(): Layer | null {
|
|
106
|
+
const active = getActiveLayer();
|
|
107
|
+
const tree = getLayers();
|
|
108
|
+
|
|
109
|
+
if (active) {
|
|
110
|
+
if (active.type === 'group') return active;
|
|
111
|
+
const containing = findGroupContaining(tree, active.id);
|
|
112
|
+
if (containing) return containing;
|
|
113
|
+
}
|
|
114
|
+
return findFirstGroup(tree);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Push a standard-format notification for variant command feedback. */
|
|
118
|
+
function notify(
|
|
119
|
+
message: string,
|
|
120
|
+
type: 'info' | 'warning' | 'success' = 'info',
|
|
121
|
+
): void {
|
|
122
|
+
notificationState.push({
|
|
123
|
+
id: `variant-cmd-${crypto.randomUUID()}`,
|
|
124
|
+
message,
|
|
125
|
+
type,
|
|
126
|
+
autoDismissMs: 4000,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Materialize a detached VariantPreset (as returned by the randomizer /
|
|
132
|
+
* interpolator) into a real preset in the reactive store. Returns the id
|
|
133
|
+
* of the newly-added preset.
|
|
134
|
+
*
|
|
135
|
+
* The input preset's id is discarded; addPreset() assigns a fresh uuid so
|
|
136
|
+
* that snapshots captured here are guaranteed to match what's in the store.
|
|
137
|
+
*/
|
|
138
|
+
function materializePreset(detached: VariantPreset, name: string): string {
|
|
139
|
+
const created = variantState.addPreset(name);
|
|
140
|
+
for (const [groupId, colorMap] of detached.groupOverrides) {
|
|
141
|
+
for (const [original, replacement] of colorMap) {
|
|
142
|
+
variantState.setColorOverride(created.id, groupId, original, replacement);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return created.id;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const variantCommandsPlugin: PluginModule = {
|
|
149
|
+
name: 'builtin/variants',
|
|
150
|
+
version: '1.0.0',
|
|
151
|
+
description: 'Variant preset management commands',
|
|
152
|
+
dependencies: ['builtin/layers'],
|
|
153
|
+
|
|
154
|
+
register(api) {
|
|
155
|
+
// --- add_variant_preset ---
|
|
156
|
+
api.addCommand('add_variant_preset', {
|
|
157
|
+
tier: 'project',
|
|
158
|
+
execute(params) {
|
|
159
|
+
const preset = variantState.addPreset(params["name"]);
|
|
160
|
+
params["_presetId"] = preset.id;
|
|
161
|
+
},
|
|
162
|
+
undo(params) {
|
|
163
|
+
variantState.removePreset(params["_presetId"]);
|
|
164
|
+
},
|
|
165
|
+
describe(params) {
|
|
166
|
+
return `Added variant preset "${String(params["name"])}"`;
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// --- remove_variant_preset ---
|
|
171
|
+
api.addCommand('remove_variant_preset', {
|
|
172
|
+
tier: 'project',
|
|
173
|
+
execute(params) {
|
|
174
|
+
const preset = variantState.getPreset(params["id"]);
|
|
175
|
+
if (!preset) return;
|
|
176
|
+
// Stash the name on params so describe() can use it (describe only gets params)
|
|
177
|
+
params["_removedName"] = preset.name;
|
|
178
|
+
const snapshot: RemovePresetSnapshot = {
|
|
179
|
+
preset: snapshotPreset(preset),
|
|
180
|
+
wasActive: variantState.getActivePresetId() === preset.id,
|
|
181
|
+
};
|
|
182
|
+
variantState.removePreset(params["id"]);
|
|
183
|
+
return snapshot;
|
|
184
|
+
},
|
|
185
|
+
undo(_params, _ctx, snapshot) {
|
|
186
|
+
const typed = snapshot as RemovePresetSnapshot | undefined;
|
|
187
|
+
if (!typed) return;
|
|
188
|
+
// Re-insert the preset via deserialization
|
|
189
|
+
const currentState = variantState.serialize();
|
|
190
|
+
const restored = restorePreset(typed.preset);
|
|
191
|
+
// We need to re-add; since addPreset generates a new ID, we use deserialize
|
|
192
|
+
currentState.presets.push({
|
|
193
|
+
id: restored.id,
|
|
194
|
+
name: restored.name,
|
|
195
|
+
groupOverrides: typed.preset.groupOverrides,
|
|
196
|
+
});
|
|
197
|
+
if (typed.wasActive) {
|
|
198
|
+
currentState.activePresetId = restored.id;
|
|
199
|
+
}
|
|
200
|
+
variantState.deserialize(currentState);
|
|
201
|
+
},
|
|
202
|
+
describe(params) {
|
|
203
|
+
return `Removed variant preset "${String(params["_removedName"] ?? params["id"])}"`;
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// --- set_color_override ---
|
|
208
|
+
api.addCommand('set_color_override', {
|
|
209
|
+
tier: 'project',
|
|
210
|
+
execute(params) {
|
|
211
|
+
const preset = variantState.getPreset(params["presetId"]);
|
|
212
|
+
if (!preset) return;
|
|
213
|
+
const groupMap = preset.groupOverrides.get(params["groupId"]);
|
|
214
|
+
const snapshot: ColorOverrideSnapshot = {
|
|
215
|
+
previousColor: groupMap?.get(params["originalColor"]) ?? null,
|
|
216
|
+
};
|
|
217
|
+
variantState.setColorOverride(
|
|
218
|
+
params["presetId"],
|
|
219
|
+
params["groupId"],
|
|
220
|
+
params["originalColor"],
|
|
221
|
+
params["newColor"],
|
|
222
|
+
);
|
|
223
|
+
return snapshot;
|
|
224
|
+
},
|
|
225
|
+
undo(params, _ctx, snapshot) {
|
|
226
|
+
const typed = snapshot as ColorOverrideSnapshot | undefined;
|
|
227
|
+
if (!typed) return;
|
|
228
|
+
if (typed.previousColor === null) {
|
|
229
|
+
// There was no override before; remove it
|
|
230
|
+
variantState.removeColorOverride(
|
|
231
|
+
params["presetId"],
|
|
232
|
+
params["groupId"],
|
|
233
|
+
params["originalColor"],
|
|
234
|
+
);
|
|
235
|
+
} else {
|
|
236
|
+
// Restore previous override
|
|
237
|
+
variantState.setColorOverride(
|
|
238
|
+
params["presetId"],
|
|
239
|
+
params["groupId"],
|
|
240
|
+
params["originalColor"],
|
|
241
|
+
typed.previousColor,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
describe(params) {
|
|
246
|
+
return `Set color override ${String(params["originalColor"])} -> ${String(params["newColor"])}`;
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// --- generate_variants ---
|
|
251
|
+
// Generate N new variant presets for the active layer group. Modes:
|
|
252
|
+
// 'random' -- each variant is an HSV-shifted randomization.
|
|
253
|
+
// 'interpolate' -- evenly-spaced interpolation between the first two
|
|
254
|
+
// existing presets. Requires >=2 presets already.
|
|
255
|
+
// Undoable: undo removes the created presets and restores the previous
|
|
256
|
+
// active preset (if generation set it).
|
|
257
|
+
api.addCommand('generate_variants', {
|
|
258
|
+
tier: 'project',
|
|
259
|
+
undoable: true,
|
|
260
|
+
execute(params) {
|
|
261
|
+
const count = Math.max(1, params["count"] ?? 1);
|
|
262
|
+
const mode = (params["mode"] ?? 'random') as
|
|
263
|
+
| 'random'
|
|
264
|
+
| 'interpolate';
|
|
265
|
+
|
|
266
|
+
const group = findTargetGroup();
|
|
267
|
+
if (!group) {
|
|
268
|
+
notify(
|
|
269
|
+
'No layer group found. Create a group to generate variants.',
|
|
270
|
+
'warning',
|
|
271
|
+
);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const snapshot: GenerateVariantsSnapshot = {
|
|
276
|
+
createdPresetIds: [],
|
|
277
|
+
previousActiveId: variantState.getActivePresetId(),
|
|
278
|
+
count: 0,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
if (mode === 'interpolate') {
|
|
282
|
+
const existing = variantState.getPresets();
|
|
283
|
+
if (existing.length < 2) {
|
|
284
|
+
notify(
|
|
285
|
+
'Interpolate mode needs at least two existing variants.',
|
|
286
|
+
'warning',
|
|
287
|
+
);
|
|
288
|
+
return snapshot;
|
|
289
|
+
}
|
|
290
|
+
// existing.length >= 2 is guarded above
|
|
291
|
+
const presetA = existing[0];
|
|
292
|
+
const presetB = existing[existing.length - 1];
|
|
293
|
+
if (!presetA || !presetB) return snapshot;
|
|
294
|
+
const series = generateInterpolatedSeries(presetA, presetB, count);
|
|
295
|
+
const baseIndex = existing.length;
|
|
296
|
+
for (let i = 0; i < series.length; i++) {
|
|
297
|
+
const item = series[i];
|
|
298
|
+
if (!item) continue;
|
|
299
|
+
const name = `Variant ${String(baseIndex + i + 1)}`;
|
|
300
|
+
const id = materializePreset(item, name);
|
|
301
|
+
snapshot.createdPresetIds.push(id);
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
// 'random' mode -- each variant gets its own randomization pass so
|
|
305
|
+
// they explore different hue rotations (generator samples fresh rng).
|
|
306
|
+
const palette = extractGroupPalette(group.id, getLayers(), getFrames());
|
|
307
|
+
if (palette.length === 0) {
|
|
308
|
+
notify(
|
|
309
|
+
`Group "${group.name}" has no colors to randomize.`,
|
|
310
|
+
'warning',
|
|
311
|
+
);
|
|
312
|
+
return snapshot;
|
|
313
|
+
}
|
|
314
|
+
const groupPalettes = new Map<string, string[]>([[group.id, palette]]);
|
|
315
|
+
const existingCount = variantState.getPresets().length;
|
|
316
|
+
for (let i = 0; i < count; i++) {
|
|
317
|
+
const detached = generateRandomVariant(groupPalettes);
|
|
318
|
+
const name = `Variant ${String(existingCount + i + 1)}`;
|
|
319
|
+
const id = materializePreset(detached, name);
|
|
320
|
+
snapshot.createdPresetIds.push(id);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Auto-activate the first created preset so the user sees the result.
|
|
325
|
+
const firstCreated = snapshot.createdPresetIds[0];
|
|
326
|
+
if (firstCreated !== undefined) {
|
|
327
|
+
variantState.setActivePreset(firstCreated);
|
|
328
|
+
}
|
|
329
|
+
snapshot.count = snapshot.createdPresetIds.length;
|
|
330
|
+
return snapshot;
|
|
331
|
+
},
|
|
332
|
+
undo(_params, _ctx, snapshot) {
|
|
333
|
+
const typed = snapshot as GenerateVariantsSnapshot | undefined;
|
|
334
|
+
if (!typed) return;
|
|
335
|
+
// Remove created presets in reverse order (cheaper splice patterns).
|
|
336
|
+
for (let i = typed.createdPresetIds.length - 1; i >= 0; i--) {
|
|
337
|
+
const id = typed.createdPresetIds[i];
|
|
338
|
+
if (id !== undefined) variantState.removePreset(id);
|
|
339
|
+
}
|
|
340
|
+
// Restore the previously active preset if it still exists. If the
|
|
341
|
+
// previous active id was one that we just removed (shouldn't happen
|
|
342
|
+
// since we only removed newly-created ones), setActivePreset would
|
|
343
|
+
// throw -- guard against that.
|
|
344
|
+
if (
|
|
345
|
+
typed.previousActiveId !== null &&
|
|
346
|
+
variantState.getPreset(typed.previousActiveId)
|
|
347
|
+
) {
|
|
348
|
+
variantState.setActivePreset(typed.previousActiveId);
|
|
349
|
+
} else {
|
|
350
|
+
variantState.setActivePreset(null);
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
describe(params) {
|
|
354
|
+
const count = params["count"] ?? 1;
|
|
355
|
+
const mode = params["mode"] ?? 'random';
|
|
356
|
+
return `Generated ${String(count)} ${mode} variant${count === 1 ? '' : 's'}`;
|
|
357
|
+
},
|
|
358
|
+
label: 'Generate Variants',
|
|
359
|
+
category: 'Variants',
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// --- apply_variant ---
|
|
363
|
+
// Set the active variant preset. Pass presetId: null to clear.
|
|
364
|
+
// Undoable: undo restores the previously active preset.
|
|
365
|
+
api.addCommand('apply_variant', {
|
|
366
|
+
tier: 'project',
|
|
367
|
+
undoable: true,
|
|
368
|
+
execute(params) {
|
|
369
|
+
const nextId = params["presetId"] ?? null;
|
|
370
|
+
const snapshot: ApplyVariantSnapshot = {
|
|
371
|
+
previousActiveId: variantState.getActivePresetId(),
|
|
372
|
+
};
|
|
373
|
+
// Guard: setActivePreset throws for unknown ids. Silently ignore so
|
|
374
|
+
// a stale dispatch (e.g. after a preset was removed) doesn't crash.
|
|
375
|
+
if (nextId !== null && !variantState.getPreset(nextId)) {
|
|
376
|
+
return snapshot;
|
|
377
|
+
}
|
|
378
|
+
variantState.setActivePreset(nextId);
|
|
379
|
+
return snapshot;
|
|
380
|
+
},
|
|
381
|
+
undo(_params, _ctx, snapshot) {
|
|
382
|
+
const typed = snapshot as ApplyVariantSnapshot | undefined;
|
|
383
|
+
if (!typed) return;
|
|
384
|
+
if (
|
|
385
|
+
typed.previousActiveId !== null &&
|
|
386
|
+
!variantState.getPreset(typed.previousActiveId)
|
|
387
|
+
) {
|
|
388
|
+
variantState.setActivePreset(null);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
variantState.setActivePreset(typed.previousActiveId);
|
|
392
|
+
},
|
|
393
|
+
describe(params) {
|
|
394
|
+
const id = params["presetId"];
|
|
395
|
+
if (typeof id !== 'string') return 'Cleared active variant';
|
|
396
|
+
const preset = variantState.getPreset(id);
|
|
397
|
+
return `Applied variant "${preset?.name ?? id}"`;
|
|
398
|
+
},
|
|
399
|
+
label: 'Apply Variant',
|
|
400
|
+
category: 'Variants',
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// --- extract_palette_from_layer ---
|
|
404
|
+
// Read-only: extracts the palette of the active group and reports via
|
|
405
|
+
// notification. Not undoable (no state mutation).
|
|
406
|
+
api.addCommand('extract_palette_from_layer', {
|
|
407
|
+
tier: 'project',
|
|
408
|
+
execute() {
|
|
409
|
+
const group = findTargetGroup();
|
|
410
|
+
if (!group) {
|
|
411
|
+
notify('No layer group found.', 'warning');
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const palette = extractGroupPalette(group.id, getLayers(), getFrames());
|
|
415
|
+
notify(
|
|
416
|
+
`Extracted ${String(palette.length)} color${palette.length === 1 ? '' : 's'} from "${group.name}".`,
|
|
417
|
+
'success',
|
|
418
|
+
);
|
|
419
|
+
},
|
|
420
|
+
undo() {
|
|
421
|
+
// No-op: this command has no persistent side effects.
|
|
422
|
+
},
|
|
423
|
+
describe() {
|
|
424
|
+
return 'Extracted palette from active group';
|
|
425
|
+
},
|
|
426
|
+
label: 'Extract Palette from Layer',
|
|
427
|
+
category: 'Variants',
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// --- export_bisection_atlas ---
|
|
431
|
+
// Opens a preview dialog for the variant atlas (rows = variants, cols = frames).
|
|
432
|
+
// The actual download is triggered from the dialog after user confirmation.
|
|
433
|
+
// Not undoable (download is a side effect outside app state).
|
|
434
|
+
api.addCommand('export_bisection_atlas', {
|
|
435
|
+
tier: 'project',
|
|
436
|
+
execute() {
|
|
437
|
+
const group = findTargetGroup();
|
|
438
|
+
if (!group) {
|
|
439
|
+
notify('No layer group found to export.', 'warning');
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const presets = variantState.getPresets();
|
|
443
|
+
if (presets.length === 0) {
|
|
444
|
+
notify('No variants to export. Generate at least one first.', 'warning');
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
dialogState.openBisectionExport(group.id);
|
|
449
|
+
},
|
|
450
|
+
undo() {
|
|
451
|
+
// No-op: downloads can't be "undone" -- the file is already on disk.
|
|
452
|
+
},
|
|
453
|
+
describe() {
|
|
454
|
+
return 'Exported bisection atlas';
|
|
455
|
+
},
|
|
456
|
+
label: 'Export Bisection Atlas',
|
|
457
|
+
category: 'Variants',
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// --- export_variants_batch ---
|
|
461
|
+
// Downloads each variant as a separate PNG file. Multi-frame variants are
|
|
462
|
+
// arranged as a horizontal strip (one row, N columns). A small delay
|
|
463
|
+
// between downloads prevents the browser from throttling rapid blob URLs.
|
|
464
|
+
api.addCommand('export_variants_batch', {
|
|
465
|
+
tier: 'project',
|
|
466
|
+
async execute() {
|
|
467
|
+
const group = findTargetGroup();
|
|
468
|
+
if (!group) {
|
|
469
|
+
notify('No layer group found to export.', 'warning');
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const presets = variantState.getPresets();
|
|
473
|
+
if (presets.length === 0) {
|
|
474
|
+
notify('No variants to export. Generate at least one first.', 'warning');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const layerTree = getLayers();
|
|
479
|
+
const frames = getFrames();
|
|
480
|
+
const w = canvasState.canvasWidth;
|
|
481
|
+
const h = canvasState.canvasHeight;
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const entries: Array<{ blob: Blob; filename: string }> = [];
|
|
485
|
+
|
|
486
|
+
for (const preset of presets) {
|
|
487
|
+
const frameBuffers = exportGroupSpritesheet(
|
|
488
|
+
group.id, layerTree, frames, w, h, { variant: preset },
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
// Single frame: export the buffer directly.
|
|
492
|
+
// Multiple frames: stitch into a horizontal strip.
|
|
493
|
+
let result: PixelBuffer;
|
|
494
|
+
if (frameBuffers.length === 1) {
|
|
495
|
+
const only = frameBuffers[0];
|
|
496
|
+
if (!only) continue;
|
|
497
|
+
result = only;
|
|
498
|
+
} else {
|
|
499
|
+
result = new PixelBuffer(w * frameBuffers.length, h);
|
|
500
|
+
for (let i = 0; i < frameBuffers.length; i++) {
|
|
501
|
+
const fb = frameBuffers[i];
|
|
502
|
+
if (fb) result.paste(fb, i * w, 0);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const canvas = document.createElement('canvas');
|
|
507
|
+
canvas.width = result.width;
|
|
508
|
+
canvas.height = result.height;
|
|
509
|
+
const ctx = canvas.getContext('2d');
|
|
510
|
+
if (!ctx) {
|
|
511
|
+
notify('Could not create export canvas.', 'warning');
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
ctx.imageSmoothingEnabled = false;
|
|
515
|
+
ctx.putImageData(result.toImageData(), 0, 0);
|
|
516
|
+
|
|
517
|
+
const blob = await new Promise<Blob | null>((resolve) => {
|
|
518
|
+
canvas.toBlob(resolve, 'image/png');
|
|
519
|
+
});
|
|
520
|
+
if (!blob) continue;
|
|
521
|
+
|
|
522
|
+
// Sanitize names for safe filenames
|
|
523
|
+
const safeName = preset.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
524
|
+
const safeGroup = group.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
525
|
+
entries.push({ blob, filename: `${safeGroup}-${safeName}.png` });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const saved = await downloadBatch(entries);
|
|
529
|
+
if (saved) {
|
|
530
|
+
notify(
|
|
531
|
+
`Exported ${String(entries.length)} variant${entries.length === 1 ? '' : 's'} as individual PNGs.`,
|
|
532
|
+
'success',
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
} catch (err) {
|
|
536
|
+
notify(
|
|
537
|
+
`Batch export failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
538
|
+
'warning',
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
undo() {
|
|
543
|
+
// No-op: downloads are side effects outside app state.
|
|
544
|
+
},
|
|
545
|
+
describe() {
|
|
546
|
+
return 'Exported variants as individual PNGs';
|
|
547
|
+
},
|
|
548
|
+
label: 'Export Variants Individually',
|
|
549
|
+
category: 'Variants',
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// --- remove_color_override ---
|
|
553
|
+
api.addCommand('remove_color_override', {
|
|
554
|
+
tier: 'project',
|
|
555
|
+
execute(params) {
|
|
556
|
+
const preset = variantState.getPreset(params["presetId"]);
|
|
557
|
+
if (!preset) return;
|
|
558
|
+
const groupMap = preset.groupOverrides.get(params["groupId"]);
|
|
559
|
+
const snapshot: ColorOverrideSnapshot = {
|
|
560
|
+
previousColor: groupMap?.get(params["originalColor"]) ?? null,
|
|
561
|
+
};
|
|
562
|
+
variantState.removeColorOverride(
|
|
563
|
+
params["presetId"],
|
|
564
|
+
params["groupId"],
|
|
565
|
+
params["originalColor"],
|
|
566
|
+
);
|
|
567
|
+
return snapshot;
|
|
568
|
+
},
|
|
569
|
+
undo(params, _ctx, snapshot) {
|
|
570
|
+
const typed = snapshot as ColorOverrideSnapshot | undefined;
|
|
571
|
+
if (typed?.previousColor != null) {
|
|
572
|
+
variantState.setColorOverride(
|
|
573
|
+
params["presetId"],
|
|
574
|
+
params["groupId"],
|
|
575
|
+
params["originalColor"],
|
|
576
|
+
typed.previousColor,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
describe(params) {
|
|
581
|
+
return `Removed color override for ${String(params["originalColor"])}`;
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// --- Menu items for variant exports ---
|
|
586
|
+
api.addMenuItem('menu:file:export:variants-batch', {
|
|
587
|
+
commandId: 'export_variants_batch',
|
|
588
|
+
menuPath: 'file/export',
|
|
589
|
+
group: 'export',
|
|
590
|
+
order: 20,
|
|
591
|
+
label: 'Export Variants Individually...',
|
|
592
|
+
});
|
|
593
|
+
},
|
|
594
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variant Panel Plugin -- registers the VariantPanel Svelte component as a
|
|
3
|
+
* dockable panel in the right sidebar. Discovery happens through the
|
|
4
|
+
* *-plugin.ts glob in bootstrap.ts; no manual wiring needed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PluginModule } from '../core/plugin-loader.js';
|
|
8
|
+
import VariantPanel from './VariantPanel.svelte';
|
|
9
|
+
|
|
10
|
+
export const variantPanelPlugin: PluginModule = {
|
|
11
|
+
name: 'ui/variants',
|
|
12
|
+
version: '1.0.0',
|
|
13
|
+
description: 'Variant preset grid panel (generate, preview, export)',
|
|
14
|
+
// Depends on the variant commands plugin so preset state mutations are
|
|
15
|
+
// guaranteed to be registered before the panel tries to use them.
|
|
16
|
+
dependencies: ['builtin/variants'],
|
|
17
|
+
|
|
18
|
+
register(api) {
|
|
19
|
+
api.addPanel('variants', {
|
|
20
|
+
title: 'Variants',
|
|
21
|
+
component: VariantPanel,
|
|
22
|
+
position: 'right',
|
|
23
|
+
minWidth: 180,
|
|
24
|
+
maxWidth: 320,
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
};
|