feat: built-in effect params, clip click-select, effect drag-and-drop, project load with plugins

- serializer resolves node types through the factory's dynamic
  (runtime-registered OpenFX) entries, so a project carrying plugin
  nodes loads again (was: "unknown node type"); covered by a new
  CI-gated round-trip test driving the real fixture plugin
- built-in effect nodes expose their inputs as inspector parameters
  like the C++ parameter editor: localized input names from the
  behavior, combo option tables via the new
  NodeBehavior::input_combo_strings (16 nodes, string-for-string from
  the C++ set_combo_box_strings), connection/data inputs excluded
- effect library: live drag-and-drop — onto the inspector's effect
  stack (lands at the indicator position) and onto the node editor
  canvas (creates the node at the drop point); double-click still
  appends to the selected clip
- inspector parameter controls are no longer recreated per render
  (gpui stack view caches them per effect), so sliders drag and
  checkboxes click; the view observes the engine and silently re-syncs
  values (undo/redo land on the widgets)
- timeline: left-press selects clips (plain/keep-multi/Ctrl-Cmd
  toggle); clip moves clamp the shared delta so no clip of a linked
  group lands before frame 0 instead of failing with "invalid move
  target"
- oakplugin: createInstance-rejected instances skip the destroyInstance
  notification (the plugin never owned them); vendor-suite fetchSuite
  misses moved behind OAK_OFX_TRACE; the worker logs the discovered/
  registered plugin counts
- CI: the OFX probe step also runs the serialization round-trip test
- gpui submodule: params view caching, clip click-select, library
  drag payload, graph_position_at
This commit is contained in:
2026-08-20 23:46:40 +08:00
parent 0a7f3766e2
commit 9003b78176
31 changed files with 482 additions and 50 deletions
+8
View File
@@ -501,6 +501,14 @@ pub trait NodeBehavior: Send {
}
}
/// The option labels of a combo input (C++ `set_combo_box_strings`,
/// called from each node's `retranslate`). Empty for non-combo inputs;
/// the default is no options.
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
let _ = id;
Vec::new()
}
/// Inputs excluded from rendering (C++ `ignore_inputs_for_rendering()`).
fn ignore_inputs_for_rendering(&self) -> &[String] {
&[]
+10
View File
@@ -305,6 +305,16 @@ impl NodeBehavior for BlurFilterNode {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `method_in` -> "Box", "Gaussian",
/// "Directional", "Radial".
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
METHOD_INPUT => vec!["Box", "Gaussian", "Directional", "Radial"],
_ => Vec::new(),
}
}
/// Evaluate outputs (C++ `value()`): no texture -> push nothing;
/// radius <= 0.0, or box/gaussian with both horiz and vert unchecked
/// -> pass-through push of the input texture; otherwise push a shader
@@ -174,6 +174,15 @@ impl NodeBehavior for ColorDifferenceKeyNode {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `color_in` -> "Green", "Blue".
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
COLOR_INPUT => vec!["Green", "Blue"],
_ => Vec::new(),
}
}
/// Evaluate outputs (C++ `value()`): no texture on `tex_in` ->
/// push nothing; texture present -> push a `ShaderJob` with the
/// whole input row inserted.
+12
View File
@@ -158,6 +158,18 @@ impl NodeBehavior for DespillNode {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `color_in` -> "Green", "Blue";
/// `method_in` -> "Average", "Double Red Average", "Double Average",
/// "Limit".
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
COLOR_INPUT => vec!["Green", "Blue"],
METHOD_INPUT => vec!["Average", "Double Red Average", "Double Average", "Limit"],
_ => Vec::new(),
}
}
/// Evaluate outputs (C++ `value()`): builds a `ShaderJob` from
/// the whole input row, then inserts a `luma_coeffs` vec3 taken
/// from the project's color manager default luma coefficients
@@ -165,6 +165,18 @@ impl NodeBehavior for DisplayTransformNode {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `dir_in` -> "Forward", "Inverse". The
/// `display_in`/`view_in` strings come from the attached color
/// manager at runtime (`update_displays`/`update_views`) and cannot
/// be expressed statically.
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
DIRECTION_INPUT => vec!["Forward", "Inverse"],
_ => Vec::new(),
}
}
/// Input value changed (C++ `InputValueChangedEvent`): for
/// `display_in`, `view_in` or `dir_in` regenerates the processor;
/// a `display_in` change additionally refreshes the view combo.
+10
View File
@@ -100,6 +100,16 @@ impl NodeBehavior for MathNode {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `method_in` -> the five operation names
/// "Add", "Subtract", "Multiply", "Divide", "Power".
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
METHOD_INPUT => vec!["Add", "Subtract", "Multiply", "Divide", "Power"],
_ => Vec::new(),
}
}
/// Evaluate outputs (C++ `value()`): pushes both operands into
/// single-value tables, runs the [`super::mathbase::PairingCalculator`]
/// heuristic, and if a pairing was found delegates to
+11
View File
@@ -190,6 +190,17 @@ impl NodeBehavior for MultiCamNode {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `sequence_type_in` -> "Video", "Audio".
/// `current_in`'s strings are built dynamically per connected source
/// (`"<i + 1>: <name>"`) and cannot be expressed statically.
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
SEQUENCE_TYPE_INPUT => vec!["Video", "Audio"],
_ => Vec::new(),
}
}
/// Inputs excluded from rendering (C++
/// `ignore_inputs_for_rendering()`): always
/// `{ k_sequence_input }`.
+9
View File
@@ -269,6 +269,15 @@ impl NodeBehavior for OCIOLutNode {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `lut_dir_in` -> "Forward", "Inverse".
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
DIRECTION_INPUT => vec!["Forward", "Inverse"],
_ => Vec::new(),
}
}
/// Input value changed (C++ `InputValueChangedEvent`): for
/// `lut_file_in` or `lut_dir_in`, regenerates the processor
/// immediately in the main process; in the render worker (where
+10
View File
@@ -175,6 +175,16 @@ impl NodeBehavior for ShapeNode {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `type_in` -> "Rectangle", "Ellipse",
/// "Rounded Rectangle".
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
TYPE_INPUT => vec!["Rectangle", "Ellipse", "Rounded Rectangle"],
_ => Vec::new(),
}
}
/// Evaluate outputs (C++ `value()`): builds a `"shape"` shader job
/// from the input row, inserting `resolution_in` (the base
/// texture's virtual resolution when connected, else the sequence
+10
View File
@@ -202,6 +202,16 @@ impl NodeBehavior for TextGeneratorV1 {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `valign_in` -> "Top", "Center",
/// "Bottom".
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
V_ALIGN_INPUT => vec!["Top", "Center", "Bottom"],
_ => Vec::new(),
}
}
/// Evaluate outputs (C++ `value()`): if the text input is
/// non-empty, push a texture generate job at the global video
/// params; otherwise push nothing.
+10
View File
@@ -258,6 +258,16 @@ impl NodeBehavior for TextGeneratorV2 {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `valign_in` -> "Top", "Center",
/// "Bottom".
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
V_ALIGN_INPUT => vec!["Top", "Center", "Bottom"],
_ => Vec::new(),
}
}
/// Evaluate outputs (C++ `value()`): if the text input is
/// non-empty, push a texture generate job at the global video
/// params forced to `PixelFormat::f32`; otherwise push nothing.
+10
View File
@@ -269,6 +269,16 @@ impl NodeBehavior for TextGeneratorV3 {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `valign_in` -> "Top", "Middle",
/// "Bottom".
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
VERTICAL_ALIGNMENT_INPUT => vec!["Top", "Middle", "Bottom"],
_ => Vec::new(),
}
}
/// Evaluate outputs (C++ `value()`): if `use_args_in` is set and
/// the args array is non-empty, expand `%N` placeholders in the
/// text via [`Self::format_string`]; if the resulting text is
@@ -194,6 +194,27 @@ impl NodeBehavior for TileDistortNode {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `anchor_in` -> "Top-Left", "Top-Center",
/// "Top-Right", "Middle-Left", "Middle-Center", "Middle-Right",
/// "Bottom-Left", "Bottom-Center", "Bottom-Right".
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
ANCHOR_INPUT => vec![
"Top-Left",
"Top-Center",
"Top-Right",
"Middle-Left",
"Middle-Center",
"Middle-Right",
"Bottom-Left",
"Bottom-Center",
"Bottom-Right",
],
_ => Vec::new(),
}
}
/// Evaluate outputs (C++ `value()`): no texture -> push nothing;
/// scale differs from 1.0 (an approximate-equality epsilon test:
/// `abs(scale-1)*1e12 > min(abs(scale), 1)`) -> shader job over the
@@ -307,6 +307,18 @@ impl NodeBehavior for TransformDistortNode {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `autoscale_in` -> "None", "Fit", "Fill",
/// "Stretch"; `interpolation_in` -> "Nearest Neighbor", "Bilinear",
/// "Mipmapped Bilinear".
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
AUTOSCALE_INPUT => vec!["None", "Fit", "Fill", "Stretch"],
INTERPOLATION_INPUT => vec!["Nearest Neighbor", "Bilinear", "Mipmapped Bilinear"],
_ => Vec::new(),
}
}
/// Evaluate outputs (C++ `value()`): generates the matrix from the
/// inherited transform inputs (position/rotation/scale/anchor,
/// folded with `parent_in`) and always pushes it as a `k_matrix`
+21
View File
@@ -92,6 +92,27 @@ impl NodeBehavior for TrigonometryNode {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `method_in` -> "Sine", "Cosine",
/// "Tangent", "Inverse Sine", "Inverse Cosine", "Inverse Tangent",
/// "Hyperbolic Sine", "Hyperbolic Cosine", "Hyperbolic Tangent".
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
METHOD_INPUT => vec![
"Sine",
"Cosine",
"Tangent",
"Inverse Sine",
"Inverse Cosine",
"Inverse Tangent",
"Hyperbolic Sine",
"Hyperbolic Cosine",
"Hyperbolic Tangent",
],
_ => Vec::new(),
}
}
/// Evaluate outputs (C++ `value()`): reads `x_in` as a double,
/// applies the [`Operation`] selected by `method_in`
/// (sin/cos/tan/asin/acos/atan/sinh/cosh/tanh), and pushes the
+23
View File
@@ -88,6 +88,29 @@ impl NodeBehavior for ValueNode {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `type_in` -> the pretty data-type names
/// of [`SUPPORTED_TYPES`] in order — "Float", "Integer", "Rational",
/// "Vector 2D", "Vector 3D", "Vector 4D", "Color", "Text", "Boolean"
/// (matching the Rust list, which omits the C++ `k_matrix`/`k_font`
/// entries).
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
TYPE_INPUT => vec![
"Float",
"Integer",
"Rational",
"Vector 2D",
"Vector 3D",
"Vector 4D",
"Color",
"Text",
"Boolean",
],
_ => Vec::new(),
}
}
/// Evaluate outputs (C++ `value()`): pushes the `value_in` value
/// onto the table unchanged.
fn value(
@@ -122,6 +122,16 @@ impl NodeBehavior for WaveDistortNode {
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `vertical_in` -> "Horizontal",
/// "Vertical".
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
VERTICAL_INPUT => vec!["Horizontal", "Vertical"],
_ => Vec::new(),
}
}
/// Evaluate outputs (C++ `value()`): no texture -> push nothing;
/// intensity != 0.0 -> shader job over the whole value row rendered
/// at the texture's own params; intensity == 0.0 -> pass-through
+5 -3
View File
@@ -632,12 +632,14 @@ fn load_node(
// Instantiate the node type; timeline structural types (which are
// not in the factory menu) are reconstructed directly, unknown
// types fall back to an error.
// types fall back to an error. `create_any` also covers the dynamic
// (runtime-registered OpenFX plugin) entries — `find` alone would
// reject every project that carries a plugin node.
let (mut core, behavior): (NodeCore, Box<dyn crate::node::NodeBehavior>) =
match create_timeline_type(&type_id) {
Some(x) => x,
None => match crate::factory::Factory::global().find(&type_id) {
Some(meta) => (meta.create)(),
None => match crate::factory::Factory::global().create_any(&type_id) {
Some(x) => x,
None => {
// Unknown type: skip the element body.
reader.skip_current_element();