Skip to content

Commit 421ad1b

Browse files
feat(filter_protocol): FilterTransform SDK contract + surface manifest tags
Add pj_plugins/filter_protocol/ — a header-only sub-module exposing the Strategy contract for Filter Editor transforms. The SDK ships only the abstract interface + registry; plugins provide the concrete strategies and register them with the factory at load time. Headers: - pj_plugins/sdk/filter_transform.hpp — abstract PJ::sdk::FilterTransform. Two streaming surfaces: calculateNextPoint (SISO, PJ3 mirror) and appendTail (chunked; default loops calculateNextPoint; override per-transform when running state makes it O(Δsamples)). Plus saveParams / loadParams JSON, and clone() for host hand-off across the plugin DSO boundary. - pj_plugins/sdk/filter_transform_factory.hpp — registry singleton, stable registration order, plus PJ_REGISTER_FILTER_TRANSFORM macro for auto-register at static init. Lift PJ::sdk::Point2 to its own header: - pj_base/point2.hpp — generic 2D vocab type (double x, double y). Was defined inside image_annotations.hpp; promote so the Filter Editor transform contract can reuse it without an artificial coupling to image annotations. image_annotations.hpp now just includes the new header. Surface manifest tags: - PluginDescriptor / RuntimeToolboxPlugin gain a tags field, parsed from the manifest's tags array. Lets the host route a generic action — e.g. the plot's right-click 'Apply Filter…' slot — to whichever installed toolbox declares the matching tag, instead of hardcoding a plugin id in the host. Empty for plugins whose manifest omits the array (backward compatible). The pj_filter_sdk INTERFACE target is declared inline in pj_plugins/CMakeLists.txt (alongside dialog_protocol). It's a header-only sub-module with one target, so a dedicated filter_protocol/CMakeLists.txt was overhead — kept the layout to match dialog_protocol/ for header organisation, but the build wiring is now where it belongs. Wires pj_filter_sdk into the plugin SDK umbrella so plugin authors get the contract transitively from plotjuggler_sdk::plugin_sdk. Additive surface — no existing header / struct / ABI changes (Point2's struct definition + ABI layout are unchanged; only its file location moved). Required release level when merged: MINOR. Version bump kept out of this PR for release coordination.
1 parent 9003e55 commit 421ad1b

9 files changed

Lines changed: 203 additions & 7 deletions

File tree

pj_base/include/pj_base/builtin/image_annotations.hpp

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <string>
2020
#include <vector>
2121

22+
#include "pj_base/point2.hpp"
2223
#include "pj_base/types.hpp"
2324

2425
namespace PJ {
@@ -32,12 +33,8 @@ enum class AnnotationTopology : uint8_t {
3233
kLineLoop, ///< Like LineStrip but closes back to the first point. 4-point loop = rectangle.
3334
};
3435

35-
/// 2D point in image-pixel coordinates (origin top-left).
36-
struct Point2 {
37-
double x = 0.0;
38-
double y = 0.0;
39-
bool operator==(const Point2&) const = default;
40-
};
36+
// `Point2` lives in pj_base/point2.hpp (generic 2D vocab type). In this header
37+
// it's used in image-pixel coordinates (origin top-left).
4138

4239
/// 8-bit per-channel RGBA color. a=0 means transparent / disabled.
4340
struct ColorRGBA {

pj_base/include/pj_base/point2.hpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2026 Davide Faconti
2+
// SPDX-License-Identifier: Apache-2.0
3+
#pragma once
4+
5+
// Plain (x, y) 2D point. Generic double-precision vocab type — used by image
6+
// annotations (pixel coordinates), Filter Editor transforms (time/value
7+
// samples), and any other 2D context where a tagged shape would be overkill.
8+
// The semantic of x / y is owned by the caller.
9+
10+
namespace PJ::sdk {
11+
12+
struct Point2 {
13+
double x = 0.0;
14+
double y = 0.0;
15+
bool operator==(const Point2&) const = default;
16+
};
17+
18+
} // namespace PJ::sdk

pj_plugins/CMakeLists.txt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,23 @@ target_link_libraries(pj_plugin_loader_detail PUBLIC pj_base)
1717

1818
add_subdirectory(dialog_protocol)
1919

20+
# Filter Editor transform contract (header-only) — plugins implement the 12
21+
# concrete strategies and self-register with the factory at load time. See
22+
# pj_plugins/filter_protocol/include/pj_plugins/sdk/filter_transform.hpp.
23+
add_library(pj_filter_sdk INTERFACE)
24+
target_compile_features(pj_filter_sdk INTERFACE cxx_std_20)
25+
target_include_directories(pj_filter_sdk INTERFACE
26+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/filter_protocol/include>
27+
$<INSTALL_INTERFACE:include>
28+
)
29+
# Builtin strategies (in the plugin) use nlohmann/json for saveParams /
30+
# loadParams; propagate the dep so consumers get it transitively.
31+
target_link_libraries(pj_filter_sdk INTERFACE nlohmann_json::nlohmann_json)
32+
set_target_properties(pj_filter_sdk PROPERTIES EXPORT_NAME filter_sdk)
33+
add_library(plotjuggler_sdk::filter_sdk ALIAS pj_filter_sdk)
34+
install(DIRECTORY filter_protocol/include/ DESTINATION include)
35+
install(TARGETS pj_filter_sdk EXPORT plotjuggler_sdkTargets ARCHIVE DESTINATION lib LIBRARY DESTINATION lib INCLUDES DESTINATION include)
36+
2037
# ---------------------------------------------------------------------------
2138
# pj_plugin_sdk — umbrella INTERFACE library that exposes the full plugin-author
2239
# SDK surface: C++ SDK headers (MessageParserPluginBase, ObjectIngestPolicy,
@@ -29,7 +46,7 @@ target_include_directories(pj_plugin_sdk INTERFACE
2946
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
3047
$<INSTALL_INTERFACE:include>
3148
)
32-
target_link_libraries(pj_plugin_sdk INTERFACE pj_base pj_dialog_sdk)
49+
target_link_libraries(pj_plugin_sdk INTERFACE pj_base pj_dialog_sdk pj_filter_sdk)
3350
set_target_properties(pj_plugin_sdk PROPERTIES EXPORT_NAME plugin_sdk)
3451
add_library(plotjuggler_sdk::plugin_sdk ALIAS pj_plugin_sdk)
3552

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2026 Davide Faconti
2+
// SPDX-License-Identifier: MPL-2.0
3+
#pragma once
4+
5+
// Filter Editor transform contract. Plugins provide the concrete strategies;
6+
// the host consumes them by id through FilterTransformFactory.
7+
8+
#include <memory>
9+
#include <optional>
10+
#include <string>
11+
#include <vector>
12+
13+
#include "pj_base/point2.hpp"
14+
15+
namespace PJ::sdk {
16+
17+
/// Strategy interface for a Filter Editor transform.
18+
///
19+
/// Two streaming surfaces:
20+
/// - `calculateNextPoint` — SISO, one input -> at most one output (PJ3 mirror).
21+
/// - `appendTail` — process a chunk; default loops `calculateNextPoint`. Override
22+
/// when running state (sliding sum, deque) makes it O(Δsamples) per call.
23+
///
24+
/// `clone()` is the host hand-off across the plugin DSO boundary.
25+
class FilterTransform {
26+
public:
27+
virtual ~FilterTransform() = default;
28+
29+
// Catalog identity used in saveConfig JSON, the factory registry, and the
30+
// legend.
31+
[[nodiscard]] virtual const char* id() const = 0;
32+
[[nodiscard]] virtual const char* label() const = 0;
33+
[[nodiscard]] virtual const char* bracketLabel() const = 0;
34+
35+
// True if the transform can extend its output as new samples arrive.
36+
[[nodiscard]] virtual bool isStreamSafe() const = 0;
37+
38+
/// Drop accumulated state. Must be called before the first `calculateNextPoint`
39+
/// after a series replace / clear.
40+
virtual void reset() = 0;
41+
42+
/// One input -> optional output. Inputs MUST arrive in x-ascending order.
43+
/// nullopt suppresses the output (e.g. Derivative drops the first sample).
44+
[[nodiscard]] virtual std::optional<Point2> calculateNextPoint(const Point2& in) = 0;
45+
46+
/// Process the tail of points since the previous call. Default loops
47+
/// `calculateNextPoint`; override per-transform when O(Δsamples) is possible.
48+
virtual void appendTail(const std::vector<Point2>& new_raw, std::vector<Point2>& out) {
49+
out.reserve(out.size() + new_raw.size());
50+
for (const auto& p : new_raw) {
51+
if (auto r = calculateNextPoint(p); r) {
52+
out.push_back(*r);
53+
}
54+
}
55+
}
56+
57+
/// Run from scratch over a whole series. Default: reset + appendTail.
58+
virtual std::vector<Point2> applyBatch(const std::vector<Point2>& input) {
59+
reset();
60+
std::vector<Point2> out;
61+
out.reserve(input.size());
62+
appendTail(input, out);
63+
return out;
64+
}
65+
66+
/// JSON for the parameter set this transform owns (not the source binding).
67+
[[nodiscard]] virtual std::string saveParams() const {
68+
return "{}";
69+
}
70+
virtual void loadParams(const std::string& /*json_str*/) {}
71+
72+
/// Deep copy. The host calls this so the kept instance is independent of the
73+
/// plugin DSO (the cloned vtable lives in the plugin's code, which stays
74+
/// loaded for the app session).
75+
[[nodiscard]] virtual std::unique_ptr<FilterTransform> clone() const = 0;
76+
};
77+
78+
} // namespace PJ::sdk
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2026 Davide Faconti
2+
// SPDX-License-Identifier: MPL-2.0
3+
#pragma once
4+
5+
// Registry singleton for FilterTransform implementations. Plugins register
6+
// their concrete classes at load time; the host looks them up by id. Order
7+
// of registration is preserved (mirrors PJ3's dropdown order).
8+
9+
#include <functional>
10+
#include <memory>
11+
#include <string>
12+
#include <string_view>
13+
#include <utility>
14+
#include <vector>
15+
16+
#include "pj_plugins/sdk/filter_transform.hpp"
17+
18+
namespace PJ::sdk {
19+
20+
class FilterTransformFactory {
21+
public:
22+
using CreateFn = std::function<std::unique_ptr<FilterTransform>()>;
23+
24+
[[nodiscard]] static FilterTransformFactory& instance() {
25+
static FilterTransformFactory inst;
26+
return inst;
27+
}
28+
29+
/// Re-registering an existing `id` replaces the previous factory entry.
30+
void registerTransform(const char* id, CreateFn fn) {
31+
for (auto& e : entries_) {
32+
if (e.id == id) {
33+
e.fn = std::move(fn);
34+
return;
35+
}
36+
}
37+
entries_.push_back({id, std::move(fn)});
38+
}
39+
40+
[[nodiscard]] std::vector<std::string> registeredIds() const {
41+
std::vector<std::string> ids;
42+
ids.reserve(entries_.size());
43+
for (const auto& e : entries_) {
44+
ids.push_back(e.id);
45+
}
46+
return ids;
47+
}
48+
49+
/// Returns nullptr if `id` is not registered.
50+
[[nodiscard]] std::unique_ptr<FilterTransform> create(std::string_view id) const {
51+
for (const auto& e : entries_) {
52+
if (e.id == id) {
53+
return e.fn();
54+
}
55+
}
56+
return nullptr;
57+
}
58+
59+
private:
60+
struct Entry {
61+
std::string id;
62+
CreateFn fn;
63+
};
64+
std::vector<Entry> entries_;
65+
};
66+
67+
} // namespace PJ::sdk
68+
69+
/// Self-register `Class` at static-init. `Class{}` must be default-constructible
70+
/// and its `id()` must be unique.
71+
#define PJ_REGISTER_FILTER_TRANSFORM(Class) \
72+
namespace { \
73+
[[maybe_unused]] const bool _pj_register_##Class = ([] { \
74+
PJ::sdk::FilterTransformFactory::instance().registerTransform( \
75+
(Class){}.id(), [] { return std::unique_ptr<PJ::sdk::FilterTransform>(new (Class)()); }); \
76+
return true; \
77+
}(), true); \
78+
}

pj_plugins/include/pj_plugins/host/plugin_catalog.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ struct PluginDescriptor {
4949
std::vector<std::string> encoding; ///< for message parsers (one or more)
5050
std::vector<std::string> file_extensions; ///< for data sources
5151
std::vector<std::string> capabilities; ///< optional capability tags
52+
std::vector<std::string> tags; ///< manifest `tags` — free-form labels (category, role flags like "plot_action", …)
5253
};
5354

5455
/// Diagnostic for a candidate DSO that could not produce a valid descriptor.

pj_plugins/include/pj_plugins/host/plugin_runtime_catalog.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ struct RuntimeToolboxPlugin {
5656
std::string id;
5757
std::string version;
5858
uint64_t capabilities = 0;
59+
std::vector<std::string> tags; ///< from manifest `tags` (e.g. "plot_action" — host routes by role)
5960
std::filesystem::file_time_type loaded_mtime;
6061
};
6162

pj_plugins/src/plugin_catalog.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,11 +252,16 @@ Expected<PluginDescriptor> decodeManifest(
252252
if (!capabilities) {
253253
return unexpected(capabilities.error());
254254
}
255+
auto tags = readStringArray(j, "tags");
256+
if (!tags) {
257+
return unexpected(tags.error());
258+
}
255259

256260
d.description = *description;
257261
d.category = *category;
258262
d.file_extensions = *file_extensions;
259263
d.capabilities = *capabilities;
264+
d.tags = *tags;
260265

261266
auto encoding = readStringArray(j, "encoding");
262267
if (!encoding) {

pj_plugins/src/plugin_runtime_catalog.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ bool PluginRuntimeCatalog::loadAndRegisterToolbox(const PluginDescriptor& descri
247247
loaded.name = descriptor.name;
248248
loaded.version = descriptor.version;
249249
loaded.capabilities = loaded.library.createHandle().capabilities();
250+
loaded.tags = descriptor.tags;
250251

251252
// Same fail-fast contract as DataSource above: kToolboxCapabilityHasDialog
252253
// requires an exported dialog vtable.

0 commit comments

Comments
 (0)