From 824310e6117e3b728ba9db67d6127e85a1ab5f25 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 11:47:15 +0000 Subject: [PATCH] fix: brush and library names number repeats without rescanning --- crates/brush/src/lib.rs | 7 +++---- crates/engine/src/cmd/library.rs | 9 ++++++++- crates/engine/src/tests/library.rs | 12 ++++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/brush/src/lib.rs b/crates/brush/src/lib.rs index bc75f09cf..40225c054 100644 --- a/crates/brush/src/lib.rs +++ b/crates/brush/src/lib.rs @@ -781,10 +781,9 @@ pub fn unique_name(lib: &[Brush], base: &str) -> String { return base.to_string(); } // At most `lib.len()` names are taken, so one of the first `lib.len() + 1` candidates is free. - (2..=lib.len() + 2) - .map(|i| format!("{base} {i}")) - .find(|n| !lib.iter().any(|b| &b.name == n)) - .unwrap_or_else(|| format!("{base} {}", lib.len() + 2)) + // Each is looked up in a set: scanning the library per candidate is quadratic in its size. + let taken: std::collections::HashSet<&str> = lib.iter().map(|b| b.name.as_str()).collect(); + (2..=lib.len() + 2).map(|i| format!("{base} {i}")).find(|n| !taken.contains(n.as_str())).unwrap_or_else(|| format!("{base} {}", lib.len() + 2)) } // ---------- geometry ---------- diff --git a/crates/engine/src/cmd/library.rs b/crates/engine/src/cmd/library.rs index 70aa9561f..49865914e 100644 --- a/crates/engine/src/cmd/library.rs +++ b/crates/engine/src/cmd/library.rs @@ -448,6 +448,8 @@ fn clip(name: &mut String) { fn uniquify<'a>(names: impl Iterator, fallback: &str, sep: &str, fold: bool) { let key = |n: &str| if fold { n.to_lowercase() } else { n.to_string() }; let mut seen = HashSet::new(); + // Per repeated name, the number to try next: a run of one name counts on instead of from 2. + let mut next: std::collections::HashMap = std::collections::HashMap::new(); for name in names { clip(name); if name.trim().is_empty() { @@ -455,7 +457,12 @@ fn uniquify<'a>(names: impl Iterator, fallback: &str, sep } if seen.contains(&key(name)) { let base = name.clone(); - *name = (2..).map(|n| format!("{base}{sep}{n}")).find(|n| !seen.contains(&key(n))).unwrap_or_default(); + let n = next.entry(key(&base)).or_insert(2); + while seen.contains(&key(&format!("{base}{sep}{n}"))) { + *n += 1; + } + *name = format!("{base}{sep}{n}"); + *n += 1; } seen.insert(key(name)); } diff --git a/crates/engine/src/tests/library.rs b/crates/engine/src/tests/library.rs index 92c780e29..a9bafc540 100644 --- a/crates/engine/src/tests/library.rs +++ b/crates/engine/src/tests/library.rs @@ -821,6 +821,18 @@ fn imported_libraries_are_made_consistent() { ); } +/// A library of one name repeated as often as it may hold numbers the copies in one pass. +#[test] +fn a_library_of_one_repeated_name_imports_numbered() { + let mut s = session(); + let red = json!({"name": "Red", "color": {"model": "rgb", "r": 1, "g": 0, "b": 0}}); + let lib = json!({"name": "Many", "colors": vec![red; 10_000]}); + run(&mut s, "library.import", json!({"data": lib.to_string()})); + let got = run(&mut s, "library.get", json!({})); + let colors = got["colors"].as_array().unwrap(); + assert_eq!((colors.len(), colors[1]["name"].as_str(), colors[9_999]["name"].as_str()), (10_000, Some("Red 2"), Some("Red 10000"))); +} + #[cfg(not(target_arch = "wasm32"))] #[test] fn a_library_is_exported_to_a_file_and_imported_from_it() {