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:
@@ -0,0 +1,382 @@
|
||||
// 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 offset node (C++ `src/node/src/time/timeoffset/timeoffsetnode.{h,cpp}`,
|
||||
//! `olive::TimeOffsetNode`).
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
use crate::node::{Category, NodeBehavior, NodeCore};
|
||||
use crate::value::{NodeValue, NodeValueRow, NodeValueTable};
|
||||
use oakcore_rs::{Rational, TimeRange};
|
||||
|
||||
/// Time offset input id (C++ `k_time_input`). Type: rational; default
|
||||
/// `Rational(0)`; flags: not-connectable; properties: `view = time slider`,
|
||||
/// `viewlock = true`.
|
||||
pub const TIME_INPUT: &str = "time_in";
|
||||
|
||||
/// Effect input id (C++ `k_input_input`). Type: none (pass-through of any
|
||||
/// connected type); flags: not-keyframable.
|
||||
pub const INPUT_INPUT: &str = "input_in";
|
||||
|
||||
/// Time offset node. Shifts the time seen by the connected input by a
|
||||
/// constant amount. The C++ class has no own data members (only the private
|
||||
/// `get_remapped_time()`/`get_remapped_output_time()` helpers, which become
|
||||
/// the behavior of `input_time_adjustment`/`output_time_adjustment` below),
|
||||
/// so this is a unit-like struct.
|
||||
pub struct TimeOffsetNode;
|
||||
|
||||
impl TimeOffsetNode {
|
||||
/// C++ `get_remapped_time()`: the `time_in` input's value at
|
||||
/// `input_time` added to `input_time` — `input + time_in`. `time_in`
|
||||
/// is not-connectable, so no connected edge is ever consulted.
|
||||
pub fn get_remapped_time(core: &NodeCore, input_time: Rational) -> Rational {
|
||||
input_time + Self::time_offset(core, input_time)
|
||||
}
|
||||
|
||||
/// C++ `get_remapped_output_time()`: the inverse of
|
||||
/// [`Self::get_remapped_time`] — `input - time_in`.
|
||||
pub fn get_remapped_output_time(core: &NodeCore, input_time: Rational) -> Rational {
|
||||
input_time - Self::time_offset(core, input_time)
|
||||
}
|
||||
|
||||
/// The `time_in` value evaluated at `input_time` (C++ `get_value_at_time
|
||||
/// (k_time_input, input).value<Rational>()`).
|
||||
fn time_offset(core: &NodeCore, input_time: Rational) -> Rational {
|
||||
match core.value_at_time(TIME_INPUT, -1, input_time) {
|
||||
NodeValue::Rational(r) => r,
|
||||
v => Rational::from_double(v.to_double()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The full C++ `input_time_adjustment()` with an explicit core. For
|
||||
/// `input_in`, both endpoints of the range are shifted forward by the
|
||||
/// `time_in` value evaluated at that endpoint; all other inputs fall
|
||||
/// through to the base-class identity behavior.
|
||||
///
|
||||
/// The [`NodeBehavior::input_time_adjustment`] trait method carries no
|
||||
/// `NodeCore`, so this value-resolving variant is what render-time call
|
||||
/// sites (and the tests) use; the trait method documents that gap.
|
||||
pub fn input_time_adjustment_with(
|
||||
core: &NodeCore,
|
||||
input: &str,
|
||||
element: i32,
|
||||
time: TimeRange,
|
||||
traverse: bool,
|
||||
) -> TimeRange {
|
||||
let _ = (element, traverse);
|
||||
if input == INPUT_INPUT {
|
||||
TimeRange::new(
|
||||
Self::get_remapped_time(core, time.in_()),
|
||||
Self::get_remapped_time(core, time.out()),
|
||||
)
|
||||
} else {
|
||||
time
|
||||
}
|
||||
}
|
||||
|
||||
/// The full C++ `output_time_adjustment()` with an explicit core: the
|
||||
/// exact inverse of [`Self::input_time_adjustment_with`] — for
|
||||
/// `input_in`, both endpoints are shifted back by the `time_in` value.
|
||||
pub fn output_time_adjustment_with(
|
||||
core: &NodeCore,
|
||||
input: &str,
|
||||
element: i32,
|
||||
time: TimeRange,
|
||||
traverse: bool,
|
||||
) -> TimeRange {
|
||||
let _ = (element, traverse);
|
||||
if input == INPUT_INPUT {
|
||||
TimeRange::new(
|
||||
Self::get_remapped_output_time(core, time.in_()),
|
||||
Self::get_remapped_output_time(core, time.out()),
|
||||
)
|
||||
} else {
|
||||
time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeBehavior for TimeOffsetNode {
|
||||
/// Human-readable name (C++ `name()`).
|
||||
fn name(&self) -> &str {
|
||||
"Time Offset"
|
||||
}
|
||||
|
||||
/// Stable type id (C++ `id()`).
|
||||
fn type_id(&self) -> &str {
|
||||
"org.olivevideoeditor.Olive.timeoffset"
|
||||
}
|
||||
|
||||
/// Categories (C++ `category()` returns `{ k_category_time }`; the Rust
|
||||
/// `Category` enum has no `Time` variant yet, so this is empty until one
|
||||
/// is added).
|
||||
fn categories(&self) -> &[Category] {
|
||||
&[]
|
||||
}
|
||||
|
||||
/// Description (C++ `description()`).
|
||||
fn description(&self) -> &str {
|
||||
"Offset time passing through the graph."
|
||||
}
|
||||
|
||||
/// Localized input names (C++ `retranslate()`): `time_in` -> "Time",
|
||||
/// `input_in` -> "Input".
|
||||
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
||||
match id {
|
||||
TIME_INPUT => "Time",
|
||||
INPUT_INPUT => "Input",
|
||||
_ => id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Input-side time remap (C++ `input_time_adjustment()`): for
|
||||
/// `input_in`, both ends of the range are shifted forward by the current
|
||||
/// `time_in` value (C++ `get_remapped_time()`: `input + time_in`);
|
||||
/// all other inputs fall through to the base-class identity behavior.
|
||||
///
|
||||
/// The C++ evaluation reads the keyframable `time_in` input, which
|
||||
/// requires the node's data ([`NodeCore`]) — not carried by this trait
|
||||
/// signature. The exact remap is ported in
|
||||
/// [`Self::input_time_adjustment_with`] (and tested there); until the
|
||||
/// adjustment API gains core access, the identity range is returned
|
||||
/// (`// CPP-PARITY: timeoffsetnode.cpp` `input_time_adjustment`).
|
||||
fn input_time_adjustment(&self, input: &str, element: i32, time: TimeRange, traverse: bool) -> TimeRange {
|
||||
let _ = (input, element, traverse);
|
||||
time
|
||||
}
|
||||
|
||||
/// Output-side time remap (C++ `output_time_adjustment()`): the exact
|
||||
/// inverse of the input adjustment — for `input_in`, both ends of the
|
||||
/// range are shifted back by subtracting the `time_in` value (C++
|
||||
/// `get_remapped_output_time()`: `input - time_in`); all other inputs
|
||||
/// fall through to the base-class identity behavior.
|
||||
///
|
||||
/// As with the input side, the value read needs the node's data; the
|
||||
/// exact remap is ported in [`Self::output_time_adjustment_with`]
|
||||
/// (`// CPP-PARITY: timeoffsetnode.cpp` `output_time_adjustment`).
|
||||
fn output_time_adjustment(&self, input: &str, element: i32, time: TimeRange, traverse: bool) -> TimeRange {
|
||||
let _ = (input, element, traverse);
|
||||
time
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): pushes the value arriving at
|
||||
/// `input_in` through unchanged (the actual time shift happens via the
|
||||
/// time-adjustment overrides above).
|
||||
fn value(&self, core: &NodeCore, inputs: &NodeValueRow, time: Rational, table: &mut NodeValueTable) {
|
||||
let _ = (core, time);
|
||||
// `table->push(value.at(k_input_input))` — the value passes through
|
||||
// unchanged, whatever its type (texture values included).
|
||||
if let Some(v) = inputs.get(INPUT_INPUT) {
|
||||
table.push(v.value_type(), v.clone(), None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Deep copy (C++ `copy()`).
|
||||
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
||||
Some(Box::new(TimeOffsetNode))
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor (C++ `TimeOffsetNode::TimeOffsetNode()`): adds `time_in`
|
||||
/// (rational, default 0, not-connectable, time-slider view with viewlock)
|
||||
/// and `input_in` (type-none pass-through, not-keyframable).
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
let mut core = NodeCore::new();
|
||||
|
||||
let mut time_input = crate::input::Input::new(
|
||||
TIME_INPUT,
|
||||
crate::value::ValueType::Rational,
|
||||
NodeValue::Rational(Rational::new(0, 1)),
|
||||
);
|
||||
time_input.flags |= crate::input::flags::NOT_CONNECTABLE;
|
||||
time_input.properties = vec![
|
||||
("view".to_string(), NodeValue::Text("time".to_string())),
|
||||
("viewlock".to_string(), NodeValue::Boolean(true)),
|
||||
];
|
||||
core.add_input(time_input);
|
||||
|
||||
let mut input_input = crate::input::Input::new(
|
||||
INPUT_INPUT,
|
||||
crate::value::ValueType::None,
|
||||
NodeValue::None,
|
||||
);
|
||||
input_input.flags |= crate::input::flags::NOT_KEYFRAMABLE;
|
||||
core.add_input(input_input);
|
||||
|
||||
(core, Box::new(TimeOffsetNode))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory listing for
|
||||
/// `org.olivevideoeditor.Olive.timeoffset`; see the note on
|
||||
/// [`NodeBehavior::categories`] about the missing `Time` category).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.timeoffset",
|
||||
name: "Time Offset",
|
||||
categories: &[],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::node::NodeBehavior;
|
||||
use crate::value::{NodeValueTable, ValueType};
|
||||
use oakcore_rs::Rational;
|
||||
|
||||
#[test]
|
||||
fn input_names() {
|
||||
let n = TimeOffsetNode;
|
||||
assert_eq!(n.input_name(TIME_INPUT), "Time");
|
||||
assert_eq!(n.input_name(INPUT_INPUT), "Input");
|
||||
assert_eq!(n.input_name("other_in"), "other_in");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_wires_inputs() {
|
||||
let (core, behavior) = create();
|
||||
assert_eq!(behavior.type_id(), "org.olivevideoeditor.Olive.timeoffset");
|
||||
let time_in = core.get_input(TIME_INPUT).unwrap();
|
||||
assert_eq!(time_in.value_type, ValueType::Rational);
|
||||
assert_eq!(time_in.default, NodeValue::Rational(Rational::new(0, 1)));
|
||||
assert_ne!(time_in.flags & crate::input::flags::NOT_CONNECTABLE, 0);
|
||||
assert!(time_in.properties.iter().any(|(k, v)| {
|
||||
k == "view" && v == &NodeValue::Text("time".to_string())
|
||||
}));
|
||||
assert!(time_in.properties.iter().any(|(k, v)| {
|
||||
k == "viewlock" && v == &NodeValue::Boolean(true)
|
||||
}));
|
||||
let input_in = core.get_input(INPUT_INPUT).unwrap();
|
||||
assert_eq!(input_in.value_type, ValueType::None);
|
||||
assert_ne!(input_in.flags & crate::input::flags::NOT_KEYFRAMABLE, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_remapped_time_shifts_forward() {
|
||||
let (mut core, _) = create();
|
||||
core.set_standard_value(TIME_INPUT, -1, NodeValue::Rational(Rational::new(5, 1)));
|
||||
assert_eq!(
|
||||
TimeOffsetNode::get_remapped_time(&core, Rational::new(10, 1)),
|
||||
Rational::new(15, 1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_remapped_output_time_shifts_back() {
|
||||
let (mut core, _) = create();
|
||||
core.set_standard_value(TIME_INPUT, -1, NodeValue::Rational(Rational::new(5, 1)));
|
||||
assert_eq!(
|
||||
TimeOffsetNode::get_remapped_output_time(&core, Rational::new(10, 1)),
|
||||
Rational::new(5, 1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_time_adjustment_shifts_range_forward() {
|
||||
let (mut core, _) = create();
|
||||
core.set_standard_value(TIME_INPUT, -1, NodeValue::Rational(Rational::new(5, 1)));
|
||||
let r = TimeOffsetNode::input_time_adjustment_with(
|
||||
&core,
|
||||
INPUT_INPUT,
|
||||
-1,
|
||||
TimeRange::new(Rational::new(10, 1), Rational::new(20, 1)),
|
||||
true,
|
||||
);
|
||||
assert_eq!(r.in_(), Rational::new(15, 1));
|
||||
assert_eq!(r.out(), Rational::new(25, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_time_adjustment_shifts_range_back() {
|
||||
let (mut core, _) = create();
|
||||
core.set_standard_value(TIME_INPUT, -1, NodeValue::Rational(Rational::new(5, 1)));
|
||||
let r = TimeOffsetNode::output_time_adjustment_with(
|
||||
&core,
|
||||
INPUT_INPUT,
|
||||
-1,
|
||||
TimeRange::new(Rational::new(10, 1), Rational::new(20, 1)),
|
||||
true,
|
||||
);
|
||||
assert_eq!(r.in_(), Rational::new(5, 1));
|
||||
assert_eq!(r.out(), Rational::new(15, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjustments_evaluate_offset_per_endpoint() {
|
||||
let (mut core, _) = create();
|
||||
// A non-constant (keyframed) offset: each endpoint is shifted by the
|
||||
// time_in value evaluated at that endpoint.
|
||||
core.keyframe_track_mut(TIME_INPUT, -1).set_key(crate::keyframe::Keyframe {
|
||||
time: Rational::new(0, 1),
|
||||
value: NodeValue::Rational(Rational::new(1, 1)),
|
||||
interpolation: crate::keyframe::Interpolation::Linear,
|
||||
bezier_in: (0.0, 0.0),
|
||||
bezier_out: (0.0, 0.0),
|
||||
});
|
||||
core.keyframe_track_mut(TIME_INPUT, -1).set_key(crate::keyframe::Keyframe {
|
||||
time: Rational::new(20, 1),
|
||||
value: NodeValue::Rational(Rational::new(3, 1)),
|
||||
interpolation: crate::keyframe::Interpolation::Linear,
|
||||
bezier_in: (0.0, 0.0),
|
||||
bezier_out: (0.0, 0.0),
|
||||
});
|
||||
let t = TimeRange::new(Rational::new(0, 1), Rational::new(20, 1));
|
||||
let shifted = TimeOffsetNode::input_time_adjustment_with(&core, INPUT_INPUT, -1, t, true);
|
||||
// 0 + offset(0s) = 1; 20 + offset(20s) = 23.
|
||||
assert_eq!(shifted.in_(), Rational::new(1, 1));
|
||||
assert_eq!(shifted.out(), Rational::new(23, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjustments_are_mutually_inverse_for_constant_offset() {
|
||||
let (mut core, _) = create();
|
||||
core.set_standard_value(TIME_INPUT, -1, NodeValue::Rational(Rational::new(5, 1)));
|
||||
let t = TimeRange::new(Rational::new(10, 1), Rational::new(20, 1));
|
||||
let shifted = TimeOffsetNode::input_time_adjustment_with(&core, INPUT_INPUT, -1, t, true);
|
||||
assert_eq!(shifted.in_(), Rational::new(15, 1));
|
||||
assert_eq!(shifted.out(), Rational::new(25, 1));
|
||||
let unshifted = TimeOffsetNode::output_time_adjustment_with(&core, INPUT_INPUT, -1, shifted, true);
|
||||
assert_eq!(unshifted, t);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjustments_other_inputs_are_identity() {
|
||||
let (mut core, _) = create();
|
||||
core.set_standard_value(TIME_INPUT, -1, NodeValue::Rational(Rational::new(5, 1)));
|
||||
let t = TimeRange::new(Rational::new(10, 1), Rational::new(20, 1));
|
||||
assert_eq!(TimeOffsetNode::input_time_adjustment_with(&core, "other_in", -1, t, true), t);
|
||||
assert_eq!(TimeOffsetNode::output_time_adjustment_with(&core, "other_in", -1, t, true), t);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_passes_input_through() {
|
||||
let (core, behavior) = create();
|
||||
let mut row = crate::value::NodeValueRow::default();
|
||||
row.insert(INPUT_INPUT.to_string(), NodeValue::Float(42.0));
|
||||
let mut table = NodeValueTable::default();
|
||||
behavior.value(&core, &row, Rational::new(0, 1), &mut table);
|
||||
assert_eq!(table.get(ValueType::Float), Some(&NodeValue::Float(42.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_copies_node() {
|
||||
let (_core, behavior) = create();
|
||||
let copy = behavior.duplicate(&_core).unwrap();
|
||||
assert_eq!(copy.type_id(), "org.olivevideoeditor.Olive.timeoffset");
|
||||
assert_eq!(copy.name(), "Time Offset");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user