mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 12:21:54 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<vectorcraft_doc::Document>,
|
||||
indices: Vec<usize>,
|
||||
plugins: u64,
|
||||
any: bool,
|
||||
}
|
||||
thread_local!(static KEPT: std::cell::RefCell<Option<Kept>> = 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
|
||||
|
||||
Reference in New Issue
Block a user