mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 12:21:54 +00:00
Merge pull request #6389 from dotCooCoo/fix/butt-cap-collapsed-handle
Transform: a butt-capped curve with a handle on its end point stops at that end
This commit is contained in:
+65
-2
@@ -115,14 +115,38 @@ fn gentle(seg: &PathSeg, side: f64) -> bool {
|
||||
slowest > 0.0 && side * bend < slowest * slowest
|
||||
}
|
||||
|
||||
/// The box a stroke reaching `side` from `seg` paints across it, from the directions its tangent
|
||||
/// can take: the tangent is a mix of the nonzero control differences, so when their directions all
|
||||
/// lie within a quarter turn of the directions' sum, every point across the stroke is in `seg`'s
|
||||
/// box moved `side` along a normal of one of those directions. `None` when they do not. A handle on
|
||||
/// its end point adds no direction, so a straight curve with one measures as a line.
|
||||
fn turning_box(seg: &PathSeg, side: f64) -> Option<Rect> {
|
||||
let c = seg.to_cubic();
|
||||
let d = [c.p1 - c.p0, c.p2 - c.p1, c.p3 - c.p2];
|
||||
let moving = || d.iter().map(|v| (*v, v.hypot())).filter(|(_, h)| *h > 0.0).map(|(v, h)| v / h);
|
||||
let along = unit(moving().fold(Vec2::ZERO, |s, v| s + v));
|
||||
if moving().any(|v| v.dot(along) <= 0.0) {
|
||||
return None;
|
||||
}
|
||||
let angle = |v: &Vec2| along.cross(*v).atan2(along.dot(*v));
|
||||
let lo = moving().min_by(|a, b| angle(a).total_cmp(&angle(b)))?;
|
||||
let hi = moving().max_by(|a, b| angle(a).total_cmp(&angle(b)))?;
|
||||
let n = |t: Vec2| (Vec2::new(t.y, -t.x) * side).to_point();
|
||||
let one_side = round_join_box(Point::ZERO, lo, hi, side).unwrap_or_else(|| Rect::from_points(n(lo), n(hi)));
|
||||
let reach = one_side.union(Rect::new(-one_side.x1, -one_side.y1, -one_side.x0, -one_side.y0));
|
||||
let b = ParamCurveExtrema::bounding_box(seg);
|
||||
Some(Rect::new(b.x0 + reach.x0, b.y0 + reach.y0, b.x1 + reach.x1, b.y1 + reach.y1))
|
||||
}
|
||||
|
||||
/// The box a stroke reaching `side` from `seg` paints when it stops square at both ends of `seg`:
|
||||
/// the corners at the ends and the points across the stroke where the tangent is square to an axis.
|
||||
/// A curve that bends tighter than `side` is halved up to `depth` times, and a piece still bending
|
||||
/// that much is grown by `side` on every side.
|
||||
/// that much is measured by [`turning_box`], or grown by `side` on every side when its tangent
|
||||
/// turns too far.
|
||||
fn swept_box(seg: &PathSeg, side: f64, depth: u32) -> Rect {
|
||||
if !matches!(seg, PathSeg::Line(_)) && !gentle(seg, side) {
|
||||
if depth == 0 {
|
||||
return ParamCurveExtrema::bounding_box(seg).inflate(side, side);
|
||||
return turning_box(seg, side).unwrap_or_else(|| ParamCurveExtrema::bounding_box(seg).inflate(side, side));
|
||||
}
|
||||
return swept_box(&seg.subsegment(0.0..0.5), side, depth - 1).union(swept_box(&seg.subsegment(0.5..1.0), side, depth - 1));
|
||||
}
|
||||
@@ -614,6 +638,45 @@ mod tests {
|
||||
assert!((b.y1 - 50.0).abs() < 1e-9, "{b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_butt_curve_with_a_handle_on_its_end_point_stops_near_that_end() {
|
||||
let butt = stroke(2.0, |s| s.cap = LineCap::Butt);
|
||||
// A straight curve whose first or last handle sits on its end point measures like a line.
|
||||
let mut first = BezPath::new();
|
||||
first.move_to((50.0, 10.0));
|
||||
first.curve_to((50.0, 10.0), (50.0, 40.0), (50.0, 60.0));
|
||||
let mut last = BezPath::new();
|
||||
last.move_to((50.0, 10.0));
|
||||
last.curve_to((50.0, 20.0), (50.0, 60.0), (50.0, 60.0));
|
||||
for bp in [first, last] {
|
||||
assert_eq!(butt.bounds_on(&bp, bp.bounding_box()), Rect::new(49.0, 10.0, 51.0, 60.0));
|
||||
}
|
||||
// A bend that leaves its start straight up from a handle on the start point: the stroke
|
||||
// reaches only a little below the start, not half the weight.
|
||||
let mut bend = BezPath::new();
|
||||
bend.move_to((0.0, 50.0));
|
||||
bend.curve_to((0.0, 50.0), (0.0, 0.0), (50.0, 0.0));
|
||||
let b = stroke(4.0, |s| s.cap = LineCap::Butt).bounds_on(&bend, bend.bounding_box());
|
||||
assert!(b.y1 >= 50.0 && b.y1 < 50.2, "{b:?}");
|
||||
assert!((b.x0 + 2.0).abs() < 1e-9 && (b.y0 + 2.0).abs() < 1e-9 && (b.x1 - 50.0).abs() < 1e-9, "{b:?}");
|
||||
// Nearly a cusp: the curve turns back at its lowest point, where its tangent is level and
|
||||
// the stroke reaches the full half weight below it.
|
||||
let (a0, a1, a2) = (1e5 * 81.0 / 256.0, -1e5 * 9.0 / 8.0, 1e5);
|
||||
let x = |i: f64| 2e-9 * i / 3.0;
|
||||
let mut cusp = BezPath::new();
|
||||
cusp.move_to((x(0.0), a0));
|
||||
cusp.curve_to((x(1.0), a0 + a1 / 3.0), (x(2.0), a0 + 2.0 * a1 / 3.0 + a2 / 3.0), (x(3.0), a0 + a1 + a2));
|
||||
let b = butt.bounds_on(&cusp, cusp.bounding_box());
|
||||
assert!(b.y0 <= -1.0 + 1e-6, "{b:?}");
|
||||
// A curve far shorter than the weight still reaches half the weight above where its
|
||||
// tangent is level.
|
||||
let mut tiny = BezPath::new();
|
||||
tiny.move_to((0.0, 0.9e-12));
|
||||
tiny.curve_to((0.3e-12, -0.5e-12), (1.7e-12, -0.2e-12), (2e-12, 1e-12));
|
||||
let b = butt.bounds_on(&tiny, tiny.bounding_box());
|
||||
assert!(b.y0 <= -1.0 + 1e-9, "{b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_butt_stroke_is_slanted_where_a_segment_starts_or_ends_at_a_slant() {
|
||||
let mut ap = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 2.0);
|
||||
|
||||
@@ -1055,6 +1055,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Compound shapes ([Create compound shapes with Pathfinder](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/create-compound-shapes-with-pathfinder.html), [Pathfinder panel overview](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/pathfinder-panel-overview.html)): the Pathfinder panel menu's Make Compound Shape (every member in Add) and Alt-clicking a Shape Mode (every member in that mode) make the two or more selected paths, compound paths, groups, compound shapes or type a live compound shape (`object.compoundShape.make`): a group whose members each keep a shape mode and stay editable, drawn, printed and exported (SVG, PDF and the rest through the baked document) as the area the modes make from the backmost member up, open paths closed, painted with its own appearance, taken from the topmost member not subtracting. With members selected (Direct Selection, Layers panel, isolation mode) a Shape Mode button sets theirs (`object.compoundShape.setMode`) and is lit for the mode they share; paint given to a member goes to the compound shape. Release Compound Shape gives the members back as they were (`object.compoundShape.release`); Expand Compound Shape and the panel's Expand button turn it into the path or compound path of its area (`object.compoundShape.expand`). Shape Modes and Pathfinders take a compound shape as its area. Saved files keep it (`compound_shape`, `shape_mode`; an unknown mode reads as Add). Blends, envelopes and objects with a warp or another geometry effect are members as they draw (a blend's steps, an envelope's distorted art, the warped outline), and the area follows an edit of the blend, envelope or warp; Release gives them back as they were. Tests: `crates/engine/src/tests/compound_shape.rs`, `group::tests::compound_shapes_draw_the_area_their_shape_modes_make`, `group::tests::nested_compound_shapes_take_part_as_their_area`, `panels::pathfinder::tests::compound_shape_items_follow_the_selection`, the `CompoundShape` op of the property-test strategies. Not yet: the question marks on mixed member modes, hit-testing by the area (a click hits the members).
|
||||
- Effect › Path › Outline Object has no options. Type that carries it is drawn, exported (PDF and SVG as paths, with no font) and expanded as its glyph outlines while it stays editable, and the effects after it reshape those outlines. Paths and images draw as before. With Use Preview Bounds on, type is measured by its glyph outlines. Public reference: [Summary of effects](https://web.archive.org/web/20210901011839/https://helpx.adobe.com/illustrator/using/summary-effects.html) (archived copy).
|
||||
- A butt cap ends a stroke square at the end of an open path, and a projecting cap extends half the weight past it. VectorCraft's visual box of a butt-capped stroke on an open path does not grow past the path's ends: a straight segment's box comes from the corners of its stroked rectangle, and a curve's box comes from the stroke at its ends and where its tangent is square to an axis. Round and projecting caps and arrowheads still reach past an end. A curve that bends more tightly than half the weight can measure a little larger than it paints, and a butt-dashed path whose first or last dash is a gap is measured to the path's ends. Public reference: [Apply stroke on an object](https://helpx.adobe.com/illustrator/using/stroke-object.html).
|
||||
- A butt-capped curve whose first or last handle sits on its end point also stops at that end: a straight 50 pt curve with a 2 pt stroke and its first handle on its start point measures 50 pt tall, not 51 pt (doc `reach::tests::a_butt_curve_with_a_handle_on_its_end_point_stops_near_that_end`).
|
||||
- With Use Preview Bounds on, the bounding box, the Transform panel, Align, Distribute, Distribute Spacing and `object.setBounds` measure each object after its geometry effects. A path is measured by its effected shape, and a fill or stroke with geometry effects of its own is measured by the shape it paints. Type, images, live objects and symbol instances are measured by their reshaped art, and a symbol instance is also measured by its reshaped art when its symbol's art has geometry effects. A group or layer with geometry or Pathfinder effects, or with a fill or stroke of its own that has geometry effects or paints members with geometry effects, is measured by its evaluated art. A blend, envelope or repeat whose objects have geometry effects is measured by its evaluated art, with those objects measured after their effects. A 100 pt square without a stroke, with Offset Path 10, measures 120 pt, and type with Outline Object aligns and sizes by its glyphs. Raster effects are not measured. Public reference: [Create text](https://web.archive.org/web/20220127090642/https://helpx.adobe.com/illustrator/using/creating-text.html) (archived copy).
|
||||
- Initial live Revolve (#605, #846): a portable geometry and shading crate (`three-d`), editable path and compound profiles, compact options with artboard rotation and light gizmos, SVG/PDF vector output and Expand Appearance that can trim covered faces. Cap is on by default with continuous curved vector rims; shading uses vector gradients automatically, with sharp profile corners, uniform caps and round seam joins. One light and approximate visibility remain ([revolve.md](revolve.md)).
|
||||
- Effect › Path › Outline Object has no options. Type that carries it is drawn, exported (PDF and SVG as paths, with no font) and expanded as its glyph outlines while it stays editable, and the effects after it reshape those outlines. Paths and images draw as before. With Use Preview Bounds on, type is still measured by its layout box. Public reference: [Summary of effects](https://web.archive.org/web/20210901011839/https://helpx.adobe.com/illustrator/using/summary-effects.html) (archived copy).
|
||||
|
||||
Reference in New Issue
Block a user