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.jsPartCraft 0.8.1 32-bit in part3 calculationsReviewed
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.
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.