diff --git a/CMakeLists.txt b/CMakeLists.txt index 0cf071c..b45781b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -118,7 +118,7 @@ endif() if(PJ_INSTALL_SDK) include(CMakePackageConfigHelpers) - set(PJ_PACKAGE_VERSION "0.11.0") + set(PJ_PACKAGE_VERSION "0.12.0") set(PJ_PACKAGE_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/plotjuggler_sdk) install(EXPORT plotjuggler_sdkTargets diff --git a/conanfile.py b/conanfile.py index dce74df..74fc5c9 100644 --- a/conanfile.py +++ b/conanfile.py @@ -30,7 +30,7 @@ class PlotjugglerSdkConan(ConanFile): name = "plotjuggler_sdk" - version = "0.11.0" + version = "0.12.0" # Apache-2.0 covers the whole SDK (pj_base + pj_plugins). See LICENSE. license = "Apache-2.0" url = "https://github.com/PlotJuggler/plotjuggler_sdk" diff --git a/pj_base/include/pj_base/plugin_data_api.h b/pj_base/include/pj_base/plugin_data_api.h index 71b17ca..6094c54 100644 --- a/pj_base/include/pj_base/plugin_data_api.h +++ b/pj_base/include/pj_base/plugin_data_api.h @@ -826,6 +826,28 @@ typedef struct PJ_data_processors_host_vtable_t { * call on this vtable. An unknown id is an error. */ bool (*data_processor_config)( void* ctx, PJ_string_view_t id, PJ_string_view_t* out_recipe_json, PJ_error_t* out_error) PJ_NOEXCEPT; + + /* [main-thread] Create an EPHEMERAL transform node — identical to + * create_data_processor but the node is never persisted, never catalogued, + * and is automatically torn down when remove_data_processor is called with + * the same id. Intended for live preview: create on each keystroke, remove + * on cancel/close. Tail-appended: guard with struct_size before calling. */ + bool (*create_data_processor_ephemeral)( + void* ctx, PJ_string_view_t id, const PJ_string_view_t* inputs, uint64_t input_count, + const PJ_string_view_t* outputs, uint64_t output_count, PJ_string_view_t script, PJ_string_view_t params_json, + PJ_error_t* out_error) PJ_NOEXCEPT; + + /* [main-thread] Validate a transform script WITHOUT installing anything: the + * host compiles it with the backend named by `language` ("luau" today; "python" + * reserved for a future backend) to catch syntax and module-load errors. Returns + * true if the script compiles; on false, *out_error carries the reason (suitable + * for a UI semaphore / error box). Compilation-only: it does NOT run the script + * over data, so a runtime error or empty output is not detected here. An unknown + * language is an error. No node, topic or catalog entry is created. Tail-appended: + * guard with struct_size before calling. */ + bool (*validate_data_processor_script)( + void* ctx, PJ_string_view_t script, PJ_string_view_t language, PJ_string_view_t params_json, + PJ_error_t* out_error) PJ_NOEXCEPT; } PJ_data_processors_host_vtable_t; typedef struct { diff --git a/pj_base/include/pj_base/sdk/plugin_data_api.hpp b/pj_base/include/pj_base/sdk/plugin_data_api.hpp index c6d6107..a1c1b29 100644 --- a/pj_base/include/pj_base/sdk/plugin_data_api.hpp +++ b/pj_base/include/pj_base/sdk/plugin_data_api.hpp @@ -1385,6 +1385,57 @@ class DataProcessorsHostView { return std::string(toStringView(recipe)); } + /// Create an EPHEMERAL transform node for live preview. Identical to + /// createTransform but the node is never persisted or catalogued. + /// Call remove() with the same id on cancel/close to tear it down. + /// Returns an error if the host does not support this slot (SDK < 0.12.0). + [[nodiscard]] Status createEphemeralTransform( + std::string_view id, Span inputs, Span outputs, + std::string_view script, std::string_view params_json) const { + if (!valid() || + !PJ_HAS_TAIL_SLOT(PJ_data_processors_host_vtable_t, host_.vtable, create_data_processor_ephemeral)) { + return unexpected("host does not support ephemeral transforms (SDK < 0.12.0)"); + } + if (outputs.empty()) { + return unexpected("data processors transform requires at least one output topic"); + } + std::vector in_abi; + in_abi.reserve(inputs.size()); + for (const auto& name : inputs) { + in_abi.push_back(toAbiString(name)); + } + std::vector out_abi; + out_abi.reserve(outputs.size()); + for (const auto& name : outputs) { + out_abi.push_back(toAbiString(name)); + } + PJ_error_t err{}; + if (!host_.vtable->create_data_processor_ephemeral( + host_.ctx, toAbiString(id), in_abi.data(), in_abi.size(), out_abi.data(), out_abi.size(), + toAbiString(script), toAbiString(params_json), &err)) { + return unexpected(errorToString(err)); + } + return okStatus(); + } + + /// Validate a transform script without installing anything. `language` selects + /// the backend ("luau" today; "python" reserved). Returns ok if the script + /// compiles; otherwise the host's error message (ready for a semaphore / error + /// box). Compilation-only — it does not run the script over data. Errors if the + /// host predates this slot (SDK < 0.12.0) or the language is unknown. + [[nodiscard]] Status validateScript( + std::string_view script, std::string_view language, std::string_view params_json = "{}") const { + if (!valid() || !PJ_HAS_TAIL_SLOT(PJ_data_processors_host_vtable_t, host_.vtable, validate_data_processor_script)) { + return unexpected("host does not support script validation (SDK < 0.12.0)"); + } + PJ_error_t err{}; + if (!host_.vtable->validate_data_processor_script( + host_.ctx, toAbiString(script), toAbiString(language), toAbiString(params_json), &err)) { + return unexpected(errorToString(err)); + } + return okStatus(); + } + private: PJ_data_processors_host_t host_{}; }; diff --git a/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_data_view.hpp b/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_data_view.hpp index 82cfd06..f5dcf3f 100644 --- a/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_data_view.hpp +++ b/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_data_view.hpp @@ -155,6 +155,7 @@ class WidgetDataView { std::string label; std::vector> points; // {x, y} std::string color; // optional hex "#rrggbb"; empty means use chart theme default + bool dashed = false; // draw with a dashed line }; [[nodiscard]] std::optional> chartSeries(std::string_view name) const { @@ -190,6 +191,10 @@ class WidgetDataView { if (color_it != s.end() && color_it->is_string()) { sv.color = color_it->get(); } + auto dashed_it = s.find("dashed"); + if (dashed_it != s.end() && dashed_it->is_boolean()) { + sv.dashed = dashed_it->get(); + } result.push_back(std::move(sv)); } return result; @@ -200,6 +205,11 @@ class WidgetDataView { return getBool(name, "chart_zoom_enabled"); } + /// Returns whether the chart should auto-fit on every series update. + [[nodiscard]] std::optional chartAutoZoom(std::string_view name) const { + return getBool(name, "chart_auto_zoom"); + } + // --- QPlainTextEdit --- [[nodiscard]] std::optional plainText(std::string_view name) const { return getString(name, "plain_text"); diff --git a/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp b/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp index 4dee6f6..9c51c0f 100644 --- a/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp +++ b/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp @@ -23,7 +23,8 @@ struct ChartPoint { struct ChartSeries { std::string label; std::vector points; - std::string color; // optional hex "#rrggbb" + std::string color; // optional hex "#rrggbb" + bool dashed = false; // draw with a dashed line (e.g. a faded "before" ghost curve) }; /// One boundary segment on a RangeSlider (used by setRangeSliderMarkers): a box @@ -192,6 +193,9 @@ class WidgetData { if (!s.color.empty()) { entry["color"] = s.color; } + if (s.dashed) { + entry["dashed"] = true; + } arr.push_back(std::move(entry)); } e["chart_series"] = std::move(arr); @@ -211,6 +215,14 @@ class WidgetData { return *this; } + /// Auto-fit (zoom-to-extents) the chart inside the named QFrame on every series + /// update when `enabled` is true; when false, preserve the user's current zoom. + /// Mirrors the Transform/Filter editor "AutoZoom" checkbox. + WidgetData& setChartAutoZoom(std::string_view name, bool enabled) { + entry(name)["chart_auto_zoom"] = enabled; + return *this; + } + // --- QPlainTextEdit --- WidgetData& setPlainText(std::string_view name, std::string_view text) { entry(name)["plain_text"] = text;