Resources Numerical accuracy Calculation policy

Subdivide, loop cut, bisect, knife and grid fill

Places the new points when edges are cut evenly, when a plane or a knife line cuts faces, and when a quad or an open loop is filled with a grid.

How the precision is kept

Cut points are linear blends of two 64-bit vertex positions at a fraction that is either k ÷ (cuts + 1) or the ratio of two signed distances to the cutting plane. Interior grid points are a Coons patch of the four sides. Decisions about which side of a plane a vertex is on use fixed absolute tolerances.

Open item. Scale the bisect and knife plane tolerances to the size of the faces being cut instead of fixed 1e-9 and 1e-12.

Calculations under this policy

5 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.

mesh/ops.js ModelCraft 0.8.1 64-bit 5 calculations Reviewed

Tests tools/modelcraft-test/mesh.mjs

Even cuts along an edge

64-bit

The new vertices that divide an edge into cuts + 1 parts, shared by both faces on the edge.

s = k ÷ (cuts + 1); with slide: s = clamp(s + slide ÷ (cuts + 1), 0.001, 0.999); p = A + (B − A)·s

Each point is computed once from the edge's canonical half and reused by both faces, so the two faces get the same coordinates. The form A + (B − A)·s is used; s never reaches 0 or 1 here.

  • mesh/ops.js
  • lines 354 to 370
  • ModelCraft 0.8.1
/** Points on each listed edge, `cuts` of them evenly (shared by both faces): Map edge(canonical half-edge) → [new vertices from hV to head]. */
function cutEdges(m, edges, cuts, slide = 0) {
  const out = new Map();
  for (const h0 of edges) {
    const h = m.edgeOf(h0);
    if (out.has(h)) continue;
    const a = m.hV[h], b = m.head(h), A = P3(m, a), B = P3(m, b), list = [];
    for (let k = 1; k <= cuts; k++) {
      let s = k / (cuts + 1);
      if (slide) s = Math.min(0.999, Math.max(0.001, s + slide / (cuts + 1)));
      const v = newVert(m, lerp(A, B, s), a);
      list.push({ v, s });
    }
    out.set(h, list);
  }
  return out;
}

Subdivide: interior points of a quad

64-bit

The points inside a quad whose four edges are all cut, placed so they follow the four sides.

u = i ÷ N, w = j ÷ N; p = (1 − w)·S₀ + w·S₂ + (1 − u)·S₃ + u·S₁ − [(1 − u)(1 − w)·c₀₀ + u(1 − w)·c₁₀ + uw·c₁₁ + (1 − u)w·c₀₁]

A Coons patch: the four weighted side points are added and the bilinear blend of the four corners is subtracted, all in 64-bit. The subtraction is of numbers the size of the coordinates themselves.

  • mesh/ops.js
  • lines 406 to 412
  • ModelCraft 0.8.1
// a Coons patch over the four sides (flat quads stay flat; warped ones follow their sides)
const P = (x) => P3(m, x), u = i / N, w = j / N;
const c00 = P(m.hV[cs[0]]), c10 = P(m.hV[cs[1]]), c11 = P(m.hV[cs[2]]), c01 = P(m.hV[cs[3]]);
const S0 = P(side[0][i]), S2 = P(side[2][N - i]), S3 = P(side[3][N - j]), S1 = P(side[1][j]);
const ruled = add(add(mul(S0, 1 - w), mul(S2, w)), add(mul(S3, 1 - u), mul(S1, u)));
const bil = add(add(mul(c00, (1 - u) * (1 - w)), mul(c10, u * (1 - w))), add(mul(c11, u * w), mul(c01, (1 - u) * w)));
grid[i].push(newVert(m, sub(ruled, bil), m.hV[cs[0]]));

Bisect by a plane

64-bit

Cuts every listed face's edges where the plane crosses them and splits the faces along the cut.

dist(v) = P(v)·n − p·n; s = dₐ ÷ (dₐ − d_b); point = A + (B − A)·s

The signed distance is the difference of two dot products, which loses digits when the mesh lies far from its own origin. An edge is cut only if its ends are more than 1e-9 on opposite sides, and a vertex within 1e-9 counts as on the plane; both are absolute. dₐ and d_b have opposite signs, so dₐ − d_b does not cancel.

  • mesh/ops.js
  • lines 743 to 759
  • ModelCraft 0.8.1
/**
 * Bisect: cut faces with the plane {p, n}: each edge the plane crosses gets a point (shared by its faces), and a face
 * crossed at two places is split in two along the cut. Faces next to a cut edge that are not in the list just gain the
 * point. → {verts (new), edges (the cut's new edges)}.
 */
export function bisect(m, faces, plane) {
  const R = liveFaces(m, faces), n = norm(plane.n), d0 = dot(n, plane.p), eps = 1e-9;
  const dist = (v) => dot(P3(m, v), n) - d0;
  const cut = new Map();   // canonical edge → new vertex
  for (const f of R) for (const h of m.faceCorners(f, [])) {
    const k = m.edgeOf(h);
    if (cut.has(k)) continue;
    const a = m.hV[k], b = m.head(k), da = dist(a), db = dist(b);
    if ((da > eps && db < -eps) || (da < -eps && db > eps)) cut.set(k, newVert(m, lerp(P3(m, a), P3(m, b), da / (da - db)), a));
  }
  return splitByPoints(m, R, cut, (v) => Math.abs(dist(v)) <= eps);
}

Knife: where the cut crosses an edge

64-bit

For each edge of the faces round the current point, whether the cutting plane crosses it and where.

dₐ = (a − A)·n, d_b = (b − A)·n; s = dₐ ÷ (dₐ − d_b); X = a + (b − a)·s

Distances are taken from the cut's start point A, so the subtraction happens before the dot product. An end within 1e-12 (absolute) of the plane gives s = 0 or 1; a crossing must lie more than 1e-12 further along the cut than the current point, measured as an unnormalized dot product with the cut's direction.

  • mesh/ops.js
  • lines 834 to 840
  • ModelCraft 0.8.1
const a = P3(m, m.hV[g]), b = P3(m, m.head(g)), da = dot(sub(a, A), nrm), db = dot(sub(b, A), nrm);
if (!((da > 1e-12 && db < -1e-12) || (da < -1e-12 && db > 1e-12) || Math.abs(da) <= 1e-12 || Math.abs(db) <= 1e-12)) continue;
const s = Math.abs(da) <= 1e-12 ? 0 : Math.abs(db) <= 1e-12 ? 1 : da / (da - db);
const X = lerp(a, b, s);
// it must move toward the target along the cut
if (dot(sub(X, A), sub(Bp, A)) <= dot(sub(P3(m, cur), A), sub(Bp, A)) + 1e-12) continue;
if (!next || dot(sub(X, A), sub(Bp, A)) < next.t) next = { f, g, s, t: dot(sub(X, A), sub(Bp, A)) };

Grid fill: interior points

64-bit

The points inside an open loop filled with a grid of quads, following the loop's four sides.

u = i ÷ a, w = j ÷ b; p = ruled blend of the four side points − bilinear blend of the four corners

The same Coons patch as in subdivide, with the sides taken from the loop's own vertices. Eight weighted terms are added and four subtracted in 64-bit.

  • mesh/ops.js
  • lines 888 to 893
  • ModelCraft 0.8.1
const u = i / a, w = j / b, P = (x) => P3(m, x);
const S0 = P(at(i)), S2 = P(at(2 * a + b - i)), S3 = P(at(n - j)), S1 = P(at(a + j));
const c00 = P(at(0)), c10 = P(at(a)), c11 = P(at(a + b)), c01 = P(at(2 * a + b));
const ruled = add(add(mul(S0, 1 - w), mul(S2, w)), add(mul(S3, 1 - u), mul(S1, u)));
const bil = add(add(mul(c00, (1 - u) * (1 - w)), mul(c10, u * (1 - w))), add(mul(c11, u * w), mul(c01, (1 - u) * w)));
g[i].push(newVert(m, sub(ruled, bil), at(0)));

In detail

Method
Linear interpolation a + (b − a)·s along edges; plane crossing at s = dₐ ÷ (dₐ − d_b); Coons patch = two ruled blends minus the bilinear blend of the corners.
Number types
64-bit numbers from the mesh's Float64Array.
Tolerances
Bisect: 1e-9 absolute (mesh units) for a vertex to count as on the plane. Knife: 1e-12 absolute for on the plane and for progress toward the target; a cut within 1e-6 of an edge's end (as a fraction of the edge) uses the existing vertex. Loop cut slide: the fraction is kept within 0.001…0.999.
Limits
The tolerances are absolute, so they do not scale with the size of the mesh or its distance from the origin. The knife walks at most 10 000 faces per stretch.

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