diff --git a/example/assets/animations/explode_anim.json b/example/assets/animations/explode_anim.json index fdad698..86c1e24 100644 --- a/example/assets/animations/explode_anim.json +++ b/example/assets/animations/explode_anim.json @@ -1,5 +1,6 @@ { - "duration": 0.25, + "asset_type": "AnimTimeline", + "duration": 0.250000, "tiles": [ "0", "1", @@ -11,4 +12,4 @@ "7", "8" ] -} \ No newline at end of file +} diff --git a/example/assets/images/explode_atlas.json b/example/assets/images/explode_atlas.json index ccbfa4f..b3690a7 100644 --- a/example/assets/images/explode_atlas.json +++ b/example/assets/images/explode_atlas.json @@ -1,122 +1,125 @@ { + "asset_type": "TextureAtlas", "image": "images/explode_512x512.png", - "blocks": [ - { - "id": "0", - "rect": { - "lt": [ - 0, - 0 - ], - "rb": [ - 170, - 170 - ] + "tile_sheet": { + "tiles": [ + { + "id": "0", + "image_rect": { + "lt": [ + 0, + 0 + ], + "rb": [ + 170, + 170 + ] + } + }, + { + "id": "1", + "image_rect": { + "lt": [ + 170, + 0 + ], + "rb": [ + 340, + 170 + ] + } + }, + { + "id": "2", + "image_rect": { + "lt": [ + 340, + 0 + ], + "rb": [ + 510, + 170 + ] + } + }, + { + "id": "3", + "image_rect": { + "lt": [ + 0, + 170 + ], + "rb": [ + 170, + 340 + ] + } + }, + { + "id": "4", + "image_rect": { + "lt": [ + 170, + 170 + ], + "rb": [ + 340, + 340 + ] + } + }, + { + "id": "5", + "image_rect": { + "lt": [ + 340, + 170 + ], + "rb": [ + 510, + 340 + ] + } + }, + { + "id": "6", + "image_rect": { + "lt": [ + 0, + 340 + ], + "rb": [ + 170, + 510 + ] + } + }, + { + "id": "7", + "image_rect": { + "lt": [ + 170, + 340 + ], + "rb": [ + 340, + 510 + ] + } + }, + { + "id": "8", + "image_rect": { + "lt": [ + 340, + 340 + ], + "rb": [ + 510, + 510 + ] + } } - }, - { - "id": "1", - "rect": { - "lt": [ - 170, - 0 - ], - "rb": [ - 340, - 170 - ] - } - }, - { - "id": "2", - "rect": { - "lt": [ - 340, - 0 - ], - "rb": [ - 510, - 170 - ] - } - }, - { - "id": "3", - "rect": { - "lt": [ - 0, - 170 - ], - "rb": [ - 170, - 340 - ] - } - }, - { - "id": "4", - "rect": { - "lt": [ - 170, - 170 - ], - "rb": [ - 340, - 340 - ] - } - }, - { - "id": "5", - "rect": { - "lt": [ - 340, - 170 - ], - "rb": [ - 510, - 340 - ] - } - }, - { - "id": "6", - "rect": { - "lt": [ - 0, - 340 - ], - "rb": [ - 170, - 510 - ] - } - }, - { - "id": "7", - "rect": { - "lt": [ - 170, - 340 - ], - "rb": [ - 340, - 510 - ] - } - }, - { - "id": "8", - "rect": { - "lt": [ - 340, - 340 - ], - "rb": [ - 510, - 510 - ] - } - } - ] -} + ] + } +} \ No newline at end of file diff --git a/example/assets/images/pipe_128x128.9slice.json b/example/assets/images/pipe_128x128.9slice.json index 71f377e..0d515f8 100644 --- a/example/assets/images/pipe_128x128.9slice.json +++ b/example/assets/images/pipe_128x128.9slice.json @@ -1,4 +1,5 @@ { + "asset_type": "Texture9Slice", "image": "images/pipe_128x128.png", "nine_slice": { "n_left_top": [ @@ -10,4 +11,4 @@ 1.000000 ] } -} +} \ No newline at end of file diff --git a/example/flappy/src/config.hpp b/example/flappy/src/config.hpp index 7ba1c14..d080a6e 100644 --- a/example/flappy/src/config.hpp +++ b/example/flappy/src/config.hpp @@ -3,8 +3,9 @@ #include #include #include +#include #include -#include +#include #include #include #include @@ -21,7 +22,7 @@ struct Config { std::shared_ptr jump_sfx{}; std::shared_ptr explode_atlas{}; - std::optional explode_animation{}; + std::shared_ptr explode_timeline{}; std::shared_ptr explode_sfx{}; bave::Rgba background_rgba_top{bave::Rgba::from(0x1c96c5ff)}; diff --git a/example/flappy/src/flappy.cpp b/example/flappy/src/flappy.cpp index 469cec7..8e0bb9a 100644 --- a/example/flappy/src/flappy.cpp +++ b/example/flappy/src/flappy.cpp @@ -7,7 +7,6 @@ using bave::Action; using bave::App; -using bave::Driver; using bave::FocusChange; using bave::Key; using bave::KeyInput; @@ -30,8 +29,6 @@ Flappy::Flappy(App& app) : Driver(app), m_game_view(app.get_render_device().rend create_entities(); // create and setup the HUD (mainly text here). setup_hud(); - - m_im_texture = bave::ImTexture{m_config.cloud_texture}; } void Flappy::tick() { @@ -84,12 +81,6 @@ void Flappy::tick() { if (ImGui::Button(pause_text)) { m_paused = !m_paused; } ImGui::Checkbox("force lag", &m_force_lag); - - if (m_im_texture->get_id() != vk::DescriptorSet{}) { - ImGui::Separator(); - glm::vec2 const size = m_im_texture->get_texture()->get_size(); - ImGui::Image(m_im_texture->get_id(), {size.x, size.y}); - } } ImGui::End(); } @@ -181,7 +172,7 @@ void Flappy::load_assets() { m_config.jump_sfx = loader.load_audio_clip("audio_clips/beep.wav"); m_config.explode_atlas = loader.load_texture_atlas("images/explode_atlas.json"); - m_config.explode_animation = loader.load_sprite_animation("animations/explode_anim.json"); + m_config.explode_timeline = loader.load_anim_timeline("animations/explode_anim.json"); m_config.explode_sfx = loader.load_audio_clip("audio_clips/explode.wav"); m_config.cloud_texture = loader.load_texture("images/cloud_256x128.png"); @@ -193,8 +184,7 @@ void Flappy::load_assets() { void Flappy::create_entities() { // explode animation. - m_explode = SpriteAnim{m_config.explode_atlas}; - if (m_config.explode_animation) { m_explode->animation = *m_config.explode_animation; } + m_explode = SpriteAnim{m_config.explode_atlas, m_config.explode_timeline}; m_explode->repeat = false; // player sprite. diff --git a/example/flappy/src/flappy.hpp b/example/flappy/src/flappy.hpp index 7c2425e..1fef0e3 100644 --- a/example/flappy/src/flappy.hpp +++ b/example/flappy/src/flappy.hpp @@ -1,8 +1,8 @@ #pragma once #include #include +#include #include -#include #include #include #include @@ -66,8 +66,6 @@ class Flappy : public bave::Driver { bool m_force_lag{}; - std::optional m_im_texture{}; - public: // constructor needs to be public (or at least accessible by the game factory that's setup in the main target) explicit Flappy(bave::App& app); diff --git a/lib/include/bave/asset_type.hpp b/lib/include/bave/asset_type.hpp new file mode 100644 index 0000000..9b80ae3 --- /dev/null +++ b/lib/include/bave/asset_type.hpp @@ -0,0 +1,29 @@ +#pragma once +#include +#include + +namespace bave { +struct AnimTimeline; +class Texture9Slice; +class TextureAtlas; + +namespace detail { +template +constexpr auto false_v = false; +} + +template +constexpr auto get_asset_type() -> std::string_view { + if constexpr (std::same_as) { + return "AnimTimeline"; + } else if constexpr (std::same_as) { + return "Texture9Slice"; + } else if constexpr (std::same_as) { + return "TextureAtlas"; + } + + else { + static_assert(detail::false_v, "unsupported asset type"); + } +} +} // namespace bave diff --git a/lib/include/bave/graphics/anim_timeline.hpp b/lib/include/bave/graphics/anim_timeline.hpp new file mode 100644 index 0000000..dfbaf05 --- /dev/null +++ b/lib/include/bave/graphics/anim_timeline.hpp @@ -0,0 +1,13 @@ +#pragma once +#include +#include +#include + +namespace bave { +struct AnimTimeline { + std::vector tiles{}; + Seconds duration{1s}; + + [[nodiscard]] auto get_tile_at(Seconds timestamp) const -> std::string_view; +}; +} // namespace bave diff --git a/lib/include/bave/graphics/sprite.hpp b/lib/include/bave/graphics/sprite.hpp index 38b3e1f..a04b418 100644 --- a/lib/include/bave/graphics/sprite.hpp +++ b/lib/include/bave/graphics/sprite.hpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include namespace bave { class Sprite : public QuadShape { @@ -15,12 +17,15 @@ class Sprite : public QuadShape { [[nodiscard]] auto get_size() const -> glm::vec2 { return get_shape().size; } [[nodiscard]] auto get_uv() const -> UvRect const& { return get_shape().uv; } - void set_tile(TextureAtlas::Tile const& tile); + void set_tile(TileSheet::Tile const& tile); protected: + void set_tile(TileSheet::Tile const& tile, glm::ivec2 total_size); + std::optional m_max_size{}; }; +// TODO: move to its own header class Sprite9Slice : public NineQuadShape { public: void set_texture_9slice(std::shared_ptr texture); diff --git a/lib/include/bave/graphics/sprite_anim.hpp b/lib/include/bave/graphics/sprite_anim.hpp index ddfbd7f..9569eda 100644 --- a/lib/include/bave/graphics/sprite_anim.hpp +++ b/lib/include/bave/graphics/sprite_anim.hpp @@ -1,31 +1,37 @@ #pragma once -#include +#include #include -#include namespace bave { class SpriteAnim : public Sprite { public: - struct Animation { - std::vector tiles{}; - Seconds duration{}; + using Timeline = AnimTimeline; + using Tile = TileSheet::Tile; - [[nodiscard]] auto get_tile_at(Seconds timestamp) const -> std::string_view; - }; + explicit SpriteAnim(std::shared_ptr atlas = {}, std::shared_ptr timeline = {}); - explicit SpriteAnim(std::shared_ptr atlas = {}, Seconds duration = 1s); - - void tick(Seconds dt); + void set_texture_atlas(std::shared_ptr atlas); + void set_timeline(std::shared_ptr timeline); + [[nodiscard]] auto get_current_tile() const -> Ptr { return m_current_tile; } [[nodiscard]] auto get_current_tile_id() const -> std::string_view { return m_current_tile_id; } + [[nodiscard]] auto get_timeline() const -> NotNull> const& { return m_timeline; } + [[nodiscard]] auto get_atlas() const -> std::shared_ptr const& { return m_atlas; } + + void tick(Seconds dt); - std::shared_ptr atlas; - Animation animation; Seconds elapsed{}; bool repeat{true}; bool animate{true}; private: + auto set_current_tile(std::string_view id) -> bool; + void set_current_tile(Tile const& tile); + void reset_anim(); + + NotNull> m_timeline; + std::shared_ptr m_atlas; std::string m_current_tile_id{}; + Ptr m_current_tile{}; }; } // namespace bave diff --git a/lib/include/bave/graphics/texture_atlas.hpp b/lib/include/bave/graphics/texture_atlas.hpp index a6061ce..5ce980f 100644 --- a/lib/include/bave/graphics/texture_atlas.hpp +++ b/lib/include/bave/graphics/texture_atlas.hpp @@ -1,19 +1,13 @@ #pragma once -#include -#include -#include #include -#include -#include -#include -#include +#include namespace bave { class TextureAtlas : public Texture { public: struct Block { std::string id{}; - Rect rect{}; + Rect image_rect{}; }; struct Tile { @@ -22,18 +16,12 @@ class TextureAtlas : public Texture { glm::ivec2 top_left{}; }; - [[nodiscard]] static auto make_rects(glm::ivec2 size, glm::ivec2 tile_count) -> std::vector>; + explicit TextureAtlas(NotNull render_device, BitmapView bitmap, TileSheet sheet, bool mip_map = false); - explicit TextureAtlas(NotNull render_device, BitmapView bitmap, std::vector blocks, bool mip_map = false); - - [[nodiscard]] auto find_tile(std::string_view id) const -> std::optional; - - [[nodiscard]] auto get_tile_count() const -> std::size_t { return m_rects.size(); } - - [[nodiscard]] auto get_blocks() const -> std::span { return m_blocks; } + [[nodiscard]] auto get_sheet() const -> TileSheet const& { return m_sheet; } + [[nodiscard]] auto get_uv(std::string_view id) const -> UvRect; private: - std::unordered_map, StringHash, std::equal_to<>> m_rects{}; - std::vector m_blocks{}; + TileSheet m_sheet; }; } // namespace bave diff --git a/lib/include/bave/graphics/tile_sheet.hpp b/lib/include/bave/graphics/tile_sheet.hpp new file mode 100644 index 0000000..3746b02 --- /dev/null +++ b/lib/include/bave/graphics/tile_sheet.hpp @@ -0,0 +1,30 @@ +#pragma once +#include +#include +#include +#include + +namespace bave { +struct TileBlock { + std::string id{}; + Rect image_rect{}; + std::vector> colliders{}; +}; + +struct TileSheet { + struct Tile { + std::string id{}; + Rect image_rect{}; + std::vector> colliders{}; + + [[nodiscard]] auto get_size() const -> glm::ivec2; + [[nodiscard]] auto get_uv(glm::vec2 image_size) const -> UvRect; + }; + + std::vector tiles{}; + + [[nodiscard]] static auto make_rects(glm::ivec2 size, glm::ivec2 tile_count) -> std::vector>; + + [[nodiscard]] auto find_tile(std::string_view id) const -> Ptr; +}; +} // namespace bave diff --git a/lib/include/bave/json_io.hpp b/lib/include/bave/json_io.hpp index 3f83242..19b7707 100644 --- a/lib/include/bave/json_io.hpp +++ b/lib/include/bave/json_io.hpp @@ -2,6 +2,7 @@ #include #include #include +#include #include namespace bave { @@ -37,4 +38,7 @@ void from_json(dj::Json const& json, Rgba& out); void to_json(dj::Json& out, NineSlice const& nine_slice); void from_json(dj::Json const& json, NineSlice& out); + +void to_json(dj::Json& out, TileSheet const& tile_sheet); +void from_json(dj::Json const& json, TileSheet& out); } // namespace bave diff --git a/lib/include/bave/loader.hpp b/lib/include/bave/loader.hpp index db5f315..813d426 100644 --- a/lib/include/bave/loader.hpp +++ b/lib/include/bave/loader.hpp @@ -1,13 +1,15 @@ #pragma once +#include #include #include #include +#include #include #include -#include #include #include #include +#include #include #include @@ -18,15 +20,21 @@ class Loader { [[nodiscard]] auto load_bytes(std::string_view uri) const -> std::vector; [[nodiscard]] auto load_json(std::string_view uri) const -> dj::Json; + [[nodiscard]] auto load_json_asset(std::string_view uri, std::string_view asset_type) const -> dj::Json; + + template + [[nodiscard]] auto load_json_asset(std::string_view const uri) const -> dj::Json { + return load_json_asset(uri, get_asset_type()); + } + + [[nodiscard]] auto load_image_file(std::string_view uri) const -> std::optional; - [[nodiscard]] auto load_image_file(std::string_view uri) const -> std::shared_ptr; [[nodiscard]] auto load_texture(std::string_view uri, bool mip_map = false) const -> std::shared_ptr; [[nodiscard]] auto load_texture_9slice(std::string_view uri) const -> std::shared_ptr; [[nodiscard]] auto load_texture_atlas(std::string_view uri, bool mip_map = false) const -> std::shared_ptr; [[nodiscard]] auto load_font(std::string_view uri) const -> std::shared_ptr; [[nodiscard]] auto load_audio_clip(std::string_view uri) const -> std::shared_ptr; - - [[nodiscard]] auto load_sprite_animation(std::string_view uri) const -> std::optional; + [[nodiscard]] auto load_anim_timeline(std::string_view uri) const -> std::shared_ptr; private: Logger m_log{"Loader"}; diff --git a/lib/src/graphics/anim_timeline.cpp b/lib/src/graphics/anim_timeline.cpp new file mode 100644 index 0000000..2028290 --- /dev/null +++ b/lib/src/graphics/anim_timeline.cpp @@ -0,0 +1,13 @@ +#include +#include + +namespace bave { +auto AnimTimeline::get_tile_at(Seconds const timestamp) const -> std::string_view { + if (tiles.empty()) { return {}; } + if (duration <= 0s) { return tiles.back(); } + auto const ratio = std::clamp(timestamp / duration, 0.0f, 1.0f); + auto const index = static_cast(ratio * static_cast(tiles.size())); + if (index >= tiles.size()) { return tiles.back(); } + return tiles[index]; +} +} // namespace bave diff --git a/lib/src/graphics/sprite.cpp b/lib/src/graphics/sprite.cpp index f79929f..9a6c65c 100644 --- a/lib/src/graphics/sprite.cpp +++ b/lib/src/graphics/sprite.cpp @@ -24,9 +24,17 @@ void Sprite::set_uv(UvRect const uv) { } } -void Sprite::set_tile(TextureAtlas::Tile const& tile) { - set_uv(tile.uv); - if (m_max_size) { set_size(ExtentScaler{.source = tile.size}.fit_space(*m_max_size)); } +void Sprite::set_tile(TileSheet::Tile const& tile) { + auto const size = [&] { + if (auto const& texture = get_texture()) { return texture->get_size(); } + return glm::ivec2{}; + }(); + set_tile(tile, size); +} + +void Sprite::set_tile(TileSheet::Tile const& tile, glm::ivec2 const total_size) { + set_uv(tile.get_uv(total_size)); + if (m_max_size) { set_size(ExtentScaler{.source = tile.get_size()}.fit_space(*m_max_size)); } } void Sprite9Slice::set_texture_9slice(std::shared_ptr texture) { diff --git a/lib/src/graphics/sprite_anim.cpp b/lib/src/graphics/sprite_anim.cpp index 7c4bf7d..e450c84 100644 --- a/lib/src/graphics/sprite_anim.cpp +++ b/lib/src/graphics/sprite_anim.cpp @@ -1,53 +1,79 @@ #include -#include +#include namespace bave { namespace { -auto make_animation(std::shared_ptr const& atlas, Seconds const duration) { +auto make_timeline(std::shared_ptr timeline, std::shared_ptr const& atlas, Seconds duration) + -> std::shared_ptr { + if (timeline) { return timeline; } + auto ret = std::make_shared(); + ret->duration = duration; if (atlas) { - auto const blocks = atlas->get_blocks(); - auto tile_ids = std::vector{}; - for (auto const& block : blocks) { tile_ids.push_back(block.id); } - return SpriteAnim::Animation{std::move(tile_ids), duration}; + auto const& tiles = atlas->get_sheet().tiles; + ret->tiles.reserve(tiles.size()); + for (auto const& block : tiles) { ret->tiles.emplace_back(block.id); } } - return SpriteAnim::Animation{std::vector{}, {}}; + return ret; } } // namespace -auto SpriteAnim::Animation::get_tile_at(Seconds const timestamp) const -> std::string_view { - if (tiles.empty()) { return {}; } - if (duration <= 0s) { return tiles.back(); } - auto const ratio = std::clamp(timestamp / duration, 0.0f, 1.0f); - auto const index = static_cast(ratio * static_cast(tiles.size())); - if (index >= tiles.size()) { return tiles.back(); } - return tiles.at(index); -} - -SpriteAnim::SpriteAnim(std::shared_ptr atlas, Seconds const duration) - : atlas(std::move(atlas)), animation(make_animation(this->atlas, duration)) { - m_current_tile_id = animation.get_tile_at({}); - if (this->atlas) { - set_texture(this->atlas); - if (auto tile = this->atlas->find_tile(m_current_tile_id)) { set_tile(*tile); } - } +SpriteAnim::SpriteAnim(std::shared_ptr atlas, std::shared_ptr timeline) + : m_timeline(make_timeline(std::move(timeline), atlas, 1s)), m_atlas(std::move(atlas)) { + reset_anim(); +} + +void SpriteAnim::set_texture_atlas(std::shared_ptr atlas) { + if (!atlas) { return; } + m_atlas = std::move(atlas); + reset_anim(); +} + +void SpriteAnim::set_timeline(std::shared_ptr timeline) { + if (!timeline) { return; } + m_timeline = std::move(timeline); + reset_anim(); } void SpriteAnim::tick(Seconds dt) { - if (!atlas) { animate = false; } - set_texture(atlas); + if (textures[0] != m_atlas) { textures[0] = m_atlas; } + + if (!m_atlas) { animate = false; } + if (!animate) { elapsed = {}; return; } + elapsed += dt; - auto const tile_id = animation.get_tile_at(elapsed); - if (tile_id != m_current_tile_id) { - if (auto tile = atlas->find_tile(tile_id)) { set_tile(*tile); } - m_current_tile_id = tile_id; - } - if (elapsed > animation.duration) { + auto const tile_id = m_timeline->get_tile_at(elapsed); + if (tile_id != m_current_tile_id) { set_current_tile(tile_id); } + if (elapsed > m_timeline->duration) { elapsed = {}; if (!repeat) { animate = false; } } } + +void SpriteAnim::reset_anim() { + elapsed = {}; + animate = m_atlas != nullptr; + m_current_tile_id.clear(); + m_current_tile = {}; + + if (m_atlas) { set_current_tile(m_timeline->get_tile_at(elapsed)); } +} + +auto SpriteAnim::set_current_tile(std::string_view const id) -> bool { + assert(m_atlas); + if (auto const* tile = m_atlas->get_sheet().find_tile(id)) { + set_current_tile(*tile); + return true; + } + return false; +} + +void SpriteAnim::set_current_tile(Tile const& tile) { + set_tile(tile, m_atlas->get_size()); + m_current_tile = &tile; + m_current_tile_id = tile.id; +} } // namespace bave diff --git a/lib/src/graphics/texture_atlas.cpp b/lib/src/graphics/texture_atlas.cpp index 087d71f..df22ec8 100644 --- a/lib/src/graphics/texture_atlas.cpp +++ b/lib/src/graphics/texture_atlas.cpp @@ -1,54 +1,12 @@ -#include -#include -#include #include -#include namespace bave { -namespace { -constexpr auto make_tile(Rect const& rect, glm::vec2 const size) { - auto ret = TextureAtlas::Tile{.size = {rect.rb.x - rect.lt.x, rect.rb.y - rect.lt.y}}; - ret.uv = Rect<>{.lt = glm::vec2{rect.lt} / size, .rb = glm::vec2{rect.rb} / size}; - ret.top_left = rect.lt; - return ret; -} -} // namespace - -auto TextureAtlas::make_rects(glm::ivec2 size, glm::ivec2 tile_count) -> std::vector> { - if (!is_positive(size) || !is_positive(tile_count)) { return {}; } - - auto const tile_size = size / tile_count; - auto ret = std::vector>{}; - ret.reserve(static_cast(tile_count.x * tile_count.y)); - auto tile = Rect{.rb = tile_size}; - for (int row = 0; row < tile_count.y; ++row) { - for (int col = 0; col < tile_count.x; ++col) { - ret.push_back(tile); - tile.lt.x += tile_size.x; - tile.rb.x += tile_size.x; - } - tile.lt.x = 0; - tile.rb.x = tile_size.x; - tile.lt.y += tile_size.y; - tile.rb.y += tile_size.y; - } - return ret; -} - -TextureAtlas::TextureAtlas(NotNull render_device, BitmapView bitmap, std::vector blocks, bool mip_map) - : Texture(render_device, bitmap, mip_map), m_blocks(std::move(blocks)) { - for (auto const& block : m_blocks) { - if (block.id.empty()) { continue; } - m_rects.insert_or_assign(block.id, block.rect); - } -} - -auto TextureAtlas::find_tile(std::string_view const id) const -> std::optional { - if (id.empty()) { return {}; } - - auto const it = m_rects.find(id); - if (it == m_rects.end()) { return {}; } +TextureAtlas::TextureAtlas(NotNull render_device, BitmapView bitmap, TileSheet sheet, bool mip_map) + : Texture(render_device, bitmap, mip_map), m_sheet(std::move(sheet)) {} - return make_tile(it->second, get_size()); +auto TextureAtlas::get_uv(std::string_view const id) const -> UvRect { + auto const* tile = m_sheet.find_tile(id); + if (tile == nullptr) { return uv_rect_v; } + return tile->get_uv(get_size()); } } // namespace bave diff --git a/lib/src/graphics/tile_sheet.cpp b/lib/src/graphics/tile_sheet.cpp new file mode 100644 index 0000000..83bb859 --- /dev/null +++ b/lib/src/graphics/tile_sheet.cpp @@ -0,0 +1,39 @@ +#include +#include +#include + +namespace bave { +auto TileSheet::make_rects(glm::ivec2 size, glm::ivec2 tile_count) -> std::vector> { + if (!is_positive(size) || !is_positive(tile_count)) { return {}; } + + auto const tile_size = size / tile_count; + auto ret = std::vector>{}; + ret.reserve(static_cast(tile_count.x * tile_count.y)); + auto tile = Rect{.rb = tile_size}; + for (int row = 0; row < tile_count.y; ++row) { + for (int col = 0; col < tile_count.x; ++col) { + ret.push_back(tile); + tile.lt.x += tile_size.x; + tile.rb.x += tile_size.x; + } + tile.lt.x = 0; + tile.rb.x = tile_size.x; + tile.lt.y += tile_size.y; + tile.rb.y += tile_size.y; + } + return ret; +} + +auto TileSheet::Tile::get_size() const -> glm::ivec2 { return {image_rect.rb.x - image_rect.lt.x, image_rect.rb.y - image_rect.lt.y}; } + +auto TileSheet::Tile::get_uv(glm::vec2 const image_size) const -> UvRect { + if (!is_positive(image_size)) { return uv_rect_v; } + return {.lt = glm::vec2{image_rect.lt} / image_size, .rb = glm::vec2{image_rect.rb} / image_size}; +} + +auto TileSheet::find_tile(std::string_view const id) const -> Ptr { + auto const it = std::find_if(tiles.begin(), tiles.end(), [id](Tile const& t) { return t.id == id; }); + if (it == tiles.end()) { return {}; } + return &*it; +} +} // namespace bave diff --git a/lib/src/json_io.cpp b/lib/src/json_io.cpp index 15b48b3..2e654ad 100644 --- a/lib/src/json_io.cpp +++ b/lib/src/json_io.cpp @@ -13,3 +13,31 @@ void bave::from_json(dj::Json const& json, NineSlice& out) { from_json(json["n_left_top"], out.n_left_top); from_json(json["n_right_bottom"], out.n_right_bottom); } + +void bave::to_json(dj::Json& out, TileSheet const& tile_sheet) { + for (auto const& in_tile : tile_sheet.tiles) { + if (in_tile.id.empty()) { continue; } + auto& out_tile = out.push_back({}); + out_tile["id"] = in_tile.id; + to_json(out_tile["image_rect"], in_tile.image_rect); + if (!in_tile.colliders.empty()) { + auto& out_colliders = out_tile["colliders"]; + for (auto const& in_collider : in_tile.colliders) { to_json(out_colliders.push_back({}), in_collider); } + } + } +} + +void bave::from_json(dj::Json const& json, TileSheet& out) { + for (auto const& in_tile : json["tiles"].array_view()) { + auto out_tile = TileSheet::Tile{}; + out_tile.id = in_tile["id"].as_string(); + if (out_tile.id.empty()) { continue; } + from_json(in_tile["image_rect"], out_tile.image_rect); + for (auto const& in_collider : in_tile["colliders"].array_view()) { + auto out_collider = Rect{}; + from_json(in_collider, out_collider); + out_tile.colliders.push_back(out_collider); + } + out.tiles.push_back(std::move(out_tile)); + } +} diff --git a/lib/src/loader.cpp b/lib/src/loader.cpp index a4e07ba..67d9452 100644 --- a/lib/src/loader.cpp +++ b/lib/src/loader.cpp @@ -43,12 +43,35 @@ auto Loader::load_json(std::string_view const uri) const -> dj::Json { return ret; } -auto Loader::load_image_file(std::string_view uri) const -> std::shared_ptr { +auto Loader::load_json_asset(std::string_view const uri, std::string_view const asset_type) const -> dj::Json { + if (asset_type.empty()) { + m_log.warn("cannot load unknown asset type"); + return {}; + } + + auto ret = load_json(uri); + if (!ret) { return {}; } + + if (!ret.contains("asset_type")) { + m_log.warn("JSON missing 'asset_type' field: '{}'", uri); + return {}; + } + + auto const in_asset_type = ret["asset_type"].as_string(); + if (in_asset_type != asset_type) { + m_log.warn("JSON asset_type mismatch, expected: '{}', obtained: '{}'", asset_type, in_asset_type); + return {}; + } + + return ret; +} + +auto Loader::load_image_file(std::string_view uri) const -> std::optional { auto const bytes = load_bytes(uri); if (bytes.empty()) { return {}; } - auto ret = std::make_shared(); - if (!ret->load_from_bytes(bytes)) { + auto ret = ImageFile{}; + if (!ret.load_from_bytes(bytes)) { m_log.warn("failed to load ImageFile: '{}'", uri); return {}; } @@ -73,8 +96,8 @@ auto Loader::load_texture(std::string_view const uri, bool const mip_map) const } auto Loader::load_texture_9slice(std::string_view const uri) const -> std::shared_ptr { - auto json = load_json(uri); - if (!json || !json.contains("image")) { return {}; } + auto json = load_json_asset(uri); + if (!json) { return {}; } auto image = load_image_file(json["image"].as_string()); if (!image) { return {}; } @@ -83,26 +106,21 @@ auto Loader::load_texture_9slice(std::string_view const uri) const -> std::share from_json(json["nine_slice"], slice); auto ret = std::make_shared(m_render_device, image->get_bitmap_view(), slice); - m_log.info("loaded SlicedTexture: '{}'", uri); + m_log.info("loaded Texture9Slice: '{}'", uri); return ret; } auto Loader::load_texture_atlas(std::string_view uri, bool mip_map) const -> std::shared_ptr { - auto json = load_json(uri); - if (!json || !json.contains("image")) { return {}; } + auto json = load_json_asset(uri); + if (!json) { return {}; } auto image = load_image_file(json["image"].as_string()); if (!image) { return {}; } - auto blocks = std::vector{}; - for (auto const& in_block : json["blocks"].array_view()) { - auto block = TextureAtlas::Block{.id = std::string{in_block["id"].as_string()}}; - if (block.id.empty()) { continue; } - from_json(in_block["rect"], block.rect); - blocks.push_back(std::move(block)); - } + auto sheet = TileSheet{}; + from_json(json["tile_sheet"], sheet); - auto ret = std::make_shared(TextureAtlas(m_render_device, image->get_bitmap_view(), std::move(blocks), mip_map)); + auto ret = std::make_shared(m_render_device, image->get_bitmap_view(), sheet, mip_map); m_log.info("loaded TextureAtlas: '{}'", uri); return ret; } @@ -136,17 +154,21 @@ auto Loader::load_audio_clip(std::string_view const uri) const -> std::shared_pt return ret; } -auto Loader::load_sprite_animation(std::string_view const uri) const -> std::optional { - auto const json = load_json(uri); +auto Loader::load_anim_timeline(std::string_view const uri) const -> std::shared_ptr { + auto const json = load_json_asset(uri); if (!json) { return {}; } - auto ret = SpriteAnim::Animation{}; - ret.duration = Seconds{json["duration"].as()}; + auto ret = std::make_shared(); + ret->duration = Seconds{json["duration"].as()}; auto const& in_tiles = json["tiles"]; - ret.tiles.reserve(in_tiles.array_view().size()); - for (auto const& tile_id : in_tiles.array_view()) { ret.tiles.emplace_back(tile_id.as_string()); } + ret->tiles.reserve(in_tiles.array_view().size()); + for (auto const& in_tile : in_tiles.array_view()) { + auto tile_id = in_tile.as_string(); + if (tile_id.empty()) { continue; } + ret->tiles.emplace_back(tile_id); + } - m_log.info("loaded SpriteAnimation: '{}'", uri); + m_log.info("loaded AnimTimeline: '{}'", uri); return ret; } } // namespace bave diff --git a/tools/src/tools/animator.cpp b/tools/src/tools/animator.cpp index 5cff0bf..e85faa0 100644 --- a/tools/src/tools/animator.cpp +++ b/tools/src/tools/animator.cpp @@ -7,7 +7,7 @@ namespace fs = std::filesystem; Animator::Animator(App& app, NotNull> const& state) : Applet(app, state), m_loader(&app.get_data_store(), &app.get_render_device()) { setup_scene(); - if (!load_previous()) { new_animation(); } + if (!load_previous()) { new_timeline(); } } void Animator::tick() { @@ -15,7 +15,7 @@ void Animator::tick() { begin_sidepanel_window("Sprite Animation"); - if (ImGui::CollapsingHeader("Animation", ImGuiTreeNodeFlags_DefaultOpen)) { animation_control(); } + if (ImGui::CollapsingHeader("Animation", ImGuiTreeNodeFlags_DefaultOpen)) { timeline_control(); } if (ImGui::CollapsingHeader("Metadata", ImGuiTreeNodeFlags_DefaultOpen)) { metadata_control(); } @@ -27,14 +27,14 @@ void Animator::tick() { if (!m_texture) { return; } auto const tex_size = m_texture->get_size(); - if (auto const tile = m_texture->find_tile(m_sprite.get_current_tile_id())) { + if (auto const* tile = m_texture->get_sheet().find_tile(m_sprite.get_current_tile_id())) { auto const size_ratio = m_image_quad.get_shape().size / glm::vec2{tex_size}; auto outline = LineRect{}; - outline.size = glm::vec2{tile->size} * size_ratio; + outline.size = glm::vec2{tile->get_size()} * size_ratio; m_rect.set_geometry(Geometry::from(outline)); - auto const top_left = (glm::vec2{glm::ivec2{tile->top_left.x, -tile->top_left.y} + glm::ivec2{-tex_size.x / 2, tex_size.y / 2}}) * size_ratio; + auto const top_left = (glm::vec2{glm::ivec2{tile->image_rect.lt.x, -tile->image_rect.lt.y} + glm::ivec2{-tex_size.x / 2, tex_size.y / 2}}) * size_ratio; auto const centre = top_left + 0.5f * glm::vec2{outline.size.x, -outline.size.y}; m_rect.transform.position = m_image_quad.transform.position + centre; } @@ -78,9 +78,9 @@ auto Animator::load_new_uri(std::string_view const uri) -> bool { return false; } - if (can_load_anim() && load_animation(uri)) { + if (can_load_anim() && load_timeline(uri)) { m_log.info("loaded SpriteAnimation: '{}'", uri); - state->animator.last_animation = uri; + state->animator.last_timeline = uri; save_state(); return true; } @@ -88,7 +88,7 @@ auto Animator::load_new_uri(std::string_view const uri) -> bool { if (load_atlas(uri)) { m_log.info("loaded TextureAtlas: '{}'", uri); state->animator.last_atlas = uri; - state->animator.last_animation.clear(); + state->animator.last_timeline.clear(); save_state(); return true; } @@ -98,23 +98,23 @@ auto Animator::load_new_uri(std::string_view const uri) -> bool { } void Animator::file_menu_items() { - if (ImGui::MenuItem("New")) { new_animation(); } + if (ImGui::MenuItem("New")) { new_timeline(); } if (ImGui::BeginMenu("Open")) { if (ImGui::MenuItem("Atlas...")) { if (auto uri = dialog_open_file("Open Atlas"); !uri.empty()) { load_new_atlas(uri); } } if (ImGui::MenuItem("Animation...", nullptr, false, can_load_anim())) { - if (auto uri = dialog_open_file("Open Animation"); !uri.empty()) { load_new_animation(uri); } + if (auto uri = dialog_open_file("Open Animation"); !uri.empty()) { load_new_timeline(uri); } } ImGui::EndMenu(); } - if (ImGui::MenuItem("Save", nullptr, false, !state->animator.last_animation.empty())) { save_animation(); } + if (ImGui::MenuItem("Save", nullptr, false, !state->animator.last_timeline.empty())) { save_timeline(); } if (ImGui::MenuItem("Save As...", nullptr, false, !state->animator.last_atlas.empty())) { - auto json_uri = fs::path{state->animator.last_animation}; + auto json_uri = fs::path{state->animator.last_timeline}; if (json_uri.empty()) { json_uri = replace_extension(state->animator.last_atlas, ".anim.json"); } if (auto uri = dialog_save_file("Save Animation", json_uri.generic_string()); !uri.empty()) { - state->animator.last_animation = uri; - save_animation(); + state->animator.last_timeline = uri; + save_timeline(); } } @@ -122,8 +122,8 @@ void Animator::file_menu_items() { Applet::file_menu_items(); } -void Animator::animation_control() { - auto duration = m_sprite.animation.duration.count(); +void Animator::timeline_control() { + auto duration = m_timeline->duration.count(); ImGui::BeginDisabled(!m_texture); ImGui::Checkbox("animate", &m_sprite.animate); @@ -133,33 +133,36 @@ void Animator::animation_control() { auto modified = false; if (ImGui::DragFloat("duration", &duration, 0.05f, 0.0f, 500.0f)) { - m_sprite.animation.duration = Seconds{duration}; + m_timeline->duration = Seconds{duration}; modified = true; } if (ImGui::Button("Push")) { - m_sprite.animation.tiles.push_back(get_next_tile_id()); + m_timeline->tiles.push_back(get_next_tile_id()); modified = true; } if (ImGui::TreeNode("tiles")) { modified |= tiles_control(); ImGui::TreePop(); } - if (!m_sprite.animation.tiles.empty() && ImGui::Button("Pop")) { - m_sprite.animation.tiles.pop_back(); + if (!m_timeline->tiles.empty() && ImGui::Button("Pop")) { + m_timeline->tiles.pop_back(); modified = true; } - if (modified && !m_unsaved && !state->animator.last_animation.empty()) { - m_unsaved = true; - set_title(); + if (modified) { + m_sprite.set_timeline(m_timeline); + if (!m_unsaved && !state->animator.last_timeline.empty()) { + m_unsaved = true; + set_title(); + } } } auto Animator::get_next_tile_id() const -> std::string { if (m_tile_ids.empty()) { return std::string{"?"}; } - if (m_sprite.animation.tiles.empty()) { return m_tile_ids.front(); } - auto const& previous = m_sprite.animation.tiles.back(); + if (m_timeline->tiles.empty()) { return m_tile_ids.front(); } + auto const& previous = m_timeline->tiles.back(); auto const it = std::find(m_tile_ids.begin(), m_tile_ids.end(), previous); if (it == m_tile_ids.end()) { return m_tile_ids.front(); } auto const index = static_cast(it - m_tile_ids.begin()); @@ -168,8 +171,8 @@ auto Animator::get_next_tile_id() const -> std::string { auto Animator::tiles_control() -> bool { auto ret = false; - for (std::size_t i = 0; i < m_sprite.animation.tiles.size(); ++i) { - auto& tile_id = m_sprite.animation.tiles.at(i); + for (std::size_t i = 0; i < m_timeline->tiles.size(); ++i) { + auto& tile_id = m_timeline->tiles.at(i); if (ImGui::BeginCombo(FixedString{"[{}]", i}.c_str(), tile_id.c_str())) { for (auto const& id : m_tile_ids) { if (ImGui::Selectable(id.c_str(), id == tile_id)) { @@ -196,12 +199,12 @@ auto Animator::load_previous() -> bool { if (!state->animator.last_atlas.empty() && !(ret = load_atlas(state->animator.last_atlas))) { state->animator.last_atlas.clear(); - state->animator.last_animation.clear(); + state->animator.last_timeline.clear(); ret = false; } - if (ret && !state->animator.last_animation.empty() && !(ret = load_animation(state->animator.last_animation))) { - state->animator.last_animation.clear(); + if (ret && !state->animator.last_timeline.empty() && !(ret = load_timeline(state->animator.last_timeline))) { + state->animator.last_timeline.clear(); ret = false; } @@ -224,47 +227,51 @@ auto Animator::load_atlas(std::string_view uri) -> bool { m_image_quad.set_shape(quad); m_tile_ids.clear(); - m_tile_ids.reserve(m_texture->get_blocks().size()); - for (auto const& block : m_texture->get_blocks()) { m_tile_ids.push_back(block.id); } - m_sprite.atlas = m_texture; + m_tile_ids.reserve(m_texture->get_sheet().tiles.size()); + for (auto const& block : m_texture->get_sheet().tiles) { m_tile_ids.push_back(block.id); } + m_sprite.set_texture_atlas(m_texture); m_sprite.animate = true; m_unsaved = false; - m_log.info("opened TiledTexture: '{}'", uri); + m_log.info("opened TextureAtlas: '{}'", uri); return true; } - m_log.error("failed to open TiledTexture: '{}'", uri); + m_log.error("failed to open TextureAtlas: '{}'", uri); return false; } void Animator::load_new_atlas(std::string_view uri) { if (!load_atlas(uri)) { return; } - new_animation(); - state->animator.last_animation.clear(); + new_timeline(); + state->animator.last_timeline.clear(); state->animator.last_atlas = uri; save_state(); } -void Animator::new_animation() { - m_sprite.animation.tiles.clear(); - m_sprite.animation.duration = 1s; +void Animator::new_timeline() { + m_timeline->tiles.clear(); + m_timeline->duration = 1s; + m_sprite.set_timeline(m_timeline); m_sprite.set_uv(uv_rect_v); m_rect.set_geometry({}); - m_unsaved = false; + state->animator.last_timeline.clear(); + save_state(); + set_title(); } -auto Animator::load_animation(std::string_view uri) -> bool { - auto animation = m_loader.load_sprite_animation(uri); - if (!animation) { return false; } +auto Animator::load_timeline(std::string_view uri) -> bool { + auto timeline = m_loader.load_anim_timeline(uri); + if (!timeline) { return false; } - m_sprite.animation = std::move(*animation); + m_timeline = std::move(timeline); + m_sprite.set_timeline(m_timeline); m_unsaved = false; set_title(); @@ -272,28 +279,29 @@ auto Animator::load_animation(std::string_view uri) -> bool { return true; } -void Animator::load_new_animation(std::string_view const uri) { - if (!load_animation(uri)) { return; } +void Animator::load_new_timeline(std::string_view const uri) { + if (!load_timeline(uri)) { return; } - state->animator.last_animation = uri; + state->animator.last_timeline = uri; save_state(); } -void Animator::save_animation() { - if (state->animator.last_animation.empty()) { return; } +void Animator::save_timeline() { + if (state->animator.last_timeline.empty()) { return; } auto json = dj::Json{}; - json["duration"] = m_sprite.animation.duration.count(); - if (!m_sprite.animation.tiles.empty()) { + json["asset_type"] = get_asset_type(); + json["duration"] = m_timeline->duration.count(); + if (!m_timeline->tiles.empty()) { auto& out_tile_ids = json["tiles"]; - for (auto const& tile_id : m_sprite.animation.tiles) { out_tile_ids.push_back(tile_id); } + for (auto const& in_tile : m_timeline->tiles) { out_tile_ids.push_back(in_tile); } } - if (!save_json(json, state->animator.last_animation)) { - m_log.error("failed to save SpriteAnimation to '{}'", state->animator.last_animation); + if (!save_json(json, state->animator.last_timeline)) { + m_log.error("failed to save SpriteAnimation to '{}'", state->animator.last_timeline); return; } - m_log.info("saved SpriteAnimation to '{}'", state->animator.last_animation); + m_log.info("saved SpriteAnimation to '{}'", state->animator.last_timeline); m_unsaved = false; set_title(); } @@ -315,5 +323,5 @@ void Animator::setup_scene() { main_view.position.x = -150.0f; } -void Animator::set_title() { get_app().set_title(format_title("Animator", state->animator.last_animation, m_unsaved).c_str()); } +void Animator::set_title() { get_app().set_title(format_title("Animator", state->animator.last_timeline, m_unsaved).c_str()); } } // namespace bave::tools diff --git a/tools/src/tools/animator.hpp b/tools/src/tools/animator.hpp index fc10877..b96ecb0 100644 --- a/tools/src/tools/animator.hpp +++ b/tools/src/tools/animator.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -17,7 +18,7 @@ class Animator : public Applet { [[nodiscard]] auto can_load_anim() const -> bool { return m_texture != nullptr; } - void animation_control(); + void timeline_control(); [[nodiscard]] auto get_next_tile_id() const -> std::string; void metadata_control(); void misc_control(); @@ -28,10 +29,10 @@ class Animator : public Applet { auto load_atlas(std::string_view uri) -> bool; void load_new_atlas(std::string_view uri); - void new_animation(); - auto load_animation(std::string_view uri) -> bool; - void load_new_animation(std::string_view uri); - void save_animation(); + void new_timeline(); + auto load_timeline(std::string_view uri) -> bool; + void load_new_timeline(std::string_view uri); + void save_timeline(); void setup_scene(); void set_title(); @@ -44,6 +45,7 @@ class Animator : public Applet { QuadShape m_image_quad{}; SpriteAnim m_sprite{}; std::shared_ptr m_texture{}; + std::shared_ptr m_timeline{std::make_shared()}; Transform m_top_view{}; Transform m_bottom_view{}; diff --git a/tools/src/tools/applet.cpp b/tools/src/tools/applet.cpp index 2aaf084..5e91912 100644 --- a/tools/src/tools/applet.cpp +++ b/tools/src/tools/applet.cpp @@ -95,6 +95,22 @@ auto Applet::drag_ivec2(CString label, glm::ivec2& out, InclusiveRange& out, InclusiveRange> range, bool positional) -> bool { + auto ret = false; + ret |= ImGui::DragInt("left", &out.lt.x, 1.0f, range.lo.lt.x, range.hi.lt.x); + ret |= ImGui::DragInt("top", &out.lt.y, 1.0f, range.lo.lt.y, range.hi.lt.y); + ret |= ImGui::DragInt("right", &out.rb.x, 1.0f, range.lo.rb.x, range.hi.rb.x); + ret |= ImGui::DragInt("bottom", &out.rb.y, 1.0f, range.lo.rb.y, range.hi.rb.y); + if (positional) { + auto const rect_size = out.rb - out.lt; + if (drag_ivec2("position", out.lt, {.hi = range.hi.rb - rect_size})) { + out.rb = out.lt + rect_size; + ret = true; + } + } + return ret; +} + void Applet::zoom_control(CString label, glm::vec2& out_scale) { static constexpr auto zoom_range_f = InclusiveRange{.lo = zoom_scale_range_v.lo * 100.0f, .hi = zoom_scale_range_v.hi * 100.0f}; static constexpr auto zoom_range_i = InclusiveRange{static_cast(zoom_range_f.lo.x), static_cast(zoom_range_f.hi.x)}; diff --git a/tools/src/tools/applet.hpp b/tools/src/tools/applet.hpp index 157a3b9..9542393 100644 --- a/tools/src/tools/applet.hpp +++ b/tools/src/tools/applet.hpp @@ -48,6 +48,8 @@ class Applet : public Polymorphic { void begin_sidepanel_window(CString label, float min_width = 300.0f); static auto drag_ivec2(CString label, glm::ivec2& out, InclusiveRange range = {}, float width = 50.0f) -> bool; + static auto drag_irect(Rect& out, InclusiveRange> range = {}, bool positional = true) -> bool; + static void zoom_control(CString label, glm::vec2& out_scale); void wireframe_control(); static void image_meta_control(std::string_view image_uri, glm::ivec2 size); diff --git a/tools/src/tools/nine_slicer.cpp b/tools/src/tools/nine_slicer.cpp index a135b5e..ce9dee2 100644 --- a/tools/src/tools/nine_slicer.cpp +++ b/tools/src/tools/nine_slicer.cpp @@ -161,13 +161,16 @@ void NineSlicer::new_slice() { m_json_uri.clear(); m_unsaved = false; + state->nine_slicer.last_loaded.clear(); + save_state(); + main_view.scale = auto_zoom(m_image_quad->get_shape().size.current); set_title(); } auto NineSlicer::load_slice(std::string_view const uri) -> bool { - auto json = m_loader.load_json(uri); - if (!json || !json.contains("image") || !json.contains("nine_slice")) { return false; } + auto json = m_loader.load_json_asset(uri); + if (!json) { return false; } if (!load_image_at(json["image"].as_string())) { return false; } @@ -186,6 +189,7 @@ auto NineSlicer::load_slice(std::string_view const uri) -> bool { void NineSlicer::save_slice() { if (m_json_uri.empty()) { return; } auto json = dj::Json{}; + json["asset_type"] = get_asset_type(); json["image"] = m_image_uri; to_json(json["nine_slice"], m_image_quad->get_shape().slice); diff --git a/tools/src/tools/state.cpp b/tools/src/tools/state.cpp index 76a73ac..ffb5513 100644 --- a/tools/src/tools/state.cpp +++ b/tools/src/tools/state.cpp @@ -27,7 +27,7 @@ void State::load_members(dj::Json const& json) { if (auto const& in_tl = json["tiler"]) { tiler.last_loaded = in_tl["last_loaded"].as_string(); } if (auto const& in_an = json["animator"]) { - animator.last_animation = in_an["last_animation"].as_string(); + animator.last_timeline = in_an["last_timeline"].as_string(); animator.last_atlas = in_an["last_atlas"].as_string(); } } @@ -47,7 +47,7 @@ void State::save_members(dj::Json& out) const { if (!animator.is_empty()) { auto& out_an = out["animator"]; - if (!animator.last_animation.empty()) { out_an["last_animation"] = animator.last_animation; } + if (!animator.last_timeline.empty()) { out_an["last_timeline"] = animator.last_timeline; } if (!animator.last_atlas.empty()) { out_an["last_atlas"] = animator.last_atlas; } } } diff --git a/tools/src/tools/state.hpp b/tools/src/tools/state.hpp index 029538e..5f4d98f 100644 --- a/tools/src/tools/state.hpp +++ b/tools/src/tools/state.hpp @@ -27,9 +27,9 @@ struct State { struct { std::string last_atlas{}; - std::string last_animation{}; + std::string last_timeline{}; - [[nodiscard]] auto is_empty() const -> bool { return last_atlas.empty() && last_animation.empty(); } + [[nodiscard]] auto is_empty() const -> bool { return last_atlas.empty() && last_timeline.empty(); } } animator{}; diff --git a/tools/src/tools/tiler.cpp b/tools/src/tools/tiler.cpp index f62b419..bebd1b5 100644 --- a/tools/src/tools/tiler.cpp +++ b/tools/src/tools/tiler.cpp @@ -6,6 +6,7 @@ namespace bave::tools { namespace fs = std::filesystem; +// TODO: colliders Tiler::Tiler(App& app, NotNull> const& state) : Applet(app, state), m_loader(&get_app().get_data_store(), &get_app().get_render_device()) { m_sprite = push(std::make_unique()); @@ -40,13 +41,13 @@ void Tiler::render(Shader& shader) const { void Tiler::file_menu_items() { if (ImGui::MenuItem("New")) { new_atlas(); } if (ImGui::MenuItem("Open...")) { - if (auto uri = dialog_open_file("Open Image / TextureAtlas"); !uri.empty()) { load_new_uri(uri); } + if (auto uri = dialog_open_file("Open Image / TileSheet"); !uri.empty()) { load_new_uri(uri); } } if (ImGui::MenuItem("Save", nullptr, false, !m_json_uri.empty())) { save_atlas(); } if (ImGui::MenuItem("Save As...", nullptr, false, !m_image_uri.empty())) { auto json_uri = fs::path{m_json_uri}; - if (json_uri.empty()) { json_uri = replace_extension(m_image_uri, ".atlas.json"); } - if (auto uri = dialog_save_file("Save TextureAtlas", json_uri.generic_string()); !uri.empty()) { + if (json_uri.empty()) { json_uri = replace_extension(m_image_uri, ".sheet.json"); } + if (auto uri = dialog_save_file("Save TileSheet", json_uri.generic_string()); !uri.empty()) { m_json_uri = uri; save_atlas(); } @@ -72,21 +73,23 @@ auto Tiler::load_new_uri(std::string_view const uri) -> bool { void Tiler::tiles_control() { auto rgba = m_block_rgba.to_vec4(); if (ImGui::ColorEdit3("RGB", &rgba.x)) { m_block_rgba = Rgba::from(rgba); } - if (ImGui::Button("Add Block")) { m_blocks.push_back(make_block(static_cast(m_blocks.size()))); } + if (ImGui::Button("Add Tile")) { m_blocks.push_back(make_block(static_cast(m_blocks.size()))); } ImGui::Separator(); glm::ivec2 const size = m_sprite->get_size(); auto const origin_offset = 0.5f * glm::vec2{-size.x, size.y}; - im_text("Blocks"); + im_text("Tiles"); if (m_blocks.empty()) { im_text("[none]"); } + auto modified = false; for (std::size_t index = 0; index < m_blocks.size(); ++index) { auto erased = false; auto& block = m_blocks.at(index); - if (ImGui::TreeNode(FixedString{"{}###{}", block.block.id, index}.c_str())) { - block_control(block, index); + if (ImGui::TreeNode(FixedString{"{}###{}", block.tile.id, index}.c_str())) { + modified |= block_control(block, index); if (ImGui::SmallButton("Remove")) { m_blocks.erase(m_blocks.begin() + static_cast(index)); + modified = true; erased = true; } @@ -94,16 +97,16 @@ void Tiler::tiles_control() { if (erased) { break; } } - auto local_origin = 0.5f * glm::vec2{block.block.rect.lt + block.block.rect.rb}; + auto local_origin = 0.5f * glm::vec2{block.tile.image_rect.lt + block.tile.image_rect.rb}; local_origin.y = -local_origin.y; auto rect = LineRect{}; rect.origin = origin_offset + local_origin; - rect.size = glm::vec2{block.block.rect.rb - block.block.rect.lt}; + rect.size = glm::vec2{block.tile.image_rect.rb - block.tile.image_rect.lt}; block.rect.set_geometry(Geometry::from(rect)); } ImGui::Separator(); - if (ImGui::Button("Generate Blocks")) { ImGui::OpenPopup("Generate"); } + if (ImGui::Button("Generate Tiles")) { ImGui::OpenPopup("Generate"); } if (ImGui::BeginPopup("Generate")) { ImGui::SetNextItemWidth(30.0f); @@ -113,33 +116,44 @@ void Tiler::tiles_control() { ImGui::DragInt("cols", &m_tile_count.x, 1.0f, 1, 1000); if (ImGui::Button("Generate")) { generate_blocks(); + modified = true; ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } + + if (modified && !m_unsaved) { + m_unsaved = true; + set_title(); + } } -void Tiler::block_control(Block& out, std::size_t const index) const { +auto Tiler::block_control(Block& out, std::size_t const index) const -> bool { glm::ivec2 const size = m_sprite->get_size(); + auto ret = false; if (out.id("id")) { - out.block.id = out.id.as_view(); - if (out.block.id.empty()) { - out.block.id = std::to_string(index); - out.id.set_text(out.block.id); + ret = true; + out.tile.id = out.id.as_view(); + if (out.tile.id.empty()) { + out.tile.id = std::to_string(index); + out.id.set_text(out.tile.id); } } - ImGui::DragInt("left", &out.block.rect.lt.x, 1.0f, 0, out.block.rect.rb.x); - ImGui::DragInt("top", &out.block.rect.lt.y, 1.0f, 0, out.block.rect.rb.y); - ImGui::DragInt("right", &out.block.rect.rb.x, 1.0f, out.block.rect.lt.x, size.x); - ImGui::DragInt("bottom", &out.block.rect.rb.y, 1.0f, out.block.rect.lt.y, size.y); - - auto const rect_size = out.block.rect.rb - out.block.rect.lt; - if (drag_ivec2("position", out.block.rect.lt, {.hi = size - rect_size})) { out.block.rect.rb = out.block.rect.lt + rect_size; } + if (ImGui::TreeNode("image rect")) { + auto const range = InclusiveRange>{ + .lo = {.lt = {0, 0}, .rb = out.tile.image_rect.lt}, + .hi = {.lt = out.tile.image_rect.rb, .rb = size}, + }; + ret |= drag_irect(out.tile.image_rect, range); + ImGui::TreePop(); + } auto rgba = out.rect.tint.to_vec4(); if (ImGui::ColorEdit3("RGB", &rgba.x)) { out.rect.tint = Rgba::from(rgba); } + + return ret; } void Tiler::metadata_control() { @@ -185,30 +199,30 @@ auto Tiler::load_image_at(std::string_view const uri) -> bool { } void Tiler::new_atlas() { - m_sprite->set_texture({}); - m_sprite->set_size(Quad::size_v); + m_blocks.clear(); m_image_uri.clear(); m_json_uri.clear(); m_unsaved = false; + state->tiler.last_loaded.clear(); + save_state(); + main_view.scale = auto_zoom(m_sprite->get_size()); set_title(); } auto Tiler::load_atlas(std::string_view const uri) -> bool { - auto json = m_loader.load_json(uri); - if (!json || !json.contains("image") || !json.contains("blocks")) { return false; } + auto json = m_loader.load_json_asset(uri); + if (!json) { return false; } if (!load_image_at(json["image"].as_string())) { return false; } + auto sheet = TileSheet{}; + from_json(json["tile_sheet"], sheet); + m_blocks.clear(); - for (auto const& in_block : json["blocks"].array_view()) { - auto out_block = TextureAtlas::Block{}; - out_block.id = in_block["id"].as_string(); - from_json(in_block["rect"], out_block.rect); - m_blocks.push_back(make_block(std::move(out_block))); - } + for (auto& tile : sheet.tiles) { m_blocks.push_back(make_block(std::move(tile))); } m_unsaved = false; m_json_uri = uri; @@ -222,14 +236,12 @@ auto Tiler::load_atlas(std::string_view const uri) -> bool { void Tiler::save_atlas() { if (m_json_uri.empty()) { return; } auto json = dj::Json{}; + json["asset_type"] = get_asset_type(); json["image"] = m_image_uri; - auto& out_blocks = json["blocks"]; - for (auto const& in_block : m_blocks) { - auto out_block = dj::Json{}; - out_block["id"] = in_block.block.id; - to_json(out_block["rect"], in_block.block.rect); - out_blocks.push_back(std::move(out_block)); - } + auto in_sheet = TileSheet{}; + for (auto const& block : m_blocks) { in_sheet.tiles.push_back(block.tile); } + auto& out_sheet = json["tile_sheet"]; + to_json(out_sheet, in_sheet); if (!save_json(json, m_json_uri)) { m_log.error("failed to save TextureAtlas to '{}'", m_json_uri); @@ -247,7 +259,7 @@ void Tiler::save_atlas() { void Tiler::generate_blocks() { m_tile_count.x = std::max(m_tile_count.x, 1); m_tile_count.y = std::max(m_tile_count.y, 1); - auto const rects = TextureAtlas::make_rects(m_sprite->get_size(), m_tile_count); + auto const rects = TileSheet::make_rects(m_sprite->get_size(), m_tile_count); m_blocks.clear(); auto id = int{}; for (auto const& rect : rects) { m_blocks.push_back(make_block(id++, rect)); } @@ -255,16 +267,16 @@ void Tiler::generate_blocks() { auto Tiler::make_block(int id, Rect const& rect) const -> Block { auto ret = Block{}; - ret.block.rect = rect; - ret.block.id = std::to_string(id); - ret.id.set_text(ret.block.id); + ret.tile.image_rect = rect; + ret.tile.id = std::to_string(id); + ret.id.set_text(ret.tile.id); ret.rect.tint = m_block_rgba; return ret; } -auto Tiler::make_block(TextureAtlas::Block in) const -> Block { - auto ret = Block{.block = std::move(in)}; - ret.id.set_text(ret.block.id); +auto Tiler::make_block(TileSheet::Tile in) const -> Block { + auto ret = Block{.tile = std::move(in)}; + ret.id.set_text(ret.tile.id); ret.rect.tint = m_block_rgba; return ret; } diff --git a/tools/src/tools/tiler.hpp b/tools/src/tools/tiler.hpp index 96168f0..578a841 100644 --- a/tools/src/tools/tiler.hpp +++ b/tools/src/tools/tiler.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -8,7 +9,7 @@ namespace bave::tools { class Tiler : public Applet { struct Block { CustomShape rect{}; - TextureAtlas::Block block{}; + TileSheet::Tile tile{}; ImInputText id{}; }; @@ -21,7 +22,7 @@ class Tiler : public Applet { auto load_new_uri(std::string_view uri) -> bool final; void tiles_control(); - void block_control(Block& out, std::size_t index) const; + auto block_control(Block& out, std::size_t index) const -> bool; void metadata_control(); auto load_uri(std::string_view uri) -> bool; @@ -35,7 +36,7 @@ class Tiler : public Applet { void generate_blocks(); [[nodiscard]] auto make_block(int id, Rect const& rect = rect_v) const -> Block; - [[nodiscard]] auto make_block(TextureAtlas::Block in) const -> Block; + [[nodiscard]] auto make_block(TileSheet::Tile in) const -> Block; void set_title();