Files
vectorcraft/crates/brush/src/defaults.rs
T
bflatasticandClaude Opus 5.5 2bc6c66c94 Part of gap 5: Scatter, Art and Pattern Brush Options
Double-clicking a Scatter, Art or Pattern brush in the Brushes panel (or the
panel menu's Brush Options...) now opens its options dialog, as Calligraphic
and Bristle brushes already did. OK runs brush.options and every stroke
painted with the brush redraws.

- Scatter Brush Options: Name; Size (10-1000 %), Spacing (10-10000 %),
  Scatter (-1000-1000 %) and Rotation (-180-180 deg), each Fixed (the first
  value), Random (between the two values) or Pressure (the first value at
  the lightest pen pressure, the second at the heaviest); Rotation relative
  to Page or Path. Scatter brushes now keep these modes (`modes`); brushes
  saved before keep their look (Random where the two values differ).
- Art Brush Options: Name; Width (1-1000 %); Scale Proportionately, Stretch
  to Fit Stroke Length or Stretch Between Guides with the guides' Start and
  End (% of the art length); the four Directions; Flip Along and Across.
- Pattern Brush Options: Name; Scale (1-10000 %), Spacing (0-10000 %); the
  five tiles (Outer Corner, Side, Inner Corner, Start, End) drawn with their
  art or None; Flip Along and Across; Stretch to fit, Add space to fit or
  Approximate path.
- All three: Colorization (None, Tints, Tints and Shades, Hue Shift with its
  Key Color picked from the art's colours, the most used one by default) and
  a live preview of a stroke painted with the edited brush.

Brush definitions from files and commands are now kept within these ranges
(Brush::sanitize, also for Calligraphic and Bristle values); unknown modes,
directions, fits or colorization methods are refused. The brush.options and
ui.brushOptions docs list the new params and fields. Tests: the brush model
(modes, pressure, sanitizing, art colours), engine params reaching the
brush and strokes redrawing, junk params in command_sweep, and headless
dialog tests for each dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:48:05 +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);
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();
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()
}),
},
]
})
}