246 lines
12 KiB
C++
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
|