// 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 .
//! 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>)里的
//! `oaknode::graph::Graph` 条目,按 [`oaknode::id::NodeId`] 定位。
//! 本 crate 的 [`NodeRef`] 即旧句柄盒
//! `(Arc>, 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>, NodeId)` 的值型复刻。
#[derive(Clone)]
pub struct NodeRef {
/// 所属 project(节点生命周期随 project)。
pub project: Arc>,
/// 节点在 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 {
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 {
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>,
/// 节点 id。
id: oaknode::id::NodeId,
}
/// 进程级身份注册表(OnceLock 惰性初始化;测试进程内可重复 register)。
static NODE_REGISTRY: OnceLock>> = OnceLock::new();
fn registry() -> &'static Mutex> {
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>,
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 {
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(m: &Mutex) -> 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 {
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 {
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>, 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(_))
));
}
}