@codexo/exojs 0.6.9 → 0.6.10

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/exo.esm.js CHANGED
@@ -706,7 +706,7 @@ const trimRotation = (degrees) => {
706
706
  const degreesToRadians = (degree) => degree * radiansPerDegree;
707
707
  const radiansToDegrees = (radian) => radian * degreesPerRadian;
708
708
  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
709
- const sign$1 = (value) => (value && (value < 0 ? -1 : 1));
709
+ const sign = (value) => (value && (value < 0 ? -1 : 1));
710
710
  const lerp = (startValue, endValue, ratio) => (((1 - ratio) * startValue) + (ratio * endValue));
711
711
  const isPowerOfTwo = (value) => ((value !== 0) && ((value & (value - 1)) === 0));
712
712
  const inRange = (value, min, max) => (value >= Math.min(min, max) && value <= Math.max(min, max));
@@ -15452,636 +15452,166 @@ class Input {
15452
15452
  }
15453
15453
  }
15454
15454
 
15455
- function earcut(data, holeIndices, dim = 2) {
15456
-
15457
- const hasHoles = holeIndices && holeIndices.length;
15458
- const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
15459
- let outerNode = linkedList(data, 0, outerLen, dim, true);
15460
- const triangles = [];
15461
-
15462
- if (!outerNode || outerNode.next === outerNode.prev) return triangles;
15463
-
15464
- let minX, minY, invSize;
15465
-
15466
- if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
15467
-
15468
- // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
15469
- if (data.length > 80 * dim) {
15470
- minX = data[0];
15471
- minY = data[1];
15472
- let maxX = minX;
15473
- let maxY = minY;
15474
-
15475
- for (let i = dim; i < outerLen; i += dim) {
15476
- const x = data[i];
15477
- const y = data[i + 1];
15478
- if (x < minX) minX = x;
15479
- if (y < minY) minY = y;
15480
- if (x > maxX) maxX = x;
15481
- if (y > maxY) maxY = y;
15482
- }
15483
-
15484
- // minX, minY and invSize are later used to transform coords into integers for z-order calculation
15485
- invSize = Math.max(maxX - minX, maxY - minY);
15486
- invSize = invSize !== 0 ? 32767 / invSize : 0;
15487
- }
15488
-
15489
- earcutLinked(outerNode, triangles, dim, minX, minY, invSize, 0);
15490
-
15491
- return triangles;
15492
- }
15493
-
15494
- // create a circular doubly linked list from polygon points in the specified winding order
15495
- function linkedList(data, start, end, dim, clockwise) {
15496
- let last;
15497
-
15498
- if (clockwise === (signedArea(data, start, end, dim) > 0)) {
15499
- for (let i = start; i < end; i += dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
15500
- } else {
15501
- for (let i = end - dim; i >= start; i -= dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
15502
- }
15503
-
15504
- if (last && equals(last, last.next)) {
15505
- removeNode(last);
15506
- last = last.next;
15507
- }
15508
-
15509
- return last;
15510
- }
15511
-
15512
- // eliminate colinear or duplicate points
15513
- function filterPoints(start, end) {
15514
- if (!start) return start;
15515
- if (!end) end = start;
15516
-
15517
- let p = start,
15518
- again;
15519
- do {
15520
- again = false;
15521
-
15522
- if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
15523
- removeNode(p);
15524
- p = end = p.prev;
15525
- if (p === p.next) break;
15526
- again = true;
15527
-
15528
- } else {
15529
- p = p.next;
15530
- }
15531
- } while (again || p !== end);
15532
-
15533
- return end;
15534
- }
15535
-
15536
- // main ear slicing loop which triangulates a polygon (given as a linked list)
15537
- function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
15538
- if (!ear) return;
15539
-
15540
- // interlink polygon nodes in z-order
15541
- if (!pass && invSize) indexCurve(ear, minX, minY, invSize);
15542
-
15543
- let stop = ear;
15544
-
15545
- // iterate through ears, slicing them one by one
15546
- while (ear.prev !== ear.next) {
15547
- const prev = ear.prev;
15548
- const next = ear.next;
15549
-
15550
- if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {
15551
- triangles.push(prev.i, ear.i, next.i); // cut off the triangle
15552
-
15553
- removeNode(ear);
15554
-
15555
- // skipping the next vertex leads to less sliver triangles
15556
- ear = next.next;
15557
- stop = next.next;
15558
-
15559
- continue;
15560
- }
15561
-
15562
- ear = next;
15563
-
15564
- // if we looped through the whole remaining polygon and can't find any more ears
15565
- if (ear === stop) {
15566
- // try filtering points and slicing again
15567
- if (!pass) {
15568
- earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1);
15569
-
15570
- // if this didn't work, try curing all small self-intersections locally
15571
- } else if (pass === 1) {
15572
- ear = cureLocalIntersections(filterPoints(ear), triangles);
15573
- earcutLinked(ear, triangles, dim, minX, minY, invSize, 2);
15574
-
15575
- // as a last resort, try splitting the remaining polygon into two
15576
- } else if (pass === 2) {
15577
- splitEarcut(ear, triangles, dim, minX, minY, invSize);
15455
+ /**
15456
+ * Triangulate a simple 2D polygon by ear-clipping.
15457
+ *
15458
+ * Input: `vertices` is a flat sequence of (x, y) pairs (length must be even,
15459
+ * minimum 6 = 3 vertices). Polygon may be CW or CCW; the algorithm normalises
15460
+ * to CCW internally.
15461
+ *
15462
+ * Output: a `Uint32Array` of indices in groups of 3 (per triangle), referencing
15463
+ * vertex positions (the i-th vertex spans `vertices[2*i], vertices[2*i + 1]`).
15464
+ *
15465
+ * Polygons that are degenerate (all collinear), zero-area, or self-intersecting
15466
+ * may produce incomplete output but must not throw or hang.
15467
+ *
15468
+ * @param vertices flat (x, y) pairs
15469
+ * @returns triangle index list (length is multiple of 3)
15470
+ */
15471
+ function triangulate(vertices) {
15472
+ const n = vertices.length >> 1;
15473
+ if (n < 3) {
15474
+ return new Uint32Array(0);
15475
+ }
15476
+ if (n === 3) {
15477
+ // Return in CCW order; swap if input triangle is CW.
15478
+ const ax = vertices[0], ay = vertices[1];
15479
+ const bx = vertices[2], by = vertices[3];
15480
+ const cx = vertices[4], cy = vertices[5];
15481
+ return isCcwTriangle(ax, ay, bx, by, cx, cy)
15482
+ ? new Uint32Array([0, 1, 2])
15483
+ : new Uint32Array([2, 1, 0]);
15484
+ }
15485
+ // Build doubly-linked list of vertex indices.
15486
+ const prev = new Uint32Array(n);
15487
+ const next = new Uint32Array(n);
15488
+ for (let i = 0; i < n; i++) {
15489
+ prev[i] = (i + n - 1) % n;
15490
+ next[i] = (i + 1) % n;
15491
+ }
15492
+ // Normalise to CCW: compute signed area; if negative (CW), reverse the list.
15493
+ if (signedArea(vertices) < 0) {
15494
+ for (let i = 0; i < n; i++) {
15495
+ const tmp = prev[i];
15496
+ prev[i] = next[i];
15497
+ next[i] = tmp;
15498
+ }
15499
+ }
15500
+ const maxTriangles = n - 2;
15501
+ const out = new Uint32Array(maxTriangles * 3);
15502
+ let outIdx = 0;
15503
+ let remaining = n;
15504
+ let current = 0;
15505
+ // Ear-clipping: at most O(n^2) iterations; bail after one full pass finds no ear.
15506
+ while (remaining > 3) {
15507
+ // Try each remaining vertex as a potential ear, one full pass at a time.
15508
+ let earFound = false;
15509
+ const passStart = current;
15510
+ do {
15511
+ const p = prev[current];
15512
+ const nx = next[current];
15513
+ const v = current;
15514
+ const ax = vertices[p * 2], ay = vertices[p * 2 + 1];
15515
+ const bx = vertices[v * 2], by = vertices[v * 2 + 1];
15516
+ const cx = vertices[nx * 2], cy = vertices[nx * 2 + 1];
15517
+ if (isCcwTriangle(ax, ay, bx, by, cx, cy) && isEar(vertices, prev, next, p, v, nx)) {
15518
+ // Emit triangle (prev, v, next).
15519
+ out[outIdx++] = p;
15520
+ out[outIdx++] = v;
15521
+ out[outIdx++] = nx;
15522
+ // Remove v from the list.
15523
+ next[p] = nx;
15524
+ prev[nx] = p;
15525
+ remaining--;
15526
+ earFound = true;
15527
+ current = nx;
15528
+ break;
15578
15529
  }
15579
-
15530
+ current = next[current];
15531
+ } while (current !== passStart);
15532
+ if (!earFound) {
15533
+ // Degenerate polygon: no ear found in a full pass; bail out.
15580
15534
  break;
15581
15535
  }
15582
15536
  }
15583
- }
15584
-
15585
- // check whether a polygon node forms a valid ear with adjacent nodes
15586
- function isEar(ear) {
15587
- const a = ear.prev,
15588
- b = ear,
15589
- c = ear.next;
15590
-
15591
- if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
15592
-
15593
- // now make sure we don't have other points inside the potential ear
15594
- const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;
15595
-
15596
- // triangle bbox
15597
- const x0 = Math.min(ax, bx, cx),
15598
- y0 = Math.min(ay, by, cy),
15599
- x1 = Math.max(ax, bx, cx),
15600
- y1 = Math.max(ay, by, cy);
15601
-
15602
- let p = c.next;
15603
- while (p !== a) {
15604
- if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 &&
15605
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) &&
15606
- area(p.prev, p, p.next) >= 0) return false;
15607
- p = p.next;
15608
- }
15609
-
15610
- return true;
15611
- }
15612
-
15613
- function isEarHashed(ear, minX, minY, invSize) {
15614
- const a = ear.prev,
15615
- b = ear,
15616
- c = ear.next;
15617
-
15618
- if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
15619
-
15620
- const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;
15621
-
15622
- // triangle bbox
15623
- const x0 = Math.min(ax, bx, cx),
15624
- y0 = Math.min(ay, by, cy),
15625
- x1 = Math.max(ax, bx, cx),
15626
- y1 = Math.max(ay, by, cy);
15627
-
15628
- // z-order range for the current triangle bbox;
15629
- const minZ = zOrder(x0, y0, minX, minY, invSize),
15630
- maxZ = zOrder(x1, y1, minX, minY, invSize);
15631
-
15632
- let p = ear.prevZ,
15633
- n = ear.nextZ;
15634
-
15635
- // look for points inside the triangle in both directions
15636
- while (p && p.z >= minZ && n && n.z <= maxZ) {
15637
- if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
15638
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
15639
- p = p.prevZ;
15640
-
15641
- if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
15642
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
15643
- n = n.nextZ;
15644
- }
15645
-
15646
- // look for remaining points in decreasing z-order
15647
- while (p && p.z >= minZ) {
15648
- if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
15649
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
15650
- p = p.prevZ;
15651
- }
15652
-
15653
- // look for remaining points in increasing z-order
15654
- while (n && n.z <= maxZ) {
15655
- if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
15656
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
15657
- n = n.nextZ;
15658
- }
15659
-
15660
- return true;
15661
- }
15662
-
15663
- // go through all polygon nodes and cure small local self-intersections
15664
- function cureLocalIntersections(start, triangles) {
15665
- let p = start;
15666
- do {
15667
- const a = p.prev,
15668
- b = p.next.next;
15669
-
15670
- if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
15671
-
15672
- triangles.push(a.i, p.i, b.i);
15673
-
15674
- // remove two nodes involved
15675
- removeNode(p);
15676
- removeNode(p.next);
15677
-
15678
- p = start = b;
15537
+ // Emit the final triangle if exactly 3 vertices remain.
15538
+ // Emit in CCW order (the linked-list direction may be CW after prior clips on concave polygons).
15539
+ if (remaining === 3) {
15540
+ const pa = prev[current];
15541
+ const nc = next[current];
15542
+ const ax = vertices[pa * 2], ay = vertices[pa * 2 + 1];
15543
+ const bx = vertices[current * 2], by = vertices[current * 2 + 1];
15544
+ const cx = vertices[nc * 2], cy = vertices[nc * 2 + 1];
15545
+ if (isCcwTriangle(ax, ay, bx, by, cx, cy)) {
15546
+ out[outIdx++] = pa;
15547
+ out[outIdx++] = current;
15548
+ out[outIdx++] = nc;
15679
15549
  }
15680
- p = p.next;
15681
- } while (p !== start);
15682
-
15683
- return filterPoints(p);
15684
- }
15685
-
15686
- // try splitting polygon into two and triangulate them independently
15687
- function splitEarcut(start, triangles, dim, minX, minY, invSize) {
15688
- // look for a valid diagonal that divides the polygon into two
15689
- let a = start;
15690
- do {
15691
- let b = a.next.next;
15692
- while (b !== a.prev) {
15693
- if (a.i !== b.i && isValidDiagonal(a, b)) {
15694
- // split the polygon in two by the diagonal
15695
- let c = splitPolygon(a, b);
15696
-
15697
- // filter colinear points around the cuts
15698
- a = filterPoints(a, a.next);
15699
- c = filterPoints(c, c.next);
15700
-
15701
- // run earcut on each half
15702
- earcutLinked(a, triangles, dim, minX, minY, invSize, 0);
15703
- earcutLinked(c, triangles, dim, minX, minY, invSize, 0);
15704
- return;
15705
- }
15706
- b = b.next;
15707
- }
15708
- a = a.next;
15709
- } while (a !== start);
15710
- }
15711
-
15712
- // link every hole into the outer loop, producing a single-ring polygon without holes
15713
- function eliminateHoles(data, holeIndices, outerNode, dim) {
15714
- const queue = [];
15715
-
15716
- for (let i = 0, len = holeIndices.length; i < len; i++) {
15717
- const start = holeIndices[i] * dim;
15718
- const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
15719
- const list = linkedList(data, start, end, dim, false);
15720
- if (list === list.next) list.steiner = true;
15721
- queue.push(getLeftmost(list));
15722
- }
15723
-
15724
- queue.sort(compareXYSlope);
15725
-
15726
- // process holes from left to right
15727
- for (let i = 0; i < queue.length; i++) {
15728
- outerNode = eliminateHole(queue[i], outerNode);
15729
- }
15730
-
15731
- return outerNode;
15732
- }
15733
-
15734
- function compareXYSlope(a, b) {
15735
- let result = a.x - b.x;
15736
- // when the left-most point of 2 holes meet at a vertex, sort the holes counterclockwise so that when we find
15737
- // the bridge to the outer shell is always the point that they meet at.
15738
- if (result === 0) {
15739
- result = a.y - b.y;
15740
- if (result === 0) {
15741
- const aSlope = (a.next.y - a.y) / (a.next.x - a.x);
15742
- const bSlope = (b.next.y - b.y) / (b.next.x - b.x);
15743
- result = aSlope - bSlope;
15550
+ else {
15551
+ out[outIdx++] = nc;
15552
+ out[outIdx++] = current;
15553
+ out[outIdx++] = pa;
15744
15554
  }
15745
15555
  }
15746
- return result;
15747
- }
15748
-
15749
- // find a bridge between vertices that connects hole with an outer ring and link it
15750
- function eliminateHole(hole, outerNode) {
15751
- const bridge = findHoleBridge(hole, outerNode);
15752
- if (!bridge) {
15753
- return outerNode;
15754
- }
15755
-
15756
- const bridgeReverse = splitPolygon(bridge, hole);
15757
-
15758
- // filter collinear points around the cuts
15759
- filterPoints(bridgeReverse, bridgeReverse.next);
15760
- return filterPoints(bridge, bridge.next);
15556
+ return out.subarray(0, outIdx);
15761
15557
  }
15762
-
15763
- // David Eberly's algorithm for finding a bridge between hole and outer polygon
15764
- function findHoleBridge(hole, outerNode) {
15765
- let p = outerNode;
15766
- const hx = hole.x;
15767
- const hy = hole.y;
15768
- let qx = -Infinity;
15769
- let m;
15770
-
15771
- // find a segment intersected by a ray from the hole's leftmost point to the left;
15772
- // segment's endpoint with lesser x will be potential connection point
15773
- // unless they intersect at a vertex, then choose the vertex
15774
- if (equals(hole, p)) return p;
15775
- do {
15776
- if (equals(hole, p.next)) return p.next;
15777
- else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
15778
- const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
15779
- if (x <= hx && x > qx) {
15780
- qx = x;
15781
- m = p.x < p.next.x ? p : p.next;
15782
- if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint
15783
- }
15784
- }
15785
- p = p.next;
15786
- } while (p !== outerNode);
15787
-
15788
- if (!m) return null;
15789
-
15790
- // look for points inside the triangle of hole point, segment intersection and endpoint;
15791
- // if there are no points found, we have a valid connection;
15792
- // otherwise choose the point of the minimum angle with the ray as connection point
15793
-
15794
- const stop = m;
15795
- const mx = m.x;
15796
- const my = m.y;
15797
- let tanMin = Infinity;
15798
-
15799
- p = m;
15800
-
15801
- do {
15802
- if (hx >= p.x && p.x >= mx && hx !== p.x &&
15803
- pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
15804
-
15805
- const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
15806
-
15807
- if (locallyInside(p, hole) &&
15808
- (tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
15809
- m = p;
15810
- tanMin = tan;
15811
- }
15812
- }
15813
-
15814
- p = p.next;
15815
- } while (p !== stop);
15816
-
15817
- return m;
15558
+ /** Shoelace signed area. Positive = CCW (mathematical orientation), negative = CW. */
15559
+ function signedArea(vertices) {
15560
+ const n = vertices.length >> 1;
15561
+ let area = 0;
15562
+ for (let i = 0; i < n; i++) {
15563
+ const j = (i + 1) % n;
15564
+ const x0 = vertices[i * 2];
15565
+ const y0 = vertices[i * 2 + 1];
15566
+ const x1 = vertices[j * 2];
15567
+ const y1 = vertices[j * 2 + 1];
15568
+ area += (x0 * y1) - (x1 * y0);
15569
+ }
15570
+ return area; // Positive = CCW, negative = CW.
15818
15571
  }
15819
-
15820
- // whether sector in vertex m contains sector in vertex p in the same coordinates
15821
- function sectorContainsSector(m, p) {
15822
- return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;
15572
+ /**
15573
+ * Returns true if the triangle (a, b, c) has a counter-clockwise (CCW) winding.
15574
+ * Uses the cross product of (b-a) × (c-a); positive = CCW.
15575
+ */
15576
+ function isCcwTriangle(ax, ay, bx, by, cx, cy) {
15577
+ return ((bx - ax) * (cy - ay) - (by - ay) * (cx - ax)) > 0;
15823
15578
  }
15824
-
15825
- // interlink polygon nodes in z-order
15826
- function indexCurve(start, minX, minY, invSize) {
15827
- let p = start;
15828
- do {
15829
- if (p.z === 0) p.z = zOrder(p.x, p.y, minX, minY, invSize);
15830
- p.prevZ = p.prev;
15831
- p.nextZ = p.next;
15832
- p = p.next;
15833
- } while (p !== start);
15834
-
15835
- p.prevZ.nextZ = null;
15836
- p.prevZ = null;
15837
-
15838
- sortLinked(p);
15579
+ /**
15580
+ * Returns true if point (px, py) lies strictly inside triangle (a, b, c).
15581
+ * Uses sign-of-cross-products. Boundary points (including corners) return false.
15582
+ */
15583
+ function pointInTriangle(px, py, ax, ay, bx, by, cx, cy) {
15584
+ const d1 = (px - bx) * (ay - by) - (ax - bx) * (py - by);
15585
+ const d2 = (px - cx) * (by - cy) - (bx - cx) * (py - cy);
15586
+ const d3 = (px - ax) * (cy - ay) - (cx - ax) * (py - ay);
15587
+ const hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0);
15588
+ const hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0);
15589
+ // Strictly inside: all same sign and none are exactly zero (exclude boundary).
15590
+ return !(hasNeg && hasPos) && (d1 !== 0) && (d2 !== 0) && (d3 !== 0);
15839
15591
  }
15840
-
15841
- // Simon Tatham's linked list merge sort algorithm
15842
- // http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
15843
- function sortLinked(list) {
15844
- let numMerges;
15845
- let inSize = 1;
15846
-
15847
- do {
15848
- let p = list;
15849
- let e;
15850
- list = null;
15851
- let tail = null;
15852
- numMerges = 0;
15853
-
15854
- while (p) {
15855
- numMerges++;
15856
- let q = p;
15857
- let pSize = 0;
15858
- for (let i = 0; i < inSize; i++) {
15859
- pSize++;
15860
- q = q.nextZ;
15861
- if (!q) break;
15862
- }
15863
- let qSize = inSize;
15864
-
15865
- while (pSize > 0 || (qSize > 0 && q)) {
15866
-
15867
- if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {
15868
- e = p;
15869
- p = p.nextZ;
15870
- pSize--;
15871
- } else {
15872
- e = q;
15873
- q = q.nextZ;
15874
- qSize--;
15875
- }
15876
-
15877
- if (tail) tail.nextZ = e;
15878
- else list = e;
15879
-
15880
- e.prevZ = tail;
15881
- tail = e;
15592
+ /**
15593
+ * Returns true if vertex v is an ear: the triangle (prevIdx, v, nextIdx) contains
15594
+ * no other polygon vertex strictly inside it.
15595
+ */
15596
+ function isEar(vertices, prev, next, prevIdx, v, nextIdx) {
15597
+ const ax = vertices[prevIdx * 2], ay = vertices[prevIdx * 2 + 1];
15598
+ const bx = vertices[v * 2], by = vertices[v * 2 + 1];
15599
+ const cx = vertices[nextIdx * 2], cy = vertices[nextIdx * 2 + 1];
15600
+ // Walk all remaining vertices and check if any lie strictly inside the ear triangle.
15601
+ // Skip the three ear vertices themselves — they can never be "inside" by strict test,
15602
+ // but we exclude them explicitly for clarity and to avoid floating-point edge cases.
15603
+ let node = next[nextIdx];
15604
+ while (node !== prevIdx) {
15605
+ if (node !== v) {
15606
+ const px = vertices[node * 2];
15607
+ const py = vertices[node * 2 + 1];
15608
+ if (pointInTriangle(px, py, ax, ay, bx, by, cx, cy)) {
15609
+ return false;
15882
15610
  }
15883
-
15884
- p = q;
15885
15611
  }
15886
-
15887
- tail.nextZ = null;
15888
- inSize *= 2;
15889
-
15890
- } while (numMerges > 1);
15891
-
15892
- return list;
15893
- }
15894
-
15895
- // z-order of a point given coords and inverse of the longer side of data bbox
15896
- function zOrder(x, y, minX, minY, invSize) {
15897
- // coords are transformed into non-negative 15-bit integer range
15898
- x = (x - minX) * invSize | 0;
15899
- y = (y - minY) * invSize | 0;
15900
-
15901
- x = (x | (x << 8)) & 0x00FF00FF;
15902
- x = (x | (x << 4)) & 0x0F0F0F0F;
15903
- x = (x | (x << 2)) & 0x33333333;
15904
- x = (x | (x << 1)) & 0x55555555;
15905
-
15906
- y = (y | (y << 8)) & 0x00FF00FF;
15907
- y = (y | (y << 4)) & 0x0F0F0F0F;
15908
- y = (y | (y << 2)) & 0x33333333;
15909
- y = (y | (y << 1)) & 0x55555555;
15910
-
15911
- return x | (y << 1);
15912
- }
15913
-
15914
- // find the leftmost node of a polygon ring
15915
- function getLeftmost(start) {
15916
- let p = start,
15917
- leftmost = start;
15918
- do {
15919
- if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p;
15920
- p = p.next;
15921
- } while (p !== start);
15922
-
15923
- return leftmost;
15924
- }
15925
-
15926
- // check if a point lies within a convex triangle
15927
- function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
15928
- return (cx - px) * (ay - py) >= (ax - px) * (cy - py) &&
15929
- (ax - px) * (by - py) >= (bx - px) * (ay - py) &&
15930
- (bx - px) * (cy - py) >= (cx - px) * (by - py);
15931
- }
15932
-
15933
- // check if a point lies within a convex triangle but false if its equal to the first point of the triangle
15934
- function pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, px, py) {
15935
- return !(ax === px && ay === py) && pointInTriangle(ax, ay, bx, by, cx, cy, px, py);
15936
- }
15937
-
15938
- // check if a diagonal between two polygon nodes is valid (lies in polygon interior)
15939
- function isValidDiagonal(a, b) {
15940
- return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // doesn't intersect other edges
15941
- (locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
15942
- (area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors
15943
- equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case
15944
- }
15945
-
15946
- // signed area of a triangle
15947
- function area(p, q, r) {
15948
- return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
15949
- }
15950
-
15951
- // check if two points are equal
15952
- function equals(p1, p2) {
15953
- return p1.x === p2.x && p1.y === p2.y;
15954
- }
15955
-
15956
- // check if two segments intersect
15957
- function intersects(p1, q1, p2, q2) {
15958
- const o1 = sign(area(p1, q1, p2));
15959
- const o2 = sign(area(p1, q1, q2));
15960
- const o3 = sign(area(p2, q2, p1));
15961
- const o4 = sign(area(p2, q2, q1));
15962
-
15963
- if (o1 !== o2 && o3 !== o4) return true; // general case
15964
-
15965
- if (o1 === 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1
15966
- if (o2 === 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1
15967
- if (o3 === 0 && onSegment(p2, p1, q2)) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2
15968
- if (o4 === 0 && onSegment(p2, q1, q2)) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2
15969
-
15970
- return false;
15971
- }
15972
-
15973
- // for collinear points p, q, r, check if point q lies on segment pr
15974
- function onSegment(p, q, r) {
15975
- return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y);
15976
- }
15977
-
15978
- function sign(num) {
15979
- return num > 0 ? 1 : num < 0 ? -1 : 0;
15980
- }
15981
-
15982
- // check if a polygon diagonal intersects any polygon segments
15983
- function intersectsPolygon(a, b) {
15984
- let p = a;
15985
- do {
15986
- if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
15987
- intersects(p, p.next, a, b)) return true;
15988
- p = p.next;
15989
- } while (p !== a);
15990
-
15991
- return false;
15992
- }
15993
-
15994
- // check if a polygon diagonal is locally inside the polygon
15995
- function locallyInside(a, b) {
15996
- return area(a.prev, a, a.next) < 0 ?
15997
- area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 :
15998
- area(a, b, a.prev) < 0 || area(a, a.next, b) < 0;
15999
- }
16000
-
16001
- // check if the middle point of a polygon diagonal is inside the polygon
16002
- function middleInside(a, b) {
16003
- let p = a;
16004
- let inside = false;
16005
- const px = (a.x + b.x) / 2;
16006
- const py = (a.y + b.y) / 2;
16007
- do {
16008
- if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&
16009
- (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))
16010
- inside = !inside;
16011
- p = p.next;
16012
- } while (p !== a);
16013
-
16014
- return inside;
16015
- }
16016
-
16017
- // link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
16018
- // if one belongs to the outer ring and another to a hole, it merges it into a single ring
16019
- function splitPolygon(a, b) {
16020
- const a2 = createNode(a.i, a.x, a.y),
16021
- b2 = createNode(b.i, b.x, b.y),
16022
- an = a.next,
16023
- bp = b.prev;
16024
-
16025
- a.next = b;
16026
- b.prev = a;
16027
-
16028
- a2.next = an;
16029
- an.prev = a2;
16030
-
16031
- b2.next = a2;
16032
- a2.prev = b2;
16033
-
16034
- bp.next = b2;
16035
- b2.prev = bp;
16036
-
16037
- return b2;
16038
- }
16039
-
16040
- // create a node and optionally link it with previous one (in a circular doubly linked list)
16041
- function insertNode(i, x, y, last) {
16042
- const p = createNode(i, x, y);
16043
-
16044
- if (!last) {
16045
- p.prev = p;
16046
- p.next = p;
16047
-
16048
- } else {
16049
- p.next = last.next;
16050
- p.prev = last;
16051
- last.next.prev = p;
16052
- last.next = p;
16053
- }
16054
- return p;
16055
- }
16056
-
16057
- function removeNode(p) {
16058
- p.next.prev = p.prev;
16059
- p.prev.next = p.next;
16060
-
16061
- if (p.prevZ) p.prevZ.nextZ = p.nextZ;
16062
- if (p.nextZ) p.nextZ.prevZ = p.prevZ;
16063
- }
16064
-
16065
- function createNode(i, x, y) {
16066
- return {
16067
- i, // vertex index in coordinates array
16068
- x, y, // vertex coordinates
16069
- prev: null, // previous and next vertex nodes in a polygon ring
16070
- next: null,
16071
- z: 0, // z-order curve value
16072
- prevZ: null, // previous and next nodes in z-order
16073
- nextZ: null,
16074
- steiner: false // indicates whether this is a steiner point
16075
- };
16076
- }
16077
-
16078
- function signedArea(data, start, end, dim) {
16079
- let sum = 0;
16080
- for (let i = start, j = end - dim; i < end; i += dim) {
16081
- sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]);
16082
- j = i;
15612
+ node = next[node];
16083
15613
  }
16084
- return sum;
15614
+ return true;
16085
15615
  }
16086
15616
 
16087
15617
  const buildLine = (startX, startY, endX, endY, width) => {
@@ -16285,13 +15815,13 @@ const buildPolygon = (points) => {
16285
15815
  throw new Error('At least three X/Y pairs are required to build a polygon.');
16286
15816
  }
16287
15817
  const length = points.length / 2;
16288
- const triangles = earcut(points, [], 2);
15818
+ const triangles = triangulate(points);
16289
15819
  const vertices = new Float32Array(points.length);
16290
15820
  for (let i = 0; i < length; i++) {
16291
15821
  vertices[i * 2] = points[i * 2];
16292
15822
  vertices[(i * 2) + 1] = points[(i * 2) + 1];
16293
15823
  }
16294
- const indices = triangles ? new Uint16Array(triangles) : new Uint16Array(0);
15824
+ const indices = new Uint16Array(triangles);
16295
15825
  return { vertices, indices, points };
16296
15826
  };
16297
15827
  const buildRectangle = (x, y, width, height) => {
@@ -18759,5 +18289,5 @@ class IndexedDbStore {
18759
18289
  }
18760
18290
  }
18761
18291
 
18762
- export { AbstractAssetFactory, AbstractMedia, AbstractWebGl2BatchedRenderer, AbstractWebGl2Renderer, AbstractWebGpuRenderer, AnimatedSprite, Application, ApplicationStatus, ArcadeStickGamepadMapping, AudioAnalyser, BinaryFactory, BlendModes, BlurFilter, Bounds, BufferTypes, BufferUsage, BundleLoadError, CacheFirstStrategy, CallbackRenderPass, Capabilities, ChannelOffset, ChannelSize, Circle, Clock, CollisionType, Color, ColorAffector, ColorFilter, Container, Drawable, DynamicGlyphAtlas, Ellipse, FactoryRegistry, Filter, Flags, FontFactory, ForceAffector, GameCubeGamepadMapping, Gamepad, GamepadChannel, GamepadControl, GamepadMapping, GamepadMappingFamily, GamepadPromptLayouts, GenericDualAnalogGamepadMapping, Graphics, ImageFactory, IndexedDbDatabase, IndexedDbStore, Input, InputManager, Interval, JoyConLeftGamepadMapping, JoyConRightGamepadMapping, Json, JsonFactory, Keyboard, Line, Loader, Matrix, Mesh, Music, MusicFactory, NetworkOnlyStrategy, ObservableSize, ObservableVector, Particle, ParticleOptions, ParticleSystem, PlayStationGamepadMapping, Pointer, PointerState, PointerStateFlag, PolarVector, Polygon, Quadtree, Random, Rectangle, RenderBackendType, RenderNode, RenderTarget, RenderTargetPass, RenderTexture, RendererRegistry, RenderingPrimitives, Sampler, ScaleAffector, ScaleModes, Scene, SceneManager, SceneNode, Segment, Shader, ShaderAttribute, ShaderPrimitives, ShaderUniform, Signal, Size, Sound, SoundFactory, Sprite, SpriteFlags, Spritesheet, SteamControllerGamepadMapping, SvgAsset, SvgFactory, SwitchProGamepadMapping, Text, TextAsset, TextFactory, TextStyle, Texture, TextureFactory, Time, Timer, TorqueAffector, UniversalEmitter, Vector, Video, VideoFactory, View, ViewFlags, VoronoiRegion, VttAsset, VttFactory, WasmFactory, WebGl2Backend, WebGl2MeshRenderer, WebGl2ParticleRenderer, WebGl2RenderBuffer, WebGl2ShaderBlock, WebGl2SpriteRenderer, WebGl2VertexArrayObject, WebGpuBackend, WebGpuMeshRenderer, WebGpuParticleRenderer, WebGpuSpriteRenderer, WrapModes, XboxGamepadMapping, bezierCurveTo, buildCircle, buildEllipse, buildLine, buildPath, buildPolygon, buildRectangle, buildStar, builtInGamepadDefinitions, canvasSourceToDataUrl, clamp, createRenderStats, createWebGl2ShaderProgram, decodeAudioData, defineAssetManifest, degreesPerRadian, degreesToRadians, determineMimeType, emptyArrayBuffer, getAudioContext, getCanvasSourceSize, getCollisionCircleCircle, getCollisionCircleRectangle, getCollisionPolygonCircle, getCollisionRectangleRectangle, getCollisionSat, getDistance, getOfflineAudioContext, getPreciseTime, getTextureSourceSize, getVoronoiRegion$1 as getVoronoiRegion, getWebGpuBlendState, hours, inRange, intersectionCircleCircle, intersectionCircleEllipse, intersectionCirclePoly, intersectionEllipseEllipse, intersectionEllipsePoly, intersectionLineCircle, intersectionLineEllipse, intersectionLineLine, intersectionLinePoly, intersectionLineRect, intersectionPointCircle, intersectionPointEllipse, intersectionPointLine, intersectionPointPoint, intersectionPointPoly, intersectionPointRect, intersectionPolyPoly, intersectionRectCircle, intersectionRectEllipse, intersectionRectPoly, intersectionRectRect, intersectionSat, isAudioContextReady, isPowerOfTwo, layoutText, lerp, matchesIds, maxPointers, milliseconds, minutes, noop$1 as noop, normalizeIds, onAudioContextReady, parseGamepadDescriptor, pointerSlotSize, quadraticCurveTo, radiansPerDegree, radiansToDegrees, rand, removeArrayItems, resetRenderStats, resolveDefinition, resolveGamepadDefinition, seconds, sign$1 as sign, stopEvent, supportsCodec, supportsEventOptions, supportsIndexedDb, supportsPointerEvents, supportsTouchEvents, supportsWebAudio, tau, trimRotation, webGl2PrimitiveArrayConstructors, webGl2PrimitiveByteSizeMapping, webGl2PrimitiveTypeNames };
18292
+ export { AbstractAssetFactory, AbstractMedia, AbstractWebGl2BatchedRenderer, AbstractWebGl2Renderer, AbstractWebGpuRenderer, AnimatedSprite, Application, ApplicationStatus, ArcadeStickGamepadMapping, AudioAnalyser, BinaryFactory, BlendModes, BlurFilter, Bounds, BufferTypes, BufferUsage, BundleLoadError, CacheFirstStrategy, CallbackRenderPass, Capabilities, ChannelOffset, ChannelSize, Circle, Clock, CollisionType, Color, ColorAffector, ColorFilter, Container, Drawable, DynamicGlyphAtlas, Ellipse, FactoryRegistry, Filter, Flags, FontFactory, ForceAffector, GameCubeGamepadMapping, Gamepad, GamepadChannel, GamepadControl, GamepadMapping, GamepadMappingFamily, GamepadPromptLayouts, GenericDualAnalogGamepadMapping, Graphics, ImageFactory, IndexedDbDatabase, IndexedDbStore, Input, InputManager, Interval, JoyConLeftGamepadMapping, JoyConRightGamepadMapping, Json, JsonFactory, Keyboard, Line, Loader, Matrix, Mesh, Music, MusicFactory, NetworkOnlyStrategy, ObservableSize, ObservableVector, Particle, ParticleOptions, ParticleSystem, PlayStationGamepadMapping, Pointer, PointerState, PointerStateFlag, PolarVector, Polygon, Quadtree, Random, Rectangle, RenderBackendType, RenderNode, RenderTarget, RenderTargetPass, RenderTexture, RendererRegistry, RenderingPrimitives, Sampler, ScaleAffector, ScaleModes, Scene, SceneManager, SceneNode, Segment, Shader, ShaderAttribute, ShaderPrimitives, ShaderUniform, Signal, Size, Sound, SoundFactory, Sprite, SpriteFlags, Spritesheet, SteamControllerGamepadMapping, SvgAsset, SvgFactory, SwitchProGamepadMapping, Text, TextAsset, TextFactory, TextStyle, Texture, TextureFactory, Time, Timer, TorqueAffector, UniversalEmitter, Vector, Video, VideoFactory, View, ViewFlags, VoronoiRegion, VttAsset, VttFactory, WasmFactory, WebGl2Backend, WebGl2MeshRenderer, WebGl2ParticleRenderer, WebGl2RenderBuffer, WebGl2ShaderBlock, WebGl2SpriteRenderer, WebGl2VertexArrayObject, WebGpuBackend, WebGpuMeshRenderer, WebGpuParticleRenderer, WebGpuSpriteRenderer, WrapModes, XboxGamepadMapping, bezierCurveTo, buildCircle, buildEllipse, buildLine, buildPath, buildPolygon, buildRectangle, buildStar, builtInGamepadDefinitions, canvasSourceToDataUrl, clamp, createRenderStats, createWebGl2ShaderProgram, decodeAudioData, defineAssetManifest, degreesPerRadian, degreesToRadians, determineMimeType, emptyArrayBuffer, getAudioContext, getCanvasSourceSize, getCollisionCircleCircle, getCollisionCircleRectangle, getCollisionPolygonCircle, getCollisionRectangleRectangle, getCollisionSat, getDistance, getOfflineAudioContext, getPreciseTime, getTextureSourceSize, getVoronoiRegion$1 as getVoronoiRegion, getWebGpuBlendState, hours, inRange, intersectionCircleCircle, intersectionCircleEllipse, intersectionCirclePoly, intersectionEllipseEllipse, intersectionEllipsePoly, intersectionLineCircle, intersectionLineEllipse, intersectionLineLine, intersectionLinePoly, intersectionLineRect, intersectionPointCircle, intersectionPointEllipse, intersectionPointLine, intersectionPointPoint, intersectionPointPoly, intersectionPointRect, intersectionPolyPoly, intersectionRectCircle, intersectionRectEllipse, intersectionRectPoly, intersectionRectRect, intersectionSat, isAudioContextReady, isPowerOfTwo, layoutText, lerp, matchesIds, maxPointers, milliseconds, minutes, noop$1 as noop, normalizeIds, onAudioContextReady, parseGamepadDescriptor, pointerSlotSize, quadraticCurveTo, radiansPerDegree, radiansToDegrees, rand, removeArrayItems, resetRenderStats, resolveDefinition, resolveGamepadDefinition, seconds, sign, stopEvent, supportsCodec, supportsEventOptions, supportsIndexedDb, supportsPointerEvents, supportsTouchEvents, supportsWebAudio, tau, trimRotation, webGl2PrimitiveArrayConstructors, webGl2PrimitiveByteSizeMapping, webGl2PrimitiveTypeNames };
18763
18293
  //# sourceMappingURL=exo.esm.js.map