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,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec Export/Import -- serializes the semantic action log to/from a
|
|
3
|
+
* portable JSON document format.
|
|
4
|
+
*
|
|
5
|
+
* The spec format captures the complete sequence of commands needed to
|
|
6
|
+
* reproduce a pixel art sprite from scratch. This enables:
|
|
7
|
+
*
|
|
8
|
+
* - Saving and loading command histories
|
|
9
|
+
* - Sharing reproducible "recipes" for sprite creation
|
|
10
|
+
* - Diffing two specs to see what changed
|
|
11
|
+
* - AI-assisted generation by producing spec documents
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ActionLogEntry } from './action-log.svelte.js';
|
|
15
|
+
|
|
16
|
+
// --- Types ---
|
|
17
|
+
|
|
18
|
+
export interface SpecDocument {
|
|
19
|
+
version: string;
|
|
20
|
+
canvasWidth: number;
|
|
21
|
+
canvasHeight: number;
|
|
22
|
+
commands: SpecCommand[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SpecCommand {
|
|
26
|
+
type: string;
|
|
27
|
+
description: string;
|
|
28
|
+
params: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SpecDiff {
|
|
32
|
+
type: 'added' | 'removed' | 'modified';
|
|
33
|
+
index: number;
|
|
34
|
+
command?: SpecCommand; // for 'added'
|
|
35
|
+
oldCommand?: SpecCommand; // for 'removed' / 'modified'
|
|
36
|
+
newCommand?: SpecCommand; // for 'modified'
|
|
37
|
+
changes?: string[]; // list of changed param names (for 'modified')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- Current spec format version ---
|
|
41
|
+
const SPEC_VERSION = '1.0.0';
|
|
42
|
+
|
|
43
|
+
// --- Export ---
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Export the action log entries as a spec document.
|
|
47
|
+
* Only enabled entries are included in the export.
|
|
48
|
+
*/
|
|
49
|
+
export function exportSpec(
|
|
50
|
+
entries: ActionLogEntry[],
|
|
51
|
+
canvasWidth: number,
|
|
52
|
+
canvasHeight: number,
|
|
53
|
+
): SpecDocument {
|
|
54
|
+
return {
|
|
55
|
+
version: SPEC_VERSION,
|
|
56
|
+
canvasWidth,
|
|
57
|
+
canvasHeight,
|
|
58
|
+
commands: entries
|
|
59
|
+
.filter((e) => e.enabled)
|
|
60
|
+
.map((e) => ({
|
|
61
|
+
type: e.type,
|
|
62
|
+
description: e.description,
|
|
63
|
+
// Strip internal/undo-related params (those starting with underscore)
|
|
64
|
+
params: filterInternalParams(e.params),
|
|
65
|
+
})),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Import ---
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Import a spec document into a sequence of commands suitable for replay.
|
|
73
|
+
* Validates the spec structure and returns the command list.
|
|
74
|
+
*
|
|
75
|
+
* Accepts `null`/`undefined` so callers that hand us unvalidated JSON
|
|
76
|
+
* (the spec-format tests, and history-commands when the dispatcher did
|
|
77
|
+
* not pre-validate) still hit the structural check instead of crashing.
|
|
78
|
+
*/
|
|
79
|
+
export function importSpec(
|
|
80
|
+
spec: SpecDocument | null | undefined,
|
|
81
|
+
): { type: string; params: Record<string, unknown> }[] {
|
|
82
|
+
if (!spec || !Array.isArray(spec.commands)) {
|
|
83
|
+
throw new Error('Invalid spec document: missing commands array');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return spec.commands.map((cmd) => ({
|
|
87
|
+
type: cmd.type,
|
|
88
|
+
params: { ...cmd.params },
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Diff ---
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Diff two spec documents and return a list of differences.
|
|
96
|
+
*
|
|
97
|
+
* Uses a simple index-based comparison (not LCS/Myers diff) since spec
|
|
98
|
+
* commands are typically short ordered lists. For each index position:
|
|
99
|
+
* - If spec1 has a command but spec2 doesn't: 'removed'
|
|
100
|
+
* - If spec2 has a command but spec1 doesn't: 'added'
|
|
101
|
+
* - If both have commands but they differ: 'modified'
|
|
102
|
+
*/
|
|
103
|
+
export function diffSpecs(
|
|
104
|
+
spec1: SpecDocument,
|
|
105
|
+
spec2: SpecDocument,
|
|
106
|
+
): SpecDiff[] {
|
|
107
|
+
const diffs: SpecDiff[] = [];
|
|
108
|
+
const maxLen = Math.max(spec1.commands.length, spec2.commands.length);
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < maxLen; i++) {
|
|
111
|
+
const cmd1 = spec1.commands[i];
|
|
112
|
+
const cmd2 = spec2.commands[i];
|
|
113
|
+
|
|
114
|
+
if (!cmd1 && cmd2) {
|
|
115
|
+
// Command added in spec2
|
|
116
|
+
diffs.push({
|
|
117
|
+
type: 'added',
|
|
118
|
+
index: i,
|
|
119
|
+
command: cmd2,
|
|
120
|
+
});
|
|
121
|
+
} else if (cmd1 && !cmd2) {
|
|
122
|
+
// Command removed in spec2
|
|
123
|
+
diffs.push({
|
|
124
|
+
type: 'removed',
|
|
125
|
+
index: i,
|
|
126
|
+
oldCommand: cmd1,
|
|
127
|
+
});
|
|
128
|
+
} else if (cmd1 && cmd2) {
|
|
129
|
+
// Both exist -- check if they differ
|
|
130
|
+
const changes = findParamChanges(cmd1, cmd2);
|
|
131
|
+
if (cmd1.type !== cmd2.type || changes.length > 0) {
|
|
132
|
+
diffs.push({
|
|
133
|
+
type: 'modified',
|
|
134
|
+
index: i,
|
|
135
|
+
oldCommand: cmd1,
|
|
136
|
+
newCommand: cmd2,
|
|
137
|
+
changes: cmd1.type !== cmd2.type
|
|
138
|
+
? ['type', ...changes]
|
|
139
|
+
: changes,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return diffs;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Internal helpers ---
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Filter out internal params (underscore-prefixed) that are used by the
|
|
152
|
+
* undo system but should not be part of the exported spec.
|
|
153
|
+
*/
|
|
154
|
+
function filterInternalParams(params: Record<string, unknown>): Record<string, unknown> {
|
|
155
|
+
const filtered: Record<string, unknown> = {};
|
|
156
|
+
for (const [key, value] of Object.entries(params)) {
|
|
157
|
+
if (!key.startsWith('_')) {
|
|
158
|
+
filtered[key] = value;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return filtered;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Compare two spec commands' params and return the list of param names
|
|
166
|
+
* that differ between them.
|
|
167
|
+
*/
|
|
168
|
+
function findParamChanges(cmd1: SpecCommand, cmd2: SpecCommand): string[] {
|
|
169
|
+
const changes: string[] = [];
|
|
170
|
+
const allKeys = new Set([
|
|
171
|
+
...Object.keys(cmd1.params),
|
|
172
|
+
...Object.keys(cmd2.params),
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
for (const key of allKeys) {
|
|
176
|
+
const v1 = cmd1.params[key];
|
|
177
|
+
const v2 = cmd2.params[key];
|
|
178
|
+
if (!deepEqual(v1, v2)) {
|
|
179
|
+
changes.push(key);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return changes;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Simple deep equality check for JSON-serializable values.
|
|
188
|
+
* Sufficient for command params which are plain objects/arrays/primitives.
|
|
189
|
+
*/
|
|
190
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
191
|
+
if (a === b) return true;
|
|
192
|
+
if (a === null || b === null) return false;
|
|
193
|
+
if (typeof a !== typeof b) return false;
|
|
194
|
+
|
|
195
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
196
|
+
if (a.length !== b.length) return false;
|
|
197
|
+
return a.every((val, idx) => deepEqual(val, b[idx]));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
201
|
+
const objA = a as Record<string, unknown>;
|
|
202
|
+
const objB = b as Record<string, unknown>;
|
|
203
|
+
const keysA = Object.keys(objA);
|
|
204
|
+
const keysB = Object.keys(objB);
|
|
205
|
+
if (keysA.length !== keysB.length) return false;
|
|
206
|
+
return keysA.every((key) => deepEqual(objA[key], objB[key]));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SeamCheckerPanel -- dockable right-sidebar panel that inspects the
|
|
4
|
+
* current render buffer for tile-edge mismatches.
|
|
5
|
+
*
|
|
6
|
+
* Calls checkSeams() from seam-checker.ts whenever the composited render
|
|
7
|
+
* buffer changes, and displays the results as a scrollable list with
|
|
8
|
+
* color swatches for each mismatched pixel pair.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { checkSeams, type SeamIssue } from './seam-checker.js';
|
|
12
|
+
import { getRenderBuffer } from '../canvas/render-state.svelte.js';
|
|
13
|
+
import RefreshCwIcon from '~icons/lucide/refresh-cw';
|
|
14
|
+
|
|
15
|
+
// --- Seam analysis state ---
|
|
16
|
+
|
|
17
|
+
/** Manually incremented to force a re-check even when the buffer ref is the same. */
|
|
18
|
+
let refreshTick = $state(0);
|
|
19
|
+
|
|
20
|
+
/** The list of detected seam issues, derived from the current render buffer. */
|
|
21
|
+
let issues = $derived.by((): SeamIssue[] => {
|
|
22
|
+
// Read refreshTick so manual refreshes trigger a re-derive.
|
|
23
|
+
void refreshTick;
|
|
24
|
+
const buffer = getRenderBuffer();
|
|
25
|
+
if (!buffer) return [];
|
|
26
|
+
return checkSeams(buffer);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** Human-readable label for edge names (includes the opposite edge for context). */
|
|
30
|
+
function edgeLabel(edge: SeamIssue['edge']): string {
|
|
31
|
+
switch (edge) {
|
|
32
|
+
case 'top': return 'Top / Bottom';
|
|
33
|
+
case 'left': return 'Left / Right';
|
|
34
|
+
// checkSeams only emits 'top' and 'left', but handle others defensively.
|
|
35
|
+
case 'bottom': return 'Bottom / Top';
|
|
36
|
+
case 'right': return 'Right / Left';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function handleRefresh(): void {
|
|
41
|
+
refreshTick++;
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<div class="seam-panel">
|
|
46
|
+
<!-- Header -->
|
|
47
|
+
<div class="seam-header">
|
|
48
|
+
<span class="seam-title">Seam Checker</span>
|
|
49
|
+
<div class="seam-actions">
|
|
50
|
+
<button
|
|
51
|
+
class="action-btn"
|
|
52
|
+
title="Refresh"
|
|
53
|
+
aria-label="Refresh seam check"
|
|
54
|
+
onclick={handleRefresh}
|
|
55
|
+
><RefreshCwIcon /></button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<!-- Results -->
|
|
60
|
+
<div class="seam-body">
|
|
61
|
+
{#if !getRenderBuffer()}
|
|
62
|
+
<p class="seam-empty">No canvas data available.</p>
|
|
63
|
+
{:else if issues.length === 0}
|
|
64
|
+
<p class="seam-success">No seam issues found.</p>
|
|
65
|
+
{:else}
|
|
66
|
+
<p class="seam-summary">{issues.length} mismatch{issues.length === 1 ? '' : 'es'} detected</p>
|
|
67
|
+
|
|
68
|
+
<ul class="issue-list">
|
|
69
|
+
{#each issues as issue (`${issue.edge}-${String(issue.position)}`)}
|
|
70
|
+
<li class="issue-row">
|
|
71
|
+
<span class="issue-edge">{edgeLabel(issue.edge)}</span>
|
|
72
|
+
<span class="issue-pos">px {issue.position}</span>
|
|
73
|
+
<span class="swatch-pair">
|
|
74
|
+
<span
|
|
75
|
+
class="swatch"
|
|
76
|
+
style:background-color={issue.color1}
|
|
77
|
+
title={issue.color1}
|
|
78
|
+
></span>
|
|
79
|
+
<span class="swatch-sep"></span>
|
|
80
|
+
<span
|
|
81
|
+
class="swatch"
|
|
82
|
+
style:background-color={issue.color2}
|
|
83
|
+
title={issue.color2}
|
|
84
|
+
></span>
|
|
85
|
+
</span>
|
|
86
|
+
</li>
|
|
87
|
+
{/each}
|
|
88
|
+
</ul>
|
|
89
|
+
{/if}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<style>
|
|
94
|
+
.seam-panel {
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
width: 100%;
|
|
98
|
+
height: 100%;
|
|
99
|
+
background: var(--bg-panel);
|
|
100
|
+
color: var(--text-primary);
|
|
101
|
+
font-size: var(--text-base);
|
|
102
|
+
user-select: none;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.seam-header {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: space-between;
|
|
109
|
+
padding: 6px var(--space-3);
|
|
110
|
+
border-bottom: 1px solid var(--border);
|
|
111
|
+
background: var(--bg-toolbar);
|
|
112
|
+
flex-shrink: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.seam-title {
|
|
116
|
+
font-weight: 600;
|
|
117
|
+
font-size: var(--text-sm);
|
|
118
|
+
text-transform: uppercase;
|
|
119
|
+
letter-spacing: 0.5px;
|
|
120
|
+
color: var(--text-secondary);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.seam-actions {
|
|
124
|
+
display: flex;
|
|
125
|
+
gap: var(--space-1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.action-btn {
|
|
129
|
+
background: none;
|
|
130
|
+
border: 1px solid transparent;
|
|
131
|
+
border-radius: var(--radius-sm);
|
|
132
|
+
color: var(--text-secondary);
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
width: 24px;
|
|
135
|
+
height: 24px;
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: center;
|
|
139
|
+
font-size: var(--text-xl);
|
|
140
|
+
padding: 0;
|
|
141
|
+
line-height: 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.action-btn:hover {
|
|
145
|
+
background: var(--bg-primary);
|
|
146
|
+
color: var(--text-primary);
|
|
147
|
+
border-color: var(--border);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.action-btn :global(svg) {
|
|
151
|
+
width: 14px;
|
|
152
|
+
height: 14px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Scrollable body */
|
|
156
|
+
.seam-body {
|
|
157
|
+
flex: 1;
|
|
158
|
+
overflow-y: auto;
|
|
159
|
+
overflow-x: hidden;
|
|
160
|
+
padding: var(--space-2) var(--space-3);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.seam-empty,
|
|
164
|
+
.seam-success {
|
|
165
|
+
color: var(--text-secondary);
|
|
166
|
+
font-size: var(--text-sm);
|
|
167
|
+
margin: 0;
|
|
168
|
+
padding: var(--space-2) 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.seam-success {
|
|
172
|
+
color: var(--accent);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.seam-summary {
|
|
176
|
+
margin: 0 0 var(--space-2) 0;
|
|
177
|
+
font-size: var(--text-sm);
|
|
178
|
+
color: var(--text-secondary);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Issue list */
|
|
182
|
+
.issue-list {
|
|
183
|
+
list-style: none;
|
|
184
|
+
margin: 0;
|
|
185
|
+
padding: 0;
|
|
186
|
+
display: flex;
|
|
187
|
+
flex-direction: column;
|
|
188
|
+
gap: 2px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.issue-row {
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
gap: var(--space-2);
|
|
195
|
+
padding: 3px var(--space-2);
|
|
196
|
+
border-radius: var(--radius-sm);
|
|
197
|
+
font-size: var(--text-sm);
|
|
198
|
+
transition: background var(--transition-fast);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.issue-row:hover {
|
|
202
|
+
background: rgba(255, 255, 255, 0.04);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
:global([data-theme="light"]) .issue-row:hover {
|
|
206
|
+
background: rgba(0, 0, 0, 0.04);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.issue-edge {
|
|
210
|
+
flex-shrink: 0;
|
|
211
|
+
font-weight: 500;
|
|
212
|
+
min-width: 72px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.issue-pos {
|
|
216
|
+
color: var(--text-secondary);
|
|
217
|
+
flex-shrink: 0;
|
|
218
|
+
min-width: 36px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Color swatches */
|
|
222
|
+
.swatch-pair {
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
gap: 3px;
|
|
226
|
+
margin-left: auto;
|
|
227
|
+
flex-shrink: 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.swatch {
|
|
231
|
+
width: 14px;
|
|
232
|
+
height: 14px;
|
|
233
|
+
border: 1px solid var(--border);
|
|
234
|
+
border-radius: 2px;
|
|
235
|
+
/* Checkerboard to reveal alpha */
|
|
236
|
+
background-image:
|
|
237
|
+
linear-gradient(45deg, #808080 25%, transparent 25%),
|
|
238
|
+
linear-gradient(-45deg, #808080 25%, transparent 25%),
|
|
239
|
+
linear-gradient(45deg, transparent 75%, #808080 75%),
|
|
240
|
+
linear-gradient(-45deg, transparent 75%, #808080 75%);
|
|
241
|
+
background-size: 6px 6px;
|
|
242
|
+
background-position: 0 0, 0 3px, 3px -3px, -3px 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.swatch-sep {
|
|
246
|
+
width: 4px;
|
|
247
|
+
height: 1px;
|
|
248
|
+
background: var(--text-secondary);
|
|
249
|
+
flex-shrink: 0;
|
|
250
|
+
}
|
|
251
|
+
</style>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { screenToIso, isoToScreen, isoDepthCompare } from './iso-math.js';
|
|
3
|
+
|
|
4
|
+
describe('iso-math', () => {
|
|
5
|
+
const TW = 64; // tile width
|
|
6
|
+
const TH = 32; // tile height (standard 2:1 isometric)
|
|
7
|
+
|
|
8
|
+
describe('screenToIso / isoToScreen roundtrip', () => {
|
|
9
|
+
it('should roundtrip the origin tile (0, 0)', () => {
|
|
10
|
+
const screen = isoToScreen(0, 0, TW, TH);
|
|
11
|
+
const iso = screenToIso(screen.x, screen.y, TW, TH);
|
|
12
|
+
expect(iso.col).toBeCloseTo(0, 10);
|
|
13
|
+
expect(iso.row).toBeCloseTo(0, 10);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should roundtrip an arbitrary tile (3, 5)', () => {
|
|
17
|
+
const screen = isoToScreen(3, 5, TW, TH);
|
|
18
|
+
const iso = screenToIso(screen.x, screen.y, TW, TH);
|
|
19
|
+
expect(iso.col).toBeCloseTo(3, 10);
|
|
20
|
+
expect(iso.row).toBeCloseTo(5, 10);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should roundtrip with non-standard tile sizes', () => {
|
|
24
|
+
const tw = 48;
|
|
25
|
+
const th = 24;
|
|
26
|
+
const screen = isoToScreen(7, 2, tw, th);
|
|
27
|
+
const iso = screenToIso(screen.x, screen.y, tw, th);
|
|
28
|
+
expect(iso.col).toBeCloseTo(7, 10);
|
|
29
|
+
expect(iso.row).toBeCloseTo(2, 10);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should roundtrip from screen to iso and back', () => {
|
|
33
|
+
// Start from screen coords and verify roundtrip
|
|
34
|
+
const iso = screenToIso(100, 50, TW, TH);
|
|
35
|
+
const screen = isoToScreen(iso.col, iso.row, TW, TH);
|
|
36
|
+
expect(screen.x).toBeCloseTo(100, 10);
|
|
37
|
+
expect(screen.y).toBeCloseTo(50, 10);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('isoToScreen', () => {
|
|
42
|
+
it('should return (0, 0) for tile (0, 0)', () => {
|
|
43
|
+
const { x, y } = isoToScreen(0, 0, TW, TH);
|
|
44
|
+
expect(x).toBe(0);
|
|
45
|
+
expect(y).toBe(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should offset correctly for tile (1, 0) -- pure column shift', () => {
|
|
49
|
+
const { x, y } = isoToScreen(1, 0, TW, TH);
|
|
50
|
+
// col=1, row=0: x = (1-0)*32 = 32, y = (1+0)*16 = 16
|
|
51
|
+
expect(x).toBe(TW / 2);
|
|
52
|
+
expect(y).toBe(TH / 2);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('isoDepthCompare', () => {
|
|
57
|
+
it('should sort objects with smaller col+row sum first (farther from camera)', () => {
|
|
58
|
+
const far = { col: 0, row: 0, height: 0 };
|
|
59
|
+
const near = { col: 3, row: 2, height: 0 };
|
|
60
|
+
expect(isoDepthCompare(far, near)).toBeLessThan(0);
|
|
61
|
+
expect(isoDepthCompare(near, far)).toBeGreaterThan(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should sort lower height first when col+row sums are equal', () => {
|
|
65
|
+
const low = { col: 2, row: 3, height: 0 };
|
|
66
|
+
const high = { col: 2, row: 3, height: 2 };
|
|
67
|
+
expect(isoDepthCompare(low, high)).toBeLessThan(0);
|
|
68
|
+
expect(isoDepthCompare(high, low)).toBeGreaterThan(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should return 0 for identical positions and heights', () => {
|
|
72
|
+
const a = { col: 1, row: 1, height: 1 };
|
|
73
|
+
const b = { col: 1, row: 1, height: 1 };
|
|
74
|
+
expect(isoDepthCompare(a, b)).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isometric Math -- coordinate conversions and depth sorting for isometric grids.
|
|
3
|
+
*
|
|
4
|
+
* Uses the standard 2:1 isometric projection where tile width is twice tile height.
|
|
5
|
+
* Screen origin (0,0) is the top-center of the grid; columns go right-down,
|
|
6
|
+
* rows go left-down.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Result of converting screen coordinates to isometric tile coordinates. */
|
|
10
|
+
export interface IsoCoord {
|
|
11
|
+
col: number;
|
|
12
|
+
row: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Result of converting isometric tile coordinates to screen position. */
|
|
16
|
+
export interface ScreenCoord {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Object with isometric position and elevation, used for depth sorting. */
|
|
22
|
+
export interface IsoObject {
|
|
23
|
+
col: number;
|
|
24
|
+
row: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convert screen coordinates to isometric tile coordinates.
|
|
30
|
+
*
|
|
31
|
+
* Returns fractional col/row -- caller should Math.floor() for tile lookup.
|
|
32
|
+
* The formulas invert the standard isometric projection matrix:
|
|
33
|
+
* screenX = (col - row) * (tileWidth / 2)
|
|
34
|
+
* screenY = (col + row) * (tileHeight / 2)
|
|
35
|
+
*/
|
|
36
|
+
export function screenToIso(
|
|
37
|
+
screenX: number,
|
|
38
|
+
screenY: number,
|
|
39
|
+
tileWidth: number,
|
|
40
|
+
tileHeight: number,
|
|
41
|
+
): IsoCoord {
|
|
42
|
+
const halfW = tileWidth / 2;
|
|
43
|
+
const halfH = tileHeight / 2;
|
|
44
|
+
const col = (screenX / halfW + screenY / halfH) / 2;
|
|
45
|
+
const row = (screenY / halfH - screenX / halfW) / 2;
|
|
46
|
+
return { col, row };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Convert isometric tile coordinates to screen position.
|
|
51
|
+
*
|
|
52
|
+
* Returns the top-center anchor point of the tile diamond.
|
|
53
|
+
*/
|
|
54
|
+
export function isoToScreen(
|
|
55
|
+
col: number,
|
|
56
|
+
row: number,
|
|
57
|
+
tileWidth: number,
|
|
58
|
+
tileHeight: number,
|
|
59
|
+
): ScreenCoord {
|
|
60
|
+
const x = (col - row) * (tileWidth / 2);
|
|
61
|
+
const y = (col + row) * (tileHeight / 2);
|
|
62
|
+
return { x, y };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Depth sort comparator for isometric objects.
|
|
67
|
+
*
|
|
68
|
+
* Objects with smaller (col + row) are farther from the camera and should
|
|
69
|
+
* be rendered first. When sums are equal, lower height renders first.
|
|
70
|
+
* Returns negative if `a` should render before `b`.
|
|
71
|
+
*/
|
|
72
|
+
export function isoDepthCompare(a: IsoObject, b: IsoObject): number {
|
|
73
|
+
const sumA = a.col + a.row;
|
|
74
|
+
const sumB = b.col + b.row;
|
|
75
|
+
if (sumA !== sumB) return sumA - sumB;
|
|
76
|
+
return a.height - b.height;
|
|
77
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seam Checker Panel Plugin -- registers SeamCheckerPanel as a dockable
|
|
3
|
+
* panel in the right sidebar. Shows tile-edge mismatches for the current
|
|
4
|
+
* composited render buffer.
|
|
5
|
+
*
|
|
6
|
+
* Discovered by bootstrap via the `*-plugin.ts` glob.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PluginModule } from '../core/plugin-loader.js';
|
|
10
|
+
import SeamCheckerPanel from './SeamCheckerPanel.svelte';
|
|
11
|
+
|
|
12
|
+
export const seamCheckerPanelPlugin: PluginModule = {
|
|
13
|
+
name: 'ui/seam-checker-panel',
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
dependencies: [],
|
|
16
|
+
register(api) {
|
|
17
|
+
api.addPanel('seam-checker', {
|
|
18
|
+
title: 'Seam Checker',
|
|
19
|
+
component: SeamCheckerPanel,
|
|
20
|
+
position: 'right',
|
|
21
|
+
minWidth: 160,
|
|
22
|
+
maxWidth: 300,
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
};
|