mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
#1172: share an object's appearance with its undo history until it changes
Every edit of an object copied its whole appearance (its fills, strokes and effects, about 540 bytes for a basic object) into the new node, though a move leaves it as it is. Over 45 whole-selection moves of 10,000 paths, undo history grew by 589 MB; now 349 MB (41 % less), and a nudge or a drag of the selection takes 12 ms instead of 15. - Node::appearance is a vectorcraft_doc::Shared<Appearance>: copy-on-write. A clone shares it, and the first change through DerefMut copies it (Arc::make_mut). Reads, writes, equality (also against a plain Appearance) and the saved form are those of the Appearance itself, so call sites read and change it as before. Sites that build or assign a whole appearance add .into(), and the few that need a plain copy take (*n.appearance).clone(). - Node::transform changes the appearance only when it has gradients to transform, and pattern paints are transformed only when there are some: a move no longer copies an appearance it doesn't change. The workspace suite (5,301 tests) and clippy pass with the change; the doc and engine suites pass again after the transform guard. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
This commit is contained in:
1 parent
7652439b40
commit
0724aafbb6
42 files changed
+159
-71
No files matched your search
@@ -26,7 +26,7 @@ fn group(children: Vec<Node>) -> Node {
|
||||
fn compound(paths: Vec<PathData>, c: Color) -> Node {
|
||||
let children = paths.into_iter().map(|p| Arc::new(Node::path(NodeId(0), p, Appearance::default()))).collect();
|
||||
let mut n = Node::new(NodeId(0), NodeKind::Compound { children, rule: FillRule::EvenOdd });
|
||||
n.appearance = fill(c);
|
||||
n.appearance = fill(c).into();
|
||||
n
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ pub(crate) fn stitch_tile() -> Node {
|
||||
let dash = path(shapes::rounded_rectangle(Rect::new(1.5, -0.75, 8.5, 0.75), 0.75), Color::BLACK);
|
||||
// Invisible spacer that fixes the tile width.
|
||||
let mut spacer = path(shapes::rectangle(Rect::new(0.0, -0.75, 10.0, 0.75)), Color::BLACK);
|
||||
spacer.appearance = Appearance::default();
|
||||
spacer.appearance = Appearance::default().into();
|
||||
group(vec![spacer, dash])
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ fn art_brush_arc_length_mapping_is_linear() {
|
||||
Node::path(NodeId(0), shapes::rectangle(Rect::new(5.0, -1.0, 6.0, 1.0)), Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0));
|
||||
let frame = rect_art(10.0, 2.0);
|
||||
let mut spacer = frame.clone();
|
||||
spacer.appearance = Appearance::default();
|
||||
spacer.appearance = Appearance::default().into();
|
||||
let art = Node::group(NodeId(0), vec![std::sync::Arc::new(spacer), std::sync::Arc::new(marker)]);
|
||||
let out = stroke_pieces(&art_brush(art, ArtScale::Stretch), &line(0.0, 0.0, 300.0, 0.0), &stroke(1.0));
|
||||
let g = &out[0];
|
||||
|
||||
@@ -479,7 +479,7 @@ impl<'d, 'a> Build<'d, 'a> {
|
||||
.into_iter()
|
||||
.map(|sp| {
|
||||
let mut c = Node::new(self.doc.alloc_id(), kind(PathData::single(sp)));
|
||||
c.appearance = appearance.clone();
|
||||
c.appearance = appearance.clone().into();
|
||||
Arc::new(c)
|
||||
})
|
||||
.collect();
|
||||
@@ -487,7 +487,7 @@ impl<'d, 'a> Build<'d, 'a> {
|
||||
} else {
|
||||
Node::new(self.doc.alloc_id(), kind(path))
|
||||
};
|
||||
n.appearance = appearance;
|
||||
n.appearance = appearance.into();
|
||||
Some(n)
|
||||
}
|
||||
|
||||
|
||||
@@ -942,7 +942,7 @@ impl Appearance {
|
||||
})
|
||||
}
|
||||
/// Gradient paints of the fills and strokes.
|
||||
fn gradients(&self) -> impl Iterator<Item = &vectorcraft_color::GradientPaint> {
|
||||
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),
|
||||
|
||||
@@ -619,7 +619,7 @@ impl Lerp {
|
||||
}
|
||||
};
|
||||
n.id = NodeId(0);
|
||||
n.appearance = lerp_appearance(&a.appearance, &b.appearance, t);
|
||||
n.appearance = lerp_appearance(&a.appearance, &b.appearance, t).into();
|
||||
n.opacity = lerp_f32(a.opacity, b.opacity, t as f32);
|
||||
n.blend = base.blend;
|
||||
n.visible = true;
|
||||
|
||||
@@ -463,7 +463,7 @@ mod tests {
|
||||
d.insert(Some(l), 0, Node::path(a, shapes::rectangle(Rect::new(0.0, 0.0, 50.0, 50.0)), Appearance::default_art())).unwrap();
|
||||
let inner = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(100.0, 0.0, 150.0, 50.0)), Appearance::default());
|
||||
let mut compound = Node::new(c, NodeKind::Compound { children: vec![Arc::new(inner)], rule: vectorcraft_geom::FillRule::NonZero });
|
||||
compound.appearance = Appearance::default_art();
|
||||
compound.appearance = Appearance::default_art().into();
|
||||
d.insert(Some(l), 9, compound).unwrap();
|
||||
let path_only = HitOptions { path_only: true, ..Default::default() };
|
||||
for (id, inside, edge) in [(a, Point::new(25.0, 25.0), Point::new(0.5, 25.0)), (c, Point::new(125.0, 25.0), Point::new(100.5, 25.0))] {
|
||||
|
||||
@@ -16,7 +16,9 @@ pub mod hit;
|
||||
mod id_index;
|
||||
pub mod inks;
|
||||
mod inline;
|
||||
pub mod shared;
|
||||
pub use inline::{SYMBOL_HALF, SYMBOL_SIZES};
|
||||
pub use shared::Shared;
|
||||
pub mod links;
|
||||
pub mod live;
|
||||
pub mod marks;
|
||||
|
||||
@@ -1524,7 +1524,7 @@ fn pattern_art(n: &Node, tiles: PatternTiles) -> Option<Node> {
|
||||
};
|
||||
let plain = |items: Vec<AppearanceItem>| {
|
||||
Arc::new(Node {
|
||||
appearance: Appearance { items, ..Default::default() },
|
||||
appearance: Appearance { items, ..Default::default() }.into(),
|
||||
opacity: 1.0,
|
||||
blend: Default::default(),
|
||||
isolate: false,
|
||||
@@ -1560,7 +1560,7 @@ fn pattern_art(n: &Node, tiles: PatternTiles) -> Option<Node> {
|
||||
}
|
||||
Some(Node {
|
||||
kind: NodeKind::Group { children: pieces, clip: false },
|
||||
appearance: Appearance { effects: n.appearance.effects.clone(), ..Default::default() },
|
||||
appearance: Appearance { effects: n.appearance.effects.clone(), ..Default::default() }.into(),
|
||||
..n.clone()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -514,7 +514,7 @@ pub struct Node {
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
pub knockout_shape: bool,
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
pub appearance: Appearance,
|
||||
pub appearance: crate::Shared<Appearance>,
|
||||
/// Opacity mask (Transparency panel). Its art lives here, outside the layer tree.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mask: Option<Box<OpacityMask>>,
|
||||
@@ -594,7 +594,7 @@ impl Node {
|
||||
isolate: false,
|
||||
knockout: Knockout::Neutral,
|
||||
knockout_shape: false,
|
||||
appearance: Appearance::default(),
|
||||
appearance: Appearance::default().into(),
|
||||
mask: None,
|
||||
trace: None,
|
||||
wrap: None,
|
||||
@@ -611,7 +611,7 @@ impl Node {
|
||||
}
|
||||
pub fn path(id: NodeId, path: PathData, appearance: Appearance) -> Self {
|
||||
let mut n = Self::new(id, NodeKind::Path { path, rule: FillRule::NonZero, live: None, clipping: false, guide: false });
|
||||
n.appearance = appearance;
|
||||
n.appearance = appearance.into();
|
||||
n
|
||||
}
|
||||
pub fn group(id: NodeId, children: Vec<Arc<Node>>) -> Self {
|
||||
@@ -854,7 +854,11 @@ impl Node {
|
||||
self.appearance.scale_effects(k, f);
|
||||
}
|
||||
}
|
||||
self.appearance.transform_gradients(a);
|
||||
// Only through `DerefMut` when there is something to change: the appearance is shared
|
||||
// copy-on-write (`Shared`), and a move must not copy what it leaves as it is.
|
||||
if self.appearance.gradients().next().is_some() {
|
||||
self.appearance.transform_gradients(a);
|
||||
}
|
||||
if sc.patterns {
|
||||
crate::pattern::transform_pattern_paints(self, a);
|
||||
}
|
||||
|
||||
@@ -343,6 +343,14 @@ pub fn uses_pattern(n: &Node, name: Option<&str>) -> bool {
|
||||
|
||||
/// Apply `m` to the pattern placement of every pattern paint of `n` (Transform Patterns).
|
||||
pub fn transform_pattern_paints(n: &mut Node, m: Affine) {
|
||||
// Without pattern paints there is nothing to change: don't copy the shared appearance.
|
||||
let patterned = |it: &AppearanceItem| {
|
||||
matches!(it, AppearanceItem::Fill(f) if matches!(f.paint, Paint::Pattern { .. }))
|
||||
|| matches!(it, AppearanceItem::Stroke(s) if matches!(s.paint, Paint::Pattern { .. }))
|
||||
};
|
||||
if !n.appearance.items.iter().any(patterned) {
|
||||
return;
|
||||
}
|
||||
for it in n.appearance.items.iter_mut() {
|
||||
let p = match it {
|
||||
AppearanceItem::Fill(f) => &mut f.paint,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
//! [`Shared`]: a value shared between copies of a node until one of them changes it.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
/// A copy-on-write value: cloning shares it, and the first change through `DerefMut` makes the
|
||||
/// changed copy its own (`Arc::make_mut`). A node moved or recoloured keeps sharing what it didn't
|
||||
/// change with the version the undo history holds. Reads, writes, equality and the saved form are
|
||||
/// exactly those of the value itself.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Shared<T>(Arc<T>);
|
||||
|
||||
impl<T> Shared<T> {
|
||||
pub fn new(v: T) -> Self {
|
||||
Shared(Arc::new(v))
|
||||
}
|
||||
/// Whether `a` and `b` share one value (an unchanged copy).
|
||||
pub fn ptr_eq(a: &Self, b: &Self) -> bool {
|
||||
Arc::ptr_eq(&a.0, &b.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for Shared<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> std::ops::DerefMut for Shared<T> {
|
||||
fn deref_mut(&mut self) -> &mut T {
|
||||
Arc::make_mut(&mut self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Shared<T> {
|
||||
fn from(v: T) -> Self {
|
||||
Shared::new(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq for Shared<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.0, &other.0) || *self.0 == *other.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A shared value equals the plain value it holds (`node.appearance == style.appearance`).
|
||||
impl<T: PartialEq> PartialEq<T> for Shared<T> {
|
||||
fn eq(&self, other: &T) -> bool {
|
||||
*self.0 == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::fmt::Debug> std::fmt::Debug for Shared<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> Serialize for Shared<T> {
|
||||
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
||||
self.0.serialize(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: Deserialize<'de>> Deserialize<'de> for Shared<T> {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
T::deserialize(d).map(Shared::new)
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,7 @@ fn piece(d: &mut Document, n: &Node, path: PathData, rule: FillRule, item: Appea
|
||||
Node {
|
||||
id,
|
||||
name: None,
|
||||
appearance: Appearance { items: vec![item], ..Default::default() },
|
||||
appearance: Appearance { items: vec![item], ..Default::default() }.into(),
|
||||
opacity: 1.0,
|
||||
blend: Default::default(),
|
||||
isolate: false,
|
||||
|
||||
@@ -141,7 +141,7 @@ pub(crate) fn as_art(n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
|
||||
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
|
||||
*clipping = true;
|
||||
}
|
||||
let image = Node { appearance: Appearance::default(), ..Node::new(n.id, n.kind.clone()) };
|
||||
let image = Node { appearance: Appearance::default().into(), ..Node::new(n.id, n.kind.clone()) };
|
||||
Node::new(n.id, NodeKind::Group { children: vec![Arc::new(clip), Arc::new(image)], clip: true })
|
||||
}
|
||||
_ => outline_art(n, symbol_art)?,
|
||||
|
||||
@@ -200,7 +200,7 @@ fn art(n: &Node, expand: bool) -> Option<Node> {
|
||||
}
|
||||
let mut art = n.clone();
|
||||
art.kind = NodeKind::Group { children, clip: false };
|
||||
art.appearance = Appearance { effects: remaining, ..Default::default() };
|
||||
art.appearance = Appearance { effects: remaining, ..Default::default() }.into();
|
||||
Some(art)
|
||||
}
|
||||
|
||||
|
||||
@@ -443,7 +443,7 @@ fn clear_appearance(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// Clear Appearance on `n`: no fill, no stroke, and the object's Opacity row back to Default. A
|
||||
/// group keeps no fill or stroke rows of its own.
|
||||
fn clear(n: &mut Node) {
|
||||
n.appearance = if is_group(n) { Appearance::default() } else { Appearance::basic(Paint::None, Paint::None, 1.0) };
|
||||
n.appearance = if is_group(n) { Appearance::default().into() } else { Appearance::basic(Paint::None, Paint::None, 1.0).into() };
|
||||
n.opacity = 1.0;
|
||||
n.blend = BlendMode::Normal;
|
||||
}
|
||||
@@ -496,7 +496,7 @@ fn reduce_basic(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// keeps the rows it had.
|
||||
let fill =
|
||||
(fill.is_some() || !is_group(n)).then(|| AppearanceItem::Fill(FillLayer::new(fill.map_or(Paint::None, |f| f.paint().clone()))));
|
||||
n.appearance = Appearance { items: fill.into_iter().collect(), ..Default::default() };
|
||||
n.appearance = Appearance { items: fill.into_iter().collect(), ..Default::default() }.into();
|
||||
if let Some(AppearanceItem::Stroke(mut st)) = stroke {
|
||||
st.effects.clear();
|
||||
st.opacity = 1.0;
|
||||
@@ -758,7 +758,7 @@ impl Picked {
|
||||
let st = &t.runs[0].style;
|
||||
(st.basic_appearance(), Some(t.local_bounds()), Some((st.clone(), t.para.clone())))
|
||||
}
|
||||
_ => (n.appearance.clone(), n.geometric_bounds(), None),
|
||||
_ => ((*n.appearance).clone(), n.geometric_bounds(), None),
|
||||
};
|
||||
Self { appearance, bounds, opacity: n.opacity, blend: n.blend, text }
|
||||
}
|
||||
@@ -831,7 +831,7 @@ impl Picked {
|
||||
}
|
||||
let placed = self.placed(n.geometric_bounds());
|
||||
if ap.fill == FillAttrs::ALL && ap.stroke == StrokeAttrs::ALL {
|
||||
n.appearance = placed;
|
||||
n.appearance = placed.into();
|
||||
} else {
|
||||
focal_attrs(&mut n.appearance, &placed, ap);
|
||||
}
|
||||
|
||||
@@ -478,7 +478,7 @@ fn lp_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.filter_map(|e| {
|
||||
let pd = e.path_data()?;
|
||||
let sp = pd.subpaths.first().filter(|sp| sp.segment_count() > 0)?;
|
||||
Some((kurbo::ParamCurve::eval(&sp.segment(0), 0.5), e.appearance.clone()))
|
||||
Some((kurbo::ParamCurve::eval(&sp.segment(0), 0.5), (*e.appearance).clone()))
|
||||
})
|
||||
.collect();
|
||||
let mut sources = source_nodes(&old);
|
||||
@@ -505,7 +505,7 @@ fn lp_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
&& let Some(ch) = g.children_mut()
|
||||
&& let Some(e) = ch.iter_mut().find(|c| c.id == eid)
|
||||
{
|
||||
Arc::make_mut(e).appearance = app;
|
||||
Arc::make_mut(e).appearance = app.into();
|
||||
}
|
||||
}
|
||||
for r in roots.iter().filter(|r| **r != target) {
|
||||
@@ -777,8 +777,9 @@ fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {
|
||||
..StrokeLayer::new(paint, width * unit)
|
||||
})],
|
||||
..Default::default()
|
||||
},
|
||||
None => fill_only(paint),
|
||||
}
|
||||
.into(),
|
||||
None => fill_only(paint).into(),
|
||||
};
|
||||
children.push(Arc::new(n));
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ pub(crate) fn add_look(s: &mut Session, label: &str, kind: NodeKind, look: NewAr
|
||||
let (par, idx, _) = d.position(target).ok_or(crate::EngineError::NoNode(target))?;
|
||||
let mut mask = d.node(target).cloned().ok_or(crate::EngineError::NoNode(target))?;
|
||||
mask.id = d.alloc_id();
|
||||
mask.appearance = Appearance::basic(vectorcraft_color::Paint::None, vectorcraft_color::Paint::None, 0.0);
|
||||
mask.appearance = Appearance::basic(vectorcraft_color::Paint::None, vectorcraft_color::Paint::None, 0.0).into();
|
||||
if let NodeKind::Path { clipping, .. } = &mut mask.kind {
|
||||
*clipping = true;
|
||||
}
|
||||
|
||||
@@ -236,13 +236,13 @@ impl Builder {
|
||||
let appearance = self.appearance(fills, strokes, to_doc);
|
||||
if children.is_empty() {
|
||||
let mut p = self.base(n, NodeKind::Path { path: data, rule, live: None, clipping: false, guide: false });
|
||||
p.appearance = appearance;
|
||||
p.appearance = appearance.into();
|
||||
p
|
||||
} else {
|
||||
// A shape clips its children: VectorCraft's clipping path keeps its paint, its
|
||||
// fill behind the clipped art and its stroke over it, as Affinity draws it.
|
||||
let mut clip = Node::new(self.doc.alloc_id(), NodeKind::Path { path: data, rule, live: None, clipping: true, guide: false });
|
||||
clip.appearance = appearance;
|
||||
clip.appearance = appearance.into();
|
||||
let mut kids = vec![Arc::new(clip)];
|
||||
kids.extend(children);
|
||||
self.base(n, NodeKind::Group { children: kids, clip: true })
|
||||
@@ -259,7 +259,7 @@ impl Builder {
|
||||
guide: false,
|
||||
},
|
||||
);
|
||||
clip.appearance = self.appearance(background, strokes, to_doc);
|
||||
clip.appearance = self.appearance(background, strokes, to_doc).into();
|
||||
let mut kids = vec![Arc::new(clip)];
|
||||
kids.extend(children);
|
||||
let mut g = self.base(n, NodeKind::Group { children: kids, clip: true });
|
||||
|
||||
@@ -655,7 +655,7 @@ impl FlattenReport {
|
||||
art.iter_mut().for_each(opaque);
|
||||
for path in areas {
|
||||
let mut n = shape_node(&mut tmp, path.clone(), None);
|
||||
n.appearance = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 0.0)), Paint::None, 0.0);
|
||||
n.appearance = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 0.0)), Paint::None, 0.0).into();
|
||||
art.push(n);
|
||||
}
|
||||
Some(isolated_doc(&tmp, art))
|
||||
@@ -1054,7 +1054,8 @@ fn apply(d: &mut Document, sel: &mut vectorcraft_doc::Selection, plan: Plan, o:
|
||||
n.appearance = Appearance {
|
||||
items: vec![AppearanceItem::Fill(FillLayer { overprint: fill.overprint, ..FillLayer::new(fill.paint) })],
|
||||
..Appearance::default()
|
||||
};
|
||||
}
|
||||
.into();
|
||||
n.opacity = fill.alpha.min(1.0);
|
||||
children.push(Arc::new(n));
|
||||
vector += 1;
|
||||
|
||||
@@ -575,7 +575,7 @@ fn crop_image(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut bare = node.clone();
|
||||
bare.opacity = 1.0;
|
||||
bare.blend = Default::default();
|
||||
bare.appearance = Appearance::default();
|
||||
bare.appearance = Appearance::default().into();
|
||||
bare.visible = true;
|
||||
let tmp = isolated_doc(&st.doc, vec![bare]);
|
||||
let region = Rect::new(r.x0, r.y0, r.x0 + (r.width() * scale).round().max(1.0) / scale, r.y0 + (r.height() * scale).round().max(1.0) / scale);
|
||||
|
||||
@@ -94,7 +94,7 @@ impl NewArt {
|
||||
/// Give new object `n` (in `d`) this look: its placed gradients land relative to its bounds, and
|
||||
/// it links to the style (type keeps its own transparency).
|
||||
pub(crate) fn apply(&self, d: &mut vectorcraft_doc::Document, n: &mut Node) {
|
||||
n.appearance = self.appearance.clone();
|
||||
n.appearance = self.appearance.clone().into();
|
||||
if self.unit_box
|
||||
&& n.appearance.has_placed_gradient()
|
||||
&& let Some(b) = n.geometric_bounds()
|
||||
|
||||
@@ -661,7 +661,7 @@ fn compound_make(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let back = paths.first().map_or(0.0, |p| direction(p));
|
||||
for (i, mut p) in paths.into_iter().enumerate() {
|
||||
let pn = Arc::make_mut(&mut p);
|
||||
pn.appearance = Appearance::default();
|
||||
pn.appearance = Appearance::default().into();
|
||||
let flip = i > 0 && back != 0.0 && direction(pn) == back;
|
||||
if let NodeKind::Path { path, live, .. } = &mut pn.kind {
|
||||
*live = None;
|
||||
@@ -742,7 +742,7 @@ pub(super) fn as_clipping_path(c: &mut Node) -> Result<()> {
|
||||
if !matches!(c.kind, NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } | NodeKind::Text(_)) {
|
||||
return Err(EngineError::Other("the top object must be a path, compound path or text object to use as a clipping mask".into()));
|
||||
}
|
||||
c.appearance = Appearance::basic(Paint::None, Paint::None, 0.0);
|
||||
c.appearance = Appearance::basic(Paint::None, Paint::None, 0.0).into();
|
||||
match &mut c.kind {
|
||||
NodeKind::Path { clipping, .. } => *clipping = true,
|
||||
NodeKind::Text(t) => {
|
||||
|
||||
@@ -524,7 +524,7 @@ fn default_paint(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
r.style.stroke = Paint::None;
|
||||
}
|
||||
}
|
||||
_ => n.appearance = Appearance::default_art(),
|
||||
_ => n.appearance = Appearance::default_art().into(),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -203,10 +203,10 @@ struct Style {
|
||||
|
||||
impl Style {
|
||||
fn of(n: &Node) -> Self {
|
||||
Style { appearance: n.appearance.clone(), opacity: n.opacity, blend: n.blend }
|
||||
Style { appearance: (*n.appearance).clone(), opacity: n.opacity, blend: n.blend }
|
||||
}
|
||||
fn apply(&self, n: &mut Node) {
|
||||
n.appearance = self.appearance.clone();
|
||||
n.appearance = self.appearance.clone().into();
|
||||
n.opacity = self.opacity;
|
||||
n.blend = self.blend;
|
||||
}
|
||||
@@ -377,7 +377,7 @@ fn run_pf(s: &mut Session, op: PathfinderOp) -> Result<Value> {
|
||||
// Outline: open edges stroked with the face's former fill.
|
||||
let fill = st.appearance.fill_paint();
|
||||
let paint = if fill.is_none() { st.appearance.stroke_paint() } else { fill };
|
||||
n.appearance = Appearance { items: vec![AppearanceItem::Stroke(StrokeLayer::new(paint, 1.0))], ..Default::default() };
|
||||
n.appearance = Appearance { items: vec![AppearanceItem::Stroke(StrokeLayer::new(paint, 1.0))], ..Default::default() }.into();
|
||||
}
|
||||
children.push(Arc::new(n));
|
||||
}
|
||||
@@ -519,7 +519,7 @@ pub(crate) fn outlined_stroke(
|
||||
overprint: st.overprint,
|
||||
..vectorcraft_doc::appearance::FillLayer::new(st.paint.clone())
|
||||
};
|
||||
n.appearance = Appearance { items: vec![AppearanceItem::Fill(fill)], ..Default::default() };
|
||||
n.appearance = Appearance { items: vec![AppearanceItem::Fill(fill)], ..Default::default() }.into();
|
||||
Some(n)
|
||||
}
|
||||
|
||||
@@ -554,7 +554,7 @@ pub(crate) fn outline_strokes(d: &mut Document, brushes: &[vectorcraft_brush::Br
|
||||
/// A plain copy of path `l` (new ids, no transparency, name or mask) painted with `items`.
|
||||
pub(crate) fn styled_copy(d: &mut Document, l: &Node, items: Vec<AppearanceItem>) -> Node {
|
||||
let mut f = d.reid(l);
|
||||
f.appearance = Appearance { items, ..Default::default() };
|
||||
f.appearance = Appearance { items, ..Default::default() }.into();
|
||||
f.opacity = 1.0;
|
||||
f.blend = BlendMode::Normal;
|
||||
f.name = None;
|
||||
|
||||
@@ -136,7 +136,7 @@ fn settings(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// `art` in a clip group clipped by `path` filled by `rule` (a clipping path that paints nothing).
|
||||
pub(crate) fn clip_group(d: &mut Document, path: PathData, rule: FillRule, art: Node) -> Node {
|
||||
let mut clip = super::pathops::shape_node(d, path, None);
|
||||
clip.appearance = Appearance::basic(Paint::None, Paint::None, 0.0);
|
||||
clip.appearance = Appearance::basic(Paint::None, Paint::None, 0.0).into();
|
||||
match &mut clip.kind {
|
||||
NodeKind::Path { clipping, rule: r, .. } => (*clipping, *r) = (true, rule),
|
||||
NodeKind::Compound { rule: r, .. } => *r = rule,
|
||||
@@ -363,7 +363,7 @@ fn split_items(out: &mut Document, n: &Node) -> Option<Node> {
|
||||
}
|
||||
let mut stroke_piece = |d: &mut Document, path: &PathData, rule: FillRule, st: &StrokeLayer| {
|
||||
let mut p = super::pathops::shape_node(d, path.clone(), None);
|
||||
p.appearance = Appearance { items: vec![AppearanceItem::Stroke(st.clone())], ..Default::default() };
|
||||
p.appearance = Appearance { items: vec![AppearanceItem::Stroke(st.clone())], ..Default::default() }.into();
|
||||
if let NodeKind::Path { rule: r, .. } | NodeKind::Compound { rule: r, .. } = &mut p.kind {
|
||||
*r = rule;
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ fn generated(d: &mut Document, src: &[Arc<Node>], spec: &ShaperSpec, map: &Build
|
||||
let edit = edits.get(&i);
|
||||
let Some(source) = edit.and_then(|e| e.paint_source).or_else(|| fill_source(src, region)).and_then(|i| src.get(i)) else { continue };
|
||||
let path = po::unite_all(&indices.iter().filter_map(|i| map.regions.get(*i).map(|r| (&r.0.path, FillRule::NonZero))).collect::<Vec<_>>());
|
||||
let style = edit.and_then(|e| e.paint.clone()).unwrap_or_else(|| source.appearance.clone());
|
||||
let style = edit.and_then(|e| e.paint.clone()).unwrap_or_else(|| (*source.appearance).clone());
|
||||
let mut n = Node::path(d.alloc_id(), path, fill_style(style));
|
||||
n.opacity = source.opacity;
|
||||
n.blend = source.blend;
|
||||
@@ -358,7 +358,7 @@ fn generated(d: &mut Document, src: &[Arc<Node>], spec: &ShaperSpec, map: &Build
|
||||
continue;
|
||||
}
|
||||
let Some(source) = src.get(piece.key as usize) else { continue };
|
||||
let style = edge_edits.get(&i).and_then(|e| e.paint.clone()).unwrap_or_else(|| source.appearance.clone());
|
||||
let style = edge_edits.get(&i).and_then(|e| e.paint.clone()).unwrap_or_else(|| (*source.appearance).clone());
|
||||
if style.stroke_paint().is_none() {
|
||||
continue;
|
||||
}
|
||||
@@ -448,7 +448,7 @@ pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> {
|
||||
for i in related {
|
||||
let Some(selector) = face_selector(&map, &src, i) else { continue };
|
||||
if let Some(e) = spec.faces.iter_mut().rev().find(|e| e.selector == selector) {
|
||||
e.paint = Some(n.appearance.clone());
|
||||
e.paint = Some((*n.appearance).clone());
|
||||
} else {
|
||||
spec.faces.push(FaceEdit {
|
||||
selector,
|
||||
@@ -456,7 +456,7 @@ pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> {
|
||||
erase_edges: false,
|
||||
paint_source: None,
|
||||
merge,
|
||||
paint: Some(n.appearance.clone()),
|
||||
paint: Some((*n.appearance).clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -472,7 +472,7 @@ pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> {
|
||||
&& piece_point(&piece.path).is_some_and(|point| n.path_data().and_then(|p| p.nearest(point)).is_some_and(|p| p.4 < 1e-5));
|
||||
if contained && let Some(mut e) = edge_selector(&map, &src, piece) {
|
||||
e.erase = false;
|
||||
e.paint = Some(n.appearance.clone());
|
||||
e.paint = Some((*n.appearance).clone());
|
||||
spec.edges.push(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ fn unitized(mut ap: Appearance, from: Option<Rect>) -> Appearance {
|
||||
/// and stroke. Placed gradients come relative to the unit box (from the bounds of the object
|
||||
/// they're on). The pen pressure of strokes stays with their own path.
|
||||
pub(crate) fn captured(n: &Node) -> Appearance {
|
||||
let mut ap = n.appearance.clone().without_pressure();
|
||||
let mut ap = (*n.appearance).clone().without_pressure();
|
||||
if !ap.items.is_empty() {
|
||||
return unitized(ap, n.geometric_bounds());
|
||||
}
|
||||
@@ -261,7 +261,7 @@ fn override_chars(n: &mut Node, ap: &Appearance) {
|
||||
/// Give `n` style `g`: its appearance (a group or layer too: its own, above its contents; type its
|
||||
/// characters' colour too with `chars`, Override Character Color) and transparency.
|
||||
fn style_node(n: &mut Node, g: &GraphicStyle, chars: bool) {
|
||||
n.appearance = g.appearance_on(n).into_owned();
|
||||
n.appearance = g.appearance_on(n).into_owned().into();
|
||||
if chars {
|
||||
override_chars(n, &g.appearance);
|
||||
}
|
||||
|
||||
@@ -561,7 +561,7 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
d.remove(pid)?;
|
||||
let id = d.alloc_id();
|
||||
let mut n = Node::new(id, NodeKind::Text(Box::new(t)));
|
||||
n.appearance = Appearance::default();
|
||||
n.appearance = Appearance::default().into();
|
||||
n.opacity = node.opacity;
|
||||
n.name = node.name.clone();
|
||||
d.insert(par, idx, n)?;
|
||||
|
||||
@@ -248,14 +248,14 @@ fn create_outlines(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
}
|
||||
let st = t.runs.get(g.run).map(|r| r.style.clone()).unwrap_or_else(|| t.first_style());
|
||||
let mut node = shape_node(d, path, None);
|
||||
node.appearance = st.appearance();
|
||||
node.appearance = st.appearance().into();
|
||||
children.push(Arc::new(node));
|
||||
}
|
||||
// Underline and strikethrough bars become paths of their run's paint too (#847).
|
||||
for (run, bar) in vectorcraft_text::decorations(&lay, vectorcraft_text::FontDb::global(), t) {
|
||||
let st = t.runs.get(run).map(|r| r.style.clone()).unwrap_or_else(|| t.first_style());
|
||||
let mut node = shape_node(d, PathData::from_bezpath(&bar).transformed(t.xf), None);
|
||||
node.appearance = st.appearance();
|
||||
node.appearance = st.appearance().into();
|
||||
children.push(Arc::new(node));
|
||||
}
|
||||
let (par, idx, _) = d.position(tid).ok_or(EngineError::NoNode(tid))?;
|
||||
|
||||
@@ -103,7 +103,7 @@ fn effects_and_fills_apply_to_layers_by_id() {
|
||||
// Two undo steps take both off again.
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(node(&s, layer).appearance, Default::default());
|
||||
assert_eq!(node(&s, layer).appearance, vectorcraft_doc::Appearance::default());
|
||||
// Nothing selected and no ids: an error, not a silent no-op.
|
||||
assert!(s.execute("appearance.addFill", &json!({})).is_err());
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ fn sel(s: &mut Session, ids: &[NodeId]) {
|
||||
fn rect(s: &mut Session) -> NodeId {
|
||||
let a = id_of(&s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 200, "height": 100})).unwrap());
|
||||
s.edit("red", |d, _| {
|
||||
d.node_mut(a).unwrap().appearance = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
|
||||
d.node_mut(a).unwrap().appearance = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0).into();
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -126,7 +126,7 @@ fn shift_alt_click_appends_the_appearance() {
|
||||
let mut s = session();
|
||||
let (_, dst) = source_and_target(&mut s);
|
||||
click(&mut s, 325.0, 35.0, Mods { alt: true, shift: true, ..Default::default() });
|
||||
let items = node(&s, dst).appearance.items;
|
||||
let items = node(&s, dst).appearance.items.clone();
|
||||
assert_eq!(items.len(), 4, "the source's fill and stroke on top of the target's");
|
||||
assert_eq!((items[0].paint().color().unwrap().to_hex(), items[2].paint().color().unwrap().to_hex()), ("#ffffff".into(), "#ff0000".into()));
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ fn outline_stroke_of_line() {
|
||||
let r = s.execute("shape.line", &json!({"x1": 0, "y1": 50, "x2": 100, "y2": 50})).unwrap();
|
||||
let l = NodeId(r["id"].as_u64().unwrap());
|
||||
s.edit("t", |d, _| {
|
||||
d.node_mut(l).unwrap().appearance = Appearance::basic(Paint::None, Paint::solid(Color::rgb(1.0, 0.0, 0.0)), 10.0);
|
||||
d.node_mut(l).unwrap().appearance = Appearance::basic(Paint::None, Paint::solid(Color::rgb(1.0, 0.0, 0.0)), 10.0).into();
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
@@ -304,7 +304,7 @@ fn clean_up_removes_junk() {
|
||||
let keep = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
let unpainted = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
|
||||
s.edit("t", |d, _| {
|
||||
d.node_mut(unpainted).unwrap().appearance = Appearance::basic(Paint::None, Paint::None, 1.0);
|
||||
d.node_mut(unpainted).unwrap().appearance = Appearance::basic(Paint::None, Paint::None, 1.0).into();
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -1330,7 +1330,7 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
let kind = NodeKind::Path { path, rule: gs.rule, live: None, clipping: clip, guide };
|
||||
let mut node = self.new_node(kind, &gs);
|
||||
node.appearance = Appearance { items, ..Appearance::default() };
|
||||
node.appearance = Appearance { items, ..Appearance::default() }.into();
|
||||
node.opacity = gs.opacity;
|
||||
node.blend = gs.blend;
|
||||
node.knockout_shape = gs.knockout_shape;
|
||||
|
||||
@@ -1688,7 +1688,7 @@ pub(crate) fn collapse(nodes: &mut [Arc<Node>], shadings: &[NodeId]) {
|
||||
&& shadings.contains(&fill.id)
|
||||
&& let (NodeKind::Path { path, rule, clipping: true, .. }, NodeKind::Path { .. }) = (&clip.kind, &fill.kind)
|
||||
{
|
||||
let mut p = Node::path(fill.id, path.clone(), fill.appearance.clone());
|
||||
let mut p = Node::path(fill.id, path.clone(), (*fill.appearance).clone());
|
||||
if let NodeKind::Path { rule: r, .. } = &mut p.kind {
|
||||
*r = *rule;
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ fn import_invalid_dash_array_strokes_solid() {
|
||||
// And the exporter never writes an invalid array back.
|
||||
let mut d2 = doc(100.0, 100.0);
|
||||
let mut n = rect_node(Rect::new(10.0, 10.0, 90.0, 90.0), Color::rgb(1.0, 0.0, 0.0));
|
||||
n.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 2.0);
|
||||
n.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 2.0).into();
|
||||
if let Some(AppearanceItem::Stroke(st)) = n.appearance.items.last_mut() {
|
||||
st.dash = Some(Dash { pattern: vec![-5.0, 3.0], offset: 0.0, align_corners: false });
|
||||
}
|
||||
|
||||
@@ -947,7 +947,7 @@ mod tests {
|
||||
let l = d.layers[0].id;
|
||||
let id = d.layers[0].children().unwrap()[0].id;
|
||||
let mut n = (*d.node(id).unwrap()).clone();
|
||||
n.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 4.0);
|
||||
n.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 4.0).into();
|
||||
if let Some(vectorcraft_doc::AppearanceItem::Stroke(s)) =
|
||||
n.appearance.items.iter_mut().find(|i| matches!(i, vectorcraft_doc::AppearanceItem::Stroke(_)))
|
||||
{
|
||||
|
||||
@@ -860,7 +860,7 @@ impl Importer {
|
||||
text::fold_scale(&mut obj, grow);
|
||||
let mut n = self.named(&name, NodeKind::Text(Box::new(obj)));
|
||||
if let Some(ap) = under {
|
||||
n.appearance = ap;
|
||||
n.appearance = ap.into();
|
||||
}
|
||||
Some(vec![Arc::new(n)])
|
||||
}
|
||||
@@ -1190,7 +1190,7 @@ impl Importer {
|
||||
.map(|sp| {
|
||||
let id = self.doc.alloc_id();
|
||||
let mut c = Node::new(id, NodeKind::Path { path: PathData::single(sp), rule: r, live: None, clipping: false, guide: false });
|
||||
c.appearance = appearance.clone();
|
||||
c.appearance = appearance.clone().into();
|
||||
Arc::new(c)
|
||||
})
|
||||
.collect();
|
||||
@@ -1198,7 +1198,7 @@ impl Importer {
|
||||
} else {
|
||||
self.named(p.id(), NodeKind::Path { path, rule: r, live: None, clipping: false, guide: false })
|
||||
};
|
||||
n.appearance = appearance;
|
||||
n.appearance = appearance.into();
|
||||
Some(n)
|
||||
}
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ fn shared_properties_read_as_svg_export_writes_them() {
|
||||
fn gradients_give_css_gradients() {
|
||||
let mut d = doc();
|
||||
let mut n = rect(&mut d, (0.0, 0.0, 200.0, 100.0), 0.0);
|
||||
n.appearance = Appearance::basic(Paint::Gradient(Box::new(GradientPaint::new(Gradient::default()))), Paint::None, 0.0);
|
||||
n.appearance = Appearance::basic(Paint::Gradient(Box::new(GradientPaint::new(Gradient::default()))), Paint::None, 0.0).into();
|
||||
let id = add(&mut d, n);
|
||||
let r = one(&d, id, &CssOptions::default());
|
||||
assert_eq!(prop(&r, "background-image"), Some("linear-gradient(90deg, #ffffff 0%, #000000 100%)"));
|
||||
@@ -196,13 +196,13 @@ fn gradients_give_css_gradients() {
|
||||
let mut g = GradientPaint::new(Gradient::default());
|
||||
g.geom = Some(GradientGeom { start: Point::new(100.0, 0.0), end: Point::new(100.0, 50.0), aspect: 1.0, focal: None });
|
||||
let mut n = rect(&mut d, (0.0, 0.0, 200.0, 100.0), 0.0);
|
||||
n.appearance = Appearance::basic(Paint::Gradient(Box::new(g)), Paint::None, 0.0);
|
||||
n.appearance = Appearance::basic(Paint::Gradient(Box::new(g)), Paint::None, 0.0).into();
|
||||
let id = add(&mut d, n);
|
||||
assert_eq!(prop(&one(&d, id, &CssOptions::default()), "background-image"), Some("linear-gradient(180deg, #ffffff 0%, #000000 50%)"));
|
||||
// Radial: a circle at the box's centre.
|
||||
let radial = GradientPaint::new(Gradient { kind: GradientKind::Radial, ..Gradient::default() });
|
||||
let mut n = rect(&mut d, (0.0, 0.0, 100.0, 100.0), 0.0);
|
||||
n.appearance = Appearance::basic(Paint::Gradient(Box::new(radial)), Paint::None, 0.0);
|
||||
n.appearance = Appearance::basic(Paint::Gradient(Box::new(radial)), Paint::None, 0.0).into();
|
||||
let id = add(&mut d, n);
|
||||
let bg = one(&d, id, &CssOptions::default()).props.into_iter().find(|(k, _)| *k == "background-image").unwrap().1;
|
||||
assert!(bg.starts_with("radial-gradient(circle ") && bg.contains(" at 50px 50px, #ffffff 0%, #000000 100%)"), "{bg}");
|
||||
|
||||
@@ -144,7 +144,7 @@ fn compound_evenodd_roundtrip() {
|
||||
let i = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(25.0, 25.0, 75.0, 75.0)), Appearance::default());
|
||||
let cid = d.alloc_id();
|
||||
let mut c = Node::new(cid, NodeKind::Compound { children: vec![Arc::new(o), Arc::new(i)], rule: FillRule::EvenOdd });
|
||||
c.appearance = Appearance::basic(solid("#123456"), Paint::None, 1.0);
|
||||
c.appearance = Appearance::basic(solid("#123456"), Paint::None, 1.0).into();
|
||||
let d = doc_with(vec![c]);
|
||||
let s = export(&d, &ExportOptions::default());
|
||||
assert_eq!(s.matches("<path").count(), 1, "{s}");
|
||||
|
||||
@@ -79,7 +79,7 @@ fn instances_the_def_cant_stand_for_get_their_own_art() {
|
||||
let mut d = symbol_doc(6.0, &[Affine::translate((40.0, 60.0)), rot, scaled]);
|
||||
// Stained: its fill tints the art.
|
||||
let mut stained = instance(&mut d, "My Mark", Affine::translate((40.0, 20.0)));
|
||||
stained.appearance = Appearance::basic(Paint::solid(Color::rgb8(255, 0, 0)), Paint::None, 0.0);
|
||||
stained.appearance = Appearance::basic(Paint::solid(Color::rgb8(255, 0, 0)), Paint::None, 0.0).into();
|
||||
stained.appearance.items.retain(|i| i.is_fill());
|
||||
if let vectorcraft_doc::AppearanceItem::Fill(f) = &mut stained.appearance.items[0] {
|
||||
f.opacity = 0.5;
|
||||
|
||||
@@ -612,7 +612,7 @@ mod tests {
|
||||
std::sync::Arc::new(Node::path(id, path, Appearance::basic(vectorcraft_color::Paint::None, vectorcraft_color::Paint::None, 0.0)))
|
||||
};
|
||||
let mut comp = Node::new(c, NodeKind::Compound { children: vec![line(a, 200.0), line(b, 300.0)], rule: Default::default() });
|
||||
comp.appearance = Appearance::default_art();
|
||||
comp.appearance = Appearance::default_art().into();
|
||||
let st = comp.appearance.stroke_mut().unwrap();
|
||||
st.width = 10.0;
|
||||
st.profile = Some(WidthProfile { points: vec![(0.0, 1.0, 1.0), (0.5, 1.0, 1.0), (1.0, 1.0, 1.0)] });
|
||||
|
||||
Reference in new issue
Block a user