{
  "schema_version": 1,
  "type": "skill",
  "slug": "board-creator",
  "title": "Board Creator",
  "brief": "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-l",
  "version": "1.0.1",
  "tags": [],
  "license": "MIT",
  "discovery_triggers": [
    "show the board",
    "create board layout",
    "visualize board",
    "board viewer",
    "3d board",
    "2d board view",
    "pcb view",
    "board render",
    "pcb layout preview",
    "board fabrication preview",
    "show pcb",
    "pcb 2d",
    "pcb 3d",
    "board overview"
  ],
  "discovery_pitch": "I can render your board layout in 2D (interactive SVG) and 3D (real STEP geometry via Babylon.js) with pin/pad cross-highlighting — so we can check the layout without round-tripping through KiCad.",
  "source_path": "SKILL.md",
  "readme": "---\r\nname: board-creator\r\ndescription: 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.\r\n---\r\n\r\n# Board Layout Creator\r\n\r\nCreate 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.\r\n\r\n## KiCad Service\r\n\r\nDRC 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`).\r\n\r\nAPI client: `/home/adom/gallia/viewer/kicad-api-client.js`\r\n\r\n## When to Use\r\n\r\n- User asks to \"show the board\" or \"visualize the board layout\"\r\n- User wants to see a 3D or 2D view of a molecule/PCB design\r\n- After creating a molecule footprint, user wants to see the physical layout\r\n- User asks for a \"board viewer\" or \"PCB visualization\"\r\n- User wants an interactive view with hover tooltips, orbit controls, or pan/zoom\r\n\r\n## Output\r\n\r\nFiles saved to the project boards directory:\r\n\r\n```\r\n/home/adom/project/project-content/schematics/boards/BOARD_NAME/\r\n  BOARD_NAME.glb                # 3D GLB model (real STEP pins, pipeline PBR materials)\r\n  build_3d_model.py             # Python script to rebuild the GLB\r\n  BOARD_NAME-board-2d.html      # Interactive 2D SVG top-down viewer\r\n  BOARD_NAME-footprint.json     # Footprint data (copy or reference)\r\n```\r\n\r\n- **3D view:** Display via `gv_3d_display` (Gallia Viewer Babylon.js viewer)\r\n- **2D view:** Display via `gv_display_file` (HTML in Gallia Viewer iframe)\r\n\r\n## Input Data\r\n\r\nThe 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.\r\n\r\n### Footprint JSON Schema\r\n\r\n```json\r\n{\r\n  \"name\": \"Board_Name_v1\",\r\n  \"version\": \"v1\",\r\n  \"board\": {\r\n    \"width_mm\": 32.0,\r\n    \"height_mm\": 32.0,\r\n    \"size_param\": \"30x30\",\r\n    \"pin_type\": \"MachinePinMediumStandard\",\r\n    \"contact_type\": \"MachineContactMedium\",\r\n    \"mol_type\": \"4pin\",\r\n    \"border_radius\": 1.2\r\n  },\r\n  \"machine_pins\": [\r\n    { \"name\": \"MP1\", \"x\": -15.0, \"y\": -15.0, \"drill\": 1.1, \"pad\": 1.6 }\r\n  ],\r\n  \"contacts\": [\r\n    { \"name\": \"GPIO0\", \"margin\": \"leftMargin\", \"x\": -15.0, \"y\": -9.0, \"drill\": 0.78, \"pad\": 1.3, \"pitch\": 2.0 }\r\n  ],\r\n  \"margins\": [\r\n    { \"name\": \"leftMargin\", \"pcbX\": -15.0, \"pcbY\": 0, \"width\": 2.0, \"height\": 28.0 }\r\n  ]\r\n}\r\n```\r\n\r\n### Components Array (for 3D/2D rendering)\r\n\r\nIn addition to the footprint JSON, define a components array for on-board ICs, passives, and connectors:\r\n\r\n```javascript\r\nconst COMPONENTS = [\r\n  { name: \"RP2350B\", ref: \"U1\", pkg: \"QFN-80\", x: 0, y: 0, w: 7, h: 7, d: 1.0, color: \"#1a237e\" },\r\n  { name: \"W25Q128\", ref: \"U2\", pkg: \"SOIC-8\", x: 6.5, y: -3, w: 5, h: 4, d: 0.8, color: \"#1b5e20\" },\r\n  // ... more components\r\n];\r\n```\r\n\r\n## Hydrogen Color Scheme\r\n\r\nAll board visualizations MUST match the Hydrogen dark theme.\r\n\r\n### Backgrounds & Borders\r\n| Element | Color |\r\n|---------|-------|\r\n| Body background | `#0f1218` |\r\n| Info panel background | `rgba(13, 17, 23, 0.95)` |\r\n| Panel border | `#21262d` |\r\n| Board fill (PCB green) | `#0d5016` (2D) or pipeline FR4 `rgb(0.034, 0.105, 0.033)` (3D) |\r\n| Board stroke | `#1b8a2a` |\r\n| Solder mask top | Pipeline FR4 material (3D) — see Standardized PBR Materials below |\r\n\r\n### Text Colors\r\n| Element | Color |\r\n|---------|-------|\r\n| Primary text | `#e6edf3` |\r\n| Secondary text | `#7d8590` |\r\n| Teal accent | `#00b8b1` |\r\n| Info panel labels | `#7d8590` |\r\n\r\n### Signal Colors (contacts)\r\n| Signal Type | 2D Color | 3D Hex | Use For |\r\n|-------------|----------|--------|---------|\r\n| Power (+3V3, +5V, VIN) | `#f47067` | `0xe04040` | Supply rails |\r\n| Ground (GND) | `#57ab5a` | `0x40c040` | Ground connections |\r\n| GPIO | `#6cb6ff` | `0x4080e0` | General purpose I/O |\r\n| Debug (SWDIO, SWCLK) | `#daaa3f` | `0xe0c830` | SWD debug pins |\r\n| Special (RUN, ADC_VREF) | `#d2a8ff` | `0x9070c0` | Misc signals |\r\n\r\n### Standardized PBR Materials (3D)\r\n\r\nThese 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.\r\n\r\n#### PCB Layer Materials\r\n\r\n| Material | Name | Base Color (linear RGB) | Metallic | Roughness | IOR | Notes |\r\n|----------|------|------------------------|----------|-----------|-----|-------|\r\n| **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 |\r\n| **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) |\r\n| **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) |\r\n| **HASL Copper** | `Standard_HASL_Copper` | `(0.950, 0.950, 0.950)` | 1.000 | 0.150 | 1.450 | Exposed copper pads (silver finish) |\r\n\r\n#### Three.js Equivalent\r\n\r\n```javascript\r\n// Canonical pipeline materials — use these exact values\r\nconst MATERIALS = {\r\n  fr4: {\r\n    color: new THREE.Color(0.034, 0.105, 0.033),  // Dark solder mask green\r\n    metalness: 0.200,\r\n    roughness: 0.350,\r\n  },\r\n  tanSubstrate: {\r\n    color: new THREE.Color(0.471, 0.431, 0.275),  // PCB edge/internal tan\r\n    metalness: 0.000,\r\n    roughness: 0.200,\r\n  },\r\n  goldPin: {\r\n    color: new THREE.Color(0.900, 0.599, 0.200),  // Gold machine pin\r\n    metalness: 1.000,\r\n    roughness: 0.077,\r\n  },\r\n  haslCopper: {\r\n    color: new THREE.Color(0.950, 0.950, 0.950),  // Silver HASL finish\r\n    metalness: 1.000,\r\n    roughness: 0.150,\r\n  },\r\n};\r\n```\r\n\r\n#### Component Materials (non-pipeline, board-creator conventions)\r\n\r\n| Element | Hex | Properties |\r\n|---------|-----|------------|\r\n| IC body (dark) | `0x1e1e28` | roughness: 0.15 |\r\n| USB-C shell | `0x50505a` | metalness: 0.6, roughness: 0.25 |\r\n| Button body | `0x463223` | roughness: 0.5 |\r\n| Capacitor body | `0x503719` | roughness: 0.5 |\r\n\r\n### Fonts\r\n- UI text: `-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif`\r\n- Technical text: `'SF Mono', 'Fira Code', 'Consolas', monospace`\r\n\r\n## Machine Pin & Contact Specs\r\n\r\nReference dimensions from `adom-tsci-library/globals.ts`:\r\n\r\n| Type | Hole ID | Hole OD | Bounding Box |\r\n|------|---------|---------|--------------|\r\n| MachinePinMediumShort | 1.1mm | 1.6mm | 2mm |\r\n| MachinePinMediumStandard | 1.1mm | 1.6mm | 2mm |\r\n| MachinePinLargeShort | 3.45mm | 5.2mm | 6mm |\r\n| MachinePinLargeStandard | 3.45mm | 5.2mm | 6mm |\r\n| MachineContactMedium | 0.78mm | 1.3mm | 2mm |\r\n| MachineContactLarge | 2.62mm | 4.4mm | 6mm |\r\n\r\n## 3D Board Viewer (GLB + Gallia Viewer Babylon.js)\r\n\r\n**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.\r\n\r\n### Pipeline: Python GLB Builder\r\n\r\nGenerate 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`.\r\n\r\n```python\r\nimport trimesh\r\nfrom trimesh.visual.material import PBRMaterial\r\nimport colorsys, json\r\n\r\nSTEP_DIR = \"/home/adom/adom-tsci-library/lib/3D_Models\"\r\n\r\n# Load real STEP geometry (converted m → mm)\r\ndef load_step_mm(name):\r\n    scene = trimesh.load(f\"{STEP_DIR}/{name}.step\")\r\n    meshes = []\r\n    for gname, geom in scene.geometry.items():\r\n        geom.vertices *= 1000  # meters → mm\r\n        meshes.append(geom)\r\n    return meshes\r\n\r\npin_meshes = load_step_mm(\"MachinePinMediumStandard\")\r\ncontact_meshes = load_step_mm(\"MachineContactMedium\")\r\n```\r\n\r\n### Pipeline PBR Materials in Python\r\n\r\n```python\r\ndef pipeline_pbr(h, s, v, metallic, roughness, name=\"mat\"):\r\n    r, g, b = colorsys.hsv_to_rgb(h, s, v)\r\n    return PBRMaterial(baseColorFactor=[r, g, b, 1.0],\r\n                       metallicFactor=metallic, roughnessFactor=roughness, name=name)\r\n\r\nMAT_FR4       = pipeline_pbr(0.333, 0.682, 0.105, metallic=0.200, roughness=0.350, name=\"Standard_FR4_PCB\")\r\nMAT_GOLD_PIN  = pipeline_pbr(0.095, 0.778, 0.900, metallic=1.000, roughness=0.077, name=\"Standard_Gold_Machine_Pin\")\r\nMAT_HASL      = PBRMaterial(baseColorFactor=[0.95, 0.95, 0.95, 1.0],\r\n                            metallicFactor=1.000, roughnessFactor=0.150, name=\"Standard_HASL_Copper\")\r\nMAT_SUBSTRATE = pipeline_pbr(0.133, 0.417, 0.471, metallic=0.000, roughness=0.200, name=\"Standard_Tan_Substrate\")\r\n```\r\n\r\n### Building the Scene\r\n\r\n```python\r\nscene = trimesh.Scene()\r\n\r\n# PCB board (box with FR4 material)\r\nboard = trimesh.creation.box(extents=[BOARD_W, BOARD_H, PCB_THICKNESS])\r\nboard.visual = trimesh.visual.TextureVisuals(material=MAT_FR4)\r\nboard.apply_translation([0, 0, -PCB_THICKNESS/2])\r\nscene.add_geometry(board, node_name=\"PCB_Board\")\r\n\r\n# Real STEP machine pins at each corner position\r\nfor pin in footprint[\"machine_pins\"]:\r\n    for i, mesh in enumerate(pin_meshes):\r\n        m = mesh.copy()\r\n        m.visual = trimesh.visual.TextureVisuals(material=MAT_GOLD_PIN)\r\n        m.apply_translation([pin[\"x\"], pin[\"y\"], 0])\r\n        scene.add_geometry(m, node_name=f\"Pin_{pin['name']}_{i}\")\r\n\r\n# Real STEP contacts at each edge position (color-coded by signal)\r\nfor idx, contact in enumerate(footprint[\"contacts\"]):\r\n    mat = contact_material(contact[\"name\"])  # signal-based color\r\n    for i, mesh in enumerate(contact_meshes):\r\n        m = mesh.copy()\r\n        m.visual = trimesh.visual.TextureVisuals(material=mat)\r\n        m.apply_translation([contact[\"x\"], contact[\"y\"], 0])\r\n        scene.add_geometry(m, node_name=f\"Contact_{contact['name']}_{idx}_{i}\")\r\n\r\n# Components as boxes with appropriate materials\r\n# ... (IC body, flash, LDO, USB-C, crystal, buttons, caps, LEDs)\r\n\r\n# Export\r\nscene.export(\"BOARD_NAME.glb\", file_type='glb')\r\n```\r\n\r\n### Displaying in Gallia Viewer (required for 3D)\r\n\r\nAlways use `gv_3d_display` for the 3D view:\r\n\r\n```\r\ngv_3d_display(\r\n  glb_path=\"/path/to/BOARD_NAME.glb\",\r\n  part_name=\"RP2350B Core+USB\",\r\n  manufacturer=\"Adom\",\r\n  package_type=\"Molecule 32x32mm\",\r\n  pad_count=60,\r\n  body_size={ x: 32, y: 32, z: 1.6 },\r\n  title=\"Board Name 3D\"\r\n)\r\n```\r\n\r\nThis renders in the built-in Gallia Viewer Babylon.js viewer with orbit controls, shadows, PBR lighting, and proper material rendering.\r\n\r\n### Fallback: Three.js HTML Template\r\n\r\nA 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.\r\n\r\n## 2D Board Viewer (SVG)\r\n\r\nGenerate a self-contained HTML file with dynamic SVG rendering. Reference template: `board-2d-template.html` in this skill directory.\r\n\r\n### Rendering Approach\r\n\r\nUse JavaScript to build SVG string and inject into a container div. Re-render on pan/zoom/toggle.\r\n\r\n```javascript\r\nfunction render() {\r\n  const W = window.innerWidth, H = window.innerHeight;\r\n  const s = SCALE * vpScale;\r\n  const ox = W/2 + vpX, oy = H/2 + vpY;\r\n  function tx(mmx) { return ox + mmx * s; }\r\n  function ty(mmy) { return oy + mmy * s; }\r\n  function ts(mm) { return mm * s; }\r\n\r\n  let svg = `<svg xmlns=\"...\" width=\"${W}\" height=\"${H}\" viewBox=\"0 0 ${W} ${H}\">`;\r\n  // ... build SVG elements\r\n  svg += '</svg>';\r\n  container.innerHTML = svg;\r\n  // Attach hover events to data-type elements\r\n}\r\n```\r\n\r\n### Required Elements\r\n1. **Grid** — 2mm grid pattern using SVG `<pattern>`\r\n2. **Board outline** — rounded rectangle with PCB green fill\r\n3. **Silkscreen border** — dashed white outline inset from board edge\r\n4. **Machine pins** — gray circles at corners with drill hole centers\r\n5. **Contacts** — colored circles along edges, color-coded by signal\r\n6. **Components** — colored rectangles with ref designator and name labels\r\n7. **Labels** — rotated text for top/bottom contacts, horizontal for left/right\r\n8. **Dimension lines** — board width/height with arrows\r\n9. **Margin visualization** — optional dashed overlay showing edge and center margins\r\n\r\n### Interactive Controls\r\n- **Pan**: mousedown + drag\r\n- **Zoom**: mouse wheel (1.12x in, 0.89x out, clamped 0.2–10)\r\n- **Toolbar buttons**: Contacts toggle, Margins toggle, Labels toggle, Zoom Fit\r\n- **Hover tooltips**: show signal name, side, position, pad/drill specs\r\n\r\n### Signal Color Function (2D)\r\n\r\n```javascript\r\nfunction sigColor(sig) {\r\n  if (sig.startsWith('+3V3') || sig === '+5V' || sig === 'VIN') return '#f47067';\r\n  if (sig === 'GND') return '#57ab5a';\r\n  if (sig.startsWith('GPIO')) return '#6cb6ff';\r\n  if (sig === 'SWDIO' || sig === 'SWCLK') return '#daaa3f';\r\n  return '#d2a8ff';\r\n}\r\n```\r\n\r\n## QFN / IC Package Rendering\r\n\r\nWhen rendering QFN or other IC packages in 2D board layout viewers, model them as proper rotatable geometric objects.\r\n\r\n### QFN Pin Position Model\r\n\r\nStandard QFN pin numbering is **counter-clockwise from pin 1** (viewed from top). Pin 1 is at the top-left corner of the left side.\r\n\r\n```javascript\r\n// QFN parameters\r\nconst QFN = { body: 10, pitch: 0.4, pinsPerSide: 20, padW: 0.2, padL: 0.6 };\r\nconst QFN_ROTATION = 0; // 0°=pin1 top-left, rotates CW\r\nconst pinPos = {};\r\n(() => {\r\n  const SIDES = ['left','bottom','right','top'];\r\n  const rad = QFN_ROTATION * Math.PI / 180;\r\n  const cosR = Math.cos(rad), sinR = Math.sin(rad);\r\n  const span = (QFN.pinsPerSide - 1) * QFN.pitch;\r\n  const start = -span / 2;\r\n  const edge = QFN.body / 2 + QFN.padL / 2;\r\n  for (let i = 0; i < totalPins; i++) {\r\n    const pin = i + 1;\r\n    const side = Math.floor(i / QFN.pinsPerSide);\r\n    const idx = i % QFN.pinsPerSide;\r\n    let bx, by;\r\n    if (side === 0)      { bx = -edge; by = start + idx * QFN.pitch; }       // left: T→B\r\n    else if (side === 1)  { bx = start + idx * QFN.pitch; by = edge; }       // bottom: L→R\r\n    else if (side === 2)  { bx = edge; by = -(start + idx * QFN.pitch); }    // right: B→T\r\n    else                  { bx = -(start + idx * QFN.pitch); by = -edge; }    // top: R→L\r\n    const x = bx * cosR - by * sinR;\r\n    const y = bx * sinR + by * cosR;\r\n    const sideIdx = (side + Math.round(QFN_ROTATION / 90)) % 4;\r\n    pinPos[pin] = { x, y, side: SIDES[sideIdx] };\r\n  }\r\n})();\r\n```\r\n\r\n### Pin 1 Marker\r\n\r\nOffset **perpendicular inward** from the edge, NOT toward body center:\r\n\r\n```javascript\r\nconst p1 = pinPos[1];\r\nconst inw = p1.side==='left'?[1,0]:p1.side==='right'?[-1,0]:p1.side==='top'?[0,1]:[0,-1];\r\nconst markerX = p1.x + inw[0] * 1.0;\r\nconst markerY = p1.y + inw[1] * 1.0;\r\n```\r\n\r\n### Pin Tooltips\r\n\r\nRender 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.\r\n\r\n### Overlap Detection\r\n\r\nAlways run AABB overlap detection with 0.3mm clearance padding after any component repositioning. Display a visible warning banner when overlaps exist.\r\n\r\n## KiCad PCB Export\r\n\r\nGenerate `.kicad_pcb` files from layout data for DRC validation and interop with KiCad.\r\n\r\n### Generator Pattern\r\n\r\nWrite a Python script (`gen_kicad_pcb.py`) that:\r\n1. Defines component positions and footprint types matching the layout viewer\r\n2. Generates inline footprints with proper pad geometry (no external library dependencies)\r\n3. Assigns nets to pads for DRC connectivity checking\r\n4. Outputs a valid KiCad 9 `.kicad_pcb` file\r\n\r\nKey footprint pad specs (SMD roundrect, F.Cu/F.Paste/F.Mask layers):\r\n- **0402**: 2 pads at ±0.48mm, size 0.56×0.62mm\r\n- **0805**: 2 pads at ±0.9375mm, size 1.025×1.4mm\r\n- **SOT-223**: 3 pads left at 2.3mm pitch + 1 tab pad right\r\n- **SOIC-8**: 8 pads at 1.27mm pitch, 2 rows at ±2.7mm\r\n- **QFN-80**: 80 perimeter pads (0.2×0.6mm at 0.4mm pitch) + exposed thermal pad\r\n\r\n### DRC Validation\r\n\r\n```javascript\r\nimport { pcbDrc } from '/home/adom/gallia/viewer/kicad-api-client.js';\r\n\r\nconst report = await pcbDrc('/path/to/board.kicad_pcb');\r\n// report is the parsed JSON DRC report\r\n```\r\n\r\nExpected benign violations when using inline footprints:\r\n- `lib_footprint_issues` — footprint library names not found (expected for inline footprints)\r\n- `text_height` — silkscreen text below 0.8mm minimum (cosmetic)\r\n\r\n### Rendering from KiCad\r\n\r\n**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`.\r\n\r\n## Critical Rules\r\n\r\n### SVG Sizing\r\nThe 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.\r\n\r\n### Self-Contained HTML\r\nAll 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.\r\n\r\n### Coordinate System\r\n- **2D SVG**: Standard screen coordinates (Y down). Board center at screen center.\r\n- **3D Three.js**: Y-up coordinate system. Board surface at Y=0, board extends below (negative Y). Components placed above (positive Y).\r\n- **Footprint JSON**: Standard PCB coordinates (origin at center, X-right, Y-up). Contact X/Y positions are in mm from board center.\r\n\r\n### Data-Driven Rendering\r\nAll 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`.\r\n\r\n```javascript\r\nconst BOARD_DATA = {\r\n  name: \"Board Name\",\r\n  board: { width: 32, height: 32, thickness: 1.6, borderRadius: 1.2 },\r\n  machinePins: [ /* from footprint JSON */ ],\r\n  contacts: [ /* from footprint JSON */ ],\r\n  components: [ /* user-defined component array */ ],\r\n  margins: [ /* from footprint JSON */ ],\r\n};\r\n```\r\n\r\n## Display in Gallia Viewer\r\n\r\n### 3D View (default) — ALWAYS use `gv_3d_display`:\r\n```\r\ngv_3d_display(\r\n  glb_path=\"/path/to/BOARD_NAME.glb\",\r\n  part_name=\"Board Name\",\r\n  manufacturer=\"Adom\",\r\n  package_type=\"Molecule WxHmm\",\r\n  pad_count=N,\r\n  body_size={ x: W, y: H, z: 1.6 },\r\n  title=\"Board Name 3D\"\r\n)\r\n```\r\n\r\n### 2D View — use `gv_display_file`:\r\n```\r\ngv_display_file(file_path=\"/path/to/BOARD_NAME-board-2d.html\", title=\"Board Name 2D\")\r\n```\r\n\r\n## Workflow\r\n\r\n1. **Gather data** — Read footprint JSON if available, or construct from user specs\r\n2. **Create output folder** — `mkdir -p /home/adom/project/project-content/schematics/boards/BOARD_NAME/`\r\n3. **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\r\n4. **Run the builder** — `python3 build_3d_model.py`\r\n5. **Display 3D in Gallia Viewer** — Use `gv_3d_display` with the generated GLB (default view)\r\n6. **Generate 2D viewer** — Create `BOARD_NAME-board-2d.html` following the SVG template\r\n7. **Iterate** — User may request changes (different view, colors, component positions)\r\n",
  "author": {
    "id": "695820315b5f1e4db2fcf602",
    "name": "Kyle Bergstedt",
    "email": "kyle@adom.inc"
  },
  "visibility": {
    "public": true
  },
  "hero": null,
  "sample_prompts": [],
  "metadata": {},
  "created_at": "2026-05-28T05:30:06.226Z",
  "updated_at": "2026-05-28T05:30:06.226Z",
  "sub_skills": [],
  "parent_app": null
}