.ai and EPS import: envelopes of type stay live where other art differs from the page

When the layers with their live envelopes didn't draw as the page, and
didn't with the drawn art of every live envelope put back either, every
envelope of type got its drawn art back wherever the layers were still
used: the editing data trusted, the objects that differ coming in as
the page's art, or a .ai whose layers would open alone opening alone.
Art that differed anywhere on the page cost every envelope of type its
editable type, though putting its drawn art back changed nothing.

Now each envelope of type is weighed in the page check's own drawing,
on the pixels that it and its drawn art ink (the two drawn together on
a layer of their own, without the rest of the layers). It keeps its
drawn art and hidden type where the layers with it differ from the page
in more of those pixels than its own check allows against its drawn art
(10%, or 3% with all their neighbours), or where the envelopes that stay
live would together differ there in more of a page than half of what
the page check allows (a budget of its own, the same size as the one
their own checks share). Art differing on its pixels still holds it.

It also keeps its drawn art, as before, where the layers don't draw it
where it is and as itself, on the page or off it: under a clip or an
opacity mask, in a group with an appearance of its own, or in another
envelope, a repeat or a blend. Another envelope on no page stays live,
except on a page that holds all the art (an EPS's art box), where one
reaching past the page keeps its drawn art. Past 12 on the pages, all
keep it.

A .ai whose layers would open alone still opens alone: its envelopes of
type are settled again as they were with the page, in the same order
and with the same checks, so each that opens live draws as it was
weighed, and those the page held get their drawn art back. Where the
differing objects are to come in as the page's art and can't with
envelopes of type live (an object that holds type isn't replaced),
every envelope of type gets its drawn art back and that is tried again,
as before. Where putting back the drawn art of every live envelope
makes the layers draw as the page, it is still put back.

On 224 of our own files (used locally), envelopes of type that open
live go from 174 to 186 in 8 files, none fewer; no file changes between
its layers and its PDF part.

Part of #3179.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jmverges
2026-11-13 14:04:38 +00:00
co-authored by Claude Opus 5.5
parent e8c7421a3d
commit 05f38616e0
10 changed files with 836 additions and 81 deletions
+1
View File
@@ -39,6 +39,7 @@ pub(crate) use envelope::DRAWN as ENVELOPE_DRAWN;
#[cfg(test)]
pub(crate) use envelope::MAX_CHECKED;
pub(crate) use envelope::TYPE_DRAWN as ENVELOPE_TYPE_DRAWN;
pub(crate) use envelope::{MAX_ANY as ENVELOPE_MAX_ANY, MAX_SOLID as ENVELOPE_MAX_SOLID};
pub(crate) use envelope::{WARPED_TYPE_DRAWN, WarpedType};
pub(crate) use frames::text_frames;
pub use obj::{NameEncoding, Names};
+48 -10
View File
@@ -73,9 +73,9 @@ const MOVED_SLACK: f64 = 0.5;
const KNOWN_BITS: i64 = 0b111 | 1 << 16 | 1 << 17;
/// More of the inked pixels than this differing, each with all its neighbours, and the envelope
/// doesn't come out as drawn.
const MAX_SOLID: f64 = 0.03;
pub(crate) const MAX_SOLID: f64 = 0.03;
/// More of the inked pixels than this differing at all, and the envelope doesn't come out as drawn.
const MAX_ANY: f64 = 0.1;
pub(crate) const MAX_ANY: f64 = 0.1;
/// The whole number of entry `key`: `None` when it is missing, `Some(None)` when it isn't one.
fn whole(o: &Obj, key: &str) -> Option<Option<i64>> {
@@ -648,6 +648,9 @@ pub(crate) enum TypeKept {
Budget,
/// Its live envelope doesn't draw as its drawn art.
Differs,
/// A page that the layers, which don't draw as it, were compared with holds it against them
/// ([`WarpedType::open_alone`]).
Page,
}
/// What [`WarpedType::settle_envelopes`] did.
@@ -657,9 +660,10 @@ pub(crate) struct TypedSettled {
pub(crate) live: usize,
/// Why each of the others kept its drawn art.
pub(crate) kept: Vec<TypeKept>,
/// Each live envelope's id, with what takes its place again when the layers with it don't draw
/// as the page: its hidden type, then its drawn art.
pub(crate) put_back: Vec<(NodeId, Vec<Node>)>,
/// Each live envelope's place among the envelopes of type (in the order they were read) and
/// id, with what takes its place again when the layers with it don't draw as the page: its
/// hidden type, then its drawn art.
pub(crate) put_back: Vec<(usize, NodeId, Vec<Node>)>,
}
/// The text objects read with a Warp and the envelopes of type, and what the import has left of the
@@ -672,6 +676,9 @@ pub struct WarpedType {
pub(super) checks: usize,
pub(super) spent: Spent,
pub(super) page: Option<Rect>,
/// The envelopes of type that may open live in layers built without a page, by their place
/// among them ([`Self::open_alone`]): none when `None`.
pub(super) alone: Option<BTreeSet<usize>>,
}
impl WarpedType {
@@ -682,14 +689,33 @@ impl WarpedType {
/// These, for layers `compared` with a page. Layers that aren't leave the envelopes of type
/// their drawn art and their hidden type, with the note ([`TYPE_DRAWN`]) added to `notes`: a
/// live envelope opens only where the page chooses it.
/// live envelope opens only where the page chooses it. Unless [`Self::open_alone`] names some:
/// then they all wait, as with a page, and only those named may stay live.
pub(crate) fn compared(mut self, compared: bool, notes: &mut Vec<String>) -> Self {
if !compared && !std::mem::take(&mut self.envelopes).is_empty() && !notes.iter().any(|n| n == TYPE_DRAWN) {
if !compared && self.alone.is_none() && !std::mem::take(&mut self.envelopes).is_empty() && !notes.iter().any(|n| n == TYPE_DRAWN) {
notes.push(TYPE_DRAWN.into());
}
self
}
/// Let the envelopes of type at `places` (among them, in the order they were read) open live in
/// layers built without a page, where they draw as their drawn art: a page these same layers,
/// read the same way, were compared with weighed them and didn't hold them against the layers'
/// difference from it. All the envelopes of type are settled as they were then, in the same
/// order and with the same checks, so each that opens live draws as it was weighed; then the
/// others get their drawn art back ([`Self::settle_envelopes`]). `count` is how many envelopes
/// of type that reading had: a reading with another number of them names none.
pub(crate) fn open_alone(&mut self, places: &[usize], count: usize) {
if self.envelopes.len() == count {
self.alone = Some(places.iter().copied().filter(|i| *i < count).collect());
}
}
/// How many envelopes of type wait for their type.
pub(crate) fn envelope_count(&self) -> usize {
self.envelopes.len()
}
/// Make each envelope of type live where it draws as its drawn art, its contents' text slots
/// holding their type: `type_of` gives, for a slot's id, the hidden text object it became in
/// `doc` and the type the contents take in its place (the slot's visibility and lock are the
@@ -713,16 +739,28 @@ impl WarpedType {
doc.layers.iter().for_each(|l| collect_shown(l, &mut shown));
let envelopes = std::mem::take(&mut self.envelopes);
let mut waiting = envelopes.len();
for envelope in envelopes {
for (place, envelope) in envelopes.into_iter().enumerate() {
waiting -= 1;
match self.typed_live(doc, envelope, &type_of, &shown, waiting) {
Ok(put_back) => {
Ok((id, put_back)) => {
settled.live += 1;
settled.put_back.push(put_back);
settled.put_back.push((place, id, put_back));
}
Err(why) => settled.kept.push(why),
}
}
// Built without a page, the envelopes [`Self::open_alone`] doesn't name get their drawn art
// and hidden type back. One inside another that got its own back went with that art.
if let Some(free) = &self.alone {
let (stay, back): (Vec<_>, Vec<_>) = std::mem::take(&mut settled.put_back).into_iter().partition(|(place, ..)| free.contains(place));
for (_, id, nodes) in back {
if doc.insert_beside(vec![(id, nodes)], true).is_ok() || doc.node(id).is_none() {
settled.live = settled.live.saturating_sub(1);
settled.kept.push(TypeKept::Page);
}
}
settled.put_back = stay;
}
settled
}
@@ -1367,6 +1367,7 @@ impl<'a> Reader<'a> {
checks: self.envelope_checks,
spent: self.envelope_spent,
page,
alone: None,
},
styles_read,
text_patterns,
+1 -1
View File
@@ -8231,7 +8231,7 @@ fn warped_type_on_the_page_spends_what_is_left_of_the_budget() {
let copy = |w: &super::envelope::WarpedLook| super::envelope::WarpedLook { slot: w.slot, look: w.look.clone(), left_out: None };
let page = Some(Rect::new(0.0, 60.0, 60.0, 100.0));
let settle = |w: &super::envelope::WarpedLook, spent| {
let warped = super::envelope::WarpedType { looks: vec![copy(w)], envelopes: vec![], checks: 1, spent, page };
let warped = super::envelope::WarpedType { looks: vec![copy(w)], envelopes: vec![], checks: 1, spent, page, alone: None };
warped.settle(&mut doc.clone(), Some, &Default::default(), &mut vec![])
};
let spent = || super::envelope::Spent { any: MAX_DIFFERENCE / 2.0, solid: MAX_SOLID_DIFFERENCE / 2.0 };
+507 -29
View File
@@ -1,5 +1,6 @@
use super::compare::{
MAX_DIFFERENCE, MAX_PAGES_COMPARED, MAX_PIXELS_COMPARED, MAX_SOLID_DIFFERENCE, PageView, compare, comparison_samples, difference, pixels_compared,
MAX_DIFFERENCE, MAX_PAGES_COMPARED, MAX_PIXELS_COMPARED, MAX_SOLID_DIFFERENCE, PAST_THE_PAGE, PageView, compare, comparison_samples, difference,
pixels_compared,
};
use super::plan::{made_only, make_slots, plan, readable_all};
use super::slots::{
@@ -230,6 +231,24 @@ pub(super) fn build_from(
outlined: bool,
trust: bool,
unit: f64,
) -> Result<(Document, Vec<String>), String> {
build_weighing(structure, data, visible, page, outlined, trust, unit, &mut None)
}
/// [`build_from`]. When the layers with their live envelopes don't draw as the page, and don't with
/// the drawn art of every live envelope put back either, `free` is set to the envelopes of type
/// (by their place among them, as read) that opened live and that the page doesn't hold against
/// them ([`held_back`]), whether or not the layers are then used, and however.
#[allow(clippy::too_many_arguments)]
pub(super) fn build_weighing(
structure: Structure,
data: &[u8],
visible: Option<&Document>,
page: Page,
outlined: bool,
trust: bool,
unit: f64,
free: &mut Option<Vec<usize>>,
) -> Result<(Document, Vec<String>), String> {
let Structure {
mut doc,
@@ -468,12 +487,14 @@ pub(super) fn build_from(
let warped_type = warped_type.compared(frame.is_some(), &mut notes);
let bound = [&bound_text, &bound_visible];
let typed = settle_warped_type(warped_type, &mut doc, &filled, &threaded_frames, bound, &mut restyled, &mut notes)?;
// Each live envelope of type's place among the envelopes of type, by its id.
let places: Vec<(NodeId, usize)> = typed.iter().map(|(place, id, _)| (*id, *place)).collect();
// The live envelopes and what takes the place of each again, when the layers with them don't
// draw as the page, with the note that says so.
let envelopes_drawn: Vec<(NodeId, Vec<Node>, &str)> = envelopes_drawn
.into_iter()
.map(|(id, art)| (id, vec![art], ai::ENVELOPE_DRAWN))
.chain(typed.into_iter().map(|(id, nodes)| (id, nodes, ai::ENVELOPE_TYPE_DRAWN)))
.chain(typed.into_iter().map(|(_, id, nodes)| (id, nodes, ai::ENVELOPE_TYPE_DRAWN)))
.collect();
if empty > 0 {
notes.push(format!("{empty} {}", if frame.is_some() { TEXT_LEFT_OUT } else { TYPE_LEFT_OUT }));
@@ -525,20 +546,21 @@ pub(super) fn build_from(
notes.extend(unseen_note(unseen).into_iter().chain(note));
}
other => match drawn_or_trusted(&frame, &mut view, &mut doc, &envelopes_drawn, &notes, unseen, trust, why, reflow) {
Ok(more) => notes.extend(more),
Ok((more, held)) => {
*free = held.map(|held| free_places(&places, &held));
notes.extend(more);
}
// The objects that draw differently from the page come in as its art, from
// the placement that draws closer to it. The layers don't draw as the page:
// envelopes of type keep their drawn art and their type, hidden.
// the envelopes of type it holds against them keep their drawn art and their
// type, hidden. Where no placement's differing objects can then come in as the
// page's art (an object that holds type isn't replaced), every envelope of type
// keeps its drawn art, and that is tried again.
Err(why) => {
let typed = type_drawn(&mut doc, &envelopes_drawn);
let other = other.map(|(_, mut other, unseen)| {
type_drawn(&mut other, &envelopes_drawn);
(other, unseen)
});
let placements = closer_first(&frame, &mut view, (doc, unseen), other, reflow);
let Some((kept, unseen, kept_notes)) = placements.into_iter().find_map(|(base, unseen)| {
keep_differing(&base, &frame, &mut view, &threaded_frames, &shown, reflow).map(|(kept, n)| (kept, unseen, n))
}) else {
let other = other.map(|(_, other, unseen)| (other, unseen));
let layers = Placements { doc: &doc, unseen, other: other.as_ref(), drawn: &envelopes_drawn, places: &places };
let found = keep_differing_typed(&frame, &mut view, &layers, &threaded_frames, &shown, reflow, free);
let Some((kept, unseen, kept_notes, typed)) = found else {
return Err(why);
};
doc = kept;
@@ -693,8 +715,9 @@ fn bound_slots(
/// anchors. Before that, the envelopes of type open live where they draw as their drawn art, with
/// the type of their text slots (made type, one text object for the slot, not threaded, bound to
/// no text variable and no visibility variable, `bound`'s second set, and no wrap object); the
/// others keep their drawn art, with a note. Returns each live envelope of type's id with what
/// takes its place again (its hidden type, then its drawn art).
/// others keep their drawn art, with a note. Returns each live envelope of type's place among the
/// envelopes of type (in the order they were read) and id, with what takes its place again (its
/// hidden type, then its drawn art).
fn settle_warped_type(
mut warped: ai::WarpedType,
doc: &mut Document,
@@ -703,7 +726,7 @@ fn settle_warped_type(
bound: [&BTreeSet<NodeId>; 2],
restyled: &mut Vec<Rect>,
notes: &mut Vec<String>,
) -> Result<Vec<(NodeId, Vec<Node>)>, String> {
) -> Result<Vec<(usize, NodeId, Vec<Node>)>, String> {
let [bound_text, bound_visible] = bound;
let made = |slot| match filled.get(&slot).map(|f| f.text.as_slice()).filter(|_| !bound_text.contains(&slot)) {
Some(&[one]) => Some(one),
@@ -1278,6 +1301,59 @@ fn keep_differing(
Some((kept, note.into_iter().chain([format!("{DREW_DIFFERENTLY}: {}", layers.join(", "))]).collect()))
}
/// The places (among the envelopes of type, as read) of the live envelopes of type in `places`
/// (each with its id) that `held` doesn't list.
fn free_places(places: &[(NodeId, usize)], held: &BTreeSet<NodeId>) -> Vec<usize> {
places.iter().filter(|(id, _)| !held.contains(id)).map(|(_, place)| *place).collect()
}
/// The layers of [`keep_differing_typed`]: `doc`, `unseen` text objects of it taken out; the placed
/// files' other placement of it, when it has one, with how many of its text objects were; its live
/// envelopes, as [`drawn_or_trusted`] takes them; and the place of each live envelope of type
/// among the envelopes of type, by its id.
struct Placements<'a> {
doc: &'a Document,
unseen: usize,
other: Option<&'a (Document, usize)>,
drawn: &'a [(NodeId, Vec<Node>, &'a str)],
places: &'a [(NodeId, usize)],
}
/// [`keep_differing`] of the placement of `layers` that draws closer to the page, the envelopes of
/// type the page holds against them ([`held_back`]) having their drawn art and hidden type back,
/// and where none of that placement's differing objects can then come in as the page's art, every
/// envelope of type: the document, how many of its text objects were taken out, its notes, and
/// whether an envelope of type got its drawn art back. `free` is set to the places of those the
/// page doesn't hold against the layers.
#[allow(clippy::too_many_arguments)]
fn keep_differing_typed(
frame: &Frame<'_>,
view: &mut PageView,
layers: &Placements<'_>,
threaded_frames: &BTreeSet<NodeId>,
shown: &[Vec<Arc<Node>>],
reflow: bool,
free: &mut Option<Vec<usize>>,
) -> Option<(Document, usize, Vec<String>, bool)> {
let held = held_back(frame, view, layers.doc, layers.drawn, reflow);
*free = Some(free_places(layers.places, &held));
let all: BTreeSet<NodeId> = layers.places.iter().map(|(id, _)| *id).collect();
let tries = if held.len() < all.len() { vec![held, all] } else { vec![all] };
tries.into_iter().find_map(|held| {
let mut base = layers.doc.clone();
let typed = type_drawn(&mut base, layers.drawn, &held);
let other = layers.other.map(|(other, unseen)| {
let mut other = other.clone();
type_drawn(&mut other, layers.drawn, &held);
(other, *unseen)
});
let placements = closer_first(frame, view, (base, layers.unseen), other, reflow);
placements.into_iter().find_map(|(base, unseen)| {
keep_differing(&base, frame, view, threaded_frames, shown, reflow).map(|(kept, notes)| (kept, unseen, notes, typed))
})
})
}
/// Copy page drawing IDs, including mask art outside the ordinary child tree.
pub(super) fn reid_page_art(doc: &mut Document, source: &Node) -> Node {
let mut copy = doc.reid(source);
@@ -1338,9 +1414,9 @@ fn slot_content(
/// The layers `doc` don't draw as the page (`why`), `unseen` text objects of them taken out: the
/// notes to add when `doc` with each live envelope `drawn` lists replaced by the drawn art it took
/// the place of (see [`ai::Structure::envelopes_drawn`]) draws as the page, `doc` then being that;
/// else, with `trust`, the notes for `doc` as it is but for its envelopes of type, which keep their
/// drawn art ([`type_drawn`]). An error, `why`, otherwise. The envelope note is added unless
/// `notes` has it.
/// else, with `trust`, the notes for `doc` as it is but for the envelopes of type the page holds
/// against it ([`held_back`]), which keep their drawn art ([`type_drawn`]), with those envelopes.
/// An error, `why`, otherwise. The envelope note is added unless `notes` has it.
#[allow(clippy::too_many_arguments)]
fn drawn_or_trusted(
frame: &Frame<'_>,
@@ -1352,7 +1428,7 @@ fn drawn_or_trusted(
trust: bool,
why: String,
reflow: bool,
) -> Result<Vec<String>, String> {
) -> Result<(Vec<String>, Option<BTreeSet<NodeId>>), String> {
let unseen_note = |n: usize| (n > 0).then(|| format!("{n} {UNSEEN_TEXT}"));
let mut put_back = doc.clone();
// Each envelope still there, in place of the art it took the place of.
@@ -1379,26 +1455,143 @@ fn drawn_or_trusted(
envelope.push(say.to_string());
}
}
return Ok(envelope.into_iter().chain(unseen_note(unseen + more)).chain(note).collect());
return Ok((envelope.into_iter().chain(unseen_note(unseen + more)).chain(note).collect(), None));
}
}
if trust {
let typed = type_drawn(doc, drawn).then(|| ai::ENVELOPE_TYPE_DRAWN.to_string()).filter(|n| !notes.contains(n));
let held = held_back(frame, view, doc, drawn, reflow);
let typed = type_drawn(doc, drawn, &held).then(|| ai::ENVELOPE_TYPE_DRAWN.to_string()).filter(|n| !notes.contains(n));
let trusted = format!("{why}: they come from its editing data all the same");
return Ok(typed.into_iter().chain(unseen_note(unseen)).chain([trusted]).collect());
return Ok((typed.into_iter().chain(unseen_note(unseen)).chain([trusted]).collect(), Some(held)));
}
Err(why)
}
/// The live envelopes of type in `drawn` (as [`drawn_or_trusted`] takes them) that the page holds
/// against the layers `doc`, which don't draw as it even with every live envelope's drawn art put
/// back: by their ids, those where the layers don't draw as the page. The envelope and its drawn art
/// are drawn together on a layer of their own, without the rest of the layers, on the page check's
/// drawing of each page they lie on: in the pixels they ink there, the layers with the envelope may
/// differ from the page in no more than the envelope's own check allows against its drawn art (at
/// all, and with all their neighbors, away from type: [`compare`]); and in the order they were
/// read, the envelopes that stay live may differ there in no more of each page together than half
/// of what the page check allows (a budget of the same size as the one their own checks share, kept
/// apart from it). Art that differs elsewhere on the page doesn't hold them. One the layers don't
/// draw where it is and as itself ([`drawn_in_place`]) is held, wherever it lies: drawn on its own,
/// it would be weighed elsewhere than it draws, or on pixels a clip or mask hides, and what holds it
/// can draw it onto a page from off it. Another envelope on no page stays live, as the page check
/// doesn't see it, unless the page holds all the art that prints ([`Frame::whole`]), where art past
/// it is compared too: there one reaching past the page is held. Past [`MAX_WEIGHED`] of them on
/// the pages, or where the page check can't weigh one, they are all held.
fn held_back(frame: &Frame<'_>, view: &mut PageView, doc: &Document, drawn: &[(NodeId, Vec<Node>, &str)], reflow: bool) -> BTreeSet<NodeId> {
let typed: Vec<(NodeId, &Node, &Node)> = drawn
.iter()
.filter(|(_, _, say)| *say == ai::ENVELOPE_TYPE_DRAWN)
.filter_map(|(id, art, _)| Some((*id, doc.node(*id)?, art.last()?)))
.collect();
let all: BTreeSet<NodeId> = typed.iter().map(|(id, ..)| *id).collect();
if all.is_empty() {
return all;
}
let pages = view.regions(frame.rect);
let past = |b: Rect| frame.whole && !frame.rect.inflate(PAST_THE_PAGE, PAST_THE_PAGE).contains_rect(b);
let mut held = BTreeSet::new();
let mut in_place = std::collections::HashMap::new();
drawn_in_place(&doc.layers, &all, true, &mut in_place);
// Those the page check sees, by the boxes of the envelope and its drawn art.
let mut seen = vec![];
for (id, live, art) in typed {
if !in_place.get(&id).copied().unwrap_or(false) {
held.insert(id);
continue;
}
match vectorcraft_geom::union_opt(live.visual_bounds(), art.visual_bounds()) {
Some(b) if past(b) => {
held.insert(id);
}
Some(b) if pages_touched(b, &pages, 1.0).is_empty() => {}
_ => seen.push((id, live, art)),
}
}
if seen.len() > MAX_WEIGHED {
return all;
}
// The pixels of each page where the envelopes that stay live differ from it: at all, and with
// all their neighbors.
let mut spent = vec![(0usize, 0usize); pages.len()];
for (id, live, art) in seen {
// The envelope and its drawn art, shown, on a layer of their own in `doc`.
let mut nodes: Vec<Arc<Node>> = [live, art].into_iter().cloned().map(Arc::new).collect();
for n in &mut nodes {
let n = Arc::make_mut(n);
(n.visible, n.opacity) = (true, 1.0);
}
let mut one = doc.clone();
let mut layer = Node::layer(NodeId(0), "", vectorcraft_doc::LayerColor::Preset(0));
if let Some(children) = layer.children_mut() {
*children = nodes;
}
one.layers = vec![Arc::new(layer)];
let bounds = vectorcraft_render::encode::art_bounds(&one);
if bounds.is_some_and(past) {
held.insert(id);
continue;
}
let on = bounds.map_or(vec![], |b| pages_touched(b, &pages, 1.0));
let mut added = spent.clone();
let mut fits = true;
for (i, ink) in view.inked(&one, &on) {
let Some((any, solid)) = view.differing_in(doc, i, &ink, reflow) else { return all };
let inked = ink.iter().filter(|p| **p).count() as f64;
let pixels = ink.len().max(1) as f64;
let Some((a, s)) = added.get_mut(i) else { return all };
*a += any;
*s += solid;
fits &= any as f64 <= inked * ai::ENVELOPE_MAX_ANY
&& solid as f64 <= inked * ai::ENVELOPE_MAX_SOLID
&& *a as f64 <= pixels * MAX_DIFFERENCE / 2.0
&& *s as f64 <= pixels * MAX_SOLID_DIFFERENCE / 2.0;
}
if fits {
spent = added;
} else {
held.insert(id);
}
}
held
}
/// Which of the objects `ids` among `nodes` (or what they hold) are drawn where they are and as
/// themselves (`plain`: so far, above `nodes`), into `out` by id: under layers and groups that
/// neither clip them, mask them nor have an appearance of their own (whose effects can move or copy
/// them), and in no envelope, repeat, blend or other object that draws what it holds elsewhere or
/// more than once.
fn drawn_in_place(nodes: &[Arc<Node>], ids: &BTreeSet<NodeId>, plain: bool, out: &mut std::collections::HashMap<NodeId, bool>) {
for n in nodes {
if ids.contains(&n.id) {
out.insert(n.id, plain);
}
let Some(children) = n.children() else { continue };
let keeps = match n.kind {
NodeKind::Layer { clip, .. } | NodeKind::Group { clip, .. } => {
!clip && n.appearance == vectorcraft_doc::Appearance::default() && n.extras().mask.is_none()
}
_ => false,
};
drawn_in_place(children, ids, plain && keeps, out);
}
}
/// Put each live envelope of type in `drawn` (as [`drawn_or_trusted`] takes them) that `doc` still
/// has back as its hidden type and its drawn art: whether one was. An envelope of type opens live
/// only where the layers with it draw as the page, so where they don't, though they are used all
/// the same (the editing data trusted, or the objects that differ come in as the page's art), it
/// keeps its drawn art.
fn type_drawn(doc: &mut Document, drawn: &[(NodeId, Vec<Node>, &str)]) -> bool {
/// has and `held` lists back as its hidden type and its drawn art: whether one was. An envelope of
/// type opens live only where the page doesn't hold it against the layers ([`held_back`]), so where
/// they don't draw as the page though they are used all the same (the editing data trusted, or the
/// objects that differ come in as the page's art), one the page holds against them keeps its drawn
/// art.
fn type_drawn(doc: &mut Document, drawn: &[(NodeId, Vec<Node>, &str)], held: &BTreeSet<NodeId>) -> bool {
let typed: Vec<(NodeId, Vec<Node>)> = drawn
.iter()
.filter(|(id, _, say)| *say == ai::ENVELOPE_TYPE_DRAWN && doc.node(*id).is_some())
.filter(|(id, _, say)| *say == ai::ENVELOPE_TYPE_DRAWN && held.contains(id) && doc.node(*id).is_some())
.map(|(id, art, _)| (*id, art.clone()))
.collect();
if typed.is_empty() || doc.insert_beside(typed, true).is_err() {
@@ -2562,4 +2755,289 @@ mod candidate_tests {
replace_unread(&mut doc, &frame, &unread, &[], &BTreeSet::new(), &mut copied).unwrap();
assert!(copied.contains(&nodes[0].id) && !copied.contains(&nodes[1].id));
}
/// A rectangle on `rect` filled with `color`, with an id of `doc`'s.
fn filled_rect(doc: &mut Document, rect: Rect, color: vectorcraft_color::Color) -> Node {
let paint = vectorcraft_color::Paint::solid(color);
Node::path(
doc.alloc_id(),
vectorcraft_geom::shapes::rectangle(rect),
vectorcraft_doc::Appearance::basic(paint, vectorcraft_color::Paint::None, 0.0),
)
}
/// A live envelope as [`drawn_or_trusted`] takes it: its id, what takes its place again, its note.
type Envelope = (NodeId, Vec<Node>, &'static str);
/// The layers `doc` with a black rectangle on each of `lives` standing in for a live envelope of
/// type, its drawn art a black rectangle on the matching one of `arts`: the layers, and the
/// envelopes as [`drawn_or_trusted`] takes them.
fn stand_ins(lives: &[Rect], arts: &[Rect]) -> (Document, Vec<Envelope>) {
let mut doc = Document::new(100.0, 100.0);
let layer = doc.layers[0].id;
let mut drawn = vec![];
for (live, art) in lives.iter().zip(arts) {
let n = filled_rect(&mut doc, *live, vectorcraft_color::Color::BLACK);
let id = n.id;
doc.insert(Some(layer), 0, n).unwrap();
let art = filled_rect(&mut doc, *art, vectorcraft_color::Color::BLACK);
drawn.push((id, vec![art], ai::ENVELOPE_TYPE_DRAWN));
}
(doc, drawn)
}
/// The page holds an envelope of type against layers that don't draw as it only where the
/// layers with the envelope differ from the page where it or its drawn art inks: not where the
/// page draws other art that the layers don't have.
#[test]
fn the_page_holds_back_only_envelopes_of_type_where_it_differs_from_them() {
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
let mut page = Document::new(100.0, 100.0);
let layer = page.layers[0].id;
// The first two envelopes' drawn art, and a square the layers don't have.
for r in [Rect::new(10.0, 10.0, 30.0, 30.0), Rect::new(60.0, 10.0, 80.0, 30.0), Rect::new(10.0, 60.0, 30.0, 80.0)] {
let n = filled_rect(&mut page, r, vectorcraft_color::Color::BLACK);
page.insert(Some(layer), 0, n).unwrap();
}
// Live where its drawn art is; moved off its drawn art; and where the page draws nothing.
let (doc, drawn) = stand_ins(
&[Rect::new(10.0, 10.0, 30.0, 30.0), Rect::new(66.0, 10.0, 86.0, 30.0), Rect::new(60.0, 60.0, 80.0, 80.0)],
&[Rect::new(10.0, 10.0, 30.0, 30.0), Rect::new(60.0, 10.0, 80.0, 30.0), Rect::new(60.0, 60.0, 80.0, 80.0)],
);
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)], more: vec![] };
let mut view = PageView::new(&frame);
assert!(compare(&frame, &mut view, &doc, false).is_err());
assert_eq!(held_back(&frame, &mut view, &doc, &drawn, false), BTreeSet::from([drawn[1].0, drawn[2].0]));
// Only envelopes of type are weighed.
let other: Vec<_> = drawn.iter().map(|(id, art, _)| (*id, art.clone(), ai::ENVELOPE_DRAWN)).collect();
assert!(held_back(&frame, &mut view, &doc, &other, false).is_empty());
}
/// The envelopes of type that stay live may together differ from the page where they ink in no
/// more than half of what the page check allows: the next one past that is held back, though
/// on its own it would stay.
#[test]
fn envelopes_of_type_stay_live_within_half_of_what_the_page_check_allows() {
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
let halves = [Rect::new(0.0, 0.0, 100.0, 25.0), Rect::new(0.0, 25.0, 100.0, 50.0)];
let mut page = Document::new(100.0, 100.0);
let layer = page.layers[0].id;
for r in halves {
let n = filled_rect(&mut page, r, vectorcraft_color::Color::BLACK);
page.insert(Some(layer), 0, n).unwrap();
}
// White lines two pixels high over each, 6 points apart: 8% of what each inks differs, 2% of
// the page, and none of it with all its neighbors.
for (i, y) in [3.0, 9.0, 15.0, 21.0, 28.0, 34.0, 40.0, 46.0].into_iter().enumerate() {
let n = filled_rect(&mut page, Rect::new(0.0, y, 100.0, y + 0.5), vectorcraft_color::Color::WHITE);
page.insert(Some(layer), halves.len() + i, n).unwrap();
}
let (doc, drawn) = stand_ins(&halves, &halves);
let frame = Frame { page: &page, page_rect: rect, rect, whole: false, pages: vec![(rect, Affine::IDENTITY)], more: vec![] };
let mut view = PageView::new(&frame);
assert_eq!(held_back(&frame, &mut view, &doc, &drawn, false), BTreeSet::from([drawn[1].0]));
assert!(held_back(&frame, &mut view, &doc, &drawn[1..], false).is_empty());
}
/// A page of 100 points with a black rectangle on each of `rects`, and its frame (`whole`: the
/// page holds all the art that prints).
fn black_page(rects: &[Rect]) -> Document {
let mut page = Document::new(100.0, 100.0);
let layer = page.layers[0].id;
for r in rects {
let n = filled_rect(&mut page, *r, vectorcraft_color::Color::BLACK);
page.insert(Some(layer), 0, n).unwrap();
}
page
}
/// The frame of `page`, its 100-point square (`whole`: the page holds all the art that prints).
fn frame_of(page: &Document, whole: bool) -> Frame<'_> {
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
Frame { page, page_rect: rect, rect, whole, pages: vec![(rect, Affine::IDENTITY)], more: vec![] }
}
/// An envelope of type is held back where the layers with it differ from the page in more of
/// the pixels it and its drawn art ink than its own check allows against its drawn art, though
/// that is far less of the page than the page check allows.
#[test]
fn an_envelope_of_type_differing_in_more_of_its_pixels_than_its_check_allows_is_held_back() {
// Half a point off its drawn art, which the page draws: 22% of the pixels they ink differ,
// a tenth of a percent of the page, and none with all their neighbors.
let page = black_page(&[Rect::new(30.5, 10.0, 34.5, 20.0)]);
let (doc, drawn) = stand_ins(&[Rect::new(30.0, 10.0, 34.0, 20.0)], &[Rect::new(30.5, 10.0, 34.5, 20.0)]);
let frame = frame_of(&page, false);
let mut view = PageView::new(&frame);
assert_eq!(held_back(&frame, &mut view, &doc, &drawn, false), BTreeSet::from([drawn[0].0]));
}
/// An envelope of type that the layers don't draw where it is and as itself is held back though
/// it draws as the page, wherever it lies: one that a clip group or a clipping sublayer clips, an
/// opacity mask masks, or that a group with an appearance of its own, another envelope, a repeat
/// or a blend holds. Drawn on its own, it would be weighed on pixels a clip hides, or elsewhere
/// than it draws, and what holds it can draw it onto the page from off it. One in a plain group
/// is weighed, and one off the page in a plain group stays live.
#[test]
fn envelopes_of_type_the_layers_dont_draw_in_place_are_held_back() {
let at = |i: usize| {
let (x, y) = if i < 4 { (5.0 + 20.0 * i as f64, 5.0) } else { (5.0 + 20.0 * (i - 4) as f64, 30.0) };
Rect::new(x, y, x + 15.0, y + 15.0)
};
let off = [Rect::new(120.0, 5.0, 135.0, 20.0), Rect::new(140.0, 5.0, 155.0, 20.0)];
let page = black_page(&(0..8).map(at).collect::<Vec<_>>());
let mut doc = Document::new(100.0, 100.0);
let layer = doc.layers[0].id;
let mut drawn = vec![];
let mut stand_in = |doc: &mut Document, r: Rect| {
let n = filled_rect(doc, r, vectorcraft_color::Color::BLACK);
drawn.push((n.id, vec![filled_rect(doc, r, vectorcraft_color::Color::BLACK)], ai::ENVELOPE_TYPE_DRAWN));
Arc::new(n)
};
let group = |doc: &mut Document, children: Vec<Arc<Node>>, clip: bool| Node::new(doc.alloc_id(), NodeKind::Group { children, clip });
let fill = vectorcraft_doc::Appearance {
items: vec![vectorcraft_doc::AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(vectorcraft_color::Paint::solid(
vectorcraft_color::Color::BLACK,
)))],
..Default::default()
};
let mut objects = vec![];
// 0: in a plain group.
let n = stand_in(&mut doc, at(0));
objects.push(group(&mut doc, vec![n], false));
// 1: in a clip group.
let n = stand_in(&mut doc, at(1));
let mut clip = filled_rect(&mut doc, at(1).inflate(2.0, 2.0), vectorcraft_color::Color::BLACK);
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
*clipping = true;
}
objects.push(group(&mut doc, vec![Arc::new(clip), n], true));
// 2: in another envelope.
let n = stand_in(&mut doc, at(2));
let r = at(2);
let points = vec![kurbo::Point::new(r.x0, r.y0), kurbo::Point::new(r.x1, r.y0), kurbo::Point::new(r.x0, r.y1), kurbo::Point::new(r.x1, r.y1)];
let kind = Box::new(vectorcraft_doc::live::EnvelopeKind::Mesh { rows: 1, cols: 1, points, handles: vec![] });
let options = vectorcraft_doc::live::EnvelopeOptions::NEW;
objects.push(Node::new(
doc.alloc_id(),
NodeKind::Envelope { content: vec![n], kind, fidelity: 50.0, editing: false, options, frame: Box::default() },
));
// 3: in a group with an appearance of its own.
let n = stand_in(&mut doc, at(3));
let mut styled = group(&mut doc, vec![n], false);
styled.appearance = fill.clone().into();
objects.push(styled);
// 4: in a group with an opacity mask.
let n = stand_in(&mut doc, at(4));
let mut masked = group(&mut doc, vec![n], false);
let mask = filled_rect(&mut doc, at(4).inflate(2.0, 2.0), vectorcraft_color::Color::WHITE);
masked.extras_mut().mask = Some(Box::new(vectorcraft_doc::OpacityMask::new(mask, true)));
objects.push(masked);
// 5: in a clipping sublayer.
let n = stand_in(&mut doc, at(5));
let mut clip = filled_rect(&mut doc, at(5).inflate(2.0, 2.0), vectorcraft_color::Color::BLACK);
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
*clipping = true;
}
let mut sublayer = Node::layer(doc.alloc_id(), "Clipped", vectorcraft_doc::LayerColor::Preset(1));
if let NodeKind::Layer { clip: c, children, .. } = &mut sublayer.kind {
(*c, *children) = (true, vec![Arc::new(clip), n]);
}
objects.push(sublayer);
// 6: in a repeat; 7: in a blend.
let n = stand_in(&mut doc, at(6));
objects.push(Node::new(doc.alloc_id(), NodeKind::Repeat(vectorcraft_doc::RepeatSpec::grid(vec![n], 0.0, 0.0))));
let n = stand_in(&mut doc, at(7));
objects.push(Node::new(doc.alloc_id(), NodeKind::Blend { children: vec![n], spec: Box::default() }));
// 8: off the page in a group with an appearance of its own (an effect could draw it onto the
// page); 9: off the page in a plain group.
let n = stand_in(&mut doc, off[0]);
let mut styled = group(&mut doc, vec![n], false);
styled.appearance = fill.into();
objects.push(styled);
let n = stand_in(&mut doc, off[1]);
objects.push(group(&mut doc, vec![n], false));
for n in objects {
doc.insert(Some(layer), 0, n).unwrap();
}
let frame = frame_of(&page, false);
let mut view = PageView::new(&frame);
let held = held_back(&frame, &mut view, &doc, &drawn, false);
assert_eq!(held, drawn[1..9].iter().map(|(id, ..)| *id).collect::<BTreeSet<_>>());
}
/// On a page that holds all the art that prints, which the page check also compares past the
/// page, an envelope of type reaching past the page, or lying off it, is held back; on another
/// page, the part on the page is weighed, and one off it stays live.
#[test]
fn on_a_page_that_holds_all_the_art_an_envelope_of_type_past_it_is_held_back() {
let on = Rect::new(10.0, 10.0, 30.0, 30.0);
let across = Rect::new(90.0, 40.0, 110.0, 60.0);
let off = Rect::new(120.0, 10.0, 140.0, 30.0);
let page = black_page(&[on, Rect::new(90.0, 40.0, 100.0, 60.0)]);
let (doc, drawn) = stand_ins(&[on, across, off], &[on, across, off]);
for (whole, held) in [(true, BTreeSet::from([drawn[1].0, drawn[2].0])), (false, BTreeSet::new())] {
let frame = frame_of(&page, whole);
let mut view = PageView::new(&frame);
assert_eq!(held_back(&frame, &mut view, &doc, &drawn, false), held, "whole: {whole}");
}
// On the page by its box, past it by what it draws: a Transform effect of its own draws a
// copy of it 100 points to the right, off the page.
let page = black_page(&[on]);
let (mut doc, drawn) = stand_ins(&[on], &[on]);
let effect =
vectorcraft_doc::Effect { id: "distort.transform".into(), params: serde_json::json!({"moveH": 100, "copies": 1}), visible: true };
doc.node_mut(drawn[0].0).unwrap().appearance.effects.push(effect);
for (whole, held) in [(true, BTreeSet::from([drawn[0].0])), (false, BTreeSet::new())] {
let frame = frame_of(&page, whole);
let mut view = PageView::new(&frame);
assert_eq!(held_back(&frame, &mut view, &doc, &drawn, false), held, "drawn past the page, whole: {whole}");
}
}
/// Where the objects that draw differently from the page are to come in as its art and the
/// envelopes of type the page doesn't hold stay live, but one of those holds the difference in
/// its box (an object that holds type isn't replaced), every envelope of type gets its drawn art
/// back and that is tried again: the layers are still used.
#[test]
fn objects_come_in_as_the_page_art_with_every_envelope_of_type_put_back_where_a_live_one_holds_a_difference() {
let (a, b, dot, other) = (
Rect::new(10.0, 10.0, 20.0, 20.0),
Rect::new(40.0, 10.0, 50.0, 20.0),
Rect::new(27.0, 12.0, 33.0, 18.0),
Rect::new(70.0, 70.0, 90.0, 90.0),
);
// The page draws a dot in the gap between the two rectangles of the envelope's stand-in.
let page = black_page(&[a, b, dot, other]);
let mut doc = Document::new(100.0, 100.0);
let layer = doc.layers[0].id;
let square = filled_rect(&mut doc, other, vectorcraft_color::Color::BLACK);
doc.insert(Some(layer), 0, square).unwrap();
// The stand-in holds type, hidden; its drawn art is the two rectangles.
let style = vectorcraft_doc::CharStyle::default();
let mut text =
Node::new(doc.alloc_id(), NodeKind::Text(Box::new(vectorcraft_doc::TextObject::point(kurbo::Point::new(12.0, 18.0), "A", style))));
text.visible = false;
let parts = vec![
Arc::new(filled_rect(&mut doc, a, vectorcraft_color::Color::BLACK)),
Arc::new(filled_rect(&mut doc, b, vectorcraft_color::Color::BLACK)),
];
let mut children = parts.clone();
children.push(Arc::new(text));
let live = Node::new(doc.alloc_id(), NodeKind::Group { children, clip: false });
let id = live.id;
doc.insert(Some(layer), 0, live).unwrap();
let art = Node::new(doc.alloc_id(), NodeKind::Group { children: parts, clip: false });
let drawn: Vec<Envelope> = vec![(id, vec![art], ai::ENVELOPE_TYPE_DRAWN)];
let frame = frame_of(&page, false);
let mut view = PageView::new(&frame);
assert!(compare(&frame, &mut view, &doc, false).is_err());
let layers = Placements { doc: &doc, unseen: 0, other: None, drawn: &drawn, places: &[(id, 0)] };
let mut free = None;
let found = keep_differing_typed(&frame, &mut view, &layers, &BTreeSet::new(), &[], false, &mut free);
assert_eq!(free, Some(vec![0]), "the page doesn't hold the envelope");
let Some((kept, _, notes, typed)) = found else { panic!("the layers aren't used") };
assert!(typed, "the envelope got its drawn art back");
assert!(kept.node(id).is_none());
assert!(notes.iter().any(|n| n.starts_with(DREW_DIFFERENTLY)), "{notes:?}");
}
}
+52 -10
View File
@@ -570,6 +570,34 @@ impl PageView {
}
}
}
/// Which pixels of each of the pages `on` `art` inks (draws over with an alpha above 8 of 255),
/// drawn as the layers are drawn on them, row by row.
pub(super) fn inked(&mut self, art: &Document, on: &[usize]) -> Vec<(usize, Vec<bool>)> {
self.draw(art, on)
.into_iter()
.map(|(i, drawn)| (i, drawn.pixels.as_chunks::<4>().0.iter().map(|p| u32::from(p[3]) > WHITE).collect()))
.collect()
}
/// Of the pixels of page `i` that `marks` marks (row by row), how many `b` draws differently from
/// the page, as [`compared`] counts them: those that differ (with `reflow`, those away from type),
/// and those that differ with all their neighbors, away from type. `None` when `marks` isn't the
/// page's size.
pub(super) fn differing_in(&mut self, b: &Document, i: usize, marks: &[bool], reflow: bool) -> Option<(usize, usize)> {
self.layered(b, &[i]);
let printed = as_printed(b);
let b = printed.as_ref().unwrap_or(b);
let page = self.pages.get(i)?;
let layered = self.drawn.last()?.pages.get(i)?.as_ref()?;
let (differs, typed, solid) = differing_pixels(&page.drawn, &layered.pixels, &page.counted_type(b, page.region, reflow), &page.skipped())?;
if marks.len() != differs.len() {
return None;
}
let any = marks.iter().zip(&differs).zip(&typed).filter(|((m, d), t)| **m && **d && !(reflow && **t)).count();
let solid = marks.iter().zip(&solid).filter(|(m, s)| **m && **s).count();
Some((any, solid))
}
}
/// The `w` × `h` pixels of `from` from pixel (`x`, `y`): `None` unless they lie in it, from a pixel.
@@ -682,8 +710,27 @@ pub(super) fn compared(x: &vectorcraft_render::Rendered, y: &vectorcraft_render:
/// [`compared`], and which of the pixels (row by row) are counted in its `solid` share: none when
/// the drawings can't be compared.
fn compared_with_solid(x: &vectorcraft_render::Rendered, y: &vectorcraft_render::Rendered, areas: &[Rect], skip: &[Rect]) -> (Difference, Vec<bool>) {
if x.width == 0 || x.height == 0 || x.width != y.width || x.height != y.height {
let Some((differs, typed, solid)) = differing_pixels(x, y, areas, skip) else {
return (Difference { any: 1.0, art: 1.0, solid: 1.0, typed: 0.0 }, vec![]);
};
let area = (f64::from(x.width) * f64::from(x.height)).max(1.0);
let any = differs.iter().filter(|d| **d).count() as f64 / area;
let art = differs.iter().zip(&typed).filter(|(d, t)| **d && !**t).count() as f64 / area;
let shares = Difference { any, art, solid: solid.iter().filter(|s| **s).count() as f64 / area, typed: any - art };
(shares, solid)
}
/// Which pixels (row by row) of two drawings of the same size differ ([`differ`], those in `skip`
/// counting as alike), which of those lie in the type's `areas`, and which differ with all their
/// neighbors away from type: [`compared`]'s pixels. `None` when the drawings can't be compared.
fn differing_pixels(
x: &vectorcraft_render::Rendered,
y: &vectorcraft_render::Rendered,
areas: &[Rect],
skip: &[Rect],
) -> Option<(Vec<bool>, Vec<bool>, Vec<bool>)> {
if x.width == 0 || x.height == 0 || x.width != y.width || x.height != y.height {
return None;
}
let (w, h) = (x.width as usize, x.height as usize);
// Pixels in `skip` count as alike.
@@ -691,10 +738,9 @@ fn compared_with_solid(x: &vectorcraft_render::Rendered, y: &vectorcraft_render:
let differs: Vec<bool> =
x.pixels.as_chunks::<4>().0.iter().zip(y.pixels.as_chunks::<4>().0).zip(&skipped).map(|((p, q), s)| !*s && differ(p, q)).collect();
if !differs.contains(&true) {
return (Difference { any: 0.0, art: 0.0, solid: 0.0, typed: 0.0 }, vec![false; w * h]);
return Some((differs, vec![false; w * h], vec![false; w * h]));
}
let typed = in_areas(areas, w, h);
let art = differs.iter().zip(&typed).filter(|(d, t)| **d && !**t).count();
let typed: Vec<bool> = in_areas(areas, w, h).into_iter().zip(&differs).map(|(t, d)| t && *d).collect();
// Which pixels differ with their left and right neighbors.
let mut across = vec![false; w * h];
for (k, a) in across.iter_mut().enumerate() {
@@ -706,11 +752,7 @@ fn compared_with_solid(x: &vectorcraft_render::Rendered, y: &vectorcraft_render:
for (k, s) in solid.iter_mut().enumerate().skip(w).take(w * h.saturating_sub(2)) {
*s = !typed.get(k).copied().unwrap_or(true) && [k - w, k, k + w].iter().all(|n| across.get(*n).copied().unwrap_or(false));
}
let area = (w as f64 * h as f64).max(1.0);
let any = differs.iter().filter(|d| **d).count() as f64 / area;
let art = art as f64 / area;
let shares = Difference { any, art, solid: solid.iter().filter(|s| **s).count() as f64 / area, typed: any - art };
(shares, solid)
Some((differs, typed, solid))
}
/// Where the type that shows in `nodes` is, in pixels of `region` drawn at `scale` (with a margin
@@ -879,7 +921,7 @@ const NOTABLE_DIFFERENCE: f64 = 0.002;
pub(in crate::import) const MAX_SOLID_DIFFERENCE: f64 = 0.001;
/// How far (points) the art the layers print may reach past a page that holds it all.
const PAST_THE_PAGE: f64 = 1.0;
pub(super) const PAST_THE_PAGE: f64 = 1.0;
/// Do the layers' art and the page's own look alike (where the page holds all the art that prints,
/// also round it), on every page the frame has ([`Frame::more`])? An error says they don't; `Ok` has
+19 -7
View File
@@ -1,4 +1,4 @@
use super::build::{PAGE_MOVE, Page, alone_scale, build, build_from, build_on_page, build_without_page, reid_page_art};
use super::build::{PAGE_MOVE, Page, alone_scale, build, build_from, build_on_page, build_weighing, build_without_page, reid_page_art};
use super::slots::LAYERS_UNREAD;
use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect, Vec2, ai};
@@ -177,15 +177,27 @@ pub fn layered_ai_from(
add_notes(&mut all, read.notes());
(visible, all, false)
};
let built = structure
.unwrap_or_else(|| ai::read_over_page(&data, false, names).map(Box::new))
.and_then(|structure| build_from(*structure, &data, Some(&visible), Page::Artboard, outlined, trust, unit));
// The envelopes of type the page doesn't hold against layers that don't draw as it, and how many
// envelopes of type the layers had.
let (mut free, mut count) = (None, 0);
let built = structure.unwrap_or_else(|| ai::read_over_page(&data, false, names).map(Box::new)).and_then(|structure| {
count = structure.warped_type.envelope_count();
build_weighing(*structure, &data, Some(&visible), Page::Artboard, outlined, trust, unit, &mut free)
});
// Layers that waited on the page only for their envelopes of type open alone, as they would
// have without them, each envelope keeping its drawn art, unless one opened live with the page.
let live = matches!(&built, Ok((done, _)) if holds_type_envelope(&done.layers));
// have without them, unless one opened live with the layers drawing as the page. Alone, each
// envelope keeps its drawn art but those the page didn't hold against the layers, which open
// live as they were weighed. They were weighed on the layers as compared with the page, before
// any object of them came in as the page's art: those layers aren't used here, whatever came of
// them.
let live = free.is_none() && matches!(&built, Ok((done, _)) if holds_type_envelope(&done.layers));
if !live
&& let Some((mut done, notes)) = alone.and_then(|(pages, unit)| {
let structure = ai::read_over_page(&data, false, names).ok()?;
let mut structure = ai::read_over_page(&data, false, names).ok()?;
// With none free, nothing needs settling again: every envelope keeps its drawn art.
if let Some(free) = free.as_deref().filter(|free| !free.is_empty()) {
structure.warped_type.open_alone(free, count);
}
build_without_page(structure, &data, &pages, unit).ok()?.ok()
})
{
+202 -21
View File
@@ -5528,6 +5528,11 @@ fn type_envelope(keys: &str, drawn: &str) -> String {
/// [`type_envelope`], with `after` (on plain lines, written on `%_` ones) after the text object in
/// its contents, such as the text object's XML id.
fn type_envelope_with(keys: &str, drawn: &str, after: &str) -> String {
type_envelope_of(0, keys, drawn, after)
}
/// [`type_envelope_with`] around the type of story `story`.
fn type_envelope_of(story: u32, keys: &str, drawn: &str, after: &str) -> String {
let corner = |x: f64, y: f64, nx: f64, ny: f64, px: f64, py: f64| format!("[0.8 1 {x} {y} {nx} {ny} 1 {px} {py} 1] /N X#\n");
let mesh = format!(
"/Mesh X!\n2 /Version X#\n/Cartesian /Type X#\n[1 1] /Size X#\n/Data X#\n/DeviceGray /CS X#\n[0 0] /P X#\n{}{}{}{}/E X#\n/End X!\n",
@@ -5536,18 +5541,23 @@ fn type_envelope_with(keys: &str, drawn: &str, after: &str) -> String {
corner(80.0, 70.0, 80.0, 60.0, 70.0, 70.0),
corner(80.0, 40.0, 70.0, 40.0, 80.0, 50.0),
);
type_envelope_on(keys, &mesh, drawn, after)
type_envelope_on_story(story, keys, &mesh, drawn, after)
}
/// [`type_envelope_with`], with `shape` (on plain lines, written on `%_` ones) in the group before
/// its dictionary, where the editing data saves the envelope's mesh.
fn type_envelope_on(keys: &str, shape: &str, drawn: &str, after: &str) -> String {
type_envelope_on_story(0, keys, shape, drawn, after)
}
/// [`type_envelope_on`] around the type of story `story`.
fn type_envelope_on_story(story: u32, keys: &str, shape: &str, drawn: &str, after: &str) -> String {
let hidden = |s: &str| -> String { s.lines().map(|l| format!("%_{l}\n")).collect() };
format!(
"{}{}%_X=\n{}%_X+\n%_ (AIDeformArtKey) ,\n%_1 /Int (AIDeformStyle) ,\n%_;\n%_\n%_9 () XW\nu\n{drawn}U\n9 () XW\n(Adobe Deform Plugin) 1 0 0 XP\n",
hidden(&format!("u\n{shape}U\n")),
hidden(&format!("/ArtDictionary :\n{keys}")),
hidden(&format!("u\n{}{after}U\n9 () XW\n", text_object(0))),
hidden(&format!("u\n{}{after}U\n9 () XW\n", text_object(story))),
)
}
@@ -5594,7 +5604,12 @@ fn arc_envelope(text: &Node, bend: f64) -> Node {
/// The type of the file with an envelope of type whose drawn art is `drawn`, read with its text
/// document `doc`: the hidden text object it keeps, as the reader makes it.
fn made_envelope_type(doc: &str) -> Node {
let art = native_with_text(&layer("Art", true, &type_envelope(&arc_keys(0.3), &format!("{}\n", square(10, 10)))), doc);
made_envelope_type_of(doc, 0)
}
/// [`made_envelope_type`] of story `story`.
fn made_envelope_type_of(doc: &str, story: u32) -> Node {
let art = native_with_text(&layer("Art", true, &type_envelope_of(story, &arc_keys(0.3), &format!("{}\n", square(10, 10)), "")), doc);
let r = import(&eps(&page_square(10, 10), &art)).unwrap();
let mut text = None;
r.document.walk(|n| {
@@ -5685,9 +5700,9 @@ fn an_envelope_of_white_type_that_draws_apart_from_its_drawn_art_keeps_it() {
/// A `.ai` whose layers would open alone, without its PDF part, waits on the PDF part when an
/// envelope of type is in them, as for the other live envelopes: one opens live only where the
/// layers with it draw as the page. Where they don't, they open alone as before, the envelope
/// keeping its drawn art and its type, hidden, with the note, even though it would draw as that
/// drawn art; where they do, it opens live (#3179).
/// layers with it draw as the page. Where they don't, the page drawing nothing where the envelope
/// is, they open alone as before, the envelope keeping its drawn art and its type, hidden, with the
/// note, even though it would draw as that drawn art; where they do, it opens live (#3179).
#[test]
fn an_envelope_of_type_waits_on_the_page_of_layers_that_would_open_alone() {
let Some(family) = installed_family() else { return };
@@ -5751,9 +5766,10 @@ fn an_envelope_of_type_that_draws_apart_from_the_page_does_not_open_live() {
}
/// Layers used though they draw differently from the page (the editing data trusted) leave an
/// envelope of type its drawn art and its type, hidden, with the note, even where it draws as that
/// drawn art: one opens live only where the layers with it draw as the page. A `.ai` whose layers
/// would open alone then opens alone, as it would without the envelope (#3179).
/// envelope of type its drawn art and its type, hidden, with the note, where the page draws
/// nothing where the envelope is, even where it draws as that drawn art: one opens live only where
/// the layers with it draw as the page there. A `.ai` whose layers would open alone then opens
/// alone, as it would without the envelope (#3179).
#[test]
fn trusted_layers_that_draw_apart_from_the_page_leave_an_envelope_of_type_its_drawn_art() {
let Some(family) = installed_family() else { return };
@@ -5789,19 +5805,27 @@ fn trusted_layers_that_draw_apart_from_the_page_leave_an_envelope_of_type_its_dr
assert!(!notes.iter().any(|n| n.ends_with("they come from its editing data all the same")), "{notes:?}");
}
/// Layers whose objects that draw differently from the page come in as its art leave an envelope
/// of type its drawn art and its type, hidden, with the note, even where the envelope draws as the
/// page: the layers with it don't (#3179).
#[test]
fn layers_with_objects_that_come_in_as_the_page_art_leave_an_envelope_of_type_its_drawn_art() {
let Some(family) = installed_family() else { return };
let doc = type_envelope_text(&family, None);
let (drawn, page) = drawn_envelope(&arc_envelope(&made_envelope_type(&doc), 30.0));
// A square black in the layers and cyan on the page, away from the envelope.
/// The layers and the page of a file with an envelope of type that draws as its drawn art, and a
/// square black in the layers and cyan on the page, away from the envelope; the page draws the
/// envelope bent by `page_bend` (percent).
fn envelope_beside_a_differing_square(family: &str, page_bend: f64) -> (String, String) {
let doc = type_envelope_text(family, None);
let text = made_envelope_type(&doc);
let (drawn, _) = drawn_envelope(&arc_envelope(&text, 30.0));
let (_, page) = drawn_envelope(&arc_envelope(&text, page_bend));
let body = format!("{}\n{}\n{}", square(10, 10), square_16(76, BLACK), type_envelope(&arc_keys(0.3), &drawn));
let art = native_with_text(&layer("Art", true, &body), &doc);
let cyan = "1 0 0 0 setcmykcolor 76 10 moveto 92 10 lineto 92 26 lineto 76 26 lineto closepath fill";
let r = import(&eps(&format!("{}\n{cyan}\n{page}", page_square(10, 10)), &art)).unwrap();
(native_with_text(&layer("Art", true, &body), &doc), format!("{}\n{cyan}\n{page}", page_square(10, 10)))
}
/// Layers whose objects that draw differently from the page come in as its art keep an envelope of
/// type live where the envelope draws as the page: the art that differs isn't the envelope's, and
/// the layers with it then draw as the page (#3179).
#[test]
fn layers_with_objects_that_come_in_as_the_page_art_keep_an_envelope_of_type_that_draws_as_the_page_live() {
let Some(family) = installed_family() else { return };
let (art, page) = envelope_beside_a_differing_square(&family, 30.0);
let r = import(&eps(&page, &art)).unwrap();
assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings);
assert!(
r.warnings.iter().any(|n| n.starts_with("objects that drew differently from the file's own page came in as its art")),
@@ -5809,12 +5833,169 @@ fn layers_with_objects_that_come_in_as_the_page_art_leave_an_envelope_of_type_it
r.warnings
);
let (envelopes, loose) = envelopes_and_type(&r.document);
assert_eq!(envelopes, [vec!["FLUX".to_string()]], "{:?}", r.warnings);
assert!(loose.is_empty(), "{loose:?}");
assert!(!r.warnings.iter().any(|n| n == crate::import::ai::ENVELOPE_TYPE_DRAWN), "{:?}", r.warnings);
}
/// Layers used though they draw differently from the page (the editing data trusted) leave an
/// envelope of type its drawn art and its type, hidden, with the note, where the page draws it
/// otherwise than they do (bent the other way), though other art differs too: the page holds the
/// envelope against them where it is (#3179).
#[test]
fn trusted_layers_leave_an_envelope_of_type_the_page_draws_otherwise_its_drawn_art() {
let Some(family) = installed_family() else { return };
let (art, page) = envelope_beside_a_differing_square(&family, -30.0);
let r = crate::import_trusting(&eps(&page, &art), true, true).unwrap();
assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings);
assert!(r.warnings.iter().any(|n| n.ends_with("they come from its editing data all the same")), "{:?}", r.warnings);
let (envelopes, loose) = envelopes_and_type(&r.document);
assert!(envelopes.is_empty(), "{envelopes:?}");
assert_eq!(loose, [("FLUX".to_string(), false)], "{:?}", r.warnings);
assert_eq!(loose_type_names(&r.document), [None]);
assert!(r.warnings.iter().any(|n| n == crate::import::ai::ENVELOPE_TYPE_DRAWN), "{:?}", r.warnings);
}
/// Layers used though they draw differently from the page (the editing data trusted) keep an
/// envelope of type live where the page draws it as they do and the art that differs is elsewhere
/// (#3179).
#[test]
fn trusted_layers_keep_an_envelope_of_type_live_where_other_art_differs_from_the_page() {
let Some(family) = installed_family() else { return };
let (art, page) = envelope_beside_a_differing_square(&family, 30.0);
let r = crate::import_trusting(&eps(&page, &art), true, true).unwrap();
assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings);
assert!(r.warnings.iter().any(|n| n.ends_with("they come from its editing data all the same")), "{:?}", r.warnings);
let (envelopes, loose) = envelopes_and_type(&r.document);
assert_eq!(envelopes, [vec!["FLUX".to_string()]], "{:?}", r.warnings);
assert!(loose.is_empty(), "{loose:?}");
assert!(!r.warnings.iter().any(|n| n == crate::import::ai::ENVELOPE_TYPE_DRAWN), "{:?}", r.warnings);
}
/// A `.ai` whose layers would open alone, and that waits on its PDF part for an envelope of type,
/// opens alone with the envelope live where the page draws it as the layers do, though the page
/// also draws art the layers don't have: the envelope's drawn art put back wouldn't make the layers
/// draw as the page (#3179).
#[test]
fn layers_that_would_open_alone_keep_an_envelope_of_type_live_where_other_art_differs_from_the_page() {
let Some(family) = installed_family() else { return };
let stories = [("FLUX\r", 0, (8170.0, 8180.0), (0.0, 0.0), 0.0), ("Note\r", 0, (8170.0, 8220.0), (0.0, 0.0), 0.0)];
let mut doc = text_document(&stories).replace("(Helvetica)", &format!("({family})"));
// [`native_with_text`] writes 60 characters a line: the end of the data, `~>`, isn't split.
while ascii85(doc.as_bytes()).len() % 60 == 1 {
doc.push('\n');
}
let (drawn, _) = drawn_envelope(&arc_envelope(&made_envelope_type(&doc), 30.0));
let art = format!("u\n{}U\n{}", text_object(1), type_envelope(&arc_keys(0.3), &drawn));
let file = private(&native_with_text(&layer("Art", true, &art), &doc));
let names = crate::NameEncoding::default();
let open = |page: Document| {
let Err(layers) = crate::ai_layers_alone(&file, &[(100.0, 100.0)], 1.0, names) else { panic!("opened without the page") };
crate::layered_ai_from(layers, page, vec![], false, false, 1.0, names)
};
// The layers alone, with the envelope's drawn art: what the page draws.
let (mut page, notes, _) = open(pdf_part(80.0, 80.0));
assert!(envelopes_and_type(&page).0.is_empty(), "{notes:?}");
// And a black square the layers don't have, away from the type.
add_black_square(&mut page, 80.0, 4.0);
let (d, notes, read) = open(page);
assert!(read, "{notes:?}");
let (envelopes, loose) = envelopes_and_type(&d);
assert_eq!(envelopes, [vec!["FLUX".to_string()]], "{notes:?}");
assert_eq!(loose, [("Note".to_string(), true)], "{notes:?}");
assert!(!notes.iter().any(|n| n == crate::import::ai::ENVELOPE_TYPE_DRAWN), "{notes:?}");
}
/// A black square of 16 points at (`x`, `y`) put on top of `page`'s first layer.
fn add_black_square(page: &mut Document, x: f64, y: f64) {
let paint = vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK);
let item = vectorcraft_doc::AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(paint));
let path = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(x, y, x + 16.0, y + 16.0));
let square = Node::path(page.alloc_id(), path, vectorcraft_doc::Appearance { items: vec![item], ..Default::default() });
let layer = page.layers[0].id;
page.insert(Some(layer), 0, square).unwrap();
}
/// A `.ai` whose layers would open alone, with the editing data trusted, opens alone with an
/// envelope of type live where the page draws it as the layers do, though the page also draws art
/// they don't have: not from the layers compared with the page, which would say they come from the
/// editing data all the same (#3179).
#[test]
fn trusted_layers_that_would_open_alone_open_alone_with_an_envelope_of_type_the_page_doesnt_hold_live() {
let Some(family) = installed_family() else { return };
let stories = [("FLUX\r", 0, (8170.0, 8180.0), (0.0, 0.0), 0.0), ("Note\r", 0, (8170.0, 8220.0), (0.0, 0.0), 0.0)];
let mut doc = text_document(&stories).replace("(Helvetica)", &format!("({family})"));
while ascii85(doc.as_bytes()).len() % 60 == 1 {
doc.push('\n');
}
let (drawn, _) = drawn_envelope(&arc_envelope(&made_envelope_type(&doc), 30.0));
let art = format!("u\n{}U\n{}", text_object(1), type_envelope(&arc_keys(0.3), &drawn));
let file = private(&native_with_text(&layer("Art", true, &art), &doc));
let names = crate::NameEncoding::default();
let open = |page: Document, trust: bool| {
let Err(layers) = crate::ai_layers_alone(&file, &[(100.0, 100.0)], 1.0, names) else { panic!("opened without the page") };
crate::layered_ai_from(layers, page, vec![], false, trust, 1.0, names)
};
let (mut page, _, _) = open(pdf_part(80.0, 80.0), false);
add_black_square(&mut page, 80.0, 4.0);
let (d, notes, read) = open(page, true);
assert!(read, "{notes:?}");
let (envelopes, loose) = envelopes_and_type(&d);
assert_eq!(envelopes, [vec!["FLUX".to_string()]], "{notes:?}");
assert_eq!(loose, [("Note".to_string(), true)], "{notes:?}");
assert!(notes.is_empty(), "{notes:?}");
}
/// Of two envelopes of type in a `.ai` whose layers would open alone, the one the page holds
/// against the layers (it draws nothing where the envelope is) keeps its drawn art and its type,
/// hidden, with the note, and the other opens live: each by its place among the envelopes of type
/// as the layers are read, both times they are (#3179).
#[test]
fn layers_that_would_open_alone_keep_live_only_the_envelopes_of_type_the_page_doesnt_hold() {
let Some(family) = installed_family() else { return };
let stories = [
("FLUX\r", 0, (8170.0, 8180.0), (0.0, 0.0), 0.0),
("HILL\r", 0, (8170.0, 8200.0), (0.0, 0.0), 0.0),
("Note\r", 0, (8170.0, 8220.0), (0.0, 0.0), 0.0),
];
let mut doc = text_document(&stories).replace("(Helvetica)", &format!("({family})"));
while ascii85(doc.as_bytes()).len() % 60 == 1 {
doc.push('\n');
}
let drawn = |story| drawn_envelope(&arc_envelope(&made_envelope_type_of(&doc, story), 30.0)).0;
let art = format!(
"u\n{}U\n{}{}",
text_object(2),
type_envelope_of(0, &arc_keys(0.3), &drawn(0), ""),
type_envelope_of(1, &arc_keys(0.3), &drawn(1), "")
);
let file = private(&native_with_text(&layer("Art", true, &art), &doc));
let names = crate::NameEncoding::default();
let open = |page: Document| {
let Err(layers) = crate::ai_layers_alone(&file, &[(100.0, 100.0)], 1.0, names) else { panic!("opened without the page") };
crate::layered_ai_from(layers, page, vec![], false, false, 1.0, names)
};
// The layers alone, with the drawn art of both envelopes, but the first's: the page draws
// nothing where the first is.
let (mut page, notes, _) = open(pdf_part(80.0, 80.0));
let top = page.layers[0].clone();
let first = top
.children()
.into_iter()
.flatten()
.filter(|n| matches!(n.kind, NodeKind::Group { .. }))
.min_by(|a, b| a.visual_bounds().map_or(f64::MAX, |r| r.y0).total_cmp(&b.visual_bounds().map_or(f64::MAX, |r| r.y0)))
.map(|n| n.id)
.unwrap_or_else(|| panic!("no drawn art: {notes:?}"));
page.remove(first).unwrap();
let (d, notes, read) = open(page);
assert!(read, "{notes:?}");
let (envelopes, mut loose) = envelopes_and_type(&d);
assert_eq!(envelopes, [vec!["HILL".to_string()]], "{notes:?}");
loose.sort();
assert_eq!(loose, [("FLUX".to_string(), false), ("Note".to_string(), true)], "{notes:?}");
assert!(notes.iter().any(|n| n == crate::import::ai::ENVELOPE_TYPE_DRAWN), "{notes:?}");
}
/// An envelope of type whose type a visibility variable binds keeps its drawn art, as one whose type
/// a text variable binds does: the binding stays the hidden type's, which the live envelope would
/// take the place of (#3179).
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long