Resources Numerical accuracy Calculation policy

Advanced simulation add-on: finding contact pairs

Finds flat faces of two separate pieces that face each other within a gap, as candidate contact pairs.

How the precision is kept

Normals and plane origins come from the rebuild's face records and are compared in 64-bit. Each face's outline box is measured from the display mesh's triangles, whose positions kernel.js keeps in a Float32Array, so the overlap is known to 32-bit accuracy. All thresholds are absolute.

Open item. Measure the faces' outlines from the 64-bit B-rep instead of the 32-bit display mesh, and scale the 1e-6 mm thresholds to the part's size.

Calculations under this policy

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

addons/advanced-sim/lib/setup.js PartCraft 0.8.1 32-bit in part 2 calculations Reviewed

Tests tools/addons-test/browser-advanced-sim.mjs

A flat face's box in its own plane

32-bit in part

The extent of a flat face along two axes lying in its plane.

u = (n × t) ÷ |n × t|; v = n × u; box = least and largest p·u and p·v over the face's triangle corners

The corner points are read from regen.mesh.positions, the display mesh, which is 32-bit. The in-plane axes come from the face normal by cross products; t is chosen away from n, so the division by |n × t| is not near zero.

  • addons/advanced-sim/lib/setup.js
  • lines 55 to 72
  • PartCraft 0.8.1
/** A flat face's 2-D extent in its own plane (u, v axes from its normal) from the rebuild's triangles. */
function extent(regen, fi, n) {
  const P = regen.mesh.positions, I = regen.mesh.indices, T = regen.mesh.triFace;
  const t = Math.abs(n[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0];
  const u = [n[1] * t[2] - n[2] * t[1], n[2] * t[0] - n[0] * t[2], n[0] * t[1] - n[1] * t[0]], ul = Math.hypot(...u);
  for (let k = 0; k < 3; k++) u[k] /= ul;
  const v = [n[1] * u[2] - n[2] * u[1], n[2] * u[0] - n[0] * u[2], n[0] * u[1] - n[1] * u[0]];
  const box = [Infinity, Infinity, -Infinity, -Infinity];
  for (let tr = 0; tr < T.length; tr++) {
    if (T[tr] !== fi) continue;
    for (let k = 0; k < 3; k++) {
      const q = 3 * I[3 * tr + k], x = P[q], y = P[q + 1], z = P[q + 2];
      const a = x * u[0] + y * u[1] + z * u[2], b = x * v[0] + y * v[1] + z * v[2];
      box[0] = Math.min(box[0], a); box[1] = Math.min(box[1], b); box[2] = Math.max(box[2], a); box[3] = Math.max(box[3], b);
    }
  }
  return { box, u, v };
}

Facing faces within a gap

64-bit

The pairs of flat faces that face each other, with their gap and overlap.

n·m ≤ −cos 0.5°; d = (o_B − o_A)·n with −1e-6 ≤ d ≤ gap; overlap = w × h

The gap d is one dot product of the difference of the two plane origins with the first normal, in 64-bit; the stored gap is max(0, d). The overlap is the product of the two boxes' common width and height from extent. The list is sorted by gap, then by overlap.

  • addons/advanced-sim/lib/setup.js
  • lines 74 to 98
  • PartCraft 0.8.1
/**
 * Facing flat faces of different pieces: normals opposite (within 0.5°), the second face in front of the first by 0 … gap mm, their outlines
 * overlapping when seen along the normal. Side A (the faces that press, contact.js "slave") is the smaller face of the two.
 */
export function findPairs(regen, { gap = GAP_DEFAULT } = {}) {
  if (!regen || !regen.faces) return [];
  const { pieces, of } = facePieces(regen);
  if (pieces < 2) return [];
  const flat = regen.faces.map((f, i) => ({ f, i })).filter(({ f, i }) => f.type === 'plane' && f.n && f.o && of.has(i));
  const out = [], cos = Math.cos(0.5 * Math.PI / 180);
  for (let x = 0; x < flat.length; x++) for (let y = x + 1; y < flat.length; y++) {
    const A = flat[x], B = flat[y];
    if (of.get(A.i) === of.get(B.i)) continue;
    const n = A.f.n, m = B.f.n, dot = n[0] * m[0] + n[1] * m[1] + n[2] * m[2];
    if (dot > -cos) continue;
    const d = (B.f.o[0] - A.f.o[0]) * n[0] + (B.f.o[1] - A.f.o[1]) * n[1] + (B.f.o[2] - A.f.o[2]) * n[2];   // B in front of A along A's outward normal
    if (!(d >= -1e-6 && d <= gap)) continue;
    const ea = extent(regen, A.i, n), eb = extent(regen, B.i, n);
    const w = Math.min(ea.box[2], eb.box[2]) - Math.max(ea.box[0], eb.box[0]), h = Math.min(ea.box[3], eb.box[3]) - Math.max(ea.box[1], eb.box[1]);
    if (!(w > 1e-6 && h > 1e-6)) continue;
    const [sa, sb] = (A.f.area || 0) <= (B.f.area || 0) ? [A, B] : [B, A];
    out.push({ a: sa.i, b: sb.i, gap: Math.max(0, d), overlap: w * h, pieces: [of.get(sa.i), of.get(sb.i)] });
  }
  return out.sort((p, q) => p.gap - q.gap || q.overlap - p.overlap);
}

In detail

Method
Two planes pair when their normals are opposite within 0.5°, the second lies 0 to gap mm in front of the first, and their boxes in the plane's own u, v axes overlap.
Number types
64-bit numbers for normals, origins and the gap; Float32Array mesh positions for the outline boxes.
Tolerances
cos 0.5° on the normals' dot product; gap from −1e-6 mm to the chosen gap (default 0.5 mm); overlap width and height above 1e-6 mm. All absolute, not scaled to the part.
Limits
The overlap test uses bounding boxes in the plane, not the true outlines, so two faces whose boxes overlap but whose outlines do not are still offered.

Approach: 32-bit step, flagged

One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.

Every policy that takes this approach