refactor: workspace layout — crates/, app at root, legacy C++ removed
Single mechanical restructure commit: - root Cargo.toml = oakapp bin + workspace; one cargo build produces oakapp, oak-cli, oak-worker, liboakengine.dylib - app/rust/src -> src/ (app at repo root, no rust/ nesting) - src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore; src/bindings/oakotio -> crates/oakotio; src/engine/rust -> crates/oakengine (keeps cdylib+staticlib+rlib) - public C headers include/<mod>/ -> crates/oakengine/include/<mod>/ - OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone) - legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/, app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets submodule, otio-install, all build-* output (~40GB) - oakstorage kept but excluded from the workspace (skeleton w/ todos); gpui excluded (own workspace) - verified: cargo build green, cargo test --workspace 1845/0 (with the documented OCIO_RS_* env override for the homebrew OCIO)
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user