- 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)
572 lines
16 KiB
Rust
572 lines
16 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/>.
|
|
|
|
//! Orthographic matrix generator (C++
|
|
//! `src/node/src/generator/matrix/matrix.{h,cpp}`,
|
|
//! `olive::MatrixGenerator`).
|
|
|
|
use crate::factory::NodeMeta;
|
|
use crate::node::{Category, NodeBehavior, NodeCore};
|
|
|
|
/// Position input id (C++ `k_position_input`). Type: vec2; default
|
|
/// `(0.0, 0.0)`.
|
|
pub const POSITION_INPUT: &str = "pos_in";
|
|
|
|
/// Rotation input id (C++ `k_rotation_input`). Type: float; default
|
|
/// `0.0`.
|
|
pub const ROTATION_INPUT: &str = "rot_in";
|
|
|
|
/// Scale input id (C++ `k_scale_input`). Type: vec2; default
|
|
/// `(1.0, 1.0)`; properties: `min = (0, 0)`, `view = percentage`,
|
|
/// `disable1 = true` (the second component starts disabled because
|
|
/// uniform scale defaults to on).
|
|
pub const SCALE_INPUT: &str = "scale_in";
|
|
|
|
/// Uniform scale input id (C++ `k_uniform_scale_input`). Type: bool;
|
|
/// default `true`; flags: not-connectable, not-keyframable.
|
|
pub const UNIFORM_SCALE_INPUT: &str = "uniform_scale_in";
|
|
|
|
/// Anchor point input id (C++ `k_anchor_input`). Type: vec2; default
|
|
/// `(0.0, 0.0)`.
|
|
pub const ANCHOR_INPUT: &str = "anchor_in";
|
|
|
|
/// Orthographic matrix generator node. Builds a 2D transform matrix
|
|
/// from position, rotation, scale and anchor inputs. Has no own member
|
|
/// fields in C++ (state lives in the `Node` inputs).
|
|
pub struct MatrixGenerator;
|
|
|
|
impl NodeBehavior for MatrixGenerator {
|
|
/// Human-readable name (C++ `name()`).
|
|
fn name(&self) -> &str {
|
|
"Orthographic Matrix"
|
|
}
|
|
|
|
/// Short menu name (C++ `short_name()`).
|
|
fn short_name(&self) -> &str {
|
|
"Ortho"
|
|
}
|
|
|
|
/// Stable type id (C++ `id()`).
|
|
fn type_id(&self) -> &str {
|
|
"org.olivevideoeditor.Olive.ortho"
|
|
}
|
|
|
|
/// Categories (C++ `category()`): generator and math.
|
|
fn categories(&self) -> &[Category] {
|
|
&[Category::Generator, Category::Math]
|
|
}
|
|
|
|
/// Description (C++ `description()`).
|
|
fn description(&self) -> &str {
|
|
"Generate an orthographic matrix using position, rotation, and scale."
|
|
}
|
|
|
|
/// Localized input names (C++ `retranslate()`): `pos_in` ->
|
|
/// "Position", `rot_in` -> "Rotation", `scale_in` -> "Scale",
|
|
/// `uniform_scale_in` -> "Uniform Scale", `anchor_in` ->
|
|
/// "Anchor Point".
|
|
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
|
match id {
|
|
POSITION_INPUT => "Position",
|
|
ROTATION_INPUT => "Rotation",
|
|
SCALE_INPUT => "Scale",
|
|
UNIFORM_SCALE_INPUT => "Uniform Scale",
|
|
ANCHOR_INPUT => "Anchor Point",
|
|
_ => id,
|
|
}
|
|
}
|
|
|
|
/// Evaluate outputs (C++ `value()`): builds the matrix via
|
|
/// `generate_matrix(value, false, false, false, Matrix4x4())` and
|
|
/// pushes it as a `k_matrix` value. The C++ helper composes, in
|
|
/// order: translate(position), rotate(rotation around Z),
|
|
/// scale(scale.x, uniform ? scale.x : scale.y, 1), then
|
|
/// translate(-anchor).
|
|
fn value(
|
|
&self,
|
|
core: &NodeCore,
|
|
inputs: &crate::value::NodeValueRow,
|
|
time: oakcore_rs::Rational,
|
|
table: &mut crate::value::NodeValueTable,
|
|
) {
|
|
let mat = MatrixGenerator::generate_matrix(
|
|
inputs,
|
|
core,
|
|
time,
|
|
false,
|
|
false,
|
|
false,
|
|
super::mathbase::identity_matrix(),
|
|
);
|
|
table.push(
|
|
crate::value::ValueType::Matrix,
|
|
crate::value::NodeValue::Matrix(mat),
|
|
None,
|
|
);
|
|
}
|
|
|
|
/// Input value changed (C++ `InputValueChangedEvent`): when
|
|
/// `uniform_scale_in` changes, sets the scale input's `disable1`
|
|
/// property to the uniform-scale value (disabling the Y component
|
|
/// while uniform scale is on).
|
|
fn input_value_changed(&mut self, core: &mut NodeCore, input: &str, element: i32) {
|
|
if input == UNIFORM_SCALE_INPUT && element == -1 {
|
|
let uniform = core.standard_value(UNIFORM_SCALE_INPUT, -1).to_double() != 0.0;
|
|
if let Some(scale) = core.get_input_mut(SCALE_INPUT) {
|
|
scale.properties.retain(|(k, _)| k != "disable1");
|
|
scale.properties.push((
|
|
"disable1".to_string(),
|
|
crate::value::NodeValue::Boolean(uniform),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Deep copy (C++ `copy()`).
|
|
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
|
Some(Box::new(MatrixGenerator))
|
|
}
|
|
}
|
|
|
|
impl MatrixGenerator {
|
|
/// C++ `generate_matrix(const NodeValueRow&, bool, bool, bool,
|
|
/// const Matrix4x4&)` — reads the transform inputs from the render
|
|
/// row (falling back to the keyframed/standard values via `core`),
|
|
/// honoring the ignore flags, and composes the result onto `mat`
|
|
/// (used by [`MatrixGenerator::value`] and `TransformDistortNode`).
|
|
pub fn generate_matrix(
|
|
row: &crate::value::NodeValueRow,
|
|
core: &NodeCore,
|
|
time: oakcore_rs::Rational,
|
|
ignore_anchor: bool,
|
|
ignore_position: bool,
|
|
ignore_scale: bool,
|
|
mat: [f64; 16],
|
|
) -> [f64; 16] {
|
|
let anchor = if ignore_anchor {
|
|
[0.0, 0.0]
|
|
} else {
|
|
row_vec2(row, core, time, ANCHOR_INPUT)
|
|
};
|
|
let scale = if ignore_scale {
|
|
[1.0, 1.0]
|
|
} else {
|
|
row_vec2(row, core, time, SCALE_INPUT)
|
|
};
|
|
let position = if ignore_position {
|
|
[0.0, 0.0]
|
|
} else {
|
|
row_vec2(row, core, time, POSITION_INPUT)
|
|
};
|
|
let rotation = row_float(row, core, time, ROTATION_INPUT);
|
|
let uniform_scale = row_bool(row, core, time, UNIFORM_SCALE_INPUT);
|
|
|
|
Self::compose_matrix(position, rotation, scale, uniform_scale, anchor, mat)
|
|
}
|
|
|
|
/// Static matrix composition (C++
|
|
/// `generate_matrix(const Vector2D&, const float&, const Vector2D&,
|
|
/// bool, const Vector2D&, Matrix4x4)`): `mat` post-multiplied by
|
|
/// translate(position), rotate(rotation degrees around Z),
|
|
/// scale(scale.x, uniform ? scale.x : scale.y, 1), then
|
|
/// translate(-anchor). Row-major 16-element storage; every
|
|
/// post-multiply matches the C++ `Matrix4x4` member operations
|
|
/// (`// CPP-PARITY: matrix.cpp` `generate_matrix`).
|
|
pub fn compose_matrix(
|
|
pos: [f64; 2],
|
|
rot: f64,
|
|
scale: [f64; 2],
|
|
uniform_scale: bool,
|
|
anchor: [f64; 2],
|
|
mat: [f64; 16],
|
|
) -> [f64; 16] {
|
|
// Position
|
|
let mut m = matrix_translate(mat, pos[0], pos[1]);
|
|
|
|
// Rotation (2D rotation around the Z axis).
|
|
m = matrix_rotate_z(m, rot);
|
|
|
|
// Scale (uniform scale replicates the X component).
|
|
let full_scale = if uniform_scale {
|
|
(scale[0], scale[0])
|
|
} else {
|
|
(scale[0], scale[1])
|
|
};
|
|
m = matrix_scale(m, full_scale.0, full_scale.1, 1.0);
|
|
|
|
// Anchor point
|
|
matrix_translate(m, -anchor[0], -anchor[1])
|
|
}
|
|
}
|
|
|
|
/// Post-multiply `a` by `b` (C++ `Matrix4x4::operator*`; row-major
|
|
/// `m[r*4+c]` storage).
|
|
pub fn matrix_mul(a: [f64; 16], b: [f64; 16]) -> [f64; 16] {
|
|
let mut out = [0.0f64; 16];
|
|
for r in 0..4 {
|
|
for c in 0..4 {
|
|
let mut acc = 0.0;
|
|
for k in 0..4 {
|
|
acc += a[r * 4 + k] * b[k * 4 + c];
|
|
}
|
|
out[r * 4 + c] = acc;
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Post-multiply a 2D translation (C++ `Matrix4x4::translate(x, y)`).
|
|
pub fn matrix_translate(m: [f64; 16], x: f64, y: f64) -> [f64; 16] {
|
|
let mut t = super::mathbase::identity_matrix();
|
|
t[3] = x;
|
|
t[7] = y;
|
|
matrix_mul(m, t)
|
|
}
|
|
|
|
/// Post-multiply a scale (C++ `Matrix4x4::scale(x, y, z)`).
|
|
pub fn matrix_scale(m: [f64; 16], x: f64, y: f64, z: f64) -> [f64; 16] {
|
|
let mut s = super::mathbase::identity_matrix();
|
|
s[0] = x;
|
|
s[5] = y;
|
|
s[10] = z;
|
|
matrix_mul(m, s)
|
|
}
|
|
|
|
/// Post-multiply a Z-axis rotation in degrees (C++
|
|
/// `Matrix4x4::rotate(degrees)`).
|
|
fn matrix_rotate_z(m: [f64; 16], degrees: f64) -> [f64; 16] {
|
|
let radians = degrees * std::f64::consts::PI / 180.0;
|
|
let (c, s) = (radians.cos(), radians.sin());
|
|
let mut r = super::mathbase::identity_matrix();
|
|
r[0] = c;
|
|
r[1] = -s;
|
|
r[4] = s;
|
|
r[5] = c;
|
|
matrix_mul(m, r)
|
|
}
|
|
|
|
/// Resolve a vec2 input from the render row or the keyframed/standard
|
|
/// value (C++ `value.at(id).to_vec2()`; missing values read as
|
|
/// `(0, 0)`).
|
|
fn row_vec2(
|
|
row: &crate::value::NodeValueRow,
|
|
core: &NodeCore,
|
|
time: oakcore_rs::Rational,
|
|
id: &str,
|
|
) -> [f64; 2] {
|
|
match row.get(id) {
|
|
Some(crate::value::NodeValue::Vec2(v)) => *v,
|
|
Some(v) => [v.to_double(), 0.0],
|
|
None => match core.value_at_time(id, -1, time) {
|
|
crate::value::NodeValue::Vec2(v) => v,
|
|
v => [v.to_double(), 0.0],
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Resolve a float input from the render row or the keyframed/standard
|
|
/// value (C++ `value.at(id).to_double()`).
|
|
fn row_float(
|
|
row: &crate::value::NodeValueRow,
|
|
core: &NodeCore,
|
|
time: oakcore_rs::Rational,
|
|
id: &str,
|
|
) -> f64 {
|
|
match row.get(id) {
|
|
Some(v) => v.to_double(),
|
|
None => core.value_at_time(id, -1, time).to_double(),
|
|
}
|
|
}
|
|
|
|
/// Resolve a boolean input from the render row or the keyframed/standard
|
|
/// value (C++ `value.at(id).to_bool()`).
|
|
fn row_bool(
|
|
row: &crate::value::NodeValueRow,
|
|
core: &NodeCore,
|
|
time: oakcore_rs::Rational,
|
|
id: &str,
|
|
) -> bool {
|
|
match row.get(id) {
|
|
Some(v) => v.to_double() != 0.0,
|
|
None => core.value_at_time(id, -1, time).to_double() != 0.0,
|
|
}
|
|
}
|
|
|
|
/// Constructor (C++ `MatrixGenerator::MatrixGenerator()`): adds
|
|
/// `pos_in`, `rot_in`, `scale_in`, `uniform_scale_in` and `anchor_in`
|
|
/// with the defaults, flags and properties documented on the constants.
|
|
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
|
let mut core = NodeCore::new();
|
|
|
|
let mut pos = crate::input::Input::new(
|
|
POSITION_INPUT,
|
|
crate::value::ValueType::Vec2,
|
|
crate::value::NodeValue::Vec2([0.0, 0.0]),
|
|
);
|
|
pos.properties = vec![(
|
|
"view".to_string(),
|
|
crate::value::NodeValue::Text("percentage".into()),
|
|
)];
|
|
core.add_input(pos);
|
|
|
|
let mut rot = crate::input::Input::new(
|
|
ROTATION_INPUT,
|
|
crate::value::ValueType::Float,
|
|
crate::value::NodeValue::Float(0.0),
|
|
);
|
|
rot.properties = vec![
|
|
(
|
|
"view".to_string(),
|
|
crate::value::NodeValue::Text("percentage".into()),
|
|
),
|
|
("min".to_string(), crate::value::NodeValue::Float(-360.0)),
|
|
("max".to_string(), crate::value::NodeValue::Float(360.0)),
|
|
];
|
|
core.add_input(rot);
|
|
|
|
let mut scale = crate::input::Input::new(
|
|
SCALE_INPUT,
|
|
crate::value::ValueType::Vec2,
|
|
crate::value::NodeValue::Vec2([1.0, 1.0]),
|
|
);
|
|
scale.properties = vec![
|
|
("min".to_string(), crate::value::NodeValue::Vec2([0.0, 0.0])),
|
|
(
|
|
"view".to_string(),
|
|
crate::value::NodeValue::Text("percentage".into()),
|
|
),
|
|
(
|
|
"disable1".to_string(),
|
|
crate::value::NodeValue::Boolean(true),
|
|
),
|
|
];
|
|
core.add_input(scale);
|
|
|
|
let mut uniform = crate::input::Input::new(
|
|
UNIFORM_SCALE_INPUT,
|
|
crate::value::ValueType::Boolean,
|
|
crate::value::NodeValue::Boolean(true),
|
|
);
|
|
uniform.flags |= crate::input::flags::NOT_CONNECTABLE | crate::input::flags::NOT_KEYFRAMABLE;
|
|
core.add_input(uniform);
|
|
|
|
let mut anchor = crate::input::Input::new(
|
|
ANCHOR_INPUT,
|
|
crate::value::ValueType::Vec2,
|
|
crate::value::NodeValue::Vec2([0.0, 0.0]),
|
|
);
|
|
anchor.properties = vec![(
|
|
"view".to_string(),
|
|
crate::value::NodeValue::Text("percentage".into()),
|
|
)];
|
|
core.add_input(anchor);
|
|
|
|
(core, Box::new(MatrixGenerator))
|
|
}
|
|
|
|
#[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 = MatrixGenerator;
|
|
assert_eq!(n.input_name(POSITION_INPUT), "Position");
|
|
assert_eq!(n.input_name(ROTATION_INPUT), "Rotation");
|
|
assert_eq!(n.input_name(SCALE_INPUT), "Scale");
|
|
assert_eq!(n.input_name(UNIFORM_SCALE_INPUT), "Uniform Scale");
|
|
assert_eq!(n.input_name(ANCHOR_INPUT), "Anchor Point");
|
|
}
|
|
|
|
#[test]
|
|
fn create_wires_inputs() {
|
|
let (core, behavior) = create();
|
|
assert_eq!(behavior.type_id(), "org.olivevideoeditor.Olive.ortho");
|
|
assert_eq!(
|
|
core.get_input(POSITION_INPUT).unwrap().default,
|
|
NodeValue::Vec2([0.0, 0.0])
|
|
);
|
|
assert_eq!(
|
|
core.get_input(SCALE_INPUT).unwrap().default,
|
|
NodeValue::Vec2([1.0, 1.0])
|
|
);
|
|
assert_eq!(
|
|
core.get_input(UNIFORM_SCALE_INPUT).unwrap().default,
|
|
NodeValue::Boolean(true)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn value_defaults_produce_identity() {
|
|
let (core, behavior) = create();
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(
|
|
&core,
|
|
&crate::value::NodeValueRow::default(),
|
|
Rational::new(0, 1),
|
|
&mut table,
|
|
);
|
|
assert_eq!(
|
|
table.get(ValueType::Matrix),
|
|
Some(&NodeValue::Matrix(super::super::mathbase::identity_matrix()))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn value_translate_position() {
|
|
let (mut core, behavior) = create();
|
|
core.set_standard_value(POSITION_INPUT, -1, NodeValue::Vec2([10.0, 20.0]));
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(
|
|
&core,
|
|
&crate::value::NodeValueRow::default(),
|
|
Rational::new(0, 1),
|
|
&mut table,
|
|
);
|
|
// translate(10, 20) is stored at m[0][3] / m[1][3].
|
|
let m = match table.get(ValueType::Matrix).unwrap() {
|
|
NodeValue::Matrix(m) => *m,
|
|
_ => panic!("matrix expected"),
|
|
};
|
|
assert_eq!(m[3], 10.0);
|
|
assert_eq!(m[7], 20.0);
|
|
}
|
|
|
|
#[test]
|
|
fn value_non_uniform_scale() {
|
|
let (mut core, behavior) = create();
|
|
core.set_standard_value(UNIFORM_SCALE_INPUT, -1, NodeValue::Boolean(false));
|
|
core.set_standard_value(SCALE_INPUT, -1, NodeValue::Vec2([2.0, 3.0]));
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(
|
|
&core,
|
|
&crate::value::NodeValueRow::default(),
|
|
Rational::new(0, 1),
|
|
&mut table,
|
|
);
|
|
let m = match table.get(ValueType::Matrix).unwrap() {
|
|
NodeValue::Matrix(m) => *m,
|
|
_ => panic!("matrix expected"),
|
|
};
|
|
assert_eq!(m[0], 2.0);
|
|
assert_eq!(m[5], 3.0);
|
|
}
|
|
|
|
#[test]
|
|
fn value_uniform_scale_replicates_x() {
|
|
let (mut core, behavior) = create();
|
|
// uniform_scale defaults to true.
|
|
core.set_standard_value(SCALE_INPUT, -1, NodeValue::Vec2([2.0, 9.0]));
|
|
let mut table = NodeValueTable::default();
|
|
behavior.value(
|
|
&core,
|
|
&crate::value::NodeValueRow::default(),
|
|
Rational::new(0, 1),
|
|
&mut table,
|
|
);
|
|
let m = match table.get(ValueType::Matrix).unwrap() {
|
|
NodeValue::Matrix(m) => *m,
|
|
_ => panic!("matrix expected"),
|
|
};
|
|
assert_eq!(m[0], 2.0);
|
|
assert_eq!(m[5], 2.0, "uniform scale replicates X");
|
|
}
|
|
|
|
#[test]
|
|
fn compose_matrix_rotation_translate_order() {
|
|
// rotate(90) post-multiplied by translate(-10, 0): a pure
|
|
// translation (10, 0) under a 90-degree rotation lands in the
|
|
// negative Y translation slot.
|
|
let m = MatrixGenerator::compose_matrix(
|
|
[0.0, 0.0],
|
|
90.0,
|
|
[1.0, 1.0],
|
|
false,
|
|
[-10.0, 0.0],
|
|
super::super::mathbase::identity_matrix(),
|
|
);
|
|
assert!((m[0] - 0.0).abs() < 1e-9);
|
|
assert!((m[5] - 0.0).abs() < 1e-9);
|
|
assert!((m[7] - 10.0).abs() < 1e-9, "m[1][3] = +10 after rotate(90)");
|
|
assert!((m[3] - 0.0).abs() < 1e-9);
|
|
}
|
|
|
|
#[test]
|
|
fn compose_matrix_identity_inputs() {
|
|
let m = MatrixGenerator::compose_matrix(
|
|
[0.0, 0.0],
|
|
0.0,
|
|
[1.0, 1.0],
|
|
false,
|
|
[0.0, 0.0],
|
|
super::super::mathbase::identity_matrix(),
|
|
);
|
|
assert!(super::super::mathbase::matrix_is_identity(m));
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_helpers_are_row_major() {
|
|
let mut a = super::super::mathbase::identity_matrix();
|
|
a[3] = 100.0; // translate x
|
|
let mut b = super::super::mathbase::identity_matrix();
|
|
b[5] = 2.0; // scale y
|
|
let prod = matrix_mul(a, b);
|
|
assert_eq!(prod[5], 2.0);
|
|
assert_eq!(prod[3], 100.0, "post-multiplied translate survives");
|
|
}
|
|
|
|
#[test]
|
|
fn input_value_changed_toggles_disable1() {
|
|
let (mut core, behavior) = create();
|
|
let mut b = behavior;
|
|
b.input_value_changed(&mut core, UNIFORM_SCALE_INPUT, -1);
|
|
let scale = core.get_input(SCALE_INPUT).unwrap();
|
|
assert!(scale
|
|
.properties
|
|
.iter()
|
|
.any(|(k, v)| { k == "disable1" && *v == NodeValue::Boolean(true) }));
|
|
|
|
core.set_standard_value(UNIFORM_SCALE_INPUT, -1, NodeValue::Boolean(false));
|
|
b.input_value_changed(&mut core, UNIFORM_SCALE_INPUT, -1);
|
|
let scale = core.get_input(SCALE_INPUT).unwrap();
|
|
assert!(scale
|
|
.properties
|
|
.iter()
|
|
.any(|(k, v)| { k == "disable1" && *v == NodeValue::Boolean(false) }));
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_clones() {
|
|
let (core, behavior) = create();
|
|
let dup = behavior.duplicate(&core).unwrap();
|
|
assert_eq!(dup.name(), "Orthographic Matrix");
|
|
}
|
|
}
|
|
|
|
/// Register this node type (C++ factory entry for
|
|
/// `org.olivevideoeditor.Olive.ortho`).
|
|
pub fn register(meta: &mut Vec<NodeMeta>) {
|
|
meta.push(NodeMeta {
|
|
type_id: "org.olivevideoeditor.Olive.ortho",
|
|
name: "Orthographic Matrix",
|
|
categories: &[Category::Generator, Category::Math],
|
|
create,
|
|
});
|
|
}
|