main
Colby Knox 1.1.0: every molecule gets large machine pins on the 32 mm grid; gallery rebuilt; corrected notes on kicad-to-molecule c794385 4d ago
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Board to Molecule</title>
<style>
:root { --bg: #0d1117; --panel: #161b22; --panel2: #1c2330; --line: #2a3340; --text: #e6edf3; --muted: #8b98a8; --accent: #00b8b1; --gold: #f5c542; --ok: #3fb950; --bad: #f85149; --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
* { box-sizing: border-box; }
html, body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.55 Inter, system-ui, -apple-system, "Segoe UI", sans-serif; }
.wrap { max-width: 1180px; margin: 0 auto; padding: 8px 16px 40px; }
h1 { font-size: 30px; margin: 10px 0 4px; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 6px; border-top: 1px solid var(--line); }
h3 { font-size: 15px; margin: 18px 0 8px; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
p, li { color: #cdd6df; max-width: 900px; }
.lede { font-size: 17px; color: var(--text); }
a { color: var(--accent); }
code, .n { font-family: var(--mono); font-size: 13px; }
pre { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; overflow-x: auto; font: 13px/1.5 var(--mono); }
table { border-collapse: collapse; width: 100%; font-size: 14px; }
th, td { text-align: left; padding: 7px 9px; border-bottom: 1px solid var(--line); }
th { color: var(--muted); font-weight: 500; }
.pills { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0; }
.pill { border: 1px solid var(--line); border-radius: 999px; padding: 3px 12px; font-size: 13px; color: var(--muted); }
.pill b { color: var(--text); font-weight: 600; }
.tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 10px 0 14px; }
.tabs button { background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 8px 12px; font: inherit; font-size: 14px; cursor: pointer; min-height: 44px; text-align: left; }
.tabs button small { display: block; color: var(--muted); font-size: 12px; }
.tabs button.on { border-color: var(--accent); background: var(--panel2); }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; position: relative; }
.cap { position: absolute; left: 10px; top: 8px; font-size: 12px; color: var(--muted); background: rgba(22,27,34,.85); padding: 2px 8px; border-radius: 6px; z-index: 2; pointer-events: none; }
.cap b { color: var(--text); }
.card img { width: 100%; display: block; aspect-ratio: 1/1; object-fit: contain; background: #0b0f14; cursor: zoom-in; }
.card iframe { width: 100%; height: 400px; border: 0; display: block; background: #0b0f14; }
.load3d { display: flex; align-items: center; justify-content: center; height: 400px; }
.load3d button { background: var(--accent); color: #031614; border: 0; border-radius: 8px; padding: 10px 18px; font: inherit; font-weight: 600; cursor: pointer; min-height: 44px; }
.stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 8px; margin: 10px 0; }
.stat { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 9px 12px; }
.stat b { display: block; font-size: 17px; }
.stat span { color: var(--muted); font-size: 12px; }
.ok { color: var(--ok); } .bad { color: var(--bad); }
.note { color: var(--muted); font-size: 13px; }
.hub { width: 100%; height: auto; display: block; margin: 8px 0; }
@media (max-width: 760px) { .grid2, .grid3 { grid-template-columns: 1fr; } h1 { font-size: 24px; } }
</style>
</head>
<body><div class="wrap">

<h1>Board to Molecule</h1>
<p class="lede">Turn any KiCad, Fusion/EAGLE, or Altium board into an Adom molecule: wings around the board and four corner machine pins, MP1 to MP4, written back in the board's own format.</p>
<div class="pills">
  <span class="pill"><b>KiCad</b> .kicad_pcb (7, 8, 9, 10)</span>
  <span class="pill"><b>Fusion / EAGLE</b> .brd</span>
  <span class="pill"><b>Altium</b> .PcbDoc (native OLE2, no Altium needed)</span>
  <span class="pill"><b>Rust</b> single binary, most boards in under 100 ms</span>
</div>
<p>The original board never moves: every component, track, via, and pour stays byte-for-byte where it was. The tool replaces the outer outline with a grid-snapped rectangle and adds the four pins, so the workcell can pick, place, and contact the board. Every example below was converted by the CLI and then built into 3D through the real molecule pipeline (service-kicad STEP export, then step2glb molecule mode), and every molecule passed step2glb's anchoring gate.</p>

<h2 id="how">How it works</h2>
<p>Like <a href="https://wiki.adom.inc/adom/adom-lbr" target="_top">adom-lbr</a> does for parts, the tool puts one EDA-neutral description in the middle. Each reader turns its board into the same <b>molecule plan</b> (millimeters, Y up, MP1 front-left, counterclockwise from above), and each writer patches that plan back into its own format.</p>
<svg class="hub" viewBox="0 0 1000 250" xmlns="http://www.w3.org/2000/svg" font-family="Inter, system-ui, sans-serif" role="img" aria-label="Readers for KiCad, Fusion and Altium feed one molecule plan, which writers apply back to each format">
  <defs><marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0L10 5L0 10z" fill="#8b98a8"/></marker></defs>
  <g font-size="15" fill="#e6edf3">
    <rect x="10" y="20" width="200" height="54" rx="10" fill="#161b22" stroke="#2a3340"/><text x="110" y="52" text-anchor="middle">KiCad .kicad_pcb</text>
    <rect x="10" y="98" width="200" height="54" rx="10" fill="#161b22" stroke="#2a3340"/><text x="110" y="130" text-anchor="middle">Fusion / EAGLE .brd</text>
    <rect x="10" y="176" width="200" height="54" rx="10" fill="#161b22" stroke="#2a3340"/><text x="110" y="208" text-anchor="middle">Altium .PcbDoc</text>
    <rect x="370" y="70" width="260" height="110" rx="12" fill="#0e3b36" stroke="#00b8b1" stroke-width="2"/>
    <text x="500" y="112" text-anchor="middle" font-weight="600" font-size="17">Molecule plan</text>
    <text x="500" y="136" text-anchor="middle" fill="#8b98a8" font-size="13">outline, grid, pin size, MP1-MP4</text>
    <text x="500" y="156" text-anchor="middle" fill="#8b98a8" font-size="13">mm, Y up, EDA-neutral</text>
    <rect x="790" y="20" width="200" height="54" rx="10" fill="#161b22" stroke="#2a3340"/><text x="890" y="52" text-anchor="middle">.molecule.kicad_pcb</text>
    <rect x="790" y="98" width="200" height="54" rx="10" fill="#161b22" stroke="#2a3340"/><text x="890" y="130" text-anchor="middle">.molecule.brd</text>
    <rect x="790" y="176" width="200" height="54" rx="10" fill="#161b22" stroke="#2a3340"/><text x="890" y="208" text-anchor="middle">.molecule.PcbDoc</text>
  </g>
  <g stroke="#8b98a8" stroke-width="1.6" fill="none" marker-end="url(#ar)">
    <path d="M212 47 C300 47 300 110 366 110"/><path d="M212 125 L366 125"/><path d="M212 203 C300 203 300 140 366 140"/>
    <path d="M632 110 C700 110 700 47 786 47"/><path d="M632 125 L786 125"/><path d="M632 140 C700 140 700 203 786 203"/>
  </g>
  <text x="290" y="242" fill="#8b98a8" font-size="12" text-anchor="middle">read (outline, copper, parts)</text>
  <text x="710" y="242" fill="#8b98a8" font-size="12" text-anchor="middle">write in place, rest byte-identical</text>
</svg>
<table>
  <tr><th>Pins</th><th>Grid</th><th>Board edge to pin center</th><th>Pad / drill</th><th>Fusion package</th><th>KiCad / Altium footprint</th></tr>
  <tr><td>large machine pins, always</td><td class=n>32 mm</td><td class=n>3 mm</td><td class=n>5.2 / 3.45 mm</td><td><code>MACH_PIN_LRG_SHORT</code></td><td><code>MachinePinLargeShort</code></td></tr>
</table>
<p class="note">Each pin-to-pin span rounds up to a whole number of 32 mm steps (at least one), so a 10 x 10 mm board becomes 38 x 38 mm and a 35 x 85 mm board becomes 70 x 102 mm. The new outline is centered on the original. Inner cutouts (slots, mounting-hole rings) are kept; the outer outline, including notches in it, becomes the rectangle.</p>

<h2 id="examples">Examples: before and after</h2>
<p>Pick a board. The 3D panes are the wiki's own Adom 3D viewer, loading the GLBs from this page; the 2D panes are rendered by the tool. The plan view shows the original outline dashed, the wings hatched, and the pins in gold. The after view is read back from the written file, not drawn from the plan.</p>
<div class="tabs" id="tabs"></div>
<div id="ex"></div>

<h2 id="results">Results</h2>
<table>
  <tr><th>Board</th><th>Source</th><th>Original (mm)</th><th>Molecule (mm)</th><th>Grid steps</th><th>Parts</th><th>Convert</th><th>Anchored</th></tr>
  <tr><td>BMA400 breakout</td><td>KiCad</td><td class=n>10.4 x 10.4</td><td class=n>38.0 x 38.0</td><td class=n>1 x 1</td><td class=n>13</td><td class=n>10 ms</td><td>yes</td></tr><tr><td>Vimdrones ESC dev board</td><td>KiCad</td><td class=n>35.0 x 85.0</td><td class=n>70.0 x 102.0</td><td class=n>2 x 3</td><td class=n>95</td><td class=n>96 ms</td><td>yes</td></tr><tr><td>SparkFun Qwiic Button</td><td>Fusion / EAGLE</td><td class=n>25.4 x 25.4</td><td class=n>38.0 x 38.0</td><td class=n>1 x 1</td><td class=n>48</td><td class=n>29 ms</td><td>yes</td></tr><tr><td>SparkFun RedBoard</td><td>Fusion / EAGLE</td><td class=n>68.6 x 53.3</td><td class=n>102.0 x 70.0</td><td class=n>3 x 2</td><td class=n>65</td><td class=n>315 ms</td><td>yes</td></tr><tr><td>miniFOC driver</td><td>Altium</td><td class=n>40.0 x 40.0</td><td class=n>70.0 x 70.0</td><td class=n>2 x 2</td><td class=n>41</td><td class=n>144 ms</td><td>yes</td></tr><tr><td>Elk Pi audio board</td><td>Altium</td><td class=n>136.2 x 93.7</td><td class=n>166.0 x 102.0</td><td class=n>5 x 3</td><td class=n>440</td><td class=n>179 ms</td><td>yes</td></tr>
</table>
<p class="note">Convert time is read, plan, write, and read back, on one core of a shared container. Anchored is step2glb molecule mode on the GLB built from the written molecule: MP1-MP4 found, board Z-up with MP1 at the origin.</p>

<h2 id="per-eda">What each lane does</h2>
<h3>KiCad</h3>
<ul>
  <li>Writes the pin footprint in the dialect the board was saved in: KiCad 7 (<code>fp_text</code>, <code>tstamp</code>), 8 (properties, <code>uuid</code>), or 9+ (embedded files). A KiCad 8 board never receives KiCad 9 tokens.</li>
  <li>On KiCad 9+ boards the pin STEP is embedded inside the <code>.kicad_pcb</code> (<code>kicad-embed://</code>), so the pins render on any machine, including service-kicad, which only ever sees the board file. Older boards get a <code>3dmodels/</code> folder beside the board.</li>
  <li>Outer outlines made of lines, arcs, rectangles, polygons, or circles are all recognized; inner cutouts stay.</li>
</ul>
<h3>Fusion / EAGLE</h3>
<ul>
  <li>Edits the XML in place: the Dimension-layer outline, a new library, and four elements.</li>
  <li>The pins are <code>MACH_PIN_LRG_SHORT</code> copied verbatim from <code>Adom_Library_Master</code> (with their 3D package URNs), so Fusion shows the real pin model and step2glb reads the pin size from the name.</li>
  <li>Refuses to run if the board already has parts named MP1 to MP4.</li>
</ul>
<h3>Altium</h3>
<ul>
  <li>Reads and writes the binary <code>.PcbDoc</code> directly, no Altium needed. Only the streams it changes are rewritten (outline, board regions, components, pads, texts, wide strings, unique ids); every other stream stays byte-identical.</li>
  <li>New pad records are cloned from a plated through-hole pad on the same board, so they match that file's Altium version; record counts in every header are kept consistent.</li>
  <li>Reads Altium's embedded STEP models, so the 3D previews show the real component bodies.</li>
</ul>

<h2 id="use">Use</h2>
<pre>adom-wiki pkg install adom/board-to-molecule     # Linux binary + skill; old kicad-to-molecule command kept as a shim

board-to-molecule convert board.kicad_pcb         # or board.brd, board.PcbDoc
board-to-molecule plan board.brd                  # the EDA-neutral plan as JSON, writes nothing
board-to-molecule convert board.PcbDoc --vcut     # add V-cut score lines along the original outline
board-to-molecule showcase board.PcbDoc -o out/   # molecule + SVGs + before/after GLBs (step2glb)
board-to-molecule serve out-dir/                  # the before/after viewer in a Hydrogen webview</pre>
<p>Output lands next to the input as <code>&lt;board&gt;.molecule.&lt;ext&gt;</code>. Every convert reads its own output back and fails if the written outline does not match the plan. CLI answers follow the Adom contract: <code>OK:</code> or <code>ERROR:</code> plus a <code>Hint:</code>.</p>

<h2 id="kicad-to-molecule">What changed from kicad-to-molecule</h2>
<p>This replaces <code>kicad-to-molecule</code> 0.5.0 (Python, KiCad only). The outline and pin placement math carry over unchanged; what is different:</p>
<ul>
  <li><b>One rule for every board:</b> large machine pins on the 32 mm grid. The old tool picked medium pins on an 8 mm grid for boards up to 60 mm and large pins on a 16 mm grid above that.</li>
  <li><b>Headless 3D export.</b> The old KiCad output referenced the pin model as <code>${KIPRJMOD}/3dmodels/...</code>, which resolves in desktop KiCad but not on service-kicad, which only sees the board file; a cloud STEP export of the old output had no pins, and step2glb reported <code>molecule_anchored: false</code>. On KiCad 9+ boards the pin STEP is now embedded in the board.</li>
  <li><b>KiCad 8 boards</b> received KiCad 9 tokens (<code>embedded_fonts</code>, a KiCad 9 version stamp inside the footprint). The pin footprint is now written in the board's own dialect.</li>
  <li><b>KiCad 7 boards got contact-sized pads</b> (1.3 / 0.78 and 4.4 / 2.62 mm) instead of machine-pin pads.</li>
  <li><b>Polygon and circle outlines were not stripped</b>, leaving the old outline inside the new one, where KiCad treats it as a cutout.</li>
</ul>

<h2 id="limits">Limits</h2>
<ul>
  <li>The written Altium <code>.PcbDoc</code> files are verified by two independent parsers (one written from KiCad's Altium importer) and KiCad 10 accepts the pin pad record, but they have not yet been opened in Altium Designer itself. Open one and run DRC before relying on it.</li>
  <li>Fusion/EAGLE: the four pins are added to the board only. If the board is linked to a schematic, add MP1-MP4 there too (or open the board standalone) so forward/back annotation stays consistent.</li>
  <li>The 3D previews for Fusion boards show copper and silkscreen but no component bodies (Fusion keeps package 3D models in the cloud, not in the <code>.brd</code>).</li>
  <li>Only the four corner pins; intermediate edge contacts are not added.</li>
</ul>
<p class="note">Sample boards: KiCad samples from adom-inc/kicad-to-molecule; SparkFun Qwiic Button and RedBoard (CC BY-SA 4.0); HangX-Ma/miniFOC (Apache-2.0); elk-audio/elk-pi-hardware (CC BY-SA 4.0). Each example lists its source.</p>

</div>
<script>
const DATA = [{"id": "kicad-bma400", "title": "BMA400 breakout", "credit": "Bundled sample from adom-inc/kicad-to-molecule (KiCad 9 board).", "order": 1, "m": {"eda": "kicad", "plan": {"edge_offset_mm": 3.0, "frame": "y-up", "grid_mm": 32.0, "new_bbox": {"h": 38.0, "w": 38.0, "x": 5.0, "y": -43.0}, "notes": [], "orig_bbox": {"h": 10.4, "w": 10.4, "x": 18.8, "y": -29.2}, "pin": {"drill_mm": 3.45, "eagle_package": "MACH_PIN_LRG_SHORT", "footprint": "MachinePinLargeShort", "pad_mm": 5.2}, "pins": [{"ref": "MP1", "role": "front-left", "x": 8.0, "y": -40.0}, {"ref": "MP2", "role": "back-left", "x": 8.0, "y": -8.0}, {"ref": "MP3", "role": "back-right", "x": 40.0, "y": -8.0}, {"ref": "MP4", "role": "front-right", "x": 40.0, "y": -40.0}], "schema": "adom.board-to-molecule.plan/1", "size_label": "large", "source": {"eda": "kicad", "file": "BMA400.kicad_pcb"}, "units": "mm", "vcut": false, "wing_x_mm": 13.8, "wing_y_mm": 13.8}, "convert_ms": 9.952146, "board": {"components": 13, "embedded_models": 0, "pads": 31, "track_ends_on_copper": [36, 36], "tracks": 61, "vias": 6}, "files": {"molecule": "molecule.kicad_pcb", "source": "source.kicad_pcb"}, "after": {"molecule_anchored": true, "footprint_pins": 8.0, "contact_z": 5.6, "silkscreen_applied": true, "glb_bytes": 125216, "pin_size": "large"}, "before": {"glb_bytes": 65048}}}, {"id": "kicad-vimdrones-esc", "title": "Vimdrones ESC dev board", "credit": "Bundled sample from adom-inc/kicad-to-molecule (KiCad 8 board with internal cutouts).", "order": 2, "m": {"eda": "kicad", "plan": {"edge_offset_mm": 3.0, "frame": "y-up", "grid_mm": 32.0, "new_bbox": {"h": 102.0, "w": 70.0, "x": 115.0, "y": -133.5}, "notes": [], "orig_bbox": {"h": 85.0, "w": 35.0, "x": 132.5, "y": -125.0}, "pin": {"drill_mm": 3.45, "eagle_package": "MACH_PIN_LRG_SHORT", "footprint": "MachinePinLargeShort", "pad_mm": 5.2}, "pins": [{"ref": "MP1", "role": "front-left", "x": 118.0, "y": -130.5}, {"ref": "MP2", "role": "back-left", "x": 118.0, "y": -34.5}, {"ref": "MP3", "role": "back-right", "x": 182.0, "y": -34.5}, {"ref": "MP4", "role": "front-right", "x": 182.0, "y": -130.5}], "schema": "adom.board-to-molecule.plan/1", "size_label": "large", "source": {"eda": "kicad", "file": "vimdrones_esc_development_board.kicad_pcb"}, "units": "mm", "vcut": false, "wing_x_mm": 17.5, "wing_y_mm": 8.5}, "convert_ms": 96.007628, "board": {"components": 95, "embedded_models": 0, "pads": 341, "track_ends_on_copper": [225, 228], "tracks": 425, "vias": 292}, "files": {"molecule": "molecule.kicad_pcb", "source": "source.kicad_pcb"}, "after": {"molecule_anchored": true, "footprint_pins": 4.0, "contact_z": 5.6, "silkscreen_applied": true, "glb_bytes": 568904, "pin_size": "large"}, "before": {"glb_bytes": 474384}}}, {"id": "fusion-qwiic-button", "title": "SparkFun Qwiic Button", "credit": "<a href=\"https://github.com/sparkfun/Qwiic_Button\">sparkfun/Qwiic_Button</a>, CC BY-SA 4.0 (EAGLE 9.5 board, opens in Fusion Electronics).", "order": 1, "m": {"eda": "eagle", "plan": {"edge_offset_mm": 3.0, "frame": "y-up", "grid_mm": 32.0, "new_bbox": {"h": 38.0, "w": 38.0, "x": -6.3, "y": -6.3}, "notes": [], "orig_bbox": {"h": 25.4, "w": 25.4, "x": 0.0, "y": 0.0}, "pin": {"drill_mm": 3.45, "eagle_package": "MACH_PIN_LRG_SHORT", "footprint": "MachinePinLargeShort", "pad_mm": 5.2}, "pins": [{"ref": "MP1", "role": "front-left", "x": -3.3, "y": -3.3}, {"ref": "MP2", "role": "back-left", "x": -3.3, "y": 28.7}, {"ref": "MP3", "role": "back-right", "x": 28.7, "y": 28.7}, {"ref": "MP4", "role": "front-right", "x": 28.7, "y": -3.3}], "schema": "adom.board-to-molecule.plan/1", "size_label": "large", "source": {"eda": "eagle", "file": "qwiic_button.brd"}, "units": "mm", "vcut": false, "wing_x_mm": 6.3, "wing_y_mm": 6.3}, "convert_ms": 28.815862000000003, "board": {"components": 48, "embedded_models": 0, "pads": 74, "track_ends_on_copper": [97, 97], "tracks": 193, "vias": 38}, "files": {"molecule": "molecule.brd", "source": "source.brd"}, "after": {"molecule_anchored": true, "footprint_pins": 4.0, "contact_z": 5.6, "silkscreen_applied": true, "glb_bytes": 262028, "pin_size": "large"}, "before": {"glb_bytes": 193748}}}, {"id": "fusion-redboard", "title": "SparkFun RedBoard", "credit": "<a href=\"https://github.com/sparkfun/RedBoard\">sparkfun/RedBoard</a>, CC BY-SA 4.0 (EAGLE 7.7 board, opens in Fusion Electronics).", "order": 2, "m": {"eda": "eagle", "plan": {"edge_offset_mm": 3.0, "frame": "y-up", "grid_mm": 32.0, "new_bbox": {"h": 70.0, "w": 102.0, "x": -16.71, "y": -8.33}, "notes": [], "orig_bbox": {"h": 53.34, "w": 68.58, "x": 0.0, "y": 0.0}, "pin": {"drill_mm": 3.45, "eagle_package": "MACH_PIN_LRG_SHORT", "footprint": "MachinePinLargeShort", "pad_mm": 5.2}, "pins": [{"ref": "MP1", "role": "front-left", "x": -13.71, "y": -5.33}, {"ref": "MP2", "role": "back-left", "x": -13.71, "y": 58.67}, {"ref": "MP3", "role": "back-right", "x": 82.29, "y": 58.67}, {"ref": "MP4", "role": "front-right", "x": 82.29, "y": -5.33}], "schema": "adom.board-to-molecule.plan/1", "size_label": "large", "source": {"eda": "eagle", "file": "redboard.brd"}, "units": "mm", "vcut": false, "wing_x_mm": 16.71, "wing_y_mm": 8.33}, "convert_ms": 314.986027, "board": {"components": 65, "embedded_models": 0, "pads": 233, "track_ends_on_copper": [203, 208], "tracks": 424, "vias": 70}, "files": {"molecule": "molecule.brd", "source": "source.brd"}, "after": {"molecule_anchored": true, "footprint_pins": 4.0, "contact_z": 5.6, "silkscreen_applied": true, "glb_bytes": 556004, "pin_size": "large"}, "before": {"glb_bytes": 478136}}}, {"id": "altium-minifoc", "title": "miniFOC driver", "credit": "<a href=\"https://github.com/HangX-Ma/miniFOC\">HangX-Ma/miniFOC</a>, Apache-2.0 (Altium PcbDoc with 26 embedded STEP models).", "order": 1, "m": {"eda": "altium", "plan": {"edge_offset_mm": 3.0, "frame": "y-up", "grid_mm": 32.0, "new_bbox": {"h": 70.0, "w": 70.0, "x": 40.175049, "y": 55.868403}, "notes": [], "orig_bbox": {"h": 40.004807, "w": 39.95927, "x": 55.195414, "y": 70.866}, "pin": {"drill_mm": 3.45, "eagle_package": "MACH_PIN_LRG_SHORT", "footprint": "MachinePinLargeShort", "pad_mm": 5.2}, "pins": [{"ref": "MP1", "role": "front-left", "x": 43.175049, "y": 58.868403}, {"ref": "MP2", "role": "back-left", "x": 43.175049, "y": 122.868403}, {"ref": "MP3", "role": "back-right", "x": 107.175049, "y": 122.868403}, {"ref": "MP4", "role": "front-right", "x": 107.175049, "y": 58.868403}], "schema": "adom.board-to-molecule.plan/1", "size_label": "large", "source": {"eda": "altium", "file": "minifoc.PcbDoc"}, "units": "mm", "vcut": false, "wing_x_mm": 15.020365, "wing_y_mm": 14.997597}, "convert_ms": 143.985777, "board": {"components": 41, "embedded_models": 26, "pads": 143, "track_ends_on_copper": [87, 87], "tracks": 191, "vias": 242}, "files": {"molecule": "molecule.PcbDoc", "source": "source.PcbDoc"}, "after": {"molecule_anchored": true, "footprint_pins": 4.0, "contact_z": 5.6, "silkscreen_applied": true, "glb_bytes": 646900, "pin_size": "large"}, "before": {"glb_bytes": 580596}}}, {"id": "altium-elkpi", "title": "Elk Pi audio board", "credit": "<a href=\"https://github.com/elk-audio/elk-pi-hardware\">elk-audio/elk-pi-hardware</a>, CC BY-SA 4.0 (Altium PcbDoc, 440 components).", "order": 2, "m": {"eda": "altium", "plan": {"edge_offset_mm": 3.0, "frame": "y-up", "grid_mm": 32.0, "new_bbox": {"h": 102.0, "w": 166.0, "x": 39.859052, "y": 37.038784}, "notes": [], "orig_bbox": {"h": 93.701111, "w": 136.203555, "x": 54.757274, "y": 41.188229}, "pin": {"drill_mm": 3.45, "eagle_package": "MACH_PIN_LRG_SHORT", "footprint": "MachinePinLargeShort", "pad_mm": 5.2}, "pins": [{"ref": "MP1", "role": "front-left", "x": 42.859052, "y": 40.038784}, {"ref": "MP2", "role": "back-left", "x": 42.859052, "y": 136.038784}, {"ref": "MP3", "role": "back-right", "x": 202.859052, "y": 136.038784}, {"ref": "MP4", "role": "front-right", "x": 202.859052, "y": 40.038784}], "schema": "adom.board-to-molecule.plan/1", "size_label": "large", "source": {"eda": "altium", "file": "elkpi.PcbDoc"}, "units": "mm", "vcut": false, "wing_x_mm": 14.898222, "wing_y_mm": 4.149445}, "convert_ms": 178.555998, "board": {"components": 440, "embedded_models": 28, "pads": 1550, "track_ends_on_copper": [1673, 1684], "tracks": 5104, "vias": 499}, "files": {"molecule": "molecule.PcbDoc", "source": "source.PcbDoc"}, "after": {"molecule_anchored": true, "footprint_pins": 4.0, "contact_z": 5.6, "silkscreen_applied": true, "glb_bytes": 3645012, "pin_size": "large"}, "before": {"glb_bytes": 3638460}}}];
const BASE = 'https://wiki.adom.inc/adom/board-to-molecule/render', VIEWER = 'https://wiki.adom.inc/viewer/3d/adom/app/board-to-molecule';
const EDA = { kicad: 'KiCad', eagle: 'Fusion / EAGLE', altium: 'Altium' };
const NATIVE = { kicad: '.kicad_pcb', eagle: '.brd', altium: '.PcbDoc' };
const fmt = (v, d = 1) => v == null ? '-' : Number(v).toFixed(d);
// Pin-to-pin span in 32 mm grid steps, MP1 to MP4 across and MP1 to MP2 deep.
const steps = p => `${Math.round((p.pins[3].x - p.pins[0].x) / p.grid_mm)} x ${Math.round((p.pins[1].y - p.pins[0].y) / p.grid_mm)}`;
function fit() { parent.postMessage({ type: 'readme-height', height: document.documentElement.scrollHeight }, '*'); }
addEventListener('load', fit);
new ResizeObserver(fit).observe(document.documentElement);
function lightbox(src) { parent.postMessage({ type: 'readme-lightbox', src }, '*'); }

function show(id) {
  const e = DATA.find(x => x.id === id) || DATA[0];
  document.querySelectorAll('#tabs button').forEach(b => b.classList.toggle('on', b.dataset.id === e.id));
  const m = e.m, p = m.plan, a = m.after, base = `${BASE}/gallery/${e.id}/`;
  const anch = a.molecule_anchored === true;
  const view = f => `${VIEWER}?model=${encodeURIComponent('gallery/' + e.id + '/' + f)}`;
  document.getElementById('ex').innerHTML = `
    <div class="pills"><span class="pill"><b>${EDA[m.eda]}</b> ${NATIVE[m.eda]}</span>
      <span class="pill ${anch ? 'ok' : 'bad'}">${anch ? 'anchored by step2glb' : 'not anchored'}</span>
      <span class="pill">${fmt(m.convert_ms, 0)} ms convert</span></div>
    <div class="note">${e.credit}</div>
    <div class="stats">
      <div class="stat"><b>${fmt(p.orig_bbox.w)} x ${fmt(p.orig_bbox.h)}</b><span>original, mm</span></div>
      <div class="stat"><b>${fmt(p.new_bbox.w)} x ${fmt(p.new_bbox.h)}</b><span>molecule, mm</span></div>
      <div class="stat"><b>${steps(p)}</b><span>32 mm grid steps</span></div>
      <div class="stat"><b>${p.grid_mm} mm</b><span>grid, ${p.edge_offset_mm} mm edge</span></div>
      <div class="stat"><b>${m.board.components}</b><span>parts, ${m.board.pads} pads</span></div>
      <div class="stat"><b>${a.contact_z ?? '-'} mm</b><span>contact Z (step2glb)</span></div>
    </div>
    <h3>3D</h3>
    <div class="grid2">
      <div class="card"><div class="cap"><b>Before</b> original</div><div class="load3d" data-src="${view('before.glb')}"><button>Load 3D (${fmt((m.before.glb_bytes || 0) / 1048576, 1)} MB)</button></div></div>
      <div class="card"><div class="cap"><b>After</b> molecule</div><div class="load3d" data-src="${view('after.glb')}"><button>Load 3D (${fmt((a.glb_bytes || 0) / 1048576, 1)} MB)</button></div></div>
    </div>
    <h3>2D</h3>
    <div class="grid3">
      <div class="card"><div class="cap"><b>Before</b></div><img loading="lazy" src="${base}before.svg" alt="original board"></div>
      <div class="card"><div class="cap"><b>Plan</b> wings and pins</div><img loading="lazy" src="${base}overlay.svg" alt="molecule plan over the original"></div>
      <div class="card"><div class="cap"><b>After</b> read back</div><img loading="lazy" src="${base}after.svg" alt="molecule read back from the written file"></div>
    </div>
    <h3>Pins</h3>
    <table><tr><th>Ref</th><th>Role</th><th>X (mm)</th><th>Y (mm)</th></tr>
      ${p.pins.map(q => `<tr><td>${q.ref}</td><td>${q.role}</td><td class=n>${fmt(q.x, 3)}</td><td class=n>${fmt(q.y, 3)}</td></tr>`).join('')}</table>
    <p class="note">Files in this page's repo: <code>gallery/${e.id}/</code> holds the source board, the molecule (<code>${m.files.molecule}</code>), <code>plan.json</code>, both GLBs, and the SVGs.</p>`;
  document.querySelectorAll('#ex .card img').forEach(img => img.onclick = () => lightbox(img.src));
  document.querySelectorAll('#ex .load3d button').forEach(b => b.onclick = () => {
    // Both panes load together so the pair compares side by side.
    document.querySelectorAll('#ex .load3d').forEach(box => {
      const f = document.createElement('iframe');
      f.src = box.dataset.src; f.title = 'Adom 3D viewer'; f.allow = 'fullscreen';
      box.replaceWith(f);
    });
  });
  fit();
}
document.getElementById('tabs').innerHTML = DATA.map(e => `<button data-id="${e.id}">${e.title}<small>${EDA[e.m.eda]}, ${steps(e.m.plan)} grid steps</small></button>`).join('');
document.querySelectorAll('#tabs button').forEach(b => b.onclick = () => show(b.dataset.id));
show(DATA[0].id);
</script>
</body>
</html>