Summary
draw_pixels writes to cel.image with Image:putPixel, which expects image-local coordinates, but it passes the caller's sprite coordinates unmodified. Whenever the target layer's cel doesn't start at (0,0), every pixel is offset by the cel's origin, and pixels that fall outside the cel's bounds are dropped entirely. The shape tools (draw_rectangle, draw_circle, draw_line) are unaffected because they use app.useTool{...}, which operates in sprite space.
Environment
- pixel-mcp
v0.5.0 (commit 96e59ec, current main HEAD)
- Aseprite 1.3.17.2, macOS arm64 — but the defect is in generated Lua, so it's OS-independent
Steps to reproduce
create_canvas 32×32, rgb
draw_circle filled, center (16,16), radius 14 — fills the layer; on the per-op save Aseprite trims the cel to the content's bounding box, so the cel origin becomes (2,2)
draw_pixels a pixel at {x:16, y:16}
get_pixels at (16,16)
Expected: the pixel is at sprite (16,16).
Actual: (16,16) is still the circle color; the pixel was written at (18,18) — offset by the cel's (2,2) origin.
The offset equals the cel origin (not a constant)
| Setup |
Cel origin |
Sent to draw_pixels |
Landed at |
| Blank canvas (cel covers sprite) |
(0,0) |
(5,5) |
(5,5) — correct |
| 16×16, filled circle center (8,8) r3 |
(5,5) |
(2,2) |
(7,7) |
| 32×32, filled circle center (16,16) r14 |
(2,2) |
(10,19) |
(12,21) |
A pixel placed outside the trimmed cel's bounds is lost entirely rather than shifted.
Root cause
pkg/aseprite/lua_drawing.go, DrawPixels:
local cel = layer:cel(frame)
if not cel then cel = spr:newCel(layer, frame) end -- 2-arg form: image is nil
local img = cel.image
-- ...
img:putPixel(x, y, color) -- x,y are sprite coords; putPixel wants image-local coords
Two defects: (1) Image:putPixel is relative to the cel image's origin, so it needs sprite - cel.position; (2) spr:newCel(layer, frame) creates a cel with no image, so draw_pixels on a freshly added layer can hit a nil image.
Why it isn't already caught
On a fresh create_canvas the cel covers the whole sprite at (0,0), so putPixel is coincidentally correct — the bug only appears after a shape trims the cel to a non-zero origin. The existing tests TestIntegration_DrawPixels_CelPosition_Bug and TestIntegration_DrawPixels_NewLayer_Bug exercise draw_pixels-only sequences (cel stays at 0,0) and currently pass; neither covers the shape-then-draw_pixels case. They're also //go:build integration, so they require a real Aseprite install and don't appear to run in CI.
Suggested fix
Normalize the cel to a full-canvas image at (0,0) before writing (Aseprite re-trims on save, so canvas size and file size are unchanged). I have a branch with the fix plus a CI-runnable unit test and two integration regression tests.
PR: #20
Summary
draw_pixelswrites tocel.imagewithImage:putPixel, which expects image-local coordinates, but it passes the caller's sprite coordinates unmodified. Whenever the target layer's cel doesn't start at (0,0), every pixel is offset by the cel's origin, and pixels that fall outside the cel's bounds are dropped entirely. The shape tools (draw_rectangle,draw_circle,draw_line) are unaffected because they useapp.useTool{...}, which operates in sprite space.Environment
v0.5.0(commit96e59ec, currentmainHEAD)Steps to reproduce
create_canvas32×32,rgbdraw_circlefilled, center (16,16), radius 14 — fills the layer; on the per-op save Aseprite trims the cel to the content's bounding box, so the cel origin becomes (2,2)draw_pixelsa pixel at{x:16, y:16}get_pixelsat (16,16)Expected: the pixel is at sprite (16,16).
Actual: (16,16) is still the circle color; the pixel was written at (18,18) — offset by the cel's (2,2) origin.
The offset equals the cel origin (not a constant)
draw_pixelsA pixel placed outside the trimmed cel's bounds is lost entirely rather than shifted.
Root cause
pkg/aseprite/lua_drawing.go,DrawPixels:Two defects: (1)
Image:putPixelis relative to the cel image's origin, so it needssprite - cel.position; (2)spr:newCel(layer, frame)creates a cel with no image, sodraw_pixelson a freshly added layer can hit a nil image.Why it isn't already caught
On a fresh
create_canvasthe cel covers the whole sprite at (0,0), soputPixelis coincidentally correct — the bug only appears after a shape trims the cel to a non-zero origin. The existing testsTestIntegration_DrawPixels_CelPosition_BugandTestIntegration_DrawPixels_NewLayer_Bugexercisedraw_pixels-only sequences (cel stays at 0,0) and currently pass; neither covers the shape-then-draw_pixelscase. They're also//go:build integration, so they require a real Aseprite install and don't appear to run in CI.Suggested fix
Normalize the cel to a full-canvas image at (0,0) before writing (Aseprite re-trims on save, so canvas size and file size are unchanged). I have a branch with the fix plus a CI-runnable unit test and two integration regression tests.
PR: #20