feat(plugin): OfxParametricParameterSuite v1

Parametric (curve/LUT) parameters: ParamValue::Parametric holds one
ordered control-point curve per dimension (identity default over the
declared range), evaluated as piecewise cubic Hermite with auto
(centered-difference) slopes; the full suite — evaluate / count / get /
set / add / delete / delete-all — with the spec's error codes, descriptor
defaults copied to instances, and instanceChanged notifications on
edits. paramDefine accepts OfxParamTypeParametric; the dimension/range
and UI-colour properties round-trip. 148/148 real plugins discovered,
135 registered (one more than before: the parametric-suite consumer).
This commit is contained in:
2026-08-21 03:03:50 +08:00
parent 09cfd9f09e
commit c9d557e127
6 changed files with 1400 additions and 17 deletions
+1
View File
@@ -66,6 +66,7 @@ pub mod instance;
pub mod node;
pub mod node_factory;
pub mod param;
pub mod param_curve;
pub mod progress;
pub mod property;
pub mod render;
+83 -3
View File
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! 参数体系:12 种参数实例 + param ↔ oaknode 桥。
//! 参数体系:13 种参数实例 + param ↔ oaknode 桥。
//!
//! 对应 C++ 的 `ParamInstance`/`OliveParamInstance`。桥的语义
//! (M9 已定):节点输入值变化 → 写回 OFX 参数;OFX 参数被插件
@@ -69,7 +69,9 @@ pub const TYPE_PUSHBUTTON: &str = "OfxParamTypePushButton";
pub const TYPE_GROUP: &str = "OfxParamTypeGroup";
/// kOfxParamTypePage。
pub const TYPE_PAGE: &str = "OfxParamTypePage";
/// kOfxParamTypeParametric第 1 期不支持,paramDefine 拒绝)。
/// kOfxParamTypeParametric值 = 曲线列表,经
/// OfxParametricParameterSuite 读写,见
/// [`crate::suites::parametric`] 与 [`crate::param_curve`])。
pub const TYPE_PARAMETRIC: &str = "OfxParamTypeParametric";
// ---- 属性名(ofxParam.h / ofxCore.h----
@@ -150,6 +152,14 @@ pub(crate) const PAGE_SKIP_ROW: &str = "OfxParamPageSkipRow";
pub(crate) const PAGE_SKIP_COLUMN: &str = "OfxParamPageSkipColumn";
/// kOfxParamPropCustomInterpCallbackV1。
pub(crate) const P_CUSTOM_INTERP: &str = "OfxParamPropCustomCallbackV1";
/// kOfxParamPropParametricDimensionint×1,默认 1)。
pub(crate) const P_PARAMETRIC_DIMENSION: &str = "OfxParamPropParametricDimension";
/// kOfxParamPropParametricRangedouble×2,默认 (0,1))。
pub(crate) const P_PARAMETRIC_RANGE: &str = "OfxParamPropParametricRange";
/// kOfxParamPropParametricUIColourdouble×3N,默认未设)。
pub(crate) const P_PARAMETRIC_UI_COLOUR: &str = "OfxParamPropParametricUIColour";
/// kOfxParamPropParametricInteractBackgroundpointer×1,默认 NULL)。
pub(crate) const P_PARAMETRIC_INTERACT_BG: &str = "OfxParamPropParametricInteractBackground";
/// kOfxParamPropPageChild。
pub(crate) const P_PAGE_CHILD: &str = "OfxParamPropPageChild";
/// kOfxParamPropGroupOpen。
@@ -216,7 +226,8 @@ pub enum ParamKind {
}
/// OFX 类型字符串 → 值类别 + 维度(HS: ofxhParam.cpp `findType`)。
/// 无值类(PushButton/Group/Page/Unknown)返回 None。
/// 无值类(PushButton/Group/Page)与 parametric(曲线值走
/// OfxParametricParameterSuite,无标量变长入口)返回 None。
pub(crate) fn kind_of_type(ofx_type: &str) -> Option<(ParamKind, usize)> {
match ofx_type {
TYPE_INTEGER => Some((ParamKind::Int, 1)),
@@ -271,6 +282,10 @@ fn type_default(ofx_type: &str) -> ParamValue {
TYPE_BYTES | TYPE_CUSTOM => ParamValue::Bytes(Vec::new()),
TYPE_PUSHBUTTON => ParamValue::PushButton,
TYPE_GROUP | TYPE_PAGE => ParamValue::Container,
// parametric 默认 = 1 条恒等曲线(range 默认 (0,1))。
TYPE_PARAMETRIC => ParamValue::Parametric(vec![crate::param_curve::Curve::identity(
0.0, 1.0,
)]),
_ => ParamValue::Container, // 未知类型占位(paramDefine 已拒绝)
},
}
@@ -319,6 +334,11 @@ pub enum ParamValue {
PushButton,
/// kOfxParamTypeGroup / Page(容器,无值)。
Container,
/// kOfxParamTypeParametric:每维一条曲线(曲线模型见
/// [`crate::param_curve`];默认 = 1 条恒等曲线,维度由属性
/// `OfxParamPropParametricDimension` 决定,见
/// [`ParamDef::new`])。
Parametric(Vec<crate::param_curve::Curve>),
}
/// 参数定义(describe 产物,见 [`crate::descriptor::EffectDescriptor`])。
@@ -434,6 +454,33 @@ impl ParamDef {
TYPE_GROUP => {
props.set_one(P_GROUP_OPEN, Value::Int(1));
}
TYPE_PARAMETRIC => {
// parametricHS addValueParamProps(eDouble, 0) 的
// invariantProps + allParametricofxhParam.cpp:285-330):
// 值类 invariant 属性照旧;Animates=1HS 的 animates 计算
// 对 parametric 为真);另加 4 个 parametric 专属属性。
// 数值属性表(Min/Max/DisplayHS 也加(变量维 double),
// 但插件侧从不读 parametric 的数值属性(ofxsPropertyValidation
// 只校验本块 4 个 + 通用属性),本 crate 遵循
// is_numeric_type 的门控,不为 parametric 预置。
props.set_one(P_IS_ANIMATING, Value::Int(0));
props.set_one(P_IS_AUTO_KEYING, Value::Int(0));
props.set_one(P_PERSISTANT, Value::Int(1));
props.set_one(P_EVALUATE_ON_CHANGE, Value::Int(1));
props.set_one(P_CAN_UNDO, Value::Int(1));
props.set_one(
P_CACHE_INVALIDATION,
Value::String(cs(V_INVALIDATE_VALUE_CHANGE)),
);
props.set_one(P_ANIMATES, Value::Int(1));
props.set_one(P_PARAMETRIC_DIMENSION, Value::Int(1));
props.define(P_PARAMETRIC_UI_COLOUR, vec![]);
props.set_one(P_PARAMETRIC_INTERACT_BG, Value::Pointer(std::ptr::null_mut()));
props.define(
P_PARAMETRIC_RANGE,
vec![Value::Double(0.0), Value::Double(1.0)],
);
}
_ => {}
}
@@ -825,6 +872,39 @@ mod tests {
));
}
/// parametric 的默认:1 条恒等曲线;4 个专属属性就位
/// dimension=1、range=(0,1)、UIColour 未设、interact 背景 NULL)。
#[test]
fn parametric_type_default() {
assert!(matches!(
type_default(TYPE_PARAMETRIC),
ParamValue::Parametric(ref c) if c.len() == 1 && c[0].len() == 2
));
let d = ParamDef::new("curve", TYPE_PARAMETRIC);
assert!(matches!(d.default, ParamValue::Parametric(ref c) if c.len() == 1));
assert!(matches!(
d.props.get(P_PARAMETRIC_DIMENSION, 0),
Some(Value::Int(1))
));
assert!(matches!(
d.props.get(P_PARAMETRIC_RANGE, 0),
Some(Value::Double(0.0))
));
assert!(matches!(
d.props.get(P_PARAMETRIC_RANGE, 1),
Some(Value::Double(1.0))
));
assert_eq!(d.props.dimension(P_PARAMETRIC_UI_COLOUR), 0);
assert!(matches!(
d.props.get(P_PARAMETRIC_INTERACT_BG, 0),
Some(Value::Pointer(p)) if p.is_null()
));
// 默认曲线求值为恒等。
if let ParamValue::Parametric(curves) = &d.default {
assert_eq!(curves[0].evaluate(0.5), 0.5);
}
}
/// set_from_node 的维度截断/补零与字符串静默路径。
#[test]
fn set_from_node_dimension_rules() {
+393
View File
@@ -0,0 +1,393 @@
// 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 的宿主侧数据与求值)。
//!
//! 曲线 = 按 key 升序排列的控制点列表,每个控制点携带 Hermite 切线
//! slope)。suite[`crate::suites::parametric`])只暴露 key/value
//! 编辑入口,slope 一律由 [`Curve::recompute_slopes`] 自动计算——
//! 内部点用两侧差分(centered finite difference),端点用单侧差分;
//! 字段保持 `pub`,便于测试/宿主显式编辑斜率。
//!
//! 求值 = 分段三次 Hermite(见 [`Curve::evaluate`])。这是 OFX 规范
//! 建议宿主采用的"曲线编辑器式"表示(ofxParametricParam.h 的
//! parametric 参数文档),与贝塞尔形式等价(Hermite 切线 × 段长即
//! 贝塞尔控制臂)。
/// 曲线控制点:`key`parametric 位置,定义域由
/// `OfxParamPropParametricRange` 限定)、`value`(求值结果)、
/// `slope`(该点处的一阶导数,三次 Hermite 的切线)。
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ControlPoint {
/// parametric 位置(x)。
pub key: f64,
/// 求值结果(y)。
pub value: f64,
/// 一阶导数(自动差分 / 显式编辑)。
pub slope: f64,
}
impl ControlPoint {
/// 按 key/value 构造(slope 由 [`Curve::recompute_slopes`] 填入)。
pub fn new(key: f64, value: f64) -> Self {
Self {
key,
value,
slope: 0.0,
}
}
}
/// 一条参数曲线:控制点按 key 升序(不变量;由本模块的修改接口
/// 维护)。
#[derive(Clone, Debug, PartialEq)]
pub struct Curve {
/// 控制点序列(key 升序,无重复 key)。
pub points: Vec<ControlPoint>,
}
impl Curve {
/// 空曲线(`DeleteAllControlPoints` 后的状态;求值退化为恒等,
/// 见 [`Curve::evaluate`])。
pub fn empty() -> Self {
Self {
points: Vec::new(),
}
}
/// 恒等曲线:`{(lo, lo), (hi, hi)}` + 自动 slope —— parametric
/// 参数的默认 defaultofxParametricParam.h"The default default
/// value of a parametric curve is to be an identity lookup")。
/// `lo`/`hi` 来自 `OfxParamPropParametricRange`(默认 (0,1))。
pub fn identity(lo: f64, hi: f64) -> Self {
let mut c = Self {
points: vec![
ControlPoint::new(lo, lo),
ControlPoint::new(hi, hi),
],
};
c.recompute_slopes();
c
}
/// 由 key/value 对构造(slope 自动差分;按传入顺序——调用方
/// 保证升序,或经 [`Curve::upsert`] 逐点插入)。
pub fn from_pairs(pairs: &[(f64, f64)]) -> Self {
let mut c = Self {
points: pairs
.iter()
.map(|&(k, v)| ControlPoint::new(k, v))
.collect(),
};
c.recompute_slopes();
c
}
/// 控制点数。
pub fn len(&self) -> usize {
self.points.len()
}
/// 是否为空(`DeleteAllControlPoints` 后)。
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
/// 第 `i` 个控制点(越界 → None)。
pub fn nth(&self, i: usize) -> Option<&ControlPoint> {
self.points.get(i)
}
/// 重算全部 slope:内部点中心差分(两侧跨距),端点单侧差分;
/// 单点/空曲线无差分 → slope 0。修改 key/value 后必须调用,
/// 否则 Hermite 切线滞后于控制点几何。
pub fn recompute_slopes(&mut self) {
let n = self.points.len();
for i in 0..n {
self.points[i].slope = slope_at(&self.points, i);
}
}
/// 三次 Hermite 求值。
///
/// - 空曲线:恒等(f(x) = x)——与"默认 default 是恒等查找"
/// 一致,`DeleteAllControlPoints` 后曲线退化为中性恒等,无端点
/// 可钳制,直接返回 x
/// - 单点曲线:常数(该点 value);
/// - x 在首/末 key 之外:钳制到端点值(任务/规范:越界 key
/// 钳制到端点值)。
///
/// 数学:段 [x0, x1] 上令 t = (x - x0)/(x1 - x0)h = x1 - x0
/// 三次 Hermite 基函数
/// h00 = 2t³ - 3t² + 1, h01 = -2t³ + 3t²
/// h10 = t³ - 2t² + t, h11 = t³ - t²
/// f(x) = h00·y0 + h01·y1 + h·(h10·m0 + h11·m1)。
/// slope 为 1 的恒等曲线恰退化为 f(x) = x(端点钳制外)。
pub fn evaluate(&self, x: f64) -> f64 {
let n = self.points.len();
if n == 0 {
return x;
}
if n == 1 {
return self.points[0].value;
}
if x <= self.points[0].key {
return self.points[0].value;
}
if x >= self.points[n - 1].key {
return self.points[n - 1].value;
}
// 定位所在段:points[i].key <= x < points[i+1].key。
let i = self.points.partition_point(|p| p.key <= x) - 1;
let a = self.points[i];
let b = self.points[i + 1];
let h = b.key - a.key;
if h <= 0.0 {
return a.value; // 防御:重复 key(不变量保证不出现)
}
let t = (x - a.key) / h;
let t2 = t * t;
let t3 = t2 * t;
let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
let h01 = -2.0 * t3 + 3.0 * t2;
let h10 = t3 - 2.0 * t2 + t;
let h11 = t3 - t2;
h00 * a.value + h01 * b.value + h * (h10 * a.slope + h11 * b.slope)
}
/// 放入 (key, value):同 key 已存在 → 覆盖其值(返回 false);
/// 否则按 key 升序插入(返回 true)。随后重算 slope。
/// 这是 Add 与 SetNth 的公共落点(SetNth 先移除再落点)。
pub fn upsert(&mut self, key: f64, value: f64) -> bool {
if let Some(p) = self.points.iter_mut().find(|p| p.key == key) {
p.value = value;
self.recompute_slopes();
return false;
}
let idx = self.points.partition_point(|p| p.key < key);
self.points.insert(idx, ControlPoint::new(key, value));
self.recompute_slopes();
true
}
/// 改第 `nth` 个控制点为 (key, value)。key 变化可能破坏有序性
/// ofxParametricParam.hSetNthControlPoint 的 key 可前移/后移
/// 到其他点之前/之后)——先移除再按 key 插入;新 key 撞上其他
/// 点的 key 时按覆盖语义处理。nth 越界 → Err(())。
pub fn set_nth(&mut self, nth: usize, key: f64, value: f64) -> Result<(), ()> {
if nth >= self.points.len() {
return Err(());
}
self.points.remove(nth);
self.upsert(key, value);
Ok(())
}
/// 删第 `nth` 个控制点(nth 越界 → Err(()));随后重算 slope。
pub fn delete_nth(&mut self, nth: usize) -> Result<(), ()> {
if nth >= self.points.len() {
return Err(());
}
self.points.remove(nth);
self.recompute_slopes();
Ok(())
}
/// 删除全部控制点(`DeleteAllControlPoints`)。
pub fn clear(&mut self) {
self.points.clear();
}
}
/// 第 `i` 点处的差分斜率(不重算,供 [`Curve::recompute_slopes`])。
/// 内部点:中心差分 (y_{i+1} - y_{i-1}) / (x_{i+1} - x_{i-1})
/// 端点:单侧差分;单点/空曲线:0。防御重复 key 时除零 → 0。
fn slope_at(points: &[ControlPoint], i: usize) -> f64 {
let n = points.len();
if n < 2 {
return 0.0;
}
let (a, b) = if i == 0 {
(&points[0], &points[1])
} else if i == n - 1 {
(&points[n - 2], &points[n - 1])
} else {
(&points[i - 1], &points[i + 1])
};
let dx = b.key - a.key;
if dx == 0.0 {
return 0.0;
}
(b.value - a.value) / dx
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f64, b: f64, eps: f64) -> bool {
(a - b).abs() <= eps
}
/// 恒等曲线:{(0,0),(1,1)} + 自动 slope1,1)→ Hermite 恰为
/// f(x) = x(数学上精确,浮点逐位不保证——近似比较);非默认
/// range 同样恒等。
#[test]
fn identity_curve_evaluates_to_x() {
let c = Curve::identity(0.0, 1.0);
assert_eq!(c.len(), 2);
assert_eq!(
c.points[0],
ControlPoint {
key: 0.0,
value: 0.0,
slope: 1.0
}
);
assert_eq!(
c.points[1],
ControlPoint {
key: 1.0,
value: 1.0,
slope: 1.0
}
);
for x in [0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0] {
assert!(close(c.evaluate(x), x, 1e-12), "identity at {x}");
}
// 非默认 range(插件改 OfxParamPropParametricRange 后,
// 新曲线的默认 default 相应平移)。
let c = Curve::identity(0.0, 255.0);
assert!(close(c.evaluate(128.0), 128.0, 1e-12));
}
/// 单段抛物线形状:{(0,0),(0.5,0.25),(1,1)}y = x² 采样)。
/// 自动 slope = (0.5, 1, 1.5)(端点单侧、中点中心差分);
/// 求值精确等于分段三次 Hermite 的解析值(过控制点、两侧
/// 单调递增、中点手算值)。
#[test]
fn parabola_shape_honours_hermite() {
let c = Curve::from_pairs(&[(0.0, 0.0), (0.5, 0.25), (1.0, 1.0)]);
assert_eq!(
c.points.iter().map(|p| p.slope).collect::<Vec<_>>(),
vec![0.5, 1.0, 1.5]
);
// 插值性质:控制点处精确命中。
assert_eq!(c.evaluate(0.0), 0.0);
assert_eq!(c.evaluate(0.5), 0.25);
assert_eq!(c.evaluate(1.0), 1.0);
// 中点解析值(t = 1/2 段内,精确二进制分数)。
assert_eq!(c.evaluate(0.125), 0.05078125);
assert_eq!(c.evaluate(0.25), 0.09375);
assert_eq!(c.evaluate(0.75), 0.59375);
// 形状:单调递增;首段在抛物线 y = x² 之上(Hermite 端点
// 用弦斜率、中点用中心差分 → 首段整体上凸)。
let xs = [0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9];
for w in xs.windows(2) {
assert!(c.evaluate(w[0]) < c.evaluate(w[1]));
}
assert!(c.evaluate(0.25) > 0.25 * 0.25); // 高于抛物线采样点
}
/// 越界钳制:x 在首/末 key 之外 → 端点值。
#[test]
fn out_of_range_clamps_to_endpoints() {
let c = Curve::from_pairs(&[(0.2, 0.3), (0.8, 0.9)]);
assert_eq!(c.evaluate(-100.0), 0.3);
assert_eq!(c.evaluate(0.19), 0.3);
assert_eq!(c.evaluate(0.2), 0.3);
assert_eq!(c.evaluate(0.81), 0.9);
assert_eq!(c.evaluate(100.0), 0.9);
}
/// 增删点后求值正确:upsert 新点 → 该点命中、重排有序;删除后
/// 恢复;同 key 覆盖不增点。
#[test]
fn upsert_and_delete_keep_evaluation_consistent() {
let mut c = Curve::from_pairs(&[(0.0, 0.0), (1.0, 1.0)]);
// 插入中点(返回 true = 新增),求值在其 key 处精确命中。
assert!(c.upsert(0.3, 0.7));
assert_eq!(c.len(), 3);
assert_eq!(c.evaluate(0.3), 0.7);
// 有序性保持。
let keys: Vec<f64> = c.points.iter().map(|p| p.key).collect();
assert_eq!(keys, vec![0.0, 0.3, 1.0]);
// 同 key 覆盖(返回 false = 覆盖),数量不变。
assert!(!c.upsert(0.3, 0.5));
assert_eq!(c.len(), 3);
assert_eq!(c.evaluate(0.3), 0.5);
// 删除 → 恢复双点(恒等;非 2 幂 key 处逐位有舍入)。
c.delete_nth(1).unwrap();
assert_eq!(c.len(), 2);
assert!(close(c.evaluate(0.3), 0.3, 1e-12));
// 越界删除 → Err。
assert!(c.delete_nth(2).is_err());
}
/// set_nth 改 key 后保持有序(点在序列中移动)。
#[test]
fn set_nth_reorders_on_key_change() {
let mut c = Curve::from_pairs(&[(0.0, 0.0), (0.5, 0.5), (1.0, 1.0)]);
// 把第 0 点挪到 0.75(原第 1、2 点之前……之后)。
c.set_nth(0, 0.75, 0.75).unwrap();
let pairs: Vec<(f64, f64)> = c
.points
.iter()
.map(|p| (p.key, p.value))
.collect();
assert_eq!(pairs, vec![(0.5, 0.5), (0.75, 0.75), (1.0, 1.0)]);
assert_eq!(c.evaluate(0.75), 0.75);
// 越界 nth → Err。
assert!(c.set_nth(3, 0.9, 0.9).is_err());
}
/// slope 编辑生效:同为 {(0,0),(1,1)},自动 slope (1,1) 时
/// 求值 = 恒等;把 slope 显式压平为 0 后曲线变 S 形缓起缓收
/// (中点值从 0.25 变 0.15625)。
#[test]
fn slope_editing_changes_evaluation() {
let mut c = Curve::from_pairs(&[(0.0, 0.0), (1.0, 1.0)]);
assert_eq!(c.evaluate(0.25), 0.25);
// 显式编辑 slope(宿主/测试路径;suite 无 slope 入口,
// 但字段公开即契约的一部分)。
c.points[0].slope = 0.0;
c.points[1].slope = 0.0;
assert_eq!(c.evaluate(0.25), 0.15625);
assert_eq!(c.evaluate(0.5), 0.5);
assert!(close(c.evaluate(0.25), 0.15625, 1e-12));
}
/// 空曲线与单点曲线:恒等 / 常数退化。
#[test]
fn degenerate_curves() {
let c = Curve::empty();
assert!(c.is_empty());
assert_eq!(c.evaluate(0.3), 0.3);
assert_eq!(c.evaluate(5.0), 5.0);
let c = Curve::from_pairs(&[(0.5, 0.25)]);
assert_eq!(c.evaluate(0.0), 0.25);
assert_eq!(c.evaluate(100.0), 0.25);
}
/// clearDeleteAllControlPoints 的模型侧语义。
#[test]
fn clear_empties_curve() {
let mut c = Curve::from_pairs(&[(0.0, 0.0), (1.0, 1.0)]);
c.clear();
assert!(c.is_empty());
assert_eq!(c.evaluate(0.5), 0.5);
}
}
+7 -1
View File
@@ -34,6 +34,7 @@ pub mod memory;
pub mod message;
pub mod multithread;
pub mod param;
pub mod parametric;
pub mod progress;
pub mod property;
pub mod timeline;
@@ -224,6 +225,8 @@ pub(crate) const OFX_API_VERSION: i32 = 105;
/// 第 2 期追加:OfxImageEffectOpenGLRenderSuite v1GL 路径);
/// ofxColour 无 suite 表(纯属性 + GetOutputColourspace action)。
/// 第 3 期追加:OfxInteractSuite v1、OfxDrawSuite v1interact 宿主)。
/// 第 4 期追加:OfxParametricParameterSuite v1(曲线参数,
/// suites/parametric.rs)。
pub fn fetch_suite(name: &str, version: i32) -> Option<*const std::ffi::c_void> {
let suite: *const std::ffi::c_void = match (name, version) {
("OfxPropertySuite", 1) => ptr(property::suite_v1()),
@@ -239,6 +242,7 @@ pub fn fetch_suite(name: &str, version: i32) -> Option<*const std::ffi::c_void>
("OfxImageEffectOpenGLRenderSuite", 1) => ptr(gl_render::suite_v1()),
("OfxInteractSuite", 1) => ptr(interact::suite_v1()),
("OfxDrawSuite", 1) => ptr(draw::suite_v1()),
("OfxParametricParameterSuite", 1) => ptr(parametric::suite_v1()),
_ => return None,
};
Some(suite)
@@ -253,7 +257,7 @@ fn ptr<T>(p: &'static T) -> *const std::ffi::c_void {
mod tests {
use super::*;
/// 十张 suite 分发表:版本精确匹配、未知版本/名字 → None。
/// suite 分发表:版本精确匹配、未知版本/名字 → None。
#[test]
fn fetch_suite_dispatch() {
assert!(fetch_suite("OfxPropertySuite", 1).is_some());
@@ -269,12 +273,14 @@ mod tests {
assert!(fetch_suite("OfxImageEffectOpenGLRenderSuite", 1).is_some());
assert!(fetch_suite("OfxInteractSuite", 1).is_some());
assert!(fetch_suite("OfxDrawSuite", 1).is_some());
assert!(fetch_suite("OfxParametricParameterSuite", 1).is_some());
assert!(fetch_suite("OfxPropertySuite", 2).is_none());
assert!(fetch_suite("OfxMessageSuite", 3).is_none());
assert!(fetch_suite("OfxImageEffectOpenGLRenderSuite", 2).is_none());
assert!(fetch_suite("OfxInteractSuite", 2).is_none());
assert!(fetch_suite("OfxDrawSuite", 2).is_none());
assert!(fetch_suite("OfxParametricParameterSuite", 2).is_none());
assert!(fetch_suite("OfxBogusSuite", 1).is_none());
assert!(fetch_suite("", 1).is_none());
}
+16 -13
View File
@@ -18,7 +18,8 @@
//!
//! 语义对照 HS: ofxhParam.cpp
//! - paramDefinedescribe 期把参数定义挂到效果描述符(未定义类型 →
//! kOfxStatErrUnsupportedHS:1665-1710parametric 第 1 期不支持);
//! kOfxStatErrUnsupportedHS:1665-1710parametric 类型放行,其
//! 曲线读写走 OfxParametricParameterSuite,见 suites/parametric.rs);
//! - paramGetHandle:按名查(未找到 → kOfxStatErrUnknownHS:1758);
//! - paramGetValue/paramSetValue 等变长入口在 C shim
//! cbits/ofx_param_shim.c):按 [`crate::param::ParamKind`] 解析
@@ -330,8 +331,9 @@ pub(crate) fn unregister_params_of(instance_props: usize) {
/// paramSetValue 成功后的 instanceChanged 触发(HS:
/// ofxhParam.cpp:1991-1994 `paramChangedByPlugin`)。未登记实例时
/// no-op(未绑定节点的场景本就 no-op)。
fn notify_changed(param_props: usize, name: &str) {
/// no-op(未绑定节点的场景本就 no-op)。parametric suite 的
/// Set/Add/Delete 复用此路径(pub(crate)suites/parametric.rs)。
pub(crate) fn notify_changed(param_props: usize, name: &str) {
let owner = PARAM_OWNER
.lock()
.unwrap_or_else(|e| e.into_inner())
@@ -463,9 +465,9 @@ pub unsafe extern "C" fn ofx_param_missing_feature_impl(param: *mut c_void) -> c
// ---- 非变长入口 ----------------------------------------------------------
/// paramDefinedescribe 期定义参数(未定义类型/parametric →
/// UnsupportedHS:1704;重复名 → HS 允许,原样再建一条——宿主以
/// 首个为准,与 HS 的 map 覆盖语义一致)。
/// paramDefinedescribe 期定义参数(未定义类型 → Unsupported
/// HS:1704;重复名 → HS 允许,原样再建一条——宿主以首个为准,与
/// HS 的 map 覆盖语义一致)。
unsafe extern "C" fn param_define(
param_set: *mut c_void,
param_type: *const c_char,
@@ -485,7 +487,8 @@ unsafe extern "C" fn param_define(
ParamSetRef::Descriptor(d) => d,
ParamSetRef::Instance(_) => return Err(status::ERR_BAD_HANDLE),
};
// 类型合法性(kind_of_type 之外还有无值类与 parametric)。
// 类型合法性(kind_of_type 之外还有无值类与 parametric
// parametric 的曲线值走 OfxParametricParameterSuite)。
let valid = crate::param::kind_of_type(t).is_some()
|| matches!(
t,
@@ -494,8 +497,9 @@ unsafe extern "C" fn param_define(
| crate::param::TYPE_PUSHBUTTON
| crate::param::TYPE_GROUP
| crate::param::TYPE_PAGE
| crate::param::TYPE_PARAMETRIC
);
if !valid || t == crate::param::TYPE_PARAMETRIC {
if !valid {
return Err(status::ERR_UNSUPPORTED);
}
let def = ParamDef::new(n, t);
@@ -873,7 +877,8 @@ mod tests {
);
}
// 未定义类型 / parametric → ErrUnsupportedHS:1704
// 未定义类型 → ErrUnsupportedHS:1704parametric → 放行
// (曲线读写走 OfxParametricParameterSuite)。
let bad = cs("OfxParamTypeBogus");
let par = cs("OfxParamTypeParametric");
unsafe {
@@ -881,11 +886,9 @@ mod tests {
(s.param_define)(handle, bad.as_ptr(), n.as_ptr(), &mut ph2),
status::ERR_UNSUPPORTED
);
assert_eq!(
(s.param_define)(handle, par.as_ptr(), n.as_ptr(), &mut ph2),
status::ERR_UNSUPPORTED
);
assert_eq!((s.param_define)(handle, par.as_ptr(), n.as_ptr(), &mut ph2), 0);
}
assert_eq!(tag::kind(ph2), tag::PARAM_DEF);
// 实例期 paramDefine → BadHandle。
let (_inst, ih) = make_instance();
+900
View File
@@ -0,0 +1,900 @@
// 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 期可改;
//! - 求值 = 三次 Hermiteslope 自动差分),越界 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`]——
/// 曲线无标量节点值,节点回写是 no-op,但走既有通知路径保持宿主侧
/// 变更感知的纪律一致)。
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 喂给 Instancedescribe
/// 之外的字段不被本测试触碰)。
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(),
})
}
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 参数 → BadHandleInteger 参数的句柄)。
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();
// UIColour3 个 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);
}
}