graphic design tool
git clone https://git.lucas.co/cce-designer.git
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