1use std::collections::HashMap;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum MaterialSocket {
18 Color, Float, Vec2, Vec3, Mask, }
24
25#[derive(Debug, Clone)]
27pub enum MaterialOp {
28 InputColor,
31
32 ConstantColor { r: f32, g: f32, b: f32, a: f32 },
36
37 SampleTexture { tex_index: u32 },
40
41 PremultipliedBlend,
45
46 SDFRoundRect,
50
51 SDFEllipse,
54
55 LinearGradient { start: [f32; 4], end: [f32; 4] },
59
60 RadialGradient { start: [f32; 4], end: [f32; 4] },
64
65 NeonGlow { radius: f32, intensity: f32 },
69
70 GlassBlur,
74
75 LayerBlend { mode: BlendMode },
79
80 PBRLighting,
84
85 DropShadow,
89
90 NineSlice,
94
95 Heatmap,
99
100 Raymarch { shape: RaymarchShape },
103}
104
105#[derive(Debug, Clone, Copy)]
106pub enum BlendMode {
107 Add,
108 Screen,
109 Multiply,
110 Overlay,
111}
112
113#[derive(Debug, Clone, Copy)]
114pub enum RaymarchShape {
115 Sphere,
116 Box,
117}
118
119#[derive(Debug, Clone)]
121pub struct MaterialEdge {
122 pub from_node: u32,
123 pub from_socket: MaterialSocket,
124 pub to_node: u32,
125 pub to_socket: MaterialSocket,
126}
127
128pub type MatNodeId = u32;
130
131#[derive(Debug, Clone)]
133pub struct MaterialGraph {
134 pub nodes: Vec<(MatNodeId, MaterialOp)>,
135 pub edges: Vec<MaterialEdge>,
136 pub output: Option<MatNodeId>,
137}
138
139impl MaterialGraph {
140 pub fn new() -> Self {
141 Self {
142 nodes: Vec::new(),
143 edges: Vec::new(),
144 output: None,
145 }
146 }
147
148 pub fn add_node(&mut self, op: MaterialOp) -> MatNodeId {
149 let id = self.nodes.len() as MatNodeId;
150 self.nodes.push((id, op));
151 id
152 }
153
154 pub fn connect(
155 &mut self,
156 from: MatNodeId,
157 from_socket: MaterialSocket,
158 to: MatNodeId,
159 to_socket: MaterialSocket,
160 ) {
161 self.edges.push(MaterialEdge {
162 from_node: from,
163 from_socket,
164 to_node: to,
165 to_socket,
166 });
167 }
168
169 pub fn set_output(&mut self, node: MatNodeId) {
170 self.output = Some(node);
171 }
172
173 pub fn validate(&self) -> Result<(), MaterialError> {
175 if self.output.is_none() {
176 return Err(MaterialError::NoOutput);
177 }
178 let mut visited = vec![false; self.nodes.len()];
180 let mut in_stack = vec![false; self.nodes.len()];
181
182 for &(id, _) in &self.nodes {
183 if !visited[id as usize] {
184 self.dfs_check(id, &mut visited, &mut in_stack)?;
185 }
186 }
187 Ok(())
188 }
189
190 fn dfs_check(
191 &self,
192 node: MatNodeId,
193 visited: &mut [bool],
194 in_stack: &mut [bool],
195 ) -> Result<(), MaterialError> {
196 let idx = node as usize;
197 if in_stack[idx] {
198 return Err(MaterialError::Cycle);
199 }
200 if visited[idx] {
201 return Ok(());
202 }
203 visited[idx] = true;
204 in_stack[idx] = true;
205
206 for edge in &self.edges {
208 if edge.to_node == node {
209 self.dfs_check(edge.from_node, visited, in_stack)?;
210 }
211 }
212
213 in_stack[idx] = false;
214 Ok(())
215 }
216}
217
218impl Default for MaterialGraph {
219 fn default() -> Self {
220 Self::new()
221 }
222}
223
224#[derive(Debug)]
225pub enum MaterialError {
226 NoOutput,
227 Cycle,
228 DisconnectedInput { node: MatNodeId, socket: MaterialSocket },
229 TypeMismatch { from: MaterialSocket, to: MaterialSocket },
230 CompileError(String),
231}
232
233impl std::error::Error for MaterialError {}
234
235impl std::fmt::Display for MaterialError {
236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237 match self {
238 Self::NoOutput => write!(f, "material graph has no output node"),
239 Self::Cycle => write!(f, "material graph contains a cycle"),
240 Self::DisconnectedInput { node, socket } => {
241 write!(f, "node {:?} missing input {:?}", node, socket)
242 }
243 Self::TypeMismatch { from, to } => {
244 write!(f, "type mismatch: {:?} -> {:?}", from, to)
245 }
246 Self::CompileError(msg) => write!(f, "compile error: {}", msg),
247 }
248 }
249}
250
251#[derive(Debug, Clone)]
253pub struct CompiledMaterial {
254 pub wgsl_fn: String,
256 pub fn_name: String,
258}
259
260pub struct MaterialCompiler;
262
263impl MaterialCompiler {
264 pub fn compile(graph: &MaterialGraph) -> Result<CompiledMaterial, MaterialError> {
275 graph.validate()?;
276
277 let order = Self::topo_sort(graph)?;
279
280 let mut lines: Vec<String> = Vec::new();
282 let mut var_names: HashMap<(MatNodeId, MaterialSocket), String> = HashMap::new();
283 let mut next_var = 0;
284
285 let mut mk_var = |prefix: &str| -> String {
286 let v = format!("{}_{}", prefix, next_var);
287 next_var += 1;
288 v
289 };
290
291 for &node_id in &order {
292 let (_, op) = &graph.nodes[node_id as usize];
293 let result_var = mk_var("v");
294
295 let expr = match op {
296 MaterialOp::InputColor => {
297 "col".to_string()
298 }
299 MaterialOp::ConstantColor { r, g, b, a } => {
300 format!("vec4<f32>({:.6}, {:.6}, {:.6}, {:.6})", r, g, b, a)
301 }
302 MaterialOp::SampleTexture { tex_index } => {
303 format!(
304 "textureSample(t_diffuse[{}u], s_diffuse, in.uv)",
305 tex_index
306 )
307 }
308 MaterialOp::PremultipliedBlend => {
309 let color_var = Self::find_input(&var_names, node_id, MaterialSocket::Color, graph)
310 .unwrap_or_else(|| "col".to_string());
311 let alpha_var = Self::find_input(&var_names, node_id, MaterialSocket::Float, graph)
313 .unwrap_or_else(|| "1.0".to_string());
314 format!(
315 "vec4<f32>(({}).rgb, ({}).a * ({}))",
316 color_var, color_var, alpha_var
317 )
318 }
319 MaterialOp::SDFRoundRect => {
320 let half = "in.size * 0.5";
321 format!(
322 r#"
323 let _d = sd_round_rect(in.logical - {0}, {0} - in.radius, in.radius);
324 let _aa = fwidth(_d);
325 vec4<f32>(col.rgb, col.a * (1.0 - smoothstep(0.0, _aa, _d)))"#,
326 half
327 ).trim().to_string()
328 }
329 MaterialOp::SDFEllipse => {
330 let half = "in.size * 0.5";
331 format!(
332 r#"
333 let _sh = max({0}, vec2<f32>(0.001));
334 let _d = length((in.logical - {0}) / _sh) - 1.0;
335 let _aa = fwidth(_d);
336 vec4<f32>(col.rgb, col.a * (1.0 - smoothstep(0.0, _aa, _d)))"#,
337 half
338 ).trim().to_string()
339 }
340 MaterialOp::LinearGradient { start, end } => {
341 let t_var = Self::find_input(&var_names, node_id, MaterialSocket::Float, graph)
342 .unwrap_or_else(|| "in.uv.x".to_string());
343 format!(
344 "mix(vec4<f32>({:.6},{:.6},{:.6},{:.6}), vec4<f32>({:.6},{:.6},{:.6},{:.6}), clamp({}, 0.0, 1.0))",
345 start[0], start[1], start[2], start[3],
346 end[0], end[1], end[2], end[3],
347 t_var
348 )
349 }
350 MaterialOp::RadialGradient { start, end } => {
351 format!(
352 r#"
353 let _dist = length(in.uv - 0.5) * 2.0;
354 mix(vec4<f32>({:.6},{:.6},{:.6},{:.6}), vec4<f32>({:.6},{:.6},{:.6},{:.6}), clamp(_dist, 0.0, 1.0))"#,
355 start[0], start[1], start[2], start[3],
356 end[0], end[1], end[2], end[3],
357 ).trim().to_string()
358 }
359 MaterialOp::NeonGlow { radius, intensity } => {
360 let dist_var = Self::find_input(&var_names, node_id, MaterialSocket::Float, graph)
361 .unwrap_or_else(|| "length(in.logical - in.size * 0.5) / max(in.size.x, in.size.y)".to_string());
362 format!(
363 "vec4<f32>(col.rgb * exp(-{} * {:.6}), col.a)",
364 dist_var, intensity / radius.max(0.001)
365 )
366 }
367 MaterialOp::GlassBlur => {
368 r#"
369 let _uv = clamp(in.uv, vec2<f32>(0.0), vec2<f32>(1.0));
370 let _blur_mip = theme.glass_blur_strength;
371 let _env_base = textureSampleLevel(t_env, s_env, _uv, _blur_mip).rgb;
372 vec4<f32>(_env_base, 0.02 + pow(length(in.logical / in.size - 0.5) * 1.8, 2.5) * 0.15)"#.trim().to_string()
373 }
374 MaterialOp::LayerBlend { mode } => {
375 let bottom = Self::find_input(&var_names, node_id, MaterialSocket::Color, graph)
376 .unwrap_or_else(|| "col".to_string());
377 let top = Self::find_input_map(&var_names, node_id, MaterialSocket::Color, graph, 1)
378 .unwrap_or_else(|| "col".to_string());
379 let opacity = Self::find_input(&var_names, node_id, MaterialSocket::Float, graph)
380 .unwrap_or_else(|| "1.0".to_string());
381 match mode {
382 BlendMode::Add => {
383 format!("mix({}, {}, {})", bottom, top, opacity)
384 }
385 BlendMode::Screen => {
386 format!("mix({}, 1.0 - (1.0 - {}) * (1.0 - {}), {})", bottom, bottom, top, opacity)
387 }
388 BlendMode::Multiply => {
389 format!("mix({}, {} * {}, {})", bottom, bottom, top, opacity)
390 }
391 BlendMode::Overlay => {
392 format!("mix({}, select(2.0 * {} * {}, 1.0 - 2.0 * (1.0 - {}) * (1.0 - {}), step(vec4<f32>(0.5), {})), {})", bottom, bottom, top, bottom, top, bottom, opacity)
393 }
394 }
395 }
396 MaterialOp::PBRLighting => {
397 r#"
398 let _n = normalize(in.normal);
399 let _metallic = in.slice.x;
400 let _roughness = in.slice.y;
401 let _opacity = in.slice.z;
402 let _ld = normalize(vec3<f32>(0.5, 0.8, 0.6));
403 let _lc = vec3<f32>(1.0, 0.95, 0.9);
404 let _ndl = max(dot(_n, _ld), 0.0);
405 let _diffuse = _ndl * _lc;
406 let _vd = vec3<f32>(0.0, 0.0, 1.0);
407 let _hd = normalize(_ld + _vd);
408 let _ndh = max(dot(_n, _hd), 0.0);
409 let _shiny = mix(8.0, 256.0, 1.0 - _roughness);
410 let _spec = pow(_ndh, _shiny) * _lc;
411 let _f0 = mix(vec3<f32>(0.04), col.rgb, _metallic);
412 let _fresnel = _f0 + (vec3<f32>(1.0) - _f0) * pow(1.0 - max(dot(_n, -_vd), 0.0), 5.0);
413 let _amb = vec3<f32>(0.06, 0.07, 0.1);
414 var _lit = col.rgb * (_amb + _diffuse);
415 _lit += _spec * mix(vec3<f32>(1.0), col.rgb, _metallic) * _fresnel;
416 let _depth = in.clip_position.z;
417 let _fog = clamp(1.0 - _depth * 0.0005, 0.7, 1.0);
418 _lit *= _fog;
419 vec4<f32>(_lit, col.a * _opacity)"#.trim().to_string()
420 }
421 MaterialOp::DropShadow => {
422 r#"
423 let _margin = in.uv.x;
424 let _blur = max(in.uv.y, 1.0);
425 let _original_size = in.size - 2.0 * _margin;
426 let _half_size = _original_size * 0.5;
427 let _p = in.logical - _margin - _half_size;
428 let _d = length(max(abs(_p) - (_half_size - in.radius), vec2(0.0))) + min(max(abs(_p).x - (_half_size - in.radius).x, abs(_p).y - (_half_size - in.radius).y), 0.0) - in.radius;
429 vec4<f32>(col.rgb, col.a * smoothstep(_blur, 0.0, _d))"#.trim().to_string()
430 }
431 MaterialOp::NineSlice => {
432 "col".to_string() }
434 MaterialOp::Heatmap => {
435 let val_var = Self::find_input(&var_names, node_id, MaterialSocket::Float, graph)
436 .unwrap_or_else(|| "textureSample(t_diffuse[0], s_diffuse, in.uv).r".to_string());
437 format!("vec4<f32>(heatmap_palette({}), col.a)", val_var)
438 }
439 MaterialOp::Raymarch { shape } => {
440 match shape {
441 RaymarchShape::Box => {
442 r#"
443 let _uv = (in.uv - 0.5) * 2.0;
444 let _ro = vec3<f32>(0.0, 0.0, -2.5);
445 let _rd = normalize(vec3<f32>(_uv.x, _uv.y, 1.5));
446 let _m = rotX(in.slice.x) * rotY(in.slice.y) * rotZ(in.slice.z);
447 var _t = 0.0;
448 var _hit = false;
449 var _d = 0.0;
450 for (var _i = 0; _i < 40; _i++) {
451 let _p = _m * (_ro + _rd * _t);
452 _d = sd_box_3d(_p, vec3(0.5, 0.5, 0.5));
453 if _d < 0.001 {
454 _hit = true;
455 break;
456 }
457 _t += _d;
458 if _t > 5.0 { break; }
459 }
460 if _hit {
461 let _p2 = _m * (_ro + _rd * _t);
462 let _eps = vec2(0.001, 0.0);
463 let _n = normalize(vec3(
464 sd_box_3d(_p2 + _eps.xyy, vec3(0.5)) - sd_box_3d(_p2 - _eps.xyy, vec3(0.5)),
465 sd_box_3d(_p2 + _eps.yxy, vec3(0.5)) - sd_box_3d(_p2 - _eps.yxy, vec3(0.5)),
466 sd_box_3d(_p2 + _eps.yyx, vec3(0.5)) - sd_box_3d(_p2 - _eps.yyx, vec3(0.5))
467 ));
468 let _ld2 = normalize(vec3(1.0, 1.0, -2.0));
469 let _diff2 = max(dot(_n, _ld2), 0.1);
470 let _rim = pow(1.0 - max(dot(_n, -_rd), 0.0), 3.0) * 0.5;
471 vec4<f32>(col.rgb * _diff2 + _rim, col.a)
472 } else {
473 discard;
474 }"#.trim().to_string()
475 }
476 RaymarchShape::Sphere => {
477 r#"
478 let _ro = vec3<f32>(in.uv * 2.0 - 1.0, -2.0);
479 let _rd = normalize(vec3<f32>(0.0, 0.0, 1.0));
480 var _t = 0.0;
481 var _hit = false;
482 for (var i = 0; i < 32; i++) {
483 let _p = _ro + _rd * _t;
484 let _d = length(_p) - 1.0;
485 if _d < 0.01 { _hit = true; break; }
486 _t += _d;
487 }
488 if _hit {
489 let _p = _ro + _rd * _t;
490 let _n = normalize(_p);
491 let _ld = normalize(vec3<f32>(1.0, 1.0, -1.0));
492 let _diff = max(dot(_n, _ld), 0.0);
493 vec4<f32>(col.rgb * _diff, col.a)
494 } else {
495 discard;
496 }"#.trim().to_string()
497 }
498 }
499 }
500 };
501
502 lines.push(format!(" var {} = {};", result_var, expr));
503 var_names.insert((node_id, MaterialSocket::Color), result_var);
504 }
505
506 let body = lines.join("\n");
507 let out_id = graph.output.ok_or(MaterialError::NoOutput)?;
508 let fn_name = format!("material_{}", out_id);
509
510 let wgsl_fn = format!(
511 "fn {}(in: VertexOutput, col: vec4<f32>) -> vec4<f32> {{\n{}\n return v_{};\n}}",
512 fn_name, body, out_id
513 );
514
515 Ok(CompiledMaterial { wgsl_fn, fn_name })
516 }
517
518 fn find_input(
519 names: &HashMap<(MatNodeId, MaterialSocket), String>,
520 node: MatNodeId,
521 socket: MaterialSocket,
522 graph: &MaterialGraph,
523 ) -> Option<String> {
524 for edge in &graph.edges {
525 if edge.to_node == node && edge.to_socket == socket {
526 return names.get(&(edge.from_node, edge.from_socket)).cloned();
527 }
528 }
529 None
530 }
531
532 fn find_input_map(
533 names: &HashMap<(MatNodeId, MaterialSocket), String>,
534 node: MatNodeId,
535 socket: MaterialSocket,
536 graph: &MaterialGraph,
537 offset: usize,
538 ) -> Option<String> {
539 let mut matches = graph.edges.iter().filter(|e| e.to_node == node && e.to_socket == socket);
540 let edge = matches.nth(offset)?;
541 names.get(&(edge.from_node, edge.from_socket)).cloned()
542 }
543
544 fn topo_sort(graph: &MaterialGraph) -> Result<Vec<MatNodeId>, MaterialError> {
545 let n = graph.nodes.len();
546 let mut in_degree = vec![0u32; n];
547 let mut adj: Vec<Vec<MatNodeId>> = vec![Vec::new(); n];
548
549 for edge in &graph.edges {
550 adj[edge.from_node as usize].push(edge.to_node);
551 in_degree[edge.to_node as usize] += 1;
552 }
553
554 let mut queue: std::collections::VecDeque<MatNodeId> = std::collections::VecDeque::new();
555 for (i, °) in in_degree.iter().enumerate() {
556 if deg == 0 {
557 queue.push_back(i as MatNodeId);
558 }
559 }
560
561 let mut order = Vec::with_capacity(n);
562 while let Some(node) = queue.pop_front() {
563 order.push(node);
564 for &next in &adj[node as usize] {
565 in_degree[next as usize] -= 1;
566 if in_degree[next as usize] == 0 {
567 queue.push_back(next);
568 }
569 }
570 }
571
572 if order.len() != n {
573 return Err(MaterialError::Cycle);
574 }
575
576 Ok(order)
577 }
578}
579
580pub mod builtins {
583 use super::*;
584
585 pub fn rounded_rect() -> MaterialGraph {
587 let mut g = MaterialGraph::new();
588 let input = g.add_node(MaterialOp::InputColor);
589 let sdf = g.add_node(MaterialOp::SDFRoundRect);
590 g.connect(input, MaterialSocket::Color, sdf, MaterialSocket::Color);
592 g.set_output(sdf);
593 g
594 }
595
596 pub fn glass() -> MaterialGraph {
598 let mut g = MaterialGraph::new();
599 let glass = g.add_node(MaterialOp::GlassBlur);
600 g.set_output(glass);
601 g
602 }
603
604 pub fn solid() -> MaterialGraph {
606 let mut g = MaterialGraph::new();
607 let input = g.add_node(MaterialOp::InputColor);
608 g.set_output(input);
609 g
610 }
611
612 pub fn pbr() -> MaterialGraph {
614 let mut g = MaterialGraph::new();
615 let input = g.add_node(MaterialOp::InputColor);
616 let pbr = g.add_node(MaterialOp::PBRLighting);
617 g.connect(input, MaterialSocket::Color, pbr, MaterialSocket::Color);
618 g.set_output(pbr);
619 g
620 }
621
622 pub fn text(tex_index: u32) -> MaterialGraph {
624 let mut g = MaterialGraph::new();
625 let input = g.add_node(MaterialOp::InputColor);
626 let tex = g.add_node(MaterialOp::SampleTexture { tex_index });
627 let blend = g.add_node(MaterialOp::PremultipliedBlend);
628 g.connect(input, MaterialSocket::Color, blend, MaterialSocket::Color);
629 g.connect(tex, MaterialSocket::Float, blend, MaterialSocket::Float);
630 g.set_output(blend);
631 g
632 }
633
634 pub fn textured(tex_index: u32) -> MaterialGraph {
636 let mut g = MaterialGraph::new();
637 let input = g.add_node(MaterialOp::InputColor);
638 let tex = g.add_node(MaterialOp::SampleTexture { tex_index });
639 let blend = g.add_node(MaterialOp::LayerBlend { mode: BlendMode::Multiply });
640 g.connect(input, MaterialSocket::Color, blend, MaterialSocket::Color);
641 g.connect(tex, MaterialSocket::Color, blend, MaterialSocket::Color);
642 g.set_output(blend);
643 g
644 }
645
646 pub fn neon_glow(radius: f32, intensity: f32) -> MaterialGraph {
648 let mut g = MaterialGraph::new();
649 let input = g.add_node(MaterialOp::InputColor);
650 let glow = g.add_node(MaterialOp::NeonGlow { radius, intensity });
651 g.connect(input, MaterialSocket::Color, glow, MaterialSocket::Color);
652 g.set_output(glow);
653 g
654 }
655
656 pub fn linear_gradient(start: [f32; 4], end: [f32; 4]) -> MaterialGraph {
658 let mut g = MaterialGraph::new();
659 let grad = g.add_node(MaterialOp::LinearGradient { start, end });
660 g.set_output(grad);
661 g
662 }
663
664 pub fn radial_gradient(start: [f32; 4], end: [f32; 4]) -> MaterialGraph {
666 let mut g = MaterialGraph::new();
667 let grad = g.add_node(MaterialOp::RadialGradient { start, end });
668 g.set_output(grad);
669 g
670 }
671
672 pub fn ellipse() -> MaterialGraph {
674 let mut g = MaterialGraph::new();
675 let input = g.add_node(MaterialOp::InputColor);
676 let sdf = g.add_node(MaterialOp::SDFEllipse);
677 g.connect(input, MaterialSocket::Color, sdf, MaterialSocket::Color);
678 g.set_output(sdf);
679 g
680 }
681
682 pub fn neon_line() -> MaterialGraph {
684 let mut g = MaterialGraph::new();
685 let color = g.add_node(MaterialOp::ConstantColor { r: 1.5, g: 1.5, b: 1.5, a: 1.0 });
686 g.set_output(color);
687 g
688 }
689
690 pub fn heatmap(tex_index: u32) -> MaterialGraph {
692 let mut g = MaterialGraph::new();
693 let tex = g.add_node(MaterialOp::SampleTexture { tex_index });
694 let hm = g.add_node(MaterialOp::Heatmap);
695 g.connect(tex, MaterialSocket::Float, hm, MaterialSocket::Float);
696 g.set_output(hm);
697 g
698 }
699
700 pub fn nine_slice(tex_index: u32) -> MaterialGraph {
702 let mut g = MaterialGraph::new();
703 let input = g.add_node(MaterialOp::InputColor);
704 let tex = g.add_node(MaterialOp::SampleTexture { tex_index });
705 let blend = g.add_node(MaterialOp::LayerBlend { mode: BlendMode::Multiply });
706 g.connect(input, MaterialSocket::Color, blend, MaterialSocket::Color);
707 g.connect(tex, MaterialSocket::Color, blend, MaterialSocket::Color);
708 g.set_output(blend);
709 g
710 }
711
712 pub fn raymarch_cube() -> MaterialGraph {
714 let mut g = MaterialGraph::new();
715 let input = g.add_node(MaterialOp::InputColor);
716 let rm = g.add_node(MaterialOp::Raymarch { shape: RaymarchShape::Box });
717 g.connect(input, MaterialSocket::Color, rm, MaterialSocket::Color);
718 g.set_output(rm);
719 g
720 }
721
722 pub fn stroke() -> MaterialGraph {
724 let mut g = MaterialGraph::new();
725 let input = g.add_node(MaterialOp::InputColor);
726 let sdf = g.add_node(MaterialOp::SDFRoundRect);
727 g.connect(input, MaterialSocket::Color, sdf, MaterialSocket::Color);
728 g.set_output(sdf);
729 g
730 }
731
732 pub fn drop_shadow() -> MaterialGraph {
734 let mut g = MaterialGraph::new();
735 let input = g.add_node(MaterialOp::InputColor);
736 let shadow = g.add_node(MaterialOp::DropShadow);
737 g.connect(input, MaterialSocket::Color, shadow, MaterialSocket::Color);
738 g.set_output(shadow);
739 g
740 }
741
742 pub fn dashed_stroke() -> MaterialGraph {
744 let mut g = MaterialGraph::new();
745 let input = g.add_node(MaterialOp::InputColor);
746 let sdf = g.add_node(MaterialOp::SDFRoundRect);
747 g.connect(input, MaterialSocket::Color, sdf, MaterialSocket::Color);
748 g.set_output(sdf);
749 g
750 }
751}
752
753#[cfg(test)]
754mod tests {
755 use super::*;
756
757 #[test]
758 fn test_solid_material_compiles() {
759 let graph = builtins::solid();
760 let compiled = MaterialCompiler::compile(&graph).unwrap();
761 assert!(compiled.wgsl_fn.contains("fn material_"));
762 assert!(compiled.wgsl_fn.contains("col"));
763 }
764
765 #[test]
766 fn test_rounded_rect_compiles() {
767 let graph = builtins::rounded_rect();
768 let compiled = MaterialCompiler::compile(&graph).unwrap();
769 assert!(compiled.wgsl_fn.contains("sd_round_rect"));
770 }
771
772 #[test]
773 fn test_pbr_compiles() {
774 let graph = builtins::pbr();
775 let compiled = MaterialCompiler::compile(&graph).unwrap();
776 assert!(compiled.wgsl_fn.contains("PBRLighting") || compiled.wgsl_fn.contains("_n"));
777 }
778
779 #[test]
780 fn test_graph_validation_no_output() {
781 let mut g = MaterialGraph::new();
782 g.add_node(MaterialOp::InputColor);
783 assert!(g.validate().is_err());
784 }
785
786 #[test]
787 fn test_graph_validation_cycle() {
788 let mut g = MaterialGraph::new();
789 let a = g.add_node(MaterialOp::InputColor);
790 let b = g.add_node(MaterialOp::NeonGlow { radius: 1.0, intensity: 1.0 });
791 g.connect(a, MaterialSocket::Color, b, MaterialSocket::Color);
792 g.connect(b, MaterialSocket::Color, a, MaterialSocket::Color); g.set_output(b);
794 assert!(g.validate().is_err());
795 }
796
797 #[test]
798 fn test_all_builtins_compile() {
799 let graphs: Vec<MaterialGraph> = vec![
800 builtins::solid(),
801 builtins::rounded_rect(),
802 builtins::glass(),
803 builtins::pbr(),
804 builtins::text(0),
805 builtins::textured(0),
806 builtins::neon_glow(4.0, 1.5),
807 builtins::linear_gradient([1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0]),
808 builtins::radial_gradient([1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 1.0]),
809 builtins::ellipse(),
810 builtins::neon_line(),
811 builtins::heatmap(0),
812 builtins::nine_slice(0),
813 builtins::raymarch_cube(),
814 builtins::stroke(),
815 builtins::drop_shadow(),
816 builtins::dashed_stroke(),
817 ];
818
819 for (i, graph) in graphs.iter().enumerate() {
820 match MaterialCompiler::compile(graph) {
821 Ok(compiled) => {
822 assert!(!compiled.wgsl_fn.is_empty(), "graph {} produced empty WGSL", i);
823 assert!(!compiled.fn_name.is_empty(), "graph {} produced empty fn name", i);
824 }
825 Err(e) => {
826 panic!("graph {} failed to compile: {}", i, e);
827 }
828 }
829 }
830 }
831}