Resources Numerical accuracy Calculation policy

Welding of imported geometry

How the points of an imported file (OBJ, glTF, Collada, DXF, DWG) are joined into shared vertices and its polygons into faces.

How the precision is kept

Imported points are 64-bit and are kept as read, but two points are joined when they fall in the same cell of a 1e-5 m (0.01 mm) grid: the first point in a cell gives the vertex its coordinates and later ones are moved onto it. Polygons that are not flat within a tolerance scaled to their size come in as triangle fans.

Open item. Weld by distance with the kernel tolerance (1e-6 m) instead of a 1e-5 m grid, so imported points are not moved by up to 0.01 mm.

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.

m3d/model.js CadCraft 0.8.2 64-bit 2 calculations Reviewed

Tests bin/test-cadcraft-3d.mjs

Faces from imported polygons

64-bit

Builds faces from the polygons of an imported file, sharing vertices between them.

key = round(x × 1e5), round(y × 1e5), round(z × 1e5)

Points with the same key (a 1e-5 m grid) share one vertex, which keeps the first point's 64-bit coordinates; a later point in that cell is moved to it, by up to about 0.017 mm across the cell's diagonal. A polygon whose corners leave its Newell plane by more than max(EPS, size × 1e-6) becomes a fan of triangles.

  • m3d/model.js
  • lines 3441 to 3498
  • CadCraft 0.8.2
/**
 * A definition from imported polygons. Each entry is either an array of points (one outer loop)
 * or {loops: [outer, ...holes], color}. Holes are turned to run against the outer loop; a polygon
 * that is not flat (beyond 1 µm per meter of size) comes in as a fan of triangles. Edges between
 * coplanar faces are soft (a triangulated flat area still reads as one surface). `segs` ([p, q]
 * pairs) become plain edges in the same definition, welded to the faces' vertices.
 */
export function meshDef(model, polys, name, kind = 'group', segs = []) {
  const d = model.newDef(kind, name);
  const grid = new Map();
  const vid = (p) => {
    const k = p.map((x) => Math.round(x * 1e5)).join(',');
    let id = grid.get(k);
    if (id === undefined) { id = vAdd(d, p); grid.set(k, id); }
    return id;
  };
  const ring = (pts) => {
    const loop = [];
    for (const p of pts) { const v = vid(p); if (loop[loop.length - 1] !== v) loop.push(v); }
    while (loop.length > 1 && loop[0] === loop[loop.length - 1]) loop.pop();
    if (loop.length < 3 || new Set(loop).size !== loop.length) return null;
    return polyArea3(loop.map((v) => d.verts.get(v))) <= EPS2 ? null : loop;
  };
  for (const poly of polys) {
    const obj = !Array.isArray(poly);
    const rings = obj ? (Array.isArray(poly.loops) ? poly.loops : []) : [poly];
    const color = obj && typeof poly.color === 'string' ? poly.color : undefined;
    const outer = rings.length ? ring(rings[0]) : null;
    if (!outer) continue;
    const P = outer.map((v) => d.verts.get(v));
    const n = newell(P);
    let size = 0;
    for (const p of P) size = Math.max(size, dist(p, P[0]));
    const tol = Math.max(EPS, size * 1e-6);
    if (P.some((p) => Math.abs(dot(n, sub(p, P[0]))) > tol)) {
      // not flat: a fan of triangles (each one flat by definition)
      for (let i = 1; i + 1 < outer.length; i++) {
        const t = [outer[0], outer[i], outer[i + 1]];
        if (polyArea3(t.map((v) => d.verts.get(v))) > EPS2) fAdd(d, [t], color);
      }
      continue;
    }
    const holes = [];
    for (const r of rings.slice(1)) {
      const h = ring(r);
      if (!h || h.some((v) => outer.includes(v))) continue;
      holes.push(dot(newell(h.map((v) => d.verts.get(v))), n) > 0 ? h.reverse() : h);
    }
    fAdd(d, [outer, ...holes], color);
  }
  for (const [p, q] of segs) { const a = vid(p), b = vid(q); if (a !== b) eAdd(d, a, b); }
  const ef = edgeFaces(d);
  for (const [eid, e] of d.edges) {
    const fs = ef.get(key(e.a, e.b)) || [];
    if (fs.length === 2 && dot(faceNormal(d, d.faces.get(fs[0])), faceNormal(d, d.faces.get(fs[1]))) > 1 - 1e-6) e.soft = true;
  }
  return d;
}

Edges from imported segments

64-bit

Builds plain edges from the line segments of an imported drawing.

key = round(x × 1e5), round(y × 1e5), round(z × 1e5)

The same 1e-5 m weld grid as for faces; a segment whose two ends fall in one cell is dropped.

  • m3d/model.js
  • lines 3500 to 3512
  • CadCraft 0.8.2
/** Plain edges (no faces) from a list of [p, q] segments, e.g. a site plan. */
export function edgesDef(model, segs, name) {
  const d = model.newDef('group', name);
  const grid = new Map();
  const vid = (p) => {
    const k = p.map((x) => Math.round(x * 1e5)).join(',');
    let id = grid.get(k);
    if (id === undefined) { id = vAdd(d, p); grid.set(k, id); }
    return id;
  };
  for (const [p, q] of segs) { const a = vid(p), b = vid(q); if (a !== b) eAdd(d, a, b); }
  return d;
}

In detail

Method
Grid hashing of Math.round(x × 1e5) per coordinate; Newell normal and flatness test per polygon.
Number types
64-bit numbers.
Tolerances
Weld grid 1e-5 m. Flatness max(EPS, size × 1e-6) with EPS = 1e-6 m. Polygons of area EPS² (1e-12 m²) or less are dropped. Edges between faces whose normals agree within 1 − 1e-6 are made soft.
Limits
Detail finer than 0.01 mm in an imported file is merged. Two points closer than the grid size but on either side of a grid line are not merged.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach