---
name: board-creator
description: Create interactive board layout visualizations for Adom molecules and PCB designs. 3D view uses a Python-built GLB with real STEP machine pin/contact geometry displayed in the Gallia Viewer Babylon.js 3D viewer. 2D view renders as interactive SVG HTML. Use when the user asks to "show the board", "create a board layout", "visualize the board", "board viewer", "3D board view", "2D board view", "PCB visualization", or wants to see an interactive PCB board visualization in the Gallia Viewer viewer.
---

# Board Layout Creator

Create interactive board layout visualizations and display them in the Gallia Viewer viewer. The **3D view** generates a GLB model using real STEP geometry for machine pins/contacts (from `adom-tsci-library`) with standardized pipeline PBR materials, displayed in the built-in Gallia Viewer Babylon.js 3D viewer. The **2D view** renders an interactive SVG HTML file. These are visual board viewers — not KiCad `.kicad_pcb` files.

## KiCad Service

DRC validation uses the remote KiCad CLI service — KiCad is NOT installed locally in user containers. The service URL is configured via the `KICAD_API` environment variable (default: `http://127.0.0.1:8780`).

API client: `/home/adom/gallia/viewer/kicad-api-client.js`

## When to Use

- User asks to "show the board" or "visualize the board layout"
- User wants to see a 3D or 2D view of a molecule/PCB design
- After creating a molecule footprint, user wants to see the physical layout
- User asks for a "board viewer" or "PCB visualization"
- User wants an interactive view with hover tooltips, orbit controls, or pan/zoom

## Output

Files saved to the project boards directory:

```
/home/adom/project/project-content/schematics/boards/BOARD_NAME/
  BOARD_NAME.glb                # 3D GLB model (real STEP pins, pipeline PBR materials)
  build_3d_model.py             # Python script to rebuild the GLB
  BOARD_NAME-board-2d.html      # Interactive 2D SVG top-down viewer
  BOARD_NAME-footprint.json     # Footprint data (copy or reference)
```

- **3D view:** Display via `gv_3d_display` (Gallia Viewer Babylon.js viewer)
- **2D view:** Display via `gv_display_file` (HTML in Gallia Viewer iframe)

## Input Data

The board viewer is driven by a footprint JSON object. If the user has a `_footprint.json` file, read it. Otherwise, construct the data from the user's design specs.

### Footprint JSON Schema

```json
{
  "name": "Board_Name_v1",
  "version": "v1",
  "board": {
    "width_mm": 32.0,
    "height_mm": 32.0,
    "size_param": "30x30",
    "pin_type": "MachinePinMediumStandard",
    "contact_type": "MachineContactMedium",
    "mol_type": "4pin",
    "border_radius": 1.2
  },
  "machine_pins": [
    { "name": "MP1", "x": -15.0, "y": -15.0, "drill": 1.1, "pad": 1.6 }
  ],
  "contacts": [
    { "name": "GPIO0", "margin": "leftMargin", "x": -15.0, "y": -9.0, "drill": 0.78, "pad": 1.3, "pitch": 2.0 }
  ],
  "margins": [
    { "name": "leftMargin", "pcbX": -15.0, "pcbY": 0, "width": 2.0, "height": 28.0 }
  ]
}
```

### Components Array (for 3D/2D rendering)

In addition to the footprint JSON, define a components array for on-board ICs, passives, and connectors:

```javascript
const COMPONENTS = [
  { name: "RP2350B", ref: "U1", pkg: "QFN-80", x: 0, y: 0, w: 7, h: 7, d: 1.0, color: "#1a237e" },
  { name: "W25Q128", ref: "U2", pkg: "SOIC-8", x: 6.5, y: -3, w: 5, h: 4, d: 0.8, color: "#1b5e20" },
  // ... more components
];
```

## Hydrogen Color Scheme

All board visualizations MUST match the Hydrogen dark theme.

### Backgrounds & Borders
| Element | Color |
|---------|-------|
| Body background | `#0f1218` |
| Info panel background | `rgba(13, 17, 23, 0.95)` |
| Panel border | `#21262d` |
| Board fill (PCB green) | `#0d5016` (2D) or pipeline FR4 `rgb(0.034, 0.105, 0.033)` (3D) |
| Board stroke | `#1b8a2a` |
| Solder mask top | Pipeline FR4 material (3D) — see Standardized PBR Materials below |

### Text Colors
| Element | Color |
|---------|-------|
| Primary text | `#e6edf3` |
| Secondary text | `#7d8590` |
| Teal accent | `#00b8b1` |
| Info panel labels | `#7d8590` |

### Signal Colors (contacts)
| Signal Type | 2D Color | 3D Hex | Use For |
|-------------|----------|--------|---------|
| Power (+3V3, +5V, VIN) | `#f47067` | `0xe04040` | Supply rails |
| Ground (GND) | `#57ab5a` | `0x40c040` | Ground connections |
| GPIO | `#6cb6ff` | `0x4080e0` | General purpose I/O |
| Debug (SWDIO, SWCLK) | `#daaa3f` | `0xe0c830` | SWD debug pins |
| Special (RUN, ADC_VREF) | `#d2a8ff` | `0x9070c0` | Misc signals |

### Standardized PBR Materials (3D)

These are the **canonical pipeline materials** from `molecule-converter/scripts/compress_glb.py :: create_standard_materials()`. All board 3D views MUST use these exact values for consistency with the production Blender pipeline.

#### PCB Layer Materials

| Material | Name | Base Color (linear RGB) | Metallic | Roughness | IOR | Notes |
|----------|------|------------------------|----------|-----------|-----|-------|
| **FR4 PCB** | `Standard_FR4_PCB` | `(0.034, 0.105, 0.033)` | 0.200 | 0.350 | 1.000 | Solder mask green; HSV(0.333, 0.682, 0.105); sheenWeight=0.2 |
| **Tan Substrate** | `Standard_Tan_Substrate` | `(0.471, 0.431, 0.275)` | 0.000 | 0.200 | 1.329 | Internal PCB layer edges; HSV(0.133, 0.417, 0.471) |
| **Gold Machine Pin** | `Standard_Gold_Machine_Pin` | `(0.900, 0.599, 0.200)` | 1.000 | 0.077 | 50.000 | Machine pins and contacts; HSV(0.095, 0.778, 0.900) |
| **HASL Copper** | `Standard_HASL_Copper` | `(0.950, 0.950, 0.950)` | 1.000 | 0.150 | 1.450 | Exposed copper pads (silver finish) |

#### Three.js Equivalent

```javascript
// Canonical pipeline materials — use these exact values
const MATERIALS = {
  fr4: {
    color: new THREE.Color(0.034, 0.105, 0.033),  // Dark solder mask green
    metalness: 0.200,
    roughness: 0.350,
  },
  tanSubstrate: {
    color: new THREE.Color(0.471, 0.431, 0.275),  // PCB edge/internal tan
    metalness: 0.000,
    roughness: 0.200,
  },
  goldPin: {
    color: new THREE.Color(0.900, 0.599, 0.200),  // Gold machine pin
    metalness: 1.000,
    roughness: 0.077,
  },
  haslCopper: {
    color: new THREE.Color(0.950, 0.950, 0.950),  // Silver HASL finish
    metalness: 1.000,
    roughness: 0.150,
  },
};
```

#### Component Materials (non-pipeline, board-creator conventions)

| Element | Hex | Properties |
|---------|-----|------------|
| IC body (dark) | `0x1e1e28` | roughness: 0.15 |
| USB-C shell | `0x50505a` | metalness: 0.6, roughness: 0.25 |
| Button body | `0x463223` | roughness: 0.5 |
| Capacitor body | `0x503719` | roughness: 0.5 |

### Fonts
- UI text: `-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif`
- Technical text: `'SF Mono', 'Fira Code', 'Consolas', monospace`

## Machine Pin & Contact Specs

Reference dimensions from `adom-tsci-library/globals.ts`:

| Type | Hole ID | Hole OD | Bounding Box |
|------|---------|---------|--------------|
| MachinePinMediumShort | 1.1mm | 1.6mm | 2mm |
| MachinePinMediumStandard | 1.1mm | 1.6mm | 2mm |
| MachinePinLargeShort | 3.45mm | 5.2mm | 6mm |
| MachinePinLargeStandard | 3.45mm | 5.2mm | 6mm |
| MachineContactMedium | 0.78mm | 1.3mm | 2mm |
| MachineContactLarge | 2.62mm | 4.4mm | 6mm |

## 3D Board Viewer (GLB + Gallia Viewer Babylon.js)

**IMPORTANT:** The 3D board view MUST use the built-in Gallia Viewer Babylon.js 3D viewer (`gv_3d_display`), NOT a self-contained Three.js HTML file. This ensures real STEP geometry for machine pins/contacts, proper PBR materials, and a consistent viewer experience.

### Pipeline: Python GLB Builder

Generate a composite GLB model using `trimesh` in Python, loading actual STEP models from `adom-tsci-library/lib/3D_Models/` for machine pins and contacts. Reference implementation: `/home/adom/Molecule_RP2350B_Core_USB_v1/build_3d_model.py`.

```python
import trimesh
from trimesh.visual.material import PBRMaterial
import colorsys, json

STEP_DIR = "/home/adom/adom-tsci-library/lib/3D_Models"

# Load real STEP geometry (converted m → mm)
def load_step_mm(name):
    scene = trimesh.load(f"{STEP_DIR}/{name}.step")
    meshes = []
    for gname, geom in scene.geometry.items():
        geom.vertices *= 1000  # meters → mm
        meshes.append(geom)
    return meshes

pin_meshes = load_step_mm("MachinePinMediumStandard")
contact_meshes = load_step_mm("MachineContactMedium")
```

### Pipeline PBR Materials in Python

```python
def pipeline_pbr(h, s, v, metallic, roughness, name="mat"):
    r, g, b = colorsys.hsv_to_rgb(h, s, v)
    return PBRMaterial(baseColorFactor=[r, g, b, 1.0],
                       metallicFactor=metallic, roughnessFactor=roughness, name=name)

MAT_FR4       = pipeline_pbr(0.333, 0.682, 0.105, metallic=0.200, roughness=0.350, name="Standard_FR4_PCB")
MAT_GOLD_PIN  = pipeline_pbr(0.095, 0.778, 0.900, metallic=1.000, roughness=0.077, name="Standard_Gold_Machine_Pin")
MAT_HASL      = PBRMaterial(baseColorFactor=[0.95, 0.95, 0.95, 1.0],
                            metallicFactor=1.000, roughnessFactor=0.150, name="Standard_HASL_Copper")
MAT_SUBSTRATE = pipeline_pbr(0.133, 0.417, 0.471, metallic=0.000, roughness=0.200, name="Standard_Tan_Substrate")
```

### Building the Scene

```python
scene = trimesh.Scene()

# PCB board (box with FR4 material)
board = trimesh.creation.box(extents=[BOARD_W, BOARD_H, PCB_THICKNESS])
board.visual = trimesh.visual.TextureVisuals(material=MAT_FR4)
board.apply_translation([0, 0, -PCB_THICKNESS/2])
scene.add_geometry(board, node_name="PCB_Board")

# Real STEP machine pins at each corner position
for pin in footprint["machine_pins"]:
    for i, mesh in enumerate(pin_meshes):
        m = mesh.copy()
        m.visual = trimesh.visual.TextureVisuals(material=MAT_GOLD_PIN)
        m.apply_translation([pin["x"], pin["y"], 0])
        scene.add_geometry(m, node_name=f"Pin_{pin['name']}_{i}")

# Real STEP contacts at each edge position (color-coded by signal)
for idx, contact in enumerate(footprint["contacts"]):
    mat = contact_material(contact["name"])  # signal-based color
    for i, mesh in enumerate(contact_meshes):
        m = mesh.copy()
        m.visual = trimesh.visual.TextureVisuals(material=mat)
        m.apply_translation([contact["x"], contact["y"], 0])
        scene.add_geometry(m, node_name=f"Contact_{contact['name']}_{idx}_{i}")

# Components as boxes with appropriate materials
# ... (IC body, flash, LDO, USB-C, crystal, buttons, caps, LEDs)

# Export
scene.export("BOARD_NAME.glb", file_type='glb')
```

### Displaying in Gallia Viewer (required for 3D)

Always use `gv_3d_display` for the 3D view:

```
gv_3d_display(
  glb_path="/path/to/BOARD_NAME.glb",
  part_name="RP2350B Core+USB",
  manufacturer="Adom",
  package_type="Molecule 32x32mm",
  pad_count=60,
  body_size={ x: 32, y: 32, z: 1.6 },
  title="Board Name 3D"
)
```

This renders in the built-in Gallia Viewer Babylon.js viewer with orbit controls, shadows, PBR lighting, and proper material rendering.

### Fallback: Three.js HTML Template

A Three.js HTML template (`board-3d-template.html`) is included in this skill directory as a **fallback only** — use it when the user specifically requests a self-contained HTML file for sharing outside Gallia Viewer. It uses primitive cylinder geometry for pins (not real STEP models) and should not be the default 3D output.

## 2D Board Viewer (SVG)

Generate a self-contained HTML file with dynamic SVG rendering. Reference template: `board-2d-template.html` in this skill directory.

### Rendering Approach

Use JavaScript to build SVG string and inject into a container div. Re-render on pan/zoom/toggle.

```javascript
function render() {
  const W = window.innerWidth, H = window.innerHeight;
  const s = SCALE * vpScale;
  const ox = W/2 + vpX, oy = H/2 + vpY;
  function tx(mmx) { return ox + mmx * s; }
  function ty(mmy) { return oy + mmy * s; }
  function ts(mm) { return mm * s; }

  let svg = `<svg xmlns="..." width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">`;
  // ... build SVG elements
  svg += '</svg>';
  container.innerHTML = svg;
  // Attach hover events to data-type elements
}
```

### Required Elements
1. **Grid** — 2mm grid pattern using SVG `<pattern>`
2. **Board outline** — rounded rectangle with PCB green fill
3. **Silkscreen border** — dashed white outline inset from board edge
4. **Machine pins** — gray circles at corners with drill hole centers
5. **Contacts** — colored circles along edges, color-coded by signal
6. **Components** — colored rectangles with ref designator and name labels
7. **Labels** — rotated text for top/bottom contacts, horizontal for left/right
8. **Dimension lines** — board width/height with arrows
9. **Margin visualization** — optional dashed overlay showing edge and center margins

### Interactive Controls
- **Pan**: mousedown + drag
- **Zoom**: mouse wheel (1.12x in, 0.89x out, clamped 0.2–10)
- **Toolbar buttons**: Contacts toggle, Margins toggle, Labels toggle, Zoom Fit
- **Hover tooltips**: show signal name, side, position, pad/drill specs

### Signal Color Function (2D)

```javascript
function sigColor(sig) {
  if (sig.startsWith('+3V3') || sig === '+5V' || sig === 'VIN') return '#f47067';
  if (sig === 'GND') return '#57ab5a';
  if (sig.startsWith('GPIO')) return '#6cb6ff';
  if (sig === 'SWDIO' || sig === 'SWCLK') return '#daaa3f';
  return '#d2a8ff';
}
```

## QFN / IC Package Rendering

When rendering QFN or other IC packages in 2D board layout viewers, model them as proper rotatable geometric objects.

### QFN Pin Position Model

Standard QFN pin numbering is **counter-clockwise from pin 1** (viewed from top). Pin 1 is at the top-left corner of the left side.

```javascript
// QFN parameters
const QFN = { body: 10, pitch: 0.4, pinsPerSide: 20, padW: 0.2, padL: 0.6 };
const QFN_ROTATION = 0; // 0°=pin1 top-left, rotates CW
const pinPos = {};
(() => {
  const SIDES = ['left','bottom','right','top'];
  const rad = QFN_ROTATION * Math.PI / 180;
  const cosR = Math.cos(rad), sinR = Math.sin(rad);
  const span = (QFN.pinsPerSide - 1) * QFN.pitch;
  const start = -span / 2;
  const edge = QFN.body / 2 + QFN.padL / 2;
  for (let i = 0; i < totalPins; i++) {
    const pin = i + 1;
    const side = Math.floor(i / QFN.pinsPerSide);
    const idx = i % QFN.pinsPerSide;
    let bx, by;
    if (side === 0)      { bx = -edge; by = start + idx * QFN.pitch; }       // left: T→B
    else if (side === 1)  { bx = start + idx * QFN.pitch; by = edge; }       // bottom: L→R
    else if (side === 2)  { bx = edge; by = -(start + idx * QFN.pitch); }    // right: B→T
    else                  { bx = -(start + idx * QFN.pitch); by = -edge; }    // top: R→L
    const x = bx * cosR - by * sinR;
    const y = bx * sinR + by * cosR;
    const sideIdx = (side + Math.round(QFN_ROTATION / 90)) % 4;
    pinPos[pin] = { x, y, side: SIDES[sideIdx] };
  }
})();
```

### Pin 1 Marker

Offset **perpendicular inward** from the edge, NOT toward body center:

```javascript
const p1 = pinPos[1];
const inw = p1.side==='left'?[1,0]:p1.side==='right'?[-1,0]:p1.side==='top'?[0,1]:[0,-1];
const markerX = p1.x + inw[0] * 1.0;
const markerY = p1.y + inw[1] * 1.0;
```

### Pin Tooltips

Render transparent hit-area rects **after** all component groups (for z-order). Use `data-*` attributes for pin name, net, and notes. Show tooltips on mouseenter with component name, pin number, function, net assignment, and contextual notes.

### Overlap Detection

Always run AABB overlap detection with 0.3mm clearance padding after any component repositioning. Display a visible warning banner when overlaps exist.

## KiCad PCB Export

Generate `.kicad_pcb` files from layout data for DRC validation and interop with KiCad.

### Generator Pattern

Write a Python script (`gen_kicad_pcb.py`) that:
1. Defines component positions and footprint types matching the layout viewer
2. Generates inline footprints with proper pad geometry (no external library dependencies)
3. Assigns nets to pads for DRC connectivity checking
4. Outputs a valid KiCad 9 `.kicad_pcb` file

Key footprint pad specs (SMD roundrect, F.Cu/F.Paste/F.Mask layers):
- **0402**: 2 pads at ±0.48mm, size 0.56×0.62mm
- **0805**: 2 pads at ±0.9375mm, size 1.025×1.4mm
- **SOT-223**: 3 pads left at 2.3mm pitch + 1 tab pad right
- **SOIC-8**: 8 pads at 1.27mm pitch, 2 rows at ±2.7mm
- **QFN-80**: 80 perimeter pads (0.2×0.6mm at 0.4mm pitch) + exposed thermal pad

### DRC Validation

```javascript
import { pcbDrc } from '/home/adom/gallia/viewer/kicad-api-client.js';

const report = await pcbDrc('/path/to/board.kicad_pcb');
// report is the parsed JSON DRC report
```

Expected benign violations when using inline footprints:
- `lib_footprint_issues` — footprint library names not found (expected for inline footprints)
- `text_height` — silkscreen text below 0.8mm minimum (cosmetic)

### Rendering from KiCad

**IMPORTANT**: The KiCad service SVG export renders pads as thin outlines only, not filled shapes. The result is nearly invisible. Instead, use a custom Python renderer (`render_pcb.py`) with Pillow that draws filled copper pads, IC bodies, and silkscreen on a PCB-green board. Display the PNG via `gv_display_file`.

## Critical Rules

### SVG Sizing
The SVG MUST use dynamic `width` and `height` based on `window.innerWidth` / `window.innerHeight`. Do NOT use fixed pixel dimensions — they break in the Gallia Viewer iframe.

### Self-Contained HTML
All CSS, JavaScript, and data MUST be inline in the single HTML file. The only external resources allowed are CDN imports (Three.js via importmap for 3D views). No local file references.

### Coordinate System
- **2D SVG**: Standard screen coordinates (Y down). Board center at screen center.
- **3D Three.js**: Y-up coordinate system. Board surface at Y=0, board extends below (negative Y). Components placed above (positive Y).
- **Footprint JSON**: Standard PCB coordinates (origin at center, X-right, Y-up). Contact X/Y positions are in mm from board center.

### Data-Driven Rendering
All board-specific data (pins, contacts, components, dimensions) MUST be defined in a `BOARD_DATA` config object at the top of the script. The rendering code below should be generic and work with any valid `BOARD_DATA`.

```javascript
const BOARD_DATA = {
  name: "Board Name",
  board: { width: 32, height: 32, thickness: 1.6, borderRadius: 1.2 },
  machinePins: [ /* from footprint JSON */ ],
  contacts: [ /* from footprint JSON */ ],
  components: [ /* user-defined component array */ ],
  margins: [ /* from footprint JSON */ ],
};
```

## Display in Gallia Viewer

### 3D View (default) — ALWAYS use `gv_3d_display`:
```
gv_3d_display(
  glb_path="/path/to/BOARD_NAME.glb",
  part_name="Board Name",
  manufacturer="Adom",
  package_type="Molecule WxHmm",
  pad_count=N,
  body_size={ x: W, y: H, z: 1.6 },
  title="Board Name 3D"
)
```

### 2D View — use `gv_display_file`:
```
gv_display_file(file_path="/path/to/BOARD_NAME-board-2d.html", title="Board Name 2D")
```

## Workflow

1. **Gather data** — Read footprint JSON if available, or construct from user specs
2. **Create output folder** — `mkdir -p /home/adom/project/project-content/schematics/boards/BOARD_NAME/`
3. **Generate 3D GLB** — Write a `build_3d_model.py` that loads real STEP pin/contact models from `adom-tsci-library/lib/3D_Models/`, applies pipeline PBR materials, adds component geometry, and exports as GLB
4. **Run the builder** — `python3 build_3d_model.py`
5. **Display 3D in Gallia Viewer** — Use `gv_3d_display` with the generated GLB (default view)
6. **Generate 2D viewer** — Create `BOARD_NAME-board-2d.html` following the SVG template
7. **Iterate** — User may request changes (different view, colors, component positions)
