| 630 | |
| 631 | const inv = 1 / det; |
| 632 | _s[0] = inv * (b0*(a11*a22 - a12*a12) - a01*(b1*a22 - a12*b2) + a02*(b1*a12 - a11*b2)); |
| 633 | _s[1] = inv * (a00*(b1*a22 - a12*b2) - b0*(a01*a22 - a12*a02) + a02*(a01*b2 - b1*a02)); |
| 634 | _s[2] = inv * (a00*(a11*b2 - b1*a12) - a01*(a01*b2 - b1*a02) + b0*(a01*a12 - a11*a02)); |
| 635 | return true; |
| 636 | } |
| 637 | |
| 638 | function pushEdge(heap, quadrics, positions, version, v1, v2) { |
| 639 | let px, py, pz; |
| 640 | |
| 641 | if (solveQ(quadrics, v1, v2)) { |
| 642 | px = _s[0]; py = _s[1]; pz = _s[2]; |
| 643 | } else { |
| 644 | const mx = (positions[v1*3] + positions[v2*3]) / 2; |
| 645 | const my = (positions[v1*3+1] + positions[v2*3+1]) / 2; |
| 646 | const mz = (positions[v1*3+2] + positions[v2*3+2]) / 2; |
| 647 | const e1 = evalQSum(quadrics, v1, v2, positions[v1*3], positions[v1*3+1], positions[v1*3+2]); |
| 648 | const e2 = evalQSum(quadrics, v1, v2, positions[v2*3], positions[v2*3+1], positions[v2*3+2]); |
| 649 | const em = evalQSum(quadrics, v1, v2, mx, my, mz); |
| 650 | // Prefer midpoint when costs are near-equal (degenerate / flat surfaces). |
| 651 | // Midpoint minimises displacement of adjacent triangles, reducing normal |
| 652 | // flips and preventing the collapse loop from stalling on coplanar geometry. |
| 653 | const eMin = Math.min(e1, e2, em); |
| 654 | const eTol = eMin * 1e-2 + 1e-12; |
| 655 | if (em <= eMin + eTol) { px = mx; py = my; pz = mz; } |
| 656 | else if (e1 <= e2) { px = positions[v1*3]; py = positions[v1*3+1]; pz = positions[v1*3+2]; } |
| 657 | else { px = positions[v2*3]; py = positions[v2*3+1]; pz = positions[v2*3+2]; } |
| 658 | } |
| 659 | |
| 660 | const cost = evalQSum(quadrics, v1, v2, px, py, pz); |
| 661 | // Tiny edge-length tiebreaker: on degenerate (flat) surfaces where QEM |
| 662 | // costs are ~0, prefer collapsing shorter edges first for better triangle |
| 663 | // quality and fewer guard rejections. |