tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main llimphi-voxel-studio/src/graph.rs Rust · 450 siq'ikuna · 16.9 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

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}