Resources Numerical accuracy Calculation policy

P&ID line geometry and connections

Works out where process and signal lines run, how long they are, and which symbols and lines they are joined to in a P&ID drawing.

How the precision is kept

Points, lengths and angles are ordinary 64-bit numbers from the drawing to the result. A connection is found by comparing a distance with a tolerance of 4 % of the larger symbol size, so the decision scales with the drawing's units. Duplicate points are removed with an absolute tolerance of 1e-9 drawing units. Numbers written to CSV keep at most 4 decimals.

Open item. Write CSV numbers at full precision instead of toFixed(4).

Calculations under this policy

15 calculations in 4 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/core.js CadCraft 0.8.2 64-bit 5 calculations Reviewed

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

Size units per drawing unit

64-bit

How many inches or millimeters one drawing unit is, from the drawing's unit setting.

factor = (meters per drawing unit) ÷ 0.0254 or ÷ 0.001

The unit table holds the defined values 0.0254, 0.3048 and 0.9144 m. The factor is one 64-bit division of two decimal constants; for a drawing in feet sized in inches, 0.3048 ÷ 0.0254 evaluates to 12.000000000000002, not 12.

  • addons/pid/lib/core.js
  • line 37
  • CadCraft 0.8.2
export const perUnit = (doc, u) => mPerUnitOf(doc) / (u === 'in' ? 0.0254 : 0.001);

Length of a line

64-bit

The length of a P&ID line along its corner points.

length = Σ √(Δxᵢ² + Δyᵢ²)

A sum of Math.hypot terms added in point order, all 64-bit. No tolerance is involved.

  • addons/pid/lib/core.js
  • lines 45 to 49
  • CadCraft 0.8.2
export function pathLength(pts) {
  let s = 0;
  for (let i = 1; i < pts.length; i++) s += Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]);
  return s;
}

Nearest point on a line

64-bit

The point of a line closest to a given point, its distance, and how far along the line it lies.

u = clamp((p − a)·(b − a) ÷ |b − a|², 0, 1); point = a + u (b − a)

The parameter is clamped to [0, 1], and a zero-length segment is skipped by testing the squared length against exactly 0. The distance along the line adds the full lengths of the earlier segments in order.

  • addons/pid/lib/core.js
  • lines 50 to 62
  • CadCraft 0.8.2
/** The point of a path nearest to p ({x, y}): { x, y, s (distance along), dist, seg } | null. */
export function nearestOnPath(pts, p) {
  let best = null, run = 0;
  for (let i = 1; i < pts.length; i++) {
    const [ax, ay] = pts[i - 1], [bx, by] = pts[i];
    const dx = bx - ax, dy = by - ay, L2 = dx * dx + dy * dy, L = Math.sqrt(L2);
    const u = L2 ? Math.max(0, Math.min(1, ((p.x - ax) * dx + (p.y - ay) * dy) / L2)) : 0;
    const x = ax + u * dx, y = ay + u * dy, dist = Math.hypot(p.x - x, p.y - y);
    if (!best || dist < best.dist) best = { x, y, s: run + u * L, dist, seg: i - 1 };
    run += L;
  }
  return best;
}

Label position on the longest segment

64-bit

The middle and direction of a line's longest segment, where its line number is written.

mid = (a + b) ÷ 2; angle = atan2(Δy, Δx)

Lengths are compared with a plain greater-than, so of two equal longest segments the first wins. The midpoint is one addition and one halving per coordinate.

  • addons/pid/lib/core.js
  • lines 63 to 71
  • CadCraft 0.8.2
/** The middle of the longest segment and its direction: { x, y, ang, len } | null (where a line's number is written). */
export function pointAtLongest(pts) {
  let best = null;
  for (let i = 1; i < pts.length; i++) {
    const [ax, ay] = pts[i - 1], [bx, by] = pts[i], len = Math.hypot(bx - ax, by - ay);
    if (len > 0 && (!best || len > best.len)) best = { x: (ax + bx) / 2, y: (ay + by) / 2, ang: Math.atan2(by - ay, bx - ax), len };
  }
  return best;
}

Numbers in CSV exports

64-bit

How a number cell is written when a P&ID list is exported as CSV.

A number cell is written with toFixed(4) and trailing zeros removed, so an exported number keeps at most 4 decimals whatever its size. The value in the drawing is not changed.

  • addons/pid/lib/core.js
  • lines 80 to 87
  • CadCraft 0.8.2
export function csv(rows) {
  const q = (v) => {
    let s = v === null || v === undefined ? '' : typeof v === 'number' ? fmtNum(v, 4) : String(v);
    if (/^[=+\-@\t\r]/.test(s) && !(typeof v === 'number')) s = "'" + s;
    return /[",\r\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
  };
  return rows.map((r) => r.map(q).join(',')).join('\r\n') + '\r\n';
}
addons/pid/lib/lines.js CadCraft 0.8.2 64-bit 3 calculations Reviewed

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

Cleaning a line's points

64-bit

Checks a line's data and removes repeated corner points before it is stored.

A point is dropped when it lies within 1e-9 drawing units of the point before it in the input list; the tolerance is absolute, not scaled to the size of the coordinates. Coordinates that are kept are stored unchanged.

  • addons/pid/lib/lines.js
  • lines 28 to 40
  • CadCraft 0.8.2
export function cleanLine(raw) {
  if (!raw || typeof raw !== 'object' || !Array.isArray(raw.pts)) return null;
  const pts = raw.pts.slice(0, LIMITS.pts).filter((p) => Array.isArray(p) && fin(p[0]) && fin(p[1])).map((p) => [p[0], p[1]]);
  const cleanPts = pts.filter((p, i) => i === 0 || Math.hypot(p[0] - pts[i - 1][0], p[1] - pts[i - 1][1]) > 1e-9);
  if (cleanPts.length < 2) return null;
  const str = (v) => String(v ?? '').replace(/[\u0000-\u001f]/g, ' ').trim().slice(0, LIMITS.text);
  const k = fin(raw.k) && raw.k > 0 ? raw.k : 1;
  return {
    pts: cleanPts, style: STYLES.includes(raw.style) ? raw.style : 'process',
    size: str(raw.size), svc: str(raw.svc), seq: str(raw.seq), spec: str(raw.spec), lnOverride: str(raw.lnOverride), insul: str(raw.insul),
    arrow: ARROWS.includes(raw.arrow) ? raw.arrow : 'end', lbl: raw.lbl !== false, k, th: fin(raw.th) && raw.th > 0 ? raw.th : k * 0.2,
  };
}

Line ends and their directions

64-bit

The two end points of a line and the direction in which the line leaves each.

a = atan2(end − neighbor)

Each direction is one atan2 of coordinate differences between an end point and the corner next to it. The end coordinates are returned unchanged.

  • addons/pid/lib/lines.js
  • lines 57 to 60
  • CadCraft 0.8.2
export function endsOf(d) {
  const n = d.pts.length, [ax, ay] = d.pts[0], [bx, by] = d.pts[1], [cx, cy] = d.pts[n - 1], [ex, ey] = d.pts[n - 2];
  return [{ x: ax, y: ay, a: Math.atan2(ay - by, ax - bx) }, { x: cx, y: cy, a: Math.atan2(cy - ey, cx - ex) }];
}

Move, rotate, scale or mirror a line

64-bit

The line's points, symbol size and text height after a transform.

x′ = m₀x + m₂y + m₄; y′ = m₁x + m₃y + m₅; s = √|m₀m₃ − m₁m₂|

Each point goes through the 2×3 matrix in one 64-bit expression. The symbol size and text height are multiplied by s only when s is above 1e-12 and differs from 1 by more than 1e-12; a non-uniform scale is reduced to that single factor.

  • addons/pid/lib/lines.js
  • lines 100 to 106
  • CadCraft 0.8.2
export function transformLine(d, m) {
  const out = structuredClone(d);
  out.pts = d.pts.map(([x, y]) => [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]]);
  const sc = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
  if (sc > 1e-12 && Math.abs(sc - 1) > 1e-12) { out.k = d.k * sc; out.th = d.th * sc; }
  return out;
}
addons/pid/lib/model.js CadCraft 0.8.2 64-bit 4 calculations Reviewed

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

Symbol connection points in the drawing

64-bit

Turns a point of a symbol's own frame into drawing coordinates using the symbol's position, rotation and scale.

x′ = x₀ + (x·sx)·cos θ − (y·sy)·sin θ; y′ = y₀ + (x·sx)·sin θ + (y·sy)·cos θ

cos and sin of the rotation are taken once and reused for every point. A scale that is missing, zero or not finite is replaced by 1.

  • addons/pid/lib/model.js
  • lines 30 to 35
  • CadCraft 0.8.2
function place(d, e) {
  const rot = Number(e.rot) || 0, c = Math.cos(rot), s = Math.sin(rot);
  const sx = Number.isFinite(e.sx) && e.sx ? e.sx : 1, sy = Number.isFinite(e.sy) && e.sy ? e.sy : 1;
  const tx = (x, y) => ({ x: (e.x || 0) + (x * sx) * c - (y * sy) * s, y: (e.y || 0) + (x * sx) * s + (y * sy) * c });
  return { tx, rot };
}

Connection tolerance

64-bit

How close a line end must be to a connection point, a line end or a line for the two to count as joined.

tol = 0.04 × max(size a, size b, 1e-6); joined when √(Δx² + Δy²) ≤ tol

The tolerance is 4 % of the larger of the two symbol sizes, so it scales with the drawing's units; the floor of 1e-6 applies only when both sizes are missing, zero or smaller than that. The comparison includes equality.

  • addons/pid/lib/model.js
  • lines 65 to 66
  • CadCraft 0.8.2
const tolOf = (a, b) => 0.04 * Math.max(a || 0, b || 0, 1e-6);   // 4 % of a symbol: the commands snap exactly, this only absorbs rounding
const near = (p, q, tol) => Math.hypot(p.x - q.x, p.y - q.y) <= tol;

Candidate search grid

64-bit

Limits which symbol connections and lines are tested against each line end.

cell = max(tol of the largest size, 1e-9); index = floor(v ÷ cell)

The cell size is the tolerance of the largest symbol or line size in the drawing. Connection points are looked up in the 3 × 3 cells around a line end, and each segment is registered in every cell of its bounding box grown by one cell; a line with a segment whose box covers more than 4096 cells is instead tested against every end.

  • addons/pid/lib/model.js
  • lines 67 to 87
  • CadCraft 0.8.2
let maxSize = 1e-6;
for (const s of symbols) maxSize = Math.max(maxSize, s.l || 0);
for (const ln of lines) maxSize = Math.max(maxSize, ln.data.k || 0);
const cell = Math.max(tolOf(maxSize, maxSize), 1e-9), cx = (v) => Math.floor(v / cell);
const grid = () => new Map();
const put = (g, x, y, v) => { const k = cx(x) + ',' + cx(y); const l = g.get(k); if (l) l.push(v); else g.set(k, [v]); };
const around = (g, x, y, out) => { const i = cx(x), j = cx(y); for (let a = i - 1; a <= i + 1; a++) for (let b = j - 1; b <= j + 1; b++) { const l = g.get(a + ',' + b); if (l) for (const v of l) out.push(v); } };
const connGrid = grid();                                            // symbol connection points: [symbol index, connection index]
symbols.forEach((s, si) => s.conns.forEach((c, ci) => put(connGrid, c.x, c.y, [si, ci])));
const lineGrid = grid(), bigLines = [];                             // each line's segments, boxes grown by the largest tolerance
lines.forEach((o, oi) => {
  const reg = new Set();
  for (let k = 1; k < o.pts.length; k++) {
    const [ax, ay] = o.pts[k - 1], [bx, by] = o.pts[k];
    const i0 = cx(Math.min(ax, bx) - cell), i1 = cx(Math.max(ax, bx) + cell), j0 = cx(Math.min(ay, by) - cell), j1 = cx(Math.max(ay, by) + cell);
    if ((i1 - i0 + 1) * (j1 - j0 + 1) > 4096) { bigLines.push(oi); return; }   // a very long segment: checked against every end
    for (let i = i0; i <= i1; i++) for (let j = j0; j <= j1; j++) reg.add(i + ',' + j);
  }
  for (const k of reg) { const l = lineGrid.get(k); if (l) l.push(oi); else lineGrid.set(k, [oi]); }
});
const bigSet = new Set(bigLines), tmp = [];

Line end on the body of another line

64-bit

Decides whether a line end lands on another line between its ends, which makes it a branch.

u = clamp((p − a)·(b − a) ÷ |b − a|², 0, 1); hit when |p − (a + u (b − a))| ≤ tol

The projection parameter is clamped to [0, 1] and a zero-length segment is guarded. The hit counts as a branch only when the end is not also within the tolerance of either end of the other line.

  • addons/pid/lib/model.js
  • lines 102 to 108
  • CadCraft 0.8.2
let hit = false;
for (let k = 1; k < o.pts.length && !hit; k++) {
  const [ax, ay] = o.pts[k - 1], [bx, by] = o.pts[k], dx = bx - ax, dy = by - ay, L2 = dx * dx + dy * dy;
  const u = L2 ? Math.max(0, Math.min(1, ((end.x - ax) * dx + (end.y - ay) * dy) / L2)) : 0;
  if (Math.hypot(end.x - (ax + u * dx), end.y - (ay + u * dy)) <= tol) hit = true;
}
if (hit && !near(end, o.ends[0], tol) && !near(end, o.ends[1], tol)) { add(key, { uid: o.uid, kind: 'line', at: 'mid' }); add(o.uid + '#mid', { uid: ln.uid, kind: 'line', at: 'end' + i }); }
addons/pid/lib/symbols.js CadCraft 0.8.2 64-bit 3 calculations Reviewed

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

Connection points of a symbol

64-bit

Where lines attach to a placed symbol, in the symbol's own frame.

x = x_unit × l; y = y_unit × l

Each connection is a coordinate of the unit-size symbol table multiplied once by the symbol length l. An actuator connection is added at a fixed fraction of l (0.52 to 0.67, by actuator type).

  • addons/pid/lib/symbols.js
  • lines 293 to 300
  • CadCraft 0.8.2
/** The connection points of a placed symbol in its own frame, drawing units (a valve's actuator adds `actuator`). */
export function connPoints(d) {
  const def = SYMBOLS[d.sym];
  if (!def) return [];
  const out = def.conn.map((c) => ({ x: c.x * d.l, y: c.y * d.l, a: c.a, n: c.n }));
  if (def.group === 'valve' && ACT_TOP[d.act]) out.push({ x: 0, y: ACT_TOP[d.act] * d.l, a: PI / 2, n: 'actuator' });
  return out;
}

Symbol drawn at its size

64-bit

Scales the unit-size symbol shapes to the placed symbol's length.

v′ = v × k

Every coordinate, radius and text height is multiplied once by k in 64-bit. Arc angles and polyline bulges are left as they are.

  • addons/pid/lib/symbols.js
  • lines 48 to 62
  • CadCraft 0.8.2
/** Entities scaled about the origin (text heights too). */
export function scale(ents, k) {
  const s = (v) => v * k;
  return ents.map((e) => {
    switch (e.type) {
      case 'line': return { ...e, x1: s(e.x1), y1: s(e.y1), x2: s(e.x2), y2: s(e.y2) };
      case 'circle': return { ...e, cx: s(e.cx), cy: s(e.cy), r: s(e.r) };
      case 'arc': return { ...e, cx: s(e.cx), cy: s(e.cy), r: s(e.r) };
      case 'poly': return { ...e, pts: e.pts.map((p) => ({ x: s(p.x), y: s(p.y), b: p.b || 0 })) };
      case 'hatch': return { ...e, loops: e.loops.map((l) => l.map(s)) };
      case 'text': return { ...e, x: s(e.x), y: s(e.y), h: s(e.h) };
      default: return e;
    }
  });
}

Symbol turned and moved

64-bit

Rotates symbol entities about the origin and moves them to a point.

x′ = pₓ + x cos θ − y sin θ; y′ = p_y + x sin θ + y cos θ

cos and sin are taken once per call. Arc angles and text rotations have the angle added; radii and bulges are unchanged.

  • addons/pid/lib/symbols.js
  • lines 315 to 328
  • CadCraft 0.8.2
/** Entities turned by `ang` about the origin and moved to p (the preview while placing; a placed symbol is turned by its reference). */
export function placeEnts(ents, p, ang = 0) {
  const c = Math.cos(ang), s = Math.sin(ang);
  const tx = (x, y) => [p.x + x * c - y * s, p.y + x * s + y * c];
  return ents.map((e) => {
    if (e.type === 'line') { const [x1, y1] = tx(e.x1, e.y1), [x2, y2] = tx(e.x2, e.y2); return { ...e, x1, y1, x2, y2 }; }
    if (e.type === 'circle') { const [cx, cy] = tx(e.cx, e.cy); return { ...e, cx, cy }; }
    if (e.type === 'arc') { const [cx, cy] = tx(e.cx, e.cy); return { ...e, cx, cy, a0: e.a0 + ang, a1: e.a1 + ang }; }
    if (e.type === 'poly') return { ...e, pts: e.pts.map((q) => { const [x, y] = tx(q.x, q.y); return { x, y, b: q.b || 0 }; }) };
    if (e.type === 'hatch') return { ...e, loops: e.loops.map((l) => { const o = []; for (let i = 0; i + 1 < l.length; i += 2) o.push(...tx(l[i], l[i + 1])); return o; }) };
    if (e.type === 'text') { const [x, y] = tx(e.x, e.y); return { ...e, x, y, rot: (e.rot || 0) + ang }; }
    return e;
  });
}

In detail

Method
Polyline length as a sum of segment lengths; nearest point by clamped projection onto each segment; connection by distance between a line end and a symbol connection point, another line end, or the body of another line; candidates found through a uniform grid.
Number types
64-bit numbers throughout; points are plain arrays and objects.
Tolerances
Connection: 0.04 × max(size a, size b, 1e-6), relative to symbol size. Duplicate points: 1e-9 drawing units, absolute. Scale change on transform: applied only when the factor differs from 1 by more than 1e-12. Text direction: 1e-9 radian.
Limits
A non-uniform scale is reduced to one factor, √|determinant|, for the symbol size and text height. CSV numbers are rounded to 4 decimals.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach