diff --git a/crates/oakplugin/src/lib.rs b/crates/oakplugin/src/lib.rs
index 136e8a0bc..b70a83d97 100644
--- a/crates/oakplugin/src/lib.rs
+++ b/crates/oakplugin/src/lib.rs
@@ -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;
diff --git a/crates/oakplugin/src/param.rs b/crates/oakplugin/src/param.rs
index 5821694d9..515b52ab3 100644
--- a/crates/oakplugin/src/param.rs
+++ b/crates/oakplugin/src/param.rs
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-//! 参数体系: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";
+/// kOfxParamPropParametricDimension(int×1,默认 1)。
+pub(crate) const P_PARAMETRIC_DIMENSION: &str = "OfxParamPropParametricDimension";
+/// kOfxParamPropParametricRange(double×2,默认 (0,1))。
+pub(crate) const P_PARAMETRIC_RANGE: &str = "OfxParamPropParametricRange";
+/// kOfxParamPropParametricUIColour(double×3N,默认未设)。
+pub(crate) const P_PARAMETRIC_UI_COLOUR: &str = "OfxParamPropParametricUIColour";
+/// kOfxParamPropParametricInteractBackground(pointer×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),
}
/// 参数定义(describe 产物,见 [`crate::descriptor::EffectDescriptor`])。
@@ -434,6 +454,33 @@ impl ParamDef {
TYPE_GROUP => {
props.set_one(P_GROUP_OPEN, Value::Int(1));
}
+ TYPE_PARAMETRIC => {
+ // parametric(HS addValueParamProps(eDouble, 0) 的
+ // invariantProps + allParametric,ofxhParam.cpp:285-330):
+ // 值类 invariant 属性照旧;Animates=1(HS 的 animates 计算
+ // 对 parametric 为真);另加 4 个 parametric 专属属性。
+ // 数值属性表(Min/Max/Display)HS 也加(变量维 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() {
diff --git a/crates/oakplugin/src/param_curve.rs b/crates/oakplugin/src/param_curve.rs
new file mode 100644
index 000000000..af1704d46
--- /dev/null
+++ b/crates/oakplugin/src/param_curve.rs
@@ -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 .
+
+//! 参数化曲线模型(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,
+}
+
+impl Curve {
+ /// 空曲线(`DeleteAllControlPoints` 后的状态;求值退化为恒等,
+ /// 见 [`Curve::evaluate`])。
+ pub fn empty() -> Self {
+ Self {
+ points: Vec::new(),
+ }
+ }
+
+ /// 恒等曲线:`{(lo, lo), (hi, hi)}` + 自动 slope —— parametric
+ /// 参数的默认 default(ofxParametricParam.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.h:SetNthControlPoint 的 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)} + 自动 slope(1,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![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 = 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);
+ }
+
+ /// clear:DeleteAllControlPoints 的模型侧语义。
+ #[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);
+ }
+}
diff --git a/crates/oakplugin/src/suites/mod.rs b/crates/oakplugin/src/suites/mod.rs
index 19fca7bc6..0cd619065 100644
--- a/crates/oakplugin/src/suites/mod.rs
+++ b/crates/oakplugin/src/suites/mod.rs
@@ -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 v1(GL 路径);
/// ofxColour 无 suite 表(纯属性 + GetOutputColourspace action)。
/// 第 3 期追加:OfxInteractSuite v1、OfxDrawSuite v1(interact 宿主)。
+/// 第 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(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());
}
diff --git a/crates/oakplugin/src/suites/param.rs b/crates/oakplugin/src/suites/param.rs
index e416c0ec2..5ae376485 100644
--- a/crates/oakplugin/src/suites/param.rs
+++ b/crates/oakplugin/src/suites/param.rs
@@ -18,7 +18,8 @@
//!
//! 语义对照 HS: ofxhParam.cpp:
//! - paramDefine:describe 期把参数定义挂到效果描述符(未定义类型 →
-//! kOfxStatErrUnsupported,HS:1665-1710;parametric 第 1 期不支持);
+//! kOfxStatErrUnsupported,HS:1665-1710;parametric 类型放行,其
+//! 曲线读写走 OfxParametricParameterSuite,见 suites/parametric.rs);
//! - paramGetHandle:按名查(未找到 → kOfxStatErrUnknown,HS: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
// ---- 非变长入口 ----------------------------------------------------------
-/// paramDefine:describe 期定义参数(未定义类型/parametric →
-/// Unsupported,HS:1704;重复名 → HS 允许,原样再建一条——宿主以
-/// 首个为准,与 HS 的 map 覆盖语义一致)。
+/// paramDefine:describe 期定义参数(未定义类型 → 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 → ErrUnsupported(HS:1704)。
+ // 未定义类型 → ErrUnsupported(HS:1704);parametric → 放行
+ // (曲线读写走 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();
diff --git a/crates/oakplugin/src/suites/parametric.rs b/crates/oakplugin/src/suites/parametric.rs
new file mode 100644
index 000000000..1de6394cf
--- /dev/null
+++ b/crates/oakplugin/src/suites/parametric.rs
@@ -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 .
+
+//! 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, 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, 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) {
+ 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 {
+ 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, 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 = 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 {
+ 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, *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, *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);
+ }
+}