Fix cubic input-name repair when loading large designs

This commit is contained in:
bherbruck committed 2026-10-10 19:04:19 -04:00
1 parent feaed2e3f6
commit e3ee1b359e
4 files changed
+180 -4

No files matched your search

+65 -4
View File
@@ -197,10 +197,7 @@ fn rigid(m: &crate::Mat) -> bool {
/// Feature input names and component occurrences, which format 1 files may predate.
fn normalise(d: &mut Document) -> Result<()> {
let ids: Vec<u64> = d.features.iter().map(|f| f.id).collect();
for id in ids {
d.name_feature_inputs(id);
}
d.name_all_feature_inputs();
let missing: Vec<(u64, u64)> =
d.components.iter().filter(|c| !d.occurrences.iter().any(|o| o.component == c.id)).map(|c| (c.id, c.parent)).collect();
for (c, p) in missing {
@@ -213,6 +210,70 @@ fn normalise(d: &mut Document) -> Result<()> {
mod tests {
use super::*;
#[test]
fn bulk_input_repair_preserves_existing_names_and_reuses_surplus_names() {
let mut d = Document::new("input repair");
d.set_param("d2", "1", None, None).unwrap();
for i in 0..5 {
d.add_feature(
crate::FeatureKind::Box {
corner: ["0".into(), "0".into(), "0".into()],
length: "10".into(),
width: "10".into(),
height: "10".into(),
operation: crate::Operation::NewBody,
},
Some(&format!("Box{i}")),
)
.unwrap();
}
d.features[0].param_names = vec!["".into(), "custom".into(), "".into(), "d1".into(), "d2".into()];
d.features[1].param_names.clear();
d.features[2].param_names = vec!["d7".into(), "".into(), "d9".into()];
d.features[3].param_names = vec!["d7".into(), "d11".into(), "d12".into(), "d1".into()];
d.features[4].param_names.clear();
let mut expected = d.clone();
for id in expected.features.iter().map(|f| f.id).collect::<Vec<_>>() {
expected.name_feature_inputs(id);
}
d.name_all_feature_inputs();
assert_eq!(d, expected);
assert_eq!(read(&d.to_json()).unwrap(), d);
}
#[test]
fn large_unnamed_timeline_loads_with_unique_inputs() {
let mut d = Document::new("large input repair");
let id = d
.add_feature(
crate::FeatureKind::Box {
corner: ["0".into(), "0".into(), "0".into()],
length: "10".into(),
width: "10".into(),
height: "10".into(),
operation: crate::Operation::NewBody,
},
None,
)
.unwrap();
let mut f = d.feature(id).unwrap().clone();
f.param_names.clear();
d.features = (1..=5000)
.map(|id| {
let mut f = f.clone();
f.id = id;
f
})
.collect();
d.next_id = 5001;
let back = read(&d.to_json()).unwrap();
let names: Vec<_> = back.features.iter().flat_map(|f| &f.param_names).collect();
assert_eq!(names.len(), 15000);
assert_eq!(names.first().unwrap().as_str(), "d1");
assert_eq!(names.last().unwrap().as_str(), "d15000");
assert_eq!(names.iter().collect::<std::collections::BTreeSet<_>>().len(), names.len());
}
#[test]
fn versions() {
assert_eq!(version_of("solvecraft/1"), Some(1));
+49
View File
@@ -398,6 +398,55 @@ impl Document {
}
}
/// Repair a loaded timeline in one pass. Reserve even later features' names, just as
/// `name_feature_inputs` does, without restarting a document scan for every `dN` candidate.
pub(crate) fn name_all_feature_inputs(&mut self) {
let mut taken: BTreeMap<String, usize> = BTreeMap::new();
for name in self.params.iter().map(|p| &p.name).chain(self.features.iter().flat_map(|f| &f.param_names)) {
*taken.entry(name.clone()).or_default() += 1;
}
let mut next = 1usize;
for f in &mut self.features {
let n = f.kind.inputs().len();
for k in 0..n {
if f.param_names.get(k).is_some_and(|name| !name.is_empty()) {
continue;
}
let name = loop {
let name = format!("d{next}");
next += 1;
if !taken.contains_key(&name) {
break name;
}
};
if f.param_names.len() <= k {
f.param_names.resize(k + 1, String::new());
}
// Empty slots cannot collide with a generated name.
if let Some(slot) = f.param_names.get_mut(k) {
*slot = name.clone();
}
taken.insert(name, 1);
}
// Match the single-feature repair: surplus names become available only after
// this feature has been repaired. Counts preserve names shared with parameters.
for name in f.param_names.drain(n..) {
if let Some(count) = taken.get_mut(&name) {
*count -= 1;
if *count == 0 {
taken.remove(&name);
if let Some(i) = name.strip_prefix('d').and_then(|s| s.parse::<usize>().ok())
&& name == format!("d{i}")
&& i > 0
{
next = next.min(i);
}
}
}
}
}
}
/// Change a parameter: a stored one, or a feature input by its name.
/// A typed length that is a bare number means the design's units: it gets that unit written
/// in (feature inputs are stored in mm otherwise). Anything else is returned unchanged.