Skip to content

fix(indexed): resolve exact colors against the sprite's own palette#22

Open
Fuitad wants to merge 1 commit into
willibrandon:mainfrom
Fuitad:exact-palette-color-fix
Open

fix(indexed): resolve exact colors against the sprite's own palette#22
Fuitad wants to merge 1 commit into
willibrandon:mainfrom
Fuitad:exact-palette-color-fix

Conversation

@Fuitad

@Fuitad Fuitad commented Jul 2, 2026

Copy link
Copy Markdown

Summary

Fixes a second, independent indexed-color-mode bug found while using the pixel-plugin Claude Code plugin (which bundles this server) for a game project: img:putPixel/app.useTool calls made with use_palette=false (exact colors, the default) silently draw the wrong pixel on an indexed sprite, with no error.

Root cause

On an indexed sprite, both img:putPixel and app.useTool expect a palette index, not an arbitrary RGBA value. Passing a Color object directly resolves correctly when the sprite's palette was set earlier in the SAME Aseprite process. But every pixel-mcp tool call is its own aseprite --batch invocation: the palette is set in one process (set_palette) and drawing happens in the next (draw_rectangle, etc).

In that cross-process scenario (the normal case for every real tool call), Aseprite's own Color-to-index resolution for img:putPixel/app.useTool does not reliably match the sprite's actual, correctly-loaded palette. I confirmed this by reading spr.palettes[1] back at the point of failure: the palette is exactly right, but the index Aseprite picks for the requested RGBA is unrelated to it. The result silently corrupts the drawn color (in one repro, a raw RGBA that exactly equals the palette's index 3 resolved to index 8 instead, which was out of a 4-color palette's range and therefore read back as fully transparent).

Isolated with a minimal repro directly against the aseprite CLI (bypassing this project's Go/MCP layer): the SAME create+set-palette+draw sequence resolves the color correctly when done as three statements in one process, or when reloaded via app.open() within that same process, but incorrectly whenever the draw happens in a freshly-started aseprite process that only opens a previously-saved file (which is exactly how --batch <file> --script ... and pixel-mcp itself always operate).

Affects draw_pixels, draw_line, draw_contour, draw_rectangle, draw_circle, and fill_area whenever use_palette is false (the default) on an indexed sprite.

Fix

resolveExactPaletteColor (pkg/aseprite/lua_core.go) resolves the color itself by reading the sprite's actual active palette at Lua runtime, bypassing Aseprite's unreliable resolution entirely:

  • A fully-transparent color (alpha 0, e.g. a "paint transparent" fill_area call establishing a background) resolves directly to spr.transparentColor, since transparency is a designated index, not a color expected to exist in the palette.
  • Any other color requires an exact match in the active palette and errors otherwise, so a caller drawing an approximate/non-locked color gets a clear error telling them to pass use_palette=true to snap to the nearest palette color, instead of a silently wrong pixel.

Wired into DrawPixels (FormatColorWithPalette) and DrawLine/DrawContour/DrawRectangle/DrawCircle/FillArea (FormatColorWithPaletteForTool).

Testing

go build ./...            # clean
go vet ./...               # clean
go test ./...               # 627 passed
go test -tags=integration ./...   # 801 passed

Added TestIntegration_IndexedMode_ExactColorResolution (pkg/tools/exact_palette_color_indexed_mode_bug_test.go) with three subtests: exact match via draw_rectangle, exact match via draw_pixels, and an error on no match. Verified RED without the fix (all three fail/misbehave exactly as described above) and GREEN with it.

Updated two pre-existing unit tests that asserted the old, buggy literal output (TestFormatColorWithPalette_NoPalette, TestLuaGenerator_DrawPixels) to expect resolveExactPaletteColor(...) instead of a raw Color(...) call. Also fixed TestIntegration_SetPalette_ThenApplyShading, which drew an approximate "mid-tone" color with use_palette=false even though that color was never one of the locked palette's exact entries; it now correctly passes use_palette=true to snap it, which is what the test's own comment ("mid-tone") already implied.

Notes

This is a separate, independent bug from the one I opened in #21 (transparentColor colliding with a real palette color after a resize) - unrelated root cause, unrelated code paths, no overlap in the fix. This branch is based directly on main so it applies cleanly regardless of #21's or #20's review status.

Root cause: on an indexed sprite, img:putPixel and app.useTool both
expect a palette index, not an arbitrary RGBA value. Passing a Color
object directly resolves correctly when the sprite's palette was set
earlier in the SAME Aseprite process, but pixel-mcp runs each tool
call as its own `aseprite --batch` invocation: the palette is set in
one process and drawing happens in the next. In that (normal, for
every real tool call) cross-process scenario, Aseprite's own
Color-to-index resolution for img:putPixel/app.useTool does not
reliably match the sprite's actual, correctly-loaded palette (verified
by reading spr.palettes[1] back at the point of failure) and silently
resolves to an unrelated index instead, corrupting the drawn color
with no error.

Affects draw_pixels, draw_line, draw_contour, draw_rectangle,
draw_circle, and fill_area whenever use_palette is false (the default)
on an indexed sprite.

Fix: resolveExactPaletteColor (pkg/aseprite/lua_core.go) resolves the
color itself by reading the sprite's actual active palette at Lua
runtime, bypassing Aseprite's unreliable resolution entirely.
- Fully-transparent colors (alpha 0, e.g. a "paint transparent"
  fill_area call) resolve directly to spr.transparentColor, since
  transparency is a designated index, not a color expected to exist
  in the palette.
- Any other color requires an exact match in the active palette and
  errors if none exists, so a caller drawing an approximate/non-locked
  color silently gets the wrong pixel instead of an error telling them
  to pass use_palette=true to snap to the nearest palette color.

Wired into DrawPixels (FormatColorWithPalette) and DrawLine/
DrawContour/DrawRectangle/DrawCircle/FillArea (FormatColorWithPaletteForTool).

Testing:
- go test ./...: 628 passed
- go test -tags=integration ./...: 805 passed
- Added TestIntegration_IndexedMode_ExactColorResolution
  (pkg/tools/exact_palette_color_indexed_mode_bug_test.go): exact match
  via draw_rectangle, exact match via draw_pixels, and error on no
  match. Verified RED without the fix (all three fail/misbehave
  exactly as described above) and GREEN with it.
- Updated two pre-existing tests that asserted the old, buggy
  behavior: TestFormatColorWithPalette_NoPalette and
  TestLuaGenerator_DrawPixels now expect resolveExactPaletteColor(...)
  instead of a raw Color(...) literal.
- Fixed TestIntegration_SetPalette_ThenApplyShading, which drew an
  approximate "mid-tone" color with use_palette=false even though that
  color was never one of the locked palette's 16 exact entries; it now
  correctly passes use_palette=true to snap it.
Fuitad added a commit to Fuitad/pixel-plugin that referenced this pull request Jul 2, 2026
Refreshes all 5 platform binaries from Fuitad/pixel-mcp @ 7bc0829,
which adds a fix for indexed-mode shape/pixel tools silently
mis-drawing colors when use_palette is false (the default):
draw_pixels, draw_line, draw_contour, draw_rectangle, draw_circle, and
fill_area now resolve the requested color against the sprite's actual
active palette directly instead of trusting Aseprite's own raw-Color
resolution, which is unreliable once the palette was set in a
previous process (upstream willibrandon/pixel-mcp#22).

Bumps plugin version to 0.5.2 and documents the change in CHANGELOG.md.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant