- 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
687 lines
21 KiB
Rust
687 lines
21 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/>.
|
||
|
||
//! oaknode 桥(single-lib unification):节点值 POD、身份注册表与
|
||
//! undoable 参数回写。
|
||
//!
|
||
//! ## Value 布局冻结
|
||
//!
|
||
//! [`Value`] 即 include/node/node.h:93 的 `oaknode_value` POD,字段
|
||
//! 逐字一致(type/num/den/f[4];`type` 取值见 [`node_value_type`])。
|
||
//! 字符串族输入(k_file/k_text/k_font/k_str_combo,node.h:48-52)没有
|
||
//! POD 表示——走 `*_input_string_*` 专用函数(本桥的
|
||
//! [`set_input_string_undoable`]);`OAKNODE_VALUE_STRING` 的 POD 里
|
||
//! 不携带字符串数据。
|
||
//!
|
||
//! ## 身份注册表(单库化重建)
|
||
//!
|
||
//! oaknode 的 C ABI 已删除(单库化):节点对象是
|
||
//! `oaknode::project::Project`(Arc<Mutex<Project>>)里的
|
||
//! `oaknode::graph::Graph` 条目,按 [`oaknode::id::NodeId`] 定位。
|
||
//! 本 crate 的 [`NodeRef`] 即旧句柄盒
|
||
//! `(Arc<Mutex<Project>>, NodeId)` 的值型复刻;进程级注册表
|
||
//! [`register_node`]/[`node_from_identity`] 按
|
||
//! [`oaknode::id::NodeId::identity`] 的打包身份(u64)做弱引用
|
||
//! 映射(project 释放后条目自然失效,升级失败 → None)——
|
||
//! 对应 M9 C++ 版 `oaknode_node_identity()` 注册表的地址语义。
|
||
//! facade 装配期调 [`register_node`] 登记节点,把返回身份写进
|
||
//! [`crate::instance::Instance::bind_node`](`node_identity`)。
|
||
//!
|
||
//! ## undoable 回写
|
||
//!
|
||
//! [`set_input_undoable`]/[`set_input_string_undoable`] 按
|
||
//! `oaknode::ops::set_value_at_time_command` 的同一 closure
|
||
//! 模式(`command_from_closures`)构造未执行的
|
||
//! `oakundo::undocommand::UndoCommand`:redo 闭包锁 project、
|
||
//! `graph.get_mut(id)`、`NodeCore::set_standard_value` 写标准值;
|
||
//! undo 闭包回放创建期快照的旧值。命令由调用方
|
||
//! ([`crate::param::notify_instance_changed`] →
|
||
//! [`crate::instance::Instance::submit_undo_command`])提交。
|
||
|
||
use std::collections::HashMap;
|
||
use std::sync::{Arc, Mutex, OnceLock, Weak};
|
||
|
||
use oakundo::undocommand::UndoCommand;
|
||
|
||
/// oaknode 节点引用(single-lib):旧 C ABI 句柄盒
|
||
/// `(Arc<Mutex<Project>>, NodeId)` 的值型复刻。
|
||
#[derive(Clone)]
|
||
pub struct NodeRef {
|
||
/// 所属 project(节点生命周期随 project)。
|
||
pub project: Arc<Mutex<oaknode::project::Project>>,
|
||
/// 节点在 project.graph 里的 id。
|
||
pub id: oaknode::id::NodeId,
|
||
}
|
||
|
||
/// oaknode_value_type 的取值(node.h:74)。
|
||
pub mod node_value_type {
|
||
/// OAKNODE_VALUE_NONE。
|
||
pub const NONE: i32 = 0;
|
||
/// OAKNODE_VALUE_INT。
|
||
pub const INT: i32 = 1;
|
||
/// OAKNODE_VALUE_FLOAT。
|
||
pub const FLOAT: i32 = 2;
|
||
/// OAKNODE_VALUE_BOOL。
|
||
pub const BOOL: i32 = 3;
|
||
/// OAKNODE_VALUE_RATIONAL。
|
||
pub const RATIONAL: i32 = 4;
|
||
/// OAKNODE_VALUE_COLOR。
|
||
pub const COLOR: i32 = 5;
|
||
/// OAKNODE_VALUE_VEC2。
|
||
pub const VEC2: i32 = 6;
|
||
/// OAKNODE_VALUE_VEC3。
|
||
pub const VEC3: i32 = 7;
|
||
/// OAKNODE_VALUE_VEC4。
|
||
pub const VEC4: i32 = 8;
|
||
/// OAKNODE_VALUE_COMBO。
|
||
pub const COMBO: i32 = 9;
|
||
/// OAKNODE_VALUE_STRING。
|
||
pub const STRING: i32 = 10;
|
||
}
|
||
|
||
/// oaknode_value(include/node/node.h:93,字段逐字一致)。
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||
pub struct Value {
|
||
/// 类型([`node_value_type`])。
|
||
pub r#type: i32,
|
||
/// INT/COMBO 值、BOOL 0/1、RATIONAL 分子。
|
||
pub num: i64,
|
||
/// RATIONAL 分母。
|
||
pub den: i64,
|
||
/// FLOAT f[0];VEC2/3/4 f[0..n-1];COLOR r,g,b,a。
|
||
pub f: [f64; 4],
|
||
}
|
||
|
||
impl Value {
|
||
/// 类型化构造:整数 / choice 索引。
|
||
pub const fn int(v: i64) -> Self {
|
||
Self {
|
||
r#type: node_value_type::INT,
|
||
num: v,
|
||
den: 0,
|
||
f: [0.0; 4],
|
||
}
|
||
}
|
||
|
||
/// 类型化构造:浮点。
|
||
pub const fn float(v: f64) -> Self {
|
||
Self {
|
||
r#type: node_value_type::FLOAT,
|
||
num: 0,
|
||
den: 0,
|
||
f: [v, 0.0, 0.0, 0.0],
|
||
}
|
||
}
|
||
|
||
/// 类型化构造:布尔。
|
||
pub const fn bool_(v: bool) -> Self {
|
||
Self {
|
||
r#type: node_value_type::BOOL,
|
||
num: v as i64,
|
||
den: 0,
|
||
f: [0.0; 4],
|
||
}
|
||
}
|
||
|
||
/// 类型化构造:choice(COMBO)。
|
||
pub const fn combo(v: i64) -> Self {
|
||
Self {
|
||
r#type: node_value_type::COMBO,
|
||
num: v,
|
||
den: 0,
|
||
f: [0.0; 4],
|
||
}
|
||
}
|
||
|
||
/// 类型化构造:颜色。
|
||
pub const fn color(r: f64, g: f64, b: f64, a: f64) -> Self {
|
||
Self {
|
||
r#type: node_value_type::COLOR,
|
||
num: 0,
|
||
den: 0,
|
||
f: [r, g, b, a],
|
||
}
|
||
}
|
||
|
||
/// 类型化构造:vec2/3/4(长度按 f 数组尾部 0 判定)。
|
||
pub const fn vec(v: &[f64]) -> Self {
|
||
let t = match v.len() {
|
||
2 => node_value_type::VEC2,
|
||
3 => node_value_type::VEC3,
|
||
_ => node_value_type::VEC4,
|
||
};
|
||
let mut f = [0.0; 4];
|
||
let mut i = 0;
|
||
while i < v.len() && i < 4 {
|
||
f[i] = v[i];
|
||
i += 1;
|
||
}
|
||
Self {
|
||
r#type: t,
|
||
num: 0,
|
||
den: 0,
|
||
f,
|
||
}
|
||
}
|
||
|
||
/// 类型化构造:字符串族(POD 不携带数据;值经
|
||
/// [`set_input_string_undoable`] 传递)。
|
||
pub const fn string() -> Self {
|
||
Self {
|
||
r#type: node_value_type::STRING,
|
||
num: 0,
|
||
den: 0,
|
||
f: [0.0; 4],
|
||
}
|
||
}
|
||
|
||
/// POD → [`oaknode::value::NodeValue`],按 POD kind 映射(与
|
||
/// `oaknode::value::OakNodeValue::to_node_value` 的 kind 分支一致,
|
||
/// 不按声明类型重量化)。`STRING` 族 POD 不携带数据 → `None`;
|
||
/// `NONE` → [`oaknode::value::NodeValue::None`]。
|
||
pub fn to_node_value(&self) -> Option<oaknode::value::NodeValue> {
|
||
use oaknode::value::NodeValue as NV;
|
||
match self.r#type {
|
||
node_value_type::NONE => Some(NV::None),
|
||
node_value_type::INT => Some(NV::Int(self.num)),
|
||
node_value_type::FLOAT => Some(NV::Float(self.f[0])),
|
||
node_value_type::BOOL => Some(NV::Boolean(self.num != 0)),
|
||
node_value_type::RATIONAL => Some(NV::Rational(oakcore_rs::Rational::new(
|
||
self.num, self.den,
|
||
))),
|
||
node_value_type::COLOR => Some(NV::Color(self.f)),
|
||
node_value_type::VEC2 => Some(NV::Vec2([self.f[0], self.f[1]])),
|
||
node_value_type::VEC3 => Some(NV::Vec3([self.f[0], self.f[1], self.f[2]])),
|
||
node_value_type::VEC4 => Some(NV::Vec4(self.f)),
|
||
node_value_type::COMBO => Some(NV::Combo(self.num)),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// [`oaknode::value::NodeValue`] → POD(`ValueType::to_oak` 的逆)。
|
||
/// 无 POD 表达的类型(字符串族/纹理/采样/矩阵等)→ `None`。
|
||
pub fn from_node_value(v: &oaknode::value::NodeValue) -> Option<Value> {
|
||
use oaknode::value::NodeValue as NV;
|
||
Some(match v {
|
||
NV::None => Value {
|
||
r#type: node_value_type::NONE,
|
||
num: 0,
|
||
den: 0,
|
||
f: [0.0; 4],
|
||
},
|
||
NV::Int(i) => Value::int(*i),
|
||
NV::Float(f) => Value::float(*f),
|
||
NV::Boolean(b) => Value::bool_(*b),
|
||
NV::Rational(r) => Value {
|
||
r#type: node_value_type::RATIONAL,
|
||
num: r.numerator(),
|
||
den: r.denominator(),
|
||
f: [0.0; 4],
|
||
},
|
||
NV::Color(c) => Value::color(c[0], c[1], c[2], c[3]),
|
||
NV::Vec2(a) => Value::vec(&[a[0], a[1]]),
|
||
NV::Vec3(a) => Value::vec(&[a[0], a[1], a[2]]),
|
||
NV::Vec4(a) => Value::vec(&[a[0], a[1], a[2], a[3]]),
|
||
NV::Combo(i) => Value::combo(*i),
|
||
_ => return None,
|
||
})
|
||
}
|
||
}
|
||
|
||
// ---- 身份注册表 -----------------------------------------------------------
|
||
|
||
/// 注册表条目:弱 project 引用(project 释放后升级失败,条目变死)
|
||
/// + 节点 id。
|
||
struct RegistryEntry {
|
||
/// 所属 project(弱引用;强引用由调用方/节点持有)。
|
||
project: Weak<Mutex<oaknode::project::Project>>,
|
||
/// 节点 id。
|
||
id: oaknode::id::NodeId,
|
||
}
|
||
|
||
/// 进程级身份注册表(OnceLock 惰性初始化;测试进程内可重复 register)。
|
||
static NODE_REGISTRY: OnceLock<Mutex<HashMap<u64, RegistryEntry>>> = OnceLock::new();
|
||
|
||
fn registry() -> &'static Mutex<HashMap<u64, RegistryEntry>> {
|
||
NODE_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
|
||
}
|
||
|
||
/// 登记 oaknode 节点(facade 装配期调用;对应 M9 C++ 版
|
||
/// `oaknode_node_identity()` 注册表的登记侧)。返回打包身份
|
||
/// ([`oaknode::id::NodeId::identity`]),写入
|
||
/// [`crate::instance::Instance::bind_node`]。同一身份重复登记
|
||
/// 覆盖旧条目(重绑定)。
|
||
pub fn register_node(
|
||
project: Arc<Mutex<oaknode::project::Project>>,
|
||
id: oaknode::id::NodeId,
|
||
) -> u64 {
|
||
let identity = id.identity();
|
||
let entry = RegistryEntry {
|
||
project: Arc::downgrade(&project),
|
||
id,
|
||
};
|
||
registry()
|
||
.lock()
|
||
.unwrap_or_else(|e| e.into_inner())
|
||
.insert(identity, entry);
|
||
identity
|
||
}
|
||
|
||
/// 摘除身份(节点销毁路径调用;未知身份 no-op)。
|
||
pub fn unregister_node(identity: u64) {
|
||
registry()
|
||
.lock()
|
||
.unwrap_or_else(|e| e.into_inner())
|
||
.remove(&identity);
|
||
}
|
||
|
||
/// 身份 → 节点引用(param 回写入口)。
|
||
///
|
||
/// `None`:身份未登记,或 project 已释放(弱引用升级失败——旧 C
|
||
/// ABI 对悬垂句柄的可解释失败路径)。调用方
|
||
/// ([`crate::param::notify_instance_changed`])按 "未绑定节点 →
|
||
/// no-op" 处理。
|
||
pub fn node_from_identity(identity: usize) -> Option<NodeRef> {
|
||
let (project, id) = {
|
||
let reg = registry()
|
||
.lock()
|
||
.unwrap_or_else(|e| e.into_inner());
|
||
let entry = reg.get(&(identity as u64))?;
|
||
(entry.project.clone(), entry.id)
|
||
};
|
||
let project = project.upgrade()?;
|
||
Some(NodeRef { project, id })
|
||
}
|
||
|
||
// ---- 桥调用面(undoable 回写)---------------------------------------------
|
||
|
||
/// oaknode 桥错误(set_input* 的失败原因;调用方按失败跳过提交)。
|
||
#[derive(Debug, thiserror::Error)]
|
||
pub enum NodeBridgeError {
|
||
/// 节点 id 已失效(图内无此节点)。
|
||
#[error("node id is stale or not present in the graph")]
|
||
StaleNode,
|
||
/// 节点上无该输入。
|
||
#[error("node has no input '{0}'")]
|
||
UnknownInput(String),
|
||
/// 值 POD 无法转换(STRING 族 POD 不携带数据;数值族恒可转换)。
|
||
#[error("value POD of type {0} cannot be converted to a node value")]
|
||
InvalidValue(i32),
|
||
/// 输入不是字符串族类型(string setter 限定 Text/StrCombo)。
|
||
#[error("input '{0}' is not a string-family input")]
|
||
NotStringInput(String),
|
||
}
|
||
|
||
/// 取锁(毒锁接管,一次 panic 不级联)。
|
||
fn lock_any<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||
m.lock().unwrap_or_else(|e| e.into_inner())
|
||
}
|
||
|
||
/// 从 redo/undo 闭包构造未执行的 [`UndoCommand`](closure 模式,与
|
||
/// `oaknode::ops::command_from_closures` 同构)。
|
||
fn command_from_closures(
|
||
redo: impl FnMut() + Send + 'static,
|
||
undo: impl FnMut() + Send + 'static,
|
||
) -> UndoCommand {
|
||
UndoCommand::from_closures(redo, undo)
|
||
}
|
||
|
||
/// 以 undoable 方式设置节点的标准输入值(POD 路径;数值族输入)。
|
||
///
|
||
/// 返回**未执行**的 [`UndoCommand`](调用方
|
||
/// [`crate::instance::Instance::submit_undo_command`] 提交):redo
|
||
/// 锁 project、`graph.get_mut(id)`、`NodeCore::set_standard_value`
|
||
/// 写新值;undo 回放创建期快照的旧值(与
|
||
/// `oaknode::ops::set_value_at_time_command` 的 standard-value 分支
|
||
/// 同构,element = -1 整值)。节点/输入失效时返回
|
||
/// [`NodeBridgeError`](不产出命令)。
|
||
pub fn set_input_undoable(
|
||
node: &NodeRef,
|
||
input: &str,
|
||
value: &Value,
|
||
) -> Result<UndoCommand, NodeBridgeError> {
|
||
let nv = value
|
||
.to_node_value()
|
||
.ok_or(NodeBridgeError::InvalidValue(value.r#type))?;
|
||
// 校验节点与输入存在性 + 旧值快照(创建期状态,redo/undo 据此
|
||
// 回放;不校验声明类型——按 POD kind 存原样,与 ops.rs 一致)。
|
||
let old = {
|
||
let mut guard = lock_any(&node.project);
|
||
let entry = guard
|
||
.graph
|
||
.get_mut(node.id)
|
||
.ok_or(NodeBridgeError::StaleNode)?;
|
||
if entry.core.input_data_type(input).is_none() {
|
||
return Err(NodeBridgeError::UnknownInput(input.to_string()));
|
||
}
|
||
entry.core.standard_value(input, -1)
|
||
};
|
||
|
||
let project = node.project.clone();
|
||
let id = node.id;
|
||
let input_redo = input.to_string();
|
||
let input_undo = input.to_string();
|
||
let value_redo = nv.clone();
|
||
let project_redo = project.clone();
|
||
let project_undo = project.clone();
|
||
Ok(command_from_closures(
|
||
move || {
|
||
let mut guard = lock_any(&project_redo);
|
||
if let Some(entry) = guard.graph.get_mut(id) {
|
||
entry
|
||
.core
|
||
.set_standard_value(&input_redo, -1, value_redo.clone());
|
||
}
|
||
},
|
||
move || {
|
||
let mut guard = lock_any(&project_undo);
|
||
if let Some(entry) = guard.graph.get_mut(id) {
|
||
entry
|
||
.core
|
||
.set_standard_value(&input_undo, -1, old.clone());
|
||
}
|
||
},
|
||
))
|
||
}
|
||
|
||
/// 以 undoable 方式设置节点的标准输入值(字符串族输入:Text /
|
||
/// StrCombo / Parametric——POD 不携带字符串数据)。
|
||
///
|
||
/// 按输入的声明类型([`oaknode::value::ValueType`])选存储变体:
|
||
/// `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,
|
||
input: &str,
|
||
value: &str,
|
||
) -> Result<UndoCommand, NodeBridgeError> {
|
||
let (old, declared) = {
|
||
let guard = lock_any(&node.project);
|
||
let entry = guard
|
||
.graph
|
||
.get(node.id)
|
||
.ok_or(NodeBridgeError::StaleNode)?;
|
||
let declared = entry
|
||
.core
|
||
.input_data_type(input)
|
||
.ok_or_else(|| NodeBridgeError::UnknownInput(input.to_string()))?;
|
||
(entry.core.standard_value(input, -1), declared)
|
||
};
|
||
let nv = match declared {
|
||
oaknode::value::ValueType::StrCombo => oaknode::value::NodeValue::StrCombo(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())),
|
||
};
|
||
|
||
let project = node.project.clone();
|
||
let id = node.id;
|
||
let input_redo = input.to_string();
|
||
let input_undo = input.to_string();
|
||
let value_redo = nv.clone();
|
||
let project_redo = project.clone();
|
||
let project_undo = project.clone();
|
||
Ok(command_from_closures(
|
||
move || {
|
||
let mut guard = lock_any(&project_redo);
|
||
if let Some(entry) = guard.graph.get_mut(id) {
|
||
entry
|
||
.core
|
||
.set_standard_value(&input_redo, -1, value_redo.clone());
|
||
}
|
||
},
|
||
move || {
|
||
let mut guard = lock_any(&project_undo);
|
||
if let Some(entry) = guard.graph.get_mut(id) {
|
||
entry
|
||
.core
|
||
.set_standard_value(&input_undo, -1, old.clone());
|
||
}
|
||
},
|
||
))
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// 建一个含 Text/StrCombo/Float 输入的测试节点。
|
||
fn test_project() -> (Arc<Mutex<oaknode::project::Project>>, oaknode::id::NodeId) {
|
||
let project = oaknode::project::Project::new();
|
||
let mut guard = project.lock().unwrap();
|
||
let id = guard.graph.add_node(
|
||
oaknode::node::NodeCore::empty(),
|
||
Box::new(oaknode::nodes::EmptyBehavior),
|
||
);
|
||
// NodeCore::empty 无输入;手动声明三种输入。
|
||
use oaknode::input::Input;
|
||
let core = guard.graph.get_mut(id).unwrap();
|
||
core.core.add_input(Input::new(
|
||
"label",
|
||
oaknode::value::ValueType::Text,
|
||
oaknode::value::NodeValue::Text(String::new()),
|
||
));
|
||
core.core.add_input(Input::new(
|
||
"choice",
|
||
oaknode::value::ValueType::StrCombo,
|
||
oaknode::value::NodeValue::StrCombo(String::new()),
|
||
));
|
||
core.core.add_input(Input::new(
|
||
"opacity",
|
||
oaknode::value::ValueType::Float,
|
||
oaknode::value::NodeValue::Float(1.0),
|
||
));
|
||
drop(guard);
|
||
(project, id)
|
||
}
|
||
|
||
/// 注册表:register → node_from_identity → unregister 全链。
|
||
#[test]
|
||
fn identity_register_roundtrip() {
|
||
let (project, id) = test_project();
|
||
let identity = register_node(project.clone(), id);
|
||
let node = node_from_identity(identity as usize).expect("已登记身份应可解析");
|
||
assert_eq!(node.id, id);
|
||
let found = node.project.lock().unwrap().graph.get(id).is_some();
|
||
assert!(found);
|
||
|
||
// 未登记身份 → None。
|
||
assert!(node_from_identity(0xfeed_face).is_none());
|
||
unregister_node(identity);
|
||
assert!(node_from_identity(identity as usize).is_none());
|
||
}
|
||
|
||
/// project 释放后弱条目升级失败 → None(悬垂语义);重复登记
|
||
/// 覆盖死条目后重新解析到新 project。
|
||
#[test]
|
||
fn identity_project_dropped() {
|
||
let (project, id) = test_project();
|
||
let identity = register_node(project.clone(), id);
|
||
drop(project);
|
||
assert!(node_from_identity(identity as usize).is_none());
|
||
// 新 project 同槽位身份重登记(覆盖死条目)。
|
||
let (project2, id2) = test_project();
|
||
register_node(project2.clone(), id2);
|
||
let node = node_from_identity(identity as usize).expect("重登记后应可解析");
|
||
assert_eq!(node.id, id2);
|
||
let alive = node.project.lock().unwrap().graph.get(id2).is_some();
|
||
assert!(alive);
|
||
}
|
||
|
||
/// Value POD ↔ NodeValue 全类型映射;STRING 族 POD 无数据。
|
||
#[test]
|
||
fn pod_node_value_roundtrip() {
|
||
use oaknode::value::NodeValue as NV;
|
||
let cases: Vec<(Value, NV)> = vec![
|
||
(Value::int(7), NV::Int(7)),
|
||
(Value::float(1.5), NV::Float(1.5)),
|
||
(Value::bool_(true), NV::Boolean(true)),
|
||
(Value::combo(3), NV::Combo(3)),
|
||
(
|
||
Value {
|
||
r#type: node_value_type::RATIONAL,
|
||
num: 3,
|
||
den: 2,
|
||
f: [0.0; 4],
|
||
},
|
||
NV::Rational(oakcore_rs::Rational::new(3, 2)),
|
||
),
|
||
(Value::color(0.1, 0.2, 0.3, 0.4), NV::Color([0.1, 0.2, 0.3, 0.4])),
|
||
(Value::vec(&[1.0, 2.0]), NV::Vec2([1.0, 2.0])),
|
||
(Value::vec(&[1.0, 2.0, 3.0]), NV::Vec3([1.0, 2.0, 3.0])),
|
||
(
|
||
Value::vec(&[1.0, 2.0, 3.0, 4.0]),
|
||
NV::Vec4([1.0, 2.0, 3.0, 4.0]),
|
||
),
|
||
];
|
||
for (pod, nv) in cases {
|
||
let to = pod.to_node_value().expect("POD 应可转换");
|
||
assert_eq!(to, nv, "POD → NodeValue");
|
||
assert_eq!(Value::from_node_value(&nv), Some(pod), "NodeValue → POD");
|
||
}
|
||
// STRING POD 无数据。
|
||
assert!(Value::string().to_node_value().is_none());
|
||
// 无 POD 表达的类型。
|
||
assert_eq!(Value::from_node_value(&NV::Text("x".into())), None);
|
||
assert_eq!(Value::from_node_value(&NV::Matrix([0.0; 16])), None);
|
||
}
|
||
|
||
/// set_input_undoable:redo 写标准值、undo 回放旧值;错误路径。
|
||
#[test]
|
||
fn set_input_undoable_applies_and_undoes() {
|
||
let (project, id) = test_project();
|
||
let node = NodeRef {
|
||
project: project.clone(),
|
||
id,
|
||
};
|
||
let mut cmd = set_input_undoable(&node, "opacity", &Value::float(0.25)).unwrap();
|
||
// 命令未执行时值未变。
|
||
assert_eq!(
|
||
project
|
||
.lock()
|
||
.unwrap()
|
||
.graph
|
||
.get(id)
|
||
.unwrap()
|
||
.core
|
||
.standard_value("opacity", -1),
|
||
oaknode::value::NodeValue::Float(1.0)
|
||
);
|
||
cmd.redo_now();
|
||
assert_eq!(
|
||
project
|
||
.lock()
|
||
.unwrap()
|
||
.graph
|
||
.get(id)
|
||
.unwrap()
|
||
.core
|
||
.standard_value("opacity", -1),
|
||
oaknode::value::NodeValue::Float(0.25)
|
||
);
|
||
cmd.undo_now();
|
||
assert_eq!(
|
||
project
|
||
.lock()
|
||
.unwrap()
|
||
.graph
|
||
.get(id)
|
||
.unwrap()
|
||
.core
|
||
.standard_value("opacity", -1),
|
||
oaknode::value::NodeValue::Float(1.0)
|
||
);
|
||
|
||
// 未知输入 / STRING POD / 失效节点。
|
||
assert!(matches!(
|
||
set_input_undoable(&node, "nope", &Value::float(1.0)),
|
||
Err(NodeBridgeError::UnknownInput(_))
|
||
));
|
||
assert!(matches!(
|
||
set_input_undoable(&node, "opacity", &Value::string()),
|
||
Err(NodeBridgeError::InvalidValue(_))
|
||
));
|
||
let stale = NodeRef {
|
||
project,
|
||
id: oaknode::id::NodeId::from_identity(999).unwrap(),
|
||
};
|
||
assert!(matches!(
|
||
set_input_undoable(&stale, "opacity", &Value::float(1.0)),
|
||
Err(NodeBridgeError::StaleNode)
|
||
));
|
||
}
|
||
|
||
/// set_input_string_undoable:Text/StrCombo 变体选择与回放。
|
||
#[test]
|
||
fn set_input_string_undoable_variants() {
|
||
let (project, id) = test_project();
|
||
let node = NodeRef {
|
||
project: project.clone(),
|
||
id,
|
||
};
|
||
let mut cmd = set_input_string_undoable(&node, "label", "hello").unwrap();
|
||
cmd.redo_now();
|
||
assert_eq!(
|
||
project
|
||
.lock()
|
||
.unwrap()
|
||
.graph
|
||
.get(id)
|
||
.unwrap()
|
||
.core
|
||
.standard_value("label", -1),
|
||
oaknode::value::NodeValue::Text("hello".to_string())
|
||
);
|
||
cmd.undo_now();
|
||
assert_eq!(
|
||
project
|
||
.lock()
|
||
.unwrap()
|
||
.graph
|
||
.get(id)
|
||
.unwrap()
|
||
.core
|
||
.standard_value("label", -1),
|
||
oaknode::value::NodeValue::Text(String::new())
|
||
);
|
||
|
||
let mut cmd = set_input_string_undoable(&node, "choice", "low").unwrap();
|
||
cmd.redo_now();
|
||
assert_eq!(
|
||
project
|
||
.lock()
|
||
.unwrap()
|
||
.graph
|
||
.get(id)
|
||
.unwrap()
|
||
.core
|
||
.standard_value("choice", -1),
|
||
oaknode::value::NodeValue::StrCombo("low".to_string())
|
||
);
|
||
|
||
// 数值输入走字符串 setter → NotStringInput。
|
||
assert!(matches!(
|
||
set_input_string_undoable(&node, "opacity", "1.0"),
|
||
Err(NodeBridgeError::NotStringInput(_))
|
||
));
|
||
}
|
||
}
|