Resources Numerical accuracy Calculation policy

Hull hydrostatics and righting arms

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.js PartCraft 0.8.1 32-bit in part 5 calculations Reviewed

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.

value_m = round(value_mm ÷ 1000 × 10⁴) ÷ 10⁴; volume_m³ = round(V ÷ 1e9 × 10⁴) ÷ 10⁴

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.

  • hydrostatics.js
  • lines 182 to 189
  • PartCraft 0.8.1
const m = (v) => r4(v / 1000);
const out = {
  water: { name: a.water || 'sea', density_kg_m3: rho }, draft_m: m(draft),
  displacement_t: r4(h.displacement / 1000), volume_m3: r4(h.volume / 1e9),
  kb_m: m(h.kb), lcb_m: m(h.lcb), tcb_m: m(h.tcb),
  waterplane: { area_m2: r4(h.waterplane.area / 1e6), lcf_m: m(h.waterplane.lcf), length_m: m(h.lwl), beam_m: m(h.bwl) },
  wetted_surface_m2: r4(h.wetted / 1e6), bm_transverse_m: m(h.bmT), bm_longitudinal_m: m(h.bmL), km_transverse_m: m(h.kmT), km_longitudinal_m: m(h.kmL),
  cb: r4(h.cb), cw: r4(h.cw), tonnes_per_cm: r4(h.tpc * 10),

In detail

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

Every policy that takes this approach