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