三维频谱做好

This commit is contained in:
2026-08-22 20:08:57 +08:00
parent e2ba9b8384
commit cd028e8d4f
22 changed files with 1352 additions and 144 deletions
+145
View File
@@ -0,0 +1,145 @@
#include "Axis.hpp"
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <limits>
#include <sstream>
#include <stdexcept>
namespace aethera::plot {
namespace {
std::string trim_number(std::string value) {
const auto exponent = value.find_first_of("eE");
const auto fraction_end = exponent == std::string::npos ? value.size() : exponent;
const auto decimal = value.find('.');
if (decimal == std::string::npos || decimal >= fraction_end) return value;
auto last = fraction_end;
while (last > decimal + 1 && value[last - 1] == '0') --last;
if (last == decimal + 1) --last;
value.erase(last, fraction_end - last);
return value;
}
std::string numeric_label(double value, int precision) {
std::ostringstream stream;
const auto magnitude = std::abs(value);
std::string suffix;
if (magnitude >= 1.0e3 && magnitude < 1.0e6) {
value /= 1.0e3;
suffix = "k";
stream << std::fixed;
} else if ((magnitude >= 1.0e6 || (magnitude > 0.0 && magnitude < 1.0e-4)))
stream << std::scientific;
else
stream << std::fixed;
stream << std::setprecision(std::clamp(precision, 0, 12)) << value;
auto result = trim_number(stream.str()) + suffix;
return result;
}
std::vector<Axis_Tick> linear_ticks(const Axis_Descriptor& axis) {
const auto step = nice_tick_step(axis.range, axis.target_tick_count);
if (!(step > 0.0) || !std::isfinite(step)) return {};
const auto [minimum, maximum] = std::minmax(axis.range.origin, axis.range.target);
const auto first = std::ceil(minimum / step) * step;
std::vector<Axis_Tick> ticks;
const auto maximum_count = std::max<std::size_t>(2, axis.target_tick_count * 4 + 4);
for (std::size_t index = 0; index < maximum_count; ++index) {
const auto coordinate = first + static_cast<double>(index) * step;
if (coordinate > maximum + step * 1.0e-9) break;
ticks.push_back({coordinate, numeric_label(coordinate, axis.precision)});
}
if (axis.range.length() < 0.0) std::ranges::reverse(ticks);
return ticks;
}
std::vector<Axis_Tick> logarithmic_ticks(const Axis_Descriptor& axis) {
const auto [minimum, maximum] = std::minmax(axis.range.origin, axis.range.target);
if (!(minimum > 0.0) || !std::isfinite(maximum)) return {};
const auto first_power = static_cast<int>(std::floor(std::log10(minimum)));
const auto last_power = static_cast<int>(std::ceil(std::log10(maximum)));
const auto decade_count = static_cast<std::size_t>(last_power - first_power + 1);
const auto multiples = axis.target_tick_count <= decade_count
? std::vector{1.0}
: axis.target_tick_count <= decade_count * 2
? std::vector{1.0, 5.0}
: std::vector{1.0, 2.0, 5.0};
std::vector<Axis_Tick> ticks;
for (int power = first_power; power <= last_power; ++power) {
const auto decade = std::pow(10.0, static_cast<double>(power));
for (const double multiple : multiples) {
const auto coordinate = multiple * decade;
if (coordinate >= minimum * (1.0 - 1.0e-12) &&
coordinate <= maximum * (1.0 + 1.0e-12))
ticks.push_back({coordinate, numeric_label(coordinate, axis.precision)});
}
}
const auto add_boundary = [&](double coordinate) {
const auto tolerance = coordinate * 1.0e-9;
if (std::ranges::none_of(ticks, [&](const Axis_Tick& tick) {
return std::abs(tick.coordinate - coordinate) <= tolerance;
})) {
const auto logarithmic_span = std::log(maximum / minimum);
const auto minimum_spacing = 0.5 /
static_cast<double>(std::max<std::size_t>(2, axis.target_tick_count) - 1);
if (logarithmic_span > 0.0) {
const auto nearest = std::ranges::min_element(
ticks, {}, [&](const Axis_Tick& tick) {
return std::abs(std::log(tick.coordinate / coordinate)) /
logarithmic_span;
});
if (nearest != ticks.end() &&
std::abs(std::log(nearest->coordinate / coordinate)) /
logarithmic_span < minimum_spacing)
ticks.erase(nearest);
}
ticks.push_back({coordinate, numeric_label(coordinate, axis.precision)});
}
};
add_boundary(minimum);
add_boundary(maximum);
std::ranges::sort(ticks, {}, &Axis_Tick::coordinate);
if (axis.range.length() < 0.0) std::ranges::reverse(ticks);
return ticks;
}
} // namespace
Axis_Coordinate Axis_Range::size() const noexcept {
return std::abs(length());
}
Axis_Coordinate Axis_Range::length() const noexcept {
return target - origin;
}
Axis_Coordinate Axis_Range::center() const noexcept {
return origin + length() * 0.5;
}
bool Axis_Range::contains(Axis_Coordinate coordinate) const noexcept {
const auto [minimum, maximum] = std::minmax(origin, target);
return coordinate >= minimum - 1.0e-9 && coordinate <= maximum + 1.0e-9;
}
Axis_Coordinate nice_tick_step(Axis_Range range, std::size_t target_tick_count) {
if (!std::isfinite(range.origin) || !std::isfinite(range.target) || range.size() <= 0.0)
throw std::invalid_argument("axis range must be finite and non-empty");
const auto count = std::max<std::size_t>(2, target_tick_count);
const auto raw = range.size() / static_cast<double>(count - 1);
const auto magnitude = std::pow(10.0, std::floor(std::log10(raw)));
const auto normalized = raw / magnitude;
const auto nice = normalized <= 1.0 ? 1.0 : normalized <= 2.0 ? 2.0 :
normalized <= 5.0 ? 5.0 : 10.0;
return nice * magnitude;
}
std::vector<Axis_Tick> axis_ticks(const Axis_Descriptor& axis) {
if (!axis.visible) return {};
if (axis.scale == Axis_Scale::logarithmic) return logarithmic_ticks(axis);
return linear_ticks(axis);
}
} // namespace aethera::plot
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace aethera::plot {
using Axis_Coordinate = double;
enum class Axis_Scale : std::uint8_t {
linear,
logarithmic,
time
};
struct Axis_Range {
Axis_Coordinate origin{};
Axis_Coordinate target{};
[[nodiscard]] Axis_Coordinate size() const noexcept;
[[nodiscard]] Axis_Coordinate length() const noexcept;
[[nodiscard]] Axis_Coordinate center() const noexcept;
[[nodiscard]] bool contains(Axis_Coordinate coordinate) const noexcept;
bool operator==(const Axis_Range&) const = default;
};
struct Axis_Point {
Axis_Coordinate horizontal{};
Axis_Coordinate vertical{};
bool operator==(const Axis_Point&) const = default;
};
struct Axis_Rectangle {
Axis_Range horizontal{};
Axis_Range vertical{};
bool operator==(const Axis_Rectangle&) const = default;
};
struct Axis_Descriptor {
Axis_Range range{};
Axis_Scale scale{Axis_Scale::linear};
std::string label{};
std::string unit{};
std::size_t target_tick_count{6};
int precision{2};
bool visible{true};
bool grid_visible{true};
bool labels_visible{true};
bool operator==(const Axis_Descriptor&) const = default;
};
struct Axis_Tick {
Axis_Coordinate coordinate{};
std::string label{};
bool operator==(const Axis_Tick&) const = default;
};
[[nodiscard]] Axis_Coordinate nice_tick_step(Axis_Range range,
std::size_t target_tick_count = 6);
[[nodiscard]] std::vector<Axis_Tick> axis_ticks(const Axis_Descriptor& axis);
} // namespace aethera::plot
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include <cstdint>
namespace aethera::plot {
struct Spatial_Point {
double x{};
double y{};
double z{};
bool operator==(const Spatial_Point&) const = default;
};
enum class Camera_Projection : std::uint8_t {
perspective,
orthographic
};
struct Camera_View {
Spatial_Point eye{2.8, -3.2, 2.4};
Spatial_Point target{};
Spatial_Point up{0.0, 0.0, 1.0};
bool operator==(const Camera_View&) const = default;
};
struct Camera_Control {
double yaw_speed{0.20};
double pitch_speed{0.20};
double zoom_speed{0.10};
double pan_speed{0.002};
double minimum_pitch{-1.45};
double maximum_pitch{1.45};
double minimum_distance{0.25};
double maximum_distance{50.0};
bool rotate_enabled{true};
bool zoom_enabled{true};
bool pan_enabled{true};
bool operator==(const Camera_Control&) const = default;
};
struct Camera_Descriptor {
Camera_View initial_view{};
Camera_Projection projection{Camera_Projection::perspective};
Camera_Control control{};
double vertical_field_of_view_degrees{45.0};
double near_plane{0.01};
double far_plane{100.0};
bool operator==(const Camera_Descriptor&) const = default;
};
} // namespace aethera::plot
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include "Axis.hpp"
#include <cstddef>
#include <cstdint>
namespace aethera::plot {
struct Time_Domain {
Axis_Range seconds{};
std::size_t visible_sample_count{};
bool realtime{};
bool newest_at_start{};
bool operator==(const Time_Domain&) const = default;
};
} // namespace aethera::plot