molecule
Evan — Adom Molecule LED Nameplate
Public Made by Adomby adom
Evan's name in 66 green 0603 LEDs on a 5x7 dot-matrix grid, USB-C powered off the east edge. A real Adom molecule: four corner machine pins, 96x44 mm absolute, every part from the Adom Basic Parts catalog.
main
John Lauer
Repoint docs at the adom-tsci guide and the wiki skillpack instead of the standalone page
6134435
2mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
<!doctype html><html><head><meta charset="utf-8">
<meta name="readme-max-height" content="32000">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
/* No @font-face here: the wiki renders README.html in a null-origin iframe,
so adom.inc's font files fail CORS on every load and only add console
noise. Fall back to the system stack, which the wiki chrome matches. */
:root { --bg:#0d1117; --surface:#161b22; --elev:#1c2128; --border:#30363d;
--text:#e6edf3; --body:#c9d1d9; --muted:#8b949e; --accent:#00b8b1; --purple:#8C6BF7; }
* { box-sizing:border-box; margin:0; padding:0; }
html,body { background:var(--bg); color:var(--body);
font:15px/1.65 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
scrollbar-width:none; }
html::-webkit-scrollbar, body::-webkit-scrollbar { width:0; height:0; display:none; }
#wrap { max-width:900px; margin:0 auto; padding:34px 22px 60px; }
h1 { color:var(--text); font-size:40px; line-height:1.1; letter-spacing:-.02em; margin:0 0 10px; }
h2 { color:var(--text); font-size:21px; letter-spacing:-.01em;
margin:38px 0 12px; padding-bottom:7px; border-bottom:1px solid var(--border); }
h3 { color:var(--text); font-size:16px; margin:22px 0 8px; }
p { margin:0 0 13px; }
.lede { font-size:17.5px; color:var(--text); margin-bottom:20px; }
a { color:var(--accent); }
code { font:13px/1.5 ui-monospace,"SF Mono",Menlo,Consolas,monospace;
background:var(--elev); border:1px solid var(--border);
border-radius:5px; padding:1px 5px; color:var(--text); }
pre { background:var(--elev); border:1px solid var(--border); border-radius:10px;
padding:14px 16px; overflow-x:auto; margin:0 0 16px; }
pre code { background:none; border:0; padding:0; font-size:12.5px; line-height:1.6; }
ul,ol { margin:0 0 14px; padding-left:22px; }
li { margin:0 0 6px; }
table { width:100%; border-collapse:collapse; margin:0 0 18px; font-size:13.5px; }
th,td { text-align:left; padding:8px 11px; border-bottom:1px solid var(--border); vertical-align:top; }
th { color:var(--muted); font-weight:600; white-space:nowrap; }
td:first-child { color:var(--text); white-space:nowrap; }
.viewer { border:1px solid var(--border); border-radius:12px; overflow:hidden;
background:var(--surface); margin:22px 0 8px;
box-shadow:0 12px 40px rgba(0,0,0,.45); }
.viewer iframe { display:block; width:100%; height:620px; border:0; }
.poster { display:block; width:100%; padding:0; border:0; background:none;
cursor:pointer; position:relative; font:inherit; color:inherit; }
.poster img { display:block; width:100%; height:auto; opacity:.55; transition:opacity .18s; }
.poster:hover img { opacity:.75; }
.play { position:absolute; inset:0; display:flex; flex-direction:column;
align-items:center; justify-content:center; gap:6px; text-align:center; }
.play b { background:var(--accent); color:#04120f; padding:10px 20px;
border-radius:999px; font-size:15px; font-weight:700; }
.play i { color:var(--text); font-style:normal; font-size:12.5px; opacity:.85; }
.cap { color:var(--muted); font-size:13px; margin:0 0 26px; }
.cap a { color:var(--accent); text-decoration:none; }
.call { background:var(--surface); border:1px solid var(--border);
border-left:3px solid var(--accent); border-radius:0 10px 10px 0;
padding:14px 18px; margin:0 0 22px; color:var(--body); }
.call b { color:var(--text); }
.warn { border-left-color:var(--purple); }
figure { margin:0 0 20px; }
figure img { display:block; width:100%; height:auto; border-radius:10px;
border:1px solid var(--border); background:#000; }
figcaption { color:var(--muted); font-size:12.5px; margin-top:7px; }
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
@media (max-width:680px) { .grid2 { grid-template-columns:1fr; } }
.muted { color:var(--muted); }
</style></head>
<body><div id="wrap">
<h1>Evan</h1>
<p class="lede">An Adom molecule LED nameplate. Evan's name is spelled out by 66 green
0603 LEDs on a 5 × 7 dot-matrix grid, powered from a USB-C receptacle on the east
edge. Plug it into a phone charger and it lights up.</p>
<div class="viewer" id="viewer">
<button class="poster" id="viewer-load" type="button">
<img src="https://wiki.adom.inc/adom/evan-led-nameplate/render/docs/hero.png" alt="Evan LED nameplate">
<span class="play"><b>Load interactive 3D viewer</b><i>Orbit, inspect, measure, x-ray the copper. About 15 MB.</i></span>
</button>
</div>
<p class="cap">The viewer loads when it scrolls into view, about 15 MB. The guided walkthrough starts on its own, seven steps, and the PCB / Schematic / Parts tabs are live. <a href="https://wiki.adom.inc/adom/evan-led-nameplate/render/viewer/index.html" target="_blank">Open it full screen</a>.</p>
<div class="call"><b>The one idea: the letters are the components.</b>
There is no display, no driver, no firmware. Each lit pixel of the bitmap is a physical
0603 LED placed at that coordinate, with its own 330 Ω resistor tucked 1.65 mm
below it. The board is a picture of the word made out of the parts that light it, and the
netlist is 66 identical two-part branches hanging off one 5 V rail.</div>
<h2>The board</h2>
<table>
<tr><th>Board</th><td>96 × 44 mm nominal, 96.4 × 44.4 mm real (nominal wing adds 0.2 mm per side), 2 layer</td></tr>
<tr><th>Molecule</th><td><code>type="4pin"</code>, <code>MachinePinMediumStandard</code>, absolute sizing, rounded edges</td></tr>
<tr><th>Letters</th><td>66 LEDs, 5 × 7 glyphs, 3 mm pixel pitch, 3 mm letter gap, 69 mm of text</td></tr>
<tr><th>Power in</th><td>USB-C on the east edge, mouth 0.4 mm proud of the board edge</td></tr>
<tr><th>Draw</th><td>~400 mA at 5 V, 2.0 W, all 66 LEDs lit</td></tr>
<tr><th>Components</th><td>141 total: 66 LEDs, 68 resistors, 2 capacitors, 1 connector, 4 machine pins</td></tr>
<tr><th>DRC</th><td>Zero errors in <code>circuit.json</code>. <code>adom-tsci lint</code>: routing, CAD models and USB-C geometry all pass; the one open item is the four machine-pin symbols, see below.</td></tr>
</table>
<h2>Bill of materials</h2>
<p>Every line is an <a href="https://wiki.adom.inc/adom/adom-basic-parts">Adom Basic Part</a>,
so the whole board builds off reels already loaded on the PnP line.</p>
<table>
<tr><th>Refdes</th><th>Qty</th><th>Part</th><th>Package</th><th>Notes</th></tr>
<tr><td>D0–D65</td><td>66</td><td><code>IN-S63BTG</code> Inolux</td><td>0603</td><td>Green InGaN, 527 nm dominant, Vf 3.2 V typ @ 20 mA, 560 mcd, 25 mA max</td></tr>
<tr><td>R0–R65</td><td>66</td><td><code>CR0402-FX-3300GLF</code> Bourns</td><td>0402</td><td>330 Ω 1%, one per LED, sets ~6 mA</td></tr>
<tr><td>J1</td><td>1</td><td><code>TYPE-C-31-M-12</code></td><td>SMD 16P</td><td>LCSC C165948. <a href="https://wiki.adom.inc/adom/type-c-31-m-12">Adom part page</a></td></tr>
<tr><td>R_CC1, R_CC2</td><td>2</td><td><code>CR0402-FX-4701GLF</code> Bourns</td><td>0402</td><td>4.7 kΩ Rd pulldowns, tell the charger to enable VBUS</td></tr>
<tr><td>C_BULK</td><td>1</td><td><code>CL21A106KOQNNNG</code> Samsung</td><td>0805</td><td>10 µF X5R, inrush when 66 LEDs light at once</td></tr>
<tr><td>C_HF</td><td>1</td><td><code>0402B104J160CT</code> Walsin</td><td>0402</td><td>100 nF X7R, high frequency decoupling</td></tr>
<tr><td>MP1–MP4</td><td>4</td><td><code>MachinePinMediumStandard</code></td><td>—</td><td>Workcell fixturing, mechanical only, no electrical function</td></tr>
</table>
<h2>How the letters are generated</h2>
<p><code>FONT</code> in <code>src/lib/index.tsx</code> holds one 5 × 7 glyph per character
and <code>TEXT = "EVAN"</code> selects them. A generator walks each glyph's lit pixels and
emits one <code><led></code> plus one <code><resistor></code> per pixel. Changing
the name is a one-line change; a longer name needs <code>BOARD_W</code> grown to keep the
16 mm USB-C reservation on the east edge.</p>
<div class="call warn"><b>Why 5 × 7 and not the 3 × 5 the earlier nameplates used.</b>
Three columns is not enough for an N. The diagonal has nowhere to go, so it fills the
middle and the glyph reads as H or M. Five columns gives the N a real diagonal, walking
column 1 to 2 to 3 down the glyph, and gives the V a real point. The cost is 66 LEDs
instead of 42.</div>
<h2>Power</h2>
<p><code>IN-S63BTG</code> is an InGaN part: Vf 3.2 V typ at 20 mA, dropping to about 3.0 V
at single-digit mA. From a 5 V rail:</p>
<pre><code>(5 V − 3.0 V) / 330 Ω = 6.1 mA per LED
66 LEDs × 6.1 mA = ~400 mA, 2.0 W</code></pre>
<p>That is inside what a source advertising Default USB Power will hand out, so it runs off
a phone charger without negotiation. At 560 mcd @ 20 mA these are bright parts, so 6 mA is
a comfortable desk brightness rather than a dim one.</p>
<p>There is deliberately no power LED. The letters are the power indicator: lit means 5 V is
present. And because each LED is fused by its own resistor, one LED dying does not affect
the others. Replace the LED and its 330 Ω pair.</p>
<h2>The USB-C body offset fix</h2>
<p>Worth recording, because it bit this build and will bite the next one.</p>
<p>The wrapper this board inherited placed the receptacle's 3D body 2.09 mm forward of its
own pad row, hanging 2.5 mm off the board edge. The cause is that tscircuit anchors
<code>cadModel.positionOffset</code> at <code>pcb_component.center</code>, which is the
bounding box of pads <b>and</b> holes, not the pad row. Every hole in this footprint sits on
the mouth side, so that centre is 2.7366 mm forward of the pads and the offset inherits the
error.</p>
<p><code>adom-tsci lint</code> does not catch it. It derives the mouth vector from
<code>cad.rotation.z</code>, so the error cancels out of its own check and it reports a
healthy <code>body_fwd</code>.</p>
<p>The constants are re-derived in <code>src/lib/UsbCReceptacle.tsx</code> as</p>
<pre><code>BODY_FORWARD_MAG = MODEL_ORIGIN_TO_BACK − FOOTPRINT_CENTER_FROM_PADS
USB_C_MOUTH_FROM_PADS = MODEL_ORIGIN_TO_BACK + MODEL_ORIGIN_TO_MOUTH</code></pre>
<p>with the model numbers measured off the J1 node of a <b>built <code>3d.glb</code></b>, the
artifact that actually gets rendered, so it cannot disagree with what you see. The result:
shell back face exactly on the pad row, mouth 0.4 mm proud, easternmost copper 1.68 mm
inside the edge.</p>
<p><code>src/scripts/check-usbc.mjs</code> re-runs that measurement against the GLB and exits
non-zero if either lands outside tolerance. Step 2 of the walkthrough above flies the camera
to the receptacle so you can see the seating for yourself.</p>
<h2>Schematic and layout</h2>
<div class="grid2">
<figure>
<img src="https://wiki.adom.inc/adom/evan-led-nameplate/render/docs/pcb.png" alt="PCB layout, copper and silkscreen">
<figcaption>Copper and silkscreen. The letters are the component field; J1 and its
support parts occupy the east 16 mm.</figcaption>
</figure>
<figure>
<img src="https://wiki.adom.inc/adom/evan-led-nameplate/render/docs/top-view.png" alt="Rendered top view of the assembled board">
<figcaption>Assembled top view. 66 green LEDs, USB-C cantilevered off the right edge.</figcaption>
</figure>
</div>
<p>Full vector versions:
<a href="https://wiki.adom.inc/adom/evan-led-nameplate/render/schematic.svg" target="_blank">schematic.svg</a>
(141 symbols) and
<a href="https://wiki.adom.inc/adom/evan-led-nameplate/render/pcb.svg" target="_blank">pcb.svg</a>.
Both are also tabs inside the interactive viewer above.</p>
<h2>Mechanical and molecule compliance</h2>
<ul>
<li><b>Absolute sizing is deliberate.</b> A molecule's wings grow the board past its nominal
size, and the USB-C mouth has to be positioned against an edge whose coordinate is a number
you can trust. <code>size="96x44 absolute"</code> gives a real outer edge of 96.4 × 44.4 mm.</li>
<li><b>Four corner machine pins</b>, <code>MachinePinMediumStandard</code>, fixture the board
into the standard Adom workcell. No electrical function.</li>
<li><b>No SMT pad or anchor hole overhangs.</b> Easternmost copper sits 1.68 mm inside the
edge; only the connector shell cantilevers.</li>
<li><b>The bottom silkscreen is a documentation panel.</b> Flip the board over and it explains
how to power it, how it works, the full BOM, how it was built, the workcell fixturing, and
what to do if an LED goes dark.</li>
</ul>
<h2>Known issues and lessons learned</h2>
<ul>
<li><b>The 1.65 mm resistor offset is load bearing.</b> Resistors sit 1.65 mm below their LED.
At 1.5, 1.55 or 1.6 mm the autorouter drops a via inside a pad's clearance, 0.053 to 0.083 mm
against a 0.1 mm minimum. If you change the pitch, the board size, or the part placement,
sweep this again and re-check <code>circuit.json</code> for
<code>pcb_pad_pad_clearance_error</code>.</li>
<li><b><code>traces: 208 routed / 210 declared</code> is not a failure.</b> The autorouter merges
some net members into shared routes without emitting a per-source-trace row. Every port still
lands on copper of the right net, so verify geometrically rather than by counting.</li>
<li><b>Do not set <code>schX</code>/<code>schY</code> on only some parts.</b> Giving J1 explicit
schematic coordinates disabled tscircuit's auto-layout for everything else and stacked all
141 symbols at the origin. Either place every part or place none.</li>
<li><b>The whole 3D viewer hung on one <code>localStorage</code> read.</b> The wiki serves this content under a CSP <code>sandbox</code> with no <code>allow-same-origin</code>, so the document has a null origin and any storage access throws a <code>SecurityError</code>. The viewer reads storage during init, and because <code>Adom3DViewer.init()</code> is not awaited, that throw became an unhandled rejection that abandoned everything after it: no light rig, no <code>.env</code> IBL, no render loop, no camera framing, and a <code>getCamera()</code> returning null, which in turn made every walkthrough camera fly bail on its first guard. Nothing was logged and <code>engine.getFps()</code> still reported 60. An in-memory storage shim ahead of the bundle fixes all of it at once. Two genuine CSP limits remain and are handled separately: GLB textures ship as <code>data:</code> URIs because <code>blob:</code> is refused, and the 13 MB bundle ships as parts because <code>render/</code> 413s over ~4 MB. Full writeup in the <a href="https://wiki.adom.inc/adom/adom-tsci/render/guides/export-wiki.md">export skill</a>.</li>
<li><b>MP1–MP4 share one schematic symbol, and cannot be separated.</b> Every other
part here has an explicit <code>schX</code>/<code>schY</code> slot, but the machine pins are
emitted by <code><Molecule></code> as
<code>jsx(MachinePin, {type, name, pcbX, pcbY})</code>, and
<code>MachinePinOrContact</code> in <code>@tsci/adom-inc.library</code> destructures only
<code>{portHints, pcbX, pcbY, name}</code>, dropping any schematic coordinates passed in. So
they stack at the origin and <code>adom-tsci lint</code> reports four overlapping symbols
that no caller can fix. Cosmetic on a board where the pins carry no net. Filed against the
molecule and library packages.</li>
<li><b>Mixing tscircuit versions breaks the build.</b> A stale nested <code>circuit-json</code>
under <code>@tscircuit/cli</code> produced <code>Export named
'getRotationBetweenPcbPin1Locations' not found</code>. The fix is a clean
<code>rm -rf node_modules bun.lock && bun install</code>, not a patch.</li>
</ul>
<h2>Variants</h2>
<ul>
<li><b>Different name.</b> <code>TEXT</code> plus the glyphs in <code>FONT</code> are the whole
design. Another four-letter name is a one-line change.</li>
<li><b>Different colour.</b> Swap <code>LED_MPN</code> for another Adom 0603 LED:
<code>IN-S63BTA</code> amber, <code>IN-S63BT5B</code> blue, <code>IN-S63BTR</code> red,
<code>IN-S63BT5UW</code> white. Re-derive <code>R_VALUE</code> for that part's forward voltage.
Red and amber are roughly 2.0 V parts and want about 470 Ω, not 330 Ω.</li>
</ul>
<h2>Reproducing it</h2>
<pre><code>tar xzf evan-led-nameplate-source.tar.gz
cd src && bun install
bunx tsci build lib/index.tsx --glbs --svgs --3d-png --pcb-png
node scripts/check-usbc.mjs # USB-C geometry, measured off the GLB
adom-tsci lint # DRC, routing, schematic overlap
adom-tsci start . --texture-resolution 4096
adom-tsci stop # when done, so tsci dev processes do not leak</code></pre>
<h2>Files</h2>
<p><b>Source, to recreate the molecule</b></p>
<ul>
<li><a href="https://wiki.adom.inc/adom/evan-led-nameplate/render/plan.md"><code>plan.md</code></a>, the original brief, every refinement, and what was deliberately left out</li>
<li><a href="https://wiki.adom.inc/adom/evan-led-nameplate/render/evan-led-nameplate-source.tar.gz"><code>evan-led-nameplate-source.tar.gz</code></a>, full project source: <code>lib/</code>, <code>scripts/</code>, configs, walkthrough</li>
</ul>
<p><b>Build outputs</b></p>
<ul>
<li><a href="https://wiki.adom.inc/adom/evan-led-nameplate/render/board.glb"><code>board.glb</code></a>, 3D model with a 4096 px baked board texture</li>
<li><code>pcb.svg</code>, copper, silkscreen and drill layers</li>
<li><code>schematic.svg</code>, full schematic, 141 symbols</li>
<li><code>viewer/</code>, the self-contained interactive viewer embedded above</li>
</ul>
<h2 class="muted" style="font-size:15px">Provenance</h2>
<p class="muted" style="font-size:13px">Authored in tscircuit (TSX) with
<a href="https://wiki.adom.inc/adom/adom-tsci">adom-tsci</a>, autorouted by the tscircuit
autorouter. <code>@tsci/adom-inc.molecule</code> 1.1.15,
<code>@tsci/adom-inc.library</code> 1.2.1, tscircuit 0.0.2375. Walkthrough generated by
<code>adom-tsci walkthrough-gen</code> from <code>circuit.json</code>. Part of the Adom LED
nameplate family alongside
<a href="https://wiki.adom.inc/adom/brianna-led-nameplate">brianna-led-nameplate</a> and
<a href="https://wiki.adom.inc/adom/alex-nameplate">alex-nameplate</a>.
Designed by Adom in Fort Worth, Texas. 2026-08-20.</p>
</div>
<script>
(function () {
// The wiki renders README.html inside an iframe that does not know our
// height, so it has to be told. Without this the page is clipped to the
// default frame height and everything below the 3D viewer is unreachable.
var wrap = document.getElementById('wrap');
var last = 0;
var host = document.getElementById('viewer');
var btn = document.getElementById('viewer-load');
var loaded = false;
function loadViewer() {
if (loaded || !host) return;
loaded = true;
var f = document.createElement('iframe');
f.src = 'https://wiki.adom.inc/adom/evan-led-nameplate/render/viewer/index.html';
f.title = 'Evan nameplate, interactive 3D';
f.setAttribute('allow', 'fullscreen');
f.addEventListener('load', post);
host.innerHTML = '';
host.appendChild(f);
post();
}
if (btn) btn.addEventListener('click', loadViewer);
// Auto-load once the poster scrolls into view. A click-to-load poster reads
// as "broken" to anyone who does not realise it is a button, and a synthetic
// click cannot reach it from outside the sandboxed frame either, so it was
// untestable. Deferring on visibility keeps the 15 MB off first paint
// without making the reader do anything.
// Defer until the page has settled. The wiki renders its OWN native 3D
// Model viewer above this README, and that fetches board.glb (~2.8 MB). If
// this iframe starts pulling its 15 MB of bundle parts at the same time, the
// native viewer's fetch is starved and it gives up with "Failed to load
// model — taking too long". Let the page finish first, then load on view.
function armViewerLoad() {
if (!host) return;
if (window.IntersectionObserver) {
var io = new IntersectionObserver(function (entries) {
for (var i = 0; i < entries.length; i++) {
if (entries[i].isIntersecting) { io.disconnect(); loadViewer(); return; }
}
}, { rootMargin: '100px' });
io.observe(host);
} else {
setTimeout(loadViewer, 1200);
}
}
if (document.readyState === 'complete') setTimeout(armViewerLoad, 2500);
else addEventListener('load', function () { setTimeout(armViewerLoad, 2500); });
function post() {
var h = wrap.offsetHeight + 24;
if (h !== last) { last = h; parent.postMessage({ type: 'readme-height', height: h }, '*'); }
}
post();
addEventListener('load', post);
addEventListener('resize', post);
// Images and the lazily-loaded viewer iframe land after first paint and each
// one changes our height, so re-post as they arrive.
document.querySelectorAll('img, iframe').forEach(function (el) {
el.addEventListener('load', post);
});
if (window.ResizeObserver) new ResizeObserver(post).observe(wrap);
[100, 400, 1200, 3000].forEach(function (t) { setTimeout(post, t); });
})();
</script>
</body></html>