Resources Numerical accuracy Calculation policy

Triangles inside a trimmed face

Adds points inside a triangulated face and keeps the triangles well shaped, for curved faces read from a .3dm file.

How the precision is kept

Orientation and in-circle tests are evaluated in 64-bit floating point, as written, without exact arithmetic. The in-circle test subtracts the tested point from the other three first, which keeps its products small. A point too close to a side or corner is left out by a tolerance relative to the triangle's area.

Open item. Compute the orientation and in-circle signs with exact arithmetic, so nearly collinear or cocircular points cannot flip a decision.

Calculations under this policy

6 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

io/3dm/cdt.js ModelCraft 0.8.1 64-bit 6 calculations Reviewed

Tests None found by the review

Orientation of three points

64-bit

Twice the signed area of a triangle: which way it turns.

(b − a) × (c − a) = (bₓ − aₓ)(c_y − a_y) − (b_y − a_y)(cₓ − aₓ)

Differences from point a are taken first, then two products are subtracted. For nearly collinear points the two products are nearly equal and the sign can be wrong; it is not computed exactly.

  • io/3dm/cdt.js
  • line 13
  • ModelCraft 0.8.1
const orient = (X, Y, a, b, c) => (X[b] - X[a]) * (Y[c] - Y[a]) - (Y[b] - Y[a]) * (X[c] - X[a]);

Point inside a circumcircle

64-bit

Whether a fourth point lies inside the circle through three others.

determinant of the rows (pₓ − dₓ, p_y − d_y, |p − d|²) for p = a, b, c; inside when it is positive

All three points are taken relative to the tested point before squaring, so the size of the coordinates does not matter, only the size of the triangle. The determinant's sign is a floating-point result with no error bound or exact fallback.

  • io/3dm/cdt.js
  • lines 15 to 18
  • ModelCraft 0.8.1
function inCircle(X, Y, a, b, c, d) {
  const ax = X[a] - X[d], ay = Y[a] - Y[d], bx = X[b] - X[d], by = Y[b] - Y[d], cx = X[c] - X[d], cy = Y[c] - Y[d];
  return (ax * ax + ay * ay) * (bx * cy - cx * by) - (bx * bx + by * by) * (ax * cy - cx * ay) + (cx * cx + cy * cy) * (ax * by - bx * ay);
}

Triangle that holds a point

64-bit

Walks from the last triangle toward the point, then searches all triangles if the walk fails.

A point is inside when all three edge tests are zero or positive, a plain comparison with zero and no tolerance. The walk is capped at 4096 steps before the full search.

  • io/3dm/cdt.js
  • lines 43 to 67
  • ModelCraft 0.8.1
/** The triangle holding (x, y): a walk from the last one, then every triangle when the walk gets lost. */
locate(x, y) {
  const X = this.X, Y = this.Y;
  const inside = (t) => {
    const T = this.T[t];
    if (!T) return -1;
    const [a, b, c] = T;
    const o = (p, q) => (X[q] - X[p]) * (y - Y[p]) - (Y[q] - Y[p]) * (x - X[p]);
    const e0 = o(a, b), e1 = o(b, c), e2 = o(c, a);
    if (e0 >= 0 && e1 >= 0 && e2 >= 0) return -2;
    // step across the side it lies most outside of
    const m = Math.min(e0, e1, e2);
    const [p, q] = m === e0 ? [a, b] : m === e1 ? [b, c] : [c, a];
    const n = this.he.get(this.key(q, p));
    return n === undefined ? -1 : n;
  };
  let t = this.T[this.last] ? this.last : this.T.findIndex(Boolean);
  for (let step = 0; step < 4096 && t >= 0; step++) {
    const r = inside(t);
    if (r === -2) return t;
    t = r;
  }
  for (let k = 0; k < this.T.length; k++) if (inside(k) === -2) return k;
  return -1;
}

Point insertion

64-bit

Splits the triangle a new point falls in, or the side it falls on.

ε = 10⁻⁶ × orient(a, b, c); on a side when a sub-area ≤ ε

The tolerance scales with the triangle's own area. A point within it of exactly one side splits that side and the triangle beyond; a point near a corner, on a boundary side or outside by more than the tolerance is left out, and the function returns false.

  • io/3dm/cdt.js
  • lines 68 to 102
  • ModelCraft 0.8.1
insert(x, y) {
  const t = this.locate(x, y);
  if (t < 0) return false;
  const X = this.X, Y = this.Y;
  const [a, b, c] = this.T[t];
  // too close to a side or corner: leave it out (the refinement after this fills in where needed)
  const area = orient(X, Y, a, b, c);
  const p = X.length;
  X.push(x); Y.push(y);
  const w0 = orient(X, Y, a, b, p), w1 = orient(X, Y, b, c, p), w2 = orient(X, Y, c, a, p);
  const eps = area * 1e-6;
  if (Math.min(w0, w1, w2) <= eps) {
    // on a side (the other two well inside): split it and the triangle beyond; on a corner or a polygon side: leave it out
    const on = [w0, w1, w2].filter((w) => w <= eps).length;
    const [q, r, o] = w0 <= eps ? [a, b, c] : w1 <= eps ? [b, c, a] : [c, a, b];
    const n = this.he.get(this.key(r, q));
    if (on !== 1 || n === undefined || Math.min(w0, w1, w2) < -eps) { X.pop(); Y.pop(); return false; }
    const N = this.T[n];
    const d = N[0] === r ? N[2] : N[1] === r ? N[0] : N[1];
    if (orient(X, Y, r, p, d) <= 0 || orient(X, Y, p, q, d) <= 0) { X.pop(); Y.pop(); return false; }
    this.drop(n);
    this.put(t, q, p, o); this.put(n, p, r, o);
    const t3 = this.T.length; this.put(t3, r, p, d);
    const t4 = this.T.length; this.put(t4, p, q, d);
    this.last = t;
    this.legalize(r, o, p); this.legalize(o, q, p); this.legalize(q, d, p); this.legalize(d, r, p);
    return true;
  }
  this.put(t, a, b, p);
  const t1 = this.T.length; this.put(t1, b, c, p);
  const t2 = this.T.length; this.put(t2, c, a, p);
  this.last = t;
  this.legalize(a, b, p); this.legalize(b, c, p); this.legalize(c, a, p);
  return true;
}

Edge flip

64-bit

Flips an edge while the opposite corner lies inside the circumcircle.

A flip needs a strictly positive in-circle value and two strictly positive orientation tests for the new triangles, so a flip never creates a turned-over triangle as far as the floating-point signs are right. The recursion stops at depth 64.

  • io/3dm/cdt.js
  • lines 103 to 119
  • ModelCraft 0.8.1
/** Edge a→b of triangle (a, b, p): flip it while the far corner lies in the circle. */
legalize(a, b, p, depth = 0) {
  if (depth > 64) return;
  const t = this.he.get(this.key(a, b));
  const n = this.he.get(this.key(b, a));
  if (t === undefined || n === undefined) return;
  const N = this.T[n];
  const d = N[0] === b ? N[2] : N[1] === b ? N[0] : N[1];   // the corner of (b, a, d) not on the edge
  if (inCircle(this.X, this.Y, a, b, p, d) <= 0) return;
  // the new edge p–d must leave both triangles turning the same way
  if (orient(this.X, this.Y, a, d, p) <= 0 || orient(this.X, this.Y, d, b, p) <= 0) return;
  this.drop(n);
  this.put(t, a, d, p);
  this.put(n, d, b, p);
  this.legalize(a, d, p, depth + 1);
  this.legalize(d, b, p, depth + 1);
}

Flips with no new points

64-bit

Makes an existing triangulation Delaunay by flipping edges.

The same three tests as the edge flip, over every triangle, for at most 8 passes. It stops early when a pass makes no flip; after 8 passes edges that still fail the test are left as they are.

  • io/3dm/cdt.js
  • lines 129 to 151
  • ModelCraft 0.8.1
export function flipAll(X, Y, tris) {
  const m = new Mesh2(X, Y, tris);
  for (let pass = 0; pass < 8; pass++) {
    let flips = 0;
    for (let t = 0; t < m.T.length; t++) {
      const T = m.T[t];
      if (!T) continue;
      for (let k = 0; k < 3; k++) {
        const a = T[k], b = T[(k + 1) % 3], p = T[(k + 2) % 3];
        const n = m.he.get(m.key(b, a));
        if (n === undefined) continue;
        const N = m.T[n];
        const d = N[0] === b ? N[2] : N[1] === b ? N[0] : N[1];
        if (inCircle(X, Y, a, b, p, d) > 0 && orient(X, Y, a, d, p) > 0 && orient(X, Y, d, b, p) > 0) {
          m.drop(n); m.put(t, a, d, p); m.put(n, d, b, p); flips++;
          break;
        }
      }
    }
    if (!flips) break;
  }
  return m.out();
}

In detail

Method
Point insertion with Lawson edge flips; the file's header cites de Berg et al., Computational Geometry, chapter 9.
Number types
64-bit numbers in plain arrays.
Tolerances
A new point counts as on a side when its sub-triangle area is at most 1e-6 × the triangle's area. Flips stop at depth 64 per insertion; flipAll makes at most 8 passes.
Limits
The signs of the orientation and in-circle expressions are not computed exactly, so nearly collinear or nearly cocircular points can be decided either way.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach