mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 21:17:48 +00:00
Merge pull request #7119 from dotCooCoo/perf/pathops-overlap-sweep
Path ops: Offset Path sweeps long, low paths along their length
This commit is contained in:
@@ -183,10 +183,18 @@ fn offset_region(
|
||||
} else {
|
||||
BezPath::new()
|
||||
};
|
||||
let arr = Arrangement::new_budgeted(
|
||||
vec![(fill, FillRule::NonZero), (ring, FillRule::NonZero), (open_bp, FillRule::NonZero)],
|
||||
MAX_OFFSET_SEGMENT_PAIRS,
|
||||
)?;
|
||||
let parts = vec![fill, ring, open_bp];
|
||||
match crate::sweep_axis::turned_outlines(&parts) {
|
||||
Some(turned) => offset_faces(turned, delta, |p, r| within(crate::sweep_axis::unturn_point(p), r)).map(crate::sweep_axis::unturn),
|
||||
None => offset_faces(parts, delta, within),
|
||||
}
|
||||
}
|
||||
|
||||
/// The faces of the offset by `delta` from `parts` (the closed fill, the ring stroked around it and
|
||||
/// the stroke of the open subpaths, in that order), less the spurious ones that `within` finds.
|
||||
fn offset_faces(parts: Vec<BezPath>, delta: f64, within: impl Fn(kurbo::Point, f64) -> bool) -> Result<PathData, PathOpsError> {
|
||||
let d = delta.abs();
|
||||
let arr = Arrangement::new_budgeted(parts.into_iter().map(|p| (p, FillRule::NonZero)).collect(), MAX_OFFSET_SEGMENT_PAIRS)?;
|
||||
let c = if delta >= 0.0 { arr.contours_covering(|c| !c.is_empty()) } else { arr.contours_covering(|c| c.contains(&0) && !c.contains(&1)) };
|
||||
// When `d` exceeds a curvature radius the stroker's inner offset inverts and its loops cancel
|
||||
// winding, leaving faces uncovered that are really within `d` of the path. Every face of the
|
||||
|
||||
@@ -149,6 +149,11 @@ pub(crate) fn unturn(mut path: PathData) -> PathData {
|
||||
path
|
||||
}
|
||||
|
||||
/// A point of turned input, turned back.
|
||||
pub(crate) fn unturn_point(p: kurbo::Point) -> kurbo::Point {
|
||||
UNTURN * p
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use kurbo::{Join, Stroke, StrokeOpts};
|
||||
@@ -251,6 +256,39 @@ mod tests {
|
||||
assert_eq!(turns_during(|| drop(stroke_region(std::slice::from_ref(&short)))), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_path_sweeps_a_long_low_path_turned_with_the_result_of_the_path_on_end() {
|
||||
let mut rng = Rng(17);
|
||||
// Grown past the zigzag's bends, where the stroker leaves loops whose faces are dropped by
|
||||
// the distance test; grown a little; and inset.
|
||||
for (closed, delta) in [(false, 1.5), (true, 0.5), (true, -0.3)] {
|
||||
let what = format!("closed {closed}, offset {delta}");
|
||||
let low = zigzag(3, 300, 0.4, &mut rng, closed);
|
||||
let tall = upright(&low);
|
||||
let offset = |p: &PathData| crate::try_offset_path(p, delta, Join::Round, 4.0).unwrap();
|
||||
let mut turned = PathData::default();
|
||||
assert_eq!(turns_during(|| turned = offset(&low)), 1, "{what}: low");
|
||||
let mut on_end = PathData::default();
|
||||
assert_eq!(turns_during(|| on_end = offset(&tall)), 0, "{what}: tall");
|
||||
assert!(!turned.is_empty(), "{what}: empty");
|
||||
assert_same_region(&turned, &upright(&on_end), &what);
|
||||
}
|
||||
// A long plate with a row of round holes: growing it by 1 closes the narrow holes, which
|
||||
// the distance test drops, and keeps the wide ones.
|
||||
use kurbo::Shape;
|
||||
let mut plate = kurbo::Rect::new(0.0, 0.0, 4000.0, 20.0).to_path(0.1);
|
||||
for i in 0..200u32 {
|
||||
let r = if i % 2 == 0 { 0.8 } else { 4.0 };
|
||||
plate.extend(kurbo::Circle::new((10.0 + 20.0 * f64::from(i), 10.0), r).to_path(0.01).reverse_subpaths());
|
||||
}
|
||||
let plate = PathData::from_bezpath(&plate);
|
||||
let grow = |p: &PathData| crate::try_offset_path(p, 1.0, Join::Round, 4.0).unwrap();
|
||||
let mut grown = PathData::default();
|
||||
assert_eq!(turns_during(|| grown = grow(&plate)), 1, "plate");
|
||||
assert_eq!(grown.subpaths.len(), 1 + 100, "plate");
|
||||
assert_same_region(&grown, &upright(&grow(&upright(&plate))), "plate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_repeating_path_is_measured_at_every_position() {
|
||||
// Short segments in a row along the bottom, with a long one stacked above the row after
|
||||
|
||||
@@ -1173,6 +1173,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Pathfinder (10 exact curve booleans; single-result shape operations retain source names and SVG IDs, #1293), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below. With one group selected alone, the Pathfinder panel's buttons work on its members (among other selected objects a group still acts as one shape), and a greyed-out button says what it needs (#1450; behaviour reference: [combine objects using Pathfinder effects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/combine-objects-using-pathfinder-effects.html); one plain path selected alone stays disabled, which the documentation doesn't settle). Split Into Grid takes the number, height or width, gutter and total of the rows and of the columns, Add Guides and Preview, and several selected paths become one grid painted as the topmost one (#1462, `object.path.splitIntoGrid`; behaviour reference: [divide or split objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/divide-or-split-objects.html); a typed height or width keeps the gutter and changes the total, our choice). Object › Path › Remove Anchor Points (`path.removeAnchors`) removes the direct-selected anchors without opening their paths (the Delete key still deletes them with their segments): the neighbours keep their handle directions and their facing handles are refitted so one cubic follows the two old segments, and straight sides stay straight. The Delete Anchor Point tool uses the same refit. The Control bar, the contextual task bar, the Properties quick actions and the canvas context menu offer it when anchors are selected. With anchors selected, the Control bar and the Properties panel also convert them to corner or smooth (`path.convertAnchors`), connect end points (Join) and cut the path at them (`path.cutAtAnchors`, also on the task bar and the context menu): a closed path opens there and an open one becomes one path per piece, each cut leaving one of its two coincident anchors selected so a drag pulls the path apart. In the Pathfinder panel a group selected alone counts its nested shapes, so an expanded stroked object keeps the Shape Modes enabled (#1642, #1635).
|
||||
Outline Stroke, Expand's Stroke option and Flatten Transparency's Convert All Strokes to Outlines apply the object's visible geometry effects (Offset Path, Zig Zag and the others) to the path, and then apply each stroke's own geometry effects before outlining that stroke, as the canvas draws it. The filled outline of a stroke without a brush keeps the stroke's other effects, such as a drop shadow.
|
||||
Outline Stroke and Unite remove the overlaps of a long, low path with the sweep turned along it: when the estimated number of segments the sweep holds at once is under two thirds as large for the path turned a quarter turn (paths of 2,048 segments or more), the overlaps are removed on the turned path and the result is turned back, which moves no point by rounding. The lookups of the input curve that a piece of the result lies on, and of the input anchors that are kept, can search along y instead of x: once lookups with more than 64 candidates along x have counted 16 for each input curve or anchor, a lookup searches along y when it finds fewer candidates there (under a quarter as many for curves), and where that has rarely happened it measures along y in only one lookup of 32. On one open path of 200,000 anchors in rows of 1,000, Outline Stroke takes 17.3 s of processor time instead of 31.3 s and Unite 2.9 s instead of 6.5 s in a release build. A turned result has as many subpaths as the result without the turn, and the area where the two differ is no larger than the refit precision times the outline's length (`crates/pathops/src/sweep_axis.rs` tests).
|
||||
Offset Path turns the sweep the same way: when the estimate for its fill, the ring stroked around the fill and the outline of its open subpaths is under two thirds as large turned a quarter turn, the faces are found on the turned parts, the faces that lie within the offset distance of the path are dropped by testing their points turned back, and the result is turned back. On an open path of 10,000 anchors in rows of 1,000, offsetting by 2 pt takes 1.0 s of processor time instead of 1.8 s in a release build. The offset of a long, low path has as many subpaths as the offset of the same path stood on end, and the area where the two differ is no larger than the refit precision times the outline's length (`offset_path_sweeps_a_long_low_path_turned_with_the_result_of_the_path_on_end`).
|
||||
- Divide uses open lines, bent paths and curves as cutting edges (#1307), preserving the frontmost filled face's appearance and holes. Engine tests cover implicit closure of filled open contours and unrelated objects; native automation confirms independent piece movement, one Undo/Redo, canvas/window captures and an exact saved-document read by a separate CLI process. The shared planar map keeps a segment cap (65,536 since #937, was 4,096) and its filled-area fallback; other Pathfinder operations are unchanged.
|
||||
- Type that Transform effects only move and copy (no scale, rotation, reflection or Random) exports as type in each place, and PDF export writes every copy as selectable, searchable text, when its characters are in solid colors without underline or strikethrough, the type object has no fills or strokes of its own, no inline graphics and no perspective, its transform does not scale or shear stroked characters, and the copies do not overlap. When the effect is on the type itself, the type can also have transparency, an opacity mask and raster effects. When the effect is on the group or layer holding the type, the type has no transparency or effects of its own and is not in a clipping path, the group or layer has no fill, stroke or Pathfinder effect of its own, and no group between them has a fill, stroke, Pathfinder or geometry effect. Type still exports as outlines when the effect stops its copies early, or when the copies would number more than 10,000 or take more than 20,000,000 bytes of document data, and so does other type under a geometry effect.
|
||||
- Extrude & Bevel and Inflate treat strokes as outlined: each visible stroke builds a solid from its outline in its own paint beside the top fill's ([three-d.md](three-d.md#fills-and-strokes)).
|
||||
|
||||
Reference in New Issue
Block a user