// 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 .
//! Free functions replacing the C++ `Node` static methods
//! (COVERAGE.md §6/§9).
use oakcore_rs::TimeRange;
use oakundo::undocommand::UndoCommand;
use crate::graph::Graph;
use crate::id::NodeId;
use crate::node::Category;
/// Human-readable category name (C++ `Node::get_category_name`).
pub fn category_name(c: Category) -> &'static str {
match c {
Category::Output => "Output",
Category::Effect => "Effect",
Category::Generator => "Generator",
Category::Input => "Input",
Category::Math => "Math",
Category::Color => "Color",
Category::Distort => "Distort",
Category::Filter => "Filter",
Category::Keying => "Keying",
Category::OpenFx => "OpenFX",
Category::Timeline => "Timeline",
Category::Group => "Group",
}
}
/// Copy inputs (and optionally connections) between two nodes
/// (C++ `Node::copy_inputs` / `copy_input` /
/// `copy_values_of_element`).
pub fn copy_inputs(
graph: &mut Graph,
src: NodeId,
dst: NodeId,
include_connections: bool,
) -> crate::error::Result<()> {
use crate::error::Error;
if graph.get(src).is_none() || graph.get(dst).is_none() {
return Err(Error::NotFound);
}
let src_inputs: Vec = graph
.get(src)
.map(|e| e.core.inputs.iter().map(|i| i.id.clone()).collect())
.ok_or(Error::NotFound)?;
// Copy every declared input's standard value (whole-value semantics;
// C++ copies per-element too — array elements are covered when the
// array family lands).
for id in &src_inputs {
if !graph
.get(dst)
.map(|e| e.core.has_input(id))
.unwrap_or(false)
{
continue;
}
let value = {
let entry = graph.get(src).ok_or(Error::NotFound)?;
entry.core.standard_value(id, -1)
};
let declared = {
let entry = graph.get(src).ok_or(Error::NotFound)?;
entry
.core
.input_data_type(id)
.unwrap_or(crate::value::ValueType::None)
};
let value = {
// Re-quantize to the destination's declared type so the copy
// never stores a mismatched payload.
let entry = graph.get(dst).ok_or(Error::NotFound)?;
let dst_declared = entry.core.input_data_type(id).ok_or(Error::NotFound)?;
if dst_declared == declared {
value
} else {
crate::value::NodeValue::with_scalar(&value, dst_declared, value.to_double())
}
};
let entry = graph.get_mut(dst).ok_or(Error::NotFound)?;
entry.core.set_standard_value(id, -1, value);
}
if include_connections {
// Recreate src's scalar input connections on dst.
for id in &src_inputs {
if let Some(from) = graph.connected_output(src, id, -1) {
graph.connect(from, dst, id, -1).ok();
}
}
}
Ok(())
}
/// Copy a node with its upstream dependency subgraph
/// (C++ `copy_dependency_graph` / `copy_node_in_graph` /
/// `copy_node_and_dependency_graph_minus_items`). Returns the new
/// node ids (source order). Undo packaging happens at the caller via
/// oakundo's `UndoCommand`.
pub fn copy_subgraph(
graph: &mut Graph,
nodes: &[NodeId],
exclude_items: bool,
) -> crate::error::Result> {
todo!()
}
/// Transform a time range from one node's frame of reference to
/// another's along the connection path (C++ `Node::transform_time_to`).
pub fn transform_time_to(
graph: &Graph,
time: TimeRange,
from: NodeId,
to: NodeId,
) -> crate::error::Result {
todo!()
}
/// Undo-command display strings (C++
/// `get_connect_command_string`/`get_disconnect_command_string`).
pub fn connect_command_string(output: NodeId, input: NodeId, input_id: &str) -> String {
todo!()
}
/// See [`connect_command_string`].
pub fn disconnect_command_string(output: NodeId, input: NodeId, input_id: &str) -> String {
todo!()
}
/// Lock a project mutex (poison-tolerant).
fn lock_any(m: &std::sync::Mutex) -> std::sync::MutexGuard<'_, T> {
m.lock().unwrap_or_else(|e| e.into_inner())
}
/// Build an un-executed [`UndoCommand`] from redo/undo closures (the
/// direct-Rust replacement of the former oakundo bridge
/// `command_from_closures`).
fn command_from_closures(
redo: impl FnMut() + Send + 'static,
undo: impl FnMut() + Send + 'static,
) -> UndoCommand {
UndoCommand::from_closures(redo, undo)
}
/// Set a keyframed/standard value at a time, returning an un-executed
/// undo command (C++ `Node::set_value_at_time` static; command creation
/// via oakundo's `UndoCommand`). The mutation is chosen from the current
/// state at creation time, like the C++ (`// CPP-PARITY: node.cpp:1782`):
/// - keyframing input with a key at `time` -> replace the key's value;
/// - keyframing input without a key -> insert a key (best type =
/// closest key's type, default Linear);
/// - non-keyframing input -> set the standard value.
///
/// `project` is the project owning `graph`; the returned command's
/// closures lock it on redo/undo.
pub fn set_value_at_time_command(
project: &std::sync::Arc>,
graph: &Graph,
node: NodeId,
input: &str,
element: i32,
time: oakcore_rs::Rational,
value: &crate::value::NodeValue,
) -> crate::error::Result {
use crate::error::Error;
use crate::keyframe::{Interpolation, Keyframe};
// Determine the mutation from the current state.
let declared = graph
.get(node)
.and_then(|e| e.core.input_data_type(input))
.ok_or(Error::NotFound)?;
let keyframing = graph
.get(node)
.and_then(|e| e.core.keyframe_track(input, element))
.map(|t| !t.keys().is_empty())
.unwrap_or(false);
let project = project.clone();
let node = node;
let input = input.to_string();
let value = value.clone();
let element = element;
if keyframing {
let existing = graph
.get(node)
.and_then(|e| e.core.keyframe_track(&input, element))
.and_then(|t| {
t.keys()
.iter()
.find(|k| k.time == time)
.map(|k| (k.time, k.value.clone(), k.interpolation))
});
match existing {
Some((key_time, old_value, _interp)) => {
// Replace the key's value (preserving type/handles).
let project_redo = project.clone();
let project_undo = project.clone();
Ok(command_from_closures(
{
let value = value.clone();
let input = input.clone();
let project = project_redo;
move || {
let mut g = lock_any(&project);
if let Some(e) = g.graph.get_mut(node) {
e.core
.keyframe_track_mut(&input, element)
.set_key_value(key_time, value.clone());
}
}
},
{
let old_value = old_value.clone();
let input = input.clone();
let project = project_undo;
move || {
let mut g = lock_any(&project);
if let Some(e) = g.graph.get_mut(node) {
e.core
.keyframe_track_mut(&input, element)
.set_key_value(key_time, old_value.clone());
}
}
},
))
}
None => {
let input_redo = input.clone();
let input_undo = input.clone();
// Insert a new key.
let interp = graph
.get(node)
.and_then(|e| e.core.keyframe_track(&input, element))
.and_then(|t| {
t.keys()
.iter()
.filter(|k| k.time <= time)
.next_back()
.map(|k| k.interpolation)
})
.unwrap_or(Interpolation::Linear);
let project_redo = project.clone();
let project_undo = project.clone();
let value_redo = value.clone();
Ok(command_from_closures(
move || {
let mut g = lock_any(&project_redo);
if let Some(e) = g.graph.get_mut(node) {
let track = e.core.keyframe_track_mut(&input_redo, element);
track.set_key(Keyframe {
time,
value: value_redo.clone(),
interpolation: interp,
bezier_in: (0.0, 0.0),
bezier_out: (0.0, 0.0),
});
}
},
move || {
let mut g = lock_any(&project_undo);
if let Some(e) = g.graph.get_mut(node) {
e.core
.keyframe_track_mut(&input_undo, element)
.remove_key(time);
}
},
))
}
}
} else {
// Set the standard value.
let old = graph
.get(node)
.map(|e| e.core.standard_value(&input, element))
.unwrap_or(crate::value::NodeValue::None);
let project_redo = project.clone();
let project_undo = project.clone();
let input_redo = input.clone();
let input_undo = input.clone();
let value_redo = value.clone();
Ok(command_from_closures(
move || {
let mut g = lock_any(&project_redo);
if let Some(e) = g.graph.get_mut(node) {
e.core
.set_standard_value(&input_redo, element, value_redo.clone());
}
},
move || {
let mut g = lock_any(&project_undo);
if let Some(e) = g.graph.get_mut(node) {
e.core.set_standard_value(&input_undo, element, old.clone());
}
},
))
}
}