#include "Gallery_Plots.hpp" #include "Renderable_Adapter.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace aethera::web { namespace { using namespace render_3d; using Scene_3D = Impl; 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(value), static_cast(minimum), static_cast(maximum), 1.0); field["editor"] = "integer"; return field; } template Json generator_schema() { Json fields = Json::array(); if constexpr (std::same_as) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) label = "生成三维图元顶点"; else if constexpr (std::same_as) label = "生成三维网格顶点"; else if constexpr (std::same_as) { 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) { 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) { 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) { 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) { 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(); 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(maximum) || std::floor(value) != value) throw std::invalid_argument(std::string(key) + " is outside the supported integer range"); return static_cast(value); } std::pair 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::max() || maximum > std::numeric_limits::max()) throw std::invalid_argument("generator range exceeds float coordinates"); return {static_cast(minimum), static_cast(maximum)}; } template 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(x_min, x_max); y = std::uniform_real_distribution(y_min, y_max); z = std::uniform_real_distribution(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(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(lower, upper); }; if constexpr (std::same_as || std::same_as) set_first("diameter_min", "diameter_max"); else if constexpr (std::same_as) { set_first("sigma_min", "sigma_max"); set_second("angle_min", "angle_max"); } else if constexpr (std::same_as || std::same_as) set_first("size_min", "size_max"); else if constexpr (std::same_as) set_first("radius_min", "radius_max"); else if constexpr (std::same_as || std::same_as) set_first("width_min", "width_max"); else if constexpr (std::same_as) set_first("direction_min", "direction_max"); else if constexpr (std::same_as || std::same_as) set_first("extent_min", "extent_max"); else if constexpr (std::same_as) 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) item.diameter_px = first(engine); else if constexpr (std::same_as) { item.sigma = {first(engine), first(engine)}; item.angle = second(engine); } else if constexpr (std::same_as) item.size_px = first(engine); else if constexpr (std::same_as) item.diameter_px = first(engine); else if constexpr (std::same_as) item.radius = first(engine); else if constexpr (std::same_as) item.width_px = first(engine); else if constexpr (std::same_as) { item.direction = {first(engine), first(engine), first(engine)}; } else if constexpr (std::same_as) item.width_px = first(engine); else if constexpr (std::same_as || std::same_as) item.extent = {first(engine), first(engine)}; else if constexpr (std::same_as) item.angle = first(engine); else if constexpr (std::same_as) item.size_px = first(engine); } private: std::uniform_real_distribution x{}; std::uniform_real_distribution y{}; std::uniform_real_distribution z{}; std::uniform_real_distribution first{}; std::uniform_real_distribution second{}; }; struct Random_Data_Generator {}; template class Visual_Scene_View final : public Plot::Scene_View { public: using Camera_Object = Impl; using Axes_Object = Impl; using Marker_Object = Impl; Visual_Scene_View(Scene_3D& scene, std::unique_ptr camera, std::unique_ptr axes, std::unique_ptr visual, std::string label, Data_Generator data_generator = {}, std::unique_ptr 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, detail::Prop_Field<&Render_Scene_3D::Prop::view_active, "view_active", "Whether the scene publishes rendered frames.">, detail::State_Field, detail::State_Field, detail::State_Field>; using Visual_Adapter = detail::Renderable_Adapter, 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, detail::State_Field, detail::State_Field>; using Camera_Adapter = detail::Renderable_Adapter, 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, 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) { using Marker_Adapter = detail::Renderable_Adapter, 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, detail::State_Field, detail::State_Field>; 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, 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, detail::State_Field, detail::State_Field>; 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) { using Definition = typename Visual_Object::Attached_Object; return generator_schema(); } else { return data_generator_.schema(); } } [[nodiscard]] nlohmann::json generate_data(const nlohmann::json& input) override { if constexpr (!std::same_as) { 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().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) { 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 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(); prop.field_width = static_cast(width); prop.field_height = static_cast(height); prop.field_depth = static_cast(depth); }); } else { count = input_count(input, "count", 5'000'000); const auto& current = visual_->template read_prop().items; if (current.empty()) throw std::invalid_argument("visual has no item template"); const auto prototype = current.front(); Item_Randomizer 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_; /* Scene 引用的 Camera 唯一所有者。 */ std::unique_ptr axes_; /* Scene 引用的 Axes 唯一所有者。 */ std::unique_ptr visual_; /* Scene 引用的 Visual 唯一所有者。 */ std::unique_ptr markers_; /* 可选的独立 Marker Visual 所有者。 */ std::vector> descriptors_; /* Prop/State 协议描述。 */ }; struct Scene_Components_3D { plot::Camera_Descriptor camera{}; std::array 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 std::shared_ptr 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> markers = {}) { using Visual_Object = Impl; using Camera_Object = Impl; using Axes_Object = Impl; 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>( *scene, std::move(camera), std::move(axes), std::move(visual), std::move(label), std::move(data_generator), std::move(markers)); return std::make_shared(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 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::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(animation_seconds); float level = 0.075F + 0.055F * std::sin( 2.0F * std::numbers::pi_v * (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 spectrogram_mesh(const Spectrogram_Parameters& parameters, double animation_seconds = 0.0) { struct Sample { Vec3 position; Color color; }; std::vector 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(time_index) / static_cast(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(frequency_index) / static_cast(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; 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& visual, Impl& 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(); 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& visual, Impl&, Impl* 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().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 make_datoviz_point_plot(asio::any_io_executor executor) { return make_visual_plot(std::move(executor), "Point Visual", Impl::Builder{} .set(&Point_Visual::Prop::items, std::vector{{{-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 make_datoviz_splat_plot(asio::any_io_executor executor) { return make_visual_plot(std::move(executor), "Splat Visual", Impl::Builder{} .set(&Splat_Visual::Prop::items, std::vector{{{-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 make_datoviz_pixel_plot(asio::any_io_executor executor) { std::vector 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(90 + 6 * (x + 12)), static_cast(100 + 8 * (y + 8)), 230), 5.0F}); return make_visual_plot(std::move(executor), "Pixel Visual", Impl::Builder{}.set(&Pixel_Visual::Prop::items, std::move(pixels)).build()); } std::shared_ptr make_datoviz_marker_plot(asio::any_io_executor executor) { return make_visual_plot(std::move(executor), "Marker Visual", Impl::Builder{} .set(&Marker_Visual::Prop::items, std::vector{{{-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 make_datoviz_sphere_plot(asio::any_io_executor executor) { return make_visual_plot(std::move(executor), "Sphere Visual", Impl::Builder{} .set(&Sphere_Visual::Prop::items, std::vector{{{-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 make_datoviz_segment_plot(asio::any_io_executor executor) { std::vector segments; for (int index = 0; index < 12; ++index) { const float angle = static_cast(index) * std::numbers::pi_v / 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(80 + index * 13), static_cast(220 - index * 8), 240), 4.0F}); } return make_visual_plot(std::move(executor), "Segment Visual", Impl::Builder{}.set(&Segment_Visual::Prop::items, std::move(segments)).build()); } std::shared_ptr make_datoviz_vector_plot(asio::any_io_executor executor) { return make_visual_plot(std::move(executor), "Vector Visual", Impl::Builder{} .set(&Vector_Visual::Prop::items, std::vector{{{-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 make_datoviz_primitive_plot(asio::any_io_executor executor) { return make_visual_plot(std::move(executor), "Primitive Visual", Impl::Builder{} .set(&Primitive_Visual::Prop::topology, Primitive_Topology::triangle_list) .set(&Primitive_Visual::Prop::items, std::vector{{{-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 make_datoviz_mesh_plot(asio::any_io_executor executor) { const std::vector 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(std::move(executor), "Mesh Visual", Impl::Builder{}.set(&Mesh_Visual::Prop::items, mesh).build()); } std::shared_ptr make_datoviz_spectrogram_plot(asio::any_io_executor executor) { const Spectrogram_Parameters parameters; auto marker_result = Impl::Builder{} .set(&Marker_Visual::Prop::items, std::vector{ {{-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( std::move(executor), "3D Spectrogram", Impl::Builder{} .set(&Mesh_Visual::Prop::items, spectrogram_mesh(parameters)) .build(), std::move(components), Spectrogram_Data_Generator{parameters}, std::move(markers)); } std::shared_ptr make_datoviz_path_plot(asio::any_io_executor executor) { std::vector path; for (int index = 0; index < 64; ++index) { const float t = static_cast(index) / 63.0F; path.push_back({{-0.9F + 1.8F * t, 0.48F * std::sin(t * 4.0F * std::numbers::pi_v), 0.25F * std::cos(t * 2.0F * std::numbers::pi_v)}, color(static_cast(70 + 170 * t), static_cast(220 - 80 * t), 245), 5.0F}); } return make_visual_plot(std::move(executor), "Path Visual", Impl::Builder{}.set(&Path_Visual::Prop::items, std::move(path)).build()); } std::shared_ptr make_datoviz_image_plot(asio::any_io_executor executor) { return make_visual_plot(std::move(executor), "Image Visual", Impl::Builder{} .set(&Image_Visual::Prop::field_width, 32U).set(&Image_Visual::Prop::field_height, 32U) .set(&Image_Visual::Prop::items, std::vector{{{0.0F, 0.0F, 0.0F}, {1.45F, 1.0F}, {0, 0, 1, 1}, color(255, 255, 255)}}).build()); } std::shared_ptr make_datoviz_labels_plot(asio::any_io_executor executor) { return make_visual_plot(std::move(executor), "Labels Visual", Impl::Builder{} .set(&Labels_Visual::Prop::field_width, 8U).set(&Labels_Visual::Prop::field_height, 8U) .set(&Labels_Visual::Prop::items, std::vector