graphic design tool
git clone https://git.lucas.co/cce-designer.git
perf(markers): draw every marker kind as one instanced sphere
Point, vertex, selected-group, marked-group and spreadsheet-row markers
are a white marker_sphere (240 vertices) drawn over a list of instances
(cce-ui's SceneDraw::instances), where each marker was the sphere copied
to its point and the whole list uploaded every frame: 46 MB at ten
thousand points, 240 KB now. A size change re-uploads the sphere alone.
Shadow captures of the old and new builds differ in no pixel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 33 ++++++--
src/app.rs | 256 ++++++++++++++++++++++++++++++++++++--------------------
src/geometry.rs | 57 ++++++++++---
src/main.rs | 119 ++++++++++++++++++--------
src/render.rs | 26 +++---
src/vk_smoke.rs | 6 +-
6 files changed, 334 insertions(+), 163 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index b21ec32..3fce28d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2291,7 +2291,7 @@ wears and at Group Marker Size.
- **Nothing is evaluated by a selection.** `State::spreadsheet_points` is
where the rows' points were when the table was last filled, kept from
- that evaluation; `rebuild_row_marker_verts` builds the markers from it,
+ that evaluation; `rebuild_row_markers` builds the markers from it,
and runs from the press, from every refill of the table and from a
change of Group Marker Size.
- **The selection stands across a frame and an edit**, since the rows are
@@ -3402,9 +3402,26 @@ both got cheaper without changing what they show — the table looks each
column up once and formats through `app::fmt4` (`{:.4}` to the
character, `fmt4_is_format_4`), and `points_vertices` works the marker
sphere out once and moves it to each point. A replayed frame went from
-140 ms to 24 there. What is left grows with the mesh: the markers'
-vertices (240 a point, uploaded every frame — instancing in cce-ui's
-renderer is the fix), the visualizers' copy of the scene, and the table.
+140 ms to 24 there. What is left grows with the mesh: the visualizers'
+copy of the scene, and the table.
+
+**The markers are instanced** (the same day, cce-ui's
+`SceneDraw::instances`): every kind — Show Point Markers, Show Vertex
+Markers, the selected group's, the marked groups' and the spreadsheet
+rows' — is one white `geometry::marker_sphere` (240 vertices) drawn over
+a list of instances, a marker's place and colour
+(`geometry::marker_instances`), where each marker was the sphere copied
+to its point and the whole list uploaded every frame (46 MB at ten
+thousand points; 240 KB instanced). The spheres are three meshes —
+the group markers' at Group Marker Size, shared by the selected group,
+the marked groups and the rows; the points' at Point Marker Size; the
+vertices' at `VERTEX_MARKER_SCALE` of it — re-uploaded by the flush when
+a size moves (`State::marker_sphere_radii`), so a size slider uploads 240
+vertices and builds nothing else. `State::drawn_markers` (tests) expands
+instances over their sphere as the shader does;
+`instanced_markers_draw_what_the_copied_spheres_drew` holds that to the
+old per-point copies bit for bit, and shadow captures of the old and new
+builds with point markers and with vertex markers on differ in no pixel.
### The playbar shows what is cached, and what is stale (since 2026-10-06)
@@ -3527,7 +3544,7 @@ number does, so all three of a vertex's overlays name one place. The
markers are the point markers' spheres at `VERTEX_MARKER_SCALE` (0.6) of
Point Marker Size, in the vertex green, smaller so a point's marker is not
lost among the markers of the vertices around it; they share the point
-markers' mesh, and `rebuild_overlay_marker_verts` builds both lists, so
+markers' mesh, and `rebuild_overlay_markers` builds both lists, so
the size slider re-sizes both without an evaluation. **A vertex's normal
is its `N` attribute where the detail carries a Float3 one on its
vertices, and its primitive's normal where it does not** — the normal of
@@ -3569,10 +3586,10 @@ anywhere over the open menu is swallowed rather than orbiting the scene.
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
+keeps from its evaluation, by `rebuild_group_markers`; 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
+`rebuild_overlay_markers`). 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 Group
@@ -4105,7 +4122,7 @@ rather than two:
that fails to parse reads as the defaults), so it rides the project
file too. `State::scene_groups` is every point group of the scene as
last built with its members' positions, kept by `rebuild_scene_geometry`
- so the list and the markers (`rebuild_marked_group_verts`,
+ so the list and the markers (`rebuild_marked_group_markers`,
`meshes.marked_points`) come from what is on screen and a switch
evaluates nothing; the markers follow the geometry through a rebuild.
A marked name the scene has no group for marks nothing and is kept, so
diff --git a/src/app.rs b/src/app.rs
index 6c1e610..cb3e91d 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -2928,8 +2928,20 @@ pub struct SceneMeshes {
pub grid: cce_ui::vk::MeshId,
pub origin: cce_ui::vk::MeshId,
pub pivot: cce_ui::vk::MeshId,
+ /// The marker spheres the marker draws instance (`geometry::marker_sphere`,
+ /// white): the group markers' at Group Marker Size — the selected group,
+ /// the marked groups and the spreadsheet's rows share it — the points'
+ /// at Point Marker Size and the vertices' at `VERTEX_MARKER_SCALE` of
+ /// it. Re-uploaded when a size moves (`State::marker_sphere_radii`),
+ /// which is 240 vertices whatever the scene.
+ pub group_sphere: cce_ui::vk::MeshId,
+ pub point_sphere: cce_ui::vk::MeshId,
+ pub vertex_sphere: 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.
+ /// marker per vertex it tags, so the selection SHOWS the group. This and
+ /// the other marker meshes below hold INSTANCES (cce-ui's
+ /// `SceneDraw::instances`) — a marker's place and colour — drawn over
+ /// their sphere.
pub group_points: cce_ui::vk::MeshId,
/// Markers on the points whose rows are selected in the spreadsheet.
pub row_points: cce_ui::vk::MeshId,
@@ -2937,6 +2949,8 @@ pub struct SceneMeshes {
pub marked_points: cce_ui::vk::MeshId,
/// The Show Point Markers overlay.
pub overlay_points: cce_ui::vk::MeshId,
+ /// The Show Vertex Markers overlay.
+ pub vertex_points: cce_ui::vk::MeshId,
/// The Show Point Normals overlay (LINE_LIST whiskers).
pub overlay_normals: cce_ui::vk::MeshId,
/// Pull arrows (LINE_LIST): while a point-moving Attribute node is
@@ -2944,6 +2958,20 @@ pub struct SceneMeshes {
pub pull_arrows: cce_ui::vk::MeshId,
}
+/// Which markers [`State::drawn_markers`] reads.
+#[cfg(test)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum MarkerKind {
+ /// The selected Group node's members.
+ Group,
+ /// The spreadsheet's selected rows.
+ Row,
+ /// The marked groups' members.
+ Marked,
+ /// Show Point Markers and Show Vertex Markers.
+ Overlay,
+}
+
/// A left-press on the detached circular window's chrome that becomes an
/// interactive move/resize once the pointer travels past a small threshold
/// (so a plain click doesn't start a compositor grab).
@@ -3453,21 +3481,21 @@ pub struct State {
pub sorted_fill_key: Option<(u64, bool, [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
+ /// flushed to `meshes.group_points`; `group_point_count` gates the
/// draw. The key — (node id, params, geometry version) — spares the
/// re-evaluation on unrelated `sync_nodes` runs.
- pub group_point_verts: Vec<Vertex3D>,
+ pub group_point_instances: Vec<Vertex3D>,
/// The spreadsheet's selected rows, shown in the scene: a marker on the
/// point each row is. `spreadsheet_points` is where the rows' points
/// were when the table was last filled, kept so a selection stages its
/// markers without an evaluation; the markers are staged by
- /// `rebuild_row_marker_verts` and flushed to `meshes.row_points`.
+ /// `rebuild_row_markers` and flushed to `meshes.row_points`.
pub spreadsheet_points: Vec<[f32; 3]>,
- pub row_marker_verts: Vec<Vertex3D>,
+ pub row_marker_instances: Vec<Vertex3D>,
pub row_markers_dirty: bool,
- pub row_marker_vertex_count: u32,
+ pub row_marker_count: u32,
pub group_points_dirty: bool,
- pub group_point_vertex_count: u32,
+ pub group_point_count: u32,
pub last_group_points_key: Option<(String, Vec<(String, String)>, u64)>,
/// Pull arrows: while the params pane shows an Attribute node that moves
/// points (`geometry::moves_points`), an arrow from where each of a
@@ -3485,9 +3513,9 @@ pub struct State {
/// The selected Group's member positions, kept from the evaluation so
/// 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`).
+ /// drag) only rebuilds the spheres (`rebuild_group_markers`).
pub group_members: Vec<Vertex3D>,
- /// The marker radius `group_point_verts` was built at.
+ /// The marker radius `group_point_instances` was built at.
pub last_group_marker_size: f32,
/// The point overlays on the visible scene, rebuilt with it: marker
/// geometry for Show Point Markers, and (position, vertex index) labels
@@ -3500,18 +3528,25 @@ pub struct State {
/// settings, and display settings belong to the view: three commands in
/// the palette (`toggle_point_markers` / `_numbers` / `_normals`) over
/// the flags below, persisted in `ViewportSettings` beside Show Grid.
- pub overlay_marker_verts: Vec<Vertex3D>,
+ pub overlay_marker_instances: 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`).
+ /// (`rebuild_overlay_markers`).
pub overlay_marker_points: Vec<Vertex3D>,
/// The same for Show Vertex Markers: where each vertex's marker
/// stands, inset from its point as its number is.
pub overlay_vertex_marker_points: Vec<Vertex3D>,
pub overlay_dirty: bool,
pub overlay_point_count: u32,
+ /// The Show Vertex Markers instances, built beside the points'
+ /// (`rebuild_overlay_markers`) and flushed with them.
+ pub vertex_marker_instances: Vec<Vertex3D>,
+ pub vertex_marker_count: u32,
+ /// The radii the marker spheres were last uploaded at — group, point,
+ /// vertex — so the flush re-uploads them when a size moves.
+ pub marker_sphere_radii: Option<[f32; 3]>,
pub overlay_number_labels: Vec<([f32; 3], u32)>,
/// How much of each label above shows through the fill in front of its
/// point (`geometry::point_transmittance`), worked out by the stage
@@ -3576,11 +3611,11 @@ pub struct State {
/// positions: what the dialog lists and what the markers are built
/// from, so a switch flipped evaluates nothing.
pub scene_groups: Vec<(String, Vec<[f32; 3]>)>,
- /// The marked groups' markers, staged by `rebuild_marked_group_verts`,
+ /// The marked groups' markers, staged by `rebuild_marked_group_markers`,
/// flushed to `meshes.marked_points`.
- pub marked_group_verts: Vec<Vertex3D>,
+ pub marked_group_instances: Vec<Vertex3D>,
pub marked_groups_dirty: bool,
- pub marked_group_vertex_count: u32,
+ pub marked_group_count: u32,
/// 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`.
@@ -7201,11 +7236,11 @@ impl State {
"wire_opacity" => self.wire_opacity = v.clamp(0.0, 1.0),
"point_marker_size" => {
self.point_marker_size = v.clamp(0.005, 0.1);
- self.rebuild_overlay_marker_verts();
+ self.rebuild_overlay_markers();
}
"group_marker_size" => {
self.group_marker_size = v.clamp(0.0, GROUP_MARKER_SIZE_MAX);
- self.rebuild_group_marker_verts();
+ self.rebuild_group_markers();
}
"pull_arrow_scale" => {
self.pull_arrow_scale = v.clamp(0.25, 10.0);
@@ -8231,7 +8266,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
}
self.spreadsheet_mut().set_spreadsheet_data(headers, rows);
self.spreadsheet_points = points;
- self.rebuild_row_marker_verts();
+ self.rebuild_row_markers();
self.last_spreadsheet_node_name = current_name;
self.last_spreadsheet_node_params = current_params;
self.last_spreadsheet_read_at = read_at;
@@ -8239,11 +8274,11 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
if let Some(members) = group_update {
self.group_members = members;
self.last_group_points_key = group_key;
- self.rebuild_group_marker_verts();
+ self.rebuild_group_markers();
} 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();
+ self.rebuild_group_markers();
}
}
@@ -8365,35 +8400,53 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
self.overlay_number_alpha = alpha;
}
- /// Build the Selected-Group marker spheres from the kept members at the
- /// 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 marker overlays' spheres from the kept scene positions at
- /// the current Point Marker Size, without the evaluation that produced
- /// the positions: the points' in the marker colour, and the vertices'
- /// in the vertex overlays' green at `VERTEX_MARKER_SCALE` of the size —
- /// smaller, so that a point's marker is not lost among the markers of
- /// the vertices around it.
- pub(crate) fn rebuild_overlay_marker_verts(&mut self) {
- self.overlay_marker_verts = crate::geometry::points_vertices(
+ /// Build the marker overlays' instances from the kept scene positions,
+ /// without the evaluation that produced the positions: the points' in
+ /// the marker colour, and the vertices' in the vertex overlays' green —
+ /// drawn over a sphere `VERTEX_MARKER_SCALE` the size of the points',
+ /// so that a point's marker is not lost among the markers of the
+ /// vertices around it. A SIZE change needs none of this: the instances
+ /// stand where they stood, and the flush re-uploads the sphere they are
+ /// drawn over (`marker_sphere_radii`).
+ pub(crate) fn rebuild_overlay_markers(&mut self) {
+ self.overlay_marker_instances = crate::geometry::marker_instances(
&self.overlay_marker_points,
- self.point_marker_size,
cce_ui::colors::to_linear_rgb(self.point_marker_color),
);
- self.overlay_marker_verts.extend(crate::geometry::points_vertices(
+ self.vertex_marker_instances = crate::geometry::marker_instances(
&self.overlay_vertex_marker_points,
- self.point_marker_size * crate::render::VERTEX_MARKER_SCALE,
cce_ui::colors::to_linear_rgb(crate::render::VERTEX_LABEL_COLOR.map(|c| c as f32 / 255.0)),
- ));
+ );
self.overlay_dirty = true;
}
+ /// What the marker draws show, as vertices: each kind's instances over
+ /// its sphere at its size — what the renderer draws, expanded. For the
+ /// tests, which read a marker's size and place.
+ #[cfg(test)]
+ pub(crate) fn drawn_markers(&self, kind: MarkerKind) -> Vec<Vertex3D> {
+ use crate::geometry::{expand_instances, marker_sphere};
+ let group = || marker_sphere(self.group_marker_size);
+ match kind {
+ MarkerKind::Group => expand_instances(&group(), &self.group_point_instances),
+ MarkerKind::Row => expand_instances(&group(), &self.row_marker_instances),
+ MarkerKind::Marked => expand_instances(&group(), &self.marked_group_instances),
+ MarkerKind::Overlay => {
+ let mut out = expand_instances(&marker_sphere(self.point_marker_size), &self.overlay_marker_instances);
+ out.extend(expand_instances(
+ &marker_sphere(self.point_marker_size * crate::render::VERTEX_MARKER_SCALE),
+ &self.vertex_marker_instances,
+ ));
+ out
+ }
+ }
+ }
+
/// Stage a marker on the point of every row selected in the
/// spreadsheet, at the group markers' size and in the highlight colour
/// the rows themselves wear. From the positions the table was filled
/// from, so a press on a row evaluates nothing.
- pub(crate) fn rebuild_row_marker_verts(&mut self) {
+ pub(crate) fn rebuild_row_markers(&mut self) {
let _ = self.spreadsheet_mut().take_selection_change();
let rows = self.spreadsheet_mut().selected_rows();
let at: Vec<Vertex3D> = rows
@@ -8402,11 +8455,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
.map(|&position| Vertex3D { position, color: [0.0; 3] })
.collect();
let [r, g, b, _] = cce_ui::colors::highlight_primary_color();
- self.row_marker_verts = if at.is_empty() {
- Vec::new()
- } else {
- crate::geometry::points_vertices(&at, self.group_marker_size, cce_ui::colors::to_linear_rgb([r, g, b]))
- };
+ self.row_marker_instances = crate::geometry::marker_instances(&at, cce_ui::colors::to_linear_rgb([r, g, b]));
self.row_markers_dirty = true;
self.viewport_dirty = true;
}
@@ -8419,18 +8468,14 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
/// Stage a marker on every member of every marked group, at Group
/// Marker Size in the group markers' amber, from the positions the
/// last scene rebuild kept — a switch flipped evaluates nothing.
- pub(crate) fn rebuild_marked_group_verts(&mut self) {
+ pub(crate) fn rebuild_marked_group_markers(&mut self) {
let at: Vec<Vertex3D> = self
.scene_groups
.iter()
.filter(|(name, _)| self.marked_groups.contains(name))
.flat_map(|(_, members)| members.iter().map(|&position| Vertex3D { position, color: [0.0; 3] }))
.collect();
- self.marked_group_verts = if at.is_empty() {
- Vec::new()
- } else {
- crate::geometry::points_vertices(&at, self.group_marker_size, cce_ui::colors::to_linear_rgb([1.0, 0.78, 0.20]))
- };
+ self.marked_group_instances = crate::geometry::marker_instances(&at, cce_ui::colors::to_linear_rgb([1.0, 0.78, 0.20]));
self.marked_groups_dirty = true;
self.viewport_dirty = true;
}
@@ -8465,23 +8510,19 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
self.marked_groups.retain(|g| g != group);
}
if on != was {
- self.rebuild_marked_group_verts();
+ self.rebuild_marked_group_markers();
self.save_settings();
}
}
- pub(crate) fn rebuild_group_marker_verts(&mut self) {
+ pub(crate) fn rebuild_group_markers(&mut self) {
// One size for every kind of marker on a group's members.
- self.rebuild_row_marker_verts();
- self.rebuild_marked_group_verts();
- let size = self.group_marker_size;
- self.group_point_verts = crate::geometry::points_vertices(
- &self.group_members,
- size,
- cce_ui::colors::to_linear_rgb([1.0, 0.78, 0.20]),
- );
+ self.rebuild_row_markers();
+ self.rebuild_marked_group_markers();
+ self.group_point_instances =
+ crate::geometry::marker_instances(&self.group_members, cce_ui::colors::to_linear_rgb([1.0, 0.78, 0.20]));
self.group_points_dirty = true;
- self.last_group_marker_size = size;
+ self.last_group_marker_size = self.group_marker_size;
}
@@ -8915,13 +8956,13 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
sorted_fill_key: None,
scene_smooth_verts: Vec::new(),
environment: crate::environment::Environment::default(),
- group_point_verts: Vec::new(),
+ group_point_instances: Vec::new(),
spreadsheet_points: Vec::new(),
- row_marker_verts: Vec::new(),
+ row_marker_instances: Vec::new(),
row_markers_dirty: false,
- row_marker_vertex_count: 0,
+ row_marker_count: 0,
group_points_dirty: false,
- group_point_vertex_count: 0,
+ group_point_count: 0,
last_group_points_key: None,
pull_arrow_verts: Vec::new(),
pull_arrows_dirty: false,
@@ -8929,10 +8970,13 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
last_pull_arrows_key: None,
group_members: Vec::new(),
last_group_marker_size: 0.0,
- overlay_marker_verts: Vec::new(),
+ overlay_marker_instances: Vec::new(),
overlay_marker_points: Vec::new(),
overlay_vertex_marker_points: Vec::new(),
overlay_dirty: false,
+ vertex_marker_instances: Vec::new(),
+ vertex_marker_count: 0,
+ marker_sphere_radii: None,
overlay_point_count: 0,
overlay_number_labels: Vec::new(),
overlay_number_alpha: Vec::new(),
@@ -8959,9 +9003,9 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
scene_attributes: Vec::new(),
scene_base: None,
scene_groups: Vec::new(),
- marked_group_verts: Vec::new(),
+ marked_group_instances: Vec::new(),
marked_groups_dirty: false,
- marked_group_vertex_count: 0,
+ marked_group_count: 0,
scene_edge_verts: Vec::new(),
point_marker_size: settings.viewport.point_marker_size,
point_marker_color: settings.viewport.point_marker_color,
@@ -12129,7 +12173,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
// A press on a row selects it, and what is
// selected is marked in the scene.
if i == SPREADSHEET_IDX {
- self.rebuild_row_marker_verts();
+ self.rebuild_row_markers();
}
}
@@ -13102,11 +13146,25 @@ 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 spheres the markers are drawn over, at the sizes as they are.
+ let radii = [
+ self.group_marker_size,
+ self.point_marker_size,
+ self.point_marker_size * crate::render::VERTEX_MARKER_SCALE,
+ ];
+ if self.marker_sphere_radii != Some(radii) {
+ self.marker_sphere_radii = Some(radii);
+ for (mesh, r) in [meshes.group_sphere, meshes.point_sphere, meshes.vertex_sphere].into_iter().zip(radii) {
+ renderer.update_mesh(mesh, bytemuck::cast_slice(&crate::geometry::marker_sphere(r)));
+ }
+ self.viewport_dirty = true;
+ }
+
// Selected-Group markers, staged by sync_nodes.
if self.group_points_dirty {
self.group_points_dirty = false;
- renderer.update_mesh(meshes.group_points, bytemuck::cast_slice(&self.group_point_verts));
- self.group_point_vertex_count = self.group_point_verts.len() as u32;
+ renderer.update_mesh(meshes.group_points, bytemuck::cast_slice(&self.group_point_instances));
+ self.group_point_count = self.group_point_instances.len() as u32;
self.viewport_dirty = true;
}
@@ -13114,24 +13172,26 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
// by every scene rebuild.
if self.marked_groups_dirty {
self.marked_groups_dirty = false;
- renderer.update_mesh(meshes.marked_points, bytemuck::cast_slice(&self.marked_group_verts));
- self.marked_group_vertex_count = self.marked_group_verts.len() as u32;
+ renderer.update_mesh(meshes.marked_points, bytemuck::cast_slice(&self.marked_group_instances));
+ self.marked_group_count = self.marked_group_instances.len() as u32;
self.viewport_dirty = true;
}
// The spreadsheet's selected rows, staged by their selection.
if self.row_markers_dirty {
self.row_markers_dirty = false;
- renderer.update_mesh(meshes.row_points, bytemuck::cast_slice(&self.row_marker_verts));
- self.row_marker_vertex_count = self.row_marker_verts.len() as u32;
+ renderer.update_mesh(meshes.row_points, bytemuck::cast_slice(&self.row_marker_instances));
+ self.row_marker_count = self.row_marker_instances.len() as u32;
self.viewport_dirty = true;
}
// The point overlays, staged by rebuild_scene_geometry.
if self.overlay_dirty {
self.overlay_dirty = false;
- renderer.update_mesh(meshes.overlay_points, bytemuck::cast_slice(&self.overlay_marker_verts));
- self.overlay_point_count = self.overlay_marker_verts.len() as u32;
+ renderer.update_mesh(meshes.overlay_points, bytemuck::cast_slice(&self.overlay_marker_instances));
+ self.overlay_point_count = self.overlay_marker_instances.len() as u32;
+ renderer.update_mesh(meshes.vertex_points, bytemuck::cast_slice(&self.vertex_marker_instances));
+ self.vertex_marker_count = self.vertex_marker_instances.len() as u32;
renderer
.update_mesh(meshes.overlay_normals, bytemuck::cast_slice(&self.overlay_normal_verts));
self.overlay_normal_count = self.overlay_normal_verts.len() as u32;
@@ -13194,10 +13254,14 @@ 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)),
+ group_sphere: renderer.create_mesh(&[]),
+ point_sphere: renderer.create_mesh(&[]),
+ vertex_sphere: renderer.create_mesh(&[]),
group_points: renderer.create_mesh(&[]),
row_points: renderer.create_mesh(&[]),
marked_points: renderer.create_mesh(&[]),
overlay_points: renderer.create_mesh(&[]),
+ vertex_points: renderer.create_mesh(&[]),
overlay_normals: renderer.create_mesh(&[]),
// Seeded with what is staged: a replacement renderer gets the
// arrows back without waiting for the selection to change.
@@ -13205,8 +13269,14 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
});
self.pull_arrow_count = self.pull_arrow_verts.len() as u32;
// Scene geometry built during `State::new` (before the renderer
- // existed) uploads on the first frame's flush.
+ // existed) uploads on the first frame's flush, the markers with it:
+ // the spheres at their sizes and the instances already staged.
self.spheres_dirty = !self.rt_sphere_verts.is_empty();
+ self.marker_sphere_radii = None;
+ self.group_points_dirty = true;
+ self.marked_groups_dirty = true;
+ self.row_markers_dirty = true;
+ self.overlay_dirty = true;
self.viewport_dirty = true;
}
@@ -13460,33 +13530,37 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
// nothing re-uploads; the next entry just sorts anew.
self.sorted_fill_key = None;
}
- let mut draws = vec![SceneDraw { mesh: meshes.viewport_bg, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false }];
+ let mut draws = vec![SceneDraw { mesh: meshes.viewport_bg, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false, instances: None }];
if self.viewport().show_grid {
- draws.push(SceneDraw { mesh: meshes.grid, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false });
+ draws.push(SceneDraw { mesh: meshes.grid, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false, instances: None });
}
if self.viewport().show_origin {
- draws.push(SceneDraw { mesh: meshes.origin, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false });
+ draws.push(SceneDraw { mesh: meshes.origin, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false, instances: None });
}
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 });
+ 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, instances: None });
}
// Selected-Group markers: full-opacity selection feedback,
// deliberately outside the Render node's Opacity.
- if self.group_point_vertex_count > 0 {
- draws.push(SceneDraw { mesh: meshes.group_points, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false });
+ if self.group_point_count > 0 {
+ draws.push(SceneDraw { mesh: meshes.group_sphere, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false, instances: Some(meshes.group_points) });
}
// The marked groups: the same amber as a selected
// group's markers, and the same tier.
- if self.marked_group_vertex_count > 0 {
- draws.push(SceneDraw { mesh: meshes.marked_points, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false });
+ if self.marked_group_count > 0 {
+ draws.push(SceneDraw { mesh: meshes.group_sphere, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false, instances: Some(meshes.marked_points) });
}
// The spreadsheet's selected rows, while it is shown.
- if self.show_spreadsheet && self.row_marker_vertex_count > 0 {
- draws.push(SceneDraw { mesh: meshes.row_points, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false });
+ if self.show_spreadsheet && self.row_marker_count > 0 {
+ draws.push(SceneDraw { mesh: meshes.group_sphere, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false, instances: Some(meshes.row_points) });
}
- // Per-node meta "Point Markers", same full-opacity tier.
+ // Show Point Markers and Show Vertex Markers, the same
+ // full-opacity tier, each its sphere instanced.
if self.overlay_point_count > 0 {
- draws.push(SceneDraw { mesh: meshes.overlay_points, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false });
+ draws.push(SceneDraw { mesh: meshes.point_sphere, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false, instances: Some(meshes.overlay_points) });
+ }
+ if self.vertex_marker_count > 0 {
+ draws.push(SceneDraw { mesh: meshes.vertex_sphere, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false, instances: Some(meshes.vertex_points) });
}
// The page the level shows stands in the scene as an
// image: after the furniture and the markers, which are
@@ -13509,18 +13583,18 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
// width deliberately fixed (a chunky Wire Width is a
// wireframe styling choice, not a normals one).
if self.overlay_normal_count > 0 {
- draws.push(SceneDraw { mesh: meshes.overlay_normals, mvp, wireframe: true, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: true });
+ draws.push(SceneDraw { mesh: meshes.overlay_normals, mvp, wireframe: true, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: true, instances: None });
}
// Pull arrows: selection feedback, like the group
// markers — full opacity, a little heavier than the
// whiskers.
if self.pull_arrow_count > 0 {
- draws.push(SceneDraw { mesh: meshes.pull_arrows, mvp, wireframe: true, wire_tint: NO_TINT, opacity: 1.0, line_width: 2.0, wire_base_width: 0.0, prelit: false, see_through: true });
+ draws.push(SceneDraw { mesh: meshes.pull_arrows, mvp, wireframe: true, wire_tint: NO_TINT, opacity: 1.0, line_width: 2.0, wire_base_width: 0.0, prelit: false, see_through: true, instances: None });
}
// With wires coming, the fill is pushed back by its
// slope-scaled offset so the lattice reads solid.
let base = if self.wireframe { self.wire_width } else { 0.0 };
- let mut fill = Some(SceneDraw { mesh: meshes.spheres, mvp, wireframe: false, wire_tint: NO_TINT, opacity: geo_opacity, line_width: 1.0, wire_base_width: base, prelit: self.smooth_shading, see_through });
+ let mut fill = Some(SceneDraw { mesh: meshes.spheres, mvp, wireframe: false, wire_tint: NO_TINT, opacity: geo_opacity, line_width: 1.0, wire_base_width: base, prelit: self.smooth_shading, see_through, instances: None });
// A see-through fill writes no depth, so wires drawn
// AFTER it pass everywhere and the far side's paint
// over the near faces. Seen through, the wires go
@@ -13550,7 +13624,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
[0.0, 0.0, 0.0, 0.0]
};
let wire_alpha = self.wire_opacity.clamp(0.0, 1.0);
- draws.push(SceneDraw { mesh: meshes.sphere_edges, mvp, wireframe: true, wire_tint: tint, opacity: wire_alpha, line_width: self.wire_width, wire_base_width: 0.0, prelit: false, see_through });
+ draws.push(SceneDraw { mesh: meshes.sphere_edges, mvp, wireframe: true, wire_tint: tint, opacity: wire_alpha, line_width: self.wire_width, wire_base_width: 0.0, prelit: false, see_through, instances: None });
}
draws.extend(fill);
}
diff --git a/src/geometry.rs b/src/geometry.rs
index bb7b908..67d16cf 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -6541,18 +6541,20 @@ pub fn origin_vectors_vertices(scale: f32) -> Vec<Vertex3D> {
/// sphere kernel's exactly — that winding is the one the raster pass's
/// backface cull is known to keep.
pub fn points_vertices(src: &[Vertex3D], size: f32, color: [f32; 3]) -> Vec<Vertex3D> {
+ expand_instances(&marker_sphere(size), &marker_instances(src, color))
+}
+
+/// The sphere every point marker is, about the origin at radius `size`, in
+/// white: the mesh a marker draw instances ([`marker_instances`], cce-ui's
+/// `SceneDraw::instances`), each instance moving it to a point and giving
+/// it its colour. 240 corners — 4 bands of 10 quads.
+pub fn marker_sphere(size: f32) -> Vec<Vertex3D> {
let r = size.max(0.001);
const LAT_STEPS: usize = 4;
const LON_STEPS: usize = 10;
let pi = std::f32::consts::PI;
- // The sphere's corners about the origin, worked out once: every marker
- // is the same sphere moved, and each is its centre plus these. Until
- // 2026-10-06 each of a marker's 240 corners took its own sines and
- // cosines, which at ten thousand points was most of a playing frame.
- // The same arithmetic in the same order, so the corners are the same
- // to the bit.
let off = |theta: f32, phi: f32| [r * theta.sin() * phi.cos(), r * theta.cos(), r * theta.sin() * phi.sin()];
- let mut unit = Vec::with_capacity(LAT_STEPS * LON_STEPS * 6);
+ let mut out = Vec::with_capacity(LAT_STEPS * LON_STEPS * 6);
for lat in 0..LAT_STEPS {
let theta0 = pi * lat as f32 / LAT_STEPS as f32;
let theta1 = pi * (lat + 1) as f32 / LAT_STEPS as f32;
@@ -6571,22 +6573,51 @@ pub fn points_vertices(src: &[Vertex3D], size: f32, color: [f32; 3]) -> Vec<Vert
// surface showed only where that far half poked out of the
// mesh, vanishing from the views where it did not.
// `point_markers_wind_outward` holds the sign.
- unit.extend([p00, p11, p10, p00, p01, p11]);
+ for position in [p00, p11, p10, p00, p01, p11] {
+ out.push(Vertex3D { position, color: [1.0; 3] });
+ }
}
}
+ out
+}
+
+/// One marker instance per distinct point of `src` — where the marker
+/// stands and its colour — for a draw of [`marker_sphere`]. Points within
+/// a thousandth of a unit of each other share one marker. Until 2026-10-06
+/// every marker was the sphere's 240 corners copied to its point, so the
+/// markers on ten thousand points were 2.4 million vertices built and
+/// uploaded on every frame of a playing simulation; instanced, they are
+/// ten thousand of these.
+pub fn marker_instances(src: &[Vertex3D], color: [f32; 3]) -> Vec<Vertex3D> {
let mut seen = std::collections::HashSet::with_capacity(src.len());
- let mut out = Vec::with_capacity(src.len() * unit.len());
+ let mut out = Vec::with_capacity(src.len());
for v in src {
let key = (
(v.position[0] * 1000.0).round() as i32,
(v.position[1] * 1000.0).round() as i32,
(v.position[2] * 1000.0).round() as i32,
);
- if !seen.insert(key) {
- continue;
+ if seen.insert(key) {
+ out.push(Vertex3D { position: v.position, color });
}
- let [cx, cy, cz] = v.position;
- out.extend(unit.iter().map(|o| Vertex3D { position: [cx + o[0], cy + o[1], cz + o[2]], color }));
+ }
+ out
+}
+
+/// What a renderer draws for `mesh` instanced over `instances` (cce-ui's
+/// `SceneDraw::instances`): the mesh once per instance, each instance's
+/// position added to every vertex and its colour multiplying theirs — the
+/// same arithmetic as the shader, so the same to the bit. For tests, and
+/// for reading what a marker draw shows.
+pub fn expand_instances(mesh: &[Vertex3D], instances: &[Vertex3D]) -> Vec<Vertex3D> {
+ let mut out = Vec::with_capacity(mesh.len() * instances.len());
+ for i in instances {
+ let [cx, cy, cz] = i.position;
+ let [r, g, b] = i.color;
+ out.extend(mesh.iter().map(|v| Vertex3D {
+ position: [v.position[0] + cx, v.position[1] + cy, v.position[2] + cz],
+ color: [v.color[0] * r, v.color[1] * g, v.color[2] * b],
+ }));
}
out
}
diff --git a/src/main.rs b/src/main.rs
index b9fe369..d15aca3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3057,16 +3057,18 @@ mod tests {
state.show_point_markers = false;
state.show_vertex_markers = false;
state.rebuild_scene_geometry();
- assert!(state.overlay_marker_verts.is_empty());
+ let drawn = |state: &State| state.drawn_markers(crate::app::MarkerKind::Overlay);
+ assert!(drawn(&state).is_empty());
state.run_command("toggle_vertex_markers");
- let built = state.overlay_marker_verts.clone();
+ let built = drawn(&state);
assert!(!built.is_empty() && !state.overlay_vertex_marker_points.is_empty());
+ assert_eq!(built.len(), state.vertex_marker_instances.len() * 240, "a sphere drawn over each instance");
state.point_marker_size *= 2.0;
- state.rebuild_overlay_marker_verts();
- assert_eq!(state.overlay_marker_verts.len(), built.len());
- assert_ne!(state.overlay_marker_verts[0].position, built[0].position, "re-sized");
+ state.rebuild_overlay_markers();
+ assert_eq!(drawn(&state).len(), built.len());
+ assert_ne!(drawn(&state)[0].position, built[0].position, "re-sized");
state.run_command("toggle_vertex_markers");
- assert!(state.overlay_marker_verts.is_empty(), "and gone with the switch");
+ assert!(drawn(&state).is_empty(), "and gone with the switch");
// And the app collects them by its switches, and persists those.
let mut state = State::new(false);
@@ -3423,11 +3425,11 @@ mod tests {
};
let label = crate::command::by_id("toggle_point_markers").unwrap().label;
assert_eq!(row(&state), format!("○ {label}"));
- assert!(state.overlay_marker_verts.is_empty());
+ assert!(state.overlay_marker_instances.is_empty());
state.run_viewport_menu_action(A::Command("toggle_point_markers"));
assert!(state.show_point_markers);
assert_eq!(row(&state), format!("● {label}"));
- assert!(!state.overlay_marker_verts.is_empty(), "the overlay was built");
+ assert!(!state.overlay_marker_instances.is_empty(), "the overlay was built");
assert!(!state.overlay_marker_points.is_empty(), "and its positions kept for re-sizing");
let kdl = fs::read_to_string(crate::app::DesignSettings::file_path()).expect("saved");
assert!(crate::app::DesignSettings::from_kdl_str(&kdl).viewport.show_point_markers, "persisted");
@@ -3551,12 +3553,12 @@ mod tests {
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();
+ let resized: Vec<[f32; 3]> = state.drawn_markers(crate::app::MarkerKind::Overlay).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();
+ let rebuilt: Vec<[f32; 3]> = state.drawn_markers(crate::app::MarkerKind::Overlay).iter().map(|v| v.position).collect();
assert_eq!(resized, rebuilt);
// Off, nothing is kept to re-size.
@@ -3617,9 +3619,9 @@ mod tests {
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();
+ state.rebuild_group_markers();
let radius = |state: &State| {
- state.group_point_verts.iter().map(|v| (v.position[1] - centre[1]).abs()).fold(0.0f32, f32::max)
+ state.drawn_markers(crate::app::MarkerKind::Group).iter().map(|v| (v.position[1] - centre[1]).abs()).fold(0.0f32, f32::max)
};
assert!((radius(&state) - 0.025).abs() < 1e-5);
@@ -4984,6 +4986,56 @@ mod tests {
assert_eq!(markers.len() % 240, 0);
}
+ /// A marker draw instances one white sphere over a marker a point
+ /// (cce-ui's `SceneDraw::instances`). What that draws — each instance's
+ /// position added to the sphere, its colour multiplying the white — is
+ /// the sphere copied to every point as markers were built until
+ /// 2026-10-06, to the bit: the reference below is that code.
+ #[test]
+ fn instanced_markers_draw_what_the_copied_spheres_drew() {
+ use crate::geometry::{expand_instances, marker_instances, marker_sphere, Vertex3D};
+ let reference = |src: &[Vertex3D], size: f32, color: [f32; 3]| -> Vec<Vertex3D> {
+ let mut seen = std::collections::HashSet::new();
+ let mut out = Vec::new();
+ let r = size.max(0.001);
+ let pi = std::f32::consts::PI;
+ for v in src {
+ let key = ((v.position[0] * 1000.0).round() as i32, (v.position[1] * 1000.0).round() as i32, (v.position[2] * 1000.0).round() as i32);
+ if !seen.insert(key) {
+ continue;
+ }
+ let [cx, cy, cz] = v.position;
+ let sp = |theta: f32, phi: f32| [cx + r * theta.sin() * phi.cos(), cy + r * theta.cos(), cz + r * theta.sin() * phi.sin()];
+ for lat in 0..4 {
+ let (t0, t1) = (pi * lat as f32 / 4.0, pi * (lat + 1) as f32 / 4.0);
+ for lon in 0..10 {
+ let (p0, p1) = (2.0 * pi * lon as f32 / 10.0, 2.0 * pi * (lon + 1) as f32 / 10.0);
+ let (p00, p10, p11, p01) = (sp(t0, p0), sp(t1, p0), sp(t1, p1), sp(t0, p1));
+ for position in [p00, p11, p10, p00, p01, p11] {
+ out.push(Vertex3D { position, color });
+ }
+ }
+ }
+ }
+ out
+ };
+ let src: Vec<Vertex3D> = (0..200)
+ .map(|i| {
+ let t = i as f32 * 0.37;
+ Vertex3D { position: [t.sin() * 3.1, (t * 0.7).cos() * -2.3 + 0.001 * (i % 3) as f32, t * 0.05 - 4.0], color: [0.0; 3] }
+ })
+ .collect();
+ for (size, color) in [(0.02f32, [1.0, 0.5, 0.0]), (0.0005, [0.2, 0.9, 0.3]), (0.137, [0.11, 0.22, 0.33])] {
+ let drawn = expand_instances(&marker_sphere(size), &marker_instances(&src, color));
+ let want = reference(&src, size, color);
+ assert_eq!(drawn.len(), want.len());
+ for (a, b) in drawn.iter().zip(&want) {
+ assert_eq!(a.position.map(f32::to_bits), b.position.map(f32::to_bits));
+ assert_eq!(a.color.map(f32::to_bits), b.color.map(f32::to_bits));
+ }
+ }
+ }
+
/// A marker sphere winds counter-clockwise seen from OUTSIDE — the
/// raster fill's culling convention, and what `sphere_detail` does.
/// Until 2026-09-24 `points_vertices` kept the retired soup's inward
@@ -5415,12 +5467,13 @@ mod tests {
crate::render::scene_point_overlays(&geom, false, false, false, 0.02, [1.0, 0.5, 0.0]);
assert!(markers.is_empty() && labels.is_empty() && normals.is_empty());
- // …and all three off the one Detail: 240 marker verts per POINT, one
- // label per point, one whisker pair per point.
+ // …and all three off the one Detail: one marker instance per POINT
+ // (drawn over the marker sphere), one label per point, one whisker
+ // pair per point.
let (markers, labels, normals) =
crate::render::scene_point_overlays(&geom, true, true, true, 0.02, [1.0, 0.5, 0.0]);
assert_eq!(labels.len(), points, "one label per point");
- assert_eq!(markers.len(), points * 240);
+ assert_eq!(markers.len(), points);
assert!(labels.iter().any(|(_, i)| *i > 0));
// The marker color parameter flows into the vertices (linearized).
let expect = cce_ui::colors::to_linear_rgb([1.0, 0.5, 0.0]);
@@ -8797,7 +8850,7 @@ mod tests {
state.current_dir_mut().set_child_geometry_visible(tagged, true);
state.rebuild_scene_geometry();
assert!(state.scene_groups.iter().any(|(n, m)| n == "five" && m.len() == 5), "{:?}", state.scene_groups.iter().map(|(n, m)| (n.clone(), m.len())).collect::<Vec<_>>());
- assert!(state.marked_group_verts.is_empty(), "nothing is marked yet");
+ assert!(state.marked_group_instances.is_empty(), "nothing is marked yet");
// From the palette: the command turns it into the groups list.
state.run_command("command_palette");
@@ -8815,12 +8868,12 @@ mod tests {
assert!(state.dialog_visible(), "a switch is worked in place");
assert_eq!(state.slots.dialog.rows[row].toggle(), Some(true));
assert!(state.group_marked("five"));
- assert!(!state.marked_group_verts.is_empty() && state.marked_groups_dirty, "the markers are staged");
- let one = state.marked_group_verts.len();
+ assert!(!state.marked_group_instances.is_empty() && state.marked_groups_dirty, "the markers are staged");
+ let one = state.marked_group_instances.len();
// On the group's points, at Group Marker Size.
let members: Vec<[f32; 3]> = state.scene_groups.iter().find(|(n, _)| n == "five").unwrap().1.clone();
for m in &members {
- assert!(state.marked_group_verts.iter().any(|v| (0..3).all(|k| (v.position[k] - m[k]).abs() <= state.group_marker_size + 1e-4)), "a marker at {m:?}");
+ assert!(state.marked_group_instances.iter().any(|v| (0..3).all(|k| (v.position[k] - m[k]).abs() <= state.group_marker_size + 1e-4)), "a marker at {m:?}");
}
let kdl = fs::read_to_string(crate::app::DesignSettings::file_path()).expect("saved");
assert_eq!(crate::app::DesignSettings::from_kdl_str(&kdl).viewport.marked_groups, "five", "persisted");
@@ -8828,16 +8881,16 @@ mod tests {
// The markers follow the geometry: a bigger sphere, farther points.
let sphere = state.current_dir().children.iter().position(|c| c.name == "sphere1").unwrap();
- let far = |state: &State| state.marked_group_verts.iter().map(|v| (v.position[0].powi(2) + v.position[2].powi(2)).sqrt()).fold(0.0f32, f32::max);
+ let far = |state: &State| state.marked_group_instances.iter().map(|v| (v.position[0].powi(2) + v.position[2].powi(2)).sqrt()).fold(0.0f32, f32::max);
let before = far(&state);
state.apply_action(McpAction::SetParam { slot: sphere, name: "radius".into(), value: "2.0".into() }, &mut redraw).unwrap();
assert!(far(&state) > before * 1.5, "{} against {before}", far(&state));
- assert_eq!(state.marked_group_verts.len(), one);
+ assert_eq!(state.marked_group_instances.len(), one);
// Enter again unmarks; Escape closes; a query filters the names.
state.dialog_key_input(&key_press(Key::Named(NamedKey::Enter)));
assert!(!state.group_marked("five"));
- assert!(state.marked_group_verts.is_empty());
+ assert!(state.marked_group_instances.is_empty());
state.dialog_key_input(&typed("z"));
assert!(state.slots.dialog.rows.is_empty(), "no group matches");
state.dialog_key_input(&key_press(Key::Named(NamedKey::Escape)));
@@ -8845,7 +8898,7 @@ mod tests {
// A marked name the scene has no group for marks nothing and is kept.
state.set_group_marked("gone", true);
- assert!(state.marked_group_verts.is_empty());
+ assert!(state.marked_group_instances.is_empty());
assert!(state.group_marked("gone"));
}
@@ -18755,7 +18808,7 @@ mod tests {
state.apply_action(McpAction::Select { slot: a }, &mut redraw).unwrap();
state.sync_nodes();
assert_eq!(state.spreadsheet_points.len(), 8, "a box has eight points, a row each");
- assert!(state.row_marker_verts.is_empty());
+ assert!(state.row_marker_instances.is_empty());
let (sx, sy, sw, sh) = state.positions[SPREADSHEET_IDX];
assert!(sw > 0.0 && sh > 60.0, "the spreadsheet is laid out: {sw} x {sh}");
@@ -18769,19 +18822,19 @@ mod tests {
let version = state.rt_geometry_version;
press(&mut state, 1);
assert_eq!(state.selected_spreadsheet_points(), vec![1]);
- assert!(!state.row_marker_verts.is_empty() && state.row_markers_dirty, "the marker is staged");
- let one = state.row_marker_verts.len();
+ assert!(!state.row_marker_instances.is_empty() && state.row_markers_dirty, "the marker is staged");
+ let one = state.row_marker_instances.len();
// The marker stands on the row's point.
let p = state.spreadsheet_points[1];
let n = one as f32;
- let mid = state.row_marker_verts.iter().fold([0.0f32; 3], |m, v| [m[0] + v.position[0] / n, m[1] + v.position[1] / n, m[2] + v.position[2] / n]);
+ let mid = state.row_marker_instances.iter().fold([0.0f32; 3], |m, v| [m[0] + v.position[0] / n, m[1] + v.position[1] / n, m[2] + v.position[2] / n]);
assert!((0..3).all(|k| (mid[k] - p[k]).abs() < 1e-3), "{mid:?} is not at {p:?}");
state.modifiers.ctrl = true;
press(&mut state, 0);
state.modifiers.ctrl = false;
assert_eq!(state.selected_spreadsheet_points(), vec![0, 1]);
- assert_eq!(state.row_marker_verts.len(), 2 * one, "a marker a row");
+ assert_eq!(state.row_marker_instances.len(), 2 * one, "a marker a row");
assert_eq!(state.rt_geometry_version, version, "selecting evaluates nothing");
// A refresh of the same node's table keeps it.
@@ -18789,7 +18842,7 @@ mod tests {
state.sync_nodes();
assert_eq!(state.selected_spreadsheet_points(), vec![0, 1]);
let moved = state.spreadsheet_points[1];
- let mid = state.row_marker_verts[one..].iter().chain(&state.row_marker_verts[..one]).fold([0.0f32; 3], |m, v| [m[0] + v.position[0], m[1] + v.position[1], m[2] + v.position[2]]);
+ let mid = state.row_marker_instances[one..].iter().chain(&state.row_marker_instances[..one]).fold([0.0f32; 3], |m, v| [m[0] + v.position[0], m[1] + v.position[1], m[2] + v.position[2]]);
let both = [moved, state.spreadsheet_points[0]];
let want = [both[0][0] + both[1][0], both[0][1] + both[1][1], both[0][2] + both[1][2]];
assert!((0..3).all(|k| (mid[k] / one as f32 - want[k]).abs() < 1e-2), "the markers followed the points");
@@ -18798,7 +18851,7 @@ mod tests {
state.apply_action(McpAction::Select { slot: b }, &mut redraw).unwrap();
state.sync_nodes();
assert!(state.selected_spreadsheet_points().is_empty());
- assert!(state.row_marker_verts.is_empty());
+ assert!(state.row_marker_instances.is_empty());
}
/// A node INSIDE a simnet is read as the scene draws it there: as the
@@ -18839,10 +18892,10 @@ mod tests {
state.sync_nodes();
assert!(!state.spreadsheet_points.is_empty());
state.spreadsheet_mut().set_selected_rows(&[3]);
- state.rebuild_row_marker_verts();
+ state.rebuild_row_markers();
let middle = |state: &State| {
- let n = state.row_marker_verts.len() as f32;
- state.row_marker_verts.iter().fold(0.0f32, |m, v| m + v.position[0] / n)
+ let n = state.row_marker_instances.len() as f32;
+ state.row_marker_instances.iter().fold(0.0f32, |m, v| m + v.position[0] / n)
};
let (row_at, marker_at) = (state.spreadsheet_points[3][0], middle(&state));
assert!((row_at - marker_at).abs() < 1e-3);
diff --git a/src/render.rs b/src/render.rs
index 4fa5d2b..16f8b63 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -1544,7 +1544,7 @@ impl State {
.iter()
.map(|g| (g.to_string(), geom.points().group_members(g).iter().map(|&p| geom.positions()[p as usize]).collect()))
.collect();
- self.rebuild_marked_group_verts();
+ self.rebuild_marked_group_markers();
// The point overlays ride the same rebuild, off the same `geom`:
// they annotate what is on screen, and what is on screen is exactly
@@ -1557,9 +1557,9 @@ impl State {
self.point_marker_size,
self.point_marker_color,
);
- self.overlay_marker_verts = markers;
+ self.overlay_marker_instances = markers;
// Kept for re-sizing the markers without this evaluation — see
- // `State::rebuild_overlay_marker_verts`.
+ // `State::rebuild_overlay_markers`.
self.overlay_marker_points = if self.show_point_markers {
geom.positions()
.iter()
@@ -1588,10 +1588,10 @@ impl State {
self.overlay_vertex_alpha.clear();
self.overlay_normal_verts.extend(elements.normals);
self.overlay_vertex_marker_points = elements.vertex_markers;
- if !self.overlay_vertex_marker_points.is_empty() {
- // Both marker lists, by the one builder the size slider uses.
- self.rebuild_overlay_marker_verts();
- }
+ self.vertex_marker_instances = crate::geometry::marker_instances(
+ &self.overlay_vertex_marker_points,
+ cce_ui::colors::to_linear_rgb(VERTEX_LABEL_COLOR.map(|c| c as f32 / 255.0)),
+ );
// The wire pass's edges, likewise — topological, and only while the
// wireframe is actually on.
self.scene_edge_verts =
@@ -1658,8 +1658,8 @@ impl State {
}
}
-/// The point overlays on the displayed scene: marker geometry for Show
-/// Point Markers, `(position, index)` labels for Show Point Numbers, and
+/// The point overlays on the displayed scene: marker INSTANCES for Show
+/// Point Markers (one a point, drawn over `geometry::marker_sphere`), `(position, index)` labels for Show Point Numbers, and
/// normal whiskers for Show Point Normals — each read straight off the
/// merged scene `Detail` the geometry rebuild has already produced.
///
@@ -1686,17 +1686,13 @@ pub(crate) fn scene_point_overlays(
let mut normals = Vec::new();
if markers_on {
// One marker per point. The soup emitted one per corner and leaned
- // on points_vertices deduping by position.
+ // on the instances deduping by position.
let src: Vec<crate::geometry::Vertex3D> = geom
.positions()
.iter()
.map(|&position| crate::geometry::Vertex3D { position, color: [0.0; 3] })
.collect();
- markers.extend(crate::geometry::points_vertices(
- &src,
- point_size,
- cce_ui::colors::to_linear_rgb(marker_color),
- ));
+ markers = crate::geometry::marker_instances(&src, cce_ui::colors::to_linear_rgb(marker_color));
}
if normals_on {
// Smooth point normals: for each point, the normalized sum of the
diff --git a/src/vk_smoke.rs b/src/vk_smoke.rs
index 70162f6..cea73d2 100644
--- a/src/vk_smoke.rs
+++ b/src/vk_smoke.rs
@@ -481,9 +481,9 @@ fn main() {
renderer.stage_scene(
pane,
vec![
- SceneDraw { mesh: bg_mesh, mvp: Mat4::IDENTITY.to_cols_array_2d(), wireframe: false, wire_tint: [0.0; 4], opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false },
- SceneDraw { mesh: grid_mesh, mvp, wireframe: false, wire_tint: [0.0; 4], opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false },
- SceneDraw { mesh: cube_mesh, mvp, wireframe: false, wire_tint: [0.0; 4], opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false },
+ SceneDraw { mesh: bg_mesh, mvp: Mat4::IDENTITY.to_cols_array_2d(), wireframe: false, wire_tint: [0.0; 4], opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false, instances: None },
+ SceneDraw { mesh: grid_mesh, mvp, wireframe: false, wire_tint: [0.0; 4], opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false, instances: None },
+ SceneDraw { mesh: cube_mesh, mvp, wireframe: false, wire_tint: [0.0; 4], opacity: 1.0, line_width: 1.0, wire_base_width: 0.0, prelit: false, see_through: false, instances: None },
],
);
}