mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 18:10:01 -06:00
Follow-ups to the review of #4697 and #4698: - With textAs: outlines, a .ai saved without PDF compatibility, read from its editing data alone, has its type outlined too, as an empty page or a text-only placeholder already did. - Threaded type read from the editing data holds its story in its first frame: it now flows through its frames before it is outlined, so each frame's outlines show the text it holds, and the outlined frames leave their threads. - Each outline group keeps its type's opacity, blend mode, isolation, opacity mask and effects (the same carry-over the flattener uses). - A gradient-mesh corner (/N) holding anything but numbers is unread, instead of being read with the numbers after it shifted into its colour and opacity.
153 lines
6.1 KiB
Rust
153 lines
6.1 KiB
Rust
use super::{Arc, BTreeSet, Document, Effect, ImageBlob, Knockout, Node, NodeKind, Rect, effects, outline_strokes};
|
|
|
|
// ---------- plain art ----------
|
|
|
|
/// Converts art into what the compositor works on.
|
|
pub(super) struct Scratch {
|
|
/// A copy of the document the art comes from (outlining allocates ids in it).
|
|
pub(super) doc: Document,
|
|
pub(super) brushes: Vec<vectorcraft_brush::Brush>,
|
|
}
|
|
|
|
impl Scratch {
|
|
/// `n` as plain filled shapes: type outlined, symbol instances replaced by their art, live
|
|
/// objects evaluated and strokes outlined (brush strokes as their art), each keeping the
|
|
/// object's transparency.
|
|
pub(super) fn plain(&mut self, n: &Node) -> Node {
|
|
if !n.visible {
|
|
return n.clone();
|
|
}
|
|
let made = match &n.kind {
|
|
NodeKind::Text(_) => outlined_text(n),
|
|
NodeKind::SymbolInstance { symbol, .. } => {
|
|
let art = self.doc.symbols.iter().find(|s| &s.name == symbol).map(|s| vectorcraft_render::instance_art(&s.art, n));
|
|
art.and_then(|a| effects::outline_art(Some(&self.doc), n, Some(&a))).map(|g| carry(n, g))
|
|
}
|
|
// A placed document stays one: its art's resources aren't the document's.
|
|
NodeKind::PlacedDocument(_) => None,
|
|
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) => {
|
|
let hook: &dyn Fn(&Node) -> Option<Node> = &effects::outline_text;
|
|
let hooks = vectorcraft_doc::live::Hooks { outline: Some(hook), symbols: Some(&self.doc), ..Default::default() };
|
|
Some(carry(n, vectorcraft_doc::live::expanded_group_hooks(n, hooks)))
|
|
}
|
|
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => outline_strokes(&mut self.doc, &self.brushes, n),
|
|
_ => None,
|
|
};
|
|
let mut m = match made {
|
|
// The pieces of one object never knock each other out.
|
|
Some(m) if m.is_container() => Node { knockout: Knockout::Off, ..m },
|
|
Some(m) => m,
|
|
None => n.clone(),
|
|
};
|
|
if !matches!(m.kind, NodeKind::Compound { .. })
|
|
&& let Some(ch) = m.children_mut()
|
|
{
|
|
for c in ch.iter_mut() {
|
|
*c = Arc::new(self.plain(c));
|
|
}
|
|
}
|
|
m
|
|
}
|
|
}
|
|
|
|
/// Type as outlines painted like it ([`effects::outline_text`]), keeping its transparency.
|
|
pub(crate) fn outlined_text(n: &Node) -> Option<Node> {
|
|
effects::outline_text(n).map(|g| Node { knockout: Knockout::Off, ..carry(n, g) })
|
|
}
|
|
|
|
/// `to` (art made from `from`) with `from`'s name, transparency, opacity mask and effects.
|
|
fn carry(from: &Node, mut to: Node) -> Node {
|
|
to.name = from.name.clone();
|
|
to.opacity = from.opacity;
|
|
to.blend = from.blend;
|
|
to.isolate = from.isolate;
|
|
to.knockout_shape = from.knockout_shape;
|
|
to.extras_mut().mask = from.extras().mask.clone();
|
|
to.appearance.effects = from.appearance.effects.clone();
|
|
to
|
|
}
|
|
|
|
/// The keys of `doc`'s images that have see-through pixels (an alpha channel that isn't opaque
|
|
/// everywhere).
|
|
pub(crate) fn see_through_images(doc: &Document) -> BTreeSet<String> {
|
|
let see_through = |b: &ImageBlob| {
|
|
b.mime != "image/jpeg" && image::load_from_memory(&b.bytes).is_ok_and(|i| i.color().has_alpha() && i.to_rgba8().pixels().any(|p| p[3] < 255))
|
|
};
|
|
doc.images.iter().filter(|(_, b)| see_through(b)).map(|(k, _)| k.clone()).collect()
|
|
}
|
|
|
|
/// `n` shows an image of `keys` (itself or inside).
|
|
pub(super) fn draws_image(n: &Node, keys: &BTreeSet<String>) -> bool {
|
|
!keys.is_empty()
|
|
&& n.visible
|
|
&& match &n.kind {
|
|
NodeKind::Image(im) => keys.contains(&im.key),
|
|
_ => n.children().is_some_and(|ch| ch.iter().any(|c| draws_image(c, keys))),
|
|
}
|
|
}
|
|
|
|
fn visible_fx(fx: &[Effect]) -> bool {
|
|
fx.iter().any(|e| e.visible)
|
|
}
|
|
|
|
/// Effects on the object or on one of its fills or strokes (raster effects, once baked).
|
|
pub(super) fn has_effects(n: &Node) -> bool {
|
|
visible_fx(&n.appearance.effects) || n.appearance.items.iter().any(|i| visible_fx(i.effects()))
|
|
}
|
|
|
|
pub(super) fn masked(n: &Node) -> bool {
|
|
n.extras().mask.as_ref().is_some_and(|m| !m.disabled)
|
|
}
|
|
|
|
/// Where `n` can paint: its visual bounds grown by its effects (and a margin for antialiasing).
|
|
pub(super) fn reach(n: &Node) -> Option<Rect> {
|
|
if !n.visible {
|
|
return None;
|
|
}
|
|
let b = match n.children() {
|
|
Some(ch) if !n.clips() && !matches!(n.kind, NodeKind::Compound { .. }) => ch.iter().filter_map(|c| reach(c)).reduce(|a, b| a.union(b))?,
|
|
_ => n.visual_bounds()?,
|
|
};
|
|
let fx = n.appearance.items.iter().map(|i| effects::outset(i.effects(), b)).fold(effects::outset(&n.appearance.effects, b), f64::max);
|
|
Some(b.inflate(fx + 1.0, fx + 1.0))
|
|
}
|
|
|
|
/// Indices of `bounds` grouped by overlap (transitively), each group in index order.
|
|
pub(super) fn overlapping(bounds: &[Option<Rect>]) -> Vec<Vec<usize>> {
|
|
fn find(p: &mut [usize], mut i: usize) -> usize {
|
|
while p[i] != i {
|
|
p[i] = p[p[i]];
|
|
i = p[i];
|
|
}
|
|
i
|
|
}
|
|
let mut parent: Vec<usize> = (0..bounds.len()).collect();
|
|
let mut order: Vec<(usize, Rect)> = bounds.iter().enumerate().filter_map(|(i, b)| b.map(|b| (i, b))).collect();
|
|
order.sort_by(|a, b| a.1.x0.total_cmp(&b.1.x0));
|
|
// Sweep left to right, comparing each box with those still open at its left edge.
|
|
let mut open: Vec<(usize, Rect)> = vec![];
|
|
for (i, b) in order {
|
|
open.retain(|(_, o)| o.x1 >= b.x0);
|
|
for (j, o) in &open {
|
|
if o.y0 <= b.y1 && b.y0 <= o.y1 {
|
|
let (a, c) = (find(&mut parent, i), find(&mut parent, *j));
|
|
parent[a.max(c)] = a.min(c);
|
|
}
|
|
}
|
|
open.push((i, b));
|
|
}
|
|
let mut groups: Vec<Vec<usize>> = vec![];
|
|
let mut slot: Vec<Option<usize>> = vec![None; bounds.len()];
|
|
for i in 0..bounds.len() {
|
|
let r = find(&mut parent, i);
|
|
match slot[r] {
|
|
Some(g) => groups[g].push(i),
|
|
None => {
|
|
slot[r] = Some(groups.len());
|
|
groups.push(vec![i]);
|
|
}
|
|
}
|
|
}
|
|
groups
|
|
}
|