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