diff --git a/Kernel/src/renderive/base/node/Directed_Acyclic_Node.hpp b/Kernel/src/renderive/base/node/Directed_Acyclic_Node.hpp new file mode 100644 index 0000000..5e8c688 --- /dev/null +++ b/Kernel/src/renderive/base/node/Directed_Acyclic_Node.hpp @@ -0,0 +1,101 @@ +#pragma once +#include +#include +#include +#include +#include + +template +class Directed_Acyclic_Node { +public: + explicit Directed_Acyclic_Node( + Owner* owner = nullptr, + std::pmr::memory_resource& memory_resource = *std::pmr::get_default_resource()) noexcept + : owner_(owner), parents_(&memory_resource), children_(&memory_resource) {} + Directed_Acyclic_Node(const Directed_Acyclic_Node&) = delete; + Directed_Acyclic_Node& operator=(const Directed_Acyclic_Node&) = delete; + Directed_Acyclic_Node(Directed_Acyclic_Node&&) = delete; + Directed_Acyclic_Node& operator=(Directed_Acyclic_Node&&) = delete; + ~Directed_Acyclic_Node() { + detach(); + while (!children_.empty()) { + children_.front()->remove_parent(*this); + } + } + + void append_child(Directed_Acyclic_Node& child) { + insert_child(children_.size(), child); + } + + void insert_child(std::size_t index, Directed_Acyclic_Node& child) { + if (index > children_.size()) { + throw std::out_of_range("directed acyclic node child index"); + } + if (&child == this || child.is_ancestor_of(*this)) { + throw std::invalid_argument("directed acyclic node cycle"); + } + const auto current = std::find(children_.begin(), children_.end(), &child); + if (current != children_.end()) { + const auto current_index = static_cast(current - children_.begin()); + if (current_index < index) { + --index; + } + if (current_index == index) { + return; + } + children_.erase(current); + children_.insert(children_.begin() + static_cast(index), &child); + return; + } + children_.insert(children_.begin() + static_cast(index), &child); + child.parents_.push_back(this); + } + + void replace_parent(Directed_Acyclic_Node* parent) { + detach(); + if (parent) { + parent->append_child(*this); + } + } + + void remove_parent(Directed_Acyclic_Node& parent) noexcept { + std::erase(parents_, &parent); + std::erase(parent.children_, this); + } + + void detach() noexcept { + while (!parents_.empty()) { + remove_parent(*parents_.front()); + } + } + + bool has_parent(const Directed_Acyclic_Node& parent) const noexcept { + return std::find(parents_.begin(), parents_.end(), &parent) != parents_.end(); + } + + bool is_ancestor_of(const Directed_Acyclic_Node& node) const noexcept { + for (Directed_Acyclic_Node* child : children_) { + if (child == &node || child->is_ancestor_of(node)) { + return true; + } + } + return false; + } + + Owner* owner() noexcept { return owner_; } + const Owner* owner() const noexcept { return owner_; } + Directed_Acyclic_Node* parent() noexcept { return parents_.empty() ? nullptr : parents_.front(); } + const Directed_Acyclic_Node* parent() const noexcept { return parents_.empty() ? nullptr : parents_.front(); } + std::size_t parent_count() const noexcept { return parents_.size(); } + Directed_Acyclic_Node& parent(std::size_t index) { return *parents_.at(index); } + const Directed_Acyclic_Node& parent(std::size_t index) const { return *parents_.at(index); } + std::size_t child_count() const noexcept { return children_.size(); } + bool children_empty() const noexcept { return children_.empty(); } + Directed_Acyclic_Node& child(std::size_t index) { return *children_.at(index); } + const Directed_Acyclic_Node& child(std::size_t index) const { return *children_.at(index); } + +private: + Owner* owner_{}; + std::pmr::vector parents_; + std::pmr::vector children_; +}; diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base.hpp b/Kernel/src/renderive/renderable/base/Renderable_Base.hpp index 3230016..c784534 100644 --- a/Kernel/src/renderive/renderable/base/Renderable_Base.hpp +++ b/Kernel/src/renderive/renderable/base/Renderable_Base.hpp @@ -6,7 +6,7 @@ #include #include #include "renderive/base/memory/Memory_Resource.hpp" -#include "renderive/base/node/Multiway_Node.hpp" +#include "renderive/base/node/Directed_Acyclic_Node.hpp" #include "renderive/renderable/Renderable_Task_Graph.hpp" #include "renderive/scene/base/Scene_Lifetime.hpp" class Frame_Strategy_Real_Time_Data_Observer; @@ -27,8 +27,8 @@ private: std::shared_ptr memory_domain_; std::shared_ptr real_time_data_state_; public: - using Layer_Node = Multiway_Node; - using Dependency_Node = Multiway_Node; + using Layer_Node = Directed_Acyclic_Node; + using Dependency_Node = Directed_Acyclic_Node; explicit Renderable_Base(Scene_Base& scene, Renderable_Configuration configuration = {}); virtual ~Renderable_Base(); virtual void render(const Scene_Render_Context& context); diff --git a/Kernel/src/renderive/scene/Scene2D_Context.hpp b/Kernel/src/renderive/scene/Scene2D_Context.hpp index 1324e3d..b56785b 100644 --- a/Kernel/src/renderive/scene/Scene2D_Context.hpp +++ b/Kernel/src/renderive/scene/Scene2D_Context.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include "renderive/base/memory/Memory_Resource.hpp" @@ -76,8 +75,8 @@ protected: Cache_Pointer cache = make_cache_pointer(this->memory_resource()); auto& display_node = Scene_Base::layer_node(renderable); auto& dependency_node = Scene_Base::dependency_node(renderable); - const bool attach_display = !display_node.parent(); - const bool attach_dependency = !dependency_node.parent(); + const bool attach_display = display_node.parent_count() == 0; + const bool attach_dependency = dependency_node.parent_count() == 0; try { if (attach_display) { display_root_.append_child(display_node); @@ -98,17 +97,17 @@ protected: renderable.invalidate_cache(); } void on_renderable_detached(Renderable_Base& renderable) override { - promote_children(Scene_Base::layer_node(renderable)); + promote_graph_children(Scene_Base::layer_node(renderable), display_root_); auto& dependency = Scene_Base::dependency_node(renderable); for (std::size_t index = 0; index < dependency.child_count(); ++index) { dependency.child(index).owner()->invalidate_cache(); } - promote_children(dependency); + promote_graph_children(dependency, dependency_root_); color_caches_.erase(&renderable); } void prepare_render_task(Render_Task& task, const Renderable_List& renderables) override { - task.render_order = tree_order(dependency_root_, renderables); - task.display_order = tree_order(display_root_, renderables); + task.render_order = Scene_Base::dependency_order(renderables); + task.display_order = Scene_Base::display_order(renderables); } Color_Cache* prepare_renderable_cache(Renderable_Base& renderable, bool clear) override { Cache* cache = color_caches_.at(&renderable).get(); @@ -157,55 +156,23 @@ private: } } template - void promote_children(Node& node) { - if (!node.parent()) { - return; + void promote_graph_children(Node& node, Node& root) { + std::pmr::vector parents(&this->memory_resource()); + parents.reserve(node.parent_count()); + for (std::size_t index = 0; index < node.parent_count(); ++index) { + parents.push_back(&node.parent(index)); } - Node* parent = node.parent(); - std::size_t index{}; - while (&parent->child(index) != &node) { - ++index; - } - std::pmr::vector children(&this->memory_resource()); - children.reserve(node.child_count()); - for (std::size_t child_index = 0; child_index < node.child_count(); ++child_index) { - children.push_back(&node.child(child_index)); + while (!node.children_empty()) { + auto& child = node.child(0); + child.remove_parent(node); + for (auto* parent : parents) { + parent->append_child(child); + } + if (child.parent_count() == 0) { + root.append_child(child); + } } node.detach(); - for (Node* child : children) { - parent->insert_child(index++, *child); - } - } - template - static void append_tree_order(Node& node, const Attached& attached, Visited& visited, Renderable_List& order) { - if (node.owner()) { - auto iterator = attached.find(node.owner()); - if (iterator != attached.end() && visited.insert(node.owner()).second) { - order.push_back(iterator->second); - } - } - for (std::size_t index = 0; index < node.child_count(); ++index) { - append_tree_order(node.child(index), attached, visited, order); - } - } - template - Renderable_List tree_order(Node& root, const Renderable_List& renderables) { - std::pmr::monotonic_buffer_resource scratch_resource(&this->memory_resource()); - std::pmr::unordered_map attached(&scratch_resource); - attached.reserve(renderables.size()); - for (const Renderable& renderable : renderables) { - attached.emplace(renderable.get(), renderable); - } - std::pmr::unordered_set visited(&scratch_resource); - Renderable_List order(&this->memory_resource()); - order.reserve(renderables.size()); - append_tree_order(root, attached, visited, order); - for (const Renderable& renderable : renderables) { - if (visited.insert(renderable.get()).second) { - order.push_back(renderable); - } - } - return order; } Cache final_color_cache_; Renderable_Base::Layer_Node display_root_; diff --git a/Kernel/src/renderive/scene/Scene3D_Context.hpp b/Kernel/src/renderive/scene/Scene3D_Context.hpp index f867e81..db491e7 100644 --- a/Kernel/src/renderive/scene/Scene3D_Context.hpp +++ b/Kernel/src/renderive/scene/Scene3D_Context.hpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "renderive/base/observer/Observer.hpp" #include "renderive/frame_control/Frame_Control.hpp" #include "renderive/state/Triple_State_Strategy.hpp" @@ -48,12 +49,12 @@ protected: return frame_control; } void on_renderable_detached(Renderable_Base& renderable) override { - promote_children(Scene_Base::layer_node(renderable)); + promote_graph_children(Scene_Base::layer_node(renderable)); auto& dependency = Scene_Base::dependency_node(renderable); for (std::size_t index = 0; index < dependency.child_count(); ++index) { dependency.child(index).owner()->invalidate_cache(); } - promote_children(dependency); + promote_graph_children(dependency); } std::uint64_t acquire_scene_state() override { return this->Scene_State_Strategy::acquire_render_state(); @@ -66,22 +67,20 @@ protected: } private: template - static void promote_children(Node& node) { - Node* parent = node.parent(); - if (!parent) { - while (!node.children_empty()) { - node.child(0).detach(); - } - return; + static void promote_graph_children(Node& node) { + std::vector parents; + parents.reserve(node.parent_count()); + for (std::size_t index = 0; index < node.parent_count(); ++index) { + parents.push_back(&node.parent(index)); } - std::size_t index{}; - while (&parent->child(index) != &node) { - ++index; + while (!node.children_empty()) { + auto& child = node.child(0); + child.remove_parent(node); + for (auto* parent : parents) { + parent->append_child(child); + } } node.detach(); - while (!node.children_empty()) { - parent->insert_child(index++, node.child(0)); - } } template static Frame_Control make_frame_control(std::pmr::memory_resource& memory_resource, Args&&... args) { diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp index 37578e3..8006cda 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -5,9 +5,62 @@ #include #include #include +#include #include #include "renderive/renderable/color/Color_Cache.hpp" static_assert(TF_VERSION == 400100, "Renderive requires Taskflow 4.1.0"); +namespace { +template +Scene_Base::Renderable_List topological_order( + const Scene_Base::Renderable_List& renderables, + std::pmr::memory_resource& memory_resource, + Node_Accessor node_of, + const char* graph_name) { + std::pmr::monotonic_buffer_resource scratch_resource(&memory_resource); + std::pmr::unordered_map attached(&scratch_resource); + std::pmr::unordered_map indegree(&scratch_resource); + attached.reserve(renderables.size()); + indegree.reserve(renderables.size()); + for (const auto& renderable : renderables) { + attached.emplace(renderable.get(), renderable); + indegree.emplace(renderable.get(), 0U); + } + for (const auto& renderable : renderables) { + auto& node = node_of(*renderable); + for (std::size_t index = 0; index < node.parent_count(); ++index) { + auto* parent = node.parent(index).owner(); + if (parent && attached.contains(parent)) { + ++indegree.at(renderable.get()); + } + } + } + std::pmr::vector ready(&scratch_resource); + ready.reserve(renderables.size()); + for (const auto& renderable : renderables) { + if (indegree.at(renderable.get()) == 0) { + ready.push_back(renderable.get()); + } + } + Scene_Base::Renderable_List order(&memory_resource); + order.reserve(renderables.size()); + for (std::size_t ready_index = 0; ready_index < ready.size(); ++ready_index) { + Renderable_Base* renderable = ready[ready_index]; + order.push_back(attached.at(renderable)); + auto& node = node_of(*renderable); + for (std::size_t child_index = 0; child_index < node.child_count(); ++child_index) { + Renderable_Base* child = node.child(child_index).owner(); + auto iterator = indegree.find(child); + if (iterator != indegree.end() && --iterator->second == 0) { + ready.push_back(child); + } + } + } + if (order.size() != renderables.size()) { + throw std::logic_error(std::string(graph_name) + " graph contains a cycle"); + } + return order; +} +} class Scene_Base::Execution_Context { public: static tf::Executor& executor() { @@ -148,15 +201,32 @@ void Scene_Base::set_display_parent(Renderable_Base& renderable, Renderable_Base } auto& node = layer_node(renderable); auto* target = parent ? &layer_node(*parent) : display_root_node(); - if (node.parent() == target) { + if (node.parent_count() == 1 && node.parent() == target) { return; } if (target) { - target->append_child(node); + node.replace_parent(target); } else { node.detach(); } } +void Scene_Base::add_display_parent(Renderable_Base& renderable, Renderable_Base& parent) { + auto task_lock = lock_render_idle(); + validate_renderable_scene(renderable); + validate_renderable_scene(parent); + std::lock_guard lock(renderable_mutex_); + validate_renderable_attached_locked(renderable); + validate_renderable_attached_locked(parent); + auto& node = layer_node(renderable); + auto& parent_node = layer_node(parent); + if (node.has_parent(parent_node)) { + return; + } + if (auto* root = display_root_node(); root && node.has_parent(*root)) { + node.remove_parent(*root); + } + parent_node.append_child(node); +} void Scene_Base::set_dependency_parent(Renderable_Base& renderable, Renderable_Base* parent) { auto task_lock = lock_render_idle(); validate_renderable_scene(renderable); @@ -170,16 +240,34 @@ void Scene_Base::set_dependency_parent(Renderable_Base& renderable, Renderable_B } auto& node = dependency_node(renderable); auto* target = parent ? &dependency_node(*parent) : dependency_root_node(); - if (node.parent() == target) { + if (node.parent_count() == 1 && node.parent() == target) { return; } if (target) { - target->append_child(node); + node.replace_parent(target); } else { node.detach(); } renderable.invalidate_cache(); } +void Scene_Base::add_dependency_parent(Renderable_Base& renderable, Renderable_Base& parent) { + auto task_lock = lock_render_idle(); + validate_renderable_scene(renderable); + validate_renderable_scene(parent); + std::lock_guard lock(renderable_mutex_); + validate_renderable_attached_locked(renderable); + validate_renderable_attached_locked(parent); + auto& node = dependency_node(renderable); + auto& parent_node = dependency_node(parent); + if (node.has_parent(parent_node)) { + return; + } + if (auto* root = dependency_root_node(); root && node.has_parent(*root)) { + node.remove_parent(*root); + } + parent_node.append_child(node); + renderable.invalidate_cache(); +} void Scene_Base::set_renderable_configuration(Renderable_Base& renderable, Renderable_Configuration configuration) { auto task_lock = lock_render_idle(); validate_renderable_scene(renderable); @@ -204,15 +292,30 @@ Scene_Base::Topology_Snapshot Scene_Base::topology_snapshot() const { snapshot.renderables.push_back(std::move(value)); } for (const Renderable& renderable : *cache_renderables_) { - const auto* display_parent_node = renderable->layer_node_.parent(); - const auto* dependency_parent_node = renderable->dependency_node_.parent(); - const auto* display_parent = display_parent_node ? display_parent_node->owner() : nullptr; - const auto* dependency_parent = dependency_parent_node ? dependency_parent_node->owner() : nullptr; - snapshot.display.push_back({renderables.at(renderable.get()), display_parent ? renderables.at(display_parent) : Const_Renderable{}}); - snapshot.dependency.push_back({renderables.at(renderable.get()), dependency_parent ? renderables.at(dependency_parent) : Const_Renderable{}}); + if (renderable->layer_node_.parent_count() == 0) { + snapshot.display.push_back({renderables.at(renderable.get()), Const_Renderable{}}); + } else { + for (std::size_t parent_index = 0; parent_index < renderable->layer_node_.parent_count(); ++parent_index) { + const auto* display_parent = renderable->layer_node_.parent(parent_index).owner(); + snapshot.display.push_back({renderables.at(renderable.get()), display_parent ? renderables.at(display_parent) : Const_Renderable{}}); + } + } + if (renderable->dependency_node_.parent_count() == 0) { + snapshot.dependency.push_back({renderables.at(renderable.get()), Const_Renderable{}}); + continue; + } + for (std::size_t parent_index = 0; parent_index < renderable->dependency_node_.parent_count(); ++parent_index) { + const auto* dependency_parent = renderable->dependency_node_.parent(parent_index).owner(); + snapshot.dependency.push_back({renderables.at(renderable.get()), dependency_parent ? renderables.at(dependency_parent) : Const_Renderable{}}); + } } return snapshot; } +std::vector Scene_Base::paint_order_snapshot() const { + std::lock_guard lock(renderable_mutex_); + const auto ordered = display_order(*cache_renderables_); + return {ordered.begin(), ordered.end()}; +} std::pmr::memory_resource& Scene_Base::memory_resource() const noexcept { return memory_domain_->resource(); } @@ -237,6 +340,16 @@ Renderable_Base::Layer_Node& Scene_Base::layer_node(Renderable_Base& renderable) Renderable_Base::Dependency_Node& Scene_Base::dependency_node(Renderable_Base& renderable) noexcept { return renderable.dependency_node_; } +Scene_Base::Renderable_List Scene_Base::display_order(const Renderable_List& renderables) const { + return topological_order(renderables, memory_resource(), [](Renderable_Base& renderable) -> auto& { + return renderable.layer_node_; + }, "display"); +} +Scene_Base::Renderable_List Scene_Base::dependency_order(const Renderable_List& renderables) const { + return topological_order(renderables, memory_resource(), [](Renderable_Base& renderable) -> auto& { + return renderable.dependency_node_; + }, "dependency"); +} Renderable_Base::Layer_Node* Scene_Base::display_root_node() noexcept { return nullptr; } @@ -401,10 +514,12 @@ void Scene_Base::execute_taskflow(Render_Task& task) { } for (std::size_t index = 0; index < task.render_order.size(); ++index) { auto& node = dependency_node(*task.render_order[index]); - Renderable_Base* parent = node.parent() ? node.parent()->owner() : nullptr; - auto parent_iterator = indices.find(parent); - if (parent_iterator != indices.end()) { - modules[parent_iterator->second].module_task.precede(modules[index].module_task); + for (std::size_t parent_index = 0; parent_index < node.parent_count(); ++parent_index) { + Renderable_Base* parent = node.parent(parent_index).owner(); + auto parent_iterator = indices.find(parent); + if (parent_iterator != indices.end()) { + modules[parent_iterator->second].module_task.precede(modules[index].module_task); + } } } const Scene_Render_Context final_context{this, task.frame_control_state, task.scene_state_revision, task.render_sequence, nullptr, nullptr}; diff --git a/Kernel/src/renderive/scene/base/Scene_Base.hpp b/Kernel/src/renderive/scene/base/Scene_Base.hpp index c96b2bc..3215716 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.hpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.hpp @@ -57,10 +57,13 @@ public: void attach_renderable(Renderable renderable); void detach_renderable(Renderable_Base& renderable); void set_display_parent(Renderable_Base& renderable, Renderable_Base* parent); + void add_display_parent(Renderable_Base& renderable, Renderable_Base& parent); void set_dependency_parent(Renderable_Base& renderable, Renderable_Base* parent); + void add_dependency_parent(Renderable_Base& renderable, Renderable_Base& parent); void set_renderable_configuration(Renderable_Base& renderable, Renderable_Configuration configuration); std::size_t renderable_count() const; Topology_Snapshot topology_snapshot() const; + std::vector paint_order_snapshot() const; std::pmr::memory_resource& memory_resource() const noexcept; std::pmr::memory_resource& upstream_memory_resource() const noexcept; Frame_Control_Strategy_Base& frame_control_strategy(); @@ -82,6 +85,8 @@ protected: }; static Renderable_Base::Layer_Node& layer_node(Renderable_Base& renderable) noexcept; static Renderable_Base::Dependency_Node& dependency_node(Renderable_Base& renderable) noexcept; + Renderable_List display_order(const Renderable_List& renderables) const; + Renderable_List dependency_order(const Renderable_List& renderables) const; virtual Frame_Control_Strategy_Base& frame_control_strategy_impl(); virtual const Frame_Control_Strategy_Base& frame_control_strategy_impl() const; virtual Renderable_Base::Layer_Node* display_root_node() noexcept; diff --git a/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp b/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp index 7eec0dc..a955057 100644 --- a/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene2D_Context_Test.cpp @@ -118,6 +118,35 @@ TEST(scene2d_context_test, dependency_reparent_invalidates_cached_child) { EXPECT_EQ(second_parent->render_count, 1); EXPECT_EQ(child->render_count, 2); } +TEST(scene2d_context_test, multiple_dependency_parents_invalidate_cached_child) { + Scene2D_Context<> scene; + auto first_parent = std::make_shared(scene, 1); + auto second_parent = std::make_shared(scene, 2); + auto child = std::make_shared(scene, 3); + scene.attach_renderable(first_parent); + scene.attach_renderable(second_parent); + scene.attach_renderable(child); + scene.set_dependency_parent(*child, first_parent.get()); + scene.add_dependency_parent(*child, *second_parent); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(child->render_count, 1); + first_parent->invalidate_cache(); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(child->render_count, 2); + second_parent->invalidate_cache(); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(child->render_count, 3); + int dependency_edges{}; + for (const auto& relation : scene.topology_snapshot().dependency) { + if (relation.child.get() == child.get() && relation.parent) { + ++dependency_edges; + } + } + EXPECT_EQ(dependency_edges, 2); +} TEST(scene2d_context_test, final_color_cache_callback_can_reenter_scene_control_api) { Scene2D_Context<> scene; auto renderable = std::make_shared(scene, 9); diff --git a/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp b/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp index 2abd9c5..c890ef5 100644 --- a/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene2D_Render_Order_Test.cpp @@ -37,3 +37,26 @@ TEST(scene2d_render_order_test, separates_dependency_order_from_display_order) { EXPECT_EQ(final_values.at(0), 2); EXPECT_EQ(final_values.at(1), 1); } +TEST(scene2d_render_order_test, shared_overlay_waits_for_every_display_parent) { + Scene2D_Context<> scene; + Scene2D_Render_Order_State state; + auto first_plot = std::make_shared(scene, state, 2); + auto axis = std::make_shared(scene, state, 1); + auto second_plot = std::make_shared(scene, state, 3); + scene.attach_renderable(first_plot); + scene.attach_renderable(axis); + scene.attach_renderable(second_plot); + scene.add_dependency_parent(*first_plot, *axis); + scene.add_dependency_parent(*second_plot, *axis); + scene.add_display_parent(*axis, *first_plot); + scene.add_display_parent(*axis, *second_plot); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(state.render_order.front(), 1); + std::vector final_values; + scene.with_final_color_cache([&final_values](const Recording_Color_Cache& cache) { + final_values.assign(cache.values.begin(), cache.values.end()); + }); + ASSERT_EQ(final_values.size(), 3); + EXPECT_EQ(final_values.back(), 1); +} diff --git a/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp index 81a9acd..55b6e4b 100644 --- a/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp +++ b/Kernel/tests/renderive/scene/base/Scene_Base_Test.cpp @@ -164,6 +164,18 @@ TEST(scene_base_test, setting_same_topology_parent_is_a_noop) { } EXPECT_TRUE(found); } +TEST(scene_base_test, display_graph_rejects_cycles) { + Scene2D_Context<> scene; + auto first = std::make_shared(scene); + auto second = std::make_shared(scene); + auto third = std::make_shared(scene); + scene.attach_renderable(first); + scene.attach_renderable(second); + scene.attach_renderable(third); + scene.set_display_parent(*second, first.get()); + scene.add_display_parent(*third, *second); + EXPECT_THROW(scene.add_display_parent(*first, *third), std::invalid_argument); +} TEST(scene_base_test, topology_snapshot_retains_renderable_lifetime) { Scene2D_Context<> scene; auto renderable = std::make_shared(scene); diff --git a/render_2D/axis/Abs_Axis.cpp b/render_2D/axis/Abs_Axis.cpp new file mode 100644 index 0000000..95ef3e7 --- /dev/null +++ b/render_2D/axis/Abs_Axis.cpp @@ -0,0 +1,155 @@ +#include "Abs_Axis.h" + +#include "Axis_Format.h" +#include "../render/Blend2D_Cache.h" + +#include +#include + +namespace renderive { + +Abs_Axis::Abs_Axis(Plot_Core& plot, Orientation orientation) + : Abs_Axis(plot, Properties{.orientation = orientation}) {} + +Abs_Axis::Abs_Axis(Plot_Core& plot, const Properties& properties) + : Base(properties, plot, true) {} + +Abs_Axis::~Abs_Axis() = default; + +#define RENDERIVE_AXIS_PROPERTY(Type, Name) \ + Type Abs_Axis::Name() const { return Base::template get<&Properties::Name>(); } \ + void Abs_Axis::set_##Name(Type value) { set_axis_property<&Properties::Name>(std::move(value)); } + +RENDERIVE_AXIS_PROPERTY(int, x) +RENDERIVE_AXIS_PROPERTY(int, y) +RENDERIVE_AXIS_PROPERTY(Orientation, orientation) +RENDERIVE_AXIS_PROPERTY(std::size_t, pixel_length) +RENDERIVE_AXIS_PROPERTY(int, tick_length) +RENDERIVE_AXIS_PROPERTY(int, sub_tick_length) +RENDERIVE_AXIS_PROPERTY(Color, color) +RENDERIVE_AXIS_PROPERTY(Number_Locale, locale) +RENDERIVE_AXIS_PROPERTY(std::string, unit_text) +RENDERIVE_AXIS_PROPERTY(Font, unit_text_font) +RENDERIVE_AXIS_PROPERTY(Pen, unit_text_pen) +RENDERIVE_AXIS_PROPERTY(Brush, unit_text_background_brush) +RENDERIVE_AXIS_PROPERTY(int, label_rotation_degrees) + +#undef RENDERIVE_AXIS_PROPERTY + +Abs_Axis::Properties Abs_Axis::axis_state() const { + return Base::read([](const Properties& value) { return value; }); +} + +Abs_Axis::Properties Abs_Axis::render_axis_state() const { + return Base::render_use_state(); +} + +Axis_Transform Abs_Axis::transform() const { + const Properties state = axis_state(); + return { + coord_range(), + state.orientation == Orientation::Horizontal ? static_cast(state.x) + : static_cast(state.y), + static_cast(state.pixel_length) + }; +} + +double Abs_Axis::pixel_to_coord(double pixel) const { return transform().pixel_to_coord(pixel); } +double Abs_Axis::coord_to_pixel(double coordinate) const { return transform().coord_to_pixel(coordinate); } +double Abs_Axis::start_coord() const { return coord_range().origin; } +double Abs_Axis::end_coord() const { return coord_range().target; } + +int Abs_Axis::pixel_sample_count(Range range) const { + const Axis_Transform value = transform(); + const double first = value.coord_to_pixel(range.origin); + const double last = value.coord_to_pixel(range.target); + return std::max(0, static_cast(std::abs(last - first)) + 1); +} + +int Abs_Axis::pixel_sample_count() const { + const auto length = pixel_length(); + return static_cast(length) + (length > 0 ? 1 : 0); +} + +double Abs_Axis::tick_step(Range range) const { + const double raw = range.size() / 5.0; + if (!(raw > 0.0) || !std::isfinite(raw)) + return 1.0; + const double scale = std::pow(10.0, std::floor(std::log10(raw))); + const double normalized = raw / scale; + const double nice = normalized <= 1.0 ? 1.0 : normalized <= 2.0 ? 2.0 : normalized <= 5.0 ? 5.0 : 10.0; + return nice * scale; +} + +int Abs_Axis::sub_tick_count(double) const { return 4; } + +std::string Abs_Axis::tick_label(double tick) const { + return detail::localized_axis_number(tick, 2, locale()); +} + +void Abs_Axis::paint(detail::Painter& painter) { + const Properties state = render_axis_state(); + if (state.pixel_length == 0) + return; + const Range coordinates = coord_range(); + const double step = tick_step(coordinates); + if (!(step > 0.0)) + return; + + const Pen axis_pen{state.color, 1.0}; + const PointF first{static_cast(state.x), static_cast(state.y)}; + const PointF last = state.orientation == Orientation::Horizontal + ? PointF{first.x + state.pixel_length, first.y} + : PointF{first.x, first.y + state.pixel_length}; + painter.line(first, last, axis_pen); + + const auto [low, high] = std::minmax(coordinates.origin, coordinates.target); + const double initial = std::ceil(low / step) * step; + int tick_index{}; + for (double tick = initial; tick <= high + step * 1e-6 && tick_index < 1000; + tick += step, ++tick_index) { + const double pixel = coord_to_pixel(tick); + PointF tick_start; + PointF tick_end; + PointF label; + if (state.orientation == Orientation::Horizontal) { + tick_start = {pixel, static_cast(state.y)}; + tick_end = {pixel, static_cast(state.y + state.tick_length)}; + label = {pixel + 2.0, static_cast(state.y + state.tick_length + 2)}; + } else { + tick_start = {static_cast(state.x), pixel}; + tick_end = {static_cast(state.x + state.tick_length), pixel}; + label = {static_cast(state.x + state.tick_length + 2), pixel - 7.0}; + } + painter.line(tick_start, tick_end, axis_pen); + painter.text(label, tick_label(tick), state.unit_text_font, state.unit_text_pen, + state.label_rotation_degrees); + const int subdivisions = std::max(0, sub_tick_count(step)); + for (int sub_index = 1; sub_index <= subdivisions; ++sub_index) { + const double sub_tick = tick + step * sub_index / (subdivisions + 1.0); + if (sub_tick >= high) + break; + const double sub_pixel = coord_to_pixel(sub_tick); + if (state.orientation == Orientation::Horizontal) { + painter.line({sub_pixel, static_cast(state.y)}, + {sub_pixel, static_cast(state.y + state.sub_tick_length)}, + axis_pen); + } else { + painter.line({static_cast(state.x), sub_pixel}, + {static_cast(state.x + state.sub_tick_length), sub_pixel}, + axis_pen); + } + } + } + if (!state.unit_text.empty()) { + const PointF position{last.x + 4.0, last.y + 4.0}; + const double estimated_width = std::max(4.0, state.unit_text.size() * state.unit_text_font.size * 0.65); + painter.rect({position.x - 2.0, position.y - 2.0, + estimated_width + 4.0, state.unit_text_font.size * 1.5 + 4.0}, + Pen{.style = Line_Style::None}, state.unit_text_background_brush); + painter.text(position, state.unit_text, + state.unit_text_font, state.unit_text_pen); + } +} + +} // namespace renderive diff --git a/render_2D/axis/Abs_Axis.h b/render_2D/axis/Abs_Axis.h new file mode 100644 index 0000000..93d8b4a --- /dev/null +++ b/render_2D/axis/Abs_Axis.h @@ -0,0 +1,73 @@ +#pragma once + +#include "../renderable/Renderable.h" +#include "Axis_Types.h" + +#include + +#include +#include + +namespace renderive { + +class LIB_DECL Abs_Axis : public Double_State_Strategy { +public: + using Properties = Axis_Base_Properties; + using Base = Double_State_Strategy; + + explicit Abs_Axis(Plot_Core& plot, Orientation orientation = Orientation::Horizontal); + Abs_Axis(Plot_Core& plot, const Properties& properties); + ~Abs_Axis() override; + + [[nodiscard]] int x() const; + void set_x(int value); + [[nodiscard]] int y() const; + void set_y(int value); + [[nodiscard]] Orientation orientation() const; + void set_orientation(Orientation value); + [[nodiscard]] std::size_t pixel_length() const; + void set_pixel_length(std::size_t value); + [[nodiscard]] int tick_length() const; + void set_tick_length(int value); + [[nodiscard]] int sub_tick_length() const; + void set_sub_tick_length(int value); + [[nodiscard]] Color color() const; + void set_color(Color value); + [[nodiscard]] Number_Locale locale() const; + void set_locale(Number_Locale value); + [[nodiscard]] std::string unit_text() const; + void set_unit_text(std::string value); + [[nodiscard]] Font unit_text_font() const; + void set_unit_text_font(Font value); + [[nodiscard]] Pen unit_text_pen() const; + void set_unit_text_pen(Pen value); + [[nodiscard]] Brush unit_text_background_brush() const; + void set_unit_text_background_brush(Brush value); + [[nodiscard]] int label_rotation_degrees() const; + void set_label_rotation_degrees(int value); + + [[nodiscard]] virtual Range coord_range() const = 0; + [[nodiscard]] virtual double pixel_to_coord(double pixel) const; + [[nodiscard]] virtual double coord_to_pixel(double coordinate) const; + [[nodiscard]] Axis_Transform transform() const; + [[nodiscard]] double start_coord() const; + [[nodiscard]] double end_coord() const; + [[nodiscard]] int pixel_sample_count(Range range) const; + [[nodiscard]] int pixel_sample_count() const; + [[nodiscard]] virtual double tick_step(Range range) const; + [[nodiscard]] virtual int sub_tick_count(double major_step) const; + [[nodiscard]] virtual std::string tick_label(double tick) const; + +protected: + [[nodiscard]] Properties axis_state() const; + [[nodiscard]] Properties render_axis_state() const; + void paint(detail::Painter& painter) override; + + template Value> + void set_axis_property(Value&& value) { + Base::template set(std::forward(value)); + changed(); + } +}; + +} // namespace renderive diff --git a/render_2D/axis/Axis.cpp b/render_2D/axis/Axis.cpp deleted file mode 100644 index 862727a..0000000 --- a/render_2D/axis/Axis.cpp +++ /dev/null @@ -1,406 +0,0 @@ -#include "Axis.h" - -#include "../plot/Plot_Core.h" -#include "../render/Blend2D_Cache.h" - -#include -#include -#include -#include - -namespace renderive { -namespace { - -bool valid_range(Range range) { - return std::isfinite(range.origin) && std::isfinite(range.target) && range.size() > 0.0; -} - -std::string fixed_number(double value, int precision) { - std::ostringstream stream; - stream << std::fixed << std::setprecision(std::clamp(precision, 0, 12)) << value; - std::string result = stream.str(); - if (result.find('.') != std::string::npos) { - while (!result.empty() && result.back() == '0') - result.pop_back(); - if (!result.empty() && result.back() == '.') - result.pop_back(); - } - return result; -} - -std::string localized_number(double value, int precision, Number_Locale locale) { - std::string result = fixed_number(value, precision); - if (locale.decimal_point != '.') - std::replace(result.begin(), result.end(), '.', locale.decimal_point); - return result; -} - -std::string formatted_time(Time_Of_Day time, std::string_view format) { - const auto total = time.milliseconds; - const int hours = static_cast((total / 3'600'000) % 24); - const int minutes = static_cast((total / 60'000) % 60); - const int seconds = static_cast((total / 1'000) % 60); - const int milliseconds = static_cast(total % 1'000); - const auto digits = [](int value, int width) { - std::ostringstream stream; - stream << std::setfill('0') << std::setw(width) << value; - return stream.str(); - }; - std::string result; - for (std::size_t index = 0; index < format.size();) { - const std::string_view rest = format.substr(index); - if (rest.starts_with("zzz")) { - result += digits(milliseconds, 3); - index += 3; - } else if (rest.starts_with("hh") || rest.starts_with("HH")) { - result += digits(hours, 2); - index += 2; - } else if (rest.starts_with("mm")) { - result += digits(minutes, 2); - index += 2; - } else if (rest.starts_with("ss")) { - result += digits(seconds, 2); - index += 2; - } else { - result.push_back(format[index++]); - } - } - return result; -} - -} // namespace - -Abs_Axis::Abs_Axis(Plot_Core& plot, Orientation orientation) : Renderable(plot, true) { - axis_state_.orientation = orientation; -} - -Abs_Axis::~Abs_Axis() = default; - -#define RENDERIVE_AXIS_PROPERTY(Type, Name) \ - Type Abs_Axis::Name() const { std::lock_guard lock(axis_mutex_); return axis_state_.Name; } \ - void Abs_Axis::set_##Name(Type value) { \ - { std::lock_guard lock(axis_mutex_); if (axis_state_.Name == value) return; axis_state_.Name = std::move(value); } \ - changed(); \ - } - -RENDERIVE_AXIS_PROPERTY(int, x) -RENDERIVE_AXIS_PROPERTY(int, y) -RENDERIVE_AXIS_PROPERTY(Orientation, orientation) -RENDERIVE_AXIS_PROPERTY(std::size_t, pixel_length) -RENDERIVE_AXIS_PROPERTY(int, tick_length) -RENDERIVE_AXIS_PROPERTY(int, sub_tick_length) -RENDERIVE_AXIS_PROPERTY(Color, color) -RENDERIVE_AXIS_PROPERTY(Number_Locale, locale) -RENDERIVE_AXIS_PROPERTY(std::string, unit_text) -RENDERIVE_AXIS_PROPERTY(Font, unit_text_font) -RENDERIVE_AXIS_PROPERTY(Pen, unit_text_pen) -RENDERIVE_AXIS_PROPERTY(Brush, unit_text_background_brush) -RENDERIVE_AXIS_PROPERTY(int, label_rotation_degrees) - -#undef RENDERIVE_AXIS_PROPERTY - -Abs_Axis::State Abs_Axis::axis_state() const { - std::lock_guard lock(axis_mutex_); - return axis_state_; -} - -Axis_Transform Abs_Axis::transform() const { - const State state = axis_state(); - return { - coord_range(), - state.orientation == Orientation::Horizontal ? static_cast(state.x) - : static_cast(state.y), - static_cast(state.pixel_length) - }; -} - -double Abs_Axis::pixel_to_coord(double pixel) const { return transform().pixel_to_coord(pixel); } -double Abs_Axis::coord_to_pixel(double coordinate) const { return transform().coord_to_pixel(coordinate); } -double Abs_Axis::start_coord() const { return coord_range().origin; } -double Abs_Axis::end_coord() const { return coord_range().target; } - -int Abs_Axis::pixel_sample_count(Range range) const { - const Axis_Transform value = transform(); - const double first = value.coord_to_pixel(range.origin); - const double last = value.coord_to_pixel(range.target); - return std::max(0, static_cast(std::abs(last - first)) + 1); -} - -int Abs_Axis::pixel_sample_count() const { - return static_cast(pixel_length()) + (pixel_length() > 0 ? 1 : 0); -} - -double Abs_Axis::tick_step(Range range) const { - const double raw = range.size() / 5.0; - if (!(raw > 0.0) || !std::isfinite(raw)) - return 1.0; - const double scale = std::pow(10.0, std::floor(std::log10(raw))); - const double normalized = raw / scale; - const double nice = normalized <= 1.0 ? 1.0 : normalized <= 2.0 ? 2.0 : normalized <= 5.0 ? 5.0 : 10.0; - return nice * scale; -} - -int Abs_Axis::sub_tick_count(double) const { return 4; } - -std::string Abs_Axis::tick_label(double tick) const { - return localized_number(tick, 2, locale()); -} - -void Abs_Axis::paint(detail::Painter& painter) { - const State state = axis_state(); - if (state.pixel_length == 0) - return; - const Range coordinates = coord_range(); - const double step = tick_step(coordinates); - if (!(step > 0.0)) - return; - - const Pen axis_pen{state.color, 1.0}; - const PointF first{static_cast(state.x), static_cast(state.y)}; - const PointF last = state.orientation == Orientation::Horizontal - ? PointF{first.x + state.pixel_length, first.y} - : PointF{first.x, first.y + state.pixel_length}; - painter.line(first, last, axis_pen); - - const auto [low, high] = std::minmax(coordinates.origin, coordinates.target); - const double initial = std::ceil(low / step) * step; - int tick_index{}; - for (double tick = initial; tick <= high + step * 1e-6 && tick_index < 1000; - tick += step, ++tick_index) { - const double pixel = coord_to_pixel(tick); - PointF tick_start; - PointF tick_end; - PointF label; - if (state.orientation == Orientation::Horizontal) { - tick_start = {pixel, static_cast(state.y)}; - tick_end = {pixel, static_cast(state.y + state.tick_length)}; - label = {pixel + 2.0, static_cast(state.y + state.tick_length + 2)}; - } else { - tick_start = {static_cast(state.x), pixel}; - tick_end = {static_cast(state.x + state.tick_length), pixel}; - label = {static_cast(state.x + state.tick_length + 2), pixel - 7.0}; - } - painter.line(tick_start, tick_end, axis_pen); - painter.text(label, tick_label(tick), state.unit_text_font, state.unit_text_pen, - state.label_rotation_degrees); - const int subdivisions = std::max(0, sub_tick_count(step)); - for (int sub_index = 1; sub_index <= subdivisions; ++sub_index) { - const double sub_tick = tick + step * sub_index / (subdivisions + 1.0); - if (sub_tick >= high) - break; - const double sub_pixel = coord_to_pixel(sub_tick); - if (state.orientation == Orientation::Horizontal) { - painter.line({sub_pixel, static_cast(state.y)}, - {sub_pixel, static_cast(state.y + state.sub_tick_length)}, - axis_pen); - } else { - painter.line({static_cast(state.x), sub_pixel}, - {static_cast(state.x + state.sub_tick_length), sub_pixel}, - axis_pen); - } - } - } - if (!state.unit_text.empty()) { - const PointF position{last.x + 4.0, last.y + 4.0}; - const double estimated_width = std::max(4.0, state.unit_text.size() * state.unit_text_font.size * 0.65); - painter.rect({position.x - 2.0, position.y - 2.0, - estimated_width + 4.0, state.unit_text_font.size * 1.5 + 4.0}, - Pen{.style = Line_Style::None}, state.unit_text_background_brush); - painter.text(position, state.unit_text, - state.unit_text_font, state.unit_text_pen); - } -} - -Axis::Axis(Plot_Core& plot, Orientation orientation) : Abs_Axis(plot, orientation) {} - -Range Axis::coord_range() const { std::lock_guard lock(interaction_mutex_); return coordinates_; } -int Axis::label_precision() const { std::lock_guard lock(interaction_mutex_); return precision_; } -void Axis::set_label_precision(int value) { { std::lock_guard lock(interaction_mutex_); precision_ = std::clamp(value, 0, 12); } changed(); } -double Axis::coord_start() const { return coord_range().origin; } -void Axis::set_coord_start(double value) { auto range = coord_range(); set_coord_range({value, value + range.length()}); } -double Axis::coord_length() const { return coord_range().length(); } -void Axis::set_coord_length(double value) { auto range = coord_range(); set_coord_range({range.origin, range.origin + value}); } -void Axis::set_coord_range(Range range) { - if (!valid_range(range)) - return; - { - std::lock_guard lock(interaction_mutex_); - if (coordinates_ == range) - return; - coordinates_ = range; - } - changed(); -} -void Axis::set_use_wheel(bool value) { std::lock_guard lock(interaction_mutex_); wheel_enabled_ = value; } -void Axis::set_use_drag(bool value) { std::lock_guard lock(interaction_mutex_); drag_enabled_ = value; } -bool Axis::use_wheel() const { std::lock_guard lock(interaction_mutex_); return wheel_enabled_; } -bool Axis::use_drag() const { std::lock_guard lock(interaction_mutex_); return drag_enabled_; } - -void Axis::handle_event(const Event& event) { - if (event.type == Event_Type::Wheel && use_wheel()) { - const auto& wheel = static_cast(event); - Range range = coord_range(); - const double anchor_pixel = orientation() == Orientation::Horizontal ? wheel.position.x : wheel.position.y; - const double anchor = pixel_to_coord(anchor_pixel); - const double factor = wheel.angle_delta_y >= 0.0 ? 0.9 : 1.1; - set_coord_range({anchor + (range.origin - anchor) * factor, - anchor + (range.target - anchor) * factor}); - event.accept(); - return; - } - if (!use_drag()) - return; - if (event.type == Event_Type::Pointer_Press) { - const auto& pointer = static_cast(event); - if (pointer.button == Mouse_Button::Left) { - std::lock_guard lock(interaction_mutex_); - dragging_ = true; - last_pointer_ = pointer.position; - event.accept(); - } - } else if (event.type == Event_Type::Pointer_Move) { - const auto& pointer = static_cast(event); - PointF previous; - { - std::lock_guard lock(interaction_mutex_); - if (!dragging_) - return; - previous = last_pointer_; - last_pointer_ = pointer.position; - } - const double delta = orientation() == Orientation::Horizontal - ? pointer.position.x - previous.x - : pointer.position.y - previous.y; - Range range = coord_range(); - const double shift = pixel_length() == 0 ? 0.0 : -delta * range.length() / pixel_length(); - set_coord_range({range.origin + shift, range.target + shift}); - event.accept(); - } else if (event.type == Event_Type::Pointer_Release) { - std::lock_guard lock(interaction_mutex_); - if (dragging_) { - dragging_ = false; - event.accept(); - } - } -} - -std::string Axis::tick_label(double tick) const { - return localized_number(tick, label_precision(), locale()); -} - -Axis::Builder::Builder(std::shared_ptr parent, Orientation orientation) - : Axis_Builder_Base(std::move(parent), orientation) {} - -std::shared_ptr Axis::Builder::build() { - if (!parent_) - return {}; - auto result = parent_->plot().make_renderable(parent_, orientation_); - apply(*result); - result->set_coord_range(coordinates_); - result->set_label_precision(precision_); - result->set_use_wheel(wheel_); - result->set_use_drag(drag_); - return result; -} - -Frequency_Axis::Frequency_Axis(Plot_Core& plot, Orientation orientation) : Axis(plot, orientation) {} - -std::string Frequency_Axis::tick_label(double tick) const { - const double absolute = std::abs(tick); - if (absolute >= 1'000'000.0) - return localized_number(tick / 1'000'000.0, label_precision(), locale()) + " MHz"; - if (absolute >= 1'000.0) - return localized_number(tick / 1'000.0, label_precision(), locale()) + " kHz"; - return localized_number(tick, label_precision(), locale()) + " Hz"; -} - -Frequency_Axis::Builder::Builder(std::shared_ptr parent, Orientation orientation) - : Axis_Builder_Base(std::move(parent), orientation) {} - -std::shared_ptr Frequency_Axis::Builder::build() { - if (!parent_) - return {}; - auto result = parent_->plot().make_renderable(parent_, orientation_); - apply(*result); - result->set_coord_range(coordinates_); - result->set_label_precision(precision_); - result->set_use_wheel(wheel_); - result->set_use_drag(drag_); - return result; -} - -Time_Axis::Time_Axis(Plot_Core& plot, Orientation orientation) : Abs_Axis(plot, orientation) {} - -int Time_Axis::visible_time_point_count() const { std::lock_guard lock(time_mutex_); return time_state_.visible_count; } -void Time_Axis::set_visible_time_point_count(int value) { { std::lock_guard lock(time_mutex_); time_state_.visible_count = std::max(2, value); } changed(); } -int Time_Axis::tick_label_spacing_px() const { std::lock_guard lock(time_mutex_); return time_state_.tick_label_spacing_px; } -void Time_Axis::set_tick_label_spacing_px(int value) { { std::lock_guard lock(time_mutex_); time_state_.tick_label_spacing_px = std::max(0, value); } changed(); } -std::string Time_Axis::time_format() const { std::lock_guard lock(time_mutex_); return time_state_.format; } -void Time_Axis::set_time_format(std::string value) { { std::lock_guard lock(time_mutex_); time_state_.format = std::move(value); } changed(); } -Font Time_Axis::font() const { return unit_text_font(); } -void Time_Axis::set_font(Font value) { set_unit_text_font(value); } -bool Time_Axis::newest_at_axis_start() const { std::lock_guard lock(time_mutex_); return time_state_.newest_at_start; } -void Time_Axis::set_newest_at_axis_start(bool value) { { std::lock_guard lock(time_mutex_); time_state_.newest_at_start = value; } changed(); } -std::size_t Time_Axis::time_point_count() const { std::lock_guard lock(time_mutex_); return time_state_.samples.size(); } - -int Time_Axis::append_time(Time_Of_Day time) { - int tick{}; - { - std::lock_guard lock(time_mutex_); - tick = time_state_.next_tick++; - time_state_.samples.emplace_back(tick, time); - const auto limit = static_cast(std::max(512, time_state_.visible_count * 4)); - while (time_state_.samples.size() > limit) - time_state_.samples.pop_front(); - } - changed(); - return tick; -} - -Time_Of_Day Time_Axis::tick_to_time(int tick) const { - std::lock_guard lock(time_mutex_); - auto iterator = std::find_if(time_state_.samples.begin(), time_state_.samples.end(), - [tick](const auto& value) { return value.first == tick; }); - return iterator == time_state_.samples.end() ? Time_Of_Day{} : iterator->second; -} - -Range Time_Axis::coord_range() const { - std::lock_guard lock(time_mutex_); - const int latest = std::max(1, time_state_.next_tick - 1); - const int earliest = std::max(0, latest - time_state_.visible_count + 1); - return time_state_.newest_at_start ? Range{static_cast(latest), static_cast(earliest)} - : Range{static_cast(earliest), static_cast(latest)}; -} - -double Time_Axis::tick_step(Range range) const { - const double available = static_cast(pixel_length()); - const double label_width = std::max(48.0, font().size * 7.0); - const double spacing = static_cast(tick_label_spacing_px()); - const double label_count = std::max(1.0, available / (label_width + spacing)); - return std::max(1.0, std::ceil(range.size() / label_count)); -} - -std::string Time_Axis::tick_label(double tick) const { - const Time_Of_Day time = tick_to_time(static_cast(std::llround(tick))); - if (!time.valid()) - return {}; - return formatted_time(time, time_format()); -} - -Time_Axis::Builder::Builder(std::shared_ptr parent, Orientation orientation) - : Axis_Builder_Base(std::move(parent), orientation) {} - -std::shared_ptr Time_Axis::Builder::build() { - if (!parent_) - return {}; - auto result = parent_->plot().make_renderable(parent_, orientation_); - apply(*result); - result->set_visible_time_point_count(visible_count_); - result->set_tick_label_spacing_px(spacing_); - result->set_time_format(format_); - result->set_font(font_); - result->set_newest_at_axis_start(newest_at_start_); - return result; -} - -} // namespace renderive diff --git a/render_2D/axis/Axis.h b/render_2D/axis/Axis.h index e367e73..57a92b0 100644 --- a/render_2D/axis/Axis.h +++ b/render_2D/axis/Axis.h @@ -1,266 +1,6 @@ #pragma once -#include "../renderable/Renderable.h" - -#include -#include -#include -#include -#include -#include - -namespace renderive { - -struct Axis_Transform { - Range coordinate_range; - double pixel_origin{}; - double pixel_length{}; - - [[nodiscard]] double coord_to_pixel(double coordinate) const noexcept { - const double span = coordinate_range.length(); - if (span == 0.0) - return pixel_origin; - return pixel_origin + (coordinate - coordinate_range.origin) / span * pixel_length; - } - [[nodiscard]] double pixel_to_coord(double pixel) const noexcept { - if (pixel_length == 0.0) - return coordinate_range.origin; - return coordinate_range.origin + (pixel - pixel_origin) / pixel_length * coordinate_range.length(); - } -}; - -class LIB_DECL Abs_Axis : public Renderable { -public: - explicit Abs_Axis(Plot_Core& plot, Orientation orientation = Orientation::Horizontal); - ~Abs_Axis() override; - - [[nodiscard]] int x() const; - void set_x(int value); - [[nodiscard]] int y() const; - void set_y(int value); - [[nodiscard]] Orientation orientation() const; - void set_orientation(Orientation value); - [[nodiscard]] std::size_t pixel_length() const; - void set_pixel_length(std::size_t value); - [[nodiscard]] int tick_length() const; - void set_tick_length(int value); - [[nodiscard]] int sub_tick_length() const; - void set_sub_tick_length(int value); - [[nodiscard]] Color color() const; - void set_color(Color value); - [[nodiscard]] Number_Locale locale() const; - void set_locale(Number_Locale value); - [[nodiscard]] std::string unit_text() const; - void set_unit_text(std::string value); - [[nodiscard]] Font unit_text_font() const; - void set_unit_text_font(Font value); - [[nodiscard]] Pen unit_text_pen() const; - void set_unit_text_pen(Pen value); - [[nodiscard]] Brush unit_text_background_brush() const; - void set_unit_text_background_brush(Brush value); - [[nodiscard]] int label_rotation_degrees() const; - void set_label_rotation_degrees(int value); - - [[nodiscard]] virtual Range coord_range() const = 0; - [[nodiscard]] virtual double pixel_to_coord(double pixel) const; - [[nodiscard]] virtual double coord_to_pixel(double coordinate) const; - [[nodiscard]] Axis_Transform transform() const; - [[nodiscard]] double start_coord() const; - [[nodiscard]] double end_coord() const; - [[nodiscard]] int pixel_sample_count(Range range) const; - [[nodiscard]] int pixel_sample_count() const; - [[nodiscard]] virtual double tick_step(Range range) const; - [[nodiscard]] virtual int sub_tick_count(double major_step) const; - [[nodiscard]] virtual std::string tick_label(double tick) const; - -protected: - struct State { - int x{}; - int y{}; - Orientation orientation = Orientation::Horizontal; - std::size_t pixel_length{}; - int tick_length = 10; - int sub_tick_length = 5; - Color color = Color::white(); - Number_Locale locale; - std::string unit_text; - Font unit_text_font; - Pen unit_text_pen{Color::white()}; - Brush unit_text_background_brush{Color::black(), Brush_Style::Solid}; - int label_rotation_degrees{}; - }; - - [[nodiscard]] State axis_state() const; - void paint(detail::Painter& painter) override; - -private: - mutable std::mutex axis_mutex_; - State axis_state_; -}; - -template -class Axis_Builder_Base { -public: - Derived& set_x(int value) { x_ = value; return derived(); } - Derived& set_y(int value) { y_ = value; return derived(); } - Derived& set_orientation(Orientation value) { orientation_ = value; return derived(); } - Derived& set_pixel_length(std::size_t value) { pixel_length_ = value; return derived(); } - Derived& set_tick_length(int value) { tick_length_ = value; return derived(); } - Derived& set_sub_tick_length(int value) { sub_tick_length_ = value; return derived(); } - Derived& set_color(Color value) { color_ = value; return derived(); } - Derived& set_unit_text(std::string value) { unit_text_ = std::move(value); return derived(); } - Derived& set_unit_text_font(Font value) { unit_text_font_ = value; return derived(); } - Derived& set_unit_text_pen(Pen value) { unit_text_pen_ = value; return derived(); } - Derived& set_unit_text_background_brush(Brush value) { unit_background_ = value; return derived(); } - Derived& set_label_rotation_degrees(int value) { label_rotation_ = value; return derived(); } - -protected: - explicit Axis_Builder_Base(std::shared_ptr parent, Orientation orientation) - : parent_(std::move(parent)), orientation_(orientation) {} - - void apply(Abs_Axis& axis) const { - axis.set_x(x_); - axis.set_y(y_); - axis.set_orientation(orientation_); - axis.set_pixel_length(pixel_length_); - axis.set_tick_length(tick_length_); - axis.set_sub_tick_length(sub_tick_length_); - axis.set_color(color_); - axis.set_unit_text(unit_text_); - axis.set_unit_text_font(unit_text_font_); - axis.set_unit_text_pen(unit_text_pen_); - axis.set_unit_text_background_brush(unit_background_); - axis.set_label_rotation_degrees(label_rotation_); - } - [[nodiscard]] Derived& derived() { return static_cast(*this); } - - std::shared_ptr parent_; - int x_{}; - int y_{}; - Orientation orientation_; - std::size_t pixel_length_{}; - int tick_length_ = 10; - int sub_tick_length_ = 5; - Color color_ = Color::white(); - std::string unit_text_; - Font unit_text_font_; - Pen unit_text_pen_{Color::white()}; - Brush unit_background_{Color::black(), Brush_Style::Solid}; - int label_rotation_{}; -}; - -class LIB_DECL Axis : public Abs_Axis, public Event_Handler { -public: - explicit Axis(Plot_Core& plot, Orientation orientation = Orientation::Horizontal); - [[nodiscard]] Range coord_range() const override; - [[nodiscard]] int label_precision() const; - void set_label_precision(int value); - [[nodiscard]] double coord_start() const; - void set_coord_start(double value); - [[nodiscard]] double coord_length() const; - void set_coord_length(double value); - void set_coord_range(Range range); - void set_use_wheel(bool value); - void set_use_drag(bool value); - [[nodiscard]] bool use_wheel() const; - [[nodiscard]] bool use_drag() const; - void handle_event(const Event& event) override; - [[nodiscard]] std::string tick_label(double tick) const override; - - class Builder : public Axis_Builder_Base { - public: - Builder(std::shared_ptr parent, Orientation orientation); - Builder& set_coord_range(Range value) { coordinates_ = value; return *this; } - Builder& set_label_precision(int value) { precision_ = value; return *this; } - Builder& set_use_wheel(bool value) { wheel_ = value; return *this; } - Builder& set_use_drag(bool value) { drag_ = value; return *this; } - std::shared_ptr build(); - private: - Range coordinates_{0.0, 20.0}; - int precision_ = 2; - bool wheel_{}; - bool drag_{}; - }; - -private: - mutable std::mutex interaction_mutex_; - Range coordinates_{0.0, 20.0}; - int precision_ = 2; - bool wheel_enabled_{}; - bool drag_enabled_{}; - bool dragging_{}; - PointF last_pointer_{}; -}; - -class LIB_DECL Frequency_Axis : public Axis { -public: - explicit Frequency_Axis(Plot_Core& plot, Orientation orientation = Orientation::Horizontal); - [[nodiscard]] std::string tick_label(double tick) const override; - - class Builder : public Axis_Builder_Base { - public: - Builder(std::shared_ptr parent, Orientation orientation); - Builder& set_coord_range(Range value) { coordinates_ = value; return *this; } - Builder& set_label_precision(int value) { precision_ = value; return *this; } - Builder& set_use_wheel(bool value) { wheel_ = value; return *this; } - Builder& set_use_drag(bool value) { drag_ = value; return *this; } - std::shared_ptr build(); - private: - Range coordinates_{0.0, 20.0}; - int precision_ = 2; - bool wheel_{}; - bool drag_{}; - }; -}; - -class LIB_DECL Time_Axis : public Abs_Axis { -public: - explicit Time_Axis(Plot_Core& plot, Orientation orientation = Orientation::Horizontal); - [[nodiscard]] int visible_time_point_count() const; - void set_visible_time_point_count(int value); - [[nodiscard]] int tick_label_spacing_px() const; - void set_tick_label_spacing_px(int value); - [[nodiscard]] std::string time_format() const; - void set_time_format(std::string value); - [[nodiscard]] Font font() const; - void set_font(Font value); - [[nodiscard]] bool newest_at_axis_start() const; - void set_newest_at_axis_start(bool value); - [[nodiscard]] std::size_t time_point_count() const; - int append_time(Time_Of_Day time); - [[nodiscard]] Time_Of_Day tick_to_time(int tick) const; - [[nodiscard]] Range coord_range() const override; - [[nodiscard]] double tick_step(Range range) const override; - [[nodiscard]] std::string tick_label(double tick) const override; - - class Builder : public Axis_Builder_Base { - public: - Builder(std::shared_ptr parent, Orientation orientation); - Builder& set_visible_time_point_count(int value) { visible_count_ = value; return *this; } - Builder& set_tick_label_spacing_px(int value) { spacing_ = value; return *this; } - Builder& set_time_format(std::string value) { format_ = std::move(value); return *this; } - Builder& set_font(Font value) { font_ = value; return *this; } - Builder& set_newest_at_axis_start(bool value) { newest_at_start_ = value; return *this; } - std::shared_ptr build(); - private: - int visible_count_ = 100; - int spacing_ = 8; - std::string format_ = "mm:ss.zzz"; - Font font_; - bool newest_at_start_{}; - }; - -private: - struct Time_State { - int visible_count = 100; - int tick_label_spacing_px = 8; - std::string format = "mm:ss.zzz"; - bool newest_at_start{}; - int next_tick{}; - std::deque> samples; - }; - mutable std::mutex time_mutex_; - Time_State time_state_; -}; - -} // namespace renderive +#include "Abs_Axis.h" +#include "Numeric_Axis.h" +#include "Frequency_Axis.h" +#include "Time_Axis.h" diff --git a/render_2D/axis/Axis_Builder.h b/render_2D/axis/Axis_Builder.h new file mode 100644 index 0000000..8a76804 --- /dev/null +++ b/render_2D/axis/Axis_Builder.h @@ -0,0 +1,83 @@ +#pragma once + +#include "../renderable/Renderable.h" +#include "Axis_Types.h" + +#include +#include + +namespace renderive::detail { + +template +class Axis_Renderable_Builder { +public: + using Self = Axis_Renderable_Builder; + + Axis_Renderable_Builder(std::shared_ptr parent, Orientation orientation) + : parent_(std::move(parent)) { + properties_.orientation = orientation; + } + + Self& set_x(int value) { properties_.x = value; return *this; } + Self& set_y(int value) { properties_.y = value; return *this; } + Self& set_orientation(Orientation value) { properties_.orientation = value; return *this; } + Self& set_pixel_length(std::size_t value) { properties_.pixel_length = value; return *this; } + Self& set_tick_length(int value) { properties_.tick_length = value; return *this; } + Self& set_sub_tick_length(int value) { properties_.sub_tick_length = value; return *this; } + Self& set_color(Color value) { properties_.color = value; return *this; } + Self& set_locale(Number_Locale value) { properties_.locale = value; return *this; } + Self& set_unit_text(std::string value) { properties_.unit_text = std::move(value); return *this; } + Self& set_unit_text_font(Font value) { properties_.unit_text_font = value; return *this; } + Self& set_unit_text_pen(Pen value) { properties_.unit_text_pen = value; return *this; } + Self& set_unit_text_background_brush(Brush value) { properties_.unit_text_background_brush = value; return *this; } + Self& set_label_rotation_degrees(int value) { properties_.label_rotation_degrees = value; return *this; } + + Self& set_coord_range(Range value) requires requires(Properties properties) { properties.coordinates = value; } { + properties_.coordinates = value; + return *this; + } + Self& set_label_precision(int value) requires requires(Properties properties) { properties.precision = value; } { + properties_.precision = value; + return *this; + } + Self& set_use_wheel(bool value) requires requires(Properties properties) { properties.wheel = value; } { + properties_.wheel = value; + return *this; + } + Self& set_use_drag(bool value) requires requires(Properties properties) { properties.drag = value; } { + properties_.drag = value; + return *this; + } + Self& set_visible_time_point_count(int value) requires requires(Properties properties) { properties.visible_count = value; } { + properties_.visible_count = value; + return *this; + } + Self& set_tick_label_spacing_px(int value) requires requires(Properties properties) { properties.tick_label_spacing_px = value; } { + properties_.tick_label_spacing_px = value; + return *this; + } + Self& set_time_format(std::string value) requires requires(Properties properties) { properties.format = value; } { + properties_.format = std::move(value); + return *this; + } + Self& set_font(Font value) requires requires(Properties properties) { properties.unit_text_font = value; } { + properties_.unit_text_font = value; + return *this; + } + Self& set_newest_at_axis_start(bool value) requires requires(Properties properties) { properties.newest_at_start = value; } { + properties_.newest_at_start = value; + return *this; + } + + std::shared_ptr build() const { + if (!parent_) + return {}; + return parent_->plot().template make_renderable(parent_, properties_); + } + +private: + std::shared_ptr parent_; + Properties properties_; +}; + +} // namespace renderive::detail diff --git a/render_2D/axis/Axis_Format.cpp b/render_2D/axis/Axis_Format.cpp new file mode 100644 index 0000000..c733aee --- /dev/null +++ b/render_2D/axis/Axis_Format.cpp @@ -0,0 +1,66 @@ +#include "Axis_Format.h" + +#include +#include +#include + +namespace renderive::detail { +namespace { + +std::string fixed_number(double value, int precision) { + std::ostringstream stream; + stream << std::fixed << std::setprecision(std::clamp(precision, 0, 12)) << value; + std::string result = stream.str(); + if (result.find('.') != std::string::npos) { + while (!result.empty() && result.back() == '0') + result.pop_back(); + if (!result.empty() && result.back() == '.') + result.pop_back(); + } + return result; +} + +std::string digits(int value, int width) { + std::ostringstream stream; + stream << std::setfill('0') << std::setw(width) << value; + return stream.str(); +} + +} // namespace + +std::string localized_axis_number(double value, int precision, Number_Locale locale) { + std::string result = fixed_number(value, precision); + if (locale.decimal_point != '.') + std::replace(result.begin(), result.end(), '.', locale.decimal_point); + return result; +} + +std::string formatted_axis_time(Time_Of_Day time, std::string_view format) { + const auto total = time.milliseconds; + const int hours = static_cast((total / 3'600'000) % 24); + const int minutes = static_cast((total / 60'000) % 60); + const int seconds = static_cast((total / 1'000) % 60); + const int milliseconds = static_cast(total % 1'000); + std::string result; + for (std::size_t index = 0; index < format.size();) { + const std::string_view rest = format.substr(index); + if (rest.starts_with("zzz")) { + result += digits(milliseconds, 3); + index += 3; + } else if (rest.starts_with("hh") || rest.starts_with("HH")) { + result += digits(hours, 2); + index += 2; + } else if (rest.starts_with("mm")) { + result += digits(minutes, 2); + index += 2; + } else if (rest.starts_with("ss")) { + result += digits(seconds, 2); + index += 2; + } else { + result.push_back(format[index++]); + } + } + return result; +} + +} // namespace renderive::detail diff --git a/render_2D/axis/Axis_Format.h b/render_2D/axis/Axis_Format.h new file mode 100644 index 0000000..83cef6e --- /dev/null +++ b/render_2D/axis/Axis_Format.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Axis_Types.h" + +#include +#include + +namespace renderive::detail { + +std::string localized_axis_number(double value, int precision, Number_Locale locale); +std::string formatted_axis_time(Time_Of_Day time, std::string_view format); + +} // namespace renderive::detail diff --git a/render_2D/axis/Axis_Types.h b/render_2D/axis/Axis_Types.h new file mode 100644 index 0000000..3df2963 --- /dev/null +++ b/render_2D/axis/Axis_Types.h @@ -0,0 +1,44 @@ +#pragma once + +#include "../base/Types.h" + +#include +#include + +namespace renderive { + +struct Axis_Transform { + Range coordinate_range; + double pixel_origin{}; + double pixel_length{}; + + [[nodiscard]] double coord_to_pixel(double coordinate) const noexcept { + const double span = coordinate_range.length(); + if (span == 0.0) + return pixel_origin; + return pixel_origin + (coordinate - coordinate_range.origin) / span * pixel_length; + } + [[nodiscard]] double pixel_to_coord(double pixel) const noexcept { + if (pixel_length == 0.0) + return coordinate_range.origin; + return coordinate_range.origin + (pixel - pixel_origin) / pixel_length * coordinate_range.length(); + } +}; + +struct Axis_Base_Properties { + int x{}; + int y{}; + Orientation orientation = Orientation::Horizontal; + std::size_t pixel_length{}; + int tick_length = 10; + int sub_tick_length = 5; + Color color = Color::white(); + Number_Locale locale; + std::string unit_text; + Font unit_text_font; + Pen unit_text_pen{Color::white()}; + Brush unit_text_background_brush{Color::black(), Brush_Style::Solid}; + int label_rotation_degrees{}; +}; + +} // namespace renderive diff --git a/render_2D/axis/Frequency_Axis.cpp b/render_2D/axis/Frequency_Axis.cpp new file mode 100644 index 0000000..5a7ad5d --- /dev/null +++ b/render_2D/axis/Frequency_Axis.cpp @@ -0,0 +1,21 @@ +#include "Frequency_Axis.h" + +#include "Axis_Format.h" + +#include + +namespace renderive::detail { + +Frequency_Axis_Control::Frequency_Axis_Control(Plot_Core& plot, const Axis_Properties& properties) + : Axis(plot, properties) {} + +std::string Frequency_Axis_Control::tick_label(double tick) const { + const double absolute = std::abs(tick); + if (absolute >= 1'000'000.0) + return localized_axis_number(tick / 1'000'000.0, label_precision(), locale()) + " MHz"; + if (absolute >= 1'000.0) + return localized_axis_number(tick / 1'000.0, label_precision(), locale()) + " kHz"; + return localized_axis_number(tick, label_precision(), locale()) + " Hz"; +} + +} // namespace renderive::detail diff --git a/render_2D/axis/Frequency_Axis.h b/render_2D/axis/Frequency_Axis.h new file mode 100644 index 0000000..8e9bdb6 --- /dev/null +++ b/render_2D/axis/Frequency_Axis.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Numeric_Axis.h" + +namespace renderive { + +namespace detail { + +class LIB_DECL Frequency_Axis_Control : public Axis { +public: + Frequency_Axis_Control(Plot_Core& plot, const Axis_Properties& properties); + [[nodiscard]] std::string tick_label(double tick) const override; +}; + +} // namespace detail + +using Frequency_Axis = Attach_Builder; + +} // namespace renderive diff --git a/render_2D/axis/Numeric_Axis.cpp b/render_2D/axis/Numeric_Axis.cpp new file mode 100644 index 0000000..4829b9c --- /dev/null +++ b/render_2D/axis/Numeric_Axis.cpp @@ -0,0 +1,157 @@ +#include "Numeric_Axis.h" + +#include "Axis_Format.h" + +#include +#include + +namespace renderive::detail { +namespace { + +bool valid_range(Range range) { + return std::isfinite(range.origin) && std::isfinite(range.target) && range.size() > 0.0; +} + +Numeric_Axis_State numeric_state_from(const Axis_Properties& properties) { + return { + properties.coordinates, + std::clamp(properties.precision, 0, 12), + properties.wheel, + properties.drag + }; +} + +} // namespace + +Axis_Control::Axis_Control(Plot_Core& plot, const Axis_Properties& properties) + : Abs_Axis(plot, properties), numeric_(numeric_state_from(properties)) {} + +Range Axis_Control::coord_range() const { + return numeric_.get<&Numeric_Axis_State::coordinates>(); +} + +int Axis_Control::label_precision() const { + return numeric_.get<&Numeric_Axis_State::precision>(); +} + +void Axis_Control::set_label_precision(int value) { + numeric_.set<&Numeric_Axis_State::precision>(std::clamp(value, 0, 12)); + changed(); +} + +double Axis_Control::coord_start() const { return coord_range().origin; } + +void Axis_Control::set_coord_start(double value) { + auto range = coord_range(); + set_coord_range({value, value + range.length()}); +} + +double Axis_Control::coord_length() const { return coord_range().length(); } + +void Axis_Control::set_coord_length(double value) { + auto range = coord_range(); + set_coord_range({range.origin, range.origin + value}); +} + +void Axis_Control::set_coord_range(Range range) { + if (!valid_range(range)) + return; + numeric_.set<&Numeric_Axis_State::coordinates>(range); + changed(); +} + +void Axis_Control::set_use_wheel(bool value) { + numeric_.set<&Numeric_Axis_State::wheel>(value); + changed(); +} + +void Axis_Control::set_use_drag(bool value) { + numeric_.set<&Numeric_Axis_State::drag>(value); + changed(); +} + +bool Axis_Control::use_wheel() const { + return numeric_.get<&Numeric_Axis_State::wheel>(); +} + +bool Axis_Control::use_drag() const { + return numeric_.get<&Numeric_Axis_State::drag>(); +} + +void Axis_Control::handle_event(const Event& event) { + if (event.type == Event_Type::Wheel && use_wheel()) { + const auto& wheel = static_cast(event); + Range range = coord_range(); + const double anchor_pixel = orientation() == Orientation::Horizontal ? wheel.position.x : wheel.position.y; + const double anchor = pixel_to_coord(anchor_pixel); + const double factor = wheel.angle_delta_y >= 0.0 ? 0.9 : 1.1; + set_coord_range({anchor + (range.origin - anchor) * factor, + anchor + (range.target - anchor) * factor}); + event.accept(); + return; + } + if (!use_drag()) + return; + if (event.type == Event_Type::Pointer_Press) { + const auto& pointer = static_cast(event); + if (pointer.button == Mouse_Button::Left) { + interaction_.update([&](Axis_Interaction_State& interaction) { + interaction.dragging = true; + interaction.last_pointer = pointer.position; + }); + event.accept(); + } + } else if (event.type == Event_Type::Pointer_Move) { + const auto& pointer = static_cast(event); + PointF previous; + bool dragging{}; + interaction_.update([&](Axis_Interaction_State& interaction) { + dragging = interaction.dragging; + if (!dragging) + return; + previous = interaction.last_pointer; + interaction.last_pointer = pointer.position; + }); + if (!dragging) + return; + const double delta = orientation() == Orientation::Horizontal + ? pointer.position.x - previous.x + : pointer.position.y - previous.y; + Range range = coord_range(); + const double shift = pixel_length() == 0 ? 0.0 : -delta * range.length() / pixel_length(); + set_coord_range({range.origin + shift, range.target + shift}); + event.accept(); + } else if (event.type == Event_Type::Pointer_Release) { + bool accepted{}; + interaction_.update([&](Axis_Interaction_State& interaction) { + accepted = interaction.dragging; + interaction.dragging = false; + }); + if (accepted) + event.accept(); + } +} + +std::string Axis_Control::tick_label(double tick) const { + return localized_axis_number(tick, label_precision(), locale()); +} + +void Axis_Control::publish() { + Abs_Axis::publish(); + numeric_.publish(); + interaction_.publish(); +} + +std::uint64_t Axis_Control::state_revision() const { + return Abs_Axis::state_revision() + numeric_.state_revision() + interaction_.state_revision(); +} + +Numeric_Axis_State Axis_Control::numeric_state() const { + return numeric_.read([](const Numeric_Axis_State& value) { return value; }); +} + +Numeric_Axis_State Axis_Control::render_numeric_state() const { + return numeric_.render_use_state(); +} + +} // namespace renderive::detail diff --git a/render_2D/axis/Numeric_Axis.h b/render_2D/axis/Numeric_Axis.h new file mode 100644 index 0000000..d12a1ac --- /dev/null +++ b/render_2D/axis/Numeric_Axis.h @@ -0,0 +1,70 @@ +#pragma once + +#include "Abs_Axis.h" +#include "Axis_Builder.h" + +#include +#include + +namespace renderive { + +struct Axis_Properties : Axis_Base_Properties { + Range coordinates{0.0, 20.0}; + int precision = 2; + bool wheel{}; + bool drag{}; +}; + +namespace detail { + +struct Numeric_Axis_State { + Range coordinates{0.0, 20.0}; + int precision = 2; + bool wheel{}; + bool drag{}; +}; + +struct Axis_Interaction_State { + bool dragging{}; + PointF last_pointer{}; +}; + +class LIB_DECL Axis_Control : public Abs_Axis, public Event_Handler { +public: + Axis_Control(Plot_Core& plot, const Axis_Properties& properties); + [[nodiscard]] Range coord_range() const override; + [[nodiscard]] int label_precision() const; + void set_label_precision(int value); + [[nodiscard]] double coord_start() const; + void set_coord_start(double value); + [[nodiscard]] double coord_length() const; + void set_coord_length(double value); + void set_coord_range(Range range); + void set_use_wheel(bool value); + void set_use_drag(bool value); + [[nodiscard]] bool use_wheel() const; + [[nodiscard]] bool use_drag() const; + void handle_event(const Event& event) override; + [[nodiscard]] std::string tick_label(double tick) const override; + + void publish() override; + [[nodiscard]] std::uint64_t state_revision() const override; + +protected: + [[nodiscard]] Numeric_Axis_State numeric_state() const; + [[nodiscard]] Numeric_Axis_State render_numeric_state() const; + +private: + struct Numeric_Base {}; + struct Interaction_Base {}; + using Numeric_State = Double_State_Strategy; + using Interaction_State = Double_State_Strategy; + Numeric_State numeric_; + Interaction_State interaction_; +}; + +} // namespace detail + +using Axis = Attach_Builder; + +} // namespace renderive diff --git a/render_2D/axis/Time_Axis.cpp b/render_2D/axis/Time_Axis.cpp new file mode 100644 index 0000000..e37dd53 --- /dev/null +++ b/render_2D/axis/Time_Axis.cpp @@ -0,0 +1,125 @@ +#include "Time_Axis.h" + +#include "Axis_Format.h" + +#include +#include + +namespace renderive::detail { +namespace { + +Time_Axis_State time_state_from(const Time_Axis_Properties& properties) { + return { + std::max(2, properties.visible_count), + std::max(0, properties.tick_label_spacing_px), + properties.format, + properties.newest_at_start, + 0, + {} + }; +} + +} // namespace + +Time_Axis_Control::Time_Axis_Control(Plot_Core& plot, const Time_Axis_Properties& properties) + : Abs_Axis(plot, properties), time_(time_state_from(properties)) {} + +int Time_Axis_Control::visible_time_point_count() const { + return time_.get<&Time_Axis_State::visible_count>(); +} + +void Time_Axis_Control::set_visible_time_point_count(int value) { + time_.set<&Time_Axis_State::visible_count>(std::max(2, value)); + changed(); +} + +int Time_Axis_Control::tick_label_spacing_px() const { + return time_.get<&Time_Axis_State::tick_label_spacing_px>(); +} + +void Time_Axis_Control::set_tick_label_spacing_px(int value) { + time_.set<&Time_Axis_State::tick_label_spacing_px>(std::max(0, value)); + changed(); +} + +std::string Time_Axis_Control::time_format() const { + return time_.get<&Time_Axis_State::format>(); +} + +void Time_Axis_Control::set_time_format(std::string value) { + time_.set<&Time_Axis_State::format>(std::move(value)); + changed(); +} + +Font Time_Axis_Control::font() const { return unit_text_font(); } + +void Time_Axis_Control::set_font(Font value) { set_unit_text_font(value); } + +bool Time_Axis_Control::newest_at_axis_start() const { + return time_.get<&Time_Axis_State::newest_at_start>(); +} + +void Time_Axis_Control::set_newest_at_axis_start(bool value) { + time_.set<&Time_Axis_State::newest_at_start>(value); + changed(); +} + +std::size_t Time_Axis_Control::time_point_count() const { + return time_.read([](const Time_Axis_State& state) { return state.samples.size(); }); +} + +int Time_Axis_Control::append_time(Time_Of_Day time) { + int tick{}; + time_.update([&](Time_Axis_State& state) { + tick = state.next_tick++; + state.samples.emplace_back(tick, time); + const auto limit = static_cast(std::max(512, state.visible_count * 4)); + while (state.samples.size() > limit) + state.samples.pop_front(); + }); + changed(); + return tick; +} + +Time_Of_Day Time_Axis_Control::tick_to_time(int tick) const { + return time_.read([tick](const Time_Axis_State& state) { + auto iterator = std::find_if(state.samples.begin(), state.samples.end(), + [tick](const auto& value) { return value.first == tick; }); + return iterator == state.samples.end() ? Time_Of_Day{} : iterator->second; + }); +} + +Range Time_Axis_Control::coord_range() const { + return time_.read([](const Time_Axis_State& state) { + const int latest = std::max(1, state.next_tick - 1); + const int earliest = std::max(0, latest - state.visible_count + 1); + return state.newest_at_start ? Range{static_cast(latest), static_cast(earliest)} + : Range{static_cast(earliest), static_cast(latest)}; + }); +} + +double Time_Axis_Control::tick_step(Range range) const { + const double available = static_cast(pixel_length()); + const double label_width = std::max(48.0, font().size * 7.0); + const double spacing = static_cast(tick_label_spacing_px()); + const double label_count = std::max(1.0, available / (label_width + spacing)); + return std::max(1.0, std::ceil(range.size() / label_count)); +} + +std::string Time_Axis_Control::tick_label(double tick) const { + const Time_Of_Day time = tick_to_time(static_cast(std::llround(tick))); + if (!time.valid()) + return {}; + return formatted_axis_time(time, time_format()); +} + +void Time_Axis_Control::publish() { + Abs_Axis::publish(); + time_.publish(); +} + +std::uint64_t Time_Axis_Control::state_revision() const { + return Abs_Axis::state_revision() + time_.state_revision(); +} + +} // namespace renderive::detail diff --git a/render_2D/axis/Time_Axis.h b/render_2D/axis/Time_Axis.h new file mode 100644 index 0000000..55b70ed --- /dev/null +++ b/render_2D/axis/Time_Axis.h @@ -0,0 +1,65 @@ +#pragma once + +#include "Abs_Axis.h" +#include "Axis_Builder.h" + +#include +#include + +#include +#include + +namespace renderive { + +struct Time_Axis_Properties : Axis_Base_Properties { + int visible_count = 100; + int tick_label_spacing_px = 8; + std::string format = "mm:ss.zzz"; + bool newest_at_start{}; +}; + +namespace detail { + +struct Time_Axis_State { + int visible_count = 100; + int tick_label_spacing_px = 8; + std::string format = "mm:ss.zzz"; + bool newest_at_start{}; + int next_tick{}; + std::deque> samples; +}; + +class LIB_DECL Time_Axis_Control : public Abs_Axis { +public: + Time_Axis_Control(Plot_Core& plot, const Time_Axis_Properties& properties); + [[nodiscard]] int visible_time_point_count() const; + void set_visible_time_point_count(int value); + [[nodiscard]] int tick_label_spacing_px() const; + void set_tick_label_spacing_px(int value); + [[nodiscard]] std::string time_format() const; + void set_time_format(std::string value); + [[nodiscard]] Font font() const; + void set_font(Font value); + [[nodiscard]] bool newest_at_axis_start() const; + void set_newest_at_axis_start(bool value); + [[nodiscard]] std::size_t time_point_count() const; + int append_time(Time_Of_Day time); + [[nodiscard]] Time_Of_Day tick_to_time(int tick) const; + [[nodiscard]] Range coord_range() const override; + [[nodiscard]] double tick_step(Range range) const override; + [[nodiscard]] std::string tick_label(double tick) const override; + + void publish() override; + [[nodiscard]] std::uint64_t state_revision() const override; + +private: + struct Time_Base {}; + using Time_State = Double_State_Strategy; + Time_State time_; +}; + +} // namespace detail + +using Time_Axis = Attach_Builder; + +} // namespace renderive diff --git a/render_2D/plot/Plot_Core.cpp b/render_2D/plot/Plot_Core.cpp index c1d6b99..2153ea9 100644 --- a/render_2D/plot/Plot_Core.cpp +++ b/render_2D/plot/Plot_Core.cpp @@ -328,11 +328,11 @@ Size Plot_Core::viewport_size() const noexcept { } void Plot_Core::dispatch_event(const Event& event) { - const auto topology = with_scene(impl_->scene, [](const auto& scene) { - return scene.topology_snapshot(); + const auto paint_order = with_scene(impl_->scene, [](const auto& scene) { + return scene.paint_order_snapshot(); }); - for (auto iterator = topology.display.rbegin(); iterator != topology.display.rend(); ++iterator) { - auto base = std::const_pointer_cast<::Renderable_Base>(iterator->child); + for (auto iterator = paint_order.rbegin(); iterator != paint_order.rend(); ++iterator) { + auto base = std::const_pointer_cast<::Renderable_Base>(*iterator); if (auto renderable = std::dynamic_pointer_cast(base)) { if (renderable->is_visible()) { if (auto handler = std::dynamic_pointer_cast(base)) @@ -354,8 +354,8 @@ bool Plot_Core::prepare_frame() { if (!paint_frame) return false; const auto topology = scene.topology_snapshot(); - for (const auto& entry : topology.display) { - auto base = std::const_pointer_cast<::Renderable_Base>(entry.child); + for (const auto& renderable : topology.renderables) { + auto base = std::const_pointer_cast<::Renderable_Base>(renderable); if (auto state = std::dynamic_pointer_cast<::State_Strategy_Base>(base)) state->publish(); } diff --git a/render_2D/plottable/Afterglow.cpp b/render_2D/plottable/Afterglow.cpp index de34771..75218e8 100644 --- a/render_2D/plottable/Afterglow.cpp +++ b/render_2D/plottable/Afterglow.cpp @@ -29,14 +29,14 @@ std::size_t Afterglow_Control::latest_spectrum_point_count() const { std::size_t Afterglow_Control::rendered_cell_count() const { const auto state = properties(); const auto history = impl_->history.snapshot(); - if(history.empty()) + if (history.empty()) return 0; const int width = std::min(state.frequency_point_size.get(), static_cast(history.back().size())); const int height = state.power_point_size.get() > 0 ? state.power_point_size.get() : std::max(1, static_cast(impl_->power_axis->pixel_length())); return width > 0 && height > 0 ? static_cast(width) * static_cast(height) : 0; } void Afterglow_Control::append_spectrum(std::span values) { - if(get<&Afterglow_Properties::frequency_point_size>() <= 0) + if (get<&Afterglow_Properties::frequency_point_size>() <= 0) set<&Afterglow_Properties::frequency_point_size>(static_cast(values.size())); impl_->history.update({values.begin(), values.end()}, 64); changed(); @@ -50,31 +50,31 @@ void Afterglow_Control::publish() { void Afterglow_Control::paint(Painter& painter) { const auto state = render_properties(); const auto history = impl_->history.snapshot(); - if(history.empty()) + if (history.empty()) return; const int width = std::min(state.frequency_point_size.get(), static_cast(history.back().size())); const int height = state.power_point_size.get() > 0 ? state.power_point_size.get() : std::max(1, static_cast(impl_->power_axis->pixel_length())); - if(width <= 0 || height <= 0) + if (width <= 0 || height <= 0) return; std::vector intensity(static_cast(width) * height); double weight = 1.0; const double decay = 1.0 - state.attenuation_rate.get(); - for(auto iterator = history.rbegin(); iterator != history.rend(); ++iterator) { + for (auto iterator = history.rbegin(); iterator != history.rend(); ++iterator) { const int count = std::min(width, static_cast(iterator->size())); - for(int x = 0; x < count; ++x) { + for (int x = 0; x < count; ++x) { const double normalized = normalized_value((*iterator)[static_cast(x)], state.power_range); const int y = std::clamp(height - 1 - static_cast(normalized * (height - 1)), 0, height - 1); intensity[static_cast(y) * width + x] += weight; - if(state.interpolate && y + 1 < height) + if (state.interpolate && y + 1 < height) intensity[static_cast(y + 1) * width + x] += weight * 0.35; } weight *= decay; - if(weight < 0.01) + if (weight < 0.01) break; } const double maximum = std::max(1.0, *std::max_element(intensity.begin(), intensity.end())); std::vector pixels(intensity.size()); - for(std::size_t index = 0; index < pixels.size(); ++index) + for (std::size_t index = 0; index < pixels.size(); ++index) pixels[index] = state.color_map.at_normalized(intensity[index] / maximum); painter.heatmap(mapped_rect(impl_->frequency_axis->transform(), impl_->power_axis->transform(), state.frequency_range, state.power_range), width, height, pixels, Image_Interpolation_Mode::Bilinear); } diff --git a/render_2D/plottable/Plottable.h b/render_2D/plottable/Plottable.h index 6787a7b..b46eb49 100644 --- a/render_2D/plottable/Plottable.h +++ b/render_2D/plottable/Plottable.h @@ -1,11 +1,14 @@ #pragma once #include "../renderable/Renderable_Builder.h" #include +#include #include #include #include #include namespace renderive { +class Abs_Axis; + template class Clamped_Property { public: @@ -46,6 +49,8 @@ private: double value{}; }; namespace detail { +class Paint_Overlay {}; + template class Plottable_State : public Double_State_Strategy { public: @@ -82,5 +87,27 @@ private: }; template using Attach_Plottable = Attach_Builder; + +template +requires std::derived_from && std::derived_from +void attach_renderable_dependency(Renderable_Type& renderable, const std::shared_ptr& axis) { + if (axis) { + renderable.scene().add_dependency_parent(renderable, *axis); + if constexpr (std::derived_from) { + renderable.scene().add_display_parent(renderable, *axis); + } else { + renderable.scene().add_display_parent(*axis, renderable); + } + } +} + +template +void attach_renderable_dependency(Renderable_Type&, const Value&) {} + +template +requires std::derived_from +void attach_renderable_dependencies(Control& renderable, const Args&... args) { + (attach_renderable_dependency(renderable, args), ...); +} } } diff --git a/render_2D/plottable/Selection_Rectangle_Overlay.h b/render_2D/plottable/Selection_Rectangle_Overlay.h index e239a54..12ae1a1 100644 --- a/render_2D/plottable/Selection_Rectangle_Overlay.h +++ b/render_2D/plottable/Selection_Rectangle_Overlay.h @@ -9,7 +9,7 @@ struct Selection_Rectangle_Overlay_Properties { Pen selection_border_pen{Color::white(), 1.0, Line_Style::Dash}; }; namespace detail { -class LIB_DECL Selection_Rectangle_Overlay_Control : public Plottable_State, public Event_Handler { +class LIB_DECL Selection_Rectangle_Overlay_Control : public Plottable_State, public Paint_Overlay, public Event_Handler { public: Selection_Rectangle_Overlay_Control(Plot_Core& plot, const Selection_Rectangle_Overlay_Properties& properties, std::shared_ptr horizontal_axis, std::shared_ptr vertical_axis); ~Selection_Rectangle_Overlay_Control() override; diff --git a/render_2D/renderable/Renderable_Builder.h b/render_2D/renderable/Renderable_Builder.h index da31a59..b608fe6 100644 --- a/render_2D/renderable/Renderable_Builder.h +++ b/render_2D/renderable/Renderable_Builder.h @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include namespace renderive { template Validator_Type = No_Property_Validator> @@ -36,6 +38,20 @@ public: if(!parent || !(valid_argument(args) && ...)) return {}; validator(properties); + if constexpr (requires(Product& product) { attach_renderable_dependencies(product, args...); } && + (std::copy_constructible> && ...)) { + auto dependency_args = std::make_tuple(args...); + auto result = parent->plot().make_renderable(parent, properties, std::forward(args)...); + try { + std::apply([&result](const auto&... values) { + attach_renderable_dependencies(*result, values...); + }, dependency_args); + } catch (...) { + result->scene().detach_renderable(*result); + throw; + } + return result; + } return parent->plot().make_renderable(parent, properties, std::forward(args)...); } private: diff --git a/render_2D/tests/render_2D_Integration_Tests.cpp b/render_2D/tests/render_2D_Integration_Tests.cpp index 7cd20b5..ea537bc 100644 --- a/render_2D/tests/render_2D_Integration_Tests.cpp +++ b/render_2D/tests/render_2D_Integration_Tests.cpp @@ -2,6 +2,7 @@ #include "render_2D/plottable/Curve_Sampling.h" #include "render_2D/render/Blend2D_Cache.h" #include +#include #include #include #include @@ -584,5 +585,62 @@ TEST(Renderive_Core2, PlottableDataUpdatesReachKernelRealTimeDataStrategy) { EXPECT_EQ(history.last_event, "real_time_data_updated"); EXPECT_GT(history.observation_count, latest.observation_count); } +TEST(Renderive_Core2, PlottableAxesAreDataDependenciesAndPaintOverlays) { + Plot_Core plot; + plot.init(); + const auto root = plot.root_renderable(); + const auto frequency = Frequency_Axis::Builder(root, Orientation::Horizontal).build(); + const auto power = Axis::Builder(root, Orientation::Vertical).build(); + const auto spectrum = Spectrum::Builder{}.build(root, frequency, power); + ASSERT_TRUE(spectrum); + const auto topology = spectrum->scene().topology_snapshot(); + const auto has_relationship = [](const auto& relationships, const auto* child, const auto* parent) { + return std::any_of(relationships.begin(), relationships.end(), [child, parent](const auto& relationship) { + return relationship.child.get() == child && relationship.parent.get() == parent; + }); + }; + EXPECT_TRUE(has_relationship(topology.dependency, spectrum.get(), frequency.get())); + EXPECT_TRUE(has_relationship(topology.dependency, spectrum.get(), power.get())); + EXPECT_TRUE(has_relationship(topology.display, frequency.get(), spectrum.get())); + EXPECT_TRUE(has_relationship(topology.display, power.get(), spectrum.get())); + const auto paint_order = spectrum->scene().paint_order_snapshot(); + const auto spectrum_position = std::find_if(paint_order.begin(), paint_order.end(), [&spectrum](const auto& renderable) { + return renderable.get() == spectrum.get(); + }); + const auto frequency_position = std::find_if(paint_order.begin(), paint_order.end(), [&frequency](const auto& renderable) { + return renderable.get() == frequency.get(); + }); + const auto power_position = std::find_if(paint_order.begin(), paint_order.end(), [&power](const auto& renderable) { + return renderable.get() == power.get(); + }); + ASSERT_NE(spectrum_position, paint_order.end()); + ASSERT_NE(frequency_position, paint_order.end()); + ASSERT_NE(power_position, paint_order.end()); + EXPECT_LT(spectrum_position, frequency_position); + EXPECT_LT(spectrum_position, power_position); +} +TEST(Renderive_Core2, InteractionOverlayPaintsAboveItsAxes) { + Plot_Core plot; + plot.init(); + const auto root = plot.root_renderable(); + const auto horizontal = Axis::Builder(root, Orientation::Horizontal).build(); + const auto vertical = Axis::Builder(root, Orientation::Vertical).build(); + const auto selection = Selection_Rectangle_Overlay::Builder{}.build(root, horizontal, vertical); + ASSERT_TRUE(selection); + const auto paint_order = selection->scene().paint_order_snapshot(); + const auto position = [&paint_order](const auto* target) { + return std::find_if(paint_order.begin(), paint_order.end(), [target](const auto& renderable) { + return renderable.get() == target; + }); + }; + const auto selection_position = position(selection.get()); + const auto horizontal_position = position(horizontal.get()); + const auto vertical_position = position(vertical.get()); + ASSERT_NE(selection_position, paint_order.end()); + ASSERT_NE(horizontal_position, paint_order.end()); + ASSERT_NE(vertical_position, paint_order.end()); + EXPECT_LT(horizontal_position, selection_position); + EXPECT_LT(vertical_position, selection_position); +} } // namespace } // namespace renderive