- oakengine_sequence_move_clip implemented for real (oaktimeline TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple trim bugs the stub was hiding); same-track via the frozen C ABI, cross-track supported by the module command - oaknode clip blocks now declare a tex_in texture input and set effect_input to it, so timeline clips can host effect chains; facade test covers effect insert/remove on a real clip - oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a Homebrew upgrade left the system ffmpeg .pc pointing at a deleted dav1d Cellar path, breaking links); reads a git-ignored workspace .env for IDEs that cannot inject env vars (RustRover); links the C++ stdlib for C++ codec libs (svt-av1) - oakengine re-exports oaknode so tests share one crate instance; it_node uses the direct instance's value type where it calls the module FFI (the --workspace dev-dependency feature split builds oaknode twice)
392 lines
12 KiB
Rust
392 lines
12 KiB
Rust
// Oak Video Editor - Non-Linear Video Editor
|
|
// Copyright (C) 2026 Oak Team
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
//! Math node (C++ `src/node/src/math/math/math.{h,cpp}`,
|
|
//! `olive::MathNode`); behavior shared with other binary math nodes
|
|
//! lives in [`super::mathbase`] (C++ `MathNodeBase`).
|
|
|
|
use crate::factory::NodeMeta;
|
|
use crate::node::{Category, NodeBehavior, NodeCore};
|
|
|
|
/// Operation/method input id (C++ `k_method_in`). Type: combo;
|
|
/// flags: not-connectable, not-keyframable; combo strings are the
|
|
/// operation names Add/Subtract/Multiply/Divide/Power.
|
|
pub const METHOD_INPUT: &str = "method_in";
|
|
|
|
/// Operand A input id (C++ `k_param_a_in`). Type: float; default
|
|
/// `0.0`; properties: `decimalplaces = 8`, `autotrim = true`.
|
|
pub const PARAM_A_INPUT: &str = "param_a_in";
|
|
|
|
/// Operand B input id (C++ `k_param_b_in`). Type: float; default
|
|
/// `0.0`; properties: `decimalplaces = 8`, `autotrim = true`.
|
|
pub const PARAM_B_INPUT: &str = "param_b_in";
|
|
|
|
/// Operand C input id (C++ `k_param_c_in`). Declared as a static but
|
|
/// never added in the constructor — reserved/unused upstream.
|
|
pub const PARAM_C_INPUT: &str = "param_c_in";
|
|
|
|
/// Math node: applies a binary arithmetic operation to two values.
|
|
/// Unit-like — the C++ class has no own data members (inputs live in
|
|
/// [`NodeCore`], shared logic in [`super::mathbase`]).
|
|
pub struct MathNode;
|
|
|
|
impl MathNode {
|
|
/// Construct the behavior struct (C++ `MathNode()`; the node's
|
|
/// inputs are wired in [`create`]). Exposed so other nodes can own
|
|
/// a child math node (e.g. `super::opacity` multiplies via one).
|
|
pub fn new() -> Box<MathNode> {
|
|
Box::new(MathNode)
|
|
}
|
|
|
|
/// The operation selected by the `method_in` combo (C++
|
|
/// `MathNode::get_operation()`).
|
|
pub fn operation(&self, core: &NodeCore) -> super::mathbase::Operation {
|
|
let idx = core.standard_value(METHOD_INPUT, -1).to_double() as usize;
|
|
match idx {
|
|
0 => super::mathbase::Operation::Add,
|
|
1 => super::mathbase::Operation::Subtract,
|
|
2 => super::mathbase::Operation::Multiply,
|
|
3 => super::mathbase::Operation::Divide,
|
|
_ => super::mathbase::Operation::Power,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl NodeBehavior for MathNode {
|
|
/// Human-readable name (C++ `name()`): if the node is parented
|
|
/// (i.e. owned as a child of another node) and the current
|
|
/// operation has a name, returns that operation name
|
|
/// (Add/Subtract/Multiply/Divide/Power); otherwise "Math".
|
|
fn name(&self) -> &str {
|
|
"Math"
|
|
}
|
|
|
|
/// Stable type id (C++ `id()`).
|
|
fn type_id(&self) -> &str {
|
|
"org.olivevideoeditor.Olive.math"
|
|
}
|
|
|
|
/// Categories (C++ `category()`).
|
|
fn categories(&self) -> &[Category] {
|
|
&[Category::Math]
|
|
}
|
|
|
|
/// Description (C++ `description()`).
|
|
fn description(&self) -> &str {
|
|
"Perform a mathematical operation between two values."
|
|
}
|
|
|
|
/// Localized input names (C++ `retranslate()`): `method_in` ->
|
|
/// "Method", `param_a_in`/`param_b_in` -> "Value"; also sets the
|
|
/// combo strings on `method_in` to the five operation names.
|
|
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
|
match id {
|
|
METHOD_INPUT => "Method",
|
|
PARAM_A_INPUT | PARAM_B_INPUT => "Value",
|
|
_ => id,
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
/// [`super::mathbase::MathNodeBase::value_internal`] with the
|
|
/// current operation; otherwise pushes nothing.
|
|
fn value(
|
|
&self,
|
|
core: &NodeCore,
|
|
inputs: &crate::value::NodeValueRow,
|
|
time: oakcore_rs::Rational,
|
|
table: &mut crate::value::NodeValueTable,
|
|
) {
|
|
let a = inputs
|
|
.get(PARAM_A_INPUT)
|
|
.cloned()
|
|
.unwrap_or_else(|| core.value_at_time(PARAM_A_INPUT, -1, time));
|
|
let b = inputs
|
|
.get(PARAM_B_INPUT)
|
|
.cloned()
|
|
.unwrap_or_else(|| core.value_at_time(PARAM_B_INPUT, -1, time));
|
|
|
|
let mut at = crate::value::NodeValueTable::default();
|
|
at.push(a.value_type(), a, None);
|
|
let mut bt = crate::value::NodeValueTable::default();
|
|
bt.push(b.value_type(), b, None);
|
|
|
|
let calc = super::mathbase::PairingCalculator::new(&at, &bt);
|
|
if calc.found_most_likely_pairing() {
|
|
super::mathbase::MathNodeBase::value_internal(
|
|
self.operation(core),
|
|
calc.most_likely_pairing,
|
|
PARAM_A_INPUT,
|
|
&calc.most_likely_value_a,
|
|
PARAM_B_INPUT,
|
|
&calc.most_likely_value_b,
|
|
core,
|
|
inputs,
|
|
table,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Process a span of samples (C++ `process_samples()`): delegates to
|
|
/// [`super::mathbase::MathNodeBase::process_samples_internal`]
|
|
/// with the current operation and the `param_a_in`/`param_b_in`
|
|
/// ids (only used for the sample*number pairing). The C++ signature
|
|
/// receives the input buffer and a sample index; the Rust trait
|
|
/// instead hands over a time `range` and the destination buffer, so
|
|
/// the samples operand is located in the row and every index of the
|
|
/// output span is filled.
|
|
fn process_samples(
|
|
&self,
|
|
core: &NodeCore,
|
|
inputs: &crate::value::NodeValueRow,
|
|
range: oakcore_rs::TimeRange,
|
|
output: &mut crate::value::SampleBuffer,
|
|
) {
|
|
let _ = range;
|
|
let input = match inputs.get(PARAM_A_INPUT) {
|
|
Some(crate::value::NodeValue::Samples(b)) => b.clone(),
|
|
_ => match inputs.get(PARAM_B_INPUT) {
|
|
Some(crate::value::NodeValue::Samples(b)) => b.clone(),
|
|
_ => return,
|
|
},
|
|
};
|
|
for index in 0..output.sample_count {
|
|
super::mathbase::MathNodeBase::process_samples_internal(
|
|
inputs,
|
|
self.operation(core),
|
|
PARAM_A_INPUT,
|
|
PARAM_B_INPUT,
|
|
&input,
|
|
output,
|
|
index,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Shader code request (C++ `get_shader_code()`): delegates to
|
|
/// [`super::mathbase::MathNodeBase::shader_code_internal`] with the
|
|
/// request id (expected `"<op>.<pairing>.<type_a>.<type_b>"`) and
|
|
/// the `param_a_in`/`param_b_in` uniform names. The C++ `ShaderCode`
|
|
/// carries a vertex shader for the texture*matrix case; the trait's
|
|
/// single-string return carries only the fragment shader
|
|
/// (`// CPP-PARITY: math.cpp` `get_shader_code`).
|
|
fn shader_code(&self, request: &str) -> Option<String> {
|
|
let (frag, _vert) = super::mathbase::MathNodeBase::shader_code_internal(
|
|
request,
|
|
PARAM_A_INPUT,
|
|
PARAM_B_INPUT,
|
|
);
|
|
if frag.is_empty() {
|
|
None
|
|
} else {
|
|
Some(frag)
|
|
}
|
|
}
|
|
|
|
/// Deep copy (C++ `copy()`).
|
|
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
|
Some(Box::new(MathNode))
|
|
}
|
|
}
|
|
|
|
/// Operation combo strings (C++ `MathNodeBase::retranslate` order).
|
|
pub const OPERATION_NAMES: [&str; 5] = ["Add", "Subtract", "Multiply", "Divide", "Power"];
|
|
|
|
/// Constructor (C++ `MathNode::MathNode()`): adds `method_in` as a
|
|
/// not-connectable/not-keyframable combo, and `param_a_in`/
|
|
/// `param_b_in` as float inputs defaulting to 0.0 with
|
|
/// `decimalplaces = 8` and `autotrim = true`.
|
|
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
|
let mut core = NodeCore::new();
|
|
let mut method = crate::input::Input::new(
|
|
METHOD_INPUT,
|
|
crate::value::ValueType::Combo,
|
|
crate::value::NodeValue::Combo(0),
|
|
);
|
|
method.flags |= crate::input::flags::NOT_CONNECTABLE | crate::input::flags::NOT_KEYFRAMABLE;
|
|
method.properties = vec![(
|
|
"combobox_strings".to_string(),
|
|
crate::value::NodeValue::Binary(OPERATION_NAMES.concat().into_bytes()),
|
|
)];
|
|
core.add_input(method);
|
|
|
|
let mut a = crate::input::Input::new(
|
|
PARAM_A_INPUT,
|
|
crate::value::ValueType::Float,
|
|
crate::value::NodeValue::Float(0.0),
|
|
);
|
|
a.properties = vec![
|
|
("decimalplaces".to_string(), crate::value::NodeValue::Int(8)),
|
|
(
|
|
"autotrim".to_string(),
|
|
crate::value::NodeValue::Boolean(true),
|
|
),
|
|
];
|
|
core.add_input(a);
|
|
|
|
let mut b = crate::input::Input::new(
|
|
PARAM_B_INPUT,
|
|
crate::value::ValueType::Float,
|
|
crate::value::NodeValue::Float(0.0),
|
|
);
|
|
b.properties = vec![
|
|
("decimalplaces".to_string(), crate::value::NodeValue::Int(8)),
|
|
(
|
|
"autotrim".to_string(),
|
|
crate::value::NodeValue::Boolean(true),
|
|
),
|
|
];
|
|
core.add_input(b);
|
|
|
|
(core, MathNode::new())
|
|
}
|
|
|
|
/// Register this node type (C++ `k_math_node` in
|
|
/// `factory.cpp::create_from_factory_index`).
|
|
pub fn register(meta: &mut Vec<NodeMeta>) {
|
|
meta.push(NodeMeta {
|
|
type_id: "org.olivevideoeditor.Olive.math",
|
|
name: "Math",
|
|
categories: &[Category::Math],
|
|
create,
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::node::NodeBehavior;
|
|
use crate::value::{NodeValue, NodeValueTable, ValueType};
|
|
use oakcore_rs::Rational;
|
|
|
|
fn row(values: &[(&str, NodeValue)]) -> crate::value::NodeValueRow {
|
|
values
|
|
.iter()
|
|
.map(|(k, v)| (k.to_string(), v.clone()))
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn value_adds_numbers() {
|
|
let (core, behavior) = create();
|
|
let inputs = row(&[
|
|
(PARAM_A_INPUT, NodeValue::Float(2.0)),
|
|
(PARAM_B_INPUT, NodeValue::Float(3.0)),
|
|
]);
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
|
assert_eq!(table.get(ValueType::Float), Some(&NodeValue::Float(5.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn value_uses_standard_operands_when_unconnected() {
|
|
let (mut core, behavior) = create();
|
|
core.set_standard_value(PARAM_A_INPUT, -1, NodeValue::Float(10.0));
|
|
core.set_standard_value(PARAM_B_INPUT, -1, NodeValue::Float(4.0));
|
|
let inputs = crate::value::NodeValueRow::default();
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
|
assert_eq!(table.get(ValueType::Float), Some(&NodeValue::Float(14.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn value_multiply_color_by_number() {
|
|
let (core, behavior) = create();
|
|
let mut core = core;
|
|
core.set_standard_value(METHOD_INPUT, -1, NodeValue::Combo(2)); // Multiply
|
|
let inputs = row(&[
|
|
(PARAM_A_INPUT, NodeValue::Color([1.0, 1.0, 1.0, 1.0])),
|
|
(PARAM_B_INPUT, NodeValue::Float(0.5)),
|
|
]);
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
|
assert_eq!(
|
|
table.get(ValueType::Color),
|
|
Some(&NodeValue::Color([0.5, 0.5, 0.5, 0.5]))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn value_empty_operands_push_nothing() {
|
|
let (core, behavior) = create();
|
|
let inputs = row(&[
|
|
(PARAM_A_INPUT, NodeValue::None),
|
|
(PARAM_B_INPUT, NodeValue::None),
|
|
]);
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
|
|
assert!(table.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn operation_from_combo() {
|
|
let (mut core, _) = create();
|
|
let n = MathNode;
|
|
core.set_standard_value(METHOD_INPUT, -1, NodeValue::Combo(0));
|
|
assert_eq!(n.operation(&core), super::super::mathbase::Operation::Add);
|
|
core.set_standard_value(METHOD_INPUT, -1, NodeValue::Combo(4));
|
|
assert_eq!(n.operation(&core), super::super::mathbase::Operation::Power);
|
|
core.set_standard_value(METHOD_INPUT, -1, NodeValue::Combo(99));
|
|
assert_eq!(
|
|
n.operation(&core),
|
|
super::super::mathbase::Operation::Power,
|
|
"clamped"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn shader_code_delegates_to_mathbase() {
|
|
let n = MathNode;
|
|
let code = n.shader_code("0.0.2.2").unwrap();
|
|
assert!(code.contains("param_a_in + param_b_in"));
|
|
assert_eq!(n.shader_code("garbage"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn process_samples_multiplies_number() {
|
|
let (mut core, behavior) = create();
|
|
core.set_standard_value(METHOD_INPUT, -1, NodeValue::Combo(2)); // Multiply
|
|
let mut buf = crate::value::SampleBuffer {
|
|
format: oakcore_rs::SampleFormat::F32Planar,
|
|
channels: 1,
|
|
sample_count: 2,
|
|
data: vec![0u8; 8],
|
|
};
|
|
buf.set_sample_value(0, 0, 1.0);
|
|
buf.set_sample_value(0, 1, 2.0);
|
|
let inputs = row(&[
|
|
(PARAM_A_INPUT, NodeValue::Samples(buf.clone())),
|
|
(PARAM_B_INPUT, NodeValue::Float(3.0)),
|
|
]);
|
|
let mut out = crate::value::SampleBuffer {
|
|
format: oakcore_rs::SampleFormat::F32Planar,
|
|
channels: 1,
|
|
sample_count: 2,
|
|
data: vec![0u8; 8],
|
|
};
|
|
behavior.process_samples(
|
|
&core,
|
|
&inputs,
|
|
oakcore_rs::TimeRange::new(Rational::new(0, 1), Rational::new(2, 1)),
|
|
&mut out,
|
|
);
|
|
assert_eq!(out.sample_value(0, 0), 3.0);
|
|
assert_eq!(out.sample_value(0, 1), 6.0);
|
|
}
|
|
}
|