367 lines
20 KiB
C++
367 lines
20 KiB
C++
#pragma once
|
|
#include <boost/pfr.hpp>
|
|
#include <magic_enum/magic_enum.hpp>
|
|
#include <nlohmann/json.hpp>
|
|
#include <render_2D/plottable/Plottables.hpp>
|
|
#include <render_3D/Render_3D.hpp>
|
|
#include <array>
|
|
#include <deque>
|
|
#include <limits>
|
|
#include <ranges>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <tuple>
|
|
#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>
|
|
struct Prop_Field {
|
|
static constexpr auto member = Member;
|
|
static constexpr auto key = Key;
|
|
static constexpr auto description = Description;
|
|
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>
|
|
class Renderable_Adapter final : public structive::Property_Object<Renderable_Adapter<Object, Fields...>, structive::No_Lock_Policy> {
|
|
public:
|
|
explicit Renderable_Adapter(Object& value) : object(&value) {}
|
|
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>
|
|
struct Renderable_Field_Accessor<Renderable_Adapter<Object, Fields...>, Prop_Field<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 = 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 T> struct is_std_array : std::false_type {};
|
|
template <typename T, std::size_t Size> struct is_std_array<std::array<T, Size>> : std::true_type {};
|
|
template <typename T> inline constexpr bool is_std_array_v = is_std_array<std::remove_cvref_t<T>>::value;
|
|
template <typename T> struct is_pair : std::false_type {};
|
|
template <typename A, typename B> struct is_pair<std::pair<A, B>> : std::true_type {};
|
|
template <typename T> inline constexpr bool is_pair_v = is_pair<std::remove_cvref_t<T>>::value;
|
|
|
|
template <typename T>
|
|
concept Json_Sequence = std::ranges::range<T> && requires(T value, typename T::value_type item) {
|
|
value.clear();
|
|
value.emplace_back(std::move(item));
|
|
} && !std::same_as<std::remove_cvref_t<T>, std::string>;
|
|
|
|
template <typename Value> nlohmann::json encode_protocol_value(const Value& value);
|
|
template <typename Value> void decode_protocol_value(Value& target, const nlohmann::json& input);
|
|
|
|
template <typename Value, std::size_t... Index>
|
|
nlohmann::json encode_aggregate(const Value& value, std::index_sequence<Index...>) {
|
|
nlohmann::json result = nlohmann::json::object();
|
|
((result[std::string(boost::pfr::get_name<Index, Value>())] =
|
|
encode_protocol_value(boost::pfr::get<Index>(value))), ...);
|
|
return result;
|
|
}
|
|
|
|
template <typename Value, std::size_t... Index>
|
|
void decode_aggregate(Value& target, const nlohmann::json& input, std::index_sequence<Index...>) {
|
|
(decode_protocol_value(boost::pfr::get<Index>(target),
|
|
input.at(std::string(boost::pfr::get_name<Index, Value>()))), ...);
|
|
}
|
|
|
|
template <typename Value>
|
|
nlohmann::json encode_protocol_value(const Value& value) {
|
|
using Type = std::remove_cvref_t<Value>;
|
|
if constexpr (std::same_as<Type, bool> || std::integral<Type> || std::floating_point<Type> || std::same_as<Type, std::string>) {
|
|
return value;
|
|
} else if constexpr (std::same_as<Type, char>) {
|
|
return std::string(1, value);
|
|
} else if constexpr (std::is_enum_v<Type>) {
|
|
return magic_enum::enum_name(value);
|
|
} else if constexpr (std::same_as<Type, render_2d::Color_Map>) {
|
|
return {{"stops", encode_protocol_value(value.stops)}};
|
|
} else if constexpr (is_pair_v<Type>) {
|
|
return nlohmann::json::array({encode_protocol_value(value.first), encode_protocol_value(value.second)});
|
|
} else if constexpr (is_std_array_v<Type> || Json_Sequence<Type>) {
|
|
nlohmann::json result = nlohmann::json::array();
|
|
for (const auto& item : value) result.push_back(encode_protocol_value(item));
|
|
return result;
|
|
} else if constexpr (std::is_aggregate_v<Type>) {
|
|
return encode_aggregate(value, std::make_index_sequence<boost::pfr::tuple_size_v<Type>>{});
|
|
} else {
|
|
static_assert(std::is_aggregate_v<Type>, "Aethera protocol needs an explicit codec for this value type");
|
|
}
|
|
}
|
|
|
|
template <typename Value>
|
|
void decode_protocol_value(Value& target, const nlohmann::json& input) {
|
|
using Type = std::remove_cvref_t<Value>;
|
|
if constexpr (std::same_as<Type, bool> || std::integral<Type> || std::floating_point<Type> || std::same_as<Type, std::string>) {
|
|
target = input.template get<Type>();
|
|
} else if constexpr (std::same_as<Type, char>) {
|
|
const auto text = input.template get<std::string>();
|
|
if (text.size() != 1) throw std::invalid_argument("character value must contain exactly one character");
|
|
target = text.front();
|
|
} else if constexpr (std::is_enum_v<Type>) {
|
|
const auto value = magic_enum::enum_cast<Type>(input.template get<std::string>());
|
|
if (!value) throw std::invalid_argument("unknown enum value");
|
|
target = *value;
|
|
} else if constexpr (std::same_as<Type, render_2d::Color_Map>) {
|
|
decode_protocol_value(target.stops, input.at("stops"));
|
|
} else if constexpr (is_pair_v<Type>) {
|
|
if (!input.is_array() || input.size() != 2) throw std::invalid_argument("pair value must be a two-item array");
|
|
decode_protocol_value(target.first, input[0]);
|
|
decode_protocol_value(target.second, input[1]);
|
|
} else if constexpr (is_std_array_v<Type>) {
|
|
if (!input.is_array() || input.size() != target.size()) throw std::invalid_argument("array has the wrong size");
|
|
for (std::size_t index = 0; index < target.size(); ++index) decode_protocol_value(target[index], input[index]);
|
|
} else if constexpr (Json_Sequence<Type>) {
|
|
if (!input.is_array()) throw std::invalid_argument("value must be an array");
|
|
Type updated;
|
|
for (const auto& encoded : input) {
|
|
typename Type::value_type item{};
|
|
decode_protocol_value(item, encoded);
|
|
updated.emplace_back(std::move(item));
|
|
}
|
|
target = std::move(updated);
|
|
} else if constexpr (std::is_aggregate_v<Type>) {
|
|
if (!input.is_object()) throw std::invalid_argument("value must be an object");
|
|
decode_aggregate(target, input, std::make_index_sequence<boost::pfr::tuple_size_v<Type>>{});
|
|
} else {
|
|
static_assert(std::is_aggregate_v<Type>, "Aethera protocol needs an explicit codec for this value type");
|
|
}
|
|
}
|
|
|
|
template <typename Value>
|
|
constexpr std::string_view protocol_editor() {
|
|
using Type = std::remove_cvref_t<Value>;
|
|
if constexpr (std::same_as<Type, bool>) return "boolean";
|
|
if constexpr (std::integral<Type>) return "integer";
|
|
if constexpr (std::floating_point<Type>) return "number";
|
|
if constexpr (std::same_as<Type, std::string> || std::same_as<Type, char>) return "text";
|
|
if constexpr (std::is_enum_v<Type>) return "select";
|
|
if constexpr (std::same_as<Type, Color> || std::same_as<Type, render_3d::Linear_Color>) return "color";
|
|
if constexpr (std::same_as<Type, render_2d::Point_F>) return "point2";
|
|
if constexpr (std::same_as<Type, render_2d::Size> || std::same_as<Type, render_3d::Extent>) return "size";
|
|
if constexpr (std::same_as<Type, render_2d::Rect_F>) return "rect";
|
|
if constexpr (std::same_as<Type, render_2d::Axis_Range>) return "range";
|
|
if constexpr (std::same_as<Type, render_2d::Pen>) return "pen";
|
|
if constexpr (std::same_as<Type, render_2d::Brush>) return "brush";
|
|
if constexpr (std::same_as<Type, render_2d::Font>) return "font";
|
|
if constexpr (std::same_as<Type, render_2d::Color_Map>) return "color-map";
|
|
if constexpr (std::same_as<Type, render_3d::Vec3>) return "vector3";
|
|
if constexpr (std::same_as<Type, render_3d::Matrix4>) return "matrix4";
|
|
return "json";
|
|
}
|
|
|
|
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", "启用视图"},
|
|
{"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_mode", "分区模式"},
|
|
{"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", "已选区域"}, {"transform", "空间变换"}, {"visible", "是否可见"},
|
|
{"depth_test", "深度测试"}, {"items", "项目数据"},
|
|
{"prepare_dirty", "准备阶段待更新"}, {"paint_dirty", "绘制阶段待更新"},
|
|
{"prepare_executed", "准备阶段已执行"}, {"paint_executed", "绘制阶段已执行"},
|
|
{"prepare_graph_rebuilt", "准备任务图已重建"}, {"paint_graph_rebuilt", "绘制任务图已重建"},
|
|
{"prepare_task_count", "准备任务数量"}, {"paint_task_count", "绘制任务数量"},
|
|
{"prepare_execution_time_ns", "准备耗时"}, {"paint_execution_time_ns", "绘制耗时"},
|
|
{"taskflow_rebuilt", "场景任务图已重建"}, {"renderable_count", "可渲染对象数量"},
|
|
{"taskflow_task_count", "场景任务数量"}, {"taskflow_dependency_count", "任务依赖数量"},
|
|
{"taskflow_max_predecessors", "最大前驱数量"}, {"taskflow_max_successors", "最大后继数量"},
|
|
{"taskflow_execution_time_ns", "场景执行耗时"}, {"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();
|
|
nlohmann::json state = nlohmann::json::object();
|
|
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 auto value = field.accessor.read(adapter);
|
|
const bool editable = field.template attribute<Renderable_Field_Role_Category>().value == Renderable_Field_Role::prop;
|
|
const auto field_label = protocol_field_label(field.key());
|
|
nlohmann::json item{{"key", field.key()}, {"label", field_label}, {"editor", protocol_editor<Value>()},
|
|
{"editable", editable},
|
|
{"description", std::string(field_label) + "。协议字段:" + std::string(field.key()) + "。"},
|
|
{"technical_description", std::string(Field::accessor_type::description.view())},
|
|
{"value", encode_protocol_value(value)}};
|
|
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}});
|
|
}
|
|
}
|
|
} else {
|
|
state[field.key()] = encode_protocol_value(value);
|
|
}
|
|
fields.push_back(std::move(item));
|
|
});
|
|
return {{"id", id}, {"label", label}, {"kind", kind},
|
|
{"fields", std::move(fields)}, {"state", std::move(state)}};
|
|
}
|
|
|
|
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{};
|
|
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"] = 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)) {}
|
|
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>::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));
|
|
}
|
|
}
|