Artboards: Fit to Artwork Bounds' kept answer kept by the document, with the plug-ins and fonts it reads

The answer whether Fit to Artwork Bounds has art to fit moves from a
function-local thread_local to DocState, beside any_empty_artboards, and
drops with the document. It is kept for that version of the document
(held weakly, as any_empty_artboards holds its own) and the artboards
asked about, and measured again when the installed plug-ins, whose
effects draw art, or the installed fonts, which set type drawn by its
effects' art, change: their generations are an explicit input of the
measure. A change of the selection or the view keeps it.

The test counts the measures, as the Delete Empty Artboards check's
does: a later frame, select.set and a view change keep the answer; an
edit, other artboards, and each generation measure again. Taking any
part out of the key fails it.
This commit is contained in:
Drew Dibble
2026-11-09 15:13:41 -07:00
parent 758a31890d
commit 78b31831c2
3 changed files with 79 additions and 27 deletions
+43
View File
@@ -112,6 +112,8 @@ pub struct DocState {
pub opened_by: Option<Arc<file_access::AutomationRoots>>,
/// What [`DocState::any_empty_artboards`] last measured. Not saved.
empty_artboards: std::cell::RefCell<EmptyArtboards>,
/// What [`DocState::any_art_to_fit`] last measured. Not saved.
art_to_fit: std::cell::RefCell<Option<ArtToFit>>,
}
/// 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<Document>,
indices: Vec<usize>,
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<usize> = 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<usize> = 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 {
+32
View File
@@ -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]