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.
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.
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++; }
}
}
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.
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.
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.
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.
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.