Resources Numerical accuracy Calculation policy

DEXPI and PCF plant exchange files

Reads and writes plant models as DEXPI XML and piping component files (PCF), converting positions, sizes and angles between the file's units and the drawing.

How the precision is kept

Inside the app the numbers are 64-bit. The files set the accuracy: the DEXPI export rounds every position and size to 0.001 mm and directions to 9 decimals, and the PCF writer rounds coordinates and bores to 0.0001 mm and angles to a hundredth of a degree, which is how PCF stores them. Unit factors are the exact definitions (25.4, 304.8). On import, line ends near a symbol connection are moved onto it.

Open item. Write DEXPI positions and PCF coordinates with the shortest text that reads back to the same 64-bit number instead of toFixed(3) and toFixed(4).

Calculations under this policy

15 calculations in 3 source files. 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.

addons/pid/lib/dexpi-pid.js CadCraft 0.8.2 64-bit 4 calculations Reviewed

Tests tools/addons-test/browser-pid.mjs

Drawing units to millimeters on export

64-bit

Converts every exported symbol position, line vertex and symbol size to millimeters.

mm = round(v × mmPerUnit, 3 decimals)

The value is multiplied by the unit factor and then rounded with toFixed(3) before it goes into the export model. Digits beyond 0.001 mm are not written to the file.

  • addons/pid/lib/dexpi-pid.js
  • line 22
  • CadCraft 0.8.2
const mm = (v) => +(v * mmPerUnit).toFixed(3);

Drawing extent on export

64-bit

The width and height stated for the exported drawing.

w = ceil(mm(max x) + 50); h = ceil(mm(max y) + 50)

The running maximum starts at 0, so coordinates below zero do not enlarge the extent, and the minimum is not tracked. The result is rounded up to a whole millimeter after 50 mm is added.

  • addons/pid/lib/dexpi-pid.js
  • line 100
  • CadCraft 0.8.2
out.extent = { w: Math.ceil(mm(maxX) + 50), h: Math.ceil(mm(maxY) + 50) };

Symbol position and size on import

64-bit

Places an imported plant item in the drawing at the file's position and size.

x = x_file × perMm; l = SymbolSize × perMm

The position is one multiplication of the number read from the file. The size comes from parseFloat of the SymbolSize attribute when it is above zero, otherwise from a default size by symbol group (×2 for equipment, ×0.6 for instruments).

  • addons/pid/lib/dexpi-pid.js
  • lines 119 to 125
  • CadCraft 0.8.2
const push = (it, sym, extra = {}) => {
  const def = SYMBOLS[sym];
  const own = parseFloat(attrOf(it, 'SymbolSize')) * perMm;   // our own export says how big it drew the symbol
  const l = own > 0 ? own : symL * (def.group === 'equip' ? 2 : def.group === 'inst' ? 0.6 : 1);
  const d = cleanSymbol({ sym, l, tag: it.tag || it.name || '', desc: attrOf(it, 'Description', 'ComponentClassDescription') || '', act: extra.act || 'none', attrs: extra.attrs || {} });
  if (!d) { lost.bad++; return null; }
  const e = { id: it.id, data: d, x: it.x === null ? null : it.x * perMm, y: it.y === null ? null : it.y * perMm, rot: it.rot || 0, src: it };

Line ends moved onto connections on import

64-bit

Moves an imported line end onto the nearest symbol connection when it is close enough.

moved when distance ≤ 0.6 × symbol size

An end within 0.6 × the symbol size of a connection point takes that point's coordinates, so imported line ends can change by up to that distance. Moves are counted in the import report unless they are 1e-9 or smaller.

  • addons/pid/lib/dexpi-pid.js
  • lines 164 to 175
  • CadCraft 0.8.2
const tol = symL * 0.6;
const connsOf = (e) => { const c = Math.cos(e.rot), sn = Math.sin(e.rot); return e.x === null ? [] : connPoints(e.data).map((q) => ({ x: e.x + q.x * c - q.y * sn, y: e.y + q.x * sn + q.y * c })); };
const all = symbols.filter((e) => e.x !== null).flatMap(connsOf);
let snapped = 0;
for (const l of lines) {
  const pts = l.data.pts;
  for (const k of [0, pts.length - 1]) {
    let best = null;
    for (const q of all) { const d = Math.hypot(q.x - pts[k][0], q.y - pts[k][1]); if (d <= tol && (!best || d < best.d)) best = { q, d }; }
    if (best && best.d > 1e-9) { pts[k] = [best.q.x, best.q.y]; snapped++; }
  }
}
addons/pid/lib/interop/dexpi.js CadCraft 0.8.2 64-bit 3 calculations Reviewed

Tests tools/addons-test/browser-pid.mjs

Numbers read from DEXPI attributes

64-bit

Turns an attribute's text into a number.

parseFloat reads the leading decimal number to the nearest 64-bit value and ignores any text after it. Text that does not start with a number becomes null, not zero.

  • addons/pid/lib/interop/dexpi.js
  • line 34
  • CadCraft 0.8.2
const num = (v) => { const x = parseFloat(v); return Number.isFinite(x) ? x : null; };

Position and rotation of a plant item

64-bit

The location of an item and its rotation in the plane, read from its Position element.

rotation = atan2(Reference.Y, Reference.X)

The location is taken as read. The rotation is atan2 of the Reference vector; the Axis element is not read, so a flipped axis does not change the result.

  • addons/pid/lib/interop/dexpi.js
  • lines 47 to 50
  • CadCraft 0.8.2
else if (c.tag === 'Position') {
  const loc = find(c, 'Location'), ref = find(c, 'Reference');
  if (loc) { it.x = num(attr(loc, 'X')); it.y = num(attr(loc, 'Y')); it.z = num(attr(loc, 'Z')); }
  if (ref) { const rx = num(attr(ref, 'X')), ry = num(attr(ref, 'Y')); if (rx !== null && ry !== null) it.rot = Math.atan2(ry, rx); }

Position and direction written to DEXPI

64-bit

Writes an item's location and its direction vector.

Reference = (round(cos θ, 9 decimals), round(sin θ, 9 decimals), 0)

The direction is cos and sin of the rotation, each rounded by toFixed(9). A rotation read back with atan2 from those two numbers agrees with the original to about 1e-9 radian, not to the last 64-bit digit.

  • addons/pid/lib/interop/dexpi.js
  • lines 119 to 122
  • CadCraft 0.8.2
if (it.x !== null && it.y !== null) {
  w.open('Position');
  w.leaf('Location', { X: it.x, Y: it.y, Z: it.z ?? 0 }); w.leaf('Axis', { X: 0, Y: 0, Z: 1 });
  const r = it.rot || 0; w.leaf('Reference', { X: +Math.cos(r).toFixed(9), Y: +Math.sin(r).toFixed(9), Z: 0 });
addons/pid/lib/interop/pcf.js CadCraft 0.8.2 64-bit 8 calculations Reviewed

Tests bin/test-interop.mjs

Numbers written to PCF

64-bit

How a coordinate or a bore is written in a PCF file.

text = round(n, 4 decimals)

toFixed(4) then conversion back to a number to drop trailing zeros. Coordinates and bores, both in millimeters, keep at most 0.0001 mm in the file.

  • addons/pid/lib/interop/pcf.js
  • line 36
  • CadCraft 0.8.2
export function fmt(n) { return String(+Number(n).toFixed(4)); }

End point lines read from PCF

64-bit

Reads a component's end point: three coordinates, a bore and an end type.

Each token goes through Number(). The line is kept only when the three coordinates and the bore are all finite; a line with no bore is counted as a bad number line and left out, so the fallback bore of 0 in the same statement is never used.

  • addons/pid/lib/interop/pcf.js
  • line 66
  • CadCraft 0.8.2
if (key === 'END-POINT') { const v = nums(parts.slice(1), 3); const bore = Number(parts[4]); if (v && Number.isFinite(bore) !== false) comp.ends.push({ x: v[0], y: v[1], z: v[2], bore: Number.isFinite(bore) ? bore : 0, end: clean(parts[5] || '') }); else lost.badNumbers++; }

PCF units to millimeters

64-bit

Converts coordinates and bores from the units named in the file header to millimeters.

x_mm = x × factor; factors: inch 25.4, foot 304.8, meter 1000, cm 10

The factors are the exact definitions and each value is multiplied once. A unit name that is not in the table is counted as a problem and treated as millimeters. The weight table is used only to recognize the unit name; no weight is converted.

  • addons/pid/lib/interop/pcf.js
  • lines 76 to 86
  • CadCraft 0.8.2
const cu = rawUnits['UNITS-CO-ORDS'], bu = rawUnits['UNITS-BORE'], wu = rawUnits['UNITS-WEIGHT'];
const kc = cu === undefined ? 1 : MM[cu], kb = bu === undefined ? 1 : MM[bu];
if (kc === undefined || kb === undefined) { lost.unitUnknown++; }
const fc = kc ?? 1, fb = kb ?? 1;
if (wu && !(wu in KG)) lost.unitUnknown++;
for (const p of model.pipelines) for (const c of p.components) {
  for (const e of c.ends) { e.x *= fc; e.y *= fc; e.z *= fc; e.bore *= fb; }
  if (c.centre) c.centre = c.centre.map((v) => v * fc);
  if (c.at) c.at = c.at.map((v) => v * fc);
  if (c.branch) { c.branch.x *= fc; c.branch.y *= fc; c.branch.z *= fc; c.branch.bore *= fb; }
}

Bend angle read from PCF

64-bit

Reads a component's angle in degrees.

angle = value ÷ 100

PCF stores hundredths of a degree, so the reader divides by 100. The writer rounds the angle times 100 to a whole number, which limits a written angle to 0.01°.

  • addons/pid/lib/interop/pcf.js
  • line 71
  • CadCraft 0.8.2
else if (key === 'ANGLE') { const a = Number(parts[1]); comp.angle = Number.isFinite(a) ? a / 100 : null; }   // PCF writes hundredths of a degree

Geometry checks on a PCF file

64-bit

Flags pipes with no length, elbows whose center is not equally far from both ends, and straight parts that change bore.

|r₀ − r₁| > 0.01 × max(r₀, r₁, 1)

A pipe is flagged when its length is not above 0.01 mm, and a bore change when it exceeds 0.01 mm; both are absolute. The elbow test is relative: 1 % of the larger radius, with a floor of 0.01 mm.

  • addons/pid/lib/interop/pcf.js
  • lines 94 to 99
  • CadCraft 0.8.2
if (c.type === 'PIPE' && c.ends.length === 2) { const L = lengthOf(c); if (!(L > 0.01)) lost.zero++; }
if ((c.type === 'ELBOW' || c.type === 'BEND') && c.centre && c.ends.length === 2) {
  const d = (e) => Math.hypot(e.x - c.centre[0], e.y - c.centre[1], e.z - c.centre[2]);
  if (Math.abs(d(c.ends[0]) - d(c.ends[1])) > 0.01 * Math.max(d(c.ends[0]), d(c.ends[1]), 1)) lost.elbowCentre++;
}
if ((c.type === 'PIPE' || c.type === 'FLANGE' || c.type === 'VALVE') && c.ends.length === 2 && Math.abs(c.ends[0].bore - c.ends[1].bore) > 0.01 && c.ends[0].bore > 0 && c.ends[1].bore > 0) lost.bore++;

Length of a PCF component

64-bit

The straight distance between a component's two end points, in millimeters.

length = √(Δx² + Δy² + Δz²)

One Math.hypot of three coordinate differences in 64-bit. A component with fewer than two ends returns 0.

  • addons/pid/lib/interop/pcf.js
  • lines 113 to 116
  • CadCraft 0.8.2
export function lengthOf(c) {
  if (c.ends.length < 2) return 0;
  return Math.hypot(c.ends[1].x - c.ends[0].x, c.ends[1].y - c.ends[0].y, c.ends[1].z - c.ends[0].z);
}

Which PCF components meet

64-bit

Finds end points and branch points that coincide, and the ends left open.

joined when √(Δx² + Δy² + Δz²) ≤ tol

The tolerance is 0.5 mm by default and absolute. Points are binned in a grid whose cell is the tolerance (at least 1e-6), and each point is tested against the 27 cells around it, so no pair within the tolerance is missed.

  • addons/pid/lib/interop/pcf.js
  • lines 118 to 139
  • CadCraft 0.8.2
/** Where components meet: every end point within `tol` mm of another joins them. → { joins: [[a, ea, b, eb]], open: [{ c, end }] } with component indices. */
export function connect(pipeline, tol = 0.5) {
  const pts = [];
  pipeline.components.forEach((c, i) => {
    c.ends.forEach((e, k) => pts.push({ i, k, x: e.x, y: e.y, z: e.z }));
    if (c.branch) pts.push({ i, k: 2, x: c.branch.x, y: c.branch.y, z: c.branch.z });
  });
  const joins = [], touched = new Set();
  const cell = (v) => Math.floor(v / Math.max(tol, 1e-6));
  const grid = new Map();
  for (const p of pts) { const key = cell(p.x) + ',' + cell(p.y) + ',' + cell(p.z); (grid.get(key) || grid.set(key, []).get(key)).push(p); }
  for (const p of pts) {
    for (let dx = -1; dx <= 1; dx++) for (let dy = -1; dy <= 1; dy++) for (let dz = -1; dz <= 1; dz++) {
      for (const q of grid.get((cell(p.x) + dx) + ',' + (cell(p.y) + dy) + ',' + (cell(p.z) + dz)) || []) {
        if (q.i <= p.i || Math.hypot(p.x - q.x, p.y - q.y, p.z - q.z) > tol) continue;
        joins.push([p.i, p.k, q.i, q.k]); touched.add(p.i + ':' + p.k); touched.add(q.i + ':' + q.k);
      }
    }
  }
  const open = pts.filter((p) => !touched.has(p.i + ':' + p.k)).map((p) => ({ c: p.i, end: p.k }));
  return { joins, open };
}

Point lines written to PCF

64-bit

Writes an end point, branch point or center point line.

Each coordinate and the bore go through fmt, so they are rounded to 4 decimals of a millimeter on the way out. The header always states millimeters.

  • addons/pid/lib/interop/pcf.js
  • line 147
  • CadCraft 0.8.2
const pt = (k, x, y, z, bore, end) => '    ' + k + ' ' + [fmt(x), fmt(y), fmt(z), bore !== undefined && bore !== null ? fmt(bore) : '', end || ''].filter((s, i) => i < 3 || s !== '').join(' ');

In detail

Method
Text parsing with Number() and parseFloat; unit conversion by one multiplication per value; end-point matching through a uniform 3D grid with a distance test.
Number types
64-bit numbers; values in files are decimal text.
Tolerances
PCF joins: 0.5 mm by default, absolute. PCF checks: pipe length must exceed 0.01 mm; elbow center radii may differ by 1 % of the larger radius (at least 0.01 mm); bore mismatch above 0.01 mm. DEXPI import snap: 0.6 × symbol size; moves smaller than 1e-9 are not counted.
Limits
DEXPI export: 0.001 mm for positions, 9 decimals for the direction vector. PCF export: 4 decimals of a millimeter, angles to 0.01°. The DEXPI reader takes the rotation from the Reference vector only and does not read the Axis element.

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