This commit is contained in:
David Capello 2025-09-30 23:57:43 -05:00 committed by GitHub
commit 5cbcb347b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
74 changed files with 1657 additions and 847 deletions

View File

@ -906,6 +906,11 @@
<param name="tilemap" value="true" />
<param name="ask" value="true" />
</item>
<separator />
<item command="NewLayer" text="@.layer_new_audio_layer">
<param name="type" value="audio" />
<param name="ask" value="true" />
</item>
</menu>
<item command="RemoveLayer" text="@.layer_delete_layer" group="layer_remove" />
<menu text="@.layer_convert_to">

View File

@ -365,6 +365,14 @@ NewLayer_Layer = Layer
NewLayer_Group = Group
NewLayer_ReferenceLayer = Reference Layer
NewLayer_TilemapLayer = Tilemap
NewLayer_FillLayer = Fill
NewLayer_MaskLayer = Mask
NewLayer_FxLayer = Fx
NewLayer_TextLayer = Text
NewLayer_VectorLayer = Vector
NewLayer_AudioLayer = Audio
NewLayer_SubspriteLayer = Subsprite
NewLayer_HitboxLayer = Hitbox
NewLayer_FromClipboard = {} from Clipboard
NewLayer_ViaCopy = {} via Copy
NewLayer_ViaCut = {} via Cut
@ -1149,6 +1157,7 @@ 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_new_audio_layer = New Audio Layer
layer_delete_layer = Delete Laye&r
layer_convert_to = Conv&ert To...
layer_convert_to_background = &Background

View File

@ -1,5 +1,5 @@
<!-- Aseprite -->
<!-- Copyright (C) 2020 by Igara Studio S.A. -->
<!-- Copyright (C) 2020-2025 by Igara Studio S.A. -->
<!-- Copyright (C) 2001-2016 by David Capello -->
<gui>
<window id="layer_properties" text="@.title">
@ -9,11 +9,11 @@
<entry text="" id="name" magnet="true" maxsize="256" minwidth="64" cell_align="horizontal" />
<button id="user_data" icon="icon_user_data" tooltip="@general.user_data" />
<label text="@.mode" for="mode" />
<label id="mode_label" text="@.mode" for="mode" />
<combobox id="mode" />
<button id="tileset" icon="tiles" tooltip="@.tileset_tooltip" />
<label text="@.opacity" for="opacity" />
<label id="opacity_label" text="@.opacity" for="opacity" />
<opacityslider id="opacity" width="128" cell_align="horizontal" cell_hspan="2" />
<label id="uuid_label" text="@.uuid" visible="false" for="uuid" />

36
docs/new-layer-type.md Normal file
View File

@ -0,0 +1,36 @@
# New Layer Type
This is a little hacking guide about how to add a new kind of layer to Aseprite:
1. The first step is to add a new [`doc::ObjectType` value](../src/doc/object_type.h)
for the new kind of layer
1. Create a new `LayerNAME` class derived from `Layer` or `LayerImage` (e.g. [`LayerAudio`](../src/doc/layer_audio.h))
1. Add this new `LayerNAME` [in `Layer::Layer()`'s `ASSERT()`](../src/doc/layer.cpp)
1. Expand [the `type` parameter in `NewLayerParams` for
`NewLayerCommand`](../src/app/commands/cmd_new_layer.cpp) and
`NewLayerCommand::Type` for `LayerNAME`
1. Add the necessary code in `NewLayerCommand::onExecute()` to create
this new kind of layer with a transaction
1. Complete `NewLayerCommand::layerPrefix()` function (which is used
in `NewLayerCommand::onGetFriendlyName()`) to get a proper text for
this command when it creates this new kind of layer, you will need
some [new `NewLayer_*Layer` string in
`en.ini`](../data/strings/en.ini)
1. Add a [new menu option in `gui.xml`](../data/gui.xml) in `<menu
text="@.layer" id="layer_menu">` for a new *Layer > New* menu,
you will need some [new `layer_new_*_layer` string in
`en.ini`](../data/strings/en.ini) in the `[main_menu]` section
1. Add the serialization of the layer specifics in
[`write_layer`/`read_layer` functions](../src/doc/layer_io.cpp),
this code is used for [undo/redo](../src/app/cmd/add_layer.cpp)
1. Add the serialization for [data recovery](https://www.aseprite.org/docs/data-recovery/)
purposes of the layer specifics in [Writer::writeLayerStructure()](../src/app/crash/write_document.cpp)
and [Reader::readLayer()](../src/app/crash/read_document.cpp) functions
1. To make `DuplicateLayerCommand` work, add the new `doc::ObjectType`
in `app::DocApi::copyLayerWithSprite()` and `app::Doc::copyLayerContent()`
# Layer Properties
Customize the `LayerPropertiesCommand` dialog:
1. Hide widgets that don't make sense for this layer type

View File

@ -315,6 +315,7 @@ target_sources(app-lib PRIVATE
cmd/set_cel_bounds.cpp
cmd/set_cel_data.cpp
cmd/set_cel_frame.cpp
cmd/set_cel_image.cpp
cmd/set_cel_opacity.cpp
cmd/set_cel_position.cpp
cmd/set_cel_zindex.cpp

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -112,7 +112,7 @@ std::string convert_filter_to_layer_path_if_possible(const Sprite* sprite,
}
}
if (layer->isGroup()) {
for (auto child : static_cast<const LayerGroup*>(layer)->layers())
for (Layer* child : layer->layers())
layers.push(child);
}
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2016-2018 David Capello
//
// This program is distributed under the terms of
@ -238,15 +238,14 @@ void PreviewCliDelegate::showLayersFilter(const CliOpenFile& cof)
}
}
void PreviewCliDelegate::showLayerVisibility(const doc::LayerGroup* group,
const std::string& indent)
void PreviewCliDelegate::showLayerVisibility(const doc::Layer* group, const std::string& indent)
{
for (auto layer : group->layers()) {
if (!layer->isVisible())
continue;
std::cout << indent << "- " << layer->name() << "\n";
if (layer->isGroup())
showLayerVisibility(static_cast<const LayerGroup*>(layer), indent + " ");
showLayerVisibility(layer, indent + " ");
}
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2016-2018 David Capello
//
// This program is distributed under the terms of
@ -14,7 +14,7 @@
#include <string>
namespace doc {
class LayerGroup;
class Layer;
}
namespace app {
@ -37,7 +37,7 @@ public:
private:
void showLayersFilter(const CliOpenFile& cof);
void showLayerVisibility(const doc::LayerGroup* group, const std::string& indent);
void showLayerVisibility(const doc::Layer* group, const std::string& indent);
};
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2020-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -49,10 +49,12 @@ void AddCel::onUndo()
ASSERT(cel);
// Save the CelData only if the cel isn't linked
bool has_data = (cel->links() == 0);
write8(m_stream, has_data ? 1 : 0);
const bool has_data = (cel->links() == 0);
const bool has_image = (cel->imageId() != NullId);
write8(m_stream, (has_data ? 1 : 0) | (has_image ? 2 : 0));
if (has_data) {
write_image(m_stream, cel->image());
if (has_image)
write_image(m_stream, cel->image());
write_celdata(m_stream, cel->data());
}
write_cel(m_stream, cel);
@ -67,10 +69,14 @@ void AddCel::onRedo()
ASSERT(layer);
SubObjectsFromSprite io(layer->sprite());
bool has_data = (read8(m_stream) != 0);
const uint8_t flags = read8(m_stream);
const bool has_data = (flags & 1 ? true : false);
const bool has_image = (flags & 2 ? true : false);
if (has_data) {
ImageRef image(read_image(m_stream));
io.addImageRef(image);
if (has_image) {
ImageRef image(read_image(m_stream));
io.addImageRef(image);
}
CelDataRef celdata(read_celdata(m_stream, &io));
io.addCelDataRef(celdata);
@ -87,7 +93,7 @@ void AddCel::onRedo()
void AddCel::addCel(Layer* layer, Cel* cel)
{
static_cast<LayerImage*>(layer)->addCel(cel);
layer->addCel(cel);
layer->incrementVersion();
Doc* doc = static_cast<Doc*>(cel->document());
@ -107,7 +113,7 @@ void AddCel::removeCel(Layer* layer, Cel* cel)
ev.cel(cel);
doc->notify_observers<DocEvent&>(&DocObserver::onBeforeRemoveCel, ev);
static_cast<LayerImage*>(layer)->removeCel(cel);
layer->removeCel(cel);
layer->incrementVersion();
doc->notify_observers<DocEvent&>(&DocObserver::onAfterRemoveCel, ev);

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -64,7 +65,7 @@ void AddLayer::onRedo()
void AddLayer::addLayer(Layer* group, Layer* newLayer, Layer* afterThis)
{
static_cast<LayerGroup*>(group)->insertLayer(newLayer, afterThis);
group->insertLayer(newLayer, afterThis);
group->incrementVersion();
group->sprite()->incrementVersion();
@ -83,7 +84,7 @@ void AddLayer::removeLayer(Layer* group, Layer* layer)
ev.layer(layer);
doc->notify_observers<DocEvent&>(&DocObserver::onBeforeRemoveLayer, ev);
static_cast<LayerGroup*>(group)->removeLayer(layer);
group->removeLayer(layer);
group->incrementVersion();
group->sprite()->incrementVersion();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -86,7 +86,7 @@ void convert_color_profile(doc::Sprite* sprite, const gfx::ColorSpaceRef& newCS)
if (sprite->pixelFormat() != doc::IMAGE_INDEXED) {
for (Cel* cel : sprite->uniqueCels()) {
ImageRef old_image = cel->imageRef();
if (old_image.get()->pixelFormat() != IMAGE_TILEMAP) {
if (old_image && old_image->pixelFormat() != IMAGE_TILEMAP) {
ImageRef new_image = convert_image_color_space(old_image.get(), newCS, conversion.get());
sprite->replaceImage(old_image->id(), new_image);
@ -177,7 +177,7 @@ ConvertColorProfile::ConvertColorProfile(doc::Sprite* sprite, const gfx::ColorSp
if (sprite->pixelFormat() != doc::IMAGE_INDEXED) {
for (Cel* cel : sprite->uniqueCels()) {
ImageRef old_image = cel->imageRef();
if (old_image.get()->pixelFormat() != IMAGE_TILEMAP) {
if (old_image && old_image->pixelFormat() != IMAGE_TILEMAP) {
ImageRef new_image = convert_image_color_space(old_image.get(), newCS, conversion.get());
m_seq.add(new cmd::ReplaceImage(sprite, old_image, new_image));

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This program is distributed under the terms of
@ -33,9 +33,9 @@ namespace app { namespace cmd {
using namespace doc;
MoveCel::MoveCel(LayerImage* srcLayer,
MoveCel::MoveCel(Layer* srcLayer,
frame_t srcFrame,
LayerImage* dstLayer,
Layer* dstLayer,
frame_t dstFrame,
bool continuous)
: m_srcLayer(srcLayer)
@ -48,9 +48,8 @@ MoveCel::MoveCel(LayerImage* srcLayer,
void MoveCel::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);

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2001-2015 David Capello
//
// This program is distributed under the terms of
@ -13,19 +14,14 @@
#include "doc/color.h"
#include "doc/frame.h"
namespace doc {
class LayerImage;
}
namespace app { namespace cmd {
using namespace doc;
class MoveCel : public CmdSequence {
public:
MoveCel(LayerImage* srcLayer,
frame_t srcFrame,
LayerImage* dstLayer,
frame_t dstFrame,
MoveCel(doc::Layer* srcLayer,
doc::frame_t srcFrame,
doc::Layer* dstLayer,
doc::frame_t dstFrame,
bool continuous);
protected:
@ -34,7 +30,7 @@ protected:
private:
WithLayer m_srcLayer, m_dstLayer;
frame_t m_srcFrame, m_dstFrame;
doc::frame_t m_srcFrame, m_dstFrame;
bool m_continuous;
};

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -32,8 +33,8 @@ void MoveLayer::onExecute()
{
Layer* layer = m_layer.layer();
Layer* afterThis = m_newAfterThis.layer();
LayerGroup* oldParent = static_cast<LayerGroup*>(m_oldParent.layer());
LayerGroup* newParent = static_cast<LayerGroup*>(m_newParent.layer());
Layer* oldParent = m_oldParent.layer();
Layer* newParent = m_newParent.layer();
ASSERT(layer);
ASSERT(oldParent);
ASSERT(newParent);
@ -61,8 +62,8 @@ void MoveLayer::onUndo()
{
Layer* layer = m_layer.layer();
Layer* afterThis = m_oldAfterThis.layer();
LayerGroup* oldParent = static_cast<LayerGroup*>(m_oldParent.layer());
LayerGroup* newParent = static_cast<LayerGroup*>(m_newParent.layer());
Layer* oldParent = m_oldParent.layer();
Layer* newParent = m_newParent.layer();
ASSERT(layer);
ASSERT(oldParent);
ASSERT(newParent);

View File

@ -0,0 +1,89 @@
// Aseprite
// Copyright (C) 2025 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/set_cel_image.h"
#include "doc/cel.h"
#include "doc/sprite.h"
namespace app { namespace cmd {
using namespace doc;
SetCelImage::SetCelImage(Cel* cel, const ImageRef& newImage)
: WithCel(cel)
, m_oldImageId(cel->imageId())
, m_newImageId(newImage ? newImage->id() : NullId)
, m_newImage(newImage)
{
}
void SetCelImage::onExecute()
{
Cel* cel = this->cel();
Sprite* sprite = cel->sprite();
// Save old image in m_copy. We cannot keep an ImageRef to this
// image, because there are other undo branches that could try to
// modify/re-add this same image ID
if (m_oldImageId) {
ImageRef oldImage = sprite->getImageRef(m_oldImageId);
ASSERT(oldImage);
m_copy.reset(Image::createCopy(oldImage.get()));
}
cel->data()->setImage(m_newImage, cel->layer());
cel->data()->incrementVersion();
m_newImage.reset();
}
void SetCelImage::onUndo()
{
Cel* cel = this->cel();
Sprite* sprite = cel->sprite();
ImageRef newImage;
if (m_newImageId) {
newImage = sprite->getImageRef(m_newImageId);
ASSERT(newImage);
}
if (m_oldImageId) {
ASSERT(!sprite->getImageRef(m_oldImageId));
m_copy->setId(m_oldImageId);
}
cel->data()->setImage(m_copy, cel->layer());
m_copy.reset(newImage ? Image::createCopy(newImage.get()) : nullptr);
}
void SetCelImage::onRedo()
{
Cel* cel = this->cel();
Sprite* sprite = cel->sprite();
ImageRef oldImage;
if (m_oldImageId)
oldImage = sprite->getImageRef(m_oldImageId);
if (m_newImageId) {
ASSERT(!sprite->getImageRef(m_newImageId));
m_copy->setId(m_newImageId);
cel->data()->setImage(m_copy, cel->layer());
}
else {
cel->data()->setImage(nullptr, nullptr);
}
cel->data()->incrementVersion();
m_copy.reset(oldImage ? Image::createCopy(oldImage.get()) : nullptr);
}
}} // namespace app::cmd

View File

@ -0,0 +1,43 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_CMD_SET_CEL_IMAGE_H_INCLUDED
#define APP_CMD_SET_CEL_IMAGE_H_INCLUDED
#pragma once
#include "app/cmd.h"
#include "app/cmd/with_cel.h"
#include "doc/image_ref.h"
#include <sstream>
namespace app { namespace cmd {
class SetCelImage : public Cmd,
public WithCel {
public:
SetCelImage(doc::Cel* cel, const doc::ImageRef& newImage);
protected:
void onExecute() override;
void onUndo() override;
void onRedo() override;
size_t onMemSize() const override { return sizeof(*this); }
private:
doc::ObjectId m_oldImageId;
doc::ObjectId m_newImageId;
// Reference used only to keep the copy of the new image from the
// SetCelImage() ctor until the SetCelImage::onExecute() call. Then
// the reference is not used anymore.
doc::ImageRef m_newImage;
doc::ImageRef m_copy;
};
}} // namespace app::cmd
#endif

View File

@ -390,8 +390,8 @@ CelPropertiesCommand::CelPropertiesCommand() : Command(CommandId::CelProperties(
bool CelPropertiesCommand::onEnabled(Context* context)
{
return context->isUIAvailable() && context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
ContextFlags::ActiveLayerIsImage);
return context->isUIAvailable() &&
context->checkFlags(ContextFlags::ActiveDocumentIsWritable | ContextFlags::HasActiveCel);
}
void CelPropertiesCommand::onExecute(Context* context)

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -51,9 +51,6 @@ void ClearCelCommand::onExecute(Context* context)
const Site& site = writer.site();
if (site.inTimeline() && !site.selectedLayers().empty() && !site.selectedFrames().empty()) {
for (Layer* layer : site.selectedLayers()) {
if (!layer->isImage())
continue;
if (!layer->isEditableHierarchy()) {
nonEditableLayers = true;
continue;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -146,8 +146,8 @@ void destroy_doc(Context* ctx, Doc* doc)
void insert_layers_to_selected_layers(Layer* layer, SelectedLayers& selectedLayers)
{
if (layer->isGroup()) {
auto children = static_cast<LayerGroup*>(layer)->layers();
for (auto child : children)
auto children = layer->layers();
for (Layer* child : children)
insert_layers_to_selected_layers(child, selectedLayers);
}
else

View File

@ -377,6 +377,8 @@ private:
return;
auto tilemap = static_cast<LayerTilemap*>(m_layer);
expandWindow(gfx::Size(bounds().w, sizeHint().h));
auto tileset = tilemap->tileset();
// Information about the tileset to be used for new tilemaps
@ -442,8 +444,11 @@ private:
name()->setText(m_layer->name().c_str());
name()->setEnabled(true);
if (m_layer->isImage() ||
(m_layer->isGroup() && Preferences::instance().experimental.composeGroups())) {
const bool imageProps =
((m_layer->isImage() ||
(m_layer->isGroup() && Preferences::instance().experimental.composeGroups())));
if (imageProps) {
mode()->setSelectedItem(nullptr);
for (auto item : *mode()) {
if (auto blendModeItem = dynamic_cast<BlendModeItem*>(item)) {
@ -457,10 +462,11 @@ private:
opacity()->setValue(m_layer->opacity());
opacity()->setEnabled(!m_layer->isBackground());
}
else {
mode()->setEnabled(false);
opacity()->setEnabled(false);
}
modeLabel()->setVisible(imageProps);
mode()->setVisible(imageProps);
opacityLabel()->setVisible(imageProps);
opacity()->setVisible(imageProps);
color_t c = m_layer->userData().color();
m_userDataView.color()->setColor(
@ -486,6 +492,8 @@ private:
tileset()->setVisible(tilemapVisibility);
tileset()->parent()->layout();
}
expandWindow(gfx::Size(bounds().w, sizeHint().h));
}
Timer m_timer;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -123,15 +123,9 @@ void NewFrameCommand::onExecute(Context* context)
site.selectedFrames().firstFrame() + 1);
for (Layer* layer : selLayers) {
if (layer->isImage()) {
for (frame_t srcFrame : site.selectedFrames().reversed()) {
frame_t dstFrame = srcFrame + frameRange;
api.copyCel(static_cast<LayerImage*>(layer),
srcFrame,
static_cast<LayerImage*>(layer),
dstFrame,
continuous.get());
}
for (frame_t srcFrame : site.selectedFrames().reversed()) {
frame_t dstFrame = srcFrame + frameRange;
api.copyCel(layer, srcFrame, layer, dstFrame, continuous.get());
}
}
@ -139,7 +133,7 @@ void NewFrameCommand::onExecute(Context* context)
if (timeline)
timeline->moveRange(range);
}
else if (auto layer = static_cast<LayerImage*>(writer.layer())) {
else if (Layer* layer = writer.layer()) {
api.copyCel(layer, writer.frame(), layer, writer.frame() + 1, continuous.get());
context->setActiveFrame(writer.frame() + 1);

View File

@ -33,7 +33,15 @@
#include "app/util/clipboard.h"
#include "app/util/new_image_from_mask.h"
#include "doc/layer.h"
#include "doc/layer_audio.h"
#include "doc/layer_fill.h"
#include "doc/layer_fx.h"
#include "doc/layer_hitbox.h"
#include "doc/layer_mask.h"
#include "doc/layer_subsprite.h"
#include "doc/layer_text.h"
#include "doc/layer_tilemap.h"
#include "doc/layer_vector.h"
#include "doc/primitives.h"
#include "doc/sprite.h"
#include "fmt/format.h"
@ -58,6 +66,8 @@ struct NewLayerParams : public NewParams {
Param<bool> group{ this, false, "group" };
Param<bool> reference{ this, false, "reference" };
Param<bool> tilemap{ this, false, "tilemap" };
// Alternative for group/reference/tilemap params and used for future layer types
Param<std::string> type{ this, {}, "type" };
Param<gfx::Rect> gridBounds{ this, gfx::Rect(), "gridBounds" };
Param<bool> ask{ this, false, "ask" };
Param<bool> fromFile{
@ -74,7 +84,20 @@ struct NewLayerParams : public NewParams {
class NewLayerCommand : public CommandWithNewParams<NewLayerParams> {
public:
enum class Type { Layer, Group, ReferenceLayer, TilemapLayer };
enum class Type {
Layer,
Group,
ReferenceLayer,
TilemapLayer,
FillLayer,
MaskLayer,
FxLayer,
TextLayer,
VectorLayer,
AudioLayer,
SubspriteLayer,
HitboxLayer
};
enum class Place { AfterActiveLayer, BeforeActiveLayer, Top };
NewLayerCommand();
@ -105,12 +128,30 @@ void NewLayerCommand::onLoadParams(const Params& commandParams)
CommandWithNewParams<NewLayerParams>::onLoadParams(commandParams);
m_type = Type::Layer;
if (params().group())
if (params().group() || params().type() == "group")
m_type = Type::Group;
else if (params().reference())
else if (params().reference() || params().type() == "reference")
m_type = Type::ReferenceLayer;
else if (params().tilemap())
else if (params().tilemap() || params().type() == "tilemap")
m_type = Type::TilemapLayer;
#ifdef ENABLE_DEVMODE // TODO not yet production-ready
else if (params().type() == "fill")
m_type = Type::FillLayer;
else if (params().type() == "mask")
m_type = Type::MaskLayer;
else if (params().type() == "fx")
m_type = Type::FxLayer;
else if (params().type() == "text")
m_type = Type::TextLayer;
else if (params().type() == "vector")
m_type = Type::VectorLayer;
else if (params().type() == "audio")
m_type = Type::AudioLayer;
else if (params().type() == "subsprite")
m_type = Type::SubspriteLayer;
else if (params().type() == "hitbox")
m_type = Type::HitboxLayer;
#endif // ENABLE_DEVMODE
else
m_type = Type::Layer;
@ -244,12 +285,12 @@ void NewLayerCommand::onExecute(Context* context)
}
}
LayerGroup* parent = sprite->root();
Layer* parent = sprite->root();
Layer* activeLayer = reader.layer();
SelectedLayers selLayers = site.selectedLayers();
if (activeLayer) {
if (activeLayer->isGroup() && activeLayer->isExpanded() && m_type != Type::Group) {
parent = static_cast<LayerGroup*>(activeLayer);
parent = activeLayer;
activeLayer = nullptr;
}
else {
@ -293,6 +334,70 @@ void NewLayerCommand::onExecute(Context* context)
layer = api.newTilemapAfter(parent, name, tsi, activeLayer);
break;
}
case Type::FillLayer: {
layer = new LayerFill(parent->sprite());
layer->setName(name);
api.addLayer(parent, layer, parent->lastLayer());
break;
}
case Type::MaskLayer: {
layer = new LayerMask(parent->sprite());
layer->setName(name);
api.addLayer(parent, layer, parent->lastLayer());
break;
}
case Type::FxLayer: {
layer = new LayerFx(parent->sprite());
layer->setName(name);
api.addLayer(parent, layer, parent->lastLayer());
break;
}
case Type::TextLayer: {
layer = new LayerText(parent->sprite());
layer->setName(name);
api.addLayer(parent, layer, parent->lastLayer());
break;
}
case Type::VectorLayer: {
layer = new LayerVector(parent->sprite());
layer->setName(name);
api.addLayer(parent, layer, parent->lastLayer());
break;
}
case Type::AudioLayer: {
layer = new LayerAudio(parent->sprite());
layer->setName(name);
api.addLayer(parent, layer, parent->lastLayer());
break;
}
case Type::SubspriteLayer: {
layer = new LayerSubsprite(parent->sprite());
layer->setName(name);
api.addLayer(parent, layer, parent->lastLayer());
break;
}
case Type::HitboxLayer: {
layer = new LayerHitbox(parent->sprite());
layer->setName(name);
api.addLayer(parent, layer, parent->lastLayer());
break;
}
}
ASSERT(layer);
@ -324,7 +429,7 @@ void NewLayerCommand::onExecute(Context* context)
// Put all selected layers inside the group
if (m_type == Type::Group && site.inTimeline()) {
LayerGroup* commonParent = nullptr;
Layer* commonParent = nullptr;
layer_t sameParents = 0;
for (Layer* l : selLayers) {
if (!commonParent || commonParent == l->parent()) {
@ -335,7 +440,7 @@ void NewLayerCommand::onExecute(Context* context)
if (sameParents == selLayers.size()) {
for (Layer* newChild : selLayers.toBrowsableLayerList()) {
tx(new cmd::MoveLayer(newChild, layer, static_cast<LayerGroup*>(layer)->lastLayer()));
tx(new cmd::MoveLayer(newChild, layer, layer->lastLayer()));
}
}
}
@ -518,7 +623,7 @@ int NewLayerCommand::getMaxLayerNum(const Layer* layer) const
max = std::strtol(layer->name().c_str() + prefix.size(), NULL, 10);
if (layer->isGroup()) {
for (const Layer* child : static_cast<const LayerGroup*>(layer)->layers()) {
for (const Layer* child : layer->layers()) {
int tmp = getMaxLayerNum(child);
max = std::max(tmp, max);
}
@ -534,6 +639,14 @@ std::string NewLayerCommand::layerPrefix() const
case Type::Group: return Strings::commands_NewLayer_Group();
case Type::ReferenceLayer: return Strings::commands_NewLayer_ReferenceLayer();
case Type::TilemapLayer: return Strings::commands_NewLayer_TilemapLayer();
case Type::FillLayer: return Strings::commands_NewLayer_FillLayer();
case Type::MaskLayer: return Strings::commands_NewLayer_MaskLayer();
case Type::FxLayer: return Strings::commands_NewLayer_FxLayer();
case Type::TextLayer: return Strings::commands_NewLayer_TextLayer();
case Type::VectorLayer: return Strings::commands_NewLayer_VectorLayer();
case Type::AudioLayer: return Strings::commands_NewLayer_AudioLayer();
case Type::SubspriteLayer: return Strings::commands_NewLayer_SubspriteLayer();
case Type::HitboxLayer: return Strings::commands_NewLayer_HitboxLayer();
}
return "Unknown";
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -96,16 +96,15 @@ void ContextFlags::updateFlagsFromSite(const Site& site)
if (layer->isTilemap())
m_flags |= ActiveLayerIsTilemap;
if (layer->isImage()) {
if (layer->isImage())
m_flags |= ActiveLayerIsImage;
Cel* cel = layer->cel(frame);
if (cel) {
m_flags |= HasActiveCel;
Cel* cel = layer->cel(frame);
if (cel) {
m_flags |= HasActiveCel;
if (cel->image())
m_flags |= HasActiveImage;
}
if (cel->image())
m_flags |= HasActiveImage;
}
if (site.selectedColors().picks() > 0)

View File

@ -28,7 +28,15 @@
#include "doc/frame.h"
#include "doc/image_io.h"
#include "doc/layer.h"
#include "doc/layer_audio.h"
#include "doc/layer_fill.h"
#include "doc/layer_fx.h"
#include "doc/layer_hitbox.h"
#include "doc/layer_mask.h"
#include "doc/layer_subsprite.h"
#include "doc/layer_text.h"
#include "doc/layer_tilemap.h"
#include "doc/layer_vector.h"
#include "doc/palette.h"
#include "doc/palette_io.h"
#include "doc/serial_format.h"
@ -289,7 +297,7 @@ private:
// Read layers
int nlayers = read32(s);
if (nlayers >= 1 && nlayers < 0xfffff) {
std::map<ObjectId, LayerGroup*> layersMap;
std::map<ObjectId, Layer*> layersMap;
layersMap[0] = spr->root(); // parentId = 0 is the root level
for (int i = 0; i < nlayers; ++i) {
@ -308,7 +316,7 @@ private:
Layer* lay = loadObject<Layer*>("lay", layId, &Reader::readLayer);
if (lay) {
if (lay->isGroup())
layersMap[layId] = static_cast<LayerGroup*>(lay);
layersMap[layId] = lay;
layersMap[parentId]->addLayer(lay);
}
@ -324,7 +332,7 @@ private:
return nullptr;
const auto& pair = m_celsToLoad[i];
LayerImage* lay = doc::get<LayerImage>(pair.first);
Layer* lay = doc::get<Layer>(pair.first);
if (!lay)
continue;
@ -447,59 +455,68 @@ private:
{
LayerFlags flags = (LayerFlags)read32(s);
ObjectType type = (ObjectType)read16(s);
ASSERT(type == ObjectType::LayerImage || type == ObjectType::LayerGroup ||
type == ObjectType::LayerTilemap);
std::string name = read_string(s);
std::unique_ptr<Layer> lay;
switch (type) {
case ObjectType::LayerImage:
case ObjectType::LayerTilemap: {
switch (type) {
case ObjectType::LayerImage: lay.reset(new LayerImage(m_sprite)); break;
case ObjectType::LayerImage: lay = std::make_unique<LayerImage>(m_sprite); break;
case ObjectType::LayerTilemap: {
tileset_index tilesetIndex = read32(s);
lay.reset(new LayerTilemap(m_sprite, tilesetIndex));
lay = std::make_unique<LayerTilemap>(m_sprite, tilesetIndex);
break;
}
}
lay->setName(name);
lay->setFlags(flags);
// Blend mode & opacity
static_cast<LayerImage*>(lay.get())->setBlendMode((BlendMode)read16(s));
static_cast<LayerImage*>(lay.get())->setOpacity(read8(s));
// Cels
int ncels = read32(s);
for (int i = 0; i < ncels; ++i) {
if (canceled())
return nullptr;
// Add a new cel to load in the future after we load all layers
ObjectId celId = read32(s);
m_celsToLoad.push_back(std::make_pair(lay->id(), celId));
}
lay->setBlendMode((BlendMode)read16(s));
lay->setOpacity(read8(s));
break;
}
case ObjectType::LayerGroup:
lay.reset(new LayerGroup(m_sprite));
lay->setName(name);
lay->setFlags(flags);
break;
case ObjectType::LayerGroup: lay = std::make_unique<LayerGroup>(m_sprite); break;
case ObjectType::LayerFill: lay = std::make_unique<LayerFill>(m_sprite); break;
case ObjectType::LayerMask: lay = std::make_unique<LayerMask>(m_sprite); break;
case ObjectType::LayerFx: lay = std::make_unique<LayerFx>(m_sprite); break;
case ObjectType::LayerText: lay = std::make_unique<LayerText>(m_sprite); break;
case ObjectType::LayerVector: lay = std::make_unique<LayerVector>(m_sprite); break;
case ObjectType::LayerAudio: lay = std::make_unique<LayerAudio>(m_sprite); break;
case ObjectType::LayerSubsprite: lay = std::make_unique<LayerSubsprite>(m_sprite); break;
case ObjectType::LayerHitbox: lay = std::make_unique<LayerHitbox>(m_sprite); break;
default:
Console().printf("Unable to load layer named '%s', type #%d\n", name.c_str(), (int)type);
break;
}
if (!lay)
return nullptr;
lay->setName(name);
lay->setFlags(flags);
// LayerGroup doesn't contain cels
if (type != ObjectType::LayerGroup) {
const int ncels = read32(s);
for (int i = 0; i < ncels; ++i) {
if (canceled())
return nullptr;
// Add a new cel to load in the future after we load all layers
ObjectId celId = read32(s);
m_celsToLoad.push_back(std::make_pair(lay->id(), celId));
}
}
UserData userData = read_user_data(s, m_serial);
lay->setUserData(userData);

View File

@ -104,14 +104,12 @@ public:
// Save original cel data (skip links)
for (Layer* lay : layers) {
CelList cels;
lay->getCels(cels);
for (Cel* cel : cels) {
for (const Cel* cel : lay->cels()) {
if (cel->link()) // Skip link
continue;
if (!saveObject("img", cel->image(), &Writer::writeImage))
// TODO backup other cel data, e.g. for audio layers
if (cel->image() && !saveObject("img", cel->image(), &Writer::writeImage))
return false;
if (!saveObject("celdata", cel->data(), &Writer::writeCelData))
@ -121,10 +119,7 @@ public:
// Save all cels (original and links)
for (Layer* lay : layers) {
CelList cels;
lay->getCels(cels);
for (Cel* cel : cels)
for (Cel* cel : lay->cels())
if (!saveObject("cel", cel, &Writer::writeCel))
return false;
}
@ -235,14 +230,14 @@ private:
return true;
}
void writeAllLayersID(std::ofstream& s, ObjectId parentId, const LayerGroup* group)
void writeAllLayersID(std::ofstream& s, ObjectId parentId, const Layer* group)
{
for (const Layer* lay : group->layers()) {
write32(s, lay->id());
write32(s, parentId);
if (lay->isGroup())
writeAllLayersID(s, lay->id(), static_cast<const LayerGroup*>(lay));
writeAllLayersID(s, lay->id(), lay);
}
}
@ -259,19 +254,9 @@ private:
if (lay->type() == ObjectType::LayerTilemap)
write32(s, static_cast<const LayerTilemap*>(lay)->tilesetIndex());
CelConstIterator it, begin = static_cast<const LayerImage*>(lay)->getCelBegin();
CelConstIterator end = static_cast<const LayerImage*>(lay)->getCelEnd();
// Blend mode & opacity
write16(s, (int)static_cast<const LayerImage*>(lay)->blendMode());
write8(s, static_cast<const LayerImage*>(lay)->opacity());
// Cels
write32(s, static_cast<const LayerImage*>(lay)->getCelsCount());
for (it = begin; it != end; ++it) {
const Cel* cel = *it;
write32(s, cel->id());
}
write16(s, (int)lay->blendMode());
write8(s, lay->opacity());
break;
}
@ -279,6 +264,26 @@ private:
// Do nothing (the layer parent/children structure is saved in
// writeSprite/writeAllLayersID() functions)
break;
case ObjectType::LayerFill:
case ObjectType::LayerMask:
case ObjectType::LayerFx:
case ObjectType::LayerText:
case ObjectType::LayerVector:
case ObjectType::LayerAudio:
case ObjectType::LayerSubsprite:
case ObjectType::LayerHitbox: {
// TODO
break;
}
}
// Save cels
if (lay->type() != ObjectType::LayerGroup) {
const CelList& cels = lay->cels();
write32(s, cels.size());
for (const Cel* cel : cels)
write32(s, cel->id());
}
// Save user data

View File

@ -515,9 +515,9 @@ void Doc::resetTransformation()
//////////////////////////////////////////////////////////////////////
// Copying
void Doc::copyLayerContent(const Layer* sourceLayer0, Doc* destDoc, Layer* destLayer0) const
void Doc::copyLayerContent(const Layer* sourceLayer, Doc* destDoc, Layer* destLayer) const
{
LayerFlags dstFlags = sourceLayer0->flags();
LayerFlags dstFlags = sourceLayer->flags();
// Remove the "background" flag if the destDoc already has a background layer.
if (((int)dstFlags & (int)LayerFlags::Background) == (int)LayerFlags::Background &&
@ -526,53 +526,15 @@ void Doc::copyLayerContent(const Layer* sourceLayer0, Doc* destDoc, Layer* destL
}
// Copy the layer name/flags/user data
destLayer0->setName(sourceLayer0->name());
destLayer0->setFlags(dstFlags);
destLayer0->setUserData(sourceLayer0->userData());
destLayer->setName(sourceLayer->name());
destLayer->setFlags(dstFlags);
destLayer->setUserData(sourceLayer->userData());
if (sourceLayer0->isImage() && destLayer0->isImage()) {
const LayerImage* sourceLayer = static_cast<const LayerImage*>(sourceLayer0);
LayerImage* destLayer = static_cast<LayerImage*>(destLayer0);
// Copy blend mode and opacity
destLayer->setBlendMode(sourceLayer->blendMode());
destLayer->setOpacity(sourceLayer->opacity());
// Copy cels
CelConstIterator it = sourceLayer->getCelBegin();
CelConstIterator end = sourceLayer->getCelEnd();
std::map<ObjectId, Cel*> linked;
for (; it != end; ++it) {
const Cel* sourceCel = *it;
if (sourceCel->frame() > destLayer->sprite()->lastFrame())
break;
std::unique_ptr<Cel> newCel(nullptr);
auto it = linked.find(sourceCel->data()->id());
if (it != linked.end()) {
newCel.reset(Cel::MakeLink(sourceCel->frame(), it->second));
newCel->copyNonsharedPropertiesFrom(sourceCel);
}
else {
newCel.reset(create_cel_copy(nullptr, // TODO add undo information?
sourceCel,
destLayer->sprite(),
destLayer,
sourceCel->frame()));
linked.insert(std::make_pair(sourceCel->data()->id(), newCel.get()));
}
destLayer->addCel(newCel.get());
newCel.release();
}
}
else if (sourceLayer0->isGroup() && destLayer0->isGroup()) {
const LayerGroup* sourceLayer = static_cast<const LayerGroup*>(sourceLayer0);
LayerGroup* destLayer = static_cast<LayerGroup*>(destLayer0);
// Copy blend mode and opacity
destLayer->setBlendMode(sourceLayer->blendMode());
destLayer->setOpacity(sourceLayer->opacity());
if (sourceLayer->isGroup() && destLayer->isGroup()) {
for (Layer* sourceChild : sourceLayer->layers()) {
std::unique_ptr<Layer> destChild(nullptr);
@ -608,6 +570,38 @@ void Doc::copyLayerContent(const Layer* sourceLayer0, Doc* destDoc, Layer* destL
destLayer->stackLayer(newLayer, afterThis);
}
}
else if (sourceLayer->type() == destLayer->type()) {
// Copy cels
CelConstIterator it = sourceLayer->getCelBegin();
const CelConstIterator end = sourceLayer->getCelEnd();
std::map<ObjectId, Cel*> linked;
for (; it != end; ++it) {
const Cel* sourceCel = *it;
if (sourceCel->frame() > destLayer->sprite()->lastFrame())
break;
std::unique_ptr<Cel> newCel(nullptr);
auto it = linked.find(sourceCel->data()->id());
if (it != linked.end()) {
newCel.reset(Cel::MakeLink(sourceCel->frame(), it->second));
newCel->copyNonsharedPropertiesFrom(sourceCel);
}
else {
newCel.reset(create_cel_copy(nullptr, // TODO add undo information?
sourceCel,
destLayer->sprite(),
destLayer,
sourceCel->frame()));
linked.insert(std::make_pair(sourceCel->data()->id(), newCel.get()));
}
destLayer->addCel(newCel.get());
newCel.release();
}
}
else {
ASSERT(false && "Trying to copy two incompatible layers");
}

View File

@ -53,7 +53,15 @@
#include "doc/algorithm/flip_image.h"
#include "doc/algorithm/shrink_bounds.h"
#include "doc/cel.h"
#include "doc/layer_audio.h"
#include "doc/layer_fill.h"
#include "doc/layer_fx.h"
#include "doc/layer_hitbox.h"
#include "doc/layer_mask.h"
#include "doc/layer_subsprite.h"
#include "doc/layer_text.h"
#include "doc/layer_tilemap.h"
#include "doc/layer_vector.h"
#include "doc/mask.h"
#include "doc/palette.h"
#include "doc/slice.h"
@ -74,9 +82,9 @@
namespace app {
DocApi::HandleLinkedCels::HandleLinkedCels(DocApi& api,
doc::LayerImage* srcLayer,
doc::Layer* srcLayer,
const doc::frame_t srcFrame,
doc::LayerImage* dstLayer,
doc::Layer* dstLayer,
const doc::frame_t dstFrame)
: m_api(api)
, m_srcDataId(doc::NullId)
@ -351,11 +359,8 @@ void DocApi::copyFrame(Sprite* sprite,
if (fromFrame >= newFrame)
++fromFrame;
for (Layer* layer : sprite->allLayers()) {
if (layer->isImage()) {
copyCel(static_cast<LayerImage*>(layer), fromFrame, static_cast<LayerImage*>(layer), newFrame);
}
}
for (Layer* layer : sprite->allLayers())
copyCel(layer, fromFrame, layer, newFrame);
adjustTags(sprite, newFrame0, +1, dropFramePlace, tagsHandling);
}
@ -474,14 +479,14 @@ void DocApi::moveFrameLayer(Layer* layer, frame_t frame, frame_t beforeFrame)
}
case ObjectType::LayerGroup: {
for (Layer* child : static_cast<LayerGroup*>(layer)->layers())
for (Layer* child : layer->layers())
moveFrameLayer(child, frame, beforeFrame);
break;
}
}
}
void DocApi::addCel(LayerImage* layer, Cel* cel)
void DocApi::addCel(Layer* layer, Cel* cel)
{
ASSERT(layer);
ASSERT(cel);
@ -540,7 +545,7 @@ void DocApi::clearCelAndAllLinks(Cel* cel)
}
}
void DocApi::moveCel(LayerImage* srcLayer, frame_t srcFrame, LayerImage* dstLayer, frame_t dstFrame)
void DocApi::moveCel(Layer* srcLayer, frame_t srcFrame, Layer* dstLayer, frame_t dstFrame)
{
ASSERT(srcLayer != dstLayer || srcFrame != dstFrame);
if (srcLayer == dstLayer && srcFrame == dstFrame)
@ -557,9 +562,9 @@ void DocApi::moveCel(LayerImage* srcLayer, frame_t srcFrame, LayerImage* dstLaye
new cmd::MoveCel(srcLayer, srcFrame, dstLayer, dstFrame, dstLayer->isContinuous()));
}
void DocApi::copyCel(LayerImage* srcLayer,
void DocApi::copyCel(Layer* srcLayer,
frame_t srcFrame,
LayerImage* dstLayer,
Layer* dstLayer,
frame_t dstFrame,
const bool* forceContinuous)
{
@ -579,7 +584,7 @@ void DocApi::copyCel(LayerImage* srcLayer,
(forceContinuous ? *forceContinuous : dstLayer->isContinuous())));
}
void DocApi::swapCel(LayerImage* layer, frame_t frame1, frame_t frame2)
void DocApi::swapCel(Layer* layer, frame_t frame1, frame_t frame2)
{
ASSERT(frame1 != frame2);
@ -598,7 +603,7 @@ void DocApi::swapCel(LayerImage* layer, frame_t frame1, frame_t frame2)
setCelFramePosition(cel2, frame1);
}
LayerImage* DocApi::newLayer(LayerGroup* parent, const std::string& name)
LayerImage* DocApi::newLayer(Layer* parent, const std::string& name)
{
LayerImage* newLayer = new LayerImage(parent->sprite());
newLayer->setName(name);
@ -607,7 +612,7 @@ LayerImage* DocApi::newLayer(LayerGroup* parent, const std::string& name)
return newLayer;
}
LayerImage* DocApi::newLayerAfter(LayerGroup* parent, const std::string& name, Layer* afterThis)
LayerImage* DocApi::newLayerAfter(Layer* parent, const std::string& name, Layer* afterThis)
{
LayerImage* newLayer = new LayerImage(parent->sprite());
newLayer->setName(name);
@ -619,7 +624,7 @@ LayerImage* DocApi::newLayerAfter(LayerGroup* parent, const std::string& name, L
return newLayer;
}
LayerGroup* DocApi::newGroup(LayerGroup* parent, const std::string& name)
LayerGroup* DocApi::newGroup(Layer* parent, const std::string& name)
{
LayerGroup* newLayerGroup = new LayerGroup(parent->sprite());
newLayerGroup->setName(name);
@ -628,7 +633,7 @@ LayerGroup* DocApi::newGroup(LayerGroup* parent, const std::string& name)
return newLayerGroup;
}
LayerGroup* DocApi::newGroupAfter(LayerGroup* parent, const std::string& name, Layer* afterThis)
LayerGroup* DocApi::newGroupAfter(Layer* parent, const std::string& name, Layer* afterThis)
{
LayerGroup* newLayerGroup = new LayerGroup(parent->sprite());
newLayerGroup->setName(name);
@ -640,7 +645,7 @@ LayerGroup* DocApi::newGroupAfter(LayerGroup* parent, const std::string& name, L
return newLayerGroup;
}
LayerTilemap* DocApi::newTilemapAfter(LayerGroup* parent,
LayerTilemap* DocApi::newTilemapAfter(Layer* parent,
const std::string& name,
tileset_index tsi,
Layer* afterThis)
@ -655,7 +660,7 @@ LayerTilemap* DocApi::newTilemapAfter(LayerGroup* parent,
return newTilemap;
}
void DocApi::addLayer(LayerGroup* parent, Layer* newLayer, Layer* afterThis)
void DocApi::addLayer(Layer* parent, Layer* newLayer, Layer* afterThis)
{
m_transaction.execute(new cmd::AddLayer(parent, newLayer, afterThis));
}
@ -667,7 +672,7 @@ void DocApi::removeLayer(Layer* layer)
m_transaction.execute(new cmd::RemoveLayer(layer));
}
void DocApi::restackLayerAfter(Layer* layer, LayerGroup* parent, Layer* afterThis)
void DocApi::restackLayerAfter(Layer* layer, Layer* parent, Layer* afterThis)
{
ASSERT(parent);
@ -677,7 +682,7 @@ void DocApi::restackLayerAfter(Layer* layer, LayerGroup* parent, Layer* afterThi
m_transaction.execute(new cmd::MoveLayer(layer, parent, afterThis));
}
void DocApi::restackLayerBefore(Layer* layer, LayerGroup* parent, Layer* beforeThis)
void DocApi::restackLayerBefore(Layer* layer, Layer* parent, Layer* beforeThis)
{
ASSERT(parent);
@ -696,27 +701,41 @@ void DocApi::restackLayerBefore(Layer* layer, LayerGroup* parent, Layer* beforeT
Layer* DocApi::copyLayerWithSprite(doc::Layer* layer, doc::Sprite* sprite)
{
std::unique_ptr<doc::Layer> clone;
if (layer->isTilemap()) {
auto* srcTilemap = static_cast<LayerTilemap*>(layer);
tileset_index tilesetIndex = srcTilemap->tilesetIndex();
// If the caller is trying to make a copy of a tilemap layer specifying a
// different sprite as its owner, then we must copy the tilesets of the
// given tilemap layer into the new owner.
if (sprite != srcTilemap->sprite()) {
auto* srcTilesetCopy = Tileset::MakeCopyCopyingImagesForSprite(srcTilemap->tileset(), sprite);
auto* addTileset = new cmd::AddTileset(sprite, srcTilesetCopy);
m_transaction.execute(addTileset);
tilesetIndex = addTileset->tilesetIndex();
switch (layer->type()) {
case ObjectType::LayerImage: clone = std::make_unique<LayerImage>(sprite); break;
case ObjectType::LayerGroup: clone = std::make_unique<LayerGroup>(sprite); break;
case ObjectType::LayerTilemap: {
auto* srcTilemap = static_cast<LayerTilemap*>(layer);
tileset_index tilesetIndex = srcTilemap->tilesetIndex();
// If the caller is trying to make a copy of a tilemap layer specifying a
// different sprite as its owner, then we must copy the tilesets of the
// given tilemap layer into the new owner.
if (sprite != srcTilemap->sprite()) {
auto* srcTilesetCopy = Tileset::MakeCopyCopyingImagesForSprite(srcTilemap->tileset(),
sprite);
auto* addTileset = new cmd::AddTileset(sprite, srcTilesetCopy);
m_transaction.execute(addTileset);
tilesetIndex = addTileset->tilesetIndex();
}
clone = std::make_unique<LayerTilemap>(sprite, tilesetIndex);
break;
}
clone = std::make_unique<LayerTilemap>(sprite, tilesetIndex);
case ObjectType::LayerFill: clone = std::make_unique<LayerFill>(sprite); break;
case ObjectType::LayerMask: clone = std::make_unique<LayerMask>(sprite); break;
case ObjectType::LayerFx: clone = std::make_unique<LayerFx>(sprite); break;
case ObjectType::LayerText: clone = std::make_unique<LayerText>(sprite); break;
case ObjectType::LayerVector: clone = std::make_unique<LayerVector>(sprite); break;
case ObjectType::LayerAudio: clone = std::make_unique<LayerAudio>(sprite); break;
case ObjectType::LayerSubsprite: clone = std::make_unique<LayerSubsprite>(sprite); break;
case ObjectType::LayerHitbox: clone = std::make_unique<LayerHitbox>(sprite); break;
default: throw std::runtime_error("Invalid layer type");
}
else if (layer->isImage())
clone = std::make_unique<LayerImage>(sprite);
else if (layer->isGroup())
clone = std::make_unique<LayerGroup>(sprite);
else
throw std::runtime_error("Invalid layer type");
if (auto* doc = dynamic_cast<app::Doc*>(sprite->document())) {
doc->copyLayerContent(layer, doc, clone.get());
@ -726,7 +745,7 @@ Layer* DocApi::copyLayerWithSprite(doc::Layer* layer, doc::Sprite* sprite)
}
Layer* DocApi::duplicateLayerAfter(Layer* sourceLayer,
LayerGroup* parent,
Layer* parent,
Layer* afterLayer,
const std::string& nameSuffix)
{
@ -741,7 +760,7 @@ Layer* DocApi::duplicateLayerAfter(Layer* sourceLayer,
}
Layer* DocApi::duplicateLayerBefore(Layer* sourceLayer,
LayerGroup* parent,
Layer* parent,
Layer* beforeLayer,
const std::string& nameSuffix)
{

View File

@ -91,40 +91,40 @@ public:
const TagsHandling tagsHandling);
// Cels API
void addCel(LayerImage* layer, Cel* cel);
void addCel(Layer* layer, Cel* cel);
Cel* addCel(LayerImage* layer, frame_t frameNumber, const ImageRef& image);
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);
void setCelOpacity(Sprite* sprite, Cel* cel, int newOpacity);
void moveCel(LayerImage* srcLayer, frame_t srcFrame, LayerImage* dstLayer, frame_t dstFrame);
void copyCel(LayerImage* srcLayer,
void moveCel(Layer* srcLayer, frame_t srcFrame, Layer* dstLayer, frame_t dstFrame);
void copyCel(Layer* srcLayer,
frame_t srcFrame,
LayerImage* dstLayer,
Layer* dstLayer,
frame_t dstFrame,
const bool* forceContinuous = nullptr);
void swapCel(LayerImage* layer, frame_t frame1, frame_t frame2);
void swapCel(Layer* layer, frame_t frame1, frame_t frame2);
// Layers API
LayerImage* newLayer(LayerGroup* parent, const std::string& name);
LayerImage* newLayerAfter(LayerGroup* parent, const std::string& name, Layer* afterThis);
LayerGroup* newGroup(LayerGroup* parent, const std::string& name);
LayerGroup* newGroupAfter(LayerGroup* parent, const std::string& name, Layer* afterThis);
LayerTilemap* newTilemapAfter(LayerGroup* parent,
LayerImage* newLayer(Layer* parent, const std::string& name);
LayerImage* newLayerAfter(Layer* parent, const std::string& name, Layer* afterThis);
LayerGroup* newGroup(Layer* parent, const std::string& name);
LayerGroup* newGroupAfter(Layer* parent, const std::string& name, Layer* afterThis);
LayerTilemap* newTilemapAfter(Layer* parent,
const std::string& name,
tileset_index tsi,
Layer* afterThis);
void addLayer(LayerGroup* parent, Layer* newLayer, Layer* afterThis);
void addLayer(Layer* parent, Layer* newLayer, Layer* afterThis);
void removeLayer(Layer* layer);
void restackLayerAfter(Layer* layer, LayerGroup* parent, Layer* afterThis);
void restackLayerBefore(Layer* layer, LayerGroup* parent, Layer* beforeThis);
void restackLayerAfter(Layer* layer, Layer* parent, Layer* afterThis);
void restackLayerBefore(Layer* layer, Layer* parent, Layer* beforeThis);
Layer* duplicateLayerAfter(Layer* sourceLayer,
LayerGroup* parent,
Layer* parent,
Layer* afterLayer,
const std::string& nameSuffix = std::string());
Layer* duplicateLayerBefore(Layer* sourceLayer,
LayerGroup* parent,
Layer* parent,
Layer* beforeLayer,
const std::string& nameSuffix = std::string());
@ -166,9 +166,9 @@ private:
class HandleLinkedCels {
public:
HandleLinkedCels(DocApi& api,
doc::LayerImage* srcLayer,
doc::Layer* srcLayer,
const doc::frame_t srcFrame,
doc::LayerImage* dstLayer,
doc::Layer* dstLayer,
const doc::frame_t dstFrame);
~HandleLinkedCels();
bool linkWasCreated() { return m_created; }

View File

@ -43,12 +43,12 @@ void setup_insertion_layer(Doc* destDoc,
doc::layer_t layerIndex,
InsertionPoint& insert,
Layer*& layer,
LayerGroup*& group)
Layer*& group)
{
const LayerList& allLayers = destDoc->sprite()->allLayers();
layer = allLayers[layerIndex];
if (insert == InsertionPoint::BeforeLayer && layer->isGroup()) {
group = static_cast<LayerGroup*>(layer);
group = layer;
// The user is trying to drop layers into an empty group, so there is no after
// nor before layer...
if (group->layersCount() == 0) {
@ -79,7 +79,7 @@ void DocApi::dropDocumentsOnTimeline(app::Doc* destDoc,
// inserted after or before it.
Layer* refLayer = nullptr;
// Parent group of the reference layer layer.
LayerGroup* group = nullptr;
Layer* group = nullptr;
// Keep track of the current insertion point.
setup_insertion_layer(destDoc, layerIndex, insert, refLayer, group);

View File

@ -49,11 +49,11 @@ static void move_or_copy_cels(DocApi& api,
auto dstFrameEnd = dstFrames.end();
for (; srcFrame != srcFrameEnd && dstFrame != dstFrameEnd; ++srcFrame, ++dstFrame) {
if (i >= 0 && i < srcLayers.size() && srcLayers[i]->isImage()) {
LayerImage* srcLayer = static_cast<LayerImage*>(srcLayers[i]);
if (i >= 0 && i < srcLayers.size()) {
Layer* srcLayer = srcLayers[i];
if (i < dstLayers.size() && dstLayers[i]->isImage()) {
LayerImage* dstLayer = static_cast<LayerImage*>(dstLayers[i]);
if (i < dstLayers.size()) {
Layer* dstLayer = dstLayers[i];
#ifdef TRACE_RANGE_OPS
std::clog << (op == Move ? "Moving" : "Copying") << " cel " << srcLayer->name() << "["
@ -186,12 +186,12 @@ static DocRange move_or_copy_frames(DocApi& api,
return result;
}
static bool has_child(LayerGroup* parent, Layer* child)
static bool has_child(Layer* parent, Layer* child)
{
for (auto c : parent->layers()) {
if (c == child)
return true;
else if (c->isGroup() && has_child(static_cast<LayerGroup*>(c), child))
else if (c->isGroup() && has_child(c, child))
return true;
}
return false;
@ -205,11 +205,11 @@ static DocRange drop_range_op(Doc* doc,
DocRange to)
{
// Convert "first child" operation into a insert after last child.
LayerGroup* parent = nullptr;
Layer* parent = nullptr;
if (to.type() == DocRange::kLayers && !to.selectedLayers().empty()) {
if (place == kDocRangeFirstChild && (*to.selectedLayers().begin())->isGroup()) {
if (place == kDocRangeFirstChild) {
place = kDocRangeAfter;
parent = static_cast<LayerGroup*>((*to.selectedLayers().begin()));
parent = *to.selectedLayers().begin();
to.clearRange();
to.startRange(parent->lastLayer(), -1, DocRange::kLayers);
@ -221,8 +221,7 @@ static DocRange drop_range_op(Doc* doc,
// Check that we're not moving a group inside itself
for (auto moveThis : from.selectedLayers()) {
if (moveThis == parent ||
(moveThis->isGroup() && has_child(static_cast<LayerGroup*>(moveThis), parent)))
if (moveThis == parent || (moveThis->isGroup() && has_child(moveThis, parent)))
return from;
}
}
@ -498,17 +497,13 @@ void reverse_frames(Doc* doc, const DocRange& range)
}
else if (swapCels) {
for (Layer* layer : layers) {
if (!layer->isImage())
continue;
for (frame_t frame = frameBegin, frameRev = frameEnd;
frame != (frameBegin + frameEnd) / 2 + 1;
++frame, --frameRev) {
if (frame == frameRev)
continue;
LayerImage* imageLayer = static_cast<LayerImage*>(layer);
api.swapCel(imageLayer, frame, frameRev);
api.swapCel(layer, frame, frameRev);
}
}
}

View File

@ -644,7 +644,7 @@ static void ase_file_write_layers(FILE* f,
ase_file_write_user_data_chunk(f, fop, frame_header, ext_files, &layer->userData());
if (layer->isGroup()) {
for (const Layer* child : static_cast<const LayerGroup*>(layer)->layers())
for (const Layer* child : layer->layers())
ase_file_write_layers(f, fop, header, frame_header, ext_files, child, child_index + 1);
}
}
@ -683,7 +683,7 @@ static layer_t ase_file_write_cels(FILE* f,
++layer_index;
if (layer->isGroup()) {
for (const Layer* child : static_cast<const LayerGroup*>(layer)->layers()) {
for (const Layer* child : layer->layers()) {
layer_index =
ase_file_write_cels(f, fop, frame_header, ext_files, sprite, child, layer_index, frame);
}
@ -1449,7 +1449,7 @@ static void ase_file_write_external_files_chunk(FILE* f,
putExtentionIds(layer->userData().propertiesMaps(), ext_files);
if (layer->isGroup()) {
auto childLayers = static_cast<const LayerGroup*>(layer)->layers();
auto childLayers = layer->layers();
layers.insert(layers.end(), childLayers.begin(), childLayers.end());
}
else if (layer->isImage()) {

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -69,7 +69,7 @@ static bool has_cels(const Layer* layer, frame_t frame)
case ObjectType::LayerImage: return (layer->cel(frame) ? true : false);
case ObjectType::LayerGroup: {
for (const Layer* child : static_cast<const LayerGroup*>(layer)->layers()) {
for (const Layer* child : layer->layers()) {
if (has_cels(child, frame))
return true;
}

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2016 David Capello
//
// This program is distributed under the terms of
@ -39,7 +40,7 @@ void RestoreVisibleLayers::showSelectedLayers(Sprite* sprite, const SelectedLaye
setLayerVisiblity(sprite->root(), selLayers);
}
void RestoreVisibleLayers::setLayerVisiblity(LayerGroup* group, const SelectedLayers& selLayers)
void RestoreVisibleLayers::setLayerVisiblity(Layer* group, const SelectedLayers& selLayers)
{
for (Layer* layer : group->layers()) {
bool selected = (selLayers.contains(layer));
@ -48,7 +49,7 @@ void RestoreVisibleLayers::setLayerVisiblity(LayerGroup* group, const SelectedLa
layer->setVisible(selected);
}
if (selected && layer->isGroup())
setLayerVisiblity(static_cast<LayerGroup*>(layer), selLayers);
setLayerVisiblity(layer, selLayers);
}
}

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
// Copyright (C) 2016 David Capello
//
// This program is distributed under the terms of
@ -27,7 +28,7 @@ public:
void showSelectedLayers(doc::Sprite* sprite, const doc::SelectedLayers& selLayers);
private:
void setLayerVisiblity(doc::LayerGroup* group, const doc::SelectedLayers& selLayers);
void setLayerVisiblity(doc::Layer* group, const doc::SelectedLayers& selLayers);
std::vector<std::pair<doc::Layer*, bool>> m_restore;
};

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -44,7 +44,6 @@ namespace doc {
class Cel;
class Image;
class Layer;
class LayerGroup;
class Mask;
class Palette;
class Sprite;
@ -151,7 +150,7 @@ void push_cels(lua_State* L, doc::Sprite* sprite);
void push_color_space(lua_State* L, const gfx::ColorSpace& cs);
void push_doc_range(lua_State* L, Site& site);
void push_editor(lua_State* L, Editor* editor);
void push_group_layers(lua_State* L, doc::LayerGroup* group);
void push_group_layers(lua_State* L, doc::Layer* group);
void push_image(lua_State* L, doc::Image* image);
void push_layers(lua_State* L, const doc::ObjectIds& layers);
void push_palette(lua_State* L, doc::Palette* palette);

View File

@ -82,7 +82,7 @@ int Layer_get_layers(lua_State* L)
{
auto layer = get_docobj<Layer>(L, 1);
if (layer->isGroup())
push_group_layers(L, static_cast<LayerGroup*>(layer));
push_group_layers(L, layer);
else
lua_pushnil(L);
return 1;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2019 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
@ -31,7 +31,7 @@ struct LayersObj {
for (const Layer* layer : sprite->root()->layers())
layers.push_back(layer->id());
}
LayersObj(LayerGroup* group)
LayersObj(Layer* group)
{
for (const Layer* layer : group->layers())
layers.push_back(layer->id());
@ -102,7 +102,7 @@ void push_sprite_layers(lua_State* L, Sprite* sprite)
push_new<LayersObj>(L, sprite);
}
void push_group_layers(lua_State* L, LayerGroup* group)
void push_group_layers(lua_State* L, Layer* group)
{
push_new<LayersObj>(L, group);
}

View File

@ -455,16 +455,13 @@ int Sprite_deleteFrame(lua_State* L)
int Sprite_newCel(lua_State* L)
{
auto sprite = get_docobj<Sprite>(L, 1);
auto layerBase = get_docobj<Layer>(L, 2);
if (!layerBase->isImage())
return luaL_error(L, "unexpected kind of layer in Sprite:newCel()");
auto layer = get_docobj<Layer>(L, 2);
frame_t frame = get_frame_number_from_arg(L, 3);
if (frame < 0 || frame > sprite->lastFrame())
return luaL_error(L, "frame index out of bounds %d", frame + 1);
Doc* doc = static_cast<Doc*>(sprite->document());
LayerImage* layer = static_cast<LayerImage*>(layerBase);
ImageRef image(nullptr);
Image* srcImage = may_get_image_from_arg(L, 4);
@ -488,8 +485,11 @@ int Sprite_newCel(lua_State* L)
else {
if (srcImage)
image.reset(Image::createCopy(srcImage));
else
else if (layer->isImage())
image.reset(Image::create(sprite->spec()));
else {
// TODO copy any other kind of cel data
}
cel = new Cel(frame, image);
cel->setPosition(pos);

View File

@ -118,7 +118,7 @@ struct Timeline::DrawCelData {
namespace {
template<typename Pred>
void for_each_expanded_layer(LayerGroup* group,
void for_each_expanded_layer(Layer* group,
Pred&& pred,
int level = 0,
LayerFlags flags = LayerFlags(int(LayerFlags::Visible) |
@ -131,11 +131,8 @@ void for_each_expanded_layer(LayerGroup* group,
flags = static_cast<LayerFlags>(int(flags) & ~int(LayerFlags::Editable));
for (Layer* child : group->layers()) {
if (child->isGroup() && !child->isCollapsed())
for_each_expanded_layer<Pred>(static_cast<LayerGroup*>(child),
std::forward<Pred>(pred),
level + 1,
flags);
if (child->isExpanded())
for_each_expanded_layer<Pred>(child, std::forward<Pred>(pred), level + 1, flags);
pred(child, level, flags);
}
@ -453,7 +450,7 @@ void Timeline::setLayer(Layer* layer)
// Expand all parents
if (m_layer) {
LayerGroup* group = m_layer->parent();
Layer* group = m_layer->parent();
while (group != m_layer->sprite()->root()) {
// Expand this group
group->setCollapsed(false);
@ -1728,7 +1725,7 @@ void Timeline::onPaint(ui::PaintEvent& ev)
continue;
Layer* layerPtr = getLayer(layer);
if (!layerPtr || !layerPtr->isImage()) {
if (!layerPtr) {
// Draw empty cels
for (frame = firstFrame; frame <= lastFrame; frame = col_t(frame + 1)) {
drawCel(g, layer, frame, nullptr, nullptr);
@ -1741,13 +1738,12 @@ void Timeline::onPaint(ui::PaintEvent& ev)
// Get the first CelIterator to be drawn (it is the first cel with cel->frame >=
// first_frame)
LayerImage* layerImagePtr = static_cast<LayerImage*>(layerPtr);
data.begin = layerImagePtr->getCelBegin();
data.end = layerImagePtr->getCelEnd();
data.begin = layerPtr->getCelBegin();
data.end = layerPtr->getCelEnd();
const frame_t firstRealFrame(m_adapter->toRealFrame(firstFrame));
const frame_t lastRealFrame(m_adapter->toRealFrame(lastFrame));
data.it = layerImagePtr->findFirstCelIteratorAfter(firstRealFrame - 1);
data.it = layerPtr->findFirstCelIteratorAfter(firstRealFrame - 1);
if (firstRealFrame > 0 && data.it != data.begin)
data.prevIt = data.it - 1;
@ -1760,33 +1756,34 @@ void Timeline::onPaint(ui::PaintEvent& ev)
data.lastLink = data.end;
if (layerPtr == m_layer) {
data.activeIt = layerImagePtr->findCelIterator(frame_t(realActiveFrame));
data.activeIt = layerPtr->findCelIterator(frame_t(realActiveFrame));
if (data.activeIt != data.end) {
data.firstLink = data.activeIt;
data.lastLink = data.activeIt;
ObjectId imageId = (*data.activeIt)->image()->id();
// TODO impl an alternative way to find links (not only by image ID)
if (ObjectId imageId = (*data.activeIt)->imageId()) {
auto it2 = data.activeIt;
if (it2 != data.begin) {
do {
--it2;
if ((*it2)->imageId() == imageId) {
data.firstLink = it2;
if ((*it2)->frame() < firstRealFrame)
break;
}
} while (it2 != data.begin);
}
auto it2 = data.activeIt;
if (it2 != data.begin) {
do {
--it2;
if ((*it2)->image()->id() == imageId) {
data.firstLink = it2;
if ((*it2)->frame() < firstRealFrame)
it2 = data.activeIt;
while (it2 != data.end) {
if ((*it2)->imageId() == imageId) {
data.lastLink = it2;
if ((*it2)->frame() > lastRealFrame)
break;
}
} while (it2 != data.begin);
}
it2 = data.activeIt;
while (it2 != data.end) {
if ((*it2)->image()->id() == imageId) {
data.lastLink = it2;
if ((*it2)->frame() > lastRealFrame)
break;
++it2;
}
++it2;
}
}
}
@ -2350,6 +2347,16 @@ void Timeline::drawLayer(ui::Graphics* g, const int layerIdx)
(hotlayer && m_hot.part == PART_ROW_CONTINUOUS_ICON),
(clklayer && m_clk.part == PART_ROW_CONTINUOUS_ICON));
}
// Just an empty box for other kind of layers
else {
drawPart(g,
bounds,
nullptr,
styles.timelineBox(),
is_active || (clklayer && m_clk.part == PART_ROW_CONTINUOUS_ICON),
(hotlayer && m_hot.part == PART_ROW_CONTINUOUS_ICON),
(clklayer && m_clk.part == PART_ROW_CONTINUOUS_ICON));
}
// Get the layer's name bounds.
bounds = getPartBounds(Hit(PART_ROW_TEXT, layerIdx));
@ -2448,7 +2455,7 @@ void Timeline::drawCel(ui::Graphics* g,
bool is_hover = (m_hot.part == PART_CEL && m_hot.layer == layerIndex && m_hot.frame == col);
const bool is_active = isCelActive(layerIndex, col);
const bool is_loosely_active = isCelLooselyActive(layerIndex, col);
const bool is_empty = (image == nullptr);
const bool is_empty = (cel == nullptr);
gfx::Rect bounds = getPartBounds(Hit(PART_CEL, layerIndex, col));
gfx::Rect full_bounds = bounds;
IntersectClip clip(g, bounds);
@ -2503,10 +2510,8 @@ void Timeline::drawCel(ui::Graphics* g,
if (right && right->frame() != frame + 1)
right = nullptr;
ObjectId leftImg = (left ? left->image()->id() : 0);
ObjectId rightImg = (right ? right->image()->id() : 0);
fromLeft = (leftImg == cel->image()->id());
fromRight = (rightImg == cel->image()->id());
fromLeft = (left && left->image() && left->imageId() == cel->imageId());
fromRight = (right && right->image() && right->imageId() == cel->imageId());
if (fromLeft && fromRight)
style = styles.timelineFromBoth();
@ -2638,25 +2643,27 @@ void Timeline::drawCelLinkDecorators(ui::Graphics* g,
const DrawCelData* data)
{
auto& styles = skinTheme()->styles;
ObjectId imageId = (*data->activeIt)->image()->id();
ObjectId imageId = (*data->activeIt)->imageId();
ui::Style* style1 = nullptr;
ui::Style* style2 = nullptr;
// Links at the left or right side
fr_t frame = m_adapter->toRealFrame(col);
bool left = (data->firstLink != data->end ? frame > (*data->firstLink)->frame() : false);
bool right = (data->lastLink != data->end ? frame < (*data->lastLink)->frame() : false);
bool left = (imageId && data->firstLink != data->end ? frame > (*data->firstLink)->frame() :
false);
bool right = (imageId && data->lastLink != data->end ? frame < (*data->lastLink)->frame() :
false);
if (cel && cel->image()->id() == imageId) {
if (cel && cel->imageId() == imageId) {
if (left) {
Cel* prevCel = m_layer->cel(cel->frame() - 1);
if (!prevCel || prevCel->image()->id() != imageId)
if (!prevCel || prevCel->imageId() != imageId)
style1 = styles.timelineLeftLink();
}
if (right) {
Cel* nextCel = m_layer->cel(cel->frame() + 1);
if (!nextCel || nextCel->image()->id() != imageId)
if (!nextCel || nextCel->imageId() != imageId)
style2 = styles.timelineRightLink();
}
}
@ -4264,10 +4271,13 @@ void Timeline::updateDropRange(const gfx::Point& pt)
m_dropTarget.vhit = DropTarget::VeryBottom;
else if (pt.y < bounds.y + bounds.h / 2)
m_dropTarget.vhit = DropTarget::Top;
// Special drop target for expanded groups
else if (m_range.type() == Range::kLayers && m_hot.layer >= 0 &&
m_hot.layer < int(m_rows.size()) && m_rows[m_hot.layer].layer()->isGroup() &&
static_cast<LayerGroup*>(m_rows[m_hot.layer].layer())->isExpanded()) {
m_hot.layer < int(m_rows.size()) &&
// Special drop target for expanded groups
((m_rows[m_hot.layer].layer()->isGroup() && m_rows[m_hot.layer].layer()->isExpanded()) ||
// Special drop for mask/fx inside layers
(m_rows[m_clk.layer].layer()->isMaskOrFx() &&
m_rows[m_hot.layer].layer()->acceptMaskOrFx()))) {
m_dropTarget.vhit = DropTarget::FirstChild;
}
else {

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -302,38 +302,49 @@ Cel* create_cel_copy(CmdSequence* cmds,
Layer* dstLayer,
const frame_t dstFrame)
{
gfx::Size dstSize(0, 0);
doc::PixelFormat dstPixelFormat = dstSprite->pixelFormat();
const Image* srcImage = srcCel->image();
doc::PixelFormat dstPixelFormat = (dstLayer->isTilemap() ? IMAGE_TILEMAP :
dstSprite->pixelFormat());
gfx::Size dstSize(srcImage->width(), srcImage->height());
if (srcImage) {
if (dstLayer->isTilemap())
dstPixelFormat = IMAGE_TILEMAP;
dstSize = gfx::Size(srcImage->width(), srcImage->height());
// From Tilemap -> Image
if (srcCel->layer()->isTilemap() && !dstLayer->isTilemap()) {
auto layerTilemap = static_cast<doc::LayerTilemap*>(srcCel->layer());
dstSize = layerTilemap->tileset()->grid().tilemapSizeToCanvas(dstSize);
}
// From Image or Tilemap -> Tilemap
else if (dstLayer->isTilemap()) {
auto dstLayerTilemap = static_cast<doc::LayerTilemap*>(dstLayer);
// From Tilemap -> Image
if (srcCel->layer()->isTilemap() && !dstLayer->isTilemap()) {
auto layerTilemap = static_cast<doc::LayerTilemap*>(srcCel->layer());
dstSize = layerTilemap->tileset()->grid().tilemapSizeToCanvas(dstSize);
}
// From Image or Tilemap -> Tilemap
else if (dstLayer->isTilemap()) {
auto dstLayerTilemap = static_cast<doc::LayerTilemap*>(dstLayer);
Grid grid = dstLayerTilemap->tileset()->grid();
Grid grid = dstLayerTilemap->tileset()->grid();
// Tilemap -> Tilemap
if (srcCel->layer()->isTilemap())
grid.origin(srcCel->position());
// Tilemap -> Tilemap
if (srcCel->layer()->isTilemap())
grid.origin(srcCel->position());
const gfx::Rect tilemapBounds = grid.canvasToTile(srcCel->bounds());
dstSize = tilemapBounds.size();
const gfx::Rect tilemapBounds = grid.canvasToTile(srcCel->bounds());
dstSize = tilemapBounds.size();
}
}
// New cel
auto dstCel =
std::make_unique<Cel>(dstFrame, ImageRef(Image::create(dstPixelFormat, dstSize.w, dstSize.h)));
ImageRef dstImage;
if (dstSize.w > 0 && dstSize.h > 0)
dstImage.reset(Image::create(dstPixelFormat, dstSize.w, dstSize.h));
auto dstCel = std::make_unique<Cel>(dstFrame, dstImage);
dstCel->setOpacity(srcCel->opacity());
dstCel->copyNonsharedPropertiesFrom(srcCel);
dstCel->data()->setUserData(srcCel->data()->userData());
if (!srcImage)
return dstCel.release();
// The following code is to copy the image between cels...
// Special case were we copy from a tilemap...
if (srcCel->layer()->isTilemap()) {
if (dstLayer->isTilemap()) {
@ -341,7 +352,7 @@ Cel* create_cel_copy(CmdSequence* cmds,
// 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()));
dstImage->copy(srcImage, gfx::Clip(0, 0, srcImage->bounds()));
}
// Tilemap -> Tilemap (with different tilesets)
else {

View File

@ -688,10 +688,7 @@ void Clipboard::paste(Context* ctx, const bool interactive, const gfx::Point* po
Cel* srcCel = srcLayer->cel(srcFrame);
if (srcCel && srcCel->image()) {
api.copyCel(static_cast<LayerImage*>(srcLayer),
srcFrame,
static_cast<LayerImage*>(dstLayer),
dstFrame);
api.copyCel(srcLayer, srcFrame, dstLayer, dstFrame);
}
else {
if (Cel* dstCel = dstLayer->cel(dstFrame))
@ -741,15 +738,8 @@ void Clipboard::paste(Context* ctx, const bool interactive, const gfx::Point* po
auto srcLayer = *srcIt;
auto dstLayer = *dstIt;
if (!srcLayer->isImage() || !dstLayer->isImage())
continue;
Cel* cel = static_cast<LayerImage*>(srcLayer)->cel(srcFrame);
if (cel && cel->image()) {
api.copyCel(static_cast<LayerImage*>(srcLayer),
srcFrame,
static_cast<LayerImage*>(dstLayer),
dstFrame);
if (Cel* cel = srcLayer->cel(srcFrame)) {
api.copyCel(srcLayer, srcFrame, dstLayer, dstFrame);
}
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -18,6 +18,8 @@
#include "app/cmd/copy_rect.h"
#include "app/cmd/copy_region.h"
#include "app/cmd/patch_cel.h"
#include "app/cmd/set_cel_image.h"
#include "app/cmd/set_cel_position.h"
#include "app/cmd_sequence.h"
#include "app/context.h"
#include "app/doc.h"
@ -220,7 +222,9 @@ void ExpandCelCanvas::commit()
}
// Was the cel created in the start of the tool-loop?
if (m_celCreated) {
if ((m_celCreated) ||
// Was the cel without an image when the tool-loop started?
(m_dstImage && !m_celImage)) {
ASSERT(m_cel);
ASSERT(!m_celImage);
@ -230,7 +234,8 @@ void ExpandCelCanvas::commit()
if (previewSpecificLayerChanges()) {
// We can temporary remove the cel.
static_cast<LayerImage*>(m_layer)->removeCel(m_cel);
if (m_celCreated)
m_layer->removeCel(m_cel);
gfx::Rect trimBounds = getTrimDstImageBounds();
if (!trimBounds.isEmpty()) {
@ -251,16 +256,26 @@ void ExpandCelCanvas::commit()
ImageRef newImage(trimDstImage(trimBounds));
ASSERT(newImage);
m_cel->data()->setImage(newImage, m_layer);
m_cel->setPosition(m_cel->position() + (m_layer->isTilemap() ?
// TODO we should get the exact coordinate from
// getTrimDstImageBounds()
m_grid.tileToCanvas(trimBounds.origin()) :
trimBounds.origin()));
gfx::Point newPosition = (m_cel->position() +
// TODO we should get the exact coordinate from
// getTrimDstImageBounds()
(m_layer->isTilemap() ?
m_grid.tileToCanvas(trimBounds.origin()) :
trimBounds.origin()));
if (m_celCreated) {
m_cel->data()->setImage(newImage, m_layer);
m_cel->setPosition(newPosition);
}
else {
m_cmds->executeAndAdd(new cmd::SetCelImage(m_cel, newImage));
m_cmds->executeAndAdd(new cmd::SetCelPosition(m_cel, newPosition.x, newPosition.y));
}
}
// And add the cel again in the layer.
m_cmds->executeAndAdd(new cmd::AddCel(m_layer, m_cel));
if (m_celCreated)
m_cmds->executeAndAdd(new cmd::AddCel(m_layer, m_cel));
}
else {
// Delete unused cel

View File

@ -610,7 +610,7 @@ doc::Layer* AsepriteDecoder::readLayerChunk(AsepriteHeader* header,
else if (child_level > *current_level)
static_cast<doc::LayerGroup*>(*previous_layer)->addLayer(layer);
else if (child_level < *current_level) {
doc::LayerGroup* parent = (*previous_layer)->parent();
doc::Layer* parent = (*previous_layer)->parent();
ASSERT(parent);
if (parent) {
int levels = (*current_level - child_level);

View File

@ -1,5 +1,5 @@
# Aseprite Document Library
# Copyright (C) 2019-2024 Igara Studio S.A.
# Copyright (C) 2019-2025 Igara Studio S.A.
# Copyright (C) 2001-2018 David Capello
if(WIN32)
@ -47,9 +47,17 @@ add_library(doc-lib
image_io.cpp
image_iterators2.cpp
layer.cpp
layer_audio.cpp
layer_fill.cpp
layer_fx.cpp
layer_hitbox.cpp
layer_io.cpp
layer_list.cpp
layer_mask.cpp
layer_subsprite.cpp
layer_text.cpp
layer_tilemap.cpp
layer_vector.cpp
mask.cpp
mask_boundaries.cpp
mask_io.cpp

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2019-2024 Igara Studio S.A.
// Copyright (c) 2019-2025 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -147,7 +147,7 @@ std::size_t Cel::links() const
return links;
}
void Cel::setParentLayer(LayerImage* layer)
void Cel::setParentLayer(Layer* layer)
{
m_layer = layer;
fixupImage();

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2019-2024 Igara Studio S.A.
// Copyright (c) 2019-2025 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -21,9 +21,16 @@ namespace doc {
class Document;
class Grid;
class LayerImage;
class Layer;
class Sprite;
// Short for celluloid. Initially used to represent an image in a
// specific layer/frame location, but it could refer any kind of data
// for other layers in a specific frame (e.g. a block of audio).
//
// This means that the image of a cel could be nullptr for layers that
// don't require it (e.g. an audio layer) or could be a rendered cache
// of the layer content (e.g. a text layer).
class Cel : public Object {
public:
Cel(frame_t frame, const ImageRef& image);
@ -43,8 +50,9 @@ public:
gfx::Rect imageBounds() const { return m_data->imageBounds(); }
LayerImage* layer() const { return m_layer; }
Layer* layer() const { return m_layer; }
Image* image() const { return m_data->image(); }
ObjectId imageId() const { return m_data->image() ? m_data->image()->id() : NullId; }
ImageRef imageRef() const { return m_data->imageRef(); }
CelData* data() const { return const_cast<CelData*>(m_data.get()); }
CelDataRef dataRef() const { return m_data; }
@ -55,7 +63,7 @@ public:
// You should change the frame only if the cel isn't member of a
// layer. If the cel is already in a layer, you should use
// LayerImage::moveCel() member function.
// Layer::moveCel() member function.
void setFrame(frame_t frame);
void setDataRef(const CelDataRef& celData);
void setPosition(int x, int y);
@ -67,7 +75,7 @@ public:
virtual int getMemSize() const override { return sizeof(Cel) + m_data->getMemSize(); }
void setParentLayer(LayerImage* layer);
void setParentLayer(Layer* layer);
Grid grid() const;
// Copies properties that are not shared between linked cels
@ -77,7 +85,7 @@ public:
private:
void fixupImage();
LayerImage* m_layer;
Layer* m_layer;
frame_t m_frame; // Frame position
CelDataRef m_data;
int m_zIndex = 0;

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2019-2022 Igara Studio S.A.
// Copyright (c) 2019-2025 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -44,8 +44,6 @@ CelData::~CelData()
void CelData::setImage(const ImageRef& image, Layer* layer)
{
ASSERT(image.get());
m_image = image;
adjustBounds(layer);
}
@ -57,9 +55,40 @@ void CelData::setPosition(const gfx::Point& pos)
m_boundsF->setOrigin(gfx::PointF(pos));
}
void CelData::setBounds(const gfx::Rect& bounds)
{
#if _DEBUG
if (m_image) {
ASSERT(bounds.w > 0);
ASSERT(bounds.h > 0);
}
#endif
m_bounds = bounds;
if (m_boundsF)
*m_boundsF = gfx::RectF(bounds);
}
void CelData::setBoundsF(const gfx::RectF& boundsF)
{
if (m_boundsF)
*m_boundsF = boundsF;
else
m_boundsF = std::make_unique<gfx::RectF>(boundsF);
m_bounds = gfx::Rect(boundsF);
if (m_bounds.w <= 0)
m_bounds.w = 1;
if (m_bounds.h <= 0)
m_bounds.h = 1;
}
void CelData::adjustBounds(Layer* layer)
{
ASSERT(m_image);
if (!m_image) {
m_bounds = {};
return;
}
if (m_image->pixelFormat() == IMAGE_TILEMAP) {
Tileset* tileset = nullptr;
if (layer && layer->isTilemap())

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -31,6 +31,7 @@ public:
const gfx::Rect& bounds() const { return m_bounds; }
int opacity() const { return m_opacity; }
Image* image() const { return const_cast<Image*>(m_image.get()); };
ObjectId imageId() const { return m_image ? m_image->id() : NullId; };
ImageRef imageRef() const { return m_image; }
// Returns a rectangle with the bounds of the image (width/height
@ -39,7 +40,9 @@ public:
// bounds).
gfx::Rect imageBounds() const
{
return gfx::Rect(m_bounds.x, m_bounds.y, m_image->width(), m_image->height());
if (m_image)
return gfx::Rect(m_bounds.x, m_bounds.y, m_image->width(), m_image->height());
return {};
}
void setImage(const ImageRef& image, Layer* layer);
@ -47,28 +50,8 @@ public:
void setOpacity(int opacity) { m_opacity = opacity; }
void setBounds(const gfx::Rect& bounds)
{
ASSERT(bounds.w > 0);
ASSERT(bounds.h > 0);
m_bounds = bounds;
if (m_boundsF)
*m_boundsF = gfx::RectF(bounds);
}
void setBoundsF(const gfx::RectF& boundsF)
{
if (m_boundsF)
*m_boundsF = boundsF;
else
m_boundsF = std::make_unique<gfx::RectF>(boundsF);
m_bounds = gfx::Rect(boundsF);
if (m_bounds.w <= 0)
m_bounds.w = 1;
if (m_bounds.h <= 0)
m_bounds.h = 1;
}
void setBounds(const gfx::Rect& bounds);
void setBoundsF(const gfx::RectF& boundsF);
const gfx::RectF& boundsF() const
{
@ -81,8 +64,7 @@ public:
virtual int getMemSize() const override
{
ASSERT(m_image);
return sizeof(CelData) + m_image->getMemSize();
return sizeof(CelData) + (m_image ? m_image->getMemSize() : 0);
}
void adjustBounds(Layer* layer);
@ -97,7 +79,7 @@ private:
mutable std::unique_ptr<gfx::RectF> m_boundsF;
};
typedef std::shared_ptr<CelData> CelDataRef;
using CelDataRef = std::shared_ptr<CelData>;
} // namespace doc

View File

@ -35,7 +35,7 @@ void write_celdata(std::ostream& os, const CelData* celdata)
write32(os, celdata->bounds().w);
write32(os, celdata->bounds().h);
write8(os, celdata->opacity());
write32(os, celdata->image()->id());
write32(os, celdata->imageId());
write_user_data(os, celdata->userData());
if (celdata->hasBoundsF()) { // Reference layer
@ -78,9 +78,12 @@ CelData* read_celdata(std::istream& is,
}
}
ImageRef image(subObjects->getImageRef(imageId));
if (!image)
return nullptr;
ImageRef image;
if (imageId) {
image = subObjects->getImageRef(imageId);
if (!image)
return nullptr;
}
std::unique_ptr<CelData> celdata(new CelData(image));
celdata->setBounds(gfx::Rect(x, y, w, h));

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2023 Igara Studio S.A.
// Copyright (c) 2023-2025 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -27,7 +27,7 @@ void write_cel(std::ostream& os, const Cel* cel)
{
write32(os, cel->id());
write16(os, cel->frame());
write32(os, cel->dataRef()->id());
write32(os, cel->data() ? cel->data()->id() : 0);
write16(os, uint16_t(int16_t(cel->zIndex())));
}
@ -40,9 +40,12 @@ Cel* read_cel(std::istream& is, SubObjectsIO* subObjects, bool setId)
if (is.eof())
zIndex = 0;
CelDataRef celData(subObjects->getCelDataRef(celDataId));
if (!celData)
return nullptr;
CelDataRef celData;
if (celDataId) {
celData = subObjects->getCelDataRef(celDataId);
if (!celData)
return nullptr;
}
auto cel = std::make_unique<Cel>(frame, celData);
cel->setZIndex(zIndex);

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2019-2023 Igara Studio S.A.
// Copyright (c) 2019-2025 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -42,14 +42,12 @@ CelsRange::iterator::iterator(const Sprite* sprite,
// Get first cel
Layer* layer = sprite->root()->firstLayerInWholeHierarchy();
while (layer && !m_cel) {
if (layer->isImage()) {
m_frameIterator = m_selFrames.begin();
auto endFrame = m_selFrames.end();
for (; m_frameIterator != endFrame; ++m_frameIterator) {
m_cel = layer->cel(*m_frameIterator);
if (m_cel)
break;
}
m_frameIterator = m_selFrames.begin();
auto endFrame = m_selFrames.end();
for (; m_frameIterator != endFrame; ++m_frameIterator) {
m_cel = layer->cel(*m_frameIterator);
if (m_cel)
break;
}
if (!m_cel)
@ -72,21 +70,19 @@ CelsRange::iterator& CelsRange::iterator::operator++()
Layer* layer = m_cel->layer();
m_cel = nullptr;
while (layer && !m_cel) {
if (layer->isImage()) {
for (; m_frameIterator != endFrame; ++m_frameIterator) {
m_cel = layer->cel(*m_frameIterator);
if (m_cel) {
if (m_flags == CelsRange::UNIQUE) {
if (m_visited.find(m_cel->data()->id()) == m_visited.end()) {
m_visited.insert(m_cel->data()->id());
break;
}
else
m_cel = nullptr;
for (; m_frameIterator != endFrame; ++m_frameIterator) {
m_cel = layer->cel(*m_frameIterator);
if (m_cel) {
if (m_flags == CelsRange::UNIQUE) {
if (m_visited.find(m_cel->data()->id()) == m_visited.end()) {
m_visited.insert(m_cel->data()->id());
break;
}
else
break;
m_cel = nullptr;
}
else
break;
}
}

View File

@ -32,19 +32,29 @@ Layer::Layer(ObjectType type, Sprite* sprite)
, m_blendmode(BlendMode::NORMAL)
, m_opacity(255)
{
ASSERT(type == ObjectType::LayerImage || type == ObjectType::LayerGroup ||
type == ObjectType::LayerTilemap);
setName("Layer");
}
Layer::~Layer()
{
destroyAllCels();
destroyAllLayers();
}
int Layer::getMemSize() const
{
return sizeof(Layer);
int size = sizeof(Layer);
for (const Cel* cel : m_cels) {
if (cel->link()) // Skip link
continue;
size += cel->getMemSize();
}
for (const Layer* layer : m_layers)
size += layer->getMemSize();
return size;
}
Layer* Layer::getPrevious() const
@ -78,14 +88,14 @@ Layer* Layer::getPreviousBrowsable() const
{
// Go to children
if (isBrowsable())
return static_cast<const LayerGroup*>(this)->lastLayer();
return lastLayer();
// Go to previous layer
if (Layer* prev = getPrevious())
return prev;
// Go to previous layer in the parent
LayerGroup* parent = this->parent();
Layer* parent = this->parent();
while (parent != sprite()->root() && !parent->getPrevious()) {
parent = parent->parent();
}
@ -98,7 +108,7 @@ Layer* Layer::getNextBrowsable() const
if (Layer* next = getNext()) {
// Go to children
while (next->isBrowsable()) {
Layer* firstChild = static_cast<const LayerGroup*>(next)->firstLayer();
Layer* firstChild = next->firstLayer();
if (!firstChild)
break;
next = firstChild;
@ -116,15 +126,15 @@ Layer* Layer::getNextBrowsable() const
Layer* Layer::getPreviousInWholeHierarchy() const
{
// Go to children
if (isGroup() && static_cast<const LayerGroup*>(this)->layersCount() > 0)
return static_cast<const LayerGroup*>(this)->lastLayer();
if (layersCount() > 0)
return lastLayer();
// Go to previous layer
if (Layer* prev = getPrevious())
return prev;
// Go to previous layer in the parent
LayerGroup* parent = this->parent();
Layer* parent = this->parent();
while (parent != sprite()->root() && !parent->getPrevious()) {
parent = parent->parent();
}
@ -136,8 +146,8 @@ Layer* Layer::getNextInWholeHierarchy() const
// Go to next layer
if (Layer* next = getNext()) {
// Go to children
while (next->isGroup() && static_cast<const LayerGroup*>(next)->layersCount() > 0) {
Layer* firstChild = static_cast<const LayerGroup*>(next)->firstLayer();
while (next->hasSublayers() && next->layersCount() > 0) {
Layer* firstChild = next->firstLayer();
if (!firstChild)
break;
next = firstChild;
@ -212,88 +222,49 @@ Grid Layer::grid() const
Cel* Layer::cel(frame_t frame) const
{
const CelConstIterator it = findCelIterator(frame);
if (it != getCelEnd())
return *it;
return nullptr;
}
//////////////////////////////////////////////////////////////////////
// LayerImage class
LayerImage::LayerImage(ObjectType type, Sprite* sprite) : Layer(type, sprite)
void Layer::destroyAllCels()
{
}
LayerImage::LayerImage(Sprite* sprite) : LayerImage(ObjectType::LayerImage, sprite)
{
}
LayerImage::~LayerImage()
{
destroyAllCels();
}
int LayerImage::getMemSize() const
{
int size = sizeof(LayerImage);
CelConstIterator it = getCelBegin();
CelConstIterator end = getCelEnd();
for (; it != end; ++it) {
const Cel* cel = *it;
if (cel->link()) // Skip link
continue;
size += cel->getMemSize();
}
return size;
}
void LayerImage::destroyAllCels()
{
CelIterator it = getCelBegin();
CelIterator end = getCelEnd();
for (; it != end; ++it) {
Cel* cel = *it;
for (Cel* cel : m_cels)
delete cel;
}
m_cels.clear();
}
Cel* LayerImage::cel(frame_t frame) const
void Layer::destroyAllLayers()
{
CelConstIterator it = findCelIterator(frame);
if (it != getCelEnd())
return *it;
else
return nullptr;
for (Layer* layer : m_layers)
delete layer;
m_layers.clear();
}
void LayerImage::getCels(CelList& cels) const
void Layer::getCels(CelList& cels) const
{
CelConstIterator it = getCelBegin();
CelConstIterator end = getCelEnd();
for (Cel* cel : m_cels)
cels.push_back(cel);
for (; it != end; ++it)
cels.push_back(*it);
for (const Layer* layer : m_layers)
layer->getCels(cels);
}
Cel* LayerImage::getLastCel() const
Cel* Layer::getLastCel() const
{
if (!m_cels.empty())
return m_cels.back();
else
return NULL;
return nullptr;
}
CelConstIterator LayerImage::findCelIterator(frame_t frame) const
CelConstIterator Layer::findCelIterator(frame_t frame) const
{
CelIterator it = const_cast<LayerImage*>(this)->findCelIterator(frame);
const CelIterator it = const_cast<Layer*>(this)->findCelIterator(frame);
return CelConstIterator(it);
}
CelIterator LayerImage::findCelIterator(frame_t frame)
CelIterator Layer::findCelIterator(frame_t frame)
{
auto first = getCelBegin();
auto end = getCelEnd();
@ -310,7 +281,7 @@ CelIterator LayerImage::findCelIterator(frame_t frame)
return end;
}
CelIterator LayerImage::findFirstCelIteratorAfter(frame_t firstAfterFrame)
CelIterator Layer::findFirstCelIteratorAfter(frame_t firstAfterFrame)
{
auto first = getCelBegin();
auto end = getCelEnd();
@ -323,14 +294,11 @@ CelIterator LayerImage::findFirstCelIteratorAfter(frame_t firstAfterFrame)
return first;
}
void LayerImage::addCel(Cel* cel)
void Layer::addCel(Cel* cel)
{
ASSERT(cel);
ASSERT(cel->data() && "The cel doesn't contain CelData");
ASSERT(cel->image());
ASSERT(sprite());
ASSERT(cel->image()->pixelFormat() == sprite()->pixelFormat() ||
cel->image()->pixelFormat() == IMAGE_TILEMAP);
CelIterator it = findFirstCelIteratorAfter(cel->frame());
m_cels.insert(it, cel);
@ -344,7 +312,7 @@ void LayerImage::addCel(Cel* cel)
* It doesn't destroy the cel, you have to delete it after calling
* this routine.
*/
void LayerImage::removeCel(Cel* cel)
void Layer::removeCel(Cel* cel)
{
ASSERT(cel);
CelIterator it = findCelIterator(cel->frame());
@ -355,7 +323,7 @@ void LayerImage::removeCel(Cel* cel)
cel->setParentLayer(NULL);
}
void LayerImage::moveCel(Cel* cel, frame_t frame)
void Layer::moveCel(Cel* cel, frame_t frame)
{
removeCel(cel);
cel->setFrame(frame);
@ -363,6 +331,221 @@ void LayerImage::moveCel(Cel* cel, frame_t frame)
addCel(cel);
}
void Layer::displaceFrames(frame_t fromThis, frame_t delta)
{
Sprite* sprite = this->sprite();
if (delta > 0) {
for (frame_t c = sprite->lastFrame(); c >= fromThis; --c) {
if (Cel* cel = this->cel(c))
moveCel(cel, c + delta);
}
}
else {
for (frame_t c = fromThis; c <= sprite->lastFrame(); ++c) {
if (Cel* cel = this->cel(c))
moveCel(cel, c + delta);
}
}
for (Layer* layer : m_layers)
layer->displaceFrames(fromThis, delta);
}
Layer* Layer::firstLayerInWholeHierarchy() const
{
Layer* layer = firstLayer();
if (layer) {
while (layer->layersCount() > 0)
layer = layer->firstLayer();
}
return layer;
}
void Layer::allLayers(LayerList& list) const
{
for (Layer* child : m_layers) {
if (child->hasSublayers())
child->allLayers(list);
list.push_back(child);
}
}
layer_t Layer::allLayersCount() const
{
layer_t count = 0;
for (Layer* child : m_layers) {
count += child->allLayersCount();
++count;
}
return count;
}
bool Layer::hasVisibleReferenceLayers() const
{
for (Layer* child : m_layers) {
if ((child->isReference() && child->isVisible()) ||
(child->hasSublayers() && child->hasVisibleReferenceLayers()))
return true;
}
return false;
}
void Layer::allVisibleLayers(LayerList& list) const
{
for (Layer* child : m_layers) {
if (!child->isVisible())
continue;
if (child->hasSublayers())
child->allVisibleLayers(list);
list.push_back(child);
}
}
void Layer::allVisibleReferenceLayers(LayerList& list) const
{
for (Layer* child : m_layers) {
if (!child->isVisible())
continue;
if (child->hasSublayers())
child->allVisibleReferenceLayers(list);
if (!child->isReference())
continue;
list.push_back(child);
}
}
void Layer::allBrowsableLayers(LayerList& list) const
{
for (Layer* child : m_layers) {
if (child->isBrowsable())
child->allBrowsableLayers(list);
list.push_back(child);
}
}
void Layer::allTilemaps(LayerList& list) const
{
for (Layer* child : m_layers) {
if (child->hasSublayers())
child->allTilemaps(list);
if (child->isTilemap())
list.push_back(child);
}
}
std::string Layer::visibleLayerHierarchyAsString(const std::string& indent) const
{
std::string str;
for (Layer* child : m_layers) {
if (!child->isVisible())
continue;
str += indent + child->name() + (child->isGroup() ? "/" : "") + "\n";
if (child->hasSublayers())
str += child->visibleLayerHierarchyAsString(indent + " ");
}
return str;
}
void Layer::addLayer(Layer* layer)
{
m_layers.push_back(layer);
layer->setParent(this);
if (!isGroup())
setCollapsed(m_layers.empty());
}
void Layer::removeLayer(Layer* layer)
{
auto it = std::find(m_layers.begin(), m_layers.end(), layer);
ASSERT(it != m_layers.end());
m_layers.erase(it);
layer->setParent(nullptr);
if (!isGroup())
setCollapsed(m_layers.empty());
}
void Layer::insertLayer(Layer* layer, Layer* after)
{
auto after_it = m_layers.begin();
if (after) {
after_it = std::find(m_layers.begin(), m_layers.end(), after);
if (after_it != m_layers.end())
++after_it;
}
m_layers.insert(after_it, layer);
layer->setParent(this);
if (!isGroup())
setCollapsed(m_layers.empty());
}
void Layer::insertLayerBefore(Layer* layer, Layer* before)
{
auto before_it = m_layers.end();
if (before) {
before_it = std::find(m_layers.begin(), m_layers.end(), before);
}
m_layers.insert(before_it, layer);
layer->setParent(this);
if (!isGroup())
setCollapsed(m_layers.empty());
}
void Layer::stackLayer(Layer* layer, Layer* after)
{
ASSERT(layer != after);
if (layer == after)
return;
removeLayer(layer);
insertLayer(layer, after);
}
layer_t Layer::getLayerIndex(const Layer* layer, layer_t& index) const
{
for (Layer* child : this->layers()) {
if ((child->hasSublayers() && child->getLayerIndex(layer, index) != -1) || (child == layer)) {
return index;
}
index++;
}
return -1;
}
layer_t Layer::getLayerIndex(const Layer* layer) const
{
layer_t index = 0;
return this->getLayerIndex(layer, index);
}
//////////////////////////////////////////////////////////////////////
// LayerImage class
LayerImage::LayerImage(ObjectType type, Sprite* sprite) : Layer(type, sprite)
{
}
LayerImage::LayerImage(Sprite* sprite) : LayerImage(ObjectType::LayerImage, sprite)
{
}
LayerImage::~LayerImage()
{
}
/**
* Configures some properties of the specified layer to make it as the
* "Background" of the sprite.
@ -381,24 +564,6 @@ void LayerImage::configureAsBackground()
sprite()->root()->stackLayer(this, NULL);
}
void LayerImage::displaceFrames(frame_t fromThis, frame_t delta)
{
Sprite* sprite = this->sprite();
if (delta > 0) {
for (frame_t c = sprite->lastFrame(); c >= fromThis; --c) {
if (Cel* cel = this->cel(c))
moveCel(cel, c + delta);
}
}
else {
for (frame_t c = fromThis; c <= sprite->lastFrame(); ++c) {
if (Cel* cel = this->cel(c))
moveCel(cel, c + delta);
}
}
}
//////////////////////////////////////////////////////////////////////
// LayerGroup class
@ -409,210 +574,6 @@ LayerGroup::LayerGroup(Sprite* sprite) : Layer(ObjectType::LayerGroup, sprite)
LayerGroup::~LayerGroup()
{
destroyAllLayers();
}
void LayerGroup::destroyAllLayers()
{
for (Layer* layer : m_layers)
delete layer;
m_layers.clear();
}
int LayerGroup::getMemSize() const
{
int size = sizeof(LayerGroup);
for (const Layer* layer : m_layers) {
size += layer->getMemSize();
}
return size;
}
Layer* LayerGroup::firstLayerInWholeHierarchy() const
{
Layer* layer = firstLayer();
if (layer) {
while (layer->isGroup() && static_cast<LayerGroup*>(layer)->layersCount() > 0) {
layer = static_cast<LayerGroup*>(layer)->firstLayer();
}
}
return layer;
}
void LayerGroup::allLayers(LayerList& list) const
{
for (Layer* child : m_layers) {
if (child->isGroup())
static_cast<LayerGroup*>(child)->allLayers(list);
list.push_back(child);
}
}
layer_t LayerGroup::allLayersCount() const
{
layer_t count = 0;
for (Layer* child : m_layers) {
if (child->isGroup())
count += static_cast<LayerGroup*>(child)->allLayersCount();
++count;
}
return count;
}
bool LayerGroup::hasVisibleReferenceLayers() const
{
for (Layer* child : m_layers) {
if ((child->isReference() && child->isVisible()) ||
(child->isGroup() && static_cast<LayerGroup*>(child)->hasVisibleReferenceLayers()))
return true;
}
return false;
}
void LayerGroup::allVisibleLayers(LayerList& list) const
{
for (Layer* child : m_layers) {
if (!child->isVisible())
continue;
if (child->isGroup())
static_cast<LayerGroup*>(child)->allVisibleLayers(list);
list.push_back(child);
}
}
void LayerGroup::allVisibleReferenceLayers(LayerList& list) const
{
for (Layer* child : m_layers) {
if (!child->isVisible())
continue;
if (child->isGroup())
static_cast<LayerGroup*>(child)->allVisibleReferenceLayers(list);
if (!child->isReference())
continue;
list.push_back(child);
}
}
void LayerGroup::allBrowsableLayers(LayerList& list) const
{
for (Layer* child : m_layers) {
if (child->isBrowsable())
static_cast<LayerGroup*>(child)->allBrowsableLayers(list);
list.push_back(child);
}
}
void LayerGroup::allTilemaps(LayerList& list) const
{
for (Layer* child : m_layers) {
if (child->isGroup())
static_cast<LayerGroup*>(child)->allTilemaps(list);
if (child->isTilemap())
list.push_back(child);
}
}
std::string LayerGroup::visibleLayerHierarchyAsString(const std::string& indent) const
{
std::string str;
for (Layer* child : m_layers) {
if (!child->isVisible())
continue;
str += indent + child->name() + (child->isGroup() ? "/" : "") + "\n";
if (child->isGroup())
str += static_cast<LayerGroup*>(child)->visibleLayerHierarchyAsString(indent + " ");
}
return str;
}
void LayerGroup::getCels(CelList& cels) const
{
for (const Layer* layer : m_layers)
layer->getCels(cels);
}
void LayerGroup::addLayer(Layer* layer)
{
m_layers.push_back(layer);
layer->setParent(this);
}
void LayerGroup::removeLayer(Layer* layer)
{
auto it = std::find(m_layers.begin(), m_layers.end(), layer);
ASSERT(it != m_layers.end());
m_layers.erase(it);
layer->setParent(nullptr);
}
void LayerGroup::insertLayer(Layer* layer, Layer* after)
{
auto after_it = m_layers.begin();
if (after) {
after_it = std::find(m_layers.begin(), m_layers.end(), after);
if (after_it != m_layers.end())
++after_it;
}
m_layers.insert(after_it, layer);
layer->setParent(this);
}
void LayerGroup::insertLayerBefore(Layer* layer, Layer* before)
{
auto before_it = m_layers.end();
if (before) {
before_it = std::find(m_layers.begin(), m_layers.end(), before);
}
m_layers.insert(before_it, layer);
layer->setParent(this);
}
void LayerGroup::stackLayer(Layer* layer, Layer* after)
{
ASSERT(layer != after);
if (layer == after)
return;
removeLayer(layer);
insertLayer(layer, after);
}
void LayerGroup::displaceFrames(frame_t fromThis, frame_t delta)
{
for (Layer* layer : m_layers)
layer->displaceFrames(fromThis, delta);
}
layer_t LayerGroup::getLayerIndex(const Layer* layer, layer_t& index) const
{
for (Layer* child : this->layers()) {
if ((child->isGroup() && static_cast<LayerGroup*>(child)->getLayerIndex(layer, index) != -1) ||
(child == layer)) {
return index;
}
index++;
}
return -1;
}
layer_t LayerGroup::getLayerIndex(const Layer* layer) const
{
layer_t index = 0;
return this->getLayerIndex(layer, index);
}
} // namespace doc

View File

@ -55,9 +55,13 @@ enum class LayerFlags {
class Layer : public WithUserData {
protected:
// Only constructured by derived classes
Layer(ObjectType type, Sprite* sprite);
public:
// Disable assigment
Layer& operator=(const Layer& other) = delete;
virtual ~Layer();
virtual int getMemSize() const override;
@ -66,8 +70,8 @@ public:
void setName(const std::string& name) { m_name = name; }
Sprite* sprite() const { return m_sprite; }
LayerGroup* parent() const { return m_parent; }
void setParent(LayerGroup* group) { m_parent = group; }
Layer* parent() const { return m_parent; }
void setParent(Layer* parent) { m_parent = parent; }
// Gets the previous sibling of this layer.
Layer* getPrevious() const;
@ -83,9 +87,22 @@ public:
{
return (type() == ObjectType::LayerImage || type() == ObjectType::LayerTilemap);
}
bool hasSublayers() const { return !m_layers.empty(); }
bool isGroup() const { return type() == ObjectType::LayerGroup; }
bool isTilemap() const { return type() == ObjectType::LayerTilemap; }
virtual bool isBrowsable() const { return false; }
bool isBrowsable() const { return isExpanded() && !m_layers.empty(); }
// If this layer is a Mask or FX
bool isMaskOrFx() const
{
return type() == ObjectType::LayerMask || type() == ObjectType::LayerFx;
}
// In case we can drop a Mask or FX inside this kind of layer
bool acceptMaskOrFx() const
{
return type() == ObjectType::LayerImage || type() == ObjectType::LayerTilemap ||
type() == ObjectType::LayerText || type() == ObjectType::LayerVector ||
type() == ObjectType::LayerSubsprite;
}
bool isBackground() const { return hasFlags(LayerFlags::Background); }
bool isTransparent() const { return !hasFlags(LayerFlags::Background); }
@ -143,71 +160,31 @@ public:
}
virtual Grid grid() const;
virtual Cel* cel(frame_t frame) const;
virtual void getCels(CelList& cels) const = 0;
virtual void displaceFrames(frame_t fromThis, frame_t delta) = 0;
private:
std::string m_name; // layer name
Sprite* m_sprite; // owner of the layer
LayerGroup* m_parent; // parent layer
LayerFlags m_flags; // stack order cannot be changed
mutable base::Uuid m_uuid; // lazily generated layer's UUID
BlendMode m_blendmode;
int m_opacity;
// Disable assigment
Layer& operator=(const Layer& other);
};
//////////////////////////////////////////////////////////////////////
// LayerImage class
class LayerImage : public Layer {
public:
LayerImage(ObjectType type, Sprite* sprite);
explicit LayerImage(Sprite* sprite);
virtual ~LayerImage();
virtual int getMemSize() const override;
// Cels management
void addCel(Cel* cel);
void removeCel(Cel* cel);
void moveCel(Cel* cel, frame_t frame);
Cel* cel(frame_t frame) const override;
void getCels(CelList& cels) const override;
void displaceFrames(frame_t fromThis, frame_t delta) override;
Cel* cel(frame_t frame) const;
const CelList& cels() const { return m_cels; }
virtual void getCels(CelList& cels) const;
virtual void displaceFrames(frame_t fromThis, frame_t delta);
Cel* getLastCel() const;
CelConstIterator findCelIterator(frame_t frame) const;
CelIterator findCelIterator(frame_t frame);
CelIterator findFirstCelIteratorAfter(frame_t firstAfterFrame);
void configureAsBackground();
CelIterator getCelBegin() { return m_cels.begin(); }
CelIterator getCelEnd() { return m_cels.end(); }
CelConstIterator getCelBegin() const { return m_cels.begin(); }
CelConstIterator getCelEnd() const { return m_cels.end(); }
int getCelsCount() const { return (int)m_cels.size(); }
private:
void destroyAllCels();
CelList m_cels; // List of all cels inside this layer used by frames.
};
//////////////////////////////////////////////////////////////////////
// LayerGroup class
class LayerGroup : public Layer {
public:
explicit LayerGroup(Sprite* sprite);
virtual ~LayerGroup();
virtual int getMemSize() const override;
// Sublayers management. Groups accept any kind of
// children. Image/rendered layers accept Mask or FXs.
const LayerList& layers() const { return m_layers; }
int layersCount() const { return (int)m_layers.size(); }
@ -231,21 +208,55 @@ public:
void allTilemaps(LayerList& list) const;
std::string visibleLayerHierarchyAsString(const std::string& indent) const;
void getCels(CelList& cels) const override;
void displaceFrames(frame_t fromThis, frame_t delta) override;
bool isBrowsable() const override { return isGroup() && isExpanded() && !m_layers.empty(); }
layer_t getLayerIndex(const Layer* layer) const;
private:
layer_t getLayerIndex(const Layer* layer, layer_t& index) const;
void destroyAllCels();
void destroyAllLayers();
layer_t getLayerIndex(const Layer* layer, layer_t& index) const;
std::string m_name; // layer name
Sprite* m_sprite; // owner of the layer
Layer* m_parent; // parent layer
LayerFlags m_flags; // stack order cannot be changed
mutable base::Uuid m_uuid; // lazily generated layer's UUID
// Some of these fields might not be used depending on the layer
// kind (e.g. the blend mode and opacity don't make sense for audio
// layers).
BlendMode m_blendmode;
int m_opacity;
// List of all cels inside this layer used by frames.
CelList m_cels;
// List of all layers inside this layer. For a group, this can be a
// list of any kind. For other layers, they might be limited to
// Masks and FXs.
LayerList m_layers;
};
//////////////////////////////////////////////////////////////////////
// LayerImage class
class LayerImage : public Layer {
public:
LayerImage(ObjectType type, Sprite* sprite);
explicit LayerImage(Sprite* sprite);
virtual ~LayerImage();
void configureAsBackground();
};
//////////////////////////////////////////////////////////////////////
// LayerGroup class
class LayerGroup final : public Layer {
public:
explicit LayerGroup(Sprite* sprite);
virtual ~LayerGroup();
};
} // namespace doc
#endif

23
src/doc/layer_audio.cpp Normal file
View File

@ -0,0 +1,23 @@
// Aseprite Document Library
// Copyright (c) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "doc/layer_audio.h"
namespace doc {
LayerAudio::LayerAudio(Sprite* sprite) : Layer(ObjectType::LayerAudio, sprite)
{
}
LayerAudio::~LayerAudio()
{
}
} // namespace doc

26
src/doc/layer_audio.h Normal file
View File

@ -0,0 +1,26 @@
// Aseprite Document Library
// Copyright (C) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef DOC_LAYER_AUDIO_H_INCLUDED
#define DOC_LAYER_AUDIO_H_INCLUDED
#pragma once
#include "doc/layer.h"
namespace doc {
// Starts playing audio at specific times.
class LayerAudio final : public Layer {
public:
explicit LayerAudio(Sprite* sprite);
virtual ~LayerAudio();
// TODO associated audio data from/to frame
};
} // namespace doc
#endif

23
src/doc/layer_fill.cpp Normal file
View File

@ -0,0 +1,23 @@
// Aseprite Document Library
// Copyright (c) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "doc/layer_fill.h"
namespace doc {
LayerFill::LayerFill(Sprite* sprite) : Layer(ObjectType::LayerFill, sprite)
{
}
LayerFill::~LayerFill()
{
}
} // namespace doc

27
src/doc/layer_fill.h Normal file
View File

@ -0,0 +1,27 @@
// Aseprite Document Library
// Copyright (C) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef DOC_LAYER_FILL_H_INCLUDED
#define DOC_LAYER_FILL_H_INCLUDED
#pragma once
#include "doc/layer.h"
namespace doc {
// Fills the whole canvas with a solid color or a gradient.
// TODO to implement
class LayerFill final : public Layer {
public:
LayerFill(Sprite* sprite);
virtual ~LayerFill();
// TODO associated fill data per frame/keyframe
};
} // namespace doc
#endif

23
src/doc/layer_fx.cpp Normal file
View File

@ -0,0 +1,23 @@
// Aseprite Document Library
// Copyright (c) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "doc/layer_fx.h"
namespace doc {
LayerFx::LayerFx(Sprite* sprite) : Layer(ObjectType::LayerFx, sprite)
{
}
LayerFx::~LayerFx()
{
}
} // namespace doc

27
src/doc/layer_fx.h Normal file
View File

@ -0,0 +1,27 @@
// Aseprite Document Library
// Copyright (C) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef DOC_LAYER_FX_H_INCLUDED
#define DOC_LAYER_FX_H_INCLUDED
#pragma once
#include "doc/layer.h"
namespace doc {
// Applies an FX to the next non-FX Layer
// TODO to implement
class LayerFx final : public Layer {
public:
LayerFx(Sprite* sprite);
virtual ~LayerFx();
// TODO associated FX data per frame/keyframe
};
} // namespace doc
#endif

23
src/doc/layer_hitbox.cpp Normal file
View File

@ -0,0 +1,23 @@
// Aseprite Document Library
// Copyright (c) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "doc/layer_hitbox.h"
namespace doc {
LayerHitbox::LayerHitbox(Sprite* sprite) : Layer(ObjectType::LayerHitbox, sprite)
{
}
LayerHitbox::~LayerHitbox()
{
}
} // namespace doc

28
src/doc/layer_hitbox.h Normal file
View File

@ -0,0 +1,28 @@
// Aseprite Document Library
// Copyright (C) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef DOC_LAYER_HITBOX_H_INCLUDED
#define DOC_LAYER_HITBOX_H_INCLUDED
#pragma once
#include "doc/layer.h"
namespace doc {
// Abstract vectorial shapes, data, and events that can be associated
// to a specific frame. Generally with the purpose to define hitboxes
// to be used in a game engine.
class LayerHitbox final : public Layer {
public:
explicit LayerHitbox(Sprite* sprite);
virtual ~LayerHitbox();
// TODO associated hitbox data
};
} // namespace doc
#endif

View File

@ -18,8 +18,16 @@
#include "doc/cel_io.h"
#include "doc/image_io.h"
#include "doc/layer.h"
#include "doc/layer_audio.h"
#include "doc/layer_fill.h"
#include "doc/layer_fx.h"
#include "doc/layer_hitbox.h"
#include "doc/layer_io.h"
#include "doc/layer_mask.h"
#include "doc/layer_subsprite.h"
#include "doc/layer_text.h"
#include "doc/layer_tilemap.h"
#include "doc/layer_vector.h"
#include "doc/sprite.h"
#include "doc/string_io.h"
#include "doc/subobjects_io.h"
@ -46,14 +54,18 @@ void write_layer(std::ostream& os, const Layer* layer)
switch (layer->type()) {
case ObjectType::LayerImage:
case ObjectType::LayerTilemap: {
const LayerImage* imgLayer = static_cast<const LayerImage*>(layer);
CelConstIterator it, begin = imgLayer->getCelBegin();
CelConstIterator end = imgLayer->getCelEnd();
case ObjectType::LayerTilemap:
case ObjectType::LayerText:
case ObjectType::LayerVector:
case ObjectType::LayerAudio:
case ObjectType::LayerHitbox: {
CelConstIterator it;
const CelConstIterator begin = layer->getCelBegin();
const CelConstIterator end = layer->getCelEnd();
// Blend mode & opacity
write16(os, (int)imgLayer->blendMode());
write8(os, imgLayer->opacity());
write16(os, (int)layer->blendMode());
write8(os, layer->opacity());
// Images
int images = 0;
@ -61,7 +73,8 @@ void write_layer(std::ostream& os, const Layer* layer)
for (it = begin; it != end; ++it) {
const Cel* cel = *it;
if (!cel->link()) {
++images;
if (cel->image())
++images;
++celdatas;
}
}
@ -69,7 +82,7 @@ void write_layer(std::ostream& os, const Layer* layer)
write16(os, images);
for (it = begin; it != end; ++it) {
const Cel* cel = *it;
if (!cel->link())
if (!cel->link() && cel->image())
write_image(os, cel->image());
}
@ -81,7 +94,7 @@ void write_layer(std::ostream& os, const Layer* layer)
}
// Cels
write16(os, imgLayer->getCelsCount());
write16(os, layer->getCelsCount());
for (it = begin; it != end; ++it) {
const Cel* cel = *it;
write_cel(os, cel);
@ -119,60 +132,90 @@ Layer* read_layer(std::istream& is, SubObjectsFromSprite* subObjects, const Seri
switch (static_cast<ObjectType>(layer_type)) {
case ObjectType::LayerImage:
case ObjectType::LayerTilemap: {
LayerImage* imgLayer;
if ((static_cast<ObjectType>(layer_type)) == ObjectType::LayerTilemap) {
imgLayer = new LayerTilemap(subObjects->sprite(), 0);
}
else {
imgLayer = new LayerImage(subObjects->sprite());
}
case ObjectType::LayerTilemap:
case ObjectType::LayerFill:
case ObjectType::LayerMask:
case ObjectType::LayerFx:
case ObjectType::LayerText:
case ObjectType::LayerVector:
case ObjectType::LayerAudio:
case ObjectType::LayerSubsprite:
case ObjectType::LayerHitbox: {
// Create layer
layer.reset(imgLayer);
switch ((static_cast<ObjectType>(layer_type))) {
case ObjectType::LayerImage:
layer = std::make_unique<LayerImage>(subObjects->sprite());
break;
case ObjectType::LayerTilemap:
layer = std::make_unique<LayerTilemap>(subObjects->sprite(), 0);
break;
case ObjectType::LayerFill:
layer = std::make_unique<LayerFill>(subObjects->sprite());
break;
case ObjectType::LayerMask:
layer = std::make_unique<LayerMask>(subObjects->sprite());
break;
case ObjectType::LayerFx: layer = std::make_unique<LayerFx>(subObjects->sprite()); break;
case ObjectType::LayerText:
layer = std::make_unique<LayerText>(subObjects->sprite());
break;
case ObjectType::LayerVector:
layer = std::make_unique<LayerVector>(subObjects->sprite());
break;
case ObjectType::LayerAudio:
layer = std::make_unique<LayerAudio>(subObjects->sprite());
break;
case ObjectType::LayerSubsprite:
layer = std::make_unique<LayerSubsprite>(subObjects->sprite());
break;
case ObjectType::LayerHitbox:
layer = std::make_unique<LayerHitbox>(subObjects->sprite());
break;
}
// Blend mode & opacity
imgLayer->setBlendMode((BlendMode)read16(is));
imgLayer->setOpacity(read8(is));
layer->setBlendMode((BlendMode)read16(is));
layer->setOpacity(read8(is));
// Read images
int images = read16(is); // Number of images
const int images = read16(is); // Number of images
for (int c = 0; c < images; ++c) {
ImageRef image(read_image(is));
subObjects->addImageRef(image);
}
// Read celdatas
int celdatas = read16(is);
const int celdatas = read16(is);
for (int c = 0; c < celdatas; ++c) {
CelDataRef celdata(read_celdata(is, subObjects, true, serial));
subObjects->addCelDataRef(celdata);
}
// Read cels
int cels = read16(is); // Number of cels
const int cels = read16(is); // Number of cels
for (int c = 0; c < cels; ++c) {
// Read the cel
Cel* cel = read_cel(is, subObjects);
ASSERT(cel);
// Add the cel in the layer
imgLayer->addCel(cel);
layer->addCel(cel);
}
// Create the layer tilemap
if (imgLayer->isTilemap()) {
doc::tileset_index tsi = read32(is); // Tileset index
static_cast<LayerTilemap*>(imgLayer)->setTilesetIndex(tsi);
if (layer->isTilemap()) {
const doc::tileset_index tsi = read32(is); // Tileset index
static_cast<LayerTilemap*>(layer.get())->setTilesetIndex(tsi);
}
break;
}
case ObjectType::LayerGroup: {
// Create the layer group
layer.reset(new LayerGroup(subObjects->sprite()));
layer = std::make_unique<LayerGroup>(subObjects->sprite());
// Number of sub-layers
int layers = read16(is);
const int layers = read16(is);
for (int c = 0; c < layers; c++) {
Layer* child = read_layer(is, subObjects, serial);
if (child)

23
src/doc/layer_mask.cpp Normal file
View File

@ -0,0 +1,23 @@
// Aseprite Document Library
// Copyright (c) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "doc/layer_mask.h"
namespace doc {
LayerMask::LayerMask(Sprite* sprite) : LayerImage(ObjectType::LayerMask, sprite)
{
}
LayerMask::~LayerMask()
{
}
} // namespace doc

25
src/doc/layer_mask.h Normal file
View File

@ -0,0 +1,25 @@
// Aseprite Document Library
// Copyright (C) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef DOC_LAYER_MASK_H_INCLUDED
#define DOC_LAYER_MASK_H_INCLUDED
#pragma once
#include "doc/layer.h"
namespace doc {
// Applies Cel images as a mask for the next layer.
// TODO to implement
class LayerMask final : public LayerImage {
public:
LayerMask(Sprite* sprite);
virtual ~LayerMask();
};
} // namespace doc
#endif

View File

@ -0,0 +1,23 @@
// Aseprite Document Library
// Copyright (c) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "doc/layer_subsprite.h"
namespace doc {
LayerSubsprite::LayerSubsprite(Sprite* sprite) : Layer(ObjectType::LayerSubsprite, sprite)
{
}
LayerSubsprite::~LayerSubsprite()
{
}
} // namespace doc

27
src/doc/layer_subsprite.h Normal file
View File

@ -0,0 +1,27 @@
// Aseprite Document Library
// Copyright (C) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef DOC_LAYER_SUBSPRITE_H_INCLUDED
#define DOC_LAYER_SUBSPRITE_H_INCLUDED
#pragma once
#include "doc/layer.h"
namespace doc {
// Includes an subsprite (external or embedded)
// TODO to implement
class LayerSubsprite final : public Layer {
public:
LayerSubsprite(Sprite* sprite);
virtual ~LayerSubsprite();
// TODO associated subsprite and bounds/rotation/transformation per frame/keyframe
};
} // namespace doc
#endif

23
src/doc/layer_text.cpp Normal file
View File

@ -0,0 +1,23 @@
// Aseprite Document Library
// Copyright (c) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "doc/layer_text.h"
namespace doc {
LayerText::LayerText(Sprite* sprite) : Layer(ObjectType::LayerText, sprite)
{
}
LayerText::~LayerText()
{
}
} // namespace doc

27
src/doc/layer_text.h Normal file
View File

@ -0,0 +1,27 @@
// Aseprite Document Library
// Copyright (C) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef DOC_LAYER_TEXT_H_INCLUDED
#define DOC_LAYER_TEXT_H_INCLUDED
#pragma once
#include "doc/layer.h"
namespace doc {
// Dynamically renders a text for each cel.
// TODO to implement
class LayerText final : public Layer {
public:
LayerText(Sprite* sprite);
virtual ~LayerText();
// TODO associated text/font/location per frame/keyframe
};
} // namespace doc
#endif

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2019-2020 Igara Studio S.A.
// Copyright (c) 2019-2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@ -14,9 +14,10 @@
namespace doc {
class LayerTilemap : public LayerImage {
// Renders a tilemap on each cel using a tileset.
class LayerTilemap final : public LayerImage {
public:
explicit LayerTilemap(Sprite* sprite, const tileset_index tsi);
LayerTilemap(Sprite* sprite, tileset_index tsi);
~LayerTilemap();
Grid grid() const override;

23
src/doc/layer_vector.cpp Normal file
View File

@ -0,0 +1,23 @@
// Aseprite Document Library
// Copyright (c) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "doc/layer_vector.h"
namespace doc {
LayerVector::LayerVector(Sprite* sprite) : Layer(ObjectType::LayerVector, sprite)
{
}
LayerVector::~LayerVector()
{
}
} // namespace doc

27
src/doc/layer_vector.h Normal file
View File

@ -0,0 +1,27 @@
// Aseprite Document Library
// Copyright (C) 2025 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#ifndef DOC_LAYER_VECTOR_H_INCLUDED
#define DOC_LAYER_VECTOR_H_INCLUDED
#pragma once
#include "doc/layer.h"
namespace doc {
// Dynamically renders a path/vector for each cel.
// TODO to implement
class LayerVector final : public Layer {
public:
LayerVector(Sprite* sprite);
virtual ~LayerVector();
// TODO associated path/vector data per frame/keyframe
};
} // namespace doc
#endif

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -9,9 +9,11 @@
#define DOC_OBJECT_TYPE_H_INCLUDED
#pragma once
#include "base/ints.h"
namespace doc {
enum class ObjectType {
enum class ObjectType : uint16_t {
Unknown = 0,
Image = 1,
Palette = 2,
@ -35,6 +37,16 @@ enum class ObjectType {
LayerTilemap = 14,
Tileset = 15,
Tilesets = 16,
LayerFill = 17,
LayerMask = 18,
LayerFx = 19,
LayerText = 20,
LayerVector = 21,
LayerAudio = 22,
LayerHitbox = 23,
LayerSubsprite = 24,
};
} // namespace doc

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -213,8 +213,10 @@ void Sprite::setSize(int width, int height)
void Sprite::setColorSpace(const gfx::ColorSpaceRef& colorSpace)
{
m_spec.setColorSpace(colorSpace);
for (auto cel : uniqueCels())
cel->image()->setColorSpace(colorSpace);
for (Cel* cel : uniqueCels()) {
if (Image* img = cel->image())
img->setColorSpace(colorSpace);
}
}
bool Sprite::isOpaque() const
@ -301,7 +303,7 @@ Layer* Sprite::firstLayer() const
{
Layer* layer = root()->firstLayer();
while (layer && layer->isGroup())
layer = static_cast<LayerGroup*>(layer)->firstLayer();
layer = layer->firstLayer();
return layer;
}
@ -309,7 +311,7 @@ Layer* Sprite::firstBrowsableLayer() const
{
Layer* layer = root()->firstLayer();
while (layer->isBrowsable())
layer = static_cast<LayerGroup*>(layer)->firstLayer();
layer = layer->firstLayer();
return layer;
}
@ -532,7 +534,7 @@ void Sprite::setDurationForAllFrames(int msecs)
ImageRef Sprite::getImageRef(ObjectId imageId)
{
for (Cel* cel : cels()) {
if (cel->image()->id() == imageId)
if (cel->imageId() == imageId)
return cel->imageRef();
}
if (hasTilesets()) {
@ -565,7 +567,7 @@ CelDataRef Sprite::getCelDataRef(ObjectId celDataId)
void Sprite::replaceImage(ObjectId curImageId, const ImageRef& newImage)
{
for (Cel* cel : cels()) {
if (cel->image()->id() == curImageId)
if (cel->imageId() == curImageId)
cel->data()->setImage(newImage, cel->layer());
}
@ -604,7 +606,7 @@ void Sprite::replaceTileset(tileset_index tsi, Tileset* newTileset)
void Sprite::getImages(std::vector<ImageRef>& images) const
{
for (Cel* cel : uniqueCels())
if (cel->image()->pixelFormat() != IMAGE_TILEMAP)
if (cel->image() && cel->image()->pixelFormat() != IMAGE_TILEMAP)
images.push_back(cel->imageRef());
if (hasTilesets()) {

View File

@ -1,5 +1,5 @@
// Aseprite View Library
// Copyright (c) 2020-2023 Igara Studio S.A.
// Copyright (c) 2020-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -24,7 +24,7 @@ Layer* candidate_if_layer_is_deleted(const Layer* selectedLayer, const Layer* la
if ((selectedLayer == layerToDelete) ||
(selectedLayer && selectedLayer->hasAncestor(layerToDelete))) {
Sprite* sprite = selectedLayer->sprite();
LayerGroup* parent = layerToDelete->parent();
Layer* parent = layerToDelete->parent();
// Select previous layer, or next layer, or the parent (if it is
// not the main layer of sprite set).