From eaab849f42da45362c4b9c53ffb48aab790c11ae Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 2 Nov 2026 17:01:29 -0800 Subject: [PATCH] Document Info: count objects that share an id as separate objects The walk that skips objects it has already visited keys them by address instead of id, so two objects that share an id in a damaged file are both counted. An object reached through a selected group and again on its own still counts once. --- crates/engine/src/cmd/docinfo.rs | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/docinfo.rs b/crates/engine/src/cmd/docinfo.rs index 0fafdf7dc..260b981d2 100644 --- a/crates/engine/src/cmd/docinfo.rs +++ b/crates/engine/src/cmd/docinfo.rs @@ -1,7 +1,7 @@ //! Document Info panel data, its categories and the text report (Document Info › Save…, File → //! Package), and Object → Make Pixel Perfect. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; use serde_json::{Value, json}; use vectorcraft_color::Paint; @@ -294,10 +294,11 @@ fn info(s: &mut Session, p: &Value) -> Result { let mut repeats: Vec = vec![]; let mut repeat_rows: Vec<(String, String)> = vec![]; // The objects the walk has visited: a selection can hold a group and objects inside it, and each object counts once. - let mut seen: BTreeSet = BTreeSet::new(); + // The set holds each object's address, so two objects that share an id (as a damaged file can hold) count as two. + let mut seen: HashSet<*const Node> = HashSet::new(); for r in roots { r.walk(&mut |n| { - if !seen.insert(n.id) { + if !seen.insert(std::ptr::from_ref(n)) { return; } let k = match &n.kind { @@ -668,6 +669,29 @@ mod tests { } } + /// Document Info › Objects counts two objects that share an id (as a damaged file can hold) as + /// two, in the whole document and in a selected group that holds both. + #[test] + fn info_counts_objects_that_share_an_id() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap(); + let square = |s: &mut Session, x: f64| { + s.execute("shape.rectangle", &json!({"x": x, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap() + }; + let (a, b) = (square(&mut s, 0.0), square(&mut s, 100.0)); + s.execute("select.set", &json!({"ids": [a, b]})).unwrap(); + let group = s.execute("object.group", &json!({})).unwrap()["id"].clone(); + s.execute("select.set", &json!({"ids": [group]})).unwrap(); + let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc); + d.node_mut(NodeId(b)).unwrap().id = NodeId(a); + for p in [json!({}), json!({"selectionOnly": true})] { + let i = s.execute("document.info", &p).unwrap(); + let o = &i["objects"]; + assert_eq!((o["paths"].as_u64(), o["groups"].as_u64()), (Some(2), Some(1)), "{p}: {o}"); + assert!((i["pathLength"].as_f64().unwrap() - 80.0).abs() < 1e-6, "{p}: {}", i["pathLength"]); + } + } + #[test] fn embedding_permissions_read_from_fs_type() { assert_eq!(embedding_label(0), "embedding allowed");