mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 14:58:05 +08:00
Add node for executing rhai scripts
This commit is contained in:
@@ -198,6 +198,31 @@ impl<I, O> DowncastBothNode<I, O> {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Boxes the input and downcasts the output.
|
||||
/// Wraps around a node taking Box<dyn DynAny> and returning Box<dyn DynAny>
|
||||
#[derive(Clone)]
|
||||
pub struct DowncastNoneNode {
|
||||
node: SharedNodeContainer,
|
||||
}
|
||||
impl<'input> Node<'input, Any<'input>> for DowncastNoneNode {
|
||||
type Output = FutureAny<'input>;
|
||||
#[inline]
|
||||
fn eval(&'input self, input: Any<'input>) -> Self::Output {
|
||||
self.node.eval(input)
|
||||
}
|
||||
fn reset(&self) {
|
||||
self.node.reset();
|
||||
}
|
||||
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
|
||||
self.node.serialize()
|
||||
}
|
||||
}
|
||||
impl DowncastNoneNode {
|
||||
pub const fn new(node: SharedNodeContainer) -> Self {
|
||||
Self { node }
|
||||
}
|
||||
}
|
||||
pub struct FutureWrapperNode<Node> {
|
||||
node: Node,
|
||||
}
|
||||
|
||||
@@ -207,6 +207,7 @@ pub enum Type {
|
||||
Fn(Box<Type>, Box<Type>),
|
||||
/// Represents a future which promises to return the inner type.
|
||||
Future(Box<Type>),
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
impl Default for Type {
|
||||
@@ -258,6 +259,15 @@ impl Type {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn fn_fut_output(&self) -> Option<&Type> {
|
||||
match self {
|
||||
Type::Fn(_, second) => match second.as_ref() {
|
||||
Type::Future(fut) => Some(fut),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn function(input: &Type, output: &Type) -> Type {
|
||||
Type::Fn(Box::new(input.clone()), Box::new(output.clone()))
|
||||
@@ -281,6 +291,7 @@ impl Type {
|
||||
Self::Concrete(ty) => Some(ty.size),
|
||||
Self::Fn(_, _) => None,
|
||||
Self::Future(_) => None,
|
||||
Self::Dynamic => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +301,7 @@ impl Type {
|
||||
Self::Concrete(ty) => Some(ty.align),
|
||||
Self::Fn(_, _) => None,
|
||||
Self::Future(_) => None,
|
||||
Self::Dynamic => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,6 +311,7 @@ impl Type {
|
||||
Self::Concrete(_) => self,
|
||||
Self::Fn(_, output) => output.nested_type(),
|
||||
Self::Future(output) => output.nested_type(),
|
||||
Self::Dynamic => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,6 +333,7 @@ impl core::fmt::Debug for Type {
|
||||
Self::Concrete(arg0) => write!(f, "Concrete<{}>", format_type(&arg0.name)),
|
||||
Self::Fn(arg0, arg1) => write!(f, "{arg0:?} → {arg1:?}"),
|
||||
Self::Future(arg0) => write!(f, "Future<{arg0:?}>"),
|
||||
Self::Dynamic => write!(f, "Dynamic"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,6 +345,7 @@ impl std::fmt::Display for Type {
|
||||
Type::Concrete(ty) => write!(f, "{}", format_type(&ty.name)),
|
||||
Type::Fn(input, output) => write!(f, "{input} → {output}"),
|
||||
Type::Future(ty) => write!(f, "Future<{ty}>"),
|
||||
Self::Dynamic => write!(f, "Dynamic"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ macro_rules! tagged_value {
|
||||
Type::Generic(_) => {
|
||||
None
|
||||
}
|
||||
Type::Dynamic => None,
|
||||
Type::Concrete(concrete_type) => {
|
||||
let internal_id = concrete_type.id?;
|
||||
use std::any::TypeId;
|
||||
@@ -279,6 +280,7 @@ impl TaggedValue {
|
||||
}
|
||||
|
||||
match ty {
|
||||
Type::Dynamic => None,
|
||||
Type::Generic(_) => None,
|
||||
Type::Concrete(concrete_type) => {
|
||||
let internal_id = concrete_type.id?;
|
||||
|
||||
@@ -696,7 +696,7 @@ impl TypingContext {
|
||||
// Direct comparison of two concrete types.
|
||||
(Type::Concrete(type1), Type::Concrete(type2)) => type1 == type2,
|
||||
// Check inner type for futures
|
||||
(Type::Future(type1), Type::Future(type2)) => type1 == type2,
|
||||
(Type::Future(type1), Type::Future(type2)) => valid_subtype(type1, type2),
|
||||
// Loose comparison of function types, where loose means that functions are considered on a "greater than or equal to" basis of its function type's generality.
|
||||
// That means we compare their types with a contravariant relationship, which means that a more general type signature may be substituted for a more specific type signature.
|
||||
// For example, we allow `T -> V` to be substituted with `T' -> V` or `() -> V` where T' and () are more specific than T.
|
||||
@@ -708,6 +708,8 @@ impl TypingContext {
|
||||
// For example, Rust implements these same relations as it describes here: <https://doc.rust-lang.org/nomicon/subtyping.html>
|
||||
// More details explained here: <https://github.com/GraphiteEditor/Graphite/issues/1741>
|
||||
(Type::Fn(in1, out1), Type::Fn(in2, out2)) => valid_subtype(out2, out1) && (valid_subtype(in1, in2) || **in1 == concrete!(())),
|
||||
// Allow Dynamic types an input to concrete or generic types
|
||||
(Type::Concrete(_), Type::Dynamic) | (Type::Generic(_), Type::Dynamic) => true,
|
||||
// If either the proposed input or the allowed input are generic, we allow the substitution (meaning this is a valid subtype).
|
||||
// TODO: Add proper generic counting which is not based on the name
|
||||
(Type::Generic(_), _) | (_, Type::Generic(_)) => true,
|
||||
@@ -823,6 +825,10 @@ fn collect_generics(types: &NodeIOTypes) -> Vec<Cow<'static, str>> {
|
||||
let mut generics = inputs
|
||||
.filter_map(|t| match t {
|
||||
Type::Generic(out) => Some(out.clone()),
|
||||
Type::Future(fut) => match fut.as_ref() {
|
||||
Type::Generic(out) => Some(out.clone()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -837,7 +843,9 @@ fn collect_generics(types: &NodeIOTypes) -> Vec<Cow<'static, str>> {
|
||||
fn check_generic(types: &NodeIOTypes, input: &Type, parameters: &[Type], generic: &str) -> Result<Type, String> {
|
||||
let inputs = [(Some(&types.call_argument), Some(input))]
|
||||
.into_iter()
|
||||
.chain(types.inputs.iter().map(|x| x.fn_output()).zip(parameters.iter().map(|x| x.fn_output())));
|
||||
.chain(types.inputs.iter().map(|x| x.fn_fut_output()).zip(parameters.iter().map(|x| x.fn_fut_output())));
|
||||
let inputs: Vec<_> = inputs.collect();
|
||||
let inputs = inputs.into_iter();
|
||||
let concrete_inputs = inputs.filter(|(ni, _)| matches!(ni, Some(Type::Generic(input)) if generic == input));
|
||||
let mut outputs = concrete_inputs.flat_map(|(_, out)| out);
|
||||
let out_ty = outputs
|
||||
|
||||
@@ -90,5 +90,12 @@ web-sys = { workspace = true, optional = true, features = [
|
||||
image-compare = { version = "0.4.1", optional = true }
|
||||
ndarray = "0.16.1"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
rhai = { version = "1.21.0", features = ["serde", "wasm-bindgen"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
rhai = { version = "1.21.0", features = ["serde"] }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["macros"] }
|
||||
|
||||
@@ -30,3 +30,5 @@ pub mod wasm_application_io;
|
||||
pub mod dehaze;
|
||||
|
||||
pub mod imaginate;
|
||||
|
||||
pub mod rhai;
|
||||
|
||||
139
node-graph/gstd/src/rhai.rs
Normal file
139
node-graph/gstd/src/rhai.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use graph_craft::{
|
||||
document::value::TaggedValue,
|
||||
proto::{Any, FutureAny},
|
||||
};
|
||||
use graphene_core::{Context, Node};
|
||||
use rhai::{Engine, Scope};
|
||||
|
||||
// For Serde conversion
|
||||
use rhai::serde::{from_dynamic, to_dynamic};
|
||||
|
||||
pub struct RhaiNode<Source, Input> {
|
||||
source: Source,
|
||||
input: Input,
|
||||
}
|
||||
|
||||
impl<'n, S, I> Node<'n, Any<'n>> for RhaiNode<S, I>
|
||||
where
|
||||
S: Node<'n, Any<'n>, Output = FutureAny<'n>>,
|
||||
I: Node<'n, Any<'n>, Output = FutureAny<'n>>,
|
||||
{
|
||||
type Output = FutureAny<'n>;
|
||||
|
||||
fn eval(&'n self, ctx: Any<'n>) -> Self::Output {
|
||||
let ctx: Box<Context> = dyn_any::downcast(ctx).unwrap();
|
||||
let source = self.source.eval(ctx.clone());
|
||||
let input = self.input.eval(ctx);
|
||||
Box::pin(async move {
|
||||
// Get the script source and input value
|
||||
let source = source.await;
|
||||
let input = input.await;
|
||||
|
||||
// Convert to appropriate types
|
||||
let script: String = match dyn_any::downcast::<String>(source) {
|
||||
Ok(script) => *script,
|
||||
Err(err) => {
|
||||
log::error!("Failed to convert script source to String: {}", err);
|
||||
return Box::new(()) as Any<'n>;
|
||||
}
|
||||
};
|
||||
|
||||
let tagged_value = match TaggedValue::try_from_any(input) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
log::error!("Failed to convert input to TaggedValue: {}", err);
|
||||
return Box::new(()) as Any<'n>;
|
||||
}
|
||||
};
|
||||
|
||||
// Set up Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register any additional utility functions
|
||||
register_utility_functions(&mut engine);
|
||||
|
||||
// Create a scope and add the input value
|
||||
let mut scope = Scope::new();
|
||||
|
||||
// Convert TaggedValue to appropriate Rhai type
|
||||
// This is the key part we need to fix
|
||||
match tagged_value {
|
||||
TaggedValue::F64(val) => {
|
||||
// Directly push as primitive f64
|
||||
scope.push("input", val);
|
||||
}
|
||||
TaggedValue::U64(val) => {
|
||||
// Convert to i64 which Rhai uses for integers
|
||||
scope.push("input", val as i64);
|
||||
}
|
||||
TaggedValue::U32(val) => {
|
||||
// Convert to i64 which Rhai uses for integers
|
||||
scope.push("input", val as i64);
|
||||
}
|
||||
TaggedValue::Bool(val) => {
|
||||
scope.push("input", val);
|
||||
}
|
||||
TaggedValue::String(val) => {
|
||||
scope.push("input", val.clone());
|
||||
}
|
||||
// For complex types, use Serde conversion
|
||||
_ => match to_dynamic(tagged_value.clone()) {
|
||||
Ok(dynamic) => {
|
||||
scope.push("input", dynamic);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to convert input to Rhai Dynamic: {}", err);
|
||||
return Box::new(()) as Any<'n>;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Evaluate the script
|
||||
match engine.eval_with_scope::<rhai::Dynamic>(&mut scope, &script) {
|
||||
Ok(result) => {
|
||||
// Convert Rhai result back to TaggedValue
|
||||
if result.is::<f64>() {
|
||||
let val = result.cast::<f64>();
|
||||
TaggedValue::F64(val).to_any()
|
||||
} else if result.is::<i64>() {
|
||||
let val = result.cast::<i64>();
|
||||
TaggedValue::F64(val as f64).to_any()
|
||||
} else if result.is::<bool>() {
|
||||
let val = result.cast::<bool>();
|
||||
TaggedValue::Bool(val).to_any()
|
||||
} else if result.is::<String>() {
|
||||
let val = result.cast::<String>();
|
||||
TaggedValue::String(val).to_any()
|
||||
} else {
|
||||
// For complex types, use Serde conversion
|
||||
match from_dynamic(&result) {
|
||||
Ok(value) => TaggedValue::to_any(value),
|
||||
Err(err) => {
|
||||
log::error!("Failed to convert Rhai result to TaggedValue: {}", err);
|
||||
Box::new(()) as Any<'n>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Rhai script evaluation error: {}", err);
|
||||
Box::new(()) as Any<'n>
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Register utility functions that would be useful in scripts
|
||||
fn register_utility_functions(engine: &mut Engine) {
|
||||
// Logging function
|
||||
engine.register_fn("log", |msg: &str| {
|
||||
log::info!("Rhai script log: {}", msg);
|
||||
});
|
||||
}
|
||||
|
||||
impl<S, I> RhaiNode<S, I> {
|
||||
pub fn new(input: I, source: S) -> RhaiNode<S, I> {
|
||||
RhaiNode { source, input }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use dyn_any::StaticType;
|
||||
use glam::{DVec2, UVec2};
|
||||
use graph_craft::document::value::RenderOutput;
|
||||
use graph_craft::proto::{NodeConstructor, TypeErasedBox};
|
||||
use graph_craft::proto::{DowncastNoneNode, NodeConstructor, TypeErasedBox};
|
||||
use graphene_core::fn_type;
|
||||
use graphene_core::raster::color::Color;
|
||||
use graphene_core::raster::image::ImageFrameTable;
|
||||
@@ -66,6 +66,15 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||
// |_| Box::pin(async move { FutureWrapperNode::new(IdentityNode::new()).into_type_erased() }),
|
||||
// NodeIOTypes::new(generic!(I), generic!(I), vec![]),
|
||||
// ),
|
||||
(
|
||||
ProtoNodeIdentifier::new("graphene_std::rhai::RhaiNode"),
|
||||
|mut vec| Box::pin(async move { Box::new(graphene_std::rhai::RhaiNode::new(DowncastNoneNode::new(vec.remove(0)), DowncastNoneNode::new(vec.remove(0)))) as TypeErasedBox }),
|
||||
NodeIOTypes::new(
|
||||
generic!(C),
|
||||
Type::Future(Box::new(Type::Dynamic)),
|
||||
vec![Type::Fn(Box::new(concrete!(Context)), Box::new(Type::Future(Box::new(generic!(I))))), fn_type_fut!(Context, String)],
|
||||
),
|
||||
),
|
||||
// async_node!(graphene_core::ops::IntoNode<ImageFrameTable<SRGBA8>>, input: ImageFrameTable<Color>, params: []),
|
||||
// async_node!(graphene_core::ops::IntoNode<ImageFrameTable<Color>>, input: ImageFrameTable<SRGBA8>, params: []),
|
||||
async_node!(graphene_core::ops::IntoNode<GraphicGroupTable>, input: ImageFrameTable<Color>, params: []),
|
||||
|
||||
Reference in New Issue
Block a user