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

commitf925b3c846a650b66ffd0d7b97f196e7a406fd26
parentb59e606843
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-29 21:30
feat: the spreadsheet shows the point groups first, as 1 and 0

They were g: columns after every attribute, off the right of the pane,
and blank for a point outside the group.

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

 CLAUDE.md   |  9 +++++++++
 src/app.rs  | 31 ++++++++++++++++---------------
 src/main.rs | 25 ++++++++++++++-----------
 3 files changed, 39 insertions(+), 26 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 9136ba0..69b7f48 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1682,6 +1682,15 @@ wears and at Group Marker Size.
 
 `selected_spreadsheet_rows_are_marked_in_the_scene` drives it by pointer.
 
+**The point groups are the spreadsheet's first columns** after the
+point's number (`geometry_to_spreadsheet_data`, since 2026-09-29):
+`group:<name>`, 1 for a member and 0 for the rest. They were `g:` columns
+after every attribute, the thirteenth column of a sphere's table and off
+the right of any pane, and blank for a point outside the group — so a
+group of one point among five hundred was a column that looked empty.
+Sorting the column descending brings the members to the top. Only POINT
+groups: a row is a point, and the table has no primitive rows.
+
 **What reads a selected node for display reads it as the scene shows it**
 (`geometry::node_geometry_as_shown`, the same day): the spreadsheet's
 rows, the markers on them and the selected group's. A node inside a
diff --git a/src/app.rs b/src/app.rs
index abd92da..32a9e22 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -5883,15 +5883,24 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
     // 362 places — and the row number meant nothing a user could point at.
     // It is now the point index, which is also what the Point Numbers overlay
     // draws.
-    let mut headers = vec![
-        "Point".to_string(),
+    // The groups stand FIRST after the point's number, a column each,
+    // `group:<name>`, 1 for a member and 0 for the rest. Until 2026-09-29
+    // they were `g:` columns after every attribute — the thirteenth column
+    // of a sphere's table, off the right of any pane — and blank for a
+    // point not in the group, so a group of one point among five hundred
+    // was a column that looked empty. Sorting the column, descending,
+    // brings the members to the top.
+    let groups = geom.points().group_names();
+    let mut headers = vec!["Point".to_string()];
+    headers.extend(groups.iter().map(|g| format!("group:{}", g)));
+    headers.extend([
         "Pos.x".to_string(),
         "Pos.y".to_string(),
         "Pos.z".to_string(),
         "Col.r".to_string(),
         "Col.g".to_string(),
         "Col.b".to_string(),
-    ];
+    ]);
 
     // Columnar storage means the columns are known up front, from the store
     // rather than from a scan of every element's map. `names()` is sorted, so
@@ -5926,11 +5935,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
         }
     }
 
-    let groups = geom.points().group_names();
-    for g in &groups {
-        headers.push(format!("g:{}", g));
-    }
-
     // Detail attributes ride along as `d:` columns, constant down the table —
     // which is what a detail attribute IS. Without this the Analysis node
     // would write its answers somewhere nothing could show them.
@@ -5956,15 +5960,16 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
     for p in 0..geom.num_points() {
         let pos = geom.positions()[p];
         let col = geom.color(p);
-        let mut row = vec![
-            p.to_string(),
+        let mut row = vec![p.to_string()];
+        row.extend(groups.iter().map(|g| if geom.points().in_group(g, p) { "1" } else { "0" }.to_string()));
+        row.extend([
             format!("{:.4}", pos[0]),
             format!("{:.4}", pos[1]),
             format!("{:.4}", pos[2]),
             format!("{:.4}", col[0]),
             format!("{:.4}", col[1]),
             format!("{:.4}", col[2]),
-        ];
+        ]);
 
         for (name, ty) in &attribs {
             // A column covers its whole class, so there is no "this element
@@ -5985,10 +5990,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             }
         }
 
-        for g in &groups {
-            row.push(if geom.points().in_group(g, p) { "1".to_string() } else { String::new() });
-        }
-
         for (name, ty) in &detail {
             match geom.detail().value(name, 0) {
                 Some(crate::detail::AttribValue::Float(f)) => row.push(format!("{:.4}", f)),
diff --git a/src/main.rs b/src/main.rs
index 660b748..e7592d7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5248,24 +5248,27 @@ mod tests {
         assert_eq!(
             headers,
             vec![
-                "Point", "Pos.x", "Pos.y", "Pos.z", "Col.r", "Col.g", "Col.b",
-                "ID", "UV.x", "UV.y", "g:pinned", "d:mass_max~",
+                "Point", "group:pinned", "Pos.x", "Pos.y", "Pos.z", "Col.r", "Col.g", "Col.b",
+                "ID", "UV.x", "UV.y", "d:mass_max~",
             ]
         );
 
         assert_eq!(rows.len(), 2, "one row per point");
+        assert!(rows.iter().all(|r| r.len() == headers.len()), "a cell a column");
         assert_eq!(rows[0][0], "0");
-        assert_eq!(rows[0][1], "1.0000"); // Pos.x
-        assert_eq!(rows[0][4], "1.0000"); // Col.r
-        assert_eq!(rows[0][7], "42"); // ID, an integer and printed as one
-        assert_eq!(rows[0][8], "0.1000"); // UV.x
-        assert_eq!(rows[0][10], "", "point 0 is not in the group");
+        // The groups stand beside the point's number, where they are seen
+        // without scrolling, and say 0 as plainly as 1.
+        assert_eq!(rows[0][1], "0", "point 0 is not in the group");
+        assert_eq!(rows[0][2], "1.0000"); // Pos.x
+        assert_eq!(rows[0][5], "1.0000"); // Col.r
+        assert_eq!(rows[0][8], "42"); // ID, an integer and printed as one
+        assert_eq!(rows[0][9], "0.1000"); // UV.x
 
         assert_eq!(rows[1][0], "1");
-        assert_eq!(rows[1][1], "4.0000");
-        assert_eq!(rows[1][7], "0", "unwritten is the type's zero, not a dash");
-        assert_eq!(rows[1][9], "0.4000"); // UV.y
-        assert_eq!(rows[1][10], "1", "point 1 is in the group");
+        assert_eq!(rows[1][1], "1", "point 1 is in the group");
+        assert_eq!(rows[1][2], "4.0000");
+        assert_eq!(rows[1][8], "0", "unwritten is the type's zero, not a dash");
+        assert_eq!(rows[1][10], "0.4000"); // UV.y
         assert_eq!(rows[0][11], "9.5000");
         assert_eq!(rows[1][11], "9.5000", "a detail value repeats down the column");
         // The trailing ~ says this one resets at every step boundary.