git.lucas.co / cce-graph
node-based graph editor
git clone https://git.lucas.co/cce-graph.git

commitbde42dc2d1ee2ae2bb931b6d8619f573fdc1e3c5
parentf9019995c0
authorLucas Galante <[email protected]>
date2026-07-01 21:41
Render loaded image as background quads matching grid position and size

 Cargo.toml  |   1 +
 src/main.rs | 165 +++++++++++++++++++++++++++++++++++++++++++++++++++++-------
 2 files changed, 148 insertions(+), 18 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 88f0c60..7236966 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,3 +18,4 @@ glyphon = "0.8"
 tokio = { version = "1", features = ["full"] }
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
+image = { version = "0.25.10", features = ["png", "jpeg"] }
diff --git a/src/main.rs b/src/main.rs
index 76601af..8fa5f72 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,6 +2,7 @@ use wayland_client::QueueHandle;
 use glyphon::{FontSystem, Buffer, Metrics, Attrs};
 use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
 use cce_ui::widget::{MouseButton, ElementState, MouseScrollDelta, KeyEvent, TextItem, Element, Graph, GraphNode, MenuBar, MenuController, GraphController, WidgetId};
+use image::GenericImageView;
 
 #[derive(Debug, Clone)]
 enum AppMessage {
@@ -19,15 +20,32 @@ enum AppMessage {
     AddImage,
 }
 
+#[derive(serde::Serialize, serde::Deserialize, Clone)]
+struct GraphProjectImage {
+    path: String,
+    position: (f32, f32), // (column, row)
+    size: (f32, f32), // (w_cols, h_rows)
+}
+
 #[derive(serde::Serialize, serde::Deserialize)]
 struct GraphProjectState {
     name: String,
     nodes: Vec<GraphNode>,
+    images: Vec<GraphProjectImage>,
     show_grid: bool,
     uniform_background: bool,
     opacity: f32,
 }
 
+struct LoadedImage {
+    path: String,
+    position: (f32, f32),
+    size: (f32, f32),
+    pixels: Vec<[u8; 4]>,
+    pixel_width: u32,
+    pixel_height: u32,
+}
+
 struct GraphApp {
     menu_bar: MenuBar,
     graph: Graph,
@@ -43,6 +61,24 @@ struct GraphApp {
     opacity: f32,
     ui_context: cce_ui::context::UiContext,
     loaded_project_path: Option<std::path::PathBuf>,
+    loaded_images: Vec<LoadedImage>,
+}
+
+fn load_image_pixels(path: &std::path::Path) -> Option<(Vec<[u8; 4]>, u32, u32)> {
+    let img = image::open(path).ok()?;
+    let max_dim = 96;
+    let (w, h) = img.dimensions();
+    let (nw, nh) = if w > h {
+        (max_dim, (h as f32 * (max_dim as f32 / w as f32)) as u32)
+    } else {
+        ((w as f32 * (max_dim as f32 / h as f32)) as u32, max_dim)
+    };
+    let img = img.resize_exact(nw, nh, image::imageops::FilterType::Triangle);
+    let rgba = img.to_rgba8();
+    let pixels = rgba.chunks_exact(4)
+        .map(|c| [c[0], c[1], c[2], c[3]])
+        .collect();
+    Some((pixels, nw, nh))
 }
 
 impl GraphApp {
@@ -74,6 +110,7 @@ impl GraphApp {
 
     fn new_project(&mut self) {
         self.graph.set_nodes(&[]);
+        self.loaded_images.clear();
         self.loaded_project_path = None;
         self.needs_rebuild = true;
     }
@@ -88,12 +125,21 @@ impl GraphApp {
 
         let state_file_path = project_dir.join("state.json");
 
+        let project_images: Vec<GraphProjectImage> = self.loaded_images.iter()
+            .map(|img| GraphProjectImage {
+                path: img.path.clone(),
+                position: img.position,
+                size: img.size,
+            })
+            .collect();
+
         let state = GraphProjectState {
             name: project_dir.file_name()
                 .and_then(|n| n.to_str())
                 .unwrap_or("Graph Project")
                 .to_string(),
             nodes: self.graph.get_nodes(),
+            images: project_images,
             show_grid: self.show_grid,
             uniform_background: self.uniform_background,
             opacity: self.opacity,
@@ -126,6 +172,29 @@ impl GraphApp {
         self.uniform_background = state.uniform_background;
         self.opacity = state.opacity;
 
+        // Load images
+        self.loaded_images.clear();
+        for img in state.images {
+            let image_path = if std::path::Path::new(&img.path).is_absolute() {
+                std::path::PathBuf::from(&img.path)
+            } else {
+                project_dir.join(&img.path)
+            };
+
+            if let Some((pixels, pw, ph)) = load_image_pixels(&image_path) {
+                self.loaded_images.push(LoadedImage {
+                    path: img.path.clone(),
+                    position: img.position,
+                    size: img.size,
+                    pixels,
+                    pixel_width: pw,
+                    pixel_height: ph,
+                });
+            } else {
+                eprintln!("Warning: Failed to load image at {:?}", image_path);
+            }
+        }
+
         // Apply grid/background settings to self.graph
         self.graph.set_show_network_grid(self.show_grid);
         self.graph.set_uniform_background(self.uniform_background);
@@ -143,7 +212,7 @@ impl GraphApp {
         Ok(())
     }
 
-    fn add_image_node(&mut self, src_path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
+    fn add_image(&mut self, src_path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
         let image_name = src_path.file_name()
             .map(|n| n.to_string_lossy().to_string())
             .unwrap_or_else(|| "image.png".to_string());
@@ -153,27 +222,29 @@ impl GraphApp {
             std::fs::create_dir_all(&dest_dir)?;
             let dest_path = dest_dir.join(&image_name);
             std::fs::copy(src_path, &dest_path)?;
-            // Save relative path to state
             format!("assets/{}", image_name)
         } else {
-            // If no project loaded, use the absolute path
             src_path.to_string_lossy().to_string()
         };
 
-        let mut nodes = self.graph.get_nodes();
-        let next_id = nodes.len() + 1;
-        nodes.push(GraphNode {
-            id: String::new(),
-            name: image_name,
-            position: (2.0 + (next_id % 3) as f32, 2.0 + (next_id / 3) as f32),
-            parameters: vec![("image".to_string(), final_path, "string".to_string())],
-            geom_visible: true,
-            node_type: "image".to_string(),
-            inputs: 0,
-            outputs: 1,
-        });
-        self.graph.set_nodes(&nodes);
-        self.needs_rebuild = true;
+        if let Some((pixels, pw, ph)) = load_image_pixels(src_path) {
+            let aspect = ph as f32 / pw as f32;
+            let size_w = 4.0;
+            let size_h = size_w * aspect;
+            
+            let col = 2.0;
+            let row = 2.0 + self.loaded_images.len() as f32 * 5.0;
+
+            self.loaded_images.push(LoadedImage {
+                path: final_path,
+                position: (col, row),
+                size: (size_w, size_h),
+                pixels,
+                pixel_width: pw,
+                pixel_height: ph,
+            });
+            self.needs_rebuild = true;
+        }
         Ok(())
     }
 }
@@ -270,6 +341,7 @@ impl Application for GraphApp {
             opacity,
             ui_context: cce_ui::context::UiContext::new(),
             loaded_project_path: None,
+            loaded_images: Vec::new(),
         };
         
         app.menu_bar.set_rect(0.0, 0.0, 1024.0, 26.0);
@@ -406,7 +478,7 @@ impl Application for GraphApp {
                 let exts: &[&str] = &["png", "jpg", "jpeg", "gif", "bmp"];
                 let filters = [("Images", exts)];
                 if let Some(path) = cce_ui::file_dialog::pick_file("Select Image", &filters) {
-                    if let Err(e) = self.add_image_node(&path) {
+                    if let Err(e) = self.add_image(&path) {
                         eprintln!("Failed to add image: {:?}", e);
                     }
                 }
@@ -460,6 +532,63 @@ impl Application for GraphApp {
             let (gx, gy, gw, gh) = self.graph.rect();
             quads.push((gx, gy, gw, gh, graph_color));
         }
+
+        // Draw background images in the graph grid
+        let (grid_origin_x, grid_origin_y) = self.graph.grid_origin();
+        let (grid_size_x, grid_size_y) = self.graph.grid_sizes();
+        let (skipped_row_h, skipped_col_w) = self.graph.skipped_sizes();
+        
+        let step_x = grid_size_x + skipped_col_w;
+        let step_y = grid_size_y + skipped_row_h;
+
+        let (graph_x, graph_y, graph_w, graph_h) = self.graph.rect();
+        let min_x = graph_x;
+        let min_y = graph_y;
+        let max_x = graph_x + graph_w;
+        let max_y = graph_y + graph_h;
+
+        let push_clipped = |qx: f32, qy: f32, qw: f32, qh: f32, qc: [f32; 4], q: &mut Vec<(f32, f32, f32, f32, [f32; 4])>| {
+            let rx1 = qx.max(min_x);
+            let ry1 = qy.max(min_y);
+            let rx2 = (qx + qw).min(max_x);
+            let ry2 = (qy + qh).min(max_y);
+            let rw = rx2 - rx1;
+            let rh = ry2 - ry1;
+            if rw > 0.0 && rh > 0.0 {
+                q.push((rx1, ry1, rw, rh, qc));
+            }
+        };
+
+        for img in &self.loaded_images {
+            let col = img.position.0;
+            let row = img.position.1;
+            
+            let screen_x = grid_origin_x + col * step_x;
+            let screen_y = grid_origin_y + row * step_y;
+            
+            let screen_w = img.size.0 * grid_size_x + (img.size.0 - 1.0).max(0.0) * skipped_col_w;
+            let screen_h = img.size.1 * grid_size_y + (img.size.1 - 1.0).max(0.0) * skipped_row_h;
+            
+            let px_w = screen_w / img.pixel_width as f32;
+            let px_h = screen_h / img.pixel_height as f32;
+            
+            for y in 0..img.pixel_height {
+                for x in 0..img.pixel_width {
+                    let idx = (y * img.pixel_width + x) as usize;
+                    let rgba = img.pixels[idx];
+                    let r = rgba[0] as f32 / 255.0;
+                    let g = rgba[1] as f32 / 255.0;
+                    let b = rgba[2] as f32 / 255.0;
+                    let a = (rgba[3] as f32 / 255.0) * self.opacity;
+                    
+                    let px_x = screen_x + (x as f32) * px_w;
+                    let px_y = screen_y + (y as f32) * px_h;
+                    
+                    push_clipped(px_x, px_y, px_w + 0.5, px_h + 0.5, [r, g, b, a], quads);
+                }
+            }
+        }
+
         quads.extend(self.graph.extra_quads());
 
         // 3. Add MenuBar background and highlights/dropdowns