app
Adom Fields
Public Made by Adomby adom
Adom Fields: where the current and the heat go on your board.
main
John Lauer
adom-fields 0.8.5: solid bodies follow the parts' offset as the stack opens
4ef4cc2
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
// Adom Fields: the solid bodies off the board (a package, its pad, a finned heatsink) solved with the board's heat
// (spec `solids`). Each body is drawn from its solve cells: every face that is not shared with a cell of the same body
// (the outside, and the faces where it meets another body), coloured by that cell's heat flux in the heat view and by
// its temperature in the temperature view, on the same palettes as the copper. The heatsink is drawn see-through so
// the package and pad under it show; hiding a body reveals the interface faces of the one it sits on.
const INFERNO = [[0.001, 0.0, 0.014], [0.16, 0.04, 0.33], [0.38, 0.09, 0.44], [0.59, 0.19, 0.38], [0.79, 0.30, 0.26], [0.93, 0.47, 0.11], [0.98, 0.68, 0.06], [0.97, 0.87, 0.30], [0.99, 1.0, 0.64]];
const MAGMA = [[0.001, 0.0, 0.014], [0.11, 0.06, 0.28], [0.29, 0.06, 0.48], [0.47, 0.11, 0.51], [0.66, 0.18, 0.47], [0.83, 0.28, 0.40], [0.96, 0.44, 0.36], [0.99, 0.65, 0.47], [0.99, 0.99, 0.75]];
function ramp(t, cm) { t = Math.min(1, Math.max(0, t)) * 8; const i = Math.min(7, Math.floor(t)), f = t - i, a = cm[i], b = cm[i + 1]; return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f, a[2] + (b[2] - a[2]) * f]; }
export async function attach(ctx) {
const { BAB, scene, state, Z, root } = ctx;
const meta = state.fields.solids;
if (!meta || !meta.file) return null;
const S = await fetch(meta.file).then(r => { if (!r.ok) throw new Error(meta.file + ': HTTP ' + r.status); return r.json(); });
const board = state.fields.board, T = board.thicknessMm;
const cells = S.cells, n = cells.length;
const key = (a, b, c) => Math.round(a * 1000) + ',' + Math.round(b * 1000) + ',' + Math.round(c * 1000);
const lo = new Map(); cells.forEach((c, i) => lo.set(key(c[0], c[1], c[2]), i));
const bx = (x) => x - board.x0 - board.widthMm / 2, by = (y) => board.y0 + board.heightMm / 2 - y, bz = (z) => T / 2 + z;
// the faces of one body: [x0,y0,z0,x1,y1,z1] corners per axis and side, with a shade so the shape reads in 3D
const faceDefs = [
{ nb: (c) => key(c[3], c[1], c[2]), quad: (c) => [[c[3], c[1], c[2]], [c[3], c[4], c[2]], [c[3], c[4], c[5]], [c[3], c[1], c[5]]], shade: 0.78 },
{ back: 0, quad: (c) => [[c[0], c[1], c[2]], [c[0], c[1], c[5]], [c[0], c[4], c[5]], [c[0], c[4], c[2]]], shade: 0.70 },
{ nb: (c) => key(c[0], c[4], c[2]), quad: (c) => [[c[0], c[4], c[2]], [c[0], c[4], c[5]], [c[3], c[4], c[5]], [c[3], c[4], c[2]]], shade: 0.86 },
{ back: 1, quad: (c) => [[c[0], c[1], c[2]], [c[3], c[1], c[2]], [c[3], c[1], c[5]], [c[0], c[1], c[5]]], shade: 0.66 },
{ nb: (c) => key(c[0], c[1], c[5]), quad: (c) => [[c[0], c[1], c[5]], [c[3], c[1], c[5]], [c[3], c[4], c[5]], [c[0], c[4], c[5]]], shade: 1.0 },
{ back: 2, quad: (c) => [[c[0], c[1], c[2]], [c[0], c[4], c[2]], [c[3], c[4], c[2]], [c[3], c[1], c[2]]], shade: 0.55 },
];
// the negative-side neighbour: the cell whose far corner on that axis is this cell's near one
const hi = [new Map(), new Map(), new Map()];
cells.forEach((c, i) => { hi[0].set(key(c[3], c[1], c[2]), i); hi[1].set(key(c[0], c[4], c[2]), i); hi[2].set(key(c[0], c[1], c[5]), i); });
const neighbour = (c, f) => {
if (f.back === undefined) { const j = lo.get(f.nb(c)); return j === undefined ? -1 : j; }
const j = hi[f.back].get(key(c[0], c[1], c[2])); return j === undefined ? -1 : j;
};
const bodies = S.bodies.map((name, bi) => {
const pos = [], idx = [], cellOf = [], shade = [];
for (let i = 0; i < n; i++) {
const c = cells[i]; if (c[6] !== bi) continue;
for (const f of faceDefs) {
const j = neighbour(c, f); if (j >= 0 && cells[j][6] === bi) continue;
const v0 = pos.length / 3;
for (const p of f.quad(c)) pos.push(bx(p[0]), by(p[1]), bz(p[2]));
idx.push(v0, v0 + 1, v0 + 2, v0, v0 + 2, v0 + 3);
cellOf.push(i); shade.push(f.shade);
}
}
const mesh = new BAB.Mesh('solid-' + name, scene);
const vd = new BAB.VertexData(); vd.positions = pos; vd.indices = idx;
const norms = []; BAB.VertexData.ComputeNormals(pos, idx, norms); vd.normals = norms;
vd.colors = new Array(pos.length / 3 * 4).fill(1);
vd.applyToMesh(mesh, true);
const mat = new BAB.StandardMaterial('solid-mat-' + name, scene);
mat.disableLighting = true; mat.emissiveColor = new BAB.Color3(1, 1, 1); mat.diffuseColor = new BAB.Color3(0, 0, 0); mat.backFaceCulling = false;
const sink = /heat ?sink/i.test(name);
if (sink) { mat.alpha = 0.62; mat.needDepthPrePass = true; }
mesh.material = mat; mesh.useVertexColors = true; mesh.hasVertexAlpha = false; mesh.isPickable = false; mesh.parent = root; mesh.renderingGroupId = 1;
return { name, mesh, cellOf, shade, sink };
});
// the regimes: still and moving air, and each fan level (spec fanLevels) when the solve has them
const R = { still: S.still, moving: S.moving, ...(S.levels || {}) };
const fluxes = Object.fromEntries(Object.entries(R).map(([k, v]) => [k, v.q.map(q => Math.hypot(q[0], q[1], q[2]))]));
// the bodies' own ranges, so purple to yellow spans what happens inside them: flux from its 2nd percentile to its peak
// (log), temperature from the coolest to the hottest body cell
const pct = (arr, p) => { const v = arr.filter(x => x > 0).sort((a, b) => a - b); return v.length ? v[Math.min(v.length - 1, Math.floor(p * v.length))] : 1e-9; };
// with fan levels, one range over all of them, so the levels compare by colour; without, each regime its own
const keys = Object.keys(R), fanned = !!S.levels;
// loops, not spreads: five levels of 40k cells overflow the call stack as Math.max(...arguments)
const all = (pick) => keys.flatMap(k => Array.from(pick(k)));
const mx = (a) => { let m = -Infinity; for (const x of a) if (x > m) m = x; return m; }, mn = (a) => { let m = Infinity; for (const x of a) if (x < m) m = x; return m; };
const shared = { qLo: Math.max(1e-9, pct(all(k => fluxes[k]), 0.02)), qHi: Math.max(1e-9, mx(all(k => fluxes[k]))), tLo: mn(all(k => R[k].t)), tHi: mx(all(k => R[k].t)) };
const range = Object.fromEntries(keys.map(r => [r, fanned ? shared : { qLo: Math.max(1e-9, pct(fluxes[r], 0.02)), qHi: Math.max(1e-9, mx(fluxes[r])), tLo: mn(R[r].t), tHi: mx(R[r].t) }]));
const qHi = range[keys[0]].qHi;
// the legend line under the copper's: what the bodies show and the hottest point of each
const legend = document.getElementById('legend');
let info = document.getElementById('solidsInfo');
if (!info && legend) { info = document.createElement('div'); info.id = 'solidsInfo'; info.style.cssText = 'margin-top:6px;font-size:11px;line-height:1.35;opacity:.9;max-width:340px'; legend.appendChild(info); }
// pulled apart with the stack, every frame of the animation: the bodies ride up exactly as the parts model does, so the
// solved heatsink stays on the real one
scene.onBeforeRenderObservable.add(() => { const z = window.fieldsFlow && window.fieldsFlow.topOffset ? window.fieldsFlow.topOffset() : 0; for (const b of bodies) b.mesh.position.z = z; });
function apply(view) {
const q = view.quantity, show = q === 'heat' || q === 'temperature';
const fk = S.levels ? (view.air === 'still' ? 'fan0' : view.air === 'moving' ? 'fan100' : view.air) : null;
const reg = fk && R[fk] ? fk : (view.air === 'moving' ? 'moving' : 'still');
const tHi = (state.fields.scales && state.fields.scales.tHi) || 1;
const t = R[reg].t, fq = fluxes[reg];
for (const b of bodies) {
b.mesh.setEnabled(show);
if (!show) continue;
const col = new Float32Array(b.cellOf.length * 16);
for (let f = 0; f < b.cellOf.length; f++) {
const i = b.cellOf[f];
const rg = range[reg];
const c = q === 'heat' ? ramp(fq[i] <= rg.qLo ? 0 : Math.log(fq[i] / rg.qLo) / Math.log(rg.qHi / rg.qLo), INFERNO) : ramp((t[i] - rg.tLo) / Math.max(1e-6, rg.tHi - rg.tLo), MAGMA);
const s = b.shade[f];
for (let v = 0; v < 4; v++) { const o = (f * 4 + v) * 4; col[o] = c[0] * s; col[o + 1] = c[1] * s; col[o + 2] = c[2] * s; col[o + 3] = 1; }
}
b.mesh.setVerticesData(BAB.VertexBuffer.ColorKind, col, true);
}
if (info) {
info.hidden = !show;
if (show) {
const lvl = state.fields.airStates && state.fields.airStates.levels.find(l => l.name === reg);
const stats = ((lvl && lvl.solids) || state.fields.solids[reg] || []).map(x => `${x.name} +${x.maxRiseC.toFixed(1)} °C`).join(' · ');
const rg = range[reg];
info.textContent = (q === 'heat' ? `Q1 and the heatsink: heat flux, W/mm² (log, ${rg.qLo.toExponential(1)} purple to ${rg.qHi.toFixed(3)} yellow). ` : `Q1 and the heatsink: +${rg.tLo.toFixed(1)} °C purple to +${rg.tHi.toFixed(1)} °C yellow (${reg.startsWith('fan') ? (reg === 'fan0' ? 'fan off' : 'fan ' + reg.slice(3) + ' %') : reg + ' air'}). `) + `Hottest: ${stats}`;
}
}
}
return { apply, bodies };
}