Resources Numerical accuracy Calculation policy

Library item placed as a block

Turns a Library item's plan into a block in the drawing's units and counts the Library items a drawing uses.

How the precision is kept

An item's plan is in millimeters and degrees. Placing it multiplies every coordinate by 1 ÷ (millimeters per drawing unit) and converts angles to radians, in 64-bit numbers. The size attributes written on the block are text with 10 significant digits.

Open item. Write the size attributes with every digit of the item's value instead of 10.

Calculations under this policy

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

libplace.js CadCraft 0.8.2 64-bit 4 calculations Reviewed

Tests bin/test-cadcraft.mjs

Millimeters to drawing units

64-bit

The factor that puts an item's millimeters into the drawing's units.

k = 1 ÷ (mm per drawing unit), or 1 when the drawing has no units

The reciprocal is computed once and every coordinate is multiplied by it. In an inch drawing that is a multiplication by the rounded value of 1 ÷ 25.4 instead of a division by 25.4, which can differ from the division in the last digit.

  • libplace.js
  • lines 148 to 149
  • CadCraft 0.8.2
const mm = mmPerUnit(doc.meta?.units);
const k = mm ? 1 / mm : 1;

Plan entities in drawing units

64-bit

Each line, circle, arc, polyline, ellipse, text, point and hatch of the item as a drawing object.

length × k; angle in degrees × π ÷ 180, reduced to [0, 2π)

Coordinates, radii and text heights are multiplied by k. Arc and text angles are converted to radians and reduced to [0, 2π); ellipse parameters are converted without reduction. Bulges and the ellipse ratio are copied unchanged.

  • libplace.js
  • lines 55 to 101
  • CadCraft 0.8.2
/** One plan entity (mm, degrees) → a CadCraft entity in drawing units (×k), or null. */
function convert(e, k) {
  if (!e || typeof e !== 'object') return null;
  const base = { layer: str(e.layer, 255) || '0', color: colorOf(e.color) };
  const P = (v) => v * k;
  switch (e.type) {
    case 'line': return [e.x1, e.y1, e.x2, e.y2].every(fin) ? { type: 'line', ...base, x1: P(e.x1), y1: P(e.y1), x2: P(e.x2), y2: P(e.y2) } : null;
    case 'circle': { const [x, y] = xyOf(e); return [x, y, e.r].every(fin) && e.r > 0 ? { type: 'circle', ...base, cx: P(x), cy: P(y), r: P(e.r) } : null; }
    case 'arc': { const [x, y] = xyOf(e); return [x, y, e.r, e.a0, e.a1].every(fin) && e.r > 0 ? { type: 'arc', ...base, cx: P(x), cy: P(y), r: P(e.r), a0: normAngle(rad(e.a0)), a1: normAngle(rad(e.a1)) } : null; }
    case 'poly': {
      if (!Array.isArray(e.pts) || e.pts.length < 2 || e.pts.length > 100000) return null;
      const pts = [];
      for (const p of e.pts) {
        const [x, y, b] = Array.isArray(p) ? p : [p?.x, p?.y, p?.b];
        if (!fin(x) || !fin(y)) return null;
        pts.push({ x: P(x), y: P(y), b: fin(b) && Math.abs(b) < 1e3 ? b : 0 });
      }
      return { type: 'poly', ...base, pts, closed: !!e.closed };
    }
    case 'ellipse': {
      const [x, y] = xyOf(e);
      if (![x, y, e.mx, e.my, e.ratio].every(fin) || !(e.ratio > 0) || !(Math.hypot(e.mx, e.my) > 0)) return null;
      const t0 = fin(e.t0) ? rad(e.t0) : 0, t1 = fin(e.t1) ? rad(e.t1) : TAU;   // parameters in degrees
      return { type: 'ellipse', ...base, cx: P(x), cy: P(y), mx: P(e.mx), my: P(e.my), ratio: e.ratio, t0, t1 };
    }
    case 'text': {
      const s = str(e.value ?? e.s, 500);
      if (!s || ![e.x, e.y].every(fin)) return null;
      const h = fin(e.h) && e.h > 0 ? e.h : 100;
      const ha = ['left', 'center', 'right'].includes(e.ha) ? e.ha : 'left', va = ['baseline', 'bottom', 'middle', 'top'].includes(e.va) ? e.va : 'baseline';
      return { type: 'text', ...base, x: P(e.x), y: P(e.y), h: P(h), rot: normAngle(rad(fin(e.rot) ? e.rot : 0)), s, ha, va };
    }
    case 'point': return [e.x, e.y].every(fin) ? { type: 'point', ...base, x: P(e.x), y: P(e.y) } : null;
    case 'hatch': {
      if (!Array.isArray(e.loops) || !e.loops.length) return null;
      const loops = [];
      for (const l of e.loops) {
        if (!Array.isArray(l)) return null;
        const flat = Array.isArray(l[0]) ? l.flat() : l;
        if (flat.length < 6 || flat.length % 2 || !flat.every(fin)) return null;
        loops.push(flat.map(P));
      }
      return { type: 'hatch', ...base, solid: true, loops };
    }
    default: return null;
  }
}

Size attributes

64-bit

The width, depth, height and weight written as attributes on the block.

Each number goes through toPrecision(10) before it becomes the attribute's text, so the attribute holds 10 significant digits of the item's value, not all of them.

  • libplace.js
  • lines 110 to 124
  • CadCraft 0.8.2
/** Canonical params → [tag, value] pairs with a value (numbers in mm / kg). */
function paramAttrs(params) {
  const out = [];
  if (!params || typeof params !== 'object') return out;
  for (const [k, tag] of [['width', 'CC-WIDTH-MM'], ['depth', 'CC-DEPTH-MM'], ['height', 'CC-HEIGHT-MM'], ['weight_kg', 'CC-WEIGHT-KG']]) {
    if (fin(params[k]) && params[k] !== 0) out.push([tag, String(Number(params[k].toPrecision(10)))]);
  }
  for (const [group, prefix] of [['elec', 'CC-ELEC-'], ['plumb', 'CC-PLUMB-']]) {
    const g = params[group];
    if (g && typeof g === 'object') for (const [k, v] of Object.entries(g)) { const s = str(v, 120); if (s && /^[a-z0-9_]{1,20}$/i.test(k)) out.push([prefix + k.toUpperCase(), s]); }
  }
  const r = str(params.remarks, 500);
  if (r) out.push(['CC-REMARKS', r]);
  return out;
}

Count and quantity of each item

64-bit

How many times each Library item is placed and the total of their quantities, for the credits and schedule.

placed = number of references; quantity = Σ parseFloat(quantity attribute), 1 when not above 0

The count is a whole number. The quantity is a 64-bit running sum in the order the references are found; parseFloat reads the leading number of the attribute's text.

  • libplace.js
  • lines 211 to 234
  • CadCraft 0.8.2
/** Every library item the drawing uses (references in the model and on sheets), grouped by uuid@version. */
export function libraryUses(doc) {
  const map = new Map();
  for (const e of doc.everyEntity()) {
    if (e.type !== 'insert') continue;
    const a = e.attrs || {};
    const uuid = a[LIB_TAGS.uuid];
    if (!uuid) continue;
    const version = parseInt(a[LIB_TAGS.ver], 10) || 1;
    const key = uuid + '@' + version;
    let u = map.get(key);
    if (!u) {
      u = { uuid, version, block: e.block, name: a[LIB_TAGS.name] || '', library: a[LIB_TAGS.library] || '', sha256: a[LIB_TAGS.sha] || '', license: a[LIB_TAGS.license] || '', attribution: a[LIB_TAGS.attrib] || '', licenseUrl: a[LIB_TAGS.licenseUrl] || '', maker: a[LIB_TAGS.maker] || '', model: a[LIB_TAGS.model] || '', category: a[LIB_TAGS.category] || '', count: 0, quantity: 0, numbers: [] };
      map.set(key, u);
    }
    u.count++;
    u.quantity += parseFloat(a[LIB_TAGS.qty]) > 0 ? parseFloat(a[LIB_TAGS.qty]) : 1;
    const n = String(a[LIB_TAGS.item] ?? '').trim();
    if (n && !u.numbers.includes(n)) u.numbers.push(n);
  }
  const out = [...map.values()];
  for (const u of out) u.numbers.sort((x, y) => x.localeCompare(y, undefined, { numeric: true }));
  return out.sort((x, y) => (x.numbers[0] || '~').localeCompare(y.numbers[0] || '~', undefined, { numeric: true }) || x.name.localeCompare(y.name));
}

In detail

Method
One scale factor for the whole plan; angles × π ÷ 180.
Number types
64-bit numbers; attribute values are text.
Tolerances
A number that is not finite or is larger than 1e7 in size makes its entity be skipped. A bulge of 1000 or more in size is set to 0.
Limits
Width, depth, height and weight attributes keep 10 significant digits.

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