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

commitbdae9dc7daf3f7b7f4b0767e88c7ee7b9764fbbe
parentfa802140b6
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-25 10:27
feat: the viewport menu's display rows in three separated groups

Framing, then wireframe (switch, thickness), points (point size, point
marker size, group marker scale) and surface (flat/smooth shading,
opacity, Show Occluded), a separator apart; the editor pin rows follow as
before. the_viewport_menu_groups_its_display_rows holds the order.

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

 CLAUDE.md   |  5 ++++-
 src/app.rs  | 63 ++++++++++++++++++++++++++++++-------------------------------
 src/main.rs | 23 ++++++++++++++++++++++
 3 files changed, 58 insertions(+), 33 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index f2e29db..2ed2cc4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1377,7 +1377,10 @@ multi-selection, so taking `Shift+L` now would have to be given back later.
 ### Display mode: the viewport menu, and smooth shading
 
 The viewport's right-click menu carries the DISPLAY MODE under Frame All
-and View 1:1: the Show Wireframe switch (its registry command), **Flat
+and View 1:1, in three groups a separator apart — wireframe (switch,
+thickness), points (the three size sliders), surface (shading, opacity,
+Show Occluded); `the_viewport_menu_groups_its_display_rows` holds the
+order. The rows: the Show Wireframe switch (its registry command), **Flat
 Shading / Smooth Shading** as a radio pair over `toggle_smooth_shading`,
 a **Wire Thickness** slider under the wireframe switch (1–8 px by
 half a pixel, the palette row's range), a **Point Size** slider (0–0.1
diff --git a/src/app.rs b/src/app.rs
index 5d4d24d..858c23f 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -4461,42 +4461,41 @@ impl State {
         true
     }
 
-    /// The viewport menu's rows and what each does: framing, then the
-    /// DISPLAY MODE — the wireframe switch and its thickness slider, the
-    /// point size, point marker size and group marker scale sliders, flat or
-    /// smooth shading as a radio pair, the polygon opacity slider and Show
-    /// Occluded — then the editor pin. Split from the open so a test can
-    /// read it. Marks are the ●/○ the pin rows and the network menu use.
+    /// The viewport menu's rows and what each does, in groups a separator
+    /// apart: framing; the WIREFRAME (its switch and thickness); the POINTS
+    /// (point size, point marker size, group marker scale); the SURFACE
+    /// (flat or smooth shading as a radio pair, the polygon opacity, Show
+    /// Occluded — the three that decide how the fill itself reads); then
+    /// the editor pin. Split from the open so a test can read it. Marks are
+    /// the ●/○ the pin rows and the network menu use.
     pub(crate) fn viewport_menu_rows(&self) -> (Vec<String>, Vec<ViewportMenuAction>) {
         let mut options = vec!["Frame All".to_string(), "View 1:1".to_string()];
         let mut actions = vec![ViewportMenuAction::FrameAll, ViewportMenuAction::OneToOne];
         let mark = |on: bool| if on { "●" } else { "○" };
-
-        options.push("-".to_string());
-        actions.push(ViewportMenuAction::Separator);
-        let wire_label = crate::command::by_id("toggle_wireframe").map(|c| c.label).unwrap_or("Show Wireframe");
-        options.push(format!("{} {wire_label}", mark(self.wireframe)));
-        actions.push(ViewportMenuAction::Command("toggle_wireframe"));
-        options.push("Wire Thickness".to_string());
-        actions.push(ViewportMenuAction::WireThicknessSlider);
-        options.push("Point Size".to_string());
-        actions.push(ViewportMenuAction::PointSizeSlider);
-        options.push("Point Marker Size".to_string());
-        actions.push(ViewportMenuAction::PointMarkerSizeSlider);
-        options.push("Group Marker Scale".to_string());
-        actions.push(ViewportMenuAction::GroupMarkerScaleSlider);
-        options.push(format!("{} Flat Shading", mark(!self.smooth_shading)));
-        actions.push(ViewportMenuAction::Shading(false));
-        options.push(format!("{} Smooth Shading", mark(self.smooth_shading)));
-        actions.push(ViewportMenuAction::Shading(true));
-
-        options.push("-".to_string());
-        actions.push(ViewportMenuAction::Separator);
-        options.push("Opacity".to_string());
-        actions.push(ViewportMenuAction::OpacitySlider);
-        let occluded_label = crate::command::by_id("toggle_show_occluded").map(|c| c.label).unwrap_or("Show Occluded");
-        options.push(format!("{} {occluded_label}", mark(self.show_occluded)));
-        actions.push(ViewportMenuAction::Command("toggle_show_occluded"));
+        let label = |id: &str, fallback: &'static str| crate::command::by_id(id).map(|c| c.label).unwrap_or(fallback);
+        let row = |options: &mut Vec<String>, actions: &mut Vec<ViewportMenuAction>, text: String, a: ViewportMenuAction| {
+            options.push(text);
+            actions.push(a);
+        };
+        let sep = ViewportMenuAction::Separator;
+
+        // Wireframe.
+        row(&mut options, &mut actions, "-".into(), sep);
+        row(&mut options, &mut actions, format!("{} {}", mark(self.wireframe), label("toggle_wireframe", "Show Wireframe")), ViewportMenuAction::Command("toggle_wireframe"));
+        row(&mut options, &mut actions, "Wire Thickness".into(), ViewportMenuAction::WireThicknessSlider);
+
+        // Points.
+        row(&mut options, &mut actions, "-".into(), sep);
+        row(&mut options, &mut actions, "Point Size".into(), ViewportMenuAction::PointSizeSlider);
+        row(&mut options, &mut actions, "Point Marker Size".into(), ViewportMenuAction::PointMarkerSizeSlider);
+        row(&mut options, &mut actions, "Group Marker Scale".into(), ViewportMenuAction::GroupMarkerScaleSlider);
+
+        // Surface.
+        row(&mut options, &mut actions, "-".into(), sep);
+        row(&mut options, &mut actions, format!("{} Flat Shading", mark(!self.smooth_shading)), ViewportMenuAction::Shading(false));
+        row(&mut options, &mut actions, format!("{} Smooth Shading", mark(self.smooth_shading)), ViewportMenuAction::Shading(true));
+        row(&mut options, &mut actions, "Opacity".into(), ViewportMenuAction::OpacitySlider);
+        row(&mut options, &mut actions, format!("{} {}", mark(self.show_occluded), label("toggle_show_occluded", "Show Occluded")), ViewportMenuAction::Command("toggle_show_occluded"));
 
         // The viewport's editor binding, as a radio group: follow the active
         // editor, or pin to one. Pin rows appear only while a second editor
diff --git a/src/main.rs b/src/main.rs
index 02cddf9..9095932 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1644,6 +1644,29 @@ mod tests {
         context_menu::hide();
     }
 
+    /// The viewport menu reads in groups a separator apart: framing, then
+    /// the wireframe, the points, and the surface (shading, opacity, Show
+    /// Occluded) — every display row in exactly one group.
+    #[test]
+    fn the_viewport_menu_groups_its_display_rows() {
+        use crate::app::ViewportMenuAction as A;
+        let state = State::new(false);
+        let (options, actions) = state.viewport_menu_rows();
+        let groups: Vec<Vec<A>> = actions
+            .split(|a| *a == A::Separator)
+            .map(|g| g.to_vec())
+            .collect();
+        assert_eq!(groups[0], vec![A::FrameAll, A::OneToOne]);
+        assert_eq!(groups[1], vec![A::Command("toggle_wireframe"), A::WireThicknessSlider]);
+        assert_eq!(groups[2], vec![A::PointSizeSlider, A::PointMarkerSizeSlider, A::GroupMarkerScaleSlider]);
+        assert_eq!(
+            groups[3],
+            vec![A::Shading(false), A::Shading(true), A::OpacitySlider, A::Command("toggle_show_occluded")]
+        );
+        assert_eq!(options.len(), actions.len());
+        assert!(options.iter().zip(&actions).all(|(o, a)| (o == "-") == (*a == A::Separator)), "separator rows line up");
+    }
+
     /// Wire Thickness is a slider row right under Show Wireframe, over the
     /// palette row's 1–8 px: the wheel steps half a pixel and saves, a press
     /// on the band jumps, and the value is the live `wire_width` the wire