Resources Numerical accuracy Calculation policy

Nesting add-on: outlines in, placed copies out

Reads closed outlines and their holes from the drawing as parts for the nesting engine, and writes the placed copies and sheet rectangles back.

How the precision is kept

Outlines are passed to the engine with their bulges, so arcs stay arcs. Each part is shifted by its own lower-left corner and the same shift is undone when the copy is placed, in 64-bit numbers with nothing rounded. Arcs are turned into 8 chords only to decide which outline lies inside which.

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.

addons/nesting/lib/parts.js CadCraft 0.8.2 64-bit 5 calculations Reviewed

Tests None found by the review

A drawing object as a nest loop

64-bit

Turns a circle or closed polyline into a loop of points with bulges.

circle = (cx + r, cy) and (cx − r, cy), each with bulge 1

A circle becomes two exact half-circle bulges; a polyline's points and bulges are copied unchanged.

  • addons/nesting/lib/parts.js
  • lines 17 to 22
  • CadCraft 0.8.2
/** A closed drawing object as a nest loop ({ x, y, b } with DXF bulges), or null. */
export function loopOf(e) {
  if (e.type === 'circle' && e.r > 0) return [{ x: e.cx + e.r, y: e.cy, b: 1 }, { x: e.cx - e.r, y: e.cy, b: 1 }];
  if (e.type === 'poly' && e.closed && e.pts && e.pts.length >= 2) return e.pts.map((p) => ({ x: p.x, y: p.y, b: p.b || 0 }));
  return null;
}

Outline as chords for containment

64-bit

Points along an outline, with each arc as 8 chords, used to test which outline contains which.

θ = 4 atan(b); r = c ÷ (2 sin(θ ÷ 2)); center = chord middle + normal × r cos(θ ÷ 2)

A fixed 8 chords per arc whatever its size, so the polygon can be well inside a large arc. It is used for containment, the size ranking and the part's corner, not for the geometry handed to the engine. A zero-length chord with a bulge would divide by zero.

  • addons/nesting/lib/parts.js
  • lines 23 to 38
  • CadCraft 0.8.2
/** Points along a loop for containment tests (bulges as a few chords). */
function flat(loop) {
  const out = [];
  for (let i = 0; i < loop.length; i++) {
    const p = loop[i], q = loop[(i + 1) % loop.length];
    out.push([p.x, p.y]);
    if (Math.abs(p.b || 0) > 1e-12) {
      const th = 4 * Math.atan(p.b), dx = q.x - p.x, dy = q.y - p.y, c = Math.hypot(dx, dy), r = c / (2 * Math.sin(th / 2));
      // the centre is on the chord's left (signed r: a negative bulge turns clockwise, its centre on the right)
      const mx = (p.x + q.x) / 2, my = (p.y + q.y) / 2, h = r * Math.cos(th / 2), nx = -dy / c, ny = dx / c, cx = mx + nx * h, cy = my + ny * h;
      const a0 = Math.atan2(p.y - cy, p.x - cx), R = Math.abs(r);
      for (let k = 1; k < 8; k++) { const a = a0 + th * k / 8; out.push([cx + R * Math.cos(a), cy + R * Math.sin(a)]); }
    }
  }
  return out;
}

Parts and their holes

64-bit

Groups outlines into parts with holes and moves each part to its own corner.

parent = smallest larger outline containing the first point; shifted point = p − (x₀, y₀)

The corner (x₀, y₀) is the minimum of the chord points, so on an arc that bulges past its end points it can be slightly inside the true corner. The same corner is subtracted here and in placeEntity, so the placed copy is not affected.

  • addons/nesting/lib/parts.js
  • lines 42 to 66
  • CadCraft 0.8.2
export function partsFrom(entities) {
  const shapes = [], skipped = [];
  for (const e of entities) {
    const loop = loopOf(e);
    if (!loop) { skipped.push({ id: e.id, why: e.type === 'poly' || e.type === 'circle' ? 'open' : 'type' }); continue; }
    const f = flat(loop);
    shapes.push({ e, loop, f, a: area(f) });
  }
  // the nearest shape round each one (the smallest that contains its first point); depth by walking up
  for (const s of shapes) {
    let best = null;
    for (const o of shapes) if (o !== s && o.a > s.a && inside(s.f[0], o.f) && (!best || o.a < best.a)) best = o;
    s.parent = best;
  }
  const depth = (s) => { let d = 0; for (let p = s.parent; p; p = p.parent) d++; return d; };
  const parts = [];
  for (const s of shapes) if (depth(s) % 2 === 0) {
    const holes = shapes.filter((h) => h.parent === s && depth(h) % 2 === 1);
    let x0 = Infinity, y0 = Infinity;
    for (const [x, y] of s.f) { x0 = Math.min(x0, x); y0 = Math.min(y0, y); }
    const shift = (l) => l.map((p) => ({ x: p.x - x0, y: p.y - y0, b: p.b }));
    parts.push({ name: s.e.name || s.e.layer || '', loops: [shift(s.loop), ...holes.map((h) => shift(h.loop))], ents: [s.e, ...holes.map((h) => h.e)], origin: [x0, y0] });
  }
  return { parts, skipped };
}

A part's object placed on the sheet

64-bit

Moves and turns one object of a part to where the engine placed it.

q = at + (x, y) + R(rot) · (p − origin)

The cosine and sine are computed once per object. Bulges and circle radii are copied unchanged. The placement's mirror flag is not read.

  • addons/nesting/lib/parts.js
  • lines 68 to 75
  • CadCraft 0.8.2
/** One object of a part placed: translate to the part's origin, turn by rot, then move to (x, y) + at. */
export function placeEntity(e, origin, pl, at, layer = null) {
  const c = Math.cos(pl.rot), s = Math.sin(pl.rot);
  const T = (x, y) => { const u = x - origin[0], v = y - origin[1]; return { x: at[0] + pl.x + c * u - s * v, y: at[1] + pl.y + s * u + c * v }; };
  const keep = { layer: layer || e.layer, color: e.color ?? null, ...(e.linetype ? { linetype: e.linetype } : {}) };
  if (e.type === 'circle') { const q = T(e.cx, e.cy); return { type: 'circle', ...keep, cx: q.x, cy: q.y, r: e.r }; }
  return { type: 'poly', ...keep, closed: true, pts: e.pts.map((p) => ({ ...T(p.x, p.y), b: p.b || 0 })) };
}

Sheet rectangles and labels

64-bit

A rectangle for each sheet, side by side, and the part names over the copies.

xₖ₊₁ = xₖ + w + gap, gap = max(20, 0.1 × h) by default; label at the mean of the outline's points

Sheet offsets are a running sum. A label sits at the average of the outline's vertices, which is not the centroid of the area.

  • addons/nesting/lib/parts.js
  • lines 78 to 97
  • CadCraft 0.8.2
export function sheetEntities(result, sheet, at, { gap = null, layer = 'NEST-SHEET', labelLayer = 'NEST-LABELS', textH = null } = {}) {
  const n = Math.max(1, result.sheets.length);
  const W = sheet.w && Number.isFinite(sheet.w) ? sheet.w : null, H = sheet.h;
  const g = gap ?? Math.max(20, 0.1 * H);
  const sheets = [], offsets = [];
  let x = at[0];
  for (let k = 0; k < n; k++) {
    const w = W ?? Math.max(1, result.sheets[k]?.used || result.length || 1);
    offsets.push([x, at[1]]);
    sheets.push({ type: 'poly', layer, color: null, closed: true, pts: [{ x, y: at[1], b: 0 }, { x: x + w, y: at[1], b: 0 }, { x: x + w, y: at[1] + H, b: 0 }, { x, y: at[1] + H, b: 0 }] });
    x += w + g;
  }
  const h = textH ?? Math.max(1, H / 60);
  const labels = result.placed.map((pl) => {
    const o = offsets[pl.sheet] || offsets[0];
    let cx = 0, cy = 0; const L = pl.loops[0]; for (const p of L) { cx += p.x; cy += p.y; } cx /= L.length; cy /= L.length;
    return { type: 'text', layer: labelLayer, color: null, x: o[0] + cx, y: o[1] + cy, h, rot: 0, s: pl.name || String(pl.part + 1), ha: 'center', va: 'middle' };
  });
  return { sheets, labels, offsets };
}

In detail

Method
Even-odd point-in-polygon on chord polygons for containment; smallest containing outline as the parent; even depth is a part, odd a hole; placement q = at + (x, y) + R(rot) · (p − origin).
Number types
64-bit numbers.
Tolerances
A bulge of 1e-12 or less is a straight edge.
Limits
Containment is tested with one point of each outline against an 8-chord approximation of arcs. A mirrored placement is not applied by placeEntity.

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