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

commit4e920844728b7d89de2a3e4b41fd788fff12d088
parent47cfa94fd3
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-29 18:01
feat: a Camera Pivot Size slider in the viewport menu

Under Show Camera Pivot, over the palette row's 0.1-5 by a tenth. It
lands through the row's own write and re-bakes the marker alone.

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

 CLAUDE.md     |  3 ++-
 src/app.rs    | 21 ++++++++++++++++++++-
 src/dialog.rs | 19 +++++++++++++++++++
 src/main.rs   | 37 ++++++++++++++++++++++++++++++++++++-
 4 files changed, 77 insertions(+), 3 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 2d12c94..221c5a4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2180,7 +2180,8 @@ multi-selection, so taking `Shift+L` now would have to be given back later.
 The viewport's right-click menu has two SUBMENUS (since 2026-09-29;
 until then it was one list of some twenty rows). The menu itself holds
 what is done — Frame All, View 1:1 — the guides (Show Grid, Show Origin,
-and since 2026-09-29 Show Camera Pivot, which until then was in the Guides
+and since 2026-09-29 Show Camera Pivot with a Camera Pivot Size slider
+under it, 0.1–5 by a tenth, which until then were in the Guides
 menubar and the palette only;
 the reference CUBE guide was removed on 2026-09-25 — its command, mesh,
 RT-scene copy, settings field and menubar item, with the Guides menubar
diff --git a/src/app.rs b/src/app.rs
index 8fb05e2..c097fb0 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -480,6 +480,9 @@ pub enum ViewportMenuAction {
     /// Pull Arrow Scale, the pull arrows' length as a multiple of the true
     /// displacement: the palette row's 0.25–10.
     PullArrowScaleSlider,
+    /// Camera Pivot Size, the pivot marker's scale, under its switch: the
+    /// palette row's 0.1–5 by a tenth.
+    CameraPivotSizeSlider,
     /// A "-" row: engraved, inert.
     Separator,
 }
@@ -5150,6 +5153,14 @@ impl State {
             // plain x rather than a multiplication sign, which the menu face
             // may not carry and which a fallback glyph would then
             // under-measure.
+            ViewportMenuAction::CameraPivotSizeSlider => MenuSlider {
+                value: self.camera_pivot_size_shown().clamp(0.1, 5.0),
+                min: 0.1,
+                max: 5.0,
+                step: 0.1,
+                decimals: 1,
+                suffix: "x",
+            },
             ViewportMenuAction::PullArrowScaleSlider => MenuSlider {
                 value: self.pull_arrow_scale.clamp(0.25, 10.0),
                 min: 0.25,
@@ -5171,6 +5182,12 @@ impl State {
     /// graph.
     fn land_viewport_menu_slider(&mut self, action: ViewportMenuAction, v: f32) {
         // The menu's opacity rows are in percent; the field is a fraction.
+        if action == ViewportMenuAction::CameraPivotSizeSlider {
+            // Owned by the active camera, as the palette's row is.
+            self.set_camera_pivot_size(v.clamp(0.1, 5.0));
+            self.viewport_dirty = true;
+            return;
+        }
         let (key, v) = match action {
             ViewportMenuAction::OpacitySlider => ("geo_opacity", v / 100.0),
             ViewportMenuAction::WireThicknessSlider => ("wire_width", v),
@@ -5374,6 +5391,7 @@ impl State {
         toggle(&mut options, &mut actions, "toggle_grid");
         toggle(&mut options, &mut actions, "toggle_origin");
         toggle(&mut options, &mut actions, "toggle_camera_pivot");
+        row(&mut options, &mut actions, "Camera Pivot Size".into(), ViewportMenuAction::CameraPivotSizeSlider);
 
         // The display settings, a submenu each; the menu marks the rows.
         row(&mut options, &mut actions, "-".into(), sep);
@@ -5442,7 +5460,8 @@ impl State {
             | ViewportMenuAction::WireOpacitySlider
             | ViewportMenuAction::PointMarkerSizeSlider
             | ViewportMenuAction::GroupMarkerSizeSlider
-            | ViewportMenuAction::PullArrowScaleSlider => {}
+            | ViewportMenuAction::PullArrowScaleSlider
+            | ViewportMenuAction::CameraPivotSizeSlider => {}
             ViewportMenuAction::Separator => {}
         }
     }
diff --git a/src/dialog.rs b/src/dialog.rs
index 9d55981..559241b 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -1860,6 +1860,25 @@ impl State {
         }
     }
 
+    /// The pivot marker's scale as the Camera Pivot Size row reads it: the
+    /// active camera's param where it has one, else the live field.
+    pub(crate) fn camera_pivot_size_shown(&self) -> f32 {
+        SETTINGS
+            .iter()
+            .find(|s| s.label == "Camera Pivot Size")
+            .and_then(|s| self.setting_value(s).parse::<f32>().ok())
+            .map_or(self.camera_pivot_size, |tenths| tenths / 10.0)
+    }
+
+    /// Set it, through the row's own write, and re-bake the marker. The
+    /// viewport menu's slider lands here; nothing is evaluated.
+    pub(crate) fn set_camera_pivot_size(&mut self, v: f32) {
+        if let Some(s) = SETTINGS.iter().find(|s| s.label == "Camera Pivot Size") {
+            self.setting_write(s, &((v * 10.0).round() as i64).to_string());
+            self.update_pivot_geometry();
+        }
+    }
+
     /// Write one setting's value to whatever owns it.
     fn setting_write(&mut self, s: &Setting, value: &str) {
         match s.owner {
diff --git a/src/main.rs b/src/main.rs
index 31babdb..834fa6d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2072,7 +2072,7 @@ mod tests {
             groups(None),
             vec![
                 vec![A::FrameAll, A::OneToOne],
-                vec![A::Command("toggle_grid"), A::Command("toggle_origin"), A::Command("toggle_camera_pivot")],
+                vec![A::Command("toggle_grid"), A::Command("toggle_origin"), A::Command("toggle_camera_pivot"), A::CameraPivotSizeSlider],
                 vec![A::Submenu(P::Style), A::Submenu(P::Markers)],
             ]
         );
@@ -2359,6 +2359,41 @@ mod tests {
         assert!(crate::app::DesignSettings::from_kdl_str(&kdl).viewport.show_camera_pivot_enabled, "persisted");
     }
 
+    /// Camera Pivot Size is a slider under Show Camera Pivot, over the
+    /// palette row's 0.1–5: the wheel steps a tenth, re-bakes the marker
+    /// and nothing else, and saves.
+    #[test]
+    fn the_viewport_menu_sets_the_camera_pivot_size() {
+        use crate::app::ViewportMenuAction as A;
+        use crate::window::WindowEvent;
+        use cce_ui::widget::{context_menu, MouseScrollDelta};
+        let mut state = State::new(false);
+        state.camera_pivot_size = 1.0;
+        state.pending_pivot = None;
+        let version = state.rt_geometry_version;
+        state.cursor_x = 300.0;
+        state.cursor_y = 200.0;
+        state.open_viewport_context_menu();
+        let actions = state.viewport_menu_actions.clone();
+        let i = actions.iter().position(|a| *a == A::CameraPivotSizeSlider).expect("a Camera Pivot Size row");
+        assert_eq!(actions[i - 1], A::Command("toggle_camera_pivot"), "it sits under its switch");
+        let sl = context_menu::slider(i).expect("a slider");
+        assert_eq!((sl.min, sl.max, sl.step, sl.decimals), (0.1, 5.0, 0.1, 1));
+        assert!((sl.value - 1.0).abs() < 1e-6);
+
+        state.cursor_x = context_menu::x() + 20.0;
+        state.cursor_y = context_menu::row_y(i) + context_menu::ROW_H * 0.5;
+        state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(0.0, 5.0) });
+        assert!((state.camera_pivot_size - 1.5).abs() < 1e-5, "{}", state.camera_pivot_size);
+        assert!(state.pending_pivot.is_some(), "the marker re-bakes");
+        assert_eq!(state.rt_geometry_version, version, "and the graph is not evaluated");
+        assert!(context_menu::is_visible(), "a slider row keeps the menu up");
+        let kdl = fs::read_to_string(crate::app::DesignSettings::file_path()).expect("saved");
+        let saved = crate::app::DesignSettings::from_kdl_str(&kdl).viewport.camera_pivot_size;
+        assert!((saved - 1.5).abs() < 1e-5, "persisted: {saved}");
+        context_menu::hide();
+    }
+
     /// Show Point Markers is a switch in the Points group, over its own
     /// size slider, marked from the live flag; the row runs the command,
     /// which rebuilds the overlay (and keeps the positions the size slider