Add tilemap layers (#977)

This is the first commit with a simple tilemap editor. Still buggy but
functional in several ways. Several changes were made:

* NewLayer command can receive a tilemap=true to create a new tilemap
  layer
* New ToggleTilesMode command added to switch between the palette and
  the tileset in the ColorBar (the ColorBar was expanded to show
  colors or tilesets with a generic AbstractPaletteViewAdapter)
* All commands to create new layers were moved to Layer >
  New... submenu
* There are a new tileset chunk to save tilesets in .aseprite files,
  and a new kind of cels to save tilemaps
* Added doc::LayerTilemap, doc::Tileset, etc. and several other types
  to handle tilesets/tilemaps in the doc layer.
* Added doc::Grid class with grid specifications that indicates how a
  tilemap <-> tileset must be drawn
* Added and expanded cel operations to work with tilemaps and
  conversions between regular LayerImage cels <-> LayerTilemap cels
  (e.g. copy cels in the timeline between layer types)
This commit is contained in:
David Capello 2019-03-29 15:57:10 -03:00
parent 9fe05a5dd5
commit 26139c4ae2
124 changed files with 4007 additions and 506 deletions

View File

@ -138,6 +138,7 @@
<key command="Timeline" shortcut="Tab">
<param name="switch" value="true" />
</key>
<key command="ToggleTilesMode" shortcut="Shift+Tab" />
<key command="PaletteEditor" shortcut="A" />
<key command="PaletteEditor" shortcut="F4">
<param name="edit" value="switch" />
@ -807,6 +808,9 @@
<param name="reference" value="true" />
<param name="fromFile" value="true" />
</item>
<item command="NewLayer" text="@.layer_new_tilemap_layer">
<param name="tilemap" value="true" />
</item>
</menu>
<item command="RemoveLayer" text="@.layer_delete_layer" />
<item command="BackgroundFromLayer" text="@.layer_background_from_layer" />

View File

@ -195,6 +195,7 @@
</section>
<section id="color_bar">
<option id="box_size" type="int" default="11" />
<option id="tiles_box_size" type="int" default="16" />
<option id="fg_color" type="app::Color" default="app::Color::fromRgb(255, 255, 255)" />
<option id="bg_color" type="app::Color" default="app::Color::fromRgb(0, 0, 0)" />
<option id="selector" type="app::ColorBar::ColorSelector" default="app::ColorBar::ColorSelector::TINT_SHADE_TONE" />

View File

@ -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

View File

@ -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,

View File

@ -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

92
src/app/cmd/add_tile.cpp Normal file
View File

@ -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

56
src/app/cmd/add_tile.h Normal file
View File

@ -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 <sstream>
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

View File

@ -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

53
src/app/cmd/add_tileset.h Normal file
View File

@ -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 <sstream>
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

View File

@ -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<LayerImage*>(m_srcLayer.layer());
LayerImage* dstLayer = static_cast<LayerImage*>(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));
}

View File

@ -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;

View File

@ -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();
}

View File

@ -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

View File

@ -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);

View File

@ -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));

View File

@ -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

30
src/app/cmd/remove_tile.h Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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));
}

View File

@ -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) {

View File

@ -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<Tileset>(m_tilesetId);
}
} // namespace cmd
} // namespace app

View File

@ -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

View File

@ -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<Cmd*> m_cmds;
};

View File

@ -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<LayerImage*>(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);
}
}
}

View File

@ -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

View File

@ -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());

View File

@ -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<std::string> name { this, std::string(), "name" };
Param<bool> group { this, false, "group" };
Param<bool> reference { this, false, "reference" };
Param<bool> tilemap { this, false, "tilemap" };
Param<bool> ask { this, false, "ask" };
Param<bool> fromFile { this, false, { "fromFile", "from-file" } };
Param<bool> fromClipboard { this, false, "fromClipboard" };
@ -63,7 +66,7 @@ struct NewLayerParams : public NewParams {
class NewLayerCommand : public CommandWithNewParams<NewLayerParams> {
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";
}

View File

@ -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

View File

@ -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)

View File

@ -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<DocEvent&>(&DocObserver::onSelectionBoundariesChanged, ev);
}
void Doc::notifyTilesetChanged(Tileset* tileset)
{
DocEvent ev(this);
ev.tileset(tileset);
notify_observers<DocEvent&>(&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()));

View File

@ -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

View File

@ -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<Layer> newLayerPtr;
if (sourceLayer->isImage())
if (sourceLayer->isTilemap()) {
newLayerPtr.reset(new LayerTilemap(sourceLayer->sprite(),
static_cast<LayerTilemap*>(sourceLayer)->tilesetIndex()));
}
else if (sourceLayer->isImage())
newLayerPtr.reset(new LayerImage(sourceLayer->sprite()));
else if (sourceLayer->isGroup())
newLayerPtr.reset(new LayerGroup(sourceLayer->sprite()));

View File

@ -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);

View File

@ -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

View File

@ -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).

View File

@ -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() { }
};

View File

@ -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<int>(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<const LayerTilemap*>(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<w; ++x) {
void write_scanline(RgbTraits::address_t address, int w, uint8_t* buffer) {
for (int x=0; x<w; ++x, ++address) {
*(buffer++) = rgba_getr(*address);
*(buffer++) = rgba_getg(*address);
*(buffer++) = rgba_getb(*address);
*(buffer++) = rgba_geta(*address);
++address;
}
}
};
@ -643,12 +662,10 @@ public:
fputc(graya_getv(c), f);
fputc(graya_geta(c), f);
}
void write_scanline(GrayscaleTraits::address_t address, int w, uint8_t* buffer)
{
for (int x=0; x<w; ++x) {
void write_scanline(GrayscaleTraits::address_t address, int w, uint8_t* buffer) {
for (int x=0; x<w; ++x, ++address) {
*(buffer++) = graya_getv(*address);
*(buffer++) = graya_geta(*address);
++address;
}
}
};
@ -659,12 +676,27 @@ public:
void write_pixel(FILE* f, IndexedTraits::pixel_t c) {
fputc(c, f);
}
void write_scanline(IndexedTraits::address_t address, int w, uint8_t* buffer)
{
void write_scanline(IndexedTraits::address_t address, int w, uint8_t* buffer) {
memcpy(buffer, address, w);
}
};
template<>
class PixelIO<TilemapTraits> {
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<w; ++x, ++address) {
*(buffer++) = ((*address) & 0x000000ffl);
*(buffer++) = ((*address) & 0x0000ff00l) >> 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<typename ImageTraits>
static void write_compressed_image(FILE* f, const Image* image)
static void write_compressed_image_templ(FILE* f, const Image* image)
{
PixelIO<ImageTraits> 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<RgbTraits>(f, image);
break;
case IMAGE_GRAYSCALE:
write_compressed_image_templ<GrayscaleTraits>(f, image);
break;
case IMAGE_INDEXED:
write_compressed_image_templ<IndexedTraits>(f, image);
break;
case IMAGE_TILEMAP:
write_compressed_image_templ<TilemapTraits>(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<IndexedTraits>(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<RgbTraits>(f, image);
break;
case IMAGE_GRAYSCALE:
write_compressed_image<GrayscaleTraits>(f, image);
break;
case IMAGE_INDEXED:
write_compressed_image<IndexedTraits>(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; i<tileset->size(); ++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()) {

View File

@ -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)

View File

@ -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;

View File

@ -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) {

View File

@ -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);

View File

@ -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);

View File

@ -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<doc::Grid>(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<doc::Grid>(L, 1)->~Grid();
return 0;
}
int Grid_get_tileSize(lua_State* L)
{
auto grid = get_obj<doc::Grid>(L, 1);
push_obj(L, grid->tileSize());
return 1;
}
int Grid_get_origin(lua_State* L)
{
auto grid = get_obj<doc::Grid>(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

View File

@ -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<ImageObj>(L, image);
}
void push_tileset_image(lua_State* L, doc::Tileset* tileset, doc::Image* image)
{
push_new<ImageObj>(L, tileset, image);
}
doc::Image* may_get_image_from_arg(lua_State* L, int index)
{
auto obj = may_get_obj<ImageObj>(L, index);

View File

@ -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<Layer>(L, 1);
lua_pushboolean(L, layer->isTilemap());
return 1;
}
int Layer_get_isTransparent(lua_State* L)
{
auto layer = get_docobj<Layer>(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<Layer>(L, 1);
if (layer->isTilemap())
push_tileset(L, static_cast<doc::LayerTilemap*>(layer)->tileset());
else
lua_pushnil(L);
return 1;
}
int Layer_set_name(lua_State* L)
{
auto layer = get_docobj<Layer>(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<Layer>, UserData_set_color<Layer> },
{ "data", UserData_get_text<Layer>, UserData_set_text<Layer> },
{ "tileset", Layer_get_tileset, nullptr },
{ nullptr, nullptr, nullptr }
};

View File

@ -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 }
};

View File

@ -662,6 +662,13 @@ int Sprite_get_slices(lua_State* L)
return 1;
}
int Sprite_get_tilesets(lua_State* L)
{
auto sprite = get_docobj<Sprite>(L, 1);
push_tilesets(L, sprite->tilesets());
return 1;
}
int Sprite_get_backgroundLayer(lua_State* L)
{
auto sprite = get_docobj<Sprite>(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 },

View File

@ -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<Tileset>(L, 1);
const auto b = get_docobj<Tileset>(L, 2);
lua_pushboolean(L, a->id() == b->id());
return 1;
}
int Tileset_len(lua_State* L)
{
auto tileset = get_docobj<Tileset>(L, 1);
lua_pushinteger(L, tileset->size());
return 1;
}
int Tileset_getTile(lua_State* L)
{
auto tileset = get_docobj<Tileset>(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<Tileset>(L, 1);
lua_pushstring(L, tileset->name().c_str());
return 1;
}
int Tileset_set_name(lua_State* L)
{
auto tileset = get_docobj<Tileset>(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<Tileset>(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

View File

@ -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<Tilesets>(L, 1);
lua_pushinteger(L, obj->size());
return 1;
}
int Tilesets_index(lua_State* L)
{
auto obj = get_docobj<Tilesets>(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

View File

@ -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<LayerTilemap*>(m_layer)->tileset()->grid();
return gfx::Rect(grid.tileOffset() + cel->bounds().origin(),
grid.tileSize());
}
}
gfx::Rect bounds;
if (m_sprite) {
bounds = m_sprite->gridBounds();

View File

@ -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

24
src/app/tileset_mode.h Normal file
View File

@ -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

View File

@ -9,6 +9,7 @@
#define APP_TRANSACTION_H_INCLUDED
#pragma once
#include "app/cmd_transaction.h"
#include "app/doc_observer.h"
#include <string>
@ -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 {

View File

@ -73,6 +73,10 @@ namespace app {
return *m_transaction;
}
operator CmdTransaction*() {
return m_transaction->cmds();
}
private:
Doc* m_doc;
Transaction* m_transaction;

View File

@ -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<SkinTheme*>(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<void>(&ColorBar::onFixWarningClick, this, &m_bgColor, m_bgWarningIcon));
m_redrawTimer.Tick.connect(base::Bind<void>(&ColorBar::onTimerTick, this));
m_buttons.ItemChange.connect(base::Bind<void>(&ColorBar::onPaletteButtonClick, this));
m_tilesButtons.ItemChange.connect(base::Bind<void>(&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<SkinTheme*>(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<SkinTheme*>(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<const doc::LayerTilemap*>(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<TilesButton>(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<bool> 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

View File

@ -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<PalettePopup> 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<doc::Palette> 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;

View File

@ -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 =

View File

@ -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,

View File

@ -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;

View File

@ -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);

View File

@ -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();

View File

@ -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 <memory>
@ -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);
};

View File

@ -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);

View File

@ -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; i<palette.size(); ++i) {
if (!picks[i])
newPalette.setEntry(remap[i], palette.getEntry(i));
}
paletteView->setNewPalette(&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<LayerTilemap*>(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; i<palette->size(); ++i) {
for (int i=0; i<m_adapter->size(); ++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; i<palette.size(); ++i) {
if (!m_selectedEntries[i])
newPalette.setEntry(remap[i], palette.getEntry(i));
}
m_adapter->clearSelection(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; i<palette->size(); ++i) {
const int size = m_adapter->size();
for (int i=0; i<size; ++i) {
if (!m_selectedEntries[i])
continue;
@ -585,13 +780,13 @@ void PaletteView::onPaint(ui::PaintEvent& ev)
if (dragging) {
gfx::Rect box2 = getPaletteEntryBounds(k);
gfx::Color negColor;
drawEntry(g, i, k, box2, negColor);
m_adapter->drawEntry(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<std::string>(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<SkinTheme*>(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; i<palette->size(); ++i) {
for (int i=0; i<size; ++i) {
if (!m_selectedEntries[i])
continue;
const int max = palette->size();
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-1 && i+1 < max ? m_selectedEntries[i+1]: false);
bool top = (i >= 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)<m_columns-1 && i+1 < size ? m_selectedEntries[i+1]: false);
gfx::Rect box = getPaletteEntryBounds(i);
box.enlarge(outlineWidth);
@ -759,17 +953,18 @@ PaletteView::Hit PaletteView::hitTest(const gfx::Point& pos)
}
// Check if we are in the resize handle
if (getPaletteEntryBounds(palette->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<SkinTheme*>(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()

View File

@ -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 <memory>
#include <vector>
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<AbstractPaletteViewAdapter> m_adapter;
int m_columns;
double m_boxsize;
int m_currentEntry;

View File

@ -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);
}

540
src/app/util/cel_ops.cpp Normal file
View File

@ -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 <cmath>
#include <memory>
#define OPS_TRACE(...)
namespace app {
using namespace doc;
template<typename ImageTraits>
static void mask_image_templ(Image* image, const Image* bitmap)
{
LockImageBits<ImageTraits> bits1(image);
const LockImageBits<BitmapTraits> bits2(bitmap);
typename LockImageBits<ImageTraits>::iterator it1, end1;
LockImageBits<BitmapTraits>::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<RgbTraits>(image, bitmap);
case IMAGE_GRAYSCALE: return mask_image_templ<GrayscaleTraits>(image, bitmap);
case IMAGE_INDEXED: return mask_image_templ<IndexedTraits>(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<doc::LayerTilemap*>(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<doc::LayerTilemap*>(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<Cel> 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<doc::LayerTilemap*>(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<doc::LayerTilemap*>(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<Cel> 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<Doc*>(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<doc::LayerTilemap*>(cel->layer());
Doc* doc = static_cast<Doc*>(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<Doc*>(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

67
src/app/util/cel_ops.h Normal file
View File

@ -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 <functional>
namespace doc {
class Cel;
class Layer;
class LayerTilemap;
class Sprite;
}
namespace app {
class CmdSequence;
typedef std::function<doc::ImageRef(const doc::ImageRef& origTile,
const gfx::Rect& tileBoundsInCanvas)> 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

View File

@ -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<Cel>(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();

View File

@ -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 <cmath>
#include <memory>
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<Cel> 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<Cel> 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

View File

@ -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

View File

@ -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<LayerImage*>(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<LayerImage*>(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<doc::LayerTilemap*>(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()));
}

View File

@ -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

View File

@ -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(),

View File

@ -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

View File

@ -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

View File

@ -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<typename ImageTraits>
void read_raw_image(FileInterface* f,
DecodeDelegate* delegate,
doc::Image* image,
AsepriteHeader* header)
const AsepriteHeader* header)
{
PixelIO<ImageTraits> pixel_io;
int x, y;
@ -505,11 +512,11 @@ void read_raw_image(FileInterface* f,
//////////////////////////////////////////////////////////////////////
template<typename ImageTraits>
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<ImageTraits> 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<doc::RgbTraits>(
f, delegate, image, header, chunk_end);
break;
case doc::IMAGE_GRAYSCALE:
read_compressed_image_templ<doc::GrayscaleTraits>(
f, delegate, image, header, chunk_end);
break;
case doc::IMAGE_INDEXED:
read_compressed_image_templ<doc::IndexedTraits>(
f, delegate, image, header, chunk_end);
break;
case doc::IMAGE_TILEMAP:
read_compressed_image_templ<doc::TilemapTraits>(
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<doc::RgbTraits>(
f(), delegate(), image.get(), chunk_end, header);
break;
case doc::IMAGE_GRAYSCALE:
read_compressed_image<doc::GrayscaleTraits>(
f(), delegate(), image.get(), chunk_end, header);
break;
case doc::IMAGE_INDEXED:
read_compressed_image<doc::IndexedTraits>(
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; i<ntiles; ++i) {
read32(); // Flags (ignore)
const size_t dataSize = read32(); // Size of compressed data
const size_t dataBeg = f()->tell();
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

View File

@ -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

View File

@ -46,22 +46,21 @@ public:
}
void read_scanline(doc::RgbTraits::address_t address,
int w, uint8_t* buffer) {
for (int x=0; x<w; ++x) {
for (int x=0; x<w; ++x, ++address) {
r = *(buffer++);
g = *(buffer++);
b = *(buffer++);
a = *(buffer++);
*(address++) = doc::rgba(r, g, b, a);
*address = doc::rgba(r, g, b, a);
}
}
void write_scanline(doc::RgbTraits::address_t address,
int w, uint8_t* buffer) {
for (int x=0; x<w; ++x) {
for (int x=0; x<w; ++x, ++address) {
*(buffer++) = doc::rgba_getr(*address);
*(buffer++) = doc::rgba_getg(*address);
*(buffer++) = doc::rgba_getb(*address);
*(buffer++) = doc::rgba_geta(*address);
++address;
}
}
};
@ -82,19 +81,18 @@ public:
void read_scanline(doc::GrayscaleTraits::address_t address,
int w, uint8_t* buffer)
{
for (int x=0; x<w; ++x) {
for (int x=0; x<w; ++x, ++address) {
k = *(buffer++);
a = *(buffer++);
*(address++) = doc::graya(k, a);
*address = doc::graya(k, a);
}
}
void write_scanline(doc::GrayscaleTraits::address_t address,
int w, uint8_t* buffer)
{
for (int x=0; x<w; ++x) {
for (int x=0; x<w; ++x, ++address) {
*(buffer++) = doc::graya_getv(*address);
*(buffer++) = doc::graya_geta(*address);
++address;
}
}
};
@ -118,6 +116,35 @@ public:
}
};
template<>
class PixelIO<doc::TilemapTraits> {
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; x<w; ++x, ++address) {
b1 = *(buffer++);
b2 = *(buffer++);
b3 = *(buffer++);
b4 = *(buffer++);
*address = ((b4 << 24) | (b3 << 16) | (b2 << 8) | b1);
}
}
};
} // namespace dio
#endif

View File

@ -1,5 +1,6 @@
# Aseprite Document Library
# Copyright (C) 2001-2018 David Capello
# Copyright (C) 2019 Igara Studio S.A.
# Copyright (C) 2001-2018 David Capello
if(WIN32)
add_definitions(-D_CRT_SECURE_NO_WARNINGS)
@ -36,6 +37,8 @@ add_library(doc-lib
file/gpl_file.cpp
file/hex_file.cpp
file/pal_file.cpp
grid.cpp
grid_io.cpp
handle_anidir.cpp
image.cpp
image_impl.cpp
@ -43,6 +46,7 @@ add_library(doc-lib
layer.cpp
layer_io.cpp
layer_list.cpp
layer_tilemap.cpp
mask.cpp
mask_boundaries.cpp
mask_io.cpp
@ -66,6 +70,9 @@ add_library(doc-lib
tag.cpp
tag_io.cpp
tags.cpp
tileset.cpp
tileset_io.cpp
tilesets.cpp
user_data_io.cpp)
# TODO Remove 'os' as dependency and move conversion_to_surface.cpp/h files

View File

@ -35,6 +35,7 @@ void fill_selection(Image* image,
for (int u=0; u<maskBounds.w; ++u, ++it) {
ASSERT(it != maskBits.end());
if (*it) {
// TODO use iterators
put_pixel(image,
u + offset.x,
v + offset.y, color);

View File

@ -106,7 +106,10 @@ ImageRef shift_image_with_mask(const Cel* cel,
// Bounds and Image shrinking (we have to fit compound image (compImage) and bounds (compCelBounds))
gfx::Rect newBounds = compImage->bounds();
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());
}

View File

@ -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 <thread>
@ -47,6 +53,12 @@ bool is_same_pixel<IndexedTraits>(color_t pixel1, color_t pixel2)
return pixel1 == pixel2;
}
template<>
bool is_same_pixel<BitmapTraits>(color_t pixel1, color_t pixel2)
{
return pixel1 == pixel2;
}
template<typename ImageTraits>
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<const LayerTilemap*>(layer)->tileset();
bool shrink;
int u, v;
// Shrink left side
for (u=bounds.x; u<bounds.x+bounds.w; ++u) {
shrink = true;
for (v=bounds.y; v<bounds.y+bounds.h; ++v) {
const tile_t tile = get_pixel_fast<TilemapTraits>(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<bounds.y+bounds.h; ++v) {
const tile_t tile = get_pixel_fast<TilemapTraits>(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<bounds.y+bounds.h; ++v) {
shrink = true;
for (u=bounds.x; u<bounds.x+bounds.w; ++u) {
const tile_t tile = get_pixel_fast<TilemapTraits>(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<bounds.x+bounds.w; ++u) {
const tile_t tile = get_pixel_fast<TilemapTraits>(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<RgbTraits>(image, bounds, refpixel);
case IMAGE_GRAYSCALE: return shrink_bounds_templ<GrayscaleTraits>(image, bounds, refpixel);
case IMAGE_INDEXED: return shrink_bounds_templ<IndexedTraits>(image, bounds, refpixel);
case IMAGE_BITMAP:
// Not supported
break;
case IMAGE_BITMAP: return shrink_bounds_templ<BitmapTraits>(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<doc::LayerTilemap*>(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<RgbTraits>(a, b, bounds);
case IMAGE_GRAYSCALE: return shrink_bounds_templ2<GrayscaleTraits>(a, b, bounds);
case IMAGE_INDEXED: return shrink_bounds_templ2<IndexedTraits>(a, b, bounds);
case IMAGE_BITMAP: return shrink_bounds_templ2<BitmapTraits>(a, b, bounds);
// case IMAGE_TILEMAP: return shrink_bounds_templ2<TilemapTraits>(a, b, bounds);
}
ASSERT(false);
return false;

View File

@ -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

View File

@ -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

View File

@ -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<LayerTilemap*>(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

View File

@ -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<Image*>(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;

View File

@ -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.

View File

@ -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

View File

@ -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

163
src/doc/grid.cpp Normal file
View File

@ -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 <algorithm>
#include <cmath>
#include <limits>
#include <vector>
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<int>::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<gfx::Point> Grid::tilesInCanvasRegion(const gfx::Region& rgn) const
{
std::vector<gfx::Point> 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<BitmapTraits> 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

76
src/doc/grid.h Normal file
View File

@ -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<gfx::Point> 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

84
src/doc/grid_io.cpp Normal file
View File

@ -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 <iostream>
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

22
src/doc/grid_io.h Normal file
View File

@ -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 <iosfwd>
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

86
src/doc/grid_tests.cpp Normal file
View File

@ -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 <gtest/gtest.h>
#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();
}

View File

@ -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<GrayscaleTraits>(spec, buffer);
case ColorMode::INDEXED: return new ImageImpl<IndexedTraits>(spec, buffer);
case ColorMode::BITMAP: return new ImageImpl<BitmapTraits>(spec, buffer);
case ColorMode::TILEMAP: return new ImageImpl<TilemapTraits>(spec, buffer);
}
return nullptr;
}

View File

@ -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;
}

Some files were not shown because too many files have changed in this diff Show More