// 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(); } } // ---- 曲线集 ↔ JSON(节点输入 / 工程序列化的值载荷)-------------------- // // 格式(确定性、紧凑,无空白): // {"curves":[[{"key":0.0,"value":0.0,"slope":1.0},...],...]} // 外层按 dimension 顺序一维一条曲线;每维一个控制点对象,字段序固定 // key/value/slope。数值用 Rust 最短往返格式化(`{}`)。非有限值 JSON // 无标准写法,取对称 token:NaN → `null`,+Inf → `"inf"`, // -Inf → `"-inf"`(字符串字面量;解析器对称处理,有限值恒为裸数字)。 // 解析器容忍空白与字段乱序;结构不符 → None(调用方按字符串族静默 // 路径处理)。手写实现,不引入 serde(crate 无该依赖)。 /// 曲线集 → JSON(节点输入默认值 / 插件回写节点输入的载荷)。 pub fn curves_to_json(curves: &[Curve]) -> String { let mut out = String::with_capacity(8 + 12 * curves.iter().map(|c| c.len()).sum::()); out.push_str("{\"curves\":["); for (ci, c) in curves.iter().enumerate() { if ci > 0 { out.push(','); } out.push('['); for (pi, p) in c.points.iter().enumerate() { if pi > 0 { out.push(','); } out.push_str("{\"key\":"); push_num(&mut out, p.key); out.push_str(",\"value\":"); push_num(&mut out, p.value); out.push_str(",\"slope\":"); push_num(&mut out, p.slope); out.push('}'); } out.push(']'); } out.push_str("]}"); out } /// JSON → 曲线集(格式见 [`curves_to_json`];结构不符 → None)。 pub fn curves_from_json(text: &str) -> Option> { let mut p = JsonParser { bytes: text.as_bytes(), pos: 0, }; p.skip_ws(); if !p.eat(b'{') { return None; } p.skip_ws(); if p.parse_string()?.as_str() != "curves" { return None; } p.skip_ws(); if !p.eat(b':') { return None; } p.skip_ws(); let mut curves = Vec::new(); if p.eat(b'[') { loop { p.skip_ws(); if p.eat(b']') { break; } curves.push(p.parse_curve()?); p.skip_ws(); if p.eat(b',') { continue; } if p.eat(b']') { break; } return None; } } else { return None; } p.skip_ws(); if !p.eat(b'}') { return None; } p.skip_ws(); if p.pos != p.bytes.len() { return None; } Some(curves) } /// 单个数值 → JSON token(有限值裸数字;NaN/±Inf 见模块文档)。 fn push_num(out: &mut String, v: f64) { if v.is_nan() { out.push_str("null"); } else if v == f64::INFINITY { out.push_str("\"inf\""); } else if v == f64::NEG_INFINITY { out.push_str("\"-inf\""); } else { out.push_str(&format!("{v}")); } } /// 手写 JSON 解析器(仅本格式的子集;字段名无转义引号,控制点字段 /// 均为固定标识符)。 struct JsonParser<'a> { bytes: &'a [u8], pos: usize, } impl<'a> JsonParser<'a> { fn skip_ws(&mut self) { while self.pos < self.bytes.len() && self.bytes[self.pos].is_ascii_whitespace() { self.pos += 1; } } fn eat(&mut self, byte: u8) -> bool { if self.bytes.get(self.pos) == Some(&byte) { self.pos += 1; true } else { false } } /// 原样匹配一段字节(字段名;不做字符串语义)。 fn eat_str(&mut self, want: &[u8]) -> bool { if self.bytes[self.pos..].starts_with(want) { self.pos += want.len(); true } else { false } } /// 引号字符串(无转义;字段名/`"inf"` token 用)。 fn parse_string(&mut self) -> Option { if !self.eat(b'"') { return None; } let start = self.pos; while self.pos < self.bytes.len() && self.bytes[self.pos] != b'"' { self.pos += 1; } let s = std::str::from_utf8(&self.bytes[start..self.pos]).ok()?.to_string(); if !self.eat(b'"') { return None; } Some(s) } /// 数值 token:`null` → NaN;`"inf"`/`"-inf"` → ±∞;否则裸数字 /// (`f64::from_str`,上溢 → ±∞,与 `{}` 最短往返格式对称)。 fn parse_num(&mut self) -> Option { if self.eat_str(b"null") { return Some(f64::NAN); } if self.bytes.get(self.pos) == Some(&b'"') { return match self.parse_string()?.as_str() { "inf" => Some(f64::INFINITY), "-inf" => Some(f64::NEG_INFINITY), _ => None, }; } let start = self.pos; while self.pos < self.bytes.len() && matches!( self.bytes[self.pos], b'-' | b'+' | b'.' | b'0'..=b'9' | b'e' | b'E' ) { self.pos += 1; } if self.pos == start { return None; } std::str::from_utf8(&self.bytes[start..self.pos]) .ok()? .parse() .ok() } /// 一条曲线:`[` 控制点* `]`(控制点按 key 升序存储,解析按原序)。 fn parse_curve(&mut self) -> Option { if !self.eat(b'[') { return None; } let mut points = Vec::new(); loop { self.skip_ws(); if self.eat(b']') { break; } points.push(self.parse_point()?); self.skip_ws(); if self.eat(b',') { continue; } if self.eat(b']') { break; } return None; } Some(Curve { points }) } /// 一个控制点:`{` ("name" `:` 数值)* `}`(字段乱序可;未知字段 /// 拒绝)。 fn parse_point(&mut self) -> Option { if !self.eat(b'{') { return None; } let mut key = None; let mut value = None; let mut slope = None; loop { self.skip_ws(); if self.eat(b'}') { break; } let name = self.parse_string()?; self.skip_ws(); if !self.eat(b':') { return None; } self.skip_ws(); let v = self.parse_num()?; match name.as_str() { "key" => key = Some(v), "value" => value = Some(v), "slope" => slope = Some(v), _ => return None, } self.skip_ws(); if self.eat(b',') { continue; } if self.eat(b'}') { break; } return None; } Some(ControlPoint { key: key?, value: value?, slope: slope.unwrap_or(0.0), }) } } /// 第 `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); } /// JSON 往返:恒等曲线(默认值)序列化形状逐字 + 解析回等值模型。 #[test] fn json_roundtrip_identity() { let curves = vec![Curve::identity(0.0, 1.0)]; let json = curves_to_json(&curves); assert_eq!( json, r#"{"curves":[[{"key":0,"value":0,"slope":1},{"key":1,"value":1,"slope":1}]]}"# ); let back = curves_from_json(&json).expect("应可解析"); assert_eq!(back, curves); } /// JSON 往返:多维 + 非平凡形状 + 显式编辑 slope(slope 也是载荷 /// 的一部分,逐位保留)。 #[test] fn json_roundtrip_multidim_and_slope() { let mut c = Curve::from_pairs(&[(0.0, 0.0), (0.5, 0.25), (1.0, 1.0)]); c.points[1].slope = 0.0; // 显式编辑(字段公开即契约) let curves = vec![c.clone(), Curve::identity(0.0, 255.0)]; let json = curves_to_json(&curves); let back = curves_from_json(&json).expect("应可解析"); assert_eq!(back.len(), 2); assert_eq!(back, curves); assert_eq!(back[0].points[1].slope, 0.0); assert_eq!(back[1].points[1].key, 255.0); // 求值不受序列化影响。 assert_eq!(back[0].evaluate(0.5), 0.25); } /// JSON 往返:特殊值(NaN/±Inf,slope 与值均可)与空曲线/空集。 #[test] fn json_roundtrip_special_and_empty() { let curves = vec![Curve { points: vec![ ControlPoint { key: 0.0, value: f64::NAN, slope: 0.0, }, ControlPoint { key: 1.0, value: f64::INFINITY, slope: f64::NEG_INFINITY, }, ], }]; let json = curves_to_json(&curves); assert_eq!( json, r#"{"curves":[[{"key":0,"value":null,"slope":0},{"key":1,"value":"inf","slope":"-inf"}]]}"# ); let back = curves_from_json(&json).expect("应可解析"); assert!(back[0].points[0].value.is_nan()); assert_eq!(back[0].points[1].value, f64::INFINITY); assert_eq!(back[0].points[1].slope, f64::NEG_INFINITY); // 空曲线(DeleteAll 后)与空集。 let curves = vec![Curve::empty(), Curve::empty()]; let json = curves_to_json(&curves); assert_eq!(json, r#"{"curves":[[],[]]}"#); assert_eq!(curves_from_json(&json).unwrap(), curves); assert_eq!(curves_from_json(r#"{"curves":[]}"#).unwrap(), Vec::::new()); } /// JSON 解析:容忍空白与字段乱序;结构不符 → None(静默路径)。 #[test] fn json_parser_tolerances() { let curves = vec![Curve::from_pairs(&[(0.0, 0.0), (1.0, 1.0)])]; // 空白 + 字段乱序。 let spaced = r#"{ "curves" : [ [ { "slope" : 1 , "key" : 0 , "value" : 0 } , { "key" : 1 , "value" : 1 , "slope" : 1 } ] ] }"#; assert_eq!(curves_from_json(spaced).unwrap(), curves); // 结构不符 → None。 for bad in [ "", "{}", r#"{"curves"}"#, r#"{"curve":[]}"#, r#"{"curves":[{"key":0}]}"#, r#"{"curves":[[{"key":"x"}]]}"#, r#"{"curves":[[{"key":0}]]"#, r#"{"curves":[[{"key":0,"value":0,"slope":0}]]}junk"#, ] { assert!(curves_from_json(bad).is_none(), "应拒绝:{bad}"); } // 缺 slope 字段 → 0(宽容)。 let no_slope = r#"{"curves":[[{"key":0,"value":0}]]}"#; let back = curves_from_json(no_slope).unwrap(); assert_eq!(back[0].points[0].slope, 0.0); } }