785 lines
54 KiB
C++
785 lines
54 KiB
C++
#include "Gallery_Plots.hpp"
|
|
#include "Renderable_Adapter.hpp"
|
|
#include <render_3D/Render_3D.hpp>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <numbers>
|
|
#include <random>
|
|
#include <stdexcept>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace aethera::web {
|
|
namespace {
|
|
using namespace render_3d;
|
|
using Scene_3D = Impl<Render_Scene_3D>;
|
|
|
|
using Json = nlohmann::json;
|
|
|
|
Json number_field(std::string key, std::string label, std::string description,
|
|
double value, double minimum, double maximum, double step) {
|
|
return {{"key", std::move(key)}, {"label", std::move(label)}, {"description", std::move(description)},
|
|
{"editor", "number"}, {"editable", true}, {"value", value},
|
|
{"minimum", minimum}, {"maximum", maximum}, {"step", step}};
|
|
}
|
|
|
|
Json integer_field(std::string key, std::string label, std::string description,
|
|
std::size_t value, std::size_t minimum, std::size_t maximum) {
|
|
auto field = number_field(std::move(key), std::move(label), std::move(description),
|
|
static_cast<double>(value), static_cast<double>(minimum),
|
|
static_cast<double>(maximum), 1.0);
|
|
field["editor"] = "integer";
|
|
return field;
|
|
}
|
|
|
|
template <typename Definition>
|
|
Json generator_schema() {
|
|
Json fields = Json::array();
|
|
if constexpr (std::same_as<Definition, Volume_Visual>) {
|
|
fields.push_back(integer_field("width", "体数据宽度", "体素网格 X 方向尺寸;总量为宽×高×深。", 64, 1, 256));
|
|
fields.push_back(integer_field("height", "体数据高度", "体素网格 Y 方向尺寸;总量为宽×高×深。", 64, 1, 256));
|
|
fields.push_back(integer_field("depth", "体数据深度", "体素网格 Z 方向尺寸;总量为宽×高×深。", 64, 1, 256));
|
|
fields.push_back(number_field("value_min", "体素值下界", "每个体素随机标量值的下界。", 0.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(number_field("value_max", "体素值上界", "每个体素随机标量值的上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(integer_field("seed", "随机种子", "固定种子可复现体素压力数据,便于跨策略对比。", 42, 1, 4'294'967'295ULL));
|
|
return {{"label", "生成体素标量场"}, {"description", "按三维网格尺寸生成连续体数据,不使用随机位置。"}, {"fields", std::move(fields)}};
|
|
}
|
|
fields.push_back(integer_field("count", "图元数量", "本次替换到 Visual 的图元数量;用于逐级提升 CPU Prepare、GPU 上传与绘制压力。", 10'000, 1, 5'000'000));
|
|
fields.push_back(integer_field("seed", "随机种子", "固定种子可复现相同空间分布,确保多图与传输模式的性能结果可比较。", 42, 1, 4'294'967'295ULL));
|
|
fields.push_back(number_field("x_min", "Scene X 下界", "随机位置在 Scene X 轴上的下界。", -1.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(number_field("x_max", "Scene X 上界", "随机位置在 Scene X 轴上的上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(number_field("y_min", "Scene Y 下界", "随机位置在 Scene Y 轴上的下界。", -1.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(number_field("y_max", "Scene Y 上界", "随机位置在 Scene Y 轴上的上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(number_field("z_min", "Scene Z 下界", "随机位置在 Scene Z 轴上的下界。", -1.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(number_field("z_max", "Scene Z 上界", "随机位置在 Scene Z 轴上的上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0, 0.01));
|
|
std::string label{"生成三维图元"};
|
|
std::string description{"按各轴独立范围随机生成 Scene 坐标。"};
|
|
if constexpr (std::same_as<Definition, Point_Visual>) {
|
|
label = "生成三维点";
|
|
fields.push_back(number_field("diameter_min", "点直径下界", "随机点直径下界,单位为屏幕像素。", 2.0, 0.1, 4096.0, 0.1));
|
|
fields.push_back(number_field("diameter_max", "点直径上界", "随机点直径上界,单位为屏幕像素。", 12.0, 0.1, 4096.0, 0.1));
|
|
} else if constexpr (std::same_as<Definition, Splat_Visual>) {
|
|
label = "生成三维高斯 Splat";
|
|
fields.push_back(number_field("sigma_min", "标准差下界", "高斯主轴标准差下界,使用 Scene 坐标。", 0.01, 0.0001, 1000.0, 0.001));
|
|
fields.push_back(number_field("sigma_max", "标准差上界", "高斯主轴标准差上界,使用 Scene 坐标。", 0.08, 0.0001, 1000.0, 0.001));
|
|
fields.push_back(number_field("angle_min", "旋转角下界", "Splat 主轴旋转角下界,单位为弧度。", -3.14159, -1000.0, 1000.0, 0.01));
|
|
fields.push_back(number_field("angle_max", "旋转角上界", "Splat 主轴旋转角上界,单位为弧度。", 3.14159, -1000.0, 1000.0, 0.01));
|
|
} else if constexpr (std::same_as<Definition, Pixel_Visual>) {
|
|
label = "生成三维像素";
|
|
fields.push_back(number_field("size_min", "像素边长下界", "方形像素边长下界,单位为屏幕像素。", 1.0, 0.1, 4096.0, 0.1));
|
|
fields.push_back(number_field("size_max", "像素边长上界", "方形像素边长上界,单位为屏幕像素。", 6.0, 0.1, 4096.0, 0.1));
|
|
} else if constexpr (std::same_as<Definition, Marker_Visual>) {
|
|
label = "生成三维标记";
|
|
fields.push_back(number_field("diameter_min", "标记直径下界", "标记直径下界,单位为屏幕像素。", 4.0, 0.1, 4096.0, 0.1));
|
|
fields.push_back(number_field("diameter_max", "标记直径上界", "标记直径上界,单位为屏幕像素。", 18.0, 0.1, 4096.0, 0.1));
|
|
} else if constexpr (std::same_as<Definition, Sphere_Visual>) {
|
|
label = "生成三维球体";
|
|
fields.push_back(number_field("radius_min", "球体半径下界", "球体半径下界,使用 Scene 坐标。", 0.01, 0.0001, 1000.0, 0.001));
|
|
fields.push_back(number_field("radius_max", "球体半径上界", "球体半径上界,使用 Scene 坐标。", 0.08, 0.0001, 1000.0, 0.001));
|
|
} else if constexpr (std::same_as<Definition, Segment_Visual>) {
|
|
label = "生成三维线段"; description = "起点和终点分别在各轴范围内随机生成。";
|
|
fields.push_back(number_field("width_min", "线宽下界", "线段宽度下界,单位为屏幕像素。", 1.0, 0.1, 4096.0, 0.1));
|
|
fields.push_back(number_field("width_max", "线宽上界", "线段宽度上界,单位为屏幕像素。", 5.0, 0.1, 4096.0, 0.1));
|
|
} else if constexpr (std::same_as<Definition, Vector_Visual>) {
|
|
label = "生成三维向量"; description = "原点按 Scene 范围随机生成,方向分量使用单独范围。";
|
|
fields.push_back(number_field("direction_min", "方向分量下界", "向量 X/Y/Z 方向分量的随机下界。", -0.3, -1'000'000.0, 1'000'000.0, 0.01));
|
|
fields.push_back(number_field("direction_max", "方向分量上界", "向量 X/Y/Z 方向分量的随机上界。", 0.3, -1'000'000.0, 1'000'000.0, 0.01));
|
|
} else if constexpr (std::same_as<Definition, Primitive_Visual>) label = "生成三维图元顶点";
|
|
else if constexpr (std::same_as<Definition, Mesh_Visual>) label = "生成三维网格顶点";
|
|
else if constexpr (std::same_as<Definition, Path_Visual>) {
|
|
label = "生成三维路径顶点";
|
|
fields.push_back(number_field("width_min", "路径宽度下界", "路径宽度下界,单位为屏幕像素。", 1.0, 0.1, 4096.0, 0.1));
|
|
fields.push_back(number_field("width_max", "路径宽度上界", "路径宽度上界,单位为屏幕像素。", 5.0, 0.1, 4096.0, 0.1));
|
|
} else if constexpr (std::same_as<Definition, Image_Visual>) {
|
|
label = "生成三维图像实例";
|
|
fields.push_back(number_field("extent_min", "图像尺寸下界", "图像宽高的 Scene 坐标下界。", 0.02, 0.0001, 1000.0, 0.001));
|
|
fields.push_back(number_field("extent_max", "图像尺寸上界", "图像宽高的 Scene 坐标上界。", 0.2, 0.0001, 1000.0, 0.001));
|
|
} else if constexpr (std::same_as<Definition, Labels_Visual>) {
|
|
label = "生成三维标签实例";
|
|
fields.push_back(number_field("extent_min", "标签尺寸下界", "标签宽高的屏幕像素下界。", 8.0, 0.1, 4096.0, 0.1));
|
|
fields.push_back(number_field("extent_max", "标签尺寸上界", "标签宽高的屏幕像素上界。", 48.0, 0.1, 4096.0, 0.1));
|
|
} else if constexpr (std::same_as<Definition, Glyph_Visual>) {
|
|
label = "生成三维字形实例";
|
|
fields.push_back(number_field("angle_min", "字形旋转下界", "字形旋转角下界,单位为弧度。", -3.14159, -1000.0, 1000.0, 0.01));
|
|
fields.push_back(number_field("angle_max", "字形旋转上界", "字形旋转角上界,单位为弧度。", 3.14159, -1000.0, 1000.0, 0.01));
|
|
} else if constexpr (std::same_as<Definition, Text_Visual>) {
|
|
label = "生成三维文本实例";
|
|
fields.push_back(number_field("size_min", "字号下界", "随机文本字号下界,单位为屏幕像素。", 10.0, 0.1, 4096.0, 0.1));
|
|
fields.push_back(number_field("size_max", "字号上界", "随机文本字号上界,单位为屏幕像素。", 28.0, 0.1, 4096.0, 0.1));
|
|
}
|
|
return {{"label", std::move(label)}, {"description", std::move(description)}, {"fields", std::move(fields)}};
|
|
}
|
|
|
|
double input_number(const Json& input, std::string_view key) {
|
|
const auto& value = input.at(key);
|
|
if (!value.is_number()) throw std::invalid_argument(std::string(key) + " must be a number");
|
|
const auto result = value.get<double>();
|
|
if (!std::isfinite(result)) throw std::invalid_argument(std::string(key) + " must be finite");
|
|
return result;
|
|
}
|
|
|
|
std::size_t input_count(const Json& input, std::string_view key, std::size_t maximum = 1'000'000) {
|
|
const auto value = input_number(input, key);
|
|
if (value < 1.0 || value > static_cast<double>(maximum) || std::floor(value) != value)
|
|
throw std::invalid_argument(std::string(key) + " is outside the supported integer range");
|
|
return static_cast<std::size_t>(value);
|
|
}
|
|
|
|
std::pair<float, float> input_range(const Json& input, std::string_view minimum_key,
|
|
std::string_view maximum_key) {
|
|
const auto minimum = input_number(input, minimum_key);
|
|
const auto maximum = input_number(input, maximum_key);
|
|
if (minimum >= maximum) throw std::invalid_argument(std::string(maximum_key) + " must be greater than " + std::string(minimum_key));
|
|
if (minimum < -std::numeric_limits<float>::max() || maximum > std::numeric_limits<float>::max())
|
|
throw std::invalid_argument("generator range exceeds float coordinates");
|
|
return {static_cast<float>(minimum), static_cast<float>(maximum)};
|
|
}
|
|
|
|
template <typename Definition>
|
|
class Item_Randomizer {
|
|
public:
|
|
explicit Item_Randomizer(const Json& input) {
|
|
const auto [x_min, x_max] = input_range(input, "x_min", "x_max");
|
|
const auto [y_min, y_max] = input_range(input, "y_min", "y_max");
|
|
const auto [z_min, z_max] = input_range(input, "z_min", "z_max");
|
|
x = std::uniform_real_distribution<float>(x_min, x_max);
|
|
y = std::uniform_real_distribution<float>(y_min, y_max);
|
|
z = std::uniform_real_distribution<float>(z_min, z_max);
|
|
const auto set_first = [&](std::string_view minimum, std::string_view maximum) {
|
|
const auto [lower, upper] = input_range(input, minimum, maximum);
|
|
first = std::uniform_real_distribution<float>(lower, upper);
|
|
};
|
|
const auto set_second = [&](std::string_view minimum, std::string_view maximum) {
|
|
const auto [lower, upper] = input_range(input, minimum, maximum);
|
|
second = std::uniform_real_distribution<float>(lower, upper);
|
|
};
|
|
if constexpr (std::same_as<Definition, Point_Visual> || std::same_as<Definition, Marker_Visual>) set_first("diameter_min", "diameter_max");
|
|
else if constexpr (std::same_as<Definition, Splat_Visual>) { set_first("sigma_min", "sigma_max"); set_second("angle_min", "angle_max"); }
|
|
else if constexpr (std::same_as<Definition, Pixel_Visual> || std::same_as<Definition, Text_Visual>) set_first("size_min", "size_max");
|
|
else if constexpr (std::same_as<Definition, Sphere_Visual>) set_first("radius_min", "radius_max");
|
|
else if constexpr (std::same_as<Definition, Segment_Visual> || std::same_as<Definition, Path_Visual>) set_first("width_min", "width_max");
|
|
else if constexpr (std::same_as<Definition, Vector_Visual>) set_first("direction_min", "direction_max");
|
|
else if constexpr (std::same_as<Definition, Image_Visual> || std::same_as<Definition, Labels_Visual>) set_first("extent_min", "extent_max");
|
|
else if constexpr (std::same_as<Definition, Glyph_Visual>) set_first("angle_min", "angle_max");
|
|
}
|
|
void operator()(typename Definition::Item& item, std::mt19937_64& engine) {
|
|
const auto vector = [&] { return Vec3{x(engine), y(engine), z(engine)}; };
|
|
if constexpr (requires { item.position = vector(); }) item.position = vector();
|
|
else if constexpr (requires { item.center = vector(); }) item.center = vector();
|
|
else if constexpr (requires { item.origin = vector(); }) item.origin = vector();
|
|
else if constexpr (requires { item.start = vector(); item.end = vector(); }) { item.start = vector(); item.end = vector(); }
|
|
if constexpr (std::same_as<Definition, Point_Visual>) item.diameter_px = first(engine);
|
|
else if constexpr (std::same_as<Definition, Splat_Visual>) {
|
|
item.sigma = {first(engine), first(engine)};
|
|
item.angle = second(engine);
|
|
} else if constexpr (std::same_as<Definition, Pixel_Visual>) item.size_px = first(engine);
|
|
else if constexpr (std::same_as<Definition, Marker_Visual>) item.diameter_px = first(engine);
|
|
else if constexpr (std::same_as<Definition, Sphere_Visual>) item.radius = first(engine);
|
|
else if constexpr (std::same_as<Definition, Segment_Visual>) item.width_px = first(engine);
|
|
else if constexpr (std::same_as<Definition, Vector_Visual>) {
|
|
item.direction = {first(engine), first(engine), first(engine)};
|
|
} else if constexpr (std::same_as<Definition, Path_Visual>) item.width_px = first(engine);
|
|
else if constexpr (std::same_as<Definition, Image_Visual> || std::same_as<Definition, Labels_Visual>)
|
|
item.extent = {first(engine), first(engine)};
|
|
else if constexpr (std::same_as<Definition, Glyph_Visual>) item.angle = first(engine);
|
|
else if constexpr (std::same_as<Definition, Text_Visual>) item.size_px = first(engine);
|
|
}
|
|
private:
|
|
std::uniform_real_distribution<float> x{};
|
|
std::uniform_real_distribution<float> y{};
|
|
std::uniform_real_distribution<float> z{};
|
|
std::uniform_real_distribution<float> first{};
|
|
std::uniform_real_distribution<float> second{};
|
|
};
|
|
|
|
struct Random_Data_Generator {};
|
|
|
|
template <typename Visual_Object, typename Data_Generator = Random_Data_Generator>
|
|
class Visual_Scene_View final : public Plot::Scene_View {
|
|
public:
|
|
using Camera_Object = Impl<Camera_3D>;
|
|
using Axes_Object = Impl<Axes_3D>;
|
|
using Marker_Object = Impl<Marker_Visual>;
|
|
Visual_Scene_View(Scene_3D& scene, std::unique_ptr<Camera_Object> camera,
|
|
std::unique_ptr<Axes_Object> axes,
|
|
std::unique_ptr<Visual_Object> visual, std::string label,
|
|
Data_Generator data_generator = {},
|
|
std::unique_ptr<Marker_Object> markers = {})
|
|
: data_generator_(std::move(data_generator)), camera_(std::move(camera)),
|
|
axes_(std::move(axes)), visual_(std::move(visual)), markers_(std::move(markers)) {
|
|
using Definition = typename Visual_Object::Attached_Object;
|
|
using Prop = typename Definition::Prop;
|
|
using State = typename Definition::State;
|
|
using Scene_Adapter = detail::Renderable_Adapter<Scene_3D,
|
|
detail::Prop_Field<&Render_Scene_3D::Prop::clear_color, "clear_color", "Linear scene clear color.">,
|
|
detail::Prop_Field<&Render_Scene_3D::Prop::view_active, "view_active", "Whether the scene publishes rendered frames.">,
|
|
detail::State_Field<Scene::Base_Tag, &Scene::State::renderable_count, "renderable_count", "Number of renderables attached to the scene.">,
|
|
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_task_count, "taskflow_task_count", "Number of tasks in the scene graph.">,
|
|
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_execution_time_ns, "taskflow_execution_time_ns", "Scene graph execution time in nanoseconds.">>;
|
|
using Visual_Adapter = detail::Renderable_Adapter<Visual_Object,
|
|
detail::Prop_Field<&Prop::transform, "transform", "World transform applied to the complete visual.">,
|
|
detail::Prop_Field<&Prop::visible, "visible", "Whether the visual participates in rendering.">,
|
|
detail::Prop_Field<&Prop::depth_test, "depth_test", "Whether fragments use depth testing.">,
|
|
detail::State_Field<Definition::Base_Tag, &State::item_count, "item_count", "Number of published input items.">,
|
|
detail::State_Field<Definition::Base_Tag, &State::prepared_item_count, "prepared_item_count", "Number of prepared backend items.">,
|
|
detail::State_Field<Definition::Base_Tag, &State::prepared_revision, "prepared_revision", "Property revision represented by prepared GPU data.">>;
|
|
using Camera_Adapter = detail::Renderable_Adapter<Camera_Object,
|
|
detail::Prop_Field<&Camera_3D::Prop::initial_view, "initial_view", "Initial eye, target and world-up vectors used by reset view.">,
|
|
detail::Prop_Field<&Camera_3D::Prop::projection, "projection", "Perspective or orthographic camera projection.">,
|
|
detail::Prop_Field<&Camera_3D::Prop::controller, "controller", "Datoviz native camera controller: turntable, arcball, fly or panzoom.">,
|
|
detail::Prop_Field<&Camera_3D::Prop::turntable_control, "turntable_control", "Turntable orbit, zoom and pan speeds and limits.">,
|
|
detail::Prop_Field<&Camera_3D::Prop::arcball_control, "arcball_control", "Arcball free rotation and optional constraint axis.">,
|
|
detail::Prop_Field<&Camera_3D::Prop::fly_control, "fly_control", "Fly camera movement mode, keyboard speed and pointer look settings.">,
|
|
detail::Prop_Field<&Camera_3D::Prop::panzoom_control, "panzoom_control", "Planar panzoom axis locks and aspect-ratio policy.">,
|
|
detail::Prop_Field<&Camera_3D::Prop::vertical_field_of_view_degrees, "vertical_field_of_view_degrees", "Vertical field of view in degrees.">,
|
|
detail::Prop_Field<&Camera_3D::Prop::near_plane, "near_plane", "Nearest visible camera distance.">,
|
|
detail::Prop_Field<&Camera_3D::Prop::far_plane, "far_plane", "Farthest visible camera distance.">>;
|
|
using Axes_Adapter = detail::Renderable_Adapter<Axes_Object,
|
|
detail::Prop_Field<&Axes_3D::Prop::x_axis, "x_axis", "X axis range, scale, ticks, label and unit.">,
|
|
detail::Prop_Field<&Axes_3D::Prop::y_axis, "y_axis", "Y axis range, scale, ticks, label and unit.">,
|
|
detail::Prop_Field<&Axes_3D::Prop::z_axis, "z_axis", "Z axis range, scale, ticks, label and unit.">>;
|
|
descriptors_.push_back(detail::make_renderable_descriptor("scene", "3D 场景", "scene", Scene_Adapter{scene}));
|
|
descriptors_.push_back(detail::make_renderable_descriptor("camera", "相机控制", "camera", Camera_Adapter{*camera_}));
|
|
descriptors_.push_back(detail::make_renderable_descriptor("axes", "三维坐标轴", "axes", Axes_Adapter{*axes_}));
|
|
if constexpr (std::same_as<Definition, Marker_Visual>) {
|
|
using Marker_Adapter = detail::Renderable_Adapter<Visual_Object,
|
|
detail::Prop_Field<&Prop::items, "items", "Marker collection; editing the collection adds, moves or removes actual Scene markers.">,
|
|
detail::Prop_Field<&Prop::transform, "transform", "World transform applied to the complete marker collection.">,
|
|
detail::Prop_Field<&Prop::visible, "visible", "Whether markers participate in rendering.">,
|
|
detail::Prop_Field<&Prop::depth_test, "depth_test", "Whether marker fragments use depth testing.">,
|
|
detail::State_Field<Definition::Base_Tag, &State::item_count, "item_count", "Number of markers in the Scene.">,
|
|
detail::State_Field<Definition::Base_Tag, &State::prepared_item_count, "prepared_item_count", "Number of prepared marker items.">,
|
|
detail::State_Field<Definition::Base_Tag, &State::prepared_revision, "prepared_revision", "Marker GPU data revision.">>;
|
|
descriptors_.push_back(detail::make_renderable_descriptor(
|
|
"visual", std::move(label), "marker", Marker_Adapter{*visual_}));
|
|
} else {
|
|
descriptors_.push_back(detail::make_renderable_descriptor(
|
|
"visual", std::move(label), "visual", Visual_Adapter{*visual_}));
|
|
}
|
|
if (markers_) {
|
|
using Marker_Prop = Marker_Visual::Prop;
|
|
using Marker_State = Marker_Visual::State;
|
|
using Marker_Adapter = detail::Renderable_Adapter<Marker_Object,
|
|
detail::Prop_Field<&Marker_Prop::items, "items", "Surface marker anchors; X and Y are editable while Z is derived from the current spectrogram surface.", "surface-marker-list">,
|
|
detail::Prop_Field<&Marker_Prop::visible, "visible", "Whether the spectrogram marker layer is visible.">,
|
|
detail::Prop_Field<&Marker_Prop::depth_test, "depth_test", "Whether markers may be occluded by the surface.">,
|
|
detail::State_Field<Marker_Visual::Base_Tag, &Marker_State::item_count, "item_count", "Number of spectrogram markers.">,
|
|
detail::State_Field<Marker_Visual::Base_Tag, &Marker_State::prepared_item_count, "prepared_item_count", "Number of markers prepared for Datoviz.">,
|
|
detail::State_Field<Marker_Visual::Base_Tag, &Marker_State::prepared_revision, "prepared_revision", "Marker GPU data revision.">>;
|
|
descriptors_.push_back(detail::make_renderable_descriptor(
|
|
"markers", "频谱标记", "marker", Marker_Adapter{*markers_}));
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] nlohmann::json schema() const override {
|
|
nlohmann::json components = nlohmann::json::array();
|
|
for (const auto& descriptor : descriptors_) components.push_back(descriptor->schema());
|
|
return {{"protocol", "aethera.plot.inspector"}, {"version", 2}, {"components", std::move(components)}};
|
|
}
|
|
|
|
[[nodiscard]] nlohmann::json write_prop(std::string_view component, std::string_view key, const nlohmann::json& value) override {
|
|
const auto found = std::ranges::find_if(descriptors_, [&](const auto& descriptor) { return descriptor->id() == component; });
|
|
if (found == descriptors_.end()) return {{"success", false}, {"error", "unknown component"}};
|
|
auto result = (*found)->write_prop(key, value);
|
|
result["component"] = component;
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] nlohmann::json data_generator_schema() const override {
|
|
if constexpr (std::same_as<Data_Generator, Random_Data_Generator>) {
|
|
using Definition = typename Visual_Object::Attached_Object;
|
|
return generator_schema<Definition>();
|
|
} else {
|
|
return data_generator_.schema();
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] nlohmann::json generate_data(const nlohmann::json& input) override {
|
|
if constexpr (!std::same_as<Data_Generator, Random_Data_Generator>) {
|
|
return data_generator_.generate(*visual_, *axes_, input);
|
|
} else {
|
|
using Definition = typename Visual_Object::Attached_Object;
|
|
using Prop = typename Definition::Prop;
|
|
using Items = std::remove_cvref_t<decltype(std::declval<Prop>().items)>;
|
|
try {
|
|
std::mt19937_64 engine{input_count(input, "seed", 4'294'967'295ULL)};
|
|
Items generated;
|
|
std::size_t count{};
|
|
if constexpr (std::same_as<Definition, Volume_Visual>) {
|
|
const auto width = input_count(input, "width", 256);
|
|
const auto height = input_count(input, "height", 256);
|
|
const auto depth = input_count(input, "depth", 256);
|
|
if (width > 1'000'000 / height || width * height > 1'000'000 / depth)
|
|
throw std::invalid_argument("volume dimensions exceed 1,000,000 voxels");
|
|
count = width * height * depth;
|
|
const auto [minimum, maximum] = input_range(input, "value_min", "value_max");
|
|
std::uniform_real_distribution<float> distribution(minimum, maximum);
|
|
generated.resize(count);
|
|
for (auto& item : generated) item.value = distribution(engine);
|
|
visual_->template update_prop<&Prop::items>([&](auto props) {
|
|
auto& prop = props.template get<typename Definition::Base_Tag>();
|
|
prop.field_width = static_cast<std::uint32_t>(width);
|
|
prop.field_height = static_cast<std::uint32_t>(height);
|
|
prop.field_depth = static_cast<std::uint32_t>(depth);
|
|
});
|
|
} else {
|
|
count = input_count(input, "count", 5'000'000);
|
|
const auto& current = visual_->template read_prop<typename Definition::Base_Tag>().items;
|
|
if (current.empty()) throw std::invalid_argument("visual has no item template");
|
|
const auto prototype = current.front();
|
|
Item_Randomizer<Definition> randomize(input);
|
|
generated.reserve(count);
|
|
for (std::size_t index = 0; index < count; ++index) {
|
|
auto item = prototype;
|
|
randomize(item, engine);
|
|
generated.push_back(std::move(item));
|
|
}
|
|
}
|
|
if (visual_->update_items(std::move(generated)) != Definition::Update_Items_Result::updated)
|
|
throw std::invalid_argument("generated items were rejected by the Visual validator");
|
|
return {{"success", true}, {"generated_count", count}};
|
|
} catch (const std::exception& error) {
|
|
return {{"success", false}, {"error", error.what()}};
|
|
}
|
|
}
|
|
}
|
|
|
|
void update(const Plot_Frame_Request& request) override {
|
|
if constexpr (requires(Data_Generator& generator, Visual_Object& visual,
|
|
Axes_Object& axes, Marker_Object* markers,
|
|
const Plot_Frame_Request& frame) {
|
|
generator.update(visual, axes, markers, frame);
|
|
}) {
|
|
data_generator_.update(*visual_, *axes_, markers_.get(), request);
|
|
}
|
|
}
|
|
|
|
private:
|
|
[[no_unique_address]] Data_Generator data_generator_; /* Plot 业务数据生成策略。 */
|
|
std::unique_ptr<Camera_Object> camera_; /* Scene 引用的 Camera 唯一所有者。 */
|
|
std::unique_ptr<Axes_Object> axes_; /* Scene 引用的 Axes 唯一所有者。 */
|
|
std::unique_ptr<Visual_Object> visual_; /* Scene 引用的 Visual 唯一所有者。 */
|
|
std::unique_ptr<Marker_Object> markers_; /* 可选的独立 Marker Visual 所有者。 */
|
|
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> descriptors_; /* Prop/State 协议描述。 */
|
|
};
|
|
|
|
struct Scene_Components_3D {
|
|
plot::Camera_Descriptor camera{};
|
|
std::array<plot::Axis_Descriptor, 3> axes{
|
|
plot::Axis_Descriptor{{-1.0, 1.0}, plot::Axis_Scale::linear, "X", "", 5, 2, true, true, true},
|
|
plot::Axis_Descriptor{{-1.0, 1.0}, plot::Axis_Scale::linear, "Y", "", 5, 2, true, true, true},
|
|
plot::Axis_Descriptor{{-1.0, 1.0}, plot::Axis_Scale::linear, "Z", "", 5, 2, true, true, true}};
|
|
};
|
|
|
|
template <typename Definition, typename Build_Result,
|
|
typename Data_Generator = Random_Data_Generator>
|
|
std::shared_ptr<Plot> make_visual_plot(asio::any_io_executor executor, std::string label,
|
|
Build_Result build_result,
|
|
Scene_Components_3D components = {},
|
|
Data_Generator data_generator = {},
|
|
std::unique_ptr<Impl<Marker_Visual>> markers = {}) {
|
|
using Visual_Object = Impl<Definition>;
|
|
using Camera_Object = Impl<Camera_3D>;
|
|
using Axes_Object = Impl<Axes_3D>;
|
|
if (!build_result) throw std::logic_error("3D Gallery visual dependency graph is invalid");
|
|
auto visual = std::move(build_result).value();
|
|
auto camera_result = Camera_Object::Builder{}
|
|
.set(&Camera_3D::Prop::initial_view, components.camera.initial_view)
|
|
.set(&Camera_3D::Prop::projection, components.camera.projection)
|
|
.set(&Camera_3D::Prop::controller, components.camera.controller)
|
|
.set(&Camera_3D::Prop::turntable_control, components.camera.turntable_control)
|
|
.set(&Camera_3D::Prop::arcball_control, components.camera.arcball_control)
|
|
.set(&Camera_3D::Prop::fly_control, components.camera.fly_control)
|
|
.set(&Camera_3D::Prop::panzoom_control, components.camera.panzoom_control)
|
|
.set(&Camera_3D::Prop::vertical_field_of_view_degrees,
|
|
components.camera.vertical_field_of_view_degrees)
|
|
.set(&Camera_3D::Prop::near_plane, components.camera.near_plane)
|
|
.set(&Camera_3D::Prop::far_plane, components.camera.far_plane)
|
|
.build();
|
|
auto axes_result = Axes_Object::Builder{}
|
|
.set(&Axes_3D::Prop::x_axis, components.axes[0])
|
|
.set(&Axes_3D::Prop::y_axis, components.axes[1])
|
|
.set(&Axes_3D::Prop::z_axis, components.axes[2])
|
|
.build();
|
|
if (!camera_result || !axes_result)
|
|
throw std::logic_error("3D Gallery component construction failed");
|
|
auto camera = std::move(camera_result).value();
|
|
auto axes = std::move(axes_result).value();
|
|
auto scene_builder = Scene_3D::Builder{};
|
|
scene_builder.add_camera(camera.get())
|
|
.add_axes(axes.get())
|
|
.add_renderable(visual.get())
|
|
.set(&Render_Scene_3D::Prop::viewport, Extent{720, 420})
|
|
.set(&Render_Scene_3D::Prop::clear_color, Linear_Color{0.018F, 0.027F, 0.047F, 1.0F})
|
|
.set(&Render_Scene_3D::Prop::view_active, true);
|
|
if (markers) scene_builder.add_renderable(markers.get());
|
|
auto scene_result = scene_builder.build();
|
|
if (!scene_result) throw std::logic_error("3D Gallery scene dependency graph is invalid");
|
|
auto scene = std::move(scene_result).value();
|
|
auto view = std::make_unique<Visual_Scene_View<Visual_Object, Data_Generator>>(
|
|
*scene, std::move(camera), std::move(axes), std::move(visual),
|
|
std::move(label), std::move(data_generator), std::move(markers));
|
|
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
|
}
|
|
|
|
Color color(std::uint8_t red, std::uint8_t green, std::uint8_t blue, std::uint8_t alpha = 255) {
|
|
return {red, green, blue, alpha};
|
|
}
|
|
|
|
struct Spectrogram_Parameters {
|
|
std::size_t time_sample_count{80};
|
|
std::size_t frequency_bin_count{96};
|
|
std::size_t ridge_count{5};
|
|
double time_span_seconds{4.0};
|
|
double minimum_frequency_hz{10.0};
|
|
double maximum_frequency_hz{20'000.0};
|
|
double minimum_level_db{18.0};
|
|
double maximum_level_db{78.0};
|
|
double animation_speed{1.0};
|
|
std::size_t update_every_n_frames{1};
|
|
bool animation_enabled{true};
|
|
};
|
|
|
|
Color spectrogram_color(float value) {
|
|
struct Stop { float position; std::array<float, 3> rgb; };
|
|
static constexpr std::array stops{
|
|
Stop{0.0F, {22, 10, 54}}, Stop{0.22F, {76, 18, 112}},
|
|
Stop{0.45F, {151, 40, 103}}, Stop{0.68F, {226, 83, 61}},
|
|
Stop{0.86F, {252, 169, 52}}, Stop{1.0F, {252, 246, 164}}};
|
|
value = std::clamp(value, 0.0F, 1.0F);
|
|
for (std::size_t index = 1; index < stops.size(); ++index) {
|
|
if (value > stops[index].position) continue;
|
|
const auto& lower = stops[index - 1];
|
|
const auto& upper = stops[index];
|
|
const auto ratio = (value - lower.position) /
|
|
(upper.position - lower.position);
|
|
const auto channel = [&](std::size_t component) {
|
|
return static_cast<std::uint8_t>(std::lround(
|
|
std::lerp(lower.rgb[component], upper.rgb[component], ratio)));
|
|
};
|
|
return color(channel(0), channel(1), channel(2));
|
|
}
|
|
return color(252, 246, 164);
|
|
}
|
|
|
|
Vec3 face_normal(Vec3 first, Vec3 second, Vec3 third) {
|
|
const Vec3 a{second.x - first.x, second.y - first.y, second.z - first.z};
|
|
const Vec3 b{third.x - first.x, third.y - first.y, third.z - first.z};
|
|
Vec3 result{a.y * b.z - a.z * b.y,
|
|
a.z * b.x - a.x * b.z,
|
|
a.x * b.y - a.y * b.x};
|
|
const auto length = std::sqrt(result.x * result.x + result.y * result.y +
|
|
result.z * result.z);
|
|
if (!(length > 0.0F)) return {0, 0, 1};
|
|
result.x /= length;
|
|
result.y /= length;
|
|
result.z /= length;
|
|
return result;
|
|
}
|
|
|
|
float spectrogram_level(float time, float frequency, double animation_seconds,
|
|
std::size_t ridge_count) {
|
|
struct Ridge { float center; float width; float phase; float speed; float strength; };
|
|
static constexpr std::array ridges{
|
|
Ridge{0.12F, 0.035F, 0.20F, 0.42F, 0.72F},
|
|
Ridge{0.28F, 0.060F, 1.45F, 0.67F, 0.58F},
|
|
Ridge{0.47F, 0.045F, 2.70F, 0.53F, 0.82F},
|
|
Ridge{0.68F, 0.075F, 4.10F, 0.31F, 0.52F},
|
|
Ridge{0.84F, 0.030F, 5.20F, 0.78F, 0.68F},
|
|
Ridge{0.57F, 0.022F, 0.90F, 1.05F, 0.44F},
|
|
Ridge{0.36F, 0.028F, 3.60F, 0.91F, 0.40F},
|
|
Ridge{0.76F, 0.050F, 2.10F, 0.58F, 0.46F},
|
|
Ridge{0.20F, 0.024F, 4.80F, 0.72F, 0.38F},
|
|
Ridge{0.92F, 0.020F, 1.90F, 0.84F, 0.36F},
|
|
Ridge{0.52F, 0.090F, 5.70F, 0.27F, 0.32F},
|
|
Ridge{0.08F, 0.018F, 3.00F, 1.12F, 0.30F}};
|
|
const float animation = static_cast<float>(animation_seconds);
|
|
float level = 0.075F + 0.055F * std::sin(
|
|
2.0F * std::numbers::pi_v<float> *
|
|
(0.78F * time + 0.24F * frequency + 0.11F * animation));
|
|
const auto active_ridges = std::min(ridge_count, ridges.size());
|
|
for (std::size_t index = 0; index < active_ridges; ++index) {
|
|
const auto& ridge = ridges[index];
|
|
const auto moving_center = std::clamp(
|
|
ridge.center + 0.065F * std::sin(
|
|
ridge.phase + ridge.speed * animation + 1.3F * time),
|
|
0.015F, 0.985F);
|
|
const auto distance = (frequency - moving_center) / ridge.width;
|
|
const auto envelope = 0.52F + 0.48F * std::sin(
|
|
ridge.phase * 0.61F + 2.2F * time +
|
|
(ridge.speed + 0.18F) * animation);
|
|
level += ridge.strength * envelope * std::exp(-0.5F * distance * distance);
|
|
}
|
|
const float detail = 0.025F * std::sin(
|
|
31.0F * frequency + 8.0F * time + 1.7F * animation) +
|
|
0.018F * std::cos(
|
|
19.0F * frequency - 13.0F * time + animation);
|
|
return std::clamp(level + detail, 0.0F, 1.0F);
|
|
}
|
|
|
|
std::vector<Mesh_Vertex> spectrogram_mesh(const Spectrogram_Parameters& parameters,
|
|
double animation_seconds = 0.0) {
|
|
|
|
struct Sample { Vec3 position; Color color; };
|
|
std::vector<Sample> samples(parameters.time_sample_count *
|
|
parameters.frequency_bin_count);
|
|
for (std::size_t time_index = 0; time_index < parameters.time_sample_count;
|
|
++time_index) {
|
|
const auto time = static_cast<float>(time_index) /
|
|
static_cast<float>(parameters.time_sample_count - 1);
|
|
for (std::size_t frequency_index = 0;
|
|
frequency_index < parameters.frequency_bin_count; ++frequency_index) {
|
|
const auto frequency = static_cast<float>(frequency_index) /
|
|
static_cast<float>(parameters.frequency_bin_count - 1);
|
|
const float level = spectrogram_level(
|
|
time, frequency, animation_seconds, parameters.ridge_count);
|
|
samples[time_index * parameters.frequency_bin_count + frequency_index] = {
|
|
{-1.0F + 2.0F * time, -1.0F + 2.0F * frequency,
|
|
-1.0F + 2.0F * level}, spectrogram_color(level)};
|
|
}
|
|
}
|
|
|
|
std::vector<Mesh_Vertex> mesh;
|
|
mesh.reserve((parameters.time_sample_count - 1) *
|
|
(parameters.frequency_bin_count - 1) * 6);
|
|
const auto append_triangle = [&](const Sample& first, const Sample& second,
|
|
const Sample& third) {
|
|
const auto normal = face_normal(first.position, second.position, third.position);
|
|
mesh.push_back({first.position, first.color, normal, {0, 0}});
|
|
mesh.push_back({second.position, second.color, normal, {0, 0}});
|
|
mesh.push_back({third.position, third.color, normal, {0, 0}});
|
|
};
|
|
for (std::size_t time_index = 0; time_index + 1 < parameters.time_sample_count;
|
|
++time_index) {
|
|
for (std::size_t frequency_index = 0;
|
|
frequency_index + 1 < parameters.frequency_bin_count; ++frequency_index) {
|
|
const auto current = time_index * parameters.frequency_bin_count + frequency_index;
|
|
const auto next_time = current + parameters.frequency_bin_count;
|
|
append_triangle(samples[current], samples[next_time], samples[next_time + 1]);
|
|
append_triangle(samples[current], samples[next_time + 1], samples[current + 1]);
|
|
}
|
|
}
|
|
return mesh;
|
|
}
|
|
|
|
struct Spectrogram_Data_Generator {
|
|
Spectrogram_Parameters parameters{};
|
|
|
|
explicit Spectrogram_Data_Generator(Spectrogram_Parameters value = {})
|
|
: parameters(std::move(value)) {}
|
|
|
|
[[nodiscard]] Json schema() const {
|
|
Json fields = Json::array();
|
|
fields.push_back(integer_field("time_sample_count", "时间采样数", "时间方向网格采样数;GPU 顶点数约为 6×(时间采样数-1)×(频率分箱数-1)。", parameters.time_sample_count, 16, 1024));
|
|
fields.push_back(integer_field("frequency_bin_count", "频率分箱数", "对数频率方向分箱数;与时间采样数共同决定三角形和每次上传的数据量。", parameters.frequency_bin_count, 16, 1024));
|
|
fields.push_back(integer_field("ridge_count", "谱峰轨迹数", "生成随时间漂移的窄带谱峰数量。", 5, 1, 12));
|
|
fields.push_back(number_field("time_span_seconds", "时间跨度", "X 轴时间范围,单位秒。", parameters.time_span_seconds, 0.1, 3600.0, 0.1));
|
|
fields.push_back(number_field("minimum_frequency_hz", "最低频率", "对数频率轴下界,必须大于零。", parameters.minimum_frequency_hz, 0.001, 1.0e12, 1.0));
|
|
fields.push_back(number_field("maximum_frequency_hz", "最高频率", "对数频率轴上界,必须大于最低频率。", parameters.maximum_frequency_hz, 0.002, 1.0e12, 10.0));
|
|
fields.push_back(number_field("minimum_level_db", "最低声压级", "Z 轴色阶与高度下界,单位 dB。", parameters.minimum_level_db, -1000.0, 1000.0, 1.0));
|
|
fields.push_back(number_field("maximum_level_db", "最高声压级", "Z 轴色阶与高度上界,必须大于下界。", parameters.maximum_level_db, -1000.0, 1000.0, 1.0));
|
|
fields.push_back(number_field("animation_speed", "动态速度倍率", "谱峰随时间运动的倍率;0 表示保持当前相位。", parameters.animation_speed, 0.0, 100.0, 0.1));
|
|
fields.push_back(integer_field("update_every_n_frames", "数据更新帧间隔", "每 N 个渲染请求重建并上传一次 Mesh;可分离固定几何绘制与持续数据上传压力。", parameters.update_every_n_frames, 1, 10'000));
|
|
fields.push_back({{"key", "animation_enabled"}, {"label", "持续生成动态数据"}, {"description", "关闭后保留生成的数据集,仅测试固定 Mesh 的重复绘制;开启后按更新间隔持续重建。"}, {"editor", "boolean"}, {"editable", true}, {"value", parameters.animation_enabled}});
|
|
return {{"label", "生成三维频谱瀑布"},
|
|
{"description", "按时间采样、对数频率分箱和声压级范围生成连续 GPU Mesh 表面。"},
|
|
{"fields", std::move(fields)}};
|
|
}
|
|
|
|
[[nodiscard]] Json generate(Impl<Mesh_Visual>& visual, Impl<Axes_3D>& axes,
|
|
const Json& input) {
|
|
try {
|
|
Spectrogram_Parameters next;
|
|
next.time_sample_count = input_count(input, "time_sample_count", 1024);
|
|
next.frequency_bin_count = input_count(input, "frequency_bin_count", 1024);
|
|
next.ridge_count = input_count(input, "ridge_count", 12);
|
|
next.time_span_seconds = input_number(input, "time_span_seconds");
|
|
next.minimum_frequency_hz = input_number(input, "minimum_frequency_hz");
|
|
next.maximum_frequency_hz = input_number(input, "maximum_frequency_hz");
|
|
next.minimum_level_db = input_number(input, "minimum_level_db");
|
|
next.maximum_level_db = input_number(input, "maximum_level_db");
|
|
next.animation_speed = input_number(input, "animation_speed");
|
|
next.update_every_n_frames = input_count(input, "update_every_n_frames", 10'000);
|
|
const auto animation = input.at("animation_enabled");
|
|
if (!animation.is_boolean()) throw std::invalid_argument("animation_enabled must be boolean");
|
|
next.animation_enabled = animation.get<bool>();
|
|
if (!(next.time_span_seconds > 0.0) ||
|
|
!(next.minimum_frequency_hz > 0.0) ||
|
|
!(next.maximum_frequency_hz > next.minimum_frequency_hz) ||
|
|
!(next.maximum_level_db > next.minimum_level_db))
|
|
throw std::invalid_argument("spectrogram ranges are invalid");
|
|
const auto cells = (next.time_sample_count - 1) * (next.frequency_bin_count - 1);
|
|
if (cells > 1'400'000)
|
|
throw std::invalid_argument("spectrogram exceeds the 8,400,000 vertex stress-test limit");
|
|
parameters = next;
|
|
auto mesh = spectrogram_mesh(parameters);
|
|
const auto vertex_count = mesh.size();
|
|
if (visual.update_items(std::move(mesh)) != Mesh_Visual::Update_Items_Result::updated)
|
|
throw std::invalid_argument("generated spectrogram mesh was rejected");
|
|
plot::Axis_Descriptor time_axis{{0.0, parameters.time_span_seconds},
|
|
plot::Axis_Scale::time, "Time", "s", 6, 1, true, true, true};
|
|
plot::Axis_Descriptor frequency_axis{{parameters.minimum_frequency_hz,
|
|
parameters.maximum_frequency_hz}, plot::Axis_Scale::logarithmic,
|
|
"Frequency", "Hz", 5, 0, true, true, true};
|
|
plot::Axis_Descriptor level_axis{{parameters.minimum_level_db,
|
|
parameters.maximum_level_db}, plot::Axis_Scale::linear,
|
|
"SPL", "dB", 7, 0, true, true, true};
|
|
axes.set<&Axes_3D::Prop::x_axis>(std::move(time_axis));
|
|
axes.set<&Axes_3D::Prop::y_axis>(std::move(frequency_axis));
|
|
axes.set<&Axes_3D::Prop::z_axis>(std::move(level_axis));
|
|
return {{"success", true}, {"generated_count", vertex_count},
|
|
{"triangle_count", vertex_count / 3}};
|
|
} catch (const std::exception& error) {
|
|
return {{"success", false}, {"error", error.what()}};
|
|
}
|
|
}
|
|
|
|
void update(Impl<Mesh_Visual>& visual, Impl<Axes_3D>&,
|
|
Impl<Marker_Visual>* markers,
|
|
const Plot_Frame_Request& request) {
|
|
if (!parameters.animation_enabled ||
|
|
request.correlation_id % parameters.update_every_n_frames != 0) return;
|
|
const double animation_seconds = request.time_milliseconds / 1000.0 * parameters.animation_speed;
|
|
auto mesh = spectrogram_mesh(parameters, animation_seconds);
|
|
if (visual.update_items(std::move(mesh)) != Mesh_Visual::Update_Items_Result::updated)
|
|
throw std::logic_error("animated spectrogram mesh was rejected");
|
|
if (markers == nullptr) return;
|
|
auto items = markers->template read_prop<Marker_Visual::Base_Tag>().items;
|
|
for (auto& marker : items) {
|
|
const float time = std::clamp((marker.position.x + 1.0F) * 0.5F, 0.0F, 1.0F);
|
|
const float frequency = std::clamp((marker.position.y + 1.0F) * 0.5F, 0.0F, 1.0F);
|
|
marker.position.z = -1.0F + 2.0F * spectrogram_level(
|
|
time, frequency, animation_seconds, parameters.ridge_count);
|
|
}
|
|
if (markers->update_items(std::move(items)) != Marker_Visual::Update_Items_Result::updated)
|
|
throw std::logic_error("surface-attached spectrogram markers were rejected");
|
|
}
|
|
};
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_point_plot(asio::any_io_executor executor) {
|
|
return make_visual_plot<Point_Visual>(std::move(executor), "Point Visual", Impl<Point_Visual>::Builder{}
|
|
.set(&Point_Visual::Prop::items, std::vector<Point>{{{-0.65F, -0.25F, 0.05F}, color(255, 91, 110), 28.0F}, {{0.0F, 0.58F, 0.25F}, color(82, 226, 190), 34.0F}, {{0.62F, -0.12F, -0.15F}, color(75, 145, 255), 30.0F}}).build());
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_splat_plot(asio::any_io_executor executor) {
|
|
return make_visual_plot<Splat_Visual>(std::move(executor), "Splat Visual", Impl<Splat_Visual>::Builder{}
|
|
.set(&Splat_Visual::Prop::items, std::vector<Splat>{{{-0.48F, 0.0F, 0.1F}, color(255, 98, 115, 210), {0.18F, 0.08F}, 0.45F}, {{0.28F, 0.15F, 0.0F}, color(66, 218, 188, 210), {0.12F, 0.22F}, -0.3F}, {{0.15F, -0.38F, 0.2F}, color(76, 132, 255, 210), {0.2F, 0.1F}, 0.9F}}).build());
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_pixel_plot(asio::any_io_executor executor) {
|
|
std::vector<Pixel> pixels;
|
|
for (int y = -8; y <= 8; ++y) for (int x = -12; x <= 12; ++x) pixels.push_back({{x / 13.0F, y / 9.0F, 0.12F * std::sin(x * .45F) * std::cos(y * .35F)}, color(static_cast<std::uint8_t>(90 + 6 * (x + 12)), static_cast<std::uint8_t>(100 + 8 * (y + 8)), 230), 5.0F});
|
|
return make_visual_plot<Pixel_Visual>(std::move(executor), "Pixel Visual", Impl<Pixel_Visual>::Builder{}.set(&Pixel_Visual::Prop::items, std::move(pixels)).build());
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_marker_plot(asio::any_io_executor executor) {
|
|
return make_visual_plot<Marker_Visual>(std::move(executor), "Marker Visual", Impl<Marker_Visual>::Builder{}
|
|
.set(&Marker_Visual::Prop::items, std::vector<Marker>{{{-0.7F, 0.0F, 0.0F}, color(255, 93, 115), 34.0F, 0.0F, Marker_Shape::disc}, {{-0.35F, 0.25F, 0.1F}, color(91, 226, 193), 36.0F, 0.25F, Marker_Shape::square}, {{0.0F, -0.2F, 0.2F}, color(100, 158, 255), 38.0F, 0.5F, Marker_Shape::triangle}, {{0.35F, 0.25F, 0.1F}, color(250, 195, 92), 40.0F, 0.75F, Marker_Shape::diamond}, {{0.7F, 0.0F, 0.0F}, color(201, 132, 255), 42.0F, 1.0F, Marker_Shape::cross}}).build());
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_sphere_plot(asio::any_io_executor executor) {
|
|
return make_visual_plot<Sphere_Visual>(std::move(executor), "Sphere Visual", Impl<Sphere_Visual>::Builder{}
|
|
.set(&Sphere_Visual::Prop::items, std::vector<Sphere>{{{-0.48F, -0.2F, 0.0F}, color(255, 91, 110), 0.28F}, {{0.08F, 0.25F, 0.18F}, color(82, 226, 190), 0.36F}, {{0.55F, -0.18F, -0.12F}, color(75, 145, 255), 0.24F}}).build());
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_segment_plot(asio::any_io_executor executor) {
|
|
std::vector<Segment> segments;
|
|
for (int index = 0; index < 12; ++index) { const float angle = static_cast<float>(index) * std::numbers::pi_v<float> / 6.0F; segments.push_back({{0.0F, 0.0F, 0.0F}, {0.82F * std::cos(angle), 0.82F * std::sin(angle), 0.18F * std::sin(2 * angle)}, color(static_cast<std::uint8_t>(80 + index * 13), static_cast<std::uint8_t>(220 - index * 8), 240), 4.0F}); }
|
|
return make_visual_plot<Segment_Visual>(std::move(executor), "Segment Visual", Impl<Segment_Visual>::Builder{}.set(&Segment_Visual::Prop::items, std::move(segments)).build());
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_vector_plot(asio::any_io_executor executor) {
|
|
return make_visual_plot<Vector_Visual>(std::move(executor), "Vector Visual", Impl<Vector_Visual>::Builder{}
|
|
.set(&Vector_Visual::Prop::items, std::vector<Vector_Glyph>{{{-0.55F, -0.35F, 0.0F}, {0.55F, 0.2F, 0.25F}, color(255, 98, 115), 4.0F}, {{-0.1F, 0.0F, 0.0F}, {0.2F, 0.62F, 0.18F}, color(80, 225, 190), 5.0F}, {{0.35F, -0.25F, 0.0F}, {-0.12F, 0.25F, 0.65F}, color(78, 145, 255), 4.0F}}).build());
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_primitive_plot(asio::any_io_executor executor) {
|
|
return make_visual_plot<Primitive_Visual>(std::move(executor), "Primitive Visual", Impl<Primitive_Visual>::Builder{}
|
|
.set(&Primitive_Visual::Prop::topology, Primitive_Topology::triangle_list)
|
|
.set(&Primitive_Visual::Prop::items, std::vector<Primitive_Vertex>{{{-0.72F, -0.55F, 0.0F}, color(255, 86, 110), {0, 0, 1}}, {{0.72F, -0.55F, 0.0F}, color(75, 145, 255), {0, 0, 1}}, {{0.0F, 0.72F, 0.25F}, color(82, 226, 190), {0, 0, 1}}}).build());
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_mesh_plot(asio::any_io_executor executor) {
|
|
const std::vector<Mesh_Vertex> mesh{{{-0.65F, -0.55F, 0.0F}, color(255, 94, 112), {0, 0, 1}, {0, 0}}, {{0.65F, -0.55F, 0.0F}, color(75, 145, 255), {0, 0, 1}, {1, 0}}, {{0.65F, 0.55F, 0.0F}, color(82, 226, 190), {0, 0, 1}, {1, 1}}, {{-0.65F, -0.55F, 0.0F}, color(255, 94, 112), {0, 0, 1}, {0, 0}}, {{0.65F, 0.55F, 0.0F}, color(82, 226, 190), {0, 0, 1}, {1, 1}}, {{-0.65F, 0.55F, 0.0F}, color(244, 190, 86), {0, 0, 1}, {0, 1}}};
|
|
return make_visual_plot<Mesh_Visual>(std::move(executor), "Mesh Visual", Impl<Mesh_Visual>::Builder{}.set(&Mesh_Visual::Prop::items, mesh).build());
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_spectrogram_plot(asio::any_io_executor executor) {
|
|
const Spectrogram_Parameters parameters;
|
|
auto marker_result = Impl<Marker_Visual>::Builder{}
|
|
.set(&Marker_Visual::Prop::items, std::vector<Marker>{
|
|
{{-0.35F, -0.18F, 0.72F}, color(255, 244, 170), 23.0F, 0.0F,
|
|
Marker_Shape::diamond, true},
|
|
{{0.28F, 0.34F, 0.86F}, color(88, 236, 211), 21.0F, 0.0F,
|
|
Marker_Shape::cross, true}})
|
|
.set(&Marker_Visual::Prop::depth_test, false)
|
|
.build();
|
|
if (!marker_result)
|
|
throw std::logic_error("3D Spectrogram marker construction failed");
|
|
auto markers = std::move(marker_result).value();
|
|
Scene_Components_3D components;
|
|
components.camera.initial_view = {{3.35, -3.55, 2.45}, {0.0, 0.0, -0.05},
|
|
{0.0, 0.0, 1.0}};
|
|
components.camera.turntable_control = {0.15, 0.15, 0.10, 0.0015,
|
|
-1.35, 1.35, 1.25, 12.0,
|
|
true, true, true, false};
|
|
components.camera.vertical_field_of_view_degrees = 41.0;
|
|
components.axes = {
|
|
plot::Axis_Descriptor{{0.0, parameters.time_span_seconds},
|
|
plot::Axis_Scale::time, "Time", "s", 6, 1, true, true, true},
|
|
plot::Axis_Descriptor{{parameters.minimum_frequency_hz,
|
|
parameters.maximum_frequency_hz}, plot::Axis_Scale::logarithmic,
|
|
"Frequency", "Hz", 5, 0, true, true, true},
|
|
plot::Axis_Descriptor{{parameters.minimum_level_db,
|
|
parameters.maximum_level_db}, plot::Axis_Scale::linear,
|
|
"SPL", "dB", 7, 0, true, true, true}};
|
|
return make_visual_plot<Mesh_Visual>(
|
|
std::move(executor), "3D Spectrogram",
|
|
Impl<Mesh_Visual>::Builder{}
|
|
.set(&Mesh_Visual::Prop::items, spectrogram_mesh(parameters))
|
|
.build(),
|
|
std::move(components), Spectrogram_Data_Generator{parameters}, std::move(markers));
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_path_plot(asio::any_io_executor executor) {
|
|
std::vector<Path_Vertex> path;
|
|
for (int index = 0; index < 64; ++index) { const float t = static_cast<float>(index) / 63.0F; path.push_back({{-0.9F + 1.8F * t, 0.48F * std::sin(t * 4.0F * std::numbers::pi_v<float>), 0.25F * std::cos(t * 2.0F * std::numbers::pi_v<float>)}, color(static_cast<std::uint8_t>(70 + 170 * t), static_cast<std::uint8_t>(220 - 80 * t), 245), 5.0F}); }
|
|
return make_visual_plot<Path_Visual>(std::move(executor), "Path Visual", Impl<Path_Visual>::Builder{}.set(&Path_Visual::Prop::items, std::move(path)).build());
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_image_plot(asio::any_io_executor executor) {
|
|
return make_visual_plot<Image_Visual>(std::move(executor), "Image Visual", Impl<Image_Visual>::Builder{}
|
|
.set(&Image_Visual::Prop::field_width, 32U).set(&Image_Visual::Prop::field_height, 32U)
|
|
.set(&Image_Visual::Prop::items, std::vector<Image>{{{0.0F, 0.0F, 0.0F}, {1.45F, 1.0F}, {0, 0, 1, 1}, color(255, 255, 255)}}).build());
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_labels_plot(asio::any_io_executor executor) {
|
|
return make_visual_plot<Labels_Visual>(std::move(executor), "Labels Visual", Impl<Labels_Visual>::Builder{}
|
|
.set(&Labels_Visual::Prop::field_width, 8U).set(&Labels_Visual::Prop::field_height, 8U)
|
|
.set(&Labels_Visual::Prop::items, std::vector<Label>{{{-0.55F, 0.28F, 0.0F}, 0, {72, 34}, color(255, 255, 255)}, {{0.5F, 0.25F, 0.1F}, 1, {72, 34}, color(255, 255, 255)}, {{-0.45F, -0.32F, 0.1F}, 2, {72, 34}, color(255, 255, 255)}, {{0.55F, -0.3F, 0.0F}, 3, {72, 34}, color(255, 255, 255)}}).build());
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_glyph_plot(asio::any_io_executor executor) {
|
|
return make_visual_plot<Glyph_Visual>(std::move(executor), "Glyph Visual", Impl<Glyph_Visual>::Builder{}
|
|
.set(&Glyph_Visual::Prop::field_width, 32U).set(&Glyph_Visual::Prop::field_height, 32U)
|
|
.set(&Glyph_Visual::Prop::items, std::vector<Glyph>{{{-0.55F, -0.15F, 0.0F}, {-0.18F, -0.18F, 0.18F, 0.18F}, {0, 0, 1, 1}, color(255, 100, 120), -0.2F}, {{0.0F, 0.22F, 0.1F}, {-0.22F, -0.22F, 0.22F, 0.22F}, {0, 0, 1, 1}, color(82, 226, 190), 0.25F}, {{0.55F, -0.15F, 0.0F}, {-0.2F, -0.2F, 0.2F, 0.2F}, {0, 0, 1, 1}, color(80, 145, 255), 0.55F}}).build());
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_text_plot(asio::any_io_executor executor) {
|
|
return make_visual_plot<Text_Visual>(std::move(executor), "Text Visual", Impl<Text_Visual>::Builder{}
|
|
.set(&Text_Visual::Prop::items, std::vector<Text_Label>{{{-0.72F, 0.28F, 0.0F}, "Aethera", color(82, 226, 190), 28.0F}, {{-0.62F, -0.15F, 0.1F}, "Datoviz Visual", color(90, 155, 255), 22.0F}}).build());
|
|
}
|
|
|
|
std::shared_ptr<Plot> make_datoviz_volume_plot(asio::any_io_executor executor) {
|
|
std::vector<Voxel> voxels(16U * 16U * 16U);
|
|
for (std::size_t index = 0; index < voxels.size(); ++index) voxels[index].value = static_cast<float>(index % 256U) / 255.0F;
|
|
return make_visual_plot<Volume_Visual>(std::move(executor), "Volume Visual", Impl<Volume_Visual>::Builder{}
|
|
.set(&Volume_Visual::Prop::field_width, 16U).set(&Volume_Visual::Prop::field_height, 16U).set(&Volume_Visual::Prop::field_depth, 16U)
|
|
.set(&Volume_Visual::Prop::items, std::move(voxels)).build());
|
|
}
|
|
}
|