Resources Numerical accuracy Calculation policy

Plant symbols from SVG files and CAD blocks

Turns an SVG drawing or a CAD block into a plant symbol kept in the project, and scales it to each plant's spread on the plan.

How the precision is kept

Arithmetic is 64-bit, but a stored symbol is deliberately reduced: it is scaled into a unit circle and every coordinate, radius and angle is rounded to 4 decimals. Curves in an SVG path are replaced by straight pieces. The stored symbol is a plan graphic; no quantity is taken from it.

Calculations under this policy

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

landscape/symbols.js CivilCraft 0.8.1 64-bit 3 calculations Reviewed

Tests bin/test-landscape.mjs

Scale a symbol into the unit circle

64-bit

Centers the symbol's entities and scales them so the farthest point is at radius 1.

x′ = round((x − cx) ÷ R × 10⁴) ÷ 10⁴

Every stored coordinate and radius is rounded to 4 decimals of the unit radius, and arc angles to 4 decimals of a degree. The rounding is applied to the value that is saved in the project, not only to a display.

  • landscape/symbols.js
  • lines 37 to 55
  • CivilCraft 0.8.1
/** Entities (circle / arc / line / poly with [x, y] points) → centred on their box, scaled so the farthest point is at radius 1. */
function normalize(ents) {
  const ext = [];   // [x, y, r]: a point, or a circle's centre and radius
  for (const e of ents) {
    if (e.type === 'circle' || e.type === 'arc') ext.push([e.x, e.y, e.r]);
    else if (e.type === 'line') ext.push([e.x1, e.y1, 0], [e.x2, e.y2, 0]);
    else for (const p of e.pts) ext.push([p[0], p[1], 0]);
  }
  if (!ext.length) throw new Error('empty');
  let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
  for (const [x, y, r] of ext) { x0 = Math.min(x0, x - r); x1 = Math.max(x1, x + r); y0 = Math.min(y0, y - r); y1 = Math.max(y1, y + r); }
  const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2;
  let R = 0;
  for (const [x, y, r] of ext) R = Math.max(R, Math.hypot(x - cx, y - cy) + r);
  if (!(R > 0)) throw new Error('empty');
  const X = (x) => r4((x - cx) / R), Y = (y) => r4((y - cy) / R);
  return ents.map((e) => (e.type === 'circle' ? { type: 'circle', x: X(e.x), y: Y(e.y), r: r4(e.r / R) } : e.type === 'arc' ? { type: 'arc', x: X(e.x), y: Y(e.y), r: r4(e.r / R), a0: r4(e.a0), a1: r4(e.a1) }
    : e.type === 'line' ? { type: 'line', x1: X(e.x1), y1: Y(e.y1), x2: X(e.x2), y2: Y(e.y2) } : { type: 'poly', pts: e.pts.map((p) => [X(p[0]), Y(p[1])]), closed: !!e.closed }));
}

SVG path to polylines

64-bit

Reads an SVG path's commands into straight-line polylines.

Bézier point at t = k ÷ 8, k = 1 … 8

Each cubic or quadratic curve becomes 8 chords whatever its size, and the S command is treated as a quadratic with the points given. An elliptical arc command is replaced by one straight segment to its end point. Relative commands add to the running position.

  • landscape/symbols.js
  • lines 59 to 81
  • CivilCraft 0.8.1
function pathEntities(d, push, limitPts) {
  const tok = String(d || '').match(/[MmLlHhVvZzCcSsQqTtAa]|-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/g) || [];
  let i = 0, cmd = '', x = 0, y = 0, sx = 0, sy = 0, cur = null, total = 0;
  const flush = (closed = false) => { if (cur && cur.length >= 2) push({ type: 'poly', pts: cur, closed }); cur = null; };
  const add = (px, py) => { if (!cur) cur = [[x, y]]; cur.push([px, py]); x = px; y = py; if (++total > limitPts) throw new Error('too-big'); };
  const n = () => { const v = Number(tok[i++]); if (!Number.isFinite(v)) throw new Error('bad-svg'); return v; };
  const bez = (pts) => { for (let k = 1; k <= 8; k++) { const t = k / 8, u = 1 - t; let px, py; if (pts.length === 4) { px = u * u * u * pts[0][0] + 3 * u * u * t * pts[1][0] + 3 * u * t * t * pts[2][0] + t * t * t * pts[3][0]; py = u * u * u * pts[0][1] + 3 * u * u * t * pts[1][1] + 3 * u * t * t * pts[2][1] + t * t * t * pts[3][1]; } else { px = u * u * pts[0][0] + 2 * u * t * pts[1][0] + t * t * pts[2][0]; py = u * u * pts[0][1] + 2 * u * t * pts[1][1] + t * t * pts[2][1]; } add(px, py); } };
  while (i < tok.length) {
    if (/[A-Za-z]/.test(tok[i])) cmd = tok[i++];
    const rel = cmd === cmd.toLowerCase(), C = cmd.toUpperCase(), ox = rel ? x : 0, oy = rel ? y : 0;
    if (C === 'Z') { if (cur) { cur.push([sx, sy]); x = sx; y = sy; } flush(true); continue; }
    if (C === 'M') { flush(); x = n() + ox; y = n() + oy; sx = x; sy = y; cur = [[x, y]]; cmd = rel ? 'l' : 'L'; continue; }
    if (C === 'L') { add(n() + ox, n() + oy); continue; }
    if (C === 'H') { add(n() + ox, y); continue; }
    if (C === 'V') { add(x, n() + oy); continue; }
    if (C === 'C') { const a = [n() + ox, n() + oy], b = [n() + ox, n() + oy], e = [n() + ox, n() + oy]; bez([[x, y], a, b, e]); continue; }
    if (C === 'S' || C === 'Q') { const a = [n() + ox, n() + oy], e = [n() + ox, n() + oy]; bez([[x, y], a, e]); continue; }
    if (C === 'T') { add(n() + ox, n() + oy); continue; }
    if (C === 'A') { n(); n(); n(); n(); n(); add(n() + ox, n() + oy); continue; }
    throw new Error('bad-svg');
  }
  flush();
}

Scale a symbol to a plant

64-bit

The symbol's entities at the plant's radius on the plan.

x = x′ × R

One multiplication per coordinate by the plant's radius. The inputs already carry the 4-decimal rounding of the stored symbol.

  • landscape/symbols.js
  • lines 121 to 124
  • CivilCraft 0.8.1
export function placeSymbol(sym, R) {
  return (sym?.entities || []).map((e) => (e.type === 'circle' ? { type: 'circle', x: e.x * R, y: e.y * R, r: e.r * R } : e.type === 'arc' ? { type: 'arc', x: e.x * R, y: e.y * R, r: e.r * R, a0: e.a0, a1: e.a1 }
    : e.type === 'line' ? { type: 'line', x1: e.x1 * R, y1: e.y1 * R, x2: e.x2 * R, y2: e.y2 * R } : { type: 'poly', pts: e.pts.map((p) => [p[0] * R, p[1] * R]), closed: e.closed }));
}

In detail

Method
Entities are centered on their bounding box and divided by the distance to the farthest point. SVG cubic and quadratic curves are cut into 8 straight pieces, an ellipse into 24, and an elliptical arc command into one straight segment, as the file header says.
Number types
64-bit numbers; stored values rounded to 1e-4 of the symbol's radius.
Tolerances
Stored coordinates, radii and arc angles rounded with Math.round(v × 1e4) ÷ 1e4.
Limits
SVG transforms are not applied (a note is returned). Curves are polylines. Each coordinate of a symbol placed at radius R can be off by up to R × 5e-5 because of the stored rounding.

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