diff --git a/Core2/axis/Axis.cpp b/Core2/axis/Axis.cpp new file mode 100644 index 0000000..5a91cbd --- /dev/null +++ b/Core2/axis/Axis.cpp @@ -0,0 +1,405 @@ +#include "Axis.h" + +#include "../plot/Plot_Core.h" +#include "../render/Blend2D_Cache.h" + +#include +#include +#include +#include + +namespace renderive { +namespace { + +bool valid_range(Range range) { + return std::isfinite(range.origin) && std::isfinite(range.target) && range.size() > 0.0; +} + +std::string fixed_number(double value, int precision) { + std::ostringstream stream; + stream << std::fixed << std::setprecision(std::clamp(precision, 0, 12)) << value; + std::string result = stream.str(); + if (result.find('.') != std::string::npos) { + while (!result.empty() && result.back() == '0') + result.pop_back(); + if (!result.empty() && result.back() == '.') + result.pop_back(); + } + return result; +} + +std::string localized_number(double value, int precision, Number_Locale locale) { + std::string result = fixed_number(value, precision); + if (locale.decimal_point != '.') + std::replace(result.begin(), result.end(), '.', locale.decimal_point); + return result; +} + +std::string formatted_time(Time_Of_Day time, std::string_view format) { + const auto total = time.milliseconds; + const int hours = static_cast((total / 3'600'000) % 24); + const int minutes = static_cast((total / 60'000) % 60); + const int seconds = static_cast((total / 1'000) % 60); + const int milliseconds = static_cast(total % 1'000); + const auto digits = [](int value, int width) { + std::ostringstream stream; + stream << std::setfill('0') << std::setw(width) << value; + return stream.str(); + }; + std::string result; + for (std::size_t index = 0; index < format.size();) { + const std::string_view rest = format.substr(index); + if (rest.starts_with("zzz")) { + result += digits(milliseconds, 3); + index += 3; + } else if (rest.starts_with("hh") || rest.starts_with("HH")) { + result += digits(hours, 2); + index += 2; + } else if (rest.starts_with("mm")) { + result += digits(minutes, 2); + index += 2; + } else if (rest.starts_with("ss")) { + result += digits(seconds, 2); + index += 2; + } else { + result.push_back(format[index++]); + } + } + return result; +} + +} // namespace + +Abs_Axis::Abs_Axis(Plot_Core& plot, Orientation orientation) : Renderable(plot, true) { + axis_state_.orientation = orientation; +} + +Abs_Axis::~Abs_Axis() = default; + +#define RENDERIVE_AXIS_PROPERTY(Type, Name) \ + Type Abs_Axis::Name() const { std::lock_guard lock(axis_mutex_); return axis_state_.Name; } \ + void Abs_Axis::set_##Name(Type value) { \ + { std::lock_guard lock(axis_mutex_); if (axis_state_.Name == value) return; axis_state_.Name = std::move(value); } \ + changed(); \ + } + +RENDERIVE_AXIS_PROPERTY(int, x) +RENDERIVE_AXIS_PROPERTY(int, y) +RENDERIVE_AXIS_PROPERTY(Orientation, orientation) +RENDERIVE_AXIS_PROPERTY(std::size_t, pixel_length) +RENDERIVE_AXIS_PROPERTY(int, tick_length) +RENDERIVE_AXIS_PROPERTY(int, sub_tick_length) +RENDERIVE_AXIS_PROPERTY(Color, color) +RENDERIVE_AXIS_PROPERTY(Number_Locale, locale) +RENDERIVE_AXIS_PROPERTY(std::string, unit_text) +RENDERIVE_AXIS_PROPERTY(Font, unit_text_font) +RENDERIVE_AXIS_PROPERTY(Pen, unit_text_pen) +RENDERIVE_AXIS_PROPERTY(Brush, unit_text_background_brush) +RENDERIVE_AXIS_PROPERTY(int, label_rotation_degrees) + +#undef RENDERIVE_AXIS_PROPERTY + +Abs_Axis::State Abs_Axis::axis_state() const { + std::lock_guard lock(axis_mutex_); + return axis_state_; +} + +Axis_Transform Abs_Axis::transform() const { + const State state = axis_state(); + return { + coord_range(), + state.orientation == Orientation::Horizontal ? static_cast(state.x) + : static_cast(state.y), + static_cast(state.pixel_length) + }; +} + +double Abs_Axis::pixel_to_coord(double pixel) const { return transform().pixel_to_coord(pixel); } +double Abs_Axis::coord_to_pixel(double coordinate) const { return transform().coord_to_pixel(coordinate); } +double Abs_Axis::start_coord() const { return coord_range().origin; } +double Abs_Axis::end_coord() const { return coord_range().target; } + +int Abs_Axis::pixel_sample_count(Range range) const { + const Axis_Transform value = transform(); + const double first = value.coord_to_pixel(range.origin); + const double last = value.coord_to_pixel(range.target); + return std::max(0, static_cast(std::abs(last - first)) + 1); +} + +int Abs_Axis::pixel_sample_count() const { + return static_cast(pixel_length()) + (pixel_length() > 0 ? 1 : 0); +} + +double Abs_Axis::tick_step(Range range) const { + const double raw = range.size() / 5.0; + if (!(raw > 0.0) || !std::isfinite(raw)) + return 1.0; + const double scale = std::pow(10.0, std::floor(std::log10(raw))); + const double normalized = raw / scale; + const double nice = normalized <= 1.0 ? 1.0 : normalized <= 2.0 ? 2.0 : normalized <= 5.0 ? 5.0 : 10.0; + return nice * scale; +} + +int Abs_Axis::sub_tick_count(double) const { return 4; } + +std::string Abs_Axis::tick_label(double tick) const { + return localized_number(tick, 2, locale()); +} + +void Abs_Axis::paint(detail::Painter& painter) { + const State state = axis_state(); + if (state.pixel_length == 0) + return; + const Range coordinates = coord_range(); + const double step = tick_step(coordinates); + if (!(step > 0.0)) + return; + + const Pen axis_pen{state.color, 1.0}; + const PointF first{static_cast(state.x), static_cast(state.y)}; + const PointF last = state.orientation == Orientation::Horizontal + ? PointF{first.x + state.pixel_length, first.y} + : PointF{first.x, first.y + state.pixel_length}; + painter.line(first, last, axis_pen); + + const auto [low, high] = std::minmax(coordinates.origin, coordinates.target); + const double initial = std::ceil(low / step) * step; + int tick_index{}; + for (double tick = initial; tick <= high + step * 1e-6 && tick_index < 1000; + tick += step, ++tick_index) { + const double pixel = coord_to_pixel(tick); + PointF tick_start; + PointF tick_end; + PointF label; + if (state.orientation == Orientation::Horizontal) { + tick_start = {pixel, static_cast(state.y)}; + tick_end = {pixel, static_cast(state.y + state.tick_length)}; + label = {pixel + 2.0, static_cast(state.y + state.tick_length + 2)}; + } else { + tick_start = {static_cast(state.x), pixel}; + tick_end = {static_cast(state.x + state.tick_length), pixel}; + label = {static_cast(state.x + state.tick_length + 2), pixel - 7.0}; + } + painter.line(tick_start, tick_end, axis_pen); + painter.text(label, tick_label(tick), state.unit_text_font, state.unit_text_pen, + state.label_rotation_degrees); + const int subdivisions = std::max(0, sub_tick_count(step)); + for (int sub_index = 1; sub_index <= subdivisions; ++sub_index) { + const double sub_tick = tick + step * sub_index / (subdivisions + 1.0); + if (sub_tick >= high) + break; + const double sub_pixel = coord_to_pixel(sub_tick); + if (state.orientation == Orientation::Horizontal) { + painter.line({sub_pixel, static_cast(state.y)}, + {sub_pixel, static_cast(state.y + state.sub_tick_length)}, + axis_pen); + } else { + painter.line({static_cast(state.x), sub_pixel}, + {static_cast(state.x + state.sub_tick_length), sub_pixel}, + axis_pen); + } + } + } + if (!state.unit_text.empty()) { + const PointF position{last.x + 4.0, last.y + 4.0}; + const double estimated_width = std::max(4.0, state.unit_text.size() * state.unit_text_font.size * 0.65); + painter.rect({position.x - 2.0, position.y - 2.0, + estimated_width + 4.0, state.unit_text_font.size * 1.5 + 4.0}, + Pen{.style = Line_Style::None}, state.unit_text_background_brush); + painter.text(position, state.unit_text, + state.unit_text_font, state.unit_text_pen); + } +} + +Axis::Axis(Plot_Core& plot, Orientation orientation) : Abs_Axis(plot, orientation) {} + +Range Axis::coord_range() const { std::lock_guard lock(interaction_mutex_); return coordinates_; } +int Axis::label_precision() const { std::lock_guard lock(interaction_mutex_); return precision_; } +void Axis::set_label_precision(int value) { { std::lock_guard lock(interaction_mutex_); precision_ = std::clamp(value, 0, 12); } changed(); } +double Axis::coord_start() const { return coord_range().origin; } +void Axis::set_coord_start(double value) { auto range = coord_range(); set_coord_range({value, value + range.length()}); } +double Axis::coord_length() const { return coord_range().length(); } +void Axis::set_coord_length(double value) { auto range = coord_range(); set_coord_range({range.origin, range.origin + value}); } +void Axis::set_coord_range(Range range) { + if (!valid_range(range)) + return; + { + std::lock_guard lock(interaction_mutex_); + if (coordinates_ == range) + return; + coordinates_ = range; + } + changed(); +} +void Axis::set_use_wheel(bool value) { std::lock_guard lock(interaction_mutex_); wheel_enabled_ = value; } +void Axis::set_use_drag(bool value) { std::lock_guard lock(interaction_mutex_); drag_enabled_ = value; } +bool Axis::use_wheel() const { std::lock_guard lock(interaction_mutex_); return wheel_enabled_; } +bool Axis::use_drag() const { std::lock_guard lock(interaction_mutex_); return drag_enabled_; } + +void Axis::handle_event(const Event& event) { + if (event.type == Event_Type::Wheel && use_wheel()) { + const auto& wheel = static_cast(event); + Range range = coord_range(); + const double anchor_pixel = orientation() == Orientation::Horizontal ? wheel.position.x : wheel.position.y; + const double anchor = pixel_to_coord(anchor_pixel); + const double factor = wheel.angle_delta_y >= 0.0 ? 0.9 : 1.1; + set_coord_range({anchor + (range.origin - anchor) * factor, + anchor + (range.target - anchor) * factor}); + event.accept(); + return; + } + if (!use_drag()) + return; + if (event.type == Event_Type::Pointer_Press) { + const auto& pointer = static_cast(event); + if (pointer.button == Mouse_Button::Left) { + std::lock_guard lock(interaction_mutex_); + dragging_ = true; + last_pointer_ = pointer.position; + event.accept(); + } + } else if (event.type == Event_Type::Pointer_Move) { + const auto& pointer = static_cast(event); + PointF previous; + { + std::lock_guard lock(interaction_mutex_); + if (!dragging_) + return; + previous = last_pointer_; + last_pointer_ = pointer.position; + } + const double delta = orientation() == Orientation::Horizontal + ? pointer.position.x - previous.x + : pointer.position.y - previous.y; + Range range = coord_range(); + const double shift = pixel_length() == 0 ? 0.0 : -delta * range.length() / pixel_length(); + set_coord_range({range.origin + shift, range.target + shift}); + event.accept(); + } else if (event.type == Event_Type::Pointer_Release) { + std::lock_guard lock(interaction_mutex_); + if (dragging_) { + dragging_ = false; + event.accept(); + } + } +} + +std::string Axis::tick_label(double tick) const { + return localized_number(tick, label_precision(), locale()); +} + +Axis::Builder::Builder(std::shared_ptr parent, Orientation orientation) + : Axis_Builder_Base(std::move(parent), orientation) {} + +std::shared_ptr Axis::Builder::build() { + if (!parent_) + return {}; + auto result = parent_->plot().make_renderable(parent_, orientation_); + apply(*result); + result->set_coord_range(coordinates_); + result->set_label_precision(precision_); + result->set_use_wheel(wheel_); + result->set_use_drag(drag_); + return result; +} + +Frequency_Axis::Frequency_Axis(Plot_Core& plot, Orientation orientation) : Axis(plot, orientation) {} + +std::string Frequency_Axis::tick_label(double tick) const { + const double absolute = std::abs(tick); + if (absolute >= 1'000'000.0) + return localized_number(tick / 1'000'000.0, label_precision(), locale()) + " MHz"; + if (absolute >= 1'000.0) + return localized_number(tick / 1'000.0, label_precision(), locale()) + " kHz"; + return localized_number(tick, label_precision(), locale()) + " Hz"; +} + +Frequency_Axis::Builder::Builder(std::shared_ptr parent, Orientation orientation) + : Axis_Builder_Base(std::move(parent), orientation) {} + +std::shared_ptr Frequency_Axis::Builder::build() { + if (!parent_) + return {}; + auto result = parent_->plot().make_renderable(parent_, orientation_); + apply(*result); + result->set_coord_range(coordinates_); + result->set_label_precision(precision_); + result->set_use_wheel(wheel_); + result->set_use_drag(drag_); + return result; +} + +Time_Axis::Time_Axis(Plot_Core& plot, Orientation orientation) : Abs_Axis(plot, orientation) {} + +int Time_Axis::visible_time_point_count() const { std::lock_guard lock(time_mutex_); return time_state_.visible_count; } +void Time_Axis::set_visible_time_point_count(int value) { { std::lock_guard lock(time_mutex_); time_state_.visible_count = std::max(2, value); } changed(); } +int Time_Axis::tick_label_spacing_px() const { std::lock_guard lock(time_mutex_); return time_state_.tick_label_spacing_px; } +void Time_Axis::set_tick_label_spacing_px(int value) { { std::lock_guard lock(time_mutex_); time_state_.tick_label_spacing_px = std::max(0, value); } changed(); } +std::string Time_Axis::time_format() const { std::lock_guard lock(time_mutex_); return time_state_.format; } +void Time_Axis::set_time_format(std::string value) { { std::lock_guard lock(time_mutex_); time_state_.format = std::move(value); } changed(); } +Font Time_Axis::font() const { return unit_text_font(); } +void Time_Axis::set_font(Font value) { set_unit_text_font(value); } +bool Time_Axis::newest_at_axis_start() const { std::lock_guard lock(time_mutex_); return time_state_.newest_at_start; } +void Time_Axis::set_newest_at_axis_start(bool value) { { std::lock_guard lock(time_mutex_); time_state_.newest_at_start = value; } changed(); } + +int Time_Axis::append_time(Time_Of_Day time) { + int tick{}; + { + std::lock_guard lock(time_mutex_); + tick = time_state_.next_tick++; + time_state_.samples.emplace_back(tick, time); + const auto limit = static_cast(std::max(512, time_state_.visible_count * 4)); + while (time_state_.samples.size() > limit) + time_state_.samples.pop_front(); + } + changed(); + return tick; +} + +Time_Of_Day Time_Axis::tick_to_time(int tick) const { + std::lock_guard lock(time_mutex_); + auto iterator = std::find_if(time_state_.samples.begin(), time_state_.samples.end(), + [tick](const auto& value) { return value.first == tick; }); + return iterator == time_state_.samples.end() ? Time_Of_Day{} : iterator->second; +} + +Range Time_Axis::coord_range() const { + std::lock_guard lock(time_mutex_); + const int latest = std::max(1, time_state_.next_tick - 1); + const int earliest = std::max(0, latest - time_state_.visible_count + 1); + return time_state_.newest_at_start ? Range{static_cast(latest), static_cast(earliest)} + : Range{static_cast(earliest), static_cast(latest)}; +} + +double Time_Axis::tick_step(Range range) const { + const double available = static_cast(pixel_length()); + const double label_width = std::max(48.0, font().size * 7.0); + const double spacing = static_cast(tick_label_spacing_px()); + const double label_count = std::max(1.0, available / (label_width + spacing)); + return std::max(1.0, std::ceil(range.size() / label_count)); +} + +std::string Time_Axis::tick_label(double tick) const { + const Time_Of_Day time = tick_to_time(static_cast(std::llround(tick))); + if (!time.valid()) + return {}; + return formatted_time(time, time_format()); +} + +Time_Axis::Builder::Builder(std::shared_ptr parent, Orientation orientation) + : Axis_Builder_Base(std::move(parent), orientation) {} + +std::shared_ptr Time_Axis::Builder::build() { + if (!parent_) + return {}; + auto result = parent_->plot().make_renderable(parent_, orientation_); + apply(*result); + result->set_visible_time_point_count(visible_count_); + result->set_tick_label_spacing_px(spacing_); + result->set_time_format(format_); + result->set_font(font_); + result->set_newest_at_axis_start(newest_at_start_); + return result; +} + +} // namespace renderive diff --git a/Core2/axis/Axis.h b/Core2/axis/Axis.h new file mode 100644 index 0000000..dbd55ad --- /dev/null +++ b/Core2/axis/Axis.h @@ -0,0 +1,265 @@ +#pragma once + +#include "../renderable/Renderable.h" + +#include +#include +#include +#include +#include +#include + +namespace renderive { + +struct Axis_Transform { + Range coordinate_range; + double pixel_origin{}; + double pixel_length{}; + + [[nodiscard]] double coord_to_pixel(double coordinate) const noexcept { + const double span = coordinate_range.length(); + if (span == 0.0) + return pixel_origin; + return pixel_origin + (coordinate - coordinate_range.origin) / span * pixel_length; + } + [[nodiscard]] double pixel_to_coord(double pixel) const noexcept { + if (pixel_length == 0.0) + return coordinate_range.origin; + return coordinate_range.origin + (pixel - pixel_origin) / pixel_length * coordinate_range.length(); + } +}; + +class LIB_DECL Abs_Axis : public Renderable { +public: + explicit Abs_Axis(Plot_Core& plot, Orientation orientation = Orientation::Horizontal); + ~Abs_Axis() override; + + [[nodiscard]] int x() const; + void set_x(int value); + [[nodiscard]] int y() const; + void set_y(int value); + [[nodiscard]] Orientation orientation() const; + void set_orientation(Orientation value); + [[nodiscard]] std::size_t pixel_length() const; + void set_pixel_length(std::size_t value); + [[nodiscard]] int tick_length() const; + void set_tick_length(int value); + [[nodiscard]] int sub_tick_length() const; + void set_sub_tick_length(int value); + [[nodiscard]] Color color() const; + void set_color(Color value); + [[nodiscard]] Number_Locale locale() const; + void set_locale(Number_Locale value); + [[nodiscard]] std::string unit_text() const; + void set_unit_text(std::string value); + [[nodiscard]] Font unit_text_font() const; + void set_unit_text_font(Font value); + [[nodiscard]] Pen unit_text_pen() const; + void set_unit_text_pen(Pen value); + [[nodiscard]] Brush unit_text_background_brush() const; + void set_unit_text_background_brush(Brush value); + [[nodiscard]] int label_rotation_degrees() const; + void set_label_rotation_degrees(int value); + + [[nodiscard]] virtual Range coord_range() const = 0; + [[nodiscard]] virtual double pixel_to_coord(double pixel) const; + [[nodiscard]] virtual double coord_to_pixel(double coordinate) const; + [[nodiscard]] Axis_Transform transform() const; + [[nodiscard]] double start_coord() const; + [[nodiscard]] double end_coord() const; + [[nodiscard]] int pixel_sample_count(Range range) const; + [[nodiscard]] int pixel_sample_count() const; + [[nodiscard]] virtual double tick_step(Range range) const; + [[nodiscard]] virtual int sub_tick_count(double major_step) const; + [[nodiscard]] virtual std::string tick_label(double tick) const; + +protected: + struct State { + int x{}; + int y{}; + Orientation orientation = Orientation::Horizontal; + std::size_t pixel_length{}; + int tick_length = 10; + int sub_tick_length = 5; + Color color = Color::white(); + Number_Locale locale; + std::string unit_text; + Font unit_text_font; + Pen unit_text_pen{Color::white()}; + Brush unit_text_background_brush{Color::black(), Brush_Style::Solid}; + int label_rotation_degrees{}; + }; + + [[nodiscard]] State axis_state() const; + void paint(detail::Painter& painter) override; + +private: + mutable std::mutex axis_mutex_; + State axis_state_; +}; + +template +class Axis_Builder_Base { +public: + Derived& set_x(int value) { x_ = value; return derived(); } + Derived& set_y(int value) { y_ = value; return derived(); } + Derived& set_orientation(Orientation value) { orientation_ = value; return derived(); } + Derived& set_pixel_length(std::size_t value) { pixel_length_ = value; return derived(); } + Derived& set_tick_length(int value) { tick_length_ = value; return derived(); } + Derived& set_sub_tick_length(int value) { sub_tick_length_ = value; return derived(); } + Derived& set_color(Color value) { color_ = value; return derived(); } + Derived& set_unit_text(std::string value) { unit_text_ = std::move(value); return derived(); } + Derived& set_unit_text_font(Font value) { unit_text_font_ = value; return derived(); } + Derived& set_unit_text_pen(Pen value) { unit_text_pen_ = value; return derived(); } + Derived& set_unit_text_background_brush(Brush value) { unit_background_ = value; return derived(); } + Derived& set_label_rotation_degrees(int value) { label_rotation_ = value; return derived(); } + +protected: + explicit Axis_Builder_Base(std::shared_ptr parent, Orientation orientation) + : parent_(std::move(parent)), orientation_(orientation) {} + + void apply(Abs_Axis& axis) const { + axis.set_x(x_); + axis.set_y(y_); + axis.set_orientation(orientation_); + axis.set_pixel_length(pixel_length_); + axis.set_tick_length(tick_length_); + axis.set_sub_tick_length(sub_tick_length_); + axis.set_color(color_); + axis.set_unit_text(unit_text_); + axis.set_unit_text_font(unit_text_font_); + axis.set_unit_text_pen(unit_text_pen_); + axis.set_unit_text_background_brush(unit_background_); + axis.set_label_rotation_degrees(label_rotation_); + } + [[nodiscard]] Derived& derived() { return static_cast(*this); } + + std::shared_ptr parent_; + int x_{}; + int y_{}; + Orientation orientation_; + std::size_t pixel_length_{}; + int tick_length_ = 10; + int sub_tick_length_ = 5; + Color color_ = Color::white(); + std::string unit_text_; + Font unit_text_font_; + Pen unit_text_pen_{Color::white()}; + Brush unit_background_{Color::black(), Brush_Style::Solid}; + int label_rotation_{}; +}; + +class LIB_DECL Axis : public Abs_Axis, public Event_Handler { +public: + explicit Axis(Plot_Core& plot, Orientation orientation = Orientation::Horizontal); + [[nodiscard]] Range coord_range() const override; + [[nodiscard]] int label_precision() const; + void set_label_precision(int value); + [[nodiscard]] double coord_start() const; + void set_coord_start(double value); + [[nodiscard]] double coord_length() const; + void set_coord_length(double value); + void set_coord_range(Range range); + void set_use_wheel(bool value); + void set_use_drag(bool value); + [[nodiscard]] bool use_wheel() const; + [[nodiscard]] bool use_drag() const; + void handle_event(const Event& event) override; + [[nodiscard]] std::string tick_label(double tick) const override; + + class Builder : public Axis_Builder_Base { + public: + Builder(std::shared_ptr parent, Orientation orientation); + Builder& set_coord_range(Range value) { coordinates_ = value; return *this; } + Builder& set_label_precision(int value) { precision_ = value; return *this; } + Builder& set_use_wheel(bool value) { wheel_ = value; return *this; } + Builder& set_use_drag(bool value) { drag_ = value; return *this; } + std::shared_ptr build(); + private: + Range coordinates_{0.0, 20.0}; + int precision_ = 2; + bool wheel_{}; + bool drag_{}; + }; + +private: + mutable std::mutex interaction_mutex_; + Range coordinates_{0.0, 20.0}; + int precision_ = 2; + bool wheel_enabled_{}; + bool drag_enabled_{}; + bool dragging_{}; + PointF last_pointer_{}; +}; + +class LIB_DECL Frequency_Axis : public Axis { +public: + explicit Frequency_Axis(Plot_Core& plot, Orientation orientation = Orientation::Horizontal); + [[nodiscard]] std::string tick_label(double tick) const override; + + class Builder : public Axis_Builder_Base { + public: + Builder(std::shared_ptr parent, Orientation orientation); + Builder& set_coord_range(Range value) { coordinates_ = value; return *this; } + Builder& set_label_precision(int value) { precision_ = value; return *this; } + Builder& set_use_wheel(bool value) { wheel_ = value; return *this; } + Builder& set_use_drag(bool value) { drag_ = value; return *this; } + std::shared_ptr build(); + private: + Range coordinates_{0.0, 20.0}; + int precision_ = 2; + bool wheel_{}; + bool drag_{}; + }; +}; + +class LIB_DECL Time_Axis : public Abs_Axis { +public: + explicit Time_Axis(Plot_Core& plot, Orientation orientation = Orientation::Horizontal); + [[nodiscard]] int visible_time_point_count() const; + void set_visible_time_point_count(int value); + [[nodiscard]] int tick_label_spacing_px() const; + void set_tick_label_spacing_px(int value); + [[nodiscard]] std::string time_format() const; + void set_time_format(std::string value); + [[nodiscard]] Font font() const; + void set_font(Font value); + [[nodiscard]] bool newest_at_axis_start() const; + void set_newest_at_axis_start(bool value); + int append_time(Time_Of_Day time); + [[nodiscard]] Time_Of_Day tick_to_time(int tick) const; + [[nodiscard]] Range coord_range() const override; + [[nodiscard]] double tick_step(Range range) const override; + [[nodiscard]] std::string tick_label(double tick) const override; + + class Builder : public Axis_Builder_Base { + public: + Builder(std::shared_ptr parent, Orientation orientation); + Builder& set_visible_time_point_count(int value) { visible_count_ = value; return *this; } + Builder& set_tick_label_spacing_px(int value) { spacing_ = value; return *this; } + Builder& set_time_format(std::string value) { format_ = std::move(value); return *this; } + Builder& set_font(Font value) { font_ = value; return *this; } + Builder& set_newest_at_axis_start(bool value) { newest_at_start_ = value; return *this; } + std::shared_ptr build(); + private: + int visible_count_ = 100; + int spacing_ = 8; + std::string format_ = "mm:ss.zzz"; + Font font_; + bool newest_at_start_{}; + }; + +private: + struct Time_State { + int visible_count = 100; + int tick_label_spacing_px = 8; + std::string format = "mm:ss.zzz"; + bool newest_at_start{}; + int next_tick{}; + std::deque> samples; + }; + mutable std::mutex time_mutex_; + Time_State time_state_; +}; + +} // namespace renderive diff --git a/Core2/base/Types.cpp b/Core2/base/Types.cpp new file mode 100644 index 0000000..097a658 --- /dev/null +++ b/Core2/base/Types.cpp @@ -0,0 +1,64 @@ +#include "Types.h" + +#include +#include + +namespace renderive { +namespace { + +constexpr std::size_t domain_count = static_cast(Memory_Domain::Count); + +auto& domain_resources() { + static std::array resources; + return resources; +} + +std::vector default_gradient() { + std::vector colors; + colors.reserve(256); + for (int index = 0; index < 256; ++index) { + const double x = static_cast(index) / 255.0; + const auto channel = [x](double center) { + const double value = 1.5 - std::abs(4.0 * x - center); + return static_cast(std::clamp(value, 0.0, 1.0) * 255.0 + 0.5); + }; + colors.push_back(pack_rgba(channel(3.0), channel(2.0), channel(1.0))); + } + return colors; +} + +} // namespace + +std::pmr::memory_resource* memory_resource() noexcept { + return memory_resource(Memory_Domain::Other); +} + +std::pmr::memory_resource* memory_resource(Memory_Domain domain) noexcept { + auto index = static_cast(domain); + if (index >= domain_count) + index = static_cast(Memory_Domain::Other); + return &domain_resources()[index]; +} + +Color_Map::Color_Map() : colors_(default_gradient()) {} + +Color_Map::Color_Map(std::vector colors) { + set_colors(std::move(colors)); +} + +void Color_Map::set_colors(std::vector colors) { + colors_ = std::move(colors); + if (colors_.empty()) + colors_ = default_gradient(); +} + +Pixel Color_Map::at_normalized(double value) const noexcept { + if (colors_.empty()) + return 0; + value = std::clamp(value, 0.0, 1.0); + const auto index = static_cast(value * static_cast(colors_.size() - 1)); + return colors_[index]; +} + +} // namespace renderive + diff --git a/Core2/base/Types.h b/Core2/base/Types.h new file mode 100644 index 0000000..c5946f6 --- /dev/null +++ b/Core2/base/Types.h @@ -0,0 +1,246 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef LIB_DECL +#define LIB_DECL +#endif + +namespace renderive { + +struct Point { + int x{}; + int y{}; +}; + +struct PointF { + double x{}; + double y{}; +}; + +struct Size { + int width{}; + int height{}; + + [[nodiscard]] bool empty() const noexcept { return width <= 0 || height <= 0; } + friend bool operator==(Size, Size) = default; +}; + +struct Rect { + int x{}; + int y{}; + int width{}; + int height{}; + + [[nodiscard]] bool empty() const noexcept { return width <= 0 || height <= 0; } + [[nodiscard]] int right() const noexcept { return x + width; } + [[nodiscard]] int bottom() const noexcept { return y + height; } + [[nodiscard]] bool contains(Point point) const noexcept { + return point.x >= x && point.x <= right() && point.y >= y && point.y <= bottom(); + } +}; + +struct RectF { + double x{}; + double y{}; + double width{}; + double height{}; + + [[nodiscard]] bool empty() const noexcept { return width <= 0.0 || height <= 0.0; } + [[nodiscard]] double right() const noexcept { return x + width; } + [[nodiscard]] double bottom() const noexcept { return y + height; } + [[nodiscard]] bool contains(PointF point) const noexcept { + return point.x >= x && point.x <= right() && point.y >= y && point.y <= bottom(); + } + [[nodiscard]] RectF normalized() const noexcept { + RectF value = *this; + if (value.width < 0.0) { + value.x += value.width; + value.width = -value.width; + } + if (value.height < 0.0) { + value.y += value.height; + value.height = -value.height; + } + return value; + } +}; + +enum class Orientation : std::uint8_t { Horizontal, Vertical }; + +struct Range { + double origin{}; + double target{}; + + [[nodiscard]] double size() const noexcept { return std::abs(target - origin); } + [[nodiscard]] double length() const noexcept { return target - origin; } + [[nodiscard]] double center() const noexcept { return origin + length() * 0.5; } + [[nodiscard]] bool contains(double value) const noexcept { + const auto [low, high] = std::minmax(origin, target); + return value >= low - 1e-9 && value <= high + 1e-9; + } + friend bool operator==(Range, Range) = default; +}; + +struct Color { + std::uint8_t r{}; + std::uint8_t g{}; + std::uint8_t b{}; + std::uint8_t a{255}; + + static constexpr Color transparent() noexcept { return {0, 0, 0, 0}; } + static constexpr Color black() noexcept { return {0, 0, 0, 255}; } + static constexpr Color white() noexcept { return {255, 255, 255, 255}; } + static constexpr Color red() noexcept { return {255, 0, 0, 255}; } + static constexpr Color green() noexcept { return {0, 255, 0, 255}; } + static constexpr Color yellow() noexcept { return {255, 255, 0, 255}; } + friend bool operator==(Color, Color) = default; +}; + +using Pixel = std::uint32_t; + +constexpr Pixel premultiply(Color color) noexcept { + const auto r = static_cast((color.r * color.a + 127) / 255); + const auto g = static_cast((color.g * color.a + 127) / 255); + const auto b = static_cast((color.b * color.a + 127) / 255); + return (static_cast(color.a) << 24) | (r << 16) | (g << 8) | b; +} + +constexpr Pixel pack_rgba(std::uint8_t r, std::uint8_t g, std::uint8_t b, std::uint8_t a = 255) noexcept { + return premultiply(Color{r, g, b, a}); +} + +enum class Line_Style : std::uint8_t { None, Solid, Dash, Dot }; +enum class Line_Cap : std::uint8_t { Butt, Square, Round }; +enum class Line_Join : std::uint8_t { Miter, Bevel, Round }; + +struct Pen { + Color color = Color::white(); + double width = 1.0; + Line_Style style = Line_Style::Solid; + Line_Cap cap = Line_Cap::Butt; + Line_Join join = Line_Join::Miter; + + [[nodiscard]] bool enabled() const noexcept { + return style != Line_Style::None && width > 0.0 && color.a != 0; + } + friend bool operator==(const Pen&, const Pen&) = default; +}; + +enum class Brush_Style : std::uint8_t { None, Solid }; + +struct Brush { + Color color = Color::transparent(); + Brush_Style style = Brush_Style::None; + + [[nodiscard]] bool enabled() const noexcept { + return style != Brush_Style::None && color.a != 0; + } + friend bool operator==(const Brush&, const Brush&) = default; +}; + +struct Font { + double size = 12.0; + int weight = 400; + bool italic{}; + + friend bool operator==(const Font&, const Font&) = default; +}; + +struct Number_Locale { + char decimal_point = '.'; + + friend bool operator==(Number_Locale, Number_Locale) = default; +}; + +struct Time_Of_Day { + std::int64_t milliseconds = -1; + + [[nodiscard]] bool valid() const noexcept { + return milliseconds >= 0 && milliseconds < 24LL * 60LL * 60LL * 1000LL; + } + friend bool operator==(Time_Of_Day, Time_Of_Day) = default; +}; + +enum class Image_Interpolation_Mode : std::uint8_t { Nearest, Bilinear, Bicubic }; +enum class Line_Interpolation_Mode : std::uint8_t { + Nearest_Sample, + Linear_Value, + Linear_Power_Domain, + Step_Left, + Step_Right, + Cubic_Value +}; +enum class Renderable_Cache_Mode : std::uint8_t { Direct, Local_Pixel }; + +enum class Pixel_Format : std::uint8_t { Premultiplied_32 }; + +struct Image_View { + const std::byte* data{}; + int width{}; + int height{}; + int stride{}; + Pixel_Format format = Pixel_Format::Premultiplied_32; + + [[nodiscard]] bool empty() const noexcept { + return data == nullptr || width <= 0 || height <= 0; + } +}; + +enum class Memory_Domain : std::uint8_t { + Renderable, + Input_Buffer, + Waterfall, + Spectrum, + Frame_Arena, + Ring_Buffer, + Image, + Audio, + Afterglow, + Constellation_Diagram, + Curve, + Point_Set, + Axis, + Scheduler_Task, + Renderable_Cache_Image, + Plot_Frame, + Update_Completion, + Other, + Count +}; + +LIB_DECL std::pmr::memory_resource* memory_resource() noexcept; +LIB_DECL std::pmr::memory_resource* memory_resource(Memory_Domain domain) noexcept; + +template +std::shared_ptr make_shared(Memory_Domain domain, Args&&... args) { + return std::allocate_shared(std::pmr::polymorphic_allocator(memory_resource(domain)), + std::forward(args)...); +} + +class LIB_DECL Color_Map { +public: + Color_Map(); + explicit Color_Map(std::vector colors); + + [[nodiscard]] const std::vector& colors() const noexcept { return colors_; } + void set_colors(std::vector colors); + [[nodiscard]] int size() const noexcept { return static_cast(colors_.size()); } + [[nodiscard]] Pixel at_normalized(double value) const noexcept; + +private: + std::vector colors_; +}; + +} // namespace renderive diff --git a/Core2/event/Event.h b/Core2/event/Event.h new file mode 100644 index 0000000..fb3475d --- /dev/null +++ b/Core2/event/Event.h @@ -0,0 +1,92 @@ +#pragma once + +#include "../base/Types.h" + +namespace renderive { + +enum class Event_Type : std::uint8_t { + Resize, + Show, + Hide, + Leave, + Pointer_Move, + Pointer_Press, + Pointer_Release, + Wheel, + Key_Press, + Key_Release +}; + +struct Event { + explicit Event(Event_Type value) : type(value) {} + virtual ~Event() = default; + void accept() const noexcept { accepted = true; } + [[nodiscard]] bool is_accepted() const noexcept { return accepted; } + + Event_Type type; +private: + mutable bool accepted{}; +}; + +enum class Mouse_Button : std::uint8_t { None, Left, Right, Middle }; +using Mouse_Button_Mask = std::uint8_t; + +enum class Keyboard_Modifier : std::uint8_t { + None = 0, + Ctrl = 1 << 0, + Shift = 1 << 1, + Alt = 1 << 2, + Meta = 1 << 3 +}; + +constexpr Keyboard_Modifier operator|(Keyboard_Modifier left, Keyboard_Modifier right) noexcept { + return static_cast(static_cast(left) | + static_cast(right)); +} + +struct Pointer_Event : Event { + explicit Pointer_Event(Event_Type value = Event_Type::Pointer_Move) : Event(value) {} + PointF position; + PointF global_position; + Mouse_Button button = Mouse_Button::None; + Mouse_Button_Mask buttons{}; + Keyboard_Modifier modifiers = Keyboard_Modifier::None; +}; + +struct Wheel_Event : Pointer_Event { + Wheel_Event() : Pointer_Event(Event_Type::Wheel) {} + double angle_delta_x{}; + double angle_delta_y{}; + double pixel_delta_x{}; + double pixel_delta_y{}; +}; + +struct Resize_Event : Event { + Resize_Event() : Event(Event_Type::Resize) {} + Size old_size; + Size new_size; +}; + +enum class Key : std::uint16_t { + Unknown, + Escape, + Enter, + Space, + Delete, + Backspace, + Left, + Right, + Up, + Down +}; + +struct Key_Event : Event { + explicit Key_Event(Event_Type value) : Event(value) {} + Key key = Key::Unknown; + std::uint32_t native_key{}; + Keyboard_Modifier modifiers = Keyboard_Modifier::None; + bool auto_repeat{}; +}; + +} // namespace renderive + diff --git a/Core2/export.h b/Core2/export.h new file mode 100644 index 0000000..6549629 --- /dev/null +++ b/Core2/export.h @@ -0,0 +1,11 @@ +#pragma once + +#include "base/Types.h" +#include "event/Event.h" +#include "renderable/Renderable.h" +#include "plot/Plot_Core.h" +#include "axis/Axis.h" +#include "plottable/Hover_Tooltip.h" +#include "plottable/Plottables.h" +#include "plottable/Performance_Overlay.h" + diff --git a/Core2/plot/Plot_Core.cpp b/Core2/plot/Plot_Core.cpp new file mode 100644 index 0000000..dd636fc --- /dev/null +++ b/Core2/plot/Plot_Core.cpp @@ -0,0 +1,286 @@ +#include "Plot_Core.h" + +#include "../plottable/Performance_Overlay.h" +#include "../render/Blend2D_Cache.h" +#include "../renderable/Renderable.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace renderive { +namespace { + +using Plot_Frame_Control = ::Low_Latency_Strategy<::Scene2D_Frame_Data>; +using Plot_Scene = ::Scene2D_Context; + +Rect full_rect(Size size) { + return {0, 0, std::max(0, size.width), std::max(0, size.height)}; +} + +class Renderable_Group final : public Renderable { +public: + using Renderable::Renderable; + +private: + void paint(detail::Painter&) override {} +}; + +} // namespace + +struct Plot_Core::Impl { + Impl() + : scene(*memory_resource(Memory_Domain::Plot_Frame)) {} + + Plot_Scene scene; + mutable std::mutex mutex; + std::mutex initialization_mutex; + std::weak_ptr presentation_sink; + std::shared_ptr performance; + Color background = Color::black(); + Size viewport; + std::atomic_bool active{}; + std::atomic_bool dirty{true}; +}; + +Plot_Core::Plot_Core() + : impl_(std::make_unique()) {} + +Plot_Core::~Plot_Core() = default; + +void Plot_Core::init() { + std::lock_guard initialization_lock(impl_->initialization_mutex); + if (root_renderable()) + return; + auto root = renderive::make_shared(Memory_Domain::Renderable, *this, true); + root->set_object_name("root"); + impl_->scene.attach_renderable(root); + notify_model_dirty(); +} + +std::shared_ptr Plot_Core::root_renderable() const { + const auto topology = impl_->scene.topology_snapshot(); + for (const auto& relationship : topology.display) { + if (relationship.parent) + continue; + auto base = std::const_pointer_cast<::Renderable_Base>(relationship.child); + if (auto renderable = std::dynamic_pointer_cast(base)) + return renderable; + } + return {}; +} + +std::shared_ptr Plot_Core::create_renderable_node( + const std::shared_ptr& parent, std::string object_name) { + auto renderable = renderive::make_shared( + Memory_Domain::Renderable, *this, true); + renderable->set_object_name(std::move(object_name)); + attach_renderable(renderable, parent); + return renderable; +} + +void Plot_Core::attach_renderable(const std::shared_ptr& renderable, + const std::shared_ptr& parent) { + if (!renderable) + throw std::invalid_argument("renderable is null"); + Renderable* parent_pointer = parent.get(); + if (!parent_pointer) { + auto root = root_renderable(); + if (!root) + throw std::logic_error("Plot_Core::init must be called before adding renderables"); + parent_pointer = root.get(); + } + impl_->scene.attach_renderable(renderable); + try { + impl_->scene.set_display_parent(*renderable, parent_pointer); + impl_->scene.set_dependency_parent(*renderable, parent_pointer); + } catch (...) { + impl_->scene.detach_renderable(*renderable); + throw; + } + notify_model_dirty(); +} + +void Plot_Core::remove_renderable(const std::shared_ptr& renderable) { + if (!renderable || renderable == root_renderable()) + return; + impl_->scene.detach_renderable(*renderable); + notify_model_dirty(); +} + +void Plot_Core::set_background_color(Color color) { + { + std::lock_guard lock(impl_->mutex); + if (impl_->background == color) + return; + impl_->background = color; + } + notify_model_dirty(); +} + +Color Plot_Core::background_color() const noexcept { + std::lock_guard lock(impl_->mutex); + return impl_->background; +} + +void Plot_Core::set_viewport_size(Size size) { + size.width = std::max(0, size.width); + size.height = std::max(0, size.height); + { + std::lock_guard lock(impl_->mutex); + if (impl_->viewport == size) + return; + impl_->viewport = size; + } + const auto topology = impl_->scene.topology_snapshot(); + for (const auto& renderable : topology.renderables) + const_cast<::Renderable_Base&>(*renderable).invalidate_cache(); + notify_model_dirty(); +} + +Size Plot_Core::viewport_size() const noexcept { + std::lock_guard lock(impl_->mutex); + return impl_->viewport; +} + +void Plot_Core::dispatch_event(const Event& event) { + const auto topology = impl_->scene.topology_snapshot(); + for (auto iterator = topology.display.rbegin(); iterator != topology.display.rend(); ++iterator) { + auto base = std::const_pointer_cast<::Renderable_Base>(iterator->child); + if (auto renderable = std::dynamic_pointer_cast(base)) { + if (renderable->is_visible()) { + if (auto handler = std::dynamic_pointer_cast(base)) + handler->handle_event(event); + } + if (event.is_accepted()) + break; + } + } +} + +void Plot_Core::notify_model_dirty() noexcept { + impl_->dirty.store(true, std::memory_order_release); +} + +bool Plot_Core::render_frame(bool force) { + if (!view_active() && !force) + return false; + const Size viewport = viewport_size(); + if (viewport.empty()) + return false; + if (!impl_->dirty.exchange(false, std::memory_order_acq_rel) && !force) + return false; + + const auto started = std::chrono::steady_clock::now(); + try { + { + auto paint_frame = impl_->scene.frame_control.acquire_painter(); + auto& scene_state = static_cast(impl_->scene); + scene_state.template set<&::Scene2D_State::revision>(scene_state.state_revision() + 1); + scene_state.publish(); + } + { + auto render_frame = impl_->scene.frame_control.acquire_renderer(); + impl_->scene.render(); + impl_->scene.wait_for_render(); + } + } catch (...) { + notify_model_dirty(); + throw; + } + const auto finished = std::chrono::steady_clock::now(); + const double duration_ms = std::chrono::duration(finished - started).count(); + + std::shared_ptr sink; + std::shared_ptr overlay; + { + std::lock_guard lock(impl_->mutex); + sink = impl_->presentation_sink.lock(); + overlay = impl_->performance; + } + if (overlay) + overlay->record_frame(duration_ms, diagnostics(), impl_->scene.renderable_count(), viewport); + if (sink) + sink->request_present(full_rect(viewport)); + return true; +} + +void Plot_Core::with_frame(const std::function& consumer) { + if (!consumer) + return; + impl_->scene.with_final_color_cache([&consumer](const detail::Blend2D_Color_Cache& cache) { + consumer(cache.view()); + }); +} + +void Plot_Core::activate_view() noexcept { + impl_->active.store(true, std::memory_order_release); + notify_model_dirty(); +} + +void Plot_Core::deactivate_view() noexcept { + impl_->active.store(false, std::memory_order_release); +} + +bool Plot_Core::view_active() const noexcept { + return impl_->active.load(std::memory_order_acquire); +} + +void Plot_Core::set_max_render_fps(double fps) { + if (!std::isfinite(fps) || fps <= 0.0) + throw std::invalid_argument("maximum render FPS must be finite and positive"); + impl_->scene.frame_control.set_frequency_hz(fps); + notify_model_dirty(); +} + +double Plot_Core::max_render_fps() const noexcept { + return impl_->scene.frame_control.state().frequency_hz; +} + +Low_Latency_Diagnostics Plot_Core::diagnostics() const { + const auto state = impl_->scene.frame_control.state(); + return { + view_active(), + {state.frequency_hz, state.completed_lifecycle_count, state.next_refresh_interval_ns} + }; +} + +Refresh_Control_Snapshot Plot_Core::refresh_feedback_snapshot() const { + return diagnostics().refresh; +} + +void Plot_Core::set_presentation_sink(std::weak_ptr sink) { + std::lock_guard lock(impl_->mutex); + impl_->presentation_sink = std::move(sink); +} + +std::shared_ptr Plot_Core::performance_overlay() const { + std::lock_guard lock(impl_->mutex); + return impl_->performance; +} + +void Plot_Core::set_performance_overlay(std::shared_ptr overlay) { + { + std::lock_guard lock(impl_->mutex); + impl_->performance = std::move(overlay); + } + notify_model_dirty(); +} + +::Scene_Base& Plot_Core::kernel_scene() const noexcept { + return impl_->scene; +} + +void Plot_Core::set_renderable_cache(Renderable& renderable, Renderable_Cache_Mode mode) { + impl_->scene.set_renderable_configuration( + renderable, {.cache_enabled = mode == Renderable_Cache_Mode::Local_Pixel}); + notify_model_dirty(); +} + +} // namespace renderive diff --git a/Core2/plot/Plot_Core.h b/Core2/plot/Plot_Core.h new file mode 100644 index 0000000..67cde93 --- /dev/null +++ b/Core2/plot/Plot_Core.h @@ -0,0 +1,97 @@ +#pragma once + +#include "../base/Types.h" +#include "../event/Event.h" + +#include +#include +#include +#include +#include + +class Scene_Base; + +namespace renderive { + +class Performance_Overlay; +struct Performance_Overlay_Options; +class Renderable; + +struct Refresh_Control_Snapshot { + double frequency_hz{}; + std::uint64_t frame_count{}; + std::uint64_t next_refresh_interval_ns{}; +}; + +struct Low_Latency_Diagnostics { + bool active{}; + Refresh_Control_Snapshot refresh; +}; + +class Presentation_Sink { +public: + virtual ~Presentation_Sink() = default; + virtual void request_present(Rect dirty_rect) = 0; +}; + +class LIB_DECL Plot_Core { +public: + Plot_Core(); + ~Plot_Core(); + Plot_Core(const Plot_Core&) = delete; + Plot_Core& operator=(const Plot_Core&) = delete; + + void init(); + [[nodiscard]] std::shared_ptr root_renderable() const; + [[nodiscard]] std::shared_ptr create_renderable_node( + const std::shared_ptr& parent, + std::string object_name = {}); + + template + requires std::derived_from && std::constructible_from + std::shared_ptr make_renderable(const std::shared_ptr& parent, Args&&... args) { + auto renderable = renderive::make_shared(Memory_Domain::Renderable, *this, + std::forward(args)...); + attach_renderable(renderable, parent); + return renderable; + } + + void remove_renderable(const std::shared_ptr& renderable); + void set_background_color(Color color); + [[nodiscard]] Color background_color() const noexcept; + void set_viewport_size(Size size); + [[nodiscard]] Size viewport_size() const noexcept; + + void dispatch_event(const Event& event); + void notify_model_dirty() noexcept; + [[nodiscard]] bool render_frame(bool force = false); + void with_frame(const std::function& consumer); + + void activate_view() noexcept; + void deactivate_view() noexcept; + [[nodiscard]] bool view_active() const noexcept; + + void set_max_render_fps(double fps); + [[nodiscard]] double max_render_fps() const noexcept; + [[nodiscard]] Low_Latency_Diagnostics diagnostics() const; + [[nodiscard]] Refresh_Control_Snapshot refresh_feedback_snapshot() const; + + void set_presentation_sink(std::weak_ptr sink); + [[nodiscard]] std::shared_ptr performance_overlay() const; + +private: + friend class Renderable; + friend std::shared_ptr attach_performance_overlay(Plot_Core&); + friend std::shared_ptr attach_performance_overlay( + Plot_Core&, const Performance_Overlay_Options&); + struct Impl; + std::unique_ptr impl_; + + [[nodiscard]] ::Scene_Base& kernel_scene() const noexcept; + void attach_renderable(const std::shared_ptr& renderable, + const std::shared_ptr& parent); + void set_renderable_cache(Renderable& renderable, Renderable_Cache_Mode mode); + void set_performance_overlay(std::shared_ptr overlay); +}; + +} // namespace renderive diff --git a/Core2/plottable/Curve_Sampling.h b/Core2/plottable/Curve_Sampling.h new file mode 100644 index 0000000..9c2736c --- /dev/null +++ b/Core2/plottable/Curve_Sampling.h @@ -0,0 +1,150 @@ +#pragma once + +#include "../axis/Axis.h" + +#include +#include +#include +#include + +namespace renderive::detail { +namespace curve_sampling { + +struct Sample { + double coordinate{}; + double value{}; +}; + +inline double power_domain_lerp(double first, double second, double ratio) { + const double first_power = std::pow(10.0, std::clamp(first, -3'000.0, 3'000.0) / 10.0); + const double second_power = std::pow(10.0, std::clamp(second, -3'000.0, 3'000.0) / 10.0); + const double power = first_power + (second_power - first_power) * ratio; + return 10.0 * std::log10(std::max(power, 1e-300)); +} + +inline double cubic_value(double previous, double first, double second, double next, + double ratio) { + const double ratio2 = ratio * ratio; + const double ratio3 = ratio2 * ratio; + return 0.5 * ((2.0 * first) + (-previous + second) * ratio + + (2.0 * previous - 5.0 * first + 4.0 * second - next) * ratio2 + + (-previous + 3.0 * first - 3.0 * second + next) * ratio3); +} + +inline std::vector interpolate(std::span values, Range domain, + Line_Interpolation_Mode mode) { + std::vector result; + if (values.empty()) + return result; + if (values.size() == 1) { + result.push_back({domain.origin, values.front()}); + return result; + } + + constexpr int smooth_subdivisions = 4; + const std::size_t multiplier = + mode == Line_Interpolation_Mode::Linear_Power_Domain || + mode == Line_Interpolation_Mode::Cubic_Value + ? smooth_subdivisions + : mode == Line_Interpolation_Mode::Linear_Value ? 1 : 3; + result.reserve(1 + (values.size() - 1) * multiplier); + + const double denominator = static_cast(values.size() - 1); + const auto coordinate_at = [domain, denominator](std::size_t index) { + return domain.origin + domain.length() * static_cast(index) / denominator; + }; + result.push_back({coordinate_at(0), values.front()}); + for (std::size_t index = 0; index + 1 < values.size(); ++index) { + const double first_coordinate = coordinate_at(index); + const double second_coordinate = coordinate_at(index + 1); + const double first_value = values[index]; + const double second_value = values[index + 1]; + switch (mode) { + case Line_Interpolation_Mode::Nearest_Sample: { + const double middle = (first_coordinate + second_coordinate) * 0.5; + result.push_back({middle, first_value}); + result.push_back({middle, second_value}); + result.push_back({second_coordinate, second_value}); + break; + } + case Line_Interpolation_Mode::Linear_Value: + result.push_back({second_coordinate, second_value}); + break; + case Line_Interpolation_Mode::Linear_Power_Domain: + for (int part = 1; part <= smooth_subdivisions; ++part) { + const double ratio = static_cast(part) / smooth_subdivisions; + result.push_back({first_coordinate + + (second_coordinate - first_coordinate) * ratio, + power_domain_lerp(first_value, second_value, ratio)}); + } + break; + case Line_Interpolation_Mode::Step_Left: + result.push_back({second_coordinate, first_value}); + result.push_back({second_coordinate, second_value}); + break; + case Line_Interpolation_Mode::Step_Right: + result.push_back({first_coordinate, second_value}); + result.push_back({second_coordinate, second_value}); + break; + case Line_Interpolation_Mode::Cubic_Value: { + const double previous = values[index == 0 ? 0 : index - 1]; + const double next = values[std::min(index + 2, values.size() - 1)]; + for (int part = 1; part <= smooth_subdivisions; ++part) { + const double ratio = static_cast(part) / smooth_subdivisions; + result.push_back({first_coordinate + + (second_coordinate - first_coordinate) * ratio, + cubic_value(previous, first_value, second_value, next, ratio)}); + } + break; + } + } + } + return result; +} + +inline std::vector visible_samples(std::vector samples, + Range visible_range) { + if (samples.size() < 2) + return visible_range.contains(samples.empty() ? 0.0 : samples.front().coordinate) + ? std::move(samples) + : std::vector{}; + const auto [visible_low, visible_high] = + std::minmax(visible_range.origin, visible_range.target); + std::size_t first = samples.size(); + std::size_t last{}; + for (std::size_t index = 0; index + 1 < samples.size(); ++index) { + const auto [segment_low, segment_high] = + std::minmax(samples[index].coordinate, samples[index + 1].coordinate); + if (segment_high < visible_low || segment_low > visible_high) + continue; + first = std::min(first, index); + last = std::max(last, index + 1); + } + if (first == samples.size()) + return {}; + return {samples.begin() + static_cast(first), + samples.begin() + static_cast(last + 1)}; +} + +} // namespace curve_sampling + +inline std::vector curve_points(std::span values, + Range domain, + const Axis_Transform& x_axis, + const Axis_Transform& y_axis, + bool visible_only, + Line_Interpolation_Mode mode) { + auto samples = curve_sampling::interpolate(values, domain, mode); + if (visible_only) + samples = curve_sampling::visible_samples(std::move(samples), x_axis.coordinate_range); + std::vector points; + 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)}); + } + return points; +} + +} // namespace renderive::detail diff --git a/Core2/plottable/Heatmaps.cpp b/Core2/plottable/Heatmaps.cpp new file mode 100644 index 0000000..88821b8 --- /dev/null +++ b/Core2/plottable/Heatmaps.cpp @@ -0,0 +1,421 @@ +#include "Plottables.h" + +#include "../plot/Plot_Core.h" +#include "../render/Blend2D_Cache.h" + +#include +#include +#include +#include +#include + +namespace renderive { +namespace { + +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)}; +} + +double normalized_value(double value, Range range) { + if (range.length() == 0.0) + return 0.0; + return std::clamp((value - range.origin) / range.length(), 0.0, 1.0); +} + +struct Frequency_Columns { + int first{}; + int last{}; + Range range; +}; + +std::optional frequency_columns(Range data_range, + Range visible_range, + int column_count, + bool visible_only) { + if (column_count <= 0) + return std::nullopt; + if (!visible_only || column_count == 1 || data_range.length() == 0.0) + return Frequency_Columns{0, column_count - 1, data_range}; + + const auto [data_low, data_high] = std::minmax(data_range.origin, data_range.target); + const auto [visible_low, visible_high] = std::minmax(visible_range.origin, + visible_range.target); + const double clipped_low = std::max(data_low, visible_low); + const double clipped_high = std::min(data_high, visible_high); + if (clipped_low > clipped_high) + return std::nullopt; + + const auto position = [data_range, column_count](double coordinate) { + return (coordinate - data_range.origin) / data_range.length() * + static_cast(column_count - 1); + }; + const auto [position_low, position_high] = + std::minmax(position(clipped_low), position(clipped_high)); + int first = std::clamp(static_cast(std::floor(position_low)), + 0, column_count - 1); + int last = std::clamp(static_cast(std::ceil(position_high)), + first, column_count - 1); + if (first == last) { + if (last + 1 < column_count) + ++last; + else if (first > 0) + --first; + } + const auto coordinate = [data_range, column_count](int index) { + return data_range.origin + data_range.length() * static_cast(index) / + static_cast(column_count - 1); + }; + return Frequency_Columns{first, last, {coordinate(first), coordinate(last)}}; +} + +} // namespace + +Waterfall::Waterfall(Plot_Core& plot, + std::shared_ptr frequency_axis, + std::shared_ptr time_axis) + : Renderable(plot) { + state_.frequency_axis = std::move(frequency_axis); + state_.time_axis = std::move(time_axis); +} + +void Waterfall::append_row(int tick, std::span values) { + std::shared_ptr axis; + { + std::lock_guard lock(mutex_); + state_.rows.push_back(Row{tick, {values.begin(), values.end()}}); + if (state_.bin_count <= 0) + state_.bin_count = static_cast(values.size()); + axis = state_.time_axis; + } + const std::size_t limit = axis + ? static_cast(std::max(2, axis->visible_time_point_count())) + : 256; + { + std::lock_guard lock(mutex_); + while (state_.rows.size() > limit) + state_.rows.pop_front(); + } + changed(); +} + +void Waterfall::append_row(int 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) { + auto axis = time_axis(); + if (!axis) + return; + append_row(axis->append_time(time), values); +} + +void Waterfall::append_row(Time_Of_Day time, std::pmr::vector&& values) { + append_row(time, std::span(values.data(), values.size())); +} + +std::shared_ptr Waterfall::frequency_axis() const { std::lock_guard lock(mutex_); return state_.frequency_axis; } +std::shared_ptr Waterfall::time_axis() const { std::lock_guard lock(mutex_); return state_.time_axis; } + +#define RENDERIVE_WATERFALL_PROPERTY(Type, Method, Field) \ + Type Waterfall::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ + void Waterfall::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } + +RENDERIVE_WATERFALL_PROPERTY(Range, frequency_range, frequency_range) +RENDERIVE_WATERFALL_PROPERTY(Range, power_range, power_range) +RENDERIVE_WATERFALL_PROPERTY(bool, visible_range_only, visible_only) +RENDERIVE_WATERFALL_PROPERTY(Image_Interpolation_Mode, interpolation_mode, interpolation) + +#undef RENDERIVE_WATERFALL_PROPERTY + +int Waterfall::frequency_bin_count() const { + std::lock_guard lock(mutex_); + return state_.bin_count; +} + +void Waterfall::set_frequency_bin_count(int value) { + { + std::lock_guard lock(mutex_); + state_.bin_count = std::max(0, value); + } + changed(); +} + +Waterfall::State Waterfall::state_snapshot() const { std::lock_guard lock(mutex_); return state_; } + +void Waterfall::handle_event(const Event& event) { update_hover(event); } + +void Waterfall::paint(detail::Painter& painter) { + const State state = state_snapshot(); + if (!state.frequency_axis || !state.time_axis || state.rows.empty()) + return; + const int source_width = std::min(state.bin_count, + static_cast(std::min_element( + state.rows.begin(), state.rows.end(), + [](const Row& left, const Row& right) { + return left.values.size() < right.values.size(); + })->values.size())); + const int height = static_cast(state.rows.size()); + if (source_width <= 0 || height <= 0) + return; + const Axis_Transform horizontal = state.frequency_axis->transform(); + const auto columns = frequency_columns(state.frequency_range, + horizontal.coordinate_range, + source_width, + state.visible_only); + if (!columns) + return; + const int width = columns->last - columns->first + 1; + std::vector pixels(static_cast(width) * height); + for (int y = 0; y < height; ++y) { + const auto& row = state.rows[static_cast(y)].values; + for (int x = 0; x < width; ++x) { + pixels[static_cast(y) * width + x] = + state.color_map.at_normalized(normalized_value( + row[static_cast(columns->first + x)], + state.power_range)); + } + } + const Axis_Transform vertical = state.time_axis->transform(); + const Range time_range{static_cast(state.rows.front().tick), + static_cast(state.rows.back().tick)}; + RectF target = mapped_rect(horizontal, vertical, columns->range, time_range); + if (target.height < 1.0) + target.height = std::max(1.0, static_cast(state.time_axis->pixel_length())); + painter.heatmap(target, width, height, pixels, state.interpolation); + + const Hover_Tooltip_Snapshot tooltip = tooltip_snapshot(); + if (tooltip.enabled && tooltip.active && target.contains(tooltip.position)) { + const double frequency = horizontal.pixel_to_coord(tooltip.position.x); + std::ostringstream text; + text << std::fixed << std::setprecision(2) << frequency << " Hz"; + const RectF box{tooltip.position.x + 8.0, tooltip.position.y + 8.0, 110.0, 24.0}; + painter.rect(box, Pen{tooltip.text_pen.color}, tooltip.background); + painter.text({box.x + 4.0, box.y + 3.0}, text.str(), tooltip.font, tooltip.text_pen); + } +} + +Waterfall::Builder::Builder(std::shared_ptr parent, + std::shared_ptr frequency_axis, + std::shared_ptr time_axis) + : parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)), + time_axis_(std::move(time_axis)) {} + +std::shared_ptr Waterfall::Builder::build() { + if (!parent_ || !frequency_axis_ || !time_axis_) + return {}; + auto result = parent_->plot().make_renderable(parent_, frequency_axis_, time_axis_); + result->set_frequency_range(frequency_range_); + result->set_power_range(power_range_); + result->set_frequency_bin_count(bin_count_); + result->set_visible_range_only(visible_only_); + result->set_interpolation_mode(interpolation_); + { + std::lock_guard lock(result->mutex_); + result->state_.color_map = color_map_; + } + return result; +} + +Frequency_Trace::Frequency_Trace(Plot_Core& plot, + std::shared_ptr time_axis, + std::shared_ptr value_axis) + : Renderable(plot), time_axis_(std::move(time_axis)), value_axis_(std::move(value_axis)) {} + +void Frequency_Trace::append_sample(int tick, double value) { + int limit = 512; + { + std::lock_guard lock(mutex_); + samples_.emplace_back(tick, value); + if (time_axis_) + limit = std::max(2, time_axis_->visible_time_point_count()); + while (samples_.size() > static_cast(limit)) + samples_.pop_front(); + } + changed(); +} + +void Frequency_Trace::append_sample(Time_Of_Day time, double value) { + auto axis = time_axis(); + if (axis) + append_sample(axis->append_time(time), value); +} + +std::shared_ptr Frequency_Trace::time_axis() const { std::lock_guard lock(mutex_); return time_axis_; } +std::shared_ptr Frequency_Trace::value_axis() const { std::lock_guard lock(mutex_); return value_axis_; } + +void Frequency_Trace::paint(detail::Painter& painter) { + std::shared_ptr time_axis; + std::shared_ptr value_axis; + std::deque> samples; + Pen pen; + { + std::lock_guard lock(mutex_); + time_axis = time_axis_; + value_axis = value_axis_; + samples = samples_; + pen = pen_; + } + if (!time_axis || !value_axis || samples.size() < 2) + return; + const Axis_Transform x = time_axis->transform(); + const Axis_Transform y = value_axis->transform(); + std::vector points; + points.reserve(samples.size()); + for (const auto& [tick, value] : samples) + points.push_back({x.coord_to_pixel(tick), y.coord_to_pixel(value)}); + painter.polyline(points, pen); +} + +Frequency_Trace::Builder::Builder(std::shared_ptr parent, + std::shared_ptr time_axis, + std::shared_ptr value_axis) + : parent_(std::move(parent)), time_axis_(std::move(time_axis)), + value_axis_(std::move(value_axis)) {} + +std::shared_ptr Frequency_Trace::Builder::build() { + if (!parent_ || !time_axis_ || !value_axis_) + return {}; + auto result = parent_->plot().make_renderable(parent_, time_axis_, value_axis_); + { std::lock_guard lock(result->mutex_); result->pen_ = pen_; } + return result; +} + +Afterglow::Afterglow(Plot_Core& plot, + std::shared_ptr frequency_axis, + std::shared_ptr power_axis) + : Renderable(plot) { + state_.frequency_axis = std::move(frequency_axis); + state_.power_axis = std::move(power_axis); +} + +std::shared_ptr Afterglow::frequency_axis() const { std::lock_guard lock(mutex_); return state_.frequency_axis; } +std::shared_ptr Afterglow::power_axis() const { std::lock_guard lock(mutex_); return state_.power_axis; } + +#define RENDERIVE_AFTERGLOW_PROPERTY(Type, Method, Field) \ + Type Afterglow::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ + void Afterglow::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } + +RENDERIVE_AFTERGLOW_PROPERTY(Range, frequency_range, frequency_range) +RENDERIVE_AFTERGLOW_PROPERTY(Range, power_range, power_range) +RENDERIVE_AFTERGLOW_PROPERTY(bool, interpolate, interpolate) + +#undef RENDERIVE_AFTERGLOW_PROPERTY + +int Afterglow::frequency_point_size() const { + std::lock_guard lock(mutex_); + return state_.frequency_count; +} + +void Afterglow::set_frequency_point_size(int value) { + { std::lock_guard lock(mutex_); state_.frequency_count = std::max(0, value); } + changed(); +} + +int Afterglow::power_point_size() const { + std::lock_guard lock(mutex_); + return state_.power_count; +} + +void Afterglow::set_power_point_size(int value) { + { std::lock_guard lock(mutex_); state_.power_count = std::max(0, value); } + changed(); +} + +double Afterglow::attenuation_rate() const { + std::lock_guard lock(mutex_); + return state_.attenuation; +} + +void Afterglow::set_attenuation_rate(double value) { + if (!std::isfinite(value)) + value = 0.0; + { std::lock_guard lock(mutex_); state_.attenuation = std::clamp(value, 0.0, 1.0); } + changed(); +} + +void Afterglow::append_spectrum(std::span values) { + { + std::lock_guard lock(mutex_); + state_.history.emplace_back(values.begin(), values.end()); + if (state_.frequency_count <= 0) + state_.frequency_count = static_cast(values.size()); + while (state_.history.size() > 64) + state_.history.pop_front(); + } + changed(); +} + +void Afterglow::append_spectrum(std::pmr::vector&& values) { + append_spectrum(std::span(values.data(), values.size())); +} + +Afterglow::State Afterglow::state_snapshot() const { std::lock_guard lock(mutex_); return state_; } + +void Afterglow::paint(detail::Painter& painter) { + const State state = state_snapshot(); + if (!state.frequency_axis || !state.power_axis || state.history.empty()) + return; + const int width = std::min(state.frequency_count, + static_cast(state.history.back().size())); + const int height = state.power_count > 0 + ? state.power_count + : std::max(1, static_cast(state.power_axis->pixel_length())); + if (width <= 0 || height <= 0) + return; + std::vector intensity(static_cast(width) * height); + double weight = 1.0; + const double decay = 1.0 - state.attenuation; + for (auto iterator = state.history.rbegin(); iterator != state.history.rend(); ++iterator) { + const auto& spectrum = *iterator; + const int count = std::min(width, static_cast(spectrum.size())); + for (int x = 0; x < count; ++x) { + const double normalized = normalized_value(spectrum[static_cast(x)], + state.power_range); + const int y = std::clamp(height - 1 - static_cast(normalized * (height - 1)), + 0, height - 1); + intensity[static_cast(y) * width + x] += weight; + if (state.interpolate && y + 1 < height) + intensity[static_cast(y + 1) * 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 pixels(intensity.size()); + for (std::size_t index = 0; index < pixels.size(); ++index) + pixels[index] = state.color_map.at_normalized(intensity[index] / maximum); + const RectF target = mapped_rect(state.frequency_axis->transform(), state.power_axis->transform(), + state.frequency_range, state.power_range); + painter.heatmap(target, width, height, pixels, Image_Interpolation_Mode::Bilinear); +} + +Afterglow::Builder::Builder(std::shared_ptr parent, + std::shared_ptr frequency_axis, + std::shared_ptr power_axis) + : parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)), + power_axis_(std::move(power_axis)) {} + +std::shared_ptr Afterglow::Builder::build() { + if (!parent_ || !frequency_axis_ || !power_axis_) + return {}; + auto result = parent_->plot().make_renderable(parent_, frequency_axis_, power_axis_); + result->set_frequency_range(frequency_range_); + result->set_power_range(power_range_); + result->set_frequency_point_size(frequency_count_); + result->set_power_point_size(power_count_); + result->set_interpolate(interpolate_); + result->set_attenuation_rate(attenuation_); + { + std::lock_guard lock(result->mutex_); + result->state_.color_map = color_map_; + } + return result; +} + +} // namespace renderive diff --git a/Core2/plottable/Hover_Tooltip.h b/Core2/plottable/Hover_Tooltip.h new file mode 100644 index 0000000..0174763 --- /dev/null +++ b/Core2/plottable/Hover_Tooltip.h @@ -0,0 +1,89 @@ +#pragma once + +#include "../event/Event.h" + +#include + +namespace renderive { + +struct Hover_Tooltip_Snapshot { + bool enabled{true}; + bool active{}; + PointF position; + Brush background{Color::white(), Brush_Style::Solid}; + Pen text_pen{Color::black()}; + Font font; +}; + +template +class Hover_Tooltip_Mixin { +public: + [[nodiscard]] bool hover_tooltip_enabled() const { + std::lock_guard lock(tooltip_mutex_); + return tooltip_.enabled; + } + void set_use_hover_info(bool enabled) { + { + std::lock_guard lock(tooltip_mutex_); + tooltip_.enabled = enabled; + if (!enabled) + tooltip_.active = false; + } + static_cast(this)->tooltip_changed(); + } + [[nodiscard]] Font hover_tooltip_font() const { + std::lock_guard lock(tooltip_mutex_); + return tooltip_.font; + } + void set_hover_tooltip_font(Font value) { + { std::lock_guard lock(tooltip_mutex_); tooltip_.font = value; } + static_cast(this)->tooltip_changed(); + } + [[nodiscard]] Brush hover_info_background_brush() const { + std::lock_guard lock(tooltip_mutex_); + return tooltip_.background; + } + void set_hover_tooltip_background_brush(Brush value) { + { std::lock_guard lock(tooltip_mutex_); tooltip_.background = value; } + static_cast(this)->tooltip_changed(); + } + [[nodiscard]] Pen tooltip_text_pen() const { + std::lock_guard lock(tooltip_mutex_); + return tooltip_.text_pen; + } + void set_tooltip_text_pen(Pen value) { + { std::lock_guard lock(tooltip_mutex_); tooltip_.text_pen = value; } + static_cast(this)->tooltip_changed(); + } + +protected: + void update_hover(const Event& event) { + bool changed{}; + { + std::lock_guard lock(tooltip_mutex_); + if (event.type == Event_Type::Pointer_Move) { + const auto& pointer = static_cast(event); + changed = !tooltip_.active || tooltip_.position.x != pointer.position.x || + tooltip_.position.y != pointer.position.y; + tooltip_.active = tooltip_.enabled; + tooltip_.position = pointer.position; + } else if (event.type == Event_Type::Leave) { + changed = tooltip_.active; + tooltip_.active = false; + } + } + if (changed) + static_cast(this)->tooltip_changed(); + } + [[nodiscard]] Hover_Tooltip_Snapshot tooltip_snapshot() const { + std::lock_guard lock(tooltip_mutex_); + return tooltip_; + } + +private: + mutable std::mutex tooltip_mutex_; + Hover_Tooltip_Snapshot tooltip_; +}; + +} // namespace renderive + diff --git a/Core2/plottable/Overlays.cpp b/Core2/plottable/Overlays.cpp new file mode 100644 index 0000000..7e6ad50 --- /dev/null +++ b/Core2/plottable/Overlays.cpp @@ -0,0 +1,402 @@ +#include "Plottables.h" + +#include "Curve_Sampling.h" +#include "../plot/Plot_Core.h" +#include "../render/Blend2D_Cache.h" + +#include +#include +#include +#include + +namespace renderive { +namespace { + +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)}; +} + +} // namespace + +Sweep_Spectrum::Sweep_Spectrum(Plot_Core& plot, + std::shared_ptr frequency_axis, + std::shared_ptr power_axis) + : Renderable(plot) { + state_.frequency_axis = std::move(frequency_axis); + state_.power_axis = std::move(power_axis); +} + +void Sweep_Spectrum::append_block(std::span values) { + { + std::lock_guard lock(mutex_); + state_.data.emplace_back(values.begin(), values.end()); + if (state_.bins <= 0) + state_.bins = static_cast(values.size()); + const int limit = std::max(1, state_.blocks); + while (state_.data.size() > static_cast(limit)) + state_.data.pop_front(); + } + changed(); +} + +void Sweep_Spectrum::append_block(std::pmr::vector&& values) { + append_block(std::span(values.data(), values.size())); +} + +std::shared_ptr Sweep_Spectrum::frequency_axis() const { std::lock_guard lock(mutex_); return state_.frequency_axis; } +std::shared_ptr Sweep_Spectrum::power_axis() const { std::lock_guard lock(mutex_); return state_.power_axis; } + +#define RENDERIVE_SWEEP_PROPERTY(Type, Method, Field) \ + Type Sweep_Spectrum::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ + void Sweep_Spectrum::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } + +RENDERIVE_SWEEP_PROPERTY(Range, frequency_range, frequency_range) +RENDERIVE_SWEEP_PROPERTY(Pen, pen, pen) +RENDERIVE_SWEEP_PROPERTY(Pen, cur_frequency_pen, current_pen) +RENDERIVE_SWEEP_PROPERTY(bool, visible_range_only, visible_only) +RENDERIVE_SWEEP_PROPERTY(Line_Interpolation_Mode, interpolation_mode, interpolation) + +#undef RENDERIVE_SWEEP_PROPERTY + +int Sweep_Spectrum::bins_per_block() const { + std::lock_guard lock(mutex_); + return state_.bins; +} + +void Sweep_Spectrum::set_bins_per_block(int value) { + { std::lock_guard lock(mutex_); state_.bins = std::max(0, value); } + changed(); +} + +int Sweep_Spectrum::block_count() const { + std::lock_guard lock(mutex_); + return state_.blocks; +} + +void Sweep_Spectrum::set_block_count(int value) { + { + std::lock_guard lock(mutex_); + state_.blocks = std::max(1, value); + while (state_.data.size() > static_cast(state_.blocks)) + state_.data.pop_front(); + } + changed(); +} + +Sweep_Spectrum::State Sweep_Spectrum::state_snapshot() const { std::lock_guard lock(mutex_); return state_; } + +void Sweep_Spectrum::paint(detail::Painter& painter) { + const State state = state_snapshot(); + if (!state.frequency_axis || !state.power_axis || state.data.empty()) + return; + std::vector values; + for (const auto& block : state.data) + values.insert(values.end(), block.begin(), block.end()); + if (values.size() < 2) + return; + const Axis_Transform x = state.frequency_axis->transform(); + const Axis_Transform y = state.power_axis->transform(); + auto points = detail::curve_points(values, state.frequency_range, x, y, + state.visible_only, state.interpolation); + painter.polyline(points, state.pen); + const double completed = state.blocks > 0 + ? std::min(1.0, static_cast(state.data.size()) / state.blocks) + : 1.0; + 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_pen); +} + +Sweep_Spectrum::Builder::Builder(std::shared_ptr parent, + std::shared_ptr frequency_axis, + std::shared_ptr power_axis) + : parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)), + power_axis_(std::move(power_axis)) {} + +std::shared_ptr Sweep_Spectrum::Builder::build() { + if (!parent_ || !frequency_axis_ || !power_axis_) + return {}; + auto result = parent_->plot().make_renderable(parent_, frequency_axis_, power_axis_); + result->set_frequency_range(frequency_range_); + result->set_bins_per_block(bins_); + result->set_block_count(blocks_); + result->set_pen(pen_); + result->set_cur_frequency_pen(current_pen_); + result->set_visible_range_only(visible_only_); + result->set_interpolation_mode(interpolation_); + return result; +} + +Selection_Rectangle_Overlay::Selection_Rectangle_Overlay( + Plot_Core& plot, + std::shared_ptr horizontal_axis, + std::shared_ptr vertical_axis) + : Renderable(plot), horizontal_axis_(std::move(horizontal_axis)), + vertical_axis_(std::move(vertical_axis)) {} + +void Selection_Rectangle_Overlay::set_horizontal_axis(std::shared_ptr axis) { + { std::lock_guard lock(mutex_); horizontal_axis_ = std::move(axis); } + changed(); +} + +void Selection_Rectangle_Overlay::set_vertical_axis(std::shared_ptr axis) { + { std::lock_guard lock(mutex_); vertical_axis_ = std::move(axis); } + changed(); +} + +#define RENDERIVE_SELECTION_PROPERTY(Type, Method, Field) \ + Type Selection_Rectangle_Overlay::Method() const { std::lock_guard lock(mutex_); return Field; } \ + void Selection_Rectangle_Overlay::set_##Method(Type value) { { std::lock_guard lock(mutex_); Field = std::move(value); } changed(); } + +RENDERIVE_SELECTION_PROPERTY(Font, label_font, font_) +RENDERIVE_SELECTION_PROPERTY(Pen, label_pen, label_pen_) +RENDERIVE_SELECTION_PROPERTY(Brush, selection_brush, brush_) +RENDERIVE_SELECTION_PROPERTY(Pen, selection_border_pen, border_) + +#undef RENDERIVE_SELECTION_PROPERTY + +std::vector Selection_Rectangle_Overlay::selected_regions() const { + std::lock_guard lock(mutex_); + return regions_; +} + +void Selection_Rectangle_Overlay::clear_selected_regions() { + { std::lock_guard lock(mutex_); regions_.clear(); selecting_ = false; } + changed(); +} + +void Selection_Rectangle_Overlay::handle_event(const Event& event) { + std::shared_ptr horizontal; + std::shared_ptr vertical; + { + std::lock_guard lock(mutex_); + horizontal = horizontal_axis_; + vertical = vertical_axis_; + } + if (!horizontal || !vertical) + return; + const RectF content = axis_content_rect(horizontal->transform(), vertical->transform()); + if (event.type == Event_Type::Pointer_Press) { + const auto& pointer = static_cast(event); + if (pointer.button != Mouse_Button::Left || !content.contains(pointer.position)) + return; + { + std::lock_guard lock(mutex_); + selecting_ = true; + selection_start_ = selection_current_ = pointer.position; + } + event.accept(); + changed(); + } else if (event.type == Event_Type::Pointer_Move) { + const auto& pointer = static_cast(event); + { + std::lock_guard lock(mutex_); + if (!selecting_) + return; + selection_current_ = pointer.position; + } + event.accept(); + changed(); + } else if (event.type == Event_Type::Pointer_Release) { + const auto& pointer = static_cast(event); + PointF start; + { + std::lock_guard lock(mutex_); + if (!selecting_) + return; + selecting_ = false; + start = selection_start_; + selection_current_ = pointer.position; + } + const double x1 = horizontal->pixel_to_coord(start.x); + const double x2 = horizontal->pixel_to_coord(pointer.position.x); + const double y1 = vertical->pixel_to_coord(start.y); + const double y2 = vertical->pixel_to_coord(pointer.position.y); + const RectF region{x1, y1, x2 - x1, y2 - y1}; + if (std::abs(region.width) > 1e-9 && std::abs(region.height) > 1e-9) { + std::lock_guard lock(mutex_); + regions_.push_back(region.normalized()); + } + event.accept(); + changed(); + } +} + +void Selection_Rectangle_Overlay::paint(detail::Painter& painter) { + std::shared_ptr horizontal; + std::shared_ptr vertical; + std::vector regions; + Font font; + Pen label_pen; + Brush brush; + Pen border; + bool selecting{}; + PointF start; + PointF current; + { + std::lock_guard lock(mutex_); + horizontal = horizontal_axis_; + vertical = vertical_axis_; + regions = regions_; + font = font_; + label_pen = label_pen_; + brush = brush_; + border = border_; + selecting = selecting_; + start = selection_start_; + current = selection_current_; + } + if (!horizontal || !vertical) + return; + for (const RectF& region : regions) { + const double x1 = horizontal->coord_to_pixel(region.x); + const double x2 = horizontal->coord_to_pixel(region.right()); + const double y1 = vertical->coord_to_pixel(region.y); + const double y2 = vertical->coord_to_pixel(region.bottom()); + RectF pixels{x1, y1, x2 - x1, y2 - y1}; + painter.rect(pixels, border, brush); + std::ostringstream text; + text << region.width << " x " << region.height; + const RectF normalized = pixels.normalized(); + painter.text({normalized.x + 3.0, normalized.y + 3.0}, text.str(), font, label_pen); + } + if (selecting) + painter.rect({start.x, start.y, current.x - start.x, current.y - start.y}, border, brush); +} + +Selection_Rectangle_Overlay::Builder::Builder( + std::shared_ptr parent, + std::shared_ptr horizontal_axis, + std::shared_ptr vertical_axis) + : parent_(std::move(parent)), horizontal_axis_(std::move(horizontal_axis)), + vertical_axis_(std::move(vertical_axis)) {} + +std::shared_ptr Selection_Rectangle_Overlay::Builder::build() { + if (!parent_ || !horizontal_axis_ || !vertical_axis_) + return {}; + auto result = parent_->plot().make_renderable( + parent_, horizontal_axis_, vertical_axis_); + result->set_label_font(font_); + result->set_label_pen(label_pen_); + result->set_selection_brush(brush_); + result->set_selection_border_pen(border_); + return result; +} + +Constellation_Diagram::Constellation_Diagram(Plot_Core& plot, + std::shared_ptr i_axis, + std::shared_ptr q_axis) + : Renderable(plot) { + state_.i_axis = std::move(i_axis); + state_.q_axis = std::move(q_axis); +} + +void Constellation_Diagram::append_point(PointF point) { + const auto now = std::chrono::steady_clock::now(); + { + std::lock_guard lock(mutex_); + state_.points.push_back({point, now}); + const auto cutoff = now - std::chrono::milliseconds(std::max(0, state_.lifetime_ms)); + while (!state_.points.empty() && state_.points.front().time < cutoff) + state_.points.pop_front(); + } + changed(); +} + +std::shared_ptr Constellation_Diagram::i_axis() const { std::lock_guard lock(mutex_); return state_.i_axis; } +std::shared_ptr Constellation_Diagram::q_axis() const { std::lock_guard lock(mutex_); return state_.q_axis; } + +#define RENDERIVE_CONSTELLATION_PROPERTY(Type, Method, Field) \ + Type Constellation_Diagram::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ + void Constellation_Diagram::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } + +RENDERIVE_CONSTELLATION_PROPERTY(Range, i_range, i_range) +RENDERIVE_CONSTELLATION_PROPERTY(Range, q_range, q_range) +RENDERIVE_CONSTELLATION_PROPERTY(Color, point_color, point_color) +RENDERIVE_CONSTELLATION_PROPERTY(Color, anchor_color, anchor_color) + +#undef RENDERIVE_CONSTELLATION_PROPERTY + +int Constellation_Diagram::point_lifetime_ms() const { + std::lock_guard lock(mutex_); + return state_.lifetime_ms; +} + +void Constellation_Diagram::set_point_lifetime_ms(int value) { + { + std::lock_guard lock(mutex_); + state_.lifetime_ms = std::max(0, value); + } + changed(); +} + +void Constellation_Diagram::fit_square_to_axes() { + auto horizontal = i_axis(); + auto vertical = q_axis(); + if (!horizontal || !vertical) + return; + const Range i = i_range(); + const Range q = q_range(); + const double side = std::max(i.size(), q.size()); + horizontal->set_coord_range({i.center() - side * 0.5, i.center() + side * 0.5}); + vertical->set_coord_range({q.center() + side * 0.5, q.center() - side * 0.5}); +} + +Constellation_Diagram::State Constellation_Diagram::state_snapshot() const { + std::lock_guard lock(mutex_); + return state_; +} + +void Constellation_Diagram::paint(detail::Painter& painter) { + const State state = state_snapshot(); + if (!state.i_axis || !state.q_axis) + return; + const Axis_Transform x = state.i_axis->transform(); + const Axis_Transform y = state.q_axis->transform(); + const int count = static_cast(state.type); + const double radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; + for (int index = 0; index < count; ++index) { + const double angle = state.phase + 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}); + } + const auto cutoff = std::chrono::steady_clock::now() - + std::chrono::milliseconds(std::max(0, state.lifetime_ms)); + for (const Timed_Point& value : state.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}); + } +} + +Constellation_Diagram::Builder::Builder(std::shared_ptr parent, + std::shared_ptr i_axis, + std::shared_ptr q_axis) + : parent_(std::move(parent)), i_axis_(std::move(i_axis)), q_axis_(std::move(q_axis)) {} + +std::shared_ptr Constellation_Diagram::Builder::build() { + if (!parent_ || !i_axis_ || !q_axis_) + return {}; + auto result = parent_->plot().make_renderable(parent_, i_axis_, q_axis_); + result->set_i_range(i_range_); + result->set_q_range(q_range_); + result->set_point_color(point_color_); + result->set_anchor_color(anchor_color_); + result->set_point_lifetime_ms(lifetime_); + { + std::lock_guard lock(result->mutex_); + result->state_.type = type_; + result->state_.phase = phase_; + } + return result; +} + +} // namespace renderive diff --git a/Core2/plottable/Performance_Overlay.cpp b/Core2/plottable/Performance_Overlay.cpp new file mode 100644 index 0000000..b215554 --- /dev/null +++ b/Core2/plottable/Performance_Overlay.cpp @@ -0,0 +1,191 @@ +#include "Performance_Overlay.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace renderive { +namespace { + +std::string safe_filename(std::string value) { + if (value.empty()) + return "plot"; + for (char& character : value) { + const auto byte = static_cast(character); + if (!(std::isalnum(byte) || character == '-' || character == '_')) + character = '_'; + } + return value; +} + +std::string fixed(double value, int precision = 2) { + std::ostringstream stream; + stream << std::fixed << std::setprecision(precision) << value; + return stream.str(); +} + +} // namespace + +struct Performance_Overlay::Impl { + mutable std::mutex mutex; + std::atomic_bool enabled{}; + Performance_Overlay_Options options; + std::string plot_name; + std::shared_ptr snapshot; + std::ofstream log; + std::filesystem::path log_path; + std::chrono::steady_clock::time_point previous_frame; +}; + +Performance_Overlay::Performance_Overlay() + : Performance_Overlay(Performance_Overlay_Options{}) {} + +Performance_Overlay::Performance_Overlay(Performance_Overlay_Options options) + : impl_(std::make_unique()) { + impl_->options = std::move(options); +} + +Performance_Overlay::~Performance_Overlay() = default; + +void Performance_Overlay::set_enabled(bool enabled) { + if (impl_->enabled.exchange(enabled, std::memory_order_acq_rel) == enabled) + return; + std::lock_guard lock(impl_->mutex); + impl_->previous_frame = {}; + if (!enabled) { + impl_->log.close(); + impl_->log_path.clear(); + } +} + +bool Performance_Overlay::enabled() const noexcept { + return impl_->enabled.load(std::memory_order_acquire); +} + +void Performance_Overlay::set_options(Performance_Overlay_Options options) { + std::lock_guard lock(impl_->mutex); + impl_->options = std::move(options); + impl_->log.close(); + impl_->log_path.clear(); +} + +std::shared_ptr +Performance_Overlay::display_snapshot() const { + std::lock_guard lock(impl_->mutex); + return impl_->snapshot; +} + +void Performance_Overlay::set_plot_name(std::string name) { + std::lock_guard lock(impl_->mutex); + impl_->plot_name = std::move(name); + impl_->log.close(); + impl_->log_path.clear(); +} + +void Performance_Overlay::record_frame(double render_duration_ms, + const Low_Latency_Diagnostics& diagnostics, + std::size_t renderable_count, + Size viewport) { + if (!enabled()) + return; + + { + std::lock_guard lock(impl_->mutex); + if (!enabled()) + return; + const auto now = std::chrono::steady_clock::now(); + double rendered_fps{}; + if (impl_->previous_frame.time_since_epoch().count() != 0) { + const auto seconds = std::chrono::duration(now - impl_->previous_frame).count(); + if (seconds > 0.0) + rendered_fps = 1.0 / seconds; + } + impl_->previous_frame = now; + + auto snapshot = std::make_shared(); + snapshot->style = impl_->options.style; + snapshot->viewport_size = viewport; + snapshot->plot_name = impl_->plot_name; + snapshot->frame_count = diagnostics.refresh.frame_count; + snapshot->lines = { + "Renderive Core2 / Kernel", + "Plot: " + (impl_->plot_name.empty() ? std::string{"unnamed"} : impl_->plot_name), + "Frame: " + std::to_string(snapshot->frame_count), + "Render: " + fixed(render_duration_ms, 3) + " ms", + "Observed: " + fixed(rendered_fps) + " FPS", + "Limit: " + fixed(diagnostics.refresh.frequency_hz) + " FPS", + "Kernel interval: " + fixed(diagnostics.refresh.next_refresh_interval_ns / 1'000'000.0, 3) + " ms", + "Renderables: " + std::to_string(renderable_count) + }; + + impl_->snapshot = std::move(snapshot); + const auto& log_options = impl_->options.log; + if (log_options.enabled && !log_options.directory.empty()) { + std::error_code error; + const std::filesystem::path directory(log_options.directory); + std::filesystem::create_directories(directory, error); + const auto path = directory / + (safe_filename(log_options.session_name) + "_" + safe_filename(impl_->plot_name) + ".csv"); + if (!error && (!impl_->log.is_open() || impl_->log_path != path)) { + impl_->log.close(); + const bool exists = std::filesystem::exists(path, error); + impl_->log.open(path, std::ios::out | std::ios::app); + impl_->log_path = path; + if (impl_->log && !exists) + impl_->log << "frame,render_ms,observed_fps,max_fps,renderables\n"; + } + if (impl_->log) { + impl_->log << diagnostics.refresh.frame_count << ',' << render_duration_ms << ',' + << rendered_fps << ',' << diagnostics.refresh.frequency_hz << ',' + << renderable_count << '\n'; + impl_->log.flush(); + } + } + } +} + +std::shared_ptr attach_performance_overlay(Plot_Core& plot) { + if (auto existing = plot.performance_overlay()) + return existing; + auto overlay = std::make_shared(); + plot.set_performance_overlay(overlay); + return overlay; +} + +std::shared_ptr attach_performance_overlay( + Plot_Core& plot, const Performance_Overlay_Options& options) { + if (auto existing = plot.performance_overlay()) { + existing->set_options(options); + plot.notify_model_dirty(); + return existing; + } + auto overlay = std::make_shared(options); + plot.set_performance_overlay(overlay); + return overlay; +} + +void set_performance_plot_name(Plot_Core& plot, std::string name) { + auto overlay = plot.performance_overlay(); + if (!overlay) + overlay = attach_performance_overlay(plot); + overlay->set_plot_name(std::move(name)); + plot.notify_model_dirty(); +} + +void set_performance_overlay_enabled(Plot_Core& plot, bool enabled) { + auto overlay = plot.performance_overlay(); + if (!overlay && !enabled) + return; + if (!overlay) + overlay = attach_performance_overlay(plot); + overlay->set_enabled(enabled); + plot.notify_model_dirty(); +} + +} // namespace renderive diff --git a/Core2/plottable/Performance_Overlay.h b/Core2/plottable/Performance_Overlay.h new file mode 100644 index 0000000..63945e5 --- /dev/null +++ b/Core2/plottable/Performance_Overlay.h @@ -0,0 +1,79 @@ +#pragma once + +#include "../base/Types.h" +#include "../plot/Plot_Core.h" + +#include +#include +#include +#include + +namespace renderive { + +struct Performance_Log_Options { + bool enabled{}; + std::string directory; + std::string session_name = "renderive"; +}; + +struct Performance_Overlay_Style { + Font font; + int top_margin = 4; + int left_margin = 6; + int right_margin = 12; + int bottom_margin = 6; + Color background = Color::white(); + Color section_color{64, 142, 255}; + Color label_color{190, 190, 190}; + Color value_color{235, 218, 130}; +}; + +struct Performance_Overlay_Options { + Performance_Overlay_Style style; + Performance_Log_Options log; +}; + +struct Performance_Display_Snapshot { + Performance_Overlay_Style style; + Size viewport_size; + std::string plot_name; + std::vector lines; + std::uint64_t frame_count{}; +}; + +class LIB_DECL Performance_Overlay { +public: + Performance_Overlay(); + explicit Performance_Overlay(Performance_Overlay_Options options); + ~Performance_Overlay(); + + [[nodiscard]] bool enabled() const noexcept; + [[nodiscard]] std::shared_ptr display_snapshot() const; + +private: + friend class Plot_Core; + friend std::shared_ptr attach_performance_overlay(Plot_Core&); + friend std::shared_ptr attach_performance_overlay( + Plot_Core&, const Performance_Overlay_Options&); + friend void set_performance_plot_name(Plot_Core&, std::string); + friend void set_performance_overlay_enabled(Plot_Core&, bool); + + struct Impl; + std::unique_ptr impl_; + + void set_enabled(bool enabled); + void set_plot_name(std::string name); + void set_options(Performance_Overlay_Options options); + void record_frame(double render_duration_ms, + const Low_Latency_Diagnostics& diagnostics, + std::size_t renderable_count, + Size viewport); +}; + +LIB_DECL std::shared_ptr attach_performance_overlay(Plot_Core& plot); +LIB_DECL std::shared_ptr attach_performance_overlay( + Plot_Core& plot, const Performance_Overlay_Options& options); +LIB_DECL void set_performance_plot_name(Plot_Core& plot, std::string name); +LIB_DECL void set_performance_overlay_enabled(Plot_Core& plot, bool enabled); + +} // namespace renderive diff --git a/Core2/plottable/Plottables.h b/Core2/plottable/Plottables.h new file mode 100644 index 0000000..c3ef931 --- /dev/null +++ b/Core2/plottable/Plottables.h @@ -0,0 +1,534 @@ +#pragma once + +#include "../axis/Axis.h" +#include "Hover_Tooltip.h" + +#include +#include +#include +#include +#include + +namespace renderive { + +class LIB_DECL Spectrum final : public Renderable, public Event_Handler, + public Hover_Tooltip_Mixin { +public: + Spectrum(Plot_Core& plot, std::shared_ptr frequency_axis, + std::shared_ptr power_axis); + + [[nodiscard]] std::shared_ptr frequency_axis() const; + void set_frequency_axis(std::shared_ptr value); + [[nodiscard]] std::shared_ptr power_axis() const; + void set_power_axis(std::shared_ptr value); + [[nodiscard]] int frequency_point_size() const; + void set_frequency_point_size(int value); + [[nodiscard]] Range frequency_range() const; + void set_frequency_range(Range value); + [[nodiscard]] double center_frequency() const; + void set_center_frequency(double value); + [[nodiscard]] Range sweep_frequency_range() const; + void set_sweep_frequency_range(Range value); + [[nodiscard]] bool max_hold_visible() const; + void set_max_hold_visible(bool value); + [[nodiscard]] bool min_hold_visible() const; + void set_min_hold_visible(bool value); + [[nodiscard]] bool max_marker_visible() const; + void set_max_marker_visible(bool value); + [[nodiscard]] bool use_min_marker() const; + void set_use_min_marker(bool value); + [[nodiscard]] bool sweep_region_visible() const; + void set_sweep_region_visible(bool value); + [[nodiscard]] bool visible_range_only() const; + void set_visible_range_only(bool value); + [[nodiscard]] Line_Interpolation_Mode interpolation_mode() const; + void set_interpolation_mode(Line_Interpolation_Mode value); + [[nodiscard]] Brush max_brush() const; + void set_max_brush(Brush value); + [[nodiscard]] Brush current_brush() const; + void set_current_brush(Brush value); + [[nodiscard]] Brush min_brush() const; + void set_min_brush(Brush value); + [[nodiscard]] Pen max_pen() const; + void set_max_pen(Pen value); + [[nodiscard]] Pen current_pen() const; + void set_current_pen(Pen value); + [[nodiscard]] Pen min_pen() const; + void set_min_pen(Pen value); + [[nodiscard]] Pen selected_marker_pen() const; + void set_selected_marker_pen(Pen value); + [[nodiscard]] Pen marker_pen() const; + void set_marker_pen(Pen value); + [[nodiscard]] Pen middle_frequency_pen() const; + void set_middle_frequency_pen(Pen value); + [[nodiscard]] Brush sweep_region_brush() const; + void set_sweep_region_brush(Brush value); + + void update_samples(std::span values); + void update_samples(std::pmr::vector&& values); + template void update_samples(const Values& values) { + update_samples(std::span(values.data(), values.size())); + } + [[nodiscard]] double power_at(double frequency, bool& ok) const; + void add_custom_marker(double frequency); + void add_custom_line_marker(double frequency); + void remove_custom_marker(double frequency); + void remove_selected_marker(); + void clear_custom_markers(); + [[nodiscard]] int selectable_line_marker_count() const; + [[nodiscard]] int selected_marker_index() const; + void set_selected_marker_index(int index); + void select_next_marker(); + void select_previous_marker(); + void clear_marker_selection(); + [[nodiscard]] double marker_frequency(int index) const; + void set_marker_frequency(int index, double frequency); + void set_current_marker_frequency(double frequency); + void handle_event(const Event& event) override; + void tooltip_changed() { changed(); } + + class Builder { + public: + Builder(std::shared_ptr parent, + std::shared_ptr frequency_axis, + std::shared_ptr power_axis); + Builder& set_frequency_range(Range value) { frequency_range_ = value; return *this; } + Builder& set_frequency_point_size(int value) { point_count_ = value; return *this; } + Builder& set_center_frequency(double value) { center_ = value; return *this; } + Builder& set_sweep_frequency_range(Range value) { sweep_range_ = value; return *this; } + Builder& set_max_hold_visible(bool value) { max_hold_ = value; return *this; } + Builder& set_min_hold_visible(bool value) { min_hold_ = value; return *this; } + Builder& set_max_marker_visible(bool value) { max_marker_ = value; return *this; } + Builder& set_use_min_marker(bool value) { min_marker_ = value; return *this; } + Builder& set_sweep_region_visible(bool value) { sweep_visible_ = value; return *this; } + Builder& set_visible_range_only(bool value) { visible_only_ = value; return *this; } + Builder& set_interpolation_mode(Line_Interpolation_Mode value) { interpolation_ = value; return *this; } + std::shared_ptr build(); + private: + std::shared_ptr parent_; + std::shared_ptr frequency_axis_; + std::shared_ptr power_axis_; + Range frequency_range_{}; + int point_count_{}; + double center_ = 50.0; + Range sweep_range_{40.0, 60.0}; + bool max_hold_{}; + bool min_hold_{}; + bool max_marker_{}; + bool min_marker_{}; + bool sweep_visible_{}; + bool visible_only_{true}; + Line_Interpolation_Mode interpolation_ = Line_Interpolation_Mode::Linear_Value; + }; + +protected: + void paint(detail::Painter& painter) override; + +private: + struct State { + std::shared_ptr frequency_axis; + std::shared_ptr power_axis; + int point_count{}; + Range frequency_range{}; + double center_frequency = 50.0; + Range sweep_range{40.0, 60.0}; + bool max_hold{}; + bool min_hold{}; + bool max_marker{}; + bool min_marker{}; + bool sweep_visible{}; + bool visible_only{true}; + Line_Interpolation_Mode interpolation = Line_Interpolation_Mode::Linear_Value; + Brush max_brush; + Brush current_brush; + Brush min_brush; + Pen max_pen{Color::red()}; + Pen current_pen{Color::green()}; + Pen min_pen{Color::white()}; + Pen selected_marker_pen{Color{0, 0, 139, 255}, 2.0}; + Pen marker_pen{Color::red()}; + Pen middle_pen{Color::red()}; + Brush sweep_brush{Color{255, 255, 0, 100}, Brush_Style::Solid}; + std::vector samples; + std::vector maxima; + std::vector minima; + std::vector markers; + int selected_marker = -1; + }; + [[nodiscard]] State state_snapshot() const; + mutable std::mutex mutex_; + State state_; +}; + +class LIB_DECL Waterfall final : public Renderable, public Event_Handler, + public Hover_Tooltip_Mixin { +public: + Waterfall(Plot_Core& plot, std::shared_ptr frequency_axis, + std::shared_ptr time_axis); + void append_row(int tick, std::span values); + void append_row(int 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(Time_Of_Day time, const Values& values) { + append_row(time, std::span(values.data(), values.size())); + } + [[nodiscard]] std::shared_ptr frequency_axis() const; + [[nodiscard]] std::shared_ptr time_axis() const; + [[nodiscard]] Range frequency_range() const; + void set_frequency_range(Range value); + [[nodiscard]] Range power_range() const; + void set_power_range(Range value); + [[nodiscard]] int frequency_bin_count() const; + void set_frequency_bin_count(int value); + [[nodiscard]] bool visible_range_only() const; + void set_visible_range_only(bool value); + [[nodiscard]] Image_Interpolation_Mode interpolation_mode() const; + void set_interpolation_mode(Image_Interpolation_Mode value); + void handle_event(const Event& event) override; + void tooltip_changed() { changed(); } + + class Builder { + public: + Builder(std::shared_ptr parent, + std::shared_ptr frequency_axis, + std::shared_ptr time_axis); + Builder& set_frequency_range(Range value) { frequency_range_ = value; return *this; } + Builder& set_power_range(Range value) { power_range_ = value; return *this; } + Builder& set_frequency_bin_count(int value) { bin_count_ = value; return *this; } + Builder& set_visible_range_only(bool value) { visible_only_ = value; return *this; } + Builder& set_interpolation_mode(Image_Interpolation_Mode value) { interpolation_ = value; return *this; } + Builder& set_color_map(Color_Map value) { color_map_ = std::move(value); return *this; } + std::shared_ptr build(); + private: + std::shared_ptr parent_; + std::shared_ptr frequency_axis_; + std::shared_ptr time_axis_; + Range frequency_range_{0.0, 10.0}; + Range power_range_{0.0, 10.0}; + int bin_count_{}; + bool visible_only_{true}; + Image_Interpolation_Mode interpolation_ = Image_Interpolation_Mode::Nearest; + Color_Map color_map_; + }; + +protected: + void paint(detail::Painter& painter) override; + +private: + struct Row { int tick{}; std::vector values; }; + struct State { + std::shared_ptr frequency_axis; + std::shared_ptr time_axis; + Range frequency_range{0.0, 10.0}; + Range power_range{0.0, 10.0}; + int bin_count{}; + bool visible_only{true}; + Image_Interpolation_Mode interpolation = Image_Interpolation_Mode::Nearest; + Color_Map color_map; + std::deque rows; + }; + [[nodiscard]] State state_snapshot() const; + mutable std::mutex mutex_; + State state_; +}; + +class LIB_DECL Frequency_Trace final : public Renderable { +public: + Frequency_Trace(Plot_Core& plot, std::shared_ptr time_axis, + std::shared_ptr value_axis); + void append_sample(int tick, double value); + void append_sample(Time_Of_Day time, double value); + [[nodiscard]] std::shared_ptr time_axis() const; + [[nodiscard]] std::shared_ptr value_axis() const; + + class Builder { + public: + Builder(std::shared_ptr parent, std::shared_ptr time_axis, + std::shared_ptr value_axis); + Builder& set_color(Color value) { pen_.color = value; return *this; } + Builder& set_pen(Pen value) { pen_ = value; return *this; } + std::shared_ptr build(); + private: + std::shared_ptr parent_; + std::shared_ptr time_axis_; + std::shared_ptr value_axis_; + Pen pen_{Color::yellow()}; + }; + +protected: + void paint(detail::Painter& painter) override; + +private: + mutable std::mutex mutex_; + std::shared_ptr time_axis_; + std::shared_ptr value_axis_; + Pen pen_{Color::yellow()}; + std::deque> samples_; +}; + +class LIB_DECL Afterglow final : public Renderable { +public: + Afterglow(Plot_Core& plot, std::shared_ptr frequency_axis, + std::shared_ptr power_axis); + [[nodiscard]] std::shared_ptr frequency_axis() const; + [[nodiscard]] std::shared_ptr power_axis() const; + [[nodiscard]] Range frequency_range() const; + void set_frequency_range(Range value); + [[nodiscard]] Range power_range() const; + void set_power_range(Range value); + [[nodiscard]] int frequency_point_size() const; + void set_frequency_point_size(int value); + [[nodiscard]] int power_point_size() const; + void set_power_point_size(int value); + [[nodiscard]] bool interpolate() const; + void set_interpolate(bool value); + [[nodiscard]] double attenuation_rate() const; + void set_attenuation_rate(double value); + void append_spectrum(std::span values); + void append_spectrum(std::pmr::vector&& values); + template void append_spectrum(const Values& values) { + append_spectrum(std::span(values.data(), values.size())); + } + + class Builder { + public: + Builder(std::shared_ptr parent, + std::shared_ptr frequency_axis, + std::shared_ptr power_axis); + Builder& set_frequency_range(Range value) { frequency_range_ = value; return *this; } + Builder& set_power_range(Range value) { power_range_ = value; return *this; } + Builder& set_frequency_bin_count(int value) { frequency_count_ = value; return *this; } + Builder& set_power_bin_count(int value) { power_count_ = value; return *this; } + Builder& set_interpolate_power_bins(bool value) { interpolate_ = value; return *this; } + Builder& set_decay_rate(double value) { attenuation_ = value; return *this; } + Builder& set_color_map(Color_Map value) { color_map_ = std::move(value); return *this; } + std::shared_ptr build(); + private: + std::shared_ptr parent_; + std::shared_ptr frequency_axis_; + std::shared_ptr power_axis_; + Range frequency_range_{0.0, 10.0}; + Range power_range_{0.0, 10.0}; + int frequency_count_{}; + int power_count_{}; + bool interpolate_{true}; + double attenuation_ = 0.2; + Color_Map color_map_; + }; + +protected: + void paint(detail::Painter& painter) override; + +private: + struct State { + std::shared_ptr frequency_axis; + std::shared_ptr power_axis; + Range frequency_range{0.0, 10.0}; + Range power_range{0.0, 10.0}; + int frequency_count{}; + int power_count{}; + bool interpolate{true}; + double attenuation = 0.2; + Color_Map color_map; + std::deque> history; + }; + [[nodiscard]] State state_snapshot() const; + mutable std::mutex mutex_; + State state_; +}; + +class LIB_DECL Sweep_Spectrum final : public Renderable { +public: + Sweep_Spectrum(Plot_Core& plot, std::shared_ptr frequency_axis, + std::shared_ptr power_axis); + void append_block(std::span values); + void append_block(std::pmr::vector&& values); + template void append_block(const Values& values) { + append_block(std::span(values.data(), values.size())); + } + [[nodiscard]] std::shared_ptr frequency_axis() const; + [[nodiscard]] std::shared_ptr power_axis() const; + [[nodiscard]] Range frequency_range() const; + void set_frequency_range(Range value); + [[nodiscard]] int bins_per_block() const; + void set_bins_per_block(int value); + [[nodiscard]] int block_count() const; + void set_block_count(int value); + [[nodiscard]] Pen pen() const; + void set_pen(Pen value); + [[nodiscard]] Pen cur_frequency_pen() const; + void set_cur_frequency_pen(Pen value); + [[nodiscard]] bool visible_range_only() const; + void set_visible_range_only(bool value); + [[nodiscard]] Line_Interpolation_Mode interpolation_mode() const; + void set_interpolation_mode(Line_Interpolation_Mode value); + + class Builder { + public: + Builder(std::shared_ptr parent, std::shared_ptr frequency_axis, + std::shared_ptr power_axis); + Builder& set_frequency_range(Range value) { frequency_range_ = value; return *this; } + Builder& set_bins_per_block(int value) { bins_ = value; return *this; } + Builder& set_block_num(int value) { blocks_ = value; return *this; } + Builder& set_pen(Pen value) { pen_ = value; return *this; } + Builder& set_current_frequency_pen(Pen value) { current_pen_ = value; return *this; } + Builder& set_visible_range_only(bool value) { visible_only_ = value; return *this; } + Builder& set_interpolation_mode(Line_Interpolation_Mode value) { interpolation_ = value; return *this; } + std::shared_ptr build(); + private: + std::shared_ptr parent_; + std::shared_ptr frequency_axis_; + std::shared_ptr power_axis_; + Range frequency_range_{}; + int bins_{}; + int blocks_{}; + Pen pen_{Color::yellow()}; + Pen current_pen_{Color::red(), 2.0}; + bool visible_only_{true}; + Line_Interpolation_Mode interpolation_ = Line_Interpolation_Mode::Linear_Value; + }; + +protected: + void paint(detail::Painter& painter) override; + +private: + struct State { + std::shared_ptr frequency_axis; + std::shared_ptr power_axis; + Range frequency_range{}; + int bins{}; + int blocks{}; + Pen pen{Color::yellow()}; + Pen current_pen{Color::red(), 2.0}; + bool visible_only{true}; + Line_Interpolation_Mode interpolation = Line_Interpolation_Mode::Linear_Value; + std::deque> data; + }; + [[nodiscard]] State state_snapshot() const; + mutable std::mutex mutex_; + State state_; +}; + +class LIB_DECL Selection_Rectangle_Overlay final : public Renderable, public Event_Handler { +public: + Selection_Rectangle_Overlay(Plot_Core& plot, std::shared_ptr horizontal_axis, + std::shared_ptr vertical_axis); + void set_horizontal_axis(std::shared_ptr axis); + void set_vertical_axis(std::shared_ptr axis); + [[nodiscard]] Font label_font() const; + [[nodiscard]] Pen label_pen() const; + [[nodiscard]] Brush selection_brush() const; + [[nodiscard]] Pen selection_border_pen() const; + void set_label_font(Font value); + void set_label_pen(Pen value); + void set_selection_brush(Brush value); + void set_selection_border_pen(Pen value); + [[nodiscard]] std::vector selected_regions() const; + void clear_selected_regions(); + void handle_event(const Event& event) override; + + class Builder { + public: + Builder(std::shared_ptr parent, std::shared_ptr horizontal_axis, + std::shared_ptr vertical_axis); + Builder& set_font(Font value) { font_ = value; return *this; } + Builder& set_font_pen(Pen value) { label_pen_ = value; return *this; } + Builder& set_rect_brush(Brush value) { brush_ = value; return *this; } + Builder& set_border_pen(Pen value) { border_ = value; return *this; } + std::shared_ptr build(); + private: + std::shared_ptr parent_; + std::shared_ptr horizontal_axis_; + std::shared_ptr vertical_axis_; + Font font_; + Pen label_pen_{Color::white()}; + Brush brush_{Color{0, 0, 255, 50}, Brush_Style::Solid}; + Pen border_{Color::white(), 1.0, Line_Style::Dash}; + }; + +protected: + void paint(detail::Painter& painter) override; + +private: + mutable std::mutex mutex_; + std::shared_ptr horizontal_axis_; + std::shared_ptr vertical_axis_; + Font font_; + Pen label_pen_{Color::white()}; + Brush brush_{Color{0, 0, 255, 50}, Brush_Style::Solid}; + Pen border_{Color::white(), 1.0, Line_Style::Dash}; + std::vector regions_; + bool selecting_{}; + PointF selection_start_{}; + PointF selection_current_{}; +}; + +enum class Constellation_Diagram_Type : std::uint8_t { Psk4 = 4, Psk8 = 8, Psk16 = 16 }; + +class LIB_DECL Constellation_Diagram final : public Renderable { +public: + Constellation_Diagram(Plot_Core& plot, std::shared_ptr i_axis, + std::shared_ptr q_axis); + void append_point(PointF point); + [[nodiscard]] std::shared_ptr i_axis() const; + [[nodiscard]] std::shared_ptr q_axis() const; + [[nodiscard]] Range i_range() const; + void set_i_range(Range value); + [[nodiscard]] Range q_range() const; + void set_q_range(Range value); + [[nodiscard]] Color point_color() const; + void set_point_color(Color value); + [[nodiscard]] Color anchor_color() const; + void set_anchor_color(Color value); + [[nodiscard]] int point_lifetime_ms() const; + void set_point_lifetime_ms(int value); + void fit_square_to_axes(); + + class Builder { + public: + Builder(std::shared_ptr parent, std::shared_ptr i_axis, + std::shared_ptr q_axis); + Builder& set_i_range(Range value) { i_range_ = value; return *this; } + Builder& set_q_range(Range value) { q_range_ = value; return *this; } + Builder& set_point_color(Color value) { point_color_ = value; return *this; } + Builder& set_anchor_color(Color value) { anchor_color_ = value; return *this; } + Builder& set_point_lifetime_ms(int value) { lifetime_ = value; return *this; } + Builder& set_type(Constellation_Diagram_Type value) { type_ = value; return *this; } + Builder& set_phase_offset_radians(double value) { phase_ = value; return *this; } + std::shared_ptr build(); + private: + std::shared_ptr parent_; + std::shared_ptr i_axis_; + std::shared_ptr q_axis_; + Range i_range_{0.0, 100.0}; + Range q_range_{0.0, 100.0}; + Color point_color_ = Color::red(); + Color anchor_color_ = Color::yellow(); + int lifetime_ = 1000; + Constellation_Diagram_Type type_ = Constellation_Diagram_Type::Psk8; + double phase_{}; + }; + +protected: + void paint(detail::Painter& painter) override; + +private: + struct Timed_Point { PointF point; std::chrono::steady_clock::time_point time; }; + struct State { + std::shared_ptr i_axis; + std::shared_ptr q_axis; + Range i_range{0.0, 100.0}; + Range q_range{0.0, 100.0}; + Color point_color = Color::red(); + Color anchor_color = Color::yellow(); + int lifetime_ms = 1000; + Constellation_Diagram_Type type = Constellation_Diagram_Type::Psk8; + double phase{}; + std::deque points; + }; + [[nodiscard]] State state_snapshot() const; + mutable std::mutex mutex_; + State state_; +}; + +} // namespace renderive diff --git a/Core2/plottable/Spectrum.cpp b/Core2/plottable/Spectrum.cpp new file mode 100644 index 0000000..51e9eb3 --- /dev/null +++ b/Core2/plottable/Spectrum.cpp @@ -0,0 +1,340 @@ +#include "Plottables.h" + +#include "Curve_Sampling.h" +#include "../plot/Plot_Core.h" +#include "../render/Blend2D_Cache.h" + +#include +#include +#include +#include + +namespace renderive { +namespace { + +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)}; +} + +void draw_curve(detail::Painter& painter, + std::span 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 = detail::curve_points(values, domain, x_axis, y_axis, + visible_only, interpolation); + if (points.size() < 2) + return; + if (brush.enabled()) { + std::vector polygon; + polygon.reserve(points.size() + 2); + polygon.push_back({points.front().x, y_axis.coord_to_pixel(y_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)}); + painter.polygon(polygon, Pen{.style = Line_Style::None}, brush); + } + painter.polyline(points, pen); +} + +} // namespace + +Spectrum::Spectrum(Plot_Core& plot, + std::shared_ptr frequency_axis, + std::shared_ptr power_axis) + : Renderable(plot), state_{} { + state_.frequency_axis = std::move(frequency_axis); + state_.power_axis = std::move(power_axis); +} + +#define RENDERIVE_SPECTRUM_PROPERTY(Type, Method, Field) \ + Type Spectrum::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ + void Spectrum::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } + +RENDERIVE_SPECTRUM_PROPERTY(std::shared_ptr, frequency_axis, frequency_axis) +RENDERIVE_SPECTRUM_PROPERTY(std::shared_ptr, power_axis, power_axis) +RENDERIVE_SPECTRUM_PROPERTY(Range, frequency_range, frequency_range) +RENDERIVE_SPECTRUM_PROPERTY(double, center_frequency, center_frequency) +RENDERIVE_SPECTRUM_PROPERTY(Range, sweep_frequency_range, sweep_range) +RENDERIVE_SPECTRUM_PROPERTY(bool, max_hold_visible, max_hold) +RENDERIVE_SPECTRUM_PROPERTY(bool, min_hold_visible, min_hold) +RENDERIVE_SPECTRUM_PROPERTY(bool, max_marker_visible, max_marker) +RENDERIVE_SPECTRUM_PROPERTY(bool, use_min_marker, min_marker) +RENDERIVE_SPECTRUM_PROPERTY(bool, sweep_region_visible, sweep_visible) +RENDERIVE_SPECTRUM_PROPERTY(bool, visible_range_only, visible_only) +RENDERIVE_SPECTRUM_PROPERTY(Line_Interpolation_Mode, interpolation_mode, interpolation) +RENDERIVE_SPECTRUM_PROPERTY(Brush, max_brush, max_brush) +RENDERIVE_SPECTRUM_PROPERTY(Brush, current_brush, current_brush) +RENDERIVE_SPECTRUM_PROPERTY(Brush, min_brush, min_brush) +RENDERIVE_SPECTRUM_PROPERTY(Pen, max_pen, max_pen) +RENDERIVE_SPECTRUM_PROPERTY(Pen, current_pen, current_pen) +RENDERIVE_SPECTRUM_PROPERTY(Pen, min_pen, min_pen) +RENDERIVE_SPECTRUM_PROPERTY(Pen, selected_marker_pen, selected_marker_pen) +RENDERIVE_SPECTRUM_PROPERTY(Pen, marker_pen, marker_pen) +RENDERIVE_SPECTRUM_PROPERTY(Pen, middle_frequency_pen, middle_pen) +RENDERIVE_SPECTRUM_PROPERTY(Brush, sweep_region_brush, sweep_brush) + +#undef RENDERIVE_SPECTRUM_PROPERTY + +int Spectrum::frequency_point_size() const { + std::lock_guard lock(mutex_); + return state_.point_count; +} + +void Spectrum::set_frequency_point_size(int value) { + { std::lock_guard lock(mutex_); state_.point_count = std::max(0, value); } + changed(); +} + +void Spectrum::update_samples(std::span values) { + { + std::lock_guard lock(mutex_); + state_.samples.assign(values.begin(), values.end()); + if (state_.point_count <= 0) + state_.point_count = static_cast(values.size()); + if (state_.maxima.size() != values.size()) + state_.maxima.assign(values.begin(), values.end()); + else + for (std::size_t index = 0; index < values.size(); ++index) + state_.maxima[index] = std::max(state_.maxima[index], values[index]); + if (state_.minima.size() != values.size()) + state_.minima.assign(values.begin(), values.end()); + else + for (std::size_t index = 0; index < values.size(); ++index) + state_.minima[index] = std::min(state_.minima[index], values[index]); + } + changed(); +} + +void Spectrum::update_samples(std::pmr::vector&& values) { + update_samples(std::span(values.data(), values.size())); +} + +double Spectrum::power_at(double frequency, bool& ok) const { + std::lock_guard lock(mutex_); + ok = false; + if (state_.samples.empty() || !state_.frequency_range.contains(frequency) || + state_.frequency_range.length() == 0.0) + return 0.0; + const double normalized = (frequency - state_.frequency_range.origin) / + state_.frequency_range.length(); + const double position = std::clamp(normalized, 0.0, 1.0) * + static_cast(state_.samples.size() - 1); + const auto lower = static_cast(std::floor(position)); + const auto upper = std::min(lower + 1, state_.samples.size() - 1); + const double fraction = position - static_cast(lower); + ok = true; + return state_.samples[lower] * (1.0 - fraction) + state_.samples[upper] * fraction; +} + +void Spectrum::add_custom_marker(double frequency) { add_custom_line_marker(frequency); } + +void Spectrum::add_custom_line_marker(double frequency) { + { std::lock_guard lock(mutex_); state_.markers.push_back(frequency); } + changed(); +} + +void Spectrum::remove_custom_marker(double frequency) { + { + std::lock_guard lock(mutex_); + if (state_.markers.empty()) + return; + auto closest = std::min_element(state_.markers.begin(), state_.markers.end(), + [frequency](double left, double right) { + return std::abs(left - frequency) < std::abs(right - frequency); + }); + const int removed = static_cast(std::distance(state_.markers.begin(), closest)); + state_.markers.erase(closest); + if (state_.selected_marker == removed) + state_.selected_marker = -1; + else if (state_.selected_marker > removed) + --state_.selected_marker; + } + changed(); +} + +void Spectrum::remove_selected_marker() { + { + std::lock_guard lock(mutex_); + if (state_.selected_marker < 0 || state_.selected_marker >= static_cast(state_.markers.size())) + return; + state_.markers.erase(state_.markers.begin() + state_.selected_marker); + state_.selected_marker = -1; + } + changed(); +} + +void Spectrum::clear_custom_markers() { + { std::lock_guard lock(mutex_); state_.markers.clear(); state_.selected_marker = -1; } + changed(); +} + +int Spectrum::selectable_line_marker_count() const { + std::lock_guard lock(mutex_); + return static_cast(state_.markers.size()); +} + +int Spectrum::selected_marker_index() const { std::lock_guard lock(mutex_); return state_.selected_marker; } + +void Spectrum::set_selected_marker_index(int index) { + { + std::lock_guard lock(mutex_); + state_.selected_marker = index >= 0 && index < static_cast(state_.markers.size()) ? index : -1; + } + changed(); +} + +void Spectrum::select_next_marker() { + std::lock_guard lock(mutex_); + if (state_.markers.empty()) + state_.selected_marker = -1; + else + state_.selected_marker = (state_.selected_marker + 1) % static_cast(state_.markers.size()); + invalidate_cache(); + plot().notify_model_dirty(); +} + +void Spectrum::select_previous_marker() { + std::lock_guard lock(mutex_); + if (state_.markers.empty()) + state_.selected_marker = -1; + else + state_.selected_marker = (state_.selected_marker <= 0 + ? static_cast(state_.markers.size()) + : state_.selected_marker) - 1; + invalidate_cache(); + plot().notify_model_dirty(); +} + +void Spectrum::clear_marker_selection() { set_selected_marker_index(-1); } + +double Spectrum::marker_frequency(int index) const { + std::lock_guard lock(mutex_); + return index >= 0 && index < static_cast(state_.markers.size()) ? state_.markers[index] : 0.0; +} + +void Spectrum::set_marker_frequency(int index, double frequency) { + { + std::lock_guard lock(mutex_); + if (index < 0 || index >= static_cast(state_.markers.size())) + return; + state_.markers[index] = frequency; + } + changed(); +} + +void Spectrum::set_current_marker_frequency(double frequency) { + const int index = selected_marker_index(); + if (index >= 0) + set_marker_frequency(index, frequency); +} + +Spectrum::State Spectrum::state_snapshot() const { + std::lock_guard lock(mutex_); + return state_; +} + +void Spectrum::handle_event(const Event& event) { + update_hover(event); +} + +void Spectrum::paint(detail::Painter& painter) { + const State state = state_snapshot(); + if (!state.frequency_axis || !state.power_axis) + return; + const Axis_Transform horizontal = state.frequency_axis->transform(); + const Axis_Transform vertical = state.power_axis->transform(); + const RectF content = axes_rect(horizontal, vertical); + + if (state.sweep_visible) { + const double first = horizontal.coord_to_pixel(state.sweep_range.origin); + const double last = horizontal.coord_to_pixel(state.sweep_range.target); + painter.rect({std::min(first, last), content.y, std::abs(last - first), content.height}, + Pen{.style = Line_Style::None}, state.sweep_brush); + } + if (state.max_hold) + draw_curve(painter, state.maxima, state.frequency_range, horizontal, vertical, + state.visible_only, state.interpolation, state.max_pen, state.max_brush); + if (state.min_hold) + draw_curve(painter, state.minima, state.frequency_range, horizontal, vertical, + state.visible_only, state.interpolation, state.min_pen, state.min_brush); + draw_curve(painter, state.samples, state.frequency_range, horizontal, vertical, + state.visible_only, state.interpolation, state.current_pen, state.current_brush); + + if (state.middle_pen.enabled()) { + const double x = horizontal.coord_to_pixel(state.center_frequency); + painter.line({x, content.y}, {x, content.bottom()}, state.middle_pen); + } + for (std::size_t index = 0; index < state.markers.size(); ++index) { + const double frequency = state.markers[index]; + const double x = horizontal.coord_to_pixel(frequency); + const Pen& pen = static_cast(index) == state.selected_marker + ? state.selected_marker_pen + : state.marker_pen; + painter.line({x, content.y}, {x, content.bottom()}, pen); + } + if (!state.samples.empty() && (state.max_marker || state.min_marker)) { + const auto draw_extreme = [&](bool maximum) { + auto iterator = maximum ? std::max_element(state.samples.begin(), state.samples.end()) + : std::min_element(state.samples.begin(), state.samples.end()); + const std::size_t index = static_cast(std::distance(state.samples.begin(), iterator)); + const double denominator = state.samples.size() > 1 ? state.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 Pen& pen = maximum ? state.max_pen : state.min_pen; + painter.circle(point, 3.0, pen, Brush{pen.color, Brush_Style::Solid}); + }; + if (state.max_marker) + draw_extreme(true); + if (state.min_marker) + draw_extreme(false); + } + + const Hover_Tooltip_Snapshot tooltip = tooltip_snapshot(); + if (tooltip.enabled && tooltip.active && content.contains(tooltip.position)) { + const double frequency = horizontal.pixel_to_coord(tooltip.position.x); + bool ok{}; + const double power = power_at(frequency, ok); + if (ok) { + std::ostringstream text; + text << std::fixed << std::setprecision(2) << frequency << " Hz " << power; + const RectF box{tooltip.position.x + 8.0, tooltip.position.y + 8.0, 170.0, 24.0}; + painter.rect(box, Pen{tooltip.text_pen.color}, tooltip.background); + painter.text({box.x + 4.0, box.y + 3.0}, text.str(), tooltip.font, tooltip.text_pen); + } + } +} + +Spectrum::Builder::Builder(std::shared_ptr parent, + std::shared_ptr frequency_axis, + std::shared_ptr power_axis) + : parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)), + power_axis_(std::move(power_axis)) {} + +std::shared_ptr Spectrum::Builder::build() { + if (!parent_ || !frequency_axis_ || !power_axis_) + return {}; + auto result = parent_->plot().make_renderable(parent_, frequency_axis_, power_axis_); + result->set_frequency_range(frequency_range_); + result->set_frequency_point_size(point_count_); + result->set_center_frequency(center_); + result->set_sweep_frequency_range(sweep_range_); + result->set_max_hold_visible(max_hold_); + result->set_min_hold_visible(min_hold_); + result->set_max_marker_visible(max_marker_); + result->set_use_min_marker(min_marker_); + result->set_sweep_region_visible(sweep_visible_); + result->set_visible_range_only(visible_only_); + result->set_interpolation_mode(interpolation_); + return result; +} + +} // namespace renderive diff --git a/Core2/render/Blend2D_Cache.cpp b/Core2/render/Blend2D_Cache.cpp new file mode 100644 index 0000000..ae12612 --- /dev/null +++ b/Core2/render/Blend2D_Cache.cpp @@ -0,0 +1,384 @@ +#include "Blend2D_Cache.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace renderive::detail { +namespace { + +void clear_image(BLImage& image) { + BLImageData data{}; + if (image.get_data(&data) != BL_SUCCESS) + return; + for (int y = 0; y < data.size.h; ++y) { + auto* row = static_cast(data.pixel_data) + + static_cast(y) * data.stride; + std::memset(row, 0, static_cast(data.size.w) * sizeof(Pixel)); + } +} + +void load_font_face(BLFontFace& face, std::initializer_list candidates) { + for (const char* path : candidates) { + std::error_code error; + if (std::filesystem::exists(path, error) && face.create_from_file(path) == BL_SUCCESS) + return; + } +} + +BLFontFace& default_font_face(const Font& font) { + static std::array faces; + static std::once_flag once; + std::call_once(once, [] { + load_font_face(faces[0], { + "C:/Windows/Fonts/msyh.ttc", + "C:/Windows/Fonts/simsun.ttc", + "C:/Windows/Fonts/segoeui.ttf", + "C:/Windows/Fonts/arial.ttf", + "C:/Windows/Fonts/tahoma.ttf" + }); + load_font_face(faces[1], { + "C:/Windows/Fonts/msyhbd.ttc", + "C:/Windows/Fonts/segoeuib.ttf", + "C:/Windows/Fonts/arialbd.ttf", + "C:/Windows/Fonts/tahomabd.ttf" + }); + load_font_face(faces[2], { + "C:/Windows/Fonts/segoeuii.ttf", + "C:/Windows/Fonts/ariali.ttf", + "C:/Windows/Fonts/tahomai.ttf" + }); + load_font_face(faces[3], { + "C:/Windows/Fonts/segoeuiz.ttf", + "C:/Windows/Fonts/arialbi.ttf", + "C:/Windows/Fonts/tahomabi.ttf" + }); + }); + const std::size_t index = (font.weight >= 600 ? 1u : 0u) | (font.italic ? 2u : 0u); + return faces[index] ? faces[index] : faces[0]; +} + +BLStrokeCap stroke_cap(Line_Cap cap) noexcept { + switch (cap) { + case Line_Cap::Square: + return BL_STROKE_CAP_SQUARE; + case Line_Cap::Round: + return BL_STROKE_CAP_ROUND; + case Line_Cap::Butt: + default: + return BL_STROKE_CAP_BUTT; + } +} + +BLStrokeJoin stroke_join(Line_Join join) noexcept { + switch (join) { + case Line_Join::Bevel: + return BL_STROKE_JOIN_BEVEL; + case Line_Join::Round: + return BL_STROKE_JOIN_ROUND; + case Line_Join::Miter: + default: + return BL_STROKE_JOIN_MITER_CLIP; + } +} + +double cubic_weight(double distance) noexcept { + constexpr double a = -0.5; + distance = std::abs(distance); + if (distance < 1.0) + return (a + 2.0) * distance * distance * distance - + (a + 3.0) * distance * distance + 1.0; + if (distance < 2.0) + return a * distance * distance * distance - 5.0 * a * distance * distance + + 8.0 * a * distance - 4.0 * a; + return 0.0; +} + +std::uint8_t pixel_channel(Pixel pixel, int shift) noexcept { + return static_cast((pixel >> shift) & 0xFFu); +} + +std::vector bicubic_resample(std::span source, + int source_width, + int source_height, + int destination_width, + int destination_height) { + std::vector destination( + static_cast(destination_width) * destination_height); + for (int y = 0; y < destination_height; ++y) { + const double source_y = (static_cast(y) + 0.5) * source_height / + destination_height - + 0.5; + const int base_y = static_cast(std::floor(source_y)); + for (int x = 0; x < destination_width; ++x) { + const double source_x = (static_cast(x) + 0.5) * source_width / + destination_width - + 0.5; + const int base_x = static_cast(std::floor(source_x)); + double channels[4]{}; + double total_weight{}; + for (int offset_y = -1; offset_y <= 2; ++offset_y) { + const int sample_y = std::clamp(base_y + offset_y, 0, source_height - 1); + const double weight_y = cubic_weight(source_y - (base_y + offset_y)); + for (int offset_x = -1; offset_x <= 2; ++offset_x) { + const int sample_x = std::clamp(base_x + offset_x, 0, source_width - 1); + const double weight = weight_y * + cubic_weight(source_x - (base_x + offset_x)); + const Pixel pixel = source[static_cast(sample_y) * + source_width + + sample_x]; + channels[0] += pixel_channel(pixel, 0) * weight; + channels[1] += pixel_channel(pixel, 8) * weight; + channels[2] += pixel_channel(pixel, 16) * weight; + channels[3] += pixel_channel(pixel, 24) * weight; + total_weight += weight; + } + } + const auto channel = [total_weight](double value) { + if (total_weight != 0.0) + value /= total_weight; + return static_cast(std::clamp(std::lround(value), 0L, 255L)); + }; + destination[static_cast(y) * destination_width + x] = + channel(channels[0]) | (channel(channels[1]) << 8) | + (channel(channels[2]) << 16) | (channel(channels[3]) << 24); + } + } + return destination; +} + +} // namespace + +void Blend2D_Color_Cache::clear() { + clear_image(image_); +} + +void Blend2D_Color_Cache::composite(const ::Color_Cache& source) { + const auto& typed = dynamic_cast(source); + if (typed.image_.is_empty()) + return; + const Size source_size = typed.size(); + if (image_.is_empty()) { + ensure_size(source_size); + clear_image(image_); + } + if (size() != source_size) + return; + + BLContext context(image_); + if (!context) + return; + context.set_comp_op(BL_COMP_OP_SRC_OVER); + context.blit_image(BLRect(0.0, 0.0, + static_cast(source_size.width), + static_cast(source_size.height)), + typed.image_, + BLRectI(0, 0, source_size.width, source_size.height)); + context.end(); +} + +void Blend2D_Color_Cache::ensure_size(Size requested) { + if (requested.empty()) { + image_.reset(); + return; + } + if (size() == requested) + return; + image_.create(requested.width, requested.height, BL_FORMAT_PRGB32); + clear_image(image_); +} + +Size Blend2D_Color_Cache::size() const noexcept { + return {image_.width(), image_.height()}; +} + +Image_View Blend2D_Color_Cache::view() const noexcept { + BLImageData data{}; + if (image_.get_data(&data) != BL_SUCCESS) + return {}; + return { + static_cast(data.pixel_data), + data.size.w, + data.size.h, + static_cast(data.stride), + Pixel_Format::Premultiplied_32 + }; +} + +Painter::Painter(Blend2D_Color_Cache& cache, Size size) { + cache.ensure_size(size); + if (!cache.image_.is_empty() && context_.begin(cache.image_) == BL_SUCCESS) { + context_.set_comp_op(BL_COMP_OP_SRC_OVER); + active_ = true; + } +} + +Painter::~Painter() { + if (active_) + context_.end(); +} + +BLRgba Painter::rgba(Color color) noexcept { + constexpr double scale = 1.0 / 255.0; + return BLRgba(color.r * scale, color.g * scale, color.b * scale, color.a * scale); +} + +void Painter::apply_pen(const Pen& pen) { + context_.set_stroke_style(rgba(pen.color)); + context_.set_stroke_width(std::max(0.1, pen.width)); + context_.set_stroke_caps(stroke_cap(pen.cap)); + context_.set_stroke_join(stroke_join(pen.join)); + BLArray pattern; + if (pen.style == Line_Style::Dash) { + pattern.append(std::max(1.0, pen.width * 4.0)); + pattern.append(std::max(1.0, pen.width * 2.0)); + } else if (pen.style == Line_Style::Dot) { + pattern.append(std::max(1.0, pen.width)); + pattern.append(std::max(1.0, pen.width * 2.0)); + } + context_.set_stroke_dash_array(pattern); +} + +void Painter::line(PointF first, PointF second, const Pen& pen) { + if (!active_ || !pen.enabled()) + return; + apply_pen(pen); + context_.stroke_line(first.x, first.y, second.x, second.y); +} + +void Painter::polyline(std::span points, const Pen& pen) { + if (!active_ || !pen.enabled() || points.size() < 2) + return; + BLPath path; + path.move_to(points.front().x, points.front().y); + for (std::size_t index = 1; index < points.size(); ++index) + path.line_to(points[index].x, points[index].y); + apply_pen(pen); + context_.stroke_path(path); +} + +void Painter::polygon(std::span points, const Pen& pen, const Brush& brush) { + if (!active_ || points.size() < 3) + return; + BLPath path; + path.move_to(points.front().x, points.front().y); + for (std::size_t index = 1; index < points.size(); ++index) + path.line_to(points[index].x, points[index].y); + path.close(); + if (brush.enabled()) { + context_.set_fill_style(rgba(brush.color)); + context_.fill_path(path); + } + if (pen.enabled()) { + apply_pen(pen); + context_.stroke_path(path); + } +} + +void Painter::rect(RectF value, const Pen& pen, const Brush& brush) { + if (!active_) + return; + value = value.normalized(); + if (value.empty()) + return; + const BLRect rect(value.x, value.y, value.width, value.height); + if (brush.enabled()) { + context_.set_fill_style(rgba(brush.color)); + context_.fill_rect(rect); + } + if (pen.enabled()) { + apply_pen(pen); + context_.stroke_rect(rect); + } +} + +void Painter::circle(PointF center, double radius, const Pen& pen, const Brush& brush) { + if (!active_ || radius <= 0.0) + return; + BLPath path; + path.add_ellipse(BLEllipse(center.x, center.y, radius, radius)); + if (brush.enabled()) { + context_.set_fill_style(rgba(brush.color)); + context_.fill_path(path); + } + if (pen.enabled()) { + apply_pen(pen); + context_.stroke_path(path); + } +} + +BLFont Painter::make_font(const Font& font) { + BLFont result; + auto& face = default_font_face(font); + if (face) + result.create_from_face(face, static_cast(std::max(1.0, font.size))); + return result; +} + +void Painter::text(PointF position, std::string_view value, const Font& font, const Pen& pen, + double rotation_degrees) { + if (!active_ || value.empty() || !pen.enabled()) + return; + BLFont bl_font = make_font(font); + if (!bl_font) + return; + context_.set_fill_style(rgba(pen.color)); + const auto& metrics = bl_font.metrics(); + const double baseline = position.y + metrics.ascent; + const bool rotated = std::isfinite(rotation_degrees) && rotation_degrees != 0.0; + if (rotated) { + context_.save(); + context_.rotate(rotation_degrees * std::numbers::pi / 180.0, position.x, baseline); + } + context_.fill_utf8_text(BLPoint(position.x, baseline), + bl_font, value.data(), value.size()); + if (rotated) + context_.restore(); +} + +void Painter::heatmap(RectF target, + int width, + int height, + std::span pixels, + Image_Interpolation_Mode interpolation) { + target = target.normalized(); + if (!active_ || target.empty() || width <= 0 || height <= 0 || + pixels.size() < static_cast(width) * static_cast(height)) + return; + + std::vector resampled; + if (interpolation == Image_Interpolation_Mode::Bicubic && + target.width <= static_cast(std::numeric_limits::max()) && + target.height <= static_cast(std::numeric_limits::max())) { + const int destination_width = std::max(1, static_cast(std::lround(target.width))); + const int destination_height = std::max(1, static_cast(std::lround(target.height))); + resampled = bicubic_resample(pixels, width, height, + destination_width, destination_height); + pixels = resampled; + width = destination_width; + height = destination_height; + } + + BLImage image(width, height, BL_FORMAT_PRGB32); + BLImageData data{}; + if (image.get_data(&data) != BL_SUCCESS) + return; + for (int y = 0; y < height; ++y) { + auto* destination = reinterpret_cast( + static_cast(data.pixel_data) + static_cast(y) * data.stride); + std::copy_n(pixels.data() + static_cast(y) * width, width, destination); + } + context_.set_pattern_quality(interpolation == Image_Interpolation_Mode::Bilinear + ? BL_PATTERN_QUALITY_BILINEAR + : BL_PATTERN_QUALITY_NEAREST); + context_.blit_image(BLRect(target.x, target.y, target.width, target.height), + image, BLRectI(0, 0, width, height)); +} + +} // namespace renderive::detail diff --git a/Core2/render/Blend2D_Cache.h b/Core2/render/Blend2D_Cache.h new file mode 100644 index 0000000..a79a50c --- /dev/null +++ b/Core2/render/Blend2D_Cache.h @@ -0,0 +1,59 @@ +#pragma once + +#include "../base/Types.h" + +#include +#include + +#include +#include + +namespace renderive::detail { + +class Blend2D_Color_Cache final : public ::Color_Cache { +public: + Blend2D_Color_Cache() = default; + explicit Blend2D_Color_Cache(std::pmr::memory_resource&) {} + + void clear() override; + void composite(const ::Color_Cache& source) override; + void ensure_size(Size size); + [[nodiscard]] Size size() const noexcept; + [[nodiscard]] Image_View view() const noexcept; + +private: + friend class Painter; + BLImage image_; +}; + +class Painter { +public: + Painter(Blend2D_Color_Cache& cache, Size size); + ~Painter(); + Painter(const Painter&) = delete; + Painter& operator=(const Painter&) = delete; + + [[nodiscard]] explicit operator bool() const noexcept { return active_; } + void line(PointF first, PointF second, const Pen& pen); + void polyline(std::span points, const Pen& pen); + void polygon(std::span points, const Pen& pen, const Brush& brush); + void rect(RectF rect, const Pen& pen, const Brush& brush = {}); + void circle(PointF center, double radius, const Pen& pen, const Brush& brush = {}); + void text(PointF position, std::string_view value, const Font& font, const Pen& pen, + double rotation_degrees = 0.0); + void heatmap(RectF target, + int width, + int height, + std::span pixels, + Image_Interpolation_Mode interpolation); + +private: + void apply_pen(const Pen& pen); + static BLRgba rgba(Color color) noexcept; + static BLFont make_font(const Font& font); + + BLContext context_; + bool active_{}; +}; + +} // namespace renderive::detail diff --git a/Core2/renderable/Renderable.cpp b/Core2/renderable/Renderable.cpp new file mode 100644 index 0000000..9973b2f --- /dev/null +++ b/Core2/renderable/Renderable.cpp @@ -0,0 +1,64 @@ +#include "Renderable.h" + +#include "../plot/Plot_Core.h" +#include "../render/Blend2D_Cache.h" + +#include + +namespace renderive { + +Renderable::Renderable(Plot_Core& plot, bool cache_enabled) + : ::Renderable_Base(plot.kernel_scene(), {.cache_enabled = cache_enabled}), plot_(plot) {} + +Renderable::~Renderable() = default; + +Renderable_Cache_Mode Renderable::get_cache_mode() const noexcept { + return configuration().cache_enabled + ? Renderable_Cache_Mode::Local_Pixel + : Renderable_Cache_Mode::Direct; +} + +void Renderable::set_cache_mode(Renderable_Cache_Mode mode) { + plot_.set_renderable_cache(*this, mode); +} + +std::string Renderable::object_name() const { + std::lock_guard lock(metadata_mutex_); + return object_name_; +} + +void Renderable::set_object_name(std::string name) { + std::lock_guard lock(metadata_mutex_); + object_name_ = std::move(name); +} + +bool Renderable::is_visible() const noexcept { + return visible_.load(std::memory_order_acquire); +} + +void Renderable::set_visible(bool visible) { + if (visible_.exchange(visible, std::memory_order_acq_rel) != visible) + changed(); +} + +void Renderable::render(const ::Scene_Render_Context& context) { + if (!is_visible() || context.color_cache == nullptr) + return; + auto* cache = dynamic_cast(context.color_cache); + if (!cache) + return; + detail::Painter painter(*cache, viewport_size()); + if (painter) + paint(painter); +} + +void Renderable::changed() noexcept { + invalidate_cache(); + plot_.notify_model_dirty(); +} + +Size Renderable::viewport_size() const noexcept { + return plot_.viewport_size(); +} + +} // namespace renderive diff --git a/Core2/renderable/Renderable.h b/Core2/renderable/Renderable.h new file mode 100644 index 0000000..b6eaa15 --- /dev/null +++ b/Core2/renderable/Renderable.h @@ -0,0 +1,53 @@ +#pragma once + +#include "../base/Types.h" +#include "../event/Event.h" + +#include + +#include +#include +#include + +namespace renderive { + +class Plot_Core; +namespace detail { +class Painter; +} + +class Event_Handler { +public: + virtual ~Event_Handler() = default; + virtual void handle_event(const Event& event) = 0; +}; + +class LIB_DECL Renderable : public ::Renderable_Base { +public: + explicit Renderable(Plot_Core& plot, bool cache_enabled = true); + ~Renderable() override; + + [[nodiscard]] Plot_Core& plot() const noexcept { return plot_; } + [[nodiscard]] Renderable_Cache_Mode get_cache_mode() const noexcept; + void set_cache_mode(Renderable_Cache_Mode mode); + + [[nodiscard]] std::string object_name() const; + void set_object_name(std::string name); + [[nodiscard]] bool is_visible() const noexcept; + void set_visible(bool visible); + + void render(const ::Scene_Render_Context& context) final; + +protected: + virtual void paint(detail::Painter& painter) = 0; + void changed() noexcept; + [[nodiscard]] Size viewport_size() const noexcept; + +private: + Plot_Core& plot_; + mutable std::mutex metadata_mutex_; + std::string object_name_; + std::atomic visible_{true}; +}; + +} // namespace renderive diff --git a/Core2/tests/Core2_Integration_Tests.cpp b/Core2/tests/Core2_Integration_Tests.cpp new file mode 100644 index 0000000..6a1be1c --- /dev/null +++ b/Core2/tests/Core2_Integration_Tests.cpp @@ -0,0 +1,219 @@ +#include "Core2/export.h" +#include "Core2/plottable/Curve_Sampling.h" +#include "Core2/render/Blend2D_Cache.h" + +#include + +#include +#include +#include + +namespace renderive { +namespace { + +TEST(Renderive_Core2, RootIdentityAndRefreshDiagnosticsComeFromKernelState) { + Plot_Core plot; + plot.init(); + const auto root = plot.root_renderable(); + ASSERT_TRUE(root); + root->set_object_name("renamed-root"); + EXPECT_EQ(plot.root_renderable(), root); + + plot.set_viewport_size({32, 24}); + plot.activate_view(); + plot.set_max_render_fps(144.0); + ASSERT_TRUE(plot.render_frame()); + const auto diagnostics = plot.diagnostics(); + EXPECT_DOUBLE_EQ(diagnostics.refresh.frequency_hz, 144.0); + EXPECT_EQ(diagnostics.refresh.frame_count, 1u); +} + +TEST(Renderive_Core2, EveryCurveInterpolationModeHasDistinctSamplingSemantics) { + const std::array edge_values{0.0, 10.0}; + const Range domain{0.0, 1.0}; + const auto nearest = detail::curve_sampling::interpolate( + edge_values, domain, Line_Interpolation_Mode::Nearest_Sample); + const auto linear = detail::curve_sampling::interpolate( + edge_values, domain, Line_Interpolation_Mode::Linear_Value); + const auto power = detail::curve_sampling::interpolate( + edge_values, domain, Line_Interpolation_Mode::Linear_Power_Domain); + const auto step_left = detail::curve_sampling::interpolate( + edge_values, domain, Line_Interpolation_Mode::Step_Left); + const auto step_right = detail::curve_sampling::interpolate( + edge_values, domain, Line_Interpolation_Mode::Step_Right); + EXPECT_EQ(nearest.size(), 4u); + EXPECT_EQ(linear.size(), 2u); + ASSERT_EQ(power.size(), 5u); + EXPECT_NEAR(power[2].value, 7.4036269, 1e-6); + ASSERT_EQ(step_left.size(), 3u); + ASSERT_EQ(step_right.size(), 3u); + EXPECT_DOUBLE_EQ(step_left[1].coordinate, 1.0); + EXPECT_DOUBLE_EQ(step_left[1].value, 0.0); + EXPECT_DOUBLE_EQ(step_right[1].coordinate, 0.0); + EXPECT_DOUBLE_EQ(step_right[1].value, 10.0); + + const std::array curved_values{0.0, 10.0, 0.0, 10.0}; + const auto cubic = detail::curve_sampling::interpolate( + curved_values, {0.0, 3.0}, Line_Interpolation_Mode::Cubic_Value); + ASSERT_EQ(cubic.size(), 13u); + EXPECT_NEAR(cubic[2].value, 5.625, 1e-9); + + const std::array visible_values{}; + const Axis_Transform visible_x{{4.0, 6.0}, 0.0, 100.0}; + const Axis_Transform visible_y{{0.0, 1.0}, 0.0, 100.0}; + const auto clipped = detail::curve_points( + visible_values, {0.0, 10.0}, visible_x, visible_y, true, + Line_Interpolation_Mode::Linear_Value); + EXPECT_LT(clipped.size(), visible_values.size()); + ASSERT_GE(clipped.size(), 2u); + EXPECT_LE(clipped.front().x, 0.0); + EXPECT_GE(clipped.back().x, 100.0); +} + +TEST(Renderive_Core2, BicubicHeatmapUsesRealCubicResampling) { + const std::array source{ + pack_rgba(255, 0, 0), pack_rgba(0, 0, 0), pack_rgba(255, 255, 255), pack_rgba(0, 0, 255), + pack_rgba(0, 255, 0), pack_rgba(255, 255, 255), pack_rgba(0, 0, 0), pack_rgba(255, 0, 0), + pack_rgba(0, 0, 255), pack_rgba(0, 0, 0), pack_rgba(255, 255, 255), pack_rgba(0, 255, 0), + pack_rgba(255, 255, 255), pack_rgba(255, 0, 0), pack_rgba(0, 255, 0), pack_rgba(0, 0, 0) + }; + const auto render = [&source](Image_Interpolation_Mode interpolation) { + detail::Blend2D_Color_Cache cache; + { + detail::Painter painter(cache, {11, 11}); + painter.heatmap({0.0, 0.0, 11.0, 11.0}, 4, 4, source, interpolation); + } + const Image_View view = cache.view(); + std::vector pixels; + pixels.reserve(static_cast(view.width) * view.height); + for (int y = 0; y < view.height; ++y) { + const auto* row = reinterpret_cast( + view.data + static_cast(y) * view.stride); + pixels.insert(pixels.end(), row, row + view.width); + } + return pixels; + }; + const auto bilinear = render(Image_Interpolation_Mode::Bilinear); + const auto bicubic = render(Image_Interpolation_Mode::Bicubic); + EXPECT_EQ(bilinear.size(), bicubic.size()); + EXPECT_NE(bilinear, bicubic); +} + +TEST(Renderive_Core2, KernelSceneRendersBusinessObjectsIntoBlend2DFrame) { + Plot_Core plot; + plot.init(); + plot.set_viewport_size({320, 180}); + plot.activate_view(); + + const auto root = plot.root_renderable(); + ASSERT_TRUE(root); + auto frequency_axis = Frequency_Axis::Builder(root, Orientation::Horizontal) + .set_x(32) + .set_y(150) + .set_pixel_length(270) + .set_coord_range({88.0, 108.0}) + .build(); + frequency_axis->set_locale({','}); + frequency_axis->set_label_rotation_degrees(15); + EXPECT_EQ(frequency_axis->tick_label(88.5), "88,5 Hz"); + frequency_axis->set_label_precision(4); + EXPECT_EQ(frequency_axis->tick_label(1'234'567.0), "1,2346 MHz"); + auto power_axis = Axis::Builder(root, Orientation::Vertical) + .set_x(32) + .set_y(8) + .set_pixel_length(142) + .set_coord_range({-120.0, 0.0}) + .build(); + auto spectrum = Spectrum::Builder(root, frequency_axis, power_axis) + .set_frequency_range({88.0, 108.0}) + .set_frequency_point_size(64) + .set_max_hold_visible(true) + .build(); + + std::vector samples(64); + for (std::size_t index = 0; index < samples.size(); ++index) + samples[index] = -100.0 + static_cast(index % 24) * 3.0; + spectrum->update_samples(samples); + + ASSERT_TRUE(plot.render_frame()); + bool saw_frame = false; + bool saw_drawn_pixel = false; + plot.with_frame([&](Image_View view) { + saw_frame = !view.empty() && view.width == 320 && view.height == 180; + if (!saw_frame) + return; + for (int y = 0; y < view.height && !saw_drawn_pixel; ++y) { + const auto* row = view.data + static_cast(y) * view.stride; + for (int x = 0; x < view.width * static_cast(sizeof(Pixel)); ++x) { + if (row[x] != std::byte{}) { + saw_drawn_pixel = true; + break; + } + } + } + }); + EXPECT_TRUE(saw_frame); + EXPECT_TRUE(saw_drawn_pixel); + EXPECT_EQ(plot.diagnostics().refresh.frame_count, 1u); + EXPECT_FALSE(plot.render_frame()); + + spectrum->update_samples(samples); + EXPECT_TRUE(plot.render_frame()); + + frequency_axis->set_use_wheel(true); + const Range before_zoom = frequency_axis->coord_range(); + Wheel_Event wheel; + wheel.position = {160.0, 150.0}; + wheel.angle_delta_y = 120.0; + plot.dispatch_event(wheel); + EXPECT_TRUE(wheel.is_accepted()); + EXPECT_LT(frequency_axis->coord_range().size(), before_zoom.size()); + EXPECT_TRUE(plot.render_frame()); + + plot.remove_renderable(spectrum); + EXPECT_TRUE(plot.render_frame(true)); + EXPECT_FALSE(plot.render_frame()); +} + +TEST(Renderive_Core2, TimeAxisUsesOneFontStateAndFormatsConfiguredLabels) { + Plot_Core plot; + plot.init(); + const auto root = plot.root_renderable(); + auto axis = Time_Axis::Builder(root, Orientation::Horizontal) + .set_time_format("hh-mm-ss.zzz") + .set_font(Font{18.0, 700, true}) + .set_tick_label_spacing_px(17) + .build(); + ASSERT_TRUE(axis); + const int tick = axis->append_time(Time_Of_Day{3'723'045}); + EXPECT_EQ(axis->tick_label(tick), "01-02-03.045"); + EXPECT_EQ(axis->font(), (Font{18.0, 700, true})); + EXPECT_EQ(axis->unit_text_font(), axis->font()); + EXPECT_EQ(axis->tick_label_spacing_px(), 17); +} + +TEST(Renderive_Core2, PerformanceOverlayConsumesKernelFrameDiagnostics) { + Plot_Core plot; + plot.init(); + plot.set_viewport_size({160, 90}); + plot.activate_view(); + plot.set_max_render_fps(120.0); + Performance_Overlay_Options options; + options.log.enabled = false; + const auto overlay = attach_performance_overlay(plot, options); + ASSERT_TRUE(overlay); + set_performance_plot_name(plot, "integration"); + set_performance_overlay_enabled(plot, true); + + ASSERT_TRUE(plot.render_frame(true)); + const auto snapshot = overlay->display_snapshot(); + ASSERT_TRUE(snapshot); + EXPECT_EQ(snapshot->plot_name, "integration"); + EXPECT_EQ(snapshot->viewport_size, (Size{160, 90})); + EXPECT_FALSE(snapshot->lines.empty()); + EXPECT_EQ(snapshot->frame_count, 1u); + EXPECT_DOUBLE_EQ(plot.max_render_fps(), 120.0); +} + +} // namespace +} // namespace renderive diff --git a/Core2/tests/Qt_Bridge_Tests.cpp b/Core2/tests/Qt_Bridge_Tests.cpp new file mode 100644 index 0000000..5e14527 --- /dev/null +++ b/Core2/tests/Qt_Bridge_Tests.cpp @@ -0,0 +1,57 @@ +#include "Qt/plot/Latency_Eager_Plot.h" + +#include +#include +#include +#include + +#include + +namespace renderive { +namespace { + +TEST(Renderive_Qt, WidgetLifecycleDrivesAndStopsKernelScene) { + auto* application = qobject_cast(QCoreApplication::instance()); + ASSERT_NE(application, nullptr); + + Latency_Eager_Plot plot; + plot.resize(240, 120); + plot.init(); + auto x_axis = Frequency_Axis::Builder(plot.root_renderable(), Orientation::Horizontal) + .set_x(20) + .set_y(100) + .set_pixel_length(200) + .set_coord_range({0.0, 10.0}) + .build(); + auto y_axis = Axis::Builder(plot.root_renderable(), Orientation::Vertical) + .set_x(20) + .set_y(10) + .set_pixel_length(90) + .set_coord_range({1.0, 0.0}) + .build(); + auto spectrum = Spectrum::Builder(plot.root_renderable(), x_axis, y_axis) + .set_frequency_range({0.0, 10.0}) + .set_frequency_point_size(8) + .build(); + spectrum->update_samples(std::vector{0.1, 0.3, 0.8, 0.5, 0.9, 0.4, 0.2, 0.7}); + + plot.show(); + QTimer::singleShot(150, application, &QCoreApplication::quit); + EXPECT_EQ(application->exec(), 0); + EXPECT_GT(plot.diagnostics().refresh.frame_count, 0u); + plot.pause(); + plot.hide(); + application->processEvents(); + EXPECT_FALSE(plot.running()); +} + +} // namespace +} // namespace renderive + +int main(int argc, char** argv) { + qputenv("QT_QPA_PLATFORM", "offscreen"); + QApplication application(argc, argv); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + diff --git a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp index b1bd2fc..67b233e 100644 --- a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp @@ -160,7 +160,6 @@ private: bool discard_pending_frame_locked(Observation& observation); Frame frames_[3]; Observer observer_; - Configuration configuration_; State state_; Frame* paint_; Frame* cache_; diff --git a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl index 587f761..22cb659 100644 --- a/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl +++ b/Kernel/src/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.inl @@ -204,9 +204,9 @@ auto Low_Latency_Strategy::Render_Lease::get() con } template Low_Latency_Strategy::Low_Latency_Strategy(Observer observer, Configuration configuration) - : Frame_Control_Strategy_Base(checked_frequency_hz(configuration.frequency_hz), frequency_interval_ns(configuration.frequency_hz)), observer_(std::move(observer)), configuration_(configuration), paint_(&frames_[0]), cache_(&frames_[1]), render_(&frames_[2]) { - state_.frequency_hz = configuration_.frequency_hz; - state_.target_interval_ns = frequency_interval_ns(configuration_.frequency_hz); + : Frame_Control_Strategy_Base(checked_frequency_hz(configuration.frequency_hz), frequency_interval_ns(configuration.frequency_hz)), observer_(std::move(observer)), paint_(&frames_[0]), cache_(&frames_[1]), render_(&frames_[2]) { + state_.frequency_hz = configuration.frequency_hz; + state_.target_interval_ns = frequency_interval_ns(configuration.frequency_hz); state_.next_refresh_interval_ns = state_.target_interval_ns; } template @@ -221,7 +221,11 @@ template void Low_Latency_Strategy::set_frequency_hz(double frequency_hz) { frequency_hz = checked_frequency_hz(frequency_hz); std::lock_guard lock(state_mutex_); - configuration_.frequency_hz = frequency_hz; + state_.frequency_hz = frequency_hz; + state_.target_interval_ns = frequency_interval_ns(frequency_hz); + state_.next_refresh_interval_ns = std::max(state_.target_interval_ns, + state_.bottleneck_duration_ns); + update_frame_control_state(state_.frequency_hz, state_.next_refresh_interval_ns); } template void Low_Latency_Strategy::swap() { @@ -317,8 +321,6 @@ std::uint64_t Low_Latency_Strategy::now_ns() const } template void Low_Latency_Strategy::update_state(const Frame& frame) { - state_.frequency_hz = configuration_.frequency_hz; - state_.target_interval_ns = frequency_interval_ns(configuration_.frequency_hz); state_.paint_duration_ns = frame.statistics.timing.paint_duration_ns; state_.render_duration_ns = frame.statistics.timing.render_duration_ns; state_.bottleneck_duration_ns = std::max(state_.paint_duration_ns, state_.render_duration_ns); diff --git a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp index 3c61582..fbe0e71 100644 --- a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp +++ b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -21,6 +22,7 @@ public: static_assert(Timed_Struct_Observer); explicit History_Real_Time_Data(Observer observer = {}); History_Real_Time_Data(std::pmr::memory_resource& memory_resource, Observer observer = {}); + ~History_Real_Time_Data() override; History_Real_Time_Data(const History_Real_Time_Data&) = delete; History_Real_Time_Data& operator=(const History_Real_Time_Data&) = delete; void update(Value value); @@ -40,11 +42,15 @@ public: } private: static Container make_container(std::pmr::memory_resource& memory_resource); + void reserve_update_times(std::size_t capacity); mutable Mutex mutex_; std::recursive_mutex mutation_mutex_; Observer observer_; Container values_; - std::pmr::vector update_times_; + std::pmr::memory_resource* memory_resource_{}; + std::uint64_t* update_times_{}; + std::size_t update_times_size_{}; + std::size_t update_times_capacity_{}; std::uint64_t revision_{}; std::uint64_t total_update_count_{}; std::uint64_t last_update_time_ns_{}; diff --git a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl index 6728aab..fdcbcc7 100644 --- a/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl +++ b/Kernel/src/renderive/real_time_data/History_Real_Time_Data.inl @@ -4,7 +4,12 @@ History_Real_Time_Data::History_Real_Tim : History_Real_Time_Data(*std::pmr::get_default_resource(), std::move(observer)) {} template History_Real_Time_Data::History_Real_Time_Data(std::pmr::memory_resource& memory_resource, Observer observer) - : observer_(std::move(observer)), values_(make_container(memory_resource)), update_times_(&memory_resource) {} + : observer_(std::move(observer)), values_(make_container(memory_resource)), memory_resource_(&memory_resource) {} +template +History_Real_Time_Data::~History_Real_Time_Data() { + if (update_times_) + memory_resource_->deallocate(update_times_, update_times_capacity_ * sizeof(std::uint64_t), alignof(std::uint64_t)); +} template auto History_Real_Time_Data::make_container(std::pmr::memory_resource& memory_resource) -> Container { if constexpr (requires { typename Container::allocator_type; }) { @@ -19,20 +24,29 @@ auto History_Real_Time_Data::make_contai } } template +void History_Real_Time_Data::reserve_update_times(std::size_t capacity) { + if (capacity <= update_times_capacity_) + return; + const std::size_t next_capacity = std::max(capacity, std::max(8, update_times_capacity_ * 2)); + auto* next = static_cast( + memory_resource_->allocate(next_capacity * sizeof(std::uint64_t), alignof(std::uint64_t))); + if (update_times_size_ != 0) + std::copy_n(update_times_, update_times_size_, next); + if (update_times_) + memory_resource_->deallocate(update_times_, update_times_capacity_ * sizeof(std::uint64_t), alignof(std::uint64_t)); + update_times_ = next; + update_times_capacity_ = next_capacity; +} +template void History_Real_Time_Data::update(Value value) { std::lock_guard mutation_lock(mutation_mutex_); Real_Time_Data_Observation observation; { std::lock_guard lock(mutex_); - update_times_.push_back(0); - try { - values_.push_back(std::move(value)); - } catch (...) { - update_times_.pop_back(); - throw; - } + reserve_update_times(update_times_size_ + 1); + values_.push_back(std::move(value)); const std::uint64_t update_time_ns = observer_.now_ns(); - update_times_.back() = update_time_ns; + update_times_[update_times_size_++] = update_time_ns; ++revision_; ++total_update_count_; last_update_time_ns_ = update_time_ns; @@ -47,7 +61,7 @@ void History_Real_Time_Data::clear() { { std::lock_guard lock(mutex_); values_.clear(); - update_times_.clear(); + update_times_size_ = 0; ++revision_; last_update_time_ns_ = observer_.now_ns(); observation = {Real_Time_Data_Observation_Event::cleared, {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, 0}}; @@ -86,14 +100,15 @@ std::size_t History_Real_Time_Data::disc std::size_t discarded{}; { std::lock_guard lock(mutex_); - while (discarded < update_times_.size() && update_times_[discarded] < time_ns) { + while (discarded < update_times_size_ && update_times_[discarded] < time_ns) { ++discarded; } if (discarded == 0) { return 0; } values_.erase(values_.begin(), std::next(values_.begin(), static_cast(discarded))); - update_times_.erase(update_times_.begin(), update_times_.begin() + static_cast(discarded)); + std::move(update_times_ + discarded, update_times_ + update_times_size_, update_times_); + update_times_size_ -= discarded; ++revision_; observation = {Real_Time_Data_Observation_Event::discarded, {this, Real_Time_Data_Retention::history, revision_, last_update_time_ns_, total_update_count_, values_.size()}}; } diff --git a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp index 63f8ef9..048c551 100644 --- a/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp +++ b/Kernel/tests/renderive/frame_control/strategy/low_latency/Low_Latency_Strategy_Test.cpp @@ -70,6 +70,7 @@ TEST(low_latency_strategy_test, updates_frequency_after_completed_lifecycle) { Low_Latency_Test_Observer observer; auto strategy = make_low_latency_test_strategy(time_source, observer); strategy.set_frequency_hz(100.0); + EXPECT_DOUBLE_EQ(strategy.state().frequency_hz, 100.0); { auto frame = strategy.acquire_painter(); frame->value = 3; diff --git a/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp b/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp index bacf018..18041a1 100644 --- a/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp +++ b/Kernel/tests/renderive/real_time_data/Real_Time_Data_Test.cpp @@ -235,6 +235,8 @@ private: } }; TEST(history_real_time_data_test, failed_timestamp_allocation_rolls_back_value_and_revision) { + // MSVC's debug vector allocates a proxy at construction, so the production + // container must defer binding this deliberately failing resource to update(). Real_Time_Data_Failing_Memory_Resource memory_resource; History_Real_Time_Data data(memory_resource); EXPECT_THROW(data.update(7), std::bad_alloc); diff --git a/Qt/bridge/Qt_Event_Adapter.cpp b/Qt/bridge/Qt_Event_Adapter.cpp index 5e534f5..4fd7d4e 100644 --- a/Qt/bridge/Qt_Event_Adapter.cpp +++ b/Qt/bridge/Qt_Event_Adapter.cpp @@ -2,33 +2,6 @@ namespace renderive { -Event_Type Qt_Event_Adapter::map_event_type(QEvent::Type type) { - switch (type) { - case QEvent::MouseMove: - return Event_Type::Pointer_Move; - case QEvent::MouseButtonPress: - return Event_Type::Pointer_Press; - case QEvent::MouseButtonRelease: - return Event_Type::Pointer_Release; - case QEvent::Wheel: - return Event_Type::Wheel; - case QEvent::KeyPress: - return Event_Type::Key_Press; - case QEvent::KeyRelease: - return Event_Type::Key_Release; - case QEvent::Resize: - return Event_Type::Resize; - case QEvent::Show: - return Event_Type::Show; - case QEvent::Hide: - return Event_Type::Hide; - case QEvent::Leave: - return Event_Type::Leave; - default: - return Event_Type::Pointer_Move; - } -} - Pointer_Event Qt_Event_Adapter::to_pointer_event(const QMouseEvent* qt_event, Event_Type type) { Pointer_Event event(type); event.position = to_pointf(qt_event->pos()); @@ -73,10 +46,6 @@ PointF Qt_Event_Adapter::to_pointf(const QPointF& p) { return {p.x(), p.y()}; } -Point Qt_Event_Adapter::to_point(const QPoint& p) { - return {p.x(), p.y()}; -} - Size Qt_Event_Adapter::to_size(const QSize& s) { return {s.width(), s.height()}; } diff --git a/Qt/bridge/Qt_Event_Adapter.h b/Qt/bridge/Qt_Event_Adapter.h index f903f16..5243bde 100644 --- a/Qt/bridge/Qt_Event_Adapter.h +++ b/Qt/bridge/Qt_Event_Adapter.h @@ -1,9 +1,5 @@ #pragma once -#include "Core/event/Event.h" -#include "Core/event/Key_Event.h" -#include "Core/event/Pointer_Event.h" -#include "Core/event/Resize_Event.h" -#include "Core/event/Wheel_Event.h" +#include "Core2/event/Event.h" #include #include #include @@ -13,15 +9,12 @@ namespace renderive { struct Qt_Event_Adapter { - static Event_Type map_event_type(QEvent::Type type); - static Pointer_Event to_pointer_event(const QMouseEvent* qt_event, Event_Type type); static Wheel_Event to_wheel_event(const QWheelEvent* qt_event); static Key_Event to_key_event(const QKeyEvent* qt_event, Event_Type type); static Resize_Event to_resize_event(const QResizeEvent* qt_event); static PointF to_pointf(const QPointF& p); - static Point to_point(const QPoint& p); static Size to_size(const QSize& s); static Mouse_Button to_mouse_button(Qt::MouseButton button); diff --git a/Qt/bridge/Qt_Image_Adapter.h b/Qt/bridge/Qt_Image_Adapter.h index f292cd9..e5fc7d3 100644 --- a/Qt/bridge/Qt_Image_Adapter.h +++ b/Qt/bridge/Qt_Image_Adapter.h @@ -1,5 +1,5 @@ #pragma once -#include "Core/render/Image_View.h" +#include "Core2/base/Types.h" #include namespace renderive { diff --git a/Qt/bridge/Qt_Presentation_Sink.cpp b/Qt/bridge/Qt_Presentation_Sink.cpp index 3bb276e..0638f57 100644 --- a/Qt/bridge/Qt_Presentation_Sink.cpp +++ b/Qt/bridge/Qt_Presentation_Sink.cpp @@ -16,15 +16,16 @@ Qt_Presentation_Sink::Qt_Presentation_Sink(QWidget* widget) Qt_Presentation_Sink::~Qt_Presentation_Sink() = default; void Qt_Presentation_Sink::request_present(Rect dirty_rect) { - (void)dirty_rect; if (!d->widget) return; + const QRect update_rect(dirty_rect.x, dirty_rect.y, + dirty_rect.width, dirty_rect.height); QMetaObject::invokeMethod( d->widget.data(), - [w = QPointer(d->widget)]() { + [w = QPointer(d->widget), update_rect]() { if (w) - w->update(); + w->update(update_rect); }, Qt::QueuedConnection ); diff --git a/Qt/bridge/Qt_Presentation_Sink.h b/Qt/bridge/Qt_Presentation_Sink.h index 512f0a3..c19722a 100644 --- a/Qt/bridge/Qt_Presentation_Sink.h +++ b/Qt/bridge/Qt_Presentation_Sink.h @@ -1,5 +1,5 @@ #pragma once -#include "Core/plot/Presentation_Sink.h" +#include "Core2/plot/Plot_Core.h" #include #include diff --git a/Qt/plot/Explicit_Plot.cpp b/Qt/plot/Explicit_Plot.cpp index 7b4f828..950780d 100644 --- a/Qt/plot/Explicit_Plot.cpp +++ b/Qt/plot/Explicit_Plot.cpp @@ -1,23 +1,14 @@ -#include "Explicit_Plot_p.h" +#include "Explicit_Plot.h" + +#include "Plot_p.h" namespace renderive { -Explicit_Plot_Private::Explicit_Plot_Private() - : Abs_Plot_Private(std::make_unique()) { - explicit_flow = static_cast(flow); -} - Explicit_Plot::Explicit_Plot() - : Abs_Plot(new Explicit_Plot_Private()) {} + : Abs_Plot(new Abs_Plot_Private(false)) {} Render_Ticket Explicit_Plot::replot() { - auto* data = static_cast(d); - if (data->explicit_flow) { - Render_Ticket ticket = data->explicit_flow->request_render(); - data->core->request_explicit_render(); - return ticket; - } - return {}; + return {d->core->render_frame(true)}; } } // namespace renderive diff --git a/Qt/plot/Explicit_Plot.h b/Qt/plot/Explicit_Plot.h index 879f7e5..52dcad8 100644 --- a/Qt/plot/Explicit_Plot.h +++ b/Qt/plot/Explicit_Plot.h @@ -1,11 +1,19 @@ #pragma once -#include "Core/flow/Render_Ticket.h" + #include "Plot.h" + namespace renderive { + +struct Render_Ticket { + bool rendered{}; + [[nodiscard]] explicit operator bool() const noexcept { return rendered; } +}; + class LIB_DECL Explicit_Plot : public Abs_Plot { public: Explicit_Plot(); ~Explicit_Plot() override = default; - Render_Ticket replot(); + [[nodiscard]] Render_Ticket replot(); }; + } // namespace renderive diff --git a/Qt/plot/Explicit_Plot_p.h b/Qt/plot/Explicit_Plot_p.h deleted file mode 100644 index d53584b..0000000 --- a/Qt/plot/Explicit_Plot_p.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once -#include "Explicit_Plot.h" -#include "Plot_p.h" -#include "Core/flow/explicit/Explicit_Frame_Flow.h" - -namespace renderive { - -struct Explicit_Plot_Private : Abs_Plot_Private { - Explicit_Plot_Private(); - Explicit_Frame_Flow* explicit_flow{}; -}; - -} // namespace renderive diff --git a/Qt/plot/Latency_Eager_Plot.cpp b/Qt/plot/Latency_Eager_Plot.cpp index f8f1dd2..24feaed 100644 --- a/Qt/plot/Latency_Eager_Plot.cpp +++ b/Qt/plot/Latency_Eager_Plot.cpp @@ -1,37 +1,39 @@ -#include "Latency_Eager_Plot_p.h" +#include "Latency_Eager_Plot.h" + +#include "Plot_p.h" + namespace renderive { -Latency_Eager_Plot_Private::Latency_Eager_Plot_Private() - : Abs_Plot_Private(std::make_unique()) { - latency_flow = static_cast(flow); -} + Latency_Eager_Plot::Latency_Eager_Plot() - : Abs_Plot(new Latency_Eager_Plot_Private()) {} + : Abs_Plot(new Abs_Plot_Private(true)) {} + void Latency_Eager_Plot::start() { - d->core->activate_view(); + start_render_timer(); } + void Latency_Eager_Plot::pause() { - d->core->deactivate_view(); + stop_render_timer(); } + bool Latency_Eager_Plot::running() const { return d->core->view_active(); } + double Latency_Eager_Plot::max_render_fps() const { - auto* data = static_cast(d); - return data->latency_flow ? data->latency_flow->max_render_fps() : 0.0; + return d->core->max_render_fps(); } + void Latency_Eager_Plot::set_max_render_fps(double fps) { - auto* data = static_cast(d); - if (data->latency_flow) { - data->latency_flow->set_max_render_fps(fps); - data->core->notify_model_dirty(); - } + d->core->set_max_render_fps(fps); + update_render_interval(); } + Low_Latency_Diagnostics Latency_Eager_Plot::diagnostics() const { - auto* data = static_cast(d); - return data->latency_flow ? data->latency_flow->low_latency_diagnostics() : Low_Latency_Diagnostics{}; + return d->core->diagnostics(); } + Refresh_Control_Snapshot Latency_Eager_Plot::refresh_feedback_snapshot() const { - auto* data = static_cast(d); - return data->latency_flow ? data->latency_flow->feedback_state() : Refresh_Control_Snapshot{}; + return d->core->refresh_feedback_snapshot(); } + } // namespace renderive diff --git a/Qt/plot/Latency_Eager_Plot.h b/Qt/plot/Latency_Eager_Plot.h index 5a76235..dfe76a3 100644 --- a/Qt/plot/Latency_Eager_Plot.h +++ b/Qt/plot/Latency_Eager_Plot.h @@ -1,7 +1,9 @@ #pragma once -#include "Core/flow/Flow_Diagnostics.h" + #include "Plot.h" + namespace renderive { + class LIB_DECL Latency_Eager_Plot : public Abs_Plot { public: Latency_Eager_Plot(); @@ -14,4 +16,5 @@ public: [[nodiscard]] Low_Latency_Diagnostics diagnostics() const; [[nodiscard]] Refresh_Control_Snapshot refresh_feedback_snapshot() const; }; + } // namespace renderive diff --git a/Qt/plot/Latency_Eager_Plot_p.h b/Qt/plot/Latency_Eager_Plot_p.h deleted file mode 100644 index 7427cde..0000000 --- a/Qt/plot/Latency_Eager_Plot_p.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once -#include "Latency_Eager_Plot.h" -#include "Plot_p.h" -#include "Core/flow/low_latency/Low_Latency_Frame_Flow.h" - -namespace renderive { - -struct Latency_Eager_Plot_Private : Abs_Plot_Private { - Latency_Eager_Plot_Private(); - Low_Latency_Frame_Flow* latency_flow{}; -}; - -} // namespace renderive diff --git a/Qt/plot/Plot.cpp b/Qt/plot/Plot.cpp index 0a4c221..d6a458b 100644 --- a/Qt/plot/Plot.cpp +++ b/Qt/plot/Plot.cpp @@ -1,21 +1,20 @@ #include "Plot_p.h" + #include "../bridge/Qt_Event_Adapter.h" #include "../bridge/Qt_Image_Adapter.h" #include "../bridge/Qt_Presentation_Sink.h" -#include "Core/architecture/Render_Time.h" -#include "Core/base/global.h" -#include "Core/plottable/export.h" -#include "Core/plot/Plot_Core_Access.h" -#include "Core/architecture/Plot_Render_Context.h" -#include -#include + +#include +#include #include -#include #include #include +#include + +#include +#include namespace renderive { - namespace { Color from_qcolor(const QColor& color) { @@ -32,38 +31,25 @@ QColor to_qcolor(Color color) { } std::string to_utf8_string(const QString& text) { - QByteArray bytes = text.toUtf8(); - return std::string(bytes.constData(), static_cast(bytes.size())); + const QByteArray bytes = text.toUtf8(); + return {bytes.constData(), static_cast(bytes.size())}; } } // namespace -void start_render_scheduler(Render_Runtime_Config config) { - Global::instance()->start_render_scheduler(config); -} - -Abs_Plot_Private::Abs_Plot_Private(std::unique_ptr frame_flow) - : flow(frame_flow.get()), - core(std::make_unique(std::move(frame_flow))) {} +Abs_Plot_Private::Abs_Plot_Private(bool continuous_rendering) + : core(std::make_unique()), continuous(continuous_rendering) {} Abs_Plot_Private::~Abs_Plot_Private() = default; void Abs_Plot_Private::attach_widget(Abs_Plot* plot) { presentation_sink = std::make_shared(plot); core->set_presentation_sink(presentation_sink); - QPointer guarded_plot(plot); - auto pending = performance_update_pending; - set_performance_overlay_update_callback(*core, [guarded_plot, pending]() mutable { - if (!guarded_plot || pending->exchange(true, std::memory_order_acq_rel)) - return; - QTimer::singleShot(0, guarded_plot.data(), [guarded_plot, pending]() mutable { - pending->store(false, std::memory_order_release); - if (guarded_plot) - guarded_plot->update(); - }); - }); - set_performance_overlay_clipboard_callback(*core, [](std::string text) { - QGuiApplication::clipboard()->setText(QString::fromUtf8(text.data(), static_cast(text.size()))); + timer = new QTimer(plot); + timer->setTimerType(Qt::PreciseTimer); + QObject::connect(timer, &QTimer::timeout, plot, [this] { + if (core->view_active()) + (void)core->render_frame(); }); } @@ -73,23 +59,20 @@ Abs_Plot::Abs_Plot(Abs_Plot_Private* private_data) setAttribute(Qt::WA_OpaquePaintEvent, true); setMouseTracking(true); setFocusPolicy(Qt::NoFocus); + update_render_interval(); } Abs_Plot::~Abs_Plot() { - if (d) { - set_performance_overlay_update_callback(*d->core, {}); - set_performance_overlay_clipboard_callback(*d->core, {}); - d->core->deactivate_view(); - delete d; - d = nullptr; - } + if (!d) + return; + if (d->timer) + d->timer->stop(); + d->core->deactivate_view(); + delete d; + d = nullptr; } void Abs_Plot::init() { - init_renderable_tree(); -} - -void Abs_Plot::init_renderable_tree() { d->core->init(); } @@ -101,15 +84,16 @@ std::shared_ptr Abs_Plot::root_renderable() const { return d->core->root_renderable(); } -std::shared_ptr Abs_Plot::create_renderable_node(const std::shared_ptr& parent, const QString& object_name) const { +std::shared_ptr Abs_Plot::create_renderable_node( + const std::shared_ptr& parent, const QString& object_name) const { return d->core->create_renderable_node(parent, to_utf8_string(object_name)); } -void Abs_Plot::remove_renderable(const std::shared_ptr& able) { - d->core->remove_renderable(able); +void Abs_Plot::remove_renderable(const std::shared_ptr& renderable) { + d->core->remove_renderable(renderable); } -QColor Abs_Plot::background_color() { +QColor Abs_Plot::background_color() const { return to_qcolor(d->core->background_color()); } @@ -117,120 +101,166 @@ void Abs_Plot::set_background_color(const QColor& color) { d->core->set_background_color(from_qcolor(color)); } -void Abs_Plot::paintEvent(QPaintEvent* event) { - d->paint_event(this, event); +void Abs_Plot::start_render_timer() { + d->core->activate_view(); + update_render_interval(); + if (d->timer && !d->timer->isActive()) + d->timer->start(); +} + +void Abs_Plot::stop_render_timer() { + if (d->timer) + d->timer->stop(); + d->core->deactivate_view(); +} + +void Abs_Plot::update_render_interval() { + if (!d->timer) + return; + const double fps = std::max(1.0, d->core->max_render_fps()); + d->timer->setInterval(std::max(1, static_cast(std::lround(1000.0 / fps)))); +} + +void Abs_Plot::paintEvent(QPaintEvent*) { + d->paint_event(this); +} + +void Abs_Plot_Private::paint_event(Abs_Plot* plot) { + QPainter painter(plot); + painter.fillRect(plot->rect(), plot->background_color()); + core->with_frame([&painter](Image_View view) { + if (!view.empty()) + painter.drawImage(QPoint(0, 0), Qt_Image_Adapter::to_qimage(view)); + }); + draw_performance_overlay(plot, painter); + painter.end(); +} + +void Abs_Plot_Private::draw_performance_overlay(Abs_Plot* plot, QPainter& painter) { + const auto overlay = core->performance_overlay(); + if (!overlay || !overlay->enabled()) + return; + const auto snapshot = overlay->display_snapshot(); + if (!snapshot || snapshot->viewport_size != core->viewport_size()) + return; + const auto& options = snapshot->style; + QFont font = painter.font(); + font.setPointSizeF(std::max(1.0, options.font.size)); + font.setWeight(std::clamp(options.font.weight / 10, 0, 99)); + font.setItalic(options.font.italic); + painter.setFont(font); + const QFontMetrics metrics(font); + const int line_height = std::max(1, metrics.height() + 2); + const int left_margin = std::max(0, options.left_margin); + const int right_margin = std::max(0, options.right_margin); + const int top_margin = std::max(0, options.top_margin); + const int bottom_margin = std::max(0, options.bottom_margin); + int text_width{}; + for (const auto& line : snapshot->lines) + text_width = std::max(text_width, metrics.horizontalAdvance(QString::fromStdString(line))); + const int panel_width = std::min(plot->width(), left_margin + text_width + right_margin); + const int panel_height = std::min( + plot->height(), top_margin + bottom_margin + + line_height * static_cast(snapshot->lines.size())); + painter.fillRect(QRect(0, 0, panel_width, panel_height), to_qcolor(options.background)); + int y = top_margin + metrics.ascent(); + for (std::size_t index = 0; index < snapshot->lines.size(); ++index) { + const QString line = QString::fromStdString(snapshot->lines[index]); + if (index == 0) { + painter.setPen(to_qcolor(options.section_color)); + painter.drawText(left_margin, y, line); + } else if (const qsizetype separator = line.indexOf(':'); separator >= 0) { + const QString label = line.left(separator + 1); + painter.setPen(to_qcolor(options.label_color)); + painter.drawText(left_margin, y, label); + painter.setPen(to_qcolor(options.value_color)); + painter.drawText(left_margin + metrics.horizontalAdvance(label), y, + line.mid(separator + 1)); + } else { + painter.setPen(to_qcolor(options.label_color)); + painter.drawText(left_margin, y, line); + } + y += line_height; + } } bool Abs_Plot::event(QEvent* event) { - bool base_result = QWidget::event(event); - return d->event(event, base_result); -} - -void Abs_Plot_Private::paint_event(Abs_Plot* plot, QPaintEvent* event) { - (void)event; - std::uint64_t paint_begin_time = steady_now_ns(); - auto frame = core->begin_present(); - - QPainter painter(plot); - if (frame && !frame.image().empty()) { - QImage image = Qt_Image_Adapter::to_qimage(frame.image()); - painter.drawImage(QPoint(0, 0), image); - } - else { - painter.fillRect(plot->rect(), plot->background_color()); - } - auto performance_snapshot = performance_overlay_snapshot(*core); - if (performance_snapshot && !performance_snapshot->image.empty()) { - Size current_size = core->viewport_size(); - auto ctx = Plot_Core_Context_Access::render_context(*core); - std::uint64_t current_viewport_version = ctx ? ctx->viewport_version.load(std::memory_order_acquire) : 0; - if (performance_snapshot->viewport_size == current_size && performance_snapshot->viewport_version == current_viewport_version) { - QImage overlay = Qt_Image_Adapter::to_qimage(performance_snapshot->image.view()); - const RectF& target = performance_snapshot->destination_rect; - painter.drawImage(QRectF(target.x, target.y, target.width, target.height), overlay); - } - } - painter.end(); - - core->end_present(std::move(frame), paint_begin_time, steady_now_ns()); -} - -bool Abs_Plot_Private::event(QEvent* event, bool base_result) { + const auto dispatch = [this, event](const Event& translated) { + d->core->dispatch_event(translated); + if (!translated.is_accepted()) + return false; + event->accept(); + return true; + }; switch (event->type()) { case QEvent::Resize: { - auto* resize_event = static_cast(event); - core->set_viewport_size(Qt_Event_Adapter::to_size(resize_event->size())); - notify_performance_overlay_viewport_changed(*core); - core->dispatch_event(Qt_Event_Adapter::to_resize_event(resize_event)); - core->notify_model_dirty(); + auto* resize = static_cast(event); + d->core->set_viewport_size(Qt_Event_Adapter::to_size(resize->size())); + d->core->dispatch_event(Qt_Event_Adapter::to_resize_event(resize)); break; } - case QEvent::Show: { - Event show_event(Event_Type::Show); - core->dispatch_event(show_event); - core->activate_view(); + case QEvent::Show: + d->core->dispatch_event(Event(Event_Type::Show)); + if (d->continuous) + start_render_timer(); break; - } - case QEvent::Hide: { - Event hide_event(Event_Type::Hide); - core->dispatch_event(hide_event); - core->deactivate_view(); + case QEvent::Hide: + d->core->dispatch_event(Event(Event_Type::Hide)); + if (d->continuous) + stop_render_timer(); break; - } case QEvent::Leave: { - performance_overlay_handle_pointer_leave(*core); - Event leave_event(Event_Type::Leave); - core->dispatch_event(leave_event); + const Event translated(Event_Type::Leave); + if (dispatch(translated)) + return true; break; } case QEvent::Wheel: { - auto wheel_event = Qt_Event_Adapter::to_wheel_event(static_cast(event)); - if (performance_overlay_handle_wheel(*core, wheel_event.position, wheel_event.angle_delta_y, wheel_event.pixel_delta_y)) { - event->accept(); + const auto translated = Qt_Event_Adapter::to_wheel_event( + static_cast(event)); + if (dispatch(translated)) return true; - } - core->dispatch_event(wheel_event); break; } case QEvent::MouseMove: { - auto pointer_event = Qt_Event_Adapter::to_pointer_event(static_cast(event), Event_Type::Pointer_Move); - if (performance_overlay_handle_pointer_move(*core, pointer_event.position)) { - event->accept(); + const auto translated = Qt_Event_Adapter::to_pointer_event( + static_cast(event), Event_Type::Pointer_Move); + if (dispatch(translated)) return true; - } - core->dispatch_event(pointer_event); break; } case QEvent::MouseButtonPress: { - auto* mouse_event = static_cast(event); - auto pointer_event = Qt_Event_Adapter::to_pointer_event(mouse_event, Event_Type::Pointer_Press); - if (mouse_event->button() == Qt::LeftButton && performance_overlay_handle_pointer_press(*core, pointer_event.position)) { - event->accept(); + const auto translated = Qt_Event_Adapter::to_pointer_event( + static_cast(event), Event_Type::Pointer_Press); + if (dispatch(translated)) return true; - } - core->dispatch_event(pointer_event); break; } case QEvent::MouseButtonRelease: { - auto* mouse_event = static_cast(event); - auto pointer_event = Qt_Event_Adapter::to_pointer_event(mouse_event, Event_Type::Pointer_Release); - if (mouse_event->button() == Qt::LeftButton && performance_overlay_handle_pointer_release(*core, pointer_event.position)) { - event->accept(); + const auto translated = Qt_Event_Adapter::to_pointer_event( + static_cast(event), Event_Type::Pointer_Release); + if (dispatch(translated)) return true; - } - core->dispatch_event(pointer_event); break; } - case QEvent::KeyPress: - core->dispatch_event(Qt_Event_Adapter::to_key_event(static_cast(event), Event_Type::Key_Press)); + case QEvent::KeyPress: { + const auto translated = Qt_Event_Adapter::to_key_event( + static_cast(event), Event_Type::Key_Press); + if (dispatch(translated)) + return true; break; - case QEvent::KeyRelease: - core->dispatch_event(Qt_Event_Adapter::to_key_event(static_cast(event), Event_Type::Key_Release)); + } + case QEvent::KeyRelease: { + const auto translated = Qt_Event_Adapter::to_key_event( + static_cast(event), Event_Type::Key_Release); + if (dispatch(translated)) + return true; break; + } default: break; } - return base_result; + return QWidget::event(event); } } // namespace renderive diff --git a/Qt/plot/Plot.h b/Qt/plot/Plot.h index 585ca8a..664b7b2 100644 --- a/Qt/plot/Plot.h +++ b/Qt/plot/Plot.h @@ -1,37 +1,43 @@ #pragma once -#include "Core/architecture/global.h" -#include "Core/architecture/Render_Config.h" -#include "Core/architecture/Renderable.h" + +#include "Core2/export.h" + #include #include #include #include #include + #include + namespace renderive { -void LIB_DECL start_render_scheduler(Render_Runtime_Config config = {}); + struct Abs_Plot_Private; -class Plot_Core; -class Performance_Overlay; class Latency_Eager_Plot; class Explicit_Plot; + class LIB_DECL Abs_Plot : public QWidget { Q_OBJECT public: virtual void init(); - QColor background_color(); + [[nodiscard]] QColor background_color() const; [[nodiscard]] Plot_Core* core() const; [[nodiscard]] std::shared_ptr root_renderable() const; - [[nodiscard]] std::shared_ptr create_renderable_node(const std::shared_ptr& parent, const QString& object_name = {}) const; - QString object_name; + [[nodiscard]] std::shared_ptr create_renderable_node( + const std::shared_ptr& parent, const QString& object_name = {}) const; void set_background_color(const QColor& color); - void remove_renderable(const std::shared_ptr& able); - virtual void init_renderable_tree(); + void remove_renderable(const std::shared_ptr& renderable); ~Abs_Plot() override; + protected: explicit Abs_Plot(Abs_Plot_Private* private_data); + void start_render_timer(); + void stop_render_timer(); + void update_render_interval(); + bool event(QEvent* event) final; void paintEvent(QPaintEvent* event) final; Abs_Plot_Private* d{}; }; + } // namespace renderive diff --git a/Qt/plot/Plot_p.h b/Qt/plot/Plot_p.h index 02c23da..e76107f 100644 --- a/Qt/plot/Plot_p.h +++ b/Qt/plot/Plot_p.h @@ -1,26 +1,28 @@ #pragma once + #include "Plot.h" -#include "Core/flow/Frame_Flow.h" -#include "Core/plot/Plot_Core.h" -#include + #include +class QTimer; +class QPainter; + namespace renderive { class Qt_Presentation_Sink; struct Abs_Plot_Private { - explicit Abs_Plot_Private(std::unique_ptr frame_flow); - virtual ~Abs_Plot_Private(); + explicit Abs_Plot_Private(bool continuous_rendering); + ~Abs_Plot_Private(); void attach_widget(Abs_Plot* plot); - bool event(QEvent* event, bool base_result); - void paint_event(Abs_Plot* plot, QPaintEvent* event); + void paint_event(Abs_Plot* plot); + void draw_performance_overlay(Abs_Plot* plot, QPainter& painter); - Frame_Flow* flow{}; std::unique_ptr core; std::shared_ptr presentation_sink; - std::shared_ptr performance_update_pending = std::make_shared(false); + QTimer* timer{}; + bool continuous{}; }; } // namespace renderive diff --git a/export.h b/export.h index bd63907..dec4d42 100644 --- a/export.h +++ b/export.h @@ -10,7 +10,7 @@ #include #include #include -#include "Core/export.h" +#include "Core2/export.h" #include "Qt/plot/export.h" #include "Widget/export.h" @@ -61,6 +61,55 @@ inline Line_Style qt_to_line_style(Qt::PenStyle style) { } } +inline Line_Cap qt_to_line_cap(Qt::PenCapStyle cap) { + switch (cap) { + case Qt::SquareCap: + return Line_Cap::Square; + case Qt::RoundCap: + return Line_Cap::Round; + case Qt::FlatCap: + default: + return Line_Cap::Butt; + } +} + +inline Qt::PenCapStyle to_qt_line_cap(Line_Cap cap) { + switch (cap) { + case Line_Cap::Square: + return Qt::SquareCap; + case Line_Cap::Round: + return Qt::RoundCap; + case Line_Cap::Butt: + default: + return Qt::FlatCap; + } +} + +inline Line_Join qt_to_line_join(Qt::PenJoinStyle join) { + switch (join) { + case Qt::BevelJoin: + return Line_Join::Bevel; + case Qt::RoundJoin: + return Line_Join::Round; + case Qt::MiterJoin: + case Qt::SvgMiterJoin: + default: + return Line_Join::Miter; + } +} + +inline Qt::PenJoinStyle to_qt_line_join(Line_Join join) { + switch (join) { + case Line_Join::Bevel: + return Qt::BevelJoin; + case Line_Join::Round: + return Qt::RoundJoin; + case Line_Join::Miter: + default: + return Qt::MiterJoin; + } +} + inline Qt::PenStyle to_qt_pen_style(Line_Style style) { switch (style) { case Line_Style::None: @@ -75,7 +124,8 @@ inline Qt::PenStyle to_qt_pen_style(Line_Style style) { } inline Pen qt_to_pen(const QPen& pen) { - return Pen{qt_to_color(pen.color()), pen.widthF(), qt_to_line_style(pen.style())}; + return Pen{qt_to_color(pen.color()), pen.widthF(), qt_to_line_style(pen.style()), + qt_to_line_cap(pen.capStyle()), qt_to_line_join(pen.joinStyle())}; } inline Pen qt_to_pen(const QColor& color) { @@ -88,6 +138,8 @@ inline QPen to_qpen(const Pen& pen) { QPen ret(to_qcolor(pen.color)); ret.setWidthF(pen.width); ret.setStyle(to_qt_pen_style(pen.style)); + ret.setCapStyle(to_qt_line_cap(pen.cap)); + ret.setJoinStyle(to_qt_line_join(pen.join)); return ret; } @@ -130,12 +182,6 @@ inline Time_Of_Day qt_to_time_of_day(const QTime& time) { return {time.isValid() ? time.msecsSinceStartOfDay() : -1}; } -inline Plot_Core* plot_core(Abs_Plot* plot) { - if (!plot) - return nullptr; - return plot->core(); -} - namespace detail { inline std::default_random_engine& mock_data_random_engine() { static thread_local std::default_random_engine engine(std::random_device{}()); @@ -152,7 +198,7 @@ inline void fill_data(Range value_range, std::span output) { } inline std::vector get_data(Range value_range, int size) { - std::vector ret(size); + std::vector ret(static_cast(std::max(0, size))); fill_data(value_range, ret); return ret; } diff --git a/main.cmake b/main.cmake index be59b92..c26f1cf 100644 --- a/main.cmake +++ b/main.cmake @@ -1,72 +1,150 @@ include_guard(GLOBAL) -set(renderive_dependencies global::GTest global::blend2d global::taskflow global::mpmcqueue ${Psc_Core_dependencies}) + +set(renderive_dependencies + global::GTest + global::blend2d + global::taskflow + global::mpmcqueue + ${Psc_Core_dependencies} +) rcl_add_dependency_action_targets(renderive_env ${renderive_dependencies}) library_is_installed_with_rely(renderive_dependencies_installed ${renderive_dependencies}) if (NOT renderive_dependencies_installed) - message(STATUS "renderive_Core 依赖未安装,请先构建 renderive_env 目标") + set(log "[FATAL_ERROR] renderive_Core 依赖未安装,请先构建 renderive_env 目标") + rcl_log_append("${log}") return() -else () - find_package(Qt5 MODULE REQUIRED COMPONENTS Core - Gui Widgets - Svg Xml - PrintSupport - QuickWidgets - Multimedia - ) - find_package(blend2d REQUIRED) - find_package(GTest REQUIRED) - find_package(Taskflow CONFIG REQUIRED) - find_package(MPMCQueue CONFIG REQUIRED) - set(renderive_private_libraries - blend2d::blend2d - GTest::GTest - Taskflow::Taskflow - MPMCQueue::MPMCQueue - Psc_Core_Static - ) - set(qt_libraries - Qt5::Widgets - Qt5::Core - Qt5::Svg - Qt5::Xml - Qt5::PrintSupport - Qt5::Multimedia - Qt5::QuickWidgets - ) endif () -set(root_dir ${CMAKE_CURRENT_LIST_DIR}) -append_glob_source(kernel_srcs ${root_dir}/Kernel/src ${root_dir}/Kernel/tests) -add_executable(Renderive_Kernel - ${kernel_srcs} + +find_package(Qt5 MODULE REQUIRED COMPONENTS + Core Gui Widgets Svg Xml PrintSupport QuickWidgets Multimedia ) -target_include_directories(Renderive_Kernel PUBLIC - ${root_dir}/Kernel/src +find_package(blend2d REQUIRED) +find_package(GTest REQUIRED) +find_package(Taskflow CONFIG REQUIRED) +find_package(MPMCQueue CONFIG REQUIRED) +find_package(Threads REQUIRED) + +set(qt_libraries + Qt5::Widgets + Qt5::Core + Qt5::Svg + Qt5::Xml + Qt5::PrintSupport + Qt5::Multimedia + Qt5::QuickWidgets ) -target_compile_definitions(Renderive_Kernel PUBLIC RENDERIVE_WITH_GTEST) -target_link_libraries(Renderive_Kernel PUBLIC ${renderive_private_libraries}) -append_glob_source(srcs ${root_dir}/Core) -add_library(Renderive_Core STATIC ${srcs}) -target_include_directories(Renderive_Core PUBLIC ${root_dir}) -target_link_libraries(Renderive_Core PUBLIC ${renderive_private_libraries}) -enable_testing() -add_executable(Renderive_Core_Tests - ${root_dir}/test/Renderive_Core_Tests.cpp +set(root_dir "${CMAKE_CURRENT_LIST_DIR}") + +# Kernel is the only rendering architecture used by Core2. +add_library(Renderive_Kernel STATIC + "${root_dir}/Kernel/src/renderive/renderable/Renderable_Task_Graph.cpp" + "${root_dir}/Kernel/src/renderive/renderable/base/Renderable_Base.cpp" + "${root_dir}/Kernel/src/renderive/scene/base/Scene_Base.cpp" ) -target_link_libraries(Renderive_Core_Tests PRIVATE Renderive_Core GTest::GTest) -add_test(NAME Renderive_Core_Tests COMMAND Renderive_Core_Tests) -append_glob_source(src2 ${root_dir}/Qt) -add_library(Renderive_Qt STATIC ${src2}) +target_include_directories(Renderive_Kernel PUBLIC "${root_dir}/Kernel/src") +target_compile_features(Renderive_Kernel PUBLIC cxx_std_20) +target_link_libraries(Renderive_Kernel PRIVATE Taskflow::Taskflow Threads::Threads) + +# Preserve the legacy Core target and sources, but keep it out of Radio's build. +file(GLOB_RECURSE renderive_legacy_core_sources CONFIGURE_DEPENDS + "${root_dir}/Core/*.cpp" + "${root_dir}/Core/*.h" + "${root_dir}/Core/*.hpp" +) +add_library(Renderive_Core STATIC EXCLUDE_FROM_ALL ${renderive_legacy_core_sources}) +target_include_directories(Renderive_Core PUBLIC "${root_dir}") +target_compile_features(Renderive_Core PUBLIC cxx_std_20) +target_precompile_headers(Renderive_Core PRIVATE + "$<$:${root_dir}/../CPP_Core/src/Psc_Cpp_Core/Base/global_include.h>" +) +target_link_libraries(Renderive_Core PUBLIC + blend2d::blend2d + Taskflow::Taskflow + MPMCQueue::MPMCQueue + Psc_Core_Static +) + +file(GLOB_RECURSE renderive_core2_sources CONFIGURE_DEPENDS + "${root_dir}/Core2/*.cpp" + "${root_dir}/Core2/*.h" + "${root_dir}/Core2/*.hpp" +) +list(FILTER renderive_core2_sources EXCLUDE REGEX "[/\\\\]tests[/\\\\]") +add_library(Renderive_Core2 STATIC ${renderive_core2_sources}) +target_include_directories(Renderive_Core2 PUBLIC "${root_dir}") +target_compile_features(Renderive_Core2 PUBLIC cxx_std_20) +target_link_libraries(Renderive_Core2 + PUBLIC Renderive_Kernel + PRIVATE blend2d::blend2d +) + +set(renderive_qt_sources + "${root_dir}/Qt/bridge/Qt_Event_Adapter.cpp" + "${root_dir}/Qt/bridge/Qt_Image_Adapter.cpp" + "${root_dir}/Qt/bridge/Qt_Presentation_Sink.cpp" + "${root_dir}/Qt/plot/Plot.cpp" + "${root_dir}/Qt/plot/Plot.h" + "${root_dir}/Qt/plot/Latency_Eager_Plot.cpp" + "${root_dir}/Qt/plot/Latency_Eager_Plot.h" + "${root_dir}/Qt/plot/Explicit_Plot.cpp" + "${root_dir}/Qt/plot/Explicit_Plot.h" +) +add_library(Renderive_Qt STATIC ${renderive_qt_sources}) set_property(TARGET Renderive_Qt PROPERTY AUTOMOC ON) -target_link_libraries(Renderive_Qt PUBLIC Renderive_Core) -target_link_libraries(Renderive_Qt PUBLIC ${qt_libraries}) -# 纯粹的Qt组件库 -append_glob_source(Psc_Widget_src ${root_dir}/Widget) +target_include_directories(Renderive_Qt PUBLIC "${root_dir}") +target_compile_features(Renderive_Qt PUBLIC cxx_std_20) +target_link_libraries(Renderive_Qt PUBLIC Renderive_Core2 ${qt_libraries}) + +unset(Psc_Widget_src) +append_glob_source(Psc_Widget_src "${root_dir}/Widget") add_library(Psc_Widget STATIC ${Psc_Widget_src}) set_property(TARGET Psc_Widget PROPERTY AUTOMOC ON) set_property(TARGET Psc_Widget PROPERTY AUTORCC ON) -target_link_libraries(Psc_Widget PUBLIC ${qt_libraries}) -target_link_libraries(Psc_Widget PUBLIC Psc_Core_Static) -# Demo 画廊 -#append_glob_source(Demo_Gallery_src ${root_dir}/Demo_Gallery) -#add_executable(Demo_Gallery ${Demo_Gallery_src}) -#target_link_libraries(Demo_Gallery PUBLIC Renderive_Qt Psc_Widget) +target_link_libraries(Psc_Widget PUBLIC ${qt_libraries} Psc_Core_Static) + +enable_testing() +file(GLOB_RECURSE renderive_kernel_test_sources CONFIGURE_DEPENDS + "${root_dir}/Kernel/tests/renderive/*.cpp" +) +add_executable(Renderive_Kernel_Tests EXCLUDE_FROM_ALL + "${root_dir}/Kernel/tests/main.cpp" + ${renderive_kernel_test_sources} +) +target_compile_definitions(Renderive_Kernel_Tests PRIVATE RENDERIVE_WITH_GTEST) +target_link_libraries(Renderive_Kernel_Tests PRIVATE Renderive_Kernel GTest::GTest) +add_test(NAME Renderive_Kernel_Tests COMMAND Renderive_Kernel_Tests) + +add_executable(Renderive_Core2_Tests EXCLUDE_FROM_ALL + "${root_dir}/Kernel/tests/main.cpp" + "${root_dir}/Core2/tests/Core2_Integration_Tests.cpp" +) +target_link_libraries(Renderive_Core2_Tests PRIVATE + Renderive_Core2 + GTest::GTest + blend2d::blend2d +) +add_test(NAME Renderive_Core2_Tests COMMAND Renderive_Core2_Tests) + +add_executable(Renderive_Qt_Tests EXCLUDE_FROM_ALL + "${root_dir}/Core2/tests/Qt_Bridge_Tests.cpp" +) +target_link_libraries(Renderive_Qt_Tests PRIVATE Renderive_Qt GTest::GTest) +get_filename_component(renderive_qt_runtime_root "${Qt5_DIR}/../../.." ABSOLUTE) +add_test(NAME Renderive_Qt_Tests + COMMAND "${CMAKE_COMMAND}" -E env + "PATH=${renderive_qt_runtime_root}/bin\;$ENV{PATH}" + "QT_QPA_PLATFORM=offscreen" + "QT_QPA_PLATFORM_PLUGIN_PATH=${renderive_qt_runtime_root}/plugins/platforms" + "$" +) + +add_executable(Renderive_Core_Tests EXCLUDE_FROM_ALL + "${root_dir}/test/Renderive_Core_Tests.cpp" +) +target_link_libraries(Renderive_Core_Tests PRIVATE Renderive_Core GTest::GTest) + +if (MSVC) + target_compile_options(Renderive_Kernel PRIVATE /utf-8) + target_compile_options(Renderive_Core2 PRIVATE /utf-8) + target_compile_options(Renderive_Qt PRIVATE /utf-8) +endif ()