Resources Numerical accuracy Calculation policy

Plant sizes, spacing and symbols

The height, spread, spacing and supplied sizes of each plant species, and the 2D symbol and library item made from them.

How the precision is kept

Every length that enters the species table, from a file, a CSV or the table editor, is range-checked and rounded to 0.001 m. Later values are 64-bit expressions on those rounded numbers and are rounded again to three decimals where they are stored: spacing, symbol coordinates, CSV cells. A library item's width, depth and height are rounded to a whole millimeter.

Calculations under this policy

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

plants/plants.js BimCraft 0.8.1 64-bit 10 calculations Reviewed

Tests bin/test-landscape.mjs, bin/test-modelcraft-plants.mjs

Rounding to three decimals

64-bit

The rounding applied to every stored length, spacing and symbol coordinate.

r3(v) = round(v × 1000) ÷ 1000

Multiplies by 1000, rounds to the nearest whole number and divides by 1000 in 64-bit. The kept value is the nearest 64-bit number to a three-decimal value.

  • plants/plants.js
  • line 64
  • BimCraft 0.8.1
const r3 = (v) => Math.round(v * 1000) / 1000;

Reading a length

64-bit

A height, spread, spacing, volume or girth read from a file, a CSV cell or the editor.

Text is parsed with Number after the first decimal comma is replaced by a point. A value that is not finite, not above the lower limit or above the field's cap becomes null; an accepted value is rounded to three decimals before it is stored.

  • plants/plants.js
  • lines 66 to 67
  • BimCraft 0.8.1
const num = (v) => (typeof v === 'string' && v.trim() !== '' ? Number(v.trim().replace(',', '.')) : v);
const len = (v, max, min = 0) => { const x = num(v); return fin(x) && x > min && x <= max ? r3(x) : null; };

Sizes, hardiness zone and spacing of one species

64-bit

The mature size, planting size, zone range and spacing kept for a species row.

The planting size is capped at the mature size. Zones are kept only as whole numbers from 1 to 13. A spacing of exactly 0 is kept; otherwise it must be above 0.0499 m and is rounded to three decimals.

  • plants/plants.js
  • lines 123 to 129
  • BimCraft 0.8.1
const mature = cleanSize(raw.size?.mature);
if (!mature) return { sp: null, why: 'size' };
let plant = cleanSize(raw.size?.plant);
if (plant) plant = { h: Math.min(plant.h, mature.h), s: Math.min(plant.s, mature.s) };
const zmin = Number.isInteger(num(raw.zone?.min)) ? num(raw.zone.min) : null, zmax = Number.isInteger(num(raw.zone?.max)) ? num(raw.zone.max) : null;
const zone = zmin != null && zmax != null && zmin >= 1 && zmax <= 13 && zmin <= zmax ? { min: zmin, max: zmax } : null;
const spacing = raw.spacing === 0 || raw.spacing === '0' ? 0 : len(raw.spacing, LIMITS.spacing, 0.0499);

Default planting size and spacing

64-bit

The planting size, container and spacing filled in when a species row leaves them empty.

spacing = max(q, round(k × spread ÷ q) × q), by plant type

Fixed factors and steps per plant type (for example 0.75 of the spread in steps of 0.05 m for a shrub). Rounding to a step by multiplying back can leave binary noise in the last digits, which the final rounding to three decimals removes. The planting size is capped at the mature size.

  • plants/plants.js
  • lines 76 to 98
  • BimCraft 0.8.1
/** The defaults the seed list fills in (said in its source note), and the CSV import uses for empty planting sizes. */
export function defaultsFor(type, form, mature) {
  const M = mature && fin(mature.h) && fin(mature.s) ? mature : { h: 1, s: 1 };
  const cap = (h, s) => ({ h: r3(Math.min(h, M.h)), s: r3(Math.min(s, M.s)) });
  const step = (v, q) => Math.max(q, Math.round(v / q) * q);
  let plant, container, spacing;
  switch (type) {
    case 'tree':
      if (form === 'palm') { plant = cap(2, 1.5); container = '100 L'; }
      else if (form === 'conifer' || form === 'columnar') { plant = cap(1.75, 0.6); container = '45 L'; }
      else { plant = cap(2.75, 1); container = '45 L'; }
      spacing = step(M.s, 0.5);
      break;
    case 'hedge': plant = cap(0.6, 0.3); container = '5 L'; spacing = M.s >= 1.5 ? 0.6 : M.s >= 0.8 ? 0.4 : 0.3; break;
    case 'shrub': plant = cap(0.5, 0.4); container = '10 L'; spacing = step(M.s * 0.75, 0.05); break;
    case 'grass': if (form === 'turf') { plant = cap(0.05, 0.05); container = 'seed or turf'; spacing = 0; } else { plant = cap(0.3, 0.3); container = '3 L'; spacing = step(M.s * 0.7, 0.05); } break;
    case 'perennial': plant = cap(0.25, 0.25); container = '2 L'; spacing = step(M.s * 0.6, 0.05); break;
    case 'groundcover': plant = cap(0.1, 0.15); container = '9 cm pot'; spacing = step(M.s * 0.5, 0.05); break;
    case 'climber': plant = cap(1, 0.3); container = '5 L'; spacing = step(Math.min(M.s, 3) * 0.6, 0.1); break;
    default: plant = cap(0.3, 0.3); container = ''; spacing = step(M.s * 0.7, 0.05);
  }
  return { plant, container, spacing: r3(spacing) };
}

Spacing from planting density

64-bit

The center-to-center spacing that gives a stated number of plants per square meter.

square: s = 1 ÷ √d; triangular: s = √(2 ÷ (√3 × d))

One 64-bit expression with Math.sqrt, then rounded to 0.001 m. A density that is not a finite number above zero gives null.

  • plants/plants.js
  • lines 212 to 213
  • BimCraft 0.8.1
/** Plants per m² → centre-to-centre spacing (m): triangular d = 2 / (√3 s²), square d = 1 / s². */
export function densityToSpacing(d, pattern = 'triangular') { return fin(d) && d > 0 ? r3(pattern === 'square' ? 1 / Math.sqrt(d) : Math.sqrt(2 / (Math.sqrt(3) * d))) : null; }

Spacing used for a species

64-bit

The spacing a bed or hedge is planted at: the species' own, else from its density, else the default.

Picks one of three already rounded values; no arithmetic of its own.

  • plants/plants.js
  • line 377
  • BimCraft 0.8.1
export function spacingOf(sp, pattern = 'triangular') { return sp.spacing != null ? sp.spacing : sp.density ? densityToSpacing(sp.density, pattern) : defaultsFor(sp.type, sp.form, sp.size.mature).spacing; }

Supplied sizes written to CSV

64-bit

Container volume, height, spread, girth and clear stem of each supplied size as one CSV cell.

Each number is rounded to three decimals and written with String, which prints the shortest text that reads back to the same 64-bit number.

  • plants/plants.js
  • lines 271 to 275
  • BimCraft 0.8.1
/** Supplied sizes as one cell: "c10 container 10L h0.6 s0.4; rb rootball h3 g14-16 st1.8" (id, kind, then tagged numbers). */
export function suppliesToText(list) {
  return (list || []).map((x) => [x.id, x.kind, x.volume != null ? g3(x.volume) + 'L' : '', x.h != null ? 'h' + g3(x.h) : '', x.s != null ? 's' + g3(x.s) : '',
    x.girth ? 'g' + g3(x.girth.min) + (x.girth.max > x.girth.min ? '-' + g3(x.girth.max) : '') : '', x.stem != null ? 'st' + g3(x.stem) : ''].filter(Boolean).join(' ')).join('; ');
}

Supplied sizes read from CSV

64-bit

The same cell read back into numbers.

Numbers are matched as digits with an optional point or comma and at most one fraction, so exponent notation and negative values are not read. They then pass the same range checks and three-decimal rounding as any other length.

  • plants/plants.js
  • lines 276 to 293
  • BimCraft 0.8.1
export function suppliesFromText(text) {
  const out = [];
  for (const part of String(text || '').split(';').slice(0, LIMITS.supplies)) {
    const w = part.trim().split(/\s+/).filter(Boolean);
    if (w.length < 2) continue;
    const o = { id: w[0], kind: w[1].toLowerCase() };
    for (const tok of w.slice(2)) {
      let m;
      if ((m = /^(\d+(?:[.,]\d+)?)l$/i.exec(tok))) o.volume = m[1];
      else if ((m = /^st(\d+(?:[.,]\d+)?)$/i.exec(tok))) o.stem = m[1];
      else if ((m = /^h(\d+(?:[.,]\d+)?)$/i.exec(tok))) o.h = m[1];
      else if ((m = /^s(\d+(?:[.,]\d+)?)$/i.exec(tok))) o.s = m[1];
      else if ((m = /^g(\d+(?:[.,]\d+)?)(?:-(\d+(?:[.,]\d+)?))?$/i.exec(tok))) o.girth = { min: m[1], max: m[2] ?? m[1] };
    }
    out.push(o);
  }
  return cleanSupplies(out.map((o) => ({ ...o, volume: num(o.volume), h: num(o.h), s: num(o.s), stem: num(o.stem), girth: o.girth ? { min: num(o.girth.min), max: num(o.girth.max) } : null })));
}

2D planting symbol

64-bit

The circles, lines, arcs and outlines of a plant's plan symbol, sized to its spread.

R = spread ÷ 2 × scale; point = (r cos a, r sin a)

Every coordinate and radius is rounded to three decimals of the drawing unit (0.001 mm at the default scale of 1000 per meter); arc angles are converted to degrees with 180 ÷ π and rounded to 0.001°. Points meant to lie on a circle are therefore rounded off it by less than a thousandth of a unit.

  • plants/plants.js
  • lines 431 to 463
  • BimCraft 0.8.1
/** A 2D planting symbol centred on (0,0), radius = half the spread, in `scale` units per metre (1000 → mm). Entities in the
 *  CadCraft Doc subset the library item format uses: circle {x,y,r}, arc {x,y,r,a0,a1}, line, poly {pts, closed}. */
export function symbolPlan(sp, { which = 'mature', scale = 1000, sym = null } = {}) {
  const R = (sizeOf(sp, which).s / 2) * scale, kind = sym || symbolOf(sp);
  const L = 'PLANT', D = 'PLANT-DETAIL';
  const E = [], ln = (x1, y1, x2, y2, layer = D) => E.push({ type: 'line', layer, color: null, x1: r3(x1), y1: r3(y1), x2: r3(x2), y2: r3(y2) });
  const circle = (r, layer = L) => E.push({ type: 'circle', layer, color: null, x: 0, y: 0, r: r3(r) });
  const P = (a, r) => [r3(r * Math.cos(a)), r3(r * Math.sin(a))];
  switch (kind) {
    case 'canopy': circle(R); circle(R * 0.06, D); for (let i = 0; i < 6; i++) { const a = (i * Math.PI) / 3; ln(R * 0.12 * Math.cos(a), R * 0.12 * Math.sin(a), R * 0.55 * Math.cos(a), R * 0.55 * Math.sin(a)); } break;
    case 'canopy-scallop': {   // a scalloped outline: arcs bulging outwards between points on the circle
      const n = Math.max(8, Math.min(18, Math.round(R / (0.35 * scale)) + 8)), pts = [];
      for (let i = 0; i < n; i++) { const a = (2 * Math.PI * i) / n; pts.push([...P(a, R * 0.92), 0.35]); }
      E.push({ type: 'poly', layer: L, color: null, pts, closed: true });
      circle(R * 0.06, D);
      break;
    }
    case 'conifer': circle(R); for (let i = 0; i < 12; i++) { const a = (i * Math.PI) / 6; const [x, y] = P(a, R * (i % 2 ? 0.7 : 0.95)); ln(0, 0, x, y); } break;
    case 'palm': for (let i = 0; i < 9; i++) { const a = (2 * Math.PI * i) / 9; const [x, y] = P(a, R); const [mx, my] = P(a + 0.12, R * 0.6); E.push({ type: 'poly', layer: L, color: null, pts: [[0, 0], [mx, my], [x, y]], closed: false }); } circle(R * 0.08, D); break;
    case 'shrub': {
      const n = 7, pts = [];
      for (let i = 0; i < n; i++) { const a = (2 * Math.PI * i) / n + 0.2; pts.push([...P(a, R * (i % 2 ? 0.86 : 0.97)), 0.5]); }
      E.push({ type: 'poly', layer: L, color: null, pts, closed: true });
      break;
    }
    case 'grass': circle(R, D); for (let i = 0; i < 10; i++) { const a = -Math.PI / 2 + ((i - 4.5) / 4.5) * 1.2; const [x, y] = P(a + Math.PI, R * 0.9); ln(0, -R * 0.2, x, -y, L); } break;
    case 'groundcover': circle(R, D); for (let i = 0; i < 5; i++) { const [x, y] = P((2 * Math.PI * i) / 5, R * 0.45); E.push({ type: 'circle', layer: L, color: null, x, y, r: r3(R * 0.12) }); } break;
    case 'climber': circle(R * 0.15); for (let i = 0; i < 3; i++) { const a = (2 * Math.PI * i) / 3; const [x, y] = P(a, R); E.push({ type: 'arc', layer: L, color: null, x: r3(x / 2), y: r3(y / 2), r: r3(R / 2), a0: r3((a * 180) / Math.PI), a1: r3((a * 180) / Math.PI + 180) }); } break;
    case 'hedge': { const w = R * 0.85, h = R * 0.5; /* a piece of hedge, inside the spread circle */ E.push({ type: 'poly', layer: L, color: null, pts: [[-w, -h, 0.25], [w, -h, 0.25], [w, h, 0.25], [-w, h, 0.25]], closed: true }); break; }
    default: circle(R);
  }
  return { layers: [{ name: L, color: 3 }, { name: D, color: 8 }], entities: E };
}

Library item size

64-bit

The width, depth and height of the library item placed for a plant.

mm = round(m × 1000)

Height and spread in meters are multiplied by 1000 and rounded to a whole millimeter. The size written into the item's attributes is the meter value as text.

  • plants/plants.js
  • lines 465 to 478
  • BimCraft 0.8.1
/** A library item (format 1, partial: no uuid/version/license/provenance) — what BimCraft and ModelCraft place. `body` = an
 *  optional mesh in mm (the app passes the entourage model scaled to this plant), else null (2D only). */
export function libraryItem(sp, { which = 'mature', body = null, lang = 'en' } = {}) {
  const sz = sizeOf(sp, which), mm = (v) => Math.round(v * 1000);
  return {
    format: 'cadcraft-library-item', format_version: 1,
    name: `${displayName(sp, lang)} (${sp.botanical})`.slice(0, 200), kind: 'symbol', category: 'site/landscape',
    description: clean(`${sp.type}${sp.foliage ? ', ' + sp.foliage : ''}; ${which === 'plant' ? 'at planting' : 'at maturity'} ${sz.h} m high, ${sz.s} m across`, 500),
    maker: '', model: '', ifc: { class: 'IfcGeographicElement', predefined: 'USERDEFINED' },
    plan: symbolPlan(sp, { which, scale: 1000 }), body,
    params: { width: mm(sz.s), depth: mm(sz.s), height: mm(sz.h), remarks: clean(sp.notes, 500) },
    source_attrs: { 'CC-PLANT-KEY': sp.key, 'CC-PLANT-BOTANICAL': sp.botanical, 'CC-PLANT-TYPE': sp.type, 'CC-PLANT-SIZE': which, 'CC-PLANT-HEIGHT-M': String(sz.h), 'CC-PLANT-SPREAD-M': String(sz.s) },
  };
}

In detail

Method
Field-by-field checks of untrusted rows. Spacing from density uses the bed pattern's geometry: square 1 ÷ √d, triangular √(2 ÷ (√3 × d)). Symbols are drawn from the spread with fixed proportions.
Number types
64-bit numbers; lengths in meters in the table, millimeters in a library item. Text numbers are parsed with Number after a decimal comma is replaced by a point.
Tolerances
No comparison tolerances. Range limits: height 150 m, spread 60 m, spacing 50 m (above 0.0499 m, or exactly 0), density 0.01 to 400 per m², girth 500 cm, volume 10000 L.
Limits
Values finer than 0.001 m are lost on reading. Default planting sizes, containers and spacings are rules of thumb by plant type, not data from a source.

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