@weasel-js/geom 1.7.0 → 1.7.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 +11 -1
- package/dist/3d/index.d.ts +17 -0
- package/dist/3d/index.js +3 -2
- package/dist/3d/index.js.map +1 -1
- package/dist/booleans/index.d.ts +90 -44
- package/dist/booleans/index.js +645 -5
- package/dist/booleans/index.js.map +1 -1
- package/dist/chunk-6T5I3PR5.js +172 -0
- package/dist/chunk-6T5I3PR5.js.map +1 -0
- package/dist/chunk-7ROQRYOA.js +29 -0
- package/dist/chunk-7ROQRYOA.js.map +1 -0
- package/dist/chunk-HEZTRG2M.js +55 -0
- package/dist/chunk-HEZTRG2M.js.map +1 -0
- package/dist/chunk-HPVINMOM.js +34 -0
- package/dist/chunk-HPVINMOM.js.map +1 -0
- package/dist/{chunk-36DHVSOV.js → chunk-RQYCDPU5.js} +36 -134
- package/dist/chunk-RQYCDPU5.js.map +1 -0
- package/dist/{chunk-3GFNUO4N.js → chunk-TXEGHCJ4.js} +3 -29
- package/dist/chunk-TXEGHCJ4.js.map +1 -0
- package/dist/chunk-URQMDR6Q.js +80 -0
- package/dist/chunk-URQMDR6Q.js.map +1 -0
- package/dist/cubicMath-CpCvuEl8.d.ts +37 -0
- package/dist/curves/index.d.ts +84 -0
- package/dist/curves/index.js +513 -0
- package/dist/curves/index.js.map +1 -0
- package/dist/index.d.ts +539 -2
- package/dist/index.js +1550 -60
- package/dist/index.js.map +1 -1
- package/dist/path-D0CoudGM.d.ts +45 -0
- package/dist/tessellate/index.d.ts +87 -0
- package/dist/tessellate/index.js +316 -0
- package/dist/tessellate/index.js.map +1 -0
- package/package.json +15 -2
- package/dist/chunk-36DHVSOV.js.map +0 -1
- package/dist/chunk-3GFNUO4N.js.map +0 -1
package/dist/booleans/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { elevateQuadraticToCubic, splitQuadraticAt, splitCubicAt } from '../chunk-URQMDR6Q.js';
|
|
2
|
+
import { DEFAULT_FLATTEN_TOLERANCE, flattenCubic, segmentsCross, pointInPolygon } from '../chunk-RQYCDPU5.js';
|
|
3
|
+
import '../chunk-7ROQRYOA.js';
|
|
4
|
+
import { PATH_Z, PATH_Q, PATH_C, PATH_L, PATH_M } from '../chunk-HEZTRG2M.js';
|
|
2
5
|
import polygonClipping from 'polygon-clipping';
|
|
3
6
|
|
|
4
7
|
// src/booleans/adapter.ts
|
|
5
|
-
var DEFAULT_FLATTEN_TOLERANCE = 0.5;
|
|
6
8
|
function pathToMultiPolygon(path, opts = {}) {
|
|
7
9
|
if (path.kind === "rect") {
|
|
8
10
|
const { x, y, width, height } = path;
|
|
@@ -83,7 +85,7 @@ function pathToMultiPolygon(path, opts = {}) {
|
|
|
83
85
|
}
|
|
84
86
|
finalizeCurrent();
|
|
85
87
|
if (rings.length === 0) return [];
|
|
86
|
-
return groupRings(rings, path.fillRule
|
|
88
|
+
return groupRings(rings, path.fillRule);
|
|
87
89
|
}
|
|
88
90
|
function ringSignedArea(ring) {
|
|
89
91
|
let a = 0;
|
|
@@ -202,7 +204,7 @@ function multiPolygonToPath(mp) {
|
|
|
202
204
|
return { kind: "polygon", commands, coords, fillRule: "nonzero" };
|
|
203
205
|
}
|
|
204
206
|
|
|
205
|
-
// src/booleans/
|
|
207
|
+
// src/booleans/ops.ts
|
|
206
208
|
function pathUnion(...paths) {
|
|
207
209
|
if (paths.length === 0) return multiPolygonToPath([]);
|
|
208
210
|
const mps = paths.map((p) => pathToMultiPolygon(p));
|
|
@@ -274,6 +276,644 @@ function pathCrop(...paths) {
|
|
|
274
276
|
return out;
|
|
275
277
|
}
|
|
276
278
|
|
|
277
|
-
|
|
279
|
+
// src/booleans/splitBySegment.ts
|
|
280
|
+
function splitPathBySegment(path, a, b, opts = {}) {
|
|
281
|
+
return splitPathByPolyline(path, [a, b], opts);
|
|
282
|
+
}
|
|
283
|
+
function splitPathByPolyline(path, cut, opts = {}) {
|
|
284
|
+
let pts = distinctPoints(cut);
|
|
285
|
+
if (pts.length < 2) return null;
|
|
286
|
+
const outRule = fillRuleOf(path);
|
|
287
|
+
const boundaries = resolveBoundaries(path, opts);
|
|
288
|
+
if (boundaries.length === 0) return null;
|
|
289
|
+
pts = openClosedCut(pts, boundaries);
|
|
290
|
+
const loops = interiorLoops(pts, boundaries);
|
|
291
|
+
const pieces = knife(path, pts, opts, pts.length);
|
|
292
|
+
if (loops.length === 0) return pieces;
|
|
293
|
+
const regions = pieces ? pieces.map((p) => orientBoundaries(parseContours(p), outRule)) : [boundaries];
|
|
294
|
+
if (!cutOutLoops(regions, loops)) return pieces;
|
|
295
|
+
return regions.map((r) => toPath(r, outRule));
|
|
296
|
+
}
|
|
297
|
+
var fillRuleOf = (path) => path.kind === "polygon" ? path.fillRule : "nonzero";
|
|
298
|
+
function resolveBoundaries(path, opts) {
|
|
299
|
+
const contours = parseContours(path);
|
|
300
|
+
if (contoursIntersect(contours, opts.flattenTolerance ?? 0.5)) {
|
|
301
|
+
return orientBoundaries(parseContours(pathUnion(path)), "nonzero");
|
|
302
|
+
}
|
|
303
|
+
return orientBoundaries(contours, fillRuleOf(path));
|
|
304
|
+
}
|
|
305
|
+
function knife(path, cut, opts, budget) {
|
|
306
|
+
const outRule = fillRuleOf(path);
|
|
307
|
+
const boundaries = resolveBoundaries(path, opts);
|
|
308
|
+
if (boundaries.length === 0) return null;
|
|
309
|
+
const crossings = findCrossings(boundaries, cut);
|
|
310
|
+
const partner = /* @__PURE__ */ new Map();
|
|
311
|
+
const chord = /* @__PURE__ */ new Map();
|
|
312
|
+
const accepted = [];
|
|
313
|
+
let deferred = false;
|
|
314
|
+
for (let k = 0; k + 1 < crossings.length; k++) {
|
|
315
|
+
const c0 = crossings[k], c1 = crossings[k + 1];
|
|
316
|
+
if (!c0.enters || c1.enters || c1.s - c0.s <= 1e-9) continue;
|
|
317
|
+
const run = splitLoops(cutBetween(cut, c0, c1)).spine;
|
|
318
|
+
if (accepted.some((other) => polylinesCross(run, other))) {
|
|
319
|
+
deferred = true;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
accepted.push(run);
|
|
323
|
+
partner.set(c0, c1);
|
|
324
|
+
partner.set(c1, c0);
|
|
325
|
+
chord.set(c0, run);
|
|
326
|
+
chord.set(c1, run.slice().reverse());
|
|
327
|
+
}
|
|
328
|
+
if (partner.size === 0) return null;
|
|
329
|
+
const { loops, untouched } = traceLoops(boundaries, partner, chord);
|
|
330
|
+
const traced = assembleRegions(loops, untouched.filter((c) => signedArea(c) < 0));
|
|
331
|
+
const islands = untouched.filter((c) => signedArea(c) > 0);
|
|
332
|
+
const regions = traced.filter((r) => Math.abs(regionArea(r)) > 1e-6);
|
|
333
|
+
if (islands.length > 0) {
|
|
334
|
+
const leftover = untouched.filter((c) => !traced.some((r) => r.includes(c)));
|
|
335
|
+
regions.push(leftover);
|
|
336
|
+
}
|
|
337
|
+
const pieces = regions.map((r) => toPath(r, outRule));
|
|
338
|
+
if (!deferred || budget <= 1) return pieces;
|
|
339
|
+
return pieces.flatMap((p) => knife(p, cut, opts, budget - 1) ?? [p]);
|
|
340
|
+
}
|
|
341
|
+
function snipPathByPolyline(path, cut) {
|
|
342
|
+
const pts = distinctPoints(cut);
|
|
343
|
+
if (pts.length < 2) return null;
|
|
344
|
+
const rule = path.kind === "polygon" ? path.fillRule : "nonzero";
|
|
345
|
+
const runs = [];
|
|
346
|
+
const missed = [];
|
|
347
|
+
for (const sub of parseSubpaths(path, false)) {
|
|
348
|
+
const stops = snipStations(sub, pts);
|
|
349
|
+
if (stops.length === 0) {
|
|
350
|
+
missed.push(sub);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const segs = sub.segs;
|
|
354
|
+
if (sub.closed) {
|
|
355
|
+
for (let i = 0; i < stops.length; i++) {
|
|
356
|
+
runs.push(arcBetween(segs, stops[i], stops[(i + 1) % stops.length], stops.length === 1));
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
const last = segs[segs.length - 1];
|
|
360
|
+
const all = [
|
|
361
|
+
{ seg: 0, t: 0, pt: segs[0][0] },
|
|
362
|
+
...stops,
|
|
363
|
+
{ seg: segs.length - 1, t: 1, pt: last[last.length - 1] }
|
|
364
|
+
];
|
|
365
|
+
for (let i = 0; i + 1 < all.length; i++) runs.push(arcBetween(segs, all[i], all[i + 1], false));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (runs.length === 0) return null;
|
|
369
|
+
const pieces = runs.filter((r) => r.length > 0).map((r) => writePath([{ segs: r, closed: false }], rule));
|
|
370
|
+
if (missed.length > 0) pieces.push(writePath(missed, rule));
|
|
371
|
+
return pieces;
|
|
372
|
+
}
|
|
373
|
+
function snipStations(sub, cut) {
|
|
374
|
+
const raw = [];
|
|
375
|
+
sub.segs.forEach((seg, si) => {
|
|
376
|
+
for (let k = 0; k + 1 < cut.length; k++) {
|
|
377
|
+
const a = cut[k];
|
|
378
|
+
const d = { x: cut[k + 1].x - a.x, y: cut[k + 1].y - a.y };
|
|
379
|
+
for (const c of signChanges(sideCoeffs(seg, a, d), true)) {
|
|
380
|
+
const pt = evalSeg(seg, c.t);
|
|
381
|
+
if (cutPosition(cut, k, pt) !== null) raw.push({ seg: si, t: c.t, pt });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
raw.sort((x, y) => x.seg - y.seg || x.t - y.t);
|
|
386
|
+
const same = (p, q) => Math.abs(p.x - q.x) <= 1e-7 && Math.abs(p.y - q.y) <= 1e-7;
|
|
387
|
+
const groups = [];
|
|
388
|
+
for (const st of raw) {
|
|
389
|
+
const g = groups[groups.length - 1];
|
|
390
|
+
if (g && same(g[0].pt, st.pt)) g.push(st);
|
|
391
|
+
else groups.push([st]);
|
|
392
|
+
}
|
|
393
|
+
if (sub.closed && groups.length > 1 && same(groups[0][0].pt, groups[groups.length - 1][0].pt)) {
|
|
394
|
+
groups[0].push(...groups.pop());
|
|
395
|
+
}
|
|
396
|
+
const first = sub.segs[0][0];
|
|
397
|
+
const lastSeg = sub.segs[sub.segs.length - 1];
|
|
398
|
+
const end = lastSeg[lastSeg.length - 1];
|
|
399
|
+
return groups.filter((g) => g.length % 2 === 1).map((g) => g[0]).filter((st) => sub.closed || !same(st.pt, first) && !same(st.pt, end));
|
|
400
|
+
}
|
|
401
|
+
function distinctPoints(cut) {
|
|
402
|
+
const out = [];
|
|
403
|
+
for (const p of cut) {
|
|
404
|
+
const q = out[out.length - 1];
|
|
405
|
+
if (!q || q.x !== p.x || q.y !== p.y) out.push({ x: p.x, y: p.y });
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
function cutBetween(cut, from, to) {
|
|
410
|
+
const out = [from.pt];
|
|
411
|
+
for (let v = Math.floor(from.s) + 1; v < to.s; v++) out.push(cut[v]);
|
|
412
|
+
out.push(to.pt);
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
function properCrossing(p0, p1, q0, q1) {
|
|
416
|
+
const r = { x: p1.x - p0.x, y: p1.y - p0.y };
|
|
417
|
+
const s = { x: q1.x - q0.x, y: q1.y - q0.y };
|
|
418
|
+
const den = r.x * s.y - r.y * s.x;
|
|
419
|
+
if (den === 0) return null;
|
|
420
|
+
const qp = { x: q0.x - p0.x, y: q0.y - p0.y };
|
|
421
|
+
const t = (qp.x * s.y - qp.y * s.x) / den;
|
|
422
|
+
const u = (qp.x * r.y - qp.y * r.x) / den;
|
|
423
|
+
const eps = 1e-9;
|
|
424
|
+
if (t <= eps || t >= 1 - eps || u <= eps || u >= 1 - eps) return null;
|
|
425
|
+
return { x: p0.x + t * r.x, y: p0.y + t * r.y };
|
|
426
|
+
}
|
|
427
|
+
function splitLoops(run) {
|
|
428
|
+
const spine = [run[0]];
|
|
429
|
+
const loops = [];
|
|
430
|
+
for (let i = 1; i < run.length; i++) {
|
|
431
|
+
const to = run[i];
|
|
432
|
+
for (; ; ) {
|
|
433
|
+
const from = spine[spine.length - 1];
|
|
434
|
+
let hit = null;
|
|
435
|
+
for (let j = 0; j + 2 < spine.length; j++) {
|
|
436
|
+
const x = properCrossing(from, to, spine[j], spine[j + 1]);
|
|
437
|
+
if (!x) continue;
|
|
438
|
+
const d = Math.hypot(x.x - from.x, x.y - from.y);
|
|
439
|
+
if (!hit || d < hit.d) hit = { j, x, d };
|
|
440
|
+
}
|
|
441
|
+
if (!hit) {
|
|
442
|
+
spine.push(to);
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
loops.push([hit.x, ...spine.slice(hit.j + 1)]);
|
|
446
|
+
spine.length = hit.j + 1;
|
|
447
|
+
spine.push(hit.x);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const last = spine[spine.length - 1];
|
|
451
|
+
if (spine.length >= 4 && last.x === spine[0].x && last.y === spine[0].y) {
|
|
452
|
+
loops.push(spine.slice(0, -1));
|
|
453
|
+
return { spine: [], loops };
|
|
454
|
+
}
|
|
455
|
+
return { spine, loops };
|
|
456
|
+
}
|
|
457
|
+
function pointAt(cut, s) {
|
|
458
|
+
const k = Math.min(Math.floor(s), cut.length - 2);
|
|
459
|
+
const f = s - k;
|
|
460
|
+
const a = cut[k], b = cut[k + 1];
|
|
461
|
+
return { x: a.x + f * (b.x - a.x), y: a.y + f * (b.y - a.y) };
|
|
462
|
+
}
|
|
463
|
+
function openClosedCut(cut, boundaries) {
|
|
464
|
+
const n = cut.length - 1;
|
|
465
|
+
if (n < 3 || cut[0].x !== cut[n].x || cut[0].y !== cut[n].y) return cut;
|
|
466
|
+
const crossings = findCrossings(boundaries, cut);
|
|
467
|
+
const k = crossings.findIndex((c) => !c.enters);
|
|
468
|
+
if (k < 0) return cut;
|
|
469
|
+
const next = k + 1 < crossings.length ? crossings[k + 1].s : crossings[0].s + n;
|
|
470
|
+
const s = (crossings[k].s + next) / 2 % n;
|
|
471
|
+
const seg = Math.min(Math.floor(s), n - 1);
|
|
472
|
+
const start = pointAt(cut, s);
|
|
473
|
+
const ring = cut.slice(0, n);
|
|
474
|
+
return distinctPoints([start, ...ring.slice(seg + 1), ...ring.slice(0, seg + 1), start]);
|
|
475
|
+
}
|
|
476
|
+
function interiorLoops(cut, boundaries) {
|
|
477
|
+
const crossings = findCrossings(boundaries, cut);
|
|
478
|
+
const chords = [];
|
|
479
|
+
for (let k = 0; k + 1 < crossings.length; k++) {
|
|
480
|
+
const c0 = crossings[k], c1 = crossings[k + 1];
|
|
481
|
+
if (c0.enters && !c1.enters && c1.s - c0.s > 1e-9) chords.push(splitLoops(cutBetween(cut, c0, c1)).spine);
|
|
482
|
+
}
|
|
483
|
+
const last = cut.length - 1;
|
|
484
|
+
const stops = [{ s: 0, pt: cut[0] }, ...crossings, { s: last, pt: cut[last] }];
|
|
485
|
+
const rings = [];
|
|
486
|
+
const loops = [];
|
|
487
|
+
for (let i = 0; i + 1 < stops.length; i++) {
|
|
488
|
+
const a = stops[i], b = stops[i + 1];
|
|
489
|
+
if (b.s - a.s <= 1e-9) continue;
|
|
490
|
+
if (windingAt(pointAt(cut, (a.s + b.s) / 2), boundaries) === 0) continue;
|
|
491
|
+
const run = [a.pt];
|
|
492
|
+
for (let v = Math.floor(a.s) + 1; v < b.s; v++) run.push(cut[v]);
|
|
493
|
+
run.push(b.pt);
|
|
494
|
+
for (const loop of splitLoops(distinctPoints(run)).loops) {
|
|
495
|
+
const ring = [...loop, loop[0]];
|
|
496
|
+
if (chords.some((c) => polylinesCross(ring, c)) || rings.some((r) => polylinesCross(ring, r))) continue;
|
|
497
|
+
rings.push(ring);
|
|
498
|
+
loops.push(loop);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return loops;
|
|
502
|
+
}
|
|
503
|
+
function cutOutLoops(regions, loops) {
|
|
504
|
+
let cut = false;
|
|
505
|
+
for (const loop of loops) {
|
|
506
|
+
let ring = loop.map((p, i) => [p, loop[(i + 1) % loop.length]]);
|
|
507
|
+
const a = signedArea(ring);
|
|
508
|
+
if (Math.abs(a) <= 1e-6) continue;
|
|
509
|
+
if (a < 0) ring = reverseContour(ring);
|
|
510
|
+
const probe = evalSeg(ring[0], 0.5);
|
|
511
|
+
const at = regions.findIndex((r) => windingAt(probe, r) !== 0);
|
|
512
|
+
if (at < 0) continue;
|
|
513
|
+
const samples = sampleContour(ring);
|
|
514
|
+
const within = (c) => {
|
|
515
|
+
const p = evalSeg(c[0], 0.5);
|
|
516
|
+
return pointInPolygon(samples, p.x, p.y);
|
|
517
|
+
};
|
|
518
|
+
const region = regions[at];
|
|
519
|
+
regions[at] = [...region.filter((c) => !within(c)), reverseContour(ring)];
|
|
520
|
+
regions.push([ring, ...region.filter(within)]);
|
|
521
|
+
cut = true;
|
|
522
|
+
}
|
|
523
|
+
return cut;
|
|
524
|
+
}
|
|
525
|
+
function polylinesCross(a, b) {
|
|
526
|
+
for (let i = 0; i + 1 < a.length; i++) {
|
|
527
|
+
for (let j = 0; j + 1 < b.length; j++) {
|
|
528
|
+
if (properCrossing(a[i], a[i + 1], b[j], b[j + 1])) return true;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
function evalSeg(s, t) {
|
|
534
|
+
const u = 1 - t;
|
|
535
|
+
if (s.length === 2) return { x: u * s[0].x + t * s[1].x, y: u * s[0].y + t * s[1].y };
|
|
536
|
+
if (s.length === 3) {
|
|
537
|
+
const a2 = u * u, b2 = 2 * u * t, c2 = t * t;
|
|
538
|
+
return { x: a2 * s[0].x + b2 * s[1].x + c2 * s[2].x, y: a2 * s[0].y + b2 * s[1].y + c2 * s[2].y };
|
|
539
|
+
}
|
|
540
|
+
const a = u * u * u, b = 3 * u * u * t, c = 3 * u * t * t, d = t * t * t;
|
|
541
|
+
return {
|
|
542
|
+
x: a * s[0].x + b * s[1].x + c * s[2].x + d * s[3].x,
|
|
543
|
+
y: a * s[0].y + b * s[1].y + c * s[2].y + d * s[3].y
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function derivSeg(s, t) {
|
|
547
|
+
const n = s.length - 1;
|
|
548
|
+
const d = [];
|
|
549
|
+
for (let i = 0; i < n; i++) d.push({ x: n * (s[i + 1].x - s[i].x), y: n * (s[i + 1].y - s[i].y) });
|
|
550
|
+
return d.length === 1 ? d[0] : evalSeg(d, t);
|
|
551
|
+
}
|
|
552
|
+
function splitSeg(s, t) {
|
|
553
|
+
const pts = (c) => {
|
|
554
|
+
const out = [];
|
|
555
|
+
for (let i = 0; i < c.length; i += 2) out.push({ x: c[i], y: c[i + 1] });
|
|
556
|
+
return out;
|
|
557
|
+
};
|
|
558
|
+
if (s.length === 2) {
|
|
559
|
+
const m = evalSeg(s, t);
|
|
560
|
+
return [[s[0], m], [m, s[1]]];
|
|
561
|
+
}
|
|
562
|
+
if (s.length === 3) {
|
|
563
|
+
const [l2, r2] = splitQuadraticAt(s[0].x, s[0].y, s[1].x, s[1].y, s[2].x, s[2].y, t);
|
|
564
|
+
return [pts(l2), pts(r2)];
|
|
565
|
+
}
|
|
566
|
+
const [l, r] = splitCubicAt(s[0].x, s[0].y, s[1].x, s[1].y, s[2].x, s[2].y, s[3].x, s[3].y, t);
|
|
567
|
+
return [pts(l), pts(r)];
|
|
568
|
+
}
|
|
569
|
+
function subSeg(s, t0, t1, p0, p1) {
|
|
570
|
+
if (t1 - t0 <= 0) return null;
|
|
571
|
+
let out = s;
|
|
572
|
+
if (t0 > 0) out = splitSeg(out, t0)[1];
|
|
573
|
+
const u = (t1 - t0) / (1 - t0);
|
|
574
|
+
if (u < 1) out = splitSeg(out, u)[0];
|
|
575
|
+
out = out.slice();
|
|
576
|
+
out[0] = p0;
|
|
577
|
+
out[out.length - 1] = p1;
|
|
578
|
+
return out;
|
|
579
|
+
}
|
|
580
|
+
var reverseContour = (c) => c.map((s) => s.slice().reverse()).reverse();
|
|
581
|
+
function sampleContour(c) {
|
|
582
|
+
const out = [];
|
|
583
|
+
for (const s of c) {
|
|
584
|
+
const n = s.length === 2 ? 1 : 16;
|
|
585
|
+
for (let i = 0; i < n; i++) {
|
|
586
|
+
const p = evalSeg(s, i / n);
|
|
587
|
+
out.push(p.x, p.y);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return out;
|
|
591
|
+
}
|
|
592
|
+
function signedArea(c) {
|
|
593
|
+
const pts = sampleContour(c);
|
|
594
|
+
let a = 0;
|
|
595
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
596
|
+
const j = (i + 2) % pts.length;
|
|
597
|
+
a += pts[i] * pts[j + 1] - pts[j] * pts[i + 1];
|
|
598
|
+
}
|
|
599
|
+
return a / 2;
|
|
600
|
+
}
|
|
601
|
+
function sideCoeffs(s, o, d) {
|
|
602
|
+
return s.map((p) => d.x * (p.y - o.y) - d.y * (p.x - o.x));
|
|
603
|
+
}
|
|
604
|
+
function bernstein(f, t) {
|
|
605
|
+
const u = 1 - t;
|
|
606
|
+
if (f.length === 2) return u * f[0] + t * f[1];
|
|
607
|
+
if (f.length === 3) return u * u * f[0] + 2 * u * t * f[1] + t * t * f[2];
|
|
608
|
+
return u * u * u * f[0] + 3 * u * u * t * f[1] + 3 * u * t * t * f[2] + t * t * t * f[3];
|
|
609
|
+
}
|
|
610
|
+
function signChanges(f, zeroIsPositive) {
|
|
611
|
+
const sgn = (v) => v > 0 ? 1 : v < 0 ? -1 : zeroIsPositive ? 1 : 0;
|
|
612
|
+
const cuts = [0, ...criticalPoints(f), 1];
|
|
613
|
+
const out = [];
|
|
614
|
+
for (let i = 0; i + 1 < cuts.length; i++) {
|
|
615
|
+
let lo = cuts[i], hi = cuts[i + 1];
|
|
616
|
+
const slo = sgn(bernstein(f, lo)), shi = sgn(bernstein(f, hi));
|
|
617
|
+
if (slo === 0 && i > 0) {
|
|
618
|
+
const before = sgn(bernstein(f, lo - 1e-7)), after = sgn(bernstein(f, lo + 1e-7));
|
|
619
|
+
if (before * after < 0) out.push({ t: lo, before, after });
|
|
620
|
+
}
|
|
621
|
+
if (slo * shi >= 0) continue;
|
|
622
|
+
for (let it = 0; it < 64; it++) {
|
|
623
|
+
const mid = (lo + hi) / 2;
|
|
624
|
+
if (sgn(bernstein(f, mid)) === slo) lo = mid;
|
|
625
|
+
else hi = mid;
|
|
626
|
+
}
|
|
627
|
+
out.push({ t: (lo + hi) / 2, before: slo, after: shi });
|
|
628
|
+
}
|
|
629
|
+
return out;
|
|
630
|
+
}
|
|
631
|
+
function criticalPoints(f) {
|
|
632
|
+
if (f.length < 3) return [];
|
|
633
|
+
const n = f.length - 1;
|
|
634
|
+
const d = f.slice(1).map((v, i) => n * (v - f[i]));
|
|
635
|
+
let A = 0, B, C;
|
|
636
|
+
if (d.length === 2) {
|
|
637
|
+
B = d[1] - d[0];
|
|
638
|
+
C = d[0];
|
|
639
|
+
} else {
|
|
640
|
+
A = d[0] - 2 * d[1] + d[2];
|
|
641
|
+
B = 2 * (d[1] - d[0]);
|
|
642
|
+
C = d[0];
|
|
643
|
+
}
|
|
644
|
+
const out = [];
|
|
645
|
+
const scale = Math.max(Math.abs(A), Math.abs(B), Math.abs(C)) || 1;
|
|
646
|
+
if (Math.abs(A) / scale < 1e-12) {
|
|
647
|
+
if (Math.abs(B) / scale > 1e-12) out.push(-C / B);
|
|
648
|
+
} else {
|
|
649
|
+
const disc = B * B - 4 * A * C;
|
|
650
|
+
if (disc >= 0) {
|
|
651
|
+
const q = -0.5 * (B + Math.sign(B || 1) * Math.sqrt(disc));
|
|
652
|
+
out.push(q / A);
|
|
653
|
+
if (q !== 0) out.push(C / q);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return out.filter((t) => t > 0 && t < 1).sort((x, y) => x - y);
|
|
657
|
+
}
|
|
658
|
+
function endSide(f, fromStart) {
|
|
659
|
+
const n = f.length;
|
|
660
|
+
for (let i = 1; i < n; i++) {
|
|
661
|
+
const v = fromStart ? f[i] : f[n - 1 - i];
|
|
662
|
+
if (v !== 0) return Math.sign(v);
|
|
663
|
+
}
|
|
664
|
+
return 0;
|
|
665
|
+
}
|
|
666
|
+
function parseContours(path) {
|
|
667
|
+
return parseSubpaths(path).map((s) => s.segs);
|
|
668
|
+
}
|
|
669
|
+
function parseSubpaths(path, implicitClose = true) {
|
|
670
|
+
if (path.kind === "rect") {
|
|
671
|
+
const { x, y, width: w, height: h } = path;
|
|
672
|
+
const p = [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }];
|
|
673
|
+
return [{ segs: p.map((q, i) => [q, p[(i + 1) % 4]]), closed: true }];
|
|
674
|
+
}
|
|
675
|
+
const out = [];
|
|
676
|
+
let cur = [];
|
|
677
|
+
let start = { x: 0, y: 0 };
|
|
678
|
+
let pen = start;
|
|
679
|
+
const finish = (closed) => {
|
|
680
|
+
if (cur.length > 0 && (closed || implicitClose) && (pen.x !== start.x || pen.y !== start.y)) {
|
|
681
|
+
cur.push([pen, start]);
|
|
682
|
+
}
|
|
683
|
+
if (cur.length > 0) out.push({ segs: cur, closed });
|
|
684
|
+
cur = [];
|
|
685
|
+
pen = start;
|
|
686
|
+
};
|
|
687
|
+
const c = path.coords;
|
|
688
|
+
let k = 0;
|
|
689
|
+
for (const cmd of path.commands) {
|
|
690
|
+
if (cmd === PATH_M) {
|
|
691
|
+
finish(false);
|
|
692
|
+
start = pen = { x: c[k], y: c[k + 1] };
|
|
693
|
+
k += 2;
|
|
694
|
+
} else if (cmd === PATH_L) {
|
|
695
|
+
const e = { x: c[k], y: c[k + 1] };
|
|
696
|
+
if (e.x !== pen.x || e.y !== pen.y) cur.push([pen, e]);
|
|
697
|
+
pen = e;
|
|
698
|
+
k += 2;
|
|
699
|
+
} else if (cmd === PATH_Q) {
|
|
700
|
+
const e = { x: c[k + 2], y: c[k + 3] };
|
|
701
|
+
cur.push([pen, { x: c[k], y: c[k + 1] }, e]);
|
|
702
|
+
pen = e;
|
|
703
|
+
k += 4;
|
|
704
|
+
} else if (cmd === PATH_C) {
|
|
705
|
+
const e = { x: c[k + 4], y: c[k + 5] };
|
|
706
|
+
cur.push([pen, { x: c[k], y: c[k + 1] }, { x: c[k + 2], y: c[k + 3] }, e]);
|
|
707
|
+
pen = e;
|
|
708
|
+
k += 6;
|
|
709
|
+
} else if (cmd === PATH_Z) {
|
|
710
|
+
finish(true);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
finish(false);
|
|
714
|
+
return out;
|
|
715
|
+
}
|
|
716
|
+
function contoursIntersect(contours, tol) {
|
|
717
|
+
const edges = [];
|
|
718
|
+
contours.forEach((contour, ci) => {
|
|
719
|
+
const pts = [];
|
|
720
|
+
for (const s of contour) {
|
|
721
|
+
let n = 1;
|
|
722
|
+
if (s.length > 2) {
|
|
723
|
+
let len = 0;
|
|
724
|
+
for (let i = 0; i + 1 < s.length; i++) len += Math.hypot(s[i + 1].x - s[i].x, s[i + 1].y - s[i].y);
|
|
725
|
+
n = Math.min(64, Math.max(2, Math.ceil(Math.sqrt(len / Math.max(tol, 1e-3)))));
|
|
726
|
+
}
|
|
727
|
+
for (let i = 0; i < n; i++) pts.push(evalSeg(s, i / n));
|
|
728
|
+
}
|
|
729
|
+
for (let i = 0; i < pts.length; i++) {
|
|
730
|
+
const p = pts[i], q = pts[(i + 1) % pts.length];
|
|
731
|
+
edges.push({ x0: p.x, y0: p.y, x1: q.x, y1: q.y, c: ci, i, n: pts.length });
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
for (let i = 0; i < edges.length; i++) {
|
|
735
|
+
const e = edges[i];
|
|
736
|
+
for (let j = i + 1; j < edges.length; j++) {
|
|
737
|
+
const f = edges[j];
|
|
738
|
+
if (e.c === f.c && (j - i === 1 || i === 0 && f.i === f.n - 1)) continue;
|
|
739
|
+
if (Math.max(e.x0, e.x1) < Math.min(f.x0, f.x1) || Math.max(f.x0, f.x1) < Math.min(e.x0, e.x1)) continue;
|
|
740
|
+
if (Math.max(e.y0, e.y1) < Math.min(f.y0, f.y1) || Math.max(f.y0, f.y1) < Math.min(e.y0, e.y1)) continue;
|
|
741
|
+
if (segmentsCross(e.x0, e.y0, e.x1, e.y1, f.x0, f.y0, f.x1, f.y1)) return true;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
function windingAt(q, contours) {
|
|
747
|
+
const dir = { x: 1, y: 0 };
|
|
748
|
+
let w = 0;
|
|
749
|
+
for (const contour of contours) {
|
|
750
|
+
for (const s of contour) {
|
|
751
|
+
for (const c of signChanges(sideCoeffs(s, q, dir), true)) {
|
|
752
|
+
if (evalSeg(s, c.t).x > q.x) w += c.after > 0 ? 1 : -1;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return w;
|
|
757
|
+
}
|
|
758
|
+
function orientBoundaries(contours, fillRule) {
|
|
759
|
+
const filled = (q) => {
|
|
760
|
+
const w = windingAt(q, contours);
|
|
761
|
+
return fillRule === "evenodd" ? (w & 1) !== 0 : w !== 0;
|
|
762
|
+
};
|
|
763
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
764
|
+
for (const c of contours) for (const s of c) for (const p of s) {
|
|
765
|
+
minX = Math.min(minX, p.x);
|
|
766
|
+
maxX = Math.max(maxX, p.x);
|
|
767
|
+
minY = Math.min(minY, p.y);
|
|
768
|
+
maxY = Math.max(maxY, p.y);
|
|
769
|
+
}
|
|
770
|
+
const eps = Math.max(Math.hypot(maxX - minX, maxY - minY) * 1e-6, 1e-9);
|
|
771
|
+
const out = [];
|
|
772
|
+
for (const contour of contours) {
|
|
773
|
+
let probe = contour[0];
|
|
774
|
+
let best = -1;
|
|
775
|
+
for (const s of contour) {
|
|
776
|
+
const len = Math.hypot(s[s.length - 1].x - s[0].x, s[s.length - 1].y - s[0].y);
|
|
777
|
+
if (len > best) {
|
|
778
|
+
best = len;
|
|
779
|
+
probe = s;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
const p = evalSeg(probe, 0.5);
|
|
783
|
+
let tan = derivSeg(probe, 0.5);
|
|
784
|
+
const tl = Math.hypot(tan.x, tan.y);
|
|
785
|
+
if (tl === 0) continue;
|
|
786
|
+
tan = { x: tan.x / tl, y: tan.y / tl };
|
|
787
|
+
const left = filled({ x: p.x - tan.y * eps, y: p.y + tan.x * eps });
|
|
788
|
+
const right = filled({ x: p.x + tan.y * eps, y: p.y - tan.x * eps });
|
|
789
|
+
if (left && !right) out.push(contour);
|
|
790
|
+
else if (right && !left) out.push(reverseContour(contour));
|
|
791
|
+
}
|
|
792
|
+
return out;
|
|
793
|
+
}
|
|
794
|
+
function cutPosition(cut, k, pt) {
|
|
795
|
+
const a = cut[k], b = cut[k + 1];
|
|
796
|
+
const d = { x: b.x - a.x, y: b.y - a.y };
|
|
797
|
+
const s = ((pt.x - a.x) * d.x + (pt.y - a.y) * d.y) / (d.x * d.x + d.y * d.y);
|
|
798
|
+
const eps = 1e-9;
|
|
799
|
+
const last = k + 2 === cut.length;
|
|
800
|
+
if (s < (k === 0 ? 0 : -eps)) return null;
|
|
801
|
+
if (last ? s > 1 : s >= 1 - eps) return null;
|
|
802
|
+
return k + Math.min(1, Math.max(0, s));
|
|
803
|
+
}
|
|
804
|
+
function findCrossings(boundaries, cut) {
|
|
805
|
+
const out = [];
|
|
806
|
+
for (let k = 0; k + 1 < cut.length; k++) crossingsOnSegment(boundaries, cut, k, out);
|
|
807
|
+
return out.sort((x, y) => x.s - y.s);
|
|
808
|
+
}
|
|
809
|
+
function crossingsOnSegment(boundaries, cut, k, out) {
|
|
810
|
+
const a = cut[k];
|
|
811
|
+
const d = { x: cut[k + 1].x - a.x, y: cut[k + 1].y - a.y };
|
|
812
|
+
const push = (contour, seg, t, before, after) => {
|
|
813
|
+
const pt = evalSeg(boundaries[contour][seg], t);
|
|
814
|
+
const s = cutPosition(cut, k, pt);
|
|
815
|
+
if (s === null) return;
|
|
816
|
+
out.push({ contour, seg, t, pt, s, enters: before > 0 && after < 0 });
|
|
817
|
+
};
|
|
818
|
+
boundaries.forEach((contour, ci) => {
|
|
819
|
+
const fs = contour.map((s) => sideCoeffs(s, a, d));
|
|
820
|
+
const n = contour.length;
|
|
821
|
+
fs.forEach((f, si) => {
|
|
822
|
+
for (const c of signChanges(f, false)) push(ci, si, c.t, c.before, c.after);
|
|
823
|
+
if (f[0] !== 0) return;
|
|
824
|
+
const after = endSide(f, true);
|
|
825
|
+
if (after === 0) return;
|
|
826
|
+
let before = 0;
|
|
827
|
+
for (let k2 = 1; k2 <= n && before === 0; k2++) before = endSide(fs[(si - k2 + n) % n], false);
|
|
828
|
+
if (before !== 0 && before !== after) push(ci, si, 0, before, after);
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
function traceLoops(boundaries, partner, chord) {
|
|
833
|
+
const byContour = /* @__PURE__ */ new Map();
|
|
834
|
+
for (const c of partner.keys()) {
|
|
835
|
+
const list = byContour.get(c.contour) ?? [];
|
|
836
|
+
list.push(c);
|
|
837
|
+
byContour.set(c.contour, list);
|
|
838
|
+
}
|
|
839
|
+
const arcFrom = /* @__PURE__ */ new Map();
|
|
840
|
+
const untouched = [];
|
|
841
|
+
boundaries.forEach((contour, ci) => {
|
|
842
|
+
const cuts = byContour.get(ci);
|
|
843
|
+
if (!cuts) {
|
|
844
|
+
untouched.push(contour);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
cuts.sort((x, y) => x.seg - y.seg || x.t - y.t);
|
|
848
|
+
for (let k = 0; k < cuts.length; k++) {
|
|
849
|
+
const from = cuts[k], to = cuts[(k + 1) % cuts.length];
|
|
850
|
+
arcFrom.set(from, { segs: arcBetween(contour, from, to, cuts.length === 1), end: to });
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
const loops = [];
|
|
854
|
+
const seen = /* @__PURE__ */ new Set();
|
|
855
|
+
for (const start of arcFrom.keys()) {
|
|
856
|
+
if (seen.has(start)) continue;
|
|
857
|
+
const loop = [];
|
|
858
|
+
let at = start;
|
|
859
|
+
while (!seen.has(at)) {
|
|
860
|
+
seen.add(at);
|
|
861
|
+
const arc = arcFrom.get(at);
|
|
862
|
+
loop.push(...arc.segs);
|
|
863
|
+
const run = chord.get(arc.end);
|
|
864
|
+
for (let i = 0; i + 1 < run.length; i++) loop.push([run[i], run[i + 1]]);
|
|
865
|
+
at = partner.get(arc.end);
|
|
866
|
+
}
|
|
867
|
+
loops.push(loop);
|
|
868
|
+
}
|
|
869
|
+
return { loops, untouched };
|
|
870
|
+
}
|
|
871
|
+
function arcBetween(contour, from, to, whole) {
|
|
872
|
+
const out = [];
|
|
873
|
+
const add = (s) => {
|
|
874
|
+
if (s) out.push(s);
|
|
875
|
+
};
|
|
876
|
+
if (!whole && to.seg === from.seg && to.t > from.t) {
|
|
877
|
+
add(subSeg(contour[from.seg], from.t, to.t, from.pt, to.pt));
|
|
878
|
+
return out;
|
|
879
|
+
}
|
|
880
|
+
const n = contour.length;
|
|
881
|
+
const first = contour[from.seg];
|
|
882
|
+
add(subSeg(first, from.t, 1, from.pt, first[first.length - 1]));
|
|
883
|
+
for (let i = (from.seg + 1) % n; i !== to.seg; i = (i + 1) % n) out.push(contour[i]);
|
|
884
|
+
const last = contour[to.seg];
|
|
885
|
+
add(subSeg(last, 0, to.t, last[0], to.pt));
|
|
886
|
+
return out;
|
|
887
|
+
}
|
|
888
|
+
function assembleRegions(outers, holes) {
|
|
889
|
+
const regions = outers.map((o) => ({ loops: [o], ring: sampleContour(o), area: signedArea(o) }));
|
|
890
|
+
for (const h of holes) {
|
|
891
|
+
const hp = evalSeg(h[0], 0.5);
|
|
892
|
+
let best = null;
|
|
893
|
+
for (const r of regions) {
|
|
894
|
+
if (pointInPolygon(r.ring, hp.x, hp.y) && (!best || r.area < best.area)) best = r;
|
|
895
|
+
}
|
|
896
|
+
best?.loops.push(h);
|
|
897
|
+
}
|
|
898
|
+
return regions.map((r) => r.loops);
|
|
899
|
+
}
|
|
900
|
+
var regionArea = (r) => r.reduce((s, c) => s + signedArea(c), 0);
|
|
901
|
+
var toPath = (loops, fillRule) => writePath(loops.map((segs) => ({ segs, closed: true })), fillRule);
|
|
902
|
+
function writePath(subpaths, fillRule) {
|
|
903
|
+
const cmds = [];
|
|
904
|
+
const xs = [];
|
|
905
|
+
for (const { segs, closed } of subpaths) {
|
|
906
|
+
cmds.push(PATH_M);
|
|
907
|
+
xs.push(segs[0][0].x, segs[0][0].y);
|
|
908
|
+
for (const s of segs) {
|
|
909
|
+
cmds.push(s.length === 2 ? PATH_L : s.length === 3 ? PATH_Q : PATH_C);
|
|
910
|
+
for (let i = 1; i < s.length; i++) xs.push(s[i].x, s[i].y);
|
|
911
|
+
}
|
|
912
|
+
if (closed) cmds.push(PATH_Z);
|
|
913
|
+
}
|
|
914
|
+
return { kind: "polygon", commands: new Uint8Array(cmds), coords: new Float32Array(xs), fillRule };
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
export { multiPolygonToPath, pathCrop, pathDivide, pathExclude, pathIntersect, pathSubtract, pathToMultiPolygon, pathUnion, snipPathByPolyline, splitPathByPolyline, splitPathBySegment };
|
|
278
918
|
//# sourceMappingURL=index.js.map
|
|
279
919
|
//# sourceMappingURL=index.js.map
|