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