graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: a Point Marker Size slider in the viewport menu
A slider row under Point Size over the palette row's 0.005-0.1 world
units. It re-sizes the Show Point Markers overlay live from the scene
positions rebuild_scene_geometry now keeps while markers are on, landing on
the same spheres a full rebuild would draw without re-evaluating the graph.
CLAUDE.md's app.rs size claim refreshed to ~9.8k.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 10 +++++++---
src/app.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++---
src/main.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++
src/render.rs | 10 ++++++++++
4 files changed, 106 insertions(+), 6 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 3c59ee4..68ba064 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -97,7 +97,7 @@ engine's shaping/glyph pass (the app has no `FontSystem` or buffer cache of its
`cce_ui::cosmic_text`; `glyphon` is not a dependency of this crate at all, having
gone from cce-ui with the wgpu path).
-- `src/app.rs` (~8.9k lines) — the heart: `State` (the entire app model), `McpAction` /
+- `src/app.rs` (~9.8k lines) — the heart: `State` (the entire app model), `McpAction` /
`CustomEvent`, node-template loading, pane layout. `tick_frame` (simulation:
config polling, inertia, widget ticks) and `stage_frame` (renderer staging) are the
two halves of the old render loop. GPU mesh updates are staged CPU-side
@@ -1381,7 +1381,8 @@ and View 1:1: 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
-world units by 0.005, no suffix since the World Unit names them), and the
+world units by 0.005, no suffix since the World Unit names them), a
+**Point Marker Size** slider (the palette row's 0.005–0.1), and the
polygon **Opacity** as a
SLIDER row — cce-ui's `context_menu::MenuSlider` (2026-09-25), set on the
shown menu by `open_viewport_context_menu`. `viewport_menu_slider` is the
@@ -1400,7 +1401,10 @@ anywhere over the open menu is swallowed rather than orbiting the scene.
it feeds: opacity and wire thickness are draw-time, and point size re-bakes
the Render points (the stage pass's size key) and the Selected-Group
markers — re-sized from `State::group_members`, the positions `sync_nodes`
-keeps from its evaluation, by `rebuild_group_marker_verts`. None of it
+keeps from its evaluation, by `rebuild_group_marker_verts`; point marker
+size re-sizes the Show Point Markers overlay from the scene positions
+`rebuild_scene_geometry` keeps while it is on (`overlay_marker_points`,
+`rebuild_overlay_marker_verts`). None of it
re-evaluates the graph, which `apply_setting`'s regenerate pass would do per
pixel of drag. `sync_nodes` also re-sizes the markers when only the size
moved (`last_group_marker_size`), so the palette's Point Size and Group
diff --git a/src/app.rs b/src/app.rs
index 7e93205..f02d91a 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -457,6 +457,9 @@ pub enum ViewportMenuAction {
/// Point Size in world units, the Render points' radius and (times
/// Group Marker Scale) the group markers': 0–0.1 like the palette's row.
PointSizeSlider,
+ /// Point Marker Size in world units, the Show Point Markers overlay's
+ /// radius: the palette row's 0.005–0.1.
+ PointMarkerSizeSlider,
/// A "-" row: engraved, inert.
Separator,
}
@@ -2103,6 +2106,12 @@ pub struct State {
/// the palette (`toggle_point_markers` / `_numbers` / `_normals`) over
/// the flags below, persisted in `ViewportSettings` beside Show Grid.
pub overlay_marker_verts: Vec<Vertex3D>,
+ /// The scene's point positions, kept by `rebuild_scene_geometry` while
+ /// Show Point Markers is on (empty otherwise), so the markers can be
+ /// re-SIZED without re-evaluating the graph — the viewport menu's Point
+ /// Marker Size slider does that on every motion of a drag
+ /// (`rebuild_overlay_marker_verts`).
+ pub overlay_marker_points: Vec<Vertex3D>,
pub overlay_dirty: bool,
pub overlay_point_count: u32,
pub overlay_number_labels: Vec<([f32; 3], u32)>,
@@ -4371,6 +4380,16 @@ impl State {
decimals: 3,
suffix: "",
},
+ // The palette row's spin is 5–100 thousandths; the same range in
+ // world units here, where the readout has room for the decimals.
+ ViewportMenuAction::PointMarkerSizeSlider => MenuSlider {
+ value: self.point_marker_size.clamp(0.005, 0.1),
+ min: 0.005,
+ max: 0.1,
+ step: 0.005,
+ decimals: 3,
+ suffix: "",
+ },
_ => return None,
})
}
@@ -4380,7 +4399,9 @@ impl State {
/// uniform, a line width and the fill's matching depth bias). Point
/// size is baked into two meshes: the Render points re-bake in the stage
/// pass off their own size key, and the group markers are re-sized here
- /// from their kept members — neither re-evaluates the graph.
+ /// from their kept members. Point Marker Size re-sizes the overlay from
+ /// the scene positions its rebuild kept. None of it re-evaluates the
+ /// graph.
fn land_viewport_menu_slider(&mut self, action: ViewportMenuAction, v: f32) {
match action {
ViewportMenuAction::OpacitySlider => self.geo_opacity = (v / 100.0).clamp(0.0, 1.0),
@@ -4389,6 +4410,10 @@ impl State {
self.point_size = v.clamp(0.0, 0.1);
self.rebuild_group_marker_verts();
}
+ ViewportMenuAction::PointMarkerSizeSlider => {
+ self.point_marker_size = v.clamp(0.005, 0.1);
+ self.rebuild_overlay_marker_verts();
+ }
_ => return,
}
self.viewport_dirty = true;
@@ -4420,7 +4445,7 @@ impl State {
/// The viewport menu's rows and what each does: framing, then the
/// DISPLAY MODE — the wireframe switch and its thickness slider, the
- /// point size slider, flat or
+ /// point size and point marker size 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.
@@ -4438,6 +4463,8 @@ impl State {
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(format!("{} Flat Shading", mark(!self.smooth_shading)));
actions.push(ViewportMenuAction::Shading(false));
options.push(format!("{} Smooth Shading", mark(self.smooth_shading)));
@@ -4502,7 +4529,8 @@ impl State {
// The slider row is worked, not picked.
ViewportMenuAction::OpacitySlider
| ViewportMenuAction::WireThicknessSlider
- | ViewportMenuAction::PointSizeSlider => {}
+ | ViewportMenuAction::PointSizeSlider
+ | ViewportMenuAction::PointMarkerSizeSlider => {}
ViewportMenuAction::Separator => {}
}
}
@@ -5114,6 +5142,19 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
/// current size — the cheap half of the markers, with no evaluation, so
/// a size change can run it on every motion of a drag. The Highlight
/// bake's warm accent, so the markers and the tint read as one feature.
+ /// Build the Show Point Markers overlay's spheres from the kept scene
+ /// positions at the current Point Marker Size — the same call
+ /// `render::scene_point_overlays` makes, without the evaluation that
+ /// produced the positions.
+ pub(crate) fn rebuild_overlay_marker_verts(&mut self) {
+ self.overlay_marker_verts = crate::geometry::points_vertices(
+ &self.overlay_marker_points,
+ self.point_marker_size,
+ cce_ui::colors::to_linear_rgb(self.point_marker_color),
+ );
+ self.overlay_dirty = true;
+ }
+
pub(crate) fn rebuild_group_marker_verts(&mut self) {
let size = self.group_marker_size();
self.group_point_verts = crate::geometry::points_vertices(
@@ -5539,6 +5580,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
group_members: Vec::new(),
last_group_marker_size: 0.0,
overlay_marker_verts: Vec::new(),
+ overlay_marker_points: Vec::new(),
overlay_dirty: false,
overlay_point_count: 0,
overlay_number_labels: Vec::new(),
diff --git a/src/main.rs b/src/main.rs
index 90e4172..82d5320 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1733,6 +1733,50 @@ mod tests {
assert!((radius(&state) - 0.03).abs() < 1e-4, "{}", radius(&state));
}
+ /// Point Marker Size is a viewport-menu slider over the palette row's
+ /// 0.005–0.1, and it re-sizes the Show Point Markers overlay from the
+ /// scene positions the last rebuild kept — landing on exactly what a full
+ /// rebuild at that size would draw, without re-evaluating the graph.
+ #[test]
+ fn the_viewport_menu_sets_the_point_marker_size_without_a_rebuild() {
+ use crate::app::ViewportMenuAction as A;
+ use crate::window::WindowEvent;
+ use cce_ui::widget::{context_menu, MouseScrollDelta};
+ let mut state = State::new(false);
+ state.show_point_markers = true;
+ state.point_marker_size = 0.02;
+ state.rebuild_scene_geometry();
+ assert!(!state.overlay_marker_points.is_empty(), "the bundled scene has points");
+ let version = state.rt_geometry_version;
+
+ state.cursor_x = 300.0;
+ state.cursor_y = 200.0;
+ state.open_viewport_context_menu();
+ 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::PointSizeSlider, "it sits under Point Size");
+ let sl = context_menu::slider(i).expect("a slider");
+ assert_eq!((sl.min, sl.max, sl.step), (0.005, 0.1, 0.005));
+ assert!((sl.value - 0.02).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, 4.0) });
+ assert!((state.point_marker_size - 0.04).abs() < 1e-5, "{}", state.point_marker_size);
+ assert_eq!(state.rt_geometry_version, version, "no rebuild ran");
+ assert!(state.overlay_dirty);
+ let resized: Vec<[f32; 3]> = state.overlay_marker_verts.iter().map(|v| v.position).collect();
+ context_menu::hide();
+
+ // The full path at the same size draws the same spheres.
+ state.rebuild_scene_geometry();
+ let rebuilt: Vec<[f32; 3]> = state.overlay_marker_verts.iter().map(|v| v.position).collect();
+ assert_eq!(resized, rebuilt);
+
+ // Off, nothing is kept to re-size.
+ state.run_command("toggle_point_markers");
+ assert!(state.overlay_marker_points.is_empty());
+ }
+
/// The dialog plate carries its own backdrop compression, above a
/// menu's: whatever the plates' own is (0 in a config that keeps the
/// panes clear), the modal pulls its backdrop toward the tint, and a
diff --git a/src/render.rs b/src/render.rs
index 7bebf50..a62e8da 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -1172,6 +1172,16 @@ impl State {
self.point_marker_color,
);
self.overlay_marker_verts = markers;
+ // Kept for re-sizing the markers without this evaluation — see
+ // `State::rebuild_overlay_marker_verts`.
+ self.overlay_marker_points = if self.show_point_markers {
+ geom.positions()
+ .iter()
+ .map(|&position| crate::geometry::Vertex3D { position, color: [0.0; 3] })
+ .collect()
+ } else {
+ Vec::new()
+ };
self.overlay_number_labels = labels;
self.overlay_normal_verts = normals;
// The wire pass's edges, likewise — topological, and only while the