pattapatta 0.2.1 → 0.3.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.
@@ -1,4 +1,5 @@
1
- import { FillRule, JoinType, inflatePathsD, EndType, isPositiveD, pointInPolygonD, PointInPolygonResult } from 'clipper2-ts';
1
+ import { FillRule, JoinType, inflatePathsD, EndType, triangulateD, pointInPolygonD, PointInPolygonResult, isPositiveD } from 'clipper2-ts';
2
+ import { Delaunay } from 'd3-delaunay';
2
3
 
3
4
  // src/types/vec2.ts
4
5
  function vec2(x, y) {
@@ -11,6 +12,16 @@ function equalsVec2(a, b, eps = 1e-9) {
11
12
  return Math.abs(a.x - b.x) <= eps && Math.abs(a.y - b.y) <= eps;
12
13
  }
13
14
 
15
+ // src/types/circle.ts
16
+ function circle(x, y, r) {
17
+ return { x, y, r };
18
+ }
19
+
20
+ // src/types/segment.ts
21
+ function segment(a, b) {
22
+ return { a, b };
23
+ }
24
+
14
25
  // src/types/path.ts
15
26
  function path(rings, closed = true) {
16
27
  return {
@@ -18,6 +29,12 @@ function path(rings, closed = true) {
18
29
  closed
19
30
  };
20
31
  }
32
+ function polyline(points) {
33
+ return path([points], false);
34
+ }
35
+ function polygon(exterior, holes = []) {
36
+ return path([exterior, ...holes], true);
37
+ }
21
38
  function normalizeRing(ring, eps = 1e-9) {
22
39
  if (ring.length < 2) return ring.map(cloneVec2);
23
40
  const first = ring[0];
@@ -101,6 +118,1299 @@ function buffer(path2, delta, options = {}) {
101
118
  return pathsDToGroup(inflated);
102
119
  }
103
120
 
121
+ // src/predicates/index.ts
122
+ function ringArea(ring) {
123
+ if (ring.length < 3) return 0;
124
+ let sum = 0;
125
+ for (let i = 0; i < ring.length; i++) {
126
+ const a = ring[i];
127
+ const b = ring[(i + 1) % ring.length];
128
+ sum += a.x * b.y - b.x * a.y;
129
+ }
130
+ return sum / 2;
131
+ }
132
+ function centroid(p) {
133
+ const ring = p.rings[0];
134
+ if (!ring || ring.length === 0) return vec2(0, 0);
135
+ const a = ringArea(ring);
136
+ if (Math.abs(a) < 1e-12) {
137
+ let sx = 0;
138
+ let sy = 0;
139
+ for (const v of ring) {
140
+ sx += v.x;
141
+ sy += v.y;
142
+ }
143
+ return vec2(sx / ring.length, sy / ring.length);
144
+ }
145
+ let cx = 0;
146
+ let cy = 0;
147
+ for (let i = 0; i < ring.length; i++) {
148
+ const p0 = ring[i];
149
+ const p1 = ring[(i + 1) % ring.length];
150
+ const cross = p0.x * p1.y - p1.x * p0.y;
151
+ cx += (p0.x + p1.x) * cross;
152
+ cy += (p0.y + p1.y) * cross;
153
+ }
154
+ return vec2(cx / (6 * a), cy / (6 * a));
155
+ }
156
+ function containsPoint(p, point) {
157
+ if (!p.closed || !p.rings[0]) return false;
158
+ if (!pointInRing(point, p.rings[0])) return false;
159
+ for (let i = 1; i < p.rings.length; i++) {
160
+ const hole = p.rings[i];
161
+ if (hole && pointInRing(point, hole)) return false;
162
+ }
163
+ return true;
164
+ }
165
+ function bounds(p) {
166
+ let minX = Infinity;
167
+ let minY = Infinity;
168
+ let maxX = -Infinity;
169
+ let maxY = -Infinity;
170
+ for (const ring of p.rings) {
171
+ for (const v of ring) {
172
+ minX = Math.min(minX, v.x);
173
+ minY = Math.min(minY, v.y);
174
+ maxX = Math.max(maxX, v.x);
175
+ maxY = Math.max(maxY, v.y);
176
+ }
177
+ }
178
+ if (!Number.isFinite(minX)) {
179
+ return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
180
+ }
181
+ return { minX, minY, maxX, maxY };
182
+ }
183
+
184
+ // src/processing/index.ts
185
+ function densify(p, maxSegLen) {
186
+ if (maxSegLen <= 0) return p;
187
+ return path(
188
+ p.rings.map((ring) => densifyRing(ring, maxSegLen, p.closed)),
189
+ p.closed
190
+ );
191
+ }
192
+ function densifyRing(ring, maxSegLen, closed) {
193
+ if (ring.length < 2) return [...ring];
194
+ const out = [];
195
+ const n = ring.length;
196
+ const edges = closed ? n : n - 1;
197
+ for (let i = 0; i < edges; i++) {
198
+ const a = ring[i];
199
+ const b = ring[(i + 1) % n];
200
+ out.push(a);
201
+ const dist = Math.hypot(b.x - a.x, b.y - a.y);
202
+ const steps = Math.floor(dist / maxSegLen);
203
+ for (let s = 1; s < steps; s++) {
204
+ const t = s / steps;
205
+ out.push({
206
+ x: a.x + (b.x - a.x) * t,
207
+ y: a.y + (b.y - a.y) * t
208
+ });
209
+ }
210
+ }
211
+ if (!closed) out.push(ring[n - 1]);
212
+ return closed ? normalizeRing(out) : out;
213
+ }
214
+
215
+ // src/pointSet/index.ts
216
+ function mulberry32(a) {
217
+ return function() {
218
+ let t = a += 1831565813;
219
+ t = Math.imul(t ^ t >>> 15, t | 1);
220
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
221
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
222
+ };
223
+ }
224
+ function poisson(minDistance, minX, minY, maxX, maxY, seed = 1, maxAttempts = 30) {
225
+ const rng = mulberry32(seed >>> 0);
226
+ const r = Math.max(minDistance, 1e-9);
227
+ const cell = r / Math.SQRT2;
228
+ const w = maxX - minX;
229
+ const h = maxY - minY;
230
+ const gw = Math.max(1, Math.ceil(w / cell));
231
+ const gh = Math.max(1, Math.ceil(h / cell));
232
+ const grid = Array(gw * gh).fill(null);
233
+ const points = [];
234
+ const active = [];
235
+ const gx = (p) => Math.min(gw - 1, Math.floor((p.x - minX) / cell));
236
+ const gy = (p) => Math.min(gh - 1, Math.floor((p.y - minY) / cell));
237
+ const emit = (p) => {
238
+ const i = points.length;
239
+ points.push(p);
240
+ active.push(i);
241
+ grid[gy(p) * gw + gx(p)] = i;
242
+ };
243
+ emit({
244
+ x: minX + rng() * w,
245
+ y: minY + rng() * h
246
+ });
247
+ while (active.length > 0) {
248
+ const ai = Math.floor(rng() * active.length);
249
+ const i = active[ai];
250
+ const src = points[i];
251
+ let found = false;
252
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
253
+ const ang = rng() * Math.PI * 2;
254
+ const rad = r * (1 + rng());
255
+ const cand = {
256
+ x: src.x + Math.cos(ang) * rad,
257
+ y: src.y + Math.sin(ang) * rad
258
+ };
259
+ if (cand.x < minX || cand.x > maxX || cand.y < minY || cand.y > maxY) {
260
+ continue;
261
+ }
262
+ const cx = gx(cand);
263
+ const cy = gy(cand);
264
+ let ok = true;
265
+ for (let yy = Math.max(0, cy - 2); yy <= Math.min(gh - 1, cy + 2) && ok; yy++) {
266
+ for (let xx = Math.max(0, cx - 2); xx <= Math.min(gw - 1, cx + 2); xx++) {
267
+ const j = grid[yy * gw + xx];
268
+ if (j == null) continue;
269
+ const q = points[j];
270
+ if (Math.hypot(cand.x - q.x, cand.y - q.y) < r) {
271
+ ok = false;
272
+ break;
273
+ }
274
+ }
275
+ }
276
+ if (ok) {
277
+ emit(cand);
278
+ found = true;
279
+ break;
280
+ }
281
+ }
282
+ if (!found) active.splice(ai, 1);
283
+ }
284
+ return points;
285
+ }
286
+
287
+ // src/triangulation/index.ts
288
+ function earCutTriangulation(p, options = {}) {
289
+ if (!p.closed || p.rings.length === 0) return [];
290
+ const { solution } = triangulateD(
291
+ pathToPathsD(p),
292
+ options.precision ?? 8,
293
+ options.useDelaunay ?? false
294
+ );
295
+ const tris = [];
296
+ for (const pd of solution) {
297
+ const ring = pathDToRing(pd);
298
+ if (ring.length >= 3) tris.push(polygon(ring.slice(0, 3)));
299
+ }
300
+ return tris;
301
+ }
302
+ function delaunayTriangulation(p) {
303
+ return earCutTriangulation(p, { useDelaunay: true });
304
+ }
305
+
306
+ // src/contour/dissolve.ts
307
+ function key(v, digits = 9) {
308
+ return `${v.x.toFixed(digits)},${v.y.toFixed(digits)}`;
309
+ }
310
+ function same(a, b, eps = 1e-9) {
311
+ return Math.abs(a.x - b.x) <= eps && Math.abs(a.y - b.y) <= eps;
312
+ }
313
+ function dissolveSegments(segments) {
314
+ const segs = segments.filter((s) => !same(s.a, s.b));
315
+ if (segs.length === 0) return group([]);
316
+ const adj = /* @__PURE__ */ new Map();
317
+ const pts = /* @__PURE__ */ new Map();
318
+ const add = (a, b, segIdx) => {
319
+ const ka = key(a);
320
+ const kb = key(b);
321
+ pts.set(ka, a);
322
+ pts.set(kb, b);
323
+ if (!adj.has(ka)) adj.set(ka, []);
324
+ if (!adj.has(kb)) adj.set(kb, []);
325
+ adj.get(ka).push({ segIdx, other: kb });
326
+ adj.get(kb).push({ segIdx, other: ka });
327
+ };
328
+ for (let i = 0; i < segs.length; i++) {
329
+ add(segs[i].a, segs[i].b, i);
330
+ }
331
+ const used = /* @__PURE__ */ new Set();
332
+ const paths = [];
333
+ const unusedDegree = (k) => (adj.get(k) ?? []).filter((e) => !used.has(e.segIdx)).length;
334
+ const nextUnused = (k) => (adj.get(k) ?? []).find((e) => !used.has(e.segIdx));
335
+ const starts = [...adj.keys()].sort(
336
+ (a, b) => unusedDegree(a) - unusedDegree(b)
337
+ );
338
+ for (const start of starts) {
339
+ let link = nextUnused(start);
340
+ while (link) {
341
+ const chain = [pts.get(start)];
342
+ used.add(link.segIdx);
343
+ let next = link.other;
344
+ chain.push(pts.get(next));
345
+ while (unusedDegree(next) === 1) {
346
+ const e = nextUnused(next);
347
+ if (!e) break;
348
+ used.add(e.segIdx);
349
+ next = e.other;
350
+ chain.push(pts.get(next));
351
+ if (next === start) break;
352
+ }
353
+ if (chain.length >= 2) paths.push(polyline(chain));
354
+ link = nextUnused(start);
355
+ }
356
+ }
357
+ for (let i = 0; i < segs.length; i++) {
358
+ if (used.has(i)) continue;
359
+ const s = segs[i];
360
+ paths.push(polyline([s.a, s.b]));
361
+ }
362
+ return group(paths);
363
+ }
364
+
365
+ // src/contour/chordalAxis.ts
366
+ function mid(a, b) {
367
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
368
+ }
369
+ function len(a, b) {
370
+ return Math.hypot(b.x - a.x, b.y - a.y);
371
+ }
372
+ function edgeKey(a, b) {
373
+ const ka = `${a.x.toFixed(8)},${a.y.toFixed(8)}`;
374
+ const kb = `${b.x.toFixed(8)},${b.y.toFixed(8)}`;
375
+ return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
376
+ }
377
+ function chordalAxis(shape) {
378
+ if (!shape.closed || shape.rings.length === 0) return dissolveSegments([]);
379
+ const b = boundsSpan(shape);
380
+ const densified = densify(shape, Math.max(b / 40, 1e-3));
381
+ const tris = delaunayTriangulation(densified);
382
+ if (tris.length === 0) return dissolveSegments([]);
383
+ const edgeCount = /* @__PURE__ */ new Map();
384
+ const triEdges = [];
385
+ for (const t of tris) {
386
+ const r = t.rings[0];
387
+ if (!r || r.length < 3) continue;
388
+ const a = r[0];
389
+ const b0 = r[1];
390
+ const c = r[2];
391
+ triEdges.push([a, b0, c]);
392
+ for (const [u, v] of [
393
+ [a, b0],
394
+ [b0, c],
395
+ [c, a]
396
+ ]) {
397
+ const k = edgeKey(u, v);
398
+ edgeCount.set(k, (edgeCount.get(k) ?? 0) + 1);
399
+ }
400
+ }
401
+ const isBoundary = (u, v) => (edgeCount.get(edgeKey(u, v)) ?? 0) <= 1;
402
+ const segs = [];
403
+ for (const [a, b0, c] of triEdges) {
404
+ const edges = [
405
+ [a, b0],
406
+ [b0, c],
407
+ [c, a]
408
+ ];
409
+ const interior = edges.filter(([u, v]) => !isBoundary(u, v));
410
+ const degree = interior.length;
411
+ if (degree === 1) {
412
+ const [u, v] = interior[0];
413
+ const cen = centroid({ rings: [[a, b0, c]]});
414
+ segs.push(segment(cen, mid(u, v)));
415
+ } else if (degree === 2) {
416
+ const m0 = mid(interior[0][0], interior[0][1]);
417
+ const m1 = mid(interior[1][0], interior[1][1]);
418
+ segs.push(segment(m0, m1));
419
+ } else if (degree === 3) {
420
+ const ranked = edges.map(([u, v]) => ({ u, v, l: len(u, v) })).sort((x, y) => y.l - x.l);
421
+ const longest = ranked[0];
422
+ const shortA = ranked[1];
423
+ const shortB = ranked[2];
424
+ const mL = mid(longest.u, longest.v);
425
+ segs.push(segment(mid(shortA.u, shortA.v), mL));
426
+ segs.push(segment(mid(shortB.u, shortB.v), mL));
427
+ }
428
+ }
429
+ return dissolveSegments(segs);
430
+ }
431
+ function boundsSpan(p) {
432
+ let minX = Infinity;
433
+ let minY = Infinity;
434
+ let maxX = -Infinity;
435
+ let maxY = -Infinity;
436
+ for (const ring of p.rings) {
437
+ for (const v of ring) {
438
+ minX = Math.min(minX, v.x);
439
+ minY = Math.min(minY, v.y);
440
+ maxX = Math.max(maxX, v.x);
441
+ maxY = Math.max(maxY, v.y);
442
+ }
443
+ }
444
+ return Math.max(maxX - minX, maxY - minY, 1);
445
+ }
446
+
447
+ // src/circlePacking/index.ts
448
+ function maximumInscribedPack(path2, n, tolerance = 1) {
449
+ return obstaclePack(path2, [], n, tolerance);
450
+ }
451
+ function obstaclePack(path2, obstacles, n, tolerance = 1) {
452
+ const packing = obstacles.map((c) => ({ ...c }));
453
+ const placed = [];
454
+ const tol = Math.max(0.01, tolerance);
455
+ for (let i = 0; i < n; i++) {
456
+ const next = findLargestEmptyCircle(path2, packing, tol);
457
+ if (!next || next.r <= 1e-9) break;
458
+ packing.push(next);
459
+ placed.push(next);
460
+ }
461
+ return placed;
462
+ }
463
+ function findLargestEmptyCircle(path2, obstacles, tolerance) {
464
+ const b = bounds(path2);
465
+ const diag = Math.hypot(b.maxX - b.minX, b.maxY - b.minY) || 1;
466
+ const step = Math.max(diag * 0.02 * Math.sqrt(tolerance), diag * 0.01);
467
+ let best = null;
468
+ for (let x = b.minX; x <= b.maxX; x += step) {
469
+ for (let y = b.minY; y <= b.maxY; y += step) {
470
+ const p = { x, y };
471
+ if (!containsPoint(path2, p)) continue;
472
+ const r = clearanceRadius(p, path2, obstacles);
473
+ if (!best || r > best.r) best = circle(p.x, p.y, r);
474
+ }
475
+ }
476
+ if (!best) return null;
477
+ let cur = best;
478
+ let span = step;
479
+ for (let iter = 0; iter < 8; iter++) {
480
+ let improved = cur;
481
+ for (let dx = -1; dx <= 1; dx++) {
482
+ for (let dy = -1; dy <= 1; dy++) {
483
+ if (dx === 0 && dy === 0) continue;
484
+ const p = { x: cur.x + dx * span, y: cur.y + dy * span };
485
+ if (!containsPoint(path2, p)) continue;
486
+ const r = clearanceRadius(p, path2, obstacles);
487
+ if (r > improved.r) improved = circle(p.x, p.y, r);
488
+ }
489
+ }
490
+ cur = improved;
491
+ span *= 0.5;
492
+ }
493
+ return cur;
494
+ }
495
+ function clearanceRadius(p, path2, obstacles) {
496
+ let r = distanceToBoundary(p, path2);
497
+ for (const c of obstacles) {
498
+ r = Math.min(r, Math.hypot(p.x - c.x, p.y - c.y) - c.r);
499
+ }
500
+ return Math.max(0, r);
501
+ }
502
+ function distanceToBoundary(p, path2) {
503
+ let min = Infinity;
504
+ for (const ring of path2.rings) {
505
+ for (let i = 0; i < ring.length; i++) {
506
+ const a = ring[i];
507
+ const b = ring[(i + 1) % ring.length];
508
+ min = Math.min(min, pointSegmentDistance(p, a, b));
509
+ }
510
+ }
511
+ return min;
512
+ }
513
+ function pointSegmentDistance(p, a, b) {
514
+ const abx = b.x - a.x;
515
+ const aby = b.y - a.y;
516
+ const len2 = abx * abx + aby * aby;
517
+ if (len2 < 1e-18) return Math.hypot(p.x - a.x, p.y - a.y);
518
+ let t = ((p.x - a.x) * abx + (p.y - a.y) * aby) / len2;
519
+ t = Math.max(0, Math.min(1, t));
520
+ return Math.hypot(p.x - (a.x + t * abx), p.y - (a.y + t * aby));
521
+ }
522
+
523
+ // src/optimisation/index.ts
524
+ function maximumInscribedCircle(p, tolerance = 1) {
525
+ return maximumInscribedPack(p, 1, tolerance)[0] ?? null;
526
+ }
527
+
528
+ // src/contour/boundaryDistance.ts
529
+ function distanceToBoundary2(path2, p) {
530
+ let best = Infinity;
531
+ for (const ring of path2.rings) {
532
+ if (ring.length < 2) continue;
533
+ const n = ring.length;
534
+ const closed = path2.closed;
535
+ const limit = closed ? n : n - 1;
536
+ for (let i = 0; i < limit; i++) {
537
+ const a = ring[i];
538
+ const b = ring[(i + 1) % n];
539
+ best = Math.min(best, distPointSeg(p, a, b));
540
+ }
541
+ }
542
+ return best;
543
+ }
544
+ function sampleBoundary(path2, maxSegLen) {
545
+ const pts = [];
546
+ const lim = Math.max(maxSegLen, 1e-9);
547
+ for (const ring of path2.rings) {
548
+ if (ring.length < 2) continue;
549
+ const n = ring.length;
550
+ const limit = path2.closed ? n : n - 1;
551
+ for (let i = 0; i < limit; i++) {
552
+ const a = ring[i];
553
+ const b = ring[(i + 1) % n];
554
+ const len2 = Math.hypot(b.x - a.x, b.y - a.y);
555
+ const steps = Math.max(1, Math.ceil(len2 / lim));
556
+ for (let s = 0; s < steps; s++) {
557
+ const t = s / steps;
558
+ pts.push({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t });
559
+ }
560
+ }
561
+ }
562
+ return pts;
563
+ }
564
+ function distPointSeg(p, a, b) {
565
+ const dx = b.x - a.x;
566
+ const dy = b.y - a.y;
567
+ const len2 = dx * dx + dy * dy;
568
+ if (len2 < 1e-18) return Math.hypot(p.x - a.x, p.y - a.y);
569
+ let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
570
+ t = Math.max(0, Math.min(1, t));
571
+ return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
572
+ }
573
+
574
+ // src/contour/medialAxis.ts
575
+ function medialAxis(shape, axialThreshold = 0, distanceThreshold = 0, areaThreshold = 0) {
576
+ const g = buildMedialAxisGraph(shape);
577
+ if (!g) return dissolveSegments([]);
578
+ const kept = pruneEdges(g, axialThreshold, distanceThreshold, areaThreshold);
579
+ const segs = kept.map((e) => {
580
+ const a = g.nodes[e.head].position;
581
+ const b = g.nodes[e.tail].position;
582
+ return segment(a, b);
583
+ });
584
+ return dissolveSegments(segs);
585
+ }
586
+ function buildMedialAxisGraph(shape) {
587
+ if (!shape.closed || shape.rings.length === 0) return null;
588
+ const b = bounds(shape);
589
+ const span = Math.max(b.maxX - b.minX, b.maxY - b.minY, 1);
590
+ const densified = densify(shape, Math.max(span / 60, 1e-3));
591
+ const samples = sampleBoundary(densified, Math.max(span / 80, 1e-3));
592
+ if (samples.length < 3) return null;
593
+ const uniq = [];
594
+ const seen = /* @__PURE__ */ new Set();
595
+ for (const p of samples) {
596
+ const k = `${p.x.toFixed(6)},${p.y.toFixed(6)}`;
597
+ if (seen.has(k)) continue;
598
+ seen.add(k);
599
+ uniq.push(p);
600
+ }
601
+ if (uniq.length < 3) return null;
602
+ const delaunay = Delaunay.from(
603
+ uniq,
604
+ (d) => d.x,
605
+ (d) => d.y
606
+ );
607
+ const pad = span * 0.5;
608
+ const voronoi = delaunay.voronoi([
609
+ b.minX - pad,
610
+ b.minY - pad,
611
+ b.maxX + pad,
612
+ b.maxY + pad
613
+ ]);
614
+ const raw = [];
615
+ const edgeSeen = /* @__PURE__ */ new Set();
616
+ for (let i = 0; i < uniq.length; i++) {
617
+ const poly = voronoi.cellPolygon(i);
618
+ if (!poly || poly.length < 2) continue;
619
+ for (let j = 0; j < poly.length - 1; j++) {
620
+ const p0 = poly[j];
621
+ const p1 = poly[j + 1];
622
+ const a = { x: p0[0], y: p0[1] };
623
+ const bb = { x: p1[0], y: p1[1] };
624
+ if (!containsPoint(shape, a) || !containsPoint(shape, bb)) continue;
625
+ const ek = edgeKey2(a, bb);
626
+ if (edgeSeen.has(ek)) continue;
627
+ edgeSeen.add(ek);
628
+ const mid3 = { x: (a.x + bb.x) / 2, y: (a.y + bb.y) / 2 };
629
+ const site = uniq[i];
630
+ const radius = Math.hypot(mid3.x - site.x, mid3.y - site.y);
631
+ raw.push({ a, b: bb, radius });
632
+ }
633
+ }
634
+ if (raw.length === 0) return null;
635
+ const nodeIndex = /* @__PURE__ */ new Map();
636
+ const nodes = [];
637
+ const ensure = (p, radius) => {
638
+ const k = key2(p);
639
+ let id = nodeIndex.get(k);
640
+ if (id !== void 0) {
641
+ nodes[id].radius = Math.max(nodes[id].radius, radius);
642
+ return id;
643
+ }
644
+ id = nodes.length;
645
+ nodeIndex.set(k, id);
646
+ nodes.push({
647
+ id,
648
+ position: p,
649
+ radius,
650
+ parent: null,
651
+ children: [],
652
+ rootDist: 0,
653
+ featureArea: Math.PI * radius * radius
654
+ });
655
+ return id;
656
+ };
657
+ const undirected = [];
658
+ for (const e of raw) {
659
+ const midR = e.radius;
660
+ const u = ensure(e.a, midR);
661
+ const v = ensure(e.b, midR);
662
+ if (u === v) continue;
663
+ const length = Math.hypot(
664
+ nodes[u].position.x - nodes[v].position.x,
665
+ nodes[u].position.y - nodes[v].position.y
666
+ );
667
+ const axial = length > 1e-12 ? Math.abs(nodes[u].radius - nodes[v].radius) / length : 0;
668
+ undirected.push({ u, v, axial, length });
669
+ }
670
+ const mic = maximumInscribedCircle(shape, Math.max(span * 0.01, 0.1));
671
+ const rootPos = mic ? { x: mic.x, y: mic.y } : {
672
+ x: (b.minX + b.maxX) / 2,
673
+ y: (b.minY + b.maxY) / 2
674
+ };
675
+ let root = 0;
676
+ let bestD = Infinity;
677
+ for (const n of nodes) {
678
+ const d = Math.hypot(n.position.x - rootPos.x, n.position.y - rootPos.y);
679
+ if (d < bestD) {
680
+ bestD = d;
681
+ root = n.id;
682
+ }
683
+ }
684
+ const adj = /* @__PURE__ */ new Map();
685
+ for (const e of undirected) {
686
+ if (!adj.has(e.u)) adj.set(e.u, []);
687
+ if (!adj.has(e.v)) adj.set(e.v, []);
688
+ adj.get(e.u).push({ to: e.v, axial: e.axial, length: e.length });
689
+ adj.get(e.v).push({ to: e.u, axial: e.axial, length: e.length });
690
+ }
691
+ const edges = [];
692
+ const visited = /* @__PURE__ */ new Set([root]);
693
+ const queue = [root];
694
+ nodes[root].rootDist = 0;
695
+ while (queue.length) {
696
+ const u = queue.shift();
697
+ for (const link of adj.get(u) ?? []) {
698
+ if (visited.has(link.to)) continue;
699
+ visited.add(link.to);
700
+ nodes[link.to].parent = u;
701
+ nodes[u].children.push(link.to);
702
+ nodes[link.to].rootDist = nodes[u].rootDist + link.length;
703
+ edges.push({
704
+ head: u,
705
+ tail: link.to,
706
+ axial: link.axial,
707
+ length: link.length
708
+ });
709
+ queue.push(link.to);
710
+ }
711
+ }
712
+ const post = [];
713
+ const stack = [root];
714
+ const seenN = /* @__PURE__ */ new Set();
715
+ while (stack.length) {
716
+ const u = stack.pop();
717
+ if (seenN.has(u)) {
718
+ post.push(u);
719
+ continue;
720
+ }
721
+ seenN.add(u);
722
+ stack.push(u);
723
+ for (const c of nodes[u].children) stack.push(c);
724
+ }
725
+ for (const u of post) {
726
+ let area = nodes[u].featureArea;
727
+ for (const c of nodes[u].children) area += nodes[c].featureArea;
728
+ nodes[u].featureArea = area;
729
+ }
730
+ return { nodes, edges, root };
731
+ }
732
+ function pruneEdges(g, axialT, distT, areaT) {
733
+ const maxDist = Math.max(...g.nodes.map((n) => n.rootDist), 1e-9);
734
+ const maxArea = Math.max(...g.nodes.map((n) => n.featureArea), 1e-9);
735
+ const maxAxial = Math.max(...g.edges.map((e) => e.axial), 1e-9);
736
+ const aT = clamp01(axialT);
737
+ const dT = clamp01(distT);
738
+ const arT = clamp01(areaT);
739
+ return g.edges.filter((e) => {
740
+ const tail = g.nodes[e.tail];
741
+ if (aT > 0 && e.axial / maxAxial < aT * 0.15 && aT > 0.05) {
742
+ if (tail.children.length === 0 && e.axial / maxAxial < aT) return false;
743
+ }
744
+ if (dT > 0) {
745
+ const nd = tail.rootDist / maxDist;
746
+ if (tail.children.length === 0 && nd > 1 - dT && dT > 0) ;
747
+ if (tail.children.length === 0 && 1 - nd < dT * 0.5 && dT >= 0.5) {
748
+ return false;
749
+ }
750
+ if (tail.children.length === 0 && nd * dT > 0.85) return false;
751
+ }
752
+ if (arT > 0) {
753
+ const na = tail.featureArea / maxArea;
754
+ if (na < arT) return false;
755
+ }
756
+ if (aT > 0 && e.axial / maxAxial > 1 - aT * 0.5 && tail.children.length === 0) {
757
+ return false;
758
+ }
759
+ return true;
760
+ });
761
+ }
762
+ function centerLine(shape, straightnessWeighting = 0.7, smoothing = 50) {
763
+ const g = buildMedialAxisGraph(shape);
764
+ if (!g || g.nodes.length < 2) {
765
+ return polyline([]);
766
+ }
767
+ const root = g.nodes[g.root];
768
+ const childRoots = root.children;
769
+ const leavesOf = (start) => {
770
+ const out = [];
771
+ const stack = [start];
772
+ while (stack.length) {
773
+ const u = stack.pop();
774
+ const n = g.nodes[u];
775
+ if (n.children.length === 0) out.push(n);
776
+ else stack.push(...n.children);
777
+ }
778
+ return out;
779
+ };
780
+ let bestPath = [];
781
+ if (childRoots.length <= 1) {
782
+ bestPath = longestPathNodes(g);
783
+ } else if (childRoots.length === 2) {
784
+ bestPath = pathBetweenLeaves(
785
+ g,
786
+ leavesOf(childRoots[0]),
787
+ leavesOf(childRoots[1]),
788
+ root,
789
+ straightnessWeighting
790
+ );
791
+ } else {
792
+ const groups = childRoots.map(leavesOf);
793
+ let bestW = -Infinity;
794
+ for (let i = 0; i < groups.length; i++) {
795
+ for (let j = i + 1; j < groups.length; j++) {
796
+ const { path: p, weight } = bestLeafPair(
797
+ g,
798
+ groups[i],
799
+ groups[j],
800
+ root,
801
+ straightnessWeighting
802
+ );
803
+ if (weight > bestW) {
804
+ bestW = weight;
805
+ bestPath = p;
806
+ }
807
+ }
808
+ }
809
+ }
810
+ if (bestPath.length < 2) bestPath = longestPathNodes(g);
811
+ const pts = bestPath.map((id) => g.nodes[id].position);
812
+ return polyline(gaussianSmooth(pts, smoothing));
813
+ }
814
+ function bestLeafPair(g, a, b, root, straightness) {
815
+ let bestW = -Infinity;
816
+ let best = [];
817
+ for (const d1 of a) {
818
+ for (const d2 of b) {
819
+ const angle = angleBetween(d1.position, root.position, d2.position);
820
+ const aw = Math.pow(1 + angle, straightness);
821
+ const weight = (d1.rootDist + d2.rootDist) * Math.max(aw - 1, 1);
822
+ if (weight > bestW) {
823
+ bestW = weight;
824
+ best = [...pathToRoot(g, d1.id), ...pathToRoot(g, d2.id).reverse().slice(1)];
825
+ }
826
+ }
827
+ }
828
+ return { path: best, weight: bestW };
829
+ }
830
+ function pathBetweenLeaves(g, a, b, root, straightness) {
831
+ return bestLeafPair(g, a, b, root, straightness).path;
832
+ }
833
+ function pathToRoot(g, id) {
834
+ const path2 = [id];
835
+ let cur = id;
836
+ while (g.nodes[cur].parent !== null) {
837
+ cur = g.nodes[cur].parent;
838
+ path2.push(cur);
839
+ }
840
+ return path2;
841
+ }
842
+ function longestPathNodes(g) {
843
+ const leaves = g.nodes.filter((n) => n.children.length === 0 && n.id !== g.root);
844
+ if (leaves.length === 0) return g.nodes.map((n) => n.id);
845
+ let farthest = leaves[0];
846
+ let best = -1;
847
+ for (const L of leaves) {
848
+ if (L.rootDist > best) {
849
+ best = L.rootDist;
850
+ farthest = L;
851
+ }
852
+ }
853
+ let bestPath = pathToRoot(g, farthest.id);
854
+ let bestLen = farthest.rootDist;
855
+ for (const L of leaves) {
856
+ if (L.id === farthest.id) continue;
857
+ const p = [
858
+ ...pathToRoot(g, farthest.id),
859
+ ...pathToRoot(g, L.id).reverse().slice(1)
860
+ ];
861
+ const len2 = farthest.rootDist + L.rootDist;
862
+ if (len2 > bestLen) {
863
+ bestLen = len2;
864
+ bestPath = p;
865
+ }
866
+ }
867
+ return bestPath;
868
+ }
869
+ function angleBetween(a, apex, b) {
870
+ const ax = a.x - apex.x;
871
+ const ay = a.y - apex.y;
872
+ const bx = b.x - apex.x;
873
+ const by = b.y - apex.y;
874
+ const la = Math.hypot(ax, ay) || 1;
875
+ const lb = Math.hypot(bx, by) || 1;
876
+ const cos = Math.max(-1, Math.min(1, (ax * bx + ay * by) / (la * lb)));
877
+ return Math.acos(cos);
878
+ }
879
+ function gaussianSmooth(pts, sigma) {
880
+ if (pts.length < 3 || sigma <= 0) return pts;
881
+ const radius = Math.max(1, Math.round(Math.min(pts.length / 4, sigma / 10)));
882
+ const out = [];
883
+ for (let i = 0; i < pts.length; i++) {
884
+ let sx = 0;
885
+ let sy = 0;
886
+ let w = 0;
887
+ for (let k = -radius; k <= radius; k++) {
888
+ const j = Math.max(0, Math.min(pts.length - 1, i + k));
889
+ const wk = Math.exp(-(k * k) / (2 * (radius * 0.5) ** 2 + 1e-9));
890
+ sx += pts[j].x * wk;
891
+ sy += pts[j].y * wk;
892
+ w += wk;
893
+ }
894
+ out.push({ x: sx / w, y: sy / w });
895
+ }
896
+ out[0] = pts[0];
897
+ out[out.length - 1] = pts[pts.length - 1];
898
+ return out;
899
+ }
900
+ function key2(p) {
901
+ return `${p.x.toFixed(7)},${p.y.toFixed(7)}`;
902
+ }
903
+ function edgeKey2(a, b) {
904
+ const ka = key2(a);
905
+ const kb = key2(b);
906
+ return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
907
+ }
908
+ function clamp01(v) {
909
+ return Math.max(0, Math.min(1, v));
910
+ }
911
+
912
+ // src/contour/straightSkeleton.ts
913
+ function straightSkeleton(shape) {
914
+ const parts = straightSkeletonParts(shape);
915
+ return group([
916
+ ...parts.faces.paths,
917
+ ...parts.branches.paths,
918
+ ...parts.bones.paths
919
+ ]);
920
+ }
921
+ function straightSkeletonParts(shape) {
922
+ const empty = {
923
+ faces: group([]),
924
+ branches: group([]),
925
+ bones: group([])
926
+ };
927
+ if (!shape.closed || !shape.rings[0] || shape.rings[0].length < 3) {
928
+ return empty;
929
+ }
930
+ const b = bounds(shape);
931
+ const span = Math.max(b.maxX - b.minX, b.maxY - b.minY, 1);
932
+ const delta = Math.max(span / 40, 0.5);
933
+ const densified = densify(shape, Math.max(span / 40, 1e-3));
934
+ const levels = [densified];
935
+ let current = densified;
936
+ for (let i = 0; i < 40; i++) {
937
+ const next = buffer(current, -delta);
938
+ if (next.paths.length === 0) break;
939
+ let best = next.paths[0];
940
+ let bestA = -1;
941
+ for (const p of next.paths) {
942
+ const bb = bounds(p);
943
+ const a = (bb.maxX - bb.minX) * (bb.maxY - bb.minY);
944
+ if (a > bestA) {
945
+ bestA = a;
946
+ best = p;
947
+ }
948
+ }
949
+ if (!best.rings[0] || best.rings[0].length < 3) break;
950
+ levels.push(best);
951
+ current = best;
952
+ }
953
+ const boneSegs = [];
954
+ for (let i = 0; i < levels.length - 1; i++) {
955
+ const a = levels[i];
956
+ const bb = levels[i + 1];
957
+ const ca = centroid(a);
958
+ const cb = centroid(bb);
959
+ if (containsPoint(shape, mid2(ca, cb))) {
960
+ boneSegs.push(segment(ca, cb));
961
+ }
962
+ for (const ring of a.rings) {
963
+ for (const p of ring) {
964
+ const q = nearestOnPath(bb, p);
965
+ if (q && containsPoint(shape, mid2(p, q))) {
966
+ boneSegs.push(segment(p, q));
967
+ }
968
+ }
969
+ }
970
+ }
971
+ const branchSegs = [];
972
+ const outer = densified.rings[0];
973
+ const target = levels[1] ?? levels[0];
974
+ for (let i = 0; i < outer.length; i++) {
975
+ const a = outer[i];
976
+ const bb = outer[(i + 1) % outer.length];
977
+ const m = mid2(a, bb);
978
+ const q = nearestOnPath(target, m);
979
+ if (q) branchSegs.push(segment(m, q));
980
+ }
981
+ const faces = [];
982
+ if (levels.length >= 2) {
983
+ const inner = levels[1].rings[0] ?? [];
984
+ for (let i = 0; i < outer.length; i++) {
985
+ const a = outer[i];
986
+ const bb = outer[(i + 1) % outer.length];
987
+ const ia = nearestOnRing(inner, a);
988
+ const ib = nearestOnRing(inner, bb);
989
+ if (ia && ib) {
990
+ const face = [a, bb, ib, ia];
991
+ const c = {
992
+ x: (a.x + bb.x + ib.x + ia.x) / 4,
993
+ y: (a.y + bb.y + ib.y + ia.y) / 4
994
+ };
995
+ if (containsPoint(shape, c)) faces.push(polygon(face));
996
+ }
997
+ }
998
+ }
999
+ return {
1000
+ faces: group(faces),
1001
+ branches: dissolveSegments(branchSegs),
1002
+ bones: dissolveSegments(boneSegs)
1003
+ };
1004
+ }
1005
+ function mid2(a, b) {
1006
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1007
+ }
1008
+ function nearestOnPath(p, q) {
1009
+ let best = null;
1010
+ let bestD = Infinity;
1011
+ for (const ring of p.rings) {
1012
+ for (const v of ring) {
1013
+ const d = Math.hypot(v.x - q.x, v.y - q.y);
1014
+ if (d < bestD) {
1015
+ bestD = d;
1016
+ best = v;
1017
+ }
1018
+ }
1019
+ }
1020
+ return best;
1021
+ }
1022
+ function nearestOnRing(ring, q) {
1023
+ if (ring.length === 0) return null;
1024
+ let best = ring[0];
1025
+ let bestD = Infinity;
1026
+ for (const v of ring) {
1027
+ const d = Math.hypot(v.x - q.x, v.y - q.y);
1028
+ if (d < bestD) {
1029
+ bestD = d;
1030
+ best = v;
1031
+ }
1032
+ }
1033
+ return best;
1034
+ }
1035
+
1036
+ // src/contour/marchingSquares.ts
1037
+ function isolinesFromFunction(bounds2, sampleSpacing, contourInterval, fn, isolineMin = Number.NaN, isolineMax = Number.NaN) {
1038
+ const [xmin, ymin, xmax, ymax] = bounds2;
1039
+ const dx = Math.max(sampleSpacing, 1e-9);
1040
+ const dy = dx;
1041
+ const nx = Math.max(2, Math.ceil((xmax - xmin) / dx) + 1);
1042
+ const ny = Math.max(2, Math.ceil((ymax - ymin) / dy) + 1);
1043
+ const grid = [];
1044
+ let gmin = Infinity;
1045
+ let gmax = -Infinity;
1046
+ for (let j = 0; j < ny; j++) {
1047
+ const row = [];
1048
+ const y = ymin + j * ((ymax - ymin) / (ny - 1));
1049
+ for (let i = 0; i < nx; i++) {
1050
+ const x = xmin + i * ((xmax - xmin) / (nx - 1));
1051
+ let v = fn(x, y);
1052
+ if (!Number.isFinite(v)) v = Number.NEGATIVE_INFINITY;
1053
+ row.push(v);
1054
+ if (Number.isFinite(v)) {
1055
+ gmin = Math.min(gmin, v);
1056
+ gmax = Math.max(gmax, v);
1057
+ }
1058
+ }
1059
+ grid.push(row);
1060
+ }
1061
+ const z0 = Number.isFinite(isolineMin) ? isolineMin : gmin;
1062
+ const z1 = Number.isFinite(isolineMax) ? isolineMax : gmax;
1063
+ if (!Number.isFinite(z0) || !Number.isFinite(z1) || contourInterval <= 0) {
1064
+ return group([]);
1065
+ }
1066
+ const levels = [];
1067
+ const start = Math.ceil(z0 / contourInterval) * contourInterval;
1068
+ for (let z = start; z <= z1 + 1e-12; z += contourInterval) {
1069
+ levels.push(z);
1070
+ }
1071
+ const segs = [];
1072
+ for (const level of levels) {
1073
+ segs.push(...marchLevel(grid, xmin, ymin, xmax, ymax, nx, ny, level));
1074
+ }
1075
+ return dissolveSegments(segs);
1076
+ }
1077
+ function isolineZeroFromFunction(bounds2, sampleSpacing, fn) {
1078
+ return isolinesFromFunction(bounds2, sampleSpacing, 1, fn, 0, 0);
1079
+ }
1080
+ function marchLevel(grid, xmin, ymin, xmax, ymax, nx, ny, level) {
1081
+ const segs = [];
1082
+ const xAt = (i) => xmin + i / (nx - 1) * (xmax - xmin);
1083
+ const yAt = (j) => ymin + j / (ny - 1) * (ymax - ymin);
1084
+ const lerp = (x0, y0, v0, x1, y1, v1) => {
1085
+ const t = (level - v0) / (v1 - v0 || 1e-15);
1086
+ return { x: x0 + t * (x1 - x0), y: y0 + t * (y1 - y0) };
1087
+ };
1088
+ for (let j = 0; j < ny - 1; j++) {
1089
+ for (let i = 0; i < nx - 1; i++) {
1090
+ const v00 = grid[j][i];
1091
+ const v10 = grid[j][i + 1];
1092
+ const v11 = grid[j + 1][i + 1];
1093
+ const v01 = grid[j + 1][i];
1094
+ const x0 = xAt(i);
1095
+ const x1 = xAt(i + 1);
1096
+ const y0 = yAt(j);
1097
+ const y1 = yAt(j + 1);
1098
+ let code = 0;
1099
+ if (v00 >= level) code |= 1;
1100
+ if (v10 >= level) code |= 2;
1101
+ if (v11 >= level) code |= 4;
1102
+ if (v01 >= level) code |= 8;
1103
+ if (code === 0 || code === 15) continue;
1104
+ const bottom = () => lerp(x0, y0, v00, x1, y0, v10);
1105
+ const right = () => lerp(x1, y0, v10, x1, y1, v11);
1106
+ const top = () => lerp(x0, y1, v01, x1, y1, v11);
1107
+ const left = () => lerp(x0, y0, v00, x0, y1, v01);
1108
+ const add = (a, b) => segs.push(segment(a, b));
1109
+ switch (code) {
1110
+ case 1:
1111
+ case 14:
1112
+ add(left(), bottom());
1113
+ break;
1114
+ case 2:
1115
+ case 13:
1116
+ add(bottom(), right());
1117
+ break;
1118
+ case 3:
1119
+ case 12:
1120
+ add(left(), right());
1121
+ break;
1122
+ case 4:
1123
+ case 11:
1124
+ add(right(), top());
1125
+ break;
1126
+ case 6:
1127
+ case 9:
1128
+ add(bottom(), top());
1129
+ break;
1130
+ case 7:
1131
+ case 8:
1132
+ add(left(), top());
1133
+ break;
1134
+ case 5: {
1135
+ const avg = (v00 + v10 + v11 + v01) / 4;
1136
+ if (avg >= level) {
1137
+ add(left(), top());
1138
+ add(bottom(), right());
1139
+ } else {
1140
+ add(left(), bottom());
1141
+ add(right(), top());
1142
+ }
1143
+ break;
1144
+ }
1145
+ case 10: {
1146
+ const avg = (v00 + v10 + v11 + v01) / 4;
1147
+ if (avg >= level) {
1148
+ add(left(), bottom());
1149
+ add(right(), top());
1150
+ } else {
1151
+ add(left(), top());
1152
+ add(bottom(), right());
1153
+ }
1154
+ break;
1155
+ }
1156
+ }
1157
+ }
1158
+ }
1159
+ return segs;
1160
+ }
1161
+ function isolinesFromPoints(points, values, intervals, _smoothing = 0) {
1162
+ if (points.length < 3 || intervals < 1) return group([]);
1163
+ let minZ = Infinity;
1164
+ let maxZ = -Infinity;
1165
+ for (const v of values) {
1166
+ minZ = Math.min(minZ, v);
1167
+ maxZ = Math.max(maxZ, v);
1168
+ }
1169
+ if (!Number.isFinite(minZ) || maxZ - minZ < 1e-15) return group([]);
1170
+ let minX = Infinity;
1171
+ let minY = Infinity;
1172
+ let maxX = -Infinity;
1173
+ let maxY = -Infinity;
1174
+ for (const p of points) {
1175
+ minX = Math.min(minX, p.x);
1176
+ minY = Math.min(minY, p.y);
1177
+ maxX = Math.max(maxX, p.x);
1178
+ maxY = Math.max(maxY, p.y);
1179
+ }
1180
+ const span = Math.max(maxX - minX, maxY - minY, 1);
1181
+ const sampleSpacing = span / Math.max(20, Math.sqrt(points.length));
1182
+ const interval = (maxZ - minZ) / intervals;
1183
+ const fn = (x, y) => {
1184
+ let num = 0;
1185
+ let den = 0;
1186
+ for (let i = 0; i < points.length; i++) {
1187
+ const p = points[i];
1188
+ const d2 = (x - p.x) ** 2 + (y - p.y) ** 2;
1189
+ const w = 1 / Math.max(d2, 1e-12);
1190
+ num += w * values[i];
1191
+ den += w;
1192
+ }
1193
+ return num / den;
1194
+ };
1195
+ return isolinesFromFunction(
1196
+ [minX, minY, maxX, maxY],
1197
+ sampleSpacing,
1198
+ interval,
1199
+ fn,
1200
+ minZ,
1201
+ maxZ
1202
+ );
1203
+ }
1204
+
1205
+ // src/contour/fields.ts
1206
+ function distanceField(shape, spacing, pole) {
1207
+ if (!shape.closed || shape.rings.length === 0 || spacing <= 0) return group([]);
1208
+ const b = bounds(shape);
1209
+ const span = Math.max(b.maxX - b.minX, b.maxY - b.minY, 1);
1210
+ let polePt = pole;
1211
+ if (!polePt) {
1212
+ const mic = maximumInscribedCircle(shape, Math.max(span * 0.01, 0.1));
1213
+ polePt = mic ? { x: mic.x, y: mic.y } : { x: (b.minX + b.maxX) / 2, y: (b.minY + b.maxY) / 2 };
1214
+ }
1215
+ const pad = Math.max(spacing, 1);
1216
+ const box = [
1217
+ b.minX - pad,
1218
+ b.minY - pad,
1219
+ b.maxX + pad,
1220
+ b.maxY + pad
1221
+ ];
1222
+ const sampleSpacing = Math.max(spacing / 10, span / 80);
1223
+ const fn = (x, y) => {
1224
+ const p = { x, y };
1225
+ if (!insideSafe(shape, p)) return Number.NEGATIVE_INFINITY;
1226
+ const dGeo = distanceToBoundary2(shape, p);
1227
+ const dPoint = Math.hypot(x - polePt.x, y - polePt.y);
1228
+ return dGeo - dPoint;
1229
+ };
1230
+ const raw = isolinesFromFunction(box, sampleSpacing, spacing, fn);
1231
+ return clipPolylinesToPath(raw, shape);
1232
+ }
1233
+ function contrastField(shape, intervals, reference) {
1234
+ if (!shape.closed || shape.rings.length === 0 || intervals < 1) {
1235
+ return group([]);
1236
+ }
1237
+ const b = bounds(shape);
1238
+ const areaApprox = (b.maxX - b.minX) * (b.maxY - b.minY);
1239
+ const count = Math.max(100, Math.floor(areaApprox / 100));
1240
+ const minDist = Math.sqrt(areaApprox / (count * 1.5));
1241
+ const samples = poisson(
1242
+ Math.max(minDist, 1e-3),
1243
+ b.minX,
1244
+ b.minY,
1245
+ b.maxX,
1246
+ b.maxY,
1247
+ 1337
1248
+ ).filter((p) => containsPoint(shape, p));
1249
+ if (samples.length < 3) return group([]);
1250
+ const values = samples.map((p) => {
1251
+ const dB = distanceToBoundary2(shape, p);
1252
+ const dR = Math.hypot(p.x - reference.x, p.y - reference.y);
1253
+ return Math.abs(dB - dR);
1254
+ });
1255
+ const lines = isolinesFromPoints(samples, values, Math.max(1, intervals), 0);
1256
+ return clipPolylinesToPath(lines, shape);
1257
+ }
1258
+ function isolines(shape, highPoint, intervalSpacing) {
1259
+ if (!shape.closed || intervalSpacing <= 0) return group([]);
1260
+ const b = bounds(shape);
1261
+ const span = Math.max(b.maxX - b.minX, b.maxY - b.minY, 1);
1262
+ const pad = intervalSpacing;
1263
+ const box = [
1264
+ b.minX - pad,
1265
+ b.minY - pad,
1266
+ b.maxX + pad,
1267
+ b.maxY + pad
1268
+ ];
1269
+ const fn = (x, y) => {
1270
+ if (!containsPoint(shape, { x, y })) return -1;
1271
+ return Math.hypot(x - highPoint.x, y - highPoint.y);
1272
+ };
1273
+ const raw = isolinesFromFunction(
1274
+ box,
1275
+ Math.max(intervalSpacing / 8, span / 100),
1276
+ intervalSpacing,
1277
+ fn,
1278
+ 0,
1279
+ span
1280
+ );
1281
+ return clipPolylinesToPath(raw, shape);
1282
+ }
1283
+ function clipPolylinesToPath(g, shape) {
1284
+ const paths = [];
1285
+ for (const p of g.paths) {
1286
+ const ring = p.rings[0];
1287
+ if (!ring || ring.length < 2) continue;
1288
+ let current = [];
1289
+ const flush = () => {
1290
+ if (current.length >= 2) {
1291
+ paths.push({ rings: [current], closed: false });
1292
+ }
1293
+ current = [];
1294
+ };
1295
+ for (let i = 0; i < ring.length - 1; i++) {
1296
+ const a = ring[i];
1297
+ const b = ring[i + 1];
1298
+ if (!finitePt(a) || !finitePt(b)) {
1299
+ flush();
1300
+ continue;
1301
+ }
1302
+ const mid3 = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1303
+ if (insideSafe(shape, mid3)) {
1304
+ if (current.length === 0) current.push(a);
1305
+ current.push(b);
1306
+ } else {
1307
+ flush();
1308
+ }
1309
+ }
1310
+ flush();
1311
+ }
1312
+ return group(paths);
1313
+ }
1314
+ function finitePt(p) {
1315
+ return Number.isFinite(p.x) && Number.isFinite(p.y);
1316
+ }
1317
+ function insideSafe(shape, point) {
1318
+ if (!finitePt(point)) return false;
1319
+ try {
1320
+ return containsPoint(shape, point);
1321
+ } catch {
1322
+ return pointInPoly(point, shape.rings[0]) && shape.rings.slice(1).every((h) => !pointInPoly(point, h));
1323
+ }
1324
+ }
1325
+ function pointInPoly(point, ring) {
1326
+ let inside = false;
1327
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
1328
+ const pi = ring[i];
1329
+ const pj = ring[j];
1330
+ if (pi.y > point.y !== pj.y > point.y && point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y + 1e-15) + pi.x) {
1331
+ inside = !inside;
1332
+ }
1333
+ }
1334
+ return inside;
1335
+ }
1336
+
1337
+ // src/contour/distanceTree.ts
1338
+ function key3(v) {
1339
+ return `${v.x.toFixed(8)},${v.y.toFixed(8)}`;
1340
+ }
1341
+ function distanceTree(mesh, source, flatten) {
1342
+ const adj = /* @__PURE__ */ new Map();
1343
+ const pts = /* @__PURE__ */ new Map();
1344
+ const addEdge = (a, b) => {
1345
+ const ka = key3(a);
1346
+ const kb = key3(b);
1347
+ if (ka === kb) return;
1348
+ pts.set(ka, a);
1349
+ pts.set(kb, b);
1350
+ if (!adj.has(ka)) adj.set(ka, []);
1351
+ if (!adj.has(kb)) adj.set(kb, []);
1352
+ adj.get(ka).push({ to: kb, pt: b });
1353
+ adj.get(kb).push({ to: ka, pt: a });
1354
+ };
1355
+ for (const face of mesh.paths) {
1356
+ for (const ring of face.rings) {
1357
+ if (ring.length < 2) continue;
1358
+ const n = ring.length;
1359
+ const closed = face.closed;
1360
+ const limit = closed ? n : n - 1;
1361
+ for (let i = 0; i < limit; i++) {
1362
+ addEdge(ring[i], ring[(i + 1) % n]);
1363
+ }
1364
+ }
1365
+ }
1366
+ if (pts.size === 0) return group([]);
1367
+ let sourceKey = "";
1368
+ let best = Infinity;
1369
+ for (const [k, p] of pts) {
1370
+ const d = Math.hypot(p.x - source.x, p.y - source.y);
1371
+ if (d < best) {
1372
+ best = d;
1373
+ sourceKey = k;
1374
+ }
1375
+ }
1376
+ const parent = /* @__PURE__ */ new Map();
1377
+ parent.set(sourceKey, null);
1378
+ const q = [sourceKey];
1379
+ while (q.length) {
1380
+ const u = q.shift();
1381
+ for (const { to } of adj.get(u) ?? []) {
1382
+ if (parent.has(to)) continue;
1383
+ parent.set(to, u);
1384
+ q.push(to);
1385
+ }
1386
+ }
1387
+ if (flatten) {
1388
+ const segs = [];
1389
+ const seen = /* @__PURE__ */ new Set();
1390
+ for (const [v, p] of parent) {
1391
+ if (p === null) continue;
1392
+ const ek = v < p ? `${v}|${p}` : `${p}|${v}`;
1393
+ if (seen.has(ek)) continue;
1394
+ seen.add(ek);
1395
+ segs.push(segment(pts.get(p), pts.get(v)));
1396
+ }
1397
+ return dissolveSegments(segs);
1398
+ }
1399
+ const paths = [];
1400
+ for (const [v] of parent) {
1401
+ if (v === sourceKey) continue;
1402
+ const chain = [];
1403
+ let cur = v;
1404
+ while (cur !== null) {
1405
+ chain.push(pts.get(cur));
1406
+ cur = parent.get(cur) ?? null;
1407
+ }
1408
+ chain.reverse();
1409
+ if (chain.length >= 2) paths.push(polyline(chain));
1410
+ }
1411
+ return group(paths);
1412
+ }
1413
+
104
1414
  // src/contour/index.ts
105
1415
  function offsetCurvesOutward(p, distance) {
106
1416
  return buffer(p, Math.abs(distance));
@@ -110,9 +1420,22 @@ function offsetCurvesInward(p, distance) {
110
1420
  }
111
1421
  var contour = {
112
1422
  offsetCurvesOutward,
113
- offsetCurvesInward
1423
+ offsetCurvesInward,
1424
+ medialAxis,
1425
+ chordalAxis,
1426
+ straightSkeleton,
1427
+ straightSkeletonParts,
1428
+ centerLine,
1429
+ distanceField,
1430
+ contrastField,
1431
+ distanceTree,
1432
+ isolines,
1433
+ isolinesFromFunction,
1434
+ isolineZeroFromFunction,
1435
+ isolinesFromPoints,
1436
+ dissolveSegments
114
1437
  };
115
1438
 
116
- export { contour, offsetCurvesInward, offsetCurvesOutward };
1439
+ export { centerLine, chordalAxis, contour, contrastField, dissolveSegments, distanceField, distanceTree, isolineZeroFromFunction, isolines, isolinesFromFunction, isolinesFromPoints, medialAxis, offsetCurvesInward, offsetCurvesOutward, straightSkeleton, straightSkeletonParts };
117
1440
  //# sourceMappingURL=index.js.map
118
1441
  //# sourceMappingURL=index.js.map