拆出mcp
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
#include "Control_Service.hpp"
|
||||
#include "Control_Service.ipp"
|
||||
#include "Protocol_Type.hpp"
|
||||
#include "runtime/Gallery_Plots.hpp"
|
||||
#include <render_common.hpp>
|
||||
#include <array>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
namespace aethera::mcp {
|
||||
namespace {
|
||||
|
||||
struct Empty_Request {};
|
||||
struct Plot_Request {
|
||||
std::string plot; /* Gallery Plot 稳定标识。 */
|
||||
};
|
||||
struct Component_Request {
|
||||
std::string plot; /* Gallery Plot 稳定标识。 */
|
||||
std::string component; /* Renderable 业务组件标识。 */
|
||||
};
|
||||
struct Write_Property_Request {
|
||||
std::string plot; /* Gallery Plot 稳定标识。 */
|
||||
std::string component; /* Renderable 业务组件标识。 */
|
||||
std::string property; /* 待修改属性键。 */
|
||||
nlohmann::json value; /* 属性协议值。 */
|
||||
};
|
||||
struct Generate_Data_Request {
|
||||
std::string plot; /* Gallery Plot 稳定标识。 */
|
||||
nlohmann::json input; /* Plot 数据生成器输入。 */
|
||||
};
|
||||
|
||||
using Invoke = Tool_Call_Output (*)(Control_Service&, const nlohmann::json&);
|
||||
using Schema = nlohmann::json (*)();
|
||||
|
||||
struct Operation {
|
||||
std::string_view name; /* MCP 稳定 tool name。 */
|
||||
std::string_view description; /* 模型选择工具时使用的业务说明。 */
|
||||
Schema schema; /* PFR 自动生成的输入结构。 */
|
||||
Invoke invoke; /* 协议无关的业务调用入口。 */
|
||||
};
|
||||
|
||||
template <typename Request>
|
||||
[[nodiscard]] nlohmann::json request_schema() {
|
||||
return describe_protocol_type<Request>();
|
||||
}
|
||||
|
||||
template <typename Request, typename Callback>
|
||||
[[nodiscard]] Tool_Call_Output decode_and_call(
|
||||
const nlohmann::json& arguments, Callback&& callback) {
|
||||
try {
|
||||
Request request{};
|
||||
decode_protocol_value(request, arguments);
|
||||
return std::forward<Callback>(callback)(request);
|
||||
} catch (const nlohmann::json::exception& failure) {
|
||||
return {Tool_Call_Result::invalid_arguments, {}, failure.what()};
|
||||
} catch (const std::invalid_argument& failure) {
|
||||
return {Tool_Call_Result::invalid_arguments, {}, failure.what()};
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] Tool_Call_Output list_plots(
|
||||
Control_Service& service, const nlohmann::json& arguments) {
|
||||
return decode_and_call<Empty_Request>(arguments, [&service](const auto&) {
|
||||
return Tool_Call_Output{Tool_Call_Result::ok, service.plot_catalog(), {}};
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] Tool_Call_Output plot_schema(
|
||||
Control_Service& service, const nlohmann::json& arguments) {
|
||||
return decode_and_call<Plot_Request>(arguments, [&service](const auto& request) {
|
||||
const auto plot = service.find_plot(request.plot);
|
||||
if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
|
||||
return Tool_Call_Output{Tool_Call_Result::ok, plot->schema(), {}};
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] Tool_Call_Output plot_diagnostics(
|
||||
Control_Service& service, const nlohmann::json& arguments) {
|
||||
return decode_and_call<Plot_Request>(arguments, [&service](const auto& request) {
|
||||
const auto plot = service.find_plot(request.plot);
|
||||
if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
|
||||
return Tool_Call_Output{Tool_Call_Result::ok, plot->diagnostics(), {}};
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] Tool_Call_Output reset_plot_diagnostics(
|
||||
Control_Service& service, const nlohmann::json& arguments) {
|
||||
return decode_and_call<Plot_Request>(arguments, [&service](const auto& request) {
|
||||
const auto plot = service.find_plot(request.plot);
|
||||
if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
|
||||
plot->reset_diagnostics();
|
||||
return Tool_Call_Output{Tool_Call_Result::ok, {{"accepted", true}}, {}};
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] Tool_Call_Output component_state(
|
||||
Control_Service& service, const nlohmann::json& arguments) {
|
||||
return decode_and_call<Component_Request>(arguments, [&service](const auto& request) {
|
||||
const auto plot = service.find_plot(request.plot);
|
||||
if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
|
||||
auto result = plot->component_state(request.component);
|
||||
if (!result.value("success", true))
|
||||
return Tool_Call_Output{Tool_Call_Result::rejected, std::move(result), "unknown component"};
|
||||
return Tool_Call_Output{Tool_Call_Result::ok, std::move(result), {}};
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] Tool_Call_Output write_property(
|
||||
Control_Service& service, const nlohmann::json& arguments) {
|
||||
return decode_and_call<Write_Property_Request>(arguments, [&service](const auto& request) {
|
||||
const auto plot = service.find_plot(request.plot);
|
||||
if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
|
||||
auto result = plot->write_prop(
|
||||
request.component, request.property, request.value);
|
||||
const bool success = result.value("success", false);
|
||||
return Tool_Call_Output{success ? Tool_Call_Result::ok : Tool_Call_Result::rejected,
|
||||
std::move(result), success ? "" : "property write rejected"};
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] Tool_Call_Output generate_data(
|
||||
Control_Service& service, const nlohmann::json& arguments) {
|
||||
return decode_and_call<Generate_Data_Request>(arguments, [&service](const auto& request) {
|
||||
const auto plot = service.find_plot(request.plot);
|
||||
if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
|
||||
return Tool_Call_Output{Tool_Call_Result::ok,
|
||||
plot->generate_data(request.input), {}};
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] Tool_Call_Output begin_benchmark(
|
||||
Control_Service& service, const nlohmann::json& arguments) {
|
||||
return decode_and_call<Plot_Request>(arguments, [&service](const auto& request) {
|
||||
const auto plot = service.find_plot(request.plot);
|
||||
if (!plot) return Tool_Call_Output{Tool_Call_Result::unknown_plot, {}, "unknown plot"};
|
||||
plot->reset_diagnostics();
|
||||
plot->render_once();
|
||||
return Tool_Call_Output{Tool_Call_Result::ok,
|
||||
{{"accepted", true}, {"plot", request.plot},
|
||||
{"status_tool", "aethera_benchmark_read"}}, {}};
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] nlohmann::json task_runtime_json() {
|
||||
const auto state = task_runtime_state();
|
||||
nlohmann::json workers = nlohmann::json::array();
|
||||
for (const auto& worker : state.workers)
|
||||
workers.push_back({
|
||||
{"id", worker.id}, {"task_count", worker.task_count},
|
||||
{"current_queue_size", worker.current_queue_size},
|
||||
{"current_queue_capacity", worker.current_queue_capacity},
|
||||
{"peak_queue_size", worker.peak_observed_queue_size},
|
||||
{"max_queue_capacity", worker.max_observed_queue_capacity},
|
||||
{"active_task", {{"native_id", std::to_string(worker.active_task_hash)},
|
||||
{"type", worker.active_task_type},
|
||||
{"time_ns", worker.active_task_time_ns}}},
|
||||
{"task_time_ns", worker.task_time_ns},
|
||||
{"busy_time_ns", worker.busy_time_ns},
|
||||
{"cpu_time_ns", worker.cpu_time_ns},
|
||||
{"non_cpu_time_ns", worker.non_cpu_time_ns},
|
||||
{"idle_time_ns", worker.idle_time_ns},
|
||||
{"min_task_time_ns", worker.min_task_time_ns},
|
||||
{"max_task_time_ns", worker.max_task_time_ns},
|
||||
{"utilization", worker.utilization},
|
||||
{"cpu_utilization", worker.cpu_utilization}});
|
||||
nlohmann::json task_types = nlohmann::json::array();
|
||||
for (const auto& type : state.task_types)
|
||||
task_types.push_back({
|
||||
{"name", type.name}, {"count", type.count},
|
||||
{"total_time_ns", type.total_time_ns},
|
||||
{"min_time_ns", type.min_time_ns},
|
||||
{"max_time_ns", type.max_time_ns}});
|
||||
return {
|
||||
{"protocol", "aethera.taskflow.runtime"}, {"version", 1},
|
||||
{"worker_count", state.worker_count},
|
||||
{"active_topologies", state.active_topology_count},
|
||||
{"active_taskflows", state.active_taskflow_count},
|
||||
{"peak_active_taskflows", state.peak_active_taskflow_count},
|
||||
{"completed_taskflows", state.completed_taskflow_count},
|
||||
{"failed_taskflows", state.failed_taskflow_count},
|
||||
{"active_tasks", state.active_task_count},
|
||||
{"peak_active_tasks", state.peak_active_task_count},
|
||||
{"active_workers", state.active_worker_count},
|
||||
{"peak_active_workers", state.peak_active_worker_count},
|
||||
{"observed_tasks", state.observed_task_count},
|
||||
{"named_tasks", state.named_task_count},
|
||||
{"peak_worker_queue_size", state.peak_observed_worker_queue_size},
|
||||
{"max_worker_queue_capacity", state.max_observed_worker_queue_capacity},
|
||||
{"longest_task", {{"native_id", std::to_string(state.longest_task_hash)},
|
||||
{"name", state.longest_task_name},
|
||||
{"type", state.longest_task_type},
|
||||
{"time_ns", state.longest_task_time_ns}}},
|
||||
{"total_task_time_ns", state.total_task_time_ns},
|
||||
{"worker_busy_time_ns", state.worker_busy_time_ns},
|
||||
{"worker_cpu_time_ns", state.worker_cpu_time_ns},
|
||||
{"observed_wall_time_ns", state.observed_wall_time_ns},
|
||||
{"worker_utilization", state.worker_utilization},
|
||||
{"worker_cpu_utilization", state.worker_cpu_utilization},
|
||||
{"task_types", std::move(task_types)}, {"workers", std::move(workers)}};
|
||||
}
|
||||
|
||||
[[nodiscard]] Tool_Call_Output task_runtime(
|
||||
Control_Service&, const nlohmann::json& arguments) {
|
||||
return decode_and_call<Empty_Request>(arguments, [](const auto&) {
|
||||
return Tool_Call_Output{Tool_Call_Result::ok, task_runtime_json(), {}};
|
||||
});
|
||||
}
|
||||
|
||||
constexpr std::array operations{
|
||||
Operation{"aethera_plot_list", "List every 2D and 3D gallery plot.",
|
||||
&request_schema<Empty_Request>, &list_plots},
|
||||
Operation{"aethera_plot_schema", "Describe a plot and its editable render components.",
|
||||
&request_schema<Plot_Request>, &plot_schema},
|
||||
Operation{"aethera_plot_diagnostics", "Read current frame, scene, GPU and Datoviz diagnostics.",
|
||||
&request_schema<Plot_Request>, &plot_diagnostics},
|
||||
Operation{"aethera_plot_diagnostics_reset", "Reset the plot's authoritative diagnostic counters.",
|
||||
&request_schema<Plot_Request>, &reset_plot_diagnostics},
|
||||
Operation{"aethera_component_state", "Read one render component's published state.",
|
||||
&request_schema<Component_Request>, &component_state},
|
||||
Operation{"aethera_component_write", "Write one editable render component property.",
|
||||
&request_schema<Write_Property_Request>, &write_property},
|
||||
Operation{"aethera_data_generate", "Generate input data for a gallery plot.",
|
||||
&request_schema<Generate_Data_Request>, &generate_data},
|
||||
Operation{"aethera_benchmark_begin", "Reset diagnostics and asynchronously start rendering a plot.",
|
||||
&request_schema<Plot_Request>, &begin_benchmark},
|
||||
Operation{"aethera_benchmark_read", "Read benchmark results from the plot's current diagnostics.",
|
||||
&request_schema<Plot_Request>, &plot_diagnostics},
|
||||
Operation{"aethera_task_runtime", "Read Taskflow executor utilization and queue diagnostics.",
|
||||
&request_schema<Empty_Request>, &task_runtime},
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Control_Service::Control_Service() : d(std::make_unique<Private>()) {}
|
||||
Control_Service::~Control_Service() = default;
|
||||
|
||||
std::shared_ptr<Control_Service> Control_Service::create() {
|
||||
auto service = std::shared_ptr<Control_Service>(new Control_Service);
|
||||
service->d->plots.reserve(web::gallery_plot_definitions().size());
|
||||
for (const auto& definition : web::gallery_plot_definitions())
|
||||
service->d->plots.emplace(definition.id, definition.create());
|
||||
return service;
|
||||
}
|
||||
|
||||
std::shared_ptr<web::Plot> Control_Service::find_plot(
|
||||
std::string_view id) const {
|
||||
const auto found = d->plots.find(std::string{id});
|
||||
return found == d->plots.end() ? nullptr : found->second;
|
||||
}
|
||||
|
||||
nlohmann::json Control_Service::plot_catalog() const {
|
||||
nlohmann::json result = nlohmann::json::array();
|
||||
for (const auto& definition : web::gallery_plot_definitions())
|
||||
result.push_back({
|
||||
{"id", definition.id}, {"title", definition.title},
|
||||
{"category", definition.category},
|
||||
{"description", definition.description},
|
||||
{"dimension", web::plot_dimension_name(definition.dimension)}});
|
||||
return result;
|
||||
}
|
||||
|
||||
nlohmann::json Control_Service::tool_catalog() const {
|
||||
nlohmann::json result = nlohmann::json::array();
|
||||
for (const auto& operation : operations)
|
||||
result.push_back({{"name", operation.name},
|
||||
{"description", operation.description},
|
||||
{"inputSchema", operation.schema()}});
|
||||
return result;
|
||||
}
|
||||
|
||||
Tool_Call_Output Control_Service::call_tool(
|
||||
std::string_view name, const nlohmann::json& arguments) {
|
||||
for (const auto& operation : operations)
|
||||
if (operation.name == name) return operation.invoke(*this, arguments);
|
||||
return {Tool_Call_Result::unknown_tool, {}, "unknown tool"};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace aethera::web {
|
||||
struct Plot;
|
||||
}
|
||||
|
||||
namespace aethera::mcp {
|
||||
|
||||
enum struct Tool_Call_Result : std::uint8_t {
|
||||
ok,
|
||||
unknown_tool,
|
||||
invalid_arguments,
|
||||
unknown_plot,
|
||||
rejected
|
||||
};
|
||||
|
||||
struct Tool_Call_Output {
|
||||
Tool_Call_Result result{Tool_Call_Result::ok}; /* 调用的已知业务结果。 */
|
||||
nlohmann::json content{}; /* 成功值或结构化错误细节。 */
|
||||
std::string message{}; /* 供协议适配器显示的简短说明。 */
|
||||
};
|
||||
|
||||
struct Control_Service final {
|
||||
static std::shared_ptr<Control_Service> create();
|
||||
~Control_Service();
|
||||
Control_Service(const Control_Service&) = delete;
|
||||
Control_Service& operator=(const Control_Service&) = delete;
|
||||
|
||||
[[nodiscard]] std::shared_ptr<web::Plot> find_plot(
|
||||
std::string_view id) const;
|
||||
[[nodiscard]] nlohmann::json plot_catalog() const;
|
||||
[[nodiscard]] nlohmann::json tool_catalog() const;
|
||||
[[nodiscard]] Tool_Call_Output call_tool(
|
||||
std::string_view name, const nlohmann::json& arguments);
|
||||
|
||||
private:
|
||||
Control_Service();
|
||||
struct Private;
|
||||
std::unique_ptr<Private> d;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include "runtime/Plot.hpp"
|
||||
#include <unordered_map>
|
||||
|
||||
namespace aethera::mcp {
|
||||
|
||||
struct Control_Service::Private {
|
||||
std::unordered_map<std::string, std::shared_ptr<web::Plot>> plots; /* Plot 唯一实例注册表。 */
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
namespace aethera::mcp {
|
||||
|
||||
template <typename T>
|
||||
struct Type_Tag {
|
||||
using Type = std::remove_cvref_t<T>;
|
||||
};
|
||||
|
||||
template <typename T> struct Boolean_Type;
|
||||
template <typename T> struct Integer_Type;
|
||||
template <typename T> struct Number_Type;
|
||||
template <typename T> struct Text_Type;
|
||||
template <typename T> struct Enum_Type;
|
||||
template <typename T> struct Pair_Type;
|
||||
template <typename T> struct Fixed_Array_Type;
|
||||
template <typename T> struct Sequence_Type;
|
||||
template <typename T> struct Aggregate_Type;
|
||||
struct Json_Value_Type;
|
||||
|
||||
template <typename T>
|
||||
[[nodiscard]] nlohmann::json encode_protocol_value(const T& value);
|
||||
|
||||
template <typename T>
|
||||
void decode_protocol_value(T& value, const nlohmann::json& input);
|
||||
|
||||
template <typename T>
|
||||
[[nodiscard]] nlohmann::json describe_protocol_type();
|
||||
|
||||
template <typename T>
|
||||
[[nodiscard]] constexpr std::string_view protocol_editor() noexcept;
|
||||
|
||||
}
|
||||
|
||||
#include "Protocol_Type.ipp"
|
||||
@@ -0,0 +1,330 @@
|
||||
#pragma once
|
||||
#include <boost/pfr/core.hpp>
|
||||
#include <boost/pfr/core_name.hpp>
|
||||
#include <magic_enum/magic_enum.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <array>
|
||||
#include <concepts>
|
||||
#include <ranges>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace aethera::mcp {
|
||||
|
||||
template <typename T>
|
||||
concept Boolean = std::same_as<std::remove_cvref_t<T>, bool>;
|
||||
|
||||
template <typename T>
|
||||
concept Integer = std::integral<std::remove_cvref_t<T>> && !Boolean<T> &&
|
||||
!std::same_as<std::remove_cvref_t<T>, char>;
|
||||
|
||||
template <typename T>
|
||||
concept Number = std::floating_point<std::remove_cvref_t<T>>;
|
||||
|
||||
template <typename T>
|
||||
concept Text = std::same_as<std::remove_cvref_t<T>, std::string> ||
|
||||
std::same_as<std::remove_cvref_t<T>, char>;
|
||||
|
||||
template <typename T>
|
||||
concept Enum = std::is_enum_v<std::remove_cvref_t<T>>;
|
||||
|
||||
template <typename T>
|
||||
struct Is_Pair : std::false_type {};
|
||||
|
||||
template <typename First, typename Second>
|
||||
struct Is_Pair<std::pair<First, Second>> : std::true_type {};
|
||||
|
||||
template <typename T>
|
||||
concept Pair = Is_Pair<std::remove_cvref_t<T>>::value;
|
||||
|
||||
template <typename T>
|
||||
struct Is_Fixed_Array : std::false_type {};
|
||||
|
||||
template <typename Value, std::size_t Size>
|
||||
struct Is_Fixed_Array<std::array<Value, Size>> : std::true_type {};
|
||||
|
||||
template <typename T>
|
||||
concept Fixed_Array = Is_Fixed_Array<std::remove_cvref_t<T>>::value;
|
||||
|
||||
template <typename T>
|
||||
concept Sequence = std::ranges::range<T> &&
|
||||
requires(T value, typename T::value_type item) {
|
||||
value.clear();
|
||||
value.emplace_back(std::move(item));
|
||||
} && !Text<T> && !Fixed_Array<T>;
|
||||
|
||||
template <Boolean T>
|
||||
[[nodiscard]] auto match_protocol_type(Type_Tag<T>) -> Boolean_Type<T>;
|
||||
|
||||
template <Integer T>
|
||||
[[nodiscard]] auto match_protocol_type(Type_Tag<T>) -> Integer_Type<T>;
|
||||
|
||||
template <Number T>
|
||||
[[nodiscard]] auto match_protocol_type(Type_Tag<T>) -> Number_Type<T>;
|
||||
|
||||
template <Text T>
|
||||
[[nodiscard]] auto match_protocol_type(Type_Tag<T>) -> Text_Type<T>;
|
||||
|
||||
template <Enum T>
|
||||
[[nodiscard]] auto match_protocol_type(Type_Tag<T>) -> Enum_Type<T>;
|
||||
|
||||
template <Pair T>
|
||||
[[nodiscard]] auto match_protocol_type(Type_Tag<T>) -> Pair_Type<T>;
|
||||
|
||||
template <Fixed_Array T>
|
||||
[[nodiscard]] auto match_protocol_type(Type_Tag<T>) -> Fixed_Array_Type<T>;
|
||||
|
||||
template <Sequence T>
|
||||
[[nodiscard]] auto match_protocol_type(Type_Tag<T>) -> Sequence_Type<T>;
|
||||
|
||||
template <typename T>
|
||||
requires std::is_aggregate_v<std::remove_cvref_t<T>> &&
|
||||
(!Boolean<T>) && (!Integer<T>) && (!Number<T>) && (!Text<T>) &&
|
||||
(!Enum<T>) && (!Pair<T>) && (!Fixed_Array<T>) && (!Sequence<T>)
|
||||
[[nodiscard]] auto match_protocol_type(Type_Tag<T>) -> Aggregate_Type<T>;
|
||||
|
||||
[[nodiscard]] auto match_protocol_type(
|
||||
Type_Tag<nlohmann::json>) -> Json_Value_Type;
|
||||
|
||||
template <typename T>
|
||||
using Matched_Protocol_Type = decltype(
|
||||
match_protocol_type(Type_Tag<std::remove_cvref_t<T>>{}));
|
||||
|
||||
struct Json_Value_Type {
|
||||
[[nodiscard]] static nlohmann::json encode(const nlohmann::json& value) {
|
||||
return value;
|
||||
}
|
||||
static void decode(nlohmann::json& value, const nlohmann::json& input) {
|
||||
value = input;
|
||||
}
|
||||
[[nodiscard]] static nlohmann::json describe() { return {}; }
|
||||
[[nodiscard]] static constexpr std::string_view editor() noexcept {
|
||||
return "json";
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
template <typename T, std::size_t... Index>
|
||||
[[nodiscard]] nlohmann::json encode_aggregate(
|
||||
const T& value, std::index_sequence<Index...>) {
|
||||
nlohmann::json result = nlohmann::json::object();
|
||||
((result[std::string(boost::pfr::get_name<Index, T>())] =
|
||||
encode_protocol_value(boost::pfr::get<Index>(value))), ...);
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename T, std::size_t... Index>
|
||||
void decode_aggregate(T& value, const nlohmann::json& input,
|
||||
std::index_sequence<Index...>) {
|
||||
(decode_protocol_value(
|
||||
boost::pfr::get<Index>(value),
|
||||
input.at(std::string(boost::pfr::get_name<Index, T>()))), ...);
|
||||
}
|
||||
|
||||
template <typename T, std::size_t... Index>
|
||||
[[nodiscard]] nlohmann::json describe_aggregate(
|
||||
std::index_sequence<Index...>) {
|
||||
nlohmann::json properties = nlohmann::json::object();
|
||||
nlohmann::json required = nlohmann::json::array();
|
||||
((properties[std::string(boost::pfr::get_name<Index, T>())] =
|
||||
describe_protocol_type<std::remove_cvref_t<
|
||||
decltype(boost::pfr::get<Index>(std::declval<T&>()))>>(),
|
||||
required.push_back(std::string(boost::pfr::get_name<Index, T>()))), ...);
|
||||
return {{"type", "object"}, {"properties", std::move(properties)},
|
||||
{"required", std::move(required)},
|
||||
{"additionalProperties", false}};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
struct Boolean_Type {
|
||||
[[nodiscard]] static nlohmann::json encode(const T& value) { return value; }
|
||||
static void decode(T& value, const nlohmann::json& input) {
|
||||
value = input.template get<T>();
|
||||
}
|
||||
[[nodiscard]] static nlohmann::json describe() { return {{"type", "boolean"}}; }
|
||||
[[nodiscard]] static constexpr std::string_view editor() noexcept { return "boolean"; }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Integer_Type {
|
||||
[[nodiscard]] static nlohmann::json encode(const T& value) { return value; }
|
||||
static void decode(T& value, const nlohmann::json& input) {
|
||||
value = input.template get<T>();
|
||||
}
|
||||
[[nodiscard]] static nlohmann::json describe() { return {{"type", "integer"}}; }
|
||||
[[nodiscard]] static constexpr std::string_view editor() noexcept { return "integer"; }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Number_Type {
|
||||
[[nodiscard]] static nlohmann::json encode(const T& value) { return value; }
|
||||
static void decode(T& value, const nlohmann::json& input) {
|
||||
value = input.template get<T>();
|
||||
}
|
||||
[[nodiscard]] static nlohmann::json describe() { return {{"type", "number"}}; }
|
||||
[[nodiscard]] static constexpr std::string_view editor() noexcept { return "number"; }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Text_Type {
|
||||
[[nodiscard]] static nlohmann::json encode(const T& value) {
|
||||
if constexpr (std::same_as<T, char>) return std::string(1, value);
|
||||
return value;
|
||||
}
|
||||
static void decode(T& value, const nlohmann::json& input) {
|
||||
if constexpr (std::same_as<T, char>) {
|
||||
const auto text = input.template get<std::string>();
|
||||
if (text.size() != 1)
|
||||
throw std::invalid_argument(
|
||||
"character value must contain exactly one character");
|
||||
value = text.front();
|
||||
} else {
|
||||
value = input.template get<T>();
|
||||
}
|
||||
}
|
||||
[[nodiscard]] static nlohmann::json describe() {
|
||||
nlohmann::json result{{"type", "string"}};
|
||||
if constexpr (std::same_as<T, char>) {
|
||||
result["minLength"] = 1;
|
||||
result["maxLength"] = 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
[[nodiscard]] static constexpr std::string_view editor() noexcept { return "text"; }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Enum_Type {
|
||||
[[nodiscard]] static nlohmann::json encode(const T& value) {
|
||||
return magic_enum::enum_name(value);
|
||||
}
|
||||
static void decode(T& value, const nlohmann::json& input) {
|
||||
const auto decoded = magic_enum::enum_cast<T>(
|
||||
input.template get<std::string>());
|
||||
if (!decoded) throw std::invalid_argument("unknown enum value");
|
||||
value = *decoded;
|
||||
}
|
||||
[[nodiscard]] static nlohmann::json describe() {
|
||||
nlohmann::json values = nlohmann::json::array();
|
||||
for (const auto value : magic_enum::enum_values<T>())
|
||||
values.push_back(magic_enum::enum_name(value));
|
||||
return {{"type", "string"}, {"enum", std::move(values)}};
|
||||
}
|
||||
[[nodiscard]] static constexpr std::string_view editor() noexcept { return "select"; }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Pair_Type {
|
||||
[[nodiscard]] static nlohmann::json encode(const T& value) {
|
||||
return nlohmann::json::array({encode_protocol_value(value.first),
|
||||
encode_protocol_value(value.second)});
|
||||
}
|
||||
static void decode(T& value, const nlohmann::json& input) {
|
||||
if (!input.is_array() || input.size() != 2)
|
||||
throw std::invalid_argument("pair value must be a two-item array");
|
||||
decode_protocol_value(value.first, input[0]);
|
||||
decode_protocol_value(value.second, input[1]);
|
||||
}
|
||||
[[nodiscard]] static nlohmann::json describe() {
|
||||
return {{"type", "array"}, {"minItems", 2}, {"maxItems", 2},
|
||||
{"prefixItems", nlohmann::json::array({
|
||||
describe_protocol_type<typename T::first_type>(),
|
||||
describe_protocol_type<typename T::second_type>()})}};
|
||||
}
|
||||
[[nodiscard]] static constexpr std::string_view editor() noexcept { return "json"; }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Fixed_Array_Type {
|
||||
[[nodiscard]] static nlohmann::json encode(const T& value) {
|
||||
nlohmann::json result = nlohmann::json::array();
|
||||
for (const auto& item : value)
|
||||
result.push_back(encode_protocol_value(item));
|
||||
return result;
|
||||
}
|
||||
static void decode(T& value, const nlohmann::json& input) {
|
||||
if (!input.is_array() || input.size() != value.size())
|
||||
throw std::invalid_argument("array has the wrong size");
|
||||
for (std::size_t index = 0; index < value.size(); ++index)
|
||||
decode_protocol_value(value[index], input[index]);
|
||||
}
|
||||
[[nodiscard]] static nlohmann::json describe() {
|
||||
constexpr auto size = std::tuple_size_v<T>;
|
||||
return {{"type", "array"},
|
||||
{"items", describe_protocol_type<typename T::value_type>()},
|
||||
{"minItems", size}, {"maxItems", size}};
|
||||
}
|
||||
[[nodiscard]] static constexpr std::string_view editor() noexcept { return "json"; }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Sequence_Type {
|
||||
[[nodiscard]] static nlohmann::json encode(const T& value) {
|
||||
nlohmann::json result = nlohmann::json::array();
|
||||
for (const auto& item : value)
|
||||
result.push_back(encode_protocol_value(item));
|
||||
return result;
|
||||
}
|
||||
static void decode(T& value, const nlohmann::json& input) {
|
||||
if (!input.is_array())
|
||||
throw std::invalid_argument("value must be an array");
|
||||
T updated;
|
||||
for (const auto& encoded : input) {
|
||||
typename T::value_type item{};
|
||||
decode_protocol_value(item, encoded);
|
||||
updated.emplace_back(std::move(item));
|
||||
}
|
||||
value = std::move(updated);
|
||||
}
|
||||
[[nodiscard]] static nlohmann::json describe() {
|
||||
return {{"type", "array"},
|
||||
{"items", describe_protocol_type<typename T::value_type>()}};
|
||||
}
|
||||
[[nodiscard]] static constexpr std::string_view editor() noexcept { return "json"; }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Aggregate_Type {
|
||||
[[nodiscard]] static nlohmann::json encode(const T& value) {
|
||||
return detail::encode_aggregate(
|
||||
value, std::make_index_sequence<boost::pfr::tuple_size_v<T>>{});
|
||||
}
|
||||
static void decode(T& value, const nlohmann::json& input) {
|
||||
if (!input.is_object())
|
||||
throw std::invalid_argument("value must be an object");
|
||||
detail::decode_aggregate(
|
||||
value, input,
|
||||
std::make_index_sequence<boost::pfr::tuple_size_v<T>>{});
|
||||
}
|
||||
[[nodiscard]] static nlohmann::json describe() {
|
||||
return detail::describe_aggregate<T>(
|
||||
std::make_index_sequence<boost::pfr::tuple_size_v<T>>{});
|
||||
}
|
||||
[[nodiscard]] static constexpr std::string_view editor() noexcept { return "json"; }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
nlohmann::json encode_protocol_value(const T& value) {
|
||||
return Matched_Protocol_Type<T>::encode(value);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void decode_protocol_value(T& value, const nlohmann::json& input) {
|
||||
Matched_Protocol_Type<T>::decode(value, input);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
nlohmann::json describe_protocol_type() {
|
||||
return Matched_Protocol_Type<T>::describe();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
constexpr std::string_view protocol_editor() noexcept {
|
||||
return Matched_Protocol_Type<T>::editor();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include "Protocol_Type.hpp"
|
||||
#include <render_2D/plottable/Plot_Types.hpp>
|
||||
|
||||
namespace aethera::mcp {
|
||||
|
||||
struct Color_Map_Type;
|
||||
|
||||
[[nodiscard]] auto match_protocol_type(
|
||||
Type_Tag<render_2d::Color_Map>) -> Color_Map_Type;
|
||||
|
||||
}
|
||||
|
||||
#include "Render_Types.ipp"
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace aethera::mcp {
|
||||
|
||||
struct Color_Map_Type {
|
||||
[[nodiscard]] static nlohmann::json encode(
|
||||
const render_2d::Color_Map& value) {
|
||||
return {{"stops", encode_protocol_value(value.stops)}};
|
||||
}
|
||||
static void decode(render_2d::Color_Map& value,
|
||||
const nlohmann::json& input) {
|
||||
decode_protocol_value(value.stops, input.at("stops"));
|
||||
}
|
||||
[[nodiscard]] static nlohmann::json describe() {
|
||||
return {{"type", "object"},
|
||||
{"properties", {{"stops",
|
||||
describe_protocol_type<std::vector<Color>>()}}},
|
||||
{"required", nlohmann::json::array({"stops"})},
|
||||
{"additionalProperties", false}};
|
||||
}
|
||||
[[nodiscard]] static constexpr std::string_view editor() noexcept {
|
||||
return "color-map";
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "Gallery_Plots.hpp"
|
||||
#include <array>
|
||||
#include <ranges>
|
||||
|
||||
namespace aethera::web {
|
||||
namespace {
|
||||
constexpr std::array definitions{
|
||||
Plot_Definition{"axes", "坐标轴", "基础组件", "二维数值轴与时间轴布局。", Plot_Dimension::two_d, make_axes_plot},
|
||||
Plot_Definition{"spectrum", "频谱图", "频域分析", "实时频谱、保持曲线和标记。", Plot_Dimension::two_d, make_spectrum_plot},
|
||||
Plot_Definition{"frequency_trace", "频率轨迹", "频域分析", "按时间推进的频率轨迹。", Plot_Dimension::two_d, make_frequency_trace_plot},
|
||||
Plot_Definition{"sweep_spectrum", "扫频图", "频域分析", "分块到达的扫频数据。", Plot_Dimension::two_d, make_sweep_spectrum_plot},
|
||||
Plot_Definition{"afterglow", "余辉图", "密度分析", "带衰减历史的频率功率密度。", Plot_Dimension::two_d, make_afterglow_plot},
|
||||
Plot_Definition{"waterfall", "瀑布图", "密度分析", "分区并行绘制的频谱历史。", Plot_Dimension::two_d, make_waterfall_plot},
|
||||
Plot_Definition{"constellation", "星座图", "信号分析", "调制星座点、参考锚点与选区。", Plot_Dimension::two_d, make_constellation_plot},
|
||||
Plot_Definition{"selection_overlay", "矩形选区", "交互组件", "独立的二维矩形选择覆盖层。", Plot_Dimension::two_d, make_selection_overlay_plot},
|
||||
Plot_Definition{"datoviz_point", "三维点图", "Datoviz 图元", "Datoviz point visual。", Plot_Dimension::three_d, make_datoviz_point_plot},
|
||||
Plot_Definition{"datoviz_splat", "三维高斯 Splat", "Datoviz 图元", "Datoviz Gaussian splat visual。", Plot_Dimension::three_d, make_datoviz_splat_plot},
|
||||
Plot_Definition{"datoviz_pixel", "三维像素", "Datoviz 图元", "Datoviz pixel visual。", Plot_Dimension::three_d, make_datoviz_pixel_plot},
|
||||
Plot_Definition{"datoviz_marker", "三维标记", "Datoviz 图元", "可拾取并显示坐标的 marker visual。", Plot_Dimension::three_d, make_datoviz_marker_plot},
|
||||
Plot_Definition{"datoviz_sphere", "三维球体", "Datoviz 图元", "Datoviz sphere visual。", Plot_Dimension::three_d, make_datoviz_sphere_plot},
|
||||
Plot_Definition{"datoviz_segment", "三维线段", "Datoviz 图元", "Datoviz segment visual。", Plot_Dimension::three_d, make_datoviz_segment_plot},
|
||||
Plot_Definition{"datoviz_vector", "三维向量", "Datoviz 图元", "Datoviz vector visual。", Plot_Dimension::three_d, make_datoviz_vector_plot},
|
||||
Plot_Definition{"datoviz_primitive", "三维图元", "Datoviz 图元", "基础拓扑顶点图元。", Plot_Dimension::three_d, make_datoviz_primitive_plot},
|
||||
Plot_Definition{"datoviz_mesh", "三维网格", "Datoviz 表面", "带法线和纹理坐标的 mesh visual。", Plot_Dimension::three_d, make_datoviz_mesh_plot},
|
||||
Plot_Definition{"datoviz_spectrogram", "三维频谱瀑布", "Datoviz 表面", "动态频谱网格与峰值标记。", Plot_Dimension::three_d, make_datoviz_spectrogram_plot},
|
||||
Plot_Definition{"datoviz_path", "三维路径", "Datoviz 图元", "Datoviz path visual。", Plot_Dimension::three_d, make_datoviz_path_plot},
|
||||
Plot_Definition{"datoviz_image", "三维图像", "Datoviz 纹理", "空间中的采样图像。", Plot_Dimension::three_d, make_datoviz_image_plot},
|
||||
Plot_Definition{"datoviz_labels", "三维标签", "Datoviz 纹理", "分类字段标签 visual。", Plot_Dimension::three_d, make_datoviz_labels_plot},
|
||||
Plot_Definition{"datoviz_glyph", "三维字形", "Datoviz 文本", "可控制几何和 atlas 坐标的 glyph visual。", Plot_Dimension::three_d, make_datoviz_glyph_plot},
|
||||
Plot_Definition{"datoviz_text", "三维文本", "Datoviz 文本", "动态 UTF-8 文本标签。", Plot_Dimension::three_d, make_datoviz_text_plot},
|
||||
Plot_Definition{"datoviz_volume", "三维体数据", "Datoviz 体渲染", "动态标量体字段的最大强度投影。", Plot_Dimension::three_d, make_datoviz_volume_plot},
|
||||
};
|
||||
}
|
||||
|
||||
std::span<const Plot_Definition> gallery_plot_definitions() noexcept {
|
||||
return definitions;
|
||||
}
|
||||
|
||||
const Plot_Definition* find_gallery_plot_definition(
|
||||
std::string_view id) noexcept {
|
||||
const auto found = std::ranges::find(definitions, id,
|
||||
&Plot_Definition::id);
|
||||
return found == definitions.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
std::string_view plot_dimension_name(Plot_Dimension dimension) noexcept {
|
||||
return dimension == Plot_Dimension::three_d ? "3D" : "2D";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include "Gallery_Plots_2D.hpp"
|
||||
#include "Gallery_Plots_3D.hpp"
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
|
||||
namespace aethera::web {
|
||||
enum struct Plot_Dimension : std::uint8_t { two_d, three_d };
|
||||
|
||||
struct Plot_Definition {
|
||||
std::string_view id; /* 协议和路由使用的稳定标识。 */
|
||||
std::string_view title; /* 前端直接展示的业务名称。 */
|
||||
std::string_view category; /* Catalog 分组名称。 */
|
||||
std::string_view description; /* Plot 能力说明。 */
|
||||
Plot_Dimension dimension{Plot_Dimension::two_d}; /* 渲染后端维度。 */
|
||||
std::shared_ptr<Plot> (*create)(); /* 创建该 Plot 唯一实例的工厂。 */
|
||||
};
|
||||
|
||||
[[nodiscard]] std::span<const Plot_Definition>
|
||||
gallery_plot_definitions() noexcept;
|
||||
[[nodiscard]] const Plot_Definition* find_gallery_plot_definition(
|
||||
std::string_view id) noexcept;
|
||||
[[nodiscard]] std::string_view plot_dimension_name(
|
||||
Plot_Dimension dimension) noexcept;
|
||||
}
|
||||
@@ -0,0 +1,929 @@
|
||||
#include "Gallery_Plots_2D.hpp"
|
||||
#include "Renderable_Adapter.hpp"
|
||||
#include <render_2D/plottable/Plottables.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <numbers>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
namespace aethera::web {
|
||||
namespace {
|
||||
using namespace render_2d;
|
||||
using Scene_2D = Render_Scene_2D;
|
||||
using Frequency_Axis_Object = Frequency_Axis;
|
||||
using Numeric_Axis_Object = Numeric_Axis;
|
||||
using Time_Axis_Object = Time_Axis;
|
||||
using Selection_Object = Selection_Rectangle_Overlay;
|
||||
template <typename... Owned_Objects>
|
||||
struct Scene_View_Model final : public Plot::Scene_View {
|
||||
public:
|
||||
struct Data_Generator {
|
||||
nlohmann::json schema;
|
||||
std::function<nlohmann::json(const nlohmann::json &)> generate;
|
||||
std::function<void(const nlohmann::json &, const Plot_Render_Tick &)> advance;
|
||||
explicit operator bool() const noexcept {
|
||||
return static_cast<bool>(generate);
|
||||
}
|
||||
};
|
||||
Scene_View_Model(std::vector<std::unique_ptr<detail::Renderable_Descriptor>> value_descriptors,
|
||||
std::function<void(const Plot_Render_Tick &, bool)> value_update,
|
||||
Data_Generator value_data_generator,
|
||||
Owned_Objects... owned_objects) : descriptors(std::move(value_descriptors)),
|
||||
update_scene(std::move(value_update)),
|
||||
data_generator(std::move(value_data_generator)),
|
||||
objects(std::move(owned_objects)...) {}
|
||||
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", 3}, {"components", std::move(components)}};
|
||||
}
|
||||
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& item) {
|
||||
return item->id() == component;
|
||||
});
|
||||
if (found == descriptors.end()) return {{"success", false}, {"error", "unknown component"}};
|
||||
auto result = (*found)->write_prop(key, value);
|
||||
result["component"] = component;
|
||||
return result;
|
||||
}
|
||||
nlohmann::json component_state(std::string_view component) const override {
|
||||
const auto found = std::ranges::find_if(descriptors, [&](const auto& item) {
|
||||
return item->id() == component;
|
||||
});
|
||||
if (found == descriptors.end())
|
||||
return {{"success", false}, {"error", "unknown component"}};
|
||||
return (*found)->state();
|
||||
}
|
||||
nlohmann::json capture_components(
|
||||
const std::vector<std::string>& components) const override {
|
||||
nlohmann::json result = nlohmann::json::object();
|
||||
for (const auto& component : components) {
|
||||
const auto found = std::ranges::find_if(descriptors, [&](const auto& item) {
|
||||
return item->id() == component;
|
||||
});
|
||||
if (found != descriptors.end())
|
||||
result[component] = (*found)->values();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
nlohmann::json data_generator_schema() const override {
|
||||
if (!data_generator) return nullptr;
|
||||
return data_generator.schema;
|
||||
}
|
||||
nlohmann::json generate_data(const nlohmann::json& input) override {
|
||||
if (!data_generator) return {{"success", false}, {"error", "this plot has no raw data input"}};
|
||||
auto result = data_generator.generate(input);
|
||||
if (result.value("success", false))
|
||||
generated_data.store(std::make_shared<const nlohmann::json>(input),
|
||||
std::memory_order_release);
|
||||
return result;
|
||||
}
|
||||
void update(const Plot_Render_Tick& request) override {
|
||||
const auto input = generated_data.load(std::memory_order_acquire);
|
||||
if (input && data_generator.advance) data_generator.advance(*input, request);
|
||||
update_scene(request, !input);
|
||||
}
|
||||
private:
|
||||
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> descriptors;
|
||||
std::function<void(const Plot_Render_Tick &, bool)> update_scene;
|
||||
Data_Generator data_generator;
|
||||
std::atomic<std::shared_ptr<const nlohmann::json>> generated_data{}; /* 成功生成后发布不可变参数;渲染任务按同一配置持续产生压力数据。 */
|
||||
std::tuple<Owned_Objects...> objects;
|
||||
};
|
||||
template <auto Member, structive::Fixed_String Key, structive::Fixed_String Description>
|
||||
using Prop_Field = detail::Prop_Field<Member, Key, Description>;
|
||||
template <typename Definition, auto Member, structive::Fixed_String Key, structive::Fixed_String Description>
|
||||
using State_Field = detail::State_Field<typename Definition::Base_Tag, Member, Key, Description>;
|
||||
template <typename Object, typename... Fields>
|
||||
std::unique_ptr<detail::Renderable_Descriptor> make_renderable_component(
|
||||
std::string id, std::string label, std::string kind, Object& object) {
|
||||
using Definition = typename Object::Attached_Object;
|
||||
using Adapter = detail::Renderable_Adapter<Object, Fields...,
|
||||
detail::Prop_Field < &Renderable_2D::Prop::cache_enabled, "cache_enabled", "Whether this renderable reuses its complete color result across unchanged frames.">>;
|
||||
return detail::make_renderable_descriptor(std::move(id), std::move(label), std::move(kind), Adapter{object});
|
||||
}
|
||||
template <typename Scene_Object>
|
||||
std::unique_ptr<detail::Renderable_Descriptor> make_scene_component(Scene_Object& scene) {
|
||||
using Definition = typename Scene_Object::Attached_Object;
|
||||
using Prop = typename Definition::Prop;
|
||||
using Adapter = detail::Renderable_Adapter<Scene_Object,
|
||||
detail::Prop_Field < &Prop::viewport, "viewport", "Final scene viewport in physical pixels.">,
|
||||
detail::Prop_Field < &Prop::background, "background", "Scene clear color." >,
|
||||
detail::Prop_Field < &Prop::view_active, "view_active", "Whether the scene publishes rendered frames." >>;
|
||||
return detail::make_renderable_descriptor("scene", "场景", "scene", Adapter{scene});
|
||||
}
|
||||
template <typename Axis_Object>
|
||||
std::unique_ptr<detail::Renderable_Descriptor> make_axis_component(
|
||||
std::string id, std::string label, Axis_Object& axis) {
|
||||
return make_renderable_component<Axis_Object,
|
||||
Prop_Field<&Abs_Axis::Prop::position, "position", "Axis origin in viewport pixels.">,
|
||||
Prop_Field<&Abs_Axis::Prop::pixel_length, "pixel_length", "Signed axis length in pixels.">,
|
||||
Prop_Field<&Abs_Axis::Prop::orientation, "orientation", "Axis orientation.">,
|
||||
Prop_Field<&Abs_Axis::Prop::tick_length, "tick_length", "Major tick length.">,
|
||||
Prop_Field<&Abs_Axis::Prop::sub_tick_length, "sub_tick_length", "Minor tick length.">,
|
||||
Prop_Field<&Abs_Axis::Prop::axis_pen, "axis_pen", "Axis line and tick style.">,
|
||||
Prop_Field<&Abs_Axis::Prop::unit_text, "unit_text", "Axis unit label.">,
|
||||
Prop_Field<&Abs_Axis::Prop::unit_text_font, "unit_text_font", "Axis label font.">,
|
||||
Prop_Field<&Abs_Axis::Prop::unit_text_pen, "unit_text_pen", "Axis label foreground.">,
|
||||
Prop_Field<&Abs_Axis::Prop::unit_text_background_brush, "unit_text_background_brush", "Axis label background.">,
|
||||
Prop_Field<&Abs_Axis::Prop::label_rotation_degrees, "label_rotation_degrees", "Tick label rotation.">,
|
||||
Prop_Field<&Numeric_Axis::Prop::coordinate_range, "coordinate_range", "Visible coordinate range.">,
|
||||
Prop_Field<&Numeric_Axis::Prop::precision, "precision", "Maximum decimal precision.">,
|
||||
Prop_Field<&Numeric_Axis::Prop::locale, "locale", "Numeric label locale.">,
|
||||
Prop_Field<&Numeric_Axis::Prop::wheel_enabled, "wheel_enabled", "Allows wheel zoom.">,
|
||||
Prop_Field<&Numeric_Axis::Prop::drag_enabled, "drag_enabled", "Allows pointer drag panning.">>(
|
||||
std::move(id), std::move(label), "axis", axis);
|
||||
}
|
||||
template <>
|
||||
std::unique_ptr<detail::Renderable_Descriptor> make_axis_component<Time_Axis_Object>(
|
||||
std::string id, std::string label, Time_Axis_Object& axis) {
|
||||
return make_renderable_component<Time_Axis_Object,
|
||||
Prop_Field<&Abs_Axis::Prop::position, "position", "Axis origin in viewport pixels.">,
|
||||
Prop_Field<&Abs_Axis::Prop::pixel_length, "pixel_length", "Signed axis length in pixels.">,
|
||||
Prop_Field<&Abs_Axis::Prop::orientation, "orientation", "Axis orientation.">,
|
||||
Prop_Field<&Abs_Axis::Prop::tick_length, "tick_length", "Major tick length.">,
|
||||
Prop_Field<&Abs_Axis::Prop::sub_tick_length, "sub_tick_length", "Minor tick length.">,
|
||||
Prop_Field<&Abs_Axis::Prop::axis_pen, "axis_pen", "Axis line and tick style.">,
|
||||
Prop_Field<&Abs_Axis::Prop::unit_text, "unit_text", "Axis unit label.">,
|
||||
Prop_Field<&Abs_Axis::Prop::unit_text_font, "unit_text_font", "Axis label font.">,
|
||||
Prop_Field<&Abs_Axis::Prop::unit_text_pen, "unit_text_pen", "Axis label foreground.">,
|
||||
Prop_Field<&Abs_Axis::Prop::unit_text_background_brush, "unit_text_background_brush", "Axis label background.">,
|
||||
Prop_Field<&Abs_Axis::Prop::label_rotation_degrees, "label_rotation_degrees", "Tick label rotation.">,
|
||||
Prop_Field<&Time_Axis::Prop::visible_count, "visible_count", "Maximum visible time samples.">,
|
||||
Prop_Field<&Time_Axis::Prop::tick_label_spacing_px, "tick_label_spacing_px", "Spacing between time labels.">,
|
||||
Prop_Field<&Time_Axis::Prop::estimated_label_width_px, "estimated_label_width_px", "Estimated time label width.">,
|
||||
Prop_Field<&Time_Axis::Prop::format, "format", "Time label format.">,
|
||||
Prop_Field<&Time_Axis::Prop::newest_at_start, "newest_at_start", "Places the newest time at the range origin.">,
|
||||
State_Field<Time_Axis, &Time_Axis::State::next_tick, "next_tick", "Next allocated time tick.">>(
|
||||
std::move(id), std::move(label), "axis", axis);
|
||||
}
|
||||
using Json = nlohmann::json;
|
||||
Json generator_number_field(std::string key, std::string label, std::string description,
|
||||
double value, double minimum, double maximum, double step = 0.01) {
|
||||
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 generator_integer_field(std::string key, std::string label, std::string description,
|
||||
std::size_t value, std::size_t maximum = 1'000'000) {
|
||||
auto field = generator_number_field(std::move(key), std::move(label), std::move(description),
|
||||
static_cast<double>(value), 1.0, static_cast<double>(maximum), 1.0);
|
||||
field["editor"] = "integer";
|
||||
return field;
|
||||
}
|
||||
double generator_number(const Json& input, std::string_view key) {
|
||||
const auto& value = input.at(std::string(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 generator_count(const Json& input, std::string_view key, std::size_t maximum = 1'000'000) {
|
||||
const auto value = generator_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<double, double> generator_range(const Json& input, std::string_view minimum_key, std::string_view maximum_key) {
|
||||
const auto minimum = generator_number(input, minimum_key);
|
||||
const auto maximum = generator_number(input, maximum_key);
|
||||
if (minimum >= maximum) throw std::invalid_argument(std::string(maximum_key) + " must be greater than " + std::string(minimum_key));
|
||||
return {minimum, maximum};
|
||||
}
|
||||
void generate_spectral_row(std::vector<Plot_Value>& values, std::size_t row,
|
||||
std::size_t signal_count, double minimum, double maximum,
|
||||
double noise_standard_deviation, std::mt19937_64& engine) {
|
||||
if (noise_standard_deviation < 0.0) throw std::invalid_argument("noise_stddev must not be negative");
|
||||
std::normal_distribution<double> noise(0.0, noise_standard_deviation);
|
||||
const double span = maximum - minimum;
|
||||
const double denominator = static_cast<double>(std::max<std::size_t>(1, values.size() - 1));
|
||||
for (std::size_t index = 0; index < values.size(); ++index) {
|
||||
const double x = static_cast<double>(index) / denominator;
|
||||
double value = minimum + span * 0.10 + noise(engine);
|
||||
for (std::size_t signal = 0; signal < signal_count; ++signal) {
|
||||
const double phase = static_cast<double>(signal + 1) / static_cast<double>(signal_count + 1);
|
||||
const double center = std::clamp(phase + 0.035 * std::sin(row * 0.09 + signal * 1.73), 0.01, 0.99);
|
||||
const double width = 0.003 + 0.018 * static_cast<double>((signal % 5) + 1) / 5.0;
|
||||
const double distance = (x - center) / width;
|
||||
value += span * (0.45 + 0.45 * std::sin(signal * 2.17 + row * 0.037)) * std::exp(-0.5 * distance * distance);
|
||||
}
|
||||
values[index] = std::clamp(value, minimum, maximum);
|
||||
}
|
||||
}
|
||||
template <typename Definition>
|
||||
Json generator_2d_schema() {
|
||||
Json fields = Json::array();
|
||||
std::string label;
|
||||
std::string description;
|
||||
if constexpr (std::same_as<Definition, Spectrum>) {
|
||||
label = "生成频谱采样";
|
||||
description = "生成一条含可控噪声底和多个窄带谱峰的完整功率频谱。";
|
||||
fields.push_back(generator_integer_field("sample_count", "频谱采样点数", "一次频谱更新包含的功率采样点数。", 4096));
|
||||
fields.push_back(generator_number_field("power_min", "功率下界", "噪声底和谱峰最终裁剪的功率下界。", -110.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_number_field("power_max", "功率上界", "谱峰最终裁剪的功率上界,必须大于下界。", -20.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_integer_field("signal_count", "窄带信号数", "叠加在噪声底上的漂移高斯谱峰数量。", 8, 256));
|
||||
fields.push_back(generator_number_field("noise_stddev", "噪声标准差", "功率噪声的标准差,单位与功率值一致。", 2.0, 0.0, 1'000'000.0));
|
||||
}
|
||||
else if constexpr (std::same_as<Definition, Frequency_Trace>) {
|
||||
label = "生成频率轨迹";
|
||||
description = "按时间顺序生成一组轨迹采样。";
|
||||
fields.push_back(generator_integer_field("sample_count", "轨迹采样点数", "时间有序的轨迹点数量。", 4096));
|
||||
fields.push_back(generator_number_field("value_min", "轨迹值下界", "随机轨迹值下界。", -1.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_number_field("value_max", "轨迹值上界", "随机轨迹值上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_integer_field("tick_step", "时间刻度步长", "相邻样本之间的整数时间刻度差。", 1, 1'000'000));
|
||||
}
|
||||
else if constexpr (std::same_as<Definition, Sweep_Spectrum>) {
|
||||
label = "生成分块扫频";
|
||||
description = "按块数与每块频点数生成一条完整扫频曲线。";
|
||||
fields.push_back(generator_integer_field("block_count", "扫频块数", "组成一次完整扫频的块数量。", 64, 65'536));
|
||||
fields.push_back(generator_integer_field("bins_per_block", "每块频点数", "每个扫频块保存的连续频点数量。", 8, 65'536));
|
||||
fields.push_back(generator_number_field("power_min", "功率下界", "随机扫频功率下界。", -110.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_number_field("power_max", "功率上界", "随机扫频功率上界,必须大于下界。", -20.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_integer_field("signal_count", "窄带信号数", "跨扫频块连续分布的谱峰数量。", 8, 256));
|
||||
fields.push_back(generator_number_field("noise_stddev", "噪声标准差", "扫频噪声底标准差。", 2.0, 0.0, 1'000'000.0));
|
||||
}
|
||||
else if constexpr (std::same_as<Definition, Afterglow>) {
|
||||
label = "生成余辉历史";
|
||||
description = "生成多帧频谱历史,用于测试余辉累积和衰减。";
|
||||
fields.push_back(generator_integer_field("history_count", "历史频谱帧数", "余辉中保留的历史频谱数量。", 32, 4096));
|
||||
fields.push_back(generator_integer_field("samples_per_spectrum", "每帧采样点数", "每条历史频谱包含的功率采样点数。", 512, 65'536));
|
||||
fields.push_back(generator_number_field("power_min", "功率下界", "随机功率值下界。", -110.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_number_field("power_max", "功率上界", "随机功率值上界,必须大于下界。", -20.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_integer_field("signal_count", "漂移信号数", "在历史帧之间连续漂移的谱峰数量。", 8, 256));
|
||||
fields.push_back(generator_number_field("noise_stddev", "噪声标准差", "历史频谱噪声底标准差。", 2.0, 0.0, 1'000'000.0));
|
||||
}
|
||||
else if constexpr (std::same_as<Definition, Waterfall>) {
|
||||
label = "生成瀑布图历史";
|
||||
description = "生成带时间刻度的多行频谱数据。";
|
||||
fields.push_back(generator_integer_field("row_count", "瀑布行数", "瀑布图中保存的时间行数量。", 256, 4096));
|
||||
fields.push_back(generator_integer_field("bins_per_row", "每行频点数", "每一时间行包含的频率采样点数。", 512, 65'536));
|
||||
fields.push_back(generator_number_field("power_min", "功率下界", "随机功率值下界。", -110.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_number_field("power_max", "功率上界", "随机功率值上界,必须大于下界。", -20.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_integer_field("signal_count", "漂移信号数", "沿时间行移动的窄带谱峰数量。", 8, 256));
|
||||
fields.push_back(generator_number_field("noise_stddev", "噪声标准差", "瀑布噪声底标准差。", 2.0, 0.0, 1'000'000.0));
|
||||
}
|
||||
else if constexpr (std::same_as<Definition, Constellation_Diagram>) {
|
||||
label = "生成星座采样";
|
||||
description = "分别按 I/Q 坐标范围生成随机星座点。";
|
||||
fields.push_back(generator_integer_field("point_count", "星座点数", "本次写入的 I/Q 采样数量。", 10'000));
|
||||
fields.push_back(generator_number_field("i_min", "I 坐标下界", "同相分量随机范围下界。", -1.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_number_field("i_max", "I 坐标上界", "同相分量随机范围上界。", 1.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_number_field("q_min", "Q 坐标下界", "正交分量随机范围下界。", -1.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_number_field("q_max", "Q 坐标上界", "正交分量随机范围上界。", 1.0, -1'000'000.0, 1'000'000.0));
|
||||
}
|
||||
else if constexpr (std::same_as<Definition, Selection_Rectangle_Overlay>) {
|
||||
label = "生成矩形选区";
|
||||
description = "按 X/Y 坐标范围生成随机矩形区域。";
|
||||
fields.push_back(generator_integer_field("region_count", "矩形数量", "本次写入的选区数量。", 128));
|
||||
fields.push_back(generator_number_field("x_min", "X 坐标下界", "矩形起点 X 随机范围下界。", 0.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_number_field("x_max", "X 坐标上界", "矩形终点 X 随机范围上界。", 100.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_number_field("y_min", "Y 坐标下界", "矩形起点 Y 随机范围下界。", 0.0, -1'000'000.0, 1'000'000.0));
|
||||
fields.push_back(generator_number_field("y_max", "Y 坐标上界", "矩形终点 Y 随机范围上界。", 100.0, -1'000'000.0, 1'000'000.0));
|
||||
}
|
||||
if (!fields.empty()) {
|
||||
fields.push_back(generator_integer_field("seed", "随机种子", "固定种子可重现同一压力数据集,便于对比不同帧策略和像素传输模式。", 42, 4'294'967'295ULL));
|
||||
fields.push_back(generator_integer_field(
|
||||
"update_every_n_frames", "更新帧间隔",
|
||||
"每隔多少个渲染帧重新生成一次本图压力数据;1 表示每帧更新。",
|
||||
1, 100'000));
|
||||
}
|
||||
return {{"label", std::move(label)}, {"description", std::move(description) + " 可通过数据规模与坐标/数值范围构造可重复的压力负载。"}, {"fields", std::move(fields)}};
|
||||
}
|
||||
template <typename Object>
|
||||
nlohmann::json generate_2d_data(Object& object, const Json& input) {
|
||||
using Definition = typename Object::Attached_Object;
|
||||
try {
|
||||
std::mt19937_64 engine{generator_count(input, "seed", 4'294'967'295ULL)};
|
||||
const auto animation_row = input.value("_animation_row", std::size_t{});
|
||||
std::size_t generated_count{};
|
||||
if constexpr (std::same_as<Definition, Spectrum>) {
|
||||
const auto count = generator_count(input, "sample_count");
|
||||
const auto [minimum, maximum] = generator_range(input, "power_min", "power_max");
|
||||
std::vector<Plot_Value> values(count);
|
||||
generate_spectral_row(values, animation_row, generator_count(input, "signal_count", 256), minimum, maximum, generator_number(input, "noise_stddev"), engine);
|
||||
object.template pending_buffer<Spectrum_Frame_Tag>() = Spectrum_Frame{std::move(values)};
|
||||
object.template mark_dirty<Render_Graph_Tag>();
|
||||
generated_count = count;
|
||||
}
|
||||
else if constexpr (std::same_as<Definition, Frequency_Trace>) {
|
||||
const auto count = generator_count(input, "sample_count");
|
||||
const auto tick_step = generator_count(input, "tick_step");
|
||||
const auto [minimum, maximum] = generator_range(input, "value_min", "value_max");
|
||||
std::uniform_real_distribution<double> distribution(minimum, maximum);
|
||||
std::vector<Frequency_Trace_Sample> samples(count);
|
||||
for (std::size_t index = 0; index < count; ++index) samples[index] = {static_cast<Plot_Time_Tick>(index * tick_step), distribution(engine)};
|
||||
for (const auto& sample : samples) object.template submit_stream<Frequency_Trace_Stream_Tag>(sample);
|
||||
object.template mark_dirty<Render_Graph_Tag>();
|
||||
generated_count = count;
|
||||
}
|
||||
else if constexpr (std::same_as<Definition, Sweep_Spectrum>) {
|
||||
const auto block_count = generator_count(input, "block_count", 65'536);
|
||||
const auto width = generator_count(input, "bins_per_block", 65'536);
|
||||
if (block_count > 1'000'000 / width) throw std::invalid_argument("sweep data exceeds 1,000,000 samples");
|
||||
const auto [minimum, maximum] = generator_range(input, "power_min", "power_max");
|
||||
std::vector<std::vector<Plot_Value>> blocks(block_count, std::vector<Plot_Value>(width));
|
||||
std::vector<Plot_Value> complete(block_count * width);
|
||||
generate_spectral_row(complete, animation_row, generator_count(input, "signal_count", 256), minimum, maximum, generator_number(input, "noise_stddev"), engine);
|
||||
for (std::size_t block = 0; block < block_count; ++block) std::ranges::copy_n(complete.begin() + block * width, width, blocks[block].begin());
|
||||
object.template set<&Sweep_Spectrum::Prop::bins_per_block>(width);
|
||||
object.template set<&Sweep_Spectrum::Prop::block_count>(block_count);
|
||||
for (std::size_t block_index = 0; block_index < blocks.size(); ++block_index)
|
||||
object.template submit_stream<Sweep_Spectrum_Stream_Tag>(
|
||||
std::make_shared<const Sweep_Spectrum_Block>(
|
||||
Sweep_Spectrum_Block{block_index, std::move(blocks[block_index])}));
|
||||
object.template mark_dirty<Render_Graph_Tag>();
|
||||
generated_count = block_count * width;
|
||||
}
|
||||
else if constexpr (std::same_as<Definition, Afterglow>) {
|
||||
const auto row_count = generator_count(input, "history_count", 4096);
|
||||
const auto width = generator_count(input, "samples_per_spectrum", 65'536);
|
||||
if (row_count > 1'000'000 / width) throw std::invalid_argument("afterglow data exceeds 1,000,000 samples");
|
||||
const auto [minimum, maximum] = generator_range(input, "power_min", "power_max");
|
||||
std::vector<std::vector<Plot_Value>> spectra(row_count, std::vector<Plot_Value>(width));
|
||||
const auto signal_count = generator_count(input, "signal_count", 256);
|
||||
const auto noise_stddev = generator_number(input, "noise_stddev");
|
||||
for (std::size_t row = 0; row < row_count; ++row) generate_spectral_row(spectra[row], animation_row + row, signal_count, minimum, maximum, noise_stddev, engine);
|
||||
for (auto& spectrum : spectra)
|
||||
object.template submit_stream<Afterglow_Stream_Tag>(
|
||||
std::make_shared<const std::vector<Plot_Value>>(std::move(spectrum)));
|
||||
object.template mark_dirty<Render_Graph_Tag>();
|
||||
generated_count = row_count * width;
|
||||
}
|
||||
else if constexpr (std::same_as<Definition, Waterfall>) {
|
||||
const auto row_count = generator_count(input, "row_count", 4096);
|
||||
const auto width = generator_count(input, "bins_per_row", 65'536);
|
||||
if (row_count > 1'000'000 / width) throw std::invalid_argument("waterfall data exceeds 1,000,000 samples");
|
||||
const auto [minimum, maximum] = generator_range(input, "power_min", "power_max");
|
||||
std::vector<Waterfall_Row> rows;
|
||||
rows.reserve(row_count);
|
||||
const auto signal_count = generator_count(input, "signal_count", 256);
|
||||
const auto noise_stddev = generator_number(input, "noise_stddev");
|
||||
for (std::size_t row = 0; row < row_count; ++row) {
|
||||
std::vector<Plot_Value> row_values(width);
|
||||
generate_spectral_row(row_values, animation_row + row, signal_count, minimum, maximum, noise_stddev, engine);
|
||||
rows.push_back({static_cast<Plot_Time_Tick>(row), std::move(row_values)});
|
||||
}
|
||||
object.template set<&Waterfall::Prop::frequency_bin_count>(width);
|
||||
for (auto& row : rows)
|
||||
object.template submit_stream<Waterfall_Stream_Tag>(
|
||||
std::make_shared<const Waterfall_Row>(std::move(row)));
|
||||
object.template mark_dirty<Render_Graph_Tag>();
|
||||
generated_count = row_count * width;
|
||||
}
|
||||
else if constexpr (std::same_as<Definition, Constellation_Diagram>) {
|
||||
const auto count = generator_count(input, "point_count");
|
||||
const auto [i_min, i_max] = generator_range(input, "i_min", "i_max");
|
||||
const auto [q_min, q_max] = generator_range(input, "q_min", "q_max");
|
||||
std::uniform_real_distribution<double> i_distribution(i_min, i_max), q_distribution(q_min, q_max);
|
||||
std::vector<Constellation_Point> points(count);
|
||||
const auto submitted = monotonic_milliseconds();
|
||||
for (std::size_t index = 0; index < count; ++index) points[index] = {{i_distribution(engine), q_distribution(engine)}, submitted};
|
||||
for (const auto& point : points) object.template submit_stream<Constellation_Stream_Tag>(point);
|
||||
object.template mark_dirty<Render_Graph_Tag>();
|
||||
generated_count = count;
|
||||
}
|
||||
else if constexpr (std::same_as<Definition, Selection_Rectangle_Overlay>) {
|
||||
const auto count = generator_count(input, "region_count");
|
||||
const auto [x_min, x_max] = generator_range(input, "x_min", "x_max");
|
||||
const auto [y_min, y_max] = generator_range(input, "y_min", "y_max");
|
||||
std::uniform_real_distribution<double> x_distribution(x_min, x_max), y_distribution(y_min, y_max);
|
||||
std::vector<Axis_Rectangle> regions(count);
|
||||
for (auto& region : regions) {
|
||||
const auto first_x = x_distribution(engine), second_x = x_distribution(engine);
|
||||
const auto first_y = y_distribution(engine), second_y = y_distribution(engine);
|
||||
region = {
|
||||
{std::min(first_x, second_x), std::max(first_x, second_x)},
|
||||
{std::min(first_y, second_y), std::max(first_y, second_y)}
|
||||
};
|
||||
}
|
||||
object.template set<&Selection_Rectangle_Overlay::Prop::selected_regions>(std::move(regions));
|
||||
generated_count = count;
|
||||
}
|
||||
else {
|
||||
return {{"success", false}, {"error", "this plot has no raw data input"}};
|
||||
}
|
||||
return {{"success", true}, {"generated_count", generated_count}};
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
return {{"success", false}, {"error", error.what()}};
|
||||
}
|
||||
}
|
||||
template <typename... Fields, typename Object, typename... Owned_Objects>
|
||||
std::unique_ptr<Plot::Scene_View> make_scene_view(
|
||||
Object & object,
|
||||
Scene_2D & scene,
|
||||
std::function < void(const Plot_Render_Tick &, bool) > update,
|
||||
Owned_Objects &&... owned_objects) {
|
||||
using Definition = typename Object::Attached_Object;
|
||||
using Tag = typename Definition::Base_Tag;
|
||||
using State = typename Definition::State;
|
||||
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> components;
|
||||
components.push_back(make_scene_component(scene));
|
||||
components.push_back(make_renderable_component<Object, Fields...>("plot", "主绘图组件", "renderable", object));
|
||||
std::size_t axis_index{};
|
||||
const auto append_owned = [&](const auto& owned) {
|
||||
using Owned = std::remove_cvref_t<decltype(owned)>;
|
||||
if constexpr (std::same_as<typename Owned::element_type, Frequency_Axis_Object>
|
||||
|| std::same_as<typename Owned::element_type, Numeric_Axis_Object>
|
||||
|| std::same_as<typename Owned::element_type, Time_Axis_Object>) {
|
||||
const auto id = axis_index++ == 0 ? "axis-x" : "axis-y";
|
||||
components.push_back(make_axis_component(id, id == std::string_view{"axis-x"} ? "横向坐标轴" : "纵向坐标轴", *owned));
|
||||
}
|
||||
else if constexpr (std::same_as<typename Owned::element_type, Selection_Object> && !std::same_as<Object, Selection_Object>) {
|
||||
components.push_back(make_renderable_component<Selection_Object,
|
||||
Prop_Field<&Selection_Rectangle_Overlay::Prop::label_font, "label_font", "Font used for labels attached to selected regions.">,
|
||||
Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen", "Pen used to draw selected-region label text.">,
|
||||
Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush", "Brush used to fill selected rectangular regions.">,
|
||||
Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen", "Pen used to draw selected-region borders.">,
|
||||
State_Field<Selection_Rectangle_Overlay, &Selection_Rectangle_Overlay::State::selected_region_count, "selected_region_count", "Number of rectangular regions currently selected.">>("selection", "矩形选区", "overlay", *owned));
|
||||
}
|
||||
};
|
||||
(append_owned(owned_objects), ...);
|
||||
return std::make_unique<Scene_View_Model<std::remove_cvref_t<Owned_Objects>...>>(
|
||||
std::move(components),
|
||||
std::move(update),
|
||||
typename Scene_View_Model<std::remove_cvref_t<Owned_Objects>...>::Data_Generator{
|
||||
generator_2d_schema<Definition>(),
|
||||
[&object](const nlohmann::json& input) {
|
||||
return generate_2d_data(object, input);
|
||||
},
|
||||
[&object](const nlohmann::json& input,
|
||||
const Plot_Render_Tick& tick) {
|
||||
const auto interval = generator_count(
|
||||
input, "update_every_n_frames", 100'000);
|
||||
if (tick.sequence % interval != 0) return;
|
||||
auto frame_input = input;
|
||||
constexpr std::uint64_t maximum_seed{4'294'967'295ULL};
|
||||
const auto base_seed = generator_count(
|
||||
input, "seed", maximum_seed);
|
||||
frame_input["seed"] =
|
||||
1 + (base_seed - 1 + tick.sequence) % maximum_seed;
|
||||
frame_input["_animation_row"] = tick.sequence;
|
||||
const auto result = generate_2d_data(object, frame_input);
|
||||
if (!result.value("success", false))
|
||||
throw std::runtime_error(result.value(
|
||||
"error", "continuous 2D data generation failed"));
|
||||
}
|
||||
},
|
||||
std::forward<Owned_Objects>(owned_objects)...);
|
||||
}
|
||||
std::unique_ptr<Frequency_Axis_Object> make_frequency_axis() {
|
||||
auto result = Frequency_Axis_Object::Builder<Frequency_Axis_Object>{}
|
||||
.set(&Abs_Axis::Prop::orientation, Axis_Orientation::horizontal)
|
||||
.set(&Abs_Axis::Prop::position, Point_F{64.0, 370.0})
|
||||
.set(&Abs_Axis::Prop::pixel_length, 620.0)
|
||||
.set(&Numeric_Axis::Prop::coordinate_range, Axis_Range{0.0, 100.0})
|
||||
.build();
|
||||
if (!result) throw std::logic_error("frequency axis dependency graph is invalid");
|
||||
return std::move(result).value();
|
||||
}
|
||||
std::unique_ptr<Numeric_Axis_Object> make_numeric_axis(
|
||||
Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length,
|
||||
Axis_Range range) {
|
||||
auto result = Numeric_Axis_Object::Builder<Numeric_Axis_Object>{}
|
||||
.set(&Abs_Axis::Prop::orientation, orientation)
|
||||
.set(&Abs_Axis::Prop::position, position)
|
||||
.set(&Abs_Axis::Prop::pixel_length, length)
|
||||
.set(&Numeric_Axis::Prop::coordinate_range, range)
|
||||
.build();
|
||||
if (!result) throw std::logic_error("numeric axis dependency graph is invalid");
|
||||
return std::move(result).value();
|
||||
}
|
||||
std::unique_ptr<Time_Axis_Object> make_time_axis(
|
||||
Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length) {
|
||||
auto result = Time_Axis_Object::Builder<Time_Axis_Object>{}
|
||||
.set(&Abs_Axis::Prop::orientation, orientation)
|
||||
.set(&Abs_Axis::Prop::position, position)
|
||||
.set(&Abs_Axis::Prop::pixel_length, length)
|
||||
.build();
|
||||
if (!result) throw std::logic_error("time axis dependency graph is invalid");
|
||||
return std::move(result).value();
|
||||
}
|
||||
template <Axis_Object Horizontal_Axis, Axis_Object Vertical_Axis>
|
||||
std::unique_ptr<Selection_Object> selection_overlay(Horizontal_Axis* horizontal_axis, Vertical_Axis* vertical_axis) {
|
||||
auto result = Selection_Object::Builder<Selection_Object>(horizontal_axis, vertical_axis).build();
|
||||
if (!result) throw std::logic_error("selection overlay axes form an invalid dependency graph");
|
||||
return std::move(result).value();
|
||||
}
|
||||
template <typename... Axes>
|
||||
void resize_axes(Scene_2D* scene, Size viewport, Axes*... axes) {
|
||||
const Size previous_viewport = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport;
|
||||
if (previous_viewport == viewport || previous_viewport.empty()) return;
|
||||
const auto resize_axis = [&](auto* axis) {
|
||||
const auto layout = axis->template read_prop<Abs_Axis::Base_Tag>();
|
||||
const double horizontal_scale = static_cast<double>(viewport.width) / previous_viewport.width;
|
||||
const double vertical_scale = static_cast<double>(viewport.height) / previous_viewport.height;
|
||||
axis->template set<&Abs_Axis::Prop::position>(Point_F{
|
||||
layout.position.x * horizontal_scale,
|
||||
layout.position.y * vertical_scale
|
||||
});
|
||||
axis->template set<&Abs_Axis::Prop::pixel_length>(layout.pixel_length *
|
||||
(layout.orientation == Axis_Orientation::horizontal ? horizontal_scale : vertical_scale));
|
||||
};
|
||||
(resize_axis(axes), ...);
|
||||
}
|
||||
}
|
||||
std::shared_ptr<Plot> make_axes_plot() {
|
||||
constexpr Size canvas{720, 420};
|
||||
auto frequency = make_frequency_axis();
|
||||
frequency->template set<&Abs_Axis::Prop::unit_text>("Hz");
|
||||
auto numeric = make_numeric_axis(
|
||||
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-100.0, 0.0});
|
||||
numeric->template set<&Abs_Axis::Prop::unit_text>("dB");
|
||||
auto time = make_time_axis(
|
||||
Axis_Orientation::horizontal, {64.0, 190.0}, 620.0);
|
||||
time->template set<&Abs_Axis::Prop::unit_text>("Time");
|
||||
auto scene = *Scene_2D::Builder<Scene_2D>{}
|
||||
.set(&Render_Scene_2D::Prop::viewport, canvas)
|
||||
.set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255})
|
||||
.set(&Render_Scene_2D::Prop::view_active, true)
|
||||
.add_dependency_node<Render_Graph_Tag>(frequency.get())
|
||||
.add_dependency_node<Render_Graph_Tag>(numeric.get())
|
||||
.add_dependency_node<Render_Graph_Tag>(time.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Plot_Render_Tick& event, bool) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, numeric, time);
|
||||
constexpr double day_milliseconds = 86'400'000.0;
|
||||
time->append_time(Time_Of_Day{
|
||||
static_cast<std::int64_t>(
|
||||
std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds))
|
||||
});
|
||||
};
|
||||
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> components;
|
||||
components.push_back(make_scene_component(*scene));
|
||||
components.push_back(make_axis_component("axis-frequency", "频率轴", *frequency));
|
||||
components.push_back(make_axis_component("axis-value", "数值轴", *numeric));
|
||||
components.push_back(make_axis_component("axis-time", "时间轴", *time));
|
||||
Json generator_fields = Json::array({
|
||||
generator_integer_field("time_sample_count", "时间样本数", "写入时间轴保留窗口的连续时间样本数量。", 16'384),
|
||||
generator_integer_field("time_step_ms", "时间步长 (ms)", "相邻时间样本之间的毫秒间隔。", 10),
|
||||
generator_number_field("frequency_min", "频率下界", "频率轴可见坐标下界。", 0.0, -1'000'000'000.0, 1'000'000'000.0),
|
||||
generator_number_field("frequency_max", "频率上界", "频率轴可见坐标上界,必须大于下界。", 100.0, -1'000'000'000.0, 1'000'000'000.0),
|
||||
generator_number_field("value_min", "数值下界", "数值轴可见坐标下界。", -100.0, -1'000'000'000.0, 1'000'000'000.0),
|
||||
generator_number_field("value_max", "数值上界", "数值轴可见坐标上界,必须大于下界。", 0.0, -1'000'000'000.0, 1'000'000'000.0)
|
||||
});
|
||||
auto generate = [frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Json& input) {
|
||||
try {
|
||||
const auto sample_count = generator_count(input, "time_sample_count");
|
||||
const auto time_step = generator_count(input, "time_step_ms");
|
||||
const auto [frequency_minimum, frequency_maximum] = generator_range(input, "frequency_min", "frequency_max");
|
||||
const auto [value_minimum, value_maximum] = generator_range(input, "value_min", "value_max");
|
||||
frequency->template set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{frequency_minimum, frequency_maximum});
|
||||
numeric->template set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{value_minimum, value_maximum});
|
||||
time->template set<&Time_Axis::Prop::visible_count>(static_cast<Axis_Visible_Count>(sample_count));
|
||||
constexpr std::uint64_t day_milliseconds = 86'400'000;
|
||||
for (std::size_t index = 0; index < sample_count; ++index) time->append_time(Time_Of_Day{static_cast<std::int64_t>((index * time_step) % day_milliseconds)});
|
||||
return Json{{"success", true}, {"generated_count", sample_count}};
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
return Json{{"success", false}, {"error", error.what()}};
|
||||
}
|
||||
};
|
||||
auto view = std::make_unique<Scene_View_Model<decltype(frequency), decltype(numeric), decltype(time)>>(
|
||||
std::move(components), std::move(update),
|
||||
Scene_View_Model<decltype(frequency), decltype(numeric), decltype(time)>::Data_Generator{
|
||||
Json{
|
||||
{"label", "生成坐标轴压力数据"},
|
||||
{"description", "按时间样本规模和三个业务坐标范围生成可重复的坐标轴压力负载。"},
|
||||
{"fields", std::move(generator_fields)}
|
||||
},
|
||||
std::move(generate), {}
|
||||
},
|
||||
std::move(frequency), std::move(numeric), std::move(time));
|
||||
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> make_spectrum_plot() {
|
||||
constexpr Size canvas{720, 420};
|
||||
auto frequency = make_frequency_axis();
|
||||
auto vertical = make_numeric_axis(Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0});
|
||||
auto spectrum = *Spectrum::Builder<Spectrum>(frequency.get(), vertical.get(), 4u)
|
||||
.set(&Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0})
|
||||
.set(&Spectrum::Prop::max_hold_visible, true)
|
||||
.build();
|
||||
auto selection = selection_overlay(frequency.get(), vertical.get());
|
||||
auto scene = *Scene_2D::Builder<Scene_2D>()
|
||||
.set(&Render_Scene_2D::Prop::viewport, canvas)
|
||||
.set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255})
|
||||
.set(&Render_Scene_2D::Prop::view_active, true)
|
||||
.add_renderable(spectrum.get())
|
||||
.add_renderable(selection.get())
|
||||
.add_dependency<Render_Graph_Tag>(selection.get(), spectrum.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Render_Tick& event, bool demo_data) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
if (!demo_data) return;
|
||||
std::array < double, 256 > samples{};
|
||||
for (std::size_t i = 0; i < samples.size(); ++i) {
|
||||
const double x = static_cast<double>(i) / samples.size();
|
||||
samples[i] = -92.0 + 54.0 * std::exp(-180.0 * std::pow(x - 0.28 - 0.03 * std::sin(event.time_milliseconds * 0.001), 2.0))
|
||||
+ 42.0 * std::exp(-260.0 * std::pow(x - 0.68, 2.0))
|
||||
+ 2.5 * std::sin(i * 0.31 + event.time_milliseconds * 0.004);
|
||||
}
|
||||
raw->template pending_buffer<Spectrum_Frame_Tag>() =
|
||||
Spectrum_Frame{std::vector<Spectrum_Power>(samples.begin(), samples.end())};
|
||||
raw->template mark_dirty<Render_Graph_Tag>();
|
||||
};
|
||||
auto view = make_scene_view<
|
||||
Prop_Field<&Spectrum::Prop::center_frequency, "center_frequency", "Frequency placed at the visual center of the spectrum axis.">,
|
||||
Prop_Field<&Spectrum::Prop::partition_count, "partition_count", "Number of partitions used to prepare and render spectrum samples.">,
|
||||
Prop_Field<&Spectrum::Prop::max_hold_visible, "max_hold_visible", "Shows the accumulated maximum-hold spectrum curve when enabled.">,
|
||||
Prop_Field<&Spectrum::Prop::min_hold_visible, "min_hold_visible", "Shows the accumulated minimum-hold spectrum curve when enabled.">,
|
||||
Prop_Field<&Spectrum::Prop::max_marker_visible, "max_marker_visible", "Displays the marker attached to the strongest visible sample.">,
|
||||
Prop_Field<&Spectrum::Prop::min_marker_visible, "min_marker_visible", "Displays the marker attached to the weakest visible sample.">,
|
||||
Prop_Field<&Spectrum::Prop::sweep_region_visible, "sweep_region_visible", "Highlights the configured sweep-frequency interval on the plot.">,
|
||||
Prop_Field<&Spectrum::Prop::visible_range_only, "visible_range_only", "Restricts sample preparation to the frequency range currently visible on the axis.">,
|
||||
Prop_Field<&Spectrum::Prop::frequency_range, "frequency_range", "Maps the complete input sample span onto frequency coordinates.">,
|
||||
Prop_Field<&Spectrum::Prop::sweep_frequency_range, "sweep_frequency_range", "Defines the frequency interval rendered as the sweep region.">,
|
||||
Prop_Field<&Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects the interpolation algorithm used between adjacent spectrum samples.">,
|
||||
Prop_Field<&Spectrum::Prop::max_brush, "max_brush", "Fill brush used for the maximum-hold area.">,
|
||||
Prop_Field<&Spectrum::Prop::current_brush, "current_brush", "Fill brush used for the current spectrum area.">,
|
||||
Prop_Field<&Spectrum::Prop::min_brush, "min_brush", "Fill brush used for the minimum-hold area.">,
|
||||
Prop_Field<&Spectrum::Prop::max_pen, "max_pen", "Stroke style used for the maximum-hold curve.">,
|
||||
Prop_Field<&Spectrum::Prop::current_pen, "current_pen", "Stroke style used for the current spectrum curve.">,
|
||||
Prop_Field<&Spectrum::Prop::min_pen, "min_pen", "Stroke style used for the minimum-hold curve.">,
|
||||
Prop_Field<&Spectrum::Prop::selected_marker_pen, "selected_marker_pen", "Stroke style used to emphasize the currently selected marker.">,
|
||||
Prop_Field<&Spectrum::Prop::marker_pen, "marker_pen", "Default stroke style used for unselected spectrum markers.">,
|
||||
Prop_Field<&Spectrum::Prop::middle_frequency_pen, "middle_frequency_pen", "Stroke style used for the center-frequency indicator.">,
|
||||
Prop_Field<&Spectrum::Prop::sweep_region_brush, "sweep_region_brush", "Fill brush used to highlight the sweep-frequency interval.">,
|
||||
Prop_Field<&Spectrum::Prop::custom_markers, "custom_markers", "User-defined marker positions and presentation data.">,
|
||||
Prop_Field<&Spectrum::Prop::selected_marker, "selected_marker", "Index of the custom marker currently selected for interaction.">,
|
||||
State_Field<Spectrum, &Spectrum::State::sample_count, "sample_count", "Number of input spectrum samples available in the latest update.">,
|
||||
State_Field<Spectrum, &Spectrum::State::rendered_point_count, "rendered_point_count", "Number of curve points emitted by the latest render preparation.">,
|
||||
State_Field<Spectrum, &Spectrum::State::selectable_marker_count, "selectable_marker_count", "Number of markers currently eligible for selection.">>(
|
||||
*spectrum, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(spectrum), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> make_frequency_trace_plot() {
|
||||
constexpr Size canvas{720, 420};
|
||||
auto time = make_time_axis(
|
||||
Axis_Orientation::horizontal, {64.0, 370.0}, 620.0);
|
||||
auto vertical = make_numeric_axis(
|
||||
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2});
|
||||
auto trace = *Frequency_Trace::Builder<Frequency_Trace>(time.get(), vertical.get(), 4u).build();
|
||||
auto selection = selection_overlay(time.get(), vertical.get());
|
||||
auto scene = *Scene_2D::Builder<Scene_2D>{}
|
||||
.set(&Render_Scene_2D::Prop::viewport, canvas)
|
||||
.set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255})
|
||||
.set(&Render_Scene_2D::Prop::view_active, true)
|
||||
.add_renderable(trace.get())
|
||||
.add_renderable(selection.get())
|
||||
.add_dependency<Render_Graph_Tag>(selection.get(), trace.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Render_Tick& event, bool demo_data) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, time, vertical);
|
||||
if (!demo_data) return;
|
||||
constexpr double day_milliseconds = 86'400'000.0;
|
||||
const auto tick = time->append_time(Time_Of_Day{
|
||||
static_cast<std::int64_t>(
|
||||
std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds))
|
||||
});
|
||||
raw->template submit_stream<Frequency_Trace_Stream_Tag>(Frequency_Trace_Sample{
|
||||
tick, std::sin(event.time_milliseconds * 0.0025) * 0.8
|
||||
+ std::sin(event.time_milliseconds * 0.0007) * 0.2
|
||||
});
|
||||
raw->template mark_dirty<Render_Graph_Tag>();
|
||||
};
|
||||
auto view = make_scene_view<
|
||||
Prop_Field<&Frequency_Trace::Prop::partition_count, "partition_count", "Number of partitions used to prepare the time-ordered trace.">,
|
||||
Prop_Field<&Frequency_Trace::Prop::pen, "pen", "Stroke style used to draw the frequency trace.">>(
|
||||
*trace, *scene, std::move(update), std::move(time), std::move(vertical), std::move(trace), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> make_sweep_spectrum_plot() {
|
||||
constexpr Size canvas{720, 420};
|
||||
auto frequency = make_frequency_axis();
|
||||
auto vertical = make_numeric_axis(
|
||||
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0});
|
||||
auto sweep = *Sweep_Spectrum::Builder<Sweep_Spectrum>(frequency.get(), vertical.get(), 4u)
|
||||
.set(&Sweep_Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0})
|
||||
.set(&Sweep_Spectrum::Prop::bins_per_block, std::size_t{8})
|
||||
.set(&Sweep_Spectrum::Prop::block_count, std::size_t{64})
|
||||
.build();
|
||||
auto selection = selection_overlay(frequency.get(), vertical.get());
|
||||
auto scene = *Scene_2D::Builder<Scene_2D>{}
|
||||
.set(&Render_Scene_2D::Prop::viewport, canvas)
|
||||
.set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255})
|
||||
.set(&Render_Scene_2D::Prop::view_active, true)
|
||||
.add_renderable(sweep.get())
|
||||
.add_renderable(selection.get())
|
||||
.add_dependency<Render_Graph_Tag>(selection.get(), sweep.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Render_Tick& event, bool demo_data) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
if (!demo_data) return;
|
||||
const auto& state = raw->template read_prop<Sweep_Spectrum::Base_Tag>();
|
||||
const std::size_t block_count = std::max<std::size_t>(1, state.block_count);
|
||||
const std::size_t bins_per_block = std::max<std::size_t>(1, state.bins_per_block);
|
||||
const std::size_t block_index = static_cast<std::size_t>(event.sequence % block_count);
|
||||
std::vector<double> values(bins_per_block);
|
||||
for (std::size_t i = 0; i < values.size(); ++i) {
|
||||
const auto sweep_index = block_index * values.size() + i;
|
||||
values[i] = -90.0 + 35.0 * std::sin(sweep_index * 0.08 + event.time_milliseconds * 0.002);
|
||||
}
|
||||
raw->template submit_stream<Sweep_Spectrum_Stream_Tag>(
|
||||
std::make_shared<const Sweep_Spectrum_Block>(
|
||||
Sweep_Spectrum_Block{block_index, std::move(values)}));
|
||||
raw->template mark_dirty<Render_Graph_Tag>();
|
||||
};
|
||||
auto view = make_scene_view<
|
||||
Prop_Field<&Sweep_Spectrum::Prop::bins_per_block, "bins_per_block", "Number of frequency bins stored in each incoming sweep block.">,
|
||||
Prop_Field<&Sweep_Spectrum::Prop::block_count, "block_count", "Number of blocks required to compose one complete sweep.">,
|
||||
Prop_Field<&Sweep_Spectrum::Prop::partition_count, "partition_count", "Number of partitions used during sweep preparation.">,
|
||||
Prop_Field<&Sweep_Spectrum::Prop::visible_range_only, "visible_range_only", "Restricts preparation to the frequency interval visible on the axis.">,
|
||||
Prop_Field<&Sweep_Spectrum::Prop::frequency_range, "frequency_range", "Maps the complete sweep span onto frequency coordinates.">,
|
||||
Prop_Field<&Sweep_Spectrum::Prop::pen, "pen", "Stroke style used for the completed sweep curve.">,
|
||||
Prop_Field<&Sweep_Spectrum::Prop::current_frequency_pen, "current_frequency_pen", "Stroke style used for the current sweep-frequency indicator.">,
|
||||
Prop_Field<&Sweep_Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation between adjacent sweep bins.">>(
|
||||
*sweep, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> make_afterglow_plot() {
|
||||
constexpr Size canvas{720, 420};
|
||||
auto frequency = make_frequency_axis();
|
||||
auto vertical = make_numeric_axis(
|
||||
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0});
|
||||
auto afterglow = *Afterglow::Builder<Afterglow>(
|
||||
frequency.get(), vertical.get(), Plot_Partition_Grid{4, 3})
|
||||
.set(&Afterglow::Prop::frequency_range, Axis_Range{0.0, 100.0})
|
||||
.set(&Afterglow::Prop::power_range, Axis_Range{-110.0, 0.0})
|
||||
.set(&Afterglow::Prop::power_point_size, 96)
|
||||
.build();
|
||||
auto selection = selection_overlay(frequency.get(), vertical.get());
|
||||
auto scene = *Scene_2D::Builder<Scene_2D>{}
|
||||
.set(&Render_Scene_2D::Prop::viewport, canvas)
|
||||
.set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255})
|
||||
.set(&Render_Scene_2D::Prop::view_active, true)
|
||||
.add_renderable(afterglow.get())
|
||||
.add_renderable(selection.get())
|
||||
.add_dependency<Render_Graph_Tag>(selection.get(), afterglow.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Render_Tick& event, bool demo_data) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
if (!demo_data) return;
|
||||
std::array < double, 192 > values{};
|
||||
for (std::size_t i = 0; i < values.size(); ++i)
|
||||
values[i] = -95.0 + 62.0 * std::exp(-220.0 * std::pow(
|
||||
static_cast<double>(i) / values.size() - 0.5
|
||||
- 0.18 * std::sin(event.time_milliseconds * 0.0008), 2.0));
|
||||
raw->template submit_stream<Afterglow_Stream_Tag>(
|
||||
std::make_shared<const std::vector<Plot_Value>>(values.begin(), values.end()));
|
||||
raw->template mark_dirty<Render_Graph_Tag>();
|
||||
};
|
||||
auto view = make_scene_view<
|
||||
Prop_Field<&Afterglow::Prop::frequency_point_size, "frequency_point_size", "Number of frequency cells allocated across each afterglow row.">,
|
||||
Prop_Field<&Afterglow::Prop::power_point_size, "power_point_size", "Number of power cells allocated along the vertical afterglow range.">,
|
||||
Prop_Field<&Afterglow::Prop::partition_grid, "partition_grid", "Framebuffer partition grid expressed as column count by row count.">,
|
||||
Prop_Field<&Afterglow::Prop::interpolate, "interpolate", "Enables interpolation when mapping samples into the afterglow grid.">,
|
||||
Prop_Field<&Afterglow::Prop::attenuation_rate, "attenuation_rate", "Controls how quickly historical energy fades between updates.">,
|
||||
Prop_Field<&Afterglow::Prop::frequency_range, "frequency_range", "Maps input samples onto the afterglow frequency axis.">,
|
||||
Prop_Field<&Afterglow::Prop::power_range, "power_range", "Defines the minimum and maximum power represented by the color grid.">,
|
||||
Prop_Field<&Afterglow::Prop::color_map, "color_map", "Maps accumulated energy values to rendered colors.">>(
|
||||
*afterglow, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> make_waterfall_plot() {
|
||||
constexpr Size canvas{720, 420};
|
||||
auto frequency = make_frequency_axis();
|
||||
auto time = make_time_axis(
|
||||
Axis_Orientation::vertical, {64.0, 370.0}, -320.0);
|
||||
auto waterfall = *Waterfall::Builder<Waterfall>(
|
||||
frequency.get(), time.get(), Plot_Partition_Grid{4, 3})
|
||||
.set(&Waterfall::Prop::frequency_range, Axis_Range{0.0, 100.0})
|
||||
.set(&Waterfall::Prop::power_range, Axis_Range{-110.0, 0.0})
|
||||
.build();
|
||||
auto selection = selection_overlay(frequency.get(), time.get());
|
||||
auto scene = *Scene_2D::Builder<Scene_2D>{}
|
||||
.set(&Render_Scene_2D::Prop::viewport, canvas)
|
||||
.set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255})
|
||||
.set(&Render_Scene_2D::Prop::view_active, true)
|
||||
.add_renderable(waterfall.get())
|
||||
.add_renderable(selection.get())
|
||||
.add_dependency<Render_Graph_Tag>(selection.get(), waterfall.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Render_Tick& event, bool demo_data) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, time);
|
||||
if (!demo_data) return;
|
||||
constexpr double day_milliseconds = 86'400'000.0;
|
||||
const auto tick = time->append_time(Time_Of_Day{
|
||||
static_cast<std::int64_t>(
|
||||
std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds))
|
||||
});
|
||||
std::array < double, 192 > values{};
|
||||
for (std::size_t i = 0; i < values.size(); ++i)
|
||||
values[i] = -100.0 + 70.0 * std::exp(-240.0 * std::pow(
|
||||
static_cast<double>(i) / values.size() - 0.5
|
||||
- 0.22 * std::sin(static_cast<double>(tick) * 0.01), 2.0));
|
||||
raw->template submit_stream<Waterfall_Stream_Tag>(
|
||||
std::make_shared<const Waterfall_Row>(Waterfall_Row{
|
||||
tick, {values.begin(), values.end()}
|
||||
}));
|
||||
raw->template mark_dirty<Render_Graph_Tag>();
|
||||
};
|
||||
auto view = make_scene_view<
|
||||
Prop_Field<&Waterfall::Prop::tooltip_enabled, "tooltip_enabled", "Enables value inspection tooltips over waterfall cells.">,
|
||||
Prop_Field<&Waterfall::Prop::tooltip_font, "tooltip_font", "Font used to render waterfall tooltip text.">,
|
||||
Prop_Field<&Waterfall::Prop::tooltip_text_pen, "tooltip_text_pen", "Pen used to draw tooltip text and its foreground color.">,
|
||||
Prop_Field<&Waterfall::Prop::tooltip_background_brush, "tooltip_background_brush", "Brush used to fill the tooltip background panel.">,
|
||||
Prop_Field<&Waterfall::Prop::frequency_bin_count, "frequency_bin_count", "Number of frequency bins expected in each waterfall row.">,
|
||||
Prop_Field<&Waterfall::Prop::partition_grid, "partition_grid", "Framebuffer partition grid expressed as column count by row count.">,
|
||||
Prop_Field<&Waterfall::Prop::visible_range_only, "visible_range_only", "Restricts preparation to frequencies visible on the current axis.">,
|
||||
Prop_Field<&Waterfall::Prop::frequency_range, "frequency_range", "Maps row samples onto waterfall frequency coordinates.">,
|
||||
Prop_Field<&Waterfall::Prop::power_range, "power_range", "Defines the power interval mapped through the waterfall color map.">,
|
||||
Prop_Field<&Waterfall::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation when samples are mapped to raster cells.">,
|
||||
Prop_Field<&Waterfall::Prop::color_map, "color_map", "Maps sample power values to waterfall colors.">>(
|
||||
*waterfall, *scene, std::move(update), std::move(frequency), std::move(time), std::move(waterfall), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> make_constellation_plot() {
|
||||
constexpr Size canvas{720, 420};
|
||||
auto horizontal = make_numeric_axis(
|
||||
Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {-1.2, 1.2});
|
||||
auto vertical = make_numeric_axis(
|
||||
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2});
|
||||
auto constellation = *Constellation_Diagram::Builder<Constellation_Diagram>(
|
||||
horizontal.get(), vertical.get(), 4u)
|
||||
.set(&Constellation_Diagram::Prop::i_range, Axis_Range{-1.2, 1.2})
|
||||
.set(&Constellation_Diagram::Prop::q_range, Axis_Range{-1.2, 1.2})
|
||||
.build();
|
||||
auto selection = selection_overlay(horizontal.get(), vertical.get());
|
||||
auto scene = *Scene_2D::Builder<Scene_2D>{}
|
||||
.set(&Render_Scene_2D::Prop::viewport, canvas)
|
||||
.set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255})
|
||||
.set(&Render_Scene_2D::Prop::view_active, true)
|
||||
.add_renderable(constellation.get())
|
||||
.add_renderable(selection.get())
|
||||
.add_dependency<Render_Graph_Tag>(selection.get(), constellation.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Render_Tick& event, bool demo_data) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
|
||||
if (!demo_data) return;
|
||||
const auto& state = raw->template read_prop<Constellation_Diagram::Base_Tag>();
|
||||
const int anchor_count = static_cast<int>(state.type);
|
||||
const double radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4;
|
||||
const double phase = event.time_milliseconds * 0.001;
|
||||
for (int index = 0; index < anchor_count; ++index) {
|
||||
const double angle = state.phase_offset_radians
|
||||
+ 2.0 * std::numbers::pi * static_cast<double>(index) / anchor_count;
|
||||
const double noise_i = 0.025 * std::sin(phase * 11.0 + index * 1.73)
|
||||
+ 0.012 * std::cos(phase * 23.0 + index * 0.61);
|
||||
const double noise_q = 0.025 * std::cos(phase * 13.0 + index * 1.37)
|
||||
+ 0.012 * std::sin(phase * 19.0 + index * 0.47);
|
||||
raw->template submit_stream<Constellation_Stream_Tag>(Constellation_Point{
|
||||
{
|
||||
state.i_range.center() + std::cos(angle) * radius + noise_i,
|
||||
state.q_range.center() + std::sin(angle) * radius + noise_q
|
||||
},
|
||||
monotonic_milliseconds()
|
||||
});
|
||||
}
|
||||
raw->template mark_dirty<Render_Graph_Tag>();
|
||||
};
|
||||
auto view = make_scene_view<
|
||||
Prop_Field<&Constellation_Diagram::Prop::partition_count, "partition_count", "Number of partitions used to render received constellation points.">,
|
||||
Prop_Field<&Constellation_Diagram::Prop::point_lifetime_ms, "point_lifetime_ms", "Time in milliseconds that an appended constellation point remains visible.">,
|
||||
Prop_Field<&Constellation_Diagram::Prop::type, "type", "Selects the modulation constellation used to generate reference anchors.">,
|
||||
Prop_Field<&Constellation_Diagram::Prop::phase_offset_radians, "phase_offset_radians", "Rotates constellation points and anchors by the specified phase angle.">,
|
||||
Prop_Field<&Constellation_Diagram::Prop::i_range, "i_range", "Defines the horizontal in-phase coordinate interval.">,
|
||||
Prop_Field<&Constellation_Diagram::Prop::q_range, "q_range", "Defines the vertical quadrature coordinate interval.">,
|
||||
Prop_Field<&Constellation_Diagram::Prop::point_color, "point_color", "Color used to render received I/Q samples.">,
|
||||
Prop_Field<&Constellation_Diagram::Prop::anchor_color, "anchor_color", "Color used to render ideal modulation anchors.">>(
|
||||
*constellation, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
||||
}
|
||||
std::shared_ptr<Plot> make_selection_overlay_plot() {
|
||||
constexpr Size canvas{720, 420};
|
||||
auto horizontal = make_numeric_axis(
|
||||
Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {0.0, 100.0});
|
||||
auto vertical = make_numeric_axis(
|
||||
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {0.0, 100.0});
|
||||
auto selection = *Selection_Rectangle_Overlay::Builder<Selection_Rectangle_Overlay>(horizontal.get(), vertical.get()).build();
|
||||
auto scene = *Scene_2D::Builder<Scene_2D>{}
|
||||
.set(&Render_Scene_2D::Prop::viewport, canvas)
|
||||
.set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255})
|
||||
.set(&Render_Scene_2D::Prop::view_active, true)
|
||||
.add_renderable(selection.get())
|
||||
.build();
|
||||
auto update = [scene = scene.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Render_Tick& event, bool) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
|
||||
};
|
||||
auto view = make_scene_view<
|
||||
Prop_Field<&Selection_Rectangle_Overlay::Prop::label_font, "label_font", "Font used for labels attached to selected regions.">,
|
||||
Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen", "Pen used to draw selected-region label text.">,
|
||||
Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush", "Brush used to fill selected rectangular regions.">,
|
||||
Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen", "Pen used to draw selected-region borders.">,
|
||||
State_Field<Selection_Rectangle_Overlay,
|
||||
&Selection_Rectangle_Overlay::State::selected_region_count,
|
||||
"selected_region_count",
|
||||
"Number of rectangular regions currently selected.">>(
|
||||
*selection, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(scene), std::move(view));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include "Plot.hpp"
|
||||
|
||||
namespace aethera::web {
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_spectrum_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_axes_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_frequency_trace_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_sweep_spectrum_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_afterglow_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_waterfall_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_constellation_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_selection_overlay_plot();
|
||||
}
|
||||
@@ -0,0 +1,910 @@
|
||||
#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 = 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>
|
||||
struct 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 {
|
||||
static std::uint64_t mix(std::uint64_t value) noexcept {
|
||||
value += 0x9E3779B97F4A7C15ULL;
|
||||
value = (value ^ (value >> 30U)) * 0xBF58476D1CE4E5B9ULL;
|
||||
value = (value ^ (value >> 27U)) * 0x94D049BB133111EBULL;
|
||||
return value ^ (value >> 31U);
|
||||
}
|
||||
static float signed_unit(std::uint64_t sequence, std::size_t index,
|
||||
std::uint64_t lane) noexcept {
|
||||
const auto bits = mix(sequence ^
|
||||
(static_cast<std::uint64_t>(index) << 8U) ^ lane);
|
||||
return static_cast<float>(bits >> 40U) /
|
||||
static_cast<float>(1U << 24U) * 2.0F - 1.0F;
|
||||
}
|
||||
static void push(Vec3& value, std::uint64_t sequence,
|
||||
std::size_t index, std::uint64_t lane,
|
||||
float amount = 0.018F) noexcept {
|
||||
value.x = std::clamp(value.x + amount * signed_unit(sequence, index, lane),
|
||||
-0.96F, 0.96F);
|
||||
value.y = std::clamp(value.y + amount * signed_unit(sequence, index, lane + 1U),
|
||||
-0.96F, 0.96F);
|
||||
value.z = std::clamp(value.z + amount * signed_unit(sequence, index, lane + 2U),
|
||||
-0.96F, 0.96F);
|
||||
}
|
||||
template <typename Visual_Object>
|
||||
void update(Visual_Object& visual, Axes_3D&, Marker_Visual*,
|
||||
const Plot_Render_Tick& request) {
|
||||
using Definition = typename Visual_Object::Attached_Object;
|
||||
using Prop = typename Definition::Prop;
|
||||
const auto sequence = request.sequence != 0
|
||||
? request.sequence
|
||||
: static_cast<std::uint64_t>(request.time_milliseconds * 10.0);
|
||||
auto items = visual.template read_prop<typename Definition::Base_Tag>().items;
|
||||
for (std::size_t index = 0; index < items.size(); ++index) {
|
||||
auto& item = items[index];
|
||||
if constexpr (requires { item.position; })
|
||||
push(item.position, sequence, index, 0U);
|
||||
else if constexpr (requires { item.center; })
|
||||
push(item.center, sequence, index, 0U);
|
||||
else if constexpr (requires { item.origin; }) {
|
||||
push(item.origin, sequence, index, 0U);
|
||||
if constexpr (requires { item.direction; })
|
||||
push(item.direction, sequence, index, 3U, 0.008F);
|
||||
}
|
||||
else if constexpr (requires { item.start; item.end; }) {
|
||||
push(item.start, sequence, index, 0U);
|
||||
push(item.end, sequence, index, 3U);
|
||||
}
|
||||
if constexpr (requires { item.value; })
|
||||
item.value = std::clamp(
|
||||
item.value + 0.07F * signed_unit(sequence, index, 7U),
|
||||
0.0F, 1.0F);
|
||||
if constexpr (std::same_as<Definition, Text_Visual>) {
|
||||
if (index == 0)
|
||||
item.text = "Aethera frame " +
|
||||
std::to_string(sequence % 10'000U);
|
||||
}
|
||||
}
|
||||
visual.template set<&Prop::items>(std::move(items));
|
||||
|
||||
}
|
||||
};
|
||||
template <typename Visual_Object, typename Data_Generator = Random_Data_Generator>
|
||||
struct Visual_Scene_View final : public Plot::Scene_View {
|
||||
public:
|
||||
using Camera_Object = Camera_3D;
|
||||
using Axes_Object = Axes_3D;
|
||||
using Marker_Object = 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." >>;
|
||||
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.", "axis3d">,
|
||||
detail::Prop_Field < &Axes_3D::Prop::y_axis, "y_axis", "Y axis range, scale, ticks, label and unit.", "axis3d" >,
|
||||
detail::Prop_Field<&Axes_3D::Prop::z_axis, "z_axis", "Z axis range, scale, ticks, label and unit.", "axis3d"> >;
|
||||
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", 3}, {"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 component_state(std::string_view component) const 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"}};
|
||||
return (*found)->state();
|
||||
}
|
||||
[[nodiscard]] nlohmann::json capture_components(
|
||||
const std::vector<std::string>& components) const override {
|
||||
nlohmann::json result = nlohmann::json::object();
|
||||
for (const auto& component : components) {
|
||||
const auto found = std::ranges::find_if(descriptors_, [&](const auto& descriptor) {
|
||||
return descriptor->id() == component;
|
||||
});
|
||||
if (found != descriptors_.end())
|
||||
result[component] = (*found)->values();
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
visual_->template set<&Definition::Prop::items>(std::move(generated));
|
||||
return {{"success", true}, {"generated_count", count}};
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
return {{"success", false}, {"error", error.what()}};
|
||||
}
|
||||
}
|
||||
}
|
||||
void update(const Plot_Render_Tick& request) override {
|
||||
if constexpr (requires(Data_Generator& generator, Visual_Object& visual,
|
||||
Axes_Object& axes, Marker_Object* markers,
|
||||
const Plot_Render_Tick& 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 {
|
||||
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(std::string label,
|
||||
Build_Result build_result,
|
||||
Scene_Components_3D components = {},
|
||||
Data_Generator data_generator = {},
|
||||
std::unique_ptr<Marker_Visual> markers = {}) {
|
||||
using Visual_Object = Definition;
|
||||
using Camera_Object = Camera_3D;
|
||||
using Axes_Object = 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<Camera_Object>{}
|
||||
.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<Axes_Object>{}
|
||||
.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_3D>{};
|
||||
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(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(Mesh_Visual& visual, 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();
|
||||
visual.template set<&Mesh_Visual::Prop::items>(std::move(mesh));
|
||||
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(Mesh_Visual& visual, Axes_3D&,
|
||||
Marker_Visual* markers,
|
||||
const Plot_Render_Tick& request) {
|
||||
if (!parameters.animation_enabled ||
|
||||
request.sequence % 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);
|
||||
visual.template set<&Mesh_Visual::Prop::items>(std::move(mesh));
|
||||
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);
|
||||
}
|
||||
markers->template set<&Marker_Visual::Prop::items>(std::move(items));
|
||||
}
|
||||
};
|
||||
}
|
||||
std::shared_ptr<Plot> make_datoviz_point_plot() {
|
||||
return make_visual_plot<Point_Visual>("Point Visual", Point_Visual::Builder<Point_Visual>{}
|
||||
.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() {
|
||||
return make_visual_plot<Splat_Visual>("Splat Visual", Splat_Visual::Builder<Splat_Visual>{}
|
||||
.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() {
|
||||
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>("Pixel Visual", Pixel_Visual::Builder<Pixel_Visual>{}.set(&Pixel_Visual::Prop::items, std::move(pixels)).build());
|
||||
}
|
||||
std::shared_ptr<Plot> make_datoviz_marker_plot() {
|
||||
return make_visual_plot<Marker_Visual>("Marker Visual", Marker_Visual::Builder<Marker_Visual>{}
|
||||
.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() {
|
||||
return make_visual_plot<Sphere_Visual>("Sphere Visual", Sphere_Visual::Builder<Sphere_Visual>{}
|
||||
.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() {
|
||||
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>("Segment Visual", Segment_Visual::Builder<Segment_Visual>{}.set(&Segment_Visual::Prop::items, std::move(segments)).build());
|
||||
}
|
||||
std::shared_ptr<Plot> make_datoviz_vector_plot() {
|
||||
return make_visual_plot<Vector_Visual>("Vector Visual", Vector_Visual::Builder<Vector_Visual>{}
|
||||
.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() {
|
||||
return make_visual_plot<Primitive_Visual>("Primitive Visual", Primitive_Visual::Builder<Primitive_Visual>{}
|
||||
.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() {
|
||||
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>("Mesh Visual", Mesh_Visual::Builder<Mesh_Visual>{}.set(&Mesh_Visual::Prop::items, mesh).build());
|
||||
}
|
||||
std::shared_ptr<Plot> make_datoviz_spectrogram_plot() {
|
||||
const Spectrogram_Parameters parameters;
|
||||
auto marker_result = Marker_Visual::Builder<Marker_Visual>{}
|
||||
.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, false
|
||||
},
|
||||
{
|
||||
{0.28F, 0.34F, 0.86F}, color(88, 236, 211), 21.0F, 0.0F,
|
||||
Marker_Shape::cross, false
|
||||
}
|
||||
})
|
||||
.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>(
|
||||
"3D Spectrogram",
|
||||
Mesh_Visual::Builder<Mesh_Visual>{}
|
||||
.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() {
|
||||
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>("Path Visual", Path_Visual::Builder<Path_Visual>{}.set(&Path_Visual::Prop::items, std::move(path)).build());
|
||||
}
|
||||
std::shared_ptr<Plot> make_datoviz_image_plot() {
|
||||
std::vector<Color> pixels(32U * 32U);
|
||||
for (std::uint32_t y = 0; y < 32U; ++y)
|
||||
for (std::uint32_t x = 0; x < 32U; ++x)
|
||||
pixels[y * 32U + x] = (x / 8U + y / 8U) % 2U == 0U
|
||||
? color(40, 235, 205) : color(18, 42, 72);
|
||||
return make_visual_plot<Image_Visual>("Image Visual", Image_Visual::Builder<Image_Visual>{}
|
||||
.set(&Image_Visual::Prop::field_width, 32U).set(&Image_Visual::Prop::field_height, 32U)
|
||||
.set(&Image_Visual::Prop::field_pixels, std::move(pixels))
|
||||
.set(&Image_Visual::Prop::items, std::vector<Image>{{{0.0F, 0.0F, 0.0F}, {1.45F, 1.0F}, {0, 0, 1, 1}}}).build());
|
||||
}
|
||||
std::shared_ptr<Plot> make_datoviz_labels_plot() {
|
||||
std::vector<std::int32_t> labels(8U * 8U);
|
||||
for (std::uint32_t y = 0; y < 8U; ++y)
|
||||
for (std::uint32_t x = 0; x < 8U; ++x)
|
||||
labels[y * 8U + x] = static_cast<std::int32_t>(
|
||||
x / 4U + 2U * (y / 4U));
|
||||
return make_visual_plot<Labels_Visual>("Labels Visual", Labels_Visual::Builder<Labels_Visual>{}
|
||||
.set(&Labels_Visual::Prop::field_width, 8U).set(&Labels_Visual::Prop::field_height, 8U)
|
||||
.set(&Labels_Visual::Prop::field_labels, std::move(labels))
|
||||
.set(&Labels_Visual::Prop::items, std::vector<Label>{{{-0.55F, 0.28F, 0.0F}, {72, 34}, {0, 0, 0.5F, 0.5F}}, {{0.5F, 0.25F, 0.1F}, {72, 34}, {0.5F, 0, 1, 0.5F}}, {{-0.45F, -0.32F, 0.1F}, {72, 34}, {0, 0.5F, 0.5F, 1}}, {{0.55F, -0.3F, 0.0F}, {72, 34}, {0.5F, 0.5F, 1, 1}}}).build());
|
||||
}
|
||||
std::shared_ptr<Plot> make_datoviz_glyph_plot() {
|
||||
return make_visual_plot<Glyph_Visual>("Glyph Visual", Glyph_Visual::Builder<Glyph_Visual>{}
|
||||
.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() {
|
||||
return make_visual_plot<Text_Visual>("Text Visual", Text_Visual::Builder<Text_Visual>{}
|
||||
.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() {
|
||||
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>("Volume Visual", Volume_Visual::Builder<Volume_Visual>{}
|
||||
.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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include "Plot.hpp"
|
||||
|
||||
namespace aethera::web {
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_point_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_splat_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_pixel_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_marker_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_sphere_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_segment_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_vector_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_primitive_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_mesh_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_spectrogram_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_path_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_image_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_labels_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_glyph_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_text_plot();
|
||||
[[nodiscard]] std::shared_ptr<Plot> make_datoviz_volume_plot();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
||||
#pragma once
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <render_2D/scene/Render_Scene_2D.hpp>
|
||||
#include <render_3D/scene/Render_Scene_3D.hpp>
|
||||
#include <cstddef>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
namespace aethera::web {
|
||||
struct Gallery_Video_Stream;
|
||||
struct Plot_Input_Event {
|
||||
Event_Type type{Event_Type::pointer_move}; /* 输入事件业务类型。 */
|
||||
render_2d::Point_F position{}; /* Plot 像素坐标。 */
|
||||
render_2d::Point_F global_position{}; /* 浏览器屏幕像素坐标。 */
|
||||
Mouse_Button button{Mouse_Button::none}; /* 本次变化涉及的鼠标按键。 */
|
||||
Mouse_Button_Mask buttons{}; /* 事件产生时保持按下的按键集合。 */
|
||||
Keyboard_Modifier modifiers{Keyboard_Modifier::none}; /* 事件产生时的修饰键集合。 */
|
||||
double pixel_delta_x{}; /* 水平高精度滚轮增量。 */
|
||||
double pixel_delta_y{}; /* 垂直高精度滚轮增量。 */
|
||||
double angle_delta_x{}; /* 水平离散滚轮增量。 */
|
||||
double angle_delta_y{}; /* 垂直离散滚轮增量。 */
|
||||
Key key{Key::unknown}; /* 标准化键盘按键。 */
|
||||
std::uint32_t native_key{}; /* 浏览器原生按键码。 */
|
||||
bool auto_repeat{}; /* 是否为系统重复按键。 */
|
||||
};
|
||||
struct Plot_Render_Tick {
|
||||
std::chrono::steady_clock::time_point issued_at{}; /* 页面帧时钟发布本 tick 的单调时刻;手动帧在提交时填写。 */
|
||||
std::uint64_t sequence{}; /* Kernel 全局 Frame_Scheduler 时间轴上的关联序号。 */
|
||||
double time_milliseconds{}; /* 页面级单调时间线,所有图共享同一个动画时刻。 */
|
||||
std::uint32_t width{320}; /* 当前图在媒体图集中的固定像素宽度。 */
|
||||
std::uint32_t height{192}; /* 当前图在媒体图集中的固定像素高度。 */
|
||||
Frame_Request_Source source{Frame_Request_Source::immediate}; /* 本次请求来自周期时钟、手动操作或最大吞吐自驱动。 */
|
||||
};
|
||||
enum struct Plot_Pixel_Layout : std::uint8_t {
|
||||
bgra8,
|
||||
rgba8
|
||||
};
|
||||
struct Plot_Pixel_Frame {
|
||||
std::shared_ptr<const std::vector<std::byte>> pixels{}; /* 可选的不可变原生像素;停传输时为空但完成身份仍有效。 */
|
||||
Plot_Pixel_Layout layout{Plot_Pixel_Layout::rgba8}; /* 像素真实通道布局,由产生帧的后端标注。 */
|
||||
std::chrono::microseconds presentation_time{}; /* 页面媒体时钟上的显示时间戳。 */
|
||||
std::uint64_t sequence{}; /* 对应 Plot 完成帧的局部单调序号。 */
|
||||
std::uint64_t correlation_id{}; /* 触发本帧的页面级时钟序号;手动帧等于局部序号。 */
|
||||
std::uint64_t rendered_sequence{}; /* 实际产生当前像素的 Scene/GPU 提交帧序号。 */
|
||||
std::uint64_t rendered_correlation_id{}; /* 实际画面对应的页面级时钟序号。 */
|
||||
std::uint32_t width{}; /* 原生像素宽度。 */
|
||||
std::uint32_t height{}; /* 原生像素高度。 */
|
||||
};
|
||||
struct Plot_Stream_Frame {
|
||||
std::string notification{}; /* 仅用于终止错误等低频控制通知;正常帧为空。 */
|
||||
std::shared_ptr<const Plot_Pixel_Frame> pixels{}; /* 每帧完成进度及其可选图集像素。 */
|
||||
};
|
||||
struct Plot final : public std::enable_shared_from_this<Plot> {
|
||||
public:
|
||||
using Stream_Id = std::uint64_t;
|
||||
using Stream_Handler = std::function<void(std::shared_ptr<const Plot_Stream_Frame>)>;
|
||||
using Json_Handler = std::function<void(nlohmann::json)>;
|
||||
struct Scene_View {
|
||||
public:
|
||||
virtual ~Scene_View() = default;
|
||||
[[nodiscard]] virtual nlohmann::json schema() const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json write_prop(std::string_view component,
|
||||
std::string_view key,
|
||||
const nlohmann::json& value) = 0;
|
||||
[[nodiscard]] virtual nlohmann::json component_state(
|
||||
std::string_view component) const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json capture_components(
|
||||
const std::vector<std::string>& components) const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json data_generator_schema() const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json generate_data(const nlohmann::json& input) = 0;
|
||||
virtual void update(const Plot_Render_Tick& tick) = 0;
|
||||
};
|
||||
Plot(std::unique_ptr<render_2d::Render_Scene_2D> scene,
|
||||
std::unique_ptr<Scene_View> view);
|
||||
Plot(std::unique_ptr<render_3d::Render_Scene_3D> scene,
|
||||
std::unique_ptr<Scene_View> view);
|
||||
~Plot();
|
||||
Plot(const Plot&) = delete;
|
||||
Plot& operator=(const Plot&) = delete;
|
||||
[[nodiscard]] Stream_Id subscribe(Stream_Handler handler);
|
||||
void unsubscribe(Stream_Id stream);
|
||||
void configure_stream(Stream_Id stream, std::uint32_t width, std::uint32_t height);
|
||||
void schedule_render(Plot_Render_Tick tick);
|
||||
void render_once();
|
||||
void submit_input(Plot_Input_Event event);
|
||||
[[nodiscard]] nlohmann::json schema();
|
||||
[[nodiscard]] nlohmann::json write_prop(std::string_view component,
|
||||
std::string_view key,
|
||||
const nlohmann::json& value);
|
||||
[[nodiscard]] nlohmann::json component_state(std::string_view component) const;
|
||||
[[nodiscard]] nlohmann::json generate_data(const nlohmann::json& input);
|
||||
[[nodiscard]] nlohmann::json diagnostics() const;
|
||||
/* 清空旧捕获并请求接下来实际完成的 frame_count 帧 Task DAG。 */
|
||||
void request_taskflow_trace(std::size_t frame_count);
|
||||
[[nodiscard]] nlohmann::json taskflow_trace() const;
|
||||
/* 仅捕获 publish 之后真实执行的外接 Task_Graph;不追踪另一张 Plot 的 Render DAG。 */
|
||||
void request_post_publish_taskflow_trace(std::size_t frame_count);
|
||||
[[nodiscard]] nlohmann::json post_publish_taskflow_trace() const;
|
||||
void reset_diagnostics();
|
||||
private:
|
||||
friend struct Gallery_Video_Stream;
|
||||
struct Private;
|
||||
void ensure_started();
|
||||
void attach_scene_completion(std::unique_ptr<Task_Graph> completion);
|
||||
std::unique_ptr<Private> d;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <structive/property/property.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
namespace aethera::web::detail {
|
||||
enum struct Renderable_Field_Role {
|
||||
prop,
|
||||
state
|
||||
};
|
||||
template <Renderable_Field_Role Role>
|
||||
struct Renderable_Field_Role_Attribute;
|
||||
template <auto Member, structive::Fixed_String Key, structive::Fixed_String Description,
|
||||
structive::Fixed_String Editor = "">
|
||||
struct Prop_Field;
|
||||
template <typename Tag, auto Member, structive::Fixed_String Key, structive::Fixed_String Description>
|
||||
struct State_Field;
|
||||
template <typename Object, typename... Fields>
|
||||
struct Renderable_Adapter;
|
||||
struct Renderable_Descriptor {
|
||||
public:
|
||||
virtual ~Renderable_Descriptor() = default;
|
||||
[[nodiscard]] virtual std::string_view id() const noexcept = 0;
|
||||
[[nodiscard]] virtual nlohmann::json schema() const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json state() const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json values() const = 0;
|
||||
[[nodiscard]] virtual nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) = 0;
|
||||
};
|
||||
template <typename Adapter>
|
||||
struct Renderable_Descriptor_Model final : public Renderable_Descriptor {
|
||||
public:
|
||||
Renderable_Descriptor_Model(std::string id, std::string label, std::string kind, Adapter adapter);
|
||||
[[nodiscard]] std::string_view id() const noexcept override;
|
||||
[[nodiscard]] nlohmann::json schema() const override;
|
||||
[[nodiscard]] nlohmann::json state() const override;
|
||||
[[nodiscard]] nlohmann::json values() const override;
|
||||
[[nodiscard]] nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) override;
|
||||
private:
|
||||
std::string component_id;
|
||||
std::string component_label;
|
||||
std::string component_kind;
|
||||
Adapter adapter; /* 与 Plot 内真实 Renderable 同生共死的无所有权协议视图。 */
|
||||
};
|
||||
template <typename Adapter>
|
||||
[[nodiscard]] std::unique_ptr<Renderable_Descriptor> make_renderable_descriptor(
|
||||
std::string id, std::string label, std::string kind, Adapter adapter);
|
||||
}
|
||||
#include "Renderable_Adapter.ipp"
|
||||
@@ -0,0 +1,325 @@
|
||||
#pragma once
|
||||
#include <mcp/core/Render_Types.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <render_2D/plottable/Plottables.hpp>
|
||||
#include <render_3D/Render_3D.hpp>
|
||||
#include <deque>
|
||||
#include <limits>
|
||||
#include <ranges>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
namespace aethera::web::detail {
|
||||
struct Renderable_Field_Role_Category {};
|
||||
template <Renderable_Field_Role Role>
|
||||
struct Renderable_Field_Role_Attribute {
|
||||
using attribute_category = Renderable_Field_Role_Category;
|
||||
static constexpr bool single_valued = true;
|
||||
static constexpr bool inheritable = false;
|
||||
static constexpr auto value = Role;
|
||||
};
|
||||
template <auto Member, structive::Fixed_String Key, structive::Fixed_String Description,
|
||||
structive::Fixed_String Editor>
|
||||
struct Prop_Field {
|
||||
static constexpr auto member = Member;
|
||||
static constexpr auto key = Key;
|
||||
static constexpr auto description = Description;
|
||||
static constexpr auto editor = Editor;
|
||||
static constexpr auto role = Renderable_Field_Role::prop;
|
||||
};
|
||||
template <typename Tag, auto Member, structive::Fixed_String Key, structive::Fixed_String Description>
|
||||
struct State_Field {
|
||||
using tag_type = Tag;
|
||||
static constexpr auto member = Member;
|
||||
static constexpr auto key = Key;
|
||||
static constexpr auto description = Description;
|
||||
static constexpr auto role = Renderable_Field_Role::state;
|
||||
};
|
||||
template <typename Object, typename... Fields>
|
||||
struct Renderable_Adapter final : public structive::Property_Object<Renderable_Adapter<Object, Fields...>, structive::No_Lock_Policy> {
|
||||
public:
|
||||
explicit Renderable_Adapter(Object& value) : object(&value) {}
|
||||
void identify(std::string_view id) {
|
||||
if constexpr (std::derived_from<Object, aethera::Renderable>)
|
||||
double_buffer::detail::Internal_Access::layer<aethera::Renderable>(object)
|
||||
.diagnostic_component_id = id;
|
||||
}
|
||||
private:
|
||||
template <typename Adapter, typename Field>
|
||||
friend struct Renderable_Field_Accessor;
|
||||
Object* object;
|
||||
};
|
||||
template <typename Adapter, typename Field>
|
||||
struct Renderable_Field_Accessor;
|
||||
template <typename Object, typename... Fields, auto Member, structive::Fixed_String Key,
|
||||
structive::Fixed_String Description, structive::Fixed_String Editor>
|
||||
struct Renderable_Field_Accessor<Renderable_Adapter<Object, Fields...>, Prop_Field<Member, Key, Description, Editor>> {
|
||||
using object_type = Renderable_Adapter<Object, Fields...>;
|
||||
using value_type = typename structive::Member_Pointer_Traits<decltype(Member)>::value_type;
|
||||
using storage_identity = void;
|
||||
using dependency_spec = structive::No_Property_Dependencies;
|
||||
static constexpr auto description = Description;
|
||||
static constexpr auto editor = Editor;
|
||||
static constexpr bool readable = true;
|
||||
static constexpr bool writable = true;
|
||||
static constexpr bool synchronized_view_read = false;
|
||||
static constexpr bool trusted_object_access = true;
|
||||
[[nodiscard]] value_type read(const object_type& adapter) const {
|
||||
return adapter.object->template get<Member>();
|
||||
}
|
||||
void write(object_type& adapter, value_type value) const {
|
||||
adapter.object->template set<Member>(std::move(value));
|
||||
}
|
||||
};
|
||||
template <typename Object, typename... Fields, typename Tag, auto Member, structive::Fixed_String Key, structive::Fixed_String Description>
|
||||
struct Renderable_Field_Accessor<Renderable_Adapter<Object, Fields...>, State_Field<Tag, Member, Key, Description>> {
|
||||
using object_type = Renderable_Adapter<Object, Fields...>;
|
||||
using value_type = typename structive::Member_Pointer_Traits<decltype(Member)>::value_type;
|
||||
using storage_identity = void;
|
||||
using dependency_spec = structive::No_Property_Dependencies;
|
||||
static constexpr auto description = Description;
|
||||
static constexpr bool readable = true;
|
||||
static constexpr bool writable = false;
|
||||
static constexpr bool synchronized_view_read = false;
|
||||
static constexpr bool trusted_object_access = true;
|
||||
[[nodiscard]] value_type read(const object_type& adapter) const {
|
||||
value_type result{};
|
||||
adapter.object->template access_state<Tag>([&](const auto& state) {
|
||||
result = state.*Member;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
};
|
||||
template <typename Adapter, typename Field>
|
||||
constexpr auto renderable_field_descriptor() {
|
||||
using Accessor = Renderable_Field_Accessor<Adapter, Field>;
|
||||
using Role = Renderable_Field_Role_Attribute<Field::role>;
|
||||
using Key = structive::Key_Attribute<Field::key>;
|
||||
return structive::Property_Descriptor<Accessor, Key, Role>{{}, {Key{}, Role{}}};
|
||||
}
|
||||
}
|
||||
namespace structive {
|
||||
template <typename Object, typename... Fields>
|
||||
struct Type_Descriptor<aethera::web::detail::Renderable_Adapter<Object, Fields...>> {
|
||||
static auto get() {
|
||||
using Adapter = aethera::web::detail::Renderable_Adapter<Object, Fields...>;
|
||||
return object<Adapter>(synchronization(sync_all_unsynchronized),
|
||||
aethera::web::detail::renderable_field_descriptor<Adapter, Fields>()...);
|
||||
}
|
||||
};
|
||||
}
|
||||
namespace aethera::web::detail {
|
||||
template <typename Value>
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<Value>) noexcept {
|
||||
return mcp::protocol_editor<Value>();
|
||||
}
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<Color>) noexcept { return "color"; }
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<render_3d::Linear_Color>) noexcept { return "color"; }
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<render_2d::Point_F>) noexcept { return "point2"; }
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<render_2d::Size>) noexcept { return "size"; }
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<render_3d::Extent>) noexcept { return "size"; }
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<render_2d::Rect_F>) noexcept { return "rect"; }
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<render_2d::Axis_Range>) noexcept { return "range"; }
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<render_2d::Plot_Partition_Grid>) noexcept {
|
||||
return "partition-grid";
|
||||
}
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<render_2d::Pen>) noexcept { return "pen"; }
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<render_2d::Brush>) noexcept { return "brush"; }
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<render_2d::Font>) noexcept { return "font"; }
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<render_2d::Color_Map>) noexcept { return "color-map"; }
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<render_3d::Vec3>) noexcept { return "vector3"; }
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<render_3d::Matrix4>) noexcept { return "matrix4"; }
|
||||
[[nodiscard]] constexpr std::string_view renderable_editor(
|
||||
mcp::Type_Tag<std::vector<render_3d::Marker>>) noexcept {
|
||||
return "marker-list";
|
||||
}
|
||||
|
||||
inline std::string_view protocol_field_label(std::string_view key) {
|
||||
static constexpr std::pair<std::string_view, std::string_view> labels[] = {
|
||||
{"position", "位置"}, {"viewport", "视口尺寸"}, {"background", "背景颜色"},
|
||||
{"clear_color", "清屏颜色"}, {"view_active", "启用视图"},
|
||||
{"initial_view", "初始相机视图"}, {"projection", "投影方式"},
|
||||
{"controller", "Datoviz 相机模式"}, {"turntable_control", "Turntable 控制"},
|
||||
{"arcball_control", "Arcball 控制"}, {"fly_control", "Fly 控制"},
|
||||
{"panzoom_control", "Panzoom 控制"}, {"vertical_field_of_view_degrees", "垂直视场角"},
|
||||
{"near_plane", "近裁剪面"}, {"far_plane", "远裁剪面"},
|
||||
{"x_axis", "X 坐标轴"}, {"y_axis", "Y 坐标轴"}, {"z_axis", "Z 坐标轴"},
|
||||
{"pixel_length", "轴线长度"}, {"orientation", "轴线方向"}, {"tick_length", "主刻度长度"},
|
||||
{"sub_tick_length", "次刻度长度"}, {"axis_pen", "轴线画笔"}, {"unit_text", "单位文字"},
|
||||
{"unit_text_font", "单位字体"}, {"unit_text_pen", "单位文字画笔"},
|
||||
{"unit_text_background_brush", "单位文字背景"}, {"label_rotation_degrees", "标签旋转角度"},
|
||||
{"coordinate_range", "坐标范围"}, {"precision", "小数精度"}, {"locale", "数字区域格式"},
|
||||
{"wheel_enabled", "允许滚轮缩放"}, {"drag_enabled", "允许拖动平移"},
|
||||
{"visible_count", "可见数量"}, {"tick_label_spacing_px", "刻度文字间距"},
|
||||
{"estimated_label_width_px", "预计标签宽度"}, {"format", "时间格式"},
|
||||
{"newest_at_start", "最新数据置于起点"}, {"center_frequency", "中心频率"},
|
||||
{"partition_count", "分区数量"}, {"partition_grid", "分区网格"},
|
||||
{"interpolation_mode", "插值模式"}, {"visible_range_only", "仅处理可见范围"},
|
||||
{"frequency_range", "频率范围"}, {"power_range", "功率范围"}, {"i_range", "同相范围"},
|
||||
{"q_range", "正交范围"}, {"phase_offset_radians", "相位偏移"}, {"pen", "画笔"},
|
||||
{"samples", "样本数据"}, {"rows", "瀑布图行数据"}, {"color_map", "颜色映射"},
|
||||
{"tooltip_enabled", "启用提示框"}, {"tooltip_font", "提示框字体"},
|
||||
{"tooltip_text_pen", "提示文字画笔"}, {"tooltip_background_brush", "提示框背景"},
|
||||
{"frequency_bin_count", "频率箱数量"}, {"frequency_point_size", "频率网格数量"},
|
||||
{"power_point_size", "功率网格数量"}, {"attenuation_rate", "衰减速率"},
|
||||
{"interpolate", "启用插值"}, {"spectra", "频谱历史"}, {"blocks", "扫频块数据"},
|
||||
{"bins_per_block", "每块频点数"}, {"block_count", "扫频块数量"},
|
||||
{"max_hold_visible", "显示最大保持"}, {"min_hold_visible", "显示最小保持"},
|
||||
{"max_marker_visible", "显示最大标记"}, {"min_marker_visible", "显示最小标记"},
|
||||
{"sweep_region_visible", "显示扫频区域"}, {"sweep_frequency_range", "扫频范围"},
|
||||
{"max_brush", "最大保持画刷"}, {"current_brush", "当前曲线画刷"},
|
||||
{"min_brush", "最小保持画刷"}, {"max_pen", "最大保持画笔"},
|
||||
{"current_pen", "当前曲线画笔"}, {"min_pen", "最小保持画笔"},
|
||||
{"selected_marker_pen", "选中标记画笔"}, {"marker_pen", "标记画笔"},
|
||||
{"middle_frequency_pen", "中心频率画笔"}, {"sweep_region_brush", "扫频区域画刷"},
|
||||
{"current_frequency_pen", "当前频率指示线"}, {"custom_markers", "自定义标记"},
|
||||
{"selected_marker", "当前选中标记"}, {"point_lifetime_ms", "点保留时间"},
|
||||
{"type", "星座类型"}, {"point_color", "数据点颜色"}, {"anchor_color", "参考点颜色"},
|
||||
{"points", "星座点数据"}, {"label_font", "标签字体"}, {"label_pen", "标签画笔"},
|
||||
{"selection_brush", "选区画刷"}, {"selection_border_pen", "选区边框画笔"},
|
||||
{"selected_regions", "已选区域"}, {"cache_enabled", "启用绘制缓存"},
|
||||
{"transform", "空间变换"}, {"visible", "是否可见"},
|
||||
{"depth_test", "深度测试"}, {"items", "项目数据"},
|
||||
{"next_tick", "下一时间刻度"},
|
||||
{"sample_count", "样本数量"}, {"rendered_point_count", "已绘制点数量"},
|
||||
{"selectable_marker_count", "可选标记数量"}, {"stored_block_count", "已保存数据块数量"},
|
||||
{"stored_point_count", "已保存数据点数量"}, {"history_count", "历史帧数量"},
|
||||
{"latest_spectrum_point_count", "最新频谱点数量"}, {"rendered_cell_count", "已绘制单元数量"},
|
||||
{"row_count", "瀑布图行数量"}, {"point_count", "数据点数量"},
|
||||
{"selected_region_count", "已选区域数量"}, {"item_count", "项目数量"},
|
||||
{"prepared_item_count", "已准备项目数量"}, {"prepared_revision", "已准备修订号"}
|
||||
};
|
||||
const auto found = std::ranges::find_if(labels, [key](const auto& entry) {
|
||||
return entry.first == key;
|
||||
});
|
||||
return found == std::end(labels) ? key : found->second;
|
||||
}
|
||||
template <typename Adapter>
|
||||
nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std::string_view label, std::string_view kind) {
|
||||
nlohmann::json fields = nlohmann::json::array();
|
||||
structive::type_descriptor<Adapter>().for_each_property([&](auto, const auto& field) {
|
||||
using Field = std::remove_cvref_t<decltype(field)>;
|
||||
using Value = typename Field::value_type;
|
||||
const bool editable = field.template attribute<Renderable_Field_Role_Category>().value == Renderable_Field_Role::prop;
|
||||
const auto field_label = protocol_field_label(field.key());
|
||||
std::string_view editor = renderable_editor(mcp::Type_Tag<Value>{});
|
||||
if constexpr (requires { Field::accessor_type::editor; }) {
|
||||
if (!Field::accessor_type::editor.view().empty()) editor = Field::accessor_type::editor.view();
|
||||
}
|
||||
nlohmann::json item{
|
||||
{"key", field.key()}, {"label", field_label}, {"editor", editor},
|
||||
{"editable", editable},
|
||||
{"description", std::string(field_label) + "。协议字段:" + std::string(field.key()) + "。"},
|
||||
{"technical_description", std::string(Field::accessor_type::description.view())}
|
||||
};
|
||||
if (editable) item["value"] = mcp::encode_protocol_value(field.accessor.read(adapter));
|
||||
if constexpr (std::same_as<Value, render_3d::Linear_Color>) item["color_channel_scale"] = "normalized";
|
||||
else if constexpr (std::same_as<Value, Color>) item["color_channel_scale"] = "byte";
|
||||
if (editable) {
|
||||
if constexpr (std::is_enum_v<Value>) {
|
||||
item["options"] = nlohmann::json::array();
|
||||
for (const auto enum_value : magic_enum::enum_values<Value>()) {
|
||||
const auto name = magic_enum::enum_name(enum_value);
|
||||
item["options"].push_back({{"value", name}, {"label", name}});
|
||||
}
|
||||
}
|
||||
}
|
||||
fields.push_back(std::move(item));
|
||||
});
|
||||
return {
|
||||
{"id", id}, {"label", label}, {"kind", kind},
|
||||
{"fields", std::move(fields)}
|
||||
};
|
||||
}
|
||||
template <typename Adapter>
|
||||
nlohmann::json adapter_values(const Adapter& adapter, Renderable_Field_Role role) {
|
||||
nlohmann::json result = nlohmann::json::object();
|
||||
structive::type_descriptor<Adapter>().for_each_property([&](auto, const auto& field) {
|
||||
if (field.template attribute<Renderable_Field_Role_Category>().value == role)
|
||||
result[field.key()] = mcp::encode_protocol_value(field.accessor.read(adapter));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
template <typename Adapter>
|
||||
nlohmann::json write_adapter_prop(Adapter& adapter, std::string_view key, const nlohmann::json& input) {
|
||||
nlohmann::json result{{"success", false}, {"key", key}};
|
||||
const bool found = structive::visit_schema_property(structive::type_descriptor<Adapter>(), key, [&](auto, const auto& field) {
|
||||
using Field = std::remove_cvref_t<decltype(field)>;
|
||||
using Value = typename Field::value_type;
|
||||
if constexpr (!Field::writable) {
|
||||
result["error"] = "property is read-only";
|
||||
}
|
||||
else {
|
||||
try {
|
||||
Value value{};
|
||||
mcp::decode_protocol_value(value, input);
|
||||
const auto status = adapter.runtime_write(key, typeid(Value), &value);
|
||||
if (status != structive::Runtime_Access_Result::ok) {
|
||||
result["error"] = "property write was rejected";
|
||||
return;
|
||||
}
|
||||
result["success"] = true;
|
||||
result["value"] = mcp::encode_protocol_value(field.accessor.read(adapter));
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
result["error"] = error.what();
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!found) result["error"] = "unknown property";
|
||||
return result;
|
||||
}
|
||||
template <typename Adapter>
|
||||
Renderable_Descriptor_Model<Adapter>::Renderable_Descriptor_Model(
|
||||
std::string id, std::string label, std::string kind, Adapter value) : component_id(std::move(id)), component_label(std::move(label)), component_kind(std::move(kind)), adapter(std::move(value)) {
|
||||
adapter.identify(component_id);
|
||||
}
|
||||
template <typename Adapter>
|
||||
std::string_view Renderable_Descriptor_Model<Adapter>::id() const noexcept {
|
||||
return component_id;
|
||||
}
|
||||
template <typename Adapter>
|
||||
nlohmann::json Renderable_Descriptor_Model<Adapter>::schema() const {
|
||||
return adapter_schema(adapter, component_id, component_label, component_kind);
|
||||
}
|
||||
template <typename Adapter>
|
||||
nlohmann::json Renderable_Descriptor_Model<Adapter>::state() const {
|
||||
return {{"protocol", "aethera.component.state"}, {"version", 1},
|
||||
{"component", component_id},
|
||||
{"state", adapter_values(adapter, Renderable_Field_Role::state)}};
|
||||
}
|
||||
template <typename Adapter>
|
||||
nlohmann::json Renderable_Descriptor_Model<Adapter>::values() const {
|
||||
return {{"component", component_id}, {"label", component_label},
|
||||
{"kind", component_kind},
|
||||
{"prop", adapter_values(adapter, Renderable_Field_Role::prop)},
|
||||
{"state", adapter_values(adapter, Renderable_Field_Role::state)}};
|
||||
}
|
||||
template <typename Adapter>
|
||||
nlohmann::json Renderable_Descriptor_Model<Adapter>::write_prop(std::string_view key, const nlohmann::json& value) {
|
||||
return write_adapter_prop(adapter, key, value);
|
||||
}
|
||||
template <typename Adapter>
|
||||
std::unique_ptr<Renderable_Descriptor> make_renderable_descriptor(
|
||||
std::string id, std::string label, std::string kind, Adapter adapter) {
|
||||
return std::make_unique < Renderable_Descriptor_Model<Adapter> > (
|
||||
std::move(id), std::move(label), std::move(kind), std::move(adapter));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <frame.hpp>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
||||
namespace aethera::web {
|
||||
[[nodiscard]] nlohmann::json taskflow_trace_json(
|
||||
const Taskflow_Frame_Trace& trace,
|
||||
const nlohmann::json& captured_components = {},
|
||||
const nlohmann::json& captured_backend = {});
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../web_server/cmake/rely.cmake")
|
||||
set(Aethera_MCP_dependencies
|
||||
global::drogon
|
||||
global::pfr)
|
||||
rcl_add_dependency_action_targets(Aethera_MCP_env ${Aethera_MCP_dependencies})
|
||||
set_target_properties(Aethera_MCP_env PROPERTIES FOLDER Aethera_MCP)
|
||||
library_get_missing_with_rely(Aethera_MCP_dependencies_missing
|
||||
${Aethera_MCP_dependencies})
|
||||
if (Aethera_MCP_dependencies_missing)
|
||||
rcl_log_append(${CMAKE_CURRENT_LIST_LINE}
|
||||
"[FATAL_ERROR] Aethera_MCP dependencies\n${Aethera_MCP_dependencies_missing}\n are not installed. Build Aethera_MCP_env first")
|
||||
return()
|
||||
endif ()
|
||||
rcl_load_dependency_environment(${Aethera_MCP_dependencies})
|
||||
find_package(Drogon CONFIG REQUIRED)
|
||||
if (NOT TARGET Aethera_render_2D)
|
||||
rcl_log_append(${CMAKE_CURRENT_LIST_LINE}
|
||||
"[FATAL_ERROR] Aethera_MCP requires Aethera_render_2D")
|
||||
return()
|
||||
endif ()
|
||||
if (NOT TARGET Aethera_render_3D)
|
||||
rcl_log_append(${CMAKE_CURRENT_LIST_LINE}
|
||||
"[FATAL_ERROR] Aethera_MCP requires Aethera_render_3D")
|
||||
return()
|
||||
endif ()
|
||||
if (NOT TARGET structive::property_core)
|
||||
set(Aethera_MCP_saved_build_testing "${BUILD_TESTING}")
|
||||
set(BUILD_TESTING OFF)
|
||||
set(STRUCTIVE_BUILD_EXAMPLES OFF)
|
||||
set(STRUCTIVE_BUILD_TESTS OFF)
|
||||
set(STRUCTIVE_INSTALL OFF)
|
||||
add_subdirectory(
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../third_party/Structive"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/third_party/Structive"
|
||||
EXCLUDE_FROM_ALL)
|
||||
set(BUILD_TESTING "${Aethera_MCP_saved_build_testing}")
|
||||
unset(Aethera_MCP_saved_build_testing)
|
||||
endif ()
|
||||
set(Aethera_MCP_core_dir "${CMAKE_CURRENT_LIST_DIR}/core")
|
||||
append_glob_source(Aethera_MCP_core_sources "${Aethera_MCP_core_dir}")
|
||||
add_library(Aethera_MCP_Core STATIC ${Aethera_MCP_core_sources})
|
||||
target_include_directories(Aethera_MCP_Core PUBLIC
|
||||
"$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/..>"
|
||||
"$<BUILD_INTERFACE:${EXTERNAL_DIR}/source/pfr/include>"
|
||||
"$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/../web_server/third_party/include>")
|
||||
target_compile_features(Aethera_MCP_Core PUBLIC cxx_std_23)
|
||||
target_compile_definitions(Aethera_MCP_Core PUBLIC NOMINMAX)
|
||||
target_link_libraries(Aethera_MCP_Core PUBLIC
|
||||
Aethera_render_2D
|
||||
Aethera_render_3D
|
||||
structive::property_core)
|
||||
add_executable(Aethera_MCP_Server
|
||||
"${CMAKE_CURRENT_LIST_DIR}/server/main.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/server/Mcp_Server.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/server/Mcp_Server.hpp")
|
||||
target_link_libraries(Aethera_MCP_Server PRIVATE
|
||||
Aethera_MCP_Core
|
||||
Drogon::Drogon
|
||||
TBB::tbbmalloc_proxy)
|
||||
if (MSVC)
|
||||
target_compile_options(Aethera_MCP_Core PRIVATE /utf-8 /bigobj)
|
||||
target_compile_options(Aethera_MCP_Server PRIVATE /utf-8 /bigobj)
|
||||
target_link_options(Aethera_MCP_Server PRIVATE "/INCLUDE:__TBB_malloc_proxy")
|
||||
add_custom_command(TARGET Aethera_MCP_Server POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"$<TARGET_FILE:TBB::tbbmalloc>"
|
||||
"$<TARGET_FILE:TBB::tbbmalloc_proxy>"
|
||||
"$<TARGET_FILE_DIR:Aethera_MCP_Server>"
|
||||
COMMENT "Deploying the scalable allocator runtime for Aethera MCP"
|
||||
VERBATIM)
|
||||
endif ()
|
||||
if (Aethera_BUILD_TESTS)
|
||||
add_executable(Aethera_MCP_Protocol_Type_Tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/Protocol_Type_Tests.cpp")
|
||||
target_link_libraries(Aethera_MCP_Protocol_Type_Tests PRIVATE
|
||||
Aethera_MCP_Core
|
||||
GTest::gtest_main)
|
||||
if (MSVC)
|
||||
target_compile_options(Aethera_MCP_Protocol_Type_Tests PRIVATE
|
||||
/utf-8 /bigobj)
|
||||
endif ()
|
||||
add_test(NAME Aethera_MCP_Protocol_Type_Tests
|
||||
COMMAND Aethera_MCP_Protocol_Type_Tests)
|
||||
set_tests_properties(Aethera_MCP_Protocol_Type_Tests PROPERTIES
|
||||
LABELS "Aethera_MCP")
|
||||
add_custom_target(Aethera_MCP_check
|
||||
COMMAND "${CMAKE_CTEST_COMMAND}"
|
||||
--test-dir "${CMAKE_BINARY_DIR}"
|
||||
-C "$<CONFIG>" -L "^Aethera_MCP$" --output-on-failure
|
||||
DEPENDS Aethera_MCP_Protocol_Type_Tests
|
||||
USES_TERMINAL)
|
||||
endif ()
|
||||
@@ -0,0 +1,147 @@
|
||||
#include "Mcp_Server.hpp"
|
||||
#include <mcp/core/Control_Service.hpp>
|
||||
#include <drogon/drogon.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace aethera::mcp {
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view protocol_version{"2026-07-28"};
|
||||
|
||||
[[nodiscard]] drogon::HttpResponsePtr json_response(
|
||||
nlohmann::json value,
|
||||
drogon::HttpStatusCode status = drogon::k200OK) {
|
||||
auto response = drogon::HttpResponse::newHttpResponse();
|
||||
response->setContentTypeCode(drogon::CT_APPLICATION_JSON);
|
||||
response->setStatusCode(status);
|
||||
response->addHeader("MCP-Protocol-Version", std::string{protocol_version});
|
||||
response->setBody(value.dump());
|
||||
return response;
|
||||
}
|
||||
|
||||
[[nodiscard]] nlohmann::json rpc_error(
|
||||
const nlohmann::json& id, int code, std::string message) {
|
||||
return {{"jsonrpc", "2.0"}, {"id", id},
|
||||
{"error", {{"code", code}, {"message", std::move(message)}}}};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool accepted_origin(const drogon::HttpRequestPtr& request) {
|
||||
const auto origin = request->getHeader("Origin");
|
||||
if (origin.empty()) return true;
|
||||
return origin == "null" || origin.starts_with("http://127.0.0.1") ||
|
||||
origin.starts_with("http://localhost") ||
|
||||
origin.starts_with("https://127.0.0.1") ||
|
||||
origin.starts_with("https://localhost");
|
||||
}
|
||||
|
||||
[[nodiscard]] nlohmann::json tool_result(const Tool_Call_Output& output) {
|
||||
const bool failed = output.result != Tool_Call_Result::ok;
|
||||
const auto text = failed ? output.message : output.content.dump();
|
||||
nlohmann::json result{
|
||||
{"content", nlohmann::json::array({{{"type", "text"}, {"text", text}}})},
|
||||
{"isError", failed}};
|
||||
if (!output.content.is_null() && !output.content.empty())
|
||||
result["structuredContent"] = output.content;
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] nlohmann::json dispatch_request(
|
||||
Control_Service& control, const nlohmann::json& request) {
|
||||
const auto id = request.value("id", nlohmann::json{});
|
||||
const auto method = request.value("method", std::string{});
|
||||
if (method == "server/discover" || method == "initialize") {
|
||||
return {{"jsonrpc", "2.0"}, {"id", id},
|
||||
{"result", {
|
||||
{"protocolVersion", protocol_version},
|
||||
{"capabilities", {{"tools", nlohmann::json::object()}}},
|
||||
{"serverInfo", {{"name", "aethera"}, {"version", "1.0.0"}}}}}};
|
||||
}
|
||||
if (method == "ping")
|
||||
return {{"jsonrpc", "2.0"}, {"id", id},
|
||||
{"result", nlohmann::json::object()}};
|
||||
if (method == "tools/list")
|
||||
return {{"jsonrpc", "2.0"}, {"id", id},
|
||||
{"result", {{"tools", control.tool_catalog()},
|
||||
{"ttlMs", 1000}, {"cacheScope", "server"}}}};
|
||||
if (method == "tools/call") {
|
||||
if (!request.contains("params") || !request["params"].is_object())
|
||||
return rpc_error(id, -32602, "tools/call requires params");
|
||||
const auto& parameters = request["params"];
|
||||
const auto name = parameters.value("name", std::string{});
|
||||
if (name.empty()) return rpc_error(id, -32602, "tool name is required");
|
||||
const auto arguments = parameters.value(
|
||||
"arguments", nlohmann::json::object());
|
||||
const auto output = control.call_tool(name, arguments);
|
||||
if (output.result == Tool_Call_Result::unknown_tool)
|
||||
return rpc_error(id, -32602, output.message);
|
||||
return {{"jsonrpc", "2.0"}, {"id", id},
|
||||
{"result", tool_result(output)}};
|
||||
}
|
||||
return rpc_error(id, -32601, "method not found");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int run_mcp_server(std::uint16_t port) {
|
||||
auto control = Control_Service::create();
|
||||
auto& app = drogon::app();
|
||||
app.registerHandler(
|
||||
"/mcp",
|
||||
[control](const drogon::HttpRequestPtr& request,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||||
if (!accepted_origin(request)) {
|
||||
callback(json_response(
|
||||
rpc_error(nullptr, -32000, "origin is not allowed"),
|
||||
drogon::k403Forbidden));
|
||||
return;
|
||||
}
|
||||
if (request->method() == drogon::Get) {
|
||||
callback(json_response(
|
||||
rpc_error(nullptr, -32600, "SSE stream is not provided"),
|
||||
drogon::k405MethodNotAllowed));
|
||||
return;
|
||||
}
|
||||
nlohmann::json message;
|
||||
try {
|
||||
message = nlohmann::json::parse(request->body());
|
||||
} catch (const nlohmann::json::exception&) {
|
||||
callback(json_response(
|
||||
rpc_error(nullptr, -32700, "invalid JSON"),
|
||||
drogon::k400BadRequest));
|
||||
return;
|
||||
}
|
||||
if (!message.is_object() || message.value("jsonrpc", "") != "2.0" ||
|
||||
!message.contains("method")) {
|
||||
callback(json_response(
|
||||
rpc_error(message.value("id", nlohmann::json{}),
|
||||
-32600, "invalid JSON-RPC request"),
|
||||
drogon::k400BadRequest));
|
||||
return;
|
||||
}
|
||||
if (!message.contains("id")) {
|
||||
auto response = drogon::HttpResponse::newHttpResponse();
|
||||
response->setStatusCode(drogon::k202Accepted);
|
||||
callback(std::move(response));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
callback(json_response(dispatch_request(*control, message)));
|
||||
} catch (const std::exception& failure) {
|
||||
callback(json_response(
|
||||
rpc_error(message["id"], -32603, failure.what()),
|
||||
drogon::k500InternalServerError));
|
||||
}
|
||||
},
|
||||
{drogon::Get, drogon::Post});
|
||||
app.addListener("127.0.0.1", port)
|
||||
.setThreadNum(std::min(8U, std::max(2U, std::thread::hardware_concurrency())))
|
||||
.setIdleConnectionTimeout(90)
|
||||
.run();
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace aethera::mcp {
|
||||
|
||||
int run_mcp_server(std::uint16_t port);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "Mcp_Server.hpp"
|
||||
#include <render_common.hpp>
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] std::uint16_t parse_port(int argc, char** argv) {
|
||||
constexpr std::uint16_t default_port{8850};
|
||||
if (argc == 1) return default_port;
|
||||
if (argc != 3 || std::string_view{argv[1]} != "--port") return 0;
|
||||
unsigned value{};
|
||||
const std::string_view text{argv[2]};
|
||||
const auto [end, error] = std::from_chars(
|
||||
text.data(), text.data() + text.size(), value);
|
||||
if (error != std::errc{} || end != text.data() + text.size() ||
|
||||
value == 0 || value > 65535)
|
||||
return 0;
|
||||
return static_cast<std::uint16_t>(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const auto port = parse_port(argc, argv);
|
||||
if (port == 0) return 2;
|
||||
aethera::initialize_runtime({});
|
||||
std::cout << "Aethera MCP http://127.0.0.1:" << port << "/mcp\n";
|
||||
return aethera::mcp::run_mcp_server(port);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#include <mcp/core/Render_Types.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace aethera::mcp::tests {
|
||||
|
||||
enum struct Sample_Mode : std::uint8_t {
|
||||
first,
|
||||
second
|
||||
};
|
||||
|
||||
struct Nested_Value {
|
||||
double gain{}; /* 测试嵌套数值。 */
|
||||
Sample_Mode mode{Sample_Mode::first}; /* 测试枚举描述。 */
|
||||
bool operator==(const Nested_Value&) const = default;
|
||||
};
|
||||
|
||||
struct Request_Value {
|
||||
std::string plot; /* 测试字段名反射。 */
|
||||
std::vector<Nested_Value> values; /* 测试递归容器。 */
|
||||
bool operator==(const Request_Value&) const = default;
|
||||
};
|
||||
|
||||
struct Semantic_Value {
|
||||
int value{}; /* 自定义类型值。 */
|
||||
bool operator==(const Semantic_Value&) const = default;
|
||||
};
|
||||
|
||||
struct Semantic_Value_Type : Aggregate_Type<Semantic_Value> {
|
||||
[[nodiscard]] static constexpr std::string_view editor() noexcept {
|
||||
return "semantic-value";
|
||||
}
|
||||
};
|
||||
|
||||
/* 自定义类型把精确重载直接放在 struct 定义之后;PFR 通用分派无需修改。 */
|
||||
[[nodiscard]] auto match_protocol_type(
|
||||
Type_Tag<Semantic_Value>) -> Semantic_Value_Type;
|
||||
|
||||
TEST(Protocol_Type, Pfr_Field_Names_Drive_Recursive_Schema) {
|
||||
const auto schema = describe_protocol_type<Request_Value>();
|
||||
ASSERT_EQ(schema.at("type"), "object");
|
||||
ASSERT_TRUE(schema.at("properties").contains("plot"));
|
||||
ASSERT_TRUE(schema.at("properties").contains("values"));
|
||||
const auto& nested = schema.at("properties").at("values").at("items");
|
||||
EXPECT_TRUE(nested.at("properties").contains("gain"));
|
||||
EXPECT_EQ(nested.at("properties").at("mode").at("enum").size(), 2);
|
||||
}
|
||||
|
||||
TEST(Protocol_Type, Pfr_Codec_Round_Trips_Nested_Values) {
|
||||
const Request_Value input{"plot-3d", {{1.5, Sample_Mode::second}}};
|
||||
const auto encoded = encode_protocol_value(input);
|
||||
Request_Value decoded{};
|
||||
decode_protocol_value(decoded, encoded);
|
||||
EXPECT_EQ(decoded, input);
|
||||
}
|
||||
|
||||
TEST(Protocol_Type, Exact_Matcher_Overload_Wins_Without_If_Dispatch) {
|
||||
EXPECT_EQ(protocol_editor<Semantic_Value>(), "semantic-value");
|
||||
EXPECT_EQ(encode_protocol_value(Semantic_Value{7}).at("value"), 7);
|
||||
}
|
||||
|
||||
TEST(Protocol_Type, Non_Aggregate_Render_Type_Has_Explicit_Matcher) {
|
||||
render_2d::Color_Map color_map;
|
||||
color_map.stops = {Color::red_color(), Color::green_color()};
|
||||
const auto encoded = encode_protocol_value(color_map);
|
||||
render_2d::Color_Map decoded;
|
||||
decode_protocol_value(decoded, encoded);
|
||||
EXPECT_EQ(decoded, color_map);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user