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,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Macro System -- record and replay sequences of commands.
|
|
3
|
+
*
|
|
4
|
+
* Macros capture commands as they are dispatched (via the onExecute hook) and
|
|
5
|
+
* can replay them later. Two coordinate modes are supported:
|
|
6
|
+
*
|
|
7
|
+
* - 'absolute': commands are replayed with their original coordinates.
|
|
8
|
+
* - 'relative': coordinate params are shifted by an offset from the recorded
|
|
9
|
+
* origin (the first command's position), allowing the macro to be applied
|
|
10
|
+
* at different canvas locations.
|
|
11
|
+
*
|
|
12
|
+
* Coordinate params are identified by naming convention: params named x, y,
|
|
13
|
+
* x0, y0, x1, y1, cx, cy, or items in a `pixels` array with x/y fields.
|
|
14
|
+
*
|
|
15
|
+
* Module-level singleton using Svelte 5 $state runes.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { dispatch as dispatchCommand, onExecute } from '../core/dispatcher.js';
|
|
19
|
+
import type { Command } from '../core/commands.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Deep-clone a plain object. Uses JSON round-trip instead of deepClone
|
|
23
|
+
* because command params may be Svelte 5 reactive proxies ($state) which
|
|
24
|
+
* deepClone cannot handle.
|
|
25
|
+
*/
|
|
26
|
+
function deepClone<T>(obj: T): T {
|
|
27
|
+
return JSON.parse(JSON.stringify(obj)) as T;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- Types ---
|
|
31
|
+
|
|
32
|
+
export interface Macro {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
commands: { type: string; params: Record<string, unknown> }[];
|
|
36
|
+
coordMode: 'absolute' | 'relative';
|
|
37
|
+
// For relative mode: the reference origin (first command's coordinate position)
|
|
38
|
+
originX?: number;
|
|
39
|
+
originY?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface RecordedCommand {
|
|
43
|
+
type: string;
|
|
44
|
+
params: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- Coordinate parameter names recognized for relative mode ---
|
|
48
|
+
|
|
49
|
+
const COORD_X_NAMES = new Set(['x', 'x0', 'x1', 'cx']);
|
|
50
|
+
const COORD_Y_NAMES = new Set(['y', 'y0', 'y1', 'cy']);
|
|
51
|
+
|
|
52
|
+
// --- Reactive state ---
|
|
53
|
+
|
|
54
|
+
let macros = $state<Macro[]>([]);
|
|
55
|
+
let recording = $state<boolean>(false);
|
|
56
|
+
let recordedCommands = $state<RecordedCommand[]>([]);
|
|
57
|
+
|
|
58
|
+
/** Unsubscribe function for the onExecute hook during recording. */
|
|
59
|
+
let unsubscribeHook: (() => void) | null = null;
|
|
60
|
+
|
|
61
|
+
// --- Internal helpers ---
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extract the first x/y coordinate pair from a command's params.
|
|
65
|
+
* Used to determine the origin for relative-mode macros.
|
|
66
|
+
*/
|
|
67
|
+
function extractOrigin(params: Record<string, unknown>): { x: number; y: number } | null {
|
|
68
|
+
// Check direct params first
|
|
69
|
+
for (const xName of COORD_X_NAMES) {
|
|
70
|
+
for (const yName of COORD_Y_NAMES) {
|
|
71
|
+
const xv = params[xName];
|
|
72
|
+
const yv = params[yName];
|
|
73
|
+
if (typeof xv === 'number' && typeof yv === 'number') {
|
|
74
|
+
return { x: xv, y: yv };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check pixels array
|
|
80
|
+
const pixels = params["pixels"];
|
|
81
|
+
if (Array.isArray(pixels) && pixels.length > 0) {
|
|
82
|
+
const first: unknown = pixels[0];
|
|
83
|
+
if (typeof first === 'object' && first !== null) {
|
|
84
|
+
const rec = first as Record<string, unknown>;
|
|
85
|
+
if (typeof rec["x"] === 'number' && typeof rec["y"] === 'number') {
|
|
86
|
+
return { x: rec["x"], y: rec["y"] };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Shift coordinate params in a command by the given offset.
|
|
96
|
+
* Returns a new params object (does not mutate the original).
|
|
97
|
+
*/
|
|
98
|
+
function offsetParams(
|
|
99
|
+
params: Record<string, unknown>,
|
|
100
|
+
offsetX: number,
|
|
101
|
+
offsetY: number,
|
|
102
|
+
): Record<string, unknown> {
|
|
103
|
+
const result: Record<string, unknown> = { ...params };
|
|
104
|
+
|
|
105
|
+
// Shift direct coordinate params
|
|
106
|
+
for (const xName of COORD_X_NAMES) {
|
|
107
|
+
if (typeof result[xName] === 'number') {
|
|
108
|
+
result[xName] = result[xName] + offsetX;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const yName of COORD_Y_NAMES) {
|
|
112
|
+
if (typeof result[yName] === 'number') {
|
|
113
|
+
result[yName] = result[yName] + offsetY;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Shift pixels array entries
|
|
118
|
+
if (Array.isArray(result["pixels"])) {
|
|
119
|
+
result["pixels"] = (result["pixels"] as Array<Record<string, unknown>>).map((px) => {
|
|
120
|
+
const shifted: Record<string, unknown> = { ...px };
|
|
121
|
+
if (typeof shifted["x"] === 'number') shifted["x"] = shifted["x"] + offsetX;
|
|
122
|
+
if (typeof shifted["y"] === 'number') shifted["y"] = shifted["y"] + offsetY;
|
|
123
|
+
return shifted;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- Recording operations ---
|
|
131
|
+
|
|
132
|
+
/** Start recording commands. Hooks into the dispatcher's onExecute. */
|
|
133
|
+
function startRecording(): void {
|
|
134
|
+
if (recording) return;
|
|
135
|
+
recording = true;
|
|
136
|
+
recordedCommands = [];
|
|
137
|
+
|
|
138
|
+
unsubscribeHook = onExecute((command: Command) => {
|
|
139
|
+
recordedCommands.push({
|
|
140
|
+
type: command.type,
|
|
141
|
+
// Clone params to capture the state at recording time
|
|
142
|
+
params: deepClone(command.params),
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Stop recording and create a macro from the recorded commands.
|
|
149
|
+
* Returns the new Macro.
|
|
150
|
+
*/
|
|
151
|
+
function stopRecording(name: string, coordMode: 'absolute' | 'relative'): Macro {
|
|
152
|
+
if (unsubscribeHook) {
|
|
153
|
+
unsubscribeHook();
|
|
154
|
+
unsubscribeHook = null;
|
|
155
|
+
}
|
|
156
|
+
recording = false;
|
|
157
|
+
|
|
158
|
+
// Determine origin for relative mode from the first command's coordinates
|
|
159
|
+
let originX: number | undefined;
|
|
160
|
+
let originY: number | undefined;
|
|
161
|
+
|
|
162
|
+
if (coordMode === 'relative' && recordedCommands.length > 0) {
|
|
163
|
+
const first = recordedCommands[0];
|
|
164
|
+
const origin = first ? extractOrigin(first.params) : null;
|
|
165
|
+
if (origin) {
|
|
166
|
+
originX = origin.x;
|
|
167
|
+
originY = origin.y;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Omit originX/originY when absent (exactOptionalPropertyTypes)
|
|
172
|
+
const macro: Macro = {
|
|
173
|
+
id: crypto.randomUUID(),
|
|
174
|
+
name,
|
|
175
|
+
commands: recordedCommands.map((c) => ({
|
|
176
|
+
type: c.type,
|
|
177
|
+
params: deepClone(c.params),
|
|
178
|
+
})),
|
|
179
|
+
coordMode,
|
|
180
|
+
};
|
|
181
|
+
if (originX !== undefined) macro.originX = originX;
|
|
182
|
+
if (originY !== undefined) macro.originY = originY;
|
|
183
|
+
|
|
184
|
+
macros.push(macro);
|
|
185
|
+
recordedCommands = [];
|
|
186
|
+
|
|
187
|
+
return macro;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Cancel recording and discard any recorded commands. */
|
|
191
|
+
function cancelRecording(): void {
|
|
192
|
+
if (unsubscribeHook) {
|
|
193
|
+
unsubscribeHook();
|
|
194
|
+
unsubscribeHook = null;
|
|
195
|
+
}
|
|
196
|
+
recording = false;
|
|
197
|
+
recordedCommands = [];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- Macro management ---
|
|
201
|
+
|
|
202
|
+
/** Delete a macro by ID. */
|
|
203
|
+
function deleteMacro(id: string): void {
|
|
204
|
+
const idx = macros.findIndex((m) => m.id === id);
|
|
205
|
+
if (idx !== -1) macros.splice(idx, 1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Rename a macro. */
|
|
209
|
+
function renameMacro(id: string, name: string): void {
|
|
210
|
+
const macro = macros.find((m) => m.id === id);
|
|
211
|
+
if (macro) macro.name = name;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Replay a macro by dispatching its commands through the dispatcher.
|
|
216
|
+
*
|
|
217
|
+
* For 'relative' mode, offsetX/offsetY shift all coordinate params
|
|
218
|
+
* relative to the macro's recorded origin.
|
|
219
|
+
*
|
|
220
|
+
* For 'absolute' mode, offsetX/offsetY are ignored.
|
|
221
|
+
*/
|
|
222
|
+
function replayMacro(
|
|
223
|
+
macro: Macro,
|
|
224
|
+
offsetX?: number,
|
|
225
|
+
offsetY?: number,
|
|
226
|
+
): void {
|
|
227
|
+
for (const cmd of macro.commands) {
|
|
228
|
+
let params = deepClone(cmd.params);
|
|
229
|
+
|
|
230
|
+
if (macro.coordMode === 'relative' && (offsetX !== undefined || offsetY !== undefined)) {
|
|
231
|
+
// Compute the shift from the macro's origin to the target position
|
|
232
|
+
const dx = (offsetX ?? 0) - (macro.originX ?? 0);
|
|
233
|
+
const dy = (offsetY ?? 0) - (macro.originY ?? 0);
|
|
234
|
+
params = offsetParams(params, dx, dy);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const command: Command = {
|
|
238
|
+
type: cmd.type,
|
|
239
|
+
plugin: 'macro',
|
|
240
|
+
version: '1.0.0',
|
|
241
|
+
params,
|
|
242
|
+
timestamp: Date.now(),
|
|
243
|
+
id: crypto.randomUUID(),
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
dispatchCommand(command);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- Serialization ---
|
|
251
|
+
|
|
252
|
+
/** Serialize all macros to a plain object for persistence. */
|
|
253
|
+
function serialize(): object {
|
|
254
|
+
return {
|
|
255
|
+
macros: macros.map((m) => ({
|
|
256
|
+
id: m.id,
|
|
257
|
+
name: m.name,
|
|
258
|
+
commands: m.commands,
|
|
259
|
+
coordMode: m.coordMode,
|
|
260
|
+
originX: m.originX,
|
|
261
|
+
originY: m.originY,
|
|
262
|
+
})),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Deserialize macros from a previously serialized object. */
|
|
267
|
+
function deserialize(data: object): void {
|
|
268
|
+
// Type as Partial<Macro>[] since `data` came from untrusted JSON --
|
|
269
|
+
// the defensive fallbacks below only make sense when each field is
|
|
270
|
+
// possibly absent.
|
|
271
|
+
const d = data as { macros?: Partial<Macro>[] };
|
|
272
|
+
if (Array.isArray(d.macros)) {
|
|
273
|
+
// Omit originX/originY when absent (exactOptionalPropertyTypes)
|
|
274
|
+
macros = d.macros.map((m) => {
|
|
275
|
+
const restored: Macro = {
|
|
276
|
+
id: m.id ?? crypto.randomUUID(),
|
|
277
|
+
name: m.name ?? 'Unnamed',
|
|
278
|
+
commands: m.commands ?? [],
|
|
279
|
+
coordMode: m.coordMode ?? 'absolute',
|
|
280
|
+
};
|
|
281
|
+
if (m.originX !== undefined) restored.originX = m.originX;
|
|
282
|
+
if (m.originY !== undefined) restored.originY = m.originY;
|
|
283
|
+
return restored;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// --- Test helper ---
|
|
289
|
+
|
|
290
|
+
/** Reset all macro state. Intended for tests only. */
|
|
291
|
+
function _resetForTesting(): void {
|
|
292
|
+
if (unsubscribeHook) {
|
|
293
|
+
unsubscribeHook();
|
|
294
|
+
unsubscribeHook = null;
|
|
295
|
+
}
|
|
296
|
+
macros = [];
|
|
297
|
+
recording = false;
|
|
298
|
+
recordedCommands = [];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// --- Public API ---
|
|
302
|
+
|
|
303
|
+
export const macroSystem = {
|
|
304
|
+
get macros() { return macros; },
|
|
305
|
+
get recording() { return recording; },
|
|
306
|
+
get recordedCommands() { return recordedCommands; },
|
|
307
|
+
|
|
308
|
+
startRecording,
|
|
309
|
+
stopRecording,
|
|
310
|
+
cancelRecording,
|
|
311
|
+
deleteMacro,
|
|
312
|
+
renameMacro,
|
|
313
|
+
replayMacro,
|
|
314
|
+
serialize,
|
|
315
|
+
deserialize,
|
|
316
|
+
_resetForTesting,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Re-export the offsetParams helper for testing
|
|
320
|
+
export { offsetParams as _offsetParamsForTesting };
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Macro System.
|
|
3
|
+
*
|
|
4
|
+
* Tests recording, replaying (absolute and relative modes), coordinate
|
|
5
|
+
* offsetting, cancellation, deletion, renaming, and serialization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
9
|
+
import type { CommandDefinition, CommandContext } from '../core/commands.js';
|
|
10
|
+
import { commandRegistry } from '../core/registries.svelte.js';
|
|
11
|
+
import { dispatch, _resetForTesting as resetDispatcher } from '../core/dispatcher.js';
|
|
12
|
+
import type { Command } from '../core/commands.js';
|
|
13
|
+
import type { CommandType, ParamsOf } from '../core/command-params.js';
|
|
14
|
+
import { macroSystem, _offsetParamsForTesting as offsetParams } from './macros.svelte.js';
|
|
15
|
+
|
|
16
|
+
// --- Helpers ---
|
|
17
|
+
|
|
18
|
+
function registerTestCommand(name: string): void {
|
|
19
|
+
const def: CommandDefinition = {
|
|
20
|
+
execute(params, ctx: CommandContext) {
|
|
21
|
+
// Track execution for verification
|
|
22
|
+
const execLog = ctx["_execLog"] as Array<Record<string, unknown>> | undefined;
|
|
23
|
+
if (execLog) {
|
|
24
|
+
execLog.push({ type: name, ...params });
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
undo() {},
|
|
28
|
+
describe: (params) => `${name}(${JSON.stringify(params)})`,
|
|
29
|
+
tier: 'frame',
|
|
30
|
+
};
|
|
31
|
+
commandRegistry.set(name, def);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Typed overload: validates params when type is a known command literal
|
|
35
|
+
function makeAndDispatch<T extends CommandType>(type: T, params: ParamsOf<T>): Command;
|
|
36
|
+
// String fallback: for dynamic/unknown command types in tests
|
|
37
|
+
function makeAndDispatch(type: string, params?: Record<string, unknown>): Command;
|
|
38
|
+
function makeAndDispatch(type: string, params: Record<string, unknown> = {}): Command {
|
|
39
|
+
const cmd: Command = {
|
|
40
|
+
type,
|
|
41
|
+
plugin: 'test/plugin',
|
|
42
|
+
version: '1.0.0',
|
|
43
|
+
params,
|
|
44
|
+
timestamp: Date.now(),
|
|
45
|
+
id: crypto.randomUUID(),
|
|
46
|
+
};
|
|
47
|
+
dispatch(cmd);
|
|
48
|
+
return cmd;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- Tests ---
|
|
52
|
+
|
|
53
|
+
describe('Macro System', () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
resetDispatcher();
|
|
56
|
+
macroSystem._resetForTesting();
|
|
57
|
+
registerTestCommand('draw_pixel');
|
|
58
|
+
registerTestCommand('draw_line');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should record commands and create a macro in absolute mode', () => {
|
|
62
|
+
macroSystem.startRecording();
|
|
63
|
+
expect(macroSystem.recording).toBe(true);
|
|
64
|
+
|
|
65
|
+
makeAndDispatch('draw_pixel', { x: 5, y: 10, color: '#ff0000' });
|
|
66
|
+
makeAndDispatch('draw_pixel', { x: 6, y: 11, color: '#00ff00' });
|
|
67
|
+
|
|
68
|
+
const macro = macroSystem.stopRecording('Test Macro', 'absolute');
|
|
69
|
+
|
|
70
|
+
expect(macroSystem.recording).toBe(false);
|
|
71
|
+
expect(macro.name).toBe('Test Macro');
|
|
72
|
+
expect(macro.coordMode).toBe('absolute');
|
|
73
|
+
expect(macro.commands).toHaveLength(2);
|
|
74
|
+
expect(macro.commands[0]?.type).toBe('draw_pixel');
|
|
75
|
+
expect(macro.commands[0]?.params["x"]).toBe(5);
|
|
76
|
+
expect(macro.commands[1]?.params["x"]).toBe(6);
|
|
77
|
+
expect(macroSystem.macros).toHaveLength(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should record commands and create a macro in relative mode with origin', () => {
|
|
81
|
+
macroSystem.startRecording();
|
|
82
|
+
|
|
83
|
+
makeAndDispatch('draw_pixel', { x: 10, y: 20, color: '#ff0000' });
|
|
84
|
+
makeAndDispatch('draw_pixel', { x: 12, y: 22, color: '#00ff00' });
|
|
85
|
+
|
|
86
|
+
const macro = macroSystem.stopRecording('Relative Macro', 'relative');
|
|
87
|
+
|
|
88
|
+
expect(macro.coordMode).toBe('relative');
|
|
89
|
+
expect(macro.originX).toBe(10);
|
|
90
|
+
expect(macro.originY).toBe(20);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should replay a macro in absolute mode dispatching commands', () => {
|
|
94
|
+
// Record
|
|
95
|
+
macroSystem.startRecording();
|
|
96
|
+
makeAndDispatch('draw_pixel', { x: 5, y: 10 });
|
|
97
|
+
const macro = macroSystem.stopRecording('Abs Macro', 'absolute');
|
|
98
|
+
|
|
99
|
+
// Replay -- should dispatch through the dispatcher
|
|
100
|
+
macroSystem.replayMacro(macro);
|
|
101
|
+
|
|
102
|
+
// The macro's commands were dispatched (we can check the macro system
|
|
103
|
+
// does not throw and commands exist)
|
|
104
|
+
expect(macro.commands).toHaveLength(1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should replay a macro in relative mode with coordinate offset', () => {
|
|
108
|
+
// Record at origin (10, 20)
|
|
109
|
+
macroSystem.startRecording();
|
|
110
|
+
makeAndDispatch('draw_pixel', { x: 10, y: 20 });
|
|
111
|
+
makeAndDispatch('draw_pixel', { x: 12, y: 22 });
|
|
112
|
+
const macro = macroSystem.stopRecording('Rel Macro', 'relative');
|
|
113
|
+
|
|
114
|
+
// The macro's origin should be (10, 20)
|
|
115
|
+
expect(macro.originX).toBe(10);
|
|
116
|
+
expect(macro.originY).toBe(20);
|
|
117
|
+
|
|
118
|
+
// Verify coordinate offset logic directly
|
|
119
|
+
// Replay at (30, 40): shift = (30-10, 40-20) = (+20, +20)
|
|
120
|
+
const shifted = offsetParams({ x: 10, y: 20 }, 20, 20);
|
|
121
|
+
expect(shifted["x"]).toBe(30);
|
|
122
|
+
expect(shifted["y"]).toBe(40);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should correctly shift coordinate params including x0, y0, x1, y1, cx, cy', () => {
|
|
126
|
+
const params = { x: 0, y: 0, x0: 5, y0: 10, x1: 15, y1: 20, cx: 10, cy: 15 };
|
|
127
|
+
const shifted = offsetParams(params, 3, 7);
|
|
128
|
+
|
|
129
|
+
expect(shifted["x"]).toBe(3);
|
|
130
|
+
expect(shifted["y"]).toBe(7);
|
|
131
|
+
expect(shifted["x0"]).toBe(8);
|
|
132
|
+
expect(shifted["y0"]).toBe(17);
|
|
133
|
+
expect(shifted["x1"]).toBe(18);
|
|
134
|
+
expect(shifted["y1"]).toBe(27);
|
|
135
|
+
expect(shifted["cx"]).toBe(13);
|
|
136
|
+
expect(shifted["cy"]).toBe(22);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should offset pixels array entries in relative mode', () => {
|
|
140
|
+
const params = {
|
|
141
|
+
pixels: [
|
|
142
|
+
{ x: 0, y: 0, color: '#ff0000' },
|
|
143
|
+
{ x: 1, y: 2, color: '#00ff00' },
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
const shifted = offsetParams(params, 10, 20);
|
|
147
|
+
const pixels = shifted["pixels"] as Array<{ x: number; y: number; color: string }>;
|
|
148
|
+
|
|
149
|
+
expect(pixels[0]?.x).toBe(10);
|
|
150
|
+
expect(pixels[0]?.y).toBe(20);
|
|
151
|
+
expect(pixels[0]?.color).toBe('#ff0000');
|
|
152
|
+
expect(pixels[1]?.x).toBe(11);
|
|
153
|
+
expect(pixels[1]?.y).toBe(22);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should cancel recording and discard commands', () => {
|
|
157
|
+
macroSystem.startRecording();
|
|
158
|
+
makeAndDispatch('draw_pixel', { x: 1, y: 1 });
|
|
159
|
+
makeAndDispatch('draw_pixel', { x: 2, y: 2 });
|
|
160
|
+
|
|
161
|
+
macroSystem.cancelRecording();
|
|
162
|
+
|
|
163
|
+
expect(macroSystem.recording).toBe(false);
|
|
164
|
+
expect(macroSystem.recordedCommands).toHaveLength(0);
|
|
165
|
+
expect(macroSystem.macros).toHaveLength(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should delete a macro by ID', () => {
|
|
169
|
+
macroSystem.startRecording();
|
|
170
|
+
makeAndDispatch('draw_pixel', { x: 1, y: 1 });
|
|
171
|
+
const macro = macroSystem.stopRecording('Delete Me', 'absolute');
|
|
172
|
+
|
|
173
|
+
expect(macroSystem.macros).toHaveLength(1);
|
|
174
|
+
|
|
175
|
+
macroSystem.deleteMacro(macro.id);
|
|
176
|
+
expect(macroSystem.macros).toHaveLength(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should rename a macro', () => {
|
|
180
|
+
macroSystem.startRecording();
|
|
181
|
+
makeAndDispatch('draw_pixel', { x: 1, y: 1 });
|
|
182
|
+
const macro = macroSystem.stopRecording('Old Name', 'absolute');
|
|
183
|
+
|
|
184
|
+
macroSystem.renameMacro(macro.id, 'New Name');
|
|
185
|
+
|
|
186
|
+
expect(macroSystem.macros[0]?.name).toBe('New Name');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should serialize and deserialize macros (roundtrip)', () => {
|
|
190
|
+
// Create two macros
|
|
191
|
+
macroSystem.startRecording();
|
|
192
|
+
makeAndDispatch('draw_pixel', { x: 5, y: 10 });
|
|
193
|
+
macroSystem.stopRecording('Macro A', 'absolute');
|
|
194
|
+
|
|
195
|
+
macroSystem.startRecording();
|
|
196
|
+
makeAndDispatch('draw_line', { x0: 0, y0: 0, x1: 10, y1: 10 });
|
|
197
|
+
macroSystem.stopRecording('Macro B', 'relative');
|
|
198
|
+
|
|
199
|
+
const serialized = macroSystem.serialize();
|
|
200
|
+
|
|
201
|
+
// Reset and restore
|
|
202
|
+
macroSystem._resetForTesting();
|
|
203
|
+
expect(macroSystem.macros).toHaveLength(0);
|
|
204
|
+
|
|
205
|
+
macroSystem.deserialize(serialized);
|
|
206
|
+
expect(macroSystem.macros).toHaveLength(2);
|
|
207
|
+
expect(macroSystem.macros[0]?.name).toBe('Macro A');
|
|
208
|
+
expect(macroSystem.macros[0]?.coordMode).toBe('absolute');
|
|
209
|
+
expect(macroSystem.macros[1]?.name).toBe('Macro B');
|
|
210
|
+
expect(macroSystem.macros[1]?.coordMode).toBe('relative');
|
|
211
|
+
expect(macroSystem.macros[1]?.commands[0]?.params["x0"]).toBe(0);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should not record commands when not in recording mode', () => {
|
|
215
|
+
// Dispatch without starting recording
|
|
216
|
+
makeAndDispatch('draw_pixel', { x: 1, y: 1 });
|
|
217
|
+
|
|
218
|
+
// Start recording, then stop immediately
|
|
219
|
+
macroSystem.startRecording();
|
|
220
|
+
const macro = macroSystem.stopRecording('Empty', 'absolute');
|
|
221
|
+
|
|
222
|
+
expect(macro.commands).toHaveLength(0);
|
|
223
|
+
});
|
|
224
|
+
});
|