diff --git a/source/core/CMakeLists.txt b/source/core/CMakeLists.txt index 365da9f42e..fabba67ba2 100644 --- a/source/core/CMakeLists.txt +++ b/source/core/CMakeLists.txt @@ -123,6 +123,7 @@ target_sources(tactile-core "src/tactile/core/ui/menu/tileset_menu.cpp" "src/tactile/core/ui/menu/view_menu.cpp" "src/tactile/core/ui/render/hexagon_info.cpp" + "src/tactile/core/ui/render/orthogonal_renderer.cpp" "src/tactile/core/ui/canvas_overlay.cpp" "src/tactile/core/ui/canvas_renderer.cpp" "src/tactile/core/ui/fonts.cpp" @@ -259,6 +260,7 @@ target_sources(tactile-core "inc/tactile/core/ui/menu/tileset_menu.hpp" "inc/tactile/core/ui/menu/view_menu.hpp" "inc/tactile/core/ui/render/hexagon_info.hpp" + "inc/tactile/core/ui/render/orthogonal_renderer.hpp" "inc/tactile/core/ui/render/primitives.hpp" "inc/tactile/core/ui/canvas_overlay.hpp" "inc/tactile/core/ui/canvas_renderer.hpp" diff --git a/source/core/inc/tactile/core/ui/canvas_renderer.hpp b/source/core/inc/tactile/core/ui/canvas_renderer.hpp index 51b814a05f..c5e7af4ed6 100644 --- a/source/core/inc/tactile/core/ui/canvas_renderer.hpp +++ b/source/core/inc/tactile/core/ui/canvas_renderer.hpp @@ -175,6 +175,15 @@ class CanvasRenderer final [[nodiscard]] auto get_canvas_tile_size() const -> Float2; + /** + * Returns the canvas scale factor. + * + * \return + * A scale factor. + */ + [[nodiscard]] + auto get_scale() const -> float; + /** * Converts a screen-space position to the corresponding world-space position. * @@ -197,6 +206,9 @@ class CanvasRenderer final [[nodiscard]] auto to_screen_pos(const Float2& world_pos) const noexcept -> Float2; + [[nodiscard]] + auto to_screen_pos(const MatrixIndex& tile_pos) const noexcept -> Float2; + /** * Returns the draw list associated with the window. * @@ -222,6 +234,9 @@ class CanvasRenderer final /** The bottom-right window corner position (screen-space). */ Float2 mWindowBR; + /** The scale factor of rendered content. */ + float mScale; + /** The region visible via the current viewport. */ VisibleRegion mVisibleRegion; diff --git a/source/core/inc/tactile/core/ui/render/orthogonal_renderer.hpp b/source/core/inc/tactile/core/ui/render/orthogonal_renderer.hpp new file mode 100644 index 0000000000..0f3fba4004 --- /dev/null +++ b/source/core/inc/tactile/core/ui/render/orthogonal_renderer.hpp @@ -0,0 +1,22 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#pragma once + +#include "tactile/base/prelude.hpp" +#include "tactile/core/entity/entity.hpp" +#include "tactile/core/ui/canvas_renderer.hpp" + +namespace tactile { + +class Registry; + +namespace ui { + +class CanvasRenderer; + +void render_orthogonal_map(const CanvasRenderer& canvas_renderer, + const Registry& registry, + EntityID map_id); + +} // namespace ui +} // namespace tactile diff --git a/source/core/inc/tactile/core/ui/render/primitives.hpp b/source/core/inc/tactile/core/ui/render/primitives.hpp index 455e5c54ea..9f72c21733 100644 --- a/source/core/inc/tactile/core/ui/render/primitives.hpp +++ b/source/core/inc/tactile/core/ui/render/primitives.hpp @@ -6,7 +6,6 @@ #include -#include "tactile/base/meta/color.hpp" #include "tactile/base/numeric/vec.hpp" #include "tactile/base/prelude.hpp" #include "tactile/core/meta/color.hpp" @@ -37,6 +36,15 @@ inline void draw_rect(const Float2& screen_pos, } } +inline void draw_rect_shadowed(const Float2& screen_pos, + const Float2& size, + const UColor& color, + const float thickness = 1.0f) +{ + draw_rect(screen_pos + Float2 {thickness, thickness}, size, kColorBlack, thickness); + draw_rect(screen_pos, size, color, thickness); +} + /** * Renders a filled rectangle. * @@ -110,4 +118,56 @@ inline void draw_hexagon(const Float2& screen_pos, draw_ngon(screen_pos, radius, color_mask, 6, thickness, half_pi); } +inline void draw_hexagon_shadowed(const Float2& screen_pos, + const float radius, + const uint32 color_mask, + const float thickness = 1.0f) +{ + draw_hexagon(screen_pos + Float2 {thickness, thickness}, radius, IM_COL32_BLACK, thickness); + draw_hexagon(screen_pos, radius, color_mask, thickness); +} + +inline void draw_ellipse(const Float2& center_pos, + const Float2& radius, + const UColor& color, + const float thickness = 1.0f) +{ + if (auto* draw_list = ImGui::GetWindowDrawList()) { + draw_list->AddEllipse(to_imvec2(center_pos), + to_imvec2(radius), + to_uint32_abgr(color), + 0.0f, + 50, + thickness); + } +} + +inline void draw_ellipse_shadowed(const Float2& center_pos, + const Float2& radius, + const UColor& color, + const float thickness = 1.0f) +{ + draw_ellipse(center_pos + Float2 {thickness, thickness}, radius, kColorBlack, thickness); + draw_ellipse(center_pos, radius, color, thickness); +} + +inline void draw_circle(const Float2& center_pos, + const float radius, + const UColor& color, + const float thickness = 1.0f) +{ + if (auto* draw_list = ImGui::GetWindowDrawList()) { + draw_list->AddCircle(to_imvec2(center_pos), radius, to_uint32_abgr(color), 0, thickness); + } +} + +inline void draw_circle_shadowed(const Float2& center_pos, + const float radius, + const UColor& color, + const float thickness = 1.0f) +{ + draw_circle(center_pos + Float2 {thickness, thickness}, radius, kColorBlack, thickness); + draw_circle(center_pos, radius, color, thickness); +} + } // namespace tactile::ui diff --git a/source/core/src/tactile/core/ui/canvas_renderer.cpp b/source/core/src/tactile/core/ui/canvas_renderer.cpp index f0ea160e9c..550e55c354 100644 --- a/source/core/src/tactile/core/ui/canvas_renderer.cpp +++ b/source/core/src/tactile/core/ui/canvas_renderer.cpp @@ -78,6 +78,7 @@ CanvasRenderer::CanvasRenderer(const Float2& canvas_tl, mCanvasTileSize {vec_cast(tile_size) * viewport.scale}, mWindowTL {canvas_tl}, mWindowBR {canvas_br}, + mScale {viewport.scale}, mVisibleRegion {_get_visible_region(mViewportPos, mWindowBR - mWindowTL)}, mVisibleTiles {_get_visible_tiles(mVisibleRegion, mCanvasTileSize)}, mRenderBounds {_get_render_bounds(mVisibleTiles, mExtent)} @@ -181,6 +182,11 @@ auto CanvasRenderer::get_canvas_tile_size() const -> Float2 return mCanvasTileSize; } +auto CanvasRenderer::get_scale() const -> float +{ + return mScale; +} + auto CanvasRenderer::to_world_pos(const Float2& screen_pos) const noexcept -> Float2 { return screen_pos + mViewportPos; @@ -191,6 +197,11 @@ auto CanvasRenderer::to_screen_pos(const Float2& world_pos) const noexcept -> Fl return world_pos - mViewportPos + mWindowTL; } +auto CanvasRenderer::to_screen_pos(const MatrixIndex& tile_pos) const noexcept -> Float2 +{ + return to_screen_pos(to_float2(tile_pos) * mCanvasTileSize); +} + auto CanvasRenderer::get_draw_list() noexcept -> ImDrawList& { auto* const draw_list = ImGui::GetWindowDrawList(); diff --git a/source/core/src/tactile/core/ui/dock/document_dock.cpp b/source/core/src/tactile/core/ui/dock/document_dock.cpp index 888f54a31c..67520bf203 100644 --- a/source/core/src/tactile/core/ui/dock/document_dock.cpp +++ b/source/core/src/tactile/core/ui/dock/document_dock.cpp @@ -11,102 +11,113 @@ #include "tactile/core/event/event_dispatcher.hpp" #include "tactile/core/event/viewport_events.hpp" #include "tactile/core/map/map.hpp" -#include "tactile/core/meta/color.hpp" #include "tactile/core/model/model.hpp" +#include "tactile/core/ui/canvas_overlay.hpp" #include "tactile/core/ui/canvas_renderer.hpp" #include "tactile/core/ui/common/buttons.hpp" +#include "tactile/core/ui/common/overlays.hpp" #include "tactile/core/ui/common/widgets.hpp" #include "tactile/core/ui/common/window.hpp" #include "tactile/core/ui/i18n/language.hpp" +#include "tactile/core/ui/render/orthogonal_renderer.hpp" #include "tactile/core/ui/viewport.hpp" namespace tactile::ui { namespace { -void _render_map(const Float2& canvas_tl, - const Float2& canvas_br, - const Registry& registry, - const EntityID map_id) +void _push_map_document_overlay(const Registry& registry, + const EntityID map_id, + const CanvasRenderer& canvas_renderer) { - const auto& map = registry.get(map_id); - const auto& viewport = registry.get(map_id); - - const CanvasRenderer canvas_renderer {canvas_tl, - canvas_br, - map.extent, - map.tile_size, - viewport}; - - constexpr UColor bg_color {0x33, 0x33, 0x33, 0xFF}; - canvas_renderer.clear_canvas(bg_color); - canvas_renderer.draw_orthogonal_grid(kColorBlack); + if (const OverlayScope overlay {"##MapDocumentOverlay", Float2 {1.0f, 0.0f}, 0.5f}; + overlay.is_open()) { + const auto& viewport = registry.get(map_id); + + push_viewport_info_section(viewport); + push_canvas_info_section(canvas_renderer); + push_viewport_mouse_info_section(canvas_renderer); + } } void _push_document_tab(const IDocument& document, EventDispatcher& dispatcher) { - const auto canvas_tl = ImGui::GetCursorScreenPos(); - const auto canvas_br = canvas_tl + ImGui::GetContentRegionAvail(); - const auto canvas_size = canvas_br - canvas_tl; - const auto& registry = document.get_registry(); const auto& document_info = registry.get(); - const auto& viewport = registry.get(document_info.root); - if (viewport.size != to_float2(canvas_size)) { - dispatcher.push(document_info.root, to_float2(canvas_size)); - } + const IdScope document_scope {document_info.root}; - if (is_map(registry, document_info.root)) { - _render_map(to_float2(canvas_tl), to_float2(canvas_br), registry, document_info.root); - } - else { - // TODO render tileset - } + if (const TabItemScope tab {document_info.name.c_str()}; tab.is_open()) { + const auto canvas_tl = ImGui::GetCursorScreenPos(); + const auto canvas_br = canvas_tl + ImGui::GetContentRegionAvail(); + const auto canvas_size = canvas_br - canvas_tl; - ImGui::InvisibleButton("##Canvas", canvas_size, ImGuiButtonFlags_MouseButtonLeft); + const auto& viewport = registry.get(document_info.root); + if (viewport.size != to_float2(canvas_size)) { + dispatcher.push(document_info.root, to_float2(canvas_size)); + } - if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left, 0.0f)) { - const auto delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Left, 0.0f); - dispatcher.push(document_info.root, to_float2(-delta)); - ImGui::ResetMouseDragDelta(ImGuiMouseButton_Left); - } + const auto tile_size = document.get_tile_size(); + const auto extent = document.get_extent(); - if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_8, ImGuiInputFlags_RouteFocused)) { - dispatcher.push(document_info.root); - } + const CanvasRenderer canvas_renderer {to_float2(canvas_tl), + to_float2(canvas_br), + extent, + tile_size, + viewport}; - if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_9, - ImGuiInputFlags_RouteFocused | ImGuiInputFlags_Repeat)) { - dispatcher.push(document_info.root); - } + if (is_map(registry, document_info.root)) { + render_orthogonal_map(canvas_renderer, registry, document_info.root); + _push_map_document_overlay(registry, document_info.root, canvas_renderer); + } + else { + // TODO render tileset + } - if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_0, - ImGuiInputFlags_RouteFocused | ImGuiInputFlags_Repeat)) { - dispatcher.push(document_info.root); - } + ImGui::InvisibleButton("##Canvas", canvas_size, ImGuiButtonFlags_MouseButtonLeft); - if (ImGui::Shortcut(ImGuiMod_Shift | ImGuiKey_Space, ImGuiInputFlags_RouteFocused)) { - dispatcher.push(document_info.root, document.get_content_size()); - } + if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left, 0.0f)) { + const auto delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Left, 0.0f); + dispatcher.push(document_info.root, to_float2(-delta)); + ImGui::ResetMouseDragDelta(ImGuiMouseButton_Left); + } - if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_UpArrow, - ImGuiInputFlags_RouteFocused | ImGuiInputFlags_Repeat)) { - dispatcher.push(document_info.root); - } + if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_8, ImGuiInputFlags_RouteFocused)) { + dispatcher.push(document_info.root); + } - if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_DownArrow, - ImGuiInputFlags_RouteFocused | ImGuiInputFlags_Repeat)) { - dispatcher.push(document_info.root); - } + if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_9, + ImGuiInputFlags_RouteFocused | ImGuiInputFlags_Repeat)) { + dispatcher.push(document_info.root); + } - if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_LeftArrow, - ImGuiInputFlags_RouteFocused | ImGuiInputFlags_Repeat)) { - dispatcher.push(document_info.root); - } + if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_0, + ImGuiInputFlags_RouteFocused | ImGuiInputFlags_Repeat)) { + dispatcher.push(document_info.root); + } + + if (ImGui::Shortcut(ImGuiMod_Shift | ImGuiKey_Space, ImGuiInputFlags_RouteFocused)) { + dispatcher.push(document_info.root, document.get_content_size()); + } + + if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_UpArrow, + ImGuiInputFlags_RouteFocused | ImGuiInputFlags_Repeat)) { + dispatcher.push(document_info.root); + } + + if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_DownArrow, + ImGuiInputFlags_RouteFocused | ImGuiInputFlags_Repeat)) { + dispatcher.push(document_info.root); + } + + if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_LeftArrow, + ImGuiInputFlags_RouteFocused | ImGuiInputFlags_Repeat)) { + dispatcher.push(document_info.root); + } - if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_RightArrow, - ImGuiInputFlags_RouteFocused | ImGuiInputFlags_Repeat)) { - dispatcher.push(document_info.root); + if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_RightArrow, + ImGuiInputFlags_RouteFocused | ImGuiInputFlags_Repeat)) { + dispatcher.push(document_info.root); + } } } @@ -118,14 +129,7 @@ void _push_document_tabs(const Model& model, EventDispatcher& dispatcher) for (const auto& document_uuid : open_documents) { const auto& document = document_manager.get_document(document_uuid); - - const auto& registry = document.get_registry(); - const auto& document_info = registry.get(); - - const IdScope document_scope {document_info.root}; - if (const TabItemScope tab {document_info.name.c_str()}; tab.is_open()) { - _push_document_tab(document, dispatcher); - } + _push_document_tab(document, dispatcher); } } } diff --git a/source/core/src/tactile/core/ui/render/orthogonal_renderer.cpp b/source/core/src/tactile/core/ui/render/orthogonal_renderer.cpp new file mode 100644 index 0000000000..6236ba0c92 --- /dev/null +++ b/source/core/src/tactile/core/ui/render/orthogonal_renderer.cpp @@ -0,0 +1,187 @@ +// Copyright (C) 2024 Albin Johansson (GNU General Public License v3.0) + +#include "tactile/core/ui/render/orthogonal_renderer.hpp" + +#include // min + +#include "tactile/base/meta/color.hpp" +#include "tactile/core/debug/assert.hpp" +#include "tactile/core/entity/registry.hpp" +#include "tactile/core/io/texture.hpp" +#include "tactile/core/layer/group_layer.hpp" +#include "tactile/core/layer/layer.hpp" +#include "tactile/core/layer/object.hpp" +#include "tactile/core/layer/object_layer.hpp" +#include "tactile/core/layer/tile_layer.hpp" +#include "tactile/core/map/map.hpp" +#include "tactile/core/meta/color.hpp" +#include "tactile/core/tile/tileset.hpp" +#include "tactile/core/ui/canvas_renderer.hpp" +#include "tactile/core/ui/common/window.hpp" +#include "tactile/core/ui/imgui_compat.hpp" +#include "tactile/core/ui/render/primitives.hpp" +#include "tactile/core/util/lookup.hpp" + +namespace tactile::ui { +namespace { + +void _render_tile(const CanvasRenderer& canvas_renderer, + const MatrixIndex& position_in_world, + const MatrixIndex& position_in_tileset, + void* texture_handle, + const Float2& uv_tile_size) +{ + const auto position_in_texture = to_float2(position_in_tileset) * uv_tile_size; + + const auto world_pos = canvas_renderer.to_screen_pos(position_in_world); + const auto world_size = canvas_renderer.get_canvas_tile_size(); + + auto* draw_list = ImGui::GetWindowDrawList(); + draw_list->AddImage(texture_handle, + to_imvec2(world_pos), + to_imvec2(world_pos + world_size), + to_imvec2(position_in_texture), + to_imvec2(position_in_texture + uv_tile_size)); +} + +void _render_tile_layer(const CanvasRenderer& canvas_renderer, + const Registry& registry, + const EntityID layer_id) +{ + const auto& render_bounds = canvas_renderer.get_render_bounds(); + const auto& tile_cache = registry.get(); + + each_layer_tile( + registry, + layer_id, + render_bounds.begin, + render_bounds.end, + [&](const MatrixIndex& position_in_world, const TileID tile_id) { + if (tile_id == kEmptyTile) { + return; + } + + const auto tileset_id = lookup_in(tile_cache.tileset_mapping, tile_id); + + const auto& texture = registry.get(tileset_id); + const auto& tileset = registry.get(tileset_id); + const auto& tileset_instance = registry.get(tileset_id); + + const TileIndex tile_index {tile_id - tileset_instance.tile_range.first_id}; + const auto apparent_tile_index = get_tile_appearance(registry, tileset_id, tile_index); + + const auto position_in_tileset = + make_matrix_index(static_cast(apparent_tile_index), + tileset.extent.cols); + + _render_tile(canvas_renderer, + position_in_world, + position_in_tileset, + texture.raw_handle, + tileset.uv_tile_size); + }); +} + +void _render_object(const CanvasRenderer& canvas_renderer, + const Registry& registry, + const EntityID object_id) +{ + const auto& object = registry.get(object_id); + + if (!object.is_visible) { + return; + } + + // TODO cull objects outside of render bounds + + const auto canvas_scale = canvas_renderer.get_scale(); + const auto scaled_pos = object.position * canvas_scale; + const auto scaled_size = object.size * canvas_scale; + const auto screen_pos = canvas_renderer.to_screen_pos(scaled_pos); + + constexpr auto line_color = kColorYellow; + constexpr auto line_thickness = 2.0f; + + switch (object.type) { + case ObjectType::kPoint: { + const auto radius = std::min(canvas_renderer.get_canvas_tile_size().x() * 0.2f, 6.0f); + draw_circle_shadowed(screen_pos, radius, line_color, line_thickness); + break; + } + case ObjectType::kRect: { + draw_rect_shadowed(screen_pos, scaled_size, line_color, line_thickness); + break; + } + case ObjectType::kEllipse: { + const auto radius = scaled_size * 0.5f; + draw_ellipse_shadowed(screen_pos + radius, radius, line_color, line_thickness); + break; + } + } +} + +void _render_object_layer(const CanvasRenderer& canvas_renderer, + const Registry& registry, + const EntityID layer_id) +{ + const auto& object_layer = registry.get(layer_id); + for (const auto object_id : object_layer.objects) { + _render_object(canvas_renderer, registry, object_id); + } +} + +void _render_layer(const CanvasRenderer& canvas_renderer, + const Registry& registry, + const EntityID layer_id) +{ + if (const auto& layer = registry.get(layer_id); !layer.visible) { + return; + } + + if (is_tile_layer(registry, layer_id)) { + _render_tile_layer(canvas_renderer, registry, layer_id); + } + else if (is_object_layer(registry, layer_id)) { + _render_object_layer(canvas_renderer, registry, layer_id); + } + else if (is_group_layer(registry, layer_id)) { + const auto& group_layer = registry.get(layer_id); + for (const auto sublayer_id : group_layer.layers) { + _render_layer(canvas_renderer, registry, sublayer_id); + } + } +} + +} // namespace + +void render_orthogonal_map(const CanvasRenderer& canvas_renderer, + const Registry& registry, + const EntityID map_id) +{ + TACTILE_ASSERT(is_map(registry, map_id)); + + constexpr UColor bg_color {50, 50, 50, 255}; + constexpr UColor border_color {255, 0, 0, 255}; + constexpr UColor grid_color {0, 0, 0, 50}; + + canvas_renderer.clear_canvas(bg_color); + + const auto& map = registry.get(map_id); + const auto& root_layer = registry.get(map.root_layer); + + for (const auto layer_id : root_layer.layers) { + _render_layer(canvas_renderer, registry, layer_id); + } + + canvas_renderer.draw_orthogonal_grid(grid_color); + + const auto tile_size = canvas_renderer.get_canvas_tile_size(); + const Float2 map_size { + static_cast(map.extent.cols) * tile_size.x(), + static_cast(map.extent.rows) * tile_size.y(), + }; + + draw_rect(canvas_renderer.to_screen_pos(Float2 {0, 0}), map_size, border_color, 2.0f); +} + +} // namespace tactile::ui