Resources Numerical accuracy Calculation policy

Automatic sheet layout

Splits a drawing into parts, picks a paper size and standard scale, and places a viewport for every part on the sheet.

How the precision is kept

Bounding boxes, viewport sizes and positions are 64-bit numbers. Sizes on paper are the model window times the scale plus fixed millimeter allowances, converted to paper units with 1 ÷ (millimeters per unit). Packing uses an absolute allowance of 1e-9 paper units. Scales come from the standard list and are stored as computed.

Calculations under this policy

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

autosheet.js CadCraft 0.8.2 64-bit 7 calculations Reviewed

Tests bin/test-autosheet.mjs

Clusters of the model

64-bit

Groups the model's visible objects whose bounding boxes are within a gap of each other, and returns each group's box.

Boxes are compared with the gap added directly and no further tolerance; boxes that overlap in x and y within the gap are joined by union-find. Each group's box is the exact minimum and maximum of its members.

  • autosheet.js
  • lines 25 to 42
  • CadCraft 0.8.2
/** Clusters of the model's visible objects whose boxes are within `gap` of each other (sweep over minX; no spatial index needed at this size). */
export function clusters(doc, gap, cap = 100000) {
  const items = [];
  for (const e of doc.model.values()) { if (!doc.visible(e)) continue; const b = boxOf(doc, e); if (b) items.push(b); if (items.length >= cap) break; }
  items.sort((a, b) => a.minX - b.minX);
  const parent = items.map((_, i) => i);
  const find = (i) => { while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; } return i; };
  let active = [];
  for (let i = 0; i < items.length; i++) {
    const a = items[i];
    active = active.filter((j) => items[j].maxX + gap >= a.minX);
    for (const j of active) { const b = items[j]; if (a.minY - gap <= b.maxY && a.maxY + gap >= b.minY) parent[find(i)] = find(j); }
    active.push(i);
  }
  const groups = new Map();
  items.forEach((b, i) => { const r = find(i); const g = groups.get(r) || emptyBounds(); growPt(g, b.minX, b.minY); growPt(g, b.maxX, b.maxY); groups.set(r, g); });
  return [...groups.values()];
}

Gap that separates parts

64-bit

The distance below which two objects count as one part.

gap = max(Δx, Δy, 1e-9) × 0.04

Scaled to the model: 4 % of the larger side of the visible model's box. The floor of 1e-9 drawing units is absolute.

  • autosheet.js
  • line 54
  • CadCraft 0.8.2
const gap = Math.max(whole.maxX - whole.minX, whole.maxY - whole.minY, 1e-9) * gapFrac;

Size of a part's viewport at a scale

64-bit

The paper width and height a part needs at a scale 1:n, with padding and a caption band.

w = Δx × s + 2 × 4 mm × k; h = Δy × s + 2 × 4 mm × k + 7 mm × k; k = 1 ÷ mm per paper unit

s is the unrounded viewport scale for 1:n. The 4 mm padding and 7 mm caption band are converted with k = 1 ÷ (mm per unit), a rounded number on inch paper.

  • autosheet.js
  • lines 75 to 78
  • CadCraft 0.8.2
const sizeAt = (doc, unit, part, n) => {
  const s = sOf(doc, unit, n), k = 1 / UNIT_MM[unit];
  return { s, w: (part.win.maxX - part.win.minX) * s + 2 * PAD_MM * k, h: (part.win.maxY - part.win.minY) * s + 2 * PAD_MM * k + CAP_MM * k };
};

Shelf packing

64-bit

Places boxes in rows inside the free zone and returns each box's lower-left corner, or nothing when they do not fit.

Fit tests allow 1e-9 paper units, an absolute tolerance that is not scaled to the sheet. Positions are running sums of widths, heights and gaps in the order the boxes are placed.

  • autosheet.js
  • lines 60 to 73
  • CadCraft 0.8.2
/** Shelf-pack boxes {w,h} into a zone; returns their lower-left corners or null. */
function pack(boxes, zw, zh, gap) {
  const order = boxes.map((b, i) => i).sort((a, b) => boxes[b].h - boxes[a].h || boxes[b].w - boxes[a].w);
  const at = new Array(boxes.length);
  let x = 0, y = zh, rowH = 0;                       // fill from the top of the zone down
  for (const i of order) {
    const b = boxes[i];
    if (b.w > zw + 1e-9 || b.h > zh + 1e-9) return null;
    if (x > 0 && x + b.w > zw + 1e-9) { x = 0; y -= rowH + gap; rowH = 0; }
    if (y - b.h < -1e-9) return null;
    at[i] = { x, y: y - b.h }; x += b.w + gap; rowH = Math.max(rowH, b.h);
  }
  return at;
}

Scale and place for every part on one paper

64-bit

Chooses the scale or scales and the position of each viewport on a given paper.

Scales are tried from the largest (smallest n). The single-part fit test compares sizes with no allowance, while the packing test allows 1e-9. With a scale per part, the part covering the most paper area goes one step smaller until everything packs, at most 2000 times.

  • autosheet.js
  • lines 80 to 109
  • CadCraft 0.8.2
/** One sheet's arrangement on a given paper: { items[{part, n, s, x, y, w, h}], zone } or null. */
function arrange(doc, paper, parts, { scale = null, each = false } = {}) {
  const unit = paper.unit, k = 1 / UNIT_MM[unit], zone = viewZone(paper), zw = zone.x1 - zone.x0, zh = zone.y1 - zone.y0, gap = GAP_MM * k;
  const stops = scaleList(doc).map((x) => x.n).sort((a, b) => a - b);          // 1:1 … 1:10000, smallest n = largest scale first
  const fitIdx = (part) => { for (let i = 0; i < stops.length; i++) { const z = sizeAt(doc, unit, part, stops[i]); if (z.w <= zw && z.h <= zh) return i; } return -1; };
  let idx;
  if (scale) { const n = scale; idx = parts.map(() => ({ n })); }
  else if (!each) {
    let common = -1;
    for (let i = 0; i < stops.length && common < 0; i++) if (pack(parts.map((p) => sizeAt(doc, unit, p, stops[i])), zw, zh, gap)) common = i;
    if (common < 0) return null;
    idx = parts.map(() => ({ n: stops[common] }));
  } else {
    const at = parts.map(fitIdx);
    if (at.some((i) => i < 0)) return null;
    for (let guard = 0; guard < 2000; guard++) {
      const boxes = parts.map((p, i) => sizeAt(doc, unit, p, stops[at[i]]));
      if (pack(boxes, zw, zh, gap)) break;
      let worst = -1, wa = -1;                       // make the part that takes the most paper one step smaller
      boxes.forEach((b, i) => { if (at[i] < stops.length - 1 && b.w * b.h > wa) { wa = b.w * b.h; worst = i; } });
      if (worst < 0) return null;
      at[worst]++;
    }
    idx = at.map((i) => ({ n: stops[i] }));
  }
  const boxes = parts.map((p, i) => sizeAt(doc, unit, p, idx[i].n));
  const place = pack(boxes, zw, zh, gap);
  if (!place) return null;
  return { zone, items: parts.map((part, i) => ({ part, n: idx[i].n, s: boxes[i].s, w: boxes[i].w, h: boxes[i].h, x: zone.x0 + place[i].x, y: zone.y0 + place[i].y })) };
}

Scale on automatic paper

64-bit

With no scale given and automatic paper, the scale the largest part gets on the default paper.

The largest part is the one with the largest window area (Δx × Δy). The scale is the first listed scale, largest first, at which that part fits the default paper's view zone, compared with no allowance.

  • autosheet.js
  • lines 121 to 126
  • CadCraft 0.8.2
let fixed = scale;
if (!fixed && pk === 'auto' && !each) {
  const dp = makePaper(defaultPaper(doc).key, portrait), zone = viewZone(dp), unit = dp.unit;
  const big = parts.reduce((a, b) => ((b.win.maxX - b.win.minX) * (b.win.maxY - b.win.minY) > (a.win.maxX - a.win.minX) * (a.win.maxY - a.win.minY) ? b : a));
  for (const x of scaleList(doc).slice().sort((a, b) => a.n - b.n)) { const z = sizeAt(doc, unit, big, x.n); if (z.w <= zone.x1 - zone.x0 && z.h <= zone.y1 - zone.y0) { fixed = x.n; break; } }
}

Viewport placed for a part

64-bit

The viewport stored for each part: paper size and center, model center and scale.

h = packed h − 7 mm × k; paper center = (x + w ÷ 2, y + 7 mm × k + h ÷ 2); model center = midpoint of the window

64-bit sums and halvings. The stored scale is the one the packing used, unrounded.

  • autosheet.js
  • lines 177 to 180
  • CadCraft 0.8.2
const w = it.w, h = it.h - CAP_MM * k, cxp = it.x + w / 2, cyp = it.y + CAP_MM * k + h / 2;
const frz = [...doc.layers.values()].filter((l) => l.newVpFrozen).map((l) => l.name);
const win = it.part.win;
doc.add({ type: 'viewport', layer: 'Viewports', color: null, x: cxp, y: cyp, w, h, cx: (win.minX + win.maxX) / 2, cy: (win.minY + win.maxY) / 2, s: it.s, ...(frz.length ? { frz } : {}) }, layout.entities);

In detail

Method
Shelf packing: parts, tallest first, in rows from the top of the free zone with an 8 mm gap. With a common scale, the largest listed scale at which all parts pack; with a scale per part, the part covering the most paper goes one scale smaller until all pack.
Number types
64-bit numbers.
Tolerances
Packing fit: 1e-9 paper units (absolute; millimeters or inches). Cluster gap: 4 % of the model's larger extent, floored at 1e-9 drawing units. Single-part fit tests use no allowance.
Limits
At most 60 parts and 100000 boxes are read. The per-part scale search stops after 2000 steps.

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