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

commit7ac1bce6d6a8a88b2cf453881e1bdd0b6d32442d
parent99321f4d16
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-28 15:17
fix: the dialog's spin rows land as draw-time values too

Grid Thickness, Origin Size, Point Marker Size and Camera Pivot Size were
the four dialog rows still going through `apply_setting` on every motion
of a drag — a graph evaluation and a state.kdl write per pointer event —
when each of them feeds exactly one small guide mesh (the grid, the three
axes, the overlay markers, the pivot).

`land_draw_time_setting` gains the grid and origin arms, and
`land_dialog_slider` lands a spin row's whole number over the row's unit
through it; the Camera Pivot Size row, whose owner is the active camera,
writes as `setting_write` always has and re-bakes the pivot. Same
save-on-release contract as the slider rows. The drag test covers all
four: the field, the re-baked mesh, an unmoved geometry version, and the
file written on a single landing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

 CLAUDE.md     |  5 +++--
 src/app.rs    | 18 +++++++++++++++---
 src/dialog.rs | 50 +++++++++++++++++++++++++++++++-------------------
 src/main.rs   | 25 ++++++++++++++++++++++---
 4 files changed, 71 insertions(+), 27 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 51dc5c5..6359777 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1933,8 +1933,9 @@ selection stays where it was:
   params pane's pickers), a path-tracer restart and a synchronous file
   write, per pointer event, for six values the graph never reads — which
   is what made the dialog's sliders drag behind the pointer while the
-  menu's did not. A row the landing does not know (a spin row) still
-  takes the full apply, whose regenerate it needs;
+  menu's did not. The spin rows (Grid Thickness, Origin Size, Point
+  Marker Size, Camera Pivot Size) land the same way, their whole number
+  over the row's unit, each re-baking only the guide mesh that reads it;
   `a_dialog_slider_drag_lands_without_re_evaluating_the_graph` pins all
   of it. The **zoom row**
   (`ZOOM_ROW_ID`, only while the network pane is focused, since zoom is that
diff --git a/src/app.rs b/src/app.rs
index 9c2b59c..4a4919e 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -4791,9 +4791,11 @@ impl State {
 
     /// Land a DRAW-TIME display setting by its `DesignSettings` field key —
     /// the one landing behind the viewport menu's sliders AND the dialog's
-    /// (`land_dialog_slider`), so the two cannot disagree about a clamp or
-    /// about which marker mesh a size feeds. Returns false for a key that is
-    /// not one of these, which the dialog takes as "run the full apply".
+    /// slider and spin rows (`land_dialog_slider`), so the two cannot
+    /// disagree about a clamp or about which mesh a size feeds. The three
+    /// guide sizes re-bake their own small mesh (a grid, three axes, a
+    /// pivot) and nothing else. Returns false for a key that is not one of
+    /// these, which the dialog takes as "run the full apply".
     ///
     /// Until 2026-09-28 the dialog's sliders went through `apply_setting` on
     /// every motion of a drag: a whole graph evaluation, a second one for
@@ -4822,6 +4824,16 @@ impl State {
                 self.pull_arrow_scale = v.clamp(0.25, 10.0);
                 self.rebuild_pull_arrow_verts();
             }
+            // The spin rows' ranges, in world units (the rows read in
+            // thousandths and tenths).
+            "grid_thickness" => {
+                self.grid_thickness = v.clamp(0.002, 0.2);
+                self.update_grid_geometry();
+            }
+            "origin_size" => {
+                self.origin_size = v.clamp(0.1, 5.0);
+                self.update_origin_geometry();
+            }
             _ => return false,
         }
         self.viewport_dirty = true;
diff --git a/src/dialog.rs b/src/dialog.rs
index 9ae7c0d..5a6f548 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -2082,30 +2082,42 @@ impl State {
     /// A slider row's value arriving from a drag, a wheel or an arrow: the
     /// zoom row zooms, a setting row writes its setting.
     ///
-    /// A slider row is a draw-time value, so it lands through the viewport
-    /// menu's own `land_draw_time_setting` — the field, the one mesh it
-    /// feeds, a redraw — and the row re-reads in place. During a drag this
-    /// runs on every motion, so state.kdl is written on the RELEASE
-    /// (`dialog_mouse_input`) rather than here; a wheel notch or an arrow
-    /// key is a single landing and saves at once, as the menu's wheel does.
-    /// A row the landing does not know falls through to `apply_setting`,
-    /// whose full regenerate pass is what a spin row like Grid Thickness
-    /// needs.
+    /// A slider or spin row is a draw-time value, so it lands through the
+    /// viewport menu's own `land_draw_time_setting` — the field, the one
+    /// mesh it feeds, a redraw — and the row re-reads in place. During a
+    /// drag this runs on every motion, so state.kdl is written on the
+    /// RELEASE (`dialog_mouse_input`) rather than here; a wheel notch or an
+    /// arrow key is a single landing and saves at once, as the menu's wheel
+    /// does. A spin row lands its whole number over the row's unit; the
+    /// Camera Pivot Size row, whose owner is the active camera, writes as
+    /// `setting_write` always has and re-bakes the pivot. A row the landing
+    /// does not know falls through to `apply_setting`.
     pub(crate) fn land_dialog_slider(&mut self, id: &str, v: f32) {
         if id == ZOOM_ROW_ID {
             self.set_zoom_percent(v);
         } else if let Some(s) = setting_of_row(id) {
-            if let (Owner::Field(key), Ctl::Slider { dec, .. }) = (s.owner, s.ctl) {
-                // Rounded as the row shows it, so the field holds the value
-                // the readout names rather than the pointer's raw fraction.
-                let shown: f32 = format!("{:.*}", dec, v).parse().unwrap_or(v);
-                if self.land_draw_time_setting(key, shown) {
-                    self.refresh_dialog_controls();
-                    if !self.slots.dialog.slider_dragging() {
-                        self.save_settings();
-                    }
-                    return;
+            let landed = match (s.owner, s.ctl) {
+                (Owner::Field(key), Ctl::Slider { dec, .. }) => {
+                    // Rounded as the row shows it, so the field holds the
+                    // value the readout names rather than the pointer's raw
+                    // fraction.
+                    let shown: f32 = format!("{:.*}", dec, v).parse().unwrap_or(v);
+                    self.land_draw_time_setting(key, shown)
+                }
+                (Owner::Field(key), Ctl::Spin { unit, .. }) => self.land_draw_time_setting(key, v.round() / unit),
+                (Owner::ActiveCamera(_), Ctl::Spin { .. }) => {
+                    self.setting_write(s, &(v.round() as i64).to_string());
+                    self.update_pivot_geometry();
+                    true
                 }
+                _ => false,
+            };
+            if landed {
+                self.refresh_dialog_controls();
+                if !self.slots.dialog.slider_dragging() {
+                    self.save_settings();
+                }
+                return;
             }
             let value = match s.ctl {
                 Ctl::Slider { dec, .. } => format!("{:.*}", dec, v),
diff --git a/src/main.rs b/src/main.rs
index 78814cc..8ca38c7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10964,14 +10964,33 @@ mod tests {
         assert!((state.geo_opacity - 1.0).abs() < 1e-6, "clamped");
         assert_eq!(state.rt_geometry_version, version);
 
-        // Point Size re-sizes the group markers from their kept members,
-        // and a spin row still takes the full regenerate pass.
+        // Point Size re-sizes the group markers from their kept members.
         state.land_dialog_slider(&setting_row_id("Point Size"), 0.05);
         assert!((state.point_size - 0.05).abs() < 1e-6);
         assert!((state.last_group_marker_size - state.group_marker_size()).abs() < 1e-6);
         assert_eq!(state.rt_geometry_version, version);
+
+        // The spin rows land the same way: a whole number over the row's
+        // unit, re-baking only the guide mesh that reads it.
+        state.pending_grid = None;
+        state.pending_origin = None;
+        state.pending_pivot = None;
         state.land_dialog_slider(&setting_row_id("Grid Thickness"), 40.0);
-        assert!(state.rt_geometry_version > version, "a spin row regenerates");
+        assert!((state.grid_thickness - 0.04).abs() < 1e-6, "{}", state.grid_thickness);
+        assert!(state.pending_grid.is_some(), "the grid re-baked");
+        state.land_dialog_slider(&setting_row_id("Origin Size"), 25.0);
+        assert!((state.origin_size - 2.5).abs() < 1e-6, "{}", state.origin_size);
+        assert!(state.pending_origin.is_some(), "the origin re-baked");
+        state.land_dialog_slider(&setting_row_id("Point Marker Size"), 50.0);
+        assert!((state.point_marker_size - 0.05).abs() < 1e-6, "{}", state.point_marker_size);
+        state.land_dialog_slider(&setting_row_id("Camera Pivot Size"), 20.0);
+        assert!((state.camera_pivot_size - 2.0).abs() < 1e-6, "{}", state.camera_pivot_size);
+        assert!(state.pending_pivot.is_some(), "the pivot re-baked");
+        assert_eq!(state.rt_geometry_version, version, "a spin row re-evaluated the graph");
+        assert!((saved(&path) - 1.0).abs() < 1e-3, "the file follows every single landing");
+        let kdl = fs::read_to_string(&path).expect("a settings file");
+        let back = crate::app::DesignSettings::from_kdl_str(&kdl).viewport;
+        assert!((back.grid_thickness - 0.04).abs() < 1e-6 && (back.origin_size - 2.5).abs() < 1e-6, "{kdl}");
     }
 
     /// A right press is the dialog's while it is open: inside the plate it is