Files
oak-editor/crates/oaknode/src/sequence.rs
T
Mike-Solar 74b080f88a 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.
2026-08-18 20:45:01 +08:00

251 lines
7.8 KiB
Rust

// 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 <http://www.gnu.org/licenses/>.
//! Sequence: the C++ `ViewerOutput`/`Sequence` pair — a node that owns
//! tracks, markers, work area, and playback caches.
//! `// CPP-PARITY: src/node/src/output/viewer/viewer.{h,cpp}`,
//! `// CPP-PARITY: src/node/src/project/sequence/sequence.{h,cpp}`.
use crate::id::NodeId;
use crate::input::Input;
use crate::node::{Category, NodeBehavior, NodeCore};
use crate::value::{AudioParams, NodeValue, ValueType, VideoParams};
/// Sequence texture/samples input ids (ViewerOutput::k_texture_input /
/// k_samples_input) and the track input id format (Sequence::
/// k_track_input_format).
pub const TEXTURE_INPUT: &str = "tex_in";
pub const SAMPLES_INPUT: &str = "samples_in";
pub const TRACK_INPUT_FORMAT: &str = "track_in_%1";
/// Sequence behavior (viewer node).
pub struct SequenceBehavior {
/// Track list node ids (video then audio, C++ order).
pub track_lists: Vec<NodeId>,
/// Timeline markers handle (oaktimeline, owned; created lazily by
/// the facade through the C ABI).
pub markers: crate::handle::CHandle,
/// Work area handle (oaktimeline, owned; created lazily by the
/// facade through the C ABI).
pub workarea: crate::handle::CHandle,
/// Length cache (C++ last_length_).
pub last_length: oakcore_rs::Rational,
/// Autocache toggles.
pub autocache_video: bool,
/// Audio autocache toggle.
pub autocache_audio: bool,
/// Playhead position (C++ ViewerOutput::playhead_).
pub playhead: oakcore_rs::Rational,
/// Video parameter streams.
pub video_params: Vec<VideoParams>,
/// Audio parameter streams.
pub audio_params: Vec<AudioParams>,
}
impl SequenceBehavior {
/// Empty sequence with zero tracks and no parameters.
pub fn new() -> Self {
SequenceBehavior {
track_lists: Vec::new(),
markers: crate::handle::CHandle::null(),
workarea: crate::handle::CHandle::null(),
last_length: oakcore_rs::Rational::new(0, 1),
autocache_video: false,
autocache_audio: false,
playhead: oakcore_rs::Rational::new(0, 1),
video_params: Vec::new(),
audio_params: Vec::new(),
}
}
/// Constructor: the C++ `Sequence` input surface with default
/// parameters but no track lists (`// CPP-PARITY: sequence.cpp:36`,
/// `viewer.cpp:84`). Used by the serializer to rebuild a sequence
/// 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 [
("video_param_in", ValueType::VideoParams),
("audio_param_in", ValueType::AudioParams),
("subtitle_param_in", ValueType::None),
] {
let mut input = Input::new(id, ty, NodeValue::None);
input.flags |= crate::input::flags::NOT_CONNECTABLE
| crate::input::flags::NOT_KEYFRAMABLE
| crate::input::flags::ARRAY
| crate::input::flags::HIDDEN;
core.add_input(input);
}
core.add_input(Input::new(
TEXTURE_INPUT,
ValueType::Texture,
NodeValue::None,
));
core.add_input(Input::new(
SAMPLES_INPUT,
ValueType::Samples,
NodeValue::None,
));
// One array input per track list (`track_in_%1`; the video list
// owns track_in_0, audio track_in_1, subtitle track_in_2 —
// `// CPP-PARITY: sequence.h`).
for base in 0..3 {
let mut track_input = Input::new(
&TRACK_INPUT_FORMAT.replace("%1", &base.to_string()),
ValueType::None,
NodeValue::None,
);
track_input.flags |= crate::input::flags::ARRAY;
core.add_input(track_input);
}
let mut behavior = SequenceBehavior::new();
behavior.set_default_parameters();
(core, Box::new(behavior))
}
/// Apply the default video/audio parameters (C++
/// `ViewerOutput::set_default_parameters()`; the config lookups read
/// the oakcommon config store directly).
pub fn set_default_parameters(&mut self) {
let config = oakcommon::configstore::ConfigStore::instance();
let width = config.get_int(None, "DefaultSequenceWidth", 1920);
let height = config.get_int(None, "DefaultSequenceHeight", 1080);
let sample_rate = config.get_int(None, "DefaultSequenceAudioFrequency", 48000);
let fps_num = config.get_int(None, "DefaultSequenceFrameRateNum", 30);
let fps_den = config.get_int(None, "DefaultSequenceFrameRateDen", 1);
self.video_params = vec![VideoParams {
width,
height,
frame_rate: oakcore_rs::Rational::new(fps_num as i64, fps_den as i64),
pixel_format: 4, // f32
channels: 4,
}];
self.audio_params = vec![AudioParams {
sample_rate,
channel_layout: 0x3, // stereo
format: 4, // f32
}];
}
/// Recompute the cached lengths from the track lists (C++
/// `ViewerOutput::verify_length()`).
pub fn verify_length(
&mut self,
lengths: (
oakcore_rs::Rational,
oakcore_rs::Rational,
oakcore_rs::Rational,
),
) {
let (video, audio, overall) = lengths;
self.last_length = overall;
let _ = (video, audio);
}
/// Total stream counts.
pub fn video_stream_count(&self) -> usize {
self.video_params.len()
}
/// Audio stream count.
pub fn audio_stream_count(&self) -> usize {
self.audio_params.len()
}
}
impl NodeBehavior for SequenceBehavior {
fn name(&self) -> &str {
"Sequence"
}
fn type_id(&self) -> &str {
"org.olivevideoeditor.Olive.sequence"
}
fn categories(&self) -> &[Category] {
&[Category::Output]
}
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
Some(Box::new(SequenceBehavior::new()))
}
/// Custom project save (C++ `Sequence::SaveCustom` writes the
/// workarea and markers; those live behind opaque oaktimeline
/// handles in Rust, so the track-list references are all that
/// persists).
fn save_custom(&self, core: &NodeCore, writer: &mut dyn crate::serializer::XmlWrite) {
let _ = core;
if !self.track_lists.is_empty() {
writer.start_element("tracklists");
for t in &self.track_lists {
writer.text_element("tracklist", &t.identity().to_string());
}
writer.end_element(); // tracklists
}
}
/// Custom project load (C++ `Sequence::LoadCustom`): the track-list
/// references are collected here and resolved to live ids by the
/// serializer's post-load pass; the C++ workarea/markers segments
/// are skipped (opaque handles).
fn load_custom(
&mut self,
_core: &mut NodeCore,
reader: &mut dyn crate::serializer::XmlRead,
) -> bool {
while reader.next_start_element() {
match reader.name() {
"tracklists" => {
while reader.next_start_element() {
if reader.name() == "tracklist" {
if let Some(id) =
crate::serializer::parse_node_ref(&reader.read_element_text())
{
self.track_lists.push(id);
}
} else {
reader.skip_current_element();
}
}
}
_ => reader.skip_current_element(),
}
}
true
}
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
}
impl Default for SequenceBehavior {
fn default() -> Self {
SequenceBehavior::new()
}
}