完美一版
This commit is contained in:
@@ -18,7 +18,7 @@ struct Graph_WebSocket::Private {
|
||||
};
|
||||
Graph_WebSocket::Graph_WebSocket(drogon::WebSocketConnectionPtr connection, std::shared_ptr<Plot> plot) : d(std::make_unique<Private>()) { d->connection = connection; d->plot = std::move(plot); d->owner = connection.get(); }
|
||||
Graph_WebSocket::~Graph_WebSocket() { close(); }
|
||||
void Graph_WebSocket::start() { if (d->attached) return; const auto weak = weak_from_this(); d->plot->attach(d->owner, [weak](std::string pixels) { const auto socket = weak.lock(); if (!socket) return; const auto connection = socket->d->connection.lock(); if (connection && connection->connected()) connection->send(pixels.data(), pixels.size(), drogon::WebSocketMessageType::Binary); }); d->attached = true; }
|
||||
void Graph_WebSocket::start() { if (d->attached) return; const auto weak = weak_from_this(); d->plot->attach(d->owner, [weak](std::shared_ptr<const Plot_Frame_Message> frame) { const auto socket = weak.lock(); if (!socket) return; const auto connection = socket->d->connection.lock(); if (!connection || !connection->connected()) return; connection->send(frame->metadata, drogon::WebSocketMessageType::Text); connection->send(frame->pixels.data(), frame->pixels.size(), drogon::WebSocketMessageType::Binary); }); d->attached = true; }
|
||||
void Graph_WebSocket::receive(std::string_view message) {
|
||||
const auto json = nlohmann::json::parse(message, nullptr, false);
|
||||
if (json.is_discarded() || !json.is_object()) return;
|
||||
@@ -26,14 +26,16 @@ void Graph_WebSocket::receive(std::string_view message) {
|
||||
if (kind == json.end() || !kind->is_string()
|
||||
|| (*kind != "frame" && *kind != "input")) return;
|
||||
try {
|
||||
Plot_Event event;
|
||||
Plot_Frame_Request request;
|
||||
if (const auto value = json.find("request_id"); value != json.end() && value->is_number_unsigned())
|
||||
request.correlation_id = value->get<std::uint64_t>();
|
||||
if (const auto value = json.find("time"); value != json.end() && value->is_number())
|
||||
event.time_milliseconds = value->get<double>();
|
||||
request.time_milliseconds = value->get<double>();
|
||||
if (const auto viewport = json.find("viewport"); viewport != json.end() && viewport->is_object()) {
|
||||
if (const auto value = viewport->find("width"); value != viewport->end() && value->is_number_unsigned())
|
||||
event.width = std::clamp(value->get<std::uint32_t>(), 160U, 1920U);
|
||||
request.width = std::clamp(value->get<std::uint32_t>(), 160U, 1920U);
|
||||
if (const auto value = viewport->find("height"); value != viewport->end() && value->is_number_unsigned())
|
||||
event.height = std::clamp(value->get<std::uint32_t>(), 120U, 1080U);
|
||||
request.height = std::clamp(value->get<std::uint32_t>(), 120U, 1080U);
|
||||
}
|
||||
if (*kind == "input") {
|
||||
const auto input = json.find("event");
|
||||
@@ -47,8 +49,8 @@ void Graph_WebSocket::receive(std::string_view message) {
|
||||
if (value == input->end() || !value->is_object()) return;
|
||||
const auto x = value->value("x", 0.0);
|
||||
const auto y = value->value("y", 0.0);
|
||||
point.x = viewport_relative ? std::clamp(x, 0.0, static_cast<double>(event.width)) : x;
|
||||
point.y = viewport_relative ? std::clamp(y, 0.0, static_cast<double>(event.height)) : y;
|
||||
point.x = viewport_relative ? std::clamp(x, 0.0, static_cast<double>(request.width)) : x;
|
||||
point.y = viewport_relative ? std::clamp(y, 0.0, static_cast<double>(request.height)) : y;
|
||||
};
|
||||
read_point("position", decoded.position, true);
|
||||
read_point("global_position", decoded.global_position, false);
|
||||
@@ -62,9 +64,10 @@ void Graph_WebSocket::receive(std::string_view message) {
|
||||
decoded.key = magic_enum::enum_cast<Key>(input->value("key", std::string{"unknown"})).value_or(Key::unknown);
|
||||
decoded.native_key = input->value("native_key", 0U);
|
||||
decoded.auto_repeat = input->value("auto_repeat", false);
|
||||
event.input = decoded;
|
||||
d->plot->submit_input(std::move(decoded));
|
||||
return;
|
||||
}
|
||||
d->plot->submit(event);
|
||||
d->plot->submit_frame(d->owner, std::move(request));
|
||||
} catch (const nlohmann::json::exception&) {
|
||||
return;
|
||||
}
|
||||
|
||||
+261
-99
@@ -3,16 +3,17 @@
|
||||
#include <asio/co_spawn.hpp>
|
||||
#include <asio/error_code.hpp>
|
||||
#include <asio/experimental/concurrent_channel.hpp>
|
||||
#include <asio/post.hpp>
|
||||
#include <asio/redirect_error.hpp>
|
||||
#include <asio/strand.hpp>
|
||||
#include <asio/use_awaitable.hpp>
|
||||
#include <render_2D/plottable/Plottables.hpp>
|
||||
#include <render_3D/Render_3D.hpp>
|
||||
#include <magic_enum/magic_enum.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <numbers>
|
||||
@@ -33,23 +34,104 @@ using Frequency_Axis_Object = Impl<Frequency_Axis>;
|
||||
using Numeric_Axis_Object = Impl<Numeric_Axis>;
|
||||
using Time_Axis_Object = Impl<Time_Axis>;
|
||||
using Selection_Object = Impl<Selection_Rectangle_Overlay>;
|
||||
constexpr std::uint16_t frame_protocol_version{4};
|
||||
|
||||
template <typename Integer>
|
||||
void append_binary(std::string& output, Integer value) {
|
||||
const auto start = output.size();
|
||||
output.resize(start + sizeof(Integer));
|
||||
std::memcpy(output.data() + start, &value, sizeof(Integer));
|
||||
enum class Frame_Pacing_Mode : std::uint32_t {
|
||||
manual,
|
||||
fixed_rate,
|
||||
minimum_latency,
|
||||
maximum_rate
|
||||
};
|
||||
|
||||
struct Frame_Pacing_Properties {
|
||||
Frame_Pacing_Mode mode{Frame_Pacing_Mode::fixed_rate}; /* 控制后继 render 请求节奏的策略。 */
|
||||
double fixed_rate_fps{30.0}; /* 固定频率策略的目标帧率,单位为 FPS。 */
|
||||
double minimum_latency_headroom{1.25}; /* 最低延迟策略相对 P95 生成耗时的安全系数。 */
|
||||
};
|
||||
|
||||
class Frame_Policy final {
|
||||
public:
|
||||
[[nodiscard]] Frame_Pacing_Properties snapshot() const;
|
||||
[[nodiscard]] nlohmann::json schema() const;
|
||||
[[nodiscard]] nlohmann::json write_prop(std::string_view key, const nlohmann::json& value);
|
||||
private:
|
||||
mutable std::mutex mutex; /* 保护异步帧发布与 HTTP 属性编辑读取。 */
|
||||
Frame_Pacing_Properties pacing{}; /* 可编辑帧调度属性的唯一权威来源。 */
|
||||
};
|
||||
|
||||
std::string_view pacing_mode_name(Frame_Pacing_Mode mode) {
|
||||
switch (mode) {
|
||||
case Frame_Pacing_Mode::manual: return "manual";
|
||||
case Frame_Pacing_Mode::fixed_rate: return "fixed_rate";
|
||||
case Frame_Pacing_Mode::minimum_latency: return "minimum_latency";
|
||||
case Frame_Pacing_Mode::maximum_rate: return "maximum_rate";
|
||||
}
|
||||
throw std::logic_error("unknown frame pacing mode");
|
||||
}
|
||||
|
||||
std::string encode_frame(Image_View image, std::uint64_t sequence) {
|
||||
std::optional<Frame_Pacing_Mode> parse_pacing_mode(std::string_view value) {
|
||||
if (value == "manual") return Frame_Pacing_Mode::manual;
|
||||
if (value == "fixed_rate") return Frame_Pacing_Mode::fixed_rate;
|
||||
if (value == "minimum_latency") return Frame_Pacing_Mode::minimum_latency;
|
||||
if (value == "maximum_rate") return Frame_Pacing_Mode::maximum_rate;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Frame_Pacing_Properties Frame_Policy::snapshot() const { std::lock_guard lock(mutex); return pacing; }
|
||||
nlohmann::json Frame_Policy::schema() const {
|
||||
std::lock_guard lock(mutex);
|
||||
nlohmann::json fields = nlohmann::json::array();
|
||||
fields.push_back({{"key", "pacing_mode"}, {"label", "帧刷新策略"}, {"editor", "select"}, {"editable", true}, {"description", "选择浏览器如何安排下一次 render 调用。"}, {"technical_description", "Controls client-side render cadence using end-to-end samples computed by the browser."}, {"value", pacing_mode_name(pacing.mode)}, {"options", nlohmann::json::array({{{"value", "manual"}, {"label", "手动刷新"}}, {{"value", "fixed_rate"}, {"label", "固定频率"}}, {{"value", "minimum_latency"}, {"label", "最低延迟"}}, {{"value", "maximum_rate"}, {"label", "最高频率"}}})}});
|
||||
fields.push_back({{"key", "fixed_rate_fps"}, {"label", "固定目标帧率"}, {"editor", "number"}, {"editable", true}, {"description", "固定频率策略下每秒发起的 render 次数。"}, {"technical_description", "Target render request rate used by fixed_rate pacing, in frames per second."}, {"value", pacing.fixed_rate_fps}});
|
||||
fields.push_back({{"key", "minimum_latency_headroom"}, {"label", "最低延迟余量"}, {"editor", "number"}, {"editable", true}, {"description", "最低延迟策略使用的浏览器端 P95 端到端耗时安全系数。"}, {"technical_description", "Multiplier applied to browser-computed P95 request-to-pixel latency before scheduling the next render request."}, {"value", pacing.minimum_latency_headroom}});
|
||||
return {{"id", "frame-runtime"}, {"label", "帧策略与诊断"}, {"kind", "runtime"}, {"fields", std::move(fields)}, {"state", nlohmann::json::object()}};
|
||||
}
|
||||
|
||||
nlohmann::json Frame_Policy::write_prop(std::string_view key, const nlohmann::json& value) {
|
||||
std::lock_guard lock(mutex);
|
||||
if (key == "pacing_mode") {
|
||||
if (!value.is_string()) return {{"success", false}, {"error", "pacing_mode requires a string"}};
|
||||
const auto parsed = parse_pacing_mode(value.get_ref<const std::string&>());
|
||||
if (!parsed) return {{"success", false}, {"error", "unknown frame pacing mode"}};
|
||||
pacing.mode = *parsed;
|
||||
return {{"success", true}, {"component", "frame-runtime"}, {"key", key}, {"value", pacing_mode_name(pacing.mode)}};
|
||||
}
|
||||
if (key == "fixed_rate_fps") {
|
||||
if (!value.is_number()) return {{"success", false}, {"error", "fixed_rate_fps requires a number"}};
|
||||
const double next = value.get<double>();
|
||||
if (!std::isfinite(next) || next < 0.1 || next > 240.0) return {{"success", false}, {"error", "fixed_rate_fps must be between 0.1 and 240"}};
|
||||
pacing.fixed_rate_fps = next;
|
||||
return {{"success", true}, {"component", "frame-runtime"}, {"key", key}, {"value", pacing.fixed_rate_fps}};
|
||||
}
|
||||
if (key == "minimum_latency_headroom") {
|
||||
if (!value.is_number()) return {{"success", false}, {"error", "minimum_latency_headroom requires a number"}};
|
||||
const double next = value.get<double>();
|
||||
if (!std::isfinite(next) || next < 1.0 || next > 4.0) return {{"success", false}, {"error", "minimum_latency_headroom must be between 1 and 4"}};
|
||||
pacing.minimum_latency_headroom = next;
|
||||
return {{"success", true}, {"component", "frame-runtime"}, {"key", key}, {"value", pacing.minimum_latency_headroom}};
|
||||
}
|
||||
return {{"success", false}, {"error", "unknown frame runtime property"}};
|
||||
}
|
||||
|
||||
nlohmann::json frame_metadata(Render_Frame& frame, std::uint32_t width, std::uint32_t height, std::size_t byte_length, const Frame_Pacing_Properties& pacing) {
|
||||
nlohmann::json markers = nlohmann::json::object();
|
||||
for (const auto& point : frame.trace_points()) markers[std::string(magic_enum::enum_name(point.marker))] = point.elapsed_ns;
|
||||
nlohmann::json measurements = nlohmann::json::object();
|
||||
for (const auto& value : frame.trace_values()) measurements[std::string(magic_enum::enum_name(value.measurement))] = value.value_ns;
|
||||
const auto identity = frame.identity();
|
||||
return {{"kind", "frame_metadata"}, {"protocol", "aethera.frame"}, {"version", frame_protocol_version},
|
||||
{"sequence", identity.sequence}, {"correlation_id", identity.correlation_id},
|
||||
{"created_time_unix_ms", static_cast<double>(frame.created_time_unix_ns()) / 1'000'000.0},
|
||||
{"pixel", {{"width", width}, {"height", height}, {"format", "rgba8"}, {"byte_length", byte_length}}},
|
||||
{"pacing", {{"mode", pacing_mode_name(pacing.mode)}, {"fixed_rate_fps", pacing.fixed_rate_fps}, {"minimum_latency_headroom", pacing.minimum_latency_headroom}}},
|
||||
{"trace", {{"clock", "steady_elapsed_ns"}, {"markers", std::move(markers)}, {"measurements", std::move(measurements)}}}};
|
||||
}
|
||||
|
||||
std::shared_ptr<const Plot_Frame_Message> encode_frame(Frame_2D* frame, const Frame_Pacing_Properties& pacing) {
|
||||
frame->mark(Frame_Trace_Marker::websocket_publish_started);
|
||||
const Image_View image = frame->image();
|
||||
std::string output;
|
||||
output.reserve(24 + static_cast<std::size_t>(image.width) * image.height * 4);
|
||||
append_binary(output, std::uint32_t{0x41544852});
|
||||
append_binary(output, std::uint16_t{1});
|
||||
append_binary(output, std::uint16_t{});
|
||||
append_binary(output, static_cast<std::uint32_t>(image.width));
|
||||
append_binary(output, static_cast<std::uint32_t>(image.height));
|
||||
append_binary(output, sequence);
|
||||
output.reserve(static_cast<std::size_t>(image.width) * image.height * 4);
|
||||
for (int y = 0; y < image.height; ++y) {
|
||||
const auto* row = reinterpret_cast<const std::uint8_t*>(
|
||||
image.data + static_cast<std::ptrdiff_t>(y) * image.stride);
|
||||
@@ -61,36 +143,42 @@ std::string encode_frame(Image_View image, std::uint64_t sequence) {
|
||||
output.push_back(static_cast<char>(pixel[3]));
|
||||
}
|
||||
}
|
||||
return output;
|
||||
frame->mark(Frame_Trace_Marker::websocket_publish_finished);
|
||||
auto message = std::make_shared<Plot_Frame_Message>();
|
||||
message->pixels = std::move(output);
|
||||
message->metadata = frame_metadata(*frame, static_cast<std::uint32_t>(image.width), static_cast<std::uint32_t>(image.height), message->pixels.size(), pacing).dump();
|
||||
return message;
|
||||
}
|
||||
|
||||
std::string encode_frame(const Pixel_Frame& frame, std::uint64_t sequence) {
|
||||
std::string output;
|
||||
output.reserve(24 + frame.rgba8.size());
|
||||
append_binary(output, std::uint32_t{0x41544852});
|
||||
append_binary(output, std::uint16_t{1});
|
||||
append_binary(output, std::uint16_t{});
|
||||
append_binary(output, frame.extent.width);
|
||||
append_binary(output, frame.extent.height);
|
||||
append_binary(output, sequence);
|
||||
output.append(reinterpret_cast<const char*>(frame.rgba8.data()), frame.rgba8.size());
|
||||
return output;
|
||||
std::shared_ptr<const Plot_Frame_Message> encode_frame(Frame_3D* frame, const Frame_Pacing_Properties& pacing) {
|
||||
frame->mark(Frame_Trace_Marker::websocket_publish_started);
|
||||
const auto pixels = frame->pixels();
|
||||
auto message = std::make_shared<Plot_Frame_Message>();
|
||||
message->pixels.assign(reinterpret_cast<const char*>(pixels.data()), pixels.size());
|
||||
frame->mark(Frame_Trace_Marker::websocket_publish_finished);
|
||||
const auto extent = frame->extent();
|
||||
message->metadata = frame_metadata(*frame, extent.width, extent.height, message->pixels.size(), pacing).dump();
|
||||
return message;
|
||||
}
|
||||
|
||||
struct Schema_Query { Plot::Json_Handler handler; };
|
||||
struct Frame_Submission {
|
||||
const void* owner{}; /* 只向发起 render 的 WebSocket 连接返回完成帧。 */
|
||||
Plot_Frame_Request request{}; /* 浏览器帧请求及关联标识。 */
|
||||
};
|
||||
struct Prop_Write {
|
||||
std::string component;
|
||||
std::string key;
|
||||
nlohmann::json value;
|
||||
Plot::Json_Handler handler;
|
||||
};
|
||||
using Plot_Input = std::variant<Plot_Event, Schema_Query, Prop_Write>;
|
||||
using Plot_Input = std::variant<Frame_Submission, Schema_Query, Prop_Write>;
|
||||
|
||||
template <typename... Owned_Objects>
|
||||
class Scene_View_Model final : public Plot::Scene_View {
|
||||
public:
|
||||
Scene_View_Model(std::vector<std::unique_ptr<detail::Renderable_Descriptor>> value_descriptors,
|
||||
std::function<void(const Plot_Event&)> value_update,
|
||||
std::function<void(const Plot_Frame_Request&)> value_update,
|
||||
Owned_Objects... owned_objects)
|
||||
: descriptors(std::move(value_descriptors)),
|
||||
update_scene(std::move(value_update)),
|
||||
@@ -108,11 +196,11 @@ public:
|
||||
result["component"] = component;
|
||||
return result;
|
||||
}
|
||||
void update(const Plot_Event& event) override { update_scene(event); }
|
||||
void update(const Plot_Frame_Request& request) override { update_scene(request); }
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> descriptors;
|
||||
std::function<void(const Plot_Event&)> update_scene;
|
||||
std::function<void(const Plot_Frame_Request&)> update_scene;
|
||||
std::tuple<Owned_Objects...> objects;
|
||||
};
|
||||
|
||||
@@ -227,7 +315,7 @@ 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_Event&)> update,
|
||||
std::function<void(const Plot_Frame_Request&)> update,
|
||||
Owned_Objects&&... owned_objects) {
|
||||
using Definition = typename Object::Attached_Object;
|
||||
using Tag = typename Definition::Base_Tag;
|
||||
@@ -390,9 +478,8 @@ std::shared_ptr<Plot> make_axes_plot(asio::any_io_executor executor) {
|
||||
paint.add(frequency.get()); paint.add(numeric.get()); paint.add(time.get());
|
||||
});
|
||||
if (!topology) throw std::logic_error("axis gallery topology is invalid");
|
||||
auto update = [scene = scene.get(), frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Plot_Event& event) {
|
||||
auto update = [scene = scene.get(), frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, numeric, time);
|
||||
if (event.input) return;
|
||||
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))});
|
||||
@@ -425,9 +512,8 @@ std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
|
||||
scene_builder.add_renderable(selection.get());
|
||||
auto scene = *scene_builder.build();
|
||||
place_selection_over(scene.get(), selection.get(), spectrum.get());
|
||||
auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) {
|
||||
auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
if (event.input) 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();
|
||||
@@ -485,9 +571,8 @@ std::shared_ptr<Plot> make_frequency_trace_plot(asio::any_io_executor executor)
|
||||
scene_builder.add_renderable(selection.get());
|
||||
auto scene = *scene_builder.build();
|
||||
place_selection_over(scene.get(), selection.get(), trace.get());
|
||||
auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Event& event) {
|
||||
auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, time, vertical);
|
||||
if (event.input) 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))});
|
||||
@@ -525,23 +610,18 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
|
||||
scene_builder.add_renderable(selection.get());
|
||||
auto scene = *scene_builder.build();
|
||||
place_selection_over(scene.get(), selection.get(), sweep.get());
|
||||
auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get(), block_index = std::size_t{}](const Plot_Event& event) mutable {
|
||||
auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
if (event.input) 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);
|
||||
if (block_index >= block_count) {
|
||||
raw->template set<&Sweep_Spectrum::Prop::blocks>(std::vector<std::vector<Plot_Value>>{});
|
||||
block_index = 0;
|
||||
}
|
||||
const std::size_t block_index = state.blocks.size() < block_count ? state.blocks.size() : state.next_block_index % 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->append_block(values);
|
||||
++block_index;
|
||||
};
|
||||
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.">,
|
||||
@@ -553,10 +633,10 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
|
||||
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.">,
|
||||
Prop_Field<&Sweep_Spectrum::Prop::blocks, "blocks", "Current collection of incremental sweep blocks.">,
|
||||
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_block_count, "stored_block_count", "Number of sweep blocks currently retained.">,
|
||||
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_point_count, "stored_point_count", "Total number of frequency points retained across all blocks.">,
|
||||
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::rendered_point_count, "rendered_point_count", "Number of points emitted for the latest sweep frame.">>(
|
||||
Prop_Field<&Sweep_Spectrum::Prop::blocks, "blocks", "Latest data stored in each fixed frequency-segment slot.">,
|
||||
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_block_count, "stored_block_count", "Number of frequency segments that currently contain data.">,
|
||||
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_point_count, "stored_point_count", "Total number of points retained by the single composite sweep curve.">,
|
||||
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::rendered_point_count, "rendered_point_count", "Number of points emitted for the single composite sweep curve.">>(
|
||||
*sweep, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep), std::move(selection));
|
||||
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
@@ -580,9 +660,8 @@ std::shared_ptr<Plot> make_afterglow_plot(asio::any_io_executor executor) {
|
||||
scene_builder.add_renderable(selection.get());
|
||||
auto scene = *scene_builder.build();
|
||||
place_selection_over(scene.get(), selection.get(), afterglow.get());
|
||||
auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) {
|
||||
auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||
if (event.input) 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(
|
||||
@@ -626,9 +705,8 @@ std::shared_ptr<Plot> make_waterfall_plot(asio::any_io_executor executor) {
|
||||
scene_builder.add_renderable(selection.get());
|
||||
auto scene = *scene_builder.build();
|
||||
place_selection_over(scene.get(), selection.get(), waterfall.get());
|
||||
auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Event& event) {
|
||||
auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, time);
|
||||
if (event.input) return;
|
||||
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(
|
||||
@@ -679,9 +757,8 @@ std::shared_ptr<Plot> make_constellation_plot(asio::any_io_executor executor) {
|
||||
scene_builder.add_renderable(selection.get());
|
||||
auto scene = *scene_builder.build();
|
||||
place_selection_over(scene.get(), selection.get(), constellation.get());
|
||||
auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) {
|
||||
auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
|
||||
if (event.input) 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;
|
||||
@@ -724,7 +801,7 @@ std::shared_ptr<Plot> make_selection_overlay_plot(asio::any_io_executor executor
|
||||
.set(&Render_Scene_2D::Prop::view_active, true);
|
||||
scene_builder.add_renderable(selection.get());
|
||||
auto scene = *scene_builder.build();
|
||||
auto update = [scene = scene.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) {
|
||||
auto update = [scene = scene.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
|
||||
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
|
||||
};
|
||||
auto view = make_scene_view<
|
||||
@@ -743,33 +820,119 @@ std::shared_ptr<Plot> make_selection_overlay_plot(asio::any_io_executor executor
|
||||
|
||||
struct Plot::Private {
|
||||
using Scene = std::variant<std::unique_ptr<Scene_2D>, std::unique_ptr<Scene_3D>>;
|
||||
asio::strand<asio::any_io_executor> strand;
|
||||
asio::experimental::concurrent_channel<void(asio::error_code, Plot_Input)> inputs;
|
||||
Scene scene;
|
||||
std::unique_ptr<Scene_View> view;
|
||||
std::once_flag start_once;
|
||||
std::mutex handlers_mutex;
|
||||
std::unordered_map<const void*, Frame_Handler> handlers;
|
||||
std::atomic_uint64_t frame_sequence{};
|
||||
using Frame = std::variant<std::unique_ptr<Frame_2D>, std::unique_ptr<Frame_3D>>;
|
||||
struct Managed_Frame {
|
||||
const void* owner{}; /* 发起本帧的 WebSocket 连接身份。 */
|
||||
Plot_Frame_Request request{}; /* 创建本帧的浏览器请求及 viewport。 */
|
||||
Frame frame{}; /* Web 层唯一拥有并传给 Scene 的外部帧。 */
|
||||
};
|
||||
asio::strand<asio::any_io_executor> strand; /* 串行执行帧请求、属性写入和 Schema 查询。 */
|
||||
asio::experimental::concurrent_channel<void(asio::error_code, Plot_Input)> inputs; /* 不承载输入事件的异步命令通道。 */
|
||||
std::unique_ptr<Scene_View> view; /* 使用层数据推进及反射描述实现;拥有 Scene 引用的图元。 */
|
||||
Scene scene; /* 当前 Plot 唯一拥有的 2D 或 3D Scene;析构先于 view 所有图元。 */
|
||||
std::once_flag start_once; /* 保证回调与协程只安装一次。 */
|
||||
std::mutex handlers_mutex; /* 保护跨 Drogon 连接线程修改的订阅表。 */
|
||||
std::unordered_map<const void*, Frame_Handler> handlers; /* 以连接身份索引的完成帧订阅。 */
|
||||
std::uint64_t next_frame_sequence{1}; /* 下一外部帧使用的单调序号;只在 strand 访问。 */
|
||||
Frame_Policy frame_policy{}; /* 仅保存可编辑刷新策略,不保存衍生统计。 */
|
||||
std::optional<Managed_Frame> active_frame{}; /* 当前由 Scene/异步后端借用指针的外部帧。 */
|
||||
std::deque<const void*> pending_order{}; /* 按首次等待顺序保存连接身份,避免连接间饥饿。 */
|
||||
std::unordered_map<const void*, Managed_Frame> pending_frames{}; /* 每个连接只保留最新一个尚未提交 Scene 的外部帧。 */
|
||||
bool frame_in_flight{}; /* Scene 是否已有一次尚未完成回调的帧。 */
|
||||
|
||||
template <typename Scene_Object>
|
||||
Private(asio::any_io_executor executor,
|
||||
std::unique_ptr<Scene_Object> value_scene,
|
||||
std::unique_ptr<Scene_View> value_view)
|
||||
: strand(asio::make_strand(std::move(executor))), inputs(strand, 32),
|
||||
scene(std::move(value_scene)), view(std::move(value_view)) {}
|
||||
view(std::move(value_view)), scene(std::move(value_scene)) {}
|
||||
|
||||
void publish(std::string pixels) {
|
||||
std::vector<Frame_Handler> outputs;
|
||||
void publish(const void* owner, std::shared_ptr<const Plot_Frame_Message> frame) {
|
||||
Frame_Handler output;
|
||||
{
|
||||
std::lock_guard lock(handlers_mutex);
|
||||
outputs.reserve(handlers.size());
|
||||
for (const auto& [owner, handler] : handlers) outputs.push_back(handler);
|
||||
const auto found = handlers.find(owner);
|
||||
if (found != handlers.end()) output = found->second;
|
||||
}
|
||||
for (auto& output : outputs) output(pixels);
|
||||
if (output) output(std::move(frame));
|
||||
}
|
||||
|
||||
[[nodiscard]] nlohmann::json schema() const;
|
||||
[[nodiscard]] Managed_Frame make_frame(Frame_Submission submission);
|
||||
void request_frame(Frame_Submission submission);
|
||||
void render_frame(Managed_Frame frame);
|
||||
void publish_completed_frame(Render_Frame* frame);
|
||||
void frame_completed();
|
||||
};
|
||||
|
||||
nlohmann::json Plot::Private::schema() const {
|
||||
auto result = view->schema();
|
||||
result["components"].push_back(frame_policy.schema());
|
||||
return result;
|
||||
}
|
||||
|
||||
Plot::Private::Managed_Frame Plot::Private::make_frame(Frame_Submission submission) {
|
||||
const Frame_Identity identity{next_frame_sequence++, submission.request.correlation_id};
|
||||
if (std::holds_alternative<std::unique_ptr<Scene_2D>>(scene)) return {submission.owner, std::move(submission.request), std::make_unique<Frame_2D>(identity)};
|
||||
return {submission.owner, std::move(submission.request), std::make_unique<Frame_3D>(identity)};
|
||||
}
|
||||
|
||||
void Plot::Private::request_frame(Frame_Submission submission) {
|
||||
auto frame = make_frame(std::move(submission));
|
||||
if (frame_in_flight) {
|
||||
const auto found = pending_frames.find(frame.owner);
|
||||
if (found != pending_frames.end()) found->second = std::move(frame);
|
||||
else {
|
||||
const auto owner = frame.owner;
|
||||
pending_order.push_back(owner);
|
||||
pending_frames.emplace(owner, std::move(frame));
|
||||
}
|
||||
return;
|
||||
}
|
||||
render_frame(std::move(frame));
|
||||
}
|
||||
|
||||
void Plot::Private::render_frame(Managed_Frame frame) {
|
||||
frame_in_flight = true;
|
||||
active_frame = std::move(frame);
|
||||
auto& request = active_frame->request;
|
||||
request.width = std::clamp(request.width, 160U, 1920U);
|
||||
request.height = std::clamp(request.height, 120U, 1080U);
|
||||
view->update(request);
|
||||
if (auto* scene_2d = std::get_if<std::unique_ptr<Scene_2D>>(&scene)) {
|
||||
(*scene_2d)->set<&Render_Scene_2D::Prop::viewport>(Size{static_cast<int>(request.width), static_cast<int>(request.height)});
|
||||
const auto result = (*scene_2d)->render(std::get<std::unique_ptr<Frame_2D>>(active_frame->frame).get());
|
||||
if (result != Render_Scene_2D::Render_Result::completed) asio::post(strand, [this] { frame_completed(); });
|
||||
return;
|
||||
}
|
||||
auto& scene_3d = std::get<std::unique_ptr<Scene_3D>>(scene);
|
||||
scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{request.width, request.height});
|
||||
const auto result = scene_3d->render(std::get<std::unique_ptr<Frame_3D>>(active_frame->frame).get());
|
||||
if (result != Render_Scene_3D::Render_Result::submitted) asio::post(strand, [this] { frame_completed(); });
|
||||
}
|
||||
|
||||
void Plot::Private::publish_completed_frame(Render_Frame* frame) {
|
||||
if (!active_frame) throw std::logic_error("frame callback has no externally owned active frame");
|
||||
const auto pacing = frame_policy.snapshot();
|
||||
if (auto* frame_2d = std::get_if<std::unique_ptr<Frame_2D>>(&active_frame->frame); frame_2d && frame_2d->get() == frame) publish(active_frame->owner, encode_frame(frame_2d->get(), pacing));
|
||||
else if (auto* frame_3d = std::get_if<std::unique_ptr<Frame_3D>>(&active_frame->frame); frame_3d && frame_3d->get() == frame) publish(active_frame->owner, encode_frame(frame_3d->get(), pacing));
|
||||
else throw std::logic_error("frame callback does not match the externally owned active frame");
|
||||
frame_completed();
|
||||
}
|
||||
|
||||
void Plot::Private::frame_completed() {
|
||||
active_frame.reset();
|
||||
frame_in_flight = false;
|
||||
while (!pending_order.empty()) {
|
||||
const auto owner = pending_order.front();
|
||||
pending_order.pop_front();
|
||||
auto next = pending_frames.extract(owner);
|
||||
if (next.empty()) continue;
|
||||
render_frame(std::move(next.mapped()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Plot::Plot(asio::any_io_executor executor,
|
||||
std::unique_ptr<Scene_2D> scene,
|
||||
std::unique_ptr<Scene_View> view)
|
||||
@@ -786,18 +949,18 @@ void Plot::ensure_started() {
|
||||
std::call_once(d->start_once, [this] {
|
||||
auto self = shared_from_this();
|
||||
if (auto* scene = std::get_if<std::unique_ptr<Scene_2D>>(&d->scene)) {
|
||||
(*scene)->set_frame_callback([weak = weak_from_this()](Image_View image) {
|
||||
(*scene)->set_frame_callback([weak = weak_from_this()](Frame_2D* frame) {
|
||||
if (auto owner = weak.lock()) {
|
||||
const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1;
|
||||
owner->d->publish(encode_frame(image, sequence));
|
||||
frame->mark(Frame_Trace_Marker::callback_finished);
|
||||
asio::post(owner->d->strand, [weak, frame] { if (auto next_owner = weak.lock()) next_owner->d->publish_completed_frame(frame); });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
std::get<std::unique_ptr<Scene_3D>>(d->scene)->set_frame_callback(
|
||||
[weak = weak_from_this()](std::shared_ptr<const Pixel_Frame> frame) {
|
||||
[weak = weak_from_this()](Frame_3D* frame) {
|
||||
if (auto owner = weak.lock()) {
|
||||
const auto sequence = owner->d->frame_sequence.fetch_add(1, std::memory_order_acq_rel) + 1;
|
||||
owner->d->publish(encode_frame(*frame, sequence));
|
||||
frame->mark(Frame_Trace_Marker::callback_finished);
|
||||
asio::post(owner->d->strand, [weak, frame] { if (auto next_owner = weak.lock()) next_owner->d->publish_completed_frame(frame); });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -808,28 +971,17 @@ void Plot::ensure_started() {
|
||||
asio::redirect_error(asio::use_awaitable, error));
|
||||
if (error) co_return;
|
||||
if (auto* query = std::get_if<Schema_Query>(&input)) {
|
||||
query->handler(self->d->view->schema());
|
||||
query->handler(self->d->schema());
|
||||
continue;
|
||||
}
|
||||
if (auto* write = std::get_if<Prop_Write>(&input)) {
|
||||
write->handler(self->d->view->write_prop(write->component, write->key, write->value));
|
||||
write->handler(write->component == "frame-runtime"
|
||||
? self->d->frame_policy.write_prop(write->key, write->value)
|
||||
: self->d->view->write_prop(write->component, write->key, write->value));
|
||||
continue;
|
||||
}
|
||||
auto event = std::get<Plot_Event>(input);
|
||||
event.width = std::clamp(event.width, 160U, 1920U);
|
||||
event.height = std::clamp(event.height, 120U, 1080U);
|
||||
self->d->view->update(event);
|
||||
if (auto* scene = std::get_if<std::unique_ptr<Scene_2D>>(&self->d->scene)) {
|
||||
(*scene)->set<&Render_Scene_2D::Prop::viewport>(
|
||||
Size{static_cast<int>(event.width), static_cast<int>(event.height)});
|
||||
if (event.input) dispatch_plot_input(**scene, *event.input);
|
||||
(*scene)->render();
|
||||
} else {
|
||||
auto& scene_3d = std::get<std::unique_ptr<Scene_3D>>(self->d->scene);
|
||||
scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{event.width, event.height});
|
||||
if (event.input) dispatch_plot_input(*scene_3d, *event.input);
|
||||
scene_3d->render();
|
||||
}
|
||||
auto submission = std::get<Frame_Submission>(input);
|
||||
self->d->request_frame(std::move(submission));
|
||||
}
|
||||
}, [](std::exception_ptr exception) {
|
||||
if (exception) std::rethrow_exception(exception);
|
||||
@@ -850,9 +1002,15 @@ void Plot::detach(const void* owner) {
|
||||
d->handlers.erase(owner);
|
||||
}
|
||||
|
||||
void Plot::submit(Plot_Event event) {
|
||||
void Plot::submit_frame(const void* owner, Plot_Frame_Request request) {
|
||||
ensure_started();
|
||||
static_cast<void>(d->inputs.try_send(asio::error_code{}, Plot_Input{event}));
|
||||
static_cast<void>(d->inputs.try_send(asio::error_code{}, Plot_Input{Frame_Submission{owner, std::move(request)}}));
|
||||
}
|
||||
|
||||
void Plot::submit_input(Plot_Input_Event event) {
|
||||
ensure_started();
|
||||
if (auto* scene = std::get_if<std::unique_ptr<Scene_2D>>(&d->scene)) dispatch_plot_input(**scene, event);
|
||||
else dispatch_plot_input(*std::get<std::unique_ptr<Scene_3D>>(d->scene), event);
|
||||
}
|
||||
|
||||
void Plot::async_schema(Json_Handler handler) {
|
||||
@@ -876,10 +1034,14 @@ std::shared_ptr<Plot> make_datoviz_point_plot(asio::any_io_executor executor) {
|
||||
render_3d::Point{.position = {0.0F, 0.5F, 0.0F}, .color = Color::green_color(), .diameter_px = 30.0F},
|
||||
render_3d::Point{.position = {0.55F, -0.1F, 0.0F}, .color = Color{42, 120, 255, 255}, .diameter_px = 26.0F}}));
|
||||
visual->advance();
|
||||
auto scene = *Scene_3D::Builder(visual.get())
|
||||
/* 保留派生 Builder 身份;链式 set() 返回公共 Builder,不能用其 build() 绕过 3D 后端挂接。 */
|
||||
Scene_3D::Builder scene_builder(visual.get());
|
||||
scene_builder
|
||||
.set(&Render_Scene_3D::Prop::viewport, Extent{720, 420})
|
||||
.set(&Render_Scene_3D::Prop::view_active, true)
|
||||
.build();
|
||||
.set(&Render_Scene_3D::Prop::view_active, true);
|
||||
auto scene_result = scene_builder.build();
|
||||
if (!scene_result) throw std::logic_error("Datoviz point plot dependency graph is invalid");
|
||||
auto scene = std::move(scene_result).value();
|
||||
using Adapter = detail::Renderable_Adapter<Visual_Object,
|
||||
detail::Prop_Field<&Point_Visual::Prop::transform, "transform", "World transform applied to every point in the visual.">,
|
||||
detail::Prop_Field<&Point_Visual::Prop::visible, "visible", "Controls whether the point visual participates in scene rendering.">,
|
||||
@@ -893,7 +1055,7 @@ std::shared_ptr<Plot> make_datoviz_point_plot(asio::any_io_executor executor) {
|
||||
components.push_back(detail::make_renderable_descriptor("plot", "主绘图组件", "visual", Adapter{*visual}));
|
||||
auto view = std::make_unique<Scene_View_Model<decltype(visual)>>(
|
||||
std::move(components),
|
||||
[](const Plot_Event&) {}, std::move(visual));
|
||||
[](const Plot_Frame_Request&) {}, std::move(visual));
|
||||
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -6,7 +6,6 @@
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
namespace aethera::web {
|
||||
@@ -25,15 +24,19 @@ struct Plot_Input_Event {
|
||||
std::uint32_t native_key{};
|
||||
bool auto_repeat{};
|
||||
};
|
||||
struct Plot_Event {
|
||||
double time_milliseconds{};
|
||||
std::uint32_t width{720};
|
||||
std::uint32_t height{420};
|
||||
std::optional<Plot_Input_Event> input{};
|
||||
struct Plot_Frame_Request {
|
||||
std::uint64_t correlation_id{}; /* 浏览器请求标识;完成帧元数据原样返回。 */
|
||||
double time_milliseconds{}; /* 使用层推进图表时间轴的本地日内毫秒。 */
|
||||
std::uint32_t width{720}; /* 请求帧宽度,单位为物理像素。 */
|
||||
std::uint32_t height{420}; /* 请求帧高度,单位为物理像素。 */
|
||||
};
|
||||
struct Plot_Frame_Message {
|
||||
std::string metadata{}; /* 单帧 JSON 元数据 WebSocket 文本消息。 */
|
||||
std::string pixels{}; /* 紧随元数据发送的纯 RGBA8 WebSocket 二进制消息。 */
|
||||
};
|
||||
class Plot final : public std::enable_shared_from_this<Plot> {
|
||||
public:
|
||||
using Frame_Handler = std::function<void(std::string)>;
|
||||
using Frame_Handler = std::function<void(std::shared_ptr<const Plot_Frame_Message>)>;
|
||||
using Json_Handler = std::function<void(nlohmann::json)>;
|
||||
class Scene_View {
|
||||
public:
|
||||
@@ -41,7 +44,7 @@ public:
|
||||
[[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;
|
||||
virtual void update(const Plot_Event& event) = 0;
|
||||
virtual void update(const Plot_Frame_Request& request) = 0;
|
||||
};
|
||||
Plot(asio::any_io_executor executor,
|
||||
std::unique_ptr<Impl<render_2d::Render_Scene_2D>> scene,
|
||||
@@ -54,7 +57,8 @@ public:
|
||||
Plot& operator=(const Plot&) = delete;
|
||||
void attach(const void* owner, Frame_Handler handler);
|
||||
void detach(const void* owner);
|
||||
void submit(Plot_Event event);
|
||||
void submit_frame(const void* owner, Plot_Frame_Request request);
|
||||
void submit_input(Plot_Input_Event event);
|
||||
void async_schema(Json_Handler handler);
|
||||
void async_write_prop(std::string component, std::string key, nlohmann::json value, Json_Handler handler);
|
||||
private:
|
||||
|
||||
@@ -299,6 +299,8 @@ nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std::
|
||||
{"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();
|
||||
|
||||
Reference in New Issue
Block a user