Resources Numerical accuracy Calculation policy

P&ID placement, zones and values written to the drawing

Places P&ID symbols on process lines and connection points, cuts lines around inline symbols, draws hazard zones to scale, and writes imported stream values and relief sizes into the drawing.

How the precision is kept

Placement geometry is 64-bit: unit directions from Math.hypot, cut points at a half-length either side of the symbol, zone circles scaled by drawing units per meter. Values written into the drawing are rounded first: stream table cells to fixed decimals and the relief area to a whole square millimeter. The calculators themselves are in other files.

Open item. Write the relief area to the symbol unrounded and round it only where it is shown.

Calculations under this policy

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

addons/pid/main.js CadCraft 0.8.2 64-bit 8 calculations Reviewed

Tests None found by the review

A click landing on a connection, end or line

64-bit

Finds the connection point, line end or point on a line within reach of a click.

nearest within tol; a point on a line is ranked with distance + 0.1 × tol

The returned point is the connection's or end's own coordinates, or the projection on the line from nearestOnPath. Distances are Math.hypot in drawing units.

  • addons/pid/main.js
  • lines 98 to 109
  • CadCraft 0.8.2
/** The connection point, line end or line within reach of p: { x, y, kind, ... } | null. Snaps a new line's start. */
function snapTo(model, p, tol) {
  let best = null;
  const take = (cand, d) => { if (d <= tol && (!best || d < best.d)) best = { ...cand, d }; };
  for (const s of model.symbols) for (const c of s.conns) take({ x: c.x, y: c.y, kind: 'conn', sym: s, conn: c }, Math.hypot(p.x - c.x, p.y - c.y));
  for (const l of model.lines) {
    l.ends.forEach((e, i) => take({ x: e.x, y: e.y, kind: 'end', line: l, end: i }, Math.hypot(p.x - e.x, p.y - e.y)));
    const n = nearestOnPath(l.pts, p);
    if (n) take({ x: n.x, y: n.y, kind: 'on', line: l, seg: n.seg }, n.dist + tol * 0.1);   // a click on a line stays on it unless a nozzle or an end is closer
  }
  return best;
}

A line cut around an inline symbol

64-bit

Cuts a process line either side of a symbol placed on it, so the symbol's ends meet the line's.

u = (b − a) ÷ |b − a|; cut points = at ± u × half

The unit direction is the segment divided by its Math.hypot length, with no guard against a zero-length segment in this function. The line is left whole when the symbol is nearer than half its length to either corner. The longer piece, by summed length, keeps the line number.

  • addons/pid/main.js
  • lines 140 to 157
  • CadCraft 0.8.2
/** Put an inline symbol on a process line: the line is cut either side of it, so the symbol's ends meet the line's. */
function splitLineAround(doc, hit, at, half) {
  const L = hit.l, d = L.data;
  const [ax, ay] = d.pts[hit.seg], [bx, by] = d.pts[hit.seg + 1];
  const len = Math.hypot(bx - ax, by - ay), ux = (bx - ax) / len, uy = (by - ay) / len;
  const su = Math.hypot(at.x - ax, at.y - ay);
  if (su < half || len - su < half) return false;   // too near a corner: the symbol goes on, the line is left whole
  const before = d.pts.slice(0, hit.seg + 1).concat([[at.x - ux * half, at.y - uy * half]]);
  const after = [[at.x + ux * half, at.y + uy * half]].concat(d.pts.slice(hit.seg + 1));
  const plen = (pts) => pts.slice(1).reduce((n, p, i) => n + Math.hypot(p[0] - pts[i][0], p[1] - pts[i][1]), 0);
  const aKeeps = plen(before) >= plen(after);   // the longer piece keeps writing the number
  const A = { ...structuredClone(d), pts: before, arrow: d.arrow === 'start' || d.arrow === 'both' ? 'start' : 'none', lbl: d.lbl && aKeeps };
  const B = { ...structuredClone(d), pts: after, arrow: d.arrow === 'end' || d.arrow === 'both' ? 'end' : 'none', lbl: d.lbl && !aKeeps };
  const eid = Number(L.uid);
  doc.update(eid, api.objects.update(doc.get(eid), cleanLine(A)));
  doc.add(api.objects.create('pidline', cleanLine(B), { layer: LAYER_OF[d.style] }));
  return true;
}

A symbol's connection landed on a picked point

64-bit

Moves a new symbol so its nearest connection sits exactly on the connection or line end that was clicked.

offset = R(rot) · connection × size; at = picked − offset

The offset is recomputed with Math.cos and Math.sin on each call. The symbol's insertion point is the picked point minus the offset, so the connection lands on the target to within a rounding of that subtraction.

  • addons/pid/main.js
  • lines 195 to 197
  • CadCraft 0.8.2
const off = (cn) => ({ x: (cn.x * Math.cos(rot) - cn.y * Math.sin(rot)) * data.l, y: (cn.x * Math.sin(rot) + cn.y * Math.cos(rot)) * data.l });
const mine = SYMBOLS[sym].conn.reduce((b, cn) => (Math.hypot(r.x + off(cn).x - sn.x, r.y + off(cn).y - sn.y) < Math.hypot(r.x + off(b).x - sn.x, r.y + off(b).y - sn.y) ? cn : b));
at = { x: sn.x - off(mine).x, y: sn.y - off(mine).y };

Scale of an imported DEXPI drawing

64-bit

The drawing units per millimeter used to place an imported P&ID.

k = 1 ÷ (millimeters per drawing unit)

One reciprocal of the drawing's unit factor, passed to the importer.

  • addons/pid/main.js
  • lines 408 to 409
  • CadCraft 0.8.2
const k = 1 / perUnit(doc, 'mm');   // drawing units in a millimetre
const r = fromDexpi(d, { symL: sizes(doc).sym, perMm: k, fmt: settings.lineFormat });

Imported stream table values

64-bit

Converts stream temperature, pressure and flows to display units and writes them into a table in the drawing.

°C = K − 273.15 (1 decimal); bar = Pa ÷ 1e5 (3); kg/h = kg/s × 3600 (1); kmol/h = mol/s × 3.6 (3); fractions 3 and 4 decimals

The factors are exact. Each value is rounded with toFixed and stored in the table as text, so the drawing holds the rounded figures. 3.6 has no exact binary form, which matters far less than the rounding.

  • addons/pid/main.js
  • lines 426 to 428
  • CadCraft 0.8.2
const comps = r.data.components, f = (v, d = 2) => (Number.isFinite(v) ? String(+v.toFixed(d)) : '');
const rows = [[t('streams.col.stream'), '°C', 'bar', 'kg/h', 'kmol/h', 'VF', ...comps.map((c) => c + ' (' + r.data.basis + ')')],
  ...r.data.streams.map((s) => [s.name, f(s.T - 273.15, 1), f(s.P / 1e5, 3), f(s.mass * 3600, 1), f(s.mole * 3.6, 3), f(s.vf, 3), ...comps.map((c) => f(s.comp[c], 4))])];

Drawing coordinates for the area calculator

64-bit

Hands the symbols' positions and the meters per drawing unit to the hazardous-area calculator.

toMetres = (millimeters per drawing unit) ÷ 1000

Positions are passed in drawing units with one factor; the division by 1000 is exact up to one rounding.

  • addons/pid/main.js
  • lines 472 to 474
  • CadCraft 0.8.2
// PIDSOURCES (0.8 CH.17): the release sources of the drawing itself. Grade by what each symbol is, the substance by its service (the list is the person's), then the area calculator
// classifies them; PIDZONE draws the zones. Items carry the drawing's coordinates in metres.
const drawingItems = (doc) => { const model = readModel(doc), m = perUnit(doc, 'mm') / 1000; return { toMetres: m, items: model.symbols.map((sy) => ({ tag: sy.tag, sym: sy.sym, x: sy.x, y: sy.y, svc: (sy.attrs && sy.attrs.svc) || '', attrs: sy.attrs || {} })) }; };

Consequence zones drawn to scale

64-bit

Draws each zone of the consequence calculator as a circle or polygon at the picked point.

k = 1000 ÷ (millimeters per drawing unit); center = at + (cx, cy) × k; radius = r × k

Zone sizes in meters are multiplied by k, the drawing units per meter; nothing is rounded.

  • addons/pid/main.js
  • lines 499 to 500
  • CadCraft 0.8.2
if (z.type === 'circle') doc.add({ type: 'circle', cx: at.x + z.cx * k, cy: at.y + z.cy * k, r: z.r * k, layer });
else if (z.polygon && z.polygon.length > 2) doc.add({ type: 'poly', pts: z.polygon.map(([x, y]) => [at.x + x * k, at.y + y * k, 0]), closed: true, layer });

Relief valve area written to the symbol

64-bit

Sizes a relief valve with the relief calculator and writes its orifice letter and area on the symbol.

area_mm2 = round(A × 1e6)

The area A in square meters comes from relief.js. It is stored on the symbol as text rounded to a whole square millimeter.

  • addons/pid/main.js
  • lines 517 to 518
  • CadCraft 0.8.2
const A = basis === 'gas' ? R.gasArea({ ...inp.gas, Kc }).A : basis === 'steam' ? R.steamArea({ ...inp.steam, Kc }).A : R.liquidArea(inp.liquid).A, o = R.orificeLetter(A);
doc.change('PIDRELIEF', () => { const eid = Number(sy.uid); doc.update(eid, api.objects.update(doc.get(eid), cleanSymbol({ ...sy.data, attrs: { ...sy.data.attrs, orifice: o.letter || 'over T', area_mm2: String(Math.round(A * 1e6)), basis } }))); });

In detail

Method
Line cut at ± half the symbol length along the segment's unit direction; a symbol's nearest connection moved onto the picked connection; zones placed at at + (x, y) × k.
Number types
64-bit numbers; table cells and attributes are text.
Tolerances
A click reaches a connection, end or line within 0.4 × the symbol size; a line candidate is ranked with an extra 0.1 × tol so ends and connections win.
Limits
Stream table cells and the relief area attribute hold rounded text, so later reads of the drawing see the rounded figures.

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