// 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 . //! Transform distort effect (C++ //! `src/node/src/distort/transform/transformdistortnode.{h,cpp}`, //! `olive::TransformDistortNode`). In C++ this derives from //! `MatrixGenerator` (`super::matrix`), which provides the //! `pos_in`/`rot_in`/`scale_in`/`uniform_scale_in`/`anchor_in` inputs //! and the `generate_matrix` helper. use crate::factory::NodeMeta; use crate::node::{Category, Gizmo, NodeBehavior, NodeCore}; /// Parent matrix input id (C++ `k_parent_input`). Type: matrix; no /// default (identity when unconnected). pub const PARENT_INPUT: &str = "parent_in"; /// Texture input id (C++ `k_texture_input`). Type: texture; flags: /// not-keyframable; this is the node's effect input. Note: C++ /// `prepend_input`s it so it appears before the inherited matrix /// inputs. pub const TEXTURE_INPUT: &str = "tex_in"; /// Auto-scale combo input id (C++ `k_autoscale_input`). Type: combo; /// default `0` (`AutoScaleType::None`); combo strings: "None", "Fit", /// "Fill", "Stretch". pub const AUTOSCALE_INPUT: &str = "autoscale_in"; /// Interpolation combo input id (C++ `k_interpolation_input`). Type: /// combo; default `2` (mipmapped bilinear); combo strings: "Nearest /// Neighbor", "Bilinear", "Mipmapped Bilinear". pub const INTERPOLATION_INPUT: &str = "interpolation_in"; /// Number of scale point gizmos (C++ `k_gizmo_scale_count` from /// `node.h`: top-left, top-center, top-right, bottom-left, /// bottom-center, bottom-right, center-left, center-right). pub const GIZMO_SCALE_COUNT: usize = 8; /// Auto-scale mode (C++ `AutoScaleType`); values match the /// `autoscale_in` combo indices. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum AutoScaleType { /// No auto-scaling. None = 0, /// Fit the texture inside the sequence frame (letterbox). Fit = 1, /// Fill the sequence frame (crop overflow). Fill = 2, /// Stretch the texture to the sequence frame ignoring aspect. Stretch = 3, } /// Rotation direction for wrap-around detection (C++ private enum /// `RotationDirection`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum RotationDirection { /// No direction established yet. None, /// Clockwise (C++ `k_direction_positive`). Positive, /// Counter-clockwise (C++ `k_direction_negative`). Negative, } /// Which axes a scale gizmo drags (C++ private enum `GizmoScaleType`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum GizmoScaleType { /// Horizontal center handles (C++ `k_gizmo_scale_x_only`). XOnly, /// Vertical center handles (C++ `k_gizmo_scale_y_only`). YOnly, /// Corner handles (C++ `k_gizmo_scale_both`). Both, } /// Transform distort node. Applies a 2D position/rotation/scale/anchor /// transform to a texture, equivalent to multiplying by an orthographic /// matrix. /// /// The C++ class also keeps a `Matrix4x4 gizmo_inverted_transform_` /// drag-state member; `Matrix4x4` has no Rust equivalent in this crate /// yet, so the field is omitted here (it is recomputed at drag start /// and consumed within the drag). pub struct TransformDistortNode { /// Inherited matrix-generator state (C++ base class /// `MatrixGenerator`; provides the transform inputs and /// `generate_matrix`). pub matrix: super::matrix::MatrixGenerator, /// Angle between the drag-start mouse position and the anchor /// (C++ `double gizmo_start_angle_`). gizmo_start_angle: f64, /// Anchor point in screen space captured at drag start (C++ /// `PointF gizmo_anchor_pt_`). gizmo_anchor_pt: (f64, f64), /// Whether uniform scale was on at scale-drag start (C++ /// `bool gizmo_scale_uniform_`). gizmo_scale_uniform: bool, /// Previous-frame raw rotation angle (C++ `double /// gizmo_last_angle_`). gizmo_last_angle: f64, /// Previous-frame perpendicular rotation angle used to disambiguate /// wrap-around (C++ `double gizmo_last_alt_angle_`). gizmo_last_alt_angle: f64, /// Number of full revolutions accumulated while rotation dragging /// (C++ `int gizmo_rotate_wrap_`). gizmo_rotate_wrap: i32, /// Last raw rotation direction (C++ `gizmo_rotate_last_dir_`). gizmo_rotate_last_dir: RotationDirection, /// Last perpendicular rotation direction (C++ /// `gizmo_rotate_last_alt_dir_`). gizmo_rotate_last_alt_dir: RotationDirection, /// Axes dragged by the current scale gizmo (C++ /// `gizmo_scale_axes_`). gizmo_scale_axes: GizmoScaleType, /// Scale reference point captured at drag start (texture-space /// anchor, flipped for right/bottom handles; C++ `Vector2D /// gizmo_scale_anchor_`). gizmo_scale_anchor: (f64, f64), /// Scale drag handles in `k_gizmo_scale_*` order (C++ /// `PointGizmo *point_gizmo_[k_gizmo_scale_count]`; absolute drag /// value behavior, bound to both tracks of the inherited /// `scale_in`). point_gizmo: [Gizmo; GIZMO_SCALE_COUNT], /// Anchor point handle (C++ `PointGizmo *anchor_gizmo_`; anchor /// shape, drags the inherited `anchor_in` and `pos_in` tracks). anchor_gizmo: Gizmo, /// Frame outline handle (C++ `PolygonGizmo *poly_gizmo_`; drags the /// inherited `pos_in` tracks). poly_gizmo: Gizmo, /// Rotation ring handle (C++ `ScreenGizmo *rotation_gizmo_`; /// absolute drag value behavior, bound to the inherited `rot_in`). rotation_gizmo: Gizmo, } impl TransformDistortNode { /// Static matrix composition (C++ /// `adjust_matrix_by_resolutions()`): scale to a 2x2 square in /// sequence space, apply the texture offset and the generated /// matrix, scale back out to texture size, then apply the /// auto-scale mode (stretch: per-axis; fit/fill: uniform by width /// or height depending on the aspect-ratio comparison). /// /// Also covers the C++ private helpers `create_scale_point` and /// `get_direction_from_angles` (below). The remaining helpers are not /// representable: `generate_auto_scaled_matrix` needs the texture /// params from the C++ `VideoParams` (the Rust texture handle carries /// no params) and `is_a_scale_gizmo` compares gizmo pointers. fn adjust_matrix_by_resolutions( mat: [f64; 16], sequence_res: (f64, f64), texture_res: (f64, f64), offset: (f64, f64), autoscale_type: AutoScaleType, ) -> [f64; 16] { // First, create an identity matrix. let mut adjusted_matrix = super::mathbase::identity_matrix(); // Scale it to a square based on the sequence's resolution. adjusted_matrix = super::matrix::matrix_scale( adjusted_matrix, 2.0 / sequence_res.0, 2.0 / sequence_res.1, 1.0, ); // Apply offset if applicable. adjusted_matrix = super::matrix::matrix_translate(adjusted_matrix, offset.0, offset.1); // Adjust by the matrix we generated earlier. adjusted_matrix = super::matrix::matrix_mul(adjusted_matrix, mat); // Scale back out to texture size (adjusted by pixel aspect). adjusted_matrix = super::matrix::matrix_scale( adjusted_matrix, texture_res.0 * 0.5, texture_res.1 * 0.5, 1.0, ); // If auto-scale is enabled, fit the texture to the sequence // (without cropping). if autoscale_type != AutoScaleType::None { if autoscale_type == AutoScaleType::Stretch { adjusted_matrix = super::matrix::matrix_scale( adjusted_matrix, sequence_res.0 / texture_res.0, sequence_res.1 / texture_res.1, 1.0, ); } else { let footage_real_ar = texture_res.0 / texture_res.1; let sequence_real_ar = sequence_res.0 / sequence_res.1; let scale_by_x = sequence_res.0 / texture_res.0; let scale_by_y = sequence_res.1 / texture_res.1; let autoscale_val; if (autoscale_type == AutoScaleType::Fit) == (sequence_real_ar > footage_real_ar) { // Scale by height. Either the sequence is wider than // the footage or we're using fill and cutting off the // sides. autoscale_val = scale_by_y; } else { // Scale by width. Either the footage is wider than the // sequence or we're using fill and cutting off the top // and bottom. autoscale_val = scale_by_x; } adjusted_matrix = super::matrix::matrix_scale(adjusted_matrix, autoscale_val, autoscale_val, 1.0); } } adjusted_matrix } /// C++ `Matrix4x4::map(PointF)` equivalent: maps `p` through the /// row-major matrix treating it as `(x, y, 0, 1)`, dividing by the /// resulting `w` whenever it is not exactly 1. fn map_point(mat: [f64; 16], p: (f64, f64)) -> (f64, f64) { let x = p.0 * mat[0] + p.1 * mat[1] + mat[3]; let y = p.0 * mat[4] + p.1 * mat[5] + mat[7]; let w = p.0 * mat[12] + p.1 * mat[13] + mat[15]; if w == 1.0 { (x, y) } else { (x / w, y / w) } } /// Scale-point gizmo placement (C++ `create_scale_point()`): maps the /// unit-square position through `mat` and adds the sequence half /// resolution. fn create_scale_point(x: f64, y: f64, half_res: (f64, f64), mat: [f64; 16]) -> (f64, f64) { let p = Self::map_point(mat, (x, y)); (p.0 + half_res.0, p.1 + half_res.1) } /// Rotation direction of a mouse angle step (C++ /// `get_direction_from_angles()`): positive when `current` is greater /// than `last`, negative otherwise. fn get_direction_from_angles(last: f64, current: f64) -> RotationDirection { if current > last { RotationDirection::Positive } else { RotationDirection::Negative } } } impl NodeBehavior for TransformDistortNode { /// Human-readable name (C++ `name()`). fn name(&self) -> &str { "Transform" } /// Short menu name (C++ `short_name()`; overrides the /// `MatrixGenerator` short name "Ortho" to just return `name()`). fn short_name(&self) -> &str { "Transform" } /// Stable type id (C++ `id()`). fn type_id(&self) -> &str { "org.olivevideoeditor.Olive.transform" } /// Categories (C++ `category()`). fn categories(&self) -> &[Category] { &[Category::Distort] } /// Description (C++ `description()`). fn description(&self) -> &str { "Transform an image in 2D space. Equivalent to multiplying by an orthographic matrix." } /// Localized input names (C++ `retranslate()`): `parent_in` -> /// "Parent", `autoscale_in` -> "Auto-Scale" (combo strings "None", /// "Fit", "Fill", "Stretch"), `tex_in` -> "Texture", /// `interpolation_in` -> "Interpolation" (combo strings "Nearest /// Neighbor", "Bilinear", "Mipmapped Bilinear"), plus the inherited /// `MatrixGenerator` input names via its retranslate. fn input_name<'a>(&self, id: &'a str) -> &'a str { match id { PARENT_INPUT => "Parent", AUTOSCALE_INPUT => "Auto-Scale", TEXTURE_INPUT => "Texture", INTERPOLATION_INPUT => "Interpolation", // The inherited MatrixGenerator input names (the combo strings // above are UI-level properties, C++ `set_combo_box_strings`). _ => self.matrix.input_name(id), } } /// 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` /// value. With a texture: builds the auto-scaled real matrix via /// `adjust_matrix_by_resolutions`; if it is not identity, renders a /// shader job at the GLOBAL video params (the transform may change /// the size) binding `ove_maintex` and `ove_mvpmat` with the /// interpolation selected by `interpolation_in`, and pushes that; /// identity matrix (or no texture) -> pass-through push of the /// input texture value. /// /// The real matrix needs the texture's params and the sequence /// resolution (the Rust texture handle carries no params and the /// value() signature no globals), so the identity check — and thus /// the pass-through-vs-job decision — is not representable here: with /// a texture the job is always queued for the renderer seam /// (`// CPP-PARITY: transformdistortnode.cpp` value()). fn value( &self, core: &NodeCore, inputs: &crate::value::NodeValueRow, time: oak_core::Rational, table: &mut crate::value::NodeValueTable, ) { let parent = match inputs.get(PARENT_INPUT) { Some(crate::value::NodeValue::Matrix(m)) => *m, _ => match core.value_at_time(PARENT_INPUT, -1, time) { crate::value::NodeValue::Matrix(m) => m, _ => super::mathbase::identity_matrix(), }, }; // Generate matrix. let generated_matrix = super::matrix::MatrixGenerator::generate_matrix( inputs, core, time, false, false, false, parent, ); table.push( crate::value::ValueType::Matrix, crate::value::NodeValue::Matrix(generated_matrix), None, ); match inputs.get(TEXTURE_INPUT) { Some(tex @ crate::value::NodeValue::Texture(_)) => { // C++ builds the auto-scaled real matrix and pushes a job // at the global video params binding `ove_maintex` / // `ove_mvpmat`; the deferred job is resolved by the // renderer seam (`// CPP-PARITY: transformdistortnode.cpp` // value()). let _ = tex; table.push( crate::value::ValueType::Texture, crate::value::NodeValue::Texture(crate::handle::CHandle::null()), None, ); } _ => { // No texture: C++ re-pushes the input value (k_none), // which is a no-op here. } } } /// Shader code request (C++ `get_shader_code()`): ignores the /// request id and returns a default (empty) `ShaderCode` — the /// node relies on the renderer's default vertex/fragment shaders, /// so this maps to `None`. fn shader_code(&self, request: &str) -> Option { let _ = request; None } /// Gizmo transform/positions (C++ `update_gizmo_positions()` and /// `gizmo_transformation()`). Positions: with a texture, builds the /// rectangle matrix (sequence half-res scale folded with the /// auto-scaled generated matrix), maps the unit square through it /// for the polygon gizmo, places the anchor gizmo at the mapped /// origin, the eight scale handles at the mapped unit-square /// corners/edge midpoints, and sets the `offset` input property of /// `pos_in` (half sequence res + texture offset) and `anchor_in` /// (half texture size). Transformation: with a texture returns the /// auto-scaled generated matrix (generated with an identity parent); /// without one falls back to the base `MatrixGenerator` /// transformation. /// /// Every placement needs the texture's params and the sequence /// resolution (neither available here), and the gizmo point /// positions have no storage in [`Gizmo`] — not representable /// (`// CPP-PARITY: transformdistortnode.cpp` /// `update_gizmo_positions` / `gizmo_transformation`). fn gizmo_update(&self, core: &NodeCore, row: &crate::value::NodeValueRow) { let _ = (core, row); } /// Gizmo drag callbacks (C++ `gizmo_drag_start()` / /// `gizmo_drag_move()`). Start: anchor gizmo -> stores the inverted /// anchor-aware generated matrix; scale handle -> requires a /// texture, records uniform-scale state, screen-space anchor point, /// per-handle axes (corners both, left/right center X, top/bottom /// center Y), the texture-space scale anchor flipped for /// right/bottom handles, and the inverted fully-generated matrix; /// rotation gizmo -> records the anchor point, start/last angles /// (raw and perpendicular) and resets the wrap counter and /// direction. Move: polygon gizmo -> drags the position X/Y tracks /// by the mouse delta; anchor gizmo -> drags the anchor tracks by /// the inverse-matrix-mapped delta and the position tracks by the /// raw delta; rotation gizmo -> accumulates wrap-aware angle /// (detecting direction reversal across ±pi using the perpendicular /// angle) and drags the rotation track by the degree difference /// from the start angle; scale handle -> maps the mouse delta into /// inverted-matrix space relative to the anchor, then drags the /// scale tracks by the normalized magnitude per the handle's axes, /// collapsing to a single uniform value (diagonal ratio for corner /// handles) when uniform scale is on. /// /// The drags write keyframe tracks through `NodeInputDragger`s with /// per-drag start values, which the Rust data model does not carry — /// not representable here (`// CPP-PARITY: transformdistortnode.cpp` /// `gizmo_drag_start` / `gizmo_drag_move`). The drag-state fields /// above mirror the C++ members but are never written. fn gizmo_drag(&mut self, core: &mut NodeCore, start: bool, x: f64, y: f64, modifiers: u32) { let _ = (core, start, x, y, modifiers); } /// Deep copy (C++ `copy()`). fn duplicate(&self, _core: &NodeCore) -> Option> { Some(Box::new(TransformDistortNode { matrix: super::matrix::MatrixGenerator, gizmo_start_angle: self.gizmo_start_angle, gizmo_anchor_pt: self.gizmo_anchor_pt, gizmo_scale_uniform: self.gizmo_scale_uniform, gizmo_last_angle: self.gizmo_last_angle, gizmo_last_alt_angle: self.gizmo_last_alt_angle, gizmo_rotate_wrap: self.gizmo_rotate_wrap, gizmo_rotate_last_dir: self.gizmo_rotate_last_dir, gizmo_rotate_last_alt_dir: self.gizmo_rotate_last_alt_dir, gizmo_scale_axes: self.gizmo_scale_axes, gizmo_scale_anchor: self.gizmo_scale_anchor, point_gizmo: self.point_gizmo.clone(), anchor_gizmo: self.anchor_gizmo.clone(), poly_gizmo: self.poly_gizmo.clone(), rotation_gizmo: self.rotation_gizmo.clone(), })) } } /// Constructor (C++ `TransformDistortNode::TransformDistortNode()`): /// adds `parent_in` (matrix), `autoscale_in` (combo, default 0) and /// `interpolation_in` (combo, default 2), and PREPENDS `tex_in` /// (texture, not-keyframable) ahead of the inherited `MatrixGenerator` /// inputs; creates the rotation screen gizmo (absolute drag behavior, /// bound to `rot_in`), the frame polygon gizmo (bound to `pos_in`), the /// anchor point gizmo (anchor shape, bound to `anchor_in` and `pos_in`) /// and the eight scale point gizmos (absolute drag behavior, bound to /// `scale_in`); sets the video-effect flag and the effect input. pub fn create() -> (NodeCore, Box) { // Inherit the MatrixGenerator inputs (pos/rot/scale/uniform/anchor). let (mut core, _) = super::matrix::create(); let mut tex = crate::input::Input::new( TEXTURE_INPUT, crate::value::ValueType::Texture, crate::value::NodeValue::None, ); tex.flags |= crate::input::flags::NOT_KEYFRAMABLE; let parent_input = crate::input::Input::new( PARENT_INPUT, crate::value::ValueType::Matrix, crate::value::NodeValue::None, ); let autoscale_input = crate::input::Input::new( AUTOSCALE_INPUT, crate::value::ValueType::Combo, crate::value::NodeValue::Combo(AutoScaleType::None as i64), ); let interpolation_input = crate::input::Input::new( INTERPOLATION_INPUT, crate::value::ValueType::Combo, crate::value::NodeValue::Combo(2), ); // The C++ input order is `enabled_in`, `tex_in` (prepended), the // node's own inputs, then the inherited matrix inputs. `tex_in` goes // right after `enabled_in`; `parent_in`/`autoscale_in`/ // `interpolation_in` go between it and the matrix inputs (inserted // in reverse at index 2 to keep that order). core.inputs.insert(1, tex); core.inputs.insert(2, interpolation_input); core.inputs.insert(2, autoscale_input); core.inputs.insert(2, parent_input); // Gizmos, in C++ add order: the rotation screen gizmo (absolute drag // behavior, bound to `rot_in`), the frame polygon gizmo (bound to // `pos_in`), the anchor point gizmo (bound to `anchor_in` and // `pos_in`), and the eight scale point gizmos (absolute drag // behavior, bound to `scale_in`) in `k_gizmo_scale_*` order. let rotation_gizmo = Gizmo { position_inputs: vec![(super::matrix::ROTATION_INPUT.to_string(), -1, 0)], drag_point: (0.0, 0.0), }; let poly_gizmo = Gizmo { position_inputs: vec![ (super::matrix::POSITION_INPUT.to_string(), -1, 0), (super::matrix::POSITION_INPUT.to_string(), -1, 1), ], drag_point: (0.0, 0.0), }; let anchor_gizmo = Gizmo { position_inputs: vec![ (super::matrix::ANCHOR_INPUT.to_string(), -1, 0), (super::matrix::ANCHOR_INPUT.to_string(), -1, 1), (super::matrix::POSITION_INPUT.to_string(), -1, 0), (super::matrix::POSITION_INPUT.to_string(), -1, 1), ], drag_point: (0.0, 0.0), }; let scale_gizmo = |_i: usize| Gizmo { position_inputs: vec![ (super::matrix::SCALE_INPUT.to_string(), -1, 0), (super::matrix::SCALE_INPUT.to_string(), -1, 1), ], drag_point: (0.0, 0.0), }; let point_gizmo: [Gizmo; GIZMO_SCALE_COUNT] = [ scale_gizmo(0), scale_gizmo(1), scale_gizmo(2), scale_gizmo(3), scale_gizmo(4), scale_gizmo(5), scale_gizmo(6), scale_gizmo(7), ]; core.gizmos = vec![ rotation_gizmo.clone(), poly_gizmo.clone(), anchor_gizmo.clone(), point_gizmo[0].clone(), point_gizmo[1].clone(), point_gizmo[2].clone(), point_gizmo[3].clone(), point_gizmo[4].clone(), point_gizmo[5].clone(), point_gizmo[6].clone(), point_gizmo[7].clone(), ]; core.flags |= crate::node::flags::VIDEO_EFFECT; core.effect_input = TEXTURE_INPUT.to_string(); ( core, Box::new(TransformDistortNode { matrix: super::matrix::MatrixGenerator, gizmo_start_angle: 0.0, gizmo_anchor_pt: (0.0, 0.0), gizmo_scale_uniform: false, gizmo_last_angle: 0.0, gizmo_last_alt_angle: 0.0, gizmo_rotate_wrap: 0, gizmo_rotate_last_dir: RotationDirection::None, gizmo_rotate_last_alt_dir: RotationDirection::None, gizmo_scale_axes: GizmoScaleType::Both, gizmo_scale_anchor: (0.0, 0.0), point_gizmo, anchor_gizmo, poly_gizmo, rotation_gizmo, }), ) } #[cfg(test)] mod tests { use super::*; use crate::node::NodeBehavior; use crate::value::{NodeValue, NodeValueTable, ValueType}; use oak_core::Rational; fn tex() -> NodeValue { NodeValue::Texture(crate::handle::CHandle::null()) } fn empty_gizmo() -> Gizmo { Gizmo { position_inputs: vec![], drag_point: (0.0, 0.0), } } fn empty_node() -> TransformDistortNode { TransformDistortNode { matrix: super::super::matrix::MatrixGenerator, gizmo_start_angle: 0.0, gizmo_anchor_pt: (0.0, 0.0), gizmo_scale_uniform: false, gizmo_last_angle: 0.0, gizmo_last_alt_angle: 0.0, gizmo_rotate_wrap: 0, gizmo_rotate_last_dir: RotationDirection::None, gizmo_rotate_last_alt_dir: RotationDirection::None, gizmo_scale_axes: GizmoScaleType::Both, gizmo_scale_anchor: (0.0, 0.0), point_gizmo: std::array::from_fn(|_| empty_gizmo()), anchor_gizmo: empty_gizmo(), poly_gizmo: empty_gizmo(), rotation_gizmo: empty_gizmo(), } } #[test] fn input_names() { let n = empty_node(); assert_eq!(n.input_name(PARENT_INPUT), "Parent"); assert_eq!(n.input_name(AUTOSCALE_INPUT), "Auto-Scale"); assert_eq!(n.input_name(TEXTURE_INPUT), "Texture"); assert_eq!(n.input_name(INTERPOLATION_INPUT), "Interpolation"); // Inherited matrix-generator names. assert_eq!( n.input_name(super::super::matrix::POSITION_INPUT), "Position" ); assert_eq!( n.input_name(super::super::matrix::ROTATION_INPUT), "Rotation" ); assert_eq!(n.input_name(super::super::matrix::SCALE_INPUT), "Scale"); assert_eq!( n.input_name(super::super::matrix::UNIFORM_SCALE_INPUT), "Uniform Scale" ); assert_eq!( n.input_name(super::super::matrix::ANCHOR_INPUT), "Anchor Point" ); } #[test] fn create_wires_inputs_and_flags() { let (core, behavior) = create(); assert_eq!(behavior.type_id(), "org.olivevideoeditor.Olive.transform"); // C++ input order: enabled_in, tex_in (prepended), parent_in, // autoscale_in, interpolation_in, then the inherited matrix inputs. let ids: Vec<&str> = core.inputs.iter().map(|i| i.id.as_str()).collect(); assert_eq!( ids, vec![ crate::node::ENABLED_INPUT, TEXTURE_INPUT, PARENT_INPUT, AUTOSCALE_INPUT, INTERPOLATION_INPUT, super::super::matrix::POSITION_INPUT, super::super::matrix::ROTATION_INPUT, super::super::matrix::SCALE_INPUT, super::super::matrix::UNIFORM_SCALE_INPUT, super::super::matrix::ANCHOR_INPUT, ] ); assert_eq!( core.get_input(AUTOSCALE_INPUT).unwrap().default, NodeValue::Combo(0) ); assert_eq!( core.get_input(INTERPOLATION_INPUT).unwrap().default, NodeValue::Combo(2) ); // Eleven gizmos: rotation + polygon + anchor + eight scale points. assert_eq!(core.gizmos.len(), 11); assert_eq!(core.effect_input, TEXTURE_INPUT); assert_ne!(core.flags & crate::node::flags::VIDEO_EFFECT, 0); } #[test] fn value_pushes_matrix_and_job_with_texture() { let (core, behavior) = create(); let inputs = crate::value::NodeValueRow::from([(TEXTURE_INPUT.to_string(), tex())]); let mut table = NodeValueTable::default(); behavior.value(&core, &inputs, Rational::new(0, 1), &mut table); // Matrix output always pushed; texture job queued for the seam. assert!(table.get(ValueType::Matrix).is_some()); assert!(table.get(ValueType::Texture).is_some()); } #[test] fn value_without_texture_pushes_matrix_only() { let (core, behavior) = create(); let mut table = NodeValueTable::default(); behavior.value( &core, &crate::value::NodeValueRow::default(), Rational::new(0, 1), &mut table, ); assert!(table.get(ValueType::Matrix).is_some()); assert!(table.get(ValueType::Texture).is_none()); } #[test] fn value_folds_parent_matrix() { let (mut core, behavior) = create(); let mut parent = super::super::mathbase::identity_matrix(); parent[3] = 50.0; core.set_standard_value(PARENT_INPUT, -1, NodeValue::Matrix(parent)); 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[3], 50.0, "parent translation folds through"); } #[test] fn shader_code_returns_none() { let n = empty_node(); assert!(n.shader_code("anything").is_none()); } #[test] fn adjust_matrix_identity_when_matching_resolutions() { // sequence == texture at a power-of-two resolution: the // scale(2/res) * scale(res/2) composition is exactly identity. let m = TransformDistortNode::adjust_matrix_by_resolutions( super::super::mathbase::identity_matrix(), (640.0, 360.0), (640.0, 360.0), (0.0, 0.0), AutoScaleType::None, ); assert!(super::super::mathbase::matrix_is_identity(m)); } #[test] fn adjust_matrix_translation_and_scale() { // sequence (640, 360), texture (320, 180), offset (10, 20): // scale(2/640) * translate(10, 20) * scale(160, 90) = diag(0.5) // with m[0][3] = 10*2/640 and m[1][3] = 20*2/360. let m = TransformDistortNode::adjust_matrix_by_resolutions( super::super::mathbase::identity_matrix(), (640.0, 360.0), (320.0, 180.0), (10.0, 20.0), AutoScaleType::None, ); assert!((m[0] - 0.5).abs() < 1e-12); assert!((m[5] - 0.5).abs() < 1e-12); assert!((m[3] - 10.0 * 2.0 / 640.0).abs() < 1e-12); assert!((m[7] - 20.0 * 2.0 / 360.0).abs() < 1e-12); } #[test] fn adjust_matrix_stretch_autoscale() { // Stretch scales per-axis to the sequence size on top of the // base composition: with sequence == texture the base is already // identity, and stretch adds scale(seq/tex) = 1 — identity. let m = TransformDistortNode::adjust_matrix_by_resolutions( super::super::mathbase::identity_matrix(), (640.0, 360.0), (640.0, 360.0), (0.0, 0.0), AutoScaleType::Stretch, ); assert!(super::super::mathbase::matrix_is_identity(m)); // Half-size texture with a position offset: the base composition // halves the texture (0.5) and stretch's (2, 2) brings it back to // unit scale. The offset lives in the pre-scale matrix, so the // sequence-space scale (2/640) applies to it: 100 * 2/640 = 0.3125 // (post-multiplied scales preserve the translation column). let mat = super::super::matrix::matrix_translate( super::super::mathbase::identity_matrix(), 100.0, 0.0, ); let m = TransformDistortNode::adjust_matrix_by_resolutions( mat, (640.0, 360.0), (320.0, 180.0), (0.0, 0.0), AutoScaleType::Stretch, ); assert!((m[0] - 1.0).abs() < 1e-12); assert!((m[5] - 1.0).abs() < 1e-12); assert!( (m[3] - 100.0 * 2.0 / 640.0).abs() < 1e-12, "offset scaled into sequence units" ); } #[test] fn create_scale_point_maps_and_offsets() { let m = super::super::mathbase::identity_matrix(); let pt = TransformDistortNode::create_scale_point(1.0, -1.0, (320.0, 180.0), m); assert_eq!(pt, (321.0, 179.0)); } #[test] fn get_direction_from_angles() { assert_eq!( TransformDistortNode::get_direction_from_angles(0.0, 1.0), RotationDirection::Positive ); assert_eq!( TransformDistortNode::get_direction_from_angles(1.0, 0.0), RotationDirection::Negative ); } #[test] fn duplicate_clones() { let (core, behavior) = create(); let dup = behavior.duplicate(&core).unwrap(); assert_eq!(dup.name(), "Transform"); assert_eq!(dup.short_name(), "Transform"); } } /// Register this node type (C++ factory entry for /// `org.olivevideoeditor.Olive.transform`). pub fn register(meta: &mut Vec) { meta.push(NodeMeta { type_id: "org.olivevideoeditor.Olive.transform", name: "Transform", categories: &[Category::Distort], create, }); }