Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/ oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain Rust, CHandle marshalling shrinks to the oakengine boundary, and tests call the Rust APIs directly (pure C-ABI wrapper tests removed where the domain layer already covers the behavior). exporter.h family implemented: oakengine_export_render (CLI contract), oakengine_export_render_with_params (was a stub), last_error and progress callback; synchronous path reuses task_create_export + start_sync. Fixes on the way: oaktask video ticket self-deadlock, audio params dropped on the export path, codec encoder AAC slicing and H.264 time base. Real-mp4 tests cover both entry points, progress and the illegal-argument matrix. Also: oakstorage session maps null project handles to None (version- info path), configstore test double literal 3.14 -> 3.15 (clippy PI lint), oakaudio output callback scratch buffer + env-aware P1 test, cli media round-trip test uses a generated 16-frame clip (no more minute-long debug runs).
835 lines
30 KiB
Rust
835 lines
30 KiB
Rust
// Oak Video Editor - Non-Linear Video Editor
|
||
// Copyright (C) 2026 Oak Team
|
||
//
|
||
// This program is free software: you can redistribute it and/or modify
|
||
// it under the terms of the GNU General Public License as published by
|
||
// the Free Software Foundation, either version 3 of the License, or
|
||
// (at your option) any later version.
|
||
//
|
||
// This program is distributed in the hope that it will be useful,
|
||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
// GNU General Public License for more details.
|
||
//
|
||
// You should have received a copy of the GNU General Public License
|
||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
||
//! 参数体系:12 种参数实例 + param ↔ oaknode 桥。
|
||
//!
|
||
//! 对应 C++ 的 `ParamInstance`/`OliveParamInstance`。桥的语义
|
||
//! (M9 已定):节点输入值变化 → 写回 OFX 参数;OFX 参数被插件
|
||
//! 改动 → 经 oaknode 回写节点(undoable 经
|
||
//! `oakundo::undocommand::UndoCommand`)。
|
||
//!
|
||
//! 句柄约定([`crate::suites::tag`]):`ParamDef`/`ParamInstance`
|
||
//! `#[repr(C)]` 且 `props` 在偏移 0;元素装箱(`Vec<Box<..>>`)保证
|
||
//! Vec 重分配不移动对象、句柄不悬垂。
|
||
//!
|
||
//! 节点值布局:随 [`crate::node`] 冻结(`include/node/node.h` 的
|
||
//! `oaknode_value` POD);单库化后 oaknode 的身份注册表随其 ffi
|
||
//! 删除,回写路径是保留失败路径的本地桩(见 [`crate::node`])。
|
||
|
||
use std::ffi::CString;
|
||
|
||
use crate::property::{PropertySet, Value};
|
||
|
||
// ---- OFX 参数类型字符串(ofxParam.h)----
|
||
|
||
/// kOfxParamTypeInteger。
|
||
pub const TYPE_INTEGER: &str = "OfxParamTypeInteger";
|
||
/// kOfxParamTypeInteger2D。
|
||
pub const TYPE_INTEGER2D: &str = "OfxParamTypeInteger2D";
|
||
/// kOfxParamTypeInteger3D。
|
||
pub const TYPE_INTEGER3D: &str = "OfxParamTypeInteger3D";
|
||
/// kOfxParamTypeDouble。
|
||
pub const TYPE_DOUBLE: &str = "OfxParamTypeDouble";
|
||
/// kOfxParamTypeDouble2D。
|
||
pub const TYPE_DOUBLE2D: &str = "OfxParamTypeDouble2D";
|
||
/// kOfxParamTypeDouble3D。
|
||
pub const TYPE_DOUBLE3D: &str = "OfxParamTypeDouble3D";
|
||
/// kOfxParamTypeBoolean。
|
||
pub const TYPE_BOOLEAN: &str = "OfxParamTypeBoolean";
|
||
/// kOfxParamTypeChoice。
|
||
pub const TYPE_CHOICE: &str = "OfxParamTypeChoice";
|
||
/// kOfxParamTypeString。
|
||
pub const TYPE_STRING: &str = "OfxParamTypeString";
|
||
/// kOfxParamTypeStrChoice。
|
||
pub const TYPE_STRCHOICE: &str = "OfxParamTypeStrChoice";
|
||
/// kOfxParamTypeRGB。
|
||
pub const TYPE_RGB: &str = "OfxParamTypeRGB";
|
||
/// kOfxParamTypeRGBA。
|
||
pub const TYPE_RGBA: &str = "OfxParamTypeRGBA";
|
||
/// kOfxParamTypeBytes。
|
||
pub const TYPE_BYTES: &str = "OfxParamTypeBytes";
|
||
/// kOfxParamTypeCustom。
|
||
pub const TYPE_CUSTOM: &str = "OfxParamTypeCustom";
|
||
/// kOfxParamTypePushButton。
|
||
pub const TYPE_PUSHBUTTON: &str = "OfxParamTypePushButton";
|
||
/// kOfxParamTypeGroup。
|
||
pub const TYPE_GROUP: &str = "OfxParamTypeGroup";
|
||
/// kOfxParamTypePage。
|
||
pub const TYPE_PAGE: &str = "OfxParamTypePage";
|
||
/// kOfxParamTypeParametric(第 1 期不支持,paramDefine 拒绝)。
|
||
pub const TYPE_PARAMETRIC: &str = "OfxParamTypeParametric";
|
||
|
||
// ---- 属性名(ofxParam.h / ofxCore.h)----
|
||
|
||
/// kOfxParamPropSecret。
|
||
pub(crate) const P_SECRET: &str = "OfxParamPropSecret";
|
||
/// kOfxParamPropHint。
|
||
pub(crate) const P_HINT: &str = "OfxParamPropHint";
|
||
/// kOfxParamPropScriptName。
|
||
pub(crate) const P_SCRIPT_NAME: &str = "OfxParamPropScriptName";
|
||
/// kOfxParamPropParent。
|
||
pub(crate) const P_PARENT: &str = "OfxParamPropParent";
|
||
/// kOfxParamPropEnabled。
|
||
pub(crate) const P_ENABLED: &str = "OfxParamPropEnabled";
|
||
/// kOfxParamPropDataPtr。
|
||
pub(crate) const P_DATA_PTR: &str = "OfxParamPropDataPtr";
|
||
/// kOfxParamPropType。
|
||
pub(crate) const P_TYPE: &str = "OfxParamPropType";
|
||
/// kOfxParamPropIsAnimating。
|
||
pub(crate) const P_IS_ANIMATING: &str = "OfxParamPropIsAnimating";
|
||
/// kOfxParamPropIsAutoKeying。
|
||
pub(crate) const P_IS_AUTO_KEYING: &str = "OfxParamPropIsAutoKeying";
|
||
/// kOfxParamPropPersistant。
|
||
pub(crate) const P_PERSISTANT: &str = "OfxParamPropPersistant";
|
||
/// kOfxParamPropEvaluateOnChange。
|
||
pub(crate) const P_EVALUATE_ON_CHANGE: &str = "OfxParamPropEvaluateOnChange";
|
||
/// kOfxParamPropCanUndo。
|
||
pub(crate) const P_CAN_UNDO: &str = "OfxParamPropCanUndo";
|
||
/// kOfxParamPropCacheInvalidation。
|
||
pub(crate) const P_CACHE_INVALIDATION: &str = "OfxParamPropCacheInvalidation";
|
||
/// kOfxParamInvalidateValueChange(P_CACHE_INVALIDATION 的默认值)。
|
||
pub(crate) const V_INVALIDATE_VALUE_CHANGE: &str = "OfxParamInvalidateValueChange";
|
||
/// kOfxParamPropAnimates。
|
||
pub(crate) const P_ANIMATES: &str = "OfxParamPropAnimates";
|
||
/// kOfxParamPropDefault。
|
||
pub(crate) const P_DEFAULT: &str = "OfxParamPropDefault";
|
||
/// kOfxParamPropDisplayMin。
|
||
pub(crate) const P_DISPLAY_MIN: &str = "OfxParamPropDisplayMin";
|
||
/// kOfxParamPropDisplayMax。
|
||
pub(crate) const P_DISPLAY_MAX: &str = "OfxParamPropDisplayMax";
|
||
/// kOfxParamPropMin。
|
||
pub(crate) const P_MIN: &str = "OfxParamPropMin";
|
||
/// kOfxParamPropMax。
|
||
pub(crate) const P_MAX: &str = "OfxParamPropMax";
|
||
/// kOfxParamPropIncrement。
|
||
pub(crate) const P_INCREMENT: &str = "OfxParamPropIncrement";
|
||
/// kOfxParamPropDigits。
|
||
pub(crate) const P_DIGITS: &str = "OfxParamPropDigits";
|
||
/// kOfxParamPropDoubleType。
|
||
pub(crate) const P_DOUBLE_TYPE: &str = "OfxParamPropDoubleType";
|
||
/// kOfxParamDoubleTypePlain。
|
||
pub(crate) const V_DOUBLE_TYPE_PLAIN: &str = "OfxParamDoubleTypePlain";
|
||
/// kOfxParamPropDefaultCoordinateSystem。
|
||
pub(crate) const P_DEFAULT_COORD_SYS: &str = "OfxParamPropDefaultCoordinateSystem";
|
||
/// kOfxParamCoordinatesCanonical。
|
||
pub(crate) const V_COORD_CANONICAL: &str = "OfxParamCoordinatesCanonical";
|
||
/// kOfxParamPropShowTimeMarker。
|
||
pub(crate) const P_SHOW_TIME_MARKER: &str = "OfxParamPropShowTimeMarker";
|
||
/// kOfxParamPropDimensionLabel。
|
||
pub(crate) const P_DIMENSION_LABEL: &str = "OfxParamPropDimensionLabel";
|
||
/// kOfxParamPropStringMode。
|
||
pub(crate) const P_STRING_MODE: &str = "OfxParamPropStringMode";
|
||
/// kOfxParamStringIsSingleLine。
|
||
pub(crate) const V_STRING_SINGLE_LINE: &str = "OfxParamStringIsSingleLine";
|
||
/// kOfxParamPropStringFilePathExists。
|
||
pub(crate) const P_STRING_FILE_EXISTS: &str = "OfxParamPropStringFilePathExists";
|
||
/// kOfxParamPropChoiceOption。
|
||
pub(crate) const P_CHOICE_OPTION: &str = "OfxParamPropChoiceOption";
|
||
/// kOfxParamPropCustomInterpCallbackV1。
|
||
pub(crate) const P_CUSTOM_INTERP: &str = "OfxParamPropCustomCallbackV1";
|
||
/// kOfxParamPropPageChild。
|
||
pub(crate) const P_PAGE_CHILD: &str = "OfxParamPropPageChild";
|
||
/// kOfxParamPropGroupOpen。
|
||
pub(crate) const P_GROUP_OPEN: &str = "OfxParamPropGroupOpen";
|
||
/// kOfxParamPropInteractV1。
|
||
pub(crate) const P_INTERACT_V1: &str = "OfxParamPropInteractV1";
|
||
/// kOfxParamPropInteractSize。
|
||
pub(crate) const P_INTERACT_SIZE: &str = "OfxParamPropInteractSize";
|
||
/// kOfxParamPropInteractSizeAspect。
|
||
pub(crate) const P_INTERACT_ASPECT: &str = "OfxParamPropInteractSizeAspect";
|
||
/// kOfxParamPropInteractMinimumSize。
|
||
pub(crate) const P_INTERACT_MIN_SIZE: &str = "OfxParamPropInteractMinimumSize";
|
||
/// kOfxParamPropInteractPreferedSize。
|
||
pub(crate) const P_INTERACT_PREF_SIZE: &str = "OfxParamPropInteractPreferedSize";
|
||
/// kOfxPropType。
|
||
pub(crate) const PROP_TYPE: &str = "OfxPropType";
|
||
/// kOfxPropName。
|
||
pub(crate) const PROP_NAME: &str = "OfxPropName";
|
||
/// kOfxPropLabel。
|
||
pub(crate) const PROP_LABEL: &str = "OfxPropLabel";
|
||
/// kOfxPropShortLabel。
|
||
pub(crate) const PROP_SHORT_LABEL: &str = "OfxPropShortLabel";
|
||
/// kOfxPropLongLabel。
|
||
pub(crate) const PROP_LONG_LABEL: &str = "OfxPropLongLabel";
|
||
/// kOfxPropIcon。
|
||
pub(crate) const PROP_ICON: &str = "OfxPropIcon";
|
||
/// kOfxTypeParameter。
|
||
pub(crate) const TYPE_PARAMETER: &str = "OfxTypeParameter";
|
||
|
||
fn cs(s: &str) -> CString {
|
||
// 静态 ASCII 常量,无内嵌 NUL。
|
||
CString::new(s).unwrap()
|
||
}
|
||
|
||
/// 参数值类别(param shim 的变长参数分发;与
|
||
/// cbits/ofx_param_shim.c 的 KIND_* 枚举逐字对应)。
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum ParamKind {
|
||
/// Integer。
|
||
Int = 1,
|
||
/// Integer2D。
|
||
Int2 = 2,
|
||
/// Integer3D。
|
||
Int3 = 3,
|
||
/// Double。
|
||
Double = 4,
|
||
/// Double2D。
|
||
Double2 = 5,
|
||
/// Double3D。
|
||
Double3 = 6,
|
||
/// Boolean。
|
||
Bool = 7,
|
||
/// Choice。
|
||
Choice = 8,
|
||
/// RGB。
|
||
Rgb = 9,
|
||
/// RGBA。
|
||
Rgba = 10,
|
||
/// String。
|
||
Str = 11,
|
||
/// StrChoice。
|
||
StrChoice = 12,
|
||
}
|
||
|
||
/// OFX 类型字符串 → 值类别 + 维度(HS: ofxhParam.cpp `findType`)。
|
||
/// 无值类(PushButton/Group/Page/Unknown)返回 None。
|
||
pub(crate) fn kind_of_type(ofx_type: &str) -> Option<(ParamKind, usize)> {
|
||
match ofx_type {
|
||
TYPE_INTEGER => Some((ParamKind::Int, 1)),
|
||
TYPE_INTEGER2D => Some((ParamKind::Int2, 2)),
|
||
TYPE_INTEGER3D => Some((ParamKind::Int3, 3)),
|
||
TYPE_DOUBLE => Some((ParamKind::Double, 1)),
|
||
TYPE_DOUBLE2D => Some((ParamKind::Double2, 2)),
|
||
TYPE_DOUBLE3D => Some((ParamKind::Double3, 3)),
|
||
TYPE_BOOLEAN => Some((ParamKind::Bool, 1)),
|
||
TYPE_CHOICE => Some((ParamKind::Choice, 1)),
|
||
TYPE_STRING => Some((ParamKind::Str, 1)),
|
||
TYPE_STRCHOICE => Some((ParamKind::StrChoice, 1)),
|
||
TYPE_RGB => Some((ParamKind::Rgb, 3)),
|
||
TYPE_RGBA => Some((ParamKind::Rgba, 4)),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// 数值类参数(Min/Max/DisplayMin/Max 表格适用;HS
|
||
/// `addNumericParamProps` 的 isDoubleParam || isIntParam ||
|
||
/// isColourParam)。
|
||
fn is_numeric_type(ofx_type: &str) -> bool {
|
||
matches!(
|
||
ofx_type,
|
||
TYPE_INTEGER
|
||
| TYPE_INTEGER2D
|
||
| TYPE_INTEGER3D
|
||
| TYPE_DOUBLE
|
||
| TYPE_DOUBLE2D
|
||
| TYPE_DOUBLE3D
|
||
| TYPE_RGB
|
||
| TYPE_RGBA
|
||
)
|
||
}
|
||
|
||
/// 该类型的默认 ParamValue(describe 期基线;HS 的属性默认在 props
|
||
/// 的 kOfxParamPropDefault,值语义等价)。
|
||
fn type_default(ofx_type: &str) -> ParamValue {
|
||
match kind_of_type(ofx_type) {
|
||
Some((ParamKind::Int, d)) | Some((ParamKind::Int2, d)) | Some((ParamKind::Int3, d)) => {
|
||
ParamValue::Int([0; 3], d)
|
||
}
|
||
Some((ParamKind::Double, d))
|
||
| Some((ParamKind::Double2, d))
|
||
| Some((ParamKind::Double3, d)) => ParamValue::Double([0.0; 3], d),
|
||
Some((ParamKind::Rgb, d)) | Some((ParamKind::Rgba, d)) => ParamValue::Color([0.0; 4], d),
|
||
Some((ParamKind::Bool, _)) => ParamValue::Bool(false),
|
||
Some((ParamKind::Choice, _)) => ParamValue::Choice(0),
|
||
Some((ParamKind::Str, _)) => ParamValue::String(cs("")),
|
||
Some((ParamKind::StrChoice, _)) => ParamValue::StrChoice(cs("")),
|
||
None => match ofx_type {
|
||
TYPE_BYTES | TYPE_CUSTOM => ParamValue::Bytes(Vec::new()),
|
||
TYPE_PUSHBUTTON => ParamValue::PushButton,
|
||
TYPE_GROUP | TYPE_PAGE => ParamValue::Container,
|
||
_ => ParamValue::Container, // 未知类型占位(paramDefine 已拒绝)
|
||
},
|
||
}
|
||
}
|
||
|
||
/// 数值属性表的值(Min/Max/Display 族;HS ofxhParam.cpp:420-449)。
|
||
/// `is_max` 选 ±f64::MAX / ±i32::MAX;colour 参数恒 0/1。
|
||
fn numeric_value(ofx_type: &str, kind: ParamKind, dim: usize, is_max: bool) -> Vec<Value> {
|
||
if ofx_type == TYPE_RGB || ofx_type == TYPE_RGBA {
|
||
// colour:display 范围 0..1,min/max 同。
|
||
let v = Value::Double(if is_max { 1.0 } else { 0.0 });
|
||
return vec![v; dim];
|
||
}
|
||
match kind {
|
||
ParamKind::Double | ParamKind::Double2 | ParamKind::Double3 => {
|
||
let v = Value::Double(if is_max { f64::MAX } else { f64::MIN });
|
||
vec![v; dim]
|
||
}
|
||
_ => {
|
||
let v = Value::Int(if is_max { i32::MAX } else { i32::MIN });
|
||
vec![v; dim]
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 参数值(与 OFX 参数类型一一对应)。
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub enum ParamValue {
|
||
/// kOfxParamTypeInteger / Integer2D / Integer3D。
|
||
Int([i32; 3], usize),
|
||
/// kOfxParamTypeDouble / Double2D / Double3D。
|
||
Double([f64; 3], usize),
|
||
/// kOfxParamTypeBoolean。
|
||
Bool(bool),
|
||
/// kOfxParamTypeChoice(选项索引)。
|
||
Choice(i32),
|
||
/// kOfxParamTypeString。
|
||
String(std::ffi::CString),
|
||
/// kOfxParamTypeRGB / RGBA。
|
||
Color([f64; 4], usize),
|
||
/// kOfxParamTypeStrChoice。
|
||
StrChoice(std::ffi::CString),
|
||
/// kOfxParamTypeBytes / Custom(不透明字节串)。
|
||
Bytes(Vec<u8>),
|
||
/// kOfxParamTypePushButton(无值,仅触发)。
|
||
PushButton,
|
||
/// kOfxParamTypeGroup / Page(容器,无值)。
|
||
Container,
|
||
}
|
||
|
||
/// 参数定义(describe 产物,见 [`crate::descriptor::EffectDescriptor`])。
|
||
/// `#[repr(C)]` + props 在偏移 0(句柄约定,见 [`crate::suites::tag`])。
|
||
#[derive(Clone)]
|
||
#[repr(C)]
|
||
pub struct ParamDef {
|
||
/// 定义属性(label/hint/parent/coordinate-system/secret/
|
||
/// display min-max/choice 选项与排序等;describe 期预置
|
||
/// HostSupport 同款表格)。
|
||
pub props: PropertySet,
|
||
/// 参数名(OFX 标识)。
|
||
pub name: String,
|
||
/// OFX 类型字符串(kOfxParamType*)。
|
||
pub ofx_type: String,
|
||
/// 默认值(describe 后读取)。
|
||
pub default: ParamValue,
|
||
}
|
||
|
||
impl ParamDef {
|
||
/// 按 OFX 类型构建定义(镜像 HS ofxhParam.cpp:224-352:
|
||
/// universalProps + addStandardParamProps 的分类型属性表)。
|
||
pub(crate) fn new(name: &str, ofx_type: &str) -> Self {
|
||
let mut props = PropertySet::new();
|
||
let uname = name.to_string();
|
||
let utype = ofx_type.to_string();
|
||
|
||
// universalProps(HS ofxhParam.cpp:229-246)。
|
||
props.set_one(PROP_TYPE, Value::String(cs(TYPE_PARAMETER)));
|
||
props.set_one(P_SECRET, Value::Int(0));
|
||
props.set_one(P_HINT, Value::String(cs("")));
|
||
props.set_one(P_SCRIPT_NAME, Value::String(cs(&uname)));
|
||
props.set_one(P_PARENT, Value::String(cs("")));
|
||
props.set_one(P_ENABLED, Value::Int(1));
|
||
props.set_one(P_DATA_PTR, Value::Pointer(std::ptr::null_mut()));
|
||
props.set_one(P_TYPE, Value::String(cs(&utype)));
|
||
props.set_one(PROP_NAME, Value::String(cs(&uname)));
|
||
props.set_one(PROP_LABEL, Value::String(cs(&uname)));
|
||
props.set_one(PROP_SHORT_LABEL, Value::String(cs(&uname)));
|
||
props.set_one(PROP_LONG_LABEL, Value::String(cs(&uname)));
|
||
props.define(
|
||
PROP_ICON,
|
||
vec![Value::String(cs("")), Value::String(cs(""))],
|
||
);
|
||
|
||
// 值类参数(HS addValueParamProps,ofxhParam.cpp:352-380)。
|
||
if let Some((kind, dim)) = kind_of_type(ofx_type) {
|
||
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)),
|
||
);
|
||
// 可动画性(HS ofxhParam.cpp:367-372:custom/string/
|
||
// boolean/choice 默认不可动画)。
|
||
let animates = ofx_type != TYPE_CUSTOM
|
||
&& ofx_type != TYPE_STRING
|
||
&& ofx_type != TYPE_BOOLEAN
|
||
&& ofx_type != TYPE_CHOICE;
|
||
props.set_one(P_ANIMATES, Value::Int(animates as i32));
|
||
props.define(P_DEFAULT, default_values(kind, dim));
|
||
|
||
// 数值类(HS addNumericParamProps,ofxhParam.cpp:391-455)。
|
||
if is_numeric_type(ofx_type) {
|
||
props.define(P_DISPLAY_MIN, numeric_value(ofx_type, kind, dim, false));
|
||
props.define(P_DISPLAY_MAX, numeric_value(ofx_type, kind, dim, true));
|
||
props.define(P_MIN, numeric_value(ofx_type, kind, dim, false));
|
||
props.define(P_MAX, numeric_value(ofx_type, kind, dim, true));
|
||
if matches!(
|
||
kind,
|
||
ParamKind::Double | ParamKind::Double2 | ParamKind::Double3
|
||
) {
|
||
props.set_one(P_INCREMENT, Value::Double(1.0));
|
||
props.set_one(P_DIGITS, Value::Int(2));
|
||
}
|
||
if ofx_type == TYPE_DOUBLE || ofx_type == TYPE_DOUBLE2D || ofx_type == TYPE_DOUBLE3D
|
||
{
|
||
props.set_one(P_DOUBLE_TYPE, Value::String(cs(V_DOUBLE_TYPE_PLAIN)));
|
||
props.set_one(P_DEFAULT_COORD_SYS, Value::String(cs(V_COORD_CANONICAL)));
|
||
if dim == 1 {
|
||
props.set_one(P_SHOW_TIME_MARKER, Value::Int(0));
|
||
}
|
||
}
|
||
if dim == 2 || dim == 3 {
|
||
let labels: Vec<Value> = ["x", "y", "z"][..dim]
|
||
.iter()
|
||
.map(|l| Value::String(cs(l)))
|
||
.collect();
|
||
props.define(P_DIMENSION_LABEL, labels);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 分类型附加(HS addStandardParamProps,ofxhParam.cpp:248-301)。
|
||
match ofx_type {
|
||
TYPE_STRING => {
|
||
props.set_one(P_STRING_MODE, Value::String(cs(V_STRING_SINGLE_LINE)));
|
||
props.set_one(P_STRING_FILE_EXISTS, Value::Int(1));
|
||
}
|
||
TYPE_CHOICE => {
|
||
// 维度 0:选项数由插件 SetN 决定。
|
||
props.define(P_CHOICE_OPTION, vec![]);
|
||
}
|
||
TYPE_CUSTOM => {
|
||
props.set_one(P_CUSTOM_INTERP, Value::Pointer(std::ptr::null_mut()));
|
||
}
|
||
TYPE_PAGE => {
|
||
props.define(P_PAGE_CHILD, vec![]);
|
||
}
|
||
TYPE_GROUP => {
|
||
props.set_one(P_GROUP_OPEN, Value::Int(1));
|
||
}
|
||
_ => {}
|
||
}
|
||
|
||
// 交互属性(HS addInteractParamProps,ofxhParam.cpp:305-317;
|
||
// group/page 除外)。
|
||
if ofx_type != TYPE_GROUP && ofx_type != TYPE_PAGE {
|
||
props.set_one(P_INTERACT_V1, Value::Pointer(std::ptr::null_mut()));
|
||
props.define(
|
||
P_INTERACT_SIZE,
|
||
vec![Value::Double(0.0), Value::Double(0.0)],
|
||
);
|
||
props.set_one(P_INTERACT_ASPECT, Value::Double(1.0));
|
||
props.define(
|
||
P_INTERACT_MIN_SIZE,
|
||
vec![Value::Double(10.0), Value::Double(10.0)],
|
||
);
|
||
props.define(P_INTERACT_PREF_SIZE, vec![Value::Int(10), Value::Int(10)]);
|
||
}
|
||
|
||
Self {
|
||
props,
|
||
name: uname,
|
||
ofx_type: utype,
|
||
default: type_default(ofx_type),
|
||
}
|
||
}
|
||
|
||
/// 值类别(变长参数分发用;无值类返回 None)。
|
||
pub(crate) fn kind(&self) -> Option<ParamKind> {
|
||
kind_of_type(&self.ofx_type).map(|(k, _)| k)
|
||
}
|
||
}
|
||
|
||
/// kOfxParamPropDefault 的属性值(HS ofxhParam.cpp:376-377)。
|
||
fn default_values(kind: ParamKind, dim: usize) -> Vec<Value> {
|
||
match kind {
|
||
ParamKind::Int
|
||
| ParamKind::Int2
|
||
| ParamKind::Int3
|
||
| ParamKind::Bool
|
||
| ParamKind::Choice => {
|
||
vec![Value::Int(0); dim.max(1)]
|
||
}
|
||
ParamKind::Double
|
||
| ParamKind::Double2
|
||
| ParamKind::Double3
|
||
| ParamKind::Rgb
|
||
| ParamKind::Rgba => vec![Value::Double(0.0); dim.max(1)],
|
||
ParamKind::Str | ParamKind::StrChoice => vec![Value::String(cs(""))],
|
||
}
|
||
}
|
||
|
||
/// 参数实例(createInstance 后由 [`ParamDef`] 实例化)。
|
||
/// `#[repr(C)]` + props 在偏移 0(句柄约定)。
|
||
#[repr(C)]
|
||
pub struct ParamInstance {
|
||
/// 实例级属性(当前值镜像等)。
|
||
pub props: PropertySet,
|
||
/// 对应定义。
|
||
pub def: ParamDef,
|
||
/// 当前值(节点桥与 OFX 侧的同步点)。
|
||
value: std::sync::Mutex<ParamValue>,
|
||
}
|
||
|
||
impl ParamInstance {
|
||
/// 从定义实例化(createInstance 路径调用)。实例 props 是定义
|
||
/// props 的深拷贝(HS: SetInstance::makeParam 的 descriptor 属性
|
||
/// 复制语义——插件在实例期读 label/min/max 等)。
|
||
pub fn from_def(def: ParamDef) -> Self {
|
||
let value = def.default.clone();
|
||
let props = def.props.clone();
|
||
Self {
|
||
props,
|
||
def,
|
||
value: std::sync::Mutex::new(value),
|
||
}
|
||
}
|
||
|
||
/// 读当前值。
|
||
pub fn get(&self) -> ParamValue {
|
||
self.value.lock().unwrap_or_else(|e| e.into_inner()).clone()
|
||
}
|
||
|
||
/// 写当前值(OFX 语义:paramSetValue action 语义,不打节点桥)。
|
||
pub fn set_ofx(&self, value: ParamValue) {
|
||
*self.value.lock().unwrap_or_else(|e| e.into_inner()) = value;
|
||
}
|
||
|
||
/// 字符串值的内驻指针(param suite 的 get_string 用;指向实例
|
||
/// 存储,下次 set 前有效——克隆的 CString 指针会悬垂,不能给)。
|
||
pub(crate) fn string_ptr(&self) -> Option<*const std::ffi::c_char> {
|
||
let v = self.value.lock().unwrap_or_else(|e| e.into_inner());
|
||
match &*v {
|
||
ParamValue::String(s) | ParamValue::StrChoice(s) => Some(s.as_ptr()),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// 从 oaknode 输入值写入(节点→插件方向)。类型映射表逐行对照
|
||
/// C++ 的 `valueconvert`/`paraminstance.h`(`node_get` 的镜像):
|
||
/// INT→Integer、FLOAT→Double、BOOL→Boolean、COMBO→Choice、
|
||
/// COLOR→RGB/RGBA、VEC2/VEC3→Double(2D/3D)/Integer(2D/3D)。
|
||
/// 维度不齐按 OFX 语义截断/补零(缺失元素按 0)。字符串族
|
||
/// (OAKNODE_VALUE_STRING)的 POD 不携带数据——此路径不改值
|
||
/// (走 facade 的字符串 API,见 `include/plugin/instance.h`)。
|
||
/// 类型不匹配 → 忽略(保持现值;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) {
|
||
self.set_ofx(pv);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 节点值 → ParamValue(按参数类型解释;字符串族走专用路径)。
|
||
/// 镜像 `set_from_node` 的映射表;render 驱动的参数覆盖
|
||
/// (apply_param_overrides)复用同一转换。
|
||
pub(crate) fn param_value_from_node(
|
||
v: &crate::node::Value,
|
||
ofx_type: &str,
|
||
) -> Option<ParamValue> {
|
||
use crate::node::node_value_type as T;
|
||
match ofx_type {
|
||
TYPE_DOUBLE => (v.r#type == T::FLOAT).then(|| ParamValue::Double([v.f[0], 0.0, 0.0], 1)),
|
||
TYPE_DOUBLE2D => (v.r#type == T::VEC2).then(|| ParamValue::Double([v.f[0], v.f[1], 0.0], 2)),
|
||
TYPE_DOUBLE3D => {
|
||
(v.r#type == T::VEC3).then(|| ParamValue::Double([v.f[0], v.f[1], v.f[2]], 3))
|
||
}
|
||
TYPE_INTEGER => (v.r#type == T::INT).then(|| ParamValue::Int([v.num as i32, 0, 0], 1)),
|
||
TYPE_INTEGER2D | TYPE_INTEGER3D => {
|
||
let dim = if ofx_type == TYPE_INTEGER2D { 2 } else { 3 };
|
||
Some(ParamValue::Int(
|
||
[v.f[0] as i32, v.f[1] as i32, v.f[2] as i32],
|
||
dim,
|
||
))
|
||
}
|
||
TYPE_BOOLEAN => (v.r#type == T::BOOL).then(|| ParamValue::Bool(v.num != 0)),
|
||
TYPE_CHOICE => (v.r#type == T::COMBO).then(|| ParamValue::Choice(v.num as i32)),
|
||
TYPE_RGB => {
|
||
(v.r#type == T::COLOR).then(|| ParamValue::Color([v.f[0], v.f[1], v.f[2], 0.0], 3))
|
||
}
|
||
TYPE_RGBA => (v.r#type == T::COLOR)
|
||
.then(|| ParamValue::Color([v.f[0], v.f[1], v.f[2], v.f[3]], 4)),
|
||
_ => None, // 字符串/无值类:POD 无数据,不改值
|
||
}
|
||
}
|
||
|
||
/// ParamValue → [`crate::node::Value`](插件→节点方向;
|
||
/// 字符串族经 [`crate::node::set_input_string_undoable`])。
|
||
/// 镜像 C++ `paraminstance.h` 的 `value_int`/`value_double`/
|
||
/// `value_vec`/`value_color` 构造:RGB 颜色补 alpha=1(C++
|
||
/// `RGBInstance::set` 的 `value_color(r,g,b,1.0)`)。无值类与 Bytes
|
||
/// 无节点对应 → None。
|
||
pub(crate) fn to_node_value(v: &ParamValue) -> Option<crate::node::Value> {
|
||
use crate::node::node_value_type as T;
|
||
let mut out = crate::node::Value::default();
|
||
match v {
|
||
ParamValue::Double(d, 1) => {
|
||
out.r#type = T::FLOAT;
|
||
out.f = [d[0], 0.0, 0.0, 0.0];
|
||
}
|
||
ParamValue::Double(d, 2) => {
|
||
out.r#type = T::VEC2;
|
||
out.f = [d[0], d[1], 0.0, 0.0];
|
||
}
|
||
ParamValue::Double(d, 3) => {
|
||
out.r#type = T::VEC3;
|
||
out.f = [d[0], d[1], d[2], 0.0];
|
||
}
|
||
ParamValue::Double(d, _) => {
|
||
out.r#type = T::FLOAT;
|
||
out.f = [d[0], 0.0, 0.0, 0.0];
|
||
}
|
||
ParamValue::Int(a, 1) => {
|
||
out.r#type = T::INT;
|
||
out.num = a[0] as i64;
|
||
}
|
||
ParamValue::Int(a, 2) => {
|
||
out.r#type = T::VEC2;
|
||
out.f = [a[0] as f64, a[1] as f64, 0.0, 0.0];
|
||
}
|
||
ParamValue::Int(a, 3) => {
|
||
out.r#type = T::VEC3;
|
||
out.f = [a[0] as f64, a[1] as f64, a[2] as f64, 0.0];
|
||
}
|
||
ParamValue::Bool(b) => {
|
||
out.r#type = T::BOOL;
|
||
out.num = *b as i64;
|
||
}
|
||
ParamValue::Choice(c) => {
|
||
out.r#type = T::COMBO;
|
||
out.num = *c as i64;
|
||
}
|
||
ParamValue::Color(c, 3) => {
|
||
out.r#type = T::COLOR;
|
||
out.f = [c[0], c[1], c[2], 1.0];
|
||
}
|
||
ParamValue::Color(c, 4) => {
|
||
out.r#type = T::COLOR;
|
||
out.f = *c;
|
||
}
|
||
_ => return None,
|
||
}
|
||
Some(out)
|
||
}
|
||
|
||
/// 参数实例集(实例级)。
|
||
pub struct ParamSetInstance {
|
||
/// 全部参数(定义顺序稳定;Box 保证句柄地址稳定)。
|
||
pub params: Vec<Box<ParamInstance>>,
|
||
}
|
||
|
||
impl ParamSetInstance {
|
||
/// 按名查找。
|
||
pub fn find(&self, name: &str) -> Option<&ParamInstance> {
|
||
self.params
|
||
.iter()
|
||
.find(|p| p.def.name == name)
|
||
.map(|b| b.as_ref())
|
||
}
|
||
|
||
/// 快照(内省 C ABI 与快照测试用)。
|
||
///
|
||
/// 声明原为 `Vec<(&str, &ParamValue)>`:值在 Mutex 内,无法返回
|
||
/// 活引用——改为拥有型 `(String, ParamValue)`(内省语义不变)。
|
||
pub fn snapshot(&self) -> Vec<(String, ParamValue)> {
|
||
self.params
|
||
.iter()
|
||
.map(|p| (p.def.name.clone(), p.get()))
|
||
.collect()
|
||
}
|
||
}
|
||
|
||
/// 插件 → 节点方向的回写入口(instanceChanged action 触发)。
|
||
/// 经 [`crate::node`] 定位绑定节点(身份注册表),再经
|
||
/// `oakundo::undocommand::UndoCommand` 包成 undoable 修改。未绑定
|
||
/// 节点或身份查无时 no-op(单库化后身份注册表重建为
|
||
/// [`crate::node::register_node`]/[`crate::node::node_from_identity`];
|
||
/// facade 装配期登记,project 释放后弱条目自然失效)。
|
||
///
|
||
/// 语义对照 C++ `paraminstance.h` 的 `set()` 路径(`detail::node_set`/
|
||
/// `node_set_at`):
|
||
/// - 只回写 [`ChangeReason::PluginEdited`](插件自改)。UserEdited/
|
||
/// TimeChanged 是宿主侧变更,值已由 [`ParamInstance::set_from_node`]
|
||
/// 同步,不重复写回;
|
||
/// - 字符串族经 [`crate::node::set_input_string_undoable`]
|
||
/// (POD 不携带字符串数据);
|
||
/// - 无值类(PushButton/Group/Page)与 Bytes 无节点对应 → no-op;
|
||
/// - 编辑事务内([`crate::instance::Instance::in_edit`])并入 multi
|
||
/// 命令,否则单命令立即 redo 生效(C++ `submit_undo_command`)。
|
||
pub(crate) fn notify_instance_changed(
|
||
instance: &crate::instance::Instance,
|
||
param_name: &str,
|
||
reason: ChangeReason,
|
||
) {
|
||
use crate::node;
|
||
use std::sync::atomic::Ordering;
|
||
|
||
if !matches!(reason, ChangeReason::PluginEdited) {
|
||
return;
|
||
}
|
||
// 未绑定节点 → no-op(C++:!node_.ctx 时 set 只写本地值)。
|
||
let node_id = instance.node_identity.load(Ordering::Relaxed);
|
||
if node_id == 0 {
|
||
return;
|
||
}
|
||
// 身份查无(未登记 / project 已释放)→ no-op。
|
||
let Some(node_ref) = node::node_from_identity(node_id) else {
|
||
return;
|
||
};
|
||
let Some(param) = instance.params.find(param_name) else {
|
||
return;
|
||
};
|
||
let value = param.get();
|
||
let label = format!("Change {param_name}");
|
||
match to_node_value(&value) {
|
||
Some(nv) => {
|
||
if let Ok(cmd) = node::set_input_undoable(&node_ref, param_name, &nv) {
|
||
instance.submit_undo_command(cmd, &label);
|
||
}
|
||
}
|
||
None => match &value {
|
||
ParamValue::String(s) | ParamValue::StrChoice(s) => {
|
||
let Ok(s) = s.to_str() else {
|
||
return;
|
||
};
|
||
if let Ok(cmd) = node::set_input_string_undoable(&node_ref, param_name, s) {
|
||
instance.submit_undo_command(cmd, &label);
|
||
}
|
||
}
|
||
_ => {} // 无值类 / Bytes:无节点对应
|
||
},
|
||
}
|
||
}
|
||
|
||
/// instanceChanged 的原因(OFX kOfxChange*)。
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub enum ChangeReason {
|
||
/// 用户编辑。
|
||
UserEdited,
|
||
/// 插件自改。
|
||
PluginEdited,
|
||
/// 时间变化。
|
||
TimeChanged,
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::node::node_value_type as T;
|
||
|
||
fn param(ofx_type: &str) -> ParamInstance {
|
||
ParamInstance::from_def(ParamDef::new("p", ofx_type))
|
||
}
|
||
|
||
/// ParamValue → oaknode Value 的类型映射表(镜像 C++
|
||
/// paraminstance.h 的 value_* 构造)。
|
||
#[test]
|
||
fn to_node_value_mapping() {
|
||
use crate::node::Value;
|
||
let c = |t: i32| Value {
|
||
r#type: t,
|
||
num: 0,
|
||
den: 0,
|
||
f: [0.0; 4],
|
||
};
|
||
// Double 族。
|
||
let v = to_node_value(&ParamValue::Double([1.5, 0.0, 0.0], 1)).unwrap();
|
||
assert_eq!(v, Value::float(1.5));
|
||
let v = to_node_value(&ParamValue::Double([1.0, 2.0, 0.0], 2)).unwrap();
|
||
assert_eq!(v, Value::vec(&[1.0, 2.0]));
|
||
let v = to_node_value(&ParamValue::Double([1.0, 2.0, 3.0], 3)).unwrap();
|
||
assert_eq!(v, Value::vec(&[1.0, 2.0, 3.0]));
|
||
// Int 族(2D/3D 升格为 VEC2/VEC3 浮点载荷)。
|
||
let v = to_node_value(&ParamValue::Int([7, 0, 0], 1)).unwrap();
|
||
assert_eq!(v, Value::int(7));
|
||
let v = to_node_value(&ParamValue::Int([1, 2, 0], 2)).unwrap();
|
||
assert_eq!(v, Value::vec(&[1.0, 2.0]));
|
||
// Bool / Choice。
|
||
assert_eq!(
|
||
to_node_value(&ParamValue::Bool(true)).unwrap(),
|
||
Value::bool_(true)
|
||
);
|
||
assert_eq!(
|
||
to_node_value(&ParamValue::Choice(3)).unwrap(),
|
||
Value::combo(3)
|
||
);
|
||
// Color:RGB 补 alpha=1(C++ oracle),RGBA 全透传。
|
||
let v = to_node_value(&ParamValue::Color([0.1, 0.2, 0.3, 0.0], 3)).unwrap();
|
||
assert_eq!(v, Value::color(0.1, 0.2, 0.3, 1.0));
|
||
let v = to_node_value(&ParamValue::Color([0.1, 0.2, 0.3, 0.9], 4)).unwrap();
|
||
assert_eq!(v, Value::color(0.1, 0.2, 0.3, 0.9));
|
||
// 无值类 / Bytes → None(无节点对应)。
|
||
assert_eq!(to_node_value(&ParamValue::PushButton), None);
|
||
assert_eq!(to_node_value(&ParamValue::Container), None);
|
||
assert_eq!(to_node_value(&ParamValue::Bytes(vec![1])), None);
|
||
// 字符串族走专用桥(POD 路径 None)。
|
||
assert_eq!(to_node_value(&ParamValue::String(cs(""))), None);
|
||
let _ = c(T::NONE); // 哨兵:类型常量可达
|
||
}
|
||
|
||
/// type_default 的无值类/字节族基线(paramDefine 拒绝前的兜底)。
|
||
#[test]
|
||
fn type_default_edge_cases() {
|
||
assert!(matches!(type_default(TYPE_BYTES), ParamValue::Bytes(_)));
|
||
assert!(matches!(type_default(TYPE_CUSTOM), ParamValue::Bytes(_)));
|
||
assert!(matches!(
|
||
type_default(TYPE_PUSHBUTTON),
|
||
ParamValue::PushButton
|
||
));
|
||
assert!(matches!(type_default(TYPE_GROUP), ParamValue::Container));
|
||
assert!(matches!(type_default(TYPE_PAGE), ParamValue::Container));
|
||
// 未知类型占位(paramDefine 已拒绝,此处兜底)。
|
||
assert!(matches!(
|
||
type_default("OfxParamTypeBogus"),
|
||
ParamValue::Container
|
||
));
|
||
// 数值族的默认 0。
|
||
assert!(matches!(
|
||
type_default(TYPE_INTEGER),
|
||
ParamValue::Int([0, 0, 0], 1)
|
||
));
|
||
assert!(matches!(
|
||
type_default(TYPE_DOUBLE2D),
|
||
ParamValue::Double([0.0, 0.0, 0.0], 2)
|
||
));
|
||
assert!(matches!(
|
||
type_default(TYPE_RGBA),
|
||
ParamValue::Color([0.0, 0.0, 0.0, 0.0], 4)
|
||
));
|
||
}
|
||
|
||
/// set_from_node 的维度截断/补零与字符串静默路径。
|
||
#[test]
|
||
fn set_from_node_dimension_rules() {
|
||
// Integer2D 缺第三维补零;浮点截断。
|
||
let p = param(TYPE_INTEGER2D);
|
||
p.set_from_node(&crate::node::Value::vec(&[1.9, 2.9]));
|
||
assert_eq!(p.get(), ParamValue::Int([1, 2, 0], 2));
|
||
// StrChoice 参数遇 STRING 类型:POD 无数据 → 不改值。
|
||
let p = param(TYPE_STRCHOICE);
|
||
p.set_from_node(&crate::node::Value::string());
|
||
assert!(matches!(p.get(), ParamValue::StrChoice(_)));
|
||
// Bytes/Custom 参数:任意节点值都不改(无映射)。
|
||
let p = param(TYPE_CUSTOM);
|
||
p.set_from_node(&crate::node::Value::float(3.0));
|
||
assert!(matches!(p.get(), ParamValue::Bytes(_)));
|
||
}
|
||
}
|