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