STEP: bound assembly traversal, name imports after a lone body
A visit budget stops shared sub-assemblies from growing exponentially (with a test); the loose-solid pass gets its own budget. An Import feature is named after the root product, else its only body, else the file. Restores a test function closed off by the rebase. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
d224fde7d4
commit
b8d0fcc8e2
4 files changed
+45
-2
No files matched your search
@@ -380,6 +380,7 @@ fn timeline_edits_reresolve_references() {
|
||||
run(&mut s, "UndoCommand", json!({}));
|
||||
run(&mut s, "UndoCommand", json!({}));
|
||||
assert!(rel(volume(&mut s), 60.0 * 30.0 * 25.0 - f4 - PI * 9.0 * 25.0) < 1e-3);
|
||||
}
|
||||
|
||||
/// A 40 × 30 × 20 box exported to a STEP file in a fresh temp directory.
|
||||
fn step_box_file(tag: &str) -> std::path::PathBuf {
|
||||
|
||||
@@ -61,8 +61,10 @@ pub fn step_import_feature(bytes: &[u8], file: &str) -> Result<StepFeature> {
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or_else(|| "Import".into());
|
||||
let generic = |n: &str| n.trim().is_empty() || n.starts_with('(') || n.eq_ignore_ascii_case("import") || n.eq_ignore_ascii_case("unnamed");
|
||||
let name = match imp.tree.as_slice() {
|
||||
[root] if !generic(&root.name) => root.name.clone(),
|
||||
// The root product's name; else the only body's name; else the file name.
|
||||
let name = match (imp.tree.as_slice(), imp.bodies.as_slice()) {
|
||||
([root], _) if !generic(&root.name) => root.name.clone(),
|
||||
(_, [only]) if !generic(&only.name) => only.name.clone(),
|
||||
_ => stem,
|
||||
};
|
||||
let file_name = std::path::Path::new(file).file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| file.to_string());
|
||||
|
||||
@@ -26,6 +26,9 @@ use p21::{Entity, Exchange, Param};
|
||||
const MAX_ASSEMBLY_DEPTH: usize = 32;
|
||||
/// Most body instances in one import.
|
||||
const MAX_BODIES: usize = 10_000;
|
||||
/// Most product occurrences and mapped representations visited (a shared sub-assembly used
|
||||
/// twice at every level would otherwise grow exponentially).
|
||||
const MAX_VISITS: usize = 100_000;
|
||||
|
||||
/// A body read from a STEP file.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -228,6 +231,8 @@ struct Reader<'a> {
|
||||
used_items: HashSet<u64>,
|
||||
/// Items already instanced for the product being read.
|
||||
node_items: HashSet<u64>,
|
||||
/// Occurrences and mapped representations visited so far.
|
||||
visits: usize,
|
||||
bodies: Vec<ImportedBody>,
|
||||
}
|
||||
|
||||
@@ -417,6 +422,11 @@ impl<'a> Reader<'a> {
|
||||
self.warn("mapped items nested too deeply");
|
||||
return;
|
||||
}
|
||||
self.visits += 1;
|
||||
if self.visits > MAX_VISITS {
|
||||
self.warn(format!("more than {MAX_VISITS} component instances: the rest are left out"));
|
||||
return;
|
||||
}
|
||||
let u = self.units_of(rep);
|
||||
let items = self.reps.get(&rep).map(|r| r.0.clone()).unwrap_or_default();
|
||||
for item in items {
|
||||
@@ -542,6 +552,11 @@ impl<'a> Reader<'a> {
|
||||
self.warn("assembly nested too deeply or recursive: some components were left out");
|
||||
return node;
|
||||
}
|
||||
self.visits += 1;
|
||||
if self.visits > MAX_VISITS {
|
||||
self.warn(format!("more than {MAX_VISITS} component instances: the rest are left out"));
|
||||
return node;
|
||||
}
|
||||
stack.push(pd);
|
||||
let mut path = path.to_vec();
|
||||
path.push(name.clone());
|
||||
@@ -609,6 +624,7 @@ pub fn step_import(text: &str) -> Result<StepImport> {
|
||||
solids: HashMap::new(),
|
||||
used_items: HashSet::new(),
|
||||
node_items: HashSet::new(),
|
||||
visits: 0,
|
||||
bodies: Vec::new(),
|
||||
};
|
||||
rd.index();
|
||||
@@ -642,6 +658,7 @@ pub fn step_import(text: &str) -> Result<StepImport> {
|
||||
if !loose.is_empty() {
|
||||
let mut bodies = Vec::new();
|
||||
rd.node_items = rd.used_items.clone();
|
||||
rd.visits = 0;
|
||||
for r in loose {
|
||||
rd.rep_bodies(r, &identity, &[], "Import", &mut bodies, 0);
|
||||
}
|
||||
|
||||
@@ -283,3 +283,26 @@ fn assembly_cycles_are_bounded() {
|
||||
let imp = r.unwrap();
|
||||
assert_eq!(imp.bodies.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exponential_assemblies_are_bounded() {
|
||||
// P0 uses P1 twice, P1 uses P2 twice, … 30 levels: 2^30 occurrences if followed blindly.
|
||||
let mut t = String::from(HEAD);
|
||||
t += "#8=PRODUCT_DEFINITION_CONTEXT('part definition',#9,'design');\n#9=APPLICATION_CONTEXT('');\n";
|
||||
t += &cube_entities();
|
||||
t += &context(20, "(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.))");
|
||||
let levels = 30u64;
|
||||
for l in 0..=levels {
|
||||
t += &product(1000 + l * 10, &format!("P{l}"));
|
||||
if l < levels {
|
||||
for k in 0..2 {
|
||||
t += &format!("#{}=NEXT_ASSEMBLY_USAGE_OCCURRENCE('','','',#{},#{},$);\n", 2000 + l * 10 + k, 1000 + l * 10, 1000 + (l + 1) * 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
t += &format!("#60=ADVANCED_BREP_SHAPE_REPRESENTATION('',(#101),#20);\n#61=SHAPE_DEFINITION_REPRESENTATION(#{},#60);\n", 1000 + levels * 10 + 4);
|
||||
t += TAIL;
|
||||
let imp = step_import(&t).unwrap();
|
||||
assert!(!imp.bodies.is_empty() && imp.bodies.len() <= MAX_BODIES);
|
||||
assert!(imp.warnings.iter().any(|w| w.contains("left out")), "{:?}", imp.warnings);
|
||||
}
|
||||
Reference in new issue
Block a user