feat(plugin,node): bridge parametric params into the node/inspector path
- ValueType::Parametric; the node input carries the whole curve set as NodeValue::Text(JSON) so undo and project serialization come for free - translation pass builds the input with the default-curve JSON and the dimension/range/ui-colour properties - edits flow both ways: node input (UI) -> curves_from_json -> set_ofx(Parametric) on the instance; plugin-side Set/Add/Delete -> notify_instance_changed -> JSON written back to the input (undoable) - screenshot example: gate the macOS-only offscreen capture items so the workspace tests build on Linux/Windows
This commit is contained in:
@@ -206,7 +206,7 @@ pub fn value_to_string(declared: ValueType, value: &NodeValue, key_track: bool)
|
||||
}
|
||||
}
|
||||
ValueType::Float => format!("{}", value.to_double()),
|
||||
ValueType::Text | ValueType::StrCombo => match value {
|
||||
ValueType::Text | ValueType::StrCombo | ValueType::Parametric => match value {
|
||||
NodeValue::Text(s) => s.clone(),
|
||||
NodeValue::StrCombo(s) => s.clone(),
|
||||
_ => String::new(),
|
||||
@@ -249,6 +249,7 @@ pub fn string_to_value(declared: ValueType, text: &str) -> NodeValue {
|
||||
ValueType::Float => NodeValue::Float(text.trim().parse().unwrap_or(0.0)),
|
||||
ValueType::Text => NodeValue::Text(text.to_string()),
|
||||
ValueType::StrCombo => NodeValue::StrCombo(text.to_string()),
|
||||
ValueType::Parametric => NodeValue::Text(text.to_string()),
|
||||
_ => NodeValue::None,
|
||||
}
|
||||
}
|
||||
@@ -1082,3 +1083,42 @@ fn resolve_folder_children(
|
||||
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
m.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Value codec 对 Parametric 输入的往返(save_immediate /
|
||||
/// load_immediate 对 standard 值走的纯函数路径:split →
|
||||
/// value_to_string → string_to_value → combine;XML 桥由
|
||||
/// tests/serializer_test.rs 覆盖)。
|
||||
#[test]
|
||||
fn parametric_input_value_codec_roundtrip() {
|
||||
let declared = ValueType::Parametric;
|
||||
let json = r#"{"curves":[[{"key":0,"value":0,"slope":1},{"key":0.5,"value":0.6,"slope":1}]]}"#;
|
||||
let value = NodeValue::Text(json.to_string());
|
||||
|
||||
// save 侧:standard 值按声明类型拆轨(Text → 单轨整值)。
|
||||
let tracks = value.split_into_tracks(declared);
|
||||
assert_eq!(tracks.len(), 1);
|
||||
let text = value_to_string(declared, &tracks[0], true);
|
||||
assert_eq!(text, json);
|
||||
|
||||
// load 侧:track 文本还原 + 合并回整值。
|
||||
let restored = string_to_value(declared, &text);
|
||||
assert_eq!(restored, value);
|
||||
let combined = NodeValue::combine_tracks(&[restored], declared);
|
||||
assert_eq!(combined, value);
|
||||
|
||||
// key_track=false 形态(整值轨道)同样往返。
|
||||
assert_eq!(value_to_string(declared, &value, false), json);
|
||||
|
||||
// 载荷不是 Text(防御)→ 空串;声明类型 Parametric 无键帧
|
||||
// 插值、无 C++ 判别值。
|
||||
assert_eq!(value_to_string(declared, &NodeValue::None, true), "");
|
||||
assert!(!declared.can_interpolate());
|
||||
assert_eq!(declared.to_cpp_discriminant(), 0);
|
||||
assert!(declared.is_string());
|
||||
assert_eq!(declared.to_oak(), crate::value::oak::STRING);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,11 @@ pub enum ValueType {
|
||||
NodeRef,
|
||||
/// Push button (no payload).
|
||||
PushButton,
|
||||
/// Parametric curve (OpenFX parametric param; the value payload is a
|
||||
/// [`NodeValue::Text`] JSON document, see
|
||||
/// `oakplugin::param_curve::curves_to_json` — string-carried like
|
||||
/// [`ValueType::Text`]).
|
||||
Parametric,
|
||||
}
|
||||
|
||||
/// A node value. `Texture` stores an oakrender handle; dropping the
|
||||
@@ -310,8 +315,9 @@ impl ValueType {
|
||||
ValueType::Vec3 => oak::VEC3,
|
||||
ValueType::Vec4 => oak::VEC4,
|
||||
ValueType::Combo => oak::COMBO,
|
||||
// String-carried types (k_file/k_text/k_font/k_str_combo).
|
||||
ValueType::Text | ValueType::StrCombo => oak::STRING,
|
||||
// String-carried types (k_file/k_text/k_font/k_str_combo;
|
||||
// parametric carries a JSON text).
|
||||
ValueType::Text | ValueType::StrCombo | ValueType::Parametric => oak::STRING,
|
||||
_ => oak::NONE,
|
||||
}
|
||||
}
|
||||
@@ -320,7 +326,10 @@ impl ValueType {
|
||||
/// the dedicated string getters/setters, `// CPP-PARITY: valueconvert.h`
|
||||
/// `value_type_is_string`).
|
||||
pub fn is_string(self) -> bool {
|
||||
matches!(self, ValueType::Text | ValueType::StrCombo)
|
||||
matches!(
|
||||
self,
|
||||
ValueType::Text | ValueType::StrCombo | ValueType::Parametric
|
||||
)
|
||||
}
|
||||
|
||||
/// Number of keyframe tracks the type splits into (C++
|
||||
@@ -363,8 +372,9 @@ impl ValueType {
|
||||
// k_subtitle_params = 20 has no Rust type counterpart.
|
||||
ValueType::Binary => 21,
|
||||
ValueType::PushButton => 22,
|
||||
// No C++ counterpart (k_none).
|
||||
ValueType::NodeRef => 0,
|
||||
// No C++ counterpart (k_none); the parametric curve payload
|
||||
// lives in the JSON text (k_bezier = 15 is a different model).
|
||||
ValueType::NodeRef | ValueType::Parametric => 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -399,12 +399,13 @@ pub fn set_input_undoable(
|
||||
}
|
||||
|
||||
/// 以 undoable 方式设置节点的标准输入值(字符串族输入:Text /
|
||||
/// StrCombo——POD 不携带字符串数据)。
|
||||
/// StrCombo / Parametric——POD 不携带字符串数据)。
|
||||
///
|
||||
/// 按输入的声明类型([`oaknode::value::ValueType`])选存储变体:
|
||||
/// `StrCombo` → [`oaknode::value::NodeValue::StrCombo`],其余
|
||||
/// 字符串族(Text)→ [`oaknode::value::NodeValue::Text`]。声明类型
|
||||
/// 不是字符串族 → [`NodeBridgeError::NotStringInput`]。命令语义与
|
||||
/// `StrCombo` → [`oaknode::value::NodeValue::StrCombo`],
|
||||
/// `Text` / `Parametric`(值 = 曲线 JSON 文本)→
|
||||
/// [`oaknode::value::NodeValue::Text`]。声明类型不是字符串族 →
|
||||
/// [`NodeBridgeError::NotStringInput`]。命令语义与
|
||||
/// [`set_input_undoable`] 相同。
|
||||
pub fn set_input_string_undoable(
|
||||
node: &NodeRef,
|
||||
@@ -425,7 +426,9 @@ pub fn set_input_string_undoable(
|
||||
};
|
||||
let nv = match declared {
|
||||
oaknode::value::ValueType::StrCombo => oaknode::value::NodeValue::StrCombo(value.to_string()),
|
||||
oaknode::value::ValueType::Text => oaknode::value::NodeValue::Text(value.to_string()),
|
||||
oaknode::value::ValueType::Text | oaknode::value::ValueType::Parametric => {
|
||||
oaknode::value::NodeValue::Text(value.to_string())
|
||||
}
|
||||
_ => return Err(NodeBridgeError::NotStringInput(input.to_string())),
|
||||
};
|
||||
|
||||
|
||||
@@ -281,7 +281,18 @@ fn default_value_for_param(def: &ParamDef) -> Option<NodeValue> {
|
||||
}
|
||||
}
|
||||
ofx::TYPE_BYTES => Some(NodeValue::Binary(Vec::new())),
|
||||
// PushButton/Group/Page/Parametric/未知 → invalid(C++ 末尾
|
||||
// parametric:值 = 默认曲线的确定性 JSON(节点输入无曲线模型,
|
||||
// 经 Text 承载;撤销/工程序列化随标准值免费获得)。默认曲线
|
||||
// 来自 def.default(describe 期插件经 parametric suite 改过
|
||||
// 的默认),不是 P_DEFAULT 属性(parametric 无标量默认)。
|
||||
ofx::TYPE_PARAMETRIC => match &def.default {
|
||||
ParamValue::Parametric(curves) => {
|
||||
Some(NodeValue::Text(crate::param_curve::curves_to_json(curves)))
|
||||
}
|
||||
// 防御:type_default 恒产出 Parametric,此处只兜底。
|
||||
_ => None,
|
||||
},
|
||||
// PushButton/Group/Page/未知 → invalid(C++ 末尾
|
||||
// return QVariant())。
|
||||
_ => None,
|
||||
}
|
||||
@@ -450,6 +461,9 @@ fn input_type_for(ofx_type: &str) -> Option<ValueType> {
|
||||
ofx::TYPE_STRCHOICE => ValueType::StrCombo,
|
||||
ofx::TYPE_BYTES | ofx::TYPE_CUSTOM => ValueType::Binary,
|
||||
ofx::TYPE_PUSHBUTTON => ValueType::PushButton,
|
||||
// parametric:值 = 默认曲线的 JSON 文本(见
|
||||
// [`crate::param_curve::curves_to_json`])。
|
||||
ofx::TYPE_PARAMETRIC => ValueType::Parametric,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -533,6 +547,44 @@ fn build_core(inst: &Instance) -> NodeCore {
|
||||
.push(("ui_page".to_string(), NodeValue::Text(page.clone())));
|
||||
}
|
||||
|
||||
// parametric 专属属性(消费方 = 检查器曲线编辑器;值从
|
||||
// OfxParametricParameterSuite 的属性表搬运):
|
||||
// - "parametric_dimension":int,维度数;
|
||||
// - "parametric_range":Vec2,定义域 (lo, hi);
|
||||
// - "parametric_ui_colour":每维一条(重复键,对齐 combo_option
|
||||
// 惯例),Color([r,g,b,1]),取自 double×3N 属性,未配置不携带。
|
||||
// 无参数动画(第 1 期)→ 不可键帧。
|
||||
if matches!(value_type, ValueType::Parametric) {
|
||||
input.flags |= input_flags::NOT_KEYFRAMABLE;
|
||||
let dim = prop_int(&def.props, ofx::P_PARAMETRIC_DIMENSION, 0).max(1);
|
||||
input.properties.push((
|
||||
"parametric_dimension".to_string(),
|
||||
NodeValue::Int(dim as i64),
|
||||
));
|
||||
input.properties.push((
|
||||
"parametric_range".to_string(),
|
||||
NodeValue::Vec2([
|
||||
prop_double(&def.props, ofx::P_PARAMETRIC_RANGE, 0),
|
||||
prop_double(&def.props, ofx::P_PARAMETRIC_RANGE, 1),
|
||||
]),
|
||||
));
|
||||
let colour_dim = def.props.dimension(ofx::P_PARAMETRIC_UI_COLOUR);
|
||||
for i in 0..dim as usize {
|
||||
if i * 3 + 2 >= colour_dim {
|
||||
break;
|
||||
}
|
||||
input.properties.push((
|
||||
"parametric_ui_colour".to_string(),
|
||||
NodeValue::Color([
|
||||
prop_double(&def.props, ofx::P_PARAMETRIC_UI_COLOUR, i * 3),
|
||||
prop_double(&def.props, ofx::P_PARAMETRIC_UI_COLOUR, i * 3 + 1),
|
||||
prop_double(&def.props, ofx::P_PARAMETRIC_UI_COLOUR, i * 3 + 2),
|
||||
1.0,
|
||||
]),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(value_type, ValueType::Color) {
|
||||
let semantic = deduce_color_semantic(def, &group_labels);
|
||||
input.properties.push((
|
||||
@@ -757,23 +809,38 @@ pub fn register_plugin_nodes() -> Vec<String> {
|
||||
// 渲染执行器 + duplicator(依赖反转的 oakplugin 侧半环)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 字符串族参数注入(POD 无字符串表达;直接 set_ofx)。
|
||||
fn set_string_param(inst: &Instance, key: &str, expected_type: &str, value: &str) {
|
||||
/// 文本族参数注入(POD 无字符串表达;直接 set_ofx)。按参数的 OFX
|
||||
/// 类型分发:String → String、StrChoice → StrChoice;Parametric →
|
||||
/// 解析 JSON 曲线集([`crate::param_curve::curves_from_json`],宿主侧
|
||||
/// [`crate::param_curve::curves_to_json`] 的格式)写回
|
||||
/// `ParamValue::Parametric`——即"节点输入值变化 → 写回实例"的
|
||||
/// parametric 路径(渲染期参数覆盖的分支,见
|
||||
/// [`execute_plugin_job`])。JSON 解析失败 / 类型不符 → 忽略(与
|
||||
/// 字符串族类型不匹配静默一致)。
|
||||
fn set_text_param(inst: &Instance, key: &str, text: &str) {
|
||||
let Some(p) = inst.params.find(key) else {
|
||||
return;
|
||||
};
|
||||
if p.def.ofx_type != expected_type {
|
||||
return;
|
||||
match p.def.ofx_type.as_str() {
|
||||
ofx::TYPE_STRING => {
|
||||
let Ok(cs) = CString::new(text) else {
|
||||
return;
|
||||
};
|
||||
p.set_ofx(ParamValue::String(cs));
|
||||
}
|
||||
ofx::TYPE_STRCHOICE => {
|
||||
let Ok(cs) = CString::new(text) else {
|
||||
return;
|
||||
};
|
||||
p.set_ofx(ParamValue::StrChoice(cs));
|
||||
}
|
||||
ofx::TYPE_PARAMETRIC => {
|
||||
if let Some(curves) = crate::param_curve::curves_from_json(text) {
|
||||
p.set_ofx(ParamValue::Parametric(curves));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let Ok(cs) = CString::new(value) else {
|
||||
return;
|
||||
};
|
||||
let pv = if expected_type == ofx::TYPE_STRING {
|
||||
ParamValue::String(cs)
|
||||
} else {
|
||||
ParamValue::StrChoice(cs)
|
||||
};
|
||||
p.set_ofx(pv);
|
||||
}
|
||||
|
||||
/// executor 槽实现:JobSpec::Plugin → render_driver::render_frame。
|
||||
@@ -801,14 +868,15 @@ fn execute_plugin_job(
|
||||
return Err(Error::Failed("plugin job 无可用输入纹理".into()));
|
||||
}
|
||||
|
||||
// 参数注入:数值族走 render_driver 的 POD 覆盖;字符串族 POD 无
|
||||
// 表达,这里直接 set_ofx(对齐 pluginrenderer.cpp 的
|
||||
// StringInstance::set 分支)。
|
||||
// 参数注入:数值族走 render_driver 的 POD 覆盖;字符串/曲线族
|
||||
// POD 无表达,这里直接 set_ofx(对齐 pluginrenderer.cpp 的
|
||||
// StringInstance::set 分支 + parametric 的 JSON 写回)。
|
||||
let mut pod_values = Vec::new();
|
||||
for (key, nv) in values {
|
||||
match nv {
|
||||
NodeValue::Text(s) => set_string_param(&inst.value, key, ofx::TYPE_STRING, s),
|
||||
NodeValue::StrCombo(s) => set_string_param(&inst.value, key, ofx::TYPE_STRCHOICE, s),
|
||||
NodeValue::Text(s) | NodeValue::StrCombo(s) => {
|
||||
set_text_param(&inst.value, key, s)
|
||||
}
|
||||
NodeValue::PushButton | NodeValue::None => {}
|
||||
other => {
|
||||
if let Some(v) = crate::node::Value::from_node_value(other) {
|
||||
@@ -940,7 +1008,12 @@ mod tests {
|
||||
// 容器与未知类型:跳过。
|
||||
assert_eq!(input_type_for(ofx::TYPE_GROUP), None);
|
||||
assert_eq!(input_type_for(ofx::TYPE_PAGE), None);
|
||||
assert_eq!(input_type_for("OfxParamTypeParametric"), None);
|
||||
assert_eq!(input_type_for("OfxParamTypeBogus"), None);
|
||||
// parametric → Parametric(值 = 曲线 JSON 文本)。
|
||||
assert_eq!(
|
||||
input_type_for(ofx::TYPE_PARAMETRIC),
|
||||
Some(ValueType::Parametric)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1107,4 +1180,163 @@ mod tests {
|
||||
assert_eq!(calls, vec!["OfxActionInstanceChanged", "OfxChangeUserEdited"]);
|
||||
unregister_instance(id);
|
||||
}
|
||||
|
||||
/// 构造一个带 parametric 参数(维度 2、自定义 range、双维 UI 颜色
|
||||
/// 已配置)与一个 String 参数的实例(直接登记进注册表)。
|
||||
fn instance_with_parametric() -> u64 {
|
||||
use std::ffi::{c_char, c_void};
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use crate::descriptor::EffectDescriptor;
|
||||
use crate::handle::RefBox;
|
||||
use crate::host::Plugin;
|
||||
use crate::param::{ParamDef, ParamInstance, ParamSetInstance};
|
||||
|
||||
unsafe extern "C" fn dummy_entry(
|
||||
_: *const c_char,
|
||||
_: *const c_void,
|
||||
_: *mut c_void,
|
||||
_: *mut c_void,
|
||||
) -> i32 {
|
||||
0
|
||||
}
|
||||
let mut def = ParamDef::new("curve", ofx::TYPE_PARAMETRIC);
|
||||
def.props
|
||||
.set_one(ofx::PROP_LABEL, PropValue::String(CString::new("Curve").unwrap()));
|
||||
def.props
|
||||
.set_one(ofx::P_PARAMETRIC_DIMENSION, PropValue::Int(2));
|
||||
def.props.define(
|
||||
ofx::P_PARAMETRIC_RANGE,
|
||||
vec![PropValue::Double(0.0), PropValue::Double(1.0)],
|
||||
);
|
||||
def.props.define(
|
||||
ofx::P_PARAMETRIC_UI_COLOUR,
|
||||
vec![
|
||||
PropValue::Double(1.0),
|
||||
PropValue::Double(0.0),
|
||||
PropValue::Double(0.0),
|
||||
PropValue::Double(0.0),
|
||||
PropValue::Double(1.0),
|
||||
PropValue::Double(0.0),
|
||||
],
|
||||
);
|
||||
let mut params = ParamSetInstance { params: Vec::new() };
|
||||
params
|
||||
.params
|
||||
.push(Box::new(ParamInstance::from_def(def)));
|
||||
params.params.push(Box::new(ParamInstance::from_def(
|
||||
ParamDef::new("title", ofx::TYPE_STRING),
|
||||
)));
|
||||
let plugin = Arc::new(Plugin {
|
||||
identifier: "test.parametric".into(),
|
||||
version: (1, 0),
|
||||
bundle_path: std::path::PathBuf::new(),
|
||||
contexts: vec![],
|
||||
descriptor: EffectDescriptor::new(),
|
||||
lib: std::ptr::null_mut(),
|
||||
entry: dummy_entry,
|
||||
ofx_plugin: std::ptr::null_mut(),
|
||||
});
|
||||
let inst = crate::instance::Instance {
|
||||
props: crate::property::PropertySet::new(),
|
||||
plugin,
|
||||
context: "OfxImageEffectContextFilter".into(),
|
||||
params,
|
||||
clips: Vec::new(),
|
||||
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),
|
||||
};
|
||||
register_instance(Arc::new(RefBox {
|
||||
refs: AtomicU32::new(1),
|
||||
value: inst,
|
||||
}))
|
||||
}
|
||||
|
||||
/// 翻译 pass 产出 Parametric 输入:value_type、默认值(= 默认曲线
|
||||
/// 的 JSON)、display_name(= label)、不可键帧 + 专属属性
|
||||
/// (dimension / range / 每维一条 UI 颜色)。
|
||||
#[test]
|
||||
fn build_core_produces_parametric_input() {
|
||||
let id = instance_with_parametric();
|
||||
let inst = instance_from_id(id).expect("已登记");
|
||||
let core = build_core(&inst.value);
|
||||
|
||||
let input = core.get_input("curve").expect("parametric 输入应存在");
|
||||
assert_eq!(input.value_type, ValueType::Parametric);
|
||||
assert_eq!(input.display_name, "Curve");
|
||||
assert_ne!(input.flags & input_flags::NOT_KEYFRAMABLE, 0);
|
||||
|
||||
// 默认值 = def.default 曲线集的 JSON(type_default 产出 1 条
|
||||
// 恒等曲线;dimension 属性只做索引门控,未配置曲线按恒等读,
|
||||
// 不扩充默认值)。
|
||||
let expected = crate::param_curve::curves_to_json(&[crate::param_curve::Curve::identity(
|
||||
0.0, 1.0,
|
||||
)]);
|
||||
match &input.default {
|
||||
NodeValue::Text(s) => assert_eq!(s, &expected, "默认值应为曲线 JSON"),
|
||||
other => panic!("预期 Text(JSON),实际 {other:?}"),
|
||||
}
|
||||
// 标准值 = 默认(工程序列化从这里走)。
|
||||
assert_eq!(core.standard_value("curve", -1), input.default);
|
||||
|
||||
// 专属属性。
|
||||
let props = |name: &str| {
|
||||
input
|
||||
.properties
|
||||
.iter()
|
||||
.filter(|(k, _)| k == name)
|
||||
.map(|(_, v)| v.clone())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(props("parametric_dimension"), vec![NodeValue::Int(2)]);
|
||||
assert_eq!(
|
||||
props("parametric_range"),
|
||||
vec![NodeValue::Vec2([0.0, 1.0])]
|
||||
);
|
||||
// 双维 → 两条颜色(重复键)。
|
||||
assert_eq!(
|
||||
props("parametric_ui_colour"),
|
||||
vec![
|
||||
NodeValue::Color([1.0, 0.0, 0.0, 1.0]),
|
||||
NodeValue::Color([0.0, 1.0, 0.0, 1.0]),
|
||||
]
|
||||
);
|
||||
unregister_instance(id);
|
||||
}
|
||||
|
||||
/// parametric 输入(Text JSON)→ 实例写回:有效 JSON 解析并
|
||||
/// `set_ofx(Parametric)`;坏 JSON / 类型不符 → 静默忽略(保持现值)。
|
||||
#[test]
|
||||
fn parametric_input_writes_back_to_instance() {
|
||||
let id = instance_with_parametric();
|
||||
let inst = instance_from_id(id).expect("已登记");
|
||||
|
||||
// 有效 JSON(双维,第二维单点)→ 曲线写回,求值即实例曲线。
|
||||
let json = r#"{"curves":[[{"key":0,"value":0,"slope":1},{"key":0.5,"value":0.7,"slope":1}],[{"key":0,"value":0,"slope":1}]]}"#;
|
||||
set_text_param(&inst.value, "curve", json);
|
||||
let curves = match inst.value.params.find("curve").unwrap().get() {
|
||||
ParamValue::Parametric(c) => c,
|
||||
other => panic!("应写回 Parametric,实际 {other:?}"),
|
||||
};
|
||||
assert_eq!(curves[0].evaluate(0.5), 0.7);
|
||||
assert_eq!(curves[1].len(), 1);
|
||||
|
||||
// 坏 JSON → 不改值(与字符串族类型不匹配静默一致)。
|
||||
let before = inst.value.params.find("curve").unwrap().get();
|
||||
set_text_param(&inst.value, "curve", "{broken");
|
||||
assert_eq!(inst.value.params.find("curve").unwrap().get(), before);
|
||||
|
||||
// 非 parametric 参数(String)遇任意文本 → 字符串值路径。
|
||||
set_text_param(&inst.value, "title", "hello");
|
||||
assert!(matches!(
|
||||
inst.value.params.find("title").unwrap().get(),
|
||||
ParamValue::String(_)
|
||||
));
|
||||
unregister_instance(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,7 +585,10 @@ impl ParamInstance {
|
||||
/// COLOR→RGB/RGBA、VEC2/VEC3→Double(2D/3D)/Integer(2D/3D)。
|
||||
/// 维度不齐按 OFX 语义截断/补零(缺失元素按 0)。字符串族
|
||||
/// (OAKNODE_VALUE_STRING)的 POD 不携带数据——此路径不改值
|
||||
/// (走 facade 的字符串 API,见 `include/plugin/instance.h`)。
|
||||
/// (走 facade 的字符串 API,见 `include/plugin/instance.h`);
|
||||
/// parametric 同理(曲线 JSON 文本经渲染期注入
|
||||
/// [`crate::node_factory`] 的 set_text_param 写回,见
|
||||
/// `crate::node_factory::execute_plugin_job`)。
|
||||
/// 类型不匹配 → 忽略(保持现值;C++ `node_get` 失败时参数不回写)。
|
||||
pub fn set_from_node(&self, node_value: &crate::node::Value) {
|
||||
if let Some(pv) = param_value_from_node(node_value, &self.def.ofx_type) {
|
||||
@@ -726,7 +729,10 @@ impl ParamSetInstance {
|
||||
/// TimeChanged 是宿主侧变更,值已由 [`ParamInstance::set_from_node`]
|
||||
/// 同步,不重复写回;
|
||||
/// - 字符串族经 [`crate::node::set_input_string_undoable`]
|
||||
/// (POD 不携带字符串数据);
|
||||
/// (POD 不携带字符串数据);parametric 曲线同样经该字符串 setter
|
||||
/// 以 JSON 文本写回输入([`crate::param_curve::curves_to_json`],
|
||||
/// 与标量回写同一 undo 语义——插件在实例上改曲线 → 节点输入更新,
|
||||
/// 工程序列化随标准值保留);
|
||||
/// - 无值类(PushButton/Group/Page)与 Bytes 无节点对应 → no-op;
|
||||
/// - 编辑事务内([`crate::instance::Instance::in_edit`])并入 multi
|
||||
/// 命令,否则单命令立即 redo 生效(C++ `submit_undo_command`)。
|
||||
@@ -770,6 +776,14 @@ pub(crate) fn notify_instance_changed(
|
||||
instance.submit_undo_command(cmd, &label);
|
||||
}
|
||||
}
|
||||
// parametric:曲线序列化为 JSON 文本写回输入(节点输入是
|
||||
// Parametric 声明类型的 Text 值;经字符串 setter 存储)。
|
||||
ParamValue::Parametric(curves) => {
|
||||
let json = crate::param_curve::curves_to_json(curves);
|
||||
if let Ok(cmd) = node::set_input_string_undoable(&node_ref, param_name, &json) {
|
||||
instance.submit_undo_command(cmd, &label);
|
||||
}
|
||||
}
|
||||
_ => {} // 无值类 / Bytes:无节点对应
|
||||
},
|
||||
}
|
||||
|
||||
@@ -212,6 +212,256 @@ impl Curve {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 曲线集 ↔ 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::<usize>());
|
||||
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<Vec<Curve>> {
|
||||
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<String> {
|
||||
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<f64> {
|
||||
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<Curve> {
|
||||
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<ControlPoint> {
|
||||
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。
|
||||
@@ -390,4 +640,95 @@ mod tests {
|
||||
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::<Curve>::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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,8 +163,9 @@ fn curves_of(r: &ParamRef) -> Result<Vec<Curve>, c_int> {
|
||||
|
||||
/// 写回曲线:Def → 默认值;Instance → 当前值 + instanceChanged 通知
|
||||
/// (复用 param suite 的 [`crate::suites::param::notify_changed`]——
|
||||
/// 曲线无标量节点值,节点回写是 no-op,但走既有通知路径保持宿主侧
|
||||
/// 变更感知的纪律一致)。
|
||||
/// 曲线经 [`crate::param::notify_instance_changed`] 序列化为 JSON
|
||||
/// 文本写回节点输入(undoable,同标量路径),保持宿主侧变更感知的
|
||||
/// 纪律一致)。
|
||||
fn write_curves(r: &mut ParamRef, curves: Vec<Curve>) {
|
||||
match r {
|
||||
ParamRef::Def(d) => d.default = ParamValue::Parametric(curves),
|
||||
|
||||
Reference in New Issue
Block a user