@weasel-js/geom 1.9.4 → 1.10.0

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/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 { P as PolygonPath, a as Path } from '../path-Bf9KeVxC.js';
2
+ import { P as Point } from '../cubicMath-CFXbq6vp.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,4 @@ 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
+ export { type BooleanPiece, type MultiPolygon, type Pair, type PathToMultiPolygonOptions, type Polygon, type Ring, type SplitBySegmentOptions, multiPolygonToPath, pathCrop, pathCropPieces, pathDivide, pathDividePieces, pathExclude, pathIntersect, pathMerge, pathSubtract, pathToMultiPolygon, pathTrim, pathUnion, snipPathByPolyline, 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, segmentsCross, pointInPolygon } from '../chunk-WQMYOCFB.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,6 @@ 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
+ export { multiPolygonToPath, pathCrop, pathCropPieces, pathDivide, pathDividePieces, pathExclude, pathIntersect, pathMerge, pathSubtract, pathToMultiPolygon, pathTrim, pathUnion, snipPathByPolyline, splitPathByPolyline, splitPathBySegment, trianglesToPath };
962
998
  //# sourceMappingURL=index.js.map
963
999
  //# sourceMappingURL=index.js.map