四个方向 分块渲染

This commit is contained in:
2026-08-12 09:01:25 +08:00
parent b34c0d31b1
commit 9331b4cbf6
24 changed files with 788 additions and 108 deletions
+123 -14
View File
@@ -2,12 +2,26 @@
#include "Heatmap_Utils.h"
#include "Plottable_Real_Time_Data.h"
#include "../render/Blend2D_Cache.h"
#include "../renderable/Render_Partition.h"
#include <algorithm>
#include <array>
#include <deque>
namespace renderive {
namespace detail {
namespace {
using Afterglow_History = Plottable_History_Real_Time_Data<std::vector<double>, std::deque<std::vector<double>>>;
struct Afterglow_Render_Frame {
Adaptive_Render_Partitioner partitioner;
Axis_Raster_Layout layout;
std::vector<double> intensity;
std::vector<Pixel> pixels;
int source_width{};
int source_height{};
int active_partitions{1};
double maximum{1.0};
std::size_t work_size{};
bool valid{};
};
}
struct Afterglow_Control::Impl {
Impl(Afterglow_Control& owner, std::shared_ptr<Frequency_Axis> frequency, std::shared_ptr<Axis> power)
@@ -15,6 +29,7 @@ struct Afterglow_Control::Impl {
std::shared_ptr<Frequency_Axis> frequency_axis;
std::shared_ptr<Axis> power_axis;
Afterglow_History history;
Afterglow_Render_Frame render_frame;
};
Afterglow_Control::Afterglow_Control(Plot_Core& plot, const Afterglow_Properties& properties, std::shared_ptr<Frequency_Axis> frequency_axis, std::shared_ptr<Axis> power_axis)
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(*this, std::move(frequency_axis), std::move(power_axis))) {}
@@ -49,9 +64,47 @@ void Afterglow_Control::append_spectrum(std::pmr::vector<double>&& values) {
void Afterglow_Control::publish() {
publish_properties();
}
void Afterglow_Control::paint(Painter& painter, const Render_State_View& view) {
void Afterglow_Control::build_paint_task_graph(Renderable_Task_Graph& graph) {
const auto prepare = graph.emplace([this](const Scene_Render_Context&) {
prepare_render_frame(render_state_view());
}, "prepare afterglow");
std::array<Renderable_Task_Graph::Task, maximum_render_partitions> accumulation;
std::array<Renderable_Task_Graph::Task, maximum_render_partitions> coloring;
for (int index = 0; index < maximum_render_partitions; ++index) {
accumulation[static_cast<std::size_t>(index)] = graph.emplace(
[this, index](const Scene_Render_Context&) {
accumulate_partition(render_state_view(), index);
},
"accumulate afterglow partition");
graph.precede(prepare, accumulation[static_cast<std::size_t>(index)]);
}
const auto normalize = graph.emplace([this](const Scene_Render_Context&) {
normalize_render_frame();
}, "normalize afterglow");
for (const auto task : accumulation)
graph.precede(task, normalize);
for (int index = 0; index < maximum_render_partitions; ++index) {
coloring[static_cast<std::size_t>(index)] = graph.emplace(
[this, index](const Scene_Render_Context&) {
color_partition(render_state_view(), index);
},
"color afterglow partition");
graph.precede(normalize, coloring[static_cast<std::size_t>(index)]);
}
const auto compose = add_paint_task(
graph, "compose afterglow",
[this](Painter& painter, const Render_State_View& view) { paint(painter, view); });
for (const auto task : coloring)
graph.precede(task, compose);
}
void Afterglow_Control::prepare_render_frame(const Render_State_View& view) {
const auto& state = render_properties(view);
const auto& history = view.get(impl_->history);
auto& output = impl_->render_frame;
output.valid = false;
output.work_size = 0;
if (history.empty())
return;
const int width = std::min(state.frequency_point_size.get(), static_cast<int>(history.back().size()));
@@ -60,27 +113,83 @@ void Afterglow_Control::paint(Painter& painter, const Render_State_View& view) {
: std::max(1, static_cast<int>(impl_->power_axis->transform(view).pixel_length));
if (width <= 0 || height <= 0)
return;
std::vector<double> intensity(static_cast<std::size_t>(width) * height);
const auto layout = axis_raster_layout(impl_->frequency_axis->transform(view),
impl_->power_axis->transform(view),
state.frequency_range, state.power_range,
width, height);
if (!layout.valid())
return;
output.layout = layout;
output.source_width = width;
output.source_height = height;
output.work_size = static_cast<std::size_t>(width) * height;
output.intensity.resize(output.work_size);
output.pixels.resize(output.work_size);
output.active_partitions =
output.partitioner.begin(state.partition_count.get(), output.work_size);
output.valid = true;
}
void Afterglow_Control::accumulate_partition(const Render_State_View& view,
int partition_index) {
auto& output = impl_->render_frame;
if (!output.valid || partition_index >= output.active_partitions)
return;
const auto& state = render_properties(view);
const auto& history = view.get(impl_->history);
const auto columns = render_partition_range(static_cast<std::size_t>(output.source_width),
partition_index, output.active_partitions);
for (std::size_t x = columns.first; x < columns.last; ++x)
for (int y = 0; y < output.source_height; ++y)
output.intensity[static_cast<std::size_t>(y) * output.source_width + x] = 0.0;
double weight = 1.0;
const double decay = 1.0 - state.attenuation_rate.get();
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) {
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)
intensity[static_cast<std::size_t>(y + 1) * width + x] += weight * 0.35;
const std::size_t count = std::min<std::size_t>(output.source_width, iterator->size());
for (std::size_t x = columns.first; x < std::min(columns.last, count); ++x) {
const double normalized = normalized_value((*iterator)[x], state.power_range);
const int y = std::clamp(static_cast<int>(normalized * (output.source_height - 1)),
0, output.source_height - 1);
output.intensity[static_cast<std::size_t>(y) * output.source_width + x] += weight;
if (state.interpolate && y + 1 < output.source_height)
output.intensity[static_cast<std::size_t>(y + 1) * output.source_width + x] += weight * 0.35;
}
weight *= decay;
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)
pixels[index] = state.color_map.at_normalized(intensity[index] / maximum);
painter.heatmap(mapped_rect(impl_->frequency_axis->transform(view), impl_->power_axis->transform(view), state.frequency_range, state.power_range), width, height, pixels, Image_Interpolation_Mode::Bilinear);
}
void Afterglow_Control::normalize_render_frame() {
auto& output = impl_->render_frame;
if (output.valid)
output.maximum = std::max(1.0, *std::max_element(output.intensity.begin(),
output.intensity.end()));
}
void Afterglow_Control::color_partition(const Render_State_View& view,
int partition_index) {
auto& output = impl_->render_frame;
if (!output.valid || partition_index >= output.active_partitions)
return;
const auto& state = render_properties(view);
const auto range = render_partition_range(output.work_size, partition_index,
output.active_partitions);
for (std::size_t cell = range.first; cell < range.last; ++cell) {
const int y = static_cast<int>(cell / static_cast<std::size_t>(output.source_width));
const int x = static_cast<int>(cell % static_cast<std::size_t>(output.source_width));
output.pixels[output.layout.index(x, y, output.source_width, output.source_height)] =
state.color_map.at_normalized(output.intensity[cell] / output.maximum);
}
}
void Afterglow_Control::paint(Painter& painter, const Render_State_View& view) {
auto& output = impl_->render_frame;
if (!output.valid)
return;
painter.heatmap(output.layout.target, output.layout.width, output.layout.height,
output.pixels, Image_Interpolation_Mode::Bilinear);
output.partitioner.finish(output.work_size);
}
}
}
+6
View File
@@ -9,6 +9,7 @@ struct Afterglow_Properties {
Range power_range{0.0, 10.0};
Nonnegative_Count frequency_point_size;
Nonnegative_Count power_point_size;
Nonnegative_Count partition_count;
bool interpolate = true;
Unit_Interval attenuation_rate{0.2};
Color_Map color_map;
@@ -29,9 +30,14 @@ public:
}
protected:
void paint(Painter& painter, const Render_State_View& state) override;
void build_paint_task_graph(Renderable_Task_Graph& graph) override;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
void prepare_render_frame(const Render_State_View& state);
void accumulate_partition(const Render_State_View& state, int partition_index);
void normalize_render_frame();
void color_partition(const Render_State_View& state, int partition_index);
void publish() override;
};
}
@@ -1,5 +1,6 @@
#include "Constellation_Diagram.h"
#include "Plottable_Real_Time_Data.h"
#include "Heatmap_Utils.h"
#include "../render/Blend2D_Cache.h"
#include <algorithm>
#include <chrono>
@@ -58,13 +59,17 @@ void Constellation_Diagram_Control::paint(Painter& painter, const Render_State_V
for(int index = 0; index < count; ++index) {
const double angle = state.phase_offset_radians + 2.0 * std::numbers::pi * index / count;
const PointF point{state.i_range.center() + std::cos(angle) * radius, state.q_range.center() + std::sin(angle) * radius};
painter.circle({x.coord_to_pixel(point.x), y.coord_to_pixel(point.y)}, 3.0, Pen{state.anchor_color}, Brush{state.anchor_color, Brush_Style::Solid});
painter.circle(mapped_point(x, point.x, y, point.y), 3.0,
Pen{state.anchor_color},
Brush{state.anchor_color, Brush_Style::Solid});
}
const auto cutoff = std::chrono::steady_clock::now() - std::chrono::milliseconds(state.point_lifetime_ms.get());
for(const auto& value : points) {
if(value.time < cutoff)
continue;
painter.circle({x.coord_to_pixel(value.point.x), y.coord_to_pixel(value.point.y)}, 2.0, Pen{state.point_color}, Brush{state.point_color, Brush_Style::Solid});
painter.circle(mapped_point(x, value.point.x, y, value.point.y), 2.0,
Pen{state.point_color},
Brush{state.point_color, Brush_Style::Solid});
}
}
}
+2 -2
View File
@@ -1,6 +1,7 @@
#pragma once
#include "../axis/Axis.h"
#include "Heatmap_Utils.h"
#include <algorithm>
#include <cmath>
@@ -141,8 +142,7 @@ inline std::vector<PointF> curve_points(std::span<const double> values,
points.reserve(samples.size());
for (const auto& sample : samples) {
if (std::isfinite(sample.coordinate) && std::isfinite(sample.value))
points.push_back({x_axis.coord_to_pixel(sample.coordinate),
y_axis.coord_to_pixel(sample.value)});
points.push_back(mapped_point(x_axis, sample.coordinate, y_axis, sample.value));
}
return points;
}
+2 -1
View File
@@ -1,5 +1,6 @@
#include "Frequency_Trace.h"
#include "Plottable_Real_Time_Data.h"
#include "Heatmap_Utils.h"
#include "../render/Blend2D_Cache.h"
#include <algorithm>
#include <deque>
@@ -47,7 +48,7 @@ void Frequency_Trace_Control::paint(Painter& painter, const Render_State_View& v
std::vector<PointF> points;
points.reserve(samples.size());
for(const auto& [tick, value] : samples)
points.push_back({x.coord_to_pixel(tick), y.coord_to_pixel(value)});
points.push_back(mapped_point(x, tick, y, value));
painter.polyline(points, state.pen);
}
}
+71 -6
View File
@@ -4,12 +4,77 @@
#include <cmath>
#include <optional>
namespace renderive::detail {
inline RectF mapped_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical, Range horizontal_range, Range vertical_range) {
const double x1 = horizontal.coord_to_pixel(horizontal_range.origin);
const double x2 = horizontal.coord_to_pixel(horizontal_range.target);
const double y1 = vertical.coord_to_pixel(vertical_range.origin);
const double y2 = vertical.coord_to_pixel(vertical_range.target);
return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)};
inline bool axes_are_orthogonal(const Axis_Transform& first,
const Axis_Transform& second) noexcept {
return first.orientation != second.orientation;
}
inline PointF mapped_point(const Axis_Transform& first,
double first_coordinate,
const Axis_Transform& second,
double second_coordinate) noexcept {
if (!axes_are_orthogonal(first, second))
return {};
const double first_pixel = first.coord_to_pixel(first_coordinate);
const double second_pixel = second.coord_to_pixel(second_coordinate);
return first.orientation == Orientation::Horizontal
? PointF{first_pixel, second_pixel}
: PointF{second_pixel, first_pixel};
}
inline RectF mapped_rect(const Axis_Transform& first,
const Axis_Transform& second,
Range first_range,
Range second_range) noexcept {
if (!axes_are_orthogonal(first, second))
return {};
const PointF origin = mapped_point(first, first_range.origin, second, second_range.origin);
const PointF target = mapped_point(first, first_range.target, second, second_range.target);
return {std::min(origin.x, target.x), std::min(origin.y, target.y),
std::abs(target.x - origin.x), std::abs(target.y - origin.y)};
}
struct Axis_Raster_Layout {
int width{};
int height{};
RectF target;
bool first_reversed{};
bool second_reversed{};
bool first_is_horizontal{};
[[nodiscard]] bool valid() const noexcept {
return width > 0 && height > 0 && !target.empty();
}
[[nodiscard]] std::size_t index(int first, int second,
int first_count, int second_count) const noexcept {
if (first_reversed)
first = first_count - 1 - first;
if (second_reversed)
second = second_count - 1 - second;
const int x = first_is_horizontal ? first : second;
const int y = first_is_horizontal ? second : first;
return static_cast<std::size_t>(y) * width + x;
}
};
inline Axis_Raster_Layout axis_raster_layout(const Axis_Transform& first,
const Axis_Transform& second,
Range first_range,
Range second_range,
int first_count,
int second_count) noexcept {
if (!axes_are_orthogonal(first, second) || first_count <= 0 || second_count <= 0)
return {};
const bool first_horizontal = first.orientation == Orientation::Horizontal;
return {
first_horizontal ? first_count : second_count,
first_horizontal ? second_count : first_count,
mapped_rect(first, second, first_range, second_range),
first.coord_to_pixel(first_range.origin) > first.coord_to_pixel(first_range.target),
second.coord_to_pixel(second_range.origin) > second.coord_to_pixel(second_range.target),
first_horizontal
};
}
inline double normalized_value(double value, Range range) {
if(range.length() == 0.0)
@@ -1,5 +1,6 @@
#include "Selection_Rectangle_Overlay.h"
#include "Plottable_Real_Time_Data.h"
#include "Heatmap_Utils.h"
#include "../render/Blend2D_Cache.h"
#include <algorithm>
#include <cmath>
@@ -14,12 +15,8 @@ struct Selection_Interaction {
PointF selection_current{};
};
using Selection_Interaction_State = Double_State_Strategy<Selection_Interaction_Base, Selection_Interaction>;
RectF axis_content_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) {
const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin);
const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target);
const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin);
const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target);
return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)};
RectF axis_content_rect(const Axis_Transform& first, const Axis_Transform& second) {
return mapped_rect(first, second, first.coordinate_range, second.coordinate_range);
}
}
struct Selection_Rectangle_Overlay_Control::Impl {
@@ -88,9 +85,11 @@ void Selection_Rectangle_Overlay_Control::handle_event(const Event& event) {
return;
const Axis_Transform horizontal = impl_->horizontal_axis->transform();
const Axis_Transform vertical = impl_->vertical_axis->transform();
const RectF region{horizontal.pixel_to_coord(start.x), vertical.pixel_to_coord(start.y),
horizontal.pixel_to_coord(pointer.position.x) - horizontal.pixel_to_coord(start.x),
vertical.pixel_to_coord(pointer.position.y) - vertical.pixel_to_coord(start.y)};
const double first_start = horizontal.point_to_coord(start);
const double second_start = vertical.point_to_coord(start);
const RectF region{first_start, second_start,
horizontal.point_to_coord(pointer.position) - first_start,
vertical.point_to_coord(pointer.position) - second_start};
if(std::abs(region.width) > 1e-9 && std::abs(region.height) > 1e-9)
impl_->regions.update(region.normalized());
event.accept();
@@ -106,7 +105,9 @@ void Selection_Rectangle_Overlay_Control::paint(Painter& painter, const Render_S
const auto horizontal = impl_->horizontal_axis->transform(view);
const auto vertical = impl_->vertical_axis->transform(view);
for(const RectF& region : view.get(impl_->regions)) {
RectF pixels{horizontal.coord_to_pixel(region.x), vertical.coord_to_pixel(region.y), horizontal.coord_to_pixel(region.right()) - horizontal.coord_to_pixel(region.x), vertical.coord_to_pixel(region.bottom()) - vertical.coord_to_pixel(region.y)};
const RectF pixels = mapped_rect(horizontal, vertical,
{region.x, region.right()},
{region.y, region.bottom()});
painter.rect(pixels, state.selection_border_pen, state.selection_brush);
std::ostringstream text;
text << region.width << " x " << region.height;
+149 -27
View File
@@ -1,7 +1,10 @@
#include "Spectrum.h"
#include "Curve_Sampling.h"
#include "Plottable_Real_Time_Data.h"
#include "../render/Blend2D_Cache.h"
#include "../renderable/Render_Partition.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <iomanip>
#include <sstream>
@@ -20,23 +23,56 @@ struct Spectrum_Interaction {
Hover_Tooltip_Runtime tooltip;
};
using Spectrum_Interaction_State = Double_State_Strategy<Spectrum_Interaction_Base, Spectrum_Interaction>;
RectF axes_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) {
const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin);
const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target);
const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin);
const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target);
return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)};
struct Spectrum_Render_Frame {
Adaptive_Render_Partitioner partitioner;
std::array<Blend2D_Color_Cache, maximum_render_partitions> layers;
int active_partitions{1};
std::size_t work_size{};
bool valid{};
};
struct Curve_Partition {
std::span<const double> values;
Range domain;
Range clip_domain;
};
Curve_Partition curve_partition(std::span<const double> values, Range domain,
int partition_index, int partition_count) {
if (values.size() < 2 || partition_count <= 0)
return {};
const std::size_t segment_count = values.size() - 1;
const auto core = render_partition_range(segment_count, partition_index, partition_count);
if (core.first == core.last)
return {};
const std::size_t first = core.first == 0 ? 0 : core.first - 1;
const std::size_t last = std::min(segment_count, core.last + 1);
const auto coordinate = [domain, segment_count](std::size_t index) {
return domain.origin + domain.length() * static_cast<double>(index) /
static_cast<double>(segment_count);
};
return {
values.subspan(first, last - first + 1),
{coordinate(first), coordinate(last)},
{coordinate(core.first), coordinate(core.last)}
};
}
void draw_curve(Painter& painter, std::span<const double> values, Range domain, const Axis_Transform& x_axis, const Axis_Transform& y_axis, bool visible_only, Line_Interpolation_Mode interpolation, const Pen& pen, const Brush& brush) {
auto points = curve_points(values, domain, x_axis, y_axis, visible_only, interpolation);
void draw_curve(Painter& painter, std::span<const double> values, Range domain,
const Axis_Transform& frequency_axis, const Axis_Transform& power_axis,
bool visible_only, Line_Interpolation_Mode interpolation,
const Pen& pen, const Brush& brush) {
auto points = curve_points(values, domain, frequency_axis, power_axis,
visible_only, interpolation);
if(points.size() < 2)
return;
if(brush.enabled()) {
std::vector<PointF> polygon;
polygon.reserve(points.size() + 2);
polygon.push_back({points.front().x, y_axis.coord_to_pixel(y_axis.coordinate_range.target)});
polygon.push_back(mapped_point(frequency_axis, domain.origin, power_axis,
power_axis.coordinate_range.target));
polygon.insert(polygon.end(), points.begin(), points.end());
polygon.push_back({points.back().x, y_axis.coord_to_pixel(y_axis.coordinate_range.target)});
polygon.push_back(mapped_point(frequency_axis, domain.target, power_axis,
power_axis.coordinate_range.target));
painter.polygon(polygon, Pen{.style = Line_Style::None}, brush);
}
painter.polyline(points, pen);
@@ -62,6 +98,7 @@ struct Spectrum_Control::Impl {
Plottable_Latest_Real_Time_Data<Spectrum_Frame> frame;
Spectrum_Interaction_State interaction;
std::mutex frame_update_mutex;
Spectrum_Render_Frame render_frame;
};
Spectrum_Control::Spectrum_Control(Plot_Core& plot, const Spectrum_Properties& properties, std::shared_ptr<Frequency_Axis> frequency_axis, std::shared_ptr<Axis> power_axis)
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(*this, std::move(frequency_axis), std::move(power_axis))) {}
@@ -205,32 +242,116 @@ void Spectrum_Control::publish() {
publish_properties();
impl_->interaction.publish();
}
void Spectrum_Control::build_paint_task_graph(Renderable_Task_Graph& graph) {
const auto prepare = graph.emplace([this](const Scene_Render_Context&) {
prepare_render_frame(render_state_view());
}, "prepare spectrum");
std::array<Renderable_Task_Graph::Task, maximum_render_partitions> partitions;
for (int index = 0; index < maximum_render_partitions; ++index) {
partitions[static_cast<std::size_t>(index)] = graph.emplace(
[this, index](const Scene_Render_Context&) {
render_partition(render_state_view(), index);
},
"paint spectrum partition");
graph.precede(prepare, partitions[static_cast<std::size_t>(index)]);
}
const auto compose = add_paint_task(
graph, "compose spectrum",
[this](Painter& painter, const Render_State_View& view) { paint(painter, view); });
for (const auto task : partitions)
graph.precede(task, compose);
}
void Spectrum_Control::prepare_render_frame(const Render_State_View& view) {
const auto& state = render_properties(view);
const auto& published_frame = view.get(impl_->frame);
auto& output = impl_->render_frame;
const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view);
const Axis_Transform power_axis = impl_->power_axis->transform(view);
output.valid = axes_are_orthogonal(frequency_axis, power_axis);
output.work_size = published_frame ? published_frame->samples.size() : 0;
output.active_partitions =
output.partitioner.begin(state.partition_count.get(), output.work_size);
for (int index = 0; index < output.active_partitions; ++index)
output.layers[static_cast<std::size_t>(index)].clear();
}
void Spectrum_Control::render_partition(const Render_State_View& view,
int partition_index) {
auto& output = impl_->render_frame;
if (!output.valid || partition_index >= output.active_partitions)
return;
const auto& state = render_properties(view);
const auto& published_frame = view.get(impl_->frame);
if (!published_frame)
return;
const Spectrum_Frame& frame = *published_frame;
const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view);
const Axis_Transform power_axis = impl_->power_axis->transform(view);
const auto current = curve_partition(frame.samples, state.frequency_range,
partition_index, output.active_partitions);
if (current.values.empty())
return;
Painter painter(output.layers[static_cast<std::size_t>(partition_index)], viewport_size());
if (!painter)
return;
painter.clip(mapped_rect(frequency_axis, power_axis, current.clip_domain,
power_axis.coordinate_range));
if (state.max_hold_visible) {
const auto maximum = curve_partition(frame.maxima, state.frequency_range,
partition_index, output.active_partitions);
draw_curve(painter, maximum.values, maximum.domain, frequency_axis, power_axis,
state.visible_range_only, state.interpolation_mode,
state.max_pen, state.max_brush);
}
if (state.min_hold_visible) {
const auto minimum = curve_partition(frame.minima, state.frequency_range,
partition_index, output.active_partitions);
draw_curve(painter, minimum.values, minimum.domain, frequency_axis, power_axis,
state.visible_range_only, state.interpolation_mode,
state.min_pen, state.min_brush);
}
draw_curve(painter, current.values, current.domain, frequency_axis, power_axis,
state.visible_range_only, state.interpolation_mode,
state.current_pen, state.current_brush);
}
void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) {
const auto& state = render_properties(view);
const auto& published_frame = view.get(impl_->frame);
const Spectrum_Frame empty_frame;
const Spectrum_Frame& frame = published_frame ? *published_frame : empty_frame;
const auto& interaction = view.get(impl_->interaction);
const Axis_Transform horizontal = impl_->frequency_axis->transform(view);
const Axis_Transform vertical = impl_->power_axis->transform(view);
const RectF content = axes_rect(horizontal, vertical);
const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view);
const Axis_Transform power_axis = impl_->power_axis->transform(view);
const RectF content = mapped_rect(frequency_axis, power_axis,
frequency_axis.coordinate_range,
power_axis.coordinate_range);
if (content.empty())
return;
auto& output = impl_->render_frame;
if(state.sweep_region_visible) {
const double first = horizontal.coord_to_pixel(state.sweep_frequency_range.origin);
const double last = horizontal.coord_to_pixel(state.sweep_frequency_range.target);
painter.rect({std::min(first, last), content.y, std::abs(last - first), content.height}, Pen{.style = Line_Style::None}, state.sweep_region_brush);
painter.rect(mapped_rect(frequency_axis, power_axis, state.sweep_frequency_range,
power_axis.coordinate_range),
Pen{.style = Line_Style::None}, state.sweep_region_brush);
}
if(state.max_hold_visible)
draw_curve(painter, frame.maxima, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.max_pen, state.max_brush);
if(state.min_hold_visible)
draw_curve(painter, frame.minima, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.min_pen, state.min_brush);
draw_curve(painter, frame.samples, state.frequency_range, horizontal, vertical, state.visible_range_only, state.interpolation_mode, state.current_pen, state.current_brush);
for (int index = 0; output.valid && index < output.active_partitions; ++index)
painter.composite(output.layers[static_cast<std::size_t>(index)]);
if(state.middle_frequency_pen.enabled()) {
const double x = horizontal.coord_to_pixel(state.center_frequency);
painter.line({x, content.y}, {x, content.bottom()}, state.middle_frequency_pen);
painter.line(mapped_point(frequency_axis, state.center_frequency, power_axis,
power_axis.coordinate_range.origin),
mapped_point(frequency_axis, state.center_frequency, power_axis,
power_axis.coordinate_range.target),
state.middle_frequency_pen);
}
for(std::size_t index = 0; index < interaction.markers.size(); ++index) {
const double x = horizontal.coord_to_pixel(interaction.markers[index]);
painter.line({x, content.y}, {x, content.bottom()}, static_cast<int>(index) == interaction.selected_marker ? state.selected_marker_pen : state.marker_pen);
painter.line(mapped_point(frequency_axis, interaction.markers[index], power_axis,
power_axis.coordinate_range.origin),
mapped_point(frequency_axis, interaction.markers[index], power_axis,
power_axis.coordinate_range.target),
static_cast<int>(index) == interaction.selected_marker ? state.selected_marker_pen : state.marker_pen);
}
if(!frame.samples.empty() && (state.max_marker_visible || state.use_min_marker)) {
const auto draw_extreme = [&](bool maximum) {
@@ -238,7 +359,7 @@ void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) {
const std::size_t index = static_cast<std::size_t>(std::distance(frame.samples.begin(), iterator));
const double denominator = frame.samples.size() > 1 ? frame.samples.size() - 1.0 : 1.0;
const double frequency = state.frequency_range.origin + state.frequency_range.length() * index / denominator;
const PointF point{horizontal.coord_to_pixel(frequency), vertical.coord_to_pixel(*iterator)};
const PointF point = mapped_point(frequency_axis, frequency, power_axis, *iterator);
const Pen& pen = maximum ? state.max_pen : state.min_pen;
painter.circle(point, 3.0, pen, Brush{pen.color, Brush_Style::Solid});
};
@@ -248,7 +369,7 @@ void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) {
draw_extreme(false);
}
if(state.tooltip_enabled && interaction.tooltip.active && content.contains(interaction.tooltip.position)) {
const double frequency = horizontal.pixel_to_coord(interaction.tooltip.position.x);
const double frequency = frequency_axis.point_to_coord(interaction.tooltip.position);
bool ok{};
const double power = spectrum_power_at(state, frame, frequency, ok);
if(ok) {
@@ -259,6 +380,7 @@ void Spectrum_Control::paint(Painter& painter, const Render_State_View& view) {
painter.text({box.x + 4.0, box.y + 3.0}, text.str(), state.tooltip_font, state.tooltip_text_pen);
}
}
output.partitioner.finish(output.work_size);
}
}
}
+4
View File
@@ -8,6 +8,7 @@
namespace renderive {
struct Spectrum_Properties : Hover_Tooltip_Properties {
Nonnegative_Count frequency_point_size;
Nonnegative_Count partition_count;
Range frequency_range{};
double center_frequency = 50.0;
Range sweep_frequency_range{40.0, 60.0};
@@ -60,9 +61,12 @@ public:
void handle_event(const Event& event) override;
protected:
void paint(Painter& painter, const Render_State_View& state) override;
void build_paint_task_graph(Renderable_Task_Graph& graph) override;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
void prepare_render_frame(const Render_State_View& state);
void render_partition(const Render_State_View& state, int partition_index);
void publish() override;
};
}
+3 -10
View File
@@ -9,13 +9,6 @@ namespace renderive {
namespace detail {
namespace {
using Sweep_Spectrum_History = Plottable_History_Real_Time_Data<std::vector<double>, std::deque<std::vector<double>>>;
RectF axis_content_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical) {
const double x1 = horizontal.coord_to_pixel(horizontal.coordinate_range.origin);
const double x2 = horizontal.coord_to_pixel(horizontal.coordinate_range.target);
const double y1 = vertical.coord_to_pixel(vertical.coordinate_range.origin);
const double y2 = vertical.coord_to_pixel(vertical.coordinate_range.target);
return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)};
}
std::vector<double> flatten(const std::deque<std::vector<double>>& blocks) {
std::vector<double> values;
for(const auto& block : blocks)
@@ -74,9 +67,9 @@ void Sweep_Spectrum_Control::paint(Painter& painter, const Render_State_View& vi
painter.polyline(curve_points(values, state.frequency_range, x, y, state.visible_range_only, state.interpolation_mode), state.pen);
const double completed = std::min(1.0, static_cast<double>(blocks.size()) / state.block_count.get());
const double frequency = state.frequency_range.origin + state.frequency_range.length() * completed;
const RectF content = axis_content_rect(x, y);
const double marker_x = x.coord_to_pixel(frequency);
painter.line({marker_x, content.y}, {marker_x, content.bottom()}, state.current_frequency_pen);
painter.line(mapped_point(x, frequency, y, y.coordinate_range.origin),
mapped_point(x, frequency, y, y.coordinate_range.target),
state.current_frequency_pen);
}
}
}
+93 -19
View File
@@ -1,7 +1,9 @@
#include "Waterfall.h"
#include "Heatmap_Utils.h"
#include "Plottable_Real_Time_Data.h"
#include "../renderable/Render_Partition.h"
#include <algorithm>
#include <array>
#include <deque>
#include <iomanip>
#include <sstream>
@@ -18,6 +20,17 @@ struct Waterfall_Interaction {
};
using Waterfall_History = Plottable_History_Real_Time_Data<Waterfall_Row, std::deque<Waterfall_Row>>;
using Waterfall_Interaction_State = Double_State_Strategy<Waterfall_Interaction_Base, Waterfall_Interaction>;
struct Waterfall_Render_Frame {
Adaptive_Render_Partitioner partitioner;
Axis_Raster_Layout layout;
Frequency_Columns columns;
std::vector<Pixel> pixels;
int source_width{};
int source_height{};
int active_partitions{1};
std::size_t work_size{};
bool valid{};
};
}
struct Waterfall_Control::Impl {
Impl(Waterfall_Control& owner, std::shared_ptr<Frequency_Axis> frequency, std::shared_ptr<Time_Axis> time)
@@ -26,6 +39,7 @@ struct Waterfall_Control::Impl {
std::shared_ptr<Time_Axis> time_axis;
Waterfall_History rows;
Waterfall_Interaction_State interaction;
Waterfall_Render_Frame render_frame;
};
Waterfall_Control::Waterfall_Control(Plot_Core& plot, const Waterfall_Properties& properties, std::shared_ptr<Frequency_Axis> frequency_axis, std::shared_ptr<Time_Axis> time_axis)
: Plottable_State(plot, properties), impl_(std::make_unique<Impl>(*this, std::move(frequency_axis), std::move(time_axis))) {}
@@ -82,35 +96,95 @@ void Waterfall_Control::publish() {
publish_properties();
impl_->interaction.publish();
}
void Waterfall_Control::paint(Painter& painter, const Render_State_View& view) {
void Waterfall_Control::build_paint_task_graph(Renderable_Task_Graph& graph) {
const auto prepare = graph.emplace([this](const Scene_Render_Context&) {
prepare_render_frame(render_state_view());
}, "prepare waterfall");
std::array<Renderable_Task_Graph::Task, maximum_render_partitions> partitions;
for (int index = 0; index < maximum_render_partitions; ++index) {
partitions[static_cast<std::size_t>(index)] = graph.emplace(
[this, index](const Scene_Render_Context&) {
render_partition(render_state_view(), index);
},
"raster waterfall partition");
graph.precede(prepare, partitions[static_cast<std::size_t>(index)]);
}
const auto compose = add_paint_task(
graph, "compose waterfall",
[this](Painter& painter, const Render_State_View& view) { paint(painter, view); });
for (const auto partition : partitions)
graph.precede(partition, compose);
}
void Waterfall_Control::prepare_render_frame(const Render_State_View& view) {
const auto& state = render_properties(view);
const auto& rows = view.get(impl_->rows);
const auto& interaction = view.get(impl_->interaction);
if(rows.empty())
auto& output = impl_->render_frame;
output.valid = false;
output.work_size = 0;
if (rows.empty())
return;
const int source_width = std::min(state.frequency_bin_count.get(), static_cast<int>(std::min_element(rows.begin(), rows.end(), [](const auto& left, const auto& right) { return left.values.size() < right.values.size(); })->values.size()));
const int height = static_cast<int>(rows.size());
if(source_width <= 0 || height <= 0)
if (source_width <= 0 || height <= 0)
return;
const Axis_Transform horizontal = impl_->frequency_axis->transform(view);
const auto columns = frequency_columns(state.frequency_range, horizontal.coordinate_range, source_width, state.visible_range_only);
if(!columns)
const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view);
const Axis_Transform time_axis = impl_->time_axis->transform(view);
const auto columns = frequency_columns(state.frequency_range, frequency_axis.coordinate_range,
source_width, state.visible_range_only);
if (!columns)
return;
const int width = columns->last - columns->first + 1;
std::vector<Pixel> pixels(static_cast<std::size_t>(width) * height);
for(int y = 0; y < height; ++y) {
const Range time_range = rows.size() == 1
? time_axis.coordinate_range
: Range{static_cast<double>(rows.front().tick),
static_cast<double>(rows.back().tick)};
const auto layout = axis_raster_layout(frequency_axis, time_axis, columns->range,
time_range, width, height);
if (!layout.valid())
return;
output.layout = layout;
output.columns = *columns;
output.source_width = width;
output.source_height = height;
output.work_size = static_cast<std::size_t>(width) * height;
output.pixels.resize(output.work_size);
output.active_partitions =
output.partitioner.begin(state.partition_count.get(), output.work_size);
output.valid = true;
}
void Waterfall_Control::render_partition(const Render_State_View& view, int partition_index) {
auto& output = impl_->render_frame;
if (!output.valid || partition_index >= output.active_partitions)
return;
const auto& state = render_properties(view);
const auto& rows = view.get(impl_->rows);
const auto range = render_partition_range(output.work_size, partition_index,
output.active_partitions);
for (std::size_t cell = range.first; cell < range.last; ++cell) {
const int y = static_cast<int>(cell / static_cast<std::size_t>(output.source_width));
const int x = static_cast<int>(cell % static_cast<std::size_t>(output.source_width));
const auto& row = rows[static_cast<std::size_t>(y)].values;
for(int x = 0; x < width; ++x)
pixels[static_cast<std::size_t>(y) * width + x] = state.color_map.at_normalized(normalized_value(row[static_cast<std::size_t>(columns->first + x)], state.power_range));
output.pixels[output.layout.index(x, y, output.source_width, output.source_height)] =
state.color_map.at_normalized(normalized_value(
row[static_cast<std::size_t>(output.columns.first + x)], state.power_range));
}
const Axis_Transform vertical = impl_->time_axis->transform(view);
const Range time_range{static_cast<double>(rows.front().tick), static_cast<double>(rows.back().tick)};
RectF target = mapped_rect(horizontal, vertical, columns->range, time_range);
if(target.height < 1.0)
target.height = std::max(1.0, impl_->time_axis->transform(view).pixel_length);
painter.heatmap(target, width, height, pixels, state.interpolation_mode);
if(state.tooltip_enabled && interaction.tooltip.active && target.contains(interaction.tooltip.position)) {
const double frequency = horizontal.pixel_to_coord(interaction.tooltip.position.x);
}
void Waterfall_Control::paint(Painter& painter, const Render_State_View& view) {
const auto& state = render_properties(view);
const auto& interaction = view.get(impl_->interaction);
auto& output = impl_->render_frame;
if (!output.valid)
return;
painter.heatmap(output.layout.target, output.layout.width, output.layout.height,
output.pixels, state.interpolation_mode);
output.partitioner.finish(output.work_size);
if(state.tooltip_enabled && interaction.tooltip.active && output.layout.target.contains(interaction.tooltip.position)) {
const Axis_Transform frequency_axis = impl_->frequency_axis->transform(view);
const double frequency = frequency_axis.point_to_coord(interaction.tooltip.position);
std::ostringstream text;
text << std::fixed << std::setprecision(2) << frequency << " Hz";
const RectF box{interaction.tooltip.position.x + 8.0, interaction.tooltip.position.y + 8.0, 110.0, 24.0};
+4
View File
@@ -9,6 +9,7 @@ struct Waterfall_Properties : Hover_Tooltip_Properties {
Range frequency_range{0.0, 10.0};
Range power_range{0.0, 10.0};
Nonnegative_Count frequency_bin_count;
Nonnegative_Count partition_count;
bool visible_range_only{true};
Image_Interpolation_Mode interpolation_mode = Image_Interpolation_Mode::Nearest;
Color_Map color_map;
@@ -36,9 +37,12 @@ public:
void handle_event(const Event& event) override;
protected:
void paint(Painter& painter, const Render_State_View& state) override;
void build_paint_task_graph(Renderable_Task_Graph& graph) override;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
void prepare_render_frame(const Render_State_View& state);
void render_partition(const Render_State_View& state, int partition_index);
void publish() override;
};
}