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.7.16: the community's issues. #10 plated pads only on real copper, stackup from the board; #11 six-layer…
ee2efa2
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
// Adom Fields: the ground. Three checks on the board's return path, drawn on it:
// 1. Ground offset: the GND copper as a rubber sheet, height = the solved millivolts (exaggerated, stated),
// contour rings; each part's ground pin tagged with its offset from the connectors' ground.
// 2. Ground loops and islands: every connector that carries ground to the outside is a cable that may meet
// another off the board and close a loop (dashed arcs, the loop's area estimated); ground copper with no
// via or pin tying it to the rest floats (an antenna), flagged red.
// 3. The stitching fence: each ground via covers lambda/40 around it (lambda/20 spacing at the highest
// frequency of interest, 1 GHz unless the spec says); stretches of the board edge no via covers glow red.
export async function attach(ctx) {
const { BAB, scene, V, camera, state, LAYERS, frameDt, root, flow } = ctx;
if (!flow || !flow.data) return null;
const { lay, real, W, H, d, meta, nets, zOf, cellOf, bx, by, ER } = flow.data;
const board = state.fields.board;
const gid = nets.findIndex(n => n.ground);
if (gid < 0) return { apply() {}, describe: () => ['<b>Ground</b>', 'No ground net in this solve'], narration: () => 'This board has no ground net in the solve.' };
const G = nets[gid], N = W * H;
// ---- 1. offset: the GND cells' volts, in mV ----------------------------------------------------------
let vmin = Infinity, vmax = -Infinity;
for (const k of real) for (let i = 0; i < N; i++) if (lay[k].net[i] === gid && Number.isFinite(lay[k].volt[i])) { vmin = Math.min(vmin, lay[k].volt[i]); vmax = Math.max(vmax, lay[k].volt[i]); }
if (!Number.isFinite(vmin)) { vmin = 0; vmax = 1e-6; }
const span = Math.max(1e-7, vmax - vmin), H_MM = Math.min(4, 0.15 * Math.min(board.widthMm, board.heightMm)), kz = H_MM / span; // mm per volt
BAB.Effect.ShadersStore.fieldsGroundVertexShader = `precision highp float;
attribute vec3 position; attribute vec2 uv; attribute float mv;
uniform mat4 world; uniform mat4 viewProjection; uniform float lift; varying vec2 vUV; varying float vMv; varying vec3 vW;
void main(void) { vUV = uv; vMv = mv; vec3 p = position + vec3(0.0, 0.0, lift * mv); vW = (world * vec4(p, 1.0)).xyz; gl_Position = viewProjection * vec4(vW, 1.0); }`;
BAB.Effect.ShadersStore.fieldsGroundFragmentShader = `precision highp float;
varying vec2 vUV; varying float vMv; varying vec3 vW; uniform sampler2D mask; uniform float top; uniform float step; uniform float alpha;
vec3 magma(float t) { t = clamp(t, 0.0, 1.0); vec3 a = vec3(0.05, 0.03, 0.18), b = vec3(0.55, 0.12, 0.5), c = vec3(0.98, 0.45, 0.35), e = vec3(0.99, 0.92, 0.7);
return t < 0.33 ? mix(a, b, t / 0.33) : t < 0.66 ? mix(b, c, (t - 0.33) / 0.33) : mix(c, e, (t - 0.66) / 0.34); }
void main(void) {
if (texture2D(mask, vUV).a < 0.5) discard;
float t = vMv / top; vec3 col = magma(t);
float f = fract(vMv / step); float ring = smoothstep(0.06, 0.0, min(f, 1.0 - f)); // a ring every step of the scale
vec3 n = normalize(cross(dFdx(vW), dFdy(vW))); float sh = 0.75 + 0.25 * abs(n.z);
gl_FragColor = vec4(col * sh + ring * 0.6, alpha);
}`;
const terrains = [];
const stride = Math.max(1, Math.ceil(Math.sqrt(N / 60000)));
for (const k of real) {
const L = lay[k]; let cells = 0; for (let i = 0; i < N; i++) if (L.net[i] === gid) cells++; if (cells < 0.02 * N) continue;
const cw = Math.ceil(W / stride) + 1, ch = Math.ceil(H / stride) + 1, pos = new Float32Array(cw * ch * 3), uvs = new Float32Array(cw * ch * 2), mv = new Float32Array(cw * ch), ind = [];
for (let rr = 0; rr < ch; rr++) for (let cc = 0; cc < cw; cc++) {
const c = Math.min(W - 1, cc * stride), r = Math.min(H - 1, rr * stride), v = rr * cw + cc;
pos[v * 3] = bx(meta.x0 + Math.min(W, cc * stride) * d); pos[v * 3 + 1] = by(meta.y0 + Math.min(H, rr * stride) * d);
uvs[v * 2] = Math.min(W, cc * stride) / W; uvs[v * 2 + 1] = Math.min(H, rr * stride) / H;
let i = r * W + c; if (L.net[i] !== gid) { let best = -1; for (let rad = 1; rad <= 3 && best < 0; rad++) for (let dr = -rad; dr <= rad && best < 0; dr++) for (let dc = -rad; dc <= rad; dc++) { const r2 = r + dr, c2 = c + dc; if (r2 < 0 || c2 < 0 || r2 >= H || c2 >= W) continue; if (L.net[r2 * W + c2] === gid) { best = r2 * W + c2; break; } } if (best >= 0) i = best; }
mv[v] = L.net[i] === gid && Number.isFinite(L.volt[i]) ? (L.volt[i] - vmin) * 1000 : 0;
}
for (let rr = 0; rr < ch - 1; rr++) for (let cc = 0; cc < cw - 1; cc++) { const a = rr * cw + cc; ind.push(a, a + cw, a + 1, a + 1, a + cw, a + cw + 1); }
const mesh = new BAB.Mesh('G-' + L.name, scene); const vd = new BAB.VertexData(); vd.positions = pos; vd.uvs = uvs; vd.indices = ind; vd.applyToMesh(mesh, false); mesh.setVerticesData('mv', mv, false, 1);
const tex = new Uint8Array(N * 4); for (let i = 0; i < N; i++) tex[i * 4 + 3] = L.net[i] === gid ? 255 : 0;
const mtex = new BAB.RawTexture(tex, W, H, BAB.Constants.TEXTUREFORMAT_RGBA, scene, false, false, BAB.Texture.BILINEAR_SAMPLINGMODE, BAB.Constants.TEXTURETYPE_UNSIGNED_BYTE);
const mat = new BAB.ShaderMaterial('fields-ground-' + L.name, scene, { vertex: 'fieldsGround', fragment: 'fieldsGround' }, { attributes: ['position', 'uv', 'mv'], uniforms: ['world', 'viewProjection', 'lift', 'top', 'step', 'alpha'], samplers: ['mask'], needAlphaBlending: true });
const topMv = span * 1000, step = Math.pow(10, Math.floor(Math.log10(topMv / 4))) * (topMv / Math.pow(10, Math.floor(Math.log10(topMv / 4))) > 20 ? 5 : 1);
mat.setTexture('mask', mtex); mat.setFloat('top', topMv); mat.setFloat('step', step); mat.setFloat('lift', kz / 1000); mat.backFaceCulling = false;
mesh.material = mat; mesh.parent = root; mesh.isPickable = false; mesh.renderingGroupId = 1; mesh.setEnabled(false);
terrains.push({ k, mesh, mat, step });
}
// the parts' ground pins: their offset from the connectors
const term = (G.terminals || []);
const mvAt = (x, y) => { const i = cellOf(x, y); if (i < 0) return null; for (const k of real) if (lay[k].net[i] === gid && Number.isFinite(lay[k].volt[i])) return (lay[k].volt[i] - vmin) * 1000; return null; };
// ---- 2. ground loops through the outside, and floating ground islands --------------------------------
const ext = term.filter(t => /^(J|P|MC|MP)\d/i.test(t.ref));
const outside = [...new Map(ext.map(t => [t.ref, t])).values()];
const loops = [];
for (let a = 0; a < outside.length; a++) for (let b = a + 1; b < outside.length; b++) { const p = outside[a], q = outside[b], dist = Math.hypot(p.x - q.x, p.y - q.y); loops.push({ a: p.ref, b: q.ref, p, q, dist, mm2: dist * 10 }); } // a cable pair 10 mm off the board, an estimate
loops.sort((x, y) => y.mm2 - x.mm2);
// islands: ground copper components, joined across layers only by ground vias and pins
const comp = new Int32Array(real.length * N).fill(-1); const sizes = [];
const link = new Set(); for (const v of (state.fields.vias || [])) if (v.net === G.name) { const i = cellOf(v.x, v.y); if (i >= 0) link.add(i); }
for (const p of (state.fields.pads || [])) if (p.net === G.name) { const i = cellOf(p.x, p.y); if (i >= 0) link.add(i); }
let nc = 0;
for (let li = 0; li < real.length; li++) for (let i = 0; i < N; i++) {
if (lay[real[li]].net[i] !== gid || comp[li * N + i] >= 0) continue;
const stack = [li * N + i]; comp[li * N + i] = nc; let sz = 0;
while (stack.length) {
const v = stack.pop(), l = Math.floor(v / N), j = v % N, r = Math.floor(j / W), c = j % W; sz++;
const nb = []; if (c > 0) nb.push(v - 1); if (c < W - 1) nb.push(v + 1); if (r > 0) nb.push(v - W); if (r < H - 1) nb.push(v + W);
if (link.has(j)) for (let l2 = 0; l2 < real.length; l2++) if (l2 !== l) nb.push(l2 * N + j);
for (const u of nb) { const l2 = Math.floor(u / N), j2 = u % N; if (comp[u] < 0 && lay[real[l2]].net[j2] === gid) { comp[u] = nc; stack.push(u); } }
}
sizes.push(sz); nc++;
}
const mainComp = sizes.indexOf(Math.max(...sizes, 0));
const islands = []; for (let c = 0; c < nc; c++) if (c !== mainComp && sizes[c] * d * d > 0.5) islands.push({ c, mm2: sizes[c] * d * d });
const isl = new Uint8Array(N * 4);
for (let li = 0; li < real.length; li++) for (let i = 0; i < N; i++) { const c = comp[li * N + i]; if (c >= 0 && c !== mainComp && sizes[c] * d * d > 0.5) { isl[i * 4] = 255; isl[i * 4 + 1] = 60; isl[i * 4 + 2] = 50; isl[i * 4 + 3] = 230; } }
// ---- 3. the stitching fence ---------------------------------------------------------------------------
const FMAX = 1e9, lambda = 299.792458 / (FMAX / 1e9) / Math.sqrt((ER + 1) / 2), reach = lambda / 40; // mm, in the board's half-air field
const gvias = (state.fields.vias || []).filter(v => v.net === G.name);
const edge = []; const per = 2 * (board.widthMm + board.heightMm), stepE = 0.4;
for (let s = 0; s < per; s += stepE) {
let x, y; const w = board.widthMm, h = board.heightMm;
if (s < w) { x = board.x0 + s; y = board.y0; } else if (s < w + h) { x = board.x0 + w; y = board.y0 + (s - w); } else if (s < 2 * w + h) { x = board.x0 + w - (s - w - h); y = board.y0 + h; } else { x = board.x0; y = board.y0 + h - (s - 2 * w - h); }
const near = gvias.some(v => Math.hypot(v.x - x, v.y - y) <= reach + 1.0); // 1 mm: the via's own ring and the pour's edge
edge.push({ x, y, ok: near });
}
const gaps = []; { let run = null; for (const e of edge) { if (!e.ok) { if (!run) run = { pts: [] }; run.pts.push(e); } else if (run) { gaps.push(run); run = null; } } if (run) gaps.push(run); }
for (const g of gaps) g.mm = g.pts.length * stepE;
gaps.sort((a, b) => b.mm - a.mm);
const covered = edge.filter(e => e.ok).length / edge.length * 100;
// ---- drawing ----------------------------------------------------------------------------------------------
const group = new BAB.TransformNode('fields-ground', scene); group.parent = root; group.setEnabled(false);
const lines = [];
const gl = (pts3, rgb, width, dash, name) => { try { const m = BAB.CreateGreasedLine(name, { points: pts3 }, { color: new BAB.Color3(...rgb), width, useDash: !!dash, dashCount: 30, dashRatio: 0.5, sizeAttenuation: true }, scene); m.parent = group; m.renderingGroupId = 2; m.isPickable = false; lines.push(m); return m; } catch (e) { return null; } };
const zt = () => zOf(real[0]) + 0.1;
// the fence: a ring per ground via, the gaps along the edge in red
for (const v of gvias) { const pts = []; for (let a = 0; a <= 32; a++) { const t = a / 32 * Math.PI * 2; pts.push(bx(v.x + Math.cos(t) * reach), by(v.y + Math.sin(t) * reach), 0); } gl(pts, [0.3, 0.85, 1.0], 0.05, false, 'gnd-fence'); }
for (const g of gaps) { if (g.pts.length < 2) continue; gl(g.pts.flatMap(p => [bx(p.x), by(p.y), 0.15]), [1, 0.3, 0.25], 0.35, false, 'gnd-gap'); }
// the possible loops through the outside: arcs above the board between ground connectors
for (const l of loops.slice(0, 3)) { const pts = []; for (let a = 0; a <= 24; a++) { const t = a / 24, x = l.p.x + (l.q.x - l.p.x) * t, y = l.p.y + (l.q.y - l.p.y) * t; pts.push(bx(x), by(y), 2 + 8 * Math.sin(Math.PI * t)); } gl(pts, [1, 0.65, 0.2], 0.12, true, 'gnd-loop'); }
// islands: a red sheet over floating ground copper
const it = new BAB.RawTexture(isl, W, H, BAB.Constants.TEXTUREFORMAT_RGBA, scene, false, false, BAB.Texture.NEAREST_SAMPLINGMODE, BAB.Constants.TEXTURETYPE_UNSIGNED_BYTE);
const ip = BAB.MeshBuilder.CreatePlane('gnd-islands', { width: W * d, height: H * d, sideOrientation: BAB.Mesh.DOUBLESIDE }, scene);
{ const uvs = ip.getVerticesData(BAB.VertexBuffer.UVKind); for (let i = 1; i < uvs.length; i += 2) uvs[i] = 1 - uvs[i]; ip.setVerticesData(BAB.VertexBuffer.UVKind, uvs); }
ip.position.x = bx(meta.x0 + W * d / 2); ip.position.y = by(meta.y0 + H * d / 2); ip.position.z = 0.2;
const im = new BAB.StandardMaterial('gnd-islands-m', scene); im.diffuseTexture = it; it.hasAlpha = true; im.useAlphaFromDiffuseTexture = true; im.emissiveColor = new BAB.Color3(1, 1, 1); im.disableLighting = true; im.backFaceCulling = false;
ip.material = im; ip.parent = group; ip.renderingGroupId = 2; ip.isPickable = false;
// tags: each part's ground offset, the connectors
const host = document.getElementById('labels'), tags = [];
const tag = (html, x, y, z) => { const el = document.createElement('div'); el.className = 'lbl ground'; el.innerHTML = html; el.hidden = true; host.appendChild(el); tags.push({ el, x, y, z }); };
const seen = new Set();
for (const t of term) { if (seen.has(t.ref)) continue; seen.add(t.ref); const m = mvAt(t.x, t.y); if (m == null) continue; const isExt = outside.some(o => o.ref === t.ref); tag(`<b>${t.ref}</b> ${isExt ? 'to the outside' : 'ground pin'} <span class="v">${m >= 0.01 ? '+' + m.toFixed(2) : m.toFixed(2)} mV</span>`, t.x, t.y, 1.5); }
for (const l of loops.slice(0, 1)) tag(`<b>possible ground loop</b> ${l.a}–${l.b} <span class="v">~${l.mm2.toFixed(0)} mm²</span>`, (l.p.x + l.q.x) / 2, (l.p.y + l.q.y) / 2, 10.5);
if (gaps[0] && gaps[0].mm > 1) { const g = gaps[0], m = g.pts[Math.floor(g.pts.length / 2)]; tag(`<b>stitching gap</b> <span class="v">${g.mm.toFixed(1)} mm</span>`, m.x, m.y, 1.2); }
// ---- the panel -------------------------------------------------------------------------------------------
const panel = document.createElement('div'); panel.className = 'cyclemeter groundpanel'; panel.hidden = true; document.querySelector('.stage').appendChild(panel);
panel.innerHTML = `<div class="ch" title="Click to fold">Ground</div><div class="body">
<div class="row"><span class="dot" style="background:#fca678"></span>Offset across ${G.name}<i>${(span * 1000).toFixed(2)} mV</i></div>
<div class="row"><span class="dot" style="background:#ffa63d"></span>Ground to the outside<i>${outside.length} (${outside.map(o => o.ref).join(', ')})</i></div>
${loops[0] ? `<div class="row"><span class="dot" style="background:#ffa63d"></span>Largest possible loop<i>${loops[0].a}–${loops[0].b} ~${loops[0].mm2.toFixed(0)} mm²</i></div>` : ''}
<div class="row"><span class="dot" style="background:${islands.length ? '#ff5a4a' : '#5cf08f'}"></span>Floating ground islands<i>${islands.length ? islands.length + ' · ' + islands.reduce((s, x) => s + x.mm2, 0).toFixed(1) + ' mm²' : 'none'}</i></div>
<div class="row"><span class="dot" style="background:${covered > 95 ? '#5cf08f' : '#ff5a4a'}"></span>Edge stitched (λ/20 at 1 GHz)<i>${covered.toFixed(0)} %${gaps[0] && gaps[0].mm > 1 ? ' · gap ' + gaps[0].mm.toFixed(1) + ' mm' : ''}</i></div>
<div class="row" style="color:var(--text-2)">Height ×${(kz).toFixed(0)} mm per volt; a ring every ${terrains[0] ? terrains[0].step : '?'} mV</div></div>`;
panel.addEventListener('click', (e) => { if (e.target.closest('.ch')) panel.classList.toggle('folded'); });
// ---- frame and view -----------------------------------------------------------------------------------
const knob = { on: 0, to: 0 };
scene.onBeforeRenderObservable.add(() => {
const dt = frameDt();
knob.on += (knob.to - knob.on) * (1 - Math.exp(-dt * 2.5)); if (Math.abs(knob.to - knob.on) < 1e-3) knob.on = knob.to;
const live = knob.on > 0.01; group.setEnabled(live); group.position.z = zt();
for (const t of terrains) { t.mesh.setEnabled(live); if (!live) continue; t.mesh.position.z = zOf(t.k) + 0.05; t.mat.setFloat('alpha', knob.on * (t.k === real[0] || knob.on < 0.5 ? 0.95 : 0.8)); }
im.alpha = knob.on;
if (!live) { for (const t of tags) t.el.hidden = true; return; }
const e = V.getEngine(), w = e.getRenderWidth(), h = e.getRenderHeight(), hostEl = document.getElementById('host'), wm = group.getWorldMatrix();
for (const t of tags) { const p = BAB.Vector3.Project(BAB.Vector3.TransformCoordinates(new BAB.Vector3(bx(t.x), by(t.y), t.z), wm), BAB.Matrix.Identity(), scene.getTransformMatrix(), camera.viewport.toGlobal(w, h)); t.el.hidden = p.z > 1 || p.z < 0; t.el.style.left = (p.x / (w / hostEl.clientWidth)) + 'px'; t.el.style.top = (p.y / (h / hostEl.clientHeight)) + 'px'; }
});
function apply(view) {
knob.to = view.quantity === 'ground' ? 1 : 0; panel.hidden = view.quantity !== 'ground';
if (view.quantity !== 'ground') { const leg = document.getElementById('legend'); if (leg) leg.classList.remove('ground'); return; }
for (const Lname of LAYERS) { const m = ctx.layerMeshes[Lname]; if (m && m.material) m.material.alpha = 0.25; }
const leg = document.getElementById('legend'); if (leg) { leg.hidden = false; leg.classList.add('magma'); document.getElementById('legendTitle').textContent = `${G.name} above its lowest point, mV (height and colour)`; document.getElementById('legLo').textContent = '0'; document.getElementById('legHi').textContent = (span * 1000).toFixed(2); }
}
function readout(Lname, x, y) { const m = mvAt(x, y); return m == null ? 'not ground copper' : `${G.name} +${m.toFixed(3)} mV above its lowest point`; }
function describe() {
return ['<b>Ground</b>: the return path',
'<b>The sheet</b> = the ground copper, lifted by its voltage: ground is not one voltage, the return current drops a little across it (rings every ' + (terrains[0] ? terrains[0].step : '?') + ' mV)',
'<b>Dashed orange arcs</b> = cables from ground connectors that may meet off the board and close a loop',
'<b>Red copper</b> = ground islands with no via or pin to the rest: they float and act as antennas',
'<b>Blue rings</b> = each ground via\'s reach for stitching (λ/20 at 1 GHz); <b>red edge</b> = a gap in the fence',
'DC offset from the solve; the loop areas assume cables 10 mm off the board'];
}
function narration() {
const u = term.filter(t => !outside.some(o => o.ref === t.ref)).map(t => ({ ref: t.ref, m: mvAt(t.x, t.y) })).filter(t => t.m != null).sort((a, b) => b.m - a.m)[0];
return `This is the Ground view. Ground is supposed to be zero volts everywhere, but it is copper too, and the current returning through it makes small voltage drops. The sheet is the ground copper lifted by its voltage, greatly exaggerated: across this board it spans ${(span * 1000).toFixed(1)} millivolts${u ? `, highest under ${u.ref}, ${u.m.toFixed(1)} millivolts above the connectors` : ''}. That difference is noise that every chip sees on its own ground. ${outside.length > 1 ? `${outside.length} connectors carry ground to the outside: ${outside.map(o => o.ref).join(', ')}. If their cables meet somewhere off the board, they close a ground loop, drawn as the dashed orange arcs, and that loop picks up magnetic noise. ` : ''}${islands.length ? `There ${islands.length === 1 ? 'is one floating island' : 'are ' + islands.length + ' floating islands'} of ground copper, in red, with no via tying ${islands.length === 1 ? 'it' : 'them'} to the rest; floating copper acts as an antenna. ` : 'Every piece of ground copper is tied to the rest, with no floating islands. '}The blue rings show how far each ground via or plated ground pin stitches the layers together for frequencies up to ${(() => { const fm = (((state.fields.review || {}).ground || {}).stitching || {}).fmaxHz || 1e9; return fm >= 1e9 ? 'one gigahertz' : Math.round(fm / 1e6) + ' megahertz'; })()}, this board's strongest tones; ${covered > 95 ? 'the board edge is fully covered.' : `about ${covered.toFixed(0)} percent of the board edge is covered, and the red stretches are gaps where noise can leak out.`}`;
}
console.log(`ground: ${G.name} spans ${(span * 1000).toFixed(3)} mV; ${outside.length} ground connectors (${outside.map(o => o.ref).join(' ')}); ${islands.length} floating island(s) ${islands.map(i => i.mm2.toFixed(1) + ' mm²').join(' ')}; edge stitched ${covered.toFixed(0)} % with ${gvias.length} ground vias, reach ${reach.toFixed(1)} mm, largest gap ${gaps[0] ? gaps[0].mm.toFixed(1) : 0} mm`);
return { apply, describe, narration, readout };
}