From c9d557e127a6175f42abf305be39aa0f7b76a6b3 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Fri, 21 Aug 2026 03:03:50 +0800 Subject: [PATCH] feat(plugin): OfxParametricParameterSuite v1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- crates/oakplugin/src/lib.rs | 1 + crates/oakplugin/src/param.rs | 86 ++- crates/oakplugin/src/param_curve.rs | 393 ++++++++++ crates/oakplugin/src/suites/mod.rs | 8 +- crates/oakplugin/src/suites/param.rs | 29 +- crates/oakplugin/src/suites/parametric.rs | 900 ++++++++++++++++++++++ 6 files changed, 1400 insertions(+), 17 deletions(-) create mode 100644 crates/oakplugin/src/param_curve.rs create mode 100644 crates/oakplugin/src/suites/parametric.rs 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); + } +}