Works out a floating part's displaced volume, displacement, centers of buoyancy and flotation, metacentric heights and the righting-arm curve at a given draft.
How the precision is kept
The integrals are 64-bit sums over the part's triangle mesh, clipped at the waterline triangle by triangle. The mesh comes from the kernel's result, whose vertex positions are a Float32Array (kernel.js); meshOf copies those 32-bit values into a Float64Array, which does not restore the lost digits. The answer returned by describe is also rounded to 4 decimals in meters, square meters, cubic meters and tonnes.
Open item. Feed the hull calculation from 64-bit vertex coordinates instead of the kernel's 32-bit mesh array, and return the results unrounded.
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.
hydrostatics.jsPartCraft 0.8.1 32-bit in part5 calculationsReviewed
Tests tools/partcraft-test/test-hydrostatics.mjs
Hull mesh taken from the built part
32-bit in part
Copies the part's triangle mesh for the hull calculation.
Float64Array.from copies regen.mesh.positions, which kernel.js fills as a Float32Array from the solid kernel's mesh. Every hull result below therefore starts from vertex coordinates with 32-bit precision.
hydrostatics.js
lines 19 to 23
PartCraft 0.8.1
export function meshOf(regen) {
const m = regen && regen.mesh;
if (!m || !m.positions || !m.indices) throw Object.assign(new Error('The part has no solid to float.'), { code: 'failed', next: ['Build the part first; fix any failing feature.'] });
return { pos: Float64Array.from(m.positions), idx: Uint32Array.from(m.indices) };
}
Volume, moments and waterplane under a waterline
32-bit in part
Clips the mesh at the waterline and sums the wet volume, its first moments, the wetted area and the waterplane's boundary integrals.
V = Σ a·(b × c) ÷ 6; ∫x dV = ½ ∮ x² nₓ dS; A = ½ Σ (pₓ q_z − qₓ p_z); ∫z² dA = Σ (p_z² + p_z q_z + q_z²)(pₓ q_z − qₓ p_z) ÷ 12
64-bit sums over all triangles, added in mesh order with no compensation; the tetrahedra are taken from the model origin, so a hull far from the origin sums large terms that cancel. Cut points are linear interpolations along the edge; the division is by a height difference that cannot be zero because one end is at or under the waterline and the other above. The vertex inputs are the 32-bit mesh values.
hydrostatics.js
lines 42 to 84
PartCraft 0.8.1
/** Everything under the plane y = h: volume, moments, wet skin and the waterplane's boundary integrals. */
function below(mesh, h) {
const { pos, idx } = mesh;
let V = 0, Mx = 0, My = 0, Mz = 0, S = 0;
let wA = 0, wCx = 0, wCz = 0, wIx = 0, wIz = 0, wMinX = Infinity, wMaxX = -Infinity, wMinZ = Infinity, wMaxZ = -Infinity, cuts = 0;
const tri = (a, b, c) => {
// a, b, c are [x, y, z] under the plane (y ≤ h)
const ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2], vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2];
const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx, area2 = Math.hypot(nx, ny, nz);
S += area2 / 2;
V += (a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0]) + a[2] * (b[0] * c[1] - b[1] * c[0])) / 6;
// ∫ x dV = ½ ∮ x² n_x dS, and the same for y and z; ∫_T q² dS = A (q0² + q1² + q2² + q0q1 + q0q2 + q1q2) / 6
const q = (i) => { const s = a[i] ** 2 + b[i] ** 2 + c[i] ** 2 + a[i] * b[i] + a[i] * c[i] + b[i] * c[i]; return s / 6; };
Mx += 0.5 * (nx / 2) * q(0); My += 0.5 * (ny / 2) * q(1); Mz += 0.5 * (nz / 2) * q(2); // n dS = (cross)/2 · … : the area vector is cross/2, so (n_x dS) = nx/2 over the triangle
};
const seg = (p, q) => { // an edge of the waterplane's boundary, in the order the clipped triangle gives it
const cr = p[0] * q[2] - q[0] * p[2];
wA += cr / 2; wCx += (p[0] + q[0]) * cr; wCz += (p[2] + q[2]) * cr;
wIx += (p[2] ** 2 + p[2] * q[2] + q[2] ** 2) * cr; wIz += (p[0] ** 2 + p[0] * q[0] + q[0] ** 2) * cr;
for (const r of [p, q]) { if (r[0] < wMinX) wMinX = r[0]; if (r[0] > wMaxX) wMaxX = r[0]; if (r[2] < wMinZ) wMinZ = r[2]; if (r[2] > wMaxZ) wMaxZ = r[2]; }
cuts++;
};
const P = (i) => [pos[3 * i], pos[3 * i + 1], pos[3 * i + 2]];
const lerp = (p, q) => { const t = (h - p[1]) / (q[1] - p[1]); return [p[0] + t * (q[0] - p[0]), h, p[2] + t * (q[2] - p[2])]; };
for (let t = 0; t < idx.length; t += 3) {
const v = [P(idx[t]), P(idx[t + 1]), P(idx[t + 2])];
const inside = v.map((p) => p[1] <= h);
const n = inside.filter(Boolean).length;
if (n === 3) tri(v[0], v[1], v[2]);
else if (n === 0) continue;
else if (n === 1) { // one corner under: a small triangle; the cut runs from where the edge leaves to where the edge before enters
const k = inside.indexOf(true), a = v[k], b = v[(k + 1) % 3], c = v[(k + 2) % 3];
const pb = lerp(a, b), pc = lerp(c, a);
tri(a, pb, pc); seg(pb, pc);
} else { // two corners under: a quad cut into two triangles
const k = inside.indexOf(false), a = v[k], b = v[(k + 1) % 3], c = v[(k + 2) % 3]; // a is above; b, c under (b follows a)
const pa = lerp(b, a), pc = lerp(a, c);
tri(b, c, pc); tri(b, pc, pa); seg(pc, pa);
}
}
// the waterplane (cap at y = h, normal +y): its volume term is h·A/3 and its moment terms ½ h² A for y only
return { V, Mx, My, Mz, S, wA, wCx, wCz, wIx, wIz, cuts, bounds: [wMinX, wMaxX, wMinZ, wMaxZ] };
}
Hydrostatic properties at a draft
32-bit in part
Volume, displacement, KB, LCB, waterplane area and centroid, BM, KM, block and waterplane coefficients, and mass per unit immersion.
V = V_skin + h × A ÷ 3; I_T = ∫z² dA − A × c_z²; BM = I ÷ V; KM = KB + BM; Δ = V × 1e-9 × ρ
The waterplane's second moments are moved to its own centroid by subtracting A × c², which cancels digits when the waterplane is far from the origin. If no triangle is cut (the draft is above the whole hull) the waterplane area is 0 and the centroid is 0 ÷ 0; that case is not guarded, so the waterplane and BM values become NaN. Inputs are the 32-bit mesh values.
hydrostatics.js
lines 86 to 111
PartCraft 0.8.1
/** Properties of the hull floating with the keel's lowest point `draft` mm below the waterline. mesh: meshOf() (Y up, X along, Z across). */
export function hullAt(meshIn, draft, opts = {}) {
const mesh = aligned(meshIn, opts.axes), rho = opts.rho ?? 1025, keel = keelOf(mesh), h = keel + draft;
if (!(draft > 0)) return null;
const r = below(mesh, h);
const A = Math.abs(r.wA);
if (!(r.V > 0) && !(A > 0)) return null;
const V = r.V + h * A / 3;
if (!(V > 0)) return null;
const sgn = r.wA < 0 ? -1 : 1;
// the boundary integrals: signed by the order of the cut edges; fold the sign out
const aw = r.wA * sgn, cx = (r.wCx * sgn) / (6 * aw) , cz = (r.wCz * sgn) / (6 * aw);
const Ixx = (r.wIx * sgn) / 12, Izz = (r.wIz * sgn) / 12; // ∫ z² dA and ∫ x² dA about the origin
const iT = Ixx - aw * cz * cz, iL = Izz - aw * cx * cx;
const mx = r.Mx, my = r.My + 0.5 * h * h * A, mz = r.Mz;
const lcb = mx / V, vcb = my / V, tcb = mz / V, kb = vcb - keel;
const lwl = r.bounds[1] - r.bounds[0], bwl = r.bounds[3] - r.bounds[2];
const out = {
draft, volume: V, displacement: V * 1e-9 * rho, kb, lcb, tcb, wetted: r.S,
waterplane: { area: aw, lcf: cx, tcf: cz, iT, iL },
bmT: iT / V, bmL: iL / V, kmT: kb + iT / V, kmL: kb + iL / V,
cb: lwl > 0 && bwl > 0 ? V / (lwl * bwl * draft) : null, cw: lwl > 0 && bwl > 0 ? aw / (lwl * bwl) : null,
tpc: rho * 1e-9 * aw, lwl, bwl,
};
return out;
}
Righting arms at fixed displacement
32-bit in part
The righting arm GZ at each heel angle, the metacentric height GM, the largest GZ and the angle where stability vanishes.
GZ = z(B) − (keel + KG) × sin θ; GM = KM_T − KG; vanishing angle by linear interpolation between samples
For each angle the mesh is rotated in 64-bit numbers and the waterline is found by bisection on the volume, up to 80 steps to 1e-9 of the hull height; a row is rejected if the volume is off by more than 0.1 %. The maximum GZ is the largest sampled value, not a fitted peak. Inputs are the 32-bit mesh values.
hydrostatics.js
lines 122 to 158
PartCraft 0.8.1
/**
* Righting arms at a fixed displacement. kg = the centre of gravity's height above the keel (mm); the heel angles in degrees. Each heeled hull is
* lifted until it displaces what the upright one does (bisection on the waterline), then GZ = z(B) − z(G) with both turned together.
*/
export function rightingArms(meshIn, { draft = null, volume = null, kg, angles = [0, 5, 10, 15, 20, 30, 40, 50, 60], opts = {} } = {}) {
const mesh = aligned(meshIn, opts.axes), keel = keelOf(mesh);
if (!(kg > 0) && kg !== 0) throw Object.assign(new Error('Give the height of the centre of gravity above the keel (kg).'), { code: 'invalid_argument', next: ['Give kg in millimetres.'] });
const up = draft !== null ? hullAt(mesh, draft, { ...opts, axes: undefined }) : null;
const V0 = volume ?? (up && up.volume);
if (!(V0 > 0)) throw Object.assign(new Error('Nothing is submerged at that draft.'), { code: 'invalid_argument', next: ['Give a draft between the keel and the deck.'] });
const rows = [];
// the upright hull for the metacentric height
const base = draft !== null ? up : null;
for (const ang of angles) {
const M = ang === 0 ? mesh : heeled(mesh, ang);
// the waterline height h in the heeled frame that gives V0: V(h) rises with h
let lo = Infinity, hi = -Infinity;
for (let i = 1; i < M.pos.length; i += 3) { if (M.pos[i] < lo) lo = M.pos[i]; if (M.pos[i] > hi) hi = M.pos[i]; }
const vol = (h) => { const r = below(M, h); return r.V + h * Math.abs(r.wA) / 3; };
let a = lo, b = hi;
for (let it = 0; it < 80 && b - a > 1e-9 * Math.max(1, hi - lo); it++) { const mid = (a + b) / 2; if (vol(mid) < V0) a = mid; else b = mid; }
const h = (a + b) / 2, r = below(M, h), Vh = r.V + h * Math.abs(r.wA) / 3;
if (Math.abs(Vh / V0 - 1) > 1e-3) { rows.push({ heel: ang, draft: null, gz: null, note: 'The hull cannot hold that displacement at this heel (it is submerged or out of the water).' }); continue; }
const A = Math.abs(r.wA);
const zB = r.Mz / Vh, yB = (r.My + 0.5 * h * h * A) / Vh;
// G turns with the hull about the same axis: G at (y = kg + keel, z = 0) before heeling
const zG = (keel + kg) * Math.sin(ang * RAD); // z' = y sin + z cos with z = 0
const gz = zB - zG;
rows.push({ heel: ang, draft: h - lo, volume: Vh, gz, kb: yB - lo, zb: zB });
}
const real = rows.filter((x) => x.gz !== null && x.gz !== undefined);
const best = real.reduce((m, x) => (!m || x.gz > m.gz ? x : m), null);
const gm = base ? base.kmT - kg : null;
let vanishing = null;
for (let i = 1; i < real.length; i++) if (real[i - 1].gz > 0 && real[i].gz <= 0) { const p = real[i - 1], q = real[i]; vanishing = p.heel + (q.heel - p.heel) * p.gz / (p.gz - q.gz); break; }
return { gm, kg, volume: V0, rows, maxGz: best ? best.gz : null, at: best ? best.heel : null, vanishing };
}
Units and rounding of the answer
32-bit in part
Converts the hull results to meters, square meters, cubic meters and tonnes for the tool's answer.
Every returned number is rounded to 4 decimals (r4): 0.1 mm for lengths, 1e-4 m³ for volume and 1e-4 t for displacement, so a small hull keeps few significant digits. The unit factors 25.4 and 304.8 are exact. The field tonnes_per_cm is tpc × 10, where tpc is kilograms per millimeter of immersion; that product is kilograms per centimeter, 1000 times the value in tonnes per centimeter.
The file header cites Biran, Ship Hydrostatics and Stability, and Tupper. Volume and first moments by the divergence theorem over the wet skin plus the waterplane cap; waterplane area, centroid and second moments from its boundary segments (Green's theorem); BM = I ÷ V; KM = KB + BM; righting arm GZ = z(B) − z(G) after rotating the mesh about its length axis and finding the waterline of equal volume by bisection.
Number types
64-bit numbers and Float64Array for the arithmetic; the input vertex positions are 32-bit values read from the kernel mesh; Uint32Array indexes.
Tolerances
A vertex is under water when y ≤ h, with no tolerance. Equal-volume bisection: up to 80 steps, stops when the bracket is under 1e-9 × max(1, hull height). A heel angle is reported as not holding the displacement when the volume found differs by more than 1e-3 (relative).
Limits
Results are for the faceted mesh, not the true surface. Water densities are the nominal 1025 and 1000 kg/m³. The largest righting arm and the vanishing angle are taken from the sampled heel angles (every 2.5° or 5°), the latter by a straight line between two samples. No trim, free surface or appendages, as the function's own notes say.
Approach: 32-bit step, flagged
One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.