@weasel-js/geom 1.9.4 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,13 @@
1
1
  # @weasel-js/geom
2
2
 
3
- Pure 2D geometry kernel for @weasel-js/core: affine, box, curve, polyline, the
4
- `Path` command stream and the math over it (builders, SVG `d` parsing, bounds,
5
- transforms, hit-testing, sampling, curve fitting), and easing curves.
6
- Dependency-free core. Subpaths:
7
-
8
- - `./curves` — alternate curve representations (cubic and quadratic Bezier, NURBS, Spiro)
9
- - `./tessellate` — fill tessellation, polylines and trimming; needs the optional peer `earcut`
10
- - `./booleans` — polygon booleans and path splitting; needs the optional peer `polygon-clipping`
11
- - `./3d` — vectors, quaternions, 4x4 matrices and ray intersection
12
- - `./nd` — the port-curve rule (`portControls`, `portCurvePoints`, `PORT_REACH`) over plain number arrays, for any dimension
3
+ The 2D geometry kernel the rest of weasel is built on: affine transforms, boxes,
4
+ Bezier math, the `Path` shape type and everything that operates on it, and
5
+ easing curves. It is plain functions over numbers and typed arrays, with no
6
+ DOM, no React, and no runtime dependencies in the root entry.
13
7
 
14
8
  Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
15
9
  scene-graph canvas kit for React. See the
16
- [API reference](https://orochi235.github.io/weasel/api/).
10
+ [API reference](https://orochi235.github.io/weasel/#pkg/geom).
17
11
 
18
12
  ## Install
19
13
 
@@ -21,13 +15,135 @@ scene-graph canvas kit for React. See the
21
15
  npm install @weasel-js/geom
22
16
  ```
23
17
 
24
- ## Usage
18
+ The `./tessellate` entry uses the optional peer `earcut`, and `./booleans` uses
19
+ the optional peer `polygon-clipping`. Install a peer only if you import the
20
+ entry that needs it.
21
+
22
+ ## Value types
23
+
24
+ Most functions take loose `x, y` numbers rather than point objects, so a hot
25
+ loop allocates nothing. The types that do exist are small:
26
+
27
+ | Type | Shape | Notes |
28
+ |---|---|---|
29
+ | `Mat3` | `[a, b, c, d, e, f]` | A 2D affine in canvas and `DOMMatrix` order: `x' = a·x + c·y + e`, `y' = b·x + d·y + f`. |
30
+ | `Box` | `[minX, minY, maxX, maxY]` | An axis-aligned box as a tuple. |
31
+ | `Rect` | `{ x, y, width, height }` | The same box in object form. |
32
+ | `View` | `{ x, y, scale: { x, y } }` | A viewport: the world point drawn at the canvas's top-left corner, and pixels per world unit on each axis. |
33
+ | `GlMat3` | nine-element `Float64Array` or `Float32Array` | The same affine laid out column-major for a WebGL `uniformMatrix3fv` upload. |
34
+ | `Path` | `RectPath \| PolygonPath` | A shape. See below. |
35
+
36
+ `translate`, `scale`, `rotate`, `rotateAboutPoint`, and `boxToBox` build a
37
+ `Mat3`. `multiply(m, n)` composes two, applying `n` first. `invert` returns
38
+ `null` for a singular matrix, and `applyToPoint` maps one point.
39
+
40
+ ```ts
41
+ import { applyToPoint, invert, multiply, scale, translate } from '@weasel-js/geom';
42
+
43
+ // World to screen for a view at (10, 20), zoomed 2x: translate, then scale.
44
+ const worldToScreen = multiply(scale(2, 2), translate(-10, -20));
45
+ applyToPoint(worldToScreen, 15, 25); // [10, 10]
46
+
47
+ const screenToWorld = invert(worldToScreen); // Mat3 | null
48
+ ```
49
+
50
+ The `mat3` namespace holds the `GlMat3` operations (`mat3.fromAffine`,
51
+ `mat3.toAffine`, `mat3.multiply`, `mat3.toUniform`, and others), and
52
+ `viewToMat3(view)` builds the world-to-screen `GlMat3` for a `View`.
53
+
54
+ ## Paths
55
+
56
+ A `Path` is the shape type every weasel package shares. It is either:
57
+
58
+ - A `RectPath`, `{ kind: 'rect', x, y, width, height }`, for the common case of
59
+ an axis-aligned rectangle.
60
+ - A `PolygonPath`, an SVG-style command stream: command codes (`M`, `L`, `C`,
61
+ `Q`, `Z`) in a `Uint8Array`, their coordinates in a parallel `Float32Array`,
62
+ and a `fillRule`. It covers polylines, polygons, Bezier outlines, and shapes
63
+ with holes.
64
+
65
+ What operates on a path:
66
+
67
+ - **Building.** `PathBuilder` for move, line, curve, and close calls;
68
+ `rectPath`, `polygonFromPoints`, `polylineFromPoints`, `ellipsePath`,
69
+ `regularPolygonPath`, `starPath`, `linePath`, `circlePath`, and
70
+ `roundRectPath` for common shapes; `pathFromD` to parse an SVG `d` string;
71
+ `schneiderFit` to fit cubic curves through sampled points.
72
+ - **Measuring.** `boundsOfPath`, `unionBoundsPath`, `pathSignedArea`,
73
+ `pointAlongPath`.
74
+ - **Transforming.** `translatePath`, `transformPath`, `scalePathToBounds`,
75
+ `reversePath`.
76
+ - **Hit-testing.** `pointInPath` for the filled region, `strokeHitTest` for
77
+ the outline, `pathContainsRect`, `pathIntersectsRect`, and the polygon
78
+ variants for marquee and lasso selection, and `nearestOnPath` and
79
+ `pathDistanceToPoint` for the closest point.
80
+ - **Editing.** `pathToAnchors` and `anchorsToPath` convert between the command
81
+ stream and a list of anchors with control handles, the form a pen tool
82
+ edits. `splitSubpaths` and `splitByClosure` take a compound path apart.
83
+ - **Walking the stream.** `forEachSegment` visits each command with its
84
+ coordinate offset and the pen position before it.
85
+
86
+ ```ts
87
+ import {
88
+ boundsOfPath, pathFromD, pointInPath, rotateAboutPoint, strokeHitTest, transformPath,
89
+ } from '@weasel-js/geom';
90
+
91
+ const triangle = pathFromD('M0 0 L100 0 L50 80 Z');
92
+
93
+ boundsOfPath(triangle); // { kind: 'rect', x: 0, y: 0, width: 100, height: 80 }
94
+ pointInPath(triangle, 50, 20); // true: inside the fill
95
+ strokeHitTest(triangle, 50, 3, 4); // true: within 4 units of the outline
96
+
97
+ const turned = transformPath(triangle, rotateAboutPoint(50, 40, Math.PI / 2));
98
+ ```
99
+
100
+ Below the path level, the curve functions work on one segment at a time:
101
+ `cubicEvalAt`, `splitCubicAt`, `cubicBounds`, `flattenCubic`,
102
+ `nearestOnCubic`, and their quadratic and line counterparts.
103
+
104
+ ## Easings
105
+
106
+ An `EasingFn` maps progress `t` in 0..1 to eased progress. The package exports
107
+ the standard families by name (`easeOutCubic`, `easeInOutSine`,
108
+ `easeOutBounce`, and so on), the same set keyed by name in `EASINGS`, and
109
+ `cubicBezierEasing(x1, y1, x2, y2)` for CSS-style control points.
110
+
111
+ `EasingSpec` is an easing as a value that can be stored or serialized: a
112
+ function, a name, or `{ bezier: [x1, y1, x2, y2] }`. `resolveEasing` turns any
113
+ of them into the function.
114
+
115
+ ```ts
116
+ import { resolveEasing } from '@weasel-js/geom';
117
+
118
+ const ease = resolveEasing('easeOutCubic');
119
+ ease(0.5); // 0.875
120
+
121
+ const custom = resolveEasing({ bezier: [0.4, 0, 0.2, 1] });
122
+ ```
123
+
124
+ ## Subpath entries
125
+
126
+ | Entry | What it adds | Optional peer |
127
+ |---|---|---|
128
+ | `@weasel-js/geom/curves` | Curve representations that read one shared anchor list and each produce a `PolygonPath`: `bezierCubic`, `bezierQuadratic`, `nurbs`, and `spiro`, also keyed by kind in `CURVE_REPS`. Each can evaluate a point and the curvature at a parameter. | none |
129
+ | `@weasel-js/geom/tessellate` | `tessellate` turns a path's fill into a triangle `Mesh` (vertices and indices). `extractPolylines` flattens each contour to straight segments, and `trimPolyline` shortens one from either end. | `earcut`, used by `tessellate` |
130
+ | `@weasel-js/geom/booleans` | Boolean operations on path fills: `pathUnion`, `pathIntersect`, `pathSubtract`, `pathExclude`, `pathDivide`, `pathCrop`, `pathTrim`, and `pathMerge`. Also cutting a path with a line or polyline: `splitPathBySegment`, `splitPathByPolyline`, `snipPathByPolyline`. | `polygon-clipping` |
131
+ | `@weasel-js/geom/3d` | The same kind of math one dimension up: `Vec3` and quaternion helpers, `Mat4` with `compose`, `perspective`, and `lookAt`, and ray intersection against planes and axis-aligned boxes. | none |
132
+ | `@weasel-js/geom/nd` | The port-curve rule over plain number arrays of any length: `portControls` returns the two cubic control points that make a curve leave one port along its normal and arrive against the next port's normal, and `portCurvePoints` samples that curve. | none |
133
+
134
+ The `./3d` entry reuses names from the root (`dot`, `scale`, `invert`,
135
+ `identity`), so import from one or the other, or alias at the import.
136
+
137
+ Boolean operations work on what a fill covers, so curves are flattened first
138
+ and the result is a `PolygonPath` of straight segments.
25
139
 
26
140
  ```ts
27
- import { /* … */ } from '@weasel-js/geom';
28
- import { /* … */ } from '@weasel-js/geom/booleans';
29
- import { /* … */ } from '@weasel-js/geom/curves';
30
- import { /* … */ } from '@weasel-js/geom/tessellate';
141
+ import { ellipsePath, rectPath } from '@weasel-js/geom';
142
+ import { pathSubtract } from '@weasel-js/geom/booleans';
143
+
144
+ const plate = rectPath(0, 0, 100, 60);
145
+ const hole = ellipsePath({ x: 30, y: 10, width: 40, height: 40 });
146
+ const cut = pathSubtract(plate, hole); // the plate with the ellipse removed
31
147
  ```
32
148
 
33
149
  ## License
package/dist/3d/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { PORT_REACH, portControls, portCurvePoints, cubicAt } from '../chunk-YQADYJJ7.js';
2
2
  export { PORT_REACH } from '../chunk-YQADYJJ7.js';
3
- import { SINGULAR_RATIO } from '../chunk-N5SYTHMR.js';
3
+ import { SINGULAR_RATIO } from '../chunk-755Y4YZS.js';
4
4
 
5
5
  // src/3d/vec3.ts
6
6
  function add(a, b) {
@@ -1,5 +1,5 @@
1
- import { P as Path, a as PolygonPath } from '../path-D0CoudGM.js';
2
- import { P as Point } from '../cubicMath-CpCvuEl8.js';
1
+ import { a as PolygonPath, P as Path } from '../path-ljHxobyu.js';
2
+ import { P as Point } from '../cubicMath-BW-HJrCE.js';
3
3
 
4
4
  /** Union of N paths. Commutative. Returns an empty path if all inputs are empty. */
5
5
  declare function pathUnion(...paths: Path[]): PolygonPath;
@@ -9,30 +9,48 @@ declare function pathIntersect(...paths: Path[]): PolygonPath;
9
9
  declare function pathSubtract(a: Path, b: Path): PolygonPath;
10
10
  /** Symmetric difference (XOR) of N paths. Commutative. */
11
11
  declare function pathExclude(...paths: Path[]): PolygonPath;
12
+ /** One path of a result that keeps its pieces apart, and the inputs it came
13
+ * from: their indices in the call, ascending, so the last is the topmost of
14
+ * inputs given back to front. */
15
+ interface BooleanPiece {
16
+ path: PolygonPath;
17
+ sources: number[];
18
+ }
19
+ /** {@link pathDividePieces} as bare paths. */
20
+ declare function pathDivide(...paths: Path[]): PolygonPath[];
21
+ /** {@link pathCropPieces} as bare paths. */
22
+ declare function pathCrop(...paths: Path[]): PolygonPath[];
12
23
  /**
13
24
  * Fracture N paths along every intersection into the maximal set of
14
- * non-overlapping regions. Returns one polygon path per region.
25
+ * non-overlapping regions. Returns one piece per region, naming every input
26
+ * that covers it. For N=2 these are the three Illustrator "Divide" outputs
27
+ * (A−B, B−A, A∩B).
15
28
  *
16
- * Algorithm: for every non-empty subset S ⊆ {A1..AN}, emit the region
17
- * `(∩ S) − (∪ complement)` — points covered by exactly the members of S
18
- * and no others. By construction the emitted regions are pairwise disjoint
19
- * and their union equals `pathUnion(A1..AN)`.
20
- *
21
- * For N=2 this collapses to the three Illustrator "Divide" outputs
22
- * (A−B, B−A, A∩B). For N=3 up to 7 regions are emitted. The 2^N − 1
23
- * subset count limits this to small N in practice; passing more than ~8
24
- * inputs is a misuse — the polygon-clipping kernel dominates anyway.
29
+ * One region per non-empty subset of the inputs, so the 2^N − 1 subset count
30
+ * limits this to small N; passing more than ~8 inputs is a misuse.
25
31
  */
26
- declare function pathDivide(...paths: Path[]): PolygonPath[];
32
+ declare function pathDividePieces(...paths: Path[]): BooleanPiece[];
27
33
  /**
28
- * Clip each non-topmost path to the topmost path (Illustrator "Crop").
29
- * Returns `N - 1` results, one per source-below-top, in input order.
30
- * Discards the topmost path itself — it acts purely as the clipping mask.
31
- *
32
- * Empty results (a source that lies entirely outside the mask) are
33
- * filtered. With `<2` inputs the result is empty.
34
+ * Remove from each path what the paths after it cover (Illustrator "Trim"):
35
+ * given back to front, every piece is the part of its input left showing.
36
+ * One piece per input in input order, less the inputs wholly hidden.
34
37
  */
35
- declare function pathCrop(...paths: Path[]): PolygonPath[];
38
+ declare function pathTrim(...paths: Path[]): BooleanPiece[];
39
+ /**
40
+ * {@link pathTrim}, then unite the pieces of the inputs `keyOf` gives the same
41
+ * key (Illustrator "Merge", keyed by paint). Keys compare with `===`. A piece
42
+ * names every input left showing in it, and the pieces are ordered by their
43
+ * topmost one. Pieces that share a key and do not touch come back as one path
44
+ * of several contours.
45
+ */
46
+ declare function pathMerge(paths: readonly Path[], keyOf: (index: number) => unknown): BooleanPiece[];
47
+ /**
48
+ * Clip each non-topmost path to the topmost path (Illustrator "Crop"), which
49
+ * acts purely as the mask and is discarded. Returns up to `N - 1` pieces in
50
+ * input order, each naming the one input it was clipped from; a source
51
+ * entirely outside the mask is dropped.
52
+ */
53
+ declare function pathCropPieces(...paths: Path[]): BooleanPiece[];
36
54
 
37
55
  /**
38
56
  * Union the triangles of an indexed mesh (`vertices` as x, y pairs; `indices`
@@ -74,16 +92,13 @@ interface SplitBySegmentOptions {
74
92
  */
75
93
  declare function splitPathBySegment(path: Path, a: Point, b: Point, opts?: SplitBySegmentOptions): Path[] | null;
76
94
  /**
77
- * Split `path` along the open polyline `cut` into closed pieces (Knife). A
78
- * freehand cut is a polyline through its samples.
95
+ * Split `path` along the open polyline `cut` into closed pieces (Knife).
79
96
  *
80
97
  * Only chords the cut crosses end to end are cut: a stretch of the cut that
81
98
  * starts or stops inside the fill cuts nothing, and nothing beyond the cut's
82
- * ends is touched, so a concave shape splits exactly where the stroke crosses
83
- * it. Curves are split at the crossing parameters by de Casteljau
84
- * subdivision, so every piece traces the original boundary exactly and keeps
85
- * its quadratic and cubic segments; the new edges follow the cut as straight
86
- * lines, bends included.
99
+ * ends is touched. Curves are split at the crossing parameters by de Casteljau
100
+ * subdivision, so every piece keeps its quadratic and cubic segments; the new
101
+ * edges follow the cut as straight lines, bends included.
87
102
  *
88
103
  * A loop the cut draws inside the fill, where it crosses its own track, cuts
89
104
  * out the region it encloses: that region is returned as its own piece, with
@@ -91,7 +106,7 @@ declare function splitPathBySegment(path: Path, a: Point, b: Point, opts?: Split
91
106
  * running through a loop leaves the loop uncut. A closed cut (its last point
92
107
  * equal to its first) is one such loop or, where it crosses the boundary, is
93
108
  * cut as the chords it makes. Chords that cross each other are cut one after
94
- * another, so a cross-hatch yields every piece.
109
+ * another.
95
110
  *
96
111
  * Each connected piece the cut produces is returned as its own path, holes
97
112
  * included. Contours the cut misses entirely are returned together as one
@@ -122,4 +137,23 @@ declare function splitPathByPolyline(path: Path, cut: readonly Point[], opts?: S
122
137
  */
123
138
  declare function snipPathByPolyline(path: Path, cut: readonly Point[]): Path[] | null;
124
139
 
125
- export { type MultiPolygon, type Pair, type PathToMultiPolygonOptions, type Polygon, type Ring, type SplitBySegmentOptions, multiPolygonToPath, pathCrop, pathDivide, pathExclude, pathIntersect, pathSubtract, pathToMultiPolygon, pathUnion, snipPathByPolyline, splitPathByPolyline, splitPathBySegment, trianglesToPath };
140
+ /** Options for {@link snipPathsAtCrossings}. */
141
+ interface SnipAtCrossingsOptions {
142
+ /** How closely a cutting path's curves are followed when they are flattened
143
+ * to find where they cross. Default 0.05. */
144
+ flattenTolerance?: number;
145
+ }
146
+ /**
147
+ * Cut the stroke of every path in `paths` wherever another of them crosses
148
+ * it, as {@link snipPathByPolyline} cuts along one polyline: an open subpath
149
+ * becomes open runs between crossings, and a closed one is opened at them.
150
+ * A path is not cut where it crosses itself.
151
+ *
152
+ * Answers one entry per path, in order: its pieces, or `null` when no other
153
+ * path crosses it. The cutting paths' curves are flattened, so a crossing on
154
+ * a curve is found to within `flattenTolerance`; the pieces keep their own
155
+ * segment types.
156
+ */
157
+ declare function snipPathsAtCrossings(paths: readonly Path[], opts?: SnipAtCrossingsOptions): (Path[] | null)[];
158
+
159
+ export { type BooleanPiece, type MultiPolygon, type Pair, type PathToMultiPolygonOptions, type Polygon, type Ring, type SnipAtCrossingsOptions, type SplitBySegmentOptions, multiPolygonToPath, pathCrop, pathCropPieces, pathDivide, pathDividePieces, pathExclude, pathIntersect, pathMerge, pathSubtract, pathToMultiPolygon, pathTrim, pathUnion, snipPathByPolyline, snipPathsAtCrossings, splitPathByPolyline, splitPathBySegment, trianglesToPath };
@@ -1,7 +1,7 @@
1
1
  import { elevateQuadraticToCubic, splitQuadraticAt, splitCubicAt } from '../chunk-URQMDR6Q.js';
2
- import { DEFAULT_FLATTEN_TOLERANCE, flattenCubic, segmentsCross, pointInPolygon } from '../chunk-X6GJRIQD.js';
3
- import { EPS } from '../chunk-N5SYTHMR.js';
4
- import { PATH_Z, PATH_Q, PATH_C, PATH_L, PATH_M } from '../chunk-E4TJRGJD.js';
2
+ import { DEFAULT_FLATTEN_TOLERANCE, fillsOpenSubpaths, flattenCubic, flattenQuadratic, segmentsCross, pointInPolygon } from '../chunk-IDXY2NCA.js';
3
+ import { EPS } from '../chunk-755Y4YZS.js';
4
+ import { PATH_Z, PATH_Q, PATH_C, PATH_L, PATH_M } from '../chunk-OXH3NOXC.js';
5
5
  import polygonClipping from 'polygon-clipping';
6
6
 
7
7
  // src/booleans/adapter.ts
@@ -24,7 +24,9 @@ function pathToMultiPolygon(path, opts = {}) {
24
24
  let startX = 0, startY = 0;
25
25
  let ci = 0;
26
26
  const { commands, coords } = path;
27
- const finalizeCurrent = () => {
27
+ const fillsOpen = fillsOpenSubpaths(path);
28
+ const finalizeCurrent = (closed = fillsOpen) => {
29
+ if (!closed) current = null;
28
30
  if (!current || current.length === 0) return;
29
31
  const first = current[0];
30
32
  const last = current[current.length - 1];
@@ -82,7 +84,7 @@ function pathToMultiPolygon(path, opts = {}) {
82
84
  break;
83
85
  }
84
86
  case PATH_Z: {
85
- finalizeCurrent();
87
+ finalizeCurrent(true);
86
88
  cx = startX;
87
89
  cy = startY;
88
90
  break;
@@ -235,9 +237,15 @@ function pathExclude(...paths) {
235
237
  return multiPolygonToPath(polygonClipping.xor(head, ...rest));
236
238
  }
237
239
  function pathDivide(...paths) {
240
+ return pathDividePieces(...paths).map((p) => p.path);
241
+ }
242
+ function pathCrop(...paths) {
243
+ return pathCropPieces(...paths).map((p) => p.path);
244
+ }
245
+ function pathDividePieces(...paths) {
238
246
  if (paths.length === 0) return [];
239
247
  if (paths.length === 1) {
240
- return [multiPolygonToPath(pathToMultiPolygon(paths[0]))];
248
+ return [{ path: multiPolygonToPath(pathToMultiPolygon(paths[0])), sources: [0] }];
241
249
  }
242
250
  const mps = paths.map((p) => pathToMultiPolygon(p));
243
251
  const n = mps.length;
@@ -248,7 +256,9 @@ function pathDivide(...paths) {
248
256
  for (const mask of subsets) {
249
257
  const inside = [];
250
258
  const outside = [];
259
+ const sources = [];
251
260
  for (let i = 0; i < n; i++) {
261
+ if (mask & 1 << i) sources.push(i);
252
262
  (mask & 1 << i ? inside : outside).push(mps[i]);
253
263
  }
254
264
  const [insideHead, ...insideRest] = inside;
@@ -260,10 +270,35 @@ function pathDivide(...paths) {
260
270
  region = polygonClipping.difference(region, outsideUnion);
261
271
  if (region.length === 0) continue;
262
272
  }
263
- out.push(multiPolygonToPath(region));
273
+ out.push({ path: multiPolygonToPath(region), sources });
274
+ }
275
+ return out;
276
+ }
277
+ function pathTrim(...paths) {
278
+ const mps = paths.map((p) => pathToMultiPolygon(p));
279
+ const out = [];
280
+ let cover = null;
281
+ for (let i = mps.length - 1; i >= 0; i--) {
282
+ const shown = cover ? polygonClipping.difference(mps[i], cover) : mps[i];
283
+ if (shown.length > 0) out.unshift({ path: multiPolygonToPath(shown), sources: [i] });
284
+ cover = cover ? polygonClipping.union(cover, mps[i]) : mps[i];
264
285
  }
265
286
  return out;
266
287
  }
288
+ function pathMerge(paths, keyOf) {
289
+ const groups = /* @__PURE__ */ new Map();
290
+ for (const piece of pathTrim(...paths)) {
291
+ const key = keyOf(piece.sources[0]);
292
+ const group = groups.get(key);
293
+ if (group) group.push(piece);
294
+ else groups.set(key, [piece]);
295
+ }
296
+ const top = (p) => p.sources[p.sources.length - 1];
297
+ return [...groups.values()].map((g) => g.length === 1 ? g[0] : {
298
+ path: pathUnion(...g.map((p) => p.path)),
299
+ sources: g.map((p) => p.sources[0])
300
+ }).sort((a, b) => top(a) - top(b));
301
+ }
267
302
  function popcount(n) {
268
303
  let c = 0;
269
304
  while (n) {
@@ -272,14 +307,14 @@ function popcount(n) {
272
307
  }
273
308
  return c;
274
309
  }
275
- function pathCrop(...paths) {
310
+ function pathCropPieces(...paths) {
276
311
  if (paths.length < 2) return [];
277
312
  const mps = paths.map((p) => pathToMultiPolygon(p));
278
313
  const mask = mps[mps.length - 1];
279
314
  const out = [];
280
315
  for (let i = 0; i < mps.length - 1; i++) {
281
316
  const clipped = polygonClipping.intersection(mps[i], mask);
282
- if (clipped.length > 0) out.push(multiPolygonToPath(clipped));
317
+ if (clipped.length > 0) out.push({ path: multiPolygonToPath(clipped), sources: [i] });
283
318
  }
284
319
  return out;
285
320
  }
@@ -475,7 +510,8 @@ function crossingsOnSegment(boundaries, cut, k, out) {
475
510
 
476
511
  // src/booleans/contours.ts
477
512
  function parseContours(path) {
478
- return parseSubpaths(path).map((s) => s.segs);
513
+ const fillsOpen = path.kind === "rect" || fillsOpenSubpaths(path);
514
+ return parseSubpaths(path).filter((s) => s.closed || fillsOpen).map((s) => s.segs);
479
515
  }
480
516
  function parseSubpaths(path, implicitClose = true) {
481
517
  if (path.kind === "rect") {
@@ -958,6 +994,45 @@ function snipStations(sub, cut) {
958
994
  return groups.filter((g) => g.length % 2 === 1).map((g) => g[0]).filter((st) => sub.closed || !same(st.pt, first) && !same(st.pt, end));
959
995
  }
960
996
 
961
- export { multiPolygonToPath, pathCrop, pathDivide, pathExclude, pathIntersect, pathSubtract, pathToMultiPolygon, pathUnion, snipPathByPolyline, splitPathByPolyline, splitPathBySegment, trianglesToPath };
997
+ // src/booleans/snipAtCrossings.ts
998
+ function snipPathsAtCrossings(paths, opts = {}) {
999
+ const tol = opts.flattenTolerance ?? 0.05;
1000
+ const cuts = paths.map((p) => cutLinesOf(p, tol));
1001
+ return paths.map((path, i) => {
1002
+ let pieces = [path];
1003
+ let cut = false;
1004
+ cuts.forEach((lines, j) => {
1005
+ if (j === i) return;
1006
+ for (const line of lines) {
1007
+ pieces = pieces.flatMap((piece) => {
1008
+ const snipped = snipPathByPolyline(piece, line);
1009
+ if (!snipped) return [piece];
1010
+ cut = true;
1011
+ return snipped;
1012
+ });
1013
+ }
1014
+ });
1015
+ return cut ? pieces : null;
1016
+ });
1017
+ }
1018
+ function cutLinesOf(path, tol) {
1019
+ return parseSubpaths(path, false).map(({ segs, closed }) => {
1020
+ const flat = [segs[0][0].x, segs[0][0].y];
1021
+ for (const s of segs) {
1022
+ if (s.length === 2) flat.push(s[1].x, s[1].y);
1023
+ else if (s.length === 3) flattenQuadratic(s[0].x, s[0].y, s[1].x, s[1].y, s[2].x, s[2].y, tol, flat);
1024
+ else flattenCubic(s[0].x, s[0].y, s[1].x, s[1].y, s[2].x, s[2].y, s[3].x, s[3].y, tol, flat);
1025
+ }
1026
+ const pts = [];
1027
+ for (let k = 0; k < flat.length; k += 2) pts.push({ x: flat[k], y: flat[k + 1] });
1028
+ if (closed) {
1029
+ const end = pts[pts.length - 1];
1030
+ if (end.x !== pts[0].x || end.y !== pts[0].y) pts.push({ ...pts[0] });
1031
+ }
1032
+ return pts;
1033
+ });
1034
+ }
1035
+
1036
+ export { multiPolygonToPath, pathCrop, pathCropPieces, pathDivide, pathDividePieces, pathExclude, pathIntersect, pathMerge, pathSubtract, pathToMultiPolygon, pathTrim, pathUnion, snipPathByPolyline, snipPathsAtCrossings, splitPathByPolyline, splitPathBySegment, trianglesToPath };
962
1037
  //# sourceMappingURL=index.js.map
963
1038
  //# sourceMappingURL=index.js.map