diff --git a/crates/engine/src/doc_state.rs b/crates/engine/src/doc_state.rs index a0ae4cd65..8c8079fd9 100644 --- a/crates/engine/src/doc_state.rs +++ b/crates/engine/src/doc_state.rs @@ -112,6 +112,8 @@ pub struct DocState { pub opened_by: Option>, /// What [`DocState::any_empty_artboards`] last measured. Not saved. empty_artboards: std::cell::RefCell, + /// What [`DocState::any_art_to_fit`] last measured. Not saved. + art_to_fit: std::cell::RefCell>, } /// Whether a document has an artboard for Delete Empty Artboards to delete, with the document that @@ -124,10 +126,23 @@ struct EmptyArtboards { any: bool, } +/// Whether an unlocked artboard of those asked about has art that Fit to Artwork Bounds fits it +/// to, with what was measured: the document (held as [`EmptyArtboards::doc`] holds it), the +/// artboards, and the generations of what the measure reads outside the document. +#[derive(Clone, Debug)] +struct ArtToFit { + doc: std::sync::Weak, + indices: Vec, + outside: (u64, u64), + any: bool, +} + #[cfg(test)] thread_local! { /// Tests: how many times [`DocState::any_empty_artboards`] measured a document on this thread. pub(crate) static EMPTY_ARTBOARD_MEASURES: std::cell::Cell = const { std::cell::Cell::new(0) }; + /// Tests: how many times [`DocState::any_art_to_fit`] measured a document on this thread. + pub(crate) static ART_TO_FIT_MEASURES: std::cell::Cell = const { std::cell::Cell::new(0) }; } static NEXT_DOC_UID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); @@ -309,6 +324,7 @@ impl DocState { last_perspective: None, opened_by: file_access::current(), empty_artboards: Default::default(), + art_to_fit: Default::default(), } } /// `measure`'s answer for the document now. `measure` runs once for each version of the document, @@ -323,6 +339,33 @@ impl DocState { } last.any } + /// Whether an unlocked artboard of `indices` has art that Fit to Artwork Bounds fits it to. A + /// menu asks on every frame it is open, and measuring builds the solids of 3D effects, so the + /// answer is kept for this version of the document and those artboards, and a change of the + /// selection or the view keeps it. It is measured again when what the measure reads outside the + /// document changes: the installed plug-ins, whose effects draw art, and the installed fonts, + /// which set type drawn by its effects' art (an envelope, a 3D effect). + pub fn any_art_to_fit(&self, indices: &[usize]) -> bool { + self.any_art_to_fit_with(indices, (vectorcraft_plugins::registry::revision(), vectorcraft_text::FontDb::global().generation())) + } + /// [`Self::any_art_to_fit`], with `outside` the generations of the plug-ins and the fonts. + pub(crate) fn any_art_to_fit_with(&self, indices: &[usize], outside: (u64, u64)) -> bool { + let mut last = self.art_to_fit.borrow_mut(); + if let Some(k) = last.as_ref() + && std::ptr::eq(k.doc.as_ptr(), Arc::as_ptr(&self.doc)) + && k.indices == indices + && k.outside == outside + { + return k.any; + } + #[cfg(test)] + ART_TO_FIT_MEASURES.with(|c| c.set(c.get() + 1)); + // One renderer measures every artboard: each object once. + let mut r = vectorcraft_render::Renderer::new(); + let any = indices.iter().filter_map(|i| self.doc.artboards.get(*i)).any(|a| !a.locked && r.art_bounds_on(&self.doc, a.rect).is_some()); + *last = Some(ArtToFit { doc: Arc::downgrade(&self.doc), indices: indices.to_vec(), outside, any }); + any + } /// Unsaved changes: the document differs from the saved one (selection changes and changes that /// are not edits to save don't count, [`DocState::same_content`]). pub fn is_dirty(&self) -> bool { diff --git a/crates/engine/src/tests/panelcmds.rs b/crates/engine/src/tests/panelcmds.rs index 0870ffb87..8dda7196c 100644 --- a/crates/engine/src/tests/panelcmds.rs +++ b/crates/engine/src/tests/panelcmds.rs @@ -1786,6 +1786,38 @@ fn delete_empty_artboards_check_measures_a_document_once() { assert_eq!(delete_empty_enabled_counted(&s), (true, 0), "after another change of the selection"); } +/// Whether Fit to Artwork Bounds has art to fit on `indices`, as a menu asks for it, with the +/// plug-ins' and fonts' generations `outside`, and how many times asking measured a document on +/// this thread. +fn art_to_fit_counted(s: &Session, indices: &[usize], outside: (u64, u64)) -> (bool, usize) { + let measures = || crate::doc_state::ART_TO_FIT_MEASURES.with(std::cell::Cell::get); + let before = measures(); + let any = s.doc().unwrap().any_art_to_fit_with(indices, outside); + (any, measures() - before) +} + +/// A menu asks whether Fit to Artwork Bounds has art on every frame while it is open: the answer is +/// measured once for a version of the document and the artboards asked about, kept through a change +/// of the selection or the view, and measured again after an edit, for other artboards, and when +/// the plug-ins or the fonts change. +#[test] +fn art_to_fit_is_measured_once_for_a_version_of_the_document() { + let mut s = row_of_artboards(2); + let both = [0, 1]; + assert_eq!(art_to_fit_counted(&s, &both, (0, 0)), (false, 1), "the first frame"); + assert_eq!(art_to_fit_counted(&s, &both, (0, 0)), (false, 0), "a later frame"); + let art = box_at(&mut s, 10.0, 20.0); + assert_eq!(art_to_fit_counted(&s, &both, (0, 0)), (true, 1), "an edit puts art on artboard 1"); + s.execute("select.set", &json!({"ids": [art.0]})).unwrap(); + assert_eq!(art_to_fit_counted(&s, &both, (0, 0)), (true, 0), "after a change of the selection"); + s.execute("view.transparencyGrid", &json!({})).unwrap(); + assert_eq!(art_to_fit_counted(&s, &both, (0, 0)), (true, 0), "after a change of the view"); + assert_eq!(art_to_fit_counted(&s, &[1], (0, 0)), (false, 1), "other artboards"); + assert_eq!(art_to_fit_counted(&s, &[1], (1, 0)), (false, 1), "a plug-in installed or removed"); + assert_eq!(art_to_fit_counted(&s, &[1], (1, 1)), (false, 1), "fonts installed"); + assert_eq!(art_to_fit_counted(&s, &[1], (1, 1)), (false, 0)); +} + /// The enabled state follows an edit, an undo, a redo, an added and a removed artboard, a locked /// artboard, and each change measures the document once. #[test] diff --git a/crates/ui-egui/src/panels/artboards.rs b/crates/ui-egui/src/panels/artboards.rs index a950a24c7..d46e5c4ac 100644 --- a/crates/ui-egui/src/panels/artboards.rs +++ b/crates/ui-egui/src/panels/artboards.rs @@ -312,34 +312,11 @@ pub struct MenuFacts { pub(crate) any_art: bool, } -/// Whether an unlocked artboard of `indices` has art that Fit to Artwork Bounds fits it to. The -/// canvas menu asks on every frame it is open without its [`MenuFacts`], and measuring builds the -/// solids of 3D effects, so one answer is kept, the last asked: for that version of the document -/// (the same `Arc`, held weakly so its address isn't reused, as -/// `DocState::any_empty_artboards` keeps its answer) and those artboards, until a plug-in is -/// installed or removed, which can change what its effects draw. A change of the selection or the -/// view keeps it. +/// Whether an unlocked artboard of `indices` has art that Fit to Artwork Bounds fits it to, kept by +/// the document ([`vectorcraft_engine::DocState::any_art_to_fit`]): the canvas menu asks on every +/// frame it is open without its [`MenuFacts`]. pub(crate) fn fit_has_art(app: &VectorcraftApp, indices: &[usize]) -> bool { - /// The document measured, the artboards asked about, the plug-ins' revision, and the answer. - struct Kept { - doc: std::sync::Weak, - indices: Vec, - plugins: u64, - any: bool, - } - thread_local!(static KEPT: std::cell::RefCell> = const { std::cell::RefCell::new(None) }); - app.session.active().is_some_and(|st| { - let plugins = crate::menus::plugin_revision(); - let same = |k: &Kept| std::ptr::eq(k.doc.as_ptr(), std::sync::Arc::as_ptr(&st.doc)) && k.indices == indices && k.plugins == plugins; - if let Some(any) = KEPT.with_borrow(|k| k.as_ref().filter(|k| same(k)).map(|k| k.any)) { - return any; - } - // One renderer measures every artboard: each object once. - let mut r = vectorcraft_render::Renderer::new(); - let any = indices.iter().filter_map(|i| st.doc.artboards.get(*i)).any(|a| !a.locked && r.art_bounds_on(&st.doc, a.rect).is_some()); - KEPT.set(Some(Kept { doc: std::sync::Arc::downgrade(&st.doc), indices: indices.to_vec(), plugins, any })); - any - }) + app.session.active().is_some_and(|st| st.any_art_to_fit(indices)) } /// A right-click on the canvas at `p` with the Artboard tool: an artboard under the pointer that