Files
oak-editor/crates/oakplugin/src/node.rs
T
Mike-Solar a43302d9b7 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
2026-08-21 04:20:14 +08:00

687 lines
21 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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_combonode.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_valueinclude/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],
}
}
/// 类型化构造:choiceCOMBO)。
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_undoableredo 写标准值、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_undoableText/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(_))
));
}
}