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,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-adaptive file download helper.
|
|
3
|
+
*
|
|
4
|
+
* On desktop (pywebview): opens a native save dialog, then writes the file
|
|
5
|
+
* via the pywebview bridge. For batch exports, opens a directory picker
|
|
6
|
+
* and writes all files into the chosen folder.
|
|
7
|
+
*
|
|
8
|
+
* On browser: falls back to the classic <a download> trick with
|
|
9
|
+
* URL.createObjectURL.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Base64 helper
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
function uint8ArrayToBase64(bytes: Uint8Array): string {
|
|
17
|
+
let binary = '';
|
|
18
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
19
|
+
binary += String.fromCharCode(bytes[i]!);
|
|
20
|
+
}
|
|
21
|
+
return btoa(binary);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Desktop detection
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/** Check if running inside a pywebview desktop window. */
|
|
29
|
+
export function isDesktop(): boolean {
|
|
30
|
+
return !!window.pywebview;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Single-file download
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
export interface DownloadFileOptions {
|
|
38
|
+
/** The blob to save. */
|
|
39
|
+
blob: Blob;
|
|
40
|
+
/** Suggested filename including extension (e.g. "sprite.png"). */
|
|
41
|
+
filename: string;
|
|
42
|
+
/** Filter label for the native dialog (e.g. "PNG Image"). */
|
|
43
|
+
filterName?: string;
|
|
44
|
+
/** Allowed extensions without dot (e.g. ["png"]). */
|
|
45
|
+
filterExtensions?: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Download / save a single file. On desktop a native save dialog is shown;
|
|
50
|
+
* on browser the file is downloaded directly.
|
|
51
|
+
*
|
|
52
|
+
* Returns `true` if the file was saved, `false` if the user cancelled
|
|
53
|
+
* the native dialog (browser always returns `true`).
|
|
54
|
+
*/
|
|
55
|
+
export async function downloadFile(opts: DownloadFileOptions): Promise<boolean> {
|
|
56
|
+
if (isDesktop()) {
|
|
57
|
+
return downloadFileDesktop(opts);
|
|
58
|
+
}
|
|
59
|
+
downloadFileBrowser(opts.blob, opts.filename);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function downloadFileDesktop(opts: DownloadFileOptions): Promise<boolean> {
|
|
64
|
+
const ext = opts.filterExtensions
|
|
65
|
+
?? [opts.filename.split('.').pop() ?? 'bin'];
|
|
66
|
+
const filterName = opts.filterName
|
|
67
|
+
?? ext.map((e) => e.toUpperCase()).join('/') + ' File';
|
|
68
|
+
|
|
69
|
+
const buffer = new Uint8Array(await opts.blob.arrayBuffer());
|
|
70
|
+
const b64 = uint8ArrayToBase64(buffer);
|
|
71
|
+
|
|
72
|
+
const path = await window.pywebview!.api.save_file(
|
|
73
|
+
b64, opts.filename, filterName, ext,
|
|
74
|
+
);
|
|
75
|
+
return path !== null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function downloadFileBrowser(blob: Blob, filename: string): void {
|
|
79
|
+
const url = URL.createObjectURL(blob);
|
|
80
|
+
const a = document.createElement('a');
|
|
81
|
+
a.href = url;
|
|
82
|
+
a.download = filename;
|
|
83
|
+
document.body.appendChild(a);
|
|
84
|
+
a.click();
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
document.body.removeChild(a);
|
|
87
|
+
URL.revokeObjectURL(url);
|
|
88
|
+
}, 100);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Multi-file download (batch)
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
export interface BatchFileEntry {
|
|
96
|
+
blob: Blob;
|
|
97
|
+
filename: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Download multiple files. On desktop a directory picker is shown and all
|
|
102
|
+
* files are written into the chosen folder. On browser the files are
|
|
103
|
+
* downloaded sequentially with a small delay between each to avoid
|
|
104
|
+
* throttling.
|
|
105
|
+
*
|
|
106
|
+
* Returns `true` if files were saved, `false` if the user cancelled
|
|
107
|
+
* the directory picker (browser always returns `true`).
|
|
108
|
+
*/
|
|
109
|
+
export async function downloadBatch(entries: BatchFileEntry[]): Promise<boolean> {
|
|
110
|
+
if (entries.length === 0) return true;
|
|
111
|
+
if (isDesktop()) {
|
|
112
|
+
return downloadBatchDesktop(entries);
|
|
113
|
+
}
|
|
114
|
+
downloadBatchBrowser(entries, 0);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function downloadBatchDesktop(entries: BatchFileEntry[]): Promise<boolean> {
|
|
119
|
+
const dir = await window.pywebview!.api.pick_directory();
|
|
120
|
+
if (!dir) return false;
|
|
121
|
+
|
|
122
|
+
const files = await Promise.all(
|
|
123
|
+
entries.map(async (entry) => ({
|
|
124
|
+
name: entry.filename,
|
|
125
|
+
data: uint8ArrayToBase64(new Uint8Array(await entry.blob.arrayBuffer())),
|
|
126
|
+
})),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return window.pywebview!.api.write_files_to_directory(dir, files);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Trigger browser downloads one at a time with a small delay between
|
|
134
|
+
* each to prevent the browser from blocking rapid successive downloads.
|
|
135
|
+
*/
|
|
136
|
+
function downloadBatchBrowser(entries: BatchFileEntry[], index: number): void {
|
|
137
|
+
if (index >= entries.length) return;
|
|
138
|
+
const entry = entries[index];
|
|
139
|
+
if (!entry) return;
|
|
140
|
+
downloadFileBrowser(entry.blob, entry.filename);
|
|
141
|
+
// 50ms gap so the browser does not throttle/block consecutive downloads.
|
|
142
|
+
setTimeout(() => { downloadBatchBrowser(entries, index + 1); }, 50);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Multi-file companion download (e.g. PNG + JSON from same export)
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Download multiple companion files from a single export operation.
|
|
151
|
+
* On desktop the first file gets a save dialog; subsequent files are written
|
|
152
|
+
* to the same directory as the first. On browser each file is downloaded.
|
|
153
|
+
*
|
|
154
|
+
* Returns `true` if files were saved, `false` if the user cancelled.
|
|
155
|
+
*/
|
|
156
|
+
export async function downloadCompanionFiles(
|
|
157
|
+
entries: DownloadFileOptions[],
|
|
158
|
+
): Promise<boolean> {
|
|
159
|
+
if (entries.length === 0) return true;
|
|
160
|
+
if (isDesktop()) {
|
|
161
|
+
return downloadCompanionFilesDesktop(entries);
|
|
162
|
+
}
|
|
163
|
+
// Browser: download each file sequentially
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
downloadFileBrowser(entry.blob, entry.filename);
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function downloadCompanionFilesDesktop(
|
|
171
|
+
entries: DownloadFileOptions[],
|
|
172
|
+
): Promise<boolean> {
|
|
173
|
+
// Show save dialog for the first (primary) file
|
|
174
|
+
const first = entries[0]!;
|
|
175
|
+
const ext = first.filterExtensions
|
|
176
|
+
?? [first.filename.split('.').pop() ?? 'bin'];
|
|
177
|
+
const filterName = first.filterName
|
|
178
|
+
?? ext.map((e) => e.toUpperCase()).join('/') + ' File';
|
|
179
|
+
|
|
180
|
+
const buffer = new Uint8Array(await first.blob.arrayBuffer());
|
|
181
|
+
const b64 = uint8ArrayToBase64(buffer);
|
|
182
|
+
|
|
183
|
+
const path = await window.pywebview!.api.save_file(
|
|
184
|
+
b64, first.filename, filterName, ext,
|
|
185
|
+
);
|
|
186
|
+
if (!path) return false;
|
|
187
|
+
|
|
188
|
+
// Write companion files to the same directory
|
|
189
|
+
if (entries.length > 1) {
|
|
190
|
+
const dir = path.substring(0, path.lastIndexOf('/'));
|
|
191
|
+
const companionFiles = await Promise.all(
|
|
192
|
+
entries.slice(1).map(async (entry) => ({
|
|
193
|
+
name: entry.filename,
|
|
194
|
+
data: uint8ArrayToBase64(new Uint8Array(await entry.blob.arrayBuffer())),
|
|
195
|
+
})),
|
|
196
|
+
);
|
|
197
|
+
await window.pywebview!.api.write_files_to_directory(dir, companionFiles);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PNG metadata injection -- embeds tEXt chunks into PNG files.
|
|
3
|
+
*
|
|
4
|
+
* PNG files have a well-defined chunk structure. After the 8-byte signature,
|
|
5
|
+
* the file is a sequence of chunks: [4-byte length][4-byte type][data][4-byte CRC].
|
|
6
|
+
* tEXt chunks store key-value pairs separated by a null byte.
|
|
7
|
+
*
|
|
8
|
+
* This module inserts tEXt chunks after IHDR and before the first IDAT chunk,
|
|
9
|
+
* which is the standard location for ancillary metadata chunks.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// --- CRC32 ---
|
|
13
|
+
|
|
14
|
+
/** Pre-computed CRC32 lookup table (IEEE polynomial 0xEDB88320). */
|
|
15
|
+
const crcTable = buildCrcTable();
|
|
16
|
+
|
|
17
|
+
function buildCrcTable(): Uint32Array {
|
|
18
|
+
const table = new Uint32Array(256);
|
|
19
|
+
for (let n = 0; n < 256; n++) {
|
|
20
|
+
let c = n;
|
|
21
|
+
for (let k = 0; k < 8; k++) {
|
|
22
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
23
|
+
}
|
|
24
|
+
table[n] = c;
|
|
25
|
+
}
|
|
26
|
+
return table;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Compute CRC32 over one or more byte arrays. */
|
|
30
|
+
function crc32(...buffers: Uint8Array[]): number {
|
|
31
|
+
let crc = 0xffffffff;
|
|
32
|
+
// Uint8Array indexing is `number | undefined` under noUncheckedIndexedAccess;
|
|
33
|
+
// ?? 0 fallbacks never trigger because i < buf.length and the table is dense.
|
|
34
|
+
for (const buf of buffers) {
|
|
35
|
+
for (let i = 0; i < buf.length; i++) {
|
|
36
|
+
crc = (crcTable[(crc ^ (buf[i] ?? 0)) & 0xff] ?? 0) ^ (crc >>> 8);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- Text encoding helpers ---
|
|
43
|
+
|
|
44
|
+
const textEncoder = new TextEncoder();
|
|
45
|
+
|
|
46
|
+
/** Encode a string as Latin-1 bytes (values > 255 are clamped). */
|
|
47
|
+
function toLatin1(str: string): Uint8Array {
|
|
48
|
+
const bytes = new Uint8Array(str.length);
|
|
49
|
+
for (let i = 0; i < str.length; i++) {
|
|
50
|
+
bytes[i] = str.charCodeAt(i) & 0xff;
|
|
51
|
+
}
|
|
52
|
+
return bytes;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- tEXt chunk construction ---
|
|
56
|
+
|
|
57
|
+
/** PNG chunk type identifier for tEXt. */
|
|
58
|
+
const TEXT_TYPE = textEncoder.encode('tEXt');
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build a complete PNG tEXt chunk (length + type + data + CRC).
|
|
62
|
+
*
|
|
63
|
+
* tEXt data format: keyword (1-79 Latin-1 chars) + null byte + value (Latin-1).
|
|
64
|
+
* The CRC covers the type field and data, but not the length field.
|
|
65
|
+
*/
|
|
66
|
+
function buildTextChunk(key: string, value: string): Uint8Array {
|
|
67
|
+
// Clamp key to 79 characters per PNG spec
|
|
68
|
+
const clampedKey = key.slice(0, 79);
|
|
69
|
+
const keyBytes = toLatin1(clampedKey);
|
|
70
|
+
const valueBytes = toLatin1(value);
|
|
71
|
+
|
|
72
|
+
// data = key + \0 + value
|
|
73
|
+
const dataLength = keyBytes.length + 1 + valueBytes.length;
|
|
74
|
+
// chunk = 4 (length) + 4 (type) + dataLength + 4 (CRC)
|
|
75
|
+
const chunk = new Uint8Array(4 + 4 + dataLength + 4);
|
|
76
|
+
const view = new DataView(chunk.buffer);
|
|
77
|
+
|
|
78
|
+
// Write length (big-endian)
|
|
79
|
+
view.setUint32(0, dataLength);
|
|
80
|
+
|
|
81
|
+
// Write chunk type "tEXt"
|
|
82
|
+
chunk.set(TEXT_TYPE, 4);
|
|
83
|
+
|
|
84
|
+
// Write key + null separator + value
|
|
85
|
+
chunk.set(keyBytes, 8);
|
|
86
|
+
chunk[8 + keyBytes.length] = 0;
|
|
87
|
+
chunk.set(valueBytes, 8 + keyBytes.length + 1);
|
|
88
|
+
|
|
89
|
+
// Compute CRC over type + data
|
|
90
|
+
const crcData = chunk.slice(4, 8 + dataLength);
|
|
91
|
+
const crcValue = crc32(crcData);
|
|
92
|
+
view.setUint32(8 + dataLength, crcValue);
|
|
93
|
+
|
|
94
|
+
return chunk;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- PNG parsing helpers ---
|
|
98
|
+
|
|
99
|
+
/** PNG magic signature (8 bytes). */
|
|
100
|
+
const PNG_SIGNATURE_LENGTH = 8;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Read a 4-byte ASCII chunk type from a DataView at the given offset.
|
|
104
|
+
*/
|
|
105
|
+
function readChunkType(view: DataView, offset: number): string {
|
|
106
|
+
return String.fromCharCode(
|
|
107
|
+
view.getUint8(offset),
|
|
108
|
+
view.getUint8(offset + 1),
|
|
109
|
+
view.getUint8(offset + 2),
|
|
110
|
+
view.getUint8(offset + 3),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Find the byte offset of the first IDAT chunk in a PNG buffer.
|
|
116
|
+
*
|
|
117
|
+
* Walks the chunk list starting after the 8-byte signature.
|
|
118
|
+
* Returns the offset of the IDAT chunk's length field, which is where
|
|
119
|
+
* we want to splice in tEXt chunks.
|
|
120
|
+
*/
|
|
121
|
+
function findFirstIdatOffset(view: DataView): number {
|
|
122
|
+
let offset = PNG_SIGNATURE_LENGTH;
|
|
123
|
+
|
|
124
|
+
while (offset < view.byteLength) {
|
|
125
|
+
const chunkDataLength = view.getUint32(offset);
|
|
126
|
+
const chunkType = readChunkType(view, offset + 4);
|
|
127
|
+
|
|
128
|
+
if (chunkType === 'IDAT') {
|
|
129
|
+
return offset;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Advance past this chunk: 4 (length) + 4 (type) + data + 4 (CRC)
|
|
133
|
+
offset += 4 + 4 + chunkDataLength + 4;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw new Error('PNG has no IDAT chunk -- file is malformed.');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- Public API ---
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Embed tEXt metadata key-value pairs into a PNG blob.
|
|
143
|
+
*
|
|
144
|
+
* Inserts tEXt chunks after IHDR and before the first IDAT chunk.
|
|
145
|
+
* Returns a new Blob; the original is not mutated.
|
|
146
|
+
*/
|
|
147
|
+
export async function embedPngMetadata(
|
|
148
|
+
pngBlob: Blob,
|
|
149
|
+
metadata: Record<string, string>,
|
|
150
|
+
): Promise<Blob> {
|
|
151
|
+
const buffer = await pngBlob.arrayBuffer();
|
|
152
|
+
const view = new DataView(buffer);
|
|
153
|
+
|
|
154
|
+
const insertOffset = findFirstIdatOffset(view);
|
|
155
|
+
|
|
156
|
+
// Build all tEXt chunks
|
|
157
|
+
const chunks: Uint8Array[] = [];
|
|
158
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
159
|
+
chunks.push(buildTextChunk(key, value));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Total bytes to insert
|
|
163
|
+
const insertLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
164
|
+
|
|
165
|
+
// Assemble: [before IDAT] + [tEXt chunks] + [IDAT onward]
|
|
166
|
+
const before = new Uint8Array(buffer, 0, insertOffset);
|
|
167
|
+
const after = new Uint8Array(buffer, insertOffset);
|
|
168
|
+
|
|
169
|
+
const result = new Uint8Array(before.length + insertLength + after.length);
|
|
170
|
+
result.set(before, 0);
|
|
171
|
+
|
|
172
|
+
let writeOffset = before.length;
|
|
173
|
+
for (const chunk of chunks) {
|
|
174
|
+
result.set(chunk, writeOffset);
|
|
175
|
+
writeOffset += chunk.length;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
result.set(after, writeOffset);
|
|
179
|
+
|
|
180
|
+
return new Blob([result], { type: 'image/png' });
|
|
181
|
+
}
|