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