Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.5.0] - 2025-10-18

### Added
- **Layer Flattening Tool** (`flatten_layers`)
- Flattens all layers in a sprite into a single layer
- Uses Aseprite's built-in flatten operation
- Integration test and example demonstration included

## [0.4.0] - 2025-10-18

### Added
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ MCP Client → MCP Server (Go) → Lua Script Generation → Aseprite CLI (--bat
- `pkg/config/` - Configuration management (file-based only)
- `pkg/server/` - MCP server implementation
- `pkg/tools/` - MCP tool implementations organized by category:
- `canvas.go` - Sprite/layer/frame management (create_sprite, add_layer, add_frame, delete_layer with protection, delete_frame with protection)
- `canvas.go` - Sprite/layer/frame management (create_sprite, add_layer, add_frame, delete_layer with protection, delete_frame with protection, flatten_layers)
- `drawing.go` - Drawing primitives (pixels, lines, rectangles, circles, fill, contours for polylines/polygons)
- `selection.go` - Selection and clipboard operations (8 tools)
- `animation.go` - Animation and timeline operations (frame duration, tags, tag deletion, duplication, linked cels)
Expand Down Expand Up @@ -130,7 +130,7 @@ MCP Client → MCP Server (Go) → Lua Script Generation → Aseprite CLI (--bat

Core functionality implemented and tested:
- Canvas creation and management (RGB, Grayscale, Indexed)
- Layer and frame operations (add, delete with last-layer/frame protection)
- Layer and frame operations (add, delete with last-layer/frame protection, flatten)
- Drawing primitives (pixels, lines, rectangles, circles, fill) with optional palette-aware color snapping
- Advanced drawing: Contour tool for drawing polylines and closed polygons with points arrays
- **Selection and Clipboard Tools (8 tools):**
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Then use natural language to create sprites:
| `create_canvas` | Create new sprite with specified dimensions and color mode |
| `add_layer` | Add a new layer to the sprite |
| `delete_layer` | Delete a layer from the sprite (cannot delete last layer) |
| `flatten_layers` | Flatten all layers in a sprite into a single layer |
| `get_sprite_info` | Get sprite metadata (size, layers, frames) |

### Drawing & Painting
Expand Down
4 changes: 2 additions & 2 deletions cmd/pixel-mcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (
"syscall"
"time"

"github.com/willibrandon/pixel-mcp/pkg/config"
"github.com/willibrandon/pixel-mcp/pkg/server"
"github.com/willibrandon/mtlog"
"github.com/willibrandon/mtlog/core"
"github.com/willibrandon/mtlog/sinks"
"github.com/willibrandon/pixel-mcp/pkg/config"
"github.com/willibrandon/pixel-mcp/pkg/server"
)

var (
Expand Down
5 changes: 5 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The `client/` directory contains a complete example MCP client that demonstrates
- Connecting to the Aseprite MCP server via stdio transport
- Creating a 64x64 RGB sprite
- Adding and deleting layers and frames
- Flattening multiple layers into a single layer
- Drawing animated content (growing circles)
- Drawing polylines and polygons (zigzag, triangle, star)
- Filling areas with colors
Expand Down Expand Up @@ -137,6 +138,7 @@ Available tools:
- create_canvas: Create a new Aseprite sprite
- add_layer: Add a new layer to the sprite
- delete_layer: Delete a layer from the sprite
- flatten_layers: Flatten all layers into a single layer
- add_frame: Add a new frame to the sprite timeline
- delete_frame: Delete a frame from the sprite
- get_sprite_info: Get metadata about a sprite
Expand Down Expand Up @@ -251,6 +253,9 @@ Step 17: Demonstrating layer and frame deletion...
Deleted Layer 2 (2 layers remaining)
Deleted frame 2 (2 frames remaining)
Final state: {"width":32,"height":32,"color_mode":"RGB","frame_count":2,"layer_count":2,"layers":["Layer 1","Layer 3"]}
Flattening remaining 2 layers into 1...
Layers flattened successfully
After flattening: {"width":32,"height":32,"color_mode":"RGB","frame_count":2,"layer_count":1,"layers":["Layer 1"]}

Step 18: Demonstrating polylines and polygons...
Drawing zigzag polyline on frame 1...
Expand Down
18 changes: 18 additions & 0 deletions examples/client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,24 @@ func createAnimatedSprite(ctx context.Context, session *mcp.ClientSession, logge
}
logger.Information(" Final state: {Info}", finalInfoResp)

// Demonstrate flatten_layers
logger.Information(" Flattening remaining 2 layers into 1...")
if _, err := callTool(ctx, session, "flatten_layers", map[string]any{
"sprite_path": deleteSprite,
}); err != nil {
return fmt.Errorf("flatten_layers failed: %w", err)
}
logger.Information(" Layers flattened successfully")

// Verify flattened state
flattenedInfoResp, err := callTool(ctx, session, "get_sprite_info", map[string]any{
"sprite_path": deleteSprite,
})
if err != nil {
return fmt.Errorf("get_sprite_info after flattening failed: %w", err)
}
logger.Information(" After flattening: {Info}", flattenedInfoResp)

// Step 18: Demonstrate drawing polylines and polygons
logger.Information("")
logger.Information("Step 18: Demonstrating polylines and polygons...")
Expand Down
42 changes: 21 additions & 21 deletions examples/quantization/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,18 @@ func run(logger core.Logger) error {
// Draw horizontal gradient bars (red to yellow to green to cyan to blue)
// We'll batch pixels to make this much faster
colors := []struct{ r, g, b int }{
{255, 0, 0}, // Red
{255, 128, 0}, // Orange
{255, 255, 0}, // Yellow
{128, 255, 0}, // Yellow-Green
{0, 255, 0}, // Green
{0, 255, 128}, // Green-Cyan
{0, 255, 255}, // Cyan
{0, 128, 255}, // Cyan-Blue
{0, 0, 255}, // Blue
{128, 0, 255}, // Blue-Magenta
{255, 0, 255}, // Magenta
{255, 0, 128}, // Magenta-Red
{255, 0, 0}, // Red
{255, 128, 0}, // Orange
{255, 255, 0}, // Yellow
{128, 255, 0}, // Yellow-Green
{0, 255, 0}, // Green
{0, 255, 128}, // Green-Cyan
{0, 255, 255}, // Cyan
{0, 128, 255}, // Cyan-Blue
{0, 0, 255}, // Blue
{128, 0, 255}, // Blue-Magenta
{255, 0, 255}, // Magenta
{255, 0, 128}, // Magenta-Red
}

barHeight := 128 / len(colors)
Expand Down Expand Up @@ -189,10 +189,10 @@ func run(logger core.Logger) error {

// Step 3: Test each quantization algorithm
algorithms := []struct {
name string
name string
targetColors int
dither bool
description string
dither bool
description string
}{
{"median_cut", 16, false, "Median Cut (balanced, no dither)"},
{"median_cut", 16, true, "Median Cut with Floyd-Steinberg dithering"},
Expand Down Expand Up @@ -253,12 +253,12 @@ func run(logger core.Logger) error {

// Apply quantization
quantizeResp, err := callTool(ctx, session, "quantize_palette", map[string]any{
"sprite_path": copyPath,
"target_colors": algo.targetColors,
"algorithm": algo.name,
"dither": algo.dither,
"preserve_transparency": false,
"convert_to_indexed": true,
"sprite_path": copyPath,
"target_colors": algo.targetColors,
"algorithm": algo.name,
"dither": algo.dither,
"preserve_transparency": false,
"convert_to_indexed": true,
})
if err != nil {
return fmt.Errorf("quantize_palette failed: %w", err)
Expand Down
18 changes: 9 additions & 9 deletions pkg/aseprite/lua_auto_shading.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,13 @@ spr:saveAs(spr.filename)

-- Print JSON result
print(json)`,
EscapeString(layerName), // layer name for finding
EscapeString(layerName), // layer name for error
tempImagePath, // shaded image path
frameNumber, // frame number for cel lookup
frameNumber, // frame number for error message
frameNumber, // frame number for newCel
colorList, // generated colors
len(generatedColors), // colors_added
regionsShadedCount) // regions_shaded
EscapeString(layerName), // layer name for finding
EscapeString(layerName), // layer name for error
tempImagePath, // shaded image path
frameNumber, // frame number for cel lookup
frameNumber, // frame number for error message
frameNumber, // frame number for newCel
colorList, // generated colors
len(generatedColors), // colors_added
regionsShadedCount) // regions_shaded
}
28 changes: 14 additions & 14 deletions pkg/aseprite/lua_drawing.go
Original file line number Diff line number Diff line change
Expand Up @@ -1039,18 +1039,18 @@ print("Dithering applied successfully")`,
frameNumber, frameNumber,
c1.R, c1.G, c1.B, c1.A,
c2.R, c2.G, c2.B, c2.A,
width, // line 915: error buffer width
height, // line 920: py loop
width, // line 925: clear buffer width
width, // line 930: px loop
width, // line 933: width check for division by zero
width, // line 936: gradient calculation
width, // line 967: right neighbor check
height, // line 972: bottom neighbor check
width, // line 981: bottom-right check
width, // line 1007: right neighbor check (RGB)
height, // line 1012: bottom neighbor check (RGB)
width, // line 1021: bottom-right check (RGB)
x, // line 1031: x coordinate
y) // line 1031: y coordinate
width, // line 915: error buffer width
height, // line 920: py loop
width, // line 925: clear buffer width
width, // line 930: px loop
width, // line 933: width check for division by zero
width, // line 936: gradient calculation
width, // line 967: right neighbor check
height, // line 972: bottom neighbor check
width, // line 981: bottom-right check
width, // line 1007: right neighbor check (RGB)
height, // line 1012: bottom neighbor check (RGB)
width, // line 1021: bottom-right check (RGB)
x, // line 1031: x coordinate
y) // line 1031: y coordinate
}
10 changes: 5 additions & 5 deletions pkg/aseprite/lua_quantization.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@ spr:saveAs(spr.filename)

-- Print JSON result
print(json)`,
len(palette), // palette resize
colorList, // color list
conversionCode, // conversion code
originalColors, // original_colors
len(palette), // quantized_colors
len(palette), // palette resize
colorList, // color list
conversionCode, // conversion code
originalColors, // original_colors
len(palette), // quantized_colors
EscapeString(algorithm)) // algorithm_used
}

Expand Down
6 changes: 3 additions & 3 deletions pkg/aseprite/quantization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,10 @@ func TestQuantizePalette(t *testing.T) {
}

tests := []struct {
name string
name string
targetColors int
algorithm string
wantErr bool
algorithm string
wantErr bool
}{
{
name: "median_cut to 4 colors",
Expand Down
2 changes: 1 addition & 1 deletion pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import (
"fmt"

"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/willibrandon/mtlog/core"
"github.com/willibrandon/pixel-mcp/pkg/aseprite"
"github.com/willibrandon/pixel-mcp/pkg/config"
"github.com/willibrandon/pixel-mcp/pkg/tools"
"github.com/willibrandon/mtlog/core"
)

// Server wraps the MCP server and provides Aseprite tool implementations.
Expand Down
2 changes: 1 addition & 1 deletion pkg/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package server
import (
"testing"

"github.com/willibrandon/pixel-mcp/internal/testutil"
"github.com/willibrandon/mtlog"
"github.com/willibrandon/mtlog/sinks"
"github.com/willibrandon/pixel-mcp/internal/testutil"
)

func TestNew(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/tools/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
"os"

"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/willibrandon/mtlog/core"
"github.com/willibrandon/pixel-mcp/pkg/aseprite"
"github.com/willibrandon/pixel-mcp/pkg/config"
"github.com/willibrandon/mtlog/core"
)

// AnalyzeReferenceInput defines the input parameters for the analyze_reference tool.
Expand Down
4 changes: 2 additions & 2 deletions pkg/tools/analysis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import (
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/willibrandon/pixel-mcp/internal/testutil"
"github.com/willibrandon/pixel-mcp/pkg/aseprite"
"github.com/willibrandon/mtlog"
"github.com/willibrandon/mtlog/core"
"github.com/willibrandon/pixel-mcp/internal/testutil"
"github.com/willibrandon/pixel-mcp/pkg/aseprite"
)

// createAnalysisTestSession creates an MCP session with analysis tools registered
Expand Down
2 changes: 1 addition & 1 deletion pkg/tools/animation.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (
"strings"

"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/willibrandon/mtlog/core"
"github.com/willibrandon/pixel-mcp/pkg/aseprite"
"github.com/willibrandon/pixel-mcp/pkg/config"
"github.com/willibrandon/mtlog/core"
)

// SetFrameDurationInput defines the input parameters for the set_frame_duration tool.
Expand Down
4 changes: 2 additions & 2 deletions pkg/tools/animation_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import (
"testing"
"time"

"github.com/willibrandon/pixel-mcp/internal/testutil"
"github.com/willibrandon/pixel-mcp/pkg/aseprite"
"github.com/willibrandon/mtlog"
"github.com/willibrandon/mtlog/sinks"
"github.com/willibrandon/pixel-mcp/internal/testutil"
"github.com/willibrandon/pixel-mcp/pkg/aseprite"
)

// Integration tests for animation tools with real Aseprite.
Expand Down
4 changes: 2 additions & 2 deletions pkg/tools/animation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/willibrandon/pixel-mcp/internal/testutil"
"github.com/willibrandon/pixel-mcp/pkg/aseprite"
"github.com/willibrandon/mtlog"
"github.com/willibrandon/mtlog/core"
"github.com/willibrandon/pixel-mcp/internal/testutil"
"github.com/willibrandon/pixel-mcp/pkg/aseprite"
)

func TestSetFrameDurationInput_Validation(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/tools/antialiasing.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"math"

"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/willibrandon/mtlog/core"
"github.com/willibrandon/pixel-mcp/pkg/aseprite"
"github.com/willibrandon/pixel-mcp/pkg/config"
"github.com/willibrandon/mtlog/core"
)

// SuggestAntialiasingInput defines the input parameters for antialiasing suggestions.
Expand Down
4 changes: 2 additions & 2 deletions pkg/tools/antialiasing_mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/willibrandon/pixel-mcp/internal/testutil"
"github.com/willibrandon/pixel-mcp/pkg/aseprite"
"github.com/willibrandon/mtlog"
"github.com/willibrandon/mtlog/core"
"github.com/willibrandon/pixel-mcp/internal/testutil"
"github.com/willibrandon/pixel-mcp/pkg/aseprite"
)

// createAntialiasingTestSession creates an MCP session with antialiasing tools registered
Expand Down
Loading