Files
Renderive/web_server/app/Gallery_Protocol.cpp
T
2026-08-17 23:34:59 +08:00

246 lines
12 KiB
C++

#include "Gallery_Protocol.h"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <array>
#include <string>
#include <utility>
namespace renderive::web {
namespace {
using Json = nlohmann::json;
struct Case_Info { std::string_view id, title, component, category; };
constexpr std::array cases{
Case_Info{"axis_lab", "Axis Lab", "Axis", "2D"},
Case_Info{"spectrum", "Spectrum", "Spectrum", "2D"},
Case_Info{"afterglow", "Afterglow", "Afterglow", "2D"},
Case_Info{"sweep_spectrum", "Sweep Spectrum", "Sweep_Spectrum", "2D"},
Case_Info{"waterfall", "Waterfall", "Waterfall", "2D"},
Case_Info{"frequency_trace", "Frequency Trace", "Frequency_Trace", "2D"},
Case_Info{"selection_overlay", "Selection Overlay", "Selection", "2D"},
Case_Info{"constellation", "Constellation", "Constellation_Diagram", "2D"},
Case_Info{"datoviz_3d", "Datoviz 3D", "Point", "3D"},
Case_Info{"datoviz_splat", "Datoviz Splat", "Splat", "3D"},
Case_Info{"datoviz_pixel", "Datoviz Pixel", "Pixel", "3D"},
Case_Info{"datoviz_marker", "Datoviz Marker", "Marker", "3D"},
Case_Info{"datoviz_sphere", "Datoviz Sphere", "Sphere", "3D"},
Case_Info{"datoviz_segment", "Datoviz Segment", "Segment", "3D"},
Case_Info{"datoviz_vector", "Datoviz Vector", "Vector", "3D"},
Case_Info{"datoviz_primitive", "Datoviz Primitive", "Primitive", "3D"},
Case_Info{"datoviz_mesh", "Datoviz Mesh", "Mesh", "3D"},
Case_Info{"datoviz_path", "Datoviz Path", "Path", "3D"},
Case_Info{"datoviz_image", "Datoviz Image", "Image", "3D"},
Case_Info{"datoviz_labels", "Datoviz Labels", "Labels", "3D"},
Case_Info{"datoviz_glyph", "Datoviz Glyph", "Glyph", "3D"},
Case_Info{"datoviz_text", "Datoviz Text", "Text", "3D"},
Case_Info{"datoviz_volume", "Datoviz Volume", "Volume", "3D"},
};
std::string_view mode_id(Gallery_Frame_Mode mode) {
switch (mode) {
case Gallery_Frame_Mode::Manual: return "manual";
case Gallery_Frame_Mode::Low_Latency: return "low_latency";
case Gallery_Frame_Mode::Playback: return "playback";
}
return "low_latency";
}
std::optional<Gallery_Frame_Mode> parse_mode(std::string_view value) {
if (value == "manual") return Gallery_Frame_Mode::Manual;
if (value == "low_latency") return Gallery_Frame_Mode::Low_Latency;
if (value == "playback") return Gallery_Frame_Mode::Playback;
return std::nullopt;
}
Json parse_object(std::string_view text) {
Json value = Json::parse(text, nullptr, false);
return value.is_object() ? std::move(value) : Json::object();
}
Json actions() {
Json result = Json::array({
{{"id", "frame_strategy_low_latency"}, {"label", "Low latency"},
{"api", "Renderable_Editor::replace_frame_control_strategy"},
{"group", "Frame strategy"}, {"request_frame", true}},
{{"id", "frame_strategy_manual"}, {"label", "Manual"},
{"api", "Renderable_Editor::replace_frame_control_strategy"},
{"group", "Frame strategy"}, {"request_frame", true}},
{{"id", "frame_strategy_playback"}, {"label", "Playback"},
{"api", "Renderable_Editor::replace_frame_control_strategy"},
{"group", "Frame strategy"}, {"request_frame", true}}
});
result.push_back({
{"id", "capture_next_frame"}, {"label", "Capture next frame"},
{"api", "Scene_Base::capture_next_frame"},
{"group", "Performance Capture"}, {"request_frame", true}
});
result.push_back({
{"id", "capture_frames"}, {"label", "Capture N frames"},
{"api", "Scene_Base::capture_frames"}, {"group", "Performance Capture"},
{"argument_input", "number"}, {"argument_default", 20},
{"request_frame", true}
});
result.push_back({
{"id", "reset"}, {"label", "Reset scene"},
{"api", "Scene::Builder"}, {"group", "Scene"},
{"request_frame", true}
});
return result;
}
template <class T>
Gallery_Request_Result<T> fail(Gallery_Request_Error error) {
return {.value = std::nullopt, .error = error};
}
} // namespace
std::string Gallery_Protocol::catalog_json() {
Json published_cases = Json::array();
for (std::size_t index = 0; index < cases.size(); ++index) {
const auto& item = cases[index];
published_cases.push_back({
{"id", item.id}, {"title", item.title}, {"component", item.component},
{"category", item.category}, {"description", "Live Renderive scene"},
{"order", index},
{"control_count_by_mode", {{"manual", 0}, {"low_latency", 0}, {"playback", 0}}},
{"action_count_by_mode", {{"manual", 6}, {"low_latency", 6}, {"playback", 6}}}
});
}
Json field_mode{{"label", "Mode"}, {"source", "kernel_observer.mode"}};
Json field_limit{{"label", "Limit"}, {"source", "kernel_observer.limit"}};
Json field_event{{"label", "Event"}, {"source", "kernel_observer.event"}};
Json root{
{"category", "gallery"}, {"type", "catalog"},
{"protocol", "renderive.control-gallery"}, {"protocol_version", 6},
{"transport", {{"pixels", "websocket-binary-rvp2"}, {"socket_per_canvas", true}}},
{"navigation", {{"default_mode", "low_latency"}, {"all_categories_label", "All"},
{"hero_eyebrow", "Renderive"}, {"hero_title", "Scene Gallery"},
{"catalog_loaded_text", "Gallery ready"}}},
{"frame_modes", Json::array({
{{"id", "manual"}, {"title", "Manual"}, {"strategy", "Manual"},
{"description", "Explicit frame requests"}, {"order", 0}, {"accent", "#ffbd69"},
{"observer_visible", true}, {"frame_button_label", "Render frame"}},
{{"id", "low_latency"}, {"title", "Low latency"}, {"strategy", "Low_Latency"},
{"description", "Latest-state rendering"}, {"order", 1}, {"accent", "#59d6c5"},
{"observer_visible", true}, {"frame_button_label", "Render frame"}},
{{"id", "playback"}, {"title", "Playback"}, {"strategy", "Flow"},
{"description", "Ordered frame playback"}, {"order", 2}, {"accent", "#84b3ce"},
{"observer_visible", true}, {"frame_button_label", "Render frame"}}
})},
{"cases", std::move(published_cases)},
{"coverage", {{"case_count", cases.size()}, {"page_count", 1},
{"canvas_count", cases.size()}, {"manual_control_count", 0},
{"manual_action_count", cases.size() * 6}}},
{"dashboard", {
{"performance", {{"fields", Json::array({
{{"label", "Rendered"}, {"source", "performance.successful_render_count"},
{"format", "integer"}, {"summary", true}},
{{"label", "Transport FPS"}, {"source", "client_performance.transport_fps"},
{"digits", 1}, {"summary", true}}
})}}},
{"limits", {{"title", "Frame limits"}, {"aria_label", "Frame limits"},
{"current_source", "kernel_observer.limit"}, {"active_label", "active"},
{"inactive_label", "inactive"}, {"disabled_label", "disabled"},
{"fields", Json::array()}}},
{"observer", {{"aria_label", "Kernel observer"},
{"header", {{"prefix", ""}, {"suffix", ""}, {"event_label", ""},
{"mode", field_mode}, {"limit", field_limit},
{"event", field_event}}},
{"sections", Json::array()}}},
{"menu_views", Json::object()}
}}
};
return root.dump();
}
bool Gallery_Protocol::is_case(std::string_view case_id) {
return std::ranges::any_of(cases, [case_id](const auto& item) {
return item.id == case_id;
});
}
std::string Gallery_Protocol::error_json(std::string_view message,
std::string_view field) {
Json root{{"category", "gallery"}, {"type", "error"}, {"message", message}};
if (!field.empty()) root["field_errors"] = {{field, message}};
return root.dump();
}
std::string Gallery_Protocol::observer_json(std::string_view case_id,
Gallery_Frame_Mode frame_mode,
std::string_view telemetry_json) {
return Json{{"category", "gallery"}, {"type", "observer_state"},
{"case", case_id}, {"frame_mode", mode_id(frame_mode)},
{"telemetry", parse_object(telemetry_json)}}.dump();
}
std::string Gallery_Protocol::case_json_from_controls(
std::string_view case_id, std::string_view controls_json,
std::string_view telemetry_json, std::string_view notice,
Gallery_Frame_Mode frame_mode, bool manual_refresh) {
return Json{{"category", "gallery"},
{"type", manual_refresh ? "refresh_state" : "case_state"},
{"case", case_id}, {"frame_mode", mode_id(frame_mode)},
{"controls", parse_object(controls_json)},
{"actions", {{"data", actions()}}},
{"telemetry", parse_object(telemetry_json)}, {"notice", notice}}.dump();
}
Gallery_Request_Result<Gallery_Open_Request> Gallery_Protocol::open_request(
std::string_view message) {
const Json root = Json::parse(message, nullptr, false);
if (!root.is_object()) return fail<Gallery_Open_Request>(Gallery_Request_Error::invalid_json);
if (root.value("category", "") != "event" || root.value("type", "") != "gallery_open")
return fail<Gallery_Open_Request>(Gallery_Request_Error::wrong_message_type);
if (!root.contains("case") || !root["case"].is_string() ||
!root.contains("frame_mode") || !root["frame_mode"].is_string())
return fail<Gallery_Open_Request>(Gallery_Request_Error::missing_field);
const auto case_id = root["case"].get<std::string>();
const auto mode = parse_mode(root["frame_mode"].get<std::string>());
if (!is_case(case_id) || !mode)
return fail<Gallery_Open_Request>(Gallery_Request_Error::invalid_field);
return {.value = Gallery_Open_Request{case_id, *mode},
.error = Gallery_Request_Error::none};
}
Gallery_Request_Result<Gallery_Action_Request> Gallery_Protocol::action_request(
std::string_view message) {
const Json root = Json::parse(message, nullptr, false);
if (!root.is_object()) return fail<Gallery_Action_Request>(Gallery_Request_Error::invalid_json);
if (root.value("category", "") != "event" || root.value("type", "") != "gallery_action")
return fail<Gallery_Action_Request>(Gallery_Request_Error::wrong_message_type);
if (!root.contains("action") || !root["action"].is_string())
return fail<Gallery_Action_Request>(Gallery_Request_Error::missing_field);
Gallery_Action_Request request{root["action"].get<std::string>(), std::nullopt};
if (const auto argument = root.find("argument"); argument != root.end()) {
if (argument->is_boolean()) request.argument = argument->get<bool>();
else if (argument->is_number()) request.argument = argument->get<double>();
else if (argument->is_string()) request.argument = argument->get<std::string>();
else return fail<Gallery_Action_Request>(Gallery_Request_Error::invalid_field);
}
return {.value = std::move(request), .error = Gallery_Request_Error::none};
}
Gallery_Request_Result<Gallery_Control_Patch_Request>
Gallery_Protocol::control_patch_request(std::string_view message) {
const Json root = Json::parse(message, nullptr, false);
if (!root.is_object()) return fail<Gallery_Control_Patch_Request>(Gallery_Request_Error::invalid_json);
if (root.value("category", "") != "event" || root.value("type", "") != "gallery_patch")
return fail<Gallery_Control_Patch_Request>(Gallery_Request_Error::wrong_message_type);
if (!root.contains("target") || !root["target"].is_string() ||
!root.contains("patch") || !root["patch"].is_object())
return fail<Gallery_Control_Patch_Request>(Gallery_Request_Error::missing_field);
return {.value = Gallery_Control_Patch_Request{
root["target"].get<std::string>(), root["patch"].dump()},
.error = Gallery_Request_Error::none};
}
bool Gallery_Protocol::action_available(std::string_view case_id,
std::string_view action_id) {
if (!is_case(case_id))
return false;
return action_id == "reset" || action_id == "capture_next_frame" ||
action_id == "capture_frames" ||
action_id == "frame_strategy_low_latency" ||
action_id == "frame_strategy_manual" ||
action_id == "frame_strategy_playback";
}
} // namespace renderive::web