Resources Numerical accuracy Calculation policy

Orthogonal pipe router

Finds a pipe route between two nozzles that runs only along X, Y and Z, keeps clear of obstacles and has few bends, and reports its length.

How the precision is kept

The route is searched on a grid whose lines are 64-bit coordinates taken from the nozzles, the obstacle faces moved out by the clearance, the preferred levels and the midpoints between neighbors. Route points are grid coordinates, so the reported length is a sum of exact coordinate differences. The search state rounds the length of the current leg to a whole millimeter, and that rounding decides whether a bend is allowed.

Open item. Cap the stored run at the minimum leg itself (or at its ceiling), so a minimum leg with a fraction below one half does not block every later bend.

Calculations under this policy

6 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/plant/lib/route.js BimCraft 0.8.1 64-bit 6 calculations Reviewed

Tests None found by the review

Legs of a route

64-bit

The straight legs of a route with their axis and length.

axis = first of x, y, z with |Δ| > 10⁻⁶; length = |Δ| on that axis

The length of a leg is the absolute difference of one coordinate, which is exact for a leg that runs along an axis. A leg that is not along an axis would be given the length of one component only.

  • addons/plant/lib/route.js
  • lines 26 to 34
  • BimCraft 0.8.1
export function legsOf(points) {
  const out = [];
  for (let i = 1; i < points.length; i++) {
    const a = points[i - 1], b = points[i], d = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
    const ax = Math.abs(d[0]) > EPS ? 0 : Math.abs(d[1]) > EPS ? 1 : 2;
    out.push({ a, b, axis: 'xyz'[ax], len: Math.abs(d[ax]) });
  }
  return out;
}

Route against obstacles

64-bit

Whether the center line of a route comes within a clearance of any obstacle box.

Each leg is tested at 21 evenly spaced points against each box grown by the clearance, with a 1e-6 margin. An obstacle thinner than one twentieth of a leg can fall between the sample points.

  • addons/plant/lib/route.js
  • lines 37 to 44
  • BimCraft 0.8.1
/** Does a route's centre line come within `clear` of any obstacle's inside? (the router's own promise, tested). */
export function hitsObstacle(points, obstacles, clear = 0) {
  for (const l of legsOf(points)) for (const o of obstacles) {
    const g = grow(o, clear);
    for (let k = 0; k <= 20; k++) { const t = k / 20; if (inside([l.a[0] + (l.b[0] - l.a[0]) * t, l.a[1] + (l.b[1] - l.a[1]) * t, l.a[2] + (l.b[2] - l.a[2]) * t], g)) return true; }
  }
  return false;
}

Grid lines of the search

64-bit

The X, Y and Z coordinates at which a route may turn.

lines = nozzle and lead coordinates, obstacle faces ± clearance, levels, two outer lines, and the midpoint of each neighboring pair

Coordinates are collected in a Set, so only bit-identical values merge; two faces that differ in the last digit give two lines and a midpoint between them. The outer lines are 2 × the minimum leg beyond the extremes.

  • addons/plant/lib/route.js
  • lines 73 to 83
  • BimCraft 0.8.1
const lines = [0, 1, 2].map((k) => {
  const set = new Set([s[k], g[k], opt.from.p[k], opt.to.p[k]]);
  for (const b of obst) { set.add(b.min[k]); set.add(b.max[k]); }
  if (k === 2) for (const z of opt.levels || []) set.add(z);
  const sorted = [...set].sort((a, b) => a - b);
  const lo = sorted[0] - minLeg * 2, hi = sorted[sorted.length - 1] + minLeg * 2;   // room to go round the outside
  const all = new Set([...sorted, lo, hi]);
  const s2 = [...all].sort((a, b) => a - b);
  for (let i = 1; i < s2.length; i++) all.add((s2[i - 1] + s2[i]) / 2);
  return [...all].sort((a, b) => a - b);
});

Search state and the minimum leg

64-bit

The key that identifies a search state: grid node, direction and the length of the current leg.

cap = round(minLeg); key = ((node × 6 + direction) × (cap + 1)) + min(cap, round(run))

The run so far is rounded to a whole millimeter and capped at Math.round(minLeg), and the capped value is also what the next step reads back. A bend needs run ≥ minLeg − 1e-9, so when minLeg has a fraction below one half the capped run stays below it and every bend after the first is refused. Evaluating the function for this review with no obstacles gave a route for a minimum leg of 438 and 'no-route' for 438.2.

  • addons/plant/lib/route.js
  • lines 94 to 95
  • BimCraft 0.8.1
const cap = Math.round(minLeg);
const key = (i, j, k, d, run) => ((idx(i, j, k) * 6 + d) * (cap + 1)) + Math.min(cap, Math.round(run));

Cost of a step

64-bit

The cost added for moving to the next grid node.

cost = g + step × (1 + climbCost if vertical, else 1) + (bendCost if the direction changes)

The step is the sum of absolute coordinate differences between two grid nodes. Costs are added in 64-bit along the path and compared with a slack of 1e-9. Defaults: climb factor 0.3, bend cost 4 × the outside diameter + 200.

  • addons/plant/lib/route.js
  • lines 117 to 120
  • BimCraft 0.8.1
const step = Math.abs(b[0] - a[0]) + Math.abs(b[1] - a[1]) + Math.abs(b[2] - a[2]);
const cost = n.g + step * (axisOf(DIRS[d]) === 2 ? 1 + climbCost : 1) + (turn ? bendCost : 0);
const run = turn ? step : n.run + step;
const kk = key(ni, nj, nk, d, run);

Route points and length

64-bit

The final corner points of the route with collinear points removed, its length, bends and climbs.

length = Σ leg lengths; bends = legs − 1

A point closer than 1e-6 mm to the previous one is dropped, and a middle point is removed when the cross product of the two legs is below 1e-6 in size and they point the same way. The cross product is in square millimeters, so that test is absolute and not scaled to the leg lengths.

  • addons/plant/lib/route.js
  • lines 135 to 149
  • BimCraft 0.8.1
const raw = [opt.from.p.slice(), ...chain, opt.to.p.slice()];
const pts = [];
for (const p of raw) {
  const q = pts[pts.length - 1];
  if (q && Math.hypot(p[0] - q[0], p[1] - q[1], p[2] - q[2]) < EPS) continue;
  pts.push(p);
  while (pts.length >= 3) {
    const a = pts[pts.length - 3], b = pts[pts.length - 2], c = pts[pts.length - 1];
    const u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]], v = [c[0] - b[0], c[1] - b[1], c[2] - b[2]];
    const cr = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
    if (Math.hypot(...cr) < 1e-6 && u[0] * v[0] + u[1] * v[1] + u[2] * v[2] > 0) pts.splice(pts.length - 2, 1); else break;
  }
}
const legs = legsOf(pts);
return { ok: true, points: pts, length: legs.reduce((n, l) => n + l.len, 0), bends: legs.length - 1, climbs: legs.filter((l) => l.axis === 'z').length };

In detail

Method
A* over (grid node, direction, run so far) with a Manhattan-distance estimate; cost = length, plus a factor for climbs and a fixed cost per bend.
Number types
64-bit numbers; integer keys for the search states.
Tolerances
1e-6 mm (absolute) for inside-a-box tests, for the axis of a leg and for removing collinear points; 1e-9 for cost comparisons and for the minimum-leg test.
Limits
The grid has only the lines listed above, so a route is the best one on that grid. The minimum leg is capped in the search state at Math.round(minLeg): when the minimum leg is not a whole number and rounds down, the stored run never reaches it and no bend after the first is accepted. The default minimum leg is 2 × the outside diameter (at least 150), which is not a whole number for several pipe sizes.

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