Makes simple meshes in a script and gives scripts the app's own element matrices, voxel solver, stresses and lattice Boltzmann fields.
How the precision is kept
Every result these builtins hand to a script is 64-bit. The element matrices and solvers are the ones in fea.js, tetfea.js, thermal.js and flow.js, scaled here by 64-bit factors. Round shapes are faceted, and the voxel solve stops at a residual tolerance.
Calculations under this policy
10 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.
Each vertex angle is computed from the index directly (2π × i ÷ n), not by adding a step, so rounding does not build up around the circle. The surface is an n-sided prism inscribed in the cylinder.
cs-lib-mesh.js
lines 63 to 73
PartCraft 0.8.1
export function cylinderSurface(r, h, n = 32) {
const P = [0, 0, 0, 0, 0, h], I = [], tf = [];
for (let i = 0; i < n; i++) { const a = 2 * Math.PI * i / n; P.push(r * Math.cos(a), r * Math.sin(a), 0, r * Math.cos(a), r * Math.sin(a), h); }
const b = (i) => 2 + 2 * (i % n), t = (i) => 3 + 2 * (i % n);
for (let i = 0; i < n; i++) {
I.push(0, b(i + 1), b(i)); tf.push(0);
I.push(1, t(i), t(i + 1)); tf.push(1);
I.push(b(i), b(i + 1), t(i + 1), b(i), t(i + 1), t(i)); tf.push(2, 2);
}
return meshValue('tri', Float64Array.from(P), Int32Array.from(I), { triFace: Int32Array.from(tf) });
}
Sphere surface
64-bit
A triangle surface of a sphere from rings and segments.
Vertices are r times products of Math.cos and Math.sin of angles computed from the ring and segment index. The surface is a polyhedron inscribed in the sphere.
cs-lib-mesh.js
lines 74 to 83
PartCraft 0.8.1
export function sphereSurface(r, n = 16) {
const P = [0, 0, -r], I = [], rings = n, seg = 2 * n;
for (let k = 1; k < rings; k++) { const th = Math.PI * k / rings - Math.PI / 2; for (let i = 0; i < seg; i++) { const a = 2 * Math.PI * i / seg; P.push(r * Math.cos(th) * Math.cos(a), r * Math.cos(th) * Math.sin(a), r * Math.sin(th)); } }
P.push(0, 0, r);
const v = (k, i) => 1 + (k - 1) * seg + (i % seg), top = P.length / 3 - 1;
for (let i = 0; i < seg; i++) I.push(0, v(1, i + 1), v(1, i));
for (let k = 1; k < rings - 1; k++) for (let i = 0; i < seg; i++) I.push(v(k, i), v(k, i + 1), v(k + 1, i + 1), v(k, i), v(k + 1, i + 1), v(k + 1, i));
for (let i = 0; i < seg; i++) I.push(v(rings - 1, i), v(rings - 1, i + 1), top);
return meshValue('tri', Float64Array.from(P), Int32Array.from(I));
}
Structured hexahedron grid
64-bit
Node coordinates and elements of a box divided into equal cells.
x = o + l × i ÷ n
Each coordinate is the length times the index divided by the count, so the last node lands on o + l without accumulated step error.
cs-lib-mesh.js
lines 84 to 90
PartCraft 0.8.1
export function hexGrid(lx, ly, lz, nx, ny, nz, o = [0, 0, 0]) {
const N = (nx + 1) * (ny + 1) * (nz + 1), X = new Float64Array(3 * N);
for (let k = 0, n = 0; k <= nz; k++) for (let j = 0; j <= ny; j++) for (let i = 0; i <= nx; i++, n++) { X[3 * n] = o[0] + lx * i / nx; X[3 * n + 1] = o[1] + ly * j / ny; X[3 * n + 2] = o[2] + lz * k / nz; }
const E = new Int32Array(8 * nx * ny * nz), id = (i, j, k) => i + (nx + 1) * (j + (ny + 1) * k);
for (let k = 0, e = 0; k < nz; k++) for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++, e++) E.set([id(i, j, k), id(i + 1, j, k), id(i + 1, j + 1, k), id(i, j + 1, k), id(i, j, k + 1), id(i + 1, j, k + 1), id(i + 1, j + 1, k + 1), id(i, j + 1, k + 1)], 8 * e);
return meshValue('hex8', X, E, { dims: [nx, ny, nz] });
}
Voxel mesh nodes
64-bit
Node coordinates of a voxel grid as a script mesh.
x = origin + i × h
Coordinates are the grid origin plus the index times the cell size, in a Float64Array. Node indexes are split with Math.floor of exact whole-number divisions.
cs-lib-mesh.js
lines 99 to 108
PartCraft 0.8.1
// ─────────────────────────────────────────────────────────────── voxelize, tet mesh, surfaces, quality
export function voxelMeshOf(grid) {
const ids = []; for (let e = 0; e < grid.solid.length; e++) if (grid.solid[e] === 1) ids.push(e);
const off = VX.cornerOffsets(grid), E = new Int32Array(8 * ids.length);
ids.forEach((e, m) => { const n0 = VX.elemNode0(grid, e); for (let c = 0; c < 8; c++) E[8 * m + c] = n0 + off[c]; });
const NX = grid.nx + 1, NY = grid.ny + 1, N = NX * NY * (grid.nz + 1), X = new Float64Array(3 * N);
for (let n = 0; n < N; n++) { const i = n % NX, j = Math.floor(n / NX) % NY, k = Math.floor(n / (NX * NY)); X[3 * n] = grid.origin[0] + i * grid.h; X[3 * n + 1] = grid.origin[1] + j * grid.h; X[3 * n + 2] = grid.origin[2] + k * grid.h; }
const g = { h: grid.h, nx: grid.nx, ny: grid.ny, nz: grid.nz, origin: grid.origin.slice(), solid: grid.solid, count: ids.length, oddRows: grid.oddRows || 0 };
return meshValue('voxel', X, E, { grid: g, elemIds: Int32Array.from(ids) });
}
Hexahedron stiffness for a script
64-bit
The 24 × 24 stiffness of a cube of side h.
K = E × h × K_unit
The unit-cube matrix from fea.js is multiplied entry by entry by E × h in 64-bit.
cs-lib-mesh.js
lines 154 to 159
PartCraft 0.8.1
/** h8/h8i stiffness of a cube of side h → 24×24 (= E·h·Ke of the unit cube). */
export function hexKe(E, nu, h, type = 'h8i') {
checkEnu(E, nu);
const el = kHexElement(nu, hexType(type)), s = E * h;
return mat(24, 24, el.Ke.map((v) => v * s)); // symmetric: row-major = column-major
}
Hexahedron stress operator for a script
64-bit
The matrix that turns a cube's nodal displacements into stresses at its corners and center.
σ = (E ÷ h) × S u
The unit operator from fea.js is scaled by E ÷ h and reordered to column-major in a Float64Array.
cs-lib-mesh.js
lines 160 to 166
PartCraft 0.8.1
/** Stress operator of a cube: rows (point·6 + component) for the 8 corners then the centre, σ = op · ue → 54×24. */
export function hexStressOp(E, nu, h, type = 'h8i') {
checkEnu(E, nu);
const S = kHexElement(nu, hexType(type)).S, s = E / h, out = new Float64Array(54 * 24);
for (let r = 0; r < 54; r++) for (let j = 0; j < 24; j++) out[r + j * 54] = S[r * 24 + j] * s;
return mat(54, 24, out);
}
Mean stress of quadratic tetrahedra
64-bit
Each element's stress as the average of its four Gauss points.
σ_e = ¼ Σ σ_gauss
Each Gauss point's stress from tetfea.js is divided by 4 and added, in 64-bit. This is an element mean, not a peak value.
cs-lib-mesh.js
lines 192 to 202
PartCraft 0.8.1
/** Element-mean stress (average of the 4 Gauss points) → ne×6 [xx yy zz xy yz zx]. */
export function t10Stress(m, u, E, nu) {
m = t10Mesh(m); checkEnu(E, nu);
const ne = m.elems.length / 10, D = elasticD(nu), el = new Int32Array(10), ue = new Float64Array(30), sig = new Float64Array(6), eps = new Float64Array(6), out = new Float64Array(6 * ne);
if (u.length !== m.nodes.length) throw err(t('script.mesh.t10_u', { n: m.nodes.length }));
for (let e = 0; e < ne; e++) {
for (let a = 0; a < 10; a++) { el[a] = m.elems[10 * e + a]; for (let i = 0; i < 3; i++) ue[3 * a + i] = u[3 * el[a] + i]; }
for (let p = 0; p < 4; p++) { kGpStress(m.nodes, el, p, ue, E, D, sig, eps); for (let c = 0; c < 6; c++) out[e + c * ne] += sig[c] / 4; }
}
return mat(ne, 6, out);
}
Voxel element stress for a script
64-bit
Stress components and von Mises stress at the center of each voxel element.
σ = (E ÷ h) × S_center u_e
A 24-term 64-bit sum per component, written to Float64Array results. Unlike the app's own result arrays, nothing here is stored in 32-bit.
cs-lib-mesh.js
lines 243 to 253
PartCraft 0.8.1
/** Element-centre stress of a voxel mesh → ne×6, and von Mises ne×1. */
export function voxelStressOf(m, u, E, nu, type = 'h8i') {
m = voxelArg(m); checkEnu(E, nu);
const S = kHexElement(nu, hexType(type)).S, s = E / m.grid.h, ne = m.elems.length / 8, out = new Float64Array(6 * ne), vm = new Float64Array(ne), v = new Float64Array(6);
if (u.length !== m.nodes.length) throw err(t('script.mesh.voxel_u', { n: m.nodes.length }));
for (let e = 0; e < ne; e++) {
for (let r = 0; r < 6; r++) { let q = 0; for (let a = 0; a < 8; a++) { const n = m.elems[8 * e + a]; for (let i = 0; i < 3; i++) q += S[8 * 144 + r * 24 + 3 * a + i] * u[3 * n + i]; } v[r] = q * s; out[e + r * ne] = v[r]; }
vm[e] = kVonMises(v);
}
return [mat(ne, 6, out), mat(ne, 1, vm)];
}
Voxel solve for a script
64-bit
Solves a voxel model for displacements with the multigrid conjugate gradient solver.
Passes the tolerance (1e-8 by default) to pcg in fea.js and returns its Float64Array solution with the residual and a converged flag. The builtin raises an error on no convergence unless the script asks for the second output.
Copies the solver's Float64Array state into Float64Array tensors, in lattice units. Cells that are not fluid hold NaN.
cs-lib-mesh.js
lines 294 to 300
PartCraft 0.8.1
export function lbmFieldOf(Sv) {
const { st } = lbmOf(Sv), [nx, ny, nz] = st.dims, N = nx * ny * nz, { cells } = st.S.lat;
const mk = () => new Float64Array(N).fill(NaN), ux = mk(), uy = mk(), uz = mk(), rho = mk();
for (let k = 0; k < cells.length; k++) { const c = cells[k]; ux[c] = st.S.ux[k]; uy[c] = st.S.uy[k]; uz[c] = st.S.uz[k]; rho[c] = st.S.rho[k]; }
const shape = nz === 1 ? [nx, ny] : [nx, ny, nz];
return struct({ ux: tensor(shape, ux), uy: tensor(shape, uy), uz: tensor(shape, uz), rho: tensor(shape, rho), step: st.S.step });
}
In detail
Method
Wrappers that scale and reorder the kernels' results; primitives from Math.cos and Math.sin at equal angle steps.
Number types
Float64Array for nodes, matrices and results; Int32Array for element node numbers.
Tolerances
voxelSolve: residual 1e-8 of the load by default. lbmRun: relative velocity change 1e-7 per step by default.
Limits
A cylinder or sphere surface is a polyhedron with its vertices on the true surface. Material checks: E > 0 and −1 < ν < 0.5.
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.