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.
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.
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';
}
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.
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;
}
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 }); }
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.
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.