Commit Graph
8 Commits
Author SHA1 Message Date
dotCooCoo 7960fa722f Fix #1770: Shape Builder merges leave no spur or split outline when shared edges differ by rounding
The Shape Builder merges faces by uniting their contours, and the edges
two faces share cancel out. The sweep can give two faces copies of a
shared edge whose ends lie a few times its tolerance apart, with the
difference left as pieces shorter than the tolerance. The union could
then keep the space between the copies: a hairline that joined a hole
as a spur, or a seam that cut a petal tip off the ring's outer outline
as an outline of its own. On Apple Silicon, turning one petal of the
rosette test by -180 degrees instead of 180 leaves a spur.

A Shape Builder action unites the picked faces first, and then what is
left of each shape. When two copies of a shared edge differ in the
first of these unions that unites the faces' contours, each point of
the faces' contours that lies within 16 times the sweep's tolerance of
a point kept before it now moves onto that point before that union and
each later one, with the handles next to it. A piece left within that
distance of one point is dropped. The planar map joins the ends of
copies of an edge within the same distance. Copies that share their
ends leave at most a hairline contour, which the merge drops as a
sliver. When the copies in that union all match exactly, the unions
take the contours unchanged. Only the Shape Builder's merges and
deletions unite faces this way, so Pathfinder output does not change.
2026-10-16 02:21:46 +07:00
bflatastic 65e2168791 Shape Builder: the gaps closed paths enclose are regions (#1691) 2026-10-14 11:28:21 +07:00
dotCooCoo 138d9499a7 Shape Builder: a line's loose end past a crossing no longer runs out and back in most cases
Since #1035 moved linesweeper to 0.5, the sweep can cut the paths
through one crossing at points more than its tolerance (eps) apart.
The planar map merged a point only into a vertex within eps of it, so
the two passes of an open path could end on two different vertices,
and it kept them as two edges with different ends. Past the crossing,
shape_builder then returned a line's loose end as one piece that runs
out to the line's end and back, and an extra piece of the line it
crosses, about 1.4e-6 long. In live_paint, the face around the loose
end ran out along it and back, and FaceMerger::merge of the faces
returned a second subpath along it. pieces_cut_out_of_their_inputs
fails on main for such an input.

The map now joins the ends of pieces whose middles and ends lie within
16 eps of each other. The two passes then share their ends and become
one edge. The new test is the failing input proptest saved for the
property. Lines that slope close to -1/32 in the document are nearly
level in the sweep's frame, and the change does not cover all of them.
2026-10-12 00:16:22 +07:00
Alejandro LondonoandClaude Opus 5.5 8957545c33 #893: merge Shape Builder faces in the planar map
After linesweeper 0.5 (#1028 on main), uniting the faces of a grid of
lines could split the merged strip again even with their corners shared
exactly: a sweep over seven or more faces moved shared vertices by about
1e-6 (within its eps) and the edges between faces no longer cancelled.

Faces cut by open paths now merge like closed shapes' faces already do
(FaceMerger::exact): in the planar map itself. build() is split into
map() (the graph and its faces) and build(); Map::merge traces the
boundary of the picked faces along the map's own edges, dropping the
edges between two picked faces and the loose ends inside them, so no
sweep runs on the result. FaceMerger finds each face again by a point
inside it, checks the area, and keeps the boolean union as a fallback.

Tests: the regression test merges through FaceMerger; a grid test
covers a ring with its hole, cells touching at a corner, a loose end,
and that the map path (not the fallback) produced the result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:24:44 +07:00
Alejandro LondonoandClaude Opus 5.5 cfa36fc55f #893: Shape Builder merges leave no seams on grids of lines
The planar map merged sweep endpoints within eps into one vertex id but
kept each segment's own copy of the coordinates, so faces meeting at a
vertex could disagree on that corner by ~1e-13. Uniting them then failed
to cancel their shared edge, and a merged strip came back as several
touching subpaths whose stroke drew the inner edges as leftover lines.

Pin every edge's ends to its vertex's canonical position, so faces share
their corners bit for bit. Regression test from the reporter's grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:24:44 +07:00
bflatasticandClaude Opus 5.5 17f66003cf Fix #439: Shape Builder erase mode deletes edges, shows what will go and follows Alt live
Alt already deleted regions and pieces of open lines, but erase mode had
no edges of closed shapes, no distinct pointer, highlighted regions in the
merge-mode grey, only noticed Alt on a mouse move, and an erase drag could
flip mode at release. Like the reference app's erase mode:

- pathops::shape_builder(shapes, edges) returns a BuilderArrangement; with
  `edges` it also cuts the outlines of closed (and filled open) paths
  wherever other paths meet them. pathops::cut_out removes such a piece
  from a path: a closed subpath opens there, an open one splits or
  shortens, a piece all the way round takes its subpath, and a filled open
  path's invisible closing edge never comes back as a visible segment.
  Specks where paths only touch cut nothing; a zero-length input is no
  longer the source of edges running through it. encloses_area is shared
  with the tool's closing of filled open paths.
- BuilderMap (with_edges) hit-tests edges like line pieces (nearest wins);
  an erase gesture that crosses an edge into a region deletes the region,
  not the edge (erased_edges).
- shapeBuilder.merge {erase}: deletes the edges touched within tolerance
  (from every shape along them), after the regions; returns `edges`.
  shapeBuilder.regions lists `edges` (bounds, source). One undo step.
- Shape Builder tool: Alt held switches to erase mode and back on release:
  red mesh/edge highlights of what will be deleted, a minus pointer
  (Cursor::ShapeBuilder / ShapeBuilderErase, a plus in merge mode); a drag
  keeps the mode it started in. The canvas re-hovers the tool when a
  modifier changes, so the highlight follows Alt without a mouse move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:37:35 +07:00
bflatasticandClaude Opus 5.5 28fc79d57a Fix #374: Shape Builder cuts regions with open paths and lines
The Shape Builder only used the filled areas of the selected paths, so a
stroke-only line or open path across a shape did nothing. Like the
reference app's Shape Builder, open paths now act as cutting edges:

- pathops::shape_builder: with an open path in the selection, the
  regions are the Live Paint planar map's faces (every path an edge), so a
  line across a shape splits it and lines can enclose a region of their
  own; it also returns each open path cut where other paths meet it. Closed
  shapes alone, more than SHAPE_BUILDER_MAX_SEGMENTS segments, or a map
  that can't be built fall back to the filled-area arrangement.
- Filled open paths count as closed (an invisible closing edge) where that
  encloses an area; unfilled open paths and straight lines stay edges.
- shapeBuilder.merge: merging or deleting regions on both sides of a line
  piece removes that piece (the line is split, its other pieces kept); an
  area only lines enclose takes the current fill and the front-most
  object's look. Alt (erase) within `tolerance` of a line piece deletes it.
  Returns `lines` (pieces deleted).
- shapeBuilder.regions also lists the line pieces.
- The tool shares BuilderMap/Touched with the command, highlights the line
  piece under the pointer while Alt is held and passes its tolerance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 06:55:40 +07:00
bflatasticandClaude Opus 5.5 700b6786c4 Fix #206: Live Paint treats every path as an edge, open lines included
The Live Paint Bucket now fills areas enclosed by any paths. Crossing
lines enclose faces it can fill, a line across a shape splits that shape's
face in two, and an area enclosed by paths becomes a face even when nothing
fills it (it starts unfilled). Edges are the paths cut wherever another path
meets them, keeping their stroke, so Shift-click paints each piece. Closed
shapes give the same faces as before.

Root cause: Live Paint built its faces from the Pathfinder regions, which
divide the filled areas of closed shapes. An open path counted only as the
area it would fill if closed, which for a straight line is nothing, so lines
divided nothing and the bucket found no face to fill. A new planar map in
vectorcraft-pathops (live_paint) sweeps every path as an edge: an open path
goes in traced there and back, with crossing counts that never cancel; faces
are walked on the resulting graph (holes included), and their fills and the
edges' sources are found geometrically. The sweep runs in an exactly sheared
frame, because it mishandles a horizontal line traced there and back.
Make, Merge, the bucket and Live Paint Selection all use it;
interior_point moves to vectorcraft-pathops with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 02:08:39 +07:00