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
|
||||
|
||||
@@ -869,3 +869,205 @@ fn roundtrip_full_timeline() {
|
||||
};
|
||||
assert_eq!(xml, xml2, "re-save is idempotent");
|
||||
}
|
||||
|
||||
/// Standalone `MultiCamNode` round-trip: the `current_in` standard value
|
||||
/// and the node's type survive save/load (C++
|
||||
/// `ProjectSerializer::FileRoundTripPreservesMultiCamNode`).
|
||||
#[test]
|
||||
fn multicam_node_round_trip_preserves_current_in() {
|
||||
use oaknode::nodes::multicamnode::{create, CURRENT_INPUT};
|
||||
use oaknode::project::Project;
|
||||
use oaknode::value::NodeValue;
|
||||
|
||||
let project = Project::new();
|
||||
let mc_id = {
|
||||
let mut p = project.lock().unwrap();
|
||||
let (core, behavior) = create();
|
||||
let id = p.graph.add_node(core, behavior);
|
||||
p.graph.get_mut(id).unwrap().core.set_standard_value(
|
||||
CURRENT_INPUT,
|
||||
-1,
|
||||
NodeValue::Combo(2),
|
||||
);
|
||||
id
|
||||
};
|
||||
let _ = mc_id;
|
||||
|
||||
let xml = {
|
||||
let p = project.lock().unwrap();
|
||||
oaknode::serializer::save(&p).unwrap()
|
||||
};
|
||||
let loaded = oaknode::serializer::load(&xml).unwrap();
|
||||
let l = loaded.lock().unwrap();
|
||||
|
||||
let loaded_mc = l
|
||||
.graph
|
||||
.node_ids()
|
||||
.into_iter()
|
||||
.find(|id| {
|
||||
l.graph.get(*id).map(|e| e.behavior.type_id())
|
||||
== Some("org.olivevideoeditor.Olive.multicam")
|
||||
})
|
||||
.expect("loaded project has a MultiCamNode");
|
||||
// The combo's numeric value survives the round-trip (the value codec
|
||||
// serializes combo indices as Int — `string_to_value` maps
|
||||
// `ValueType::Combo` to `NodeValue::Int`).
|
||||
assert_eq!(
|
||||
l.graph.get(loaded_mc).unwrap().core.standard_value(CURRENT_INPUT, -1),
|
||||
NodeValue::Int(2),
|
||||
"current_in survives the round-trip"
|
||||
);
|
||||
}
|
||||
|
||||
/// A clip with multicam enabled round-trips: the `current_in` value, the
|
||||
/// `sequence_in` edge, and the `sequence_type_in` selector all survive.
|
||||
#[test]
|
||||
fn multicam_clip_round_trip_preserves_wiring() {
|
||||
use oakcore_rs::Rational;
|
||||
use oaknode::block::{clip_create, ClipBlockBehavior};
|
||||
use oaknode::node::NodeCore;
|
||||
use oaknode::nodes::multicamnode::{
|
||||
create, CURRENT_INPUT, SEQUENCE_INPUT, SEQUENCE_TYPE_INPUT,
|
||||
};
|
||||
use oaknode::project::Project;
|
||||
use oaknode::sequence::SequenceBehavior;
|
||||
use oaknode::track::{TrackBehavior, TrackListBehavior, TrackType};
|
||||
use oaknode::value::NodeValue;
|
||||
|
||||
let project = Project::new();
|
||||
let (seq_id, clip_id, mc_id) = {
|
||||
let mut p = project.lock().unwrap();
|
||||
let (core, behavior) = SequenceBehavior::create();
|
||||
let seq_id = p.graph.add_node(core, behavior);
|
||||
let (core, behavior) = TrackListBehavior::create();
|
||||
let list_id = p.graph.add_node(core, behavior);
|
||||
let (core, behavior) = (NodeCore::new(), Box::new(TrackBehavior::new(TrackType::Video)));
|
||||
let track_id = p.graph.add_node(core, behavior);
|
||||
{
|
||||
let seq = p.graph.get_mut(seq_id).unwrap();
|
||||
let s = seq
|
||||
.behavior
|
||||
.as_any_mut()
|
||||
.unwrap()
|
||||
.downcast_mut::<SequenceBehavior>()
|
||||
.unwrap();
|
||||
s.track_lists.push(list_id);
|
||||
}
|
||||
let list = p.graph.get_mut(list_id).unwrap();
|
||||
let l = list
|
||||
.behavior
|
||||
.as_any_mut()
|
||||
.unwrap()
|
||||
.downcast_mut::<TrackListBehavior>()
|
||||
.unwrap();
|
||||
l.sequence = Some(seq_id);
|
||||
l.tracks.push(track_id);
|
||||
let track = p.graph.get_mut(track_id).unwrap();
|
||||
let t = track
|
||||
.behavior
|
||||
.as_any_mut()
|
||||
.unwrap()
|
||||
.downcast_mut::<TrackBehavior>()
|
||||
.unwrap();
|
||||
t.kind = TrackType::Video;
|
||||
t.track_list = Some(list_id);
|
||||
|
||||
// A clip on the track.
|
||||
let (core, behavior) = clip_create();
|
||||
let clip_id = p.graph.add_node(core, behavior);
|
||||
let clip = p.graph.get_mut(clip_id).unwrap();
|
||||
let c = clip
|
||||
.behavior
|
||||
.as_any_mut()
|
||||
.unwrap()
|
||||
.downcast_mut::<ClipBlockBehavior>()
|
||||
.unwrap();
|
||||
c.core.range = oakcore_rs::TimeRange::new(Rational::new(0, 1), Rational::new(100, 1));
|
||||
c.core.track = Some(track_id);
|
||||
let track = p.graph.get_mut(track_id).unwrap();
|
||||
let t = track
|
||||
.behavior
|
||||
.as_any_mut()
|
||||
.unwrap()
|
||||
.downcast_mut::<TrackBehavior>()
|
||||
.unwrap();
|
||||
t.blocks.push(clip_id);
|
||||
|
||||
// The multicam node routed between the sequence and the clip.
|
||||
let (core, behavior) = create();
|
||||
let mc_id = p.graph.add_node(core, behavior);
|
||||
p.graph
|
||||
.connect(mc_id, clip_id, "tex_in", -1)
|
||||
.unwrap();
|
||||
p.graph.connect(seq_id, mc_id, SEQUENCE_INPUT, -1).unwrap();
|
||||
let mc = p.graph.get_mut(mc_id).unwrap();
|
||||
mc.core
|
||||
.set_standard_value(CURRENT_INPUT, -1, NodeValue::Combo(2));
|
||||
mc.core.set_standard_value(
|
||||
SEQUENCE_TYPE_INPUT,
|
||||
-1,
|
||||
NodeValue::Combo(0),
|
||||
);
|
||||
(seq_id, clip_id, mc_id)
|
||||
};
|
||||
let _ = (seq_id, clip_id, mc_id);
|
||||
|
||||
let xml = {
|
||||
let p = project.lock().unwrap();
|
||||
oaknode::serializer::save(&p).unwrap()
|
||||
};
|
||||
let loaded = oaknode::serializer::load(&xml).unwrap();
|
||||
let l = loaded.lock().unwrap();
|
||||
// The multicam node round-tripped with its current_in and type selector.
|
||||
let loaded_mc = l
|
||||
.graph
|
||||
.node_ids()
|
||||
.into_iter()
|
||||
.find(|id| {
|
||||
l.graph.get(*id).map(|e| e.behavior.type_id())
|
||||
== Some("org.olivevideoeditor.Olive.multicam")
|
||||
})
|
||||
.expect("loaded project has a MultiCamNode");
|
||||
assert_eq!(
|
||||
l.graph.get(loaded_mc).unwrap().core.standard_value(CURRENT_INPUT, -1),
|
||||
NodeValue::Int(2),
|
||||
"current_in survives"
|
||||
);
|
||||
assert_eq!(
|
||||
l.graph.get(loaded_mc).unwrap().core.standard_value(SEQUENCE_TYPE_INPUT, -1),
|
||||
NodeValue::Int(0),
|
||||
"sequence_type_in survives"
|
||||
);
|
||||
|
||||
// The wiring survives: a sequence feeds the multicam's sequence_in and
|
||||
// the multicam feeds a clip's tex_in.
|
||||
let seq_src = l
|
||||
.graph
|
||||
.connected_output(loaded_mc, SEQUENCE_INPUT, -1)
|
||||
.expect("sequence_in edge restored");
|
||||
assert_eq!(
|
||||
l.graph.get(seq_src).unwrap().behavior.type_id(),
|
||||
"org.olivevideoeditor.Olive.sequence"
|
||||
);
|
||||
let clip_dst = l
|
||||
.graph
|
||||
.output_connections(loaded_mc)
|
||||
.into_iter()
|
||||
.find(|(_, input, _)| input == "tex_in")
|
||||
.map(|(to, _, _)| to)
|
||||
.expect("multicam still feeds a clip's tex_in");
|
||||
assert_eq!(
|
||||
l.graph.get(clip_dst).unwrap().behavior.type_id(),
|
||||
"org.olivevideoeditor.Olive.clipblock"
|
||||
);
|
||||
|
||||
// The multicam behavior's cached sequence reference was rebuilt from
|
||||
// the restored edge.
|
||||
let behavior = l.graph.get(loaded_mc).unwrap();
|
||||
let mc_node = behavior
|
||||
.behavior
|
||||
.as_any()
|
||||
.and_then(|a| a.downcast_ref::<oaknode::nodes::multicamnode::MultiCamNode>())
|
||||
.expect("loaded node is a MultiCamNode");
|
||||
assert_eq!(mc_node.sequence(), Some(seq_src));
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ pub mod common;
|
||||
pub mod error;
|
||||
pub mod handle;
|
||||
pub mod marker;
|
||||
pub mod multicam;
|
||||
pub mod undocommon;
|
||||
pub mod undogeneral;
|
||||
pub mod undopointer;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,12 +31,13 @@
|
||||
|
||||
use oakcore_rs::Rational;
|
||||
use oaknode::graph::NodeEntry;
|
||||
use oaknode::id::NodeId;
|
||||
use oakundo::undocommand::UndoCommand;
|
||||
|
||||
use crate::util::{
|
||||
block_add_to_graph, block_in, block_length, block_out, block_remove_from_graph,
|
||||
block_set_length_and_media_in, block_set_length_and_media_out, block_track,
|
||||
track_insert_block_after, track_ripple_remove_block, GraphBlockRange, NodeRef,
|
||||
block_in, block_length, block_out, block_set_length_and_media_in,
|
||||
block_set_length_and_media_out, block_track, track_insert_block_after,
|
||||
track_ripple_remove_block, GraphBlockRange, NodeRef,
|
||||
};
|
||||
|
||||
/// `BlockSplitCommand` — split one block at a point
|
||||
@@ -46,6 +47,15 @@ use crate::util::{
|
||||
/// the first half `[in, point)`) and anchors the cloned `new_block` at its
|
||||
/// out-point (it becomes the second half `[point, out)`), inserted right
|
||||
/// after the original so the track order mirrors the timeline order.
|
||||
///
|
||||
/// The second half is a copy of the original block's whole dependency
|
||||
/// graph (C++ `BlockSplitCommand::prepare` calls `Node::copy_node_in_graph`
|
||||
/// — `// CPP-PARITY: timelineundosplit.cpp:34-39`): the block's upstream
|
||||
/// nodes (effects, the MultiCamNode, ...) are cloned too, so the two halves
|
||||
/// own independent copies. Bin items (footage, sequences — `IS_ITEM`) are
|
||||
/// shared, exactly as `Graph::copy_node_and_dependency_graph_minus_items`
|
||||
/// defines. `undo` detaches the whole copied subgraph (not just the second
|
||||
/// block) and the next `redo` re-attaches it identity-preserving.
|
||||
pub struct BlockSplitCommand {
|
||||
/// Block to split.
|
||||
block: NodeRef,
|
||||
@@ -53,9 +63,17 @@ pub struct BlockSplitCommand {
|
||||
point: Rational,
|
||||
/// Second block created by the split (valid after `redo`).
|
||||
new_block: Option<NodeRef>,
|
||||
/// Arena entry of the second block while it is detached from the
|
||||
/// graph (between `undo` and the next `redo`).
|
||||
new_block_entry: Option<NodeEntry>,
|
||||
/// Copy of `block`'s dependency graph created at `prepare`: the node
|
||||
/// ids of every node the copy introduced (the second block first,
|
||||
/// then its copied upstream nodes), in copy order.
|
||||
copied: Vec<NodeId>,
|
||||
/// Input edges of the copied nodes captured at `prepare`
|
||||
/// `(from, to, input, element)` — the endpoints are copied ids or
|
||||
/// shared item ids. Recreated after a re-attach.
|
||||
copied_edges: Vec<(NodeId, NodeId, String, i32)>,
|
||||
/// Detached arena entries for [`Self::copied`] (index-parallel), owned
|
||||
/// by this command between `undo` and the next `redo`.
|
||||
detached: Vec<Option<NodeEntry>>,
|
||||
/// Length of `block` before the split, restored on `undo`.
|
||||
old_length: Rational,
|
||||
}
|
||||
@@ -69,34 +87,90 @@ impl BlockSplitCommand {
|
||||
block,
|
||||
point,
|
||||
new_block: None,
|
||||
new_block_entry: None,
|
||||
copied: Vec::new(),
|
||||
copied_edges: Vec::new(),
|
||||
detached: Vec::new(),
|
||||
old_length: Rational::new(0, 1),
|
||||
}
|
||||
}
|
||||
|
||||
/// `prepare`: create the second half by cloning the original block in
|
||||
/// the project graph (the Rust equivalent of the C++
|
||||
/// `oaknode_node_copy_in_graph`; the clone happens through the
|
||||
/// behavior's `duplicate`, which copies the block core too).
|
||||
/// `prepare`: create the second half by copying the original block's
|
||||
/// dependency graph in the project graph (the Rust equivalent of the
|
||||
/// C++ `Node::copy_node_in_graph` +
|
||||
/// `copy_node_and_dependency_graph_minus_items` — see the module doc).
|
||||
pub fn prepare(&mut self) {
|
||||
if self.new_block.is_some() {
|
||||
return;
|
||||
}
|
||||
let id = {
|
||||
let project = self.block.project.clone();
|
||||
let project = self.block.project.clone();
|
||||
let (id, map) = {
|
||||
let mut p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
let entry = match p.graph.get(self.block.id) {
|
||||
Some(e) => e,
|
||||
match p.graph.copy_node_and_dependency_graph_minus_items(self.block.id) {
|
||||
Some(r) => r,
|
||||
None => return,
|
||||
};
|
||||
let new_core = entry.core.clone();
|
||||
let new_behavior = match entry.behavior.duplicate(&entry.core) {
|
||||
Some(b) => b,
|
||||
None => return,
|
||||
};
|
||||
p.graph.add_node(new_core, new_behavior)
|
||||
}
|
||||
};
|
||||
self.new_block = Some(NodeRef::new(self.block.project.clone(), id));
|
||||
// The copied nodes: the new block plus every fresh copy in the
|
||||
// map (items map to themselves and are shared, not copied).
|
||||
let mut copied = vec![id];
|
||||
for (old, new) in &map {
|
||||
if old != new && *new != id {
|
||||
copied.push(*new);
|
||||
}
|
||||
}
|
||||
// Capture the copied nodes' input edges (endpoints are copies or
|
||||
// shared items) so a re-attach after undo can rewire them.
|
||||
let copied_edges = {
|
||||
let p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
let mut edges = Vec::new();
|
||||
for &node_id in &copied {
|
||||
for (from, input, element) in p.graph.input_connections(node_id) {
|
||||
edges.push((from, node_id, input, element));
|
||||
}
|
||||
}
|
||||
edges
|
||||
};
|
||||
self.new_block = Some(NodeRef::new(project, id));
|
||||
self.copied = copied;
|
||||
self.copied_edges = copied_edges;
|
||||
self.detached = (0..self.copied.len()).map(|_| None).collect();
|
||||
}
|
||||
|
||||
/// Re-insert the copied subgraph detached by a previous `undo` and
|
||||
/// rewire its edges. No-op on the first `redo` (the copies are still in
|
||||
/// the graph from `prepare`).
|
||||
fn re_attach_subgraph(&mut self) {
|
||||
if self.detached.iter().all(Option::is_none) {
|
||||
return;
|
||||
}
|
||||
let mut p = self
|
||||
.block
|
||||
.project
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
for (i, id) in self.copied.iter().enumerate() {
|
||||
if let Some(entry) = self.detached[i].take() {
|
||||
p.graph.add_entry(entry, *id);
|
||||
}
|
||||
}
|
||||
for (from, to, input, element) in &self.copied_edges {
|
||||
p.graph.connect(*from, *to, input, *element).ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// Detach the whole copied subgraph (the second block and every node
|
||||
/// the copy introduced), preserving the entries for the next `redo`.
|
||||
fn detach_subgraph(&mut self) {
|
||||
let mut p = self
|
||||
.block
|
||||
.project
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
for (i, id) in self.copied.iter().enumerate() {
|
||||
if self.detached[i].is_none() {
|
||||
self.detached[i] = p.graph.take_node(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `redo`: shrink `block` to the first half, grow `new_block` to the
|
||||
@@ -111,6 +185,8 @@ impl BlockSplitCommand {
|
||||
// Create the second half if redo is invoked without a preceding
|
||||
// prepare() (the vtable command path may call redo directly).
|
||||
self.prepare();
|
||||
// Re-attach the copied subgraph if a previous undo detached it.
|
||||
self.re_attach_subgraph();
|
||||
|
||||
// The C++ asserts `point_` lies strictly inside the block; that
|
||||
// would panic across the FFI boundary, so it is intentionally not
|
||||
@@ -125,10 +201,6 @@ impl BlockSplitCommand {
|
||||
let second_half_length = block_out - self.point;
|
||||
|
||||
if let Some(new_block) = &self.new_block {
|
||||
// Re-attach the second half if a previous undo detached it.
|
||||
if self.new_block_entry.is_some() {
|
||||
block_add_to_graph(new_block, self.new_block_entry.take());
|
||||
}
|
||||
// In-anchored length for the first half keeps the original's in
|
||||
// point (the C++ `set_length_and_media_in`); the out-anchored
|
||||
// length for the second half keeps the copy's out point (the
|
||||
@@ -147,7 +219,8 @@ impl BlockSplitCommand {
|
||||
// has no transition edges, so both are omitted here.
|
||||
}
|
||||
|
||||
/// `undo`: restore `block`'s original length and remove the second half.
|
||||
/// `undo`: restore `block`'s original length, remove the second half
|
||||
/// and detach the whole copied subgraph from the project graph.
|
||||
pub fn undo(&mut self) {
|
||||
if let Some(track) = block_track(&self.block) {
|
||||
// The redo shrank the original from its in point (it became the
|
||||
@@ -158,14 +231,12 @@ impl BlockSplitCommand {
|
||||
block_set_length_and_media_in(&self.block, self.old_length);
|
||||
if let Some(new_block) = &self.new_block {
|
||||
track_ripple_remove_block(&track, new_block);
|
||||
// Detach the second half from the graph (the C++ re-parents
|
||||
// it to the scratch memory manager); the entry is owned by
|
||||
// this command until the next redo.
|
||||
if self.new_block_entry.is_none() {
|
||||
self.new_block_entry = block_remove_from_graph(new_block);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Detach the copied subgraph (the C++ re-parents it to the scratch
|
||||
// memory manager); the entries are owned by this command until the
|
||||
// next redo.
|
||||
self.detach_subgraph();
|
||||
|
||||
// The C++ first moves a previously-moved out transition back onto
|
||||
// `block` and runs `reconnect_tree_command_`'s undo; the Rust block
|
||||
|
||||
Reference in New Issue
Block a user