// 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 . //! The graph arena: node storage, edges, traversal order. //! //! Replaces the C++ pointer web (`Node::parent_`, output_connections_) //! with a slab arena + edge set. All structural mutation goes through //! `&mut Graph` methods; evaluation takes `&Graph`. use std::collections::{BTreeSet, HashMap, HashSet}; use crate::id::NodeId; use crate::node::{NodeBehavior, NodeCore}; /// One arena slot. pub struct NodeEntry { /// Shared data. pub core: NodeCore, /// Polymorphic behavior. pub behavior: Box, /// Generation for stale-id detection. pub generation: u32, /// True when the slot is free. pub vacant: bool, } /// A directed edge: `from` node's output feeds `to` node's `input` /// (element for array inputs). #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Edge { /// Source node. pub from: NodeId, /// Destination node. pub to: NodeId, /// Destination input id hash + element (string stored arena-side). pub input_key: u64, /// Array element (-1 = scalar input). pub element: i32, } /// The node graph. Nodes are owned here, exclusively. pub struct Graph { entries: Vec, free_list: Vec, edges: BTreeSet, /// Arena-side storage for edge input ids (key = hash into this map). input_names: HashMap, } impl Graph { /// Empty graph. pub fn new() -> Self { Graph { entries: Vec::new(), free_list: Vec::new(), edges: BTreeSet::new(), input_names: HashMap::new(), } } /// Insert a node; returns its id. pub fn add_node(&mut self, core: NodeCore, behavior: Box) -> NodeId { let (index, generation) = match self.free_list.pop() { Some(i) => { // Reuse the slot with a fresh generation (the slot's // previous occupant is gone; its generation counter // advances so stale ids fail). let gen = self.entries[i as usize].generation.wrapping_add(1); self.entries[i as usize] = NodeEntry { core, behavior, generation: gen, vacant: false, }; (i, gen) } None => { let index = self.entries.len() as u32; let entry = NodeEntry { core, behavior, generation: 0, vacant: false, }; self.entries.push(entry); (index, 0) } }; NodeId::new(index, generation) } /// Insert a previously detached [`NodeEntry`] (from /// [`Graph::take_node`]) back into this graph, preserving its identity /// when the original slot is free here. `id` is the node's identity /// before the detach; it is reused when its slot is vacant. Used by /// the project node-transfer paths so a node's stable identity /// survives a move. pub fn add_entry(&mut self, entry: NodeEntry, id: NodeId) -> NodeId { let index = id.index(); if (index as usize) < self.entries.len() && self.entries[index as usize].vacant { // Original slot free: reuse (index, generation) unchanged. The // slot was pushed to the free list by `take_node` — reclaim it, // or `node_count` keeps undercounting and, worse, the next // `add_node` hands the same slot out again and silently // clobbers the restored node (the undo/redo divergence). self.free_list.retain(|&i| i != index); let generation = entry.generation; self.entries[index as usize] = entry; return NodeId::new(index, generation); } // Slot occupied (or out of range): allocate fresh. match self.free_list.pop() { Some(i) => { let gen = self.entries[i as usize].generation.wrapping_add(1); let new_id = NodeId::new(i, gen); self.entries[i as usize] = NodeEntry { core: entry.core, behavior: entry.behavior, generation: gen, vacant: false, }; new_id } None => { let i = self.entries.len() as u32; self.entries.push(NodeEntry { core: entry.core, behavior: entry.behavior, generation: 0, vacant: false, }); NodeId::new(i, 0) } } } /// Detach a node from the arena, returning its full entry (core + /// behavior + generation) and removing all its edges. `None` for a /// stale id. Unlike [`Graph::remove_node`] the entry is preserved for /// re-insertion elsewhere (project detach/attach moves). pub fn take_node(&mut self, id: NodeId) -> Option { if !self.is_valid(id) { return None; } let idx = id.index() as usize; let vacant_entry = NodeEntry { core: NodeCore::empty(), behavior: Box::new(crate::nodes::EmptyBehavior), generation: self.entries[idx].generation, vacant: true, }; // Move the real entry out. let taken = std::mem::replace(&mut self.entries[idx], vacant_entry); self.free_list.push(id.index()); self.drop_edges_touching(id); Some(taken) } /// Remove a node and all its edges (C++: ~Node + set_parent(null) /// + disconnect_all side effects — see `// CPP-PARITY: node.cpp`). pub fn remove_node(&mut self, id: NodeId) -> Option> { let entry = self.take_node(id)?; Some(entry.behavior) } /// Validated access; `None` for stale ids. pub fn get(&self, id: NodeId) -> Option<&NodeEntry> { if !self.is_valid(id) { return None; } Some(&self.entries[id.index() as usize]) } /// Mutable access; `None` for stale ids. pub fn get_mut(&mut self, id: NodeId) -> Option<&mut NodeEntry> { if !self.is_valid(id) { return None; } Some(&mut self.entries[id.index() as usize]) } /// True when `id` names a live slot. pub fn is_valid(&self, id: NodeId) -> bool { id.valid() && (id.index() as usize) < self.entries.len() && !self.entries[id.index() as usize].vacant && self.entries[id.index() as usize].generation == id.generation() } /// Number of live nodes (used by the project node_count family). pub fn node_count(&self) -> usize { self.entries.len() - self.free_list.len() } /// Live node ids in slot order (stable within a session). pub fn node_ids(&self) -> Vec { let mut ids = Vec::with_capacity(self.node_count()); for (i, e) in self.entries.iter().enumerate() { if !e.vacant { ids.push(NodeId::new(i as u32, e.generation)); } } ids } /// Connect `from`'s output to `to.input[element]` /// (C++ `Node::connect_edge` incl. cycle rejection). /// /// Errors: `NotFound` for a stale endpoint or unknown input id; /// `Invalid` when the input is not connectable; `State` when the /// input is already connected, `from == to`, or the edge would create /// a cycle (the Rust design rejects cycles at connect time so the /// arena never contains them — `// CPP-PARITY: node.cpp:210`). pub fn connect( &mut self, from: NodeId, to: NodeId, input: &str, element: i32, ) -> crate::error::Result<()> { use crate::error::Error; if !self.is_valid(from) || !self.is_valid(to) { return Err(Error::NotFound); } // Input must exist and be connectable (checked before the // state-level rejections, matching the C++ c_api precedence: // NOT_FOUND > INVALID(not connectable) > STATE). let input_flags = { let entry = self.get(to).expect("validated above"); let input = entry.core.get_input(input).ok_or(Error::NotFound)?; input.flags }; if input_flags & crate::input::flags::NOT_CONNECTABLE != 0 { return Err(Error::Invalid); } // Array inputs address elements >= 0; scalar inputs only -1. let is_array = input_flags & crate::input::flags::ARRAY != 0; if !is_array && element != -1 { return Err(Error::Invalid); } if from == to { // Self-connection is a trivial cycle. return Err(Error::State); } // Already connected on this input. if self.connected_output(to, input, element).is_some() { return Err(Error::State); } // Cycle rejection: adding from->to must not let `to` reach `from`. if self.reaches(to, from) { return Err(Error::State); } let key = self.intern_input(input); let edge = Edge { from, to, input_key: key, element, }; self.edges.insert(edge); Ok(()) } /// Disconnect one edge (no-op when absent). pub fn disconnect(&mut self, from: NodeId, to: NodeId, input: &str, element: i32) { let key = self.input_key(input); let edge = Edge { from, to, input_key: key, element, }; self.edges.remove(&edge); } /// Remove the edge feeding `to.input[element]` (returns the source /// node id, or `None` when absent). pub fn disconnect_input(&mut self, to: NodeId, input: &str, element: i32) -> Option { let from = self.connected_output(to, input, element)?; self.disconnect(from, to, input, element); Some(from) } /// The node feeding `to.input[element]`, if any. pub fn connected_output(&self, to: NodeId, input: &str, element: i32) -> Option { let key = self.input_key(input); self.edges .iter() .find(|e| e.to == to && e.input_key == key && e.element == element) .map(|e| e.from) } /// True when `to.input[element]` has a connected edge. pub fn is_input_connected(&self, to: NodeId, input: &str, element: i32) -> bool { self.connected_output(to, input, element).is_some() } /// Incoming edges of `id`: `(source, input_id, element)` — the /// edges feeding this node's inputs. pub fn input_connections(&self, id: NodeId) -> Vec<(NodeId, String, i32)> { self.edges .iter() .filter(|e| e.to == id) .map(|e| { ( e.from, self.input_names .get(&e.input_key) .cloned() .unwrap_or_default(), e.element, ) }) .collect() } /// Outgoing edges of `id`, in stable (edge) order: /// `(target, input_id, element)`. pub fn output_connections(&self, id: NodeId) -> Vec<(NodeId, String, i32)> { self.edges .iter() .filter(|e| e.from == id) .map(|e| { ( e.to, self.input_names .get(&e.input_key) .cloned() .unwrap_or_default(), e.element, ) }) .collect() } /// Every edge in the graph as `(from, to, input_id, element)` (used /// by the deep-copy paths). pub fn output_connections_all(&self) -> Vec<(NodeId, NodeId, String, i32)> { self.edges .iter() .map(|e| { ( e.from, e.to, self.input_names .get(&e.input_key) .cloned() .unwrap_or_default(), e.element, ) }) .collect() } /// Nodes directly upstream of `id` (evaluation order helper). pub fn upstream(&self, id: NodeId) -> Vec { let mut v: Vec = self .edges .iter() .filter(|e| e.to == id) .map(|e| e.from) .collect(); v.sort_unstable(); v } /// Nodes directly downstream of `id` (invalidation fan-out). pub fn downstream(&self, id: NodeId) -> Vec { let mut v: Vec = self .edges .iter() .filter(|e| e.from == id) .map(|e| e.to) .collect(); v.sort_unstable(); v.dedup(); v } /// True when `from` can reach `to` through existing edges (DFS on /// the BTreeSet adjacency). fn reaches(&self, from: NodeId, to: NodeId) -> bool { let mut stack = vec![from]; let mut seen: HashSet = HashSet::new(); while let Some(n) = stack.pop() { if n == to { return true; } if !seen.insert(n) { continue; } stack.extend(self.edges.iter().filter(|e| e.from == n).map(|e| e.to)); } false } /// Topological order from sources to sinks (Kahn; cycles are /// rejected at connect time so this cannot fail). Deterministic: /// ready nodes are taken in ascending [`NodeId`] order. pub fn topological_order(&self) -> Vec { // In-degree per live node. let mut indegree: HashMap = HashMap::new(); for e in &self.edges { *indegree.entry(e.to).or_insert(0) += 1; indegree.entry(e.from).or_insert(0); } let mut ready: BTreeSet = indegree .iter() .filter(|(_, d)| **d == 0) .map(|(n, _)| *n) .collect(); let mut order = Vec::with_capacity(indegree.len()); while let Some(n) = ready.iter().next().copied() { ready.remove(&n); order.push(n); for e in self.edges.iter().filter(|e| e.from == n) { let d = indegree .get_mut(&e.to) .expect("every edge endpoint is counted"); *d -= 1; if *d == 0 { ready.insert(e.to); } } } // Isolated nodes (no edges) are absent from `indegree`; append them. let mut isolated: Vec = self .node_ids() .into_iter() .filter(|n| !indegree.contains_key(n)) .collect(); order.append(&mut isolated); order } /// Intern an input id string, returning its stable key. fn intern_input(&mut self, input: &str) -> u64 { let key = hash_str(input); self.input_names .entry(key) .or_insert_with(|| input.to_string()); key } /// Key for an already-interned (or new) input id; never inserts. fn input_key(&self, input: &str) -> u64 { hash_str(input) } /// Link two nodes bidirectionally (C++ `Node::link`); false when /// already linked or `a == b`. pub fn link(&mut self, a: NodeId, b: NodeId) -> bool { if a == b || !self.is_valid(a) || !self.is_valid(b) || self.are_linked(a, b) { return false; } let (a_idx, b_idx) = (a.index() as usize, b.index() as usize); let a_placeholder = vacant_entry(self.entries[a_idx].generation); let b_placeholder = vacant_entry(self.entries[b_idx].generation); let mut a_entry = std::mem::replace(&mut self.entries[a_idx], a_placeholder); let mut b_entry = std::mem::replace(&mut self.entries[b_idx], b_placeholder); a_entry.core.links.push(b); b_entry.core.links.push(a); self.entries[a_idx] = a_entry; self.entries[b_idx] = b_entry; true } /// Unlink two nodes (C++ `Node::unlink`); false when not linked. pub fn unlink(&mut self, a: NodeId, b: NodeId) -> bool { if !self.are_linked(a, b) { return false; } let (a_idx, b_idx) = (a.index() as usize, b.index() as usize); let a_placeholder = vacant_entry(self.entries[a_idx].generation); let b_placeholder = vacant_entry(self.entries[b_idx].generation); let mut a_entry = std::mem::replace(&mut self.entries[a_idx], a_placeholder); let mut b_entry = std::mem::replace(&mut self.entries[b_idx], b_placeholder); a_entry.core.links.retain(|n| *n != b); b_entry.core.links.retain(|n| *n != a); self.entries[a_idx] = a_entry; self.entries[b_idx] = b_entry; true } /// True when `a` and `b` are linked (C++ `Node::are_linked`). pub fn are_linked(&self, a: NodeId, b: NodeId) -> bool { self.get(a) .map(|e| e.core.links.contains(&b)) .unwrap_or(false) } /// Linked node ids of `id` (C++ `Node::links`). pub fn links_of(&self, id: NodeId) -> Vec { self.get(id) .map(|e| e.core.links.clone()) .unwrap_or_default() } /// Insert an element into an array input, shifting per-element values, /// keyframes and edges (C++ `Node::input_array_insert`). pub fn input_array_insert( &mut self, id: NodeId, input: &str, index: i32, ) -> crate::error::Result<()> { use crate::error::Error; let entry = self.get_mut(id).ok_or(Error::NotFound)?; let input_ = entry.core.get_input(input).ok_or(Error::NotFound)?; if !input_.is_array() { return Err(Error::Invalid); } let size = input_.array_size; if index < 0 || index > size as i32 { return Err(Error::Invalid); } drop(input_); let entry = self.get_mut(id).ok_or(Error::NotFound)?; entry.core.input_array_insert(input, index as usize); // Move connections down one element. let key = self.input_key(input); let moves: Vec = self .edges .iter() .filter(|e| e.to == id && e.input_key == key && e.element >= index) .copied() .collect(); for e in moves { self.edges.remove(&e); let mut shifted = e; shifted.element += 1; self.edges.insert(shifted); } Ok(()) } /// Remove an array element, shifting per-element values, keyframes /// and edges up (C++ `Node::input_array_remove`). pub fn input_array_remove( &mut self, id: NodeId, input: &str, index: i32, ) -> crate::error::Result<()> { use crate::error::Error; let entry = self.get_mut(id).ok_or(Error::NotFound)?; let input_ = entry.core.get_input(input).ok_or(Error::NotFound)?; if !input_.is_array() { return Err(Error::Invalid); } let size = input_.array_size; if index < 0 || index >= size as i32 { return Err(Error::Invalid); } drop(input_); let entry = self.get_mut(id).ok_or(Error::NotFound)?; entry.core.input_array_remove(input, index as usize); // Move connections up one element; drop the connection on the // removed element. let key = self.input_key(input); let moves: Vec = self .edges .iter() .filter(|e| e.to == id && e.input_key == key && e.element >= index) .copied() .collect(); for e in moves { self.edges.remove(&e); if e.element > index { let mut shifted = e; shifted.element -= 1; self.edges.insert(shifted); } } Ok(()) } /// Transfer every live node and edge from `other` into `self`, /// preserving identities where the slots are free. Returns the /// original-id -> new-id mapping. Used when a project adopts a /// self-contained subgraph (e.g. a sequence with its track lists). pub fn transfer_all(&mut self, other: &mut Graph) -> std::collections::HashMap { let mut map = std::collections::HashMap::new(); let ids = other.node_ids(); for id in ids { let entry = match other.take_node(id) { Some(e) => e, None => continue, }; let new_id = self.add_entry(entry, id); map.insert(id, new_id); } // Re-create edges with remapped endpoints. let edges = other.output_connections_all(); for (from, to, input, element) in edges { let from = *map.get(&from).unwrap_or(&from); let to = *map.get(&to).unwrap_or(&to); self.connect(from, to, &input, element).ok(); } map } /// Copy `root` and its upstream dependency graph into fresh nodes (C++ /// `Node::copy_node_and_dependency_graph_minus_items`, /// `// CPP-PARITY: node.cpp:1141-1229`). "Minus items": nodes carrying /// [`crate::node::flags::IS_ITEM`] (folders, footage, sequences) are /// shared, not cloned — copied nodes connect straight to them. Each /// copy inherits the original's core data and behavior state but NO /// links (the C++ `Node::copy()` leaves `links_` empty; the split/link /// commands re-link explicitly), and context positions whose context is /// itself copied are remapped to the copy. Returns the copy of `root` /// plus the old -> new id map (items map to themselves), or `None` /// when `root` is stale or a behavior refuses to duplicate. pub fn copy_node_and_dependency_graph_minus_items( &mut self, root: NodeId, ) -> Option<(NodeId, HashMap)> { let mut created: HashMap = HashMap::new(); let copy = self.copy_dependency_graph_internal(root, &mut created)?; Some((copy, created)) } /// Recursive worker of /// [`Graph::copy_node_and_dependency_graph_minus_items`]. fn copy_dependency_graph_internal( &mut self, node: NodeId, created: &mut HashMap, ) -> Option { if let Some(&existing) = created.get(&node) { return Some(existing); } // Clone core + behavior up front (the recursive adds below would // invalidate any borrow of the entry). let (mut core, behavior, is_item) = { let entry = self.get(node)?; ( entry.core.clone(), entry.behavior.duplicate(&entry.core)?, entry.core.flags & crate::node::flags::IS_ITEM != 0, ) }; if is_item { // Items are shared: upstream edges connect to the original. created.insert(node, node); return Some(node); } // The C++ copy carries no links (`Node::copy()` leaves `links_` // empty). core.links.clear(); // Context positions: a context that is itself copied points at the // copy (the C++ maps context children through the created table). for (context, _, _) in core.context_positions.iter_mut() { if let Some(&mapped) = created.get(context) { *context = mapped; } } let copy = self.add_node(core, behavior); created.insert(node, copy); // Copy the upstream edges, recursing into each source. for (from, input, element) in self.input_connections(node) { let from_copy = self.copy_dependency_graph_internal(from, created)?; // The C++ asserts every reconnect succeeds; a rejected edge // (duplicate input, cycle) is skipped here. self.connect(from_copy, copy, &input, element).ok(); } Some(copy) } /// Drop every edge touching `id` (C++ `disconnect_all`). fn drop_edges_touching(&mut self, id: NodeId) { let doomed: Vec = self .edges .iter() .filter(|e| e.from == id || e.to == id) .copied() .collect(); for e in doomed { self.edges.remove(&e); } } } /// A vacant-slot placeholder entry (used by the two-at-a-time link /// edits, which cannot borrow two slots mutably at once). fn vacant_entry(generation: u32) -> NodeEntry { NodeEntry { core: NodeCore::empty(), behavior: Box::new(crate::nodes::EmptyBehavior), generation, vacant: true, } } /// Deterministic string hash for edge input-id keys (stable across runs; /// `DefaultHasher::new()` uses fixed SipHash keys). fn hash_str(input: &str) -> u64 { use std::hash::{Hash, Hasher}; let mut h = std::collections::hash_map::DefaultHasher::new(); input.hash(&mut h); h.finish() }