Files
oak-editor/crates/oakcore/src/timerange.rs
T
Mike-Solar 18ff60f147 feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline
  TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple
  trim bugs the stub was hiding); same-track via the frozen C ABI,
  cross-track supported by the module command
- oaknode clip blocks now declare a tex_in texture input and set
  effect_input to it, so timeline clips can host effect chains; facade
  test covers effect insert/remove on a real clip
- oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a
  Homebrew upgrade left the system ffmpeg .pc pointing at a deleted
  dav1d Cellar path, breaking links); reads a git-ignored workspace
  .env for IDEs that cannot inject env vars (RustRover); links the C++
  stdlib for C++ codec libs (svt-av1)
- oakengine re-exports oaknode so tests share one crate instance;
  it_node uses the direct instance's value type where it calls the
  module FFI (the --workspace dev-dependency feature split builds
  oaknode twice)
2026-08-11 23:04:48 +08:00

250 lines
6.9 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/>.
//! Time ranges and normalized range lists (oakcore `TimeRange` /
//! `TimeRangeList` equivalents).
use crate::rational::{self, Rational};
/// Half-open time range [in, out) — mirrors `olive::core::TimeRange`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub struct TimeRange {
in_: Rational,
out: Rational,
}
impl TimeRange {
/// Construct and normalize (C++ ctor calls `normalize()`: if `out <
/// in` the two are swapped). The doc comment on the skeleton claimed
/// normalization is not performed; matching C++ takes precedence.
pub fn new(in_: Rational, out: Rational) -> TimeRange {
let mut r = TimeRange { in_, out };
r.normalize();
r
}
/// Inclusive start.
pub fn in_(&self) -> Rational {
self.in_
}
/// Exclusive end.
pub fn out(&self) -> Rational {
self.out
}
/// `out - in`. When either endpoint is a `RATIONAL_MIN/MAX`
/// sentinel the subtraction propagates NaN, matching C++ which
/// stores the same sentinel value for `length_`.
pub fn length(&self) -> Rational {
self.out - self.in_
}
/// True when `t` lies in [in, out).
pub fn contains(&self, t: Rational) -> bool {
t >= self.in_ && t < self.out
}
/// True when `self` contains `compare`, honoring inclusivity of the
/// in/out edges (C++ `TimeRange::contains(TimeRange)`).
fn contains_range(&self, compare: &TimeRange, in_inclusive: bool, out_inclusive: bool) -> bool {
let contains_in = if in_inclusive {
compare.in_ >= self.in_
} else {
compare.in_ > self.in_
};
let contains_out = if out_inclusive {
compare.out <= self.out
} else {
compare.out < self.out
};
contains_in && contains_out
}
/// True when `self` and `a` overlap, honoring edge inclusivity
/// (C++ `TimeRange::overlaps_with`).
fn overlaps_with(&self, a: &TimeRange, in_inclusive: bool, out_inclusive: bool) -> bool {
let does_not_overlap_in = if in_inclusive {
a.out < self.in_
} else {
a.out <= self.in_
};
let does_not_overlap_out = if out_inclusive {
a.in_ > self.out
} else {
a.in_ >= self.out
};
!does_not_overlap_in && !does_not_overlap_out
}
/// Intersection; empty when disjoint (C++ `intersected`).
///
/// Note: C++ normalizes the result, so disjoint inputs produce a
/// swapped (in > out) range rather than an "empty" marker; we match
/// that bit-for-bit.
pub fn intersected(&self, other: &TimeRange) -> TimeRange {
TimeRange::new(
std::cmp::max(self.in_, other.in_),
std::cmp::min(self.out, other.out),
)
}
/// Union that also merges touching ranges (C++ `combined`).
pub fn combined(&self, other: &TimeRange) -> TimeRange {
TimeRange::new(
std::cmp::min(self.in_, other.in_),
std::cmp::max(self.out, other.out),
)
}
/// C++ `set_in` + `normalize`.
fn set_in(&mut self, in_: Rational) {
self.in_ = in_;
self.normalize();
}
/// C++ `set_out` + `normalize`.
fn set_out(&mut self, out: Rational) {
self.out = out;
self.normalize();
}
/// C++ `normalize`: swap if `out < in`.
fn normalize(&mut self) {
if self.out < self.in_ {
std::mem::swap(&mut self.out, &mut self.in_);
}
}
}
/// Normalized (sorted, non-overlapping) list of ranges — mirrors
/// `olive::core::TimeRangeList` including its merge-on-insert and
/// subtraction semantics.
#[derive(Clone, Debug, Default)]
pub struct TimeRangeList {
ranges: Vec<TimeRange>,
}
impl TimeRangeList {
/// Empty list.
pub fn new() -> Self {
TimeRangeList { ranges: Vec::new() }
}
/// True when any element fully contains `range` (C++
/// `TimeRangeList::contains`, inclusive edges).
fn contains_range(&self, range: &TimeRange) -> bool {
self.ranges
.iter()
.any(|r| r.contains_range(range, true, true))
}
/// Insert a range, merging overlaps and touching neighbors
/// (C++ `insert(TimeRange)`).
pub fn insert(&mut self, range: TimeRange) {
// If the list already fully contains this range, nothing to do.
if self.contains_range(&range) {
return;
}
let mut range = range;
let mut i = 0;
while i < self.ranges.len() {
let compare = self.ranges[i];
if compare.overlaps_with(&range, true, true) {
range = compare.combined(&range);
self.ranges.remove(i);
} else {
i += 1;
}
}
self.ranges.push(range);
}
/// Subtract a range (C++ `remove`, via `util_remove`).
pub fn remove(&mut self, range: TimeRange) {
let mut additions: Vec<TimeRange> = Vec::new();
let mut i = 0;
while i < self.ranges.len() {
let compare = self.ranges[i];
if range.contains_range(&compare, true, true) {
// The removal range entirely encompasses this element.
self.ranges.remove(i);
} else if compare.contains_range(&range, false, false) {
// The removal range is strictly inside this element:
// split it into two.
let mut new_range = compare;
new_range.set_in(range.out);
let mut trimmed = compare;
trimmed.set_out(range.in_);
self.ranges[i] = trimmed;
additions.push(new_range);
break;
} else {
if compare.in_ < range.in_ && compare.out > range.in_ {
// This element's out overlaps the range's in: trim it.
self.ranges[i].set_out(range.in_);
} else if compare.in_ < range.out && compare.out > range.out {
// This element's in overlaps the range's out: trim it.
self.ranges[i].set_in(range.out);
}
i += 1;
}
}
self.ranges.extend(additions);
}
/// Sorted ranges view.
pub fn ranges(&self) -> &[TimeRange] {
&self.ranges
}
/// True when the list has no ranges.
pub fn is_empty(&self) -> bool {
self.ranges.is_empty()
}
/// Total covered duration (sum of each range's length).
pub fn total_length(&self) -> Rational {
let mut total = Rational::new(0, 1);
for r in &self.ranges {
total = total + r.length();
}
total
}
/// First time covered by any range (C++ `in()` on the first range);
/// null rational when empty.
pub fn first(&self) -> Rational {
match self.ranges.first() {
Some(r) => r.in_(),
None => Rational::NULL,
}
}
/// Frame-accurate iteration helper: snap a time to the containing
/// frame grid of `timebase` (C++ TimeRangeListFrameIterator snap,
/// `k_floor` rounding).
pub fn snap(&self, time: Rational, timebase: Rational) -> Rational {
let _ = self; // self carries no state relevant to a single snap
rational::snap_time_to_timebase(time, timebase)
}
}