From 64ce25fae287e499d7da8d3c14462ee34d71caa8 Mon Sep 17 00:00:00 2001 From: David Capello Date: Wed, 15 Feb 2023 18:49:36 -0300 Subject: [PATCH] Add property to disable the standard tilemap UI Added a Sprite.tileManagementPlugin property for plugins that want to replace the standard tilemap/tileset interface. This includes a new external file field in .aseprite files to specify that the sprite tiles are controlled by a specific plugin. Once this property is set, the standard tilemap/tileset modes selectors will disappear and the only way to make then available will be setting this property to nil/empty string again. Fix https://github.com/aseprite/Attachment-System/issues/21 --- docs/ase-file-specs.md | 12 +++- src/app/CMakeLists.txt | 1 + .../cmd/set_sprite_tile_management_plugin.cpp | 53 ++++++++++++++ .../cmd/set_sprite_tile_management_plugin.h | 42 +++++++++++ src/app/doc_observer.h | 5 +- src/app/file/ase_format.cpp | 6 ++ src/app/script/sprite_class.cpp | 27 +++++++ src/app/ui/color_bar.cpp | 39 ++++++++++- src/app/ui/color_bar.h | 6 +- src/dio/CMakeLists.txt | 3 +- src/dio/aseprite_common.cpp | 70 +++++++++++++++++++ src/dio/aseprite_common.h | 45 +++--------- src/dio/aseprite_decoder.cpp | 7 ++ src/doc/sprite.h | 24 ++++++- tests/scripts/sprite.lua | 24 ++++++- 15 files changed, 319 insertions(+), 45 deletions(-) create mode 100644 src/app/cmd/set_sprite_tile_management_plugin.cpp create mode 100644 src/app/cmd/set_sprite_tile_management_plugin.h create mode 100644 src/dio/aseprite_common.cpp diff --git a/docs/ase-file-specs.md b/docs/ase-file-specs.md index 183f07b81..5e1b36cad 100644 --- a/docs/ase-file-specs.md +++ b/docs/ase-file-specs.md @@ -275,8 +275,9 @@ reference external palettes, tilesets, or extensions that make use of extended p 0 - External palette 1 - External tileset 2 - Extension name for properties + 3 - Extension name for tile management (can exist one per sprite) BYTE[7] Reserved (set to zero) - STRING External file name or extension ID + STRING External file name or extension ID (see NOTE.4) ### Mask Chunk (0x2016) DEPRECATED @@ -515,6 +516,15 @@ Details about the ZLIB and DEFLATE compression methods: * Some extra notes that might help you to decode the data: http://george.chiramattel.com/blog/2007/09/deflatestream-block-length-does-not-match.html +#### NOTE.4 + +The extension ID must be a string like `publisher/ExtensionName`, for +example, the [Aseprite Attachment System](https://github.com/aseprite/Attachment-System) +uses `aseprite/Attachment-System`. + +This string will be used in a future to automatically link to the +extension URL in the [Aseprite Store](https://github.com/aseprite/aseprite/issues/1928). + ## File Format Changes 1. The first change from the first release of the new .ase format, diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 1380a6119..b11ed4e67 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -520,6 +520,7 @@ add_library(app-lib cmd/set_slice_key.cpp cmd/set_slice_name.cpp cmd/set_sprite_size.cpp + cmd/set_sprite_tile_management_plugin.cpp cmd/set_tag_anidir.cpp cmd/set_tag_color.cpp cmd/set_tag_name.cpp diff --git a/src/app/cmd/set_sprite_tile_management_plugin.cpp b/src/app/cmd/set_sprite_tile_management_plugin.cpp new file mode 100644 index 000000000..71201147e --- /dev/null +++ b/src/app/cmd/set_sprite_tile_management_plugin.cpp @@ -0,0 +1,53 @@ +// Aseprite +// Copyright (c) 2023 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_sprite_tile_management_plugin.h" + +#include "app/doc.h" +#include "app/doc_event.h" +#include "doc/sprite.h" + +namespace app { +namespace cmd { + +SetSpriteTileManagementPlugin::SetSpriteTileManagementPlugin( + Sprite* sprite, + const std::string& value) + : WithSprite(sprite) + , m_oldValue(sprite->tileManagementPlugin()) + , m_newValue(value) +{ +} + +void SetSpriteTileManagementPlugin::onExecute() +{ + Sprite* spr = sprite(); + spr->setTileManagementPlugin(m_newValue); + spr->incrementVersion(); +} + +void SetSpriteTileManagementPlugin::onUndo() +{ + Sprite* spr = sprite(); + spr->setTileManagementPlugin(m_oldValue); + spr->incrementVersion(); +} + +void SetSpriteTileManagementPlugin::onFireNotifications() +{ + Sprite* spr = sprite(); + Doc* doc = static_cast(spr->document()); + DocEvent ev(doc); + ev.sprite(spr); + doc->notify_observers(&DocObserver::onTileManagementPluginChange, ev); +} + +} // namespace cmd +} // namespace app diff --git a/src/app/cmd/set_sprite_tile_management_plugin.h b/src/app/cmd/set_sprite_tile_management_plugin.h new file mode 100644 index 000000000..b7d50eda1 --- /dev/null +++ b/src/app/cmd/set_sprite_tile_management_plugin.h @@ -0,0 +1,42 @@ +// Aseprite +// Copyright (c) 2023 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifndef APP_CMD_SET_SPRITE_TILE_MANAGEMENT_PLUGIN_H_INCLUDED +#define APP_CMD_SET_SPRITE_TILE_MANAGEMENT_PLUGIN_H_INCLUDED +#pragma once + +#include "app/cmd.h" +#include "app/cmd/with_sprite.h" + +#include + +namespace app { +namespace cmd { + using namespace doc; + + class SetSpriteTileManagementPlugin : public Cmd + , public WithSprite { + public: + SetSpriteTileManagementPlugin(Sprite* sprite, + const std::string& value); + + protected: + void onExecute() override; + void onUndo() override; + void onFireNotifications() override; + size_t onMemSize() const override { + return sizeof(*this) + m_oldValue.size() + m_newValue.size(); + } + + private: + std::string m_oldValue; + std::string m_newValue; + }; + +} // namespace cmd +} // namespace app + +#endif diff --git a/src/app/doc_observer.h b/src/app/doc_observer.h index 1c2453247..5b95ed321 100644 --- a/src/app/doc_observer.h +++ b/src/app/doc_observer.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2022 Igara Studio S.A. +// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -99,6 +99,9 @@ namespace app { // The tileset was remapped (e.g. when tiles are re-ordered). virtual void onRemapTileset(DocEvent& ev, const doc::Remap& remap) { } + // When the tile management plugin property is changed. + virtual void onTileManagementPluginChange(DocEvent& ev) { } + }; } // namespace app diff --git a/src/app/file/ase_format.cpp b/src/app/file/ase_format.cpp index 6a07f6c8f..c582a6304 100644 --- a/src/app/file/ase_format.cpp +++ b/src/app/file/ase_format.cpp @@ -1375,6 +1375,12 @@ static void ase_file_write_external_files_chunk( putExtentionIds(slice->userData().propertiesMaps(), ext_files); } + // Tile management plugin + if (sprite->hasTileManagementPlugin()) { + ext_files.insert(ASE_EXTERNAL_FILE_TILE_MANAGEMENT, + sprite->tileManagementPlugin()); + } + // No external files to write if (ext_files.items().empty()) return; diff --git a/src/app/script/sprite_class.cpp b/src/app/script/sprite_class.cpp index 8ec87bb63..36443d22e 100644 --- a/src/app/script/sprite_class.cpp +++ b/src/app/script/sprite_class.cpp @@ -29,6 +29,7 @@ #include "app/cmd/set_mask.h" #include "app/cmd/set_pixel_ratio.h" #include "app/cmd/set_sprite_size.h" +#include "app/cmd/set_sprite_tile_management_plugin.h" #include "app/cmd/set_transparent_color.h" #include "app/color_spaces.h" #include "app/commands/commands.h" @@ -978,6 +979,31 @@ int Sprite_set_pixelRatio(lua_State* L) return 0; } +int Sprite_get_tileManagementPlugin(lua_State* L) +{ + const auto sprite = get_docobj(L, 1); + if (sprite->hasTileManagementPlugin()) + lua_pushstring(L, sprite->tileManagementPlugin().c_str()); + else + lua_pushnil(L); + return 1; +} + +int Sprite_set_tileManagementPlugin(lua_State* L) +{ + auto sprite = get_docobj(L, 1); + std::string value; + if (const char* p = lua_tostring(L, 2)) + value = p; + + if (sprite->tileManagementPlugin() != value) { + Tx tx; + tx(new cmd::SetSpriteTileManagementPlugin(sprite, value)); + tx.commit(); + } + return 0; +} + const luaL_Reg Sprite_methods[] = { { "__eq", Sprite_eq }, { "resize", Sprite_resize }, @@ -1041,6 +1067,7 @@ const Property Sprite_properties[] = { { "properties", UserData_get_properties, UserData_set_properties }, { "pixelRatio", Sprite_get_pixelRatio, Sprite_set_pixelRatio }, { "events", Sprite_get_events, nullptr }, + { "tileManagementPlugin", Sprite_get_tileManagementPlugin, Sprite_set_tileManagementPlugin }, { nullptr, nullptr, nullptr } }; diff --git a/src/app/ui/color_bar.cpp b/src/app/ui/color_bar.cpp index a30d0d956..3f42b1747 100644 --- a/src/app/ui/color_bar.cpp +++ b/src/app/ui/color_bar.cpp @@ -539,6 +539,12 @@ TilemapMode ColorBar::tilemapMode() const void ColorBar::setTilemapMode(TilemapMode mode) { + // With sprites that has a custom tile management plugin, we support + // only editing pixels in manual mode. + if (customTileManagement()) { + mode = TilemapMode::Pixels; + } + if (m_tilemapMode != mode) { m_tilemapMode = mode; updateFromTilemapMode(); @@ -610,8 +616,14 @@ TilesetMode ColorBar::tilesetMode() const return TilesetMode::Manual; } -void ColorBar::setTilesetMode(const TilesetMode mode) +void ColorBar::setTilesetMode(TilesetMode mode) { + // With sprites that has a custom tile management plugin, we support + // only the manual mode. + if (customTileManagement()) { + mode = TilesetMode::Manual; + } + m_tilesetMode = mode; for (int i=0; i<3; ++i) { @@ -652,9 +664,16 @@ void ColorBar::onActiveSiteChange(const Site& site) } bool isTilemap = false; - if (site.layer()) + if (site.layer()) { isTilemap = site.layer()->isTilemap(); + if (isTilemap && customTileManagement()) { + isTilemap = false; + m_tilesetMode = TilesetMode::Manual; + m_tilemapMode = TilemapMode::Pixels; + } + } + if (m_tilesHBox.isVisible() != isTilemap) { m_tilesHBox.setVisible(isTilemap); updateFromTilemapMode(); @@ -692,6 +711,12 @@ void ColorBar::onTilesetChanged(DocEvent& ev) m_tilesView.deselect(); } +void ColorBar::onTileManagementPluginChange(DocEvent& ev) +{ + // Same update process as in onActiveSiteChange() + onActiveSiteChange(UIContext::instance()->activeSite()); +} + void ColorBar::onAppPaletteChange() { COLOR_BAR_TRACE("ColorBar::onAppPaletteChange()\n"); @@ -2008,7 +2033,15 @@ bool ColorBar::canEditTiles() const { const Site site = UIContext::instance()->activeSite(); return (site.layer() && - site.layer()->isTilemap()); + site.layer()->isTilemap() && + !customTileManagement()); +} + +bool ColorBar::customTileManagement() const +{ + return (m_lastDocument && + m_lastDocument->sprite() && + m_lastDocument->sprite()->hasTileManagementPlugin()); } } // namespace app diff --git a/src/app/ui/color_bar.h b/src/app/ui/color_bar.h index 4422e998a..c185ca691 100644 --- a/src/app/ui/color_bar.h +++ b/src/app/ui/color_bar.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2021 Igara Studio S.A. +// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -94,7 +94,7 @@ namespace app { void setTilemapMode(TilemapMode mode); TilesetMode tilesetMode() const; - void setTilesetMode(const TilesetMode mode); + void setTilesetMode(TilesetMode mode); ColorButton* fgColorButton() { return &m_fgColor; } ColorButton* bgColorButton() { return &m_bgColor; } @@ -105,6 +105,7 @@ namespace app { // DocObserver impl void onGeneralUpdate(DocEvent& ev) override; void onTilesetChanged(DocEvent& ev) override; + void onTileManagementPluginChange(DocEvent& ev) override; // InputChainElement impl void onNewInputPriority(InputChainElement* element, @@ -190,6 +191,7 @@ namespace app { void showPalettePresets(); void showPaletteOptions(); bool canEditTiles() const; + bool customTileManagement() const; void updateFromTilemapMode(); static void fixColorIndex(ColorButton& color); diff --git a/src/dio/CMakeLists.txt b/src/dio/CMakeLists.txt index 343e0b0d4..fab02fbaa 100644 --- a/src/dio/CMakeLists.txt +++ b/src/dio/CMakeLists.txt @@ -1,8 +1,9 @@ # Aseprite Document IO Library -# Copyright (c) 2022 Igara Studio S.A. +# Copyright (c) 2022-2023 Igara Studio S.A. # Copyright (c) 2016-2018 David Capello add_library(dio-lib + aseprite_common.cpp aseprite_decoder.cpp decode_file.cpp decoder.cpp diff --git a/src/dio/aseprite_common.cpp b/src/dio/aseprite_common.cpp new file mode 100644 index 000000000..7bdb8bdc6 --- /dev/null +++ b/src/dio/aseprite_common.cpp @@ -0,0 +1,70 @@ +// Aseprite Document IO Library +// Copyright (c) 2018-2023 Igara Studio S.A. +// Copyright (c) 2001-2018 David Capello +// +// 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 "dio/aseprite_common.h" + +namespace dio { + +uint32_t AsepriteExternalFiles::insert(const uint8_t type, + const std::string& filename) +{ + auto it = m_toID[type].find(filename); + if (it != m_toID[type].end()) + return it->second; + else { + insert(++m_lastid, type, filename); + return m_lastid; + } +} + +void AsepriteExternalFiles::insert(uint32_t id, + const uint8_t type, + const std::string& filename) +{ + ASSERT(type >= 0 && type < ASE_EXTERNAL_FILE_TYPES); + + m_items[id] = Item{ filename, type }; + m_toID[type][filename] = id; +} + +bool AsepriteExternalFiles::getIDByFilename(const uint8_t type, + const std::string& fn, + uint32_t& id) const +{ + ASSERT(type >= 0 && type < ASE_EXTERNAL_FILE_TYPES); + + auto it = m_toID[type].find(fn); + if (it == m_toID[type].end()) + return false; + id = it->second; + return true; +} + +bool AsepriteExternalFiles::getFilenameByID(uint32_t id, std::string& fn) const +{ + auto it = m_items.find(id); + if (it == m_items.end()) + return false; + fn = it->second.fn; + return true; +} + +std::string AsepriteExternalFiles::tileManagementPlugin() const +{ + constexpr uint8_t type = ASE_EXTERNAL_FILE_TILE_MANAGEMENT; + auto it = m_toID[type].begin(); + if (it != m_toID[type].end()) + return it->first; + else + return std::string(); +} + +} // namespace dio diff --git a/src/dio/aseprite_common.h b/src/dio/aseprite_common.h index c8d1847d4..f49d952d1 100644 --- a/src/dio/aseprite_common.h +++ b/src/dio/aseprite_common.h @@ -1,5 +1,5 @@ // Aseprite Document IO Library -// Copyright (c) 2018-2020 Igara Studio S.A. +// Copyright (c) 2018-2023 Igara Studio S.A. // Copyright (c) 2001-2018 David Capello // // This file is released under the terms of the MIT license. @@ -66,7 +66,8 @@ #define ASE_EXTERNAL_FILE_PALETTE 0 #define ASE_EXTERNAL_FILE_TILESET 1 #define ASE_EXTERNAL_FILE_EXTENSION 2 -#define ASE_EXTERNAL_FILE_TYPES 3 +#define ASE_EXTERNAL_FILE_TILE_MANAGEMENT 3 +#define ASE_EXTERNAL_FILE_TYPES 4 namespace dio { @@ -115,53 +116,27 @@ public: using Items = std::map; - const Items& items() const { - return m_items; - } + bool empty() const { return m_items.empty(); } + const Items& items() const { return m_items; } // Adds the external filename with the next autogenerated ID and specified type. uint32_t insert(const uint8_t type, - const std::string& filename) { - auto it = m_toID[type].find(filename); - if (it != m_toID[type].end()) - return it->second; - else { - insert(++m_lastid, type, filename); - return m_lastid; - } - } + const std::string& filename); // Adds the external filename using the specified ID and type. void insert(uint32_t id, const uint8_t type, - const std::string& filename) { - ASSERT(type >= 0 && type < ASE_EXTERNAL_FILE_TYPES); - - m_items[id] = Item{ filename, type }; - m_toID[type][filename] = id; - } + const std::string& filename); // Returns true if the given filename exists in the external files // chunk, and assign its ID in "id" bool getIDByFilename(const uint8_t type, const std::string& fn, - uint32_t& id) const { - ASSERT(type >= 0 && type < ASE_EXTERNAL_FILE_TYPES); + uint32_t& id) const; - auto it = m_toID[type].find(fn); - if (it == m_toID[type].end()) - return false; - id = it->second; - return true; - } + bool getFilenameByID(uint32_t id, std::string& fn) const; - bool getFilenameByID(uint32_t id, std::string& fn) const { - auto it = m_items.find(id); - if (it == m_items.end()) - return false; - fn = it->second.fn; - return true; - } + std::string tileManagementPlugin() const; private: uint32_t m_lastid = 0; // ID used to add new items diff --git a/src/dio/aseprite_decoder.cpp b/src/dio/aseprite_decoder.cpp index 9d8ab82ef..62ce57805 100644 --- a/src/dio/aseprite_decoder.cpp +++ b/src/dio/aseprite_decoder.cpp @@ -187,6 +187,13 @@ bool AsepriteDecoder::decode() case ASE_FILE_CHUNK_EXTERNAL_FILE: readExternalFiles(extFiles); + + // Tile management plugin + if (!extFiles.empty()) { + std::string fn = extFiles.tileManagementPlugin(); + if (!fn.empty()) + sprite->setTileManagementPlugin(fn); + } break; case ASE_FILE_CHUNK_MASK: { diff --git a/src/doc/sprite.h b/src/doc/sprite.h index 7a3c52e45..2eda4fdcd 100644 --- a/src/doc/sprite.h +++ b/src/doc/sprite.h @@ -1,5 +1,5 @@ // Aseprite Document Library -// Copyright (C) 2018-2021 Igara Studio S.A. +// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This file is released under the terms of the MIT license. @@ -29,6 +29,7 @@ #include "gfx/rect.h" #include +#include #include #define DOC_SPRITE_MAX_WIDTH 65535 @@ -222,6 +223,18 @@ namespace doc { bool hasTilesets() const { return m_tilesets != nullptr; } Tilesets* tilesets() const; + const std::string& tileManagementPlugin() const { + return m_tileManagementPlugin; + } + + bool hasTileManagementPlugin() const { + return !m_tileManagementPlugin.empty(); + } + + void setTileManagementPlugin(const std::string& plugin) { + m_tileManagementPlugin = plugin; + } + private: Document* m_document; ImageSpec m_spec; @@ -242,6 +255,15 @@ namespace doc { // Tilesets mutable Tilesets* m_tilesets; + // Custom tile management plugin. This can be an ID that specifies + // a custom plugin that will be used to handle tilesets and + // tilemaps for this specific sprite. This property is saved + // inside .aseprite files (ASE_EXTERNAL_FILE_TILE_MANAGEMENT), and + // it's used by the UI to disable the standard tileset/tilemap UX + // (e.g. drag & drop tiles, or TilesetMode::Auto mode, etc.), + // giving the possibility to handle tiles exclusively to a plugin. + std::string m_tileManagementPlugin; + // Disable default constructor and copying Sprite(); DISABLE_COPYING(Sprite); diff --git a/tests/scripts/sprite.lua b/tests/scripts/sprite.lua index 16f52be5d..3bb536219 100644 --- a/tests/scripts/sprite.lua +++ b/tests/scripts/sprite.lua @@ -1,4 +1,4 @@ --- Copyright (C) 2019-2022 Igara Studio S.A. +-- Copyright (C) 2019-2023 Igara Studio S.A. -- Copyright (C) 2018 David Capello -- -- This file is released under the terms of the MIT license. @@ -206,3 +206,25 @@ do assert(a == a) assert(a ~= b) -- Compares IDs, not sprite size end + +-- Tile management plugin + +do + local fn = "_test_sprite_tileManagementPlugin.aseprite" + local a = Sprite(1, 1) + assert(a.tileManagementPlugin == nil) + a.tileManagementPlugin = "test" + app.undo() + assert(a.tileManagementPlugin == nil) + app.redo() + assert(a.tileManagementPlugin == "test") + a:saveAs(fn) + + b = app.open(fn) + assert(b.tileManagementPlugin == "test") + b.tileManagementPlugin = nil + b:saveAs(fn) + + c = app.open(fn) + assert(c.tileManagementPlugin == nil) +end