Files
vectorcraft/crates/brush/src/defaults.rs
T
arhammusheerandClaude Opus 5.5 0724aafbb6 #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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
2026-10-11 23:27:21 +07:00

221 lines
9.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The default brush library. All art is original and drawn here in code.
//!
//! Art is authored at 1 pt stroke weight, left → right, centred on y = 0.
use std::sync::{Arc, OnceLock};
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{Appearance, AppearanceItem, FillLayer, Node, NodeId, NodeKind};
use vectorcraft_geom::{Anchor, FillRule, PathData, Point, Rect, SubPath, shapes};
use crate::{ArtBrush, ArtScale, Bristle, BristleShape, Brush, BrushKind, Calligraphic, Colorization, Direction, PatternBrush, PatternFit, Scatter};
fn fill(c: Color) -> Appearance {
Appearance { items: vec![AppearanceItem::Fill(FillLayer::new(Paint::solid(c)))], ..Default::default() }
}
fn path(p: PathData, c: Color) -> Node {
Node::path(NodeId(0), p, fill(c))
}
fn group(children: Vec<Node>) -> Node {
Node::group(NodeId(0), children.into_iter().map(Arc::new).collect())
}
/// Even-odd compound of `paths`, filled `c`.
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).into();
n
}
fn smooth_closed(pts: &[Point]) -> PathData {
let n = pts.len();
let anchors = (0..n)
.map(|i| {
let (prev, p, next) = (pts[(i + n - 1) % n], pts[i], pts[(i + 1) % n]);
let t = (next - prev) / 6.0;
Anchor::with_handles(p, p - t, p + t)
})
.collect();
PathData::single(SubPath::new(anchors, true))
}
/// A 4 pt dot (Dots scatter brush, default scatter art).
pub(crate) fn dot_art() -> Node {
path(shapes::ellipse(Rect::new(-2.0, -2.0, 2.0, 2.0)), Color::BLACK)
}
/// A smooth lens tapering to points at both ends, 100 × 4 pt.
pub(crate) fn taper_art() -> Node {
let sp = SubPath::new(
vec![
Anchor::with_handles(Point::new(0.0, 0.0), Point::new(30.0, 2.7), Point::new(30.0, -2.7)),
Anchor::with_handles(Point::new(100.0, 0.0), Point::new(70.0, -2.7), Point::new(70.0, 2.7)),
],
true,
);
path(PathData::single(sp), Color::BLACK)
}
/// A rough, tapered charcoal smear, 100 × 6 pt.
fn charcoal_art() -> Node {
let n = 24;
let jitter =
[0.82, 1.0, 0.9, 0.7, 0.95, 0.85, 1.0, 0.75, 0.92, 0.8, 0.98, 0.72, 0.88, 1.0, 0.78, 0.93, 0.86, 0.7, 0.97, 0.83, 0.9, 0.76, 1.0, 0.85];
let mut pts = vec![];
for i in 0..=n {
let x = 100.0 * i as f64 / n as f64;
let env = (std::f64::consts::PI * x / 100.0).sin().powf(0.6);
pts.push(Point::new(x, -3.0 * env * jitter[i % jitter.len()]));
}
for i in (1..n).rev() {
let x = 100.0 * i as f64 / n as f64;
let env = (std::f64::consts::PI * x / 100.0).sin().powf(0.6);
pts.push(Point::new(x, 3.0 * env * jitter[(i * 7 + 3) % jitter.len()]));
}
let main = path(PathData::single(SubPath::polyline(&pts, true)), Color::gray(0.85));
// A few grainy streaks inside.
let streak = |y: f64, x0: f64, x1: f64| path(shapes::ellipse(Rect::new(x0, y - 0.35, x1, y + 0.35)), Color::WHITE);
group(vec![main, streak(-1.2, 20.0, 45.0), streak(0.9, 55.0, 85.0), streak(0.1, 8.0, 22.0)])
}
/// A shaft with a triangular head at the right end, 100 × 8 pt.
fn arrow_art() -> Node {
let shaft = shapes::rectangle(Rect::new(0.0, -1.0, 84.0, 1.0));
let head = PathData::single(SubPath::polyline(&[Point::new(80.0, -4.0), Point::new(100.0, 0.0), Point::new(80.0, 4.0)], true));
group(vec![path(shaft, Color::BLACK), path(head, Color::BLACK)])
}
/// A small leaf, 12 × 6 pt, pointing right.
fn leaf_art() -> Node {
let body = smooth_closed(&[Point::new(0.0, 0.0), Point::new(6.0, -3.0), Point::new(12.0, 0.0), Point::new(6.0, 3.0)]);
let vein = shapes::rectangle(Rect::new(1.0, -0.2, 11.0, 0.2));
group(vec![path(body, Color::rgb8(0x3f, 0x8f, 0x4a)), path(vein, Color::rgb8(0xc9, 0xe8, 0xb5))])
}
/// Two confetti squares of different greys (tinted by the stroke colour).
fn confetti_art() -> Node {
group(vec![
path(shapes::rectangle(Rect::new(-1.5, -1.5, 1.5, 1.5)), Color::BLACK),
path(shapes::rectangle(Rect::new(0.5, 0.5, 2.5, 2.5)), Color::gray(0.45)),
])
}
/// One chain link (an oval ring) plus half of the connecting links, 14 × 6 pt.
fn chain_tile() -> Node {
let ring = compound(vec![shapes::ellipse(Rect::new(1.0, -3.0, 13.0, 3.0)), shapes::ellipse(Rect::new(3.0, -1.4, 11.0, 1.4))], Color::BLACK);
let bar_l = path(shapes::rectangle(Rect::new(0.0, -0.6, 1.5, 0.6)), Color::BLACK);
let bar_r = path(shapes::rectangle(Rect::new(12.5, -0.6, 14.0, 0.6)), Color::BLACK);
group(vec![ring, bar_l, bar_r])
}
/// A round ring for chain corners, 8 × 8 pt.
fn chain_corner() -> Node {
compound(vec![shapes::ellipse(Rect::new(-4.0, -4.0, 4.0, 4.0)), shapes::ellipse(Rect::new(-2.2, -2.2, 2.2, 2.2))], Color::BLACK)
}
/// A dash with a clear gap (running stitches), 10 × 1.5 pt.
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().into();
group(vec![spacer, dash])
}
/// A diamond tile for borders, 8 × 6 pt.
fn diamond_tile() -> Node {
let d = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(4.0, -3.0), Point::new(8.0, 0.0), Point::new(4.0, 3.0)], true));
path(d, Color::BLACK)
}
fn calli(name: &str, angle: f64, roundness: f64, size: f64) -> Brush {
Brush { name: name.into(), kind: BrushKind::Calligraphic(Calligraphic { angle, roundness, size, ..Default::default() }) }
}
/// The default library.
pub fn defaults() -> &'static [Brush] {
static LIB: OnceLock<Vec<Brush>> = OnceLock::new();
LIB.get_or_init(|| {
vec![
calli("3 pt. Round", 0.0, 100.0, 3.0),
calli("6 pt. Flat", 45.0, 15.0, 6.0),
calli("10 pt. Oval", 30.0, 45.0, 10.0),
Brush {
name: "Charcoal".into(),
kind: BrushKind::Art(ArtBrush { art: charcoal_art(), colorization: Colorization::Tints, ..Default::default() }),
},
Brush { name: "Tapered Stroke".into(), kind: BrushKind::Art(ArtBrush::default()) },
Brush {
name: "Arrow".into(),
kind: BrushKind::Art(ArtBrush {
art: arrow_art(),
direction: Direction::LeftToRight,
scale: ArtScale::BetweenGuides { start: 0.0, end: 0.78 },
colorization: Colorization::Tints,
..Default::default()
}),
},
Brush {
name: "Dots".into(),
kind: BrushKind::Scatter(Scatter { spacing: (160.0, 160.0), colorization: Colorization::Tints, ..Default::default() }),
},
Brush {
name: "Confetti".into(),
kind: BrushKind::Scatter(Scatter {
art: confetti_art(),
size: (60.0, 140.0),
spacing: (120.0, 220.0),
scatter: (-150.0, 150.0),
rotation: (-180.0, 180.0),
rotation_relative_to_path: false,
colorization: Colorization::Tints,
modes: None,
}),
},
Brush {
name: "Leaves".into(),
kind: BrushKind::Scatter(Scatter {
art: leaf_art(),
size: (80.0, 120.0),
spacing: (90.0, 130.0),
scatter: (-60.0, 60.0),
rotation: (-35.0, 35.0),
rotation_relative_to_path: true,
colorization: Colorization::None,
modes: None,
}),
},
Brush {
name: "Chain".into(),
kind: BrushKind::Pattern(PatternBrush {
side: chain_tile(),
outer_corner: Some(chain_corner()),
inner_corner: Some(chain_corner()),
fit: PatternFit::Stretch,
..Default::default()
}),
},
Brush { name: "Stitches".into(), kind: BrushKind::Pattern(PatternBrush::default()) },
Brush {
name: "Diamonds".into(),
kind: BrushKind::Pattern(PatternBrush { side: diamond_tile(), spacing: 25.0, fit: PatternFit::AddSpace, ..Default::default() }),
},
Brush { name: "Bristle Round".into(), kind: BrushKind::Bristle(Bristle::default()) },
Brush {
name: "Bristle Flat Fan".into(),
kind: BrushKind::Bristle(Bristle {
shape: BristleShape::FlatFan,
size: 9.0,
density: 80.0,
thickness: 25.0,
opacity: 55.0,
..Default::default()
}),
},
]
})
}