refactor: workspace layout — crates/, app at root, legacy C++ removed

Single mechanical restructure commit:
- root Cargo.toml = oakapp bin + workspace; one cargo build produces
  oakapp, oak-cli, oak-worker, liboakengine.dylib
- app/rust/src -> src/ (app at repo root, no rust/ nesting)
- src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore;
  src/bindings/oakotio -> crates/oakotio; src/engine/rust ->
  crates/oakengine (keeps cdylib+staticlib+rlib)
- public C headers include/<mod>/ -> crates/oakengine/include/<mod>/
- OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone)
- legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/,
  app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets
  submodule, otio-install, all build-* output (~40GB)
- oakstorage kept but excluded from the workspace (skeleton w/ todos);
  gpui excluded (own workspace)
- verified: cargo build green, cargo test --workspace 1845/0
  (with the documented OCIO_RS_* env override for the homebrew OCIO)
This commit is contained in:
2026-08-10 20:24:25 +08:00
parent f8540e3892
commit 013a175707
4212 changed files with 8331 additions and 2274987 deletions
+373
View File
@@ -0,0 +1,373 @@
// 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/>.
//! Tracks and track lists (C++ `Track`, `TrackList`).
//! `// CPP-PARITY: src/node/src/output/track/track.{h,cpp}`.
use crate::id::NodeId;
use crate::node::{Category, NodeBehavior, NodeCore};
/// Track media type (values match C++ `Track::Type`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TrackType {
/// Video.
Video,
/// Audio.
Audio,
/// Subtitle.
Subtitle,
}
impl TrackType {
/// C ABI value (`OakNodeTrackType`: NONE=-1, VIDEO=0, AUDIO=1,
/// SUBTITLE=2).
pub fn to_c(self) -> i32 {
match self {
TrackType::Video => 0,
TrackType::Audio => 1,
TrackType::Subtitle => 2,
}
}
/// From a C ABI value; `None` for anything outside 0..=2.
pub fn from_c(v: i32) -> Option<TrackType> {
match v {
0 => Some(TrackType::Video),
1 => Some(TrackType::Audio),
2 => Some(TrackType::Subtitle),
_ => None,
}
}
}
/// Track behavior: an ordered block list (C++ `Track`).
#[derive(Clone)]
pub struct TrackBehavior {
/// Media type.
pub kind: TrackType,
/// Block node ids in timeline order.
pub blocks: Vec<NodeId>,
/// Muted flag.
pub muted: bool,
/// Locked flag.
pub locked: bool,
/// Height in internal units (C++ `track_height_`).
pub height: f64,
/// Index inside its track list (C++ `index_`).
pub index: i32,
/// Owning track list id (None when detached).
pub track_list: Option<NodeId>,
}
/// Track list behavior (C++ `TrackList`): the per-type collection of
/// tracks inside a sequence.
#[derive(Clone)]
pub struct TrackListBehavior {
/// Media type of this list.
pub kind: TrackType,
/// Track node ids in stack order.
pub tracks: Vec<NodeId>,
/// Owning sequence id (None when detached).
pub sequence: Option<NodeId>,
/// The sequence input-array element index base (C++
/// `TrackList::k_track_input_format` index; the first list owns
/// elements 0..n, the second n.., etc.).
pub array_base: i32,
}
/// Default track height in internal units (C++ `k_track_height_default`).
pub const DEFAULT_HEIGHT_INTERNAL: f64 = 3.0;
/// Minimum track height in internal units (C++ `k_track_height_minimum`).
pub const MINIMUM_HEIGHT_INTERNAL: f64 = 1.5;
/// Default font height in pixels (C++ `Track::default_font_height`).
pub const DEFAULT_FONT_HEIGHT: f64 = 13.0;
/// Internal -> pixel height (C++ `Track::internal_height_to_pixel_height`).
pub fn internal_height_to_pixel_height(h: f64) -> i32 {
(h * DEFAULT_FONT_HEIGHT).round() as i32
}
/// Pixel -> internal height (C++ `Track::pixel_height_to_internal_height`).
pub fn pixel_height_to_internal_height(h: i32) -> f64 {
h as f64 / DEFAULT_FONT_HEIGHT
}
impl TrackBehavior {
/// New track of the given type.
pub fn new(kind: TrackType) -> Self {
TrackBehavior {
kind,
blocks: Vec::new(),
muted: false,
locked: false,
height: DEFAULT_HEIGHT_INTERNAL,
index: 0,
track_list: None,
}
}
/// Block at `index` (None out of range).
pub fn block_at(&self, index: usize) -> Option<NodeId> {
self.blocks.get(index).copied()
}
/// Index of `block` in the block list.
pub fn block_index(&self, block: NodeId) -> Option<usize> {
self.blocks.iter().position(|b| *b == block)
}
/// Append a block (C++ `Track::append_block`).
pub fn append_block(&mut self, block: NodeId) {
self.blocks.push(block);
}
/// Prepend a block (C++ `Track::prepend_block`).
pub fn prepend_block(&mut self, block: NodeId) {
self.blocks.insert(0, block);
}
/// Insert a block at `index` (clamped; C++ `insert_block_at_index`).
pub fn insert_block_at_index(&mut self, block: NodeId, index: usize) {
let index = index.min(self.blocks.len());
self.blocks.insert(index, block);
}
/// Insert `block` after `before` (C++ `insert_block_after`).
pub fn insert_block_after(&mut self, block: NodeId, before: NodeId) -> bool {
if let Some(i) = self.block_index(before) {
self.blocks.insert(i + 1, block);
true
} else {
false
}
}
/// Insert `block` before `after` (C++ `insert_block_before`).
pub fn insert_block_before(&mut self, block: NodeId, after: NodeId) -> bool {
if let Some(i) = self.block_index(after) {
self.blocks.insert(i, block);
true
} else {
false
}
}
/// Remove `block` (no-op when absent; C++ `Track::remove_block`).
pub fn remove_block(&mut self, block: NodeId) -> bool {
let before = self.blocks.len();
self.blocks.retain(|b| *b != block);
self.blocks.len() != before
}
/// Ripple-remove: drop the block and shift the successors' positions
/// earlier by the block's length (C++ `Track::ripple_remove_block`).
pub fn ripple_remove_block(&mut self, block: NodeId) -> bool {
self.remove_block(block)
}
/// Replace `old` with `replace` (C++ `Track::replace_block`); both
/// must have equal lengths (the caller validates).
pub fn replace_block(&mut self, old: NodeId, replace: NodeId) -> bool {
if let Some(i) = self.block_index(old) {
self.blocks[i] = replace;
true
} else {
false
}
}
/// Block strictly containing `time` (in < time < out; C++
/// `block_containing_time`).
pub fn block_containing_time(&self, time: oakcore_rs::Rational, blocks: &dyn BlockRange) -> Option<NodeId> {
self.blocks
.iter()
.find(|b| blocks.contains_strict(**b, time))
.copied()
}
/// Block visible at `time` (in <= time < out; C++
/// `visible_block_at_time`).
pub fn visible_block_at_time(&self, time: oakcore_rs::Rational, blocks: &dyn BlockRange) -> Option<NodeId> {
self.blocks
.iter()
.find(|b| blocks.contains(**b, time))
.copied()
}
/// Whether the [in, out) range holds no block or only a gap (C++
/// `is_range_free`).
pub fn is_range_free(&self, range: oakcore_rs::TimeRange, blocks: &dyn BlockRange) -> bool {
!self
.blocks
.iter()
.any(|b| blocks.overlaps(*b, range))
}
/// Total length (end of the last block; C++ `Track::get_length`).
pub fn length(&self, blocks: &dyn BlockRange) -> oakcore_rs::Rational {
let mut end = oakcore_rs::Rational::new(0, 1);
for b in &self.blocks {
let out = blocks.out(*b);
if out > end {
end = out;
}
}
end
}
/// Track reference as (type, index) (C++ `Track::Reference`).
pub fn reference(&self) -> (i32, i32) {
(self.kind.to_c(), self.index)
}
}
/// Block-range accessor trait: the graph-backed queries the track needs
/// (block in/out/length) without coupling track.rs to the graph arena.
pub trait BlockRange {
/// In-point of `block`.
fn in_(&self, block: NodeId) -> oakcore_rs::Rational;
/// Out-point of `block`.
fn out(&self, block: NodeId) -> oakcore_rs::Rational;
/// True when `time` is strictly inside the block.
fn contains_strict(&self, block: NodeId, time: oakcore_rs::Rational) -> bool {
let (in_, out) = (self.in_(block), self.out(block));
time > in_ && time < out
}
/// True when `time` is visible on the block (in <= t < out).
fn contains(&self, block: NodeId, time: oakcore_rs::Rational) -> bool {
let (in_, out) = (self.in_(block), self.out(block));
time >= in_ && time < out
}
/// True when the block's span overlaps `range`.
fn overlaps(&self, block: NodeId, range: oakcore_rs::TimeRange) -> bool {
let (in_, out) = (self.in_(block), self.out(block));
!(out <= range.in_() || in_ >= range.out())
}
}
impl TrackListBehavior {
/// New empty list.
pub fn new(kind: TrackType) -> Self {
TrackListBehavior {
kind,
tracks: Vec::new(),
sequence: None,
array_base: 0,
}
}
/// Track at `index`.
pub fn track_at(&self, index: usize) -> Option<NodeId> {
self.tracks.get(index).copied()
}
/// Index of `track` in the list.
pub fn track_index(&self, track: NodeId) -> Option<usize> {
self.tracks.iter().position(|t| *t == track)
}
/// Combined length of the longest track (C++
/// `TrackList::get_total_length`).
pub fn total_length(&self, tracks: &dyn TrackRange) -> oakcore_rs::Rational {
let mut longest = oakcore_rs::Rational::new(0, 1);
for t in &self.tracks {
let len = tracks.length(*t);
if len > longest {
longest = len;
}
}
longest
}
}
impl NodeBehavior for TrackBehavior {
fn name(&self) -> &str {
match self.kind {
TrackType::Video => "Video Track",
TrackType::Audio => "Audio Track",
TrackType::Subtitle => "Subtitle Track",
}
}
fn type_id(&self) -> &str {
"org.olivevideoeditor.Olive.track"
}
fn categories(&self) -> &[Category] {
&[Category::Timeline]
}
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
Some(Box::new(TrackBehavior {
kind: self.kind,
blocks: self.blocks.clone(),
muted: self.muted,
locked: self.locked,
height: self.height,
index: self.index,
track_list: self.track_list,
}))
}
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 NodeBehavior for TrackListBehavior {
fn name(&self) -> &str {
match self.kind {
TrackType::Video => "Video Tracks",
TrackType::Audio => "Audio Tracks",
TrackType::Subtitle => "Subtitle Tracks",
}
}
fn type_id(&self) -> &str {
"org.olivevideoeditor.Olive.tracklist"
}
fn categories(&self) -> &[Category] {
&[Category::Timeline]
}
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
Some(Box::new(TrackListBehavior {
kind: self.kind,
tracks: self.tracks.clone(),
sequence: self.sequence,
array_base: self.array_base,
}))
}
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)
}
}
/// Track-length accessor trait (the graph-backed query the list needs).
pub trait TrackRange {
/// Total length of `track`.
fn length(&self, track: NodeId) -> oakcore_rs::Rational;
}