Resources Numerical accuracy Calculation policy

Node graph functions

Works out the value of each node in a node graph or script: arithmetic, lists, points and vectors, transforms and arrays of geometry.

How the precision is kept

Each node is one 64-bit expression of its inputs; results that are not finite are refused. Angles are degrees at the ports and are multiplied by π ÷ 180 before any trigonometry. Points are Float64Arrays, and copies of an array are placed from the original by k times the step.

Calculations under this policy

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

nodes/library.js ModelCraft 0.8.1 64-bit 12 calculations Reviewed

Tests None found by the review

Rotation matrix

64-bit

The matrix that turns geometry by an angle about an axis through a point.

R = c·I + s·[axis]ₓ + (1 − c)·axis·axisᵀ, c = cos(angle·π ÷ 180), s = sin(angle·π ÷ 180); t = p − R·p

The axis is divided by its Math.hypot length, which must exceed 1e-12. The translation p − R·p subtracts two nearly equal vectors when the angle is small and the pivot far from the origin.

  • nodes/library.js
  • lines 31 to 38
  • ModelCraft 0.8.1
function mRotate(angleDeg, p, axis) {
  const l = Math.hypot(axis[0], axis[1], axis[2]);
  need(l > 1e-12, 'nodes.err.axis');
  const [x, y, z] = [axis[0] / l, axis[1] / l, axis[2] / l], c = Math.cos(angleDeg * RAD), s = Math.sin(angleDeg * RAD), k = 1 - c;
  const R = [c + x * x * k, x * y * k - z * s, x * z * k + y * s, y * x * k + z * s, c + y * y * k, y * z * k - x * s, z * x * k - y * s, z * y * k + x * s, c + z * z * k];
  const t = [p[0] - (R[0] * p[0] + R[1] * p[1] + R[2] * p[2]), p[1] - (R[3] * p[0] + R[4] * p[1] + R[5] * p[2]), p[2] - (R[6] * p[0] + R[7] * p[1] + R[8] * p[2])];
  return [R[0], R[1], R[2], t[0], R[3], R[4], R[5], t[1], R[6], R[7], R[8], t[2], 0, 0, 0, 1];
}

Scale matrix

64-bit

The matrix that scales geometry by a factor about a center.

p′ = f·p + c·(1 − f)

Three products c·(1 − f) form the translation; applied to the center itself the result f·c + c·(1 − f) can differ from c in the last bit.

  • nodes/library.js
  • line 39
  • ModelCraft 0.8.1
function mScale(f, c) { return [f, 0, 0, c[0] * (1 - f), 0, f, 0, c[1] * (1 - f), 0, 0, f, c[2] * (1 - f), 0, 0, 0, 1]; }

Applying a matrix to geometry

64-bit

Moves a point, a curve, a surface, a mesh or a list of them by a 4 × 4 matrix.

Points and mesh positions go through xform into new Float64Arrays; curves and surfaces have their control points transformed and keep their weights and knots.

  • nodes/library.js
  • lines 41 to 53
  • ModelCraft 0.8.1
/** Apply a matrix to any geometry (a list of geometry maps over it). */
export function applyMatrix(g, m) {
  if (Array.isArray(g)) return g.map((x) => applyMatrix(x, m));
  if (isVec(g)) return xp(m, g);
  if (isCurve(g)) return N.curve.transform(g, m);
  if (isSurface(g)) return N.surface.transform(g, m);
  if (isMesh(g)) {
    const p = new Float64Array(g.positions.length);
    for (let k = 0; k < p.length; k += 3) p.set(V.xform(m, [g.positions[k], g.positions[k + 1], g.positions[k + 2]]), k);
    return { positions: p, indices: g.indices.slice() };
  }
  return fail('nodes.err.not_geometry');
}

Divide, power and remainder

64-bit

The Divide, Power, Smaller, Larger and Remainder nodes.

a ÷ b; aᵇ; remainder = a − b·floor(a ÷ b)

Divide and Remainder refuse a divisor of size 1e-300 or less. The remainder takes the sign of b and is formed by subtraction, so when a is many times larger than b it has fewer correct digits than a itself.

  • nodes/library.js
  • lines 73 to 78
  • ModelCraft 0.8.1
one('divide', 'math', 'Divide', [i('a', 'number', 1, { unit: '*' }), i('b', 'number', 1, { unit: '*' })], o('out', 'number'),
  (a) => { need(Math.abs(a.b) > 1e-300, 'nodes.err.divide_zero'); return a.a / a.b; }, 'div');
bin('power', 'Power', (x, y) => Math.pow(x, y), 'mul');
bin('minimum', 'Smaller', Math.min);
bin('maximum', 'Larger', Math.max);
one('modulo', 'math', 'Remainder', [i('a', 'number', 7, { unit: '*' }), i('b', 'number', 3, { unit: '*' })], o('out', 'number'), (a) => { need(Math.abs(a.b) > 1e-300, 'nodes.err.divide_zero'); return a.a - a.b * Math.floor(a.a / a.b); }, '=');

Sine, cosine, tangent and angle

64-bit

Trigonometry with angles in degrees.

sin(x·π ÷ 180), cos(x·π ÷ 180), tan(x·π ÷ 180); angle = atan2(y, x) ÷ (π ÷ 180)

The degrees are multiplied by the 64-bit value of π ÷ 180 first, so sin(180) returns about 1.2e-16 and not 0, and tan(90) returns a large finite number.

  • nodes/library.js
  • lines 85 to 88
  • ModelCraft 0.8.1
un('sin', 'Sine', (x) => Math.sin(x * RAD), 'in-angle');
un('cos', 'Cosine', (x) => Math.cos(x * RAD), 'in-angle');
un('tan', 'Tangent', (x) => Math.tan(x * RAD), 'in-angle');
one('atan2', 'math', 'Angle of (x, y)', [i('y', 'number', 0, { unit: '*' }), i('x', 'number', 1, { unit: '*' })], o('out', 'number', A), (a) => Math.atan2(a.y, a.x) / RAD, A);

Series and range

64-bit

A list of evenly stepped numbers, by step or between two ends.

series: start + k·step; range: start + (end − start)·k ÷ (n − 1)

Each item is computed from its index, not by adding the step repeatedly. The last item of a range is start + (end − start)·1, which can differ from end in the last bit.

  • nodes/library.js
  • lines 96 to 99
  • ModelCraft 0.8.1
one('series', 'list', 'Series', [i('start', 'number', 0, { unit: '*' }), i('step', 'number', 1, { unit: '*' }), i('count', 'number', 5, { step: 1, min: 0 })], o('out', 'number', '', { list: true }),
  (a) => Array.from({ length: count(a.count) }, (_, k) => a.start + k * a.step), '=');
one('range', 'list', 'Range', [i('start', 'number', 0, { unit: '*' }), i('end', 'number', 1, { unit: '*' }), i('count', 'number', 5, { step: 1, min: 2 })], o('out', 'number', '', { list: true }),
  (a) => { const n = count(a.count); need(n >= 2, 'nodes.err.count_min', { min: 2 }); return Array.from({ length: n }, (_, k) => a.start + (a.end - a.start) * k / (n - 1)); }, '=');

Sum of a list

64-bit

Adds the numbers of a list.

Σ xᵢ

The numbers are added left to right in list order with no compensation, so a long list of mixed sizes accumulates rounding in that order.

  • nodes/library.js
  • line 112
  • ModelCraft 0.8.1
one('sum', 'list', 'Sum', [i('numbers', 'number', null, { list: true, unit: '*' })], o('out', 'number'), (a) => a.numbers.reduce((s, x) => s + x, 0), '=');

Vector length and distance

64-bit

The length of a vector and the distance between two points.

√(Δx² + Δy² + Δz²)

Math.hypot of the three differences.

  • nodes/library.js
  • lines 121 to 122
  • ModelCraft 0.8.1
one('vector_length', 'vector', 'Vector length', [i('vector', 'vector', vec(0, 0, 1), { unit: L })], o('out', 'number', L), (a) => Math.hypot(...a.vector), L);
one('distance', 'vector', 'Distance', [P('a'), P('b', vec(10, 0, 0))], o('out', 'number', L), (a) => Math.hypot(a.a[0] - a.b[0], a.a[1] - a.b[1], a.a[2] - a.b[2]), L);

Point on a circle

64-bit

A point at a radius and an angle from a center, in the plane z = center z.

[c_x + r·cos(a·π ÷ 180), c_y + r·sin(a·π ÷ 180), c_z]

One product and one addition per coordinate after the angle is converted from degrees.

  • nodes/library.js
  • lines 126 to 127
  • ModelCraft 0.8.1
one('polar', 'vector', 'Point on a circle', [P('center'), i('radius', 'number', 10, { unit: L }), i('angle', 'number', 0, { unit: A })], o('out', 'point', L),
  (a) => vec(a.center[0] + a.radius * Math.cos(a.angle * RAD), a.center[1] + a.radius * Math.sin(a.angle * RAD), a.center[2]), L);

Point on curve and divide curve

64-bit

A point at a fraction of a curve, and the points that divide a curve into a number of parts.

u = u₀ + (u₁ − u₀)·t; divide: u = u₀ + (u₁ − u₀)·k ÷ n

The fraction is of the curve's parameter range, so the parts are equal in parameter and are equal in length only where the curve's speed is constant.

  • nodes/library.js
  • lines 139 to 143
  • ModelCraft 0.8.1
one('curve_point', 'curve', 'Point on curve', [Cv('curve'), i('t', 'number', 0.5, { min: 0, max: 1 })], o('out', 'point', L),
  (a) => { const [u0, u1] = N.curve.domain(a.curve); return Float64Array.from(N.curve.point(a.curve, u0 + (u1 - u0) * Math.min(1, Math.max(0, a.t)))); }, L);
one('curve_length', 'curve', 'Curve length', [Cv('curve')], o('out', 'number', L), (a) => N.curve.arcLength(a.curve), L);
one('divide_curve', 'curve', 'Divide curve', [Cv('curve'), i('count', 'number', 8, { step: 1, min: 1 })], o('out', 'point', L, { list: true }),
  (a) => { const n = count(a.count); need(n >= 1, 'nodes.err.count_min', { min: 1 }); const [u0, u1] = N.curve.domain(a.curve); return Array.from({ length: n + 1 }, (_, k) => Float64Array.from(N.curve.point(a.curve, u0 + (u1 - u0) * k / n))); }, L);

Array around an axis

64-bit

Copies of geometry turned round an axis in equal steps of a full circle.

copy k: rotate by 360·k ÷ n degrees

Each copy gets its own rotation matrix from the original at 360·k ÷ n degrees, so errors do not add up from copy to copy.

  • nodes/library.js
  • lines 153 to 154
  • ModelCraft 0.8.1
one('array_polar', 'transform', 'Array around an axis', [i('geometry', 'geometry', null), P('about'), i('axis', 'vector', vec(0, 0, 1), { unit: '*' }), i('count', 'number', 6, { step: 1, min: 1 })], o('out', 'geometry', L, { list: true }),
  (a) => { const n = count(a.count); return Array.from({ length: n }, (_, k) => applyMatrix(a.geometry, mRotate(360 * k / n, a.about, a.axis))); }, L);

Surface to mesh

64-bit

A triangle mesh of a surface within a tolerance.

The tolerance (default 0.1, which must be above 0) is handed to surface.tessellate; positions are copied into a Float64Array. How closely the mesh follows the surface is set by that tolerance.

  • nodes/library.js
  • lines 170 to 171
  • ModelCraft 0.8.1
one('to_mesh', 'surface', 'Surface to mesh', [S('surface'), i('tolerance', 'number', 0.1, { unit: L, min: 0.001 })], o('out', 'mesh', L),
  (a) => { need(a.tolerance > 0, 'nodes.err.tolerance'); const t = N.surface.tessellate(a.surface, { tol: a.tolerance }); return { positions: Float64Array.from(t.positions), indices: Uint32Array.from(t.indices) }; }, L);

In detail

Method
Textbook formulas, as the file's header says; rotation by Rodrigues' formula as a 4 × 4 row-major matrix; curve and surface nodes call the NURBS library.
Number types
64-bit numbers; Float64Array points and mesh positions; Uint32Array mesh indices.
Tolerances
Divide and remainder refuse |b| ≤ 1e-300. An axis or a vector to normalize must be longer than 1e-12; a line needs ends more than 1e-9 apart; an extrude direction must be longer than 1e-9; a scale factor must exceed 1e-12 in size and a revolve angle 1e-9 degrees. All absolute. Lists hold at most 100 000 items.
Limits
Sine, cosine and tangent of whole degrees such as 90 or 180 are within rounding of 0 or ±1 but not exactly those, because the angle passes through π ÷ 180. Divide curve and Point on curve step evenly in the curve's parameter, not in length.

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