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,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selection Tool Plugin -- rectangle and lasso (polygon) selection.
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - Command: `select_rect` (tier: 'frame') -- not pixel-modifying, but recorded for undo
|
|
6
|
+
* - Command: `select_lasso` (tier: 'frame')
|
|
7
|
+
* - Command: `move_selection` (tier: 'frame') -- moves selected pixels
|
|
8
|
+
* - Command: `delete_selection` (tier: 'frame') -- clears selected pixels to transparent
|
|
9
|
+
* - Command: `deselect` (tier: 'frame')
|
|
10
|
+
* - Tool: `rect-select`
|
|
11
|
+
* - Tool: `lasso-select`
|
|
12
|
+
*
|
|
13
|
+
* Selection state is a module-level Set<string> of "x,y" keys.
|
|
14
|
+
* Only operations that modify pixels (move, delete) are true undoable commands.
|
|
15
|
+
* Selecting/deselecting is non-destructive but still recorded for undo convenience.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { PluginModule } from '../../src/lib/core/plugin-loader.js';
|
|
19
|
+
import type { PixelData, Point } from './drawing-utils.js';
|
|
20
|
+
import { snapshotPixels, applyPixels } from './drawing-utils.js';
|
|
21
|
+
import RectSelectIcon from '~icons/lucide/square-dashed';
|
|
22
|
+
import SelectIcon from '~icons/lucide/lasso';
|
|
23
|
+
|
|
24
|
+
// --- Selection state (module-level singleton) ---
|
|
25
|
+
|
|
26
|
+
/** The current set of selected pixel coordinates as "x,y" strings. */
|
|
27
|
+
let selectedPixels = new Set<string>();
|
|
28
|
+
|
|
29
|
+
/** Whether any pixels are currently selected. */
|
|
30
|
+
export function hasSelection(): boolean {
|
|
31
|
+
return selectedPixels.size > 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Get the current selection set (read-only reference). */
|
|
35
|
+
export function getSelectedPixels(): ReadonlySet<string> {
|
|
36
|
+
return selectedPixels;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Check if a specific pixel is selected. */
|
|
40
|
+
export function isSelected(x: number, y: number): boolean {
|
|
41
|
+
return selectedPixels.has(`${String(x)},${String(y)}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Select all pixels inside a rectangle. */
|
|
45
|
+
export function selectRect(x: number, y: number, w: number, h: number): void {
|
|
46
|
+
selectedPixels = new Set<string>();
|
|
47
|
+
for (let py = y; py < y + h; py++) {
|
|
48
|
+
for (let px = x; px < x + w; px++) {
|
|
49
|
+
selectedPixels.add(`${String(px)},${String(py)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Select all pixels inside a polygon defined by the given vertices.
|
|
56
|
+
* Uses the ray-casting algorithm for point-in-polygon testing.
|
|
57
|
+
*/
|
|
58
|
+
export function selectLasso(points: Point[]): void {
|
|
59
|
+
if (points.length < 3) {
|
|
60
|
+
selectedPixels = new Set<string>();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Compute bounding box of the polygon
|
|
65
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
66
|
+
for (const p of points) {
|
|
67
|
+
if (p.x < minX) minX = p.x;
|
|
68
|
+
if (p.y < minY) minY = p.y;
|
|
69
|
+
if (p.x > maxX) maxX = p.x;
|
|
70
|
+
if (p.y > maxY) maxY = p.y;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
selectedPixels = new Set<string>();
|
|
74
|
+
// Test each pixel in the bounding box
|
|
75
|
+
for (let py = Math.floor(minY); py <= Math.ceil(maxY); py++) {
|
|
76
|
+
for (let px = Math.floor(minX); px <= Math.ceil(maxX); px++) {
|
|
77
|
+
if (pointInPolygon(px, py, points)) {
|
|
78
|
+
selectedPixels.add(`${String(px)},${String(py)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Clear the current selection. */
|
|
85
|
+
export function deselect(): void {
|
|
86
|
+
selectedPixels = new Set<string>();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Ray-casting point-in-polygon test.
|
|
91
|
+
* Casts a ray from (px, py) in the +X direction and counts edge crossings.
|
|
92
|
+
* An odd count means the point is inside.
|
|
93
|
+
*/
|
|
94
|
+
export function pointInPolygon(px: number, py: number, polygon: Point[]): boolean {
|
|
95
|
+
let inside = false;
|
|
96
|
+
const n = polygon.length;
|
|
97
|
+
|
|
98
|
+
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
99
|
+
const pi = polygon[i];
|
|
100
|
+
const pj = polygon[j];
|
|
101
|
+
if (!pi || !pj) continue;
|
|
102
|
+
const xi = pi.x, yi = pi.y;
|
|
103
|
+
const xj = pj.x, yj = pj.y;
|
|
104
|
+
|
|
105
|
+
// Check if the ray from (px, py) in +X direction crosses the edge (i, j)
|
|
106
|
+
const intersect =
|
|
107
|
+
((yi > py) !== (yj > py)) &&
|
|
108
|
+
(px < (xj - xi) * (py - yi) / (yj - yi) + xi);
|
|
109
|
+
|
|
110
|
+
if (intersect) inside = !inside;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return inside;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- Plugin ---
|
|
117
|
+
|
|
118
|
+
export const selectionToolPlugin: PluginModule = {
|
|
119
|
+
name: 'builtin/selection',
|
|
120
|
+
version: '1.0.0',
|
|
121
|
+
dependencies: [],
|
|
122
|
+
register(api) {
|
|
123
|
+
// --- Commands ---
|
|
124
|
+
|
|
125
|
+
api.addCommand('select_rect', {
|
|
126
|
+
tier: 'frame',
|
|
127
|
+
|
|
128
|
+
execute(params) {
|
|
129
|
+
// Capture previous selection as snapshot for undo
|
|
130
|
+
const prevSelection = Array.from(selectedPixels);
|
|
131
|
+
selectRect(
|
|
132
|
+
params["x"],
|
|
133
|
+
params["y"],
|
|
134
|
+
params["w"],
|
|
135
|
+
params["h"],
|
|
136
|
+
);
|
|
137
|
+
return prevSelection;
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
undo(_params, _ctx, snapshot) {
|
|
141
|
+
selectedPixels = new Set((snapshot ?? []) as string[]);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
describe(params) {
|
|
145
|
+
return `Selected rect (${String(params["x"])},${String(params["y"])}) ${String(params["w"])}x${String(params["h"])}`;
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
api.addCommand('select_lasso', {
|
|
150
|
+
tier: 'frame',
|
|
151
|
+
|
|
152
|
+
execute(params) {
|
|
153
|
+
const prevSelection = Array.from(selectedPixels);
|
|
154
|
+
selectLasso(params["points"]);
|
|
155
|
+
return prevSelection;
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
undo(_params, _ctx, snapshot) {
|
|
159
|
+
selectedPixels = new Set((snapshot ?? []) as string[]);
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
describe(params) {
|
|
163
|
+
const pts = params["points"];
|
|
164
|
+
return `Selected lasso with ${String(pts.length)} vertices`;
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
api.addCommand('move_selection', {
|
|
169
|
+
tier: 'frame',
|
|
170
|
+
|
|
171
|
+
execute(params, ctx) {
|
|
172
|
+
const buffer = ctx.getActiveBuffer?.();
|
|
173
|
+
if (!buffer) return;
|
|
174
|
+
|
|
175
|
+
const dx = params["dx"];
|
|
176
|
+
const dy = params["dy"];
|
|
177
|
+
|
|
178
|
+
// Build snapshot of all affected pixels (source + destination) before modifying
|
|
179
|
+
const allPoints: Point[] = [];
|
|
180
|
+
for (const key of selectedPixels) {
|
|
181
|
+
const [sx, sy] = key.split(',').map(Number) as [number, number];
|
|
182
|
+
allPoints.push({ x: sx, y: sy });
|
|
183
|
+
allPoints.push({ x: sx + dx, y: sy + dy });
|
|
184
|
+
}
|
|
185
|
+
const snapshot = snapshotPixels(buffer, allPoints);
|
|
186
|
+
|
|
187
|
+
// Read selected pixel colors, clear originals, write at offset
|
|
188
|
+
const pixelColors: { x: number; y: number; r: number; g: number; b: number; a: number }[] = [];
|
|
189
|
+
for (const key of selectedPixels) {
|
|
190
|
+
const [sx, sy] = key.split(',').map(Number) as [number, number];
|
|
191
|
+
const [r, g, b, a] = buffer.getPixel(sx, sy);
|
|
192
|
+
pixelColors.push({ x: sx, y: sy, r, g, b, a });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Clear original positions
|
|
196
|
+
for (const pc of pixelColors) {
|
|
197
|
+
buffer.setPixel(pc.x, pc.y, 0, 0, 0, 0);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Write at new positions
|
|
201
|
+
for (const pc of pixelColors) {
|
|
202
|
+
const nx = pc.x + dx;
|
|
203
|
+
const ny = pc.y + dy;
|
|
204
|
+
if (buffer.inBounds(nx, ny)) {
|
|
205
|
+
buffer.setPixel(nx, ny, pc.r, pc.g, pc.b, pc.a);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Update selection coordinates
|
|
210
|
+
const newSelection = new Set<string>();
|
|
211
|
+
for (const key of selectedPixels) {
|
|
212
|
+
const [sx, sy] = key.split(',').map(Number) as [number, number];
|
|
213
|
+
newSelection.add(`${String(sx + dx)},${String(sy + dy)}`);
|
|
214
|
+
}
|
|
215
|
+
selectedPixels = newSelection;
|
|
216
|
+
|
|
217
|
+
return snapshot;
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
undo(params, ctx, snapshot) {
|
|
221
|
+
const buffer = ctx.getActiveBuffer?.();
|
|
222
|
+
if (!buffer) return;
|
|
223
|
+
|
|
224
|
+
applyPixels(buffer, snapshot as PixelData[]);
|
|
225
|
+
|
|
226
|
+
// Reverse the selection coordinate shift
|
|
227
|
+
const dx = params["dx"];
|
|
228
|
+
const dy = params["dy"];
|
|
229
|
+
const restoredSelection = new Set<string>();
|
|
230
|
+
for (const key of selectedPixels) {
|
|
231
|
+
const [sx, sy] = key.split(',').map(Number) as [number, number];
|
|
232
|
+
restoredSelection.add(`${String(sx - dx)},${String(sy - dy)}`);
|
|
233
|
+
}
|
|
234
|
+
selectedPixels = restoredSelection;
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
describe(params) {
|
|
238
|
+
return `Moved selection by (${String(params["dx"])}, ${String(params["dy"])})`;
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
api.addCommand('delete_selection', {
|
|
243
|
+
tier: 'frame',
|
|
244
|
+
|
|
245
|
+
execute(_params, ctx) {
|
|
246
|
+
const buffer = ctx.getActiveBuffer?.();
|
|
247
|
+
if (!buffer) return;
|
|
248
|
+
|
|
249
|
+
// Snapshot selected pixels for undo
|
|
250
|
+
const points: Point[] = [];
|
|
251
|
+
for (const key of selectedPixels) {
|
|
252
|
+
const [x, y] = key.split(',').map(Number) as [number, number];
|
|
253
|
+
points.push({ x, y });
|
|
254
|
+
}
|
|
255
|
+
const snapshot = snapshotPixels(buffer, points);
|
|
256
|
+
|
|
257
|
+
// Clear selected pixels to transparent
|
|
258
|
+
for (const key of selectedPixels) {
|
|
259
|
+
const [x, y] = key.split(',').map(Number) as [number, number];
|
|
260
|
+
buffer.setPixel(x, y, 0, 0, 0, 0);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return snapshot;
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
undo(_params, ctx, snapshot) {
|
|
267
|
+
const buffer = ctx.getActiveBuffer?.();
|
|
268
|
+
if (!buffer) return;
|
|
269
|
+
|
|
270
|
+
applyPixels(buffer, snapshot as PixelData[]);
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
describe() {
|
|
274
|
+
return `Deleted selected pixels`;
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
api.addCommand('deselect', {
|
|
279
|
+
tier: 'frame',
|
|
280
|
+
|
|
281
|
+
execute() {
|
|
282
|
+
const prevSelection = Array.from(selectedPixels);
|
|
283
|
+
deselect();
|
|
284
|
+
return prevSelection;
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
undo(_params, _ctx, snapshot) {
|
|
288
|
+
selectedPixels = new Set((snapshot ?? []) as string[]);
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
describe() {
|
|
292
|
+
return 'Deselected';
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// --- Toolbar contributions ---
|
|
297
|
+
|
|
298
|
+
api.addToolbarItem('toolbar:drawing-tools:rect-select', {
|
|
299
|
+
toolbarId: 'drawing-tools',
|
|
300
|
+
kind: 'tool',
|
|
301
|
+
targetId: 'rect-select',
|
|
302
|
+
group: 'select',
|
|
303
|
+
order: 10,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
api.addToolbarItem('toolbar:drawing-tools:lasso-select', {
|
|
307
|
+
toolbarId: 'drawing-tools',
|
|
308
|
+
kind: 'tool',
|
|
309
|
+
targetId: 'lasso-select',
|
|
310
|
+
group: 'select',
|
|
311
|
+
order: 20,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// --- Tools ---
|
|
315
|
+
|
|
316
|
+
// Rectangle select tool
|
|
317
|
+
let rectDrawing = false;
|
|
318
|
+
let rectStartX = 0;
|
|
319
|
+
let rectStartY = 0;
|
|
320
|
+
|
|
321
|
+
api.addTool('rect-select', {
|
|
322
|
+
icon: RectSelectIcon,
|
|
323
|
+
cursor: 'crosshair',
|
|
324
|
+
|
|
325
|
+
onPointerDown(_e, ctx) {
|
|
326
|
+
rectDrawing = true;
|
|
327
|
+
rectStartX = ctx.canvasX;
|
|
328
|
+
rectStartY = ctx.canvasY;
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
onPointerMove(_e, _ctx) {
|
|
332
|
+
// Preview handled by canvas viewport
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
onPointerUp(_e, ctx) {
|
|
336
|
+
if (!rectDrawing) return;
|
|
337
|
+
rectDrawing = false;
|
|
338
|
+
|
|
339
|
+
const x = Math.min(rectStartX, ctx.canvasX);
|
|
340
|
+
const y = Math.min(rectStartY, ctx.canvasY);
|
|
341
|
+
const w = Math.abs(ctx.canvasX - rectStartX) + 1;
|
|
342
|
+
const h = Math.abs(ctx.canvasY - rectStartY) + 1;
|
|
343
|
+
|
|
344
|
+
ctx.api.dispatch({
|
|
345
|
+
type: 'select_rect',
|
|
346
|
+
plugin: 'builtin/selection',
|
|
347
|
+
version: '1.0.0',
|
|
348
|
+
params: { x, y, w, h },
|
|
349
|
+
});
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Lasso select tool
|
|
354
|
+
let lassoDrawing = false;
|
|
355
|
+
let lassoPoints: Point[] = [];
|
|
356
|
+
|
|
357
|
+
api.addTool('lasso-select', {
|
|
358
|
+
icon: SelectIcon,
|
|
359
|
+
cursor: 'crosshair',
|
|
360
|
+
|
|
361
|
+
onPointerDown(_e, ctx) {
|
|
362
|
+
lassoDrawing = true;
|
|
363
|
+
lassoPoints = [{ x: ctx.canvasX, y: ctx.canvasY }];
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
onPointerMove(_e, ctx) {
|
|
367
|
+
if (!lassoDrawing) return;
|
|
368
|
+
lassoPoints.push({ x: ctx.canvasX, y: ctx.canvasY });
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
onPointerUp(_e, ctx) {
|
|
372
|
+
if (!lassoDrawing) return;
|
|
373
|
+
lassoDrawing = false;
|
|
374
|
+
|
|
375
|
+
if (lassoPoints.length < 3) return;
|
|
376
|
+
|
|
377
|
+
ctx.api.dispatch({
|
|
378
|
+
type: 'select_lasso',
|
|
379
|
+
plugin: 'builtin/selection',
|
|
380
|
+
version: '1.0.0',
|
|
381
|
+
params: { points: lassoPoints },
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
lassoPoints = [];
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
},
|
|
388
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the selection tool plugin.
|
|
3
|
+
*
|
|
4
|
+
* Covers rect select, lasso select, point-in-polygon,
|
|
5
|
+
* move selection, delete selection, and deselect.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
9
|
+
import { PixelBuffer } from '../../src/lib/canvas/pixel-buffer.js';
|
|
10
|
+
import { commandRegistry } from '../../src/lib/core/registries.svelte.js';
|
|
11
|
+
import { dispatch, undoLast, _resetForTesting as resetDispatcher, setContext } from '../../src/lib/core/dispatcher.js';
|
|
12
|
+
import type { Command } from '../../src/lib/core/commands.js';
|
|
13
|
+
import type { CommandType, ParamsOf } from '../../src/lib/core/command-params.js';
|
|
14
|
+
import { createPluginAPI } from '../../src/lib/core/plugin-api.js';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
selectionToolPlugin,
|
|
18
|
+
selectRect,
|
|
19
|
+
selectLasso,
|
|
20
|
+
deselect,
|
|
21
|
+
isSelected,
|
|
22
|
+
hasSelection,
|
|
23
|
+
getSelectedPixels,
|
|
24
|
+
pointInPolygon,
|
|
25
|
+
} from './selection-tool.js';
|
|
26
|
+
|
|
27
|
+
// --- Helpers ---
|
|
28
|
+
|
|
29
|
+
// Typed overload: validates params when type is a known command literal
|
|
30
|
+
function makeCommand<T extends CommandType>(type: T, params: ParamsOf<T>): Command;
|
|
31
|
+
// String fallback: for dynamic/unknown command types in tests
|
|
32
|
+
function makeCommand(type: string, params?: Record<string, unknown>): Command;
|
|
33
|
+
function makeCommand(type: string, params: Record<string, unknown> = {}): Command {
|
|
34
|
+
return { type, plugin: 'test', version: '1.0.0', params, timestamp: Date.now(), id: crypto.randomUUID() };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setupBuffer(width = 16, height = 16): PixelBuffer {
|
|
38
|
+
const buffer = new PixelBuffer(width, height);
|
|
39
|
+
setContext({ getActiveBuffer: () => buffer });
|
|
40
|
+
return buffer;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- Tests ---
|
|
44
|
+
|
|
45
|
+
describe('Selection Tool Plugin', () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
resetDispatcher();
|
|
48
|
+
deselect();
|
|
49
|
+
const api = createPluginAPI('builtin/selection');
|
|
50
|
+
selectionToolPlugin.register(api);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should register all selection commands', () => {
|
|
54
|
+
expect(commandRegistry.get('select_rect')).toBeDefined();
|
|
55
|
+
expect(commandRegistry.get('select_lasso')).toBeDefined();
|
|
56
|
+
expect(commandRegistry.get('move_selection')).toBeDefined();
|
|
57
|
+
expect(commandRegistry.get('delete_selection')).toBeDefined();
|
|
58
|
+
expect(commandRegistry.get('deselect')).toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('rect select sets correct pixels in selection', () => {
|
|
62
|
+
selectRect(2, 3, 4, 5);
|
|
63
|
+
expect(hasSelection()).toBe(true);
|
|
64
|
+
expect(isSelected(2, 3)).toBe(true);
|
|
65
|
+
expect(isSelected(5, 7)).toBe(true);
|
|
66
|
+
expect(isSelected(1, 3)).toBe(false);
|
|
67
|
+
expect(isSelected(6, 3)).toBe(false);
|
|
68
|
+
expect(getSelectedPixels().size).toBe(20); // 4 * 5
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('lasso select with triangle selects correct interior points', () => {
|
|
72
|
+
// Triangle with vertices at (0,0), (10,0), (5,10)
|
|
73
|
+
const triangle = [{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 5, y: 10 }];
|
|
74
|
+
selectLasso(triangle);
|
|
75
|
+
expect(hasSelection()).toBe(true);
|
|
76
|
+
// Center of the triangle should be selected
|
|
77
|
+
expect(isSelected(5, 3)).toBe(true);
|
|
78
|
+
// Far outside should not be selected
|
|
79
|
+
expect(isSelected(15, 15)).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('point-in-polygon handles boundary and concave shapes', () => {
|
|
83
|
+
// Concave L-shape polygon
|
|
84
|
+
const lShape = [
|
|
85
|
+
{ x: 0, y: 0 },
|
|
86
|
+
{ x: 4, y: 0 },
|
|
87
|
+
{ x: 4, y: 2 },
|
|
88
|
+
{ x: 2, y: 2 },
|
|
89
|
+
{ x: 2, y: 4 },
|
|
90
|
+
{ x: 0, y: 4 },
|
|
91
|
+
];
|
|
92
|
+
// Inside the L
|
|
93
|
+
expect(pointInPolygon(1, 1, lShape)).toBe(true);
|
|
94
|
+
expect(pointInPolygon(1, 3, lShape)).toBe(true);
|
|
95
|
+
// Inside the notch (outside the L)
|
|
96
|
+
expect(pointInPolygon(3, 3, lShape)).toBe(false);
|
|
97
|
+
// Clearly outside
|
|
98
|
+
expect(pointInPolygon(5, 5, lShape)).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('move selection: pixels move and old area becomes transparent', () => {
|
|
102
|
+
const buffer = setupBuffer(16, 16);
|
|
103
|
+
// Paint some pixels
|
|
104
|
+
buffer.setPixel(2, 2, 255, 0, 0, 255);
|
|
105
|
+
buffer.setPixel(3, 2, 0, 255, 0, 255);
|
|
106
|
+
selectRect(2, 2, 2, 1);
|
|
107
|
+
|
|
108
|
+
dispatch(makeCommand('move_selection', { dx: 5, dy: 3 }));
|
|
109
|
+
|
|
110
|
+
// Old positions should be transparent
|
|
111
|
+
expect(buffer.getPixel(2, 2)).toEqual([0, 0, 0, 0]);
|
|
112
|
+
expect(buffer.getPixel(3, 2)).toEqual([0, 0, 0, 0]);
|
|
113
|
+
// New positions should have the colors
|
|
114
|
+
expect(buffer.getPixel(7, 5)).toEqual([255, 0, 0, 255]);
|
|
115
|
+
expect(buffer.getPixel(8, 5)).toEqual([0, 255, 0, 255]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('delete selection: selected pixels become transparent', () => {
|
|
119
|
+
const buffer = setupBuffer(8, 8);
|
|
120
|
+
buffer.fill(255, 0, 0, 255);
|
|
121
|
+
selectRect(1, 1, 3, 3);
|
|
122
|
+
|
|
123
|
+
dispatch(makeCommand('delete_selection', {}));
|
|
124
|
+
|
|
125
|
+
// Selected region should be transparent
|
|
126
|
+
expect(buffer.getPixel(1, 1)).toEqual([0, 0, 0, 0]);
|
|
127
|
+
expect(buffer.getPixel(2, 2)).toEqual([0, 0, 0, 0]);
|
|
128
|
+
expect(buffer.getPixel(3, 3)).toEqual([0, 0, 0, 0]);
|
|
129
|
+
// Outside selection should be unchanged
|
|
130
|
+
expect(buffer.getPixel(0, 0)).toEqual([255, 0, 0, 255]);
|
|
131
|
+
expect(buffer.getPixel(4, 4)).toEqual([255, 0, 0, 255]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('deselect clears the selection', () => {
|
|
135
|
+
selectRect(0, 0, 5, 5);
|
|
136
|
+
expect(hasSelection()).toBe(true);
|
|
137
|
+
deselect();
|
|
138
|
+
expect(hasSelection()).toBe(false);
|
|
139
|
+
expect(getSelectedPixels().size).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('undo delete_selection restores pixels', () => {
|
|
143
|
+
const buffer = setupBuffer(8, 8);
|
|
144
|
+
buffer.fill(100, 200, 50, 255);
|
|
145
|
+
selectRect(2, 2, 2, 2);
|
|
146
|
+
|
|
147
|
+
dispatch(makeCommand('delete_selection', {}));
|
|
148
|
+
expect(buffer.getPixel(2, 2)).toEqual([0, 0, 0, 0]);
|
|
149
|
+
|
|
150
|
+
undoLast();
|
|
151
|
+
expect(buffer.getPixel(2, 2)).toEqual([100, 200, 50, 255]);
|
|
152
|
+
expect(buffer.getPixel(3, 3)).toEqual([100, 200, 50, 255]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('undo move_selection restores pixels to original positions', () => {
|
|
156
|
+
const buffer = setupBuffer(16, 16);
|
|
157
|
+
buffer.setPixel(4, 4, 255, 128, 0, 255);
|
|
158
|
+
selectRect(4, 4, 1, 1);
|
|
159
|
+
|
|
160
|
+
dispatch(makeCommand('move_selection', { dx: 3, dy: 2 }));
|
|
161
|
+
expect(buffer.getPixel(4, 4)).toEqual([0, 0, 0, 0]);
|
|
162
|
+
expect(buffer.getPixel(7, 6)).toEqual([255, 128, 0, 255]);
|
|
163
|
+
|
|
164
|
+
undoLast();
|
|
165
|
+
expect(buffer.getPixel(4, 4)).toEqual([255, 128, 0, 255]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('point-in-polygon correctly handles a square', () => {
|
|
169
|
+
const square = [
|
|
170
|
+
{ x: 0, y: 0 },
|
|
171
|
+
{ x: 10, y: 0 },
|
|
172
|
+
{ x: 10, y: 10 },
|
|
173
|
+
{ x: 0, y: 10 },
|
|
174
|
+
];
|
|
175
|
+
expect(pointInPolygon(5, 5, square)).toBe(true);
|
|
176
|
+
expect(pointInPolygon(-1, 5, square)).toBe(false);
|
|
177
|
+
expect(pointInPolygon(11, 5, square)).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('select_rect command via dispatch', () => {
|
|
181
|
+
setupBuffer();
|
|
182
|
+
dispatch(makeCommand('select_rect', { x: 1, y: 1, w: 3, h: 3 }));
|
|
183
|
+
expect(isSelected(1, 1)).toBe(true);
|
|
184
|
+
expect(isSelected(3, 3)).toBe(true);
|
|
185
|
+
expect(getSelectedPixels().size).toBe(9);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('select_lasso command via dispatch', () => {
|
|
189
|
+
setupBuffer();
|
|
190
|
+
const triangle = [{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 5, y: 10 }];
|
|
191
|
+
dispatch(makeCommand('select_lasso', { points: triangle }));
|
|
192
|
+
expect(hasSelection()).toBe(true);
|
|
193
|
+
expect(isSelected(5, 3)).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
});
|