Files
oak-editor/crates/oaknode/src/nodes/trigonometry.rs
T
Mike-Solar 18ff60f147 feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- 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)
2026-08-11 23:04:48 +08:00

356 lines
10 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/>.
//! Trigonometry node (C++
//! `src/node/src/math/trigonometry/trigonometry.{h,cpp}`,
//! `olive::TrigonometryNode`).
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: Sine,
/// Cosine, Tangent, Inverse Sine, Inverse Cosine, Inverse Tangent,
/// Hyperbolic Sine, Hyperbolic Cosine, Hyperbolic Tangent.
pub const METHOD_INPUT: &str = "method_in";
/// Operand input id (C++ `k_x_in`). Type: float; default `0.0`.
pub const X_INPUT: &str = "x_in";
/// Trigonometry operation (C++ private `TrigonometryNode::Operation`;
/// discriminants are the `method_in` combo indices).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Operation {
/// `sin(x)` (C++ `k_op_sine`).
Sine,
/// `cos(x)` (C++ `k_op_cosine`).
Cosine,
/// `tan(x)` (C++ `k_op_tangent`).
Tangent,
/// `asin(x)` (C++ `k_op_arc_sine`).
ArcSine,
/// `acos(x)` (C++ `k_op_arc_cosine`).
ArcCosine,
/// `atan(x)` (C++ `k_op_arc_tangent`).
ArcTangent,
/// `sinh(x)` (C++ `k_op_hyp_sine`).
HyperbolicSine,
/// `cosh(x)` (C++ `k_op_hyp_cosine`).
HyperbolicCosine,
/// `tanh(x)` (C++ `k_op_hyp_tangent`).
HyperbolicTangent,
}
/// Trigonometry node. Unit-like — the C++ class has no own data
/// members (its `Operation` enum is modeled above; inputs live in
/// [`NodeCore`]).
pub struct TrigonometryNode;
impl NodeBehavior for TrigonometryNode {
/// Human-readable name (C++ `name()`).
fn name(&self) -> &str {
"Trigonometry"
}
/// Stable type id (C++ `id()`).
fn type_id(&self) -> &str {
"org.olivevideoeditor.Olive.trigonometry"
}
/// Categories (C++ `category()`).
fn categories(&self) -> &[Category] {
&[Category::Math]
}
/// Description (C++ `description()`).
fn description(&self) -> &str {
"Perform a trigonometry operation on a value."
}
/// Localized input names (C++ `retranslate()`): `method_in` ->
/// "Method", `x_in` -> "Value"; also sets the combo strings on
/// `method_in` to the nine function names documented on
/// [`METHOD_INPUT`].
fn input_name<'a>(&self, id: &'a str) -> &'a str {
match id {
METHOD_INPUT => "Method",
X_INPUT => "Value",
_ => id,
}
}
/// 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
/// result as a float.
fn value(
&self,
core: &NodeCore,
inputs: &crate::value::NodeValueRow,
time: oakcore_rs::Rational,
table: &mut crate::value::NodeValueTable,
) {
let mut x = match inputs.get(X_INPUT) {
Some(v) => v.to_double(),
None => core.value_at_time(X_INPUT, -1, time).to_double(),
};
match self.operation(core) {
Operation::Sine => x = x.sin(),
Operation::Cosine => x = x.cos(),
Operation::Tangent => x = x.tan(),
Operation::ArcSine => x = x.asin(),
Operation::ArcCosine => x = x.acos(),
Operation::ArcTangent => x = x.atan(),
Operation::HyperbolicSine => x = x.sinh(),
Operation::HyperbolicCosine => x = x.cosh(),
Operation::HyperbolicTangent => x = x.tanh(),
}
table.push(
crate::value::ValueType::Float,
crate::value::NodeValue::Float(x),
None,
);
}
/// Deep copy (C++ `copy()`).
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
Some(Box::new(TrigonometryNode))
}
}
impl TrigonometryNode {
/// The operation selected by the `method_in` combo (C++
/// `TrigonometryNode::get_operation()`).
pub fn operation(&self, core: &NodeCore) -> Operation {
match core.standard_value(METHOD_INPUT, -1).to_double() as usize {
0 => Operation::Sine,
1 => Operation::Cosine,
2 => Operation::Tangent,
3 => Operation::ArcSine,
4 => Operation::ArcCosine,
5 => Operation::ArcTangent,
6 => Operation::HyperbolicSine,
7 => Operation::HyperbolicCosine,
_ => Operation::HyperbolicTangent,
}
}
}
/// Constructor (C++ `TrigonometryNode::TrigonometryNode()`): adds
/// `method_in` as a not-connectable/not-keyframable combo and `x_in`
/// as a float input defaulting to 0.0.
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 x = crate::input::Input::new(
X_INPUT,
crate::value::ValueType::Float,
crate::value::NodeValue::Float(0.0),
);
core.add_input(x);
(core, Box::new(TrigonometryNode))
}
/// Operation combo strings (C++ `retranslate` order).
pub const OPERATION_NAMES: [&str; 9] = [
"Sine",
"Cosine",
"Tangent",
"Inverse Sine",
"Inverse Cosine",
"Inverse Tangent",
"Hyperbolic Sine",
"Hyperbolic Cosine",
"Hyperbolic Tangent",
];
#[cfg(test)]
mod tests {
use super::*;
use crate::node::NodeBehavior;
use crate::value::{NodeValue, NodeValueTable, ValueType};
use oakcore_rs::Rational;
#[test]
fn input_names() {
let n = TrigonometryNode;
assert_eq!(n.input_name(METHOD_INPUT), "Method");
assert_eq!(n.input_name(X_INPUT), "Value");
}
#[test]
fn create_wires_inputs() {
let (core, behavior) = create();
assert_eq!(
behavior.type_id(),
"org.olivevideoeditor.Olive.trigonometry"
);
assert_eq!(
core.get_input(X_INPUT).unwrap().default,
NodeValue::Float(0.0)
);
let method = core.get_input(METHOD_INPUT).unwrap();
assert_ne!(method.flags & crate::input::flags::NOT_CONNECTABLE, 0);
}
#[test]
fn value_sine() {
let (core, behavior) = create();
let inputs = crate::value::NodeValueRow::from([(
X_INPUT.to_string(),
NodeValue::Float(std::f64::consts::FRAC_PI_2),
)]);
let mut table = NodeValueTable::default();
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
assert_eq!(table.get(ValueType::Float), Some(&NodeValue::Float(1.0)));
}
#[test]
fn value_cosine_and_tangent() {
let (mut core, behavior) = create();
core.set_standard_value(METHOD_INPUT, -1, NodeValue::Combo(1));
let inputs =
crate::value::NodeValueRow::from([(X_INPUT.to_string(), NodeValue::Float(0.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(1.0)));
core.set_standard_value(METHOD_INPUT, -1, NodeValue::Combo(2));
let inputs = crate::value::NodeValueRow::from([(
X_INPUT.to_string(),
NodeValue::Float(std::f64::consts::FRAC_PI_4),
)]);
let mut table = NodeValueTable::default();
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
let v = table.get(ValueType::Float).unwrap().to_double();
assert!((v - 1.0).abs() < 1e-9);
}
#[test]
fn value_uses_standard_operand() {
let (mut core, behavior) = create();
core.set_standard_value(X_INPUT, -1, NodeValue::Float(0.0));
let mut table = NodeValueTable::default();
behavior.value(
&core,
&crate::value::NodeValueRow::default(),
Rational::new(0, 1),
&mut table,
);
assert_eq!(table.get(ValueType::Float), Some(&NodeValue::Float(0.0)));
}
#[test]
fn operation_mapping() {
let (mut core, _) = create();
let n = TrigonometryNode;
core.set_standard_value(METHOD_INPUT, -1, NodeValue::Combo(3));
assert_eq!(n.operation(&core), Operation::ArcSine);
core.set_standard_value(METHOD_INPUT, -1, NodeValue::Combo(8));
assert_eq!(n.operation(&core), Operation::HyperbolicTangent);
core.set_standard_value(METHOD_INPUT, -1, NodeValue::Combo(99));
assert_eq!(n.operation(&core), Operation::HyperbolicTangent, "clamped");
}
#[test]
fn value_all_operations() {
use std::f64::consts::FRAC_PI_4;
let (mut core, behavior) = create();
// (combo index, input, expected fn applied)
let cases: [(i64, f64, fn(f64) -> f64); 9] = [
(0, FRAC_PI_4, f64::sin),
(1, FRAC_PI_4, f64::cos),
(2, FRAC_PI_4, f64::tan),
(3, 0.5, f64::asin),
(4, 0.5, f64::acos),
(5, 0.5, f64::atan),
(6, 0.5, f64::sinh),
(7, 0.5, f64::cosh),
(8, 0.5, f64::tanh),
];
for (op, x, f) in cases {
core.set_standard_value(METHOD_INPUT, -1, NodeValue::Combo(op));
let inputs =
crate::value::NodeValueRow::from([(X_INPUT.to_string(), NodeValue::Float(x))]);
let mut table = NodeValueTable::default();
behavior.value(&core, &inputs, Rational::new(0, 1), &mut table);
let got = table.get(ValueType::Float).unwrap().to_double();
assert!(
(got - f(x)).abs() < 1e-12,
"op {}: got {}, want {}",
op,
got,
f(x)
);
}
}
#[test]
fn value_keyframed_operand() {
let (mut core, behavior) = create();
core.keyframe_track_mut(X_INPUT, -1)
.set_key(crate::keyframe::Keyframe {
time: Rational::new(0, 1),
value: NodeValue::Float(1.0),
interpolation: crate::keyframe::Interpolation::Linear,
bezier_in: (0.0, 0.0),
bezier_out: (0.0, 0.0),
});
let mut table = NodeValueTable::default();
behavior.value(
&core,
&crate::value::NodeValueRow::default(),
Rational::new(0, 1),
&mut table,
);
let v = table.get(ValueType::Float).unwrap().to_double();
assert!((v - 1.0_f64.sin()).abs() < 1e-12);
}
#[test]
fn duplicate_clones() {
let (core, behavior) = create();
let dup = behavior.duplicate(&core).unwrap();
assert_eq!(dup.name(), "Trigonometry");
}
}
/// Register this node type (C++ `k_trigonometry_node` in
/// `factory.cpp::create_from_factory_index`).
pub fn register(meta: &mut Vec<NodeMeta>) {
meta.push(NodeMeta {
type_id: "org.olivevideoeditor.Olive.trigonometry",
name: "Trigonometry",
categories: &[Category::Math],
create,
});
}