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,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for drawing utility functions.
|
|
3
|
+
*
|
|
4
|
+
* Covers line algorithms, rectangle/ellipse/diamond primitives,
|
|
5
|
+
* flood fill, and snapshot/apply helpers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { PixelBuffer } from '../../src/lib/canvas/pixel-buffer.js';
|
|
10
|
+
import {
|
|
11
|
+
bresenhamLine,
|
|
12
|
+
bresenhamLineNoDoubles,
|
|
13
|
+
rectOutline,
|
|
14
|
+
rectFilled,
|
|
15
|
+
ellipseOutline,
|
|
16
|
+
ellipseFilled,
|
|
17
|
+
diamondOutline,
|
|
18
|
+
diamondFilled,
|
|
19
|
+
floodFill,
|
|
20
|
+
snapshotPixels,
|
|
21
|
+
applyPixels,
|
|
22
|
+
hexToRgba,
|
|
23
|
+
} from './drawing-utils.js';
|
|
24
|
+
|
|
25
|
+
// --- Helpers ---
|
|
26
|
+
|
|
27
|
+
/** Convert point array to a set of "x,y" strings for easy lookup. */
|
|
28
|
+
function toSet(points: { x: number; y: number }[]): Set<string> {
|
|
29
|
+
return new Set(points.map((p) => `${String(p.x)},${String(p.y)}`));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Check that a point list has no duplicate coordinates. */
|
|
33
|
+
function hasNoDuplicates(points: { x: number; y: number }[]): boolean {
|
|
34
|
+
return toSet(points).size === points.length;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- Bresenham Line ---
|
|
38
|
+
|
|
39
|
+
describe('bresenhamLine', () => {
|
|
40
|
+
it('should return a single point for same start and end', () => {
|
|
41
|
+
const points = bresenhamLine(5, 5, 5, 5);
|
|
42
|
+
expect(points).toEqual([{ x: 5, y: 5 }]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should draw a horizontal line', () => {
|
|
46
|
+
const points = bresenhamLine(0, 0, 4, 0);
|
|
47
|
+
expect(points).toHaveLength(5);
|
|
48
|
+
for (let x = 0; x <= 4; x++) {
|
|
49
|
+
expect(points[x]).toEqual({ x, y: 0 });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should draw a vertical line', () => {
|
|
54
|
+
const points = bresenhamLine(2, 0, 2, 3);
|
|
55
|
+
expect(points).toHaveLength(4);
|
|
56
|
+
for (let y = 0; y <= 3; y++) {
|
|
57
|
+
expect(points[y]).toEqual({ x: 2, y });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should draw a 45-degree diagonal', () => {
|
|
62
|
+
const points = bresenhamLine(0, 0, 3, 3);
|
|
63
|
+
expect(points).toHaveLength(4);
|
|
64
|
+
for (let i = 0; i <= 3; i++) {
|
|
65
|
+
expect(points[i]).toEqual({ x: i, y: i });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should draw a line in reverse direction', () => {
|
|
70
|
+
const forward = bresenhamLine(0, 0, 5, 3);
|
|
71
|
+
const reverse = bresenhamLine(5, 3, 0, 0);
|
|
72
|
+
// Both should cover the same set of points
|
|
73
|
+
expect(toSet(forward)).toEqual(toSet(reverse));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should draw a steep line (dy > dx)', () => {
|
|
77
|
+
const points = bresenhamLine(0, 0, 2, 6);
|
|
78
|
+
expect(points).toHaveLength(7);
|
|
79
|
+
// Should start at origin and end at (2,6)
|
|
80
|
+
expect(points[0]).toEqual({ x: 0, y: 0 });
|
|
81
|
+
expect(points[points.length - 1]).toEqual({ x: 2, y: 6 });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should include both endpoints', () => {
|
|
85
|
+
const points = bresenhamLine(1, 2, 7, 5);
|
|
86
|
+
const set = toSet(points);
|
|
87
|
+
expect(set.has('1,2')).toBe(true);
|
|
88
|
+
expect(set.has('7,5')).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// --- No-Doubles Bresenham ---
|
|
93
|
+
|
|
94
|
+
describe('bresenhamLineNoDoubles', () => {
|
|
95
|
+
it('should return a single point for same start and end', () => {
|
|
96
|
+
const points = bresenhamLineNoDoubles(3, 3, 3, 3);
|
|
97
|
+
expect(points).toEqual([{ x: 3, y: 3 }]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should produce the same result as standard for horizontal line', () => {
|
|
101
|
+
const standard = bresenhamLine(0, 0, 5, 0);
|
|
102
|
+
const noDoubles = bresenhamLineNoDoubles(0, 0, 5, 0);
|
|
103
|
+
expect(toSet(noDoubles)).toEqual(toSet(standard));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should produce the same result as standard for vertical line', () => {
|
|
107
|
+
const standard = bresenhamLine(0, 0, 0, 5);
|
|
108
|
+
const noDoubles = bresenhamLineNoDoubles(0, 0, 0, 5);
|
|
109
|
+
expect(toSet(noDoubles)).toEqual(toSet(standard));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should produce the same result as standard for diagonal line', () => {
|
|
113
|
+
const standard = bresenhamLine(0, 0, 4, 4);
|
|
114
|
+
const noDoubles = bresenhamLineNoDoubles(0, 0, 4, 4);
|
|
115
|
+
expect(toSet(noDoubles)).toEqual(toSet(standard));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should never have two consecutive pixels differing only on minor axis', () => {
|
|
119
|
+
// Test with a shallow slope where doubles are likely
|
|
120
|
+
const points = bresenhamLineNoDoubles(0, 0, 10, 3);
|
|
121
|
+
for (let i = 1; i < points.length; i++) {
|
|
122
|
+
const prev = points[i - 1];
|
|
123
|
+
const curr = points[i];
|
|
124
|
+
if (!prev || !curr) continue;
|
|
125
|
+
// Two consecutive pixels should not have same x but different y
|
|
126
|
+
// (that would be a "double" on the minor axis for a shallow slope)
|
|
127
|
+
const sameX = prev.x === curr.x;
|
|
128
|
+
const sameY = prev.y === curr.y;
|
|
129
|
+
// At least one axis must change; if x is the major axis and stays same,
|
|
130
|
+
// that's a double
|
|
131
|
+
expect(sameX && sameY).toBe(false);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should produce fewer or equal points than standard for non-axis-aligned lines', () => {
|
|
136
|
+
const standard = bresenhamLine(0, 0, 10, 4);
|
|
137
|
+
const noDoubles = bresenhamLineNoDoubles(0, 0, 10, 4);
|
|
138
|
+
expect(noDoubles.length).toBeLessThanOrEqual(standard.length);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should include both endpoints', () => {
|
|
142
|
+
const points = bresenhamLineNoDoubles(1, 2, 8, 5);
|
|
143
|
+
const set = toSet(points);
|
|
144
|
+
expect(set.has('1,2')).toBe(true);
|
|
145
|
+
expect(set.has('8,5')).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// --- Rectangle Outline ---
|
|
150
|
+
|
|
151
|
+
describe('rectOutline', () => {
|
|
152
|
+
it('should return empty for zero-sized rect', () => {
|
|
153
|
+
expect(rectOutline(0, 0, 0, 0)).toEqual([]);
|
|
154
|
+
expect(rectOutline(0, 0, 0, 5)).toEqual([]);
|
|
155
|
+
expect(rectOutline(0, 0, 5, 0)).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should return single point for 1x1 rect', () => {
|
|
159
|
+
expect(rectOutline(3, 4, 1, 1)).toEqual([{ x: 3, y: 4 }]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should return correct perimeter for a 3x3 rect', () => {
|
|
163
|
+
const points = rectOutline(0, 0, 3, 3);
|
|
164
|
+
// 3x3 rect has 8 perimeter points (3*4 - 4 corners counted once)
|
|
165
|
+
expect(points).toHaveLength(8);
|
|
166
|
+
expect(hasNoDuplicates(points)).toBe(true);
|
|
167
|
+
// Center should NOT be included (that's interior)
|
|
168
|
+
expect(toSet(points).has('1,1')).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should return correct perimeter for a 4x2 rect', () => {
|
|
172
|
+
const points = rectOutline(0, 0, 4, 2);
|
|
173
|
+
// Top: (0,0)(1,0)(2,0)(3,0), Bottom: (3,1)(2,1)(1,1)(0,1) = 8 points
|
|
174
|
+
expect(points).toHaveLength(8);
|
|
175
|
+
expect(hasNoDuplicates(points)).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should handle 1-wide rect (vertical line)', () => {
|
|
179
|
+
const points = rectOutline(5, 2, 1, 4);
|
|
180
|
+
expect(points).toHaveLength(4);
|
|
181
|
+
for (let y = 2; y <= 5; y++) {
|
|
182
|
+
expect(toSet(points).has(`5,${String(y)}`)).toBe(true);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should handle 1-tall rect (horizontal line)', () => {
|
|
187
|
+
const points = rectOutline(0, 3, 5, 1);
|
|
188
|
+
expect(points).toHaveLength(5);
|
|
189
|
+
for (let x = 0; x <= 4; x++) {
|
|
190
|
+
expect(toSet(points).has(`${String(x)},3`)).toBe(true);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// --- Filled Rectangle ---
|
|
196
|
+
|
|
197
|
+
describe('rectFilled', () => {
|
|
198
|
+
it('should return empty for zero-sized rect', () => {
|
|
199
|
+
expect(rectFilled(0, 0, 0, 0)).toEqual([]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should return correct area for a 3x2 rect', () => {
|
|
203
|
+
const points = rectFilled(1, 1, 3, 2);
|
|
204
|
+
expect(points).toHaveLength(6);
|
|
205
|
+
expect(hasNoDuplicates(points)).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should contain all interior and perimeter points', () => {
|
|
209
|
+
const points = rectFilled(0, 0, 4, 3);
|
|
210
|
+
expect(points).toHaveLength(12);
|
|
211
|
+
const set = toSet(points);
|
|
212
|
+
for (let y = 0; y < 3; y++) {
|
|
213
|
+
for (let x = 0; x < 4; x++) {
|
|
214
|
+
expect(set.has(`${String(x)},${String(y)}`)).toBe(true);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// --- Ellipse Outline ---
|
|
221
|
+
|
|
222
|
+
describe('ellipseOutline', () => {
|
|
223
|
+
it('should return single point for radius 0,0', () => {
|
|
224
|
+
const points = ellipseOutline(5, 5, 0, 0);
|
|
225
|
+
expect(points).toEqual([{ x: 5, y: 5 }]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should return a vertical line for rx=0', () => {
|
|
229
|
+
const points = ellipseOutline(5, 5, 0, 3);
|
|
230
|
+
expect(points).toHaveLength(7);
|
|
231
|
+
const set = toSet(points);
|
|
232
|
+
for (let y = 2; y <= 8; y++) {
|
|
233
|
+
expect(set.has(`5,${String(y)}`)).toBe(true);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should return a horizontal line for ry=0', () => {
|
|
238
|
+
const points = ellipseOutline(5, 5, 3, 0);
|
|
239
|
+
expect(points).toHaveLength(7);
|
|
240
|
+
const set = toSet(points);
|
|
241
|
+
for (let x = 2; x <= 8; x++) {
|
|
242
|
+
expect(set.has(`${String(x)},5`)).toBe(true);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should produce an approximately circular shape for equal radii', () => {
|
|
247
|
+
const r = 5;
|
|
248
|
+
const points = ellipseOutline(10, 10, r, r);
|
|
249
|
+
expect(points.length).toBeGreaterThan(0);
|
|
250
|
+
expect(hasNoDuplicates(points)).toBe(true);
|
|
251
|
+
|
|
252
|
+
// All points should be approximately distance r from center
|
|
253
|
+
for (const p of points) {
|
|
254
|
+
const dist = Math.sqrt((p.x - 10) ** 2 + (p.y - 10) ** 2);
|
|
255
|
+
// Allow some tolerance for pixel grid rasterization
|
|
256
|
+
expect(dist).toBeGreaterThan(r - 1.5);
|
|
257
|
+
expect(dist).toBeLessThan(r + 1.5);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should be symmetric around center', () => {
|
|
262
|
+
const points = ellipseOutline(10, 10, 4, 3);
|
|
263
|
+
const set = toSet(points);
|
|
264
|
+
// For every point (cx+dx, cy+dy), the mirrors should also exist
|
|
265
|
+
for (const p of points) {
|
|
266
|
+
const dx = p.x - 10;
|
|
267
|
+
const dy = p.y - 10;
|
|
268
|
+
expect(set.has(`${String(10 - dx)},${String(10 + dy)}`)).toBe(true);
|
|
269
|
+
expect(set.has(`${String(10 + dx)},${String(10 - dy)}`)).toBe(true);
|
|
270
|
+
expect(set.has(`${String(10 - dx)},${String(10 - dy)}`)).toBe(true);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should include cardinal extreme points', () => {
|
|
275
|
+
const points = ellipseOutline(10, 10, 5, 3);
|
|
276
|
+
const set = toSet(points);
|
|
277
|
+
expect(set.has('15,10')).toBe(true); // right
|
|
278
|
+
expect(set.has('5,10')).toBe(true); // left
|
|
279
|
+
expect(set.has('10,13')).toBe(true); // bottom
|
|
280
|
+
expect(set.has('10,7')).toBe(true); // top
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// --- Filled Ellipse ---
|
|
285
|
+
|
|
286
|
+
describe('ellipseFilled', () => {
|
|
287
|
+
it('should return single point for radius 0,0', () => {
|
|
288
|
+
const points = ellipseFilled(5, 5, 0, 0);
|
|
289
|
+
expect(points).toEqual([{ x: 5, y: 5 }]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should fill more points than outline', () => {
|
|
293
|
+
const outline = ellipseOutline(10, 10, 4, 3);
|
|
294
|
+
const filled = ellipseFilled(10, 10, 4, 3);
|
|
295
|
+
expect(filled.length).toBeGreaterThan(outline.length);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should include the center point', () => {
|
|
299
|
+
const points = ellipseFilled(10, 10, 3, 3);
|
|
300
|
+
expect(toSet(points).has('10,10')).toBe(true);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should contain all outline points', () => {
|
|
304
|
+
const outline = ellipseOutline(10, 10, 4, 3);
|
|
305
|
+
const filledSet = toSet(ellipseFilled(10, 10, 4, 3));
|
|
306
|
+
for (const p of outline) {
|
|
307
|
+
expect(filledSet.has(`${String(p.x)},${String(p.y)}`)).toBe(true);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// --- Diamond Outline ---
|
|
313
|
+
|
|
314
|
+
describe('diamondOutline', () => {
|
|
315
|
+
it('should return single point for radius 0,0', () => {
|
|
316
|
+
const points = diamondOutline(5, 5, 0, 0);
|
|
317
|
+
expect(points).toEqual([{ x: 5, y: 5 }]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should have no duplicate points', () => {
|
|
321
|
+
const points = diamondOutline(10, 10, 5, 5);
|
|
322
|
+
expect(hasNoDuplicates(points)).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should include the four vertices', () => {
|
|
326
|
+
const cx = 10, cy = 10, rx = 4, ry = 3;
|
|
327
|
+
const set = toSet(diamondOutline(cx, cy, rx, ry));
|
|
328
|
+
expect(set.has(`${String(cx)},${String(cy - ry)}`)).toBe(true); // top
|
|
329
|
+
expect(set.has(`${String(cx + rx)},${String(cy)}`)).toBe(true); // right
|
|
330
|
+
expect(set.has(`${String(cx)},${String(cy + ry)}`)).toBe(true); // bottom
|
|
331
|
+
expect(set.has(`${String(cx - rx)},${String(cy)}`)).toBe(true); // left
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should be symmetric', () => {
|
|
335
|
+
const cx = 10, cy = 10;
|
|
336
|
+
const points = diamondOutline(cx, cy, 5, 3);
|
|
337
|
+
const set = toSet(points);
|
|
338
|
+
for (const p of points) {
|
|
339
|
+
const dx = p.x - cx;
|
|
340
|
+
const dy = p.y - cy;
|
|
341
|
+
expect(set.has(`${String(cx - dx)},${String(cy + dy)}`)).toBe(true);
|
|
342
|
+
expect(set.has(`${String(cx + dx)},${String(cy - dy)}`)).toBe(true);
|
|
343
|
+
expect(set.has(`${String(cx - dx)},${String(cy - dy)}`)).toBe(true);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// --- Diamond Filled ---
|
|
349
|
+
|
|
350
|
+
describe('diamondFilled', () => {
|
|
351
|
+
it('should return single point for radius 0,0', () => {
|
|
352
|
+
const points = diamondFilled(5, 5, 0, 0);
|
|
353
|
+
expect(points).toEqual([{ x: 5, y: 5 }]);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should fill more points than outline', () => {
|
|
357
|
+
const outline = diamondOutline(10, 10, 5, 5);
|
|
358
|
+
const filled = diamondFilled(10, 10, 5, 5);
|
|
359
|
+
expect(filled.length).toBeGreaterThanOrEqual(outline.length);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should include the center', () => {
|
|
363
|
+
const set = toSet(diamondFilled(10, 10, 4, 3));
|
|
364
|
+
expect(set.has('10,10')).toBe(true);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// --- Flood Fill ---
|
|
369
|
+
|
|
370
|
+
describe('floodFill', () => {
|
|
371
|
+
it('should return empty for out-of-bounds start', () => {
|
|
372
|
+
const buf = new PixelBuffer(4, 4);
|
|
373
|
+
expect(floodFill(buf, -1, 0)).toEqual([]);
|
|
374
|
+
expect(floodFill(buf, 4, 0)).toEqual([]);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should fill entire buffer when all pixels are the same color', () => {
|
|
378
|
+
const buf = new PixelBuffer(4, 3);
|
|
379
|
+
// All transparent (default)
|
|
380
|
+
const points = floodFill(buf, 0, 0);
|
|
381
|
+
expect(points).toHaveLength(12);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should stop at color boundaries', () => {
|
|
385
|
+
const buf = new PixelBuffer(5, 5);
|
|
386
|
+
// Draw a vertical wall at x=2
|
|
387
|
+
for (let y = 0; y < 5; y++) {
|
|
388
|
+
buf.setPixel(2, y, 255, 0, 0, 255);
|
|
389
|
+
}
|
|
390
|
+
// Fill from (0,0) should only reach x=0 and x=1 (5 rows * 2 cols = 10)
|
|
391
|
+
const points = floodFill(buf, 0, 0);
|
|
392
|
+
expect(points).toHaveLength(10);
|
|
393
|
+
// All points should have x < 2
|
|
394
|
+
for (const p of points) {
|
|
395
|
+
expect(p.x).toBeLessThan(2);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should fill a bounded region', () => {
|
|
400
|
+
const buf = new PixelBuffer(6, 6);
|
|
401
|
+
// Create a box with walls
|
|
402
|
+
for (let i = 0; i < 6; i++) {
|
|
403
|
+
buf.setPixel(i, 0, 255, 0, 0, 255); // top wall
|
|
404
|
+
buf.setPixel(i, 5, 255, 0, 0, 255); // bottom wall
|
|
405
|
+
buf.setPixel(0, i, 255, 0, 0, 255); // left wall
|
|
406
|
+
buf.setPixel(5, i, 255, 0, 0, 255); // right wall
|
|
407
|
+
}
|
|
408
|
+
// Fill from center (3,3), should fill interior: 4x4 = 16 transparent pixels
|
|
409
|
+
const points = floodFill(buf, 3, 3);
|
|
410
|
+
expect(points).toHaveLength(16);
|
|
411
|
+
for (const p of points) {
|
|
412
|
+
expect(p.x).toBeGreaterThanOrEqual(1);
|
|
413
|
+
expect(p.x).toBeLessThanOrEqual(4);
|
|
414
|
+
expect(p.y).toBeGreaterThanOrEqual(1);
|
|
415
|
+
expect(p.y).toBeLessThanOrEqual(4);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should return single pixel if it is surrounded by different colors', () => {
|
|
420
|
+
const buf = new PixelBuffer(3, 3);
|
|
421
|
+
buf.fill(255, 0, 0, 255); // Fill all red
|
|
422
|
+
buf.setPixel(1, 1, 0, 0, 0, 0); // Center is transparent
|
|
423
|
+
const points = floodFill(buf, 1, 1);
|
|
424
|
+
expect(points).toHaveLength(1);
|
|
425
|
+
expect(points[0]).toEqual({ x: 1, y: 1 });
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('should handle L-shaped region', () => {
|
|
429
|
+
const buf = new PixelBuffer(4, 4);
|
|
430
|
+
buf.fill(255, 0, 0, 255);
|
|
431
|
+
// Create an L-shaped transparent region
|
|
432
|
+
buf.setPixel(0, 0, 0, 0, 0, 0);
|
|
433
|
+
buf.setPixel(0, 1, 0, 0, 0, 0);
|
|
434
|
+
buf.setPixel(0, 2, 0, 0, 0, 0);
|
|
435
|
+
buf.setPixel(1, 2, 0, 0, 0, 0);
|
|
436
|
+
|
|
437
|
+
const points = floodFill(buf, 0, 0);
|
|
438
|
+
expect(points).toHaveLength(4);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// --- snapshotPixels / applyPixels roundtrip ---
|
|
443
|
+
|
|
444
|
+
describe('snapshotPixels and applyPixels', () => {
|
|
445
|
+
it('should roundtrip pixel data correctly', () => {
|
|
446
|
+
const buf = new PixelBuffer(4, 4);
|
|
447
|
+
buf.setPixel(1, 1, 255, 0, 0, 255);
|
|
448
|
+
buf.setPixel(2, 2, 0, 255, 0, 128);
|
|
449
|
+
buf.setPixel(3, 3, 0, 0, 255, 64);
|
|
450
|
+
|
|
451
|
+
const points = [{ x: 1, y: 1 }, { x: 2, y: 2 }, { x: 3, y: 3 }];
|
|
452
|
+
const snapshot = snapshotPixels(buf, points);
|
|
453
|
+
|
|
454
|
+
// Overwrite the pixels
|
|
455
|
+
buf.setPixel(1, 1, 0, 0, 0, 0);
|
|
456
|
+
buf.setPixel(2, 2, 0, 0, 0, 0);
|
|
457
|
+
buf.setPixel(3, 3, 0, 0, 0, 0);
|
|
458
|
+
|
|
459
|
+
// Restore from snapshot
|
|
460
|
+
applyPixels(buf, snapshot);
|
|
461
|
+
|
|
462
|
+
expect(buf.getPixel(1, 1)).toEqual([255, 0, 0, 255]);
|
|
463
|
+
expect(buf.getPixel(2, 2)).toEqual([0, 255, 0, 128]);
|
|
464
|
+
expect(buf.getPixel(3, 3)).toEqual([0, 0, 255, 64]);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should snapshot out-of-bounds points as [0,0,0,0]', () => {
|
|
468
|
+
const buf = new PixelBuffer(2, 2);
|
|
469
|
+
buf.fill(255, 255, 255, 255);
|
|
470
|
+
|
|
471
|
+
const snapshot = snapshotPixels(buf, [{ x: -1, y: 0 }, { x: 10, y: 10 }]);
|
|
472
|
+
expect(snapshot[0]).toEqual({ x: -1, y: 0, r: 0, g: 0, b: 0, a: 0 });
|
|
473
|
+
expect(snapshot[1]).toEqual({ x: 10, y: 10, r: 0, g: 0, b: 0, a: 0 });
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// --- hexToRgba ---
|
|
478
|
+
|
|
479
|
+
describe('hexToRgba', () => {
|
|
480
|
+
it('should parse black', () => {
|
|
481
|
+
expect(hexToRgba('#000000')).toEqual({ r: 0, g: 0, b: 0, a: 255 });
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should parse white', () => {
|
|
485
|
+
expect(hexToRgba('#FFFFFF')).toEqual({ r: 255, g: 255, b: 255, a: 255 });
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('should parse red', () => {
|
|
489
|
+
expect(hexToRgba('#FF0000')).toEqual({ r: 255, g: 0, b: 0, a: 255 });
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('should parse arbitrary color', () => {
|
|
493
|
+
expect(hexToRgba('#1A2B3C')).toEqual({ r: 26, g: 43, b: 60, a: 255 });
|
|
494
|
+
});
|
|
495
|
+
});
|