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

commitda19622006ebdadbed45350d6034392f6a180f6f
parentea700d64c3
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-29 17:55
refactor: Show Points is retired for Show Point Markers

The two drew the same small sphere on every point of the visible scene,
each with a size and colour of its own. Show Points goes, with Point Size
and Point Color; the Selected-Group markers, which were sized as Point
Size times Group Marker Scale, get Group Marker Size in world units, and
an older file's pair is multiplied out on load.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

 CLAUDE.md       |  48 ++++++++----
 src/app.rs      | 238 ++++++++++++++++----------------------------------------
 src/command.rs  |   1 -
 src/dialog.rs   |  20 +----
 src/main.rs     | 167 +++++++++++++--------------------------
 src/project.rs  |   5 +-
 src/shortcut.rs |   1 -
 7 files changed, 159 insertions(+), 321 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 511a9bd..7e9ef62 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -328,7 +328,8 @@ Where it all went:
   the zoom family, Detach Circular Window) were already registry commands.
   Three settings that were toggles on those nodes and reachable NOWHERE else
   became commands: `toggle_ray_traced_preview`, `toggle_wire_single_color`,
-  `toggle_render_points`.
+  and `toggle_render_points` — Show Points, which was retired on
+  2026-09-29 (see "Display mode" below).
 - **The recent-projects list** was the Main node's "Open" dropdown, which
   would have left `recent_files` written and read by nothing. It is rows at
   the head of the palette's Commands list (`RECENT_ROW_PREFIX`), under the
@@ -2185,8 +2186,8 @@ addressed through `GUIDES_MENU` / `GUIDE_*` so no item slid onto another's
 action, while old files carrying `show_cube_enabled` still load), and a
 row for each submenu: **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
+opacity and Show Occluded) and **Markers** (what is drawn on it: Group
+Marker Size 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 Markers, Show Vertex Numbers, Show Vertex Normals).
@@ -2248,10 +2249,10 @@ half a pixel, the palette row's range), a **Wire Opacity** slider under
 that (percent by 5, `State::wire_opacity` — the wires' own, apart from the
 polygons' Opacity; until 2026-09-25 it was the Wire Color's ALPHA, and
 `StoredRenderSettings` moves an old alpha, from state.kdl's `#rrggbbaa` or
-a project's four-component array, into it on load), a **Point Size** slider (0–0.1
-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), a **Group
-Marker Scale** slider (0.5–4 by 0.05, read "1.25x"), and the
+a project's four-component array, into it on load), a
+**Point Marker Size** slider (the palette row's 0.005–0.1, no suffix since
+the World Unit names the units), a **Group
+Marker Size** slider (0–0.2 world units by 0.005), 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
@@ -2267,8 +2268,8 @@ MouseInput, and `mouse_wheel` at the top of MouseWheel — where a wheel
 anywhere over the open menu is swallowed rather than orbiting the scene.
 `drain_viewport_menu_slider` lands a change through
 `land_viewport_menu_slider`, which sets the live field and redoes only what
-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
+it feeds: opacity and wire thickness are draw-time, and group marker size
+re-bakes the Selected-Group
 markers — re-sized from `State::group_members`, the positions `sync_nodes`
 keeps from its evaluation, by `rebuild_group_marker_verts`; point marker
 size re-sizes the Show Point Markers overlay from the scene positions
@@ -2276,11 +2277,24 @@ size re-sizes the Show Point Markers overlay from the scene positions
 `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
-Marker Scale rows reach them the same way. `viewport_menu_rows`
+moved (`last_group_marker_size`), so the palette's Group
+Marker Size row reaches them the same way. `viewport_menu_rows`
 and `run_viewport_menu_action` are split from the open and the click so a
 test reads and runs the rows.
 
+**There is one display of a marker on every point, Show Point Markers**
+(since 2026-09-29). Until then there was a second, **Show Points**
+(`toggle_render_points`, with Point Size and Point Color), which the
+retired Render node had brought: the same small sphere on the same points
+of the same scene, with a size and a colour of its own, differing only in
+following the fill's Opacity. It is gone — the command, the mesh, the three
+settings. The group markers were sized off it, Point Size times Group
+Marker Scale, and have a size of their own now
+(`State::group_marker_size`, world units); `StoredRenderSettings` reads a
+file from before by multiplying the old pair out, and does not read
+`render_points` or `point_color`.
+`an_older_render_block_gives_the_group_markers_their_size` is the test.
+
 **Smooth shading is baked, not shaded.** The raster pass flat-shades every
 fill in `scene3d.wgsl` from screen-space derivative normals, and cce-ui's
 `Vertex3D` carries no normal. The light is fixed in WORLD space, though, so
@@ -2334,7 +2348,7 @@ opaque mesh.
 
 **Every annotation is dimmed by what is in front of it** (since
 2026-09-29). Three mechanisms, because there are three kinds of annotation.
-The MARKERS (Render points, Selected-Group markers, Show Point Markers)
+The MARKERS (Selected-Group markers, Show Point Markers)
 always were: they draw before the fill and the wires and write depth, so a
 nearer translucent face or wire blends over them. The LINE annotations —
 the normal whiskers, Visualize's vectors, the pull arrows — drew AFTER the
@@ -2398,7 +2412,7 @@ and the arrowed points are exactly the ones that moved. An arrow's full length,
 head included, is the displacement times **Pull Arrow Scale**
 (`State::pull_arrow_scale`, default 1 — the true vector; a dialog row and a
 viewport-menu slider, 0.25–10x, persisted in the render block beside Group
-Marker Scale). The scale is display only: the sampled pairs are kept
+Marker Size). The scale is display only: the sampled pairs are kept
 unscaled on `pull_arrow_pairs`, and `rebuild_pull_arrow_verts` stretches
 each arrow from its fixed base, so a slider drag re-evaluates nothing.
 
@@ -2680,10 +2694,10 @@ would throw the selection to the top. `dialog_settings_rows_name_owners_that_exi
 is the backstop, because the failure is silent — a `Field` key no dispatch
 arm names reads a default and writes nowhere, so the row draws, takes an
 edit and does nothing, which is why that test round-trips every one of them.
-**Group Marker Scale** is the one row added with the collapse: the
-Selected-Group markers' radius as a multiple of Point Size
-(`State::group_marker_scale`, persisted in the render block; 1.25 was the
-hard-coded ratio).
+**Group Marker Size** is the one row added with the collapse: the
+Selected-Group markers' radius in world units
+(`State::group_marker_size`, persisted in the render block; it was a
+multiple of the retired Point Size until 2026-09-29).
 
 **The open project's PATH heads the Commands list**, as a row rather than a
 command (`PATH_ROW_ID`): the label is the path, the chord column carries the
diff --git a/src/app.rs b/src/app.rs
index 8ce821b..05c2e74 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -471,15 +471,12 @@ pub enum ViewportMenuAction {
     /// and stepped the same 5%: the two are independent, so a translucent
     /// fill can carry a solid lattice and the other way round.
     WireOpacitySlider,
-    /// 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,
-    /// Group Marker Scale, the Selected-Group markers' radius as a multiple
-    /// of Point Size: the palette row's 0.5–4.
-    GroupMarkerScaleSlider,
+    /// Group Marker Size in world units, the Selected-Group markers'
+    /// radius: the palette row's 0–0.2.
+    GroupMarkerSizeSlider,
     /// Pull Arrow Scale, the pull arrows' length as a multiple of the true
     /// displacement: the palette row's 0.25–10.
     PullArrowScaleSlider,
@@ -1337,13 +1334,12 @@ pub struct RenderSettings {
     pub wire_opacity: f32,
     pub wire_width: f32,
     pub geo_opacity: f32,
-    pub render_points: bool,
-    pub point_size: f32,
-    pub point_color: [f32; 3],
-    /// The Selected-Group markers' radius as a multiple of `point_size` —
-    /// they draw on the same vertices as the Render points, so the ratio is
-    /// what keeps both legible. Hard-coded at 1.25 until 2026-09-24.
-    pub group_marker_scale: f32,
+    /// The Selected-Group markers' radius, in world units. Until
+    /// 2026-09-29 it was Point Size times Group Marker Scale, Point Size
+    /// being the radius of the Show Points display, which went that day as
+    /// a double of Show Point Markers; [`StoredRenderSettings`] multiplies
+    /// an old pair out.
+    pub group_marker_size: f32,
     /// The pull arrows' length as a multiple of the displacement they show.
     /// 1 draws the true vector; a longer arrow is legible when the pull is
     /// small beside the model. Display only — the pull itself is untouched.
@@ -1364,7 +1360,10 @@ pub struct RenderSettings {
 /// with three components or four. A fourth is the wire opacity as it was
 /// stored before `wire_opacity` existed, and becomes it when the file names
 /// no `wire_opacity` of its own — dropping it would make every translucent
-/// wireframe opaque on the first load.
+/// wireframe opaque on the first load. The group markers' size is read the
+/// same way: its own key, else the `point_size` x `group_marker_scale` it
+/// was until 2026-09-29. `render_points` and `point_color`, the rest of the
+/// retired Show Points display, are in older files and not read.
 #[derive(Deserialize)]
 struct StoredRenderSettings {
     #[serde(default)]
@@ -1380,13 +1379,11 @@ struct StoredRenderSettings {
     #[serde(default = "default_geo_opacity")]
     geo_opacity: f32,
     #[serde(default)]
-    render_points: bool,
-    #[serde(default = "default_point_size")]
-    point_size: f32,
-    #[serde(default = "default_point_color")]
-    point_color: [f32; 3],
-    #[serde(default = "default_group_marker_scale")]
-    group_marker_scale: f32,
+    group_marker_size: Option<f32>,
+    #[serde(default)]
+    point_size: Option<f32>,
+    #[serde(default)]
+    group_marker_scale: Option<f32>,
     #[serde(default = "default_pull_arrow_scale")]
     pull_arrow_scale: f32,
     #[serde(default)]
@@ -1410,10 +1407,10 @@ impl From<StoredRenderSettings> for RenderSettings {
             wire_opacity: s.wire_opacity.or(old_alpha).unwrap_or(1.0).clamp(0.0, 1.0),
             wire_width: s.wire_width,
             geo_opacity: s.geo_opacity,
-            render_points: s.render_points,
-            point_size: s.point_size,
-            point_color: s.point_color,
-            group_marker_scale: s.group_marker_scale,
+            group_marker_size: s
+                .group_marker_size
+                .unwrap_or_else(|| s.point_size.unwrap_or(0.02) * s.group_marker_scale.unwrap_or(1.25))
+                .clamp(0.0, GROUP_MARKER_SIZE_MAX),
             pull_arrow_scale: s.pull_arrow_scale,
             smooth_shading: s.smooth_shading,
             show_occluded: s.show_occluded,
@@ -1421,10 +1418,13 @@ impl From<StoredRenderSettings> for RenderSettings {
     }
 }
 
-fn default_group_marker_scale() -> f32 {
-    1.25
+fn default_group_marker_size() -> f32 {
+    0.025
 }
 
+/// The far end of Group Marker Size's range, in world units.
+pub(crate) const GROUP_MARKER_SIZE_MAX: f32 = 0.2;
+
 fn default_pull_arrow_scale() -> f32 {
     1.0
 }
@@ -1441,14 +1441,6 @@ fn default_geo_opacity() -> f32 {
     1.0
 }
 
-fn default_point_size() -> f32 {
-    0.02
-}
-
-fn default_point_color() -> [f32; 3] {
-    [1.0, 1.0, 1.0]
-}
-
 impl Default for RenderSettings {
     fn default() -> Self {
         Self {
@@ -1458,10 +1450,7 @@ impl Default for RenderSettings {
             wire_opacity: 1.0,
             wire_width: default_wire_width(),
             geo_opacity: default_geo_opacity(),
-            render_points: false,
-            point_size: default_point_size(),
-            point_color: default_point_color(),
-            group_marker_scale: default_group_marker_scale(),
+            group_marker_size: default_group_marker_size(),
             pull_arrow_scale: default_pull_arrow_scale(),
             smooth_shading: false,
             show_occluded: false,
@@ -1762,7 +1751,6 @@ impl DesignSettings {
         ("viewport", "grid_color", 3),
         ("viewport", "point_marker_color", 3),
         ("render", "wire_color", 4),
-        ("render", "point_color", 3),
     ];
 
     pub(crate) fn from_kdl_str(content: &str) -> Self {
@@ -1942,8 +1930,6 @@ pub struct SceneMeshes {
     pub grid: cce_ui::vk::MeshId,
     pub origin: cce_ui::vk::MeshId,
     pub pivot: cce_ui::vk::MeshId,
-    /// The Render node's point display (one octahedron per distinct vertex).
-    pub points: cce_ui::vk::MeshId,
     /// Selected-Group membership markers: while a Group node is selected, one
     /// marker per vertex it tags, so the selection SHOWS the group.
     pub group_points: cce_ui::vk::MeshId,
@@ -2344,15 +2330,9 @@ pub struct State {
     /// slider): 1.0 opaque, straight-alpha blended toward the viewport bg.
     pub geo_opacity: f32,
     pub last_viewport_geo_opacity: f32,
-    /// Point display of the node geometry (the Render node's "Render Points"
-    /// toggle): one small octahedron per distinct vertex, sized by
-    /// "Point Size" and tinted by "Point Color".
-    pub render_points: bool,
-    pub point_size: f32,
-    pub point_color: [f32; 3],
-    /// Selected-Group marker radius as a multiple of `point_size` (a
-    /// setting row of the dialog; persisted in the render block).
-    pub group_marker_scale: f32,
+    /// Selected-Group marker radius in world units (a setting row of the
+    /// dialog and a viewport-menu slider; persisted in the render block).
+    pub group_marker_size: f32,
     /// Pull arrow length as a multiple of the true displacement (a setting
     /// row of the dialog and a viewport-menu slider; persisted in the render
     /// block).
@@ -2376,13 +2356,6 @@ pub struct State {
     /// orbit is every frame — and clears it when see-through ends, so the
     /// next entry sorts afresh.
     pub sorted_fill_key: Option<(u64, bool, [f32; 3])>,
-    /// (geometry version, quantized size, color) the points mesh was last
-    /// built from; `point_vertex_count` gates the draw.
-    pub last_points_key: Option<(u64, i32, [u8; 3])>,
-    pub point_vertex_count: u32,
-    pub last_viewport_render_points: bool,
-    pub last_viewport_point_size: f32,
-    pub last_viewport_point_color: [f32; 3],
     /// Selected-Group membership markers: marker vertices staged CPU-side by
     /// `sync_nodes` whenever the selection is a Group node (empty otherwise),
     /// flushed to `meshes.group_points`; `group_point_vertex_count` gates the
@@ -2406,9 +2379,9 @@ pub struct State {
     pub pull_arrow_count: u32,
     pub last_pull_arrows_key: Option<(String, Vec<(String, String)>, u64)>,
     /// The selected Group's member positions, kept from the evaluation so
-    /// the markers can be re-SIZED without re-evaluating the node — a point
-    /// size or marker scale change (the viewport menu's slider, per motion
-    /// of a drag) only rebuilds the spheres (`rebuild_group_marker_verts`).
+    /// the markers can be re-SIZED without re-evaluating the node — a
+    /// marker size change (the viewport menu's slider, per motion of a
+    /// drag) only rebuilds the spheres (`rebuild_group_marker_verts`).
     pub group_members: Vec<Vertex3D>,
     /// The marker radius `group_point_verts` was built at.
     pub last_group_marker_size: f32,
@@ -2662,10 +2635,7 @@ impl State {
                 wire_opacity: self.wire_opacity,
                 wire_width: self.wire_width,
                 geo_opacity: self.geo_opacity,
-                render_points: self.render_points,
-                point_size: self.point_size,
-                point_color: self.point_color,
-                group_marker_scale: self.group_marker_scale,
+                group_marker_size: self.group_marker_size,
                 pull_arrow_scale: self.pull_arrow_scale,
                 smooth_shading: self.smooth_shading,
                 show_occluded: self.show_occluded,
@@ -2740,10 +2710,7 @@ impl State {
         self.wire_opacity = r.wire_opacity;
         self.wire_width = r.wire_width;
         self.geo_opacity = r.geo_opacity;
-        self.render_points = r.render_points;
-        self.point_size = r.point_size;
-        self.point_color = r.point_color;
-        self.group_marker_scale = r.group_marker_scale;
+        self.group_marker_size = r.group_marker_size;
         self.pull_arrow_scale = r.pull_arrow_scale;
         self.rebuild_pull_arrow_verts();
         self.smooth_shading = r.smooth_shading;
@@ -5160,14 +5127,6 @@ impl State {
             // World units, so no suffix — the World Unit declaration is what
             // names them, and a readout saying "mm" under a cm declaration
             // would be wrong.
-            ViewportMenuAction::PointSizeSlider => MenuSlider {
-                value: self.point_size.clamp(0.0, 0.1),
-                min: 0.0,
-                max: 0.1,
-                step: 0.005,
-                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 {
@@ -5178,19 +5137,19 @@ impl State {
                 decimals: 3,
                 suffix: "",
             },
-            // A multiple of Point Size, read as one ("1.25x"): a plain x
-            // rather than a multiplication sign, which the menu face may not
-            // carry and which a fallback glyph would then under-measure.
-            ViewportMenuAction::GroupMarkerScaleSlider => MenuSlider {
-                value: self.group_marker_scale.clamp(0.5, 4.0),
-                min: 0.5,
-                max: 4.0,
-                step: 0.05,
-                decimals: 2,
-                suffix: "x",
+            // World units, as the point markers' size is.
+            ViewportMenuAction::GroupMarkerSizeSlider => MenuSlider {
+                value: self.group_marker_size.clamp(0.0, GROUP_MARKER_SIZE_MAX),
+                min: 0.0,
+                max: GROUP_MARKER_SIZE_MAX,
+                step: 0.005,
+                decimals: 3,
+                suffix: "",
             },
-            // A multiple of the true displacement, read as the group
-            // markers' scale is.
+            // A multiple of the true displacement, read as one ("1.25x"): a
+            // plain x rather than a multiplication sign, which the menu face
+            // may not carry and which a fallback glyph would then
+            // under-measure.
             ViewportMenuAction::PullArrowScaleSlider => MenuSlider {
                 value: self.pull_arrow_scale.clamp(0.25, 10.0),
                 min: 0.25,
@@ -5205,10 +5164,9 @@ impl State {
 
     /// Write a slider row's value onto the live field, and redo only what
     /// that value feeds. The opacities and wire thickness are draw-time values (a
-    /// 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. Point Marker Size re-sizes the overlay from
+    /// uniform, a line width and the fill's matching depth bias). Group
+    /// Marker Size re-sizes the group markers 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) {
@@ -5217,9 +5175,8 @@ impl State {
             ViewportMenuAction::OpacitySlider => ("geo_opacity", v / 100.0),
             ViewportMenuAction::WireThicknessSlider => ("wire_width", v),
             ViewportMenuAction::WireOpacitySlider => ("wire_opacity", v / 100.0),
-            ViewportMenuAction::PointSizeSlider => ("point_size", v),
             ViewportMenuAction::PointMarkerSizeSlider => ("point_marker_size", v),
-            ViewportMenuAction::GroupMarkerScaleSlider => ("group_marker_scale", v),
+            ViewportMenuAction::GroupMarkerSizeSlider => ("group_marker_size", v),
             ViewportMenuAction::PullArrowScaleSlider => ("pull_arrow_scale", v),
             _ => return,
         };
@@ -5245,16 +5202,12 @@ impl State {
             "geo_opacity" => self.geo_opacity = v.clamp(0.0, 1.0),
             "wire_width" => self.wire_width = v.clamp(1.0, 8.0),
             "wire_opacity" => self.wire_opacity = v.clamp(0.0, 1.0),
-            "point_size" => {
-                self.point_size = v.clamp(0.0, 0.1);
-                self.rebuild_group_marker_verts();
-            }
             "point_marker_size" => {
                 self.point_marker_size = v.clamp(0.005, 0.1);
                 self.rebuild_overlay_marker_verts();
             }
-            "group_marker_scale" => {
-                self.group_marker_scale = v.clamp(0.5, 4.0);
+            "group_marker_size" => {
+                self.group_marker_size = v.clamp(0.0, GROUP_MARKER_SIZE_MAX);
                 self.rebuild_group_marker_verts();
             }
             "pull_arrow_scale" => {
@@ -5315,9 +5268,8 @@ impl State {
     /// apart: framing; the GUIDES (Show Grid, Show Origin — the scene
     /// furniture that is not the geometry); the WIREFRAME (its switch,
     /// thickness and opacity); the
-    /// POINTS
-    /// (the Show Points switch, point size, and the group marker scale that
-    /// multiplies it); the OVERLAYS (Show Point Markers and its size, Show
+    /// SELECTION FEEDBACK
+    /// (the group markers' size and the pull arrows' scale); the OVERLAYS (Show Point Markers and its size, Show
     /// Point Numbers, Show Point Normals — the annotations drawn over the
     /// scene's points); the SURFACE (flat or smooth shading as a radio pair,
     /// the polygon opacity, Show Occluded — the three that decide how the
@@ -5384,11 +5336,9 @@ impl State {
                 return (options, actions);
             }
             Some(ViewportMenuPage::Markers) => {
-                // The Render points, the group markers sized off them, and
-                // the pull arrows' length, the other selection feedback.
-                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);
+                // The selection feedback: the group markers' size and the
+                // pull arrows' length.
+                row(&mut options, &mut actions, "Group Marker Size".into(), ViewportMenuAction::GroupMarkerSizeSlider);
                 row(&mut options, &mut actions, "Pull Arrow Scale".into(), ViewportMenuAction::PullArrowScaleSlider);
 
                 // The overlays, a class at a time: points, primitives,
@@ -5489,9 +5439,8 @@ impl State {
             ViewportMenuAction::OpacitySlider
             | ViewportMenuAction::WireThicknessSlider
             | ViewportMenuAction::WireOpacitySlider
-            | ViewportMenuAction::PointSizeSlider
             | ViewportMenuAction::PointMarkerSizeSlider
-            | ViewportMenuAction::GroupMarkerScaleSlider
+            | ViewportMenuAction::GroupMarkerSizeSlider
             | ViewportMenuAction::PullArrowScaleSlider => {}
             ViewportMenuAction::Separator => {}
         }
@@ -6164,9 +6113,9 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             self.group_members = members;
             self.last_group_points_key = group_key;
             self.rebuild_group_marker_verts();
-        } else if (self.group_marker_size() - self.last_group_marker_size).abs() > f32::EPSILON {
-            // Same members, new size (the palette's Point Size or Group
-            // Marker Scale): re-size without re-evaluating.
+        } else if (self.group_marker_size - self.last_group_marker_size).abs() > f32::EPSILON {
+            // Same members, new size (the palette's Group Marker Size):
+            // re-size without re-evaluating.
             self.rebuild_group_marker_verts();
         }
     }
@@ -6241,13 +6190,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
         self.pull_arrows_dirty = true;
     }
 
-    /// The Selected-Group markers' radius: Point Size times Group Marker
-    /// Scale, larger than the Render node's points so both stay legible
-    /// together.
-    pub fn group_marker_size(&self) -> f32 {
-        self.point_size * self.group_marker_scale
-    }
-
     /// Works out how much of each point number shows through the fill, for
     /// the eye the stage pass is staging (`eye` in mesh space, the space of
     /// the labels and the triangles).
@@ -6305,7 +6247,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
     }
 
     pub(crate) fn rebuild_group_marker_verts(&mut self) {
-        let size = self.group_marker_size();
+        let size = self.group_marker_size;
         self.group_point_verts = crate::geometry::points_vertices(
             &self.group_members,
             size,
@@ -6706,21 +6648,13 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             last_viewport_wire_width: 1.0,
             geo_opacity: settings.render.geo_opacity,
             last_viewport_geo_opacity: 1.0,
-            render_points: settings.render.render_points,
-            point_size: settings.render.point_size,
-            point_color: settings.render.point_color,
-            group_marker_scale: settings.render.group_marker_scale,
+            group_marker_size: settings.render.group_marker_size,
             pull_arrow_scale: settings.render.pull_arrow_scale,
             pull_arrow_pairs: Vec::new(),
             smooth_shading: settings.render.smooth_shading,
             show_occluded: settings.render.show_occluded,
             sorted_fill_key: None,
             scene_smooth_verts: Vec::new(),
-            last_points_key: None,
-            point_vertex_count: 0,
-            last_viewport_render_points: false,
-            last_viewport_point_size: 0.0,
-            last_viewport_point_color: [0.0, 0.0, 0.0],
             group_point_verts: Vec::new(),
             group_points_dirty: false,
             group_point_vertex_count: 0,
@@ -8402,11 +8336,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 self.rebuild_scene_geometry();
                 settings_changed = true;
             }
-            Action::ToggleRenderPoints => {
-                self.render_points = !self.render_points;
-                self.viewport_dirty = true;
-                settings_changed = true;
-            }
             Action::ToggleWireSingleColor => {
                 self.wire_single_color = !self.wire_single_color;
                 self.viewport_dirty = true;
@@ -10624,31 +10553,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             renderer.update_mesh(meshes.sphere_edges, bytemuck::cast_slice(&self.scene_edge_verts));
         }
 
-        // The Render node's point display: rebuilt whenever the geometry or
-        // the point params moved (the key), skipped entirely while off.
-        if self.render_points {
-            let key = (
-                self.rt_geometry_version,
-                (self.point_size * 1000.0).round() as i32,
-                [
-                    (self.point_color[0] * 255.0).round().clamp(0.0, 255.0) as u8,
-                    (self.point_color[1] * 255.0).round().clamp(0.0, 255.0) as u8,
-                    (self.point_color[2] * 255.0).round().clamp(0.0, 255.0) as u8,
-                ],
-            );
-            if self.last_points_key != Some(key) {
-                let verts = crate::geometry::points_vertices(
-                    &self.rt_sphere_verts,
-                    self.point_size,
-                    cce_ui::colors::to_linear_rgb(self.point_color),
-                );
-                self.point_vertex_count = verts.len() as u32;
-                renderer.update_mesh(meshes.points, bytemuck::cast_slice(&verts));
-                self.last_points_key = Some(key);
-                self.viewport_dirty = true;
-            }
-        }
-
         // Selected-Group markers, staged by sync_nodes.
         if self.group_points_dirty {
             self.group_points_dirty = false;
@@ -10724,7 +10628,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             grid: renderer.create_mesh(bytemuck::cast_slice(&grid_verts)),
             origin: renderer.create_mesh(bytemuck::cast_slice(&origin_verts)),
             pivot: renderer.create_mesh(bytemuck::cast_slice(&pivot_verts)),
-            points: renderer.create_mesh(&[]),
             group_points: renderer.create_mesh(&[]),
             overlay_points: renderer.create_mesh(&[]),
             overlay_normals: renderer.create_mesh(&[]),
@@ -10936,10 +10839,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                     || self.last_viewport_wire_single_color != self.wire_single_color
                     || self.last_viewport_wire_color != self.wire_color
                     || self.last_viewport_wire_opacity != self.wire_opacity
-                    || self.last_viewport_wire_width != self.wire_width
-                    || self.last_viewport_render_points != self.render_points
-                    || self.last_viewport_point_size != self.point_size
-                    || self.last_viewport_point_color != self.point_color;
+                    || self.last_viewport_wire_width != self.wire_width;
 
                 if viewport_changed {
                     if !rt_mode {
@@ -11001,9 +10901,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                     if self.viewport().show_camera_pivot {
                         draws.push(SceneDraw { mesh: meshes.pivot, mvp: mvp_pivot, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false });
                     }
-                    if self.render_points && self.point_vertex_count > 0 {
-                        draws.push(SceneDraw { mesh: meshes.points, mvp, wireframe: false, wire_tint: NO_TINT, opacity: geo_opacity, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false });
-                    }
                     // Selected-Group markers: full-opacity selection feedback,
                     // deliberately outside the Render node's Opacity.
                     if self.group_point_vertex_count > 0 {
@@ -11114,9 +11011,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                     self.last_viewport_wire_color = self.wire_color;
                     self.last_viewport_wire_opacity = self.wire_opacity;
                     self.last_viewport_wire_width = self.wire_width;
-                    self.last_viewport_render_points = self.render_points;
-                    self.last_viewport_point_size = self.point_size;
-                    self.last_viewport_point_color = self.point_color;
                     self.last_viewport_rt_mode = rt_mode;
                     self.viewport_dirty = false;
                 }
diff --git a/src/command.rs b/src/command.rs
index 5c19447..716c958 100644
--- a/src/command.rs
+++ b/src/command.rs
@@ -217,7 +217,6 @@ pub const COMMANDS: &[Command] = &[
     Command { id: "toggle_vertex_markers", label: "Show Vertex Markers", context: Context::Viewport, run: Run::Key(Action::ToggleVertexMarkers), 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_vertex_normals", label: "Show Vertex Normals", context: Context::Viewport, run: Run::Key(Action::ToggleVertexNormals), 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 },
     Command { id: "toggle_square_viewport", label: "Square Aspect", context: Context::Viewport, run: Run::Key(Action::ToggleSquareViewport), default_chord: Some("Ctrl+a") },
diff --git a/src/dialog.rs b/src/dialog.rs
index 7eae6b9..9d55981 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -1297,13 +1297,8 @@ pub const SETTINGS: &[Setting] = &[
     // polygons'. It was the wire colour's alpha until 2026-09-25.
     Setting::field("Wire Opacity", "wire_opacity", Ctl::Slider { min: 0.0, max: 1.0, dec: 2 }),
     Setting::field("Wire Thickness", "wire_width", Ctl::Slider { min: 1.0, max: 8.0, dec: 1 }),
-    Setting::field("Point Size", "point_size", Ctl::Slider { min: 0.0, max: 0.1, dec: 3 }),
-    Setting::field("Point Color", "point_color", Ctl::Color),
-    // The Selected-Group markers, as a multiple of Point Size: they draw
-    // beside the Render points on the same vertices, so what matters is
-    // how much larger they are — 1.25 was the hard-coded ratio until
-    // 2026-09-24.
-    Setting::field("Group Marker Scale", "group_marker_scale", Ctl::Slider { min: 0.5, max: 4.0, dec: 2 }),
+    // The Selected-Group markers' radius, in world units.
+    Setting::field("Group Marker Size", "group_marker_size", Ctl::Slider { min: 0.0, max: crate::app::GROUP_MARKER_SIZE_MAX, dec: 3 }),
     // The pull arrows' length over the true displacement; 1 is the vector
     // as it is.
     Setting::field("Pull Arrow Scale", "pull_arrow_scale", Ctl::Slider { min: 0.25, max: 10.0, dec: 2 }),
@@ -1613,7 +1608,6 @@ impl State {
             "toggle_vertex_numbers" => self.show_vertex_numbers,
             "toggle_vertex_markers" => self.show_vertex_markers,
             "toggle_vertex_normals" => self.show_vertex_normals,
-            "toggle_render_points" => self.render_points,
             "toggle_wire_single_color" => self.wire_single_color,
             "toggle_ray_traced_preview" => self.viewport().rt_mode,
             "toggle_square_viewport" => self.square_viewport,
@@ -1754,7 +1748,6 @@ impl State {
     fn settings_field_bool(&self, key: &str) -> bool {
         match key {
             "wire_single_color" => self.wire_single_color,
-            "render_points" => self.render_points,
             _ => false,
         }
     }
@@ -1763,7 +1756,6 @@ impl State {
         match key {
             "bg_color" => self.viewport().bg_color,
             "grid_color" => self.viewport().grid_color,
-            "point_color" => self.point_color,
             "point_marker_color" => self.point_marker_color,
             "wire_color" => self.wire_color,
             _ => [0.0; 3],
@@ -1778,8 +1770,7 @@ impl State {
             "wire_width" => self.wire_width,
             "geo_opacity" => self.geo_opacity,
             "wire_opacity" => self.wire_opacity,
-            "point_size" => self.point_size,
-            "group_marker_scale" => self.group_marker_scale,
+            "group_marker_size" => self.group_marker_size,
             "pull_arrow_scale" => self.pull_arrow_scale,
             _ => 0.0,
         }
@@ -1805,7 +1796,6 @@ impl State {
                 let on = value == "true";
                 match key {
                     "wire_single_color" => self.wire_single_color = on,
-                    "render_points" => self.render_points = on,
                     _ => {}
                 }
             }
@@ -1814,7 +1804,6 @@ impl State {
                 match key {
                     "bg_color" => self.viewport_mut().bg_color = c,
                     "grid_color" => self.viewport_mut().grid_color = c,
-                    "point_color" => self.point_color = c,
                     "point_marker_color" => self.point_marker_color = c,
                     "wire_color" => {
                         // Setting a wire colour means wanting to see it: the
@@ -1847,8 +1836,7 @@ impl State {
                     "wire_width" => self.wire_width = v,
                     "geo_opacity" => self.geo_opacity = v,
                     "wire_opacity" => self.wire_opacity = v,
-                    "point_size" => self.point_size = v,
-                    "group_marker_scale" => self.group_marker_scale = v,
+                    "group_marker_size" => self.group_marker_size = v,
                     "pull_arrow_scale" => {
                         self.pull_arrow_scale = v;
                         self.rebuild_pull_arrow_verts();
diff --git a/src/main.rs b/src/main.rs
index 5a62d82..299e9a3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -951,7 +951,6 @@ mod tests {
                 subnet("render", vec![
                     p("Show Wireframe", "toggle", "true"),
                     p("Wire Thickness", "slider", "4.0"),
-                    p("Point Color", "color", "#00ff00"),
                 ]),
                 subnet("main", vec![p("Circular Pane", "toggle", "true")]),
             ],
@@ -981,7 +980,6 @@ mod tests {
         assert!((state.grid_thickness - 0.04).abs() < 1e-6);
         assert!(state.wireframe);
         assert!((state.wire_width - 4.0).abs() < 1e-6);
-        assert_eq!(state.point_color[1], 1.0);
         assert!(state.circular_network_pane);
 
         // Idempotent: a second pass has nothing to find and changes nothing.
@@ -1646,7 +1644,7 @@ mod tests {
         state.geo_opacity = 0.5;
         state.grid_thickness = 0.07;
         state.world_unit = cce_ui::units::Unit::Cm;
-        state.point_color = [0.1, 0.9, 0.2];
+        state.point_marker_color = [0.1, 0.9, 0.2];
         state.save_to_file(&dir).expect("save");
         assert!(!state.has_unsaved_changes());
 
@@ -1660,7 +1658,7 @@ mod tests {
         state.geo_opacity = 1.0;
         state.grid_thickness = 0.03;
         state.world_unit = cce_ui::units::Unit::Mm;
-        state.point_color = [1.0, 1.0, 1.0];
+        state.point_marker_color = [1.0, 1.0, 1.0];
 
         state.load_from_file(&dir).expect("load");
         assert!(!state.viewport().show_grid, "the grid comes back off");
@@ -1669,7 +1667,7 @@ mod tests {
         assert!((state.geo_opacity - 0.5).abs() < 1e-6);
         assert!((state.grid_thickness - 0.07).abs() < 1e-6);
         assert_eq!(state.world_unit, cce_ui::units::Unit::Cm);
-        assert_eq!(state.point_color, [0.1, 0.9, 0.2]);
+        assert_eq!(state.point_marker_color, [0.1, 0.9, 0.2]);
         assert!(!state.scene_smooth_verts.is_empty(), "the scene was rebuilt smooth");
         assert!(!state.has_unsaved_changes(), "a fresh load is clean");
         assert_eq!(state.command_toggle_state("toggle_grid"), Some(false), "the palette's switch agrees");
@@ -2088,7 +2086,7 @@ mod tests {
         assert_eq!(
             groups(Some(P::Markers)),
             vec![
-                vec![A::Command("toggle_render_points"), A::PointSizeSlider, A::GroupMarkerScaleSlider, A::PullArrowScaleSlider],
+                vec![A::GroupMarkerSizeSlider, A::PullArrowScaleSlider],
                 vec![
                     A::Command("toggle_point_markers"),
                     A::PointMarkerSizeSlider,
@@ -2367,26 +2365,6 @@ mod tests {
         assert!(crate::app::DesignSettings::from_kdl_str(&kdl).viewport.show_point_markers, "persisted");
     }
 
-    /// Show Points heads the Points group as a switch over the Render
-    /// points, marked from the live flag, and the row runs its command.
-    #[test]
-    fn the_viewport_menu_toggles_show_points() {
-        use crate::app::ViewportMenuAction as A;
-        let mut state = State::new(false);
-        state.render_points = false;
-        let row = |state: &State| {
-            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()
-        };
-        assert_eq!(row(&state), "○ Show Points");
-        state.run_viewport_menu_action(A::Command("toggle_render_points"));
-        assert!(state.render_points);
-        assert_eq!(row(&state), "● Show Points");
-        let kdl = fs::read_to_string(crate::app::DesignSettings::file_path()).expect("saved");
-        assert!(crate::app::DesignSettings::from_kdl_str(&kdl).render.render_points, "persisted");
-    }
-
     /// 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
@@ -2470,56 +2448,6 @@ mod tests {
         context_menu::hide();
     }
 
-    /// Point Size is a viewport-menu slider over the palette row's 0–0.1,
-    /// and it re-sizes what it feeds without re-evaluating anything: the
-    /// Selected-Group markers are rebuilt from their KEPT members at Point
-    /// Size x Group Marker Scale on every step.
-    #[test]
-    fn the_viewport_menu_sets_the_point_size_and_resizes_the_group_markers() {
-        use crate::app::ViewportMenuAction as A;
-        use crate::window::WindowEvent;
-        use cce_ui::widget::{context_menu, MouseScrollDelta};
-        let mut state = State::new(false);
-        state.point_size = 0.02;
-        state.group_marker_scale = 2.0;
-        let centre = [0.3f32, 0.4, 0.5];
-        state.group_members = vec![crate::geometry::Vertex3D { position: centre, color: [0.0; 3] }];
-        state.rebuild_group_marker_verts();
-        let radius = |state: &State| {
-            state.group_point_verts.iter().map(|v| {
-                let d = [v.position[0] - centre[0], v.position[1] - centre[1], v.position[2] - centre[2]];
-                (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
-            }).fold(0.0f32, f32::max)
-        };
-        assert!((radius(&state) - 0.04).abs() < 1e-4, "{}", radius(&state));
-
-        state.cursor_x = 300.0;
-        state.cursor_y = 200.0;
-        state.open_viewport_context_menu();
-        let sub_actions = state.open_viewport_submenu_with(A::PointSizeSlider);
-        let i = sub_actions.iter().position(|a| *a == A::PointSizeSlider).expect("a Point Size row");
-        let sl = context_menu::submenu::slider(i).expect("a slider");
-        assert_eq!((sl.min, sl.max, sl.step, sl.decimals), (0.0, 0.1, 0.005, 3));
-        assert!((sl.value - 0.02).abs() < 1e-6);
-
-        state.cursor_x = context_menu::submenu::x() + 20.0;
-        state.cursor_y = context_menu::submenu::row_y(i) + context_menu::ROW_H * 0.5;
-        state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(0.0, 2.0) });
-        assert!((state.point_size - 0.03).abs() < 1e-5, "{}", state.point_size);
-        assert!((radius(&state) - 0.06).abs() < 1e-4, "the markers re-sized: {}", radius(&state));
-        assert!(state.group_points_dirty, "and will re-upload");
-        let kdl = fs::read_to_string(crate::app::DesignSettings::file_path()).expect("saved");
-        let saved = crate::app::DesignSettings::from_kdl_str(&kdl).render.point_size;
-        assert!((saved - 0.03).abs() < 1e-6, "persisted: {saved}");
-
-        // A size change arriving another way (the palette's Group Marker
-        // Scale) re-sizes too, through sync_nodes' size check.
-        context_menu::hide();
-        state.group_marker_scale = 1.0;
-        state.sync_nodes();
-        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
@@ -2567,7 +2495,7 @@ mod tests {
 
     /// Pull Arrow Scale stretches the pull arrows along the pull from their
     /// kept pairs, base fixed — 1 by default, the true vector — and is a
-    /// viewport-menu slider under Group Marker Scale.
+    /// viewport-menu slider under Group Marker Size.
     #[test]
     fn pull_arrow_scale_stretches_the_arrows_from_their_base() {
         use crate::app::ViewportMenuAction as A;
@@ -2606,17 +2534,16 @@ mod tests {
         context_menu::hide();
     }
 
-    /// Group Marker Scale is a viewport-menu slider over the palette row's
-    /// 0.5–4, re-sizing the Selected-Group markers from their kept members
-    /// at Point Size x the scale — no re-evaluation of the Group node.
+    /// Group Marker Size is a viewport-menu slider over the palette row's
+    /// 0–0.2 world units, re-sizing the Selected-Group markers from their
+    /// kept members — no re-evaluation of the Group node.
     #[test]
-    fn the_viewport_menu_sets_the_group_marker_scale() {
+    fn the_viewport_menu_sets_the_group_marker_size() {
         use crate::app::ViewportMenuAction as A;
         use crate::window::WindowEvent;
         use cce_ui::widget::{context_menu, MouseScrollDelta};
         let mut state = State::new(false);
-        state.point_size = 0.02;
-        state.group_marker_scale = 1.25;
+        state.group_marker_size = 0.025;
         let centre = [0.0f32, 1.0, 0.0];
         state.group_members = vec![crate::geometry::Vertex3D { position: centre, color: [0.0; 3] }];
         state.rebuild_group_marker_verts();
@@ -2628,22 +2555,45 @@ mod tests {
         state.cursor_x = 300.0;
         state.cursor_y = 200.0;
         state.open_viewport_context_menu();
-        let sub_actions = state.open_viewport_submenu_with(A::GroupMarkerScaleSlider);
-        let i = sub_actions.iter().position(|a| *a == A::GroupMarkerScaleSlider).expect("a Group Marker Scale row");
-        assert_eq!(sub_actions[i - 1], A::PointSizeSlider, "it sits under the size it multiplies");
+        let sub_actions = state.open_viewport_submenu_with(A::GroupMarkerSizeSlider);
+        let i = sub_actions.iter().position(|a| *a == A::GroupMarkerSizeSlider).expect("a Group Marker Size row");
         let sl = context_menu::submenu::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");
+        assert_eq!((sl.min, sl.max, sl.step, sl.decimals), (0.0, 0.2, 0.005, 3));
+        assert!((sl.value - 0.025).abs() < 1e-6);
 
         state.cursor_x = context_menu::submenu::x() + 20.0;
         state.cursor_y = context_menu::submenu::row_y(i) + context_menu::ROW_H * 0.5;
-        state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(0.0, 15.0) });
-        assert!((state.group_marker_scale - 2.0).abs() < 1e-5, "{}", state.group_marker_scale);
+        state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(0.0, 3.0) });
+        assert!((state.group_marker_size - 0.04).abs() < 1e-5, "{}", state.group_marker_size);
         assert!((radius(&state) - 0.04).abs() < 1e-5, "the markers re-sized: {}", radius(&state));
+        assert!(state.group_points_dirty, "and will re-upload");
         let kdl = fs::read_to_string(crate::app::DesignSettings::file_path()).expect("saved");
-        let saved = crate::app::DesignSettings::from_kdl_str(&kdl).render.group_marker_scale;
-        assert!((saved - 2.0).abs() < 1e-5, "persisted: {saved}");
+        let saved = crate::app::DesignSettings::from_kdl_str(&kdl).render.group_marker_size;
+        assert!((saved - 0.04).abs() < 1e-5, "persisted: {saved}");
+
+        // A size change arriving another way re-sizes too, through
+        // sync_nodes' size check.
         context_menu::hide();
+        state.group_marker_size = 0.03;
+        state.sync_nodes();
+        assert!((radius(&state) - 0.03).abs() < 1e-5, "{}", radius(&state));
+    }
+
+    /// Show Points, the second display of a marker on every point, is
+    /// retired for Show Point Markers, and the group markers' size is a
+    /// setting of its own. A file from before carries the old keys: the
+    /// size is the old pair multiplied out, and the rest is not read.
+    #[test]
+    fn an_older_render_block_gives_the_group_markers_their_size() {
+        assert!(crate::command::by_id("toggle_render_points").is_none());
+        let old = "render {\n    render_points (bool)true\n    point_size (f64)0.04\n    point_color (rgb)\"#00ff00\"\n    group_marker_scale (f64)2.0\n}\n";
+        let read = crate::app::DesignSettings::from_kdl_str(old).render;
+        assert!((read.group_marker_size - 0.08).abs() < 1e-6, "{}", read.group_marker_size);
+        let new = "render {\n    point_size (f64)0.04\n    group_marker_size (f64)0.01\n}\n";
+        let read = crate::app::DesignSettings::from_kdl_str(new).render;
+        assert!((read.group_marker_size - 0.01).abs() < 1e-6, "its own key wins: {}", read.group_marker_size);
+        let none = crate::app::DesignSettings::from_kdl_str("render {\n    wireframe (bool)true\n}\n").render;
+        assert!((none.group_marker_size - 0.025).abs() < 1e-6, "{}", none.group_marker_size);
     }
 
     /// The dialog plate IS the menu plate: its fill is `Material::menu`'s
@@ -6440,10 +6390,7 @@ mod tests {
         a.wire_opacity = 0.5;
         a.wire_width = 3.0;
         a.geo_opacity = 0.75;
-        a.render_points = true;
-        a.point_size = 0.05;
-        a.point_color = [0.0, 1.0, 0.0];
-        a.group_marker_scale = 2.5;
+        a.group_marker_size = 0.125;
         a.pull_arrow_scale = 4.0;
         a.smooth_shading = true;
         a.show_occluded = true;
@@ -6466,7 +6413,6 @@ mod tests {
             (back.viewport.bg_color, [0.1, 0.2, 0.3]),
             (back.viewport.grid_color, [0.4, 0.5, 0.6]),
             (back.viewport.point_marker_color, [1.0, 0.5, 0.0]),
-            (back.render.point_color, [0.0, 1.0, 0.0]),
         ] {
             for k in 0..3 {
                 assert!(close(got[k], want[k]), "colour {got:?} came back as {want:?}");
@@ -6479,9 +6425,7 @@ mod tests {
         assert!(close(back.render.wire_opacity, 0.5));
         assert!(close(back.render.wire_width, 3.0));
         assert!(close(back.render.geo_opacity, 0.75));
-        assert!(back.render.render_points);
-        assert!(close(back.render.point_size, 0.05));
-        assert!(close(back.render.group_marker_scale, 2.5));
+        assert!((back.render.group_marker_size - 0.125).abs() < 1e-4);
         assert!(close(back.render.pull_arrow_scale, 4.0));
         assert!(back.render.smooth_shading);
         assert!(back.render.show_occluded);
@@ -12417,10 +12361,11 @@ mod tests {
         assert!((state.geo_opacity - 1.0).abs() < 1e-6, "clamped");
         assert_eq!(state.rt_geometry_version, version);
 
-        // Point Size re-sizes the group markers from their kept members.
-        state.land_dialog_slider(&setting_row_id("Point Size"), 0.05);
-        assert!((state.point_size - 0.05).abs() < 1e-6);
-        assert!((state.last_group_marker_size - state.group_marker_size()).abs() < 1e-6);
+        // Group Marker Size re-sizes the group markers from their kept
+        // members.
+        state.land_dialog_slider(&setting_row_id("Group Marker Size"), 0.05);
+        assert!((state.group_marker_size - 0.05).abs() < 1e-6);
+        assert!((state.last_group_marker_size - 0.05).abs() < 1e-6);
         assert_eq!(state.rt_geometry_version, version);
 
         // The spin rows land the same way: a whole number over the row's
@@ -12538,7 +12483,7 @@ mod tests {
         assert!(matches!(control("Grid Thickness"), Some(Control::Slider { dec: 0, .. })), "a spin is a whole-number slider");
         assert!(matches!(control("Geometry Opacity"), Some(Control::Slider { dec: 2, .. })));
         assert!(matches!(control("World Unit"), Some(Control::Choice { .. })));
-        assert!(matches!(control("Group Marker Scale"), Some(Control::Slider { .. })));
+        assert!(matches!(control("Group Marker Size"), Some(Control::Slider { .. })));
         // A command's switch is its own row; the settings table lists none
         // of them twice.
         assert!(control("Show Grid").is_none());
@@ -12597,13 +12542,13 @@ mod tests {
         assert_ne!(state.world_unit, before, "Enter steps a choice too");
         assert!(state.dialog_visible(), "and keeps the dialog up");
 
-        let scale_row = state.slots.dialog.rows.iter().position(|r| r.id == setting_row_id("Group Marker Scale")).unwrap();
+        let scale_row = state.slots.dialog.rows.iter().position(|r| r.id == setting_row_id("Group Marker Size")).unwrap();
         state.slots.dialog.selected = scale_row;
-        let before = state.group_marker_scale;
+        let before = state.group_marker_size;
         state.dialog_key_input(&key_press(Key::Named(NamedKey::ArrowRight)));
-        assert!(state.group_marker_scale > before, "right arrow grows the markers");
+        assert!(state.group_marker_size > before, "right arrow grows the markers");
         state.dialog_key_input(&key_press(Key::Named(NamedKey::ArrowLeft)));
-        assert!((state.group_marker_scale - before).abs() < 1e-5, "left arrow shrinks them back");
+        assert!((state.group_marker_size - before).abs() < 1e-5, "left arrow shrinks them back");
 
         // And it survives an unrelated parameter edit, which is the whole
         // reason the subnets had to be the owner before.
@@ -12675,7 +12620,7 @@ mod tests {
             "Grid Color", "Grid Thickness", "Origin Size", "Point Marker Size",
             "Point Marker Color", "World Unit",
             // render
-            "Wireframe Color", "Wire Opacity", "Wire Thickness", "Geometry Opacity", "Point Size", "Point Color",
+            "Wireframe Color", "Wire Opacity", "Wire Thickness", "Geometry Opacity",
             // main
             "Background Color",
             // camera
@@ -12686,7 +12631,7 @@ mod tests {
         let mut state = State::new(false);
         for id in [
             "toggle_grid", "toggle_origin", "toggle_wireframe",
-            "toggle_wire_single_color", "toggle_render_points", "toggle_ray_traced_preview",
+            "toggle_wire_single_color", "toggle_ray_traced_preview",
             "toggle_circular_pane", "toggle_camera_pivot", "toggle_square_viewport",
             "toggle_network_plate",
         ] {
diff --git a/src/project.rs b/src/project.rs
index ad96dea..28bd916 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -784,9 +784,8 @@ impl State {
                 },
                 "Wire Thickness" => if let Some(v) = as_f32(&p) { self.wire_width = v.clamp(1.0, 8.0); },
                 "Opacity" => if let Some(v) = as_f32(&p) { self.geo_opacity = v.clamp(0.0, 1.0); },
-                "Render Points" => if let Some(v) = as_bool(&p) { self.render_points = v; },
-                "Point Size" => if let Some(v) = as_f32(&p) { self.point_size = v.clamp(0.0, 0.1); },
-                "Point Color" => if let Some(c) = hex_to_color(p.text()) { self.point_color = c; },
+                // Render Points, Point Size and Point Color were the Show
+                // Points display, retired as a double of Show Point Markers.
                 _ => {}
             }
         }
diff --git a/src/shortcut.rs b/src/shortcut.rs
index 3e82278..349534b 100644
--- a/src/shortcut.rs
+++ b/src/shortcut.rs
@@ -39,7 +39,6 @@ pub enum Action {
     /// commands, which is what makes them reachable at all.
     ToggleRayTracedPreview,
     ToggleWireSingleColor,
-    ToggleRenderPoints,
     ToggleCircularPane,
     DetachCircularWindow,
     Save,