From e3d3bfd7677e2441c3ae1a30b4dc6d36ef4c2214 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Fri, 21 Aug 2026 22:12:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=AF=94=E8=BE=83=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E7=9A=84=E4=B8=80=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Project_detail_specification.md | 3 + kernel/src/kernel/renderable.cpp | 4 - kernel/src/kernel/renderable.hpp | 2 - kernel/src/kernel/scene.cpp | 17 +++ kernel/src/kernel/scene.ipp | 41 ++++++ render_2D/render_2D/axis/Abs_Axis.ipp | 14 ++ render_2D/render_2D/axis/Axis_Types.cpp | 2 + render_2D/render_2D/axis/Axis_Types.hpp | 12 ++ render_2D/render_2D/axis/Numeric_Axis.ipp | 14 +- render_2D/render_2D/axis/Time_Axis.ipp | 17 ++- render_2D/render_2D/base/Renderable_2D.ipp | 20 ++- .../render_2D/plottable/Frequency_Trace.cpp | 1 - .../render_2D/plottable/Frequency_Trace.hpp | 2 +- .../render_2D/plottable/Frequency_Trace.ipp | 17 +-- .../plottable/Selection_Rectangle_Overlay.cpp | 20 ++- .../plottable/Selection_Rectangle_Overlay.hpp | 16 +-- .../plottable/Selection_Rectangle_Overlay.ipp | 83 +++++++---- render_2D/render_2D/plottable/Waterfall.cpp | 2 +- render_2D/render_2D/plottable/Waterfall.hpp | 58 +++++--- render_2D/render_2D/plottable/Waterfall.ipp | 22 +-- .../render_2D/plottable/common/Curve_Plot.cpp | 8 +- .../render_2D/plottable/common/Curve_Plot.hpp | 1 + render_2D/render_2D/scene/Render_Scene_2D.cpp | 4 +- render_2D/render_2D/scene/Render_Scene_2D.hpp | 5 +- render_2D/render_2D/scene/Render_Scene_2D.ipp | 54 ++++--- render_2D/tests/Axis_Test.cpp | 71 +++++---- render_2D/tests/Plottable_Migration_Test.cpp | 81 ++++++++--- .../render_3D/detail/Async_Render_Backend.cpp | 27 +++- .../render_3D/detail/Async_Render_Backend.hpp | 5 +- render_3D/render_3D/scene/Render_Scene_3D.cpp | 2 +- render_3D/render_3D/scene/Render_Scene_3D.hpp | 5 +- render_3D/render_3D/scene/Render_Scene_3D.ipp | 11 +- web_server/src/Gallery_Plots.hpp | 1 + web_server/src/Plot.cpp | 136 ++++++++++++++---- web_server/src/Web_Server.cpp | 1 + webapp_gallery/src/app.tsx | 2 +- webapp_gallery/src/styles.css | 6 +- 37 files changed, 563 insertions(+), 224 deletions(-) diff --git a/Project_detail_specification.md b/Project_detail_specification.md index 2c91c9a..e427d95 100644 --- a/Project_detail_specification.md +++ b/Project_detail_specification.md @@ -16,6 +16,9 @@ * 依赖可以选择 Prop/State 的单字段或整个 `Base_Tag` 层;字段写入必须同时发出字段级和所属层级变更,使用方按实际重建粒度选择一种依赖。 * 整体对象依赖只表达依赖图中的拓扑顺序,不传播 dirty;准备顺序、绘图顺序等业务含义由各自 Tag 解释。具体字段依赖才用于对应 Tag 的 dirty 传播,例如绘图缓存失效。 * Kernel `Scene` 只负责 2D/3D 共有的 Prepare 数据阶段;Paint、缓存失效、像素合成和异步后端提交由对应渲染模块自己的 Scene、Tag 与 Taskflow 负责。 +* `Scene::Private` 是输入事件流的唯一所有者:外部转移事件对象所有权,无锁提交到双缓冲队列,Prepare 入口交换并按 FIFO 消费,不复制事件快照。2D 按 Renderable 区域与 Paint 顺序形成接受链,区域默认整个 viewport;3D 无等待提交渲染域,满载时保留当前事件供下一次 Prepare 重试。 +* `Time_Axis` 是时间与 tick 的唯一权威来源;使用层先推进时间轴,再把同一 tick 分发给所有相关数据图元。时间窗口从第一条数据起始终锚定最新 tick,未产生数据的槽位保持背景。 +* 图表选区保存两根轴上的数据范围,绘制时才映射为像素;选区作为独立 Renderable 在使用层与图元组合,禁止在各图元内复制选区状态。 * `Root` 只保存一个最终 `Private` 指针;`Builder::build()` 校验成功后创建并挂接完整 Private,`Root` 通过公共 Private 基类的虚析构统一释放。禁止直接公开该指针。 * 能从权威结构查询或计算的数据即时获取,不保存为成员。类只保存自身职责需要且无法推导的状态,并检查每个新增成员的读写者和生命周期。 * 公共接口只表达业务语义,不暴露 `Private`、内部指针、线程状态或缓冲区角色;接口保持正交,不增加空配置、未完成接口、无消费者统计或只做转发的 getter/setter。 diff --git a/kernel/src/kernel/renderable.cpp b/kernel/src/kernel/renderable.cpp index 533fd6b..787174f 100644 --- a/kernel/src/kernel/renderable.cpp +++ b/kernel/src/kernel/renderable.cpp @@ -2,10 +2,6 @@ #include #include namespace aethera { -void Renderable::dispatch_event(const Event& event) { - const auto& data = static_cast(*d); - if (data.event_run) data.event_run(this, event); -} std::optional Renderable::event_routing_distance(const Event& event) const { const auto& data = static_cast(*d); return data.event_routing_distance_run ? data.event_routing_distance_run(this, event) : std::nullopt; diff --git a/kernel/src/kernel/renderable.hpp b/kernel/src/kernel/renderable.hpp index f8e5ded..e42eb5b 100644 --- a/kernel/src/kernel/renderable.hpp +++ b/kernel/src/kernel/renderable.hpp @@ -86,8 +86,6 @@ struct Renderable : Def { }; /* 完整声明、内部派发以及 Prepare/Paint CRTP 能力契约见 renderable.ipp 中的 Renderable::Private。 */ struct Private; - /* 将事件交给最终 Private 的可选 handle_event(...) 能力。 */ - void dispatch_event(const Event& event); /* 返回该对象参与当前事件竞争时的几何距离;无值表示沿用普通绘制层级路由。 */ [[nodiscard]] std::optional event_routing_distance(const Event& event) const; private: diff --git a/kernel/src/kernel/scene.cpp b/kernel/src/kernel/scene.cpp index 8236aae..bc642f8 100644 --- a/kernel/src/kernel/scene.cpp +++ b/kernel/src/kernel/scene.cpp @@ -1,7 +1,24 @@ #include "scene.hpp" /* 后端共有 Prepare Scene 实现。 */ +#include namespace aethera { +Scene::Private::Event_Node::Event_Node(std::shared_ptr event_value) : event(std::move(event_value)) {} +void Scene::Private::Event_Double_Buffer::destroy(Event_Node* node) noexcept { + while (node) { Event_Node* next = node->next; delete node; node = next; } +} +Scene::Private::Event_Double_Buffer::~Event_Double_Buffer() { + destroy(current); destroy(pending.exchange(nullptr, std::memory_order_acquire)); +} +void Scene::Private::Event_Double_Buffer::push(std::shared_ptr event) { + auto* node = new Event_Node(std::move(event)); + node->next = pending.load(std::memory_order_relaxed); + while (!pending.compare_exchange_weak(node->next, node, std::memory_order_release, std::memory_order_relaxed)) {} +} Scene::Private::Private() : runtime(std::make_unique()) {} Scene::Private::~Private() { if (runtime->taskflow) detail::clear_stage_observers(runtime->taskflow.get()); } +void Scene::Private::push_event(std::unique_ptr event) { + if (!event) throw std::invalid_argument("scene event ownership must not be empty"); + event_buffer.push(std::shared_ptr{std::move(event)}); +} } diff --git a/kernel/src/kernel/scene.ipp b/kernel/src/kernel/scene.ipp index bce790c..1186311 100644 --- a/kernel/src/kernel/scene.ipp +++ b/kernel/src/kernel/scene.ipp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -7,9 +8,32 @@ namespace aethera { struct Scene::Private : Prev_Private { struct Result {}; /* process(...) 完成回调的结果类型;当前仅表示完成。 */ struct Runtime; /* Scene 的 Taskflow 构建产物;完整定义位于本文件下方。 */ + /* 单生产批次节点只在外部入队与内部消费之间转移,不回收到待处理链。 */ + struct Event_Node { + std::shared_ptr event; /* 原始事件对象的所有权;3D 异步提交成功后可延长到渲染域任务。 */ + Event_Node* next{}; /* 原子待处理栈或内部当前批次中的下一节点。 */ + explicit Event_Node(std::shared_ptr event_value); + }; + /* Scene 唯一事件流:多生产者无锁压入 pending,Prepare 开始时原子交换为单消费者 current。 */ + struct Event_Double_Buffer : Pinned { + std::atomic pending{}; /* 外部线程并发提交的待处理事件栈。 */ + Event_Node* current{}; /* Scene 执行线程独占、已恢复 FIFO 的当前批次。 */ + ~Event_Double_Buffer(); + void push(std::shared_ptr event); + template + void consume(Callback&& callback) requires std::predicate&>; + private: + static void destroy(Event_Node* node) noexcept; + }; std::unique_ptr runtime; /* Scene 唯一运行时构建产物的所有权。 */ + Event_Double_Buffer event_buffer; /* 跨输入线程与 Scene Prepare 边界交换的唯一事件队列。 */ Private(); ~Private(); + /* 将事件所有权无锁提交到外部待处理批次;空所有权违反调用契约。 */ + void push_event(std::unique_ptr event); + /* 在 Scene 执行线程交换并按提交顺序消费一个完整批次。 */ + template + void consume_events(Callback&& callback) requires std::predicate&>; /* Def CRTP hook:所有缓冲推进后重建必要的总 Taskflow,并更新 Scene_State_Tag 状态层。 */ template void after_advance(Object* object, @@ -25,6 +49,23 @@ struct Scene::Private : Prev_Private { struct Scene::Private::Runtime { std::unique_ptr taskflow; /* 当前已构建的总 Taskflow;为空表示尚未构建。 */ }; +template +void Scene::Private::Event_Double_Buffer::consume(Callback&& callback) requires std::predicate&> { + if (!current) { + Event_Node* incoming = pending.exchange(nullptr, std::memory_order_acquire); + while (incoming) { + Event_Node* next = incoming->next; incoming->next = current; current = incoming; incoming = next; + } + } + while (current) { + if (!std::invoke(callback, std::as_const(current->event))) return; + std::unique_ptr node{current}; current = current->next; + } +} +template +void Scene::Private::consume_events(Callback&& callback) requires std::predicate&> { + event_buffer.consume(std::forward(callback)); +} template void Scene::Private::process(Object* object, Callback&& callback) requires std::invocable { auto& private_data = static_cast(*this); diff --git a/render_2D/render_2D/axis/Abs_Axis.ipp b/render_2D/render_2D/axis/Abs_Axis.ipp index d9b0275..205c20f 100644 --- a/render_2D/render_2D/axis/Abs_Axis.ipp +++ b/render_2D/render_2D/axis/Abs_Axis.ipp @@ -69,6 +69,8 @@ struct Abs_Axis::Private : Prev_Private { void after_prop_set(Object* object, Member Owner::* member, Prop_Access props); /* CRTP 默认:每两个主刻度之间生成 4 个次刻度。 */ [[nodiscard]] int sub_tick_count(const Attached auto* object, double major_step) const; + /* CRTP 覆盖:轴的事件区域由轴段、刻度长度和标签字号即时计算,不占用整个 viewport。 */ + [[nodiscard]] Rect_F event_region(const Attached auto* object, Size viewport) const; /* CRTP 覆盖:绑定 Renderable 机制和最终轴公开薄壳分派;派生 Private 必须先调用此实现。 */ template void bind_private_crtp(Object* object); @@ -178,6 +180,18 @@ void Abs_Axis::Private::bind_private_crtp(Object* object) { return std::hypot(pointer->position_x() - nearest_x, pointer->position_y() - nearest_y); }; } +inline Rect_F Abs_Axis::Private::event_region(const Attached auto* object, Size) const { + using Object = std::remove_cv_t>; + const auto& private_data = static_cast(*this); + const auto& layout = static_cast(*private_data.current); + const double margin = std::max(8.0, std::abs(layout.tick_length) + layout.unit_text_font.size * 1.5); + const Point_F second = layout.orientation == Axis_Orientation::horizontal + ? Point_F{layout.position.x + layout.pixel_length, layout.position.y} + : Point_F{layout.position.x, layout.position.y + layout.pixel_length}; + return Rect_F{layout.position.x - margin, layout.position.y - margin, + second.x - layout.position.x + margin * 2.0, + second.y - layout.position.y + margin * 2.0}.normalized(); +} inline void Abs_Axis::Private::prepare_data(Attached auto* object) { using Object = std::remove_pointer_t; auto& private_data = static_cast(*this); diff --git a/render_2D/render_2D/axis/Axis_Types.cpp b/render_2D/render_2D/axis/Axis_Types.cpp index baecae4..30048fd 100644 --- a/render_2D/render_2D/axis/Axis_Types.cpp +++ b/render_2D/render_2D/axis/Axis_Types.cpp @@ -19,4 +19,6 @@ bool Axis_Range::contains(Axis_Coordinate coordinate) const noexcept { } bool Axis_Range::operator==(const Axis_Range&) const = default; +bool Axis_Point::operator==(const Axis_Point&) const = default; +bool Axis_Rectangle::operator==(const Axis_Rectangle&) const = default; } diff --git a/render_2D/render_2D/axis/Axis_Types.hpp b/render_2D/render_2D/axis/Axis_Types.hpp index 4fd3d4e..e69dfbd 100644 --- a/render_2D/render_2D/axis/Axis_Types.hpp +++ b/render_2D/render_2D/axis/Axis_Types.hpp @@ -33,4 +33,16 @@ struct Axis_Range { [[nodiscard]] bool contains(Axis_Coordinate coordinate) const noexcept; bool operator==(const Axis_Range&) const; }; +/* 两根正交轴上的一个数据坐标点;不表示画布像素。 */ +struct Axis_Point { + Axis_Coordinate horizontal{}; /* 水平轴上的业务坐标。 */ + Axis_Coordinate vertical{}; /* 垂直轴上的业务坐标。 */ + bool operator==(const Axis_Point&) const; +}; +/* 两根正交轴定义的数据选择区域;绘制时才映射为像素矩形。 */ +struct Axis_Rectangle { + Axis_Range horizontal{}; /* 水平轴上的已选坐标范围。 */ + Axis_Range vertical{}; /* 垂直轴上的已选坐标范围。 */ + bool operator==(const Axis_Rectangle&) const; +}; } diff --git a/render_2D/render_2D/axis/Numeric_Axis.ipp b/render_2D/render_2D/axis/Numeric_Axis.ipp index 10f4361..4cba27d 100644 --- a/render_2D/render_2D/axis/Numeric_Axis.ipp +++ b/render_2D/render_2D/axis/Numeric_Axis.ipp @@ -1,10 +1,8 @@ #pragma once #include -#include #include namespace aethera::render_2d { struct Numeric_Axis::Private : Prev_Private { - std::mutex interaction_mutex{}; /* 保护跨事件保留的拖动手势状态。 */ bool dragging{}; /* 左键拖动手势是否已经开始且尚未释放。 */ Point_F last_pointer{}; /* 上一个拖动事件的位置,单位为画布局部像素。 */ /* CRTP 实现:直接返回 Numeric_Axis::Base_Tag 中唯一保存的数值范围。 */ @@ -57,21 +55,16 @@ inline void Numeric_Axis::Private::handle_event(Attached auto* object, const Eve } if (!numeric_state.drag_enabled || !pointer) return; if (event.type == Event_Type::pointer_press && pointer->pointer_button() == Mouse_Button::left) { - std::lock_guard guard(interaction_mutex); dragging = true; last_pointer = {pointer->position_x(), pointer->position_y()}; event.accept(); return; } if (event.type == Event_Type::pointer_move) { - Point_F previous{}; const Point_F current{pointer->position_x(), pointer->position_y()}; - { - std::lock_guard guard(interaction_mutex); - if (!dragging) return; - previous = last_pointer; - last_pointer = current; - } + if (!dragging) return; + const Point_F previous = last_pointer; + last_pointer = current; const double delta = axis_state.orientation == Axis_Orientation::horizontal ? current.x - previous.x : current.y - previous.y; const double shift = axis_state.pixel_length == 0.0 @@ -85,7 +78,6 @@ inline void Numeric_Axis::Private::handle_event(Attached auto* object, const Eve return; } if (event.type == Event_Type::pointer_release) { - std::lock_guard guard(interaction_mutex); const bool was_dragging = dragging; dragging = false; if (was_dragging) event.accept(); diff --git a/render_2D/render_2D/axis/Time_Axis.ipp b/render_2D/render_2D/axis/Time_Axis.ipp index 4c49d91..946778e 100644 --- a/render_2D/render_2D/axis/Time_Axis.ipp +++ b/render_2D/render_2D/axis/Time_Axis.ipp @@ -3,7 +3,7 @@ #include namespace aethera::render_2d { struct Time_Axis::Private : Prev_Private { - /* CRTP 实现:由 next_tick、visible_count 和 newest_at_start 即时计算范围,不保存镜像坐标区间。 */ + /* CRTP 实现:用固定数量的半开槽位窗口定位 tick;样本不足时保留空槽,禁止把已有样本拉伸铺满整轴。 */ [[nodiscard]] Axis_Range coordinate_range(const Attached auto* object) const; /* CRTP 实现:根据像素长度、估算标签宽度和间距计算至少为 1 的整数刻度步长。 */ [[nodiscard]] double tick_step(const Attached auto* object, Axis_Range coordinate_range) const; @@ -25,6 +25,9 @@ struct Time_Axis::Private : Prev_Private { [[nodiscard]] static const Time_Dispatch& time_dispatch_for(); /* 将一天内时间按支持的占位符格式化;未知字符原样保留。 */ [[nodiscard]] static std::string formatted_time(Time_Of_Day time, std::string_view format); + /* CRTP 覆盖:时间样本写入后使本轴的刻度布局失效;样本仍只保存在 Time_Axis::State。 */ + template + void after_state_set(Object* object, Member Owner::* member, State_Access pending_states); /* CRTP 覆盖:绑定通用轴机制和最终时间轴公开薄壳分派;派生 Private 必须先调用此实现。 */ template void bind_private_crtp(Object* object); @@ -34,10 +37,10 @@ inline Axis_Range Time_Axis::Private::coordinate_range(const Attached auto* obje const auto& private_data = static_cast(*this); const auto& state = static_cast(*private_data.state.current); const auto& prop = static_cast(*private_data.current); - const int latest = std::max(1, state.next_tick - 1); - const int earliest = std::max(0, latest - std::max(2, prop.visible_count) + 1); - if (prop.newest_at_start) return {static_cast(latest), static_cast(earliest)}; - return {static_cast(earliest), static_cast(latest)}; + const int visible_count = std::max(2, prop.visible_count); + const int latest = std::max(0, state.next_tick - 1); + if (prop.newest_at_start) return {static_cast(latest) + 0.5, static_cast(latest - visible_count) + 0.5}; + return {static_cast(latest - visible_count) + 0.5, static_cast(latest) + 0.5}; } inline double Time_Axis::Private::tick_step(const Attached auto* object, Axis_Range coordinate_range) const { using Object = std::remove_cv_t>; @@ -98,4 +101,8 @@ void Time_Axis::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); time_dispatch = &Private::time_dispatch_for(); } +template +void Time_Axis::Private::after_state_set(Object* object, Member Owner::*, State_Access) { + if constexpr (std::same_as) object->template mark_dirty(); +} } diff --git a/render_2D/render_2D/base/Renderable_2D.ipp b/render_2D/render_2D/base/Renderable_2D.ipp index 06f2c35..899e21f 100644 --- a/render_2D/render_2D/base/Renderable_2D.ipp +++ b/render_2D/render_2D/base/Renderable_2D.ipp @@ -4,12 +4,16 @@ namespace aethera::render_2d { struct Renderable_2D::Private : Prev_Private { using Scene_Attach = std::function(Root*)>; using Cache_Access = Blend2D_Cache* (*)(Root*); - Cache_Access pending_cache{}; /* 非空时返回最终对象本轮可写的缓存物理对象。 */ - Blend2D_Cache* paint_target{}; /* 仅在 Scene Paint 阶段有效的非拥有绘制目标。 */ - Blend2D_Cache* valid_cache{}; /* 缓存根最近一次完整重绘产生的权威物理缓存。 */ - Scene_Attach scene_attach{}; /* Scene Builder 在构造期执行的所属场景及拓扑绑定。 */ + using Event_Region_Run = Rect_F (*)(const Root*, Size); + Cache_Access pending_cache{}; /* 非空时返回最终对象本轮可写的缓存物理对象。 */ + Event_Region_Run event_region_run{}; /* 计算最终对象当前事件区域的无虚函数入口。 */ + Blend2D_Cache* paint_target{}; /* 仅在 Scene Paint 阶段有效的非拥有绘制目标。 */ + Blend2D_Cache* valid_cache{}; /* 缓存根最近一次完整重绘产生的权威物理缓存。 */ + Scene_Attach scene_attach{}; /* Scene Builder 在构造期执行的所属场景及拓扑绑定。 */ /* Paint 实现使用:返回 Scene 为本轮指定的目标;未进入 Paint 阶段属于契约错误。 */ [[nodiscard]] Blend2D_Cache& paint_surface(); + /* CRTP 可覆盖:返回最终对象可接收指针与滚轮事件的画布区域;默认覆盖整个 viewport。 */ + [[nodiscard]] Rect_F event_region(const Attached auto* object, Size viewport) const; /* CRTP 覆盖:识别最终类型是否选择了二维颜色缓存。 */ template void bind_private_crtp(Object* object); }; @@ -23,10 +27,18 @@ inline Blend2D_Cache& Renderable_2D::Private::pai if (!paint_target) throw std::logic_error("2D renderable painted without a scene paint target"); return *paint_target; } +inline Rect_F Renderable_2D::Private::event_region(const Attached auto*, Size viewport) const { + return {0.0, 0.0, static_cast(viewport.width), static_cast(viewport.height)}; +} template void Renderable_2D::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); object->template mark_dirty(); + this->event_region_run = [](const Root* root, Size viewport) { + const auto* value = static_cast(root); + const auto& private_data = static_cast(*value->d); + return private_data.event_region(value, viewport); + }; } template void Renderable_2D::Private::bind_private_crtp(Object* object) { diff --git a/render_2D/render_2D/plottable/Frequency_Trace.cpp b/render_2D/render_2D/plottable/Frequency_Trace.cpp index a810019..22cb369 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.cpp +++ b/render_2D/render_2D/plottable/Frequency_Trace.cpp @@ -4,7 +4,6 @@ bool Frequency_Trace_Sample::operator==(const Frequency_Trace_Sample&) const = d bool Frequency_Trace::Prop::operator==(const Prop&) const = default; bool Frequency_Trace::State::operator==(const State&) const = default; void Frequency_Trace::append_sample(Plot_Time_Tick tick, Plot_Value value) { static_cast(*d).dispatch->append(this, tick, value); } -void Frequency_Trace::append_sample(Time_Of_Day time, Plot_Value value) { static_cast(*d).dispatch->append_time(this, time, value); } std::size_t Frequency_Trace::sample_count() const { return static_cast(*d).dispatch->sample_count(this); } std::size_t Frequency_Trace::rendered_point_count() const { return static_cast(*d).dispatch->rendered_point_count(this); } } diff --git a/render_2D/render_2D/plottable/Frequency_Trace.hpp b/render_2D/render_2D/plottable/Frequency_Trace.hpp index 30169ad..2c7c33a 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.hpp +++ b/render_2D/render_2D/plottable/Frequency_Trace.hpp @@ -38,8 +38,8 @@ struct Frequency_Trace : Def partitions{}; /* Prepare 子图各分块的曲线输出。 */ - std::vector values{}; /* 当前状态样本提取出的连续值。 */ - Axis_Range domain{}; /* 当前值集合对应的时间 tick 范围。 */ + std::vector samples{}; /* 保留每个值自己的时间 tick,禁止按首尾范围重新均分。 */ Size canvas{}; /* 当前颜色层尺寸。 */ bool valid{}; /* 两根轴正交且画布有效。 */ }; using Append_Run = void (*)(Root*, Plot_Time_Tick, Plot_Value); - using Time_Append_Run = void (*)(Root*, Time_Of_Day, Plot_Value); using Count_Run = std::size_t (*)(const Root*); struct Dispatch { Append_Run append; /* 按 tick 向最终对象提交样本。 */ - Time_Append_Run append_time; /* 按时刻分配 tick 后提交样本。 */ Count_Run sample_count; /* 查询权威样本数。 */ Count_Run rendered_point_count; /* 查询已准备的曲线点数。 */ }; @@ -64,14 +61,13 @@ void Frequency_Trace::Private::prepare_frame(Object* object, Plot_Partition_Coun const auto& state = object->template read_prop(); const auto& time_layout = time_axis->template read_prop(); const auto& value_layout = value_axis->template read_prop(); prepared = {}; prepared.partitions.resize(partition_count); prepared.canvas = scene->template read_prop().viewport; if (prepared.canvas.empty() || time_layout.orientation == value_layout.orientation || state.samples.empty()) return; - prepared.values.reserve(state.samples.size()); for (const auto& sample : state.samples) prepared.values.push_back(sample.value); - prepared.domain = {static_cast(state.samples.front().tick), static_cast(state.samples.back().tick)}; prepared.valid = true; + prepared.samples.reserve(state.samples.size()); for (const auto& sample : state.samples) prepared.samples.push_back({static_cast(sample.tick), sample.value}); prepared.valid = true; } template void Frequency_Trace::Private::prepare_partition(Object* object, Plot_Partition_Count partition_index) { if (!prepared.valid) return; const auto& state = object->template read_prop(); const auto& time_layout = time_axis->template read_prop(); const auto& value_layout = value_axis->template read_prop(); const auto& value_state = value_axis->template read_prop(); - const auto range = detail::curve_partition_range(prepared.values.size(), partition_index, prepared.partitions.size(), prepared.domain); - prepared.partitions[partition_index] = detail::prepare_curve(std::span(prepared.values).subspan(range.first_sample, range.sample_count), range.domain, Line_Interpolation_Mode::linear_value, true, time_axis->coordinate_range(), value_state.coordinate_range, time_axis, value_axis, time_layout.orientation, value_layout.orientation); + const Axis_Range domain{prepared.samples.front().coordinate, prepared.samples.back().coordinate}; const auto range = detail::curve_partition_range(prepared.samples.size(), partition_index, prepared.partitions.size(), domain); + prepared.partitions[partition_index] = detail::prepare_curve(std::span(prepared.samples).subspan(range.first_sample, range.sample_count), true, time_axis->coordinate_range(), value_state.coordinate_range, time_axis, value_axis, time_layout.orientation, value_layout.orientation); } template void Frequency_Trace::Private::paint_frame(Object* object) { @@ -96,11 +92,6 @@ const Frequency_Trace::Private::Dispatch& Frequency_Trace::Private::dispatch_for samples.erase(samples.begin(), samples.begin() + static_cast(samples.size() - visible_count)); }); }, - [](Root* root, Time_Of_Day time, Plot_Value sample_value) { - auto* object = static_cast(root); - auto& data = static_cast(*object->d); - data.dispatch->append(root, data.time_axis->append_time(time), sample_value); - }, [](const Root* root) { return static_cast(root)->template read_prop().samples.size(); }, [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); std::size_t result{}; for (const auto& curve : data.prepared.partitions) result += curve.points.size(); return result; } }; return value; diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp index cd9665d..db5da49 100644 --- a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.cpp @@ -1,7 +1,25 @@ #include "Selection_Rectangle_Overlay.hpp" /* Selection Overlay 最终实例及三类依赖实现。 */ +#include +#include +#include namespace aethera::render_2d { bool Selection_Rectangle_Overlay::Prop::operator==(const Prop&) const = default; bool Selection_Rectangle_Overlay::State::operator==(const State&) const = default; -std::vector Selection_Rectangle_Overlay::selected_regions() const { return static_cast(*d).dispatch->selected_regions(this); } +Axis_Rectangle Selection_Rectangle_Overlay::Private::axis_rectangle(Axis_Point first, Axis_Point second) { + const auto [horizontal_low, horizontal_high] = std::minmax(first.horizontal, second.horizontal); + const auto [vertical_low, vertical_high] = std::minmax(first.vertical, second.vertical); + return {{horizontal_low, horizontal_high}, {vertical_low, vertical_high}}; +} +std::string Selection_Rectangle_Overlay::Private::range_label(const Abs_Axis* axis, Axis_Range range) { + const auto coordinate_label = [axis](Axis_Coordinate coordinate) { + auto label = axis->tick_label(coordinate); + if (!label.empty()) return label; + std::ostringstream stream; + stream << std::fixed << std::setprecision(3) << coordinate; + return stream.str(); + }; + return coordinate_label(range.origin) + " .. " + coordinate_label(range.target); +} +std::vector Selection_Rectangle_Overlay::selected_regions() const { return static_cast(*d).dispatch->selected_regions(this); } void Selection_Rectangle_Overlay::clear_selected_regions() { static_cast(*d).dispatch->clear_selected_regions(this); } } diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp index 3fa2a45..06d0d7f 100644 --- a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.hpp @@ -4,18 +4,18 @@ #include "../scene/Render_Scene_2D.hpp" #include "Plot_Types.hpp" #include +#include #include #include namespace aethera::render_2d { struct Selection_Rectangle_Overlay : Def> { using Scene_Object = Impl; - using Axis_Object = Impl; struct Prop : Prev_Prop { - Font label_font{}; /* 选择范围标签使用的字体。 */ - Pen label_pen{Color::white()}; /* 选择范围标签的文字样式。 */ + Font label_font{}; /* 选择范围标签使用的字体。 */ + Pen label_pen{Color::white()}; /* 选择范围标签的文字样式。 */ Brush selection_brush{Color{0, 0, 255, 50}, Brush_Style::solid}; /* 选择矩形内部填充。 */ Pen selection_border_pen{Color::white(), 1.0, Line_Style::dash}; /* 选择矩形边框样式。 */ - std::vector selected_regions{}; /* 已完成选择的轴坐标矩形。 */ + std::vector selected_regions{}; /* 已完成选择的轴数据区域。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { @@ -26,13 +26,13 @@ struct Selection_Rectangle_Overlay : Def struct Builder : Prev_Builder { using Base = Prev_Builder; - Builder(Axis_Object* horizontal_axis, Axis_Object* vertical_axis); + template + Builder(Horizontal_Axis* horizontal_axis, Vertical_Axis* vertical_axis); [[nodiscard]] std::expected, Dependency_Graph_Error> build(); private: - Axis_Object* horizontal_axis{}; /* 不拥有的水平坐标轴。 */ - Axis_Object* vertical_axis{}; /* 不拥有的垂直坐标轴。 */ + std::function initialize{}; /* build 成功后绑定两根最终轴及其 Scene 拓扑。 */ }; - [[nodiscard]] std::vector selected_regions() const; + [[nodiscard]] std::vector selected_regions() const; void clear_selected_regions(); }; } diff --git a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp index d68c4a2..42d8c06 100644 --- a/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp +++ b/render_2D/render_2D/plottable/Selection_Rectangle_Overlay.ipp @@ -1,47 +1,67 @@ #pragma once #include "common/Curve_Plot.hpp" +#include #include -#include +#include namespace aethera::render_2d { struct Selection_Rectangle_Overlay::Private : Prev_Private { using No_Prepare = void; - using Regions_Get = std::vector (*)(const Root*); + using Regions_Get = std::vector (*)(const Root*); using Clear_Run = void (*)(Root*); struct Dispatch { Regions_Get selected_regions; /* 查询最终对象已发布选择区域。 */ Clear_Run clear_selected_regions; /* 清空最终对象选择区域。 */ }; Scene_Object* scene{}; /* 不拥有的所属 Scene。 */ - Axis_Object* horizontal_axis{}; /* 不拥有的水平坐标轴。 */ - Axis_Object* vertical_axis{}; /* 不拥有的垂直坐标轴。 */ - Point_F drag_origin{}; /* 当前拖动起点,单位为画布像素。 */ - Point_F drag_current{}; /* 当前拖动终点,单位为画布像素。 */ + Abs_Axis* horizontal_axis{}; /* 不拥有的水平坐标轴。 */ + Abs_Axis* vertical_axis{}; /* 不拥有的垂直坐标轴。 */ + Axis_Point drag_origin{}; /* 当前拖动起点的两轴数据坐标。 */ + Axis_Point drag_current{}; /* 当前拖动终点的两轴数据坐标。 */ + Point_F press_position{}; /* 仅用于区分点击与拖动的按下像素位置。 */ bool dragging{}; /* 是否正在构造尚未提交的选择矩形。 */ const Dispatch* dispatch{}; /* 最终类型公开薄壳分派表。 */ /* CRTP 覆盖:绑定 Paint-only、事件能力和最终 Overlay 分派表。 */ template void bind_private_crtp(Object* object); - void bind_sources(Axis_Object* horizontal_axis_value, Axis_Object* vertical_axis_value); + void bind_sources(Abs_Axis* horizontal_axis_value, Abs_Axis* vertical_axis_value); template [[nodiscard]] static const Dispatch& dispatch_for(); + [[nodiscard]] static Axis_Rectangle axis_rectangle(Axis_Point first, Axis_Point second); + [[nodiscard]] static std::string range_label(const Abs_Axis* axis, Axis_Range range); /* CRTP 覆盖:无需 Prepare 子图,直接绘制选择矩形。 */ template void paint(Object* object); /* CRTP 覆盖:处理拖拽并更新权威选择区域状态。 */ template void handle_event(Object* object, const Event& event); + /* CRTP 覆盖:空闲时只在两轴围成的图域接收事件,拖动期间扩展到完整 viewport。 */ + [[nodiscard]] Rect_F event_region(const Attached auto* object, Size viewport) const; /* CRTP 覆盖:本类状态写入后只标记 Paint 数据失效。 */ template void after_prop_set(Object* object, Member Owner::* member, Prop_Access pending_states); template void before_advance(Object* object, Prop_Type* pending_prop, State_Access pending_states, const Prop_Type* current_prop, State_Access current_states); }; template -Selection_Rectangle_Overlay::Builder::Builder(Axis_Object* horizontal_axis_value, Axis_Object* vertical_axis_value) : Base(), horizontal_axis(horizontal_axis_value), vertical_axis(vertical_axis_value) {} +template +Selection_Rectangle_Overlay::Builder::Builder(Horizontal_Axis* horizontal_axis, Vertical_Axis* vertical_axis) : Base() { + if (!horizontal_axis || !vertical_axis) throw std::invalid_argument("selection rectangle requires two axes"); + initialize = [horizontal_axis, vertical_axis](Object* overlay) { + auto& private_data = static_cast(*overlay->d); + private_data.bind_sources(horizontal_axis, vertical_axis); + private_data.scene_attach = [object = overlay, horizontal_axis, vertical_axis](Root* root) -> std::expected { + auto* scene = static_cast(root); + auto& data = static_cast(*object->d); + data.scene = scene; + return scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { + prepare.add_dependency(object, scene); prepare.add_dependency(object, horizontal_axis); prepare.add_dependency(object, vertical_axis); paint.add_dependency(horizontal_axis, object); paint.add_dependency(vertical_axis, object); cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(object, scene); + const auto bind_axis = [&](Axis* axis) { cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, axis); if constexpr (std::derived_from) { cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::precision>(object, axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::locale>(object, axis); } else if constexpr (std::derived_from) { cache.template add_prop_dependency<&Time_Axis::Prop::visible_count>(object, axis); cache.template add_prop_dependency<&Time_Axis::Prop::newest_at_start>(object, axis); cache.template add_prop_dependency<&Time_Axis::Prop::format>(object, axis); cache.template add_dependency<&Time_Axis::State::next_tick>(object, axis); } }; + bind_axis(horizontal_axis); bind_axis(vertical_axis); + }); + }; + }; +} template std::expected, Dependency_Graph_Error> Selection_Rectangle_Overlay::Builder::build() { auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto overlay = std::move(result).value(); - auto& private_data = static_cast(*overlay->d); private_data.bind_sources(horizontal_axis, vertical_axis); - private_data.scene_attach = [object = overlay.get()](Root* root) -> std::expected { - auto* scene = static_cast(root); auto& data = static_cast(*object->d); data.scene = scene; auto* horizontal_axis = data.horizontal_axis; auto* vertical_axis = data.vertical_axis; - return scene->template edit_dependency_graph([&](auto& prepare, auto& paint, auto& cache) { prepare.add_dependency(object, scene); prepare.add_dependency(object, horizontal_axis); prepare.add_dependency(object, vertical_axis); paint.add_dependency(horizontal_axis, object); paint.add_dependency(vertical_axis, object); cache.template add_prop_dependency<&Render_Scene_2D::Prop::viewport>(object, scene); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, horizontal_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, horizontal_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, horizontal_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, horizontal_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::position>(object, vertical_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::pixel_length>(object, vertical_axis); cache.template add_prop_dependency<&Abs_Axis::Prop::orientation>(object, vertical_axis); cache.template add_prop_dependency<&Numeric_Axis::Prop::coordinate_range>(object, vertical_axis); }); - }; return overlay; + initialize(overlay.get()); + return overlay; } template void Selection_Rectangle_Overlay::Private::paint(Object* object) { @@ -51,37 +71,44 @@ void Selection_Rectangle_Overlay::Private::paint(Object* object) { auto& cache = data.paint_surface(); if (canvas.empty()) return; detail::Painter painter(cache, canvas); - const auto paint_region = [&](Rect_F region) { - const auto rect = detail::map_plot_rect(horizontal_axis, {region.x, region.x + region.width}, vertical_axis, {region.y, region.y + region.height}, Axis_Orientation::horizontal); + const auto plot_region = detail::map_plot_rect(horizontal_axis, horizontal_axis->coordinate_range(), vertical_axis, vertical_axis->coordinate_range(), Axis_Orientation::horizontal); + auto clip = painter.scoped_clip(plot_region); + const auto paint_region = [&](const Axis_Rectangle& region) { + const auto rect = detail::map_plot_rect(horizontal_axis, region.horizontal, vertical_axis, region.vertical, Axis_Orientation::horizontal); painter.rect(rect, state.selection_border_pen, state.selection_brush); + const Point_F label_position{rect.x + 4.0, rect.y + 3.0}; + painter.text(label_position, "X " + range_label(horizontal_axis, region.horizontal), state.label_font, state.label_pen); + painter.text({label_position.x, label_position.y + std::max(12.0, state.label_font.size * 1.35)}, "Y " + range_label(vertical_axis, region.vertical), state.label_font, state.label_pen); }; for (const auto& region : state.selected_regions) paint_region(region); - if (dragging) painter.rect(Rect_F{drag_origin.x, drag_origin.y, drag_current.x - drag_origin.x, drag_current.y - drag_origin.y}.normalized(), state.selection_border_pen, state.selection_brush); + if (dragging) paint_region(axis_rectangle(drag_origin, drag_current)); } template void Selection_Rectangle_Overlay::Private::handle_event(Object* object, const Event& event) { const auto* pointer = dynamic_cast(&event); if (!pointer) return; const Point_F point{pointer->position_x(), pointer->position_y()}; + const Axis_Point axis_point{horizontal_axis->point_to_coordinate(point), vertical_axis->point_to_coordinate(point)}; if (event.type == Event_Type::pointer_press && pointer->pointer_button() == Mouse_Button::left) { const auto modifiers = static_cast>(pointer->keyboard_modifiers()); const auto control = static_cast>(Keyboard_Modifier::control); - if ((modifiers & control) == 0) object->template set<&Prop::selected_regions>(std::vector{}); - dragging = true; drag_origin = point; drag_current = point; object->template mark_dirty(); event.accept(); return; + if ((modifiers & control) == 0) object->template set<&Prop::selected_regions>(std::vector{}); + dragging = true; drag_origin = axis_point; drag_current = axis_point; press_position = point; object->template mark_dirty(); event.accept(); return; } - if (event.type == Event_Type::pointer_move && dragging) { drag_current = point; object->template mark_dirty(); event.accept(); return; } + if (event.type == Event_Type::pointer_move && dragging) { drag_current = axis_point; object->template mark_dirty(); event.accept(); return; } if (event.type != Event_Type::pointer_release || !dragging) return; - dragging = false; drag_current = point; - if (std::hypot(drag_current.x - drag_origin.x, drag_current.y - drag_origin.y) < 3.0) { + dragging = false; drag_current = axis_point; + if (std::hypot(point.x - press_position.x, point.y - press_position.y) < 3.0) { object->template mark_dirty(); event.accept(); return; } - const Axis_Coordinate first_x = horizontal_axis->point_to_coordinate(drag_origin); - const Axis_Coordinate second_x = horizontal_axis->point_to_coordinate(drag_current); - const Axis_Coordinate first_y = vertical_axis->point_to_coordinate(drag_origin); - const Axis_Coordinate second_y = vertical_axis->point_to_coordinate(drag_current); - object->template update_prop<&Prop::selected_regions>([=](Prop_Access props) { props.template get().selected_regions.push_back(Rect_F{first_x, first_y, second_x - first_x, second_y - first_y}.normalized()); }); + const Axis_Rectangle selected = axis_rectangle(drag_origin, drag_current); + object->template update_prop<&Prop::selected_regions>([=](Prop_Access props) { props.template get().selected_regions.push_back(selected); }); event.accept(); } +inline Rect_F Selection_Rectangle_Overlay::Private::event_region(const Attached auto*, Size viewport) const { + if (dragging) return {0.0, 0.0, static_cast(viewport.width), static_cast(viewport.height)}; + return detail::map_plot_rect(horizontal_axis, horizontal_axis->coordinate_range(), vertical_axis, vertical_axis->coordinate_range(), Axis_Orientation::horizontal); +} template void Selection_Rectangle_Overlay::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } template @@ -90,11 +117,11 @@ template const Selection_Rectangle_Overlay::Private::Dispatch& Selection_Rectangle_Overlay::Private::dispatch_for() { static const Dispatch value{ [](const Root* root) { return static_cast(root)->template read_prop().selected_regions; }, - [](Root* root) { static_cast(root)->template set<&Prop::selected_regions>(std::vector{}); } + [](Root* root) { static_cast(root)->template set<&Prop::selected_regions>(std::vector{}); } }; return value; } template void Selection_Rectangle_Overlay::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } -inline void Selection_Rectangle_Overlay::Private::bind_sources(Axis_Object* horizontal_axis_value, Axis_Object* vertical_axis_value) { horizontal_axis = horizontal_axis_value; vertical_axis = vertical_axis_value; } +inline void Selection_Rectangle_Overlay::Private::bind_sources(Abs_Axis* horizontal_axis_value, Abs_Axis* vertical_axis_value) { horizontal_axis = horizontal_axis_value; vertical_axis = vertical_axis_value; } } diff --git a/render_2D/render_2D/plottable/Waterfall.cpp b/render_2D/render_2D/plottable/Waterfall.cpp index 45ec89a..31b645b 100644 --- a/render_2D/render_2D/plottable/Waterfall.cpp +++ b/render_2D/render_2D/plottable/Waterfall.cpp @@ -1,4 +1,4 @@ #include "Waterfall.hpp" /* Waterfall 最终实例及三类依赖实现。 */ namespace aethera::render_2d { bool Waterfall_Row::operator==(const Waterfall_Row&) const = default; bool Waterfall::Prop::operator==(const Prop&) const = default; -bool Waterfall::State::operator==(const State&) const = default; void Waterfall::append_row(Plot_Time_Tick tick, std::span values) { static_cast(*d).dispatch->append(this, tick, values); } void Waterfall::append_row(Plot_Time_Tick tick, std::pmr::vector&& values) { append_row(tick, std::span(values.data(), values.size())); } void Waterfall::append_row(Time_Of_Day time, std::span values) { static_cast(*d).dispatch->append_time(this, time, values); } void Waterfall::append_row(Time_Of_Day time, std::pmr::vector&& values) { append_row(time, std::span(values.data(), values.size())); } std::size_t Waterfall::row_count() const { return static_cast(*d).dispatch->row_count(this); } std::size_t Waterfall::stored_point_count() const { return static_cast(*d).dispatch->point_count(this); } std::size_t Waterfall::rendered_cell_count() const { return static_cast(*d).dispatch->rendered_count(this); } } +bool Waterfall::State::operator==(const State&) const = default; void Waterfall::append_row(Plot_Time_Tick tick, std::span values) { static_cast(*d).dispatch->append(this, tick, values); } void Waterfall::append_row(Plot_Time_Tick tick, std::pmr::vector&& values) { append_row(tick, std::span(values.data(), values.size())); } std::size_t Waterfall::row_count() const { return static_cast(*d).dispatch->row_count(this); } std::size_t Waterfall::stored_point_count() const { return static_cast(*d).dispatch->point_count(this); } std::size_t Waterfall::rendered_cell_count() const { return static_cast(*d).dispatch->rendered_count(this); } } diff --git a/render_2D/render_2D/plottable/Waterfall.hpp b/render_2D/render_2D/plottable/Waterfall.hpp index 08bb377..e5b03cb 100644 --- a/render_2D/render_2D/plottable/Waterfall.hpp +++ b/render_2D/render_2D/plottable/Waterfall.hpp @@ -10,43 +10,55 @@ #include #include namespace aethera::render_2d { -struct Waterfall_Row { Plot_Time_Tick tick{}; std::vector values{}; bool operator==(const Waterfall_Row&) const; }; +struct Waterfall_Row { + Plot_Time_Tick tick{}; /* 使用层从绑定时间轴取得的行位置 tick。 */ + std::vector values{}; /* 该时间槽从低频到高频排列的功率值。 */ + bool operator==(const Waterfall_Row&) const; +}; struct Waterfall : Def> { - using Scene_Object = Impl; using Frequency_Object = Impl; using Time_Object = Impl; + using Scene_Object = Impl; + using Frequency_Object = Impl; + using Time_Object = Impl; struct Prop : Prev_Prop { - bool tooltip_enabled{true}; /* 是否响应指针位置显示提示。 */ - Font tooltip_font{}; /* 提示文字字体。 */ - Pen tooltip_text_pen{Color::white()}; /* 提示文字样式。 */ - Brush tooltip_background_brush{Color{20, 20, 20, 220}, Brush_Style::solid}; /* 提示背景样式。 */ - std::size_t frequency_bin_count{}; /* 目标频率列数;零值使用最新行尺寸。 */ - Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ - bool visible_range_only{true}; /* 是否按频率轴范围裁剪栅格。 */ - Axis_Range frequency_range{0.0, 10.0}; /* 每行频谱覆盖的频率范围。 */ - Axis_Range power_range{0.0, 10.0}; /* 颜色映射使用的功率范围。 */ - Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ + bool tooltip_enabled{true}; /* 是否响应指针位置显示提示。 */ + Font tooltip_font{}; /* 提示文字字体。 */ + Pen tooltip_text_pen{Color::white()}; /* 提示文字样式。 */ + Brush tooltip_background_brush{Color{20, 20, 20, 220}, Brush_Style::solid}; /* 提示背景样式。 */ + std::size_t frequency_bin_count{}; /* 目标频率列数;零值使用最新行尺寸。 */ + Plot_Partition_Count partition_count{1}; /* fixed 模式使用的 Prepare 子图分块数。 */ + bool visible_range_only{true}; /* 是否按频率轴范围裁剪栅格。 */ + Axis_Range frequency_range{0.0, 10.0}; /* 每行频谱覆盖的频率范围。 */ + Axis_Range power_range{0.0, 10.0}; /* 颜色映射使用的功率范围。 */ + Plot_Partition_Mode partition_mode{Plot_Partition_Mode::automatic}; /* Prepare 子图分块策略。 */ Image_Interpolation_Mode interpolation_mode{Image_Interpolation_Mode::nearest}; /* 栅格放大时的图像插值方式。 */ - Color_Map color_map{}; /* 功率到颜色的映射。 */ - std::vector rows{}; /* 从旧到新的瀑布行唯一权威集合。 */ + Color_Map color_map{}; /* 功率到颜色的映射。 */ + std::vector rows{}; /* 从旧到新的瀑布行唯一权威集合。 */ bool operator==(const Prop&) const; }; struct State : Prev_State { - std::size_t row_count{}; /* 当前发布的瀑布行数。 */ - std::size_t stored_point_count{}; /* 当前发布的功率点总数。 */ - std::size_t rendered_cell_count{}; /* 最近一次 Prepare 生成的色块数。 */ + std::size_t row_count{}; /* 当前发布的瀑布行数。 */ + std::size_t stored_point_count{}; /* 当前发布的功率点总数。 */ + std::size_t rendered_cell_count{}; /* 最近一次 Prepare 生成的色块数。 */ bool operator==(const State&) const; }; struct Private; - template struct Builder : Prev_Builder { + template + struct Builder : Prev_Builder { using Base = Prev_Builder; Builder(Frequency_Object* frequency_axis, Time_Object* time_axis); [[nodiscard]] std::expected, Dependency_Graph_Error> build(); private: - Frequency_Object* frequency_axis{}; /* 不拥有的频率轴;生命周期必须覆盖 Waterfall。 */ - Time_Object* time_axis{}; /* 不拥有的时间轴;生命周期必须覆盖 Waterfall。 */ + Frequency_Object* frequency_axis{}; /* 不拥有的频率轴;生命周期必须覆盖 Waterfall。 */ + Time_Object* time_axis{}; /* 不拥有的时间轴;生命周期必须覆盖 Waterfall。 */ }; - void append_row(Plot_Time_Tick tick, std::span values); void append_row(Plot_Time_Tick tick, std::pmr::vector&& values); void append_row(Time_Of_Day time, std::span values); void append_row(Time_Of_Day time, std::pmr::vector&& values); - template void append_row(Plot_Time_Tick tick, const Values& values); template void append_row(Time_Of_Day time, const Values& values); - [[nodiscard]] std::size_t row_count() const; [[nodiscard]] std::size_t stored_point_count() const; [[nodiscard]] std::size_t rendered_cell_count() const; + /* 提交同一使用层刚由绑定 Time_Axis::append_time() 分配的 tick;本图元不推进时间轴。 */ + void append_row(Plot_Time_Tick tick, std::span values); + void append_row(Plot_Time_Tick tick, std::pmr::vector&& values); + template + void append_row(Plot_Time_Tick tick, const Values& values); + [[nodiscard]] std::size_t row_count() const; + [[nodiscard]] std::size_t stored_point_count() const; + [[nodiscard]] std::size_t rendered_cell_count() const; }; } #include "Waterfall.ipp" diff --git a/render_2D/render_2D/plottable/Waterfall.ipp b/render_2D/render_2D/plottable/Waterfall.ipp index 5aeb143..8b84a4d 100644 --- a/render_2D/render_2D/plottable/Waterfall.ipp +++ b/render_2D/render_2D/plottable/Waterfall.ipp @@ -2,24 +2,29 @@ #include "common/Curve_Plot.hpp" #include "common/Raster_Plot.hpp" #include +#include #include #include #include namespace aethera::render_2d { struct Waterfall::Private : Prev_Private { + struct Prepared_Row { + std::size_t source{}; /* Prop::rows 中提供该可见行功率值的下标。 */ + int slot{}; /* 该 tick 在当前固定时间窗口中的离散槽位。 */ + }; struct Prepared { - detail::Raster_Layout layout{}; /* 可视频段与时间行组成的色块布局。 */ - std::vector pixels{}; /* 当前行集合转换后的像素矩阵。 */ + detail::Raster_Layout layout{}; /* 可视频段与完整时间窗口组成的色块布局。 */ + std::vector pixels{}; /* 固定时间窗口的像素矩阵;无数据槽保持透明。 */ + std::vector rows{}; /* 可见源行到固定时间槽位的映射。 */ Rect_F tooltip_box{}; /* 当前 hover 提示框的画布矩形。 */ std::string tooltip_text{}; /* 当前 hover 频率文本;空值表示不绘制。 */ Size canvas{}; /* 当前 Scene viewport 的像素尺寸。 */ int source_first{}; /* 可视频段在源频谱行中的首列。 */ bool valid{}; /* 轴布局和行数据是否足以生成色块。 */ }; - using Append_Run = void (*)(Root*, Plot_Time_Tick, std::span); using Time_Append_Run = void (*)(Root*, Time_Of_Day, std::span); using Count_Run = std::size_t (*)(const Root*); + using Append_Run = void (*)(Root*, Plot_Time_Tick, std::span); using Count_Run = std::size_t (*)(const Root*); struct Dispatch { Append_Run append; /* 按 tick 向最终对象提交一行。 */ - Time_Append_Run append_time; /* 按时刻分配 tick 后提交一行。 */ Count_Run row_count; /* 查询权威行数。 */ Count_Run point_count; /* 查询权威样本总数。 */ Count_Run rendered_count; /* 查询已准备的色块数。 */ @@ -59,20 +64,19 @@ std::expected, Dependency_Graph_Error> Waterfall::Builde }; return plot; } template void Waterfall::append_row(Plot_Time_Tick tick, const Values& values) { append_row(tick, std::span(std::data(values), std::size(values))); } -template void Waterfall::append_row(Time_Of_Day time, const Values& values) { append_row(time, std::span(std::data(values), std::size(values))); } template bool Waterfall::Private::should_rebuild_prepare_graph(Object*, const Prop& state) { const std::size_t cells = state.rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : state.rows.empty() ? 1 : state.rows.back().values.size()); return graph_partition_count != detail::curve_partition_count(state.partition_mode, state.partition_count, cells); } template tf::Taskflow Waterfall::Private::build_prepare_graph(Object* object, const Prop& state) { const std::size_t cells = state.rows.size() * (state.frequency_bin_count ? state.frequency_bin_count : state.rows.empty() ? 1 : state.rows.back().values.size()); graph_partition_count = detail::curve_partition_count(state.partition_mode, state.partition_count, cells); tf::Taskflow graph; auto begin = graph.emplace([this, object] { prepare_frame(object); }).name("waterfall.prepare.frame"); for (Plot_Partition_Count index = 0; index < graph_partition_count; ++index) { auto task = graph.emplace([this, object, index] { prepare_partition(object, index); }).name("waterfall.prepare.partition"); begin.precede(task); } return graph; } template tf::Taskflow Waterfall::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { paint_frame(object); }).name("waterfall.paint.frame"); return graph; } template -void Waterfall::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop(); const auto& frequency_layout = frequency_axis->template read_prop(); const auto& time_layout = time_axis->template read_prop(); prepared = {}; prepared.canvas = scene->template read_prop().viewport; if (state.rows.empty()) return; const auto shortest = std::min_element(state.rows.begin(), state.rows.end(), [](const Waterfall_Row& left, const Waterfall_Row& right) { return left.values.size() < right.values.size(); }); const std::size_t available = shortest->values.size(); const int source_columns = static_cast(state.frequency_bin_count ? std::min(state.frequency_bin_count, available) : available); const auto selection = detail::raster_axis_selection(state.frequency_range, frequency_axis->coordinate_range(), source_columns, state.visible_range_only); if (!selection) return; const int rows = static_cast(state.rows.size()); const Axis_Range time_range = rows == 1 ? time_axis->coordinate_range() : Axis_Range{static_cast(state.rows.front().tick), static_cast(state.rows.back().tick)}; prepared.layout = detail::raster_layout(frequency_axis, selection->range, selection->count(), time_axis, time_range, rows, frequency_layout.orientation, time_layout.orientation); if (prepared.canvas.empty() || !prepared.layout.valid()) return; prepared.source_first = selection->first; prepared.pixels.assign(static_cast(prepared.layout.width) * prepared.layout.height, 0); if (state.tooltip_enabled && tooltip.active && prepared.layout.target.contains(tooltip.position)) { std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency_axis->point_to_coordinate(tooltip.position) << " Hz"; prepared.tooltip_text = text.str(); prepared.tooltip_box = {tooltip.position.x + 8.0, tooltip.position.y + 8.0, 110.0, 24.0}; } prepared.valid = true; } +void Waterfall::Private::prepare_frame(Object* object) { const auto& state = object->template read_prop(); const auto& frequency_layout = frequency_axis->template read_prop(); const auto& time_layout = time_axis->template read_prop(); prepared = {}; prepared.canvas = scene->template read_prop().viewport; if (state.rows.empty()) return; const auto shortest = std::min_element(state.rows.begin(), state.rows.end(), [](const Waterfall_Row& left, const Waterfall_Row& right) { return left.values.size() < right.values.size(); }); const std::size_t available = shortest->values.size(); const int source_columns = static_cast(state.frequency_bin_count ? std::min(state.frequency_bin_count, available) : available); const auto selection = detail::raster_axis_selection(state.frequency_range, frequency_axis->coordinate_range(), source_columns, state.visible_range_only); if (!selection) return; const int time_slots = std::max(2, time_axis->template read_prop().visible_count); const Axis_Range time_range = time_axis->coordinate_range(); prepared.layout = detail::raster_layout(frequency_axis, selection->range, selection->count(), time_axis, time_range, time_slots, frequency_layout.orientation, time_layout.orientation); if (prepared.canvas.empty() || !prepared.layout.valid()) return; prepared.source_first = selection->first; prepared.pixels.assign(static_cast(prepared.layout.width) * prepared.layout.height, 0); const Axis_Coordinate direction = time_range.length() < 0.0 ? -1.0 : 1.0; const Axis_Coordinate first_center = time_range.origin + direction * 0.5; for (std::size_t source = 0; source < state.rows.size(); ++source) { const int slot = static_cast(std::llround((static_cast(state.rows[source].tick) - first_center) / direction)); if (slot >= 0 && slot < time_slots) prepared.rows.push_back({source, slot}); } if (prepared.rows.empty()) return; if (state.tooltip_enabled && tooltip.active && prepared.layout.target.contains(tooltip.position)) { std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency_axis->point_to_coordinate(tooltip.position) << " Hz"; prepared.tooltip_text = text.str(); prepared.tooltip_box = {tooltip.position.x + 8.0, tooltip.position.y + 8.0, 110.0, 24.0}; } prepared.valid = true; } template -void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const std::size_t cells = static_cast(columns) * state.rows.size(); const auto [first, last] = detail::raster_partition_range(cells, index, graph_partition_count); for (std::size_t cell = first; cell < last; ++cell) { const std::size_t row_index = cell / static_cast(columns); const int column = static_cast(cell % static_cast(columns)); const auto& values = state.rows[row_index].values; const std::size_t source = static_cast(prepared.source_first + column); prepared.pixels[prepared.layout.index(column, static_cast(row_index))] = premultiply(state.color_map.sample(detail::normalized_plot_value(values[source], state.power_range))); } } +void Waterfall::Private::prepare_partition(Object* object, Plot_Partition_Count index) { if (!prepared.valid) return; const auto& state = object->template read_prop(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; const std::size_t cells = static_cast(columns) * prepared.rows.size(); const auto [first, last] = detail::raster_partition_range(cells, index, graph_partition_count); for (std::size_t cell = first; cell < last; ++cell) { const auto& row = prepared.rows[cell / static_cast(columns)]; const int column = static_cast(cell % static_cast(columns)); const auto& values = state.rows[row.source].values; const std::size_t source = static_cast(prepared.source_first + column); prepared.pixels[prepared.layout.index(column, row.slot)] = premultiply(state.color_map.sample(detail::normalized_plot_value(values[source], state.power_range))); } } template void Waterfall::Private::paint_frame(Object* object) { const auto& state = object->template read_prop(); auto& cache = this->paint_surface(); if (!prepared.valid) return; detail::Painter painter(cache, prepared.canvas); detail::paint_raster(painter, prepared.layout, prepared.pixels, state.interpolation_mode); if (!prepared.tooltip_text.empty()) { painter.rect(prepared.tooltip_box, Pen{state.tooltip_text_pen.color}, state.tooltip_background_brush); painter.text({prepared.tooltip_box.x + 4.0, prepared.tooltip_box.y + 3.0}, prepared.tooltip_text, state.tooltip_font, state.tooltip_text_pen); } } template void Waterfall::Private::handle_event(Object* object, const Event& event) { if (detail::update_hover_tooltip(tooltip, event)) object->template mark_dirty(); } template void Waterfall::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } -template void Waterfall::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { auto& state = pending_states.template get(); const auto& prop = static_cast(*current_prop); state.row_count = prop.rows.size(); state.stored_point_count = 0; for (const auto& row : prop.rows) state.stored_point_count += row.values.size(); state.rendered_cell_count = prepared.valid ? prepared.pixels.size() : 0; } +template void Waterfall::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { auto& state = pending_states.template get(); const auto& prop = static_cast(*current_prop); state.row_count = prop.rows.size(); state.stored_point_count = 0; for (const auto& row : prop.rows) state.stored_point_count += row.values.size(); const int columns = prepared.layout.first_horizontal ? prepared.layout.width : prepared.layout.height; state.rendered_cell_count = prepared.valid ? prepared.rows.size() * static_cast(columns) : 0; } template -const Waterfall::Private::Dispatch& Waterfall::Private::dispatch_for() { static const Dispatch value{[](Root* root, Plot_Time_Tick tick, std::span values) { auto* object = static_cast(root); auto& data = static_cast(*object->d); const std::size_t row_limit = static_cast(std::max(2, data.time_axis->template read_prop().visible_count)); object->template update_prop<&Prop::rows>([=](Prop_Access props) { auto& state = props.template get(); state.rows.push_back({tick, {values.begin(), values.end()}}); while (state.rows.size() > row_limit) state.rows.erase(state.rows.begin()); }); }, [](Root* root, Time_Of_Day time, std::span values) { auto* object = static_cast(root); auto& data = static_cast(*object->d); data.dispatch->append(root, data.time_axis->append_time(time), values); }, [](const Root* root) { return static_cast(root)->template read_prop().rows.size(); }, [](const Root* root) { const auto& rows = static_cast(root)->template read_prop().rows; std::size_t count{}; for (const auto& row : rows) count += row.values.size(); return count; }, [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); return data.prepared.valid ? data.prepared.pixels.size() : 0; }}; return value; } +const Waterfall::Private::Dispatch& Waterfall::Private::dispatch_for() { static const Dispatch value{[](Root* root, Plot_Time_Tick tick, std::span values) { auto* object = static_cast(root); auto& data = static_cast(*object->d); const std::size_t row_limit = static_cast(std::max(2, data.time_axis->template read_prop().visible_count)); object->template update_prop<&Prop::rows>([=](Prop_Access props) { auto& state = props.template get(); state.rows.push_back({tick, {values.begin(), values.end()}}); while (state.rows.size() > row_limit) state.rows.erase(state.rows.begin()); }); }, [](const Root* root) { return static_cast(root)->template read_prop().rows.size(); }, [](const Root* root) { const auto& rows = static_cast(root)->template read_prop().rows; std::size_t count{}; for (const auto& row : rows) count += row.values.size(); return count; }, [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); return data.prepared.valid ? data.prepared.pixels.size() : 0; }}; return value; } template void Waterfall::Private::bind_private_crtp(Object* object) { Prev_Private::bind_private_crtp(object); dispatch = &dispatch_for(); } inline void Waterfall::Private::bind_sources(Frequency_Object* frequency_axis_value, Time_Object* time_axis_value) { frequency_axis = frequency_axis_value; time_axis = time_axis_value; } } diff --git a/render_2D/render_2D/plottable/common/Curve_Plot.cpp b/render_2D/render_2D/plottable/common/Curve_Plot.cpp index 68270e7..ec8e399 100644 --- a/render_2D/render_2D/plottable/common/Curve_Plot.cpp +++ b/render_2D/render_2D/plottable/common/Curve_Plot.cpp @@ -72,9 +72,9 @@ Rect_F map_plot_rect(const Abs_Axis* coordinate_axis, Axis_Range coordinate_rang const Point_F second = map_plot_point(coordinate_axis, coordinate_range.target, value_axis, value_range.target, coordinate_orientation); return Rect_F{first.x, first.y, second.x - first.x, second.y - first.y}.normalized(); } -Curve_Prepared prepare_curve(std::span values, Axis_Range domain, Line_Interpolation_Mode mode, bool visible_only, Axis_Range visible_coordinate_range, Axis_Range value_range, const Abs_Axis* coordinate_axis, const Abs_Axis* value_axis, Axis_Orientation coordinate_orientation, Axis_Orientation value_orientation) { +Curve_Prepared prepare_curve(std::span source, bool visible_only, Axis_Range visible_coordinate_range, Axis_Range value_range, const Abs_Axis* coordinate_axis, const Abs_Axis* value_axis, Axis_Orientation coordinate_orientation, Axis_Orientation value_orientation) { Curve_Prepared result; - auto samples = interpolate_curve(values, domain, mode); + std::vector samples{source.begin(), source.end()}; if (visible_only) samples = visible_curve_samples(std::move(samples), visible_coordinate_range); for (const auto& sample : samples) if (std::isfinite(sample.coordinate) && std::isfinite(sample.value)) result.points.push_back(map_plot_point(coordinate_axis, sample.coordinate, value_axis, sample.value, coordinate_orientation)); if (result.points.size() < 2) return result; @@ -84,6 +84,10 @@ Curve_Prepared prepare_curve(std::span values, Axis_Range doma result.fill.reserve(result.points.size() + 2); result.fill.push_back(first); result.fill.insert(result.fill.end(), result.points.begin(), result.points.end()); result.fill.push_back(last); return result; } +Curve_Prepared prepare_curve(std::span values, Axis_Range domain, Line_Interpolation_Mode mode, bool visible_only, Axis_Range visible_coordinate_range, Axis_Range value_range, const Abs_Axis* coordinate_axis, const Abs_Axis* value_axis, Axis_Orientation coordinate_orientation, Axis_Orientation value_orientation) { + const auto samples = interpolate_curve(values, domain, mode); + return prepare_curve(samples, visible_only, visible_coordinate_range, value_range, coordinate_axis, value_axis, coordinate_orientation, value_orientation); +} void paint_curve(Painter& painter, const Curve_Prepared& curve, const Pen& pen, const Brush& brush) { if (curve.points.size() < 2) return; if (brush.enabled()) painter.polygon(curve.fill, Pen{.style = Line_Style::none}, brush); diff --git a/render_2D/render_2D/plottable/common/Curve_Plot.hpp b/render_2D/render_2D/plottable/common/Curve_Plot.hpp index 1dcecaa..e8a781f 100644 --- a/render_2D/render_2D/plottable/common/Curve_Plot.hpp +++ b/render_2D/render_2D/plottable/common/Curve_Plot.hpp @@ -24,6 +24,7 @@ struct Curve_Partition_Range { [[nodiscard]] std::vector visible_curve_samples(std::vector samples, Axis_Range visible_range); [[nodiscard]] Point_F map_plot_point(const Abs_Axis* coordinate_axis, Plot_Coordinate coordinate, const Abs_Axis* value_axis, Plot_Value value, Axis_Orientation coordinate_orientation); [[nodiscard]] Rect_F map_plot_rect(const Abs_Axis* coordinate_axis, Axis_Range coordinate_range, const Abs_Axis* value_axis, Axis_Range value_range, Axis_Orientation coordinate_orientation); +[[nodiscard]] Curve_Prepared prepare_curve(std::span samples, bool visible_only, Axis_Range visible_coordinate_range, Axis_Range value_range, const Abs_Axis* coordinate_axis, const Abs_Axis* value_axis, Axis_Orientation coordinate_orientation, Axis_Orientation value_orientation); [[nodiscard]] Curve_Prepared prepare_curve(std::span values, Axis_Range domain, Line_Interpolation_Mode mode, bool visible_only, Axis_Range visible_coordinate_range, Axis_Range value_range, const Abs_Axis* coordinate_axis, const Abs_Axis* value_axis, Axis_Orientation coordinate_orientation, Axis_Orientation value_orientation); void paint_curve(Painter& painter, const Curve_Prepared& curve, const Pen& pen, const Brush& brush = {}); } diff --git a/render_2D/render_2D/scene/Render_Scene_2D.cpp b/render_2D/render_2D/scene/Render_Scene_2D.cpp index 8ae0339..ecb5404 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.cpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.cpp @@ -5,8 +5,8 @@ bool Render_Scene_2D::Prop::operator==(const Prop&) const = default; void Render_Scene_2D::render() { static_cast(*d).dispatch->render(this); } void Render_Scene_2D::set_frame_callback(Frame_Callback callback) { static_cast(*d).dispatch->set_frame_callback(this, std::move(callback)); } -void Render_Scene_2D::dispatch_event(const Event& event) { - static_cast(*d).dispatch->dispatch_event(this, event); +void Render_Scene_2D::dispatch_event(std::unique_ptr event) { + static_cast(*d).dispatch->push_event(this, std::move(event)); } void Render_Scene_2D::activate_view() { static_cast(*d).dispatch->set_active(this, true); diff --git a/render_2D/render_2D/scene/Render_Scene_2D.hpp b/render_2D/render_2D/scene/Render_Scene_2D.hpp index dcc513a..36a9ad0 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.hpp +++ b/render_2D/render_2D/scene/Render_Scene_2D.hpp @@ -3,6 +3,7 @@ #include "../render/Blend2D_Cache.hpp" #include #include +#include namespace aethera::render_2d { struct Scene_Color_Cache_Tag {}; /* 执行二维 Renderable 图、合成颜色层并发布最终像素帧。 */ @@ -38,8 +39,8 @@ struct Render_Scene_2D : Def event); /* 激活后 render 才会执行。 */ void activate_view(); /* 停止后续 render 调用,不清除最后一帧。 */ diff --git a/render_2D/render_2D/scene/Render_Scene_2D.ipp b/render_2D/render_2D/scene/Render_Scene_2D.ipp index f900397..805bfb2 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.ipp +++ b/render_2D/render_2D/scene/Render_Scene_2D.ipp @@ -34,7 +34,7 @@ std::expected, Dependency_Graph_Error> Render_Scene_2D:: struct Render_Scene_2D::Private : Prev_Private { using Render_Run = void (*)(Root*); using Callback_Run = void (*)(Root*, Frame_Callback); - using Event_Run = void (*)(Root*, const Event&); + using Event_Run = void (*)(Root*, std::unique_ptr); using Active_Run = void (*)(Root*, bool); using Frame_View_Run = Image_View (*)(const Root*); struct Paint_Node { @@ -49,10 +49,14 @@ struct Render_Scene_2D::Private : Prev_Private { std::vector members{}; /* 本缓存覆盖的根及无冲突后继节点。 */ bool rebuild{}; /* 本轮是否因任一成员失效而整体重绘。 */ }; + struct Event_Target { + Renderable_2D_Base* object{}; /* 当前 Paint 图中的事件候选对象;Scene 不拥有。 */ + Renderable_2D_Base::Private* private_data{}; /* 候选对象的二维能力层;仅在本次 Prepare 分发期间有效。 */ + }; struct Dispatch { Render_Run render; /* 执行最终 Scene 并合成颜色层。 */ Callback_Run set_frame_callback; /* 安装最终完成帧回调。 */ - Event_Run dispatch_event; /* 向最终 Scene 当前 Paint 图派发事件。 */ + Event_Run push_event; /* 向最终 Scene 的无锁待处理批次转移事件所有权。 */ Active_Run set_active; /* 修改最终 Scene 的视图活动状态。 */ Frame_View_Run frame_view; /* 访问最终 Scene 已合成的写侧帧。 */ }; @@ -71,10 +75,9 @@ struct Render_Scene_2D::Private : Prev_Private { void after_advance(Object* object, Prop* pending_prop, State_Access pending_states, const Prop* current_prop, State_Access current_states); /* 每帧在 Prepare 完成后计算组级 dirty,并为所有二维节点指定唯一绘制目标。 */ template void prepare_paint_targets(Object* object, Blend2D_Cache& frame, Size viewport); + /* Prepare 开始时交换 Scene 事件批次,按区域、Paint 逆序和最近目标规则逐事件完成接受链。 */ + template void dispatch_events(Object* object, Size viewport); template void render(Object* object); - /* CRTP 业务实现:按 Paint 图拓扑逆序派发事件。 */ - template - void dispatch_event(Object* object, const Event& event); template [[nodiscard]] static const Dispatch& dispatch_for(); /* CRTP 覆盖:Builder 挂接最终 Private 后安装二维 Scene 的无虚函数业务分派。 */ @@ -232,6 +235,7 @@ void Render_Scene_2D::Private::process(Object* object, Callback&& callback) if (state.viewport.empty()) { return; } + dispatch_events(object, state.viewport); object->template current_dependency_graph().for_each([](const Dependency_Graph::Node& node) { if (!node.object->template take_dirty()) return; node.object->template mark_dirty(); @@ -271,20 +275,30 @@ void Render_Scene_2D::Private::render(Object* object) { rendering = false; } template -void Render_Scene_2D::Private::dispatch_event(Object* object, const Event& event) { - std::vector order; - const auto graph = object->template current_dependency_graph(); - const auto result = graph.for_each_topological_view([&](const auto& view, const Dependency_Graph::Node& node) { - if (auto* renderable = view.object(node)) order.push_back(renderable); +void Render_Scene_2D::Private::dispatch_events(Object* object, Size viewport) { + this->consume_events([&](const std::shared_ptr& owned_event) { + const Event& event = *owned_event; + const auto* pointer = dynamic_cast(&event); + std::vector order; + const auto graph = object->template current_dependency_graph(); + const auto result = graph.for_each_topological_view([&](const auto& view, const Dependency_Graph::Node& node) { + auto* renderable = view.object(node); auto* data = view.private_data(node); + if (!renderable || !data || !data->event_run) return; + if (pointer) { + const Rect_F region = data->event_region_run(node.object, viewport); + if (!region.contains({pointer->position_x(), pointer->position_y()})) return; + } + order.push_back({renderable, data}); + }); + if (!result) throw std::logic_error("render scene paint graph became invalid during event dispatch"); + std::vector delivery_order(order.rbegin(), order.rend()); + detail::Nearest_Event_Target_Rule::apply(delivery_order, [&](const Event_Target& target) { return target.object->event_routing_distance(event); }); + for (const auto& target : delivery_order) { + if (event.is_accepted()) break; + target.private_data->event_run(target.object, event); + } + return true; }); - if (!result) throw std::logic_error("render scene paint graph became invalid during event dispatch"); - std::vector delivery_order(order.rbegin(), order.rend()); - detail::Nearest_Event_Target_Rule::apply(delivery_order, - [&](const Renderable* target) { return target->event_routing_distance(event); }); - for (auto* target : delivery_order) { - if (event.is_accepted()) break; - target->dispatch_event(event); - } } template const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for() { @@ -297,9 +311,9 @@ const Render_Scene_2D::Private::Dispatch& Render_Scene_2D::Private::dispatch_for auto* object = static_cast(root); static_cast(*object->d).frame_callback = std::move(callback); }, - [](Root* root, const Event& event) { + [](Root* root, std::unique_ptr event) { auto* object = static_cast(root); - static_cast(*object->d).dispatch_event(object, event); + static_cast(*object->d).push_event(std::move(event)); }, [](Root* root, bool active) { static_cast(root)->template set<&Prop::view_active>(active); diff --git a/render_2D/tests/Axis_Test.cpp b/render_2D/tests/Axis_Test.cpp index fd8df9d..e212192 100644 --- a/render_2D/tests/Axis_Test.cpp +++ b/render_2D/tests/Axis_Test.cpp @@ -94,27 +94,33 @@ TEST(axis_render, scene_prepares_and_paints_uncached_axis_into_frame) { } TEST(axis_event, numeric_axis_wheel_zoom_and_drag_update_authoritative_range) { using Object = Impl; + using Scene_Object = Impl; + initialize_runtime(2); auto axis = build_axis(); + auto scene = build_axis(); axis->set<&Abs_Axis::Prop::position>(Point_F{0.0, 0.0}); axis->set<&Abs_Axis::Prop::pixel_length>(100.0); axis->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 10.0}); - axis->advance(); - Wheel_Event wheel; - wheel.position = {50.0, 0.0}; - wheel.angle_delta_y = 120.0; - axis->dispatch_event(wheel); - EXPECT_TRUE(wheel.is_accepted()); - axis->advance(); + scene->set<&Render_Scene_2D::Prop::viewport>(Size{120, 64}); + ASSERT_TRUE((scene->edit_dependency_graph([&](auto& prepare, auto& paint) { + prepare.add(axis.get()); paint.add(axis.get()); + }).has_value())); + scene->activate_view(); + scene->render(); + auto wheel = std::make_unique(); + wheel->position = {50.0, 0.0}; + wheel->angle_delta_y = 120.0; + scene->dispatch_event(std::move(wheel)); + scene->render(); EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.5, 9.5})); - Pointer_Event press(Event_Type::pointer_press); - press.position = {50.0, 0.0}; - press.button = Mouse_Button::left; - axis->dispatch_event(press); - Pointer_Event move(Event_Type::pointer_move); - move.position = {60.0, 0.0}; - axis->dispatch_event(move); - EXPECT_TRUE(move.is_accepted()); - axis->advance(); + auto press = std::make_unique(Event_Type::pointer_press); + press->position = {50.0, 0.0}; + press->button = Mouse_Button::left; + scene->dispatch_event(std::move(press)); + auto move = std::make_unique(Event_Type::pointer_move); + move->position = {60.0, 0.0}; + scene->dispatch_event(std::move(move)); + scene->render(); EXPECT_EQ(axis->coordinate_range(), (Axis_Range{-0.4, 8.6})); } TEST(axis_event, scene_routes_pointer_interaction_to_the_nearest_axis_segment) { @@ -139,18 +145,18 @@ TEST(axis_event, scene_routes_pointer_interaction_to_the_nearest_axis_segment) { scene->activate_view(); scene->render(); - Wheel_Event near_vertical; - near_vertical.position = {22.0, 80.0}; - near_vertical.angle_delta_y = 120.0; - scene->dispatch_event(near_vertical); + auto near_vertical = std::make_unique(); + near_vertical->position = {22.0, 80.0}; + near_vertical->angle_delta_y = 120.0; + scene->dispatch_event(std::move(near_vertical)); scene->render(); EXPECT_DOUBLE_EQ(horizontal->coordinate_range().size(), 10.0); EXPECT_DOUBLE_EQ(vertical->coordinate_range().size(), 9.0); - Wheel_Event near_horizontal; - near_horizontal.position = {120.0, 178.0}; - near_horizontal.angle_delta_y = 120.0; - scene->dispatch_event(near_horizontal); + auto near_horizontal = std::make_unique(); + near_horizontal->position = {120.0, 178.0}; + near_horizontal->angle_delta_y = 120.0; + scene->dispatch_event(std::move(near_horizontal)); scene->render(); EXPECT_DOUBLE_EQ(horizontal->coordinate_range().size(), 9.0); EXPECT_DOUBLE_EQ(vertical->coordinate_range().size(), 9.0); @@ -177,8 +183,23 @@ TEST(axis_time, sample_state_drives_range_lookup_and_label_without_snapshot) { EXPECT_EQ(axis->time_point_count(), 0u); axis->advance(); EXPECT_EQ(axis->time_point_count(), 1u); - EXPECT_EQ(axis->coordinate_range(), (Axis_Range{0.0, 1.0})); + EXPECT_EQ(axis->coordinate_range(), (Axis_Range{-99.5, 0.5})); EXPECT_EQ(axis->tick_to_time(0), (Time_Of_Day{3'723'004})); EXPECT_EQ(axis->tick_label(0.0), "02:03.004"); EXPECT_FALSE(axis->tick_to_time(99).valid()); } +TEST(axis_time, appending_each_time_sample_invalidates_tick_preparation) { + using Object = Impl; + auto axis = build_axis(); + axis->advance(); + axis->template take_dirty(); + EXPECT_FALSE(axis->template dirty()); + EXPECT_EQ(axis->append_time({1'000}), 0); + EXPECT_TRUE(axis->template dirty()); + axis->advance(); + axis->template take_dirty(); + EXPECT_EQ(axis->append_time({2'000}), 1); + EXPECT_TRUE(axis->template dirty()); + axis->advance(); + EXPECT_EQ(axis->coordinate_range(), (Axis_Range{-98.5, 1.5})); +} diff --git a/render_2D/tests/Plottable_Migration_Test.cpp b/render_2D/tests/Plottable_Migration_Test.cpp index 1358069..23b930b 100644 --- a/render_2D/tests/Plottable_Migration_Test.cpp +++ b/render_2D/tests/Plottable_Migration_Test.cpp @@ -56,9 +56,9 @@ TEST(plottable_migration, curve_plots_share_partitioned_rendering) { auto sweep = build_object(frequency.get(), power.get()); trace->set<&Frequency_Trace::Prop::partition_mode>(Plot_Partition_Mode::fixed); trace->set<&Frequency_Trace::Prop::partition_count>(2u); - trace->append_sample(0, 10.0); - trace->append_sample(1, 50.0); - trace->append_sample(2, 90.0); + trace->append_sample(time->append_time({1'000}), 10.0); + trace->append_sample(time->append_time({2'000}), 50.0); + trace->append_sample(time->append_time({3'000}), 90.0); sweep->set<&Sweep_Spectrum::Prop::frequency_range>(Axis_Range{0.0, 100.0}); sweep->set<&Sweep_Spectrum::Prop::partition_mode>(Plot_Partition_Mode::fixed); sweep->set<&Sweep_Spectrum::Prop::partition_count>(2u); @@ -100,8 +100,8 @@ TEST(plottable_migration, raster_plots_share_partitioned_color_blocks) { waterfall->set<&Waterfall::Prop::power_range>(Axis_Range{-100.0, 0.0}); const std::array row{-90.0, -60.0, -30.0, -10.0}; glow->append_spectrum(row); - waterfall->append_row(0, row); - waterfall->append_row(1, row); + waterfall->append_row(time->append_time({1'000}), row); + waterfall->append_row(time->append_time({2'000}), row); auto scene = build_scene(glow.get(), waterfall.get()); scene->set<&Render_Scene_2D::Prop::viewport>(canvas); scene->activate_view(); @@ -158,20 +158,20 @@ TEST(selection_overlay, control_extends_selection_and_plain_click_clears_it) { scene->render(); const auto drag = [&](Point_F first, Point_F second, Keyboard_Modifier modifiers) { - Pointer_Event press(Event_Type::pointer_press); - press.position = first; - press.button = Mouse_Button::left; - press.modifiers = modifiers; - overlay->dispatch_event(press); - Pointer_Event move(Event_Type::pointer_move); - move.position = second; - move.modifiers = modifiers; - overlay->dispatch_event(move); - Pointer_Event release(Event_Type::pointer_release); - release.position = second; - release.button = Mouse_Button::left; - release.modifiers = modifiers; - overlay->dispatch_event(release); + auto press = std::make_unique(Event_Type::pointer_press); + press->position = first; + press->button = Mouse_Button::left; + press->modifiers = modifiers; + scene->dispatch_event(std::move(press)); + auto move = std::make_unique(Event_Type::pointer_move); + move->position = second; + move->modifiers = modifiers; + scene->dispatch_event(std::move(move)); + auto release = std::make_unique(Event_Type::pointer_release); + release->position = second; + release->button = Mouse_Button::left; + release->modifiers = modifiers; + scene->dispatch_event(std::move(release)); scene->render(); }; drag({30.0, 90.0}, {60.0, 60.0}, Keyboard_Modifier::none); @@ -181,3 +181,46 @@ TEST(selection_overlay, control_extends_selection_and_plain_click_clears_it) { drag({120.0, 40.0}, {120.0, 40.0}, Keyboard_Modifier::none); EXPECT_TRUE(overlay->selected_regions().empty()); } + +TEST(selection_overlay, time_axis_selection_keeps_axis_coordinates_while_window_moves) { + using Scene = Impl; + using Time = Impl; + using Numeric = Impl; + using Overlay = Impl; + initialize_runtime(2); + const Size canvas{160, 120}; + auto time = build_object