.ai import: Objects on Path records measure each group once

Each Objects on Path record measured the bounds of every object it names,
and checked its path, before it was turned down. A thousand copies of a
record whose object is a group of 20,000 paths took 5.6 s to open instead
of about 50 ms. A record now checks its group and ids first, and the path
and objects of a group are measured for one record at most.
This commit is contained in:
dotCooCoo
2026-11-02 12:03:56 +07:00
committed by bflatastic
parent bb354fc92a
commit 9596130be4
2 changed files with 71 additions and 11 deletions
+23 -11
View File
@@ -131,13 +131,15 @@ pub(super) fn apply(doc: &mut Document, found: &OnPath) {
// Where every object with an id is, found in one walk of the tree (setting a record doesn't
// move anything).
let at = doc.index_paths(found.tags.keys().copied());
// The groups whose path and objects have been measured.
let mut tried = HashSet::new();
for r in &found.records {
// A record that doesn't match leaves the group as it was read.
let _ = apply_one(doc, found, &at, r);
let _ = apply_one(doc, found, &at, &mut tried, r);
}
}
fn apply_one(doc: &mut Document, found: &OnPath, at: &HashMap<NodeId, Vec<usize>>, r: &Record) -> Option<()> {
fn apply_one(doc: &mut Document, found: &OnPath, at: &HashMap<NodeId, Vec<usize>>, tried: &mut HashSet<NodeId>, r: &Record) -> Option<()> {
// The one object still in the document with the id `uuid` (a commented copy may have gone).
let resolve = |uuid: &str| -> Option<(NodeId, &Vec<usize>)> {
let mut live = found.ids.get(uuid)?.iter().filter_map(|id| at.get(id).map(|p| (*id, p)));
@@ -150,26 +152,36 @@ fn apply_one(doc: &mut Document, found: &OnPath, at: &HashMap<NodeId, Vec<usize>
return None;
}
let (_, group_at) = path_at.split_last()?;
let g = doc.node_at(group_at)?;
let NodeKind::Group { children, clip: false } = &g.kind else { return None };
if g.extras().objects_on_path.is_some() || children.len() != r.ids.len() {
return None;
}
let mut seen = HashSet::from([path]);
let mut members = Vec::with_capacity(rest.len());
let mut placed = Vec::with_capacity(rest.len());
for (i, uuid) in rest.iter().enumerate() {
let (id, place) = resolve(uuid)?;
let tag = found.tags.get(&id)?;
if !seen.insert(id) || place.split_last().map(|(_, up)| up) != Some(group_at) || tag.key_path || tag.order != Some(i as f64) {
return None;
}
// The frame the object is drawn in: turned by its angle (the document's y points down)
// about the center of its bounds.
let angle = -tag.angle?;
let center = doc.node_at(place)?.geometric_bounds()?.center();
members.push(Member { id, frame: Affine::rotate_about(angle, center), angle });
placed.push((id, place, -tag.angle?));
}
let g = doc.node_at(group_at)?;
let NodeKind::Group { children, clip: false } = &g.kind else { return None };
if g.extras().objects_on_path.is_some() || children.len() != seen.len() || children.iter().any(|c| !seen.contains(&c.id)) {
if children.iter().any(|c| !seen.contains(&c.id)) {
return None;
}
// What is left depends on the group's objects alone, so each group is measured once.
if !tried.insert(g.id) {
return None;
}
attachable_path(children.iter().find(|c| c.id == path)?)?;
let mut members = Vec::with_capacity(placed.len());
for (id, place, angle) in placed {
// The frame the object is drawn in: turned by its angle (the document's y points down)
// about the center of its bounds.
let center = doc.node_at(place)?.geometric_bounds()?.center();
members.push(Member { id, frame: Affine::rotate_about(angle, center), angle });
}
let on_path = ObjectsOnPath { path, members, pivot: r.pivot, rotate: r.rotate };
doc.node_mut_at(group_at)?.extras_mut().objects_on_path = Some(Box::new(on_path));
Some(())
+48
View File
@@ -3400,3 +3400,51 @@ fn an_objects_on_path_group_in_versioned_content_opens_once() {
assert_eq!(kids[0].children().unwrap().len(), 5);
assert!(kids[0].extras().objects_on_path.is_some());
}
/// The path and objects of a group are measured once however many records name it: a thousand
/// copies of a record open about as fast as one copy followed by 999 records whose path id is
/// missing, when the group opens live, when its long path has no length, and when its first object
/// is a group of many paths and its last object has no bounds.
#[test]
fn repeated_objects_on_path_records_dont_measure_the_objects_again() {
use vectorcraft_testkit::ai::{on_path_member, on_path_options, on_path_record};
let paths: String = (0..20_000).map(|i| format!("{}f\n", rect_path(f64::from(i % 200), 40.0, f64::from(i % 200) + 1.0, 46.0))).collect();
let big = format!("u\n{RED}{paths}U\n{}", on_path_member(ON_PATH_IDS[1], Some(0), 0.0));
let empty = format!("u\nU\n{}", on_path_member(ON_PATH_IDS[2], Some(1), 0.0));
let still = format!("20 20 m\n{}S\n", "20 20 L\n".repeat(20_000));
let cases = [
("a live group", OPEN_PATH.to_string(), big.clone(), 2, true),
("a path without length", still, big.clone(), 2, false),
("an object without bounds", OPEN_PATH.to_string(), format!("{big}{empty}"), 3, false),
];
for (why, path, objects, n, live) in cases {
let ids = &ON_PATH_IDS[..n];
let group = format!(
"0 Ae\nu\n/ConstraintGroup :\n128 /GroupType ,\n;\n0 0 1 0 0 0 1 XA\n2 w\n{path}{}{objects}U\n",
on_path_member(ids[0], None, 0.0)
);
let record = on_path_record(ALIGN_ON_PATH, ids, &on_path_options(n - 1, 4, 0.0, ""));
let (start, end) = (record.find("%_/Dictionary :\n%_1 /Int").unwrap(), record.find("%_; (Constraint Data)").unwrap());
let copy = &record[start..end];
let unresolved = copy.replace(ids[0], "0a6d1e52-ffff-4000-8000-000000000000");
let one = format!("{}{copy}{}{}", &record[..start], unresolved.repeat(999), &record[end..]);
let many = format!("{}{}{}", &record[..start], copy.repeat(1000), &record[end..]);
let time = |data: &str| {
let started = std::time::Instant::now();
let doc = read_ok(&format!("{data}{}", layer(&group))).doc;
let elapsed = started.elapsed();
let g = art(&doc)[0].clone();
assert_eq!(g.children().unwrap().len(), n, "{why}");
assert_eq!(g.extras().objects_on_path.is_some(), live, "{why}");
elapsed
};
// The fastest of three runs each, taken in turn, so that other tests running at the same
// time weigh on both.
let (mut fastest_one, mut fastest_many) = (std::time::Duration::MAX, std::time::Duration::MAX);
for _ in 0..3 {
fastest_one = fastest_one.min(time(&one));
fastest_many = fastest_many.min(time(&many));
}
assert!(fastest_many < fastest_one * 5, "{why}: one copy: {fastest_one:?}, 1000 copies: {fastest_many:?}");
}
}