mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 10:02:38 +00:00
A path that keeps its shape because the effect would pass the geometry budget holds a group's own placed gradients in place. A hidden path in a layer, which the layer's bounds leave out, no longer holds them, so the layer's gradient moves with its visible art. The art's own gradients follow the same rule. A freeform gradient whose points were edited keeps its vector fitted to the bounds, so it did not count as placed and its points stayed behind under a Transform effect and Free Distort unless another placed gradient was in the same appearance. Its points now move like a placed gradient.
665 lines
29 KiB
Rust
665 lines
29 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::{AngleProfile, PressureProfile, ProfilePreset, SavedProfile, WidthProfile};
|
||
pub use stroke_style::{ArrowAlign, Arrowhead, Dash, LineCap, LineJoin, StrokeAlign};
|
||
|
||
/// One entry of the appearance stack.
|
||
// A stroke takes 328 bytes and a fill 104; an appearance holds a few of each.
|
||
#[allow(clippy::large_enum_variant)]
|
||
#[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, tilt, bearing and rotation its strokes
|
||
/// were drawn with stay with them.
|
||
pub fn without_pressure(mut self) -> Self {
|
||
for it in &mut self.items {
|
||
if let AppearanceItem::Stroke(s) = it {
|
||
s.pressure = None;
|
||
s.tilt = None;
|
||
s.bearing = None;
|
||
s.rotation = 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);
|
||
}
|
||
}
|
||
/// The id of Effect › SVG Filters' effect, whose `origin` parameter is a point of the document.
|
||
pub const SVG_FILTER: &str = "svg.filter";
|
||
|
||
/// Does any effect (the object's or a fill's or stroke's) hold an SVG filter?
|
||
pub fn has_svg_filter(&self) -> bool {
|
||
self.effects.iter().chain(self.items.iter().flat_map(|i| i.effects().iter())).any(|e| e.id == Self::SVG_FILTER)
|
||
}
|
||
|
||
/// Move the `origin` of every SVG filter effect (where its user space starts in the document)
|
||
/// by `a`, as the object it is on moves.
|
||
pub fn transform_svg_filters(&mut self, a: kurbo::Affine) {
|
||
let items = self.items.iter_mut().flat_map(|i| i.effects_mut().iter_mut());
|
||
for e in self.effects.iter_mut().chain(items).filter(|e| e.id == Self::SVG_FILTER) {
|
||
let at =
|
||
|i: usize| e.params.get("origin").and_then(|o| o.get(i)).and_then(serde_json::Value::as_f64).filter(|v| v.is_finite()).unwrap_or(0.0);
|
||
let p = a * kurbo::Point::new(at(0), at(1));
|
||
if p.x.is_finite() && p.y.is_finite() && e.params.is_object() {
|
||
e.params["origin"] = serde_json::json!([p.x, p.y]);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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)
|
||
}
|
||
/// Does a fill or stroke carry a gradient that is drawn at a place in document space: a placed
|
||
/// one, or a freeform gradient with points of its own whose vector is fitted to the bounds?
|
||
pub fn has_positioned_gradient(&self) -> bool {
|
||
self.gradients().any(|g| g.is_placed() || g.has_freeform_points())
|
||
}
|
||
/// 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)
|
||
}
|
||
/// Move items `from` (ascending paint-order indices, no repeats) as one block, in their order,
|
||
/// so the lowest lands at `to` (clamped); returns it. The contents stay between the same unmoved
|
||
/// items; a block landing right at them keeps its lowest item's side. None (and nothing changed)
|
||
/// when an index is missing.
|
||
pub fn move_items(&mut self, from: &[usize], to: usize) -> Option<usize> {
|
||
let (&first, _) = from.split_first()?;
|
||
if from.iter().any(|i| *i >= self.items.len()) {
|
||
return None;
|
||
}
|
||
let below = first < self.contents_at();
|
||
// From the top down, so the lower indices stay valid; then back in order.
|
||
let mut block: Vec<AppearanceItem> = from.iter().rev().filter_map(|&i| self.remove_item(i)).collect();
|
||
block.reverse();
|
||
let others_below = self.contents_at();
|
||
let to = to.min(self.items.len());
|
||
let m = block.len();
|
||
self.items.splice(to..to, block);
|
||
let now_below = if below { to <= others_below } else { to < others_below };
|
||
self.set_contents_at(others_below + if now_below { m } else { 0 });
|
||
Some(to)
|
||
}
|
||
}
|
||
|
||
/// The most fills, strokes or effects a stack may have to be matched against another's
|
||
/// ([`align`]); a larger stack shares nothing with the others.
|
||
const MAX_ALIGNED: usize = 1024;
|
||
|
||
/// Where each entry of `first` is in `other`: the entries equal in both (`same`), matched in their
|
||
/// stacking order (a longest common subsequence, so the matches never cross), by index into
|
||
/// `other`; `None` where `other` has no equal entry left for it.
|
||
pub fn align<T>(first: &[T], other: &[T], same: impl Fn(&T, &T) -> bool) -> Vec<Option<usize>> {
|
||
let (n, m) = (first.len(), other.len());
|
||
let mut out = vec![None; n];
|
||
if n == 0 || m == 0 || n > MAX_ALIGNED || m > MAX_ALIGNED {
|
||
return out;
|
||
}
|
||
// `len[i][j]`: the longest match of `first[i..]` and `other[j..]`.
|
||
let w = m + 1;
|
||
let mut len = vec![0u16; (n + 1) * w];
|
||
for i in (0..n).rev() {
|
||
for j in (0..m).rev() {
|
||
let here = match (first.get(i), other.get(j)) {
|
||
(Some(a), Some(b)) if same(a, b) => len.get((i + 1) * w + j + 1).copied().unwrap_or(0).saturating_add(1),
|
||
_ => len.get((i + 1) * w + j).copied().unwrap_or(0).max(len.get(i * w + j + 1).copied().unwrap_or(0)),
|
||
};
|
||
if let Some(c) = len.get_mut(i * w + j) {
|
||
*c = here;
|
||
}
|
||
}
|
||
}
|
||
let (mut i, mut j) = (0, 0);
|
||
while i < n && j < m {
|
||
let at = |a: usize, b: usize| len.get(a * w + b).copied().unwrap_or(0);
|
||
match (first.get(i), other.get(j)) {
|
||
(Some(a), Some(b)) if same(a, b) && at(i, j) == at(i + 1, j + 1).saturating_add(1) => {
|
||
if let Some(o) = out.get_mut(i) {
|
||
*o = Some(j);
|
||
}
|
||
i += 1;
|
||
j += 1;
|
||
}
|
||
_ if at(i + 1, j) >= at(i, j + 1) => i += 1,
|
||
_ => j += 1,
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// What several objects' appearances share, as the Appearance panel lists a mixed selection: the
|
||
/// fills and strokes, and the object-level effects, that every one of them has an equal copy of, in
|
||
/// stacking order ([`align`]), each with where it is in every object's own stack.
|
||
#[derive(Clone, Debug, Default, PartialEq)]
|
||
pub struct SharedStack {
|
||
/// Each shared fill or stroke, in the first object's paint order: its index in each object's
|
||
/// `items` (the first object's first).
|
||
pub items: Vec<Vec<usize>>,
|
||
/// Each shared object-level effect: its index in each object's `effects`.
|
||
pub effects: Vec<Vec<usize>>,
|
||
}
|
||
|
||
impl SharedStack {
|
||
/// The rows `appearances` share (see [`SharedStack`]). One appearance shares all of itself.
|
||
pub fn of(appearances: &[&Appearance]) -> Self {
|
||
let Some((first, rest)) = appearances.split_first() else { return Self::default() };
|
||
fn shared<T>(first: &[T], rest: Vec<&[T]>, same: impl Fn(&T, &T) -> bool + Copy) -> Vec<Vec<usize>> {
|
||
let maps: Vec<Vec<Option<usize>>> = rest.iter().map(|o| align(first, o, same)).collect();
|
||
(0..first.len())
|
||
.filter_map(|i| {
|
||
let mut at = vec![i];
|
||
for m in &maps {
|
||
at.push((*m.get(i)?)?);
|
||
}
|
||
Some(at)
|
||
})
|
||
.collect()
|
||
}
|
||
Self {
|
||
items: shared(&first.items, rest.iter().map(|a| a.items.as_slice()).collect(), |a, b| a == b),
|
||
effects: shared(&first.effects, rest.iter().map(|a| a.effects.as_slice()).collect(), |a, b| a == b),
|
||
}
|
||
}
|
||
|
||
/// Where the first object's fill or stroke `first` is in object `k`'s stack, when it is shared.
|
||
pub fn item_in(&self, first: usize, k: usize) -> Option<usize> {
|
||
self.items.iter().find(|at| at.first() == Some(&first))?.get(k).copied()
|
||
}
|
||
|
||
/// Where the first object's object-level effect `first` is in object `k`'s effects, when shared.
|
||
pub fn effect_in(&self, first: usize, k: usize) -> Option<usize> {
|
||
self.effects.iter().find(|at| at.first() == Some(&first))?.get(k).copied()
|
||
}
|
||
|
||
/// Is the first object's fill or stroke `i` shared?
|
||
pub fn has_item(&self, i: usize) -> bool {
|
||
self.items.iter().any(|at| at.first() == Some(&i))
|
||
}
|
||
|
||
/// Is the first object's object-level effect `k` shared?
|
||
pub fn has_effect(&self, k: usize) -> bool {
|
||
self.effects.iter().any(|at| at.first() == Some(&k))
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests;
|