timeline: adjustment layers and first-class transitions
Adjustment layers (docs/zh/plans/adjustment-layers-and-transitions.md): a new timeline block type whose effect chain grades the composite of every video track below it, over its own range (spanning clips or a slice of one). The graph path flushes the lower tracks at the block's track boundary and sweeps the composite through the chain via a transient texture-source node; the montage path mirrors it with AdjustmentSpan tickets (wire-compatible), so worker previews and exports agree. An empty-area context menu creates one; the block trims/moves/deletes like a clip, with undo everywhere. Transitions: seam blocks come alive - cross dissolve/fade/wipe/slide evaluate both neighbors through the graph path with progress from the transition's own range (never the whole clip). Ctrl+Shift+D or the clip menu inserts a default transition; the gpui wedges render and drag to resize offsets undoably, and TransitionRemoveCommand now restores offsets and edges on undo. The transitionfx node form runs the same shaders on an adjustment layer with progress_in auto-filled from the layer's span (explicit value wins). Also: every built-in effect name and parameter name is now translatable (360 node.* keys per locale, zh-CN fully translated, two coverage tests guard future gaps); the new nodes register in nodes/mod.rs with the factory smoke table updated; textfootage and adjustment-layer i18n keys included.
This commit is contained in:
@@ -31,7 +31,10 @@ use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
use oak_core::videoparams::VideoParams as CommonVideoParams;
|
||||
use oak_core::Rational;
|
||||
use oak_node::block::{BlockCore, ClipBlockBehavior, GapBlockBehavior, TransitionBlockBehavior};
|
||||
use oak_node::block::{
|
||||
AdjustmentBlockBehavior, BlockCore, ClipBlockBehavior, GapBlockBehavior,
|
||||
TransitionBlockBehavior,
|
||||
};
|
||||
use oak_node::folder::FolderBehavior;
|
||||
use oak_node::footage::{FootageBehavior, StreamInfo};
|
||||
use oak_node::graph::Graph;
|
||||
@@ -108,6 +111,9 @@ pub fn block_core_of(graph: &Graph, block: NodeId) -> Option<BlockCore> {
|
||||
if let Some(b) = any.downcast_ref::<TransitionBlockBehavior>() {
|
||||
return Some(b.core.clone());
|
||||
}
|
||||
if let Some(b) = any.downcast_ref::<AdjustmentBlockBehavior>() {
|
||||
return Some(b.core.clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -132,6 +138,10 @@ fn set_block_core(graph: &mut Graph, block: NodeId, f: impl FnOnce(&mut BlockCor
|
||||
f(&mut b.core);
|
||||
return true;
|
||||
}
|
||||
if let Some(b) = any.downcast_mut::<AdjustmentBlockBehavior>() {
|
||||
f(&mut b.core);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
@@ -728,6 +738,18 @@ pub fn block_kind(project: &ProjectRef, block: NodeId) -> BlockKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the block is an adjustment layer (neither `BlockKind` covers
|
||||
/// it: the enum mirrors the deleted C ABI's block types).
|
||||
pub fn block_is_adjustment(project: &ProjectRef, block: NodeId) -> bool {
|
||||
let guard = lock_project(project);
|
||||
guard
|
||||
.graph
|
||||
.get(block)
|
||||
.and_then(|e| e.behavior.as_any())
|
||||
.map(|a| a.downcast_ref::<AdjustmentBlockBehavior>().is_some())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// The block's timeline in point (`oaknode_block_get_in`).
|
||||
pub fn block_in(project: &ProjectRef, block: NodeId) -> Rational {
|
||||
let guard = lock_project(project);
|
||||
|
||||
@@ -143,8 +143,10 @@ impl SaveOTIOTask {
|
||||
}
|
||||
|
||||
/// Serialize one track: a `Track` whose kind matches the native track
|
||||
/// type, one OTIO block per native block, plus a trailing `Gap` when the
|
||||
/// track is shorter than `max_track_length`.
|
||||
/// type, one OTIO block per native block (an adjustment layer, which has
|
||||
/// no OTIO equivalent, becomes an equal-length `Gap` so later children
|
||||
/// keep their timeline positions), plus a trailing `Gap` when the track
|
||||
/// is shorter than `max_track_length`.
|
||||
///
|
||||
/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track)
|
||||
fn serialize_track(
|
||||
@@ -258,7 +260,28 @@ impl SaveOTIOTask {
|
||||
|
||||
Some(Composable::Transition(otio_transition))
|
||||
}
|
||||
nodeops::BlockKind::Other => None,
|
||||
nodeops::BlockKind::Other => {
|
||||
// Adjustment layers have no OTIO equivalent. OTIO places
|
||||
// children sequentially, so dropping the block would shift
|
||||
// every following child left; keeping its span as a Gap
|
||||
// holds the timeline in place (and round-trips as a gap,
|
||||
// which is what "no adjustment applied" means here).
|
||||
nodeops::block_is_adjustment(project, block).then(|| {
|
||||
Composable::Gap(Gap::new(
|
||||
TimeRange::new(
|
||||
RationalTime::from_rational(
|
||||
block_in_of(project, block),
|
||||
sequence_rate,
|
||||
),
|
||||
RationalTime::from_rational(
|
||||
block_length_of(project, block),
|
||||
sequence_rate,
|
||||
),
|
||||
),
|
||||
nodeops::node_label(project, block),
|
||||
))
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let Some(otio_block) = otio_block else {
|
||||
|
||||
@@ -542,6 +542,7 @@ impl RenderTask {
|
||||
cache_timebase: None,
|
||||
footage: Some((filename, 0)),
|
||||
montage: Vec::new(),
|
||||
adjustments: Vec::new(),
|
||||
}),
|
||||
// Sequence viewer: the montage is resolved without the lock.
|
||||
None => {
|
||||
@@ -558,6 +559,7 @@ impl RenderTask {
|
||||
cache_timebase: None,
|
||||
footage: None,
|
||||
montage,
|
||||
adjustments: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user