Files
oak-editor/crates/oak-node/src/nodes/transformdistortnode.rs
T
Mike-Solar 244d5e860f
CI / Build & test (Windows) (push) Failing after 7s
workspace: kebab-case crates, app under crates/oak-app, shared versions
All crates take the oak-* kebab-case naming (oak-audio, oak-codec,
oak-common, oak-core, oak-ffmpeg-link, oak-node, oak-otio, oak-plugin,
oak-render, oak-storage, oak-task, oak-timeline, oak-undo), with the
lib identifiers rewritten (oakrender:: -> oak_render::, oakcore_rs:: ->
oak_core::, ...) across all 226 referencing files.

The GUI application moves from the workspace root into
crates/oak-app/: src/, build.rs (paths fixed for the new location) and
tests/ travel with it, the root Cargo.toml becomes workspace-only
([workspace] + workspace.package + profiles), and the app package
inherits the workspace version. The screenshots example becomes a
standalone crate examples/simple_player/ with its own Cargo.toml.

Every crate now inherits the single workspace version
(version.workspace = true), and the workflows' crate paths and the
build docs follow the renames.

Validated with a clean cargo check --workspace.
2026-08-22 16:58:37 +08:00

856 lines
30 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/>.
//! 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<String> {
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<Box<dyn NodeBehavior>> {
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<dyn NodeBehavior>) {
// 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<NodeMeta>) {
meta.push(NodeMeta {
type_id: "org.olivevideoeditor.Olive.transform",
name: "Transform",
categories: &[Category::Distort],
create,
});
}