Skip to content

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

Description

@hansonchris

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

  1. create_canvas 32×32, rgb
  2. 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)
  3. draw_pixels a pixel at {x:16, y:16}
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions