Resources Numerical accuracy Calculation policy

Member frames and local coordinates

Turns a member's start point, end point and rotation into its own axes, and moves points between the model and a member's own coordinates.

How the precision is kept

Plain 64-bit numbers in arrays of three, millimeters, z up. The member direction is the difference of the two end points scaled by one over its length from Math.hypot; the section axes come from one projection and one cross product. A member counts as vertical when the z part of its unit direction is above 0.999999 (within about 0.08 degrees of vertical) and then takes fixed axes.

Calculations under this policy

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

geom.js DetailCraft 0.8.1 64-bit 5 calculations Reviewed

Tests bin/test-detailcraft.mjs

Frame of a member

64-bit

The origin, the unit direction, the two section axes and the length of a member line.

x = (e − s) ÷ |e − s|; v = unit(Z − x·x_z); u = v × x; u′ = cos a·u + sin a·v, v′ = −sin a·u + cos a·v

The direction is the difference multiplied by 1 ÷ L rather than divided by L, which can differ from a true division in the last bit. For a member just under the 0.999999 threshold, Z − x·x_z has a z part 1 − x_z² that subtracts nearly equal numbers before it is normalized. The rotation is degrees × Math.PI ÷ 180.

  • geom.js
  • lines 25 to 40
  • DetailCraft 0.8.1
/** Frame of a line from s to e with rotation (deg): { o, x, u, v, len }. */
export function frame(s, e, rotation = 0) {
  const d = sub(e, s), L = len(d);
  const x = L > 0 ? mul(d, 1 / L) : [1, 0, 0];
  let v;
  if (Math.abs(x[2]) > 0.999999) v = [1, 0, 0];
  else v = norm(sub([0, 0, 1], mul(x, x[2])));
  let u = cross(v, x);
  if (rotation) {
    const a = rotation * Math.PI / 180, c = Math.cos(a), sn = Math.sin(a);
    // rotate about +x: u' = c·u + s·v, v' = −s·u + c·v
    const u2 = add(mul(u, c), mul(v, sn)), v2 = add(mul(u, -sn), mul(v, c));
    u = u2; v = v2;
  }
  return { o: s, x, u, v, len: L };
}

Local point to model point

64-bit

The model coordinates of a point given along the member and across its section.

p = o + x·x̂ + u·û + (v + voff)·v̂

Three scaled axes added to the origin in a fixed order, all 64-bit. No tolerance and no rounding.

  • geom.js
  • lines 47 to 50
  • DetailCraft 0.8.1
/** World point of local (x along, u, v) in a frame with a v offset (section coordinates centered). */
export function toWorld(f, x, u, v, voff = 0) {
  return add(add(add(f.o, mul(f.x, x)), mul(f.u, u)), mul(f.v, v + voff));
}

Model point to local point

64-bit

Where a model point lies along a member and across its section.

d = p − o; x = d·x̂, u = d·û, v = d·v̂ − voff

The frame origin is subtracted first, then three dot products are taken, so the products work on distances from the member and not on the model's absolute coordinates.

  • geom.js
  • lines 51 to 55
  • DetailCraft 0.8.1
/** Local {x, u, v} (section-centered) of a world point. */
export function toLocal(f, p, voff = 0) {
  const d = sub(p, f.o);
  return { x: dot(d, f.x), u: dot(d, f.u), v: dot(d, f.v) - voff };
}

End face position on a plane-cut end

64-bit

How far along the part its end face is at a given point of the section, for square ends and mitre cuts.

x = x0 + ku·u + kv·v

One 64-bit expression. A square end returns 0 or the stored length unchanged.

  • geom.js
  • lines 57 to 65
  • DetailCraft 0.8.1
/**
 * Where a part's end face is, along the part (x from its cut start), at section point (u, v) (v from the section's center):
 * a square end is at 0 / length; a plane cut (a mitre, 0.7.1 71.20) is `part.endCuts[end]` = { x0, ku, kv }, x = x0 + ku·u + kv·v.
 */
export function endX(part, end, u = 0, v = 0) {
  const c = part.endCuts && part.endCuts[end];
  if (!c) return end === 'start' ? 0 : part.length;
  return c.x0 + c.ku * u + c.kv * v;
}

Section outline clipped at a height

64-bit

The part of a section outline below or above a line, used for coped ends.

t = (limit − a_v) ÷ (b_v − a_v); new point = (a_u + (b_u − a_u)·t, limit)

Sutherland–Hodgman on one line. The side test allows 1e-9 mm, absolute. The division only runs when the two points are on different sides, so their v values differ; the new point's v is set to the limit itself, not computed.

  • geom.js
  • lines 75 to 89
  • DetailCraft 0.8.1
/** Clips a section loop to v ≤ vmax (or v ≥ vmin with below = false). Sutherland–Hodgman on one line. */
export function clipLoopV(loop, limit, keepBelow = true) {
  const inside = (p) => keepBelow ? p[1] <= limit + 1e-9 : p[1] >= limit - 1e-9;
  const out = [];
  for (let i = 0; i < loop.length; i++) {
    const a = loop[i], b = loop[(i + 1) % loop.length];
    const ia = inside(a), ib = inside(b);
    if (ia) out.push(a);
    if (ia !== ib) {
      const t = (limit - a[1]) / (b[1] - a[1]);
      out.push([a[0] + (b[0] - a[0]) * t, limit]);
    }
  }
  return out.length >= 3 ? out : [];
}

In detail

Method
x = (end − start) ÷ length; v = global Z made square to x (global X for a vertical member); u = v × x; a rotation in degrees then turns u and v about x.
Number types
64-bit numbers in plain arrays.
Tolerances
Vertical test: |x_z| > 0.999999 on a unit vector. Loop clipping: 1e-9 mm, absolute, on the side test.
Limits
A line of zero length gets the direction [1, 0, 0]. Either side of the vertical threshold the rule for the section axes changes, so a member within about 0.08 degrees of vertical is oriented by the vertical rule.

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