mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 18:27:02 -06:00
G5: Art brush Overlap option bends art per corner run
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Cursor
parent
4be63905b1
commit
b929561fc3
@@ -287,6 +287,8 @@ pub struct ArtBrush {
|
||||
pub width: f64,
|
||||
pub flip_along: bool,
|
||||
pub flip_across: bool,
|
||||
/// When true, art is placed on each run between sharp corners so folds do not overlap.
|
||||
pub overlap: bool,
|
||||
pub colorization: Colorization,
|
||||
}
|
||||
|
||||
@@ -304,6 +306,7 @@ impl Default for ArtBrush {
|
||||
width: 100.0,
|
||||
flip_along: false,
|
||||
flip_across: false,
|
||||
overlap: false,
|
||||
colorization: Colorization::Tints,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,24 @@ fn art_brush_between_guides_keeps_ends() {
|
||||
assert!(near(h.x0, 280.0, 1e-6) && near(h.x1, 300.0, 1e-6), "{h:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn art_brush_overlap_splits_at_corners() {
|
||||
let mut bp = BezPath::new();
|
||||
bp.move_to((0.0, 0.0));
|
||||
bp.line_to((100.0, 0.0));
|
||||
bp.line_to((100.0, 100.0));
|
||||
let brush = |overlap: bool| {
|
||||
Brush {
|
||||
name: "a".into(),
|
||||
kind: BrushKind::Art(ArtBrush { art: rect_art(10.0, 4.0), scale: ArtScale::Stretch, overlap, colorization: Colorization::None, ..Default::default() }),
|
||||
}
|
||||
};
|
||||
let one = stroke_pieces(&Document::new(1.0, 1.0), &brush(false), &bp, &stroke(1.0));
|
||||
let two = stroke_pieces(&Document::new(1.0, 1.0), &brush(true), &bp, &stroke(1.0));
|
||||
assert_eq!(one.len(), 1, "one bent copy along the whole path");
|
||||
assert_eq!(two.len(), 2, "one copy per run between corners");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn art_brush_follows_curves() {
|
||||
let bp = shapes::ellipse(Rect::new(0.0, 0.0, 200.0, 200.0)).to_bezpath();
|
||||
|
||||
+51
-21
@@ -154,35 +154,65 @@ fn bend(t: &Track, art: &Node, b: Rect, along: AlongMap, cross: f64) -> Option<N
|
||||
warp_node(art, &f, flat_tol, MAX_SEG / along.max_scale(), cross.abs())
|
||||
}
|
||||
|
||||
/// Arc-length spans to place art on when [`ArtBrush::overlap`] is on: one run between each sharp
|
||||
/// corner (the whole path when it has none).
|
||||
fn art_runs(t: &Track) -> Vec<(f64, f64)> {
|
||||
let l = t.len();
|
||||
let corners = t.corner_positions();
|
||||
if corners.is_empty() {
|
||||
return vec![(0.0, l)];
|
||||
}
|
||||
if t.closed {
|
||||
let first = corners[0].1;
|
||||
let mut bounds = corners.iter().map(|c| c.1).collect::<Vec<_>>();
|
||||
bounds.push(first + l);
|
||||
return bounds.windows(2).map(|w| (w[0], w[1])).collect();
|
||||
}
|
||||
let mut bounds = vec![0.0];
|
||||
bounds.extend(corners.iter().map(|c| c.1));
|
||||
bounds.push(l);
|
||||
bounds.windows(2).map(|w| (w[0], w[1])).collect()
|
||||
}
|
||||
|
||||
fn art_on_run(a: &ArtBrush, t: &Track, s0: f64, s1: f64, art: &Node, b: Rect, cross: f64) -> Option<Node> {
|
||||
let l = s1 - s0;
|
||||
if l <= 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let w = b.width();
|
||||
let (along, cross) = match a.scale {
|
||||
ArtScale::Stretch => (AlongMap::linear(w, s0, s1), cross),
|
||||
ArtScale::Proportional => {
|
||||
let k = l / w;
|
||||
(AlongMap::linear(w, s0, s1), k * a.width / 100.0)
|
||||
}
|
||||
ArtScale::BetweenGuides { start, end } => {
|
||||
let (g0, g1) = (start.clamp(0.0, 1.0) * w, end.clamp(0.0, 1.0) * w);
|
||||
let (g0, g1) = (g0.min(g1), g0.max(g1));
|
||||
let fixed = (g0 + (w - g1)) * cross;
|
||||
if fixed >= l || g1 - g0 < 1e-9 {
|
||||
(AlongMap::linear(w, s0, s1), cross)
|
||||
} else {
|
||||
(AlongMap { xs: [0.0, g0, g1, w], ss: [s0, s0 + g0 * cross, s1 - (w - g1) * cross, s1] }, cross)
|
||||
}
|
||||
}
|
||||
};
|
||||
bend(t, art, b, along, cross)
|
||||
}
|
||||
|
||||
pub(crate) fn art(a: &ArtBrush, bp: &BezPath, weight: f64) -> Vec<Node> {
|
||||
let Some((art, b)) = oriented(&a.art, a.direction, a.flip_along, a.flip_across) else { return vec![] };
|
||||
let cross = weight * a.width / 100.0;
|
||||
let mut out = vec![];
|
||||
for t in tracks(bp, 0.05) {
|
||||
let l = t.len();
|
||||
if l <= 1e-9 {
|
||||
if t.len() <= 1e-9 {
|
||||
continue;
|
||||
}
|
||||
let w = b.width();
|
||||
let (along, cross) = match a.scale {
|
||||
ArtScale::Stretch => (AlongMap::linear(w, 0.0, l), cross),
|
||||
ArtScale::Proportional => {
|
||||
let k = l / w;
|
||||
(AlongMap::linear(w, 0.0, l), k * a.width / 100.0)
|
||||
let runs = if a.overlap { art_runs(&t) } else { vec![(0.0, t.len())] };
|
||||
for (s0, s1) in runs {
|
||||
if let Some(n) = art_on_run(a, &t, s0, s1, &art, b, cross) {
|
||||
out.push(n);
|
||||
}
|
||||
ArtScale::BetweenGuides { start, end } => {
|
||||
let (g0, g1) = (start.clamp(0.0, 1.0) * w, end.clamp(0.0, 1.0) * w);
|
||||
let (g0, g1) = (g0.min(g1), g0.max(g1));
|
||||
let fixed = (g0 + (w - g1)) * cross;
|
||||
if fixed >= l || g1 - g0 < 1e-9 {
|
||||
(AlongMap::linear(w, 0.0, l), cross)
|
||||
} else {
|
||||
(AlongMap { xs: [0.0, g0, g1, w], ss: [0.0, g0 * cross, l - (w - g1) * cross, l] }, cross)
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(n) = bend(&t, &art, b, along, cross) {
|
||||
out.push(n);
|
||||
}
|
||||
}
|
||||
out
|
||||
|
||||
@@ -89,7 +89,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Brush Options…",
|
||||
[],
|
||||
None,
|
||||
"{name, params?: {…fields to change (brush.get shows them; values outside their ranges are clamped). calligraphic: angle −180..180°, roundness 0..100%, size 0..1296 pt, variation: [angle°, roundness%, size pt], modes: [fixed|random|pressure for angle, roundness, size] (pressure goes from value − variation at the lightest pen pressure to value + variation at the heaviest). scatter: size [a, b] 1..10000%, spacing [a, b] 1..10000%, scatter [a, b] −1000..1000%, rotation [a, b] −180..180°, modes: [fixed|random|pressure for size, spacing, scatter, rotation] (fixed: a; random: between a and b; pressure: a at the lightest pen pressure to b at the heaviest), rotation_relative_to_path. art: width 1..1000%, scale: {mode: proportional|stretch|betweenGuides, start, end: 0..1 of the art length}, direction: leftToRight|rightToLeft|topToBottom|bottomToTop, flip_along, flip_across. pattern: scale 1..10000%, spacing 0..10000%, fit: stretch|addSpace|approximate, flip_along, flip_across. scatter, art and pattern: colorization: {method: none|tints|tintsAndShades|hueShift, key: colour (hueShift: the art colour that becomes the stroke colour)}}, newName? (trimmed; blank: the name is kept)} edit a brush definition (strokes using it update) → {name}",
|
||||
"{name, params?: {…fields to change (brush.get shows them; values outside their ranges are clamped). calligraphic: angle −180..180°, roundness 0..100%, size 0..1296 pt, variation: [angle°, roundness%, size pt], modes: [fixed|random|pressure for angle, roundness, size] (pressure goes from value − variation at the lightest pen pressure to value + variation at the heaviest). scatter: size [a, b] 1..10000%, spacing [a, b] 1..10000%, scatter [a, b] −1000..1000%, rotation [a, b] −180..180°, modes: [fixed|random|pressure for size, spacing, scatter, rotation] (fixed: a; random: between a and b; pressure: a at the lightest pen pressure to b at the heaviest), rotation_relative_to_path. art: width 1..1000%, scale: {mode: proportional|stretch|betweenGuides, start, end: 0..1 of the art length}, direction: leftToRight|rightToLeft|topToBottom|bottomToTop, flip_along, flip_across, overlap. pattern: scale 1..10000%, spacing 0..10000%, fit: stretch|addSpace|approximate, flip_along, flip_across. scatter, art and pattern: colorization: {method: none|tints|tintsAndShades|hueShift, key: colour (hueShift: the art colour that becomes the stroke colour)}}, newName? (trimmed; blank: the name is kept)} edit a brush definition (strokes using it update) → {name}",
|
||||
has_doc,
|
||||
brush_options
|
||||
),
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
//! Scatter: per value (`size`, `spacing`, `scatter` %, `rotation` °) `…Min`, `…Max` and `…Mode`
|
||||
//! (`fixed` | `random` | `pressure`), `rotationRelativeTo` (`page` | `path`).
|
||||
//! Art: `width` (%), `scaleMode` (`proportional` | `stretch` | `betweenGuides`), `guideStart` and
|
||||
//! `guideEnd` (% of the art's length), `direction` (`leftToRight` …), `flipAlong`, `flipAcross`.
|
||||
//! `guideEnd` (% of the art's length), `direction` (`leftToRight` …), `flipAlong`, `flipAcross`,
|
||||
//! `overlap` (bend on each run between sharp corners).
|
||||
//! Pattern: `scale`, `spacing` (%), `fit` (`stretch` | `addSpace` | `approximate`), `flipAlong`,
|
||||
//! `flipAcross`, `__tiles` (slot → preview key, for the tiles the brush has).
|
||||
|
||||
@@ -96,6 +97,7 @@ pub(super) fn fields(b: &Brush, def: Value, f: &mut Map<String, Value>) {
|
||||
f.insert("direction".into(), json!(id_of(&a.direction)));
|
||||
f.insert("flipAlong".into(), json!(a.flip_along));
|
||||
f.insert("flipAcross".into(), json!(a.flip_across));
|
||||
f.insert("overlap".into(), json!(a.overlap));
|
||||
}
|
||||
BrushKind::Pattern(p) => {
|
||||
f.insert("scale".into(), json!(p.scale));
|
||||
@@ -168,6 +170,7 @@ pub(super) fn params(d: &Dialog) -> Value {
|
||||
"direction": id_of(&parse_field::<Direction>(d, "direction")),
|
||||
"flip_along": d.bool("flipAlong"),
|
||||
"flip_across": d.bool("flipAcross"),
|
||||
"overlap": d.bool("overlap"),
|
||||
})
|
||||
}
|
||||
_ => json!({
|
||||
@@ -284,6 +287,8 @@ fn art_body(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
}
|
||||
});
|
||||
flips(ui, d);
|
||||
ui.add_space(4.0);
|
||||
super::form::check(ui, d, "overlap", tl!("Overlap"));
|
||||
}
|
||||
|
||||
/// A toggle button showing an arrow pointing the way `dir` runs. Whether it was clicked.
|
||||
|
||||
Reference in New Issue
Block a user