Resources Numerical accuracy Calculation policy

Trees, people and cars from numbers

Generates the mesh and plan symbol of each starter tree, shrub, rock, person and car from a size and a seed.

How the precision is kept

Shapes are computed in 64-bit numbers in millimeters, then every mesh coordinate and every plan polygon point is rounded to 0.1 mm before it is stored. The seeded generator makes 32-bit random fractions that only vary the shapes. The stored geometry is therefore on a 0.1 mm grid by design.

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.

entourage.js ModelCraft 0.8.1 64-bit 7 calculations Reviewed

Tests bin/test-render.mjs

Rounding to 0.1 mm

64-bit

The rounding applied to every stored mesh coordinate, plan polygon point and item size.

r1(v) = round(v × 10) ÷ 10

The value kept is rounded to one decimal of a millimeter. Math.round rounds halves toward +∞, and the result is the 64-bit number nearest to that tenth.

  • entourage.js
  • line 31
  • ModelCraft 0.8.1
const r1 = (v) => Math.round(v * 10) / 10;

Mesh point

64-bit

Adds one point to a part's mesh.

Each of x, y and z passes through r1 before it is pushed, so every mesh point of every item is stored on the 0.1 mm grid.

  • entourage.js
  • line 36
  • ModelCraft 0.8.1
v(x, y, z) { this.P.push(r1(x), r1(y), r1(z)); return this.P.length / 3 - 1; }

Ring loft

64-bit

Trunks, pots, limbs and conifer tiers: rings of points round an axis joined into a capped solid.

a = 2π·i ÷ seg; p = [cx + rx·cos a, cy + ry·sin a, z]

Each ring point is one 64-bit expression, then rounded to 0.1 mm by v. The solid is a prism with seg flat sides, not a round one.

  • entourage.js
  • lines 38 to 58
  • ModelCraft 0.8.1
/** A ring-lofted solid: rings [{z, r, cx, cy, rx?, ry?}] round the Z axis (or an offset centre), `seg` sides, capped. */
loft(rings, seg = 12, cap = true) {
  const base = this.P.length / 3;
  for (const g of rings) for (let i = 0; i < seg; i++) {
    const a = (2 * Math.PI * i) / seg;
    this.v((g.cx || 0) + (g.rx ?? g.r) * Math.cos(a), (g.cy || 0) + (g.ry ?? g.r) * Math.sin(a), g.z);
  }
  for (let k = 0; k + 1 < rings.length; k++) for (let i = 0; i < seg; i++) {
    const a = base + k * seg + i, b = base + k * seg + ((i + 1) % seg), c = a + seg, d = b + seg;
    this.tri(a, b, d); this.tri(a, d, c);
  }
  if (cap) {
    const bot = this.v(rings[0].cx || 0, rings[0].cy || 0, rings[0].z), top = this.v(rings.at(-1).cx || 0, rings.at(-1).cy || 0, rings.at(-1).z);
    for (let i = 0; i < seg; i++) {
      this.tri(bot, base + ((i + 1) % seg), base + i);
      const t0 = base + (rings.length - 1) * seg;
      this.tri(top, t0 + i, t0 + ((i + 1) % seg));
    }
  }
  return this;
}

Crown and head blobs

64-bit

Tree crowns, shrubs, rocks and heads: an icosphere scaled to an ellipsoid with a seeded bump.

t = (1 + √5) ÷ 2; unit point p; k = 1 + bump × (2·rand − 1); point = c + p × [rx, ry, rz] × k

The 12 starting points are divided by their length, and each edge midpoint added by a subdivision is normalized again, so all points lie on the unit sphere to 64-bit rounding before scaling. The bump factor uses the seeded generator, whose fractions have 32 bits. Results are rounded to 0.1 mm by v.

  • entourage.js
  • lines 59 to 76
  • ModelCraft 0.8.1
/** An icosphere (subdivided `sub` times) scaled to (rx, ry, rz) at c, its surface pushed in and out by `bump` (0…1). */
blob(c, rx, ry, rz, sub, bump, R) {
  const t = (1 + Math.sqrt(5)) / 2;
  let V = [[-1, t, 0], [1, t, 0], [-1, -t, 0], [1, -t, 0], [0, -1, t], [0, 1, t], [0, -1, -t], [0, 1, -t], [t, 0, -1], [t, 0, 1], [-t, 0, -1], [-t, 0, 1]].map((p) => { const l = Math.hypot(...p); return p.map((x) => x / l); });
  let F = [[0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1]];
  for (let s = 0; s < sub; s++) {
    const mid = new Map(), m = (a, b) => {
      const k = a < b ? a + '|' + b : b + '|' + a;
      if (!mid.has(k)) { const p = V[a].map((x, i) => (x + V[b][i]) / 2), l = Math.hypot(...p); V.push(p.map((x) => x / l)); mid.set(k, V.length - 1); }
      return mid.get(k);
    };
    F = F.flatMap(([a, b, c]) => { const ab = m(a, b), bc = m(b, c), ca = m(c, a); return [[a, ab, ca], [b, bc, ab], [c, ca, bc], [ab, bc, ca]]; });
  }
  const base = this.P.length / 3;
  for (const p of V) { const k = 1 + bump * (R() * 2 - 1); this.v(c[0] + p[0] * rx * k, c[1] + p[1] * ry * k, c[2] + p[2] * rz * k); }
  for (const [a, b, d] of F) this.tri(base + a, base + b, base + d);
  return this;
}

Wheel

64-bit

A car wheel: a 14-sided cylinder along y.

a = 2π·i ÷ seg − π ÷ 2; p = [x + r·cos a, y, z + r·sin a]

The first corner is at −90° so a corner is at the lowest point. cos(−π ÷ 2) does not evaluate to exactly zero in 64-bit, but the rounding to 0.1 mm in v removes the difference.

  • entourage.js
  • lines 95 to 107
  • ModelCraft 0.8.1
/** A wheel: a cylinder along Y at (x, z) of radius r from y0 to y1. */
wheel(x, z, r, y0, y1, seg = 14) {
  const b = this.P.length / 3;
  // a corner at the bottom (−90°), so the tyre stands on the ground exactly; wound outward
  for (const y of [y0, y1]) for (let i = 0; i < seg; i++) { const a = (2 * Math.PI * i) / seg - Math.PI / 2; this.v(x + r * Math.cos(a), y, z + r * Math.sin(a)); }
  const c0 = this.v(x, y0, z), c1 = this.v(x, y1, z);
  for (let i = 0; i < seg; i++) {
    const j = (i + 1) % seg;
    this.tri(b + i, b + seg + j, b + j); this.tri(b + i, b + seg + i, b + seg + j);
    this.tri(c0, b + i, b + j); this.tri(c1, b + seg + j, b + seg + i);
  }
  return this;
}

Canopy plan symbol

64-bit

The plan outline of a tree: a 14-point irregular ring, a trunk circle and a cross.

a = 2π·i ÷ 14; k = r × (0.86 + 0.18·rand); p = [k·cos a, k·sin a]

The ring points pass through r1 (0.1 mm) in poly. The trunk circle's radius is stored as computed, without rounding.

  • entourage.js
  • lines 113 to 118
  • ModelCraft 0.8.1
function canopyPlan(r, seed, trunk) {
  const R = rng(seed), n = 14, pts = [];
  for (let i = 0; i < n; i++) { const a = (2 * Math.PI * i) / n, k = r * (0.86 + R() * 0.18); pts.push([k * Math.cos(a), k * Math.sin(a)]); }
  return { layers: [{ name: 'PLANTING', color: 3 }, { name: 'PLANTING-DETAIL', color: 3 }],
    entities: [poly(pts, 'PLANTING'), circ(0, 0, trunk, 'PLANTING-DETAIL'), poly([[-r * 0.25, 0], [r * 0.25, 0]], 'PLANTING-DETAIL', false), poly([[0, -r * 0.25], [0, r * 0.25]], 'PLANTING-DETAIL', false)] };
}

Library item sizes

64-bit

The width, depth and height written into the library item.

The three sizes are rounded to 0.1 mm with r1. They come from the proportions each maker function returns (for example 2.9 × the crown radius for a deciduous tree), not from the bounding box of the finished mesh.

  • entourage.js
  • lines 325 to 336
  • ModelCraft 0.8.1
export function libraryItem(e) {
  const m = merged(e);
  return {
    format: 'cadcraft-library-item', format_version: 1,
    name: e.name, kind: 'furniture', category: e.cat, description: e.description, maker: '', model: '',
    plan: e.plan,
    body: { type: 'mesh', positions: m.positions, indices: m.indices },
    clearances: [],
    params: { width: r1(e.size[0]), depth: r1(e.size[1]), height: r1(e.size[2]) },
    source_attrs: { 'CC-RENDER-PARTS': partsText(m.ranges), 'CC-ENTOURAGE': String(ENTOURAGE_VERSION) },
  };
}

In detail

Method
Rings of points lofted into solids, subdivided icospheres scaled to ellipsoids and pushed in and out by seeded noise, swept side profiles, boxes and cylinders.
Number types
64-bit numbers in plain arrays; the random generator works on 32-bit integers and returns a fraction of 2³².
Tolerances
No comparison tolerances. Stored coordinates are rounded to 0.1 mm (Math.round(v × 10) ÷ 10).
Limits
Coordinates are on a 0.1 mm grid; round parts are low-polygon (8 to 16 sides, icospheres subdivided 0 to 2 times).

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