use std::collections::{HashMap, HashSet, VecDeque};
use std::fmt;
#[derive(Clone, Debug)]
pub struct FlowGraph {
pub name: String,
pub target: FlowTarget,
pub workgroup_size: Option<u32>,
pub inputs: Vec<FlowInput>,
pub nodes: Vec<FlowNode>,
pub outputs: Vec<FlowOutput>,
pub topo_order: Vec<usize>,
pub steps: Vec<FlowStep>,
pub chains: Vec<FlowChain>,
pub uses: Vec<FlowUse>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FlowTarget {
Fragment,
Compute,
Vertex,
Material,
}
impl FlowGraph {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
target: FlowTarget::Fragment,
workgroup_size: None,
inputs: Vec::new(),
nodes: Vec::new(),
outputs: Vec::new(),
topo_order: Vec::new(),
steps: Vec::new(),
chains: Vec::new(),
uses: Vec::new(),
}
}
fn resolve_name(&self, name: &str) -> Option<NameResolution> {
for (i, input) in self.inputs.iter().enumerate() {
if input.name == name {
return Some(NameResolution::Input(i));
}
}
for (i, node) in self.nodes.iter().enumerate() {
if node.name == name {
return Some(NameResolution::Node(i));
}
}
None
}
}
#[derive(Debug)]
enum NameResolution {
Input(usize),
Node(usize),
}
#[derive(Clone, Debug)]
pub struct FlowInput {
pub name: String,
pub source: FlowInputSource,
pub ty: Option<FlowType>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum FlowInputSource {
Builtin(BuiltinVar),
Buffer { name: String, ty: FlowType },
CssProperty(String),
EnvVar(String),
Auto,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BuiltinVar {
Uv,
Time,
Resolution,
Sdf,
FrameIndex,
Pointer,
VertexPosition,
VertexNormal,
VertexTangent,
VertexColor,
VertexJoints,
VertexWeights,
VertexIndex,
WorldPosition,
WorldNormal,
WorldTangent,
TangentHandedness,
CameraPosition,
LightDirection,
LightIntensity,
ModelMatrix,
ViewProjectionMatrix,
}
impl BuiltinVar {
pub fn parse(s: &str) -> Option<Self> {
match s {
"uv" => Some(Self::Uv),
"time" => Some(Self::Time),
"resolution" => Some(Self::Resolution),
"sdf" => Some(Self::Sdf),
"frame-index" | "frame_index" => Some(Self::FrameIndex),
"pointer" => Some(Self::Pointer),
"vertex_position" | "position" => Some(Self::VertexPosition),
"vertex_normal" | "normal" => Some(Self::VertexNormal),
"vertex_tangent" | "tangent" => Some(Self::VertexTangent),
"vertex_color" => Some(Self::VertexColor),
"vertex_joints" | "joints" => Some(Self::VertexJoints),
"vertex_weights" | "weights" => Some(Self::VertexWeights),
"vertex_index" => Some(Self::VertexIndex),
"world_position" | "world_pos" => Some(Self::WorldPosition),
"world_normal" => Some(Self::WorldNormal),
"world_tangent" => Some(Self::WorldTangent),
"tangent_handedness" => Some(Self::TangentHandedness),
"camera_position" | "camera_pos" => Some(Self::CameraPosition),
"light_direction" | "light_dir" => Some(Self::LightDirection),
"light_intensity" => Some(Self::LightIntensity),
"model_matrix" | "model" => Some(Self::ModelMatrix),
"view_proj" | "view_projection" => Some(Self::ViewProjectionMatrix),
_ => None,
}
}
pub fn output_type(&self) -> FlowType {
match self {
Self::Uv | Self::Resolution | Self::Pointer => FlowType::Vec2,
Self::Time
| Self::Sdf
| Self::FrameIndex
| Self::VertexIndex
| Self::TangentHandedness
| Self::LightIntensity => FlowType::Float,
Self::VertexPosition
| Self::VertexNormal
| Self::WorldPosition
| Self::WorldNormal
| Self::WorldTangent
| Self::CameraPosition
| Self::LightDirection => FlowType::Vec3,
Self::VertexTangent | Self::VertexColor | Self::VertexJoints | Self::VertexWeights => {
FlowType::Vec4
}
Self::ModelMatrix | Self::ViewProjectionMatrix => FlowType::Mat4,
}
}
}
#[derive(Clone, Debug)]
pub struct FlowNode {
pub name: String,
pub expr: FlowExpr,
pub inferred_type: Option<FlowType>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum FlowExpr {
Float(f32),
Vec2(Box<FlowExpr>, Box<FlowExpr>),
Vec3(Box<FlowExpr>, Box<FlowExpr>, Box<FlowExpr>),
Vec4(Box<FlowExpr>, Box<FlowExpr>, Box<FlowExpr>, Box<FlowExpr>),
Color(f32, f32, f32, f32),
Ref(String),
Add(Box<FlowExpr>, Box<FlowExpr>),
Sub(Box<FlowExpr>, Box<FlowExpr>),
Mul(Box<FlowExpr>, Box<FlowExpr>),
Div(Box<FlowExpr>, Box<FlowExpr>),
Neg(Box<FlowExpr>),
Swizzle(Box<FlowExpr>, String),
Call { func: FlowFunc, args: Vec<FlowExpr> },
}
impl FlowExpr {
pub fn collect_refs(&self, refs: &mut HashSet<String>) {
match self {
Self::Ref(name) => {
refs.insert(name.clone());
}
Self::Float(_) | Self::Color(_, _, _, _) => {}
Self::Vec2(a, b)
| Self::Add(a, b)
| Self::Sub(a, b)
| Self::Mul(a, b)
| Self::Div(a, b) => {
a.collect_refs(refs);
b.collect_refs(refs);
}
Self::Vec3(a, b, c) => {
a.collect_refs(refs);
b.collect_refs(refs);
c.collect_refs(refs);
}
Self::Vec4(a, b, c, d) => {
a.collect_refs(refs);
b.collect_refs(refs);
c.collect_refs(refs);
d.collect_refs(refs);
}
Self::Neg(a) | Self::Swizzle(a, _) => a.collect_refs(refs),
Self::Call { args, .. } => {
for arg in args {
arg.collect_refs(refs);
}
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FlowFunc {
Sin,
Cos,
Tan,
Abs,
Floor,
Ceil,
Fract,
Sqrt,
Pow,
Atan2,
Exp,
Log,
Sign,
Mod,
Min,
Max,
Clamp,
Mix,
Smoothstep,
Step,
Length,
Distance,
Dot,
Cross,
Normalize,
Reflect,
SdfBox,
SdfCircle,
SdfEllipse,
SdfRoundRect,
SdfUnion,
SdfIntersect,
SdfSubtract,
SdfSmoothUnion,
SdfSmoothIntersect,
SdfSmoothSubtract,
Sdf,
Sobel,
BufferRead,
Phong,
BlinnPhong,
Perlin,
Simplex,
Worley,
WorleyGrad,
Fbm,
FbmEx,
Checkerboard,
SpringEval,
WaveStep,
FluidStep,
SampleScene,
Mat4MulVec4,
Mat4Mul,
Mat4Inverse,
Mat4Transpose,
TransformNormal,
TranslationMatrix,
RotationMatrix,
ScaleMatrix,
PerspectiveMatrix,
LookAtMatrix,
SampleTexture,
}
impl FlowFunc {
pub fn parse(s: &str) -> Option<Self> {
match s {
"sin" => Some(Self::Sin),
"cos" => Some(Self::Cos),
"tan" => Some(Self::Tan),
"abs" => Some(Self::Abs),
"floor" => Some(Self::Floor),
"ceil" => Some(Self::Ceil),
"fract" => Some(Self::Fract),
"sqrt" => Some(Self::Sqrt),
"pow" => Some(Self::Pow),
"atan2" => Some(Self::Atan2),
"exp" => Some(Self::Exp),
"log" => Some(Self::Log),
"sign" => Some(Self::Sign),
"mod" => Some(Self::Mod),
"min" => Some(Self::Min),
"max" => Some(Self::Max),
"clamp" => Some(Self::Clamp),
"mix" => Some(Self::Mix),
"smoothstep" => Some(Self::Smoothstep),
"step" => Some(Self::Step),
"length" => Some(Self::Length),
"distance" => Some(Self::Distance),
"dot" => Some(Self::Dot),
"cross" => Some(Self::Cross),
"normalize" => Some(Self::Normalize),
"reflect" => Some(Self::Reflect),
"sdf_box" | "sdf-box" => Some(Self::SdfBox),
"sdf_circle" | "sdf-circle" => Some(Self::SdfCircle),
"sdf_ellipse" | "sdf-ellipse" => Some(Self::SdfEllipse),
"sdf_round_rect" | "sdf-round-rect" => Some(Self::SdfRoundRect),
"sdf_union" | "sdf-union" => Some(Self::SdfUnion),
"sdf_intersect" | "sdf-intersect" => Some(Self::SdfIntersect),
"sdf_subtract" | "sdf-subtract" => Some(Self::SdfSubtract),
"sdf_smooth_union" | "sdf-smooth-union" => Some(Self::SdfSmoothUnion),
"sdf_smooth_intersect" | "sdf-smooth-intersect" => Some(Self::SdfSmoothIntersect),
"sdf_smooth_subtract" | "sdf-smooth-subtract" => Some(Self::SdfSmoothSubtract),
"sdf" => Some(Self::Sdf),
"sobel" => Some(Self::Sobel),
"buffer_read" | "buffer-read" => Some(Self::BufferRead),
"phong" => Some(Self::Phong),
"blinn_phong" | "blinn-phong" => Some(Self::BlinnPhong),
"perlin" => Some(Self::Perlin),
"simplex" => Some(Self::Simplex),
"worley" => Some(Self::Worley),
"worley_grad" | "worley-grad" => Some(Self::WorleyGrad),
"fbm" => Some(Self::Fbm),
"fbm_ex" | "fbm-ex" => Some(Self::FbmEx),
"checkerboard" => Some(Self::Checkerboard),
"spring_eval" | "spring-eval" => Some(Self::SpringEval),
"wave_step" | "wave-step" => Some(Self::WaveStep),
"fluid_step" | "fluid-step" => Some(Self::FluidStep),
"sample_scene" | "sample-scene" => Some(Self::SampleScene),
"mat4_mul_vec4" | "mat4-mul-vec4" => Some(Self::Mat4MulVec4),
"mat4_mul" | "mat4-mul" => Some(Self::Mat4Mul),
"mat4_inverse" | "mat4-inverse" | "inverse" => Some(Self::Mat4Inverse),
"mat4_transpose" | "mat4-transpose" | "transpose" => Some(Self::Mat4Transpose),
"transform_normal" | "transform-normal" => Some(Self::TransformNormal),
"translation_matrix" | "translation-matrix" => Some(Self::TranslationMatrix),
"rotation_matrix" | "rotation-matrix" => Some(Self::RotationMatrix),
"scale_matrix" | "scale-matrix" => Some(Self::ScaleMatrix),
"perspective" | "perspective_matrix" | "perspective-matrix" => {
Some(Self::PerspectiveMatrix)
}
"look_at" | "look-at" | "lookat" => Some(Self::LookAtMatrix),
"sample_texture" | "sample-texture" => Some(Self::SampleTexture),
_ => None,
}
}
pub fn arg_count(&self) -> (usize, usize) {
match self {
Self::Sin
| Self::Cos
| Self::Tan
| Self::Abs
| Self::Floor
| Self::Ceil
| Self::Fract
| Self::Sqrt
| Self::Exp
| Self::Log
| Self::Sign
| Self::Length
| Self::Normalize
| Self::Sdf => (1, 1),
Self::Pow
| Self::Atan2
| Self::Mod
| Self::Min
| Self::Max
| Self::Step
| Self::Distance
| Self::Dot
| Self::Reflect
| Self::Sobel
| Self::SdfCircle
| Self::SdfEllipse
| Self::SdfUnion
| Self::SdfIntersect
| Self::SdfSubtract => (2, 2),
Self::Clamp
| Self::Mix
| Self::Smoothstep
| Self::Cross
| Self::SdfBox
| Self::SdfRoundRect
| Self::SdfSmoothUnion
| Self::SdfSmoothIntersect
| Self::SdfSmoothSubtract
| Self::Phong
| Self::BlinnPhong
| Self::Perlin
| Self::Simplex
| Self::Worley
| Self::Fbm => (2, 4),
Self::WorleyGrad => (1, 1),
Self::FbmEx => (3, 3),
Self::Checkerboard => (1, 2),
Self::SampleScene => (1, 1),
Self::BufferRead => (1, 3),
Self::SpringEval | Self::WaveStep | Self::FluidStep => (2, 5),
Self::Mat4MulVec4 | Self::Mat4Mul | Self::TransformNormal => (2, 2),
Self::Mat4Inverse | Self::Mat4Transpose => (1, 1),
Self::TranslationMatrix | Self::ScaleMatrix => (1, 1),
Self::RotationMatrix => (2, 2), Self::PerspectiveMatrix => (4, 4), Self::LookAtMatrix => (3, 3), Self::SampleTexture => (2, 2), }
}
pub fn return_type(&self, arg_types: &[FlowType]) -> Option<FlowType> {
match self {
Self::Length
| Self::Distance
| Self::Dot
| Self::Sdf
| Self::SdfBox
| Self::SdfCircle
| Self::SdfEllipse
| Self::SdfRoundRect
| Self::SdfUnion
| Self::SdfIntersect
| Self::SdfSubtract
| Self::SdfSmoothUnion
| Self::SdfSmoothIntersect
| Self::SdfSmoothSubtract
| Self::Step
| Self::Atan2 => Some(FlowType::Float),
Self::Sin
| Self::Cos
| Self::Tan
| Self::Abs
| Self::Floor
| Self::Ceil
| Self::Fract
| Self::Sqrt
| Self::Exp
| Self::Log
| Self::Sign
| Self::Pow
| Self::Mod
| Self::Min
| Self::Max
| Self::Clamp
| Self::Mix
| Self::Smoothstep
| Self::Normalize
| Self::Reflect => arg_types.first().cloned().or(Some(FlowType::Float)),
Self::Cross => Some(FlowType::Vec3),
Self::Perlin
| Self::Simplex
| Self::Worley
| Self::Fbm
| Self::FbmEx
| Self::Checkerboard => Some(FlowType::Float),
Self::WorleyGrad => Some(FlowType::Vec3),
Self::Sobel => Some(FlowType::Vec3),
Self::Phong | Self::BlinnPhong => Some(FlowType::Vec4),
Self::SpringEval | Self::WaveStep | Self::FluidStep => {
arg_types.first().cloned().or(Some(FlowType::Float))
}
Self::BufferRead | Self::SampleScene | Self::SampleTexture => Some(FlowType::Vec4),
Self::Mat4MulVec4 => Some(FlowType::Vec4),
Self::Mat4Mul
| Self::Mat4Inverse
| Self::Mat4Transpose
| Self::TranslationMatrix
| Self::RotationMatrix
| Self::ScaleMatrix
| Self::PerspectiveMatrix
| Self::LookAtMatrix => Some(FlowType::Mat4),
Self::TransformNormal => Some(FlowType::Vec3),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StepType {
PatternNoise,
PatternRipple,
PatternWaves,
PatternGradient,
PatternGrid,
PatternPlasma,
PatternWorley,
TransformWarp,
TransformRotate,
TransformScale,
TransformTile,
TransformMirror,
TransformPolar,
TransformWet,
SurfaceLight,
SurfaceDepth,
SurfaceGlow,
EffectRefract,
EffectFrost,
EffectSpecular,
EffectFog,
EffectLight,
ColorRamp,
ColorShift,
ColorTint,
ColorInvert,
ComposeBlend,
ComposeMask,
ComposeLayer,
AdjustFalloff,
AdjustRemap,
AdjustThreshold,
AdjustEase,
AdjustClamp,
}
impl StepType {
pub fn parse(s: &str) -> Option<Self> {
match s {
"pattern-noise" | "pattern_noise" => Some(Self::PatternNoise),
"pattern-ripple" | "pattern_ripple" => Some(Self::PatternRipple),
"pattern-waves" | "pattern_waves" => Some(Self::PatternWaves),
"pattern-gradient" | "pattern_gradient" => Some(Self::PatternGradient),
"pattern-grid" | "pattern_grid" => Some(Self::PatternGrid),
"pattern-plasma" | "pattern_plasma" => Some(Self::PatternPlasma),
"pattern-worley" | "pattern_worley" => Some(Self::PatternWorley),
"transform-warp" | "transform_warp" => Some(Self::TransformWarp),
"transform-rotate" | "transform_rotate" => Some(Self::TransformRotate),
"transform-scale" | "transform_scale" => Some(Self::TransformScale),
"transform-tile" | "transform_tile" => Some(Self::TransformTile),
"transform-mirror" | "transform_mirror" => Some(Self::TransformMirror),
"transform-polar" | "transform_polar" => Some(Self::TransformPolar),
"transform-wet" | "transform_wet" => Some(Self::TransformWet),
"surface-light" | "surface_light" => Some(Self::SurfaceLight),
"surface-depth" | "surface_depth" => Some(Self::SurfaceDepth),
"surface-glow" | "surface_glow" => Some(Self::SurfaceGlow),
"effect-refract" | "effect_refract" => Some(Self::EffectRefract),
"effect-frost" | "effect_frost" => Some(Self::EffectFrost),
"effect-specular" | "effect_specular" => Some(Self::EffectSpecular),
"effect-fog" | "effect_fog" => Some(Self::EffectFog),
"effect-light" | "effect_light" => Some(Self::EffectLight),
"color-ramp" | "color_ramp" => Some(Self::ColorRamp),
"color-shift" | "color_shift" => Some(Self::ColorShift),
"color-tint" | "color_tint" => Some(Self::ColorTint),
"color-invert" | "color_invert" => Some(Self::ColorInvert),
"compose-blend" | "compose_blend" => Some(Self::ComposeBlend),
"compose-mask" | "compose_mask" => Some(Self::ComposeMask),
"compose-layer" | "compose_layer" => Some(Self::ComposeLayer),
"adjust-falloff" | "adjust_falloff" => Some(Self::AdjustFalloff),
"adjust-remap" | "adjust_remap" => Some(Self::AdjustRemap),
"adjust-threshold" | "adjust_threshold" => Some(Self::AdjustThreshold),
"adjust-ease" | "adjust_ease" => Some(Self::AdjustEase),
"adjust-clamp" | "adjust_clamp" => Some(Self::AdjustClamp),
_ => None,
}
}
pub fn output_type(&self) -> FlowType {
match self {
Self::PatternNoise
| Self::PatternRipple
| Self::PatternWaves
| Self::PatternGrid
| Self::PatternWorley => FlowType::Float,
Self::PatternGradient | Self::PatternPlasma => FlowType::Vec4,
Self::TransformWarp
| Self::TransformRotate
| Self::TransformScale
| Self::TransformTile
| Self::TransformMirror
| Self::TransformPolar => FlowType::Float,
Self::TransformWet => FlowType::Vec2,
Self::SurfaceLight | Self::SurfaceDepth | Self::SurfaceGlow => FlowType::Float,
Self::ColorRamp | Self::ColorShift | Self::ColorTint | Self::ColorInvert => {
FlowType::Vec4
}
Self::ComposeBlend | Self::ComposeMask | Self::ComposeLayer => FlowType::Vec4,
Self::EffectRefract | Self::EffectFrost => FlowType::Vec2,
Self::EffectSpecular | Self::EffectLight => FlowType::Float,
Self::EffectFog => FlowType::Vec4,
Self::AdjustFalloff
| Self::AdjustRemap
| Self::AdjustThreshold
| Self::AdjustEase
| Self::AdjustClamp => FlowType::Float,
}
}
pub fn required_params(&self) -> &[&str] {
match self {
Self::PatternNoise => &[],
Self::PatternRipple => &[],
Self::PatternWaves => &["source"],
Self::PatternGradient => &[],
Self::PatternGrid => &[],
Self::PatternPlasma => &[],
Self::PatternWorley => &["scale"],
Self::EffectRefract => &["sources"],
Self::EffectFrost => &[],
Self::EffectSpecular => &[],
Self::EffectFog => &["source"],
Self::EffectLight => &["sources"],
Self::TransformWarp => &["source"],
Self::TransformRotate => &["source"],
Self::TransformScale => &["source"],
Self::TransformTile => &["source"],
Self::TransformMirror => &["source"],
Self::TransformPolar => &[],
Self::TransformWet => &[],
Self::SurfaceLight => &["source"],
Self::SurfaceDepth => &["source"],
Self::SurfaceGlow => &["source"],
Self::ColorRamp => &["source", "stops"],
Self::ColorShift => &["source"],
Self::ColorTint => &["source", "color"],
Self::ColorInvert => &["source"],
Self::ComposeBlend => &["a", "b"],
Self::ComposeMask => &["source", "mask"],
Self::ComposeLayer => &["a", "b"],
Self::AdjustFalloff => &["source"],
Self::AdjustRemap => &["source"],
Self::AdjustThreshold => &["source"],
Self::AdjustEase => &["source"],
Self::AdjustClamp => &["source"],
}
}
}
impl fmt::Display for StepType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Self::PatternNoise => "pattern-noise",
Self::PatternRipple => "pattern-ripple",
Self::PatternWaves => "pattern-waves",
Self::PatternGradient => "pattern-gradient",
Self::PatternGrid => "pattern-grid",
Self::PatternPlasma => "pattern-plasma",
Self::PatternWorley => "pattern-worley",
Self::EffectRefract => "effect-refract",
Self::EffectFrost => "effect-frost",
Self::EffectSpecular => "effect-specular",
Self::EffectFog => "effect-fog",
Self::EffectLight => "effect-light",
Self::TransformWarp => "transform-warp",
Self::TransformRotate => "transform-rotate",
Self::TransformScale => "transform-scale",
Self::TransformTile => "transform-tile",
Self::TransformMirror => "transform-mirror",
Self::TransformPolar => "transform-polar",
Self::TransformWet => "transform-wet",
Self::SurfaceLight => "surface-light",
Self::SurfaceDepth => "surface-depth",
Self::SurfaceGlow => "surface-glow",
Self::ColorRamp => "color-ramp",
Self::ColorShift => "color-shift",
Self::ColorTint => "color-tint",
Self::ColorInvert => "color-invert",
Self::ComposeBlend => "compose-blend",
Self::ComposeMask => "compose-mask",
Self::ComposeLayer => "compose-layer",
Self::AdjustFalloff => "adjust-falloff",
Self::AdjustRemap => "adjust-remap",
Self::AdjustThreshold => "adjust-threshold",
Self::AdjustEase => "adjust-ease",
Self::AdjustClamp => "adjust-clamp",
};
write!(f, "{}", name)
}
}
#[derive(Clone, Debug)]
pub enum StepParam {
Expr(FlowExpr),
ColorStops(Vec<(FlowExpr, f32)>),
Ident(String),
Int(i32),
IdentList(Vec<String>),
FloatList(Vec<f32>),
}
#[derive(Clone, Debug)]
pub struct FlowStep {
pub name: String,
pub step_type: StepType,
pub params: HashMap<String, StepParam>,
}
#[derive(Clone, Debug)]
pub struct FlowChain {
pub name: String,
pub links: Vec<ChainLink>,
}
#[derive(Clone, Debug)]
pub struct ChainLink {
pub step_type: StepType,
pub params: HashMap<String, StepParam>,
}
#[derive(Clone, Debug)]
pub struct FlowUse {
pub flow_name: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FlowType {
Float,
Vec2,
Vec3,
Vec4,
Mat4,
}
impl FlowType {
pub fn components(&self) -> usize {
match self {
Self::Float => 1,
Self::Vec2 => 2,
Self::Vec3 => 3,
Self::Vec4 => 4,
Self::Mat4 => 16,
}
}
pub fn broadcast_with(&self, other: &Self) -> Option<Self> {
if self == other {
Some(*self)
} else if *self == Self::Float {
Some(*other)
} else if *other == Self::Float {
Some(*self)
} else {
None }
}
}
impl fmt::Display for FlowType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Float => write!(f, "float"),
Self::Vec2 => write!(f, "vec2"),
Self::Vec3 => write!(f, "vec3"),
Self::Vec4 => write!(f, "vec4"),
Self::Mat4 => write!(f, "mat4x4<f32>"),
}
}
}
#[derive(Clone, Debug)]
pub struct FlowOutput {
pub name: String,
pub target: FlowOutputTarget,
pub expr: Option<FlowExpr>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum FlowOutputTarget {
Color,
Alpha,
Displacement,
Buffer { name: String },
CssVar(String),
Position,
WorldNormalOut,
WorldPositionOut,
Albedo,
Metallic,
Roughness,
Emissive,
SurfaceNormal,
AlphaOut,
}
#[derive(Clone, Debug)]
pub enum FlowError {
CycleDetected {
nodes: Vec<String>,
},
UndefinedReference {
in_node: String,
name: String,
},
TypeMismatch {
in_node: String,
message: String,
},
ArgCountMismatch {
in_node: String,
func: String,
expected: (usize, usize),
got: usize,
},
MissingOutput {
message: String,
},
DuplicateName {
name: String,
},
UnknownStepType {
name: String,
step_type: String,
},
MissingStepParam {
step_name: String,
param: String,
},
InvalidStepParam {
step_name: String,
param: String,
message: String,
},
FlowNotFound {
name: String,
},
CircularComposition {
chain: Vec<String>,
},
InvalidIdentifier { name: String, reason: String },
}
impl fmt::Display for FlowError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CycleDetected { nodes } => {
write!(f, "cycle detected in flow graph: {}", nodes.join(" → "))
}
Self::UndefinedReference { in_node, name } => {
write!(f, "in node '{}': undefined reference '{}'", in_node, name)
}
Self::TypeMismatch { in_node, message } => {
write!(f, "in node '{}': type mismatch: {}", in_node, message)
}
Self::ArgCountMismatch {
in_node,
func,
expected,
got,
} => {
write!(
f,
"in node '{}': function '{}' expects {}-{} args, got {}",
in_node, func, expected.0, expected.1, got
)
}
Self::MissingOutput { message } => write!(f, "missing output: {}", message),
Self::DuplicateName { name } => {
write!(f, "duplicate name '{}' in flow graph", name)
}
Self::UnknownStepType { name, step_type } => {
write!(f, "step '{}': unknown type '{}'", name, step_type)
}
Self::MissingStepParam { step_name, param } => {
write!(
f,
"step '{}': missing required parameter '{}'",
step_name, param
)
}
Self::InvalidStepParam {
step_name,
param,
message,
} => {
write!(
f,
"step '{}': invalid parameter '{}': {}",
step_name, param, message
)
}
Self::FlowNotFound { name } => {
write!(f, "referenced flow '{}' not found", name)
}
Self::CircularComposition { chain } => {
write!(f, "circular flow composition: {}", chain.join(" → "))
}
Self::InvalidIdentifier { name, reason } => {
write!(f, "invalid identifier '{}': {}", name, reason)
}
}
}
}
impl FlowGraph {
fn expand_semantic_layer(
&mut self,
flow_registry: Option<&HashMap<String, FlowGraph>>,
) -> Result<(), Vec<FlowError>> {
let mut errors = Vec::new();
self.resolve_uses(flow_registry, &mut errors);
self.desugar_chains();
if !self.steps.is_empty() {
self.ensure_input("uv", BuiltinVar::Uv);
self.ensure_input("time", BuiltinVar::Time);
}
self.expand_steps(&mut errors);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn ensure_input(&mut self, name: &str, builtin: BuiltinVar) {
if !self.inputs.iter().any(|i| i.name == name) {
self.inputs.push(FlowInput {
name: name.to_string(),
source: FlowInputSource::Builtin(builtin),
ty: Some(builtin.output_type()),
});
}
}
fn resolve_uses(
&mut self,
flow_registry: Option<&HashMap<String, FlowGraph>>,
errors: &mut Vec<FlowError>,
) {
let uses = std::mem::take(&mut self.uses);
let registry = match flow_registry {
Some(r) => r,
None => {
for u in &uses {
errors.push(FlowError::FlowNotFound {
name: u.flow_name.clone(),
});
}
return;
}
};
for flow_use in &uses {
let referenced = match registry.get(&flow_use.flow_name) {
Some(g) => g,
None => {
errors.push(FlowError::FlowNotFound {
name: flow_use.flow_name.clone(),
});
continue;
}
};
if referenced.uses.iter().any(|u| u.flow_name == self.name) {
errors.push(FlowError::CircularComposition {
chain: vec![self.name.clone(), flow_use.flow_name.clone()],
});
continue;
}
let prefix = format!("{}_", flow_use.flow_name.replace('-', "_"));
for input in &referenced.inputs {
if !self.inputs.iter().any(|i| i.name == input.name) {
self.inputs.push(input.clone());
}
}
for node in &referenced.nodes {
let prefixed_name = format!("{}{}", prefix, node.name);
let prefixed_expr = prefix_refs_in_expr(&node.expr, &prefix, &referenced.inputs);
self.nodes.push(FlowNode {
name: prefixed_name,
expr: prefixed_expr,
inferred_type: None,
});
}
for output in &referenced.outputs {
let output_name =
format!("{}_{}", flow_use.flow_name.replace('-', "_"), output.name);
let expr = if let Some(ref e) = output.expr {
prefix_refs_in_expr(e, &prefix, &referenced.inputs)
} else {
FlowExpr::Ref(format!("{}{}", prefix, output.name))
};
self.nodes.push(FlowNode {
name: output_name,
expr,
inferred_type: None,
});
}
}
}
fn desugar_chains(&mut self) {
let chains = std::mem::take(&mut self.chains);
for chain in chains {
let link_count = chain.links.len();
for (i, link) in chain.links.into_iter().enumerate() {
let step_name = if i == link_count - 1 {
chain.name.clone()
} else {
format!("_chain_{}_{}", chain.name, i)
};
let mut params = link.params;
if i > 0 {
let prev_name = if i == 1 {
format!("_chain_{}_{}", chain.name, 0)
} else {
format!("_chain_{}_{}", chain.name, i - 1)
};
params
.entry("source".to_string())
.or_insert(StepParam::Expr(FlowExpr::Ref(prev_name)));
}
self.steps.push(FlowStep {
name: step_name,
step_type: link.step_type,
params,
});
}
}
}
fn expand_steps(&mut self, errors: &mut Vec<FlowError>) {
let steps = std::mem::take(&mut self.steps);
for step in &steps {
for ¶m in step.step_type.required_params() {
if !step.params.contains_key(param) {
errors.push(FlowError::MissingStepParam {
step_name: step.name.clone(),
param: param.to_string(),
});
}
}
match expand_step(step) {
Ok(nodes) => self.nodes.extend(nodes),
Err(e) => errors.push(e),
}
}
}
}
fn prefix_refs_in_expr(expr: &FlowExpr, prefix: &str, inputs: &[FlowInput]) -> FlowExpr {
let input_names: HashSet<&str> = inputs.iter().map(|i| i.name.as_str()).collect();
prefix_refs_recursive(expr, prefix, &input_names)
}
fn prefix_refs_recursive(expr: &FlowExpr, prefix: &str, inputs: &HashSet<&str>) -> FlowExpr {
match expr {
FlowExpr::Ref(name) => {
if inputs.contains(name.as_str()) {
FlowExpr::Ref(name.clone())
} else {
FlowExpr::Ref(format!("{}{}", prefix, name))
}
}
FlowExpr::Float(_) | FlowExpr::Color(_, _, _, _) => expr.clone(),
FlowExpr::Vec2(a, b) => FlowExpr::Vec2(
Box::new(prefix_refs_recursive(a, prefix, inputs)),
Box::new(prefix_refs_recursive(b, prefix, inputs)),
),
FlowExpr::Vec3(a, b, c) => FlowExpr::Vec3(
Box::new(prefix_refs_recursive(a, prefix, inputs)),
Box::new(prefix_refs_recursive(b, prefix, inputs)),
Box::new(prefix_refs_recursive(c, prefix, inputs)),
),
FlowExpr::Vec4(a, b, c, d) => FlowExpr::Vec4(
Box::new(prefix_refs_recursive(a, prefix, inputs)),
Box::new(prefix_refs_recursive(b, prefix, inputs)),
Box::new(prefix_refs_recursive(c, prefix, inputs)),
Box::new(prefix_refs_recursive(d, prefix, inputs)),
),
FlowExpr::Add(a, b) => FlowExpr::Add(
Box::new(prefix_refs_recursive(a, prefix, inputs)),
Box::new(prefix_refs_recursive(b, prefix, inputs)),
),
FlowExpr::Sub(a, b) => FlowExpr::Sub(
Box::new(prefix_refs_recursive(a, prefix, inputs)),
Box::new(prefix_refs_recursive(b, prefix, inputs)),
),
FlowExpr::Mul(a, b) => FlowExpr::Mul(
Box::new(prefix_refs_recursive(a, prefix, inputs)),
Box::new(prefix_refs_recursive(b, prefix, inputs)),
),
FlowExpr::Div(a, b) => FlowExpr::Div(
Box::new(prefix_refs_recursive(a, prefix, inputs)),
Box::new(prefix_refs_recursive(b, prefix, inputs)),
),
FlowExpr::Neg(a) => FlowExpr::Neg(Box::new(prefix_refs_recursive(a, prefix, inputs))),
FlowExpr::Swizzle(a, s) => FlowExpr::Swizzle(
Box::new(prefix_refs_recursive(a, prefix, inputs)),
s.clone(),
),
FlowExpr::Call { func, args } => FlowExpr::Call {
func: *func,
args: args
.iter()
.map(|a| prefix_refs_recursive(a, prefix, inputs))
.collect(),
},
}
}
fn param_expr(params: &HashMap<String, StepParam>, key: &str, default: FlowExpr) -> FlowExpr {
match params.get(key) {
Some(StepParam::Expr(e)) => e.clone(),
_ => default,
}
}
fn param_ident(params: &HashMap<String, StepParam>, key: &str, default: &str) -> String {
match params.get(key) {
Some(StepParam::Ident(s)) => s.clone(),
_ => default.to_string(),
}
}
fn param_int(params: &HashMap<String, StepParam>, key: &str, default: i32) -> i32 {
match params.get(key) {
Some(StepParam::Int(n)) => *n,
Some(StepParam::Expr(FlowExpr::Float(f))) => *f as i32,
_ => default,
}
}
fn expand_step(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
match step.step_type {
StepType::PatternNoise => expand_pattern_noise(step),
StepType::PatternRipple => expand_pattern_ripple(step),
StepType::PatternWaves => expand_pattern_waves(step),
StepType::AdjustFalloff => expand_adjust_falloff(step),
StepType::ColorRamp => expand_color_ramp(step),
StepType::TransformWarp => expand_transform_warp(step),
StepType::ComposeBlend => expand_compose_blend(step),
StepType::SurfaceLight => expand_surface_light(step),
StepType::PatternWorley => expand_pattern_worley(step),
StepType::EffectRefract => expand_effect_refract(step),
StepType::EffectFrost => expand_effect_frost(step),
StepType::EffectSpecular => expand_effect_specular(step),
StepType::EffectFog => expand_effect_fog(step),
StepType::EffectLight => expand_effect_light(step),
StepType::TransformWet => expand_transform_wet(step),
_ => Ok(vec![FlowNode {
name: step.name.clone(),
expr: param_expr(&step.params, "source", FlowExpr::Float(0.0)),
inferred_type: None,
}]),
}
}
fn expand_pattern_noise(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let scale = param_expr(&step.params, "scale", FlowExpr::Float(4.0));
let detail = param_int(&step.params, "detail", 4);
let animation = param_expr(
&step.params,
"animation",
FlowExpr::Mul(
Box::new(FlowExpr::Ref("time".to_string())),
Box::new(FlowExpr::Float(0.5)),
),
);
let _style = param_ident(&step.params, "style", "turbulence");
let uv_name = format!("_s_{}_p", step.name);
let uv_node = FlowNode {
name: uv_name.clone(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref("uv".to_string())),
Box::new(scale),
)),
Box::new(FlowExpr::Vec2(
Box::new(animation),
Box::new(FlowExpr::Float(0.0)),
)),
),
inferred_type: None,
};
let out_node = FlowNode {
name: step.name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Fbm,
args: vec![FlowExpr::Ref(uv_name), FlowExpr::Float(detail as f32)],
},
inferred_type: None,
};
Ok(vec![uv_node, out_node])
}
fn expand_pattern_ripple(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let center = param_expr(
&step.params,
"center",
FlowExpr::Vec2(
Box::new(FlowExpr::Float(0.5)),
Box::new(FlowExpr::Float(0.5)),
),
);
let density = param_expr(&step.params, "density", FlowExpr::Float(30.0));
let speed = param_expr(&step.params, "speed", FlowExpr::Float(4.0));
let dist_name = format!("_s_{}_d", step.name);
let dist_node = FlowNode {
name: dist_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Length,
args: vec![FlowExpr::Sub(
Box::new(FlowExpr::Ref("uv".to_string())),
Box::new(center),
)],
},
inferred_type: None,
};
let out_node = FlowNode {
name: step.name.clone(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Call {
func: FlowFunc::Sin,
args: vec![FlowExpr::Sub(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref(dist_name)),
Box::new(density),
)),
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref("time".to_string())),
Box::new(speed),
)),
)],
}),
Box::new(FlowExpr::Float(0.5)),
)),
Box::new(FlowExpr::Float(0.5)),
),
inferred_type: None,
};
Ok(vec![dist_node, out_node])
}
fn expand_pattern_waves(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let source = param_expr(&step.params, "source", FlowExpr::Float(0.0));
let density = param_expr(&step.params, "density", FlowExpr::Float(10.0));
let speed = param_expr(&step.params, "speed", FlowExpr::Float(1.0));
let offset = param_expr(&step.params, "offset", FlowExpr::Float(0.0));
let out_node = FlowNode {
name: step.name.clone(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Call {
func: FlowFunc::Sin,
args: vec![FlowExpr::Add(
Box::new(FlowExpr::Sub(
Box::new(FlowExpr::Mul(Box::new(source), Box::new(density))),
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref("time".to_string())),
Box::new(speed),
)),
)),
Box::new(offset),
)],
}),
Box::new(FlowExpr::Float(0.5)),
)),
Box::new(FlowExpr::Float(0.5)),
),
inferred_type: None,
};
Ok(vec![out_node])
}
fn expand_adjust_falloff(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let source = param_expr(&step.params, "source", FlowExpr::Float(0.0));
let radius = param_expr(&step.params, "radius", FlowExpr::Float(0.5));
let curve = param_ident(&step.params, "curve", "smooth");
let out_node = if curve == "linear" {
FlowNode {
name: step.name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Clamp,
args: vec![
FlowExpr::Sub(
Box::new(FlowExpr::Float(1.0)),
Box::new(FlowExpr::Div(Box::new(source), Box::new(radius))),
),
FlowExpr::Float(0.0),
FlowExpr::Float(1.0),
],
},
inferred_type: None,
}
} else {
FlowNode {
name: step.name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Smoothstep,
args: vec![radius, FlowExpr::Float(0.0), source],
},
inferred_type: None,
}
};
Ok(vec![out_node])
}
fn expand_color_ramp(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let source = param_expr(&step.params, "source", FlowExpr::Float(0.0));
let opacity = step.params.get("opacity");
let stops = match step.params.get("stops") {
Some(StepParam::ColorStops(stops)) if stops.len() >= 2 => stops,
_ => {
return Err(FlowError::InvalidStepParam {
step_name: step.name.clone(),
param: "stops".to_string(),
message: "color-ramp requires at least 2 color stops".to_string(),
});
}
};
let has_opacity = opacity.is_some();
let mut nodes = Vec::new();
let mut prev_mix_name: Option<String> = None;
for i in 0..stops.len() - 1 {
let (ref color_a, pos_a) = stops[i];
let (ref color_b, pos_b) = stops[i + 1];
let t_name = format!("_s_{}_t{}{}", step.name, i, i + 1);
let is_last = i == stops.len() - 2;
let mix_name = if is_last && !has_opacity {
step.name.clone()
} else if is_last && has_opacity {
format!("_s_{}_rgb", step.name)
} else {
format!("_s_{}_m{}{}", step.name, i, i + 1)
};
nodes.push(FlowNode {
name: t_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Smoothstep,
args: vec![
FlowExpr::Float(pos_a),
FlowExpr::Float(pos_b),
source.clone(),
],
},
inferred_type: None,
});
let a_expr = if let Some(ref prev) = prev_mix_name {
FlowExpr::Ref(prev.clone())
} else {
color_a.clone()
};
nodes.push(FlowNode {
name: mix_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Mix,
args: vec![a_expr, color_b.clone(), FlowExpr::Ref(t_name)],
},
inferred_type: None,
});
prev_mix_name = Some(mix_name);
}
if let Some(opacity_param) = opacity {
let opacity_expr = match opacity_param {
StepParam::Expr(e) => e.clone(),
_ => FlowExpr::Float(1.0),
};
let rgb_ref = prev_mix_name.unwrap_or_else(|| step.name.clone());
nodes.push(FlowNode {
name: step.name.clone(),
expr: FlowExpr::Vec4(
Box::new(FlowExpr::Swizzle(
Box::new(FlowExpr::Ref(rgb_ref.clone())),
"x".to_string(),
)),
Box::new(FlowExpr::Swizzle(
Box::new(FlowExpr::Ref(rgb_ref.clone())),
"y".to_string(),
)),
Box::new(FlowExpr::Swizzle(
Box::new(FlowExpr::Ref(rgb_ref.clone())),
"z".to_string(),
)),
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Swizzle(
Box::new(FlowExpr::Ref(rgb_ref)),
"w".to_string(),
)),
Box::new(opacity_expr),
)),
),
inferred_type: None,
});
}
Ok(nodes)
}
fn expand_transform_warp(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let source = param_expr(&step.params, "source", FlowExpr::Float(0.0));
let strength = param_expr(&step.params, "strength", FlowExpr::Float(0.1));
let direction = param_expr(&step.params, "direction", FlowExpr::Float(0.0));
let ca_name = format!("_s_{}_ca", step.name);
let sa_name = format!("_s_{}_sa", step.name);
let cx_name = format!("_s_{}_cx", step.name);
let cy_name = format!("_s_{}_cy", step.name);
let rx_name = format!("_s_{}_rx", step.name);
let ry_name = format!("_s_{}_ry", step.name);
let nodes = vec![
FlowNode {
name: ca_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Cos,
args: vec![direction.clone()],
},
inferred_type: None,
},
FlowNode {
name: sa_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Sin,
args: vec![direction],
},
inferred_type: None,
},
FlowNode {
name: cx_name.clone(),
expr: FlowExpr::Sub(
Box::new(FlowExpr::Swizzle(
Box::new(FlowExpr::Ref("uv".to_string())),
"x".to_string(),
)),
Box::new(FlowExpr::Float(0.5)),
),
inferred_type: None,
},
FlowNode {
name: cy_name.clone(),
expr: FlowExpr::Sub(
Box::new(FlowExpr::Swizzle(
Box::new(FlowExpr::Ref("uv".to_string())),
"y".to_string(),
)),
Box::new(FlowExpr::Float(0.5)),
),
inferred_type: None,
},
FlowNode {
name: rx_name.clone(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Add(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref(cx_name.clone())),
Box::new(FlowExpr::Ref(ca_name.clone())),
)),
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref(cy_name.clone())),
Box::new(FlowExpr::Ref(sa_name.clone())),
)),
)),
Box::new(FlowExpr::Float(0.5)),
),
inferred_type: None,
},
FlowNode {
name: ry_name.clone(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Add(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref(cx_name)),
Box::new(FlowExpr::Neg(Box::new(FlowExpr::Ref(sa_name)))),
)),
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref(cy_name)),
Box::new(FlowExpr::Ref(ca_name)),
)),
)),
Box::new(FlowExpr::Float(0.5)),
),
inferred_type: None,
},
FlowNode {
name: step.name.clone(),
expr: FlowExpr::Mul(Box::new(source), Box::new(strength)),
inferred_type: None,
},
];
Ok(nodes)
}
fn expand_compose_blend(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let a = param_expr(&step.params, "a", FlowExpr::Float(0.0));
let b = param_expr(&step.params, "b", FlowExpr::Float(0.0));
let mode = param_ident(&step.params, "mode", "multiply");
let expr = match mode.as_str() {
"screen" => {
FlowExpr::Sub(
Box::new(FlowExpr::Add(Box::new(a.clone()), Box::new(b.clone()))),
Box::new(FlowExpr::Mul(Box::new(a), Box::new(b))),
)
}
"add" => FlowExpr::Call {
func: FlowFunc::Clamp,
args: vec![
FlowExpr::Add(Box::new(a), Box::new(b)),
FlowExpr::Float(0.0),
FlowExpr::Float(1.0),
],
},
_ => {
FlowExpr::Mul(Box::new(a), Box::new(b))
}
};
Ok(vec![FlowNode {
name: step.name.clone(),
expr,
inferred_type: None,
}])
}
fn expand_surface_light(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let source = param_expr(&step.params, "source", FlowExpr::Float(0.0));
let direction = param_expr(
&step.params,
"direction",
FlowExpr::Vec3(
Box::new(FlowExpr::Float(0.3)),
Box::new(FlowExpr::Float(-0.8)),
Box::new(FlowExpr::Float(0.5)),
),
);
let softness = param_expr(&step.params, "softness", FlowExpr::Float(0.15));
let source_dx = param_expr(&step.params, "source_dx", source.clone());
let source_dy = param_expr(&step.params, "source_dy", source.clone());
let precision = param_expr(&step.params, "precision", FlowExpr::Float(0.005));
let gx_name = format!("_s_{}_gx", step.name);
let gy_name = format!("_s_{}_gy", step.name);
let normal_name = format!("_s_{}_n", step.name);
let nodes = vec![
FlowNode {
name: gx_name.clone(),
expr: FlowExpr::Div(
Box::new(FlowExpr::Sub(Box::new(source_dx), Box::new(source.clone()))),
Box::new(precision.clone()),
),
inferred_type: None,
},
FlowNode {
name: gy_name.clone(),
expr: FlowExpr::Div(
Box::new(FlowExpr::Sub(Box::new(source_dy), Box::new(source))),
Box::new(precision),
),
inferred_type: None,
},
FlowNode {
name: normal_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Normalize,
args: vec![FlowExpr::Vec3(
Box::new(FlowExpr::Neg(Box::new(FlowExpr::Ref(gx_name)))),
Box::new(FlowExpr::Neg(Box::new(FlowExpr::Ref(gy_name)))),
Box::new(FlowExpr::Float(1.0)),
)],
},
inferred_type: None,
},
FlowNode {
name: step.name.clone(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Call {
func: FlowFunc::Clamp,
args: vec![
FlowExpr::Call {
func: FlowFunc::Dot,
args: vec![
FlowExpr::Ref(normal_name),
FlowExpr::Call {
func: FlowFunc::Normalize,
args: vec![direction],
},
],
},
FlowExpr::Float(0.0),
FlowExpr::Float(1.0),
],
}),
Box::new(softness),
),
inferred_type: None,
},
];
Ok(nodes)
}
fn expand_pattern_worley(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let uv_input = param_expr(&step.params, "uv", FlowExpr::Ref("uv".to_string()));
let scale = param_expr(&step.params, "scale", FlowExpr::Float(10.0));
let threshold = param_expr(&step.params, "threshold", FlowExpr::Float(0.05));
let edge = param_expr(&step.params, "edge", FlowExpr::Float(0.005));
let mask = param_expr(&step.params, "mask", FlowExpr::Float(1.0));
let sc_name = format!("_s_{}_sc", step.name);
let wg_name = format!("_s_{}_wg", step.name);
let eval_name = format!("_s_{}_eval", step.name);
let gx_name = format!("_s_{}_gx", step.name);
let gy_name = format!("_s_{}_gy", step.name);
let mut nodes = Vec::new();
nodes.push(FlowNode {
name: sc_name.clone(),
expr: uv_input,
inferred_type: None,
});
nodes.push(FlowNode {
name: wg_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::WorleyGrad,
args: vec![FlowExpr::Mul(
Box::new(FlowExpr::Ref(sc_name)),
Box::new(scale),
)],
},
inferred_type: None,
});
nodes.push(FlowNode {
name: eval_name.clone(),
expr: FlowExpr::Swizzle(Box::new(FlowExpr::Ref(wg_name.clone())), "x".to_string()),
inferred_type: None,
});
nodes.push(FlowNode {
name: step.name.clone(),
expr: FlowExpr::Mul(
Box::new(FlowExpr::Call {
func: FlowFunc::Smoothstep,
args: vec![threshold, edge, FlowExpr::Ref(eval_name)],
}),
Box::new(mask),
),
inferred_type: None,
});
nodes.push(FlowNode {
name: gx_name,
expr: FlowExpr::Swizzle(Box::new(FlowExpr::Ref(wg_name.clone())), "y".to_string()),
inferred_type: None,
});
nodes.push(FlowNode {
name: gy_name,
expr: FlowExpr::Swizzle(Box::new(FlowExpr::Ref(wg_name)), "z".to_string()),
inferred_type: None,
});
nodes.push(FlowNode {
name: format!("{}_gx", step.name),
expr: FlowExpr::Ref(format!("_s_{}_gx", step.name)),
inferred_type: None,
});
nodes.push(FlowNode {
name: format!("{}_gy", step.name),
expr: FlowExpr::Ref(format!("_s_{}_gy", step.name)),
inferred_type: None,
});
Ok(nodes)
}
fn expand_effect_refract(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let sources = match step.params.get("sources") {
Some(StepParam::IdentList(list)) => list.clone(),
Some(StepParam::Ident(s)) => vec![s.clone()],
_ => {
return Err(FlowError::InvalidStepParam {
step_name: step.name.clone(),
param: "sources".to_string(),
message: "effect-refract requires 'sources' as a comma-separated list of pattern-worley step names".to_string(),
});
}
};
let weights = match step.params.get("weights") {
Some(StepParam::FloatList(list)) => list.clone(),
Some(StepParam::Expr(FlowExpr::Float(f))) => vec![*f],
_ => vec![1.0; sources.len()],
};
let strength = param_expr(&step.params, "strength", FlowExpr::Float(0.1));
let mut nodes = Vec::new();
let ox_name = format!("_s_{}_ox", step.name);
let oy_name = format!("_s_{}_oy", step.name);
let mut ox_expr: Option<FlowExpr> = None;
let mut oy_expr: Option<FlowExpr> = None;
for (i, src) in sources.iter().enumerate() {
let w = weights.get(i).copied().unwrap_or(1.0);
let gx_ref = format!("_s_{}_gx", src);
let gy_ref = format!("_s_{}_gy", src);
let term_x = FlowExpr::Mul(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref(gx_ref)),
Box::new(strength.clone()),
)),
Box::new(FlowExpr::Float(w)),
)),
Box::new(FlowExpr::Ref(src.clone())),
);
let term_y = FlowExpr::Mul(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref(gy_ref)),
Box::new(strength.clone()),
)),
Box::new(FlowExpr::Float(w)),
)),
Box::new(FlowExpr::Ref(src.clone())),
);
ox_expr = Some(match ox_expr {
None => term_x,
Some(prev) => FlowExpr::Add(Box::new(prev), Box::new(term_x)),
});
oy_expr = Some(match oy_expr {
None => term_y,
Some(prev) => FlowExpr::Add(Box::new(prev), Box::new(term_y)),
});
}
let ox_final = ox_expr.unwrap_or(FlowExpr::Float(0.0));
let oy_final = oy_expr.unwrap_or(FlowExpr::Float(0.0));
nodes.push(FlowNode {
name: ox_name.clone(),
expr: ox_final,
inferred_type: None,
});
nodes.push(FlowNode {
name: oy_name.clone(),
expr: oy_final,
inferred_type: None,
});
nodes.push(FlowNode {
name: step.name.clone(),
expr: FlowExpr::Vec2(
Box::new(FlowExpr::Ref(ox_name)),
Box::new(FlowExpr::Ref(oy_name)),
),
inferred_type: None,
});
Ok(nodes)
}
fn expand_effect_frost(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let strength = param_expr(&step.params, "strength", FlowExpr::Float(0.003));
let mask = param_expr(&step.params, "mask", FlowExpr::Float(1.0));
let scale = param_expr(&step.params, "scale", FlowExpr::Float(30.0));
let nx_name = format!("_s_{}_nx", step.name);
let ny_name = format!("_s_{}_ny", step.name);
let nx_p = format!("_s_{}_nxp", step.name);
let ny_p = format!("_s_{}_nyp", step.name);
let s_name = format!("_s_{}_s", step.name);
let nodes = vec![
FlowNode {
name: nx_p.clone(),
expr: FlowExpr::Mul(
Box::new(FlowExpr::Ref("uv".to_string())),
Box::new(scale.clone()),
),
inferred_type: None,
},
FlowNode {
name: nx_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Fbm,
args: vec![FlowExpr::Ref(nx_p), FlowExpr::Float(2.0)],
},
inferred_type: None,
},
FlowNode {
name: ny_p.clone(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref("uv".to_string())),
Box::new(scale),
)),
Box::new(FlowExpr::Vec2(
Box::new(FlowExpr::Float(100.0)),
Box::new(FlowExpr::Float(0.0)),
)),
),
inferred_type: None,
},
FlowNode {
name: ny_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Fbm,
args: vec![FlowExpr::Ref(ny_p), FlowExpr::Float(2.0)],
},
inferred_type: None,
},
FlowNode {
name: s_name.clone(),
expr: FlowExpr::Mul(Box::new(strength), Box::new(mask)),
inferred_type: None,
},
FlowNode {
name: step.name.clone(),
expr: FlowExpr::Vec2(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Sub(
Box::new(FlowExpr::Ref(nx_name)),
Box::new(FlowExpr::Float(0.5)),
)),
Box::new(FlowExpr::Ref(s_name.clone())),
)),
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Sub(
Box::new(FlowExpr::Ref(ny_name)),
Box::new(FlowExpr::Float(0.5)),
)),
Box::new(FlowExpr::Ref(s_name)),
)),
),
inferred_type: None,
},
];
Ok(nodes)
}
#[allow(clippy::vec_init_then_push)]
fn expand_effect_specular(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let scale = param_expr(&step.params, "scale", FlowExpr::Float(20.0));
let mask = param_expr(&step.params, "mask", FlowExpr::Float(1.0));
let density = param_expr(&step.params, "density", FlowExpr::Float(0.5));
let size = param_expr(&step.params, "size", FlowExpr::Float(0.025));
let gs_name = format!("_s_{}_gs", step.name);
let cs_name = format!("_s_{}_cs", step.name);
let fs_name = format!("_s_{}_fs", step.name);
let ha_name = format!("_s_{}_ha", step.name);
let hb_name = format!("_s_{}_hb", step.name);
let hc_name = format!("_s_{}_hc", step.name);
let d_name = format!("_s_{}_d", step.name);
let mut nodes = Vec::new();
nodes.push(FlowNode {
name: gs_name.clone(),
expr: FlowExpr::Mul(Box::new(FlowExpr::Ref("uv".to_string())), Box::new(scale)),
inferred_type: None,
});
nodes.push(FlowNode {
name: cs_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Floor,
args: vec![FlowExpr::Ref(gs_name.clone())],
},
inferred_type: None,
});
nodes.push(FlowNode {
name: fs_name.clone(),
expr: FlowExpr::Sub(
Box::new(FlowExpr::Call {
func: FlowFunc::Fract,
args: vec![FlowExpr::Ref(gs_name)],
}),
Box::new(FlowExpr::Vec2(
Box::new(FlowExpr::Float(0.5)),
Box::new(FlowExpr::Float(0.5)),
)),
),
inferred_type: None,
});
nodes.push(FlowNode {
name: ha_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Fract,
args: vec![FlowExpr::Mul(
Box::new(FlowExpr::Call {
func: FlowFunc::Sin,
args: vec![FlowExpr::Call {
func: FlowFunc::Dot,
args: vec![
FlowExpr::Ref(cs_name.clone()),
FlowExpr::Vec2(
Box::new(FlowExpr::Float(127.1)),
Box::new(FlowExpr::Float(311.7)),
),
],
}],
}),
Box::new(FlowExpr::Float(43758.5)),
)],
},
inferred_type: None,
});
nodes.push(FlowNode {
name: hb_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Fract,
args: vec![FlowExpr::Mul(
Box::new(FlowExpr::Call {
func: FlowFunc::Sin,
args: vec![FlowExpr::Call {
func: FlowFunc::Dot,
args: vec![
FlowExpr::Ref(cs_name.clone()),
FlowExpr::Vec2(
Box::new(FlowExpr::Float(269.5)),
Box::new(FlowExpr::Float(183.3)),
),
],
}],
}),
Box::new(FlowExpr::Float(43758.5)),
)],
},
inferred_type: None,
});
nodes.push(FlowNode {
name: hc_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Fract,
args: vec![FlowExpr::Mul(
Box::new(FlowExpr::Call {
func: FlowFunc::Sin,
args: vec![FlowExpr::Call {
func: FlowFunc::Dot,
args: vec![
FlowExpr::Ref(cs_name),
FlowExpr::Vec2(
Box::new(FlowExpr::Float(97.3)),
Box::new(FlowExpr::Float(157.1)),
),
],
}],
}),
Box::new(FlowExpr::Float(43758.5)),
)],
},
inferred_type: None,
});
nodes.push(FlowNode {
name: d_name.clone(),
expr: FlowExpr::Sub(
Box::new(FlowExpr::Ref(fs_name)),
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Vec2(
Box::new(FlowExpr::Sub(
Box::new(FlowExpr::Ref(ha_name)),
Box::new(FlowExpr::Float(0.5)),
)),
Box::new(FlowExpr::Sub(
Box::new(FlowExpr::Ref(hb_name)),
Box::new(FlowExpr::Float(0.5)),
)),
)),
Box::new(FlowExpr::Float(0.4)),
)),
),
inferred_type: None,
});
nodes.push(FlowNode {
name: step.name.clone(),
expr: FlowExpr::Mul(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Call {
func: FlowFunc::Smoothstep,
args: vec![
size,
FlowExpr::Float(0.0),
FlowExpr::Call {
func: FlowFunc::Length,
args: vec![FlowExpr::Ref(d_name)],
},
],
}),
Box::new(FlowExpr::Call {
func: FlowFunc::Step,
args: vec![density, FlowExpr::Ref(hc_name)],
}),
)),
Box::new(mask),
),
inferred_type: None,
});
Ok(nodes)
}
#[allow(clippy::vec_init_then_push)]
fn expand_effect_fog(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let source = param_expr(&step.params, "source", FlowExpr::Float(0.0));
let fog_density = param_expr(&step.params, "fog_density", FlowExpr::Float(0.1));
let tint_strength = param_expr(&step.params, "tint_strength", FlowExpr::Float(0.1));
let clear_mask = param_expr(&step.params, "clear_mask", FlowExpr::Float(0.0));
let highlights = param_expr(&step.params, "highlights", FlowExpr::Float(0.0));
let tint_name = format!("_s_{}_tint", step.name);
let r_name = format!("_s_{}_r", step.name);
let g_name = format!("_s_{}_g", step.name);
let b_name = format!("_s_{}_b", step.name);
let a_name = format!("_s_{}_a", step.name);
let mut nodes = Vec::new();
nodes.push(FlowNode {
name: tint_name.clone(),
expr: FlowExpr::Mul(
Box::new(FlowExpr::Sub(
Box::new(FlowExpr::Float(1.0)),
Box::new(clear_mask.clone()),
)),
Box::new(tint_strength),
),
inferred_type: None,
});
nodes.push(FlowNode {
name: r_name.clone(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Add(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Swizzle(Box::new(source.clone()), "x".to_string())),
Box::new(FlowExpr::Sub(
Box::new(FlowExpr::Float(1.0)),
Box::new(FlowExpr::Ref(tint_name.clone())),
)),
)),
Box::new(FlowExpr::Ref(tint_name.clone())),
)),
Box::new(highlights.clone()),
),
inferred_type: None,
});
nodes.push(FlowNode {
name: g_name.clone(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Add(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Swizzle(Box::new(source.clone()), "y".to_string())),
Box::new(FlowExpr::Sub(
Box::new(FlowExpr::Float(1.0)),
Box::new(FlowExpr::Ref(tint_name.clone())),
)),
)),
Box::new(FlowExpr::Ref(tint_name.clone())),
)),
Box::new(highlights.clone()),
),
inferred_type: None,
});
nodes.push(FlowNode {
name: b_name.clone(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Add(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Swizzle(Box::new(source), "z".to_string())),
Box::new(FlowExpr::Sub(
Box::new(FlowExpr::Float(1.0)),
Box::new(FlowExpr::Ref(tint_name.clone())),
)),
)),
Box::new(FlowExpr::Ref(tint_name)),
)),
Box::new(highlights),
),
inferred_type: None,
});
nodes.push(FlowNode {
name: a_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Mix,
args: vec![fog_density, FlowExpr::Float(1.0), clear_mask],
},
inferred_type: None,
});
nodes.push(FlowNode {
name: step.name.clone(),
expr: FlowExpr::Vec4(
Box::new(FlowExpr::Ref(r_name)),
Box::new(FlowExpr::Ref(g_name)),
Box::new(FlowExpr::Ref(b_name)),
Box::new(FlowExpr::Ref(a_name)),
),
inferred_type: None,
});
Ok(nodes)
}
#[allow(clippy::vec_init_then_push)]
fn expand_transform_wet(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let speed = param_expr(&step.params, "speed", FlowExpr::Float(0.02));
let offset = param_expr(
&step.params,
"offset",
FlowExpr::Vec2(
Box::new(FlowExpr::Float(0.0)),
Box::new(FlowExpr::Float(0.0)),
),
);
let x_scale = param_expr(&step.params, "x_scale", FlowExpr::Float(1.0));
let y_scale = param_expr(&step.params, "y_scale", FlowExpr::Float(1.0));
let aspect_name = format!("_s_{}_aspect", step.name);
let scroll_name = format!("_s_{}_scroll", step.name);
let mut nodes = Vec::new();
nodes.push(FlowNode {
name: aspect_name.clone(),
expr: FlowExpr::Div(
Box::new(FlowExpr::Swizzle(
Box::new(FlowExpr::Ref("resolution".to_string())),
"x".to_string(),
)),
Box::new(FlowExpr::Call {
func: FlowFunc::Max,
args: vec![
FlowExpr::Swizzle(
Box::new(FlowExpr::Ref("resolution".to_string())),
"y".to_string(),
),
FlowExpr::Float(1.0),
],
}),
),
inferred_type: None,
});
nodes.push(FlowNode {
name: scroll_name.clone(),
expr: FlowExpr::Mul(Box::new(FlowExpr::Ref("time".to_string())), Box::new(speed)),
inferred_type: None,
});
nodes.push(FlowNode {
name: step.name.clone(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Vec2(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Swizzle(
Box::new(FlowExpr::Ref("uv".to_string())),
"x".to_string(),
)),
Box::new(FlowExpr::Ref(aspect_name)),
)),
Box::new(x_scale),
)),
Box::new(FlowExpr::Sub(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Swizzle(
Box::new(FlowExpr::Ref("uv".to_string())),
"y".to_string(),
)),
Box::new(y_scale),
)),
Box::new(FlowExpr::Ref(scroll_name)),
)),
)),
Box::new(offset),
),
inferred_type: None,
});
Ok(nodes)
}
fn expand_effect_light(step: &FlowStep) -> Result<Vec<FlowNode>, FlowError> {
let sources = match step.params.get("sources") {
Some(StepParam::IdentList(list)) => list.clone(),
_ => {
return Err(FlowError::MissingStepParam {
step_name: step.name.clone(),
param: "sources".to_string(),
});
}
};
let weights: Vec<f32> = match step.params.get("weights") {
Some(StepParam::FloatList(list)) => list.clone(),
_ => vec![1.0; sources.len()],
};
let angle_deg = match step.params.get("angle") {
Some(StepParam::Expr(FlowExpr::Float(f))) => *f,
_ => 225.0,
};
let shininess = param_expr(&step.params, "shininess", FlowExpr::Float(32.0));
let intensity = param_expr(&step.params, "intensity", FlowExpr::Float(0.6));
let mask = param_expr(&step.params, "mask", FlowExpr::Float(1.0));
let angle_rad = angle_deg * std::f32::consts::PI / 180.0;
let lx = angle_rad.cos();
let ly = angle_rad.sin();
let gx_sum_name = format!("_s_{}_gx_sum", step.name);
let gy_sum_name = format!("_s_{}_gy_sum", step.name);
let dot_name = format!("_s_{}_dot", step.name);
let spec_name = format!("_s_{}_spec", step.name);
let mut nodes = Vec::new();
let mut gx_expr: Option<FlowExpr> = None;
let mut gy_expr: Option<FlowExpr> = None;
for (i, src) in sources.iter().enumerate() {
let w = if i < weights.len() { weights[i] } else { 1.0 };
let gx_ref = FlowExpr::Ref(format!("_s_{}_gx", src));
let gy_ref = FlowExpr::Ref(format!("_s_{}_gy", src));
let weighted_gx = FlowExpr::Mul(Box::new(gx_ref), Box::new(FlowExpr::Float(w)));
let weighted_gy = FlowExpr::Mul(Box::new(gy_ref), Box::new(FlowExpr::Float(w)));
gx_expr = Some(match gx_expr {
None => weighted_gx,
Some(prev) => FlowExpr::Add(Box::new(prev), Box::new(weighted_gx)),
});
gy_expr = Some(match gy_expr {
None => weighted_gy,
Some(prev) => FlowExpr::Add(Box::new(prev), Box::new(weighted_gy)),
});
}
nodes.push(FlowNode {
name: gx_sum_name.clone(),
expr: gx_expr.unwrap_or(FlowExpr::Float(0.0)),
inferred_type: None,
});
nodes.push(FlowNode {
name: gy_sum_name.clone(),
expr: gy_expr.unwrap_or(FlowExpr::Float(0.0)),
inferred_type: None,
});
nodes.push(FlowNode {
name: dot_name.clone(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref(gx_sum_name)),
Box::new(FlowExpr::Float(lx)),
)),
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref(gy_sum_name)),
Box::new(FlowExpr::Float(ly)),
)),
),
inferred_type: None,
});
nodes.push(FlowNode {
name: spec_name.clone(),
expr: FlowExpr::Call {
func: FlowFunc::Pow,
args: vec![
FlowExpr::Call {
func: FlowFunc::Max,
args: vec![FlowExpr::Ref(dot_name), FlowExpr::Float(0.0)],
},
shininess,
],
},
inferred_type: None,
});
nodes.push(FlowNode {
name: step.name.clone(),
expr: FlowExpr::Mul(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref(spec_name)),
Box::new(intensity),
)),
Box::new(mask),
),
inferred_type: None,
});
Ok(nodes)
}
impl FlowGraph {
pub fn validate(
&mut self,
flow_registry: Option<&HashMap<String, FlowGraph>>,
) -> Result<(), Vec<FlowError>> {
if !self.steps.is_empty() || !self.chains.is_empty() || !self.uses.is_empty() {
self.expand_semantic_layer(flow_registry)?;
}
let mut errors = Vec::new();
self.check_wgsl_identifiers(&mut errors);
self.check_duplicates(&mut errors);
self.check_references(&mut errors);
match self.topological_sort() {
Ok(order) => self.topo_order = order,
Err(cycle_nodes) => {
errors.push(FlowError::CycleDetected { nodes: cycle_nodes });
}
}
if errors
.iter()
.all(|e| !matches!(e, FlowError::CycleDetected { .. }))
{
self.infer_types(&mut errors);
}
self.check_function_args(&mut errors);
self.check_outputs(&mut errors);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn validate_standalone(&mut self) -> Result<(), Vec<FlowError>> {
self.validate(None)
}
const WGSL_KEYWORDS: &'static [&'static str] = &[
"alias",
"break",
"case",
"const",
"const_assert",
"continue",
"continuing",
"default",
"diagnostic",
"discard",
"else",
"enable",
"false",
"fn",
"for",
"if",
"let",
"loop",
"override",
"return",
"struct",
"switch",
"true",
"var",
"while",
"NULL",
"Self",
"abstract",
"active",
"alignas",
"alignof",
"as",
"asm",
"asm_fragment",
"async",
"attribute",
"auto",
"await",
"become",
"binding_array",
"cast",
"catch",
"class",
"co_await",
"co_return",
"co_yield",
"coherent",
"column_major",
"common",
"compile",
"compile_fragment",
"concept",
"const_cast",
"consteval",
"constexpr",
"constinit",
"crate",
"debugger",
"decltype",
"delete",
"demote",
"demote_to_helper",
"do",
"dynamic_cast",
"enum",
"explicit",
"export",
"extends",
"extern",
"external",
"fallthrough",
"filter",
"final",
"finally",
"friend",
"from",
"fxgroup",
"get",
"goto",
"groupshared",
"highp",
"impl",
"implements",
"import",
"in",
"inline",
"instanceof",
"interface",
"layout",
"lowp",
"macro",
"match",
"mediump",
"meta",
"mod",
"module",
"move",
"mut",
"mutable",
"namespace",
"new",
"nil",
"noexcept",
"noinline",
"nointerpolation",
"noperspective",
"null",
"nullptr",
"of",
"operator",
"package",
"packoffset",
"partition",
"pass",
"patch",
"pixelfragment",
"precise",
"precision",
"premerge",
"priv",
"protected",
"pub",
"public",
"readonly",
"ref",
"regardless",
"register",
"reinterpret_cast",
"require",
"resource",
"restrict",
"self",
"set",
"shared",
"sizeof",
"smooth",
"snorm",
"static",
"static_assert",
"static_cast",
"std",
"subroutine",
"super",
"target",
"template",
"this",
"thread_local",
"throw",
"trait",
"try",
"type",
"typedef",
"typeid",
"typename",
"typeof",
"union",
"unless",
"unorm",
"unsafe",
"unsized",
"use",
"using",
"varying",
"virtual",
"volatile",
"wgsl",
"with",
"writeonly",
"yield",
];
fn check_wgsl_identifiers(&self, errors: &mut Vec<FlowError>) {
let keywords: HashSet<&str> = Self::WGSL_KEYWORDS.iter().copied().collect();
let check = |name: &str, errors: &mut Vec<FlowError>| {
if name.starts_with("__") {
errors.push(FlowError::InvalidIdentifier {
name: name.to_string(),
reason: "identifiers starting with '__' are reserved in WGSL".to_string(),
});
}
if !name
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
{
errors.push(FlowError::InvalidIdentifier {
name: name.to_string(),
reason: "must start with a letter or underscore".to_string(),
});
} else if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
errors.push(FlowError::InvalidIdentifier {
name: name.to_string(),
reason: "must contain only letters, digits, and underscores".to_string(),
});
}
if keywords.contains(name) {
errors.push(FlowError::InvalidIdentifier {
name: name.to_string(),
reason: format!("'{}' is a reserved WGSL keyword", name),
});
}
};
for input in &self.inputs {
check(&input.name, errors);
}
for node in &self.nodes {
check(&node.name, errors);
}
for output in &self.outputs {
check(&output.name, errors);
}
}
fn check_duplicates(&self, errors: &mut Vec<FlowError>) {
let mut names = HashSet::new();
for input in &self.inputs {
if !names.insert(&input.name) {
errors.push(FlowError::DuplicateName {
name: input.name.clone(),
});
}
}
for node in &self.nodes {
if !names.insert(&node.name) {
errors.push(FlowError::DuplicateName {
name: node.name.clone(),
});
}
}
}
fn check_references(&self, errors: &mut Vec<FlowError>) {
let mut all_names: HashSet<String> = HashSet::new();
for input in &self.inputs {
all_names.insert(input.name.clone());
}
for node in &self.nodes {
all_names.insert(node.name.clone());
}
for node in &self.nodes {
let mut refs = HashSet::new();
node.expr.collect_refs(&mut refs);
for r in &refs {
if !all_names.contains(r) {
errors.push(FlowError::UndefinedReference {
in_node: node.name.clone(),
name: r.clone(),
});
}
}
}
for output in &self.outputs {
if let Some(expr) = &output.expr {
let mut refs = HashSet::new();
expr.collect_refs(&mut refs);
for r in &refs {
if !all_names.contains(r) {
errors.push(FlowError::UndefinedReference {
in_node: format!("output:{}", output.name),
name: r.clone(),
});
}
}
} else {
if !all_names.contains(&output.name) {
errors.push(FlowError::UndefinedReference {
in_node: "output".to_string(),
name: output.name.clone(),
});
}
}
}
}
fn topological_sort(&self) -> Result<Vec<usize>, Vec<String>> {
let n = self.nodes.len();
if n == 0 {
return Ok(Vec::new());
}
let mut node_index: HashMap<&str, usize> = HashMap::new();
for (i, node) in self.nodes.iter().enumerate() {
node_index.insert(&node.name, i);
}
let mut in_degree = vec![0usize; n];
let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); n];
for (i, node) in self.nodes.iter().enumerate() {
let mut refs = HashSet::new();
node.expr.collect_refs(&mut refs);
for r in &refs {
if let Some(&dep_idx) = node_index.get(r.as_str()) {
dependents[dep_idx].push(i);
in_degree[i] += 1;
}
}
}
let mut queue: VecDeque<usize> = in_degree
.iter()
.enumerate()
.filter(|(_, °)| deg == 0)
.map(|(i, _)| i)
.collect();
let mut order = Vec::with_capacity(n);
while let Some(idx) = queue.pop_front() {
order.push(idx);
for &dep in &dependents[idx] {
in_degree[dep] -= 1;
if in_degree[dep] == 0 {
queue.push_back(dep);
}
}
}
if order.len() == n {
Ok(order)
} else {
let processed: HashSet<usize> = order.iter().copied().collect();
let cycle_nodes: Vec<String> = (0..n)
.filter(|i| !processed.contains(i))
.map(|i| self.nodes[i].name.clone())
.collect();
Err(cycle_nodes)
}
}
fn infer_types(&mut self, errors: &mut Vec<FlowError>) {
let mut type_map: HashMap<String, FlowType> = HashMap::new();
for input in &self.inputs {
let ty = input.ty.unwrap_or_else(|| match &input.source {
FlowInputSource::Builtin(b) => b.output_type(),
FlowInputSource::Buffer { ty, .. } => *ty,
FlowInputSource::CssProperty(_) | FlowInputSource::EnvVar(_) => FlowType::Float,
FlowInputSource::Auto => FlowType::Float,
});
type_map.insert(input.name.clone(), ty);
}
for &idx in &self.topo_order {
let node = &self.nodes[idx];
match infer_expr_type(&node.expr, &type_map) {
Ok(ty) => {
type_map.insert(node.name.clone(), ty);
self.nodes[idx].inferred_type = Some(ty);
}
Err(msg) => {
errors.push(FlowError::TypeMismatch {
in_node: node.name.clone(),
message: msg,
});
type_map.insert(node.name.clone(), FlowType::Float);
self.nodes[idx].inferred_type = Some(FlowType::Float);
}
}
}
}
fn check_function_args(&self, errors: &mut Vec<FlowError>) {
for node in &self.nodes {
check_args_recursive(&node.expr, &node.name, errors);
}
}
fn check_outputs(&self, errors: &mut Vec<FlowError>) {
match self.target {
FlowTarget::Fragment => {
let has_color = self
.outputs
.iter()
.any(|o| o.target == FlowOutputTarget::Color);
if !has_color && self.outputs.is_empty() {
errors.push(FlowError::MissingOutput {
message: "fragment flow needs at least one output (e.g., 'output color')"
.to_string(),
});
}
}
FlowTarget::Compute => {
let has_buffer = self
.outputs
.iter()
.any(|o| matches!(o.target, FlowOutputTarget::Buffer { .. }));
if !has_buffer && self.outputs.is_empty() {
errors.push(FlowError::MissingOutput {
message: "compute flow needs at least one buffer output".to_string(),
});
}
}
FlowTarget::Vertex => {
let has_position = self
.outputs
.iter()
.any(|o| o.target == FlowOutputTarget::Position);
if !has_position {
errors.push(FlowError::MissingOutput {
message: "vertex flow needs 'output position' (vec4 clip-space)"
.to_string(),
});
}
}
FlowTarget::Material => {
let has_albedo = self
.outputs
.iter()
.any(|o| o.target == FlowOutputTarget::Albedo);
if !has_albedo && self.outputs.is_empty() {
errors.push(FlowError::MissingOutput {
message: "material flow needs at least 'output albedo' (vec4 base color)"
.to_string(),
});
}
}
}
}
}
fn infer_expr_type(
expr: &FlowExpr,
type_map: &HashMap<String, FlowType>,
) -> Result<FlowType, String> {
match expr {
FlowExpr::Float(_) => Ok(FlowType::Float),
FlowExpr::Vec2(_, _) => Ok(FlowType::Vec2),
FlowExpr::Vec3(_, _, _) => Ok(FlowType::Vec3),
FlowExpr::Vec4(_, _, _, _) => Ok(FlowType::Vec4),
FlowExpr::Color(_, _, _, _) => Ok(FlowType::Vec4),
FlowExpr::Ref(name) => type_map
.get(name)
.copied()
.ok_or_else(|| format!("undefined reference '{}'", name)),
FlowExpr::Neg(a) => infer_expr_type(a, type_map),
FlowExpr::Swizzle(_expr, components) => match components.len() {
1 => Ok(FlowType::Float),
2 => Ok(FlowType::Vec2),
3 => Ok(FlowType::Vec3),
4 => Ok(FlowType::Vec4),
_ => Err(format!("invalid swizzle length: {}", components.len())),
},
FlowExpr::Add(a, b) | FlowExpr::Sub(a, b) | FlowExpr::Mul(a, b) | FlowExpr::Div(a, b) => {
let ta = infer_expr_type(a, type_map)?;
let tb = infer_expr_type(b, type_map)?;
ta.broadcast_with(&tb)
.ok_or_else(|| format!("cannot broadcast {} with {}", ta, tb))
}
FlowExpr::Call { func, args } => {
let arg_types: Vec<FlowType> = args
.iter()
.map(|a| infer_expr_type(a, type_map))
.collect::<Result<Vec<_>, _>>()?;
func.return_type(&arg_types)
.ok_or_else(|| format!("cannot determine return type for {:?}", func))
}
}
}
fn check_args_recursive(expr: &FlowExpr, node_name: &str, errors: &mut Vec<FlowError>) {
match expr {
FlowExpr::Call { func, args } => {
let (min, max) = func.arg_count();
if args.len() < min || args.len() > max {
errors.push(FlowError::ArgCountMismatch {
in_node: node_name.to_string(),
func: format!("{:?}", func),
expected: (min, max),
got: args.len(),
});
}
for arg in args {
check_args_recursive(arg, node_name, errors);
}
}
FlowExpr::Add(a, b)
| FlowExpr::Sub(a, b)
| FlowExpr::Mul(a, b)
| FlowExpr::Div(a, b)
| FlowExpr::Vec2(a, b) => {
check_args_recursive(a, node_name, errors);
check_args_recursive(b, node_name, errors);
}
FlowExpr::Vec3(a, b, c) => {
check_args_recursive(a, node_name, errors);
check_args_recursive(b, node_name, errors);
check_args_recursive(c, node_name, errors);
}
FlowExpr::Vec4(a, b, c, d) => {
check_args_recursive(a, node_name, errors);
check_args_recursive(b, node_name, errors);
check_args_recursive(c, node_name, errors);
check_args_recursive(d, node_name, errors);
}
FlowExpr::Neg(a) | FlowExpr::Swizzle(a, _) => check_args_recursive(a, node_name, errors),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_ripple_flow() -> FlowGraph {
let mut graph = FlowGraph::new("ripple");
graph.target = FlowTarget::Fragment;
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
graph.inputs.push(FlowInput {
name: "time".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Time),
ty: Some(FlowType::Float),
});
graph.nodes.push(FlowNode {
name: "dist".to_string(),
expr: FlowExpr::Call {
func: FlowFunc::Distance,
args: vec![
FlowExpr::Ref("uv".to_string()),
FlowExpr::Vec2(
Box::new(FlowExpr::Float(0.5)),
Box::new(FlowExpr::Float(0.5)),
),
],
},
inferred_type: None,
});
graph.nodes.push(FlowNode {
name: "wave".to_string(),
expr: FlowExpr::Call {
func: FlowFunc::Sin,
args: vec![FlowExpr::Sub(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref("dist".to_string())),
Box::new(FlowExpr::Float(20.0)),
)),
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref("time".to_string())),
Box::new(FlowExpr::Float(4.0)),
)),
)],
},
inferred_type: None,
});
graph.nodes.push(FlowNode {
name: "falloff".to_string(),
expr: FlowExpr::Call {
func: FlowFunc::Smoothstep,
args: vec![
FlowExpr::Float(0.6),
FlowExpr::Float(0.0),
FlowExpr::Ref("dist".to_string()),
],
},
inferred_type: None,
});
graph.nodes.push(FlowNode {
name: "height".to_string(),
expr: FlowExpr::Mul(
Box::new(FlowExpr::Mul(
Box::new(FlowExpr::Ref("wave".to_string())),
Box::new(FlowExpr::Ref("falloff".to_string())),
)),
Box::new(FlowExpr::Float(0.15)),
),
inferred_type: None,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Vec4(
Box::new(FlowExpr::Ref("height".to_string())),
Box::new(FlowExpr::Ref("height".to_string())),
Box::new(FlowExpr::Ref("height".to_string())),
Box::new(FlowExpr::Float(1.0)),
)),
});
graph
}
#[test]
fn test_valid_dag_validates() {
let mut graph = make_ripple_flow();
assert!(graph.validate(None).is_ok());
}
#[test]
fn test_topo_order_correct() {
let mut graph = make_ripple_flow();
graph.validate(None).unwrap();
let dist_pos = graph.topo_order.iter().position(|&i| i == 0).unwrap();
let wave_pos = graph.topo_order.iter().position(|&i| i == 1).unwrap();
let falloff_pos = graph.topo_order.iter().position(|&i| i == 2).unwrap();
let height_pos = graph.topo_order.iter().position(|&i| i == 3).unwrap();
assert!(dist_pos < wave_pos);
assert!(dist_pos < falloff_pos);
assert!(wave_pos < height_pos);
assert!(falloff_pos < height_pos);
}
#[test]
fn test_type_inference() {
let mut graph = make_ripple_flow();
graph.validate(None).unwrap();
assert_eq!(graph.nodes[0].inferred_type, Some(FlowType::Float)); assert_eq!(graph.nodes[1].inferred_type, Some(FlowType::Float)); assert_eq!(graph.nodes[2].inferred_type, Some(FlowType::Float)); assert_eq!(graph.nodes[3].inferred_type, Some(FlowType::Float)); }
#[test]
fn test_cycle_detected() {
let mut graph = FlowGraph::new("cyclic");
graph.target = FlowTarget::Fragment;
graph.inputs.push(FlowInput {
name: "x".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Time),
ty: Some(FlowType::Float),
});
graph.nodes.push(FlowNode {
name: "a".to_string(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Ref("b".to_string())),
Box::new(FlowExpr::Float(1.0)),
),
inferred_type: None,
});
graph.nodes.push(FlowNode {
name: "b".to_string(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Ref("a".to_string())),
Box::new(FlowExpr::Float(1.0)),
),
inferred_type: None,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Ref("a".to_string())),
});
let result = graph.validate(None);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors
.iter()
.any(|e| matches!(e, FlowError::CycleDetected { .. })));
}
#[test]
fn test_self_reference_cycle() {
let mut graph = FlowGraph::new("self-ref");
graph.target = FlowTarget::Fragment;
graph.nodes.push(FlowNode {
name: "a".to_string(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Ref("a".to_string())),
Box::new(FlowExpr::Float(1.0)),
),
inferred_type: None,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Ref("a".to_string())),
});
let result = graph.validate(None);
assert!(result.is_err());
}
#[test]
fn test_three_node_cycle() {
let mut graph = FlowGraph::new("triangle");
graph.target = FlowTarget::Fragment;
graph.nodes.push(FlowNode {
name: "a".to_string(),
expr: FlowExpr::Ref("c".to_string()),
inferred_type: None,
});
graph.nodes.push(FlowNode {
name: "b".to_string(),
expr: FlowExpr::Ref("a".to_string()),
inferred_type: None,
});
graph.nodes.push(FlowNode {
name: "c".to_string(),
expr: FlowExpr::Ref("b".to_string()),
inferred_type: None,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Ref("a".to_string())),
});
let result = graph.validate(None);
assert!(result.is_err());
let errors = result.unwrap_err();
let cycle_err = errors
.iter()
.find(|e| matches!(e, FlowError::CycleDetected { .. }));
assert!(cycle_err.is_some());
if let FlowError::CycleDetected { nodes } = cycle_err.unwrap() {
assert_eq!(nodes.len(), 3);
}
}
#[test]
fn test_undefined_reference() {
let mut graph = FlowGraph::new("bad-ref");
graph.target = FlowTarget::Fragment;
graph.nodes.push(FlowNode {
name: "a".to_string(),
expr: FlowExpr::Ref("nonexistent".to_string()),
inferred_type: None,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Ref("a".to_string())),
});
let result = graph.validate(None);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors
.iter()
.any(|e| matches!(e, FlowError::UndefinedReference { .. })));
}
#[test]
fn test_duplicate_name() {
let mut graph = FlowGraph::new("dup");
graph.target = FlowTarget::Fragment;
graph.inputs.push(FlowInput {
name: "x".to_string(),
source: FlowInputSource::Auto,
ty: Some(FlowType::Float),
});
graph.nodes.push(FlowNode {
name: "x".to_string(), expr: FlowExpr::Float(1.0),
inferred_type: None,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Ref("x".to_string())),
});
let result = graph.validate(None);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors
.iter()
.any(|e| matches!(e, FlowError::DuplicateName { .. })));
}
#[test]
fn test_type_broadcast() {
let mut graph = FlowGraph::new("broadcast");
graph.target = FlowTarget::Fragment;
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
graph.nodes.push(FlowNode {
name: "scaled".to_string(),
expr: FlowExpr::Mul(
Box::new(FlowExpr::Ref("uv".to_string())),
Box::new(FlowExpr::Float(2.0)),
),
inferred_type: None,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Ref("scaled".to_string())),
});
graph.validate(None).unwrap();
assert_eq!(graph.nodes[0].inferred_type, Some(FlowType::Vec2));
}
#[test]
fn test_incompatible_types() {
let mut graph = FlowGraph::new("bad-types");
graph.target = FlowTarget::Fragment;
graph.inputs.push(FlowInput {
name: "a".to_string(),
source: FlowInputSource::Auto,
ty: Some(FlowType::Vec2),
});
graph.inputs.push(FlowInput {
name: "b".to_string(),
source: FlowInputSource::Auto,
ty: Some(FlowType::Vec3),
});
graph.nodes.push(FlowNode {
name: "c".to_string(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Ref("a".to_string())),
Box::new(FlowExpr::Ref("b".to_string())),
),
inferred_type: None,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Ref("c".to_string())),
});
let result = graph.validate(None);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors
.iter()
.any(|e| matches!(e, FlowError::TypeMismatch { .. })));
}
#[test]
fn test_compute_flow_needs_buffer_output() {
let mut graph = FlowGraph::new("compute-no-output");
graph.target = FlowTarget::Compute;
graph.workgroup_size = Some(64);
graph.inputs.push(FlowInput {
name: "pos".to_string(),
source: FlowInputSource::Buffer {
name: "positions".to_string(),
ty: FlowType::Vec4,
},
ty: Some(FlowType::Vec4),
});
graph.nodes.push(FlowNode {
name: "new_pos".to_string(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Ref("pos".to_string())),
Box::new(FlowExpr::Float(1.0)),
),
inferred_type: None,
});
let result = graph.validate(None);
assert!(result.is_err());
}
#[test]
fn test_compute_flow_with_buffer_output() {
let mut graph = FlowGraph::new("compute-ok");
graph.target = FlowTarget::Compute;
graph.workgroup_size = Some(64);
graph.inputs.push(FlowInput {
name: "pos".to_string(),
source: FlowInputSource::Buffer {
name: "positions".to_string(),
ty: FlowType::Vec4,
},
ty: Some(FlowType::Vec4),
});
graph.nodes.push(FlowNode {
name: "new_pos".to_string(),
expr: FlowExpr::Add(
Box::new(FlowExpr::Ref("pos".to_string())),
Box::new(FlowExpr::Float(1.0)),
),
inferred_type: None,
});
graph.outputs.push(FlowOutput {
name: "positions".to_string(),
target: FlowOutputTarget::Buffer {
name: "positions".to_string(),
},
expr: Some(FlowExpr::Ref("new_pos".to_string())),
});
assert!(graph.validate(None).is_ok());
}
#[test]
fn test_empty_fragment_flow_errors() {
let mut graph = FlowGraph::new("empty");
graph.target = FlowTarget::Fragment;
let result = graph.validate(None);
assert!(result.is_err());
}
#[test]
fn test_flowfunc_from_str() {
assert_eq!(FlowFunc::parse("sin"), Some(FlowFunc::Sin));
assert_eq!(FlowFunc::parse("distance"), Some(FlowFunc::Distance));
assert_eq!(
FlowFunc::parse("sdf-smooth-union"),
Some(FlowFunc::SdfSmoothUnion)
);
assert_eq!(
FlowFunc::parse("sdf_smooth_union"),
Some(FlowFunc::SdfSmoothUnion)
);
assert_eq!(FlowFunc::parse("unknown"), None);
}
#[test]
fn test_type_broadcast_rules() {
assert_eq!(
FlowType::Float.broadcast_with(&FlowType::Float),
Some(FlowType::Float)
);
assert_eq!(
FlowType::Float.broadcast_with(&FlowType::Vec3),
Some(FlowType::Vec3)
);
assert_eq!(
FlowType::Vec3.broadcast_with(&FlowType::Float),
Some(FlowType::Vec3)
);
assert_eq!(FlowType::Vec2.broadcast_with(&FlowType::Vec3), None);
assert_eq!(
FlowType::Vec4.broadcast_with(&FlowType::Vec4),
Some(FlowType::Vec4)
);
}
#[test]
fn test_step_type_from_str() {
assert_eq!(
StepType::parse("pattern-noise"),
Some(StepType::PatternNoise)
);
assert_eq!(StepType::parse("color-ramp"), Some(StepType::ColorRamp));
assert_eq!(
StepType::parse("compose-blend"),
Some(StepType::ComposeBlend)
);
assert_eq!(
StepType::parse("transform-warp"),
Some(StepType::TransformWarp)
);
assert_eq!(
StepType::parse("surface-light"),
Some(StepType::SurfaceLight)
);
assert_eq!(
StepType::parse("adjust-falloff"),
Some(StepType::AdjustFalloff)
);
assert_eq!(StepType::parse("unknown-step"), None);
}
#[test]
fn test_expand_pattern_noise_produces_valid_nodes() {
let mut graph = FlowGraph::new("test_noise");
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
graph.inputs.push(FlowInput {
name: "time".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Time),
ty: Some(FlowType::Float),
});
let mut params = HashMap::new();
params.insert("scale".to_string(), StepParam::Expr(FlowExpr::Float(3.0)));
params.insert("detail".to_string(), StepParam::Int(4));
graph.steps.push(FlowStep {
name: "n".to_string(),
step_type: StepType::PatternNoise,
params,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Vec4(
Box::new(FlowExpr::Ref("n".to_string())),
Box::new(FlowExpr::Ref("n".to_string())),
Box::new(FlowExpr::Ref("n".to_string())),
Box::new(FlowExpr::Float(1.0)),
)),
});
graph.validate(None).unwrap();
assert!(graph.steps.is_empty());
assert!(graph.nodes.iter().any(|n| n.name == "n"));
assert!(graph.nodes.iter().any(|n| n.name.contains("_s_n_")));
}
#[test]
fn test_expand_pattern_ripple() {
let mut graph = FlowGraph::new("test_ripple");
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
graph.inputs.push(FlowInput {
name: "time".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Time),
ty: Some(FlowType::Float),
});
let mut params = HashMap::new();
params.insert(
"center".to_string(),
StepParam::Expr(FlowExpr::Vec2(
Box::new(FlowExpr::Float(0.5)),
Box::new(FlowExpr::Float(0.5)),
)),
);
graph.steps.push(FlowStep {
name: "r".to_string(),
step_type: StepType::PatternRipple,
params,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Vec4(
Box::new(FlowExpr::Ref("r".to_string())),
Box::new(FlowExpr::Ref("r".to_string())),
Box::new(FlowExpr::Ref("r".to_string())),
Box::new(FlowExpr::Float(1.0)),
)),
});
graph.validate(None).unwrap();
assert!(graph.steps.is_empty());
assert!(graph.nodes.iter().any(|n| n.name == "r"));
}
#[test]
fn test_expand_color_ramp() {
let mut graph = FlowGraph::new("test_ramp");
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
graph.nodes.push(FlowNode {
name: "val".to_string(),
expr: FlowExpr::Swizzle(Box::new(FlowExpr::Ref("uv".to_string())), "x".to_string()),
inferred_type: None,
});
let mut params = HashMap::new();
params.insert(
"source".to_string(),
StepParam::Expr(FlowExpr::Ref("val".to_string())),
);
params.insert(
"stops".to_string(),
StepParam::ColorStops(vec![
(FlowExpr::Color(0.0, 0.0, 0.0, 1.0), 0.0),
(FlowExpr::Color(1.0, 1.0, 1.0, 1.0), 1.0),
]),
);
graph.steps.push(FlowStep {
name: "c".to_string(),
step_type: StepType::ColorRamp,
params,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Ref("c".to_string())),
});
graph.validate(None).unwrap();
assert!(graph.steps.is_empty());
assert!(graph.nodes.iter().any(|n| n.name == "c"));
assert!(graph.nodes.iter().any(|n| n.name.contains("_s_c_")));
}
#[test]
fn test_chain_desugaring() {
let mut graph = FlowGraph::new("test_chain");
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
graph.inputs.push(FlowInput {
name: "time".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Time),
ty: Some(FlowType::Float),
});
let mut link1_params = HashMap::new();
link1_params.insert(
"center".to_string(),
StepParam::Expr(FlowExpr::Vec2(
Box::new(FlowExpr::Float(0.5)),
Box::new(FlowExpr::Float(0.5)),
)),
);
let mut link2_params = HashMap::new();
link2_params.insert("radius".to_string(), StepParam::Expr(FlowExpr::Float(0.5)));
graph.chains.push(FlowChain {
name: "effect".to_string(),
links: vec![
ChainLink {
step_type: StepType::PatternRipple,
params: link1_params,
},
ChainLink {
step_type: StepType::AdjustFalloff,
params: link2_params,
},
],
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Vec4(
Box::new(FlowExpr::Ref("effect".to_string())),
Box::new(FlowExpr::Ref("effect".to_string())),
Box::new(FlowExpr::Ref("effect".to_string())),
Box::new(FlowExpr::Float(1.0)),
)),
});
graph.validate(None).unwrap();
assert!(graph.chains.is_empty());
assert!(graph.steps.is_empty());
assert!(graph.nodes.iter().any(|n| n.name == "effect"));
}
#[test]
fn test_use_composition() {
let mut base = FlowGraph::new("noise-base");
base.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
base.inputs.push(FlowInput {
name: "time".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Time),
ty: Some(FlowType::Float),
});
base.nodes.push(FlowNode {
name: "n".to_string(),
expr: FlowExpr::Call {
func: FlowFunc::Fbm,
args: vec![FlowExpr::Ref("uv".to_string()), FlowExpr::Float(4.0)],
},
inferred_type: None,
});
base.outputs.push(FlowOutput {
name: "value".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Ref("n".to_string())),
});
let mut registry = HashMap::new();
registry.insert("noise-base".to_string(), base);
let mut graph = FlowGraph::new("colored");
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
graph.inputs.push(FlowInput {
name: "time".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Time),
ty: Some(FlowType::Float),
});
graph.uses.push(FlowUse {
flow_name: "noise-base".to_string(),
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Vec4(
Box::new(FlowExpr::Ref("noise_base_n".to_string())),
Box::new(FlowExpr::Ref("noise_base_n".to_string())),
Box::new(FlowExpr::Ref("noise_base_n".to_string())),
Box::new(FlowExpr::Float(1.0)),
)),
});
graph.validate(Some(®istry)).unwrap();
assert!(graph.uses.is_empty());
assert!(graph.nodes.iter().any(|n| n.name == "noise_base_n"));
}
#[test]
fn test_missing_step_param_errors() {
let step = FlowStep {
name: "bad".to_string(),
step_type: StepType::ColorRamp,
params: HashMap::new(), };
let mut graph = FlowGraph::new("test_bad");
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
graph.steps.push(step);
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Float(1.0)),
});
let result = graph.validate(None);
assert!(result.is_err());
let errs = result.unwrap_err();
assert!(errs
.iter()
.any(|e| matches!(e, FlowError::MissingStepParam { .. })));
}
#[test]
fn test_use_flow_not_found_errors() {
let mut graph = FlowGraph::new("test_missing_use");
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
graph.uses.push(FlowUse {
flow_name: "nonexistent".to_string(),
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Float(1.0)),
});
let result = graph.validate(None);
assert!(result.is_err());
let errs = result.unwrap_err();
assert!(errs
.iter()
.any(|e| matches!(e, FlowError::FlowNotFound { .. })));
}
#[test]
fn test_mixed_step_and_node() {
let mut graph = FlowGraph::new("test_mixed");
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
graph.inputs.push(FlowInput {
name: "time".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Time),
ty: Some(FlowType::Float),
});
graph.nodes.push(FlowNode {
name: "speed".to_string(),
expr: FlowExpr::Float(0.5),
inferred_type: None,
});
let mut params = HashMap::new();
params.insert("scale".to_string(), StepParam::Expr(FlowExpr::Float(3.0)));
params.insert(
"animation".to_string(),
StepParam::Expr(FlowExpr::Mul(
Box::new(FlowExpr::Ref("time".to_string())),
Box::new(FlowExpr::Ref("speed".to_string())),
)),
);
graph.steps.push(FlowStep {
name: "n".to_string(),
step_type: StepType::PatternNoise,
params,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Vec4(
Box::new(FlowExpr::Ref("n".to_string())),
Box::new(FlowExpr::Ref("n".to_string())),
Box::new(FlowExpr::Ref("n".to_string())),
Box::new(FlowExpr::Float(1.0)),
)),
});
graph.validate(None).unwrap();
assert!(graph.nodes.iter().any(|n| n.name == "speed"));
assert!(graph.nodes.iter().any(|n| n.name == "n"));
}
#[test]
fn test_compose_blend_expansion() {
let mut graph = FlowGraph::new("test_blend");
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
graph.nodes.push(FlowNode {
name: "a".to_string(),
expr: FlowExpr::Vec4(
Box::new(FlowExpr::Float(1.0)),
Box::new(FlowExpr::Float(0.0)),
Box::new(FlowExpr::Float(0.0)),
Box::new(FlowExpr::Float(1.0)),
),
inferred_type: None,
});
graph.nodes.push(FlowNode {
name: "b".to_string(),
expr: FlowExpr::Vec4(
Box::new(FlowExpr::Float(0.0)),
Box::new(FlowExpr::Float(1.0)),
Box::new(FlowExpr::Float(0.0)),
Box::new(FlowExpr::Float(1.0)),
),
inferred_type: None,
});
let mut params = HashMap::new();
params.insert(
"a".to_string(),
StepParam::Expr(FlowExpr::Ref("a".to_string())),
);
params.insert(
"b".to_string(),
StepParam::Expr(FlowExpr::Ref("b".to_string())),
);
params.insert("mode".to_string(), StepParam::Ident("multiply".to_string()));
graph.steps.push(FlowStep {
name: "blended".to_string(),
step_type: StepType::ComposeBlend,
params,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Ref("blended".to_string())),
});
graph.validate(None).unwrap();
assert!(graph.nodes.iter().any(|n| n.name == "blended"));
}
#[test]
fn test_step_expansion_pattern_worley() {
let mut graph = FlowGraph::new("test_worley");
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
let mut params = HashMap::new();
params.insert("scale".to_string(), StepParam::Expr(FlowExpr::Float(18.0)));
params.insert(
"threshold".to_string(),
StepParam::Expr(FlowExpr::Float(0.05)),
);
params.insert("mask".to_string(), StepParam::Expr(FlowExpr::Float(1.0)));
graph.steps.push(FlowStep {
name: "w".to_string(),
step_type: StepType::PatternWorley,
params,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Vec4(
Box::new(FlowExpr::Ref("w".to_string())),
Box::new(FlowExpr::Ref("w".to_string())),
Box::new(FlowExpr::Ref("w".to_string())),
Box::new(FlowExpr::Float(1.0)),
)),
});
graph.validate(None).unwrap();
assert!(graph.steps.is_empty());
assert!(graph.nodes.iter().any(|n| n.name == "w"));
assert!(graph.nodes.iter().any(|n| n.name == "_s_w_eval"));
assert!(graph.nodes.iter().any(|n| n.name == "_s_w_gx"));
assert!(graph.nodes.iter().any(|n| n.name == "_s_w_gy"));
assert!(graph.nodes.iter().any(|n| n.name == "_s_w_sc"));
}
#[test]
fn test_step_expansion_effect_refract() {
let mut graph = FlowGraph::new("test_refract");
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
let mut wp = HashMap::new();
wp.insert("scale".to_string(), StepParam::Expr(FlowExpr::Float(18.0)));
graph.steps.push(FlowStep {
name: "d1".to_string(),
step_type: StepType::PatternWorley,
params: wp,
});
let mut rp = HashMap::new();
rp.insert(
"sources".to_string(),
StepParam::IdentList(vec!["d1".to_string()]),
);
rp.insert(
"strength".to_string(),
StepParam::Expr(FlowExpr::Float(0.15)),
);
graph.steps.push(FlowStep {
name: "r".to_string(),
step_type: StepType::EffectRefract,
params: rp,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Vec4(
Box::new(FlowExpr::Swizzle(
Box::new(FlowExpr::Ref("r".to_string())),
"x".to_string(),
)),
Box::new(FlowExpr::Swizzle(
Box::new(FlowExpr::Ref("r".to_string())),
"y".to_string(),
)),
Box::new(FlowExpr::Float(0.0)),
Box::new(FlowExpr::Float(1.0)),
)),
});
graph.validate(None).unwrap();
assert!(graph.nodes.iter().any(|n| n.name == "r"));
assert!(graph.nodes.iter().any(|n| n.name == "_s_r_ox"));
assert!(graph.nodes.iter().any(|n| n.name == "_s_r_oy"));
}
#[test]
fn test_step_expansion_effect_frost() {
let mut graph = FlowGraph::new("test_frost");
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
let mut params = HashMap::new();
params.insert(
"strength".to_string(),
StepParam::Expr(FlowExpr::Float(0.003)),
);
params.insert("scale".to_string(), StepParam::Expr(FlowExpr::Float(30.0)));
graph.steps.push(FlowStep {
name: "f".to_string(),
step_type: StepType::EffectFrost,
params,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Vec4(
Box::new(FlowExpr::Swizzle(
Box::new(FlowExpr::Ref("f".to_string())),
"x".to_string(),
)),
Box::new(FlowExpr::Swizzle(
Box::new(FlowExpr::Ref("f".to_string())),
"y".to_string(),
)),
Box::new(FlowExpr::Float(0.0)),
Box::new(FlowExpr::Float(1.0)),
)),
});
graph.validate(None).unwrap();
assert!(graph.steps.is_empty());
assert!(graph.nodes.iter().any(|n| n.name == "f"));
assert!(graph.nodes.iter().any(|n| n.name == "_s_f_nx"));
assert!(graph.nodes.iter().any(|n| n.name == "_s_f_ny"));
}
#[test]
fn test_step_expansion_effect_specular() {
let mut graph = FlowGraph::new("test_spec");
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
let mut params = HashMap::new();
params.insert("scale".to_string(), StepParam::Expr(FlowExpr::Float(20.0)));
params.insert("mask".to_string(), StepParam::Expr(FlowExpr::Float(1.0)));
graph.steps.push(FlowStep {
name: "s".to_string(),
step_type: StepType::EffectSpecular,
params,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Vec4(
Box::new(FlowExpr::Ref("s".to_string())),
Box::new(FlowExpr::Ref("s".to_string())),
Box::new(FlowExpr::Ref("s".to_string())),
Box::new(FlowExpr::Float(1.0)),
)),
});
graph.validate(None).unwrap();
assert!(graph.steps.is_empty());
assert!(graph.nodes.iter().any(|n| n.name == "s"));
assert!(graph.nodes.iter().any(|n| n.name == "_s_s_gs"));
assert!(graph.nodes.iter().any(|n| n.name == "_s_s_ha"));
assert!(graph.nodes.iter().any(|n| n.name == "_s_s_d"));
}
#[test]
fn test_step_expansion_effect_fog() {
let mut graph = FlowGraph::new("test_fog");
graph.inputs.push(FlowInput {
name: "uv".to_string(),
source: FlowInputSource::Builtin(BuiltinVar::Uv),
ty: Some(FlowType::Vec2),
});
graph.nodes.push(FlowNode {
name: "sc".to_string(),
expr: FlowExpr::Vec4(
Box::new(FlowExpr::Float(0.5)),
Box::new(FlowExpr::Float(0.5)),
Box::new(FlowExpr::Float(0.5)),
Box::new(FlowExpr::Float(1.0)),
),
inferred_type: None,
});
let mut params = HashMap::new();
params.insert(
"source".to_string(),
StepParam::Expr(FlowExpr::Ref("sc".to_string())),
);
params.insert(
"fog_density".to_string(),
StepParam::Expr(FlowExpr::Float(0.1)),
);
params.insert(
"clear_mask".to_string(),
StepParam::Expr(FlowExpr::Float(0.5)),
);
graph.steps.push(FlowStep {
name: "fg".to_string(),
step_type: StepType::EffectFog,
params,
});
graph.outputs.push(FlowOutput {
name: "color".to_string(),
target: FlowOutputTarget::Color,
expr: Some(FlowExpr::Ref("fg".to_string())),
});
graph.validate(None).unwrap();
assert!(graph.steps.is_empty());
assert!(graph.nodes.iter().any(|n| n.name == "fg"));
assert!(graph.nodes.iter().any(|n| n.name == "_s_fg_tint"));
assert!(graph.nodes.iter().any(|n| n.name == "_s_fg_r"));
assert!(graph.nodes.iter().any(|n| n.name == "_s_fg_a"));
}
#[test]
fn test_step_type_from_str_new_types() {
assert_eq!(
StepType::parse("pattern-worley"),
Some(StepType::PatternWorley)
);
assert_eq!(
StepType::parse("effect-refract"),
Some(StepType::EffectRefract)
);
assert_eq!(StepType::parse("effect-frost"), Some(StepType::EffectFrost));
assert_eq!(
StepType::parse("effect-specular"),
Some(StepType::EffectSpecular)
);
assert_eq!(StepType::parse("effect-fog"), Some(StepType::EffectFog));
}
}