Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor 6391f022af constraints: find the next default parameter name in one pass
next_name tested up to 100,000 candidate names, each against every
parameter and constraint name, so a drawing with many occupied names
made adding one dimensional constraint quadratic. Collect the numeric
suffixes in use once and take the first free one.
2026-11-02 20:36:57 -05:00
6 changed files with 58 additions and 98 deletions
+18 -2
View File
@@ -30,7 +30,7 @@ mod lm;
mod model;
mod residual;
use std::collections::{BTreeMap, HashMap};
use std::collections::{BTreeMap, HashMap, HashSet};
use cadcraft_doc::{Constraint, ConstraintKind, Drawing, GeomRef, Handle};
use cadcraft_geom::Vec2;
@@ -169,10 +169,26 @@ pub fn name_in_use(d: &Drawing, name: &str) -> bool {
d.parametric.parameters.iter().any(|p| p.name == name) || d.constraints.iter().any(|c| c.name == name)
}
/// Highest numbered default name [`next_name`] tries.
const MAX_NAME_SUFFIX: u32 = 100_000;
/// Next free parameter name for a dimensional constraint kind (`d1`, `rad2`, `ang1`, …).
pub fn next_name(d: &Drawing, kind: ConstraintKind) -> String {
let prefix = kind.name_prefix();
(1..=100_000).map(|i| format!("{prefix}{i}")).find(|n| !name_in_use(d, n)).unwrap_or_else(|| format!("{prefix}_x"))
// Collect the suffixes in use in one pass, then take the first free one: checking every
// candidate against every name is quadratic in the number of names (CWE-407). Only a
// suffix spelled as `format!("{prefix}{i}")` spells it counts (`d07` does not take `d7`).
let taken: HashSet<u32> = d
.parametric
.parameters
.iter()
.map(|p| p.name.as_str())
.chain(d.constraints.iter().map(|c| c.name.as_str()))
.filter_map(|n| n.strip_prefix(prefix))
.filter(|s| !s.starts_with('0') && s.bytes().all(|b| b.is_ascii_digit()))
.filter_map(|s| s.parse::<u32>().ok())
.collect();
(1..=MAX_NAME_SUFFIX).find(|i| !taken.contains(i)).map_or_else(|| format!("{prefix}_x"), |i| format!("{prefix}{i}"))
}
/// Next free constraint id.
+27
View File
@@ -489,3 +489,30 @@ fn arc_solved_with_a_negative_radius_keeps_its_points() {
}
}
}
/// The default name is the first free numbered one, found in one pass over many names in use.
#[test]
fn next_name_skips_names_in_use() {
let mut d = Drawing::default();
assert_eq!(next_name(&d, ConstraintKind::Radius), "rad1");
for i in (1..=20_000).filter(|i| *i != 15_000) {
d.parametric.parameters.push(cadcraft_doc::Parameter { name: format!("d{i}"), expr: "1".into(), description: String::new() });
}
assert_eq!(next_name(&d, ConstraintKind::Distance(DistAxis::Aligned)), "d15000");
d.parametric.parameters.push(cadcraft_doc::Parameter { name: "d15000".into(), expr: "1".into(), description: String::new() });
assert_eq!(next_name(&d, ConstraintKind::Distance(DistAxis::Aligned)), "d20001");
// A differently spelled suffix does not occupy the plain name.
let mut d = Drawing::default();
for name in ["d01", "d+1", "d1x", "D1"] {
d.parametric.parameters.push(cadcraft_doc::Parameter { name: name.into(), expr: "1".into(), description: String::new() });
}
assert_eq!(next_name(&d, ConstraintKind::Distance(DistAxis::Aligned)), "d1");
d.constraints.push(Constraint {
id: 1,
kind: ConstraintKind::Distance(DistAxis::Aligned),
refs: Vec::new(),
name: "d1".into(),
expr: "1".into(),
});
assert_eq!(next_name(&d, ConstraintKind::Distance(DistAxis::Aligned)), "d2");
}
+6 -41
View File
@@ -323,9 +323,8 @@ impl<'a> Cursor<'a> {
}
}
/// `xdict`'s `ACAD_ENHANCEDBLOCK` evaluation graph object, when the dictionary has one. Only the
/// dictionary's own tags are read, so asking whether a block is dynamic never walks the graph.
fn evaluation_graph<'a>(objects: &HashMap<Handle, &'a ForeignObject>, xdict: Handle) -> Option<&'a ForeignObject> {
/// The objects `xdict`'s `ACAD_ENHANCEDBLOCK` evaluation graph lists, when the dictionary has one.
fn graph_members<'a>(objects: &HashMap<Handle, &'a ForeignObject>, xdict: Handle) -> Option<Vec<&'a ForeignObject>> {
let dict = objects.get(&xdict)?;
let mut graph = None;
let mut tags = dict.tags.iter();
@@ -335,12 +334,7 @@ fn evaluation_graph<'a>(objects: &HashMap<Handle, &'a ForeignObject>, xdict: Han
break;
}
}
objects.get(&graph?).copied().filter(|o| o.kind.eq_ignore_ascii_case("ACAD_EVALUATION_GRAPH"))
}
/// The objects `xdict`'s `ACAD_ENHANCEDBLOCK` evaluation graph lists, when the dictionary has one.
fn graph_members<'a>(objects: &HashMap<Handle, &'a ForeignObject>, xdict: Handle) -> Option<Vec<&'a ForeignObject>> {
let graph = evaluation_graph(objects, xdict)?;
let graph = objects.get(&graph?).filter(|o| o.kind.eq_ignore_ascii_case("ACAD_EVALUATION_GRAPH"))?;
let members = section(&sections(&graph.tags), "AcDbEvalGraph").iter().filter(|t| t.code == 360).filter_map(|t| Handle::parse_hex(&t.value));
// Deduplicate member handles before resolving them: a graph can list the same member many
// times, and parsing each occurrence into its own owned parameter multiplies a single large
@@ -349,42 +343,13 @@ fn graph_members<'a>(objects: &HashMap<Handle, &'a ForeignObject>, xdict: Handle
Some(members.take(MAX_NODES).filter(|h| seen.insert(*h)).filter_map(|h| objects.get(&h).copied()).collect())
}
/// The kept foreign objects of a drawing by handle. Building it walks every object, so a caller
/// that asks about many blocks (listing them) builds it once and passes it to
/// [`dynamic_block_in`] and [`is_dynamic_block_in`] instead of rebuilding it per block, which
/// multiplies the block count by the object count (CWE-407).
pub struct ObjectIndex<'a>(HashMap<Handle, &'a ForeignObject>);
impl<'a> ObjectIndex<'a> {
pub fn new(d: &'a Drawing) -> Self {
ObjectIndex(d.foreign.objects.iter().map(|o| (o.handle, o)).collect())
}
}
/// The extension dictionary of block `name`'s record, if it has one.
fn block_xdict(d: &Drawing, name: &str) -> Option<Handle> {
d.foreign.records.get(&("BLOCK_RECORD".to_string(), name.trim().to_ascii_uppercase()))?.xdict
}
/// The dynamic block data of block `name` (any case; `*Model_Space` for a drawing that is a
/// block's own drawing), or `None` when it has none.
pub fn dynamic_block(d: &Drawing, name: &str) -> Option<DynamicBlock> {
// A block without an extension dictionary is never dynamic: answer before indexing objects.
block_xdict(d, name)?;
dynamic_block_in(d, &ObjectIndex::new(d), name)
}
/// Whether block `name` is a dynamic block (has an evaluation graph), without reading its
/// parameters and actions; `objects` is the drawing's [`ObjectIndex`].
pub fn is_dynamic_block_in(d: &Drawing, objects: &ObjectIndex<'_>, name: &str) -> bool {
block_xdict(d, name).and_then(|x| evaluation_graph(&objects.0, x)).is_some()
}
/// [`dynamic_block`] with the drawing's [`ObjectIndex`] already built.
pub fn dynamic_block_in(d: &Drawing, objects: &ObjectIndex<'_>, name: &str) -> Option<DynamicBlock> {
let xdict = block_xdict(d, name)?;
let xdict = d.foreign.records.get(&("BLOCK_RECORD".to_string(), name.trim().to_ascii_uppercase()))?.xdict?;
let objects: HashMap<Handle, &ForeignObject> = d.foreign.objects.iter().map(|o| (o.handle, o)).collect();
let mut out = DynamicBlock::default();
for o in graph_members(&objects.0, xdict)? {
for o in graph_members(&objects, xdict)? {
let secs = sections(&o.tags);
let node = Cursor::new(section(&secs, "AcDbEvalExpr")).int(90);
let element = Cursor::new(section(&secs, "AcDbBlockElement")).string(300);
+6 -47
View File
@@ -54,11 +54,7 @@ fn record_handle(d: &Drawing, name: &str) -> Option<Handle> {
/// The object entry `key` of dictionary `dict` refers to.
fn entry(d: &Drawing, dict: Handle, key: &str) -> Option<Handle> {
dictionary_entry(d.foreign.objects.iter().find(|o| o.handle == dict)?, key)
}
/// The object entry `key` (any case) of dictionary object `o` names.
fn dictionary_entry(o: &ForeignObject, key: &str) -> Option<Handle> {
let o = d.foreign.objects.iter().find(|o| o.handle == dict)?;
let mut tags = o.tags.iter();
while let Some(t) = tags.next() {
if t.code == 3 && t.value.eq_ignore_ascii_case(key) {
@@ -70,11 +66,7 @@ fn dictionary_entry(o: &ForeignObject, key: &str) -> Option<Handle> {
/// Every entry of dictionary `dict`: (key, object).
fn entries(d: &Drawing, dict: Handle) -> Vec<(String, Handle)> {
d.foreign.objects.iter().find(|o| o.handle == dict).map(dictionary_entries).unwrap_or_default()
}
/// Every entry of dictionary object `o`: (key, object).
fn dictionary_entries(o: &ForeignObject) -> Vec<(String, Handle)> {
let Some(o) = d.foreign.objects.iter().find(|o| o.handle == dict) else { return Vec::new() };
let mut out = Vec::new();
let mut key: Option<&str> = None;
for t in &o.tags {
@@ -700,33 +692,19 @@ fn refill_hatches(b: &mut Block) {
/// The value records of the references to the dynamic block whose record is `def_record` (from
/// any one of them, by graph id): AutoCAD's layout of each record, class ids included.
///
/// Each record is copied once at most: a key naming a record an earlier key already took is
/// skipped. Records are copied per key (and copied again per key when a new reference's data is
/// made), so a file listing one large record under many keys would otherwise multiply its size by
/// the key count (CWE-400); AutoCAD gives every key its own record.
fn templates(d: &Drawing, def_record: Handle) -> HashMap<String, Vec<RawTag>> {
let objects: HashMap<Handle, &ForeignObject> = d.foreign.objects.iter().map(|o| (o.handle, o)).collect();
// Dictionary entries looked up through the index, not by scanning every object per
// dictionary (which is quadratic in the object count).
let entry = |dict: Handle, key: &str| objects.get(&dict).and_then(|o| dictionary_entry(o, key));
let mut out: HashMap<String, Vec<RawTag>> = HashMap::new();
let mut taken: HashSet<Handle> = HashSet::new();
for rep in d.foreign.objects.iter().filter(|o| o.kind.eq_ignore_ascii_case("DICTIONARY")) {
// A representation dictionary whose data names this dynamic block.
let Some(rd) = dictionary_entry(rep, "AcDbRepData").and_then(|h| objects.get(&h)) else { continue };
let Some(rd) = entry(d, rep.handle, "AcDbRepData").and_then(|h| objects.get(&h)) else { continue };
if !rd.tags.iter().any(|t| t.code == 340 && Handle::parse_hex(&t.value) == Some(def_record)) {
continue;
}
let Some(values) = dictionary_entry(rep, "AppDataCache").and_then(|c| entry(c, "ACAD_ENHANCEDBLOCKDATA")) else { continue };
let Some(values) = objects.get(&values) else { continue };
for (k, h) in dictionary_entries(values) {
if out.contains_key(&k) || taken.contains(&h) {
continue;
}
let Some(values) = entry(d, rep.handle, "AppDataCache").and_then(|c| entry(d, c, "ACAD_ENHANCEDBLOCKDATA")) else { continue };
for (k, h) in entries(d, values) {
if let Some(x) = objects.get(&h).filter(|x| x.kind.eq_ignore_ascii_case("XRECORD")) {
taken.insert(h);
out.insert(k, x.tags.clone());
out.entry(k).or_insert_with(|| x.tags.clone());
}
}
}
@@ -1066,23 +1044,4 @@ mod tests {
assert_eq!(by_tag(&d, &def), vec![(10, Handle(1)), (20, Handle(2))]);
assert_eq!(by_tag(&d, &anon), vec![(20, Handle(5)), (1_000_001, Handle(6))]);
}
#[test]
fn a_value_record_listed_under_many_keys_is_copied_once() {
// One large XRECORD named by many keys of a reference's value dictionary: the templates
// keep one copy, not one per key.
let mut d = Drawing::new_imperial();
let (def_record, rep, rd, cache, values, record) = (Handle(0x10), Handle(0x20), Handle(0x21), Handle(0x22), Handle(0x23), Handle(0x24));
let keys: Vec<String> = (0..1000).map(|i| i.to_string()).collect();
let entries: Vec<(&str, Handle)> = keys.iter().map(|k| (k.as_str(), record)).collect();
let f = std::sync::Arc::make_mut(&mut d.foreign);
f.objects.push(dictionary(rep, Handle(1), &[("AcDbRepData", rd), ("AppDataCache", cache)]));
f.objects.push(ForeignObject { handle: rd, kind: "ACDB_BLOCKREPRESENTATION_DATA".into(), tags: vec![tag(340, def_record.hex())] });
f.objects.push(dictionary(cache, rep, &[("ACAD_ENHANCEDBLOCKDATA", values)]));
f.objects.push(dictionary(values, cache, &entries));
f.objects.push(ForeignObject { handle: record, kind: "XRECORD".into(), tags: (0..1000).map(|_| tag(1, "x")).collect() });
let t = templates(&d, def_record);
assert_eq!(t.len(), 1, "one record, one template");
assert_eq!(t.get("0").map(Vec::len), Some(1000));
}
}
+1 -4
View File
@@ -379,11 +379,8 @@ fn run_base(s: &mut Session, p: &Value) -> Result<Value> {
fn run_list(s: &mut Session, _p: &Value) -> Result<Value> {
let d = s.doc()?;
// One object index for the whole listing: rebuilding it per block multiplies the block count
// by the drawing's kept object count (CWE-407).
let objects = cadcraft_doc::dynblock::ObjectIndex::new(d);
let block = |b: &Block| {
let dynamic = cadcraft_doc::dynblock::is_dynamic_block_in(d, &objects, &b.name);
let dynamic = cadcraft_doc::dynblock::dynamic_block(d, &b.name).is_some();
json!({"name": b.name, "entities": b.entities.len(), "base": [b.base.x, b.base.y], "description": b.description, "dynamic": dynamic})
};
Ok(json!({ "blocks": d.blocks.values().filter(|b| !b.anonymous).map(|b| block(b)).collect::<Vec<_>>() }))
@@ -83,10 +83,6 @@ fn blocks_without_dynamic_data_and_bad_names() {
s.execute("block", &json!({ "name": "Window", "base": [0, 0], "handles": [l] })).unwrap();
assert_eq!(s.execute("blocks.dynamic", &json!({ "name": "Window" })).unwrap(), json!(null));
assert_eq!(s.execute("blocks.dynamic", &json!({ "name": "*Model_Space" })).unwrap(), json!(null));
let list = s.execute("blocks.list", &json!({})).unwrap();
let flags: Vec<_> = list["blocks"].as_array().unwrap().iter().map(|b| (b["name"].as_str().unwrap().to_string(), b["dynamic"].clone())).collect();
assert!(flags.contains(&("Window".into(), json!(false))), "{flags:?}");
assert!(flags.contains(&("Door".into(), json!(true))), "{flags:?}");
for p in [json!({}), json!({ "name": 3 }), json!({ "name": "Nope" }), json!(null)] {
assert!(s.execute("blocks.dynamic", &p).is_err(), "{p}");
}