Skip to content

fix: correct draw_pixels placement when cel is not at origin (0,0)#20

Open
hansonchris wants to merge 1 commit into
willibrandon:mainfrom
hansonchris:fix/draw-pixels-cel-position
Open

fix: correct draw_pixels placement when cel is not at origin (0,0)#20
hansonchris wants to merge 1 commit into
willibrandon:mainfrom
hansonchris:fix/draw-pixels-cel-position

Conversation

@hansonchris

Copy link
Copy Markdown

Fixes #19.

Problem

draw_pixels wrote to cel.image via Image:putPixel using sprite coordinates, but putPixel expects cel-local coordinates. Once a layer's cel is trimmed to a non-(0,0) origin (e.g. after any shape tool runs), every pixel was offset by the cel's position, and pixels outside the cel bounds were dropped. The shape tools are unaffected because they use app.useTool, which works in sprite space. draw_pixels on a newly added layer could also hit a nil cel image (spr:newCel 2-arg form).

Fix

Normalize the target cel to a full-canvas image at (0,0) before writing (compositing any existing content first), so sprite coordinates equal image coordinates. This also gives a fresh cel a valid image. Aseprite re-trims the cel on save, so canvas dimensions and file size are unchanged.

local cel = layer:cel(frame)
if cel and cel.image then
    local full = Image(spr.width, spr.height, spr.colorMode)
    full:drawImage(cel.image, cel.position)
    cel.image = full
    cel.position = Point(0, 0)
else
    local full = Image(spr.width, spr.height, spr.colorMode)
    cel = spr:newCel(layer, frame, full, Point(0, 0))
end
local img = cel.image
-- existing putPixel loop unchanged

Tests

  • TestLuaGenerator_DrawPixels_NormalizesCel (unit, pkg/aseprite/lua_test.go) — asserts the generated script normalizes the cel to full-canvas at (0,0). Runs without Aseprite, so it guards the fix in CI (the integration tests require a real Aseprite install).
  • TestIntegration_DrawPixels_AfterShape_CelOffset — circle, then a pixel at (16,16); asserts it lands exactly there.
  • TestIntegration_DrawPixels_AfterShape_OutsideShapeBounds — asymmetric cel origin plus a pixel outside the shape's bbox (the lost-pixel case).

All three fail on main and pass with this change.

Verification

go test ./...                      # untagged: ok
go test -race ./...                # ok
go test -tags=integration ./...    # ok (incl. existing draw_pixels cel tests)

Notes

In indexed mode a new Image is filled with index 0 (the default transparent index), so transparency is preserved in the common case; if a sprite has a non-default spr.transparentColor, that should be honored — flagging in case you'd like a follow-up.

draw_pixels wrote to cel.image via Image:putPixel using sprite
coordinates, but putPixel expects cel-LOCAL coordinates. Once a layer's
cel is trimmed to a non-(0,0) origin (e.g. after a shape tool runs),
every pixel was offset by the cel's position, and pixels falling outside
the cel's bounds were dropped entirely. The shape tools are unaffected
because they use app.useTool, which operates in sprite space.

Normalize the target cel to a full-canvas image at (0,0) before writing
(compositing any existing content first), so sprite coordinates equal
image coordinates. This also gives a freshly created cel a valid image,
fixing draw_pixels on newly added layers. Aseprite re-trims the cel on
save, so canvas dimensions and file size are unchanged.

Tests:
- pkg/aseprite/lua_test.go: unit assertion that the generated script
  normalizes the cel to full-canvas at (0,0). Runs without Aseprite, so
  it guards the fix in CI, where the integration tests do not run.
- pkg/tools: integration tests for the offset-after-shape case and for
  an asymmetric cel origin with a pixel outside the shape's bounds (the
  lost-pixel case). Both fail on stock and pass with this change.
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 @ 0fdedc5
(based on upstream v0.5.0), which includes two fixes not yet released
upstream:

- Indexed palette transparentColor collision: Palette:resize silently
  clamped Sprite.transparentColor into the resized palette's valid
  index range, colliding it with a real color (typically the
  palette's last entry). Once collided, "transparent" fills wrote
  that real color's index, and pixels drawn in that color could be
  auto-trimmed away as if transparent.
- draw_pixels cel-origin bug (willibrandon/pixel-mcp#20): draw_pixels
  wrote via Image:putPixel using sprite coordinates, but putPixel
  expects cel-local coordinates, so pixels were offset or dropped
  whenever a cel had been trimmed to a non-(0,0) origin (e.g. after
  any shape tool ran).

Bumps plugin version to 0.5.1 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.

draw_pixels writes with cel-local coordinates — pixels are offset (or lost) when the cel isn't at (0,0)

1 participant