//! The appearance model: a stack of fills and strokes, each with its own opacity, blend mode and //! effects, plus object-level effects. use serde::{Deserialize, Serialize}; use vectorcraft_color::{BlendMode, Color, Paint}; mod layers; mod profile; mod stroke_style; pub(crate) use layers::HEAD_REACH; pub use layers::{Effect, FillLayer, StrokeGradientMode, StrokeLayer, stroke_paint_bounds}; pub use profile::{PressureProfile, ProfilePreset, SavedProfile, WidthProfile}; pub use stroke_style::{ArrowAlign, Arrowhead, Dash, LineCap, LineJoin, StrokeAlign}; /// One entry of the appearance stack. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "lowercase")] pub enum AppearanceItem { Fill(FillLayer), Stroke(StrokeLayer), } impl AppearanceItem { pub fn is_fill(&self) -> bool { matches!(self, AppearanceItem::Fill(_)) } /// `"fill"` or `"stroke"` (the serialized `kind`). pub fn kind_name(&self) -> &'static str { if self.is_fill() { "fill" } else { "stroke" } } pub fn paint(&self) -> &Paint { match self { AppearanceItem::Fill(f) => &f.paint, AppearanceItem::Stroke(s) => &s.paint, } } pub fn paint_mut(&mut self) -> &mut Paint { match self { AppearanceItem::Fill(f) => &mut f.paint, AppearanceItem::Stroke(s) => &mut s.paint, } } pub fn visible(&self) -> bool { match self { AppearanceItem::Fill(f) => f.visible, AppearanceItem::Stroke(s) => s.visible, } } pub fn opacity(&self) -> f32 { match self { AppearanceItem::Fill(f) => f.opacity, AppearanceItem::Stroke(s) => s.opacity, } } pub fn blend(&self) -> BlendMode { match self { AppearanceItem::Fill(f) => f.blend, AppearanceItem::Stroke(s) => s.blend, } } /// The item's own live effects (applied to this fill or stroke only). pub fn effects(&self) -> &Vec { match self { AppearanceItem::Fill(f) => &f.effects, AppearanceItem::Stroke(s) => &s.effects, } } pub fn effects_mut(&mut self) -> &mut Vec { match self { AppearanceItem::Fill(f) => &mut f.effects, AppearanceItem::Stroke(s) => &mut s.effects, } } /// Whether this fill or stroke overprints. pub fn overprint(&self) -> bool { match self { AppearanceItem::Fill(f) => f.overprint, AppearanceItem::Stroke(s) => s.overprint, } } pub fn overprint_mut(&mut self) -> &mut bool { match self { AppearanceItem::Fill(f) => &mut f.overprint, AppearanceItem::Stroke(s) => &mut s.overprint, } } } /// Appearance attributes. `items` is in paint order: `items[0]` is painted first (the bottom of the /// Appearance panel list). #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct Appearance { #[serde(default)] pub items: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub effects: Vec, /// Groups, layers, type and images: how many items paint below the object's contents (its /// members, type's characters, or an image's pixels), the slot of the Appearance panel's /// Contents (Characters) row. `None`: every item paints above the contents (see /// [`Self::contents_at`]). #[serde(default, skip_serializing_if = "Option::is_none")] pub contents_index: Option, } impl Appearance { /// Illustrator's "basic appearance": one fill below one stroke. pub fn basic(fill: Paint, stroke: Paint, width: f64) -> Self { Self { items: vec![AppearanceItem::Fill(FillLayer::new(fill)), AppearanceItem::Stroke(StrokeLayer::new(stroke, width))], ..Self::default() } } /// White fill, 1 pt black stroke (the default for new art). pub fn default_art() -> Self { Self::basic(Paint::solid(Color::WHITE), Paint::solid(Color::BLACK), 1.0) } /// The topmost fill (what the Fill proxy shows). pub fn fill(&self) -> Option<&FillLayer> { self.items.iter().rev().find_map(|i| if let AppearanceItem::Fill(f) = i { Some(f) } else { None }) } pub fn fill_mut(&mut self) -> Option<&mut FillLayer> { self.items.iter_mut().rev().find_map(|i| if let AppearanceItem::Fill(f) = i { Some(f) } else { None }) } /// The topmost stroke (what the Stroke proxy shows). pub fn stroke(&self) -> Option<&StrokeLayer> { self.items.iter().rev().find_map(|i| if let AppearanceItem::Stroke(s) = i { Some(s) } else { None }) } pub fn stroke_mut(&mut self) -> Option<&mut StrokeLayer> { self.items.iter_mut().rev().find_map(|i| if let AppearanceItem::Stroke(s) = i { Some(s) } else { None }) } /// This appearance for other art: the pen pressure its strokes were drawn with stays with them. pub fn without_pressure(mut self) -> Self { for it in &mut self.items { if let AppearanceItem::Stroke(s) = it { s.pressure = None; } } self } pub fn fill_paint(&self) -> Paint { self.fill().map(|f| f.paint.clone()).unwrap_or(Paint::None) } pub fn stroke_paint(&self) -> Paint { self.stroke().map(|s| s.paint.clone()).unwrap_or(Paint::None) } /// Set the top fill's paint, creating a fill if there is none. pub fn set_fill(&mut self, p: Paint) { match self.fill_mut() { Some(f) => f.paint = p, None => self.insert_item(0, AppearanceItem::Fill(FillLayer::new(p))), } } /// Set the top stroke's paint, creating a 1 pt stroke if there is none. pub fn set_stroke(&mut self, p: Paint) { match self.stroke_mut() { Some(s) => s.paint = p, None => self.items.push(AppearanceItem::Stroke(StrokeLayer::new(p, 1.0))), } } pub fn stroke_width(&self) -> f64 { self.stroke().filter(|s| !s.paint.is_none()).map(|s| s.width).unwrap_or(0.0) } /// Is this a basic appearance: at most one fill and one stroke, none hidden or with its own /// opacity, blend mode or effects, and no object effects? pub fn is_basic(&self) -> bool { let count = |fill: bool| self.items.iter().filter(|i| i.is_fill() == fill).count(); self.effects.is_empty() && self.contents_index.is_none() && count(true) <= 1 && count(false) <= 1 && self.items.iter().all(|i| i.visible() && i.effects().is_empty() && i.opacity() == 1.0 && i.blend() == BlendMode::Normal) } /// Fill item `index`, or the topmost fill for `None`. `None` when that item is not a fill. pub fn fill_at(&self, index: Option) -> Option<&FillLayer> { match index { None => self.fill(), Some(i) => match self.items.get(i)? { AppearanceItem::Fill(f) => Some(f), AppearanceItem::Stroke(_) => None, }, } } pub fn fill_at_mut(&mut self, index: Option) -> Option<&mut FillLayer> { match index { None => self.fill_mut(), Some(i) => match self.items.get_mut(i)? { AppearanceItem::Fill(f) => Some(f), AppearanceItem::Stroke(_) => None, }, } } /// Stroke item `index`, or the topmost stroke for `None`. `None` when that item is not a stroke. pub fn stroke_at(&self, index: Option) -> Option<&StrokeLayer> { match index { None => self.stroke(), Some(i) => match self.items.get(i)? { AppearanceItem::Stroke(s) => Some(s), AppearanceItem::Fill(_) => None, }, } } pub fn stroke_at_mut(&mut self, index: Option) -> Option<&mut StrokeLayer> { match index { None => self.stroke_mut(), Some(i) => match self.items.get_mut(i)? { AppearanceItem::Stroke(s) => Some(s), AppearanceItem::Fill(_) => None, }, } } /// `index` when it names a fill (`fill`) or stroke item, else `None` (the topmost one): the /// item a fill or stroke edit changes while item `index` is the Appearance panel's target. pub fn item_of_kind(&self, index: Option, fill: bool) -> Option { index.filter(|i| self.items.get(*i).is_some_and(|it| it.is_fill() == fill)) } /// The fill the Fill proxy shows while item `index` is targeted: that item when it is a fill, /// else the topmost fill. pub fn fill_for(&self, index: Option) -> Option<&FillLayer> { self.fill_at(self.item_of_kind(index, true)) } /// The stroke the Stroke proxy and panel show while item `index` is targeted. pub fn stroke_for(&self, index: Option) -> Option<&StrokeLayer> { self.stroke_at(self.item_of_kind(index, false)) } /// Set the paint of fill item `index` (`None`: the top fill, created if missing). False when /// `index` is not a fill. pub fn set_fill_at(&mut self, index: Option, p: Paint) -> bool { if index.is_none() { self.set_fill(p); return true; } self.fill_at_mut(index).map(|f| f.paint = p).is_some() } /// Set the paint of stroke item `index` (`None`: the top stroke, created if missing). False /// when `index` is not a stroke. pub fn set_stroke_at(&mut self, index: Option, p: Paint) -> bool { if index.is_none() { self.set_stroke(p); return true; } self.stroke_at_mut(index).map(|s| s.paint = p).is_some() } /// The paint of fill (`fill`) or stroke item `index` (`None`: the topmost one). pub fn paint_at(&self, index: Option, fill: bool) -> Option<&Paint> { if fill { self.fill_at(index).map(|f| &f.paint) } else { self.stroke_at(index).map(|s| &s.paint) } } /// [`Self::set_fill_at`] or [`Self::set_stroke_at`]. pub fn set_paint_at(&mut self, index: Option, fill: bool, p: Paint) -> bool { if fill { self.set_fill_at(index, p) } else { self.set_stroke_at(index, p) } } /// The effects of item `index`, or the object-level effects for `None`. pub fn effects_at(&self, index: Option) -> Option<&Vec> { match index { None => Some(&self.effects), Some(i) => self.items.get(i).map(AppearanceItem::effects), } } pub fn effects_mut(&mut self, index: Option) -> Option<&mut Vec> { match index { None => Some(&mut self.effects), Some(i) => self.items.get_mut(i).map(AppearanceItem::effects_mut), } } /// The visible strokes that paint something. pub fn painted_strokes(&self) -> impl Iterator { self.items.iter().filter_map(|i| match i { AppearanceItem::Stroke(s) if s.visible && !s.paint.is_none() => Some(s), _ => None, }) } /// Largest distance the painted area can extend beyond any geometry (see /// [`StrokeLayer::reach`]); paths measure their own with [`Self::stroked_bounds`]. pub fn outset(&self) -> f64 { self.painted_strokes().map(StrokeLayer::reach).fold(0.0, f64::max) } /// Scale stroke weights, dash lengths and dash offsets (Scale Strokes & Effects). pub fn scale_strokes(&mut self, s: f64) { for i in &mut self.items { if let AppearanceItem::Stroke(st) = i { st.width *= s; if let Some(d) = &mut st.dash { d.scale(s); } } } } /// Scale the distance parameters of every effect (the object's and each fill's and stroke's) /// by `s` with `scale` (the effects catalogue knows which parameters are distances). pub fn scale_effects(&mut self, s: f64, scale: fn(&mut Effect, f64)) { let items = self.items.iter_mut().flat_map(|i| i.effects_mut().iter_mut()); for e in self.effects.iter_mut().chain(items) { scale(e, s); } } /// Gradient paints of the fills and strokes. fn gradients_mut(&mut self) -> impl Iterator { self.items.iter_mut().filter_map(|i| match i { AppearanceItem::Fill(FillLayer { paint: Paint::Gradient(g), .. }) | AppearanceItem::Stroke(StrokeLayer { paint: Paint::Gradient(g), .. }) => Some(&mut **g), _ => None, }) } /// Gradient paints of the fills and strokes. pub(crate) fn gradients(&self) -> impl Iterator { self.items.iter().filter_map(|i| match i { AppearanceItem::Fill(FillLayer { paint: Paint::Gradient(g), .. }) | AppearanceItem::Stroke(StrokeLayer { paint: Paint::Gradient(g), .. }) => Some(&**g), _ => None, }) } /// Is `self` equal to `other`, placed gradients within rounding (1e-6 pt) of each other, as /// moving them between boxes leaves them? pub fn approx_eq(&self, other: &Appearance) -> bool { if self == other { return true; } let mut a = self.clone(); for (g, o) in a.gradients_mut().zip(other.gradients()) { if let (Some(ga), Some(go)) = (&mut g.geom, &o.geom) && ga.start.distance(go.start) <= 1e-6 && ga.end.distance(go.end) <= 1e-6 && (ga.aspect - go.aspect).abs() <= 1e-6 && ga.focal_point().distance(go.focal_point()) <= 1e-6 { *ga = *go; } if let (Some(fa), Some(fo)) = (&mut g.freeform, &o.freeform) && fa.points.len() == fo.points.len() && fa.points.iter().zip(&fo.points).all(|(p, q)| p.at.distance(q.at) <= 1e-6) { fa.points.iter_mut().zip(&fo.points).for_each(|(p, q)| p.at = q.at); } } a == *other } /// Does a fill or stroke carry a gradient whose placement `placed` is (some: placed in document /// space; none: fitted to the bounds on each render)? fn has_gradient(&self, placed: bool) -> bool { self.items.iter().any(|i| match i { AppearanceItem::Fill(FillLayer { paint: Paint::Gradient(g), .. }) | AppearanceItem::Stroke(StrokeLayer { paint: Paint::Gradient(g), .. }) => g.is_placed() == placed, _ => false, }) } /// Does a fill or stroke carry a gradient that is fitted to the bounds on each render? pub fn has_unplaced_gradient(&self) -> bool { self.has_gradient(false) } /// Does a fill or stroke carry a placed gradient (start and end in document space)? pub fn has_placed_gradient(&self) -> bool { self.has_gradient(true) } /// Fix unplaced gradients to their fit on `bounds` (the object's geometric bounds; strokes fit /// the stroke-inflated box), so they can follow transforms that refitting wouldn't reproduce. pub fn pin_gradients(&mut self, bounds: vectorcraft_geom::Rect) { for i in &mut self.items { match i { AppearanceItem::Fill(FillLayer { paint: Paint::Gradient(g), .. }) => g.pin(bounds), AppearanceItem::Stroke(s) => { let b = s.paint_bounds(bounds); if let Paint::Gradient(g) = &mut s.paint { g.pin(b); } } _ => {} } } } /// Move placed gradients from an object whose geometric bounds are `from` to one whose bounds /// are `to`, keeping them at the same place relative to the box (strokes: relative to their /// stroke-inflated boxes, as they fit). pub fn rebase_gradients(&mut self, from: vectorcraft_geom::Rect, to: vectorcraft_geom::Rect) { for i in &mut self.items { match i { AppearanceItem::Fill(FillLayer { paint: Paint::Gradient(g), .. }) => g.rebase(from, to), AppearanceItem::Stroke(s) => { let (f, t) = (s.paint_bounds(from), s.paint_bounds(to)); if let Paint::Gradient(g) = &mut s.paint { g.rebase(f, t); } } _ => {} } } } /// Map placed gradients through `a`. pub fn transform_gradients(&mut self, a: vectorcraft_geom::Affine) { for g in self.gradients_mut() { g.transform(a); } } /// Envelope Options → Distort Linear Gradients: bend the linear gradients of an object whose /// geometric bounds are `bounds` with a warp, given its local affine approximation at a point /// (taken at the middle of each gradient's vector). Unplaced ones are fixed to their fit first; /// other gradients stay as they are. pub fn warp_linear_gradients(&mut self, bounds: vectorcraft_geom::Rect, near: &dyn Fn(vectorcraft_geom::Point) -> vectorcraft_geom::Affine) { for i in &mut self.items { let (g, b) = match i { AppearanceItem::Fill(FillLayer { paint: Paint::Gradient(g), .. }) => (g, bounds), AppearanceItem::Stroke(s) => { let b = s.paint_bounds(bounds); let Paint::Gradient(g) = &mut s.paint else { continue }; (g, b) } _ => continue, }; if g.gradient.kind != vectorcraft_color::GradientKind::Linear { continue; } g.pin(b); if let Some(geom) = g.geom { g.transform(near(geom.start.midpoint(geom.end))); } } } /// Map placed gradients through a warp, given its local affine approximation at a point (taken /// at each gradient's centre: the start of a radial, the middle of a linear vector). Freeform /// points each follow the warp at their own position. pub fn warp_gradients(&mut self, near: &dyn Fn(vectorcraft_geom::Point) -> vectorcraft_geom::Affine) { for g in self.gradients_mut() { let freeform = g.freeform.take(); if let Some(geom) = g.geom { let c = if g.gradient.kind == vectorcraft_color::GradientKind::Radial { geom.start } else { geom.start.midpoint(geom.end) }; g.transform(near(c)); } g.freeform = freeform.map(|mut f| { f.map_points(|p| near(p) * p); f }); } } } impl Appearance { /// Is any fill, stroke or effect (the object's or an item's) hidden? pub fn has_hidden(&self) -> bool { let hidden = |fx: &[Effect]| fx.iter().any(|e| !e.visible); hidden(&self.effects) || self.items.iter().any(|i| !i.visible() || hidden(i.effects())) } /// Make every hidden fill, stroke and effect visible (Show All Hidden Attributes). Returns /// whether anything was hidden. pub fn show_all(&mut self) -> bool { let had = self.has_hidden(); let show = |fx: &mut Vec| fx.iter_mut().for_each(|e| e.visible = true); show(&mut self.effects); for i in &mut self.items { match i { AppearanceItem::Fill(f) => f.visible = true, AppearanceItem::Stroke(s) => s.visible = true, } show(i.effects_mut()); } had } } /// The contents slot: where a group's or layer's members (type: its characters) paint among the /// object's own fills and strokes. impl Appearance { /// How many items paint below the contents (the Contents/Characters row sits above them). pub fn contents_at(&self) -> usize { self.contents_index.unwrap_or(0).min(self.items.len()) } /// Put the contents above the bottom `k` items (clamped; 0, all items above, is stored as `None`). pub fn set_contents_at(&mut self, k: usize) { let k = k.min(self.items.len()); self.contents_index = (k > 0).then_some(k); } /// The items painted below the contents and those painted above them. pub fn split_contents(&self) -> (&[AppearanceItem], &[AppearanceItem]) { self.items.split_at(self.contents_at()) } /// Insert `item` at paint-order index `at` (clamped). The contents stay between the same items; /// an item landing right above them joins the item below it (below the contents if that one is). pub fn insert_item(&mut self, at: usize, item: AppearanceItem) { let k = self.contents_at(); let at = at.min(self.items.len()); self.items.insert(at, item); if at < k || (at == k && k > 0) { self.set_contents_at(k + 1); } } /// Remove item `i` (if it exists), keeping the contents between the same items. pub fn remove_item(&mut self, i: usize) -> Option { let k = self.contents_at(); (i < self.items.len()).then(|| { let it = self.items.remove(i); if i < k { self.set_contents_at(k - 1); } it }) } /// Move item `from` to paint-order index `to` (clamped) and return where it landed, or `None` /// when there is no item `from`. The contents stay between the same other items, and the moved /// item keeps its side of them when it lands right next to them. pub fn move_item(&mut self, from: usize, to: usize) -> Option { let k = self.contents_at(); let below = from < k; let it = self.remove_item(from)?; let others_below = self.contents_at(); let to = to.min(self.items.len()); self.items.insert(to, it); let now_below = if below { to <= others_below } else { to < others_below }; self.set_contents_at(others_below + usize::from(now_below)); Some(to) } } #[cfg(test)] mod tests;