diff --git a/crates/ui-egui/src/panels/links.rs b/crates/ui-egui/src/panels/links.rs index f9d9b524c9..2c54b843a8 100644 --- a/crates/ui-egui/src/panels/links.rs +++ b/crates/ui-egui/src/panels/links.rs @@ -92,28 +92,29 @@ fn cached(app: &mut VectorcraftApp, ctx: &egui::Context, o: &Options) -> Option< let mut c: Cache = pstate(ctx, "links-cache"); let stale = c.key != key || now - c.at > RECHECK; if stale { + let same_doc = c.key == key; c.list = app.session.execute("links.list", &json!({ "show": o.show, "sort": o.sort, "dcsTransparency": o.dcs })).ok()?; (c.key, c.at) = (key, now); // One pass over the stored art for the rows outside the layer tree and the objects whose - // opacity masks hold them, not one per frame. - let mut want: std::collections::HashSet = c.list["links"] + // opacity masks hold them, not one per frame. An object in the layer tree is looked up by + // its index when drawn. + let doc = app.session.active().map(|st| &st.doc); + let want: std::collections::HashSet = c.list["links"] .as_array() .into_iter() .flatten() .filter(|r| nested_row(r)) .flat_map(|r| [r["id"].as_u64(), r["owner"]["maskOf"].as_u64()]) .flatten() + .filter(|id| doc.is_some_and(|d| d.node(NodeId(*id)).is_none())) .collect(); - c.nested.clear(); - if !want.is_empty() - && let Some(st) = app.session.active() - { - st.doc.visit_stored_arcs(|n, place| { - if !place.in_layers() && want.remove(&n.id.0) { - c.nested.push((n.id.0, std::sync::Arc::clone(n))); - } - }); - c.nested.sort_by_key(|(id, _)| *id); + // The same revision keeps the objects collected for it, so their thumbnails are not drawn + // again each time the files are looked at. + if !(same_doc && want.iter().all(|id| c.nested_node(*id).is_some())) { + c.nested = match doc { + Some(d) if !want.is_empty() => stored_objects(d, want), + _ => vec![], + }; } } let want = first.filter(|_| o.info); @@ -126,6 +127,44 @@ fn cached(app: &mut VectorcraftApp, ctx: &egui::Context, o: &Options) -> Option< Some(c) } +/// The objects of `doc` outside the layer tree whose ids are in `want`, sorted by id. The art of +/// the stored brushes is set on `doc` only while a command uses it ([`Document::mounted_art`]), so +/// when an id is not found otherwise that art is set on a copy of `doc`, which gives its objects +/// the ids `links.list` reports for them. +/// +/// [`Document::mounted_art`]: vectorcraft_doc::Document::mounted_art +pub(super) fn stored_objects( + doc: &vectorcraft_doc::Document, + mut want: std::collections::HashSet, +) -> Vec<(u64, std::sync::Arc)> { + let mut out = vec![]; + collect_stored(doc, &mut want, &mut out); + if !want.is_empty() && doc.mounted_art.is_empty() { + let art = vectorcraft_brush::stored_art::StoredArt::read(doc); + if !art.is_empty() { + let mut d = doc.clone(); + art.mount(&mut d); + collect_stored(&d, &mut want, &mut out); + } + } + out.sort_by_key(|(id, _)| *id); + out +} + +/// Adds the objects of `d` outside the layer tree whose ids are in `want` to `out`, taking their +/// ids out of `want`. +fn collect_stored( + d: &vectorcraft_doc::Document, + want: &mut std::collections::HashSet, + out: &mut Vec<(u64, std::sync::Arc)>, +) { + d.visit_stored_arcs(|n, place| { + if !place.in_layers() && want.remove(&n.id.0) { + out.push((n.id.0, std::sync::Arc::clone(n))); + } + }); +} + /// The ids of the selected image objects and placed documents. fn selected_images(app: &VectorcraftApp) -> Vec { let Some(st) = app.session.active() else { return vec![] }; diff --git a/crates/ui-egui/src/panels/tests_links.rs b/crates/ui-egui/src/panels/tests_links.rs index 6b773e9d3b..45c1fc3b04 100644 --- a/crates/ui-egui/src/panels/tests_links.rs +++ b/crates/ui-egui/src/panels/tests_links.rs @@ -230,6 +230,28 @@ fn a_mask_rows_tooltip_names_its_object_inside_a_symbol() { assert!(text.iter().any(|t| t.contains(&want)), "{want} in {text:?}"); } +/// The row of an image in a stored brush's art finds its object for the thumbnail, though that art +/// is set on the document only while a command uses it. +#[test] +fn a_row_in_a_stored_brushs_art_finds_its_object() { + let dir = Folder::new("brush-art"); + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + let leaf = dir.file("leaf.png", &png(30, 20)); + let id = app.run("file.place", json!({"path": leaf, "at": [100, 100]})).unwrap()["ids"][0].as_u64().unwrap(); + app.run("brush.new", json!({"type": "scatter", "name": "Leaf", "ids": [id]})).unwrap(); + app.run("edit.clear", json!({"ids": [id]})).unwrap(); + let list = app.run("links.list", json!({})).unwrap(); + let row = list["links"].as_array().unwrap().iter().find(|r| r["name"] == "leaf.png").unwrap_or_else(|| panic!("{list}")).clone(); + assert_eq!(row["owner"]["kind"], "brush", "{list}"); + let row = row["id"].as_u64().unwrap(); + let doc = &app.session.active().unwrap().doc; + assert!(doc.mounted_art.is_empty(), "the brush's art is not set on the document"); + let found = super::links::stored_objects(doc, [row].into()); + assert_eq!(found.iter().map(|(id, _)| *id).collect::>(), [row]); + assert!(matches!(found[0].1.kind, vectorcraft_doc::NodeKind::Image(_)), "{:?}", found[0].1.kind); +} + #[test] fn an_empty_document_shows_how_to_add_images() { let mut app = VectorcraftApp::new(Session::new(), Default::default());