mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 09:29:44 -06:00
.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:
@@ -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(())
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user