graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: primitive and vertex overlays, and the viewport menu's settings on two pages
Show Primitive Numbers, Show Primitive Normals and Show Vertex Numbers
join the point overlays, dimmed by the fill in front of them as the
point numbers are. The viewport menu keeps framing and the guides at its
top level and puts the display settings on a Style page and a Markers
page; a page is the same popup re-shown, and its rows keep it open.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CLAUDE.md | 50 +++++++++--
src/app.rs | 253 +++++++++++++++++++++++++++++++++++++++++++++++---------
src/command.rs | 3 +
src/dialog.rs | 3 +
src/main.rs | 171 +++++++++++++++++++++++++++++++-------
src/render.rs | 134 ++++++++++++++++++++++++++----
src/shortcut.rs | 6 ++
7 files changed, 523 insertions(+), 97 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 2de200e..a427819 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2064,20 +2064,52 @@ 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, in five groups a separator apart — guides (Show Grid, Show
+The viewport's right-click menu has a top level and two PAGES (since
+2026-09-29; until then it was one list of some twenty rows). The top level
+holds what is done — Frame All, View 1:1 — the guides (Show Grid, Show
Origin; 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 addressed through `GUIDES_MENU` / `GUIDE_*` so no item slid onto
another's action, while old files carrying `show_cube_enabled` still load),
-the
-scene furniture that is not the geometry); wireframe (switch,
-thickness, opacity); points (Show Points, Point Size, Group Marker Scale, which
-multiplies it, and Pull Arrow Scale); overlays (Show Point Markers and its size, Show Point
-Numbers, Show Point Normals — the annotations over the scene's points);
-surface (shading, opacity, Show Occluded).
+and a row into each page: **Style ›** (how the geometry is drawn: the
+wireframe's switch, thickness and opacity, then the surface's shading,
+opacity and Show Occluded) and **Markers ›** (what is drawn on it: Show
+Points, Point Size, Group Marker Scale and Pull Arrow Scale; then the
+overlays a class at a time — Show Point Markers and its size, Show Point
+Numbers, Show Point Normals; Show Primitive Numbers, Show Primitive
+Normals; Show Vertex Numbers).
+
+**A submenu is the same popup showing another page.** cce-ui's context
+menu is one thread-local menu in one popup surface and has no cascade, so
+`ViewportMenuAction::Submenu(page)` re-shows the menu with the page's rows
+at `viewport_menu_anchor`, where it was opened, and the page's first row
+(`‹ Markers`, `ViewportMenuAction::Back`) re-shows the top level.
+`viewport_menu_rows_of(page)` is the rows of any page, `viewport_menu_rows`
+those of the one showing (`State::viewport_menu_page`), and
+`show_viewport_menu_page` the one place the menu is put up. **A row of a
+page keeps the menu open**: a page is a panel of settings, entered to set
+several, so a switch flips, the page is shown again for its marks, and the
+menu closes on a press outside it or Escape; a top-level row closes it as
+before. `the_viewport_menu_turns_to_a_page_and_stays_open` drives it by
+presses. A cascading flyout beside the parent row would be a cce-ui
+change, to the menu and to the runner's popup hosting.
+
+**The primitive and vertex overlays** (`toggle_prim_numbers`,
+`toggle_prim_normals`, `toggle_vertex_numbers`, the same day) are
+collected by `render::scene_element_overlays` off the scene `Detail` the
+point overlays read: a primitive's number at its centroid, its normal
+(Newell's, so a quad not quite flat has one) a whisker from there, and a
+vertex's number — its index in the detail — inset `VERTEX_LABEL_INSET` of
+the way from its point toward its primitive's centroid, so the vertices
+sharing a point stand apart, each inside its own primitive. Warm for
+primitives, green for vertices, the points' pale blue unchanged. They are
+persisted beside the point overlays in `ViewportSettings`, capped at 2000
+labels a class, and dimmed by the fill in front of them as the point
+numbers are (one `point_transmittance` call over all three lists).
+`primitives_and_vertices_are_numbered_where_they_are` is the test.
+
`the_viewport_menu_groups_its_display_rows` holds the
-order. The rows: the Show Wireframe switch (its registry command), **Flat
+order of all three. 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 **Wire Opacity** slider under
diff --git a/src/app.rs b/src/app.rs
index 300c9a8..9b66c1c 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -410,8 +410,33 @@ pub const GUIDE_GRID: usize = 0;
pub const GUIDE_ORIGIN: usize = 1;
pub const GUIDE_CAMERA_PIVOT: usize = 2;
+/// A page of the viewport menu below its top level: the menu is one popup,
+/// so a submenu is the same popup showing another page, entered by a row
+/// and left by the page's first row.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ViewportMenuPage {
+ /// How the geometry itself is drawn: the wireframe and the surface.
+ Style,
+ /// What is drawn ON it: the points, and the markers, numbers and
+ /// normals of its points, primitives and vertices.
+ Markers,
+}
+
+impl ViewportMenuPage {
+ pub fn label(self) -> &'static str {
+ match self {
+ ViewportMenuPage::Style => "Style",
+ ViewportMenuPage::Markers => "Markers",
+ }
+ }
+}
+
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ViewportMenuAction {
+ /// Show a page of the menu in its place. The menu stays open.
+ Submenu(ViewportMenuPage),
+ /// A page's first row: back to the top level. The menu stays open.
+ Back,
/// Move the active camera so the visible node geometry fills the view.
FrameAll,
/// Put the pivot plane at true size: one world unit (the Guides "World
@@ -1245,6 +1270,14 @@ pub struct ViewportSettings {
pub show_point_numbers: bool,
#[serde(default)]
pub show_point_normals: bool,
+ /// The same for the other two element classes. Absent from older
+ /// files — off.
+ #[serde(default)]
+ pub show_prim_numbers: bool,
+ #[serde(default)]
+ pub show_prim_normals: bool,
+ #[serde(default)]
+ pub show_vertex_numbers: bool,
/// World-unit radius and colour of the Show Point Markers overlay.
#[serde(default = "default_point_marker_size")]
pub point_marker_size: f32,
@@ -1444,6 +1477,9 @@ impl Default for ViewportSettings {
show_point_markers: false,
show_point_numbers: false,
show_point_normals: false,
+ show_prim_numbers: false,
+ show_prim_normals: false,
+ show_vertex_numbers: false,
point_marker_size: default_point_marker_size(),
point_marker_color: default_point_marker_color(),
world_unit: default_world_unit(),
@@ -2010,6 +2046,10 @@ pub struct State {
/// machinery as the node menu; this flag says the open menu is OURS).
pub viewport_menu_active: bool,
pub viewport_menu_actions: Vec<ViewportMenuAction>,
+ /// The page the viewport menu shows, `None` for its top level, and
+ /// where it was opened: a page is shown where the menu already is.
+ pub viewport_menu_page: Option<ViewportMenuPage>,
+ pub viewport_menu_anchor: (f32, f32),
/// A parameter row's right-click menu — the same thread-local; the
/// target is (node id, parameter name) rather than a slot and a row, so
/// it holds across a re-layout of the pane.
@@ -2389,6 +2429,14 @@ pub struct State {
/// only occlusion they get. Emptied with every rebuild of the labels; a
/// label with no entry draws whole.
pub overlay_number_alpha: Vec<f32>,
+ /// Show Primitive Numbers and Show Vertex Numbers, the same two lists
+ /// each: a primitive's label stands at its centroid, a vertex's inside
+ /// its primitive, part of the way from its point to that centroid, so
+ /// the vertices that share a point are told apart.
+ pub overlay_prim_labels: Vec<([f32; 3], u32)>,
+ pub overlay_prim_alpha: Vec<f32>,
+ pub overlay_vertex_labels: Vec<([f32; 3], u32)>,
+ pub overlay_vertex_alpha: Vec<f32>,
/// Show Point Normals: LINE_LIST whiskers from each distinct point along
/// its smooth vertex normal (computed from topology — the kernel outputs
/// carry only a default up-normal attribute).
@@ -2399,6 +2447,9 @@ pub struct State {
pub show_point_markers: bool,
pub show_point_numbers: bool,
pub show_point_normals: bool,
+ pub show_prim_numbers: bool,
+ pub show_prim_normals: bool,
+ pub show_vertex_numbers: bool,
/// The visible scene's own edges for the wire pass (LINE_LIST pairs),
/// rebuilt with the scene while Show Wireframe is on and empty while it
/// is off. Topological — see `render::scene_edge_verts`.
@@ -2573,6 +2624,9 @@ impl State {
show_point_markers: self.show_point_markers,
show_point_numbers: self.show_point_numbers,
show_point_normals: self.show_point_normals,
+ show_prim_numbers: self.show_prim_numbers,
+ show_prim_normals: self.show_prim_normals,
+ show_vertex_numbers: self.show_vertex_numbers,
point_marker_size: self.point_marker_size,
point_marker_color: self.point_marker_color,
world_unit: self.world_unit.suffix().to_string(),
@@ -2648,6 +2702,9 @@ impl State {
self.show_point_markers = v.show_point_markers;
self.show_point_numbers = v.show_point_numbers;
self.show_point_normals = v.show_point_normals;
+ self.show_prim_numbers = v.show_prim_numbers;
+ self.show_prim_normals = v.show_prim_normals;
+ self.show_vertex_numbers = v.show_vertex_numbers;
self.point_marker_size = v.point_marker_size;
self.point_marker_color = v.point_marker_color;
if let Some(u) = cce_ui::units::Unit::parse(&v.world_unit) {
@@ -4996,9 +5053,20 @@ impl State {
/// Open the viewport right-click context menu at the cursor.
pub(crate) fn open_viewport_context_menu(&mut self) {
+ self.viewport_menu_anchor = (self.cursor_x, self.cursor_y);
+ self.show_viewport_menu_page(None);
+ }
+
+ /// Show a page of the viewport menu (`None`, its top level) where the
+ /// menu was opened. It is how the menu opens, how a submenu row and a
+ /// Back row turn the page, and how a page re-reads its marks after one
+ /// of its switches was flipped.
+ pub(crate) fn show_viewport_menu_page(&mut self, page: Option<ViewportMenuPage>) {
+ self.viewport_menu_page = page;
let (options, actions) = self.viewport_menu_rows();
let target = self.slots.viewport.id();
- cce_ui::widget::context_menu::show(self.cursor_x, self.cursor_y, options, 0, target);
+ let (x, y) = self.viewport_menu_anchor;
+ cce_ui::widget::context_menu::show(x, y, options, 0, target);
for (i, a) in actions.iter().enumerate() {
if let Some(slider) = self.viewport_menu_slider(*a) {
cce_ui::widget::context_menu::set_row_slider(i, slider);
@@ -5202,8 +5270,27 @@ impl State {
/// 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];
+ self.viewport_menu_rows_of(self.viewport_menu_page)
+ }
+
+ /// The page of the viewport menu that holds `action`, `None` for the
+ /// top level's own rows — how a test finds a row without knowing the
+ /// menu's layout.
+ #[cfg(test)]
+ pub(crate) fn viewport_menu_page_of(&self, action: ViewportMenuAction) -> Option<ViewportMenuPage> {
+ [ViewportMenuPage::Style, ViewportMenuPage::Markers]
+ .into_iter()
+ .find(|page| self.viewport_menu_rows_of(Some(*page)).1.contains(&action))
+ }
+
+ /// The rows of one page of the viewport menu. The top level holds what
+ /// is DONE (framing), the guides, and a row into each page; the STYLE
+ /// page holds how the geometry is drawn (wireframe, then surface) and
+ /// the MARKERS page what is drawn on it (the points, then the overlays
+ /// of each element class). A page's first row names it and leads back.
+ pub(crate) fn viewport_menu_rows_of(&self, page: Option<ViewportMenuPage>) -> (Vec<String>, Vec<ViewportMenuAction>) {
+ let mut options: Vec<String> = Vec::new();
+ let mut actions: Vec<ViewportMenuAction> = Vec::new();
let mark = |on: bool| if on { "●" } else { "○" };
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| {
@@ -5211,39 +5298,71 @@ 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));
+ };
- // 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);
+ 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);
- // Overlays: the three annotations over the scene's points.
+ // Guides: the scene furniture that is not the geometry.
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"));
+ toggle(&mut options, &mut actions, "toggle_grid");
+ toggle(&mut options, &mut actions, "toggle_origin");
- // Surface.
+ // The display settings, a page each.
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
@@ -5277,6 +5396,8 @@ impl State {
ViewportMenuAction::OneToOne => {
self.view_one_to_one();
}
+ ViewportMenuAction::Submenu(page) => self.show_viewport_menu_page(Some(page)),
+ ViewportMenuAction::Back => self.show_viewport_menu_page(None),
ViewportMenuAction::PinFollow => {
self.viewport_pin = None;
self.rebuild_scene_geometry();
@@ -5313,6 +5434,7 @@ impl State {
cce_ui::widget::context_menu::hide();
self.viewport_menu_active = false;
self.viewport_menu_actions.clear();
+ self.viewport_menu_page = None;
}
/// Route a left press while the viewport menu is open — same contract as
@@ -5330,9 +5452,27 @@ impl State {
if cce_ui::widget::context_menu::hit_test(self.cursor_x, self.cursor_y) {
let idx = cce_ui::widget::context_menu::row_at(self.cursor_x, self.cursor_y);
let picked = idx.and_then(|i| self.viewport_menu_actions.get(i).copied());
- self.close_viewport_menu();
- if let Some(action) = picked {
- self.run_viewport_menu_action(action);
+ // Turning the page keeps the menu up, and so does a row of a
+ // page: a page is a panel of settings, entered to set several,
+ // and a switch that closed it would cost a right-click and a
+ // row per setting. It is shown again for its marks.
+ let page = self.viewport_menu_page;
+ match picked {
+ Some(a @ (ViewportMenuAction::Submenu(_) | ViewportMenuAction::Back)) => {
+ self.run_viewport_menu_action(a);
+ }
+ picked if page.is_some() => {
+ if let Some(action) = picked {
+ self.run_viewport_menu_action(action);
+ self.show_viewport_menu_page(page);
+ }
+ }
+ picked => {
+ self.close_viewport_menu();
+ if let Some(action) = picked {
+ self.run_viewport_menu_action(action);
+ }
+ }
}
return true;
}
@@ -6041,19 +6181,32 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
/// the eye the stage pass is staging (`eye` in mesh space, the space of
/// the labels and the triangles).
pub(crate) fn sync_point_number_alpha(&mut self, mvp: Mat4, eye: Vec3) {
- if self.overlay_number_labels.is_empty() {
+ // One pass over the three lists, so the mesh is binned once.
+ let (points_n, prims_n) = (self.overlay_number_labels.len(), self.overlay_prim_labels.len());
+ let at: Vec<[f32; 3]> = self
+ .overlay_number_labels
+ .iter()
+ .chain(&self.overlay_prim_labels)
+ .chain(&self.overlay_vertex_labels)
+ .map(|(p, _)| *p)
+ .collect();
+ if at.is_empty() {
self.overlay_number_alpha.clear();
+ self.overlay_prim_alpha.clear();
+ self.overlay_vertex_alpha.clear();
return;
}
- let points: Vec<[f32; 3]> = self.overlay_number_labels.iter().map(|(p, _)| *p).collect();
- self.overlay_number_alpha = crate::geometry::point_transmittance(
+ let mut alpha = crate::geometry::point_transmittance(
&self.rt_sphere_verts,
mvp,
eye,
- &points,
+ &at,
self.geo_opacity,
self.see_through_active(),
);
+ self.overlay_vertex_alpha = alpha.split_off(points_n + prims_n);
+ self.overlay_prim_alpha = alpha.split_off(points_n);
+ self.overlay_number_alpha = alpha;
}
/// Build the Selected-Group marker spheres from the kept members at the
@@ -6317,6 +6470,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
node_menu_actions: Vec::new(),
viewport_menu_active: false,
viewport_menu_actions: Vec::new(),
+ viewport_menu_page: None,
+ viewport_menu_anchor: (0.0, 0.0),
param_menu_active: false,
param_menu_actions: Vec::new(),
param_menu_target: None,
@@ -6519,11 +6674,18 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
overlay_point_count: 0,
overlay_number_labels: Vec::new(),
overlay_number_alpha: Vec::new(),
+ overlay_prim_labels: Vec::new(),
+ overlay_prim_alpha: Vec::new(),
+ overlay_vertex_labels: Vec::new(),
+ overlay_vertex_alpha: Vec::new(),
overlay_normal_verts: Vec::new(),
overlay_normal_count: 0,
show_point_markers: settings.viewport.show_point_markers,
show_point_numbers: settings.viewport.show_point_numbers,
show_point_normals: settings.viewport.show_point_normals,
+ show_prim_numbers: settings.viewport.show_prim_numbers,
+ show_prim_normals: settings.viewport.show_prim_normals,
+ show_vertex_numbers: settings.viewport.show_vertex_numbers,
scene_edge_verts: Vec::new(),
point_marker_size: settings.viewport.point_marker_size,
point_marker_color: settings.viewport.point_marker_color,
@@ -8166,6 +8328,21 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
self.rebuild_scene_geometry();
settings_changed = true;
}
+ Action::TogglePrimNumbers => {
+ self.show_prim_numbers = !self.show_prim_numbers;
+ self.rebuild_scene_geometry();
+ settings_changed = true;
+ }
+ Action::TogglePrimNormals => {
+ self.show_prim_normals = !self.show_prim_normals;
+ self.rebuild_scene_geometry();
+ settings_changed = true;
+ }
+ Action::ToggleVertexNumbers => {
+ self.show_vertex_numbers = !self.show_vertex_numbers;
+ self.rebuild_scene_geometry();
+ settings_changed = true;
+ }
Action::ToggleRenderPoints => {
self.render_points = !self.render_points;
self.viewport_dirty = true;
diff --git a/src/command.rs b/src/command.rs
index c46a969..12045a5 100644
--- a/src/command.rs
+++ b/src/command.rs
@@ -200,6 +200,9 @@ pub const COMMANDS: &[Command] = &[
Command { id: "toggle_point_markers", label: "Show Point Markers", context: Context::Viewport, run: Run::Key(Action::TogglePointMarkers), default_chord: None },
Command { id: "toggle_point_numbers", label: "Show Point Numbers", context: Context::Viewport, run: Run::Key(Action::TogglePointNumbers), default_chord: None },
Command { id: "toggle_point_normals", label: "Show Point Normals", context: Context::Viewport, run: Run::Key(Action::TogglePointNormals), default_chord: None },
+ Command { id: "toggle_prim_numbers", label: "Show Primitive Numbers", context: Context::Viewport, run: Run::Key(Action::TogglePrimNumbers), default_chord: None },
+ Command { id: "toggle_prim_normals", label: "Show Primitive Normals", context: Context::Viewport, run: Run::Key(Action::TogglePrimNormals), default_chord: None },
+ Command { id: "toggle_vertex_numbers", label: "Show Vertex Numbers", context: Context::Viewport, run: Run::Key(Action::ToggleVertexNumbers), default_chord: None },
Command { id: "toggle_render_points", label: "Show Points", context: Context::Viewport, run: Run::Key(Action::ToggleRenderPoints), default_chord: None },
Command { id: "toggle_wire_single_color", label: "Wireframe Single Color", context: Context::Viewport, run: Run::Key(Action::ToggleWireSingleColor), default_chord: None },
Command { id: "toggle_ray_traced_preview", label: "Ray Traced Preview", context: Context::Viewport, run: Run::Key(Action::ToggleRayTracedPreview), default_chord: None },
diff --git a/src/dialog.rs b/src/dialog.rs
index 9e314ee..a009d60 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -1608,6 +1608,9 @@ impl State {
"toggle_point_markers" => self.show_point_markers,
"toggle_point_numbers" => self.show_point_numbers,
"toggle_point_normals" => self.show_point_normals,
+ "toggle_prim_numbers" => self.show_prim_numbers,
+ "toggle_prim_normals" => self.show_prim_normals,
+ "toggle_vertex_numbers" => self.show_vertex_numbers,
"toggle_render_points" => self.render_points,
"toggle_wire_single_color" => self.wire_single_color,
"toggle_ray_traced_preview" => self.viewport().rt_mode,
diff --git a/src/main.rs b/src/main.rs
index 74e705c..91fb864 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1795,7 +1795,7 @@ mod tests {
let mut state = State::new(false);
state.show_occluded = false;
state.geo_opacity = 1.0;
- let (options, actions) = state.viewport_menu_rows();
+ let (options, actions) = state.viewport_menu_rows_of(state.viewport_menu_page_of(A::Command("toggle_show_occluded")));
let i = actions.iter().position(|a| *a == A::Command("toggle_show_occluded")).expect("a Show Occluded row");
assert_eq!(options[i], "○ Show Occluded");
@@ -1823,7 +1823,7 @@ mod tests {
state.smooth_shading = false;
state.geo_opacity = 1.0;
let row = |state: &State, a: A| {
- let (options, actions) = state.viewport_menu_rows();
+ let (options, actions) = state.viewport_menu_rows_of(state.viewport_menu_page_of(a));
let i = actions.iter().position(|x| *x == a).unwrap_or_else(|| panic!("no {a:?} row"));
options[i].clone()
};
@@ -1869,6 +1869,7 @@ mod tests {
state.cursor_x = 300.0;
state.cursor_y = 200.0;
state.open_viewport_context_menu();
+ state.show_viewport_menu_page(state.viewport_menu_page_of(A::OpacitySlider));
assert!(state.viewport_menu_open());
let i = state.viewport_menu_actions.iter().position(|a| *a == A::OpacitySlider).expect("an Opacity row");
let s = context_menu::slider(i).expect("the row is a slider");
@@ -1959,6 +1960,7 @@ mod tests {
state.cursor_x = 300.0;
state.cursor_y = 200.0;
state.open_viewport_context_menu();
+ state.show_viewport_menu_page(state.viewport_menu_page_of(A::WireOpacitySlider));
let acts = state.viewport_menu_actions.clone();
let i = acts.iter().position(|a| *a == A::WireOpacitySlider).expect("a Wire Opacity row");
let sl = context_menu::slider(i).expect("the row is a slider");
@@ -2020,41 +2022,141 @@ mod tests {
assert_eq!((d.render.wire_color, d.render.wire_opacity), ([0.0; 3], 1.0));
}
- /// The viewport menu reads in groups a separator apart: framing, then
- /// the guides, the wireframe, the points, the point overlays, and the surface
- /// (shading, opacity, Show Occluded) — every display row in exactly one
- /// group.
+ /// The viewport menu's top level holds framing, the guides and a row
+ /// into each page; the display rows are on the pages, in groups a
+ /// separator apart — Style the wireframe then the surface, Markers the
+ /// points then each element class's overlays — every one on exactly one
+ /// page, under a first row that leads back.
#[test]
fn the_viewport_menu_groups_its_display_rows() {
- use crate::app::ViewportMenuAction as A;
+ use crate::app::{ViewportMenuAction as A, ViewportMenuPage as P};
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_grid"), A::Command("toggle_origin")]);
- assert_eq!(groups[2], vec![A::Command("toggle_wireframe"), A::WireThicknessSlider, A::WireOpacitySlider]);
+ let groups = |page: Option<P>| -> Vec<Vec<A>> {
+ let (options, actions) = state.viewport_menu_rows_of(page);
+ assert_eq!(options.len(), actions.len());
+ assert!(options.iter().zip(&actions).all(|(o, a)| (o == "-") == (*a == A::Separator)), "separator rows line up");
+ actions.split(|a| *a == A::Separator).map(|g| g.to_vec()).collect()
+ };
assert_eq!(
- groups[3],
- vec![A::Command("toggle_render_points"), A::PointSizeSlider, A::GroupMarkerScaleSlider, A::PullArrowScaleSlider]
+ groups(None),
+ vec![
+ vec![A::FrameAll, A::OneToOne],
+ vec![A::Command("toggle_grid"), A::Command("toggle_origin")],
+ vec![A::Submenu(P::Style), A::Submenu(P::Markers)],
+ ]
);
assert_eq!(
- groups[4],
+ groups(Some(P::Style)),
vec![
- A::Command("toggle_point_markers"),
- A::PointMarkerSizeSlider,
- A::Command("toggle_point_numbers"),
- A::Command("toggle_point_normals"),
+ vec![A::Back],
+ vec![A::Command("toggle_wireframe"), A::WireThicknessSlider, A::WireOpacitySlider],
+ vec![A::Shading(false), A::Shading(true), A::OpacitySlider, A::Command("toggle_show_occluded")],
]
);
assert_eq!(
- groups[5],
- vec![A::Shading(false), A::Shading(true), A::OpacitySlider, A::Command("toggle_show_occluded")]
+ groups(Some(P::Markers)),
+ vec![
+ vec![A::Back],
+ vec![A::Command("toggle_render_points"), A::PointSizeSlider, A::GroupMarkerScaleSlider, A::PullArrowScaleSlider],
+ vec![
+ A::Command("toggle_point_markers"),
+ A::PointMarkerSizeSlider,
+ A::Command("toggle_point_numbers"),
+ A::Command("toggle_point_normals"),
+ ],
+ vec![A::Command("toggle_prim_numbers"), A::Command("toggle_prim_normals")],
+ vec![A::Command("toggle_vertex_numbers")],
+ ]
);
- assert_eq!(options.len(), actions.len());
- assert!(options.iter().zip(&actions).all(|(o, a)| (o == "-") == (*a == A::Separator)), "separator rows line up");
+ }
+
+ /// A submenu is the menu showing another page where it stands: a press
+ /// on a page's row turns to it and keeps the menu up, a switch on a
+ /// page flips, re-marks and keeps it up, the first row leads back, and
+ /// a top-level row closes the menu as it always did.
+ #[test]
+ fn the_viewport_menu_turns_to_a_page_and_stays_open() {
+ use crate::app::{ViewportMenuAction as A, ViewportMenuPage as P};
+ use crate::window::WindowEvent;
+ use cce_ui::widget::{context_menu, ElementState, MouseButton};
+ let mut state = State::new(false);
+ state.show_prim_numbers = false;
+ state.cursor_x = 300.0;
+ state.cursor_y = 200.0;
+ state.open_viewport_context_menu();
+ let press = |state: &mut State, action: A| {
+ let i = state.viewport_menu_actions.iter().position(|a| *a == action).unwrap_or_else(|| panic!("no {action:?} row"));
+ 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::MouseInput { state: ElementState::Pressed, button: MouseButton::Left });
+ state.handle_event(&WindowEvent::MouseInput { state: ElementState::Released, button: MouseButton::Left });
+ };
+ let at = (context_menu::x(), context_menu::y());
+ press(&mut state, A::Submenu(P::Markers));
+ assert!(state.viewport_menu_open(), "turning the page keeps the menu up");
+ assert_eq!(state.viewport_menu_page, Some(P::Markers));
+ assert_eq!((context_menu::x(), context_menu::y()), at, "where it was");
+ assert_eq!(state.viewport_menu_actions[0], A::Back);
+
+ press(&mut state, A::Command("toggle_prim_numbers"));
+ assert!(state.show_prim_numbers, "the switch flipped");
+ assert!(state.viewport_menu_open(), "and the page is still up");
+ let i = state.viewport_menu_actions.iter().position(|a| *a == A::Command("toggle_prim_numbers")).unwrap();
+ assert!(context_menu::options()[i].starts_with('●'), "re-marked: {}", context_menu::options()[i]);
+
+ press(&mut state, A::Back);
+ assert!(state.viewport_menu_open());
+ assert_eq!(state.viewport_menu_page, None);
+ assert_eq!(state.viewport_menu_actions[0], A::FrameAll);
+
+ press(&mut state, A::Command("toggle_grid"));
+ assert!(!state.viewport_menu_open(), "a top-level row closes the menu");
+ }
+
+ /// The primitive and vertex overlays read off the scene as the points'
+ /// do: a primitive's number at its centroid, a vertex's — its index in
+ /// the detail — inset from its point toward that centroid, a
+ /// primitive's normal from the centroid along the face. Off, nothing
+ /// is collected.
+ #[test]
+ fn primitives_and_vertices_are_numbered_where_they_are() {
+ use glam::Vec3;
+ let mut d = crate::detail::Detail::new();
+ let p: Vec<u32> = [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [2.0, 2.0, 0.0], [0.0, 2.0, 0.0], [4.0, 0.0, 0.0]]
+ .into_iter()
+ .map(|at| d.add_point(Vec3::from_array(at)))
+ .collect();
+ d.add_prim(&[p[0], p[1], p[2], p[3]]);
+ d.add_prim(&[p[1], p[4], p[2]]);
+ let (prims, verts, normals) = crate::render::scene_element_overlays(&d, false, false, false, 1.0);
+ assert!(prims.is_empty() && verts.is_empty() && normals.is_empty());
+
+ let (prims, verts, normals) = crate::render::scene_element_overlays(&d, true, true, true, 0.5);
+ assert_eq!(prims.len(), 2);
+ assert_eq!(prims[0], ([1.0, 1.0, 0.0], 0));
+ assert_eq!(prims[1].1, 1);
+ assert_eq!(verts.iter().map(|(_, v)| *v).collect::<Vec<_>>(), (0..7).collect::<Vec<u32>>(), "every vertex, by its index");
+ // Points 1 and 2 are shared: each has a vertex in either primitive,
+ // and the two stand apart, each inside its own.
+ let inset = crate::render::VERTEX_LABEL_INSET;
+ assert!((Vec3::from_array(verts[1].0) - Vec3::new(2.0, 0.0, 0.0).lerp(Vec3::new(1.0, 1.0, 0.0), inset)).length() < 1e-6);
+ assert!(verts[1].0[0] < 2.0 && verts[4].0[0] > 2.0, "{:?} {:?}", verts[1], verts[4]);
+ assert_eq!(normals.len(), 4, "a whisker a primitive");
+ let n = Vec3::from_array(normals[1].position) - Vec3::from_array(normals[0].position);
+ assert!((n - Vec3::new(0.0, 0.0, 0.5)).length() < 1e-6, "{n:?}");
+
+ // And the app collects them by its switches, and persists those.
+ let mut state = State::new(false);
+ state.show_prim_numbers = false;
+ state.show_vertex_numbers = false;
+ state.rebuild_scene_geometry();
+ assert!(state.overlay_prim_labels.is_empty() && state.overlay_vertex_labels.is_empty());
+ state.run_command("toggle_prim_numbers");
+ state.run_command("toggle_vertex_numbers");
+ assert!(!state.overlay_prim_labels.is_empty() && !state.overlay_vertex_labels.is_empty());
+ let kdl = fs::read_to_string(crate::app::DesignSettings::file_path()).expect("saved");
+ let back = crate::app::DesignSettings::from_kdl_str(&kdl).viewport;
+ assert!(back.show_prim_numbers && back.show_vertex_numbers && !back.show_prim_normals);
}
/// Show Grid heads the Guides group, marked from the live flag; the row
@@ -2065,7 +2167,7 @@ mod tests {
let mut state = State::new(false);
state.viewport_mut().show_grid = true;
let row = |state: &State| {
- let (options, actions) = state.viewport_menu_rows();
+ let (options, actions) = state.viewport_menu_rows_of(state.viewport_menu_page_of(A::Command("toggle_grid")));
let i = actions.iter().position(|a| *a == A::Command("toggle_grid")).expect("a Show Grid row");
options[i].clone()
};
@@ -2087,7 +2189,7 @@ mod tests {
let mut state = State::new(false);
state.viewport_mut().show_origin = true;
let row = |state: &State| {
- let (options, actions) = state.viewport_menu_rows();
+ let (options, actions) = state.viewport_menu_rows_of(state.viewport_menu_page_of(A::Command("toggle_origin")));
let i = actions.iter().position(|a| *a == A::Command("toggle_origin")).expect("a Show Origin row");
options[i].clone()
};
@@ -2111,7 +2213,7 @@ mod tests {
state.rebuild_scene_geometry();
let whiskers = state.overlay_normal_verts.len();
let row = |state: &State| {
- let (options, actions) = state.viewport_menu_rows();
+ let (options, actions) = state.viewport_menu_rows_of(state.viewport_menu_page_of(A::Command("toggle_point_normals")));
let i = actions.iter().position(|a| *a == A::Command("toggle_point_normals")).expect("a Show Point Normals row");
options[i].clone()
};
@@ -2135,7 +2237,7 @@ mod tests {
state.show_point_numbers = false;
state.rebuild_scene_geometry();
let row = |state: &State| {
- let (options, actions) = state.viewport_menu_rows();
+ let (options, actions) = state.viewport_menu_rows_of(state.viewport_menu_page_of(A::Command("toggle_point_numbers")));
let i = actions.iter().position(|a| *a == A::Command("toggle_point_numbers")).expect("a Show Point Numbers row");
options[i].clone()
};
@@ -2161,7 +2263,7 @@ mod tests {
state.show_point_markers = false;
state.rebuild_scene_geometry();
let row = |state: &State| {
- let (options, actions) = state.viewport_menu_rows();
+ let (options, actions) = state.viewport_menu_rows_of(state.viewport_menu_page_of(A::Command("toggle_point_markers")));
let i = actions.iter().position(|a| *a == A::Command("toggle_point_markers")).expect("a Show Point Markers row");
options[i].clone()
};
@@ -2185,7 +2287,7 @@ mod tests {
let mut state = State::new(false);
state.render_points = false;
let row = |state: &State| {
- let (options, actions) = state.viewport_menu_rows();
+ let (options, actions) = state.viewport_menu_rows_of(state.viewport_menu_page_of(A::Command("toggle_render_points")));
let i = actions.iter().position(|a| *a == A::Command("toggle_render_points")).expect("a Show Points row");
options[i].clone()
};
@@ -2211,6 +2313,7 @@ mod tests {
state.cursor_x = 300.0;
state.cursor_y = 200.0;
state.open_viewport_context_menu();
+ state.show_viewport_menu_page(state.viewport_menu_page_of(A::WireThicknessSlider));
let acts = state.viewport_menu_actions.clone();
let i = acts.iter().position(|a| *a == A::WireThicknessSlider).expect("a Wire Thickness row");
assert_eq!(acts[i - 1], A::Command("toggle_wireframe"), "it sits under Show Wireframe");
@@ -2305,6 +2408,7 @@ mod tests {
state.cursor_x = 300.0;
state.cursor_y = 200.0;
state.open_viewport_context_menu();
+ state.show_viewport_menu_page(state.viewport_menu_page_of(A::PointSizeSlider));
let i = state.viewport_menu_actions.iter().position(|a| *a == A::PointSizeSlider).expect("a Point Size row");
let sl = context_menu::slider(i).expect("a slider");
assert_eq!((sl.min, sl.max, sl.step, sl.decimals), (0.0, 0.1, 0.005, 3));
@@ -2347,6 +2451,7 @@ mod tests {
state.cursor_x = 300.0;
state.cursor_y = 200.0;
state.open_viewport_context_menu();
+ state.show_viewport_menu_page(state.viewport_menu_page_of(A::PointMarkerSizeSlider));
let i = state.viewport_menu_actions.iter().position(|a| *a == A::PointMarkerSizeSlider).expect("a Point Marker Size row");
assert_eq!(state.viewport_menu_actions[i - 1], A::Command("toggle_point_markers"), "it sits under its switch");
let sl = context_menu::slider(i).expect("a slider");
@@ -2396,6 +2501,7 @@ mod tests {
state.cursor_x = 300.0;
state.cursor_y = 200.0;
state.open_viewport_context_menu();
+ state.show_viewport_menu_page(state.viewport_menu_page_of(A::PullArrowScaleSlider));
let i = state.viewport_menu_actions.iter().position(|a| *a == A::PullArrowScaleSlider).expect("a Pull Arrow Scale row");
let sl = context_menu::slider(i).expect("a slider");
assert_eq!((sl.min, sl.max, sl.step, sl.suffix), (0.25, 10.0, 0.25, "x"));
@@ -2434,6 +2540,7 @@ mod tests {
state.cursor_x = 300.0;
state.cursor_y = 200.0;
state.open_viewport_context_menu();
+ state.show_viewport_menu_page(state.viewport_menu_page_of(A::GroupMarkerScaleSlider));
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::PointSizeSlider, "it sits under the size it multiplies");
let sl = context_menu::slider(i).expect("a slider");
diff --git a/src/render.rs b/src/render.rs
index 8a9f30c..1f54af9 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -981,7 +981,18 @@ impl State {
/// camera or pane changes (`stage_frame`), so the labels track orbits;
/// a frame staged before the first scene staging simply draws none.
fn append_point_numbers(&self, pc: &mut PaintCtx) {
- if !self.show_viewport || self.overlay_number_labels.is_empty() {
+ if !self.show_viewport {
+ return;
+ }
+ // Points, primitives, vertices: each its own colour, and its own
+ // nudge off the place it names — a point's number beside its
+ // marker, the other two about on the spot, which is theirs alone.
+ let lists: [(&[([f32; 3], u32)], &[f32], [u8; 3], (f32, f32)); 3] = [
+ (&self.overlay_number_labels, &self.overlay_number_alpha, [0xee, 0xee, 0xff], (4.0, -6.0)),
+ (&self.overlay_prim_labels, &self.overlay_prim_alpha, PRIM_LABEL_COLOR, (-4.0, -6.0)),
+ (&self.overlay_vertex_labels, &self.overlay_vertex_alpha, VERTEX_LABEL_COLOR, (-3.0, -5.0)),
+ ];
+ if lists.iter().all(|(labels, ..)| labels.is_empty()) {
return;
}
let Some(mvp) = self.last_scene_mvp else { return };
@@ -990,24 +1001,26 @@ impl State {
return;
}
pc.clip(rect(vx, vy, vw, vh), |pc| {
- for (i, (pos, idx)) in self.overlay_number_labels.iter().enumerate() {
- // What the fill in front of the point lets through; a
- // number behind an opaque face is not drawn.
- let alpha = self.overlay_number_alpha.get(i).copied().unwrap_or(1.0);
- if alpha < 0.02 {
- continue;
- }
- let clip_pos = mvp * glam::Vec4::new(pos[0], pos[1], pos[2], 1.0);
- if clip_pos.w <= 0.0 {
- continue;
- }
- let ndc = clip_pos / clip_pos.w;
- if ndc.x.abs() > 1.02 || ndc.y.abs() > 1.02 {
- continue;
+ for (labels, alphas, color, (dx, dy)) in lists {
+ for (i, (pos, idx)) in labels.iter().enumerate() {
+ // What the fill in front of the place lets through; a
+ // number behind an opaque face is not drawn.
+ let alpha = alphas.get(i).copied().unwrap_or(1.0);
+ if alpha < 0.02 {
+ continue;
+ }
+ let clip_pos = mvp * glam::Vec4::new(pos[0], pos[1], pos[2], 1.0);
+ if clip_pos.w <= 0.0 {
+ continue;
+ }
+ let ndc = clip_pos / clip_pos.w;
+ if ndc.x.abs() > 1.02 || ndc.y.abs() > 1.02 {
+ continue;
+ }
+ let sx = vx + (ndc.x * 0.5 + 0.5) * vw;
+ let sy = vy + (0.5 - ndc.y * 0.5) * vh;
+ pc.text_faded(idx.to_string(), sx + dx, sy + dy, 10.0, color, alpha, None, None);
}
- let sx = vx + (ndc.x * 0.5 + 0.5) * vw;
- let sy = vy + (0.5 - ndc.y * 0.5) * vh;
- pc.text_faded(idx.to_string(), sx + 4.0, sy - 6.0, 10.0, [0xee, 0xee, 0xff], alpha, None, None);
}
});
}
@@ -1211,6 +1224,18 @@ impl State {
self.overlay_number_labels = labels;
self.overlay_number_alpha.clear();
self.overlay_normal_verts = normals;
+ let (prim_labels, vertex_labels, prim_normals) = scene_element_overlays(
+ &geom,
+ self.show_prim_numbers,
+ self.show_vertex_numbers,
+ self.show_prim_normals,
+ self.point_marker_size * 4.0,
+ );
+ self.overlay_prim_labels = prim_labels;
+ self.overlay_prim_alpha.clear();
+ self.overlay_vertex_labels = vertex_labels;
+ self.overlay_vertex_alpha.clear();
+ self.overlay_normal_verts.extend(prim_normals);
// The wire pass's edges, likewise — topological, and only while the
// wireframe is actually on.
self.scene_edge_verts =
@@ -1370,6 +1395,79 @@ pub(crate) fn scene_point_overlays(
(markers, labels, normals)
}
+/// The colours the primitive and vertex overlays are told from the points'
+/// by: warm for a primitive's number and its normal, green for a vertex's.
+pub(crate) const PRIM_LABEL_COLOR: [u8; 3] = [0xff, 0xc8, 0x8c];
+pub(crate) const VERTEX_LABEL_COLOR: [u8; 3] = [0xa0, 0xf0, 0xb0];
+
+/// How far from its point toward its primitive's centroid a vertex's
+/// number stands: far enough that the vertices sharing a point are apart,
+/// near enough to say which corner each is.
+pub(crate) const VERTEX_LABEL_INSET: f32 = 0.3;
+
+/// The overlays of the other two element classes, read off the same scene
+/// `Detail` as the points': `(position, index)` labels for Show Primitive
+/// Numbers, at each primitive's centroid; the same for Show Vertex Numbers,
+/// each vertex inset from its point toward its primitive's centroid; and
+/// LINE_LIST whiskers for Show Primitive Normals, from the centroid along
+/// the primitive's own normal, `len` long.
+///
+/// A vertex's number is its index in the detail, which is its row in the
+/// spreadsheet, as a point's is. The normal is Newell's, so a quad that is
+/// not quite flat still has one; a primitive of fewer than three points has
+/// a number and no normal. Each list of labels is capped as the points' is.
+pub(crate) fn scene_element_overlays(
+ geom: &crate::detail::Detail,
+ prim_numbers: bool,
+ vertex_numbers: bool,
+ prim_normals: bool,
+ len: f32,
+) -> (
+ Vec<([f32; 3], u32)>,
+ Vec<([f32; 3], u32)>,
+ Vec<crate::geometry::Vertex3D>,
+) {
+ use glam::Vec3;
+ const MAX_LABELS: usize = 2000;
+ let (mut prims, mut verts, mut normals) = (Vec::new(), Vec::new(), Vec::new());
+ if !(prim_numbers || vertex_numbers || prim_normals) {
+ return (prims, verts, normals);
+ }
+ let color = cce_ui::colors::to_linear_rgb(PRIM_LABEL_COLOR.map(|c| c as f32 / 255.0));
+ for prim in 0..geom.num_prims() {
+ let pts = geom.prim_points(prim);
+ if pts.is_empty() {
+ continue;
+ }
+ let at: Vec<Vec3> = pts.iter().map(|&p| geom.pos(p as usize)).collect();
+ let centroid = at.iter().copied().sum::<Vec3>() / at.len() as f32;
+ if prim_numbers && prims.len() < MAX_LABELS {
+ prims.push((centroid.to_array(), prim as u32));
+ }
+ if vertex_numbers {
+ for (v, &p) in geom.prim_verts(prim).zip(&at) {
+ if verts.len() >= MAX_LABELS {
+ break;
+ }
+ verts.push((p.lerp(centroid, VERTEX_LABEL_INSET).to_array(), v as u32));
+ }
+ }
+ if prim_normals && at.len() >= 3 {
+ let mut n = Vec3::ZERO;
+ for (k, &a) in at.iter().enumerate() {
+ let b = at[(k + 1) % at.len()];
+ n += Vec3::new((a.y - b.y) * (a.z + b.z), (a.z - b.z) * (a.x + b.x), (a.x - b.x) * (a.y + b.y));
+ }
+ let n = n.normalize_or_zero();
+ if n != Vec3::ZERO {
+ normals.push(crate::geometry::Vertex3D { position: centroid.to_array(), color });
+ normals.push(crate::geometry::Vertex3D { position: (centroid + n * len).to_array(), color });
+ }
+ }
+ }
+ (prims, verts, normals)
+}
+
/// The scene's own edges as LINE_LIST pairs for the wire pass, carrying the
/// geometry's vertex colours.
///
diff --git a/src/shortcut.rs b/src/shortcut.rs
index 26d0ca2..4e81fca 100644
--- a/src/shortcut.rs
+++ b/src/shortcut.rs
@@ -25,6 +25,12 @@ pub enum Action {
TogglePointMarkers,
TogglePointNumbers,
TogglePointNormals,
+ /// The overlays over the other two element classes: a primitive's
+ /// number and normal at its centroid, a vertex's number inside its
+ /// primitive's corner.
+ TogglePrimNumbers,
+ TogglePrimNormals,
+ ToggleVertexNumbers,
/// The path-traced preview, the wireframe's single-colour mode, and the
/// point display. All three were toggles on the root meta node's utility
/// subnets and reachable ONLY there; with those nodes retired they are