feat(oaktimeline): multicam enable/disable/switch commands, split copies the dependency graph
- oaktimeline::multicam: clip_find_multicam (buffer/tex_in depth-1 lookup), multicam_enable/disable (rewire sequence<->clip through a MultiCamNode), multicam_switch (split-preserving-links at the playhead, each half owns an independent multicam copy, linked clips switched together) as single undo commands with C++ labels. - BlockSplitCommand now duplicates the clip's whole dependency graph (copy_node_and_dependency_graph_minus_items) instead of just the block core, matching the C++ BlockSplitCommand::prepare semantics; undo detaches the copied subgraph, redo re-attaches identity- preserving. - oaknode: fix serializer dropping edges from the first-created node (ptr=0 was not registered in id_map), restoring sequence_in edge round-trips; multicam node and clip wiring serializer round-trip tests.
This commit is contained in:
@@ -163,6 +163,8 @@ impl NodeBehavior for FolderBehavior {
|
||||
/// `enabled_in` (`// CPP-PARITY: folder.cpp:26`).
|
||||
pub fn create(name: &str) -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
let mut core = NodeCore::empty();
|
||||
// Bin item (C++ `folder.cpp:38` `set_flag(k_is_item)`).
|
||||
core.flags |= crate::node::flags::IS_ITEM;
|
||||
let mut child = crate::input::Input::new(
|
||||
"child_in",
|
||||
crate::value::ValueType::None,
|
||||
|
||||
@@ -289,6 +289,9 @@ impl FootageBehavior {
|
||||
/// behavior (`// CPP-PARITY: footage.cpp:83`, `viewer.cpp:84`).
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
let mut core = NodeCore::new();
|
||||
// Bin item (C++ `footage.cpp:61` `set_flag(k_is_item)`): shared,
|
||||
// never cloned, by dependency-graph copies.
|
||||
core.flags |= crate::node::flags::IS_ITEM;
|
||||
let mut file = Input::new(
|
||||
"file_in",
|
||||
ValueType::Text,
|
||||
|
||||
@@ -617,6 +617,73 @@ impl Graph {
|
||||
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<NodeId, NodeId>)> {
|
||||
let mut created: HashMap<NodeId, NodeId> = 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<NodeId, NodeId>,
|
||||
) -> Option<NodeId> {
|
||||
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<Edge> = self
|
||||
|
||||
@@ -120,6 +120,10 @@ pub mod flags {
|
||||
pub const AUDIO_EFFECT: u64 = 0x4;
|
||||
/// `k_dont_show_in_create_menu`.
|
||||
pub const DONT_SHOW_IN_CREATE_MENU: u64 = 0x8;
|
||||
/// `k_is_item` (C++ `node.h:119`): bin items (folders, footage,
|
||||
/// sequences) — dependency-graph copies share these instead of
|
||||
/// cloning them (`// CPP-PARITY: node.cpp:1159,1199`).
|
||||
pub const IS_ITEM: u64 = 0x10;
|
||||
}
|
||||
|
||||
impl NodeCore {
|
||||
|
||||
@@ -36,7 +36,7 @@ mod mathbase;
|
||||
mod matrix;
|
||||
mod merge;
|
||||
mod mosaicfilternode;
|
||||
mod multicamnode;
|
||||
pub mod multicamnode;
|
||||
mod noise;
|
||||
mod ociobase;
|
||||
mod ociogradingtransformlinear;
|
||||
|
||||
@@ -69,6 +69,28 @@ impl MultiCamNode {
|
||||
core.standard_value(CURRENT_INPUT, -1).to_double() as i32
|
||||
}
|
||||
|
||||
/// The connected sequence node (C++ `sequence_`).
|
||||
pub fn sequence(&self) -> Option<NodeId> {
|
||||
self.sequence
|
||||
}
|
||||
|
||||
/// Set/clear the connected-sequence state (the effects of the C++
|
||||
/// `InputConnectedEvent`/`InputDisconnectedEvent` on `sequence_in`:
|
||||
/// store the sequence and toggle the `sequence_type_in` hidden flag).
|
||||
/// The graph arena does not dispatch behavior events on edge edits, so
|
||||
/// the commands that edit the `sequence_in` edge call this to keep the
|
||||
/// behavior state in sync with the graph.
|
||||
pub fn set_sequence(&mut self, core: &mut NodeCore, sequence: Option<NodeId>) {
|
||||
if let Some(slot) = core.get_input_mut(SEQUENCE_TYPE_INPUT) {
|
||||
if sequence.is_some() {
|
||||
slot.flags &= !crate::input::flags::HIDDEN;
|
||||
} else {
|
||||
slot.flags |= crate::input::flags::HIDDEN;
|
||||
}
|
||||
}
|
||||
self.sequence = sequence;
|
||||
}
|
||||
|
||||
/// Number of available sources (C++ `get_source_count()`): the
|
||||
/// connected sequence's track count when a sequence is set,
|
||||
/// otherwise the `sources_in` array size.
|
||||
|
||||
@@ -77,6 +77,9 @@ impl SequenceBehavior {
|
||||
/// from a file — the track lists arrive as separate nodes.
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
let mut core = NodeCore::new();
|
||||
// Bin item (C++ `sequence.cpp:37` `set_flag(k_is_item)`): nested
|
||||
// sequences are shared, never cloned, by dependency-graph copies.
|
||||
core.flags |= crate::node::flags::IS_ITEM;
|
||||
// Viewer parameter streams (C++ ViewerOutput::kVideoParamsInput /
|
||||
// kAudioParamsInput / kSubtitleParamsInput arrays).
|
||||
for (id, ty) in [
|
||||
|
||||
@@ -624,10 +624,11 @@ fn load_node(
|
||||
) -> crate::error::Result<NodeId> {
|
||||
use crate::error::Error;
|
||||
let type_id = reader.attribute("id").unwrap_or_default();
|
||||
let ptr = reader
|
||||
.attribute("ptr")
|
||||
.and_then(|p| p.parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
// The packed identity the file assigns this node (`ptr`). `None` when
|
||||
// the attribute is absent (foreign/old files); only present `ptr`s are
|
||||
// registered so an explicit identity of `0` (the first-created node)
|
||||
// still resolves its outgoing connections.
|
||||
let ptr = reader.attribute("ptr").and_then(|p| p.parse::<u64>().ok());
|
||||
|
||||
// Instantiate the node type; timeline structural types (which are
|
||||
// not in the factory menu) are reconstructed directly, unknown
|
||||
@@ -648,7 +649,7 @@ fn load_node(
|
||||
// The node enters the graph before its body is parsed so deferred
|
||||
// connections/links can reference it by id.
|
||||
let id = graph.add_node(core, behavior);
|
||||
if ptr != 0 {
|
||||
if let Some(ptr) = ptr {
|
||||
id_map.insert(ptr, id);
|
||||
}
|
||||
|
||||
@@ -985,6 +986,31 @@ fn resolve_timeline_refs(graph: &mut Graph, id_map: &std::collections::HashMap<u
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MultiCamNode: rebuild the cached sequence reference (C++
|
||||
// `sequence_`) from the restored `sequence_in` edge — the graph arena
|
||||
// fires no connect events at load, so the behavior state is synced
|
||||
// here (also re-applies the `sequence_type_in` unhide of the C++
|
||||
// `InputConnectedEvent`).
|
||||
for id in graph.node_ids() {
|
||||
let seq = graph.connected_output(
|
||||
id,
|
||||
crate::nodes::multicamnode::SEQUENCE_INPUT,
|
||||
-1,
|
||||
);
|
||||
if seq.is_none() {
|
||||
continue;
|
||||
}
|
||||
if let Some(entry) = graph.get_mut(id) {
|
||||
if let Some(mc) = entry
|
||||
.behavior
|
||||
.as_any_mut()
|
||||
.and_then(|a| a.downcast_mut::<crate::nodes::multicamnode::MultiCamNode>())
|
||||
{
|
||||
mc.set_sequence(&mut entry.core, seq);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fold C++ `child_in` connections into the folder children (the Rust
|
||||
|
||||
Reference in New Issue
Block a user