fix(app): context-menu actions, clip clipboard, A/V drop, add-track, default tracks, undo divergence
- Right-clicking an unselected clip selects it first (C++ parity) — this is what made Cut/Delete appear to do nothing. - Cut/Copy/Paste clipboard: clipboard_copy/cut/paste on the engine, clipboard clips keep footage/range/speed/track kind and stay linked in the pasted group; paste lands at the playhead as one undo entry. - Dropping a video-with-audio footage places the video clip plus a linked audio clip at the same range in ONE 'Add Clip' undo entry. - Add Video/Audio Track buttons in the timeline toolbar and the track header context menu; new sequences start with 2 video + 2 audio tracks (not an undoable edit). - oaknode Graph::add_entry now reclaims the slot from the free list — before, a detached-then-reattached node left its slot in the free list, so node_count undercounted and the next add_node silently clobbered the restored node. This was the user's 'undo, redo, undo, redo and the result changed' bug; regression covered by cycle tests (move/trim/delete/split/add-track/linked-placement all converge).
This commit is contained in:
@@ -111,7 +111,12 @@ impl Graph {
|
||||
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.
|
||||
// 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);
|
||||
|
||||
@@ -222,3 +222,35 @@ fn adjacency_queries() {
|
||||
assert_eq!(g.downstream(a), vec![b, c]);
|
||||
assert_eq!(g.downstream(d), Vec::<NodeId>::new());
|
||||
}
|
||||
|
||||
/// take_node + add_entry must restore the node count AND reclaim the
|
||||
/// slot: before the fix, add_entry reused the slot without removing it
|
||||
/// from the free list, so node_count kept undercounting and the next
|
||||
/// add_node silently overwrote the restored node (the undo/redo
|
||||
/// divergence the user hit: repeated undo/redo changed the result).
|
||||
#[test]
|
||||
fn add_entry_reclaims_the_free_slot() {
|
||||
let (mut g, ids) = build(3);
|
||||
let victim = ids[1];
|
||||
let count_before = g.node_count();
|
||||
|
||||
// Detach and re-attach: the count must round-trip.
|
||||
let entry = g.take_node(victim).expect("take the node");
|
||||
assert_eq!(g.node_count(), count_before - 1, "detach drops the count");
|
||||
let readded = g.add_entry(entry, victim);
|
||||
assert_eq!(readded, victim, "identity is preserved");
|
||||
assert_eq!(g.node_count(), count_before, "re-attach restores the count");
|
||||
|
||||
// A fresh add_node must NOT clobber the restored node (it must get a
|
||||
// different slot).
|
||||
let mut core = NodeCore::new();
|
||||
core.label = "fresh".to_string();
|
||||
let fresh = g.add_node(core, Box::new(TestNode { id: "fresh" }));
|
||||
assert_ne!(fresh, victim, "the fresh node takes a different slot");
|
||||
assert!(g.is_valid(victim), "the restored node survives add_node");
|
||||
assert_eq!(
|
||||
g.get(victim).map(|e| e.core.label.as_str()),
|
||||
g.get(victim).map(|e| e.core.label.as_str()),
|
||||
);
|
||||
assert_eq!(g.node_count(), count_before + 1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user