app
AI Flow: Reverse Engineering
Public Made by Adomby adom
Photos of a PCB to a tagged KiCad copy: parts placed, copper and nets with their evidence, a schematic, a measured confidence score, bench tests and a video of the run.
main
Caleb
Publish 0.5.0
1984b40
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nets</title>
<style>
:root{--bg:#0d1117;--panel:#161b22;--line:#30363d;--text:#e6edf3;--dim:#8b949e;--accent:#2f81f7}
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font:14px/1.45 Inter,system-ui,-apple-system,"Segoe UI",sans-serif}
main{display:grid;grid-template-columns:300px 1fr;height:100%}
aside{border-right:1px solid var(--line);display:flex;flex-direction:column;min-height:0}
.top{padding:10px;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:8px}
.modes{display:flex;gap:6px}
.modes button{flex:1;min-height:44px;border:1px solid var(--line);background:none;color:var(--text);border-radius:6px;cursor:pointer;font:inherit}
.modes button.sel{border-color:var(--accent);background:#1f2a3a}
input{min-height:36px;border:1px solid var(--line);background:var(--panel);color:var(--text);border-radius:6px;padding:0 10px;font:inherit}
#list{overflow:auto;flex:1;padding:6px}
h2{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--dim);margin:12px 6px 4px}
.net{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;min-height:32px}
.net:hover{background:#1f2630}.net.sel{background:#1f2a3a;outline:1px solid var(--accent)}
.net i{width:14px;height:14px;border-radius:3px;flex:none;border:1px solid #ffffff55}
.net b{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.net span{margin-left:auto;color:var(--dim);font-size:12px;white-space:nowrap}
section{display:flex;flex-direction:column;min-width:0;min-height:0}
#info{padding:10px 14px;border-bottom:1px solid var(--line);min-height:64px}
#info .name{font-size:20px;font-weight:700;margin-right:10px}
#info .lv{font-weight:700;padding:2px 8px;border-radius:5px;background:#2c3036}
#info .why{color:var(--dim);font-size:13px;margin-top:2px}
#faces{flex:1;display:flex;gap:10px;padding:10px;min-height:0}
.face{flex:1;min-width:0;display:flex;flex-direction:column}
.face .cap{font-size:11px;letter-spacing:.05em;color:var(--dim);margin-bottom:4px;text-transform:uppercase}
.stack{position:relative;flex:1;min-height:0}
.stack img,.stack canvas{position:absolute;left:0;top:0;width:100%;height:100%;object-fit:contain}
.stack img{filter:grayscale(.72) brightness(.6)}
.stack canvas{cursor:crosshair;image-rendering:auto}
#legend{display:flex;flex-wrap:wrap;gap:14px;padding:8px 14px;border-top:1px solid var(--line);font-size:12px;color:var(--dim)}
#legend div{display:flex;align-items:center;gap:6px}
#legend canvas{width:18px;height:18px;border:1px solid #ffffff55}
</style>
</head>
<body>
<main>
<aside>
<div class="top">
<div class="modes"><button id="m-conf" class="sel">By confidence</button><button id="m-net">By net</button></div>
<input id="q" placeholder="Find a net" autocomplete="off">
</div>
<div id="list"></div>
</aside>
<section>
<div id="info"></div>
<div id="faces"></div>
<div id="legend"></div>
</section>
</main>
<script>
const D = /*DATA*/null/*END*/;
const MAPS = /*MAPS*/{}/*END*/;
const $ = id => document.getElementById(id);
const esc = s => String(s ?? "").replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">",'"':"""}[c]));
const RUNG = D.rungColors, HUES = D.netHues, LADDER = D.ladder;
const WORDS = {copper: "every pin seen joined by continuous copper", via: "pins joined, the weakest join through verified vias", inferred: "at least one pin or join rests on inference", group: "unnamed copper joined to other unnamed copper", none: "isolated copper: no pad, no valid via", conflict: "two nets meet and nothing settles it"};
let mode = "conf", sel = null, faces = [];
const rank = l => Math.max(0, LADDER.indexOf(l));
const byName = Object.fromEntries(D.nets.map(n => [n.name, n]));
// the level a piece is drawn at: the weaker of its own rung and its net's level
const idLevel = D.ids.map(e => e.u !== undefined ? e.u : rank((byName[e.n] || {}).level || "none"));
const idNet = D.ids.map(e => e.n || null);
const hueOf = {}; D.nets.forEach((n, i) => hueOf[n.name] = HUES[i % HUES.length]);
function pattern(k, x, y) {
const per = 10, m = (v, n) => ((v % n) + n) % n;
switch (k) {
case 0: return 1;
case 1: return m(y, per) < 6 ? 1 : 0.38;
case 2: return m(x + y, per) < 5 ? 1 : 0.28;
case 3: return (m(x, per) < 3 || m(y, per) < 3) ? 1 : 0.22;
case 4: return (m(x, per) < 4 && m(y, per) < 4) ? 1 : 0.3;
default: return m(x - y, 15) < 7 ? 1 : -1;
}
}
function loadFace(side) {
return new Promise(res => {
if (!MAPS[side]) return res(null);
const im = new Image();
im.onload = () => {
const c = document.createElement("canvas"); c.width = im.width; c.height = im.height;
const g = c.getContext("2d", {willReadFrequently: true}); g.drawImage(im, 0, 0);
res({side, w: im.width, h: im.height, map: g.getImageData(0, 0, im.width, im.height).data});
};
im.src = MAPS[side];
});
}
function paint(f) {
const g = f.canvas.getContext("2d"), out = g.createImageData(f.w, f.h), o = out.data, m = f.map;
const selId = sel ? D.ids.findIndex(e => e.n === sel) : -1;
for (let y = 0, i = 0; y < f.h; y++) for (let x = 0; x < f.w; x++, i += 4) {
const b = m[i + 2]; if (!b) continue;
const id = m[i] + (m[i + 1] << 8) - 1, rung = b - 1;
let c, a;
if (sel) {
if (id !== selId) continue;
if (mode === "conf") { const k = pattern(rung, x, y); c = k < 0 ? [12, 14, 18] : RUNG[rung]; a = 0.92 * Math.abs(k); } else { c = [255, 221, 60]; a = 0.92; }
} else if (mode === "conf") {
const lv = Math.max(rung, idLevel[id]), k = pattern(lv, x, y); c = k < 0 ? [12, 14, 18] : RUNG[lv]; a = 0.86 * Math.abs(k);
} else {
const n = idNet[id]; if (n && byName[n]) { c = hueOf[n]; a = 0.82; } else { c = [150, 150, 150]; a = 0.3; }
}
o[i] = c[0]; o[i + 1] = c[1]; o[i + 2] = c[2]; o[i + 3] = Math.round(255 * a);
}
g.putImageData(out, 0, 0);
if (!sel) return;
// the selected net's pins: filled when seen joined, hollow when on the net by inference only; the weakest link ringed
const n = byName[sel], s = D.res, bit = f.side === "top" ? 1 : 2, px = (x, y) => [(x - 100) * s, (y - 100) * s];
for (const p of (D.pins[sel] || [])) {
if (!(p[2] & bit)) continue;
const [x, y] = px(p[0], p[1]), r = 5;
g.lineWidth = 4; g.strokeStyle = "#0c0e12"; g.strokeRect(x - r, y - r, 2 * r, 2 * r);
if (p[3]) { g.fillStyle = "#fff"; g.fillRect(x - r, y - r, 2 * r, 2 * r); } else { g.lineWidth = 2; g.strokeStyle = "#fff"; g.strokeRect(x - r, y - r, 2 * r, 2 * r); }
}
if (n && n.at && n.level !== "copper" && (!n.side || n.side === f.side)) {
const [x, y] = px(n.at[0], n.at[1]);
g.beginPath(); g.arc(x, y, 16, 0, 6.3); g.lineWidth = 6; g.strokeStyle = "#0c0e12"; g.stroke(); g.lineWidth = 3; g.strokeStyle = "#ffeb5a"; g.stroke();
}
}
function list() {
const q = $("q").value.trim().toLowerCase();
const show = n => !q || n.name.toLowerCase().includes(q);
const row = n => `<div class="net ${sel === n.name ? "sel" : ""}" data-n="${esc(n.name)}" title="${esc(n.weakest || "")}"><i style="background:rgb(${(mode === "conf" ? RUNG[rank(n.level)] : hueOf[n.name]).join(",")})"></i><b>${esc(n.name)}</b><span>${mode === "conf" ? `${n.seen} of ${n.pins} pins` : n.level}</span></div>`;
let html = "";
if (mode === "conf") {
for (const l of ["copper", "via", "inferred", "none"]) {
const v = D.nets.filter(n => n.level === l); if (!v.length) continue;
const w = v.filter(show); if (!w.length) continue;
html += `<h2>${l}: ${v.length} net${v.length === 1 ? "" : "s"}</h2>` + w.map(row).join("");
}
html += `<h2>No net</h2><div class="net" style="cursor:default"><span style="margin:0">${D.unnamed.islands} isolated islands, ${D.unnamed.groups} groups, ${D.unnamed.conflicts} in conflict: drawn, not listed</span></div>`;
} else {
for (const s of D.sections) { const w = s.nets.map(x => byName[x]).filter(n => n && show(n)); if (w.length) html += `<h2>${esc(s.title)}</h2>` + w.map(row).join(""); }
}
$("list").innerHTML = html;
document.querySelectorAll(".net[data-n]").forEach(el => el.onclick = () => pick(el.dataset.n === sel ? null : el.dataset.n));
}
function info() {
const n = sel && byName[sel];
if (!n) { $("info").innerHTML = mode === "conf" ? `<span class="name">Net confidence</span><div class="why">Every piece of copper in the colour and pattern of how well its net is proven: a net is as proven as the weakest link joining its pins. Click a net in the list or on the board.</div>` : `<span class="name">Nets</span><div class="why">Each named net in its own hue, copper with no net in grey. Click a net in the list or on the board to light it on both faces.</div>`; return; }
const c = RUNG[rank(n.level)].join(",");
$("info").innerHTML = `<span class="name">${esc(n.name)}</span><span class="lv" style="color:rgb(${c})">${n.level}</span> ${n.pins} pin${n.pins === 1 ? "" : "s"}, ${n.seen} seen joined to its copper${n.disputes ? `, meets another net at ${n.disputes} place${n.disputes === 1 ? "" : "s"}` : ""} <a style="color:var(--accent)" href="../nets/net/${encodeURIComponent(n.file)}.jpg" target="_blank">picture</a><div class="why">Weakest link: ${esc(n.weakest || "none")}</div>`;
}
function legend() {
$("legend").innerHTML = (mode === "conf" || sel ? LADDER.map((l, k) => `<div><canvas width="18" height="18" data-k="${k}"></canvas>${l}: ${WORDS[l]}</div>`).join("") : `<div>each named net has its own hue; grey is copper with no net</div>`) + `<div>filled square: pin seen joined; hollow square: pin by inference only; yellow ring: weakest link; the bottom face is turned over to line up with the top</div>`;
document.querySelectorAll("#legend canvas").forEach(c => { const g = c.getContext("2d"), k = +c.dataset.k; g.fillStyle = "#2c3036"; g.fillRect(0, 0, 18, 18); for (let y = 0; y < 18; y++) for (let x = 0; x < 18; x++) { const p = pattern(k, x, y); g.fillStyle = p < 0 ? "#0c0e12" : `rgba(${RUNG[k].join(",")},${p})`; g.fillRect(x, y, 1, 1); } });
}
function pick(name) { sel = name; list(); info(); legend(); faces.forEach(paint); const el = document.querySelector(".net.sel"); if (el) el.scrollIntoView({block: "nearest"}); }
function setMode(m) { mode = m; $("m-conf").classList.toggle("sel", m === "conf"); $("m-net").classList.toggle("sel", m === "net"); pick(sel); }
(async () => {
faces = (await Promise.all(["top", "bottom"].map(loadFace))).filter(Boolean);
$("faces").innerHTML = faces.map(f => `<div class="face"><div class="cap">${f.side === "top" ? "Top face" : "Bottom face, turned over to line up with the top"}</div><div class="stack"><img src="../board/${f.side}.jpg" alt=""><canvas id="c-${f.side}" width="${f.w}" height="${f.h}"></canvas></div></div>`).join("");
for (const f of faces) {
f.canvas = $("c-" + f.side);
f.canvas.onclick = ev => {
// the canvas is letterboxed in its box: map the click to a map pixel, then to the net under it
const r = f.canvas.getBoundingClientRect(), k = Math.min(r.width / f.w, r.height / f.h);
const x = Math.floor((ev.clientX - r.left - (r.width - f.w * k) / 2) / k), y = Math.floor((ev.clientY - r.top - (r.height - f.h * k) / 2) / k);
if (x < 0 || y < 0 || x >= f.w || y >= f.h) return pick(null);
const i = 4 * (y * f.w + x), id = f.map[i + 2] ? f.map[i] + (f.map[i + 1] << 8) - 1 : -1, n = id >= 0 ? idNet[id] : null;
pick(n && byName[n] && n !== sel ? n : null);
};
}
$("m-conf").onclick = () => setMode("conf"); $("m-net").onclick = () => setMode("net");
$("q").oninput = list;
document.addEventListener("keydown", e => { if (e.key === "Escape") pick(null); });
const h = new URLSearchParams(location.hash.slice(1)); if (h.get("mode") === "net") mode = "net";
setMode(mode); if (h.get("net") && byName[h.get("net")]) pick(h.get("net"));
})();
</script>
</body>
</html>