tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1//! `graph` — **puente entre la ecuación de una ley y un grafo de nodos**: la segunda
2//! superficie de autoría (la primera es la barra de fórmula). Ambas son vistas del
3//! **mismo AST** [`Expr`]: el grafo se deriva de las fórmulas al entrar en modo nodos
4//! ([`EqGraph::from_fuentes`]) y se recompila a fórmulas en cada edición
5//! ([`EqGraph::to_fuentes`]) — ida y vuelta sin una segunda fuente de verdad (el texto
6//! sigue siendo canónico y persistente; el grafo es estado de UI, no se guarda).
7//!
8//! Modelo: un nodo por sub‑expresión. Las **hojas** (constante, campo, parámetro,
9//! `dt`, y los muestreos de vecinos `lap`/`avg`/`abajo`… que llevan su campo adentro)
10//! no tienen entradas; los **operadores** (unario/binario/clamp) tienen 1/2/3 entradas;
11//! y cada campo remata en un **sink** `Δcampoₖ` (1 entrada, sin salida). Convertir el
12//! grafo a `Expr` es seguir el cable de cada sink hacia atrás.
13
14use llimphi_voxel::{BinOp, Dir, Expr, Reduce, Symbols, UnOp};
15use llimphi_widget_nodegraph::{NodeId, NodeSpec, PinIdx, Wire};
16
17/// La **operación** de un nodo = un constructor de [`Expr`] (o el sink de un campo).
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum NodeOp {
20 Const(f32),
21 Field(u16),
22 Param(u16),
23 Dt,
24 Lap(u16),
25 Vecinos(Reduce, u16),
26 Dir(Dir, u16),
27 Un(UnOp),
28 Bin(BinOp),
29 Clamp,
30 /// Remate del campo `k`: `Δcampoₖ/dt = <entrada>`.
31 Sink(u16),
32}
33
34impl NodeOp {
35 /// Etiquetas de los pins de entrada (su cantidad = aridad).
36 pub fn inputs(&self) -> Vec<String> {
37 match self {
38 NodeOp::Un(_) => vec!["x".into()],
39 NodeOp::Bin(_) => vec!["a".into(), "b".into()],
40 NodeOp::Clamp => vec!["x".into(), "lo".into(), "hi".into()],
41 NodeOp::Sink(_) => vec!["=".into()],
42 _ => vec![],
43 }
44 }
45 /// Etiquetas de los pins de salida (el sink no tiene).
46 pub fn outputs(&self) -> Vec<String> {
47 match self {
48 NodeOp::Sink(_) => vec![],
49 _ => vec!["".into()],
50 }
51 }
52 /// Texto del nodo (usa los nombres de campos/params de `sym`).
53 pub fn label(&self, sym: &Symbols) -> String {
54 let campo = |f: u16| sym.campos.get(f as usize).cloned().unwrap_or_else(|| format!("c{f}"));
55 match self {
56 NodeOp::Const(c) => fmt_num(*c),
57 NodeOp::Field(f) => campo(*f),
58 NodeOp::Param(p) => sym.params.get(*p as usize).cloned().unwrap_or_else(|| format!("p{p}")),
59 NodeOp::Dt => "dt".into(),
60 NodeOp::Lap(f) => format!("lap({})", campo(*f)),
61 NodeOp::Vecinos(r, f) => format!("{}({})", reduce_name(*r), campo(*f)),
62 NodeOp::Dir(d, f) => format!("{}({})", dir_name(*d), campo(*f)),
63 NodeOp::Un(u) => un_name(*u).into(),
64 NodeOp::Bin(b) => bin_name(*b).into(),
65 NodeOp::Clamp => "clamp".into(),
66 NodeOp::Sink(k) => format!("Δ{}", campo(*k)),
67 }
68 }
69}
70
71/// Un nodo del grafo: id + operación + posición en el lienzo.
72#[derive(Debug, Clone)]
73pub struct EqNode {
74 pub id: NodeId,
75 pub op: NodeOp,
76 pub x: f32,
77 pub y: f32,
78}
79
80/// Grafo editable de una ley: nodos + cables + los sinks (uno por campo, en orden).
81/// Estado de UI transitorio; se compila a `fuentes` en cada cambio.
82#[derive(Debug, Clone, Default)]
83pub struct EqGraph {
84 pub nodes: Vec<EqNode>,
85 pub wires: Vec<Wire>,
86 pub sinks: Vec<NodeId>,
87 next_id: NodeId,
88}
89
90const COL_W: f32 = 150.0;
91const ROW_H: f32 = 64.0;
92const CAMPO_GAP: f32 = 90.0;
93const ORIGIN_X: f32 = 40.0;
94const ORIGIN_Y: f32 = 40.0;
95
96impl EqGraph {
97 /// Construye el grafo a partir de las fórmulas de una ley (una por campo). Las que
98 /// no parsean caen a `Const(0)`. Auto‑distribuye los nodos (hojas a la izquierda,
99 /// sink a la derecha; un bloque por campo, apilados).
100 pub fn from_fuentes(fuentes: &[String], sym: &Symbols) -> EqGraph {
101 let mut g = EqGraph::default();
102 let mut base_y = ORIGIN_Y;
103 for (k, src) in fuentes.iter().enumerate() {
104 let expr = Expr::parse(src, sym).unwrap_or(Expr::Const(0.0));
105 let mut cursor = base_y;
106 let (root, depth) = g.build_expr(&expr, &mut cursor);
107 // Sink a la derecha del árbol.
108 let sink = g.push(NodeOp::Sink(k as u16), 0.0, 0.0);
109 let sink_depth = depth + 1;
110 g.set_pos(sink, sink_depth, (base_y + cursor - ROW_H) * 0.5);
111 g.wires.push(Wire { from_node: root, from_output: 0, to_node: sink, to_input: 0 });
112 g.sinks.push(sink);
113 base_y = cursor + CAMPO_GAP;
114 }
115 // x se fijó por profundidad relativa a la izquierda; recolocar con columnas
116 // globales para que el sink quede a la derecha del más profundo.
117 g.reflow();
118 g
119 }
120
121 fn push(&mut self, op: NodeOp, x: f32, y: f32) -> NodeId {
122 let id = self.next_id;
123 self.next_id += 1;
124 self.nodes.push(EqNode { id, op, x, y });
125 id
126 }
127
128 /// `x` provisorio = profundidad (columnas desde la izquierda); se reacomoda en
129 /// [`reflow`]. `y` = fila. Guarda la profundidad en `x` como entero flotante.
130 fn set_pos(&mut self, id: NodeId, depth: usize, y: f32) {
131 if let Some(n) = self.nodes.iter_mut().find(|n| n.id == id) {
132 n.x = depth as f32;
133 n.y = y;
134 }
135 }
136
137 /// Crea los nodos de una `Expr` (post‑orden). Devuelve `(id_raíz, profundidad)`.
138 /// `cursor` avanza una fila por hoja para apilar sin solaparse.
139 fn build_expr(&mut self, e: &Expr, cursor: &mut f32) -> (NodeId, usize) {
140 match e {
141 Expr::Const(c) => (self.leaf(NodeOp::Const(*c), cursor), 0),
142 Expr::Field(f) => (self.leaf(NodeOp::Field(*f), cursor), 0),
143 Expr::Param(p) => (self.leaf(NodeOp::Param(*p), cursor), 0),
144 Expr::Dt => (self.leaf(NodeOp::Dt, cursor), 0),
145 Expr::Laplacian(f) => (self.leaf(NodeOp::Lap(*f), cursor), 0),
146 Expr::Vecinos(r, f) => (self.leaf(NodeOp::Vecinos(*r, *f), cursor), 0),
147 Expr::Dir(d, f) => (self.leaf(NodeOp::Dir(*d, *f), cursor), 0),
148 Expr::Un(op, a) => {
149 let (ca, da) = self.build_expr(a, cursor);
150 let id = self.push(NodeOp::Un(*op), 0.0, 0.0);
151 let y = self.y_of(ca);
152 self.set_pos(id, da + 1, y);
153 self.wires.push(Wire { from_node: ca, from_output: 0, to_node: id, to_input: 0 });
154 (id, da + 1)
155 }
156 Expr::Bin(op, a, b) => {
157 let (ca, da) = self.build_expr(a, cursor);
158 let (cb, db) = self.build_expr(b, cursor);
159 let id = self.push(NodeOp::Bin(*op), 0.0, 0.0);
160 let y = (self.y_of(ca) + self.y_of(cb)) * 0.5;
161 self.set_pos(id, da.max(db) + 1, y);
162 self.wires.push(Wire { from_node: ca, from_output: 0, to_node: id, to_input: 0 });
163 self.wires.push(Wire { from_node: cb, from_output: 0, to_node: id, to_input: 1 });
164 (id, da.max(db) + 1)
165 }
166 Expr::Clamp(x, lo, hi) => {
167 let (cx, dx) = self.build_expr(x, cursor);
168 let (cl, dl) = self.build_expr(lo, cursor);
169 let (ch, dh) = self.build_expr(hi, cursor);
170 let id = self.push(NodeOp::Clamp, 0.0, 0.0);
171 let y = self.y_of(cl);
172 self.set_pos(id, dx.max(dl).max(dh) + 1, y);
173 self.wires.push(Wire { from_node: cx, from_output: 0, to_node: id, to_input: 0 });
174 self.wires.push(Wire { from_node: cl, from_output: 0, to_node: id, to_input: 1 });
175 self.wires.push(Wire { from_node: ch, from_output: 0, to_node: id, to_input: 2 });
176 (id, dx.max(dl).max(dh) + 1)
177 }
178 }
179 }
180
181 fn leaf(&mut self, op: NodeOp, cursor: &mut f32) -> NodeId {
182 let y = *cursor;
183 *cursor += ROW_H;
184 self.push(op, 0.0, y)
185 }
186
187 fn y_of(&self, id: NodeId) -> f32 {
188 self.nodes.iter().find(|n| n.id == id).map(|n| n.y).unwrap_or(0.0)
189 }
190
191 /// Convierte las profundidades guardadas en `x` a coordenadas de lienzo: la
192 /// columna 0 es la más profunda (izquierda), y crece hacia la derecha.
193 fn reflow(&mut self) {
194 let maxd = self.nodes.iter().map(|n| n.x as usize).max().unwrap_or(0);
195 for n in &mut self.nodes {
196 let depth = n.x as usize;
197 n.x = ORIGIN_X + (maxd - depth) as f32 * COL_W;
198 }
199 }
200
201 // --- Grafo → fórmulas ---------------------------------------------------
202
203 /// Fuente de cada campo, reconstruida siguiendo el cable de su sink hacia atrás.
204 /// Campo sin sink o con la entrada del sink desconectada → `"0"`.
205 pub fn to_fuentes(&self, n_campos: usize, sym: &Symbols) -> Vec<String> {
206 (0..n_campos)
207 .map(|k| {
208 let sink = self.sinks.get(k).copied();
209 let root = sink.and_then(|s| self.source_into(s, 0));
210 match root {
211 Some(r) => self.build_expr_from(r, 0).to_source(sym),
212 None => "0".to_string(),
213 }
214 })
215 .collect()
216 }
217
218 /// Nodo cuyo output alimenta la entrada `pin` del nodo `node` (o `None`).
219 pub fn source_into(&self, node: NodeId, pin: PinIdx) -> Option<NodeId> {
220 self.wires
221 .iter()
222 .find(|w| w.to_node == node && w.to_input == pin)
223 .map(|w| w.from_node)
224 }
225
226 /// Reconstruye la `Expr` de un nodo (entradas faltantes → `Const(0)`). Corta a
227 /// profundidad 128 por si un rewire malicioso arma un ciclo.
228 fn build_expr_from(&self, node: NodeId, depth: u32) -> Expr {
229 if depth > 128 {
230 return Expr::Const(0.0);
231 }
232 let Some(n) = self.nodes.iter().find(|n| n.id == node) else {
233 return Expr::Const(0.0);
234 };
235 let arg = |i: PinIdx| -> Expr {
236 match self.source_into(node, i) {
237 Some(src) => self.build_expr_from(src, depth + 1),
238 None => Expr::Const(0.0),
239 }
240 };
241 match n.op {
242 NodeOp::Const(c) => Expr::Const(c),
243 NodeOp::Field(f) => Expr::Field(f),
244 NodeOp::Param(p) => Expr::Param(p),
245 NodeOp::Dt => Expr::Dt,
246 NodeOp::Lap(f) => Expr::Laplacian(f),
247 NodeOp::Vecinos(r, f) => Expr::Vecinos(r, f),
248 NodeOp::Dir(d, f) => Expr::Dir(d, f),
249 NodeOp::Un(op) => Expr::Un(op, Box::new(arg(0))),
250 NodeOp::Bin(op) => Expr::Bin(op, Box::new(arg(0)), Box::new(arg(1))),
251 NodeOp::Clamp => Expr::Clamp(Box::new(arg(0)), Box::new(arg(1)), Box::new(arg(2))),
252 NodeOp::Sink(_) => arg(0),
253 }
254 }
255
256 // --- Edición ------------------------------------------------------------
257
258 /// Agrega un nodo suelto (para el paladar de nodos). Devuelve su id.
259 pub fn add(&mut self, op: NodeOp, x: f32, y: f32) -> NodeId {
260 self.push(op, x, y)
261 }
262
263 /// Mueve un nodo (arrastre; layout).
264 pub fn drag(&mut self, id: NodeId, dx: f32, dy: f32) {
265 if let Some(n) = self.nodes.iter_mut().find(|n| n.id == id) {
266 n.x = (n.x + dx).max(0.0);
267 n.y = (n.y + dy).max(0.0);
268 }
269 }
270
271 /// Conecta `from`(salida 0) → entrada `to_pin` de `to`, reemplazando lo que
272 /// hubiera. Rechaza auto‑lazo y conexiones que crearían un ciclo.
273 pub fn connect(&mut self, from: NodeId, to: NodeId, to_pin: PinIdx) -> bool {
274 if from == to || self.reachable(to, from) {
275 return false; // ciclo
276 }
277 self.wires.retain(|w| !(w.to_node == to && w.to_input == to_pin));
278 self.wires.push(Wire { from_node: from, from_output: 0, to_node: to, to_input: to_pin });
279 true
280 }
281
282 /// Borra un nodo y todos sus cables. Un sink no se borra (define el campo).
283 pub fn delete(&mut self, id: NodeId) {
284 if self.sinks.contains(&id) {
285 return;
286 }
287 self.nodes.retain(|n| n.id != id);
288 self.wires.retain(|w| w.from_node != id && w.to_node != id);
289 }
290
291 /// ¿Se llega de `start` a `target` siguiendo cables hacia adelante? (anti‑ciclo).
292 fn reachable(&self, start: NodeId, target: NodeId) -> bool {
293 let mut stack = vec![start];
294 let mut seen = Vec::new();
295 while let Some(n) = stack.pop() {
296 if n == target {
297 return true;
298 }
299 if seen.contains(&n) {
300 continue;
301 }
302 seen.push(n);
303 for w in self.wires.iter().filter(|w| w.from_node == n) {
304 stack.push(w.to_node);
305 }
306 }
307 false
308 }
309
310 /// Especificaciones para el widget (label por `sym`).
311 pub fn node_specs(&self, sym: &Symbols) -> Vec<NodeSpec> {
312 self.nodes
313 .iter()
314 .map(|n| NodeSpec {
315 id: n.id,
316 label: n.op.label(sym),
317 x: n.x,
318 y: n.y,
319 inputs: n.op.inputs(),
320 outputs: n.op.outputs(),
321 })
322 .collect()
323 }
324}
325
326// --- Nombres de operadores (para labels) ------------------------------------
327
328fn reduce_name(r: Reduce) -> &'static str {
329 match r {
330 Reduce::Promedio => "avg",
331 Reduce::Suma => "sum6",
332 Reduce::Min => "min6",
333 Reduce::Max => "max6",
334 }
335}
336fn dir_name(d: Dir) -> &'static str {
337 match d {
338 Dir::Abajo => "abajo",
339 Dir::Arriba => "arriba",
340 Dir::Este => "este",
341 Dir::Oeste => "oeste",
342 Dir::Norte => "norte",
343 Dir::Sur => "sur",
344 }
345}
346fn un_name(u: UnOp) -> &'static str {
347 match u {
348 UnOp::Neg => "−x",
349 UnOp::Abs => "abs",
350 UnOp::Exp => "exp",
351 UnOp::Sqrt => "sqrt",
352 }
353}
354fn bin_name(b: BinOp) -> &'static str {
355 match b {
356 BinOp::Add => "+",
357 BinOp::Sub => "−",
358 BinOp::Mul => "×",
359 BinOp::Div => "÷",
360 BinOp::Min => "min",
361 BinOp::Max => "max",
362 BinOp::Gt => ">",
363 BinOp::Lt => "<",
364 }
365}
366fn fmt_num(c: f32) -> String {
367 if c.fract() == 0.0 && c.abs() < 1e7 {
368 format!("{}", c as i64)
369 } else {
370 format!("{c}")
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 fn sym() -> Symbols {
379 Symbols {
380 campos: vec!["u".into(), "v".into()],
381 params: vec!["Du".into(), "Dv".into(), "F".into(), "k".into()],
382 }
383 }
384
385 #[test]
386 fn round_trip_expr_grafo_expr() {
387 let sym = sym();
388 let fuentes = vec![
389 "Du * lap(u) - u * v * v + F * (1 - u)".to_string(),
390 "Dv * lap(v) + u * v * v - (F + k) * v".to_string(),
391 ];
392 let g = EqGraph::from_fuentes(&fuentes, &sym);
393 let back = g.to_fuentes(2, &sym);
394 for i in 0..2 {
395 let a = Expr::parse(&fuentes[i], &sym).unwrap();
396 let b = Expr::parse(&back[i], &sym).unwrap();
397 assert_eq!(a, b, "campo {i}: {} → {}", fuentes[i], back[i]);
398 }
399 }
400
401 #[test]
402 fn round_trip_terminos_variados() {
403 let sym = Symbols { campos: vec!["c".into()], params: vec!["k".into()] };
404 for src in [
405 "clamp(c + dt, 0, 1)",
406 "min(avg(c), max6(c)) - abajo(c)",
407 "k * lap(c) + abs(c) * exp(k)",
408 "sqrt(c) < k",
409 ] {
410 let g = EqGraph::from_fuentes(&[src.to_string()], &sym);
411 let back = g.to_fuentes(1, &sym);
412 let a = Expr::parse(src, &sym).unwrap();
413 let b = Expr::parse(&back[0], &sym).unwrap();
414 assert_eq!(a, b, "{src} → {}", back[0]);
415 }
416 }
417
418 #[test]
419 fn rewire_cambia_la_formula() {
420 let sym = Symbols { campos: vec!["c".into()], params: vec!["k".into()] };
421 // c + k → reconectar la entrada `b` del `+` a un nodo `dt`.
422 let mut g = EqGraph::from_fuentes(&["c + k".to_string()], &sym);
423 // encontrar el nodo `+` (Bin(Add)).
424 let plus = g.nodes.iter().find(|n| matches!(n.op, NodeOp::Bin(BinOp::Add))).unwrap().id;
425 let dt = g.add(NodeOp::Dt, 0.0, 0.0);
426 assert!(g.connect(dt, plus, 1), "reconecta la entrada b");
427 let back = g.to_fuentes(1, &sym);
428 let got = Expr::parse(&back[0], &sym).unwrap();
429 let want = Expr::parse("c + dt", &sym).unwrap();
430 assert_eq!(got, want, "quedó: {}", back[0]);
431 }
432
433 #[test]
434 fn connect_rechaza_ciclo() {
435 let sym = Symbols { campos: vec!["c".into()], params: vec![] };
436 let mut g = EqGraph::from_fuentes(&["c + c".to_string()], &sym);
437 let plus = g.nodes.iter().find(|n| matches!(n.op, NodeOp::Bin(BinOp::Add))).unwrap().id;
438 // intentar alimentar el `+` con su propia salida → ciclo, rechazado.
439 assert!(!g.connect(plus, plus, 0), "auto-lazo rechazado");
440 }
441
442 #[test]
443 fn sink_no_se_borra() {
444 let sym = Symbols { campos: vec!["c".into()], params: vec![] };
445 let mut g = EqGraph::from_fuentes(&["c".to_string()], &sym);
446 let sink = g.sinks[0];
447 g.delete(sink);
448 assert!(g.nodes.iter().any(|n| n.id == sink), "el sink sobrevive");
449 }
450}