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

commit89bc11628f03e2798ca05c5fec985e40dc608db3
parentfefc03964d
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-06 17:34
fix(network): a config edit to the grid spacing applies at once

The live grid geometry was read from config.kdl at startup and only
zoomed after, so changing spacing_x/spacing_y or the node size waited
for Reset Zoom or a restart. The config reload now re-applies a changed
configured geometry at the zoom in hand (State::grid_base).

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

 CLAUDE.md   |  9 +++++++--
 src/app.rs  | 25 ++++++++++++++++++++++++-
 src/main.rs | 23 +++++++++++++++++++++++
 3 files changed, 54 insertions(+), 3 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index b067163..cb7d9c2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2992,8 +2992,13 @@ edit reached the lattice.
 
 `State::grid_pitch_x` / `grid_pitch_y` and `node_w` / `node_h` are the
 zoomed geometry — `configured_grid_geometry` at 100%, scaled TOGETHER by
-`scale_grid_geometry`, which is the only relation between them; there is no
-other grid geometry on `State`. `cell_center`, `cell_rect` and `cell_at` are
+`scale_grid_geometry`, which is the only relation between them. **A
+config.kdl edit to the spacing or node size shows at once** (since
+2026-10-06): `State::grid_base` is the configured geometry the live one is
+a zoom of, and the config reload (`update_graph_settings_from_config`)
+re-applies a changed one at the zoom in hand — until then the live
+geometry was read at startup and only zoomed after, so an edit waited for
+Reset Zoom or a restart (`a_grid_spacing_edit_applies_at_the_zoom_in_hand`). `cell_center`, `cell_rect` and `cell_at` are
 the three derivations every consumer goes through — the cursor outline, the
 click-to-cell of an empty-space press (`round`, not `floor`, because a cell
 is centred on its crossing and a click between two nodes belongs to the
diff --git a/src/app.rs b/src/app.rs
index c132260..85a74e0 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -2358,6 +2358,7 @@ impl Default for ViewportSettings {
 /// never writes it back — the same split `../CLAUDE.md` describes for scroll
 /// behavior. Zoom scales all four in memory together; the configured values
 /// are the 100% baseline that Reset Zoom returns to.
+#[derive(Clone, Copy, Debug, PartialEq)]
 pub struct GridGeometry {
     pub pitch_x: f32,
     pub pitch_y: f32,
@@ -3070,6 +3071,11 @@ pub struct State {
     /// The network grid's pitch at the current zoom — centre of one grid
     /// line to the centre of the next, per axis. The grid's one size; a
     /// node's (col, row) is the intersection its centre sits on.
+    /// The configured grid geometry (`configured_grid_geometry`) the live
+    /// one is a zoom of — the 100% baseline as it was last read. A config
+    /// reload that moves it re-applies the grid at the zoom in hand
+    /// (`update_graph_settings_from_config`).
+    pub grid_base: GridGeometry,
     pub grid_pitch_x: f32,
     pub grid_pitch_y: f32,
     /// The node body's size at the current zoom — its own, not the pitch's;
@@ -8446,6 +8452,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             square_viewport: settings.viewport.square,
             grid_snap_enabled: true,
             network_grid_visible: true,
+            grid_base: cfg_grid,
             grid_pitch_x: cfg_grid.pitch_x,
             grid_pitch_y: cfg_grid.pitch_y,
             node_w: cfg_grid.node_w,
@@ -8969,7 +8976,23 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             self.grid_snap_enabled = snap_enabled;
             changed = true;
         }
-        
+        // The grid's spacing and node size: re-applied at the zoom in hand,
+        // so a config.kdl edit to `spacing_y` shows at once. Until
+        // 2026-10-06 the live geometry was set at startup and only zoomed
+        // after it, so an edit waited for Reset Zoom or a restart.
+        let base = configured_grid_geometry();
+        if base != self.grid_base {
+            let zoom = if self.grid_base.pitch_x > 0.0 { self.grid_pitch_x / self.grid_base.pitch_x } else { 1.0 };
+            self.set_grid_geometry(GridGeometry {
+                pitch_x: base.pitch_x * zoom,
+                pitch_y: base.pitch_y * zoom,
+                node_w: base.node_w * zoom,
+                node_h: base.node_h * zoom,
+            });
+            self.grid_base = base;
+            changed = true;
+        }
+
         if changed {
             self.sync_grid_settings();
             self.viewport_dirty = true;
diff --git a/src/main.rs b/src/main.rs
index ad4c613..2945ebc 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16142,6 +16142,29 @@ mod tests {
     /// selection with it, rigidly, and the region travels too. Dragging a node
     /// OUTSIDE the selection is the ordinary one-node drag, and collapses the
     /// selection onto what was grabbed.
+    /// A config.kdl edit to the grid's spacing shows at once, at the zoom
+    /// in hand: the reload re-applies the configured geometry scaled as the
+    /// live one was. It used to be read at startup and only zoomed after.
+    #[test]
+    fn a_grid_spacing_edit_applies_at_the_zoom_in_hand() {
+        use crate::app::{configured_grid_geometry, GridGeometry};
+        let mut state = State::new(false);
+        let cfg = configured_grid_geometry();
+        assert_eq!(state.grid_base, cfg);
+        // As if the config had said 90 rows apart, and the view is at 150%.
+        state.grid_base = GridGeometry { pitch_y: 90.0, ..cfg };
+        state.set_grid_geometry(GridGeometry { pitch_x: cfg.pitch_x * 1.5, pitch_y: 135.0, node_w: cfg.node_w * 1.5, node_h: cfg.node_h * 1.5 });
+        // The file now says what `cfg` says.
+        state.update_graph_settings_from_config();
+        assert_eq!(state.grid_base, cfg);
+        assert_eq!(state.grid_pitch_y, cfg.pitch_y * 1.5, "the new spacing, at the same 150%");
+        assert_eq!(state.grid_pitch_x, cfg.pitch_x * 1.5);
+        assert_eq!((state.node_w, state.node_h), (cfg.node_w * 1.5, cfg.node_h * 1.5));
+        // A reload that changes nothing leaves the zoom alone.
+        state.update_graph_settings_from_config();
+        assert_eq!(state.grid_pitch_x, cfg.pitch_x * 1.5);
+    }
+
     /// A node dropped on another node swaps places with it, connections
     /// and all: in sphere1 → a → b → c, dragging b onto a leaves b where a
     /// was and a where b was, wired sphere1 → b → a → c. One undo puts both