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

commit5c684a003aacd9bbf6958682fd19d9760475a562
parentdeddd47912
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-25 10:47
feat: a Show Point Normals switch in the viewport menu's points group

The existing toggle_point_normals command as a marked row after Show Point
Numbers, with the other point overlays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md   |  3 ++-
 src/app.rs  |  4 +++-
 src/main.rs | 28 +++++++++++++++++++++++++++-
 3 files changed, 32 insertions(+), 3 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index c26cda1..ab36e5d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1379,7 +1379,8 @@ multi-selection, so taking `Shift+L` now would have to be given back later.
 The viewport's right-click menu carries the DISPLAY MODE under Frame All
 and View 1:1, in three groups a separator apart — wireframe (switch,
 thickness), points (Show Points and Point Size, Show Point Markers and
-its size, Show Point Numbers, Group Marker Scale — each switch over its
+its size, Show Point Numbers, Show Point Normals, Group Marker Scale —
+each switch over its
 own size where it has one), surface
 (shading, opacity,
 Show Occluded); `the_viewport_menu_groups_its_display_rows` holds the
diff --git a/src/app.rs b/src/app.rs
index 1515fe8..d3ac1fa 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -4464,7 +4464,8 @@ impl State {
     /// The viewport menu's rows and what each does, in groups a separator
     /// apart: framing; the WIREFRAME (its switch and thickness); the POINTS
     /// (the Show Points switch and point size, the Show Point Markers switch
-    /// and its size, the Show Point Numbers switch, group marker scale —
+    /// and its size, the Show Point Numbers and Show Point Normals
+    /// switches, group marker scale —
     /// each switch over its own size, where it has one);
     /// the SURFACE
     /// (flat or smooth shading as a radio pair, the polygon opacity, Show
@@ -4494,6 +4495,7 @@ impl State {
         row(&mut options, &mut actions, format!("{} {}", mark(self.show_point_markers), label("toggle_point_markers", "Show Point Markers")), ViewportMenuAction::Command("toggle_point_markers"));
         row(&mut options, &mut actions, "Point Marker Size".into(), ViewportMenuAction::PointMarkerSizeSlider);
         row(&mut options, &mut actions, format!("{} {}", mark(self.show_point_numbers), label("toggle_point_numbers", "Show Point Numbers")), ViewportMenuAction::Command("toggle_point_numbers"));
+        row(&mut options, &mut actions, format!("{} {}", mark(self.show_point_normals), label("toggle_point_normals", "Show Point Normals")), ViewportMenuAction::Command("toggle_point_normals"));
         row(&mut options, &mut actions, "Group Marker Scale".into(), ViewportMenuAction::GroupMarkerScaleSlider);
 
         // Surface.
diff --git a/src/main.rs b/src/main.rs
index dfb32e3..ce89240 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1666,6 +1666,7 @@ mod tests {
                 A::Command("toggle_point_markers"),
                 A::PointMarkerSizeSlider,
                 A::Command("toggle_point_numbers"),
+                A::Command("toggle_point_normals"),
                 A::GroupMarkerScaleSlider,
             ]
         );
@@ -1677,6 +1678,31 @@ mod tests {
         assert!(options.iter().zip(&actions).all(|(o, a)| (o == "-") == (*a == A::Separator)), "separator rows line up");
     }
 
+    /// Show Point Normals is a switch in the Points group after the
+    /// numbers, marked from the live flag; the row runs the command, which
+    /// collects the whiskers with the scene.
+    #[test]
+    fn the_viewport_menu_toggles_show_point_normals() {
+        use crate::app::ViewportMenuAction as A;
+        let mut state = State::new(false);
+        state.show_point_normals = false;
+        state.rebuild_scene_geometry();
+        let whiskers = state.overlay_normal_verts.len();
+        let row = |state: &State| {
+            let (options, actions) = state.viewport_menu_rows();
+            let i = actions.iter().position(|a| *a == A::Command("toggle_point_normals")).expect("a Show Point Normals row");
+            options[i].clone()
+        };
+        let label = crate::command::by_id("toggle_point_normals").unwrap().label;
+        assert_eq!(row(&state), format!("○ {label}"));
+        state.run_viewport_menu_action(A::Command("toggle_point_normals"));
+        assert!(state.show_point_normals);
+        assert_eq!(row(&state), format!("● {label}"));
+        assert!(state.overlay_normal_verts.len() > whiskers, "the whiskers were collected");
+        let kdl = fs::read_to_string(crate::app::DesignSettings::file_path()).expect("saved");
+        assert!(crate::app::DesignSettings::from_kdl_str(&kdl).viewport.show_point_normals, "persisted");
+    }
+
     /// Show Point Numbers is a switch in the Points group after the
     /// markers' size, marked from the live flag; the row runs the command,
     /// which collects the labels with the scene.
@@ -1905,7 +1931,7 @@ mod tests {
         state.cursor_y = 200.0;
         state.open_viewport_context_menu();
         let i = state.viewport_menu_actions.iter().position(|a| *a == A::GroupMarkerScaleSlider).expect("a Group Marker Scale row");
-        assert_eq!(state.viewport_menu_actions[i - 1], A::Command("toggle_point_numbers"));
+        assert_eq!(state.viewport_menu_actions[i - 1], A::Command("toggle_point_normals"));
         let sl = context_menu::slider(i).expect("a slider");
         assert_eq!((sl.min, sl.max, sl.step, sl.suffix), (0.5, 4.0, 0.05, "x"));
         assert_eq!(sl.readout(), "1.25x");