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,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Command Replay Engine.
|
|
3
|
+
*
|
|
4
|
+
* Registers test commands that set pixels on a PixelBuffer, then exercises
|
|
5
|
+
* the replay engine's core operations: replay, reorder, toggle, and
|
|
6
|
+
* parameter editing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
10
|
+
import type { CommandDefinition, CommandContext } from '../core/commands.js';
|
|
11
|
+
import { commandRegistry } from '../core/registries.svelte.js';
|
|
12
|
+
import { PixelBuffer } from '../canvas/pixel-buffer.js';
|
|
13
|
+
import { replayCommands, previewReorder } from './replay-engine.js';
|
|
14
|
+
import type { ActionLogEntry } from './action-log.svelte.js';
|
|
15
|
+
|
|
16
|
+
// --- Test command: sets a single pixel ---
|
|
17
|
+
|
|
18
|
+
function registerSetPixelCommand(): void {
|
|
19
|
+
const def: CommandDefinition = {
|
|
20
|
+
execute(params, ctx: CommandContext) {
|
|
21
|
+
// CommandContext's index signature means ctx["pixelBuffer"] is
|
|
22
|
+
// typed as unknown but may actually be undefined at runtime if
|
|
23
|
+
// the context was built without one, hence the guard.
|
|
24
|
+
const buf = ctx["pixelBuffer"] as PixelBuffer | undefined;
|
|
25
|
+
if (!buf) return;
|
|
26
|
+
const x = params["x"] as number;
|
|
27
|
+
const y = params["y"] as number;
|
|
28
|
+
const r = (params["r"] ?? 255) as number;
|
|
29
|
+
const g = (params["g"] ?? 0) as number;
|
|
30
|
+
const b = (params["b"] ?? 0) as number;
|
|
31
|
+
const a = (params["a"] ?? 255) as number;
|
|
32
|
+
buf.setPixel(x, y, r, g, b, a);
|
|
33
|
+
},
|
|
34
|
+
undo() { /* not needed for replay tests */ },
|
|
35
|
+
describe(params) {
|
|
36
|
+
return `Set pixel at (${String(params["x"])}, ${String(params["y"])})`;
|
|
37
|
+
},
|
|
38
|
+
tier: 'frame',
|
|
39
|
+
};
|
|
40
|
+
commandRegistry.set('set_pixel', def);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- Test command: fills a rect ---
|
|
44
|
+
|
|
45
|
+
function registerFillRectCommand(): void {
|
|
46
|
+
const def: CommandDefinition = {
|
|
47
|
+
execute(params, ctx: CommandContext) {
|
|
48
|
+
const buf = ctx["pixelBuffer"] as PixelBuffer | undefined;
|
|
49
|
+
if (!buf) return;
|
|
50
|
+
buf.fillRect(
|
|
51
|
+
params["x"] as number,
|
|
52
|
+
params["y"] as number,
|
|
53
|
+
params["w"] as number,
|
|
54
|
+
params["h"] as number,
|
|
55
|
+
(params["r"] ?? 0) as number,
|
|
56
|
+
(params["g"] ?? 255) as number,
|
|
57
|
+
(params["b"] ?? 0) as number,
|
|
58
|
+
(params["a"] ?? 255) as number,
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
undo() {},
|
|
62
|
+
describe(params) {
|
|
63
|
+
return `Fill rect at (${String(params["x"])}, ${String(params["y"])})`;
|
|
64
|
+
},
|
|
65
|
+
tier: 'frame',
|
|
66
|
+
};
|
|
67
|
+
commandRegistry.set('fill_rect', def);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Create a mock ActionLogEntry for testing. */
|
|
71
|
+
function makeLogEntry(
|
|
72
|
+
type: string,
|
|
73
|
+
params: Record<string, unknown>,
|
|
74
|
+
enabled = true,
|
|
75
|
+
): ActionLogEntry {
|
|
76
|
+
return {
|
|
77
|
+
commandId: crypto.randomUUID(),
|
|
78
|
+
type,
|
|
79
|
+
plugin: 'test/plugin',
|
|
80
|
+
description: `Test ${type}`,
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
enabled,
|
|
83
|
+
params,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Tests ---
|
|
88
|
+
|
|
89
|
+
describe('Replay Engine', () => {
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
registerSetPixelCommand();
|
|
92
|
+
registerFillRectCommand();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should replay a single command and produce correct pixels', () => {
|
|
96
|
+
const buf = replayCommands(
|
|
97
|
+
[{ type: 'set_pixel', params: { x: 2, y: 3, r: 100, g: 150, b: 200, a: 255 } }],
|
|
98
|
+
8, 8, 'layer1',
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(buf.getPixel(2, 3)).toEqual([100, 150, 200, 255]);
|
|
102
|
+
// Other pixels should be untouched (default zero)
|
|
103
|
+
expect(buf.getPixel(0, 0)).toEqual([0, 0, 0, 0]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should replay multiple commands in order', () => {
|
|
107
|
+
const buf = replayCommands(
|
|
108
|
+
[
|
|
109
|
+
{ type: 'set_pixel', params: { x: 0, y: 0, r: 255, g: 0, b: 0, a: 255 } },
|
|
110
|
+
{ type: 'set_pixel', params: { x: 1, y: 0, r: 0, g: 255, b: 0, a: 255 } },
|
|
111
|
+
{ type: 'set_pixel', params: { x: 2, y: 0, r: 0, g: 0, b: 255, a: 255 } },
|
|
112
|
+
],
|
|
113
|
+
8, 8, 'layer1',
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(buf.getPixel(0, 0)).toEqual([255, 0, 0, 255]);
|
|
117
|
+
expect(buf.getPixel(1, 0)).toEqual([0, 255, 0, 255]);
|
|
118
|
+
expect(buf.getPixel(2, 0)).toEqual([0, 0, 255, 255]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should replay overlapping commands where later overwrites earlier', () => {
|
|
122
|
+
const buf = replayCommands(
|
|
123
|
+
[
|
|
124
|
+
{ type: 'set_pixel', params: { x: 0, y: 0, r: 255, g: 0, b: 0, a: 255 } },
|
|
125
|
+
{ type: 'set_pixel', params: { x: 0, y: 0, r: 0, g: 255, b: 0, a: 255 } },
|
|
126
|
+
],
|
|
127
|
+
8, 8, 'layer1',
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Second command overwrites the first at (0,0)
|
|
131
|
+
expect(buf.getPixel(0, 0)).toEqual([0, 255, 0, 255]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should produce different result when overlapping commands are reordered', () => {
|
|
135
|
+
const entry1 = makeLogEntry('set_pixel', { x: 0, y: 0, r: 255, g: 0, b: 0, a: 255 });
|
|
136
|
+
const entry2 = makeLogEntry('set_pixel', { x: 0, y: 0, r: 0, g: 255, b: 0, a: 255 });
|
|
137
|
+
|
|
138
|
+
// Original order: entry1 then entry2 -> pixel is green
|
|
139
|
+
const buf1 = previewReorder(
|
|
140
|
+
[entry1, entry2],
|
|
141
|
+
[entry1.commandId, entry2.commandId],
|
|
142
|
+
8, 8, 'layer1',
|
|
143
|
+
);
|
|
144
|
+
expect(buf1.getPixel(0, 0)).toEqual([0, 255, 0, 255]);
|
|
145
|
+
|
|
146
|
+
// Reversed order: entry2 then entry1 -> pixel is red
|
|
147
|
+
const buf2 = previewReorder(
|
|
148
|
+
[entry1, entry2],
|
|
149
|
+
[entry2.commandId, entry1.commandId],
|
|
150
|
+
8, 8, 'layer1',
|
|
151
|
+
);
|
|
152
|
+
expect(buf2.getPixel(0, 0)).toEqual([255, 0, 0, 255]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should skip disabled entries during replay', () => {
|
|
156
|
+
const entry1 = makeLogEntry('set_pixel', { x: 0, y: 0, r: 255, g: 0, b: 0, a: 255 });
|
|
157
|
+
const entry2 = makeLogEntry('set_pixel', { x: 1, y: 0, r: 0, g: 255, b: 0, a: 255 }, false);
|
|
158
|
+
|
|
159
|
+
const buf = previewReorder(
|
|
160
|
+
[entry1, entry2],
|
|
161
|
+
[entry1.commandId, entry2.commandId],
|
|
162
|
+
8, 8, 'layer1',
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// entry1 pixel should be set
|
|
166
|
+
expect(buf.getPixel(0, 0)).toEqual([255, 0, 0, 255]);
|
|
167
|
+
// entry2 is disabled, so pixel should be untouched
|
|
168
|
+
expect(buf.getPixel(1, 0)).toEqual([0, 0, 0, 0]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should handle previewReorder without modifying original entries', () => {
|
|
172
|
+
const entry1 = makeLogEntry('set_pixel', { x: 0, y: 0, r: 255, g: 0, b: 0, a: 255 });
|
|
173
|
+
const entry2 = makeLogEntry('set_pixel', { x: 1, y: 0, r: 0, g: 255, b: 0, a: 255 });
|
|
174
|
+
|
|
175
|
+
const originalEntries = [entry1, entry2];
|
|
176
|
+
const originalIds = originalEntries.map((e) => e.commandId);
|
|
177
|
+
|
|
178
|
+
// Preview with reversed order
|
|
179
|
+
previewReorder(
|
|
180
|
+
originalEntries,
|
|
181
|
+
[entry2.commandId, entry1.commandId],
|
|
182
|
+
8, 8, 'layer1',
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Original entries should be unchanged
|
|
186
|
+
expect(originalEntries.map((e) => e.commandId)).toEqual(originalIds);
|
|
187
|
+
expect(originalEntries).toHaveLength(2);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should preview parameter edit with modified params', () => {
|
|
191
|
+
// We need entries in the action log for previewParamEdit.
|
|
192
|
+
// Use previewReorder to test param editing at the replay level directly.
|
|
193
|
+
const entry1 = makeLogEntry('set_pixel', { x: 0, y: 0, r: 255, g: 0, b: 0, a: 255 });
|
|
194
|
+
|
|
195
|
+
// Replay with modified params: change color from red to blue
|
|
196
|
+
const commands = [
|
|
197
|
+
{ type: entry1.type, params: { x: 0, y: 0, r: 0, g: 0, b: 255, a: 255 } },
|
|
198
|
+
];
|
|
199
|
+
const buf = replayCommands(commands, 8, 8, 'layer1');
|
|
200
|
+
|
|
201
|
+
expect(buf.getPixel(0, 0)).toEqual([0, 0, 255, 255]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should create a fresh buffer of the correct dimensions', () => {
|
|
205
|
+
const buf = replayCommands([], 16, 32, 'layer1');
|
|
206
|
+
|
|
207
|
+
expect(buf.width).toBe(16);
|
|
208
|
+
expect(buf.height).toBe(32);
|
|
209
|
+
// Empty replay produces a blank buffer
|
|
210
|
+
expect(buf.getPixel(0, 0)).toEqual([0, 0, 0, 0]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should skip unknown command types gracefully', () => {
|
|
214
|
+
const buf = replayCommands(
|
|
215
|
+
[
|
|
216
|
+
{ type: 'nonexistent_command', params: {} },
|
|
217
|
+
{ type: 'set_pixel', params: { x: 0, y: 0, r: 255, g: 0, b: 0, a: 255 } },
|
|
218
|
+
],
|
|
219
|
+
8, 8, 'layer1',
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Unknown command skipped, known command still executed
|
|
223
|
+
expect(buf.getPixel(0, 0)).toEqual([255, 0, 0, 255]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should replay fill_rect command covering multiple pixels', () => {
|
|
227
|
+
const buf = replayCommands(
|
|
228
|
+
[{ type: 'fill_rect', params: { x: 0, y: 0, w: 3, h: 2, r: 128, g: 64, b: 32, a: 255 } }],
|
|
229
|
+
8, 8, 'layer1',
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// All pixels in the 3x2 rect should be filled
|
|
233
|
+
for (let y = 0; y < 2; y++) {
|
|
234
|
+
for (let x = 0; x < 3; x++) {
|
|
235
|
+
expect(buf.getPixel(x, y)).toEqual([128, 64, 32, 255]);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Pixel outside the rect should be untouched
|
|
239
|
+
expect(buf.getPixel(3, 0)).toEqual([0, 0, 0, 0]);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Replay Engine -- the core of the semantic history system.
|
|
3
|
+
*
|
|
4
|
+
* Instead of traditional undo/redo that walks backward through state snapshots,
|
|
5
|
+
* the replay engine re-executes commands from scratch on a fresh PixelBuffer.
|
|
6
|
+
* This enables powerful operations like reordering, toggling, and parameter
|
|
7
|
+
* editing that would be impossible with snapshot-based undo.
|
|
8
|
+
*
|
|
9
|
+
* The engine looks up command definitions in the command registry and calls
|
|
10
|
+
* their execute() functions with a temporary command context containing a
|
|
11
|
+
* fresh PixelBuffer.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { PixelBuffer } from '../canvas/pixel-buffer.js';
|
|
15
|
+
import { commandRegistry } from '../core/registries.svelte.js';
|
|
16
|
+
import type { CommandContext } from '../core/commands.js';
|
|
17
|
+
import type { ActionLogEntry } from './action-log.svelte.js';
|
|
18
|
+
import { actionLog } from './action-log.svelte.js';
|
|
19
|
+
|
|
20
|
+
// --- Types ---
|
|
21
|
+
|
|
22
|
+
export interface ReplayCommand {
|
|
23
|
+
type: string;
|
|
24
|
+
params: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- Core replay ---
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Replay a sequence of commands on a fresh pixel buffer.
|
|
31
|
+
*
|
|
32
|
+
* Creates a new PixelBuffer and a temporary CommandContext, then executes
|
|
33
|
+
* each command in order. Commands that are not registered are skipped with
|
|
34
|
+
* a warning rather than throwing, to be resilient to missing plugins.
|
|
35
|
+
*/
|
|
36
|
+
export function replayCommands(
|
|
37
|
+
commands: ReplayCommand[],
|
|
38
|
+
width: number,
|
|
39
|
+
height: number,
|
|
40
|
+
layerId: string,
|
|
41
|
+
): PixelBuffer {
|
|
42
|
+
const buffer = new PixelBuffer(width, height);
|
|
43
|
+
|
|
44
|
+
// Build a temporary context with the fresh buffer
|
|
45
|
+
const ctx: CommandContext = {
|
|
46
|
+
pixelBuffer: buffer,
|
|
47
|
+
layerId,
|
|
48
|
+
width,
|
|
49
|
+
height,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
for (const cmd of commands) {
|
|
53
|
+
const def = commandRegistry.get(cmd.type);
|
|
54
|
+
if (!def) {
|
|
55
|
+
console.warn(`[replay-engine] Skipping unknown command type "${cmd.type}"`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
// Clone params to avoid mutation of the source data during execute
|
|
59
|
+
const params = { ...cmd.params };
|
|
60
|
+
def.execute(params, ctx);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return buffer;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- Preview / apply operations ---
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Preview a reorder: replay entries in a proposed new order and return the
|
|
70
|
+
* resulting buffer. Does NOT modify the action log state.
|
|
71
|
+
*
|
|
72
|
+
* Only enabled entries are replayed; disabled entries are skipped.
|
|
73
|
+
*/
|
|
74
|
+
export function previewReorder(
|
|
75
|
+
entries: ActionLogEntry[],
|
|
76
|
+
newOrder: string[],
|
|
77
|
+
width: number,
|
|
78
|
+
height: number,
|
|
79
|
+
layerId: string,
|
|
80
|
+
): PixelBuffer {
|
|
81
|
+
const entryMap = new Map(entries.map((e) => [e.commandId, e]));
|
|
82
|
+
|
|
83
|
+
const commands: ReplayCommand[] = [];
|
|
84
|
+
for (const id of newOrder) {
|
|
85
|
+
const entry = entryMap.get(id);
|
|
86
|
+
if (!entry || !entry.enabled) continue;
|
|
87
|
+
commands.push({ type: entry.type, params: entry.params });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return replayCommands(commands, width, height, layerId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Apply a reorder: update the action log entries to the new order and
|
|
95
|
+
* return the replayed buffer. The caller is responsible for applying the
|
|
96
|
+
* buffer to the canvas.
|
|
97
|
+
*/
|
|
98
|
+
export function applyReorder(
|
|
99
|
+
newOrder: string[],
|
|
100
|
+
width: number,
|
|
101
|
+
height: number,
|
|
102
|
+
layerId: string,
|
|
103
|
+
): PixelBuffer {
|
|
104
|
+
const currentEntries = actionLog.getEntries();
|
|
105
|
+
const entryMap = new Map(currentEntries.map((e) => [e.commandId, e]));
|
|
106
|
+
|
|
107
|
+
// Reorder entries according to newOrder
|
|
108
|
+
const reordered: ActionLogEntry[] = [];
|
|
109
|
+
for (const id of newOrder) {
|
|
110
|
+
const entry = entryMap.get(id);
|
|
111
|
+
if (entry) reordered.push(entry);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Update the action log
|
|
115
|
+
actionLog.setEntries(reordered);
|
|
116
|
+
|
|
117
|
+
// Replay enabled entries in the new order
|
|
118
|
+
const commands: ReplayCommand[] = reordered
|
|
119
|
+
.filter((e) => e.enabled)
|
|
120
|
+
.map((e) => ({ type: e.type, params: e.params }));
|
|
121
|
+
|
|
122
|
+
return replayCommands(commands, width, height, layerId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Preview a parameter edit: replay the full action log with one entry's
|
|
127
|
+
* params replaced. Does NOT modify the action log state.
|
|
128
|
+
*/
|
|
129
|
+
export function previewParamEdit(
|
|
130
|
+
entryId: string,
|
|
131
|
+
newParams: Record<string, unknown>,
|
|
132
|
+
width: number,
|
|
133
|
+
height: number,
|
|
134
|
+
layerId: string,
|
|
135
|
+
): PixelBuffer {
|
|
136
|
+
const entries = actionLog.getEntries();
|
|
137
|
+
|
|
138
|
+
const commands: ReplayCommand[] = [];
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (!entry.enabled) continue;
|
|
141
|
+
if (entry.commandId === entryId) {
|
|
142
|
+
commands.push({ type: entry.type, params: newParams });
|
|
143
|
+
} else {
|
|
144
|
+
commands.push({ type: entry.type, params: entry.params });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return replayCommands(commands, width, height, layerId);
|
|
149
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Spec Export/Import module.
|
|
3
|
+
*
|
|
4
|
+
* Exercises export, import, roundtrip, and diff operations on spec documents.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import type { ActionLogEntry } from './action-log.svelte.js';
|
|
9
|
+
import type { SpecDocument, SpecCommand } from './spec-format.js';
|
|
10
|
+
import { exportSpec, importSpec, diffSpecs } from './spec-format.js';
|
|
11
|
+
|
|
12
|
+
// --- Helpers ---
|
|
13
|
+
|
|
14
|
+
function makeEntry(
|
|
15
|
+
type: string,
|
|
16
|
+
params: Record<string, unknown>,
|
|
17
|
+
description: string,
|
|
18
|
+
enabled = true,
|
|
19
|
+
): ActionLogEntry {
|
|
20
|
+
return {
|
|
21
|
+
commandId: crypto.randomUUID(),
|
|
22
|
+
type,
|
|
23
|
+
plugin: 'test/plugin',
|
|
24
|
+
description,
|
|
25
|
+
timestamp: Date.now(),
|
|
26
|
+
enabled,
|
|
27
|
+
params,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeSpec(commands: SpecCommand[], width = 32, height = 32): SpecDocument {
|
|
32
|
+
return {
|
|
33
|
+
version: '1.0.0',
|
|
34
|
+
canvasWidth: width,
|
|
35
|
+
canvasHeight: height,
|
|
36
|
+
commands,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- Tests ---
|
|
41
|
+
|
|
42
|
+
describe('Spec Format', () => {
|
|
43
|
+
describe('Export', () => {
|
|
44
|
+
it('should produce a valid spec document from action log entries', () => {
|
|
45
|
+
const entries = [
|
|
46
|
+
makeEntry('draw_pixel', { x: 5, y: 10, color: '#ff0000' }, 'Drew pixel at (5, 10)'),
|
|
47
|
+
makeEntry('fill_rect', { x: 0, y: 0, w: 8, h: 8 }, 'Filled 8x8 rect'),
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const spec = exportSpec(entries, 32, 32);
|
|
51
|
+
|
|
52
|
+
expect(spec.version).toBe('1.0.0');
|
|
53
|
+
expect(spec.canvasWidth).toBe(32);
|
|
54
|
+
expect(spec.canvasHeight).toBe(32);
|
|
55
|
+
expect(spec.commands).toHaveLength(2);
|
|
56
|
+
expect(spec.commands[0]?.type).toBe('draw_pixel');
|
|
57
|
+
expect(spec.commands[0]?.description).toBe('Drew pixel at (5, 10)');
|
|
58
|
+
expect(spec.commands[0]?.params["x"]).toBe(5);
|
|
59
|
+
expect(spec.commands[1]?.type).toBe('fill_rect');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should exclude disabled entries from the export', () => {
|
|
63
|
+
const entries = [
|
|
64
|
+
makeEntry('draw_pixel', { x: 0, y: 0 }, 'Enabled'),
|
|
65
|
+
makeEntry('draw_pixel', { x: 1, y: 1 }, 'Disabled', false),
|
|
66
|
+
makeEntry('draw_pixel', { x: 2, y: 2 }, 'Also enabled'),
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const spec = exportSpec(entries, 16, 16);
|
|
70
|
+
|
|
71
|
+
expect(spec.commands).toHaveLength(2);
|
|
72
|
+
expect(spec.commands[0]?.description).toBe('Enabled');
|
|
73
|
+
expect(spec.commands[1]?.description).toBe('Also enabled');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should strip internal params (underscore-prefixed) from export', () => {
|
|
77
|
+
const entries = [
|
|
78
|
+
makeEntry('draw_pixel', {
|
|
79
|
+
x: 5,
|
|
80
|
+
y: 10,
|
|
81
|
+
_oldPixel: [0, 0, 0, 0],
|
|
82
|
+
_snapshot: { data: 'internal' },
|
|
83
|
+
}, 'Drew pixel'),
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const spec = exportSpec(entries, 32, 32);
|
|
87
|
+
|
|
88
|
+
expect(spec.commands[0]?.params).toEqual({ x: 5, y: 10 });
|
|
89
|
+
expect(spec.commands[0]?.params["_oldPixel"]).toBeUndefined();
|
|
90
|
+
expect(spec.commands[0]?.params["_snapshot"]).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('Import', () => {
|
|
95
|
+
it('should parse a spec document into a command sequence', () => {
|
|
96
|
+
const spec = makeSpec([
|
|
97
|
+
{ type: 'draw_pixel', description: 'Drew pixel', params: { x: 0, y: 0 } },
|
|
98
|
+
{ type: 'fill_rect', description: 'Filled rect', params: { w: 8, h: 8 } },
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const commands = importSpec(spec);
|
|
102
|
+
|
|
103
|
+
expect(commands).toHaveLength(2);
|
|
104
|
+
expect(commands[0]?.type).toBe('draw_pixel');
|
|
105
|
+
expect(commands[0]?.params["x"]).toBe(0);
|
|
106
|
+
expect(commands[1]?.type).toBe('fill_rect');
|
|
107
|
+
expect(commands[1]?.params["w"]).toBe(8);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should throw on invalid spec document', () => {
|
|
111
|
+
expect(() => importSpec(null as unknown as SpecDocument)).toThrow('Invalid spec document');
|
|
112
|
+
expect(() => importSpec({} as SpecDocument)).toThrow('Invalid spec document');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('Roundtrip', () => {
|
|
117
|
+
it('should produce identical spec after export -> import -> export', () => {
|
|
118
|
+
const entries = [
|
|
119
|
+
makeEntry('draw_pixel', { x: 5, y: 10, color: '#ff0000' }, 'Drew red pixel'),
|
|
120
|
+
makeEntry('fill_rect', { x: 0, y: 0, w: 16, h: 16, color: '#00ff00' }, 'Filled green rect'),
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
const spec1 = exportSpec(entries, 64, 64);
|
|
124
|
+
const commands = importSpec(spec1);
|
|
125
|
+
|
|
126
|
+
// Recreate entries from imported commands
|
|
127
|
+
const reimportedEntries = commands.map((cmd, i) => ({
|
|
128
|
+
commandId: crypto.randomUUID(),
|
|
129
|
+
type: cmd.type,
|
|
130
|
+
plugin: 'test/plugin',
|
|
131
|
+
description: spec1.commands[i]?.description ?? '',
|
|
132
|
+
timestamp: Date.now(),
|
|
133
|
+
enabled: true,
|
|
134
|
+
params: cmd.params,
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
const spec2 = exportSpec(reimportedEntries, 64, 64);
|
|
138
|
+
|
|
139
|
+
// Compare the command data (ignoring version/canvas metadata which are set by exportSpec)
|
|
140
|
+
expect(spec2.commands).toHaveLength(spec1.commands.length);
|
|
141
|
+
for (let i = 0; i < spec1.commands.length; i++) {
|
|
142
|
+
expect(spec2.commands[i]?.type).toBe(spec1.commands[i]?.type);
|
|
143
|
+
expect(spec2.commands[i]?.description).toBe(spec1.commands[i]?.description);
|
|
144
|
+
expect(spec2.commands[i]?.params).toEqual(spec1.commands[i]?.params);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('Diff', () => {
|
|
150
|
+
it('should return no differences for identical specs', () => {
|
|
151
|
+
const commands = [
|
|
152
|
+
{ type: 'draw_pixel', description: 'Drew pixel', params: { x: 0, y: 0 } },
|
|
153
|
+
];
|
|
154
|
+
const spec1 = makeSpec(commands);
|
|
155
|
+
const spec2 = makeSpec([...commands]);
|
|
156
|
+
|
|
157
|
+
const diffs = diffSpecs(spec1, spec2);
|
|
158
|
+
expect(diffs).toHaveLength(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should detect an added command', () => {
|
|
162
|
+
const spec1 = makeSpec([
|
|
163
|
+
{ type: 'draw_pixel', description: 'Drew pixel', params: { x: 0, y: 0 } },
|
|
164
|
+
]);
|
|
165
|
+
const spec2 = makeSpec([
|
|
166
|
+
{ type: 'draw_pixel', description: 'Drew pixel', params: { x: 0, y: 0 } },
|
|
167
|
+
{ type: 'fill_rect', description: 'Filled rect', params: { w: 8, h: 8 } },
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const diffs = diffSpecs(spec1, spec2);
|
|
171
|
+
|
|
172
|
+
expect(diffs).toHaveLength(1);
|
|
173
|
+
expect(diffs[0]?.type).toBe('added');
|
|
174
|
+
expect(diffs[0]?.index).toBe(1);
|
|
175
|
+
expect(diffs[0]?.command?.type).toBe('fill_rect');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should detect a removed command', () => {
|
|
179
|
+
const spec1 = makeSpec([
|
|
180
|
+
{ type: 'draw_pixel', description: 'Drew pixel', params: { x: 0, y: 0 } },
|
|
181
|
+
{ type: 'fill_rect', description: 'Filled rect', params: { w: 8, h: 8 } },
|
|
182
|
+
]);
|
|
183
|
+
const spec2 = makeSpec([
|
|
184
|
+
{ type: 'draw_pixel', description: 'Drew pixel', params: { x: 0, y: 0 } },
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
const diffs = diffSpecs(spec1, spec2);
|
|
188
|
+
|
|
189
|
+
expect(diffs).toHaveLength(1);
|
|
190
|
+
expect(diffs[0]?.type).toBe('removed');
|
|
191
|
+
expect(diffs[0]?.index).toBe(1);
|
|
192
|
+
expect(diffs[0]?.oldCommand?.type).toBe('fill_rect');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should detect modified params', () => {
|
|
196
|
+
const spec1 = makeSpec([
|
|
197
|
+
{ type: 'draw_pixel', description: 'Drew pixel', params: { x: 0, y: 0, color: '#ff0000' } },
|
|
198
|
+
]);
|
|
199
|
+
const spec2 = makeSpec([
|
|
200
|
+
{ type: 'draw_pixel', description: 'Drew pixel', params: { x: 5, y: 0, color: '#ff0000' } },
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
const diffs = diffSpecs(spec1, spec2);
|
|
204
|
+
|
|
205
|
+
expect(diffs).toHaveLength(1);
|
|
206
|
+
expect(diffs[0]?.type).toBe('modified');
|
|
207
|
+
expect(diffs[0]?.changes).toContain('x');
|
|
208
|
+
expect(diffs[0]?.changes).not.toContain('color');
|
|
209
|
+
expect(diffs[0]?.changes).not.toContain('y');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should detect type change as a modification', () => {
|
|
213
|
+
const spec1 = makeSpec([
|
|
214
|
+
{ type: 'draw_pixel', description: 'Drew pixel', params: { x: 0, y: 0 } },
|
|
215
|
+
]);
|
|
216
|
+
const spec2 = makeSpec([
|
|
217
|
+
{ type: 'fill_rect', description: 'Filled rect', params: { x: 0, y: 0 } },
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
const diffs = diffSpecs(spec1, spec2);
|
|
221
|
+
|
|
222
|
+
expect(diffs).toHaveLength(1);
|
|
223
|
+
expect(diffs[0]?.type).toBe('modified');
|
|
224
|
+
expect(diffs[0]?.changes).toContain('type');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should handle complex diffs with multiple changes', () => {
|
|
228
|
+
const spec1 = makeSpec([
|
|
229
|
+
{ type: 'cmd_a', description: 'A', params: { v: 1 } },
|
|
230
|
+
{ type: 'cmd_b', description: 'B', params: { v: 2 } },
|
|
231
|
+
{ type: 'cmd_c', description: 'C', params: { v: 3 } },
|
|
232
|
+
]);
|
|
233
|
+
const spec2 = makeSpec([
|
|
234
|
+
{ type: 'cmd_a', description: 'A', params: { v: 1 } }, // same
|
|
235
|
+
{ type: 'cmd_b', description: 'B', params: { v: 99 } }, // modified
|
|
236
|
+
{ type: 'cmd_c', description: 'C', params: { v: 3 } }, // same
|
|
237
|
+
{ type: 'cmd_d', description: 'D', params: { v: 4 } }, // added
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
const diffs = diffSpecs(spec1, spec2);
|
|
241
|
+
|
|
242
|
+
expect(diffs).toHaveLength(2);
|
|
243
|
+
expect(diffs[0]?.type).toBe('modified');
|
|
244
|
+
expect(diffs[0]?.index).toBe(1);
|
|
245
|
+
expect(diffs[0]?.changes).toContain('v');
|
|
246
|
+
expect(diffs[1]?.type).toBe('added');
|
|
247
|
+
expect(diffs[1]?.index).toBe(3);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|