From aaae3fe37844964cb215ccb0ff5b09c49499ed05 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 13:43:04 -0800 Subject: [PATCH] Links: rows of images in stored brush art show their thumbnails The panel collected the objects for its nested rows from the document as it is between commands, where the art of the stored brushes is not set, so rows whose owner is a brush found no object and drew a blank thumbnail. When an id is not found otherwise, the panel now sets that art on a copy of the document, as links.list does, and collects the objects from it. A recheck of the files at the same revision keeps the objects collected before. --- crates/ui-egui/src/panels/links.rs | 63 +++++++++++++++++++----- crates/ui-egui/src/panels/tests_links.rs | 22 +++++++++ 2 files changed, 73 insertions(+), 12 deletions(-) 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());