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

commit708d58b054503036677796efefb406bf07fbe5a8
parent2d883e161c e960f0cfee
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-29 16:42
Merge remote-tracking branch 'origin/main' into claude/2d-features

 CLAUDE.md       |  77 ++++++++++++--
 src/app.rs      | 307 +++++++++++++++++++++++++++++++++++++++++++++++---------
 src/command.rs  |   3 +
 src/dialog.rs   |   3 +
 src/geometry.rs | 125 +++++++++++++++++++++++
 src/main.rs     | 227 +++++++++++++++++++++++++++++++++++------
 src/render.rs   | 129 +++++++++++++++++++++---
 src/shortcut.rs |   6 ++
 8 files changed, 778 insertions(+), 99 deletions(-)

diff --cc src/app.rs
index c327670,9b66c1c..c50a355
--- a/src/app.rs
+++ b/src/app.rs
@@@ -5224,45 -5298,71 +5317,77 @@@ impl State 
              actions.push(a);
          };
          let sep = ViewportMenuAction::Separator;
+         let toggle = |options: &mut Vec<String>, actions: &mut Vec<ViewportMenuAction>, id: &'static str| {
+             let on = self.command_toggle_state(id).unwrap_or(false);
+             options.push(format!("{} {}", mark(on), label(id, id)));
+             actions.push(ViewportMenuAction::Command(id));
+         };
+ 
+         match page {
+             Some(ViewportMenuPage::Style) => {
+                 row(&mut options, &mut actions, format!("‹ {}", ViewportMenuPage::Style.label()), ViewportMenuAction::Back);
+ 
+                 // Wireframe.
+                 row(&mut options, &mut actions, "-".into(), sep);
+                 toggle(&mut options, &mut actions, "toggle_wireframe");
+                 row(&mut options, &mut actions, "Wire Thickness".into(), ViewportMenuAction::WireThicknessSlider);
+                 row(&mut options, &mut actions, "Wire Opacity".into(), ViewportMenuAction::WireOpacitySlider);
+ 
+                 // 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);
+                 toggle(&mut options, &mut actions, "toggle_show_occluded");
+                 return (options, actions);
+             }
+             Some(ViewportMenuPage::Markers) => {
+                 row(&mut options, &mut actions, format!("‹ {}", ViewportMenuPage::Markers.label()), ViewportMenuAction::Back);
+ 
+                 // The Render points, the group markers sized off them, and
+                 // the pull arrows' length, the other selection feedback.
+                 row(&mut options, &mut actions, "-".into(), sep);
+                 toggle(&mut options, &mut actions, "toggle_render_points");
+                 row(&mut options, &mut actions, "Point Size".into(), ViewportMenuAction::PointSizeSlider);
+                 row(&mut options, &mut actions, "Group Marker Scale".into(), ViewportMenuAction::GroupMarkerScaleSlider);
+                 row(&mut options, &mut actions, "Pull Arrow Scale".into(), ViewportMenuAction::PullArrowScaleSlider);
+ 
+                 // The overlays, a class at a time: points, primitives,
+                 // vertices.
+                 row(&mut options, &mut actions, "-".into(), sep);
+                 toggle(&mut options, &mut actions, "toggle_point_markers");
+                 row(&mut options, &mut actions, "Point Marker Size".into(), ViewportMenuAction::PointMarkerSizeSlider);
+                 toggle(&mut options, &mut actions, "toggle_point_numbers");
+                 toggle(&mut options, &mut actions, "toggle_point_normals");
+                 row(&mut options, &mut actions, "-".into(), sep);
+                 toggle(&mut options, &mut actions, "toggle_prim_numbers");
+                 toggle(&mut options, &mut actions, "toggle_prim_normals");
+                 row(&mut options, &mut actions, "-".into(), sep);
+                 toggle(&mut options, &mut actions, "toggle_vertex_numbers");
+                 return (options, actions);
+             }
+             None => {}
+         }
+ 
+         row(&mut options, &mut actions, "Frame All".into(), ViewportMenuAction::FrameAll);
+         row(&mut options, &mut actions, "View 1:1".into(), ViewportMenuAction::OneToOne);
  
 +        // The image's two camera rows, under the scene's, while one shows.
 +        if self.page_shown.is_some() {
 +            row(&mut options, &mut actions, label("frame_image", "Frame Image").to_string(), ViewportMenuAction::Command("frame_image"));
 +            row(&mut options, &mut actions, label("view_image_pixels", "View Image Pixels 1:1").to_string(), ViewportMenuAction::Command("view_image_pixels"));
 +        }
 +
          // Guides: the scene furniture that is not the geometry.
          row(&mut options, &mut actions, "-".into(), sep);
-         row(&mut options, &mut actions, format!("{} {}", mark(self.viewport().show_grid), label("toggle_grid", "Show Grid")), ViewportMenuAction::Command("toggle_grid"));
-         row(&mut options, &mut actions, format!("{} {}", mark(self.viewport().show_origin), label("toggle_origin", "Show Origin")), ViewportMenuAction::Command("toggle_origin"));
- 
-         // 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);
-         row(&mut options, &mut actions, "Wire Opacity".into(), ViewportMenuAction::WireOpacitySlider);
- 
-         // Points: the Render points, the group markers sized off them, and
-         // the pull arrows' length, the other selection feedback beside them.
-         row(&mut options, &mut actions, "-".into(), sep);
-         row(&mut options, &mut actions, format!("{} {}", mark(self.render_points), label("toggle_render_points", "Show Points")), ViewportMenuAction::Command("toggle_render_points"));
-         row(&mut options, &mut actions, "Point Size".into(), ViewportMenuAction::PointSizeSlider);
-         row(&mut options, &mut actions, "Group Marker Scale".into(), ViewportMenuAction::GroupMarkerScaleSlider);
-         row(&mut options, &mut actions, "Pull Arrow Scale".into(), ViewportMenuAction::PullArrowScaleSlider);
+         toggle(&mut options, &mut actions, "toggle_grid");
+         toggle(&mut options, &mut actions, "toggle_origin");
  
-         // Overlays: the three annotations over the scene's points.
+         // The display settings, a page each.
          row(&mut options, &mut actions, "-".into(), sep);
-         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"));
- 
-         // 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"));
+         for page in [ViewportMenuPage::Style, ViewportMenuPage::Markers] {
+             row(&mut options, &mut actions, format!("{} ›", page.label()), ViewportMenuAction::Submenu(page));
+         }
  
          // 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
@@@ -10747,12 -10947,30 +10951,35 @@@ pub(crate) fn geometry_to_spreadsheet_d
                      if self.overlay_point_count > 0 {
                          draws.push(SceneDraw { mesh: meshes.overlay_points, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false });
                      }
 +                    // The page the level shows stands in the scene as an
 +                    // image: after the furniture and the markers, which are
 +                    // opaque and may show through it, and before the
 +                    // geometry, whose fill may be translucent over it.
 +                    let image_slot = draws.len() as u32;
                      if self.vertex_count_spheres > 0 {
+                         // The line annotations go UNDER the geometry, as
+                         // the markers above do, and write depth (the wire
+                         // draw's `see_through` selects the depth-writing
+                         // line pipeline): what is nearer then blends over
+                         // them, so a whisker behind a translucent face or
+                         // wire is dimmed by it, and what is farther fails
+                         // the test and leaves them whole. Drawn after the
+                         // fill they were hidden outright behind one that
+                         // writes depth, and painted at full strength over
+                         // the near faces of one that does not.
+                         //
+                         // Show Point Normals: thin cyan whiskers,
+                         // width deliberately fixed (a chunky Wire Width is a
+                         // wireframe styling choice, not a normals one).
+                         if self.overlay_normal_count > 0 {
+                             draws.push(SceneDraw { mesh: meshes.overlay_normals, mvp, wireframe: true, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: true });
+                         }
+                         // Pull arrows: selection feedback, like the group
+                         // markers — full opacity, a little heavier than the
+                         // whiskers.
+                         if self.pull_arrow_count > 0 {
+                             draws.push(SceneDraw { mesh: meshes.pull_arrows, mvp, wireframe: true, wire_tint: NO_TINT, opacity: 1.0, line_width: 2.0, wire_base_width: 0.0, prelit: false, see_through: true });
+                         }
                          // With wires coming, the fill is pushed back by its
                          // slope-scaled offset so the lattice reads solid.
                          let base = if self.wireframe { self.wire_width } else { 0.0 };
@@@ -10789,29 -11007,8 +11016,17 @@@
                              draws.push(SceneDraw { mesh: meshes.sphere_edges, mvp, wireframe: true, wire_tint: tint, opacity: wire_alpha, line_width: self.wire_width, wire_base_width: 0.0, prelit: false, see_through });
                          }
                          draws.extend(fill);
-                         // Show Point Normals: thin cyan whiskers,
-                         // width deliberately fixed (a chunky Wire Width is a
-                         // wireframe styling choice, not a normals one).
-                         if self.overlay_normal_count > 0 {
-                             draws.push(SceneDraw { mesh: meshes.overlay_normals, mvp, wireframe: true, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false });
-                         }
-                         // Pull arrows: selection feedback, like the group
-                         // markers — full opacity, a little heavier than the
-                         // whiskers so they read over a wireframe.
-                         if self.pull_arrow_count > 0 {
-                             draws.push(SceneDraw { mesh: meshes.pull_arrows, mvp, wireframe: true, wire_tint: NO_TINT, opacity: 1.0, line_width: 2.0, wire_base_width: 0.0, prelit: false, see_through: false });
-                         }
                      }
                      renderer.stage_scene((sx, sy, cw, ch), draws);
 +                    if let (Some(image), Some(shown)) = (self.page_image, &self.page_shown) {
 +                        renderer.stage_scene_images(vec![cce_ui::vk::SceneImage {
 +                            image,
 +                            corners: shown.world_corners(self.world_unit_mm()),
 +                            mvp,
 +                            opacity: 1.0,
 +                            before: image_slot,
 +                        }]);
 +                    }
                      }
  
                      // Update viewport cache