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,1235 @@
|
|
|
1
|
+
# PixelWeaver Audit -- Implementation Plan
|
|
2
|
+
|
|
3
|
+
This document transforms the 10 design decisions from `audit-design-decisions.md` into
|
|
4
|
+
a concrete, step-by-step execution plan. Each item includes exact file paths, function
|
|
5
|
+
names, interface modifications, and line-level references to the current codebase.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [I-01: makeStrokeTool Factory (old #24)](#i-01-makestroketool-factory)
|
|
12
|
+
- [I-02: makeSnapshotUndo Factory (old #25)](#i-02-makesnapshotundo-factory)
|
|
13
|
+
- [I-03: Generic Command\<P\>, Typed CommandContext, Separate Snapshot (old #20+#23)](#i-03-generic-commandp-typed-commandcontext-separate-snapshot)
|
|
14
|
+
- [I-04: Undoable Flag on CommandDefinition (old #1)](#i-04-undoable-flag-on-commanddefinition)
|
|
15
|
+
- [I-05: Native \<dialog\> for Modals (old #22)](#i-05-native-dialog-for-modals)
|
|
16
|
+
- [I-06: Reusable PromptDialog Component (old #26)](#i-06-reusable-promptdialog-component)
|
|
17
|
+
- [I-07: God File Split (old #14)](#i-07-god-file-split)
|
|
18
|
+
- [I-08: Add getSelection() to PluginAPI (old #12)](#i-08-add-getselection-to-pluginapi)
|
|
19
|
+
- [I-09: Add Mutator Methods to PluginAPI (old #13)](#i-09-add-mutator-methods-to-pluginapi)
|
|
20
|
+
- [I-10: Enable Strict TypeScript Flags (old #19)](#i-10-enable-strict-typescript-flags)
|
|
21
|
+
- [Internal Consistency Check](#internal-consistency-check)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Re-indexed Items in Implementation Order
|
|
26
|
+
|
|
27
|
+
| New Index | Old # | Title |
|
|
28
|
+
|-----------|-------|-------|
|
|
29
|
+
| I-01 | #24 | makeStrokeTool factory |
|
|
30
|
+
| I-02 | #25 | makeSnapshotUndo factory |
|
|
31
|
+
| I-03 | #20+#23 | Generic Command\<P\>, typed CommandContext, separate snapshot |
|
|
32
|
+
| I-04 | #1 | Undoable flag on CommandDefinition |
|
|
33
|
+
| I-05 | #22 | Native \<dialog\> for modals |
|
|
34
|
+
| I-06 | #26 | Reusable PromptDialog component |
|
|
35
|
+
| I-07 | #14 | God file split (8 domain files + 3 utilities) |
|
|
36
|
+
| I-08 | #12 | Add getSelection() to PluginAPI |
|
|
37
|
+
| I-09 | #13 | Add mutator methods to PluginAPI |
|
|
38
|
+
| I-10 | #19 | Enable strict TypeScript flags |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## I-01: makeStrokeTool Factory
|
|
43
|
+
|
|
44
|
+
### Summary
|
|
45
|
+
|
|
46
|
+
`pencil-tool.ts` (128 lines) and `eraser-tool.ts` (123 lines) are structurally
|
|
47
|
+
identical. They differ only in command name, description verb, pixel color resolution,
|
|
48
|
+
icon, and toolbar order. Both maintain identical stroke state (`drawing`, `lastX`,
|
|
49
|
+
`lastY`, `strokePixels`, `strokeLayerId`), identical `onPointerDown`/`onPointerMove`/
|
|
50
|
+
`onPointerUp` handlers with Bresenham interpolation, and identical command
|
|
51
|
+
`execute`/`undo`/`describe` definitions using the `_snapshot` pattern. A factory
|
|
52
|
+
function will eliminate this duplication and make it trivial to add future stroke-based
|
|
53
|
+
tools (e.g., pixel-perfect pencil, tinted eraser).
|
|
54
|
+
|
|
55
|
+
### Files Touched
|
|
56
|
+
|
|
57
|
+
| File Path | Action | What Changes |
|
|
58
|
+
|-----------|--------|--------------|
|
|
59
|
+
| `plugins/builtin/make-stroke-tool.ts` | Create | New factory function `makeStrokeTool(config: StrokeToolConfig): PluginModule` containing the shared command definition (execute/undo/describe), stroke state management, pointer handlers, and toolbar registration |
|
|
60
|
+
| `plugins/builtin/pencil-tool.ts` | Modify | Reduce from ~128 lines to ~20 lines. Remove all inline command definition, stroke state, and pointer handlers. Import `makeStrokeTool` and call it with config: `{ pluginName: 'builtin/pencil', commandName: 'draw_pixels', toolId: 'pencil', icon: PencilIcon, toolbarOrder: 10, toolbarGroup: 'draw', describeVerb: 'Drew', resolveColor: (ctx) => hexToRgba(ctx.color) }` |
|
|
61
|
+
| `plugins/builtin/eraser-tool.ts` | Modify | Reduce from ~123 lines to ~20 lines. Same transformation as pencil. Config: `{ pluginName: 'builtin/eraser', commandName: 'erase_pixels', toolId: 'eraser', icon: EraserIcon, toolbarOrder: 20, toolbarGroup: 'draw', describeVerb: 'Erased', resolveColor: () => ({ r: 0, g: 0, b: 0, a: 0 }) }` |
|
|
62
|
+
|
|
63
|
+
### Step-by-Step Instructions
|
|
64
|
+
|
|
65
|
+
- Define the `StrokeToolConfig` interface in `make-stroke-tool.ts`:
|
|
66
|
+
- `pluginName: string` -- used in `Command.plugin` field (e.g., `'builtin/pencil'`)
|
|
67
|
+
- `commandName: string` -- used in `api.addCommand(name, ...)` (e.g., `'draw_pixels'`)
|
|
68
|
+
- `toolId: string` -- used in `api.addTool(name, ...)` (e.g., `'pencil'`)
|
|
69
|
+
- `icon: string | Component` -- the Svelte icon component
|
|
70
|
+
- `toolbarOrder: number` -- the `order` field in `ToolbarContribution`
|
|
71
|
+
- `toolbarGroup: string` -- the `group` field in `ToolbarContribution`
|
|
72
|
+
- `describeVerb: string` -- used in `describe()` like `"${verb} ${n} pixel(s)"`
|
|
73
|
+
- `resolveColor: (ctx: ToolContext) => { r: number; g: number; b: number; a: number }` -- returns RGBA for each stroke pixel
|
|
74
|
+
- Import shared dependencies from `drawing-utils.ts`: `bresenhamLine`, `snapshotPixels`, `applyPixels`; import types `PluginModule` from `plugin-loader.js`, `PixelBuffer` from `pixel-buffer.ts`, `PixelData` from `drawing-utils.ts`, `ToolContext` from `plugin-types.js`
|
|
75
|
+
- Implement `makeStrokeTool(config)` returning a `PluginModule`:
|
|
76
|
+
- The `register(api)` function body is extracted from the current `pencil-tool.ts` lines 23-127
|
|
77
|
+
- Command definition (lines 23-53 of pencil-tool.ts): `execute` uses `snapshotPixels` + `applyPixels`, `undo` restores snapshot, `describe` returns `"${config.describeVerb} ${pixels.length} pixel(s)"`
|
|
78
|
+
- Stroke state variables (lines 56-61): `drawing`, `lastX`, `lastY`, `strokePixels`, `strokeColor`, `strokeLayerId`
|
|
79
|
+
- Tool definition (lines 71-126): `onPointerDown` calls `config.resolveColor(ctx)` instead of hardcoded `hexToRgba(strokeColor)` or `{r:0,g:0,b:0,a:0}`, `onPointerMove` uses bresenham interpolation with resolved color, `onPointerUp` dispatches the command using `config.commandName` and `config.pluginName`
|
|
80
|
+
- Toolbar registration uses `config.toolId`, `config.toolbarGroup`, `config.toolbarOrder`
|
|
81
|
+
- In `pencil-tool.ts`, replace the entire body with:
|
|
82
|
+
- Import `makeStrokeTool` from `./make-stroke-tool.js`
|
|
83
|
+
- Import `hexToRgba` from `./drawing-utils.js`
|
|
84
|
+
- Import `PencilIcon` from `~icons/lucide/pencil`
|
|
85
|
+
- Export `pencilToolPlugin = makeStrokeTool({ ... })`
|
|
86
|
+
- In `eraser-tool.ts`, same pattern but with eraser-specific config
|
|
87
|
+
- Handle `strokeColor` capturing: in pencil, `resolveColor` receives the `ToolContext` on each call, so it can read `ctx.color` at pointer-down time. The factory should call `resolveColor(ctx)` in `onPointerDown` and cache the result for the duration of the stroke (the current pencil does this with `strokeColor = ctx.color` on line 79)
|
|
88
|
+
|
|
89
|
+
### Preconditions
|
|
90
|
+
|
|
91
|
+
- None. This is fully independent.
|
|
92
|
+
|
|
93
|
+
### Postconditions / Verification
|
|
94
|
+
|
|
95
|
+
- `tsc --noEmit` passes
|
|
96
|
+
- Draw with pencil: pixels appear in foreground color
|
|
97
|
+
- Draw with eraser: pixels become transparent (r=0,g=0,b=0,a=0)
|
|
98
|
+
- Undo pencil stroke: pixels revert to pre-stroke state
|
|
99
|
+
- Undo eraser stroke: pixels revert to pre-erase state
|
|
100
|
+
- Both tools appear in the drawing-tools toolbar at their correct positions (pencil order=10, eraser order=20)
|
|
101
|
+
- Both tools display correct icons
|
|
102
|
+
- `grep -r "let drawing = false" plugins/builtin/` returns only `make-stroke-tool.ts` (no duplication)
|
|
103
|
+
|
|
104
|
+
### Risks and Edge Cases
|
|
105
|
+
|
|
106
|
+
- The factory must capture `resolveColor(ctx)` once at `onPointerDown` and reuse the cached RGBA for the entire stroke, matching current pencil behavior (line 79: `strokeColor = ctx.color`). If `resolveColor` is called per-pixel in `onPointerMove`, the eraser will work fine (constant output) but the pencil could produce mixed colors if the user changes foreground color mid-stroke.
|
|
107
|
+
- The `pattern-stamp-tool.ts` should NOT use this factory; it has fundamentally different data flow (pattern buffers, tiling modes). Verify it remains unchanged.
|
|
108
|
+
- The `_snapshot` convention is still used here. I-02 (makeSnapshotUndo) and I-03 (Generic Command) will update this later.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## I-02: makeSnapshotUndo Factory
|
|
113
|
+
|
|
114
|
+
### Summary
|
|
115
|
+
|
|
116
|
+
32 out of 33 undo handlers across plugin files are byte-for-byte identical: get the
|
|
117
|
+
active buffer from context, bail if missing, apply snapshot pixels. This factory
|
|
118
|
+
extracts the pattern into a single reusable function in `drawing-utils.ts`. The single
|
|
119
|
+
exception (`move_selection` in `selection-tool.ts`) has custom undo logic and remains
|
|
120
|
+
manual. After I-03 changes the snapshot architecture (separate slot on UndoEntry), this
|
|
121
|
+
factory's signature will be updated, but it can be built now against the current
|
|
122
|
+
`_snapshot`-in-params pattern.
|
|
123
|
+
|
|
124
|
+
### Files Touched
|
|
125
|
+
|
|
126
|
+
| File Path | Action | What Changes |
|
|
127
|
+
|-----------|--------|--------------|
|
|
128
|
+
| `plugins/builtin/drawing-utils.ts` | Modify | Add `makeSnapshotUndo()` function after the existing `applyPixels` function (after line 399). Returns `(params: Record<string, unknown>, ctx: CommandContext) => void` |
|
|
129
|
+
| `plugins/builtin/make-stroke-tool.ts` | Modify | Import and use `makeSnapshotUndo()` as the `undo` handler in the command definition (created in I-01) |
|
|
130
|
+
| `plugins/builtin/pencil-tool.ts` | Modify | If not already using factory from I-01, replace inline undo handler. If using I-01 factory, no change needed here |
|
|
131
|
+
| `plugins/builtin/eraser-tool.ts` | Modify | Same as pencil-tool.ts |
|
|
132
|
+
| `plugins/builtin/rect-tool.ts` | Modify | Replace lines 56-60 (undo handler) with `undo: makeSnapshotUndo()` |
|
|
133
|
+
| `plugins/builtin/circle-tool.ts` | Modify | Replace inline undo handler with `makeSnapshotUndo()` |
|
|
134
|
+
| `plugins/builtin/line-tool.ts` | Modify | Replace inline undo handler with `makeSnapshotUndo()` |
|
|
135
|
+
| `plugins/builtin/diamond-tool.ts` | Modify | Replace inline undo handler with `makeSnapshotUndo()` |
|
|
136
|
+
| `plugins/builtin/fill-tool.ts` | Modify | Replace lines 41-46 (undo handler) with `undo: makeSnapshotUndo()` |
|
|
137
|
+
| `plugins/builtin/advanced-fill-tool.ts` | Modify | Replace inline undo handler |
|
|
138
|
+
| `plugins/builtin/noise-tool.ts` | Modify | Replace inline undo handler |
|
|
139
|
+
| `plugins/builtin/gradient-tool.ts` | Modify | Replace inline undo handler |
|
|
140
|
+
| `plugins/builtin/dither-tool.ts` | Modify | Replace inline undo handler |
|
|
141
|
+
| `plugins/builtin/pattern-stamp-tool.ts` | Modify | Replace inline undo handler |
|
|
142
|
+
| `plugins/builtin/drawing-primitives-plugin.ts` | Modify | Replace 6 inline undo handlers (one per primitive command) |
|
|
143
|
+
| `plugins/builtin/effects/blur.ts` | Modify | Replace inline undo handler |
|
|
144
|
+
| `plugins/builtin/effects/sharpen.ts` | Modify | Replace inline undo handler |
|
|
145
|
+
| `plugins/builtin/effects/glow.ts` | Modify | Replace inline undo handler |
|
|
146
|
+
| `plugins/builtin/effects/shadow.ts` | Modify | Replace inline undo handler |
|
|
147
|
+
| `plugins/builtin/effects/scale.ts` | Modify | Replace inline undo handler |
|
|
148
|
+
| `plugins/builtin/effects/flip.ts` | Modify | Replace inline undo handler |
|
|
149
|
+
| `plugins/builtin/effects/outline.ts` | Modify | Replace inline undo handler |
|
|
150
|
+
| `plugins/builtin/effects/color-effects.ts` | Modify | Replace 5 inline undo handlers |
|
|
151
|
+
| `plugins/builtin/effects/rotate.ts` | Modify | Replace inline undo handler |
|
|
152
|
+
|
|
153
|
+
### Step-by-Step Instructions
|
|
154
|
+
|
|
155
|
+
- In `plugins/builtin/drawing-utils.ts`, after `applyPixels` (line 399), add:
|
|
156
|
+
```
|
|
157
|
+
export function makeSnapshotUndo(): (params: Record<string, unknown>, ctx: Record<string, unknown>) => void {
|
|
158
|
+
return (params, ctx) => {
|
|
159
|
+
const buffer = (ctx as { getActiveBuffer?: () => PixelBuffer }).getActiveBuffer?.();
|
|
160
|
+
if (!buffer) return;
|
|
161
|
+
const snapshot = params._snapshot as PixelData[];
|
|
162
|
+
applyPixels(buffer, snapshot);
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
- Note: The `ctx` cast pattern `(ctx as { getActiveBuffer?: () => PixelBuffer })` matches the current pattern used in all 32 call sites. This will be cleaned up in I-03 when `CommandContext` gets a proper `getActiveBuffer` definition.
|
|
167
|
+
- For each of the 23 files listed above, perform the following mechanical replacement:
|
|
168
|
+
- Add `makeSnapshotUndo` to the import from `./drawing-utils.js` (or `../../plugins/builtin/drawing-utils.js` for files under `src/`)
|
|
169
|
+
- Replace the inline `undo(params, ctx) { ... }` block with `undo: makeSnapshotUndo(),`
|
|
170
|
+
- The replaced block always follows this exact 4-line pattern:
|
|
171
|
+
```
|
|
172
|
+
undo(params, ctx) {
|
|
173
|
+
const buffer = (ctx as { getActiveBuffer?: () => PixelBuffer }).getActiveBuffer?.();
|
|
174
|
+
if (!buffer) return;
|
|
175
|
+
const snapshot = params._snapshot as PixelData[];
|
|
176
|
+
applyPixels(buffer, snapshot);
|
|
177
|
+
},
|
|
178
|
+
```
|
|
179
|
+
- Verify that `selection-tool.ts` `move_selection` undo handler is NOT touched (it has custom logic: restoring selected pixel positions, not just pixel colors)
|
|
180
|
+
- If I-01 has been completed, update `make-stroke-tool.ts` to use `makeSnapshotUndo()` in its command definition instead of the inline handler
|
|
181
|
+
|
|
182
|
+
### Preconditions
|
|
183
|
+
|
|
184
|
+
- None strictly required. Ideally done after I-01 so the stroke tool factory can benefit immediately.
|
|
185
|
+
|
|
186
|
+
### Postconditions / Verification
|
|
187
|
+
|
|
188
|
+
- `tsc --noEmit` passes
|
|
189
|
+
- All 39 tests pass (run `npx vitest run`)
|
|
190
|
+
- Undo/redo works for every drawing tool: pencil, eraser, rect, circle, line, diamond, fill, advanced fill, noise, gradient, dither, pattern stamp
|
|
191
|
+
- Undo/redo works for all effects: blur, sharpen, glow, shadow, scale, flip, outline, rotate, and all color effects (brightness, contrast, hue shift, saturation, invert colors)
|
|
192
|
+
- Undo for `move_selection` still works (untouched)
|
|
193
|
+
- `grep -rn "const buffer = (ctx as" plugins/builtin/ | grep -v "make-stroke-tool\|drawing-utils\|selection-tool" | grep undo` returns 0 results (all inline undo handlers eliminated except the factory definition and the selection exception)
|
|
194
|
+
|
|
195
|
+
### Risks and Edge Cases
|
|
196
|
+
|
|
197
|
+
- The `drawing-primitives-plugin.ts` has 6 commands (`draw_line_prim`, `draw_rect_prim`, etc.) sharing the same file. Each needs its `undo` replaced individually. Count them carefully during implementation.
|
|
198
|
+
- The `color-effects.ts` has 5 commands (`brightness`, `contrast`, `hue_shift`, `saturation`, `invert_colors`). Same careful counting applies.
|
|
199
|
+
- When I-03 moves `_snapshot` out of `params` into a dedicated `UndoEntry.snapshot` slot, this factory will need updating. The update is mechanical: change from `params._snapshot` to a `snapshot` argument. This is a known forward dependency.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## I-03: Generic Command\<P\>, Typed CommandContext, Separate Snapshot
|
|
204
|
+
|
|
205
|
+
### Summary
|
|
206
|
+
|
|
207
|
+
This is the most impactful single change. It addresses three sub-problems: (A) untyped
|
|
208
|
+
`Command.params` (currently `Record<string, unknown>`) forcing ~170 `params.field as Type`
|
|
209
|
+
casts, (B) the `_snapshot` field smuggled into `params` as a mutable side-channel
|
|
210
|
+
(used in 21+ files), and (C) untyped `CommandContext` lacking `getActiveBuffer`,
|
|
211
|
+
causing 73 `(ctx as { getActiveBuffer?: ... })` casts across 24 plugin files.
|
|
212
|
+
After this change, each plugin defines a typed params interface, the snapshot lives on
|
|
213
|
+
the undo stack entry, and `CommandContext.getActiveBuffer` is properly typed.
|
|
214
|
+
|
|
215
|
+
### Files Touched
|
|
216
|
+
|
|
217
|
+
| File Path | Action | What Changes |
|
|
218
|
+
|-----------|--------|--------------|
|
|
219
|
+
| `src/lib/core/commands.ts` | Modify | Make `Command<P = Record<string, unknown>>` generic (line 11); make `CommandDefinition<P = Record<string, unknown>>` generic (line 44); `execute` returns `unknown` (snapshot data) instead of `void`; `undo` takes a third `snapshot: unknown` argument; add `getActiveBuffer` to `CommandContext` interface (line 67) |
|
|
220
|
+
| `src/lib/core/dispatcher.ts` | Modify | Capture `execute()` return value as snapshot data (line 118); store `snapshot` field on `UndoEntry` (add to interface around line 35); pass snapshot to `undo()` call (line 165); pass snapshot to `redo()` re-execution |
|
|
221
|
+
| `src/lib/core/registries.svelte.ts` | Modify | Change `commandRegistry` type from `ReactiveRegistry<CommandDefinition>` to `ReactiveRegistry<CommandDefinition<any>>` (line 66). This is the type-erasure boundary. |
|
|
222
|
+
| `src/lib/core/plugin-types.ts` | Modify | Update `PluginAPI.addCommand` signature: `addCommand<P>(name: string, definition: CommandDefinition<P>): void` (line 15) |
|
|
223
|
+
| `src/lib/core/plugin-api.ts` | Modify | Update `addCommand` implementation to accept `CommandDefinition<any>` for the registry `.set()` call; import `PixelBuffer` and wire `getActiveBuffer` into the `context` object |
|
|
224
|
+
| `plugins/builtin/drawing-utils.ts` | Modify | Update `makeSnapshotUndo` (from I-02) to accept `snapshot: unknown` as third param instead of reading `params._snapshot`; remove `PixelData[]` cast from params, cast from snapshot instead |
|
|
225
|
+
| All 23 tool/effect plugin files | Modify | Define typed params interfaces (e.g., `DrawPixelsParams`, `DrawRectParams`, `FloodFillParams`); remove all `params.field as Type` casts; remove `_snapshot: []` from dispatch params; change `execute` to return snapshot data instead of mutating params; change `undo` to accept third `snapshot` arg; remove `(ctx as { getActiveBuffer? })` casts |
|
|
226
|
+
| `src/lib/canvas/canvas-init-plugin.ts` | Modify | Remove the manual ctx cast for `getActiveBuffer`; it will now be a first-class `CommandContext` method |
|
|
227
|
+
| Test files: `tool-plugins.test.ts`, `effects.test.ts`, `gradient.test.ts`, etc. | Modify | Update mock `CommandContext` objects to include `getActiveBuffer`; update test dispatch calls to not include `_snapshot` in params |
|
|
228
|
+
|
|
229
|
+
### Step-by-Step Instructions
|
|
230
|
+
|
|
231
|
+
- **Step 1: Update `commands.ts` (lines 11-68)**
|
|
232
|
+
- Make `Command` generic: `export interface Command<P = Record<string, unknown>>` with `params: P` instead of `params: Record<string, unknown>`
|
|
233
|
+
- Make `CommandDefinition` generic: `export interface CommandDefinition<P = Record<string, unknown>>`
|
|
234
|
+
- Change `execute` signature from `(params: Record<string, unknown>, context: CommandContext) => void` to `(params: P, context: CommandContext) => unknown | void`
|
|
235
|
+
- Change `undo` signature from `(params: Record<string, unknown>, context: CommandContext) => void` to `(params: P, context: CommandContext, snapshot: unknown) => void`
|
|
236
|
+
- Change `describe` signature from `(params: Record<string, unknown>) => string` to `(params: P) => string`
|
|
237
|
+
- Add to `CommandContext` interface (currently empty index signature at line 67):
|
|
238
|
+
```
|
|
239
|
+
getActiveBuffer?: () => PixelBuffer | null;
|
|
240
|
+
```
|
|
241
|
+
- Import `PixelBuffer` type: `import type { PixelBuffer } from '../canvas/pixel-buffer.js';`
|
|
242
|
+
|
|
243
|
+
- **Step 2: Update `UndoEntry` in `commands.ts` (line 35)**
|
|
244
|
+
- Add `snapshot: unknown;` field to the `UndoEntry` interface
|
|
245
|
+
|
|
246
|
+
- **Step 3: Update `dispatcher.ts`**
|
|
247
|
+
- In `dispatch()` (line 118): capture execute return value: `const snapshot = definition.execute(command.params, context);`
|
|
248
|
+
- In the `UndoEntry` construction (lines 121-126): add `snapshot` field: `snapshot,`
|
|
249
|
+
- In `undoLast()` (line 165): pass snapshot: `definition.undo(entry.command.params, context, entry.snapshot);`
|
|
250
|
+
- In `redoLast()` (line 185): capture new snapshot from re-execution: `const newSnapshot = definition.execute(entry.command.params, context);` and update: `entry.snapshot = newSnapshot;`
|
|
251
|
+
|
|
252
|
+
- **Step 4: Update `registries.svelte.ts` (line 66)**
|
|
253
|
+
- Change `createRegistry<CommandDefinition>()` to `createRegistry<CommandDefinition<any>>()`
|
|
254
|
+
- Update the import to use the generic type
|
|
255
|
+
|
|
256
|
+
- **Step 5: Update `plugin-types.ts` (line 15)**
|
|
257
|
+
- Change `addCommand(name: string, definition: CommandDefinition): void` to `addCommand<P>(name: string, definition: CommandDefinition<P>): void`
|
|
258
|
+
|
|
259
|
+
- **Step 6: Wire `getActiveBuffer` into `CommandContext`**
|
|
260
|
+
- In `plugin-api.ts` or `canvas-init-plugin.ts`, ensure the dispatcher context includes `getActiveBuffer`. Currently `canvas-init-plugin.ts` sets this on the context; verify it uses the proper interface method name.
|
|
261
|
+
|
|
262
|
+
- **Step 7: Define param interfaces and update plugins (23 files)**
|
|
263
|
+
- For each plugin file, define a params interface at the top. Example for `rect-tool.ts`:
|
|
264
|
+
```
|
|
265
|
+
interface DrawRectParams {
|
|
266
|
+
x: number;
|
|
267
|
+
y: number;
|
|
268
|
+
w: number;
|
|
269
|
+
h: number;
|
|
270
|
+
filled: boolean;
|
|
271
|
+
color: string;
|
|
272
|
+
layerId: string;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
- Change `execute(params, ctx)` to `execute(params: DrawRectParams, ctx)` and remove all `as` casts
|
|
276
|
+
- Change `execute` to return snapshot data: `return snapshotPixels(buffer, points);` instead of `params._snapshot = snapshotPixels(buffer, points);`
|
|
277
|
+
- Change `undo(params, ctx)` to `undo(params: DrawRectParams, ctx, snapshot)` and use `snapshot as PixelData[]` instead of `params._snapshot as PixelData[]`
|
|
278
|
+
- Remove `_snapshot: []` from all `api.dispatch()` calls (e.g., rect-tool.ts line 120)
|
|
279
|
+
- Remove `(ctx as { getActiveBuffer?: () => PixelBuffer })` casts; use `ctx.getActiveBuffer?.()` directly
|
|
280
|
+
|
|
281
|
+
- **Step 8: Update `makeSnapshotUndo` (from I-02)**
|
|
282
|
+
- Change signature to accept `snapshot: unknown` as third argument:
|
|
283
|
+
```
|
|
284
|
+
export function makeSnapshotUndo(): (params: unknown, ctx: CommandContext, snapshot: unknown) => void {
|
|
285
|
+
return (_params, ctx, snapshot) => {
|
|
286
|
+
const buffer = ctx.getActiveBuffer?.();
|
|
287
|
+
if (!buffer) return;
|
|
288
|
+
applyPixels(buffer, snapshot as PixelData[]);
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
- **Step 9: Update test files**
|
|
294
|
+
- All test mocks that create a `CommandContext` need `getActiveBuffer` added
|
|
295
|
+
- All test dispatch calls need `_snapshot` removed from params
|
|
296
|
+
- Mock `getActiveBuffer` should return a test `PixelBuffer` instance
|
|
297
|
+
|
|
298
|
+
### Preconditions
|
|
299
|
+
|
|
300
|
+
- I-01 and I-02 should be completed first (they create `make-stroke-tool.ts` and `makeSnapshotUndo`, which both need updating here). However, I-03 can be done independently if the implementor is willing to handle the duplication in pencil/eraser at the same time.
|
|
301
|
+
|
|
302
|
+
### Postconditions / Verification
|
|
303
|
+
|
|
304
|
+
- `tsc --noEmit` passes with zero errors
|
|
305
|
+
- All tests pass (`npx vitest run`)
|
|
306
|
+
- `grep -rn "params\.\w\+ as " plugins/builtin/` returns 0 results (all param casts eliminated)
|
|
307
|
+
- `grep -rn "(ctx as {" plugins/builtin/` returns 0 results (all context casts eliminated)
|
|
308
|
+
- `grep -rn "_snapshot" plugins/builtin/` returns 0 results (snapshot no longer stored in params)
|
|
309
|
+
- Undo/redo works correctly for all tools and effects
|
|
310
|
+
- The dispatcher correctly captures snapshot from `execute()` and passes it to `undo()`
|
|
311
|
+
|
|
312
|
+
### Risks and Edge Cases
|
|
313
|
+
|
|
314
|
+
- **Redo snapshot freshness:** When redoing, the re-execution of `execute()` must return a fresh snapshot (the buffer state has changed since the original execution). The plan handles this by capturing `newSnapshot` in `redoLast()`.
|
|
315
|
+
- **Type erasure boundary:** The `commandRegistry` stores `CommandDefinition<any>`. This means the registry itself does not enforce param types. Type safety exists at the plugin registration boundary (each plugin's `register()` function) and at dispatch time (the `Command` object carries typed params). This is acceptable and standard for heterogeneous registries.
|
|
316
|
+
- **Incremental migration:** All 23 plugin files must be updated atomically because the `execute` return type and `undo` signature change globally. A partial migration would cause type errors. Consider doing the dispatcher + commands.ts change first (backward compatible with `void` return from execute), then migrating plugins file by file.
|
|
317
|
+
- **`_snapshot` on first-execute guard:** Currently, plugins check `if (!(params._snapshot as PixelData[])?.length)` to avoid re-snapshotting on redo. With the separate snapshot slot, this guard is unnecessary -- on first execute, snapshot is `undefined`; on redo, the dispatcher manages it. But the `execute` function must always return a snapshot, even on redo. Verify the snapshot is taken before pixels are modified (current order: snapshot first, then applyPixels).
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## I-04: Undoable Flag on CommandDefinition
|
|
322
|
+
|
|
323
|
+
### Summary
|
|
324
|
+
|
|
325
|
+
All 4 UI entry points (menu-builder.ts line 66, ToolbarPanel.svelte line 184,
|
|
326
|
+
ContextMenu.svelte line 49, command-palette-state.svelte.ts line 54) call
|
|
327
|
+
`cmd.execute({}, {})` directly, bypassing the dispatcher. This means destructive
|
|
328
|
+
commands triggered from the menu (flatten_image, resize_canvas, merge_down, cut, paste)
|
|
329
|
+
never enter the undo stack. Adding an `undoable` flag to `CommandDefinition` lets each
|
|
330
|
+
UI entry point check the flag and route through `dispatch()` for commands that need
|
|
331
|
+
undo tracking.
|
|
332
|
+
|
|
333
|
+
### Files Touched
|
|
334
|
+
|
|
335
|
+
| File Path | Action | What Changes |
|
|
336
|
+
|-----------|--------|--------------|
|
|
337
|
+
| `src/lib/core/commands.ts` | Modify | Add `undoable?: boolean` to `CommandDefinition` interface (after `tier?` on line 50) |
|
|
338
|
+
| `src/lib/ui/menu-builder.ts` | Modify | In `buildMenuItem` (line 55), change `action` from `cmd.execute({}, {})` to a conditional: if `cmd.undoable`, dispatch through the dispatcher; otherwise, call `execute` directly |
|
|
339
|
+
| `src/lib/ui/ToolbarPanel.svelte` | Modify | In `executeCommand` function (line 182), add the same conditional routing |
|
|
340
|
+
| `src/lib/ui/ContextMenu.svelte` | Modify | In the action handler (line 49), add the same conditional routing |
|
|
341
|
+
| `src/lib/ui/command-palette/command-palette-state.svelte.ts` | Modify | In the execute callback (line 54), add the same conditional routing |
|
|
342
|
+
| `src/lib/ui/menu-commands-plugin.ts` (or split successors after I-07) | Modify | Mark ~15 destructive commands with `undoable: true`: `flatten_image`, `resize_canvas`, `crop_to_selection`, `merge_down`, `cut`, `paste`, `select_all`, `invert_selection`, `select_by_color` |
|
|
343
|
+
|
|
344
|
+
### Step-by-Step Instructions
|
|
345
|
+
|
|
346
|
+
- **Step 1: Add field to `commands.ts`**
|
|
347
|
+
- After the `tier?: UndoTier` field (line 50), add:
|
|
348
|
+
```
|
|
349
|
+
/** Whether this command should be routed through the dispatcher (undo stack) when triggered from UI. Default: false. */
|
|
350
|
+
undoable?: boolean;
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
- **Step 2: Create a shared dispatch helper**
|
|
354
|
+
- To avoid repeating the routing logic in 4 places, create a helper function in a new module or in `dispatcher.ts`:
|
|
355
|
+
```
|
|
356
|
+
export function executeOrDispatch(commandId: string): void {
|
|
357
|
+
const def = commandRegistry.get(commandId);
|
|
358
|
+
if (!def) return;
|
|
359
|
+
if (def.undoable) {
|
|
360
|
+
dispatch({
|
|
361
|
+
type: commandId,
|
|
362
|
+
plugin: 'ui',
|
|
363
|
+
version: '1.0.0',
|
|
364
|
+
params: {},
|
|
365
|
+
id: crypto.randomUUID(),
|
|
366
|
+
timestamp: Date.now(),
|
|
367
|
+
});
|
|
368
|
+
} else {
|
|
369
|
+
def.execute({} as any, {} as any);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
- Alternatively, add this to `dispatcher.ts` since it already has access to `commandRegistry` and `dispatch`. Place it after `redoLast()` (line 204).
|
|
374
|
+
|
|
375
|
+
- **Step 3: Update menu-builder.ts (line 66)**
|
|
376
|
+
- Change: `action: cmd ? () => cmd.execute({}, {}) : undefined,`
|
|
377
|
+
- To: `action: cmd ? () => executeOrDispatch(contrib.commandId) : undefined,`
|
|
378
|
+
- Import `executeOrDispatch` from `../core/dispatcher.js`
|
|
379
|
+
|
|
380
|
+
- **Step 4: Update ToolbarPanel.svelte (line 184)**
|
|
381
|
+
- Change: `cmd?.execute({}, {});`
|
|
382
|
+
- To: `if (targetId) executeOrDispatch(targetId);`
|
|
383
|
+
- Import `executeOrDispatch`
|
|
384
|
+
|
|
385
|
+
- **Step 5: Update ContextMenu.svelte (line 49)**
|
|
386
|
+
- Change: `cmd.execute({}, {})`
|
|
387
|
+
- To: `executeOrDispatch(item.commandId)` (or however the command ID is referenced)
|
|
388
|
+
|
|
389
|
+
- **Step 6: Update command-palette-state.svelte.ts (line 54)**
|
|
390
|
+
- Change: `execute: () => def.execute({}, {}),`
|
|
391
|
+
- To: `execute: () => executeOrDispatch(commandId),`
|
|
392
|
+
- Import `executeOrDispatch`
|
|
393
|
+
|
|
394
|
+
- **Step 7: Mark destructive commands as undoable**
|
|
395
|
+
- In `menu-commands-plugin.ts` (or its split successors), add `undoable: true` to the command definitions for:
|
|
396
|
+
- `flatten_image`
|
|
397
|
+
- `resize_canvas`
|
|
398
|
+
- `crop_to_selection`
|
|
399
|
+
- `merge_down`
|
|
400
|
+
- `cut`
|
|
401
|
+
- `paste`
|
|
402
|
+
- `select_all`
|
|
403
|
+
- `invert_selection`
|
|
404
|
+
- `select_by_color`
|
|
405
|
+
- Do NOT mark as undoable:
|
|
406
|
+
- `undo` / `redo` (would cause infinite recursion)
|
|
407
|
+
- View/zoom commands (non-destructive)
|
|
408
|
+
- Dialog-opening commands (`new_project_dialog`, `export_dialog`, `open_file_dialog`)
|
|
409
|
+
- `about`, `keyboard_shortcuts`, `report_bug`
|
|
410
|
+
|
|
411
|
+
### Preconditions
|
|
412
|
+
|
|
413
|
+
- I-03 should be completed first so that `CommandDefinition` is already generic and the `execute`/`undo` signatures are correct. The `executeOrDispatch` helper needs to construct a properly typed `Command` object.
|
|
414
|
+
|
|
415
|
+
### Postconditions / Verification
|
|
416
|
+
|
|
417
|
+
- `tsc --noEmit` passes
|
|
418
|
+
- Trigger `flatten_image` from the Image menu: it appears on the undo stack; Ctrl+Z undoes it
|
|
419
|
+
- Trigger `zoom_in` from the View menu: it does NOT appear on the undo stack
|
|
420
|
+
- Trigger `undo` from the Edit menu: it calls `undoLast()` directly, does not recurse
|
|
421
|
+
- The command palette's execute callbacks respect the `undoable` flag
|
|
422
|
+
- Context menu actions respect the `undoable` flag
|
|
423
|
+
|
|
424
|
+
### Risks and Edge Cases
|
|
425
|
+
|
|
426
|
+
- **`undo`/`redo` recursion:** These commands must NEVER be marked `undoable: true`. The `executeOrDispatch` helper would dispatch them, which would push them onto the undo stack, and undoing them would call undo again. Add a runtime guard in `executeOrDispatch` that throws if `commandId === 'undo' || commandId === 'redo'` and `undoable` is true.
|
|
427
|
+
- **Params for undoable menu commands:** Some undoable commands need params (e.g., `resize_canvas` needs width/height from the prompt). Currently they call `execute({}, {})` with empty params because they gather input internally. After I-03, these commands will have typed params. The `executeOrDispatch` helper passes `{}` as params, which works for commands that gather their own input but breaks for commands that expect typed params. This is acceptable for now because all current menu-triggered destructive commands handle their own input gathering inside `execute()`.
|
|
428
|
+
- **Future work:** Eventually, commands like `resize_canvas` should be refactored so that the dialog gathers input and passes it as typed params to `dispatch()`. This is outside the scope of I-04.
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## I-05: Native \<dialog\> for Modals
|
|
433
|
+
|
|
434
|
+
### Summary
|
|
435
|
+
|
|
436
|
+
All 3 modal dialogs (`NewProjectDialog.svelte`, `ExportDialog.svelte`,
|
|
437
|
+
`AboutDialog.svelte`) plus `CommandPalette.svelte` use manual `<div class="modal-overlay">`
|
|
438
|
+
or `<div class="about-backdrop">` patterns. These lack focus traps, proper ARIA
|
|
439
|
+
attributes, and consistent Escape handling (AboutDialog has no Escape handler at all).
|
|
440
|
+
Replacing with the native `<dialog>` element and `.showModal()` provides free focus
|
|
441
|
+
trapping, `::backdrop` pseudo-element, `aria-modal="true"`, Escape-to-close, and
|
|
442
|
+
top-layer rendering. Tauri's Chromium WebView fully supports `<dialog>`.
|
|
443
|
+
|
|
444
|
+
### Files Touched
|
|
445
|
+
|
|
446
|
+
| File Path | Action | What Changes |
|
|
447
|
+
|-----------|--------|--------------|
|
|
448
|
+
| `src/lib/ui/NewProjectDialog.svelte` | Modify | Replace `<div class="modal-overlay">` (line 117) with `<dialog>` element; add `bind:this` for dialog ref; use `$effect` to call `.showModal()` / `.close()` based on `open` prop; replace `.modal-overlay` CSS with `dialog::backdrop`; remove manual Escape handler (lines 48-53) and `<svelte:window>` binding (line 203) since `<dialog>` handles Escape natively; remove `<!-- svelte-ignore a11y_no_static_element_interactions -->` comment (line 115) |
|
|
449
|
+
| `src/lib/ui/ExportDialog.svelte` | Modify | Same transformation as NewProjectDialog |
|
|
450
|
+
| `src/lib/ui/AboutDialog.svelte` | Modify | Replace `<div class="about-backdrop">` (line 8 in template) with `<dialog>`; add Escape support (currently missing); add `bind:this` + `$effect` for showModal/close; restyle with `dialog::backdrop` |
|
|
451
|
+
| `src/lib/ui/command-palette/CommandPalette.svelte` | Modify | Replace the overlay `<div>` with `<dialog>`; the existing Escape handler (line 20-22) can delegate to the `close` event; add `bind:this` + `$effect` |
|
|
452
|
+
| `src/lib/ui/dialog-state.svelte.ts` | Modify | No changes strictly needed (dialog refs are local to each component, not global state). However, if `.showModal()` needs to be called externally, add optional `HTMLDialogElement` ref storage. |
|
|
453
|
+
| `src/app.css` (or relevant global CSS) | Modify | Add base `dialog` and `dialog::backdrop` styles using design tokens. Remove any `.modal-overlay` global styles if they exist. |
|
|
454
|
+
|
|
455
|
+
### Step-by-Step Instructions
|
|
456
|
+
|
|
457
|
+
- **Step 1: Establish the `<dialog>` pattern**
|
|
458
|
+
- The pattern for each dialog component is:
|
|
459
|
+
```svelte
|
|
460
|
+
<script>
|
|
461
|
+
let dialogEl: HTMLDialogElement | undefined = $state();
|
|
462
|
+
|
|
463
|
+
$effect(() => {
|
|
464
|
+
if (!dialogEl) return;
|
|
465
|
+
if (open) {
|
|
466
|
+
dialogEl.showModal();
|
|
467
|
+
} else {
|
|
468
|
+
dialogEl.close();
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
function handleClose() {
|
|
473
|
+
// Native <dialog> fires 'close' event on Escape and on .close()
|
|
474
|
+
onClose();
|
|
475
|
+
}
|
|
476
|
+
</script>
|
|
477
|
+
|
|
478
|
+
<dialog bind:this={dialogEl} onclose={handleClose}>
|
|
479
|
+
<!-- dialog content -->
|
|
480
|
+
</dialog>
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
- **Step 2: Convert NewProjectDialog.svelte**
|
|
484
|
+
- Add `let dialogEl: HTMLDialogElement | undefined = $state();` to the script block
|
|
485
|
+
- Add the `$effect` for showModal/close (see pattern above)
|
|
486
|
+
- Replace lines 116-201 (the `{#if open}` block containing `<div class="modal-overlay">`) with a `<dialog bind:this={dialogEl}>` containing the `.modal` div content directly
|
|
487
|
+
- Remove the `{#if open}` guard -- the `<dialog>` element is always in the DOM; visibility is controlled by `.showModal()` / `.close()`
|
|
488
|
+
- Remove the `handleKeydown` function (lines 48-53) and the `<svelte:window>` binding (line 203)
|
|
489
|
+
- Remove the `onclick={onClose}` on the overlay div (replace with backdrop click handling via `dialogEl.addEventListener('click', ...)` that checks if click target is the dialog itself, indicating a backdrop click)
|
|
490
|
+
- Replace `.modal-overlay` CSS (lines 206-217) with:
|
|
491
|
+
```css
|
|
492
|
+
dialog::backdrop {
|
|
493
|
+
background: rgba(0, 0, 0, 0.5);
|
|
494
|
+
}
|
|
495
|
+
dialog {
|
|
496
|
+
border: none;
|
|
497
|
+
padding: 0;
|
|
498
|
+
background: transparent;
|
|
499
|
+
max-width: 90vw;
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
- Keep the `.modal` class CSS for the inner content box
|
|
503
|
+
|
|
504
|
+
- **Step 3: Convert ExportDialog.svelte**
|
|
505
|
+
- Same transformation as NewProjectDialog. Examine the first 30 lines; it has the same `Props` interface with `open` and `onClose`.
|
|
506
|
+
|
|
507
|
+
- **Step 4: Convert AboutDialog.svelte**
|
|
508
|
+
- Currently uses `<div class="about-backdrop">` with no `open` prop -- it is mounted/unmounted by `menu-commands-plugin.ts` using Svelte's `mount`/`unmount` API
|
|
509
|
+
- Two options:
|
|
510
|
+
- Option A: Add an `open` prop and use the same `<dialog>` pattern
|
|
511
|
+
- Option B: Keep mount/unmount but have `onMount` call `.showModal()`
|
|
512
|
+
- Option B is simpler since it preserves the existing mount/unmount flow in `menu-commands-plugin.ts`. Add:
|
|
513
|
+
```svelte
|
|
514
|
+
import { onMount } from 'svelte';
|
|
515
|
+
let dialogEl: HTMLDialogElement;
|
|
516
|
+
onMount(() => dialogEl.showModal());
|
|
517
|
+
```
|
|
518
|
+
- Replace `<div class="about-backdrop">` with `<dialog bind:this={dialogEl}>`
|
|
519
|
+
- Add `onclose={onClose}` to the `<dialog>` element for Escape handling
|
|
520
|
+
|
|
521
|
+
- **Step 5: Convert CommandPalette.svelte**
|
|
522
|
+
- Currently uses `commandPaletteState.isOpen` to conditionally render
|
|
523
|
+
- Add `dialogEl` ref and `$effect` like the pattern above, keyed on `commandPaletteState.isOpen`
|
|
524
|
+
- The existing Escape handler (line 20-22) can be simplified: instead of manually checking `e.key === 'Escape'`, rely on the native `<dialog>` close event. But keep the ArrowDown/ArrowUp/Enter handlers.
|
|
525
|
+
|
|
526
|
+
- **Step 6: Handle backdrop clicks**
|
|
527
|
+
- Native `<dialog>` does not close on backdrop click by default. Add a click handler:
|
|
528
|
+
```svelte
|
|
529
|
+
function handleDialogClick(e: MouseEvent) {
|
|
530
|
+
if (e.target === dialogEl) onClose();
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
- This works because clicking the `::backdrop` pseudo-element fires the click on the `<dialog>` element itself, not on any child.
|
|
534
|
+
|
|
535
|
+
### Preconditions
|
|
536
|
+
|
|
537
|
+
- None. This is fully independent of the command system changes.
|
|
538
|
+
|
|
539
|
+
### Postconditions / Verification
|
|
540
|
+
|
|
541
|
+
- Tab key stays within each dialog (focus trap works natively)
|
|
542
|
+
- Escape closes each dialog (including AboutDialog, which currently lacks this)
|
|
543
|
+
- Backdrop click closes each dialog
|
|
544
|
+
- Screen readers announce `role="dialog"` and `aria-modal="true"` (provided automatically by `<dialog>`)
|
|
545
|
+
- No `z-index: 1000` hacks remain in dialog CSS (top-layer handles stacking)
|
|
546
|
+
- No `<!-- svelte-ignore a11y_no_static_element_interactions -->` comments remain in dialog components
|
|
547
|
+
- Dialogs render correctly in Tauri desktop build (test on macOS and Linux if possible)
|
|
548
|
+
- `tsc --noEmit` passes
|
|
549
|
+
|
|
550
|
+
### Risks and Edge Cases
|
|
551
|
+
|
|
552
|
+
- **Tauri WebView compatibility:** Chromium-based WebView supports `<dialog>` fully. Test the `::backdrop` styling in Tauri specifically, as some older WebView versions may not render `::backdrop` with CSS custom properties.
|
|
553
|
+
- **Double-close guard:** If `onClose()` sets `open = false` and the `$effect` calls `.close()`, but `.close()` fires the `close` event which calls `onClose()` again, there could be a loop. Guard against this by checking `dialogEl.open` before calling `.close()` in the effect.
|
|
554
|
+
- **Focus restoration:** When a `<dialog>` is closed, the browser restores focus to the element that was focused before `.showModal()`. Verify this works correctly with the dockview panel system.
|
|
555
|
+
- **CommandPalette auto-focus:** The CommandPalette currently uses a `$effect` with `queueMicrotask` to focus the input (line 15). With `<dialog>`, the browser auto-focuses the first focusable element inside the dialog. Adding `autofocus` attribute to the input element may be sufficient, eliminating the manual focus code.
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
## I-06: Reusable PromptDialog Component
|
|
560
|
+
|
|
561
|
+
### Summary
|
|
562
|
+
|
|
563
|
+
Two `window.prompt()` calls exist in `menu-commands-plugin.ts`: one for `resize_canvas`
|
|
564
|
+
(line 382) and one for `set_frame_duration_dialog` (line 669). `window.prompt()` fails
|
|
565
|
+
silently on macOS in Tauri (returns null without showing) and is unreliable on Linux.
|
|
566
|
+
This item creates a generic `PromptDialog.svelte` component using native `<dialog>`
|
|
567
|
+
(from I-05) and wires it through `dialogState` so any command can open a themed,
|
|
568
|
+
validatable prompt.
|
|
569
|
+
|
|
570
|
+
### Files Touched
|
|
571
|
+
|
|
572
|
+
| File Path | Action | What Changes |
|
|
573
|
+
|-----------|--------|--------------|
|
|
574
|
+
| `src/lib/ui/PromptDialog.svelte` | Create | New Svelte component: native `<dialog>`, text input, validation error display, confirm/cancel buttons, all themed with design tokens |
|
|
575
|
+
| `src/lib/ui/dialog-state.svelte.ts` | Modify | Add prompt state: `promptOpen` boolean, `promptConfig` object with `title`, `message`, `defaultValue`, `validate`, `onConfirm`, `onCancel` fields; add `openPrompt(config)` and `closePrompt()` methods |
|
|
576
|
+
| `src/App.svelte` | Modify | Import and render `<PromptDialog>` alongside existing dialogs (after line 29); bind to `dialogState.promptOpen` and `dialogState.promptConfig` |
|
|
577
|
+
| `src/lib/ui/menu-commands-plugin.ts` (or split successors) | Modify | Replace `prompt()` call on line 382 (resize_canvas) with `dialogState.openPrompt({...})`. Replace `prompt()` call on line 669 (set_frame_duration_dialog) with `dialogState.openPrompt({...})` |
|
|
578
|
+
|
|
579
|
+
### Step-by-Step Instructions
|
|
580
|
+
|
|
581
|
+
- **Step 1: Extend dialog-state.svelte.ts**
|
|
582
|
+
- Define a `PromptConfig` interface:
|
|
583
|
+
```
|
|
584
|
+
export interface PromptConfig {
|
|
585
|
+
title: string;
|
|
586
|
+
message: string;
|
|
587
|
+
defaultValue: string;
|
|
588
|
+
validate?: (value: string) => string | null; // returns error message or null
|
|
589
|
+
onConfirm: (value: string) => void;
|
|
590
|
+
onCancel?: () => void;
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
- Add reactive state:
|
|
594
|
+
```
|
|
595
|
+
let promptOpen = $state(false);
|
|
596
|
+
let promptConfig = $state<PromptConfig | null>(null);
|
|
597
|
+
```
|
|
598
|
+
- Add methods to `dialogState`:
|
|
599
|
+
```
|
|
600
|
+
get promptOpen() { return promptOpen; },
|
|
601
|
+
get promptConfig() { return promptConfig; },
|
|
602
|
+
|
|
603
|
+
openPrompt(config: PromptConfig) {
|
|
604
|
+
promptConfig = config;
|
|
605
|
+
promptOpen = true;
|
|
606
|
+
},
|
|
607
|
+
closePrompt() {
|
|
608
|
+
promptOpen = false;
|
|
609
|
+
promptConfig = null;
|
|
610
|
+
},
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
- **Step 2: Create PromptDialog.svelte**
|
|
614
|
+
- Props: `open: boolean`, `config: PromptConfig | null`, `onClose: () => void`
|
|
615
|
+
- Local state: `inputValue` (initialized from `config.defaultValue`), `errorMessage: string`
|
|
616
|
+
- Use `<dialog>` with `bind:this` + `$effect` for showModal/close (same pattern as I-05)
|
|
617
|
+
- Template structure:
|
|
618
|
+
- `<h2>` with `config.title`
|
|
619
|
+
- `<p>` with `config.message`
|
|
620
|
+
- `<input>` bound to `inputValue`, with `autofocus`
|
|
621
|
+
- Error display `<p class="error">` shown when `errorMessage` is non-empty
|
|
622
|
+
- Cancel and Confirm buttons
|
|
623
|
+
- On confirm: call `config.validate(inputValue)` if provided; if validation fails, set `errorMessage`; if passes, call `config.onConfirm(inputValue)` and close
|
|
624
|
+
- On cancel: call `config.onCancel?.()` and close
|
|
625
|
+
- Style with design tokens (--bg-panel, --border, --accent, etc.)
|
|
626
|
+
|
|
627
|
+
- **Step 3: Wire into App.svelte**
|
|
628
|
+
- Import `PromptDialog` from `./lib/ui/PromptDialog.svelte`
|
|
629
|
+
- Add after line 29:
|
|
630
|
+
```svelte
|
|
631
|
+
<PromptDialog
|
|
632
|
+
open={dialogState.promptOpen}
|
|
633
|
+
config={dialogState.promptConfig}
|
|
634
|
+
onClose={() => dialogState.closePrompt()}
|
|
635
|
+
/>
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
- **Step 4: Replace prompt() calls in menu-commands-plugin.ts**
|
|
639
|
+
- For `resize_canvas` (line 382):
|
|
640
|
+
```
|
|
641
|
+
dialogState.openPrompt({
|
|
642
|
+
title: 'Resize Canvas',
|
|
643
|
+
message: 'Enter new canvas size (WxH):',
|
|
644
|
+
defaultValue: `${canvasState.canvasWidth}x${canvasState.canvasHeight}`,
|
|
645
|
+
validate: (v) => {
|
|
646
|
+
const match = v.match(/^\s*(\d+)\s*[xX]\s*(\d+)\s*$/);
|
|
647
|
+
if (!match) return 'Format must be WxH (e.g., 64x64)';
|
|
648
|
+
const w = parseInt(match[1], 10);
|
|
649
|
+
const h = parseInt(match[2], 10);
|
|
650
|
+
if (w < 1 || w > 4096 || h < 1 || h > 4096) return 'Dimensions must be 1-4096';
|
|
651
|
+
return null;
|
|
652
|
+
},
|
|
653
|
+
onConfirm: (value) => {
|
|
654
|
+
const match = value.match(/^\s*(\d+)\s*[xX]\s*(\d+)\s*$/)!;
|
|
655
|
+
const newW = parseInt(match[1], 10);
|
|
656
|
+
const newH = parseInt(match[2], 10);
|
|
657
|
+
// ... existing resize logic ...
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
```
|
|
661
|
+
- For `set_frame_duration_dialog` (line 669):
|
|
662
|
+
```
|
|
663
|
+
dialogState.openPrompt({
|
|
664
|
+
title: 'Set Frame Duration',
|
|
665
|
+
message: 'Enter frame duration in ms (empty for global FPS):',
|
|
666
|
+
defaultValue: '',
|
|
667
|
+
validate: (v) => {
|
|
668
|
+
if (v.trim() === '') return null; // empty is valid (means global FPS)
|
|
669
|
+
const n = Number(v);
|
|
670
|
+
if (isNaN(n) || n <= 0) return 'Must be a positive number';
|
|
671
|
+
return null;
|
|
672
|
+
},
|
|
673
|
+
onConfirm: (value) => {
|
|
674
|
+
// ... existing frame duration logic ...
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
```
|
|
678
|
+
- Import `dialogState` if not already imported (it is already imported on line 33)
|
|
679
|
+
|
|
680
|
+
### Preconditions
|
|
681
|
+
|
|
682
|
+
- I-05 must be completed first (PromptDialog uses native `<dialog>`)
|
|
683
|
+
|
|
684
|
+
### Postconditions / Verification
|
|
685
|
+
|
|
686
|
+
- `tsc --noEmit` passes
|
|
687
|
+
- Resize canvas command (Image menu) opens a themed prompt dialog, not a browser prompt
|
|
688
|
+
- Entering a valid "WxH" format resizes the canvas
|
|
689
|
+
- Entering invalid input (e.g., "abc") shows inline validation error
|
|
690
|
+
- Pressing Escape or Cancel closes without action
|
|
691
|
+
- Set Frame Duration command opens a themed prompt dialog
|
|
692
|
+
- Entering a valid number sets the frame duration
|
|
693
|
+
- Entering empty string resets to global FPS
|
|
694
|
+
- `grep -rn "window\.prompt\|= prompt(" src/ plugins/` returns 0 results
|
|
695
|
+
- Works in Tauri on macOS and Linux (where `window.prompt()` fails)
|
|
696
|
+
|
|
697
|
+
### Risks and Edge Cases
|
|
698
|
+
|
|
699
|
+
- **Async flow:** `window.prompt()` is synchronous and blocking. `dialogState.openPrompt()` is async (callback-based). The `execute()` function for `resize_canvas` currently does the resize inline after `prompt()` returns. With the callback pattern, the resize logic moves into `onConfirm`. This changes the control flow but not the end result. Ensure `onConfirm` captures all needed closure variables.
|
|
700
|
+
- **Multiple prompts:** If two commands try to open prompts simultaneously, the second would overwrite the first. This is unlikely in practice since prompts are modal. Add a guard: if `promptOpen` is already true, queue the second or reject it.
|
|
701
|
+
- **Enter key:** The prompt should submit on Enter key press (in addition to clicking Confirm). Add `onkeydown` handler on the input that checks for Enter.
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## I-07: God File Split
|
|
706
|
+
|
|
707
|
+
### Summary
|
|
708
|
+
|
|
709
|
+
`menu-commands-plugin.ts` is 1333 lines containing 42 commands, 55 menu items,
|
|
710
|
+
9 toolbar items, duplicated zoom logic, clipboard state, DOM queries, and now-replaced
|
|
711
|
+
`prompt()` calls. This item splits it into 8 domain-specific plugin files + 3 shared
|
|
712
|
+
utility modules, then deletes the original. Each new file is a `PluginModule`
|
|
713
|
+
auto-discovered by `bootstrap.ts` via the existing `import.meta.glob` pattern
|
|
714
|
+
(which matches `src/lib/**/*-commands.ts`).
|
|
715
|
+
|
|
716
|
+
### Files Touched
|
|
717
|
+
|
|
718
|
+
| File Path | Action | What Changes |
|
|
719
|
+
|-----------|--------|--------------|
|
|
720
|
+
| `src/lib/canvas/zoom-utils.ts` | Create | Export `ZOOM_STEPS`, `zoomStepUp()`, `zoomStepDown()`, `DEFAULT_ZOOM` (currently duplicated in menu-commands-plugin.ts lines 96-113 and input-handler.ts) |
|
|
721
|
+
| `src/lib/canvas/viewport-utils.ts` | Create | Export `viewportCenter()` function (currently at menu-commands-plugin.ts lines 116-120, duplicated as DOM query) |
|
|
722
|
+
| `src/lib/edit/clipboard-state.ts` | Create | Export `clipboardBuffer` reactive state and its type (currently at menu-commands-plugin.ts line 92) |
|
|
723
|
+
| `src/lib/ui/file-commands.ts` | Create | `PluginModule` with commands: `new_project_dialog`, `open_file_dialog`, `save_project`, `export_dialog`; menu items for File menu (~80 lines) |
|
|
724
|
+
| `src/lib/ui/edit-commands.ts` | Create | `PluginModule` with commands: `undo`, `redo`, `cut`, `copy`, `paste`, `select_all`, `deselect`; menu items for Edit menu (~150 lines). Imports `clipboardBuffer` from `clipboard-state.ts` |
|
|
725
|
+
| `src/lib/ui/image-commands.ts` | Create | `PluginModule` with commands: `resize_canvas`, `crop_to_selection`, `flatten_image`, `canvas_size`; menu items for Image menu (~120 lines). Uses `dialogState.openPrompt()` instead of `prompt()` (from I-06) |
|
|
726
|
+
| `src/lib/ui/layer-menu-commands.ts` | Create | `PluginModule` with commands: `merge_down`, `move_layer_up`, `move_layer_down`; menu items referencing existing layer commands from `layer-commands.ts` (~100 lines) |
|
|
727
|
+
| `src/lib/ui/animation-menu-commands.ts` | Create | `PluginModule` with commands: `play_animation`, `set_frame_duration_dialog`; menu items for Animation menu (~80 lines). Uses `dialogState.openPrompt()` for frame duration |
|
|
728
|
+
| `src/lib/ui/select-commands.ts` | Create | `PluginModule` with commands: `invert_selection`, `select_by_color`; shared menu items for Select menu (~60 lines) |
|
|
729
|
+
| `src/lib/ui/view-commands.ts` | Create | `PluginModule` with all 17 view/zoom commands + 9 toolbar items (~350 lines). Imports from `zoom-utils.ts` and `viewport-utils.ts` |
|
|
730
|
+
| `src/lib/ui/help-commands.ts` | Create | `PluginModule` with commands: `about`, `keyboard_shortcuts`, `report_bug`; menu items for Help menu (~70 lines) |
|
|
731
|
+
| `src/lib/ui/menu-commands-plugin.ts` | Delete | Entire file removed after all commands migrated |
|
|
732
|
+
| `src/lib/canvas/input-handler.ts` | Modify | Replace local `ZOOM_STEPS`/`zoomStepUp`/`zoomStepDown` with imports from `zoom-utils.ts` |
|
|
733
|
+
| `src/lib/ui/CanvasViewport.svelte` | Modify | Replace local zoom constants with imports from `zoom-utils.ts` if applicable |
|
|
734
|
+
|
|
735
|
+
### Step-by-Step Instructions
|
|
736
|
+
|
|
737
|
+
- **Step 1: Create the 3 shared utilities**
|
|
738
|
+
- `zoom-utils.ts`: Copy `ZOOM_STEPS`, `zoomStepUp`, `zoomStepDown`, `DEFAULT_ZOOM` from menu-commands-plugin.ts lines 96-113. Export all four.
|
|
739
|
+
- `viewport-utils.ts`: Copy `viewportCenter()` from menu-commands-plugin.ts lines 116-120. Export it.
|
|
740
|
+
- `clipboard-state.ts`: Move the clipboard buffer declaration from line 92. Make it a reactive `$state` if needed for Svelte reactivity, or keep as plain `let` since it is module-level.
|
|
741
|
+
- Run `tsc --noEmit` to verify these utility files compile.
|
|
742
|
+
|
|
743
|
+
- **Step 2: Create domain files one at a time**
|
|
744
|
+
- For each domain file:
|
|
745
|
+
- Create the file with a `PluginModule` export (matching the `*-commands.ts` glob pattern)
|
|
746
|
+
- Move relevant `api.addCommand()` and `api.addMenuItem()` calls from `menu-commands-plugin.ts`
|
|
747
|
+
- Move relevant imports (icons, state modules)
|
|
748
|
+
- Do NOT import from `menu-commands-plugin.ts` -- each file is self-contained
|
|
749
|
+
- Run `tsc --noEmit` after each file
|
|
750
|
+
|
|
751
|
+
- Order of creation (to minimize broken imports):
|
|
752
|
+
1. `file-commands.ts` -- simplest, fewest dependencies
|
|
753
|
+
2. `help-commands.ts` -- self-contained, only icons and mount/unmount for AboutDialog
|
|
754
|
+
3. `view-commands.ts` -- largest but self-contained, imports from `zoom-utils.ts` and `viewport-utils.ts`
|
|
755
|
+
4. `edit-commands.ts` -- depends on `clipboard-state.ts` and dispatcher
|
|
756
|
+
5. `select-commands.ts` -- depends on selection-tool imports
|
|
757
|
+
6. `layer-menu-commands.ts` -- references existing layer commands
|
|
758
|
+
7. `animation-menu-commands.ts` -- references existing animation commands, uses PromptDialog
|
|
759
|
+
8. `image-commands.ts` -- uses PromptDialog for resize, references selection and layer state
|
|
760
|
+
|
|
761
|
+
- **Step 3: Update `input-handler.ts`**
|
|
762
|
+
- Replace local zoom logic with: `import { ZOOM_STEPS, zoomStepUp, zoomStepDown } from './zoom-utils.js';`
|
|
763
|
+
- Remove the duplicated zoom constants and functions
|
|
764
|
+
|
|
765
|
+
- **Step 4: Verify bootstrap discovery**
|
|
766
|
+
- All 8 new files match the glob pattern `src/lib/**/*-commands.ts` used in `bootstrap.ts` (line 23)
|
|
767
|
+
- Exception: files with `-menu-commands.ts` suffix also match `*-commands.ts`
|
|
768
|
+
- Verify each file exports a `PluginModule` with `name`, `version`, and `register` fields, which passes the `isPluginModule` type guard in `bootstrap.ts` (lines 28-39)
|
|
769
|
+
|
|
770
|
+
- **Step 5: Delete `menu-commands-plugin.ts`**
|
|
771
|
+
- Only after ALL commands and menu items are accounted for in the new files
|
|
772
|
+
- Verify by counting: 42 commands, 55 menu items, 9 toolbar items must all exist across the 8 new files
|
|
773
|
+
|
|
774
|
+
- **Step 6: Verify no remaining references**
|
|
775
|
+
- `grep -rn "menu-commands-plugin" src/` should return 0 results
|
|
776
|
+
- `grep -rn "menuCommandsPlugin" src/ plugins/` should return 0 results
|
|
777
|
+
|
|
778
|
+
### Preconditions
|
|
779
|
+
|
|
780
|
+
- I-04 (undoable flag) should be completed so destructive commands are already marked `undoable: true` before being moved
|
|
781
|
+
- I-06 (PromptDialog) should be completed so `prompt()` calls are already replaced before the split
|
|
782
|
+
- I-05 (native dialog) should be completed so AboutDialog already uses `<dialog>`
|
|
783
|
+
|
|
784
|
+
### Postconditions / Verification
|
|
785
|
+
|
|
786
|
+
- `tsc --noEmit` passes
|
|
787
|
+
- All 42 commands appear in the command palette (verify count)
|
|
788
|
+
- All menus render correctly: File (4 items), Edit (7 items), Image (4 items), Layer (6 items), Animation (5 items), Select (4 items), View (17+ items), Help (3 items), Context menus
|
|
789
|
+
- All 9 view toolbar items appear in the view-controls toolbar
|
|
790
|
+
- Zoom from keyboard, mouse wheel, and menu all use the same step logic from `zoom-utils.ts`
|
|
791
|
+
- `bootstrap.ts` discovers all 8 new plugin modules (check console or debugger)
|
|
792
|
+
- `grep -rn "menu-commands-plugin" src/` returns 0 results
|
|
793
|
+
- No duplicated zoom constants remain in the codebase
|
|
794
|
+
|
|
795
|
+
### Risks and Edge Cases
|
|
796
|
+
|
|
797
|
+
- **Plugin load order:** The new domain files may have implicit dependencies (e.g., `edit-commands.ts` assumes `selection-tool` commands are already registered for menu items). Add explicit `dependencies` arrays to each `PluginModule` if needed. For example, `edit-commands.ts` might need `dependencies: ['builtin/selection']` if it references `deselect` commands.
|
|
798
|
+
- **AboutDialog mount/unmount:** The `help-commands.ts` file needs to import `mount`/`unmount` from Svelte and the `AboutDialog.svelte` component. This cross-concern import is acceptable for now. After I-05 converts it to `<dialog>`, it may become a simpler dialogState-driven flow.
|
|
799
|
+
- **Context menu items:** Some context menu items (e.g., `context/canvas/cut`, `context/layer/delete`) are currently registered in `menu-commands-plugin.ts`. These need to be distributed to the correct domain file (cut goes to `edit-commands.ts`, layer delete goes to `layer-menu-commands.ts`).
|
|
800
|
+
- **Clipboard state reactivity:** If `clipboardBuffer` is used in `$derived` expressions anywhere, moving it to a separate module needs careful handling. Currently it is a plain `let` variable, not a `$state`, so no reactivity concern.
|
|
801
|
+
|
|
802
|
+
---
|
|
803
|
+
|
|
804
|
+
## I-08: Add getSelection() to PluginAPI
|
|
805
|
+
|
|
806
|
+
### Summary
|
|
807
|
+
|
|
808
|
+
`menu-commands-plugin.ts` line 38 directly imports `selectRect`, `deselectAll`,
|
|
809
|
+
`hasSelection`, and `getSelectedPixels` from `plugins/builtin/selection-tool.ts`. This
|
|
810
|
+
breaks the plugin boundary -- one plugin reaches into another's internal module.
|
|
811
|
+
Adding `getSelection()` to `PluginAPI` provides a proper read-only interface for
|
|
812
|
+
selection state. Mutations (selecting, deselecting) should go through `dispatch()` to
|
|
813
|
+
existing `select_rect` and `deselect` commands.
|
|
814
|
+
|
|
815
|
+
### Files Touched
|
|
816
|
+
|
|
817
|
+
| File Path | Action | What Changes |
|
|
818
|
+
|-----------|--------|--------------|
|
|
819
|
+
| `src/lib/core/plugin-types.ts` | Modify | Add `SelectionView` interface and `getSelection(): SelectionView` to `PluginAPI` interface (after `getActiveLayers()` on line 39) |
|
|
820
|
+
| `src/lib/core/plugin-api.ts` | Modify | Implement `getSelection()` by importing `hasSelection` and `getSelectedPixels` from `plugins/builtin/selection-tool.ts` internally; return a `SelectionView` object |
|
|
821
|
+
| `src/lib/ui/edit-commands.ts` (or `menu-commands-plugin.ts` if I-07 not done) | Modify | Replace `import { hasSelection, getSelectedPixels } from '...selection-tool.js'` with `api.getSelection()` calls inside command handlers. Replace `selectRect(...)` calls with `api.dispatch({ type: 'select_rect', ... })`. Replace `deselectAll()` calls with `api.dispatch({ type: 'deselect', ... })` |
|
|
822
|
+
| `src/lib/ui/image-commands.ts` (or `menu-commands-plugin.ts`) | Modify | Same replacement for `crop_to_selection` and any other selection-reading commands |
|
|
823
|
+
| `src/lib/ui/select-commands.ts` (or `menu-commands-plugin.ts`) | Modify | Same replacement for `invert_selection` and `select_by_color` |
|
|
824
|
+
|
|
825
|
+
### Step-by-Step Instructions
|
|
826
|
+
|
|
827
|
+
- **Step 1: Define `SelectionView` in `plugin-types.ts`**
|
|
828
|
+
- Add before the `PluginAPI` interface (or inside it as a nested type):
|
|
829
|
+
```
|
|
830
|
+
export interface SelectionView {
|
|
831
|
+
hasSelection(): boolean;
|
|
832
|
+
getSelectedPixels(): ReadonlySet<string>;
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
- Add to `PluginAPI` interface (after `getActiveLayers(): unknown`):
|
|
836
|
+
```
|
|
837
|
+
getSelection(): SelectionView;
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
- **Step 2: Implement in `plugin-api.ts`**
|
|
841
|
+
- Import from selection-tool: `import { hasSelection, getSelectedPixels } from '../../../plugins/builtin/selection-tool.js';`
|
|
842
|
+
- Add to the returned object:
|
|
843
|
+
```
|
|
844
|
+
getSelection() {
|
|
845
|
+
return {
|
|
846
|
+
hasSelection: () => hasSelection(),
|
|
847
|
+
getSelectedPixels: () => getSelectedPixels(),
|
|
848
|
+
};
|
|
849
|
+
},
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
- **Step 3: Update consumer files**
|
|
853
|
+
- In each consumer (edit-commands, image-commands, select-commands), the `register(api)` function has access to `api`. Inside command `execute()` bodies:
|
|
854
|
+
- Replace `hasSelection()` with `api.getSelection().hasSelection()` -- but note that `api` is available in `register()` scope, not inside `execute(params, ctx)`. Options:
|
|
855
|
+
- Option A: Store `api` reference in closure: `const sel = api.getSelection;` then use `sel().hasSelection()`
|
|
856
|
+
- Option B: Add `getSelection` to `CommandContext` as well
|
|
857
|
+
- Option A is simpler and maintains the current pattern where closures capture the `api` reference.
|
|
858
|
+
- Replace direct `selectRect(x, y, w, h)` calls with:
|
|
859
|
+
```
|
|
860
|
+
api.dispatch({
|
|
861
|
+
type: 'select_rect',
|
|
862
|
+
plugin: 'ui/edit-commands',
|
|
863
|
+
version: '1.0.0',
|
|
864
|
+
params: { x, y, w, h },
|
|
865
|
+
});
|
|
866
|
+
```
|
|
867
|
+
- Replace `deselectAll()` calls with:
|
|
868
|
+
```
|
|
869
|
+
api.dispatch({
|
|
870
|
+
type: 'deselect',
|
|
871
|
+
plugin: 'ui/edit-commands',
|
|
872
|
+
version: '1.0.0',
|
|
873
|
+
params: {},
|
|
874
|
+
});
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
- **Step 4: Remove direct imports**
|
|
878
|
+
- Remove `import { selectRect, deselect as deselectAll, hasSelection, getSelectedPixels } from '../../../plugins/builtin/selection-tool.js';` from all consumer files
|
|
879
|
+
|
|
880
|
+
### Preconditions
|
|
881
|
+
|
|
882
|
+
- I-07 (god file split) should be completed first so the consumers are already in separate files, making the import changes cleaner. If I-07 is not done, the changes apply to `menu-commands-plugin.ts` instead.
|
|
883
|
+
|
|
884
|
+
### Postconditions / Verification
|
|
885
|
+
|
|
886
|
+
- `tsc --noEmit` passes
|
|
887
|
+
- `cut` command works: reads selection via `api.getSelection()`, copies pixels to clipboard, clears them
|
|
888
|
+
- `crop_to_selection` works: reads selection bounds, resizes canvas
|
|
889
|
+
- `select_all` and `deselect` work via `api.dispatch()`
|
|
890
|
+
- `grep -rn "from.*selection-tool" src/lib/ui/` returns 0 results (no direct imports from UI command files)
|
|
891
|
+
- `grep -rn "from.*selection-tool" src/lib/core/plugin-api.ts` returns 1 result (the internal implementation import, which is acceptable)
|
|
892
|
+
|
|
893
|
+
### Risks and Edge Cases
|
|
894
|
+
|
|
895
|
+
- **Circular import risk:** `plugin-api.ts` imports from `selection-tool.ts`, which imports from `plugin-loader.ts` (for `PluginModule` type), which imports from `plugin-api.ts`. Check this chain:
|
|
896
|
+
- `plugin-api.ts` -> `selection-tool.ts` -> `plugin-loader.ts` -> `plugin-api.ts`
|
|
897
|
+
- `plugin-loader.ts` only imports the TYPE `PluginAPI` from `plugin-api.ts`, not the value. Since TypeScript erases type-only imports, there is no runtime circular dependency. But verify with `import type` usage.
|
|
898
|
+
- **Selection state timing:** `getSelection()` returns a live view (calls `hasSelection()` at invocation time). If selection state changes between the read and the subsequent operation, the command could operate on stale data. This matches the current behavior (direct function calls have the same timing) so it is not a regression.
|
|
899
|
+
- **Dispatch for mutations:** Using `api.dispatch()` for `select_rect` and `deselect` means these operations go through the undo stack. If the consuming command is itself on the undo stack (e.g., `select_all` dispatches `select_rect`), this creates nested undo entries. Verify this is the desired behavior. If `select_all` should be a single undo entry, it may need to call the selection functions directly or use a transaction pattern.
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
## I-09: Add Mutator Methods to PluginAPI
|
|
904
|
+
|
|
905
|
+
### Summary
|
|
906
|
+
|
|
907
|
+
All 3 importers (`aseprite-importer-plugin.ts`, `piskel-importer-plugin.ts`,
|
|
908
|
+
`sky-spec-plugin.ts`) bypass the `PluginAPI` entirely to set canvas size, add layers
|
|
909
|
+
and frames, and write pixel data via direct imports of internal modules. The API
|
|
910
|
+
currently only has read-only getters that return `unknown`. Adding ~15 mutator methods
|
|
911
|
+
lets importers work through the API, enforcing encapsulation and enabling future
|
|
912
|
+
validation/event hooks.
|
|
913
|
+
|
|
914
|
+
### Files Touched
|
|
915
|
+
|
|
916
|
+
| File Path | Action | What Changes |
|
|
917
|
+
|-----------|--------|--------------|
|
|
918
|
+
| `src/lib/core/plugin-types.ts` | Modify | Add 15 new method signatures to `PluginAPI` interface, organized in 6 groups (canvas, layers, frames, pixel data, palette, composite) |
|
|
919
|
+
| `src/lib/core/plugin-api.ts` | Modify | Implement all 15 methods as thin wrappers around existing module functions. Add imports for `canvasState`, `layerTree`, `frameModel`, `PixelBuffer`, color palette state |
|
|
920
|
+
| `plugins/builtin/importers/aseprite-importer-plugin.ts` | Modify | Replace direct imports of internal modules with `api.*` method calls |
|
|
921
|
+
| `plugins/builtin/importers/piskel-importer-plugin.ts` | Modify | Same replacement |
|
|
922
|
+
| `plugins/builtin/importers/sky-spec-plugin.ts` | Modify | Same replacement |
|
|
923
|
+
|
|
924
|
+
### Step-by-Step Instructions
|
|
925
|
+
|
|
926
|
+
- **Step 1: Canvas methods**
|
|
927
|
+
- Add to `PluginAPI` in `plugin-types.ts`:
|
|
928
|
+
```
|
|
929
|
+
setCanvasSize(width: number, height: number): void;
|
|
930
|
+
```
|
|
931
|
+
- Implement in `plugin-api.ts`:
|
|
932
|
+
```
|
|
933
|
+
setCanvasSize(width, height) {
|
|
934
|
+
canvasState.canvasWidth = width;
|
|
935
|
+
canvasState.canvasHeight = height;
|
|
936
|
+
},
|
|
937
|
+
```
|
|
938
|
+
- `canvasState` is already imported in `plugin-api.ts` (line 22)
|
|
939
|
+
|
|
940
|
+
- **Step 2: Layer methods**
|
|
941
|
+
- Add to `PluginAPI`:
|
|
942
|
+
```
|
|
943
|
+
addLayer(name: string): unknown; // returns the new layer object
|
|
944
|
+
resetLayers(): void;
|
|
945
|
+
setLayerVisibility(id: string, visible: boolean): void;
|
|
946
|
+
setLayerOpacity(id: string, opacity: number): void;
|
|
947
|
+
```
|
|
948
|
+
- Implement using `layerTree` module functions (already partially imported via `getLayers`, `getActiveLayer` on lines 24-25 of plugin-api.ts):
|
|
949
|
+
```
|
|
950
|
+
addLayer(name) {
|
|
951
|
+
return layerTree.addLayer(name);
|
|
952
|
+
},
|
|
953
|
+
resetLayers() {
|
|
954
|
+
layerTree.deserialize({ layers: [], activeLayerId: '' });
|
|
955
|
+
},
|
|
956
|
+
```
|
|
957
|
+
- Add missing `layerTree` imports: `addLayer`, `deserialize`, `setVisibility`, `setOpacity`
|
|
958
|
+
|
|
959
|
+
- **Step 3: Frame methods**
|
|
960
|
+
- Add to `PluginAPI`:
|
|
961
|
+
```
|
|
962
|
+
addFrame(): unknown; // returns the new frame object
|
|
963
|
+
resetFrames(): void;
|
|
964
|
+
setFrameDuration(index: number, ms: number): void;
|
|
965
|
+
setCurrentFrame(index: number): void;
|
|
966
|
+
setGlobalFps(fps: number): void;
|
|
967
|
+
getFrames(): unknown;
|
|
968
|
+
```
|
|
969
|
+
- Implement using `frameModel` module (import `addFrame`, `deserialize`, `setFrameDuration`, `setCurrentFrameIndex`, `setGlobalFps`, `getFrames` from `frame-model.svelte.js`)
|
|
970
|
+
|
|
971
|
+
- **Step 4: Pixel data methods**
|
|
972
|
+
- Add to `PluginAPI`:
|
|
973
|
+
```
|
|
974
|
+
createPixelBuffer(width: number, height: number): unknown; // returns PixelBuffer
|
|
975
|
+
```
|
|
976
|
+
- Implement:
|
|
977
|
+
```
|
|
978
|
+
createPixelBuffer(width, height) {
|
|
979
|
+
return new PixelBuffer(width, height);
|
|
980
|
+
},
|
|
981
|
+
```
|
|
982
|
+
- Import `PixelBuffer` from `pixel-buffer.ts`
|
|
983
|
+
|
|
984
|
+
- **Step 5: Palette method**
|
|
985
|
+
- Add to `PluginAPI`:
|
|
986
|
+
```
|
|
987
|
+
setProjectPalette(colors: string[]): void;
|
|
988
|
+
```
|
|
989
|
+
- Implement by importing the palette state module and calling its setter
|
|
990
|
+
|
|
991
|
+
- **Step 6: Composite method**
|
|
992
|
+
- Add to `PluginAPI`:
|
|
993
|
+
```
|
|
994
|
+
resetProject(): void;
|
|
995
|
+
```
|
|
996
|
+
- Implement as a convenience that calls `resetLayers()`, `resetFrames()`, and resets canvas to defaults
|
|
997
|
+
|
|
998
|
+
- **Step 7: Migrate importers one at a time**
|
|
999
|
+
- For each importer:
|
|
1000
|
+
- Identify all direct imports of internal modules (`canvasState`, `layerTree`, `frameModel`, `PixelBuffer`)
|
|
1001
|
+
- Replace with `api.*` method calls
|
|
1002
|
+
- The `api` is available as the parameter to `register(api)`; importer functions that are called later (e.g., the `import()` method in `ImporterDefinition`) need access to `api` via closure
|
|
1003
|
+
- Remove the direct imports
|
|
1004
|
+
- Run `tsc --noEmit` after each importer migration
|
|
1005
|
+
|
|
1006
|
+
### Preconditions
|
|
1007
|
+
|
|
1008
|
+
- None strictly required. However, doing this after I-07 (god file split) means the `plugin-api.ts` is cleaner and easier to modify. Also, I-03 (typed CommandContext) should be done first so the `PluginAPI` interface is already updated with `getActiveBuffer`.
|
|
1009
|
+
|
|
1010
|
+
### Postconditions / Verification
|
|
1011
|
+
|
|
1012
|
+
- `tsc --noEmit` passes after each incremental step
|
|
1013
|
+
- Import an Aseprite file: layers, frames, and pixel data appear correctly
|
|
1014
|
+
- Import a Piskel file: same verification
|
|
1015
|
+
- Import a Sky Spec file: same verification
|
|
1016
|
+
- `grep -rn "from.*canvas-state" plugins/builtin/importers/` returns 0 results
|
|
1017
|
+
- `grep -rn "from.*layer-tree" plugins/builtin/importers/` returns 0 results
|
|
1018
|
+
- `grep -rn "from.*frame-model" plugins/builtin/importers/` returns 0 results
|
|
1019
|
+
|
|
1020
|
+
### Risks and Edge Cases
|
|
1021
|
+
|
|
1022
|
+
- **Return types:** The methods return `unknown` to avoid coupling the `PluginAPI` interface to internal implementation types. This is acceptable for now. In the future, define stable return interfaces (e.g., `LayerInfo`, `FrameInfo`) that are part of the public API contract.
|
|
1023
|
+
- **Importer closure pattern:** Importers define their `import()` function inside `register(api)`, capturing `api` in the closure. This is the same pattern used by tools that capture `api` for dispatch. Verify that all 3 importers follow this pattern and have access to `api` inside their import function.
|
|
1024
|
+
- **Incremental shippability:** Each step (canvas, layers, frames, etc.) can be shipped independently. Do not wait for all 6 steps before migrating any importer -- migrate one importer per step if possible.
|
|
1025
|
+
|
|
1026
|
+
---
|
|
1027
|
+
|
|
1028
|
+
## I-10: Enable Strict TypeScript Flags
|
|
1029
|
+
|
|
1030
|
+
### Summary
|
|
1031
|
+
|
|
1032
|
+
`tsconfig.app.json` currently extends `@tsconfig/svelte/tsconfig.json` with `target: "es2023"`,
|
|
1033
|
+
`module: "esnext"`, `allowJs: true`, `checkJs: true`, but does not enable several strict
|
|
1034
|
+
flags. This item enables 5 additional flags in ascending blast-radius order:
|
|
1035
|
+
`noFallthroughCasesInSwitch`, `noUnusedLocals`, `noUnusedParameters`,
|
|
1036
|
+
`exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`. Each flag is added
|
|
1037
|
+
independently with all resulting errors fixed before moving to the next.
|
|
1038
|
+
|
|
1039
|
+
### Files Touched
|
|
1040
|
+
|
|
1041
|
+
| File Path | Action | What Changes |
|
|
1042
|
+
|-----------|--------|--------------|
|
|
1043
|
+
| `tsconfig.app.json` | Modify | Add 5 flags incrementally to `compilerOptions` |
|
|
1044
|
+
| ~7 files | Modify | Fix `noUnusedLocals` / `noUnusedParameters` errors (delete dead imports, prefix unused params with `_`) |
|
|
1045
|
+
| ~11 files | Modify | Fix `exactOptionalPropertyTypes` errors (narrow optional properties before passing, or change types to explicitly include `undefined`) |
|
|
1046
|
+
| ~54 files | Modify | Fix `noUncheckedIndexedAccess` errors (add null checks, non-null assertions in tests, guard clauses) |
|
|
1047
|
+
|
|
1048
|
+
### Step-by-Step Instructions
|
|
1049
|
+
|
|
1050
|
+
- **Step 1: `noFallthroughCasesInSwitch`**
|
|
1051
|
+
- Add to `tsconfig.app.json` compilerOptions: `"noFallthroughCasesInSwitch": true`
|
|
1052
|
+
- Run `tsc --noEmit` -- expected 0 errors (no switch statements with fallthrough in the codebase)
|
|
1053
|
+
- Commit
|
|
1054
|
+
|
|
1055
|
+
- **Step 2: `noUnusedLocals` + `noUnusedParameters`**
|
|
1056
|
+
- Add both flags: `"noUnusedLocals": true, "noUnusedParameters": true`
|
|
1057
|
+
- Run `tsc --noEmit` -- expected ~7 errors
|
|
1058
|
+
- Fix each:
|
|
1059
|
+
- Delete unused imports (e.g., an imported icon that is no longer used)
|
|
1060
|
+
- Prefix unused function parameters with `_` (e.g., `_name` in menu-builder.ts `resolveLabel` at line 46, which already uses `_name`)
|
|
1061
|
+
- Delete unused local variables
|
|
1062
|
+
- Commit
|
|
1063
|
+
|
|
1064
|
+
- **Step 3: `exactOptionalPropertyTypes`**
|
|
1065
|
+
- Add: `"exactOptionalPropertyTypes": true`
|
|
1066
|
+
- Run `tsc --noEmit` -- expected ~11 errors
|
|
1067
|
+
- This flag means `{ x?: number }` does not accept `{ x: undefined }`. Fixes:
|
|
1068
|
+
- Where a value might be `undefined`, change the type to `x?: number | undefined` or narrow before assignment
|
|
1069
|
+
- Common in PanelConfig, ToolbarContribution, MenuContribution where optional fields are sometimes explicitly set to `undefined`
|
|
1070
|
+
- Commit
|
|
1071
|
+
|
|
1072
|
+
- **Step 4: `noUncheckedIndexedAccess`**
|
|
1073
|
+
- Add: `"noUncheckedIndexedAccess": true`
|
|
1074
|
+
- Run `tsc --noEmit` -- expected ~470 errors
|
|
1075
|
+
- This is the highest-impact flag. For each error:
|
|
1076
|
+
- **In test files** (~257 errors): Add non-null assertion `!` where test data is controlled and values are known to exist
|
|
1077
|
+
- **In production code** (~213 errors): Add proper null checks with early returns or fallback values. Categories:
|
|
1078
|
+
- Array index access: add bounds check or use `.at()` with null check
|
|
1079
|
+
- Map `.get()` followed by property access: add undefined check
|
|
1080
|
+
- `ZOOM_STEPS[i]` access: add bounds check (already handled by `zoomStepUp`/`zoomStepDown` logic)
|
|
1081
|
+
- `Object.entries()` destructuring: values are now `T | undefined`
|
|
1082
|
+
- Some of these will surface real latent bugs (array access without bounds checking). Fix those properly.
|
|
1083
|
+
- Commit after all errors fixed
|
|
1084
|
+
|
|
1085
|
+
- **Step 5: Evaluate `noPropertyAccessFromIndexSignature`**
|
|
1086
|
+
- This flag has ~698 errors and forces bracket notation on all index signature access. It is the most invasive and arguably lowest value.
|
|
1087
|
+
- Evaluate whether the benefits justify the churn after all other flags are enabled. If the prior I-03 changes have eliminated most `Record<string, unknown>` usage, the error count may be significantly lower.
|
|
1088
|
+
- If enabled: add to `tsconfig.app.json` and fix all resulting errors by switching to bracket notation where needed
|
|
1089
|
+
- If deferred: document the decision and the remaining error count
|
|
1090
|
+
|
|
1091
|
+
### Preconditions
|
|
1092
|
+
|
|
1093
|
+
- I-03 (Generic Command\<P\>) must be completed first. The generic params eliminate many `Record<string, unknown>` index access patterns that would otherwise generate hundreds of `noUncheckedIndexedAccess` errors.
|
|
1094
|
+
- All prior items should ideally be done so the codebase is stable before the flag-by-flag sweep.
|
|
1095
|
+
|
|
1096
|
+
### Postconditions / Verification
|
|
1097
|
+
|
|
1098
|
+
- `tsc --noEmit` passes with all enabled flags
|
|
1099
|
+
- All tests pass (`npx vitest run`)
|
|
1100
|
+
- No `as any` or `@ts-ignore` comments added to suppress errors (unless documented with justification)
|
|
1101
|
+
- No runtime regressions (all fixes are compile-time type narrowing)
|
|
1102
|
+
- The `noUncheckedIndexedAccess` fixes have surfaced and fixed any real latent bugs (document which bugs were found)
|
|
1103
|
+
|
|
1104
|
+
### Risks and Edge Cases
|
|
1105
|
+
|
|
1106
|
+
- **Blast radius of `noUncheckedIndexedAccess`:** 470 errors across 54 files is a large changeset. Consider doing it in multiple commits: one per directory (e.g., `plugins/builtin/`, `src/lib/core/`, `src/lib/ui/`, etc.).
|
|
1107
|
+
- **Test file noise:** 257 of the 470 errors are in test files. Using `!` assertions in tests is acceptable since test data is controlled, but it should be done thoughtfully (only where the value is truly guaranteed).
|
|
1108
|
+
- **`noPropertyAccessFromIndexSignature` cost-benefit:** 698 errors with minimal type-safety benefit for most cases. Strongly consider deferring this flag or enabling it only for new code via eslint rules instead.
|
|
1109
|
+
- **Interaction with Svelte:** Svelte 5's `$state`, `$derived`, and `$props` runes may interact with strict flags in unexpected ways. Test compilation of `.svelte` files specifically after enabling each flag.
|
|
1110
|
+
- **Third-party types:** The `@tsconfig/svelte` base config and `dockview-core` types may not be compatible with all strict flags. If a third-party type causes errors, use targeted type overrides or `d.ts` augmentations rather than disabling the flag.
|
|
1111
|
+
|
|
1112
|
+
---
|
|
1113
|
+
|
|
1114
|
+
## Internal Consistency Check
|
|
1115
|
+
|
|
1116
|
+
### 1. Dependency Validation
|
|
1117
|
+
|
|
1118
|
+
| Item | Precondition | What Precondition Produces | Verified? |
|
|
1119
|
+
|------|-------------|---------------------------|-----------|
|
|
1120
|
+
| I-01 | None | N/A | Yes |
|
|
1121
|
+
| I-02 | None (ideally after I-01) | I-01 produces `make-stroke-tool.ts` which I-02 will import `makeSnapshotUndo` into | Yes -- I-02's file list includes `make-stroke-tool.ts` as a modification target |
|
|
1122
|
+
| I-03 | I-01 + I-02 (ideally) | I-01 produces the factory, I-02 produces `makeSnapshotUndo`. I-03 updates both to use generic params and separate snapshot. | Yes -- I-03's step 8 explicitly updates `makeSnapshotUndo` |
|
|
1123
|
+
| I-04 | I-03 | I-03 produces typed `CommandDefinition<P>`, `Command<P>`, and updated dispatcher with snapshot support. I-04 needs the typed `Command` for `executeOrDispatch` to construct properly. | Yes -- I-04's `executeOrDispatch` helper constructs a `Command` object using the interface from I-03 |
|
|
1124
|
+
| I-05 | None | N/A | Yes |
|
|
1125
|
+
| I-06 | I-05 | I-05 establishes the `<dialog>` pattern. I-06's `PromptDialog.svelte` uses native `<dialog>`. | Yes -- I-06's step 2 uses `<dialog>` with `.showModal()` |
|
|
1126
|
+
| I-07 | I-04 + I-06 | I-04 provides `undoable` flag so destructive commands are marked before splitting. I-06 provides `dialogState.openPrompt()` so `prompt()` calls are already replaced. | Yes -- I-07 references `undoable: true` on destructive commands and `dialogState.openPrompt()` for resize and frame duration |
|
|
1127
|
+
| I-08 | I-07 | I-07 splits `menu-commands-plugin.ts` into domain files, making it cleaner to replace direct selection-tool imports. | Yes -- I-08 references `edit-commands.ts`, `image-commands.ts`, `select-commands.ts` (products of I-07) |
|
|
1128
|
+
| I-09 | I-03 + I-07 (ideally) | I-03 provides typed `CommandContext` with `getActiveBuffer`. I-07 provides a clean `plugin-api.ts`. | Yes -- I-09 builds on the `PluginAPI` interface already modified by I-03 (adding `getActiveBuffer` to context) and I-08 (adding `getSelection`) |
|
|
1129
|
+
| I-10 | I-03 | I-03 eliminates ~263 type casts, dramatically reducing the error count for strict flags (especially `noUncheckedIndexedAccess` which would flag `Record<string, unknown>` index access). | Yes -- without I-03, `noUncheckedIndexedAccess` would have ~263 more errors from the cast patterns |
|
|
1130
|
+
|
|
1131
|
+
### 2. File Conflict Check
|
|
1132
|
+
|
|
1133
|
+
Files touched by more than one item:
|
|
1134
|
+
|
|
1135
|
+
| File Path | Touched By | Conflict Analysis |
|
|
1136
|
+
|-----------|-----------|-------------------|
|
|
1137
|
+
| `src/lib/core/commands.ts` | I-03 (generic Command, typed CommandContext, UndoEntry.snapshot), I-04 (add `undoable` field) | **Safe.** I-03 runs first and restructures the interfaces. I-04 adds one new field (`undoable?: boolean`) to `CommandDefinition`, which is additive and does not conflict with I-03's generic params. |
|
|
1138
|
+
| `src/lib/core/dispatcher.ts` | I-03 (snapshot capture/pass), I-04 (add `executeOrDispatch` helper) | **Safe.** I-03 modifies `dispatch()`, `undoLast()`, and `redoLast()` internals. I-04 adds a new `executeOrDispatch()` function. No overlap in modified lines. |
|
|
1139
|
+
| `src/lib/core/plugin-types.ts` | I-03 (generic `addCommand`), I-08 (`getSelection`), I-09 (mutator methods) | **Safe.** Each item adds new members to the `PluginAPI` interface. I-03 modifies the `addCommand` signature (line 15) and adds `getActiveBuffer` to `CommandContext` (line 67). I-08 adds `getSelection()`. I-09 adds 15 mutator methods. All additive, no conflicts. Order: I-03 first, then I-08, then I-09. |
|
|
1140
|
+
| `src/lib/core/plugin-api.ts` | I-03 (implement `getActiveBuffer` on context), I-08 (implement `getSelection`), I-09 (implement 15 mutators) | **Safe.** Same analysis as plugin-types.ts -- all additive implementations. |
|
|
1141
|
+
| `src/lib/core/registries.svelte.ts` | I-03 (change to `CommandDefinition<any>`) | Only I-03 touches this. No conflict. |
|
|
1142
|
+
| `plugins/builtin/drawing-utils.ts` | I-02 (add `makeSnapshotUndo`), I-03 (update `makeSnapshotUndo` signature) | **Safe.** I-02 adds the function. I-03 updates its signature (params type, adds snapshot arg). Order is correct: I-02 creates, I-03 updates. |
|
|
1143
|
+
| `plugins/builtin/pencil-tool.ts` | I-01 (reduce to factory call), I-02 (if I-01 not done, replace undo handler), I-03 (add typed params) | **Safe.** I-01 reduces the file to ~20 lines (factory call). I-02 may be absorbed into I-01 if done together. I-03 adds typed params to the factory config. No conflict because I-01's output is a thin wrapper that I-03 updates. |
|
|
1144
|
+
| `plugins/builtin/eraser-tool.ts` | I-01, I-02, I-03 | Same analysis as pencil-tool.ts. |
|
|
1145
|
+
| All 23 plugin files | I-02 (replace undo handlers), I-03 (generic params, remove casts) | **Safe.** I-02 replaces the `undo` function body. I-03 replaces the `execute` and `describe` function signatures and removes `_snapshot` from params. Different lines in the same files. Order is correct: I-02 simplifies undo first, I-03 changes signatures second. |
|
|
1146
|
+
| `src/lib/ui/menu-builder.ts` | I-04 (change `action` routing) | Only I-04 touches the `action` field. No conflict. |
|
|
1147
|
+
| `src/lib/ui/dialog-state.svelte.ts` | I-06 (add prompt state) | Only I-06 touches this. I-05 does not need to modify it (dialog refs are local to components). |
|
|
1148
|
+
| `src/App.svelte` | I-06 (add PromptDialog rendering) | Only I-06 touches this. I-05 converts existing dialog components but does not change App.svelte. |
|
|
1149
|
+
| `src/lib/ui/menu-commands-plugin.ts` | I-04 (mark undoable), I-06 (replace prompt), I-07 (delete) | **Safe if ordered correctly.** I-04 adds `undoable: true` to some commands. I-06 replaces `prompt()` calls. I-07 splits and deletes the file. All three happen in order: I-04 first, I-06 second, I-07 third (which deletes the file). No simultaneous conflict. |
|
|
1150
|
+
| `tsconfig.app.json` | I-10 (add strict flags) | Only I-10 touches this. |
|
|
1151
|
+
| `src/lib/canvas/input-handler.ts` | I-07 (replace zoom imports) | Only I-07 touches this. |
|
|
1152
|
+
|
|
1153
|
+
### 3. Import Chain Validation
|
|
1154
|
+
|
|
1155
|
+
Key import chains after all changes:
|
|
1156
|
+
|
|
1157
|
+
- **Plugin registration chain:**
|
|
1158
|
+
`pencil-tool.ts` -> `make-stroke-tool.ts` -> `drawing-utils.ts` -> `pixel-buffer.ts`
|
|
1159
|
+
`pencil-tool.ts` -> `make-stroke-tool.ts` -> `plugin-loader.ts` -> `plugin-api.ts` -> `plugin-types.ts`
|
|
1160
|
+
No circularity. `plugin-types.ts` does not import from `plugin-api.ts`.
|
|
1161
|
+
|
|
1162
|
+
- **Dispatcher chain:**
|
|
1163
|
+
`dispatcher.ts` -> `commands.ts` (types only)
|
|
1164
|
+
`dispatcher.ts` -> `registries.svelte.ts` -> `commands.ts` (types only)
|
|
1165
|
+
No circularity.
|
|
1166
|
+
|
|
1167
|
+
- **PluginAPI -> selection-tool chain (after I-08):**
|
|
1168
|
+
`plugin-api.ts` -> `selection-tool.ts` -> `plugin-loader.ts` -> `plugin-api.ts`
|
|
1169
|
+
This looks circular but is safe: `plugin-loader.ts` imports only the TYPE `PluginAPI` from `plugin-api.ts` (using `import type`). TypeScript erases type-only imports at compile time, so there is no runtime circular dependency. `selection-tool.ts` imports `PluginModule` (a type) from `plugin-loader.ts`. Verified: `plugin-loader.ts` line 12 uses `import type { PluginAPI }`.
|
|
1170
|
+
|
|
1171
|
+
- **Menu builder -> dispatcher chain (after I-04):**
|
|
1172
|
+
`menu-builder.ts` -> `dispatcher.ts` (for `executeOrDispatch`)
|
|
1173
|
+
`menu-builder.ts` -> `registries.svelte.ts` (already imported, line 10)
|
|
1174
|
+
No new circularity.
|
|
1175
|
+
|
|
1176
|
+
- **Domain command files -> zoom-utils chain (after I-07):**
|
|
1177
|
+
`view-commands.ts` -> `zoom-utils.ts` (new)
|
|
1178
|
+
`input-handler.ts` -> `zoom-utils.ts` (new)
|
|
1179
|
+
No circularity. `zoom-utils.ts` is a pure utility with no imports from the project.
|
|
1180
|
+
|
|
1181
|
+
### 4. Type System Coherence
|
|
1182
|
+
|
|
1183
|
+
After I-03 (Generic Command\<P\>) and I-10 (strict flags):
|
|
1184
|
+
|
|
1185
|
+
- **Generic param flow through dispatch:**
|
|
1186
|
+
- Plugin calls `api.dispatch({ type: 'draw_rect', plugin: '...', version: '...', params: { x: 1, y: 2, ... } })`
|
|
1187
|
+
- `PluginAPI.dispatch` accepts `Omit<Command, 'id' | 'timestamp'>` which after I-03 becomes `Omit<Command<any>, 'id' | 'timestamp'>` since the dispatch API does type erasure at the boundary
|
|
1188
|
+
- `dispatcher.dispatch()` receives `Command` (type-erased to `Command<any>`) and calls `definition.execute(command.params, context)` where `definition` is `CommandDefinition<any>` from the registry
|
|
1189
|
+
- This means: type safety exists at plugin registration time (the `CommandDefinition<DrawRectParams>` enforces that `execute` receives `DrawRectParams`), but is erased at the registry/dispatcher boundary. This is correct and expected.
|
|
1190
|
+
|
|
1191
|
+
- **Snapshot flow through undo stack:**
|
|
1192
|
+
- `execute(params: P, ctx: CommandContext)` returns `unknown | void`
|
|
1193
|
+
- Dispatcher captures return value and stores as `UndoEntry.snapshot: unknown`
|
|
1194
|
+
- `undo(params: P, ctx: CommandContext, snapshot: unknown)` receives it
|
|
1195
|
+
- Each plugin's undo handler casts `snapshot as PixelData[]` (or its specific snapshot type)
|
|
1196
|
+
- This is one `as` cast per undo handler, which is acceptable at the boundary between the generic undo system and the specific plugin data. `makeSnapshotUndo` centralizes this cast.
|
|
1197
|
+
|
|
1198
|
+
- **`noUncheckedIndexedAccess` interaction:**
|
|
1199
|
+
- After I-03, `Command.params` is typed as `P` (not `Record<string, unknown>`), so index access on params is no longer needed. The ~170 `params.field as Type` casts are gone, replaced by typed property access.
|
|
1200
|
+
- Remaining index access patterns (arrays, Maps) will need null checks under `noUncheckedIndexedAccess`. These are independent of the generic Command changes.
|
|
1201
|
+
|
|
1202
|
+
### 5. Plugin System Coherence
|
|
1203
|
+
|
|
1204
|
+
After I-08 (`getSelection`) and I-09 (mutator methods):
|
|
1205
|
+
|
|
1206
|
+
- **Pattern consistency:** The existing `PluginAPI` methods follow two patterns:
|
|
1207
|
+
- Registrars: `addCommand`, `addTool`, `addPanel`, etc. -- take a name + definition, store in registry
|
|
1208
|
+
- Getters: `getCanvas`, `getProject`, `getActiveFrame`, `getActiveLayers` -- return current state
|
|
1209
|
+
|
|
1210
|
+
The new methods add two more patterns:
|
|
1211
|
+
- Query: `getSelection` -- returns a view object with methods (consistent with `getActiveLayers` which returns `{ all, active }`)
|
|
1212
|
+
- Mutators: `setCanvasSize`, `addLayer`, `addFrame`, etc. -- thin wrappers around internal modules
|
|
1213
|
+
|
|
1214
|
+
All patterns are consistent: methods are verb-prefixed (`add`, `get`, `set`, `reset`), take simple parameters, and return simple values or `unknown`.
|
|
1215
|
+
|
|
1216
|
+
- **No overlap between getter return types and mutator inputs:** `getActiveLayers()` returns `unknown` (not a typed layer object). `addLayer(name)` takes a string. There is no type inconsistency between reads and writes because both sides use `unknown` or simple primitives.
|
|
1217
|
+
|
|
1218
|
+
- **`api` instance scoping:** Each plugin receives its own `api` instance from `createPluginAPI(pluginName)`. However, all instances share the same registries and state. The `_pluginName` parameter (line 34 of plugin-api.ts) is currently unused (`eslint-disable-next-line @typescript-eslint/no-unused-vars`). The new methods do not use it either, which is consistent. If namespace scoping is needed later, all methods have access to `_pluginName` via closure.
|
|
1219
|
+
|
|
1220
|
+
### 6. Dead Code Check
|
|
1221
|
+
|
|
1222
|
+
| Item | Potential Dead Code | Resolution |
|
|
1223
|
+
|------|-------------------|------------|
|
|
1224
|
+
| I-01 | The full `register()` bodies in `pencil-tool.ts` and `eraser-tool.ts` are replaced by factory calls. The old code (130+ lines) is deleted, not orphaned. | No dead code. |
|
|
1225
|
+
| I-02 | All 32 inline undo handler bodies are replaced by `makeSnapshotUndo()` calls. The old code is deleted. | No dead code. |
|
|
1226
|
+
| I-03 | The `_snapshot` field in dispatch `params` objects (21+ files) is removed. The old snapshot-in-params convention is fully eliminated. However, verify no external code reads `command.params._snapshot` from the undo stack for display purposes. | Check: does any UI component (e.g., a history panel) read `_snapshot` from `Command.params`? If so, update it to read from `UndoEntry.snapshot`. |
|
|
1227
|
+
| I-03 | The current `CommandContext` empty interface `{ [key: string]: unknown }` (line 67 of commands.ts) is replaced with typed members. If any code relies on adding arbitrary keys to the context, the index signature removal could break it. | Check: `dispatcher.ts` `setContext()` (line 253) uses `Object.assign(context, partial)`. The typed `CommandContext` must still allow extension. Keep the index signature alongside the typed members: `{ getActiveBuffer?: ...; [key: string]: unknown; }` |
|
|
1228
|
+
| I-04 | The `executeOrDispatch` helper is new code, not dead code. The old direct `.execute({}, {})` calls in 4 files are replaced. | No dead code. |
|
|
1229
|
+
| I-05 | The `.modal-overlay` CSS class and the manual Escape handlers are removed from 3 dialog components. Any global `.modal-overlay` styles in `app.css` become dead CSS. | Check: `grep -rn "modal-overlay" src/` after I-05 should return 0 results. Remove any orphaned global styles. |
|
|
1230
|
+
| I-06 | The `window.prompt()` calls are replaced. No dead code created (the replacement is `dialogState.openPrompt()`). | No dead code. |
|
|
1231
|
+
| I-07 | The original `menu-commands-plugin.ts` is deleted. All its exports (`menuCommandsPlugin`) become dead references. | Check: `grep -rn "menuCommandsPlugin" src/` should return 0 after deletion. The `bootstrap.ts` glob discovers modules by file pattern, not by explicit import, so deletion is safe. |
|
|
1232
|
+
| I-07 | The `ZOOM_STEPS`, `zoomStepUp`, `zoomStepDown`, and `viewportCenter` functions are moved to new utility files. The originals in `menu-commands-plugin.ts` are deleted with the file. But duplicates in `input-handler.ts` must also be removed and replaced with imports. | Check: after I-07, `grep -rn "ZOOM_STEPS" src/` should return only `zoom-utils.ts` and its importers. |
|
|
1233
|
+
| I-08 | The direct imports of `selectRect`, `deselectAll`, `hasSelection`, `getSelectedPixels` from `selection-tool.ts` in UI command files are removed. The functions themselves in `selection-tool.ts` remain (they are used by `plugin-api.ts` internally and by the tool's own `register()` function). | No dead code in `selection-tool.ts`. Orphaned imports in consumer files are removed. |
|
|
1234
|
+
| I-09 | No dead code created. New methods are additive. The direct imports in importers are removed and replaced with `api.*` calls. | No dead code. |
|
|
1235
|
+
| I-10 | Unused locals and parameters are deleted (step 2). No new dead code is created by the other flags. | Step 2 specifically eliminates dead code. |
|