diff --git a/data/gui.xml b/data/gui.xml
index c1e9a4287..3934a44a7 100644
--- a/data/gui.xml
+++ b/data/gui.xml
@@ -138,6 +138,7 @@
+
@@ -807,6 +808,9 @@
+ -
+
+
diff --git a/data/pref.xml b/data/pref.xml
index 57a5f2a26..cc83d4dba 100644
--- a/data/pref.xml
+++ b/data/pref.xml
@@ -195,6 +195,7 @@
+
diff --git a/data/strings/en.ini b/data/strings/en.ini
index e75d44401..d0b266abb 100644
--- a/data/strings/en.ini
+++ b/data/strings/en.ini
@@ -346,6 +346,7 @@ NewLayer_BeforeActiveLayer = New {} Below
NewLayer_Layer = Layer
NewLayer_Group = Group
NewLayer_ReferenceLayer = Reference Layer
+NewLayer_TilemapLayer = Tilemap Layer
NewLayer_FromClipboard = {} from Clipboard
NewLayer_ViaCopy = {} via Copy
NewLayer_ViaCut = {} via Cut
@@ -443,6 +444,7 @@ SymmetryMode = Symmetry Mode
TiledMode = Tiled Mode
Timeline = Switch Timeline
TogglePreview = Toggle Preview
+ToggleTilesMode = Toggle Tiles Mode
ToggleTimelineThumbnails = Toggle Timeline Thumbnails
Undo = Undo
UndoHistory = Undo History
@@ -845,6 +847,7 @@ layer_new_group = New &Group
layer_new_layer_via_copy = New Layer via &Copy
layer_new_layer_via_cut = New Layer via Cu&t
layer_new_reference_layer_from_file = New &Reference Layer from File
+layer_new_tilemap_layer = New Tilemap Layer
layer_delete_layer = Delete Laye&r
layer_background_from_layer = &Background from Layer
layer_layer_from_background = &Layer from Background
diff --git a/docs/ase-file-specs.md b/docs/ase-file-specs.md
index 7199cab89..f8adc9a5e 100644
--- a/docs/ase-file-specs.md
+++ b/docs/ase-file-specs.md
@@ -25,7 +25,9 @@ ASE files use Intel (little-endian) byte order.
* `PIXEL`: One pixel, depending on the image pixel format:
- **RGBA**: `BYTE[4]`, each pixel have 4 bytes in this order Red, Green, Blue, Alpha.
- **Grayscale**: `BYTE[2]`, each pixel have 2 bytes in the order Value, Alpha.
- - **Indexed**: `BYTE`, Each pixel uses 1 byte (the index).
+ - **Indexed**: `BYTE`, each pixel uses 1 byte (the index).
+* `TILE`: **Tilemaps**: Each tile can be a 8-bit, 16-bit, or 32-bit
+ value and there are masks related to the meaning of each bit.
## Introduction
@@ -146,6 +148,7 @@ Ignore this chunk if you find the new palette chunk (0x2019)
WORD Layer type
0 = Normal (image) layer
1 = Group
+ 2 = Tilemap
WORD Layer child level (see NOTE.1)
WORD Default layer width in pixels (ignored)
WORD Default layer height in pixels (ignored)
@@ -173,19 +176,24 @@ Ignore this chunk if you find the new palette chunk (0x2019)
Note: valid only if file header flags field has bit 1 set
BYTE[3] For future (set to zero)
STRING Layer name
+ + If layer type = 3
+ DWORD Tileset index
### Cel Chunk (0x2005)
- This chunk determine where to put a cel in the specified
- layer/frame.
+This chunk determine where to put a cel in the specified layer/frame.
WORD Layer index (see NOTE.2)
SHORT X position
SHORT Y position
BYTE Opacity level
- WORD Cel type
+ WORD Cel Type
+ 0 - Raw Image Data (unused, compressed image is preferred)
+ 1 - Linked Cel
+ 2 - Compressed Image
+ 3 - Compressed Tilemap
BYTE[7] For future (set to zero)
- + For cel type = 0 (Raw Cel)
+ + For cel type = 0 (Raw Image Data)
WORD Width in pixels
WORD Height in pixels
PIXEL[] Raw pixel data: row by row from top to bottom,
@@ -195,14 +203,18 @@ Ignore this chunk if you find the new palette chunk (0x2019)
+ For cel type = 2 (Compressed Image)
WORD Width in pixels
WORD Height in pixels
- BYTE[] "Raw Cel" data compressed with ZLIB method
-
-Details about the ZLIB and DEFLATE compression methods:
-
-* https://www.ietf.org/rfc/rfc1950
-* https://www.ietf.org/rfc/rfc1951
-* Some extra notes that might help you to decode the data:
- http://george.chiramattel.com/blog/2007/09/deflatestream-block-length-does-not-match.html
+ BYTE[] "Raw Cel" data compressed with ZLIB method (see NOTE.3)
+ + For cel type = 3 (Compressed Tilemap)
+ WORD Width in pixels
+ WORD Height in pixels
+ WORD Bits per pixel/tile reference (8, 16, or 32)
+ DWORD Bitmask for tile ID (e.g. 0x1fffffff for 32-bit tiles)
+ DWORD Bitmask for X flip
+ DWORD Bitmask for Y flip
+ DWORD Bitmask for 90CW rotation
+ BYTE[10] Reserved
+ TILE[] Row by row, from top to bottom tile by tile
+ compressed with ZLIB method (see NOTE.3)
### Cel Extra Chunk (0x2006)
@@ -326,6 +338,26 @@ belongs to that cel, etc.
LONG Pivot X position (relative to the slice origin)
LONG Pivot Y position (relative to the slice origin)
+### Tileset Chunk (0x2023)
+
+ DWORD Tileset ID
+ DWORD Tileset flags
+ 1 - Include link to external file
+ 2 - Include tiles inside this file
+ WORD Tiles width
+ WORD Tiles height
+ BYTE[36] Reserved
+ STRING Name of the tileset
+ + If flag 1 is set
+ STRING Name of the external file (path relative to this file, in the best case)
+ DWORD Tileset ID in the external file
+ + If flag 2 is set
+ DWORD Number of tiles to read
+ + For each tile
+ DWORD Tile flags (0)
+ DWORD Compressed data length
+ PIXEL[] Read tile image (tile width x height compressed pixels, see NOTE.3)
+
### Notes
#### NOTE.1
@@ -356,6 +388,15 @@ example:
| `- Layer2 4
`- Layer3 5
+#### NOTE.3
+
+Details about the ZLIB and DEFLATE compression methods:
+
+* https://www.ietf.org/rfc/rfc1950
+* https://www.ietf.org/rfc/rfc1951
+* Some extra notes that might help you to decode the data:
+ http://george.chiramattel.com/blog/2007/09/deflatestream-block-length-does-not-match.html
+
## File Format Changes
1. The first change from the first release of the new .ase format,
diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index a1dc65759..4140d9431 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -160,6 +160,7 @@ if(ENABLE_SCRIPTING)
script/engine.cpp
script/frame_class.cpp
script/frames_class.cpp
+ script/grid_class.cpp
script/image_class.cpp
script/image_iterator_class.cpp
script/image_spec_class.cpp
@@ -184,6 +185,8 @@ if(ENABLE_SCRIPTING)
script/sprites_class.cpp
script/tag_class.cpp
script/tags_class.cpp
+ script/tileset_class.cpp
+ script/tilesets_class.cpp
script/tool_class.cpp
script/values.cpp
script/version_class.cpp
@@ -288,6 +291,7 @@ if(ENABLE_UI)
commands/cmd_tiled_mode.cpp
commands/cmd_timeline.cpp
commands/cmd_toggle_preview.cpp
+ commands/cmd_toggle_tiles_mode.cpp
commands/cmd_toggle_timeline_thumbnails.cpp
commands/cmd_undo_history.cpp
commands/cmd_unlink_cel.cpp
@@ -417,6 +421,8 @@ add_library(app-lib
cmd/add_palette.cpp
cmd/add_slice.cpp
cmd/add_tag.cpp
+ cmd/add_tile.cpp
+ cmd/add_tileset.cpp
cmd/assign_color_profile.cpp
cmd/background_from_layer.cpp
cmd/clear_cel.cpp
@@ -446,6 +452,8 @@ add_library(app-lib
cmd/remove_palette.cpp
cmd/remove_slice.cpp
cmd/remove_tag.cpp
+ cmd/remove_tile.cpp
+ cmd/remove_tileset.cpp
cmd/replace_image.cpp
cmd/reselect_mask.cpp
cmd/set_cel_bounds.cpp
@@ -485,6 +493,7 @@ add_library(app-lib
cmd/with_slice.cpp
cmd/with_sprite.cpp
cmd/with_tag.cpp
+ cmd/with_tileset.cpp
cmd_sequence.cpp
cmd_transaction.cpp
color.cpp
@@ -593,7 +602,7 @@ add_library(app-lib
ui/layer_frame_comboboxes.cpp
util/autocrop.cpp
util/buffer_region.cpp
- util/create_cel_copy.cpp
+ util/cel_ops.cpp
util/expand_cel_canvas.cpp
util/filetoks.cpp
util/freetype_utils.cpp
diff --git a/src/app/cmd/add_tile.cpp b/src/app/cmd/add_tile.cpp
new file mode 100644
index 000000000..22ba04ad7
--- /dev/null
+++ b/src/app/cmd/add_tile.cpp
@@ -0,0 +1,92 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/cmd/add_tile.h"
+
+#include "doc/image_io.h"
+#include "doc/sprite.h"
+#include "doc/tileset.h"
+#include "doc/tilesets.h"
+
+namespace app {
+namespace cmd {
+
+using namespace doc;
+
+AddTile::AddTile(doc::Tileset* tileset,
+ const doc::ImageRef& image)
+ : WithTileset(tileset)
+ , WithImage(image.get())
+ , m_size(0)
+ , m_tileIndex(-1)
+ , m_imageRef(image)
+{
+}
+
+AddTile::AddTile(doc::Tileset* tileset,
+ const doc::tile_index ti)
+ : WithTileset(tileset)
+ , WithImage(tileset->get(ti).get())
+ , m_size(0)
+ , m_tileIndex(ti)
+ , m_imageRef(nullptr)
+{
+}
+
+void AddTile::onExecute()
+{
+ doc::Tileset* tileset = this->tileset();
+ ASSERT(tileset);
+
+ addTile(tileset, m_imageRef);
+
+ m_imageRef.reset();
+}
+
+void AddTile::onUndo()
+{
+ doc::Tileset* tileset = this->tileset();
+ ASSERT(tileset);
+
+ write_image(m_stream, image());
+ m_size = size_t(m_stream.tellp());
+
+ tileset->erase(m_tileIndex);
+
+ tileset->sprite()->incrementVersion();
+ tileset->incrementVersion();
+}
+
+void AddTile::onRedo()
+{
+ doc::Tileset* tileset = this->tileset();
+
+ ASSERT(!m_imageRef);
+ m_imageRef.reset(read_image(m_stream));
+ ASSERT(m_imageRef);
+
+ addTile(tileset, m_imageRef);
+ m_imageRef.reset();
+
+ m_stream.str(std::string());
+ m_stream.clear();
+ m_size = 0;
+}
+
+void AddTile::addTile(doc::Tileset* tileset, const doc::ImageRef& image)
+{
+ m_tileIndex = tileset->add(image);
+
+ tileset->sprite()->incrementVersion();
+ tileset->incrementVersion();
+}
+
+} // namespace cmd
+} // namespace app
diff --git a/src/app/cmd/add_tile.h b/src/app/cmd/add_tile.h
new file mode 100644
index 000000000..8ac5724f5
--- /dev/null
+++ b/src/app/cmd/add_tile.h
@@ -0,0 +1,56 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifndef APP_CMD_ADD_TILE_H_INCLUDED
+#define APP_CMD_ADD_TILE_H_INCLUDED
+#pragma once
+
+#include "app/cmd.h"
+#include "app/cmd/with_image.h"
+#include "app/cmd/with_tileset.h"
+#include "doc/image_ref.h"
+#include "doc/tile.h"
+
+#include
+
+namespace doc {
+ class Tileset;
+}
+
+namespace app {
+namespace cmd {
+
+ class AddTile : public Cmd
+ , public WithTileset
+ , public WithImage {
+ public:
+ AddTile(doc::Tileset* tileset, const doc::ImageRef& image);
+ AddTile(doc::Tileset* tileset, const doc::tile_index ti);
+
+ doc::tile_index tileIndex() const { return m_tileIndex; }
+
+ protected:
+ void onExecute() override;
+ void onUndo() override;
+ void onRedo() override;
+ size_t onMemSize() const override {
+ return sizeof(*this) + m_size;
+ }
+
+ private:
+ void addTile(doc::Tileset* tileset,
+ const doc::ImageRef& image);
+
+ size_t m_size;
+ std::stringstream m_stream;
+ doc::tile_index m_tileIndex;
+ doc::ImageRef m_imageRef;
+ };
+
+} // namespace cmd
+} // namespace app
+
+#endif
diff --git a/src/app/cmd/add_tileset.cpp b/src/app/cmd/add_tileset.cpp
new file mode 100644
index 000000000..36413b7ee
--- /dev/null
+++ b/src/app/cmd/add_tileset.cpp
@@ -0,0 +1,88 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/cmd/add_tileset.h"
+
+#include "doc/sprite.h"
+#include "doc/subobjects_io.h"
+#include "doc/tileset.h"
+#include "doc/tileset_io.h"
+#include "doc/tilesets.h"
+
+namespace app {
+namespace cmd {
+
+using namespace doc;
+
+AddTileset::AddTileset(doc::Sprite* sprite, doc::Tileset* tileset)
+ : WithSprite(sprite)
+ , WithTileset(tileset)
+ , m_size(0)
+ , m_tilesetIndex(-1)
+{
+}
+
+AddTileset::AddTileset(doc::Sprite* sprite, const doc::tileset_index tsi)
+ : WithSprite(sprite)
+ , WithTileset(sprite->tilesets()->get(tsi))
+ , m_size(0)
+ , m_tilesetIndex(tsi)
+{
+}
+
+void AddTileset::onExecute()
+{
+ Tileset* tileset = this->tileset();
+
+ addTileset(tileset);
+}
+
+void AddTileset::onUndo()
+{
+ doc::Tileset* tileset = this->tileset();
+ write_tileset(m_stream, tileset);
+ m_size = size_t(m_stream.tellp());
+
+ doc::Sprite* sprite = this->sprite();
+ sprite->tilesets()->erase(m_tilesetIndex);
+
+ sprite->incrementVersion();
+ sprite->tilesets()->incrementVersion();
+
+ delete tileset;
+}
+
+void AddTileset::onRedo()
+{
+ SubObjectsFromSprite io(sprite());
+ doc::Tileset* tileset = read_tileset(m_stream, &io);
+
+ addTileset(tileset);
+
+ m_stream.str(std::string());
+ m_stream.clear();
+ m_size = 0;
+}
+
+void AddTileset::addTileset(doc::Tileset* tileset)
+{
+ auto sprite = this->sprite();
+
+ if (m_tilesetIndex == -1)
+ m_tilesetIndex = sprite->tilesets()->add(tileset);
+ else
+ sprite->tilesets()->set(m_tilesetIndex, tileset);
+
+ sprite->incrementVersion();
+ sprite->tilesets()->incrementVersion();
+}
+
+} // namespace cmd
+} // namespace app
diff --git a/src/app/cmd/add_tileset.h b/src/app/cmd/add_tileset.h
new file mode 100644
index 000000000..b3be1c4e5
--- /dev/null
+++ b/src/app/cmd/add_tileset.h
@@ -0,0 +1,53 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifndef APP_CMD_ADD_TILESET_H_INCLUDED
+#define APP_CMD_ADD_TILESET_H_INCLUDED
+#pragma once
+
+#include "app/cmd.h"
+#include "app/cmd/with_sprite.h"
+#include "app/cmd/with_tileset.h"
+#include "doc/tile.h"
+
+#include
+
+namespace doc {
+ class Tileset;
+}
+
+namespace app {
+namespace cmd {
+
+ class AddTileset : public Cmd
+ , public WithSprite
+ , public WithTileset {
+ public:
+ AddTileset(doc::Sprite* sprite, doc::Tileset* tileset);
+ AddTileset(doc::Sprite* sprite, const doc::tileset_index tsi);
+
+ doc::tileset_index tilesetIndex() const { return m_tilesetIndex; }
+
+ protected:
+ void onExecute() override;
+ void onUndo() override;
+ void onRedo() override;
+ size_t onMemSize() const override {
+ return sizeof(*this) + m_size;
+ }
+
+ private:
+ void addTileset(doc::Tileset* tileset);
+
+ size_t m_size;
+ std::stringstream m_stream;
+ doc::tileset_index m_tilesetIndex;
+ };
+
+} // namespace cmd
+} // namespace app
+
+#endif
diff --git a/src/app/cmd/copy_cel.cpp b/src/app/cmd/copy_cel.cpp
index 395a1eccb..24cec3661 100644
--- a/src/app/cmd/copy_cel.cpp
+++ b/src/app/cmd/copy_cel.cpp
@@ -19,7 +19,7 @@
#include "app/cmd/set_cel_data.h"
#include "app/cmd/unlink_cel.h"
#include "app/doc.h"
-#include "app/util/create_cel_copy.h"
+#include "app/util/cel_ops.h"
#include "doc/cel.h"
#include "doc/layer.h"
#include "doc/primitives.h"
@@ -32,8 +32,8 @@ namespace cmd {
using namespace doc;
CopyCel::CopyCel(
- LayerImage* srcLayer, frame_t srcFrame,
- LayerImage* dstLayer, frame_t dstFrame, bool continuous)
+ Layer* srcLayer, frame_t srcFrame,
+ Layer* dstLayer, frame_t dstFrame, bool continuous)
: m_srcLayer(srcLayer)
, m_dstLayer(dstLayer)
, m_srcFrame(srcFrame)
@@ -44,8 +44,8 @@ CopyCel::CopyCel(
void CopyCel::onExecute()
{
- LayerImage* srcLayer = static_cast(m_srcLayer.layer());
- LayerImage* dstLayer = static_cast(m_dstLayer.layer());
+ Layer* srcLayer = m_srcLayer.layer();
+ Layer* dstLayer = m_dstLayer.layer();
ASSERT(srcLayer);
ASSERT(dstLayer);
@@ -114,7 +114,7 @@ void CopyCel::onExecute()
if (createLink)
dstCel = Cel::MakeLink(m_dstFrame, srcCel);
else
- dstCel = create_cel_copy(srcCel, dstSprite, dstLayer, m_dstFrame);
+ dstCel = create_cel_copy(this, srcCel, dstSprite, dstLayer, m_dstFrame);
executeAndAdd(new cmd::AddCel(dstLayer, dstCel));
}
diff --git a/src/app/cmd/copy_cel.h b/src/app/cmd/copy_cel.h
index 689cf3985..a0fd10375 100644
--- a/src/app/cmd/copy_cel.h
+++ b/src/app/cmd/copy_cel.h
@@ -1,4 +1,5 @@
// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2001-2015 David Capello
//
// This program is distributed under the terms of
@@ -13,10 +14,6 @@
#include "doc/color.h"
#include "doc/frame.h"
-namespace doc {
- class LayerImage;
-}
-
namespace app {
namespace cmd {
using namespace doc;
@@ -24,8 +21,8 @@ namespace cmd {
class CopyCel : public CmdSequence {
public:
CopyCel(
- LayerImage* srcLayer, frame_t srcFrame,
- LayerImage* dstLayer, frame_t dstFrame, bool continuous);
+ Layer* srcLayer, frame_t srcFrame,
+ Layer* dstLayer, frame_t dstFrame, bool continuous);
protected:
void onExecute() override;
diff --git a/src/app/cmd/crop_cel.cpp b/src/app/cmd/crop_cel.cpp
index 35b361038..d7141c6fe 100644
--- a/src/app/cmd/crop_cel.cpp
+++ b/src/app/cmd/crop_cel.cpp
@@ -1,4 +1,5 @@
// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2016 David Capello
//
// This program is distributed under the terms of
@@ -11,6 +12,7 @@
#include "app/cmd/crop_cel.h"
#include "doc/cel.h"
+#include "doc/layer.h"
#include "doc/primitives.h"
namespace app {
@@ -22,7 +24,9 @@ CropCel::CropCel(Cel* cel, const gfx::Rect& newBounds)
: WithCel(cel)
, m_oldOrigin(cel->position())
, m_newOrigin(newBounds.origin())
- , m_oldBounds(cel->bounds())
+ // Instead of using cel->bounds() we use the image size because it
+ // works for tilemaps too.
+ , m_oldBounds(cel->position(), cel->image()->size())
, m_newBounds(newBounds)
{
m_oldBounds.offset(-m_newOrigin);
@@ -59,7 +63,7 @@ void CropCel::cropImage(const gfx::Point& origin,
image->setId(id);
image->setVersion(ver);
image->incrementVersion();
- cel->data()->setImage(image);
+ cel->data()->setImage(image, cel->layer());
cel->data()->incrementVersion();
}
diff --git a/src/app/cmd/crop_cel.h b/src/app/cmd/crop_cel.h
index 4dc65c0da..66a98fcc0 100644
--- a/src/app/cmd/crop_cel.h
+++ b/src/app/cmd/crop_cel.h
@@ -1,4 +1,5 @@
// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2016 David Capello
//
// This program is distributed under the terms of
diff --git a/src/app/cmd/flatten_layers.cpp b/src/app/cmd/flatten_layers.cpp
index ff6eab0a7..87a10893f 100644
--- a/src/app/cmd/flatten_layers.cpp
+++ b/src/app/cmd/flatten_layers.cpp
@@ -124,7 +124,7 @@ void FlattenLayers::onExecute()
else {
gfx::Rect bounds(image->bounds());
if (doc::algorithm::shrink_bounds(
- image.get(), bounds, image->maskColor())) {
+ image.get(), image->maskColor(), nullptr, bounds)) {
cel_image.reset(
doc::crop_image(image.get(), bounds, image->maskColor()));
cel = new Cel(frame, cel_image);
diff --git a/src/app/cmd/move_cel.cpp b/src/app/cmd/move_cel.cpp
index 00fe95970..eb701f0d6 100644
--- a/src/app/cmd/move_cel.cpp
+++ b/src/app/cmd/move_cel.cpp
@@ -20,7 +20,7 @@
#include "app/cmd/set_cel_frame.h"
#include "app/cmd/unlink_cel.h"
#include "app/doc.h"
-#include "app/util/create_cel_copy.h"
+#include "app/util/cel_ops.h"
#include "doc/cel.h"
#include "doc/layer.h"
#include "doc/primitives.h"
@@ -119,7 +119,7 @@ void MoveCel::onExecute()
executeAndAdd(new cmd::SetCelFrame(srcCel, m_dstFrame));
}
else {
- dstCel = create_cel_copy(srcCel, dstSprite, dstLayer, m_dstFrame);
+ dstCel = create_cel_copy(this, srcCel, dstSprite, dstLayer, m_dstFrame);
executeAndAdd(new cmd::AddCel(dstLayer, dstCel));
executeAndAdd(new cmd::ClearCel(srcCel));
diff --git a/src/app/cmd/remove_tile.cpp b/src/app/cmd/remove_tile.cpp
new file mode 100644
index 000000000..085657f85
--- /dev/null
+++ b/src/app/cmd/remove_tile.cpp
@@ -0,0 +1,42 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/cmd/remove_tile.h"
+
+#include "doc/cel.h"
+#include "doc/layer.h"
+
+namespace app {
+namespace cmd {
+
+using namespace doc;
+
+RemoveTile::RemoveTile(Tileset* tileset, const tile_index ti)
+ : AddTile(tileset, ti)
+{
+}
+
+void RemoveTile::onExecute()
+{
+ AddTile::onUndo();
+}
+
+void RemoveTile::onUndo()
+{
+ AddTile::onRedo();
+}
+
+void RemoveTile::onRedo()
+{
+ AddTile::onUndo();
+}
+
+} // namespace cmd
+} // namespace app
diff --git a/src/app/cmd/remove_tile.h b/src/app/cmd/remove_tile.h
new file mode 100644
index 000000000..f49fdd71c
--- /dev/null
+++ b/src/app/cmd/remove_tile.h
@@ -0,0 +1,30 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifndef APP_CMD_REMOVE_TILE_H_INCLUDED
+#define APP_CMD_REMOVE_TILE_H_INCLUDED
+#pragma once
+
+#include "app/cmd/add_tile.h"
+
+namespace app {
+namespace cmd {
+ using namespace doc;
+
+ class RemoveTile : public AddTile {
+ public:
+ RemoveTile(Tileset* tileset, const tile_index ti);
+
+ protected:
+ void onExecute() override;
+ void onUndo() override;
+ void onRedo() override;
+ };
+
+} // namespace cmd
+} // namespace app
+
+#endif
diff --git a/src/app/cmd/remove_tileset.cpp b/src/app/cmd/remove_tileset.cpp
new file mode 100644
index 000000000..1f3b3a3e4
--- /dev/null
+++ b/src/app/cmd/remove_tileset.cpp
@@ -0,0 +1,43 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/cmd/remove_tileset.h"
+
+#include "doc/cel.h"
+#include "doc/layer.h"
+
+namespace app {
+namespace cmd {
+
+using namespace doc;
+
+RemoveTileset::RemoveTileset(Sprite* sprite,
+ const tileset_index si)
+ : AddTileset(sprite, si)
+{
+}
+
+void RemoveTileset::onExecute()
+{
+ AddTileset::onUndo();
+}
+
+void RemoveTileset::onUndo()
+{
+ AddTileset::onRedo();
+}
+
+void RemoveTileset::onRedo()
+{
+ AddTileset::onUndo();
+}
+
+} // namespace cmd
+} // namespace app
diff --git a/src/app/cmd/remove_tileset.h b/src/app/cmd/remove_tileset.h
new file mode 100644
index 000000000..234e5fec9
--- /dev/null
+++ b/src/app/cmd/remove_tileset.h
@@ -0,0 +1,31 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifndef APP_CMD_REMOVE_TILESET_H_INCLUDED
+#define APP_CMD_REMOVE_TILESET_H_INCLUDED
+#pragma once
+
+#include "app/cmd/add_tileset.h"
+
+namespace app {
+namespace cmd {
+ using namespace doc;
+
+ class RemoveTileset : public AddTileset {
+ public:
+ RemoveTileset(Sprite* sprite,
+ const tileset_index si);
+
+ protected:
+ void onExecute() override;
+ void onUndo() override;
+ void onRedo() override;
+ };
+
+} // namespace cmd
+} // namespace app
+
+#endif
diff --git a/src/app/cmd/set_cel_data.cpp b/src/app/cmd/set_cel_data.cpp
index 33bd01cae..031bb1368 100644
--- a/src/app/cmd/set_cel_data.cpp
+++ b/src/app/cmd/set_cel_data.cpp
@@ -14,6 +14,7 @@
#include "doc/image.h"
#include "doc/image_io.h"
#include "doc/image_ref.h"
+#include "doc/layer.h"
#include "doc/sprite.h"
#include "doc/subobjects_io.h"
@@ -81,7 +82,9 @@ void SetCelData::createCopy()
ASSERT(!m_dataCopy);
m_dataCopy.reset(new CelData(*cel->data()));
- m_dataCopy->setImage(ImageRef(Image::createCopy(cel->image())));
+ m_dataCopy->setImage(
+ ImageRef(Image::createCopy(cel->image())),
+ cel->layer());
}
} // namespace cmd
diff --git a/src/app/cmd/shift_masked_cel.cpp b/src/app/cmd/shift_masked_cel.cpp
index 37686cd12..2f19e17b1 100644
--- a/src/app/cmd/shift_masked_cel.cpp
+++ b/src/app/cmd/shift_masked_cel.cpp
@@ -59,7 +59,7 @@ void ShiftMaskedCel::shift(int dx, int dy)
newImage->setId(id);
newImage->setVersion(ver);
newImage->incrementVersion();
- cel->data()->setImage(newImage);
+ cel->data()->setImage(newImage, cel->layer());
}
cel->data()->setBounds(newBounds);
cel->data()->incrementVersion();
diff --git a/src/app/cmd/trim_cel.cpp b/src/app/cmd/trim_cel.cpp
index 44f1d7603..81cf979f8 100644
--- a/src/app/cmd/trim_cel.cpp
+++ b/src/app/cmd/trim_cel.cpp
@@ -25,9 +25,7 @@ using namespace doc;
TrimCel::TrimCel(Cel* cel)
{
gfx::Rect newBounds;
- if (algorithm::shrink_bounds(cel->image(), newBounds,
- cel->image()->maskColor())) {
- newBounds.offset(cel->position());
+ if (algorithm::shrink_cel_bounds(cel, cel->image()->maskColor(), newBounds)) {
if (cel->bounds() != newBounds) {
add(new cmd::CropCel(cel, newBounds));
}
diff --git a/src/app/cmd/unlink_cel.cpp b/src/app/cmd/unlink_cel.cpp
index 95057f859..5437588c6 100644
--- a/src/app/cmd/unlink_cel.cpp
+++ b/src/app/cmd/unlink_cel.cpp
@@ -12,6 +12,7 @@
#include "doc/cel.h"
#include "doc/image.h"
+#include "doc/layer.h"
#include "doc/sprite.h"
namespace app {
@@ -36,7 +37,7 @@ void UnlinkCel::onExecute()
ImageRef imgCopy(Image::createCopy(oldCelData->image()));
CelDataRef celDataCopy(new CelData(*oldCelData));
- celDataCopy->setImage(imgCopy);
+ celDataCopy->setImage(imgCopy, cel->layer());
celDataCopy->setUserData(oldCelData->userData());
if (m_newImageId) {
diff --git a/src/app/cmd/with_tileset.cpp b/src/app/cmd/with_tileset.cpp
new file mode 100644
index 000000000..bad3e74ed
--- /dev/null
+++ b/src/app/cmd/with_tileset.cpp
@@ -0,0 +1,31 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/cmd/with_tileset.h"
+
+#include "doc/tileset.h"
+
+namespace app {
+namespace cmd {
+
+using namespace doc;
+
+WithTileset::WithTileset(Tileset* tileset)
+ : m_tilesetId(tileset->id())
+{
+}
+
+Tileset* WithTileset::tileset()
+{
+ return get(m_tilesetId);
+}
+
+} // namespace cmd
+} // namespace app
diff --git a/src/app/cmd/with_tileset.h b/src/app/cmd/with_tileset.h
new file mode 100644
index 000000000..03295fe6b
--- /dev/null
+++ b/src/app/cmd/with_tileset.h
@@ -0,0 +1,32 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifndef APP_CMD_WITH_TILESET_H_INCLUDED
+#define APP_CMD_WITH_TILESET_H_INCLUDED
+#pragma once
+
+#include "doc/object_id.h"
+
+namespace doc {
+ class Tileset;
+}
+
+namespace app {
+namespace cmd {
+
+ class WithTileset {
+ public:
+ WithTileset(doc::Tileset* tileset);
+ doc::Tileset* tileset();
+
+ private:
+ doc::ObjectId m_tilesetId;
+ };
+
+} // namespace cmd
+} // namespace app
+
+#endif
diff --git a/src/app/cmd_sequence.h b/src/app/cmd_sequence.h
index f3936a400..43f00a62b 100644
--- a/src/app/cmd_sequence.h
+++ b/src/app/cmd_sequence.h
@@ -21,16 +21,16 @@ namespace app {
void add(Cmd* cmd);
+ // Helper to create a CmdSequence in the same onExecute() member
+ // function.
+ void executeAndAdd(Cmd* cmd);
+
protected:
void onExecute() override;
void onUndo() override;
void onRedo() override;
size_t onMemSize() const override;
- // Helper to create a CmdSequence in the same onExecute() member
- // function.
- void executeAndAdd(Cmd* cmd);
-
private:
std::vector m_cmds;
};
diff --git a/src/app/commands/cmd_clear_cel.cpp b/src/app/commands/cmd_clear_cel.cpp
index 002a746a7..48624649d 100644
--- a/src/app/commands/cmd_clear_cel.cpp
+++ b/src/app/commands/cmd_clear_cel.cpp
@@ -1,4 +1,5 @@
// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@@ -61,11 +62,9 @@ void ClearCelCommand::onExecute(Context* context)
continue;
}
- LayerImage* layerImage = static_cast(layer);
-
for (frame_t frame : site->selectedFrames().reversed()) {
- if (layerImage->cel(frame))
- document->getApi(tx).clearCel(layerImage, frame);
+ if (Cel* cel = layer->cel(frame))
+ document->getApi(tx).clearCel(cel);
}
}
}
diff --git a/src/app/commands/cmd_fill_and_stroke.cpp b/src/app/commands/cmd_fill_and_stroke.cpp
index a0e8f972d..ed01e9860 100644
--- a/src/app/commands/cmd_fill_and_stroke.cpp
+++ b/src/app/commands/cmd_fill_and_stroke.cpp
@@ -1,4 +1,5 @@
// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
diff --git a/src/app/commands/cmd_mask_content.cpp b/src/app/commands/cmd_mask_content.cpp
index df624bdc6..84ec57cb1 100644
--- a/src/app/commands/cmd_mask_content.cpp
+++ b/src/app/commands/cmd_mask_content.cpp
@@ -75,8 +75,8 @@ void MaskContentCommand::onExecute(Context* context)
Mask newMask;
gfx::Rect imgBounds = cel->image()->bounds();
- if (algorithm::shrink_bounds(cel->image(), imgBounds, color)) {
- newMask.replace(imgBounds.offset(cel->bounds().origin()));
+ if (algorithm::shrink_cel_bounds(cel, color, imgBounds)) {
+ newMask.replace(imgBounds);
}
else {
newMask.replace(cel->bounds());
diff --git a/src/app/commands/cmd_new_layer.cpp b/src/app/commands/cmd_new_layer.cpp
index 54c56514e..0ef48c35b 100644
--- a/src/app/commands/cmd_new_layer.cpp
+++ b/src/app/commands/cmd_new_layer.cpp
@@ -10,6 +10,7 @@
#endif
#include "app/app.h"
+#include "app/cmd/add_tileset.h"
#include "app/cmd/clear_mask.h"
#include "app/cmd/move_layer.h"
#include "app/cmd/trim_cel.h"
@@ -30,6 +31,7 @@
#include "app/util/clipboard.h"
#include "app/util/new_image_from_mask.h"
#include "doc/layer.h"
+#include "doc/layer_tilemap.h"
#include "doc/primitives.h"
#include "doc/sprite.h"
#include "fmt/format.h"
@@ -52,6 +54,7 @@ struct NewLayerParams : public NewParams {
Param name { this, std::string(), "name" };
Param group { this, false, "group" };
Param reference { this, false, "reference" };
+ Param tilemap { this, false, "tilemap" };
Param ask { this, false, "ask" };
Param fromFile { this, false, { "fromFile", "from-file" } };
Param fromClipboard { this, false, "fromClipboard" };
@@ -63,7 +66,7 @@ struct NewLayerParams : public NewParams {
class NewLayerCommand : public CommandWithNewParams {
public:
- enum class Type { Layer, Group, ReferenceLayer };
+ enum class Type { Layer, Group, ReferenceLayer, TilemapLayer };
enum class Place { AfterActiveLayer, BeforeActiveLayer, Top };
NewLayerCommand();
@@ -98,6 +101,10 @@ void NewLayerCommand::onLoadParams(const Params& commandParams)
m_type = Type::Group;
else if (params().reference())
m_type = Type::ReferenceLayer;
+ else if (params().tilemap())
+ m_type = Type::TilemapLayer;
+ else
+ m_type = Type::Layer;
m_place = Place::AfterActiveLayer;
if (params().top())
@@ -232,6 +239,20 @@ void NewLayerCommand::onExecute(Context* context)
layer->setReference(true);
afterBackground = true;
break;
+ case Type::TilemapLayer: {
+ // TODO show a dialog to configure the grid
+ auto grid = doc::Grid::MakeRect(gfx::Size(16, 16));
+ auto tileset = new Tileset(sprite, grid, 0);
+ auto addTileset = new cmd::AddTileset(sprite, tileset);
+ tx(addTileset);
+
+ auto tsi = addTileset->tilesetIndex();
+
+ layer = new LayerTilemap(sprite, tsi);
+ layer->setName(name);
+ api.addLayer(parent, layer, parent->lastLayer());
+ break;
+ }
}
ASSERT(layer);
@@ -460,6 +481,7 @@ std::string NewLayerCommand::layerPrefix() const
case Type::Layer: return Strings::commands_NewLayer_Layer();
case Type::Group: return Strings::commands_NewLayer_Group();
case Type::ReferenceLayer: return Strings::commands_NewLayer_ReferenceLayer();
+ case Type::TilemapLayer: return Strings::commands_NewLayer_TilemapLayer();
}
return "Unknown";
}
diff --git a/src/app/commands/cmd_toggle_tiles_mode.cpp b/src/app/commands/cmd_toggle_tiles_mode.cpp
new file mode 100644
index 000000000..a5721c2d2
--- /dev/null
+++ b/src/app/commands/cmd_toggle_tiles_mode.cpp
@@ -0,0 +1,42 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/app.h"
+#include "app/commands/command.h"
+#include "app/ui/color_bar.h"
+
+namespace app {
+
+using namespace gfx;
+
+class ToggleTilesModeCommand : public Command {
+public:
+ ToggleTilesModeCommand()
+ : Command(CommandId::ToggleTilesMode(), CmdUIOnlyFlag) {
+ }
+
+protected:
+ bool onChecked(Context* context) override {
+ auto colorBar = ColorBar::instance();
+ return colorBar->inTilesMode();
+ }
+
+ void onExecute(Context* context) override {
+ auto colorBar = ColorBar::instance();
+ colorBar->setTilesMode(!colorBar->inTilesMode());
+ }
+};
+
+Command* CommandFactory::createToggleTilesModeCommand()
+{
+ return new ToggleTilesModeCommand;
+}
+
+} // namespace app
diff --git a/src/app/commands/commands_list.h b/src/app/commands/commands_list.h
index 062d0850f..e022faf3d 100644
--- a/src/app/commands/commands_list.h
+++ b/src/app/commands/commands_list.h
@@ -156,6 +156,7 @@ FOR_EACH_COMMAND(SymmetryMode)
FOR_EACH_COMMAND(TiledMode)
FOR_EACH_COMMAND(Timeline)
FOR_EACH_COMMAND(TogglePreview)
+FOR_EACH_COMMAND(ToggleTilesMode)
FOR_EACH_COMMAND(ToggleTimelineThumbnails)
FOR_EACH_COMMAND(UndoHistory)
FOR_EACH_COMMAND(UnlinkCel)
diff --git a/src/app/doc.cpp b/src/app/doc.cpp
index 7df281c50..7a07c78c8 100644
--- a/src/app/doc.cpp
+++ b/src/app/doc.cpp
@@ -23,7 +23,7 @@
#include "app/file/format_options.h"
#include "app/flatten.h"
#include "app/pref/preferences.h"
-#include "app/util/create_cel_copy.h"
+#include "app/util/cel_ops.h"
#include "base/memory.h"
#include "doc/cel.h"
#include "doc/layer.h"
@@ -213,6 +213,13 @@ void Doc::notifySelectionBoundariesChanged()
notify_observers(&DocObserver::onSelectionBoundariesChanged, ev);
}
+void Doc::notifyTilesetChanged(Tileset* tileset)
+{
+ DocEvent ev(this);
+ ev.tileset(tileset);
+ notify_observers(&DocObserver::onTilesetChanged, ev);
+}
+
bool Doc::isModified() const
{
return !m_undo->isSavedState();
@@ -395,7 +402,8 @@ void Doc::copyLayerContent(const Layer* sourceLayer0, Doc* destDoc, Layer* destL
it->second));
}
else {
- newCel.reset(create_cel_copy(sourceCel,
+ newCel.reset(create_cel_copy(nullptr, // TODO add undo information?
+ sourceCel,
destLayer->sprite(),
destLayer,
sourceCel->frame()));
diff --git a/src/app/doc.h b/src/app/doc.h
index 78995f0c4..dde6f5872 100644
--- a/src/app/doc.h
+++ b/src/app/doc.h
@@ -34,6 +34,7 @@ namespace doc {
class Mask;
class MaskBoundaries;
class Sprite;
+ class Tileset;
}
namespace gfx {
@@ -103,6 +104,7 @@ namespace app {
void notifyCelCopied(Layer* fromLayer, frame_t fromFrame, Layer* toLayer, frame_t toFrame);
void notifySelectionChanged();
void notifySelectionBoundariesChanged();
+ void notifyTilesetChanged(Tileset* tileset);
//////////////////////////////////////////////////////////////////////
// File related properties
diff --git a/src/app/doc_api.cpp b/src/app/doc_api.cpp
index 1da070c67..b748e4b34 100644
--- a/src/app/doc_api.cpp
+++ b/src/app/doc_api.cpp
@@ -277,8 +277,8 @@ bool DocApi::cropCel(LayerImage* layer,
// Try to shrink the image ignoring transparent borders
gfx::Rect frameBounds;
if (doc::algorithm::shrink_bounds(newImage.get(),
- frameBounds,
- newImage->maskColor())) {
+ newImage->maskColor(), nullptr,
+ frameBounds)) {
// In this case the new cel image can be even smaller
if (frameBounds != newImage->bounds()) {
newImage = ImageRef(
@@ -534,8 +534,9 @@ void DocApi::setCelOpacity(Sprite* sprite, Cel* cel, int newOpacity)
m_transaction.execute(new cmd::SetCelOpacity(cel, newOpacity));
}
-void DocApi::clearCel(LayerImage* layer, frame_t frame)
+void DocApi::clearCel(Layer* layer, frame_t frame)
{
+ ASSERT(layer->isImage());
if (Cel* cel = layer->cel(frame))
clearCel(cel);
}
@@ -692,7 +693,11 @@ Layer* DocApi::duplicateLayerAfter(Layer* sourceLayer, LayerGroup* parent, Layer
ASSERT(parent);
std::unique_ptr newLayerPtr;
- if (sourceLayer->isImage())
+ if (sourceLayer->isTilemap()) {
+ newLayerPtr.reset(new LayerTilemap(sourceLayer->sprite(),
+ static_cast(sourceLayer)->tilesetIndex()));
+ }
+ else if (sourceLayer->isImage())
newLayerPtr.reset(new LayerImage(sourceLayer->sprite()));
else if (sourceLayer->isGroup())
newLayerPtr.reset(new LayerGroup(sourceLayer->sprite()));
diff --git a/src/app/doc_api.h b/src/app/doc_api.h
index 7637ddcaa..15f7da751 100644
--- a/src/app/doc_api.h
+++ b/src/app/doc_api.h
@@ -75,7 +75,7 @@ namespace app {
// Cels API
void addCel(LayerImage* layer, Cel* cel);
Cel* addCel(LayerImage* layer, frame_t frameNumber, const ImageRef& image);
- void clearCel(LayerImage* layer, frame_t frame);
+ void clearCel(Layer* layer, frame_t frame);
void clearCel(Cel* cel);
void clearCelAndAllLinks(Cel* cel);
void setCelPosition(Sprite* sprite, Cel* cel, int x, int y);
diff --git a/src/app/doc_event.h b/src/app/doc_event.h
index ac90e0a89..0859d2ac7 100644
--- a/src/app/doc_event.h
+++ b/src/app/doc_event.h
@@ -20,6 +20,7 @@ namespace doc {
class Slice;
class Sprite;
class Tag;
+ class Tileset;
}
namespace app {
@@ -37,6 +38,7 @@ namespace app {
, m_frame(0)
, m_tag(nullptr)
, m_slice(nullptr)
+ , m_tileset(nullptr)
, m_targetLayer(nullptr)
, m_targetFrame(0) {
}
@@ -51,6 +53,7 @@ namespace app {
doc::frame_t frame() const { return m_frame; }
doc::Tag* tag() const { return m_tag; }
doc::Slice* slice() const { return m_slice; }
+ doc::Tileset* tileset() const { return m_tileset; }
const gfx::Region& region() const { return m_region; }
void sprite(doc::Sprite* sprite) { m_sprite = sprite; }
@@ -61,6 +64,7 @@ namespace app {
void frame(doc::frame_t frame) { m_frame = frame; }
void tag(doc::Tag* tag) { m_tag = tag; }
void slice(doc::Slice* slice) { m_slice = slice; }
+ void tileset(doc::Tileset* tileset) { m_tileset = tileset; }
void region(const gfx::Region& rgn) { m_region = rgn; }
// Destination of the operation.
@@ -80,6 +84,7 @@ namespace app {
doc::frame_t m_frame;
doc::Tag* m_tag;
doc::Slice* m_slice;
+ doc::Tileset* m_tileset;
gfx::Region m_region;
// For copy/move commands, the m_layer/m_frame are source of the
diff --git a/src/app/doc_exporter.cpp b/src/app/doc_exporter.cpp
index af024326e..9bf7d3913 100644
--- a/src/app/doc_exporter.cpp
+++ b/src/app/doc_exporter.cpp
@@ -955,7 +955,11 @@ void DocExporter::captureSamples(Samples& samples,
else if (m_ignoreEmptyCels)
refColor = sprite->transparentColor();
- if (!algorithm::shrink_bounds(sampleRender.get(), spriteBounds, frameBounds, refColor)) {
+ if (!algorithm::shrink_bounds(sampleRender.get(),
+ refColor,
+ nullptr, // layer
+ spriteBounds, // startBounds
+ frameBounds)) { // output bounds
// If shrink_bounds() returns false, it's because the whole
// image is transparent (equal to the mask color).
diff --git a/src/app/doc_observer.h b/src/app/doc_observer.h
index 8d2a29477..5ed3de52c 100644
--- a/src/app/doc_observer.h
+++ b/src/app/doc_observer.h
@@ -76,6 +76,9 @@ namespace app {
// Slices
virtual void onSliceNameChange(DocEvent& ev) { }
+ // The tileset has changed.
+ virtual void onTilesetChanged(DocEvent& ev) { }
+
// Called to destroy the observable. (Here you could call "delete this".)
virtual void dispose() { }
};
diff --git a/src/app/file/ase_format.cpp b/src/app/file/ase_format.cpp
index f674249c9..73491dc43 100644
--- a/src/app/file/ase_format.cpp
+++ b/src/app/file/ase_format.cpp
@@ -127,6 +127,13 @@ static void ase_file_write_slice_chunks(FILE* f, dio::AsepriteFrameHeader* frame
static void ase_file_write_slice_chunk(FILE* f, dio::AsepriteFrameHeader* frame_header, Slice* slice,
const frame_t fromFrame, const frame_t toFrame);
static void ase_file_write_user_data_chunk(FILE* f, dio::AsepriteFrameHeader* frame_header, const UserData* userData);
+static void ase_file_write_tileset_chunks(FILE* f,
+ dio::AsepriteFrameHeader* frame_header,
+ const Tilesets* tilesets);
+static void ase_file_write_tileset_chunk(FILE* f,
+ dio::AsepriteFrameHeader* frame_header,
+ const Tileset* tileset,
+ const tileset_index si);
static bool ase_has_groups(LayerGroup* group);
static void ase_ungroup_all(LayerGroup* group);
@@ -305,6 +312,9 @@ bool AseFormat::onSave(FileOp* fop)
// Write extra chunks in the first frame
if (frame == fop->roi().fromFrame()) {
+ // Write tilesets
+ ase_file_write_tileset_chunks(f, &frame_header, sprite->tilesets());
+
// Write layer chunks
for (Layer* child : sprite->root()->layers())
ase_file_write_layers(f, &frame_header, child, 0);
@@ -315,7 +325,7 @@ bool AseFormat::onSave(FileOp* fop)
fop->roi().fromFrame(),
fop->roi().toFrame());
- // Writer slice chunks
+ // Write slice chunks
ase_file_write_slice_chunks(f, &frame_header,
sprite->slices(),
fop->roi().fromFrame(),
@@ -585,8 +595,15 @@ static void ase_file_write_layer_chunk(FILE* f, dio::AsepriteFrameHeader* frame_
static_cast(doc::LayerFlags::PersistentFlagsMask), f);
// Layer type
- fputw((layer->isImage() ? ASE_FILE_LAYER_IMAGE:
- (layer->isGroup() ? ASE_FILE_LAYER_GROUP: -1)), f);
+ int layerType = ASE_FILE_LAYER_IMAGE;
+ if (layer->isImage()) {
+ if (layer->isTilemap())
+ layerType = ASE_FILE_LAYER_TILEMAP;
+ }
+ else if (layer->isGroup()) {
+ layerType = ASE_FILE_LAYER_GROUP;
+ }
+ fputw(layerType, f);
// Layer child level
fputw(child_level, f);
@@ -602,6 +619,10 @@ static void ase_file_write_layer_chunk(FILE* f, dio::AsepriteFrameHeader* frame_
// Layer name
ase_file_write_string(f, layer->name());
+
+ // Tileset index
+ if (layer->isTilemap())
+ fputl(static_cast(layer)->tilesetIndex(), f);
}
//////////////////////////////////////////////////////////////////////
@@ -624,14 +645,12 @@ public:
fputc(rgba_getb(c), f);
fputc(rgba_geta(c), f);
}
- void write_scanline(RgbTraits::address_t address, int w, uint8_t* buffer)
- {
- for (int x=0; x
+class PixelIO {
+public:
+ void write_pixel(FILE* f, TilemapTraits::pixel_t c) {
+ fputl(c, f);
+ }
+ void write_scanline(TilemapTraits::address_t address, int w, uint8_t* buffer) {
+ for (int x=0; x> 8;
+ *(buffer++) = ((*address) & 0x00ff0000l) >> 16;
+ *(buffer++) = ((*address) & 0xff000000l) >> 24;
+ }
+ }
+};
+
//////////////////////////////////////////////////////////////////////
// Raw Image
//////////////////////////////////////////////////////////////////////
@@ -685,7 +717,7 @@ static void write_raw_image(FILE* f, const Image* image)
//////////////////////////////////////////////////////////////////////
template
-static void write_compressed_image(FILE* f, const Image* image)
+static void write_compressed_image_templ(FILE* f, const Image* image)
{
PixelIO pixel_io;
z_stream zstream;
@@ -734,6 +766,27 @@ static void write_compressed_image(FILE* f, const Image* image)
throw base::Exception("ZLib error %d in deflateEnd().", err);
}
+static void write_compressed_image(FILE* f, const Image* image)
+{
+ switch (image->pixelFormat()) {
+ case IMAGE_RGB:
+ write_compressed_image_templ(f, image);
+ break;
+
+ case IMAGE_GRAYSCALE:
+ write_compressed_image_templ(f, image);
+ break;
+
+ case IMAGE_INDEXED:
+ write_compressed_image_templ(f, image);
+ break;
+
+ case IMAGE_TILEMAP:
+ write_compressed_image_templ(f, image);
+ break;
+ }
+}
+
//////////////////////////////////////////////////////////////////////
// Cel Chunk
//////////////////////////////////////////////////////////////////////
@@ -762,7 +815,9 @@ static void ase_file_write_cel_chunk(FILE* f, dio::AsepriteFrameHeader* frame_he
link = nullptr;
}
- int cel_type = (link ? ASE_FILE_LINK_CEL: ASE_FILE_COMPRESSED_CEL);
+ int cel_type = (link ? ASE_FILE_LINK_CEL:
+ cel->layer()->isTilemap() ? ASE_FILE_COMPRESSED_TILEMAP:
+ ASE_FILE_COMPRESSED_CEL);
fputw(layer_index, f);
fputw(cel->x(), f);
@@ -795,6 +850,7 @@ static void ase_file_write_cel_chunk(FILE* f, dio::AsepriteFrameHeader* frame_he
case IMAGE_INDEXED:
write_raw_image(f, image);
break;
+
}
}
else {
@@ -812,27 +868,12 @@ static void ase_file_write_cel_chunk(FILE* f, dio::AsepriteFrameHeader* frame_he
case ASE_FILE_COMPRESSED_CEL: {
const Image* image = cel->image();
-
+ ASSERT(image);
if (image) {
// Width and height
fputw(image->width(), f);
fputw(image->height(), f);
-
- // Pixel data
- switch (image->pixelFormat()) {
-
- case IMAGE_RGB:
- write_compressed_image(f, image);
- break;
-
- case IMAGE_GRAYSCALE:
- write_compressed_image(f, image);
- break;
-
- case IMAGE_INDEXED:
- write_compressed_image(f, image);
- break;
- }
+ write_compressed_image(f, image);
}
else {
// Width and height
@@ -841,6 +882,23 @@ static void ase_file_write_cel_chunk(FILE* f, dio::AsepriteFrameHeader* frame_he
}
break;
}
+
+ case ASE_FILE_COMPRESSED_TILEMAP: {
+ const Image* image = cel->image();
+ ASSERT(image);
+ ASSERT(image->pixelFormat() == IMAGE_TILEMAP);
+
+ fputw(image->width(), f);
+ fputw(image->height(), f);
+ fputw(32, f); // TODO use different bpp when possible
+ fputl(tile_i_mask, f);
+ fputl(tile_f_flipx, f);
+ fputl(tile_f_flipy, f);
+ fputl(tile_f_90cw, f);
+ ase_file_write_padding(f, 10);
+
+ write_compressed_image(f, image);
+ }
}
}
@@ -1085,6 +1143,48 @@ static void ase_file_write_slice_chunk(FILE* f, dio::AsepriteFrameHeader* frame_
}
}
+static void ase_file_write_tileset_chunks(FILE* f,
+ dio::AsepriteFrameHeader* frame_header,
+ const Tilesets* tilesets)
+{
+ tileset_index si = 0;
+ for (const Tileset* tileset : *tilesets) {
+ ase_file_write_tileset_chunk(f, frame_header, tileset, si);
+ ++si;
+ }
+}
+
+static void ase_file_write_tileset_chunk(FILE* f,
+ dio::AsepriteFrameHeader* frame_header,
+ const Tileset* tileset,
+ const tileset_index si)
+{
+ ChunkWriter chunk(f, frame_header, ASE_FILE_CHUNK_TILESET);
+
+ fputl(si, f); // Tileset ID
+ fputl(2, f); // Tileset Flags (2=include tiles inside file)
+ fputw(tileset->grid().tileSize().w, f);
+ fputw(tileset->grid().tileSize().h, f);
+ ase_file_write_padding(f, 20);
+ ase_file_write_string(f, tileset->name()); // tileset name
+
+ // Flag 2 = tileset
+ fputl(tileset->size(), f);
+ for (tile_index i=0; isize(); ++i) {
+ fputl(0, f); // Flags (zero)
+ size_t beg = ftell(f);
+ fputl(0, f);
+
+ ImageRef tileImg = tileset->get(i);
+ write_compressed_image(f, tileImg.get());
+
+ size_t end = ftell(f);
+ fseek(f, beg, SEEK_SET);
+ fputl(end-beg-4, f); // Save the compressed data length
+ fseek(f, end, SEEK_SET);
+ }
+}
+
static bool ase_has_groups(LayerGroup* group)
{
for (Layer* child : group->layers()) {
diff --git a/src/app/file/file.cpp b/src/app/file/file.cpp
index 1059adaeb..197e47f9b 100644
--- a/src/app/file/file.cpp
+++ b/src/app/file/file.cpp
@@ -597,7 +597,8 @@ void FileOp::operate(IFileOpProgress* progress)
auto add_image = [&]() {
canvasSize |= m_seq.image->size();
- m_seq.last_cel->data()->setImage(m_seq.image);
+ m_seq.last_cel->data()->setImage(m_seq.image,
+ m_seq.layer);
m_seq.layer->addCel(m_seq.last_cel);
if (m_document->sprite()->palette(frame)
diff --git a/src/app/file/gif_format.cpp b/src/app/file/gif_format.cpp
index 2cc9141ed..202a14b38 100644
--- a/src/app/file/gif_format.cpp
+++ b/src/app/file/gif_format.cpp
@@ -693,7 +693,7 @@ private:
try {
ImageRef celImage(Image::createCopy(m_currentImage.get()));
try {
- cel->data()->setImage(celImage);
+ cel->data()->setImage(celImage, m_layer);
}
catch (...) {
throw;
diff --git a/src/app/script/app_object.cpp b/src/app/script/app_object.cpp
index f44338727..ee181139e 100644
--- a/src/app/script/app_object.cpp
+++ b/src/app/script/app_object.cpp
@@ -311,6 +311,13 @@ int App_useTool(lua_State* L)
if (!brush)
brush.reset(new Brush(BrushType::kCircleBrushType, 1, 0));
+ // How the tileset must be modified depending on this tool usage
+ type = lua_getfield(L, 1, "tilesetMode");
+ if (type != LUA_TNIL) {
+ site.tilesetMode(TilesetMode(lua_tointeger(L, -1)));
+ }
+ lua_pop(L, 1);
+
// Do the tool loop
type = lua_getfield(L, 1, "points");
if (type == LUA_TTABLE) {
diff --git a/src/app/script/engine.cpp b/src/app/script/engine.cpp
index 5759af94e..c54c47480 100644
--- a/src/app/script/engine.cpp
+++ b/src/app/script/engine.cpp
@@ -18,6 +18,7 @@
#include "app/script/luacpp.h"
#include "app/script/security.h"
#include "app/sprite_sheet_type.h"
+#include "app/tileset_mode.h"
#include "base/chrono.h"
#include "base/file_handle.h"
#include "base/fs.h"
@@ -152,6 +153,7 @@ void register_color_space_class(lua_State* L);
void register_dialog_class(lua_State* L);
void register_frame_class(lua_State* L);
void register_frames_class(lua_State* L);
+void register_grid_class(lua_State* L);
void register_image_class(lua_State* L);
void register_image_iterator_class(lua_State* L);
void register_image_spec_class(lua_State* L);
@@ -172,6 +174,8 @@ void register_sprite_class(lua_State* L);
void register_sprites_class(lua_State* L);
void register_tag_class(lua_State* L);
void register_tags_class(lua_State* L);
+void register_tileset_class(lua_State* L);
+void register_tilesets_class(lua_State* L);
void register_tool_class(lua_State* L);
void register_version_class(lua_State* L);
@@ -250,6 +254,7 @@ Engine::Engine()
setfield_integer(L, "GRAY", doc::ColorMode::GRAYSCALE);
setfield_integer(L, "GRAYSCALE", doc::ColorMode::GRAYSCALE);
setfield_integer(L, "INDEXED", doc::ColorMode::INDEXED);
+ setfield_integer(L, "TILEMAP", doc::ColorMode::TILEMAP);
lua_pop(L, 1);
lua_newtable(L);
@@ -341,6 +346,14 @@ Engine::Engine()
setfield_integer(L, "GRAYA", TARGET_GRAY_CHANNEL | TARGET_ALPHA_CHANNEL);
lua_pop(L, 1);
+ lua_newtable(L);
+ lua_pushvalue(L, -1);
+ lua_setglobal(L, "TilesetMode");
+ setfield_integer(L, "LOCKED", TilesetMode::Locked);
+ setfield_integer(L, "MODIFY_EXISTENT", TilesetMode::ModifyExistent);
+ setfield_integer(L, "GENERATE_ALL_TILES", TilesetMode::GenerateAllTiles);
+ lua_pop(L, 1);
+
// Register classes/prototypes
register_brush_class(L);
register_cel_class(L);
@@ -350,6 +363,7 @@ Engine::Engine()
register_dialog_class(L);
register_frame_class(L);
register_frames_class(L);
+ register_grid_class(L);
register_image_class(L);
register_image_iterator_class(L);
register_image_spec_class(L);
@@ -370,6 +384,8 @@ Engine::Engine()
register_sprites_class(L);
register_tag_class(L);
register_tags_class(L);
+ register_tileset_class(L);
+ register_tilesets_class(L);
register_tool_class(L);
register_version_class(L);
diff --git a/src/app/script/engine.h b/src/app/script/engine.h
index 0860e773e..561a0bf86 100644
--- a/src/app/script/engine.h
+++ b/src/app/script/engine.h
@@ -44,6 +44,8 @@ namespace doc {
class Palette;
class Sprite;
class Tag;
+ class Tileset;
+ class Tilesets;
class WithUserData;
}
@@ -142,6 +144,9 @@ namespace app {
void push_sprite_slices(lua_State* L, doc::Sprite* sprite);
void push_sprite_tags(lua_State* L, doc::Sprite* sprite);
void push_sprites(lua_State* L);
+ void push_tileset(lua_State* L, doc::Tileset* tileset);
+ void push_tileset_image(lua_State* L, doc::Tileset* tileset, doc::Image* image);
+ void push_tilesets(lua_State* L, doc::Tilesets* tilesets);
void push_tool(lua_State* L, tools::Tool* tool);
void push_userdata(lua_State* L, doc::WithUserData* userData);
void push_version(lua_State* L, const base::Version& ver);
diff --git a/src/app/script/grid_class.cpp b/src/app/script/grid_class.cpp
new file mode 100644
index 000000000..a473bcce1
--- /dev/null
+++ b/src/app/script/grid_class.cpp
@@ -0,0 +1,94 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/script/docobj.h"
+#include "app/script/engine.h"
+#include "app/script/luacpp.h"
+#include "doc/grid.h"
+
+namespace app {
+namespace script {
+
+using namespace doc;
+
+namespace {
+
+doc::Grid Grid_new(lua_State* L, int index)
+{
+ doc::Grid grid;
+ // Copy other size
+ if (auto grid2 = may_get_obj(L, index)) {
+ grid = *grid2;
+ }
+ // Convert Rectangle into a Grid
+ else if (lua_istable(L, index)) {
+ gfx::Rect rect = convert_args_into_rect(L, index);
+ doc::Grid grid(rect.size());
+ grid.origin(rect.origin());
+ }
+ return grid;
+}
+
+int Grid_new(lua_State* L)
+{
+ push_obj(L, Grid_new(L, 1));
+ return 1;
+}
+
+int Grid_gc(lua_State* L)
+{
+ get_obj(L, 1)->~Grid();
+ return 0;
+}
+
+int Grid_get_tileSize(lua_State* L)
+{
+ auto grid = get_obj(L, 1);
+ push_obj(L, grid->tileSize());
+ return 1;
+}
+
+int Grid_get_origin(lua_State* L)
+{
+ auto grid = get_obj(L, 1);
+ push_obj(L, grid->origin());
+ return 1;
+}
+
+const luaL_Reg Grid_methods[] = {
+ { "__gc", Grid_gc },
+ { nullptr, nullptr }
+};
+
+const Property Grid_properties[] = {
+ { "tileSize", Grid_get_tileSize, nullptr },
+ { "origin", Grid_get_origin, nullptr },
+ { nullptr, nullptr, nullptr }
+};
+
+} // anonymous namespace
+
+DEF_MTNAME(Grid);
+
+void register_grid_class(lua_State* L)
+{
+ using Grid = doc::Grid;
+ REG_CLASS(L, Grid);
+ REG_CLASS_NEW(L, Grid);
+ REG_CLASS_PROPERTIES(L, Grid);
+}
+
+doc::Grid convert_args_into_grid(lua_State* L, int index)
+{
+ return Grid_new(L, index);
+}
+
+} // namespace script
+} // namespace app
diff --git a/src/app/script/image_class.cpp b/src/app/script/image_class.cpp
index 19ab5fe9d..5eec1bb71 100644
--- a/src/app/script/image_class.cpp
+++ b/src/app/script/image_class.cpp
@@ -43,6 +43,7 @@ namespace {
struct ImageObj {
doc::ObjectId imageId = 0;
doc::ObjectId celId = 0;
+ doc::ObjectId tilesetId = 0;
ImageObj(doc::Image* image)
: imageId(image->id()) {
}
@@ -50,6 +51,10 @@ struct ImageObj {
: imageId(cel->image()->id())
, celId(cel->id()) {
}
+ ImageObj(doc::Tileset* tileset, doc::Image* image)
+ : imageId(image->id())
+ , tilesetId(tileset->id()) {
+ }
ImageObj(const ImageObj&) = delete;
ImageObj& operator=(const ImageObj&) = delete;
@@ -58,7 +63,7 @@ struct ImageObj {
}
void gc(lua_State* L) {
- if (!celId)
+ if (!celId && !tilesetId)
delete this->image(L);
imageId = 0;
}
@@ -551,6 +556,11 @@ void push_image(lua_State* L, doc::Image* image)
push_new(L, image);
}
+void push_tileset_image(lua_State* L, doc::Tileset* tileset, doc::Image* image)
+{
+ push_new(L, tileset, image);
+}
+
doc::Image* may_get_image_from_arg(lua_State* L, int index)
{
auto obj = may_get_obj(L, index);
diff --git a/src/app/script/layer_class.cpp b/src/app/script/layer_class.cpp
index 518e790df..9d51a597b 100644
--- a/src/app/script/layer_class.cpp
+++ b/src/app/script/layer_class.cpp
@@ -21,6 +21,7 @@
#include "app/tx.h"
#include "base/clamp.h"
#include "doc/layer.h"
+#include "doc/layer_tilemap.h"
#include "doc/sprite.h"
namespace app {
@@ -152,6 +153,13 @@ int Layer_get_isGroup(lua_State* L)
return 1;
}
+int Layer_get_isTilemap(lua_State* L)
+{
+ auto layer = get_docobj(L, 1);
+ lua_pushboolean(L, layer->isTilemap());
+ return 1;
+}
+
int Layer_get_isTransparent(lua_State* L)
{
auto layer = get_docobj(L, 1);
@@ -208,6 +216,16 @@ int Layer_get_cels(lua_State* L)
return 1;
}
+int Layer_get_tileset(lua_State* L)
+{
+ auto layer = get_docobj(L, 1);
+ if (layer->isTilemap())
+ push_tileset(L, static_cast(layer)->tileset());
+ else
+ lua_pushnil(L);
+ return 1;
+}
+
int Layer_set_name(lua_State* L)
{
auto layer = get_docobj(L, 1);
@@ -366,6 +384,7 @@ const Property Layer_properties[] = {
{ "blendMode", Layer_get_blendMode, Layer_set_blendMode },
{ "isImage", Layer_get_isImage, nullptr },
{ "isGroup", Layer_get_isGroup, nullptr },
+ { "isTilemap", Layer_get_isTilemap, nullptr },
{ "isTransparent", Layer_get_isTransparent, nullptr },
{ "isBackground", Layer_get_isBackground, nullptr },
{ "isEditable", Layer_get_isEditable, Layer_set_isEditable },
@@ -376,6 +395,7 @@ const Property Layer_properties[] = {
{ "cels", Layer_get_cels, nullptr },
{ "color", UserData_get_color, UserData_set_color },
{ "data", UserData_get_text, UserData_set_text },
+ { "tileset", Layer_get_tileset, nullptr },
{ nullptr, nullptr, nullptr }
};
diff --git a/src/app/script/pixel_color_object.cpp b/src/app/script/pixel_color_object.cpp
index 2d4fbb55e..63d01c192 100644
--- a/src/app/script/pixel_color_object.cpp
+++ b/src/app/script/pixel_color_object.cpp
@@ -10,6 +10,7 @@
#include "app/script/luacpp.h"
#include "doc/color.h"
+#include "doc/tile.h"
namespace app {
namespace script {
@@ -70,6 +71,26 @@ int PixelColor_grayaA(lua_State* L)
return 1;
}
+int PixelColor_tile(lua_State* L)
+{
+ const int i = lua_tointeger(L, 1);
+ const int f = lua_tointeger(L, 2);
+ lua_pushinteger(L, doc::tile(i, f));
+ return 1;
+}
+
+int PixelColor_tileI(lua_State* L)
+{
+ lua_pushinteger(L, doc::tile_geti(lua_tointeger(L, 1)));
+ return 1;
+}
+
+int PixelColor_tileF(lua_State* L)
+{
+ lua_pushinteger(L, doc::tile_getf(lua_tointeger(L, 1)));
+ return 1;
+}
+
const luaL_Reg PixelColor_methods[] = {
{ "rgba", PixelColor_rgba },
{ "rgbaR", PixelColor_rgbaR },
@@ -79,6 +100,9 @@ const luaL_Reg PixelColor_methods[] = {
{ "graya", PixelColor_graya },
{ "grayaV", PixelColor_grayaV },
{ "grayaA", PixelColor_grayaA },
+ { "tile", PixelColor_tile },
+ { "tileI", PixelColor_tileI },
+ { "tileF", PixelColor_tileF },
{ nullptr, nullptr }
};
diff --git a/src/app/script/sprite_class.cpp b/src/app/script/sprite_class.cpp
index f8e241815..dfa3c9539 100644
--- a/src/app/script/sprite_class.cpp
+++ b/src/app/script/sprite_class.cpp
@@ -662,6 +662,13 @@ int Sprite_get_slices(lua_State* L)
return 1;
}
+int Sprite_get_tilesets(lua_State* L)
+{
+ auto sprite = get_docobj(L, 1);
+ push_tilesets(L, sprite->tilesets());
+ return 1;
+}
+
int Sprite_get_backgroundLayer(lua_State* L)
{
auto sprite = get_docobj(L, 1);
@@ -799,6 +806,7 @@ const Property Sprite_properties[] = {
{ "cels", Sprite_get_cels, nullptr },
{ "tags", Sprite_get_tags, nullptr },
{ "slices", Sprite_get_slices, nullptr },
+ { "tilesets", Sprite_get_tilesets, nullptr },
{ "backgroundLayer", Sprite_get_backgroundLayer, nullptr },
{ "transparentColor", Sprite_get_transparentColor, Sprite_set_transparentColor },
{ "bounds", Sprite_get_bounds, nullptr },
diff --git a/src/app/script/tileset_class.cpp b/src/app/script/tileset_class.cpp
new file mode 100644
index 000000000..2015ee465
--- /dev/null
+++ b/src/app/script/tileset_class.cpp
@@ -0,0 +1,104 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/script/docobj.h"
+#include "app/script/engine.h"
+#include "app/script/luacpp.h"
+#include "doc/tileset.h"
+
+namespace app {
+namespace script {
+
+using namespace doc;
+
+namespace {
+
+int Tileset_eq(lua_State* L)
+{
+ const auto a = get_docobj(L, 1);
+ const auto b = get_docobj(L, 2);
+ lua_pushboolean(L, a->id() == b->id());
+ return 1;
+}
+
+int Tileset_len(lua_State* L)
+{
+ auto tileset = get_docobj(L, 1);
+ lua_pushinteger(L, tileset->size());
+ return 1;
+}
+
+int Tileset_getTile(lua_State* L)
+{
+ auto tileset = get_docobj(L, 1);
+ tile_index i = lua_tointeger(L, 2);
+ ImageRef image = tileset->get(i);
+ if (image)
+ push_tileset_image(L, tileset, image.get());
+ else
+ lua_pushnil(L);
+ return 1;
+}
+
+int Tileset_get_name(lua_State* L)
+{
+ auto tileset = get_docobj(L, 1);
+ lua_pushstring(L, tileset->name().c_str());
+ return 1;
+}
+
+int Tileset_set_name(lua_State* L)
+{
+ auto tileset = get_docobj(L, 1);
+ if (const char* newName = lua_tostring(L, 2))
+ tileset->setName(newName);
+ return 0;
+}
+
+int Tileset_get_grid(lua_State* L)
+{
+ auto tileset = get_docobj(L, 1);
+ push_obj(L, tileset->grid());
+ return 1;
+}
+
+const luaL_Reg Tileset_methods[] = {
+ { "__eq", Tileset_eq },
+ { "__len", Tileset_len },
+ { "getTile", Tileset_getTile },
+ // TODO
+ // { "setTile", Tileset_setTile },
+ { nullptr, nullptr }
+};
+
+const Property Tileset_properties[] = {
+ { "name", Tileset_get_name, Tileset_set_name },
+ { "grid", Tileset_get_grid, nullptr },
+ { nullptr, nullptr, nullptr }
+};
+
+} // anonymous namespace
+
+DEF_MTNAME(Tileset);
+
+void register_tileset_class(lua_State* L)
+{
+ using Tileset = doc::Tileset;
+ REG_CLASS(L, Tileset);
+ REG_CLASS_PROPERTIES(L, Tileset);
+}
+
+void push_tileset(lua_State* L, Tileset* tileset)
+{
+ push_docobj(L, tileset);
+}
+
+} // namespace script
+} // namespace app
diff --git a/src/app/script/tilesets_class.cpp b/src/app/script/tilesets_class.cpp
new file mode 100644
index 000000000..437be52ec
--- /dev/null
+++ b/src/app/script/tilesets_class.cpp
@@ -0,0 +1,62 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/script/docobj.h"
+#include "app/script/engine.h"
+#include "app/script/luacpp.h"
+#include "doc/tilesets.h"
+
+namespace app {
+namespace script {
+
+using namespace doc;
+
+namespace {
+
+int Tilesets_len(lua_State* L)
+{
+ auto obj = get_docobj(L, 1);
+ lua_pushinteger(L, obj->size());
+ return 1;
+}
+
+int Tilesets_index(lua_State* L)
+{
+ auto obj = get_docobj(L, 1);
+ const int i = lua_tonumber(L, 2);
+ if (i >= 1 && i <= int(obj->size()))
+ push_docobj(L, *(obj->begin()+i-1));
+ else
+ lua_pushnil(L);
+ return 1;
+}
+
+const luaL_Reg Tilesets_methods[] = {
+ { "__len", Tilesets_len },
+ { "__index", Tilesets_index },
+ { nullptr, nullptr }
+};
+
+} // anonymous namespace
+
+DEF_MTNAME(Tilesets);
+
+void register_tilesets_class(lua_State* L)
+{
+ REG_CLASS(L, Tilesets);
+}
+
+void push_tilesets(lua_State* L, Tilesets* tilesets)
+{
+ push_docobj(L, tilesets);
+}
+
+} // namespace script
+} // namespace app
diff --git a/src/app/site.cpp b/src/app/site.cpp
index 214f00653..130f430eb 100644
--- a/src/app/site.cpp
+++ b/src/app/site.cpp
@@ -14,8 +14,11 @@
#include "app/pref/preferences.h"
#include "base/base.h"
#include "doc/cel.h"
+#include "doc/grid.h"
#include "doc/layer.h"
+#include "doc/layer_tilemap.h"
#include "doc/sprite.h"
+#include "doc/tileset.h"
#include "ui/system.h"
namespace app {
@@ -81,6 +84,15 @@ void Site::range(const DocRange& range)
gfx::Rect Site::gridBounds() const
{
+ if (m_layer && m_layer->isTilemap()) {
+ const Cel* cel = (m_layer ? m_layer->cel(m_frame): nullptr);
+ if (cel) {
+ const Grid& grid = static_cast(m_layer)->tileset()->grid();
+ return gfx::Rect(grid.tileOffset() + cel->bounds().origin(),
+ grid.tileSize());
+ }
+ }
+
gfx::Rect bounds;
if (m_sprite) {
bounds = m_sprite->gridBounds();
diff --git a/src/app/site.h b/src/app/site.h
index 013341c69..ad10a780d 100644
--- a/src/app/site.h
+++ b/src/app/site.h
@@ -10,6 +10,7 @@
#pragma once
#include "app/doc_range.h"
+#include "app/tileset_mode.h"
#include "doc/frame.h"
#include "doc/palette_picks.h"
#include "doc/selected_objects.h"
@@ -49,7 +50,8 @@ namespace app {
, m_document(nullptr)
, m_sprite(nullptr)
, m_layer(nullptr)
- , m_frame(0) { }
+ , m_frame(0)
+ , m_tilesetMode(TilesetMode::Locked) { }
const Focus focus() const { return m_focus; }
bool inEditor() const { return m_focus == InEditor; }
@@ -101,6 +103,9 @@ namespace app {
gfx::Rect gridBounds() const;
+ void tilesetMode(const TilesetMode& mode) { m_tilesetMode = mode; }
+ const TilesetMode& tilesetMode() const { return m_tilesetMode; }
+
private:
Focus m_focus;
Doc* m_document;
@@ -110,6 +115,7 @@ namespace app {
DocRange m_range;
doc::PalettePicks m_selectedColors;
doc::SelectedObjects m_selectedSlices;
+ TilesetMode m_tilesetMode;
};
} // namespace app
diff --git a/src/app/tileset_mode.h b/src/app/tileset_mode.h
new file mode 100644
index 000000000..54c73882d
--- /dev/null
+++ b/src/app/tileset_mode.h
@@ -0,0 +1,24 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifndef APP_TILES_MODE_H_INCLUDED
+#define APP_TILES_MODE_H_INCLUDED
+#pragma once
+
+namespace app {
+
+ // These modes are available edition modes for the tileset when an
+ // tilemap is edited.
+ enum class TilesetMode {
+ Locked, // Cannot edit the tileset (don't modify or add new tiles)
+ ModifyExistent, // Modify existent tiles (don't create new ones)
+ //GenerateNewTiles, // Auto-generate new tiles, edit existent ones
+ GenerateAllTiles, // Auto-generate new and modified tiles
+ };
+
+} // namespace app
+
+#endif
diff --git a/src/app/transaction.h b/src/app/transaction.h
index b577465f2..15a542d74 100644
--- a/src/app/transaction.h
+++ b/src/app/transaction.h
@@ -9,6 +9,7 @@
#define APP_TRANSACTION_H_INCLUDED
#pragma once
+#include "app/cmd_transaction.h"
#include "app/doc_observer.h"
#include
@@ -16,7 +17,6 @@
namespace app {
class Cmd;
- class CmdTransaction;
class Context;
class DocRange;
class DocUndo;
@@ -82,6 +82,8 @@ namespace app {
void execute(Cmd* cmd);
+ CmdTransaction* cmds() { return m_cmds; }
+
private:
// List of changes during the execution of this transaction
enum class Changes {
diff --git a/src/app/tx.h b/src/app/tx.h
index 9f9c483bf..ed980bb87 100644
--- a/src/app/tx.h
+++ b/src/app/tx.h
@@ -73,6 +73,10 @@ namespace app {
return *m_transaction;
}
+ operator CmdTransaction*() {
+ return m_transaction->cmds();
+ }
+
private:
Doc* m_doc;
Transaction* m_transaction;
diff --git a/src/app/ui/color_bar.cpp b/src/app/ui/color_bar.cpp
index e2777619f..6e6edd188 100644
--- a/src/app/ui/color_bar.cpp
+++ b/src/app/ui/color_bar.cpp
@@ -16,6 +16,7 @@
#include "app/app.h"
#include "app/app_menus.h"
#include "app/cmd/remap_colors.h"
+#include "app/cmd/remove_tile.h"
#include "app/cmd/replace_image.h"
#include "app/cmd/set_palette.h"
#include "app/cmd/set_transparent_color.h"
@@ -27,8 +28,8 @@
#include "app/commands/quick_command.h"
#include "app/console.h"
#include "app/context_access.h"
-#include "app/doc_undo.h"
#include "app/doc_api.h"
+#include "app/doc_undo.h"
#include "app/i18n/strings.h"
#include "app/ini_file.h"
#include "app/modules/editors.h"
@@ -56,6 +57,7 @@
#include "doc/cels_range.h"
#include "doc/image.h"
#include "doc/image_impl.h"
+#include "doc/layer_tilemap.h"
#include "doc/palette.h"
#include "doc/primitives.h"
#include "doc/remap.h"
@@ -86,6 +88,12 @@ enum class PalButton {
MAX
};
+enum class TilesButton {
+ MODE,
+ AUTO,
+ MAX
+};
+
using namespace app::skin;
using namespace ui;
@@ -111,13 +119,24 @@ ColorBar::ScrollableView::ScrollableView()
void ColorBar::ScrollableView::onInitTheme(InitThemeEvent& ev)
{
+ auto hbar = horizontalBar();
+ auto vbar = verticalBar();
+ setup_mini_look(hbar);
+ setup_mini_look(vbar);
+
+ View::onInitTheme(ev);
+
SkinTheme* theme = static_cast(this->theme());
setStyle(theme->styles.colorbarView());
- horizontalBar()->setStyle(theme->styles.miniScrollbar());
- verticalBar()->setStyle(theme->styles.miniScrollbar());
- horizontalBar()->setThumbStyle(theme->styles.miniScrollbarThumb());
- verticalBar()->setThumbStyle(theme->styles.miniScrollbarThumb());
+ hbar->setStyle(theme->styles.miniScrollbar());
+ vbar->setStyle(theme->styles.miniScrollbar());
+ hbar->setThumbStyle(theme->styles.miniScrollbarThumb());
+ vbar->setThumbStyle(theme->styles.miniScrollbarThumb());
+
+ const int scrollBarWidth = theme->dimensions.miniScrollbarSize();
+ hbar->setBarWidth(scrollBarWidth);
+ vbar->setBarWidth(scrollBarWidth);
}
//////////////////////////////////////////////////////////////////////
@@ -128,8 +147,10 @@ ColorBar* ColorBar::m_instance = NULL;
ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
: Box(align)
, m_buttons(int(PalButton::MAX))
+ , m_tilesButtons(int(TilesButton::MAX))
, m_splitter(Splitter::ByPercentage, VERTICAL)
, m_paletteView(true, PaletteView::FgBgColors, this, 16)
+ , m_tilesView(true, PaletteView::FgBgTiles, this, 16)
, m_remapButton("Remap")
, m_selector(ColorSelector::NONE)
, m_tintShadeTone(nullptr)
@@ -144,9 +165,12 @@ ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
, m_fromFgButton(false)
, m_fromBgButton(false)
, m_lastDocument(nullptr)
+ , m_lastTilesetId(doc::NullId)
, m_ascending(true)
, m_lastButtons(kButtonLeft)
, m_editMode(false)
+ , m_tilesMode(false)
+ , m_autoTilesMode(true)
, m_redrawTimer(250, this)
, m_redrawAll(false)
, m_implantChange(false)
@@ -161,17 +185,24 @@ ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
m_buttons.addItem(theme->parts.palPresets());
m_buttons.addItem(theme->parts.palOptions());
+ m_tilesButtons.addItem("Tiles");
+ m_tilesButtons.addItem("Auto");
+ m_tilesButtons.setVisible(false);
+ m_tilesButtons.getItem((int)TilesButton::AUTO)->setHotColor(theme->colors.editPalFace());
+
m_paletteView.setColumns(8);
+ m_tilesView.setColumns(8);
- setup_mini_look(m_scrollableView.horizontalBar());
- setup_mini_look(m_scrollableView.verticalBar());
-
- m_scrollableView.attachToView(&m_paletteView);
- m_scrollableView.setExpansive(true);
+ m_scrollablePalView.attachToView(&m_paletteView);
+ m_scrollableTilesView.attachToView(&m_tilesView);
+ m_scrollablePalView.setExpansive(true);
+ m_scrollableTilesView.setExpansive(true);
+ m_scrollableTilesView.setVisible(false);
m_remapButton.setVisible(false);
- m_palettePlaceholder.addChild(&m_scrollableView);
+ m_palettePlaceholder.addChild(&m_scrollablePalView);
+ m_palettePlaceholder.addChild(&m_scrollableTilesView);
m_palettePlaceholder.addChild(&m_remapButton);
m_splitter.setId("palette_spectrum_splitter");
m_splitter.setPosition(80);
@@ -183,6 +214,7 @@ ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
Preferences::instance().colorBar.selector());
addChild(&m_buttons);
+ addChild(&m_tilesButtons);
addChild(&m_splitter);
HBox* fgBox = new HBox;
@@ -207,6 +239,7 @@ ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
m_bgWarningIcon->Click.connect(base::Bind(&ColorBar::onFixWarningClick, this, &m_bgColor, m_bgWarningIcon));
m_redrawTimer.Tick.connect(base::Bind(&ColorBar::onTimerTick, this));
m_buttons.ItemChange.connect(base::Bind(&ColorBar::onPaletteButtonClick, this));
+ m_tilesButtons.ItemChange.connect(base::Bind(&ColorBar::onTilesButtonClick, this));
tooltipManager->addTooltipFor(&m_fgColor, "Foreground color", LEFT);
tooltipManager->addTooltipFor(&m_bgColor, "Background color", LEFT);
@@ -230,17 +263,16 @@ ColorBar::ColorBar(int align, TooltipManager* tooltipManager)
m_buttons.setMaxSize(gfx::Size(m_buttons.sizeHint().w,
theme->dimensions.colorBarButtonsHeight()));
- int scrollBarWidth = theme->dimensions.miniScrollbarSize();
- m_scrollableView.horizontalBar()->setBarWidth(scrollBarWidth);
- m_scrollableView.verticalBar()->setBarWidth(scrollBarWidth);
-
// Change color-bar background color (not ColorBar::setBgColor)
this->Widget::setBgColor(theme->colors.tabActiveFace());
m_paletteView.setBgColor(theme->colors.tabActiveFace());
- m_paletteView.setBoxSize(
- Preferences::instance().colorBar.boxSize());
+ m_paletteView.setBoxSize(Preferences::instance().colorBar.boxSize());
m_paletteView.initTheme();
+ m_tilesView.setBgColor(theme->colors.tabActiveFace());
+ m_tilesView.setBoxSize(Preferences::instance().colorBar.tilesBoxSize());
+ m_tilesView.initTheme();
+
// Styles
m_splitter.setStyle(theme->styles.workspaceSplitter());
@@ -420,6 +452,66 @@ void ColorBar::setEditMode(bool state)
m_paletteView.deselect();
}
+bool ColorBar::inTilesMode() const
+{
+ return
+ (m_tilesMode &&
+ m_tilesButtons.isVisible() &&
+ m_lastDocument &&
+ m_lastDocument->sprite());
+}
+
+void ColorBar::setTilesMode(bool state)
+{
+ const Site site = UIContext::instance()->activeSite();
+ const bool isTilemap = (site.layer() && site.layer()->isTilemap());
+
+ SkinTheme* theme = static_cast(this->theme());
+ ButtonSet::Item* item = m_tilesButtons.getItem((int)TilesButton::MODE);
+
+ m_tilesMode = state;
+ item->setHotColor(state ? theme->colors.editPalFace(): gfx::ColorNone);
+
+ if (state && isTilemap) {
+ manager()->freeWidget(&m_paletteView);
+ m_scrollablePalView.setVisible(false);
+ m_scrollableTilesView.setVisible(true);
+ }
+ else {
+ manager()->freeWidget(&m_tilesView);
+ m_scrollablePalView.setVisible(true);
+ m_scrollableTilesView.setVisible(false);
+ }
+
+ layout();
+}
+
+bool ColorBar::inAutoTilesMode() const
+{
+ return
+ (m_autoTilesMode &&
+ m_tilesButtons.isVisible() &&
+ m_lastDocument &&
+ m_lastDocument->sprite());
+}
+
+void ColorBar::setAutoTilesMode(bool state)
+{
+ SkinTheme* theme = static_cast(this->theme());
+ ButtonSet::Item* item = m_tilesButtons.getItem((int)TilesButton::AUTO);
+
+ m_autoTilesMode = state;
+ item->setHotColor(state ? theme->colors.editPalFace(): gfx::ColorNone);
+}
+
+TilesetMode ColorBar::tilesetMode() const
+{
+ if (inAutoTilesMode())
+ return TilesetMode::GenerateAllTiles;
+ else
+ return TilesetMode::ModifyExistent;
+}
+
void ColorBar::onActiveSiteChange(const Site& site)
{
if (m_lastDocument != site.document()) {
@@ -433,6 +525,29 @@ void ColorBar::onActiveSiteChange(const Site& site)
hideRemap();
}
+
+ bool isTilemap = false;
+ if (site.layer()) {
+ isTilemap = site.layer()->isTilemap();
+ if (m_tilesButtons.isVisible() != isTilemap) {
+ m_tilesButtons.setVisible(isTilemap);
+ layout();
+ }
+
+ if (isTilemap) {
+ doc::ObjectId newTilesetId =
+ static_cast(site.layer())->tileset()->id();
+ if (m_lastTilesetId != newTilesetId) {
+ m_lastTilesetId = newTilesetId;
+ m_scrollableTilesView.updateView();
+ }
+ }
+ }
+ if (!isTilemap) {
+ m_lastTilesetId = doc::NullId;
+ if (inTilesMode())
+ setTilesMode(m_tilesMode);
+ }
}
void ColorBar::onGeneralUpdate(DocEvent& ev)
@@ -441,6 +556,13 @@ void ColorBar::onGeneralUpdate(DocEvent& ev)
invalidate();
}
+void ColorBar::onTilesetChanged(DocEvent& ev)
+{
+ m_tilesButtons.deselectItems();
+ if (m_scrollableTilesView.isVisible())
+ m_scrollableTilesView.updateView();
+}
+
void ColorBar::onAppPaletteChange()
{
COLOR_BAR_TRACE("ColorBar::onAppPaletteChange()\n");
@@ -508,6 +630,24 @@ void ColorBar::onPaletteButtonClick()
}
}
+void ColorBar::onTilesButtonClick()
+{
+ int item = m_tilesButtons.selectedItem();
+ m_tilesButtons.deselectItems();
+
+ switch (static_cast(item)) {
+
+ case TilesButton::MODE:
+ setTilesMode(!inTilesMode());
+ break;
+
+ case TilesButton::AUTO:
+ setAutoTilesMode(!inAutoTilesMode());
+ break;
+
+ }
+}
+
void ColorBar::onRemapButtonClick()
{
ASSERT(m_oldPalette);
@@ -589,6 +729,10 @@ void ColorBar::onPaletteViewIndexChange(int index, ui::MouseButtons buttons)
{
COLOR_BAR_TRACE("ColorBar::onPaletteViewIndexChange(%d)\n", index);
+ // TODO select tiles to stamp
+ if (inTilesMode())
+ return;
+
base::ScopedValue lock(m_fromPalView, true, m_fromPalView);
app::Color color = app::Color::fromIndex(index);
@@ -659,7 +803,10 @@ void ColorBar::setTransparentIndex(int index)
void ColorBar::onPaletteViewChangeSize(int boxsize)
{
- Preferences::instance().colorBar.boxSize(boxsize);
+ if (inTilesMode())
+ Preferences::instance().colorBar.tilesBoxSize(boxsize);
+ else
+ Preferences::instance().colorBar.boxSize(boxsize);
}
void ColorBar::onPaletteViewPasteColors(
@@ -714,6 +861,26 @@ app::Color ColorBar::onPaletteViewGetBackgroundIndex()
return getBgColor();
}
+void ColorBar::onPaletteViewClearTiles(const doc::PalettePicks& picks)
+{
+ try {
+ ContextWriter writer(UIContext::instance(), 500);
+ Sprite* sprite = writer.sprite();
+ ASSERT(writer.layer()->isTilemap());
+ if (sprite) {
+ auto tileset = m_tilesView.tileset();
+
+ Tx tx(writer.context(), "Clear Tiles", ModifyDocument);
+ for (auto ti : picks)
+ tx(new cmd::RemoveTile(tileset, ti));
+ tx.commit();
+ }
+ }
+ catch (base::Exception& e) {
+ Console::showException(e);
+ }
+}
+
void ColorBar::onFgColorChangeFromPreferences()
{
COLOR_BAR_TRACE("ColorBar::onFgColorChangeFromPreferences() -> %s\n",
@@ -998,30 +1165,44 @@ bool ColorBar::onCanClear(Context* ctx)
bool ColorBar::onCut(Context* ctx)
{
- m_paletteView.cutToClipboard();
+ if (m_tilesMode)
+ m_tilesView.cutToClipboard();
+ else
+ m_paletteView.cutToClipboard();
return true;
}
bool ColorBar::onCopy(Context* ctx)
{
- m_paletteView.copyToClipboard();
+ if (m_tilesMode)
+ m_tilesView.copyToClipboard();
+ else
+ m_paletteView.copyToClipboard();
return true;
}
bool ColorBar::onPaste(Context* ctx)
{
- m_paletteView.pasteFromClipboard();
+ if (m_tilesMode)
+ m_tilesView.pasteFromClipboard();
+ else
+ m_paletteView.pasteFromClipboard();
return true;
}
bool ColorBar::onClear(Context* ctx)
{
- m_paletteView.clearSelection();
+ if (m_tilesMode)
+ m_tilesView.clearSelection();
+ else
+ m_paletteView.clearSelection();
return true;
}
void ColorBar::onCancel(Context* ctx)
{
+ m_tilesView.deselect();
+ m_tilesView.discardClipboardSelection();
m_paletteView.deselect();
m_paletteView.discardClipboardSelection();
invalidate();
@@ -1207,6 +1388,11 @@ void ColorBar::setupTooltips(TooltipManager* tooltipManager)
tooltipManager->addTooltipFor(m_buttons.getItem((int)PalButton::PRESETS), "Presets", BOTTOM);
tooltipManager->addTooltipFor(m_buttons.getItem((int)PalButton::OPTIONS), "Options", BOTTOM);
tooltipManager->addTooltipFor(&m_remapButton, "Matches old indexes with new indexes", BOTTOM);
+
+ tooltipManager->addTooltipFor(
+ m_tilesButtons.getItem((int)TilesButton::MODE), "Show Tileset", BOTTOM);
+ tooltipManager->addTooltipFor(
+ m_tilesButtons.getItem((int)TilesButton::AUTO), "Generate new tiles automatically", BOTTOM);
}
// static
diff --git a/src/app/ui/color_bar.h b/src/app/ui/color_bar.h
index 62590a60e..5b0f71dc9 100644
--- a/src/app/ui/color_bar.h
+++ b/src/app/ui/color_bar.h
@@ -13,10 +13,12 @@
#include "app/context_observer.h"
#include "app/doc_observer.h"
#include "app/docs_observer.h"
+#include "app/tileset_mode.h"
#include "app/ui/button_set.h"
#include "app/ui/color_button.h"
#include "app/ui/input_chain_element.h"
#include "app/ui/palette_view.h"
+#include "doc/object_id.h"
#include "doc/pixel_format.h"
#include "doc/sort_palette.h"
#include "obs/connection.h"
@@ -78,6 +80,14 @@ namespace app {
bool inEditMode() const;
void setEditMode(bool state);
+ bool inTilesMode() const;
+ void setTilesMode(bool state);
+
+ bool inAutoTilesMode() const;
+ void setAutoTilesMode(bool state);
+
+ TilesetMode tilesetMode() const;
+
ColorButton* fgColorButton() { return &m_fgColor; }
ColorButton* bgColorButton() { return &m_bgColor; }
@@ -86,6 +96,7 @@ namespace app {
// DocObserver impl
void onGeneralUpdate(DocEvent& ev) override;
+ void onTilesetChanged(DocEvent& ev) override;
// InputChainElement impl
void onNewInputPriority(InputChainElement* element,
@@ -108,6 +119,7 @@ namespace app {
void onBeforeExecuteCommand(CommandExecutionEvent& ev);
void onAfterExecuteCommand(CommandExecutionEvent& ev);
void onPaletteButtonClick();
+ void onTilesButtonClick();
void onRemapButtonClick();
void onPaletteIndexChange(PaletteIndexChangeEvent& ev);
void onFgColorChangeFromPreferences();
@@ -131,6 +143,7 @@ namespace app {
void onPaletteViewPasteColors(const Palette* fromPal, const doc::PalettePicks& from, const doc::PalettePicks& to) override;
app::Color onPaletteViewGetForegroundIndex() override;
app::Color onPaletteViewGetBackgroundIndex() override;
+ void onPaletteViewClearTiles(const doc::PalettePicks& picks) override;
private:
void showRemap();
@@ -157,12 +170,15 @@ namespace app {
class WarningIcon;
ButtonSet m_buttons;
+ ButtonSet m_tilesButtons;
std::unique_ptr m_palettePopup;
ui::Splitter m_splitter;
ui::VBox m_palettePlaceholder;
ui::VBox m_selectorPlaceholder;
- ScrollableView m_scrollableView;
+ ScrollableView m_scrollablePalView;
+ ScrollableView m_scrollableTilesView;
PaletteView m_paletteView;
+ PaletteView m_tilesView;
ui::Button m_remapButton;
ColorSelector m_selector;
ColorTintShadeTone* m_tintShadeTone;
@@ -186,6 +202,7 @@ namespace app {
std::unique_ptr m_oldPalette;
Doc* m_lastDocument;
+ doc::ObjectId m_lastTilesetId;
bool m_ascending;
obs::scoped_connection m_beforeCmdConn;
obs::scoped_connection m_afterCmdConn;
@@ -195,9 +212,13 @@ namespace app {
obs::scoped_connection m_appPalChangeConn;
ui::MouseButtons m_lastButtons;
- // True if we the editing mode is on.
+ // True if the editing mode is on.
bool m_editMode;
+ // True if we should be putting/setting tiles.
+ bool m_tilesMode;
+ bool m_autoTilesMode;
+
// Timer to redraw editors after a palette change.
ui::Timer m_redrawTimer;
bool m_redrawAll;
diff --git a/src/app/ui/doc_view.cpp b/src/app/ui/doc_view.cpp
index da7d201cc..b42186eb3 100644
--- a/src/app/ui/doc_view.cpp
+++ b/src/app/ui/doc_view.cpp
@@ -468,6 +468,11 @@ void DocView::onLayerRestacked(DocEvent& ev)
m_editor->invalidate();
}
+void DocView::onTilesetChanged(DocEvent& ev)
+{
+ m_editor->invalidate();
+}
+
void DocView::onNewInputPriority(InputChainElement* element,
const ui::Message* msg)
{
@@ -587,6 +592,7 @@ bool DocView::onClear(Context* ctx)
if (cels.empty()) // No cels to modify
return false;
+ // TODO This code is similar to clipboard::cut()
{
Tx tx(writer.context(), "Clear");
const bool deselectMask =
diff --git a/src/app/ui/doc_view.h b/src/app/ui/doc_view.h
index 8523ed407..1276addff 100644
--- a/src/app/ui/doc_view.h
+++ b/src/app/ui/doc_view.h
@@ -1,4 +1,5 @@
// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@@ -79,6 +80,7 @@ namespace app {
void onRemoveCel(DocEvent& ev) override;
void onTotalFramesChanged(DocEvent& ev) override;
void onLayerRestacked(DocEvent& ev) override;
+ void onTilesetChanged(DocEvent& ev) override;
// InputChainElement impl
void onNewInputPriority(InputChainElement* element,
diff --git a/src/app/ui/editor/drawing_state.cpp b/src/app/ui/editor/drawing_state.cpp
index bfbc410ec..590fd82ab 100644
--- a/src/app/ui/editor/drawing_state.cpp
+++ b/src/app/ui/editor/drawing_state.cpp
@@ -287,6 +287,16 @@ void DrawingState::onExposeSpritePixels(const gfx::Region& rgn)
m_toolLoop->validateDstImage(rgn);
}
+bool DrawingState::getGridBounds(Editor* editor, gfx::Rect& gridBounds)
+{
+ if (m_toolLoop) {
+ gridBounds = m_toolLoop->getGridBounds();
+ return true;
+ }
+ else
+ return false;
+}
+
void DrawingState::handleMouseMovement(const tools::Pointer& pointer)
{
m_mouseMoveReceived = true;
diff --git a/src/app/ui/editor/drawing_state.h b/src/app/ui/editor/drawing_state.h
index 55a46bac6..5a4702d08 100644
--- a/src/app/ui/editor/drawing_state.h
+++ b/src/app/ui/editor/drawing_state.h
@@ -42,6 +42,8 @@ namespace app {
// already drawing (viewing the real trace).
virtual bool requireBrushPreview() override { return false; }
+ virtual bool getGridBounds(Editor* editor, gfx::Rect& gridBounds) override;
+
void initToolLoop(Editor* editor,
const tools::Pointer& pointer);
diff --git a/src/app/ui/editor/editor.cpp b/src/app/ui/editor/editor.cpp
index f616b06a2..a4a94dcc3 100644
--- a/src/app/ui/editor/editor.cpp
+++ b/src/app/ui/editor/editor.cpp
@@ -382,6 +382,7 @@ void Editor::getSite(Site* site) const
site->sprite(m_sprite);
site->layer(m_layer);
site->frame(m_frame);
+
if (!m_selectedSlices.empty() &&
getCurrentEditorInk()->isSlice()) {
site->selectedSlices(m_selectedSlices);
@@ -393,6 +394,15 @@ void Editor::getSite(Site* site) const
timeline->range().enabled()) {
site->range(timeline->range());
}
+
+ if (m_layer && m_layer->isTilemap()) {
+ TilesetMode mode = site->tilesetMode();
+ const ColorBar* colorbar = ColorBar::instance();
+ ASSERT(colorbar);
+ if (colorbar)
+ mode = colorbar->tilesetMode();
+ site->tilesetMode(mode);
+ }
}
Site Editor::getSite() const
@@ -737,7 +747,10 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& sprite
// Draw the grid
if (m_docPref.show.grid()) {
- gfx::Rect gridrc = m_sprite->gridBounds();
+ gfx::Rect gridrc;
+ if (!m_state->getGridBounds(this, gridrc))
+ gridrc = getSite().gridBounds();
+
if (m_proj.applyX(gridrc.w) > 2 &&
m_proj.applyY(gridrc.h) > 2) {
int alpha = m_docPref.grid.opacity();
diff --git a/src/app/ui/editor/editor_state.h b/src/app/ui/editor/editor_state.h
index 440de8d16..47c3f2579 100644
--- a/src/app/ui/editor/editor_state.h
+++ b/src/app/ui/editor/editor_state.h
@@ -10,6 +10,8 @@
#pragma once
#include "base/disable_copying.h"
+#include "base/shared_ptr.h"
+#include "gfx/fwd.h"
#include "gfx/point.h"
#include
@@ -132,6 +134,10 @@ namespace app {
// Called when a tag is deleted.
virtual void onRemoveTag(Editor* editor, doc::Tag* tag) { }
+ // Used to adjust the grid origin point for temporal cels created
+ // by states like DrawingState + ExpandCelCanvas.
+ virtual bool getGridBounds(Editor* editor, gfx::Rect& gridBounds) { return false; }
+
private:
DISABLE_COPYING(EditorState);
};
diff --git a/src/app/ui/editor/pixels_movement.cpp b/src/app/ui/editor/pixels_movement.cpp
index 76e4545b1..989e50cd5 100644
--- a/src/app/ui/editor/pixels_movement.cpp
+++ b/src/app/ui/editor/pixels_movement.cpp
@@ -26,6 +26,7 @@
#include "app/ui/editor/pivot_helpers.h"
#include "app/ui/status_bar.h"
#include "app/ui_context.h"
+#include "app/util/cel_ops.h"
#include "app/util/expand_cel_canvas.h"
#include "app/util/new_image_from_mask.h"
#include "app/util/range_utils.h"
@@ -278,7 +279,9 @@ void PixelsMovement::cutMask()
{
ContextWriter writer(m_reader, 1000);
if (writer.cel()) {
- m_tx(new cmd::ClearMask(writer.cel()));
+ clear_mask_from_cel(m_tx,
+ writer.cel(),
+ m_site.tilesetMode());
// Do not trim here so we don't lost the information about all
// linked cels related to "writer.cel()"
@@ -1080,7 +1083,9 @@ void PixelsMovement::reproduceAllTransformationsWithInnerCmds()
for (const InnerCmd& c : m_innerCmds) {
switch (c.type) {
case InnerCmd::Clear:
- m_tx(new cmd::ClearMask(m_site.cel()));
+ clear_mask_from_cel(m_tx,
+ m_site.cel(),
+ m_site.tilesetMode());
break;
case InnerCmd::Flip:
flipOriginalImage(c.data.flip.type);
diff --git a/src/app/ui/palette_view.cpp b/src/app/ui/palette_view.cpp
index 26f4faf2a..e0e53824b 100644
--- a/src/app/ui/palette_view.cpp
+++ b/src/app/ui/palette_view.cpp
@@ -17,21 +17,28 @@
#include "app/modules/gfx.h"
#include "app/modules/gui.h"
#include "app/modules/palettes.h"
+#include "app/site.h"
#include "app/ui/editor/editor.h"
#include "app/ui/palette_view.h"
#include "app/ui/skin/skin_theme.h"
#include "app/ui/status_bar.h"
+#include "app/ui_context.h"
#include "app/util/clipboard.h"
#include "app/util/pal_ops.h"
#include "base/bind.h"
#include "base/convert_to.h"
+#include "doc/conversion_to_surface.h"
#include "doc/image.h"
+#include "doc/layer_tilemap.h"
#include "doc/palette.h"
#include "doc/remap.h"
+#include "doc/tileset.h"
#include "gfx/color.h"
#include "gfx/point.h"
#include "os/font.h"
#include "os/surface.h"
+#include "os/surface.h"
+#include "os/system.h"
#include "ui/graphics.h"
#include "ui/manager.h"
#include "ui/message.h"
@@ -51,12 +58,186 @@ namespace app {
using namespace ui;
using namespace app::skin;
+// Interface used to adapt the PaletteView widget to see tilesets too.
+class AbstractPaletteViewAdapter {
+public:
+ virtual ~AbstractPaletteViewAdapter() { }
+ virtual int size() const = 0;
+ virtual void paletteChange(doc::PalettePicks& picks) = 0;
+ virtual void activeSiteChange(const Site& site, doc::PalettePicks& picks) = 0;
+ virtual void clearSelection(PaletteView* paletteView,
+ doc::PalettePicks& picks) = 0;
+ virtual void showEntryInStatusBar(StatusBar* statusBar, int index) = 0;
+ virtual void showDragInfoInStatusBar(StatusBar* statusBar, bool copy, int destIndex, int newSize) = 0;
+ virtual void showResizeInfoInStatusBar(StatusBar* statusBar, int newSize) = 0;
+ virtual void drawEntry(ui::Graphics* g,
+ SkinTheme* theme,
+ const int palIdx,
+ const int offIdx,
+ const int childSpacing,
+ gfx::Rect& box,
+ gfx::Color& negColor) = 0;
+ virtual doc::Tileset* tileset() const { return nullptr; }
+};
+
+// This default adapter uses the default behavior to use the
+// PaletteView as just a doc::Palette view.
+class PaletteViewAdapter : public AbstractPaletteViewAdapter {
+public:
+ int size() const override { return palette()->size(); }
+ void paletteChange(doc::PalettePicks& picks) override {
+ picks.resize(palette()->size());
+ }
+ void activeSiteChange(const Site& site, doc::PalettePicks& picks) override {
+ // Do nothing
+ }
+ void clearSelection(PaletteView* paletteView,
+ doc::PalettePicks& picks) override {
+ Palette palette(*this->palette());
+ Palette newPalette(palette);
+ newPalette.resize(MAX(1, newPalette.size() - picks.picks()));
+
+ Remap remap = create_remap_to_move_picks(picks, palette.size());
+ for (int i=0; isetNewPalette(&palette, &newPalette, PaletteViewModification::CLEAR);
+ }
+ void showEntryInStatusBar(StatusBar* statusBar, int index) override {
+ statusBar->showColor(
+ 0, "", app::Color::fromIndex(index));
+ }
+ void showDragInfoInStatusBar(StatusBar* statusBar, bool copy, int destIndex, int newSize) override {
+ statusBar->setStatusText(
+ 0, "%s to %d - New Palette Size %d",
+ (copy ? "Copy": "Move"),
+ destIndex, newSize);
+ }
+ void showResizeInfoInStatusBar(StatusBar* statusBar, int newSize) override {
+ statusBar->setStatusText(
+ 0, "New Palette Size %d", newSize);
+ }
+ void drawEntry(ui::Graphics* g,
+ SkinTheme* theme,
+ const int palIdx,
+ const int offIdx,
+ const int childSpacing,
+ gfx::Rect& box,
+ gfx::Color& negColor) override {
+ doc::color_t palColor =
+ (palIdx < palette()->size() ? palette()->getEntry(palIdx):
+ rgba(0, 0, 0, 255));
+ app::Color appColor = app::Color::fromRgb(
+ rgba_getr(palColor),
+ rgba_getg(palColor),
+ rgba_getb(palColor),
+ rgba_geta(palColor));
+
+ if (childSpacing > 0) {
+ gfx::Color color = theme->colors.paletteEntriesSeparator();
+ g->fillRect(color, gfx::Rect(box).enlarge(childSpacing));
+ }
+ draw_color(g, box, appColor, doc::ColorMode::RGB);
+
+ const gfx::Color gfxColor = gfx::rgba(
+ rgba_getr(palColor),
+ rgba_getg(palColor),
+ rgba_getb(palColor),
+ rgba_geta(palColor));
+ negColor = color_utils::blackandwhite_neg(gfxColor);
+ }
+private:
+ doc::Palette* palette() const {
+ return get_current_palette();
+ }
+};
+
+// This adapter makes it possible to use a PaletteView to edit a
+// doc::Tileset.
+class TilesetViewAdapter : public AbstractPaletteViewAdapter {
+public:
+ int size() const override {
+ if (auto t = tileset())
+ return t->size();
+ else
+ return 0;
+ }
+ void paletteChange(doc::PalettePicks& picks) override {
+ // Do nothing
+ }
+ void activeSiteChange(const Site& site, doc::PalettePicks& picks) override {
+ if (auto tileset = this->tileset())
+ picks.resize(tileset->size());
+ else
+ picks.clear();
+ }
+ void clearSelection(PaletteView* paletteView,
+ doc::PalettePicks& picks) override {
+ paletteView->delegate()->onPaletteViewClearTiles(picks);
+ }
+ void showEntryInStatusBar(StatusBar* statusBar, int index) override {
+ statusBar->setStatusText(
+ 0, "Tile %d", index);
+ }
+ void showDragInfoInStatusBar(StatusBar* statusBar, bool copy, int destIndex, int newSize) override {
+ statusBar->setStatusText(
+ 0, "%s to %d - New Tileset Size %d",
+ (copy ? "Copy": "Move"),
+ destIndex, newSize);
+ }
+ void showResizeInfoInStatusBar(StatusBar* statusBar, int newSize) override {
+ statusBar->setStatusText(
+ 0, "New Tileset Size %d", newSize);
+ }
+ void drawEntry(ui::Graphics* g,
+ SkinTheme* theme,
+ const int palIdx,
+ const int offIdx,
+ const int childSpacing,
+ gfx::Rect& box,
+ gfx::Color& negColor) override {
+ if (childSpacing > 0) {
+ gfx::Color color = theme->colors.paletteEntriesSeparator();
+ g->fillRect(color, gfx::Rect(box).enlarge(childSpacing));
+ }
+ draw_color(g, box, app::Color::fromMask(), doc::ColorMode::RGB);
+
+ doc::ImageRef tileImage;
+ if (auto t = this->tileset())
+ tileImage = t->get(palIdx);
+ if (tileImage) {
+ int w = tileImage->width();
+ int h = tileImage->height();
+ os::Surface* surface = os::instance()->createRgbaSurface(w, h);
+ doc::convert_image_to_surface(tileImage.get(), get_current_palette(),
+ surface, 0, 0, 0, 0, w, h);
+ g->drawRgbaSurface(surface, gfx::Rect(0, 0, w, h), box);
+ surface->dispose();
+ }
+ negColor = gfx::rgba(255, 255, 255);
+ }
+ doc::Tileset* tileset() const override {
+ Site site = App::instance()->context()->activeSite();
+ if (site.layer() &&
+ site.layer()->isTilemap()) {
+ return static_cast(site.layer())->tileset();
+ }
+ else
+ return nullptr;
+ }
+private:
+};
+
PaletteView::PaletteView(bool editable, PaletteViewStyle style, PaletteViewDelegate* delegate, int boxsize)
: Widget(kGenericWidget)
, m_state(State::WAITING)
, m_editable(editable)
, m_style(style)
, m_delegate(delegate)
+ , m_adapter(isTiles() ? (AbstractPaletteViewAdapter*)new TilesetViewAdapter:
+ (AbstractPaletteViewAdapter*)new PaletteViewAdapter)
, m_columns(16)
, m_boxsize(boxsize)
, m_currentEntry(-1)
@@ -82,6 +263,9 @@ PaletteView::PaletteView(bool editable, PaletteViewStyle style, PaletteViewDeleg
});
}
+ if (isTiles())
+ UIContext::instance()->add_observer(this);
+
InitTheme.connect(
[this]{
updateBorderAndChildSpacing();
@@ -89,6 +273,12 @@ PaletteView::PaletteView(bool editable, PaletteViewStyle style, PaletteViewDeleg
initTheme();
}
+PaletteView::~PaletteView()
+{
+ if (isTiles())
+ UIContext::instance()->remove_observer(this);
+}
+
void PaletteView::setColumns(int columns)
{
int old_columns = m_columns;
@@ -107,7 +297,7 @@ void PaletteView::deselect()
{
bool invalidate = (m_selectedEntries.picks() > 0);
- m_selectedEntries.resize(currentPalette()->size());
+ m_selectedEntries.resize(m_adapter->size());
m_selectedEntries.clear();
if (invalidate)
@@ -116,7 +306,7 @@ void PaletteView::deselect()
void PaletteView::selectColor(int index)
{
- if (index < 0 || index >= currentPalette()->size())
+ if (index < 0 || index >= m_adapter->size())
return;
if (m_currentEntry != index || !m_selectedEntries[index]) {
@@ -202,17 +392,27 @@ void PaletteView::setSelectedEntries(const doc::PalettePicks& entries)
invalidate();
}
+doc::Tileset* PaletteView::tileset() const
+{
+ return m_adapter->tileset();
+}
+
app::Color PaletteView::getColorByPosition(const gfx::Point& pos)
{
gfx::Point relPos = pos - bounds().origin();
- Palette* palette = currentPalette();
- for (int i=0; isize(); ++i) {
+ for (int i=0; isize(); ++i) {
if (getPaletteEntryBounds(i).contains(relPos))
return app::Color::fromIndex(i);
}
return app::Color::fromMask();
}
+void PaletteView::onActiveSiteChange(const Site& site)
+{
+ ASSERT(isTiles());
+ m_adapter->activeSiteChange(site, m_selectedEntries);
+}
+
int PaletteView::getBoxSize() const
{
return int(m_boxsize);
@@ -220,7 +420,10 @@ int PaletteView::getBoxSize() const
void PaletteView::setBoxSize(double boxsize)
{
- m_boxsize = MID(4.0, boxsize, 32.0);
+ if (isTiles())
+ m_boxsize = MID(4.0, boxsize, 64.0);
+ else
+ m_boxsize = MID(4.0, boxsize, 32.0);
if (m_delegate)
m_delegate->onPaletteViewChangeSize(int(m_boxsize));
@@ -235,21 +438,11 @@ void PaletteView::clearSelection()
if (!m_selectedEntries.picks())
return;
- Palette palette(*currentPalette());
- Palette newPalette(palette);
- newPalette.resize(MAX(1, newPalette.size() - m_selectedEntries.picks()));
-
- Remap remap = create_remap_to_move_picks(m_selectedEntries, palette.size());
- for (int i=0; iclearSelection(this, m_selectedEntries);
m_currentEntry = m_selectedEntries.firstPick();
m_selectedEntries.clear();
stopMarchingAnts();
-
- setNewPalette(&palette, &newPalette, PaletteViewModification::CLEAR);
}
void PaletteView::cutToClipboard()
@@ -342,7 +535,7 @@ bool PaletteView::onProcessMessage(Message* msg)
if (m_state == State::SELECTING_COLOR &&
m_hot.part == Hit::COLOR) {
int idx = m_hot.color;
- idx = MID(0, idx, currentPalette()->size()-1);
+ idx = MID(0, idx, m_adapter->size()-1);
MouseButtons buttons = mouseMsg->buttons();
@@ -497,11 +690,11 @@ void PaletteView::onPaint(ui::PaintEvent& ev)
g->fillRect(theme->colors.editorFace(), bounds);
- // Draw palette entries
+ // Draw palette/tileset entries
int picksCount = m_selectedEntries.picks();
int idxOffset = 0;
int boxOffset = 0;
- int palSize = palette->size();
+ int palSize = m_adapter->size();
if (dragging && !m_copy) palSize -= picksCount;
if (resizing) palSize = m_hot.color;
@@ -519,7 +712,8 @@ void PaletteView::onPaint(ui::PaintEvent& ev)
gfx::Rect box = getPaletteEntryBounds(i + boxOffset);
gfx::Color negColor;
- drawEntry(g, i + idxOffset, i + boxOffset, box, negColor);
+ m_adapter->drawEntry(g, theme, i + idxOffset, i + boxOffset,
+ childSpacing(), box, negColor);
const int boxsize = boxSizePx();
switch (m_style) {
@@ -570,7 +764,8 @@ void PaletteView::onPaint(ui::PaintEvent& ev)
}
PalettePicks& picks = (dragging ? dragPicks: m_selectedEntries);
- for (int i=0; isize(); ++i) {
+ const int size = m_adapter->size();
+ for (int i=0; idrawEntry(g, theme, i, k, childSpacing(),
+ box2, negColor);
- gfx::Color neg = color_utils::blackandwhite_neg(negColor);
os::Font* minifont = theme->getMiniFont();
const std::string text = base::convert_to(k);
g->setFont(minifont);
- g->drawText(text, neg, gfx::ColorNone,
+ g->drawText(text, negColor, gfx::ColorNone,
gfx::Point(box2.x + box2.w/2 - minifont->textLength(text)/2,
box2.y + box2.h/2 - minifont->height()/2));
}
@@ -654,7 +849,7 @@ void PaletteView::onResize(ui::ResizeEvent& ev)
void PaletteView::onSizeHint(ui::SizeHintEvent& ev)
{
- div_t d = div(currentPalette()->size(), m_columns);
+ div_t d = div(m_adapter->size(), m_columns);
int cols = m_columns;
int rows = d.quot + ((d.rem)? 1: 0);
@@ -688,7 +883,7 @@ void PaletteView::update_scroll(int color)
scroll = view->viewScroll();
- d = div(currentPalette()->size(), m_columns);
+ d = div(m_adapter->size(), m_columns);
cols = m_columns;
y = (boxsize+childSpacing()) * (color / cols);
@@ -709,7 +904,7 @@ void PaletteView::update_scroll(int color)
void PaletteView::onAppPaletteChange()
{
- m_selectedEntries.resize(currentPalette()->size());
+ m_adapter->paletteChange(m_selectedEntries);
View* view = View::getView(this);
if (view)
@@ -734,19 +929,18 @@ PaletteView::Hit PaletteView::hitTest(const gfx::Point& pos)
{
auto theme = static_cast(this->theme());
const int outlineWidth = theme->dimensions.paletteOutlineWidth();
- Palette* palette = currentPalette();
+ const int size = m_adapter->size();
if (m_state == State::WAITING && m_editable) {
// First check if the mouse is inside the selection outline.
- for (int i=0; isize(); ++i) {
+ for (int i=0; isize();
- bool top = (i >= m_columns && i-m_columns >= 0 ? m_selectedEntries[i-m_columns]: false);
- bool bottom = (i < max-m_columns && i+m_columns < max ? m_selectedEntries[i+m_columns]: false);
- bool left = ((i%m_columns)>0 && i-1 >= 0 ? m_selectedEntries[i-1]: false);
- bool right = ((i%m_columns)= m_columns && i-m_columns >= 0 ? m_selectedEntries[i-m_columns]: false);
+ bool bottom = (i < size-m_columns && i+m_columns < size ? m_selectedEntries[i+m_columns]: false);
+ bool left = ((i%m_columns)>0 && i-1 >= 0 ? m_selectedEntries[i-1]: false);
+ bool right = ((i%m_columns)size()).contains(pos)) {
- return Hit(Hit::RESIZE_HANDLE, palette->size());
+ if (getPaletteEntryBounds(size).contains(pos)) {
+ return Hit(Hit::RESIZE_HANDLE, size);
}
}
// Check if we are inside a color.
View* view = View::getView(this);
+ ASSERT(view);
gfx::Rect vp = view->viewportBounds();
for (int i=0; ; ++i) {
gfx::Rect box = getPaletteEntryBounds(i);
- if (i >= palette->size() && box.y2() > vp.h)
+ if (i >= size && box.y2() > vp.h)
break;
box.w += childSpacing();
@@ -912,11 +1107,10 @@ void PaletteView::setStatusBar()
if ((m_hot.part == Hit::COLOR ||
m_hot.part == Hit::OUTLINE ||
m_hot.part == Hit::POSSIBLE_COLOR) &&
- (m_hot.color < currentPalette()->size())) {
- int i = MAX(0, m_hot.color);
+ (m_hot.color < m_adapter->size())) {
+ const int index = MAX(0, m_hot.color);
- statusBar->showColor(
- 0, "", app::Color::fromIndex(i));
+ m_adapter->showEntryInStatusBar(statusBar, index);
}
else {
statusBar->showDefaultText();
@@ -927,15 +1121,13 @@ void PaletteView::setStatusBar()
if (m_hot.part == Hit::COLOR) {
const int picks = m_selectedEntries.picks();
const int destIndex = MAX(0, m_hot.color);
- const int palSize = currentPalette()->size();
+ const int palSize = m_adapter->size();
const int newPalSize =
(m_copy ? MAX(palSize + picks, destIndex + picks):
MAX(palSize, destIndex + picks));
- statusBar->setStatusText(
- 0, "%s to %d - New Palette Size %d",
- (m_copy ? "Copy": "Move"),
- destIndex, newPalSize);
+ m_adapter->showDragInfoInStatusBar(
+ statusBar, m_copy, destIndex, newPalSize);
}
else {
statusBar->showDefaultText();
@@ -946,10 +1138,9 @@ void PaletteView::setStatusBar()
if (m_hot.part == Hit::COLOR ||
m_hot.part == Hit::POSSIBLE_COLOR ||
m_hot.part == Hit::RESIZE_HANDLE) {
- int newPalSize = MAX(1, m_hot.color);
- statusBar->setStatusText(
- 0, "New Palette Size %d",
- newPalSize);
+ const int newSize = MAX(1, m_hot.color);
+
+ m_adapter->showResizeInfoInStatusBar(statusBar, newSize);
}
else {
statusBar->showDefaultText();
@@ -1003,36 +1194,6 @@ void PaletteView::setNewPalette(doc::Palette* oldPalette,
manager()->invalidate();
}
-void PaletteView::drawEntry(ui::Graphics* g,
- const int palIdx,
- const int offIdx,
- gfx::Rect& box,
- gfx::Color& negColor)
-{
- doc::color_t palColor =
- (palIdx < currentPalette()->size() ? currentPalette()->getEntry(palIdx):
- rgba(0, 0, 0, 255));
- app::Color appColor = app::Color::fromRgb(
- rgba_getr(palColor),
- rgba_getg(palColor),
- rgba_getb(palColor),
- rgba_geta(palColor));
-
- if (m_withSeparator) {
- auto theme = static_cast(this->theme());
- gfx::Color color = theme->colors.paletteEntriesSeparator();
- g->fillRect(color, gfx::Rect(box).enlarge(childSpacing()));
- }
- draw_color(g, box, appColor, doc::ColorMode::RGB);
-
- const gfx::Color gfxColor = gfx::rgba(
- rgba_getr(palColor),
- rgba_getg(palColor),
- rgba_getb(palColor),
- rgba_geta(palColor));
- negColor = color_utils::blackandwhite_neg(gfxColor);
-}
-
int PaletteView::boxSizePx() const
{
return m_boxsize*guiscale()
diff --git a/src/app/ui/palette_view.h b/src/app/ui/palette_view.h
index c20d2ae92..d64f2b47a 100644
--- a/src/app/ui/palette_view.h
+++ b/src/app/ui/palette_view.h
@@ -10,19 +10,23 @@
#pragma once
#include "app/color.h"
+#include "app/context_observer.h"
#include "app/ui/color_source.h"
#include "app/ui/marching_ants.h"
#include "doc/palette_picks.h"
+#include "doc/tile.h"
#include "obs/connection.h"
#include "obs/signal.h"
#include "ui/event.h"
#include "ui/mouse_buttons.h"
#include "ui/widget.h"
+#include
#include
namespace doc {
class Palette;
+ class Tileset;
}
namespace app {
@@ -43,20 +47,32 @@ namespace app {
const doc::Palette* fromPal, const doc::PalettePicks& from, const doc::PalettePicks& to) { }
virtual app::Color onPaletteViewGetForegroundIndex() { return app::Color::fromMask(); }
virtual app::Color onPaletteViewGetBackgroundIndex() { return app::Color::fromMask(); }
+ virtual void onPaletteViewClearTiles(const doc::PalettePicks& tiles) { }
};
+ class AbstractPaletteViewAdapter;
+ class PaletteViewAdapter;
+
class PaletteView : public ui::Widget
, public MarchingAnts
- , public IColorSource {
+ , public IColorSource
+ , public ContextObserver {
+ friend class PaletteViewAdapter;
public:
enum PaletteViewStyle {
SelectOneColor,
- FgBgColors
+ FgBgColors,
+ FgBgTiles,
};
PaletteView(bool editable, PaletteViewStyle style, PaletteViewDelegate* delegate, int boxsize);
+ ~PaletteView();
+
+ PaletteViewDelegate* delegate() { return m_delegate; }
bool isEditable() const { return m_editable; }
+ bool isPalette() const { return m_style != FgBgTiles; }
+ bool isTiles() const { return m_style == FgBgTiles; }
int getColumns() const { return m_columns; }
void setColumns(int columns);
@@ -72,9 +88,14 @@ namespace app {
int getSelectedEntriesCount() const;
void setSelectedEntries(const doc::PalettePicks& entries);
+ doc::Tileset* tileset() const;
+
// IColorSource
app::Color getColorByPosition(const gfx::Point& pos) override;
+ // ContextObserver impl
+ void onActiveSiteChange(const Site& site) override;
+
int getBoxSize() const;
void setBoxSize(double boxsize);
@@ -143,11 +164,6 @@ namespace app {
int findExactIndex(const app::Color& color) const;
void setNewPalette(doc::Palette* oldPalette, doc::Palette* newPalette,
PaletteViewModification mod);
- void drawEntry(ui::Graphics* g,
- const int palIdx,
- const int offIdx,
- gfx::Rect& box,
- gfx::Color& negColor);
int boxSizePx() const;
void updateBorderAndChildSpacing();
@@ -155,6 +171,7 @@ namespace app {
bool m_editable;
PaletteViewStyle m_style;
PaletteViewDelegate* m_delegate;
+ std::unique_ptr m_adapter;
int m_columns;
double m_boxsize;
int m_currentEntry;
diff --git a/src/app/util/autocrop.cpp b/src/app/util/autocrop.cpp
index 1d3d9f780..708a996f3 100644
--- a/src/app/util/autocrop.cpp
+++ b/src/app/util/autocrop.cpp
@@ -276,7 +276,7 @@ gfx::Rect get_trimmed_bounds(
gfx::Rect frameBounds;
doc::color_t refColor;
if (get_best_refcolor_for_trimming(image, refColor) &&
- doc::algorithm::shrink_bounds(image, frameBounds, refColor)) {
+ doc::algorithm::shrink_bounds(image, refColor, nullptr, frameBounds)) {
bounds = bounds.createUnion(frameBounds);
}
diff --git a/src/app/util/cel_ops.cpp b/src/app/util/cel_ops.cpp
new file mode 100644
index 000000000..f352d2dda
--- /dev/null
+++ b/src/app/util/cel_ops.cpp
@@ -0,0 +1,540 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+// Copyright (C) 2001-2018 David Capello
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/util/cel_ops.h"
+
+#include "app/cmd/add_tile.h"
+#include "app/cmd/clear_cel.h"
+#include "app/cmd/clear_mask.h"
+#include "app/cmd/copy_region.h"
+#include "app/cmd/replace_image.h"
+#include "app/cmd/set_cel_position.h"
+#include "app/cmd_sequence.h"
+#include "app/doc.h"
+#include "doc/algorithm/fill_selection.h"
+#include "doc/algorithm/resize_image.h"
+#include "doc/algorithm/shrink_bounds.h"
+#include "doc/cel.h"
+#include "doc/grid.h"
+#include "doc/image.h"
+#include "doc/layer.h"
+#include "doc/layer_tilemap.h"
+#include "doc/mask.h"
+#include "doc/palette.h"
+#include "doc/primitives.h"
+#include "doc/sprite.h"
+#include "doc/tileset.h"
+#include "doc/tileset_hash_table.h"
+#include "doc/tilesets.h"
+#include "gfx/region.h"
+#include "render/dithering.h"
+#include "render/ordered_dither.h"
+#include "render/quantization.h"
+#include "render/render.h"
+
+#include
+#include
+
+#define OPS_TRACE(...)
+
+namespace app {
+
+using namespace doc;
+
+template
+static void mask_image_templ(Image* image, const Image* bitmap)
+{
+ LockImageBits bits1(image);
+ const LockImageBits bits2(bitmap);
+ typename LockImageBits::iterator it1, end1;
+ LockImageBits::const_iterator it2, end2;
+ for (it1 = bits1.begin(), end1 = bits1.end(),
+ it2 = bits2.begin(), end2 = bits2.end();
+ it1 != end1 && it2 != end2; ++it1, ++it2) {
+ if (!*it2)
+ *it1 = image->maskColor();
+ }
+ ASSERT(it1 == end1);
+ ASSERT(it2 == end2);
+}
+
+static void mask_image(Image* image, Image* bitmap)
+{
+ ASSERT(image->bounds() == bitmap->bounds());
+ switch (image->pixelFormat()) {
+ case IMAGE_RGB: return mask_image_templ(image, bitmap);
+ case IMAGE_GRAYSCALE: return mask_image_templ(image, bitmap);
+ case IMAGE_INDEXED: return mask_image_templ(image, bitmap);
+ }
+}
+
+Cel* create_cel_copy(CmdSequence* cmds,
+ const Cel* srcCel,
+ const Sprite* dstSprite,
+ Layer* dstLayer,
+ const frame_t dstFrame)
+{
+ const Image* srcImage = srcCel->image();
+ doc::PixelFormat dstPixelFormat =
+ (dstLayer->isTilemap() ? IMAGE_TILEMAP:
+ dstSprite->pixelFormat());
+ gfx::Size dstSize(srcImage->width(),
+ srcImage->height());
+
+ // From Tilemap -> Image
+ if (srcCel->layer()->isTilemap() && !dstLayer->isTilemap()) {
+ auto layerTilemap = static_cast(srcCel->layer());
+ const auto& grid = layerTilemap->tileset()->grid();
+ dstSize = grid.tilemapSizeToCanvas(dstSize);
+ }
+ // From Image or Tilemap -> Tilemap
+ else if (dstLayer->isTilemap()) {
+ auto layerTilemap = static_cast(dstLayer);
+ auto grid = layerTilemap->tileset()->grid();
+ if (srcCel->layer()->isTilemap()) // TODO check if this is correct
+ grid.origin(grid.origin() + srcCel->position());
+ const gfx::Rect tilemapBounds = grid.canvasToTile(srcCel->bounds());
+ dstSize = tilemapBounds.size();
+ }
+
+ // New cel
+ std::unique_ptr dstCel(
+ new Cel(dstFrame, ImageRef(Image::create(dstPixelFormat, dstSize.w, dstSize.h))));
+
+ dstCel->setOpacity(srcCel->opacity());
+ dstCel->data()->setUserData(srcCel->data()->userData());
+
+ // Special case were we copy from a tilemap...
+ if (srcCel->layer()->isTilemap()) {
+ if (dstLayer->isTilemap()) {
+ // Tilemap -> Tilemap (with same tileset)
+ // Best case, copy a cel in the same layer (we have the same
+ // tileset available, so we just copy the tilemap as it is).
+ if (srcCel->layer() == dstLayer) {
+ dstCel->image()->copy(srcImage, gfx::Clip(0, 0, srcImage->bounds()));
+ }
+ // Tilemap -> Tilemap (with different tilesets)
+ else {
+ doc::ImageSpec spec = dstSprite->spec();
+ spec.setSize(srcCel->bounds().size());
+ doc::ImageRef tmpImage(doc::Image::create(spec));
+ render::Render().renderCel(
+ tmpImage.get(),
+ dstSprite,
+ srcImage,
+ srcCel->layer(),
+ dstSprite->palette(dstCel->frame()),
+ gfx::Rect(gfx::Point(0, 0), srcCel->bounds().size()),
+ gfx::Clip(0, 0, tmpImage->bounds()),
+ 255, BlendMode::NORMAL);
+
+ doc::ImageRef tilemap = dstCel->imageRef();
+
+ draw_image_into_new_tilemap_cel(
+ cmds, static_cast(dstLayer), dstCel.get(),
+ tmpImage.get(),
+ srcCel->bounds().origin(),
+ srcCel->bounds().origin(),
+ srcCel->bounds(),
+ tilemap);
+ }
+ dstCel->setPosition(srcCel->position());
+ }
+ // Tilemap -> Image (so we convert the tilemap to a regular image)
+ else {
+ render::Render().renderCel(
+ dstCel->image(),
+ dstSprite,
+ srcImage,
+ srcCel->layer(),
+ dstSprite->palette(dstCel->frame()),
+ gfx::Rect(gfx::Point(0, 0), srcCel->bounds().size()),
+ gfx::Clip(0, 0, dstCel->image()->bounds()),
+ 255, BlendMode::NORMAL);
+
+ // Shrink image
+ if (dstLayer->isTransparent()) {
+ auto bg = dstCel->image()->maskColor();
+ gfx::Rect bounds;
+ if (algorithm::shrink_bounds(dstCel->image(), bg, dstLayer, bounds)) {
+ ImageRef trimmed(doc::crop_image(dstCel->image(), bounds, bg));
+ dstCel->data()->setImage(trimmed, dstLayer);
+ dstCel->setPosition(srcCel->position() + bounds.origin());
+ return dstCel.release();
+ }
+ }
+ }
+ }
+ // Image -> Tilemap (we'll need to generate new tilesets)
+ else if (dstLayer->isTilemap()) {
+ doc::ImageRef tilemap = dstCel->imageRef();
+ draw_image_into_new_tilemap_cel(
+ cmds, static_cast(dstLayer), dstCel.get(),
+ srcImage,
+ gfx::Point(0, 0),
+ srcCel->bounds().origin(),
+ srcCel->bounds(),
+ tilemap);
+ }
+ else if ((dstSprite->pixelFormat() != srcImage->pixelFormat()) ||
+ // If both images are indexed but with different palette, we can
+ // convert the source cel to RGB first.
+ (dstSprite->pixelFormat() == IMAGE_INDEXED &&
+ srcImage->pixelFormat() == IMAGE_INDEXED &&
+ srcCel->sprite()->palette(srcCel->frame())->countDiff(
+ dstSprite->palette(dstFrame), nullptr, nullptr))) {
+ ImageRef tmpImage(Image::create(IMAGE_RGB, srcImage->width(), srcImage->height()));
+ tmpImage->clear(0);
+
+ render::convert_pixel_format(
+ srcImage,
+ tmpImage.get(),
+ IMAGE_RGB,
+ render::Dithering(),
+ srcCel->sprite()->rgbMap(srcCel->frame()),
+ srcCel->sprite()->palette(srcCel->frame()),
+ srcCel->layer()->isBackground(),
+ 0);
+
+ render::convert_pixel_format(
+ tmpImage.get(),
+ dstCel->image(),
+ IMAGE_INDEXED,
+ render::Dithering(),
+ dstSprite->rgbMap(dstFrame),
+ dstSprite->palette(dstFrame),
+ srcCel->layer()->isBackground(),
+ dstSprite->transparentColor());
+ }
+ // Simple case, where we copy both images
+ else {
+ render::composite_image(
+ dstCel->image(),
+ srcImage,
+ srcCel->sprite()->palette(srcCel->frame()),
+ 0, 0, 255, BlendMode::SRC);
+ }
+
+ // Resize a referece cel to a non-reference layer
+ if (srcCel->layer()->isReference() && !dstLayer->isReference()) {
+ gfx::RectF srcBounds = srcCel->boundsF();
+
+ std::unique_ptr dstCel2(
+ new Cel(dstFrame,
+ ImageRef(Image::create(dstSprite->pixelFormat(),
+ std::ceil(srcBounds.w),
+ std::ceil(srcBounds.h)))));
+ algorithm::resize_image(
+ dstCel->image(), dstCel2->image(),
+ algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR,
+ nullptr, nullptr, 0);
+
+ dstCel.reset(dstCel2.release());
+ dstCel->setPosition(gfx::Point(srcBounds.origin()));
+ }
+ // Copy original cel bounds
+ else if (!dstLayer->isTilemap()) {
+ if (srcCel->layer() &&
+ srcCel->layer()->isReference()) {
+ dstCel->setBoundsF(srcCel->boundsF());
+ }
+ else {
+ dstCel->setPosition(srcCel->position());
+ }
+ }
+
+ return dstCel.release();
+}
+
+void draw_image_into_new_tilemap_cel(
+ CmdSequence* cmds,
+ doc::LayerTilemap* dstLayer,
+ doc::Cel* dstCel,
+ const doc::Image* srcImage,
+ const gfx::Point& originOffset,
+ const gfx::Point& srcImagePos,
+ const gfx::Rect& canvasBounds,
+ doc::ImageRef& newTilemap)
+{
+ ASSERT(dstLayer->isTilemap());
+
+ doc::Tileset* tileset = dstLayer->tileset();
+ doc::Grid grid = tileset->grid();
+ grid.origin(grid.origin() + originOffset);
+
+ doc::TilesetHashTable hashImages; // TODO the hashImages should be inside the Tileset
+ {
+ // Add existent tiles in the hash table
+ int i = 0;
+ for (auto& image : *tileset)
+ hashImages[image] = i++;
+ }
+
+ gfx::Size tileSize = grid.tileSize();
+ const gfx::Rect tilemapBounds = grid.canvasToTile(canvasBounds);
+
+ if (!newTilemap) {
+ newTilemap.reset(doc::Image::create(IMAGE_TILEMAP,
+ tilemapBounds.w,
+ tilemapBounds.h));
+ newTilemap->clear(0); // <- This should not necessary
+ }
+ else {
+ ASSERT(tilemapBounds.w == newTilemap->width());
+ ASSERT(tilemapBounds.h == newTilemap->height());
+ }
+
+ for (const gfx::Point& tilePt : grid.tilesInCanvasRegion(gfx::Region(canvasBounds))) {
+ const gfx::Point tilePtInCanvas = grid.tileToCanvas(tilePt);
+ doc::ImageRef tileImage(
+ doc::crop_image(srcImage,
+ tilePtInCanvas.x-srcImagePos.x,
+ tilePtInCanvas.y-srcImagePos.y,
+ tileSize.w, tileSize.h,
+ srcImage->maskColor()));
+ if (grid.hasMask())
+ mask_image(tileImage.get(), grid.mask().get());
+
+ doc::tile_index tileIndex = 0;
+
+ auto it = hashImages.find(tileImage);
+ if (it != hashImages.end()) {
+ tileIndex = it->second; // TODO
+ }
+ else {
+ auto addTile = new cmd::AddTile(tileset, tileImage);
+ cmds->executeAndAdd(addTile);
+
+ tileIndex = addTile->tileIndex();
+ hashImages[tileImage] = tileIndex;
+ }
+
+ newTilemap->putPixel(
+ tilePt.x-tilemapBounds.x,
+ tilePt.y-tilemapBounds.y, doc::tile(tileIndex, 0));
+ }
+
+ static_cast(dstLayer->sprite()->document())
+ ->notifyTilesetChanged(tileset);
+
+ dstCel->data()->setImage(newTilemap, dstLayer);
+ dstCel->setPosition(grid.tileToCanvas(tilemapBounds.origin()));
+}
+
+void modify_tilemap_cel_region(
+ CmdSequence* cmds,
+ doc::Cel* cel,
+ const gfx::Region& region,
+ const TilesetMode tilesetMode,
+ const GetTileImageFunc& getTileImage)
+{
+ OPS_TRACE("modify_tilemap_cel_region %d %d %d %d\n",
+ region.bounds().x, region.bounds().y,
+ region.bounds().w, region.bounds().h);
+
+ if (region.isEmpty())
+ return;
+
+ ASSERT(cel->layer() && cel->layer()->isTilemap());
+ ASSERT(cel->image()->pixelFormat() == IMAGE_TILEMAP);
+
+ doc::LayerTilemap* tilemapLayer = static_cast(cel->layer());
+
+ Doc* doc = static_cast(tilemapLayer->sprite()->document());
+ doc::Tileset* tileset = tilemapLayer->tileset();
+ doc::Grid grid = tileset->grid();
+ grid.origin(grid.origin() + cel->position());
+
+ const gfx::Size tileSize = grid.tileSize();
+ const gfx::Rect oldTilemapBounds(grid.canvasToTile(cel->position()),
+ cel->image()->bounds().size());
+ const gfx::Rect patchTilemapBounds = grid.canvasToTile(region.bounds());
+ const gfx::Rect newTilemapBounds = (oldTilemapBounds | patchTilemapBounds);
+
+ OPS_TRACE("modify_tilemap_cel_region:\n"
+ " - grid.origin =%d %d\n"
+ " - cel.position =%d %d\n"
+ " - oldTilemapBounds =%d %d %d %d\n"
+ " - patchTilemapBounds=%d %d %d %d (region.bounds = %d %d %d %d)\n"
+ " - newTilemapBounds =%d %d %d %d\n",
+ grid.origin().x, grid.origin().y,
+ cel->position().x, cel->position().y,
+ oldTilemapBounds.x, oldTilemapBounds.y, oldTilemapBounds.w, oldTilemapBounds.h,
+ patchTilemapBounds.x, patchTilemapBounds.y, patchTilemapBounds.w, patchTilemapBounds.h,
+ region.bounds().x, region.bounds().y, region.bounds().w, region.bounds().h,
+ newTilemapBounds.x, newTilemapBounds.y, newTilemapBounds.w, newTilemapBounds.h);
+
+ // Autogenerate tiles
+ if (tilesetMode == TilesetMode::GenerateAllTiles) {
+ doc::TilesetHashTable hashImages; // TODO the hashImages should be inside the Tileset
+ {
+ // Add existent tiles in the hash table
+ int i = 0;
+ for (auto& image : *tileset)
+ hashImages[image] = i++;
+ }
+
+ // TODO create a smaller image
+ doc::ImageRef newTilemap(
+ doc::Image::create(IMAGE_TILEMAP,
+ newTilemapBounds.w,
+ newTilemapBounds.h));
+
+ newTilemap->clear(0); // TODO find the tile with empty content?
+ newTilemap->copy(
+ cel->image(),
+ gfx::Clip(oldTilemapBounds.x-newTilemapBounds.x,
+ oldTilemapBounds.y-newTilemapBounds.y, 0, 0,
+ oldTilemapBounds.w, oldTilemapBounds.h));
+
+ gfx::Region tilePtsRgn;
+
+ // This region includes the modified region by the user + the
+ // extra region added as we've incremented the tilemap size
+ // (newTilemapBounds).
+ gfx::Region regionToPatch(grid.tileToCanvas(newTilemapBounds));
+ regionToPatch -= gfx::Region(grid.tileToCanvas(oldTilemapBounds));
+ regionToPatch |= region;
+
+ for (const gfx::Point& tilePt : grid.tilesInCanvasRegion(regionToPatch)) {
+ const int u = tilePt.x-newTilemapBounds.x;
+ const int v = tilePt.y-newTilemapBounds.y;
+ OPS_TRACE(" - modify tile xy=%d %d uv=%d %d\n", tilePt.x, tilePt.y, u, v);
+ if (!newTilemap->bounds().contains(u, v))
+ continue;
+
+ const doc::tile_t t = newTilemap->getPixel(u, v);
+ const doc::tile_index ti = doc::tile_geti(t);
+ const doc::ImageRef existenTileImage = tileset->get(ti);
+
+ const gfx::Rect tileInCanvasRc(grid.tileToCanvas(tilePt), tileSize);
+ ImageRef tileImage(getTileImage(existenTileImage, tileInCanvasRc));
+ if (grid.hasMask())
+ mask_image(tileImage.get(), grid.mask().get());
+
+ tile_index tileIndex = 0;
+
+ auto it = hashImages.find(tileImage);
+ if (it != hashImages.end()) {
+ tileIndex = it->second; // TODO
+ }
+ else {
+ auto addTile = new cmd::AddTile(tileset, tileImage);
+ cmds->executeAndAdd(addTile);
+
+ tileIndex = addTile->tileIndex();
+ hashImages[tileImage] = tileIndex;
+ }
+
+ const doc::tile_t tile = doc::tile(tileIndex, 0);
+ if (t != tile) {
+ newTilemap->putPixel(u, v, tile);
+ tilePtsRgn |= gfx::Region(gfx::Rect(u, v, 1, 1));
+ }
+ }
+
+ doc->notifyTilesetChanged(tileset);
+
+ if (newTilemap->width() != cel->image()->width() ||
+ newTilemap->height() != cel->image()->height()) {
+ gfx::Point newPos = grid.tileToCanvas(newTilemapBounds.origin());
+ if (cel->position() != newPos) {
+ cmds->executeAndAdd(
+ new cmd::SetCelPosition(cel, newPos.x, newPos.y));
+ }
+ cmds->executeAndAdd(
+ new cmd::ReplaceImage(cel->sprite(), cel->imageRef(), newTilemap));
+ }
+ else {
+ cmds->executeAndAdd(
+ new cmd::CopyRegion(
+ cel->image(),
+ newTilemap.get(),
+ tilePtsRgn,
+ gfx::Point(0, 0)));
+ }
+ }
+ // Modify active set of tiles manually / don't auto-generate new tiles
+ else if (tilesetMode == TilesetMode::ModifyExistent) {
+ for (const gfx::Point& tilePt : grid.tilesInCanvasRegion(region)) {
+ // Ignore modifications outside the tilemap
+ if (!cel->image()->bounds().contains(tilePt.x, tilePt.y))
+ continue;
+
+ const doc::tile_t t = cel->image()->getPixel(tilePt.x, tilePt.y);
+ const doc::tile_index ti = doc::tile_geti(t);
+ const doc::ImageRef existenTileImage = tileset->get(ti);
+
+ const gfx::Rect tileInCanvasRc(grid.tileToCanvas(tilePt), tileSize);
+ ImageRef tileImage(getTileImage(existenTileImage, tileInCanvasRc));
+ if (grid.hasMask())
+ mask_image(tileImage.get(), grid.mask().get());
+
+ gfx::Region tileRgn(tileInCanvasRc);
+ tileRgn.createIntersection(tileRgn, region);
+ tileRgn.offset(-tileInCanvasRc.origin());
+
+ ImageRef tileOrigImage = tileset->get(ti);
+
+ cmds->executeAndAdd(
+ new cmd::CopyRegion(
+ tileOrigImage.get(),
+ tileImage.get(),
+ tileRgn,
+ gfx::Point(0, 0)));
+ }
+
+ doc->notifyTilesetChanged(tileset);
+ }
+}
+
+void clear_mask_from_cel(CmdSequence* cmds,
+ doc::Cel* cel,
+ const TilesetMode tilesetMode)
+{
+ ASSERT(cmds);
+ ASSERT(cel);
+ ASSERT(cel->layer());
+
+ if (cel->layer()->isTilemap()) {
+ Doc* doc = static_cast(cel->document());
+
+ // Simple case (there is no visible selection, so we remove the
+ // whole cel)
+ if (!doc->isMaskVisible()) {
+ cmds->executeAndAdd(new cmd::ClearCel(cel));
+ return;
+ }
+
+ color_t bgcolor = doc->bgColor(cel->layer());
+ doc::Mask* mask = doc->mask();
+
+ modify_tilemap_cel_region(
+ cmds, cel,
+ gfx::Region(doc->mask()->bounds()),
+ tilesetMode,
+ [bgcolor, mask](const doc::ImageRef& origTile,
+ const gfx::Rect& tileBoundsInCanvas) -> doc::ImageRef {
+ doc::ImageRef modified(doc::Image::createCopy(origTile.get()));
+ doc::algorithm::fill_selection(
+ modified.get(),
+ mask->bounds().origin() - tileBoundsInCanvas.origin(),
+ mask,
+ bgcolor);
+ return modified;
+ });
+ }
+ else {
+ cmds->executeAndAdd(new cmd::ClearMask(cel));
+ }
+}
+
+} // namespace app
diff --git a/src/app/util/cel_ops.h b/src/app/util/cel_ops.h
new file mode 100644
index 000000000..9c05f0057
--- /dev/null
+++ b/src/app/util/cel_ops.h
@@ -0,0 +1,67 @@
+// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
+// Copyright (C) 2001-2016 David Capello
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifndef APP_UTIL_CEL_OPS_H_INCLUDED
+#define APP_UTIL_CEL_OPS_H_INCLUDED
+#pragma once
+
+#include "app/tileset_mode.h"
+#include "doc/frame.h"
+#include "doc/image_ref.h"
+#include "gfx/point.h"
+#include "gfx/region.h"
+
+#include
+
+namespace doc {
+ class Cel;
+ class Layer;
+ class LayerTilemap;
+ class Sprite;
+}
+
+namespace app {
+ class CmdSequence;
+
+ typedef std::function GetTileImageFunc;
+
+ // The "cmds" is used in case that new tiles must be added in the
+ // dstLayer tilesets.
+ doc::Cel* create_cel_copy(
+ CmdSequence* cmds,
+ const doc::Cel* srcCel,
+ const doc::Sprite* dstSprite,
+ doc::Layer* dstLayer,
+ const doc::frame_t dstFrame);
+
+ // Draws an image creating new tiles.
+ void draw_image_into_new_tilemap_cel(
+ CmdSequence* cmds,
+ doc::LayerTilemap* dstLayer,
+ doc::Cel* dstCel,
+ const doc::Image* srcImage,
+ const gfx::Point& originOffset,
+ const gfx::Point& srcImagePos,
+ const gfx::Rect& canvasBounds,
+ doc::ImageRef& newTilemap);
+
+ void modify_tilemap_cel_region(
+ CmdSequence* cmds,
+ doc::Cel* cel,
+ const gfx::Region& region,
+ const TilesetMode tilesetMode,
+ const GetTileImageFunc& getTileImage);
+
+ void clear_mask_from_cel(
+ CmdSequence* cmds,
+ doc::Cel* cel,
+ const TilesetMode tilesetMode);
+
+} // namespace app
+
+#endif
diff --git a/src/app/util/clipboard.cpp b/src/app/util/clipboard.cpp
index cc64b75f9..21aeccd52 100644
--- a/src/app/util/clipboard.cpp
+++ b/src/app/util/clipboard.cpp
@@ -30,6 +30,7 @@
#include "app/ui/skin/skin_theme.h"
#include "app/ui/timeline/timeline.h"
#include "app/ui_context.h"
+#include "app/util/cel_ops.h"
#include "app/util/clipboard.h"
#include "app/util/clipboard_native.h"
#include "app/util/new_image_from_mask.h"
@@ -248,12 +249,14 @@ void clear_mask_from_cels(Tx& tx,
for (Cel* cel : cels) {
ObjectId celId = cel->id();
- tx(new cmd::ClearMask(cel));
+ clear_mask_from_cel(
+ tx, cel, ColorBar::instance()->tilesetMode());
// Get cel again just in case the cmd::ClearMask() called cmd::ClearCel()
cel = doc::get(celId);
if (cel &&
- cel->layer()->isTransparent()) {
+ cel->layer()->isTransparent() &&
+ !cel->layer()->isTilemap()) {
tx(new cmd::TrimCel(cel));
}
}
@@ -278,6 +281,7 @@ void cut(ContextWriter& writer)
console.printf("Can't copying an image portion from the current layer\n");
}
else {
+ // TODO This code is similar to DocView::onClear()
{
Tx tx(writer.context(), "Cut");
Site site = writer.context()->activeSite();
diff --git a/src/app/util/create_cel_copy.cpp b/src/app/util/create_cel_copy.cpp
deleted file mode 100644
index 324d4f91b..000000000
--- a/src/app/util/create_cel_copy.cpp
+++ /dev/null
@@ -1,116 +0,0 @@
-// Aseprite
-// Copyright (C) 2019 Igara Studio S.A.
-// Copyright (C) 2001-2018 David Capello
-//
-// This program is distributed under the terms of
-// the End-User License Agreement for Aseprite.
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-#include "doc/algorithm/resize_image.h"
-#include "doc/cel.h"
-#include "doc/image.h"
-#include "doc/layer.h"
-#include "doc/palette.h"
-#include "doc/primitives.h"
-#include "doc/sprite.h"
-#include "render/dithering.h"
-#include "render/ordered_dither.h"
-#include "render/quantization.h"
-#include "render/render.h"
-
-#include
-#include
-
-namespace app {
-
-using namespace doc;
-
-Cel* create_cel_copy(const Cel* srcCel,
- const Sprite* dstSprite,
- const Layer* dstLayer,
- const frame_t dstFrame)
-{
- const Image* celImage = srcCel->image();
-
- std::unique_ptr dstCel(
- new Cel(dstFrame,
- ImageRef(Image::create(dstSprite->pixelFormat(),
- celImage->width(),
- celImage->height()))));
-
- if ((dstSprite->pixelFormat() != celImage->pixelFormat()) ||
- // If both images are indexed but with different palette, we can
- // convert the source cel to RGB first.
- (dstSprite->pixelFormat() == IMAGE_INDEXED &&
- celImage->pixelFormat() == IMAGE_INDEXED &&
- srcCel->sprite()->palette(srcCel->frame())->countDiff(
- dstSprite->palette(dstFrame), nullptr, nullptr))) {
- ImageRef tmpImage(Image::create(IMAGE_RGB, celImage->width(), celImage->height()));
- tmpImage->clear(0);
-
- render::convert_pixel_format(
- celImage,
- tmpImage.get(),
- IMAGE_RGB,
- render::Dithering(),
- srcCel->sprite()->rgbMap(srcCel->frame()),
- srcCel->sprite()->palette(srcCel->frame()),
- srcCel->layer()->isBackground(),
- 0);
-
- render::convert_pixel_format(
- tmpImage.get(),
- dstCel->image(),
- IMAGE_INDEXED,
- render::Dithering(),
- dstSprite->rgbMap(dstFrame),
- dstSprite->palette(dstFrame),
- srcCel->layer()->isBackground(),
- dstSprite->transparentColor());
- }
- else {
- render::composite_image(
- dstCel->image(),
- celImage,
- srcCel->sprite()->palette(srcCel->frame()),
- 0, 0, 255, BlendMode::SRC);
- }
-
- // Resize a referecen cel to a non-reference layer
- if (srcCel->layer()->isReference() && !dstLayer->isReference()) {
- gfx::RectF srcBounds = srcCel->boundsF();
-
- std::unique_ptr dstCel2(
- new Cel(dstFrame,
- ImageRef(Image::create(dstSprite->pixelFormat(),
- std::ceil(srcBounds.w),
- std::ceil(srcBounds.h)))));
- algorithm::resize_image(
- dstCel->image(), dstCel2->image(),
- algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR,
- nullptr, nullptr, 0);
-
- dstCel.reset(dstCel2.release());
- dstCel->setPosition(gfx::Point(srcBounds.origin()));
- }
- // Copy original cel bounds
- else {
- if (srcCel->layer() &&
- srcCel->layer()->isReference()) {
- dstCel->setBoundsF(srcCel->boundsF());
- }
- else {
- dstCel->setPosition(srcCel->position());
- }
- }
-
- dstCel->setOpacity(srcCel->opacity());
- dstCel->data()->setUserData(srcCel->data()->userData());
-
- return dstCel.release();
-}
-
-} // namespace app
diff --git a/src/app/util/create_cel_copy.h b/src/app/util/create_cel_copy.h
deleted file mode 100644
index 4ab16ba52..000000000
--- a/src/app/util/create_cel_copy.h
+++ /dev/null
@@ -1,27 +0,0 @@
-// Aseprite
-// Copyright (C) 2001-2016 David Capello
-//
-// This program is distributed under the terms of
-// the End-User License Agreement for Aseprite.
-
-#ifndef APP_UTIL_CREATE_CEL_COPY_H_INCLUDED
-#define APP_UTIL_CREATE_CEL_COPY_H_INCLUDED
-#pragma once
-
-#include "doc/frame.h"
-
-namespace doc {
- class Cel;
- class Sprite;
-}
-
-namespace app {
-
- Cel* create_cel_copy(const Cel* srcCel,
- const Sprite* dstSprite,
- const Layer* dstLayer,
- const frame_t dstFrame);
-
-} // namespace app
-
-#endif
diff --git a/src/app/util/expand_cel_canvas.cpp b/src/app/util/expand_cel_canvas.cpp
index 8ea1f77ae..f884ffdd5 100644
--- a/src/app/util/expand_cel_canvas.cpp
+++ b/src/app/util/expand_cel_canvas.cpp
@@ -13,21 +13,28 @@
#include "app/app.h"
#include "app/cmd/add_cel.h"
-#include "app/cmd/clear_cel.h"
+#include "app/cmd/copy_rect.h"
#include "app/cmd/copy_region.h"
#include "app/cmd/patch_cel.h"
+#include "app/cmd_sequence.h"
#include "app/context.h"
#include "app/doc.h"
#include "app/site.h"
-#include "app/transaction.h"
+#include "app/util/cel_ops.h"
#include "app/util/range_utils.h"
#include "doc/algorithm/shrink_bounds.h"
#include "doc/cel.h"
#include "doc/image.h"
+#include "doc/image_impl.h"
#include "doc/layer.h"
+#include "doc/layer_tilemap.h"
#include "doc/primitives.h"
#include "doc/sprite.h"
+#include "doc/tileset.h"
+#include "doc/tileset_hash_table.h"
+#include "doc/tilesets.h"
#include "gfx/rect_io.h"
+#include "render/render.h"
#define EXP_TRACE(...) // TRACEARGS
@@ -61,8 +68,11 @@ static void create_buffers()
namespace app {
ExpandCelCanvas::ExpandCelCanvas(
- Site site, Layer* layer,
- TiledMode tiledMode, Transaction& transaction, Flags flags)
+ Site site,
+ Layer* layer,
+ const TiledMode tiledMode,
+ CmdSequence* cmds,
+ const Flags flags)
: m_document(site.document())
, m_sprite(site.sprite())
, m_layer(layer)
@@ -75,9 +85,14 @@ ExpandCelCanvas::ExpandCelCanvas(
, m_dstImage(NULL)
, m_closed(false)
, m_committed(false)
- , m_transaction(transaction)
- , m_canCompareSrcVsDst((m_flags & NeedsSource) == NeedsSource)
+ , m_cmds(cmds)
+ , m_tilesetMode(site.tilesetMode())
{
+ if (m_layer && m_layer->isTilemap()) {
+ m_flags = Flags(m_flags | NeedsSource);
+ }
+ m_canCompareSrcVsDst = ((m_flags & NeedsSource) == NeedsSource);
+
ASSERT(!singleton);
singleton = this;
@@ -98,11 +113,7 @@ ExpandCelCanvas::ExpandCelCanvas(
m_origCelPos = m_cel->position();
// Region to draw
- gfx::Rect celBounds(
- m_cel->x(),
- m_cel->y(),
- m_celImage ? m_celImage->width(): m_sprite->width(),
- m_celImage ? m_celImage->height(): m_sprite->height());
+ gfx::Rect celBounds = (m_celCreated ? m_sprite->bounds(): m_cel->bounds());
gfx::Rect spriteBounds(0, 0,
m_sprite->width(),
@@ -122,11 +133,18 @@ ExpandCelCanvas::ExpandCelCanvas(
if (m_celCreated) {
getDestCanvas();
- m_cel->data()->setImage(m_dstImage);
+ m_cel->data()->setImage(m_dstImage, m_layer);
if (m_layer && m_layer->isImage())
static_cast(m_layer)->addCel(m_cel);
}
+ // If we are in a tilemap, we use m_dstImage to draw pixels (instead
+ // of the tilemap image).
+ else if (m_layer->isTilemap()) {
+ // Calling "getDestCanvas()" we create the m_dstImage
+ getDestCanvas();
+ m_cel->data()->setImage(m_dstImage, m_layer);
+ }
}
ExpandCelCanvas::~ExpandCelCanvas()
@@ -166,21 +184,34 @@ void ExpandCelCanvas::commit()
// don't have a m_celImage)
validateDestCanvas(gfx::Region(m_bounds));
- // We can temporary remove the cel.
if (m_layer->isImage()) {
+ // We can temporary remove the cel.
static_cast(m_layer)->removeCel(m_cel);
- // Add a copy of m_dstImage in the sprite's image stock
gfx::Rect trimBounds = getTrimDstImageBounds();
if (!trimBounds.isEmpty()) {
- ImageRef newImage(trimDstImage(trimBounds));
- ASSERT(newImage);
+ // Convert the image to tiles
+ if (m_layer->isTilemap()) {
+ doc::ImageRef newTilemap;
+ draw_image_into_new_tilemap_cel(
+ m_cmds, static_cast(m_layer), m_cel,
+ // Draw the dst image in the tilemap
+ m_dstImage.get(),
+ m_origCelPos,
+ gfx::Point(0, 0), // m_dstImage->bounds(),
+ trimBounds,
+ newTilemap);
+ }
+ else {
+ ImageRef newImage(trimDstImage(trimBounds));
+ ASSERT(newImage);
- m_cel->data()->setImage(newImage);
- m_cel->setPosition(m_cel->position() + trimBounds.origin());
+ m_cel->data()->setImage(newImage, m_layer);
+ m_cel->setPosition(m_cel->position() + trimBounds.origin());
+ }
- // And finally we add the cel again in the layer.
- m_transaction.execute(new cmd::AddCel(m_layer, m_cel));
+ // And add the cel again in the layer.
+ m_cmds->executeAndAdd(new cmd::AddCel(m_layer, m_cel));
}
}
// We are selecting inside a layer group...
@@ -194,7 +225,14 @@ void ExpandCelCanvas::commit()
// Restore cel position to its original position
m_cel->setPosition(m_origCelPos);
- ASSERT(m_cel->image() == m_celImage.get());
+#ifdef _DEBUG
+ if (m_layer->isTilemap()) {
+ ASSERT(m_cel->image() != m_celImage.get());
+ }
+ else {
+ ASSERT(m_cel->image() == m_celImage.get());
+ }
+#endif
gfx::Region* regionToPatch = &m_validDstRegion;
gfx::Region reduced;
@@ -214,11 +252,40 @@ void ExpandCelCanvas::commit()
EXP_TRACE(" - regionToPatch", regionToPatch->bounds());
+ // Convert the image to tiles again
+ if (m_layer->isTilemap()) {
+ ASSERT(m_celImage.get() != m_cel->image());
+ ASSERT(m_celImage->pixelFormat() == IMAGE_TILEMAP);
+
+ // Validate the whole m_dstImage (invalid areas are cleared, as we
+ // don't have a m_celImage)
+ validateDestCanvas(gfx::Region(m_bounds));
+
+ // Restore the original m_celImage, because the cel contained
+ // the m_dstImage temporally for drawing purposes. No undo
+ // information is required at this moment.
+ m_cel->data()->setImage(m_celImage, m_layer);
+
+ // Put the region in absolute sprite canvas coordinates (instead
+ // of relative to the m_cel).
+ regionToPatch->offset(m_bounds.origin());
+
+ modify_tilemap_cel_region(
+ m_cmds, m_cel,
+ *regionToPatch,
+ m_tilesetMode,
+ [this](const doc::ImageRef& origTile,
+ const gfx::Rect& tileBoundsInCanvas) -> doc::ImageRef {
+ return trimDstImage(tileBoundsInCanvas);
+ });
+ }
// Check that the region to copy or patch is not empty before we
// create the new cmd
- if (!regionToPatch->isEmpty()) {
+ else if (!regionToPatch->isEmpty()) {
+ ASSERT(m_celImage.get() == m_cel->image());
+
if (m_layer->isBackground()) {
- m_transaction.execute(
+ m_cmds->executeAndAdd(
new cmd::CopyRegion(
m_cel->image(),
m_dstImage.get(),
@@ -226,7 +293,7 @@ void ExpandCelCanvas::commit()
m_bounds.origin()));
}
else {
- m_transaction.execute(
+ m_cmds->executeAndAdd(
new cmd::PatchCel(
m_cel,
m_dstImage.get(),
@@ -257,6 +324,13 @@ void ExpandCelCanvas::rollback()
delete m_cel;
m_celImage.reset();
}
+ // Restore the original tilemap
+ else if (m_layer->isTilemap()) {
+ ASSERT(m_cel->image()->pixelFormat() == m_sprite->pixelFormat());
+ ASSERT(m_celImage->pixelFormat() == IMAGE_TILEMAP);
+ m_cel->data()->setImage(m_celImage,
+ m_cel->layer());
+ }
m_closed = true;
}
@@ -298,18 +372,41 @@ void ExpandCelCanvas::validateSourceCanvas(const gfx::Region& rgn)
if (m_celImage) {
gfx::Region rgnToClear;
- rgnToClear.createSubtraction(rgnToValidate,
+ rgnToClear.createSubtraction(
+ rgnToValidate,
gfx::Region(m_celImage->bounds()
- .offset(m_origCelPos)
- .offset(-m_bounds.origin())));
+ .offset(m_origCelPos)
+ .offset(-m_bounds.origin())));
for (const auto& rc : rgnToClear)
fill_rect(m_srcImage.get(), rc, m_srcImage->maskColor());
- for (const auto& rc : rgnToValidate)
- m_srcImage->copy(m_celImage.get(),
- gfx::Clip(rc.x, rc.y,
- rc.x+m_bounds.x-m_origCelPos.x,
- rc.y+m_bounds.y-m_origCelPos.y, rc.w, rc.h));
+ if (m_celImage->pixelFormat() == IMAGE_TILEMAP) {
+ // For tilemaps, we can use the Render class to render visible
+ // tiles in the rgnToValidate of this cel.
+ render::Render subRender;
+ for (const auto& rc : rgnToValidate) {
+ subRender.renderCel(
+ m_srcImage.get(),
+ m_sprite,
+ m_celImage.get(),
+ m_layer,
+ m_sprite->palette(m_frame),
+ gfx::RectF(0, 0, m_bounds.w, m_bounds.h),
+ gfx::Clip(rc.x, rc.y,
+ rc.x+m_bounds.x-m_origCelPos.x,
+ rc.y+m_bounds.y-m_origCelPos.y, rc.w, rc.h),
+ 255, BlendMode::NORMAL);
+ }
+ }
+ else {
+ // We can copy the cel image directly
+ for (const auto& rc : rgnToValidate)
+ m_srcImage->copy(
+ m_celImage.get(),
+ gfx::Clip(rc.x, rc.y,
+ rc.x+m_bounds.x-m_origCelPos.x,
+ rc.y+m_bounds.y-m_origCelPos.y, rc.w, rc.h));
+ }
}
else {
for (const auto& rc : rgnToValidate)
@@ -332,18 +429,19 @@ void ExpandCelCanvas::validateDestCanvas(const gfx::Region& rgn)
src_y = m_bounds.y;
}
else {
- src = m_celImage.get();
+ src = m_cel->image();
src_x = m_origCelPos.x;
src_y = m_origCelPos.y;
}
- getDestCanvas();
+ getDestCanvas(); // Create m_dstImage
gfx::Region rgnToValidate(rgn);
rgnToValidate.offset(-m_bounds.origin());
rgnToValidate.createSubtraction(rgnToValidate, m_validDstRegion);
rgnToValidate.createIntersection(rgnToValidate, gfx::Region(m_dstImage->bounds()));
+ // ASSERT(src); // TODO is it always true?
if (src) {
gfx::Region rgnToClear;
rgnToClear.createSubtraction(rgnToValidate,
@@ -406,8 +504,8 @@ gfx::Rect ExpandCelCanvas::getTrimDstImageBounds() const
return m_dstImage->bounds();
else {
gfx::Rect bounds;
- algorithm::shrink_bounds(m_dstImage.get(), bounds,
- m_dstImage->maskColor());
+ algorithm::shrink_bounds(m_dstImage.get(),
+ m_dstImage->maskColor(), nullptr, bounds);
return bounds;
}
}
@@ -416,7 +514,8 @@ ImageRef ExpandCelCanvas::trimDstImage(const gfx::Rect& bounds) const
{
return ImageRef(
crop_image(m_dstImage.get(),
- bounds.x, bounds.y,
+ bounds.x-m_bounds.x,
+ bounds.y-m_bounds.y,
bounds.w, bounds.h,
m_dstImage->maskColor()));
}
diff --git a/src/app/util/expand_cel_canvas.h b/src/app/util/expand_cel_canvas.h
index 811e2fb0e..c88e3192b 100644
--- a/src/app/util/expand_cel_canvas.h
+++ b/src/app/util/expand_cel_canvas.h
@@ -1,4 +1,5 @@
// Aseprite
+// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@@ -8,6 +9,7 @@
#define APP_UTIL_EXPAND_CEL_CANVAS_H_INCLUDED
#pragma once
+#include "app/tileset_mode.h"
#include "doc/frame.h"
#include "doc/image_ref.h"
#include "filters/tiled_mode.h"
@@ -24,9 +26,9 @@ namespace doc {
}
namespace app {
+ class CmdSequence;
class Doc;
class Site;
- class Transaction;
using namespace filters;
using namespace doc;
@@ -46,7 +48,9 @@ namespace app {
};
ExpandCelCanvas(Site site, Layer* layer,
- TiledMode tiledMode, Transaction& undo, Flags flags);
+ const TiledMode tiledMode,
+ CmdSequence* cmds,
+ const Flags flags);
~ExpandCelCanvas();
// Commit changes made in getDestCanvas() in the cel's image. Adds
@@ -87,7 +91,7 @@ namespace app {
ImageRef m_dstImage;
bool m_closed;
bool m_committed;
- Transaction& m_transaction;
+ CmdSequence* m_cmds;
gfx::Region m_validSrcRegion;
gfx::Region m_validDstRegion;
@@ -95,6 +99,8 @@ namespace app {
// cel. This is false when dst is copied to the src, so we cannot
// reduce the patched region because both images will be the same.
bool m_canCompareSrcVsDst;
+
+ TilesetMode m_tilesetMode;
};
} // namespace app
diff --git a/src/app/util/new_image_from_mask.cpp b/src/app/util/new_image_from_mask.cpp
index 3f58ec5d5..8c2ed4a2b 100644
--- a/src/app/util/new_image_from_mask.cpp
+++ b/src/app/util/new_image_from_mask.cpp
@@ -14,6 +14,7 @@
#include "app/doc.h"
#include "app/site.h"
#include "doc/image_impl.h"
+#include "doc/layer.h"
#include "doc/mask.h"
#include "doc/primitives.h"
#include "render/render.h"
@@ -53,7 +54,7 @@ doc::Image* new_image_from_mask(const Site& site,
const Image* src = nullptr;
int x = 0, y = 0;
- if (merged) {
+ if (merged || site.layer()->isTilemap()) {
render::Render render;
render.setNewBlend(newBlend);
render.renderSprite(dst.get(), srcSprite, site.frame(),
diff --git a/src/dio/LICENSE.txt b/src/dio/LICENSE.txt
index a3236b421..06faf6309 100644
--- a/src/dio/LICENSE.txt
+++ b/src/dio/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2018 Igara Studio S.A.
+Copyright (c) 2018-2019 Igara Studio S.A.
Copyright (c) 2016-2018 David Capello
Permission is hereby granted, free of charge, to any person obtaining
diff --git a/src/dio/aseprite_common.h b/src/dio/aseprite_common.h
index a404fcdcf..31df25ef3 100644
--- a/src/dio/aseprite_common.h
+++ b/src/dio/aseprite_common.h
@@ -31,10 +31,12 @@
#define ASE_FILE_LAYER_IMAGE 0
#define ASE_FILE_LAYER_GROUP 1
+#define ASE_FILE_LAYER_TILEMAP 2
#define ASE_FILE_RAW_CEL 0
#define ASE_FILE_LINK_CEL 1
#define ASE_FILE_COMPRESSED_CEL 2
+#define ASE_FILE_COMPRESSED_TILEMAP 3
#define ASE_FILE_NO_COLOR_PROFILE 0
#define ASE_FILE_SRGB_COLOR_PROFILE 1
diff --git a/src/dio/aseprite_decoder.cpp b/src/dio/aseprite_decoder.cpp
index fceaa0a60..2e7a8c095 100644
--- a/src/dio/aseprite_decoder.cpp
+++ b/src/dio/aseprite_decoder.cpp
@@ -201,8 +201,7 @@ bool AsepriteDecoder::decode()
}
case ASE_FILE_CHUNK_TILESET: {
- delegate()->error(
- "Warning: The given file contains a tileset.\nThis version of Aseprite doesn't support tilemap layers.\n");
+ readTilesetChunk(sprite.get(), &header);
break;
}
@@ -439,6 +438,12 @@ doc::Layer* AsepriteDecoder::readLayerChunk(AsepriteHeader* header,
case ASE_FILE_LAYER_GROUP:
layer = new doc::LayerGroup(sprite);
break;
+
+ case ASE_FILE_LAYER_TILEMAP: {
+ doc::tileset_index tsi = read32();
+ layer = new doc::LayerTilemap(sprite, tsi);
+ break;
+ }
}
if (layer) {
@@ -481,11 +486,13 @@ doc::Layer* AsepriteDecoder::readLayerChunk(AsepriteHeader* header,
// Raw Image
//////////////////////////////////////////////////////////////////////
+namespace {
+
template
void read_raw_image(FileInterface* f,
DecodeDelegate* delegate,
doc::Image* image,
- AsepriteHeader* header)
+ const AsepriteHeader* header)
{
PixelIO pixel_io;
int x, y;
@@ -505,11 +512,11 @@ void read_raw_image(FileInterface* f,
//////////////////////////////////////////////////////////////////////
template
-void read_compressed_image(FileInterface* f,
- DecodeDelegate* delegate,
- doc::Image* image,
- size_t chunk_end,
- AsepriteHeader* header)
+void read_compressed_image_templ(FileInterface* f,
+ DecodeDelegate* delegate,
+ doc::Image* image,
+ const AsepriteHeader* header,
+ const size_t chunk_end)
{
PixelIO pixel_io;
z_stream zstream;
@@ -538,8 +545,9 @@ void read_compressed_image(FileInterface* f,
if (input_bytes == 0)
break; // Done, we consumed all chunk
}
- else
+ else {
input_bytes = compressed.size();
+ }
size_t bytes_read = f->readBytes(&compressed[0], input_bytes);
zstream.next_in = (Bytef*)&compressed[0];
@@ -583,6 +591,46 @@ void read_compressed_image(FileInterface* f,
throw base::Exception("ZLib error %d in inflateEnd().", err);
}
+void read_compressed_image(FileInterface* f,
+ DecodeDelegate* delegate,
+ doc::Image* image,
+ const AsepriteHeader* header,
+ const size_t chunk_end)
+{
+ // Try to read pixel data
+ try {
+ switch (image->pixelFormat()) {
+
+ case doc::IMAGE_RGB:
+ read_compressed_image_templ(
+ f, delegate, image, header, chunk_end);
+ break;
+
+ case doc::IMAGE_GRAYSCALE:
+ read_compressed_image_templ(
+ f, delegate, image, header, chunk_end);
+ break;
+
+ case doc::IMAGE_INDEXED:
+ read_compressed_image_templ(
+ f, delegate, image, header, chunk_end);
+ break;
+
+ case doc::IMAGE_TILEMAP:
+ read_compressed_image_templ(
+ f, delegate, image, header, chunk_end);
+ break;
+ }
+ }
+ // OK, in case of error we can show the problem, but continue
+ // loading more cels.
+ catch (const std::exception& e) {
+ delegate->error(e.what());
+ }
+}
+
+} // anonymous namespace
+
//////////////////////////////////////////////////////////////////////
// Cel Chunk
//////////////////////////////////////////////////////////////////////
@@ -591,8 +639,8 @@ doc::Cel* AsepriteDecoder::readCelChunk(doc::Sprite* sprite,
doc::LayerList& allLayers,
doc::frame_t frame,
doc::PixelFormat pixelFormat,
- AsepriteHeader* header,
- size_t chunk_end)
+ const AsepriteHeader* header,
+ const size_t chunk_end)
{
// Read chunk data
doc::layer_t layer_index = read16();
@@ -687,33 +735,29 @@ doc::Cel* AsepriteDecoder::readCelChunk(doc::Sprite* sprite,
if (w > 0 && h > 0) {
doc::ImageRef image(doc::Image::create(pixelFormat, w, h));
+ read_compressed_image(f(), delegate(), image.get(), header, chunk_end);
- // Try to read pixel data
- try {
- switch (image->pixelFormat()) {
+ cel.reset(new doc::Cel(frame, image));
+ cel->setPosition(x, y);
+ cel->setOpacity(opacity);
+ }
+ break;
+ }
- case doc::IMAGE_RGB:
- read_compressed_image(
- f(), delegate(), image.get(), chunk_end, header);
- break;
-
- case doc::IMAGE_GRAYSCALE:
- read_compressed_image(
- f(), delegate(), image.get(), chunk_end, header);
- break;
-
- case doc::IMAGE_INDEXED:
- read_compressed_image(
- f(), delegate(), image.get(), chunk_end, header);
- break;
- }
- }
- // OK, in case of error we can show the problem, but continue
- // loading more cels.
- catch (const std::exception& e) {
- delegate()->error(e.what());
- }
+ case ASE_FILE_COMPRESSED_TILEMAP: {
+ // Read width and height
+ int w = read16();
+ int h = read16();
+ int bitsPerTile = read16();
+ uint32_t tileIDkMask = read32();
+ uint32_t flipxMask = read32();
+ uint32_t fileyMask = read32();
+ uint32_t rot90Mask = read32();
+ readPadding(10);
+ if (w > 0 && h > 0) {
+ doc::ImageRef image(doc::Image::create(doc::IMAGE_TILEMAP, w, h));
+ read_compressed_image(f(), delegate(), image.get(), header, chunk_end);
cel.reset(new doc::Cel(frame, image));
cel->setPosition(x, y);
cel->setOpacity(opacity);
@@ -925,4 +969,45 @@ doc::Slice* AsepriteDecoder::readSliceChunk(doc::Slices& slices)
return slice.release();
}
+void AsepriteDecoder::readTilesetChunk(doc::Sprite* sprite,
+ const AsepriteHeader* header)
+{
+ const doc::tileset_index id = read32();
+ const uint32_t flags = read32();
+ const int w = read16();
+ const int h = read16();
+ readPadding(20);
+ const std::string name = readString();
+
+ // TODO add support to load the external filename
+ if (flags & 1) {
+ // Ignore fields
+ readString(); // external filename
+ read32(); // tileset ID in the external file
+ }
+
+ if (flags & 2) {
+ const doc::tile_index ntiles = read32();
+
+ doc::Grid grid(gfx::Size(w, h));
+ auto tileset = new doc::Tileset(sprite, grid, ntiles);
+ tileset->setName(name);
+
+ for (doc::tile_index i=0; itell();
+ const size_t dataEnd = dataBeg+dataSize;
+
+ doc::ImageRef tile(doc::Image::create(sprite->pixelFormat(), w, h));
+ read_compressed_image(f(), delegate(), tile.get(), header, dataEnd);
+ tileset->set(i, tile);
+
+ f()->seek(dataEnd);
+ }
+
+ sprite->tilesets()->set(id, tileset);
+ }
+}
+
} // namespace dio
diff --git a/src/dio/aseprite_decoder.h b/src/dio/aseprite_decoder.h
index d1b5607e1..71ac11337 100644
--- a/src/dio/aseprite_decoder.h
+++ b/src/dio/aseprite_decoder.h
@@ -1,5 +1,5 @@
// Aseprite Document IO Library
-// Copyright (c) 2018 Igara Studio S.A.
+// Copyright (c) 2018-2019 Igara Studio S.A.
// Copyright (c) 2017 David Capello
//
// This file is released under the terms of the MIT license.
@@ -50,8 +50,8 @@ private:
doc::LayerList& allLayers,
doc::frame_t frame,
doc::PixelFormat pixelFormat,
- AsepriteHeader* header,
- size_t chunk_end);
+ const AsepriteHeader* header,
+ const size_t chunk_end);
void readCelExtraChunk(doc::Cel* cel);
void readColorProfile(doc::Sprite* sprite);
doc::Mask* readMaskChunk();
@@ -59,6 +59,8 @@ private:
void readSlicesChunk(doc::Slices& slices);
doc::Slice* readSliceChunk(doc::Slices& slices);
void readUserDataChunk(doc::UserData* userData);
+ void readTilesetChunk(doc::Sprite* sprite,
+ const AsepriteHeader* header);
};
} // namespace dio
diff --git a/src/dio/pixel_io.h b/src/dio/pixel_io.h
index 3c9cf2e75..c0741b281 100644
--- a/src/dio/pixel_io.h
+++ b/src/dio/pixel_io.h
@@ -46,22 +46,21 @@ public:
}
void read_scanline(doc::RgbTraits::address_t address,
int w, uint8_t* buffer) {
- for (int x=0; x
+class PixelIO {
+ int b1, b2, b3, b4;
+public:
+ doc::TilemapTraits::pixel_t read_pixel(FileInterface* f) {
+ int b1 = f->read8();
+ int b2 = f->read8();
+ int b3 = f->read8();
+ int b4 = f->read8();
+
+ if (f->ok()) {
+ // Little endian
+ return ((b4 << 24) | (b3 << 16) | (b2 << 8) | b1);
+ }
+ else
+ return 0;
+ }
+ void read_scanline(doc::TilemapTraits::address_t address,
+ int w, uint8_t* buffer) {
+ for (int x=0; xbounds();
- if (algorithm::shrink_bounds(compImage.get(), newBounds, compImage->maskColor())) {
+ if (algorithm::shrink_bounds(compImage.get(),
+ compImage->maskColor(),
+ nullptr,
+ newBounds)) {
compCelBounds.offset(newBounds.x, newBounds.y);
compCelBounds.setSize(newBounds.size());
}
diff --git a/src/doc/algorithm/shrink_bounds.cpp b/src/doc/algorithm/shrink_bounds.cpp
index 6df00eec4..32d8e1975 100644
--- a/src/doc/algorithm/shrink_bounds.cpp
+++ b/src/doc/algorithm/shrink_bounds.cpp
@@ -11,9 +11,15 @@
#include "doc/algorithm/shrink_bounds.h"
+#include "doc/cel.h"
+#include "doc/grid.h"
#include "doc/image.h"
#include "doc/image_impl.h"
+#include "doc/layer.h"
+#include "doc/layer_tilemap.h"
+#include "doc/primitives.h"
#include "doc/primitives_fast.h"
+#include "doc/tileset.h"
#include
@@ -47,6 +53,12 @@ bool is_same_pixel(color_t pixel1, color_t pixel2)
return pixel1 == pixel2;
}
+template<>
+bool is_same_pixel(color_t pixel1, color_t pixel2)
+{
+ return pixel1 == pixel2;
+}
+
template
bool shrink_bounds_left_templ(const Image* image, gfx::Rect& bounds, color_t refpixel, int rowSize)
{
@@ -223,46 +235,171 @@ bool shrink_bounds_templ2(const Image* a, const Image* b, gfx::Rect& bounds)
return (!bounds.isEmpty());
}
+bool shrink_bounds_tilemap(const Image* image,
+ const color_t refpixel,
+ const Layer* layer,
+ gfx::Rect& bounds)
+{
+ ASSERT(layer);
+ if (!layer)
+ return false;
+
+ ASSERT(layer->isTilemap());
+ if (!layer->isTilemap())
+ return false;
+
+ const Tileset* tileset =
+ static_cast(layer)->tileset();
+
+ bool shrink;
+ int u, v;
+
+ // Shrink left side
+ for (u=bounds.x; u(image, u, v);
+ const tile_t tileIndex = tile_geti(tile);
+ const ImageRef tileImg = tileset->get(tileIndex);
+
+ if (tileImg && !is_plain_image(tileImg.get(), refpixel)) {
+ shrink = false;
+ break;
+ }
+ }
+ if (!shrink)
+ break;
+ ++bounds.x;
+ --bounds.w;
+ }
+
+ // Shrink right side
+ for (u=bounds.x+bounds.w-1; u>=bounds.x; --u) {
+ shrink = true;
+ for (v=bounds.y; v(image, u, v);
+ const tile_t tileIndex = tile_geti(tile);
+ const ImageRef tileImg = tileset->get(tileIndex);
+
+ if (tileImg && !is_plain_image(tileImg.get(), refpixel)) {
+ shrink = false;
+ break;
+ }
+ }
+ if (!shrink)
+ break;
+ --bounds.w;
+ }
+
+ // Shrink top side
+ for (v=bounds.y; v(image, u, v);
+ const tile_t tileIndex = tile_geti(tile);
+ const ImageRef tileImg = tileset->get(tileIndex);
+
+ if (tileImg && !is_plain_image(tileImg.get(), refpixel)) {
+ shrink = false;
+ break;
+ }
+ }
+ if (!shrink)
+ break;
+ ++bounds.y;
+ --bounds.h;
+ }
+
+ // Shrink bottom side
+ for (v=bounds.y+bounds.h-1; v>=bounds.y; --v) {
+ shrink = true;
+ for (u=bounds.x; u(image, u, v);
+ const tile_t tileIndex = tile_geti(tile);
+ const ImageRef tileImg = tileset->get(tileIndex);
+
+ if (tileImg && !is_plain_image(tileImg.get(), refpixel)) {
+ shrink = false;
+ break;
+ }
+ }
+ if (!shrink)
+ break;
+ --bounds.h;
+ }
+
+ return (!bounds.isEmpty());
+}
+
}
bool shrink_bounds(const Image* image,
- const gfx::Rect& start_bounds,
- gfx::Rect& bounds,
- color_t refpixel)
+ const color_t refpixel,
+ const Layer* layer,
+ const gfx::Rect& startBounds,
+ gfx::Rect& bounds)
{
- bounds = (start_bounds & image->bounds());
+ bounds = (startBounds & image->bounds());
switch (image->pixelFormat()) {
case IMAGE_RGB: return shrink_bounds_templ(image, bounds, refpixel);
case IMAGE_GRAYSCALE: return shrink_bounds_templ(image, bounds, refpixel);
case IMAGE_INDEXED: return shrink_bounds_templ(image, bounds, refpixel);
- case IMAGE_BITMAP:
- // Not supported
- break;
+ case IMAGE_BITMAP: return shrink_bounds_templ(image, bounds, refpixel);
+ case IMAGE_TILEMAP: return shrink_bounds_tilemap(image, refpixel, layer, bounds);
}
ASSERT(false);
- bounds = start_bounds;
+ bounds = startBounds;
return true;
}
-bool shrink_bounds(const Image* image, gfx::Rect& bounds, color_t refpixel)
+bool shrink_bounds(const Image* image,
+ const color_t refpixel,
+ const Layer* layer,
+ gfx::Rect& bounds)
{
- return shrink_bounds(image, image->bounds(), bounds, refpixel);
+ return shrink_bounds(image, refpixel, layer, image->bounds(), bounds);
}
-bool shrink_bounds2(const Image* a, const Image* b,
- const gfx::Rect& start_bounds,
+bool shrink_cel_bounds(const Cel* cel,
+ const color_t refpixel,
+ gfx::Rect& bounds)
+{
+ if (shrink_bounds(cel->image(), refpixel, cel->layer(), bounds)) {
+ // For tilemaps, we have to convert imgBounds (in tiles
+ // coordinates) to canvas coordinates using the Grid specs.
+ if (cel->layer()->isTilemap()) {
+ doc::LayerTilemap* tilemapLayer = static_cast(cel->layer());
+ doc::Tileset* tileset = tilemapLayer->tileset();
+ doc::Grid grid = tileset->grid();
+ grid.origin(grid.origin() + cel->position());
+ bounds = grid.tileToCanvas(bounds);
+ }
+ else {
+ bounds.offset(cel->position());
+ }
+ return true;
+ }
+ else {
+ return false;
+ }
+}
+
+bool shrink_bounds2(const Image* a,
+ const Image* b,
+ const gfx::Rect& startBounds,
gfx::Rect& bounds)
{
ASSERT(a && b);
ASSERT(a->bounds() == b->bounds());
- bounds = (start_bounds & a->bounds());
+ bounds = (startBounds & a->bounds());
switch (a->pixelFormat()) {
case IMAGE_RGB: return shrink_bounds_templ2(a, b, bounds);
case IMAGE_GRAYSCALE: return shrink_bounds_templ2(a, b, bounds);
case IMAGE_INDEXED: return shrink_bounds_templ2(a, b, bounds);
case IMAGE_BITMAP: return shrink_bounds_templ2(a, b, bounds);
+ // case IMAGE_TILEMAP: return shrink_bounds_templ2(a, b, bounds);
}
ASSERT(false);
return false;
diff --git a/src/doc/algorithm/shrink_bounds.h b/src/doc/algorithm/shrink_bounds.h
index 0f2d3ab6f..691b2ef19 100644
--- a/src/doc/algorithm/shrink_bounds.h
+++ b/src/doc/algorithm/shrink_bounds.h
@@ -1,4 +1,5 @@
// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@@ -13,22 +14,31 @@
#include "doc/color.h"
namespace doc {
+ class Cel;
class Image;
+ class Layer;
namespace algorithm {
+ // The layer is used in case the image is a tilemap and we need its tileset.
bool shrink_bounds(const Image* image,
- const gfx::Rect& start_bounds,
- gfx::Rect& bounds,
- color_t refpixel);
+ const color_t refpixel,
+ const Layer* layer,
+ const gfx::Rect& startBounds,
+ gfx::Rect& bounds);
bool shrink_bounds(const Image* image,
- gfx::Rect& bounds,
- color_t refpixel);
+ const color_t refpixel,
+ const Layer* layer,
+ gfx::Rect& bounds);
+
+ bool shrink_cel_bounds(const Cel* cel,
+ const color_t refpixel,
+ gfx::Rect& bounds);
bool shrink_bounds2(const Image* a,
const Image* b,
- const gfx::Rect& start_bounds,
+ const gfx::Rect& startBounds,
gfx::Rect& bounds);
} // algorithm
diff --git a/src/doc/cel.cpp b/src/doc/cel.cpp
index 6c67ff34b..b0efc7b4b 100644
--- a/src/doc/cel.cpp
+++ b/src/doc/cel.cpp
@@ -148,8 +148,11 @@ void Cel::setParentLayer(LayerImage* layer)
void Cel::fixupImage()
{
// Change the mask color to the sprite mask color
- if (m_layer && image())
+ if (m_layer && image()) {
image()->setMaskColor(m_layer->sprite()->transparentColor());
+ ASSERT(m_data);
+ m_data->adjustBounds(m_layer);
+ }
}
} // namespace doc
diff --git a/src/doc/cel_data.cpp b/src/doc/cel_data.cpp
index b83b83883..ac9acc2b5 100644
--- a/src/doc/cel_data.cpp
+++ b/src/doc/cel_data.cpp
@@ -1,4 +1,5 @@
// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@@ -10,10 +11,12 @@
#include "doc/cel_data.h"
-#include "gfx/rect.h"
#include "doc/image.h"
#include "doc/layer.h"
+#include "doc/layer_tilemap.h"
#include "doc/sprite.h"
+#include "doc/tileset.h"
+#include "gfx/rect.h"
namespace doc {
@@ -43,13 +46,38 @@ CelData::~CelData()
delete m_boundsF;
}
-void CelData::setImage(const ImageRef& image)
+void CelData::setImage(const ImageRef& image, Layer* layer)
{
ASSERT(image.get());
m_image = image;
- m_bounds.w = image->width();
- m_bounds.h = image->height();
+ adjustBounds(layer);
+}
+
+void CelData::setPosition(const gfx::Point& pos)
+{
+ m_bounds.setOrigin(pos);
+}
+
+void CelData::adjustBounds(Layer* layer)
+{
+ ASSERT(m_image);
+ if (m_image->pixelFormat() == IMAGE_TILEMAP) {
+ Tileset* tileset = nullptr;
+ if (layer && layer->isTilemap())
+ tileset = static_cast(layer)->tileset();
+ if (tileset) {
+ gfx::Size canvasSize =
+ tileset->grid().tilemapSizeToCanvas(
+ gfx::Size(m_image->width(),
+ m_image->height()));
+ m_bounds.w = canvasSize.w;
+ m_bounds.h = canvasSize.h;
+ return;
+ }
+ }
+ m_bounds.w = m_image->width();
+ m_bounds.h = m_image->height();
}
} // namespace doc
diff --git a/src/doc/cel_data.h b/src/doc/cel_data.h
index c10902148..8e535e7b4 100644
--- a/src/doc/cel_data.h
+++ b/src/doc/cel_data.h
@@ -1,6 +1,6 @@
// Aseprite Document Library
// Copyright (C) 2019 Igara Studio S.A.
-// Copyright (c) 2001-2016 David Capello
+// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@@ -18,6 +18,9 @@
namespace doc {
+ class Layer;
+ class Tileset;
+
class CelData : public WithUserData {
public:
CelData(const ImageRef& image);
@@ -30,8 +33,8 @@ namespace doc {
Image* image() const { return const_cast(m_image.get()); };
ImageRef imageRef() const { return m_image; }
- void setImage(const ImageRef& image);
- void setPosition(const gfx::Point& pos) { m_bounds.setOrigin(pos); }
+ void setImage(const ImageRef& image, Layer* layer);
+ void setPosition(const gfx::Point& pos);
void setOpacity(int opacity) { m_opacity = opacity; }
void setBounds(const gfx::Rect& bounds) {
@@ -60,6 +63,8 @@ namespace doc {
return sizeof(CelData) + m_image->getMemSize();
}
+ void adjustBounds(Layer* layer);
+
private:
ImageRef m_image;
int m_opacity;
diff --git a/src/doc/cels_range.cpp b/src/doc/cels_range.cpp
index 91911a2e3..56fc20279 100644
--- a/src/doc/cels_range.cpp
+++ b/src/doc/cels_range.cpp
@@ -1,4 +1,5 @@
// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
diff --git a/src/doc/color_mode.h b/src/doc/color_mode.h
index f9675159a..3878f7642 100644
--- a/src/doc/color_mode.h
+++ b/src/doc/color_mode.h
@@ -1,4 +1,5 @@
// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2001-2014 David Capello
//
// This file is released under the terms of the MIT license.
@@ -14,7 +15,8 @@ namespace doc {
RGB,
GRAYSCALE,
INDEXED,
- BITMAP
+ BITMAP,
+ TILEMAP,
};
} // namespace doc
diff --git a/src/doc/doc.h b/src/doc/doc.h
index 17203c193..55c0e058b 100644
--- a/src/doc/doc.h
+++ b/src/doc/doc.h
@@ -23,6 +23,7 @@
#include "doc/image_ref.h"
#include "doc/images_map.h"
#include "doc/layer.h"
+#include "doc/layer_tilemap.h"
#include "doc/mask.h"
#include "doc/object.h"
#include "doc/palette.h"
@@ -38,5 +39,8 @@
#include "doc/sprite.h"
#include "doc/tag.h"
#include "doc/tags.h"
+#include "doc/tile.h"
+#include "doc/tileset.h"
+#include "doc/tilesets.h"
#endif
diff --git a/src/doc/grid.cpp b/src/doc/grid.cpp
new file mode 100644
index 000000000..05d2398c0
--- /dev/null
+++ b/src/doc/grid.cpp
@@ -0,0 +1,163 @@
+// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "doc/grid.h"
+
+#include "doc/image.h"
+#include "doc/image_impl.h"
+#include "doc/image_ref.h"
+#include "doc/primitives.h"
+#include "gfx/point.h"
+#include "gfx/rect.h"
+#include "gfx/region.h"
+#include "gfx/size.h"
+
+#include
+#include
+#include
+#include
+
+namespace doc {
+
+// static
+Grid Grid::MakeRect(const gfx::Size& sz)
+{
+ return Grid(sz);
+}
+
+// Converts a tile position into a canvas position
+gfx::Point Grid::tileToCanvas(const gfx::Point& tile) const
+{
+ gfx::Point result;
+ result.x = tile.x * m_tileOffset.x + m_origin.x;
+ result.y = tile.y * m_tileOffset.y + m_origin.y;
+ if (tile.y & 1) // Odd row
+ result += m_oddRowOffset;
+ if (tile.x & 1) // Odd column
+ result += m_oddColOffset;
+ return result;
+}
+
+gfx::Rect Grid::tileToCanvas(const gfx::Rect& tileBounds) const
+{
+ gfx::Point pt1 = tileToCanvas(tileBounds.origin());
+ gfx::Point pt2 = tileToCanvas(tileBounds.point2());
+ return gfx::Rect(pt1, pt2);
+}
+
+gfx::Point Grid::canvasToTile(const gfx::Point& canvasPoint) const
+{
+ gfx::Point tile;
+ std::div_t divx = std::div((canvasPoint.x - m_origin.x), m_tileSize.w);
+ std::div_t divy = std::div((canvasPoint.y - m_origin.y), m_tileSize.h);
+ tile.x = divx.quot;
+ tile.y = divy.quot;
+ if (canvasPoint.x < m_origin.x && divx.rem) --tile.x;
+ if (canvasPoint.y < m_origin.y && divy.rem) --tile.y;
+
+ if (m_oddRowOffset.x != 0 || m_oddRowOffset.y != 0 ||
+ m_oddColOffset.x != 0 || m_oddColOffset.y != 0) {
+ gfx::Point bestTile = tile;
+ int bestDist = std::numeric_limits::max();
+ for (int v=-1; v<=2; ++v) {
+ for (int u=-1; u<=2; ++u) {
+ gfx::Point neighbor(tile.x+u, tile.y+v);
+ gfx::Point neighborCanvas = tileToCanvas(neighbor);
+
+ if (hasMask()) {
+ if (doc::get_pixel(m_mask.get(),
+ canvasPoint.x-neighborCanvas.x,
+ canvasPoint.y-neighborCanvas.y))
+ return neighbor;
+ }
+
+ gfx::Point delta = neighborCanvas+m_tileCenter-canvasPoint;
+ int dist = delta.x*delta.x + delta.y*delta.y;
+ if (bestDist > dist) {
+ bestDist = dist;
+ bestTile = neighbor;
+ }
+ }
+ }
+ tile = bestTile;
+ }
+
+ return tile;
+}
+
+gfx::Rect Grid::canvasToTile(const gfx::Rect& canvasBounds) const
+{
+ gfx::Point pt1 = canvasToTile(canvasBounds.origin());
+ gfx::Point pt2 = canvasToTile(gfx::Point(canvasBounds.x2()-1,
+ canvasBounds.y2()-1));
+ return gfx::Rect(pt1, gfx::Size(pt2.x - pt1.x + 1,
+ pt2.y - pt1.y + 1));
+}
+
+gfx::Size Grid::tilemapSizeToCanvas(const gfx::Size& tilemapSize) const
+{
+ gfx::Point pt = tileToCanvas(gfx::Point(tilemapSize.w-1,
+ tilemapSize.h-1));
+ return gfx::Size(pt.x + m_tileSize.w,
+ pt.y + m_tileSize.h);
+}
+
+gfx::Rect Grid::tileBoundsInCanvas(const gfx::Point& tile) const
+{
+ return gfx::Rect(tileToCanvas(tile), m_tileSize);
+}
+
+gfx::Rect Grid::alignBounds(const gfx::Rect& bounds) const
+{
+ gfx::Point pt1 = canvasToTile(bounds.origin());
+ gfx::Point pt2 = canvasToTile(gfx::Point(bounds.x2()-1,
+ bounds.y2()-1));
+ return
+ tileBoundsInCanvas(pt1) |
+ tileBoundsInCanvas(pt2);
+}
+
+std::vector Grid::tilesInCanvasRegion(const gfx::Region& rgn) const
+{
+ std::vector result;
+ if (rgn.isEmpty())
+ return result;
+
+ const gfx::Rect bounds = canvasToTile(rgn.bounds());
+ if (bounds.w < 1 ||
+ bounds.h < 1)
+ return result;
+
+ ImageRef tmp(Image::create(IMAGE_BITMAP, bounds.w, bounds.h));
+ const gfx::Rect tmpBounds = tmp->bounds();
+ tmp->clear(0);
+
+ for (const gfx::Rect& rc : rgn) {
+ gfx::Rect tileBounds = canvasToTile(rc);
+ tileBounds.x -= bounds.x;
+ tileBounds.y -= bounds.y;
+ tileBounds &= tmpBounds;
+ if (!tileBounds.isEmpty())
+ tmp->fillRect(tileBounds.x,
+ tileBounds.y,
+ tileBounds.x2()-1,
+ tileBounds.y2()-1, 1);
+ }
+
+ const LockImageBits bits(tmp.get());
+ for (auto it=bits.begin(), end=bits.end(); it!=end; ++it) {
+ if (*it)
+ result.push_back(gfx::Point(it.x()+bounds.x,
+ it.y()+bounds.y));
+ }
+ return result;
+}
+
+} // namespace doc
diff --git a/src/doc/grid.h b/src/doc/grid.h
new file mode 100644
index 000000000..e94c5c926
--- /dev/null
+++ b/src/doc/grid.h
@@ -0,0 +1,76 @@
+// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifndef DOC_GRID_H_INCLUDED
+#define DOC_GRID_H_INCLUDED
+#pragma once
+
+#include "doc/image_ref.h"
+#include "gfx/fwd.h"
+#include "gfx/point.h"
+#include "gfx/size.h"
+
+namespace doc {
+
+ class Grid {
+ public:
+ Grid(const gfx::Size& sz = gfx::Size(16, 16))
+ : m_tileSize(sz)
+ , m_origin(0, 0)
+ , m_tileCenter(gfx::Point(sz.w/2, sz.h/2))
+ , m_tileOffset(gfx::Point(sz.w, sz.h))
+ , m_oddRowOffset(0, 0)
+ , m_oddColOffset(0, 0)
+ , m_mask(nullptr) { }
+
+ static Grid MakeRect(const gfx::Size& sz);
+
+ gfx::Size tileSize() const { return m_tileSize; }
+ gfx::Point origin() const { return m_origin; }
+ gfx::Point tileCenter() const { return m_tileCenter; }
+ gfx::Point tileOffset() const { return m_tileOffset; }
+ gfx::Point oddRowOffset() const { return m_oddRowOffset; }
+ gfx::Point oddColOffset() const { return m_oddColOffset; }
+
+ void tileSize(const gfx::Size& tileSize) { m_tileSize = tileSize; }
+ void origin(const gfx::Point& origin) { m_origin = origin; }
+ void tileCenter(const gfx::Point& tileCenter) { m_tileCenter = tileCenter; }
+ void tileOffset(const gfx::Point& tileOffset) { m_tileOffset = tileOffset; }
+ void oddRowOffset(const gfx::Point& oddRowOffset) { m_oddRowOffset = oddRowOffset; }
+ void oddColOffset(const gfx::Point& oddColOffset) { m_oddColOffset = oddColOffset; }
+
+ // Converts a tile position into a canvas position
+ gfx::Point tileToCanvas(const gfx::Point& tile) const;
+ gfx::Rect tileToCanvas(const gfx::Rect& tileBounds) const;
+
+ // Converts a canvas position/rectangle into a tile position
+ gfx::Point canvasToTile(const gfx::Point& canvasPoint) const;
+ gfx::Rect canvasToTile(const gfx::Rect& canvasBounds) const;
+
+ gfx::Size tilemapSizeToCanvas(const gfx::Size& tilemapSize) const;
+ gfx::Rect tileBoundsInCanvas(const gfx::Point& tile) const;
+ gfx::Rect alignBounds(const gfx::Rect& bounds) const;
+
+ bool hasMask() const { return m_mask.get() != nullptr; }
+ doc::ImageRef mask() const { return m_mask; }
+ void setMask(const doc::ImageRef& mask) { m_mask = mask; }
+
+ // Returns an array of tile positions that are touching the given region in the canvas
+ std::vector tilesInCanvasRegion(const gfx::Region& rgn) const;
+
+ private:
+ gfx::Size m_tileSize;
+ gfx::Point m_origin;
+ gfx::Point m_tileCenter;
+ gfx::Point m_tileOffset;
+ gfx::Point m_oddRowOffset;
+ gfx::Point m_oddColOffset;
+ doc::ImageRef m_mask;
+ };
+
+} // namespace doc
+
+#endif
diff --git a/src/doc/grid_io.cpp b/src/doc/grid_io.cpp
new file mode 100644
index 000000000..8c2cca6f9
--- /dev/null
+++ b/src/doc/grid_io.cpp
@@ -0,0 +1,84 @@
+// Aseprite Document Library
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "doc/grid_io.h"
+
+#include "base/serialization.h"
+#include "doc/image.h"
+#include "doc/image_io.h"
+#include "doc/grid.h"
+
+#include
+
+namespace doc {
+
+using namespace base::serialization;
+using namespace base::serialization::little_endian;
+
+bool write_grid(std::ostream& os, const Grid& grid)
+{
+ write32(os, grid.tileSize().w);
+ write32(os, grid.tileSize().h);
+ write32(os, grid.origin().x);
+ write32(os, grid.origin().y);
+ write32(os, grid.tileCenter().x);
+ write32(os, grid.tileCenter().y);
+ write32(os, grid.tileOffset().x);
+ write32(os, grid.tileOffset().y);
+ write32(os, grid.oddRowOffset().x);
+ write32(os, grid.oddRowOffset().y);
+ write32(os, grid.oddColOffset().x);
+ write32(os, grid.oddColOffset().y);
+ write8(os, grid.hasMask());
+ if (grid.hasMask())
+ return write_image(os, grid.mask().get());
+ else
+ return true;
+}
+
+Grid read_grid(std::istream& is, bool setId)
+{
+ gfx::Size tileSize;
+ gfx::Point origin;
+ gfx::Point tileCenter;
+ gfx::Point tileOffset;
+ gfx::Point oddRowOffset;
+ gfx::Point oddColOffset;
+ tileSize.w = read32(is);
+ tileSize.h = read32(is);
+ origin.x = read32(is);
+ origin.y = read32(is);
+ tileCenter.x = read32(is);
+ tileCenter.y = read32(is);
+ tileOffset.x = read32(is);
+ tileOffset.y = read32(is);
+ oddRowOffset.x = read32(is);
+ oddRowOffset.y = read32(is);
+ oddColOffset.x = read32(is);
+ oddColOffset.y = read32(is);
+ bool hasMask = read8(is);
+
+ Grid grid;
+ grid.tileSize(tileSize);
+ grid.origin(origin);
+ grid.tileCenter(tileCenter);
+ grid.tileOffset(tileOffset);
+ grid.oddRowOffset(oddRowOffset);
+ grid.oddColOffset(oddColOffset);
+
+ if (hasMask) {
+ ImageRef mask(read_image(is));
+ if (mask)
+ grid.setMask(mask);
+ }
+ return grid;
+}
+
+} // namespace doc
diff --git a/src/doc/grid_io.h b/src/doc/grid_io.h
new file mode 100644
index 000000000..93aa1edd1
--- /dev/null
+++ b/src/doc/grid_io.h
@@ -0,0 +1,22 @@
+// Aseprite Document Library
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifndef DOC_GRID_IO_H_INCLUDED
+#define DOC_GRID_IO_H_INCLUDED
+#pragma once
+
+#include
+
+namespace doc {
+
+ class Grid;
+
+ bool write_grid(std::ostream& os, const Grid& grid);
+ Grid read_grid(std::istream& is, bool setId = true);
+
+} // namespace doc
+
+#endif
diff --git a/src/doc/grid_tests.cpp b/src/doc/grid_tests.cpp
new file mode 100644
index 000000000..abe8bcc51
--- /dev/null
+++ b/src/doc/grid_tests.cpp
@@ -0,0 +1,86 @@
+// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include
+
+#include "doc/grid.h"
+#include "gfx/rect_io.h"
+#include "gfx/region.h"
+
+// TODO add gfx/point_io.h and gfx/size_io.h
+namespace gfx {
+
+inline std::ostream& operator<<(std::ostream& os, const Point& pt) {
+ return os << "("
+ << pt.x << ", "
+ << pt.y << ")";
+}
+
+inline std::ostream& operator<<(std::ostream& os, const Size& sz) {
+ return os << "("
+ << sz.w << ", "
+ << sz.h << ")";
+}
+
+} // namespace gfx
+
+using namespace doc;
+using namespace gfx;
+
+TEST(Grid, Rect)
+{
+ auto grid = Grid::MakeRect(Size(16, 16));
+
+ EXPECT_EQ(Size(16, 16), grid.tileSize());
+ EXPECT_EQ(Point(16, 16), grid.tileOffset());
+
+ EXPECT_EQ(Point(16, 16), grid.tileToCanvas(Point(1, 1)));
+ EXPECT_EQ(Point(32, 16), grid.tileToCanvas(Point(2, 1)));
+ EXPECT_EQ(Point(0, 0), grid.canvasToTile(Point(5, 5)));
+ EXPECT_EQ(Point(1, 1), grid.canvasToTile(Point(16, 16)));
+ EXPECT_EQ(Rect(1, 0, 1, 2), grid.canvasToTile(Rect(16, 5, 16, 16)));
+
+ EXPECT_EQ(Rect(0, 0, 32, 32), grid.alignBounds(Rect(4, 5, 16, 17)));
+ EXPECT_EQ(Rect(16, 16, 16, 16), grid.alignBounds(Rect(16, 16, 16, 16)));
+ EXPECT_EQ(Rect(16, 16, 32, 16), grid.alignBounds(Rect(16, 16, 17, 16)));
+ EXPECT_EQ(Rect(16, 16, 48, 16), grid.alignBounds(Rect(17, 16, 32, 16)));
+
+ gfx::Region rgn;
+ rgn |= gfx::Region(gfx::Rect(5, 17, 8, 8));
+ rgn |= gfx::Region(gfx::Rect(17, 5, 16, 8));
+ auto pts = grid.tilesInCanvasRegion(rgn);
+ ASSERT_EQ(3, pts.size());
+ EXPECT_EQ(gfx::Point(1, 0), pts[0]);
+ EXPECT_EQ(gfx::Point(2, 0), pts[1]);
+ EXPECT_EQ(gfx::Point(0, 1), pts[2]);
+}
+
+TEST(Grid, RectWithOffset)
+{
+ auto grid = Grid::MakeRect(Size(16, 16));
+ grid.origin(gfx::Point(17, 16));
+
+ EXPECT_EQ(Point(0, 0), grid.canvasToTile(Point(17, 16)));
+ EXPECT_EQ(Point(0, 0), grid.canvasToTile(Point(17+15, 16)));
+ EXPECT_EQ(Point(1, 0), grid.canvasToTile(Point(17+16, 16)));
+
+ EXPECT_EQ(Point(-1, 0), grid.canvasToTile(Point(16, 16)));
+ EXPECT_EQ(Point(-1, 0), grid.canvasToTile(Point(2, 16)));
+ EXPECT_EQ(Point(-1, 0), grid.canvasToTile(Point(1, 16)));
+
+ grid.origin(gfx::Point(-1, -1));
+ EXPECT_EQ(Rect(1, 1, 1, 1), grid.canvasToTile(Rect(30, 30, 1, 1)));
+}
+
+int main(int argc, char** argv)
+{
+ ::testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
diff --git a/src/doc/image.cpp b/src/doc/image.cpp
index dcfa8bf89..993a4482c 100644
--- a/src/doc/image.cpp
+++ b/src/doc/image.cpp
@@ -1,5 +1,5 @@
// Aseprite Document Library
-// Copyright (c) 2018 Igara Studio S.A.
+// Copyright (c) 2018-2019 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@@ -61,6 +61,7 @@ Image* Image::create(const ImageSpec& spec,
case ColorMode::GRAYSCALE: return new ImageImpl(spec, buffer);
case ColorMode::INDEXED: return new ImageImpl(spec, buffer);
case ColorMode::BITMAP: return new ImageImpl(spec, buffer);
+ case ColorMode::TILEMAP: return new ImageImpl(spec, buffer);
}
return nullptr;
}
diff --git a/src/doc/image.h b/src/doc/image.h
index c95587f99..d5da243f7 100644
--- a/src/doc/image.h
+++ b/src/doc/image.h
@@ -1,5 +1,5 @@
// Aseprite Document Library
-// Copyright (c) 2018 Igara Studio S.A.
+// Copyright (c) 2018-2019 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@@ -107,6 +107,7 @@ namespace doc {
case IMAGE_GRAYSCALE: return GrayscaleTraits::getRowStrideBytes(pixels_per_row);
case IMAGE_INDEXED: return IndexedTraits::getRowStrideBytes(pixels_per_row);
case IMAGE_BITMAP: return BitmapTraits::getRowStrideBytes(pixels_per_row);
+ case IMAGE_TILEMAP: return TilemapTraits::getRowStrideBytes(pixels_per_row);
}
return 0;
}
diff --git a/src/doc/image_io.cpp b/src/doc/image_io.cpp
index c8ab4f0fe..b229e4b27 100644
--- a/src/doc/image_io.cpp
+++ b/src/doc/image_io.cpp
@@ -1,4 +1,5 @@
// Aseprite Document Library
+// Copyright (C) 2019 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@@ -110,12 +111,14 @@ Image* read_image(std::istream& is, bool setId)
if ((pixelFormat != IMAGE_RGB &&
pixelFormat != IMAGE_GRAYSCALE &&
pixelFormat != IMAGE_INDEXED &&
- pixelFormat != IMAGE_BITMAP) ||
+ pixelFormat != IMAGE_BITMAP &&
+ pixelFormat != IMAGE_TILEMAP) ||
(width < 1 || height < 1) ||
(width > 0xfffff || height > 0xfffff))
return nullptr;
- std::unique_ptr image(Image::create(static_cast(pixelFormat), width, height));
+ std::unique_ptr image(
+ Image::create(static_cast(pixelFormat), width, height));
int rowSize = image->getRowStrideSize();
#if 0
diff --git a/src/doc/image_iterator.h b/src/doc/image_iterator.h
index 6d3a0fb8a..37b8b782f 100644
--- a/src/doc/image_iterator.h
+++ b/src/doc/image_iterator.h
@@ -1,4 +1,5 @@
// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2001-2015 David Capello
//
// This file is released under the terms of the MIT license.
@@ -379,6 +380,9 @@ namespace doc {
return m_access;
}
+ int x() const { return m_x; }
+ int y() const { return m_y; }
+
private:
Image* m_image;
pointer m_ptr;
diff --git a/src/doc/image_traits.h b/src/doc/image_traits.h
index a56e91c2b..00fb6b3d5 100644
--- a/src/doc/image_traits.h
+++ b/src/doc/image_traits.h
@@ -158,6 +158,38 @@ namespace doc {
}
};
+ struct TilemapTraits {
+ static const ColorMode color_mode = ColorMode::TILEMAP;
+ static const PixelFormat pixel_format = IMAGE_TILEMAP;
+
+ enum {
+ bits_per_pixel = 32,
+ bytes_per_pixel = 4,
+ pixels_per_byte = 0,
+ channels = 3, // Tile Index + Tile Set + Flags
+ has_alpha = false,
+ };
+
+ typedef uint32_t pixel_t;
+ typedef pixel_t* address_t;
+ typedef const pixel_t* const_address_t;
+
+ static const pixel_t min_value = 0x00000000l;
+ static const pixel_t max_value = 0xffffffffl;
+
+ static inline int getRowStrideBytes(int pixels_per_row) {
+ return bytes_per_pixel * pixels_per_row;
+ }
+
+ static inline BlendFunc get_blender(BlendMode blend_mode, bool newBlend) {
+ return get_indexed_blender(blend_mode, newBlend);
+ }
+
+ static inline bool same_color(const pixel_t a, const pixel_t b) {
+ return a == b;
+ }
+ };
+
} // namespace doc
#endif
diff --git a/src/doc/layer.cpp b/src/doc/layer.cpp
index cbd9a9fd2..1d9e4b9e0 100644
--- a/src/doc/layer.cpp
+++ b/src/doc/layer.cpp
@@ -1,4 +1,5 @@
// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@@ -28,7 +29,9 @@ Layer::Layer(ObjectType type, Sprite* sprite)
int(LayerFlags::Visible) |
int(LayerFlags::Editable)))
{
- ASSERT(type == ObjectType::LayerImage || type == ObjectType::LayerGroup);
+ ASSERT(type == ObjectType::LayerImage ||
+ type == ObjectType::LayerGroup ||
+ type == ObjectType::LayerTilemap);
setName("Layer");
}
@@ -184,13 +187,18 @@ Cel* Layer::cel(frame_t frame) const
//////////////////////////////////////////////////////////////////////
// LayerImage class
-LayerImage::LayerImage(Sprite* sprite)
- : Layer(ObjectType::LayerImage, sprite)
+LayerImage::LayerImage(ObjectType type, Sprite* sprite)
+ : Layer(type, sprite)
, m_blendmode(BlendMode::NORMAL)
, m_opacity(255)
{
}
+LayerImage::LayerImage(Sprite* sprite)
+ : LayerImage(ObjectType::LayerImage, sprite)
+{
+}
+
LayerImage::~LayerImage()
{
destroyAllCels();
@@ -297,7 +305,8 @@ void LayerImage::addCel(Cel* cel)
ASSERT(cel->data() && "The cel doesn't contain CelData");
ASSERT(cel->image());
ASSERT(sprite());
- ASSERT(cel->image()->pixelFormat() == sprite()->pixelFormat());
+ ASSERT(cel->image()->pixelFormat() == sprite()->pixelFormat() ||
+ cel->image()->pixelFormat() == IMAGE_TILEMAP);
CelIterator it = findFirstCelIteratorAfter(cel->frame());
m_cels.insert(it, cel);
diff --git a/src/doc/layer.h b/src/doc/layer.h
index ba886cb99..a1d9031d8 100644
--- a/src/doc/layer.h
+++ b/src/doc/layer.h
@@ -1,4 +1,5 @@
// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@@ -23,8 +24,8 @@ namespace doc {
class Image;
class Sprite;
class Layer;
- class LayerImage;
class LayerGroup;
+ class LayerImage;
//////////////////////////////////////////////////////////////////////
// Layer class
@@ -72,8 +73,10 @@ namespace doc {
Layer* getPreviousInWholeHierarchy() const;
Layer* getNextInWholeHierarchy() const;
- bool isImage() const { return type() == ObjectType::LayerImage; }
+ bool isImage() const { return (type() == ObjectType::LayerImage ||
+ type() == ObjectType::LayerTilemap); }
bool isGroup() const { return type() == ObjectType::LayerGroup; }
+ bool isTilemap() const { return type() == ObjectType::LayerTilemap; }
virtual bool isBrowsable() const { return false; }
bool isBackground() const { return hasFlags(LayerFlags::Background); }
@@ -135,6 +138,7 @@ namespace doc {
class LayerImage : public Layer {
public:
+ LayerImage(ObjectType type, Sprite* sprite);
explicit LayerImage(Sprite* sprite);
virtual ~LayerImage();
diff --git a/src/doc/layer_io.cpp b/src/doc/layer_io.cpp
index de4f86f6a..a57d9431d 100644
--- a/src/doc/layer_io.cpp
+++ b/src/doc/layer_io.cpp
@@ -1,4 +1,5 @@
// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@@ -18,6 +19,7 @@
#include "doc/image_io.h"
#include "doc/layer.h"
#include "doc/layer_io.h"
+#include "doc/layer_tilemap.h"
#include "doc/sprite.h"
#include "doc/string_io.h"
#include "doc/subobjects_io.h"
@@ -96,6 +98,12 @@ void write_layer(std::ostream& os, const Layer* layer)
break;
}
+ case ObjectType::LayerTilemap: {
+ // Tileset index
+ write32(os, static_cast(layer)->tilesetIndex());
+ break;
+ }
+
}
write_user_data(os, layer->userData());
@@ -148,7 +156,7 @@ Layer* read_layer(std::istream& is, SubObjectsFromSprite* subObjects)
}
case ObjectType::LayerGroup: {
- // Create the layer set
+ // Create the layer group
layer.reset(new LayerGroup(subObjects->sprite()));
// Number of sub-layers
@@ -163,6 +171,13 @@ Layer* read_layer(std::istream& is, SubObjectsFromSprite* subObjects)
break;
}
+ case ObjectType::LayerTilemap: {
+ // Create the layer tilemap
+ doc::tileset_index tsi = read32(is); // Tileset index
+ layer.reset(new LayerTilemap(subObjects->sprite(), tsi));
+ break;
+ }
+
default:
throw InvalidLayerType("Invalid layer type found in stream");
diff --git a/src/doc/layer_tilemap.cpp b/src/doc/layer_tilemap.cpp
new file mode 100644
index 000000000..e8ce535b8
--- /dev/null
+++ b/src/doc/layer_tilemap.cpp
@@ -0,0 +1,31 @@
+// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "doc/layer_tilemap.h"
+
+#include "doc/primitives.h"
+#include "doc/sprite.h"
+#include "doc/tilesets.h"
+
+namespace doc {
+
+LayerTilemap::LayerTilemap(Sprite* sprite, const tileset_index tsi)
+ : LayerImage(ObjectType::LayerTilemap, sprite)
+ , m_tileset(sprite->tilesets()->get(tsi))
+ , m_tilesetIndex(tsi)
+{
+ ASSERT(m_tileset);
+}
+
+LayerTilemap::~LayerTilemap()
+{
+}
+
+} // namespace doc
diff --git a/src/doc/layer_tilemap.h b/src/doc/layer_tilemap.h
new file mode 100644
index 000000000..3fdfb299d
--- /dev/null
+++ b/src/doc/layer_tilemap.h
@@ -0,0 +1,35 @@
+// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifndef DOC_LAYER_TILEMAP_H_INCLUDED
+#define DOC_LAYER_TILEMAP_H_INCLUDED
+#pragma once
+
+#include "doc/layer.h"
+#include "doc/tile.h"
+#include "doc/tileset.h"
+
+namespace doc {
+
+ class LayerTilemap : public LayerImage {
+ public:
+ explicit LayerTilemap(Sprite* sprite, const tileset_index tsi);
+ ~LayerTilemap();
+
+ // Returns the tileset of this layer. New automatically-created
+ // tiles should be stored into this tileset, and all tiles in the
+ // layer should share the same Grid spec.
+ Tileset* tileset() const { return m_tileset; }
+ tileset_index tilesetIndex() const { return m_tilesetIndex; }
+
+ private:
+ Tileset* m_tileset;
+ tileset_index m_tilesetIndex;
+ };
+
+} // namespace doc
+
+#endif
diff --git a/src/doc/object_type.h b/src/doc/object_type.h
index 31f897375..4d1fc2b9c 100644
--- a/src/doc/object_type.h
+++ b/src/doc/object_type.h
@@ -22,10 +22,13 @@ namespace doc {
CelData,
LayerImage,
LayerGroup,
+ LayerTilemap,
Sprite,
Document,
Tag,
Slice,
+ Tileset,
+ Tilesets,
};
} // namespace doc
diff --git a/src/doc/pixel_format.h b/src/doc/pixel_format.h
index f15819e21..3cb46fed2 100644
--- a/src/doc/pixel_format.h
+++ b/src/doc/pixel_format.h
@@ -1,4 +1,5 @@
// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2001-2014 David Capello
//
// This file is released under the terms of the MIT license.
@@ -11,10 +12,11 @@
namespace doc {
enum PixelFormat {
- IMAGE_RGB,
- IMAGE_GRAYSCALE,
- IMAGE_INDEXED,
- IMAGE_BITMAP
+ IMAGE_RGB, // 32bpp see doc::rgba()
+ IMAGE_GRAYSCALE, // 16bpp see doc::graya()
+ IMAGE_INDEXED, // 8bpp
+ IMAGE_BITMAP, // 1bpp
+ IMAGE_TILEMAP, // 32bpp see doc::tile()
};
} // namespace doc
diff --git a/src/doc/primitives.cpp b/src/doc/primitives.cpp
index 80b7037d3..cf9ce05e2 100644
--- a/src/doc/primitives.cpp
+++ b/src/doc/primitives.cpp
@@ -17,6 +17,7 @@
#include "doc/palette.h"
#include "doc/remap.h"
#include "doc/rgbmap.h"
+#include "gfx/region.h"
#include
@@ -63,6 +64,12 @@ void copy_image(Image* dst, const Image* src, int x, int y)
dst->copy(src, gfx::Clip(x, y, 0, 0, src->width(), src->height()));
}
+void copy_image(Image* dst, const Image* src, const gfx::Region& rgn)
+{
+ for (const gfx::Rect& rc : rgn)
+ dst->copy(src, gfx::Clip(rc));
+}
+
Image* crop_image(const Image* image, int x, int y, int w, int h, color_t bg, const ImageBufferPtr& buffer)
{
ASSERT(image);
@@ -353,6 +360,7 @@ bool is_plain_image(const Image* img, color_t c)
case IMAGE_GRAYSCALE: return is_plain_image_templ(img, c);
case IMAGE_INDEXED: return is_plain_image_templ(img, c);
case IMAGE_BITMAP: return is_plain_image_templ(img, c);
+ case IMAGE_TILEMAP: return is_plain_image_templ(img, c);
}
return false;
}
@@ -377,6 +385,7 @@ int count_diff_between_images(const Image* i1, const Image* i2)
case IMAGE_GRAYSCALE: return count_diff_between_images_templ(i1, i2);
case IMAGE_INDEXED: return count_diff_between_images_templ(i1, i2);
case IMAGE_BITMAP: return count_diff_between_images_templ(i1, i2);
+ case IMAGE_TILEMAP: return count_diff_between_images_templ(i1, i2);
}
ASSERT(false);
@@ -395,6 +404,7 @@ bool is_same_image(const Image* i1, const Image* i2)
case IMAGE_GRAYSCALE: return is_same_image_templ(i1, i2);
case IMAGE_INDEXED: return is_same_image_templ(i1, i2);
case IMAGE_BITMAP: return is_same_image_templ(i1, i2);
+ case IMAGE_TILEMAP: return is_same_image_templ(i1, i2);
}
ASSERT(false);
diff --git a/src/doc/primitives.h b/src/doc/primitives.h
index fb000c8c5..639c7f792 100644
--- a/src/doc/primitives.h
+++ b/src/doc/primitives.h
@@ -27,6 +27,7 @@ namespace doc {
void copy_image(Image* dst, const Image* src);
void copy_image(Image* dst, const Image* src, int x, int y);
+ void copy_image(Image* dst, const Image* src, const gfx::Region& rgn);
Image* crop_image(const Image* image, int x, int y, int w, int h, color_t bg, const ImageBufferPtr& buffer = ImageBufferPtr());
Image* crop_image(const Image* image, const gfx::Rect& bounds, color_t bg, const ImageBufferPtr& buffer = ImageBufferPtr());
void rotate_image(const Image* src, Image* dst, int angle);
diff --git a/src/doc/sprite.cpp b/src/doc/sprite.cpp
index 5fa9139c3..6d1b2a6e7 100644
--- a/src/doc/sprite.cpp
+++ b/src/doc/sprite.cpp
@@ -23,6 +23,7 @@
#include "doc/remap.h"
#include "doc/rgbmap.h"
#include "doc/tag.h"
+#include "doc/tilesets.h"
#include
#include
@@ -61,6 +62,7 @@ Sprite::Sprite(const ImageSpec& spec,
, m_rgbMap(nullptr) // Initial RGB map
, m_tags(this)
, m_slices(this)
+ , m_tilesets(nullptr)
{
// Generate palette
switch (spec.colorMode()) {
@@ -91,6 +93,9 @@ Sprite::~Sprite()
// Destroy layers
delete m_root;
+ // Destroy tilesets
+ delete m_tilesets;
+
// Destroy palettes
{
PalettesList::iterator end = m_palettes.end();
@@ -467,7 +472,7 @@ void Sprite::replaceImage(ObjectId curImageId, const ImageRef& newImage)
{
for (Cel* cel : cels()) {
if (cel->image()->id() == curImageId)
- cel->data()->setImage(newImage);
+ cel->data()->setImage(newImage, cel->layer());
}
}
@@ -506,8 +511,6 @@ void Sprite::pickCels(const double x,
for (int i=(int)layers.size()-1; i>=0; --i) {
const Layer* layer = layers[i];
- if (!layer->isImage())
- continue;
Cel* cel = layer->cel(frame);
if (!cel)
@@ -611,4 +614,14 @@ CelsRange Sprite::uniqueCels(const SelectedFrames& selFrames) const
return CelsRange(this, selFrames, CelsRange::UNIQUE);
}
+////////////////////////////////////////
+// Tilesets
+
+Tilesets* Sprite::tilesets() const
+{
+ if (!m_tilesets)
+ m_tilesets = new Tilesets;
+ return m_tilesets;
+}
+
} // namespace doc
diff --git a/src/doc/sprite.h b/src/doc/sprite.h
index 1e6915676..6cc1f60e3 100644
--- a/src/doc/sprite.h
+++ b/src/doc/sprite.h
@@ -43,6 +43,7 @@ namespace doc {
class Remap;
class RgbMap;
class SelectedFrames;
+ class Tilesets;
typedef std::vector PalettesList;
@@ -188,6 +189,11 @@ namespace doc {
CelsRange uniqueCels() const;
CelsRange uniqueCels(const SelectedFrames& selFrames) const;
+ ////////////////////////////////////////
+ // Tilesets
+
+ Tilesets* tilesets() const;
+
private:
Document* m_document;
ImageSpec m_spec;
@@ -204,6 +210,9 @@ namespace doc {
Tags m_tags;
Slices m_slices;
+ // Tilesets
+ mutable Tilesets* m_tilesets;
+
// Disable default constructor and copying
Sprite();
DISABLE_COPYING(Sprite);
diff --git a/src/doc/tile.h b/src/doc/tile.h
new file mode 100644
index 000000000..0a128cf98
--- /dev/null
+++ b/src/doc/tile.h
@@ -0,0 +1,44 @@
+// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifndef DOC_TILE_H_INCLUDED
+#define DOC_TILE_H_INCLUDED
+#pragma once
+
+#include "base/ints.h"
+
+namespace doc {
+
+ typedef uint32_t tile_t; // Same as color_t
+ typedef uint32_t tile_index;
+ typedef uint32_t tileset_index;
+ typedef uint32_t tile_flags;
+
+ const uint32_t tile_i_shift = 0; // Tile index
+ const uint32_t tile_f_shift = 28; // Flags (flip, rotation)
+
+ const uint32_t tile_i_mask = 0x1fffffff;
+ const uint32_t tile_f_mask = 0xe0000000; // 3 flags
+ const uint32_t tile_f_flipx = 0x20000000;
+ const uint32_t tile_f_flipy = 0x40000000;
+ const uint32_t tile_f_90cw = 0x80000000;
+
+ inline tile_index tile_geti(const tile_t t) {
+ return ((t & tile_i_mask) >> tile_i_shift);
+ }
+
+ inline tile_flags tile_getf(const tile_t t) {
+ return (t & tile_f_mask);
+ }
+
+ inline tile_t tile(const tile_index i, const tile_flags f) {
+ return (((i << tile_i_shift) & tile_i_mask) |
+ (f & tile_f_mask));
+ }
+
+} // namespace doc
+
+#endif
diff --git a/src/doc/tileset.cpp b/src/doc/tileset.cpp
new file mode 100644
index 000000000..d9df3b91b
--- /dev/null
+++ b/src/doc/tileset.cpp
@@ -0,0 +1,45 @@
+// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "doc/tileset.h"
+
+namespace doc {
+
+Tileset::Tileset(Sprite* sprite,
+ const Grid& grid,
+ const tileset_index ntiles)
+ : Object(ObjectType::Tileset)
+ , m_sprite(sprite)
+ , m_grid(grid)
+ , m_tiles(ntiles)
+{
+}
+
+void Tileset::setOrigin(const gfx::Point& pt)
+{
+ m_grid.origin(pt);
+}
+
+int Tileset::getMemSize() const
+{
+ int size = sizeof(Tileset) + m_name.size();
+ for (auto& img : const_cast(this)->m_tiles) {
+ if (img)
+ size += img->getMemSize();
+ }
+ return size;
+}
+
+void Tileset::resize(const tile_index ntiles)
+{
+ m_tiles.resize(ntiles);
+}
+
+} // namespace doc
diff --git a/src/doc/tileset.h b/src/doc/tileset.h
new file mode 100644
index 000000000..d0f0ec63d
--- /dev/null
+++ b/src/doc/tileset.h
@@ -0,0 +1,76 @@
+// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifndef DOC_TILESET_H_INCLUDED
+#define DOC_TILESET_H_INCLUDED
+#pragma once
+
+#include "doc/grid.h"
+#include "doc/image_ref.h"
+#include "doc/object.h"
+#include "doc/tile.h"
+
+#include
+#include
+
+namespace doc {
+
+ class Sprite;
+
+ class Tileset : public Object {
+ public:
+ typedef std::vector Tiles;
+ typedef Tiles::iterator iterator;
+
+ Tileset(Sprite* sprite,
+ const Grid& grid,
+ const tileset_index ntiles);
+
+ Sprite* sprite() const { return m_sprite; }
+ const Grid& grid() const { return m_grid; }
+ void setOrigin(const gfx::Point& pt);
+
+ const std::string& name() const { return m_name; }
+ void setName(const std::string& name) { m_name = name; }
+
+ int getMemSize() const override;
+
+ iterator begin() { return m_tiles.begin(); }
+ iterator end() { return m_tiles.end(); }
+ tile_index size() const { return tile_index(m_tiles.size()); }
+ void resize(const tile_index ntiles);
+
+ ImageRef get(const tile_index index) const {
+ if (index < size())
+ return m_tiles[index];
+ else
+ return ImageRef(nullptr);
+ }
+
+ void set(const tile_index index, ImageRef& image) {
+ m_tiles[index] = image;
+ }
+
+ tile_index add(const ImageRef& image) {
+ m_tiles.push_back(image);
+ return tile_t(m_tiles.size()-1);
+ }
+
+ void erase(const tile_index ti) {
+ ASSERT(ti >= 0 && ti < size());
+ m_tiles.erase(m_tiles.begin()+ti);
+ }
+
+ private:
+ Sprite* m_sprite;
+ Grid m_grid;
+ Tiles m_tiles;
+ std::string m_name;
+ };
+
+} // namespace doc
+
+#endif
diff --git a/src/doc/tileset_hash_table.h b/src/doc/tileset_hash_table.h
new file mode 100644
index 000000000..22122d981
--- /dev/null
+++ b/src/doc/tileset_hash_table.h
@@ -0,0 +1,29 @@
+// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifndef DOC_TILESET_HASH_TABLE_H_INCLUDED
+#define DOC_TILESET_HASH_TABLE_H_INCLUDED
+#pragma once
+
+#include "doc/image.h"
+#include "doc/image_ref.h"
+#include "doc/images_map.h"
+#include "doc/primitives.h"
+#include "doc/tile.h"
+
+#include
+
+namespace doc {
+
+ // A hash table used to match Image pixels data <-> tileset index
+ typedef std::unordered_map TilesetHashTable;
+
+} // namespace doc
+
+#endif
diff --git a/src/doc/tileset_io.cpp b/src/doc/tileset_io.cpp
new file mode 100644
index 000000000..861c2e651
--- /dev/null
+++ b/src/doc/tileset_io.cpp
@@ -0,0 +1,49 @@
+// Aseprite Document Library
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "doc/tileset_io.h"
+
+#include "base/serialization.h"
+#include "doc/grid_io.h"
+#include "doc/subobjects_io.h"
+#include "doc/tileset.h"
+
+#include
+
+namespace doc {
+
+using namespace base::serialization;
+using namespace base::serialization::little_endian;
+
+bool write_tileset(std::ostream& os,
+ const Tileset* tileset,
+ CancelIO* cancel)
+{
+ write32(os, tileset->id());
+ write32(os, tileset->size());
+ return write_grid(os, tileset->grid());
+
+ // TODO save images
+}
+
+Tileset* read_tileset(std::istream& is,
+ SubObjectsFromSprite* subObjects,
+ bool setId)
+{
+ ObjectId id = read32(is);
+ tileset_index ntiles = read32(is);
+ Grid grid = read_grid(is, setId);
+ auto tileset = new Tileset(subObjects->sprite(), grid, ntiles);
+ if (setId)
+ tileset->setId(id);
+ return tileset;
+}
+
+}
diff --git a/src/doc/tileset_io.h b/src/doc/tileset_io.h
new file mode 100644
index 000000000..7b44f3380
--- /dev/null
+++ b/src/doc/tileset_io.h
@@ -0,0 +1,29 @@
+// Aseprite Document Library
+// Copyright (C) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifndef DOC_TILESET_IO_H_INCLUDED
+#define DOC_TILESET_IO_H_INCLUDED
+#pragma once
+
+#include
+
+namespace doc {
+
+ class CancelIO;
+ class SubObjectsFromSprite;
+ class Tileset;
+
+ bool write_tileset(std::ostream& os,
+ const Tileset* tileset,
+ CancelIO* cancel = nullptr);
+
+ Tileset* read_tileset(std::istream& is,
+ SubObjectsFromSprite* subObjects,
+ bool setId = true);
+
+} // namespace doc
+
+#endif
diff --git a/src/doc/tilesets.cpp b/src/doc/tilesets.cpp
new file mode 100644
index 000000000..2a3ad4fe3
--- /dev/null
+++ b/src/doc/tilesets.cpp
@@ -0,0 +1,42 @@
+// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "doc/tilesets.h"
+
+namespace doc {
+
+Tilesets::Tilesets()
+ : Object(ObjectType::Tilesets)
+{
+}
+
+Tilesets::~Tilesets()
+{
+ for (auto tileset : m_tilesets)
+ delete tileset;
+}
+
+int Tilesets::getMemSize() const
+{
+ int size = sizeof(Tilesets);
+ for (auto tileset : m_tilesets) {
+ if (tileset)
+ size += tileset->getMemSize();
+ }
+ return size;
+}
+
+tileset_index Tilesets::add(Tileset* tileset)
+{
+ m_tilesets.push_back(tileset);
+ return tileset_index(m_tilesets.size()-1);
+}
+
+} // namespace doc
diff --git a/src/doc/tilesets.h b/src/doc/tilesets.h
new file mode 100644
index 000000000..65d53b37d
--- /dev/null
+++ b/src/doc/tilesets.h
@@ -0,0 +1,64 @@
+// Aseprite Document Library
+// Copyright (c) 2019 Igara Studio S.A.
+//
+// This file is released under the terms of the MIT license.
+// Read LICENSE.txt for more information.
+
+#ifndef DOC_TILESETS_H_INCLUDED
+#define DOC_TILESETS_H_INCLUDED
+#pragma once
+
+#include "doc/tileset.h"
+
+#include
+
+namespace doc {
+
+ class Tilesets : public Object {
+ public:
+ typedef std::vector Array;
+ typedef Array::iterator iterator;
+ typedef Array::const_iterator const_iterator;
+
+ Tilesets();
+ ~Tilesets();
+
+ int getMemSize() const override;
+
+ iterator begin() { return m_tilesets.begin(); }
+ iterator end() { return m_tilesets.end(); }
+ const_iterator begin() const { return m_tilesets.begin(); }
+ const_iterator end() const { return m_tilesets.end(); }
+ tileset_index size() const { return (tileset_index)m_tilesets.size(); }
+
+ tileset_index add(Tileset* tileset);
+
+ Tileset* get(const tileset_index tsi) {
+ if (tsi < size())
+ return m_tilesets[tsi];
+ else
+ return nullptr;
+ }
+
+ void set(const tileset_index tsi, Tileset* tileset) {
+ if (tsi >= m_tilesets.size())
+ m_tilesets.resize(tsi+1, nullptr);
+ m_tilesets[tsi] = tileset;
+ }
+
+ void erase(const tileset_index tsi) {
+ // Do not m_tilesets.erase() the tileset so other tilesets
+ // indexes/IDs are kept intact.
+ if (tsi == size()-1)
+ m_tilesets.erase(--m_tilesets.end());
+ else
+ m_tilesets[tsi] = nullptr;
+ }
+
+ private:
+ Array m_tilesets;
+ };
+
+} // namespace doc
+
+#endif
diff --git a/src/render/render.cpp b/src/render/render.cpp
index d8cbcada2..452ab97b6 100644
--- a/src/render/render.cpp
+++ b/src/render/render.cpp
@@ -17,11 +17,16 @@
#include "doc/doc.h"
#include "doc/handle_anidir.h"
#include "doc/image_impl.h"
+#include "doc/layer_tilemap.h"
+#include "doc/tileset.h"
+#include "doc/tilesets.h"
#include "gfx/clip.h"
#include "gfx/region.h"
#include
+#define TRACE_RENDER_CEL(...)
+
namespace render {
namespace {
@@ -1042,7 +1047,8 @@ void Render::renderLayer(
switch (layer->type()) {
- case ObjectType::LayerImage: {
+ case ObjectType::LayerImage:
+ case ObjectType::LayerTilemap: {
if ((!render_background && layer->isBackground()) ||
(!render_transparent && !layer->isBackground()))
break;
@@ -1084,8 +1090,6 @@ void Render::renderLayer(
m_previewPos.y,
m_previewImage->width(),
m_previewImage->height());
-
- ASSERT(celImage->pixelFormat() == cel->image()->pixelFormat());
}
else {
celImage = cel->image();
@@ -1115,8 +1119,6 @@ void Render::renderLayer(
if (!isSelected && m_nonactiveLayersOpacity != 255)
opacity = MUL_UN8(opacity, m_nonactiveLayersOpacity, t);
- ASSERT(celImage->maskColor() == m_sprite->transparentColor());
-
// Draw parts outside the "m_extraCel" area
if (drawExtra && m_extraType == ExtraType::PATCH) {
gfx::Region originalAreas(area.srcBounds());
@@ -1125,16 +1127,16 @@ void Render::renderLayer(
for (auto rc : originalAreas) {
renderCel(
- image, celImage, pal, celBounds,
+ image, celImage, layer, pal, celBounds,
gfx::Clip(area.dst.x+rc.x-area.src.x,
- area.dst.y+rc.y-area.src.y, rc), compositeImage,
- opacity, layerBlendMode);
+ area.dst.y+rc.y-area.src.y, rc),
+ compositeImage, opacity, layerBlendMode);
}
}
// Draw the whole cel
else {
renderCel(
- image, celImage, pal,
+ image, celImage, layer, pal,
celBounds, area, compositeImage,
opacity, layerBlendMode);
}
@@ -1164,6 +1166,7 @@ void Render::renderLayer(
if (m_extraCel->opacity() > 0) {
renderCel(
image, m_extraImage,
+ nullptr, // Without layer
m_sprite->palette(frame),
m_extraCel->bounds(),
gfx::Clip(area.dst.x+extraArea.x-area.src.x,
@@ -1199,6 +1202,7 @@ void Render::renderCel(
renderCel(
dst_image,
cel_image,
+ cel_layer,
pal,
celBounds,
area,
@@ -1210,6 +1214,7 @@ void Render::renderCel(
void Render::renderCel(
Image* dst_image,
const Image* cel_image,
+ const Layer* cel_layer,
const Palette* pal,
const gfx::RectF& celBounds,
const gfx::Clip& area,
@@ -1217,14 +1222,56 @@ void Render::renderCel(
const int opacity,
const BlendMode blendMode)
{
- renderImage(dst_image,
- cel_image,
- pal,
- celBounds,
- area,
- compositeImage,
- opacity,
- blendMode);
+ TRACE_RENDER_CEL("dstImage=(%d %d) celImage=(%d %d) celBounds=(%d %d %d %d) clipArea=(src=%d %d dst=%d %d %d %d)\n",
+ dst_image->width(), dst_image->height(),
+ cel_image->width(), cel_image->height(),
+ int(celBounds.x), int(celBounds.y),
+ int(celBounds.w), int(celBounds.h),
+ area.src.x, area.src.y, area.dst.x, area.dst.y, area.size.w, area.size.h);
+
+ if (cel_layer &&
+ cel_image->pixelFormat() == IMAGE_TILEMAP) {
+ ASSERT(cel_layer->isTilemap());
+
+ auto tilemapLayer = static_cast(cel_layer);
+ doc::Grid grid = tilemapLayer->tileset()->grid();
+ grid.origin(grid.origin() + gfx::Point(celBounds.origin()));
+
+ const Tileset* tileset = tilemapLayer->tileset();
+ ASSERT(tileset);
+ if (!tileset)
+ return;
+
+ gfx::Rect tilesToDraw = grid.canvasToTile(gfx::Rect(area.src, area.size));
+ tilesToDraw &= cel_image->bounds();
+ TRACE_RENDER_CEL("Drawing tilemap (%d %d %d %d)\n",
+ tilesToDraw.x, tilesToDraw.y, tilesToDraw.w, tilesToDraw.h);
+
+ for (int v=tilesToDraw.y; v (%d %d %d %d)\n", u, v,
+ tileBoundsOnCanvas.x, tileBoundsOnCanvas.y,
+ tileBoundsOnCanvas.w, tileBoundsOnCanvas.h);
+ if (!cel_image->bounds().contains(u, v))
+ continue;
+
+ const tile_t t = cel_image->getPixel(u, v);
+ const tile_index i = tile_geti(t);
+
+ const ImageRef tile_image = tileset->get(i);
+ if (!tile_image)
+ continue;
+
+ renderImage(dst_image, tile_image.get(), pal, tileBoundsOnCanvas,
+ area, compositeImage, opacity, blendMode);
+ }
+ }
+ }
+ else {
+ renderImage(dst_image, cel_image, pal, celBounds,
+ area, compositeImage, opacity, blendMode);
+ }
}
void Render::renderImage(
diff --git a/src/render/render.h b/src/render/render.h
index fc397cfe3..9641f98bf 100644
--- a/src/render/render.h
+++ b/src/render/render.h
@@ -178,6 +178,7 @@ namespace render {
void renderCel(
Image* dst_image,
const Image* cel_image,
+ const Layer* cel_layer,
const Palette* pal,
const gfx::RectF& celBounds,
const gfx::Clip& area,