Simulation workspace: plot values and the hover probe
Prepares the values a result plot colors by and reads the result under the pointer.
How the precision is kept
Solving happens in worker files; this file prepares plot values and the probe. The mode-shape magnitude is stored in a Float32Array and the probe's weights are computed from the drawn Float32Array positions, so the probed numbers carry 32-bit accuracy. Result tables print the solver's own numbers through sim-ui.js sig (4 significant figures).
Open item. Compute the probe's weights from the solver's own plot-mesh positions and keep the mode-shape magnitude in a Float64Array.
Calculations under this policy
8 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.
ws-simulation.jsPartCraft 0.8.1 32-bit in part8 calculationsReviewed
Tests tools/partcraft-test/browser-simulation.mjs
Element size of a live analysis level
64-bit
The element size the coarse and refined live solves use.
h = base size × level factor
The base is the study's own size, or the automatic size worked out from the part's volume by sim.js; one 64-bit multiplication by the level's factor.
ws-simulation.js
lines 88 to 93
PartCraft 0.8.1
/** The element size a level uses: the study's own (or the automatic one), times the level's factor. Adaptive refinement is for the refined level only. */
export function liveStudy(st, regen, factor) {
const tet = mesherOf(st) === 'tet', vol = regen.stats.volume;
const base = st.h || (tet ? tetAutoSize(vol) : autoSize(vol, AUTO_ELEMENTS.simulation));
return { ...st, h: base * factor, ...(factor > 1 ? { adaptive: null } : {}) };
}
Mode shape magnitude
32-bit in part
The size of the mode shape's displacement at each plot vertex.
|u| = √(u_x² + u_y² + u_z²)
Math.hypot of the three components, stored in a Float32Array. The probe interpolates from this array, so the relative amplitude it states has 32-bit accuracy.
ws-simulation.js
lines 197 to 198
PartCraft 0.8.1
const u = r.shapes[S.mode], nv = u.length / 3, mag = new Float32Array(nv);
for (let v = 0; v < nv; v++) mag[v] = Math.hypot(u[3 * v], u[3 * v + 1], u[3 * v + 2]);
Safety factor plot values
32-bit, display only
The safety factor at each plot vertex, for coloring.
safety = min(yield ÷ stress, 100)
Stored in a Float32Array that only colors the plot, on a fixed legend from 0 to 5. The probe computes its own safety factor from the interpolated stress.
ws-simulation.js
line 208
PartCraft 0.8.1
const y = r.material.yield, sf = Float32Array.from(v.vm, (s) => (s > 0 ? Math.min(y / s, 100) : 100));
Legend range
64-bit
The least and largest value of a plotted result, for the legend.
One pass of comparisons; the values are returned unchanged. When all values are equal the top is set to the bottom + 1 so the legend has a span.
ws-simulation.js
line 213
PartCraft 0.8.1
const range = (values, unit, title, u) => { let lo = Infinity, hi = -Infinity; for (const x of values) { if (x < lo) lo = x; if (x > hi) hi = x; } if (!(hi > lo)) hi = lo + 1; return { values, lo, hi, unit, title, u }; };
Result under the pointer
32-bit in part
The stress, displacement, strain and safety factor at the point under the pointer.
The weights come from the drawn positions (a Float32Array, displaced when the plot is deformed) and the ray's hit point; each value is the weighted sum of the three corner entries of the result arrays. The safety factor divides only when the interpolated stress is above 0.
ws-simulation.js
lines 254 to 272
PartCraft 0.8.1
/** The result under the pointer: a ray on the plot overlay, the value interpolated in its triangle. */
function probe(host, ev) {
if (!S.probeEl || !fresh(host) || !ev) return;
const ov = host.view.overlays && host.view.overlays.get('simulation-plot');
const h = ov ? host.view.ray(ev.clientX, ev.clientY).intersectObject(ov, false)[0] : null;
if (!h) { S.probeEl.textContent = t('study.ws.probe_hint'); return; }
const mesh = S.result.plotMesh, tri = h.faceIndex, g = ov.geometry.attributes.position, I = mesh.indices, P = mesh.positions;
const a = [0, 1, 2].map((k) => I[3 * tri + k]);
const w = bary(a.map((i) => [g.getX(i), g.getY(i), g.getZ(i)]), h.point.toArray());
const val = (arr) => w[0] * arr[a[0]] + w[1] * arr[a[1]] + w[2] * arr[a[2]];
const p = [0, 1, 2].map((c) => w[0] * P[3 * a[0] + c] + w[1] * P[3 * a[1] + c] + w[2] * P[3 * a[2] + c]);
const r = S.result, at = t('study.ws.probe_at', { xyz: p.map((v) => U.fmt(v, 2)).join(', ') });
if (TH.handles(r)) { S.probeEl.textContent = TH.probeText(r, S, val, at); return; }
if (r.kind === 'modal') { S.probeEl.textContent = t('study.ws.probe_modal', { a: U.fmt(val(field(host).values), 3), at }); return; }
const vm = val(r.vertex.vm), d = val(r.vertex.disp);
const sf = vm > 0 ? U.sig(r.material.yield / vm) : t('study.ws.over_100');
S.probeEl.textContent = r.vertex.s1 ? t('study.ws.probe_static_principal', { vm: U.sig(vm), s1: U.sig(val(r.vertex.s1)), s3: U.sig(val(r.vertex.s3)), e: U.sig(val(r.vertex.strain)), d: U.sig(d), sf, at })
: t('study.ws.probe_static', { vm: U.sig(vm), d: U.sig(d), sf, at });
}
Closed form from five dot products. The determinant d₀₀d₁₁ − d₀₁² subtracts nearly equal numbers for a sliver triangle; a determinant of exactly zero is replaced by 1.
The drawn position of each plot vertex when the deformed shape is shown.
drawn position = position + scale × displacement
Written into a Float32Array for the drawn geometry only.
ws-simulation.js
lines 230 to 231
PartCraft 0.8.1
const P = mesh.positions, pos = def ? new Float32Array(P.length) : Float32Array.from(P);
if (def) for (let i = 0; i < P.length; i++) pos[i] = P[i] + S.scale * f.u[i];
Automatic deformation scale
32-bit, display only
The factor the displacements are multiplied by so the deformed shape can be seen.
raw = 0.08 × box diagonal ÷ largest |u|, rounded up to 1, 2, 5 or 10 × 10ᵉ
The result is rounded with toPrecision(2) and is 1 when there is no displacement. It only scales the drawn shape.
ws-simulation.js
lines 214 to 220
PartCraft 0.8.1
function autoScale(host, f) {
const rg = host.regen, bb = rg.stats.bbox, diag = Math.hypot(bb.max[0] - bb.min[0], bb.max[1] - bb.min[1], bb.max[2] - bb.min[2]);
let m = 0; for (let i = 0; i < f.u.length; i += 3) m = Math.max(m, Math.hypot(f.u[i], f.u[i + 1], f.u[i + 2]));
if (!(m > 0)) return 1;
const raw = 0.08 * diag / m, e = Math.floor(Math.log10(raw)), b = raw / 10 ** e;
return +(([1, 2, 5, 10].find((s) => s >= b) || 10) * 10 ** e).toPrecision(2);
}
In detail
Method
Probe: ray cast on the plot overlay, barycentric weights of the hit point in its triangle, weighted sum of the three corner values. Legend range: least and largest value in one pass.
Number types
Float32Array for plot positions, mode-shape magnitude and safety-factor colors; result arrays as the solver returned them; 64-bit arithmetic.
Tolerances
bary replaces a determinant of exactly zero by 1. The live coarse level uses 4 times the study's element size.
Limits
Probe positions are printed to 2 decimals and values to 4 significant figures.
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.