Resources Numerical accuracy Calculation policy

Interference between assembly parts

Finds pairs of placed parts that overlap, with the overlapping volume, and steps a joint through a range to check for clashes along the way.

How the precision is kept

Each part's mesh positions are copied into a Float32Array and handed to the manifold-3d library, placed with the 64-bit world matrix, and intersected pair by pair. The reported volume comes back from the library as a 64-bit number, but the solids it measures were built from 32-bit vertices of a faceted mesh.

Open item. Build the interference solids from 64-bit positions (or the exact shapes) so the reported overlap volume does not start from 32-bit vertices.

Calculations under this policy

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

assembly.js PartCraft 0.8.1 32-bit in part 3 calculations Reviewed

Tests tools/partcraft-test/test-clash-filters.mjs, bin/test-partcraft.mjs

Overlap volume of a pair

32-bit in part

The common volume of two placed parts and whether it counts as a clash.

v = volume(A ∩ B); clash when v > 1e-6

The solids are made from Float32Array copies of the mesh positions; the intersection and its volume are computed by manifold-3d. The threshold of 1e-6 mm³ is absolute. Bounding boxes are compared with the same 1e-6 used as a length.

  • assembly.js
  • lines 608 to 619
  • PartCraft 0.8.1
const A = solids[x], B = solids[y], a = A.m.boundingBox(), b = B.m.boundingBox(), pad = touch ? touchTol : 0;
if ([0, 1, 2].some((k) => a.max[k] < b.min[k] - tol - pad || b.max[k] < a.min[k] - tol - pad)) continue;
const common = T(A.m.intersect(B.m)), v = common.volume();
if (v > tol) {
  const za = zonesOf(A), zb = zonesOf(B), small = Math.max(tol, 1e-4 * v);
  let kind = 'overlap';
  if (za || zb) {
    const any = za && zb ? T(za.add(zb)) : za || zb;
    if (T(common.subtract(any)).volume() <= small) kind = za && zb && T(common.subtract(T(za.intersect(zb)))).volume() <= small ? 'thread' : 'thread-depth';
  }
  out.push({ a: A.id, b: B.id, volume: v, kind });
} else if (touch && A.m.minGap(B.m, touchTol) <= touchTol) out.push({ a: A.id, b: B.id, volume: 0, kind: 'touch' });

Thread zones

64-bit

Tubes around each cosmetic thread, used to tell a bolt in a tapped hole from a real clash.

r₀ = 0.97 × minor (default 0.8 × major); r₁ = 1.03 × major + 0.001; length = depth + 2 × max(0.05 × major, 0.001)

The margins are fixed factors chosen in the code, not derived from a thread standard. The tube is a ring between two 48-sided circles. This decides the kind of result, not the volume reported.

  • assembly.js
  • lines 589 to 605
  • PartCraft 0.8.1
const zonesOf = (sd) => {
  if (sd.zones !== undefined) return sd.zones;
  const tubes = [];
  for (const th of sd.b.threads?.values?.() || []) {
    if (th.modelled) continue;
    for (const z of th.list || []) {
      if (!(z.r1 > 0) || !(z.depth > 0)) continue;
      const r0 = Math.max(0, (z.r0 ?? z.r1 * 0.8) * 0.97), r1 = z.r1 * 1.03 + 1e-3, m = Math.max(0.05 * z.r1, 1e-3), d = V.norm(z.d);
      const x = V.norm(Math.abs(d[0]) < 0.9 ? V.cross(d, [1, 0, 0]) : V.cross(d, [0, 1, 0])), y = V.cross(d, x);
      const ring = T(T(M.CrossSection.circle(r1, 48)).subtract(T(M.CrossSection.circle(r0, 48))));
      const tube = T(T(ring.extrude(z.depth + 2 * m)).translate([0, 0, -m]));
      tubes.push(T(T(tube.transform(V.basisMatrix(x, y, d, z.o))).transform(sd.W)));
    }
  }
  sd.zones = tubes.length ? (tubes.length === 1 ? tubes[0] : T(M.Manifold.union(tubes))) : null;
  return sd.zones;
};

Motion check steps

64-bit

Steps one joint value from a start to an end and reports interference at each step.

value_s = from + (to − from) × s ÷ steps, s = 0 … steps

Each step value is computed from the ends, not by repeated addition, so steps do not accumulate rounding; the last step is from + (to − from), which can differ from the end value in its last bit. The range is clamped to the joint's limits first. Clashes between steps are not detected.

  • assembly.js
  • lines 672 to 693
  • PartCraft 0.8.1
/** Step a joint through its range and report interference at each step. → [{ value, pairs }] */
export function motionCheck(asm, builds, M, jointId, from, to, steps = 12, key = null) {
  const j = asm.joints.find((x) => x.id === jointId);
  if (!j) throw new Error(t('model.assembly.no_joint'));
  key = key || JOINT_DOF[j.type]?.[0] || 'angle';
  const keep = j[key];
  const L = limitOf(j, key);
  if (L) { from = Math.max(L.min, Math.min(L.max, from)); to = Math.max(L.min, Math.min(L.max, to)); }
  const out = [];
  const hold = new Set([j.id + '.' + key]);
  let start = null;
  try {
    for (let s = 0; s <= steps; s++) {
      j[key] = from + (to - from) * s / steps;
      // the swept value is held and a loop follows it, each step starting from the last (0.7 D7.40)
      const { world, loops } = placeAll(asm, builds, { hold, start });
      start = loops?.values?.size ? loops.values : null;
      out.push({ value: j[key], pairs: interference(asm, builds, world, M), closed: loops ? loops.closed : true });
    }
  } finally { j[key] = keep; }
  return out;
}

In detail

Method
Bounding-box rejection, then a mesh Boolean intersection per pair with manifold-3d and its volume. Cosmetic threads are modeled as tubes (48-sided circles) to classify overlaps.
Number types
Float32Array vertex positions into the library; 64-bit numbers for matrices, volumes and tolerances.
Tolerances
An overlap counts when its volume exceeds 1e-6 mm³ (absolute). Thread classification allows a remainder of max(1e-6, 1e-4 × overlap volume). Touching is within 1e-3 mm. Thread zones are widened: inner radius × 0.97, outer radius × 1.03 + 0.001 mm, length + 2 × max(5 % of the radius, 0.001 mm).
Limits
Volumes are of faceted meshes, so curved parts in a close fit can show a small overlap or miss one. The 1e-6 mm³ threshold does not scale with part size.

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