Resources Numerical accuracy Calculation policy

Library item geometry from blocks, meshes and tables

Builds a Library item's plan and 3D body in millimeters from a drawing block, a mesh file (glTF, OBJ, STL) or a table of sizes.

How the precision is kept

Every coordinate, size, radius and angle written into an item is rounded to 4 decimals (0.1 µm for a length in millimeters), and a plan outline made from a mesh is rounded to 2 decimals. Curves that cannot be kept (arcs under an uneven scale, splines) become short straight pieces. Binary STL files and glTF float data hold 32-bit numbers, which limits what is read from them.

Open item. Take an arc's bounds from its own quadrant points instead of from sampled points.

Calculations under this policy

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

libitem.js CadCraft 0.8.2 64-bit 18 calculations Reviewed

Tests bin/test-cadcraft.mjs, bin/test-bimcraft.mjs

Rounding of stored values

64-bit

The two rounding helpers every stored number of an item goes through.

r4(v) = round(v × 10000) ÷ 10000; r2(v) = round(v × 100) ÷ 100

r4 is applied to the coordinates, sizes and angles this file writes into an item, so the kept value has at most 4 decimals and not the full 64-bit result; for a length in millimeters the step is 0.1 µm. r2 is used for convex hull points.

  • libitem.js
  • lines 870 to 871
  • CadCraft 0.8.2
const r4 = (v) => Math.round(v * 10000) / 10000;     // coordinates: 0.1 µm is plenty
const r2 = (v) => Math.round(v * 100) / 100;

Block to millimeters

64-bit

The transform from a block's own coordinates and units to the item's millimeters.

M = [mm, 0, 0, mm, −base.x·mm, −base.y·mm]; scale of a transform = √|det|; angle = r4(atan2(y, x) in degrees)

The block's base point and its unit's millimeters form the root matrix. A transform counts as shape-keeping when its column lengths agree and its columns are perpendicular within 1e-9, relative. Angles are divided by π ÷ 180 and rounded to 4 decimals of a degree.

  • libitem.js
  • lines 1594 to 1602
  • CadCraft 0.8.2
// m: 2D affine [a,b,c,d,e,f] in mm; zs: z scale; zo: z offset (mm)
const root = { m: [mm, 0, 0, mm, -base.x * mm, -base.y * mm], zs: mm, zo: -(base.z || 0) * mm, layer: null, color: null, depth: 0 };

const P = (m, x, y) => [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
const lin = (m, x, y) => [m[0] * x + m[2] * y, m[1] * x + m[3] * y];
const det = (m) => m[0] * m[3] - m[1] * m[2];
const similar = (m) => Math.abs(Math.hypot(m[0], m[1]) - Math.hypot(m[2], m[3])) < 1e-9 * Math.hypot(m[0], m[1]) && Math.abs(m[0] * m[2] + m[1] * m[3]) < 1e-9 * (m[0] * m[0] + m[1] * m[1]);
const scaleOf = (m) => Math.sqrt(Math.abs(det(m)));
const ang = (x, y) => { let a = Math.atan2(y, x) / D2R; if (a < 0) a += 360; return r4(a); };

Lines, circles and arcs of a block

64-bit

A block's lines, circles and arcs as plan entities in millimeters.

End points and centers go through the matrix and are rounded to 4 decimals; a radius is multiplied by √|det| and rounded. Arc angles are the directions of the transformed start and end angles in degrees, rounded to 4 decimals and swapped under a mirror. Under an uneven scale a circle or arc becomes a polyline at 5° steps.

  • libitem.js
  • lines 1641 to 1656
  • CadCraft 0.8.2
case 'LINE': { const [x1, y1] = P(m, e.start.x, e.start.y), [x2, y2] = P(m, e.end.x, e.end.y); push({ type: 'line', layer, color, x1: r4(x1), y1: r4(y1), x2: r4(x2), y2: r4(y2) }); break; }
case 'CIRCLE': {
  if (sim) { const [x, y] = P(m, e.center.x, e.center.y); push({ type: 'circle', layer, color, x: r4(x), y: r4(y), r: r4(e.radius * s) }); }
  else polyFrom(tess(sampleArcRad(e.center.x, e.center.y, e.radius, 0, 2 * Math.PI), m), true, ctx, layer, color);
  break;
}
case 'ARC': {
  if (sim) {
    const [x, y] = P(m, e.center.x, e.center.y);
    const s0 = lin(m, Math.cos(e.startAngle), Math.sin(e.startAngle)), s1 = lin(m, Math.cos(e.endAngle), Math.sin(e.endAngle));
    const a0 = ang(...s0), a1 = ang(...s1);
    push({ type: 'arc', layer, color, x: r4(x), y: r4(y), r: r4(e.radius * s), a0: flip ? a1 : a0, a1: flip ? a0 : a1 });
  } else polyFrom(tess(sampleArcRad(e.center.x, e.center.y, e.radius, e.startAngle, e.endAngle), m), false, ctx, layer, color);
  break;
}
case 'LWPOLYLINE': case 'POLYLINE_2D': case 'POLYLINE_3D': {

Nested block references

64-bit

The transform of a block reference inside the block, with its rows and columns.

local = T(insert) · R(rotation) · S(sx, sy) · T(−base + column·spacing ÷ sx, row·spacing ÷ sy)

Each nested reference's matrix is multiplied into its parent's without rounding; rounding happens only when an entity is written. Array spacing is divided by the scale because it is applied inside the scaled frame. A scale of 0 is read as 1.

  • libitem.js
  • lines 1718 to 1732
  • CadCraft 0.8.2
case 'INSERT': case 'MINSERT': {
  const sub = blocks.get(e.block);
  if (!sub) { skip(msg('libitem.block.skip.missing')); break; }
  if (sub.flags & 4) { skip(msg('libitem.block.skip.xref')); break; }
  if (ctx.depth > 20) { skip(msg('libitem.block.skip.deep')); break; }
  const b = sub.base || { x: 0, y: 0, z: 0 };
  const sx = e.scale?.x || 1, sy = e.scale?.y || 1, sz = e.scale?.z || 1, ip = e.insert;
  const cols = Math.max(1, e.cols || 1), rows = Math.max(1, e.rows || 1);
  for (let r = 0; r < rows; r++) for (let cc = 0; cc < cols; cc++) {
    const local = mulA(mulA([1, 0, 0, 1, ip.x, ip.y], rotA(e.rotation || 0)),
      mulA([sx, 0, 0, sy, 0, 0], [1, 0, 0, 1, -b.x + cc * (e.colSpacing || 0) / sx, -b.y + r * (e.rowSpacing || 0) / sy]));
    const zs = ctx.zs * sz;
    const subCtx = { m: mulA(m, local), zs, zo: ctx.zo + ((ip.z || 0) - (b.z || 0) * sz) * ctx.zs, layer, color, depth: ctx.depth + 1 };
    for (const be of sub.entities || []) if (be.type !== 'ATTDEF') emit(be, subCtx);
  }

Mesh body from a block's 3D faces

64-bit

The item's 3D body and height when the block holds 3D faces.

Corners are rounded to 4 decimals, and corners whose rounded numbers print the same are merged into one vertex. The height is the largest rounded z minus the smallest.

  • libitem.js
  • lines 1761 to 1773
  • CadCraft 0.8.2
if (faces.length >= 6 && faces.some((p) => Math.abs(p[2] - faces[0][2]) > 1e-6)) {
  const positions = [], indices = [], seen = new Map();
  for (const p of faces) {
    const k = p.map(r4).join(',');
    let i = seen.get(k);
    if (i === undefined) { i = positions.length / 3; seen.set(k, i); positions.push(...p.map(r4)); }
    indices.push(i);
  }
  if (indices.length / 3 <= LIMITS.triangles) {
    it.body = { type: 'mesh', positions, indices };
    let zMin = Infinity, zMax = -Infinity;
    for (let i = 2; i < positions.length; i += 3) { zMin = Math.min(zMin, positions[i]); zMax = Math.max(zMax, positions[i]); }
    if (!it.params.height) it.params.height = r4(zMax - zMin);

Arcs as points, 5° steps

64-bit

The points that replace a circle or arc that cannot stay an arc.

n = max(8, ⌈sweep ÷ 5°⌉); point = c + r·(cos a, sin a)

Used for circles and arcs under an uneven scale and for the arc edges of hatch boundaries. The points are on the arc; the straight pieces between them are up to r × (1 − cos 2.5°) ≈ 0.001 r inside it.

  • libitem.js
  • lines 1800 to 1806
  • CadCraft 0.8.2
function sampleArcRad(cx, cy, r, a0, a1) {
  let sweep = a1 - a0;
  while (sweep <= 0) sweep += 2 * Math.PI;
  const n = Math.max(8, Math.ceil(sweep / (5 * D2R))), out = [];
  for (let i = 0; i <= n; i++) { const a = a0 + sweep * i / n; out.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]); }
  return out;
}

Splines as points

64-bit

The points that replace a block's spline in the item's plan.

de Boor recurrence at u = lo + (hi − lo)·s ÷ n, n = max(16, 8 × control points)

A spline with fit points is replaced by straight lines between the fit points. Otherwise the curve is evaluated at equal parameter steps (with weights when the spline has them) and joined by straight lines; the last point is evaluated at the end parameter minus 1e-9 of the range and not at the end itself. A zero knot span gives a factor of 0 instead of a division by zero.

  • libitem.js
  • lines 1807 to 1827
  • CadCraft 0.8.2
function splinePoints(e) {
  if (e.fit && e.fit.length > 1) return e.fit.map((p) => [p.x, p.y]);
  const cp = e.control || [], k = e.knots || [], p = e.degree || 3;
  if (cp.length < 2) return [];
  if (k.length !== cp.length + p + 1) return cp.map((q) => [q.x, q.y]);
  const w = e.weights && e.weights.length === cp.length ? e.weights : null;
  const lo = k[p], hi = k[k.length - p - 1], n = Math.max(16, cp.length * 8), pts = [];
  for (let s = 0; s <= n; s++) {
    const u = s === n ? hi - 1e-9 * (hi - lo) : lo + (hi - lo) * (s / n);
    let span = p;
    while (span < cp.length - 1 && u >= k[span + 1]) span++;
    const d = [];
    for (let j = 0; j <= p; j++) { const q = cp[span - p + j], wt = w ? w[span - p + j] : 1; d.push([q.x * wt, q.y * wt, wt]); }
    for (let r = 1; r <= p; r++) for (let j = p; j >= r; j--) {
      const i = span - p + j, den = k[i + p - r + 1] - k[i], a = den === 0 ? 0 : (u - k[i]) / den;
      d[j] = [(1 - a) * d[j - 1][0] + a * d[j][0], (1 - a) * d[j - 1][1] + a * d[j][1], (1 - a) * d[j - 1][2] + a * d[j][2]];
    }
    pts.push([d[p][0] / d[p][2], d[p][1] / d[p][2]]);
  }
  return pts;
}

Arc points for bounds

64-bit

The points of an arc used to find a plan's bounds.

n = max(4, ⌈sweep ÷ 10°⌉); point = c + r·(cos a, sin a)

Angles are in degrees and converted with Math.PI ÷ 180. The steps are at most 10°, so an extreme of the arc that falls between two points is missed by up to r × (1 − cos 5°) ≈ 0.0038 r.

  • libitem.js
  • lines 886 to 892
  • CadCraft 0.8.2
function arcSample(cx, cy, r, a0, a1) {
  let sweep = a1 - a0;
  while (sweep <= 0) sweep += 360;
  const n = Math.max(4, Math.ceil(sweep / 10)), out = [];
  for (let i = 0; i <= n; i++) { const a = (a0 + sweep * i / n) * D2R; out.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]); }
  return out;
}

Bounds of a plan

64-bit

The box round an item's plan, from which its width and depth are taken.

The smallest and largest of the entities' points. A circle counts by center ± radius exactly; an arc, an ellipse and a bulged polyline segment count by sampled points, so their bounds can be slightly short. Text counts by its anchor point only.

  • libitem.js
  • lines 926 to 937
  • CadCraft 0.8.2
/** Bounds of the plan's geometry in mm; text counts by its anchor point only. null when empty. */
export function planBounds(plan) {
  const b = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
  for (const e of plan?.entities || []) {
    for (const [x, y] of entityPoints(e)) {
      if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
      if (x < b.minX) b.minX = x; if (x > b.maxX) b.maxX = x;
      if (y < b.minY) b.minY = y; if (y > b.maxY) b.maxY = y;
    }
  }
  return b.minX <= b.maxX ? b : null;
}

Box body on a plan

64-bit

A box body standing on the plan's bounds with a given height.

w = max x − min x; d = max y − min y; each of x, y, z, w, d, h rounded to 4 decimals

The width and depth are differences of the plan's bounds, then rounded to 4 decimals along with the corner and the height.

  • libitem.js
  • lines 939 to 946
  • CadCraft 0.8.2
/** A box body standing on the plan's bounds, `h` mm tall from `z`. null without a plan or a height. */
export function boxFromPlan(plan, h, z = 0) {
  const b = planBounds(plan);
  if (!b || !(h > 0)) return null;
  const w = b.maxX - b.minX, d = b.maxY - b.minY;
  if (!(w > 0) || !(d > 0)) return null;
  return { type: 'box', x: r4(b.minX), y: r4(b.minY), z: r4(z), w: r4(w), d: r4(d), h: r4(h) };
}

Item from a mesh file

64-bit

A mesh file's triangles as an item body in millimeters, with its footprint and sizes.

p = file point × unit in mm; Y-up: (x, y, z) → (x, −z, y); stored = r4(p − (center x, max y, min z))

Positions are multiplied by the unit's millimeters, turned Z-up and shifted so the box is centered in X with its back on y = 0 and its bottom on z = 0; each stored coordinate is then rounded to 4 decimals. Width, depth and height are the box sizes before the shift, rounded to 4 decimals. The plan outline is the convex hull of the stored points.

  • libitem.js
  • lines 1271 to 1314
  • CadCraft 0.8.2
export function itemFromMesh(bytes, kind, { units = null, upAxis = null, name = '' } = {}) {
  const notes = [];
  const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
  let tri;
  if (kind === 'obj') {
    tri = readObj(new TextDecoder().decode(u8), notes);
    if (!units) notes.push(msg('libitem.mesh.obj_units'));
    if (!upAxis) notes.push(msg('libitem.mesh.obj_up'));
  } else if (kind === 'stl') {
    tri = readStl(u8, notes);
    if (!units) notes.push(msg('libitem.mesh.stl_units'));
    if (!upAxis) notes.push(msg('libitem.mesh.stl_up'));
  } else {
    tri = readGltf(u8, notes);
    if (units && units !== 'm') notes.push(msg('libitem.mesh.gltf_units', { units }));
  }
  if (!tri) return { items: [], notes };
  const scale = UNIT_MM[units || (kind === 'obj' || kind === 'stl' ? 'mm' : 'm')] || 1;
  const up = upAxis || (kind === 'stl' ? 'z' : 'y');
  const P = tri.positions, n = P.length / 3;
  if (tri.indices.length / 3 > LIMITS.triangles) return { items: [], notes: [...notes, msg('libitem.mesh.too_many', { n: tri.indices.length / 3, limit: LIMITS.triangles })] };
  const out = new Array(P.length);
  let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
  for (let i = 0; i < n; i++) {
    let x = P[i * 3] * scale, y = P[i * 3 + 1] * scale, z = P[i * 3 + 2] * scale;
    if (up === 'y') { const t = y; y = -z; z = t; }   // Y-up → Z-up
    out[i * 3] = x; out[i * 3 + 1] = y; out[i * 3 + 2] = z;
    if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
  }
  if (!(maxX > minX) || !(maxY > minY)) return { items: [], notes: [...notes, msg('libitem.mesh.flat')] };
  const cx = (minX + maxX) / 2;
  for (let i = 0; i < n; i++) { out[i * 3] = r4(out[i * 3] - cx); out[i * 3 + 1] = r4(out[i * 3 + 1] - maxY); out[i * 3 + 2] = r4(out[i * 3 + 2] - minZ); }
  notes.push(msg('libitem.mesh.moved'));
  const it = baseItem(name || tri.name || 'Mesh item');   // i18n-ignore: item data, the same in every language (the server converter writes it too)
  it.body = { type: 'mesh', positions: out, indices: Array.from(tri.indices) };
  const hull = convexHull(Array.from({ length: n }, (_, i) => [out[i * 3], out[i * 3 + 1]]));
  it.plan.layers = [{ name: 'EQUIP', color: 7 }];
  if (hull.length >= 3) it.plan.entities.push({ type: 'poly', layer: 'EQUIP', color: null, pts: hull, closed: true });
  it.params = { width: r4(maxX - minX), depth: r4(maxY - minY), height: r4(maxZ - minZ) };
  const s = suggestCategory(it.name);
  if (s) { it.category = s; notes.push(msg('libitem.category.suggested_name', { category: s })); }
  else notes.push(msg('libitem.category.set', { category: 'other/other' }));
  return { items: [it], notes };
}

STL corners

64-bit

The corner points of an STL file's triangles.

Binary STL corners are read with getFloat32: the file format stores 32-bit numbers, so each coordinate arrives with about 7 significant digits. ASCII STL numbers are read as 64-bit. Corners are merged only when all three numbers are identical.

  • libitem.js
  • lines 1333 to 1371
  • CadCraft 0.8.2
/**
 * STL, binary or ASCII. Binary when the 84-byte header's triangle count matches the file size (some binary
 * files start with "solid" too). Corners shared by several triangles are merged when they are the same
 * point, so the item stores each one once.
 */
function readStl(u8, notes) {
  const dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
  const positions = [], indices = [], seen = new Map();
  let name = '', degenerate = 0;
  const corner = (x, y, z) => {
    const k = x + ',' + y + ',' + z;
    let i = seen.get(k);
    if (i === undefined) { i = positions.length / 3; seen.set(k, i); positions.push(x, y, z); }
    return i;
  };
  const tri = (a, b, c) => { if (a === b || b === c || a === c) degenerate++; else indices.push(a, b, c); };
  if (u8.length >= 84 && 84 + dv.getUint32(80, true) * 50 === u8.length) {
    const n = dv.getUint32(80, true);
    for (let t = 0; t < n; t++) {
      const o = 84 + t * 50 + 12, v = [];
      for (let k = 0; k < 3; k++) v.push(corner(dv.getFloat32(o + k * 12, true), dv.getFloat32(o + k * 12 + 4, true), dv.getFloat32(o + k * 12 + 8, true)));
      tri(v[0], v[1], v[2]);
    }
  } else {
    const text = new TextDecoder().decode(u8);
    if (!/^\s*solid\b/.test(text)) { notes.push(msg('libitem.mesh.stl_unreadable')); return null; }
    name = clip((/^\s*solid[ \t]*([^\r\n]*)/.exec(text) || [])[1] || '', 120).trim();
    let v = [];
    for (const m of text.matchAll(/vertex\s+(\S+)\s+(\S+)\s+(\S+)|endloop/g)) {
      if (m[0] === 'endloop') { for (let i = 1; i + 1 < v.length; i++) tri(v[0], v[i], v[i + 1]); v = []; continue; }
      const x = +m[1], y = +m[2], z = +m[3];
      if (![x, y, z].every(Number.isFinite)) { notes.push(msg('libitem.mesh.stl_corner')); return null; }
      v.push(corner(x, y, z));
    }
  }
  if (degenerate) notes.push(msg('libitem.mesh.stl_degenerate', { n: degenerate }));
  if (!indices.length || positions.some((x) => !Number.isFinite(x))) { notes.push(msg('libitem.mesh.stl_none')); return null; }
  return { positions, indices, name };
}

glTF accessor values

64-bit

The numbers of a glTF position or index accessor.

Component type 5126 is read with getFloat32, so glTF positions come from 32-bit numbers in the file; the integer types are read exactly. The values are kept in a plain array of 64-bit numbers.

  • libitem.js
  • lines 1403 to 1414
  • CadCraft 0.8.2
  const size = { 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 }[a.componentType];
  const n = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 }[a.type];
  if (!size || n !== comps) return null;
  const stride = bv.byteStride || size * n, base = (bv.byteOffset || 0) + (a.byteOffset || 0);
  const d = new DataView(buf.buffer, buf.byteOffset, buf.byteLength), out = new Array(a.count * n);
  for (let i = 0; i < a.count; i++) for (let c = 0; c < n; c++) {
    const o = base + i * stride + c * size;
    out[i * n + c] = a.componentType === 5126 ? d.getFloat32(o, true) : a.componentType === 5125 ? d.getUint32(o, true)
      : a.componentType === 5123 ? d.getUint16(o, true) : a.componentType === 5121 ? d.getUint8(o) : a.componentType === 5122 ? d.getInt16(o, true) : d.getInt8(o);
  }
  return out;
};

glTF node transforms

64-bit

The matrix that places each glTF node's mesh.

M = T · R(q) · S, R from the quaternion (qx, qy, qz, qw)

A node's matrix is built from its translation, quaternion and scale; the quaternion is used as given and is not normalized. Node matrices are multiplied down the tree as sums of four products in 64-bit numbers.

  • libitem.js
  • lines 1417 to 1425
  • CadCraft 0.8.2
const mat = (node) => {
  if (node.matrix) return node.matrix.slice();
  const [tx, ty, tz] = node.translation || [0, 0, 0], [qx, qy, qz, qw] = node.rotation || [0, 0, 0, 1], [sx, sy, sz] = node.scale || [1, 1, 1];
  return [
    (1 - 2 * (qy * qy + qz * qz)) * sx, 2 * (qx * qy + qz * qw) * sx, 2 * (qx * qz - qy * qw) * sx, 0,
    2 * (qx * qy - qz * qw) * sy, (1 - 2 * (qx * qx + qz * qz)) * sy, 2 * (qy * qz + qx * qw) * sy, 0,
    2 * (qx * qz + qy * qw) * sz, 2 * (qy * qz - qx * qw) * sz, (1 - 2 * (qx * qx + qy * qy)) * sz, 0, tx, ty, tz, 1];
};
const mul = (a, b) => { const o = new Array(16); for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) { let s = 0; for (let k = 0; k < 4; k++) s += a[k * 4 + r] * b[c * 4 + k]; o[c * 4 + r] = s; } return o; };

Convex hull footprint

64-bit

The outline round a mesh item's points, used as its plan.

monotone chain; turn = (a − o) × (b − o), popped when ≤ 0

Points are rounded to 2 decimals and duplicates removed before the hull is built, so the outline lies on a 0.01 grid. Turns are tested with a cross product of the rounded points with no tolerance, and collinear points are dropped.

  • libitem.js
  • lines 1005 to 1014
  • CadCraft 0.8.2
/** Convex hull (monotone chain) of [[x,y]…] → CCW points. */
export function convexHull(points) {
  const p = [...new Map(points.map(([x, y]) => [r2(x) + ',' + r2(y), [r2(x), r2(y)]])).values()].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
  if (p.length < 3) return p;
  const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
  const lo = [], hi = [];
  for (const q of p) { while (lo.length >= 2 && cross(lo[lo.length - 2], lo[lo.length - 1], q) <= 0) lo.pop(); lo.push(q); }
  for (const q of p.slice().reverse()) { while (hi.length >= 2 && cross(hi[hi.length - 2], hi[hi.length - 1], q) <= 0) hi.pop(); hi.push(q); }
  return lo.slice(0, -1).concat(hi.slice(0, -1));
}

Box of a body

64-bit

The box a body fills: a box body itself, or the extent of a mesh body.

For a mesh, the smallest and largest of each coordinate; the corner and the three sizes (differences) are rounded to 4 decimals.

  • libitem.js
  • lines 995 to 1003
  • CadCraft 0.8.2
/** The box a body fills: a box body itself, a mesh body's extent; null for no body. */
export function bodyBox(body) {
  if (!body) return null;
  if (body.type === 'box') return { x: body.x, y: body.y, z: body.z, w: body.w, d: body.d, h: body.h };
  if (body.type !== 'mesh' || !Array.isArray(body.positions) || body.positions.length < 3) return null;
  const P = body.positions, lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
  for (let i = 0; i + 2 < P.length; i += 3) for (let k = 0; k < 3; k++) { if (P[i + k] < lo[k]) lo[k] = P[i + k]; if (P[i + k] > hi[k]) hi[k] = P[i + k]; }
  return { x: r4(lo[0]), y: r4(lo[1]), z: r4(lo[2]), w: r4(hi[0] - lo[0]), d: r4(hi[1] - lo[1]), h: r4(hi[2] - lo[2]) };
}

Box item from width, depth and height

64-bit

The rectangle plan and box body of an item given only by its sizes.

x from −w ÷ 2 to +w ÷ 2; y from −d to 0; label height = min(0.18 d, 150), reduced to fit 90% of w

The rectangle's corners, the box and the stored width, depth and height are each rounded to 4 decimals. The label's size uses 0.6 of its height per character as an estimate of its width.

  • libitem.js
  • lines 1025 to 1041
  • CadCraft 0.8.2
/** A rectangle plan (+ the name as text) and box body centred in X with its back on y = 0. */
function boxItemGeometry(item, w, d, h, z, label) {
  const layer = 'EQUIP';
  item.plan.layers = [{ name: layer, color: 7 }];
  const x0 = -w / 2, y0 = -d;
  item.plan.entities = [{ type: 'poly', layer, color: null, pts: [[r4(x0), r4(y0)], [r4(x0 + w), r4(y0)], [r4(x0 + w), 0], [r4(x0), 0]], closed: true }];
  const text = clip(label, 120);
  if (text) {
    let th = Math.min(d * 0.18, 150);
    const wide = text.length * th * 0.6;
    if (wide > w * 0.9) th = (w * 0.9) / (text.length * 0.6);
    if (th > 1) item.plan.entities.push({ type: 'text', layer, color: null, x: r4(-text.length * th * 0.3), y: r4(-d / 2 - th / 2), h: r4(th), rot: 0, value: text });
  }
  item.body = h > 0 ? { type: 'box', x: r4(x0), y: r4(y0), z: r4(z || 0), w: r4(w), d: r4(d), h: r4(h) } : null;
  item.params.width = r4(w); item.params.depth = r4(d);
  if (h > 0) item.params.height = r4(h);
}

Sizes from attributes

64-bit

Width, depth and height read from a block's attributes.

A size is kept only when it parses to a finite number greater than 0, and is rounded to 4 decimals of a millimeter.

  • libitem.js
  • line 1125
  • CadCraft 0.8.2
const len = (v) => { const n = parseLength(v, mmPerUnit); return Number.isFinite(n) && n > 0 ? r4(n) : null; };

In detail

Method
Block entities go through 2 × 3 matrices in millimeters, with nested references multiplied in. Mesh positions are scaled to millimeters, turned Z-up and moved so the box is centered in X, with its back on y = 0 and its bottom on z = 0.
Number types
64-bit numbers. Binary STL corners and glTF float accessors are read from 32-bit floats in the file.
Tolerances
r4 rounds to 4 decimals, r2 to 2 decimals. A transform keeps arcs when its columns agree within 1e-9, relative. A bulge of 1e-9 or less is dropped. An ellipse is full when its parameter range is within 1e-6 of 2π. 3D faces make a mesh body only when their z values differ by more than 1e-6.
Limits
Arc bounds come from points at most 10° apart, so a width or depth taken from a plan with arcs can be short by up to r × (1 − cos 5°) ≈ 0.4% of the arc's radius. A spline with fit points becomes straight lines between them.

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