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,189 @@
|
|
|
1
|
+
"""MCP Bridge -- HTTP client that forwards MCP state to/from the collab server.
|
|
2
|
+
|
|
3
|
+
Instead of the MCP server managing its own isolated state, every tool
|
|
4
|
+
execution round-trips through the collaboration server's REST API:
|
|
5
|
+
|
|
6
|
+
1. Before execution: pull the latest state from the collab server
|
|
7
|
+
2. Execute the tool against the local (synced) state
|
|
8
|
+
3. After execution: push the modified state back, triggering a
|
|
9
|
+
broadcast to all WebSocket clients (the frontend)
|
|
10
|
+
|
|
11
|
+
This ensures the collab server remains the single source of truth and
|
|
12
|
+
that every MCP mutation is visible to the frontend in real time.
|
|
13
|
+
|
|
14
|
+
The collab server must be running on COLLAB_SERVER_URL for MCP to work.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import base64
|
|
20
|
+
import logging
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
COLLAB_SERVER_URL = "http://localhost:7779"
|
|
28
|
+
|
|
29
|
+
# Reusable client -- created lazily, lives for the process lifetime.
|
|
30
|
+
# The MCP stdio server is single-threaded so no concurrency concerns.
|
|
31
|
+
_client: httpx.AsyncClient | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_client() -> httpx.AsyncClient:
|
|
35
|
+
"""Return (and lazily create) the shared HTTP client."""
|
|
36
|
+
global _client
|
|
37
|
+
if _client is None:
|
|
38
|
+
_client = httpx.AsyncClient(base_url=COLLAB_SERVER_URL, timeout=30.0)
|
|
39
|
+
return _client
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CollabServerUnreachableError(Exception):
|
|
43
|
+
"""Raised when the collab server is not reachable."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def health_check() -> bool:
|
|
47
|
+
"""Return True if the collab server is reachable."""
|
|
48
|
+
try:
|
|
49
|
+
resp = await _get_client().get("/health")
|
|
50
|
+
return resp.status_code == 200
|
|
51
|
+
except httpx.HTTPError:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def pull_full_state() -> dict[str, Any]:
|
|
56
|
+
"""Fetch full serialized state from the collab server.
|
|
57
|
+
|
|
58
|
+
Returns the dict produced by GET /api/state/full.
|
|
59
|
+
Raises CollabServerUnreachableError if the server cannot be reached.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
resp = await _get_client().get("/api/state/full")
|
|
63
|
+
resp.raise_for_status()
|
|
64
|
+
return resp.json()
|
|
65
|
+
except httpx.HTTPError as exc:
|
|
66
|
+
raise CollabServerUnreachableError(
|
|
67
|
+
f"Cannot reach collab server at {COLLAB_SERVER_URL}: {exc}"
|
|
68
|
+
) from exc
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def push_full_state(state_dict: dict[str, Any]) -> None:
|
|
72
|
+
"""Push full serialized state to the collab server and trigger broadcast.
|
|
73
|
+
|
|
74
|
+
Sends POST /api/state/sync with the full state payload.
|
|
75
|
+
Raises CollabServerUnreachableError if the server cannot be reached.
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
resp = await _get_client().post("/api/state/sync", json=state_dict)
|
|
79
|
+
resp.raise_for_status()
|
|
80
|
+
except httpx.HTTPError as exc:
|
|
81
|
+
raise CollabServerUnreachableError(
|
|
82
|
+
f"Cannot reach collab server at {COLLAB_SERVER_URL}: {exc}"
|
|
83
|
+
) from exc
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def serialize_state(state: Any) -> dict[str, Any]:
|
|
87
|
+
"""Serialize a ServerState into a dict suitable for push_full_state.
|
|
88
|
+
|
|
89
|
+
Includes pixel data as base64-encoded strings so the JSON payload
|
|
90
|
+
stays valid (raw bytes are not JSON-serializable).
|
|
91
|
+
"""
|
|
92
|
+
projects: dict[str, Any] = {}
|
|
93
|
+
for name, project in state.projects.items():
|
|
94
|
+
canvases: dict[str, Any] = {}
|
|
95
|
+
for cname, canvas in project.canvases.items():
|
|
96
|
+
layers_serialized = [dict(layer) for layer in canvas.layers]
|
|
97
|
+
# Serialize ALL frames with their per-layer pixel data
|
|
98
|
+
frames_serialized = [
|
|
99
|
+
{
|
|
100
|
+
"id": frame.id,
|
|
101
|
+
"duration_ms": frame.duration_ms,
|
|
102
|
+
"pixel_data": {
|
|
103
|
+
layer_id: base64.b64encode(data).decode("ascii")
|
|
104
|
+
for layer_id, data in frame.pixel_data.items()
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
for frame in canvas.frames
|
|
108
|
+
]
|
|
109
|
+
canvases[cname] = {
|
|
110
|
+
"name": canvas.name,
|
|
111
|
+
"width": canvas.width,
|
|
112
|
+
"height": canvas.height,
|
|
113
|
+
"layers": layers_serialized,
|
|
114
|
+
"frames": frames_serialized,
|
|
115
|
+
"current_frame_index": canvas.current_frame_index,
|
|
116
|
+
"global_fps": canvas.global_fps,
|
|
117
|
+
}
|
|
118
|
+
projects[name] = {
|
|
119
|
+
"name": project.name,
|
|
120
|
+
"width": project.width,
|
|
121
|
+
"height": project.height,
|
|
122
|
+
"canvases": canvases,
|
|
123
|
+
"command_history": project.command_history,
|
|
124
|
+
"redo_stack": project.redo_stack,
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
"projects": projects,
|
|
128
|
+
"active_project": state.active_project,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def deserialize_into_state(state: Any, data: dict[str, Any]) -> None:
|
|
133
|
+
"""Overwrite a ServerState's contents from a serialized dict.
|
|
134
|
+
|
|
135
|
+
Inverse of serialize_state: restores projects, canvases, layers,
|
|
136
|
+
pixel data, and history from the dict pulled from the collab server.
|
|
137
|
+
"""
|
|
138
|
+
import uuid
|
|
139
|
+
|
|
140
|
+
from pixelweaver.state import CanvasState, FrameState, ProjectState
|
|
141
|
+
|
|
142
|
+
state.projects.clear()
|
|
143
|
+
state.active_project = data.get("active_project")
|
|
144
|
+
|
|
145
|
+
for name, pdata in data.get("projects", {}).items():
|
|
146
|
+
project = ProjectState(
|
|
147
|
+
name=pdata["name"],
|
|
148
|
+
width=pdata["width"],
|
|
149
|
+
height=pdata["height"],
|
|
150
|
+
command_history=pdata.get("command_history", []),
|
|
151
|
+
redo_stack=pdata.get("redo_stack", []),
|
|
152
|
+
)
|
|
153
|
+
for cname, cdata in pdata.get("canvases", {}).items():
|
|
154
|
+
# Reconstruct frames from serialized data
|
|
155
|
+
if "frames" in cdata:
|
|
156
|
+
# New format: full frame list with per-frame pixel data
|
|
157
|
+
frames: list[FrameState] = []
|
|
158
|
+
for fdata in cdata["frames"]:
|
|
159
|
+
pixel_data: dict[str, bytes] = {
|
|
160
|
+
lid: base64.b64decode(b64)
|
|
161
|
+
for lid, b64 in fdata.get("pixel_data", {}).items()
|
|
162
|
+
}
|
|
163
|
+
frames.append(FrameState(
|
|
164
|
+
id=fdata["id"],
|
|
165
|
+
duration_ms=fdata.get("duration_ms"),
|
|
166
|
+
pixel_data=pixel_data,
|
|
167
|
+
))
|
|
168
|
+
else:
|
|
169
|
+
# Legacy format: flat pixel_data dict, single frame
|
|
170
|
+
pixel_data_legacy: dict[str, bytes] = {
|
|
171
|
+
lid: base64.b64decode(b64)
|
|
172
|
+
for lid, b64 in cdata.get("pixel_data", {}).items()
|
|
173
|
+
}
|
|
174
|
+
frames = [FrameState(
|
|
175
|
+
id=str(uuid.uuid4()),
|
|
176
|
+
pixel_data=pixel_data_legacy,
|
|
177
|
+
)]
|
|
178
|
+
|
|
179
|
+
canvas = CanvasState(
|
|
180
|
+
name=cdata["name"],
|
|
181
|
+
width=cdata["width"],
|
|
182
|
+
height=cdata["height"],
|
|
183
|
+
layers=cdata.get("layers", []),
|
|
184
|
+
frames=frames,
|
|
185
|
+
current_frame_index=cdata.get("current_frame_index", 0),
|
|
186
|
+
global_fps=cdata.get("global_fps", 12.0),
|
|
187
|
+
)
|
|
188
|
+
project.canvases[cname] = canvas
|
|
189
|
+
state.projects[name] = project
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Drawing tools for MCP registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .mcp_registry import MCPCommandRegistry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register_drawing_tools(registry: MCPCommandRegistry) -> None:
|
|
12
|
+
"""Register all drawing-related MCP tools."""
|
|
13
|
+
drawing_tools = [
|
|
14
|
+
("draw_pixels", "Draw pixels at specified coordinates with a given color", {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"properties": {
|
|
17
|
+
"pixels": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"items": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"x": {"type": "integer"},
|
|
23
|
+
"y": {"type": "integer"},
|
|
24
|
+
},
|
|
25
|
+
"required": ["x", "y"],
|
|
26
|
+
},
|
|
27
|
+
"description": "Array of {x, y} pixel positions",
|
|
28
|
+
},
|
|
29
|
+
"color": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "RGBA hex color, e.g. '#ff0000ff'",
|
|
32
|
+
},
|
|
33
|
+
"layer_id": {"type": "string", "description": "Target layer ID"},
|
|
34
|
+
},
|
|
35
|
+
"required": ["pixels", "color"],
|
|
36
|
+
}),
|
|
37
|
+
("erase_pixels", "Erase pixels at specified coordinates (set to transparent)", {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"pixels": {
|
|
41
|
+
"type": "array",
|
|
42
|
+
"items": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"properties": {
|
|
45
|
+
"x": {"type": "integer"},
|
|
46
|
+
"y": {"type": "integer"},
|
|
47
|
+
},
|
|
48
|
+
"required": ["x", "y"],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
"layer_id": {"type": "string"},
|
|
52
|
+
},
|
|
53
|
+
"required": ["pixels"],
|
|
54
|
+
}),
|
|
55
|
+
("flood_fill", "Fill a contiguous area with a color starting from a seed point", {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"x": {"type": "integer", "description": "Seed X coordinate"},
|
|
59
|
+
"y": {"type": "integer", "description": "Seed Y coordinate"},
|
|
60
|
+
"color": {"type": "string", "description": "Fill color (RGBA hex)"},
|
|
61
|
+
"tolerance": {
|
|
62
|
+
"type": "integer",
|
|
63
|
+
"description": "Color match tolerance (0-255)",
|
|
64
|
+
"default": 0,
|
|
65
|
+
},
|
|
66
|
+
"layer_id": {"type": "string"},
|
|
67
|
+
},
|
|
68
|
+
"required": ["x", "y", "color"],
|
|
69
|
+
}),
|
|
70
|
+
("draw_line", "Draw a line between two points", {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"properties": {
|
|
73
|
+
"x1": {"type": "integer"},
|
|
74
|
+
"y1": {"type": "integer"},
|
|
75
|
+
"x2": {"type": "integer"},
|
|
76
|
+
"y2": {"type": "integer"},
|
|
77
|
+
"color": {"type": "string"},
|
|
78
|
+
"thickness": {"type": "integer", "default": 1},
|
|
79
|
+
"layer_id": {"type": "string"},
|
|
80
|
+
},
|
|
81
|
+
"required": ["x1", "y1", "x2", "y2", "color"],
|
|
82
|
+
}),
|
|
83
|
+
("draw_rect", "Draw a rectangle (filled or outline)", {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"properties": {
|
|
86
|
+
"x": {"type": "integer"},
|
|
87
|
+
"y": {"type": "integer"},
|
|
88
|
+
"width": {"type": "integer"},
|
|
89
|
+
"height": {"type": "integer"},
|
|
90
|
+
"color": {"type": "string"},
|
|
91
|
+
"filled": {"type": "boolean", "default": True},
|
|
92
|
+
"layer_id": {"type": "string"},
|
|
93
|
+
},
|
|
94
|
+
"required": ["x", "y", "width", "height", "color"],
|
|
95
|
+
}),
|
|
96
|
+
("draw_ellipse", "Draw an ellipse (filled or outline)", {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"cx": {"type": "integer", "description": "Center X"},
|
|
100
|
+
"cy": {"type": "integer", "description": "Center Y"},
|
|
101
|
+
"rx": {"type": "integer", "description": "Radius X"},
|
|
102
|
+
"ry": {"type": "integer", "description": "Radius Y"},
|
|
103
|
+
"color": {"type": "string"},
|
|
104
|
+
"filled": {"type": "boolean", "default": True},
|
|
105
|
+
"layer_id": {"type": "string"},
|
|
106
|
+
},
|
|
107
|
+
"required": ["cx", "cy", "rx", "ry", "color"],
|
|
108
|
+
}),
|
|
109
|
+
("draw_diamond", "Draw a diamond shape", {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"properties": {
|
|
112
|
+
"cx": {"type": "integer", "description": "Center X"},
|
|
113
|
+
"cy": {"type": "integer", "description": "Center Y"},
|
|
114
|
+
"rx": {"type": "integer", "description": "Half-width"},
|
|
115
|
+
"ry": {"type": "integer", "description": "Half-height"},
|
|
116
|
+
"color": {"type": "string"},
|
|
117
|
+
"filled": {"type": "boolean", "default": True},
|
|
118
|
+
"layer_id": {"type": "string"},
|
|
119
|
+
},
|
|
120
|
+
"required": ["cx", "cy", "rx", "ry", "color"],
|
|
121
|
+
}),
|
|
122
|
+
("draw_gradient", "Draw a gradient between two colors", {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"properties": {
|
|
125
|
+
"x": {"type": "integer"},
|
|
126
|
+
"y": {"type": "integer"},
|
|
127
|
+
"width": {"type": "integer"},
|
|
128
|
+
"height": {"type": "integer"},
|
|
129
|
+
"color_start": {"type": "string"},
|
|
130
|
+
"color_end": {"type": "string"},
|
|
131
|
+
"direction": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"enum": ["horizontal", "vertical", "diagonal"],
|
|
134
|
+
"default": "horizontal",
|
|
135
|
+
},
|
|
136
|
+
"layer_id": {"type": "string"},
|
|
137
|
+
},
|
|
138
|
+
"required": ["x", "y", "width", "height", "color_start", "color_end"],
|
|
139
|
+
}),
|
|
140
|
+
("draw_noise", "Fill a region with random noise", {
|
|
141
|
+
"type": "object",
|
|
142
|
+
"properties": {
|
|
143
|
+
"x": {"type": "integer"},
|
|
144
|
+
"y": {"type": "integer"},
|
|
145
|
+
"width": {"type": "integer"},
|
|
146
|
+
"height": {"type": "integer"},
|
|
147
|
+
"colors": {
|
|
148
|
+
"type": "array",
|
|
149
|
+
"items": {"type": "string"},
|
|
150
|
+
"description": "Palette of colors to sample from",
|
|
151
|
+
},
|
|
152
|
+
"density": {"type": "number", "default": 0.5, "description": "0.0-1.0"},
|
|
153
|
+
"layer_id": {"type": "string"},
|
|
154
|
+
},
|
|
155
|
+
"required": ["x", "y", "width", "height", "colors"],
|
|
156
|
+
}),
|
|
157
|
+
("draw_dither", "Apply a dither pattern between two colors", {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"properties": {
|
|
160
|
+
"x": {"type": "integer"},
|
|
161
|
+
"y": {"type": "integer"},
|
|
162
|
+
"width": {"type": "integer"},
|
|
163
|
+
"height": {"type": "integer"},
|
|
164
|
+
"color_a": {"type": "string"},
|
|
165
|
+
"color_b": {"type": "string"},
|
|
166
|
+
"pattern": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"enum": ["checker", "ordered", "bayer"],
|
|
169
|
+
"default": "checker",
|
|
170
|
+
},
|
|
171
|
+
"layer_id": {"type": "string"},
|
|
172
|
+
},
|
|
173
|
+
"required": ["x", "y", "width", "height", "color_a", "color_b"],
|
|
174
|
+
}),
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
for name, desc, schema in drawing_tools:
|
|
178
|
+
registry._register_drawing_tool(name, desc, schema)
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Export and curated high-level tools for MCP registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from PIL import Image
|
|
10
|
+
|
|
11
|
+
from pixelweaver.storage import export_frame_png
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .mcp_registry import MCPCommandRegistry
|
|
15
|
+
|
|
16
|
+
from .mcp_registry import ToolDef
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register_export_tools(registry: MCPCommandRegistry) -> None:
|
|
20
|
+
"""Register all export and curated high-level MCP tools."""
|
|
21
|
+
|
|
22
|
+
# -- Export tools ---------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
# export_png
|
|
25
|
+
async def handle_export_png(args: dict[str, Any]) -> dict[str, Any]:
|
|
26
|
+
project = registry.state.get_active_project()
|
|
27
|
+
if project is None:
|
|
28
|
+
return {"success": False, "error": "No active project"}
|
|
29
|
+
|
|
30
|
+
canvas_name = args.get("canvas_name")
|
|
31
|
+
if canvas_name is None:
|
|
32
|
+
canvas_name = next(iter(project.canvases.keys()), None)
|
|
33
|
+
if canvas_name is None:
|
|
34
|
+
return {"success": False, "error": "No canvas in project"}
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
png_bytes = export_frame_png(project, canvas_name)
|
|
38
|
+
b64 = base64.b64encode(png_bytes).decode("ascii")
|
|
39
|
+
except ValueError as exc:
|
|
40
|
+
return {"success": False, "error": str(exc)}
|
|
41
|
+
|
|
42
|
+
return {"success": True, "image_base64": b64, "format": "png"}
|
|
43
|
+
|
|
44
|
+
registry._register(
|
|
45
|
+
ToolDef(
|
|
46
|
+
"export_png",
|
|
47
|
+
"Export the active canvas as a composited PNG (returns base64)",
|
|
48
|
+
{
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": {
|
|
51
|
+
"canvas_name": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Canvas to export (default: first canvas)",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
mutates=False,
|
|
58
|
+
),
|
|
59
|
+
handle_export_png,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# export_spritesheet
|
|
63
|
+
async def handle_export_spritesheet(args: dict[str, Any]) -> dict[str, Any]:
|
|
64
|
+
project = registry.state.get_active_project()
|
|
65
|
+
if project is None:
|
|
66
|
+
return {"success": False, "error": "No active project"}
|
|
67
|
+
|
|
68
|
+
if not project.canvases:
|
|
69
|
+
return {"success": False, "error": "No canvases in project"}
|
|
70
|
+
|
|
71
|
+
canvas_name = args.get("canvas_name")
|
|
72
|
+
if canvas_name is None:
|
|
73
|
+
canvas_name = next(iter(project.canvases.keys()), None)
|
|
74
|
+
if canvas_name is None:
|
|
75
|
+
return {"success": False, "error": "No canvas in project"}
|
|
76
|
+
|
|
77
|
+
canvas = project.canvases.get(canvas_name)
|
|
78
|
+
if canvas is None:
|
|
79
|
+
return {"success": False, "error": f"Canvas {canvas_name!r} not found"}
|
|
80
|
+
|
|
81
|
+
direction = args.get("direction", "horizontal")
|
|
82
|
+
|
|
83
|
+
# Iterate animation frames within a single canvas and composite
|
|
84
|
+
# each frame's layers into a spritesheet strip.
|
|
85
|
+
frame_images: list[Image.Image] = []
|
|
86
|
+
try:
|
|
87
|
+
for i in range(canvas.frame_count):
|
|
88
|
+
png_bytes = export_frame_png(project, canvas_name, frame=i)
|
|
89
|
+
frame_images.append(Image.open(BytesIO(png_bytes)))
|
|
90
|
+
except (ValueError, IndexError) as exc:
|
|
91
|
+
return {"success": False, "error": str(exc)}
|
|
92
|
+
|
|
93
|
+
if not frame_images:
|
|
94
|
+
return {"success": False, "error": "No frames to export"}
|
|
95
|
+
|
|
96
|
+
# Build the spritesheet from all composited frames
|
|
97
|
+
frame_count = len(frame_images)
|
|
98
|
+
fw, fh = frame_images[0].width, frame_images[0].height
|
|
99
|
+
|
|
100
|
+
if direction == "vertical":
|
|
101
|
+
sheet = Image.new("RGBA", (fw, fh * frame_count), (0, 0, 0, 0))
|
|
102
|
+
for i, img in enumerate(frame_images):
|
|
103
|
+
sheet.paste(img, (0, i * fh))
|
|
104
|
+
else:
|
|
105
|
+
sheet = Image.new("RGBA", (fw * frame_count, fh), (0, 0, 0, 0))
|
|
106
|
+
for i, img in enumerate(frame_images):
|
|
107
|
+
sheet.paste(img, (i * fw, 0))
|
|
108
|
+
|
|
109
|
+
buf = BytesIO()
|
|
110
|
+
sheet.save(buf, format="PNG")
|
|
111
|
+
b64 = base64.b64encode(buf.getvalue()).decode("ascii")
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"success": True,
|
|
115
|
+
"image_base64": b64,
|
|
116
|
+
"format": "png",
|
|
117
|
+
"frame_count": frame_count,
|
|
118
|
+
"direction": direction,
|
|
119
|
+
"frame_width": fw,
|
|
120
|
+
"frame_height": fh,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
registry._register(
|
|
124
|
+
ToolDef(
|
|
125
|
+
"export_spritesheet",
|
|
126
|
+
"Export all animation frames of a canvas as a spritesheet (returns base64 PNG)",
|
|
127
|
+
{
|
|
128
|
+
"type": "object",
|
|
129
|
+
"properties": {
|
|
130
|
+
"canvas_name": {
|
|
131
|
+
"type": "string",
|
|
132
|
+
"description": "Canvas to export (default: first canvas)",
|
|
133
|
+
},
|
|
134
|
+
"direction": {
|
|
135
|
+
"type": "string",
|
|
136
|
+
"enum": ["horizontal", "vertical"],
|
|
137
|
+
"description": "Layout direction (default: horizontal)",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
mutates=False,
|
|
142
|
+
),
|
|
143
|
+
handle_export_spritesheet,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# -- Curated high-level tools --------------------------------------------
|
|
147
|
+
|
|
148
|
+
# draw_shape: combines rect/ellipse/diamond/line
|
|
149
|
+
async def handle_draw_shape(args: dict[str, Any]) -> dict[str, Any]:
|
|
150
|
+
err = registry._require_active_project()
|
|
151
|
+
if err:
|
|
152
|
+
return err
|
|
153
|
+
|
|
154
|
+
shape = args["shape"]
|
|
155
|
+
params = {k: v for k, v in args.items() if k != "shape"}
|
|
156
|
+
|
|
157
|
+
# Map shape names to the underlying command types
|
|
158
|
+
shape_to_cmd = {
|
|
159
|
+
"rect": "draw_rect",
|
|
160
|
+
"ellipse": "draw_ellipse",
|
|
161
|
+
"diamond": "draw_diamond",
|
|
162
|
+
"line": "draw_line",
|
|
163
|
+
}
|
|
164
|
+
cmd_type = shape_to_cmd.get(shape)
|
|
165
|
+
if cmd_type is None:
|
|
166
|
+
return {"success": False, "error": f"Unknown shape: {shape}"}
|
|
167
|
+
|
|
168
|
+
# Remap generic x/y/width/height to shape-specific params
|
|
169
|
+
if shape == "line":
|
|
170
|
+
params = {
|
|
171
|
+
"x1": args["x"],
|
|
172
|
+
"y1": args["y"],
|
|
173
|
+
"x2": args["width"],
|
|
174
|
+
"y2": args["height"],
|
|
175
|
+
"color": args["color"],
|
|
176
|
+
"layer_id": args.get("layer_id"),
|
|
177
|
+
}
|
|
178
|
+
# Remove None values
|
|
179
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
180
|
+
elif shape in ("ellipse", "diamond"):
|
|
181
|
+
params = {
|
|
182
|
+
"cx": args["x"],
|
|
183
|
+
"cy": args["y"],
|
|
184
|
+
"rx": args["width"],
|
|
185
|
+
"ry": args["height"],
|
|
186
|
+
"color": args["color"],
|
|
187
|
+
"filled": args.get("filled", True),
|
|
188
|
+
"layer_id": args.get("layer_id"),
|
|
189
|
+
}
|
|
190
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
191
|
+
|
|
192
|
+
return await registry._dispatch_command(cmd_type, params)
|
|
193
|
+
|
|
194
|
+
registry._register(
|
|
195
|
+
ToolDef(
|
|
196
|
+
"draw_shape",
|
|
197
|
+
"Draw a shape (rect, ellipse, diamond, or line) on the canvas",
|
|
198
|
+
{
|
|
199
|
+
"type": "object",
|
|
200
|
+
"properties": {
|
|
201
|
+
"shape": {
|
|
202
|
+
"type": "string",
|
|
203
|
+
"enum": ["rect", "ellipse", "diamond", "line"],
|
|
204
|
+
},
|
|
205
|
+
"x": {"type": "integer", "description": "X (or x1 for line)"},
|
|
206
|
+
"y": {"type": "integer", "description": "Y (or y1 for line)"},
|
|
207
|
+
"width": {"type": "integer", "description": "Width (or x2 for line)"},
|
|
208
|
+
"height": {"type": "integer", "description": "Height (or y2 for line)"},
|
|
209
|
+
"color": {"type": "string"},
|
|
210
|
+
"filled": {"type": "boolean", "default": True},
|
|
211
|
+
"layer_id": {"type": "string"},
|
|
212
|
+
},
|
|
213
|
+
"required": ["shape", "x", "y", "width", "height", "color"],
|
|
214
|
+
},
|
|
215
|
+
mutates=True,
|
|
216
|
+
),
|
|
217
|
+
handle_draw_shape,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# fill_area: flood_fill with tolerance
|
|
221
|
+
async def handle_fill_area(args: dict[str, Any]) -> dict[str, Any]:
|
|
222
|
+
err = registry._require_active_project()
|
|
223
|
+
if err:
|
|
224
|
+
return err
|
|
225
|
+
return await registry._dispatch_command("flood_fill", args)
|
|
226
|
+
|
|
227
|
+
registry._register(
|
|
228
|
+
ToolDef(
|
|
229
|
+
"fill_area",
|
|
230
|
+
"Fill a contiguous area starting from a seed point, with optional tolerance",
|
|
231
|
+
{
|
|
232
|
+
"type": "object",
|
|
233
|
+
"properties": {
|
|
234
|
+
"x": {"type": "integer"},
|
|
235
|
+
"y": {"type": "integer"},
|
|
236
|
+
"color": {"type": "string"},
|
|
237
|
+
"tolerance": {"type": "integer", "default": 0},
|
|
238
|
+
"layer_id": {"type": "string"},
|
|
239
|
+
},
|
|
240
|
+
"required": ["x", "y", "color"],
|
|
241
|
+
},
|
|
242
|
+
mutates=True,
|
|
243
|
+
),
|
|
244
|
+
handle_fill_area,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# apply_effect
|
|
248
|
+
async def handle_apply_effect(args: dict[str, Any]) -> dict[str, Any]:
|
|
249
|
+
err = registry._require_active_project()
|
|
250
|
+
if err:
|
|
251
|
+
return err
|
|
252
|
+
return await registry._dispatch_command("apply_effect", args)
|
|
253
|
+
|
|
254
|
+
registry._register(
|
|
255
|
+
ToolDef(
|
|
256
|
+
"apply_effect",
|
|
257
|
+
"Apply a visual effect to the canvas or a region",
|
|
258
|
+
{
|
|
259
|
+
"type": "object",
|
|
260
|
+
"properties": {
|
|
261
|
+
"effect": {
|
|
262
|
+
"type": "string",
|
|
263
|
+
"enum": [
|
|
264
|
+
"blur", "sharpen", "invert", "grayscale",
|
|
265
|
+
"brightness", "contrast", "hue_shift",
|
|
266
|
+
"outline", "shadow", "pixelate",
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
"intensity": {
|
|
270
|
+
"type": "number",
|
|
271
|
+
"default": 1.0,
|
|
272
|
+
"description": "Effect strength (0.0 - 10.0)",
|
|
273
|
+
},
|
|
274
|
+
"region": {
|
|
275
|
+
"type": "object",
|
|
276
|
+
"properties": {
|
|
277
|
+
"x": {"type": "integer"},
|
|
278
|
+
"y": {"type": "integer"},
|
|
279
|
+
"width": {"type": "integer"},
|
|
280
|
+
"height": {"type": "integer"},
|
|
281
|
+
},
|
|
282
|
+
"description": "Optional bounding region; omit for full canvas",
|
|
283
|
+
},
|
|
284
|
+
"layer_id": {"type": "string"},
|
|
285
|
+
},
|
|
286
|
+
"required": ["effect"],
|
|
287
|
+
},
|
|
288
|
+
mutates=True,
|
|
289
|
+
),
|
|
290
|
+
handle_apply_effect,
|
|
291
|
+
)
|