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

commit6313074291ce4709092f50b2465c69f133d73e94
parent9c92fe729a
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-28 09:56
refactor: remove the triangle soup

geometry::Geometry, GVertex, GAttribute and detail_to_soup were the
migration bridge to the kernel generators, which went native with Phase 7;
nothing spoke the soup afterwards. The grid guide, its last consumer, now
fans box_detail through detail_vertices like every other mesh. The five
wrappers over it (sphere_vertices, sphere_vertices_res, line_vertices,
curve_geometry, points_node_geometry) go with it, and the tests that read
them read a Detail instead. Roadmap notes updated to match.

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

 shapeshifter.md |  11 ++--
 src/detail.rs   |   5 +-
 src/geometry.rs | 183 ++++++++------------------------------------------------
 src/main.rs     |  46 +++++++-------
 4 files changed, 58 insertions(+), 187 deletions(-)

diff --git a/shapeshifter.md b/shapeshifter.md
index 8750af8..d5224dc 100644
--- a/shapeshifter.md
+++ b/shapeshifter.md
@@ -95,9 +95,11 @@ touches none of the geometry work and can be picked up in any gap.
 > works on it, and the pipeline's currency IS a `Detail`: the spreadsheet lists
 > points, the overlays read the point and edge lists, and `weld_points` is gone.
 >
-> What remains: kernel GENERATORS still emit a corner list that welds on the
-> way back, so `detail_to_soup` / `soup_to_detail` and `geometry::Geometry`
-> survive to serve them. Deformers no longer go near a soup (see Phase 1).
+> The soup is gone (2026-09-28). It survived the phase to serve the kernel
+> GENERATORS, which emitted a corner list that welded on the way back; with
+> those native (Phase 7) nothing spoke it, and `geometry::Geometry`,
+> `detail_to_soup` and the wrappers over it were deleted. The renderer reads
+> a `Detail` through `detail_vertices` and nothing else.
 
 Replace the vertex list with **points, vertices, primitives and detail**, each
 carrying its own columnar attribute arrays — one `Vec<f32>` per named attribute
@@ -147,7 +149,8 @@ Spreadsheet, the meta overlays, `project.rs`.
 > reading its parameter names suggest — accumulate, and discharge to the
 > neighbours on crossing a threshold. Confirm or redirect it.
 >
-> Outstanding: the generator ABI, still a corner list out.
+> The generator ABI question closed with Phase 7: the generators are native
+> and the corner list is gone (see Phase 0).
 
 Widen the kernel ABI from `(in_pos, in_col, out_pos, out_col, params)` to
 **named attribute buffers bound by the node**, plus the topology arrays as
diff --git a/src/detail.rs b/src/detail.rs
index df2f1bf..4b2dd49 100644
--- a/src/detail.rs
+++ b/src/detail.rs
@@ -1,7 +1,8 @@
 //! The geometry container: points, vertices, primitives and detail.
 //!
-//! This is the Phase 0 replacement for `geometry::Geometry`, the triangle soup
-//! (`Vec<GVertex>`, attributes stored per triangle corner). See
+//! This is the Phase 0 replacement for the triangle soup the app started on
+//! (a `Vec` of corners, attributes stored per corner), gone since 2026-09-28
+//! with the kernel generators that were its last consumer. See
 //! `shapeshifter.md` for why: every attribute operator worth having is a
 //! statement about a point *and its neighbours*, and a soup has no points, no
 //! edges, and no identity that survives a frame.
diff --git a/src/geometry.rs b/src/geometry.rs
index 18b2288..daa942c 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -1,5 +1,4 @@
 use crate::app::{FsNode, ParamDef};
-use std::collections::HashMap;
 use glam::Vec3;
 use crate::detail::{AttribData, AttribValue, Detail, CD};
 
@@ -26,43 +25,6 @@ impl SimpleRng {
     }
 }
 
-#[derive(Clone, Debug, PartialEq)]
-pub enum GAttribute {
-    Float(f32),
-    Float2([f32; 2]),
-    Float3([f32; 3]),
-    Float4([f32; 4]),
-}
-
-#[derive(Clone, Debug)]
-pub struct GVertex {
-    pub pos: [f32; 3],
-    pub col: [f32; 3],
-    pub attributes: HashMap<String, GAttribute>,
-}
-
-#[derive(Clone, Debug, Default)]
-pub struct Geometry {
-    pub vertices: Vec<GVertex>,
-}
-
-impl Geometry {
-    pub fn new() -> Self {
-        Geometry { vertices: Vec::new() }
-    }
-
-    pub fn merge(&mut self, other: Geometry) {
-        self.vertices.extend(other.vertices);
-    }
-
-    pub fn to_vertex3d_vec(&self) -> Vec<Vertex3D> {
-        self.vertices.iter().map(|v| Vertex3D {
-            position: v.pos,
-            color: v.col,
-        }).collect()
-    }
-}
-
 #[repr(C)]
 #[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
 pub struct Vertex3D {
@@ -185,64 +147,6 @@ pub fn smooth_lit_vertices(d: &Detail) -> Vec<Vertex3D> {
     out
 }
 
-/// Fan-triangulate a [`Detail`] back into the triangle soup the evaluation
-/// pipeline still speaks, carrying attributes onto every corner.
-///
-/// **This is the migration bridge, and it is meant to die.** Generators build
-/// real geometry now; the resolvers, the spreadsheet and the kernel launcher
-/// have not been converted yet, so each generator's public entry point still
-/// hands them a soup. When the pipeline's currency becomes `Detail`, this
-/// function and the adapters calling it go with it.
-///
-/// Point and vertex attributes both land on the corner, vertex winning a name
-/// clash — a vertex attribute is by definition the more specific answer for
-/// that corner. `Cd` is dropped from the attribute map because the soup keeps
-/// color in its own field. Integers widen to floats, the soup's `GAttribute`
-/// having no integer case; nothing round-trips back through here, so the
-/// narrowing is one-way and harmless.
-pub fn detail_to_soup(d: &Detail) -> Geometry {
-    let point_attrs: Vec<&str> = d.points().names().into_iter().filter(|n| *n != CD).collect();
-    let vert_attrs = d.verts().names();
-
-    let mut vertices = Vec::new();
-    for prim in 0..d.num_prims() {
-        let verts = d.prim_verts(prim);
-        let pts = d.prim_points(prim);
-        if pts.len() < 3 {
-            continue;
-        }
-        for i in 1..pts.len() - 1 {
-            for corner in [0, i, i + 1] {
-                let p = pts[corner] as usize;
-                let v = verts.start + corner;
-                let mut attributes = HashMap::new();
-                for name in &point_attrs {
-                    if let Some(val) = d.points().value(name, p) {
-                        attributes.insert(name.to_string(), soup_attr(val));
-                    }
-                }
-                for name in &vert_attrs {
-                    if let Some(val) = d.verts().value(name, v) {
-                        attributes.insert(name.to_string(), soup_attr(val));
-                    }
-                }
-                vertices.push(GVertex { pos: d.positions()[p], col: d.color(p), attributes });
-            }
-        }
-    }
-    Geometry { vertices }
-}
-
-fn soup_attr(v: AttribValue) -> GAttribute {
-    match v {
-        AttribValue::Float(x) => GAttribute::Float(x),
-        AttribValue::Float2(x) => GAttribute::Float2(x),
-        AttribValue::Float3(x) => GAttribute::Float3(x),
-        AttribValue::Float4(x) => GAttribute::Float4(x),
-        AttribValue::Int(x) => GAttribute::Float(x as f32),
-    }
-}
-
 /// A UV sphere as shared points and quads.
 ///
 /// The poles are ONE point each, not a ring of coincident copies, and the
@@ -329,14 +233,6 @@ pub fn sphere_detail(center: Vec3, radius: f32, lat_steps: usize, lon_steps: usi
     d
 }
 
-pub fn sphere_vertices_res(center: Vec3, radius: f32, lat_steps: usize, lon_steps: usize) -> Geometry {
-    detail_to_soup(&sphere_detail(center, radius, lat_steps, lon_steps))
-}
-
-pub fn sphere_vertices(center: Vec3, radius: f32) -> Geometry {
-    sphere_vertices_res(center, radius, 16, 24)
-}
-
 fn sphere_point(center: Vec3, radius: f32, theta: f32, phi: f32) -> Vec3 {
     center + Vec3::new(
         radius * theta.sin() * phi.cos(),
@@ -414,10 +310,6 @@ pub fn box_detail(start: Vec3, end: Vec3, thickness: f32) -> Detail {
     d
 }
 
-pub fn line_vertices(start: Vec3, end: Vec3, thickness: f32) -> Geometry {
-    detail_to_soup(&box_detail(start, end, thickness))
-}
-
 /// Parse a curve node's "Points" param: control points as `x y z` triples
 /// separated by `;`. Commas are accepted alongside whitespace inside a
 /// triple; chunks that don't yield exactly three numbers are skipped, so a
@@ -482,7 +374,7 @@ pub fn sample_catmull_rom(pts: &[Vec3], segs: usize) -> Vec<Vec3> {
 }
 
 /// The native `curve` node: a Catmull-Rom strip through the "Points" param,
-/// each sampled span an oriented box via [`line_vertices`]. Points are
+/// each sampled span an oriented box via [`box_detail`]. Points are
 /// absolute world coordinates — deliberately not offset by the grid index
 /// the other primitives use, because the curve viewer state edits them in
 /// world space.
@@ -504,10 +396,6 @@ pub fn curve_detail(node: &FsNode) -> Detail {
     d
 }
 
-pub fn curve_geometry(node: &FsNode) -> Geometry {
-    detail_to_soup(&curve_detail(node))
-}
-
 fn find_param<'a>(node: &'a FsNode, name: &str) -> Option<&'a ParamDef> {
     node.params.iter().find(|p| p.name.eq_ignore_ascii_case(name))
 }
@@ -5287,14 +5175,6 @@ pub fn network_sphere_vertices_with_errors(
     out
 }
 
-/// The Points node's cloud (type "points", nee "add"): `Points` markers
-/// arranged by the `Shape` param around `center`. One function for both
-/// consumers — the single-node resolver and the scene walk — so the two
-/// renderings can never drift apart.
-pub fn points_node_geometry(node: &FsNode, center: Vec3) -> Geometry {
-    detail_to_soup(&points_detail(node, center))
-}
-
 /// The Points node: a marker sphere at each generated location.
 ///
 /// Every marker stays its own piece — `merge` reallocates identities, so two
@@ -5612,36 +5492,32 @@ pub fn camera_pivot_vertices(scale: f32) -> Vec<Vertex3D> {
 pub fn grid_vertices(thickness: f32, color: [f32; 3]) -> Vec<Vertex3D> {
     let range = 4.0;
     let step = 1.0;
-    let mut geom = Geometry::new();
+    let mut verts = Vec::new();
+
+    let mut bar = |start: Vec3, end: Vec3| {
+        verts.extend(
+            detail_vertices(&box_detail(start, end, thickness))
+                .into_iter()
+                .map(|v| Vertex3D { position: v.position, color }),
+        );
+    };
 
     let mut z = -range;
     while z <= range {
-        let start = Vec3::new(-range, 0.0, z);
-        let end = Vec3::new(range, 0.0, z);
-        let mut line_geom = line_vertices(start, end, thickness);
-        for v in &mut line_geom.vertices {
-            v.col = color;
-        }
-        geom.merge(line_geom);
+        bar(Vec3::new(-range, 0.0, z), Vec3::new(range, 0.0, z));
         z += step;
     }
 
     let mut x = -range;
     while x <= range {
-        let start = Vec3::new(x, 0.0, -range);
-        let end = Vec3::new(x, 0.0, range);
-        let mut line_geom = line_vertices(start, end, thickness);
-        for v in &mut line_geom.vertices {
-            v.col = color;
-        }
-        geom.merge(line_geom);
+        bar(Vec3::new(x, 0.0, -range), Vec3::new(x, 0.0, range));
         x += step;
     }
 
-    geom.to_vertex3d_vec()
+    verts
 }
 
-/// How many soup vertices a welded UV sphere fans out to: two pole bands of
+/// How many triangle corners a welded UV sphere fans out to: two pole bands of
 /// triangles, `lat_steps - 2` bands of quads, three vertices per triangle.
 ///
 /// Spelled out because it is no longer `lat_steps * lon_steps * 6` — the two
@@ -5839,10 +5715,9 @@ mod tests {
     fn test_welded_sphere_reproduces_the_soup_it_replaced() {
         let (center, radius, lat, lon) = (Vec3::new(0.1, 0.2, 0.3), 0.7, 16, 24);
         let before = sphere_soup_before_welding(center, radius, lat, lon);
-        let after: Vec<[f32; 3]> = sphere_vertices_res(center, radius, lat, lon)
-            .vertices
+        let after: Vec<[f32; 3]> = detail_vertices(&sphere_detail(center, radius, lat, lon))
             .iter()
-            .map(|v| v.pos)
+            .map(|v| v.position)
             .collect();
 
         // Drop the triangles the old generator emitted with two corners in the
@@ -5964,14 +5839,8 @@ mod tests {
             "the faces meeting at a corner disagree: {corner_0_normals:?}"
         );
 
-        // The soup adapter still hands every corner both attributes, which is
-        // what the rest of the pipeline still reads.
-        let soup = line_vertices(Vec3::ZERO, Vec3::Y, 0.02);
-        assert_eq!(soup.vertices.len(), 36);
-        assert!(soup
-            .vertices
-            .iter()
-            .all(|v| v.attributes.contains_key("Norm") && v.attributes.contains_key("UV")));
+        // Fanned for the renderer, the six quads are twelve triangles.
+        assert_eq!(detail_vertices(&d).len(), 36);
     }
 
     #[test]
@@ -5996,7 +5865,7 @@ mod tests {
         // node has never made.
         assert_eq!(d.num_prims(), 12, "two boxes of six faces");
         assert_eq!(d.num_points(), 16, "eight corners each, nothing shared");
-        assert_eq!(detail_to_soup(&d).vertices.len(), 72);
+        assert_eq!(detail_vertices(&d).len(), 72);
 
         // Every point still has its own identity across the merge.
         let mut ids = d.ids().to_vec();
@@ -6037,21 +5906,21 @@ mod tests {
 
         // Shape "None": every point sits in the same spot, so all five marker
         // spheres cover an identical (tiny) extent. A spread shape must not.
-        let extent = |g: &Geometry| {
+        let extent = |g: &Detail| {
             let (mut min, mut max) = (Vec3::splat(f32::MAX), Vec3::splat(f32::MIN));
-            for v in &g.vertices {
-                min = min.min(Vec3::from_array(v.pos));
-                max = max.max(Vec3::from_array(v.pos));
+            for &p in g.positions() {
+                min = min.min(Vec3::from_array(p));
+                max = max.max(Vec3::from_array(p));
             }
             max - min
         };
-        let none = points_node_geometry(&points_node("None"), Vec3::ZERO);
+        let none = points_detail(&points_node("None"), Vec3::ZERO);
         let e = extent(&none);
         assert!(e.length() < 0.1, "None must collapse to one spot, extent {e:?}");
 
         for shape in ["Spiral", "Line", "Circle", "Grid"] {
-            let g = points_node_geometry(&points_node(shape), Vec3::ZERO);
-            assert_eq!(g.vertices.len(), 5 * super::sphere_soup_len(6, 8), "{shape}");
+            let g = points_detail(&points_node(shape), Vec3::ZERO);
+            assert_eq!(g.num_points(), 5 * super::sphere_point_len(6, 8), "{shape}");
             assert!(
                 extent(&g).length() > 0.3,
                 "{shape} must spread its points, extent {:?}",
diff --git a/src/main.rs b/src/main.rs
index 6b9ecc5..35d9b35 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -134,7 +134,7 @@ mod tests {
     use crate::app::{get_next_visible_pane, DesignSettings, FsNode, Project, ProjectViewState};
     use crate::slots::{LEFT_MENUBAR_IDX, RIGHT_MENUBAR_IDX, PARAM_MENUBAR_IDX, SPREADSHEET_MENUBAR_IDX};
     use crate::shortcut::{Shortcut, ShortcutManager, Action};
-    use crate::geometry::line_vertices;
+    use crate::geometry::{box_detail, detail_vertices};
     use crate::detail::{AttribData, AttribKind, AttribType, AttribValue, Class, Detail};
 
     /// The choosers open in the loaded project's parent — the "current view" —
@@ -2673,7 +2673,7 @@ mod tests {
     fn test_template_meshes_wind_ccw_outward() {
         let templates_root = crate::app::load_fs_tree();
         // Winding is a property of triangles, so this one flattens on purpose.
-        let eval_template = |name: &str| -> crate::geometry::Geometry {
+        let eval_template = |name: &str| -> Vec<crate::geometry::Vertex3D> {
             let t = templates_root
                 .children
                 .iter()
@@ -2706,12 +2706,12 @@ mod tests {
             )
             .expect("geometry");
             assert!(err.is_none(), "{name}: {err:?}");
-            crate::geometry::detail_to_soup(&g)
+            crate::geometry::detail_vertices(&g)
         };
-        let tri_cross = |g: &crate::geometry::Geometry, tri: usize| -> [f32; 3] {
-            let a = g.vertices[tri * 3].pos;
-            let b = g.vertices[tri * 3 + 1].pos;
-            let d = g.vertices[tri * 3 + 2].pos;
+        let tri_cross = |g: &[crate::geometry::Vertex3D], tri: usize| -> [f32; 3] {
+            let a = g[tri * 3].position;
+            let b = g[tri * 3 + 1].position;
+            let d = g[tri * 3 + 2].position;
             let e1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
             let e2 = [d[0] - a[0], d[1] - a[1], d[2] - a[2]];
             [
@@ -2725,19 +2725,19 @@ mod tests {
         // the mesh center) on effectively every non-degenerate triangle.
         for name in ["Sphere", "Box"] {
             let g = eval_template(name);
-            let n = g.vertices.len() as f32;
+            let n = g.len() as f32;
             let mut c = [0.0f32; 3];
-            for v in &g.vertices {
+            for v in &g {
                 for k in 0..3 {
-                    c[k] += v.pos[k] / n;
+                    c[k] += v.position[k] / n;
                 }
             }
             let (mut outward, mut total) = (0usize, 0usize);
-            for tri in 0..g.vertices.len() / 3 {
+            for tri in 0..g.len() / 3 {
                 let nrm = tri_cross(&g, tri);
-                let a = g.vertices[tri * 3].pos;
-                let b = g.vertices[tri * 3 + 1].pos;
-                let d = g.vertices[tri * 3 + 2].pos;
+                let a = g[tri * 3].position;
+                let b = g[tri * 3 + 1].position;
+                let d = g[tri * 3 + 2].position;
                 let cen = [
                     (a[0] + b[0] + d[0]) / 3.0 - c[0],
                     (a[1] + b[1] + d[1]) / 3.0 - c[1],
@@ -2762,7 +2762,7 @@ mod tests {
         // The plane's visible face is UP: the winding cross must point +Y.
         let g = eval_template("Plane");
         let (mut up, mut total) = (0usize, 0usize);
-        for tri in 0..g.vertices.len() / 3 {
+        for tri in 0..g.len() / 3 {
             let nrm = tri_cross(&g, tri);
             if nrm[1].abs() > 1e-12 {
                 total += 1;
@@ -4846,16 +4846,14 @@ mod tests {
     fn test_line_geometry_generation() {
         let start = Vec3::new(0.0, 0.0, 0.0);
         let end = Vec3::new(0.0, 1.0, 0.0);
-        let geom = line_vertices(start, end, 0.02);
-        
-        // A box line is 36 soup vertices: 6 faces * 2 triangles * 3 corners.
-        assert_eq!(geom.vertices.len(), 36);
+        let d = box_detail(start, end, 0.02);
 
-        // Every corner still carries Norm and UV through the soup adapter.
-        for v in &geom.vertices {
-            assert!(v.attributes.contains_key("Norm"));
-            assert!(v.attributes.contains_key("UV"));
-        }
+        // A box line fans to 36 renderer vertices: 6 faces * 2 triangles * 3 corners.
+        assert_eq!(detail_vertices(&d).len(), 36);
+
+        // Norm and UV ride the vertices, one per corner.
+        assert!(d.verts().has("Norm"));
+        assert!(d.verts().has("UV"));
     }
 
     /// The reference cube guide is gone, and nothing that used to carry it