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,747 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ExportDialog -- modal for exporting the current project.
|
|
4
|
+
*
|
|
5
|
+
* Lists registered exporters from the plugin registry. Also provides
|
|
6
|
+
* a built-in PNG export (single frame composite) and spritesheet export
|
|
7
|
+
* using the spritesheet-export module. Downloads the result as a file.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { canvasState } from '../canvas/canvas-state.svelte.js';
|
|
11
|
+
import { PixelBuffer } from '../canvas/pixel-buffer.js';
|
|
12
|
+
import { composite } from '../layers/compositor.js';
|
|
13
|
+
import { exporterRegistry } from '../core/registries.svelte.js';
|
|
14
|
+
import type { ExporterDefinition } from '../core/plugin-types.js';
|
|
15
|
+
import * as layerTree from '../layers/layer-tree.svelte.js';
|
|
16
|
+
import * as frameModel from '../animation/frame-model.svelte.js';
|
|
17
|
+
import { exportSpritesheet } from '../animation/spritesheet-export.js';
|
|
18
|
+
import type { LayoutType, MetadataFormat } from '../animation/spritesheet-export.js';
|
|
19
|
+
import { embedPngMetadata } from '../export/png-metadata.js';
|
|
20
|
+
import { downloadFile, downloadCompanionFiles } from '../export/download.js';
|
|
21
|
+
|
|
22
|
+
// --- Props ---
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
open: boolean;
|
|
26
|
+
onClose: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let { open, onClose }: Props = $props();
|
|
30
|
+
|
|
31
|
+
// --- Local state ---
|
|
32
|
+
|
|
33
|
+
let dialogEl: HTMLDialogElement | undefined = $state();
|
|
34
|
+
|
|
35
|
+
let filename = $state('sprite');
|
|
36
|
+
let scale = $state(1);
|
|
37
|
+
let selectedExporter = $state('__builtin_png');
|
|
38
|
+
let exporting = $state(false);
|
|
39
|
+
let errorMessage = $state('');
|
|
40
|
+
|
|
41
|
+
// Spritesheet-specific options
|
|
42
|
+
let spritesheetLayout = $state<LayoutType>('horizontal');
|
|
43
|
+
let gridColumns = $state(4);
|
|
44
|
+
let spritesheetPadding = $state(0);
|
|
45
|
+
let metadataFormat = $state<MetadataFormat>('pixelweaver');
|
|
46
|
+
let includeMetadata = $state(false);
|
|
47
|
+
|
|
48
|
+
const scaleOptions = [1, 2, 4, 8];
|
|
49
|
+
const paddingOptions = [0, 1, 2, 4];
|
|
50
|
+
const metadataFormatOptions: { value: MetadataFormat; label: string }[] = [
|
|
51
|
+
{ value: 'pixelweaver', label: 'PixelWeaver' },
|
|
52
|
+
{ value: 'texturepacker', label: 'TexturePacker' },
|
|
53
|
+
{ value: 'aseprite', label: 'Aseprite' },
|
|
54
|
+
{ value: 'css', label: 'CSS' },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
let isSpritesheet = $derived(selectedExporter === '__builtin_spritesheet');
|
|
58
|
+
|
|
59
|
+
// --- Spritesheet preview ---
|
|
60
|
+
|
|
61
|
+
let previewCanvasEl: HTMLCanvasElement | undefined = $state();
|
|
62
|
+
let previewDimensions = $state('');
|
|
63
|
+
|
|
64
|
+
// Reactively regenerate the spritesheet preview whenever relevant options change
|
|
65
|
+
$effect(() => {
|
|
66
|
+
if (!isSpritesheet || !previewCanvasEl) {
|
|
67
|
+
previewDimensions = '';
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Read reactive dependencies (layout, padding, columns)
|
|
72
|
+
const layout = spritesheetLayout;
|
|
73
|
+
const padding = spritesheetPadding;
|
|
74
|
+
const columns = gridColumns;
|
|
75
|
+
|
|
76
|
+
const frames = frameModel.getFrames();
|
|
77
|
+
if (frames.length === 0) {
|
|
78
|
+
previewDimensions = '';
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const layers = layerTree.getLayers();
|
|
83
|
+
const options: import('../animation/spritesheet-export.js').ExportOptions = {
|
|
84
|
+
layout,
|
|
85
|
+
padding,
|
|
86
|
+
metadataFormat,
|
|
87
|
+
...(layout === 'grid' ? { columns } : {}),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const { image } = exportSpritesheet(frames, layers, options);
|
|
92
|
+
|
|
93
|
+
// Determine scale to fit within a max preview area (max 200px tall, max 380px wide)
|
|
94
|
+
const maxW = 380;
|
|
95
|
+
const maxH = 200;
|
|
96
|
+
const scaleX = maxW / image.width;
|
|
97
|
+
const scaleY = maxH / image.height;
|
|
98
|
+
const fitScale = Math.min(scaleX, scaleY, 1);
|
|
99
|
+
// For pixel art, round to integer multiples when possible for crisp rendering
|
|
100
|
+
const displayW = Math.max(1, Math.round(image.width * fitScale));
|
|
101
|
+
const displayH = Math.max(1, Math.round(image.height * fitScale));
|
|
102
|
+
|
|
103
|
+
const canvas = previewCanvasEl;
|
|
104
|
+
canvas.width = displayW;
|
|
105
|
+
canvas.height = displayH;
|
|
106
|
+
const ctx = canvas.getContext('2d');
|
|
107
|
+
if (!ctx) return;
|
|
108
|
+
ctx.imageSmoothingEnabled = false;
|
|
109
|
+
|
|
110
|
+
const imageData = image.toImageData();
|
|
111
|
+
const tmpCanvas = new OffscreenCanvas(image.width, image.height);
|
|
112
|
+
const tmpCtx = tmpCanvas.getContext('2d');
|
|
113
|
+
if (!tmpCtx) return;
|
|
114
|
+
tmpCtx.putImageData(imageData, 0, 0);
|
|
115
|
+
|
|
116
|
+
ctx.clearRect(0, 0, displayW, displayH);
|
|
117
|
+
ctx.drawImage(tmpCanvas, 0, 0, displayW, displayH);
|
|
118
|
+
|
|
119
|
+
previewDimensions = `${String(image.width)} x ${String(image.height)} px`;
|
|
120
|
+
} catch {
|
|
121
|
+
previewDimensions = '';
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// --- Built-in export options ---
|
|
126
|
+
// These are always available regardless of registered exporters.
|
|
127
|
+
|
|
128
|
+
interface ExportOption {
|
|
129
|
+
id: string;
|
|
130
|
+
label: string;
|
|
131
|
+
extension: string;
|
|
132
|
+
builtin: boolean;
|
|
133
|
+
exporter?: ExporterDefinition;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Derive available export options: built-ins + registered exporters
|
|
137
|
+
let exportOptions = $derived.by(() => {
|
|
138
|
+
const options: ExportOption[] = [
|
|
139
|
+
{ id: '__builtin_png', label: 'PNG Image (current frame)', extension: 'png', builtin: true },
|
|
140
|
+
{ id: '__builtin_spritesheet', label: 'Spritesheet (all frames)', extension: 'png', builtin: true },
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// Add registered exporters from plugins
|
|
144
|
+
for (const [name, def] of exporterRegistry.getAll()) {
|
|
145
|
+
options.push({
|
|
146
|
+
id: name,
|
|
147
|
+
label: `${def.label} (.${def.extension})`,
|
|
148
|
+
extension: def.extension,
|
|
149
|
+
builtin: false,
|
|
150
|
+
exporter: def,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return options;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
let selectedOption = $derived(
|
|
158
|
+
exportOptions.find((o) => o.id === selectedExporter) ?? exportOptions[0]
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// --- Utility: PixelBuffer -> scaled PNG Blob via OffscreenCanvas ---
|
|
162
|
+
|
|
163
|
+
function pixelBufferToBlob(buffer: PixelBuffer, scaleFactor: number): Promise<Blob> {
|
|
164
|
+
const w = buffer.width * scaleFactor;
|
|
165
|
+
const h = buffer.height * scaleFactor;
|
|
166
|
+
const canvas = new OffscreenCanvas(w, h);
|
|
167
|
+
const ctx = canvas.getContext('2d');
|
|
168
|
+
if (!ctx) return Promise.reject(new Error('Failed to get 2d context'));
|
|
169
|
+
|
|
170
|
+
// Disable smoothing for crisp pixel art scaling
|
|
171
|
+
ctx.imageSmoothingEnabled = false;
|
|
172
|
+
|
|
173
|
+
// Draw the original-size image data
|
|
174
|
+
const imageData = buffer.toImageData();
|
|
175
|
+
const tmpCanvas = new OffscreenCanvas(buffer.width, buffer.height);
|
|
176
|
+
const tmpCtx = tmpCanvas.getContext('2d');
|
|
177
|
+
if (!tmpCtx) return Promise.reject(new Error('Failed to get tmp 2d context'));
|
|
178
|
+
tmpCtx.putImageData(imageData, 0, 0);
|
|
179
|
+
|
|
180
|
+
// Scale up by drawing the small canvas onto the larger one
|
|
181
|
+
ctx.drawImage(tmpCanvas, 0, 0, w, h);
|
|
182
|
+
|
|
183
|
+
return canvas.convertToBlob({ type: 'image/png' });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- Export handlers ---
|
|
187
|
+
|
|
188
|
+
async function handleExport() {
|
|
189
|
+
if (exporting) return;
|
|
190
|
+
exporting = true;
|
|
191
|
+
errorMessage = '';
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const option = selectedOption;
|
|
195
|
+
let saved = false;
|
|
196
|
+
|
|
197
|
+
if (option.id === '__builtin_png') {
|
|
198
|
+
saved = await exportCurrentFramePng();
|
|
199
|
+
} else if (option.id === '__builtin_spritesheet') {
|
|
200
|
+
saved = await exportSpritesheetPng();
|
|
201
|
+
} else if (option.exporter) {
|
|
202
|
+
saved = await exportWithPlugin(option.exporter, option.extension);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Only close the dialog if the export actually completed (user may
|
|
206
|
+
// have cancelled the native save dialog on desktop).
|
|
207
|
+
if (saved) onClose();
|
|
208
|
+
} catch (err) {
|
|
209
|
+
errorMessage = err instanceof Error ? err.message : 'Export failed.';
|
|
210
|
+
} finally {
|
|
211
|
+
exporting = false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Export the current frame as a composited PNG. Returns false if cancelled. */
|
|
216
|
+
async function exportCurrentFramePng(): Promise<boolean> {
|
|
217
|
+
const frames = frameModel.getFrames();
|
|
218
|
+
if (frames.length === 0) throw new Error('No frames to export.');
|
|
219
|
+
|
|
220
|
+
const frame = frameModel.getCurrentFrame();
|
|
221
|
+
const layers = layerTree.getLayers();
|
|
222
|
+
const w = canvasState.canvasWidth;
|
|
223
|
+
const h = canvasState.canvasHeight;
|
|
224
|
+
|
|
225
|
+
const composited = composite(layers, frame.pixelData, w, h);
|
|
226
|
+
let blob = await pixelBufferToBlob(composited, scale);
|
|
227
|
+
blob = await embedPngMetadata(blob, {
|
|
228
|
+
Software: 'PixelWeaver',
|
|
229
|
+
Description: 'Single frame export',
|
|
230
|
+
Source: `${String(w)}x${String(h)}`,
|
|
231
|
+
});
|
|
232
|
+
return downloadFile({
|
|
233
|
+
blob,
|
|
234
|
+
filename: `${filename}.png`,
|
|
235
|
+
filterName: 'PNG Image',
|
|
236
|
+
filterExtensions: ['png'],
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Export all frames as a spritesheet PNG with configurable options. Returns false if cancelled. */
|
|
241
|
+
async function exportSpritesheetPng(): Promise<boolean> {
|
|
242
|
+
const frames = frameModel.getFrames();
|
|
243
|
+
if (frames.length === 0) throw new Error('No frames to export.');
|
|
244
|
+
|
|
245
|
+
const layers = layerTree.getLayers();
|
|
246
|
+
const options = {
|
|
247
|
+
layout: spritesheetLayout,
|
|
248
|
+
padding: spritesheetPadding,
|
|
249
|
+
metadataFormat: metadataFormat,
|
|
250
|
+
...(spritesheetLayout === 'grid' ? { columns: gridColumns } : {}),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const { image, metadata } = exportSpritesheet(frames, layers, options);
|
|
254
|
+
|
|
255
|
+
let blob = await pixelBufferToBlob(image, scale);
|
|
256
|
+
blob = await embedPngMetadata(blob, {
|
|
257
|
+
Software: 'PixelWeaver',
|
|
258
|
+
Description: metadata,
|
|
259
|
+
Source: `${String(canvasState.canvasWidth)}x${String(canvasState.canvasHeight)}`,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (includeMetadata) {
|
|
263
|
+
// CSS metadata is plain text, all others are JSON
|
|
264
|
+
const ext = metadataFormat === 'css' ? 'css' : 'json';
|
|
265
|
+
const mimeType = metadataFormat === 'css' ? 'text/css' : 'application/json';
|
|
266
|
+
const metaBlob = new Blob([metadata], { type: mimeType });
|
|
267
|
+
return downloadCompanionFiles([
|
|
268
|
+
{ blob, filename: `${filename}.png`, filterName: 'PNG Image', filterExtensions: ['png'] },
|
|
269
|
+
{ blob: metaBlob, filename: `${filename}.${ext}`, filterName: `${ext.toUpperCase()} File`, filterExtensions: [ext] },
|
|
270
|
+
]);
|
|
271
|
+
}
|
|
272
|
+
return downloadFile({
|
|
273
|
+
blob,
|
|
274
|
+
filename: `${filename}.png`,
|
|
275
|
+
filterName: 'PNG Image',
|
|
276
|
+
filterExtensions: ['png'],
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Export using a registered plugin exporter. Returns false if cancelled. */
|
|
281
|
+
async function exportWithPlugin(exporter: ExporterDefinition, extension: string): Promise<boolean> {
|
|
282
|
+
const frames = frameModel.getFrames();
|
|
283
|
+
const layers = layerTree.getLayers();
|
|
284
|
+
const w = canvasState.canvasWidth;
|
|
285
|
+
const h = canvasState.canvasHeight;
|
|
286
|
+
|
|
287
|
+
// Build the ExportContext with available data
|
|
288
|
+
const context = {
|
|
289
|
+
canvasWidth: w,
|
|
290
|
+
canvasHeight: h,
|
|
291
|
+
frames,
|
|
292
|
+
layers,
|
|
293
|
+
scale,
|
|
294
|
+
filename,
|
|
295
|
+
currentFrame: frameModel.getCurrentFrame(),
|
|
296
|
+
currentFrameIndex: frameModel.getCurrentFrameIndex(),
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const result = await exporter.export(context);
|
|
300
|
+
|
|
301
|
+
let blob: Blob;
|
|
302
|
+
if (result instanceof Blob) {
|
|
303
|
+
blob = result;
|
|
304
|
+
} else {
|
|
305
|
+
blob = new Blob([result], { type: exporter.mimeType });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return downloadFile({
|
|
309
|
+
blob,
|
|
310
|
+
filename: `${filename}.${extension}`,
|
|
311
|
+
filterName: `${extension.toUpperCase()} File`,
|
|
312
|
+
filterExtensions: [extension],
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Reset form values when dialog opens
|
|
317
|
+
$effect(() => {
|
|
318
|
+
if (open) {
|
|
319
|
+
filename = 'sprite';
|
|
320
|
+
scale = 1;
|
|
321
|
+
selectedExporter = '__builtin_png';
|
|
322
|
+
errorMessage = '';
|
|
323
|
+
exporting = false;
|
|
324
|
+
spritesheetLayout = 'horizontal';
|
|
325
|
+
gridColumns = 4;
|
|
326
|
+
spritesheetPadding = 0;
|
|
327
|
+
metadataFormat = 'pixelweaver';
|
|
328
|
+
includeMetadata = false;
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Sync native <dialog> open state with the `open` prop.
|
|
333
|
+
$effect(() => {
|
|
334
|
+
if (!dialogEl) return;
|
|
335
|
+
if (open && !dialogEl.open) {
|
|
336
|
+
dialogEl.showModal();
|
|
337
|
+
} else if (!open && dialogEl.open) {
|
|
338
|
+
dialogEl.close();
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Close on backdrop click: native <dialog> fires clicks on itself for the
|
|
343
|
+
// backdrop area (child content clicks have a different target).
|
|
344
|
+
function handleDialogClick(e: MouseEvent) {
|
|
345
|
+
if (e.target === dialogEl) {
|
|
346
|
+
onClose();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
</script>
|
|
350
|
+
|
|
351
|
+
<!--
|
|
352
|
+
Native <dialog> handles focus trap, Escape (cancel event), and backdrop.
|
|
353
|
+
onclose fires on both user Escape and programmatic close -- onClose is
|
|
354
|
+
idempotent, so the echo from our own close() call is harmless.
|
|
355
|
+
-->
|
|
356
|
+
<dialog
|
|
357
|
+
bind:this={dialogEl}
|
|
358
|
+
class="pw-dialog"
|
|
359
|
+
onclick={handleDialogClick}
|
|
360
|
+
onclose={onClose}
|
|
361
|
+
>
|
|
362
|
+
{#if open}
|
|
363
|
+
<div class="modal">
|
|
364
|
+
<h2 class="modal-title">Export</h2>
|
|
365
|
+
|
|
366
|
+
<!-- Format selector -->
|
|
367
|
+
<div class="form-row">
|
|
368
|
+
<label class="form-label" for="ex-format">Format</label>
|
|
369
|
+
<select
|
|
370
|
+
id="ex-format"
|
|
371
|
+
class="form-select"
|
|
372
|
+
bind:value={selectedExporter}
|
|
373
|
+
>
|
|
374
|
+
{#each exportOptions as option (option.id)}
|
|
375
|
+
<option value={option.id}>{option.label}</option>
|
|
376
|
+
{/each}
|
|
377
|
+
</select>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<!-- Filename -->
|
|
381
|
+
<div class="form-row">
|
|
382
|
+
<label class="form-label" for="ex-filename">Filename</label>
|
|
383
|
+
<div class="filename-group">
|
|
384
|
+
<input
|
|
385
|
+
id="ex-filename"
|
|
386
|
+
class="form-input"
|
|
387
|
+
type="text"
|
|
388
|
+
bind:value={filename}
|
|
389
|
+
/>
|
|
390
|
+
<span class="form-ext">.{selectedOption.extension}</span>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<!-- Scale -->
|
|
395
|
+
<div class="form-row">
|
|
396
|
+
<label class="form-label">Scale</label>
|
|
397
|
+
<div class="scale-group">
|
|
398
|
+
{#each scaleOptions as s (s)}
|
|
399
|
+
<button
|
|
400
|
+
class="scale-btn"
|
|
401
|
+
class:scale-btn--active={scale === s}
|
|
402
|
+
onclick={() => scale = s}
|
|
403
|
+
>{s}x</button>
|
|
404
|
+
{/each}
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<!-- Spritesheet options (only shown for spritesheet export) -->
|
|
409
|
+
{#if isSpritesheet}
|
|
410
|
+
<div class="form-row">
|
|
411
|
+
<label class="form-label" for="ex-layout">Layout</label>
|
|
412
|
+
<select
|
|
413
|
+
id="ex-layout"
|
|
414
|
+
class="form-select"
|
|
415
|
+
bind:value={spritesheetLayout}
|
|
416
|
+
>
|
|
417
|
+
<option value="horizontal">Horizontal</option>
|
|
418
|
+
<option value="grid">Grid</option>
|
|
419
|
+
<option value="atlas">Atlas</option>
|
|
420
|
+
</select>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
{#if spritesheetLayout === 'grid'}
|
|
424
|
+
<div class="form-row">
|
|
425
|
+
<label class="form-label" for="ex-columns">Columns</label>
|
|
426
|
+
<input
|
|
427
|
+
id="ex-columns"
|
|
428
|
+
class="form-input columns-input"
|
|
429
|
+
type="number"
|
|
430
|
+
min="1"
|
|
431
|
+
bind:value={gridColumns}
|
|
432
|
+
/>
|
|
433
|
+
</div>
|
|
434
|
+
{/if}
|
|
435
|
+
|
|
436
|
+
<div class="form-row">
|
|
437
|
+
<label class="form-label" for="ex-padding">Padding</label>
|
|
438
|
+
<select
|
|
439
|
+
id="ex-padding"
|
|
440
|
+
class="form-select"
|
|
441
|
+
bind:value={spritesheetPadding}
|
|
442
|
+
>
|
|
443
|
+
{#each paddingOptions as p (p)}
|
|
444
|
+
<option value={p}>{p}px</option>
|
|
445
|
+
{/each}
|
|
446
|
+
</select>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
<div class="form-row">
|
|
450
|
+
<label class="form-label" for="ex-meta-format">Metadata</label>
|
|
451
|
+
<select
|
|
452
|
+
id="ex-meta-format"
|
|
453
|
+
class="form-select"
|
|
454
|
+
bind:value={metadataFormat}
|
|
455
|
+
>
|
|
456
|
+
{#each metadataFormatOptions as fmt (fmt.value)}
|
|
457
|
+
<option value={fmt.value}>{fmt.label}</option>
|
|
458
|
+
{/each}
|
|
459
|
+
</select>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<div class="form-row">
|
|
463
|
+
<label class="form-label"> </label>
|
|
464
|
+
<label class="checkbox-label">
|
|
465
|
+
<input
|
|
466
|
+
type="checkbox"
|
|
467
|
+
class="form-checkbox"
|
|
468
|
+
bind:checked={includeMetadata}
|
|
469
|
+
/>
|
|
470
|
+
Include metadata file
|
|
471
|
+
</label>
|
|
472
|
+
</div>
|
|
473
|
+
{/if}
|
|
474
|
+
|
|
475
|
+
<!-- Spritesheet preview (shown when spritesheet format is selected) -->
|
|
476
|
+
{#if isSpritesheet}
|
|
477
|
+
<div class="preview-section">
|
|
478
|
+
<canvas
|
|
479
|
+
bind:this={previewCanvasEl}
|
|
480
|
+
class="preview-canvas"
|
|
481
|
+
></canvas>
|
|
482
|
+
{#if previewDimensions}
|
|
483
|
+
<span class="preview-dimensions">{previewDimensions}</span>
|
|
484
|
+
{/if}
|
|
485
|
+
</div>
|
|
486
|
+
{/if}
|
|
487
|
+
|
|
488
|
+
<!-- Error message -->
|
|
489
|
+
{#if errorMessage}
|
|
490
|
+
<div class="error-msg">{errorMessage}</div>
|
|
491
|
+
{/if}
|
|
492
|
+
|
|
493
|
+
<!-- Actions -->
|
|
494
|
+
<div class="modal-actions">
|
|
495
|
+
<button class="btn btn--cancel" onclick={onClose}>Cancel</button>
|
|
496
|
+
<button
|
|
497
|
+
class="btn btn--primary"
|
|
498
|
+
onclick={handleExport}
|
|
499
|
+
disabled={exporting || !filename.trim()}
|
|
500
|
+
>
|
|
501
|
+
{exporting ? 'Exporting...' : 'Export'}
|
|
502
|
+
</button>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
{/if}
|
|
506
|
+
</dialog>
|
|
507
|
+
|
|
508
|
+
<style>
|
|
509
|
+
/* Reset native <dialog> UA styles so it fills the viewport and lets us
|
|
510
|
+
center the inner .modal via flexbox. The backdrop is styled separately. */
|
|
511
|
+
.pw-dialog {
|
|
512
|
+
margin: 0;
|
|
513
|
+
padding: 0;
|
|
514
|
+
border: none;
|
|
515
|
+
background: transparent;
|
|
516
|
+
max-width: none;
|
|
517
|
+
max-height: none;
|
|
518
|
+
width: 100vw;
|
|
519
|
+
height: 100vh;
|
|
520
|
+
color: inherit;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.pw-dialog[open] {
|
|
524
|
+
display: flex;
|
|
525
|
+
align-items: center;
|
|
526
|
+
justify-content: center;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.pw-dialog::backdrop {
|
|
530
|
+
background: rgba(0, 0, 0, 0.5);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.modal {
|
|
534
|
+
background: var(--bg-panel);
|
|
535
|
+
border: 1px solid var(--border);
|
|
536
|
+
border-radius: 6px;
|
|
537
|
+
box-shadow: var(--shadow-md);
|
|
538
|
+
width: 440px;
|
|
539
|
+
max-width: 90vw;
|
|
540
|
+
padding: 24px;
|
|
541
|
+
color: var(--text-primary);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.modal-title {
|
|
545
|
+
font-size: 18px;
|
|
546
|
+
font-weight: 600;
|
|
547
|
+
margin-bottom: 20px;
|
|
548
|
+
color: var(--text-primary);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/* Form layout */
|
|
552
|
+
.form-row {
|
|
553
|
+
display: flex;
|
|
554
|
+
align-items: center;
|
|
555
|
+
gap: 8px;
|
|
556
|
+
margin-bottom: 14px;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.form-label {
|
|
560
|
+
width: 80px;
|
|
561
|
+
flex-shrink: 0;
|
|
562
|
+
font-size: var(--text-lg);
|
|
563
|
+
color: var(--text-secondary);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.form-input {
|
|
567
|
+
flex: 1;
|
|
568
|
+
background: var(--bg-toolbar);
|
|
569
|
+
border: 1px solid var(--border);
|
|
570
|
+
border-radius: var(--radius-md);
|
|
571
|
+
color: var(--text-primary);
|
|
572
|
+
font-size: var(--text-lg);
|
|
573
|
+
padding: 6px 8px;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.form-input:focus-visible {
|
|
577
|
+
border-color: var(--accent);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.form-select {
|
|
581
|
+
flex: 1;
|
|
582
|
+
background: var(--bg-toolbar);
|
|
583
|
+
border: 1px solid var(--border);
|
|
584
|
+
border-radius: var(--radius-md);
|
|
585
|
+
color: var(--text-primary);
|
|
586
|
+
font-size: var(--text-lg);
|
|
587
|
+
padding: 6px 8px;
|
|
588
|
+
cursor: pointer;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.form-select:focus-visible {
|
|
592
|
+
border-color: var(--accent);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/* Filename group: input + extension label */
|
|
596
|
+
.filename-group {
|
|
597
|
+
flex: 1;
|
|
598
|
+
display: flex;
|
|
599
|
+
align-items: center;
|
|
600
|
+
gap: 4px;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.form-ext {
|
|
604
|
+
font-size: var(--text-base);
|
|
605
|
+
color: var(--text-muted);
|
|
606
|
+
flex-shrink: 0;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/* Scale buttons */
|
|
610
|
+
.scale-group {
|
|
611
|
+
display: flex;
|
|
612
|
+
gap: 6px;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.scale-btn {
|
|
616
|
+
background: var(--bg-toolbar);
|
|
617
|
+
border: 1px solid var(--border);
|
|
618
|
+
border-radius: var(--radius-md);
|
|
619
|
+
color: var(--text-secondary);
|
|
620
|
+
font-size: var(--text-base);
|
|
621
|
+
padding: 4px 12px;
|
|
622
|
+
cursor: pointer;
|
|
623
|
+
transition: background var(--transition-fast), border-color var(--transition-fast);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.scale-btn:hover {
|
|
627
|
+
background: var(--bg-primary);
|
|
628
|
+
color: var(--text-primary);
|
|
629
|
+
border-color: var(--accent);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.scale-btn--active {
|
|
633
|
+
background: var(--accent);
|
|
634
|
+
color: #ffffff;
|
|
635
|
+
border-color: var(--accent);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.scale-btn--active:hover {
|
|
639
|
+
background: var(--accent-hover);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/* Columns input -- narrower than full-width */
|
|
643
|
+
.columns-input {
|
|
644
|
+
width: 80px;
|
|
645
|
+
flex: none;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/* Checkbox row */
|
|
649
|
+
.checkbox-label {
|
|
650
|
+
display: flex;
|
|
651
|
+
align-items: center;
|
|
652
|
+
gap: 8px;
|
|
653
|
+
font-size: var(--text-lg);
|
|
654
|
+
color: var(--text-secondary);
|
|
655
|
+
cursor: pointer;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.form-checkbox {
|
|
659
|
+
width: 16px;
|
|
660
|
+
height: 16px;
|
|
661
|
+
accent-color: var(--accent);
|
|
662
|
+
cursor: pointer;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/* Spritesheet preview */
|
|
666
|
+
.preview-section {
|
|
667
|
+
display: flex;
|
|
668
|
+
flex-direction: column;
|
|
669
|
+
align-items: center;
|
|
670
|
+
gap: 6px;
|
|
671
|
+
margin-bottom: 14px;
|
|
672
|
+
padding: 12px;
|
|
673
|
+
background: var(--bg-toolbar);
|
|
674
|
+
border: 1px solid var(--border);
|
|
675
|
+
border-radius: var(--radius-md);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.preview-canvas {
|
|
679
|
+
/* Checkerboard background to show transparency */
|
|
680
|
+
background-image:
|
|
681
|
+
linear-gradient(45deg, var(--bg-primary) 25%, transparent 25%),
|
|
682
|
+
linear-gradient(-45deg, var(--bg-primary) 25%, transparent 25%),
|
|
683
|
+
linear-gradient(45deg, transparent 75%, var(--bg-primary) 75%),
|
|
684
|
+
linear-gradient(-45deg, transparent 75%, var(--bg-primary) 75%);
|
|
685
|
+
background-size: 8px 8px;
|
|
686
|
+
background-position: 0 0, 0 4px, 4px -4px, -4px 0px;
|
|
687
|
+
image-rendering: pixelated;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.preview-dimensions {
|
|
691
|
+
font-size: var(--text-base);
|
|
692
|
+
color: var(--text-muted);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/* Error message */
|
|
696
|
+
.error-msg {
|
|
697
|
+
background: rgba(255, 74, 106, 0.1);
|
|
698
|
+
border: 1px solid var(--error);
|
|
699
|
+
border-radius: var(--radius-md);
|
|
700
|
+
color: var(--error);
|
|
701
|
+
font-size: var(--text-base);
|
|
702
|
+
padding: 8px 10px;
|
|
703
|
+
margin-bottom: 12px;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/* Action buttons */
|
|
707
|
+
.modal-actions {
|
|
708
|
+
display: flex;
|
|
709
|
+
justify-content: flex-end;
|
|
710
|
+
gap: 8px;
|
|
711
|
+
margin-top: 20px;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.btn {
|
|
715
|
+
border: none;
|
|
716
|
+
border-radius: var(--radius-md);
|
|
717
|
+
font-size: var(--text-lg);
|
|
718
|
+
padding: 8px 20px;
|
|
719
|
+
cursor: pointer;
|
|
720
|
+
font-weight: 500;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.btn:disabled {
|
|
724
|
+
opacity: 0.5;
|
|
725
|
+
cursor: default;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.btn--cancel {
|
|
729
|
+
background: var(--bg-toolbar);
|
|
730
|
+
color: var(--text-secondary);
|
|
731
|
+
border: 1px solid var(--border);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.btn--cancel:hover {
|
|
735
|
+
background: var(--bg-primary);
|
|
736
|
+
color: var(--text-primary);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.btn--primary {
|
|
740
|
+
background: var(--accent);
|
|
741
|
+
color: #ffffff;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.btn--primary:hover:not(:disabled) {
|
|
745
|
+
background: var(--accent-hover);
|
|
746
|
+
}
|
|
747
|
+
</style>
|