#include "Gallery_Protocol.h" #include #include #include #include #include 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 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() { return 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}}, {{"id", "mode_render"}, {"label", "Render frame"}, {"api", "Scene::render"}, {"group", "Frame"}, {"request_frame", true}}, {{"id", "capture_next_frame"}, {"label", "Capture next frame"}, {"api", "Scene_Base::capture_next_frame"}, {"group", "Performance Capture"}, {"request_frame", true}}, {{"id", "capture_frames"}, {"label", "Capture N frames"}, {"api", "Scene_Base::capture_frames"}, {"group", "Performance Capture"}, {"argument_input", "number"}, {"argument_default", 20}, {"request_frame", true}}, {{"id", "reset"}, {"label", "Reset scene"}, {"api", "Scene::Builder"}, {"group", "Scene"}, {"request_frame", true}} }); } template Gallery_Request_Result 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", 7}, {"low_latency", 7}, {"playback", 7}}} }); } 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", 5}, {"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"}, {"automatic", false}, {"request_after_response", false}, {"request_on_animation_frame", false}, {"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"}, {"automatic", true}, {"request_after_response", true}, {"request_on_animation_frame", false}, {"observer_visible", true}, {"frame_button_label", "Render frame"}}, {{"id", "playback"}, {"title", "Playback"}, {"strategy", "Flow"}, {"description", "Ordered frame playback"}, {"order", 2}, {"accent", "#84b3ce"}, {"automatic", false}, {"request_after_response", false}, {"request_on_animation_frame", true}, {"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() * 7}}}, {"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_Protocol::open_request( std::string_view message) { const Json root = Json::parse(message, nullptr, false); if (!root.is_object()) return fail(Gallery_Request_Error::invalid_json); if (root.value("category", "") != "event" || root.value("type", "") != "gallery_open") return fail(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_Request_Error::missing_field); const auto case_id = root["case"].get(); const auto mode = parse_mode(root["frame_mode"].get()); if (!is_case(case_id) || !mode) return fail(Gallery_Request_Error::invalid_field); return {.value = Gallery_Open_Request{case_id, *mode}, .error = Gallery_Request_Error::none}; } Gallery_Request_Result Gallery_Protocol::action_request( std::string_view message) { const Json root = Json::parse(message, nullptr, false); if (!root.is_object()) return fail(Gallery_Request_Error::invalid_json); if (root.value("category", "") != "event" || root.value("type", "") != "gallery_action") return fail(Gallery_Request_Error::wrong_message_type); if (!root.contains("action") || !root["action"].is_string()) return fail(Gallery_Request_Error::missing_field); Gallery_Action_Request request{root["action"].get(), std::nullopt}; if (const auto argument = root.find("argument"); argument != root.end()) { if (argument->is_boolean()) request.argument = argument->get(); else if (argument->is_number()) request.argument = argument->get(); else if (argument->is_string()) request.argument = argument->get(); else return fail(Gallery_Request_Error::invalid_field); } return {.value = std::move(request), .error = Gallery_Request_Error::none}; } Gallery_Request_Result Gallery_Protocol::control_patch_request(std::string_view message) { const Json root = Json::parse(message, nullptr, false); if (!root.is_object()) return fail(Gallery_Request_Error::invalid_json); if (root.value("category", "") != "event" || root.value("type", "") != "gallery_patch") return fail(Gallery_Request_Error::wrong_message_type); if (!root.contains("target") || !root["target"].is_string() || !root.contains("patch") || !root["patch"].is_object()) return fail(Gallery_Request_Error::missing_field); return {.value = Gallery_Control_Patch_Request{ root["target"].get(), root["patch"].dump()}, .error = Gallery_Request_Error::none}; } bool Gallery_Protocol::action_available(std::string_view case_id, Gallery_Frame_Mode, std::string_view action_id) { return is_case(case_id) && (action_id == "reset" || action_id == "mode_render" || 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