git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commit1af58bd92445c8be98ac036e301ab33e7d171da2
parent183d064097
authorLucas Galante <[email protected]>
date2026-07-18 16:14
refactor: the graph grid's geometry is config-owned, not saved state

The node grid's cell size and gaps were written to state.kdl, so the app owned
values a user would want to set: zoom scaled them and then persisted whatever it
landed on. They now come from config (`style.surface.graph.spacing_*`/`gap_*`),
which the app reads and never writes back — the split ../CLAUDE.md describes for
scroll behavior. GraphSettings is gone from DesignSettings; a state file still
carrying the old block loads fine and drops it on the next save.

This also makes the designer honor `spacing_x`/`spacing_y`, which it previously
read past by overwriting the grid from its own state.

Reset Zoom returns to the configured values rather than the hardcoded
150/75/37.5 — otherwise the first Reset would have wiped the configured baseline
that zoom scales from.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

 src/app.rs    | 67 +++++++++++++++++++++--------------------------------------
 src/main.rs   | 30 ++++++++------------------
 src/window.rs |  9 ++++----
 3 files changed, 38 insertions(+), 68 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 0c1a644..6ef7888 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -496,38 +496,26 @@ impl Default for ViewportSettings {
     }
 }
 
-/// The node grid's cell size and the gap between cells. The step from one node slot to the
-/// next is the two added up — there is no separate key for it.
+/// The node grid's cell size and the gap between cells, as configured — `spacing_*` is the
+/// cell, `gap_*` the space after it, and one node slot to the next is the two added up.
 ///
-/// The gaps were `skipped_row_h`/`skipped_col_w`; the aliases keep state.kdl files written
-/// under the old names loading, and the next save rewrites them.
-#[derive(Serialize, Deserialize, Clone, Debug)]
-pub struct GraphSettings {
-    pub grid_size_x: f32,
-    pub grid_size_y: f32,
-    #[serde(alias = "skipped_row_h")]
-    pub gap_row_h: f32,
-    #[serde(alias = "skipped_col_w")]
-    pub gap_col_w: f32,
-}
-
-impl Default for GraphSettings {
-    fn default() -> Self {
-        Self {
-            grid_size_x: 80.0,
-            grid_size_y: 40.0,
-            gap_row_h: 20.0,
-            gap_col_w: 20.0,
-        }
-    }
+/// Config-owned (`style.surface.graph.*`), NOT state: it is user-authored, so the app reads
+/// it and never writes it back — the same split `../CLAUDE.md` describes for scroll
+/// behavior. Zoom scales these in memory; the configured values are the 100% baseline that
+/// Reset Zoom returns to.
+pub fn configured_grid_geometry() -> (f32, f32, f32, f32) {
+    (
+        cce_ui::layout::graph_spacing_x(),
+        cce_ui::layout::graph_spacing_y(),
+        cce_ui::layout::graph_gap_col_w(),
+        cce_ui::layout::graph_gap_row_h(),
+    )
 }
 
 #[derive(Serialize, Deserialize, Clone, Debug, Default)]
 pub struct DesignSettings {
     #[serde(default)]
     pub viewport: ViewportSettings,
-    #[serde(default)]
-    pub graph: GraphSettings,
 }
 
 fn float_array_to_hex(rgb: &[f32; 3]) -> String {
@@ -1200,12 +1188,6 @@ impl State {
                 grid_thickness: self.grid_thickness,
                 grid_color: self.viewport().grid_color,
             },
-            graph: GraphSettings {
-                grid_size_x: self.grid_size_x,
-                grid_size_y: self.grid_size_y,
-                gap_row_h: self.gap_row_h,
-                gap_col_w: self.gap_col_w,
-            },
         };
         settings.save();
         self.last_design_mod_time = {
@@ -1614,10 +1596,11 @@ impl State {
                 self.zoom(1.0 / 1.15, None);
             }
             "Reset Zoom" => {
-                self.grid_size_x = 150.0;
-                self.grid_size_y = 75.0;
-                self.gap_col_w = 37.5;
-                self.gap_row_h = 37.5;
+                let (gx, gy, gw, gh) = configured_grid_geometry();
+                self.grid_size_x = gx;
+                self.grid_size_y = gy;
+                self.gap_col_w = gw;
+                self.gap_row_h = gh;
                 self.sync_grid_settings();
             }
             "Detach Circular Window" | "Detach Pane" => {
@@ -2460,6 +2443,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
         // The engine detected the output scale before constructing the app.
         let scale = cce_ui::scale::scale_factor() as f64;
         let settings = DesignSettings::load();
+        // Grid geometry is config-owned, not part of the saved state.
+        let cfg_grid = configured_grid_geometry();
         let (lw, lh) = if is_detached_network {
             (400.0f32, 400.0f32)
         } else {
@@ -2640,10 +2625,10 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             square_viewport: settings.viewport.square,
             grid_snap_enabled: true,
             network_grid_visible: true,
-            grid_size_x: settings.graph.grid_size_x,
-            grid_size_y: settings.graph.grid_size_y,
-            gap_row_h: settings.graph.gap_row_h,
-            gap_col_w: settings.graph.gap_col_w,
+            grid_size_x: cfg_grid.0,
+            grid_size_y: cfg_grid.1,
+            gap_col_w: cfg_grid.2,
+            gap_row_h: cfg_grid.3,
             pan_x,
             pan_y,
             pan_velocity_x: 0.0,
@@ -4806,10 +4791,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                         self.last_design_mod_time = Some(mod_time);
                          let settings = DesignSettings::load();
                          self.square_viewport = settings.viewport.square;
-                         self.grid_size_x = settings.graph.grid_size_x;
-                         self.grid_size_y = settings.graph.grid_size_y;
-                         self.gap_row_h = settings.graph.gap_row_h;
-                         self.gap_col_w = settings.graph.gap_col_w;
                          self.grid_thickness = settings.viewport.grid_thickness;
                          self.viewport_mut().show_grid = settings.viewport.show_grid_enabled;
                          self.viewport_mut().show_cube = settings.viewport.show_cube_enabled;
diff --git a/src/main.rs b/src/main.rs
index 0497c1a..81a26a1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -444,27 +444,15 @@ mod tests {
         assert_eq!(settings.viewport.camera_pivot_size, 1.0);
         assert_eq!(settings.viewport.grid_color, [0.35, 0.35, 0.40]);
         
-        assert_eq!(settings.graph.gap_row_h, 20.0);
-        assert_eq!(settings.graph.gap_col_w, 20.0);
-
-        // A state.kdl written before the rename still loads: the gaps were skipped_row_h /
-        // skipped_col_w, and the aliases carry those onto the new fields.
-        let legacy = r#"
-        {
-            "graph": {
-                "grid_size_x": 71.0,
-                "grid_size_y": 31.0,
-                "skipped_row_h": 12.0,
-                "skipped_col_w": 14.0
-            }
-        }
-        "#;
-        let migrated: DesignSettings = serde_json::from_str(legacy).unwrap();
-        assert_eq!(migrated.graph.gap_row_h, 12.0);
-        assert_eq!(migrated.graph.gap_col_w, 14.0);
-        // ...and saving writes the new names back out.
-        let rewritten = serde_json::to_string(&migrated).unwrap();
-        assert!(rewritten.contains("gap_row_h") && !rewritten.contains("skipped_row_h"));
+        // Grid geometry is config-owned now: a state file still carrying the old graph block
+        // loads fine (the key is simply ignored) and never comes back out on save.
+        let mut with_stale_graph: serde_json::Value =
+            serde_json::from_str(&serde_json::to_string(&settings).unwrap()).unwrap();
+        with_stale_graph["graph"] = serde_json::json!({ "grid_size_x": 71.0, "gap_col_w": 14.0 });
+        let stale: DesignSettings = serde_json::from_value(with_stale_graph).unwrap();
+        assert_eq!(stale.viewport.grid_color, [0.35, 0.35, 0.40]);
+        let rewritten = serde_json::to_string(&stale).unwrap();
+        assert!(!rewritten.contains("graph"), "state must not carry graph settings: {rewritten}");
 
         let serialized = serde_json::to_string(&settings).unwrap();
         let settings_roundtrip: DesignSettings = serde_json::from_str(&serialized).unwrap();
diff --git a/src/window.rs b/src/window.rs
index 9052872..c2b592a 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -170,10 +170,11 @@ impl State {
                             changed = true;
                         }
                         2 => { // Reset Zoom
-                            state.grid_size_x = 150.0;
-                            state.grid_size_y = 75.0;
-                            state.gap_col_w = 37.5;
-                            state.gap_row_h = 37.5;
+                            let (gx, gy, gw, gh) = crate::app::configured_grid_geometry();
+                            state.grid_size_x = gx;
+                            state.grid_size_y = gy;
+                            state.gap_col_w = gw;
+                            state.gap_row_h = gh;
                             state.sync_grid_settings();
                             changed = true;
                         }