Resources Numerical accuracy Calculation policy

Landscape IFC export

Writes plants, beds, hedges, planted areas, hardscape and freeform objects to an IFC file in meters.

How the precision is kept

Coordinates are converted to meters in 64-bit and moved to a local origin near the first element, so the numbers written are small. Every real number is then written with at most 6 decimals, which is one micrometer for a length; directions get 12. The text format therefore sets the precision of the file, not the arithmetic.

Open item. Write coordinates and quantities with the shortest text that reads back to the same 64-bit number instead of 6 decimals.

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.

landscape/ifc-landscape.js CivilCraft 0.8.1 64-bit 7 calculations Reviewed

Tests bin/test-landscape.mjs, bin/test-civilcraft-ifc.mjs

Number format in the file

64-bit

Turns a number into the text written to the IFC file.

text = v.toFixed(6), trailing zeros removed

Every coordinate, dimension and quantity goes through toFixed(6) (12 for directions), so the exported value is rounded to 6 decimals. A value under 0.5e-6 in magnitude becomes 0 and a non-finite value is written as 0.

  • landscape/ifc-landscape.js
  • line 50
  • CivilCraft 0.8.1
const R = (v, d = 6) => { if (!Number.isFinite(v)) return '0.'; let s = (Math.abs(v) < 0.5 * 10 ** -d ? 0 : v).toFixed(d).replace(/(\.\d*?)0+$/, '$1'); if (!s.includes('.')) s += '.'; return s === '-0.' ? '0.' : s; };

Local origin of the site

64-bit

The point subtracted from every element position so the file holds small coordinates.

origin = round(first x ÷ 100) × 100, round(first y ÷ 100) × 100

The origin is the first element's position rounded to a whole multiple of 100 project units. It is written to the CadCraft_Site property set in meters through the 6-decimal format.

  • landscape/ifc-landscape.js
  • lines 89 to 91
  • CivilCraft 0.8.1
const first = elements.find((e) => e.at);
const ox = first ? Math.round(first.at.x / 100) * 100 : 0, oy = first ? Math.round(first.at.y / 100) * 100 : 0;
const siteLP = w.add('IFCLOCALPLACEMENT($,' + WCS + ')');

Element position and rotation

64-bit

Each element's position relative to the site origin, in meters, and its rotation as a direction.

p = ((x − oₓ) × k, (y − oᵧ) × k, z × k); direction = (cos rot, sin rot, 0)

The origin is subtracted before the multiplication by meters per project unit, so the product is formed on a small number. The result is then rounded to 6 decimals of a meter and the direction to 12 decimals.

  • landscape/ifc-landscape.js
  • lines 143 to 144
  • CivilCraft 0.8.1
const place = w.add('IFCAXIS2PLACEMENT3D(' + w.pt([(at.x - ox) * k, (at.y - oy) * k, (at.z || 0) * k]) + ',' + Z + ',' + (rot ? w.dir([Math.cos(rot), Math.sin(rot), 0]) : X) + ')');
const lp = w.add('IFCLOCALPLACEMENT(' + siteLP + ',' + place + ')');

Profiles and extrusions

64-bit

The circles, polygons and extrusion depths that make each element's body.

point = (x × k, y × k); depth = max(1e-4, (z₁ − z₀) × k)

Each profile point is multiplied by meters per project unit and written with 6 decimals. An extrusion thinner than 1e-4 m is written as 1e-4 m.

  • landscape/ifc-landscape.js
  • lines 104 to 110
  • CivilCraft 0.8.1
const circle = (r) => w.once('IFCCIRCLEPROFILEDEF(.AREA.,$,$,' + R(r) + ')');
const polygon = (pts) => {
  const ring = pts.map((p) => w.pt([p.x * k, p.y * k]));
  ring.push(ring[0]);
  return w.add('IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,' + w.add('IFCPOLYLINE((' + ring.join(',') + '))') + ')');
};
const extrude = (profile, z0, z1) => w.add('IFCEXTRUDEDAREASOLID(' + profile + ',' + w.add('IFCAXIS2PLACEMENT3D(' + w.pt([0, 0, z0 * k]) + ',$,$)') + ',' + Z + ',' + R(Math.max(1e-4, (z1 - z0) * k)) + ')');

Mesh body

64-bit

A freeform object's vertices and triangles as a triangulated face set.

vertex = (x × k, y × k, z × k), 6 decimals each

Positions arrive relative to the element's own position and are scaled to meters, then written with 6 decimals. Indices are written 1-based as integers.

  • landscape/ifc-landscape.js
  • lines 116 to 121
  • CivilCraft 0.8.1
if (b.kind === 'mesh' && b.indices?.length >= 3) {   // LD.7: a freeform mesh, 1-based indices
  const pts = [];
  for (let i = 0; i < b.positions.length; i += 3) pts.push('(' + R(b.positions[i] * k) + ',' + R(b.positions[i + 1] * k) + ',' + R(b.positions[i + 2] * k) + ')');
  const tris = [];
  for (let i = 0; i < b.indices.length; i += 3) tris.push('(' + (b.indices[i] + 1) + ',' + (b.indices[i + 1] + 1) + ',' + (b.indices[i + 2] + 1) + ')');
  return [w.add('IFCTRIANGULATEDFACESET(' + w.add('IFCCARTESIANPOINTLIST3D((' + pts.join(',') + '),$)') + ',$,$,(' + tris.join(',') + '),$)')];

Quantity sets

64-bit

Areas, lengths and volumes converted to square meters, meters and cubic meters for the file.

area × k²; volume × k³; length × k

The factor is formed by multiplying meters per project unit by itself two or three times in 64-bit. The product is written with 6 decimals like every other real.

  • landscape/ifc-landscape.js
  • lines 159 to 163
  • CivilCraft 0.8.1
const qs = Object.entries(props).filter(([, q]) => q && Number.isFinite(q.v)).map(([qk, q]) => {
  const f = q.kind === 'area' ? k * k : q.kind === 'volume' ? k * k * k : q.kind === 'length' ? k : 1;
  const T = { area: 'IFCQUANTITYAREA', volume: 'IFCQUANTITYVOLUME', length: 'IFCQUANTITYLENGTH', count: 'IFCQUANTITYCOUNT' }[q.kind] || 'IFCQUANTITYCOUNT';
  return w.add(T + '(' + S(qk) + ',$,$,' + R(q.v * f) + ',$)');
});

Plant, bed and hedge bodies

64-bit

The simple solids and property values that stand for plants, beds and hedges in the file.

tree: trunk r = max(0.05 m, 8% of crown radius) to 40% of height, crown above; bed height = share-weighted mean plant height

Sizes come from the plant list in meters and are divided by meters per project unit, then multiplied back by the writer. A bed is placed at the mean of its corner coordinates and at the mean of the ground elevations at its corners. The bed's area property is the fill's area × k².

  • landscape/ifc-landscape.js
  • lines 192 to 237
  • CivilCraft 0.8.1
/** Elements for every plant, bed and hedge (plants on the ground: `zAt(x, y)` gives the surface elevation). */
export function plantingElements(ls, index, units, { zAt = null, lang = 'en', which = null } = {}) {
  const k = unitM(units), show = which || ls.show || 'mature', out = [];
  const ground = (x, y) => { const z = zAt ? zAt(x, y) : null; return Number.isFinite(z) ? z : 0; };
  const typeOf = (sp) => ({
    key: 'plant-' + sp.key, cls: 'IfcGeographicElementType', predefined: 'VEGETATION', name: `${displayName(sp, lang)} (${sp.botanical})`,
    psets: { CadCraft_Plant: { BotanicalName: sp.botanical, CommonName: displayName(sp, lang), PlantType: sp.type, Form: sp.form, Foliage: sp.foliage,
      MatureHeight: sp.size.mature.h, MatureSpread: sp.size.mature.s, PlantingHeight: sizeOf(sp, 'plant').h, PlantingSpread: sizeOf(sp, 'plant').s,
      Container: sp.container, HardinessZone: sp.zone ? `${sp.zone.min}-${sp.zone.max}` : '', WaterNeed: sp.water, Light: sp.light.join(', '), Library: 'plants', SpeciesKey: sp.key } },
  });
  const bodyOf = (sp) => {   // in project units: trunk + crown for a tree, one round mass otherwise
    const z = sizeOf(sp, show), h = z.h / k, r = z.s / 2 / k;
    if (sp.type === 'tree') return { kind: 'cylinders', parts: [{ r: Math.max(0.05 / k, r * 0.08), z0: 0, z1: h * 0.4 }, { r, z0: h * 0.4, z1: h }] };
    return { kind: 'cylinders', parts: [{ r, z0: 0, z1: Math.max(0.02 / k, h) }] };
  };
  for (const p of ls.plants) {
    const sp = index.get(p.key);
    if (!sp) continue;
    out.push({ cls: 'IfcGeographicElement', predefined: 'VEGETATION', name: displayName(sp, lang), tag: p.id, at: { x: p.x, y: p.y, z: Number.isFinite(p.z) ? p.z : ground(p.x, p.y) },
      body: bodyOf(sp), type: typeOf(sp), psets: { CadCraft_Planting: { Source: 'plant', ShownSize: show, Label: p.label || '' } } });
  }
  for (const b of ls.beds) {
    const f = bedFill(b, index, units);
    const cx = b.pts.reduce((v, q) => v + q.x, 0) / b.pts.length, cy = b.pts.reduce((v, q) => v + q.y, 0) / b.pts.length;
    const zs = b.pts.map((q) => ground(q.x, q.y)), z = zs.reduce((a, v) => a + v, 0) / zs.length;
    let h = 0, wsum = 0;
    for (const m of b.mix) { const sp = index.get(m.key); if (sp) { h += m.pct * sizeOf(sp, show).h; wsum += m.pct; } }
    const counts = {};
    for (const [key, n] of Object.entries(f.counts)) counts[(index.get(key)?.botanical || key) + ' Count'] = n;
    // i18n-ignore: IFC file data (ObjectType), English like every library item
    out.push({ cls: 'IfcGeographicElement', predefined: 'VEGETATION', name: b.name, objectType: 'Planting bed', tag: b.id, at: { x: cx, y: cy, z },
      body: { kind: 'prism', pts: b.pts.map((q) => ({ x: q.x - cx, y: q.y - cy })), z0: 0, z1: Math.max(0.02, wsum ? h / wsum : 0.3) / k },
      psets: { CadCraft_PlantingBed: { Source: 'bed', PlantCount: f.pts.length, Spacing: bedSpacingM(b, index), Pattern: b.pattern, Mix: b.mix.map((m) => `${index.get(m.key)?.botanical || m.key} ${m.pct}%`).join('; '), Area: f.area * k * k, ...counts } } });
  }
  for (const hd of ls.hedges) {
    const sp = index.get(hd.key);
    if (!sp) continue;
    const f = hedgeFill(hd, index, units), z = sizeOf(sp, show), half = (z.s / 2 + (hd.rows === 2 ? hd.rowGap / 2 : 0)) / k;
    const a = offsetLine(hd.pts, half), b = offsetLine(hd.pts, -half).reverse();
    const o = hd.pts[0], g = ground(o.x, o.y);
    out.push({ cls: 'IfcGeographicElement', predefined: 'VEGETATION', name: hd.name, objectType: 'Hedge', tag: hd.id, at: { x: o.x, y: o.y, z: g },
      body: { kind: 'prism', pts: [...a, ...b].map((q) => ({ x: q.x - o.x, y: q.y - o.y })), z0: 0, z1: z.h / k },
      type: typeOf(sp), psets: { CadCraft_Hedge: { Source: 'hedge', PlantCount: f.pts.length, Rows: hd.rows, Length: f.length * k, Spacing: f.spacing * k } } });
  }
  return out;
}

In detail

Method
ISO 10303-21 text written directly, schema IFC4X3_ADD2. Bodies are extruded circles and polygons, or a triangulated face set for a mesh. The site origin is the first element's position rounded to 100 project units and is recorded in a property set.
Number types
64-bit numbers, written as text with toFixed(6), or toFixed(12) for directions.
Tolerances
Values smaller than 0.5e-6 in magnitude are written as 0. An extrusion depth is at least 1e-4 m. The geometric context states a precision of 1e-5.
Limits
Lengths carry 6 decimals of a meter, areas 6 decimals of a square meter and volumes 6 decimals of a cubic meter. Non-finite values are written as 0.

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