Resources Numerical accuracy Calculation policy

USD export of the steel model

Writes each steel part as an extruded section in an OpenUSD file in meters, with its length and weight.

How the precision is kept

Section loops are placed in the world with toWorld from geom.js in 64-bit and divided by 1000. The mesh points are then stored as point3f[], the 32-bit point type of a USD mesh. Length and weight are written as 64-bit attributes.

Calculations under this policy

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

usd-detailcraft.js DetailCraft 0.8.1 64-bit 3 calculations Reviewed

Tests tools/usd/test-apps.mjs

A part's solid

64-bit

Builds the side faces and end caps of one steel part.

Vertices are computed in 64-bit and converted with Float32Array.from for the point3f[] attribute, the file format's point type. Loop direction is decided by the sign of the shoelace area and cap triangles by the sign of a 2D cross product, with no tolerance. Segments under 0.01 mm long are skipped.

  • usd-detailcraft.js
  • lines 58 to 109
  • DetailCraft 0.8.1
/** A part's solid: its section loops extruded from x = 0 to its length, split where copes start, in metres. */
function partMesh(p) {
  const pr = p.profile, L = p.length, f = p.frame, voff = p.voff || 0;
  if (!pr || !pr.loops || !pr.loops.length || !(L > 0)) return null;
  const copes = (p.cuts || []).filter((c) => c.type === undefined || c.type === 'cope' || c.length);
  const xs = [...new Set([0, L, ...copes.map((c) => (c.end === 'start' ? Math.min(c.length, L) : Math.max(L - c.length, 0)))])].sort((a, c) => a - c);
  const P = [], C = [], I = [];
  // a mitre's end (0.7.1 71.20): the end ring sits on its cut face
  const cutX = (x, u, v) => (p.endCuts?.start && x === 0 ? endX(p, 'start', u, v) : p.endCuts?.end && x === L ? endX(p, 'end', u, v) : x);
  const vert = (x, u, v) => { P.push(...M(toWorld(f, cutX(x, u, v), u, v, voff))); return P.length / 3 - 1; };
  for (let s = 1; s < xs.length; s++) {
    const a = xs[s - 1], z = xs[s];
    if (z - a < 0.01) continue;
    const mid = (a + z) / 2;
    let loops = pr.loops;
    for (const c of copes) {
      const inCope = c.end === 'start' ? mid < c.length : mid > L - c.length;
      if (!inCope) continue;
      if (c.side === 'top') loops = loops.map((l, k) => (k === 0 ? clipLoopV(l, pr.depth / 2 - c.depth, true) : null)).filter((l) => l && l.length);
      else loops = loops.map((l, k) => (k === 0 ? clipLoopV(l, -pr.depth / 2 + c.depth, false) : null)).filter((l) => l && l.length);
    }
    if (!loops.length || loops[0].length < 3) continue;
    // outer loop counter-clockwise in (u, v), holes clockwise: then (u, v, x) right-handed puts every side face outward
    const area = (l) => { let s2 = 0; for (let i = 0; i < l.length; i++) { const [u1, v1] = l[i], [u2, v2] = l[(i + 1) % l.length]; s2 += u1 * v2 - u2 * v1; } return s2 / 2; };
    const ordered = loops.map((l, k) => ((k === 0) === (area(l) > 0) ? l : l.slice().reverse()));
    for (const l of ordered) {
      for (let i = 0; i < l.length; i++) {
        const [u1, v1] = l[i], [u2, v2] = l[(i + 1) % l.length];
        const q = [vert(a, u1, v1), vert(a, u2, v2), vert(z, u2, v2), vert(z, u1, v1)];
        C.push(4); I.push(...q);
      }
    }
    // caps: one triangulation, used at both ends (the near end turned round)
    const flat = [], holes = [];
    for (const [k, l] of ordered.entries()) { if (k) holes.push(flat.length / 2); for (const [u, v] of l) flat.push(u, v); }
    const tris = earcut(flat, holes);
    const n = flat.length / 2;
    const at = (x) => { const base = P.length / 3; for (let i = 0; i < n; i++) vert(x, flat[i * 2], flat[i * 2 + 1]); return base; };
    const b0 = at(a), b1 = at(z);
    for (let t = 0; t < tris.length; t += 3) {
      // counter-clockwise in (u, v) faces +x (the far end); the near end gets the other turn
      let [i0, i1, i2] = [tris[t], tris[t + 1], tris[t + 2]];
      const cr = (flat[i1 * 2] - flat[i0 * 2]) * (flat[i2 * 2 + 1] - flat[i0 * 2 + 1]) - (flat[i1 * 2 + 1] - flat[i0 * 2 + 1]) * (flat[i2 * 2] - flat[i0 * 2]);
      if (cr < 0) [i1, i2] = [i2, i1];
      C.push(3, 3);
      I.push(b0 + i0, b0 + i2, b0 + i1);
      I.push(b1 + i0, b1 + i1, b1 + i2);
    }
  }
  if (!C.length) return null;
  return { points: Float32Array.from(P), counts: C, indices: I };
}

Millimeters to meters

64-bit

Converts a world point for the USD file.

(x, y, z) m = (x, y, z) mm ÷ 1000

Three 64-bit divisions by 1000; each result is the nearest 64-bit number to the quotient.

  • usd-detailcraft.js
  • line 20
  • DetailCraft 0.8.1
const M = (p) => [p[0] / 1000, p[1] / 1000, p[2] / 1000];

Length and weight attributes

64-bit

The length in meters and the weight in kilograms written on each part.

length m = length mm ÷ 1000

Both are written as double attributes: the length divided by 1000, and the weight in kilograms as the built model holds it.

  • usd-detailcraft.js
  • lines 45 to 46
  • DetailCraft 0.8.1
b.attr(pp, 'cadcraft:length', 'double', p.length / 1000, { custom: true, uniform: true, meta: unit('m') });
if (Number.isFinite(p.weight)) b.attr(pp, 'cadcraft:weight', 'double', p.weight, { custom: true, uniform: true, meta: unit('kg') });

In detail

Method
Each part's section loops are extruded between the stations where copes start; caps are triangulated with earcut from the bundled USD library.
Number types
64-bit numbers collected in plain arrays; Float32Array for the mesh points (point3f[]); 64-bit attributes for length and weight.
Limits
Points are 32-bit in the file, about 7 significant digits in meters. A segment shorter than 0.01 mm between cope stations is skipped. Bolt holes, bolts, welds and footings are not in the meshes, as the header says.

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