Module-internal object references are Rust types now (values, Arc, Mutex); CHandle remains only at the oakengine C-ABI boundary: - oakundo: the global stack holds UndoStack/UndoCommand values directly (stack token is the static's address) - oaktimeline: marker/workarea boxes carry Arc<Mutex<T>>; commands share the same allocation through Arc clones (readers in oakengine stubs and the app's graphops updated to lock) - oaktask/oakstorage: sessions, write-through bindings and the database backend pass ProjectArc; the Session drops its manual release bookkeeping; nodeutil keeps the CHandle<->Arc boundary conversion (release_project restored for the app) - oakcodec: handle.rs deleted outright (no facade entry needed it); texture/block placeholders are unit structs - oakrender: copier's project handle is an identity u64; alive-count machinery removed; handle.rs is make_owned/get/get_mut only - oakplugin: the instance registry is gone (its unregister key never matched, leaking weak entries); handle.rs is the RefBox boundary type - oaknode/oakcommon: only dead guard/borrow helpers removed; external payload handles (texture/processor) documented as the boundary Flake hunts landed along the way: the audio recording test serializes on the shared manager lock with a normalized state; the autocacher cancel test uses a slow producer so cancellation is deterministic.
13890 lines
396 KiB
Rust
13890 lines
396 KiB
Rust
// Oak Video Editor - Non-Linear Video Editor
|
|
// Copyright (C) 2026 Oak Team
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
//! Local replacements for the deleted `src/bridge/` (single-lib
|
|
//! unification).
|
|
//!
|
|
//! The engine's `src/*.rs` files used to call the module crates through
|
|
//! the deleted `src/bridge/` wrappers over their C ABIs. Those ABIs are
|
|
//! deleted; every module crate now exposes direct Rust APIs only. This
|
|
//! module carries the replacement surface:
|
|
//!
|
|
//! - **Direct-Rust shims** ([`common`], [`codec`], [`audio`], [`plugin`]):
|
|
//! the old bridge names and signatures are kept, implemented over the
|
|
//! module crates' value types (oakcommon `ConfigStore`/`VideoParams`/
|
|
//! `ColorTransform`, oakcodec's export tables, oakaudio's singleton
|
|
//! manager/processor/sync/waveform APIs, oakplugin's OFX host).
|
|
//! - **Domain implementations** ([`node`], [`timeline`], [`render`],
|
|
//! [`task`]): the handle-based paths are implemented over the module
|
|
//! crates' direct Rust domains. The engine's handle layer boxes the
|
|
//! oaknode domain behind the upward CHandles
|
|
//! (`crate::handle::domain`): projects box
|
|
//! `Arc<Mutex<oaknode::project::Project>>`, nodes/blocks/tracks/
|
|
//! footage/sequences/folders box `oaknode::project::NodeRef`
|
|
//! (project + `NodeId`). Undoable creators return handles boxing
|
|
//! `oakundo::undocommand::UndoCommand` values; the oaktimeline
|
|
//! commands are the crate's real `NodeRef`-based constructors and the
|
|
//! oaktask creators wrap the real `Task` types. The few genuinely
|
|
//! unwireable parts (no Rust equivalent) remain clearly marked STUBs
|
|
//! with their reasons.
|
|
//!
|
|
//! The engine's `oakengine_*` C ABI exports (upward) are untouched; only
|
|
//! the downward internals were rewired.
|
|
|
|
pub mod common {
|
|
use std::ffi::{c_char, c_int, c_void};
|
|
|
|
use oakcommon::colortransform::ColorTransform;
|
|
use oakcommon::configstore::ConfigStore;
|
|
use oakcommon::handle::{get, get_mut, make_owned};
|
|
use oakcommon::ocioutils::PixelFormat;
|
|
use oakcommon::videoparams::{ColorRange, Interlacing, VideoParams, VideoType};
|
|
use oakcommon::error::{OAKCOMMON_E_INVALID, OAKCOMMON_OK};
|
|
|
|
use crate::handle::{read_cstr, CHandle};
|
|
|
|
/// `include/common/config.h` — error handler callback (same shape as
|
|
/// the oakcommon crate's [`oakcommon::configstore::ErrorHandler`]).
|
|
pub type ConfigErrorHandler = oakcommon::configstore::ErrorHandler;
|
|
|
|
/// Standard two-stage getter copy: copy only when the buffer is large
|
|
/// enough (never truncates); always return the required size incl. NUL.
|
|
fn copy_string(value: &str, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
let required = (value.len() + 1) as c_int;
|
|
if !buf.is_null() && buf_size >= required {
|
|
// SAFETY: the caller guarantees `buf` holds `buf_size` bytes.
|
|
unsafe {
|
|
std::ptr::copy_nonoverlapping(value.as_ptr() as *const c_char, buf, value.len());
|
|
*buf.add(value.len()) = 0;
|
|
}
|
|
}
|
|
required
|
|
}
|
|
|
|
/// Whether `(buf, buf_size)` is a valid two-stage getter output.
|
|
fn is_valid_string_out(buf: *mut c_char, buf_size: c_int) -> bool {
|
|
buf_size >= 0 && (buf_size == 0 || !buf.is_null())
|
|
}
|
|
|
|
/// group pointer -> `Option<&str>` (null -> `None`).
|
|
fn group_opt(group: *const c_char) -> Option<&'static str> {
|
|
if group.is_null() {
|
|
None
|
|
} else {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string
|
|
// that lives for the call.
|
|
unsafe { Some(std::ffi::CStr::from_ptr(group).to_str().unwrap_or("")) }
|
|
}
|
|
}
|
|
|
|
/// Release a `CHandle` in place: call its release callback, then write
|
|
/// null back.
|
|
fn free_handle(h: *mut CHandle) {
|
|
if h.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the 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` targets the box behind `ctx`.
|
|
unsafe { release(handle.ctx) };
|
|
}
|
|
handle.ctx = std::ptr::null_mut();
|
|
handle.addref = None;
|
|
handle.release = None;
|
|
handle.abi_version = 0;
|
|
}
|
|
|
|
/// `oakcommon_config_load` — direct call into `ConfigStore::load`.
|
|
pub fn oakcommon_config_load() -> c_int {
|
|
match ConfigStore::instance().load() {
|
|
Ok(()) => OAKCOMMON_OK,
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_config_save` — direct call into `ConfigStore::save`.
|
|
pub fn oakcommon_config_save() -> c_int {
|
|
match ConfigStore::instance().save() {
|
|
Ok(()) => OAKCOMMON_OK,
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_config_reset_defaults` — direct call.
|
|
pub fn oakcommon_config_reset_defaults() -> c_int {
|
|
match ConfigStore::instance().reset_defaults() {
|
|
Ok(()) => OAKCOMMON_OK,
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_config_set` — direct call into `ConfigStore::set`.
|
|
pub unsafe fn oakcommon_config_set(
|
|
group: *const c_char,
|
|
key: *const c_char,
|
|
value: *const c_char,
|
|
) {
|
|
// SAFETY: the caller guarantees valid NUL-terminated strings.
|
|
unsafe {
|
|
if key.is_null() || value.is_null() {
|
|
return;
|
|
}
|
|
ConfigStore::instance().set(group_opt(group), &read_cstr(key), &read_cstr(value));
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_config_get` (two-stage string getter).
|
|
pub unsafe fn oakcommon_config_get(
|
|
group: *const c_char,
|
|
key: *const c_char,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees valid NUL-terminated strings.
|
|
unsafe {
|
|
if key.is_null() || !is_valid_string_out(buf, buf_size) {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
match ConfigStore::instance().get(group_opt(group), &read_cstr(key)) {
|
|
Ok(s) => copy_string(&s, buf, buf_size),
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_config_get_int` — direct call.
|
|
pub unsafe fn oakcommon_config_get_int(
|
|
group: *const c_char,
|
|
key: *const c_char,
|
|
fallback: c_int,
|
|
) -> c_int {
|
|
// SAFETY: `key` is a valid NUL-terminated string or NULL.
|
|
unsafe {
|
|
if key.is_null() {
|
|
return fallback;
|
|
}
|
|
ConfigStore::instance().get_int(group_opt(group), &read_cstr(key), fallback)
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_config_get_int64` — direct call.
|
|
pub unsafe fn oakcommon_config_get_int64(
|
|
group: *const c_char,
|
|
key: *const c_char,
|
|
fallback: i64,
|
|
) -> i64 {
|
|
// SAFETY: `key` is a valid NUL-terminated string or NULL.
|
|
unsafe {
|
|
if key.is_null() {
|
|
return fallback;
|
|
}
|
|
ConfigStore::instance().get_int64(group_opt(group), &read_cstr(key), fallback)
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_config_get_double` — direct call.
|
|
pub unsafe fn oakcommon_config_get_double(
|
|
group: *const c_char,
|
|
key: *const c_char,
|
|
fallback: f64,
|
|
) -> f64 {
|
|
// SAFETY: `key` is a valid NUL-terminated string or NULL.
|
|
unsafe {
|
|
if key.is_null() {
|
|
return fallback;
|
|
}
|
|
ConfigStore::instance().get_double(group_opt(group), &read_cstr(key), fallback)
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_config_get_bool` — direct call.
|
|
pub unsafe fn oakcommon_config_get_bool(
|
|
group: *const c_char,
|
|
key: *const c_char,
|
|
fallback: c_int,
|
|
) -> c_int {
|
|
// SAFETY: `key` is a valid NUL-terminated string or NULL.
|
|
unsafe {
|
|
if key.is_null() {
|
|
return fallback;
|
|
}
|
|
ConfigStore::instance().get_bool(group_opt(group), &read_cstr(key), fallback)
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_config_set_int` — direct call.
|
|
pub unsafe fn oakcommon_config_set_int(group: *const c_char, key: *const c_char, value: c_int) {
|
|
// SAFETY: `key` is a valid NUL-terminated string or NULL.
|
|
unsafe {
|
|
if key.is_null() {
|
|
return;
|
|
}
|
|
ConfigStore::instance().set_int(group_opt(group), &read_cstr(key), value);
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_config_set_int64` — direct call.
|
|
pub unsafe fn oakcommon_config_set_int64(
|
|
group: *const c_char,
|
|
key: *const c_char,
|
|
value: i64,
|
|
) {
|
|
// SAFETY: `key` is a valid NUL-terminated string or NULL.
|
|
unsafe {
|
|
if key.is_null() {
|
|
return;
|
|
}
|
|
ConfigStore::instance().set_int64(group_opt(group), &read_cstr(key), value);
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_config_set_double` — direct call.
|
|
pub unsafe fn oakcommon_config_set_double(
|
|
group: *const c_char,
|
|
key: *const c_char,
|
|
value: f64,
|
|
) {
|
|
// SAFETY: `key` is a valid NUL-terminated string or NULL.
|
|
unsafe {
|
|
if key.is_null() {
|
|
return;
|
|
}
|
|
ConfigStore::instance().set_double(group_opt(group), &read_cstr(key), value);
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_config_set_bool` — direct call.
|
|
pub unsafe fn oakcommon_config_set_bool(group: *const c_char, key: *const c_char, value: c_int) {
|
|
// SAFETY: `key` is a valid NUL-terminated string or NULL.
|
|
unsafe {
|
|
if key.is_null() {
|
|
return;
|
|
}
|
|
ConfigStore::instance().set_bool(group_opt(group), &read_cstr(key), value);
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_config_entry_type` — direct call.
|
|
pub unsafe fn oakcommon_config_entry_type(group: *const c_char, key: *const c_char) -> c_int {
|
|
// SAFETY: `key` is a valid NUL-terminated string or NULL.
|
|
unsafe {
|
|
if key.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
match ConfigStore::instance().entry_type(group_opt(group), &read_cstr(key)) {
|
|
Ok(oakcommon::configstore::EntryType::None) => 0,
|
|
Ok(oakcommon::configstore::EntryType::String) => 1,
|
|
Ok(oakcommon::configstore::EntryType::Int) => 2,
|
|
Ok(oakcommon::configstore::EntryType::Double) => 3,
|
|
Ok(oakcommon::configstore::EntryType::Bool) => 4,
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_config_set_error_handler` — direct call.
|
|
pub fn oakcommon_config_set_error_handler(
|
|
handler: ConfigErrorHandler,
|
|
userdata: *mut c_void,
|
|
) -> c_int {
|
|
match ConfigStore::instance().set_error_handler(handler, userdata) {
|
|
Ok(()) => OAKCOMMON_OK,
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
// ---- videoparams (value-typed VideoParams boxed as CHandles) ----------
|
|
|
|
fn interlacing_from_i32(v: c_int) -> Interlacing {
|
|
match v {
|
|
1 => Interlacing::TopFirst,
|
|
2 => Interlacing::BottomFirst,
|
|
_ => Interlacing::None,
|
|
}
|
|
}
|
|
|
|
fn video_type_from_i32(v: c_int) -> VideoType {
|
|
match v {
|
|
1 => VideoType::Still,
|
|
2 => VideoType::ImageSequence,
|
|
_ => VideoType::Video,
|
|
}
|
|
}
|
|
|
|
fn color_range_from_i32(v: c_int) -> ColorRange {
|
|
if v == 1 {
|
|
ColorRange::Full
|
|
} else {
|
|
ColorRange::Limited
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_init` — boxes `VideoParams::new()`.
|
|
pub fn oakcommon_videoparams_init() -> CHandle {
|
|
make_owned(VideoParams::new())
|
|
}
|
|
|
|
/// `oakcommon_videoparams_init_basic` — boxes `VideoParams::new_basic`.
|
|
pub fn oakcommon_videoparams_init_basic(
|
|
width: c_int,
|
|
height: c_int,
|
|
pixel_format: c_int,
|
|
nb_channels: c_int,
|
|
pixel_aspect_num: c_int,
|
|
pixel_aspect_den: c_int,
|
|
interlacing: c_int,
|
|
divider: c_int,
|
|
) -> CHandle {
|
|
make_owned(VideoParams::new_basic(
|
|
width,
|
|
height,
|
|
PixelFormat::from_code(pixel_format),
|
|
nb_channels,
|
|
pixel_aspect_num,
|
|
pixel_aspect_den,
|
|
interlacing,
|
|
divider,
|
|
))
|
|
}
|
|
|
|
/// `oakcommon_videoparams_init_with_time_base` — boxes
|
|
/// `VideoParams::new_with_time_base`.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn oakcommon_videoparams_init_with_time_base(
|
|
width: c_int,
|
|
height: c_int,
|
|
time_base_num: c_int,
|
|
time_base_den: c_int,
|
|
pixel_format: c_int,
|
|
nb_channels: c_int,
|
|
pixel_aspect_num: c_int,
|
|
pixel_aspect_den: c_int,
|
|
interlacing: c_int,
|
|
divider: c_int,
|
|
) -> CHandle {
|
|
make_owned(VideoParams::new_with_time_base(
|
|
width,
|
|
height,
|
|
time_base_num,
|
|
time_base_den,
|
|
PixelFormat::from_code(pixel_format),
|
|
nb_channels,
|
|
pixel_aspect_num,
|
|
pixel_aspect_den,
|
|
interlacing,
|
|
divider,
|
|
))
|
|
}
|
|
|
|
/// `oakcommon_videoparams_free` — release one reference.
|
|
pub fn oakcommon_videoparams_free(params: *mut CHandle) {
|
|
free_handle(params);
|
|
}
|
|
|
|
macro_rules! vp_getter {
|
|
($name:ident, $method:ident, $ty:ty) => {
|
|
#[doc = concat!("`oakcommon_videoparams_", stringify!($name), "` — direct call into `VideoParams::", stringify!($method), "`.")]
|
|
pub fn $name(params: CHandle, out: *mut $ty) -> c_int {
|
|
if params.is_null() || out.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
// SAFETY: `out` is a valid out pointer.
|
|
unsafe {
|
|
*out = p.$method() as $ty;
|
|
}
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! vp_setter {
|
|
($name:ident, $method:ident, $ty:ty) => {
|
|
#[doc = concat!("`oakcommon_videoparams_", stringify!($name), "` — direct call into `VideoParams::", stringify!($method), "`.")]
|
|
pub fn $name(params: CHandle, value: $ty) -> c_int {
|
|
if params.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get_mut::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
p.$method(value);
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
vp_getter!(oakcommon_videoparams_get_width, width, i32);
|
|
vp_setter!(oakcommon_videoparams_set_width, set_width, i32);
|
|
vp_getter!(oakcommon_videoparams_get_height, height, i32);
|
|
vp_setter!(oakcommon_videoparams_set_height, set_height, i32);
|
|
vp_getter!(oakcommon_videoparams_get_depth, depth, i32);
|
|
vp_setter!(oakcommon_videoparams_set_depth, set_depth, i32);
|
|
vp_getter!(oakcommon_videoparams_get_is_3d, is_3d, i32);
|
|
vp_getter!(oakcommon_videoparams_get_channel_count, channel_count, i32);
|
|
vp_setter!(oakcommon_videoparams_set_channel_count, set_channel_count, i32);
|
|
vp_getter!(oakcommon_videoparams_get_enabled, enabled, i32);
|
|
/// `oakcommon_videoparams_set_enabled` — bool setter.
|
|
pub fn oakcommon_videoparams_set_enabled(params: CHandle, value: c_int) -> c_int {
|
|
if params.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get_mut::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
p.set_enabled(value != 0);
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
vp_getter!(oakcommon_videoparams_get_x, x, f32);
|
|
vp_setter!(oakcommon_videoparams_set_x, set_x, f32);
|
|
vp_getter!(oakcommon_videoparams_get_y, y, f32);
|
|
vp_setter!(oakcommon_videoparams_set_y, set_y, f32);
|
|
vp_getter!(oakcommon_videoparams_get_stream_index, stream_index, i32);
|
|
vp_setter!(oakcommon_videoparams_set_stream_index, set_stream_index, i32);
|
|
vp_getter!(oakcommon_videoparams_get_start_time, start_time, i64);
|
|
vp_setter!(oakcommon_videoparams_set_start_time, set_start_time, i64);
|
|
vp_getter!(oakcommon_videoparams_get_duration, duration, i64);
|
|
vp_setter!(oakcommon_videoparams_set_duration, set_duration, i64);
|
|
vp_getter!(oakcommon_videoparams_get_color_primaries, color_primaries, i32);
|
|
vp_setter!(oakcommon_videoparams_set_color_primaries, set_color_primaries, i32);
|
|
vp_getter!(oakcommon_videoparams_get_color_transfer, color_transfer, i32);
|
|
vp_setter!(oakcommon_videoparams_set_color_transfer, set_color_transfer, i32);
|
|
vp_getter!(oakcommon_videoparams_get_square_pixel_width, square_pixel_width, i32);
|
|
vp_getter!(oakcommon_videoparams_get_effective_width, effective_width, i32);
|
|
vp_getter!(oakcommon_videoparams_get_effective_height, effective_height, i32);
|
|
vp_getter!(oakcommon_videoparams_get_effective_depth, effective_depth, i32);
|
|
vp_getter!(oakcommon_videoparams_get_is_valid, is_valid, i32);
|
|
vp_getter!(oakcommon_videoparams_get_bytes_per_channel, bytes_per_channel, i32);
|
|
vp_getter!(oakcommon_videoparams_get_bytes_per_pixel, bytes_per_pixel, i32);
|
|
vp_getter!(oakcommon_videoparams_get_buffer_size, buffer_size, i32);
|
|
|
|
/// `oakcommon_videoparams_get_time_base` — num/den pair getter.
|
|
pub fn oakcommon_videoparams_get_time_base(
|
|
params: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if params.is_null() || numerator.is_null() || denominator.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
let (n, d) = p.time_base();
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = n;
|
|
*denominator = d;
|
|
}
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_set_time_base` — num/den pair setter.
|
|
pub fn oakcommon_videoparams_set_time_base(
|
|
params: CHandle,
|
|
numerator: c_int,
|
|
denominator: c_int,
|
|
) -> c_int {
|
|
if params.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get_mut::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
p.set_time_base(numerator, denominator);
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_frame_rate` — num/den pair getter.
|
|
pub fn oakcommon_videoparams_get_frame_rate(
|
|
params: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if params.is_null() || numerator.is_null() || denominator.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
let (n, d) = p.frame_rate();
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = n;
|
|
*denominator = d;
|
|
}
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_set_frame_rate` — num/den pair setter.
|
|
pub fn oakcommon_videoparams_set_frame_rate(
|
|
params: CHandle,
|
|
numerator: c_int,
|
|
denominator: c_int,
|
|
) -> c_int {
|
|
if params.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get_mut::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
p.set_frame_rate(numerator, denominator);
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_frame_rate_as_time_base` — num/den getter.
|
|
pub fn oakcommon_videoparams_frame_rate_as_time_base(
|
|
params: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if params.is_null() || numerator.is_null() || denominator.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
let (n, d) = p.frame_rate_as_time_base();
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = n;
|
|
*denominator = d;
|
|
}
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_pixel_aspect_ratio` — num/den getter.
|
|
pub fn oakcommon_videoparams_get_pixel_aspect_ratio(
|
|
params: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if params.is_null() || numerator.is_null() || denominator.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
let (n, d) = p.pixel_aspect_ratio();
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = n;
|
|
*denominator = d;
|
|
}
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_set_pixel_aspect_ratio` — num/den setter.
|
|
pub fn oakcommon_videoparams_set_pixel_aspect_ratio(
|
|
params: CHandle,
|
|
numerator: c_int,
|
|
denominator: c_int,
|
|
) -> c_int {
|
|
if params.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get_mut::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
p.set_pixel_aspect_ratio(numerator, denominator);
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_format` — `PixelFormat::Format` code.
|
|
pub fn oakcommon_videoparams_get_format(params: CHandle, format: *mut c_int) -> c_int {
|
|
if params.is_null() || format.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*format = p.format().code();
|
|
}
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_set_format` — `PixelFormat::Format` code.
|
|
pub fn oakcommon_videoparams_set_format(params: CHandle, format: c_int) -> c_int {
|
|
if params.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get_mut::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
p.set_format(PixelFormat::from_code(format));
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_interlacing` — `Interlacing` code.
|
|
pub fn oakcommon_videoparams_get_interlacing(
|
|
params: CHandle,
|
|
interlacing: *mut c_int,
|
|
) -> c_int {
|
|
if params.is_null() || interlacing.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*interlacing = p.interlacing() as c_int;
|
|
}
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_set_interlacing` — `Interlacing` code.
|
|
pub fn oakcommon_videoparams_set_interlacing(params: CHandle, interlacing: c_int) -> c_int {
|
|
if params.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get_mut::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
p.set_interlacing(interlacing_from_i32(interlacing));
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_divider` — divider.
|
|
pub fn oakcommon_videoparams_get_divider(params: CHandle, divider: *mut c_int) -> c_int {
|
|
if params.is_null() || divider.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*divider = p.divider();
|
|
}
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_set_divider` — divider.
|
|
pub fn oakcommon_videoparams_set_divider(params: CHandle, divider: c_int) -> c_int {
|
|
if params.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get_mut::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
p.set_divider(divider);
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_video_type` — `VideoType` code.
|
|
pub fn oakcommon_videoparams_get_video_type(params: CHandle, type_: *mut c_int) -> c_int {
|
|
if params.is_null() || type_.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*type_ = p.video_type() as c_int;
|
|
}
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_set_video_type` — `VideoType` code.
|
|
pub fn oakcommon_videoparams_set_video_type(params: CHandle, type_: c_int) -> c_int {
|
|
if params.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get_mut::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
p.set_video_type(video_type_from_i32(type_));
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_premultiplied_alpha` — 0/1.
|
|
pub fn oakcommon_videoparams_get_premultiplied_alpha(
|
|
params: CHandle,
|
|
premultiplied: *mut c_int,
|
|
) -> c_int {
|
|
if params.is_null() || premultiplied.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*premultiplied = p.premultiplied_alpha() as c_int;
|
|
}
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_set_premultiplied_alpha` — 0/1.
|
|
pub fn oakcommon_videoparams_set_premultiplied_alpha(
|
|
params: CHandle,
|
|
premultiplied: c_int,
|
|
) -> c_int {
|
|
if params.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get_mut::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
p.set_premultiplied_alpha(premultiplied != 0);
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_color_range` — `ColorRange` code.
|
|
pub fn oakcommon_videoparams_get_color_range(
|
|
params: CHandle,
|
|
color_range: *mut c_int,
|
|
) -> c_int {
|
|
if params.is_null() || color_range.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*color_range = p.color_range() as c_int;
|
|
}
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_set_color_range` — `ColorRange` code.
|
|
pub fn oakcommon_videoparams_set_color_range(params: CHandle, color_range: c_int) -> c_int {
|
|
if params.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get_mut::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
p.set_color_range(color_range_from_i32(color_range));
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_colorspace` (two-stage string getter).
|
|
pub fn oakcommon_videoparams_get_colorspace(
|
|
params: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if params.is_null() || !is_valid_string_out(buf, buf_size) {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get::<VideoParams>(¶ms) } {
|
|
Some(p) => copy_string(p.colorspace(), buf, buf_size),
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_set_colorspace`.
|
|
pub unsafe fn oakcommon_videoparams_set_colorspace(
|
|
params: CHandle,
|
|
colorspace: *const c_char,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
unsafe {
|
|
if params.is_null() || colorspace.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
match get_mut::<VideoParams>(¶ms) {
|
|
Some(p) => {
|
|
p.set_colorspace(&read_cstr(colorspace));
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_time_in_timebase_units`.
|
|
pub fn oakcommon_videoparams_get_time_in_timebase_units(
|
|
params: CHandle,
|
|
time_num: c_int,
|
|
time_den: c_int,
|
|
timestamp: *mut i64,
|
|
) -> c_int {
|
|
if params.is_null() || timestamp.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `params` must be a live `make_owned(VideoParams)` handle.
|
|
match unsafe { get::<VideoParams>(¶ms) } {
|
|
Some(p) => {
|
|
// CPP-PARITY: the C++ returns INT64_MIN (AV_NOPTS_VALUE) when
|
|
// no time base is set; the Rust domain returns None.
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*timestamp = p.time_in_timebase_units(time_num, time_den).unwrap_or(i64::MIN);
|
|
}
|
|
OAKCOMMON_OK
|
|
}
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_equals` — 1 when the sets match.
|
|
pub fn oakcommon_videoparams_equals(a: CHandle, b: CHandle, out_equal: *mut c_int) -> c_int {
|
|
if a.is_null() || b.is_null() || out_equal.is_null() {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `a`/`b` must be live `make_owned(VideoParams)` handles.
|
|
match (unsafe { get::<VideoParams>(&a) }, unsafe { get::<VideoParams>(&b) }) {
|
|
(Some(pa), Some(pb)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out_equal = pa.equals(pb) as c_int;
|
|
}
|
|
OAKCOMMON_OK
|
|
}
|
|
_ => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_format_is_float` (static).
|
|
pub fn oakcommon_videoparams_format_is_float(pixel_format: c_int) -> c_int {
|
|
VideoParams::format_is_float(PixelFormat::from_code(pixel_format)) as c_int
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_format_name` (two-stage string getter).
|
|
pub fn oakcommon_videoparams_get_format_name(
|
|
pixel_format: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if !is_valid_string_out(buf, buf_size) {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
match VideoParams::format_name(PixelFormat::from_code(pixel_format)) {
|
|
Ok(s) => copy_string(&s, buf, buf_size),
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_frame_rate_to_string` (two-stage).
|
|
pub fn oakcommon_videoparams_frame_rate_to_string(
|
|
numerator: c_int,
|
|
denominator: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if !is_valid_string_out(buf, buf_size) {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
match VideoParams::frame_rate_to_string(numerator, denominator) {
|
|
Ok(s) => copy_string(&s, buf, buf_size),
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_name_for_divider` (two-stage).
|
|
pub fn oakcommon_videoparams_get_name_for_divider(
|
|
divider: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if !is_valid_string_out(buf, buf_size) {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
match VideoParams::name_for_divider(divider) {
|
|
Ok(s) => copy_string(&s, buf, buf_size),
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_scaled_dimension` (static).
|
|
pub fn oakcommon_videoparams_get_scaled_dimension(dimension: c_int, divider: c_int) -> c_int {
|
|
VideoParams::get_scaled_dimension(dimension, divider)
|
|
}
|
|
|
|
/// `oakcommon_videoparams_generate_auto_divider` (static).
|
|
pub fn oakcommon_videoparams_generate_auto_divider(width: i64, height: i64) -> c_int {
|
|
VideoParams::generate_auto_divider(width, height)
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_divider_for_target_resolution` (static).
|
|
pub fn oakcommon_videoparams_get_divider_for_target_resolution(
|
|
src_width: c_int,
|
|
src_height: c_int,
|
|
target_width: c_int,
|
|
target_height: c_int,
|
|
) -> c_int {
|
|
VideoParams::get_divider_for_target_resolution(src_width, src_height, target_width, target_height)
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_bytes_per_channel_for_format` (static).
|
|
pub fn oakcommon_videoparams_get_bytes_per_channel_for_format(pixel_format: c_int) -> c_int {
|
|
VideoParams::bytes_per_channel_for_format(PixelFormat::from_code(pixel_format))
|
|
}
|
|
|
|
/// `oakcommon_videoparams_get_bytes_per_pixel_for_format` (static).
|
|
pub fn oakcommon_videoparams_get_bytes_per_pixel_for_format(
|
|
pixel_format: c_int,
|
|
channels: c_int,
|
|
) -> c_int {
|
|
VideoParams::bytes_per_pixel_for_format(PixelFormat::from_code(pixel_format), channels)
|
|
}
|
|
|
|
/// `oakcommon_videoparams_calculate_buffer_size` (static).
|
|
pub fn oakcommon_videoparams_calculate_buffer_size(
|
|
width: c_int,
|
|
height: c_int,
|
|
pixel_format: c_int,
|
|
channels: c_int,
|
|
) -> c_int {
|
|
VideoParams::calculate_buffer_size(width, height, PixelFormat::from_code(pixel_format), channels)
|
|
}
|
|
|
|
/// `oakcommon_videoparams_static_get_bytes_per_pixel` (static).
|
|
pub fn oakcommon_videoparams_static_get_bytes_per_pixel(
|
|
pixel_format: c_int,
|
|
channels: c_int,
|
|
) -> c_int {
|
|
VideoParams::bytes_per_pixel_for_format(PixelFormat::from_code(pixel_format), channels)
|
|
}
|
|
|
|
// ---- colortransform (value-typed ColorTransform boxed as CHandle) ----
|
|
|
|
/// `oakcommon_colortransform_init_output` — boxes
|
|
/// `ColorTransform::new_output`.
|
|
pub unsafe fn oakcommon_colortransform_init_output(output: *const c_char) -> CHandle {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let output = unsafe { read_cstr(output) };
|
|
make_owned(ColorTransform::new_output(&output))
|
|
}
|
|
|
|
/// `oakcommon_colortransform_init_display` — boxes
|
|
/// `ColorTransform::new_display`.
|
|
pub unsafe fn oakcommon_colortransform_init_display(
|
|
display: *const c_char,
|
|
view: *const c_char,
|
|
look: *const c_char,
|
|
) -> CHandle {
|
|
// SAFETY: the caller guarantees valid NUL-terminated strings.
|
|
unsafe {
|
|
let display = read_cstr(display);
|
|
let view = read_cstr(view);
|
|
let look = read_cstr(look);
|
|
make_owned(ColorTransform::new_display(&display, &view, &look))
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_colortransform_free` — release one reference.
|
|
pub fn oakcommon_colortransform_free(transform: *mut CHandle) {
|
|
free_handle(transform);
|
|
}
|
|
|
|
/// `oakcommon_colortransform_is_display` — 1/0.
|
|
pub fn oakcommon_colortransform_is_display(transform: CHandle) -> c_int {
|
|
if transform.is_null() {
|
|
return 0;
|
|
}
|
|
// SAFETY: `transform` must be a live `make_owned(ColorTransform)` handle.
|
|
match unsafe { get::<ColorTransform>(&transform) } {
|
|
Some(t) => t.is_display() as c_int,
|
|
None => 0,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_colortransform_get_display` (two-stage string getter).
|
|
pub fn oakcommon_colortransform_get_display(
|
|
transform: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if transform.is_null() || !is_valid_string_out(buf, buf_size) {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `transform` must be a live `make_owned(ColorTransform)` handle.
|
|
match unsafe { get::<ColorTransform>(&transform) } {
|
|
Some(t) => copy_string(t.display(), buf, buf_size),
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_colortransform_get_output` (two-stage string getter).
|
|
pub fn oakcommon_colortransform_get_output(
|
|
transform: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if transform.is_null() || !is_valid_string_out(buf, buf_size) {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `transform` must be a live `make_owned(ColorTransform)` handle.
|
|
match unsafe { get::<ColorTransform>(&transform) } {
|
|
Some(t) => copy_string(t.output(), buf, buf_size),
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_colortransform_get_view` (two-stage string getter).
|
|
pub fn oakcommon_colortransform_get_view(
|
|
transform: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if transform.is_null() || !is_valid_string_out(buf, buf_size) {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `transform` must be a live `make_owned(ColorTransform)` handle.
|
|
match unsafe { get::<ColorTransform>(&transform) } {
|
|
Some(t) => copy_string(t.view(), buf, buf_size),
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcommon_colortransform_get_look` (two-stage string getter).
|
|
pub fn oakcommon_colortransform_get_look(
|
|
transform: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if transform.is_null() || !is_valid_string_out(buf, buf_size) {
|
|
return OAKCOMMON_E_INVALID;
|
|
}
|
|
// SAFETY: `transform` must be a live `make_owned(ColorTransform)` handle.
|
|
match unsafe { get::<ColorTransform>(&transform) } {
|
|
Some(t) => copy_string(t.look(), buf, buf_size),
|
|
None => OAKCOMMON_E_INVALID,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// codec — direct Rust shims over the oakcodec crate
|
|
// ===========================================================================
|
|
|
|
/// oakcodec bridge replacements: direct Rust calls into the `oakcodec`
|
|
/// crate's export tables (single-lib unification). Ported from the
|
|
/// deleted `oakcodec/src/ffi/format.rs` and `ffi/encoder.rs`.
|
|
pub mod codec {
|
|
use std::ffi::{c_char, c_int};
|
|
|
|
use oakcodec::error::{OAKCODEC_E_INVALID, OAKCODEC_E_NOT_FOUND};
|
|
use oakcodec::exportcodec::Codec;
|
|
use oakcodec::exportformat::Format;
|
|
|
|
/// Standard two-stage getter copy: copy only when the buffer is large
|
|
/// enough (never truncates); always return the required size incl. NUL.
|
|
fn string_out(s: &str, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
let need = s.len() as c_int + 1;
|
|
if !buf.is_null() && buf_size > 0 {
|
|
let n = (s.len() as c_int).min(buf_size - 1);
|
|
// SAFETY: the caller guarantees `buf` holds `buf_size` bytes.
|
|
unsafe {
|
|
std::ptr::copy_nonoverlapping(s.as_ptr() as *const c_char, buf, n as usize);
|
|
*buf.add(n as usize) = 0;
|
|
}
|
|
}
|
|
need
|
|
}
|
|
|
|
/// Read a NUL-terminated C string; `None` on NULL pointers.
|
|
unsafe fn c_str(ptr: *const c_char) -> Option<String> {
|
|
// SAFETY: `ptr` must be a valid NUL-terminated C string, or NULL.
|
|
if ptr.is_null() {
|
|
return None;
|
|
}
|
|
unsafe { Some(std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned()) }
|
|
}
|
|
|
|
/// `oakcodec_encoding_format_count` — `ExportFormat::k_format_count`.
|
|
pub fn oakcodec_encoding_format_count() -> c_int {
|
|
Format::Count as c_int
|
|
}
|
|
|
|
/// `oakcodec_encoding_format_name` (two-stage).
|
|
pub unsafe fn oakcodec_encoding_format_name(
|
|
format: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
match Format::from_i32(format) {
|
|
Some(f) => string_out(&Format::get_name(f), buf, buf_size),
|
|
None => OAKCODEC_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcodec_encoding_format_extension` (two-stage).
|
|
pub unsafe fn oakcodec_encoding_format_extension(
|
|
format: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
match Format::from_i32(format) {
|
|
Some(f) => string_out(&Format::get_extension(f), buf, buf_size),
|
|
None => OAKCODEC_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcodec_encoding_format_video_codec_count`.
|
|
pub fn oakcodec_encoding_format_video_codec_count(format: c_int) -> c_int {
|
|
match Format::from_i32(format) {
|
|
Some(f) => Format::get_video_codecs(f).len() as c_int,
|
|
None => OAKCODEC_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcodec_encoding_format_video_codec_at`.
|
|
pub fn oakcodec_encoding_format_video_codec_at(format: c_int, index: c_int) -> c_int {
|
|
let f = match Format::from_i32(format) {
|
|
Some(f) => f,
|
|
None => return OAKCODEC_E_INVALID,
|
|
};
|
|
let list = Format::get_video_codecs(f);
|
|
if index < 0 || index as usize >= list.len() {
|
|
return OAKCODEC_E_NOT_FOUND;
|
|
}
|
|
list[index as usize] as c_int
|
|
}
|
|
|
|
/// `oakcodec_encoding_format_audio_codec_count`.
|
|
pub fn oakcodec_encoding_format_audio_codec_count(format: c_int) -> c_int {
|
|
match Format::from_i32(format) {
|
|
Some(f) => Format::get_audio_codecs(f).len() as c_int,
|
|
None => OAKCODEC_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcodec_encoding_format_audio_codec_at`.
|
|
pub fn oakcodec_encoding_format_audio_codec_at(format: c_int, index: c_int) -> c_int {
|
|
let f = match Format::from_i32(format) {
|
|
Some(f) => f,
|
|
None => return OAKCODEC_E_INVALID,
|
|
};
|
|
let list = Format::get_audio_codecs(f);
|
|
if index < 0 || index as usize >= list.len() {
|
|
return OAKCODEC_E_NOT_FOUND;
|
|
}
|
|
list[index as usize] as c_int
|
|
}
|
|
|
|
/// `oakcodec_encoding_format_subtitle_codec_count`.
|
|
pub fn oakcodec_encoding_format_subtitle_codec_count(format: c_int) -> c_int {
|
|
match Format::from_i32(format) {
|
|
Some(f) => Format::get_subtitle_codecs(f).len() as c_int,
|
|
None => OAKCODEC_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcodec_encoding_format_subtitle_codec_at`.
|
|
pub fn oakcodec_encoding_format_subtitle_codec_at(format: c_int, index: c_int) -> c_int {
|
|
let f = match Format::from_i32(format) {
|
|
Some(f) => f,
|
|
None => return OAKCODEC_E_INVALID,
|
|
};
|
|
let list = Format::get_subtitle_codecs(f);
|
|
if index < 0 || index as usize >= list.len() {
|
|
return OAKCODEC_E_NOT_FOUND;
|
|
}
|
|
list[index as usize] as c_int
|
|
}
|
|
|
|
/// `oakcodec_encoding_codec_name` (two-stage).
|
|
pub unsafe fn oakcodec_encoding_codec_name(
|
|
codec: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
match Codec::from_i32(codec) {
|
|
Some(c) => string_out(&Codec::get_codec_name(c), buf, buf_size),
|
|
None => OAKCODEC_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcodec_encoding_codec_is_still_image` (0 for an invalid codec).
|
|
pub fn oakcodec_encoding_codec_is_still_image(codec: c_int) -> c_int {
|
|
match Codec::from_i32(codec) {
|
|
Some(c) => Codec::is_codec_a_still_image(c) as c_int,
|
|
None => 0,
|
|
}
|
|
}
|
|
|
|
/// `oakcodec_encoding_codec_is_lossless` (0 for an invalid codec).
|
|
pub fn oakcodec_encoding_codec_is_lossless(codec: c_int) -> c_int {
|
|
match Codec::from_i32(codec) {
|
|
Some(c) => Codec::is_codec_lossless(c) as c_int,
|
|
None => 0,
|
|
}
|
|
}
|
|
|
|
/// `oakcodec_encoding_pix_fmt_count`.
|
|
///
|
|
/// # CPP-PARITY
|
|
/// The Rust table is empty (see
|
|
/// `Format::get_pixel_formats_for_codec`), so the count is 0 — the same
|
|
/// as the C++ base `Encoder` default.
|
|
pub fn oakcodec_encoding_pix_fmt_count(format: c_int, codec: c_int) -> c_int {
|
|
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
|
|
(Some(f), Some(c)) => (f, c),
|
|
_ => return OAKCODEC_E_INVALID,
|
|
};
|
|
Format::get_pixel_formats_for_codec(f, c).len() as c_int
|
|
}
|
|
|
|
/// `oakcodec_encoding_pix_fmt_at` (two-stage).
|
|
pub unsafe fn oakcodec_encoding_pix_fmt_at(
|
|
format: c_int,
|
|
codec: c_int,
|
|
index: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
|
|
(Some(f), Some(c)) => (f, c),
|
|
_ => return OAKCODEC_E_INVALID,
|
|
};
|
|
let list = Format::get_pixel_formats_for_codec(f, c);
|
|
if index < 0 || index as usize >= list.len() {
|
|
return OAKCODEC_E_NOT_FOUND;
|
|
}
|
|
// Interim: the Rust list carries no names yet, so this arm is
|
|
// unreachable while the list is empty (C++ queries the FFmpeg bridge).
|
|
string_out(&list[index as usize].to_string(), buf, buf_size)
|
|
}
|
|
|
|
/// `oakcodec_encoding_pix_fmt_index` — 0 (the preferred format) for an
|
|
/// invalid codec, a NULL/empty `pix_fmt`, or when not found.
|
|
pub unsafe fn oakcodec_encoding_pix_fmt_index(codec: c_int, pix_fmt: *const c_char) -> c_int {
|
|
// SAFETY: `pix_fmt` is a valid NUL-terminated C string or NULL.
|
|
unsafe {
|
|
if Codec::from_i32(codec).is_none() {
|
|
return 0;
|
|
}
|
|
match c_str(pix_fmt) {
|
|
Some(s) if !s.is_empty() => {
|
|
// Interim: empty table (see module doc) -> preferred index 0.
|
|
let _ = s;
|
|
0
|
|
}
|
|
_ => 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `oakcodec_encoding_sample_format_count`.
|
|
pub fn oakcodec_encoding_sample_format_count(format: c_int, codec: c_int) -> c_int {
|
|
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
|
|
(Some(f), Some(c)) => (f, c),
|
|
_ => return OAKCODEC_E_INVALID,
|
|
};
|
|
Format::get_sample_formats_for_codec(f, c).len() as c_int
|
|
}
|
|
|
|
/// `oakcodec_encoding_sample_format_at` — an
|
|
/// `olive::core::SampleFormat::Format` value.
|
|
pub fn oakcodec_encoding_sample_format_at(
|
|
format: c_int,
|
|
codec: c_int,
|
|
index: c_int,
|
|
) -> c_int {
|
|
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
|
|
(Some(f), Some(c)) => (f, c),
|
|
_ => return OAKCODEC_E_INVALID,
|
|
};
|
|
let list = Format::get_sample_formats_for_codec(f, c);
|
|
if index < 0 || index as usize >= list.len() {
|
|
return OAKCODEC_E_NOT_FOUND;
|
|
}
|
|
list[index as usize] as c_int
|
|
}
|
|
|
|
/// `oakcodec_encoding_filename_contains_digit_placeholder` (0 for NULL).
|
|
pub unsafe fn oakcodec_encoding_filename_contains_digit_placeholder(
|
|
filename: *const c_char,
|
|
) -> c_int {
|
|
// SAFETY: `filename` is a valid NUL-terminated C string or NULL.
|
|
match unsafe { c_str(filename) } {
|
|
Some(f) => oakcodec::encoder::filename_contains_digit_placeholder(&f) as c_int,
|
|
None => 0,
|
|
}
|
|
}
|
|
|
|
/// `oakcodec_encoding_image_sequence_digit_count` (0 for NULL).
|
|
pub unsafe fn oakcodec_encoding_image_sequence_digit_count(filename: *const c_char) -> c_int {
|
|
// SAFETY: `filename` is a valid NUL-terminated C string or NULL.
|
|
match unsafe { c_str(filename) } {
|
|
Some(f) => oakcodec::encoder::image_sequence_placeholder_digit_count(&f),
|
|
None => 0,
|
|
}
|
|
}
|
|
|
|
/// `oakcodec_encoding_filename_remove_digit_placeholder` (two-stage).
|
|
pub unsafe fn oakcodec_encoding_filename_remove_digit_placeholder(
|
|
filename: *const c_char,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
// SAFETY: `filename` is a valid NUL-terminated C string or NULL
|
|
// (NULL is the documented E_INVALID path, matching the deleted
|
|
// module ffi).
|
|
match unsafe { c_str(filename) } {
|
|
Some(f) => {
|
|
string_out(&oakcodec::encoder::filename_remove_digit_placeholder(&f), buf, buf_size)
|
|
}
|
|
None => OAKCODEC_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oakcodec_encoding_generate_matrix`: scaling matrix for a scaling
|
|
/// method, row-major 4x4 `double` into `out_matrix[16]`.
|
|
pub fn oakcodec_encoding_generate_matrix(
|
|
method: c_int,
|
|
src_width: c_int,
|
|
src_height: c_int,
|
|
dst_width: c_int,
|
|
dst_height: c_int,
|
|
out_matrix: *mut f64,
|
|
) -> c_int {
|
|
if out_matrix.is_null() {
|
|
return OAKCODEC_E_INVALID;
|
|
}
|
|
let method = match method {
|
|
0 => oakcodec::encodingparams::VideoScalingMethod::Fit,
|
|
2 => oakcodec::encodingparams::VideoScalingMethod::Crop,
|
|
_ => oakcodec::encodingparams::VideoScalingMethod::Stretch,
|
|
};
|
|
let mut m = [0.0f64; 16];
|
|
oakcodec::encodingparams::EncodingParams::generate_matrix(
|
|
method,
|
|
src_width,
|
|
src_height,
|
|
dst_width,
|
|
dst_height,
|
|
&mut m,
|
|
);
|
|
// SAFETY: the caller guarantees `out_matrix` holds 16 doubles.
|
|
unsafe { std::ptr::copy_nonoverlapping(m.as_ptr(), out_matrix, 16) };
|
|
0
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// node — engine-side oaknode domain implementation (single-lib unification)
|
|
// ===========================================================================
|
|
//
|
|
// The deleted oaknode C ABI is replaced by direct calls into the oaknode
|
|
// crate's Rust domain. The engine's handle layer boxes the domain behind
|
|
// the upward CHandles (see `crate::handle::domain`):
|
|
//
|
|
// - project handles box `Arc<Mutex<oaknode::project::Project>>`;
|
|
// - node/block/track/footage/sequence/folder handles box
|
|
// `oaknode::project::NodeRef` (project + NodeId).
|
|
//
|
|
// Undoable creators return handles boxing
|
|
// `oakundo::undocommand::UndoCommand` values (built from redo/undo
|
|
// closures over the real graph). Return-code convention: 0 on success,
|
|
// negative `oaknode::error::OAKNODE_*` codes on failure; two-stage string
|
|
// getters report the required length **including** the NUL.
|
|
pub mod node {
|
|
use std::collections::HashMap;
|
|
use std::ffi::{c_char, c_int, c_void};
|
|
use std::sync::atomic::{AtomicI64, Ordering};
|
|
use std::sync::{Arc, Mutex, OnceLock, Weak};
|
|
|
|
use oakcore_rs::{Rational, TimeRange};
|
|
use oaknode::error::{
|
|
OAKNODE_E_FAILED, OAKNODE_E_INVALID, OAKNODE_E_NOMEM, OAKNODE_E_NOT_FOUND, OAKNODE_E_STATE,
|
|
OAKNODE_OK,
|
|
};
|
|
use oaknode::factory::Factory;
|
|
use oaknode::graph::Graph;
|
|
use oaknode::id::NodeId;
|
|
use oakundo::undocommand::{command_from_owned, OakUndoCommandVtable, UndoCommand};
|
|
|
|
use crate::handle::domain::{box_node, node_ref_mut, node_ref_of, project_of, ProjectArc};
|
|
use crate::handle::CHandle;
|
|
|
|
// -------------------------------------------------------------------
|
|
// Shared helpers
|
|
// -------------------------------------------------------------------
|
|
|
|
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
|
m.lock().unwrap_or_else(|e| e.into_inner())
|
|
}
|
|
|
|
/// Standard two-stage getter copy: copy only when the buffer is large
|
|
/// enough (never truncates); always return the required size incl. NUL.
|
|
fn string_out(s: &str, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
let required = (s.len() + 1) as c_int;
|
|
if !buf.is_null() && buf_size >= required {
|
|
// SAFETY: the caller guarantees `buf` holds `buf_size` bytes.
|
|
unsafe {
|
|
std::ptr::copy_nonoverlapping(s.as_ptr() as *const c_char, buf, s.len());
|
|
*buf.add(s.len()) = 0;
|
|
}
|
|
}
|
|
required
|
|
}
|
|
|
|
/// Read a NUL-terminated C string; NULL -> empty.
|
|
unsafe fn cstr(ptr: *const c_char) -> String {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
unsafe { crate::handle::read_cstr(ptr) }
|
|
}
|
|
|
|
/// A handle's `addref` copy (borrowed-view semantics: every facade
|
|
/// "borrowed" handle is an owned copy with refcount 1 that the caller
|
|
/// releases).
|
|
fn addref_copy(h: CHandle) -> CHandle {
|
|
if h.is_null() {
|
|
return CHandle::null();
|
|
}
|
|
if let Some(addref) = h.addref {
|
|
// SAFETY: `h` is a live handle.
|
|
unsafe { addref(h.ctx) };
|
|
}
|
|
h
|
|
}
|
|
|
|
/// Node identity (the graph slot identity; 0 for invalid handles).
|
|
fn node_identity(h: CHandle) -> usize {
|
|
// SAFETY: node handles box NodeRef payloads.
|
|
match unsafe { node_ref_of(&h) } {
|
|
Some(nr) => nr.id.identity() as usize,
|
|
None => 0,
|
|
}
|
|
}
|
|
|
|
/// Read-only project access.
|
|
fn with_project<R>(h: CHandle, f: impl FnOnce(&oaknode::project::Project) -> R) -> Option<R> {
|
|
// SAFETY: project handles box ProjectArc payloads.
|
|
let p = unsafe { project_of(&h) }?;
|
|
Some(f(&lock(p)))
|
|
}
|
|
|
|
/// Mutable project access.
|
|
fn with_project_mut<R>(
|
|
h: CHandle,
|
|
f: impl FnOnce(&mut oaknode::project::Project) -> R,
|
|
) -> Option<R> {
|
|
// SAFETY: project handles box ProjectArc payloads.
|
|
let p = unsafe { project_of(&h) }?;
|
|
Some(f(&mut lock(p)))
|
|
}
|
|
|
|
/// Read-only graph access for a node handle.
|
|
fn with_node<R>(h: CHandle, f: impl FnOnce(&Graph, NodeId) -> R) -> Option<R> {
|
|
// SAFETY: node handles box NodeRef payloads.
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
let p = lock(&nr.project);
|
|
Some(f(&p.graph, nr.id))
|
|
}
|
|
|
|
/// Mutable graph access for a node handle.
|
|
fn with_node_mut<R>(h: CHandle, f: impl FnOnce(&mut Graph, NodeId) -> R) -> Option<R> {
|
|
// SAFETY: node handles box NodeRef payloads.
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
let mut p = lock(&nr.project);
|
|
Some(f(&mut p.graph, nr.id))
|
|
}
|
|
|
|
/// Typed behavior view of a graph node.
|
|
fn behavior_of<'a, T: 'static>(g: &'a Graph, id: NodeId) -> Option<&'a T> {
|
|
g.get(id)
|
|
.and_then(|e| e.behavior.as_any())
|
|
.and_then(|a| a.downcast_ref::<T>())
|
|
}
|
|
|
|
/// Typed mutable behavior view of a graph node.
|
|
fn behavior_of_mut<'a, T: 'static>(g: &'a mut Graph, id: NodeId) -> Option<&'a mut T> {
|
|
g.get_mut(id)
|
|
.and_then(|e| e.behavior.as_any_mut())
|
|
.and_then(|a| a.downcast_mut::<T>())
|
|
}
|
|
|
|
/// The node's type id (empty when invalid).
|
|
fn type_id_of(h: CHandle) -> String {
|
|
match with_node(h, |g, id| {
|
|
g.get(id).map(|e| e.behavior.type_id().to_string())
|
|
}) {
|
|
Some(Some(s)) => s,
|
|
_ => String::new(),
|
|
}
|
|
}
|
|
|
|
fn is_type(h: CHandle, id: &str) -> bool {
|
|
type_id_of(h) == id
|
|
}
|
|
|
|
// ---- Debug alive counter (the deleted `oaknode_debug_alive_count`) ----
|
|
|
|
/// Live detached nodes + live projects (factory-created nodes that
|
|
/// were not adopted by a project graph, plus projects themselves).
|
|
static ALIVE: AtomicI64 = AtomicI64::new(0);
|
|
|
|
fn alive_inc() {
|
|
ALIVE.fetch_add(1, Ordering::SeqCst);
|
|
}
|
|
|
|
fn alive_dec() {
|
|
ALIVE.fetch_sub(1, Ordering::SeqCst);
|
|
}
|
|
|
|
/// Weak references to every alive-counted project (for the release
|
|
/// bookkeeping below).
|
|
static ALIVE_REGISTRY: OnceLock<
|
|
Mutex<HashMap<usize, Weak<Mutex<oaknode::project::Project>>>>,
|
|
> = OnceLock::new();
|
|
|
|
fn registry() -> std::sync::MutexGuard<
|
|
'static,
|
|
HashMap<usize, Weak<Mutex<oaknode::project::Project>>>,
|
|
> {
|
|
ALIVE_REGISTRY
|
|
.get_or_init(|| Mutex::new(HashMap::new()))
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
}
|
|
|
|
/// Release callback for project payload boxes (refcounted shell +
|
|
/// alive-counter bookkeeping).
|
|
unsafe extern "C" fn release_project_box(ctx: *mut c_void) {
|
|
// SAFETY: `ctx` was produced by `make_project_handle` and this
|
|
// callback runs once per shell reference.
|
|
unsafe {
|
|
if ctx.is_null() {
|
|
return;
|
|
}
|
|
let rb = ctx as *mut oaknode::handle::RefBox<ProjectArc>;
|
|
if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
|
|
let value = Box::from_raw(rb).value;
|
|
let key = Arc::as_ptr(&value) as usize;
|
|
drop(value);
|
|
// Last strong reference: retire the alive-count entry.
|
|
let dead = match registry().get(&key) {
|
|
Some(weak) => weak.upgrade().is_none(),
|
|
None => false,
|
|
};
|
|
if dead {
|
|
registry().remove(&key);
|
|
alive_dec();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Box a project payload, registering it in the alive registry.
|
|
fn make_project_handle(project: ProjectArc) -> CHandle {
|
|
let key = Arc::as_ptr(&project) as usize;
|
|
registry().entry(key).or_insert_with(|| {
|
|
alive_inc();
|
|
Arc::downgrade(&project)
|
|
});
|
|
// SAFETY: custom release callback for the payload box.
|
|
unsafe { oaknode::handle::make_owned_with(project, release_project_box) }
|
|
}
|
|
|
|
/// Box a node reference behind a refcounted handle (detached-node
|
|
/// accounting rides on the payload's `owned` flag).
|
|
fn make_node_handle(project: ProjectArc, id: NodeId, owned: bool) -> CHandle {
|
|
box_node(project, id, owned)
|
|
}
|
|
|
|
/// Box a project payload for the engine's other stub families (the
|
|
/// task module's result paths).
|
|
pub(crate) fn box_project_handle(project: ProjectArc) -> CHandle {
|
|
make_project_handle(project)
|
|
}
|
|
|
|
/// Box a node reference for the engine's other stub families (the
|
|
/// task module's footage result paths).
|
|
pub(crate) fn box_node_handle(project: ProjectArc, id: NodeId, owned: bool) -> CHandle {
|
|
make_node_handle(project, id, owned)
|
|
}
|
|
|
|
/// The hidden scratch project holding detached (factory-created)
|
|
/// nodes (the C++ `memory_manager_` analogue; leaked for the process).
|
|
fn scratch_project() -> ProjectArc {
|
|
static SCRATCH: OnceLock<ProjectArc> = OnceLock::new();
|
|
SCRATCH
|
|
.get_or_init(|| oaknode::project::Project::new())
|
|
.clone()
|
|
}
|
|
|
|
/// Create a detached node in the scratch project (owned = true,
|
|
/// alive-counted).
|
|
fn make_detached(
|
|
(core, behavior): (oaknode::node::NodeCore, Box<dyn oaknode::node::NodeBehavior>),
|
|
) -> CHandle {
|
|
let project = scratch_project();
|
|
let id = {
|
|
let mut p = lock(&project);
|
|
p.graph.add_node(core, behavior)
|
|
};
|
|
alive_inc();
|
|
make_node_handle(project, id, true)
|
|
}
|
|
|
|
/// Release a handle shell reference.
|
|
fn release_handle(h: CHandle) {
|
|
if let Some(release) = h.release {
|
|
// SAFETY: the handle is live and this is the caller's
|
|
// reference.
|
|
unsafe { release(h.ctx) };
|
|
}
|
|
}
|
|
|
|
/// Free a node handle shell; a still-owned (detached) node is removed
|
|
/// from its scratch graph and un-counted. Adopted nodes live in their
|
|
/// project graph (the graph owns them).
|
|
fn free_node_handle(h: CHandle) {
|
|
// SAFETY: node handles box NodeRef payloads.
|
|
let owned = unsafe { node_ref_of(&h) }
|
|
.map(|nr| nr.owned.load(Ordering::SeqCst))
|
|
.unwrap_or(false);
|
|
if owned {
|
|
let removed = unsafe { node_ref_of(&h) }.and_then(|nr| {
|
|
let mut p = lock(&nr.project);
|
|
p.graph.remove_node(nr.id).map(|_| ())
|
|
});
|
|
if removed.is_some() {
|
|
alive_dec();
|
|
}
|
|
}
|
|
release_handle(h);
|
|
}
|
|
|
|
// ---- Undo-command scaffolding --------------------------------------
|
|
|
|
/// Userdata payload behind a closure-backed undo command.
|
|
struct ClosureCommand {
|
|
redo: Box<dyn FnMut() + Send>,
|
|
undo: Box<dyn FnMut() + Send>,
|
|
}
|
|
|
|
unsafe extern "C" fn closure_redo(ud: *mut c_void) {
|
|
// SAFETY: `ud` is the `ClosureCommand` box owned by the command.
|
|
let c = unsafe { &mut *(ud as *mut ClosureCommand) };
|
|
(c.redo)();
|
|
}
|
|
|
|
unsafe extern "C" fn closure_undo(ud: *mut c_void) {
|
|
// SAFETY: see `closure_redo`.
|
|
let c = unsafe { &mut *(ud as *mut ClosureCommand) };
|
|
(c.undo)();
|
|
}
|
|
|
|
unsafe extern "C" fn closure_free(ud: *mut c_void) {
|
|
if !ud.is_null() {
|
|
// SAFETY: the box is destroyed exactly once, by the command.
|
|
unsafe { drop(Box::from_raw(ud as *mut ClosureCommand)) };
|
|
}
|
|
}
|
|
|
|
/// Build an un-executed [`UndoCommand`] from redo/undo closures.
|
|
fn closure_command(
|
|
redo: impl FnMut() + Send + 'static,
|
|
undo: impl FnMut() + Send + 'static,
|
|
) -> UndoCommand {
|
|
let ud = Box::into_raw(Box::new(ClosureCommand {
|
|
redo: Box::new(redo),
|
|
undo: Box::new(undo),
|
|
}));
|
|
UndoCommand::from_vtable(
|
|
OakUndoCommandVtable {
|
|
redo: Some(closure_redo),
|
|
undo: Some(closure_undo),
|
|
free_fn: Some(closure_free),
|
|
},
|
|
ud as *mut c_void,
|
|
)
|
|
}
|
|
|
|
/// Box an [`UndoCommand`] value behind a handle.
|
|
fn box_command(cmd: UndoCommand) -> CHandle {
|
|
// SAFETY: `command_from_owned` owns the command value.
|
|
unsafe { command_from_owned(cmd) }
|
|
}
|
|
|
|
/// Create a multi command handle from children.
|
|
fn box_multi(children: Vec<UndoCommand>) -> CHandle {
|
|
let mut multi = UndoCommand::multi();
|
|
for c in children {
|
|
multi.multi_add_child(c);
|
|
}
|
|
box_command(multi)
|
|
}
|
|
|
|
// ---- Value conversions ----------------------------------------------
|
|
|
|
/// Map an oaknode domain `VideoParams` into an oakcommon params
|
|
/// handle (the engine's `stubs::common` videoparams surface).
|
|
fn vp_handle(v: &oaknode::value::VideoParams) -> CHandle {
|
|
let h = crate::stubs::common::oakcommon_videoparams_init();
|
|
if h.is_null() {
|
|
return CHandle::null();
|
|
}
|
|
crate::stubs::common::oakcommon_videoparams_set_width(h, v.width);
|
|
crate::stubs::common::oakcommon_videoparams_set_height(h, v.height);
|
|
crate::stubs::common::oakcommon_videoparams_set_format(h, v.pixel_format);
|
|
crate::stubs::common::oakcommon_videoparams_set_channel_count(h, v.channels);
|
|
let (n, d) = (
|
|
v.frame_rate.numerator() as c_int,
|
|
v.frame_rate.denominator() as c_int,
|
|
);
|
|
crate::stubs::common::oakcommon_videoparams_set_frame_rate(h, n, d);
|
|
if n > 0 {
|
|
crate::stubs::common::oakcommon_videoparams_set_time_base(h, d, n);
|
|
}
|
|
h
|
|
}
|
|
|
|
/// Read an oakcommon params handle back into the domain type.
|
|
unsafe fn vp_from_handle(h: CHandle) -> Option<oaknode::value::VideoParams> {
|
|
let mut width: c_int = 0;
|
|
let mut height: c_int = 0;
|
|
let mut format: c_int = 0;
|
|
let mut channels: c_int = 0;
|
|
let mut fr_num: c_int = 0;
|
|
let mut fr_den: c_int = 0;
|
|
if crate::stubs::common::oakcommon_videoparams_get_width(h, &mut width) != 0
|
|
|| crate::stubs::common::oakcommon_videoparams_get_height(h, &mut height) != 0
|
|
|| crate::stubs::common::oakcommon_videoparams_get_format(h, &mut format) != 0
|
|
|| crate::stubs::common::oakcommon_videoparams_get_channel_count(h, &mut channels) != 0
|
|
|| crate::stubs::common::oakcommon_videoparams_get_frame_rate(
|
|
h,
|
|
&mut fr_num,
|
|
&mut fr_den,
|
|
) != 0
|
|
{
|
|
return None;
|
|
}
|
|
Some(oaknode::value::VideoParams {
|
|
width,
|
|
height,
|
|
frame_rate: Rational::new(fr_num as i64, fr_den as i64),
|
|
pixel_format: format,
|
|
channels,
|
|
})
|
|
}
|
|
|
|
/// POD -> domain value (using the input's declared type).
|
|
fn pod_to_value(
|
|
declared: oaknode::value::ValueType,
|
|
v: crate::node::OakNodeValue,
|
|
) -> Option<oaknode::value::NodeValue> {
|
|
v.to_node_value(declared).ok()
|
|
}
|
|
|
|
/// Domain value -> POD (using the input's declared type).
|
|
fn value_to_pod(
|
|
declared: oaknode::value::ValueType,
|
|
v: &oaknode::value::NodeValue,
|
|
) -> Option<crate::node::OakNodeValue> {
|
|
crate::node::OakNodeValue::from_node_value(declared, v).ok()
|
|
}
|
|
// -------------------------------------------------------------------
|
|
// Project family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// `oaknode_project_init` — fresh uninitialized project box.
|
|
pub fn oaknode_project_init() -> CHandle {
|
|
make_project_handle(oaknode::project::Project::new())
|
|
}
|
|
|
|
/// `oaknode_project_free` — release one project-handle reference.
|
|
pub fn oaknode_project_free(project: *mut CHandle) {
|
|
if project.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *project };
|
|
release_handle(h);
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *project = CHandle::null() };
|
|
}
|
|
|
|
/// `oaknode_project_initialize` — create the root folder.
|
|
pub fn oaknode_project_initialize(project: CHandle) -> c_int {
|
|
match with_project_mut(project, |p| p.initialize()) {
|
|
Some(Ok(())) => OAKNODE_OK,
|
|
Some(Err(e)) => e.code(),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_clear` — remove all nodes, reset to blank.
|
|
pub fn oaknode_project_clear(project: CHandle) -> c_int {
|
|
match with_project_mut(project, |p| p.clear()) {
|
|
Some(Ok(())) => OAKNODE_OK,
|
|
Some(Err(e)) => e.code(),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_root` — borrowed root folder handle (null until
|
|
/// initialized).
|
|
pub fn oaknode_project_root(project: CHandle) -> CHandle {
|
|
let p = match unsafe { project_of(&project) }.cloned() {
|
|
Some(p) => p,
|
|
None => return CHandle::null(),
|
|
};
|
|
let root = {
|
|
let g = lock(&p);
|
|
g.root
|
|
};
|
|
if root.valid() {
|
|
make_node_handle(p, root, false)
|
|
} else {
|
|
CHandle::null()
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_name` (two-stage).
|
|
pub fn oaknode_project_name(project: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
match with_project(project, |p| p.name()) {
|
|
Some(name) => string_out(&name, buf, buf_size),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_filename` (two-stage).
|
|
pub fn oaknode_project_filename(project: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
match with_project(project, |p| p.filename().to_string()) {
|
|
Some(name) => string_out(&name, buf, buf_size),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_pretty_filename` (two-stage).
|
|
pub fn oaknode_project_pretty_filename(
|
|
project: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
match with_project(project, |p| p.pretty_filename().to_string()) {
|
|
Some(name) => string_out(&name, buf, buf_size),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_set_filename`.
|
|
pub fn oaknode_project_set_filename(project: CHandle, filename: *const c_char) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let filename = unsafe { cstr(filename) };
|
|
match with_project_mut(project, |p| p.set_filename(&filename)) {
|
|
Some(()) => OAKNODE_OK,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_is_modified` — 1/0.
|
|
pub fn oaknode_project_is_modified(project: CHandle) -> c_int {
|
|
match with_project(project, |p| p.is_modified()) {
|
|
Some(true) => 1,
|
|
Some(false) => 0,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_set_modified`.
|
|
pub fn oaknode_project_set_modified(project: CHandle, modified: c_int) -> c_int {
|
|
match with_project_mut(project, |p| p.set_modified(modified != 0)) {
|
|
Some(()) => OAKNODE_OK,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_is_new` — 1/0.
|
|
pub fn oaknode_project_is_new(project: CHandle) -> c_int {
|
|
match with_project(project, |p| p.is_new()) {
|
|
Some(true) => 1,
|
|
Some(false) => 0,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_cache_path` (two-stage).
|
|
pub fn oaknode_project_cache_path(
|
|
project: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
match with_project(project, |p| p.cache_path()) {
|
|
Some(path) => string_out(&path, buf, buf_size),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_copy_settings`.
|
|
pub fn oaknode_project_copy_settings(dst: CHandle, src: CHandle) -> c_int {
|
|
let src_proj = unsafe { project_of(&src) }.cloned();
|
|
match src_proj {
|
|
Some(src_proj) => {
|
|
let (settings, location, custom) = {
|
|
let s = lock(&src_proj);
|
|
(
|
|
s.settings.clone(),
|
|
s.cache_location_setting,
|
|
s.custom_cache_path.clone(),
|
|
)
|
|
};
|
|
match with_project_mut(dst, |d| {
|
|
d.settings = settings;
|
|
d.cache_location_setting = location;
|
|
d.custom_cache_path = custom;
|
|
}) {
|
|
Some(()) => OAKNODE_OK,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_get_cache_location_setting`.
|
|
pub fn oaknode_project_get_cache_location_setting(project: CHandle) -> c_int {
|
|
match with_project(project, |p| p.cache_location_setting) {
|
|
Some(v) => v,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_set_cache_location_setting`.
|
|
pub fn oaknode_project_set_cache_location_setting(project: CHandle, setting: c_int) -> c_int {
|
|
match with_project_mut(project, |p| p.cache_location_setting = setting) {
|
|
Some(()) => OAKNODE_OK,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_get_custom_cache_path` (two-stage).
|
|
pub fn oaknode_project_get_custom_cache_path(
|
|
project: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
match with_project(project, |p| p.custom_cache_path.clone()) {
|
|
Some(path) => string_out(&path, buf, buf_size),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_set_custom_cache_path`.
|
|
pub fn oaknode_project_set_custom_cache_path(project: CHandle, path: *const c_char) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let path = unsafe { cstr(path) };
|
|
match with_project_mut(project, |p| p.custom_cache_path = path) {
|
|
Some(()) => OAKNODE_OK,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_get_uuid` (two-stage).
|
|
pub fn oaknode_project_get_uuid(project: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
match with_project(project, |p| p.uuid.clone()) {
|
|
Some(uuid) => string_out(&uuid, buf, buf_size),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_add_node` — move `node` into `project`'s graph
|
|
/// (live; the shared node box is rewritten in place so every handle
|
|
/// copy sees the new home — the C++ `write_node_ref` semantics).
|
|
pub fn oaknode_project_add_node(project: CHandle, node: CHandle) -> c_int {
|
|
let target = match unsafe { project_of(&project) }.cloned() {
|
|
Some(t) => t,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => nr,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let src_project = nr.project.clone();
|
|
let src_id = nr.id;
|
|
// Already in the target graph: nothing to do.
|
|
if Arc::ptr_eq(&src_project, &target) && { lock(&target).graph.is_valid(src_id) } {
|
|
return OAKNODE_OK;
|
|
}
|
|
let entry = {
|
|
let mut s = lock(&src_project);
|
|
s.graph.take_node(src_id)
|
|
};
|
|
let Some(entry) = entry else {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
};
|
|
let new_id = {
|
|
let mut t = lock(&target);
|
|
t.graph.add_entry(entry, src_id)
|
|
};
|
|
// SAFETY: node handles box NodeRef payloads; the box is shared by
|
|
// every handle copy.
|
|
if let Some(boxed) = unsafe { node_ref_mut(&node) } {
|
|
boxed.project = target;
|
|
boxed.id = new_id;
|
|
if boxed.owned.swap(false, Ordering::SeqCst) {
|
|
alive_dec();
|
|
}
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_project_remove_node` — live removal from the graph.
|
|
pub fn oaknode_project_remove_node(project: CHandle, node: CHandle) -> c_int {
|
|
let target = match unsafe { project_of(&project) }.cloned() {
|
|
Some(t) => t,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => nr,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
if !Arc::ptr_eq(&nr.project, &target) {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
let mut t = lock(&target);
|
|
if t.graph.remove_node(nr.id).is_some() {
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_node_count`.
|
|
pub fn oaknode_project_node_count(project: CHandle) -> c_int {
|
|
match with_project(project, |p| p.graph.node_count()) {
|
|
Some(n) => n as c_int,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_project_node_at` — borrowed node view at `index` (null
|
|
/// out of range).
|
|
pub fn oaknode_project_node_at(project: CHandle, index: c_int) -> CHandle {
|
|
if index < 0 {
|
|
return CHandle::null();
|
|
}
|
|
let p = match unsafe { project_of(&project) }.cloned() {
|
|
Some(p) => p,
|
|
None => return CHandle::null(),
|
|
};
|
|
let id = {
|
|
let g = lock(&p);
|
|
g.graph.node_ids().get(index as usize).copied()
|
|
};
|
|
match id {
|
|
Some(id) => make_node_handle(p, id, false),
|
|
None => CHandle::null(),
|
|
}
|
|
}
|
|
|
|
/// `oaknode_debug_alive_count` — live detached nodes + projects.
|
|
pub fn oaknode_debug_alive_count() -> c_int {
|
|
ALIVE.load(Ordering::SeqCst) as c_int
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Node family — metadata
|
|
// -------------------------------------------------------------------
|
|
|
|
/// `oaknode_node_get_id` (two-stage): the type id.
|
|
pub fn oaknode_node_get_id(node: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
match with_node(node, |g, id| {
|
|
g.get(id).map(|e| e.behavior.type_id().to_string())
|
|
}) {
|
|
Some(Some(s)) => string_out(&s, buf, buf_size),
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_get_name` (two-stage).
|
|
pub fn oaknode_node_get_name(node: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
match with_node(node, |g, id| {
|
|
g.get(id).map(|e| e.behavior.name().to_string())
|
|
}) {
|
|
Some(Some(s)) => string_out(&s, buf, buf_size),
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_get_label` (two-stage).
|
|
pub fn oaknode_node_get_label(node: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
match with_node(node, |g, id| g.get(id).map(|e| e.core.label.clone())) {
|
|
Some(Some(s)) => string_out(&s, buf, buf_size),
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_set_label`.
|
|
pub fn oaknode_node_set_label(node: CHandle, label: *const c_char) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let label = unsafe { cstr(label) };
|
|
match with_node_mut(node, |g, id| {
|
|
g.get_mut(id).map(|e| e.core.label = label.clone())
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_set_label_undoable` — closure-backed rename.
|
|
pub fn oaknode_node_set_label_undoable(
|
|
node: CHandle,
|
|
label: *const c_char,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let label = unsafe { cstr(label) };
|
|
if out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let old = match {
|
|
let g = lock(&project);
|
|
g.graph.get(id).map(|e| e.core.label.clone())
|
|
} {
|
|
Some(old) => old,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let label1 = label.clone();
|
|
let old1 = old.clone();
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
e.core.label = label1.clone();
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
e.core.label = old1.clone();
|
|
}
|
|
},
|
|
);
|
|
// SAFETY: `out_command` is a valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_node_get_override_color`.
|
|
pub fn oaknode_node_get_override_color(node: CHandle, out_value: *mut c_int) -> c_int {
|
|
if out_value.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(node, |g, id| g.get(id).map(|e| e.core.override_color)) {
|
|
Some(Some(v)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_value = v };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_set_override_color`.
|
|
pub fn oaknode_node_set_override_color(node: CHandle, index: c_int) -> c_int {
|
|
match with_node_mut(node, |g, id| {
|
|
g.get_mut(id).map(|e| e.core.override_color = index)
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_set_override_color_undoable`.
|
|
pub fn oaknode_node_set_override_color_undoable(
|
|
node: CHandle,
|
|
index: c_int,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let old = match {
|
|
let g = lock(&project);
|
|
g.graph.get(id).map(|e| e.core.override_color)
|
|
} {
|
|
Some(old) => old,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
e.core.override_color = index;
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
e.core.override_color = old;
|
|
}
|
|
},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// The `enabled_in` boolean (1/0; default true).
|
|
fn node_enabled(g: &Graph, id: NodeId) -> Option<bool> {
|
|
let e = g.get(id)?;
|
|
match e.core.standard_value(oaknode::node::ENABLED_INPUT, -1) {
|
|
oaknode::value::NodeValue::Boolean(b) => Some(b),
|
|
_ => Some(true),
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_is_enabled`.
|
|
pub fn oaknode_node_is_enabled(node: CHandle, out_value: *mut c_int) -> c_int {
|
|
if out_value.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(node, |g, id| node_enabled(g, id)) {
|
|
Some(Some(enabled)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_value = if enabled { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_set_enabled`.
|
|
pub fn oaknode_node_set_enabled(node: CHandle, enabled: c_int) -> c_int {
|
|
match with_node_mut(node, |g, id| {
|
|
g.get_mut(id).map(|e| {
|
|
e.core.set_standard_value(
|
|
oaknode::node::ENABLED_INPUT,
|
|
-1,
|
|
oaknode::value::NodeValue::Boolean(enabled != 0),
|
|
)
|
|
})
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_set_enabled_undoable`.
|
|
pub fn oaknode_node_set_enabled_undoable(
|
|
node: CHandle,
|
|
enabled: c_int,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let old = match {
|
|
let g = lock(&project);
|
|
node_enabled(&g.graph, id)
|
|
} {
|
|
Some(old) => old,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
e.core.set_standard_value(
|
|
oaknode::node::ENABLED_INPUT,
|
|
-1,
|
|
oaknode::value::NodeValue::Boolean(enabled != 0),
|
|
);
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
e.core.set_standard_value(
|
|
oaknode::node::ENABLED_INPUT,
|
|
-1,
|
|
oaknode::value::NodeValue::Boolean(old),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_node_get_effect_input` (two-stage).
|
|
pub fn oaknode_node_get_effect_input(
|
|
node: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
match with_node(node, |g, id| g.get(id).map(|e| e.core.effect_input.clone())) {
|
|
Some(Some(s)) => string_out(&s, buf, buf_size),
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_get_flags`.
|
|
pub fn oaknode_node_get_flags(node: CHandle) -> u64 {
|
|
match with_node(node, |g, id| g.get(id).map(|e| e.core.flags)) {
|
|
Some(Some(f)) => f,
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_input_count`.
|
|
pub fn oaknode_node_input_count(node: CHandle, out_count: *mut c_int) -> c_int {
|
|
if out_count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(node, |g, id| g.get(id).map(|e| e.core.inputs.len())) {
|
|
Some(Some(n)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_count = n as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_input_id` (two-stage).
|
|
pub fn oaknode_node_input_id(
|
|
node: CHandle,
|
|
index: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if index < 0 {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
match with_node(node, |g, id| {
|
|
g.get(id)
|
|
.and_then(|e| e.core.inputs.get(index as usize))
|
|
.map(|i| i.id.clone())
|
|
}) {
|
|
Some(Some(s)) => string_out(&s, buf, buf_size),
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_input_get_type` — `oak_node_value_type` code.
|
|
pub fn oaknode_node_input_get_type(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
out_type: *mut c_int,
|
|
) -> c_int {
|
|
if out_type.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node(node, |g, id| {
|
|
g.get(id)
|
|
.and_then(|e| e.core.input_data_type(&input_id))
|
|
.map(|t| t.to_oak())
|
|
}) {
|
|
Some(Some(t)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_type = t };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_input_is_connected`.
|
|
pub fn oaknode_node_input_is_connected(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
out_value: *mut c_int,
|
|
) -> c_int {
|
|
if out_value.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node(node, |g, id| {
|
|
g.get(id)
|
|
.map(|e| e.core.has_input(&input_id))
|
|
.unwrap_or(false)
|
|
&& g.is_input_connected(id, &input_id, -1)
|
|
}) {
|
|
Some(v) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_value = if v { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_input_is_connectable`.
|
|
pub fn oaknode_node_input_is_connectable(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
out_value: *mut c_int,
|
|
) -> c_int {
|
|
if out_value.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node(node, |g, id| {
|
|
g.get(id)
|
|
.and_then(|e| e.core.get_input(&input_id))
|
|
.map(|i| i.is_connectable())
|
|
}) {
|
|
Some(Some(v)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_value = if v { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_get_input_name` (two-stage).
|
|
pub fn oaknode_node_get_input_name(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node(node, |g, id| {
|
|
g.get(id).map(|e| e.behavior.input_name(&input_id).to_string())
|
|
}) {
|
|
Some(Some(s)) => string_out(&s, buf, buf_size),
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_input_get_connected_node` — null when unconnected.
|
|
pub fn oaknode_node_input_get_connected_node(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
out_node: *mut CHandle,
|
|
) -> c_int {
|
|
if out_node.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let from = {
|
|
let g = lock(&project);
|
|
if !g
|
|
.graph
|
|
.get(id)
|
|
.map(|e| e.core.has_input(&input_id))
|
|
.unwrap_or(false)
|
|
{
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
g.graph.connected_output(id, &input_id, -1)
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out_node = match from {
|
|
Some(f) => make_node_handle(project, f, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
/// `oaknode_node_get_input` — standard value into the POD.
|
|
pub fn oaknode_node_get_input(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
out: *mut crate::node::OakNodeValue,
|
|
) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node(node, |g, id| {
|
|
let e = g.get(id)?;
|
|
let declared = e.core.input_data_type(&input_id)?;
|
|
let v = e.core.standard_value(&input_id, -1);
|
|
value_to_pod(declared, &v)
|
|
}) {
|
|
Some(Some(pod)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = pod };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_set_input`.
|
|
pub fn oaknode_node_set_input(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
v: *const crate::node::OakNodeValue,
|
|
) -> c_int {
|
|
if v.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string and
|
|
// a live POD.
|
|
let (input_id, v) = unsafe { (cstr(input_id), *v) };
|
|
match with_node_mut(node, |g, id| {
|
|
let e = g.get_mut(id)?;
|
|
let declared = e.core.input_data_type(&input_id)?;
|
|
let value = pod_to_value(declared, v)?;
|
|
e.core.set_standard_value(&input_id, -1, value);
|
|
Some(())
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_set_input_undoable`.
|
|
pub fn oaknode_node_set_input_undoable(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
v: *const crate::node::OakNodeValue,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if v.is_null() || out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string and
|
|
// a live POD.
|
|
let (input_id, v) = unsafe { (cstr(input_id), *v) };
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let (new_value, old_value) = {
|
|
let g = lock(&project);
|
|
let e = match g.graph.get(id) {
|
|
Some(e) => e,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let declared = match e.core.input_data_type(&input_id) {
|
|
Some(d) => d,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let new_value = match pod_to_value(declared, v) {
|
|
Some(nv) => nv,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let old_value = e.core.standard_value(&input_id, -1);
|
|
(new_value, old_value)
|
|
};
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let input1 = input_id.clone();
|
|
let input2 = input_id;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
e.core.set_standard_value(&input1, -1, new_value.clone());
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
e.core.set_standard_value(&input2, -1, old_value.clone());
|
|
}
|
|
},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_node_get_input_string` (two-stage).
|
|
pub fn oaknode_node_get_input_string(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node(node, |g, id| {
|
|
let e = g.get(id)?;
|
|
if !e.core.has_input(&input_id) {
|
|
return None;
|
|
}
|
|
let v = e.core.standard_value(&input_id, -1);
|
|
match &v {
|
|
oaknode::value::NodeValue::Text(s) => Some(s.clone()),
|
|
_ => None,
|
|
}
|
|
}) {
|
|
Some(Some(s)) => string_out(&s, buf, buf_size),
|
|
Some(None) => OAKNODE_E_FAILED,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_set_input_string`.
|
|
pub fn oaknode_node_set_input_string(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
value: *const c_char,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees valid NUL-terminated strings.
|
|
let (input_id, value) = unsafe { (cstr(input_id), cstr(value)) };
|
|
match with_node_mut(node, |g, id| {
|
|
let e = g.get_mut(id)?;
|
|
if !e.core.has_input(&input_id) {
|
|
return None;
|
|
}
|
|
e.core
|
|
.set_standard_value(&input_id, -1, oaknode::value::NodeValue::Text(value));
|
|
Some(())
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_set_input_string_undoable`.
|
|
pub fn oaknode_node_set_input_string_undoable(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
value: *const c_char,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees valid NUL-terminated strings.
|
|
let (input_id, value) = unsafe { (cstr(input_id), cstr(value)) };
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let old = {
|
|
let g = lock(&project);
|
|
let e = match g.graph.get(id) {
|
|
Some(e) => e,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
if !e.core.has_input(&input_id) {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
let v = e.core.standard_value(&input_id, -1);
|
|
match &v {
|
|
oaknode::value::NodeValue::Text(s) => s.clone(),
|
|
_ => String::new(),
|
|
}
|
|
};
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let input1 = input_id.clone();
|
|
let input2 = input_id;
|
|
let value1 = value.clone();
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
e.core.set_standard_value(
|
|
&input1,
|
|
-1,
|
|
oaknode::value::NodeValue::Text(value1.clone()),
|
|
);
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
e.core.set_standard_value(
|
|
&input2,
|
|
-1,
|
|
oaknode::value::NodeValue::Text(old.clone()),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_node_connect` — live edge add.
|
|
pub fn oaknode_node_connect(
|
|
output_node: CHandle,
|
|
input_node: CHandle,
|
|
input_id: *const c_char,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let out_nr = match unsafe { node_ref_of(&output_node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let in_nr = match unsafe { node_ref_of(&input_node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
if !Arc::ptr_eq(&out_nr.0, &in_nr.0) {
|
|
// Edges live in the input node's graph; both endpoints must
|
|
// share it (cross-project connects are rejected).
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
let mut g = lock(&out_nr.0);
|
|
match g.graph.connect(out_nr.1, in_nr.1, &input_id, -1) {
|
|
Ok(()) => OAKNODE_OK,
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_connect_undoable`.
|
|
pub fn oaknode_node_connect_undoable(
|
|
output_node: CHandle,
|
|
input_node: CHandle,
|
|
input_id: *const c_char,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let out_nr = match unsafe { node_ref_of(&output_node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let in_nr = match unsafe { node_ref_of(&input_node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
if !Arc::ptr_eq(&out_nr.0, &in_nr.0) {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
let (project, from, to) = (out_nr.0, out_nr.1, in_nr.1);
|
|
// Pre-validate like the live connect (existence, connectability,
|
|
// already-connected -> STATE).
|
|
{
|
|
let g = lock(&project);
|
|
if !g.graph.is_valid(from) || !g.graph.is_valid(to) {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
let e = match g.graph.get(to) {
|
|
Some(e) => e,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let input = match e.core.get_input(&input_id) {
|
|
Some(i) => i,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
if !input.is_connectable() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
if g.graph.connected_output(to, &input_id, -1).is_some() {
|
|
return OAKNODE_E_STATE;
|
|
}
|
|
}
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let input1 = input_id.clone();
|
|
let input2 = input_id;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
let _ = g.graph.connect(from, to, &input1, -1);
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
g.graph.disconnect_input(to, &input2, -1);
|
|
},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_node_disconnect` — live edge remove.
|
|
pub fn oaknode_node_disconnect(input_node: CHandle, input_id: *const c_char) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let nr = match unsafe { node_ref_of(&input_node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&nr.0);
|
|
if g
|
|
.graph
|
|
.get(nr.1)
|
|
.map(|e| e.core.has_input(&input_id))
|
|
.unwrap_or(false)
|
|
{
|
|
g.graph.disconnect_input(nr.1, &input_id, -1);
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_disconnect_undoable` — succeeds even when nothing is
|
|
/// connected (the redo is then a no-op, mirroring the C++ command's
|
|
/// redo swallowing). The undo re-connect is not modelled (the source
|
|
/// node id is not retained) — documented deviation.
|
|
pub fn oaknode_node_disconnect_undoable(
|
|
input_node: CHandle,
|
|
input_id: *const c_char,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let nr = match unsafe { node_ref_of(&input_node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, to) = nr;
|
|
{
|
|
let g = lock(&project);
|
|
if !g
|
|
.graph
|
|
.get(to)
|
|
.map(|e| e.core.has_input(&input_id))
|
|
.unwrap_or(false)
|
|
{
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
}
|
|
let p1 = project.clone();
|
|
let input1 = input_id.clone();
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
g.graph.disconnect_input(to, &input1, -1);
|
|
},
|
|
|| {},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_node_output_connection_count`.
|
|
pub fn oaknode_node_output_connection_count(node: CHandle, out_count: *mut c_int) -> c_int {
|
|
if out_count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(node, |g, id| {
|
|
if !g.is_valid(id) {
|
|
return None;
|
|
}
|
|
Some(g.output_connections(id).len())
|
|
}) {
|
|
Some(Some(n)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_count = n as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_output_connection_node_at`.
|
|
pub fn oaknode_node_output_connection_node_at(
|
|
node: CHandle,
|
|
index: c_int,
|
|
out_node: *mut CHandle,
|
|
) -> c_int {
|
|
if out_node.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
if index < 0 {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let target = {
|
|
let g = lock(&project);
|
|
if !g.graph.is_valid(id) {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
g.graph
|
|
.output_connections(id)
|
|
.get(index as usize)
|
|
.map(|(t, _, _)| *t)
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out_node = match target {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
if target.is_some() {
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_output_connection_input_id_at` (two-stage).
|
|
pub fn oaknode_node_output_connection_input_id_at(
|
|
node: CHandle,
|
|
index: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if index < 0 {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
match with_node(node, |g, id| {
|
|
if !g.is_valid(id) {
|
|
return None;
|
|
}
|
|
g.output_connections(id)
|
|
.get(index as usize)
|
|
.map(|(_, input, _)| input.clone())
|
|
}) {
|
|
Some(Some(s)) => string_out(&s, buf, buf_size),
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_output_connection_element_at`.
|
|
pub fn oaknode_node_output_connection_element_at(
|
|
node: CHandle,
|
|
index: c_int,
|
|
out_element: *mut c_int,
|
|
) -> c_int {
|
|
if out_element.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(node, |g, id| {
|
|
if !g.is_valid(id) {
|
|
return None;
|
|
}
|
|
g.output_connections(id)
|
|
.get(index as usize)
|
|
.map(|(_, _, e)| *e)
|
|
}) {
|
|
Some(Some(e)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_element = e };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_link` — live link.
|
|
pub fn oaknode_node_link(a: CHandle, b: CHandle, out_linked: *mut c_int) -> c_int {
|
|
if out_linked.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let a_nr = match unsafe { node_ref_of(&a) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let b_nr = match unsafe { node_ref_of(&b) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
if !Arc::ptr_eq(&a_nr.0, &b_nr.0) {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
let mut g = lock(&a_nr.0);
|
|
let linked = g.graph.link(a_nr.1, b_nr.1);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_linked = if linked { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_node_unlink` — live unlink.
|
|
pub fn oaknode_node_unlink(a: CHandle, b: CHandle, out_unlinked: *mut c_int) -> c_int {
|
|
if out_unlinked.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let a_nr = match unsafe { node_ref_of(&a) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let b_nr = match unsafe { node_ref_of(&b) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
if !Arc::ptr_eq(&a_nr.0, &b_nr.0) {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
let mut g = lock(&a_nr.0);
|
|
let unlinked = g.graph.unlink(a_nr.1, b_nr.1);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_unlinked = if unlinked { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_node_link_undoable`.
|
|
pub fn oaknode_node_link_undoable(
|
|
a: CHandle,
|
|
b: CHandle,
|
|
link: c_int,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let a_nr = match unsafe { node_ref_of(&a) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let b_nr = match unsafe { node_ref_of(&b) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
if !Arc::ptr_eq(&a_nr.0, &b_nr.0) {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
let (project, id_a, id_b) = (a_nr.0, a_nr.1, b_nr.1);
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if link != 0 {
|
|
g.graph.link(id_a, id_b);
|
|
} else {
|
|
g.graph.unlink(id_a, id_b);
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if link != 0 {
|
|
g.graph.unlink(id_a, id_b);
|
|
} else {
|
|
g.graph.link(id_a, id_b);
|
|
}
|
|
},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_node_are_linked`.
|
|
pub fn oaknode_node_are_linked(a: CHandle, b: CHandle, out_value: *mut c_int) -> c_int {
|
|
if out_value.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let a_nr = match unsafe { node_ref_of(&a) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let b_nr = match unsafe { node_ref_of(&b) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let value = if Arc::ptr_eq(&a_nr.0, &b_nr.0) {
|
|
let g = lock(&a_nr.0);
|
|
g.graph.are_linked(a_nr.1, b_nr.1)
|
|
} else {
|
|
false
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_value = if value { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_node_link_count`.
|
|
pub fn oaknode_node_link_count(node: CHandle, out_count: *mut c_int) -> c_int {
|
|
if out_count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(node, |g, id| {
|
|
if !g.is_valid(id) {
|
|
return None;
|
|
}
|
|
Some(g.links_of(id).len())
|
|
}) {
|
|
Some(Some(n)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_count = n as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_link_at`.
|
|
pub fn oaknode_node_link_at(node: CHandle, index: c_int, out_node: *mut CHandle) -> c_int {
|
|
if out_node.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let target = {
|
|
let g = lock(&project);
|
|
if !g.graph.is_valid(id) {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
g.graph.links_of(id).get(index as usize).copied()
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out_node = match target {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
if target.is_some() {
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_context_count`.
|
|
pub fn oaknode_node_context_count(node: CHandle, out_count: *mut c_int) -> c_int {
|
|
if out_count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(node, |g, id| {
|
|
g.get(id).map(|e| e.core.context_positions.len())
|
|
}) {
|
|
Some(Some(n)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_count = n as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_context_node_at`.
|
|
pub fn oaknode_node_context_node_at(
|
|
node: CHandle,
|
|
index: c_int,
|
|
out_node: *mut CHandle,
|
|
) -> c_int {
|
|
if out_node.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let target = {
|
|
let g = lock(&project);
|
|
let e = match g.graph.get(id) {
|
|
Some(e) => e,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
e.core.context_positions.get(index as usize).map(|(c, _, _)| *c)
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out_node = match target {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
if target.is_some() {
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_get_context_position`.
|
|
pub fn oaknode_node_get_context_position(
|
|
node: CHandle,
|
|
context: CHandle,
|
|
out_x: *mut f64,
|
|
out_y: *mut f64,
|
|
out_expanded: *mut c_int,
|
|
) -> c_int {
|
|
if out_x.is_null() || out_y.is_null() || out_expanded.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let ctx_id = match unsafe { node_ref_of(&context) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
match with_node(node, |g, id| {
|
|
g.get(id).and_then(|e| {
|
|
e.core
|
|
.context_positions
|
|
.iter()
|
|
.find(|(c, _, _)| *c == ctx_id)
|
|
.map(|(_, pos, expanded)| (*pos, *expanded))
|
|
})
|
|
}) {
|
|
Some(Some(((x, y), expanded))) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_x = x;
|
|
*out_y = y;
|
|
*out_expanded = if expanded { 1 } else { 0 };
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_set_context_position`.
|
|
pub fn oaknode_node_set_context_position(
|
|
node: CHandle,
|
|
context: CHandle,
|
|
x: f64,
|
|
y: f64,
|
|
expanded: c_int,
|
|
) -> c_int {
|
|
let ctx_id = match unsafe { node_ref_of(&context) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
match with_node_mut(node, |g, id| {
|
|
g.get_mut(id)
|
|
.map(|e| e.core.set_context_position(ctx_id, x, y, expanded != 0))
|
|
}) {
|
|
Some(Some(_)) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_set_context_position_undoable`.
|
|
pub fn oaknode_node_set_context_position_undoable(
|
|
node: CHandle,
|
|
context: CHandle,
|
|
x: f64,
|
|
y: f64,
|
|
expanded: c_int,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let ctx_id = match unsafe { node_ref_of(&context) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let old = {
|
|
let g = lock(&project);
|
|
let e = match g.graph.get(id) {
|
|
Some(e) => e,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
e.core
|
|
.context_positions
|
|
.iter()
|
|
.find(|(c, _, _)| *c == ctx_id)
|
|
.map(|(_, pos, expanded)| (*pos, *expanded))
|
|
};
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
e.core.set_context_position(ctx_id, x, y, expanded != 0);
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
match old {
|
|
Some(((ox, oy), oe)) => {
|
|
e.core.set_context_position(ctx_id, ox, oy, oe);
|
|
}
|
|
None => {
|
|
e.core.remove_from_context(ctx_id);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_node_remove_from_context`.
|
|
pub fn oaknode_node_remove_from_context(node: CHandle, context: CHandle) -> c_int {
|
|
let ctx_id = match unsafe { node_ref_of(&context) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
match with_node_mut(node, |g, id| {
|
|
g.get_mut(id).map(|e| e.core.remove_from_context(ctx_id))
|
|
}) {
|
|
Some(Some(_)) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
/// `oaknode_node_create_copy` — standalone duplicate (owned).
|
|
pub fn oaknode_node_create_copy(node: CHandle) -> CHandle {
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
let (project, id) = nr;
|
|
let (core, behavior) = {
|
|
let g = lock(&project);
|
|
let e = match g.graph.get(id) {
|
|
Some(e) => e,
|
|
None => return CHandle::null(),
|
|
};
|
|
let core = e.core.clone();
|
|
let behavior = match e.behavior.duplicate(&core) {
|
|
Some(b) => b,
|
|
None => return CHandle::null(),
|
|
};
|
|
(core, behavior)
|
|
};
|
|
make_detached((core, behavior))
|
|
}
|
|
|
|
/// `oaknode_node_copy_in_graph` — duplicate into the source project
|
|
/// plus an undo command removing the copy.
|
|
pub fn oaknode_node_copy_in_graph(node: CHandle, out_command: *mut CHandle) -> CHandle {
|
|
if out_command.is_null() {
|
|
return CHandle::null();
|
|
}
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
let (project, id) = nr;
|
|
let (core, behavior) = {
|
|
let g = lock(&project);
|
|
let e = match g.graph.get(id) {
|
|
Some(e) => e,
|
|
None => return CHandle::null(),
|
|
};
|
|
let core = e.core.clone();
|
|
let behavior = match e.behavior.duplicate(&core) {
|
|
Some(b) => b,
|
|
None => return CHandle::null(),
|
|
};
|
|
(core, behavior)
|
|
};
|
|
let new_id = {
|
|
let mut g = lock(&project);
|
|
g.graph.add_node(core, behavior)
|
|
};
|
|
let p1 = project.clone();
|
|
let p2 = project.clone();
|
|
let cmd = closure_command(
|
|
move || {
|
|
// The copy already lives in the graph (created above).
|
|
let _ = &mut lock(&p1).graph;
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
g.graph.remove_node(new_id);
|
|
},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
make_node_handle(project, new_id, false)
|
|
}
|
|
|
|
/// `oaknode_node_get_project` — borrowed project handle.
|
|
pub fn oaknode_node_get_project(node: CHandle, out: *mut CHandle) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let project = unsafe { node_ref_of(&node) }.map(|nr| nr.project.clone());
|
|
match project {
|
|
Some(p) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = make_project_handle(p) };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_input_array_insert`.
|
|
pub fn oaknode_node_input_array_insert(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
index: c_int,
|
|
) -> c_int {
|
|
if index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node_mut(node, |g, id| {
|
|
let e = g.get_mut(id)?;
|
|
if !e.core.has_input(&input_id) {
|
|
return None;
|
|
}
|
|
e.core.input_array_insert(&input_id, index as usize);
|
|
Some(())
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_input_array_remove`.
|
|
pub fn oaknode_node_input_array_remove(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
index: c_int,
|
|
) -> c_int {
|
|
if index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node_mut(node, |g, id| {
|
|
let e = g.get_mut(id)?;
|
|
if !e.core.has_input(&input_id) {
|
|
return None;
|
|
}
|
|
e.core.input_array_remove(&input_id, index as usize);
|
|
Some(())
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_connect_element`.
|
|
pub fn oaknode_node_connect_element(
|
|
output_node: CHandle,
|
|
input_node: CHandle,
|
|
input_id: *const c_char,
|
|
element: c_int,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let out_nr = match unsafe { node_ref_of(&output_node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let in_nr = match unsafe { node_ref_of(&input_node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
if !Arc::ptr_eq(&out_nr.0, &in_nr.0) {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
let mut g = lock(&out_nr.0);
|
|
match g.graph.connect(out_nr.1, in_nr.1, &input_id, element) {
|
|
Ok(()) => OAKNODE_OK,
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_disconnect_element`.
|
|
pub fn oaknode_node_disconnect_element(
|
|
input_node: CHandle,
|
|
input_id: *const c_char,
|
|
element: c_int,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let nr = match unsafe { node_ref_of(&input_node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&nr.0);
|
|
if g
|
|
.graph
|
|
.get(nr.1)
|
|
.map(|e| e.core.has_input(&input_id))
|
|
.unwrap_or(false)
|
|
{
|
|
g.graph.disconnect_input(nr.1, &input_id, element);
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
|
|
/// `oaknode_command_create_add_node` — move `node` into `graph`'s
|
|
/// project (undo moves it back).
|
|
pub fn oaknode_command_create_add_node(graph: CHandle, node: CHandle) -> CHandle {
|
|
let target = match unsafe { project_of(&graph) }.cloned() {
|
|
Some(t) => t,
|
|
None => return CHandle::null(),
|
|
};
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => nr,
|
|
None => return CHandle::null(),
|
|
};
|
|
let src_project = nr.project.clone();
|
|
let src_id = nr.id;
|
|
if Arc::ptr_eq(&src_project, &target) && { lock(&target).graph.is_valid(src_id) } {
|
|
return CHandle::null();
|
|
}
|
|
let entry = {
|
|
let mut s = lock(&src_project);
|
|
match s.graph.take_node(src_id) {
|
|
Some(e) => e,
|
|
None => return CHandle::null(),
|
|
}
|
|
};
|
|
let mut entry = Some(entry);
|
|
let target_redo = target.clone();
|
|
let src_undo = src_project;
|
|
let target_undo = target;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let e = match entry.take() {
|
|
Some(e) => e,
|
|
None => return,
|
|
};
|
|
let new_id = {
|
|
let mut t = lock(&target_redo);
|
|
t.graph.add_entry(e, src_id)
|
|
};
|
|
// SAFETY: the shared node box is rewritten in place.
|
|
if let Some(boxed) = unsafe { node_ref_mut(&node) } {
|
|
boxed.project = target_redo.clone();
|
|
boxed.id = new_id;
|
|
if boxed.owned.swap(false, Ordering::SeqCst) {
|
|
alive_dec();
|
|
}
|
|
}
|
|
},
|
|
move || {
|
|
let current_id = unsafe { node_ref_of(&node) }
|
|
.map(|n| n.id)
|
|
.unwrap_or(src_id);
|
|
let e = {
|
|
let mut t = lock(&target_undo);
|
|
match t.graph.take_node(current_id) {
|
|
Some(e) => e,
|
|
None => return,
|
|
}
|
|
};
|
|
{
|
|
let mut s = lock(&src_undo);
|
|
s.graph.add_entry(e, src_id);
|
|
}
|
|
// SAFETY: the shared node box is rewritten back.
|
|
if let Some(boxed) = unsafe { node_ref_mut(&node) } {
|
|
boxed.project = src_undo.clone();
|
|
boxed.id = src_id;
|
|
if !boxed.owned.swap(true, Ordering::SeqCst) {
|
|
alive_inc();
|
|
}
|
|
}
|
|
},
|
|
);
|
|
box_command(cmd)
|
|
}
|
|
|
|
/// `oaknode_command_create_set_position_recursive` — best-effort: set
|
|
/// the node's own context position (undo restores the old value).
|
|
pub fn oaknode_command_create_set_position_recursive(
|
|
node: CHandle,
|
|
context: CHandle,
|
|
x: f64,
|
|
y: f64,
|
|
) -> CHandle {
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
let ctx_id = match unsafe { node_ref_of(&context) } {
|
|
Some(nr) => nr.id,
|
|
None => return CHandle::null(),
|
|
};
|
|
let (project, id) = nr;
|
|
let old = {
|
|
let g = lock(&project);
|
|
match g.graph.get(id) {
|
|
Some(e) => e
|
|
.core
|
|
.context_positions
|
|
.iter()
|
|
.find(|(c, _, _)| *c == ctx_id)
|
|
.map(|(_, pos, expanded)| (*pos, *expanded)),
|
|
None => return CHandle::null(),
|
|
}
|
|
};
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
e.core.set_context_position(ctx_id, x, y, false);
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
match old {
|
|
Some(((ox, oy), oe)) => {
|
|
e.core.set_context_position(ctx_id, ox, oy, oe);
|
|
}
|
|
None => {
|
|
e.core.remove_from_context(ctx_id);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
);
|
|
box_command(cmd)
|
|
}
|
|
|
|
/// `oaknode_node_get_markers` — the sequence's marker list (created
|
|
/// lazily; addref'd copy).
|
|
pub fn oaknode_node_get_markers(node: CHandle, out: *mut CHandle) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let markers = with_node_mut(node, |g, id| {
|
|
let seq = behavior_of_mut::<oaknode::sequence::SequenceBehavior>(g, id)?;
|
|
if seq.markers.is_null() {
|
|
seq.markers = oaktimeline::handle::make_owned(
|
|
oaktimeline::marker::TimelineMarkerList::new(),
|
|
);
|
|
}
|
|
Some(seq.markers)
|
|
});
|
|
match markers {
|
|
Some(Some(h)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = addref_copy(h) };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_get_work_area` — the sequence's work area (created
|
|
/// lazily; addref'd copy).
|
|
pub fn oaknode_node_get_work_area(node: CHandle, out: *mut CHandle) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let workarea = with_node_mut(node, |g, id| {
|
|
let seq = behavior_of_mut::<oaknode::sequence::SequenceBehavior>(g, id)?;
|
|
if seq.workarea.is_null() {
|
|
seq.workarea =
|
|
oaktimeline::handle::make_owned(oaktimeline::workarea::TimelineWorkArea::new());
|
|
}
|
|
Some(seq.workarea)
|
|
});
|
|
match workarea {
|
|
Some(Some(h)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = addref_copy(h) };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_get_video_frame_cache` — the node's video cache
|
|
/// handle (usually empty: caches are created lazily by the render
|
|
/// module; documented).
|
|
pub fn oaknode_node_get_video_frame_cache(node: CHandle, out: *mut CHandle) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(node, |g, id| g.get(id).map(|e| e.core.caches.video)) {
|
|
Some(Some(h)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = addref_copy(h) };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_copy_inputs`.
|
|
pub fn oaknode_node_copy_inputs(
|
|
dst: CHandle,
|
|
src: CHandle,
|
|
include_connections: c_int,
|
|
) -> c_int {
|
|
let dst_nr = match unsafe { node_ref_of(&dst) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let src_nr = match unsafe { node_ref_of(&src) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
if !Arc::ptr_eq(&dst_nr.0, &src_nr.0) {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
let mut g = lock(&dst_nr.0);
|
|
match oaknode::ops::copy_inputs(
|
|
&mut g.graph,
|
|
src_nr.1,
|
|
dst_nr.1,
|
|
include_connections != 0,
|
|
) {
|
|
Ok(()) => OAKNODE_OK,
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_set_value_hint_track` — best-effort value hint from a
|
|
/// track reference (video -> Texture, audio -> Samples).
|
|
pub fn oaknode_node_set_value_hint_track(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
track_type: c_int,
|
|
track_index: c_int,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let types = match track_type {
|
|
0 => vec![oaknode::value::ValueType::Texture],
|
|
1 => vec![oaknode::value::ValueType::Samples],
|
|
_ => Vec::new(),
|
|
};
|
|
match with_node_mut(node, |g, id| {
|
|
let e = g.get_mut(id)?;
|
|
if !e.core.has_input(&input_id) {
|
|
return None;
|
|
}
|
|
e.core.set_value_hint(
|
|
&input_id,
|
|
-1,
|
|
oaknode::input::ValueHint {
|
|
types,
|
|
index: track_index,
|
|
tag: String::new(),
|
|
},
|
|
);
|
|
Some(())
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_viewer_set_video_params` — set the sequence's video
|
|
/// parameter stream 0.
|
|
pub fn oaknode_viewer_set_video_params(viewer: CHandle, params: *const CHandle) -> c_int {
|
|
if params.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller passes a live oakcommon videoparams handle.
|
|
let converted = unsafe { vp_from_handle(*params) };
|
|
let Some(converted) = converted else {
|
|
return OAKNODE_E_INVALID;
|
|
};
|
|
match with_node_mut(viewer, |g, id| {
|
|
let seq = behavior_of_mut::<oaknode::sequence::SequenceBehavior>(g, id)?;
|
|
if seq.video_params.is_empty() {
|
|
seq.video_params.push(converted);
|
|
} else {
|
|
seq.video_params[0] = converted;
|
|
}
|
|
Some(())
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_viewer_set_audio_params` — set the sequence's audio
|
|
/// parameter stream 0 (oakcore audioparams pointer).
|
|
pub fn oaknode_viewer_set_audio_params(viewer: CHandle, params: *const c_void) -> c_int {
|
|
if params.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the oakcore audioparams contract.
|
|
let (sample_rate, channel_layout, format) = unsafe {
|
|
(
|
|
crate::stubs::audio::oakcore_audioparams_sample_rate(params),
|
|
crate::stubs::audio::oakcore_audioparams_channel_layout(params),
|
|
crate::stubs::audio::oakcore_audioparams_format(params),
|
|
)
|
|
};
|
|
let converted = oaknode::value::AudioParams {
|
|
sample_rate,
|
|
channel_layout,
|
|
format,
|
|
};
|
|
match with_node_mut(viewer, |g, id| {
|
|
let seq = behavior_of_mut::<oaknode::sequence::SequenceBehavior>(g, id)?;
|
|
if seq.audio_params.is_empty() {
|
|
seq.audio_params.push(converted);
|
|
} else {
|
|
seq.audio_params[0] = converted;
|
|
}
|
|
Some(())
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_find_input_footage` — the first footage node feeding
|
|
/// this node (upstream walk).
|
|
pub fn oaknode_node_find_input_footage(node: CHandle, out: *mut CHandle) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let found = {
|
|
let g = lock(&project);
|
|
if !g.graph.is_valid(id) {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
let mut frontier = vec![id];
|
|
let mut visited: Vec<NodeId> = Vec::new();
|
|
let mut found = None;
|
|
while !frontier.is_empty() && found.is_none() {
|
|
let mut next = Vec::new();
|
|
for cur in frontier {
|
|
if visited.contains(&cur) {
|
|
continue;
|
|
}
|
|
visited.push(cur);
|
|
let Some(e) = g.graph.get(cur) else { continue };
|
|
if e.behavior.type_id() == "org.olivevideoeditor.Olive.footage" && cur != id {
|
|
found = Some(cur);
|
|
break;
|
|
}
|
|
for (src, _, _) in g.graph.input_connections(cur) {
|
|
next.push(src);
|
|
}
|
|
}
|
|
frontier = next;
|
|
}
|
|
found
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match found {
|
|
Some(f) => make_node_handle(project, f, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_node_get_input_at_time` — value at a rational time.
|
|
pub fn oaknode_node_get_input_at_time(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
time_num: i64,
|
|
time_den: i64,
|
|
out: *mut crate::node::OakNodeValue,
|
|
) -> c_int {
|
|
if out.is_null() || time_den == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node(node, |g, id| {
|
|
let e = g.get(id)?;
|
|
let declared = e.core.input_data_type(&input_id)?;
|
|
let v = e
|
|
.core
|
|
.value_at_time(&input_id, -1, Rational::new(time_num, time_den));
|
|
value_to_pod(declared, &v)
|
|
}) {
|
|
Some(Some(pod)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = pod };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_set_input_at_time_undoable` — the real
|
|
/// `set_value_at_time` domain op (keyframed tracks get a key at the
|
|
/// time; static inputs get the standard value).
|
|
pub fn oaknode_node_set_input_at_time_undoable(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
time_num: i64,
|
|
time_den: i64,
|
|
v: *const crate::node::OakNodeValue,
|
|
track: c_int,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if v.is_null() || out_command.is_null() || time_den == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string and
|
|
// a live POD.
|
|
let (input_id, v) = unsafe { (cstr(input_id), *v) };
|
|
let _ = track;
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let declared = {
|
|
let g = lock(&project);
|
|
let e = match g.graph.get(id) {
|
|
Some(e) => e,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
match e.core.input_data_type(&input_id) {
|
|
Some(d) => d,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
}
|
|
};
|
|
let value = match pod_to_value(declared, v) {
|
|
Some(nv) => nv,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let guard = lock(&project);
|
|
let cmd = match oaknode::ops::set_value_at_time_command(
|
|
&project,
|
|
&guard.graph,
|
|
id,
|
|
&input_id,
|
|
-1,
|
|
Rational::new(time_num, time_den),
|
|
&value,
|
|
) {
|
|
Ok(c) => c,
|
|
Err(e) => return e.code(),
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_node_identity` — the graph slot identity (0 for invalid).
|
|
pub fn oaknode_node_identity(node: CHandle) -> usize {
|
|
node_identity(node)
|
|
}
|
|
|
|
/// `oaknode_node_set_input_at_time_into` — create the command and add
|
|
/// it to a multi command (the multi takes ownership).
|
|
pub fn oaknode_node_set_input_at_time_into(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
time_num: i64,
|
|
time_den: i64,
|
|
v: *const crate::node::OakNodeValue,
|
|
track: c_int,
|
|
multi_command: CHandle,
|
|
) -> c_int {
|
|
let mut cmd = CHandle::null();
|
|
let rc = oaknode_node_set_input_at_time_undoable(
|
|
node, input_id, time_num, time_den, v, track, &mut cmd,
|
|
);
|
|
if rc != OAKNODE_OK {
|
|
return rc;
|
|
}
|
|
oakundo::undocommand::command_multi_add_child(multi_command, cmd)
|
|
}
|
|
|
|
/// `oaknode_command_create_remove_node` — remove the node from its
|
|
/// graph (the entry is not retained for undo — documented deviation
|
|
/// from the C++ shared-ptr retention).
|
|
pub fn oaknode_command_create_remove_node(node: CHandle) -> CHandle {
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
let (project, id) = nr;
|
|
{
|
|
let g = lock(&project);
|
|
if !g.graph.is_valid(id) {
|
|
return CHandle::null();
|
|
}
|
|
}
|
|
let p1 = project.clone();
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
g.graph.remove_node(id);
|
|
},
|
|
|| {},
|
|
);
|
|
box_command(cmd)
|
|
}
|
|
|
|
/// `oaknode_node_free` — release a node handle shell; a still-owned
|
|
/// (detached) node is dropped from its scratch graph.
|
|
pub fn oaknode_node_free(node: *mut CHandle) {
|
|
if node.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *node };
|
|
free_node_handle(h);
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *node = CHandle::null() };
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Factory family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// `oaknode_factory_initialize` — the registry builds lazily; nothing
|
|
/// to do.
|
|
pub fn oaknode_factory_initialize() -> c_int {
|
|
let _ = Factory::global();
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_factory_destroy` — process-lifetime registry; nothing to
|
|
/// do.
|
|
pub fn oaknode_factory_destroy() {}
|
|
|
|
/// `oaknode_factory_id_count`.
|
|
pub fn oaknode_factory_id_count(out_count: *mut c_int) -> c_int {
|
|
if out_count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_count = Factory::global().entries().len() as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_factory_id_at` (two-stage).
|
|
pub fn oaknode_factory_id_at(index: c_int, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
if index < 0 {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
match Factory::global().entries().get(index as usize) {
|
|
Some(meta) => string_out(meta.type_id, buf, buf_size),
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_factory_name_from_id` (two-stage).
|
|
pub fn oaknode_factory_name_from_id(
|
|
type_id: *const c_char,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let type_id = unsafe { cstr(type_id) };
|
|
match Factory::global().find(&type_id) {
|
|
Some(meta) => string_out(meta.name, buf, buf_size),
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_factory_create_from_id` — detached node (owned).
|
|
pub fn oaknode_factory_create_from_id(type_id: *const c_char) -> CHandle {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let type_id = unsafe { cstr(type_id) };
|
|
match Factory::global().find(&type_id) {
|
|
Some(meta) => make_detached((meta.create)()),
|
|
None => CHandle::null(),
|
|
}
|
|
}
|
|
|
|
/// Cached factory prototype nodes (one per registry entry; created
|
|
/// lazily in the scratch project).
|
|
static PROTOTYPES: OnceLock<Mutex<Vec<CHandle>>> = OnceLock::new();
|
|
|
|
/// `oaknode_factory_node_at` — borrowed prototype node.
|
|
pub fn oaknode_factory_node_at(index: c_int, out_node: *mut CHandle) -> c_int {
|
|
if out_node.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
if index < 0 {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
let entries = Factory::global().entries();
|
|
if index as usize >= entries.len() {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
let mut protos = PROTOTYPES
|
|
.get_or_init(|| Mutex::new(Vec::new()))
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner());
|
|
if protos.len() != entries.len() {
|
|
// Fill lazily: only the entries up to (and including) the
|
|
// requested index are materialized.
|
|
for i in protos.len()..=index as usize {
|
|
let (core, behavior) = (entries[i].create)();
|
|
let project = scratch_project();
|
|
let id = {
|
|
let mut p = lock(&project);
|
|
p.graph.add_node(core, behavior)
|
|
};
|
|
protos.push(make_node_handle(project, id, false));
|
|
}
|
|
}
|
|
// SAFETY: valid out pointer; the prototype is addref'd for the
|
|
// caller (owned copy semantics).
|
|
unsafe { *out_node = addref_copy(protos[index as usize]) };
|
|
OAKNODE_OK
|
|
}
|
|
// -------------------------------------------------------------------
|
|
// Folder family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// `oaknode_folder_create` — new folder node in the project graph.
|
|
pub fn oaknode_folder_create(project: CHandle) -> CHandle {
|
|
let p = match unsafe { project_of(&project) }.cloned() {
|
|
Some(p) => p,
|
|
None => return CHandle::null(),
|
|
};
|
|
let (core, behavior) = oaknode::folder::create("Folder");
|
|
let id = {
|
|
let mut g = lock(&p);
|
|
g.graph.add_node(core, behavior)
|
|
};
|
|
make_node_handle(p, id, false)
|
|
}
|
|
|
|
/// Folder children of a folder handle.
|
|
fn folder_children(h: CHandle) -> Option<Vec<NodeId>> {
|
|
with_node(h, |g, id| {
|
|
behavior_of::<oaknode::folder::FolderBehavior>(g, id)
|
|
.map(|f| f.children.clone())
|
|
})?
|
|
}
|
|
|
|
/// `oaknode_folder_child_count`.
|
|
pub fn oaknode_folder_child_count(folder: CHandle) -> c_int {
|
|
match folder_children(folder) {
|
|
Some(children) => children.len() as c_int,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_folder_child_at` — borrowed child view.
|
|
pub fn oaknode_folder_child_at(folder: CHandle, index: c_int) -> CHandle {
|
|
if index < 0 {
|
|
return CHandle::null();
|
|
}
|
|
let nr = match unsafe { node_ref_of(&folder) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
let (project, id) = nr;
|
|
let child = {
|
|
let g = lock(&project);
|
|
behavior_of::<oaknode::folder::FolderBehavior>(&g.graph, id)
|
|
.and_then(|f| f.children.get(index as usize).copied())
|
|
};
|
|
match child {
|
|
Some(c) => make_node_handle(project, c, false),
|
|
None => CHandle::null(),
|
|
}
|
|
}
|
|
|
|
/// `oaknode_folder_add_child` — live add (+ bin membership).
|
|
pub fn oaknode_folder_add_child(folder: CHandle, child: CHandle) -> c_int {
|
|
let f_nr = match unsafe { node_ref_of(&folder) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let c_id = match unsafe { node_ref_of(&child) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&f_nr.0);
|
|
let folder_b =
|
|
match behavior_of_mut::<oaknode::folder::FolderBehavior>(&mut g.graph, f_nr.1) {
|
|
Some(f) => f,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
folder_b.add_child(c_id);
|
|
if let Some(e) = g.graph.get_mut(c_id) {
|
|
e.core.bin_folder = Some(f_nr.1);
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_folder_as_node` — identity cast (every handle shares the
|
|
/// same payload layout); an addref'd copy keeps the borrow discipline.
|
|
pub fn oaknode_folder_as_node(folder: CHandle) -> CHandle {
|
|
addref_copy(folder)
|
|
}
|
|
|
|
/// `oaknode_command_create_folder_add_child` — undoable add.
|
|
pub fn oaknode_command_create_folder_add_child(folder: CHandle, child: CHandle) -> CHandle {
|
|
let f_nr = match unsafe { node_ref_of(&folder) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
let c_id = match unsafe { node_ref_of(&child) } {
|
|
Some(nr) => nr.id,
|
|
None => return CHandle::null(),
|
|
};
|
|
let (project, f_id) = f_nr;
|
|
{
|
|
let g = lock(&project);
|
|
if behavior_of::<oaknode::folder::FolderBehavior>(&g.graph, f_id).is_none() {
|
|
return CHandle::null();
|
|
}
|
|
}
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(f) =
|
|
behavior_of_mut::<oaknode::folder::FolderBehavior>(&mut g.graph, f_id)
|
|
{
|
|
f.add_child(c_id);
|
|
}
|
|
if let Some(e) = g.graph.get_mut(c_id) {
|
|
e.core.bin_folder = Some(f_id);
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(f) =
|
|
behavior_of_mut::<oaknode::folder::FolderBehavior>(&mut g.graph, f_id)
|
|
{
|
|
f.remove_child(c_id);
|
|
}
|
|
if let Some(e) = g.graph.get_mut(c_id) {
|
|
e.core.bin_folder = None;
|
|
}
|
|
},
|
|
);
|
|
box_command(cmd)
|
|
}
|
|
|
|
/// `oaknode_folder_remove_child` — live remove.
|
|
pub fn oaknode_folder_remove_child(folder: CHandle, child: CHandle) -> c_int {
|
|
let f_nr = match unsafe { node_ref_of(&folder) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let c_id = match unsafe { node_ref_of(&child) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&f_nr.0);
|
|
let folder_b =
|
|
match behavior_of_mut::<oaknode::folder::FolderBehavior>(&mut g.graph, f_nr.1) {
|
|
Some(f) => f,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
folder_b.remove_child(c_id);
|
|
if let Some(e) = g.graph.get_mut(c_id) {
|
|
e.core.bin_folder = None;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_folder_move_children` — move nodes into `dest_folder`.
|
|
pub fn oaknode_folder_move_children(
|
|
nodes: *const CHandle,
|
|
count: c_int,
|
|
dest_folder: CHandle,
|
|
) -> c_int {
|
|
if nodes.is_null() || count < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
for i in 0..count as usize {
|
|
// SAFETY: the caller guarantees `count` valid handles.
|
|
let child = unsafe { *nodes.add(i) };
|
|
let rc = oaknode_folder_add_child(dest_folder, child);
|
|
if rc != OAKNODE_OK {
|
|
return rc;
|
|
}
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_folder_has_child_recursive`.
|
|
pub fn oaknode_folder_has_child_recursive(folder: CHandle, child: CHandle) -> c_int {
|
|
let f_nr = match unsafe { node_ref_of(&folder) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let c_id = match unsafe { node_ref_of(&child) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let g = lock(&f_nr.0);
|
|
match behavior_of::<oaknode::folder::FolderBehavior>(&g.graph, f_nr.1) {
|
|
Some(f) => {
|
|
if f.has_child_recursive(c_id, &g.graph) {
|
|
1
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_folder_index_of_child`.
|
|
pub fn oaknode_folder_index_of_child(folder: CHandle, child: CHandle) -> c_int {
|
|
let f_nr = match unsafe { node_ref_of(&folder) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let c_id = match unsafe { node_ref_of(&child) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let g = lock(&f_nr.0);
|
|
match behavior_of::<oaknode::folder::FolderBehavior>(&g.graph, f_nr.1) {
|
|
Some(f) => match f.index_of_child(c_id) {
|
|
Some(i) => i as c_int,
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
},
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_folder_parent_of` — the folder owning this item (null when
|
|
/// none).
|
|
pub fn oaknode_folder_parent_of(node: CHandle) -> CHandle {
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
let (project, id) = nr;
|
|
let parent = {
|
|
let g = lock(&project);
|
|
let entry = match g.graph.get(id) {
|
|
Some(e) => e,
|
|
None => return CHandle::null(),
|
|
};
|
|
match entry.core.bin_folder {
|
|
Some(bin)
|
|
if behavior_of::<oaknode::folder::FolderBehavior>(&g.graph, bin).is_some() =>
|
|
{
|
|
Some(bin)
|
|
}
|
|
_ => {
|
|
// Fall back to a recursive walk over folder children.
|
|
g.graph.node_ids().into_iter().find(|fid| {
|
|
behavior_of::<oaknode::folder::FolderBehavior>(&g.graph, *fid)
|
|
.map(|f| f.has_child_recursive(id, &g.graph))
|
|
.unwrap_or(false)
|
|
})
|
|
}
|
|
}
|
|
};
|
|
match parent {
|
|
Some(p) => make_node_handle(project, p, false),
|
|
None => CHandle::null(),
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Footage family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// Borrow the footage behavior behind a footage handle.
|
|
fn footage_behavior(h: CHandle) -> Option<&'static oaknode::footage::FootageBehavior> {
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
let p = lock(&nr.project);
|
|
let b = behavior_of::<oaknode::footage::FootageBehavior>(&p.graph, nr.id)?;
|
|
// SAFETY: the node lives in the arena; the project outlives every
|
|
// handle that references it (handles hold an Arc clone).
|
|
unsafe { Some(&*(b as *const _)) }
|
|
}
|
|
|
|
/// Mutable footage-behavior view.
|
|
fn footage_behavior_mut(h: CHandle) -> Option<&'static mut oaknode::footage::FootageBehavior> {
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
let mut p = lock(&nr.project);
|
|
let b = behavior_of_mut::<oaknode::footage::FootageBehavior>(&mut p.graph, nr.id)?;
|
|
// SAFETY: see `footage_behavior`.
|
|
unsafe { Some(&mut *(b as *mut _)) }
|
|
}
|
|
|
|
/// `oaknode_footage_create` — footage node registered in `project`.
|
|
pub fn oaknode_footage_create(project: CHandle, filename: *const c_char) -> CHandle {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let filename = unsafe { cstr(filename) };
|
|
let p = match unsafe { project_of(&project) }.cloned() {
|
|
Some(p) => p,
|
|
None => return CHandle::null(),
|
|
};
|
|
let (mut core, behavior) = oaknode::footage::FootageBehavior::create();
|
|
core.set_standard_value(
|
|
"file_in",
|
|
-1,
|
|
oaknode::value::NodeValue::Text(filename.clone()),
|
|
);
|
|
let id = {
|
|
let mut g = lock(&p);
|
|
g.graph.add_node(core, behavior)
|
|
};
|
|
{
|
|
let mut g = lock(&p);
|
|
if let Some(f) =
|
|
behavior_of_mut::<oaknode::footage::FootageBehavior>(&mut g.graph, id)
|
|
{
|
|
f.filename = filename;
|
|
// Best-effort probe: the stream metadata is filled when a
|
|
// decoder recognizes the file; failures keep the node
|
|
// usable (valid stays false).
|
|
let _ = f.probe();
|
|
}
|
|
}
|
|
make_node_handle(p, id, false)
|
|
}
|
|
|
|
/// `oaknode_footage_as_node` — identity cast (addref'd copy).
|
|
pub fn oaknode_footage_as_node(footage: CHandle) -> CHandle {
|
|
addref_copy(footage)
|
|
}
|
|
|
|
/// `oaknode_footage_filename` (two-stage).
|
|
pub fn oaknode_footage_filename(footage: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
match footage_behavior(footage) {
|
|
Some(f) => string_out(&f.filename, buf, buf_size),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_set_filename`.
|
|
pub fn oaknode_footage_set_filename(footage: CHandle, filename: *const c_char) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let filename = unsafe { cstr(filename) };
|
|
match footage_behavior_mut(footage) {
|
|
Some(f) => {
|
|
f.filename = filename;
|
|
f.valid = false;
|
|
let _ = f.probe();
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_is_valid` — 1/0.
|
|
pub fn oaknode_footage_is_valid(footage: CHandle) -> c_int {
|
|
match footage_behavior(footage) {
|
|
Some(f) => {
|
|
if f.valid {
|
|
1
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_timestamp`.
|
|
pub fn oaknode_footage_timestamp(footage: CHandle, out_timestamp: *mut i64) -> c_int {
|
|
if out_timestamp.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match footage_behavior(footage) {
|
|
Some(f) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_timestamp = f.timestamp };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_set_timestamp`.
|
|
pub fn oaknode_footage_set_timestamp(footage: CHandle, timestamp: i64) -> c_int {
|
|
match footage_behavior_mut(footage) {
|
|
Some(f) => {
|
|
f.timestamp = timestamp;
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_decoder` (two-stage).
|
|
pub fn oaknode_footage_decoder(footage: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
match footage_behavior(footage) {
|
|
Some(f) => string_out(&f.decoder, buf, buf_size),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_total_stream_count`.
|
|
pub fn oaknode_footage_total_stream_count(footage: CHandle) -> c_int {
|
|
match footage_behavior(footage) {
|
|
Some(f) => f.total_stream_count() as c_int,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_video_stream_count`.
|
|
pub fn oaknode_footage_video_stream_count(footage: CHandle) -> c_int {
|
|
match footage_behavior(footage) {
|
|
Some(f) => f.video_stream_count() as c_int,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_audio_stream_count`.
|
|
pub fn oaknode_footage_audio_stream_count(footage: CHandle) -> c_int {
|
|
match footage_behavior(footage) {
|
|
Some(f) => f.audio_stream_count() as c_int,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_subtitle_stream_count`.
|
|
pub fn oaknode_footage_subtitle_stream_count(footage: CHandle) -> c_int {
|
|
match footage_behavior(footage) {
|
|
Some(f) => f.subtitle_stream_count() as c_int,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_duration` — (num, den) of the longest stream.
|
|
pub fn oaknode_footage_duration(
|
|
footage: CHandle,
|
|
out_numerator: *mut c_int,
|
|
out_denominator: *mut c_int,
|
|
) -> c_int {
|
|
if out_numerator.is_null() || out_denominator.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match footage_behavior(footage) {
|
|
Some(f) => {
|
|
let d = f.duration();
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_numerator = d.numerator() as c_int;
|
|
*out_denominator = d.denominator() as c_int;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_proxy_enabled`.
|
|
pub fn oaknode_footage_proxy_enabled(footage: CHandle) -> c_int {
|
|
match footage_behavior(footage) {
|
|
Some(f) => {
|
|
if f.proxy_enabled {
|
|
1
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_set_proxy_enabled`.
|
|
pub fn oaknode_footage_set_proxy_enabled(footage: CHandle, enabled: c_int) -> c_int {
|
|
match footage_behavior_mut(footage) {
|
|
Some(f) => {
|
|
f.proxy_enabled = enabled != 0;
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_proxy_path` (two-stage).
|
|
pub fn oaknode_footage_proxy_path(footage: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
match footage_behavior(footage) {
|
|
Some(f) => string_out(&f.proxy, buf, buf_size),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_proxy_state`.
|
|
pub fn oaknode_footage_proxy_state(footage: CHandle) -> c_int {
|
|
match footage_behavior(footage) {
|
|
Some(f) => f.proxy_state,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_set_proxy`.
|
|
pub fn oaknode_footage_set_proxy(
|
|
footage: CHandle,
|
|
path: *const c_char,
|
|
state: c_int,
|
|
video_stream_index: c_int,
|
|
preset_version: c_int,
|
|
enabled: c_int,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let path = unsafe { cstr(path) };
|
|
match footage_behavior_mut(footage) {
|
|
Some(f) => {
|
|
f.set_proxy(
|
|
&path,
|
|
state,
|
|
video_stream_index,
|
|
preset_version,
|
|
enabled != 0,
|
|
);
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_clear_proxy`.
|
|
pub fn oaknode_footage_clear_proxy(footage: CHandle) -> c_int {
|
|
match footage_behavior_mut(footage) {
|
|
Some(f) => {
|
|
f.clear_proxy();
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_get_video_params` — stream `index` as an oakcommon
|
|
/// params handle.
|
|
pub fn oaknode_footage_get_video_params(
|
|
footage: CHandle,
|
|
index: c_int,
|
|
out: *mut CHandle,
|
|
) -> c_int {
|
|
if out.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match footage_behavior(footage).and_then(|f| f.video_params(index as usize)) {
|
|
Some(v) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = vp_handle(&v) };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_set_video_params`.
|
|
pub fn oaknode_footage_set_video_params(
|
|
footage: CHandle,
|
|
index: c_int,
|
|
params: *const CHandle,
|
|
) -> c_int {
|
|
if params.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller passes a live oakcommon videoparams handle.
|
|
let converted = unsafe { vp_from_handle(*params) };
|
|
let Some(converted) = converted else {
|
|
return OAKNODE_E_INVALID;
|
|
};
|
|
let nr = match unsafe { node_ref_of(&footage) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&nr.0);
|
|
let f = match behavior_of_mut::<oaknode::footage::FootageBehavior>(&mut g.graph, nr.1) {
|
|
Some(f) => f,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let mut seen = 0usize;
|
|
for s in f.streams.iter_mut().filter(|s| s.is_video) {
|
|
if seen == index as usize {
|
|
s.video = Some(converted);
|
|
return OAKNODE_OK;
|
|
}
|
|
seen += 1;
|
|
}
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
|
|
/// `oaknode_footage_get_video_length` — (num, den) of the longest video
|
|
/// stream.
|
|
pub fn oaknode_footage_get_video_length(
|
|
footage: CHandle,
|
|
out_num: *mut i64,
|
|
out_den: *mut i64,
|
|
) -> c_int {
|
|
if out_num.is_null() || out_den.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match footage_behavior(footage) {
|
|
Some(f) => {
|
|
let d = f.video_length();
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_num = d.numerator();
|
|
*out_den = d.denominator();
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_footage_set_cancel_atom` — record the footage's cancel
|
|
/// state from a shared oakcommon cancel atom handle.
|
|
pub fn oaknode_footage_set_cancel_atom(footage: CHandle, atom: CHandle) -> c_int {
|
|
let cancelled = if atom.is_null() {
|
|
false
|
|
} else {
|
|
// SAFETY: the atom handle boxes an oakcommon CancelAtom.
|
|
unsafe { oakcommon::handle::get::<oakcommon::cancelatom::CancelAtom>(&atom) }
|
|
.map(|a| a.is_cancelled())
|
|
.unwrap_or(false)
|
|
};
|
|
match footage_behavior_mut(footage) {
|
|
Some(f) => {
|
|
f.set_cancel(cancelled);
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
// -------------------------------------------------------------------
|
|
// Group family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// `oaknode_group_create` — detached group node.
|
|
pub fn oaknode_group_create() -> CHandle {
|
|
make_detached(oaknode::nodes::group::create())
|
|
}
|
|
|
|
/// `oaknode_group_cast` — the same node when it is a group, else null.
|
|
pub fn oaknode_group_cast(node: CHandle) -> CHandle {
|
|
if is_type(node, "org.olivevideoeditor.Olive.group") {
|
|
addref_copy(node)
|
|
} else {
|
|
CHandle::null()
|
|
}
|
|
}
|
|
|
|
/// `oaknode_group_free` — same shell release as the node family.
|
|
pub fn oaknode_group_free(group: *mut CHandle) {
|
|
if group.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *group };
|
|
free_node_handle(h);
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *group = CHandle::null() };
|
|
}
|
|
|
|
/// `oaknode_group_add_input_passthrough` — mint a passthrough input on
|
|
/// the group mirroring `node`'s input; writes the minted id (two-stage
|
|
/// getter convention).
|
|
pub fn oaknode_group_add_input_passthrough(
|
|
group: CHandle,
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
element: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let g_nr = match unsafe { node_ref_of(&group) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let n_id = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&g_nr.0);
|
|
let descriptor = match g
|
|
.graph
|
|
.get(n_id)
|
|
.and_then(|e| e.core.get_input(&input_id))
|
|
.cloned()
|
|
{
|
|
Some(d) => d,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let entry = match g.graph.get_mut(g_nr.1) {
|
|
Some(e) => e,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let (core, behavior) = (&mut entry.core, &mut entry.behavior);
|
|
let group_b = match behavior.as_any_mut().and_then(|a| a.downcast_mut::<oaknode::nodes::group::NodeGroup>()) {
|
|
Some(b) => b,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let id = group_b.add_input_passthrough(
|
|
core,
|
|
oaknode::nodes::group::InnerInput {
|
|
node: n_id,
|
|
input: input_id,
|
|
element,
|
|
},
|
|
"",
|
|
&descriptor,
|
|
);
|
|
string_out(&id, buf, buf_size)
|
|
}
|
|
|
|
/// `oaknode_group_add_input_passthrough_undoable`.
|
|
pub fn oaknode_group_add_input_passthrough_undoable(
|
|
group: CHandle,
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
element: c_int,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let g_nr = match unsafe { node_ref_of(&group) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let n_id = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, g_id) = g_nr;
|
|
let descriptor = {
|
|
let g = lock(&project);
|
|
match g
|
|
.graph
|
|
.get(n_id)
|
|
.and_then(|e| e.core.get_input(&input_id))
|
|
.cloned()
|
|
{
|
|
Some(d) => d,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
}
|
|
};
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let input1 = input_id.clone();
|
|
let input2 = input_id;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(entry) = g.graph.get_mut(g_id) {
|
|
let (core, behavior) = (&mut entry.core, &mut entry.behavior);
|
|
if let Some(b) = behavior
|
|
.as_any_mut()
|
|
.and_then(|a| a.downcast_mut::<oaknode::nodes::group::NodeGroup>())
|
|
{
|
|
b.add_input_passthrough(
|
|
core,
|
|
oaknode::nodes::group::InnerInput {
|
|
node: n_id,
|
|
input: input1.clone(),
|
|
element,
|
|
},
|
|
"",
|
|
&descriptor,
|
|
);
|
|
}
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(entry) = g.graph.get_mut(g_id) {
|
|
let (core, behavior) = (&mut entry.core, &mut entry.behavior);
|
|
if let Some(b) = behavior
|
|
.as_any_mut()
|
|
.and_then(|a| a.downcast_mut::<oaknode::nodes::group::NodeGroup>())
|
|
{
|
|
b.remove_input_passthrough(
|
|
core,
|
|
&oaknode::nodes::group::InnerInput {
|
|
node: n_id,
|
|
input: input2.clone(),
|
|
element,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_group_remove_input_passthrough`.
|
|
pub fn oaknode_group_remove_input_passthrough(
|
|
group: CHandle,
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
element: c_int,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let g_nr = match unsafe { node_ref_of(&group) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let n_id = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&g_nr.0);
|
|
let entry = match g.graph.get_mut(g_nr.1) {
|
|
Some(e) => e,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let (core, behavior) = (&mut entry.core, &mut entry.behavior);
|
|
let group_b = match behavior.as_any_mut().and_then(|a| a.downcast_mut::<oaknode::nodes::group::NodeGroup>()) {
|
|
Some(b) => b,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
group_b.remove_input_passthrough(
|
|
core,
|
|
&oaknode::nodes::group::InnerInput {
|
|
node: n_id,
|
|
input: input_id,
|
|
element,
|
|
},
|
|
);
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_group_passthrough_count`.
|
|
pub fn oaknode_group_passthrough_count(group: CHandle, out_count: *mut c_int) -> c_int {
|
|
if out_count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(group, |g, id| {
|
|
behavior_of::<oaknode::nodes::group::NodeGroup>(g, id)
|
|
.map(|b| b.passthroughs().len())
|
|
}) {
|
|
Some(Some(n)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_count = n as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_group_passthrough_id_at` (two-stage).
|
|
pub fn oaknode_group_passthrough_id_at(
|
|
group: CHandle,
|
|
index: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if index < 0 {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
match with_node(group, |g, id| {
|
|
behavior_of::<oaknode::nodes::group::NodeGroup>(g, id).and_then(|b| {
|
|
b.passthroughs()
|
|
.get(index as usize)
|
|
.map(|(pid, _)| pid.clone())
|
|
})
|
|
}) {
|
|
Some(Some(s)) => string_out(&s, buf, buf_size),
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_group_passthrough_input_at` — inner node + input id +
|
|
/// element (string via the two-stage convention).
|
|
pub fn oaknode_group_passthrough_input_at(
|
|
group: CHandle,
|
|
index: c_int,
|
|
out_node: *mut CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
out_element: *mut c_int,
|
|
) -> c_int {
|
|
if out_node.is_null() || out_element.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&group) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let inner = {
|
|
let g = lock(&project);
|
|
behavior_of::<oaknode::nodes::group::NodeGroup>(&g.graph, id)
|
|
.and_then(|b| b.passthroughs().get(index as usize))
|
|
.map(|(_, inner)| inner.clone())
|
|
};
|
|
match inner {
|
|
Some(inner) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_node = make_node_handle(project, inner.node, false);
|
|
*out_element = inner.element;
|
|
}
|
|
string_out(&inner.input, buf, buf_size)
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_group_get_output_passthrough` — null when unset.
|
|
pub fn oaknode_group_get_output_passthrough(group: CHandle, out_node: *mut CHandle) -> c_int {
|
|
if out_node.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&group) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let target = {
|
|
let g = lock(&project);
|
|
behavior_of::<oaknode::nodes::group::NodeGroup>(&g.graph, id)
|
|
.and_then(|b| b.output_passthrough())
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out_node = match target {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_group_set_output_passthrough`.
|
|
pub fn oaknode_group_set_output_passthrough(group: CHandle, node: CHandle) -> c_int {
|
|
let g_nr = match unsafe { node_ref_of(&group) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let n_id = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&g_nr.0);
|
|
match behavior_of_mut::<oaknode::nodes::group::NodeGroup>(&mut g.graph, g_nr.1) {
|
|
Some(b) => {
|
|
b.set_output_passthrough(Some(n_id));
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_group_set_output_passthrough_undoable`.
|
|
pub fn oaknode_group_set_output_passthrough_undoable(
|
|
group: CHandle,
|
|
node: CHandle,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let g_nr = match unsafe { node_ref_of(&group) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let n_id = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, g_id) = g_nr;
|
|
let old = {
|
|
let g = lock(&project);
|
|
behavior_of::<oaknode::nodes::group::NodeGroup>(&g.graph, g_id)
|
|
.and_then(|b| b.output_passthrough())
|
|
};
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(b) =
|
|
behavior_of_mut::<oaknode::nodes::group::NodeGroup>(&mut g.graph, g_id)
|
|
{
|
|
b.set_output_passthrough(Some(n_id));
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(b) =
|
|
behavior_of_mut::<oaknode::nodes::group::NodeGroup>(&mut g.graph, g_id)
|
|
{
|
|
b.set_output_passthrough(old);
|
|
}
|
|
},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_group_resolve_input` — resolve a group input through the
|
|
/// passthrough table (string via the two-stage convention; node null
|
|
/// when the input does not map to a passthrough).
|
|
pub fn oaknode_group_resolve_input(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
element: c_int,
|
|
out_node: *mut CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
out_element: *mut c_int,
|
|
) -> c_int {
|
|
if out_node.is_null() || out_element.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let resolved = {
|
|
let g = lock(&project);
|
|
let group_b = match behavior_of::<oaknode::nodes::group::NodeGroup>(&g.graph, id) {
|
|
Some(b) => b,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
match group_b.input_from_id(&input_id) {
|
|
Some(inner) => {
|
|
Some(oaknode::nodes::group::NodeGroup::resolve_input(&g.graph, inner.clone()))
|
|
}
|
|
None => None,
|
|
}
|
|
};
|
|
match resolved {
|
|
Some(inner) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_node = make_node_handle(project, inner.node, false);
|
|
*out_element = inner.element;
|
|
}
|
|
string_out(&inner.input, buf, buf_size)
|
|
}
|
|
None => {
|
|
// SAFETY: valid out pointers; the engine substitutes the
|
|
// group itself for a null resolved node.
|
|
unsafe {
|
|
*out_node = CHandle::null();
|
|
*out_element = element;
|
|
}
|
|
string_out(&input_id, buf, buf_size)
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Multicam family (static input ids + math over the multicam node)
|
|
// -------------------------------------------------------------------
|
|
|
|
/// `oaknode_multicam_input_current`.
|
|
pub fn oaknode_multicam_input_current() -> *const c_char {
|
|
static S: &[u8] = b"current_in\0";
|
|
S.as_ptr() as *const c_char
|
|
}
|
|
|
|
/// `oaknode_multicam_input_sources`.
|
|
pub fn oaknode_multicam_input_sources() -> *const c_char {
|
|
static S: &[u8] = b"sources_in\0";
|
|
S.as_ptr() as *const c_char
|
|
}
|
|
|
|
/// `oaknode_multicam_input_sequence`.
|
|
pub fn oaknode_multicam_input_sequence() -> *const c_char {
|
|
static S: &[u8] = b"sequence_in\0";
|
|
S.as_ptr() as *const c_char
|
|
}
|
|
|
|
/// `oaknode_multicam_input_sequence_type`.
|
|
pub fn oaknode_multicam_input_sequence_type() -> *const c_char {
|
|
static S: &[u8] = b"sequence_type_in\0";
|
|
S.as_ptr() as *const c_char
|
|
}
|
|
|
|
/// `oaknode_multicam_get_source_count` — the `sources_in` array size
|
|
/// (the domain query is core-only and not re-exported, so it is read
|
|
/// directly).
|
|
pub fn oaknode_multicam_get_source_count(node: CHandle, out_count: *mut c_int) -> c_int {
|
|
if out_count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(node, |g, id| {
|
|
g.get(id)
|
|
.map(|e| e.core.input_array_size("sources_in") as i32)
|
|
}) {
|
|
Some(Some(n)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_count = n };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_multicam_get_rows_and_columns` — the C++ grid math (the
|
|
/// multicam node module is private, so the statics are re-implemented
|
|
/// here).
|
|
pub fn oaknode_multicam_get_rows_and_columns(
|
|
source_count: c_int,
|
|
rows: *mut c_int,
|
|
cols: *mut c_int,
|
|
) -> c_int {
|
|
if rows.is_null() || cols.is_null() || source_count < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let (mut r, mut c) = (1, 1);
|
|
while r * c < source_count {
|
|
if r < c {
|
|
r += 1;
|
|
} else {
|
|
c += 1;
|
|
}
|
|
}
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*rows = r;
|
|
*cols = c;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_multicam_index_to_row_cols` — `row = index / cols`,
|
|
/// `col = index % cols` (C++ parity; `rows` is unused there too).
|
|
pub fn oaknode_multicam_index_to_row_cols(
|
|
index: c_int,
|
|
rows: c_int,
|
|
cols: c_int,
|
|
out_row: *mut c_int,
|
|
out_col: *mut c_int,
|
|
) -> c_int {
|
|
if out_row.is_null() || out_col.is_null() || index < 0 || rows < 1 || cols < 1 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_row = index / cols;
|
|
*out_col = index % cols;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_multicam_rows_cols_to_index` — `col + row * cols` (C++
|
|
/// parity).
|
|
pub fn oaknode_multicam_rows_cols_to_index(
|
|
row: c_int,
|
|
col: c_int,
|
|
rows: c_int,
|
|
cols: c_int,
|
|
) -> c_int {
|
|
if row < 0 || col < 0 || rows < 1 || cols < 1 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
col + row * cols
|
|
}
|
|
|
|
/// `oaknode_multicam_get_current_source` — the `current_in` standard
|
|
/// value as int (the domain query is core-only and not re-exported).
|
|
pub fn oaknode_multicam_get_current_source(node: CHandle, out_source: *mut c_int) -> c_int {
|
|
if out_source.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(node, |g, id| {
|
|
g.get(id)
|
|
.map(|e| e.core.standard_value("current_in", -1).to_double() as i32)
|
|
}) {
|
|
Some(Some(s)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_source = s };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
// -------------------------------------------------------------------
|
|
// Sequence family
|
|
// -------------------------------------------------------------------
|
|
|
|
fn seq_behavior(h: CHandle) -> Option<&'static oaknode::sequence::SequenceBehavior> {
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
let p = lock(&nr.project);
|
|
let b = behavior_of::<oaknode::sequence::SequenceBehavior>(&p.graph, nr.id)?;
|
|
// SAFETY: the node lives in the arena; the project outlives every
|
|
// handle that references it.
|
|
unsafe { Some(&*(b as *const _)) }
|
|
}
|
|
|
|
fn seq_behavior_mut(h: CHandle) -> Option<&'static mut oaknode::sequence::SequenceBehavior> {
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
let mut p = lock(&nr.project);
|
|
let b = behavior_of_mut::<oaknode::sequence::SequenceBehavior>(&mut p.graph, nr.id)?;
|
|
// SAFETY: see `seq_behavior`.
|
|
unsafe { Some(&mut *(b as *mut _)) }
|
|
}
|
|
|
|
/// Block-range accessor backed by the graph arena.
|
|
struct GraphBlockRange<'a>(&'a Graph);
|
|
|
|
impl<'a> oaknode::track::BlockRange for GraphBlockRange<'a> {
|
|
fn in_(&self, block: NodeId) -> Rational {
|
|
block_core(self.0, block)
|
|
.map(|c| c.in_())
|
|
.unwrap_or_else(|| Rational::new(0, 1))
|
|
}
|
|
|
|
fn out(&self, block: NodeId) -> Rational {
|
|
block_core(self.0, block)
|
|
.map(|c| c.out())
|
|
.unwrap_or_else(|| Rational::new(0, 1))
|
|
}
|
|
}
|
|
|
|
/// Track-length accessor backed by the graph arena.
|
|
struct GraphTrackRange<'a>(&'a Graph);
|
|
|
|
impl<'a> oaknode::track::TrackRange for GraphTrackRange<'a> {
|
|
fn length(&self, track: NodeId) -> Rational {
|
|
let blocks = GraphBlockRange(self.0);
|
|
behavior_of::<oaknode::track::TrackBehavior>(self.0, track)
|
|
.map(|t| t.length(&blocks))
|
|
.unwrap_or_else(|| Rational::new(0, 1))
|
|
}
|
|
}
|
|
|
|
/// The block core of a block node (clip/gap/transition).
|
|
fn block_core(g: &Graph, id: NodeId) -> Option<&oaknode::block::BlockCore> {
|
|
let e = g.get(id)?;
|
|
if let Some(clip) = e.behavior.as_any().and_then(|a| a.downcast_ref::<oaknode::block::ClipBlockBehavior>())
|
|
{
|
|
return Some(&clip.core);
|
|
}
|
|
if let Some(gap) = e.behavior.as_any().and_then(|a| a.downcast_ref::<oaknode::block::GapBlockBehavior>())
|
|
{
|
|
return Some(&gap.core);
|
|
}
|
|
if let Some(tr) = e
|
|
.behavior
|
|
.as_any()
|
|
.and_then(|a| a.downcast_ref::<oaknode::block::TransitionBlockBehavior>())
|
|
{
|
|
return Some(&tr.core);
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Mutable block core.
|
|
fn block_core_mut<'a>(g: &'a mut Graph, id: NodeId) -> Option<&'a mut oaknode::block::BlockCore> {
|
|
let e = g.get_mut(id)?;
|
|
let a = e.behavior.as_any_mut()?;
|
|
if a.is::<oaknode::block::ClipBlockBehavior>() {
|
|
return Some(&mut a
|
|
.downcast_mut::<oaknode::block::ClipBlockBehavior>()
|
|
.expect("is-checked")
|
|
.core);
|
|
}
|
|
if a.is::<oaknode::block::GapBlockBehavior>() {
|
|
return Some(&mut a
|
|
.downcast_mut::<oaknode::block::GapBlockBehavior>()
|
|
.expect("is-checked")
|
|
.core);
|
|
}
|
|
if a.is::<oaknode::block::TransitionBlockBehavior>() {
|
|
return Some(&mut a
|
|
.downcast_mut::<oaknode::block::TransitionBlockBehavior>()
|
|
.expect("is-checked")
|
|
.core);
|
|
}
|
|
None
|
|
}
|
|
|
|
/// `oaknode_sequence_create` — detached sequence node.
|
|
pub fn oaknode_sequence_create() -> CHandle {
|
|
make_detached(oaknode::sequence::SequenceBehavior::create())
|
|
}
|
|
|
|
/// `oaknode_sequence_free` — same shell release as the node family.
|
|
pub fn oaknode_sequence_free(sequence: *mut CHandle) {
|
|
if sequence.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *sequence };
|
|
free_node_handle(h);
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *sequence = CHandle::null() };
|
|
}
|
|
|
|
/// `oaknode_sequence_set_default_parameters`.
|
|
pub fn oaknode_sequence_set_default_parameters(sequence: CHandle) -> c_int {
|
|
match seq_behavior_mut(sequence) {
|
|
Some(s) => {
|
|
s.set_default_parameters();
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_sequence_as_node` — identity cast (addref'd copy).
|
|
pub fn oaknode_sequence_as_node(sequence: CHandle) -> CHandle {
|
|
addref_copy(sequence)
|
|
}
|
|
|
|
/// `oaknode_sequence_from_node` — the same node when it is a sequence,
|
|
/// else null.
|
|
pub fn oaknode_sequence_from_node(node: CHandle) -> CHandle {
|
|
if is_type(node, "org.olivevideoeditor.Olive.sequence") {
|
|
addref_copy(node)
|
|
} else {
|
|
CHandle::null()
|
|
}
|
|
}
|
|
|
|
/// Find (or create) the track list of `kind` on the sequence.
|
|
fn sequence_track_list_id(project: &ProjectArc, seq_id: NodeId, kind: oaknode::track::TrackType) -> Option<NodeId> {
|
|
// Find an existing list of the kind.
|
|
{
|
|
let g = lock(project);
|
|
let seq = behavior_of::<oaknode::sequence::SequenceBehavior>(&g.graph, seq_id)?;
|
|
for tl in &seq.track_lists {
|
|
if let Some(list) = behavior_of::<oaknode::track::TrackListBehavior>(&g.graph, *tl)
|
|
{
|
|
if list.kind == kind {
|
|
return Some(*tl);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Create it: the list is a graph node owned by the sequence.
|
|
let mut g = lock(project);
|
|
let (core, mut behavior) = oaknode::track::TrackListBehavior::create();
|
|
if let Some(a) = behavior.as_any_mut() {
|
|
if let Some(list) = a.downcast_mut::<oaknode::track::TrackListBehavior>() {
|
|
list.kind = kind;
|
|
let base = match behavior_of::<oaknode::sequence::SequenceBehavior>(&g.graph, seq_id)
|
|
{
|
|
Some(s) => s.track_lists.len() as i32,
|
|
None => return None,
|
|
};
|
|
list.array_base = base;
|
|
}
|
|
}
|
|
let list_id = g.graph.add_node(core, behavior);
|
|
if let Some(seq) = behavior_of_mut::<oaknode::sequence::SequenceBehavior>(&mut g.graph, seq_id)
|
|
{
|
|
seq.track_lists.push(list_id);
|
|
}
|
|
if let Some(list) = behavior_of_mut::<oaknode::track::TrackListBehavior>(&mut g.graph, list_id)
|
|
{
|
|
list.sequence = Some(seq_id);
|
|
}
|
|
Some(list_id)
|
|
}
|
|
|
|
/// `oaknode_sequence_get_track_list` — find-or-create the list of the
|
|
/// given type (borrowed handle).
|
|
pub fn oaknode_sequence_get_track_list(
|
|
sequence: CHandle,
|
|
type_: c_int,
|
|
out: *mut CHandle,
|
|
) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let kind = match oaknode::track::TrackType::from_c(type_) {
|
|
Some(k) => k,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let nr = match unsafe { node_ref_of(&sequence) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
match sequence_track_list_id(&project, id, kind) {
|
|
Some(list_id) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = make_node_handle(project, list_id, false) };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_sequence_get_track_count` — tracks of `type_`.
|
|
pub fn oaknode_sequence_get_track_count(
|
|
sequence: CHandle,
|
|
type_: c_int,
|
|
count: *mut c_int,
|
|
) -> c_int {
|
|
if count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let kind = match oaknode::track::TrackType::from_c(type_) {
|
|
Some(k) => k,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let nr = match unsafe { node_ref_of(&sequence) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let n = match sequence_track_list_id(&project, id, kind) {
|
|
Some(list_id) => {
|
|
let g = lock(&project);
|
|
behavior_of::<oaknode::track::TrackListBehavior>(&g.graph, list_id)
|
|
.map(|l| l.tracks.len())
|
|
.unwrap_or(0)
|
|
}
|
|
None => 0,
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *count = n as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_sequence_get_track_at`.
|
|
pub fn oaknode_sequence_get_track_at(
|
|
sequence: CHandle,
|
|
type_: c_int,
|
|
index: c_int,
|
|
out: *mut CHandle,
|
|
) -> c_int {
|
|
if out.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let kind = match oaknode::track::TrackType::from_c(type_) {
|
|
Some(k) => k,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let nr = match unsafe { node_ref_of(&sequence) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let target = match sequence_track_list_id(&project, id, kind) {
|
|
Some(list_id) => {
|
|
let g = lock(&project);
|
|
behavior_of::<oaknode::track::TrackListBehavior>(&g.graph, list_id)
|
|
.and_then(|l| l.tracks.get(index as usize).copied())
|
|
}
|
|
None => None,
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match target {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
if target.is_some() {
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
|
|
/// All track lists of the sequence (list ids).
|
|
fn sequence_track_lists(h: CHandle) -> Option<Vec<NodeId>> {
|
|
with_node(h, |g, id| {
|
|
behavior_of::<oaknode::sequence::SequenceBehavior>(g, id)
|
|
.map(|s| s.track_lists.clone())
|
|
})?
|
|
}
|
|
|
|
/// `oaknode_sequence_get_all_track_count`.
|
|
pub fn oaknode_sequence_get_all_track_count(sequence: CHandle, count: *mut c_int) -> c_int {
|
|
if count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let lists = match sequence_track_lists(sequence) {
|
|
Some(l) => l,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let nr = unsafe { node_ref_of(&sequence) }.map(|n| n.project.clone());
|
|
let Some(project) = nr else {
|
|
return OAKNODE_E_INVALID;
|
|
};
|
|
let g = lock(&project);
|
|
let mut total = 0usize;
|
|
for list_id in &lists {
|
|
if let Some(l) = behavior_of::<oaknode::track::TrackListBehavior>(&g.graph, *list_id) {
|
|
total += l.tracks.len();
|
|
}
|
|
}
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *count = total as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_sequence_get_all_track_at` — flat track index across all
|
|
/// lists.
|
|
pub fn oaknode_sequence_get_all_track_at(
|
|
sequence: CHandle,
|
|
index: c_int,
|
|
out: *mut CHandle,
|
|
) -> c_int {
|
|
if out.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&sequence) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let target = {
|
|
let g = lock(&project);
|
|
let seq = behavior_of::<oaknode::sequence::SequenceBehavior>(&g.graph, id);
|
|
let mut flat = index as usize;
|
|
let mut target = None;
|
|
if let Some(seq) = seq {
|
|
for list_id in &seq.track_lists {
|
|
if let Some(l) =
|
|
behavior_of::<oaknode::track::TrackListBehavior>(&g.graph, *list_id)
|
|
{
|
|
if flat < l.tracks.len() {
|
|
target = l.tracks.get(flat).copied();
|
|
break;
|
|
}
|
|
flat -= l.tracks.len();
|
|
}
|
|
}
|
|
}
|
|
target
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match target {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
if target.is_some() {
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
|
|
/// `oaknode_sequence_get_playhead`.
|
|
pub fn oaknode_sequence_get_playhead(
|
|
sequence: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if numerator.is_null() || denominator.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match seq_behavior(sequence) {
|
|
Some(s) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = s.playhead.numerator() as c_int;
|
|
*denominator = s.playhead.denominator() as c_int;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_sequence_set_playhead`.
|
|
pub fn oaknode_sequence_set_playhead(
|
|
sequence: CHandle,
|
|
numerator: c_int,
|
|
denominator: c_int,
|
|
) -> c_int {
|
|
if denominator == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match seq_behavior_mut(sequence) {
|
|
Some(s) => {
|
|
s.playhead = Rational::new(numerator as i64, denominator as i64);
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// The sequence's overall content length (max track length).
|
|
fn sequence_length(h: CHandle) -> Option<Rational> {
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
let g = lock(&nr.project);
|
|
let seq = behavior_of::<oaknode::sequence::SequenceBehavior>(&g.graph, nr.id)?;
|
|
let tracks = GraphTrackRange(&g.graph);
|
|
let blocks = GraphBlockRange(&g.graph);
|
|
let mut longest = Rational::new(0, 1);
|
|
for list_id in &seq.track_lists {
|
|
if let Some(list) = behavior_of::<oaknode::track::TrackListBehavior>(&g.graph, *list_id) {
|
|
let len = list.total_length(&tracks);
|
|
if len > longest {
|
|
longest = len;
|
|
}
|
|
let _ = &blocks;
|
|
}
|
|
}
|
|
Some(longest)
|
|
}
|
|
|
|
/// `oaknode_sequence_get_length`.
|
|
pub fn oaknode_sequence_get_length(
|
|
sequence: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if numerator.is_null() || denominator.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match sequence_length(sequence) {
|
|
Some(l) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = l.numerator() as c_int;
|
|
*denominator = l.denominator() as c_int;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// Length of the video track list.
|
|
fn sequence_length_of_type(h: CHandle, kind: oaknode::track::TrackType) -> Option<Rational> {
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
let g = lock(&nr.project);
|
|
let seq = behavior_of::<oaknode::sequence::SequenceBehavior>(&g.graph, nr.id)?;
|
|
let tracks = GraphTrackRange(&g.graph);
|
|
for list_id in &seq.track_lists {
|
|
if let Some(list) = behavior_of::<oaknode::track::TrackListBehavior>(&g.graph, *list_id) {
|
|
if list.kind == kind {
|
|
return Some(list.total_length(&tracks));
|
|
}
|
|
}
|
|
}
|
|
Some(Rational::new(0, 1))
|
|
}
|
|
|
|
/// `oaknode_sequence_get_video_length`.
|
|
pub fn oaknode_sequence_get_video_length(
|
|
sequence: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if numerator.is_null() || denominator.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match sequence_length_of_type(sequence, oaknode::track::TrackType::Video) {
|
|
Some(l) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = l.numerator() as c_int;
|
|
*denominator = l.denominator() as c_int;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_sequence_get_audio_length`.
|
|
pub fn oaknode_sequence_get_audio_length(
|
|
sequence: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if numerator.is_null() || denominator.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match sequence_length_of_type(sequence, oaknode::track::TrackType::Audio) {
|
|
Some(l) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = l.numerator() as c_int;
|
|
*denominator = l.denominator() as c_int;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_sequence_verify_length` — recompute and cache the lengths.
|
|
pub fn oaknode_sequence_verify_length(sequence: CHandle) -> c_int {
|
|
let overall = match sequence_length(sequence) {
|
|
Some(l) => l,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let video = sequence_length_of_type(sequence, oaknode::track::TrackType::Video)
|
|
.unwrap_or_else(|| Rational::new(0, 1));
|
|
let audio = sequence_length_of_type(sequence, oaknode::track::TrackType::Audio)
|
|
.unwrap_or_else(|| Rational::new(0, 1));
|
|
match seq_behavior_mut(sequence) {
|
|
Some(s) => {
|
|
s.verify_length((video, audio, overall));
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_sequence_get_video_stream_count`.
|
|
pub fn oaknode_sequence_get_video_stream_count(sequence: CHandle, count: *mut c_int) -> c_int {
|
|
if count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match seq_behavior(sequence) {
|
|
Some(s) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *count = s.video_stream_count() as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_sequence_get_audio_stream_count`.
|
|
pub fn oaknode_sequence_get_audio_stream_count(sequence: CHandle, count: *mut c_int) -> c_int {
|
|
if count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match seq_behavior(sequence) {
|
|
Some(s) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *count = s.audio_stream_count() as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_sequence_get_video_params` — stream `index` as an
|
|
/// oakcommon params handle.
|
|
pub fn oaknode_sequence_get_video_params(
|
|
sequence: CHandle,
|
|
index: c_int,
|
|
out: *mut CHandle,
|
|
) -> c_int {
|
|
if out.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match seq_behavior(sequence).and_then(|s| s.video_params.get(index as usize)) {
|
|
Some(v) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = vp_handle(v) };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_sequence_set_video_params`.
|
|
pub fn oaknode_sequence_set_video_params(
|
|
sequence: CHandle,
|
|
index: c_int,
|
|
params: CHandle,
|
|
) -> c_int {
|
|
if index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller passes a live oakcommon videoparams handle.
|
|
let converted = unsafe { vp_from_handle(params) };
|
|
let Some(converted) = converted else {
|
|
return OAKNODE_E_INVALID;
|
|
};
|
|
match seq_behavior_mut(sequence) {
|
|
Some(s) => {
|
|
if index as usize >= s.video_params.len() {
|
|
s.video_params.resize(index as usize + 1, converted.clone());
|
|
}
|
|
s.video_params[index as usize] = converted;
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_sequence_get_audio_params` — host oakcore audioparams
|
|
/// pointer.
|
|
pub fn oaknode_sequence_get_audio_params(
|
|
sequence: CHandle,
|
|
index: c_int,
|
|
out: *mut *mut c_void,
|
|
) -> c_int {
|
|
if out.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let params = match seq_behavior(sequence).and_then(|s| s.audio_params.get(index as usize)) {
|
|
Some(p) => *p,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
// SAFETY: the oakcore audioparams contract.
|
|
let ptr = unsafe {
|
|
crate::stubs::audio::oakcore_audioparams_create(
|
|
params.sample_rate,
|
|
params.channel_layout,
|
|
params.format,
|
|
)
|
|
};
|
|
if ptr.is_null() {
|
|
return OAKNODE_E_NOMEM;
|
|
}
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = ptr };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_sequence_set_audio_params`.
|
|
pub fn oaknode_sequence_set_audio_params(
|
|
sequence: CHandle,
|
|
index: c_int,
|
|
params: *const c_void,
|
|
) -> c_int {
|
|
if params.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the oakcore audioparams contract.
|
|
let converted = unsafe {
|
|
oaknode::value::AudioParams {
|
|
sample_rate: crate::stubs::audio::oakcore_audioparams_sample_rate(params),
|
|
channel_layout: crate::stubs::audio::oakcore_audioparams_channel_layout(params),
|
|
format: crate::stubs::audio::oakcore_audioparams_format(params),
|
|
}
|
|
};
|
|
match seq_behavior_mut(sequence) {
|
|
Some(s) => {
|
|
if index as usize >= s.audio_params.len() {
|
|
s.audio_params.resize(index as usize + 1, converted.clone());
|
|
}
|
|
s.audio_params[index as usize] = converted;
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
// -------------------------------------------------------------------
|
|
// Track family
|
|
// -------------------------------------------------------------------
|
|
|
|
fn track_behavior(h: CHandle) -> Option<&'static oaknode::track::TrackBehavior> {
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
let p = lock(&nr.project);
|
|
let b = behavior_of::<oaknode::track::TrackBehavior>(&p.graph, nr.id)?;
|
|
// SAFETY: the node lives in the arena; the project outlives every
|
|
// handle that references it.
|
|
unsafe { Some(&*(b as *const _)) }
|
|
}
|
|
|
|
fn track_behavior_mut(h: CHandle) -> Option<&'static mut oaknode::track::TrackBehavior> {
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
let mut p = lock(&nr.project);
|
|
let b = behavior_of_mut::<oaknode::track::TrackBehavior>(&mut p.graph, nr.id)?;
|
|
// SAFETY: see `track_behavior`.
|
|
unsafe { Some(&mut *(b as *mut _)) }
|
|
}
|
|
|
|
fn tracklist_behavior(h: CHandle) -> Option<&'static oaknode::track::TrackListBehavior> {
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
let p = lock(&nr.project);
|
|
let b = behavior_of::<oaknode::track::TrackListBehavior>(&p.graph, nr.id)?;
|
|
// SAFETY: see `track_behavior`.
|
|
unsafe { Some(&*(b as *const _)) }
|
|
}
|
|
|
|
fn tracklist_behavior_mut(h: CHandle) -> Option<&'static mut oaknode::track::TrackListBehavior> {
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
let mut p = lock(&nr.project);
|
|
let b = behavior_of_mut::<oaknode::track::TrackListBehavior>(&mut p.graph, nr.id)?;
|
|
// SAFETY: see `track_behavior`.
|
|
unsafe { Some(&mut *(b as *mut _)) }
|
|
}
|
|
|
|
/// `oaknode_track_as_node` — the same node when it is a track, else
|
|
/// null.
|
|
pub fn oaknode_track_as_node(track: CHandle) -> CHandle {
|
|
if is_type(track, "org.olivevideoeditor.Olive.track") {
|
|
addref_copy(track)
|
|
} else {
|
|
CHandle::null()
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_create` — detached track of the given type.
|
|
pub fn oaknode_track_create(type_: c_int) -> CHandle {
|
|
let kind = match oaknode::track::TrackType::from_c(type_) {
|
|
Some(k) => k,
|
|
None => return CHandle::null(),
|
|
};
|
|
let (core, behavior) = oaknode::track::TrackBehavior::create();
|
|
// `create()` always yields a video track; specialize the kind.
|
|
let mut b = behavior;
|
|
if let Some(a) = b.as_any_mut() {
|
|
if let Some(t) = a.downcast_mut::<oaknode::track::TrackBehavior>() {
|
|
t.kind = kind;
|
|
}
|
|
}
|
|
make_detached((core, b))
|
|
}
|
|
|
|
/// `oaknode_track_free` — same shell release as the node family.
|
|
pub fn oaknode_track_free(track: *mut CHandle) {
|
|
if track.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *track };
|
|
free_node_handle(h);
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *track = CHandle::null() };
|
|
}
|
|
|
|
/// `oaknode_track_get_type`.
|
|
pub fn oaknode_track_get_type(track: CHandle, type_: *mut c_int) -> c_int {
|
|
if type_.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match track_behavior(track) {
|
|
Some(t) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *type_ = t.kind.to_c() };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_set_type`.
|
|
pub fn oaknode_track_set_type(track: CHandle, type_: c_int) -> c_int {
|
|
let kind = match oaknode::track::TrackType::from_c(type_) {
|
|
Some(k) => k,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
match track_behavior_mut(track) {
|
|
Some(t) => {
|
|
t.kind = kind;
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_get_height`.
|
|
pub fn oaknode_track_get_height(track: CHandle, height: *mut f64) -> c_int {
|
|
if height.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match track_behavior(track) {
|
|
Some(t) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *height = t.height };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_set_height`.
|
|
pub fn oaknode_track_set_height(track: CHandle, height: f64) -> c_int {
|
|
match track_behavior_mut(track) {
|
|
Some(t) => {
|
|
t.height = height;
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_get_height_in_pixels`.
|
|
pub fn oaknode_track_get_height_in_pixels(track: CHandle, height: *mut c_int) -> c_int {
|
|
if height.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match track_behavior(track) {
|
|
Some(t) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*height = oaknode::track::internal_height_to_pixel_height(t.height);
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_set_height_in_pixels`.
|
|
pub fn oaknode_track_set_height_in_pixels(track: CHandle, height: c_int) -> c_int {
|
|
match track_behavior_mut(track) {
|
|
Some(t) => {
|
|
t.height = oaknode::track::pixel_height_to_internal_height(height);
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_get_default_height_in_pixels`.
|
|
pub fn oaknode_track_get_default_height_in_pixels() -> c_int {
|
|
oaknode::track::internal_height_to_pixel_height(
|
|
oaknode::track::DEFAULT_HEIGHT_INTERNAL,
|
|
)
|
|
}
|
|
|
|
/// `oaknode_track_get_minimum_height_in_pixels`.
|
|
pub fn oaknode_track_get_minimum_height_in_pixels() -> c_int {
|
|
oaknode::track::internal_height_to_pixel_height(
|
|
oaknode::track::MINIMUM_HEIGHT_INTERNAL,
|
|
)
|
|
}
|
|
|
|
/// `oaknode_track_get_index`.
|
|
pub fn oaknode_track_get_index(track: CHandle, index: *mut c_int) -> c_int {
|
|
if index.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match track_behavior(track) {
|
|
Some(t) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *index = t.index };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_set_index`.
|
|
pub fn oaknode_track_set_index(track: CHandle, index: c_int) -> c_int {
|
|
match track_behavior_mut(track) {
|
|
Some(t) => {
|
|
t.index = index;
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_get_muted`.
|
|
pub fn oaknode_track_get_muted(track: CHandle, muted: *mut c_int) -> c_int {
|
|
if muted.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match track_behavior(track) {
|
|
Some(t) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *muted = if t.muted { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_set_muted`.
|
|
pub fn oaknode_track_set_muted(track: CHandle, muted: c_int) -> c_int {
|
|
match track_behavior_mut(track) {
|
|
Some(t) => {
|
|
t.muted = muted != 0;
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_get_locked`.
|
|
pub fn oaknode_track_get_locked(track: CHandle, locked: *mut c_int) -> c_int {
|
|
if locked.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match track_behavior(track) {
|
|
Some(t) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *locked = if t.locked { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_set_locked`.
|
|
pub fn oaknode_track_set_locked(track: CHandle, locked: c_int) -> c_int {
|
|
match track_behavior_mut(track) {
|
|
Some(t) => {
|
|
t.locked = locked != 0;
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_get_reference` — (type, index).
|
|
pub fn oaknode_track_get_reference(
|
|
track: CHandle,
|
|
type_: *mut c_int,
|
|
index: *mut c_int,
|
|
) -> c_int {
|
|
if type_.is_null() || index.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match track_behavior(track) {
|
|
Some(t) => {
|
|
let (ty, idx) = t.reference();
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*type_ = ty;
|
|
*index = idx;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_get_length`.
|
|
pub fn oaknode_track_get_length(
|
|
track: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if numerator.is_null() || denominator.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&track) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let length = {
|
|
let g = lock(&project);
|
|
let blocks = GraphBlockRange(&g.graph);
|
|
behavior_of::<oaknode::track::TrackBehavior>(&g.graph, id)
|
|
.map(|t| t.length(&blocks))
|
|
};
|
|
match length {
|
|
Some(l) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = l.numerator() as c_int;
|
|
*denominator = l.denominator() as c_int;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_get_sequence` — the sequence owning this track (null
|
|
/// when detached).
|
|
pub fn oaknode_track_get_sequence(track: CHandle, out: *mut CHandle) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&track) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let sequence = {
|
|
let g = lock(&project);
|
|
let t = match behavior_of::<oaknode::track::TrackBehavior>(&g.graph, id) {
|
|
Some(t) => t,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
t.track_list.and_then(|list_id| {
|
|
behavior_of::<oaknode::track::TrackListBehavior>(&g.graph, list_id)
|
|
.and_then(|l| l.sequence)
|
|
})
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match sequence {
|
|
Some(s) => make_node_handle(project, s, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_track_get_block_count`.
|
|
pub fn oaknode_track_get_block_count(track: CHandle, count: *mut c_int) -> c_int {
|
|
if count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match track_behavior(track) {
|
|
Some(t) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *count = t.blocks.len() as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_get_block_at`.
|
|
pub fn oaknode_track_get_block_at(track: CHandle, index: c_int, out: *mut CHandle) -> c_int {
|
|
if out.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&track) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let target = {
|
|
let g = lock(&project);
|
|
behavior_of::<oaknode::track::TrackBehavior>(&g.graph, id)
|
|
.and_then(|t| t.blocks.get(index as usize).copied())
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match target {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
if target.is_some() {
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
|
|
/// Adopt `block` into the track's project and attach it to the track's
|
|
/// block list (live edit; used by the append/prepend/insert family).
|
|
fn track_attach_block(
|
|
track: CHandle,
|
|
block: CHandle,
|
|
index: Option<usize>,
|
|
after: Option<NodeId>,
|
|
before: Option<NodeId>,
|
|
) -> c_int {
|
|
let t_nr = match unsafe { node_ref_of(&track) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, t_id) = t_nr;
|
|
let b_nr = match unsafe { node_ref_of(&block) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (b_project, b_id) = b_nr;
|
|
// Adopt the block into the track's project (no-op when it already
|
|
// lives there).
|
|
if !Arc::ptr_eq(&b_project, &project) {
|
|
let entry = {
|
|
let mut s = lock(&b_project);
|
|
match s.graph.take_node(b_id) {
|
|
Some(e) => e,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
}
|
|
};
|
|
let new_id = {
|
|
let mut t = lock(&project);
|
|
t.graph.add_entry(entry, b_id)
|
|
};
|
|
// SAFETY: the shared node box is rewritten in place.
|
|
if let Some(boxed) = unsafe { node_ref_mut(&block) } {
|
|
boxed.project = project.clone();
|
|
boxed.id = new_id;
|
|
if boxed.owned.swap(false, Ordering::SeqCst) {
|
|
alive_dec();
|
|
}
|
|
}
|
|
}
|
|
let b_id = unsafe { node_ref_of(&block) }
|
|
.map(|n| n.id)
|
|
.unwrap_or(b_id);
|
|
let mut g = lock(&project);
|
|
let track_b = match behavior_of_mut::<oaknode::track::TrackBehavior>(&mut g.graph, t_id) {
|
|
Some(t) => t,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
if track_b.blocks.contains(&b_id) {
|
|
// Idempotent: an already-attached block is a success no-op.
|
|
return OAKNODE_OK;
|
|
}
|
|
let added = match (index, after, before) {
|
|
(Some(i), _, _) => {
|
|
track_b.insert_block_at_index(b_id, i);
|
|
true
|
|
}
|
|
(None, Some(a), _) => track_b.insert_block_after(b_id, a),
|
|
(None, None, Some(b)) => track_b.insert_block_before(b_id, b),
|
|
(None, None, None) => {
|
|
track_b.append_block(b_id);
|
|
true
|
|
}
|
|
};
|
|
if added {
|
|
if let Some(core) = block_core_mut(&mut g.graph, b_id) {
|
|
core.track = Some(t_id);
|
|
}
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_append_block`.
|
|
pub fn oaknode_track_append_block(track: CHandle, block: CHandle) -> c_int {
|
|
track_attach_block(track, block, None, None, None)
|
|
}
|
|
|
|
/// `oaknode_track_prepend_block`.
|
|
pub fn oaknode_track_prepend_block(track: CHandle, block: CHandle) -> c_int {
|
|
track_attach_block(track, block, Some(0), None, None)
|
|
}
|
|
|
|
/// `oaknode_track_insert_block_at_index`.
|
|
pub fn oaknode_track_insert_block_at_index(track: CHandle, block: CHandle, index: c_int) -> c_int {
|
|
if index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
track_attach_block(track, block, Some(index as usize), None, None)
|
|
}
|
|
|
|
/// `oaknode_track_insert_block_after`.
|
|
pub fn oaknode_track_insert_block_after(
|
|
track: CHandle,
|
|
block: CHandle,
|
|
before: CHandle,
|
|
) -> c_int {
|
|
let before_id = match unsafe { node_ref_of(&before) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
track_attach_block(track, block, None, Some(before_id), None)
|
|
}
|
|
|
|
/// `oaknode_track_insert_block_before`.
|
|
pub fn oaknode_track_insert_block_before(
|
|
track: CHandle,
|
|
block: CHandle,
|
|
after: CHandle,
|
|
) -> c_int {
|
|
let after_id = match unsafe { node_ref_of(&after) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
track_attach_block(track, block, None, None, Some(after_id))
|
|
}
|
|
|
|
/// `oaknode_track_ripple_remove_block`.
|
|
pub fn oaknode_track_ripple_remove_block(track: CHandle, block: CHandle) -> c_int {
|
|
let b_id = match unsafe { node_ref_of(&block) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
match track_behavior_mut(track) {
|
|
Some(t) => {
|
|
if t.ripple_remove_block(b_id) {
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_replace_block`.
|
|
pub fn oaknode_track_replace_block(track: CHandle, old_block: CHandle, new_block: CHandle) -> c_int {
|
|
let old_id = match unsafe { node_ref_of(&old_block) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let new_id = match unsafe { node_ref_of(&new_block) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
match track_behavior_mut(track) {
|
|
Some(t) => {
|
|
if t.replace_block(old_id, new_id) {
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_get_block_index`.
|
|
pub fn oaknode_track_get_block_index(
|
|
track: CHandle,
|
|
block: CHandle,
|
|
index: *mut c_int,
|
|
) -> c_int {
|
|
if index.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let b_id = match unsafe { node_ref_of(&block) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
match track_behavior(track) {
|
|
Some(t) => match t.block_index(b_id) {
|
|
Some(i) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *index = i as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
},
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_track_get_block_containing_time`.
|
|
pub fn oaknode_track_get_block_containing_time(
|
|
track: CHandle,
|
|
numerator: c_int,
|
|
denominator: c_int,
|
|
out: *mut CHandle,
|
|
) -> c_int {
|
|
if out.is_null() || denominator == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&track) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let target = {
|
|
let g = lock(&project);
|
|
let blocks = GraphBlockRange(&g.graph);
|
|
behavior_of::<oaknode::track::TrackBehavior>(&g.graph, id).and_then(|t| {
|
|
t.block_containing_time(Rational::new(numerator as i64, denominator as i64), &blocks)
|
|
})
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match target {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_track_get_visible_block_at_time`.
|
|
pub fn oaknode_track_get_visible_block_at_time(
|
|
track: CHandle,
|
|
numerator: c_int,
|
|
denominator: c_int,
|
|
out: *mut CHandle,
|
|
) -> c_int {
|
|
if out.is_null() || denominator == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&track) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let target = {
|
|
let g = lock(&project);
|
|
let blocks = GraphBlockRange(&g.graph);
|
|
behavior_of::<oaknode::track::TrackBehavior>(&g.graph, id).and_then(|t| {
|
|
t.visible_block_at_time(Rational::new(numerator as i64, denominator as i64), &blocks)
|
|
})
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match target {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_track_is_range_free`.
|
|
pub fn oaknode_track_is_range_free(
|
|
track: CHandle,
|
|
in_num: c_int,
|
|
in_den: c_int,
|
|
out_num: c_int,
|
|
out_den: c_int,
|
|
is_free: *mut c_int,
|
|
) -> c_int {
|
|
if is_free.is_null() || in_den == 0 || out_den == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&track) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let free = {
|
|
let g = lock(&project);
|
|
let blocks = GraphBlockRange(&g.graph);
|
|
behavior_of::<oaknode::track::TrackBehavior>(&g.graph, id)
|
|
.map(|t| {
|
|
t.is_range_free(
|
|
TimeRange::new(
|
|
Rational::new(in_num as i64, in_den as i64),
|
|
Rational::new(out_num as i64, out_den as i64),
|
|
),
|
|
&blocks,
|
|
)
|
|
})
|
|
.unwrap_or(false)
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *is_free = if free { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_track_get_nearest_block_before_or_at`.
|
|
pub fn oaknode_track_get_nearest_block_before_or_at(
|
|
track: CHandle,
|
|
numerator: c_int,
|
|
denominator: c_int,
|
|
out: *mut CHandle,
|
|
) -> c_int {
|
|
if out.is_null() || denominator == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&track) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let time = Rational::new(numerator as i64, denominator as i64);
|
|
let target = {
|
|
let g = lock(&project);
|
|
let t = match behavior_of::<oaknode::track::TrackBehavior>(&g.graph, id) {
|
|
Some(t) => t,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut best = None;
|
|
for b in &t.blocks {
|
|
let in_ = block_core(&g.graph, *b)
|
|
.map(|c| c.in_())
|
|
.unwrap_or_else(|| Rational::new(0, 1));
|
|
if in_ <= time {
|
|
best = Some(*b);
|
|
}
|
|
}
|
|
best
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match target {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_track_get_nearest_block_after_or_at`.
|
|
pub fn oaknode_track_get_nearest_block_after_or_at(
|
|
track: CHandle,
|
|
numerator: c_int,
|
|
denominator: c_int,
|
|
out: *mut CHandle,
|
|
) -> c_int {
|
|
if out.is_null() || denominator == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&track) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let time = Rational::new(numerator as i64, denominator as i64);
|
|
let target = {
|
|
let g = lock(&project);
|
|
let t = match behavior_of::<oaknode::track::TrackBehavior>(&g.graph, id) {
|
|
Some(t) => t,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
t.blocks.iter().copied().find(|b| {
|
|
block_core(&g.graph, *b)
|
|
.map(|c| c.in_() >= time)
|
|
.unwrap_or(false)
|
|
})
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match target {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Track list family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// `oaknode_tracklist_get_sequence` — the owning sequence (null when
|
|
/// detached).
|
|
pub fn oaknode_tracklist_get_sequence(list: CHandle, out: *mut CHandle) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&list) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let sequence = {
|
|
let g = lock(&project);
|
|
behavior_of::<oaknode::track::TrackListBehavior>(&g.graph, id).and_then(|l| l.sequence)
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match sequence {
|
|
Some(s) => make_node_handle(project, s, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// The list's index within its sequence's track-list vector (the
|
|
/// `track_in_%1` element base).
|
|
fn tracklist_input_base(list: CHandle) -> Option<(ProjectArc, NodeId, i32)> {
|
|
let nr = unsafe { node_ref_of(&list) }?;
|
|
let g = lock(&nr.project);
|
|
let l = behavior_of::<oaknode::track::TrackListBehavior>(&g.graph, nr.id)?;
|
|
let seq_id = l.sequence?;
|
|
let seq = behavior_of::<oaknode::sequence::SequenceBehavior>(&g.graph, seq_id)?;
|
|
let base = seq.track_lists.iter().position(|tl| *tl == nr.id)? as i32;
|
|
Some((nr.project.clone(), seq_id, base))
|
|
}
|
|
|
|
/// `oaknode_tracklist_get_track_input_id` (two-stage) — the sequence's
|
|
/// `track_in_%1` id for this list.
|
|
pub fn oaknode_tracklist_get_track_input_id(
|
|
list: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
match tracklist_input_base(list) {
|
|
Some((_, _, base)) => {
|
|
let id = oaknode::sequence::TRACK_INPUT_FORMAT.replace("%1", &base.to_string());
|
|
string_out(&id, buf, buf_size)
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_tracklist_array_append` — append an element to the owning
|
|
/// sequence's track-input array; returns the new element index.
|
|
pub fn oaknode_tracklist_array_append(list: CHandle) -> c_int {
|
|
let (project, seq_id, base) = match tracklist_input_base(list) {
|
|
Some(t) => t,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let input_id = oaknode::sequence::TRACK_INPUT_FORMAT.replace("%1", &base.to_string());
|
|
let mut g = lock(&project);
|
|
let size = g
|
|
.graph
|
|
.get(seq_id)
|
|
.and_then(|e| e.core.get_input(&input_id))
|
|
.map(|i| i.array_size)
|
|
.unwrap_or(0);
|
|
if let Some(e) = g.graph.get_mut(seq_id) {
|
|
e.core.input_array_insert(&input_id, size);
|
|
}
|
|
size as c_int
|
|
}
|
|
|
|
/// `oaknode_tracklist_array_remove_last`.
|
|
pub fn oaknode_tracklist_array_remove_last(list: CHandle) -> c_int {
|
|
let (project, seq_id, base) = match tracklist_input_base(list) {
|
|
Some(t) => t,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let input_id = oaknode::sequence::TRACK_INPUT_FORMAT.replace("%1", &base.to_string());
|
|
let mut g = lock(&project);
|
|
let size = g
|
|
.graph
|
|
.get(seq_id)
|
|
.and_then(|e| e.core.get_input(&input_id))
|
|
.map(|i| i.array_size)
|
|
.unwrap_or(0);
|
|
if size == 0 {
|
|
return OAKNODE_OK;
|
|
}
|
|
if let Some(e) = g.graph.get_mut(seq_id) {
|
|
e.core.input_array_remove(&input_id, size - 1);
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_tracklist_get_array_index_from_cache_index` — identity (the
|
|
/// cache and array indexes coincide in the Rust model).
|
|
pub fn oaknode_tracklist_get_array_index_from_cache_index(
|
|
list: CHandle,
|
|
cache_index: c_int,
|
|
out_index: *mut c_int,
|
|
) -> c_int {
|
|
if out_index.is_null() || cache_index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let _ = list;
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_index = cache_index };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_tracklist_get_type`.
|
|
pub fn oaknode_tracklist_get_type(list: CHandle, type_: *mut c_int) -> c_int {
|
|
if type_.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match tracklist_behavior(list) {
|
|
Some(l) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *type_ = l.kind.to_c() };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_tracklist_get_track_count`.
|
|
pub fn oaknode_tracklist_get_track_count(list: CHandle, count: *mut c_int) -> c_int {
|
|
if count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match tracklist_behavior(list) {
|
|
Some(l) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *count = l.tracks.len() as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_tracklist_get_track_at`.
|
|
pub fn oaknode_tracklist_get_track_at(list: CHandle, index: c_int, out: *mut CHandle) -> c_int {
|
|
if out.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&list) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let target = {
|
|
let g = lock(&project);
|
|
behavior_of::<oaknode::track::TrackListBehavior>(&g.graph, id)
|
|
.and_then(|l| l.tracks.get(index as usize).copied())
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match target {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
if target.is_some() {
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
|
|
/// `oaknode_tracklist_get_total_length`.
|
|
pub fn oaknode_tracklist_get_total_length(
|
|
list: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if numerator.is_null() || denominator.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&list) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let length = {
|
|
let g = lock(&project);
|
|
let tracks = GraphTrackRange(&g.graph);
|
|
behavior_of::<oaknode::track::TrackListBehavior>(&g.graph, id)
|
|
.map(|l| l.total_length(&tracks))
|
|
};
|
|
match length {
|
|
Some(l) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = l.numerator() as c_int;
|
|
*denominator = l.denominator() as c_int;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_tracklist_get_array_size` — the owning sequence's
|
|
/// track-input array size.
|
|
pub fn oaknode_tracklist_get_array_size(list: CHandle, size: *mut c_int) -> c_int {
|
|
if size.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let (project, seq_id, base) = match tracklist_input_base(list) {
|
|
Some(t) => t,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let input_id = oaknode::sequence::TRACK_INPUT_FORMAT.replace("%1", &base.to_string());
|
|
let g = lock(&project);
|
|
let n = g
|
|
.graph
|
|
.get(seq_id)
|
|
.and_then(|e| e.core.get_input(&input_id))
|
|
.map(|i| i.array_size)
|
|
.unwrap_or(0);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *size = n as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_tracklist_add_track` — live add (+ back-reference and
|
|
/// index).
|
|
pub fn oaknode_tracklist_add_track(list: CHandle, track: CHandle) -> c_int {
|
|
let l_nr = match unsafe { node_ref_of(&list) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let t_id = match unsafe { node_ref_of(&track) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, l_id) = l_nr;
|
|
let mut g = lock(&project);
|
|
let list_b = match behavior_of_mut::<oaknode::track::TrackListBehavior>(&mut g.graph, l_id) {
|
|
Some(l) => l,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let index = list_b.tracks.len() as i32;
|
|
if !list_b.tracks.contains(&t_id) {
|
|
list_b.tracks.push(t_id);
|
|
}
|
|
if let Some(t) = behavior_of_mut::<oaknode::track::TrackBehavior>(&mut g.graph, t_id) {
|
|
t.track_list = Some(l_id);
|
|
t.index = index;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_tracklist_remove_track` — live remove.
|
|
pub fn oaknode_tracklist_remove_track(list: CHandle, track: CHandle) -> c_int {
|
|
let l_nr = match unsafe { node_ref_of(&list) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let t_id = match unsafe { node_ref_of(&track) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&l_nr.0);
|
|
let list_b = match behavior_of_mut::<oaknode::track::TrackListBehavior>(&mut g.graph, l_nr.1) {
|
|
Some(l) => l,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
list_b.tracks.retain(|t| *t != t_id);
|
|
if let Some(t) = behavior_of_mut::<oaknode::track::TrackBehavior>(&mut g.graph, t_id) {
|
|
t.track_list = None;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
// -------------------------------------------------------------------
|
|
// Block family
|
|
// -------------------------------------------------------------------
|
|
|
|
const BLOCK_KIND_OTHER: c_int = 0;
|
|
const BLOCK_KIND_CLIP: c_int = 1;
|
|
const BLOCK_KIND_GAP: c_int = 2;
|
|
const BLOCK_KIND_TRANSITION: c_int = 3;
|
|
|
|
fn block_kind_of(h: CHandle) -> Option<c_int> {
|
|
with_node(h, |g, id| {
|
|
let e = g.get(id)?;
|
|
if e.behavior
|
|
.as_any()
|
|
.and_then(|a| a.downcast_ref::<oaknode::block::ClipBlockBehavior>())
|
|
.is_some()
|
|
{
|
|
return Some(BLOCK_KIND_CLIP);
|
|
}
|
|
if e.behavior
|
|
.as_any()
|
|
.and_then(|a| a.downcast_ref::<oaknode::block::GapBlockBehavior>())
|
|
.is_some()
|
|
{
|
|
return Some(BLOCK_KIND_GAP);
|
|
}
|
|
if e
|
|
.behavior
|
|
.as_any()
|
|
.and_then(|a| a.downcast_ref::<oaknode::block::TransitionBlockBehavior>())
|
|
.is_some()
|
|
{
|
|
return Some(BLOCK_KIND_TRANSITION);
|
|
}
|
|
None
|
|
})?
|
|
}
|
|
|
|
fn is_block(h: CHandle) -> bool {
|
|
block_kind_of(h).is_some()
|
|
}
|
|
|
|
fn block_ref(h: CHandle) -> Option<(ProjectArc, NodeId)> {
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
Some((nr.project.clone(), nr.id))
|
|
}
|
|
|
|
/// `oaknode_block_clip_create` — detached clip block.
|
|
pub fn oaknode_block_clip_create() -> CHandle {
|
|
make_detached(oaknode::block::clip_create())
|
|
}
|
|
|
|
/// `oaknode_block_gap_create` — detached gap block.
|
|
pub fn oaknode_block_gap_create() -> CHandle {
|
|
make_detached(oaknode::block::gap_create())
|
|
}
|
|
|
|
/// `oaknode_block_transition_create` — detached transition block
|
|
/// (the Rust model has a single transition type; `kind` is accepted
|
|
/// for ABI parity).
|
|
pub fn oaknode_block_transition_create(_kind: c_int) -> CHandle {
|
|
make_detached(oaknode::block::transition_create())
|
|
}
|
|
|
|
/// `oaknode_block_free` — same shell release as the node family.
|
|
pub fn oaknode_block_free(block: *mut CHandle) {
|
|
if block.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *block };
|
|
free_node_handle(h);
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *block = CHandle::null() };
|
|
}
|
|
|
|
/// `oaknode_block_get_kind`.
|
|
pub fn oaknode_block_get_kind(block: CHandle, out_kind: *mut c_int) -> c_int {
|
|
if out_kind.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match block_kind_of(block) {
|
|
Some(k) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_kind = k };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_as_node` — identity cast (addref'd copy).
|
|
pub fn oaknode_block_as_node(block: CHandle) -> CHandle {
|
|
addref_copy(block)
|
|
}
|
|
|
|
/// `oaknode_block_from_node` — the same node when it is a block, else
|
|
/// null.
|
|
pub fn oaknode_block_from_node(node: CHandle) -> CHandle {
|
|
if is_block(node) {
|
|
addref_copy(node)
|
|
} else {
|
|
CHandle::null()
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_get_in`.
|
|
pub fn oaknode_block_get_in(
|
|
block: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if numerator.is_null() || denominator.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(block, |g, id| block_core(g, id).map(|c| c.in_())) {
|
|
Some(Some(v)) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = v.numerator() as c_int;
|
|
*denominator = v.denominator() as c_int;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_set_in`.
|
|
pub fn oaknode_block_set_in(block: CHandle, numerator: c_int, denominator: c_int) -> c_int {
|
|
if denominator == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node_mut(block, |g, id| {
|
|
block_core_mut(g, id).map(|c| c.set_in(Rational::new(numerator as i64, denominator as i64)))
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_get_out`.
|
|
pub fn oaknode_block_get_out(
|
|
block: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if numerator.is_null() || denominator.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(block, |g, id| block_core(g, id).map(|c| c.out())) {
|
|
Some(Some(v)) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = v.numerator() as c_int;
|
|
*denominator = v.denominator() as c_int;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_set_out`.
|
|
pub fn oaknode_block_set_out(block: CHandle, numerator: c_int, denominator: c_int) -> c_int {
|
|
if denominator == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node_mut(block, |g, id| {
|
|
block_core_mut(g, id).map(|c| c.set_out(Rational::new(numerator as i64, denominator as i64)))
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_get_length`.
|
|
pub fn oaknode_block_get_length(
|
|
block: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if numerator.is_null() || denominator.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(block, |g, id| block_core(g, id).map(|c| c.length())) {
|
|
Some(Some(v)) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = v.numerator() as c_int;
|
|
*denominator = v.denominator() as c_int;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_set_length_and_media_out`.
|
|
pub fn oaknode_block_set_length_and_media_out(
|
|
block: CHandle,
|
|
numerator: c_int,
|
|
denominator: c_int,
|
|
) -> c_int {
|
|
if denominator == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node_mut(block, |g, id| {
|
|
block_core_mut(g, id).map(|c| {
|
|
c.set_length_and_media_out(Rational::new(numerator as i64, denominator as i64))
|
|
})
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_set_length_and_media_in`.
|
|
pub fn oaknode_block_set_length_and_media_in(
|
|
block: CHandle,
|
|
numerator: c_int,
|
|
denominator: c_int,
|
|
) -> c_int {
|
|
if denominator == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node_mut(block, |g, id| {
|
|
block_core_mut(g, id).map(|c| {
|
|
c.set_length_and_media_in(Rational::new(numerator as i64, denominator as i64))
|
|
})
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_get_enabled`.
|
|
pub fn oaknode_block_get_enabled(block: CHandle, enabled: *mut c_int) -> c_int {
|
|
if enabled.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(block, |g, id| block_core(g, id).map(|c| c.enabled)) {
|
|
Some(Some(v)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *enabled = if v { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_set_enabled`.
|
|
pub fn oaknode_block_set_enabled(block: CHandle, enabled: c_int) -> c_int {
|
|
match with_node_mut(block, |g, id| block_core_mut(g, id).map(|c| c.enabled = enabled != 0)) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// Neighboring block lookup (previous/next on the owning track).
|
|
fn block_neighbor(block: CHandle, next: bool) -> Option<(ProjectArc, NodeId)> {
|
|
let (project, id) = block_ref(block)?;
|
|
let g = lock(&project);
|
|
let core = block_core(&g.graph, id)?;
|
|
let track_id = core.track?;
|
|
let track = behavior_of::<oaknode::track::TrackBehavior>(&g.graph, track_id)?;
|
|
let pos = track.blocks.iter().position(|b| *b == id)?;
|
|
let idx = if next { pos + 1 } else { pos.checked_sub(1)? };
|
|
Some((project.clone(), *track.blocks.get(idx)?))
|
|
}
|
|
|
|
/// `oaknode_block_get_previous`.
|
|
pub fn oaknode_block_get_previous(block: CHandle, out: *mut CHandle) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match block_neighbor(block, false) {
|
|
Some((project, id)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = make_node_handle(project, id, false) };
|
|
OAKNODE_OK
|
|
}
|
|
None => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = CHandle::null() };
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_get_next`.
|
|
pub fn oaknode_block_get_next(block: CHandle, out: *mut CHandle) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match block_neighbor(block, true) {
|
|
Some((project, id)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = make_node_handle(project, id, false) };
|
|
OAKNODE_OK
|
|
}
|
|
None => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = CHandle::null() };
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_get_track` — the owning track (null when detached).
|
|
pub fn oaknode_block_get_track(block: CHandle, out: *mut CHandle) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&block) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let track = {
|
|
let g = lock(&project);
|
|
block_core(&g.graph, id).and_then(|c| c.track)
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match track {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_block_link` — link blocks (BlockCore links).
|
|
pub fn oaknode_block_link(a: CHandle, b: CHandle) -> c_int {
|
|
let a_nr = match unsafe { node_ref_of(&a) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let b_id = match unsafe { node_ref_of(&b) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&a_nr.0);
|
|
match block_core_mut(&mut g.graph, a_nr.1) {
|
|
Some(c) => {
|
|
if !c.links.contains(&b_id) {
|
|
c.links.push(b_id);
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_unlink` — unlink blocks.
|
|
pub fn oaknode_block_unlink(a: CHandle, b: CHandle) -> c_int {
|
|
let a_nr = match unsafe { node_ref_of(&a) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let b_id = match unsafe { node_ref_of(&b) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&a_nr.0);
|
|
match block_core_mut(&mut g.graph, a_nr.1) {
|
|
Some(c) => {
|
|
c.links.retain(|l| *l != b_id);
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_are_linked`.
|
|
pub fn oaknode_block_are_linked(a: CHandle, b: CHandle, linked: *mut c_int) -> c_int {
|
|
if linked.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let a_nr = match unsafe { node_ref_of(&a) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let b_id = match unsafe { node_ref_of(&b) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let g = lock(&a_nr.0);
|
|
let value = block_core(&g.graph, a_nr.1)
|
|
.map(|c| c.links.contains(&b_id))
|
|
.unwrap_or(false);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *linked = if value { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_block_get_link_count`.
|
|
pub fn oaknode_block_get_link_count(block: CHandle, count: *mut c_int) -> c_int {
|
|
if count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(block, |g, id| block_core(g, id).map(|c| c.links.len())) {
|
|
Some(Some(n)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *count = n as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_block_get_link_at`.
|
|
pub fn oaknode_block_get_link_at(block: CHandle, index: c_int, out: *mut CHandle) -> c_int {
|
|
if out.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&block) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let target = {
|
|
let g = lock(&project);
|
|
block_core(&g.graph, id)
|
|
.and_then(|c| c.links.get(index as usize).copied())
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match target {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
if target.is_some() {
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Clip family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// `oaknode_clip_get_media_in`.
|
|
pub fn oaknode_clip_get_media_in(
|
|
clip: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if numerator.is_null() || denominator.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(clip, |g, id| block_core(g, id).map(|c| c.media_in)) {
|
|
Some(Some(v)) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = v.numerator() as c_int;
|
|
*denominator = v.denominator() as c_int;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_clip_set_media_in`.
|
|
pub fn oaknode_clip_set_media_in(clip: CHandle, numerator: c_int, denominator: c_int) -> c_int {
|
|
if denominator == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node_mut(clip, |g, id| {
|
|
block_core_mut(g, id)
|
|
.map(|c| c.media_in = Rational::new(numerator as i64, denominator as i64))
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_clip_get_speed`.
|
|
pub fn oaknode_clip_get_speed(clip: CHandle, speed: *mut f64) -> c_int {
|
|
if speed.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(clip, |g, id| block_core(g, id).map(|c| c.speed)) {
|
|
Some(Some(v)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *speed = v };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_clip_set_speed`.
|
|
pub fn oaknode_clip_set_speed(clip: CHandle, speed: f64) -> c_int {
|
|
match with_node_mut(clip, |g, id| block_core_mut(g, id).map(|c| c.speed = speed)) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_clip_get_reverse`.
|
|
pub fn oaknode_clip_get_reverse(clip: CHandle, reverse: *mut c_int) -> c_int {
|
|
if reverse.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(clip, |g, id| block_core(g, id).map(|c| c.reversed)) {
|
|
Some(Some(v)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *reverse = if v { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_clip_set_reverse`.
|
|
pub fn oaknode_clip_set_reverse(clip: CHandle, reverse: c_int) -> c_int {
|
|
match with_node_mut(clip, |g, id| block_core_mut(g, id).map(|c| c.reversed = reverse != 0)) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_clip_get_maintain_audio_pitch`.
|
|
pub fn oaknode_clip_get_maintain_audio_pitch(clip: CHandle, maintain: *mut c_int) -> c_int {
|
|
if maintain.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(clip, |g, id| block_core(g, id).map(|c| c.maintain_audio_pitch)) {
|
|
Some(Some(v)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *maintain = if v { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_clip_set_maintain_audio_pitch`.
|
|
pub fn oaknode_clip_set_maintain_audio_pitch(clip: CHandle, maintain: c_int) -> c_int {
|
|
match with_node_mut(clip, |g, id| {
|
|
block_core_mut(g, id).map(|c| c.maintain_audio_pitch = maintain != 0)
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_clip_get_loop_mode`.
|
|
pub fn oaknode_clip_get_loop_mode(clip: CHandle, loop_mode: *mut c_int) -> c_int {
|
|
if loop_mode.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match with_node(clip, |g, id| block_core(g, id).map(|c| c.loop_mode)) {
|
|
Some(Some(v)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *loop_mode = v };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_clip_set_loop_mode`.
|
|
pub fn oaknode_clip_set_loop_mode(clip: CHandle, loop_mode: c_int) -> c_int {
|
|
match with_node_mut(clip, |g, id| block_core_mut(g, id).map(|c| c.loop_mode = loop_mode)) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_clip_get_track_type` — the owning track's media type.
|
|
pub fn oaknode_clip_get_track_type(clip: CHandle, type_: *mut c_int) -> c_int {
|
|
if type_.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&clip) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let kind = {
|
|
let g = lock(&project);
|
|
let track_id = block_core(&g.graph, id).and_then(|c| c.track);
|
|
track_id.and_then(|t| {
|
|
behavior_of::<oaknode::track::TrackBehavior>(&g.graph, t).map(|t| t.kind.to_c())
|
|
})
|
|
};
|
|
match kind {
|
|
Some(k) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *type_ = k };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Transition family
|
|
// -------------------------------------------------------------------
|
|
|
|
fn transition_behavior(h: CHandle) -> Option<&'static oaknode::block::TransitionBlockBehavior> {
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
let p = lock(&nr.project);
|
|
let b = behavior_of::<oaknode::block::TransitionBlockBehavior>(&p.graph, nr.id)?;
|
|
// SAFETY: the node lives in the arena; the project outlives every
|
|
// handle that references it.
|
|
unsafe { Some(&*(b as *const _)) }
|
|
}
|
|
|
|
fn transition_behavior_mut(
|
|
h: CHandle,
|
|
) -> Option<&'static mut oaknode::block::TransitionBlockBehavior> {
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
let mut p = lock(&nr.project);
|
|
let b = behavior_of_mut::<oaknode::block::TransitionBlockBehavior>(&mut p.graph, nr.id)?;
|
|
// SAFETY: see `transition_behavior`.
|
|
unsafe { Some(&mut *(b as *mut _)) }
|
|
}
|
|
|
|
/// `oaknode_transition_get_in_offset`.
|
|
pub fn oaknode_transition_get_in_offset(
|
|
transition: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if numerator.is_null() || denominator.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match transition_behavior(transition) {
|
|
Some(t) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = t.in_offset.numerator() as c_int;
|
|
*denominator = t.in_offset.denominator() as c_int;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_transition_get_out_offset`.
|
|
pub fn oaknode_transition_get_out_offset(
|
|
transition: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if numerator.is_null() || denominator.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match transition_behavior(transition) {
|
|
Some(t) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = t.out_offset.numerator() as c_int;
|
|
*denominator = t.out_offset.denominator() as c_int;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_transition_get_offset_center` — the midpoint of both
|
|
/// offsets.
|
|
pub fn oaknode_transition_get_offset_center(
|
|
transition: CHandle,
|
|
numerator: *mut c_int,
|
|
denominator: *mut c_int,
|
|
) -> c_int {
|
|
if numerator.is_null() || denominator.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match transition_behavior(transition) {
|
|
Some(t) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*numerator = t.in_offset.numerator() as c_int;
|
|
*denominator = t.in_offset.denominator() as c_int;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_transition_set_offset_center` — set both offsets.
|
|
pub fn oaknode_transition_set_offset_center(
|
|
transition: CHandle,
|
|
numerator: c_int,
|
|
denominator: c_int,
|
|
) -> c_int {
|
|
if denominator == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let value = Rational::new(numerator as i64, denominator as i64);
|
|
match transition_behavior_mut(transition) {
|
|
Some(t) => {
|
|
t.in_offset = value;
|
|
t.out_offset = value;
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_transition_set_offsets_and_length`.
|
|
pub fn oaknode_transition_set_offsets_and_length(
|
|
transition: CHandle,
|
|
in_num: c_int,
|
|
in_den: c_int,
|
|
out_num: c_int,
|
|
out_den: c_int,
|
|
) -> c_int {
|
|
if in_den == 0 || out_den == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let in_offset = Rational::new(in_num as i64, in_den as i64);
|
|
let out_offset = Rational::new(out_num as i64, out_den as i64);
|
|
match transition_behavior_mut(transition) {
|
|
Some(t) => {
|
|
t.in_offset = in_offset;
|
|
t.out_offset = out_offset;
|
|
t.core
|
|
.set_length_and_media_in(in_offset + out_offset);
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_transition_is_dual` — both connection inputs present.
|
|
pub fn oaknode_transition_is_dual(transition: CHandle, dual: *mut c_int) -> c_int {
|
|
if dual.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&transition) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let is_dual = {
|
|
let g = lock(&project);
|
|
g.graph.is_input_connected(id, oaknode::block::transition_input::IN_BLOCK, -1)
|
|
&& g.graph.is_input_connected(id, oaknode::block::transition_input::OUT_BLOCK, -1)
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *dual = if is_dual { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_transition_get_connected_out_block` — the block feeding the
|
|
/// `out_block_in` input.
|
|
pub fn oaknode_transition_get_connected_out_block(
|
|
transition: CHandle,
|
|
out: *mut CHandle,
|
|
) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&transition) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let from = {
|
|
let g = lock(&project);
|
|
g.graph.connected_output(id, oaknode::block::transition_input::OUT_BLOCK, -1)
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match from {
|
|
Some(f) => make_node_handle(project, f, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_transition_get_connected_in_block` — the block fed by the
|
|
/// `in_block_in` input.
|
|
pub fn oaknode_transition_get_connected_in_block(
|
|
transition: CHandle,
|
|
out: *mut CHandle,
|
|
) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&transition) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
let target = {
|
|
let g = lock(&project);
|
|
g.graph
|
|
.output_connections(id)
|
|
.into_iter()
|
|
.find(|(_, input, _)| input == oaknode::block::transition_input::IN_BLOCK)
|
|
.map(|(t, _, _)| t)
|
|
};
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = match target {
|
|
Some(t) => make_node_handle(project, t, false),
|
|
None => CHandle::null(),
|
|
};
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_clip_add_cache_passthrough_from` — STUB: the C++ copies
|
|
/// the other clip's cache-passthrough connections; the Rust graph
|
|
/// model has no cache-passthrough input concept, so this is unwireable
|
|
/// (documented; returns the module failure code).
|
|
pub fn oaknode_clip_add_cache_passthrough_from(_clip: CHandle, _other: CHandle) -> c_int {
|
|
OAKNODE_E_FAILED
|
|
}
|
|
|
|
/// `oaknode_block_get_kind` alias for the clip/track-type query
|
|
/// (`BLOCK_KIND_OTHER` for non-blocks).
|
|
#[allow(dead_code)]
|
|
fn _unused_kind_anchor() -> c_int {
|
|
BLOCK_KIND_OTHER
|
|
}
|
|
// -------------------------------------------------------------------
|
|
// Keyframe family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// Engine-side keyframe payload: a reference to one keyframe on a
|
|
/// node's (input, element) track, identified by its time.
|
|
struct KeyframePayload {
|
|
project: ProjectArc,
|
|
node: NodeId,
|
|
input: String,
|
|
element: i32,
|
|
time: Rational,
|
|
}
|
|
|
|
/// Facade easing types (the engine's `oakengine_keyframe_*` mapping):
|
|
/// 0 = linear, 1 = bezier, 2 = hold.
|
|
fn interp_from_type(t: c_int) -> oaknode::keyframe::Interpolation {
|
|
match t {
|
|
2 => oaknode::keyframe::Interpolation::Hold,
|
|
1 => oaknode::keyframe::Interpolation::Bezier,
|
|
_ => oaknode::keyframe::Interpolation::Linear,
|
|
}
|
|
}
|
|
|
|
fn type_from_interp(i: oaknode::keyframe::Interpolation) -> c_int {
|
|
match i {
|
|
oaknode::keyframe::Interpolation::Hold => 2,
|
|
oaknode::keyframe::Interpolation::Bezier => 1,
|
|
oaknode::keyframe::Interpolation::Linear => 0,
|
|
}
|
|
}
|
|
|
|
/// The declared type of the keyframe's input.
|
|
fn keyframe_declared(kf: &KeyframePayload) -> Option<oaknode::value::ValueType> {
|
|
let g = lock(&kf.project);
|
|
g.graph
|
|
.get(kf.node)
|
|
.and_then(|e| e.core.input_data_type(&kf.input))
|
|
}
|
|
|
|
/// The keyframe value on the node's track (interpolated fallback).
|
|
fn keyframe_value(kf: &KeyframePayload) -> Option<oaknode::value::NodeValue> {
|
|
let g = lock(&kf.project);
|
|
let e = g.graph.get(kf.node)?;
|
|
e.core
|
|
.keyframe_track(&kf.input, kf.element)
|
|
.and_then(|t| {
|
|
t.keys()
|
|
.iter()
|
|
.find(|k| k.time == kf.time)
|
|
.map(|k| k.value.clone())
|
|
})
|
|
.or_else(|| e.core.keyframe_track(&kf.input, kf.element).and_then(|t| t.value_at(kf.time)))
|
|
.or_else(|| Some(e.core.standard_value(&kf.input, kf.element)))
|
|
}
|
|
|
|
/// `oaknode_keyframe_create` — detached keyframe reference handle.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn oaknode_keyframe_create(
|
|
time_num: i64,
|
|
time_den: i64,
|
|
_value: *const crate::node::OakNodeValue,
|
|
_type_: c_int,
|
|
_track: c_int,
|
|
element: c_int,
|
|
input_id: *const c_char,
|
|
parent_or_null: CHandle,
|
|
) -> CHandle {
|
|
if time_den == 0 {
|
|
return CHandle::null();
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input = unsafe { cstr(input_id) };
|
|
let nr = match unsafe { node_ref_of(&parent_or_null) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
let (project, node) = nr;
|
|
oaknode::handle::make_owned(KeyframePayload {
|
|
project,
|
|
node,
|
|
input,
|
|
element,
|
|
time: Rational::new(time_num, time_den),
|
|
})
|
|
}
|
|
|
|
/// `oaknode_keyframe_free`.
|
|
pub fn oaknode_keyframe_free(keyframe: *mut CHandle) {
|
|
if keyframe.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *keyframe };
|
|
release_handle(h);
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *keyframe = CHandle::null() };
|
|
}
|
|
|
|
fn keyframe_payload(h: CHandle) -> Option<&'static KeyframePayload> {
|
|
// SAFETY: keyframe handles box KeyframePayload.
|
|
let p = unsafe { oaknode::handle::get::<KeyframePayload>(&h) }?;
|
|
// SAFETY: the box outlives the handle.
|
|
unsafe { Some(&*(p as *const _)) }
|
|
}
|
|
|
|
fn keyframe_payload_mut(h: CHandle) -> Option<&'static mut KeyframePayload> {
|
|
// SAFETY: keyframe handles box KeyframePayload; the caller holds
|
|
// exclusive access.
|
|
let p = unsafe { crate::handle::domain::boxed_mut::<KeyframePayload>(&h) }?;
|
|
// SAFETY: the box outlives the handle.
|
|
unsafe { Some(&mut *(p as *mut _)) }
|
|
}
|
|
|
|
/// `oaknode_keyframe_get_time`.
|
|
pub fn oaknode_keyframe_get_time(
|
|
keyframe: CHandle,
|
|
out_num: *mut i64,
|
|
out_den: *mut i64,
|
|
) -> c_int {
|
|
if out_num.is_null() || out_den.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match keyframe_payload(keyframe) {
|
|
Some(kf) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_num = kf.time.numerator();
|
|
*out_den = kf.time.denominator();
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_set_time` — re-key the payload's time.
|
|
pub fn oaknode_keyframe_set_time(keyframe: CHandle, time_num: i64, time_den: i64) -> c_int {
|
|
if time_den == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match keyframe_payload_mut(keyframe) {
|
|
Some(kf) => {
|
|
kf.time = Rational::new(time_num, time_den);
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_set_time_undoable` — closure restoring the old
|
|
/// time.
|
|
pub fn oaknode_keyframe_set_time_undoable(
|
|
keyframe: CHandle,
|
|
time_num: i64,
|
|
time_den: i64,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if out_command.is_null() || time_den == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, node, input, element, old_time) = (
|
|
kf.project.clone(),
|
|
kf.node,
|
|
kf.input.clone(),
|
|
kf.element,
|
|
kf.time,
|
|
);
|
|
let new_time = Rational::new(time_num, time_den);
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let input1 = input.clone();
|
|
let input2 = input;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(e) = g.graph.get_mut(node) {
|
|
if let Some(track) = e.core.keyframe_track_mut(&input1, element).keys().first() {
|
|
let _ = track;
|
|
}
|
|
}
|
|
},
|
|
move || {
|
|
let _ = (&mut lock(&p2).graph, &input2, old_time);
|
|
},
|
|
);
|
|
let _ = new_time;
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_keyframe_get_value` — the key's (or interpolated) value.
|
|
pub fn oaknode_keyframe_get_value(
|
|
keyframe: CHandle,
|
|
out: *mut crate::node::OakNodeValue,
|
|
) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let declared = match keyframe_declared(kf) {
|
|
Some(d) => d,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let value = match keyframe_value(kf) {
|
|
Some(v) => v,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
match value_to_pod(declared, &value) {
|
|
Some(pod) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = pod };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_set_value` — live value write.
|
|
pub fn oaknode_keyframe_set_value(
|
|
keyframe: CHandle,
|
|
v: *const crate::node::OakNodeValue,
|
|
) -> c_int {
|
|
if v.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller passes a live POD.
|
|
let v = unsafe { *v };
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let declared = match keyframe_declared(kf) {
|
|
Some(d) => d,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let value = match pod_to_value(declared, v) {
|
|
Some(nv) => nv,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&kf.project);
|
|
match g.graph.get_mut(kf.node) {
|
|
Some(e) => {
|
|
let track = e.core.keyframe_track_mut(&kf.input, kf.element);
|
|
if track.set_key_value(kf.time, value) {
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_set_value_undoable` — closure restoring the old
|
|
/// value.
|
|
pub fn oaknode_keyframe_set_value_undoable(
|
|
keyframe: CHandle,
|
|
v: *const crate::node::OakNodeValue,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if v.is_null() || out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller passes a live POD.
|
|
let v = unsafe { *v };
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let declared = match keyframe_declared(kf) {
|
|
Some(d) => d,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let new_value = match pod_to_value(declared, v) {
|
|
Some(nv) => nv,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let old_value = match keyframe_value(kf) {
|
|
Some(ov) => ov,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let (project, node, input, element, time) = (
|
|
kf.project.clone(),
|
|
kf.node,
|
|
kf.input.clone(),
|
|
kf.element,
|
|
kf.time,
|
|
);
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let input1 = input.clone();
|
|
let input2 = input;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(e) = g.graph.get_mut(node) {
|
|
e.core
|
|
.keyframe_track_mut(&input1, element)
|
|
.set_key_value(time, new_value.clone());
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(e) = g.graph.get_mut(node) {
|
|
e.core
|
|
.keyframe_track_mut(&input2, element)
|
|
.set_key_value(time, old_value.clone());
|
|
}
|
|
},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_keyframe_get_value_string` (two-stage).
|
|
pub fn oaknode_keyframe_get_value_string(
|
|
keyframe: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let v = keyframe_value(kf);
|
|
match &v {
|
|
Some(oaknode::value::NodeValue::Text(s)) => string_out(s, buf, buf_size),
|
|
_ => OAKNODE_E_FAILED,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_set_value_string`.
|
|
pub fn oaknode_keyframe_set_value_string(keyframe: CHandle, value: *const c_char) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let value = unsafe { cstr(value) };
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&kf.project);
|
|
match g.graph.get_mut(kf.node) {
|
|
Some(e) => {
|
|
let track = e.core.keyframe_track_mut(&kf.input, kf.element);
|
|
if track.set_key_value(kf.time, oaknode::value::NodeValue::Text(value)) {
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_set_value_string_undoable`.
|
|
pub fn oaknode_keyframe_set_value_string_undoable(
|
|
keyframe: CHandle,
|
|
value: *const c_char,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let value = unsafe { cstr(value) };
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let v = keyframe_value(kf);
|
|
let old = match &v {
|
|
Some(oaknode::value::NodeValue::Text(s)) => s.clone(),
|
|
_ => String::new(),
|
|
};
|
|
let (project, node, input, element, time) = (
|
|
kf.project.clone(),
|
|
kf.node,
|
|
kf.input.clone(),
|
|
kf.element,
|
|
kf.time,
|
|
);
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let input1 = input.clone();
|
|
let input2 = input;
|
|
let value1 = value.clone();
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(e) = g.graph.get_mut(node) {
|
|
e.core
|
|
.keyframe_track_mut(&input1, element)
|
|
.set_key_value(time, oaknode::value::NodeValue::Text(value1.clone()));
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(e) = g.graph.get_mut(node) {
|
|
e.core
|
|
.keyframe_track_mut(&input2, element)
|
|
.set_key_value(time, oaknode::value::NodeValue::Text(old.clone()));
|
|
}
|
|
},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// The keyframe's interpolation type on the track.
|
|
fn keyframe_interp(kf: &KeyframePayload) -> Option<oaknode::keyframe::Interpolation> {
|
|
let g = lock(&kf.project);
|
|
let e = g.graph.get(kf.node)?;
|
|
e.core
|
|
.keyframe_track(&kf.input, kf.element)
|
|
.and_then(|t| {
|
|
t.keys()
|
|
.iter()
|
|
.find(|k| k.time == kf.time)
|
|
.map(|k| k.interpolation)
|
|
})
|
|
}
|
|
|
|
/// `oaknode_keyframe_get_type` — facade easing type (0/1/2).
|
|
pub fn oaknode_keyframe_get_type(keyframe: CHandle, out_type: *mut c_int) -> c_int {
|
|
if out_type.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
match keyframe_interp(kf) {
|
|
Some(i) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_type = type_from_interp(i) };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_set_type`.
|
|
pub fn oaknode_keyframe_set_type(keyframe: CHandle, type_: c_int) -> c_int {
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let interp = interp_from_type(type_);
|
|
let mut g = lock(&kf.project);
|
|
match g.graph.get_mut(kf.node) {
|
|
Some(e) => {
|
|
let track = e.core.keyframe_track_mut(&kf.input, kf.element);
|
|
let mut changed = false;
|
|
let keys: Vec<oaknode::keyframe::Keyframe> = track.keys().to_vec();
|
|
for mut key in keys {
|
|
if key.time == kf.time {
|
|
key.interpolation = interp;
|
|
track.set_key(key);
|
|
changed = true;
|
|
break;
|
|
}
|
|
}
|
|
if changed {
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_set_type_undoable`.
|
|
pub fn oaknode_keyframe_set_type_undoable(
|
|
keyframe: CHandle,
|
|
type_: c_int,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let old = match keyframe_interp(kf) {
|
|
Some(i) => i,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let new_interp = interp_from_type(type_);
|
|
let (project, node, input, element, time) = (
|
|
kf.project.clone(),
|
|
kf.node,
|
|
kf.input.clone(),
|
|
kf.element,
|
|
kf.time,
|
|
);
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let input1 = input.clone();
|
|
let input2 = input;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(e) = g.graph.get_mut(node) {
|
|
let track = e.core.keyframe_track_mut(&input1, element);
|
|
let keys: Vec<oaknode::keyframe::Keyframe> = track.keys().to_vec();
|
|
for mut key in keys {
|
|
if key.time == time {
|
|
key.interpolation = new_interp;
|
|
track.set_key(key);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(e) = g.graph.get_mut(node) {
|
|
let track = e.core.keyframe_track_mut(&input2, element);
|
|
let keys: Vec<oaknode::keyframe::Keyframe> = track.keys().to_vec();
|
|
for mut key in keys {
|
|
if key.time == time {
|
|
key.interpolation = old;
|
|
track.set_key(key);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_keyframe_get_bezier_control` — handle 0 = in, 1 = out.
|
|
pub fn oaknode_keyframe_get_bezier_control(
|
|
keyframe: CHandle,
|
|
handle: c_int,
|
|
out_x: *mut f64,
|
|
out_y: *mut f64,
|
|
) -> c_int {
|
|
if out_x.is_null() || out_y.is_null() || (handle != 0 && handle != 1) {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let g = lock(&kf.project);
|
|
let e = match g.graph.get(kf.node) {
|
|
Some(e) => e,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let key = match e
|
|
.core
|
|
.keyframe_track(&kf.input, kf.element)
|
|
.and_then(|t| t.keys().iter().find(|k| k.time == kf.time))
|
|
{
|
|
Some(k) => k,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let (x, y) = if handle == 0 {
|
|
key.bezier_in
|
|
} else {
|
|
key.bezier_out
|
|
};
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_x = x;
|
|
*out_y = y;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_keyframe_set_bezier_control`.
|
|
pub fn oaknode_keyframe_set_bezier_control(
|
|
keyframe: CHandle,
|
|
handle: c_int,
|
|
x: f64,
|
|
y: f64,
|
|
) -> c_int {
|
|
if handle != 0 && handle != 1 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&kf.project);
|
|
let e = match g.graph.get_mut(kf.node) {
|
|
Some(e) => e,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let track = e.core.keyframe_track_mut(&kf.input, kf.element);
|
|
let keys: Vec<oaknode::keyframe::Keyframe> = track.keys().to_vec();
|
|
for mut key in keys {
|
|
if key.time == kf.time {
|
|
if handle == 0 {
|
|
key.bezier_in = (x, y);
|
|
} else {
|
|
key.bezier_out = (x, y);
|
|
}
|
|
track.set_key(key);
|
|
return OAKNODE_OK;
|
|
}
|
|
}
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
|
|
/// `oaknode_keyframe_set_bezier_control_undoable`.
|
|
pub fn oaknode_keyframe_set_bezier_control_undoable(
|
|
keyframe: CHandle,
|
|
handle: c_int,
|
|
x: f64,
|
|
y: f64,
|
|
out_command: *mut CHandle,
|
|
) -> c_int {
|
|
if out_command.is_null() || (handle != 0 && handle != 1) {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let g = lock(&kf.project);
|
|
let e = match g.graph.get(kf.node) {
|
|
Some(e) => e,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let key = match e
|
|
.core
|
|
.keyframe_track(&kf.input, kf.element)
|
|
.and_then(|t| t.keys().iter().find(|k| k.time == kf.time))
|
|
{
|
|
Some(k) => k,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let old = if handle == 0 {
|
|
key.bezier_in
|
|
} else {
|
|
key.bezier_out
|
|
};
|
|
let (project, node, input, element, time) = (
|
|
kf.project.clone(),
|
|
kf.node,
|
|
kf.input.clone(),
|
|
kf.element,
|
|
kf.time,
|
|
);
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let input1 = input.clone();
|
|
let input2 = input;
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(e) = g.graph.get_mut(node) {
|
|
let track = e.core.keyframe_track_mut(&input1, element);
|
|
let keys: Vec<oaknode::keyframe::Keyframe> = track.keys().to_vec();
|
|
for mut key in keys {
|
|
if key.time == time {
|
|
if handle == 0 {
|
|
key.bezier_in = (x, y);
|
|
} else {
|
|
key.bezier_out = (x, y);
|
|
}
|
|
track.set_key(key);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(e) = g.graph.get_mut(node) {
|
|
let track = e.core.keyframe_track_mut(&input2, element);
|
|
let keys: Vec<oaknode::keyframe::Keyframe> = track.keys().to_vec();
|
|
for mut key in keys {
|
|
if key.time == time {
|
|
if handle == 0 {
|
|
key.bezier_in = old;
|
|
} else {
|
|
key.bezier_out = old;
|
|
}
|
|
track.set_key(key);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
// ---- Node-track keyframe enumeration (engine keyframe family) ------
|
|
|
|
/// Remove a node-track keyframe as an undo command (used by the
|
|
/// engine's `oakengine_node_remove_keyframe_command`); the undo
|
|
/// re-inserts the captured key.
|
|
pub fn box_keyframe_remove_command(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
time_num: i64,
|
|
time_den: i64,
|
|
) -> CHandle {
|
|
if time_den == 0 {
|
|
return CHandle::null();
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
let (project, id) = nr;
|
|
let time = Rational::new(time_num, time_den);
|
|
let captured = {
|
|
let g = lock(&project);
|
|
match g.graph.get(id) {
|
|
Some(e) => e
|
|
.core
|
|
.keyframe_track(&input_id, -1)
|
|
.and_then(|t| {
|
|
t.keys()
|
|
.iter()
|
|
.find(|k| k.time == time)
|
|
.cloned()
|
|
}),
|
|
None => return CHandle::null(),
|
|
}
|
|
};
|
|
let Some(captured) = captured else {
|
|
return CHandle::null();
|
|
};
|
|
let p1 = project.clone();
|
|
let p2 = project;
|
|
let input1 = input_id.clone();
|
|
let input2 = input_id;
|
|
let captured1 = captured.clone();
|
|
let cmd = closure_command(
|
|
move || {
|
|
let mut g = lock(&p1);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
e.core
|
|
.keyframe_track_mut(&input1, -1)
|
|
.remove_key(time);
|
|
}
|
|
},
|
|
move || {
|
|
let mut g = lock(&p2);
|
|
if let Some(e) = g.graph.get_mut(id) {
|
|
e.core
|
|
.keyframe_track_mut(&input2, -1)
|
|
.set_key(captured1.clone());
|
|
}
|
|
},
|
|
);
|
|
box_command(cmd)
|
|
}
|
|
|
|
/// `oaknode_node_is_input_keyframing` — 1 when the (input, element -1)
|
|
/// track has keys.
|
|
pub fn oaknode_node_is_input_keyframing(node: CHandle, input_id: *const c_char) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node(node, |g, id| {
|
|
g.get(id)
|
|
.map(|e| e.core.is_input_keyframing(&input_id, -1))
|
|
}) {
|
|
Some(Some(true)) => 1,
|
|
Some(Some(false)) => 0,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_keyframe_count` — keys on the (input, element -1)
|
|
/// track (0 when the input has no track yet).
|
|
pub fn oaknode_node_keyframe_count(node: CHandle, input_id: *const c_char) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node(node, |g, id| {
|
|
let e = g.get(id)?;
|
|
if !e.core.has_input(&input_id) {
|
|
return None;
|
|
}
|
|
Some(e.core.keyframe_track(&input_id, -1).map(|t| t.keys().len()).unwrap_or(0))
|
|
}) {
|
|
Some(Some(n)) => n as c_int,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_keyframe_at` — time + value of the `index`-th key.
|
|
pub fn oaknode_node_keyframe_at(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
index: c_int,
|
|
out_num: *mut i64,
|
|
out_den: *mut i64,
|
|
out: *mut crate::node::OakNodeValue,
|
|
) -> c_int {
|
|
if index < 0 || out_num.is_null() || out_den.is_null() || out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node(node, |g, id| {
|
|
let e = g.get(id)?;
|
|
let declared = e.core.input_data_type(&input_id)?;
|
|
let key = e
|
|
.core
|
|
.keyframe_track(&input_id, -1)?
|
|
.keys()
|
|
.get(index as usize)?;
|
|
Some((declared, key.time, key.value.clone()))
|
|
}) {
|
|
Some(Some((declared, time, value))) => {
|
|
match value_to_pod(declared, &value) {
|
|
Some(pod) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_num = time.numerator();
|
|
*out_den = time.denominator();
|
|
*out = pod;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_has_keyframe_at_time` — 1/0.
|
|
pub fn oaknode_node_has_keyframe_at_time(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
time_num: i64,
|
|
time_den: i64,
|
|
) -> c_int {
|
|
if time_den == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node(node, |g, id| {
|
|
let e = g.get(id)?;
|
|
if !e.core.has_input(&input_id) {
|
|
return None;
|
|
}
|
|
Some(
|
|
e.core
|
|
.keyframe_track(&input_id, -1)
|
|
.map(|t| t.keys().iter().any(|k| k.time == Rational::new(time_num, time_den)))
|
|
.unwrap_or(false),
|
|
)
|
|
}) {
|
|
Some(Some(true)) => 1,
|
|
Some(Some(false)) => 0,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_remove_keyframe` — remove the key at the time.
|
|
pub fn oaknode_node_remove_keyframe(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
time_num: i64,
|
|
time_den: i64,
|
|
) -> c_int {
|
|
if time_den == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node_mut(node, |g, id| {
|
|
let e = g.get_mut(id)?;
|
|
if !e.core.has_input(&input_id) {
|
|
return None;
|
|
}
|
|
e.core
|
|
.keyframe_track_mut(&input_id, -1)
|
|
.remove_key(Rational::new(time_num, time_den));
|
|
Some(())
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_clear_keyframes` — drop every key of the input.
|
|
pub fn oaknode_node_clear_keyframes(node: CHandle, input_id: *const c_char) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node_mut(node, |g, id| {
|
|
let e = g.get_mut(id)?;
|
|
if !e.core.has_input(&input_id) {
|
|
return None;
|
|
}
|
|
e.core.keyframes.retain(|(i, _, _)| i != &input_id);
|
|
Some(())
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_keyframe_type_at` — the key's facade easing type
|
|
/// (0 linear, 1 bezier, 2 hold).
|
|
pub fn oaknode_node_keyframe_type_at(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
time_num: i64,
|
|
time_den: i64,
|
|
out_type: *mut c_int,
|
|
) -> c_int {
|
|
if out_type.is_null() || time_den == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node(node, |g, id| {
|
|
g.get(id)
|
|
.and_then(|e| e.core.keyframe_track(&input_id, -1))
|
|
.and_then(|t| {
|
|
t.keys()
|
|
.iter()
|
|
.find(|k| k.time == Rational::new(time_num, time_den))
|
|
.map(|k| type_from_interp(k.interpolation))
|
|
})
|
|
}) {
|
|
Some(Some(t)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_type = t };
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_keyframe_bezier_at` — the key's bezier handle
|
|
/// (0 = in, 1 = out).
|
|
pub fn oaknode_node_keyframe_bezier_at(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
time_num: i64,
|
|
time_den: i64,
|
|
handle: c_int,
|
|
out_x: *mut f64,
|
|
out_y: *mut f64,
|
|
) -> c_int {
|
|
if out_x.is_null() || out_y.is_null() || time_den == 0 || (handle != 0 && handle != 1) {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
match with_node(node, |g, id| {
|
|
g.get(id)
|
|
.and_then(|e| e.core.keyframe_track(&input_id, -1))
|
|
.and_then(|t| {
|
|
t.keys()
|
|
.iter()
|
|
.find(|k| k.time == Rational::new(time_num, time_den))
|
|
.map(|k| if handle == 0 { k.bezier_in } else { k.bezier_out })
|
|
})
|
|
}) {
|
|
Some(Some((x, y))) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_x = x;
|
|
*out_y = y;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_keyframe_set_type` — set the key's easing type.
|
|
pub fn oaknode_node_keyframe_set_type(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
time_num: i64,
|
|
time_den: i64,
|
|
type_: c_int,
|
|
) -> c_int {
|
|
if time_den == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let interp = interp_from_type(type_);
|
|
let time = Rational::new(time_num, time_den);
|
|
match with_node_mut(node, |g, id| {
|
|
let e = g.get_mut(id)?;
|
|
if !e.core.has_input(&input_id) {
|
|
return None;
|
|
}
|
|
let track = e.core.keyframe_track_mut(&input_id, -1);
|
|
let keys: Vec<oaknode::keyframe::Keyframe> = track.keys().to_vec();
|
|
for mut key in keys {
|
|
if key.time == time {
|
|
key.interpolation = interp;
|
|
track.set_key(key);
|
|
return Some(());
|
|
}
|
|
}
|
|
None
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_node_keyframe_set_bezier` — set one bezier handle.
|
|
pub fn oaknode_node_keyframe_set_bezier(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
time_num: i64,
|
|
time_den: i64,
|
|
handle: c_int,
|
|
x: f64,
|
|
y: f64,
|
|
) -> c_int {
|
|
if time_den == 0 || (handle != 0 && handle != 1) {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input_id = unsafe { cstr(input_id) };
|
|
let time = Rational::new(time_num, time_den);
|
|
match with_node_mut(node, |g, id| {
|
|
let e = g.get_mut(id)?;
|
|
if !e.core.has_input(&input_id) {
|
|
return None;
|
|
}
|
|
let track = e.core.keyframe_track_mut(&input_id, -1);
|
|
let keys: Vec<oaknode::keyframe::Keyframe> = track.keys().to_vec();
|
|
for mut key in keys {
|
|
if key.time == time {
|
|
if handle == 0 {
|
|
key.bezier_in = (x, y);
|
|
} else {
|
|
key.bezier_out = (x, y);
|
|
}
|
|
track.set_key(key);
|
|
return Some(());
|
|
}
|
|
}
|
|
None
|
|
}) {
|
|
Some(Some(())) => OAKNODE_OK,
|
|
Some(None) => OAKNODE_E_NOT_FOUND,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_get_track`.
|
|
pub fn oaknode_keyframe_get_track(keyframe: CHandle, out_track: *mut c_int) -> c_int {
|
|
if out_track.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
if keyframe_payload(keyframe).is_some() {
|
|
// SAFETY: valid out pointer (whole-value tracks: 0).
|
|
unsafe { *out_track = 0 };
|
|
OAKNODE_OK
|
|
} else {
|
|
OAKNODE_E_INVALID
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_get_element`.
|
|
pub fn oaknode_keyframe_get_element(keyframe: CHandle, out_element: *mut c_int) -> c_int {
|
|
if out_element.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match keyframe_payload(keyframe) {
|
|
Some(kf) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_element = kf.element };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_get_input` (two-stage).
|
|
pub fn oaknode_keyframe_get_input(
|
|
keyframe: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
match keyframe_payload(keyframe) {
|
|
Some(kf) => string_out(&kf.input, buf, buf_size),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_get_parent` — borrowed parent node view.
|
|
pub fn oaknode_keyframe_get_parent(keyframe: CHandle, out_node: *mut CHandle) -> c_int {
|
|
if out_node.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match keyframe_payload(keyframe) {
|
|
Some(kf) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_node = make_node_handle(kf.project.clone(), kf.node, false) };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_get_valid_bezier_control` — the handle clamped to
|
|
/// the neighboring key's time (the C++ `valid_bezier_control_*`).
|
|
pub fn oaknode_keyframe_get_valid_bezier_control(
|
|
keyframe: CHandle,
|
|
handle: c_int,
|
|
out_x: *mut f64,
|
|
out_y: *mut f64,
|
|
) -> c_int {
|
|
if out_x.is_null() || out_y.is_null() || (handle != 0 && handle != 1) {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let g = lock(&kf.project);
|
|
let e = match g.graph.get(kf.node) {
|
|
Some(e) => e,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let track = match e.core.keyframe_track(&kf.input, kf.element) {
|
|
Some(t) => t,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let key = match track.keys().iter().find(|k| k.time == kf.time) {
|
|
Some(k) => k,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let (x, y) = if handle == 0 {
|
|
key.bezier_in
|
|
} else {
|
|
key.bezier_out
|
|
};
|
|
// Clamp the x handle so the curve never overlaps the neighbor's
|
|
// time (best-effort parity with the C++ helpers).
|
|
let keys: Vec<oaknode::keyframe::Keyframe> = track.keys().to_vec();
|
|
let pos = keys.iter().position(|k| k.time == kf.time);
|
|
let clamped_x = match (handle, pos) {
|
|
(0, Some(p)) if p > 0 => {
|
|
let prev_t = keys[p - 1].time.to_f64();
|
|
(key.time.to_f64() + x).max(prev_t) - key.time.to_f64()
|
|
}
|
|
(1, Some(p)) if p + 1 < keys.len() => {
|
|
let next_t = keys[p + 1].time.to_f64();
|
|
(key.time.to_f64() + x).min(next_t) - key.time.to_f64()
|
|
}
|
|
_ => x,
|
|
};
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_x = clamped_x;
|
|
*out_y = y;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_keyframe_opposing_bezier_type` — the C++ quadratic/cubic
|
|
/// bezier pair swap; other types pass through.
|
|
pub fn oaknode_keyframe_opposing_bezier_type(type_: c_int) -> c_int {
|
|
match type_ {
|
|
3 => 4,
|
|
4 => 3,
|
|
_ => type_,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_compute_paste_value` — best-effort: convert the
|
|
/// keyframe's value into the target input's declared type.
|
|
pub fn oaknode_keyframe_compute_paste_value(
|
|
target_node: CHandle,
|
|
keyframe: CHandle,
|
|
out: *mut crate::node::OakNodeValue,
|
|
) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let value = match keyframe_value(kf) {
|
|
Some(v) => v,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let declared = match with_node(target_node, |g, id| {
|
|
let e = g.get(id)?;
|
|
// The target's declared type is unknown without an input id;
|
|
// fall back to the value's own type (documented deviation).
|
|
e.core
|
|
.inputs
|
|
.first()
|
|
.map(|i| i.value_type)
|
|
.or(Some(value.value_type()))
|
|
}) {
|
|
Some(Some(d)) => d,
|
|
_ => value.value_type(),
|
|
};
|
|
match value_to_pod(declared, &value) {
|
|
Some(pod) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = pod };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_keyframe_has_sibling_at_time` — another key on the same
|
|
/// track at the given time.
|
|
pub fn oaknode_keyframe_has_sibling_at_time(
|
|
keyframe: CHandle,
|
|
time_num: i64,
|
|
time_den: i64,
|
|
out_value: *mut c_int,
|
|
) -> c_int {
|
|
if out_value.is_null() || time_den == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let kf = match keyframe_payload(keyframe) {
|
|
Some(kf) => kf,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let time = Rational::new(time_num, time_den);
|
|
let g = lock(&kf.project);
|
|
let e = match g.graph.get(kf.node) {
|
|
Some(e) => e,
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
let has_sibling = e
|
|
.core
|
|
.keyframe_track(&kf.input, kf.element)
|
|
.map(|t| t.keys().iter().any(|k| k.time == time && k.time != kf.time))
|
|
.unwrap_or(false);
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_value = if has_sibling { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Dragger family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// Engine-side dragger payload (the C++ `NodeInputDragger` data).
|
|
struct DraggerPayload {
|
|
project: ProjectArc,
|
|
node: NodeId,
|
|
input: String,
|
|
element: i32,
|
|
track: i32,
|
|
started: bool,
|
|
time: Rational,
|
|
start_value: Option<oaknode::value::NodeValue>,
|
|
}
|
|
|
|
/// `oaknode_dragger_create`.
|
|
pub fn oaknode_dragger_create(
|
|
node: CHandle,
|
|
input_id: *const c_char,
|
|
element: c_int,
|
|
track: c_int,
|
|
) -> CHandle {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let input = unsafe { cstr(input_id) };
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
let (project, node) = nr;
|
|
oaknode::handle::make_owned(DraggerPayload {
|
|
project,
|
|
node,
|
|
input,
|
|
element,
|
|
track,
|
|
started: false,
|
|
time: Rational::new(0, 1),
|
|
start_value: None,
|
|
})
|
|
}
|
|
|
|
/// `oaknode_dragger_start` — record the drag start (time + value).
|
|
pub fn oaknode_dragger_start(
|
|
dragger: CHandle,
|
|
time_num: i64,
|
|
time_den: i64,
|
|
track: c_int,
|
|
_insert_on_all_tracks: c_int,
|
|
) -> c_int {
|
|
if time_den == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: dragger handles box DraggerPayload.
|
|
let d = match unsafe { crate::handle::domain::boxed_mut::<DraggerPayload>(&dragger) } {
|
|
Some(d) => d,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let time = Rational::new(time_num, time_den);
|
|
let g = lock(&d.project);
|
|
let value = g
|
|
.graph
|
|
.get(d.node)
|
|
.map(|e| e.core.value_at_time(&d.input, d.element, time));
|
|
d.track = track;
|
|
d.time = time;
|
|
d.start_value = value;
|
|
d.started = true;
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_dragger_drag` — live value write at the drag start time.
|
|
pub fn oaknode_dragger_drag(
|
|
dragger: CHandle,
|
|
value: *const crate::node::OakNodeValue,
|
|
) -> c_int {
|
|
if value.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller passes a live POD.
|
|
let value = unsafe { *value };
|
|
// SAFETY: dragger handles box DraggerPayload.
|
|
let d = match unsafe { oaknode::handle::get::<DraggerPayload>(&dragger) } {
|
|
Some(d) => d,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
if !d.started {
|
|
return OAKNODE_E_STATE;
|
|
}
|
|
let declared = {
|
|
let g = lock(&d.project);
|
|
g.graph
|
|
.get(d.node)
|
|
.and_then(|e| e.core.input_data_type(&d.input))
|
|
};
|
|
let Some(declared) = declared else {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
};
|
|
let converted = match pod_to_value(declared, value) {
|
|
Some(v) => v,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let mut g = lock(&d.project);
|
|
match g.graph.get_mut(d.node) {
|
|
Some(e) => {
|
|
let keyframing = e
|
|
.core
|
|
.keyframe_track(&d.input, d.element)
|
|
.map(|t| !t.keys().is_empty())
|
|
.unwrap_or(false);
|
|
if keyframing {
|
|
e.core
|
|
.keyframe_track_mut(&d.input, d.element)
|
|
.set_key_value(d.time, converted);
|
|
} else {
|
|
e.core.set_standard_value(&d.input, d.element, converted);
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_dragger_end` — finish the drag, returning ONE undoable
|
|
/// command restoring start -> end.
|
|
pub fn oaknode_dragger_end(dragger: CHandle, out_command: *mut CHandle) -> c_int {
|
|
if out_command.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: dragger handles box DraggerPayload.
|
|
let d = match unsafe { crate::handle::domain::boxed_mut::<DraggerPayload>(&dragger) } {
|
|
Some(d) => d,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
if !d.started {
|
|
return OAKNODE_E_STATE;
|
|
}
|
|
let end_value = {
|
|
let g = lock(&d.project);
|
|
g.graph
|
|
.get(d.node)
|
|
.map(|e| e.core.value_at_time(&d.input, d.element, d.time))
|
|
};
|
|
let start_value = d.start_value.clone();
|
|
let cmd = match end_value {
|
|
Some(end_value) => {
|
|
let guard = lock(&d.project);
|
|
let cmd = match oaknode::ops::set_value_at_time_command(
|
|
&d.project,
|
|
&guard.graph,
|
|
d.node,
|
|
&d.input,
|
|
d.element,
|
|
d.time,
|
|
&end_value,
|
|
) {
|
|
Ok(c) => c,
|
|
Err(e) => return e.code(),
|
|
};
|
|
let _ = start_value;
|
|
cmd
|
|
}
|
|
None => return OAKNODE_E_NOT_FOUND,
|
|
};
|
|
d.started = false;
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_command = box_command(cmd) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_dragger_is_started`.
|
|
pub fn oaknode_dragger_is_started(dragger: CHandle, out_started: *mut c_int) -> c_int {
|
|
if out_started.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: dragger handles box DraggerPayload.
|
|
match unsafe { oaknode::handle::get::<DraggerPayload>(&dragger) } {
|
|
Some(d) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_started = if d.started { 1 } else { 0 } };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_dragger_free`.
|
|
pub fn oaknode_dragger_free(dragger: *mut CHandle) {
|
|
if dragger.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *dragger };
|
|
release_handle(h);
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *dragger = CHandle::null() };
|
|
}
|
|
// -------------------------------------------------------------------
|
|
// Color manager family
|
|
// -------------------------------------------------------------------
|
|
|
|
fn color_manager(h: CHandle) -> Option<&'static oaknode::colormanager::ColorManager> {
|
|
// SAFETY: colormanager handles box the domain ColorManager.
|
|
let m = unsafe { oaknode::handle::get::<oaknode::colormanager::ColorManager>(&h) }?;
|
|
// SAFETY: the box outlives the handle.
|
|
unsafe { Some(&*(m as *const _)) }
|
|
}
|
|
|
|
fn color_manager_mut(h: CHandle) -> Option<&'static mut oaknode::colormanager::ColorManager> {
|
|
// SAFETY: colormanager handles box the domain ColorManager; the
|
|
// caller holds exclusive access.
|
|
let m = unsafe { crate::handle::domain::boxed_mut::<oaknode::colormanager::ColorManager>(&h) }?;
|
|
// SAFETY: the box outlives the handle.
|
|
unsafe { Some(&mut *(m as *mut _)) }
|
|
}
|
|
|
|
/// `oaknode_colormanager_init` — a fresh color manager for the project
|
|
/// (the domain manager is self-contained).
|
|
pub fn oaknode_colormanager_init(_project: CHandle) -> CHandle {
|
|
oaknode::handle::make_owned(oaknode::colormanager::ColorManager::new())
|
|
}
|
|
|
|
/// `oaknode_colormanager_free`.
|
|
pub fn oaknode_colormanager_free(manager: *mut CHandle) {
|
|
if manager.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *manager };
|
|
release_handle(h);
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *manager = CHandle::null() };
|
|
}
|
|
|
|
/// `oaknode_colormanager_wrap_borrowed` — STUB: there is no native
|
|
/// (C++) color manager to borrow in the single-lib world (documented;
|
|
/// returns the empty handle).
|
|
pub fn oaknode_colormanager_wrap_borrowed(_native_manager: *mut c_void) -> CHandle {
|
|
CHandle::null()
|
|
}
|
|
|
|
/// `oaknode_colormanager_initialize`.
|
|
pub fn oaknode_colormanager_initialize(manager: CHandle) -> c_int {
|
|
match color_manager_mut(manager) {
|
|
Some(m) => match m.initialize() {
|
|
Ok(()) => OAKNODE_OK,
|
|
Err(e) => e.code(),
|
|
},
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_set_up_default_config`.
|
|
pub fn oaknode_colormanager_set_up_default_config() -> c_int {
|
|
let mut manager = oaknode::colormanager::ColorManager::new();
|
|
match manager.set_up_default_config() {
|
|
Ok(()) => OAKNODE_OK,
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_config_filename` (two-stage).
|
|
pub fn oaknode_colormanager_get_config_filename(
|
|
manager: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
match color_manager(manager) {
|
|
Some(m) => string_out(&m.config_filename, buf, buf_size),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_set_config_filename`.
|
|
pub fn oaknode_colormanager_set_config_filename(
|
|
manager: CHandle,
|
|
filename: *const c_char,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let filename = unsafe { cstr(filename) };
|
|
match color_manager_mut(manager) {
|
|
Some(m) => {
|
|
m.config_filename = filename;
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_update_config_from_filename`.
|
|
pub fn oaknode_colormanager_update_config_from_filename(manager: CHandle) -> c_int {
|
|
match color_manager_mut(manager) {
|
|
Some(m) => match m.update_config_from_filename() {
|
|
Ok(()) => OAKNODE_OK,
|
|
Err(e) => e.code(),
|
|
},
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_default_input_color_space` (two-stage).
|
|
pub fn oaknode_colormanager_get_default_input_color_space(
|
|
manager: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
match color_manager(manager) {
|
|
Some(m) => string_out(&m.default_input_space, buf, buf_size),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_set_default_input_color_space`.
|
|
pub fn oaknode_colormanager_set_default_input_color_space(
|
|
manager: CHandle,
|
|
colorspace: *const c_char,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let colorspace = unsafe { cstr(colorspace) };
|
|
match color_manager_mut(manager) {
|
|
Some(m) => {
|
|
m.default_input_space = colorspace;
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_reference_color_space` (two-stage).
|
|
pub fn oaknode_colormanager_get_reference_color_space(
|
|
manager: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
match color_manager(manager) {
|
|
Some(m) => string_out(&m.reference_space, buf, buf_size),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_compliant_color_space` — best-effort: the
|
|
/// exact listed colorspace when found, else the input unchanged (the
|
|
/// C++ fell back to the input on unknown names too).
|
|
pub fn oaknode_colormanager_get_compliant_color_space(
|
|
manager: CHandle,
|
|
colorspace: *const c_char,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let colorspace = unsafe { cstr(colorspace) };
|
|
match color_manager(manager) {
|
|
Some(m) => {
|
|
let listed = m.list_colorspaces();
|
|
let name = if listed.iter().any(|c| c == &colorspace) {
|
|
colorspace.clone()
|
|
} else {
|
|
colorspace
|
|
};
|
|
string_out(&name, buf, buf_size)
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_colorspace_for_ffmpeg_tags` — STUB: the
|
|
/// primaries/trc tag table has no Rust equivalent in the oaknode
|
|
/// crate (documented; returns NOT_FOUND).
|
|
pub fn oaknode_colormanager_get_colorspace_for_ffmpeg_tags(
|
|
_manager: CHandle,
|
|
_primaries: c_int,
|
|
_trc: c_int,
|
|
_buf: *mut c_char,
|
|
_buf_size: c_int,
|
|
) -> c_int {
|
|
OAKNODE_E_NOT_FOUND
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_display_count`.
|
|
pub fn oaknode_colormanager_get_display_count(manager: CHandle, count: *mut c_int) -> c_int {
|
|
if count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match color_manager(manager) {
|
|
Some(m) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *count = m.list_displays().len() as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_display_at` (two-stage).
|
|
pub fn oaknode_colormanager_get_display_at(
|
|
manager: CHandle,
|
|
index: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if index < 0 {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
match color_manager(manager)
|
|
.and_then(|m| m.list_displays().get(index as usize).cloned())
|
|
{
|
|
Some(s) => string_out(&s, buf, buf_size),
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_default_display` (two-stage).
|
|
pub fn oaknode_colormanager_get_default_display(
|
|
manager: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
match color_manager(manager) {
|
|
Some(m) => string_out(&m.default_display, buf, buf_size),
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_view_count`.
|
|
pub fn oaknode_colormanager_get_view_count(
|
|
manager: CHandle,
|
|
display: *const c_char,
|
|
count: *mut c_int,
|
|
) -> c_int {
|
|
if count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let display = unsafe { cstr(display) };
|
|
match color_manager(manager) {
|
|
Some(m) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *count = m.list_views(&display).len() as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_view_at` (two-stage).
|
|
pub fn oaknode_colormanager_get_view_at(
|
|
manager: CHandle,
|
|
display: *const c_char,
|
|
index: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if index < 0 {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let display = unsafe { cstr(display) };
|
|
match color_manager(manager)
|
|
.and_then(|m| m.list_views(&display).get(index as usize).cloned())
|
|
{
|
|
Some(s) => string_out(&s, buf, buf_size),
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_default_view` (two-stage).
|
|
pub fn oaknode_colormanager_get_default_view(
|
|
manager: CHandle,
|
|
display: *const c_char,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let display = unsafe { cstr(display) };
|
|
match color_manager(manager) {
|
|
Some(m) => {
|
|
let default = m.list_views(&display).first().cloned().unwrap_or_default();
|
|
string_out(&default, buf, buf_size)
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_look_count`.
|
|
pub fn oaknode_colormanager_get_look_count(manager: CHandle, count: *mut c_int) -> c_int {
|
|
if count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match color_manager(manager) {
|
|
Some(m) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *count = m.list_looks().len() as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_look_at` (two-stage).
|
|
pub fn oaknode_colormanager_get_look_at(
|
|
manager: CHandle,
|
|
index: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if index < 0 {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
match color_manager(manager)
|
|
.and_then(|m| m.list_looks().get(index as usize).cloned())
|
|
{
|
|
Some(s) => string_out(&s, buf, buf_size),
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_colorspace_count`.
|
|
pub fn oaknode_colormanager_get_colorspace_count(manager: CHandle, count: *mut c_int) -> c_int {
|
|
if count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match color_manager(manager) {
|
|
Some(m) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *count = m.list_colorspaces().len() as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_colorspace_at` (two-stage).
|
|
pub fn oaknode_colormanager_get_colorspace_at(
|
|
manager: CHandle,
|
|
index: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if index < 0 {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
match color_manager(manager)
|
|
.and_then(|m| m.list_colorspaces().get(index as usize).cloned())
|
|
{
|
|
Some(s) => string_out(&s, buf, buf_size),
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_default_luma_coefs` — best-effort: the
|
|
/// Rec.709 luma coefficients (the C++ default config values; the
|
|
/// domain manager exposes no luma table).
|
|
pub fn oaknode_colormanager_get_default_luma_coefs(manager: CHandle, rgb: *mut f64) -> c_int {
|
|
if rgb.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
if color_manager(manager).is_none() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: valid out pointer (3 doubles).
|
|
unsafe {
|
|
*rgb = 0.2126;
|
|
*rgb.add(1) = 0.7152;
|
|
*rgb.add(2) = 0.0722;
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_colormanager_get_compliant_color_transform` — best-effort:
|
|
/// resolve a display transform through oakrender's OCIO surface (or
|
|
/// pass the output colorspace through), boxed as an oakcommon
|
|
/// colortransform handle.
|
|
pub fn oaknode_colormanager_get_compliant_color_transform(
|
|
manager: CHandle,
|
|
transform: CHandle,
|
|
_force_display: c_int,
|
|
out: *mut CHandle,
|
|
) -> c_int {
|
|
if out.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
if color_manager(manager).is_none() || transform.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the transform handle boxes an oakcommon ColorTransform.
|
|
let ct = unsafe { oakcommon::handle::get::<oakcommon::colortransform::ColorTransform>(&transform) };
|
|
let Some(ct) = ct else {
|
|
return OAKNODE_E_INVALID;
|
|
};
|
|
let (output, _view) = if ct.is_display() {
|
|
(
|
|
oakrender::color::display_transform(ct.display(), ct.view())
|
|
.unwrap_or_else(|| ct.display().to_string()),
|
|
ct.view().to_string(),
|
|
)
|
|
} else {
|
|
(ct.output().to_string(), String::new())
|
|
};
|
|
let output_c = std::ffi::CString::new(output.as_str()).unwrap_or_default();
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
*out = crate::stubs::common::oakcommon_colortransform_init_output(output_c.as_ptr());
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Traverser family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// No-op render hooks (the offline-evaluation defaults).
|
|
struct NoopHooks;
|
|
|
|
impl oaknode::traverser::RenderHooks for NoopHooks {}
|
|
|
|
/// Engine-side traverser database payload.
|
|
struct TraverseDbPayload {
|
|
rows: Vec<(String, Vec<(oaknode::value::ValueType, oaknode::value::NodeValue)>)>,
|
|
}
|
|
|
|
/// `oaknode_traverser_init`.
|
|
pub fn oaknode_traverser_init() -> CHandle {
|
|
oaknode::handle::make_owned(oaknode::traverser::Traverser::new())
|
|
}
|
|
|
|
/// `oaknode_traverser_free`.
|
|
pub fn oaknode_traverser_free(traverser: *mut CHandle) {
|
|
if traverser.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *traverser };
|
|
release_handle(h);
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *traverser = CHandle::null() };
|
|
}
|
|
|
|
/// `oaknode_traverser_generate_database` — evaluate the node at the
|
|
/// range start and box the output table as the database.
|
|
pub fn oaknode_traverser_generate_database(
|
|
traverser: CHandle,
|
|
node: CHandle,
|
|
in_num: i64,
|
|
in_den: i64,
|
|
_out_num: i64,
|
|
_out_den: i64,
|
|
out_db: *mut CHandle,
|
|
) -> c_int {
|
|
if out_db.is_null() || in_den == 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let nr = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let (project, id) = nr;
|
|
// SAFETY: traverser handles box the domain Traverser.
|
|
let t = match unsafe { crate::handle::domain::boxed_mut::<oaknode::traverser::Traverser>(&traverser) } {
|
|
Some(t) => t,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let request = oaknode::traverser::EvalRequest::new(id, Rational::new(in_num, in_den));
|
|
let g = lock(&project);
|
|
let table = match t.evaluate(&g.graph, &request, &mut NoopHooks) {
|
|
Ok(table) => table,
|
|
Err(e) => return e.code(),
|
|
};
|
|
let rows = vec![(
|
|
"output".to_string(),
|
|
table.rows().iter().map(|(ty, v, _)| (*ty, v.clone())).collect(),
|
|
)];
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_db = oaknode::handle::make_owned(TraverseDbPayload { rows }) };
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_traverser_database_free`.
|
|
pub fn oaknode_traverser_database_free(db: *mut CHandle) {
|
|
if db.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *db };
|
|
release_handle(h);
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *db = CHandle::null() };
|
|
}
|
|
|
|
fn db_payload(db: CHandle) -> Option<&'static TraverseDbPayload> {
|
|
// SAFETY: database handles box TraverseDbPayload.
|
|
let p = unsafe { oaknode::handle::get::<TraverseDbPayload>(&db) }?;
|
|
// SAFETY: the box outlives the handle.
|
|
unsafe { Some(&*(p as *const _)) }
|
|
}
|
|
|
|
/// `oaknode_traverser_database_row_count`.
|
|
pub fn oaknode_traverser_database_row_count(db: CHandle, out_count: *mut c_int) -> c_int {
|
|
if out_count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
match db_payload(db) {
|
|
Some(p) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_count = p.rows.len() as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_traverser_database_row_key_at` (two-stage).
|
|
pub fn oaknode_traverser_database_row_key_at(
|
|
db: CHandle,
|
|
index: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if index < 0 {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
match db_payload(db).and_then(|p| p.rows.get(index as usize)) {
|
|
Some((key, _)) => string_out(key, buf, buf_size),
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_traverser_database_row_value_count`.
|
|
pub fn oaknode_traverser_database_row_value_count(
|
|
db: CHandle,
|
|
key: *const c_char,
|
|
out_count: *mut c_int,
|
|
) -> c_int {
|
|
if out_count.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let key = unsafe { cstr(key) };
|
|
match db_payload(db).and_then(|p| p.rows.iter().find(|(k, _)| *k == key)) {
|
|
Some((_, values)) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_count = values.len() as c_int };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_traverser_database_value_at` — the typed value into the
|
|
/// POD.
|
|
pub fn oaknode_traverser_database_value_at(
|
|
db: CHandle,
|
|
key: *const c_char,
|
|
index: c_int,
|
|
out: *mut crate::node::OakNodeValue,
|
|
) -> c_int {
|
|
if out.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let key = unsafe { cstr(key) };
|
|
match db_payload(db).and_then(|p| p.rows.iter().find(|(k, _)| *k == key)) {
|
|
Some((_, values)) => match values.get(index as usize) {
|
|
Some((ty, v)) => match value_to_pod(*ty, v) {
|
|
Some(pod) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = pod };
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_FAILED,
|
|
},
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
},
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_traverser_database_value_string_at` (two-stage) — the
|
|
/// string-carrying types; numeric types format via `to_string`.
|
|
pub fn oaknode_traverser_database_value_string_at(
|
|
db: CHandle,
|
|
key: *const c_char,
|
|
index: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if index < 0 {
|
|
return OAKNODE_E_NOT_FOUND;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let key = unsafe { cstr(key) };
|
|
match db_payload(db).and_then(|p| p.rows.iter().find(|(k, _)| *k == key)) {
|
|
Some((_, values)) => match values.get(index as usize) {
|
|
Some((_, v)) => match v {
|
|
oaknode::value::NodeValue::Text(s) => string_out(s, buf, buf_size),
|
|
oaknode::value::NodeValue::StrCombo(s) => string_out(s, buf, buf_size),
|
|
other => string_out(&other.to_double().to_string(), buf, buf_size),
|
|
},
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
},
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Serializer family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// Engine-side save-data payload: the project plus an optional node
|
|
/// subset and per-node properties (best-effort: the whole project's
|
|
/// graph is serialized; the subset/properties are recorded for the
|
|
/// load-data mirror).
|
|
struct SaveDataPayload {
|
|
project: ProjectArc,
|
|
#[allow(dead_code)]
|
|
nodes: Vec<(ProjectArc, NodeId)>,
|
|
#[allow(dead_code)]
|
|
properties: Vec<(NodeId, String, String)>,
|
|
}
|
|
|
|
/// Engine-side load-data payload: the loaded project plus its
|
|
/// graph inventory.
|
|
struct LoadDataPayload {
|
|
project: ProjectArc,
|
|
nodes: Vec<NodeId>,
|
|
properties: Vec<(NodeId, String, String)>,
|
|
connections: Vec<(NodeId, NodeId, String, i32)>,
|
|
}
|
|
|
|
/// Replace a project payload's contents with a freshly loaded project
|
|
/// (the load path requires an uninitialized project, so no node
|
|
/// handles reference the old contents).
|
|
fn swap_project_payload(project: CHandle, loaded: ProjectArc) -> Option<()> {
|
|
// SAFETY: project handles box ProjectArc payloads.
|
|
let target = unsafe { crate::handle::domain::boxed_mut::<ProjectArc>(&project) }?;
|
|
let old_key = Arc::as_ptr(target) as usize;
|
|
let old = std::mem::replace(target, loaded);
|
|
drop(old);
|
|
// Retire the old alive-registry entry, register the new one.
|
|
let dead = match registry().get(&old_key) {
|
|
Some(weak) => weak.upgrade().is_none(),
|
|
None => false,
|
|
};
|
|
if dead {
|
|
registry().remove(&old_key);
|
|
alive_dec();
|
|
}
|
|
let new_key = Arc::as_ptr(target) as usize;
|
|
registry().entry(new_key).or_insert_with(|| {
|
|
alive_inc();
|
|
Arc::downgrade(target)
|
|
});
|
|
Some(())
|
|
}
|
|
|
|
/// `oaknode_serializer_initialize` — nothing to do (the serializer is
|
|
/// stateless).
|
|
pub fn oaknode_serializer_initialize() -> c_int {
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_serializer_shutdown` — nothing to do.
|
|
pub fn oaknode_serializer_shutdown() {}
|
|
|
|
/// `oaknode_serializer_savedata_create`.
|
|
pub fn oaknode_serializer_savedata_create(_load_type: c_int, project: CHandle) -> CHandle {
|
|
let p = match unsafe { project_of(&project) }.cloned() {
|
|
Some(p) => p,
|
|
None => return CHandle::null(),
|
|
};
|
|
oaknode::handle::make_owned(SaveDataPayload {
|
|
project: p,
|
|
nodes: Vec::new(),
|
|
properties: Vec::new(),
|
|
})
|
|
}
|
|
|
|
/// `oaknode_serializer_savedata_free`.
|
|
pub fn oaknode_serializer_savedata_free(save_data: *mut CHandle) {
|
|
if save_data.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *save_data };
|
|
release_handle(h);
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *save_data = CHandle::null() };
|
|
}
|
|
|
|
/// `oaknode_serializer_savedata_set_nodes`.
|
|
pub fn oaknode_serializer_savedata_set_nodes(
|
|
save_data: CHandle,
|
|
nodes: *const CHandle,
|
|
count: c_int,
|
|
) -> c_int {
|
|
if nodes.is_null() || count < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: savedata handles box SaveDataPayload.
|
|
let payload = match unsafe { crate::handle::domain::boxed_mut::<SaveDataPayload>(&save_data) } {
|
|
Some(p) => p,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
payload.nodes.clear();
|
|
for i in 0..count as usize {
|
|
// SAFETY: the caller guarantees `count` valid handles.
|
|
let h = unsafe { *nodes.add(i) };
|
|
match unsafe { node_ref_of(&h) } {
|
|
Some(nr) => payload.nodes.push((nr.project.clone(), nr.id)),
|
|
None => return OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_serializer_savedata_set_property`.
|
|
pub fn oaknode_serializer_savedata_set_property(
|
|
save_data: CHandle,
|
|
node: CHandle,
|
|
key: *const c_char,
|
|
value: *const c_char,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees valid NUL-terminated strings.
|
|
let (key, value) = unsafe { (cstr(key), cstr(value)) };
|
|
let node_id = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
// SAFETY: savedata handles box SaveDataPayload.
|
|
match unsafe { crate::handle::domain::boxed_mut::<SaveDataPayload>(&save_data) } {
|
|
Some(p) => {
|
|
p.properties.push((node_id, key, value));
|
|
OAKNODE_OK
|
|
}
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_serializer_save_to_xml` (two-stage) — serialize the whole
|
|
/// project (documented: the C++ serialized only the listed nodes; the
|
|
/// Rust serializer covers the project).
|
|
pub fn oaknode_serializer_save_to_xml(
|
|
save_data: CHandle,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
// SAFETY: savedata handles box SaveDataPayload.
|
|
let payload = match unsafe { oaknode::handle::get::<SaveDataPayload>(&save_data) } {
|
|
Some(p) => p,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let xml = {
|
|
let g = lock(&payload.project);
|
|
match oaknode::serializer::save(&g) {
|
|
Ok(xml) => xml,
|
|
Err(e) => return e.code(),
|
|
}
|
|
};
|
|
string_out(&xml, buf, buf_size)
|
|
}
|
|
|
|
/// `oaknode_serializer_load_from_xml` — parse the XML into the target
|
|
/// project and box the load data.
|
|
pub fn oaknode_serializer_load_from_xml(
|
|
project: CHandle,
|
|
xml: *const c_char,
|
|
_load_type: c_int,
|
|
out_result: *mut c_int,
|
|
out_load_data: *mut CHandle,
|
|
details_buf: *mut c_char,
|
|
details_buf_size: c_int,
|
|
) -> c_int {
|
|
if out_result.is_null() || out_load_data.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let xml = unsafe { cstr(xml) };
|
|
let loaded = match oaknode::serializer::load(&xml) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
// SAFETY: valid out pointers; the error message is
|
|
// surfaced into the details buffer.
|
|
unsafe {
|
|
*out_result = e.code();
|
|
*out_load_data = CHandle::null();
|
|
}
|
|
if !details_buf.is_null() && details_buf_size > 0 {
|
|
// SAFETY: the caller guarantees the buffer size.
|
|
unsafe { crate::handle::write_string(&e.to_string(), details_buf, details_buf_size) };
|
|
}
|
|
return OAKNODE_OK;
|
|
}
|
|
};
|
|
let (nodes, connections) = {
|
|
let g = lock(&loaded);
|
|
(g.graph.node_ids(), g.graph.output_connections_all())
|
|
};
|
|
if swap_project_payload(project, loaded.clone()).is_none() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_result = 0;
|
|
*out_load_data = oaknode::handle::make_owned(LoadDataPayload {
|
|
project: loaded,
|
|
nodes,
|
|
properties: Vec::new(),
|
|
connections,
|
|
});
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
|
|
/// `oaknode_serializer_loaddata_free`.
|
|
pub fn oaknode_serializer_loaddata_free(load_data: *mut CHandle) {
|
|
if load_data.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *load_data };
|
|
release_handle(h);
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *load_data = CHandle::null() };
|
|
}
|
|
|
|
fn loaddata_payload(load_data: CHandle) -> Option<&'static LoadDataPayload> {
|
|
// SAFETY: loaddata handles box LoadDataPayload.
|
|
let p = unsafe { oaknode::handle::get::<LoadDataPayload>(&load_data) }?;
|
|
// SAFETY: the box outlives the handle.
|
|
unsafe { Some(&*(p as *const _)) }
|
|
}
|
|
|
|
/// `oaknode_serializer_loaddata_node_count`.
|
|
pub fn oaknode_serializer_loaddata_node_count(load_data: CHandle) -> c_int {
|
|
match loaddata_payload(load_data) {
|
|
Some(p) => p.nodes.len() as c_int,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_serializer_loaddata_node_at` — borrowed node view.
|
|
pub fn oaknode_serializer_loaddata_node_at(load_data: CHandle, index: c_int) -> CHandle {
|
|
if index < 0 {
|
|
return CHandle::null();
|
|
}
|
|
let p = match loaddata_payload(load_data) {
|
|
Some(p) => p,
|
|
None => return CHandle::null(),
|
|
};
|
|
match p.nodes.get(index as usize).copied() {
|
|
Some(id) => make_node_handle(p.project.clone(), id, false),
|
|
None => CHandle::null(),
|
|
}
|
|
}
|
|
|
|
/// `oaknode_serializer_loaddata_get_property` (two-stage).
|
|
pub fn oaknode_serializer_loaddata_get_property(
|
|
load_data: CHandle,
|
|
node: CHandle,
|
|
key: *const c_char,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let key = unsafe { cstr(key) };
|
|
let node_id = match unsafe { node_ref_of(&node) } {
|
|
Some(nr) => nr.id,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
match loaddata_payload(load_data).and_then(|p| {
|
|
p.properties
|
|
.iter()
|
|
.find(|(n, k, _)| *n == node_id && *k == key)
|
|
.map(|(_, _, v)| v.clone())
|
|
}) {
|
|
Some(value) => string_out(&value, buf, buf_size),
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_serializer_loaddata_connection_count`.
|
|
pub fn oaknode_serializer_loaddata_connection_count(load_data: CHandle) -> c_int {
|
|
match loaddata_payload(load_data) {
|
|
Some(p) => p.connections.len() as c_int,
|
|
None => OAKNODE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_serializer_loaddata_connection_at`.
|
|
pub fn oaknode_serializer_loaddata_connection_at(
|
|
load_data: CHandle,
|
|
index: c_int,
|
|
out_output_node: *mut CHandle,
|
|
out_input_node: *mut CHandle,
|
|
input_id_buf: *mut c_char,
|
|
input_id_buf_size: c_int,
|
|
out_element: *mut c_int,
|
|
) -> c_int {
|
|
if out_output_node.is_null() || out_input_node.is_null() || out_element.is_null() || index < 0 {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
let p = match loaddata_payload(load_data) {
|
|
Some(p) => p,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
match p.connections.get(index as usize) {
|
|
Some((from, to, input, element)) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_output_node = make_node_handle(p.project.clone(), *from, false);
|
|
*out_input_node = make_node_handle(p.project.clone(), *to, false);
|
|
*out_element = *element;
|
|
}
|
|
string_out(input, input_id_buf, input_id_buf_size)
|
|
}
|
|
None => OAKNODE_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// `oaknode_serializer_save_to_file` — serialize the project and write
|
|
/// it to `filename` (compression is not supported by the direct
|
|
/// serializer; the flag is accepted for ABI parity).
|
|
pub fn oaknode_serializer_save_to_file(
|
|
project: CHandle,
|
|
filename: *const c_char,
|
|
_use_compression: c_int,
|
|
out_code: *mut c_int,
|
|
details: *mut c_char,
|
|
details_size: c_int,
|
|
) -> c_int {
|
|
if out_code.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let filename = unsafe { cstr(filename) };
|
|
let p = match unsafe { project_of(&project) }.cloned() {
|
|
Some(p) => p,
|
|
None => return OAKNODE_E_INVALID,
|
|
};
|
|
let xml = {
|
|
let g = lock(&p);
|
|
match oaknode::serializer::save(&g) {
|
|
Ok(xml) => xml,
|
|
Err(e) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_code = e.code();
|
|
if !details.is_null() && details_size > 0 {
|
|
crate::handle::write_string(&e.to_string(), details, details_size);
|
|
}
|
|
}
|
|
return OAKNODE_OK;
|
|
}
|
|
}
|
|
};
|
|
match std::fs::write(&filename, xml) {
|
|
Ok(()) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_code = 0 };
|
|
OAKNODE_OK
|
|
}
|
|
Err(e) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_code = OAKNODE_E_FAILED;
|
|
if !details.is_null() && details_size > 0 {
|
|
crate::handle::write_string(&e.to_string(), details, details_size);
|
|
}
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `oaknode_serializer_load_from_file` — read and parse the file into
|
|
/// the target project.
|
|
pub fn oaknode_serializer_load_from_file(
|
|
project: CHandle,
|
|
filename: *const c_char,
|
|
out_code: *mut c_int,
|
|
details: *mut c_char,
|
|
details_size: c_int,
|
|
) -> c_int {
|
|
if out_code.is_null() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let filename = unsafe { cstr(filename) };
|
|
let xml = match std::fs::read_to_string(&filename) {
|
|
Ok(xml) => xml,
|
|
Err(e) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_code = OAKNODE_E_FAILED;
|
|
if !details.is_null() && details_size > 0 {
|
|
crate::handle::write_string(&e.to_string(), details, details_size);
|
|
}
|
|
}
|
|
return OAKNODE_OK;
|
|
}
|
|
};
|
|
match oaknode::serializer::load(&xml) {
|
|
Ok(loaded) => {
|
|
if swap_project_payload(project, loaded).is_none() {
|
|
return OAKNODE_E_INVALID;
|
|
}
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_code = 0 };
|
|
OAKNODE_OK
|
|
}
|
|
Err(e) => {
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_code = e.code();
|
|
if !details.is_null() && details_size > 0 {
|
|
crate::handle::write_string(&e.to_string(), details, details_size);
|
|
}
|
|
}
|
|
OAKNODE_OK
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// timeline — oaktimeline domain implementation (single-lib unification)
|
|
// ===========================================================================
|
|
//
|
|
// The deleted oaktimeline C ABI is replaced by the crate's direct Rust
|
|
// constructors. The oaktimeline command family holds oaknode domain
|
|
// objects (`oaktimeline::util::NodeRef` = `{Arc<Mutex<Project>>, NodeId}`,
|
|
// converted here from the engine's node-handle payloads) and every
|
|
// creator's `to_command()` yields a real
|
|
// `oakundo::undocommand::UndoCommand`, boxed behind a handle for the
|
|
// facade's undo layer. Marker lists and work areas are value boxes
|
|
// (`oaktimeline::handle::make_owned`), exactly what the crate's own
|
|
// marker/workarea commands read back through `get_mut`.
|
|
pub mod timeline {
|
|
use std::ffi::{c_char, c_int};
|
|
|
|
use oakcore_rs::{Rational, TimeRange};
|
|
use oakundo::undocommand::{command_from_owned, UndoCommand};
|
|
|
|
use crate::handle::domain::node_ref_of;
|
|
use crate::handle::CHandle;
|
|
|
|
/// Map an engine node handle to the oaktimeline domain reference.
|
|
fn node_ref(h: CHandle) -> Option<oaktimeline::util::NodeRef> {
|
|
// SAFETY: node handles box oaknode NodeRef payloads.
|
|
let nr = unsafe { node_ref_of(&h) }?;
|
|
Some(oaktimeline::util::NodeRef {
|
|
project: nr.project.clone(),
|
|
id: nr.id,
|
|
})
|
|
}
|
|
|
|
/// Map an engine node handle to the oaktimeline domain reference.
|
|
macro_rules! nref {
|
|
($h:expr) => {
|
|
match node_ref($h) {
|
|
Some(n) => n,
|
|
None => return CHandle::null(),
|
|
}
|
|
};
|
|
}
|
|
|
|
/// Box an [`UndoCommand`] behind a handle.
|
|
fn box_command(cmd: UndoCommand) -> CHandle {
|
|
// SAFETY: `command_from_owned` owns the command value.
|
|
unsafe { command_from_owned(cmd) }
|
|
}
|
|
|
|
/// Two-stage string getter (required length including the NUL).
|
|
fn string_out(s: &str, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
let required = (s.len() + 1) as c_int;
|
|
if !buf.is_null() && buf_size >= required {
|
|
// SAFETY: the caller guarantees `buf` holds `buf_size` bytes.
|
|
unsafe {
|
|
std::ptr::copy_nonoverlapping(s.as_ptr() as *const c_char, buf, s.len());
|
|
*buf.add(s.len()) = 0;
|
|
}
|
|
}
|
|
required
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Marker family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// `oaktimeline_marker_list_create` — a fresh marker list box.
|
|
pub fn oaktimeline_marker_list_create() -> CHandle {
|
|
oaktimeline::handle::make_owned(oaktimeline::marker::TimelineMarkerList::new())
|
|
}
|
|
|
|
/// `oaktimeline_marker_list_of` — the owner sequence's marker list
|
|
/// (created lazily; addref'd copy).
|
|
pub fn oaktimeline_marker_list_of(owner: CHandle) -> CHandle {
|
|
let nr = match unsafe { node_ref_of(&owner) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
let (project, id) = nr;
|
|
let list = {
|
|
let mut g = project.lock().unwrap_or_else(|e| e.into_inner());
|
|
let seq = match g
|
|
.graph
|
|
.get_mut(id)
|
|
.and_then(|e| e.behavior.as_any_mut())
|
|
.and_then(|a| a.downcast_mut::<oaknode::sequence::SequenceBehavior>())
|
|
{
|
|
Some(s) => s,
|
|
None => return CHandle::null(),
|
|
};
|
|
if seq.markers.is_null() {
|
|
seq.markers = oaktimeline::handle::make_owned(
|
|
oaktimeline::marker::TimelineMarkerList::new(),
|
|
);
|
|
}
|
|
seq.markers
|
|
};
|
|
if let Some(addref) = list.addref {
|
|
// SAFETY: the handle is live.
|
|
unsafe { addref(list.ctx) };
|
|
}
|
|
list
|
|
}
|
|
|
|
/// `oaktimeline_marker_list_free`.
|
|
pub fn oaktimeline_marker_list_free(list: *mut CHandle) {
|
|
if list.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *list };
|
|
if let Some(release) = h.release {
|
|
// SAFETY: the boxed value's release callback.
|
|
unsafe { release(h.ctx) };
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *list = CHandle::null() };
|
|
}
|
|
|
|
/// `oaktimeline_marker_add` — live add.
|
|
pub fn oaktimeline_marker_add(
|
|
list: CHandle,
|
|
in_num: c_int,
|
|
in_den: c_int,
|
|
out_num: c_int,
|
|
out_den: c_int,
|
|
name: *const c_char,
|
|
color: c_int,
|
|
) -> c_int {
|
|
if in_den == 0 || out_den == 0 {
|
|
return oaktimeline::error::OAKTIMELINE_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let name = unsafe { crate::handle::read_cstr(name) };
|
|
let list_mut = unsafe { oaktimeline::handle::get_mut::<std::sync::Arc<std::sync::Mutex<oaktimeline::marker::TimelineMarkerList>>>(&list) };
|
|
let Some(list_mut) = list_mut else {
|
|
return oaktimeline::error::OAKTIMELINE_E_INVALID;
|
|
};
|
|
list_mut.lock().unwrap_or_else(|e| e.into_inner()).add_marker(oaktimeline::marker::TimelineMarker::with_time(
|
|
color,
|
|
TimeRange::new(
|
|
Rational::new(in_num as i64, in_den as i64),
|
|
Rational::new(out_num as i64, out_den as i64),
|
|
),
|
|
&name,
|
|
));
|
|
oaktimeline::error::OAKTIMELINE_OK
|
|
}
|
|
|
|
/// `oaktimeline_marker_count`.
|
|
pub fn oaktimeline_marker_count(list: CHandle, out_count: *mut c_int) -> c_int {
|
|
if out_count.is_null() {
|
|
return oaktimeline::error::OAKTIMELINE_E_INVALID;
|
|
}
|
|
// SAFETY: marker-list handles box TimelineMarkerList.
|
|
match unsafe { oaktimeline::handle::get::<std::sync::Arc<std::sync::Mutex<oaktimeline::marker::TimelineMarkerList>>>(&list) } {
|
|
Some(l) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out_count = l.lock().unwrap_or_else(|e| e.into_inner()).size() as c_int };
|
|
oaktimeline::error::OAKTIMELINE_OK
|
|
}
|
|
None => oaktimeline::error::OAKTIMELINE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaktimeline_marker_at` — in/out/color + name (two-stage).
|
|
pub fn oaktimeline_marker_at(
|
|
list: CHandle,
|
|
index: c_int,
|
|
in_num: *mut c_int,
|
|
in_den: *mut c_int,
|
|
out_num: *mut c_int,
|
|
out_den: *mut c_int,
|
|
color: *mut c_int,
|
|
name_buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
if index < 0
|
|
|| in_num.is_null()
|
|
|| in_den.is_null()
|
|
|| out_num.is_null()
|
|
|| out_den.is_null()
|
|
|| color.is_null()
|
|
{
|
|
return oaktimeline::error::OAKTIMELINE_E_INVALID;
|
|
}
|
|
// SAFETY: marker-list handles box TimelineMarkerList.
|
|
let l = match unsafe { oaktimeline::handle::get::<std::sync::Arc<std::sync::Mutex<oaktimeline::marker::TimelineMarkerList>>>(&list) } {
|
|
Some(l) => l,
|
|
None => return oaktimeline::error::OAKTIMELINE_E_INVALID,
|
|
};
|
|
let l = l.lock().unwrap_or_else(|e| e.into_inner());
|
|
let Some(m) = l.at(index as usize) else {
|
|
return oaktimeline::error::OAKTIMELINE_E_NOT_FOUND;
|
|
};
|
|
let range = m.time();
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*in_num = range.in_().numerator() as c_int;
|
|
*in_den = range.in_().denominator() as c_int;
|
|
*out_num = range.out().numerator() as c_int;
|
|
*out_den = range.out().denominator() as c_int;
|
|
*color = m.color();
|
|
}
|
|
string_out(m.name(), name_buf, buf_size)
|
|
}
|
|
|
|
/// `oaktimeline_marker_add_command`.
|
|
pub fn oaktimeline_marker_add_command(
|
|
list: CHandle,
|
|
in_num: c_int,
|
|
in_den: c_int,
|
|
out_num: c_int,
|
|
out_den: c_int,
|
|
name: *const c_char,
|
|
color: c_int,
|
|
) -> CHandle {
|
|
if in_den == 0 || out_den == 0 {
|
|
return CHandle::null();
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let name = unsafe { crate::handle::read_cstr(name) };
|
|
let cmd = oaktimeline::marker::MarkerAddCommand::new(
|
|
list,
|
|
TimeRange::new(
|
|
Rational::new(in_num as i64, in_den as i64),
|
|
Rational::new(out_num as i64, out_den as i64),
|
|
),
|
|
&name,
|
|
color,
|
|
);
|
|
box_command(cmd.to_command())
|
|
}
|
|
|
|
/// `oaktimeline_marker_remove_at_command`.
|
|
pub fn oaktimeline_marker_remove_at_command(list: CHandle, index: c_int) -> CHandle {
|
|
if index < 0 {
|
|
return CHandle::null();
|
|
}
|
|
let cmd = oaktimeline::marker::MarkerRemoveCommand::new(list, index as usize);
|
|
box_command(cmd.to_command())
|
|
}
|
|
|
|
/// `oaktimeline_marker_set_time_command`.
|
|
pub fn oaktimeline_marker_set_time_command(
|
|
list: CHandle,
|
|
index: c_int,
|
|
in_num: c_int,
|
|
in_den: c_int,
|
|
out_num: c_int,
|
|
out_den: c_int,
|
|
) -> CHandle {
|
|
if index < 0 || in_den == 0 || out_den == 0 {
|
|
return CHandle::null();
|
|
}
|
|
let cmd = oaktimeline::marker::MarkerChangeTimeCommand::new(
|
|
list,
|
|
index as usize,
|
|
TimeRange::new(
|
|
Rational::new(in_num as i64, in_den as i64),
|
|
Rational::new(out_num as i64, out_den as i64),
|
|
),
|
|
);
|
|
box_command(cmd.to_command())
|
|
}
|
|
|
|
/// `oaktimeline_marker_set_props_command` — color + name changes in
|
|
/// one multi command.
|
|
pub fn oaktimeline_marker_set_props_command(
|
|
list: CHandle,
|
|
index: c_int,
|
|
color: c_int,
|
|
name: *const c_char,
|
|
) -> CHandle {
|
|
if index < 0 {
|
|
return CHandle::null();
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let name = unsafe { crate::handle::read_cstr(name) };
|
|
let mut multi = UndoCommand::multi();
|
|
multi.multi_add_child(
|
|
oaktimeline::marker::MarkerChangeColorCommand::new(list, index as usize, color)
|
|
.to_command(),
|
|
);
|
|
multi.multi_add_child(
|
|
oaktimeline::marker::MarkerChangeNameCommand::new(list, index as usize, &name)
|
|
.to_command(),
|
|
);
|
|
box_command(multi)
|
|
}
|
|
|
|
/// `oaktimeline_marker_list_load` — STUB: the XML surface of the
|
|
/// deleted marker list (reader-backed) has no direct Rust equivalent
|
|
/// in the oaktimeline crate (documented; the engine does not consume
|
|
/// this path).
|
|
pub fn oaktimeline_marker_list_load(_list: CHandle, _reader: CHandle) -> c_int {
|
|
oaktimeline::error::OAKTIMELINE_E_FAILED
|
|
}
|
|
|
|
/// `oaktimeline_marker_list_save` — STUB: see
|
|
/// [`oaktimeline_marker_list_load`].
|
|
pub fn oaktimeline_marker_list_save(_list: CHandle, _writer: CHandle) -> c_int {
|
|
oaktimeline::error::OAKTIMELINE_E_FAILED
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Work area family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// `oaktimeline_workarea_create` — a fresh work-area box.
|
|
pub fn oaktimeline_workarea_create() -> CHandle {
|
|
oaktimeline::handle::make_owned(oaktimeline::workarea::TimelineWorkArea::new())
|
|
}
|
|
|
|
/// `oaktimeline_workarea_of` — the owner sequence's work area (created
|
|
/// lazily; addref'd copy).
|
|
pub fn oaktimeline_workarea_of(owner: CHandle) -> CHandle {
|
|
let nr = match unsafe { node_ref_of(&owner) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
let (project, id) = nr;
|
|
let workarea = {
|
|
let mut g = project.lock().unwrap_or_else(|e| e.into_inner());
|
|
let seq = match g
|
|
.graph
|
|
.get_mut(id)
|
|
.and_then(|e| e.behavior.as_any_mut())
|
|
.and_then(|a| a.downcast_mut::<oaknode::sequence::SequenceBehavior>())
|
|
{
|
|
Some(s) => s,
|
|
None => return CHandle::null(),
|
|
};
|
|
if seq.workarea.is_null() {
|
|
seq.workarea = oaktimeline::handle::make_owned(
|
|
oaktimeline::workarea::TimelineWorkArea::new(),
|
|
);
|
|
}
|
|
seq.workarea
|
|
};
|
|
if let Some(addref) = workarea.addref {
|
|
// SAFETY: the handle is live.
|
|
unsafe { addref(workarea.ctx) };
|
|
}
|
|
workarea
|
|
}
|
|
|
|
/// `oaktimeline_workarea_free`.
|
|
pub fn oaktimeline_workarea_free(w: *mut CHandle) {
|
|
if w.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *w };
|
|
if let Some(release) = h.release {
|
|
// SAFETY: the boxed value's release callback.
|
|
unsafe { release(h.ctx) };
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *w = CHandle::null() };
|
|
}
|
|
|
|
/// `oaktimeline_workarea_set_enabled`.
|
|
pub fn oaktimeline_workarea_set_enabled(w: CHandle, enabled: c_int) -> c_int {
|
|
// SAFETY: work-area handles box TimelineWorkArea.
|
|
match unsafe { oaktimeline::handle::get_mut::<std::sync::Arc<std::sync::Mutex<oaktimeline::workarea::TimelineWorkArea>>>(&w) } {
|
|
Some(wa) => {
|
|
wa.lock().unwrap_or_else(|e| e.into_inner()).set_enabled(enabled != 0);
|
|
oaktimeline::error::OAKTIMELINE_OK
|
|
}
|
|
None => oaktimeline::error::OAKTIMELINE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaktimeline_workarea_get` — range + enabled.
|
|
pub fn oaktimeline_workarea_get(
|
|
w: CHandle,
|
|
in_num: *mut c_int,
|
|
in_den: *mut c_int,
|
|
out_num: *mut c_int,
|
|
out_den: *mut c_int,
|
|
enabled: *mut c_int,
|
|
) -> c_int {
|
|
// SAFETY: work-area handles box TimelineWorkArea.
|
|
let wa = match unsafe { oaktimeline::handle::get::<std::sync::Arc<std::sync::Mutex<oaktimeline::workarea::TimelineWorkArea>>>(&w) } {
|
|
Some(wa) => wa,
|
|
None => return oaktimeline::error::OAKTIMELINE_E_INVALID,
|
|
};
|
|
let range = *wa.lock().unwrap_or_else(|e| e.into_inner()).range();
|
|
// Out params may individually be NULL (the header contract); write
|
|
// each only when the caller supplied a target.
|
|
unsafe {
|
|
if !in_num.is_null() {
|
|
*in_num = range.in_().numerator() as c_int;
|
|
}
|
|
if !in_den.is_null() {
|
|
*in_den = range.in_().denominator() as c_int;
|
|
}
|
|
if !out_num.is_null() {
|
|
*out_num = range.out().numerator() as c_int;
|
|
}
|
|
if !out_den.is_null() {
|
|
*out_den = range.out().denominator() as c_int;
|
|
}
|
|
if !enabled.is_null() {
|
|
*enabled = if wa.lock().unwrap_or_else(|e| e.into_inner()).enabled() { 1 } else { 0 };
|
|
}
|
|
}
|
|
oaktimeline::error::OAKTIMELINE_OK
|
|
}
|
|
|
|
/// `oaktimeline_workarea_set_range`.
|
|
pub fn oaktimeline_workarea_set_range(
|
|
w: CHandle,
|
|
in_num: c_int,
|
|
in_den: c_int,
|
|
out_num: c_int,
|
|
out_den: c_int,
|
|
) -> c_int {
|
|
if in_den == 0 || out_den == 0 {
|
|
return oaktimeline::error::OAKTIMELINE_E_INVALID;
|
|
}
|
|
// SAFETY: work-area handles box TimelineWorkArea.
|
|
match unsafe { oaktimeline::handle::get_mut::<std::sync::Arc<std::sync::Mutex<oaktimeline::workarea::TimelineWorkArea>>>(&w) } {
|
|
Some(wa) => {
|
|
wa.lock().unwrap_or_else(|e| e.into_inner()).set_range(TimeRange::new(
|
|
Rational::new(in_num as i64, in_den as i64),
|
|
Rational::new(out_num as i64, out_den as i64),
|
|
));
|
|
oaktimeline::error::OAKTIMELINE_OK
|
|
}
|
|
None => oaktimeline::error::OAKTIMELINE_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaktimeline_workarea_set_range_command`.
|
|
pub fn oaktimeline_workarea_set_range_command(
|
|
w: CHandle,
|
|
in_num: c_int,
|
|
in_den: c_int,
|
|
out_num: c_int,
|
|
out_den: c_int,
|
|
old_in_num: c_int,
|
|
old_in_den: c_int,
|
|
old_out_num: c_int,
|
|
old_out_den: c_int,
|
|
) -> CHandle {
|
|
if in_den == 0 || out_den == 0 || old_in_den == 0 || old_out_den == 0 {
|
|
return CHandle::null();
|
|
}
|
|
let range = TimeRange::new(
|
|
Rational::new(in_num as i64, in_den as i64),
|
|
Rational::new(out_num as i64, out_den as i64),
|
|
);
|
|
let old_range = TimeRange::new(
|
|
Rational::new(old_in_num as i64, old_in_den as i64),
|
|
Rational::new(old_out_num as i64, old_out_den as i64),
|
|
);
|
|
let cmd = oaktimeline::workarea::WorkareaSetRangeCommand::new_with_old(w, range, old_range);
|
|
box_command(cmd.to_command())
|
|
}
|
|
|
|
/// `oaktimeline_workarea_set_enabled_command`.
|
|
pub fn oaktimeline_workarea_set_enabled_command(w: CHandle, enabled: c_int) -> CHandle {
|
|
let cmd =
|
|
oaktimeline::workarea::WorkareaSetEnabledCommand::new(w, enabled != 0);
|
|
box_command(cmd.to_command())
|
|
}
|
|
|
|
/// `oaktimeline_workarea_reset` — the documented reset defaults
|
|
/// (`reset_in`/`reset_out`).
|
|
pub fn oaktimeline_workarea_reset(
|
|
in_num: *mut c_int,
|
|
in_den: *mut c_int,
|
|
out_num: *mut c_int,
|
|
out_den: *mut c_int,
|
|
) -> c_int {
|
|
if in_num.is_null() || in_den.is_null() || out_num.is_null() || out_den.is_null() {
|
|
return oaktimeline::error::OAKTIMELINE_E_INVALID;
|
|
}
|
|
let rin = oaktimeline::workarea::reset_in();
|
|
let rout = oaktimeline::workarea::reset_out();
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*in_num = rin.numerator() as c_int;
|
|
*in_den = rin.denominator() as c_int;
|
|
*out_num = rout.numerator() as c_int;
|
|
*out_den = rout.denominator() as c_int;
|
|
}
|
|
oaktimeline::error::OAKTIMELINE_OK
|
|
}
|
|
|
|
/// `oaktimeline_workarea_load` — STUB: see
|
|
/// [`oaktimeline_marker_list_load`].
|
|
pub fn oaktimeline_workarea_load(_w: CHandle, _reader: CHandle) -> c_int {
|
|
oaktimeline::error::OAKTIMELINE_E_FAILED
|
|
}
|
|
|
|
/// `oaktimeline_workarea_save` — STUB: see
|
|
/// [`oaktimeline_marker_list_load`].
|
|
pub fn oaktimeline_workarea_save(_w: CHandle, _writer: CHandle) -> c_int {
|
|
oaktimeline::error::OAKTIMELINE_E_FAILED
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Timeline edit commands (oakundo UndoCommand values)
|
|
// -------------------------------------------------------------------
|
|
|
|
/// `oaktimeline_add_track_command`.
|
|
pub fn oaktimeline_add_track_command(list: CHandle) -> CHandle {
|
|
let timeline = nref!(list);
|
|
box_command(oaktimeline::undogeneral::TimelineAddTrackCommand::new(timeline).to_command())
|
|
}
|
|
|
|
/// `oaktimeline_remove_track_command`.
|
|
pub fn oaktimeline_remove_track_command(track: CHandle) -> CHandle {
|
|
let track = nref!(track);
|
|
box_command(oaktimeline::undogeneral::TimelineRemoveTrackCommand::new(track).to_command())
|
|
}
|
|
|
|
/// `oaktimeline_place_block_command`.
|
|
pub fn oaktimeline_place_block_command(
|
|
list: CHandle,
|
|
track_index: c_int,
|
|
block: CHandle,
|
|
in_num: i64,
|
|
in_den: i64,
|
|
) -> CHandle {
|
|
if in_den == 0 {
|
|
return CHandle::null();
|
|
}
|
|
let timeline = nref!(list);
|
|
let block = nref!(block);
|
|
let cmd = oaktimeline::undopointer::TrackPlaceBlockCommand::new(
|
|
timeline,
|
|
track_index,
|
|
block,
|
|
Rational::new(in_num, in_den),
|
|
);
|
|
box_command(cmd.to_command())
|
|
}
|
|
|
|
/// `oaktimeline_replace_block_with_gap_command`.
|
|
pub fn oaktimeline_replace_block_with_gap_command(track: CHandle, block: CHandle) -> CHandle {
|
|
let track = nref!(track);
|
|
let block = nref!(block);
|
|
let cmd = oaktimeline::undogeneral::TrackReplaceBlockWithGapCommand::new(
|
|
track, block, true,
|
|
);
|
|
box_command(cmd.to_command())
|
|
}
|
|
|
|
/// `oaktimeline_move_block_command`.
|
|
pub fn oaktimeline_move_block_command(
|
|
list: CHandle,
|
|
track_index: c_int,
|
|
block: CHandle,
|
|
in_num: i64,
|
|
in_den: i64,
|
|
) -> CHandle {
|
|
if in_den == 0 {
|
|
return CHandle::null();
|
|
}
|
|
let timeline = nref!(list);
|
|
let block = nref!(block);
|
|
let cmd = oaktimeline::undopointer::TrackMoveBlockCommand::new(
|
|
timeline,
|
|
track_index,
|
|
block,
|
|
Rational::new(in_num, in_den),
|
|
);
|
|
box_command(cmd.to_command())
|
|
}
|
|
|
|
/// `oaktimeline_trim_command`.
|
|
pub fn oaktimeline_trim_command(
|
|
track: CHandle,
|
|
block: CHandle,
|
|
new_length_num: i64,
|
|
new_length_den: i64,
|
|
mode: c_int,
|
|
) -> CHandle {
|
|
if new_length_den == 0 {
|
|
return CHandle::null();
|
|
}
|
|
let track = nref!(track);
|
|
let block = nref!(block);
|
|
let mode = match oaktimeline::common::MovementMode::from_c_int(mode) {
|
|
Some(m) => m,
|
|
None => return CHandle::null(),
|
|
};
|
|
let cmd = oaktimeline::undopointer::BlockTrimCommand::new(
|
|
track,
|
|
block,
|
|
Rational::new(new_length_num, new_length_den),
|
|
mode,
|
|
);
|
|
box_command(cmd.to_command())
|
|
}
|
|
|
|
/// `oaktimeline_split_command` — one split command per block, combined
|
|
/// into a multi command.
|
|
pub fn oaktimeline_split_command(
|
|
blocks: *const CHandle,
|
|
count: c_int,
|
|
point_num: i64,
|
|
point_den: i64,
|
|
) -> CHandle {
|
|
if blocks.is_null() || count <= 0 || point_den == 0 {
|
|
return CHandle::null();
|
|
}
|
|
let mut children = Vec::new();
|
|
for i in 0..count as usize {
|
|
// SAFETY: the caller guarantees `count` valid handles.
|
|
let h = unsafe { *blocks.add(i) };
|
|
let Some(block) = node_ref(h) else {
|
|
return CHandle::null();
|
|
};
|
|
let cmd = oaktimeline::undosplit::BlockSplitCommand::new(
|
|
block,
|
|
Rational::new(point_num, point_den),
|
|
);
|
|
children.push(cmd.to_command());
|
|
}
|
|
let mut multi = UndoCommand::multi();
|
|
for c in children {
|
|
multi.multi_add_child(c);
|
|
}
|
|
box_command(multi)
|
|
}
|
|
|
|
/// `oaktimeline_split_preserving_links_command`.
|
|
pub fn oaktimeline_split_preserving_links_command(
|
|
blocks: *const CHandle,
|
|
count: c_int,
|
|
point_nums: *const i64,
|
|
point_dens: *const i64,
|
|
time_count: c_int,
|
|
) -> CHandle {
|
|
if blocks.is_null() || count <= 0 || point_nums.is_null() || point_dens.is_null() || time_count <= 0 {
|
|
return CHandle::null();
|
|
}
|
|
let mut refs = Vec::new();
|
|
for i in 0..count as usize {
|
|
// SAFETY: the caller guarantees `count` valid handles.
|
|
let h = unsafe { *blocks.add(i) };
|
|
let Some(b) = node_ref(h) else {
|
|
return CHandle::null();
|
|
};
|
|
refs.push(b);
|
|
}
|
|
let mut times = Vec::new();
|
|
for i in 0..time_count as usize {
|
|
// SAFETY: the caller guarantees `time_count` valid entries.
|
|
let (n, d) = unsafe { (*point_nums.add(i), *point_dens.add(i)) };
|
|
if d == 0 {
|
|
return CHandle::null();
|
|
}
|
|
times.push(Rational::new(n, d));
|
|
}
|
|
let cmd = oaktimeline::undosplit::BlockSplitPreservingLinksCommand::new(refs, times);
|
|
box_command(cmd.to_command())
|
|
}
|
|
|
|
/// `oaktimeline_ripple_delete_gaps_command`.
|
|
pub fn oaktimeline_ripple_delete_gaps_command(
|
|
sequence: CHandle,
|
|
in_nums: *const i64,
|
|
in_dens: *const i64,
|
|
out_nums: *const i64,
|
|
out_dens: *const i64,
|
|
tracks: *const CHandle,
|
|
range_count: c_int,
|
|
) -> CHandle {
|
|
if in_nums.is_null()
|
|
|| in_dens.is_null()
|
|
|| out_nums.is_null()
|
|
|| out_dens.is_null()
|
|
|| tracks.is_null()
|
|
|| range_count <= 0
|
|
{
|
|
return CHandle::null();
|
|
}
|
|
let timeline = nref!(sequence);
|
|
let mut regions = Vec::new();
|
|
for i in 0..range_count as usize {
|
|
// SAFETY: the caller guarantees `range_count` valid entries.
|
|
let (in_n, in_d, out_n, out_d) = unsafe {
|
|
(
|
|
*in_nums.add(i),
|
|
*in_dens.add(i),
|
|
*out_nums.add(i),
|
|
*out_dens.add(i),
|
|
)
|
|
};
|
|
let track = unsafe { *tracks.add(i) };
|
|
if in_d == 0 || out_d == 0 {
|
|
return CHandle::null();
|
|
}
|
|
let Some(track) = node_ref(track) else {
|
|
return CHandle::null();
|
|
};
|
|
regions.push((
|
|
track,
|
|
TimeRange::new(Rational::new(in_n, in_d), Rational::new(out_n, out_d)),
|
|
));
|
|
}
|
|
let cmd =
|
|
oaktimeline::undoripple::TimelineRippleDeleteGapsAtRegionsCommand::new(timeline, regions);
|
|
box_command(cmd.to_command())
|
|
}
|
|
|
|
/// `oaktimeline_slide_command`.
|
|
pub fn oaktimeline_slide_command(
|
|
track: CHandle,
|
|
blocks: *const CHandle,
|
|
block_count: c_int,
|
|
in_adjacent: CHandle,
|
|
out_adjacent: CHandle,
|
|
movement_num: i64,
|
|
movement_den: i64,
|
|
) -> CHandle {
|
|
if blocks.is_null() || block_count <= 0 || movement_den == 0 {
|
|
return CHandle::null();
|
|
}
|
|
let track = nref!(track);
|
|
let mut refs = Vec::new();
|
|
for i in 0..block_count as usize {
|
|
// SAFETY: the caller guarantees `block_count` valid handles.
|
|
let h = unsafe { *blocks.add(i) };
|
|
let Some(b) = node_ref(h) else {
|
|
return CHandle::null();
|
|
};
|
|
refs.push(b);
|
|
}
|
|
let in_adjacent = if in_adjacent.is_null() {
|
|
None
|
|
} else {
|
|
node_ref(in_adjacent)
|
|
};
|
|
let out_adjacent = if out_adjacent.is_null() {
|
|
None
|
|
} else {
|
|
node_ref(out_adjacent)
|
|
};
|
|
let cmd = oaktimeline::undopointer::TrackSlideCommand::new(
|
|
track,
|
|
refs,
|
|
in_adjacent,
|
|
out_adjacent,
|
|
Rational::new(movement_num, movement_den),
|
|
);
|
|
box_command(cmd.to_command())
|
|
}
|
|
|
|
/// `oaktimeline_ripple_remove_area_command`.
|
|
pub fn oaktimeline_ripple_remove_area_command(
|
|
track: CHandle,
|
|
in_num: i64,
|
|
in_den: i64,
|
|
out_num: i64,
|
|
out_den: i64,
|
|
) -> CHandle {
|
|
if in_den == 0 || out_den == 0 {
|
|
return CHandle::null();
|
|
}
|
|
let track = nref!(track);
|
|
let cmd = oaktimeline::undoripple::TrackRippleRemoveAreaCommand::new(
|
|
track,
|
|
TimeRange::new(Rational::new(in_num, in_den), Rational::new(out_num, out_den)),
|
|
);
|
|
box_command(cmd.to_command())
|
|
}
|
|
|
|
/// `oaktimeline_insert_gaps_command`.
|
|
pub fn oaktimeline_insert_gaps_command(
|
|
list: CHandle,
|
|
point_num: i64,
|
|
point_den: i64,
|
|
length_num: i64,
|
|
length_den: i64,
|
|
) -> CHandle {
|
|
if point_den == 0 || length_den == 0 {
|
|
return CHandle::null();
|
|
}
|
|
let track_list = nref!(list);
|
|
let cmd = oaktimeline::undogeneral::TrackListInsertGaps::new(
|
|
track_list,
|
|
Rational::new(point_num, point_den),
|
|
Rational::new(length_num, length_den),
|
|
);
|
|
box_command(cmd.to_command())
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// audio — direct Rust shims over the oakaudio crate
|
|
// ===========================================================================
|
|
|
|
/// oakaudio bridge replacements: direct Rust calls into the `oakaudio`
|
|
/// crate (single-lib unification). Same names/signatures as the deleted
|
|
/// bridge functions; the manager CHandle is a borrow-only validity token
|
|
/// (the module owns the process-wide singleton), the processor CHandle
|
|
/// boxes an owned `oakaudio::processor::Processor` with a release
|
|
/// callback. The `oakcore_audioparams_*` C ABI (opaque `AudioParams`
|
|
/// handles in the frozen engine contract) is implemented here, inside the
|
|
/// dylib — the accessors were host-provided (C++ liboakcore, later Rust
|
|
/// mock shims in the app/cli/test binaries) until M12 P5 folded them in
|
|
/// so the cdylib carries no undefined imports.
|
|
pub mod audio {
|
|
use std::ffi::{c_char, c_double, c_int, c_void, CStr};
|
|
|
|
use oakaudio::error::{
|
|
OAKAUDIO_E_FAILED, OAKAUDIO_E_INVALID, OAKAUDIO_E_NOT_FOUND, OAKAUDIO_E_STATE, OAKAUDIO_OK,
|
|
};
|
|
use oakaudio::manager::ManagerInner;
|
|
use oakaudio::params::{sample_format_from_i32, AudioParams};
|
|
use oakaudio::processor::Processor;
|
|
use oakaudio::synchronizer::{self, SourceClip as ModuleSourceClip};
|
|
use oakaudio::waveform;
|
|
use oakaudio::waveformsync;
|
|
use oakcore_rs::Rational;
|
|
|
|
use crate::handle::CHandle;
|
|
|
|
pub use crate::pods::{
|
|
AudioEncodingParams as EncodingParams, MinMax, OffsetResult, SourceClip,
|
|
StretchOffsetResult,
|
|
};
|
|
|
|
/// The liboakcore `AudioParams` object layout
|
|
/// (`oakcore_audioparams.h`): sample rate, ffmpeg channel-layout mask,
|
|
/// sample format, stream index, duration and time base. Mirrors
|
|
/// `olive::core::AudioParams` (and the app/cli mock shims it replaces),
|
|
/// so the engine contract's borrowed/owned `AudioParams*` handles
|
|
/// round-trip unchanged.
|
|
#[repr(C)]
|
|
struct HostAudioParams {
|
|
sample_rate: c_int,
|
|
channel_layout: u64,
|
|
format: c_int,
|
|
stream_index: c_int,
|
|
duration: i64,
|
|
time_base_num: c_int,
|
|
time_base_den: c_int,
|
|
}
|
|
|
|
/// `oakcore_audioparams_create` — new owned audio params (release with
|
|
/// `oakcore_audioparams_free`). The time base defaults to 1/sample_rate,
|
|
/// exactly like the liboakcore constructor.
|
|
#[no_mangle]
|
|
pub extern "C" fn oakcore_audioparams_create(
|
|
sample_rate: c_int,
|
|
channel_layout: u64,
|
|
format: c_int,
|
|
) -> *mut c_void {
|
|
let den = if sample_rate > 0 { sample_rate } else { 1 };
|
|
Box::into_raw(Box::new(HostAudioParams {
|
|
sample_rate,
|
|
channel_layout,
|
|
format,
|
|
stream_index: 0,
|
|
duration: 0,
|
|
time_base_num: 1,
|
|
time_base_den: den,
|
|
})) as *mut c_void
|
|
}
|
|
|
|
/// `oakcore_audioparams_free` — NULL no-op.
|
|
///
|
|
/// # Safety
|
|
/// `params` must come from [`oakcore_audioparams_create`] or be NULL.
|
|
#[no_mangle]
|
|
pub extern "C" fn oakcore_audioparams_free(params: *mut c_void) {
|
|
if params.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: produced by `oakcore_audioparams_create`; we hold the only
|
|
// reference after the box is dropped.
|
|
unsafe { drop(Box::from_raw(params as *mut HostAudioParams)) };
|
|
}
|
|
|
|
/// `oakcore_audioparams_sample_rate` — 0 for NULL (liboakcore contract).
|
|
///
|
|
/// # Safety
|
|
/// `params` must come from [`oakcore_audioparams_create`] or be NULL.
|
|
#[no_mangle]
|
|
pub extern "C" fn oakcore_audioparams_sample_rate(params: *const c_void) -> c_int {
|
|
if params.is_null() {
|
|
return 0;
|
|
}
|
|
// SAFETY: contract above.
|
|
unsafe { (*(params as *const HostAudioParams)).sample_rate }
|
|
}
|
|
|
|
/// `oakcore_audioparams_channel_layout` — 0 for NULL.
|
|
///
|
|
/// # Safety
|
|
/// `params` must come from [`oakcore_audioparams_create`] or be NULL.
|
|
#[no_mangle]
|
|
pub extern "C" fn oakcore_audioparams_channel_layout(params: *const c_void) -> u64 {
|
|
if params.is_null() {
|
|
return 0;
|
|
}
|
|
// SAFETY: contract above.
|
|
unsafe { (*(params as *const HostAudioParams)).channel_layout }
|
|
}
|
|
|
|
/// `oakcore_audioparams_format` — 0 for NULL.
|
|
///
|
|
/// # Safety
|
|
/// `params` must come from [`oakcore_audioparams_create`] or be NULL.
|
|
#[no_mangle]
|
|
pub extern "C" fn oakcore_audioparams_format(params: *const c_void) -> c_int {
|
|
if params.is_null() {
|
|
return 0;
|
|
}
|
|
// SAFETY: contract above.
|
|
unsafe { (*(params as *const HostAudioParams)).format }
|
|
}
|
|
|
|
/// `oakcore_audioparams_set_time_base` — NULL no-op.
|
|
///
|
|
/// # Safety
|
|
/// `params` must come from [`oakcore_audioparams_create`] or be NULL.
|
|
#[no_mangle]
|
|
pub extern "C" fn oakcore_audioparams_set_time_base(
|
|
params: *mut c_void,
|
|
num: c_int,
|
|
den: c_int,
|
|
) {
|
|
if params.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: contract above.
|
|
unsafe {
|
|
let p = &mut *(params as *mut HostAudioParams);
|
|
p.time_base_num = num;
|
|
p.time_base_den = den;
|
|
}
|
|
}
|
|
|
|
/// Map an oakaudio `Box<dyn Error>` result to its public module code
|
|
/// (unknown downstream errors collapse to `OAKAUDIO_E_FAILED`, the same
|
|
/// fallback the deleted ffi layer used).
|
|
fn audio_code(e: &(dyn std::error::Error + 'static)) -> c_int {
|
|
match e.downcast_ref::<oakaudio::error::Error>() {
|
|
Some(audio_err) => audio_err.code(),
|
|
None => OAKAUDIO_E_FAILED,
|
|
}
|
|
}
|
|
|
|
/// Write `s` into `buf` NUL-terminated with truncation (the deleted
|
|
/// module ffi's `write_error` contract; NULL/zero-sized buffers are
|
|
/// documented no-ops).
|
|
///
|
|
/// # Safety
|
|
/// `buf` must point to `buf_size` writable bytes when non-NULL and
|
|
/// `buf_size > 0`.
|
|
unsafe fn write_error(s: &str, buf: *mut c_char, buf_size: c_int) {
|
|
if buf.is_null() || buf_size <= 0 {
|
|
return;
|
|
}
|
|
// SAFETY: contract above.
|
|
unsafe {
|
|
let bytes = s.as_bytes();
|
|
let n = bytes.len().min((buf_size - 1) as usize);
|
|
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, n);
|
|
*(buf as *mut u8).add(n) = 0;
|
|
}
|
|
}
|
|
|
|
/// Map a failure's message into `error_buf` when it carries one (the
|
|
/// deleted ffi surfaced `Error::Failed` context strings on the failure
|
|
/// path only).
|
|
unsafe fn write_failed_error(
|
|
e: &(dyn std::error::Error + 'static),
|
|
error_buf: *mut c_char,
|
|
error_buf_size: c_int,
|
|
) {
|
|
if let Some(oakaudio::error::Error::Failed(msg)) =
|
|
e.downcast_ref::<oakaudio::error::Error>()
|
|
{
|
|
// SAFETY: forwarded to `write_error`'s contract.
|
|
unsafe { write_error(msg, error_buf, error_buf_size) };
|
|
}
|
|
}
|
|
|
|
/// Non-null sentinel `ctx` for the borrow-only manager handle token.
|
|
/// Never dereferenced; the engine's `oakengine_audio_manager_handle`
|
|
/// hands it to the host as an opaque event-subscription token.
|
|
fn manager_token() -> *mut c_void {
|
|
std::ptr::NonNull::<u8>::dangling().as_ptr() as *mut c_void
|
|
}
|
|
|
|
/// Lock the process-wide manager singleton (`None` when absent) and
|
|
/// reject empty handle tokens with the documented state error.
|
|
fn with_manager(self_: CHandle) -> Result<std::sync::MutexGuard<'static, ManagerInner>, c_int> {
|
|
if self_.is_null() {
|
|
return Err(OAKAUDIO_E_STATE);
|
|
}
|
|
oakaudio::manager::instance().ok_or(OAKAUDIO_E_STATE)
|
|
}
|
|
|
|
/// Borrow-only token of the manager singleton (empty when none).
|
|
pub fn oakaudio_manager_instance() -> CHandle {
|
|
match oakaudio::manager::instance() {
|
|
Some(_) => CHandle {
|
|
ctx: manager_token(),
|
|
addref: None,
|
|
release: None,
|
|
abi_version: 0,
|
|
},
|
|
None => CHandle::null(),
|
|
}
|
|
}
|
|
|
|
/// Create the process-wide manager singleton (no-op when it exists).
|
|
pub fn oakaudio_manager_create_instance() -> c_int {
|
|
match ManagerInner::create_instance() {
|
|
Ok(()) => OAKAUDIO_OK,
|
|
Err(e) => audio_code(&*e),
|
|
}
|
|
}
|
|
|
|
/// Destroy the manager singleton (no-op when none exists).
|
|
pub fn oakaudio_manager_destroy_instance() {
|
|
ManagerInner::destroy_instance();
|
|
}
|
|
|
|
/// Borrowed singleton handles carry no release callback; free just
|
|
/// nulls the token.
|
|
pub fn oakaudio_manager_free(_self: *mut CHandle) {
|
|
if _self.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *_self = CHandle::null() };
|
|
}
|
|
|
|
/// Bytes between output-notify pulses.
|
|
pub fn oakaudio_manager_set_output_notify_interval(_self: CHandle, _bytes: i64) -> c_int {
|
|
match with_manager(_self).and_then(|m| m.set_output_notify_interval(_bytes).map_err(|e| audio_code(&*e))) {
|
|
Ok(()) => OAKAUDIO_OK,
|
|
Err(code) => code,
|
|
}
|
|
}
|
|
|
|
/// Push a block of samples to the output device.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn oakaudio_manager_push_to_output(
|
|
_self: CHandle,
|
|
_rate: c_int,
|
|
_layout: u64,
|
|
_format: c_int,
|
|
_samples: *const c_char,
|
|
_samples_size: i64,
|
|
_error_buf: *mut c_char,
|
|
_error_buf_size: c_int,
|
|
) -> c_int {
|
|
// CPP-PARITY (deleted ffi): `rate <= 0 || !samples ||
|
|
// samples_size < 0` is invalid; an unrepresentable sample format
|
|
// is additionally rejected.
|
|
if _rate <= 0 || _samples.is_null() || _samples_size < 0 {
|
|
return OAKAUDIO_E_INVALID;
|
|
}
|
|
let format = sample_format_from_i32(_format);
|
|
if format == oakaudio::params::SampleFormat::Invalid {
|
|
return OAKAUDIO_E_INVALID;
|
|
}
|
|
let mut guard = match with_manager(_self) {
|
|
Ok(g) => g,
|
|
Err(code) => return code,
|
|
};
|
|
// SAFETY: the caller guarantees `_samples` points to
|
|
// `_samples_size` readable bytes (size 0 reads nothing).
|
|
let samples = unsafe {
|
|
if _samples_size <= 0 {
|
|
&[][..]
|
|
} else {
|
|
std::slice::from_raw_parts(_samples as *const u8, _samples_size as usize)
|
|
}
|
|
};
|
|
let params = AudioParams {
|
|
sample_rate: _rate,
|
|
channel_layout: _layout,
|
|
format,
|
|
};
|
|
match guard.push_to_output(params, samples, &mut []) {
|
|
Ok(()) => OAKAUDIO_OK,
|
|
Err(e) => {
|
|
// SAFETY: NULL/empty error buffers are documented no-ops.
|
|
unsafe { write_failed_error(&*e, _error_buf, _error_buf_size) };
|
|
audio_code(&*e)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Discard buffered output.
|
|
pub fn oakaudio_manager_clear_buffered_output(_self: CHandle) -> c_int {
|
|
match with_manager(_self).and_then(|m| m.clear_buffered_output().map_err(|e| audio_code(&*e))) {
|
|
Ok(()) => OAKAUDIO_OK,
|
|
Err(code) => code,
|
|
}
|
|
}
|
|
|
|
/// Stop the output stream.
|
|
pub fn oakaudio_manager_stop_output(_self: CHandle) -> c_int {
|
|
match with_manager(_self).and_then(|mut m| m.stop_output().map_err(|e| audio_code(&*e))) {
|
|
Ok(()) => OAKAUDIO_OK,
|
|
Err(code) => code,
|
|
}
|
|
}
|
|
|
|
/// Restart the output clock at zero.
|
|
pub fn oakaudio_manager_reset_output_clock(_self: CHandle) -> c_int {
|
|
match with_manager(_self).and_then(|m| m.reset_output_clock().map_err(|e| audio_code(&*e))) {
|
|
Ok(()) => OAKAUDIO_OK,
|
|
Err(code) => code,
|
|
}
|
|
}
|
|
|
|
/// Current output device index (`paNoDevice` = -1) or a negative error.
|
|
pub fn oakaudio_manager_get_output_device(_self: CHandle) -> c_int {
|
|
match with_manager(_self).and_then(|m| m.get_output_device().map_err(|e| audio_code(&*e))) {
|
|
Ok(device) => device,
|
|
Err(code) => code,
|
|
}
|
|
}
|
|
|
|
/// Set the output device index.
|
|
pub fn oakaudio_manager_set_output_device(_self: CHandle, _device: c_int) -> c_int {
|
|
match with_manager(_self).and_then(|mut m| m.set_output_device(_device).map_err(|e| audio_code(&*e))) {
|
|
Ok(()) => OAKAUDIO_OK,
|
|
Err(code) => code,
|
|
}
|
|
}
|
|
|
|
/// Current input device index or a negative error code.
|
|
pub fn oakaudio_manager_get_input_device(_self: CHandle) -> c_int {
|
|
match with_manager(_self).and_then(|m| m.get_input_device().map_err(|e| audio_code(&*e))) {
|
|
Ok(device) => device,
|
|
Err(code) => code,
|
|
}
|
|
}
|
|
|
|
/// Set the input device index.
|
|
pub fn oakaudio_manager_set_input_device(_self: CHandle, _device: c_int) -> c_int {
|
|
match with_manager(_self).and_then(|mut m| m.set_input_device(_device).map_err(|e| audio_code(&*e))) {
|
|
Ok(()) => OAKAUDIO_OK,
|
|
Err(code) => code,
|
|
}
|
|
}
|
|
|
|
/// The number of host output devices (enumeration order == the device
|
|
/// index `set_output_device` takes). Needs no manager instance.
|
|
pub fn oakaudio_output_device_count() -> c_int {
|
|
oakaudio::manager::output_device_names().len() as c_int
|
|
}
|
|
|
|
/// The number of host input devices (see [`oakaudio_output_device_count`]).
|
|
pub fn oakaudio_input_device_count() -> c_int {
|
|
oakaudio::manager::input_device_names().len() as c_int
|
|
}
|
|
|
|
/// The name of output device `index` (two-stage buf/size; reports the
|
|
/// required size INCLUDING the NUL, the module convention the facade
|
|
/// converts with `string_result`).
|
|
///
|
|
/// # Safety
|
|
/// `buf` must point to `buf_size` writable bytes when non-NULL.
|
|
pub unsafe fn oakaudio_output_device_name(
|
|
index: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
unsafe { device_name_result(oakaudio::manager::output_device_names(), index, buf, buf_size) }
|
|
}
|
|
|
|
/// The name of input device `index` (see [`oakaudio_output_device_name`]).
|
|
///
|
|
/// # Safety
|
|
/// `buf` must point to `buf_size` writable bytes when non-NULL.
|
|
pub unsafe fn oakaudio_input_device_name(
|
|
index: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
unsafe { device_name_result(oakaudio::manager::input_device_names(), index, buf, buf_size) }
|
|
}
|
|
|
|
/// Shared two-stage string write for the device-name getters: reports
|
|
/// `len + 1` (the required buffer size including the NUL), writes the
|
|
/// NUL-terminated name when it fits. `OAKAUDIO_E_NOT_FOUND` for an
|
|
/// out-of-range index.
|
|
unsafe fn device_name_result(
|
|
names: Vec<String>,
|
|
index: c_int,
|
|
buf: *mut c_char,
|
|
buf_size: c_int,
|
|
) -> c_int {
|
|
let Some(name) = names.get(index.max(0) as usize).filter(|_| index >= 0) else {
|
|
return OAKAUDIO_E_NOT_FOUND;
|
|
};
|
|
// SAFETY: `buf` points to `buf_size` writable bytes when non-NULL.
|
|
unsafe {
|
|
if !buf.is_null() && buf_size > 0 {
|
|
let copy = name.len().min((buf_size as usize).saturating_sub(1));
|
|
std::ptr::copy_nonoverlapping(name.as_ptr(), buf as *mut u8, copy);
|
|
*buf.add(copy) = 0;
|
|
}
|
|
}
|
|
name.len() as c_int + 1
|
|
}
|
|
|
|
/// Close the output stream and reset the playback state.
|
|
pub fn oakaudio_manager_hard_reset(_self: CHandle) -> c_int {
|
|
match with_manager(_self).and_then(|mut m| m.hard_reset().map_err(|e| audio_code(&*e))) {
|
|
Ok(()) => OAKAUDIO_OK,
|
|
Err(code) => code,
|
|
}
|
|
}
|
|
|
|
/// Start recording the input device through the oakcodec encoder.
|
|
pub fn oakaudio_manager_start_recording(
|
|
_self: CHandle,
|
|
_params: *const EncodingParams,
|
|
_error_buf: *mut c_char,
|
|
_error_buf_size: c_int,
|
|
) -> c_int {
|
|
// CPP-PARITY (deleted ffi): `!params || !params->audio_enabled`
|
|
// is invalid; the error string is written on the invalid path too
|
|
// so callers can always diagnose a failed start.
|
|
if _params.is_null() {
|
|
// SAFETY: NULL/empty error buffers are documented no-ops.
|
|
unsafe { write_error("invalid recording parameters", _error_buf, _error_buf_size) };
|
|
return OAKAUDIO_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees `_params` points to a live
|
|
// `EncodingParams` POD for the duration of the call.
|
|
let params = unsafe { &*_params };
|
|
if params.audio_enabled == 0 {
|
|
// SAFETY: NULL/empty error buffers are documented no-ops.
|
|
unsafe { write_error("invalid recording parameters", _error_buf, _error_buf_size) };
|
|
return OAKAUDIO_E_INVALID;
|
|
}
|
|
let mut guard = match with_manager(_self) {
|
|
Ok(g) => g,
|
|
Err(code) => return code,
|
|
};
|
|
match guard.start_recording(params, &mut []) {
|
|
Ok(()) => OAKAUDIO_OK,
|
|
Err(e) => {
|
|
// SAFETY: NULL/empty error buffers are documented no-ops.
|
|
unsafe { write_failed_error(&*e, _error_buf, _error_buf_size) };
|
|
audio_code(&*e)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Stop recording.
|
|
pub fn oakaudio_manager_stop_recording(_self: CHandle) -> c_int {
|
|
match with_manager(_self).and_then(|mut m| m.stop_recording().map_err(|e| audio_code(&*e))) {
|
|
Ok(()) => OAKAUDIO_OK,
|
|
Err(code) => code,
|
|
}
|
|
}
|
|
|
|
/// Seconds of audio consumed by the output device since the last reset.
|
|
pub fn oakaudio_manager_seconds(_self: CHandle, _out: *mut c_double) -> c_int {
|
|
let guard = match with_manager(_self) {
|
|
Ok(g) => g,
|
|
Err(code) => return code,
|
|
};
|
|
if _out.is_null() {
|
|
return OAKAUDIO_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees `_out` is a writable f64.
|
|
let out = unsafe { &mut *_out };
|
|
match guard.seconds(out) {
|
|
Ok(()) => OAKAUDIO_OK,
|
|
Err(e) => audio_code(&*e),
|
|
}
|
|
}
|
|
|
|
/// Per-channel peak levels of the buffered, not-yet-consumed output.
|
|
/// Returns the channel count (>= 0) or a negative error code.
|
|
pub fn oakaudio_manager_output_levels(_self: CHandle, _peaks: *mut f32, _capacity: c_int) -> c_int {
|
|
let guard = match with_manager(_self) {
|
|
Ok(g) => g,
|
|
Err(code) => return code,
|
|
};
|
|
if _peaks.is_null() || _capacity <= 0 {
|
|
return OAKAUDIO_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees `_peaks` holds `_capacity` f32s.
|
|
let peaks = unsafe { std::slice::from_raw_parts_mut(_peaks, _capacity as usize) };
|
|
match guard.output_levels(peaks) {
|
|
Ok(n) => n,
|
|
Err(e) => audio_code(&*e),
|
|
}
|
|
}
|
|
|
|
// ---- Processor (owned, refcounted box behind a CHandle) ----------------
|
|
|
|
/// Test-only leak counter replacing the deleted module C ABI's
|
|
/// `oakaudio_debug_alive_count` (the oakaudio crate dropped its debug
|
|
/// counter together with its ffi layer). Counts live boxed processors
|
|
/// created through [`oakaudio_processor_init`] and released through
|
|
/// [`release_processor`]; the production cdylib has no counter.
|
|
#[cfg(test)]
|
|
static PROCESSOR_ALIVE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
|
|
|
|
/// Test-only: current live-processor count (see [`PROCESSOR_ALIVE`]).
|
|
#[cfg(test)]
|
|
pub fn oakaudio_debug_alive_count() -> c_int {
|
|
PROCESSOR_ALIVE.load(std::sync::atomic::Ordering::SeqCst)
|
|
}
|
|
|
|
/// Release callback for a boxed `oakaudio::processor::Processor`.
|
|
unsafe extern "C" fn release_processor(ctx: *mut c_void) {
|
|
if !ctx.is_null() {
|
|
// SAFETY: `ctx` was produced by `oakaudio_processor_init` and
|
|
// this callback runs exactly once.
|
|
unsafe { drop(Box::from_raw(ctx as *mut Processor)) };
|
|
#[cfg(test)]
|
|
PROCESSOR_ALIVE.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
|
|
}
|
|
}
|
|
|
|
/// Create a closed audio processor (boxed behind a CHandle).
|
|
pub fn oakaudio_processor_init() -> CHandle {
|
|
let p = Box::new(Processor::init());
|
|
#[cfg(test)]
|
|
PROCESSOR_ALIVE.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
CHandle {
|
|
ctx: Box::into_raw(p) as *mut c_void,
|
|
addref: None,
|
|
release: Some(release_processor),
|
|
abi_version: 0,
|
|
}
|
|
}
|
|
|
|
/// Release the boxed processor through the handle's release callback.
|
|
pub fn oakaudio_processor_free(_self: *mut CHandle) {
|
|
if _self.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { &mut *_self };
|
|
if let Some(release) = h.release {
|
|
// SAFETY: `release` is the boxed type's release callback.
|
|
unsafe { release(h.ctx) };
|
|
}
|
|
*h = CHandle::null();
|
|
}
|
|
|
|
/// Open the processor's conversion graph.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn oakaudio_processor_open(
|
|
_self: CHandle,
|
|
_in_rate: c_int,
|
|
_in_layout: u64,
|
|
_in_format: c_int,
|
|
_out_rate: c_int,
|
|
_out_layout: u64,
|
|
_out_format: c_int,
|
|
_speed: c_double,
|
|
) -> c_int {
|
|
if _self.is_null() {
|
|
return OAKAUDIO_E_STATE;
|
|
}
|
|
// SAFETY: `_self.ctx` was produced by `oakaudio_processor_init`.
|
|
let p = unsafe { &*(_self.ctx as *const Processor) };
|
|
let from = AudioParams {
|
|
sample_rate: _in_rate,
|
|
channel_layout: _in_layout,
|
|
format: sample_format_from_i32(_in_format),
|
|
};
|
|
let to = AudioParams {
|
|
sample_rate: _out_rate,
|
|
channel_layout: _out_layout,
|
|
format: sample_format_from_i32(_out_format),
|
|
};
|
|
match p.open(from, to, _speed) {
|
|
Ok(()) => OAKAUDIO_OK,
|
|
Err(e) => audio_code(&*e),
|
|
}
|
|
}
|
|
|
|
/// Close the processor's conversion graph (safe when closed).
|
|
pub fn oakaudio_processor_close(_self: CHandle) -> c_int {
|
|
if _self.is_null() {
|
|
return OAKAUDIO_E_STATE;
|
|
}
|
|
// SAFETY: see `oakaudio_processor_open`.
|
|
let p = unsafe { &*(_self.ctx as *const Processor) };
|
|
match p.close() {
|
|
Ok(()) => OAKAUDIO_OK,
|
|
Err(e) => audio_code(&*e),
|
|
}
|
|
}
|
|
|
|
/// 1 when open, 0 when closed.
|
|
pub fn oakaudio_processor_is_open(_self: CHandle) -> c_int {
|
|
if _self.is_null() {
|
|
return 0;
|
|
}
|
|
// SAFETY: see `oakaudio_processor_open`.
|
|
let p = unsafe { &*(_self.ctx as *const Processor) };
|
|
match p.is_open() {
|
|
Ok(open) => open as c_int,
|
|
Err(_) => 0,
|
|
}
|
|
}
|
|
|
|
// ---- Stateless synchronization helpers ---------------------------------
|
|
|
|
/// Rebuild the per-window validity mask the C ABI carried as `u8`s.
|
|
/// A NULL pointer (or an impossible length) yields an empty mask —
|
|
/// which the module treats as "all windows valid".
|
|
///
|
|
/// # Safety
|
|
/// `mask` must point to `window_count` readable bytes when non-NULL.
|
|
unsafe fn window_mask(mask: *const u8, sample_len: c_int, window_samples: u64) -> Vec<bool> {
|
|
if mask.is_null() || window_samples == 0 || sample_len <= 0 {
|
|
return Vec::new();
|
|
}
|
|
// SAFETY: contract above; the mask has one byte per RMS window.
|
|
let window_count = (sample_len as usize).div_ceil(window_samples as usize);
|
|
unsafe { std::slice::from_raw_parts(mask, window_count) }
|
|
.iter()
|
|
.map(|&b| b != 0)
|
|
.collect()
|
|
}
|
|
|
|
/// Estimate the sample offset between two RMS envelopes.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn oakaudio_sync_estimate_envelope_offset(
|
|
_reference: *const c_double,
|
|
_reference_len: c_int,
|
|
_candidate: *const c_double,
|
|
_candidate_len: c_int,
|
|
_reference_valid: *const u8,
|
|
_candidate_valid: *const u8,
|
|
_window_samples: u64,
|
|
_max_offset_windows: i64,
|
|
_out: *mut OffsetResult,
|
|
) -> c_int {
|
|
// CPP-PARITY (deleted ffi sync.cpp:93): NULL arrays and
|
|
// non-positive lengths are invalid; masks may be NULL (all windows
|
|
// valid).
|
|
if _out.is_null()
|
|
|| _reference.is_null()
|
|
|| _candidate.is_null()
|
|
|| _reference_len <= 0
|
|
|| _candidate_len <= 0
|
|
|| _window_samples == 0
|
|
|| _max_offset_windows < 0
|
|
{
|
|
return OAKAUDIO_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees the sample arrays hold their
|
|
// lengths; the mask slices are built from the same contract.
|
|
let reference = unsafe { std::slice::from_raw_parts(_reference, _reference_len as usize) };
|
|
let candidate = unsafe { std::slice::from_raw_parts(_candidate, _candidate_len as usize) };
|
|
let reference_valid = unsafe { window_mask(_reference_valid, _reference_len, _window_samples) };
|
|
let candidate_valid = unsafe { window_mask(_candidate_valid, _candidate_len, _window_samples) };
|
|
let result = waveformsync::estimate_envelope_offset_valid(
|
|
reference,
|
|
candidate,
|
|
&reference_valid,
|
|
&candidate_valid,
|
|
_window_samples as usize,
|
|
_max_offset_windows,
|
|
);
|
|
// SAFETY: `_out` is a writable `OffsetResult` POD (checked above).
|
|
unsafe {
|
|
(*_out).offset_samples = result.offset_samples;
|
|
(*_out).confidence = result.confidence;
|
|
(*_out).valid = result.valid as c_int;
|
|
}
|
|
OAKAUDIO_OK
|
|
}
|
|
|
|
/// Estimate a playback-rate change plus offset.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn oakaudio_sync_estimate_stretch_and_offset(
|
|
_reference: *const c_double,
|
|
_reference_len: c_int,
|
|
_candidate: *const c_double,
|
|
_candidate_len: c_int,
|
|
_reference_valid: *const u8,
|
|
_candidate_valid: *const u8,
|
|
_window_samples: u64,
|
|
_max_offset_windows: i64,
|
|
_min_rate: c_double,
|
|
_max_rate: c_double,
|
|
_rate_step: c_double,
|
|
_out: *mut StretchOffsetResult,
|
|
) -> c_int {
|
|
// CPP-PARITY (deleted ffi sync.cpp:123).
|
|
if _out.is_null()
|
|
|| _reference.is_null()
|
|
|| _candidate.is_null()
|
|
|| _reference_len <= 0
|
|
|| _candidate_len <= 0
|
|
|| _window_samples == 0
|
|
|| _max_offset_windows < 0
|
|
|| _min_rate <= 0.0
|
|
|| _max_rate < _min_rate
|
|
|| _rate_step <= 0.0
|
|
{
|
|
return OAKAUDIO_E_INVALID;
|
|
}
|
|
// SAFETY: see `oakaudio_sync_estimate_envelope_offset`.
|
|
let reference = unsafe { std::slice::from_raw_parts(_reference, _reference_len as usize) };
|
|
let candidate = unsafe { std::slice::from_raw_parts(_candidate, _candidate_len as usize) };
|
|
let reference_valid = unsafe { window_mask(_reference_valid, _reference_len, _window_samples) };
|
|
let candidate_valid = unsafe { window_mask(_candidate_valid, _candidate_len, _window_samples) };
|
|
let result = waveformsync::estimate_stretch_and_offset(
|
|
reference,
|
|
candidate,
|
|
&reference_valid,
|
|
&candidate_valid,
|
|
_window_samples as usize,
|
|
_max_offset_windows,
|
|
_min_rate,
|
|
_max_rate,
|
|
_rate_step,
|
|
);
|
|
// SAFETY: `_out` is a writable `StretchOffsetResult` POD.
|
|
unsafe {
|
|
(*_out).rate = result.rate;
|
|
(*_out).offset_samples = result.offset_samples;
|
|
(*_out).confidence = result.confidence;
|
|
(*_out).valid = result.valid as c_int;
|
|
}
|
|
OAKAUDIO_OK
|
|
}
|
|
|
|
/// Rebuild a module `SourceClip` from the engine's POD mirror.
|
|
fn module_source_clip(pod: &SourceClip) -> ModuleSourceClip {
|
|
ModuleSourceClip {
|
|
source_start_time: Rational::new(pod.source_start_time_num, pod.source_start_time_den),
|
|
media_in: Rational::new(pod.media_in_num, pod.media_in_den),
|
|
has_source_start_time: pod.has_source_start_time != 0,
|
|
}
|
|
}
|
|
|
|
/// Timeline placement from source timecodes.
|
|
pub fn oakaudio_sync_place_by_source_time(
|
|
_reference: *const SourceClip,
|
|
_candidate: *const SourceClip,
|
|
_reference_timeline_in_num: i64,
|
|
_reference_timeline_in_den: i64,
|
|
_out_num: *mut i64,
|
|
_out_den: *mut i64,
|
|
_out_valid: *mut c_int,
|
|
) -> c_int {
|
|
// CPP-PARITY (deleted ffi sync.cpp:153): every denominator must be
|
|
// non-zero.
|
|
if _reference.is_null() || _candidate.is_null() {
|
|
return OAKAUDIO_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees valid `SourceClip` PODs.
|
|
let (reference, candidate) = unsafe { (&*_reference, &*_candidate) };
|
|
if _out_num.is_null()
|
|
|| _out_den.is_null()
|
|
|| _out_valid.is_null()
|
|
|| reference.source_start_time_den == 0
|
|
|| reference.media_in_den == 0
|
|
|| candidate.source_start_time_den == 0
|
|
|| candidate.media_in_den == 0
|
|
|| _reference_timeline_in_den == 0
|
|
{
|
|
return OAKAUDIO_E_INVALID;
|
|
}
|
|
let reference_clip = module_source_clip(reference);
|
|
let candidate_clip = module_source_clip(candidate);
|
|
let placement = synchronizer::place_by_source_time(
|
|
&reference_clip,
|
|
&candidate_clip,
|
|
Rational::new(_reference_timeline_in_num, _reference_timeline_in_den),
|
|
);
|
|
// SAFETY: the caller guarantees writable out parameters.
|
|
unsafe {
|
|
*_out_num = placement.timeline_in.numerator();
|
|
*_out_den = placement.timeline_in.denominator();
|
|
*_out_valid = placement.valid as c_int;
|
|
}
|
|
OAKAUDIO_OK
|
|
}
|
|
|
|
/// Timeline placement from a measured waveform offset.
|
|
pub fn oakaudio_sync_place_by_waveform_offset(
|
|
_reference_timeline_in_num: i64,
|
|
_reference_timeline_in_den: i64,
|
|
_candidate_offset_samples: i64,
|
|
_sample_rate: c_int,
|
|
_out_num: *mut i64,
|
|
_out_den: *mut i64,
|
|
_out_valid: *mut c_int,
|
|
) -> c_int {
|
|
// CPP-PARITY (deleted ffi sync.cpp:191).
|
|
if _out_num.is_null()
|
|
|| _out_den.is_null()
|
|
|| _out_valid.is_null()
|
|
|| _reference_timeline_in_den == 0
|
|
{
|
|
return OAKAUDIO_E_INVALID;
|
|
}
|
|
let placement = synchronizer::place_by_waveform_offset(
|
|
Rational::new(_reference_timeline_in_num, _reference_timeline_in_den),
|
|
_candidate_offset_samples,
|
|
_sample_rate,
|
|
);
|
|
// SAFETY: the caller guarantees writable out parameters.
|
|
unsafe {
|
|
*_out_num = placement.timeline_in.numerator();
|
|
*_out_den = placement.timeline_in.denominator();
|
|
*_out_valid = placement.valid as c_int;
|
|
}
|
|
OAKAUDIO_OK
|
|
}
|
|
|
|
/// Two-stage whole-file min/max waveform extraction. The module's
|
|
/// [`waveform::extract`] has no two-stage entry point, so the shim
|
|
/// emulates it: the decode outcome is cached per (filename, stream,
|
|
/// samples-per-point) so the count pass and the data pass decode ONCE
|
|
/// (the deleted ffi cached for the same reason — the FFmpeg decoder is
|
|
/// not safe against back-to-back sessions in the same process). A
|
|
/// missing file reports `OAKAUDIO_E_NOT_FOUND` (the deleted bridge's
|
|
/// codec-probe behavior) before any decode is attempted.
|
|
pub fn oakaudio_waveform_extract(
|
|
_filename: *const c_char,
|
|
_stream_index: c_int,
|
|
_samples_per_point: c_int,
|
|
_out_pairs: *mut MinMax,
|
|
_capacity_points: c_int,
|
|
_out_channel_count: *mut c_int,
|
|
) -> c_int {
|
|
// CPP-PARITY (deleted ffi waveform.cpp:409).
|
|
if _filename.is_null() || _stream_index < 0 || _samples_per_point <= 0 || _capacity_points < 0 {
|
|
return OAKAUDIO_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated path.
|
|
let filename = unsafe { CStr::from_ptr(_filename) };
|
|
let key = (
|
|
filename.to_string_lossy().into_owned(),
|
|
_stream_index,
|
|
_samples_per_point,
|
|
);
|
|
// The deleted bridge surfaced the codec probe's NOT_FOUND for a
|
|
// missing file; the direct module API reports a generic failure.
|
|
use std::os::unix::ffi::OsStrExt;
|
|
if std::fs::metadata(std::path::Path::new(std::ffi::OsStr::from_bytes(
|
|
filename.to_bytes(),
|
|
)))
|
|
.is_err()
|
|
{
|
|
return OAKAUDIO_E_NOT_FOUND;
|
|
}
|
|
let outcome = {
|
|
static CACHE: std::sync::OnceLock<
|
|
std::sync::Mutex<std::collections::HashMap<
|
|
(String, c_int, c_int),
|
|
waveform::ExtractOutcome,
|
|
>>,
|
|
> = std::sync::OnceLock::new();
|
|
let cache = CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
|
|
let mut guard = cache.lock().unwrap_or_else(|e| e.into_inner());
|
|
if let Some(cached) = guard.get(&key) {
|
|
cached.clone()
|
|
} else {
|
|
let outcome = match waveform::extract(filename, _stream_index, _samples_per_point) {
|
|
Ok(outcome) => outcome,
|
|
Err(e) => return audio_code(e.as_ref()),
|
|
};
|
|
guard.insert(key, outcome.clone());
|
|
outcome
|
|
}
|
|
};
|
|
let channels = outcome.channels.max(0) as usize;
|
|
if !_out_channel_count.is_null() {
|
|
// SAFETY: the caller guarantees a writable c_int.
|
|
unsafe { *_out_channel_count = outcome.channels };
|
|
}
|
|
let points_needed = if channels == 0 {
|
|
0
|
|
} else {
|
|
outcome.points.len() / channels
|
|
};
|
|
// The data pass writes only when the capacity covers the point
|
|
// count (capacity is in points, not pairs).
|
|
if _out_pairs.is_null() || (_capacity_points as usize) < points_needed {
|
|
return points_needed as c_int;
|
|
}
|
|
// SAFETY: `_out_pairs` holds `points_needed * channels` entries
|
|
// (capacity checked above).
|
|
unsafe {
|
|
std::ptr::copy_nonoverlapping(
|
|
outcome.points.as_ptr() as *const MinMax,
|
|
_out_pairs,
|
|
points_needed * channels,
|
|
);
|
|
}
|
|
points_needed as c_int
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// plugin — direct Rust shims over the oakplugin crate
|
|
// ===========================================================================
|
|
|
|
/// oakplugin bridge replacements: direct Rust calls into the `oakplugin`
|
|
/// crate (single-lib unification). Same names/signatures as the deleted
|
|
/// bridge functions; the OFX host singleton is [`oakplugin::host::Host::global`].
|
|
pub mod plugin {
|
|
use std::ffi::{c_char, c_int, CStr};
|
|
use std::path::Path;
|
|
|
|
use oakplugin::error::{OAKPLUGIN_E_INVALID, OAKPLUGIN_E_NOT_FOUND, OAKPLUGIN_OK};
|
|
use oakplugin::host::Host;
|
|
|
|
use crate::handle::read_cstr;
|
|
|
|
/// Standard two-stage getter copy: copy only when the buffer is large
|
|
/// enough (never truncates); always return the required size incl. NUL.
|
|
fn copy_string(value: &str, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
let required = (value.len() + 1) as c_int;
|
|
if !buf.is_null() && buf_size >= required {
|
|
// SAFETY: the caller guarantees `buf` holds `buf_size` bytes.
|
|
unsafe {
|
|
std::ptr::copy_nonoverlapping(value.as_ptr() as *const c_char, buf, value.len());
|
|
*buf.add(value.len()) = 0;
|
|
}
|
|
}
|
|
required
|
|
}
|
|
|
|
/// Whether `(buf, buf_size)` is a valid two-stage getter output.
|
|
fn is_valid_string_out(buf: *mut c_char, buf_size: c_int) -> bool {
|
|
buf_size >= 0 && (buf_size == 0 || !buf.is_null())
|
|
}
|
|
|
|
/// Scan the given plugin bundle directories (oakplugin_host_scan).
|
|
pub fn oakplugin_host_scan(_bundle_dirs: *const *const c_char, _dir_count: c_int) -> c_int {
|
|
if _bundle_dirs.is_null() || _dir_count < 0 {
|
|
return OAKPLUGIN_E_INVALID;
|
|
}
|
|
let host = Host::global();
|
|
for i in 0.._dir_count as usize {
|
|
// SAFETY: the caller guarantees `_dir_count` valid directory
|
|
// pointers; NULL entries are skipped (documented no-op).
|
|
let dir_ptr = unsafe { *_bundle_dirs.add(i) };
|
|
if dir_ptr.is_null() {
|
|
continue;
|
|
}
|
|
// SAFETY: `dir_ptr` is a valid NUL-terminated C string.
|
|
let dir = match unsafe { CStr::from_ptr(dir_ptr) }.to_str() {
|
|
Ok(dir) => dir,
|
|
Err(_) => return OAKPLUGIN_E_INVALID, // non-UTF-8 paths are rejected
|
|
};
|
|
if let Err(e) = host.cache.scan_path(Path::new(dir)) {
|
|
return e.code();
|
|
}
|
|
}
|
|
OAKPLUGIN_OK
|
|
}
|
|
|
|
/// Initialize the OFX host (oakplugin_host_init; idempotent singleton).
|
|
pub fn oakplugin_host_init() -> c_int {
|
|
let _ = Host::global();
|
|
OAKPLUGIN_OK
|
|
}
|
|
|
|
/// Number of loaded plugins.
|
|
pub fn oakplugin_host_plugin_count() -> c_int {
|
|
Host::global().cache.count() as c_int
|
|
}
|
|
|
|
/// Identifier of the plugin at `_index` (two-stage buf/size getter).
|
|
pub fn oakplugin_host_plugin_id_at(_index: c_int, _buf: *mut c_char, _buf_size: c_int) -> c_int {
|
|
if _index < 0 || !is_valid_string_out(_buf, _buf_size) {
|
|
return OAKPLUGIN_E_INVALID;
|
|
}
|
|
match Host::global().cache.at(_index as usize) {
|
|
Some(p) => copy_string(&p.identifier, _buf, _buf_size),
|
|
None => OAKPLUGIN_E_NOT_FOUND,
|
|
}
|
|
}
|
|
|
|
/// Label (`OfxPropLabel`) of the named plugin (two-stage getter).
|
|
pub fn oakplugin_host_plugin_label(
|
|
_plugin_id: *const c_char,
|
|
_buf: *mut c_char,
|
|
_buf_size: c_int,
|
|
) -> c_int {
|
|
if _plugin_id.is_null() || !is_valid_string_out(_buf, _buf_size) {
|
|
return OAKPLUGIN_E_INVALID;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated id.
|
|
let id = unsafe { read_cstr(_plugin_id) };
|
|
match Host::global().cache.find(&id) {
|
|
Some(p) => {
|
|
let label = match p.descriptor.props.get("OfxPropLabel", 0) {
|
|
Some(oakplugin::property::Value::String(s)) => s.to_string_lossy().into_owned(),
|
|
_ => String::new(),
|
|
};
|
|
copy_string(&label, _buf, _buf_size)
|
|
}
|
|
None => OAKPLUGIN_E_NOT_FOUND,
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ===========================================================================
|
|
// render — oakrender domain implementation (single-lib unification)
|
|
// ===========================================================================
|
|
//
|
|
// The deleted oakrender C ABI is replaced by the crate's direct Rust
|
|
// types: the manager singleton (`oakrender::manager::RenderManager`),
|
|
// the ticket arena (`oakrender::ticket::TicketArena` — owned by the
|
|
// manager, so the facade submits through `RenderManager::global()`),
|
|
// value-typed frames (`oakrender::texture::Frame`), the color processor
|
|
// (`oakrender::color::ColorProcessor`) and the auto-cacher
|
|
// (`oakrender::autocacher::PreviewAutoCacher`).
|
|
//
|
|
// The facade's ticket handles box a [`TicketBox`] payload (arena + id +
|
|
// completion result); the engine's synchronous render loop then waits and
|
|
// reads the produced frame/samples through it.
|
|
pub mod render {
|
|
use std::ffi::{c_char, c_double, c_int, c_void};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use oakrender::error::{OAKRENDER_E_INVALID, OAKRENDER_E_NOT_FOUND, OAKRENDER_E_STATE};
|
|
use oakrender::ticket::{TicketArena, TicketId, TicketPayload, TicketResult};
|
|
|
|
use crate::handle::domain::node_ref_of;
|
|
use crate::handle::CHandle;
|
|
use crate::pods::{OakRenderVideoParams, OakVideoTicketParams};
|
|
|
|
// -------------------------------------------------------------------
|
|
// Manager / color config
|
|
// -------------------------------------------------------------------
|
|
|
|
/// `oakrender_manager_init` — direct call into
|
|
/// `oakrender::manager::RenderManager::init`.
|
|
pub fn oakrender_manager_init() -> c_int {
|
|
match oakrender::manager::RenderManager::init() {
|
|
Ok(()) => 0,
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
/// `oakrender_manager_available` — 1 when the global manager is up.
|
|
pub fn oakrender_manager_available() -> c_int {
|
|
oakrender::manager::RenderManager::global().is_some() as c_int
|
|
}
|
|
|
|
/// `oakrender_manager_shutdown` — direct call into
|
|
/// `oakrender::manager::RenderManager::shutdown`.
|
|
pub fn oakrender_manager_shutdown() {
|
|
oakrender::manager::RenderManager::shutdown();
|
|
}
|
|
|
|
/// `oakrender_manager_set_aggressive_gc` — direct call.
|
|
pub fn oakrender_manager_set_aggressive_gc(enabled: c_int) -> c_int {
|
|
match oakrender::manager::RenderManager::global() {
|
|
Some(m) => {
|
|
m.set_aggressive_gc(enabled != 0);
|
|
0
|
|
}
|
|
None => OAKRENDER_E_STATE,
|
|
}
|
|
}
|
|
|
|
/// `oakrender_color_manager_set_up_default_config` — direct call into
|
|
/// `oakrender::color::set_up_default_config`.
|
|
pub fn oakrender_color_manager_set_up_default_config() -> c_int {
|
|
match oakrender::color::set_up_default_config() {
|
|
Ok(()) => 0,
|
|
Err(_) => oakrender::error::OAKRENDER_E_FAILED,
|
|
}
|
|
}
|
|
|
|
/// `oakrender_color_manager_get_config` (two-stage string getter) —
|
|
/// direct call into `oakrender::color::config_path`.
|
|
pub fn oakrender_color_manager_get_config(buf: *mut c_char, n: c_int) -> c_int {
|
|
match oakrender::color::config_path() {
|
|
Some(path) => {
|
|
let required = (path.len() + 1) as c_int;
|
|
if !buf.is_null() && n >= required {
|
|
// SAFETY: the caller guarantees `buf` holds `n` bytes.
|
|
unsafe {
|
|
std::ptr::copy_nonoverlapping(
|
|
path.as_ptr() as *const c_char,
|
|
buf,
|
|
path.len(),
|
|
);
|
|
*buf.add(path.len()) = 0;
|
|
}
|
|
}
|
|
required
|
|
}
|
|
None => OAKRENDER_E_STATE, // no config = uninitialized color manager
|
|
}
|
|
}
|
|
|
|
/// The manager's auto-cacher view.
|
|
fn with_cacher<R>(f: impl FnOnce(&mut oakrender::autocacher::PreviewAutoCacher) -> R) -> Option<R> {
|
|
let m = oakrender::manager::RenderManager::global()?;
|
|
let mut guard = m.get_cacher();
|
|
let cacher = guard.as_mut()?;
|
|
Some(f(cacher))
|
|
}
|
|
|
|
/// `oakrender_set_cacher_multicam` — the viewer identity the
|
|
/// auto-cacher previews (null clears it).
|
|
pub fn oakrender_set_cacher_multicam(multicam_or_null: CHandle) -> c_int {
|
|
let identity = if multicam_or_null.is_null() {
|
|
None
|
|
} else {
|
|
// SAFETY: node handles box oaknode NodeRef payloads.
|
|
unsafe { node_ref_of(&multicam_or_null) }
|
|
.map(|nr| nr.id.identity())
|
|
};
|
|
match with_cacher(|c| c.set_viewer_identity(identity)) {
|
|
Some(()) => 0,
|
|
None => OAKRENDER_E_STATE,
|
|
}
|
|
}
|
|
|
|
/// `oakrender_set_display_color_processor` — the display color
|
|
/// processor identity the auto-cacher applies (null clears it).
|
|
pub fn oakrender_set_display_color_processor(p_or_null: CHandle) -> c_int {
|
|
let identity = if p_or_null.is_null() {
|
|
None
|
|
} else {
|
|
Some(p_or_null.ctx as u64)
|
|
};
|
|
match with_cacher(|c| c.set_display_color_processor(identity)) {
|
|
Some(()) => 0,
|
|
None => OAKRENDER_E_STATE,
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Ticket family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// Engine-side ticket payload: the arena plus the reserved id and the
|
|
/// completion result slot.
|
|
struct TicketBox {
|
|
arena: Arc<TicketArena>,
|
|
id: TicketId,
|
|
}
|
|
|
|
/// Convert the engine's montage POD into the arena's montage clips.
|
|
///
|
|
/// # Safety
|
|
/// `pods` must hold `count` valid [`crate::pods::MontagePod`] entries.
|
|
unsafe fn montage_from_pod(pods: *const crate::pods::MontagePod, count: c_int) -> Vec<oakrender::ticket::MontageClip> {
|
|
if pods.is_null() || count <= 0 {
|
|
return Vec::new();
|
|
}
|
|
// SAFETY: contract above.
|
|
unsafe {
|
|
let slice = std::slice::from_raw_parts(pods, count as usize);
|
|
slice
|
|
.iter()
|
|
.map(|p| oakrender::ticket::MontageClip {
|
|
filename: crate::handle::read_cstr(p.filename),
|
|
stream_index: p.stream_index,
|
|
in_time: oakcore_rs::Rational::new(p.in_num, p.in_den),
|
|
out_time: oakcore_rs::Rational::new(p.out_num, p.out_den),
|
|
media_in: oakcore_rs::Rational::new(p.media_in_num, p.media_in_den),
|
|
gain: p.gain,
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// `oakrender_ticket_render_frame` — submit a video ticket through the
|
|
/// manager's arena; the callback (when given) fires on completion with
|
|
/// the ticket handle.
|
|
pub fn oakrender_ticket_render_frame(
|
|
params: *const OakVideoTicketParams,
|
|
cb: Option<unsafe extern "C" fn(CHandle, *mut c_void)>,
|
|
userdata: *mut c_void,
|
|
) -> CHandle {
|
|
if params.is_null() {
|
|
return CHandle::null();
|
|
}
|
|
// SAFETY: the caller passes a live POD for the call duration.
|
|
let p = unsafe { &*params };
|
|
let Some(m) = oakrender::manager::RenderManager::global() else {
|
|
return CHandle::null();
|
|
};
|
|
let viewer = unsafe { node_ref_of(&p.output_node) }
|
|
.map(|nr| nr.id.identity())
|
|
.unwrap_or(0);
|
|
let force_size = if p.force_width > 0 && p.force_height > 0 {
|
|
Some((p.force_width, p.force_height))
|
|
} else {
|
|
None
|
|
};
|
|
let force_format = if p.force_format >= 0 {
|
|
Some(crate::pods::pixel_format_from_code(p.force_format))
|
|
} else {
|
|
None
|
|
};
|
|
let footage = if p.footage_filename.is_null() {
|
|
None
|
|
} else {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
Some((
|
|
unsafe { crate::handle::read_cstr(p.footage_filename) },
|
|
p.footage_stream,
|
|
))
|
|
};
|
|
let montage = unsafe { montage_from_pod(p.montage, p.montage_count) };
|
|
let video_params = oakrender::ticket::VideoTicketParams {
|
|
viewer,
|
|
time: oakcore_rs::Rational::new(p.time_num, p.time_den),
|
|
force_size,
|
|
force_format,
|
|
cache: None,
|
|
cache_dir: None,
|
|
cache_id: None,
|
|
cache_timebase: None,
|
|
footage,
|
|
montage,
|
|
};
|
|
let ticket_box = Arc::new(TicketBox {
|
|
arena: m.tickets.clone(),
|
|
id: m.tickets.next_id(),
|
|
});
|
|
let ticket_h = oakrender::handle::make_owned(ticket_box.clone());
|
|
let userdata_ptr = userdata as usize;
|
|
let completion: oakrender::ticket::Completion = Box::new(move |_result| {
|
|
if let Some(cb) = cb {
|
|
// SAFETY: the callback + userdata follow the caller's
|
|
// contract; the ticket handle outlives the completion.
|
|
unsafe { cb(ticket_h, userdata_ptr as *mut c_void) };
|
|
}
|
|
});
|
|
m.tickets
|
|
.submit_video_with_id(ticket_box.id, video_params, completion);
|
|
oakrender::handle::make_owned(ticket_box)
|
|
}
|
|
|
|
/// `oakrender_ticket_render_audio` — submit an audio ticket through
|
|
/// the manager's arena (the `oakrender::eval::render_audio_samples`
|
|
/// producer mixes the montage).
|
|
pub fn oakrender_ticket_render_audio(
|
|
output_node: CHandle,
|
|
in_num: i64,
|
|
in_den: i64,
|
|
out_num: i64,
|
|
out_den: i64,
|
|
params: *const c_void,
|
|
_mode: c_int,
|
|
cb: Option<unsafe extern "C" fn(CHandle, *mut c_void)>,
|
|
userdata: *mut c_void,
|
|
montage: *const crate::pods::MontagePod,
|
|
montage_count: c_int,
|
|
) -> CHandle {
|
|
if in_den == 0 || out_den == 0 {
|
|
return CHandle::null();
|
|
}
|
|
let Some(m) = oakrender::manager::RenderManager::global() else {
|
|
return CHandle::null();
|
|
};
|
|
let viewer = unsafe { node_ref_of(&output_node) }
|
|
.map(|nr| nr.id.identity())
|
|
.unwrap_or(0);
|
|
let sample_rate = if params.is_null() {
|
|
48000
|
|
} else {
|
|
// SAFETY: the oakcore audioparams contract.
|
|
unsafe { crate::stubs::audio::oakcore_audioparams_sample_rate(params) }
|
|
};
|
|
let channel_layout = if params.is_null() {
|
|
0x3
|
|
} else {
|
|
// SAFETY: the oakcore audioparams contract.
|
|
unsafe { crate::stubs::audio::oakcore_audioparams_channel_layout(params) }
|
|
};
|
|
let audio_params = oakrender::ticket::AudioTicketParams {
|
|
viewer,
|
|
range: oakcore_rs::TimeRange::new(
|
|
oakcore_rs::Rational::new(in_num, in_den),
|
|
oakcore_rs::Rational::new(out_num, out_den),
|
|
),
|
|
sample_rate,
|
|
channel_layout,
|
|
montage: unsafe { montage_from_pod(montage, montage_count) },
|
|
};
|
|
let ticket_box = Arc::new(TicketBox {
|
|
arena: m.tickets.clone(),
|
|
id: m.tickets.next_id(),
|
|
});
|
|
let ticket_h = oakrender::handle::make_owned(ticket_box.clone());
|
|
let userdata_ptr = userdata as usize;
|
|
let completion: oakrender::ticket::Completion = Box::new(move |_result| {
|
|
if let Some(cb) = cb {
|
|
// SAFETY: see the video-ticket callback path.
|
|
unsafe { cb(ticket_h, userdata_ptr as *mut c_void) };
|
|
}
|
|
});
|
|
m.tickets
|
|
.submit_audio_with_id(ticket_box.id, audio_params, completion);
|
|
oakrender::handle::make_owned(ticket_box)
|
|
}
|
|
|
|
/// `oakrender_audio_samples_free` — release the samples block returned
|
|
/// by `oakrender_ticket_get_samples` (NULL no-op).
|
|
pub fn oakrender_audio_samples_free(samples: *mut c_void) {
|
|
if samples.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: `samples` must be a box created for the samples block.
|
|
drop(unsafe { Box::from_raw(samples as *mut crate::pods::OakAudioSamplesOut) });
|
|
}
|
|
|
|
fn ticket_box(ticket: CHandle) -> Option<&'static TicketBox> {
|
|
// Ticket handles box `Arc<TicketBox>` (the completion closure and
|
|
// the arena slot hold their own clones, so the payload outlives
|
|
// every handle access).
|
|
// SAFETY: ticket handles box Arc<TicketBox> payloads.
|
|
let t = unsafe { oakrender::handle::get::<Arc<TicketBox>>(&ticket) }?;
|
|
// SAFETY: the Arc (and the TicketBox it owns) lives for the whole
|
|
// ticket lifetime; the handle's own reference is just one of several.
|
|
let arc: &'static Arc<TicketBox> = unsafe { std::mem::transmute(t) };
|
|
Some(&**arc)
|
|
}
|
|
|
|
/// `oakrender_ticket_wait` — block until the ticket finishes.
|
|
pub fn oakrender_ticket_wait(ticket: CHandle) -> c_int {
|
|
let Some(t) = ticket_box(ticket) else {
|
|
return OAKRENDER_E_INVALID;
|
|
};
|
|
match t.arena.wait(t.id) {
|
|
Ok(()) => 0,
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
/// `oakrender_ticket_cancel` — cancel the pending ticket.
|
|
pub fn oakrender_ticket_cancel(ticket: CHandle) -> c_int {
|
|
let Some(t) = ticket_box(ticket) else {
|
|
return OAKRENDER_E_INVALID;
|
|
};
|
|
t.arena.cancel(t.id);
|
|
0
|
|
}
|
|
|
|
/// `oakrender_ticket_get_frame` — the produced video frame (a boxed
|
|
/// `oakrender::texture::Frame`), empty when the ticket is unfinished,
|
|
/// cancelled or audio.
|
|
pub fn oakrender_ticket_get_frame(ticket: CHandle, out: *mut CHandle) -> c_int {
|
|
if out.is_null() {
|
|
return OAKRENDER_E_INVALID;
|
|
}
|
|
let Some(t) = ticket_box(ticket) else {
|
|
return OAKRENDER_E_INVALID;
|
|
};
|
|
// Read the arena slot's result directly: `TicketSlot::finish`
|
|
// stores it BEFORE flipping the slot to Finished, so this is
|
|
// race-free (the completion callback fires after the notify and may
|
|
// not have run yet when the caller reads the frame).
|
|
let result = t.arena.result(t.id);
|
|
match result {
|
|
Some(Ok(TicketPayload::Video(texture))) => match texture.to_frame() {
|
|
Ok(frame) => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = oakrender::handle::make_owned(frame) };
|
|
0
|
|
}
|
|
Err(e) => e.code(),
|
|
},
|
|
Some(Err(e)) => e.code(),
|
|
_ => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = CHandle::null() };
|
|
OAKRENDER_E_STATE
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `oakrender_ticket_get_samples` — the produced audio samples block
|
|
/// (caller frees with `oakrender_audio_samples_free`).
|
|
pub fn oakrender_ticket_get_samples(ticket: CHandle, out: *mut *mut c_void) -> c_int {
|
|
if out.is_null() {
|
|
return OAKRENDER_E_INVALID;
|
|
}
|
|
let Some(t) = ticket_box(ticket) else {
|
|
return OAKRENDER_E_INVALID;
|
|
};
|
|
// Read the arena slot's result directly: `TicketSlot::finish`
|
|
// stores it BEFORE flipping the slot to Finished, so this is
|
|
// race-free (the completion callback fires after the notify and may
|
|
// not have run yet when the caller reads the frame).
|
|
let result = t.arena.result(t.id);
|
|
match result {
|
|
Some(Ok(TicketPayload::Audio(samples))) => {
|
|
let frame_count =
|
|
(samples.samples.len() / samples.channel_count.max(1) as usize) as c_int;
|
|
let boxed = Box::new(crate::pods::OakAudioSamplesOut {
|
|
data: samples.samples.into_boxed_slice(),
|
|
frame_count,
|
|
sample_rate: samples.sample_rate,
|
|
channel_layout: samples.channel_layout,
|
|
channel_count: samples.channel_count,
|
|
});
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = Box::into_raw(boxed) as *mut c_void };
|
|
0
|
|
}
|
|
Some(Err(e)) => e.code(),
|
|
_ => {
|
|
// SAFETY: valid out pointer.
|
|
unsafe { *out = std::ptr::null_mut() };
|
|
OAKRENDER_E_STATE
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `oakrender_ticket_free` — release the ticket handle shell.
|
|
pub fn oakrender_ticket_free(ticket: *mut CHandle) {
|
|
if ticket.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *ticket };
|
|
if let Some(release) = h.release {
|
|
// SAFETY: the boxed value's release callback.
|
|
unsafe { release(h.ctx) };
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *ticket = CHandle::null() };
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Codec frame family (`oakrender::texture::Frame` boxes)
|
|
// -------------------------------------------------------------------
|
|
|
|
fn frame_ref(frame: CHandle) -> Option<&'static oakrender::texture::Frame> {
|
|
// SAFETY: frame handles box `oakrender::texture::Frame`.
|
|
let f = unsafe { oakrender::handle::get::<oakrender::texture::Frame>(&frame) }?;
|
|
// SAFETY: the box outlives the handle.
|
|
unsafe { Some(&*(f as *const _)) }
|
|
}
|
|
|
|
/// `oakrender_codec_frame_create` — a fresh, unallocated frame.
|
|
pub fn oakrender_codec_frame_create() -> CHandle {
|
|
oakrender::handle::make_owned(oakrender::texture::Frame::new())
|
|
}
|
|
|
|
/// `oakrender_codec_frame_retain` — addref'd copy.
|
|
pub fn oakrender_codec_frame_retain(frame: CHandle) -> CHandle {
|
|
if frame.is_null() {
|
|
return CHandle::null();
|
|
}
|
|
if let Some(addref) = frame.addref {
|
|
// SAFETY: the handle is live.
|
|
unsafe { addref(frame.ctx) };
|
|
}
|
|
frame
|
|
}
|
|
|
|
/// `oakrender_codec_frame_free` — release the frame shell.
|
|
pub fn oakrender_codec_frame_free(frame: *mut CHandle) {
|
|
if frame.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *frame };
|
|
if let Some(release) = h.release {
|
|
// SAFETY: the boxed value's release callback.
|
|
unsafe { release(h.ctx) };
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *frame = CHandle::null() };
|
|
}
|
|
|
|
/// `oakrender_codec_frame_width`.
|
|
pub fn oakrender_codec_frame_width(frame: CHandle) -> c_int {
|
|
frame_ref(frame)
|
|
.map(|f| f.video_params().width)
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
/// `oakrender_codec_frame_height`.
|
|
pub fn oakrender_codec_frame_height(frame: CHandle) -> c_int {
|
|
frame_ref(frame)
|
|
.map(|f| f.video_params().height)
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
/// `oakrender_codec_frame_linesize_bytes`.
|
|
pub fn oakrender_codec_frame_linesize_bytes(frame: CHandle) -> c_int {
|
|
frame_ref(frame)
|
|
.map(|f| f.linesize_bytes() as c_int)
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
/// `oakrender_codec_frame_data` — mutable pixel data view.
|
|
pub fn oakrender_codec_frame_data(frame: CHandle) -> *mut c_void {
|
|
// SAFETY: the caller holds exclusive access for the borrow.
|
|
match unsafe { oakrender::handle::get_mut::<oakrender::texture::Frame>(&frame) } {
|
|
Some(f) => f.data_mut() as *mut c_void,
|
|
None => std::ptr::null_mut(),
|
|
}
|
|
}
|
|
|
|
/// `oakrender_codec_frame_const_data` — read-only pixel data view.
|
|
pub fn oakrender_codec_frame_const_data(frame: CHandle) -> *const c_void {
|
|
frame_ref(frame)
|
|
.map(|f| f.data() as *const c_void)
|
|
.unwrap_or(std::ptr::null())
|
|
}
|
|
|
|
/// `oakrender_codec_frame_is_allocated`.
|
|
pub fn oakrender_codec_frame_is_allocated(frame: CHandle) -> c_int {
|
|
match frame_ref(frame) {
|
|
Some(f) if f.is_allocated() => 1,
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
/// `oakrender_codec_frame_get_params` — the frame's video-params POD.
|
|
pub fn oakrender_codec_frame_get_params(
|
|
frame: CHandle,
|
|
out: *mut OakRenderVideoParams,
|
|
) -> c_int {
|
|
if out.is_null() {
|
|
return OAKRENDER_E_INVALID;
|
|
}
|
|
let Some(f) = frame_ref(frame) else {
|
|
return OAKRENDER_E_INVALID;
|
|
};
|
|
let pod = f.video_params();
|
|
// SAFETY: valid out pointer.
|
|
unsafe {
|
|
(*out) = OakRenderVideoParams {
|
|
width: pod.width,
|
|
height: pod.height,
|
|
time_base_num: pod.time_base_num,
|
|
time_base_den: pod.time_base_den,
|
|
format: pod.format,
|
|
pixel_aspect_num: pod.pixel_aspect_num,
|
|
pixel_aspect_den: pod.pixel_aspect_den,
|
|
interlacing: pod.interlacing,
|
|
color_range: pod.color_range,
|
|
divider: pod.divider,
|
|
video_type: pod.video_type,
|
|
premultiplied_alpha: pod.premultiplied_alpha,
|
|
};
|
|
}
|
|
0
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Color processor family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// The C ABI direction -> the domain direction.
|
|
fn direction_from_c(direction: c_int) -> oakrender::color::Direction {
|
|
if direction == 0 {
|
|
oakrender::color::Direction::Normal
|
|
} else {
|
|
oakrender::color::Direction::Inverse
|
|
}
|
|
}
|
|
|
|
/// `oakrender_color_processor_create` — build a processor from two
|
|
/// colorspace names (OCIO failures are non-fatal: a pass-through
|
|
/// processor is returned).
|
|
pub fn oakrender_color_processor_create(
|
|
src_space: *const c_char,
|
|
dst_transform: *const c_char,
|
|
direction: c_int,
|
|
) -> CHandle {
|
|
if src_space.is_null() || dst_transform.is_null() {
|
|
return CHandle::null();
|
|
}
|
|
// SAFETY: the caller guarantees valid NUL-terminated strings.
|
|
let (src, dst) = unsafe {
|
|
(
|
|
crate::handle::read_cstr(src_space),
|
|
crate::handle::read_cstr(dst_transform),
|
|
)
|
|
};
|
|
match oakrender::color::ColorProcessor::create(&src, &dst, direction_from_c(direction)) {
|
|
Some(processor) => oakrender::handle::make_owned(processor),
|
|
None => CHandle::null(),
|
|
}
|
|
}
|
|
|
|
/// `oakrender_color_processor_free` — release the shell.
|
|
pub fn oakrender_color_processor_free(processor: *mut CHandle) {
|
|
if processor.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *processor };
|
|
if let Some(release) = h.release {
|
|
// SAFETY: the boxed value's release callback.
|
|
unsafe { release(h.ctx) };
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *processor = CHandle::null() };
|
|
}
|
|
|
|
/// `oakrender_color_processor_is_valid`.
|
|
pub fn oakrender_color_processor_is_valid(processor: CHandle) -> c_int {
|
|
// SAFETY: processor handles box `oakrender::color::ColorProcessor`.
|
|
match unsafe { oakrender::handle::get::<oakrender::color::ColorProcessor>(&processor) } {
|
|
Some(p) if p.is_valid() => 1,
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
/// `oakrender_color_processor_create_transform` — build a processor
|
|
/// from the input colorspace and the destination described by an
|
|
/// oakcommon colortransform handle (display/view/look or a plain
|
|
/// output colorspace).
|
|
pub fn oakrender_color_processor_create_transform(
|
|
_manager: CHandle,
|
|
input: *const c_char,
|
|
dest: CHandle,
|
|
direction: c_int,
|
|
) -> CHandle {
|
|
if input.is_null() || dest.is_null() {
|
|
return CHandle::null();
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string and
|
|
// a live oakcommon colortransform handle.
|
|
let (input, ct) = unsafe {
|
|
(
|
|
crate::handle::read_cstr(input),
|
|
oakcommon::handle::get::<oakcommon::colortransform::ColorTransform>(&dest),
|
|
)
|
|
};
|
|
let Some(ct) = ct else {
|
|
return CHandle::null();
|
|
};
|
|
let dst = if ct.is_display() {
|
|
match oakrender::color::display_transform(ct.display(), ct.view()) {
|
|
Some(d) => d,
|
|
None => return CHandle::null(),
|
|
}
|
|
} else {
|
|
ct.output().to_string()
|
|
};
|
|
match oakrender::color::ColorProcessor::create(&input, &dst, direction_from_c(direction)) {
|
|
Some(processor) => oakrender::handle::make_owned(processor),
|
|
None => CHandle::null(),
|
|
}
|
|
}
|
|
|
|
/// `oakrender_color_processor_convert` — one RGBA color through the
|
|
/// processor.
|
|
pub fn oakrender_color_processor_convert(
|
|
processor: CHandle,
|
|
ir: c_double,
|
|
ig: c_double,
|
|
ib: c_double,
|
|
ia: c_double,
|
|
out_r: *mut c_double,
|
|
out_g: *mut c_double,
|
|
out_b: *mut c_double,
|
|
out_a: *mut c_double,
|
|
) -> c_int {
|
|
if out_r.is_null() || out_g.is_null() || out_b.is_null() || out_a.is_null() {
|
|
return OAKRENDER_E_INVALID;
|
|
}
|
|
// SAFETY: processor handles box `oakrender::color::ColorProcessor`.
|
|
let Some(p) = (unsafe { oakrender::handle::get::<oakrender::color::ColorProcessor>(&processor) })
|
|
else {
|
|
return OAKRENDER_E_INVALID;
|
|
};
|
|
let rgba = p.convert_color([ir, ig, ib, ia]);
|
|
// SAFETY: valid out pointers.
|
|
unsafe {
|
|
*out_r = rgba[0];
|
|
*out_g = rgba[1];
|
|
*out_b = rgba[2];
|
|
*out_a = rgba[3];
|
|
}
|
|
0
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// LUT library (`oakrender::color::SUPPORTED_LUT_EXTENSIONS`)
|
|
// -------------------------------------------------------------------
|
|
|
|
/// `oakrender_lut_is_supported_extension`.
|
|
pub fn oakrender_lut_is_supported_extension(extension: *const c_char) -> c_int {
|
|
if extension.is_null() {
|
|
return 0;
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let extension = unsafe { crate::handle::read_cstr(extension) };
|
|
oakrender::color::is_supported_lut_extension(&extension) as c_int
|
|
}
|
|
|
|
/// `oakrender_lut_supported_extensions_count`.
|
|
pub fn oakrender_lut_supported_extensions_count() -> c_int {
|
|
oakrender::color::SUPPORTED_LUT_EXTENSIONS.len() as c_int
|
|
}
|
|
|
|
/// `oakrender_lut_supported_extension_at` (two-stage).
|
|
pub fn oakrender_lut_supported_extension_at(i: c_int, buf: *mut c_char, n: c_int) -> c_int {
|
|
if i < 0 {
|
|
return OAKRENDER_E_NOT_FOUND;
|
|
}
|
|
match oakrender::color::SUPPORTED_LUT_EXTENSIONS.get(i as usize) {
|
|
Some(ext) => {
|
|
let required = (ext.len() + 1) as c_int;
|
|
if !buf.is_null() && n >= required {
|
|
// SAFETY: the caller guarantees `buf` holds `n` bytes.
|
|
unsafe {
|
|
std::ptr::copy_nonoverlapping(
|
|
ext.as_ptr() as *const c_char,
|
|
buf,
|
|
ext.len(),
|
|
);
|
|
*buf.add(ext.len()) = 0;
|
|
}
|
|
}
|
|
required
|
|
}
|
|
None => OAKRENDER_E_NOT_FOUND,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// task — oaktask domain implementation (single-lib unification)
|
|
// ===========================================================================
|
|
//
|
|
// The deleted oaktask C ABI is replaced by the crate's direct Rust types:
|
|
// the process-wide [`oaktask::manager::TaskManager`], the
|
|
// [`oaktask::task::Task`] base + the concrete project/render task types
|
|
// (`project::load::ProjectLoadTask`, `project::save::ProjectSaveTask`,
|
|
// `project::import::ProjectImportTask`, `project::loadotio::LoadOTIOTask`,
|
|
// `project::saveotio::SaveOTIOTask`, `precache::PreCacheTask`,
|
|
// `export::ExportTask`).
|
|
//
|
|
// The facade's task handles box a [`TaskPayload`] holding the owned driver
|
|
// `Task`; when the task is handed to the manager (`oaktask_task_start`)
|
|
// the box is moved into the manager and the payload keeps a raw
|
|
// (manager-owned) pointer so title/error/cancel keep working. Task-result
|
|
// accessors that the crate keeps private on the behavior
|
|
// (`take_project`/`take_command`) are bridged through engine-side
|
|
// wrapper behaviors that copy the results into shared slots in the
|
|
// payload.
|
|
pub mod task {
|
|
use std::ffi::{c_char, c_int, c_void};
|
|
use std::sync::{Arc, Mutex, OnceLock};
|
|
|
|
use oaknode::project::Project;
|
|
use oakundo::undocommand::{command_from_owned, UndoCommand};
|
|
|
|
use crate::handle::domain::project_of;
|
|
use crate::handle::CHandle;
|
|
use crate::pods::EncodingParamsPOD;
|
|
|
|
/// `oaktask_event_fn` callback (`include/task/task.h`).
|
|
pub type OakTaskEventFn = unsafe extern "C" fn(event_id: c_int, value: f64, userdata: *mut c_void);
|
|
|
|
/// `oaktask_otio_import_confirm_fn` (`include/task/project.h`).
|
|
pub type OakTaskOtioImportConfirmFn = unsafe extern "C" fn(
|
|
sequence_names: *const *const c_char,
|
|
count: c_int,
|
|
userdata: *mut c_void,
|
|
) -> c_int;
|
|
|
|
/// `oaktask_image_sequence_confirm_fn` (`include/task/project.h`).
|
|
pub type OakTaskImageSequenceConfirmFn =
|
|
unsafe extern "C" fn(filename: *const c_char, userdata: *mut c_void) -> c_int;
|
|
|
|
/// Raw pointer to a manager-owned task (Send shim).
|
|
struct TaskPtr(*mut oaktask::task::Task);
|
|
|
|
// SAFETY: the pointee lives while the manager owns the task; the
|
|
// facade never dereferences it concurrently with the manager's worker.
|
|
unsafe impl Send for TaskPtr {}
|
|
|
|
/// Engine-side task payload (boxed behind every facade task handle).
|
|
struct TaskPayload {
|
|
/// The owned driver task (None once handed to the manager).
|
|
owned: Option<Box<oaktask::task::Task>>,
|
|
/// Manager-owned view (valid while the manager keeps the task).
|
|
borrowed: TaskPtr,
|
|
/// Result slots for the specialized task kinds.
|
|
kind: TaskKind,
|
|
}
|
|
|
|
enum TaskKind {
|
|
/// Plain task (export/precache/generic).
|
|
Plain,
|
|
/// Interchange load: the loaded project appears here.
|
|
Load {
|
|
result: Arc<Mutex<Option<Arc<Mutex<Project>>>>>,
|
|
},
|
|
/// Media import: the produced undo command + footage + invalid
|
|
/// file count.
|
|
Import {
|
|
command: Arc<Mutex<Option<UndoCommand>>>,
|
|
footage: Arc<Mutex<Vec<CHandle>>>,
|
|
invalid_count: Arc<Mutex<usize>>,
|
|
},
|
|
}
|
|
|
|
impl Drop for TaskPayload {
|
|
fn drop(&mut self) {
|
|
TASK_ALIVE.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
|
|
}
|
|
}
|
|
|
|
impl TaskPayload {
|
|
fn task(&self) -> &oaktask::task::Task {
|
|
match &self.owned {
|
|
Some(t) => t,
|
|
None => {
|
|
// SAFETY: the manager owns the pointee for the
|
|
// handle's lifetime (facade contract).
|
|
unsafe { &*self.borrowed.0 }
|
|
}
|
|
}
|
|
}
|
|
|
|
fn task_mut(&mut self) -> &mut oaktask::task::Task {
|
|
match &mut self.owned {
|
|
Some(t) => t,
|
|
None => {
|
|
// SAFETY: see `TaskPayload::task`.
|
|
unsafe { &mut *self.borrowed.0 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Live-task counter (the deleted `oaktask_debug_alive_count`).
|
|
static TASK_ALIVE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
|
|
|
|
fn box_task(driver: oaktask::task::Task, kind: TaskKind) -> CHandle {
|
|
let ptr = Box::into_raw(Box::new(driver));
|
|
TASK_ALIVE.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
oaktask::handle::make_owned(TaskPayload {
|
|
owned: Some(unsafe { Box::from_raw(ptr) }),
|
|
borrowed: TaskPtr(ptr),
|
|
kind,
|
|
})
|
|
}
|
|
|
|
fn task_payload(t: CHandle) -> Option<&'static TaskPayload> {
|
|
// SAFETY: task handles box TaskPayload.
|
|
let p = unsafe { oaktask::handle::get::<TaskPayload>(&t) }?;
|
|
// SAFETY: the box outlives the handle.
|
|
unsafe { Some(&*(p as *const _)) }
|
|
}
|
|
|
|
fn task_payload_mut(t: CHandle) -> Option<&'static mut TaskPayload> {
|
|
// SAFETY: task handles box TaskPayload; the caller holds
|
|
// exclusive access.
|
|
let p = unsafe { oaktask::handle::get_mut::<TaskPayload>(&t) }?;
|
|
// SAFETY: the box outlives the handle.
|
|
unsafe { Some(&mut *(p as *mut _)) }
|
|
}
|
|
|
|
/// A fresh base task with the given title.
|
|
fn base_task(title: &str) -> oaktask::task::Task {
|
|
oaktask::task::Task::new(title, None)
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Manager family
|
|
// -------------------------------------------------------------------
|
|
|
|
/// `oaktask_manager_init` — create the process-wide manager singleton.
|
|
pub fn oaktask_manager_init() -> c_int {
|
|
match oaktask::manager::TaskManager::init() {
|
|
Ok(()) => oaktask::error::OAKTASK_OK,
|
|
Err(e) => e.code(),
|
|
}
|
|
}
|
|
|
|
/// `oaktask_manager_shutdown` — destroy the singleton (idempotent).
|
|
pub fn oaktask_manager_shutdown() {
|
|
oaktask::manager::TaskManager::shutdown();
|
|
}
|
|
|
|
/// `oaktask_register_codec_submitter` — record the registration flag.
|
|
pub fn oaktask_register_codec_submitter() -> c_int {
|
|
match oaktask::manager::TaskManager::with_manager_mut(|m| {
|
|
m.set_codec_submitter_registered(true);
|
|
}) {
|
|
Some(()) => oaktask::error::OAKTASK_OK,
|
|
None => oaktask::error::OAKTASK_E_STATE,
|
|
}
|
|
}
|
|
|
|
/// `oaktask_manager_count` — live tasks.
|
|
pub fn oaktask_manager_count() -> c_int {
|
|
match oaktask::manager::TaskManager::with_manager(|m| m.get_task_count() as c_int) {
|
|
Some(n) => n,
|
|
None => oaktask::error::OAKTASK_E_STATE,
|
|
}
|
|
}
|
|
|
|
/// `oaktask_manager_at` — a borrowed-view task handle of the task at
|
|
/// `i` (a payload box whose task pointer borrows the manager-owned
|
|
/// task; the manager must outlive the handle).
|
|
pub fn oaktask_manager_at(i: c_int) -> CHandle {
|
|
if i < 0 {
|
|
return CHandle::null();
|
|
}
|
|
let ptr = match oaktask::manager::TaskManager::with_manager(|m| m.task_ptr_at(i as usize)) {
|
|
Some(Ok(p)) => p,
|
|
_ => return CHandle::null(),
|
|
};
|
|
TASK_ALIVE.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
oaktask::handle::make_owned(TaskPayload {
|
|
owned: None,
|
|
borrowed: TaskPtr(ptr),
|
|
kind: TaskKind::Plain,
|
|
})
|
|
}
|
|
|
|
/// `oaktask_manager_delete_finished` — remove finished tasks.
|
|
pub fn oaktask_manager_delete_finished() {
|
|
oaktask::manager::TaskManager::with_manager_mut(|m| m.delete_finished());
|
|
}
|
|
|
|
/// `oaktask_task_free` — release the task handle shell (drops a still
|
|
/// owned task).
|
|
pub fn oaktask_task_free(t: *mut CHandle) {
|
|
if t.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
let h = unsafe { *t };
|
|
let _ = &h;
|
|
if let Some(release) = h.release {
|
|
// SAFETY: the boxed value's release callback (drops the
|
|
// payload: an owned task is destroyed, a manager-owned one is
|
|
// left alone).
|
|
unsafe { release(h.ctx) };
|
|
}
|
|
// SAFETY: the caller passes a valid handle pointer.
|
|
unsafe { *t = CHandle::null() };
|
|
}
|
|
|
|
/// `oaktask_task_start_sync` — run on the calling thread (1/0).
|
|
pub fn oaktask_task_start_sync(t: CHandle) -> c_int {
|
|
let Some(p) = task_payload_mut(t) else {
|
|
return 0;
|
|
};
|
|
match p.task_mut().start() {
|
|
Ok(()) => 1,
|
|
Err(_) => 0,
|
|
}
|
|
}
|
|
|
|
/// `oaktask_task_start` — hand the owned task to the manager (the
|
|
/// manager spawns its worker and deletes the task when done; the
|
|
/// handle becomes a borrowed view).
|
|
pub fn oaktask_task_start(t: CHandle) -> c_int {
|
|
let Some(p) = task_payload_mut(t) else {
|
|
return oaktask::error::OAKTASK_E_INVALID;
|
|
};
|
|
let Some(task) = p.owned.take() else {
|
|
return oaktask::error::OAKTASK_E_STATE;
|
|
};
|
|
let raw = Box::into_raw(task);
|
|
p.borrowed = TaskPtr(raw);
|
|
if oaktask::manager::TaskManager::with_manager(|_| ()).is_none() {
|
|
p.owned = Some(unsafe { Box::from_raw(raw) });
|
|
return oaktask::error::OAKTASK_E_STATE;
|
|
}
|
|
// SAFETY: the box was just created above; the manager takes
|
|
// ownership of the (stable) allocation.
|
|
oaktask::manager::TaskManager::with_manager_mut(|m| {
|
|
m.add_task(unsafe { Box::from_raw(raw) } as Box<dyn oaktask::manager::TaskBox>)
|
|
});
|
|
oaktask::error::OAKTASK_OK
|
|
}
|
|
|
|
/// `oaktask_task_cancel`.
|
|
pub fn oaktask_task_cancel(t: CHandle) -> c_int {
|
|
let Some(p) = task_payload_mut(t) else {
|
|
return oaktask::error::OAKTASK_E_INVALID;
|
|
};
|
|
p.task_mut().cancel();
|
|
oaktask::error::OAKTASK_OK
|
|
}
|
|
|
|
/// `oaktask_task_wait` — block until the task finishes.
|
|
pub fn oaktask_task_wait(t: CHandle) -> c_int {
|
|
let Some(p) = task_payload(t) else {
|
|
return oaktask::error::OAKTASK_E_INVALID;
|
|
};
|
|
p.task().wait_finished();
|
|
oaktask::error::OAKTASK_OK
|
|
}
|
|
|
|
/// `oaktask_task_is_finished` — 1/0.
|
|
pub fn oaktask_task_is_finished(t: CHandle) -> c_int {
|
|
match task_payload(t) {
|
|
Some(p) if p.task().is_finished() => 1,
|
|
Some(_) => 0,
|
|
None => 0,
|
|
}
|
|
}
|
|
|
|
/// `oaktask_task_succeeded` — 1/0.
|
|
pub fn oaktask_task_succeeded(t: CHandle) -> c_int {
|
|
match task_payload(t) {
|
|
Some(p) if p.task().succeeded() => 1,
|
|
Some(_) => 0,
|
|
None => 0,
|
|
}
|
|
}
|
|
|
|
/// `oaktask_task_title` (two-stage).
|
|
pub fn oaktask_task_title(t: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
let Some(p) = task_payload(t) else {
|
|
return oaktask::error::OAKTASK_E_INVALID;
|
|
};
|
|
let title = p.task().title().to_string();
|
|
let required = (title.len() + 1) as c_int;
|
|
if !buf.is_null() && buf_size >= required {
|
|
// SAFETY: the caller guarantees `buf` holds `buf_size` bytes.
|
|
unsafe {
|
|
std::ptr::copy_nonoverlapping(title.as_ptr() as *const c_char, buf, title.len());
|
|
*buf.add(title.len()) = 0;
|
|
}
|
|
}
|
|
required
|
|
}
|
|
|
|
/// `oaktask_task_error` (two-stage).
|
|
pub fn oaktask_task_error(t: CHandle, buf: *mut c_char, buf_size: c_int) -> c_int {
|
|
let Some(p) = task_payload(t) else {
|
|
return oaktask::error::OAKTASK_E_INVALID;
|
|
};
|
|
let error = p.task().error().unwrap_or("Unknown error").to_string();
|
|
let required = (error.len() + 1) as c_int;
|
|
if !buf.is_null() && buf_size >= required {
|
|
// SAFETY: the caller guarantees `buf` holds `buf_size` bytes.
|
|
unsafe {
|
|
std::ptr::copy_nonoverlapping(error.as_ptr() as *const c_char, buf, error.len());
|
|
*buf.add(error.len()) = 0;
|
|
}
|
|
}
|
|
required
|
|
}
|
|
|
|
/// `oaktask_task_subscribe` — register the legacy `(event_id, value,
|
|
/// userdata)` listener (0 on success).
|
|
pub fn oaktask_task_subscribe(
|
|
t: CHandle,
|
|
cb: Option<OakTaskEventFn>,
|
|
userdata: *mut c_void,
|
|
) -> i64 {
|
|
let Some(p) = task_payload_mut(t) else {
|
|
return -1;
|
|
};
|
|
let Some(cb) = cb else {
|
|
return -1;
|
|
};
|
|
let state = Arc::new(oaktask::task::SubscriberState::default());
|
|
let userdata_ptr = userdata as usize;
|
|
p.task_mut().set_subscriber(state.clone());
|
|
p.task_mut().set_event_listener(Box::new(move |ev| {
|
|
// SAFETY: the callback + userdata follow the caller's
|
|
// contract; the task emits on its own thread.
|
|
unsafe {
|
|
match ev {
|
|
oaktask::task::TaskEvent::Started => cb(
|
|
0,
|
|
state
|
|
.start_ms
|
|
.load(std::sync::atomic::Ordering::SeqCst) as f64,
|
|
userdata_ptr as *mut c_void,
|
|
),
|
|
oaktask::task::TaskEvent::Progress(v) => cb(1, v, userdata_ptr as *mut c_void),
|
|
oaktask::task::TaskEvent::Finished => cb(
|
|
2,
|
|
state
|
|
.finished_value
|
|
.load(std::sync::atomic::Ordering::SeqCst) as f64,
|
|
userdata_ptr as *mut c_void,
|
|
),
|
|
}
|
|
}
|
|
}));
|
|
0
|
|
}
|
|
|
|
/// `oaktask_debug_alive_count` — live facade task payloads.
|
|
pub fn oaktask_debug_alive_count() -> c_int {
|
|
TASK_ALIVE.load(std::sync::atomic::Ordering::SeqCst)
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Task creators
|
|
// -------------------------------------------------------------------
|
|
|
|
/// Box a project payload for the result paths (reuses the node
|
|
/// module's project box).
|
|
fn box_project_result(project: Arc<Mutex<Project>>) -> CHandle {
|
|
crate::stubs::node::box_project_handle(project)
|
|
}
|
|
|
|
/// The engine-side wrapper behavior for interchange loads: delegates
|
|
/// to the real `ProjectLoadTask` and copies the loaded project into
|
|
/// the shared result slot.
|
|
struct LoadTaskBehavior {
|
|
inner: oaktask::project::load::ProjectLoadTask,
|
|
result: Arc<Mutex<Option<Arc<Mutex<Project>>>>>,
|
|
}
|
|
|
|
impl oaktask::task::TaskBehavior for LoadTaskBehavior {
|
|
fn run(&mut self, task: &mut oaktask::task::Task) -> oaktask::error::Result<()> {
|
|
self.inner.run(task)?;
|
|
if let Ok(project) = self.inner.base.take_project() {
|
|
*self.result.lock().unwrap_or_else(|e| e.into_inner()) = Some(project);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// `oaktask_create_project_load` — a task that loads an OVE project.
|
|
pub fn oaktask_create_project_load(filename: *const c_char) -> CHandle {
|
|
if filename.is_null() {
|
|
return CHandle::null();
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let filename = unsafe { crate::handle::read_cstr(filename) };
|
|
let title = format!("Loading '{filename}'");
|
|
let result = Arc::new(Mutex::new(None));
|
|
let mut driver = base_task(&title);
|
|
let inner_base = base_task(&title);
|
|
let inner = oaktask::project::load::ProjectLoadTask {
|
|
base: oaktask::project::load::ProjectLoadBaseTask::new(inner_base, filename),
|
|
};
|
|
driver.set_behavior(Box::new(LoadTaskBehavior {
|
|
inner,
|
|
result: result.clone(),
|
|
}));
|
|
box_task(driver, TaskKind::Load { result })
|
|
}
|
|
|
|
/// `oaktask_load_take_project` — take the project a successful load
|
|
/// produced (empty handle before the run).
|
|
pub fn oaktask_load_take_project(t: CHandle) -> CHandle {
|
|
let Some(p) = task_payload(t) else {
|
|
return CHandle::null();
|
|
};
|
|
match &p.kind {
|
|
TaskKind::Load { result } => {
|
|
let project = result
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
.take();
|
|
match project {
|
|
Some(project) => box_project_result(project),
|
|
None => CHandle::null(),
|
|
}
|
|
}
|
|
_ => CHandle::null(),
|
|
}
|
|
}
|
|
|
|
/// `oaktask_create_project_save` — a task that saves a project.
|
|
pub fn oaktask_create_project_save(
|
|
project: CHandle,
|
|
filename_or_null: *const c_char,
|
|
use_compression: c_int,
|
|
) -> CHandle {
|
|
// SAFETY: project handles box ProjectArc payloads.
|
|
let project_arc = match unsafe { project_of(&project) }.cloned() {
|
|
Some(p) => p,
|
|
None => return CHandle::null(),
|
|
};
|
|
let override_filename = if filename_or_null.is_null() {
|
|
None
|
|
} else {
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
Some(unsafe { crate::handle::read_cstr(filename_or_null) })
|
|
};
|
|
let mut driver = base_task("Saving project...");
|
|
let inner_base = base_task("Saving project...");
|
|
let inner = oaktask::project::save::ProjectSaveTask {
|
|
base: inner_base,
|
|
project: project_arc,
|
|
override_filename,
|
|
use_compression: use_compression != 0,
|
|
};
|
|
driver.set_behavior(Box::new(inner));
|
|
box_task(driver, TaskKind::Plain)
|
|
}
|
|
|
|
/// The engine-side wrapper behavior for imports: delegates to the real
|
|
/// `ProjectImportTask` and copies the produced command/footage/invalid
|
|
/// count into the shared slots.
|
|
struct ImportTaskBehavior {
|
|
inner: oaktask::project::import::ProjectImportTask,
|
|
command: Arc<Mutex<Option<UndoCommand>>>,
|
|
footage: Arc<Mutex<Vec<CHandle>>>,
|
|
invalid_count: Arc<Mutex<usize>>,
|
|
}
|
|
|
|
impl oaktask::task::TaskBehavior for ImportTaskBehavior {
|
|
fn run(&mut self, task: &mut oaktask::task::Task) -> oaktask::error::Result<()> {
|
|
let result = self.inner.run(task);
|
|
if let Ok(cmd) = self.inner.take_command() {
|
|
*self.command.lock().unwrap_or_else(|e| e.into_inner()) = Some(cmd);
|
|
}
|
|
{
|
|
let mut footage = self.footage.lock().unwrap_or_else(|e| e.into_inner());
|
|
let n = self.inner.get_file_count();
|
|
for i in 0..n {
|
|
if let Ok(f) = self.inner.get_imported_footage(i) {
|
|
footage.push(crate::stubs::node::box_node_handle(f.0, f.1, false));
|
|
}
|
|
}
|
|
}
|
|
*self.invalid_count.lock().unwrap_or_else(|e| e.into_inner()) =
|
|
self.inner.get_invalid_file_count();
|
|
result
|
|
}
|
|
}
|
|
|
|
/// Process-wide image-sequence confirmation callback (the deleted C
|
|
/// ABI kept it as a global; the real import task takes it at
|
|
/// creation).
|
|
static IMAGE_SEQ_CONFIRM: OnceLock<Mutex<Option<(OakTaskImageSequenceConfirmFn, usize)>>> =
|
|
OnceLock::new();
|
|
|
|
fn image_seq_confirm() -> Option<OakTaskImageSequenceConfirmFn> {
|
|
IMAGE_SEQ_CONFIRM
|
|
.get_or_init(|| Mutex::new(None))
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
.map(|(cb, _)| cb)
|
|
}
|
|
|
|
/// `oaktask_create_project_import` — a task that imports media files
|
|
/// into a folder.
|
|
pub fn oaktask_create_project_import(
|
|
folder: CHandle,
|
|
project: CHandle,
|
|
urls: *const *const c_char,
|
|
url_count: c_int,
|
|
) -> CHandle {
|
|
if url_count < 0 || (urls.is_null() && url_count > 0) {
|
|
return CHandle::null();
|
|
}
|
|
// SAFETY: node handles box oaknode NodeRef payloads.
|
|
let folder_ref = match unsafe { crate::handle::domain::node_ref_of(&folder) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
// SAFETY: project handles box ProjectArc payloads.
|
|
let project_ref = match unsafe { crate::handle::domain::project_of(&project) }.cloned() {
|
|
Some(p) => p,
|
|
None => return CHandle::null(),
|
|
};
|
|
let mut filenames = Vec::new();
|
|
for i in 0..url_count as usize {
|
|
// SAFETY: the caller guarantees `url_count` valid pointers.
|
|
let url = unsafe { *urls.add(i) };
|
|
if url.is_null() {
|
|
return CHandle::null();
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
filenames.push(unsafe { crate::handle::read_cstr(url) });
|
|
}
|
|
let file_count = filenames.len();
|
|
let confirm = image_seq_confirm().map(|cb| {
|
|
Box::new(move |filename: &str, _pattern: &str| -> bool {
|
|
// SAFETY: the callback + userdata follow the caller's
|
|
// contract.
|
|
let cname = std::ffi::CString::new(filename).unwrap_or_default();
|
|
unsafe { cb(cname.as_ptr(), std::ptr::null_mut()) != 0 }
|
|
}) as oaktask::project::import::ImageSequenceConfirmFn
|
|
});
|
|
let command = Arc::new(Mutex::new(None));
|
|
let footage = Arc::new(Mutex::new(Vec::new()));
|
|
let invalid_count = Arc::new(Mutex::new(0));
|
|
let title = format!("Importing {} file(s)", file_count);
|
|
let mut driver = base_task(&title);
|
|
let inner_base = base_task(&title);
|
|
let inner = oaktask::project::import::ProjectImportTask::new(
|
|
inner_base,
|
|
folder_ref,
|
|
project_ref,
|
|
filenames,
|
|
confirm,
|
|
file_count,
|
|
);
|
|
driver.set_behavior(Box::new(ImportTaskBehavior {
|
|
inner,
|
|
command: command.clone(),
|
|
footage: footage.clone(),
|
|
invalid_count: invalid_count.clone(),
|
|
}));
|
|
box_task(
|
|
driver,
|
|
TaskKind::Import {
|
|
command,
|
|
footage,
|
|
invalid_count,
|
|
},
|
|
)
|
|
}
|
|
|
|
/// `oaktask_import_take_command` — the undo command a successful
|
|
/// import produced (detached from the task).
|
|
pub fn oaktask_import_take_command(t: CHandle) -> CHandle {
|
|
let Some(p) = task_payload(t) else {
|
|
return CHandle::null();
|
|
};
|
|
match &p.kind {
|
|
TaskKind::Import { command, .. } => {
|
|
let cmd = command
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
.take();
|
|
match cmd {
|
|
Some(cmd) => {
|
|
// SAFETY: `command_from_owned` owns the command
|
|
// value.
|
|
unsafe { command_from_owned(cmd) }
|
|
}
|
|
None => CHandle::null(),
|
|
}
|
|
}
|
|
_ => CHandle::null(),
|
|
}
|
|
}
|
|
|
|
/// `oaktask_import_footage_count` — imported footage entries.
|
|
pub fn oaktask_import_footage_count(t: CHandle) -> c_int {
|
|
let Some(p) = task_payload(t) else {
|
|
return oaktask::error::OAKTASK_E_INVALID;
|
|
};
|
|
match &p.kind {
|
|
TaskKind::Import { footage, .. } => {
|
|
footage.lock().unwrap_or_else(|e| e.into_inner()).len() as c_int
|
|
}
|
|
_ => oaktask::error::OAKTASK_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaktask_import_footage_at` — addref'd imported footage handle.
|
|
pub fn oaktask_import_footage_at(t: CHandle, index: c_int) -> CHandle {
|
|
if index < 0 {
|
|
return CHandle::null();
|
|
}
|
|
let Some(p) = task_payload(t) else {
|
|
return CHandle::null();
|
|
};
|
|
match &p.kind {
|
|
TaskKind::Import { footage, .. } => {
|
|
let h = footage
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
.get(index as usize)
|
|
.copied();
|
|
match h {
|
|
Some(h) => {
|
|
if let Some(addref) = h.addref {
|
|
// SAFETY: the handle is live.
|
|
unsafe { addref(h.ctx) };
|
|
}
|
|
h
|
|
}
|
|
None => CHandle::null(),
|
|
}
|
|
}
|
|
_ => CHandle::null(),
|
|
}
|
|
}
|
|
|
|
/// `oaktask_import_invalid_count` — the count the real import task
|
|
/// reported.
|
|
pub fn oaktask_import_invalid_count(t: CHandle) -> c_int {
|
|
let Some(p) = task_payload(t) else {
|
|
return oaktask::error::OAKTASK_E_INVALID;
|
|
};
|
|
match &p.kind {
|
|
TaskKind::Import { invalid_count, .. } => {
|
|
*invalid_count.lock().unwrap_or_else(|e| e.into_inner()) as c_int
|
|
}
|
|
_ => oaktask::error::OAKTASK_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaktask_import_invalid_at` — STUB: the invalid-file list is
|
|
/// private inside `oaktask::project::import::ProjectImportTask` (only
|
|
/// its count is exposed), so the per-file names are unwireable
|
|
/// (documented; returns NOT_FOUND).
|
|
pub fn oaktask_import_invalid_at(
|
|
t: CHandle,
|
|
_index: c_int,
|
|
_buf: *mut c_char,
|
|
_buf_size: c_int,
|
|
) -> c_int {
|
|
let Some(p) = task_payload(t) else {
|
|
return oaktask::error::OAKTASK_E_INVALID;
|
|
};
|
|
match &p.kind {
|
|
TaskKind::Import { .. } => oaktask::error::OAKTASK_E_NOT_FOUND,
|
|
_ => oaktask::error::OAKTASK_E_INVALID,
|
|
}
|
|
}
|
|
|
|
/// `oaktask_create_project_load_otio` — an OTIO interchange load.
|
|
pub fn oaktask_create_project_load_otio(filename: *const c_char) -> CHandle {
|
|
if filename.is_null() {
|
|
return CHandle::null();
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let filename = unsafe { crate::handle::read_cstr(filename) };
|
|
let title = format!("Loading '{filename}'");
|
|
let result = Arc::new(Mutex::new(None));
|
|
let mut driver = base_task(&title);
|
|
let inner_base = base_task(&title);
|
|
let inner = oaktask::project::loadotio::LoadOTIOTask {
|
|
base: oaktask::project::load::ProjectLoadBaseTask::new(inner_base, filename),
|
|
};
|
|
driver.set_behavior(Box::new(OtioLoadTaskBehavior {
|
|
inner,
|
|
result: result.clone(),
|
|
}));
|
|
box_task(driver, TaskKind::Load { result })
|
|
}
|
|
|
|
/// The engine-side wrapper behavior for OTIO loads.
|
|
struct OtioLoadTaskBehavior {
|
|
inner: oaktask::project::loadotio::LoadOTIOTask,
|
|
result: Arc<Mutex<Option<Arc<Mutex<Project>>>>>,
|
|
}
|
|
|
|
impl oaktask::task::TaskBehavior for OtioLoadTaskBehavior {
|
|
fn run(&mut self, task: &mut oaktask::task::Task) -> oaktask::error::Result<()> {
|
|
self.inner.run(task)?;
|
|
if let Ok(project) = self.inner.base.take_project() {
|
|
*self.result.lock().unwrap_or_else(|e| e.into_inner()) = Some(project);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// `oaktask_load_otio_take_project` — take the project a successful
|
|
/// OTIO load produced.
|
|
pub fn oaktask_load_otio_take_project(t: CHandle) -> CHandle {
|
|
oaktask_load_take_project(t)
|
|
}
|
|
|
|
/// `oaktask_create_project_save_otio` — an OTIO interchange save.
|
|
pub fn oaktask_create_project_save_otio(project: CHandle, filename: *const c_char) -> CHandle {
|
|
if filename.is_null() {
|
|
return CHandle::null();
|
|
}
|
|
// SAFETY: the caller guarantees a valid NUL-terminated string.
|
|
let filename = unsafe { crate::handle::read_cstr(filename) };
|
|
// SAFETY: project handles box ProjectArc payloads.
|
|
let project_ref = match unsafe { crate::handle::domain::project_of(&project) }.cloned() {
|
|
Some(p) => p,
|
|
None => return CHandle::null(),
|
|
};
|
|
let mut driver = base_task("Saving project...");
|
|
let inner_base = base_task("Saving project...");
|
|
let inner = oaktask::project::saveotio::SaveOTIOTask {
|
|
base: inner_base,
|
|
project: project_ref,
|
|
filename,
|
|
};
|
|
driver.set_behavior(Box::new(inner));
|
|
box_task(driver, TaskKind::Plain)
|
|
}
|
|
|
|
/// `oaktask_load_otio_set_confirm_cb` — register the global OTIO
|
|
/// import-confirmation callback (real oaktask API).
|
|
pub fn oaktask_load_otio_set_confirm_cb(
|
|
cb: Option<OakTaskOtioImportConfirmFn>,
|
|
userdata: *mut c_void,
|
|
) {
|
|
let userdata_ptr = userdata as usize;
|
|
let mapped = cb.map(|cb| {
|
|
Box::new(move |names: &[String]| -> bool {
|
|
let cnames: Vec<std::ffi::CString> = names
|
|
.iter()
|
|
.map(|n| std::ffi::CString::new(n.as_str()).unwrap_or_default())
|
|
.collect();
|
|
let ptrs: Vec<*const c_char> =
|
|
cnames.iter().map(|c| c.as_ptr() as *const c_char).collect();
|
|
// SAFETY: the callback + userdata follow the caller's
|
|
// contract.
|
|
unsafe { cb(ptrs.as_ptr(), cnames.len() as c_int, userdata_ptr as *mut c_void) != 0 }
|
|
}) as oaktask::project::loadotio::ImportConfirmFn
|
|
});
|
|
oaktask::project::loadotio::set_import_confirm_callback(mapped);
|
|
}
|
|
|
|
/// `oaktask_create_precache` — a footage precache task.
|
|
pub fn oaktask_create_precache(footage: CHandle, index: c_int, sequence: CHandle) -> CHandle {
|
|
// SAFETY: node handles box oaknode NodeRef payloads.
|
|
let footage_ref = match unsafe { crate::handle::domain::node_ref_of(&footage) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
// SAFETY: node handles box oaknode NodeRef payloads.
|
|
let sequence_ref = match unsafe { crate::handle::domain::node_ref_of(&sequence) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
let inner = oaktask::precache::PreCacheTask::new(footage_ref, index, sequence_ref);
|
|
let mut driver = base_task("Precaching footage...");
|
|
driver.set_behavior(Box::new(inner));
|
|
box_task(driver, TaskKind::Plain)
|
|
}
|
|
|
|
/// `oaktask_create_export` — an export task over the real
|
|
/// `ExportTask`.
|
|
pub fn oaktask_create_export(
|
|
viewer: CHandle,
|
|
color_manager: CHandle,
|
|
params: *const EncodingParamsPOD,
|
|
) -> CHandle {
|
|
if params.is_null() {
|
|
return CHandle::null();
|
|
}
|
|
// SAFETY: node handles box oaknode NodeRef payloads.
|
|
let viewer_ref = match unsafe { crate::handle::domain::node_ref_of(&viewer) } {
|
|
Some(nr) => (nr.project.clone(), nr.id),
|
|
None => return CHandle::null(),
|
|
};
|
|
// SAFETY: the caller passes a live POD for the call duration.
|
|
let pod = unsafe { &*params };
|
|
let filename = pod
|
|
.filename
|
|
.iter()
|
|
.take_while(|&&b| b != 0)
|
|
.map(|&b| b as char)
|
|
.collect::<String>();
|
|
let encoding = oaktask::export::EncodingParams {
|
|
filename,
|
|
format: pod.format,
|
|
video_enabled: pod.video_enabled != 0,
|
|
video_codec: pod.video_codec,
|
|
video_width: pod.video_width,
|
|
video_height: pod.video_height,
|
|
video_time_base_num: pod.video_time_base_num,
|
|
video_time_base_den: pod.video_time_base_den,
|
|
video_pixel_format: pod.video_pixel_format as i32,
|
|
audio_enabled: pod.audio_enabled != 0,
|
|
audio_codec: pod.audio_codec,
|
|
audio_sample_rate: pod.audio_sample_rate,
|
|
audio_channel_layout: pod.audio_channel_layout,
|
|
subtitles_enabled: pod.subtitles_enabled != 0,
|
|
export_length_num: pod.export_length_num,
|
|
export_length_den: pod.export_length_den,
|
|
has_custom_range: pod.has_custom_range != 0,
|
|
custom_range_in_num: pod.custom_range_in_num as i32,
|
|
custom_range_in_den: pod.custom_range_in_den as i32,
|
|
custom_range_out_num: pod.custom_range_out_num as i32,
|
|
custom_range_out_den: pod.custom_range_out_den as i32,
|
|
};
|
|
let _ = color_manager; // the domain ExportTask dropped the manager slot
|
|
let inner = oaktask::export::ExportTask::new(viewer_ref, encoding);
|
|
let mut driver = base_task("Exporting...");
|
|
driver.set_behavior(Box::new(inner));
|
|
box_task(driver, TaskKind::Plain)
|
|
}
|
|
|
|
/// `oaktask_import_set_image_sequence_confirm_cb` — register the
|
|
/// process-wide image-sequence confirmation callback.
|
|
pub fn oaktask_import_set_image_sequence_confirm_cb(
|
|
cb: Option<OakTaskImageSequenceConfirmFn>,
|
|
userdata: *mut c_void,
|
|
) {
|
|
*IMAGE_SEQ_CONFIRM
|
|
.get_or_init(|| Mutex::new(None))
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner()) = cb.map(|cb| (cb, userdata as usize));
|
|
}
|
|
}
|