git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit3825007457507df4f0ee164f1be8cec626ec41cc
parent1267592727
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-01 12:26
feat: a graph names the wire through a cell, for a node added there

GraphController::input_wire_through_cell asks the hit test a dragged node's
splice drop uses about the body a NEW node would have at a lattice cell, so
a host adding a node on a wire can wire it into that chain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/widget/display/graph.rs | 60 ++++++++++++++++++++++++++++++++++++++++++---
 src/widget/mod.rs           |  7 ++++++
 2 files changed, 64 insertions(+), 3 deletions(-)

diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index bab77c3..01b2f7e 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -1417,13 +1417,32 @@ impl Graph {
             return None;
         }
         let (_, _, nw, nh) = self.node_rect(idx)?;
+        self.input_wire_meeting(nx, ny, nw, nh, Some(idx))
+    }
+
+    /// The wire into an Input (port 0) that runs through the body a node
+    /// would have at lattice cell (`col`, `row`), as (upstream id,
+    /// downstream id) — by the hit test a dragged node's splice drop uses,
+    /// so a host placing a NEW node there (the designer's Add Node at its
+    /// grid cursor) wires it in exactly where a drop would have. Whether
+    /// the cell is free is the host's to ask; a node's own wires touch it.
+    pub fn input_wire_through_cell(&self, col: f32, row: f32) -> Option<(String, String)> {
+        let (x, y) = self.cell_origin(col, row);
+        let (src, dest) = self.input_wire_meeting(x, y, self.node_w, self.node_h, None)?;
+        Some((self.nodes[src].id.clone(), self.nodes[dest].id.clone()))
+    }
+
+    /// The first wire into a port 0 (draw order) whose path, as drawn,
+    /// touches the body rect at (x, y), inflated by the wire activation
+    /// radius so a near miss still takes. `skip`'s own wires never count.
+    fn input_wire_meeting(&self, x: f32, y: f32, w: f32, h: f32, skip: Option<usize>) -> Option<(usize, usize)> {
         // Half the stroke on top of the activation radius, so the wire's
         // edge counts and not only its centreline.
         let pad = crate::layout::graph_wire_activation_radius().max(0.0) + self.wire_thickness() / 2.0;
-        let (gx1, gy1) = (nx - pad, ny - pad);
-        let (gx2, gy2) = (nx + nw + pad, ny + nh + pad);
+        let (gx1, gy1) = (x - pad, y - pad);
+        let (gx2, gy2) = (x + w + pad, y + h + pad);
         for (src, dest, port) in self.wire_pairs() {
-            if src == idx || dest == idx || port != 0 {
+            if Some(src) == skip || Some(dest) == skip || port != 0 {
                 continue;
             }
             let Some(segs) = self.wire_segments(src, dest, port) else { continue };
@@ -1571,6 +1590,9 @@ impl GraphController for Graph {
     fn take_pending_splice(&mut self) -> Option<(String, String, String)> {
         self.pending_splice.take()
     }
+    fn input_wire_through_cell(&self, col: f32, row: f32) -> Option<(String, String)> {
+        Graph::input_wire_through_cell(self, col, row)
+    }
     fn cancel_connecting(&mut self) {
         self.connecting_from = None;
     }
@@ -1779,6 +1801,38 @@ mod tests {
         assert_eq!(GraphController::take_pending_splice(&mut *g), None);
     }
 
+    /// A cell a wire runs through names that wire, in every style; a cell
+    /// clear of every wire names none.
+    #[test]
+    fn a_cell_on_a_wire_names_the_wire() {
+        for style in WireStyle::ALL {
+            let mut g = Graph::new();
+            g.set_wire_style(Some(style));
+            WidgetHost::set_rect(&mut g, 0.0, 0.0, 800.0, 600.0);
+            g.set_grid_pitch(100.0, 60.0);
+            g.set_node_size(80.0, 40.0);
+            g.set_grid_origin(140.0, 120.0);
+            let node = |id: &str, name: &str, col: f32, row: f32, input: &str| GraphNode {
+                id: id.into(),
+                name: name.into(),
+                position: (col, row),
+                parameters: vec![("Input".to_string(), input.to_string(), "node".to_string())],
+                geom_visible: true,
+                node_type: String::new(),
+                inputs: 1,
+                outputs: 1,
+            };
+            // alpha above beta, a cell between them.
+            g.set_nodes(&[node("a", "alpha", 0.0, 0.0, ""), node("b", "beta", 0.0, 2.0, "alpha")]);
+            assert_eq!(
+                g.input_wire_through_cell(0.0, 1.0),
+                Some(("a".to_string(), "b".to_string())),
+                "{style:?}"
+            );
+            assert_eq!(g.input_wire_through_cell(2.0, 1.0), None, "{style:?}: off to the side");
+        }
+    }
+
     /// Where a piece of a wire begins and ends.
     fn seg_ends(seg: &WireSeg) -> ((f32, f32), (f32, f32)) {
         match *seg {
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 3b624fb..576a3eb 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -693,6 +693,13 @@ pub trait GraphController {
     fn take_pending_splice(&mut self) -> Option<(String, String, String)> {
         None
     }
+    /// The wire into an Input that runs through the body a node would have
+    /// at lattice cell (col, row), as (upstream id, downstream id): where a
+    /// node ADDED there splices in, by the hit test a drop uses. Default
+    /// None for hosts whose graphs have no wires to splice.
+    fn input_wire_through_cell(&self, _col: f32, _row: f32) -> Option<(String, String)> {
+        None
+    }
     fn cancel_connecting(&mut self);
     fn is_node_rect(&self, qx: f32, qy: f32, qw: f32, qh: f32) -> bool;
     /// The topmost node whose body contains (px, py), window-absolute coords.