refactor: drop internal bridge/ffi layers; exporter family lands

Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are
gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/
oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain
Rust, CHandle marshalling shrinks to the oakengine boundary, and tests
call the Rust APIs directly (pure C-ABI wrapper tests removed where
the domain layer already covers the behavior).

exporter.h family implemented: oakengine_export_render (CLI contract),
oakengine_export_render_with_params (was a stub), last_error and
progress callback; synchronous path reuses task_create_export +
start_sync. Fixes on the way: oaktask video ticket self-deadlock,
audio params dropped on the export path, codec encoder AAC slicing and
H.264 time base. Real-mp4 tests cover both entry points, progress and
the illegal-argument matrix.

Also: oakstorage session maps null project handles to None (version-
info path), configstore test double literal 3.14 -> 3.15 (clippy PI
lint), oakaudio output callback scratch buffer + env-aware P1 test,
cli media round-trip test uses a generated 16-frame clip (no more
minute-long debug runs).
This commit is contained in:
2026-08-16 00:33:45 +08:00
parent 2248be8567
commit ab1a2e9c7b
293 changed files with 27826 additions and 90128 deletions
-306
View File
@@ -1,306 +0,0 @@
// 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/>.
// 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/>.
//! oakcommon C ABI imports (XML reader/writer + config), mirroring
//! `include/common/xmlutils.h` and `include/common/config.h`.
use std::ffi::{c_char, c_int};
use crate::handle::CHandle;
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_reader_init(data: *const c_char) -> CHandle {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_reader_init(data)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_init(data) }
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_reader_free(reader: *mut CHandle) {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_reader_free(reader)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_free(reader) }
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_reader_skip_current_element(reader: CHandle) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_reader_skip_current_element(reader)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_skip_current_element(reader) }
}
}
/// Direct call into the `oakcommon` crate (single-lib unification). The
/// real ABI advances the reader and writes a 1/0 `found` flag (the name is
/// read with [`oakcommon_xml_reader_name`]).
pub fn oakcommon_xml_reader_read_next_start_element(reader: CHandle, found: *mut c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_reader_read_next_start_element(reader, found)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe {
oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_next_start_element(reader, found)
}
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_reader_name(reader: CHandle, name: *mut c_char, buf_size: c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_reader_name(reader, name, buf_size)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_name(reader, name, buf_size) }
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_reader_read_element_text(
reader: CHandle,
text: *mut c_char,
buf_size: c_int,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_reader_read_element_text(reader, text, buf_size)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe {
oakcommon::ffi::xmlutils::oakcommon_xml_reader_read_element_text(reader, text, buf_size)
}
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_reader_attribute_count(reader: CHandle, count: *mut c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_reader_attribute_count(reader, count)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_count(reader, count) }
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_reader_attribute_name(
reader: CHandle,
index: c_int,
name: *mut c_char,
buf_size: c_int,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_reader_attribute_name(reader, index, name, buf_size)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe {
oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_name(
reader, index, name, buf_size,
)
}
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_reader_attribute_value(
reader: CHandle,
index: c_int,
value: *mut c_char,
buf_size: c_int,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_reader_attribute_value(reader, index, value, buf_size)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe {
oakcommon::ffi::xmlutils::oakcommon_xml_reader_attribute_value(
reader, index, value, buf_size,
)
}
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_reader_has_error(reader: CHandle, has_error: *mut c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_reader_has_error(reader, has_error)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_reader_has_error(reader, has_error) }
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_writer_init() -> CHandle {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_writer_init()
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_init() }
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_writer_free(writer: *mut CHandle) {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_writer_free(writer)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_free(writer) }
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_writer_write_start_element(writer: CHandle, name: *const c_char) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_writer_write_start_element(writer, name)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_start_element(writer, name) }
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_writer_write_end_element(writer: CHandle) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_writer_write_end_element(writer)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_element(writer) }
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_writer_write_end_document(writer: CHandle) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_writer_write_end_document(writer)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_end_document(writer) }
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_writer_write_attribute(
writer: CHandle,
key: *const c_char,
value: *const c_char,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_writer_write_attribute(writer, key, value)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe {
oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_attribute(writer, key, value)
}
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_writer_write_characters(writer: CHandle, text: *const c_char) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_writer_write_characters(writer, text)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_characters(writer, text) }
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_xml_writer_write_text_element(
writer: CHandle,
name: *const c_char,
text: *const c_char,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_xml_writer_write_text_element(writer, name, text)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe {
oakcommon::ffi::xmlutils::oakcommon_xml_writer_write_text_element(writer, name, text)
}
}
}
/// Direct call into the `oakcommon` crate (single-lib unification).
pub fn oakcommon_config_get_int(group: *const c_char, key: *const c_char, default: c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakcommon_config_get_int(group, key, default)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakcommon::ffi::config::oakcommon_config_get_int(group, key, default) }
}
}
-34
View File
@@ -1,34 +0,0 @@
// 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/>.
//! C ABI imports from other oak modules. Signatures mirror the public
//! headers verbatim; they are resolved at link time against the oaknode,
//! oakundo and oakcommon DLLs.
//!
//! Timeline never reimplements C++ internals: every cross-module access
//! goes through these frozen C ABIs (see README.md "no replication").
pub mod common;
pub mod node;
pub mod undo;
/// In-crate mock implementations of the bridge surface, compiled for tests
/// (`cfg(test)` or the `test-stubs` feature); the bridge fns route to them
/// via their `#[cfg(any(test, feature = "test-stubs"))]` variants. Plain
/// Rust (no `#[no_mangle]`), so they coexist with the real
/// oaknode/oakundo/oakcommon rlibs linked in the same test binary.
#[cfg(any(test, feature = "test-stubs"))]
pub mod teststubs;
-856
View File
@@ -1,856 +0,0 @@
// 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/>.
// 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/>.
//! oaknode C ABI imports (`include/node/{block,node,sequence,track,project}.h`).
//! Timeline reads and mutates the block/track/sequence graph exclusively
//! through these frozen functions — it never reimplements node internals.
//!
//! All time quantities cross the C ABI as `int` numerator/denominator pairs
//! (the ABI is `int`; the crate's internal `Rational` is `i64`). Converting
//! between the two is the bridge's responsibility.
use std::ffi::{c_char, c_int};
use crate::handle::CHandle;
/// A timeline block (`OakNodeBlock`) — a clip or a gap. Opaque handle.
pub type OakNodeBlock = CHandle;
/// A track (`OakNodeTrack`). Opaque handle.
pub type OakNodeTrack = CHandle;
/// A sequence/timeline (`OakNodeSequence`). Opaque handle.
pub type OakNodeSequence = CHandle;
/// A generic node (`OakNodeNode`). Opaque handle.
pub type OakNodeNode = CHandle;
/// A project (`OakNodeProject`). Opaque handle.
pub type OakNodeProject = CHandle;
/// A track list (`OakNodeTrackList`). Opaque handle.
pub type OakNodeTrackList = CHandle;
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_clip_create() -> OakNodeBlock {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_clip_create()
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_clip_create() }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_gap_create() -> OakNodeBlock {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_gap_create()
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_gap_create() }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_as_node(block: OakNodeBlock) -> OakNodeNode {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_as_node(block)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_as_node(block) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_get_in(
block: OakNodeBlock,
numerator: *mut c_int,
denominator: *mut c_int,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_get_in(block, numerator, denominator)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_get_in(block, numerator, denominator) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_get_out(
block: OakNodeBlock,
numerator: *mut c_int,
denominator: *mut c_int,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_get_out(block, numerator, denominator)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_get_out(block, numerator, denominator) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_get_length(
block: OakNodeBlock,
numerator: *mut c_int,
denominator: *mut c_int,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_get_length(block, numerator, denominator)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_get_length(block, numerator, denominator) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_set_length_and_media_out(
block: OakNodeBlock,
numerator: c_int,
denominator: c_int,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_set_length_and_media_out(block, numerator, denominator)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe {
oaknode::ffi::block::oaknode_block_set_length_and_media_out(
block,
numerator,
denominator,
)
}
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_set_length_and_media_in(
block: OakNodeBlock,
numerator: c_int,
denominator: c_int,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_set_length_and_media_in(block, numerator, denominator)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe {
oaknode::ffi::block::oaknode_block_set_length_and_media_in(
block,
numerator,
denominator,
)
}
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_set_in(block: OakNodeBlock, numerator: c_int, denominator: c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_set_in(block, numerator, denominator)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_set_in(block, numerator, denominator) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_get_enabled(block: OakNodeBlock, enabled: *mut c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_get_enabled(block, enabled)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_get_enabled(block, enabled) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_set_enabled(block: OakNodeBlock, enabled: c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_set_enabled(block, enabled)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_set_enabled(block, enabled) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_get_previous(block: OakNodeBlock, out: *mut OakNodeBlock) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_get_previous(block, out)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_get_previous(block, out) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_get_next(block: OakNodeBlock, out: *mut OakNodeBlock) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_get_next(block, out)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_get_next(block, out) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_get_track(block: OakNodeBlock, out: *mut OakNodeTrack) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_get_track(block, out)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_get_track(block, out) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_link(a: OakNodeBlock, b: OakNodeBlock) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_link(a, b)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_link(a, b) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_unlink(a: OakNodeBlock, b: OakNodeBlock) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_unlink(a, b)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_unlink(a, b) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_get_length(
track: OakNodeTrack,
numerator: *mut c_int,
denominator: *mut c_int,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_track_get_length(track, numerator, denominator)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_track_get_length(track, numerator, denominator) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_get_sequence(track: OakNodeTrack, out: *mut OakNodeSequence) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_track_get_sequence(track, out)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_track_get_sequence(track, out) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_prepend_block(track: OakNodeTrack, block: OakNodeBlock) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_track_prepend_block(track, block)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_track_prepend_block(track, block) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_insert_block_after(
track: OakNodeTrack,
block: OakNodeBlock,
before: OakNodeBlock,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_track_insert_block_after(track, block, before)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_track_insert_block_after(track, block, before) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_ripple_remove_block(track: OakNodeTrack, block: OakNodeBlock) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_track_ripple_remove_block(track, block)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_track_ripple_remove_block(track, block) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_replace_block(
track: OakNodeTrack,
old_block: OakNodeBlock,
new_block: OakNodeBlock,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_track_replace_block(track, old_block, new_block)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_track_replace_block(track, old_block, new_block) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_as_node(sequence: OakNodeSequence) -> OakNodeNode {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_sequence_as_node(sequence)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::sequence::oaknode_sequence_as_node(sequence) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_from_node(node: OakNodeNode) -> OakNodeSequence {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_sequence_from_node(node)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::sequence::oaknode_sequence_from_node(node) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_get_project(node: OakNodeNode, out: *mut OakNodeProject) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_node_get_project(node, out)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::node::oaknode_node_get_project(node, out) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_output_connection_count(node: OakNodeNode, out_count: *mut c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_node_output_connection_count(node, out_count)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::node::oaknode_node_output_connection_count(node, out_count) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_get_markers(node: OakNodeNode, out: *mut crate::handle::CHandle) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_node_get_markers(node, out)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::node::oaknode_node_get_markers(node, out as *mut std::ffi::c_void) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_get_work_area(node: OakNodeNode, out: *mut crate::handle::CHandle) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_node_get_work_area(node, out)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe {
oaknode::ffi::node::oaknode_node_get_work_area(node, out as *mut std::ffi::c_void)
}
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_add_node(project: OakNodeProject, node: OakNodeNode) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_project_add_node(project, node)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::project::oaknode_project_add_node(project, node) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_project_remove_node(project: OakNodeProject, node: OakNodeNode) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_project_remove_node(project, node)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::project::oaknode_project_remove_node(project, node) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_command_create_remove_node(node: OakNodeNode) -> crate::handle::CHandle {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_command_create_remove_node(node)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::node::oaknode_command_create_remove_node(node) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_get_kind(block: OakNodeBlock, out_kind: *mut c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_get_kind(block, out_kind)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_get_kind(block, out_kind) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_from_node(node: OakNodeNode) -> OakNodeBlock {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_from_node(node)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_from_node(node) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_block_are_linked(a: OakNodeBlock, b: OakNodeBlock, linked: *mut c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_block_are_linked(a, b, linked)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_block_are_linked(a, b, linked) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_clip_add_cache_passthrough_from(clip: OakNodeBlock, other: OakNodeBlock) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_clip_add_cache_passthrough_from(clip, other)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_clip_add_cache_passthrough_from(clip, other) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_clip_get_media_in(
clip: OakNodeBlock,
numerator: *mut c_int,
denominator: *mut c_int,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_clip_get_media_in(clip, numerator, denominator)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_clip_get_media_in(clip, numerator, denominator) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_clip_set_media_in(
clip: OakNodeBlock,
numerator: c_int,
denominator: c_int,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_clip_set_media_in(clip, numerator, denominator)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::block::oaknode_clip_set_media_in(clip, numerator, denominator) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_create(kind: c_int) -> OakNodeTrack {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_track_create(kind)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_track_create(kind) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_get_locked(track: OakNodeTrack, locked: *mut c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_track_get_locked(track, locked)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_track_get_locked(track, locked) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_set_locked(track: OakNodeTrack, locked: c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_track_set_locked(track, locked)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_track_set_locked(track, locked) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_get_block_count(track: OakNodeTrack, count: *mut c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_track_get_block_count(track, count)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_track_get_block_count(track, count) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_get_block_at(
track: OakNodeTrack,
index: c_int,
out: *mut OakNodeBlock,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_track_get_block_at(track, index, out)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_track_get_block_at(track, index, out) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_get_block_containing_time(
track: OakNodeTrack,
numerator: c_int,
denominator: c_int,
out: *mut OakNodeBlock,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_track_get_block_containing_time(
track,
numerator,
denominator,
out,
)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe {
oaknode::ffi::track::oaknode_track_get_block_containing_time(
track,
numerator,
denominator,
out,
)
}
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_get_nearest_block_before_or_at(
track: OakNodeTrack,
numerator: c_int,
denominator: c_int,
out: *mut OakNodeBlock,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_track_get_nearest_block_before_or_at(
track,
numerator,
denominator,
out,
)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe {
oaknode::ffi::track::oaknode_track_get_nearest_block_before_or_at(
track,
numerator,
denominator,
out,
)
}
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_track_get_nearest_block_after_or_at(
track: OakNodeTrack,
numerator: c_int,
denominator: c_int,
out: *mut OakNodeBlock,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_track_get_nearest_block_after_or_at(
track,
numerator,
denominator,
out,
)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe {
oaknode::ffi::track::oaknode_track_get_nearest_block_after_or_at(
track,
numerator,
denominator,
out,
)
}
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_get_type(list: OakNodeTrackList, kind: *mut c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_tracklist_get_type(list, kind)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_tracklist_get_type(list, kind) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_get_track_count(list: OakNodeTrackList, count: *mut c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_tracklist_get_track_count(list, count)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_tracklist_get_track_count(list, count) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_get_track_at(
list: OakNodeTrackList,
index: c_int,
out: *mut OakNodeTrack,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_tracklist_get_track_at(list, index, out)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_tracklist_get_track_at(list, index, out) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_array_append(list: OakNodeTrackList) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_tracklist_array_append(list)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_tracklist_array_append(list) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_tracklist_array_remove_last(list: OakNodeTrackList) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_tracklist_array_remove_last(list)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::track::oaknode_tracklist_array_remove_last(list) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_track_list(
sequence: OakNodeSequence,
kind: c_int,
out: *mut OakNodeTrackList,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_sequence_get_track_list(sequence, kind, out)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_track_list(sequence, kind, out) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_all_track_count(sequence: OakNodeSequence, count: *mut c_int) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_sequence_get_all_track_count(sequence, count)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_all_track_count(sequence, count) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_sequence_get_all_track_at(
sequence: OakNodeSequence,
index: c_int,
out: *mut OakNodeTrack,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_sequence_get_all_track_at(sequence, index, out)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::sequence::oaknode_sequence_get_all_track_at(sequence, index, out) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_connect(
output_node: OakNodeNode,
input_node: OakNodeNode,
input_id: *const c_char,
) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_node_connect(output_node, input_node, input_id)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::node::oaknode_node_connect(output_node, input_node, input_id) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_disconnect(input_node: OakNodeNode, input_id: *const c_char) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_node_disconnect(input_node, input_id)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::node::oaknode_node_disconnect(input_node, input_id) }
}
}
/// Direct call into the `oaknode` crate (single-lib unification).
pub fn oaknode_node_copy_in_graph(
node: OakNodeNode,
out_command: *mut crate::handle::CHandle,
) -> OakNodeNode {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oaknode_node_copy_in_graph(node, out_command)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oaknode::ffi::node::oaknode_node_copy_in_graph(node, out_command) }
}
}
File diff suppressed because it is too large Load Diff
-133
View File
@@ -1,133 +0,0 @@
// 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/>.
// 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/>.
//! oakundo C ABI imports (`include/undo/undocommand.h`). Undo commands are
//! created through the C ABI vtable (`oakundo_command_init` with Rust
//! closures as `userdata`) — timeline does not subclass the C++
//! `UndoCommand`; every command struct in this crate exposes `to_command()`
//! that wraps it this way.
use std::ffi::{c_char, c_int, c_void};
use crate::handle::CHandle;
/// `OakUndoCommandVtable` — callback table backing a caller-defined undo
/// command (undocommand.h). Any callback may be NULL (a NULL redo/undo makes
/// that direction a no-op); `free_fn` runs when the command is destroyed.
/// Single-lib unification: aliases the oakundo crate's vtable POD.
pub type OakUndoCommandVtable = oakundo::undocommand::OakUndoCommandVtable;
/// Direct call into the `oakundo` crate (single-lib unification).
pub fn oakundo_command_init(vtable: *const OakUndoCommandVtable, userdata: *mut c_void) -> CHandle {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakundo_command_init(vtable, userdata)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakundo::ffi::command::oakundo_command_init(vtable, userdata) }
}
}
/// Direct call into the `oakundo` crate (single-lib unification).
pub fn oakundo_command_init_multi() -> CHandle {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakundo_command_init_multi()
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakundo::ffi::command::oakundo_command_init_multi() }
}
}
/// Direct call into the `oakundo` crate (single-lib unification).
pub fn oakundo_command_multi_add_child(multi: CHandle, child: CHandle) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakundo_command_multi_add_child(multi, child)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakundo::ffi::command::oakundo_command_multi_add_child(multi, child) }
}
}
/// Direct call into the `oakundo` crate (single-lib unification).
pub fn oakundo_command_redo_now(command: CHandle) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakundo_command_redo_now(command)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakundo::ffi::command::oakundo_command_redo_now(command) }
}
}
/// Direct call into the `oakundo` crate (single-lib unification).
pub fn oakundo_command_undo_now(command: CHandle) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakundo_command_undo_now(command)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakundo::ffi::command::oakundo_command_undo_now(command) }
}
}
/// Direct call into the `oakundo` crate (single-lib unification).
pub fn oakundo_command_free(command: *mut CHandle) {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakundo_command_free(command)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakundo::ffi::command::oakundo_command_free(command) }
}
}
/// Direct call into the `oakundo` crate (single-lib unification; the C ABI
/// name is `oakundo_undostack_push` — kept as `oakundo_stack_push` here for
/// source compatibility).
pub fn oakundo_stack_push(stack: CHandle, command: CHandle, text: *const c_char) -> c_int {
#[cfg(any(test, feature = "test-stubs"))]
{
super::teststubs::oakundo_stack_push(stack, command, text)
}
#[cfg(not(any(test, feature = "test-stubs")))]
{
unsafe { oakundo::ffi::undostack::oakundo_undostack_push(stack, command, text) }
}
}
+7 -6
View File
@@ -19,8 +19,8 @@
//!
//! Enum discriminants are value-compatible with both `include/timeline/edit.h`
//! (`OAKTIMELINE_MOVEMENT_*`) and `include/timeline/displaymode.h`
//! (`OAK_TIMELINE_THUMBNAIL_*` / `OAK_TIMELINE_WAVEFORMS_*`); `ffi.rs` maps
//! between them mechanically.
//! (`OAK_TIMELINE_THUMBNAIL_*` / `OAK_TIMELINE_WAVEFORMS_*`); the deleted C
//! ABI export layer used to map between them mechanically.
use oakcore_rs::Rational;
@@ -92,13 +92,14 @@ pub enum WaveformMode {
}
/// `Timeline::EditToInfo` (timelinecommon.h): where the nearest block edge
/// falls for a pointer edit. Not `Copy`/`Clone`/`Debug` — it owns
/// [`crate::handle::CHandle`]s, which are opaque refcounted handles.
/// falls for a pointer edit. The node references are the single-lib
/// domain handles ([`crate::util::NodeRef`] — `None` where the C ABI
/// used a null handle).
pub struct EditToInfo {
/// Owning track of the nearest block.
pub track: crate::handle::CHandle,
pub track: Option<crate::util::NodeRef>,
/// Nearest edit point time.
pub nearest_time: Rational,
/// Nearest block.
pub nearest_block: crate::handle::CHandle,
pub nearest_block: Option<crate::util::NodeRef>,
}
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -41,8 +41,7 @@ pub struct RefBox<T: ?Sized> {
/// The shared ABI value-handle type (single-lib unification, see
/// `docs/zh/plans/riir/single-lib.md`): one canonical
/// `{ctx, addref, release, abi_version}` type in `oakcore-rs`, re-exported
/// here so the crate's `ffi.rs` signatures and handle scaffolding stay
/// source-compatible.
/// here so the crate's handle scaffolding stays source-compatible.
pub use oakcore_rs::handle::CHandle;
/// Generic `addref` implementation: increments the box's reference count.
+7 -7
View File
@@ -21,20 +21,20 @@
//! architectural mapping (per-header domain modules, no UndoCommand
//! inheritance → vtable commands).
//!
//! ## FFI discipline
//! ## Single-lib unification
//!
//! Identical to the oaknode crate: every export goes through
//! [`handle::guard*`], handles are opaque refcounted boxes, and all
//! cross-module access goes through the oaknode/oakundo/oakcommon C ABIs
//! (`bridge::*`).
//! The C ABI export layer (`ffi.rs`) and the oaknode/oakundo/oakcommon
//! bridge (`bridge/`) were deleted in the single-lib unification: undo
//! commands are now `oakundo::undocommand::UndoCommand` values and every
//! node/block/track reference is a [`util::NodeRef`] — an
//! `Arc<Mutex<oaknode::project::Project>>` + `oaknode::id::NodeId` pair
//! that the commands manipulate through the oaknode Rust domain directly.
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs)]
pub mod bridge;
pub mod common;
pub mod error;
pub mod ffi;
pub mod handle;
pub mod marker;
pub mod undocommon;
+7 -6
View File
@@ -26,6 +26,7 @@
//! order via `TimelineMarkerList::resort`/`resort_at`.
use oakcore_rs::{Rational, TimeRange};
use oakundo::undocommand::UndoCommand;
use crate::handle::{get, get_mut};
use crate::undocommon::{box_command, Command};
@@ -232,7 +233,7 @@ fn rational_abs(r: Rational) -> Rational {
// ---------------------------------------------------------------------------
// Marker undo commands. Each struct exposes prepare()/redo()/undo(); the
// FFI layer wraps it through bridge::undo's vtable (to_command()).
// undo stack wraps it through undocommon's vtable (to_command()).
// ---------------------------------------------------------------------------
/// `MarkerAddCommand` (timelinemarker.h).
@@ -302,7 +303,7 @@ impl MarkerAddCommand {
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> crate::handle::CHandle {
pub fn to_command(self) -> UndoCommand {
box_command(self)
}
}
@@ -369,7 +370,7 @@ impl MarkerRemoveCommand {
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> crate::handle::CHandle {
pub fn to_command(self) -> UndoCommand {
box_command(self)
}
}
@@ -437,7 +438,7 @@ impl MarkerChangeColorCommand {
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> crate::handle::CHandle {
pub fn to_command(self) -> UndoCommand {
box_command(self)
}
}
@@ -505,7 +506,7 @@ impl MarkerChangeNameCommand {
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> crate::handle::CHandle {
pub fn to_command(self) -> UndoCommand {
box_command(self)
}
}
@@ -576,7 +577,7 @@ impl MarkerChangeTimeCommand {
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> crate::handle::CHandle {
pub fn to_command(self) -> UndoCommand {
box_command(self)
}
}
+120 -80
View File
@@ -14,72 +14,122 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Shared node-removal helpers and the C-command wrapper
//! (`src/timeline/src/timelineundocommon.h`). All node-graph access goes
//! through the oaknode C ABI (`bridge::node`) and undo through
//! `bridge::undo`.
//! Shared node-removal helpers and the command wrapper
//! (`src/timeline/src/timelineundocommon.h`). Since the single-lib
//! unification, commands are `oakundo::undocommand::UndoCommand` values
//! (the oakundo C ABI and the oaknode C ABI are both gone); node-graph
//! access goes directly through the oaknode Rust domain
//! ([`crate::util::NodeRef`] + the project's graph arena).
//!
//! `CHandleCommandWrapper` in C++ subclasses `olive::UndoCommand` to wrap a
//! raw `OakUndoCommand`; the Rust equivalent holds a command `CHandle` and
//! forwards `redo`/`undo` to `bridge::undo::oakundo_command_redo_now/undo_now`.
//! raw `OakUndoCommand`; the Rust equivalent holds an
//! [`oakundo::undocommand::UndoCommand`] and forwards `redo`/`undo` to
//! `redo_now`/`undo_now`.
use std::ffi::c_void;
use crate::bridge::node::{
oaknode_block_as_node, oaknode_command_create_remove_node, oaknode_node_output_connection_count,
};
use crate::bridge::undo::{
oakundo_command_free, oakundo_command_init, oakundo_command_redo_now, oakundo_command_undo_now,
};
use crate::handle::CHandle;
use oaknode::graph::NodeEntry;
use oakundo::undocommand::{OakUndoCommandVtable, UndoCommand};
use crate::util::{block_add_to_graph, block_remove_from_graph, NodeRef};
/// An empty (all callbacks absent) command — the failure-path result of
/// the deleted oaknode remove-command factory (an empty command handle).
pub(crate) fn empty_command() -> UndoCommand {
UndoCommand::from_vtable(
OakUndoCommandVtable {
redo: None,
undo: None,
free_fn: None,
},
std::ptr::null_mut(),
)
}
/// `oaknode_command_create_remove_node` — a command that detaches a node
/// from the project graph on `redo` (the detached entry is owned by this
/// command) and re-inserts it, identity-preserving, on `undo`. The C++
/// equivalent runs `node->setParent(&memory_manager_)` / the reverse.
struct RemoveNodeCommand {
/// The node to remove.
node: NodeRef,
/// Arena entry detached on `redo`, owned here until `undo`.
entry: Option<NodeEntry>,
}
impl RemoveNodeCommand {
/// Construct from the node to remove.
fn new(node: NodeRef) -> Self {
Self { node, entry: None }
}
}
impl Command for RemoveNodeCommand {
/// `redo`: detach the node from the graph.
fn redo(&mut self) {
if self.entry.is_none() {
self.entry = block_remove_from_graph(&self.node);
}
}
/// `undo`: re-insert the node into the graph at its original slot.
fn undo(&mut self) {
if let Some(entry) = self.entry.take() {
block_add_to_graph(&self.node, Some(entry));
}
}
}
/// `node_can_be_removed(Node)`: true when the node has no output
/// connections (timelineundocommon.h).
pub fn node_can_be_removed(node: CHandle) -> bool {
let mut count = 0;
// SAFETY: `count` is a valid out pointer.
let _ = unsafe { oaknode_node_output_connection_count(node, &mut count) };
count == 0
pub fn node_can_be_removed(node: &NodeRef) -> bool {
let p = node.project.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
p.graph.output_connections(node.id).is_empty()
}
/// `node_can_be_removed(Block)`: delegates via `oaknode_block_as_node`.
pub fn block_can_be_removed(block: CHandle) -> bool {
node_can_be_removed(unsafe { oaknode_block_as_node(block) })
/// `node_can_be_removed(Block)`: the block is checked through its
/// generic-node view.
pub fn block_can_be_removed(block: &NodeRef) -> bool {
node_can_be_removed(block)
}
/// `create_remove_command(Node)` — `oaknode_command_create_remove_node`.
pub fn create_remove_command(node: CHandle) -> CHandle {
unsafe { oaknode_command_create_remove_node(node) }
pub fn create_remove_command(node: &NodeRef) -> UndoCommand {
box_command(RemoveNodeCommand::new(node.clone()))
}
/// `create_remove_command(Block)` — via `oaknode_block_as_node`.
pub fn create_block_remove_command(block: CHandle) -> CHandle {
create_remove_command(unsafe { oaknode_block_as_node(block) })
/// `create_remove_command(Block)`.
pub fn create_block_remove_command(block: &NodeRef) -> UndoCommand {
create_remove_command(block)
}
/// `create_and_run_remove_command(Node)` — create then `redo_now`.
pub fn create_and_run_remove_command(node: CHandle) -> CHandle {
let command = create_remove_command(node);
// SAFETY: `command` is a valid handle returned by the bridge.
let _ = unsafe { oakundo_command_redo_now(command.clone()) };
pub fn create_and_run_remove_command(node: &NodeRef) -> UndoCommand {
let mut command = create_remove_command(node);
command.redo_now();
command
}
/// `create_and_run_remove_command(Block)` — via `oaknode_block_as_node`.
pub fn create_and_run_block_remove_command(block: CHandle) -> CHandle {
create_and_run_remove_command(unsafe { oaknode_block_as_node(block) })
/// `create_and_run_remove_command(Block)`.
pub fn create_and_run_block_remove_command(block: &NodeRef) -> UndoCommand {
create_and_run_remove_command(block)
}
/// `free_command_handle`: release and null a command handle; NULL/empty
/// no-op.
pub fn free_command_handle(command: *mut CHandle) {
// SAFETY: passed through to the bridge, which handles NULL.
unsafe { oakundo_command_free(command) };
/// `free_command_handle`: release and null a command; NULL no-op. With the
/// `UndoCommand` value type, "freeing" is overwriting the pointee with an
/// empty command (the old value drops, running its `free_fn`).
pub fn free_command_handle(command: *mut UndoCommand) {
if command.is_null() {
return;
}
// SAFETY: the caller passes a valid pointer; overwriting drops the old
// value and installs the no-op command.
unsafe { *command = empty_command() };
}
/// Trait implemented by every timeline undo command. The crate's commands
/// are boxed into an oakundo vtable command via [`box_command`]; the bridge's
/// `redo_now`/`undo_now` dispatch to these callbacks.
/// are boxed into an oakundo vtable command via [`box_command`]; the
/// command's `redo_now`/`undo_now` dispatch to these callbacks.
pub trait Command {
/// Apply the change.
fn redo(&mut self);
@@ -96,7 +146,7 @@ unsafe extern "C" fn redo_cb<T: Command>(userdata: *mut c_void) {
return;
}
// SAFETY: box_command allocated a `Box<T>`; still alive because the
// command handle owns it.
// command owns it.
(unsafe { &mut *(userdata as *mut T) }).redo();
}
@@ -120,66 +170,56 @@ unsafe extern "C" fn free_cb<T: Command>(userdata: *mut c_void) {
if userdata.is_null() {
return;
}
// SAFETY: the handle owns this box; destroying the handle drops it.
// SAFETY: the command owns this box; dropping it frees the command.
unsafe { drop(Box::from_raw(userdata as *mut T)) };
}
/// Box a command into an oakundo vtable command handle. The handle owns the
/// boxed `T`; `redo_now`/`undo_now` dispatch to `T::redo`/`T::undo`, and
/// freeing the handle drops `T`.
pub(crate) fn box_command<T: Command + 'static>(cmd: T) -> CHandle {
/// Box a command into an oakundo vtable command value. The command owns
/// the boxed `T`; `redo_now`/`undo_now` dispatch to `T::redo`/`T::undo`,
/// and dropping the command drops `T`.
pub(crate) fn box_command<T: Command + 'static>(cmd: T) -> UndoCommand {
let userdata = Box::into_raw(Box::new(cmd)) as *mut c_void;
let vtable = crate::bridge::undo::OakUndoCommandVtable {
let vtable = OakUndoCommandVtable {
redo: Some(redo_cb::<T>),
undo: Some(undo_cb::<T>),
free_fn: Some(free_cb::<T>),
};
// SAFETY: `vtable` and `userdata` remain valid for the command's lifetime
// (the bridge copies the vtable and owns `userdata`).
unsafe { oakundo_command_init(&vtable, userdata) }
// `from_vtable` copies the vtable and takes ownership of `userdata`.
UndoCommand::from_vtable(vtable, userdata)
}
/// `CHandleCommandWrapper` — an oakundo vtable command exposed as a
/// timeline-level command. `redo`/`undo` forward to `bridge::undo`;
/// dropping frees the handle.
/// `CHandleCommandWrapper` — an oakundo command exposed as a timeline-level
/// command. `redo`/`undo` forward to `redo_now`/`undo_now`; dropping drops
/// the wrapped command.
pub struct CHandleCommandWrapper {
/// Wrapped command handle.
command: CHandle,
/// Wrapped command; `None` mirrors the old empty (null) command handle.
command: Option<UndoCommand>,
}
impl CHandleCommandWrapper {
/// Construct over an owned command handle.
pub fn new(command: CHandle) -> Self {
Self { command }
/// Construct over an owned command value.
pub fn new(command: UndoCommand) -> Self {
Self {
command: Some(command),
}
}
/// Whether the wrapped command is non-empty.
/// Whether the wrapper holds a command (the old non-null check).
pub fn is_valid(&self) -> bool {
!self.command.is_null()
self.command.is_some()
}
/// `redo`: forward to `oakundo_command_redo_now`.
/// `redo`: forward to `UndoCommand::redo_now`.
pub fn redo(&mut self) {
if !self.command.is_null() {
// SAFETY: `self.command` is a valid handle while it is non-empty.
let _ = unsafe { oakundo_command_redo_now(self.command.clone()) };
if let Some(c) = self.command.as_mut() {
c.redo_now();
}
}
/// `undo`: forward to `oakundo_command_undo_now`.
/// `undo`: forward to `UndoCommand::undo_now`.
pub fn undo(&mut self) {
if !self.command.is_null() {
// SAFETY: `self.command` is a valid handle while it is non-empty.
let _ = unsafe { oakundo_command_undo_now(self.command.clone()) };
}
}
}
impl Drop for CHandleCommandWrapper {
fn drop(&mut self) {
if !self.command.is_null() {
// SAFETY: `self.command` is a valid owned handle.
unsafe { oakundo_command_free(&mut self.command) };
if let Some(c) = self.command.as_mut() {
c.undo_now();
}
}
}
@@ -187,7 +227,7 @@ impl Drop for CHandleCommandWrapper {
/// `MultiUndoCommand` — a command that runs several child commands in order
/// on `redo` and in reverse on `undo` (timelineundocommon.h
/// `MultiUndoCommand`). Children are boxed `Command`s; the whole group wraps
/// into a single oakundo vtable handle via [`box_command`].
/// into a single oakundo vtable command via [`box_command`].
pub struct MultiUndoCommand {
/// Child commands, run in order on `redo`.
commands: Vec<Box<dyn Command>>,
@@ -211,8 +251,8 @@ impl MultiUndoCommand {
self.commands.is_empty()
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> CHandle {
/// Wrap as an oakundo vtable command value.
pub fn to_command(self) -> UndoCommand {
box_command(self)
}
}
File diff suppressed because it is too large Load Diff
+274 -332
View File
@@ -18,28 +18,25 @@
//! that compensate the adjacent block (`BlockTrimCommand`), slides
//! (`TrackSlideCommand`) and destructive placement (`TrackPlaceBlockCommand`).
//!
//! Graph mutation goes through the oaknode C ABI (`bridge::node`); command
//! wrapping through `bridge::undo`.
//! Graph mutation routes directly through the oaknode Rust domain (the
//! oaknode C ABI was deleted in the single-lib unification); command
//! wrapping through `oakundo::undocommand::UndoCommand` values.
use oakcore_rs::{Rational, TimeRange};
use oaknode::graph::NodeEntry;
use oakundo::undocommand::UndoCommand;
use crate::bridge::node::{
oaknode_block_gap_create, oaknode_block_get_kind, oaknode_track_insert_block_after,
oaknode_track_ripple_remove_block, oaknode_tracklist_get_track_at,
oaknode_tracklist_get_track_count,
};
use crate::bridge::undo::{oakundo_command_redo_now, oakundo_command_undo_now};
use crate::common::MovementMode;
use crate::handle::CHandle;
use crate::undocommon::{
block_can_be_removed, box_command, create_and_run_block_remove_command,
create_block_remove_command, Command, MultiUndoCommand,
};
use crate::util::{
block_add_to_graph, block_in, block_length, block_next, block_previous,
block_remove_from_graph, block_set_in, block_set_length_and_media_in,
block_set_length_and_media_out, block_track, track_append_block, track_insert_block_before,
track_length,
block_add_to_graph, block_gap_create, block_in, block_kind, block_length, block_next,
block_out, block_previous, block_remove_from_graph, block_set_in,
block_set_length_and_media_in, block_set_length_and_media_out, block_track,
track_append_block, track_insert_block_after, track_insert_block_before, track_length,
track_ripple_remove_block, tracklist_track_at, tracklist_track_count, BlockKind, NodeRef,
};
use super::undogeneral::{TimelineAddTrackCommand, TrackReplaceBlockWithGapCommand};
@@ -53,9 +50,9 @@ use super::undoripple::TrackRippleRemoveAreaCommand;
/// always trim into the adjacent block instead.
pub struct BlockTrimCommand {
/// Owning track (`track_`).
track: CHandle,
track: NodeRef,
/// Block to trim (`block_`).
block: CHandle,
block: NodeRef,
/// New length (`new_length_`).
new_length: Rational,
/// Trim direction (`mode_`).
@@ -69,7 +66,7 @@ pub struct BlockTrimCommand {
/// Block length before the trim (`old_length_`).
old_length_: Rational,
/// The block to compensate (`adjacent_`).
adjacent_: CHandle,
adjacent_: Option<NodeRef>,
/// Whether the adjacent block is required for this trim (`needs_adjacent_`).
needs_adjacent_: bool,
/// Whether `adjacent_` was created by this command (`we_created_adjacent_`).
@@ -77,26 +74,23 @@ pub struct BlockTrimCommand {
/// Whether the adjacent block is removed by this trim (`we_removed_adjacent_`).
we_removed_adjacent_: bool,
/// Graph-removal command for a removed adjacent (`deleted_adjacent_command_`).
///
/// Owns its handle like the C++ `OakUndoCommand`; `CHandle` is refcounted
/// and drops without a destructor, so releasing it is implicit — no explicit
/// `free_command_handle` is needed here.
deleted_adjacent_command_: CHandle,
deleted_adjacent_command_: Option<UndoCommand>,
/// Always trim into the adjacent block instead of creating a gap
/// (`trim_is_a_roll_edit_`).
trim_is_a_roll_edit_: bool,
/// Remove a zero-length adjacent block from the whole graph (default true)
/// rather than only from the track (`remove_block_from_graph_`).
remove_block_from_graph_: bool,
/// Whether `adjacent_` is a created gap still detached from the graph
/// (`adjacent_orphaned_`); a detached handle is owned by this command until
/// it is added to the graph.
adjacent_orphaned_: bool,
/// Arena entry of a created adjacent gap while it is detached from the
/// graph (between `undo` and the next `redo`).
adjacent_entry_: Option<NodeEntry>,
}
impl BlockTrimCommand {
/// Construct from track + block + new length + movement mode.
pub fn new(track: CHandle, block: CHandle, new_length: Rational, mode: MovementMode) -> Self {
///
/// New signature (single-lib): `pub fn new(track: NodeRef, block: NodeRef, new_length: Rational, mode: MovementMode) -> BlockTrimCommand`
pub fn new(track: NodeRef, block: NodeRef, new_length: Rational, mode: MovementMode) -> Self {
Self {
track,
block,
@@ -105,14 +99,14 @@ impl BlockTrimCommand {
doing_nothing_: false,
trim_diff_: Rational::new(0, 1),
old_length_: Rational::new(0, 1),
adjacent_: CHandle::null(),
adjacent_: None,
needs_adjacent_: false,
we_created_adjacent_: false,
we_removed_adjacent_: false,
deleted_adjacent_command_: CHandle::null(),
deleted_adjacent_command_: None,
trim_is_a_roll_edit_: false,
remove_block_from_graph_: true,
adjacent_orphaned_: false,
adjacent_entry_: None,
}
}
@@ -132,7 +126,7 @@ impl BlockTrimCommand {
/// `prepare`: compute the trim delta and whether an adjacent block is needed.
pub fn prepare(&mut self) {
// Store old length
self.old_length_ = block_length(self.block.clone());
self.old_length_ = block_length(&self.block);
// If the length isn't changing, set a flag to do nothing
if self.old_length_ == self.new_length {
@@ -144,40 +138,44 @@ impl BlockTrimCommand {
// Positive when trimming shorter, negative when trimming longer
self.trim_diff_ = self.old_length_ - self.new_length;
// Retrieve our adjacent block (or an empty handle if none)
// Retrieve our adjacent block (or None if there is none)
self.adjacent_ = if self.mode == MovementMode::TrimIn {
block_previous(self.block.clone())
block_previous(&self.block)
} else {
block_next(self.block.clone())
block_next(&self.block)
};
// Ignore when trimming the out with no adjacent, because the user must
// have trimmed the end of the last block in the track
self.needs_adjacent_ = self.mode == MovementMode::TrimIn || !self.adjacent_.is_null();
self.needs_adjacent_ = self.mode == MovementMode::TrimIn || self.adjacent_.is_some();
if self.needs_adjacent_ {
// If we're trimming shorter, we need an adjacent, so check if we have a
// viable one
let mut adjacent_kind = 0; // OAKNODE_BLOCK_OTHER
if !self.adjacent_.is_null() {
// SAFETY: `adjacent_kind` is a valid out pointer.
let _ =
unsafe { oaknode_block_get_kind(self.adjacent_.clone(), &mut adjacent_kind) };
}
let adjacent_kind = self.adjacent_.as_ref().map(block_kind).unwrap_or(BlockKind::Other);
self.we_created_adjacent_ = self.trim_diff_ > Rational::new(0, 1)
&& (self.adjacent_.is_null() || (adjacent_kind != 2 && !self.trim_is_a_roll_edit_));
&& (self.adjacent_.is_none()
|| (adjacent_kind != BlockKind::Gap && !self.trim_is_a_roll_edit_));
if self.we_created_adjacent_ {
// We shortened but don't have a viable adjacent to lengthen, so create
// one
// SAFETY: `oaknode_block_gap_create` returns a fresh owned handle.
self.adjacent_ = unsafe { oaknode_block_gap_create() };
block_set_length_and_media_out(self.adjacent_.clone(), self.trim_diff_);
self.adjacent_orphaned_ = true;
} else {
// one filling exactly the space the trim freed: for a trim-in it
// spans [in - diff, in), for a trim-out [out, out + diff). The
// module stores positions on the block, so both the in point and
// the length are written explicitly.
self.adjacent_ = Some(block_gap_create(&self.track.project));
if let Some(gap) = &self.adjacent_ {
if self.mode == MovementMode::TrimIn {
block_set_in(gap, block_in(&self.block) - self.trim_diff_);
} else {
block_set_in(gap, block_out(&self.block));
}
block_set_length_and_media_in(gap, self.trim_diff_);
}
} else if let Some(adjacent) = &self.adjacent_ {
// Determine if we're removing the adjacent
self.we_removed_adjacent_ =
(block_length(self.adjacent_.clone()) + self.trim_diff_).is_null();
(block_length(adjacent) + self.trim_diff_).is_null();
}
}
}
@@ -189,57 +187,43 @@ impl BlockTrimCommand {
}
if self.mode == MovementMode::TrimIn {
block_set_length_and_media_in(self.block.clone(), self.new_length);
block_set_length_and_media_in(&self.block, self.new_length);
} else {
block_set_length_and_media_out(self.block.clone(), self.new_length);
block_set_length_and_media_out(&self.block, self.new_length);
}
if self.needs_adjacent_ {
if self.we_created_adjacent_ {
// Add the adjacent and insert it
block_add_to_graph(self.adjacent_.clone(), self.track.clone());
if self.mode == MovementMode::TrimIn {
track_insert_block_before(
self.track.clone(),
self.adjacent_.clone(),
self.block.clone(),
);
} else {
// SAFETY: `track`/`adjacent_`/`block` are valid handles.
let _ = unsafe {
oaknode_track_insert_block_after(
self.track.clone(),
self.adjacent_.clone(),
self.block.clone(),
)
};
}
self.adjacent_orphaned_ = false;
} else if self.we_removed_adjacent_ {
// SAFETY: valid handles.
let _ = unsafe {
oaknode_track_ripple_remove_block(self.track.clone(), self.adjacent_.clone())
};
// It no longer inputs/outputs anything, remove it
if self.remove_block_from_graph_ && block_can_be_removed(self.adjacent_.clone()) {
if self.deleted_adjacent_command_.is_null() {
self.deleted_adjacent_command_ =
create_and_run_block_remove_command(self.adjacent_.clone());
if let Some(gap) = &self.adjacent_ {
block_add_to_graph(gap, self.adjacent_entry_.take());
if self.mode == MovementMode::TrimIn {
track_insert_block_before(&self.track, gap, &self.block);
} else {
// SAFETY: `deleted_adjacent_command_` is a valid command handle.
let _ = unsafe {
oakundo_command_redo_now(self.deleted_adjacent_command_.clone())
};
track_insert_block_after(&self.track, gap, Some(&self.block));
}
}
} else {
let adjacent_length = block_length(self.adjacent_.clone()) + self.trim_diff_;
} else if self.we_removed_adjacent_ {
if let Some(adjacent) = &self.adjacent_ {
track_ripple_remove_block(&self.track, adjacent);
// It no longer inputs/outputs anything, remove it
if self.remove_block_from_graph_ && block_can_be_removed(adjacent) {
if self.deleted_adjacent_command_.is_none() {
self.deleted_adjacent_command_ =
Some(create_and_run_block_remove_command(adjacent));
} else if let Some(c) = self.deleted_adjacent_command_.as_mut() {
c.redo_now();
}
}
}
} else if let Some(adjacent) = &self.adjacent_ {
let adjacent_length = block_length(adjacent) + self.trim_diff_;
if self.mode == MovementMode::TrimIn {
block_set_length_and_media_out(self.adjacent_.clone(), adjacent_length);
block_set_length_and_media_out(adjacent, adjacent_length);
} else {
block_set_length_and_media_in(self.adjacent_.clone(), adjacent_length);
block_set_length_and_media_in(adjacent, adjacent_length);
}
}
}
@@ -256,59 +240,45 @@ impl BlockTrimCommand {
if self.needs_adjacent_ {
if self.we_created_adjacent_ {
// The adjacent is ours, just delete it
// SAFETY: valid handles.
let _ = unsafe {
oaknode_track_ripple_remove_block(self.track.clone(), self.adjacent_.clone())
};
block_remove_from_graph(self.adjacent_.clone(), self.track.clone());
self.adjacent_orphaned_ = true;
} else {
if let Some(gap) = &self.adjacent_ {
track_ripple_remove_block(&self.track, gap);
if self.adjacent_entry_.is_none() {
self.adjacent_entry_ = block_remove_from_graph(gap);
}
}
} else if let Some(adjacent) = &self.adjacent_ {
if self.we_removed_adjacent_ {
if !self.deleted_adjacent_command_.is_null() {
if let Some(c) = self.deleted_adjacent_command_.as_mut() {
// We deleted the adjacent, restore it now
// SAFETY: `deleted_adjacent_command_` is a valid command handle.
let _ = unsafe {
oakundo_command_undo_now(self.deleted_adjacent_command_.clone())
};
c.undo_now();
}
if self.mode == MovementMode::TrimIn {
track_insert_block_before(
self.track.clone(),
self.adjacent_.clone(),
self.block.clone(),
);
track_insert_block_before(&self.track, adjacent, &self.block);
} else {
// SAFETY: valid handles.
let _ = unsafe {
oaknode_track_insert_block_after(
self.track.clone(),
self.adjacent_.clone(),
self.block.clone(),
)
};
track_insert_block_after(&self.track, adjacent, Some(&self.block));
}
} else {
let adjacent_length = block_length(self.adjacent_.clone()) - self.trim_diff_;
let adjacent_length = block_length(adjacent) - self.trim_diff_;
if self.mode == MovementMode::TrimIn {
block_set_length_and_media_out(self.adjacent_.clone(), adjacent_length);
block_set_length_and_media_out(adjacent, adjacent_length);
} else {
block_set_length_and_media_in(self.adjacent_.clone(), adjacent_length);
block_set_length_and_media_in(adjacent, adjacent_length);
}
}
}
}
if self.mode == MovementMode::TrimIn {
block_set_length_and_media_in(self.block.clone(), self.old_length_);
block_set_length_and_media_in(&self.block, self.old_length_);
} else {
block_set_length_and_media_out(self.block.clone(), self.old_length_);
block_set_length_and_media_out(&self.block, self.old_length_);
}
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> CHandle {
/// Wrap as an oakundo vtable command value.
pub fn to_command(self) -> UndoCommand {
box_command(self)
}
}
@@ -330,9 +300,9 @@ impl Command for BlockTrimCommand {
/// (timelineundopointer.h).
pub struct TrackSlideCommand {
/// Owning track (`track_`).
track: CHandle,
track: NodeRef,
/// Blocks to slide (`blocks_`; non-empty for a valid command).
blocks: Vec<CHandle>,
blocks: Vec<NodeRef>,
/// Signed movement (`movement_`).
movement: Rational,
/// Whether `prepare` created the in adjacent (`we_created_in_adjacent_`).
@@ -340,35 +310,32 @@ pub struct TrackSlideCommand {
/// Whether the slide removes the in adjacent (`we_removed_in_adjacent_`).
we_removed_in_adjacent_: bool,
/// Block adjacent on the in side (`in_adjacent_`).
in_adjacent: CHandle,
/// Graph-removal command for a removed in adjacent (`in_adjacent_remove_command_`).
///
/// Owns its handle like the C++ `OakUndoCommand`; `CHandle` is refcounted
/// and drops without a destructor, so releasing it is implicit.
in_adjacent_remove_command_: CHandle,
/// Whether `in_adjacent_` is a created gap still detached from the graph
/// (`in_adjacent_orphaned_`); a detached handle is owned by this command.
in_adjacent_orphaned_: bool,
in_adjacent: Option<NodeRef>,
/// Graph-removal command for a removed in adjacent.
in_adjacent_remove_command_: Option<UndoCommand>,
/// Arena entry of a created in adjacent gap while detached from the graph.
in_adjacent_entry_: Option<NodeEntry>,
/// Whether `prepare` created the out adjacent (`we_created_out_adjacent_`).
we_created_out_adjacent_: bool,
/// Whether the slide removes the out adjacent (`we_removed_out_adjacent_`).
we_removed_out_adjacent_: bool,
/// Block adjacent on the out side (`out_adjacent_`).
out_adjacent: CHandle,
/// Graph-removal command for a removed out adjacent (`out_adjacent_remove_command_`).
out_adjacent_remove_command_: CHandle,
/// Whether `out_adjacent_` is a created gap still detached from the graph
/// (`out_adjacent_orphaned_`); a detached handle is owned by this command.
out_adjacent_orphaned_: bool,
out_adjacent: Option<NodeRef>,
/// Graph-removal command for a removed out adjacent.
out_adjacent_remove_command_: Option<UndoCommand>,
/// Arena entry of a created out adjacent gap while detached from the graph.
out_adjacent_entry_: Option<NodeEntry>,
}
impl TrackSlideCommand {
/// Construct from track + moving blocks + in/out adjacent blocks + movement.
///
/// New signature (single-lib): `pub fn new(track: NodeRef, blocks: Vec<NodeRef>, in_adjacent: Option<NodeRef>, out_adjacent: Option<NodeRef>, movement: Rational) -> TrackSlideCommand`
pub fn new(
track: CHandle,
blocks: Vec<CHandle>,
in_adjacent: CHandle,
out_adjacent: CHandle,
track: NodeRef,
blocks: Vec<NodeRef>,
in_adjacent: Option<NodeRef>,
out_adjacent: Option<NodeRef>,
movement: Rational,
) -> Self {
Self {
@@ -378,38 +345,44 @@ impl TrackSlideCommand {
we_created_in_adjacent_: false,
we_removed_in_adjacent_: false,
in_adjacent,
in_adjacent_remove_command_: CHandle::null(),
in_adjacent_orphaned_: false,
in_adjacent_remove_command_: None,
in_adjacent_entry_: None,
we_created_out_adjacent_: false,
we_removed_out_adjacent_: false,
out_adjacent,
out_adjacent_remove_command_: CHandle::null(),
out_adjacent_orphaned_: false,
out_adjacent_remove_command_: None,
out_adjacent_entry_: None,
}
}
/// `prepare`: capture adjacent state.
pub fn prepare(&mut self) {
if self.in_adjacent.is_null() {
// SAFETY: `oaknode_block_gap_create` returns a fresh owned handle.
self.in_adjacent = unsafe { oaknode_block_gap_create() };
block_set_length_and_media_out(self.in_adjacent.clone(), self.movement);
self.in_adjacent_orphaned_ = true;
if self.in_adjacent.is_none() {
self.in_adjacent = Some(block_gap_create(&self.track.project));
if let Some(gap) = &self.in_adjacent {
// A created in adjacent only makes sense when sliding left
// (movement < 0): the gap fills [in0 + movement, in0) ahead
// of the first block. Positions are stored on the block in
// the module model, so both ends are written explicitly.
block_set_in(gap, block_in(&self.blocks[0]) + self.movement);
block_set_length_and_media_in(gap, Rational::new(0, 1) - self.movement);
}
self.we_created_in_adjacent_ = true;
} else {
self.we_created_in_adjacent_ = false;
}
if self.out_adjacent.is_null()
&& !block_next(self.blocks[self.blocks.len() - 1].clone()).is_null()
if self.out_adjacent.is_none()
&& block_next(self.blocks.last().expect("non-empty blocks")).is_some()
{
// SAFETY: `oaknode_block_gap_create` returns a fresh owned handle.
self.out_adjacent = unsafe { oaknode_block_gap_create() };
block_set_length_and_media_out(
self.out_adjacent.clone(),
Rational::new(0, 1) - self.movement,
);
self.out_adjacent_orphaned_ = true;
self.out_adjacent = Some(block_gap_create(&self.track.project));
if let Some(gap) = &self.out_adjacent {
// A created out adjacent only makes sense when sliding right
// (movement > 0): the gap fills [last_out, last_out + movement)
// after the last block.
block_set_in(gap, block_out(self.blocks.last().expect("non-empty blocks")));
block_set_length_and_media_in(gap, self.movement);
}
self.we_created_out_adjacent_ = true;
} else {
self.we_created_out_adjacent_ = false;
@@ -421,80 +394,68 @@ impl TrackSlideCommand {
// We will always have an in adjacent if there was a valid slide
if self.we_created_in_adjacent_ {
// We created the in adjacent, so all we have to do is insert it
block_add_to_graph(self.in_adjacent.clone(), self.track.clone());
// `blocks` is non-empty when the command was constructed validly
track_insert_block_before(
self.track.clone(),
self.in_adjacent.clone(),
self.blocks[0].clone(),
);
self.in_adjacent_orphaned_ = false;
} else if Rational::new(0, 1) - self.movement == block_length(self.in_adjacent.clone()) {
// Movement will remove the in adjacent
// SAFETY: valid handles.
let _ = unsafe {
oaknode_track_ripple_remove_block(self.track.clone(), self.in_adjacent.clone())
};
if let Some(gap) = &self.in_adjacent {
block_add_to_graph(gap, self.in_adjacent_entry_.take());
// `blocks` is non-empty when the command was constructed validly
track_insert_block_before(&self.track, gap, &self.blocks[0]);
}
} else if let Some(adjacent) = &self.in_adjacent {
if Rational::new(0, 1) - self.movement == block_length(adjacent) {
// Movement will remove the in adjacent
track_ripple_remove_block(&self.track, adjacent);
if block_can_be_removed(self.in_adjacent.clone()) {
if self.in_adjacent_remove_command_.is_null() {
self.in_adjacent_remove_command_ =
create_block_remove_command(self.in_adjacent.clone());
if block_can_be_removed(adjacent) {
if self.in_adjacent_remove_command_.is_none() {
self.in_adjacent_remove_command_ =
Some(create_block_remove_command(adjacent));
}
if let Some(c) = self.in_adjacent_remove_command_.as_mut() {
c.redo_now();
}
}
// SAFETY: `in_adjacent_remove_command_` is a valid command handle.
let _ =
unsafe { oakundo_command_redo_now(self.in_adjacent_remove_command_.clone()) };
self.we_removed_in_adjacent_ = true;
} else {
// Simply resize the adjacent
block_set_length_and_media_out(
adjacent,
block_length(adjacent) + self.movement,
);
}
self.we_removed_in_adjacent_ = true;
} else {
// Simply resize the adjacent
block_set_length_and_media_out(
self.in_adjacent.clone(),
block_length(self.in_adjacent.clone()) + self.movement,
);
}
// We may not have an out adjacent if the slide was at the end of the track
if !self.out_adjacent.is_null() {
if let Some(adjacent) = &self.out_adjacent {
if self.we_created_out_adjacent_ {
// We created the out adjacent, so we just have to insert it
block_add_to_graph(self.out_adjacent.clone(), self.track.clone());
// SAFETY: valid handles.
let _ = unsafe {
oaknode_track_insert_block_after(
self.track.clone(),
self.out_adjacent.clone(),
self.blocks[self.blocks.len() - 1].clone(),
)
};
self.out_adjacent_orphaned_ = false;
} else if self.movement == block_length(self.out_adjacent.clone()) {
block_add_to_graph(adjacent, self.out_adjacent_entry_.take());
track_insert_block_after(
&self.track,
adjacent,
Some(self.blocks.last().expect("non-empty blocks")),
);
} else if self.movement == block_length(adjacent) {
// Movement will remove the out adjacent
// SAFETY: valid handles.
let _ = unsafe {
oaknode_track_ripple_remove_block(self.track.clone(), self.out_adjacent.clone())
};
track_ripple_remove_block(&self.track, adjacent);
if block_can_be_removed(self.out_adjacent.clone()) {
if self.out_adjacent_remove_command_.is_null() {
if block_can_be_removed(adjacent) {
if self.out_adjacent_remove_command_.is_none() {
self.out_adjacent_remove_command_ =
create_block_remove_command(self.out_adjacent.clone());
Some(create_block_remove_command(adjacent));
}
// SAFETY: `out_adjacent_remove_command_` is a valid command handle.
let _ = unsafe {
oakundo_command_redo_now(self.out_adjacent_remove_command_.clone())
};
if let Some(c) = self.out_adjacent_remove_command_.as_mut() {
c.redo_now();
}
}
self.we_removed_out_adjacent_ = true;
} else {
// Simply resize the adjacent
block_set_length_and_media_in(
self.out_adjacent.clone(),
block_length(self.out_adjacent.clone()) - self.movement,
adjacent,
block_length(adjacent) - self.movement,
);
}
}
@@ -504,72 +465,60 @@ impl TrackSlideCommand {
pub fn undo(&mut self) {
if self.we_created_in_adjacent_ {
// We created this, so we can remove it now
// SAFETY: valid handles.
let _ = unsafe {
oaknode_track_ripple_remove_block(self.track.clone(), self.in_adjacent.clone())
};
block_remove_from_graph(self.in_adjacent.clone(), self.track.clone());
self.in_adjacent_orphaned_ = true;
} else if self.we_removed_in_adjacent_ {
if !self.in_adjacent_remove_command_.is_null() {
// We removed this, so we can restore it now
// SAFETY: `in_adjacent_remove_command_` is a valid command handle.
let _ =
unsafe { oakundo_command_undo_now(self.in_adjacent_remove_command_.clone()) };
if let Some(gap) = &self.in_adjacent {
track_ripple_remove_block(&self.track, gap);
if self.in_adjacent_entry_.is_none() {
self.in_adjacent_entry_ = block_remove_from_graph(gap);
}
}
// `blocks` is non-empty when the command was constructed validly
track_insert_block_before(
self.track.clone(),
self.in_adjacent.clone(),
self.blocks[0].clone(),
);
} else {
// Simply resize the adjacent
block_set_length_and_media_out(
self.in_adjacent.clone(),
block_length(self.in_adjacent.clone()) - self.movement,
);
}
if !self.out_adjacent.is_null() {
if self.we_created_out_adjacent_ {
// We created this, so we can remove it now
// SAFETY: valid handles.
let _ = unsafe {
oaknode_track_ripple_remove_block(self.track.clone(), self.out_adjacent.clone())
};
block_remove_from_graph(self.out_adjacent.clone(), self.track.clone());
self.out_adjacent_orphaned_ = true;
} else if self.we_removed_out_adjacent_ {
if !self.out_adjacent_remove_command_.is_null() {
} else if let Some(adjacent) = &self.in_adjacent {
if self.we_removed_in_adjacent_ {
if let Some(c) = self.in_adjacent_remove_command_.as_mut() {
// We removed this, so we can restore it now
// SAFETY: `out_adjacent_remove_command_` is a valid command handle.
let _ = unsafe {
oakundo_command_undo_now(self.out_adjacent_remove_command_.clone())
};
c.undo_now();
}
// SAFETY: valid handles.
let _ = unsafe {
oaknode_track_insert_block_after(
self.track.clone(),
self.out_adjacent.clone(),
self.blocks[self.blocks.len() - 1].clone(),
)
};
// `blocks` is non-empty when the command was constructed validly
track_insert_block_before(&self.track, adjacent, &self.blocks[0]);
} else {
// Simply resize the adjacent
block_set_length_and_media_out(
adjacent,
block_length(adjacent) - self.movement,
);
}
}
if let Some(adjacent) = &self.out_adjacent {
if self.we_created_out_adjacent_ {
// We created this, so we can remove it now
track_ripple_remove_block(&self.track, adjacent);
if self.out_adjacent_entry_.is_none() {
self.out_adjacent_entry_ = block_remove_from_graph(adjacent);
}
} else if self.we_removed_out_adjacent_ {
if let Some(c) = self.out_adjacent_remove_command_.as_mut() {
// We removed this, so we can restore it now
c.undo_now();
}
track_insert_block_after(
&self.track,
adjacent,
Some(self.blocks.last().expect("non-empty blocks")),
);
} else {
// Simply resize the adjacent
block_set_length_and_media_in(
self.out_adjacent.clone(),
block_length(self.out_adjacent.clone()) + self.movement,
adjacent,
block_length(adjacent) + self.movement,
);
}
}
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> CHandle {
/// Wrap as an oakundo vtable command value.
pub fn to_command(self) -> UndoCommand {
box_command(self)
}
}
@@ -591,18 +540,17 @@ impl Command for TrackSlideCommand {
/// extends past the end of the sequence (timelineundopointer.h).
pub struct TrackPlaceBlockCommand {
/// Track list (`timeline_`).
timeline: CHandle,
timeline: NodeRef,
/// Target track index (`track_index_`).
track_index: i32,
/// Placement in point (`in_`).
in_: Rational,
/// Gap inserted when the block extends past the end of the sequence (`gap_`).
gap_: CHandle,
/// Whether `gap_` is detached from the graph (`gap_orphaned_`); a detached
/// handle is owned by this command.
gap_orphaned_: bool,
gap_: Option<NodeRef>,
/// Arena entry of `gap_` while detached from the graph.
gap_entry_: Option<NodeEntry>,
/// Block to place (C++ `insert_`).
block: CHandle,
block: NodeRef,
/// Track-add commands created when the track index is out of range
/// (`add_track_commands_`).
add_track_commands_: Vec<TimelineAddTrackCommand>,
@@ -612,13 +560,15 @@ pub struct TrackPlaceBlockCommand {
impl TrackPlaceBlockCommand {
/// Construct from track list + track index + block + in point.
pub fn new(timeline: CHandle, track_index: i32, block: CHandle, in_: Rational) -> Self {
///
/// New signature (single-lib): `pub fn new(timeline: NodeRef, track_index: i32, block: NodeRef, in_: Rational) -> TrackPlaceBlockCommand`
pub fn new(timeline: NodeRef, track_index: i32, block: NodeRef, in_: Rational) -> Self {
Self {
timeline,
track_index,
in_,
gap_: CHandle::null(),
gap_orphaned_: false,
gap_: None,
gap_entry_: None,
block,
add_track_commands_: Vec::new(),
ripple_remove_command_: None,
@@ -628,10 +578,7 @@ impl TrackPlaceBlockCommand {
/// `redo`: place the block destructively.
pub fn redo(&mut self) {
// Determine if we need to add tracks
let mut track_count = 0;
// SAFETY: `track_count` is a valid out pointer.
let _ =
unsafe { oaknode_tracklist_get_track_count(self.timeline.clone(), &mut track_count) };
let track_count = tracklist_track_count(&self.timeline) as i32;
if self.track_index >= track_count {
if self.add_track_commands_.is_empty() {
@@ -647,41 +594,37 @@ impl TrackPlaceBlockCommand {
}
}
let mut track = CHandle::null();
// SAFETY: `track` is a valid out pointer.
let _ = unsafe {
oaknode_tracklist_get_track_at(self.timeline.clone(), self.track_index, &mut track)
let Some(track) = tracklist_track_at(&self.timeline, self.track_index as usize) else {
return;
};
let in_ = self.in_;
let append = in_ >= track_length(track.clone());
let append = in_ >= track_length(&track);
// Check if the placement location is past the end of the timeline
if append {
if in_ > track_length(track.clone()) {
// If so, insert a gap here. In the module world the gap's in point
// is stored on the gap (Olive derives positions from the track
// order), so it must be length-set IN-anchored — an out-anchored
// `set_length_and_media_out` on the fresh (in = 0) gap would push
// the in point negative.
if self.gap_.is_null() {
// SAFETY: `oaknode_block_gap_create` returns a fresh owned handle.
self.gap_ = unsafe { oaknode_block_gap_create() };
block_set_length_and_media_in(
self.gap_.clone(),
in_ - track_length(track.clone()),
);
if in_ > track_length(&track) {
// If so, insert a gap filling [track end, in_): the module
// stores the gap's span on the block, so both ends are
// written explicitly.
if self.gap_.is_none() {
self.gap_ = Some(block_gap_create(&self.timeline.project));
if let Some(gap) = &self.gap_ {
block_set_in(gap, track_length(&track));
block_set_length_and_media_in(gap, in_ - track_length(&track));
}
}
if let Some(gap) = &self.gap_ {
block_add_to_graph(gap, self.gap_entry_.take());
track_append_block(&track, gap);
}
block_add_to_graph(self.gap_.clone(), track.clone());
track_append_block(track.clone(), self.gap_.clone());
self.gap_orphaned_ = false;
}
track_append_block(track, self.block.clone());
track_append_block(&track, &self.block);
} else {
// Place the block at this point
if self.ripple_remove_command_.is_none() {
let insert_length = block_length(self.block.clone());
let insert_length = block_length(&self.block);
let mut cmd = TrackRippleRemoveAreaCommand::new(
track.clone(),
TimeRange::new(in_, in_ + insert_length),
@@ -694,39 +637,33 @@ impl TrackPlaceBlockCommand {
cmd.prepare();
cmd.redo();
// Insert after the block that follows the cleared area (`blocks` is
// non-empty when the command was constructed validly)
// Insert after the block that follows the cleared area
let index = self
.ripple_remove_command_
.as_ref()
.unwrap()
.get_insertion_index();
// SAFETY: valid handles; `index` may be empty when the insertion is at
// the front of the track, which the ABI tolerates like C++.
let _ = unsafe { oaknode_track_insert_block_after(track, self.block.clone(), index) };
track_insert_block_after(&track, &self.block, index.as_ref());
}
}
/// `undo`: remove the placed block and restore displaced blocks.
pub fn undo(&mut self) {
let mut t = CHandle::null();
// SAFETY: `t` is a valid out pointer.
let _ = unsafe {
oaknode_tracklist_get_track_at(self.timeline.clone(), self.track_index, &mut t)
let Some(t) = tracklist_track_at(&self.timeline, self.track_index as usize) else {
return;
};
// Firstly, remove our insert
// SAFETY: valid handles.
let _ = unsafe { oaknode_track_ripple_remove_block(t.clone(), self.block.clone()) };
track_ripple_remove_block(&t, &self.block);
if self.ripple_remove_command_.is_some() {
// If we ripple-removed, just undo that
self.ripple_remove_command_.as_mut().unwrap().undo();
} else if !self.gap_.is_null() {
// SAFETY: valid handles.
let _ = unsafe { oaknode_track_ripple_remove_block(t.clone(), self.gap_.clone()) };
block_remove_from_graph(self.gap_.clone(), t.clone());
self.gap_orphaned_ = true;
} else if let Some(gap) = &self.gap_ {
track_ripple_remove_block(&t, gap);
if self.gap_entry_.is_none() {
self.gap_entry_ = block_remove_from_graph(gap);
}
}
// Remove tracks if we added them
@@ -735,8 +672,8 @@ impl TrackPlaceBlockCommand {
}
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> CHandle {
/// Wrap as an oakundo vtable command value.
pub fn to_command(self) -> UndoCommand {
box_command(self)
}
}
@@ -768,7 +705,7 @@ pub struct TrackMoveBlockCommand {
/// Composite of the gap + place halves (`children_`).
children: MultiUndoCommand,
/// The block being moved (`block_`).
block: CHandle,
block: NodeRef,
/// In point captured at construction (`old_in_`).
old_in: Rational,
/// Destination in point (`in_`).
@@ -777,29 +714,34 @@ pub struct TrackMoveBlockCommand {
impl TrackMoveBlockCommand {
/// Construct from track list + destination track index + block + in point.
pub fn new(timeline: CHandle, track_index: i32, block: CHandle, in_: Rational) -> Self {
///
/// New signature (single-lib): `pub fn new(timeline: NodeRef, track_index: i32, block: NodeRef, in_: Rational) -> TrackMoveBlockCommand`
pub fn new(timeline: NodeRef, track_index: i32, block: NodeRef, in_: Rational) -> Self {
let mut children = MultiUndoCommand::new();
children.add_child(Box::new(TrackReplaceBlockWithGapCommand::new(
block_track(block.clone()),
block.clone(),
true,
)));
if let Some(track) = block_track(&block) {
children.add_child(Box::new(TrackReplaceBlockWithGapCommand::new(
track,
block.clone(),
true,
)));
}
children.add_child(Box::new(TrackPlaceBlockCommand::new(
timeline,
track_index,
block.clone(),
in_,
)));
let old_in = block_in(&block);
Self {
children,
block,
old_in: block_in(block.clone()),
old_in,
in_,
}
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> CHandle {
/// Wrap as an oakundo vtable command value.
pub fn to_command(self) -> UndoCommand {
box_command(self)
}
}
@@ -809,13 +751,13 @@ impl Command for TrackMoveBlockCommand {
fn redo(&mut self) {
// The module stores the block's position on the block itself (no
// track-order derivation), so re-home it before placing.
block_set_in(self.block.clone(), self.in_);
block_set_in(&self.block, self.in_);
self.children.redo();
}
/// `Command::undo` — the composite runs children in reverse.
fn undo(&mut self) {
self.children.undo();
block_set_in(self.block.clone(), self.old_in);
block_set_in(&self.block, self.old_in);
}
}
File diff suppressed because it is too large Load Diff
+177 -94
View File
@@ -14,132 +14,173 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Split commands (`src/timeline/src/timelineundosplit.h`). All graph
//! operations go through the oaknode C ABI.
//! Split commands (`src/timeline/src/timelineundosplit.h`). Graph
//! operations go directly through the oaknode Rust domain (the oaknode
//! C ABI was deleted in the single-lib unification).
//!
//! The C++ oracles (`timelineundosplit.cpp`) clone the split block in the
//! project graph (`oaknode_node_copy_in_graph` + `oaknode_block_from_node`),
//! run a captured node-graph reconnect command, preserve link groups
//! (`oaknode_block_are_linked`), move out-transitions across the split
//! (`oaknode_node_disconnect`/`oaknode_node_connect`), and add cache
//! passthrough (`oaknode_clip_add_cache_passthrough_from`). None of those
//! symbols are exposed by this crate's oaknode bridge, so those branches are
//! omitted here and noted at each site; the remaining length/insert/remove
//! logic mirrors the C++.
//! passthrough (`oaknode_clip_add_cache_passthrough_from`). The clone is
//! restored via `NodeBehavior::duplicate` (the Rust equivalent of
//! `Node::Copy`) and link preservation via `Graph::link`; the
//! transition/cache/reconnect steps have no Rust equivalent yet and are
//! noted at each site.
use oakcore_rs::Rational;
use oaknode::graph::NodeEntry;
use oakundo::undocommand::UndoCommand;
use crate::bridge::node::{
oaknode_block_clip_create, oaknode_track_insert_block_after, oaknode_track_ripple_remove_block,
};
use crate::handle::CHandle;
use crate::util::{
block_in, block_length, block_out, block_set_length_and_media_in,
block_set_length_and_media_out, block_track, same_block,
block_add_to_graph, block_in, block_length, block_out, block_remove_from_graph,
block_set_length_and_media_in, block_set_length_and_media_out, block_track,
track_insert_block_after, track_ripple_remove_block, GraphBlockRange, NodeRef,
};
/// `BlockSplitCommand` — split one block at a point
/// (timelineundosplit.h).
///
/// The split keeps the original block anchored at its in-point (it becomes
/// the first half `[in, point)`) and anchors the cloned `new_block` at its
/// out-point (it becomes the second half `[point, out)`), inserted right
/// after the original so the track order mirrors the timeline order.
pub struct BlockSplitCommand {
/// Block to split.
block: CHandle,
block: NodeRef,
/// Split point.
point: Rational,
/// Second block created by the split (valid after `redo`).
new_block: CHandle,
new_block: Option<NodeRef>,
/// Arena entry of the second block while it is detached from the
/// graph (between `undo` and the next `redo`).
new_block_entry: Option<NodeEntry>,
/// Length of `block` before the split, restored on `undo`.
old_length: Rational,
}
impl BlockSplitCommand {
/// Construct from block + split point.
pub fn new(block: CHandle, point: Rational) -> Self {
///
/// New signature (single-lib): `pub fn new(block: NodeRef, point: Rational) -> BlockSplitCommand`
pub fn new(block: NodeRef, point: Rational) -> Self {
Self {
block,
point,
new_block: CHandle::null(),
new_block: None,
new_block_entry: None,
old_length: Rational::new(0, 1),
}
}
/// `prepare`: create the second half of the split. The C++ clone is
/// performed by `oaknode_node_copy_in_graph` + `oaknode_block_from_node`,
/// which are absent from this bridge, so a fresh clip block is created.
/// `prepare`: create the second half by cloning the original block in
/// the project graph (the Rust equivalent of the C++
/// `oaknode_node_copy_in_graph`; the clone happens through the
/// behavior's `duplicate`, which copies the block core too).
pub fn prepare(&mut self) {
if self.new_block.is_null() {
// SAFETY: `oaknode_block_clip_create` returns an owned handle.
self.new_block = unsafe { oaknode_block_clip_create() };
if self.new_block.is_some() {
return;
}
let id = {
let project = self.block.project.clone();
let mut p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
let entry = match p.graph.get(self.block.id) {
Some(e) => e,
None => return,
};
let new_core = entry.core.clone();
let new_behavior = match entry.behavior.duplicate(&entry.core) {
Some(b) => b,
None => return,
};
p.graph.add_node(new_core, new_behavior)
};
self.new_block = Some(NodeRef::new(self.block.project.clone(), id));
}
/// `redo`: shrink `block` to the first half, grow `new_block` to the
/// second half, and insert it after `block`.
///
/// The split keeps the ORIGINAL block anchored at its out-point (it
/// becomes the second half `[point, out)`) and anchors the fresh
/// `new_block` at its in-point (it becomes the first half
/// `[in, point)`), matching the C++ `Block::set_length_and_media_out` /
/// `set_length_and_media_in` semantics of the module.
/// The split keeps the ORIGINAL block anchored at its in-point (the
/// C++ `set_length_and_media_in` on the original) and anchors the
/// cloned `new_block` at its out-point (the C++
/// `set_length_and_media_out` on the copy — the copy carried the
/// original span, so the out point is preserved exactly).
pub fn redo(&mut self) {
// Create the second half if redo is invoked without a preceding
// prepare() (the C ABI command path may call redo directly).
if self.new_block.is_null() {
// SAFETY: `oaknode_block_clip_create` returns an owned handle.
self.new_block = unsafe { oaknode_block_clip_create() };
}
// prepare() (the vtable command path may call redo directly).
self.prepare();
self.old_length = block_length(self.block.clone());
// The C++ asserts `point_` lies strictly inside the block; that
// would panic across the FFI boundary, so it is intentionally not
// replicated — a split point outside the span is clamped by the
// rational arithmetic below.
self.old_length = block_length(&self.block);
let block_in = block_in(self.block.clone());
let block_out = block_out(self.block.clone());
let block_in = block_in(&self.block);
let block_out = block_out(&self.block);
// The C++ asserts `point_` lies strictly inside the block; that would
// panic across the FFI boundary, so it is intentionally not replicated.
let first_half_length = self.point - block_in;
let second_half_length = block_out - self.point;
let track = block_track(self.block.clone());
if let Some(new_block) = &self.new_block {
// Re-attach the second half if a previous undo detached it.
if self.new_block_entry.is_some() {
block_add_to_graph(new_block, self.new_block_entry.take());
}
// In-anchored length for the first half keeps the original's in
// point (the C++ `set_length_and_media_in`); the out-anchored
// length for the second half keeps the copy's out point (the
// C++ `set_length_and_media_out`).
block_set_length_and_media_in(&self.block, first_half_length);
block_set_length_and_media_out(new_block, second_half_length);
// Out-anchored length for the second half keeps the original's out
// point (the C++ `set_length_and_media_out`); the in-anchored length
// for the first half grows the fresh block from its default in of 0
// (the C++ `set_length_and_media_in`). The two were previously
// swapped, which anchored the halves at the wrong points.
block_set_length_and_media_out(self.block.clone(), second_half_length);
block_set_length_and_media_in(self.new_block.clone(), first_half_length);
// SAFETY: bridge inserts `new_block` after `block` on `track`.
let _ = unsafe {
oaknode_track_insert_block_after(track, self.new_block.clone(), self.block.clone())
};
if let Some(track) = block_track(&self.block) {
track_insert_block_after(&track, new_block, Some(&self.block));
}
}
// The C++ also re-runs a `reconnect_tree_command_` (the node-graph
// re-connection captured while cloning) and moves an out transition
// onto `new_block` via `oaknode_*` connection APIs; those symbols are
// absent from this bridge, so both are omitted here.
// onto `new_block` via `oaknode_*` connection APIs; the Rust block
// has no transition edges, so both are omitted here.
}
/// `undo`: restore `block`'s original length and remove the second half.
pub fn undo(&mut self) {
let track = block_track(self.block.clone());
block_set_length_and_media_out(self.block.clone(), self.old_length);
// SAFETY: bridge ripple-removes `new_block` from `track`.
let _ = unsafe { oaknode_track_ripple_remove_block(track, self.new_block.clone()) };
if let Some(track) = block_track(&self.block) {
// The redo shrank the original from its in point (it became the
// first half, in-anchored), so the restore grows it in-anchored
// too — the module stores the span on the block (the C++
// `set_length_and_media_out` derives positions from the track
// order and does not apply here).
block_set_length_and_media_in(&self.block, self.old_length);
if let Some(new_block) = &self.new_block {
track_ripple_remove_block(&track, new_block);
// Detach the second half from the graph (the C++ re-parents
// it to the scratch memory manager); the entry is owned by
// this command until the next redo.
if self.new_block_entry.is_none() {
self.new_block_entry = block_remove_from_graph(new_block);
}
}
}
// The C++ first moves a previously-moved out transition back onto
// `block` and runs `reconnect_tree_command_`'s undo; both need oaknode
// symbols absent from this bridge, so they are omitted here.
// `block` and runs `reconnect_tree_command_`'s undo; the Rust block
// has no transition edges, so those are omitted here.
}
/// The second block created by the split; only valid after `redo`.
pub fn new_block(&self) -> CHandle {
///
/// New signature (single-lib): `pub fn new_block(&self) -> Option<NodeRef>`
pub fn new_block(&self) -> Option<NodeRef> {
self.new_block.clone()
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> CHandle {
/// Wrap as an oakundo vtable command value.
pub fn to_command(self) -> UndoCommand {
crate::undocommon::box_command(self)
}
}
@@ -158,19 +199,21 @@ impl crate::undocommon::Command for BlockSplitCommand {
/// times, preserving link groups (timelineundosplit.h).
pub struct BlockSplitPreservingLinksCommand {
/// Blocks to split.
blocks: Vec<CHandle>,
blocks: Vec<NodeRef>,
/// Split times.
times: Vec<Rational>,
/// Child split commands, executed in order on `redo`.
commands: Vec<BlockSplitCommand>,
/// `splits[time_index][block_index]`: second half created for that
/// block/time, or an empty handle when the block was not split there.
splits: Vec<Vec<CHandle>>,
/// block/time, or `None` when the block was not split there.
splits: Vec<Vec<Option<NodeRef>>>,
}
impl BlockSplitPreservingLinksCommand {
/// Construct from blocks + times (one per block).
pub fn new(blocks: Vec<CHandle>, times: Vec<Rational>) -> Self {
///
/// New signature (single-lib): `pub fn new(blocks: Vec<NodeRef>, times: Vec<Rational>) -> BlockSplitPreservingLinksCommand`
pub fn new(blocks: Vec<NodeRef>, times: Vec<Rational>) -> Self {
Self {
blocks,
times,
@@ -183,7 +226,7 @@ impl BlockSplitPreservingLinksCommand {
pub fn prepare(&mut self) {
let n_times = self.times.len();
let n_blocks = self.blocks.len();
self.splits = vec![vec![CHandle::null(); n_blocks]; n_times];
self.splits = vec![vec![None; n_blocks]; n_times];
for i in 0..n_times {
let time = self.times[i];
@@ -192,8 +235,8 @@ impl BlockSplitPreservingLinksCommand {
// panic across the FFI boundary, so it is not replicated.
for j in 0..n_blocks {
let b_in = block_in(self.blocks[j].clone());
let b_out = block_out(self.blocks[j].clone());
let b_in = block_in(&self.blocks[j]);
let b_out = block_out(&self.blocks[j]);
if b_in < time && b_out > time {
let mut split = BlockSplitCommand::new(self.blocks[j].clone(), time);
@@ -205,19 +248,35 @@ impl BlockSplitPreservingLinksCommand {
}
}
// The C++ then relinks every pair of originally-linked blocks by
// creating link commands between their split halves
// (`oaknode_block_are_linked`); that symbol is absent from this
// bridge, so link preservation is omitted.
// Link preservation (C++ `oaknode_block_are_linked` +
// link commands between the halves): every pair of originally
// linked blocks that was split at the same time has its new
// halves linked too (`Graph::link` mirrors `Node::link`).
let Some(first) = self.blocks.first() else {
return;
};
let project = first.project.clone();
let mut p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
for i in 0..n_times {
for j in 0..n_blocks {
for k in (j + 1)..n_blocks {
if p.graph.are_linked(self.blocks[j].id, self.blocks[k].id) {
if let (Some(a), Some(b)) = (&self.splits[i][j], &self.splits[i][k]) {
p.graph.link(a.id, b.id);
}
}
}
}
}
}
/// `redo`: redo every child command in order.
///
/// The C ABI command path never invokes `prepare()` (the oakundo vtable
/// wrapper only dispatches `redo`/`undo`), so the children are built on
/// first redo; `prepare` itself redoes each child as it builds it, so the
/// first redo has nothing left to run. Later redos (after an undo) run
/// the stored children directly.
/// The vtable command path never invokes `prepare()` (the oakundo
/// wrapper only dispatches `redo`/`undo`), so the children are built
/// on first redo; `prepare` itself redoes each child as it builds it,
/// so the first redo has nothing left to run. Later redos (after an
/// undo) run the stored children directly.
pub fn redo(&mut self) {
if self.commands.is_empty() {
self.prepare();
@@ -237,23 +296,26 @@ impl BlockSplitPreservingLinksCommand {
/// The block produced by splitting `original` at `time_index`;
/// `None` when not applicable (timelineundosplit.h `get_split`).
pub fn get_split(&self, original: CHandle, time_index: usize) -> Option<CHandle> {
///
/// New signature (single-lib): `pub fn get_split(&self, original: &NodeRef, time_index: usize) -> Option<NodeRef>`
pub fn get_split(&self, original: &NodeRef, time_index: usize) -> Option<NodeRef> {
if time_index < self.times.len() {
for (i, b) in self.blocks.iter().enumerate() {
if same_block(b.clone(), original.clone()) {
if b.id == original.id {
return self
.splits
.get(time_index)
.and_then(|row| row.get(i))
.cloned();
.cloned()
.flatten();
}
}
}
None
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> CHandle {
/// Wrap as an oakundo vtable command value.
pub fn to_command(self) -> UndoCommand {
crate::undocommon::box_command(self)
}
}
@@ -272,7 +334,7 @@ impl crate::undocommon::Command for BlockSplitPreservingLinksCommand {
/// (timelineundosplit.h).
pub struct TrackSplitAtTimeCommand {
/// Owning track.
track: CHandle,
track: NodeRef,
/// Split point.
point: Rational,
/// Inner per-block split command, built by `prepare`.
@@ -281,7 +343,9 @@ pub struct TrackSplitAtTimeCommand {
impl TrackSplitAtTimeCommand {
/// Construct from track + point.
pub fn new(track: CHandle, point: Rational) -> Self {
///
/// New signature (single-lib): `pub fn new(track: NodeRef, point: Rational) -> TrackSplitAtTimeCommand`
pub fn new(track: NodeRef, point: Rational) -> Self {
Self {
track,
point,
@@ -289,17 +353,36 @@ impl TrackSplitAtTimeCommand {
}
}
/// `prepare`: build the per-block split command. The C++ finds the block
/// via `oaknode_track_get_block_containing_time`, which is not exposed by
/// this crate's oaknode bridge (and there is no block-enumeration helper
/// to search for it), so the inner command cannot be built here and
/// redo/undo remain no-ops until that lookup API exists.
/// `prepare`: build the per-block split command. The C++ finds the
/// block via `oaknode_track_get_block_containing_time`; the Rust
/// equivalent is `TrackBehavior::block_containing_time` over the
/// graph-backed block range.
pub fn prepare(&mut self) {
let _ = (self.track.clone(), self.point);
if self.command.is_some() {
return;
}
let block_id = {
let project = self.track.project.clone();
let p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
p.graph
.get(self.track.id)
.and_then(|e| e.behavior.as_any())
.and_then(|a| a.downcast_ref::<oaknode::track::TrackBehavior>())
.and_then(|t| {
t.block_containing_time(self.point, &GraphBlockRange { graph: &p.graph })
})
};
if let Some(id) = block_id {
self.command = Some(BlockSplitCommand::new(
NodeRef::new(self.track.project.clone(), id),
self.point,
));
}
}
/// `redo`: forward to the inner command.
pub fn redo(&mut self) {
self.prepare();
if let Some(c) = self.command.as_mut() {
c.redo();
}
@@ -312,8 +395,8 @@ impl TrackSplitAtTimeCommand {
}
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> CHandle {
/// Wrap as an oakundo vtable command value.
pub fn to_command(self) -> UndoCommand {
crate::undocommon::box_command(self)
}
}
+61 -73
View File
@@ -15,58 +15,55 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Track-level block commands (`src/timeline/src/timelineundotrack.h`).
//! All graph operations go through the oaknode C ABI (`bridge::node`).
//! Graph operations go directly through the oaknode Rust domain
//! ([`crate::util::NodeRef`] + the project graph) since the single-lib
//! unification removed the oaknode C ABI.
use oakundo::undocommand::UndoCommand;
use crate::bridge::node::{
oaknode_block_get_previous, oaknode_track_insert_block_after, oaknode_track_prepend_block,
oaknode_track_replace_block, oaknode_track_ripple_remove_block,
};
use crate::handle::CHandle;
use crate::undocommon::{box_command, Command};
use crate::util::{
block_previous, track_insert_block_after, track_prepend_block, track_replace_block,
track_ripple_remove_block, NodeRef,
};
/// `TrackRippleRemoveBlockCommand` — ripple-remove a block from a track
/// (timelineundotrack.h).
pub struct TrackRippleRemoveBlockCommand {
/// Owning track.
track: CHandle,
track: NodeRef,
/// Block to remove.
block: CHandle,
block: NodeRef,
/// Predecessor of the removed block, captured at `redo` time so `undo`
/// can restore the block at its original position.
before: CHandle,
before: Option<NodeRef>,
}
impl TrackRippleRemoveBlockCommand {
/// Construct from track + block.
pub fn new(track: CHandle, block: CHandle) -> Self {
///
/// New signature (single-lib): `pub fn new(track: NodeRef, block: NodeRef) -> TrackRippleRemoveBlockCommand`
pub fn new(track: NodeRef, block: NodeRef) -> Self {
Self {
track,
block,
before: CHandle::null(),
before: None,
}
}
/// `redo`: capture the predecessor, then `oaknode_track_ripple_remove_block`.
/// `redo`: capture the predecessor, then ripple-remove.
pub fn redo(&mut self) {
unsafe {
oaknode_block_get_previous(self.block.clone(), &mut self.before);
oaknode_track_ripple_remove_block(self.track.clone(), self.block.clone());
}
self.before = block_previous(&self.block);
track_ripple_remove_block(&self.track, &self.block);
}
/// `undo`: re-insert via `oaknode_track_insert_block_after`.
/// `undo`: re-insert after the captured predecessor.
pub fn undo(&mut self) {
unsafe {
oaknode_track_insert_block_after(
self.track.clone(),
self.block.clone(),
self.before.clone(),
);
}
track_insert_block_after(&self.track, &self.block, self.before.as_ref());
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> CHandle {
/// Wrap as an oakundo vtable command value.
pub fn to_command(self) -> UndoCommand {
box_command(self)
}
}
@@ -87,33 +84,31 @@ impl Command for TrackRippleRemoveBlockCommand {
/// (timelineundotrack.h).
pub struct TrackPrependBlockCommand {
/// Owning track.
track: CHandle,
track: NodeRef,
/// Block to prepend.
block: CHandle,
block: NodeRef,
}
impl TrackPrependBlockCommand {
/// Construct from track + block.
pub fn new(track: CHandle, block: CHandle) -> Self {
///
/// New signature (single-lib): `pub fn new(track: NodeRef, block: NodeRef) -> TrackPrependBlockCommand`
pub fn new(track: NodeRef, block: NodeRef) -> Self {
Self { track, block }
}
/// `redo`: `oaknode_track_prepend_block`.
/// `redo`: prepend the block.
pub fn redo(&mut self) {
unsafe {
oaknode_track_prepend_block(self.track.clone(), self.block.clone());
}
track_prepend_block(&self.track, &self.block);
}
/// `undo`: `oaknode_track_ripple_remove_block`.
/// `undo`: ripple-remove the block.
pub fn undo(&mut self) {
unsafe {
oaknode_track_ripple_remove_block(self.track.clone(), self.block.clone());
}
track_ripple_remove_block(&self.track, &self.block);
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> CHandle {
/// Wrap as an oakundo vtable command value.
pub fn to_command(self) -> UndoCommand {
box_command(self)
}
}
@@ -131,19 +126,22 @@ impl Command for TrackPrependBlockCommand {
}
/// `TrackInsertBlockAfterCommand` — insert a block after a predecessor
/// (timelineundotrack.h).
/// (timelineundotrack.h). A `None` predecessor inserts at the front of
/// the track.
pub struct TrackInsertBlockAfterCommand {
/// Owning track.
track: CHandle,
track: NodeRef,
/// Block to insert.
block: CHandle,
/// Predecessor block.
before: CHandle,
block: NodeRef,
/// Predecessor block (`None` = front).
before: Option<NodeRef>,
}
impl TrackInsertBlockAfterCommand {
/// Construct from track + block + predecessor.
pub fn new(track: CHandle, block: CHandle, before: CHandle) -> Self {
///
/// New signature (single-lib): `pub fn new(track: NodeRef, block: NodeRef, before: Option<NodeRef>) -> TrackInsertBlockAfterCommand`
pub fn new(track: NodeRef, block: NodeRef, before: Option<NodeRef>) -> Self {
Self {
track,
block,
@@ -151,26 +149,18 @@ impl TrackInsertBlockAfterCommand {
}
}
/// `redo`: `oaknode_track_insert_block_after`.
/// `redo`: insert after the predecessor.
pub fn redo(&mut self) {
unsafe {
oaknode_track_insert_block_after(
self.track.clone(),
self.block.clone(),
self.before.clone(),
);
}
track_insert_block_after(&self.track, &self.block, self.before.as_ref());
}
/// `undo`: `oaknode_track_ripple_remove_block`.
/// `undo`: ripple-remove the block.
pub fn undo(&mut self) {
unsafe {
oaknode_track_ripple_remove_block(self.track.clone(), self.block.clone());
}
track_ripple_remove_block(&self.track, &self.block);
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> CHandle {
/// Wrap as an oakundo vtable command value.
pub fn to_command(self) -> UndoCommand {
box_command(self)
}
}
@@ -191,16 +181,18 @@ impl Command for TrackInsertBlockAfterCommand {
/// (equal lengths required) (timelineundotrack.h).
pub struct TrackReplaceBlockCommand {
/// Owning track.
track: CHandle,
track: NodeRef,
/// Block to replace.
old: CHandle,
old: NodeRef,
/// Replacement block.
replace: CHandle,
replace: NodeRef,
}
impl TrackReplaceBlockCommand {
/// Construct from track + old + replace.
pub fn new(track: CHandle, old: CHandle, replace: CHandle) -> Self {
///
/// New signature (single-lib): `pub fn new(track: NodeRef, old: NodeRef, replace: NodeRef) -> TrackReplaceBlockCommand`
pub fn new(track: NodeRef, old: NodeRef, replace: NodeRef) -> Self {
Self {
track,
old,
@@ -208,22 +200,18 @@ impl TrackReplaceBlockCommand {
}
}
/// `redo`: `oaknode_track_replace_block(track, old, replace)`.
/// `redo`: replace `old` with `replace`.
pub fn redo(&mut self) {
unsafe {
oaknode_track_replace_block(self.track.clone(), self.old.clone(), self.replace.clone());
}
track_replace_block(&self.track, &self.old, &self.replace);
}
/// `undo`: `oaknode_track_replace_block(track, replace, old)`.
/// `undo`: swap back.
pub fn undo(&mut self) {
unsafe {
oaknode_track_replace_block(self.track.clone(), self.replace.clone(), self.old.clone());
}
track_replace_block(&self.track, &self.replace, &self.old);
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> CHandle {
/// Wrap as an oakundo vtable command value.
pub fn to_command(self) -> UndoCommand {
box_command(self)
}
}
+645 -193
View File
@@ -17,247 +17,699 @@
//! Inline helpers from `src/timeline/src/timelineutil.h`: Rational ↔
//! num/den pairs, value-handle identity, and block/track/project queries.
//!
//! All graph access goes through the oaknode C ABI (`bridge::node`). The
//! C++ `oaknode_c_api::to_native` internals are NOT replicated — handles
//! are opaque here; identity (`same_*`) is decided by `ctx` equality, not
//! by `to_native` (borrowed accessors box a fresh handle per call).
//! Since the single-lib unification the oaknode C ABI is gone; every query
//! below operates directly on the oaknode Rust domain. A [`NodeRef`] pins a
//! `NodeId` inside a project's graph (`Arc<Mutex<oaknode::project::Project>>`),
//! replacing the opaque `CHandle` node/block/track references. Nullable
//! references are plain `Option<NodeRef>` — the old `CHandle::null()`
//! sentinel is `None`, and identity (`same_*`) is `NodeId` equality.
//!
//! Blocks/tracks that a command detaches from the project graph are stored
//! as an owned [`oaknode::graph::NodeEntry`] on the command itself:
//! [`block_remove_from_graph`] takes the entry out of the arena (identity is
//! preserved for re-insertion) and [`block_add_to_graph`] puts it back — the
//! Rust equivalent of the C++ `setParent(&memory_manager_)` re-homing.
use std::cmp::Ordering;
use std::sync::{Arc, Mutex, MutexGuard};
use oakcore_rs::Rational;
use oaknode::block::{BlockCore, ClipBlockBehavior, GapBlockBehavior, TransitionBlockBehavior};
use oaknode::graph::{Graph, NodeEntry};
use oaknode::id::NodeId;
use oaknode::node::NodeCore;
use oaknode::project::Project;
use oaknode::sequence::SequenceBehavior;
use oaknode::track::{TrackBehavior, TrackListBehavior, TrackType};
use crate::bridge::node::{
oaknode_block_get_in, oaknode_block_get_length, oaknode_block_get_next, oaknode_block_get_out,
oaknode_block_get_previous, oaknode_block_get_track, oaknode_block_set_in,
oaknode_block_set_length_and_media_in, oaknode_block_set_length_and_media_out,
oaknode_node_get_project, oaknode_project_add_node, oaknode_project_remove_node,
oaknode_sequence_as_node, oaknode_track_get_block_at, oaknode_track_get_block_count,
oaknode_track_get_length, oaknode_track_get_sequence, oaknode_track_insert_block_after,
oaknode_track_prepend_block,
};
use crate::handle::CHandle;
/// A reference to a node — block, track, track list or sequence — inside
/// a project's graph. `Clone` is cheap: the project `Arc` is shared and
/// [`NodeId`] is `Copy`; the lifetime of the node follows the project.
///
/// This is the domain replacement for the opaque node/block/track
/// `CHandle`s of the deleted oaknode C ABI.
#[derive(Clone)]
pub struct NodeRef {
/// The owning project (its graph holds the node).
pub project: Arc<Mutex<Project>>,
/// The node's id in that project's graph.
pub id: NodeId,
}
impl NodeRef {
/// New reference.
pub fn new(project: Arc<Mutex<Project>>, id: NodeId) -> NodeRef {
NodeRef { project, id }
}
/// Lock the owning project. A poisoned lock is recovered so a
/// panicking command body cannot wedge every later edit.
fn lock(&self) -> MutexGuard<'_, Project> {
self.project
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
/// Split a `Rational` into numerator/denominator pairs for the C ABI
/// (timelineutil.h `rat_nd`).
/// (timelineutil.h `rat_nd`). Kept for the ABI-compatible command paths.
pub fn rat_nd(r: Rational, n: &mut i32, d: &mut i32) {
*n = r.numerator() as i32;
*d = r.denominator() as i32;
}
/// Identity of two handles by `ctx` pointer equality.
fn same_handle(a: CHandle, b: CHandle) -> bool {
a.ctx == b.ctx
/// `same_block`: two block references name the same graph node
/// (`NodeId` equality; both are assumed to live in the same project).
pub fn same_block(a: &NodeRef, b: &NodeRef) -> bool {
a.id == b.id
}
/// `same_block`: two block handles wrap the same object.
pub fn same_block(a: CHandle, b: CHandle) -> bool {
same_handle(a, b)
/// `same_track`: two track references name the same graph node.
pub fn same_track(a: &NodeRef, b: &NodeRef) -> bool {
a.id == b.id
}
/// `same_track`: two track handles wrap the same object.
pub fn same_track(a: CHandle, b: CHandle) -> bool {
same_handle(a, b)
/// `same_node`: two node references name the same graph node.
pub fn same_node(a: &NodeRef, b: &NodeRef) -> bool {
a.id == b.id
}
/// `same_node`: two node handles wrap the same object.
pub fn same_node(a: CHandle, b: CHandle) -> bool {
same_handle(a, b)
/// Block kind (C++ `Block::Type` / the deleted `oaknode/block.h`
/// `OAKNODE_BLOCK_*` values).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BlockKind {
/// Not a block.
Other,
/// Clip block.
Clip,
/// Gap block.
Gap,
/// Transition block.
Transition,
}
/// Total order over handles by `ctx`, so maps work across freshly boxed
/// handles of the same origin.
fn handle_less(a: CHandle, b: CHandle) -> Ordering {
a.ctx.cmp(&b.ctx)
// ---------------------------------------------------------------------------
// Internal graph/behavior accessors
// ---------------------------------------------------------------------------
/// Immutable [`BlockCore`] of the block node `id` (any block behavior
/// kind shares the same core), or `None` for a stale/non-block node.
fn block_core_of(project: &Project, id: NodeId) -> Option<&BlockCore> {
let entry = project.graph.get(id)?;
entry.behavior.as_any().and_then(|a| {
if let Some(clip) = a.downcast_ref::<ClipBlockBehavior>() {
Some(&clip.core)
} else if let Some(gap) = a.downcast_ref::<GapBlockBehavior>() {
Some(&gap.core)
} else if let Some(transition) = a.downcast_ref::<TransitionBlockBehavior>() {
Some(&transition.core)
} else {
None
}
})
}
/// `BlockHandleLess`: total order over block handles by wrapped native
/// pointer (identity), so maps work across freshly-boxed borrowed handles.
pub fn block_handle_less(a: CHandle, b: CHandle) -> std::cmp::Ordering {
handle_less(a, b)
}
/// `TrackHandleLess`: total order over track handles by wrapped native
/// pointer.
pub fn track_handle_less(a: CHandle, b: CHandle) -> std::cmp::Ordering {
handle_less(a, b)
}
/// `free_detached_handle`: re-take ownership of a detached object's handle
/// then release, destroying it (timelineutil.h).
///
/// In this crate's model a detached block/track handle is an owned handle
/// (it owns its box), so releasing it destroys the object. NULL/empty no-op.
pub fn free_detached_handle(h: *mut CHandle) {
if h.is_null() {
return;
/// Mutable [`BlockCore`] of the block node `id`, or `None`.
fn block_core_of_mut(project: &mut Project, id: NodeId) -> Option<&mut BlockCore> {
let entry = project.graph.get_mut(id)?;
let a = entry.behavior.as_any_mut()?;
if a.is::<ClipBlockBehavior>() {
Some(&mut a.downcast_mut::<ClipBlockBehavior>().expect("checked above").core)
} else if a.is::<GapBlockBehavior>() {
Some(&mut a.downcast_mut::<GapBlockBehavior>().expect("checked above").core)
} else if a.is::<TransitionBlockBehavior>() {
Some(
&mut a
.downcast_mut::<TransitionBlockBehavior>()
.expect("checked above")
.core,
)
} else {
None
}
// SAFETY: caller passes a valid handle pointer.
let handle = unsafe { &mut *h };
if handle.ctx.is_null() {
return;
}
if let Some(release) = handle.release {
// SAFETY: `release` is the boxed type's release callback.
unsafe { release(handle.ctx) };
}
handle.ctx = std::ptr::null_mut();
handle.addref = None;
handle.release = None;
handle.abi_version = 0;
}
/// Read a block time field as a `Rational`.
fn block_pair(b: CHandle, f: fn(CHandle, *mut i32, *mut i32) -> i32) -> Rational {
let mut n = 0;
let mut d = 0;
// SAFETY: `n`/`d` are valid out pointers.
let _ = unsafe { f(b, &mut n, &mut d) };
Rational::new(n as i64, d as i64)
/// Immutable [`TrackBehavior`] of the track node `id`, or `None`.
fn track_behavior_of(project: &Project, id: NodeId) -> Option<&TrackBehavior> {
project.graph.get(id).and_then(|e| {
e.behavior
.as_any()
.and_then(|a| a.downcast_ref::<TrackBehavior>())
})
}
/// `block_in`: in point as a `Rational`.
pub fn block_in(b: CHandle) -> Rational {
block_pair(b, oaknode_block_get_in)
/// Mutable [`TrackBehavior`] of the track node `id`, or `None`.
fn track_behavior_of_mut(project: &mut Project, id: NodeId) -> Option<&mut TrackBehavior> {
project.graph.get_mut(id).and_then(|e| {
e.behavior
.as_any_mut()
.and_then(|a| a.downcast_mut::<TrackBehavior>())
})
}
/// Immutable [`TrackListBehavior`] of the track-list node `id`, or `None`.
fn tracklist_behavior_of(project: &Project, id: NodeId) -> Option<&TrackListBehavior> {
project.graph.get(id).and_then(|e| {
e.behavior
.as_any()
.and_then(|a| a.downcast_ref::<TrackListBehavior>())
})
}
/// Mutable [`TrackListBehavior`] of the track-list node `id`, or `None`.
fn tracklist_behavior_of_mut(project: &mut Project, id: NodeId) -> Option<&mut TrackListBehavior> {
project.graph.get_mut(id).and_then(|e| {
e.behavior
.as_any_mut()
.and_then(|a| a.downcast_mut::<TrackListBehavior>())
})
}
/// Immutable [`SequenceBehavior`] of the sequence node `id`, or `None`.
fn sequence_behavior_of(project: &Project, id: NodeId) -> Option<&SequenceBehavior> {
project.graph.get(id).and_then(|e| {
e.behavior
.as_any()
.and_then(|a| a.downcast_ref::<SequenceBehavior>())
})
}
/// Graph-backed [`oaknode::track::BlockRange`] view: the block span
/// queries the track needs (in/out), read from the `BlockCore`s in the
/// arena.
pub struct GraphBlockRange<'a> {
/// The graph the block nodes live in.
pub graph: &'a Graph,
}
impl oaknode::track::BlockRange for GraphBlockRange<'_> {
fn in_(&self, block: NodeId) -> Rational {
self.graph
.get(block)
.and_then(|e| e.behavior.as_any())
.and_then(|a| {
[
a.downcast_ref::<ClipBlockBehavior>()
.map(|b| &b.core),
a.downcast_ref::<GapBlockBehavior>().map(|b| &b.core),
a.downcast_ref::<TransitionBlockBehavior>()
.map(|b| &b.core),
]
.into_iter()
.flatten()
.next()
})
.map(BlockCore::in_)
.unwrap_or_else(|| Rational::new(0, 1))
}
fn out(&self, block: NodeId) -> Rational {
self.graph
.get(block)
.and_then(|e| e.behavior.as_any())
.and_then(|a| {
[
a.downcast_ref::<ClipBlockBehavior>()
.map(|b| &b.core),
a.downcast_ref::<GapBlockBehavior>().map(|b| &b.core),
a.downcast_ref::<TransitionBlockBehavior>()
.map(|b| &b.core),
]
.into_iter()
.flatten()
.next()
})
.map(BlockCore::out)
.unwrap_or_else(|| Rational::new(0, 1))
}
}
// ---------------------------------------------------------------------------
// Block value queries / mutations
// ---------------------------------------------------------------------------
/// `block_in`: in point as a `Rational` (0/1 for a stale/non-block node,
/// matching the deleted bridge's untouched-output failure path).
pub fn block_in(b: &NodeRef) -> Rational {
let p = b.lock();
block_core_of(&p, b.id).map(BlockCore::in_).unwrap_or_else(|| Rational::new(0, 1))
}
/// `block_out`: out point as a `Rational`.
pub fn block_out(b: CHandle) -> Rational {
block_pair(b, oaknode_block_get_out)
pub fn block_out(b: &NodeRef) -> Rational {
let p = b.lock();
block_core_of(&p, b.id).map(BlockCore::out).unwrap_or_else(|| Rational::new(0, 1))
}
/// `block_length`: block length as a `Rational`.
pub fn block_length(b: CHandle) -> Rational {
block_pair(b, oaknode_block_get_length)
pub fn block_length(b: &NodeRef) -> Rational {
let p = b.lock();
block_core_of(&p, b.id)
.map(BlockCore::length)
.unwrap_or_else(|| Rational::new(0, 1))
}
/// `block_set_length_and_media_out`.
pub fn block_set_length_and_media_out(b: CHandle, len: Rational) {
let (mut n, mut d) = (0, 0);
rat_nd(len, &mut n, &mut d);
// SAFETY: `n`/`d` are valid ints.
let _ = unsafe { oaknode_block_set_length_and_media_out(b, n, d) };
/// `block_kind`: which block behavior backs the node (C++
/// `oaknode_block_get_kind`).
pub fn block_kind(b: &NodeRef) -> BlockKind {
let p = b.lock();
match p.graph.get(b.id).and_then(|e| e.behavior.as_any()) {
Some(a) if a.downcast_ref::<ClipBlockBehavior>().is_some() => BlockKind::Clip,
Some(a) if a.downcast_ref::<GapBlockBehavior>().is_some() => BlockKind::Gap,
Some(a) if a.downcast_ref::<TransitionBlockBehavior>().is_some() => BlockKind::Transition,
_ => BlockKind::Other,
}
}
/// `block_set_length_and_media_in`.
pub fn block_set_length_and_media_in(b: CHandle, len: Rational) {
let (mut n, mut d) = (0, 0);
rat_nd(len, &mut n, &mut d);
// SAFETY: `n`/`d` are valid ints.
let _ = unsafe { oaknode_block_set_length_and_media_in(b, n, d) };
/// `oaknode_block_get_enabled`: the block's enabled flag (false for a
/// stale node).
pub fn block_enabled(b: &NodeRef) -> bool {
let p = b.lock();
block_core_of(&p, b.id).map(|c| c.enabled).unwrap_or(false)
}
/// `oaknode_block_set_enabled`.
pub fn block_set_enabled(b: &NodeRef, enabled: bool) {
let mut p = b.lock();
if let Some(core) = block_core_of_mut(&mut p, b.id) {
core.enabled = enabled;
}
}
/// `block_set_length_and_media_out`: keep the out point anchored (the
/// in point shifts).
pub fn block_set_length_and_media_out(b: &NodeRef, len: Rational) {
let mut p = b.lock();
if let Some(core) = block_core_of_mut(&mut p, b.id) {
core.set_length_and_media_out(len);
}
}
/// `block_set_length_and_media_in`: keep the in point anchored (the
/// out point shifts).
pub fn block_set_length_and_media_in(b: &NodeRef, len: Rational) {
let mut p = b.lock();
if let Some(core) = block_core_of_mut(&mut p, b.id) {
core.set_length_and_media_in(len);
}
}
/// `block_set_in`: set the in point, keeping the length (the out follows).
pub fn block_set_in(b: CHandle, in_: Rational) {
let (mut n, mut d) = (0, 0);
rat_nd(in_, &mut n, &mut d);
// SAFETY: `n`/`d` are valid ints.
let _ = unsafe { oaknode_block_set_in(b, n, d) };
}
/// `track_length`: track length as a `Rational`.
pub fn track_length(t: CHandle) -> Rational {
let mut n = 0;
let mut d = 0;
// SAFETY: `n`/`d` are valid out pointers.
let _ = unsafe { oaknode_track_get_length(t, &mut n, &mut d) };
Rational::new(n as i64, d as i64)
}
/// `block_previous`: the block before `b` on its track.
pub fn block_previous(b: CHandle) -> CHandle {
let mut out = CHandle::null();
// SAFETY: `out` is a valid out pointer.
let _ = unsafe { oaknode_block_get_previous(b, &mut out) };
out
}
/// `block_next`: the block after `b` on its track.
pub fn block_next(b: CHandle) -> CHandle {
let mut out = CHandle::null();
// SAFETY: `out` is a valid out pointer.
let _ = unsafe { oaknode_block_get_next(b, &mut out) };
out
}
/// `block_track`: the track owning `b`.
pub fn block_track(b: CHandle) -> CHandle {
let mut out = CHandle::null();
// SAFETY: `out` is a valid out pointer.
let _ = unsafe { oaknode_block_get_track(b, &mut out) };
out
}
/// `track_project`: the project graph owning the track's sequence, via
/// `oaknode_track_get_sequence` + `oaknode_sequence_as_node` +
/// `oaknode_node_get_project`.
pub fn track_project(track: CHandle) -> CHandle {
let mut sequence = CHandle::null();
if unsafe { oaknode_track_get_sequence(track, &mut sequence) } != 0 || sequence.is_null() {
return CHandle::null();
}
let mut project = CHandle::null();
// SAFETY: `project` is a valid out pointer; the sequence node view is valid.
let _ = unsafe { oaknode_node_get_project(oaknode_sequence_as_node(sequence), &mut project) };
project
}
/// C++ `oaknode_track_append_block` — not exposed by the bridge, so append is
/// synthesized as insert-after the last block (prepend on an empty track).
pub fn track_append_block(track: CHandle, block: CHandle) {
let mut count = 0;
// SAFETY: `count` is a valid out pointer.
let _ = unsafe { oaknode_track_get_block_count(track.clone(), &mut count) };
if count > 0 {
let mut last = CHandle::null();
// SAFETY: `last` is a valid out pointer.
let _ = unsafe { oaknode_track_get_block_at(track.clone(), count - 1, &mut last) };
// SAFETY: `track`/`last` are valid handles.
let _ = unsafe { oaknode_track_insert_block_after(track, block, last) };
} else {
// SAFETY: valid handles.
let _ = unsafe { oaknode_track_prepend_block(track, block) };
pub fn block_set_in(b: &NodeRef, in_: Rational) {
let mut p = b.lock();
if let Some(core) = block_core_of_mut(&mut p, b.id) {
core.set_in(in_);
}
}
/// C++ `oaknode_track_insert_block_before` — not exposed by the bridge, so
/// insert-before is synthesized as insert-after `next`'s predecessor (prepend
/// when `next` is the first block on the track).
pub fn track_insert_block_before(track: CHandle, block: CHandle, next: CHandle) {
let prev = block_previous(next.clone());
if prev.is_null() {
// SAFETY: valid handles.
let _ = unsafe { oaknode_track_prepend_block(track, block) };
} else {
// SAFETY: valid handles.
let _ = unsafe { oaknode_track_insert_block_after(track, block, prev) };
/// `oaknode_clip_get_media_in`: the block's media-in point. The media
/// fields live on the shared [`BlockCore`], so this works for any block
/// kind (the deleted ABI only exposed it for clips; closest behavior).
pub fn clip_media_in(b: &NodeRef) -> Rational {
let p = b.lock();
block_core_of(&p, b.id)
.map(|c| c.media_in)
.unwrap_or_else(|| Rational::new(0, 1))
}
/// `oaknode_clip_set_media_in`.
pub fn clip_set_media_in(b: &NodeRef, media_in: Rational) {
let mut p = b.lock();
if let Some(core) = block_core_of_mut(&mut p, b.id) {
core.media_in = media_in;
}
}
/// `block_add_to_graph`: attach `b` to the project graph owning `track`.
// ---------------------------------------------------------------------------
// Block navigation (track membership)
// ---------------------------------------------------------------------------
/// `block_previous`: the block before `b` on its track (`None` when
/// trackless or first).
pub fn block_previous(b: &NodeRef) -> Option<NodeRef> {
let p = b.lock();
let track_id = block_core_of(&p, b.id).and_then(|c| c.track)?;
let blocks = &track_behavior_of(&p, track_id)?.blocks;
let index = blocks.iter().position(|id| *id == b.id)?;
if index == 0 {
return None;
}
Some(NodeRef::new(b.project.clone(), blocks[index - 1]))
}
/// `block_next`: the block after `b` on its track (`None` when trackless
/// or last).
pub fn block_next(b: &NodeRef) -> Option<NodeRef> {
let p = b.lock();
let track_id = block_core_of(&p, b.id).and_then(|c| c.track)?;
let blocks = &track_behavior_of(&p, track_id)?.blocks;
let index = blocks.iter().position(|id| *id == b.id)?;
blocks
.get(index + 1)
.map(|id| NodeRef::new(b.project.clone(), *id))
}
/// `block_track`: the track owning `b` (`None` when detached from its
/// track, e.g. after a ripple-remove).
pub fn block_track(b: &NodeRef) -> Option<NodeRef> {
let p = b.lock();
block_core_of(&p, b.id)
.and_then(|c| c.track)
.map(|id| NodeRef::new(b.project.clone(), id))
}
// ---------------------------------------------------------------------------
// Track queries / mutations
// ---------------------------------------------------------------------------
/// `track_length`: track length as a `Rational` (end of the last block).
pub fn track_length(t: &NodeRef) -> Rational {
let p = t.lock();
match track_behavior_of(&p, t.id) {
Some(track) => track.length(&GraphBlockRange { graph: &p.graph }),
None => Rational::new(0, 1),
}
}
/// `oaknode_track_get_block_count`.
pub fn track_block_count(t: &NodeRef) -> usize {
let p = t.lock();
track_behavior_of(&p, t.id).map(|t| t.blocks.len()).unwrap_or(0)
}
/// `oaknode_track_get_block_at`: block at `index` on the track.
pub fn track_block_at(t: &NodeRef, index: usize) -> Option<NodeRef> {
let p = t.lock();
track_behavior_of(&p, t.id)
.and_then(|t| t.blocks.get(index))
.map(|id| NodeRef::new(t.project.clone(), *id))
}
/// `oaknode_track_get_locked`.
pub fn track_locked(t: &NodeRef) -> bool {
let p = t.lock();
track_behavior_of(&p, t.id).map(|t| t.locked).unwrap_or(false)
}
/// `oaknode_track_get_nearest_block_before_or_at`: the block containing
/// `time`, else the last block entirely before it. `None` when the track
/// is empty or `time` precedes the first block.
///
/// The block handle is passed through directly (not `oaknode_block_as_node`):
/// `oaknode_project_add_node` rewrites the shared node box it receives
/// (`write_node_ref`) to the new project, so later track operations on `b`
/// (which adopt blocks by comparing the handle's project) see the up-to-date
/// ownership instead of faulting with `E_NOT_FOUND` on a stale scratch id.
pub fn block_add_to_graph(b: CHandle, track: CHandle) {
let project = track_project(track);
if !project.is_null() {
// SAFETY: `project` is a valid handle; `b` is a node/block handle.
let _ = unsafe { oaknode_project_add_node(project, b) };
/// CPP-PARITY timelineundoripple.cpp:64.
pub fn track_nearest_block_before_or_at(t: &NodeRef, time: Rational) -> Option<NodeRef> {
let p = t.lock();
let blocks = track_behavior_of(&p, t.id)?.blocks.clone();
let mut before: Option<NodeId> = None;
for id in blocks {
let core = block_core_of(&p, id)?;
if core.in_() <= time && core.out() > time {
return Some(NodeRef::new(t.project.clone(), id));
}
if core.out() <= time {
before = Some(id);
}
}
before.map(|id| NodeRef::new(t.project.clone(), id))
}
/// `oaknode_track_get_nearest_block_after_or_at`: the block containing
/// `time`, else the first block starting at/after it.
///
/// CPP-PARITY timelineundoripple.cpp:551.
pub fn track_nearest_block_after_or_at(t: &NodeRef, time: Rational) -> Option<NodeRef> {
let p = t.lock();
let blocks = track_behavior_of(&p, t.id)?.blocks.clone();
let mut after: Option<NodeId> = None;
for id in blocks {
let core = block_core_of(&p, id)?;
if core.in_() <= time && core.out() > time {
return Some(NodeRef::new(t.project.clone(), id));
}
if core.in_() >= time {
after = Some(id);
break;
}
}
after.map(|id| NodeRef::new(t.project.clone(), id))
}
/// C++ `oaknode_track_append_block` — append a block at the end of the
/// track's block list.
pub fn track_append_block(track: &NodeRef, block: &NodeRef) {
let mut p = track.lock();
let ok = track_behavior_of_mut(&mut p, track.id)
.map(|t| {
t.append_block(block.id);
true
})
.unwrap_or(false);
if ok {
if let Some(core) = block_core_of_mut(&mut p, block.id) {
core.track = Some(track.id);
}
}
}
/// `block_remove_from_graph`: detach `b` from the project graph owning
/// `track`.
///
/// Like [`block_add_to_graph`], `b` is passed through so `write_node_ref`
/// re-homes the shared handle into the scratch project (the caller's handle
/// becomes the owner again, matching the C++ `setParent(&memory_manager_)`).
pub fn block_remove_from_graph(b: CHandle, track: CHandle) {
let project = track_project(track);
if !project.is_null() {
// SAFETY: `project` is a valid handle; `b` is a node/block handle.
let _ = unsafe { oaknode_project_remove_node(project, b) };
/// C++ `oaknode_track_insert_block_before`: insert `block` before `next`
/// on the track (prepend when `next` is absent).
pub fn track_insert_block_before(track: &NodeRef, block: &NodeRef, next: &NodeRef) {
let mut p = track.lock();
let ok = track_behavior_of_mut(&mut p, track.id)
.map(|t| t.insert_block_before(block.id, next.id))
.unwrap_or(false);
if ok {
if let Some(core) = block_core_of_mut(&mut p, block.id) {
core.track = Some(track.id);
}
}
}
/// C++ `oaknode_track_insert_block_after`: insert `block` after `before`
/// on the track. `None` for `before` prepends (the deleted ABI tolerated
/// an empty predecessor like C++ did).
pub fn track_insert_block_after(track: &NodeRef, block: &NodeRef, before: Option<&NodeRef>) {
let mut p = track.lock();
let ok = track_behavior_of_mut(&mut p, track.id)
.map(|t| match before {
Some(b) => t.insert_block_after(block.id, b.id),
None => {
t.prepend_block(block.id);
true
}
})
.unwrap_or(false);
if ok {
if let Some(core) = block_core_of_mut(&mut p, block.id) {
core.track = Some(track.id);
}
}
}
/// C++ `oaknode_track_prepend_block`.
pub fn track_prepend_block(track: &NodeRef, block: &NodeRef) {
let mut p = track.lock();
let ok = track_behavior_of_mut(&mut p, track.id)
.map(|t| {
t.prepend_block(block.id);
true
})
.unwrap_or(false);
if ok {
if let Some(core) = block_core_of_mut(&mut p, block.id) {
core.track = Some(track.id);
}
}
}
/// C++ `oaknode_track_replace_block`: swap `old` for `replace` at the
/// same track position. `old` loses its track membership, `replace`
/// gains it.
pub fn track_replace_block(track: &NodeRef, old: &NodeRef, replace: &NodeRef) {
let mut p = track.lock();
let ok = track_behavior_of_mut(&mut p, track.id)
.map(|t| t.replace_block(old.id, replace.id))
.unwrap_or(false);
if ok {
if let Some(core) = block_core_of_mut(&mut p, old.id) {
core.track = None;
}
if let Some(core) = block_core_of_mut(&mut p, replace.id) {
core.track = Some(track.id);
}
}
}
/// C++ `oaknode_track_ripple_remove_block`: remove the block from its
/// track. In the module model the block's in/out points are stored on
/// the block (not derived from track order), so "ripple" is only the
/// membership removal — positions of later blocks stay as stored, which
/// the undo re-insertion mirrors exactly (CPP-PARITY deviation).
pub fn track_ripple_remove_block(track: &NodeRef, block: &NodeRef) {
let mut p = track.lock();
let ok = track_behavior_of_mut(&mut p, track.id)
.map(|t| t.ripple_remove_block(block.id))
.unwrap_or(false);
if ok {
if let Some(core) = block_core_of_mut(&mut p, block.id) {
core.track = None;
}
}
}
// ---------------------------------------------------------------------------
// Project graph attach / detach (C++ `setParent(&memory_manager_)` paths)
// ---------------------------------------------------------------------------
/// `block_add_to_graph`: re-insert a previously detached block entry
/// into the project graph, preserving its identity where the original
/// slot is free (C++ re-parenting from the scratch memory manager back
/// into the project). Passing `None` is a no-op — blocks created via
/// [`block_gap_create`]/[`block_clip_create`] are already in the graph.
pub fn block_add_to_graph(block: &NodeRef, entry: Option<NodeEntry>) {
if let Some(entry) = entry {
let mut p = block.lock();
p.graph.add_entry(entry, block.id);
}
}
/// `block_remove_from_graph`: detach `block` from the project graph,
/// returning its full entry for the caller to own (and re-add with
/// [`block_add_to_graph`]). `None` when the node is stale or already
/// detached.
pub fn block_remove_from_graph(block: &NodeRef) -> Option<NodeEntry> {
let mut p = block.lock();
p.graph.take_node(block.id)
}
// ---------------------------------------------------------------------------
// Block / track / track-list creation
// ---------------------------------------------------------------------------
/// C++ `oaknode_block_clip_create`: a fresh clip block node in the given
/// project's graph.
pub fn block_clip_create(project: &Arc<Mutex<Project>>) -> NodeRef {
let (core, behavior) = oaknode::block::clip_create();
let id = {
let mut p = project
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
p.graph.add_node(core, behavior)
};
NodeRef::new(project.clone(), id)
}
/// C++ `oaknode_block_gap_create`: a fresh gap block node in the given
/// project's graph.
pub fn block_gap_create(project: &Arc<Mutex<Project>>) -> NodeRef {
let (core, behavior) = oaknode::block::gap_create();
let id = {
let mut p = project
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
p.graph.add_node(core, behavior)
};
NodeRef::new(project.clone(), id)
}
/// C++ `oaknode_track_create`: a fresh track node of the given type in
/// the project's graph (detached from any track list until appended).
pub fn track_create(project: &Arc<Mutex<Project>>, kind: TrackType) -> NodeRef {
let core = NodeCore::new();
let behavior = Box::new(TrackBehavior::new(kind));
let id = {
let mut p = project
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
p.graph.add_node(core, behavior)
};
NodeRef::new(project.clone(), id)
}
// ---------------------------------------------------------------------------
// Track list / sequence queries
// ---------------------------------------------------------------------------
/// `oaknode_tracklist_get_track_count`.
pub fn tracklist_track_count(list: &NodeRef) -> usize {
let p = list.lock();
tracklist_behavior_of(&p, list.id)
.map(|l| l.tracks.len())
.unwrap_or(0)
}
/// `oaknode_tracklist_get_track_at`: track at `index` in the list.
pub fn tracklist_track_at(list: &NodeRef, index: usize) -> Option<NodeRef> {
let p = list.lock();
tracklist_behavior_of(&p, list.id)
.and_then(|l| l.tracks.get(index))
.map(|id| NodeRef::new(list.project.clone(), *id))
}
/// `oaknode_tracklist_get_type`: the list's media type (`None` for a
/// stale node).
pub fn tracklist_type(list: &NodeRef) -> Option<TrackType> {
let p = list.lock();
tracklist_behavior_of(&p, list.id).map(|l| l.kind)
}
/// `oaknode_tracklist_array_append`: append `track` to the list (sets the
/// track's list membership and its index).
pub fn tracklist_append(list: &NodeRef, track: &NodeRef) {
let mut p = list.lock();
let index = tracklist_behavior_of_mut(&mut p, list.id)
.map(|l| {
l.tracks.push(track.id);
l.tracks.len() as i32 - 1
})
.unwrap_or(-1);
if index >= 0 {
if let Some(t) = track_behavior_of_mut(&mut p, track.id) {
t.track_list = Some(list.id);
t.index = index;
}
}
}
/// `oaknode_tracklist_array_remove_last`: drop the last track from the
/// list (detaching its list membership; the node itself is removed from
/// the graph separately by the caller).
pub fn tracklist_remove_last(list: &NodeRef) -> Option<NodeRef> {
let mut p = list.lock();
let removed = tracklist_behavior_of_mut(&mut p, list.id)
.and_then(|l| l.tracks.pop())?;
if let Some(t) = track_behavior_of_mut(&mut p, removed) {
t.track_list = None;
t.index = 0;
}
Some(NodeRef::new(list.project.clone(), removed))
}
/// `oaknode_sequence_get_track_list`: the sequence's track list of the
/// given type.
pub fn sequence_track_list(sequence: &NodeRef, kind: TrackType) -> Option<NodeRef> {
let p = sequence.lock();
let lists = sequence_behavior_of(&p, sequence.id)?.track_lists.clone();
for id in lists {
if tracklist_behavior_of(&p, id).map(|l| l.kind) == Some(kind) {
return Some(NodeRef::new(sequence.project.clone(), id));
}
}
None
}
/// `oaknode_sequence_get_all_track_count` / `get_all_track_at`: every
/// track of every track list owned by the sequence, in list order.
pub fn sequence_all_tracks(sequence: &NodeRef) -> Vec<NodeRef> {
let p = sequence.lock();
let mut tracks = Vec::new();
if let Some(seq) = sequence_behavior_of(&p, sequence.id) {
let lists = seq.track_lists.clone();
for list_id in lists {
if let Some(list) = tracklist_behavior_of(&p, list_id) {
for track_id in &list.tracks {
tracks.push(NodeRef::new(sequence.project.clone(), *track_id));
}
}
}
}
tracks
}
+5 -4
View File
@@ -22,6 +22,7 @@
//! job.
use oakcore_rs::{Rational, TimeRange};
use oakundo::undocommand::UndoCommand;
/// The reset sentinel `k_reset_in` (timelineworkarea.h): 0/1. Exposed as a
/// function because `Rational::new` is not yet `const` in oakcore-rs.
@@ -125,7 +126,7 @@ impl WorkareaSetEnabledCommand {
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> crate::handle::CHandle {
pub fn to_command(self) -> UndoCommand {
crate::undocommon::box_command(self)
}
}
@@ -163,8 +164,8 @@ impl WorkareaSetRangeCommand {
}
/// Construct from work area + new range + explicitly supplied old
/// range (used by the FFI layer, which may have a previously captured
/// range; `new` delegates here with the current range).
/// range (used by callers that may have a previously captured range;
/// `new` delegates here with the current range).
pub fn new_with_old(
workarea: crate::handle::CHandle,
range: TimeRange,
@@ -192,7 +193,7 @@ impl WorkareaSetRangeCommand {
}
/// Wrap as an oakundo vtable command handle.
pub fn to_command(self) -> crate::handle::CHandle {
pub fn to_command(self) -> UndoCommand {
crate::undocommon::box_command(self)
}
}