diff --git a/Kernel/readme.md b/Kernel/readme.md index 33602a2..199a74b 100644 --- a/Kernel/readme.md +++ b/Kernel/readme.md @@ -19,10 +19,9 @@ Scene_Base ```cpp scene.Scene_State_Strategy::set<&Scene_State::value>(value); scene.Scene_State_Strategy::publish(); -scene.Scene_State_Strategy::render_use_state(); ``` -三缓冲分别承担正在渲染、已发布待获取、前台写入三种职责。`render()` 提交任务前调用 `acquire_render_state()`,后台帧获得稳定的状态版本。`render_use_state()` 返回状态快照,不返回内部缓冲区引用。 +三缓冲分别承担正在渲染、已发布待获取、前台写入三种职责。`render()` 提交任务前调用 `acquire_render_state()`,后台帧获得稳定的状态版本。渲染状态不提供公共读取接口,只能在 `Renderable` 的 paint 调用栈中通过 `Render_State_View` 读取当前帧的稳定引用。 ## Scene 观察者 diff --git a/Kernel/src/renderive/real_time_data/Attach_Real_Time_Data.hpp b/Kernel/src/renderive/real_time_data/Attach_Real_Time_Data.hpp index f3e0ed4..99ac987 100644 --- a/Kernel/src/renderive/real_time_data/Attach_Real_Time_Data.hpp +++ b/Kernel/src/renderive/real_time_data/Attach_Real_Time_Data.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -102,6 +103,7 @@ private: } template void bind_data(Data_Type& data) { + render_bindings_[Index].emplace(this->bind_real_time_data(data)); if constexpr (requires { data.bind(*this); }) { @@ -117,5 +119,6 @@ private: static_assert(sizeof...(Data) > 0); static_assert((std::is_class_v && ...)); std::tuple...> data_; + std::array, sizeof...(Data)> render_bindings_; std::array, sizeof...(Data)> bindings_; }; diff --git a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp index f1fe96a..d9d8327 100644 --- a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp +++ b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp @@ -13,6 +13,7 @@ #include "Observation.hpp" #include "base/Real_Time_Data_Base.hpp" #include "concept/Real_Time_Data.hpp" +#include "renderive/state/Render_State_View.hpp" template , Mutex_Type Mutex = std::mutex, class Observer = Observer_State<>> class History_Real_Time_Data : public Real_Time_Data_Base { public: @@ -44,17 +45,24 @@ public: return observer_.bind(target); } private: + friend class Render_State_View; static Container make_container(std::pmr::memory_resource& memory_resource); void reserve_update_times(std::size_t capacity); + void publish_render_state() override; + const Container& render_state_value() const noexcept; mutable Mutex mutex_; std::recursive_mutex mutation_mutex_; Observer observer_; - Container values_; + Container states_[3]; + Container* render_state_; + Container* cache_state_; + Container* scratch_state_; std::pmr::memory_resource* memory_resource_{}; std::uint64_t* update_times_{}; std::size_t update_times_size_{}; std::size_t update_times_capacity_{}; std::uint64_t revision_{}; + std::uint64_t render_revision_{}; std::uint64_t total_update_count_{}; std::uint64_t last_update_time_ns_{}; }; diff --git a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl index 972521b..54a888b 100644 --- a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl +++ b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl @@ -4,7 +4,10 @@ History_Real_Time_Data::History_Real_Tim : History_Real_Time_Data(*std::pmr::get_default_resource(), std::move(observer)) {} template History_Real_Time_Data::History_Real_Time_Data(std::pmr::memory_resource& memory_resource, Observer observer) - : observer_(std::move(observer)), values_(make_container(memory_resource)), memory_resource_(&memory_resource) {} + : observer_(std::move(observer)), + states_{make_container(memory_resource), make_container(memory_resource), make_container(memory_resource)}, + render_state_(&states_[0]), cache_state_(&states_[1]), scratch_state_(&states_[2]), + memory_resource_(&memory_resource) {} template History_Real_Time_Data::~History_Real_Time_Data() { if (update_times_) @@ -48,19 +51,19 @@ void History_Real_Time_Data::update(Valu { std::lock_guard lock(mutex_); reserve_update_times(update_times_size_ + 1); - values_.push_back(std::move(value)); + cache_state_->push_back(std::move(value)); const std::uint64_t update_time_ns = observer_.now_ns(); update_times_[update_times_size_++] = update_time_ns; - if(values_.size() > retain_latest_count) { - const std::size_t discarded = values_.size() - retain_latest_count; - values_.erase(values_.begin(), std::next(values_.begin(), static_cast(discarded))); + if(cache_state_->size() > retain_latest_count) { + const std::size_t discarded = cache_state_->size() - retain_latest_count; + cache_state_->erase(cache_state_->begin(), std::next(cache_state_->begin(), static_cast(discarded))); std::move(update_times_ + discarded, update_times_ + update_times_size_, update_times_); update_times_size_ -= discarded; } ++revision_; ++total_update_count_; last_update_time_ns_ = update_time_ns; - observation = {Real_Time_Data_Observation_Event::updated, {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, values_.size()}}; + observation = {Real_Time_Data_Observation_Event::updated, {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, cache_state_->size()}}; } observer_.observe(observation); } @@ -70,7 +73,7 @@ void History_Real_Time_Data::clear() { Real_Time_Data_Observation observation; { std::lock_guard lock(mutex_); - values_.clear(); + cache_state_->clear(); update_times_size_ = 0; ++revision_; last_update_time_ns_ = observer_.now_ns(); @@ -85,14 +88,14 @@ std::size_t History_Real_Time_Data::reta std::size_t discarded{}; { std::lock_guard lock(mutex_); - if(values_.size() <= count) + if(cache_state_->size() <= count) return 0; - discarded = values_.size() - count; - values_.erase(values_.begin(), std::next(values_.begin(), static_cast(discarded))); + discarded = cache_state_->size() - count; + cache_state_->erase(cache_state_->begin(), std::next(cache_state_->begin(), static_cast(discarded))); std::move(update_times_ + discarded, update_times_ + update_times_size_, update_times_); update_times_size_ -= discarded; ++revision_; - observation = {Real_Time_Data_Observation_Event::discarded, {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, values_.size()}}; + observation = {Real_Time_Data_Observation_Event::discarded, {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, cache_state_->size()}}; } observer_.observe(observation); return discarded; @@ -100,12 +103,12 @@ std::size_t History_Real_Time_Data::reta template auto History_Real_Time_Data::snapshot() const -> Container { std::lock_guard lock(mutex_); - return values_; + return *cache_state_; } template std::size_t History_Real_Time_Data::size() const { std::lock_guard lock(mutex_); - return values_.size(); + return cache_state_->size(); } template std::uint64_t History_Real_Time_Data::revision() const { @@ -115,7 +118,7 @@ std::uint64_t History_Real_Time_Data::re template Real_Time_Data_Update_State History_Real_Time_Data::update_state() const { std::lock_guard lock(mutex_); - return {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, values_.size()}; + return {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, cache_state_->size()}; } template @@ -135,12 +138,26 @@ std::size_t History_Real_Time_Data::disc if (discarded == 0) { return 0; } - values_.erase(values_.begin(), std::next(values_.begin(), static_cast(discarded))); + cache_state_->erase(cache_state_->begin(), std::next(cache_state_->begin(), static_cast(discarded))); std::move(update_times_ + discarded, update_times_ + update_times_size_, update_times_); update_times_size_ -= discarded; ++revision_; - observation = {Real_Time_Data_Observation_Event::discarded, {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, values_.size()}}; + observation = {Real_Time_Data_Observation_Event::discarded, {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, cache_state_->size()}}; } observer_.observe(observation); return discarded; } +template +void History_Real_Time_Data::publish_render_state() { + std::lock_guard lock(mutex_); + if (render_revision_ == revision_) + return; + *scratch_state_ = *cache_state_; + std::swap(render_state_, scratch_state_); + render_revision_ = revision_; +} +template +auto History_Real_Time_Data::render_state_value() const noexcept + -> const Container& { + return *render_state_; +} diff --git a/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.hpp b/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.hpp index 8d0fcaa..157998f 100644 --- a/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.hpp +++ b/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.hpp @@ -9,6 +9,7 @@ #include "Observation.hpp" #include "base/Real_Time_Data_Base.hpp" #include "concept/Real_Time_Data.hpp" +#include "renderive/state/Render_State_View.hpp" template > class Latest_Real_Time_Data : public Real_Time_Data_Base { public: @@ -33,11 +34,18 @@ public: return observer_.bind(target); } private: + friend class Render_State_View; + void publish_render_state() override; + const std::optional& render_state_value() const noexcept; mutable Mutex mutex_; std::recursive_mutex mutation_mutex_; Observer observer_; - std::optional value_; + std::optional states_[3]; + std::optional* render_state_; + std::optional* cache_state_; + std::optional* scratch_state_; std::uint64_t revision_{}; + std::uint64_t render_revision_{}; std::uint64_t total_update_count_{}; std::uint64_t last_update_time_ns_{}; }; diff --git a/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.inl b/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.inl index c8e47ff..578f867 100644 --- a/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.inl +++ b/Kernel/src/renderive/real_time_data/Latest_Real_Time_Data.inl @@ -1,13 +1,14 @@ #pragma once template -Latest_Real_Time_Data::Latest_Real_Time_Data(Observer observer) : observer_(std::move(observer)) {} +Latest_Real_Time_Data::Latest_Real_Time_Data(Observer observer) + : observer_(std::move(observer)), render_state_(&states_[0]), cache_state_(&states_[1]), scratch_state_(&states_[2]) {} template void Latest_Real_Time_Data::update(Value value) { std::lock_guard mutation_lock(mutation_mutex_); Real_Time_Data_Observation observation; { std::lock_guard lock(mutex_); - value_ = std::move(value); + *cache_state_ = std::move(value); ++revision_; ++total_update_count_; last_update_time_ns_ = observer_.now_ns(); @@ -18,12 +19,12 @@ void Latest_Real_Time_Data::update(Value value) { template auto Latest_Real_Time_Data::snapshot() const -> std::optional { std::lock_guard lock(mutex_); - return value_; + return *cache_state_; } template bool Latest_Real_Time_Data::has_value() const { std::lock_guard lock(mutex_); - return value_.has_value(); + return cache_state_->has_value(); } template std::uint64_t Latest_Real_Time_Data::revision() const { @@ -33,7 +34,21 @@ std::uint64_t Latest_Real_Time_Data::revision() con template Real_Time_Data_Update_State Latest_Real_Time_Data::update_state() const { std::lock_guard lock(mutex_); - return {this, Real_Time_Data_Retention::latest, revision_, last_update_time_ns_, total_update_count_, value_ ? 1U : 0U}; + return {this, Real_Time_Data_Retention::latest, revision_, last_update_time_ns_, total_update_count_, *cache_state_ ? 1U : 0U}; +} +template +void Latest_Real_Time_Data::publish_render_state() { + std::lock_guard lock(mutex_); + if (render_revision_ == revision_) + return; + *scratch_state_ = *cache_state_; + std::swap(render_state_, scratch_state_); + render_revision_ = revision_; +} +template +auto Latest_Real_Time_Data::render_state_value() const noexcept + -> const std::optional& { + return *render_state_; } template diff --git a/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.cpp b/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.cpp new file mode 100644 index 0000000..e7dfe7d --- /dev/null +++ b/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.cpp @@ -0,0 +1,21 @@ +#include "Real_Time_Data_Base.hpp" +#include "renderive/renderable/base/Renderable_Base.hpp" +#include + +Real_Time_Data_Binding::Real_Time_Data_Binding(Renderable_Base& renderable, Real_Time_Data_Base& data) + : renderable_(&renderable), data_(&data) { + renderable_->register_real_time_data(*data_); +} + +Real_Time_Data_Binding::Real_Time_Data_Binding(Real_Time_Data_Binding&& other) noexcept + : renderable_(std::exchange(other.renderable_, nullptr)), data_(std::exchange(other.data_, nullptr)) {} + +Real_Time_Data_Binding::~Real_Time_Data_Binding() { + if (renderable_) { + renderable_->unregister_real_time_data(*data_); + } +} + +Real_Time_Data_Binding Real_Time_Data_Base::bind_renderable(Renderable_Base& renderable) { + return {renderable, *this}; +} diff --git a/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.hpp b/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.hpp index f0cba2f..dc6727b 100644 --- a/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.hpp +++ b/Kernel/src/renderive/real_time_data/base/Real_Time_Data_Base.hpp @@ -2,10 +2,30 @@ #include #include #include "renderive/real_time_data/Observation.hpp" +class Renderable_Base; +class Real_Time_Data_Base; +class Real_Time_Data_Binding { +public: + Real_Time_Data_Binding(const Real_Time_Data_Binding&) = delete; + Real_Time_Data_Binding& operator=(const Real_Time_Data_Binding&) = delete; + Real_Time_Data_Binding(Real_Time_Data_Binding&& other) noexcept; + Real_Time_Data_Binding& operator=(Real_Time_Data_Binding&&) = delete; + ~Real_Time_Data_Binding(); +private: + friend class Real_Time_Data_Base; + Real_Time_Data_Binding(Renderable_Base& renderable, Real_Time_Data_Base& data); + Renderable_Base* renderable_{}; + Real_Time_Data_Base* data_{}; +}; class Real_Time_Data_Base { public: virtual ~Real_Time_Data_Base() = default; virtual Real_Time_Data_Retention retention() const noexcept = 0; virtual Real_Time_Data_Update_State update_state() const = 0; virtual std::size_t discard_before_time_ns(std::uint64_t time_ns) = 0; +protected: + [[nodiscard]] Real_Time_Data_Binding bind_renderable(Renderable_Base& renderable); +private: + friend class Renderable_Base; + virtual void publish_render_state() = 0; }; diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base.cpp b/Kernel/src/renderive/renderable/base/Renderable_Base.cpp index e934d98..53890df 100644 --- a/Kernel/src/renderive/renderable/base/Renderable_Base.cpp +++ b/Kernel/src/renderive/renderable/base/Renderable_Base.cpp @@ -1,5 +1,7 @@ #include "Renderable_Base.hpp" #include +#include +#include "renderive/real_time_data/base/Real_Time_Data_Base.hpp" #include "renderive/scene/base/Scene_Base.hpp" Renderable_Base::Renderable_Base(Scene_Base& scene, Renderable_Configuration configuration) : memory_domain_(scene.memory_domain_), real_time_data_state_(std::allocate_shared(Scene_Memory_Allocator{memory_domain_}, scene.scene_lifetime_)), discard_stale_frame_on_latest_data_update(real_time_data_state_->discard_stale_frame_on_latest_data_update), layer_node_(this, memory_domain_->resource()), dependency_node_(this, memory_domain_->resource()), configuration_(configuration) {} @@ -55,6 +57,12 @@ void Renderable_Base::build_task_graph(Renderable_Task_Graph& graph) { render(context); }, "render"); } +Render_State_View Renderable_Base::render_state_view() const noexcept { + return {}; +} +Real_Time_Data_Binding Renderable_Base::bind_real_time_data(Real_Time_Data_Base& data) { + return data.bind_renderable(*this); +} bool Renderable_Base::requires_render() const noexcept { const auto config = configuration(); return !config.cache_enabled || rendered_cache_revision() != cache_revision(); @@ -66,3 +74,19 @@ void Renderable_Base::set_configuration(Renderable_Configuration configuration) std::lock_guard lock(configuration_mutex_); configuration_ = configuration; } +void Renderable_Base::register_real_time_data(Real_Time_Data_Base& data) { + std::lock_guard lock(real_time_data_mutex_); + if (std::find(real_time_data_.begin(), real_time_data_.end(), &data) == real_time_data_.end()) { + real_time_data_.push_back(&data); + } +} +void Renderable_Base::unregister_real_time_data(Real_Time_Data_Base& data) noexcept { + std::lock_guard lock(real_time_data_mutex_); + std::erase(real_time_data_, &data); +} +void Renderable_Base::publish_real_time_data() { + std::lock_guard lock(real_time_data_mutex_); + for (Real_Time_Data_Base* data : real_time_data_) { + data->publish_render_state(); + } +} diff --git a/Kernel/src/renderive/renderable/base/Renderable_Base.hpp b/Kernel/src/renderive/renderable/base/Renderable_Base.hpp index c784534..e25a3f7 100644 --- a/Kernel/src/renderive/renderable/base/Renderable_Base.hpp +++ b/Kernel/src/renderive/renderable/base/Renderable_Base.hpp @@ -5,11 +5,15 @@ #include #include #include +#include #include "renderive/base/memory/Memory_Resource.hpp" #include "renderive/base/node/Directed_Acyclic_Node.hpp" #include "renderive/renderable/Renderable_Task_Graph.hpp" #include "renderive/scene/base/Scene_Lifetime.hpp" +#include "renderive/state/Render_State_View.hpp" class Frame_Strategy_Real_Time_Data_Observer; +class Real_Time_Data_Base; +class Real_Time_Data_Binding; class Scene_Base; struct Renderable_Layer_Node_Tag {}; struct Renderable_Dependency_Node_Tag {}; @@ -44,12 +48,18 @@ public: std::atomic& discard_stale_frame_on_latest_data_update; protected: virtual void build_task_graph(Renderable_Task_Graph& graph); + [[nodiscard]] Render_State_View render_state_view() const noexcept; + [[nodiscard]] Real_Time_Data_Binding bind_real_time_data(Real_Time_Data_Base& data); private: friend class Frame_Strategy_Real_Time_Data_Observer; + friend class Real_Time_Data_Binding; friend class Scene_Base; bool requires_render() const noexcept; void mark_rendered(std::uint64_t revision) noexcept; void set_configuration(Renderable_Configuration configuration) noexcept; + void register_real_time_data(Real_Time_Data_Base& data); + void unregister_real_time_data(Real_Time_Data_Base& data) noexcept; + void publish_real_time_data(); Layer_Node layer_node_; Dependency_Node dependency_node_; mutable std::mutex configuration_mutex_; @@ -60,4 +70,6 @@ private: std::uint64_t built_task_graph_revision_{}; std::atomic cache_revision_{1}; std::atomic rendered_cache_revision_{}; + std::mutex real_time_data_mutex_; + std::vector real_time_data_; }; diff --git a/Kernel/src/renderive/scene/base/Scene_Base.cpp b/Kernel/src/renderive/scene/base/Scene_Base.cpp index 8006cda..2b77ae4 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.cpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.cpp @@ -8,6 +8,7 @@ #include #include #include "renderive/renderable/color/Color_Cache.hpp" +#include "renderive/state/base/State_Strategy_Base.hpp" static_assert(TF_VERSION == 400100, "Renderive requires Taskflow 4.1.0"); namespace { template @@ -210,6 +211,20 @@ void Scene_Base::set_display_parent(Renderable_Base& renderable, Renderable_Base node.detach(); } } +void Scene_Base::publish_frame_state() { + auto task_lock = lock_render_idle(); + Renderable_List renderables(&memory_resource()); + { + std::lock_guard lock(renderable_mutex_); + renderables = *cache_renderables_; + } + for (const Renderable& renderable : renderables) { + if (auto* state = dynamic_cast(renderable.get())) { + state->publish(); + } + renderable->publish_real_time_data(); + } +} void Scene_Base::add_display_parent(Renderable_Base& renderable, Renderable_Base& parent) { auto task_lock = lock_render_idle(); validate_renderable_scene(renderable); diff --git a/Kernel/src/renderive/scene/base/Scene_Base.hpp b/Kernel/src/renderive/scene/base/Scene_Base.hpp index 3215716..ff975dd 100644 --- a/Kernel/src/renderive/scene/base/Scene_Base.hpp +++ b/Kernel/src/renderive/scene/base/Scene_Base.hpp @@ -54,6 +54,7 @@ public: virtual ~Scene_Base(); void render(); void wait_for_render(); + void publish_frame_state(); void attach_renderable(Renderable renderable); void detach_renderable(Renderable_Base& renderable); void set_display_parent(Renderable_Base& renderable, Renderable_Base* parent); diff --git a/Kernel/src/renderive/state/Concepts.hpp b/Kernel/src/renderive/state/Concepts.hpp index d4ee287..05d33cd 100644 --- a/Kernel/src/renderive/state/Concepts.hpp +++ b/Kernel/src/renderive/state/Concepts.hpp @@ -14,7 +14,6 @@ concept Double_State_Strategy_Type = std::derived_from std::same_as; { const_strategy.state_revision() } -> std::same_as; - { const_strategy.render_use_state() } -> std::same_as; }; template concept Triple_State_Strategy_Type = Double_State_Strategy_Type && requires(That& strategy) { diff --git a/Kernel/src/renderive/state/Double_State_Strategy.hpp b/Kernel/src/renderive/state/Double_State_Strategy.hpp index c53cc1f..ec83d8d 100644 --- a/Kernel/src/renderive/state/Double_State_Strategy.hpp +++ b/Kernel/src/renderive/state/Double_State_Strategy.hpp @@ -10,6 +10,7 @@ #include "renderive/base/observer/Observer.hpp" #include "renderive/base/property/Concepts.hpp" #include "Concepts.hpp" +#include "Render_State_View.hpp" #include "base/State_Strategy_Base.hpp" template > struct Double_State_Strategy : That, State_Strategy_Base { @@ -92,11 +93,11 @@ struct Double_State_Strategy : That, State_Strategy_Base { std::lock_guard lock(mtx); return publish_count; } - State render_use_state() const { - std::lock_guard lock(mtx); +private: + friend class Render_State_View; + const State& render_state_value() const noexcept { return *render_state; } -private: Observer observer; State states[3]; State* render_state; diff --git a/Kernel/src/renderive/state/Render_State_View.hpp b/Kernel/src/renderive/state/Render_State_View.hpp new file mode 100644 index 0000000..0b683ad --- /dev/null +++ b/Kernel/src/renderive/state/Render_State_View.hpp @@ -0,0 +1,15 @@ +#pragma once + +class Renderable_Base; + +class Render_State_View { +public: + template + [[nodiscard]] const auto& get(const Source& source) const noexcept { + return source.render_state_value(); + } + +private: + friend class Renderable_Base; + Render_State_View() = default; +}; diff --git a/Kernel/src/renderive/state/Triple_State_Strategy.hpp b/Kernel/src/renderive/state/Triple_State_Strategy.hpp index 6c3cd44..bb7b153 100644 --- a/Kernel/src/renderive/state/Triple_State_Strategy.hpp +++ b/Kernel/src/renderive/state/Triple_State_Strategy.hpp @@ -9,6 +9,7 @@ #include "renderive/base/observer/Observer.hpp" #include "renderive/base/property/Concepts.hpp" #include "Concepts.hpp" +#include "Render_State_View.hpp" #include "base/State_Strategy_Base.hpp" template > struct Triple_State_Strategy : That, State_Strategy_Base { @@ -95,11 +96,11 @@ struct Triple_State_Strategy : That, State_Strategy_Base { std::lock_guard lock(mtx); return publish_count; } - State render_use_state() const { - std::lock_guard lock(mtx); +private: + friend class Render_State_View; + const State& render_state_value() const noexcept { return *render_state; } -private: Observer observer; State states[4]; State* render_state; diff --git a/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp b/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp index f6364f3..42ff218 100644 --- a/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp +++ b/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp @@ -26,6 +26,38 @@ using Real_Time_Data_Test_Bridge = Observer_State; using Real_Time_Data_Test_History = History_Real_Time_Data, std::mutex, Real_Time_Data_Test_Bridge>; using Real_Time_Data_Test_Attachment = Attach_Real_Time_Data; +class Real_Time_Data_Render_Probe : public Renderable_Base { + class Bound_Latest : public Latest_Real_Time_Data { + public: + explicit Bound_Latest(Renderable_Base& owner) + : binding_(bind_renderable(owner)) {} + private: + Real_Time_Data_Binding binding_; + }; +public: + explicit Real_Time_Data_Render_Probe(Scene_Base& scene) : Renderable_Base(scene), data(*this) {} + void render(const Scene_Render_Context&) override { + const auto& value = render_state_view().get(data); + rendered_value = value.value_or(-1); + } + Bound_Latest data; + int rendered_value{-1}; +}; +TEST(real_time_data_render_state_test, publishes_once_at_frame_boundary_and_reads_without_snapshot) { + Scene2D_Context<> scene; + auto renderable = std::make_shared(scene); + scene.attach_renderable(renderable); + renderable->data.update(1); + scene.publish_frame_state(); + renderable->data.update(2); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(renderable->rendered_value, 1); + scene.publish_frame_state(); + scene.render(); + scene.wait_for_render(); + EXPECT_EQ(renderable->rendered_value, 2); +} TEST(latest_real_time_data_test, retains_only_latest_value) { Latest_Real_Time_Data data; data.update(1); diff --git a/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp b/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp index 4a5e15d..ab6d67d 100644 --- a/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp +++ b/Kernel/tests/renderive/scene/Scene_State_Observer_Test.cpp @@ -5,6 +5,7 @@ #include #include #include "renderive/scene/Scene.hpp" +#include "../state/State_Test_Types.hpp" struct Scene_State_Observer_Test_State { int value{}; }; @@ -26,6 +27,7 @@ struct Scene_State_Observer_Test_Recorder { using Scene_State_Observer_Test_Scene_Observer = Observer_State; using Scene_State_Observer_Test_State_Observer = Observer_State<>; TEST(scene_state_observer_test, scene_uses_triple_state_and_observes_lifecycle) { + State_Render_State_Reader render; Scene_State_Observer_Test_Recorder recorder; using Scene = Scene2D_Context, Recording_Color_Cache, Scene_State_Observer_Test_State, Scene_State_Observer_Test_State_Observer, Scene_State_Observer_Test_Scene_Observer>; Scene scene(With_Observer(Scene_State_Observer_Test_State_Observer{}), With_Observer(Scene_State_Observer_Test_Scene_Observer(recorder, Scene_State_Observer_Test_Time_Source{}))); @@ -33,7 +35,7 @@ TEST(scene_state_observer_test, scene_uses_triple_state_and_observes_lifecycle) scene.Scene_State_Strategy::publish(); scene.render(); scene.wait_for_render(); - EXPECT_EQ(scene.Scene_State_Strategy::render_use_state().value, 9); + EXPECT_EQ(render.read(static_cast(scene)).value, 9); EXPECT_EQ(recorder.data->events, (std::vector{Scene_Base::Observation_Event::render_submitted, Scene_Base::Observation_Event::render_started, Scene_Base::Observation_Event::render_completed})); } struct Scene_Reentrant_Observer_Data { diff --git a/Kernel/tests/renderive/state/Double_State_Strategy_Test.cpp b/Kernel/tests/renderive/state/Double_State_Strategy_Test.cpp index e3446ce..80564a0 100644 --- a/Kernel/tests/renderive/state/Double_State_Strategy_Test.cpp +++ b/Kernel/tests/renderive/state/Double_State_Strategy_Test.cpp @@ -12,20 +12,22 @@ TEST(double_state_strategy_test, calls_optional_state_validator_when_present) { EXPECT_THROW(builder.build(1, "invalid"), std::invalid_argument); } TEST(double_state_strategy_test, keeps_cache_separate_until_publish) { + State_Render_State_Reader render; auto product = State_Checked_Product::Builder{}.set<&State_Checked_Value::percent>(20).set<&State_Checked_Value::batch_size>(4).build(1, "state"); product->set<&State_Checked_Value::percent>(60); EXPECT_EQ(product->get<&State_Checked_Value::percent>(), 60); - EXPECT_EQ(product->render_use_state().percent.get(), 20); + EXPECT_EQ(render.read(*product).percent.get(), 20); product->publish(); - EXPECT_EQ(product->render_use_state().percent.get(), 60); + EXPECT_EQ(render.read(*product).percent.get(), 60); } -TEST(double_state_strategy_test, returns_render_state_snapshot) { +TEST(double_state_strategy_test, keeps_acquired_render_reference_stable_until_publish) { + State_Render_State_Reader render; State_Plain_Strategy strategy(State_Plain_Value{.count = 1, .name = "snapshot"}); - const auto snapshot = strategy.render_use_state(); + const auto snapshot = render.read(strategy); strategy.set<&State_Plain_Value::count>(2); strategy.publish(); EXPECT_EQ(snapshot.count, 1); - EXPECT_EQ(strategy.render_use_state().count, 2); + EXPECT_EQ(render.read(strategy).count, 2); } TEST(double_state_strategy_test, keeps_cached_value_when_runtime_validation_fails) { auto product = State_Checked_Product::Builder{}.set<&State_Checked_Value::percent>(20).set<&State_Checked_Value::batch_size>(4).build(1, "state"); @@ -33,10 +35,11 @@ TEST(double_state_strategy_test, keeps_cached_value_when_runtime_validation_fail EXPECT_EQ(product->get<&State_Checked_Value::percent>(), 20); } TEST(double_state_strategy_test, supports_atomic_wait_mutex) { + State_Render_State_Reader render; auto product = State_Wait_Product::Builder{State_Plain_Value{.count = 1, .name = "wait"}}.build(); product->set<&State_Plain_Value::count>(2); product->publish(); - EXPECT_EQ(product->render_use_state().count, 2); + EXPECT_EQ(render.read(*product).count, 2); } TEST(double_state_strategy_test, passes_additional_builder_template_arguments) { static_assert(std::same_as); @@ -95,6 +98,7 @@ struct Double_State_Throwing_Assignment_State { inline static bool throw_on_copy_assignment{}; }; TEST(double_state_strategy_test, failed_publish_keeps_render_state_and_revision_unchanged) { + State_Render_State_Reader render; using Strategy = Double_State_Strategy; Double_State_Throwing_Assignment_State::throw_on_copy_assignment = false; Strategy strategy(Double_State_Throwing_Assignment_State(1, 2)); @@ -104,10 +108,10 @@ TEST(double_state_strategy_test, failed_publish_keeps_render_state_and_revision_ EXPECT_THROW(strategy.publish(), std::runtime_error); Double_State_Throwing_Assignment_State::throw_on_copy_assignment = false; EXPECT_EQ(strategy.state_revision(), 0); - EXPECT_EQ(strategy.render_use_state().first, 1); - EXPECT_EQ(strategy.render_use_state().second, 2); + EXPECT_EQ(render.read(strategy).first, 1); + EXPECT_EQ(render.read(strategy).second, 2); strategy.publish(); EXPECT_EQ(strategy.state_revision(), 1); - EXPECT_EQ(strategy.render_use_state().first, 10); - EXPECT_EQ(strategy.render_use_state().second, 20); + EXPECT_EQ(render.read(strategy).first, 10); + EXPECT_EQ(render.read(strategy).second, 20); } diff --git a/Kernel/tests/renderive/state/State_Test_Types.hpp b/Kernel/tests/renderive/state/State_Test_Types.hpp index 4d73168..c2176b0 100644 --- a/Kernel/tests/renderive/state/State_Test_Types.hpp +++ b/Kernel/tests/renderive/state/State_Test_Types.hpp @@ -3,6 +3,26 @@ #include #include "renderive/base/property/Property.hpp" #include "renderive/state/State_Strategy.hpp" +#include "renderive/scene/Scene.hpp" +class State_Render_State_Reader { + class Reader : public Renderable_Base { + public: + explicit Reader(Scene_Base& scene) : Renderable_Base(scene) {} + template + auto read(const Source& source) const { + return render_state_view().get(source); + } + }; +public: + State_Render_State_Reader() : reader_(scene_) {} + template + auto read(const Source& source) const { + return reader_.read(source); + } +private: + Scene2D_Context<> scene_; + Reader reader_; +}; struct State_Plain_Value { int count{}; std::string name; diff --git a/Kernel/tests/renderive/state/Triple_State_Strategy_Test.cpp b/Kernel/tests/renderive/state/Triple_State_Strategy_Test.cpp index 6762893..4fb0b6b 100644 --- a/Kernel/tests/renderive/state/Triple_State_Strategy_Test.cpp +++ b/Kernel/tests/renderive/state/Triple_State_Strategy_Test.cpp @@ -4,26 +4,29 @@ #include #include #include "renderive/state/State_Strategy.hpp" +#include "State_Test_Types.hpp" struct Triple_State_Test_Base {}; struct Triple_State_Test_State { int value{}; }; TEST(triple_state_strategy_test, publishes_then_acquires_render_state) { + State_Render_State_Reader render; Triple_State_Strategy strategy; strategy.set<&Triple_State_Test_State::value>(7); strategy.publish(); - EXPECT_EQ(strategy.render_use_state().value, 0); + EXPECT_EQ(render.read(strategy).value, 0); EXPECT_EQ(strategy.acquire_render_state(), 1); - EXPECT_EQ(strategy.render_use_state().value, 7); + EXPECT_EQ(render.read(strategy).value, 7); } TEST(triple_state_strategy_test, returns_render_state_snapshot) { + State_Render_State_Reader render; Triple_State_Strategy strategy; - const auto snapshot = strategy.render_use_state(); + const auto snapshot = render.read(strategy); strategy.set<&Triple_State_Test_State::value>(7); strategy.publish(); strategy.acquire_render_state(); EXPECT_EQ(snapshot.value, 0); - EXPECT_EQ(strategy.render_use_state().value, 7); + EXPECT_EQ(render.read(strategy).value, 7); } struct Multi_State_A_Base {}; struct Multi_State_B_Base {}; @@ -37,15 +40,17 @@ struct Multi_State_A_Strategy : Double_State_Strategy {}; struct Multi_State_Product : Multi_State_A_Strategy, Multi_State_B_Strategy {}; TEST(double_state_strategy_test, supports_named_base_access_in_multiple_inheritance) { + State_Render_State_Reader render; Multi_State_Product product; product.Multi_State_A_Strategy::set<&Multi_State_A::value>(3); product.Multi_State_B_Strategy::set<&Multi_State_B::value>(8); product.Multi_State_A_Strategy::publish(); product.Multi_State_B_Strategy::publish(); - EXPECT_EQ(product.Multi_State_A_Strategy::render_use_state().value, 3); - EXPECT_EQ(product.Multi_State_B_Strategy::render_use_state().value, 8); + EXPECT_EQ(render.read(static_cast(product)).value, 3); + EXPECT_EQ(render.read(static_cast(product)).value, 8); } TEST(triple_state_strategy_test, concurrently_acquires_render_revision_without_data_race) { + State_Render_State_Reader render; Triple_State_Strategy strategy; std::atomic finished{}; auto acquire = [&] { @@ -64,7 +69,7 @@ TEST(triple_state_strategy_test, concurrently_acquires_render_revision_without_d first.join(); second.join(); EXPECT_EQ(strategy.acquire_render_state(), 1000); - EXPECT_EQ(strategy.render_use_state().value, 1000); + EXPECT_EQ(render.read(strategy).value, 1000); } struct Triple_State_Throwing_Assignment_State { int first{}; @@ -85,6 +90,7 @@ struct Triple_State_Throwing_Assignment_State { inline static bool throw_on_copy_assignment{}; }; TEST(triple_state_strategy_test, failed_publish_keeps_published_revision_and_render_state_unchanged) { + State_Render_State_Reader render; using Strategy = Triple_State_Strategy; Triple_State_Throwing_Assignment_State::throw_on_copy_assignment = false; Strategy strategy(Triple_State_Throwing_Assignment_State(1, 2)); @@ -95,11 +101,11 @@ TEST(triple_state_strategy_test, failed_publish_keeps_published_revision_and_ren Triple_State_Throwing_Assignment_State::throw_on_copy_assignment = false; EXPECT_EQ(strategy.state_revision(), 0); EXPECT_EQ(strategy.acquire_render_state(), 0); - EXPECT_EQ(strategy.render_use_state().first, 1); - EXPECT_EQ(strategy.render_use_state().second, 2); + EXPECT_EQ(render.read(strategy).first, 1); + EXPECT_EQ(render.read(strategy).second, 2); strategy.publish(); EXPECT_EQ(strategy.state_revision(), 1); EXPECT_EQ(strategy.acquire_render_state(), 1); - EXPECT_EQ(strategy.render_use_state().first, 10); - EXPECT_EQ(strategy.render_use_state().second, 20); + EXPECT_EQ(render.read(strategy).first, 10); + EXPECT_EQ(render.read(strategy).second, 20); } diff --git a/Kernel/tests/renderive/state/base/State_Strategy_Base_Test.cpp b/Kernel/tests/renderive/state/base/State_Strategy_Base_Test.cpp index 8c8b82e..cab9459 100644 --- a/Kernel/tests/renderive/state/base/State_Strategy_Base_Test.cpp +++ b/Kernel/tests/renderive/state/base/State_Strategy_Base_Test.cpp @@ -1,10 +1,11 @@ #include #include "../State_Test_Types.hpp" TEST(state_strategy_base_test, publishes_through_virtual_interface) { + State_Render_State_Reader render; State_Plain_Strategy strategy; State_Strategy_Base* base = &strategy; strategy.set<&State_Plain_Value::count>(7); base->publish(); EXPECT_EQ(base->state_revision(), 1); - EXPECT_EQ(strategy.render_use_state().count, 7); + EXPECT_EQ(render.read(strategy).count, 7); } diff --git a/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp b/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp index 3205037..caa4554 100644 --- a/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp +++ b/Kernel/tests/renderive/threading/Threading_Contract_Test.cpp @@ -436,7 +436,7 @@ TEST(threading_contract_test, history_real_time_data_supports_concurrent_update_ EXPECT_EQ(state.retained_value_count, data.size()); EXPECT_GE(state.revision, update_count); } -TEST(threading_contract_test, double_state_returns_consistent_snapshots_during_concurrent_set_publish_and_read) { +TEST(threading_contract_test, double_state_keeps_cache_consistent_during_concurrent_set_publish_and_read) { Threading_Test_Double_State strategy; constexpr int update_count = 4000; std::atomic start{}; @@ -451,14 +451,6 @@ TEST(threading_contract_test, double_state_returns_consistent_snapshots_during_c } writer_done.store(true, std::memory_order_release); }); - std::thread render_reader([&] { - wait_start(start); - while (!writer_done.load(std::memory_order_acquire)) { - if (!valid_frame(strategy.render_use_state().frame)) { - invalid.fetch_add(1, std::memory_order_relaxed); - } - } - }); std::thread cache_reader([&] { wait_start(start); while (!writer_done.load(std::memory_order_acquire)) { @@ -469,7 +461,6 @@ TEST(threading_contract_test, double_state_returns_consistent_snapshots_during_c }); start.store(true, std::memory_order_release); writer.join(); - render_reader.join(); cache_reader.join(); EXPECT_EQ(invalid.load(std::memory_order_acquire), 0); EXPECT_EQ(strategy.state_revision(), update_count); @@ -493,14 +484,8 @@ TEST(threading_contract_test, triple_state_returns_consistent_snapshots_during_c wait_start(start); while (!writer_done.load(std::memory_order_acquire)) { strategy.acquire_render_state(); - if (!valid_frame(strategy.render_use_state().frame)) { - invalid.fetch_add(1, std::memory_order_relaxed); - } } strategy.acquire_render_state(); - if (!valid_frame(strategy.render_use_state().frame)) { - invalid.fetch_add(1, std::memory_order_relaxed); - } }); std::thread cache_reader([&] { wait_start(start); diff --git a/render_2D/axis/Abs_Axis.cpp b/render_2D/axis/Abs_Axis.cpp index 95ef3e7..9b2998a 100644 --- a/render_2D/axis/Abs_Axis.cpp +++ b/render_2D/axis/Abs_Axis.cpp @@ -1,64 +1,14 @@ #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(Plot_Core& plot) : Renderable(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); @@ -66,90 +16,8 @@ int Abs_Axis::pixel_sample_count(Range range) const { 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); - } +int Abs_Axis::sub_tick_count(double) const noexcept { + return 4; } } // namespace renderive diff --git a/render_2D/axis/Abs_Axis.h b/render_2D/axis/Abs_Axis.h index 93d8b4a..22bccbe 100644 --- a/render_2D/axis/Abs_Axis.h +++ b/render_2D/axis/Abs_Axis.h @@ -3,71 +3,21 @@ #include "../renderable/Renderable.h" #include "Axis_Types.h" -#include - #include -#include namespace renderive { -class LIB_DECL Abs_Axis : public Double_State_Strategy { +class LIB_DECL Abs_Axis : public Renderable { 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); + explicit Abs_Axis(Plot_Core& plot); ~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]] virtual Axis_Transform transform() const = 0; + [[nodiscard]] virtual Axis_Transform transform(const Render_State_View& state) const noexcept = 0; [[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(); - } + [[nodiscard]] virtual double tick_step(Range range) const = 0; + [[nodiscard]] int sub_tick_count(double major_step) const noexcept; + [[nodiscard]] virtual std::string tick_label(double tick) const = 0; }; } // namespace renderive diff --git a/render_2D/axis/Axis_State_Strategy.hpp b/render_2D/axis/Axis_State_Strategy.hpp new file mode 100644 index 0000000..512c090 --- /dev/null +++ b/render_2D/axis/Axis_State_Strategy.hpp @@ -0,0 +1,156 @@ +#pragma once + +#include "Abs_Axis.h" +#include "Axis_Format.h" +#include "../render/Blend2D_Cache.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace renderive::detail { + +template +requires std::is_base_of_v +class Axis_State_Strategy + : public Double_State_Strategy> { + using State_Observer = Observer_State; + using Base = Double_State_Strategy; + +public: + Axis_State_Strategy(Plot_Core& plot, State state) + : Base(state, + With_Observer{State_Observer(Renderable_State_Observer(this))}, + plot) {} + + [[nodiscard]] Axis_Transform transform() const final { + return Base::read([this](const State& state) { + return make_transform(state, coordinate_range(state)); + }); + } + + [[nodiscard]] Axis_Transform transform(const Render_State_View& view) const noexcept final { + const auto& state = view.get(static_cast(*this)); + return make_transform(state, coordinate_range(state)); + } + + [[nodiscard]] double tick_step(Range range) const final { + return Base::read([this, range](const State& state) { + return calculate_tick_step(range, state); + }); + } + + [[nodiscard]] std::string tick_label(double tick) const final { + return Base::read([this, tick](const State& state) { + return format_tick_label(tick, state); + }); + } + +protected: + [[nodiscard]] virtual Range coordinate_range(const State& state) const noexcept = 0; + + [[nodiscard]] virtual double calculate_tick_step(Range range, const State&) 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; + } + + [[nodiscard]] virtual std::string format_tick_label(double tick, const State& state) const { + int precision = 2; + if constexpr (requires { state.precision; }) + precision = state.precision.get(); + return localized_axis_number(tick, precision, state.locale); + } + +private: + static Axis_Transform make_transform(const State& state, Range coordinates) noexcept { + return { + coordinates, + state.orientation == Orientation::Horizontal ? static_cast(state.x) + : static_cast(state.y), + static_cast(state.pixel_length) + }; + } + + void paint(Painter& painter, const Render_State_View& view) final { + const auto& state = view.get(static_cast(*this)); + if (state.pixel_length == 0) + return; + + const Range coordinates = coordinate_range(state); + const Axis_Transform axis = make_transform(state, coordinates); + const double step = calculate_tick_step(coordinates, state); + 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 = axis.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, format_tick_label(tick, state), state.unit_text_font, + state.unit_text_pen, state.label_rotation_degrees); + const int subdivisions = std::max(0, this->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 = axis.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::detail diff --git a/render_2D/axis/Frequency_Axis.cpp b/render_2D/axis/Frequency_Axis.cpp index 5a7ad5d..446dae2 100644 --- a/render_2D/axis/Frequency_Axis.cpp +++ b/render_2D/axis/Frequency_Axis.cpp @@ -9,13 +9,14 @@ 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 { +std::string Frequency_Axis_Control::format_tick_label(double tick, + const Axis_Properties& state) 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"; + return localized_axis_number(tick / 1'000'000.0, state.precision.get(), state.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"; + return localized_axis_number(tick / 1'000.0, state.precision.get(), state.locale) + " kHz"; + return localized_axis_number(tick, state.precision.get(), state.locale) + " Hz"; } } // namespace renderive::detail diff --git a/render_2D/axis/Frequency_Axis.h b/render_2D/axis/Frequency_Axis.h index 8e9bdb6..1eb9a06 100644 --- a/render_2D/axis/Frequency_Axis.h +++ b/render_2D/axis/Frequency_Axis.h @@ -9,7 +9,8 @@ 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; +protected: + [[nodiscard]] std::string format_tick_label(double tick, const Axis_Properties& state) const override; }; } // namespace detail diff --git a/render_2D/axis/Numeric_Axis.cpp b/render_2D/axis/Numeric_Axis.cpp index 4829b9c..6f4cfb5 100644 --- a/render_2D/axis/Numeric_Axis.cpp +++ b/render_2D/axis/Numeric_Axis.cpp @@ -1,96 +1,27 @@ #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>(); -} + : Axis_State_Strategy(plot, properties) {} void Axis_Control::handle_event(const Event& event) { - if (event.type == Event_Type::Wheel && use_wheel()) { + if (event.type == Event_Type::Wheel && get<&Axis_Properties::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 Range range = get<&Axis_Properties::coordinates>(); + const Orientation orientation = get<&Axis_Base_Properties::orientation>(); + const double anchor_pixel = orientation == Orientation::Horizontal + ? wheel.position.x + : wheel.position.y; + const double anchor = transform().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}); + const Range next{anchor + (range.origin - anchor) * factor, + anchor + (range.target - anchor) * factor}; + set<&Axis_Properties::coordinates>(next); event.accept(); return; } - if (!use_drag()) + if (!get<&Axis_Properties::drag>()) return; if (event.type == Event_Type::Pointer_Press) { const auto& pointer = static_cast(event); @@ -114,12 +45,16 @@ void Axis_Control::handle_event(const Event& event) { }); if (!dragging) return; - const double delta = orientation() == Orientation::Horizontal + const Orientation orientation = get<&Axis_Base_Properties::orientation>(); + 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}); + const Range range = get<&Axis_Properties::coordinates>(); + const std::size_t pixel_length = get<&Axis_Base_Properties::pixel_length>(); + const double shift = pixel_length == 0 + ? 0.0 + : -delta * range.length() / static_cast(pixel_length); + set<&Axis_Properties::coordinates>(Range{range.origin + shift, range.target + shift}); event.accept(); } else if (event.type == Event_Type::Pointer_Release) { bool accepted{}; @@ -132,26 +67,17 @@ void Axis_Control::handle_event(const Event& event) { } } -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(); + Axis_State_Strategy::publish(); interaction_.publish(); } std::uint64_t Axis_Control::state_revision() const { - return Abs_Axis::state_revision() + numeric_.state_revision() + interaction_.state_revision(); + return Axis_State_Strategy::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(); +Range Axis_Control::coordinate_range(const Axis_Properties& state) const noexcept { + return state.coordinates.get(); } } // namespace renderive::detail diff --git a/render_2D/axis/Numeric_Axis.h b/render_2D/axis/Numeric_Axis.h index d12a1ac..2cf7c5f 100644 --- a/render_2D/axis/Numeric_Axis.h +++ b/render_2D/axis/Numeric_Axis.h @@ -1,65 +1,51 @@ #pragma once -#include "Abs_Axis.h" +#include "Axis_State_Strategy.hpp" #include "Axis_Builder.h" #include -#include +#include + +#include +#include namespace renderive { +struct Axis_Coordinate_Range_Validator { + void operator()(const Range& value) const { + if (!std::isfinite(value.origin) || !std::isfinite(value.target) || !(value.size() > 0.0)) + throw std::invalid_argument("axis coordinate range must be finite and non-empty"); + } +}; + struct Axis_Properties : Axis_Base_Properties { - Range coordinates{0.0, 20.0}; - int precision = 2; + Validated_Value coordinates{Range{0.0, 20.0}}; + Range_Value 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 { +class LIB_DECL Axis_Control : public Axis_State_Strategy, 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_; +protected: + [[nodiscard]] Range coordinate_range(const Axis_Properties& state) const noexcept override; +private: Interaction_State interaction_; }; diff --git a/render_2D/axis/Time_Axis.cpp b/render_2D/axis/Time_Axis.cpp index e37dd53..dd2fdf0 100644 --- a/render_2D/axis/Time_Axis.cpp +++ b/render_2D/axis/Time_Axis.cpp @@ -9,117 +9,62 @@ 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, - {} - }; + Time_Axis_State state; + static_cast(state) = properties; + return state; } } // 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(); -} + : Axis_State_Strategy(plot, time_state_from(properties)) {} std::size_t Time_Axis_Control::time_point_count() const { - return time_.read([](const Time_Axis_State& state) { return state.samples.size(); }); + return 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) { + 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)); + const auto limit = static_cast(std::max(512, state.visible_count.get() * 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 read([tick](const Time_Axis_State& state) { + const 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)}; - }); +Range Time_Axis_Control::coordinate_range(const Time_Axis_State& state) const noexcept { + const int latest = std::max(1, state.next_tick - 1); + const int earliest = std::max(0, latest - state.visible_count.get() + 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)); +double Time_Axis_Control::calculate_tick_step(Range range, const Time_Axis_State& state) const { + const double available = static_cast(state.pixel_length); + const double label_width = std::max(48.0, state.unit_text_font.size * 7.0); + const double label_count = + std::max(1.0, available / (label_width + state.tick_label_spacing_px.get())); 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(); +std::string Time_Axis_Control::format_tick_label(double tick, const Time_Axis_State& state) const { + const int target = static_cast(std::llround(tick)); + const auto iterator = std::find_if(state.samples.begin(), state.samples.end(), + [target](const auto& value) { return value.first == target; }); + return iterator == state.samples.end() + ? std::string{} + : formatted_axis_time(iterator->second, state.format); } } // namespace renderive::detail diff --git a/render_2D/axis/Time_Axis.h b/render_2D/axis/Time_Axis.h index 55b70ed..a6d13ac 100644 --- a/render_2D/axis/Time_Axis.h +++ b/render_2D/axis/Time_Axis.h @@ -1,61 +1,41 @@ #pragma once -#include "Abs_Axis.h" +#include "Axis_State_Strategy.hpp" #include "Axis_Builder.h" #include -#include - +#include #include +#include #include namespace renderive { struct Time_Axis_Properties : Axis_Base_Properties { - int visible_count = 100; - int tick_label_spacing_px = 8; + Range_Value::max()> visible_count{100}; + Range_Value::max()> 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{}; +struct Time_Axis_State : Time_Axis_Properties { int next_tick{}; std::deque> samples; }; -class LIB_DECL Time_Axis_Control : public Abs_Axis { +class LIB_DECL Time_Axis_Control : public Axis_State_Strategy { 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_; + [[nodiscard]] Range coordinate_range(const Time_Axis_State& state) const noexcept override; + [[nodiscard]] double calculate_tick_step(Range range, const Time_Axis_State& state) const override; + [[nodiscard]] std::string format_tick_label(double tick, const Time_Axis_State& state) const override; }; } // namespace detail diff --git a/render_2D/plot/Plot_Core.cpp b/render_2D/plot/Plot_Core.cpp index 2153ea9..5007707 100644 --- a/render_2D/plot/Plot_Core.cpp +++ b/render_2D/plot/Plot_Core.cpp @@ -180,7 +180,7 @@ public: using Renderable::Renderable; private: - void paint(detail::Painter&) override {} + void paint(detail::Painter&, const Render_State_View&) override {} }; } // namespace @@ -353,12 +353,7 @@ bool Plot_Core::prepare_frame() { auto paint_frame = scene.frame_control.acquire_painter(); if (!paint_frame) return false; - const auto topology = scene.topology_snapshot(); - 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(); - } + scene.publish_frame_state(); auto& scene_state = static_cast::Scene_State_Strategy&>(scene); scene_state.template set<&::Scene2D_State::revision>(scene_state.state_revision() + 1); scene_state.publish(); diff --git a/render_2D/plottable/Afterglow.cpp b/render_2D/plottable/Afterglow.cpp index 75218e8..87881a5 100644 --- a/render_2D/plottable/Afterglow.cpp +++ b/render_2D/plottable/Afterglow.cpp @@ -11,7 +11,7 @@ using Afterglow_History = Plottable_History_Real_Time_Data, } struct Afterglow_Control::Impl { Impl(Afterglow_Control& owner, std::shared_ptr frequency, std::shared_ptr power) - : frequency_axis(std::move(frequency)), power_axis(std::move(power)), history(observe_real_time_data(owner)) {} + : frequency_axis(std::move(frequency)), power_axis(std::move(power)), history(owner) {} std::shared_ptr frequency_axis; std::shared_ptr power_axis; Afterglow_History history; @@ -32,7 +32,9 @@ std::size_t Afterglow_Control::rendered_cell_count() const { 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())); + const int height = state.power_point_size.get() > 0 + ? state.power_point_size.get() + : std::max(1, static_cast(impl_->power_axis->transform().pixel_length)); return width > 0 && height > 0 ? static_cast(width) * static_cast(height) : 0; } void Afterglow_Control::append_spectrum(std::span values) { @@ -47,13 +49,15 @@ void Afterglow_Control::append_spectrum(std::pmr::vector&& values) { void Afterglow_Control::publish() { publish_properties(); } -void Afterglow_Control::paint(Painter& painter) { - const auto state = render_properties(); - const auto history = impl_->history.snapshot(); +void Afterglow_Control::paint(Painter& painter, const Render_State_View& view) { + const auto& state = render_properties(view); + const auto& history = view.get(impl_->history); 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())); + const int height = state.power_point_size.get() > 0 + ? state.power_point_size.get() + : std::max(1, static_cast(impl_->power_axis->transform(view).pixel_length)); if (width <= 0 || height <= 0) return; std::vector intensity(static_cast(width) * height); @@ -76,7 +80,7 @@ void Afterglow_Control::paint(Painter& painter) { std::vector pixels(intensity.size()); 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); + painter.heatmap(mapped_rect(impl_->frequency_axis->transform(view), impl_->power_axis->transform(view), state.frequency_range, state.power_range), width, height, pixels, Image_Interpolation_Mode::Bilinear); } } } diff --git a/render_2D/plottable/Afterglow.h b/render_2D/plottable/Afterglow.h index e42b228..ba11f29 100644 --- a/render_2D/plottable/Afterglow.h +++ b/render_2D/plottable/Afterglow.h @@ -28,7 +28,7 @@ public: append_spectrum(std::span(values.data(), values.size())); } protected: - void paint(Painter& painter) override; + void paint(Painter& painter, const Render_State_View& state) override; private: struct Impl; std::unique_ptr impl_; diff --git a/render_2D/plottable/Constellation_Diagram.cpp b/render_2D/plottable/Constellation_Diagram.cpp index 91071aa..2527e5d 100644 --- a/render_2D/plottable/Constellation_Diagram.cpp +++ b/render_2D/plottable/Constellation_Diagram.cpp @@ -17,7 +17,7 @@ using Constellation_History = Plottable_History_Real_Time_Data i, std::shared_ptr q) - : i_axis(std::move(i)), q_axis(std::move(q)), points(observe_real_time_data(owner)) {} + : i_axis(std::move(i)), q_axis(std::move(q)), points(owner) {} std::shared_ptr i_axis; std::shared_ptr q_axis; Constellation_History points; @@ -40,17 +40,19 @@ std::size_t Constellation_Diagram_Control::point_count() const { void Constellation_Diagram_Control::fit_square_to_axes() { const auto state = properties(); const double side = std::max(state.i_range.size(), state.q_range.size()); - impl_->i_axis->set_coord_range({state.i_range.center() - side * 0.5, state.i_range.center() + side * 0.5}); - impl_->q_axis->set_coord_range({state.q_range.center() + side * 0.5, state.q_range.center() - side * 0.5}); + impl_->i_axis->set<&Axis_Properties::coordinates>( + Range{state.i_range.center() - side * 0.5, state.i_range.center() + side * 0.5}); + impl_->q_axis->set<&Axis_Properties::coordinates>( + Range{state.q_range.center() + side * 0.5, state.q_range.center() - side * 0.5}); } void Constellation_Diagram_Control::publish() { publish_properties(); } -void Constellation_Diagram_Control::paint(Painter& painter) { - const auto state = render_properties(); - const auto points = impl_->points.snapshot(); - const Axis_Transform x = impl_->i_axis->transform(); - const Axis_Transform y = impl_->q_axis->transform(); +void Constellation_Diagram_Control::paint(Painter& painter, const Render_State_View& view) { + const auto& state = render_properties(view); + const auto& points = view.get(impl_->points); + const Axis_Transform x = impl_->i_axis->transform(view); + const Axis_Transform y = impl_->q_axis->transform(view); const int count = static_cast(state.type); const double radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; for(int index = 0; index < count; ++index) { diff --git a/render_2D/plottable/Constellation_Diagram.h b/render_2D/plottable/Constellation_Diagram.h index 4b9015c..db7c2b9 100644 --- a/render_2D/plottable/Constellation_Diagram.h +++ b/render_2D/plottable/Constellation_Diagram.h @@ -25,7 +25,7 @@ public: [[nodiscard]] std::size_t point_count() const; void fit_square_to_axes(); protected: - void paint(Painter& painter) override; + void paint(Painter& painter, const Render_State_View& state) override; private: struct Impl; std::unique_ptr impl_; diff --git a/render_2D/plottable/Frequency_Trace.cpp b/render_2D/plottable/Frequency_Trace.cpp index 0d4151c..e9e0d05 100644 --- a/render_2D/plottable/Frequency_Trace.cpp +++ b/render_2D/plottable/Frequency_Trace.cpp @@ -11,7 +11,7 @@ using Frequency_Trace_History = Plottable_History_Real_Time_Data time, std::shared_ptr value) - : time_axis(std::move(time)), value_axis(std::move(value)), samples(observe_real_time_data(owner)) {} + : time_axis(std::move(time)), value_axis(std::move(value)), samples(owner) {} std::shared_ptr time_axis; std::shared_ptr value_axis; Frequency_Trace_History samples; @@ -20,7 +20,7 @@ Frequency_Trace_Control::Frequency_Trace_Control(Plot_Core& plot, const Frequenc : Plottable_State(plot, properties), impl_(std::make_unique(*this, std::move(time_axis), std::move(value_axis))) {} Frequency_Trace_Control::~Frequency_Trace_Control() = default; void Frequency_Trace_Control::append_sample(int tick, double value) { - const int limit = std::max(2, impl_->time_axis->visible_time_point_count()); + const int limit = std::max(2, impl_->time_axis->get<&Time_Axis_Properties::visible_count>()); impl_->samples.update({tick, value}, static_cast(limit)); changed(); } @@ -37,13 +37,13 @@ std::size_t Frequency_Trace_Control::rendered_point_count() const { void Frequency_Trace_Control::publish() { publish_properties(); } -void Frequency_Trace_Control::paint(Painter& painter) { - const auto state = render_properties(); - const auto samples = impl_->samples.snapshot(); +void Frequency_Trace_Control::paint(Painter& painter, const Render_State_View& view) { + const auto& state = render_properties(view); + const auto& samples = view.get(impl_->samples); if(samples.size() < 2) return; - const Axis_Transform x = impl_->time_axis->transform(); - const Axis_Transform y = impl_->value_axis->transform(); + const Axis_Transform x = impl_->time_axis->transform(view); + const Axis_Transform y = impl_->value_axis->transform(view); std::vector points; points.reserve(samples.size()); for(const auto& [tick, value] : samples) diff --git a/render_2D/plottable/Frequency_Trace.h b/render_2D/plottable/Frequency_Trace.h index eaf407b..9059fee 100644 --- a/render_2D/plottable/Frequency_Trace.h +++ b/render_2D/plottable/Frequency_Trace.h @@ -15,7 +15,7 @@ public: [[nodiscard]] std::size_t sample_count() const; [[nodiscard]] std::size_t rendered_point_count() const; protected: - void paint(Painter& painter) override; + void paint(Painter& painter, const Render_State_View& state) override; private: struct Impl; std::unique_ptr impl_; diff --git a/render_2D/plottable/Plottable.h b/render_2D/plottable/Plottable.h index b46eb49..4e189a6 100644 --- a/render_2D/plottable/Plottable.h +++ b/render_2D/plottable/Plottable.h @@ -71,8 +71,8 @@ protected: Properties properties() const { return Base::read([](const Properties& value) { return value; }); } - Properties render_properties() const { - return Base::render_use_state(); + const Properties& render_properties(const Render_State_View& state) const noexcept { + return state.get(static_cast(*this)); } void publish_properties() { Base::publish(); @@ -82,7 +82,6 @@ private: return Base::state_revision(); } using Base::read; - using Base::render_use_state; using Base::update; }; template diff --git a/render_2D/plottable/Plottable_Real_Time_Data.h b/render_2D/plottable/Plottable_Real_Time_Data.h index d015001..8a57154 100644 --- a/render_2D/plottable/Plottable_Real_Time_Data.h +++ b/render_2D/plottable/Plottable_Real_Time_Data.h @@ -11,7 +11,23 @@ inline Plottable_Real_Time_Data_Observer observe_real_time_data(Renderable& rend return Plottable_Real_Time_Data_Observer(Frame_Strategy_Real_Time_Data_Observer(renderable)); } template -using Plottable_Latest_Real_Time_Data = Latest_Real_Time_Data; +class Plottable_Latest_Real_Time_Data + : public Latest_Real_Time_Data { + using Base = Latest_Real_Time_Data; +public: + explicit Plottable_Latest_Real_Time_Data(Renderable& owner) + : Base(observe_real_time_data(owner)), binding_(this->bind_renderable(owner)) {} +private: + Real_Time_Data_Binding binding_; +}; template -using Plottable_History_Real_Time_Data = History_Real_Time_Data; +class Plottable_History_Real_Time_Data + : public History_Real_Time_Data { + using Base = History_Real_Time_Data; +public: + explicit Plottable_History_Real_Time_Data(Renderable& owner) + : Base(observe_real_time_data(owner)), binding_(this->bind_renderable(owner)) {} +private: + Real_Time_Data_Binding binding_; +}; } diff --git a/render_2D/plottable/Selection_Rectangle_Overlay.cpp b/render_2D/plottable/Selection_Rectangle_Overlay.cpp index 158ab74..ef8c944 100644 --- a/render_2D/plottable/Selection_Rectangle_Overlay.cpp +++ b/render_2D/plottable/Selection_Rectangle_Overlay.cpp @@ -24,7 +24,7 @@ RectF axis_content_rect(const Axis_Transform& horizontal, const Axis_Transform& } struct Selection_Rectangle_Overlay_Control::Impl { Impl(Selection_Rectangle_Overlay_Control& owner, std::shared_ptr horizontal, std::shared_ptr vertical) - : horizontal_axis(std::move(horizontal)), vertical_axis(std::move(vertical)), regions(observe_real_time_data(owner)) {} + : horizontal_axis(std::move(horizontal)), vertical_axis(std::move(vertical)), regions(owner) {} std::shared_ptr horizontal_axis; std::shared_ptr vertical_axis; Plottable_History_Real_Time_Data> regions; @@ -86,7 +86,11 @@ void Selection_Rectangle_Overlay_Control::handle_event(const Event& event) { }); if(!selecting) return; - const RectF region{impl_->horizontal_axis->pixel_to_coord(start.x), impl_->vertical_axis->pixel_to_coord(start.y), impl_->horizontal_axis->pixel_to_coord(pointer.position.x) - impl_->horizontal_axis->pixel_to_coord(start.x), impl_->vertical_axis->pixel_to_coord(pointer.position.y) - impl_->vertical_axis->pixel_to_coord(start.y)}; + const Axis_Transform horizontal = impl_->horizontal_axis->transform(); + const Axis_Transform vertical = impl_->vertical_axis->transform(); + const RectF region{horizontal.pixel_to_coord(start.x), vertical.pixel_to_coord(start.y), + horizontal.pixel_to_coord(pointer.position.x) - horizontal.pixel_to_coord(start.x), + vertical.pixel_to_coord(pointer.position.y) - vertical.pixel_to_coord(start.y)}; if(std::abs(region.width) > 1e-9 && std::abs(region.height) > 1e-9) impl_->regions.update(region.normalized()); event.accept(); @@ -96,11 +100,13 @@ void Selection_Rectangle_Overlay_Control::publish() { publish_properties(); impl_->interaction.publish(); } -void Selection_Rectangle_Overlay_Control::paint(Painter& painter) { - const auto state = render_properties(); - const auto interaction = impl_->interaction.render_use_state(); - for(const RectF& region : impl_->regions.snapshot()) { - RectF pixels{impl_->horizontal_axis->coord_to_pixel(region.x), impl_->vertical_axis->coord_to_pixel(region.y), impl_->horizontal_axis->coord_to_pixel(region.right()) - impl_->horizontal_axis->coord_to_pixel(region.x), impl_->vertical_axis->coord_to_pixel(region.bottom()) - impl_->vertical_axis->coord_to_pixel(region.y)}; +void Selection_Rectangle_Overlay_Control::paint(Painter& painter, const Render_State_View& view) { + const auto& state = render_properties(view); + const auto& interaction = view.get(impl_->interaction); + const auto horizontal = impl_->horizontal_axis->transform(view); + const auto vertical = impl_->vertical_axis->transform(view); + for(const RectF& region : view.get(impl_->regions)) { + RectF pixels{horizontal.coord_to_pixel(region.x), vertical.coord_to_pixel(region.y), horizontal.coord_to_pixel(region.right()) - horizontal.coord_to_pixel(region.x), vertical.coord_to_pixel(region.bottom()) - vertical.coord_to_pixel(region.y)}; painter.rect(pixels, state.selection_border_pen, state.selection_brush); std::ostringstream text; text << region.width << " x " << region.height; diff --git a/render_2D/plottable/Selection_Rectangle_Overlay.h b/render_2D/plottable/Selection_Rectangle_Overlay.h index 12ae1a1..4b100ed 100644 --- a/render_2D/plottable/Selection_Rectangle_Overlay.h +++ b/render_2D/plottable/Selection_Rectangle_Overlay.h @@ -17,7 +17,7 @@ public: void clear_selected_regions(); void handle_event(const Event& event) override; protected: - void paint(Painter& painter) override; + void paint(Painter& painter, const Render_State_View& state) override; private: struct Impl; std::unique_ptr impl_; diff --git a/render_2D/plottable/Spectrum.cpp b/render_2D/plottable/Spectrum.cpp index efaf7d8..7c5771a 100644 --- a/render_2D/plottable/Spectrum.cpp +++ b/render_2D/plottable/Spectrum.cpp @@ -56,7 +56,7 @@ double spectrum_power_at(const Spectrum_Properties& properties, const Spectrum_F } struct Spectrum_Control::Impl { Impl(Spectrum_Control& owner, std::shared_ptr frequency, std::shared_ptr power) - : frequency_axis(std::move(frequency)), power_axis(std::move(power)), frame(observe_real_time_data(owner)) {} + : frequency_axis(std::move(frequency)), power_axis(std::move(power)), frame(owner) {} std::shared_ptr frequency_axis; std::shared_ptr power_axis; Plottable_Latest_Real_Time_Data frame; @@ -205,12 +205,14 @@ void Spectrum_Control::publish() { publish_properties(); impl_->interaction.publish(); } -void Spectrum_Control::paint(Painter& painter) { - const auto state = render_properties(); - const Spectrum_Frame frame = impl_->frame.snapshot().value_or(Spectrum_Frame{}); - const auto interaction = impl_->interaction.render_use_state(); - const Axis_Transform horizontal = impl_->frequency_axis->transform(); - const Axis_Transform vertical = impl_->power_axis->transform(); +void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) { + const auto& state = render_properties(view); + const auto& published_frame = view.get(impl_->frame); + const Spectrum_Frame empty_frame; + const Spectrum_Frame& frame = published_frame ? *published_frame : empty_frame; + const auto& interaction = view.get(impl_->interaction); + const Axis_Transform horizontal = impl_->frequency_axis->transform(view); + const Axis_Transform vertical = impl_->power_axis->transform(view); const RectF content = axes_rect(horizontal, vertical); if(state.sweep_region_visible) { const double first = horizontal.coord_to_pixel(state.sweep_frequency_range.origin); diff --git a/render_2D/plottable/Spectrum.h b/render_2D/plottable/Spectrum.h index 9b731fc..1939394 100644 --- a/render_2D/plottable/Spectrum.h +++ b/render_2D/plottable/Spectrum.h @@ -59,7 +59,7 @@ public: void set_current_marker_frequency(double frequency); void handle_event(const Event& event) override; protected: - void paint(Painter& painter) override; + void paint(Painter& painter, const Render_State_View& state) override; private: struct Impl; std::unique_ptr impl_; diff --git a/render_2D/plottable/Sweep_Spectrum.cpp b/render_2D/plottable/Sweep_Spectrum.cpp index e13b2ae..aae40ab 100644 --- a/render_2D/plottable/Sweep_Spectrum.cpp +++ b/render_2D/plottable/Sweep_Spectrum.cpp @@ -25,7 +25,7 @@ std::vector flatten(const std::deque>& blocks) { } struct Sweep_Spectrum_Control::Impl { Impl(Sweep_Spectrum_Control& owner, std::shared_ptr frequency, std::shared_ptr power) - : frequency_axis(std::move(frequency)), power_axis(std::move(power)), blocks(observe_real_time_data(owner)) {} + : frequency_axis(std::move(frequency)), power_axis(std::move(power)), blocks(owner) {} std::shared_ptr frequency_axis; std::shared_ptr power_axis; Sweep_Spectrum_History blocks; @@ -63,14 +63,14 @@ void Sweep_Spectrum_Control::publish() { const int limit = get<&Sweep_Spectrum_Properties::block_count>(); impl_->blocks.retain_latest(static_cast(limit)); } -void Sweep_Spectrum_Control::paint(Painter& painter) { - const auto state = render_properties(); - const auto blocks = impl_->blocks.snapshot(); +void Sweep_Spectrum_Control::paint(Painter& painter, const Render_State_View& view) { + const auto& state = render_properties(view); + const auto& blocks = view.get(impl_->blocks); const auto values = flatten(blocks); if(values.size() < 2) return; - const Axis_Transform x = impl_->frequency_axis->transform(); - const Axis_Transform y = impl_->power_axis->transform(); + const Axis_Transform x = impl_->frequency_axis->transform(view); + const Axis_Transform y = impl_->power_axis->transform(view); painter.polyline(curve_points(values, state.frequency_range, x, y, state.visible_range_only, state.interpolation_mode), state.pen); const double completed = std::min(1.0, static_cast(blocks.size()) / state.block_count.get()); const double frequency = state.frequency_range.origin + state.frequency_range.length() * completed; diff --git a/render_2D/plottable/Sweep_Spectrum.h b/render_2D/plottable/Sweep_Spectrum.h index 728fb50..01c2eb7 100644 --- a/render_2D/plottable/Sweep_Spectrum.h +++ b/render_2D/plottable/Sweep_Spectrum.h @@ -28,7 +28,7 @@ public: [[nodiscard]] std::size_t stored_point_count() const; [[nodiscard]] std::size_t rendered_point_count() const; protected: - void paint(Painter& painter) override; + void paint(Painter& painter, const Render_State_View& state) override; private: struct Impl; std::unique_ptr impl_; diff --git a/render_2D/plottable/Waterfall.cpp b/render_2D/plottable/Waterfall.cpp index 7b8cc90..d2247b6 100644 --- a/render_2D/plottable/Waterfall.cpp +++ b/render_2D/plottable/Waterfall.cpp @@ -21,7 +21,7 @@ using Waterfall_Interaction_State = Double_State_Strategy frequency, std::shared_ptr time) - : frequency_axis(std::move(frequency)), time_axis(std::move(time)), rows(observe_real_time_data(owner)) {} + : frequency_axis(std::move(frequency)), time_axis(std::move(time)), rows(owner) {} std::shared_ptr frequency_axis; std::shared_ptr time_axis; Waterfall_History rows; @@ -31,7 +31,8 @@ Waterfall_Control::Waterfall_Control(Plot_Core& plot, const Waterfall_Properties : Plottable_State(plot, properties), impl_(std::make_unique(*this, std::move(frequency_axis), std::move(time_axis))) {} Waterfall_Control::~Waterfall_Control() = default; void Waterfall_Control::append_row(int tick, std::span values) { - const std::size_t limit = static_cast(std::max(2, impl_->time_axis->visible_time_point_count())); + const std::size_t limit = static_cast( + std::max(2, impl_->time_axis->get<&Time_Axis_Properties::visible_count>())); if(get<&Waterfall_Properties::frequency_bin_count>() <= 0) set<&Waterfall_Properties::frequency_bin_count>(static_cast(values.size())); impl_->rows.update({tick, {values.begin(), values.end()}}, limit); @@ -64,7 +65,11 @@ std::size_t Waterfall_Control::rendered_cell_count() const { const int source_width = std::min(state.frequency_bin_count.get(), static_cast(std::min_element(rows.begin(), rows.end(), [](const auto& left, const auto& right) { return left.values.size() < right.values.size(); })->values.size())); if(source_width <= 0) return 0; - const auto columns = frequency_columns(state.frequency_range, impl_->frequency_axis->coord_range(), source_width, state.visible_range_only); + const auto columns = frequency_columns( + state.frequency_range, + impl_->frequency_axis->get<&Axis_Properties::coordinates>(), + source_width, + state.visible_range_only); return columns ? static_cast(columns->last - columns->first + 1) * rows.size() : 0; } void Waterfall_Control::handle_event(const Event& event) { @@ -77,17 +82,17 @@ void Waterfall_Control::publish() { publish_properties(); impl_->interaction.publish(); } -void Waterfall_Control::paint(Painter& painter) { - const auto state = render_properties(); - const auto rows = impl_->rows.snapshot(); - const auto interaction = impl_->interaction.render_use_state(); +void Waterfall_Control::paint(Painter& painter, const Render_State_View& view) { + const auto& state = render_properties(view); + const auto& rows = view.get(impl_->rows); + const auto& interaction = view.get(impl_->interaction); if(rows.empty()) return; const int source_width = std::min(state.frequency_bin_count.get(), static_cast(std::min_element(rows.begin(), rows.end(), [](const auto& left, const auto& right) { return left.values.size() < right.values.size(); })->values.size())); const int height = static_cast(rows.size()); if(source_width <= 0 || height <= 0) return; - const Axis_Transform horizontal = impl_->frequency_axis->transform(); + const Axis_Transform horizontal = impl_->frequency_axis->transform(view); const auto columns = frequency_columns(state.frequency_range, horizontal.coordinate_range, source_width, state.visible_range_only); if(!columns) return; @@ -98,11 +103,11 @@ void Waterfall_Control::paint(Painter& painter) { for(int x = 0; x < width; ++x) pixels[static_cast(y) * width + x] = state.color_map.at_normalized(normalized_value(row[static_cast(columns->first + x)], state.power_range)); } - const Axis_Transform vertical = impl_->time_axis->transform(); + const Axis_Transform vertical = impl_->time_axis->transform(view); const Range time_range{static_cast(rows.front().tick), static_cast(rows.back().tick)}; RectF target = mapped_rect(horizontal, vertical, columns->range, time_range); if(target.height < 1.0) - target.height = std::max(1.0, static_cast(impl_->time_axis->pixel_length())); + target.height = std::max(1.0, impl_->time_axis->transform(view).pixel_length); painter.heatmap(target, width, height, pixels, state.interpolation_mode); if(state.tooltip_enabled && interaction.tooltip.active && target.contains(interaction.tooltip.position)) { const double frequency = horizontal.pixel_to_coord(interaction.tooltip.position.x); diff --git a/render_2D/plottable/Waterfall.h b/render_2D/plottable/Waterfall.h index 809e664..4790a27 100644 --- a/render_2D/plottable/Waterfall.h +++ b/render_2D/plottable/Waterfall.h @@ -35,7 +35,7 @@ public: [[nodiscard]] std::size_t rendered_cell_count() const; void handle_event(const Event& event) override; protected: - void paint(Painter& painter) override; + void paint(Painter& painter, const Render_State_View& state) override; private: struct Impl; std::unique_ptr impl_; diff --git a/render_2D/renderable/Renderable.cpp b/render_2D/renderable/Renderable.cpp index 9973b2f..ecdf5c6 100644 --- a/render_2D/renderable/Renderable.cpp +++ b/render_2D/renderable/Renderable.cpp @@ -48,8 +48,10 @@ void Renderable::render(const ::Scene_Render_Context& context) { if (!cache) return; detail::Painter painter(*cache, viewport_size()); - if (painter) - paint(painter); + if (painter) { + const auto state = render_state_view(); + paint(painter, state); + } } void Renderable::changed() noexcept { diff --git a/render_2D/renderable/Renderable.h b/render_2D/renderable/Renderable.h index b6eaa15..8b38db1 100644 --- a/render_2D/renderable/Renderable.h +++ b/render_2D/renderable/Renderable.h @@ -14,6 +14,7 @@ namespace renderive { class Plot_Core; namespace detail { class Painter; +class Renderable_State_Observer; } class Event_Handler { @@ -39,15 +40,36 @@ public: void render(const ::Scene_Render_Context& context) final; protected: - virtual void paint(detail::Painter& painter) = 0; + virtual void paint(detail::Painter& painter, const Render_State_View& state) = 0; void changed() noexcept; [[nodiscard]] Size viewport_size() const noexcept; private: + friend class detail::Renderable_State_Observer; Plot_Core& plot_; mutable std::mutex metadata_mutex_; std::string object_name_; std::atomic visible_{true}; }; +namespace detail { + +class Renderable_State_Observer { +public: + static constexpr bool enabled = true; + + explicit Renderable_State_Observer(Renderable* renderable) noexcept : renderable_(renderable) {} + + template + void observe(const Observation& observation) noexcept { + if (observation.event == decltype(observation.event)::cache_updated) + renderable_->changed(); + } + +private: + Renderable* renderable_; +}; + +} // namespace detail + } // namespace renderive diff --git a/render_2D/tests/render_2D_Integration_Tests.cpp b/render_2D/tests/render_2D_Integration_Tests.cpp index ea537bc..c9f3bdc 100644 --- a/render_2D/tests/render_2D_Integration_Tests.cpp +++ b/render_2D/tests/render_2D_Integration_Tests.cpp @@ -9,6 +9,26 @@ #include namespace renderive { namespace { +template +concept Legacy_Axis_Property_Api = requires(Axis_Type& axis) { + axis.x(); + axis.set_x(0); + axis.coord_range(); + axis.set_coord_range(Range{}); +}; + +template +concept Legacy_Time_Axis_Property_Api = requires(Axis_Type& axis) { + axis.visible_time_point_count(); + axis.set_visible_time_point_count(1); + axis.time_format(); + axis.set_time_format({}); +}; + +static_assert(!Legacy_Axis_Property_Api); +static_assert(!Legacy_Axis_Property_Api); +static_assert(!Legacy_Time_Axis_Property_Api); + TEST(Renderive_Core2, RootIdentityAndRefreshDiagnosticsComeFromKernelState) { Plot_Core plot; plot.init(); @@ -104,10 +124,10 @@ TEST(Renderive_Core2, KernelSceneRendersBusinessObjectsIntoBlend2DFrame) { .set_pixel_length(270) .set_coord_range({88.0, 108.0}) .build(); - frequency_axis->set_locale({','}); - frequency_axis->set_label_rotation_degrees(15); + frequency_axis->set<&Axis_Base_Properties::locale>(Number_Locale{','}); + frequency_axis->set<&Axis_Base_Properties::label_rotation_degrees>(15); EXPECT_EQ(frequency_axis->tick_label(88.5), "88,5 Hz"); - frequency_axis->set_label_precision(4); + frequency_axis->set<&Axis_Properties::precision>(4); EXPECT_EQ(frequency_axis->tick_label(1'234'567.0), "1,2346 MHz"); auto power_axis = Axis::Builder(root, Orientation::Vertical) .set_x(32) @@ -147,14 +167,14 @@ TEST(Renderive_Core2, KernelSceneRendersBusinessObjectsIntoBlend2DFrame) { EXPECT_FALSE(plot.render_frame()); spectrum->update_samples(samples); EXPECT_TRUE(plot.render_frame()); - frequency_axis->set_use_wheel(true); - const Range before_zoom = frequency_axis->coord_range(); + frequency_axis->set<&Axis_Properties::wheel>(true); + const Range before_zoom = frequency_axis->get<&Axis_Properties::coordinates>(); Wheel_Event wheel; wheel.position = {160.0, 150.0}; wheel.angle_delta_y = 120.0; plot.dispatch_event(wheel); EXPECT_TRUE(wheel.is_accepted()); - EXPECT_LT(frequency_axis->coord_range().size(), before_zoom.size()); + EXPECT_LT(frequency_axis->get<&Axis_Properties::coordinates>().size(), before_zoom.size()); EXPECT_TRUE(plot.render_frame()); plot.remove_renderable(spectrum); EXPECT_TRUE(plot.render_frame(true)); @@ -172,9 +192,8 @@ TEST(Renderive_Core2, TimeAxisUsesOneFontStateAndFormatsConfiguredLabels) { ASSERT_TRUE(axis); const int tick = axis->append_time(Time_Of_Day{3'723'045}); EXPECT_EQ(axis->tick_label(tick), "01-02-03.045"); - EXPECT_EQ(axis->font(), (Font{18.0, 700, true})); - EXPECT_EQ(axis->unit_text_font(), axis->font()); - EXPECT_EQ(axis->tick_label_spacing_px(), 17); + EXPECT_EQ(axis->get<&Axis_Base_Properties::unit_text_font>(), (Font{18.0, 700, true})); + EXPECT_EQ(axis->get<&Time_Axis_Properties::tick_label_spacing_px>(), 17); } TEST(Renderive_Core2, PerformanceOverlayConsumesKernelFrameDiagnostics) { Plot_Core plot; @@ -260,32 +279,35 @@ TEST(Renderive_Core2, RetainedAxisAndTimeAxisApisRoundTripWithoutWebAdapters) { .set_use_drag(true) .build(); ASSERT_TRUE(axis); - EXPECT_EQ(axis->x(), 31); - EXPECT_EQ(axis->y(), 17); - EXPECT_EQ(axis->orientation(), Orientation::Vertical); - EXPECT_EQ(axis->pixel_length(), 240U); - EXPECT_EQ(axis->tick_length(), 13); - EXPECT_EQ(axis->sub_tick_length(), 7); - EXPECT_EQ(axis->color(), (Color{10, 20, 30, 255})); - EXPECT_EQ(axis->unit_text(), "dB"); - EXPECT_EQ(axis->unit_text_font(), (Font{15.0, 650, true})); - EXPECT_EQ(axis->unit_text_pen(), (Pen{Color{40, 50, 60, 255}, 2.0})); - EXPECT_EQ(axis->unit_text_background_brush(), + EXPECT_EQ(axis->get<&Axis_Base_Properties::x>(), 31); + EXPECT_EQ(axis->get<&Axis_Base_Properties::y>(), 17); + EXPECT_EQ(axis->get<&Axis_Base_Properties::orientation>(), Orientation::Vertical); + EXPECT_EQ(axis->get<&Axis_Base_Properties::pixel_length>(), 240U); + EXPECT_EQ(axis->get<&Axis_Base_Properties::tick_length>(), 13); + EXPECT_EQ(axis->get<&Axis_Base_Properties::sub_tick_length>(), 7); + EXPECT_EQ(axis->get<&Axis_Base_Properties::color>(), (Color{10, 20, 30, 255})); + EXPECT_EQ(axis->get<&Axis_Base_Properties::unit_text>(), "dB"); + EXPECT_EQ(axis->get<&Axis_Base_Properties::unit_text_font>(), (Font{15.0, 650, true})); + EXPECT_EQ(axis->get<&Axis_Base_Properties::unit_text_pen>(), (Pen{Color{40, 50, 60, 255}, 2.0})); + EXPECT_EQ(axis->get<&Axis_Base_Properties::unit_text_background_brush>(), (Brush{Color{3, 4, 5, 255}, Brush_Style::Solid})); - EXPECT_EQ(axis->label_rotation_degrees(), 27); - EXPECT_EQ(axis->coord_range(), (Range{-120.0, -20.0})); - EXPECT_EQ(axis->label_precision(), 4); - EXPECT_TRUE(axis->use_wheel()); - EXPECT_TRUE(axis->use_drag()); - axis->set_locale({','}); - axis->set_coord_start(-100.0); - axis->set_coord_length(80.0); - EXPECT_EQ(axis->locale(), (Number_Locale{','})); - EXPECT_EQ(axis->coord_range(), (Range{-100.0, -20.0})); - EXPECT_DOUBLE_EQ(axis->pixel_to_coord(axis->coord_to_pixel(-60.0)), -60.0); - EXPECT_GT(axis->pixel_sample_count(), 0); - EXPECT_GT(axis->tick_step(axis->coord_range()), 0.0); - EXPECT_GE(axis->sub_tick_count(axis->tick_step(axis->coord_range())), 0); + EXPECT_EQ(axis->get<&Axis_Base_Properties::label_rotation_degrees>(), 27); + EXPECT_EQ(axis->get<&Axis_Properties::coordinates>(), (Range{-120.0, -20.0})); + EXPECT_EQ(axis->get<&Axis_Properties::precision>(), 4); + EXPECT_TRUE(axis->get<&Axis_Properties::wheel>()); + EXPECT_TRUE(axis->get<&Axis_Properties::drag>()); + EXPECT_THROW(axis->set<&Axis_Properties::precision>(13), std::out_of_range); + EXPECT_THROW(axis->set<&Axis_Properties::coordinates>(Range{1.0, 1.0}), + std::invalid_argument); + axis->set<&Axis_Base_Properties::locale>(Number_Locale{','}); + axis->set<&Axis_Properties::coordinates>(Range{-100.0, -20.0}); + EXPECT_EQ(axis->get<&Axis_Base_Properties::locale>(), (Number_Locale{','})); + EXPECT_EQ(axis->get<&Axis_Properties::coordinates>(), (Range{-100.0, -20.0})); + const Axis_Transform numeric_transform = axis->transform(); + EXPECT_DOUBLE_EQ(numeric_transform.pixel_to_coord(numeric_transform.coord_to_pixel(-60.0)), -60.0); + EXPECT_GT(static_cast(numeric_transform.pixel_length) + 1, 0); + EXPECT_GT(axis->tick_step(numeric_transform.coordinate_range), 0.0); + EXPECT_GE(axis->sub_tick_count(axis->tick_step(numeric_transform.coordinate_range)), 0); const auto time_axis = Time_Axis::Builder(root, Orientation::Horizontal) .set_x(12) .set_y(280) @@ -299,11 +321,12 @@ TEST(Renderive_Core2, RetainedAxisAndTimeAxisApisRoundTripWithoutWebAdapters) { ASSERT_TRUE(time_axis); const int first = time_axis->append_time({3'723'004}); const int second = time_axis->append_time({3'724'005}); - EXPECT_EQ(time_axis->visible_time_point_count(), 32); - EXPECT_EQ(time_axis->tick_label_spacing_px(), 19); - EXPECT_EQ(time_axis->time_format(), "hh:mm:ss.zzz"); - EXPECT_EQ(time_axis->font(), (Font{16.0, 700, true})); - EXPECT_TRUE(time_axis->newest_at_axis_start()); + EXPECT_EQ(time_axis->get<&Time_Axis_Properties::visible_count>(), 32); + EXPECT_EQ(time_axis->get<&Time_Axis_Properties::tick_label_spacing_px>(), 19); + EXPECT_EQ(time_axis->get<&Time_Axis_Properties::format>(), "hh:mm:ss.zzz"); + EXPECT_EQ(time_axis->get<&Axis_Base_Properties::unit_text_font>(), (Font{16.0, 700, true})); + EXPECT_TRUE(time_axis->get<&Time_Axis_Properties::newest_at_start>()); + EXPECT_THROW(time_axis->set<&Time_Axis_Properties::visible_count>(1), std::out_of_range); EXPECT_EQ(time_axis->time_point_count(), 2U); EXPECT_EQ(time_axis->tick_to_time(first), (Time_Of_Day{3'723'004})); EXPECT_EQ(time_axis->tick_to_time(second), (Time_Of_Day{3'724'005})); @@ -542,8 +565,8 @@ TEST(Renderive_Core2, RetainedSelectionAndConstellationApisDriveInteractionAndLa constellation->append_point({-0.5, 0.5}); EXPECT_EQ(constellation->point_count(), 2U); constellation->fit_square_to_axes(); - EXPECT_EQ(horizontal->coord_range(), (Range{-3.0, 3.0})); - EXPECT_EQ(vertical->coord_range(), (Range{3.0, -3.0})); + EXPECT_EQ(horizontal->get<&Axis_Properties::coordinates>(), (Range{-3.0, 3.0})); + EXPECT_EQ(vertical->get<&Axis_Properties::coordinates>(), (Range{3.0, -3.0})); EXPECT_TRUE(plot.render_frame(true)); } TEST(Renderive_Core2, PlottablePropertiesPublishOnlyAtFrameBoundary) { diff --git a/web_server/app/Gallery_Plot_Session.cpp b/web_server/app/Gallery_Plot_Session.cpp index 7b260ac..856d101 100644 --- a/web_server/app/Gallery_Plot_Session.cpp +++ b/web_server/app/Gallery_Plot_Session.cpp @@ -23,6 +23,19 @@ #include namespace renderive::web { namespace { +template +void update_axis_state(const std::shared_ptr& axis, Update&& update) { + if (const auto numeric = std::dynamic_pointer_cast(axis)) { + numeric->update([&](Axis_Properties& state) { + std::forward(update)(static_cast(state)); + }); + } else if (const auto time = std::dynamic_pointer_cast(axis)) { + time->update([&](auto& state) { + std::forward(update)(static_cast(state)); + }); + } +} + double number_value(const Gallery_State& state, std::string_view id) { return std::get(gallery_property_value(state, id)); } @@ -379,15 +392,23 @@ public: return plot_.view_active() ? "Plot view 已激活" : "Plot view 已停用;可再次执行恢复"; } if (request.id == "axis_set_start") { - if (numeric_domain_axis_) - numeric_domain_axis_->set_coord_start(number_value(state_, "coord_origin")); - return "已通过 Axis::set_coord_start 应用当前起点"; + if (numeric_domain_axis_) { + const Range range = numeric_domain_axis_->get<&Axis_Properties::coordinates>(); + const double origin = number_value(state_, "coord_origin"); + numeric_domain_axis_->set<&Axis_Properties::coordinates>( + Range{origin, origin + range.length()}); + } + return "轴起点已写入 Axis_Properties 状态"; } if (request.id == "axis_set_length") { - if (numeric_domain_axis_) - numeric_domain_axis_->set_coord_length(number_value(state_, "coord_target") - - number_value(state_, "coord_origin")); - return "已通过 Axis::set_coord_length 应用当前跨度"; + if (numeric_domain_axis_) { + const Range range = numeric_domain_axis_->get<&Axis_Properties::coordinates>(); + const double length = number_value(state_, "coord_target") - + number_value(state_, "coord_origin"); + numeric_domain_axis_->set<&Axis_Properties::coordinates>( + Range{range.origin, range.origin + length}); + } + return "轴跨度已写入 Axis_Properties 状态"; } if (request.id == "axis_probe") return "坐标映射、刻度步长、次刻度数与标签结果已刷新到遥测区"; @@ -700,18 +721,20 @@ public: telemetry["performance_overlay_lines"] = snapshot->lines.size(); } if (numeric_domain_axis_) { - const Range range = numeric_domain_axis_->coord_range(); + const Axis_Transform transform = numeric_domain_axis_->transform(); + const Range range = transform.coordinate_range; const double center = range.center(); telemetry["axis"] = { {"origin", range.origin}, {"target", range.target}, - {"start_coord", numeric_domain_axis_->start_coord()}, - {"end_coord", numeric_domain_axis_->end_coord()}, - {"center_pixel", numeric_domain_axis_->coord_to_pixel(center)}, + {"start_coord", range.origin}, + {"end_coord", range.target}, + {"center_pixel", transform.coord_to_pixel(center)}, { - "center_roundtrip", numeric_domain_axis_->pixel_to_coord( - numeric_domain_axis_->coord_to_pixel(center)) + "center_roundtrip", transform.pixel_to_coord( + transform.coord_to_pixel(center)) }, - {"pixel_samples", numeric_domain_axis_->pixel_sample_count()}, + {"pixel_samples", static_cast(transform.pixel_length) + + (transform.pixel_length > 0.0 ? 1 : 0)}, {"tick_step", numeric_domain_axis_->tick_step(range)}, { "sub_tick_count", numeric_domain_axis_->sub_tick_count( @@ -784,7 +807,9 @@ public: } if (case_id_ == "axis_lab") { telemetry["axis_lab"] = { - {"domain_axis_pixel_samples", numeric_domain_axis_ ? numeric_domain_axis_->pixel_sample_count() : 0}, + {"domain_axis_pixel_samples", numeric_domain_axis_ + ? static_cast(numeric_domain_axis_->transform().pixel_length) + 1 + : 0}, {"time_axis_point_count", time_axis_ ? time_axis_->time_point_count() : 0} }; } @@ -830,7 +855,9 @@ public: else telemetry["data_shape"] = { {"input_points", 0}, - {"rendered_elements", numeric_domain_axis_ ? numeric_domain_axis_->pixel_sample_count() : 0}, + {"rendered_elements", numeric_domain_axis_ + ? static_cast(numeric_domain_axis_->transform().pixel_length) + 1 + : 0}, {"unit", "axis samples"} }; return telemetry.dump(); @@ -1231,66 +1258,84 @@ private: const int y_offset = integer_value(state_, "axis_y_offset"); const double length_rate = number_value(state_, "axis_length_percent") / 100.0; if (const auto horizontal = horizontal_axis()) { - horizontal->set_x(left + x_offset); - horizontal->set_y(top + height + y_offset); - horizontal->set_pixel_length(static_cast( - std::max(1, static_cast(std::lround(width * length_rate))))); + update_axis_state(horizontal, [&](Axis_Base_Properties& axis) { + axis.x = left + x_offset; + axis.y = top + height + y_offset; + axis.pixel_length = static_cast( + std::max(1, static_cast(std::lround(width * length_rate)))); + }); } if (const auto vertical = vertical_axis()) { - vertical->set_x(left + x_offset); - vertical->set_y(top + y_offset); - vertical->set_pixel_length(static_cast( - std::max(1, static_cast(std::lround(height * length_rate))))); + update_axis_state(vertical, [&](Axis_Base_Properties& axis) { + axis.x = left + x_offset; + axis.y = top + y_offset; + axis.pixel_length = static_cast( + std::max(1, static_cast(std::lround(height * length_rate)))); + }); } if (case_id_ == "axis_lab" && time_axis_) { - time_axis_->set_x(left); - time_axis_->set_y(26); - time_axis_->set_pixel_length(static_cast(width)); + time_axis_->update([&](auto& axis) { + axis.x = left; + axis.y = 26; + axis.pixel_length = static_cast(width); + }); } } void apply_axis_style(const std::shared_ptr& axis) { if (!axis) return; - axis->set_tick_length(integer_value(state_, "tick_length")); - axis->set_sub_tick_length(integer_value(state_, "sub_tick_length")); - axis->set_color(color_from_hex(string_value(state_, "axis_color"))); - axis->set_locale({string_value(state_, "decimal_separator").front()}); - axis->set_unit_text(string_value(state_, "unit_text")); - axis->set_unit_text_font({ - number_value(state_, "unit_font_size"), - integer_value(state_, "unit_font_weight"), - bool_value(state_, "unit_font_italic") + update_axis_state(axis, [&](Axis_Base_Properties& value) { + value.tick_length = integer_value(state_, "tick_length"); + value.sub_tick_length = integer_value(state_, "sub_tick_length"); + value.color = color_from_hex(string_value(state_, "axis_color")); + value.locale = {string_value(state_, "decimal_separator").front()}; + value.unit_text = string_value(state_, "unit_text"); + value.unit_text_font = { + number_value(state_, "unit_font_size"), + integer_value(state_, "unit_font_weight"), + bool_value(state_, "unit_font_italic") + }; + value.unit_text_pen = {color_from_hex(string_value(state_, "unit_pen_color"))}; + value.unit_text_background_brush = { + color_from_hex(string_value(state_, "unit_background_color")), + Brush_Style::Solid + }; + value.label_rotation_degrees = integer_value(state_, "label_rotation"); }); - axis->set_unit_text_pen({color_from_hex(string_value(state_, "unit_pen_color"))}); - axis->set_unit_text_background_brush( - {color_from_hex(string_value(state_, "unit_background_color")), Brush_Style::Solid}); - axis->set_label_rotation_degrees(integer_value(state_, "label_rotation")); } void apply_axes() { const bool horizontal = string_value(state_, "axis_orientation") == "Horizontal"; if (const auto main = horizontal_axis()) - main->set_orientation(horizontal ? Orientation::Horizontal : Orientation::Vertical); + update_axis_state(main, [horizontal](Axis_Base_Properties& axis) { + axis.orientation = horizontal ? Orientation::Horizontal : Orientation::Vertical; + }); if (const auto other = vertical_axis()) - other->set_orientation(horizontal ? Orientation::Vertical : Orientation::Horizontal); + update_axis_state(other, [horizontal](Axis_Base_Properties& axis) { + axis.orientation = horizontal ? Orientation::Vertical : Orientation::Horizontal; + }); apply_axis_style(horizontal_axis()); apply_axis_style(vertical_axis()); if (case_id_ == "axis_lab") apply_axis_style(time_axis_); if (numeric_domain_axis_) { - numeric_domain_axis_->set_coord_range({ - number_value(state_, "coord_origin"), - number_value(state_, "coord_target") + numeric_domain_axis_->update([&](Axis_Properties& axis) { + axis.coordinates = { + number_value(state_, "coord_origin"), + number_value(state_, "coord_target") + }; + axis.precision = std::clamp(integer_value(state_, "label_precision"), 0, 12); + axis.wheel = bool_value(state_, "use_wheel"); + axis.drag = bool_value(state_, "use_drag"); }); - numeric_domain_axis_->set_label_precision(integer_value(state_, "label_precision")); - numeric_domain_axis_->set_use_wheel(bool_value(state_, "use_wheel")); - numeric_domain_axis_->set_use_drag(bool_value(state_, "use_drag")); } if (time_axis_) { - time_axis_->set_visible_time_point_count(integer_value(state_, "time_visible_count")); - time_axis_->set_tick_label_spacing_px(integer_value(state_, "time_label_spacing")); - time_axis_->set_time_format(string_value(state_, "time_format")); - time_axis_->set_font({number_value(state_, "time_font_size"), 500, false}); - time_axis_->set_newest_at_axis_start(bool_value(state_, "time_newest_at_start")); + time_axis_->update([&](auto& axis) { + axis.visible_count = std::max(2, integer_value(state_, "time_visible_count")); + axis.tick_label_spacing_px = std::max(0, integer_value(state_, "time_label_spacing")); + axis.format = string_value(state_, "time_format"); + axis.unit_text_font = {number_value(state_, "time_font_size"), 500, false}; + axis.newest_at_start = bool_value(state_, "time_newest_at_start"); + }); } apply_layout(); } @@ -1415,7 +1460,7 @@ private: sweep_->set<&Sweep_Spectrum::Properties::interpolation_mode>(line_mode(string_value(state_, "line_interpolation"))); } if (trace_ && value_axis_) - value_axis_->set_coord_range({ + value_axis_->set<&Axis_Properties::coordinates>(Range{ number_value(state_, "trace_value_min"), number_value(state_, "trace_value_max") }); @@ -1426,12 +1471,12 @@ private: constellation_->set<&Constellation_Diagram::Properties::anchor_color>(color_from_hex(string_value(state_, "anchor_color"))); constellation_->set<&Constellation_Diagram::Properties::point_lifetime_ms>(integer_value(state_, "point_lifetime_ms")); if (numeric_domain_axis_) - numeric_domain_axis_->set_coord_range({ + numeric_domain_axis_->set<&Axis_Properties::coordinates>(Range{ number_value(state_, "i_origin"), number_value(state_, "i_target") }); if (value_axis_) - value_axis_->set_coord_range({ + value_axis_->set<&Axis_Properties::coordinates>(Range{ number_value(state_, "q_origin"), number_value(state_, "q_target") }); diff --git a/web_server/app/Gallery_Properties_Adminive.h b/web_server/app/Gallery_Properties_Adminive.h index 0d04278..3946e5f 100644 --- a/web_server/app/Gallery_Properties_Adminive.h +++ b/web_server/app/Gallery_Properties_Adminive.h @@ -137,36 +137,36 @@ inline auto low_latency_fields() { inline auto axis_fields() { using T = Gallery_Axis_Properties; return std::tuple{ - select_property<&T::axis_orientation>("axis_orientation", "主轴方向", "Abs_Axis::set_orientation"), - number_property<&T::axis_x_offset>("axis_x_offset", "X 偏移", "Abs_Axis::set_x"), - number_property<&T::axis_y_offset>("axis_y_offset", "Y 偏移", "Abs_Axis::set_y"), - number_property<&T::axis_length_percent>("axis_length_percent", "轴长百分比", "Abs_Axis::set_pixel_length"), - number_property<&T::tick_length>("tick_length", "主刻度长度", "Abs_Axis::set_tick_length"), - number_property<&T::sub_tick_length>("sub_tick_length", "次刻度长度", "Abs_Axis::set_sub_tick_length"), - color_property<&T::axis_color>("axis_color", "轴颜色", "Abs_Axis::set_color"), - select_property<&T::decimal_separator>("decimal_separator", "小数点", "Abs_Axis::set_locale"), - text_property<&T::unit_text>("unit_text", "单位文本", "Abs_Axis::set_unit_text"), - number_property<&T::unit_font_size>("unit_font_size", "单位字号", "Abs_Axis::set_unit_text_font"), - number_property<&T::unit_font_weight>("unit_font_weight", "单位字重", "Abs_Axis::set_unit_text_font"), - boolean_property<&T::unit_font_italic>("unit_font_italic", "单位斜体", "Abs_Axis::set_unit_text_font"), - color_property<&T::unit_pen_color>("unit_pen_color", "单位文字颜色", "Abs_Axis::set_unit_text_pen"), - color_property<&T::unit_background_color>("unit_background_color", "单位背景颜色", "Abs_Axis::set_unit_text_background_brush"), - number_property<&T::label_rotation>("label_rotation", "标签旋转角度", "Abs_Axis::set_label_rotation_degrees"), - number_property<&T::coord_origin>("coord_origin", "坐标起点", "Axis::set_coord_range"), - number_property<&T::coord_target>("coord_target", "坐标终点", "Axis::set_coord_range"), - number_property<&T::label_precision>("label_precision", "标签精度", "Axis::set_label_precision"), - boolean_property<&T::use_wheel>("use_wheel", "启用滚轮缩放", "Axis::set_use_wheel"), - boolean_property<&T::use_drag>("use_drag", "启用拖拽平移", "Axis::set_use_drag") + select_property<&T::axis_orientation>("axis_orientation", "主轴方向", "Axis::set<&Axis_Base_Properties::orientation>"), + number_property<&T::axis_x_offset>("axis_x_offset", "X 偏移", "Axis::set<&Axis_Base_Properties::x>"), + number_property<&T::axis_y_offset>("axis_y_offset", "Y 偏移", "Axis::set<&Axis_Base_Properties::y>"), + number_property<&T::axis_length_percent>("axis_length_percent", "轴长百分比", "Axis::set<&Axis_Base_Properties::pixel_length>"), + number_property<&T::tick_length>("tick_length", "主刻度长度", "Axis::set<&Axis_Base_Properties::tick_length>"), + number_property<&T::sub_tick_length>("sub_tick_length", "次刻度长度", "Axis::set<&Axis_Base_Properties::sub_tick_length>"), + color_property<&T::axis_color>("axis_color", "轴颜色", "Axis::set<&Axis_Base_Properties::color>"), + select_property<&T::decimal_separator>("decimal_separator", "小数点", "Axis::set<&Axis_Base_Properties::locale>"), + text_property<&T::unit_text>("unit_text", "单位文本", "Axis::set<&Axis_Base_Properties::unit_text>"), + number_property<&T::unit_font_size>("unit_font_size", "单位字号", "Axis::set<&Axis_Base_Properties::unit_text_font>"), + number_property<&T::unit_font_weight>("unit_font_weight", "单位字重", "Axis::set<&Axis_Base_Properties::unit_text_font>"), + boolean_property<&T::unit_font_italic>("unit_font_italic", "单位斜体", "Axis::set<&Axis_Base_Properties::unit_text_font>"), + color_property<&T::unit_pen_color>("unit_pen_color", "单位文字颜色", "Axis::set<&Axis_Base_Properties::unit_text_pen>"), + color_property<&T::unit_background_color>("unit_background_color", "单位背景颜色", "Axis::set<&Axis_Base_Properties::unit_text_background_brush>"), + number_property<&T::label_rotation>("label_rotation", "标签旋转角度", "Axis::set<&Axis_Base_Properties::label_rotation_degrees>"), + number_property<&T::coord_origin>("coord_origin", "坐标起点", "Axis::set<&Axis_Properties::coordinates>"), + number_property<&T::coord_target>("coord_target", "坐标终点", "Axis::set<&Axis_Properties::coordinates>"), + number_property<&T::label_precision>("label_precision", "标签精度", "Axis::set<&Axis_Properties::precision>"), + boolean_property<&T::use_wheel>("use_wheel", "启用滚轮缩放", "Axis::set<&Axis_Properties::wheel>"), + boolean_property<&T::use_drag>("use_drag", "启用拖拽平移", "Axis::set<&Axis_Properties::drag>") }; } inline auto time_axis_fields() { using T = Gallery_Time_Axis_Properties; return std::tuple{ - number_property<&T::time_visible_count>("time_visible_count", "可见时间点", "Time_Axis::set_visible_time_point_count"), - number_property<&T::time_label_spacing>("time_label_spacing", "时间标签间距", "Time_Axis::set_tick_label_spacing_px"), - text_property<&T::time_format>("time_format", "时间格式", "Time_Axis::set_time_format"), - number_property<&T::time_font_size>("time_font_size", "时间字体", "Time_Axis::set_font"), - boolean_property<&T::time_newest_at_start>("time_newest_at_start", "最新数据位于轴起点", "Time_Axis::set_newest_at_axis_start") + number_property<&T::time_visible_count>("time_visible_count", "可见时间点", "Time_Axis::set<&Time_Axis_Properties::visible_count>"), + number_property<&T::time_label_spacing>("time_label_spacing", "时间标签间距", "Time_Axis::set<&Time_Axis_Properties::tick_label_spacing_px>"), + text_property<&T::time_format>("time_format", "时间格式", "Time_Axis::set<&Time_Axis_Properties::format>"), + number_property<&T::time_font_size>("time_font_size", "时间字体", "Time_Axis::set<&Axis_Base_Properties::unit_text_font>"), + boolean_property<&T::time_newest_at_start>("time_newest_at_start", "最新数据位于轴起点", "Time_Axis::set<&Time_Axis_Properties::newest_at_start>") }; } inline auto hover_fields() { @@ -288,8 +288,8 @@ struct Property_Fields { return std::tuple_cat(common_fields(), axis_fields(), time_axis_fields(), std::tuple{ color_property<&T::trace_pen>("trace_pen", "轨迹颜色", "Frequency_Trace::set<&Properties::pen>"), number_property<&T::trace_pen_width>("trace_pen_width", "轨迹宽度", "Frequency_Trace::set<&Properties::pen>"), - number_property<&T::trace_value_min>("trace_value_min", "数值下限", "Axis::set_coord_range"), - number_property<&T::trace_value_max>("trace_value_max", "数值上限", "Axis::set_coord_range") + number_property<&T::trace_value_min>("trace_value_min", "数值下限", "Axis::set<&Axis_Properties::coordinates>"), + number_property<&T::trace_value_max>("trace_value_max", "数值上限", "Axis::set<&Axis_Properties::coordinates>") }); } }; diff --git a/web_server/app/Gallery_Protocol.cpp b/web_server/app/Gallery_Protocol.cpp index 098ac3c..d5dbaf8 100644 --- a/web_server/app/Gallery_Protocol.cpp +++ b/web_server/app/Gallery_Protocol.cpp @@ -90,8 +90,8 @@ std::vector actions(std::string_view case_id, action("reset", "恢复本图默认值", "Gallery_Scene::rebuild", "会话"), action("rebuild_renderable", "移除并重建控件", "Plot_Core::remove_renderable / Builder::build", "会话"), action("toggle_view", "切换 View 生命周期", "Plot_Core::activate_view / deactivate_view", "会话"), - action("axis_set_start", "通过 set_coord_start 应用起点", "Axis::set_coord_start", "Axis"), - action("axis_set_length", "通过 set_coord_length 应用跨度", "Axis::set_coord_length", "Axis"), + action("axis_set_start", "写入坐标范围起点", "Axis::set<&Axis_Properties::coordinates>", "Axis"), + action("axis_set_length", "写入坐标范围跨度", "Axis::set<&Axis_Properties::coordinates>", "Axis"), action("axis_probe", "读取坐标映射与刻度", "coord_to_pixel / pixel_to_coord / tick_step / sub_tick_count / tick_label", "Axis") }; if (frame_mode == Gallery_Frame_Mode::Manual) { @@ -126,7 +126,7 @@ std::vector actions(std::string_view case_id, } if (case_id == "axis_lab") { result.push_back(action("append_time", "追加时间点", "Time_Axis::append_time / tick_to_time", "Time_Axis")); - result.push_back(action("read_data_shape", "读取轴采样点状态", "Abs_Axis::pixel_sample_count / Time_Axis::time_point_count", "观察者")); + result.push_back(action("read_data_shape", "读取轴采样点状态", "Abs_Axis::transform / Time_Axis::time_point_count", "观察者")); } else if (case_id == "spectrum" || case_id == "selection_overlay") { result.push_back(action("push_samples", "推送一帧样本", "Spectrum::update_samples", "Spectrum")); result.push_back(action("power_at", "查询指定频率功率", "Spectrum::power_at", "Spectrum", {}, "number", "频率", 98e6)); diff --git a/web_server/app/Web_Plot_Session.cpp b/web_server/app/Web_Plot_Session.cpp index 697a8b6..00a7dc0 100644 --- a/web_server/app/Web_Plot_Session.cpp +++ b/web_server/app/Web_Plot_Session.cpp @@ -160,18 +160,26 @@ struct Web_Plot_Session::Impl { const int spectrum_height = std::max(1, available_height * 43 / 100); const int waterfall_y = top + spectrum_height + gap; const int waterfall_height = std::max(1, viewport.height - waterfall_y - bottom); - spectrum_frequency_axis->set_x(left); - spectrum_frequency_axis->set_y(top + spectrum_height); - spectrum_frequency_axis->set_pixel_length(static_cast(content_width)); - spectrum_power_axis->set_x(left); - spectrum_power_axis->set_y(top); - spectrum_power_axis->set_pixel_length(static_cast(spectrum_height)); - waterfall_frequency_axis->set_x(left); - waterfall_frequency_axis->set_y(waterfall_y + waterfall_height); - waterfall_frequency_axis->set_pixel_length(static_cast(content_width)); - waterfall_time_axis->set_x(left); - waterfall_time_axis->set_y(waterfall_y); - waterfall_time_axis->set_pixel_length(static_cast(waterfall_height)); + spectrum_frequency_axis->update([&](Axis_Properties& state) { + state.x = left; + state.y = top + spectrum_height; + state.pixel_length = static_cast(content_width); + }); + spectrum_power_axis->update([&](Axis_Properties& state) { + state.x = left; + state.y = top; + state.pixel_length = static_cast(spectrum_height); + }); + waterfall_frequency_axis->update([&](Axis_Properties& state) { + state.x = left; + state.y = waterfall_y + waterfall_height; + state.pixel_length = static_cast(content_width); + }); + waterfall_time_axis->update([&](auto& state) { + state.x = left; + state.y = waterfall_y; + state.pixel_length = static_cast(waterfall_height); + }); } void update_model() { constexpr int sample_count = 768; @@ -214,10 +222,10 @@ struct Web_Plot_Session::Impl { } void set_center_frequency(double megahertz) { const double center = megahertz * 1'000'000.0; - const double bandwidth = spectrum_frequency_axis->coord_range().size(); + const double bandwidth = spectrum_frequency_axis->get<&Axis_Properties::coordinates>().size(); const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5}; - spectrum_frequency_axis->set_coord_range(range); - waterfall_frequency_axis->set_coord_range(range); + spectrum_frequency_axis->set<&Axis_Properties::coordinates>(range); + waterfall_frequency_axis->set<&Axis_Properties::coordinates>(range); spectrum->set<&Spectrum::Properties::frequency_range>(range); spectrum->set<&Spectrum::Properties::center_frequency>(center); spectrum->set<&Spectrum::Properties::sweep_frequency_range>(Range{ @@ -230,8 +238,8 @@ struct Web_Plot_Session::Impl { const double center = spectrum->get<&Spectrum::Properties::center_frequency>(); const double bandwidth = kilohertz * 1000.0; const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5}; - spectrum_frequency_axis->set_coord_range(range); - waterfall_frequency_axis->set_coord_range(range); + spectrum_frequency_axis->set<&Axis_Properties::coordinates>(range); + waterfall_frequency_axis->set<&Axis_Properties::coordinates>(range); spectrum->set<&Spectrum::Properties::frequency_range>(range); spectrum->set<&Spectrum::Properties::sweep_frequency_range>(Range{ center - bandwidth * 0.125, diff --git a/web_server/tests/Web_Bridge_Tests.cpp b/web_server/tests/Web_Bridge_Tests.cpp index 2529b78..66557ff 100644 --- a/web_server/tests/Web_Bridge_Tests.cpp +++ b/web_server/tests/Web_Bridge_Tests.cpp @@ -269,10 +269,10 @@ TEST(RenderiveWebGallery, BackendMenuMapsRetainedControlApis) { "Plot_Core::set_background_color", "Plot_Core::set_max_render_fps", "Plot_Core::activate_view", "Plot_Core::remove_renderable", "Renderable::set_visible", "Renderable::set_cache_mode", "Renderable::set_object_name", - "Abs_Axis::set_x", "Abs_Axis::set_y", "Abs_Axis::set_orientation", - "Abs_Axis::set_pixel_length", "Abs_Axis::set_locale", "Abs_Axis::set_unit_text_font", - "Axis::set_coord_range", "Axis::set_coord_start", "Axis::set_coord_length", - "Axis::set_use_wheel", "Axis::set_use_drag", "Time_Axis::set_time_format", + "Axis::set<&Axis_Base_Properties::x>", "Axis::set<&Axis_Base_Properties::y>", "Axis::set<&Axis_Base_Properties::orientation>", + "Axis::set<&Axis_Base_Properties::pixel_length>", "Axis::set<&Axis_Base_Properties::locale>", "Axis::set<&Axis_Base_Properties::unit_text_font>", + "Axis::set<&Axis_Properties::coordinates>", "Axis::set<&Axis_Properties::precision>", "Axis::set<&Axis_Properties::wheel>", + "Axis::set<&Axis_Properties::drag>", "Time_Axis::set<&Time_Axis_Properties::visible_count>", "Time_Axis::set<&Time_Axis_Properties::format>", "Time_Axis::append_time", "Spectrum::set<&Properties::frequency_point_size>", "Spectrum::set<&Properties::interpolation_mode>", "Spectrum::set<&Properties::current_pen>", "Spectrum::set<&Properties::max_brush>", "Spectrum::update_samples", "Spectrum::power_at", "Spectrum::add_custom_marker",