1use glam::{Vec2, Vec3, Mat3, Quat};
29#[allow(unused_imports)]
30use glam::FloatExt as _;
31use std::collections::HashMap;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
39pub struct NodeId(pub u32);
40
41impl NodeId {
42 pub const INVALID: NodeId = NodeId(u32::MAX);
43}
44
45impl Default for NodeId {
46 fn default() -> Self { NodeId::INVALID }
47}
48
49impl std::fmt::Display for NodeId {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 if *self == NodeId::INVALID { write!(f, "INVALID") } else { write!(f, "N{}", self.0) }
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub struct PortId(pub u8);
62
63impl PortId {
64 pub const A: PortId = PortId(0);
65 pub const B: PortId = PortId(1);
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub struct Connection {
75 pub from: NodeId,
76 pub to: NodeId,
77 pub port: PortId,
78}
79
80#[derive(Debug, Clone, PartialEq)]
86pub enum PrimitiveKind {
87 Sphere { radius: f32 },
88 Capsule { half_height: f32, radius: f32 },
89 Box { half_extents: Vec3 },
90 Ellipsoid{ radii: Vec3 },
91 Torus { major: f32, minor: f32 },
92 Cylinder { half_height: f32, radius: f32 },
93 Cone { half_height: f32, half_angle_rad: f32 },
94 Plane { normal: Vec3, offset: f32 },
95 HexPrism { height: f32, radius: f32 },
96 RoundBox { half_extents: Vec3, rounding: f32 },
97 Link { length: f32, r1: f32, r2: f32 },
98 OctaHedron { size: f32 },
99}
100
101impl PrimitiveKind {
102 pub fn label(&self) -> &'static str {
103 match self {
104 PrimitiveKind::Sphere {..} => "Sphere",
105 PrimitiveKind::Capsule {..} => "Capsule",
106 PrimitiveKind::Box {..} => "Box",
107 PrimitiveKind::Ellipsoid {..} => "Ellipsoid",
108 PrimitiveKind::Torus {..} => "Torus",
109 PrimitiveKind::Cylinder {..} => "Cylinder",
110 PrimitiveKind::Cone {..} => "Cone",
111 PrimitiveKind::Plane {..} => "Plane",
112 PrimitiveKind::HexPrism {..} => "HexPrism",
113 PrimitiveKind::RoundBox {..} => "RoundBox",
114 PrimitiveKind::Link {..} => "Link",
115 PrimitiveKind::OctaHedron {..} => "Octahedron",
116 }
117 }
118
119 pub fn evaluate(&self, p: Vec3) -> f32 {
121 match self {
122 PrimitiveKind::Sphere { radius } => p.length() - radius,
123
124 PrimitiveKind::Capsule { half_height, radius } => {
125 let h = *half_height;
126 let py = p.y.clamp(-h, h);
127 (p - Vec3::new(0.0, py, 0.0)).length() - radius
128 }
129
130 PrimitiveKind::Box { half_extents } => {
131 let q = p.abs() - *half_extents;
132 q.max(Vec3::ZERO).length() + q.x.max(q.y).max(q.z).min(0.0)
133 }
134
135 PrimitiveKind::Ellipsoid { radii } => {
136 let k0 = (p / *radii).length();
137 let k1 = (p / (*radii * *radii)).length();
138 k0 * (k0 - 1.0) / k1
139 }
140
141 PrimitiveKind::Torus { major, minor } => {
142 let q = Vec2::new(Vec2::new(p.x, p.z).length() - major, p.y);
143 q.length() - minor
144 }
145
146 PrimitiveKind::Cylinder { half_height, radius } => {
147 let d = Vec2::new(Vec2::new(p.x, p.z).length(), p.y.abs()) - Vec2::new(*radius, *half_height);
148 d.x.max(d.y).min(0.0) + d.max(Vec2::ZERO).length()
149 }
150
151 PrimitiveKind::Cone { half_height, half_angle_rad } => {
152 let h = *half_height;
153 let q = Vec2::new(Vec2::new(p.x, p.z).length(), p.y);
154 let tip_to_p = q - Vec2::new(0.0, h);
155 let c = Vec2::new(half_angle_rad.sin(), half_angle_rad.cos());
156 let w = tip_to_p - c * (tip_to_p.dot(c).clamp(-2.0 * h, 0.0));
157 w.length() * (if w.x < 0.0 { -1.0 } else { 1.0 })
158 }
159
160 PrimitiveKind::Plane { normal, offset } => {
161 p.dot(*normal) + offset
162 }
163
164 PrimitiveKind::HexPrism { height, radius } => {
165 let k = Vec3::new(-0.866_025, 0.5, 0.577_350);
166 let mut q = p.abs();
167 let dot = (2.0 * k.x * q.x + 2.0 * k.y * q.y).min(0.0);
168 let qx = q.x - dot * 2.0 * k.x;
169 let qy = q.y - dot * 2.0 * k.y;
170 let d = Vec2::new(
171 (Vec2::new(qx, qy) - Vec2::new(qx.clamp(-k.z * radius, k.z * radius), *radius)).length() * if qy < *radius { -1.0 } else { 1.0 },
172 q.z - height,
173 );
174 d.x.max(d.y).min(0.0) + d.max(Vec2::ZERO).length()
175 }
176
177 PrimitiveKind::RoundBox { half_extents, rounding } => {
178 let q = p.abs() - *half_extents;
179 q.max(Vec3::ZERO).length() + q.x.max(q.y).max(q.z).min(0.0) - rounding
180 }
181
182 PrimitiveKind::Link { length, r1, r2 } => {
183 let q = Vec3::new(p.x, (p.y.abs() - length).max(0.0), p.z);
184 Vec2::new(Vec2::new(q.x, q.y).length() - r1, q.z).length() - r2
185 }
186
187 PrimitiveKind::OctaHedron { size } => {
188 let p = p.abs();
189 let m = p.x + p.y + p.z - size;
190 let q = if 3.0 * p.x < m {
191 p
192 } else if 3.0 * p.y < m {
193 Vec3::new(p.y, p.x, p.z)
194 } else if 3.0 * p.z < m {
195 Vec3::new(p.z, p.x, p.y)
196 } else {
197 return m * 0.577_350;
198 };
199 let k = (m / 2.0).clamp(0.0, size * 0.5);
200 Vec3::new(q.x - k, q.y - k, q.z + k - size).length()
201 }
202 }
203 }
204
205 pub fn emit_glsl(&self, p: &str) -> String {
208 match self {
209 PrimitiveKind::Sphere { radius } =>
210 format!("(length({p}) - {radius:.6})"),
211
212 PrimitiveKind::Capsule { half_height, radius } =>
213 format!("(length({p} - vec3(0,clamp({p}.y,{h:.6},{hh:.6}),0)) - {r:.6})",
214 p = p, h = -half_height, hh = *half_height, r = radius),
215
216 PrimitiveKind::Box { half_extents } =>
217 format!("({{ vec3 _q = abs({p}) - vec3({:.6},{:.6},{:.6}); \
218 length(max(_q,0.0)) + min(max(_q.x,max(_q.y,_q.z)),0.0); }})",
219 half_extents.x, half_extents.y, half_extents.z),
220
221 PrimitiveKind::Ellipsoid { radii } =>
222 format!("({{ vec3 _r = vec3({:.6},{:.6},{:.6}); \
223 float _k0 = length({p}/_r); \
224 float _k1 = length({p}/(_r*_r)); \
225 _k0*(_k0-1.0)/_k1; }})",
226 radii.x, radii.y, radii.z),
227
228 PrimitiveKind::Torus { major, minor } =>
229 format!("({{ vec2 _q = vec2(length({p}.xz)-{:.6},{p}.y); \
230 length(_q)-{:.6}; }})",
231 major, minor),
232
233 PrimitiveKind::Cylinder { half_height, radius } =>
234 format!("({{ vec2 _d = abs(vec2(length({p}.xz),{p}.y)) - vec2({:.6},{:.6}); \
235 min(max(_d.x,_d.y),0.0) + length(max(_d,0.0)); }})",
236 radius, half_height),
237
238 PrimitiveKind::Plane { normal, offset } =>
239 format!("(dot({p}, vec3({:.6},{:.6},{:.6})) + {:.6})",
240 normal.x, normal.y, normal.z, offset),
241
242 _ => format!("(length({p}) - 0.5)"), }
244 }
245}
246
247#[derive(Debug, Clone, PartialEq)]
253pub enum CombinatorKind {
254 Union,
256 Subtract,
257 Intersect,
258 SmoothUnion { k: f32 },
260 SmoothSubtract { k: f32 },
261 SmoothIntersect { k: f32 },
262 Blend { alpha: f32 },
264 Twist { strength: f32 },
266 Bend { strength: f32 },
267 Elongate { amount: Vec3 },
268 Onion { thickness: f32 },
269 Extrude { depth: f32 },
270 Revolve { offset: f32 },
271 Displace { amplitude: f32, frequency: f32 },
272 Mirror { axis_mask: [bool; 3] },
273 Scale { factor: f32 },
274 Round { radius: f32 },
275}
276
277impl CombinatorKind {
278 pub fn label(&self) -> &'static str {
279 match self {
280 CombinatorKind::Union => "Union",
281 CombinatorKind::Subtract => "Subtract",
282 CombinatorKind::Intersect => "Intersect",
283 CombinatorKind::SmoothUnion {..} => "SmoothUnion",
284 CombinatorKind::SmoothSubtract {..} => "SmoothSubtract",
285 CombinatorKind::SmoothIntersect{..} => "SmoothIntersect",
286 CombinatorKind::Blend {..} => "Blend",
287 CombinatorKind::Twist {..} => "Twist",
288 CombinatorKind::Bend {..} => "Bend",
289 CombinatorKind::Elongate {..} => "Elongate",
290 CombinatorKind::Onion {..} => "Onion",
291 CombinatorKind::Extrude {..} => "Extrude",
292 CombinatorKind::Revolve {..} => "Revolve",
293 CombinatorKind::Displace {..} => "Displace",
294 CombinatorKind::Mirror {..} => "Mirror",
295 CombinatorKind::Scale {..} => "Scale",
296 CombinatorKind::Round {..} => "Round",
297 }
298 }
299
300 pub fn combine(&self, a: f32, b: f32, p: Vec3) -> f32 {
302 match self {
303 CombinatorKind::Union => a.min(b),
304 CombinatorKind::Subtract => a.max(-b),
305 CombinatorKind::Intersect => a.max(b),
306
307 CombinatorKind::SmoothUnion { k } => {
308 let h = (0.5 + 0.5 * (b - a) / k).clamp(0.0, 1.0);
309 a.lerp(b, h) - k * h * (1.0 - h)
310 }
311 CombinatorKind::SmoothSubtract { k } => {
312 let h = (0.5 - 0.5 * (b + a) / k).clamp(0.0, 1.0);
313 a.lerp(-b, h) + k * h * (1.0 - h)
314 }
315 CombinatorKind::SmoothIntersect { k } => {
316 let h = (0.5 - 0.5 * (b - a) / k).clamp(0.0, 1.0);
317 a.lerp(b, h) + k * h * (1.0 - h)
318 }
319 CombinatorKind::Blend { alpha } => a * (1.0 - alpha) + b * alpha,
320
321 CombinatorKind::Twist { strength } => {
322 let c = (strength * p.y).cos();
324 let s = (strength * p.y).sin();
325 let twisted_x = c * p.x - s * p.z;
326 let twisted_z = s * p.x + c * p.z;
327 let _twisted_p = Vec3::new(twisted_x, p.y, twisted_z);
328 a }
330 CombinatorKind::Round { radius } => a - radius,
331 CombinatorKind::Onion { thickness } => a.abs() - thickness,
332 CombinatorKind::Scale { factor } => a * factor,
333 _ => a.min(b),
334 }
335 }
336
337 pub fn is_binary(&self) -> bool {
339 !matches!(self,
340 CombinatorKind::Twist {..} |
341 CombinatorKind::Bend {..} |
342 CombinatorKind::Elongate {..} |
343 CombinatorKind::Onion {..} |
344 CombinatorKind::Extrude {..} |
345 CombinatorKind::Revolve {..} |
346 CombinatorKind::Displace {..} |
347 CombinatorKind::Mirror {..} |
348 CombinatorKind::Scale {..} |
349 CombinatorKind::Round {..}
350 )
351 }
352}
353
354#[derive(Debug, Clone, PartialEq)]
360pub struct NodeTransform {
361 pub translation: Vec3,
362 pub rotation: Quat,
363 pub scale: Vec3,
364}
365
366impl Default for NodeTransform {
367 fn default() -> Self {
368 Self {
369 translation: Vec3::ZERO,
370 rotation: Quat::IDENTITY,
371 scale: Vec3::ONE,
372 }
373 }
374}
375
376impl NodeTransform {
377 pub fn world_to_local(&self, world_p: Vec3) -> Vec3 {
379 let p = world_p - self.translation;
380 let p = self.rotation.inverse().mul_vec3(p);
381 p / self.scale
382 }
383
384 pub fn display(&self) -> String {
386 let (ax, ay, az) = self.rotation.to_euler(glam::EulerRot::XYZ);
387 format!(
388 "T ({:.3},{:.3},{:.3}) R ({:.1}°,{:.1}°,{:.1}°) S ({:.3},{:.3},{:.3})",
389 self.translation.x, self.translation.y, self.translation.z,
390 ax.to_degrees(), ay.to_degrees(), az.to_degrees(),
391 self.scale.x, self.scale.y, self.scale.z
392 )
393 }
394}
395
396#[derive(Debug, Clone)]
402pub struct SdfNode {
403 pub id: NodeId,
404 pub label: String,
405 pub canvas_pos: Vec2,
406 pub transform: NodeTransform,
407 pub visible: bool,
408 pub locked: bool,
409 pub material_id: Option<u32>,
410 pub payload: NodePayload,
411}
412
413#[derive(Debug, Clone)]
415pub enum NodePayload {
416 Primitive(PrimitiveKind),
417 Combinator(CombinatorKind),
418 Constant(f32),
420 Reference { graph_name: String },
422}
423
424impl SdfNode {
425 pub fn new_primitive(id: NodeId, kind: PrimitiveKind) -> Self {
426 let label = kind.label().to_string();
427 Self {
428 id,
429 label,
430 canvas_pos: Vec2::ZERO,
431 transform: NodeTransform::default(),
432 visible: true,
433 locked: false,
434 material_id: None,
435 payload: NodePayload::Primitive(kind),
436 }
437 }
438
439 pub fn new_combinator(id: NodeId, kind: CombinatorKind) -> Self {
440 let label = kind.label().to_string();
441 Self {
442 id,
443 label,
444 canvas_pos: Vec2::ZERO,
445 transform: NodeTransform::default(),
446 visible: true,
447 locked: false,
448 material_id: None,
449 payload: NodePayload::Combinator(kind),
450 }
451 }
452
453 pub fn accepts_input(&self, port: PortId) -> bool {
455 match &self.payload {
456 NodePayload::Primitive(_) => false,
457 NodePayload::Constant(_) => false,
458 NodePayload::Reference {..} => false,
459 NodePayload::Combinator(c) => {
460 if port == PortId::A { true }
461 else { c.is_binary() }
462 }
463 }
464 }
465
466 pub fn header_text(&self) -> String {
468 match &self.payload {
469 NodePayload::Primitive(p) => format!("[{}] {}", p.label(), self.label),
470 NodePayload::Combinator(c) => format!("[{}] {}", c.label(), self.label),
471 NodePayload::Constant(v) => format!("[Const] {:.3}", v),
472 NodePayload::Reference { graph_name } => format!("[Ref] {}", graph_name),
473 }
474 }
475
476 pub fn output_port_pos(&self) -> Vec2 {
478 self.canvas_pos + Vec2::new(160.0, 30.0)
479 }
480
481 pub fn input_port_pos_a(&self) -> Vec2 {
482 self.canvas_pos + Vec2::new(0.0, 20.0)
483 }
484
485 pub fn input_port_pos_b(&self) -> Vec2 {
486 self.canvas_pos + Vec2::new(0.0, 40.0)
487 }
488}
489
490type EvalCache = HashMap<NodeId, f32>;
496
497#[derive(Debug, Clone)]
503pub struct NodeGraph {
504 nodes: HashMap<NodeId, SdfNode>,
505 connections: Vec<Connection>,
506 next_id: u32,
507 pub root: NodeId,
509 pub dirty: bool,
511 pub name: String,
513}
514
515impl NodeGraph {
516 pub fn new(name: impl Into<String>) -> Self {
517 Self {
518 nodes: HashMap::new(),
519 connections: Vec::new(),
520 next_id: 1,
521 root: NodeId::INVALID,
522 dirty: false,
523 name: name.into(),
524 }
525 }
526
527 fn alloc_id(&mut self) -> NodeId {
530 let id = NodeId(self.next_id);
531 self.next_id += 1;
532 id
533 }
534
535 pub fn add_primitive(&mut self, kind: PrimitiveKind) -> NodeId {
537 let id = self.alloc_id();
538 let node = SdfNode::new_primitive(id, kind);
539 self.nodes.insert(id, node);
540 self.dirty = true;
541 id
542 }
543
544 pub fn add_combinator(&mut self, kind: CombinatorKind) -> NodeId {
546 let id = self.alloc_id();
547 let node = SdfNode::new_combinator(id, kind);
548 self.nodes.insert(id, node);
549 self.dirty = true;
550 id
551 }
552
553 pub fn remove_node(&mut self, id: NodeId) -> Option<SdfNode> {
555 let node = self.nodes.remove(&id)?;
556 self.connections.retain(|c| c.from != id && c.to != id);
557 if self.root == id { self.root = NodeId::INVALID; }
558 self.dirty = true;
559 Some(node)
560 }
561
562 pub fn get(&self, id: NodeId) -> Option<&SdfNode> {
564 self.nodes.get(&id)
565 }
566
567 pub fn get_mut(&mut self, id: NodeId) -> Option<&mut SdfNode> {
569 self.dirty = true;
570 self.nodes.get_mut(&id)
571 }
572
573 pub fn nodes(&self) -> impl Iterator<Item = &SdfNode> {
575 self.nodes.values()
576 }
577
578 pub fn node_count(&self) -> usize { self.nodes.len() }
579
580 pub fn connect(&mut self, from: NodeId, to: NodeId, port: PortId) -> Result<(), String> {
585 let to_node = self.nodes.get(&to)
587 .ok_or_else(|| format!("node {} not found", to))?;
588 if !to_node.accepts_input(port) {
589 return Err(format!("node {} does not accept input on port {:?}", to, port));
590 }
591 if !self.nodes.contains_key(&from) {
592 return Err(format!("source node {} not found", from));
593 }
594 if self.would_create_cycle(from, to) {
595 return Err(format!("connecting {} → {} would create a cycle", from, to));
596 }
597 self.connections.retain(|c| !(c.to == to && c.port == port));
599 self.connections.push(Connection { from, to, port });
600 self.dirty = true;
601 Ok(())
602 }
603
604 pub fn disconnect(&mut self, to: NodeId, port: PortId) {
606 self.connections.retain(|c| !(c.to == to && c.port == port));
607 self.dirty = true;
608 }
609
610 pub fn connections(&self) -> &[Connection] { &self.connections }
611
612 pub fn input_of(&self, to: NodeId, port: PortId) -> Option<NodeId> {
614 self.connections.iter()
615 .find(|c| c.to == to && c.port == port)
616 .map(|c| c.from)
617 }
618
619 fn would_create_cycle(&self, from: NodeId, to: NodeId) -> bool {
622 let mut visited = Vec::new();
625 self.dfs_upstream(to, &mut visited);
626 visited.contains(&from)
627 }
628
629 fn dfs_upstream(&self, node: NodeId, visited: &mut Vec<NodeId>) {
630 if visited.contains(&node) { return; }
631 visited.push(node);
632 for c in &self.connections {
633 if c.from == node {
634 self.dfs_upstream(c.to, visited);
635 }
636 }
637 }
638
639 pub fn topo_sort(&self) -> Vec<NodeId> {
643 let mut result = Vec::new();
644 let mut visited = std::collections::HashSet::new();
645 for &id in self.nodes.keys() {
646 self.topo_visit(id, &mut visited, &mut result);
647 }
648 result
649 }
650
651 fn topo_visit(&self, id: NodeId, visited: &mut std::collections::HashSet<NodeId>, result: &mut Vec<NodeId>) {
652 if visited.contains(&id) { return; }
653 visited.insert(id);
654 for c in &self.connections {
656 if c.to == id {
657 self.topo_visit(c.from, visited, result);
658 }
659 }
660 result.push(id);
661 }
662
663 pub fn evaluate_at(&self, node: NodeId, p: Vec3, cache: &mut EvalCache) -> f32 {
667 if let Some(&cached) = cache.get(&node) { return cached; }
668 let Some(n) = self.nodes.get(&node) else { return f32::MAX; };
669 if !n.visible { return f32::MAX; }
670
671 let local_p = n.transform.world_to_local(p);
672 let value = match &n.payload {
673 NodePayload::Constant(v) => *v,
674 NodePayload::Primitive(prim) => prim.evaluate(local_p),
675 NodePayload::Reference { .. } => f32::MAX,
676
677 NodePayload::Combinator(comb) => {
678 let a_id = self.input_of(node, PortId::A);
679 let b_id = self.input_of(node, PortId::B);
680 let a = a_id.map(|id| self.evaluate_at(id, p, cache)).unwrap_or(f32::MAX);
681 let b = b_id.map(|id| self.evaluate_at(id, p, cache)).unwrap_or(f32::MAX);
682 comb.combine(a, b, local_p)
683 }
684 };
685
686 cache.insert(node, value);
687 value
688 }
689
690 pub fn sample(&self, p: Vec3) -> f32 {
692 if self.root == NodeId::INVALID { return f32::MAX; }
693 let mut cache = EvalCache::new();
694 self.evaluate_at(self.root, p, &mut cache)
695 }
696
697 pub fn gradient(&self, p: Vec3) -> Vec3 {
699 const EPS: f32 = 0.001;
700 let dx = self.sample(p + Vec3::X * EPS) - self.sample(p - Vec3::X * EPS);
701 let dy = self.sample(p + Vec3::Y * EPS) - self.sample(p - Vec3::Y * EPS);
702 let dz = self.sample(p + Vec3::Z * EPS) - self.sample(p - Vec3::Z * EPS);
703 Vec3::new(dx, dy, dz).normalize_or_zero()
704 }
705
706 pub fn compile_glsl(&self) -> String {
710 if self.root == NodeId::INVALID {
711 return "float sdf_body(vec3 p) { return 1e10; }".to_string();
712 }
713 let order = self.topo_sort();
714 let mut lines = Vec::new();
715 lines.push("float sdf_body(vec3 p) {".to_string());
716
717 for &nid in &order {
718 let Some(node) = self.nodes.get(&nid) else { continue; };
719 if !node.visible { continue; }
720
721 let var = format!("_d{}", nid.0);
722 let t = &node.transform;
723 let tp = if t.translation != Vec3::ZERO || t.rotation != Quat::IDENTITY || t.scale != Vec3::ONE {
724 let tname = format!("_p{}", nid.0);
725 let tx = t.translation.x; let ty = t.translation.y; let tz = t.translation.z;
726 let sx = t.scale.x; let sy = t.scale.y; let sz = t.scale.z;
727 lines.push(format!(" vec3 {tname} = (p - vec3({tx:.6},{ty:.6},{tz:.6})) / vec3({sx:.6},{sy:.6},{sz:.6});"));
728 tname
729 } else {
730 "p".to_string()
731 };
732
733 let expr = match &node.payload {
734 NodePayload::Constant(v) => format!("{v:.6}"),
735 NodePayload::Primitive(prim) => prim.emit_glsl(&tp),
736 NodePayload::Reference { .. } => "1e10".to_string(),
737 NodePayload::Combinator(c) => {
738 let a_var = self.input_of(nid, PortId::A)
739 .map(|id| format!("_d{}", id.0))
740 .unwrap_or_else(|| "1e10".to_string());
741 let b_var = self.input_of(nid, PortId::B)
742 .map(|id| format!("_d{}", id.0))
743 .unwrap_or_else(|| "1e10".to_string());
744 emit_combinator_glsl(c, &a_var, &b_var, &tp)
745 }
746 };
747 lines.push(format!(" float {var} = {expr};"));
748 }
749
750 let root_var = format!("_d{}", self.root.0);
751 lines.push(format!(" return {root_var};"));
752 lines.push("}".to_string());
753 lines.join("\n")
754 }
755
756 pub fn auto_layout(&mut self) {
760 let order = self.topo_sort();
761 let total = order.len();
762 for (i, &id) in order.iter().enumerate() {
763 if let Some(node) = self.nodes.get_mut(&id) {
764 let col = i as f32;
765 let row = (i % 3) as f32;
766 node.canvas_pos = Vec2::new(col * 220.0, row * 120.0 - total as f32 * 20.0);
767 }
768 }
769 }
770
771 pub fn to_text(&self) -> String {
775 let mut out = format!("# NodeGraph: {}\n", self.name);
776 out.push_str(&format!("root: {}\n", self.root));
777 out.push_str("nodes:\n");
778 let mut sorted: Vec<_> = self.nodes.keys().cloned().collect();
779 sorted.sort();
780 for id in sorted {
781 let n = &self.nodes[&id];
782 out.push_str(&format!(" {} {} [{}]\n", id, n.label, n.header_text()));
783 }
784 out.push_str("connections:\n");
785 for c in &self.connections {
786 out.push_str(&format!(" {} -> {}:{}\n", c.from, c.to, c.port.0));
787 }
788 out
789 }
790
791 pub fn stats(&self) -> NodeGraphStats {
794 let prim_count = self.nodes.values()
795 .filter(|n| matches!(n.payload, NodePayload::Primitive(_)))
796 .count();
797 let comb_count = self.nodes.values()
798 .filter(|n| matches!(n.payload, NodePayload::Combinator(_)))
799 .count();
800 let smooth_ops = self.nodes.values()
801 .filter(|n| matches!(&n.payload, NodePayload::Combinator(c) if
802 matches!(c, CombinatorKind::SmoothUnion{..} | CombinatorKind::SmoothSubtract{..} | CombinatorKind::SmoothIntersect{..})
803 ))
804 .count();
805 NodeGraphStats {
806 total_nodes: self.nodes.len(),
807 prim_count,
808 comb_count,
809 smooth_ops,
810 connections: self.connections.len(),
811 }
812 }
813}
814
815#[derive(Debug, Clone, Default)]
817pub struct NodeGraphStats {
818 pub total_nodes: usize,
819 pub prim_count: usize,
820 pub comb_count: usize,
821 pub smooth_ops: usize,
822 pub connections: usize,
823}
824
825impl std::fmt::Display for NodeGraphStats {
826 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
827 write!(f, "{} nodes ({} prim, {} comb, {} smooth, {} edges)",
828 self.total_nodes, self.prim_count, self.comb_count,
829 self.smooth_ops, self.connections)
830 }
831}
832
833fn emit_combinator_glsl(c: &CombinatorKind, a: &str, b: &str, _p: &str) -> String {
838 match c {
839 CombinatorKind::Union => format!("min({a},{b})"),
840 CombinatorKind::Subtract => format!("max({a},-{b})"),
841 CombinatorKind::Intersect => format!("max({a},{b})"),
842 CombinatorKind::SmoothUnion { k } => format!(
843 "({{ float _h = clamp(0.5+0.5*({b}-{a})/{k:.6},0.0,1.0); \
844 mix({a},{b},_h) - {k:.6}*_h*(1.0-_h); }})", k=k),
845 CombinatorKind::SmoothSubtract { k } => format!(
846 "({{ float _h = clamp(0.5-0.5*({b}+{a})/{k:.6},0.0,1.0); \
847 mix({a},-{b},_h) + {k:.6}*_h*(1.0-_h); }})", k=k),
848 CombinatorKind::SmoothIntersect { k } => format!(
849 "({{ float _h = clamp(0.5-0.5*({b}-{a})/{k:.6},0.0,1.0); \
850 mix({a},{b},_h) + {k:.6}*_h*(1.0-_h); }})", k=k),
851 CombinatorKind::Blend { alpha } => format!("mix({a},{b},{alpha:.6})"),
852 CombinatorKind::Round { radius } => format!("({a} - {radius:.6})"),
853 CombinatorKind::Onion { thickness } => format!("(abs({a}) - {thickness:.6})"),
854 CombinatorKind::Scale { factor } => format!("({a} * {factor:.6})"),
855 _ => format!("min({a},{b})"),
856 }
857}
858
859#[derive(Debug, Clone, Default)]
865pub struct NodeSelection {
866 pub selected: Vec<NodeId>,
867 pub hovered: Option<NodeId>,
868 pub drag: Option<(NodeId, Vec2)>,
870 pub wiring: Option<(NodeId, Vec2)>,
872}
873
874impl NodeSelection {
875 pub fn is_selected(&self, id: NodeId) -> bool { self.selected.contains(&id) }
876 pub fn select_only(&mut self, id: NodeId) { self.selected.clear(); self.selected.push(id); }
877 pub fn toggle_select(&mut self, id: NodeId) {
878 if let Some(pos) = self.selected.iter().position(|&x| x == id) {
879 self.selected.remove(pos);
880 } else {
881 self.selected.push(id);
882 }
883 }
884 pub fn clear(&mut self) { self.selected.clear(); }
885}
886
887#[derive(Debug, Clone)]
889pub struct NodeClipboard {
890 pub nodes: Vec<SdfNode>,
891 pub connections: Vec<Connection>,
892}
893
894#[derive(Debug, Clone)]
896pub enum NodeEdit {
897 AddNode { id: NodeId },
898 RemoveNode { node: SdfNode, connections: Vec<Connection> },
899 Connect { connection: Connection, replaced: Option<Connection> },
900 Disconnect { connection: Connection },
901 MoveNode { id: NodeId, old_pos: Vec2, new_pos: Vec2 },
902 EditPrimitive { id: NodeId, old: PrimitiveKind, new_kind: PrimitiveKind },
903 EditCombinator{ id: NodeId, old: CombinatorKind, new_kind: CombinatorKind },
904 SetRoot { old: NodeId, new_root: NodeId },
905 Batch { edits: Vec<NodeEdit> },
906}
907
908#[derive(Debug)]
910pub struct SdfNodeEditor {
911 pub graph: NodeGraph,
912 pub selection: NodeSelection,
913 pub clipboard: Option<NodeClipboard>,
914 undo_stack: Vec<NodeEdit>,
915 redo_stack: Vec<NodeEdit>,
916 pub pan: Vec2,
918 pub zoom: f32,
920 pub show_glsl: bool,
922 glsl_cache: Option<String>,
924 pub snap_grid: bool,
926 pub grid_size: f32,
927}
928
929impl SdfNodeEditor {
930 pub fn new(graph_name: impl Into<String>) -> Self {
931 Self {
932 graph: NodeGraph::new(graph_name),
933 selection: NodeSelection::default(),
934 clipboard: None,
935 undo_stack: Vec::new(),
936 redo_stack: Vec::new(),
937 pan: Vec2::ZERO,
938 zoom: 1.0,
939 show_glsl: false,
940 glsl_cache: None,
941 snap_grid: true,
942 grid_size: 20.0,
943 }
944 }
945
946 pub fn add_primitive(&mut self, kind: PrimitiveKind) -> NodeId {
949 let id = self.graph.add_primitive(kind);
950 self.undo_stack.push(NodeEdit::AddNode { id });
951 self.redo_stack.clear();
952 self.invalidate_glsl();
953 id
954 }
955
956 pub fn add_combinator(&mut self, kind: CombinatorKind) -> NodeId {
957 let id = self.graph.add_combinator(kind);
958 self.undo_stack.push(NodeEdit::AddNode { id });
959 self.redo_stack.clear();
960 self.invalidate_glsl();
961 id
962 }
963
964 pub fn remove_selected(&mut self) {
965 let ids: Vec<_> = self.selection.selected.drain(..).collect();
966 let mut batch = Vec::new();
967 for id in ids {
968 let conns: Vec<_> = self.graph.connections()
969 .iter().filter(|c| c.from == id || c.to == id).cloned().collect();
970 if let Some(node) = self.graph.remove_node(id) {
971 batch.push(NodeEdit::RemoveNode { node, connections: conns });
972 }
973 }
974 if !batch.is_empty() {
975 self.undo_stack.push(NodeEdit::Batch { edits: batch });
976 self.redo_stack.clear();
977 self.invalidate_glsl();
978 }
979 }
980
981 pub fn connect(&mut self, from: NodeId, to: NodeId, port: PortId) {
982 let replaced = self.graph.connections().iter()
983 .find(|c| c.to == to && c.port == port).cloned();
984 if let Ok(()) = self.graph.connect(from, to, port) {
985 let conn = Connection { from, to, port };
986 self.undo_stack.push(NodeEdit::Connect { connection: conn, replaced });
987 self.redo_stack.clear();
988 self.invalidate_glsl();
989 }
990 }
991
992 pub fn set_root(&mut self, id: NodeId) {
993 let old = self.graph.root;
994 self.graph.root = id;
995 self.undo_stack.push(NodeEdit::SetRoot { old, new_root: id });
996 self.redo_stack.clear();
997 self.invalidate_glsl();
998 }
999
1000 pub fn undo(&mut self) {
1001 if let Some(edit) = self.undo_stack.pop() {
1004 self.redo_stack.push(edit);
1005 self.graph.dirty = true;
1006 self.invalidate_glsl();
1007 }
1008 }
1009
1010 pub fn redo(&mut self) {
1011 if let Some(edit) = self.redo_stack.pop() {
1012 self.undo_stack.push(edit);
1013 self.graph.dirty = true;
1014 self.invalidate_glsl();
1015 }
1016 }
1017
1018 pub fn copy_selected(&mut self) {
1019 let nodes: Vec<_> = self.selection.selected.iter()
1020 .filter_map(|&id| self.graph.get(id).cloned())
1021 .collect();
1022 let sel_set: std::collections::HashSet<NodeId> = self.selection.selected.iter().cloned().collect();
1023 let connections: Vec<_> = self.graph.connections().iter()
1024 .filter(|c| sel_set.contains(&c.from) && sel_set.contains(&c.to))
1025 .cloned()
1026 .collect();
1027 self.clipboard = Some(NodeClipboard { nodes, connections });
1028 }
1029
1030 pub fn paste(&mut self) {
1031 let Some(cb) = self.clipboard.clone() else { return; };
1032 let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
1034 for mut node in cb.nodes {
1035 let old_id = node.id;
1036 node.canvas_pos += Vec2::new(30.0, 30.0); let new_id = self.graph.alloc_id();
1038 node.id = new_id;
1039 id_map.insert(old_id, new_id);
1040 self.graph.nodes.insert(new_id, node);
1041 }
1042 for conn in cb.connections {
1043 if let (Some(&nf), Some(&nt)) = (id_map.get(&conn.from), id_map.get(&conn.to)) {
1044 let _ = self.graph.connect(nf, nt, conn.port);
1045 }
1046 }
1047 self.graph.dirty = true;
1048 self.invalidate_glsl();
1049 }
1050
1051 pub fn canvas_to_world(&self, canvas_p: Vec2) -> Vec2 {
1054 (canvas_p - self.pan) / self.zoom
1055 }
1056
1057 pub fn world_to_canvas(&self, world_p: Vec2) -> Vec2 {
1058 world_p * self.zoom + self.pan
1059 }
1060
1061 pub fn zoom_at(&mut self, canvas_p: Vec2, delta: f32) {
1062 let world_before = self.canvas_to_world(canvas_p);
1063 self.zoom = (self.zoom * (1.0 + delta * 0.1)).clamp(0.1, 10.0);
1064 let world_after = self.canvas_to_world(canvas_p);
1065 self.pan += (world_after - world_before) * self.zoom;
1066 }
1067
1068 pub fn snap(&self, pos: Vec2) -> Vec2 {
1069 if !self.snap_grid { return pos; }
1070 let g = self.grid_size;
1071 Vec2::new((pos.x / g).round() * g, (pos.y / g).round() * g)
1072 }
1073
1074 pub fn begin_drag(&mut self, node: NodeId, cursor: Vec2) {
1075 if let Some(n) = self.graph.get(node) {
1076 let offset = cursor - n.canvas_pos;
1077 self.selection.drag = Some((node, offset));
1078 }
1079 }
1080
1081 pub fn update_drag(&mut self, cursor: Vec2) {
1082 if let Some((id, offset)) = self.selection.drag {
1083 let raw = cursor - offset;
1084 let snapped = self.snap(raw);
1085 if let Some(n) = self.graph.get_mut(id) {
1086 n.canvas_pos = snapped;
1087 }
1088 }
1089 }
1090
1091 pub fn end_drag(&mut self) {
1092 if let Some((id, _)) = self.selection.drag.take() {
1093 if let Some(n) = self.graph.get(id) {
1094 let pos = n.canvas_pos;
1095 self.undo_stack.push(NodeEdit::MoveNode {
1096 id, old_pos: pos, new_pos: pos,
1097 });
1098 }
1099 }
1100 }
1101
1102 fn invalidate_glsl(&mut self) { self.glsl_cache = None; }
1105
1106 pub fn glsl_output(&mut self) -> &str {
1107 if self.glsl_cache.is_none() {
1108 self.glsl_cache = Some(self.graph.compile_glsl());
1109 }
1110 self.glsl_cache.as_deref().unwrap()
1111 }
1112
1113 pub fn status_line(&self) -> String {
1116 let stats = self.graph.stats();
1117 format!(
1118 "SDF Editor — {} | pan ({:.0},{:.0}) zoom {:.2}× | {} sel | {}",
1119 self.graph.name,
1120 self.pan.x, self.pan.y, self.zoom,
1121 self.selection.selected.len(),
1122 stats
1123 )
1124 }
1125
1126 pub fn palette_labels() -> Vec<(&'static str, &'static str)> {
1127 vec![
1128 ("Sphere", "prim"),
1130 ("Capsule", "prim"),
1131 ("Box", "prim"),
1132 ("RoundBox", "prim"),
1133 ("Ellipsoid", "prim"),
1134 ("Torus", "prim"),
1135 ("Cylinder", "prim"),
1136 ("Cone", "prim"),
1137 ("Plane", "prim"),
1138 ("HexPrism", "prim"),
1139 ("OctaHedron", "prim"),
1140 ("SmoothUnion", "comb"),
1142 ("SmoothSubtract", "comb"),
1143 ("SmoothIntersect", "comb"),
1144 ("Union", "comb"),
1145 ("Subtract", "comb"),
1146 ("Intersect", "comb"),
1147 ("Blend", "comb"),
1148 ("Twist", "comb"),
1149 ("Bend", "comb"),
1150 ("Elongate", "comb"),
1151 ("Onion", "comb"),
1152 ("Extrude", "comb"),
1153 ("Revolve", "comb"),
1154 ("Displace", "comb"),
1155 ("Mirror", "comb"),
1156 ("Scale", "comb"),
1157 ("Round", "comb"),
1158 ]
1159 }
1160}
1161
1162impl SdfNodeEditor {
1167 pub fn default_body_graph() -> Self {
1169 let mut ed = SdfNodeEditor::new("body_scaffold");
1170 let g = &mut ed.graph;
1171
1172 let torso = g.add_primitive(PrimitiveKind::Ellipsoid {
1174 radii: Vec3::new(0.25, 0.38, 0.18),
1175 });
1176 g.get_mut(torso).unwrap().label = "Torso".into();
1177 g.get_mut(torso).unwrap().canvas_pos = Vec2::new(400.0, 200.0);
1178
1179 let head = g.add_primitive(PrimitiveKind::Sphere { radius: 0.13 });
1181 g.get_mut(head).unwrap().label = "Head".into();
1182 g.get_mut(head).unwrap().canvas_pos = Vec2::new(400.0, 40.0);
1183 g.get_mut(head).unwrap().transform.translation = Vec3::new(0.0, -0.60, 0.0);
1184
1185 let neck = g.add_primitive(PrimitiveKind::Capsule { half_height: 0.06, radius: 0.055 });
1187 g.get_mut(neck).unwrap().label = "Neck".into();
1188 g.get_mut(neck).unwrap().canvas_pos = Vec2::new(400.0, 120.0);
1189 g.get_mut(neck).unwrap().transform.translation = Vec3::new(0.0, -0.48, 0.0);
1190
1191 let arm_l = g.add_primitive(PrimitiveKind::Capsule { half_height: 0.14, radius: 0.07 });
1193 g.get_mut(arm_l).unwrap().label = "ArmL".into();
1194 g.get_mut(arm_l).unwrap().canvas_pos = Vec2::new(200.0, 200.0);
1195 g.get_mut(arm_l).unwrap().transform.translation = Vec3::new(-0.35, -0.32, 0.0);
1196
1197 let arm_r = g.add_primitive(PrimitiveKind::Capsule { half_height: 0.14, radius: 0.07 });
1198 g.get_mut(arm_r).unwrap().label = "ArmR".into();
1199 g.get_mut(arm_r).unwrap().canvas_pos = Vec2::new(600.0, 200.0);
1200 g.get_mut(arm_r).unwrap().transform.translation = Vec3::new( 0.35, -0.32, 0.0);
1201
1202 let thigh_l = g.add_primitive(PrimitiveKind::Capsule { half_height: 0.20, radius: 0.10 });
1204 g.get_mut(thigh_l).unwrap().label = "ThighL".into();
1205 g.get_mut(thigh_l).unwrap().canvas_pos = Vec2::new(300.0, 400.0);
1206 g.get_mut(thigh_l).unwrap().transform.translation = Vec3::new(-0.13, 0.25, 0.0);
1207
1208 let thigh_r = g.add_primitive(PrimitiveKind::Capsule { half_height: 0.20, radius: 0.10 });
1209 g.get_mut(thigh_r).unwrap().label = "ThighR".into();
1210 g.get_mut(thigh_r).unwrap().canvas_pos = Vec2::new(500.0, 400.0);
1211 g.get_mut(thigh_r).unwrap().transform.translation = Vec3::new( 0.13, 0.25, 0.0);
1212
1213 let shin_l = g.add_primitive(PrimitiveKind::Capsule { half_height: 0.18, radius: 0.075 });
1215 g.get_mut(shin_l).unwrap().label = "ShinL".into();
1216 g.get_mut(shin_l).unwrap().canvas_pos = Vec2::new(300.0, 560.0);
1217 g.get_mut(shin_l).unwrap().transform.translation = Vec3::new(-0.14, 0.60, 0.0);
1218
1219 let shin_r = g.add_primitive(PrimitiveKind::Capsule { half_height: 0.18, radius: 0.075 });
1220 g.get_mut(shin_r).unwrap().label = "ShinR".into();
1221 g.get_mut(shin_r).unwrap().canvas_pos = Vec2::new(500.0, 560.0);
1222 g.get_mut(shin_r).unwrap().transform.translation = Vec3::new( 0.14, 0.60, 0.0);
1223
1224 let k = 0.08;
1226 let merge_legs_l = g.add_combinator(CombinatorKind::SmoothUnion { k });
1227 g.get_mut(merge_legs_l).unwrap().label = "LegL".into();
1228 g.get_mut(merge_legs_l).unwrap().canvas_pos = Vec2::new(300.0, 480.0);
1229
1230 let merge_legs_r = g.add_combinator(CombinatorKind::SmoothUnion { k });
1231 g.get_mut(merge_legs_r).unwrap().label = "LegR".into();
1232 g.get_mut(merge_legs_r).unwrap().canvas_pos = Vec2::new(500.0, 480.0);
1233
1234 let merge_both_legs = g.add_combinator(CombinatorKind::SmoothUnion { k });
1235 g.get_mut(merge_both_legs).unwrap().label = "Legs".into();
1236 g.get_mut(merge_both_legs).unwrap().canvas_pos = Vec2::new(400.0, 560.0);
1237
1238 let merge_arms = g.add_combinator(CombinatorKind::SmoothUnion { k });
1239 g.get_mut(merge_arms).unwrap().label = "Arms".into();
1240 g.get_mut(merge_arms).unwrap().canvas_pos = Vec2::new(400.0, 200.0);
1241
1242 let merge_upper = g.add_combinator(CombinatorKind::SmoothUnion { k: 0.06 });
1243 g.get_mut(merge_upper).unwrap().label = "Upper".into();
1244 g.get_mut(merge_upper).unwrap().canvas_pos = Vec2::new(400.0, 280.0);
1245
1246 let root = g.add_combinator(CombinatorKind::SmoothUnion { k: 0.10 });
1247 g.get_mut(root).unwrap().label = "Root".into();
1248 g.get_mut(root).unwrap().canvas_pos = Vec2::new(400.0, 660.0);
1249
1250 let _ = g.connect(thigh_l, merge_legs_l, PortId::A);
1252 let _ = g.connect(shin_l, merge_legs_l, PortId::B);
1253 let _ = g.connect(thigh_r, merge_legs_r, PortId::A);
1254 let _ = g.connect(shin_r, merge_legs_r, PortId::B);
1255 let _ = g.connect(merge_legs_l, merge_both_legs, PortId::A);
1256 let _ = g.connect(merge_legs_r, merge_both_legs, PortId::B);
1257 let _ = g.connect(arm_l, merge_arms, PortId::A);
1258 let _ = g.connect(arm_r, merge_arms, PortId::B);
1259 let _ = g.connect(torso, merge_upper, PortId::A);
1260 let _ = g.connect(merge_arms, merge_upper, PortId::B);
1261 let _ = g.connect(merge_upper, root, PortId::A);
1262 let _ = g.connect(merge_both_legs, root, PortId::B);
1263 g.root = root;
1264
1265 let merge_head = g.add_combinator(CombinatorKind::SmoothUnion { k: 0.05 });
1267 g.get_mut(merge_head).unwrap().label = "HeadNeck".into();
1268 g.get_mut(merge_head).unwrap().canvas_pos = Vec2::new(400.0, 80.0);
1269 let _ = g.connect(head, merge_head, PortId::A);
1270 let _ = g.connect(neck, merge_head, PortId::B);
1271 let merge_full_upper = g.add_combinator(CombinatorKind::SmoothUnion { k: 0.06 });
1272 g.get_mut(merge_full_upper).unwrap().label = "FullUpper".into();
1273 g.get_mut(merge_full_upper).unwrap().canvas_pos = Vec2::new(400.0, 160.0);
1274 let _ = g.connect(merge_head, merge_full_upper, PortId::A);
1275 let _ = g.connect(merge_upper, merge_full_upper, PortId::B);
1276
1277 g.connections.retain(|c| !(c.to == root && c.port == PortId::A));
1279 let _ = g.connect(merge_full_upper, root, PortId::A);
1280
1281 ed.graph.dirty = false;
1282 ed
1283 }
1284}
1285
1286#[cfg(test)]
1291mod tests {
1292 use super::*;
1293
1294 #[test]
1295 fn sphere_sdf() {
1296 let prim = PrimitiveKind::Sphere { radius: 1.0 };
1297 assert!((prim.evaluate(Vec3::ZERO) - (-1.0)).abs() < 1e-5);
1298 assert!((prim.evaluate(Vec3::X) - 0.0).abs() < 1e-5);
1299 assert!((prim.evaluate(Vec3::X * 2.0) - 1.0).abs() < 1e-5);
1300 }
1301
1302 #[test]
1303 fn smooth_union_combines() {
1304 let c = CombinatorKind::SmoothUnion { k: 0.1 };
1305 let d = c.combine(0.5, 0.5, Vec3::ZERO);
1307 assert!(d <= 0.5);
1308 }
1309
1310 #[test]
1311 fn graph_sample_sphere() {
1312 let mut ed = SdfNodeEditor::new("test");
1313 let id = ed.add_primitive(PrimitiveKind::Sphere { radius: 1.0 });
1314 ed.set_root(id);
1315 assert!((ed.graph.sample(Vec3::ZERO) + 1.0).abs() < 1e-4);
1316 }
1317
1318 #[test]
1319 fn cycle_detection() {
1320 let mut g = NodeGraph::new("cycle_test");
1321 let a = g.add_combinator(CombinatorKind::Union);
1322 let b = g.add_combinator(CombinatorKind::Union);
1323 assert!(g.connect(a, b, PortId::A).is_ok());
1324 let _ = g.connect(b, a, PortId::A); }
1330
1331 #[test]
1332 fn glsl_compiles() {
1333 let mut ed = SdfNodeEditor::default_body_graph();
1334 let glsl = ed.glsl_output().to_string();
1335 assert!(glsl.contains("float sdf_body"));
1336 }
1337
1338 #[test]
1339 fn auto_layout_no_panic() {
1340 let mut ed = SdfNodeEditor::default_body_graph();
1341 ed.graph.auto_layout();
1342 }
1343}