Files
oak-editor/crates/oak-plugin/src/suites/parametric.rs
T
Mike-Solar 244d5e860f
CI / Build & test (Windows) (push) Failing after 7s
workspace: kebab-case crates, app under crates/oak-app, shared versions
All crates take the oak-* kebab-case naming (oak-audio, oak-codec,
oak-common, oak-core, oak-ffmpeg-link, oak-node, oak-otio, oak-plugin,
oak-render, oak-storage, oak-task, oak-timeline, oak-undo), with the
lib identifiers rewritten (oakrender:: -> oak_render::, oakcore_rs:: ->
oak_core::, ...) across all 226 referencing files.

The GUI application moves from the workspace root into
crates/oak-app/: src/, build.rs (paths fixed for the new location) and
tests/ travel with it, the root Cargo.toml becomes workspace-only
([workspace] + workspace.package + profiles), and the app package
inherits the workspace version. The screenshots example becomes a
standalone crate examples/simple_player/ with its own Cargo.toml.

Every crate now inherits the single workspace version
(version.workspace = true), and the workflows' crate paths and the
build docs follow the renames.

Validated with a clean cargo check --workspace.
2026-08-22 16:58:37 +08:00

903 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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/>.
//! OfxParametricParameterSuite v1:参数化曲线参数(调色插件的
//! 曲线/LUT 类参数)。
//!
//! 语义对照 ofxParametricParam.h(属性表另见 HS ofxhParam.cpp:285-330):
//! - 曲线 = 每维一条 [`crate::param_curve::Curve`],存于参数值
//! [`crate::param::ParamValue::Parametric`](def 默认 / instance 当前
//! 值,`ParamInstance::from_def` 复制默认);
//! - 维度与定义域取参数属性 [`crate::param::P_PARAMETRIC_DIMENSION`]
//! (int,默认 1)与 [`crate::param::P_PARAMETRIC_RANGE`]
//! (double×2,默认 (0,1))——paramDefine 预置,插件 describe 期可改;
//! - 求值 = 三次 Hermite(slope 自动差分),越界 parametricPosition
//! 钳制到端点值,见 [`crate::param_curve::Curve::evaluate`];
//! - describe 期(Def)的 Set/Add/Delete 改**默认值**(HS:"If a
//! plugin wishes to set a different default value for a curve, it can
//! use the suite to set key/value pairs on the descriptor. When a new
//! instance is made, it will have these curve values as a default");
//! - 第 1 期无参数动画(宿主
//! `kOfxParamHostPropSupportsParametricAnimation = 0`):`time` 恒
//! 忽略(曲线是实例级静态的),`addAnimationKey` 同理忽略;
//! - 未配置的曲线(维度内但从未写入)按恒等默认求值/读数——与
//! "default default 是恒等查找" 一致;任何写入路径先把缺失曲线
//! 补成恒等默认再落库;
//! - 错误码:坏句柄 → BadHandle,非 parametric 参数 → BadHandle,
//! curveIndex / nth 越界 → BadIndex(头文件 GetNth/Set/Add 文档
//! 里写的 Unknown 是类型未知的泛指,本实现按任务约定统一
//! BadHandle/BadIndex)。
use std::borrow::Cow;
use std::ffi::{c_char, c_double, c_int, c_void};
use crate::descriptor::EffectDescriptor;
use crate::instance::Instance;
use crate::param::{ParamDef, ParamInstance, ParamValue};
use crate::param_curve::Curve;
use crate::property::{PropertySet, Value};
use crate::suites::{status, tag};
/// 函数表布局(与 SDK `OfxParametricParameterSuiteV1` 一致;字段序
/// 以 ofxParametricParam.h 为准)。
#[repr(C)]
pub struct ParametricParameterSuiteV1 {
/// parametricParamGetValue:求值曲线(越界位置钳制到端点值)。
pub parametric_param_get_value: unsafe extern "C" fn(
*mut c_void,
c_int,
c_double,
c_double,
*mut c_double,
) -> c_int,
/// parametricParamGetNControlPoints:控制点数。
pub parametric_param_get_n_control_points:
unsafe extern "C" fn(*mut c_void, c_int, c_double, *mut c_int) -> c_int,
/// parametricParamGetNthControlPoint:第 nth 个控制点的 key/value。
pub parametric_param_get_nth_control_point: unsafe extern "C" fn(
*mut c_void,
c_int,
c_double,
c_int,
*mut c_double,
*mut c_double,
) -> c_int,
/// parametricParamSetNthControlPoint:改第 nth 个控制点
/// (key 变化后保持有序,slope 重算)。
pub parametric_param_set_nth_control_point: unsafe extern "C" fn(
*mut c_void,
c_int,
c_double,
c_int,
c_double,
c_double,
c_int,
) -> c_int,
/// parametricParamAddControlPoint:加入/覆盖控制点(保持有序)。
pub parametric_param_add_control_point: unsafe extern "C" fn(
*mut c_void,
c_int,
c_double,
c_double,
c_double,
c_int,
) -> c_int,
/// parametricParamDeleteControlPoint:删第 nth 个控制点。
pub parametric_param_delete_control_point:
unsafe extern "C" fn(*mut c_void, c_int, c_int) -> c_int,
/// parametricParamDeleteAllControlPoints:清空某维曲线。
pub parametric_param_delete_all_control_points:
unsafe extern "C" fn(*mut c_void, c_int) -> c_int,
}
// ---- 句柄解析 -----------------------------------------------------------
/// param 句柄:describe 期是定义(值=默认值),实例期是实例。
/// Def 持 `&mut`(Set/Add/Delete 直改默认值);Instance 持 `&`
/// (经 [`ParamInstance::set_ofx`] 的内部 Mutex 改当前值)。
enum ParamRef<'a> {
Def(&'a mut ParamDef),
Instance(&'a ParamInstance),
}
/// 解析 param 句柄。空指针/标签不符 → BadHandle。
fn resolve_param(handle: *mut c_void) -> Result<ParamRef<'static>, c_int> {
if handle.is_null() {
return Err(status::ERR_BAD_HANDLE);
}
unsafe {
match tag::kind(handle) {
tag::PARAM_DEF => Ok(ParamRef::Def(&mut *(tag::strip(handle) as *mut ParamDef))),
tag::PARAM_INSTANCE => Ok(ParamRef::Instance(&*(
tag::strip(handle) as *const ParamInstance
))),
_ => Err(status::ERR_BAD_HANDLE),
}
}
}
/// 公共入口模板:panic 兜底。
#[track_caller]
fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
let caller = std::panic::Location::caller();
let code = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).map_or_else(
|_| status::FAILED,
|r| r.map_or_else(|c| c, |()| status::OK),
);
if code != status::OK && std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[ofx] parametric suite error {code} at {caller}");
}
code
}
// ---- 模型存取 -----------------------------------------------------------
/// 取参数曲线(Def=默认值,Instance=当前值)。值不是 parametric 曲线
/// → BadHandle(非 parametric 参数的 suite 语义;先于索引检查,保证
/// 非 parametric 恒为 BadHandle)。
fn curves_of(r: &ParamRef) -> Result<Vec<Curve>, c_int> {
match r {
ParamRef::Def(d) => match &d.default {
ParamValue::Parametric(c) => Ok(c.clone()),
_ => Err(status::ERR_BAD_HANDLE),
},
ParamRef::Instance(p) => match p.get() {
ParamValue::Parametric(c) => Ok(c),
_ => Err(status::ERR_BAD_HANDLE),
},
}
}
/// 写回曲线:Def → 默认值;Instance → 当前值 + instanceChanged 通知
/// (复用 param suite 的 [`crate::suites::param::notify_changed`]——
/// 曲线经 [`crate::param::notify_instance_changed`] 序列化为 JSON
/// 文本写回节点输入(undoable,同标量路径),保持宿主侧变更感知的
/// 纪律一致)。
fn write_curves(r: &mut ParamRef, curves: Vec<Curve>) {
match r {
ParamRef::Def(d) => d.default = ParamValue::Parametric(curves),
ParamRef::Instance(p) => {
let name = p.def.name.clone();
let addr = &p.props as *const _ as usize;
p.set_ofx(ParamValue::Parametric(curves));
crate::suites::param::notify_changed(addr, &name);
}
}
}
/// param 的属性集(句柄即 props 地址,偏移 0 句柄约定)。
fn props_of(handle: *mut c_void) -> &'static PropertySet {
unsafe { &*tag::strip(handle) }
}
/// 维度:`OfxParamPropParametricDimension`(int,默认 1;≤0 防御性
/// 按 1 处理——头文件要求"greater than 0")。
fn dimension_of(handle: *mut c_void) -> usize {
match props_of(handle).get(crate::param::P_PARAMETRIC_DIMENSION, 0) {
Some(Value::Int(d)) => d.max(1) as usize,
_ => 1,
}
}
/// 定义域:`OfxParamPropParametricRange`(double×2,默认 (0,1))——
/// 未配置曲线补恒等默认时的端点。
fn range_of(handle: *mut c_void) -> (f64, f64) {
let props = props_of(handle);
let lo = match props.get(crate::param::P_PARAMETRIC_RANGE, 0) {
Some(Value::Double(d)) => d,
_ => 0.0,
};
let hi = match props.get(crate::param::P_PARAMETRIC_RANGE, 1) {
Some(Value::Double(d)) => d,
_ => 1.0,
};
(lo, hi)
}
/// curveIndex 越界(相对 dimension 属性)→ BadIndex。
fn check_curve_index(handle: *mut c_void, curve_index: c_int) -> Result<usize, c_int> {
let ci = usize::try_from(curve_index).map_err(|_| status::ERR_BAD_INDEX)?;
if ci >= dimension_of(handle) {
return Err(status::ERR_BAD_INDEX);
}
Ok(ci)
}
/// 取第 `ci` 条曲线;缺失(维内但从未写入)→ 恒等默认(只读路径
/// 不落库:未配置曲线 = 恒等查找)。
fn curve_at<'a>(curves: &'a [Curve], ci: usize, range: (f64, f64)) -> Cow<'a, Curve> {
match curves.get(ci) {
Some(c) => Cow::Borrowed(c),
None => Cow::Owned(Curve::identity(range.0, range.1)),
}
}
/// 确保第 `ci` 条曲线存在(写入路径:缺失时补恒等默认再落库)。
fn ensure_curve(curves: &mut Vec<Curve>, ci: usize, range: (f64, f64)) -> &mut Curve {
while curves.len() <= ci {
curves.push(Curve::identity(range.0, range.1));
}
&mut curves[ci]
}
// ---- suite 函数 ---------------------------------------------------------
/// parametricParamGetValue:求值曲线。`time` 忽略(无参数动画);
/// 越界 parametricPosition 钳制到端点值([`Curve::evaluate`])。
unsafe extern "C" fn parametric_param_get_value(
param: *mut c_void,
curve_index: c_int,
_time: c_double,
parametric_position: c_double,
out: *mut c_double,
) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_VALUE);
}
unsafe { *out = 0.0 };
let r = resolve_param(param)?;
let curves = curves_of(&r)?;
let ci = check_curve_index(param, curve_index)?;
let range = range_of(param);
let v = curve_at(&curves, ci, range).evaluate(parametric_position);
unsafe { *out = v };
Ok(())
})
}
/// parametricParamGetNControlPoints:控制点数(未配置曲线 = 恒等默认
/// 的 2 点)。
unsafe extern "C" fn parametric_param_get_n_control_points(
param: *mut c_void,
curve_index: c_int,
_time: c_double,
out: *mut c_int,
) -> c_int {
caught(|| {
if out.is_null() {
return Err(status::ERR_VALUE);
}
unsafe { *out = 0 };
let r = resolve_param(param)?;
let curves = curves_of(&r)?;
let ci = check_curve_index(param, curve_index)?;
let n = curve_at(&curves, ci, range_of(param)).len();
unsafe { *out = n as c_int };
Ok(())
})
}
/// parametricParamGetNthControlPoint:第 nth 个控制点的 key/value
/// (nth 越界 → BadIndex;未配置曲线按恒等默认读数)。
unsafe extern "C" fn parametric_param_get_nth_control_point(
param: *mut c_void,
curve_index: c_int,
_time: c_double,
nth: c_int,
key: *mut c_double,
value: *mut c_double,
) -> c_int {
caught(|| {
if key.is_null() || value.is_null() {
return Err(status::ERR_VALUE);
}
unsafe {
*key = 0.0;
*value = 0.0;
};
let r = resolve_param(param)?;
let curves = curves_of(&r)?;
let ci = check_curve_index(param, curve_index)?;
let nth = usize::try_from(nth).map_err(|_| status::ERR_BAD_INDEX)?;
let c = curve_at(&curves, ci, range_of(param));
let p = c.nth(nth).ok_or(status::ERR_BAD_INDEX)?;
unsafe {
*key = p.key;
*value = p.value;
};
Ok(())
})
}
/// parametricParamSetNthControlPoint:改第 nth 个控制点为 (key,
/// value)。key 变化可能破坏有序性(头文件明确提醒)——模型侧先移除
/// 再按 key 插入、撞 key 按覆盖处理、slope 重算([`Curve::set_nth`])。
/// `addAnimationKey` 忽略(宿主不支持参数动画)。
unsafe extern "C" fn parametric_param_set_nth_control_point(
param: *mut c_void,
curve_index: c_int,
_time: c_double,
nth: c_int,
key: c_double,
value: c_double,
_add_animation_key: c_int,
) -> c_int {
caught(|| {
let mut r = resolve_param(param)?;
let mut curves = curves_of(&r)?;
let ci = check_curve_index(param, curve_index)?;
let nth = usize::try_from(nth).map_err(|_| status::ERR_BAD_INDEX)?;
ensure_curve(&mut curves, ci, range_of(param))
.set_nth(nth, key, value)
.map_err(|()| status::ERR_BAD_INDEX)?;
write_curves(&mut r, curves);
Ok(())
})
}
/// parametricParamAddControlPoint:加入控制点;同 key 已存在 → 覆盖
/// (头文件 "If a key exists sufficiently close to 'key', then it will
/// be set to the indicated control point";本实现取精确同 key)。
/// `addAnimationKey` 忽略(无参数动画)。
unsafe extern "C" fn parametric_param_add_control_point(
param: *mut c_void,
curve_index: c_int,
_time: c_double,
key: c_double,
value: c_double,
_add_animation_key: c_int,
) -> c_int {
caught(|| {
let mut r = resolve_param(param)?;
let mut curves = curves_of(&r)?;
let ci = check_curve_index(param, curve_index)?;
ensure_curve(&mut curves, ci, range_of(param)).upsert(key, value);
write_curves(&mut r, curves);
Ok(())
})
}
/// parametricParamDeleteControlPoint:删第 nth 个控制点(越界 →
/// BadIndex)。
unsafe extern "C" fn parametric_param_delete_control_point(
param: *mut c_void,
curve_index: c_int,
nth: c_int,
) -> c_int {
caught(|| {
let mut r = resolve_param(param)?;
let mut curves = curves_of(&r)?;
let ci = check_curve_index(param, curve_index)?;
let nth = usize::try_from(nth).map_err(|_| status::ERR_BAD_INDEX)?;
ensure_curve(&mut curves, ci, range_of(param))
.delete_nth(nth)
.map_err(|()| status::ERR_BAD_INDEX)?;
write_curves(&mut r, curves);
Ok(())
})
}
/// parametricParamDeleteAllControlPoints:清空某维曲线(清空后求值
/// 退化为恒等,[`Curve::evaluate`] 的空曲线分支)。
unsafe extern "C" fn parametric_param_delete_all_control_points(
param: *mut c_void,
curve_index: c_int,
) -> c_int {
caught(|| {
let mut r = resolve_param(param)?;
let mut curves = curves_of(&r)?;
let ci = check_curve_index(param, curve_index)?;
ensure_curve(&mut curves, ci, range_of(param)).clear();
write_curves(&mut r, curves);
Ok(())
})
}
/// 静态函数表实例。
pub fn suite_v1() -> &'static ParametricParameterSuiteV1 {
static SUITE: std::sync::OnceLock<ParametricParameterSuiteV1> = std::sync::OnceLock::new();
SUITE.get_or_init(|| ParametricParameterSuiteV1 {
parametric_param_get_value: parametric_param_get_value,
parametric_param_get_n_control_points: parametric_param_get_n_control_points,
parametric_param_get_nth_control_point: parametric_param_get_nth_control_point,
parametric_param_set_nth_control_point: parametric_param_set_nth_control_point,
parametric_param_add_control_point: parametric_param_add_control_point,
parametric_param_delete_control_point: parametric_param_delete_control_point,
parametric_param_delete_all_control_points: parametric_param_delete_all_control_points,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
use std::sync::Arc;
use crate::host::Plugin;
use crate::param::{ParamInstance, ParamSetInstance};
use crate::property::PropertySet;
fn cs(s: &str) -> CString {
CString::new(s).unwrap()
}
fn descriptor_handle(d: &EffectDescriptor) -> *mut c_void {
tag::make(&d.props as *const PropertySet, tag::DESCRIPTOR)
}
/// 假插件(host::Plugin 的构造只为拿 Arc 喂给 Instance;describe
/// 之外的字段不被本测试触碰)。
fn dummy_plugin(descriptor: EffectDescriptor) -> Arc<Plugin> {
unsafe extern "C" fn dummy_entry(
_: *const c_char,
_: *const c_void,
_: *mut c_void,
_: *mut c_void,
) -> c_int {
status::OK
}
Arc::new(Plugin {
identifier: "test.plugin".into(),
version: (1, 0),
bundle_path: std::path::PathBuf::new(),
contexts: vec![],
descriptor,
lib: std::ptr::null_mut(),
entry: dummy_entry,
ofx_plugin: std::ptr::null_mut(),
unloaded: std::sync::atomic::AtomicBool::new(false),
})
}
fn instance_handle(i: &Instance) -> *mut c_void {
tag::make(&i.props as *const PropertySet, tag::INSTANCE)
}
/// describe 期建一个 parametric 参数 "curve"(默认维度 1),返回
/// 描述符与其 param 句柄。
fn define_parametric() -> (EffectDescriptor, *mut c_void) {
let desc = EffectDescriptor::new();
let s = crate::suites::param::suite_v1();
let dhandle = descriptor_handle(&desc);
let t = cs(crate::param::TYPE_PARAMETRIC);
let n = cs("curve");
let mut ph: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.param_define)(dhandle, t.as_ptr(), n.as_ptr(), &mut ph), 0);
}
assert_eq!(tag::kind(ph), tag::PARAM_DEF);
(desc, ph)
}
/// 把 describe 产物实例化(default → 实例值复制),返回
/// (实例 Arc, 实例 handle, 实例 param 句柄)。
fn instantiate(desc: EffectDescriptor) -> (Arc<Instance>, *mut c_void, *mut c_void) {
let params = ParamSetInstance {
params: desc
.params
.iter()
.map(|d| Box::new(ParamInstance::from_def((**d).clone())))
.collect(),
};
let plugin = dummy_plugin(desc);
let inst = Arc::new(Instance {
props: PropertySet::new(),
plugin,
context: "OfxImageEffectContextFilter".into(),
params,
clips: vec![],
node_identity: std::sync::atomic::AtomicUsize::new(0),
destroyed: std::sync::atomic::AtomicBool::new(false),
sequence_range: std::sync::Mutex::new(None),
progress_cb: std::sync::Mutex::new(None),
cancel: std::sync::atomic::AtomicBool::new(false),
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
render_lock: std::sync::Mutex::new(()),
interact: std::sync::Mutex::new(None),
});
let ih = instance_handle(&inst);
let s = crate::suites::param::suite_v1();
let n = cs("curve");
let mut ph: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((s.param_get_handle)(ih, n.as_ptr(), &mut ph, std::ptr::null_mut()), 0);
}
assert_eq!(tag::kind(ph), tag::PARAM_INSTANCE);
(inst, ih, ph)
}
/// describe 定义 + 实例化一步到位(默认维度 1)。
fn make_instance() -> (Arc<Instance>, *mut c_void, *mut c_void) {
let (desc, _dph) = define_parametric();
instantiate(desc)
}
fn ps() -> &'static ParametricParameterSuiteV1 {
suite_v1()
}
/// describe 期默认值:2 个恒等控制点 (0,0)/(1,1),求值 = 恒等。
#[test]
fn describe_defaults_are_identity() {
let (_desc, ph) = define_parametric();
let s = ps();
let mut n = -1;
unsafe {
assert_eq!((s.parametric_param_get_n_control_points)(ph, 0, 0.0, &mut n), 0);
}
assert_eq!(n, 2);
let (mut k, mut v) = (-1.0, -1.0);
unsafe {
assert_eq!((s.parametric_param_get_nth_control_point)(ph, 0, 0.0, 0, &mut k, &mut v), 0);
}
assert_eq!((k, v), (0.0, 0.0));
unsafe {
assert_eq!((s.parametric_param_get_nth_control_point)(ph, 0, 0.0, 1, &mut k, &mut v), 0);
}
assert_eq!((k, v), (1.0, 1.0));
// 恒等求值(越界钳制端点值)。
let mut out = 0.0;
unsafe {
assert_eq!((s.parametric_param_get_value)(ph, 0, 0.0, 0.5, &mut out), 0);
}
assert_eq!(out, 0.5);
unsafe {
assert_eq!((s.parametric_param_get_value)(ph, 0, 0.0, -3.0, &mut out), 0);
assert_eq!(out, 0.0);
assert_eq!((s.parametric_param_get_value)(ph, 0, 0.0, 7.0, &mut out), 0);
assert_eq!(out, 1.0);
}
}
/// describe 期 Set/Add/Delete 改**默认值**,实例化时复制到实例。
#[test]
fn descriptor_edits_become_instance_defaults() {
let (desc, ph) = define_parametric();
let s = ps();
// describe 期改默认:加两个点,改第 1 个点的值。
unsafe {
assert_eq!((s.parametric_param_add_control_point)(ph, 0, 0.0, 0.25, 0.5, 0), 0);
assert_eq!((s.parametric_param_add_control_point)(ph, 0, 0.0, 0.75, 0.9, 0), 0);
assert_eq!((s.parametric_param_set_nth_control_point)(ph, 0, 0.0, 1, 0.25, 0.6, 0), 0);
}
// 默认值本身生效。
let mut out = 0.0;
unsafe {
assert_eq!((s.parametric_param_get_value)(ph, 0, 0.0, 0.25, &mut out), 0);
}
assert_eq!(out, 0.6);
// 实例化:默认曲线随 from_def 复制。
let (_inst, _ih, iph) = instantiate(desc);
unsafe {
assert_eq!((s.parametric_param_get_value)(iph, 0, 0.0, 0.25, &mut out), 0);
}
assert_eq!(out, 0.6);
let mut n = 0;
unsafe {
assert_eq!((s.parametric_param_get_n_control_points)(iph, 0, 0.0, &mut n), 0);
}
assert_eq!(n, 4); // (0,0) (0.25,0.6) (0.75,0.9) (1,1)
}
/// 实例期全链路:Add → Set → Delete → DeleteAll,求值随模型变化。
#[test]
fn instance_add_set_delete_eval_chain() {
let (_inst, _ih, ph) = make_instance();
let s = ps();
// Add 中点 (0.3, 0.7):插值性质 + 有序。
unsafe {
assert_eq!((s.parametric_param_add_control_point)(ph, 0, 0.0, 0.3, 0.7, 0), 0);
}
let mut out = 0.0;
unsafe {
assert_eq!((s.parametric_param_get_value)(ph, 0, 0.0, 0.3, &mut out), 0);
}
assert_eq!(out, 0.7);
let mut n = 0;
unsafe {
assert_eq!((s.parametric_param_get_n_control_points)(ph, 0, 0.0, &mut n), 0);
}
assert_eq!(n, 3);
// SetNth 改 (0.3,0.7) → (0.4,0.6)。
unsafe {
assert_eq!((s.parametric_param_set_nth_control_point)(ph, 0, 0.0, 1, 0.4, 0.6, 0), 0);
}
let (mut k, mut v) = (-1.0, -1.0);
unsafe {
assert_eq!((s.parametric_param_get_nth_control_point)(ph, 0, 0.0, 1, &mut k, &mut v), 0);
}
assert_eq!((k, v), (0.4, 0.6));
unsafe {
assert_eq!((s.parametric_param_get_value)(ph, 0, 0.0, 0.4, &mut out), 0);
}
assert_eq!(out, 0.6);
// Delete 第 1 点 → 回到双点。
unsafe {
assert_eq!((s.parametric_param_delete_control_point)(ph, 0, 1), 0);
}
unsafe {
assert_eq!((s.parametric_param_get_n_control_points)(ph, 0, 0.0, &mut n), 0);
}
assert_eq!(n, 2);
// DeleteAll → 0 点,求值退化为恒等。
unsafe {
assert_eq!((s.parametric_param_delete_all_control_points)(ph, 0), 0);
assert_eq!((s.parametric_param_get_n_control_points)(ph, 0, 0.0, &mut n), 0);
assert_eq!((s.parametric_param_get_value)(ph, 0, 0.0, 0.5, &mut out), 0);
}
assert_eq!(n, 0);
assert_eq!(out, 0.5);
}
/// SetNth 改 key 破坏有序性 → 自动重排(头文件提醒的语义)。
#[test]
fn set_nth_reorders_on_key_change() {
let (_inst, _ih, ph) = make_instance();
let s = ps();
unsafe {
assert_eq!((s.parametric_param_add_control_point)(ph, 0, 0.0, 0.5, 0.5, 0), 0);
// 把第 0 点 (0,0) 挪到 0.75 → 序列变 (0.5) (0.75) (1)。
assert_eq!((s.parametric_param_set_nth_control_point)(ph, 0, 0.0, 0, 0.75, 0.75, 0), 0);
}
let (mut k, mut v) = (-1.0, -1.0);
for (i, (ek, ev)) in [(0.5, 0.5), (0.75, 0.75), (1.0, 1.0)].iter().enumerate() {
unsafe {
assert_eq!((s.parametric_param_get_nth_control_point)(ph, 0, 0.0, i as c_int, &mut k, &mut v), 0);
}
assert_eq!((k, v), (*ek, *ev), "nth {i}");
}
}
/// 同 key Add → 覆盖(不增点)。
#[test]
fn add_overwrites_same_key() {
let (_inst, _ih, ph) = make_instance();
let s = ps();
unsafe {
assert_eq!((s.parametric_param_add_control_point)(ph, 0, 0.0, 0.5, 0.4, 0), 0);
assert_eq!((s.parametric_param_add_control_point)(ph, 0, 0.0, 0.5, 0.9, 0), 0);
}
let mut n = 0;
unsafe {
assert_eq!((s.parametric_param_get_n_control_points)(ph, 0, 0.0, &mut n), 0);
}
assert_eq!(n, 3); // (0,0) (0.5,0.9) (1,1)
let (mut k, mut v) = (-1.0, -1.0);
unsafe {
assert_eq!((s.parametric_param_get_nth_control_point)(ph, 0, 0.0, 1, &mut k, &mut v), 0);
}
assert_eq!((k, v), (0.5, 0.9));
}
/// 错误码:坏句柄 / 非 parametric / curveIndex 越界 / nth 越界 /
/// 空 out。
#[test]
fn error_codes() {
let s = ps();
let mut out = 0.0;
// 空句柄 → BadHandle。
unsafe {
assert_eq!(
(s.parametric_param_get_value)(std::ptr::null_mut(), 0, 0.0, 0.5, &mut out),
status::ERR_BAD_HANDLE
);
}
// 空 out → ErrValue。
let (_inst, _ih, ph) = make_instance();
unsafe {
assert_eq!(
(s.parametric_param_get_value)(ph, 0, 0.0, 0.5, std::ptr::null_mut()),
status::ERR_VALUE
);
}
// curveIndex 越界(维度 1)→ BadIndex。
unsafe {
assert_eq!(
(s.parametric_param_get_value)(ph, 1, 0.0, 0.5, &mut out),
status::ERR_BAD_INDEX
);
}
// nth 越界(2 点曲线)→ BadIndex。
let mut k = 0.0;
unsafe {
assert_eq!(
(s.parametric_param_get_nth_control_point)(ph, 0, 0.0, 5, &mut k, &mut out),
status::ERR_BAD_INDEX
);
}
// 非 parametric 参数 → BadHandle(Integer 参数的句柄)。
let desc = EffectDescriptor::new();
let psuite = crate::suites::param::suite_v1();
let dhandle = descriptor_handle(&desc);
let t = cs("OfxParamTypeInteger");
let n = cs("gain");
let mut gph: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((psuite.param_define)(dhandle, t.as_ptr(), n.as_ptr(), &mut gph), 0);
// parametric 也定义一个,便于实例化后拿两个 handle。
let pt = cs(crate::param::TYPE_PARAMETRIC);
let pn = cs("curve");
let mut pph: *mut c_void = std::ptr::null_mut();
assert_eq!((psuite.param_define)(dhandle, pt.as_ptr(), pn.as_ptr(), &mut pph), 0);
let params = ParamSetInstance {
params: desc
.params
.iter()
.map(|d| Box::new(ParamInstance::from_def((**d).clone())))
.collect(),
};
let plugin = dummy_plugin(desc);
let inst = Arc::new(Instance {
props: PropertySet::new(),
plugin,
context: "OfxImageEffectContextFilter".into(),
params,
clips: vec![],
node_identity: std::sync::atomic::AtomicUsize::new(0),
destroyed: std::sync::atomic::AtomicBool::new(false),
sequence_range: std::sync::Mutex::new(None),
progress_cb: std::sync::Mutex::new(None),
cancel: std::sync::atomic::AtomicBool::new(false),
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
render_lock: std::sync::Mutex::new(()),
interact: std::sync::Mutex::new(None),
});
let ih = instance_handle(&inst);
let mut g: *mut c_void = std::ptr::null_mut();
let gn = cs("gain");
assert_eq!((psuite.param_get_handle)(ih, gn.as_ptr(), &mut g, std::ptr::null_mut()), 0);
assert_eq!(
(s.parametric_param_get_value)(g, 0, 0.0, 0.5, &mut out),
status::ERR_BAD_HANDLE
);
assert_eq!(
(s.parametric_param_add_control_point)(g, 0, 0.0, 0.5, 0.5, 0),
status::ERR_BAD_HANDLE
);
}
}
/// 维度属性驱动:插件设 dimension=2 后第二维可用、第三维 BadIndex;
/// 未配置曲线按恒等默认读数。
#[test]
fn dimension_prop_drives_curves() {
let (desc, ph) = define_parametric();
// 插件经属性 suite 把维度改成 2。
let psuite = crate::suites::property::suite_v1();
let dim = cs(crate::param::P_PARAMETRIC_DIMENSION);
unsafe {
assert_eq!((psuite.set_int)(ph, dim.as_ptr(), 0, 2), 0);
}
let s = ps();
// 第二维:Add 后求值可用。
unsafe {
assert_eq!((s.parametric_param_add_control_point)(ph, 1, 0.0, 0.25, 0.5, 0), 0);
}
let mut out = 0.0;
unsafe {
assert_eq!((s.parametric_param_get_value)(ph, 1, 0.0, 0.25, &mut out), 0);
}
assert_eq!(out, 0.5);
// 第一维未配置 → 恒等默认。
unsafe {
assert_eq!((s.parametric_param_get_value)(ph, 0, 0.0, 0.25, &mut out), 0);
}
assert_eq!(out, 0.25);
let mut n = 0;
unsafe {
assert_eq!((s.parametric_param_get_n_control_points)(ph, 0, 0.0, &mut n), 0);
}
assert_eq!(n, 2);
// 第三维 → BadIndex。
unsafe {
assert_eq!(
(s.parametric_param_get_value)(ph, 2, 0.0, 0.5, &mut out),
status::ERR_BAD_INDEX
);
assert_eq!(
(s.parametric_param_add_control_point)(ph, 2, 0.0, 0.5, 0.5, 0),
status::ERR_BAD_INDEX
);
}
// 维度属性随 from_def 复制到实例(实例句柄上同样生效)。
let (_inst, _ih, iph) = instantiate(desc);
let mut n = -1;
unsafe {
assert_eq!((s.parametric_param_get_n_control_points)(iph, 1, 0.0, &mut n), 0);
}
assert_eq!(n, 3); // (0,0) (0.25,0.5) (1,1)
}
/// range 属性参与未配置曲线的恒等默认(端点 = range 值)。
#[test]
fn range_prop_shapes_implicit_identity() {
let (_desc, ph) = define_parametric();
let psuite = crate::suites::property::suite_v1();
let dim = cs(crate::param::P_PARAMETRIC_DIMENSION);
let range = cs(crate::param::P_PARAMETRIC_RANGE);
unsafe {
assert_eq!((psuite.set_int)(ph, dim.as_ptr(), 0, 2), 0);
// range = (0, 255)。
assert_eq!((psuite.set_double)(ph, range.as_ptr(), 0, 0.0), 0);
assert_eq!((psuite.set_double)(ph, range.as_ptr(), 1, 255.0), 0);
}
let s = ps();
// 未配置的第二维:恒等默认端点 = range。
let (mut k, mut v) = (-1.0, -1.0);
unsafe {
assert_eq!((s.parametric_param_get_nth_control_point)(ph, 1, 0.0, 1, &mut k, &mut v), 0);
}
assert_eq!((k, v), (255.0, 255.0));
}
/// UI 属性(UIColour double×3N / InteractBackground 指针)经通用
/// 属性 suite set/get,并随 from_def 复制到实例。
#[test]
fn ui_props_set_get_and_copy() {
let (desc, ph) = define_parametric();
let psuite = crate::suites::property::suite_v1();
// UIColour:3 个 double(维度 1 → 1 个 RGB 三元组)。
let colour = cs(crate::param::P_PARAMETRIC_UI_COLOUR);
let rgb: [f64; 3] = [1.0, 0.0, 0.5];
unsafe {
assert_eq!((psuite.set_double_n)(ph, colour.as_ptr(), 3, rgb.as_ptr()), 0);
}
// InteractBackground:任意指针。
let bg = cs(crate::param::P_PARAMETRIC_INTERACT_BG);
let sentinel = 0x1234usize as *mut c_void;
unsafe {
assert_eq!((psuite.set_pointer)(ph, bg.as_ptr(), 0, sentinel), 0);
}
// 读回。
let mut got = [0.0; 3];
unsafe {
assert_eq!((psuite.get_double_n)(ph, colour.as_ptr(), 3, got.as_mut_ptr()), 0);
}
assert_eq!(got, rgb);
let mut gotp: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((psuite.get_pointer)(ph, bg.as_ptr(), 0, &mut gotp), 0);
}
assert_eq!(gotp, sentinel);
// 实例 props 深拷贝。
let (_inst, _ih, iph) = instantiate(desc);
let mut got2 = [0.0; 3];
unsafe {
assert_eq!((psuite.get_double_n)(iph, colour.as_ptr(), 3, got2.as_mut_ptr()), 0);
}
assert_eq!(got2, rgb);
let mut gotp2: *mut c_void = std::ptr::null_mut();
unsafe {
assert_eq!((psuite.get_pointer)(iph, bg.as_ptr(), 0, &mut gotp2), 0);
}
assert_eq!(gotp2, sentinel);
}
/// 实例编辑走 instance-changed 通知路径(未绑定节点 → no-op,
/// 不 panic 即通过;登记路径的节流语义由 param suite 测试覆盖)。
#[test]
fn instance_edits_go_through_notify_path() {
let (_inst, _ih, ph) = make_instance();
let s = ps();
unsafe {
assert_eq!((s.parametric_param_add_control_point)(ph, 0, 0.0, 0.2, 0.8, 0), 0);
assert_eq!((s.parametric_param_set_nth_control_point)(ph, 0, 0.0, 1, 0.2, 0.9, 0), 0);
assert_eq!((s.parametric_param_delete_control_point)(ph, 0, 1), 0);
assert_eq!((s.parametric_param_delete_all_control_points)(ph, 0), 0);
}
let mut out = 0.0;
unsafe {
assert_eq!((s.parametric_param_get_value)(ph, 0, 0.0, 0.2, &mut out), 0);
}
assert_eq!(out, 0.2);
}
}