Files
vectorcraft/crates/doc/src/appearance.rs
T

511 lines
22 KiB
Rust

//! 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<Effect> {
match self {
AppearanceItem::Fill(f) => &f.effects,
AppearanceItem::Stroke(s) => &s.effects,
}
}
pub fn effects_mut(&mut self) -> &mut Vec<Effect> {
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<AppearanceItem>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub effects: Vec<Effect>,
/// 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<usize>,
}
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<usize>) -> 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<usize>) -> 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<usize>) -> 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<usize>) -> 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<usize>, fill: bool) -> Option<usize> {
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<usize>) -> 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<usize>) -> 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<usize>, 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<usize>, 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<usize>, 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<usize>, 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<usize>) -> Option<&Vec<Effect>> {
match index {
None => Some(&self.effects),
Some(i) => self.items.get(i).map(AppearanceItem::effects),
}
}
pub fn effects_mut(&mut self, index: Option<usize>) -> Option<&mut Vec<Effect>> {
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<Item = &StrokeLayer> {
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<Item = &mut vectorcraft_color::GradientPaint> {
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<Item = &vectorcraft_color::GradientPaint> {
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<Effect>| 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<AppearanceItem> {
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<usize> {
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;