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
+645
View File
@@ -0,0 +1,645 @@
// 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/>.
//! Node core data and the behavior trait — the C++ `Node` class,
//! restructured for Rust. COVERAGE.md maps every C++ method to its
//! Rust home; this file carries the virtual surface ([`NodeBehavior`])
//! and the shared data ([`NodeCore`]).
use oakcore_rs::{Rational, TimeRange};
use crate::id::NodeId;
use crate::input::{Input, ValueHint};
use crate::keyframe::KeyframeTrack;
use crate::value::{NodeValue, NodeValueRow, NodeValueTable};
/// Node category (mirrors C++ `Node::CategoryID` order).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Category {
/// Output nodes.
Output,
/// Effects.
Effect,
/// Generators.
Generator,
/// Inputs (footage).
Input,
/// Math/combine.
Math,
/// Color.
Color,
/// Distort.
Distort,
/// Filter.
Filter,
/// Keying.
Keying,
/// OpenFX plugins.
OpenFx,
/// Timeline structural (tracks/blocks).
Timeline,
/// Groups.
Group,
}
/// A gizmo: viewport-interaction data object (C++ `NodeGizmo`). Data
/// only — drawing and mouse handling live in facade/app.
#[derive(Clone)]
pub struct Gizmo {
/// Keyframed position inputs (track references).
pub position_inputs: Vec<(String, i32, i32)>,
/// Current drag position.
pub drag_point: (f64, f64),
}
/// Shared per-node data (the C++ `Node` member fields). Behavior lives
/// in [`NodeBehavior`].
#[derive(Clone)]
pub struct NodeCore {
/// Inputs by id (array inputs hold multiple elements).
pub inputs: Vec<Input>,
/// Keyframe tracks per (input, element).
pub keyframes: Vec<(String, i32, KeyframeTrack)>,
/// Caches as oakrender handles (created via bridge::render).
pub caches: NodeCaches,
/// Node flags bitmask (hidden, dont-show-in-param-view, ...).
pub flags: u64,
/// Editor position (serialization only).
pub position: (f64, f64),
/// User label (C++ label_).
pub label: String,
/// Color override index (-1 = none).
pub override_color: i32,
/// Effect input id (C++ effect_input_).
pub effect_input: String,
/// Value hints per (input, element).
pub hints: Vec<((String, i32), ValueHint)>,
/// Group-context positions (C++ context_positions_).
pub context_positions: Vec<(NodeId, (f64, f64), bool)>,
/// Linked nodes (C++ links_).
pub links: Vec<NodeId>,
/// Bin folder membership (None = not in the bin).
pub bin_folder: Option<NodeId>,
/// Caches master toggle (C++ caches_enabled_).
pub caches_enabled: bool,
/// Gizmos owned by this node.
pub gizmos: Vec<Gizmo>,
/// Currently dragged gizmo index.
pub current_gizmo: Option<usize>,
/// Standard (non-keyframed) values keyed by (input id, element);
/// falls back to [`Input::default`] (the C++ `NodeInputImmediate`
/// `standard_value_` map — `// CPP-PARITY: inputimmediate.h`).
pub standard_values: std::collections::HashMap<(String, i32), crate::value::NodeValue>,
}
/// The always-present "enabled" input id (C++
/// `Node::k_enabled_input`, `"enabled_in"`).
pub const ENABLED_INPUT: &str = "enabled_in";
/// Node flag bits (C++ `Node::Flag` enum; values cross the C ABI and
/// project XML as ints — `// CPP-PARITY: node.h:109`).
pub mod flags {
/// `k_dont_show_in_param_view`.
pub const DONT_SHOW_IN_PARAM_VIEW: u64 = 0x1;
/// `k_video_effect`.
pub const VIDEO_EFFECT: u64 = 0x2;
/// `k_audio_effect`.
pub const AUDIO_EFFECT: u64 = 0x4;
/// `k_dont_show_in_create_menu`.
pub const DONT_SHOW_IN_CREATE_MENU: u64 = 0x8;
}
impl NodeCore {
/// Bare core with no inputs (vacant arena slots).
pub fn empty() -> NodeCore {
NodeCore {
inputs: Vec::new(),
keyframes: Vec::new(),
caches: NodeCaches::default(),
flags: 0,
position: (0.0, 0.0),
label: String::new(),
override_color: -1,
effect_input: String::new(),
hints: Vec::new(),
context_positions: Vec::new(),
links: Vec::new(),
bin_folder: None,
caches_enabled: true,
gizmos: Vec::new(),
current_gizmo: None,
standard_values: std::collections::HashMap::new(),
}
}
/// Fresh core for a node constructor: adds the standard `enabled_in`
/// boolean input (default true) first, exactly like the C++ `Node`
/// constructor (`// CPP-PARITY: node.cpp:91`).
pub fn new() -> NodeCore {
let mut core = NodeCore::empty();
core.add_input(Input::new(
ENABLED_INPUT,
crate::value::ValueType::Boolean,
crate::value::NodeValue::Boolean(true),
));
core
}
/// Append an input descriptor.
pub fn add_input(&mut self, input: Input) {
self.inputs.push(input);
}
/// Look up an input by id.
pub fn get_input(&self, id: &str) -> Option<&Input> {
self.inputs.iter().find(|i| i.id == id)
}
/// Mutable input lookup by id.
pub fn get_input_mut(&mut self, id: &str) -> Option<&mut Input> {
self.inputs.iter_mut().find(|i| i.id == id)
}
/// Index of the input in declaration order (C++ `inputs()`).
pub fn input_index(&self, id: &str) -> Option<usize> {
self.inputs.iter().position(|i| i.id == id)
}
/// True when the node declares `id` (C++ `has_input_with_id`).
pub fn has_input(&self, id: &str) -> bool {
self.inputs.iter().any(|i| i.id == id)
}
/// Remove the input `id`, if present (C++ `Node::remove_input`).
pub fn remove_input(&mut self, id: &str) -> bool {
let before = self.inputs.len();
self.inputs.retain(|i| i.id != id);
self.inputs.len() != before
}
/// Declared value type of `id` (C++ `get_input_data_type`).
pub fn input_data_type(&self, id: &str) -> Option<crate::value::ValueType> {
self.get_input(id).map(|i| i.value_type)
}
/// Flag bits of `id` (C++ `get_input_flags`).
pub fn input_flags(&self, id: &str) -> u32 {
self.get_input(id).map(|i| i.flags).unwrap_or(0)
}
/// Display name of `id` (C++ `get_input_name`, non-virtual part).
pub fn input_display_name(&self, id: &str) -> String {
self.get_input(id)
.map(|i| i.display_name.clone())
.unwrap_or_else(|| id.to_string())
}
/// Array size of `id` (0 for non-array inputs; C++ `input_array_size`).
pub fn input_array_size(&self, id: &str) -> usize {
self.get_input(id).map(|i| i.array_size).unwrap_or(0)
}
/// Grow/shrink an array input's element count, inserting/removing the
/// given element index. Per-element standard values and keyframe
/// tracks shift to keep their element mapping (C++
/// `Node::input_array_insert`/`input_array_remove`, values half).
pub fn input_array_insert(&mut self, id: &str, index: usize) {
if let Some(input) = self.get_input_mut(id) {
input.array_size += 1;
}
let size = self.input_array_size(id);
// Shift standard values and keyframe tracks up one element.
for e in (index + 1..size).rev() {
self.move_element_value(id, e - 1, e);
self.move_element_keyframes(id, e - 1, e);
}
// The freshly inserted slot carries no value or track.
self.standard_values.remove(&(id.to_string(), index as i32));
self.remove_element_keyframes(id, index);
}
/// See [`NodeCore::input_array_insert`].
pub fn input_array_remove(&mut self, id: &str, index: usize) {
let size = self.input_array_size(id);
if index >= size {
return;
}
// Drop the removed element's own value and track first (the
// shift below only overwrites targets whose source has data).
self.standard_values.remove(&(id.to_string(), index as i32));
self.remove_element_keyframes(id, index);
// Shift values/keyframes down one element, then drop the tail.
for e in index..size.saturating_sub(1) {
self.move_element_value(id, e + 1, e);
self.move_element_keyframes(id, e + 1, e);
}
self.standard_values.remove(&(id.to_string(), (size - 1) as i32));
self.remove_element_keyframes(id, size - 1);
if let Some(input) = self.get_input_mut(id) {
input.array_size = input.array_size.saturating_sub(1);
}
}
fn move_element_value(&mut self, id: &str, from: usize, to: usize) {
let key = |e: usize| (id.to_string(), e as i32);
if let Some(v) = self.standard_values.remove(&key(from)) {
self.standard_values.insert(key(to), v);
}
}
fn move_element_keyframes(&mut self, id: &str, from: usize, to: usize) {
let track = match self
.keyframes
.iter()
.position(|(i, e, _)| i == id && *e == from as i32)
{
Some(i) => self.keyframes.remove(i).2,
None => return,
};
// Replace or insert at the target element.
if let Some(slot) = self
.keyframes
.iter_mut()
.find(|(i, e, _)| i == id && *e == to as i32)
{
slot.2 = track;
} else {
self.keyframes.push((id.to_string(), to as i32, track));
}
}
fn remove_element_keyframes(&mut self, id: &str, element: usize) {
self.keyframes
.retain(|(i, e, _)| !(i == id && *e == element as i32));
}
/// The keyframe track for (input, element), if any.
pub fn keyframe_track(&self, id: &str, element: i32) -> Option<&KeyframeTrack> {
self.keyframes
.iter()
.find(|(i, e, _)| i == id && *e == element)
.map(|(_, _, t)| t)
}
/// Mutable keyframe track access, creating one on demand.
pub fn keyframe_track_mut(&mut self, id: &str, element: i32) -> &mut KeyframeTrack {
if let Some(i) = self
.keyframes
.iter()
.position(|(i, e, _)| i == id && *e == element)
{
return &mut self.keyframes[i].2;
}
self.keyframes.push((id.to_string(), element, KeyframeTrack::default()));
let last = self.keyframes.len() - 1;
&mut self.keyframes[last].2
}
/// Standard (non-keyframed) value of (input, element): the per-element
/// override or the input's default (C++ `get_standard_value`).
pub fn standard_value(&self, id: &str, element: i32) -> crate::value::NodeValue {
self.standard_values
.get(&(id.to_string(), element))
.cloned()
.or_else(|| self.get_input(id).map(|i| i.default.clone()))
.unwrap_or(crate::value::NodeValue::None)
}
/// Set the standard value of (input, element) (C++ `set_standard_value`).
pub fn set_standard_value(
&mut self,
id: &str,
element: i32,
value: crate::value::NodeValue,
) {
self.standard_values.insert((id.to_string(), element), value);
}
/// Value of `input` at `time`: keyframes when the (input, element)
/// track is non-empty, else the standard value (C++
/// `get_value_at_time`; `// CPP-PARITY: node.cpp:465`).
pub fn value_at_time(&self, id: &str, element: i32, time: oakcore_rs::Rational) -> crate::value::NodeValue {
match self.keyframe_track(id, element) {
Some(track) if !track.keys().is_empty() => track
.value_at(time)
.unwrap_or_else(|| self.standard_value(id, element)),
_ => self.standard_value(id, element),
}
}
/// Whether (input, element) is being keyframed (C++
/// `Node::is_input_keyframing`): the keyframe track exists and is
/// non-empty.
pub fn is_input_keyframing(&self, id: &str, element: i32) -> bool {
self.keyframe_track(id, element)
.map(|t| !t.keys().is_empty())
.unwrap_or(false)
}
/// Whether (input, element) is static at evaluation time (C++
/// `Node::is_input_static`): neither connected nor keyframed.
/// `inputs` is the render-time input row — a connected input appears
/// in the row under its id.
pub fn is_input_static(&self, inputs: &crate::value::NodeValueRow, id: &str, element: i32) -> bool {
!inputs.contains_key(id) && !self.is_input_keyframing(id, element)
}
/// Set the value hint for (input, element) (C++ `set_value_hint_for_input`).
pub fn set_value_hint(&mut self, id: &str, element: i32, hint: ValueHint) {
if let Some(slot) = self.hints.iter_mut().find(|((i, e), _)| i == id && *e == element) {
slot.1 = hint;
} else {
self.hints.push(((id.to_string(), element), hint));
}
}
/// The value hint for (input, element), if any.
pub fn value_hint(&self, id: &str, element: i32) -> Option<&ValueHint> {
self.hints
.iter()
.find(|((i, e), _)| i == id && *e == element)
.map(|(_, h)| h)
}
/// True when `context` appears in this node's context-position map
/// (C++ `context_contains_node`).
pub fn context_contains(&self, context: NodeId) -> bool {
self.context_positions.iter().any(|(c, _, _)| *c == context)
}
/// Set this node's position in `context` (C++
/// `set_node_position_in_context`). Returns true when newly added.
pub fn set_context_position(&mut self, context: NodeId, x: f64, y: f64, expanded: bool) -> bool {
let added = !self.context_contains(context);
if let Some(slot) = self.context_positions.iter_mut().find(|(c, _, _)| *c == context) {
slot.1 = (x, y);
slot.2 = expanded;
} else {
self.context_positions.push((context, (x, y), expanded));
}
added
}
/// Remove this node from `context`; false when absent (C++
/// `remove_node_from_context`).
pub fn remove_from_context(&mut self, context: NodeId) -> bool {
let before = self.context_positions.len();
self.context_positions.retain(|(c, _, _)| *c != context);
self.context_positions.len() != before
}
}
/// The node's oakrender caches (frame/thumbnail/audio/waveform),
/// owned handles released with the node.
#[derive(Clone)]
pub struct NodeCaches {
/// Video frame hash cache.
pub video: crate::bridge::render::CacheHandle,
/// Thumbnail cache.
pub thumbnail: crate::bridge::render::CacheHandle,
/// Audio playback cache.
pub audio: crate::bridge::render::CacheHandle,
/// Waveform cache.
pub waveform: crate::bridge::render::CacheHandle,
}
impl Default for NodeCaches {
/// All empty handles (caches are created lazily through bridge::render
/// when a node enters a project; `// CPP-PARITY: node.cpp:102`).
fn default() -> Self {
NodeCaches {
video: crate::handle::CHandle::null(),
thumbnail: crate::handle::CHandle::null(),
audio: crate::handle::CHandle::null(),
waveform: crate::handle::CHandle::null(),
}
}
}
/// The polymorphic surface of a node — every C++ virtual on `Node`
/// becomes a method here (see COVERAGE.md §1/§7/§8/§10).
pub trait NodeBehavior: Send {
/// Human-readable name (C++ `name()`).
fn name(&self) -> &str;
/// Short menu name (C++ `short_name()`; defaults to [`Self::name`]).
fn short_name(&self) -> &str {
self.name()
}
/// Stable type id (C++ `id()`).
fn type_id(&self) -> &str;
/// Categories (C++ `category()`).
fn categories(&self) -> &[Category] {
&[]
}
/// Sub-category (C++ `sub_category()`).
fn sub_category(&self) -> &str {
""
}
/// Description (C++ `description()`).
fn description(&self) -> &str {
""
}
/// Localized input name (C++ `get_input_name()` virtual).
fn input_name<'a>(&self, id: &'a str) -> &'a str {
// The standard enabled input displays as "Enabled" on every node
// (`// CPP-PARITY: node.cpp:155` retranslate).
if id == crate::node::ENABLED_INPUT {
"Enabled"
} else {
id
}
}
/// Inputs excluded from rendering (C++ `ignore_inputs_for_rendering()`).
fn ignore_inputs_for_rendering(&self) -> &[String] {
&[]
}
/// Array elements active at `time` (C++ `get_active_elements_at_time()`).
fn active_elements_at_time(&self, input: &str, time: Rational) -> Vec<i32> {
let _ = (input, time);
Vec::new()
}
/// Cache ranges (C++ `get_video_cache_range()` /
/// `get_audio_cache_range()`).
fn video_cache_range(&self, core: &NodeCore) -> TimeRange {
let _ = core;
TimeRange::default()
}
/// Audio cache range.
fn audio_cache_range(&self, core: &NodeCore) -> TimeRange {
let _ = core;
TimeRange::default()
}
/// Value hint for an input (C++ `get_value_hint_for_input()` virtual).
fn value_hint_for_input(&self, input: &str) -> Option<ValueHint> {
let _ = input;
None
}
/// Render-time connection resolution (C++
/// `get_connected_render_output()`; Group overrides).
fn connected_render_output(&self, core: &NodeCore, input: &str, element: i32) -> Option<NodeId> {
let _ = (core, input, element);
None
}
/// Time adjustment through this node (C++
/// `input_time_adjustment()`/`output_time_adjustment()`; clips
/// override for speed/reverse).
fn input_time_adjustment(&self, input: &str, element: i32, time: TimeRange, traverse: bool) -> TimeRange {
let _ = (input, element, traverse);
time
}
/// Output-side 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()`).
fn value(&self, core: &NodeCore, inputs: &NodeValueRow, time: Rational, table: &mut NodeValueTable) {
let _ = (core, inputs, time, table);
}
/// Process a span of samples (C++ `process_samples()`).
fn process_samples(
&self,
core: &NodeCore,
inputs: &NodeValueRow,
range: TimeRange,
output: &mut crate::value::SampleBuffer,
) {
let _ = (core, inputs, range, output);
}
/// Direct frame generation (C++ `generate_frame()`; CPU-render
/// nodes).
fn generate_frame(&self, core: &NodeCore, frame: &mut crate::bridge::render::TextureHandle, time: Rational) {
let _ = (core, frame, time);
}
/// Shader code request (C++ `get_shader_code()`; GPU nodes).
fn shader_code(&self, request: &str) -> Option<String> {
let _ = request;
None
}
/// Gizmo transform/positions (C++ `gizmo_transformation()` /
/// `update_gizmo_positions()`).
fn gizmo_update(&self, core: &NodeCore, row: &NodeValueRow) {
let _ = (core, row);
}
/// Gizmo drag callbacks (C++ `gizmo_drag_start/move`).
fn gizmo_drag(&mut self, core: &mut NodeCore, start: bool, x: f64, y: f64, modifiers: u32) {
let _ = (core, start, x, y, modifiers);
}
/// Input value changed (C++ `InputValueChangedEvent`).
fn input_value_changed(&mut self, core: &mut NodeCore, input: &str, element: i32) {
let _ = (core, input, element);
}
/// Edge connected to an input (C++ `InputConnectedEvent`).
fn input_connected(&mut self, core: &mut NodeCore, input: &str, element: i32, source: NodeId) {
let _ = (core, input, element, source);
}
/// Edge disconnected from an input (C++ `InputDisconnectedEvent`).
fn input_disconnected(&mut self, core: &mut NodeCore, input: &str, element: i32, source: NodeId) {
let _ = (core, input, element, source);
}
/// Someone connected to this node's output (C++
/// `OutputConnectedEvent`).
fn output_connected(&mut self, core: &mut NodeCore, target: NodeId, input: &str, element: i32) {
let _ = (core, target, input, element);
}
/// Output disconnected (C++ `OutputDisconnectedEvent`).
fn output_disconnected(&mut self, core: &mut NodeCore, target: NodeId, input: &str, element: i32) {
let _ = (core, target, input, element);
}
/// Attached to a preview/viewer (C++ `ConnectedToPreviewEvent`).
fn connected_to_preview(&mut self, core: &mut NodeCore) {
let _ = core;
}
/// Inserted into / removed from a project graph (C++
/// `AddedToGraphEvent` / `RemovedFromGraphEvent`).
fn added_to_graph(&mut self, core: &mut NodeCore) {
let _ = core;
}
/// See [`NodeBehavior::added_to_graph`].
fn removed_from_graph(&mut self, core: &mut NodeCore) {
let _ = core;
}
/// Node links changed (C++ `LinkChangeEvent`).
fn link_changed(&mut self, core: &mut NodeCore) {
let _ = core;
}
/// Deep copy (C++ `copy()`); None = not copiable.
fn duplicate(&self, core: &NodeCore) -> Option<Box<dyn NodeBehavior>>;
/// Custom load/save (C++ `load_custom()`/`save_custom()`).
fn load_custom(&mut self, core: &mut NodeCore, reader: &mut dyn crate::serializer::XmlRead) -> bool {
let _ = (core, reader);
true
}
/// See [`NodeBehavior::load_custom`].
fn save_custom(&self, core: &NodeCore, writer: &mut dyn crate::serializer::XmlWrite) {
let _ = (core, writer);
}
/// Post-load fixups (C++ `PostLoadEvent` / `LoadFinishedEvent`).
fn post_load(&mut self, core: &mut NodeCore) {
let _ = core;
}
/// Legacy input id mapping for old project versions (C++
/// `get_input_id_for_legacy_id()`; default identity).
fn map_legacy_input_id<'a>(&self, id: &'a str) -> &'a str {
id
}
/// Downcast to the concrete behavior (used by the timeline families
/// to reach `FolderBehavior`/`TrackBehavior`/`SequenceBehavior`
/// state). Default `None`; concrete behaviors override.
fn as_any(&self) -> Option<&dyn std::any::Any> {
None
}
/// Mutable downcast (see [`NodeBehavior::as_any`]).
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
None
}
}