mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-08 19:42:05 +00:00
Merge pull request #1847 from samhyun/fix/dashed-spread-reach
M8: a spread round a stroke with corners keeps its whole shadow
This commit is contained in:
2 files changed
+72
-5
No files matched your search
+17
-5
@@ -948,7 +948,7 @@ impl Node {
|
||||
if self.stroke.cap_start.is_arrow() || self.stroke.cap_end.is_arrow() || point_arrows { w.max(self.stroke.weight) * 3.0 } else { w };
|
||||
// Right-angled or smooth outlines (boxes, whole ellipses) end their mitres on the grown
|
||||
// box; an arc's corners don't.
|
||||
let miter = if self.grows_by_exactly_its_spread() { 1.0 } else { self.stroke.miter_limit.clamp(1.0, 4.0) };
|
||||
let miter = if self.outline_mitres_end_on_its_box() { 1.0 } else { self.stroke.miter_limit.clamp(1.0, 4.0) };
|
||||
base.max(if matches!(self.kind, NodeKind::Line | NodeKind::Vector { .. }) { caps } else { 0.0 }) * miter
|
||||
} else {
|
||||
0.0
|
||||
@@ -964,9 +964,10 @@ impl Node {
|
||||
!(matches!(self.kind, NodeKind::Group) || (matches!(self.kind, NodeKind::Text(_)) && self.has_visible_stroke()))
|
||||
}
|
||||
|
||||
/// Whether a spread grows this node's outline by exactly the spread (no sharp corners). An arc
|
||||
/// (an ellipse that doesn't go all the way round) has corners where its ends meet its sides.
|
||||
fn grows_by_exactly_its_spread(&self) -> bool {
|
||||
/// Whether this node's outline has no sharp corners (right-angled ones aside), so mitres along
|
||||
/// it end on its box grown by as much: a box or a whole ellipse. An arc (an ellipse that doesn't
|
||||
/// go all the way round) has corners where its ends meet its sides.
|
||||
fn outline_mitres_end_on_its_box(&self) -> bool {
|
||||
match self.kind {
|
||||
NodeKind::Rectangle | NodeKind::Section { .. } => true,
|
||||
NodeKind::Ellipse { sweep, .. } => figcraft_geom::full_sweep(sweep),
|
||||
@@ -974,9 +975,20 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a spread grows this node by exactly the spread: its outline mitres end on its box
|
||||
/// (see [`Node::outline_mitres_end_on_its_box`]), and so does its stroke. A dashed stroke is
|
||||
/// cut into dashes with corners wherever they fall round the outline, and a width profile or a
|
||||
/// brush can give the stroke corners of its own, which the spread mitres out.
|
||||
fn grows_by_exactly_its_spread(&self) -> bool {
|
||||
let s = &self.stroke;
|
||||
let cornered = self.has_visible_stroke() && (!s.dashes.is_empty() || s.profile().is_some() || s.brush.is_some());
|
||||
!cornered && self.outline_mitres_end_on_its_box()
|
||||
}
|
||||
|
||||
/// How much further than its spread a drop shadow can reach at the outline's sharp corners:
|
||||
/// spread moves the edges out in parallel and they meet in a mitre (see
|
||||
/// [`SPREAD_MITER_LIMIT`]). Rectangles, frames and whole ellipses grow by exactly the spread.
|
||||
/// [`SPREAD_MITER_LIMIT`]). Rectangles, frames and whole ellipses grow by exactly the spread,
|
||||
/// unless their stroke has corners (see [`Node::grows_by_exactly_its_spread`]).
|
||||
fn spread_miter_reach(&self) -> f64 {
|
||||
if self.grows_by_exactly_its_spread() {
|
||||
return 0.0;
|
||||
|
||||
@@ -192,3 +192,58 @@ fn arc_stroke_mitres_are_not_cut_off() {
|
||||
assert!(worst <= 2, "{align:?}: {worst} levels cut off");
|
||||
}
|
||||
}
|
||||
|
||||
/// A dashed stroke is cut into separate dashes with square ends, a width profile can step its
|
||||
/// width and a brush lays down shapes of its own: round a whole ellipse or a rounded box, a spread
|
||||
/// mitres out at their corners, past the box grown by the spread, and keeps its whole shadow.
|
||||
#[test]
|
||||
fn stroke_corners_spread_mitres_are_not_cut_off() {
|
||||
use figcraft_doc::{Brush, Node, StrokeAlign, StrokeCap, WidthProfile};
|
||||
type Style = fn(&mut Node);
|
||||
let strokes: [(&str, Style); 4] = [
|
||||
("dashed", |n| n.stroke.dashes = vec![30.0, 30.0]),
|
||||
("dashed with square caps", |n| (n.stroke.dashes, n.stroke.dash_cap) = (vec![30.0, 30.0], Some(StrokeCap::Square))),
|
||||
("stepped", |n| n.stroke.width_profile = Some(WidthProfile::new([(0.0, 1.0), (0.25, 4.0), (0.26, 0.2), (0.75, 4.0)]))),
|
||||
("starry", |n| n.stroke.brush = Some(Brush::new("stars"))),
|
||||
];
|
||||
let shapes = [(NodeKind::Ellipse { start: 0.0, sweep: std::f64::consts::TAU, inner: 0.0 }, 0.0), (NodeKind::Rectangle, 100.0)];
|
||||
for (kind, radius) in shapes {
|
||||
for (name, style) in strokes {
|
||||
for align in [StrokeAlign::Outside, StrokeAlign::Center] {
|
||||
let make = |effects: Vec<EffectKind>| {
|
||||
let (mut d, n) = shape(kind.clone(), effects);
|
||||
let node = d.get_mut(n).unwrap();
|
||||
node.corners.radii = [radius; 4];
|
||||
node.strokes = vec![Paint::solid(Color::WHITE)];
|
||||
(node.stroke.weight, node.stroke.align) = (20.0, align);
|
||||
style(node);
|
||||
(d, n)
|
||||
};
|
||||
let (d, n) = make(vec![EffectKind::DropShadow(shadow(30.0))]);
|
||||
let clear = Shadow { color: Color::new(0.0, 0.0, 0.0, 0.0), ..shadow(120.0) };
|
||||
let (roomy, m) = make(vec![EffectKind::DropShadow(shadow(30.0)), EffectKind::DropShadow(clear)]);
|
||||
let (a, b) = (render(&d, n), render(&roomy, m));
|
||||
let worst = a.data().chunks(4).zip(b.data().chunks(4)).map(|(p, q)| p[3].abs_diff(q[3])).max().unwrap_or(0);
|
||||
// The stroke lands a little differently on a layer of another size, even when
|
||||
// both have room to spare: up to 16 levels at a pixel or two. Cut off is 255.
|
||||
assert!(worst <= 16, "{kind:?} radius {radius}, {name} {align:?}: {worst} levels cut off");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Without a spread, a dashed stroke reaches no further than a solid one: the bounds an export
|
||||
/// goes by stay the box grown by the stroke.
|
||||
#[test]
|
||||
fn a_dashed_stroke_without_a_spread_keeps_its_bounds() {
|
||||
use figcraft_doc::StrokeAlign;
|
||||
for kind in [NodeKind::Rectangle, NodeKind::Ellipse { start: 0.0, sweep: std::f64::consts::TAU, inner: 0.0 }] {
|
||||
let (mut d, n) = shape(kind.clone(), vec![]);
|
||||
let node = d.get_mut(n).unwrap();
|
||||
node.strokes = vec![Paint::solid(Color::WHITE)];
|
||||
(node.stroke.weight, node.stroke.align) = (20.0, StrokeAlign::Outside);
|
||||
node.stroke.dashes = vec![30.0, 30.0];
|
||||
let b = crate::export_bounds(&d, n).unwrap();
|
||||
assert_eq!((b.x0, b.y0, b.x1, b.y1), (80.0, 80.0, 320.0, 320.0), "{kind:?}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user