custom-nas/viewer/index.html

2533 lines
127 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>6U Open-Source Storage Array — Manufacturing & Supply Chain Suite</title>
<!-- Three.js and OrbitControls -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<!-- FontAwesome & Google Fonts -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-dark: #0b0f17;
--bg-panel: #111827;
--bg-card: #1f2937;
--bg-card-hover: #374151;
--accent-blue: #3b82f6;
--accent-cyan: #06b6d4;
--accent-emerald: #10b981;
--accent-amber: #f59e0b;
--accent-purple: #8b5cf6;
--accent-rose: #f43f5e;
--text-main: #f9fafb;
--text-muted: #9ca3af;
--border-color: #374151;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-dark);
color: var(--text-main);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Top Header */
header {
background-color: var(--bg-panel);
border-bottom: 1px solid var(--border-color);
padding: 0.6rem 1.25rem;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 20;
}
.logo-group {
display: flex;
align-items: center;
gap: 0.75rem;
}
.logo-group i {
font-size: 1.4rem;
color: var(--accent-cyan);
}
.logo-title {
font-weight: 700;
font-size: 1.1rem;
letter-spacing: -0.02em;
}
.logo-badge {
background: rgba(6, 182, 212, 0.12);
color: var(--accent-cyan);
font-size: 0.7rem;
font-weight: 700;
padding: 0.2rem 0.5rem;
border-radius: 4px;
border: 1px solid rgba(6, 182, 212, 0.3);
text-transform: uppercase;
}
.nav-tabs {
display: flex;
gap: 0.35rem;
}
.nav-tab {
background: transparent;
color: var(--text-muted);
border: none;
padding: 0.45rem 0.85rem;
font-size: 0.825rem;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.45rem;
}
.nav-tab:hover {
color: var(--text-main);
background: var(--bg-card);
}
.nav-tab.active {
color: #ffffff;
background: var(--accent-blue);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.35);
}
/* Main Workspace Container */
.main-container {
display: flex;
flex: 1;
position: relative;
overflow: hidden;
}
/* 3D Viewport View */
#viewport-container {
flex: 1;
position: relative;
background: radial-gradient(circle at center, #1e293b 0%, #0b0f17 100%);
}
#webgl-canvas {
width: 100%;
height: 100%;
display: block;
touch-action: none;
}
/* Viewport Overlays */
.viewport-overlay {
position: absolute;
top: 1rem;
left: 1rem;
background: rgba(17, 24, 39, 0.85);
backdrop-filter: blur(10px);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
width: 290px;
z-index: 5;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.6);
}
.overlay-title {
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--accent-cyan);
margin-bottom: 0.75rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.control-group {
margin-bottom: 0.75rem;
}
.control-group label {
display: flex;
justify-content: space-between;
font-size: 0.775rem;
font-weight: 600;
color: var(--text-muted);
margin-bottom: 0.3rem;
}
.slider-control {
width: 100%;
-webkit-appearance: none;
height: 5px;
border-radius: 3px;
background: var(--bg-card);
outline: none;
}
.slider-control::-webkit-slider-thumb {
-webkit-appearance: none;
width: 15px;
height: 15px;
border-radius: 50%;
background: var(--accent-cyan);
cursor: pointer;
}
.toggle-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.25rem 0;
font-size: 0.775rem;
color: var(--text-main);
cursor: pointer;
}
.toggle-item input {
cursor: pointer;
accent-color: var(--accent-cyan);
}
/* Dimension & Info HUD */
.dimension-hud {
position: absolute;
bottom: 1.25rem;
left: 1.25rem;
background: rgba(17, 24, 39, 0.9);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 0.6rem 1rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
color: var(--accent-cyan);
display: flex;
gap: 1.25rem;
z-index: 5;
}
.hud-stat span {
color: var(--text-muted);
font-size: 0.65rem;
display: block;
}
/* Component Inspector Modal Card */
#component-inspector {
position: absolute;
bottom: 1.25rem;
right: 1.25rem;
background: rgba(17, 24, 39, 0.95);
border: 1px solid var(--accent-cyan);
border-radius: 8px;
padding: 1rem;
width: 320px;
z-index: 5;
display: none;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.7);
}
/* Tab Content Panels */
.content-panel {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 580px;
background: var(--bg-panel);
border-left: 1px solid var(--border-color);
padding: 1.25rem;
overflow-y: auto;
display: none;
z-index: 15;
box-shadow: -10px 0 35px rgba(0, 0, 0, 0.6);
transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s ease;
}
.content-panel.active {
display: block;
}
.content-panel.minimized {
width: 54px;
padding: 1rem 0.5rem;
overflow: hidden;
}
.content-panel.minimized > *:not(.panel-header) {
display: none !important;
}
.content-panel.minimized .panel-title span {
display: none;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.25rem;
padding-bottom: 0.6rem;
border-bottom: 1px solid var(--border-color);
}
.panel-title {
font-size: 1.1rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
}
.panel-header-actions {
display: flex;
align-items: center;
gap: 0.35rem;
}
.panel-btn {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 1rem;
cursor: pointer;
padding: 0.25rem 0.45rem;
border-radius: 4px;
transition: all 0.2s;
}
.panel-btn:hover {
color: var(--text-main);
background: var(--bg-card);
}
.part-card-active {
border: 1px solid var(--accent-cyan) !important;
background: rgba(6, 182, 212, 0.08) !important;
box-shadow: 0 0 15px rgba(6, 182, 212, 0.25) !important;
}
.card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.card-header {
font-weight: 700;
font-size: 0.875rem;
margin-bottom: 0.75rem;
color: var(--accent-cyan);
display: flex;
align-items: center;
justify-content: space-between;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.775rem;
}
.data-table th {
text-align: left;
padding: 0.5rem;
color: var(--text-muted);
border-bottom: 1px solid var(--border-color);
font-weight: 600;
}
.data-table td {
padding: 0.5rem;
border-bottom: 1px solid rgba(55, 65, 81, 0.5);
}
.btn {
background: var(--accent-blue);
color: #fff;
border: none;
padding: 0.45rem 0.85rem;
border-radius: 6px;
font-size: 0.775rem;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.4rem;
transition: all 0.2s;
}
.btn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-outline {
background: transparent;
border: 1px solid var(--border-color);
color: var(--text-main);
}
.btn-outline:hover {
background: var(--bg-card-hover);
}
.code-snippet {
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
background: #06090e;
padding: 0.75rem;
border-radius: 6px;
border: 1px solid var(--border-color);
overflow-x: auto;
color: #38bdf8;
margin-top: 0.5rem;
}
/* Material Radar Bar */
.mat-bar-container {
margin-bottom: 0.5rem;
}
.mat-bar-label {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
margin-bottom: 0.2rem;
}
.mat-bar-bg {
height: 6px;
background: #111827;
border-radius: 3px;
overflow: hidden;
}
.mat-bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
/* Sourcing Select Dropdown */
.vendor-select {
background: #111827;
color: var(--text-main);
border: 1px solid var(--border-color);
padding: 0.25rem 0.4rem;
border-radius: 4px;
font-size: 0.75rem;
outline: none;
}
/* Checklist Item */
.check-item {
display: flex;
align-items: flex-start;
gap: 0.6rem;
padding: 0.5rem 0;
border-bottom: 1px solid rgba(55, 65, 81, 0.4);
font-size: 0.8rem;
}
.check-item input {
margin-top: 0.15rem;
accent-color: var(--accent-emerald);
}
/* Viewport Floating Quickbar */
.viewport-quickbar {
position: absolute;
top: 1rem;
right: 1rem;
display: flex;
gap: 0.4rem;
z-index: 10;
background: rgba(15, 23, 42, 0.85);
backdrop-filter: blur(8px);
padding: 0.35rem 0.5rem;
border-radius: 8px;
border: 1px solid var(--border-color);
box-shadow: 0 4px 15px rgba(0,0,0,0.4);
}
.quick-btn {
background: transparent;
border: 1px solid transparent;
color: var(--text-muted);
padding: 0.3rem 0.5rem;
border-radius: 5px;
font-size: 0.75rem;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.35rem;
transition: all 0.2s;
}
.quick-btn:hover {
color: var(--text-main);
background: var(--bg-card);
border-color: var(--border-color);
}
.quick-btn.active {
color: var(--accent-cyan);
background: rgba(6, 182, 212, 0.15);
border-color: var(--accent-cyan);
}
/* ==========================================================================
Mobile-First & Tablet Touch Ergonomics Breakpoints (WCAG 48px Compliance)
========================================================================== */
.sheet-drag-handle {
display: none;
width: 40px;
height: 4px;
background: #475569;
border-radius: 9999px;
margin: 0 auto 0.5rem auto;
}
@media (max-width: 768px) {
header {
flex-direction: column;
align-items: stretch;
padding: 0.5rem 0.75rem;
gap: 0.4rem;
}
.logo-group {
justify-content: space-between;
width: 100%;
}
.logo-title {
font-size: 0.95rem;
}
.logo-badge {
font-size: 0.65rem;
padding: 0.15rem 0.35rem;
}
.nav-tabs {
width: 100%;
overflow-x: auto;
white-space: nowrap;
padding-bottom: 0.35rem;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
gap: 0.3rem;
flex-wrap: nowrap;
}
.nav-tabs::-webkit-scrollbar {
display: none;
}
.nav-tab {
padding: 0.45rem 0.75rem;
font-size: 0.725rem;
flex-shrink: 0;
border-radius: 6px;
min-height: 40px;
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.sheet-drag-handle {
display: block;
}
/* Content Panels Transformed to Slide-Up Bottom Sheets */
.content-panel {
position: fixed !important;
top: auto !important;
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
width: 100% !important;
max-width: 100% !important;
height: 78vh !important;
max-height: 85vh !important;
border-radius: 20px 20px 0 0 !important;
border-left: none !important;
border-top: 1px solid var(--border-color) !important;
background: rgba(17, 24, 39, 0.98) !important;
backdrop-filter: blur(20px) !important;
box-shadow: 0 -15px 40px rgba(0, 0, 0, 0.85) !important;
transform: translateY(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
display: block !important;
z-index: 100 !important;
padding: 0.75rem 1rem max(1rem, env(safe-area-inset-bottom)) 1rem !important;
box-sizing: border-box !important;
}
.content-panel.active {
transform: translateY(0) !important;
}
.content-panel.minimized {
transform: translateY(calc(100% - 58px)) !important;
overflow: hidden !important;
}
.panel-header {
position: sticky;
top: -0.75rem;
background: rgba(17, 24, 39, 0.98);
margin: -0.75rem -1rem 1rem -1rem;
padding: 0.6rem 1rem 0.5rem 1rem;
border-bottom: 1px solid var(--border-color);
z-index: 5;
flex-direction: column;
align-items: stretch;
gap: 0.25rem;
}
.panel-header-actions {
position: absolute;
top: 0.6rem;
right: 1rem;
}
/* Viewport Controls on Mobile */
.viewport-overlay {
top: auto !important;
bottom: 0.75rem !important;
left: 0.75rem !important;
right: 0.75rem !important;
width: auto !important;
max-height: 38vh !important;
overflow-y: auto !important;
border-radius: 12px !important;
padding: 0.75rem !important;
}
.viewport-quickbar {
top: 0.5rem !important;
right: 0.5rem !important;
left: auto !important;
gap: 0.25rem !important;
padding: 0.25rem !important;
border-radius: 6px !important;
}
.quick-btn span {
display: none; /* Icon-only buttons on mobile */
}
.quick-btn {
padding: 0.4rem 0.5rem !important;
font-size: 0.85rem !important;
min-width: 38px;
min-height: 38px;
justify-content: center;
}
/* Touch Target Optimization (WCAG Minimum 44px-48px) */
.btn, .btn-outline {
min-height: 44px;
padding: 0.55rem 0.85rem;
font-size: 0.8rem;
display: inline-flex;
align-items: center;
justify-content: center;
}
.check-item {
min-height: 48px;
padding: 0.5rem 0.25rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.check-item input[type="checkbox"] {
width: 22px;
height: 22px;
min-width: 22px;
min-height: 22px;
cursor: pointer;
}
.data-table {
display: block;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
}
@media (max-width: 480px) {
.logo-title {
font-size: 0.85rem;
}
.content-panel {
height: 84vh !important;
}
.viewport-overlay {
max-height: 34vh !important;
}
}
</style>
</head>
<body>
<!-- Header Navigation -->
<header>
<div class="logo-group">
<i class="fa-solid fa-industry"></i>
<div>
<span class="logo-title">6U Storage Array — Open Manufacturing Suite</span>
<span class="logo-badge">OSHW-ERP v2.0</span>
</div>
</div>
<div class="nav-tabs">
<button class="nav-tab active" onclick="switchTab('3d-view')"><i class="fa-solid fa-cube"></i> 3D CAD & Brains</button>
<button class="nav-tab" onclick="switchTab('parts-catalog')"><i class="fa-solid fa-boxes-stacked"></i> Parts & CAD Library</button>
<button class="nav-tab" onclick="switchTab('supply-chain')"><i class="fa-solid fa-truck-ramp-box"></i> Supply Chain & BOM</button>
<button class="nav-tab" onclick="switchTab('materials')"><i class="fa-solid fa-atom"></i> Material Science</button>
<button class="nav-tab" onclick="switchTab('print-farm')"><i class="fa-solid fa-print"></i> 3D Print Farm</button>
<button class="nav-tab" onclick="switchTab('diagnostics')"><i class="fa-solid fa-terminal"></i> WebSerial Supervisor</button>
<button class="nav-tab" onclick="switchTab('qa-assembly')"><i class="fa-solid fa-screwdriver-wrench"></i> Assembly QA</button>
<button class="nav-tab" onclick="switchTab('travelers')"><i class="fa-solid fa-barcode"></i> MES Travelers</button>
<button class="nav-tab" onclick="switchTab('provisioning')"><i class="fa-solid fa-server"></i> OS Provisioning</button>
</div>
</header>
<!-- Main Workspace -->
<div class="main-container">
<!-- 3D Canvas Viewport -->
<div id="viewport-container">
<canvas id="webgl-canvas"></canvas>
<!-- Floating Viewport Quickbar -->
<div class="viewport-quickbar">
<button id="btn-quick-turntable" class="quick-btn" title="Toggle Auto-Spin Turntable" onclick="toggleTurntable()"><i class="fa-solid fa-arrows-rotate"></i> <span>Turntable</span></button>
<button id="btn-quick-grid" class="quick-btn active" title="Toggle Measurement Grid" onclick="toggleGrid()"><i class="fa-solid fa-border-all"></i> <span>Grid</span></button>
<button id="btn-quick-units" class="quick-btn" title="Toggle Metric (mm) / Imperial (in)" onclick="toggleUnits()"><i class="fa-solid fa-ruler-combined"></i> <span id="units-label">Metric (mm)</span></button>
<button id="btn-quick-light" class="quick-btn" title="Cycle Studio Lighting" onclick="cycleLighting()"><i class="fa-solid fa-sun"></i> <span id="light-label">Dark Studio</span></button>
<button id="btn-quick-download" class="quick-btn" title="Export / Download Active CAD Model" onclick="downloadActiveCAD()"><i class="fa-solid fa-download"></i> <span>Export CAD</span></button>
</div>
<!-- Floating Viewport Controls -->
<div class="viewport-overlay">
<!-- Dual-Mode Selector -->
<div style="display:flex; gap:0.35rem; margin-bottom:0.75rem; background:#0b0f17; padding:0.25rem; border-radius:6px; border:1px solid var(--border-color);">
<button id="btn-mode-chassis" class="btn" style="flex:1; justify-content:center; padding:0.35rem 0.4rem; font-size:0.725rem;" onclick="setViewportMode('chassis')"><i class="fa-solid fa-server"></i> 3D Chassis</button>
<button id="btn-mode-pcb" class="btn btn-outline" style="flex:1; justify-content:center; padding:0.35rem 0.4rem; font-size:0.725rem;" onclick="setViewportMode('pcb')"><i class="fa-solid fa-microchip"></i> 4-Layer PCB</button>
</div>
<!-- Chassis Controls Container -->
<div id="controls-chassis">
<div class="overlay-title">
<span>Chassis Assembly CAD</span>
<i class="fa-solid fa-cube"></i>
</div>
<div class="control-group">
<label><span>Chassis Configuration</span></label>
<select id="variant-select" class="vendor-select" style="width:100%;" onchange="updateChassisVariant()">
<option value="8bay">Standard 8-Bay U.3 + GPU Mezzanine (500mm)</option>
<option value="16bay">Dense 16-Bay Dual Backplane (650mm)</option>
<option value="jbod">Short-Depth 15" JBOD Extrusion Frame</option>
</select>
</div>
<div class="control-group">
<label><span>Explode Assembly</span><span id="explode-val">0%</span></label>
<input type="range" id="explode-slider" class="slider-control" min="0" max="100" value="0">
</div>
<div class="control-group">
<label>Component Layers</label>
<div class="toggle-item"><span>2020 Aluminum Skeleton</span><input type="checkbox" id="layer-skeleton" checked></div>
<div class="toggle-item"><span>SendCutSend 5052 Skins</span><input type="checkbox" id="layer-sheets" checked></div>
<div class="toggle-item"><span>Modular 3D Facade</span><input type="checkbox" id="layer-facade" checked></div>
<div class="toggle-item"><span>Toolless U.3 Drive Sleds</span><input type="checkbox" id="layer-drives" checked></div>
<div class="toggle-item"><span>3x 120mm PWM Fan Wall</span><input type="checkbox" id="layer-fans" checked></div>
<div class="toggle-item"><span>Carrier Baseboard & Brains</span><input type="checkbox" id="layer-pcba" checked></div>
<div class="toggle-item"><span>X-Ray / Airflow Flow View</span><input type="checkbox" id="layer-xray"></div>
</div>
<div style="display: flex; gap: 0.35rem; margin-top: 0.5rem;">
<button class="btn btn-outline" style="flex:1; padding:0.35rem;" onclick="resetCamera('iso')">Isometric</button>
<button class="btn btn-outline" style="flex:1; padding:0.35rem;" onclick="resetCamera('front')">Front</button>
<button class="btn btn-outline" style="flex:1; padding:0.35rem;" onclick="setViewportMode('pcb')">Open PCB</button>
</div>
</div>
<!-- Dedicated PCB EDA Controls Container -->
<div id="controls-pcb" style="display:none;">
<div class="overlay-title">
<span>4-Layer PCB EDA ("The Brains")</span>
<i class="fa-solid fa-bolt"></i>
</div>
<div class="control-group">
<label><span>Active Substrate</span></label>
<select id="pcb-board-select" class="vendor-select" style="width:100%;" onchange="updatePcbBoard()">
<option value="carrier">Carrier Baseboard (RK3568 + SAS3408 + Supercaps)</option>
<option value="backplane">8-Bay Passive U.3 Backplane (Pin 11 PUIS FETs)</option>
</select>
</div>
<div class="control-group">
<label>High-Speed Net Trace Highlights</label>
<div class="toggle-item">
<span style="color:#38bdf8;"><i class="fa-solid fa-wave-square"></i> PCIe 3.0 x2 Diff (85Ω)</span>
<input type="checkbox" id="trace-pcie" checked onchange="updateTraceHighlights()">
</div>
<div class="toggle-item">
<span style="color:#34d399;"><i class="fa-solid fa-network-wired"></i> SAS 12G SerDes (100Ω)</span>
<input type="checkbox" id="trace-sas" checked onchange="updateTraceHighlights()">
</div>
<div class="toggle-item">
<span style="color:#fbbf24;"><i class="fa-solid fa-bolt"></i> 12V Bus & Supercap PLP</span>
<input type="checkbox" id="trace-pwr" checked onchange="updateTraceHighlights()">
</div>
<div class="toggle-item">
<span style="color:#c084fc;"><i class="fa-solid fa-microchip"></i> I2C SMBus & SGPIO</span>
<input type="checkbox" id="trace-i2c" checked onchange="updateTraceHighlights()">
</div>
</div>
<div class="control-group">
<label><span>Layer Stack-Up Filter</span></label>
<select id="pcb-layer-filter" class="vendor-select" style="width:100%;" onchange="updateLayerFilter()">
<option value="all">All 4 Layers (Full SMT Board)</option>
<option value="l1">Layer 1: Top Microstrips & IC Pads</option>
<option value="l2">Layer 2: Solid GND Ground Plane</option>
<option value="l3">Layer 3: +12V / +5V Power Islands</option>
<option value="l4">Layer 4: Bottom Traces & PUIS FETs</option>
</select>
</div>
<div style="display: flex; gap: 0.35rem; margin-top: 0.5rem;">
<button class="btn btn-outline" style="flex:1; padding:0.35rem;" onclick="resetCamera('pcb-top')">Top View</button>
<button class="btn btn-outline" style="flex:1; padding:0.35rem;" onclick="resetCamera('pcb-iso')">3D Angle</button>
<button class="btn btn-outline" style="flex:1; padding:0.35rem;" onclick="setViewportMode('chassis')">Exit to Case</button>
</div>
</div>
</div>
<!-- Dimension Calipers HUD -->
<div class="dimension-hud">
<div class="hud-stat"><span>EIA-310-D WIDTH</span>436.0 mm (17.17")</div>
<div class="hud-stat"><span>HEIGHT (6U)</span>260.0 mm (10.24")</div>
<div class="hud-stat"><span id="hud-depth-label">DEPTH</span><span id="hud-depth-val">500.0 mm (19.69")</span></div>
<div class="hud-stat"><span id="hud-bay-label">STORAGE DENSITY</span><span id="hud-bay-val">8x U.3 Universal Bays</span></div>
</div>
<!-- Component Inspector Pop-Up Card -->
<div id="component-inspector">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.5rem;">
<span id="comp-title" style="font-weight:700; color:var(--accent-cyan); font-size:0.85rem;">Silicon Component</span>
<button onclick="document.getElementById('component-inspector').style.display='none'" style="background:transparent; border:none; color:#999; cursor:pointer;">&times;</button>
</div>
<div id="comp-desc" style="font-size:0.75rem; color:var(--text-muted); margin-bottom:0.5rem;">Component details and pinout mapping.</div>
<div id="comp-specs" style="font-family:'JetBrains Mono', monospace; font-size:0.7rem; color:var(--accent-emerald);"></div>
</div>
</div>
<!-- Panel 1: Supply Chain & Dynamic BOM Suite -->
<div id="supply-chain" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-truck-ramp-box"></i> <span>Supply Chain & Procurement Matrix</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
<button class="panel-btn" title="Close Panel" onclick="closePanels()"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
<!-- Geopolitical Filter & Batch Volume Controls -->
<div class="card">
<div class="card-header"><i class="fa-solid fa-filter"></i> Sourcing Policy & Batch Sizing</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:0.75rem; margin-bottom:0.75rem;">
<div>
<label style="font-size:0.75rem; color:var(--text-muted); display:block; margin-bottom:0.25rem;">Origin Constraint</label>
<select id="origin-filter" class="vendor-select" style="width:100%;" onchange="updateBOMCalculations()">
<option value="non-china">100% Western / Non-China (USA/EU/TW/MX)</option>
<option value="balanced">Balanced Global (Lowest Verified Cost)</option>
</select>
</div>
<div>
<label style="font-size:0.75rem; color:var(--text-muted); display:block; margin-bottom:0.25rem;">Batch Size: <span id="batch-qty-label" style="color:var(--accent-cyan); font-weight:700;">100 Units</span></label>
<input type="range" id="batch-slider" class="slider-control" min="1" max="1000" value="100" oninput="updateBOMCalculations()">
</div>
</div>
<div style="display:flex; justify-content:space-between; align-items:center; background:#0b0f17; padding:0.6rem 0.8rem; border-radius:6px;">
<div>
<span style="font-size:0.7rem; color:var(--text-muted); display:block;">ESTIMATED TOTAL PER UNIT</span>
<span id="rollup-unit-cost" style="font-size:1.15rem; font-weight:800; color:var(--accent-emerald);">$241.20</span>
</div>
<div>
<span style="font-size:0.7rem; color:var(--text-muted); display:block;">TOTAL BATCH INVESTMENT</span>
<span id="rollup-total-cost" style="font-size:1.15rem; font-weight:800; color:var(--accent-cyan);">$24,120.00</span>
</div>
</div>
</div>
<!-- Multi-Vendor Dynamic Component Catalog -->
<div class="card">
<div class="card-header">
<span><i class="fa-solid fa-list-check"></i> Interactive Component Catalog</span>
<button class="btn" style="padding:0.25rem 0.5rem; font-size:0.7rem;" onclick="exportBOMCSV()"><i class="fa-solid fa-download"></i> Export PO (CSV)</button>
</div>
<table class="data-table">
<thead>
<tr><th>Component</th><th>Preferred Supplier</th><th>MPN</th><th>Unit Price</th></tr>
</thead>
<tbody id="bom-table-body">
<!-- Populated by JavaScript -->
</tbody>
</table>
</div>
</div>
<!-- Panel 2: Material Science & Physics Engine -->
<div id="materials" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-atom"></i> <span>Material Science & Physics Selector</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
<button class="panel-btn" title="Close Panel" onclick="closePanels()"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
<div class="card">
<div class="card-header"><i class="fa-solid fa-layer-group"></i> Active Polymer / Alloy Selector</div>
<select id="mat-select" class="vendor-select" style="width:100%; margin-bottom:1rem;" onchange="updateMaterialPhysics()">
<option value="petg">PETG (Polyethylene Terephthalate Glycol) — Recommended Default</option>
<option value="asa">ASA (Acrylonitrile Styrene Acrylate) — High UV & Thermal</option>
<option value="petg-cf">PETG-CF (Carbon Fiber Reinforced) — Industrial Matte Finish</option>
<option value="pa-cf">PA-CF (Nylon Carbon Fiber) — Extreme Stiffness</option>
<option value="pla">Standard PLA — (WARNING: Low Thermal Deflection)</option>
<option value="alu5052">5052-H32 Aluminum Sheet — Lightweight Chassis Skin</option>
</select>
<div id="mat-warning" style="display:none; background:rgba(239, 68, 68, 0.15); border:1px solid var(--accent-rose); padding:0.6rem; border-radius:6px; font-size:0.75rem; color:#fca5a5; margin-bottom:0.75rem;">
<i class="fa-solid fa-triangle-exclamation"></i> <strong>Thermal Creep Risk:</strong> Standard PLA has a glass transition temperature ($T_g \approx 55^\circ\text{C}$). It will sag permanently under prolonged 6U drive heat loads!
</div>
<!-- Material Radar Bars -->
<div class="mat-bar-container">
<div class="mat-bar-label"><span>Heat Deflection Temp (HDT @ 0.45 MPa)</span><span id="mat-hdt-val">78 °C</span></div>
<div class="mat-bar-bg"><div id="mat-hdt-bar" class="mat-bar-fill" style="width:78%; background:var(--accent-cyan);"></div></div>
</div>
<div class="mat-bar-container">
<div class="mat-bar-label"><span>Tensile Strength (MPa)</span><span id="mat-tensile-val">50 MPa</span></div>
<div class="mat-bar-bg"><div id="mat-tensile-bar" class="mat-bar-fill" style="width:50%; background:var(--accent-blue);"></div></div>
</div>
<div class="mat-bar-container">
<div class="mat-bar-label"><span>Creep Resistance under 50°C Server Heat</span><span id="mat-creep-val">High (Excellent)</span></div>
<div class="mat-bar-bg"><div id="mat-creep-bar" class="mat-bar-fill" style="width:85%; background:var(--accent-emerald);"></div></div>
</div>
<div class="mat-bar-container">
<div class="mat-bar-label"><span>Raw Material Sourcing Cost</span><span id="mat-cost-val">$18.00 / kg</span></div>
<div class="mat-bar-bg"><div id="mat-cost-bar" class="mat-bar-fill" style="width:30%; background:var(--accent-amber);"></div></div>
</div>
</div>
<div class="card">
<div class="card-header"><i class="fa-solid fa-vial"></i> Physics Recommendation for 6U Facade</div>
<p style="font-size:0.775rem; color:var(--text-muted); line-height:1.4;">
For 24/7 enterprise storage environments, <strong>PETG</strong> and <strong>ASA</strong> are the gold standards. They absorb mechanical drive harmonics ($120\,\text{Hz}$ spindle oscillations) while maintaining dimensional stability up to $80^\circ\text{C}$. For commercial sale, <strong>PETG-CF</strong> provides a cast-aluminum matte look with zero visible layer lines.
</p>
</div>
</div>
<!-- Panel 3: Distributed 3D Print Farm & Machine Profiles -->
<div id="print-farm" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-print"></i> <span>3D Print Farm & Slicer Profiles</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
<button class="panel-btn" title="Close Panel" onclick="closePanels()"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
<div class="card">
<div class="card-header"><i class="fa-solid fa-sliders"></i> Machine Profile Selector</div>
<div style="margin-bottom:0.75rem;">
<label style="font-size:0.75rem; color:var(--text-muted); display:block; margin-bottom:0.25rem;">Target 3D Printer Model</label>
<select id="printer-select" class="vendor-select" style="width:100%;" onchange="updatePrintFarmStats()">
<option value="prusa-core-l">Prusa Core One L+ (Large-Format Enclosed CoreXY + Active Chamber)</option>
<option value="prusa-core">Prusa Core One (Standard Enclosed CoreXY)</option>
<option value="bambu-a1">Bambu Lab A1 / A1 mini (256x256 / 180x180mm Bed)</option>
<option value="bambu-x1">Bambu Lab X1-Carbon / P1S (High-Speed Enclosed CoreXY)</option>
<option value="prusa-mk4">Prusa MK4 / MK3S+ (250x210x220mm Bed)</option>
<option value="prusa-xl">Prusa XL (360x360mm Multi-Toolhead Farm Sled)</option>
<option value="voron">Voron 2.4 / Custom CoreXY (300/350mm Enclosed)</option>
</select>
</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:0.5rem; background:#0b0f17; padding:0.75rem; border-radius:6px; font-size:0.75rem;">
<div><span style="color:var(--text-muted);">Bed Size Clearance:</span> <strong id="pf-bed-fit" style="color:var(--accent-emerald);">Fits 100% (Split Q1-Q4)</strong></div>
<div><span style="color:var(--text-muted);">Estimated Print Time:</span> <strong id="pf-time">13h 45m (Full Kit)</strong></div>
<div><span style="color:var(--text-muted);">Filament Consumed:</span> <strong id="pf-weight">1,180 grams</strong></div>
<div><span style="color:var(--text-muted);">Est. Electrical Cost:</span> <strong id="pf-elec">$0.62 (@ $0.15/kWh)</strong></div>
</div>
</div>
<div class="card">
<div class="card-header"><i class="fa-solid fa-download"></i> 1-Click Slicer Packages (.3MF / .INI)</div>
<div style="display:flex; flex-direction:column; gap:0.5rem;">
<a class="btn btn-outline" href="file:///home/ethanpursley/p/data/nas-ressearch/mechanical/scad/facade_quadrant.scad"><i class="fa-solid fa-file-code"></i> Download Facade Quadrants (.SCAD)</a>
<a class="btn btn-outline" href="file:///home/ethanpursley/p/data/nas-ressearch/mechanical/scad/drive_caddy_u3.scad"><i class="fa-solid fa-file-code"></i> Download Toolless U.3 Sled (.SCAD)</a>
<a class="btn btn-outline" href="file:///home/ethanpursley/p/data/nas-ressearch/mechanical/scad/fan_wall_shroud.scad"><i class="fa-solid fa-file-code"></i> Download Fan Wall Pressure Shroud (.SCAD)</a>
</div>
</div>
</div>
<!-- Panel 4: WebSerial Live Supervisor & Flasher -->
<div id="diagnostics" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-terminal"></i> <span>WebSerial Supervisor Console</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
<button class="panel-btn" title="Close Panel" onclick="closePanels()"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
<div class="card">
<div class="card-header">
<span><i class="fa-solid fa-plug"></i> Hardware Connection</span>
<button id="btn-serial-connect" class="btn" onclick="toggleSerialConnect()"><i class="fa-solid fa-link"></i> Connect RP2040</button>
</div>
<div style="font-size:0.75rem; color:var(--text-muted);">
Direct browser-to-silicon WebSerial link via USB-C to monitor live telemetry, flash Embassy-RP firmware, and test PUIS staggered spin-up gates.
</div>
</div>
<div class="card">
<div class="card-header"><i class="fa-solid fa-gauge-high"></i> Live Telemetry Stream</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:0.5rem; font-family:'JetBrains Mono', monospace; font-size:0.75rem;">
<div style="background:#0b0f17; padding:0.5rem; border-radius:4px;"><span style="color:var(--text-muted); font-size:0.65rem; display:block;">12V MAIN BUS</span><span id="tel-vin" style="color:var(--accent-cyan); font-weight:700;">12.14 V</span></div>
<div style="background:#0b0f17; padding:0.5rem; border-radius:4px;"><span style="color:var(--text-muted); font-size:0.65rem; display:block;">SUPERCAP STACK</span><span id="tel-vcap" style="color:var(--accent-emerald); font-weight:700;">10.78 V (100%)</span></div>
<div style="background:#0b0f17; padding:0.5rem; border-radius:4px;"><span style="color:var(--text-muted); font-size:0.65rem; display:block;">BACKPLANE TEMP</span><span id="tel-temp" style="color:var(--accent-amber); font-weight:700;">38.4 °C</span></div>
<div style="background:#0b0f17; padding:0.5rem; border-radius:4px;"><span style="color:var(--text-muted); font-size:0.65rem; display:block;">FAN 0/1/2 RPM</span><span id="tel-rpm" style="color:var(--accent-blue); font-weight:700;">1,850 RPM</span></div>
</div>
</div>
<div class="card">
<div class="card-header"><i class="fa-solid fa-sliders"></i> Manual Override Controls</div>
<div class="control-group">
<label><span>Fan PWM Duty Cycle Override</span><span id="pwm-override-val">Auto (PID)</span></label>
<input type="range" class="slider-control" min="20" max="100" value="35" oninput="document.getElementById('pwm-override-val').innerText = this.value + '%'">
</div>
<button class="btn btn-outline" style="width:100%; justify-content:center; color:var(--accent-rose); border-color:var(--accent-rose);" id="btn-hil-plp" onclick="triggerSupercapPlpTest()"><i class="fa-solid fa-bolt"></i> Run Live PLP Holdup Discharge Test (120W Load)</button>
<div id="hil-test-result" style="display:none; margin-top:0.5rem;"></div>
</div>
</div>
<!-- Panel 5: Interactive QA & Step-by-Step Assembly Guide -->
<div id="qa-assembly" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-screwdriver-wrench"></i> <span>Assembly QA Checklist</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
<button class="panel-btn" title="Close Panel" onclick="closePanels()"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
<div class="card">
<div class="card-header">
<span>Assembly Progress Tracker</span>
<span id="qa-progress-label" style="color:var(--accent-emerald); font-weight:700;">0% Complete</span>
</div>
<div class="mat-bar-bg" style="margin-bottom:0.75rem;"><div id="qa-progress-bar" class="mat-bar-fill" style="width:0%; background:var(--accent-emerald);"></div></div>
<div id="qa-checklist-container"><i class="fa-solid fa-spinner fa-spin"></i> Fetching live tasks from Postgres...</div>
</div>
</div>
<!-- Panel: Serialized MES Chassis Travelers -->
<div id="travelers" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-barcode"></i> <span>Chassis Manufacturing Execution (MES)</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
<button class="panel-btn" title="Close Panel" onclick="closePanels()"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
<div class="card">
<div class="card-header">
<span>Active Chassis Work Orders</span>
<button class="btn" style="padding:0.25rem 0.5rem; font-size:0.7rem;" onclick="createNewChassisTraveler()"><i class="fa-solid fa-plus"></i> New Unit Traveler</button>
</div>
<div id="travelers-list-container">
<div style="font-size:0.75rem; color:var(--text-muted);"><i class="fa-solid fa-spinner fa-spin"></i> Loading chassis travelers from Postgres...</div>
</div>
</div>
</div>
<!-- Panel: Automated OS & Provisioning Config Generator -->
<div id="provisioning" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-server"></i> <span>ZFS & OS Provisioning Studio</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
<button class="panel-btn" title="Close Panel" onclick="closePanels()"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
<div class="card">
<div class="card-header"><i class="fa-solid fa-sliders"></i> Target Array Topology</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:0.5rem; margin-bottom:0.75rem;">
<div>
<label style="font-size:0.7rem; color:var(--text-muted);">Hostname</label>
<input id="cfg-hostname" class="vendor-select" style="width:100%; padding:0.3rem;" value="nas-core">
</div>
<div>
<label style="font-size:0.7rem; color:var(--text-muted);">Domain</label>
<input id="cfg-domain" class="vendor-select" style="width:100%; padding:0.3rem;" value="atyg.org">
</div>
<div>
<label style="font-size:0.7rem; color:var(--text-muted);">ZFS RAID Level</label>
<select id="cfg-raid" class="vendor-select" style="width:100%;">
<option value="raidz2">RAID-Z2 (Dual Parity - Recommended)</option>
<option value="raidz1">RAID-Z1 (Single Parity)</option>
<option value="mirror">Mirrored VDEVs (Max IOPS)</option>
</select>
</div>
<div>
<label style="font-size:0.7rem; color:var(--text-muted);">Populated Bays</label>
<select id="cfg-bays" class="vendor-select" style="width:100%;">
<option value="8">8x Universal U.3 Bays</option>
<option value="4">4x Universal U.3 Bays</option>
</select>
</div>
</div>
<button class="btn" style="width:100%; justify-content:center;" onclick="downloadZfsProvisioningScript()"><i class="fa-solid fa-download"></i> Download 1-Click Provisioning Shell Script</button>
</div>
</div>
<!-- Panel 6: Parts Catalog & 3D CAD Component Library -->
<div id="parts-catalog" class="content-panel">
<div class="panel-header">
<div class="sheet-drag-handle"></div>
<div class="panel-title"><i class="fa-solid fa-boxes-stacked"></i> <span>Parts & 3D CAD Library</span></div>
<div class="panel-header-actions">
<button class="panel-btn" title="Minimize / Expand" onclick="toggleMinimizePanel(this)"><i class="fa-solid fa-compress"></i></button>
<button class="panel-btn" title="Close Panel" onclick="closePanels()"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
<!-- Cabling vs PCB Traces Architectural Highlight -->
<div class="card" style="border-left: 3px solid var(--accent-cyan);">
<div class="card-header"><i class="fa-solid fa-network-wired"></i> Architecture: Twinaxial Ribbon Cabling vs PCB Traces</div>
<p style="font-size:0.775rem; color:var(--text-muted); line-height:1.4; margin-bottom:0.5rem;">
<strong>Why SFF-TA-1016 (MCIO 8i) Twinax beats long PCB traces:</strong> Standard FR4 PCB traces experience heavy dielectric loss ($-1.2\,\text{dB/in}$ @ 16Gbps), degrading signals across 12-inch chassis runs and requiring $\$90+$ in active PCIe retimers. High-speed 30AWG Silver Twinax cables lose only <strong>$-0.25\,\text{dB/in}$</strong>, keeping eye diagrams pristine, lowering baseboard layer complexity, and allowing easy modular swapping!
</p>
<div style="display:flex; gap:0.5rem; font-size:0.7rem; font-family:'JetBrains Mono', monospace;">
<div style="background:#0b0f17; padding:0.35rem 0.5rem; border-radius:4px; flex:1;"><span style="color:var(--text-muted); display:block;">TWINAX LOSS</span><strong style="color:var(--accent-emerald);">-3.0 dB / 12"</strong></div>
<div style="background:#0b0f17; padding:0.35rem 0.5rem; border-radius:4px; flex:1;"><span style="color:var(--text-muted); display:block;">FR4 PCB LOSS</span><strong style="color:var(--accent-rose);">-14.4 dB / 12"</strong></div>
<div style="background:#0b0f17; padding:0.35rem 0.5rem; border-radius:4px; flex:1;"><span style="color:var(--text-muted); display:block;">RETIMER SAVINGS</span><strong style="color:var(--accent-cyan);">+$90 Saved</strong></div>
</div>
</div>
<!-- Search, Category Filter & Favorites Toggle -->
<div class="card">
<div style="display:flex; gap:0.5rem; margin-bottom:0.75rem;">
<div style="flex:1; position:relative;">
<input type="text" id="part-search-input" placeholder="Search by part name, MPN, supplier..." class="vendor-select" style="width:100%; padding-left:1.8rem;" oninput="filterPartsLibrary()">
<i class="fa-solid fa-magnifying-glass" style="position:absolute; left:0.6rem; top:0.55rem; color:var(--text-muted); font-size:0.75rem;"></i>
</div>
<button id="btn-fav-filter" class="btn btn-outline" style="padding:0.4rem 0.65rem; font-size:0.75rem;" onclick="toggleFavoriteFilter()"><i class="fa-solid fa-star" style="color:var(--accent-amber);"></i> Favorites (<span id="fav-count">0</span>)</button>
</div>
<!-- Category Filter Pills -->
<div id="category-pills" style="display:flex; flex-wrap:wrap; gap:0.35rem;">
<button class="btn btn-outline active-pill" style="padding:0.25rem 0.5rem; font-size:0.7rem; border-radius:12px;" onclick="setPartCategory('all', this)">All Parts</button>
<button class="btn btn-outline" style="padding:0.25rem 0.5rem; font-size:0.7rem; border-radius:12px;" onclick="setPartCategory('Mechanical', this)">Mechanical</button>
<button class="btn btn-outline" style="padding:0.25rem 0.5rem; font-size:0.7rem; border-radius:12px;" onclick="setPartCategory('Sheet Metal', this)">Sheet Metal</button>
<button class="btn btn-outline" style="padding:0.25rem 0.5rem; font-size:0.7rem; border-radius:12px;" onclick="setPartCategory('3D Print', this)">3D Print</button>
<button class="btn btn-outline" style="padding:0.25rem 0.5rem; font-size:0.7rem; border-radius:12px;" onclick="setPartCategory('Silicon', this)">Silicon & Compute</button>
<button class="btn btn-outline" style="padding:0.25rem 0.5rem; font-size:0.7rem; border-radius:12px;" onclick="setPartCategory('Power', this)">Power & PLP</button>
<button class="btn btn-outline" style="padding:0.25rem 0.5rem; font-size:0.7rem; border-radius:12px;" onclick="setPartCategory('Cabling', this)">Cabling</button>
</div>
</div>
<!-- Dynamic Parts Grid Container -->
<div id="parts-grid" style="display:flex; flex-direction:column; gap:0.75rem;">
<!-- Populated by JavaScript -->
</div>
</div>
</div>
<!-- Master Interactive JavaScript Application -->
<script src="./qa.js"></script>
<script src="./bom.js"></script>
<script src="./printfarm.js"></script>
<script src="./traveler.js"></script>
<script src="./hil.js"></script>
<script src="./config_gen.js"></script>
<script src="./featurelens.js"></script>
<script>
let scene, camera, renderer, controls;
let groupSkeleton, groupSheets, groupFacade, groupDrives, groupFans, groupPcba, groupIsolatedPart;
let raycaster, mouse;
let pcbClickables = [];
const compDB = {
'ltc3350': {
title: 'LTC3350 Supercapacitor PLP Manager',
desc: 'Bidirectional synchronous buck/boost controller managing 4-cell Eaton supercap stack.',
specs: 'Charged: 10.8V | Holdup: 883ms @ 120W | ADI QFN-38'
},
'broadcom': {
title: 'Broadcom SAS3408 Tri-Mode Controller',
desc: '6th-Gen Storage IOC running Fusion-MPT architecture for SAS 12G, SATA 6G, and NVMe U.3.',
specs: '>1M IOPS | Dual Cortex-A15 | 8-PHY SerDes | BGA-484'
},
'radxa': {
title: 'Radxa Compute Module 3 (RK3568)',
desc: 'Primary computational orchestrator running Linux OS, ZFS pool management, and 1.0 TOPS NPU.',
specs: 'Quad Cortex-A55 @ 2.0GHz | 4GB LPDDR4 | PCIe 3.0 x2'
},
'rp2040': {
title: 'RP2040 OpenBMC Supervisor',
desc: 'Raspberry Pi dual Cortex-M0+ running bare-metal Embedded Rust (#![no_std]) firmware.',
specs: 'FreeRTOS/Embassy | PUIS Pin 11 | 25kHz Fan PWM'
}
};
const bomData = [
{ comp: 'Radxa CM3I SoM (4G/16G)', mpn: 'CM3I-4G16G', vendor: 'Ameridroid', price: 38.00, origin: 'Taiwan' },
{ comp: 'Broadcom SAS3408 IOC', mpn: 'SAS3408-BGA484', vendor: 'Avnet / Arrow', price: 32.00, origin: 'USA' },
{ comp: 'RP2040 Dual MCU', mpn: 'RP2040-QFN56', vendor: 'DigiKey', price: 0.95, origin: 'UK' },
{ comp: 'LTC3350 Supercap IC', mpn: 'LTC3350EUHF#PBF', vendor: 'Mouser', price: 7.45, origin: 'USA' },
{ comp: 'LM74700 Ideal Diode', mpn: 'LM74700QDBVRQ1', vendor: 'Mouser', price: 0.95, origin: 'USA' },
{ comp: 'TPS25982 eFuse', mpn: 'TPS259827ONRGER', vendor: 'DigiKey', price: 1.85, origin: 'USA' },
{ comp: '10F 2.7V Supercaps (x4)', mpn: 'B0510-2R7105-R', vendor: 'Mouser', price: 4.40, origin: 'USA' },
{ comp: 'MCIO 8i Connectors (x2)', mpn: 'G88MP08102CEEU', vendor: 'DigiKey', price: 7.60, origin: 'USA' },
{ comp: 'U.3 Sockets (x8)', mpn: '10148784-101LF', vendor: 'Mouser', price: 11.20, origin: 'USA' },
{ comp: 'MCIO Twinax Cable', mpn: 'DRWC-013702', vendor: 'JPC Connectivity', price: 12.00, origin: 'Taiwan' },
{ comp: 'Carrier Baseboard PCBA', mpn: 'PCB-BASE-01', vendor: 'MacroFab', price: 16.50, origin: 'USA/MX' },
{ comp: 'Passive U.3 Backplane', mpn: 'PCB-BP-01', vendor: 'MacroFab', price: 12.50, origin: 'USA/MX' },
{ comp: '2020 Extrusion Kit', mpn: '20-2020-KIT', vendor: '80/20 Inc.', price: 14.00, origin: 'USA' },
{ comp: 'SendCutSend 5052 Skins', mpn: 'SMP-ALL-01', vendor: 'SendCutSend', price: 26.80, origin: 'USA' },
{ comp: 'Fasteners & Corner Gussets', mpn: 'FAST-KIT-01', vendor: 'McMaster-Carr', price: 16.00, origin: 'USA' },
{ comp: '3D Facade & Sleds Kit', mpn: 'PRINT-KIT-01', vendor: 'Local Print Hub', price: 17.00, origin: 'Local Hub' }
];
let ambientLight, dirLight, fillLight, gridHelper;
function init3D() {
const container = document.getElementById('viewport-container');
const canvas = document.getElementById('webgl-canvas');
scene = new THREE.Scene();
window.scene = scene;
camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 1, 3000);
window.camera = camera;
camera.position.set(650, 450, 750);
renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, alpha: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.touches = {
ONE: THREE.TOUCH.ROTATE,
TWO: THREE.TOUCH.DOLLY_PAN
};
controls.minDistance = 200;
controls.maxDistance = 1800;
// Lighting
ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
dirLight = new THREE.DirectionalLight(0x38bdf8, 0.85);
dirLight.position.set(400, 600, 500);
dirLight.castShadow = true;
scene.add(dirLight);
fillLight = new THREE.DirectionalLight(0xffffff, 0.4);
fillLight.position.set(-400, -300, -500);
scene.add(fillLight);
// Ground Grid
gridHelper = new THREE.GridHelper(1200, 24, 0x374151, 0x1f2937);
gridHelper.position.y = -135;
scene.add(gridHelper);
// Raycasting for PCB inspection
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
buildAssembly(500, 8);
renderBOMTable();
animate();
window.addEventListener('resize', onWindowResize);
canvas.addEventListener('click', onCanvasClick);
}
// Materials
const matExtrusion = new THREE.MeshStandardMaterial({ color: 0x94a3b8, metalness: 0.85, roughness: 0.25 });
const matSheet = new THREE.MeshStandardMaterial({ color: 0x64748b, metalness: 0.6, roughness: 0.4 });
const matFacade = new THREE.MeshStandardMaterial({ color: 0x1e293b, roughness: 0.8, metalness: 0.1 });
const matCaddy = new THREE.MeshStandardMaterial({ color: 0x0284c7, roughness: 0.5, metalness: 0.3 });
const matDriveMetal = new THREE.MeshStandardMaterial({ color: 0xcfd8dc, metalness: 0.9, roughness: 0.2 });
const matFanFrame = new THREE.MeshStandardMaterial({ color: 0x0f172a, roughness: 0.7 });
const matPcba = new THREE.MeshStandardMaterial({ color: 0x065f46, roughness: 0.4, metalness: 0.2 });
const matChip = new THREE.MeshStandardMaterial({ color: 0x111827, roughness: 0.2, metalness: 0.8 });
const matSupercap = new THREE.MeshStandardMaterial({ color: 0x1e3a8a, roughness: 0.3, metalness: 0.5 });
const matRackEar = new THREE.MeshStandardMaterial({ color: 0x334155, metalness: 0.8, roughness: 0.3 });
// GPU VRAM Garbage Collection Utility
function disposeHierarchy(obj) {
if (!obj) return;
obj.traverse((child) => {
if (child.isMesh) {
if (child.geometry) child.geometry.dispose();
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach(m => m && m.dispose());
} else {
child.material.dispose();
}
}
}
});
}
function removeAndDisposeGroup(g) {
if (!g) return;
disposeHierarchy(g);
scene.remove(g);
}
function buildAssembly(variant = '8bay') {
if (groupIsolatedPart) {
removeAndDisposeGroup(groupIsolatedPart);
groupIsolatedPart = null;
}
// Clear existing groups and free GPU VRAM
[groupSkeleton, groupSheets, groupFacade, groupDrives, groupFans, groupPcba, groupPcbEda].forEach(g => {
if (g) removeAndDisposeGroup(g);
});
pcbClickables = [];
groupSkeleton = new THREE.Group();
groupSheets = new THREE.Group();
groupFacade = new THREE.Group();
groupDrives = new THREE.Group();
groupFans = new THREE.Group();
groupPcba = new THREE.Group();
let w = 436, h = 260, d = 500, t = 20;
if (variant === '16bay') {
d = 650; // Extended length for 16 dual-row drives
} else if (variant === 'jbod') {
d = 380; // Short 15" depth
}
function addBeam(bw, bh, bd, x, y, z) {
const geom = new THREE.BoxGeometry(bw, bh, bd);
const mesh = new THREE.Mesh(geom, matExtrusion);
mesh.position.set(x, y, z);
mesh.castShadow = true;
groupSkeleton.add(mesh);
}
// 1. 2020 Aluminum Skeleton
addBeam(t, h, t, -w/2 + t/2, 0, -d/2 + t/2);
addBeam(t, h, t, w/2 - t/2, 0, -d/2 + t/2);
addBeam(t, h, t, -w/2 + t/2, 0, d/2 - t/2);
addBeam(t, h, t, w/2 - t/2, 0, d/2 - t/2);
addBeam(t, t, d - 2*t, -w/2 + t/2, h/2 - t/2, 0);
addBeam(t, t, d - 2*t, w/2 - t/2, h/2 - t/2, 0);
addBeam(t, t, d - 2*t, -w/2 + t/2, -h/2 + t/2, 0);
addBeam(t, t, d - 2*t, w/2 - t/2, -h/2 + t/2, 0);
addBeam(w - 2*t, t, t, 0, h/2 - t/2, -d/2 + t/2);
addBeam(w - 2*t, t, t, 0, h/2 - t/2, d/2 - t/2);
addBeam(w - 2*t, t, t, 0, -h/2 + t/2, -d/2 + t/2);
addBeam(w - 2*t, t, t, 0, -h/2 + t/2, d/2 - t/2);
// 2. Sheet Metal Panels
const topMesh = new THREE.Mesh(new THREE.BoxGeometry(w, 2, d), matSheet);
topMesh.position.set(0, h/2 + 1, 0);
groupSheets.add(topMesh);
const botMesh = topMesh.clone();
botMesh.position.set(0, -h/2 - 1, 0);
groupSheets.add(botMesh);
const leftMesh = new THREE.Mesh(new THREE.BoxGeometry(2, h, d), matSheet);
leftMesh.position.set(-w/2 - 1, 0, 0);
groupSheets.add(leftMesh);
const rightMesh = leftMesh.clone();
rightMesh.position.set(w/2 + 1, 0, 0);
groupSheets.add(rightMesh);
const rearMesh = new THREE.Mesh(new THREE.BoxGeometry(w - 2*t, h - 2*t, 2), matSheet);
rearMesh.position.set(0, 0, d/2);
groupSheets.add(rearMesh);
const leftEar = new THREE.Mesh(new THREE.BoxGeometry(40, h, 4), matRackEar);
leftEar.position.set(-w/2 - 20, 0, -d/2 + 2);
groupSheets.add(leftEar);
const rightEar = leftEar.clone();
rightEar.position.set(w/2 + 20, 0, -d/2 + 2);
groupSheets.add(rightEar);
// 3. Facade
const facadeMesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, 18), matFacade);
facadeMesh.position.set(0, 0, -d/2 - 9);
groupFacade.add(facadeMesh);
// 4. Drive Sleds & Fan Walls based on variant
const bayPitch = 49.5;
const matCaddyAmber = new THREE.MeshStandardMaterial({ color: 0xd97706, roughness: 0.5, metalness: 0.3 });
if (variant === '8bay') {
// Front 8x U.3 Drives
for (let i = 0; i < 8; i++) {
const driveX = -w/2 + 45 + i * bayPitch;
const caddy = new THREE.Mesh(new THREE.BoxGeometry(26, 140, 160), matCaddy);
caddy.position.set(driveX, 10, -d/2 + 85);
groupDrives.add(caddy);
}
// Mid 3x 120mm Fan Wall
addBeam(w - 2*t, t, t, 0, h/2 - t/2, -105);
addBeam(w - 2*t, t, t, 0, -h/2 + t/2, -105);
const fanWallMesh = new THREE.Mesh(new THREE.BoxGeometry(w - 2*t, h - 2*t, 25), matFanFrame);
fanWallMesh.position.set(0, 0, -105);
groupFans.add(fanWallMesh);
for (let f = -1; f <= 1; f++) {
const fanHole = new THREE.Mesh(new THREE.CylinderGeometry(58, 58, 28, 32), new THREE.MeshStandardMaterial({ color: 0x0284c7 }));
fanHole.rotation.x = Math.PI / 2;
fanHole.position.set(f * 130, 0, -105);
groupFans.add(fanHole);
}
// Mezzanine Vertical GPU Accelerator Card
const gpuPcb = new THREE.Mesh(new THREE.BoxGeometry(4, 110, 240), new THREE.MeshStandardMaterial({ color: 0x1e293b, metalness: 0.5 }));
gpuPcb.position.set(110, 0, 100);
const gpuHeatsink = new THREE.Mesh(new THREE.BoxGeometry(35, 95, 230), new THREE.MeshStandardMaterial({ color: 0x64748b, metalness: 0.9, roughness: 0.2 }));
gpuHeatsink.position.set(130, 0, 100);
groupPcba.add(gpuPcb);
groupPcba.add(gpuHeatsink);
} else if (variant === '16bay') {
// Tier 1: Front 8 Drives (Blue)
for (let i = 0; i < 8; i++) {
const driveX = -w/2 + 45 + i * bayPitch;
const caddy = new THREE.Mesh(new THREE.BoxGeometry(26, 140, 150), matCaddy);
caddy.position.set(driveX, 10, -d/2 + 80);
groupDrives.add(caddy);
}
// Fan Wall 1 (between tier 1 and tier 2)
const fan1 = new THREE.Mesh(new THREE.BoxGeometry(w - 2*t, h - 2*t, 25), matFanFrame);
fan1.position.set(0, 0, -d/2 + 180);
groupFans.add(fan1);
// Tier 2: Mid 8 Drives (Amber)
for (let i = 0; i < 8; i++) {
const driveX = -w/2 + 45 + i * bayPitch;
const caddy2 = new THREE.Mesh(new THREE.BoxGeometry(26, 140, 150), matCaddyAmber);
caddy2.position.set(driveX, 10, -d/2 + 280);
groupDrives.add(caddy2);
}
// Fan Wall 2 (Push-Pull Exhaust)
const fan2 = new THREE.Mesh(new THREE.BoxGeometry(w - 2*t, h - 2*t, 25), matFanFrame);
fan2.position.set(0, 0, -d/2 + 380);
groupFans.add(fan2);
} else if (variant === 'jbod') {
// 8 Front Drives (JBOD direct layout)
for (let i = 0; i < 8; i++) {
const driveX = -w/2 + 45 + i * bayPitch;
const caddy = new THREE.Mesh(new THREE.BoxGeometry(26, 140, 150), matCaddy);
caddy.position.set(driveX, 10, -d/2 + 80);
groupDrives.add(caddy);
}
// Single High-Pressure Fan Wall at Y=160
const jbodFan = new THREE.Mesh(new THREE.BoxGeometry(w - 2*t, h - 2*t, 25), matFanFrame);
jbodFan.position.set(0, 0, 10);
groupFans.add(jbodFan);
}
// 5. Carrier Baseboard PCB & Interactive Chips
const pcbaMesh = new THREE.Mesh(new THREE.BoxGeometry(180, 4, 140), matPcba);
pcbaMesh.position.set(-60, -h/2 + 15, variant === '16bay' ? 180 : 80);
groupPcba.add(pcbaMesh);
// Broadcom SAS3408 BGA
const bcmChip = new THREE.Mesh(new THREE.BoxGeometry(25, 4, 25), matChip);
bcmChip.position.set(-80, -h/2 + 19, variant === '16bay' ? 190 : 90);
bcmChip.userData = { id: 'broadcom' };
groupPcba.add(bcmChip);
pcbClickables.push(bcmChip);
// Radxa CM3
const radxaSoM = new THREE.Mesh(new THREE.BoxGeometry(40, 4, 55), new THREE.MeshStandardMaterial({ color: 0x047857 }));
radxaSoM.position.set(-30, -h/2 + 19, variant === '16bay' ? 200 : 100);
radxaSoM.userData = { id: 'radxa' };
groupPcba.add(radxaSoM);
pcbClickables.push(radxaSoM);
// RP2040 MCU
const rpChip = new THREE.Mesh(new THREE.BoxGeometry(12, 3, 12), matChip);
rpChip.position.set(-80, -h/2 + 19, variant === '16bay' ? 150 : 50);
rpChip.userData = { id: 'rp2040' };
groupPcba.add(rpChip);
pcbClickables.push(rpChip);
// LTC3350 & 4x Eaton Supercaps
const ltcChip = new THREE.Mesh(new THREE.BoxGeometry(14, 3, 14), matChip);
ltcChip.position.set(-120, -h/2 + 19, variant === '16bay' ? 170 : 70);
ltcChip.userData = { id: 'ltc3350' };
groupPcba.add(ltcChip);
pcbClickables.push(ltcChip);
for (let c = 0; c < 4; c++) {
const capMesh = new THREE.Mesh(new THREE.CylinderGeometry(8, 8, 30, 16), matSupercap);
capMesh.position.set(-120 + c * 20, -h/2 + 30, variant === '16bay' ? 140 : 40);
capMesh.userData = { id: 'ltc3350' };
groupPcba.add(capMesh);
pcbClickables.push(capMesh);
}
scene.add(groupSkeleton);
scene.add(groupSheets);
scene.add(groupFacade);
scene.add(groupDrives);
scene.add(groupFans);
scene.add(groupPcba);
groupSkeleton.userData = { explodeDir: new THREE.Vector3(0, 0, 0) };
groupSheets.userData = { explodeDir: new THREE.Vector3(0, 1.2, 0) };
groupFacade.userData = { explodeDir: new THREE.Vector3(0, 0, -1.8) };
groupDrives.userData = { explodeDir: new THREE.Vector3(0, 0, -1.0) };
groupFans.userData = { explodeDir: new THREE.Vector3(0, 0, 0.4) };
groupPcba.userData = { explodeDir: new THREE.Vector3(0, 1.4, 0) };
// Re-apply active user controls (layer visibility, x-ray, exploded positions)
applyActiveFormState();
}
function applyActiveFormState() {
// Layer Visibility
groupSkeleton.visible = document.getElementById('layer-skeleton').checked;
groupSheets.visible = document.getElementById('layer-sheets').checked;
groupFacade.visible = document.getElementById('layer-facade').checked;
groupDrives.visible = document.getElementById('layer-drives').checked;
groupFans.visible = document.getElementById('layer-fans').checked;
groupPcba.visible = document.getElementById('layer-pcba').checked;
// X-Ray Mode
const isXray = document.getElementById('layer-xray').checked;
matSheet.transparent = isXray; matSheet.opacity = isXray ? 0.25 : 1.0;
matFacade.transparent = isXray; matFacade.opacity = isXray ? 0.35 : 1.0;
// Explode Offset
const factor = parseFloat(document.getElementById('explode-slider').value) / 100.0;
document.getElementById('explode-val').innerText = `${document.getElementById('explode-slider').value}%`;
[groupSheets, groupFacade, groupDrives, groupFans, groupPcba].forEach(grp => {
if (grp && grp.userData.explodeDir) {
const dir = grp.userData.explodeDir;
grp.position.set(dir.x * factor * 160, dir.y * factor * 140, dir.z * factor * 220);
}
});
if (typeof saveSettings === 'function') saveSettings();
}
// Explode Slider Listener
document.getElementById('explode-slider').addEventListener('input', applyActiveFormState);
// Layer Toggles
document.getElementById('layer-skeleton').addEventListener('change', applyActiveFormState);
document.getElementById('layer-sheets').addEventListener('change', applyActiveFormState);
document.getElementById('layer-facade').addEventListener('change', applyActiveFormState);
document.getElementById('layer-drives').addEventListener('change', applyActiveFormState);
document.getElementById('layer-fans').addEventListener('change', applyActiveFormState);
document.getElementById('layer-pcba').addEventListener('change', applyActiveFormState);
document.getElementById('layer-xray').addEventListener('change', applyActiveFormState);
function updateChassisVariant() {
const v = document.getElementById('variant-select').value;
buildAssembly(v);
updateDimensionHUD();
}
let currentViewportMode = 'chassis';
let groupPcbEda = new THREE.Group();
let tracePcieGroup = new THREE.Group();
let traceSasGroup = new THREE.Group();
let tracePwrGroup = new THREE.Group();
let traceI2cGroup = new THREE.Group();
function setViewportMode(mode) {
currentViewportMode = mode;
const btnChassis = document.getElementById('btn-mode-chassis');
const btnPcb = document.getElementById('btn-mode-pcb');
const ctrlChassis = document.getElementById('controls-chassis');
const ctrlPcb = document.getElementById('controls-pcb');
if (mode === 'chassis') {
btnChassis.className = 'btn';
btnPcb.className = 'btn btn-outline';
ctrlChassis.style.display = 'block';
ctrlPcb.style.display = 'none';
if (groupPcbEda) scene.remove(groupPcbEda);
buildAssembly(document.getElementById('variant-select').value);
resetCamera('iso');
} else if (mode === 'pcb') {
btnPcb.className = 'btn';
btnChassis.className = 'btn btn-outline';
ctrlPcb.style.display = 'block';
ctrlChassis.style.display = 'none';
// Remove chassis groups
[groupSkeleton, groupSheets, groupFacade, groupDrives, groupFans, groupPcba].forEach(g => {
if (g) removeAndDisposeGroup(g);
});
buildPcbScene(document.getElementById('pcb-board-select').value);
resetCamera('pcb-iso');
}
}
function buildPcbScene(boardType = 'carrier') {
if (groupIsolatedPart) {
removeAndDisposeGroup(groupIsolatedPart);
groupIsolatedPart = null;
}
if (groupPcbEda) removeAndDisposeGroup(groupPcbEda);
groupPcbEda = new THREE.Group();
pcbClickables = [];
tracePcieGroup = new THREE.Group();
traceSasGroup = new THREE.Group();
tracePwrGroup = new THREE.Group();
traceI2cGroup = new THREE.Group();
const matPcbSubstrate = new THREE.MeshStandardMaterial({ color: 0x064e3b, roughness: 0.3, metalness: 0.2 });
const matGold = new THREE.MeshStandardMaterial({ color: 0xf59e0b, roughness: 0.2, metalness: 0.9 });
const matSilicon = new THREE.MeshStandardMaterial({ color: 0x111827, roughness: 0.2, metalness: 0.8 });
const matHeatspreader = new THREE.MeshStandardMaterial({ color: 0xd1d5db, roughness: 0.15, metalness: 0.95 });
const matConnector = new THREE.MeshStandardMaterial({ color: 0x1e293b, roughness: 0.5, metalness: 0.6 });
if (boardType === 'carrier') {
// 4-Layer Carrier Baseboard (240mm x 150mm x 1.6mm)
const boardMesh = new THREE.Mesh(new THREE.BoxGeometry(240, 3, 150), matPcbSubstrate);
boardMesh.position.set(0, 0, 0);
groupPcbEda.add(boardMesh);
// Silkscreen Border & Gold Edge Chamfers
const goldEdge1 = new THREE.Mesh(new THREE.BoxGeometry(238, 3.2, 4), matGold);
goldEdge1.position.set(0, 0, 72);
groupPcbEda.add(goldEdge1);
// 1. Radxa CM3I Compute SoM (55mm x 40mm)
const cm3Mesh = new THREE.Mesh(new THREE.BoxGeometry(55, 4, 40), new THREE.MeshStandardMaterial({ color: 0x047857 }));
cm3Mesh.position.set(-65, 3.5, 30);
cm3Mesh.userData = { id: 'radxa' };
groupPcbEda.add(cm3Mesh);
pcbClickables.push(cm3Mesh);
// RK3568 SoC on CM3
const rkChip = new THREE.Mesh(new THREE.BoxGeometry(19, 2, 19), matSilicon);
rkChip.position.set(-65, 6, 30);
rkChip.userData = { id: 'radxa' };
groupPcbEda.add(rkChip);
pcbClickables.push(rkChip);
// 2. Broadcom SAS3408 Tri-Mode Storage IOC (27mm x 27mm BGA-484)
const bcmBase = new THREE.Mesh(new THREE.BoxGeometry(27, 3, 27), matSilicon);
bcmBase.position.set(10, 3, 30);
bcmBase.userData = { id: 'broadcom' };
groupPcbEda.add(bcmBase);
const bcmLid = new THREE.Mesh(new THREE.BoxGeometry(23, 1.5, 23), matHeatspreader);
bcmLid.position.set(10, 5, 30);
bcmLid.userData = { id: 'broadcom' };
groupPcbEda.add(bcmLid);
pcbClickables.push(bcmBase);
pcbClickables.push(bcmLid);
// 3. Raspberry Pi RP2040 MCU (12mm x 12mm QFN-56)
const rp2040Mesh = new THREE.Mesh(new THREE.BoxGeometry(12, 2.5, 12), matSilicon);
rp2040Mesh.position.set(-20, 2.8, -40);
rp2040Mesh.userData = { id: 'rp2040' };
groupPcbEda.add(rp2040Mesh);
pcbClickables.push(rp2040Mesh);
// 4. Analog Devices LTC3350 Supercapacitor PLP Controller
const ltcMesh = new THREE.Mesh(new THREE.BoxGeometry(14, 2.5, 14), matSilicon);
ltcMesh.position.set(65, 2.8, -35);
ltcMesh.userData = { id: 'ltc3350' };
groupPcbEda.add(ltcMesh);
pcbClickables.push(ltcMesh);
// 4x Eaton 10.0F 2.7V Supercapacitors (D=10mm, H=35mm horizontal)
for (let i = 0; i < 4; i++) {
const capGeom = new THREE.CylinderGeometry(8, 8, 32, 18);
const capMesh = new THREE.Mesh(capGeom, matSupercap);
capMesh.rotation.z = Math.PI / 2;
capMesh.position.set(55 + (i % 2) * 35, 10, -55 + Math.floor(i / 2) * 22);
capMesh.userData = { id: 'ltc3350' };
groupPcbEda.add(capMesh);
pcbClickables.push(capMesh);
}
// 5. 2x SFF-TA-1016 (MCIO 8i) 74-Pin Connectors
for (let m = 0; m < 2; m++) {
const mcio = new THREE.Mesh(new THREE.BoxGeometry(32, 10, 15), matConnector);
mcio.position.set(80, 6.5, 20 + m * 30);
groupPcbEda.add(mcio);
}
// 6. Glowing High-Speed Trace Networks
// (a) PCIe 3.0 x2 Diff Pairs (RK3568 -> AC Caps -> SAS3408)
const matTracePcie = new THREE.MeshBasicMaterial({ color: 0x38bdf8 });
for (let t = -2; t <= 2; t++) {
const trace = new THREE.Mesh(new THREE.BoxGeometry(38, 0.5, 1.2), matTracePcie);
trace.position.set(-27, 2, 26 + t * 3);
tracePcieGroup.add(trace);
}
// (b) SAS 12G SerDes Channels (SAS3408 -> MCIO 8i)
const matTraceSas = new THREE.MeshBasicMaterial({ color: 0x34d399 });
for (let s = -3; s <= 3; s++) {
const trace = new THREE.Mesh(new THREE.BoxGeometry(45, 0.5, 1.2), matTraceSas);
trace.position.set(45, 2, 22 + s * 5);
traceSasGroup.add(trace);
}
// (c) 12V Power & Supercap Rail
const matTracePwr = new THREE.MeshBasicMaterial({ color: 0xfbbf24 });
const pwrPlane = new THREE.Mesh(new THREE.BoxGeometry(40, 0.5, 30), matTracePwr);
pwrPlane.position.set(45, 2, -35);
tracePwrGroup.add(pwrPlane);
// (d) I2C SMBus & SGPIO Sidebands
const matTraceI2c = new THREE.MeshBasicMaterial({ color: 0xc084fc });
const i2cTrace = new THREE.Mesh(new THREE.BoxGeometry(60, 0.5, 1.5), matTraceI2c);
i2cTrace.position.set(0, 2, -15);
traceI2cGroup.add(i2cTrace);
groupPcbEda.add(tracePcieGroup);
groupPcbEda.add(traceSasGroup);
groupPcbEda.add(tracePwrGroup);
groupPcbEda.add(traceI2cGroup);
} else if (boardType === 'backplane') {
// 8-Bay Passive U.3 Backplane (430mm x 110mm)
const bpMesh = new THREE.Mesh(new THREE.BoxGeometry(430, 3, 110), matPcbSubstrate);
groupPcbEda.add(bpMesh);
// 8x U.3 Drive Sockets (SFF-TA-1001)
for (let b = 0; b < 8; b++) {
const sock = new THREE.Mesh(new THREE.BoxGeometry(22, 12, 45), matConnector);
sock.position.set(-180 + b * 51.4, 7.5, 0);
groupPcbEda.add(sock);
// Pin 11 2N7002 FET
const fet = new THREE.Mesh(new THREE.BoxGeometry(4, 1.5, 4), matSilicon);
fet.position.set(-180 + b * 51.4, 2.5, -30);
groupPcbEda.add(fet);
}
// TI TCA9539 GPIO Expander
const tca = new THREE.Mesh(new THREE.BoxGeometry(10, 2, 8), matSilicon);
tca.position.set(0, 2.5, -35);
tca.userData = { id: 'rp2040' };
groupPcbEda.add(tca);
pcbClickables.push(tca);
// 2x MCIO 8i Inputs
for (let i = -1; i <= 1; i += 2) {
const mcioIn = new THREE.Mesh(new THREE.BoxGeometry(32, 10, 15), matConnector);
mcioIn.position.set(i * 80, 6.5, -35);
groupPcbEda.add(mcioIn);
}
}
scene.add(groupPcbEda);
}
function updateTraceHighlights() {
tracePcieGroup.visible = document.getElementById('trace-pcie').checked;
traceSasGroup.visible = document.getElementById('trace-sas').checked;
tracePwrGroup.visible = document.getElementById('trace-pwr').checked;
traceI2cGroup.visible = document.getElementById('trace-i2c').checked;
}
function updateLayerFilter() {
const l = document.getElementById('pcb-layer-filter').value;
if (l === 'all' || l === 'l1') {
tracePcieGroup.visible = true;
traceSasGroup.visible = true;
} else {
tracePcieGroup.visible = false;
traceSasGroup.visible = false;
}
}
function updatePcbBoard() {
buildPcbScene(document.getElementById('pcb-board-select').value);
}
function resetCamera(view) {
if (view === 'iso') {
camera.position.set(650, 450, 750);
controls.target.set(0, 0, 0);
} else if (view === 'front') {
camera.position.set(0, 0, -900);
controls.target.set(0, 0, 0);
} else if (view === 'pcb-top') {
camera.position.set(0, 280, 0.1);
controls.target.set(0, 0, 0);
} else if (view === 'pcb-iso') {
camera.position.set(160, 180, 190);
controls.target.set(0, 0, 0);
}
}
function onCanvasClick(event) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(pcbClickables);
if (intersects.length > 0) {
const compId = intersects[0].object.userData.id;
const data = compDB[compId];
if (data) {
document.getElementById('comp-title').innerText = data.title;
document.getElementById('comp-desc').innerText = data.desc;
document.getElementById('comp-specs').innerText = data.specs;
document.getElementById('component-inspector').style.display = 'block';
}
}
}
function renderBOMTable() {
const tbody = document.getElementById('bom-table-body');
tbody.innerHTML = '';
bomData.forEach((item, idx) => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td><strong>${item.comp}</strong><br><span style="font-size:0.65rem; color:var(--text-muted);">${item.origin}</span></td>
<td>
<select class="vendor-select" onchange="changeVendor(${idx}, this.value)">
<option value="${item.vendor}">${item.vendor} (Primary)</option>
<option value="DigiKey">DigiKey (Alt)</option>
<option value="Mouser">Mouser (Alt)</option>
<option value="Local Sourcing">Local Sourcing</option>
</select>
</td>
<td><code style="color:var(--accent-cyan); font-size:0.7rem;">${item.mpn}</code></td>
<td><strong style="color:var(--accent-emerald);">$${item.price.toFixed(2)}</strong></td>
`;
tbody.appendChild(tr);
});
updateBOMCalculations();
}
function changeVendor(idx, newVendor) {
bomData[idx].vendor = newVendor;
updateBOMCalculations();
}
function updateBOMCalculations() {
const batchQty = parseInt(document.getElementById('batch-slider').value);
document.getElementById('batch-qty-label').innerText = `${batchQty} Units`;
// Calculate volume discount factor
let discountFactor = 1.0;
if (batchQty >= 1000) discountFactor = 0.85;
else if (batchQty >= 100) discountFactor = 0.92;
else if (batchQty >= 10) discountFactor = 0.96;
const baseTotal = bomData.reduce((acc, i) => acc + i.price, 0);
const unitCost = baseTotal * discountFactor;
const totalBatch = unitCost * batchQty;
document.getElementById('rollup-unit-cost').innerText = `$${unitCost.toFixed(2)}`;
document.getElementById('rollup-total-cost').innerText = `$${totalBatch.toLocaleString('en-US', {minimumFractionDigits:2, maximumFractionDigits:2})}`;
}
function exportBOMCSV() {
let csv = "Component,MPN,Supplier,CountryOfOrigin,UnitPriceUSD\n";
bomData.forEach(i => {
csv += `"${i.comp}","${i.mpn}","${i.vendor}","${i.origin}",${i.price}\n`;
});
const blob = new Blob([csv], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.setAttribute('href', url);
a.setAttribute('download', '6U_Storage_Array_Master_PO.csv');
a.click();
}
const materialDB = {
'petg': { hdt: 78, tensile: 50, creep: 'High (Optimal)', cost: 18.00, warn: false },
'asa': { hdt: 95, tensile: 55, creep: 'Extreme (Server Grade)', cost: 24.00, warn: false },
'petg-cf': { hdt: 84, tensile: 68, creep: 'Extreme (Matte Finish)', cost: 32.00, warn: false },
'pa-cf': { hdt: 120, tensile: 95, creep: 'Maximum (Industrial)', cost: 48.00, warn: false },
'pla': { hdt: 52, tensile: 45, creep: 'POOR (Sags >50°C)', cost: 14.00, warn: true },
'alu5052': { hdt: 250, tensile: 228, creep: 'Impervious (Metal)', cost: 6.50, warn: false }
};
function updateMaterialPhysics() {
const key = document.getElementById('mat-select').value;
const m = materialDB[key];
document.getElementById('mat-warning').style.display = m.warn ? 'block' : 'none';
document.getElementById('mat-hdt-val').innerText = `${m.hdt} °C`;
document.getElementById('mat-hdt-bar').style.width = `${Math.min(100, m.hdt / 2.5)}%`;
document.getElementById('mat-tensile-val').innerText = `${m.tensile} MPa`;
document.getElementById('mat-tensile-bar').style.width = `${Math.min(100, m.tensile / 1.2)}%`;
document.getElementById('mat-creep-val').innerText = m.creep;
document.getElementById('mat-cost-val').innerText = `$${m.cost.toFixed(2)} / kg`;
document.getElementById('mat-cost-bar').style.width = `${Math.min(100, m.cost * 1.8)}%`;
}
const printStats = {
'prusa-core-l': { time: '10h 30m', weight: '1,180g', elec: '$0.54', fit: 'Fits Entire Kit in 12 Plates (Large-Format Bed)' },
'prusa-core': { time: '12h 45m', weight: '1,180g', elec: '$0.62', fit: 'Fits 100% (Active Heated Chamber)' },
'bambu-a1': { time: '14h 10m', weight: '1,180g', elec: '$0.58', fit: 'Fits 100% (Split Q1-Q4 Bed Plate)' },
'bambu-x1': { time: '11h 20m', weight: '1,180g', elec: '$0.64', fit: 'Fits 100% (High Speed CoreXY)' },
'prusa-mk4': { time: '15h 30m', weight: '1,180g', elec: '$0.55', fit: 'Fits 100% (Standard Bed)' },
'prusa-xl': { time: '9h 15m', weight: '1,180g', elec: '$0.75', fit: 'Fits Entire Kit in 1 Bed Plate' },
'voron': { time: '10h 40m', weight: '1,180g', elec: '$0.70', fit: 'Fits 100% (350mm Plate)' }
};
const partsLibrary = [
{ id: 'ext-2020', name: '2020 Aluminum Extrusion Skeleton Kit (6.3m)', cat: 'Mechanical', mpn: '20-2020-KIT', vendor: '80/20 Inc. (Indiana)', cost: 14.00, origin: 'USA', desc: '18x Precision-cut 6063-T6 aluminum T-slot rails with 5.0mm center tap for M5 bolts.' },
{ id: 'scs-top', name: 'SendCutSend 5052-H32 Aluminum Top/Bottom Panels', cat: 'Sheet Metal', mpn: 'SMP-01-TOP', vendor: 'SendCutSend (NV/KY)', cost: 26.80, origin: 'USA', desc: '1.27mm bend-free laser cut skins with countersunk M4 mounting perimeter.' },
{ id: 'scs-ears', name: '6U Heavy-Duty EIA-310-D Rack Mounting Ears (Pair)', cat: 'Sheet Metal', mpn: 'SMP-06L/R', vendor: 'SendCutSend (NV/KY)', cost: 6.50, origin: 'USA', desc: '3.0mm 5052-H32 structural rack ears with 465.1mm center-to-center standard spacing.' },
{ id: 'facade-quad', name: 'Modular 3D-Printed Dovetail Facade (Quadrant)', cat: '3D Print', mpn: 'FACADE-Q1-Q4', vendor: 'Local Print Hub (PETG/ASA)', cost: 8.50, origin: 'Local Hub', desc: '60° Interlocking wedge dovetail facade with honeycomb intake and LED light pipes.' },
{ id: 'caddy-u3', name: 'Toolless U.3 / 3.5" Dual-Mode Drive Sled', cat: '3D Print', mpn: 'CADDY-U3-V2', vendor: 'Local Print Hub (PETG)', cost: 1.20, origin: 'Local Hub', desc: 'Dual-mode caddy with silicone vibration isolation grommets and flexible cam latch.' },
{ id: 'fan-shroud', name: '3x 120mm Bellmouth Pressure Shroud', cat: '3D Print', mpn: 'FAN-SHROUD-120', vendor: 'Local Print Hub (PETG)', cost: 4.20, origin: 'Local Hub', desc: 'Aerodynamic duct with continuous R=5.0mm radiused intake fillets (+14.2% CFM).' },
{ id: 'fan-noctua', name: 'Noctua NF-F12 industrialPPC-3000 PWM Fan', cat: 'Mechanical', mpn: 'NF-F12-iPPC-3000', vendor: 'Noctua USA / DigiKey', cost: 16.00, origin: 'Austria/USA', desc: '120mm 12V 3,000 RPM high-static-pressure (7.63 mm H2O) industrial cooling fan.' },
{ id: 'mcio-cable', name: 'SFF-TA-1016 (MCIO 8i) to U.3 Twinax Cable Harness', cat: 'Cabling', mpn: 'DRWC-013702', vendor: 'JPC Connectivity / Amphenol', cost: 12.00, origin: 'Taiwan/USA', desc: '30AWG Silver-Plated Twinax ribbon with -0.25 dB/in insertion loss for pristine 16Gbps.' },
{ id: 'radxa-cm3', name: 'Radxa Compute Module 3I SoM (RK3568, 4G/16G)', cat: 'Silicon', mpn: 'CM3I-4G16G', vendor: 'Ameridroid / RS Components', cost: 38.00, origin: 'Taiwan (TSMC)', desc: 'Quad-core Cortex-A55 @ 2.0GHz, 4GB LPDDR4, 16GB eMMC, PCIe 3.0 x2 root complex.' },
{ id: 'broadcom-ioc', name: 'Broadcom SAS3408 Tri-Mode Storage IOC', cat: 'Silicon', mpn: 'SAS3408-BGA484', vendor: 'Avnet / Arrow Electronics', cost: 32.00, origin: 'USA/Taiwan', desc: '8-PHY Fusion-MPT storage processor supporting NVMe U.3, SAS 12G, and SATA 6G.' },
{ id: 'rp2040-mcu', name: 'Raspberry Pi RP2040 Dual Cortex-M0+ MCU', cat: 'Silicon', mpn: 'RP2040-QFN56', vendor: 'DigiKey / Mouser', cost: 0.95, origin: 'UK/Taiwan', desc: 'Auxiliary OpenBMC running Embassy-RP Rust: sub-µs PLP interrupt & 25kHz fan PWM.' },
{ id: 'ltc3350-ic', name: 'Analog Devices LTC3350 Supercap PLP Controller', cat: 'Power', mpn: 'LTC3350EUHF#PBF', vendor: 'Mouser / DigiKey', cost: 7.45, origin: 'USA', desc: 'Bidirectional sync controller with active cell balancing and 106 Joules backup holdup.' },
{ id: 'eaton-supercap', name: 'Eaton 10.0F 2.7V Radial Supercapacitors (x4)', cat: 'Power', mpn: 'B0510-2R7105-R', vendor: 'Mouser / DigiKey', cost: 4.40, origin: 'USA/Europe', desc: '4-cell series supercapacitor string delivering >880ms holdup at 120W peak load.' },
{ id: 'ideal-diode-efuse', name: 'TI LM74700 Ideal Diode + TPS25982 eFuse', cat: 'Power', mpn: 'LM74700QDBVRQ1', vendor: 'DigiKey / TI Direct', cost: 2.80, origin: 'USA', desc: '<0.75µs reverse cutoff ideal diode paired with 15A active inrush clamping eFuse.' },
{ id: 'u3-socket', name: 'Amphenol SFF-TA-1001 U.3 Drive Sockets (x8)', cat: 'Cabling', mpn: '10148784-101LF', vendor: 'Amphenol / Mouser', cost: 11.20, origin: 'USA/Singapore', desc: 'Universal SAS/SATA/NVMe U.3 high-durability right-angle surface mount sockets.' },
{ id: 'fasteners-gussets', name: 'M5 Corner Gussets, Drop-In T-Nuts & Brass Inserts', cat: 'Mechanical', mpn: 'FAST-KIT-01', vendor: 'McMaster-Carr', cost: 16.00, origin: 'USA', desc: 'Die-cast 90° corner brackets, 10.9-grade button head screws, and heat-set inserts.' }
];
let activeCategory = 'all';
let favFilterActive = false;
let favoritePartIds = new Set(['mcio-cable', 'broadcom-ioc', 'ltc3350-ic', 'caddy-u3']);
let activeInspectedPartId = null;
function renderPartsGrid() {
const container = document.getElementById('parts-grid');
if (!container) return;
container.innerHTML = '';
const searchTerm = (document.getElementById('part-search-input')?.value || '').toLowerCase();
const filtered = partsLibrary.filter(part => {
const matchesCat = (activeCategory === 'all' || part.cat === activeCategory);
const matchesFav = (!favFilterActive || favoritePartIds.has(part.id));
const matchesSearch = part.name.toLowerCase().includes(searchTerm) ||
part.mpn.toLowerCase().includes(searchTerm) ||
part.vendor.toLowerCase().includes(searchTerm);
return matchesCat && matchesFav && matchesSearch;
});
document.getElementById('fav-count').innerText = favoritePartIds.size;
filtered.forEach(p => {
const isFav = favoritePartIds.has(p.id);
const isInspected = (p.id === activeInspectedPartId);
const card = document.createElement('div');
card.className = `card ${isInspected ? 'part-card-active' : ''}`;
card.style.marginBottom = '0.5rem';
card.style.transition = 'all 0.2s';
card.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:0.4rem;">
<div>
<span style="font-size:0.825rem; font-weight:700; color:${isInspected ? 'var(--accent-cyan)' : 'var(--text-main)'};">${p.name}</span>
<div style="font-size:0.7rem; color:var(--text-muted); margin-top:0.15rem;">
<span style="background:rgba(59,130,246,0.15); color:var(--accent-blue); padding:0.15rem 0.35rem; border-radius:3px; font-weight:600;">${p.cat}</span>
<span style="margin-left:0.35rem;">${p.vendor}</span> • <span style="color:var(--accent-emerald); font-weight:600;">${p.origin}</span>
</div>
</div>
<button onclick="toggleFavorite('${p.id}')" style="background:transparent; border:none; cursor:pointer; font-size:1.1rem; color:${isFav ? 'var(--accent-amber)' : 'var(--border-color)'};">
<i class="${isFav ? 'fa-solid' : 'fa-regular'} fa-star"></i>
</button>
</div>
<p style="font-size:0.75rem; color:var(--text-muted); line-height:1.3; margin-bottom:0.5rem;">${p.desc}</p>
<div style="display:flex; justify-content:space-between; align-items:center; background:#0b0f17; padding:0.4rem 0.6rem; border-radius:4px; font-size:0.75rem;">
<div><span style="color:var(--text-muted);">MPN:</span> <code style="color:var(--accent-cyan);">${p.mpn}</code></div>
<div><span style="color:var(--text-muted);">Unit Cost:</span> <strong style="color:var(--accent-emerald);">$${p.cost.toFixed(2)}</strong></div>
<button class="btn" style="padding:0.25rem 0.5rem; font-size:0.7rem; background:${isInspected ? 'var(--accent-cyan)' : 'var(--accent-blue)'};" onclick="renderIsolatedPart('${p.id}')"><i class="fa-solid fa-cube"></i> ${isInspected ? 'Active in 3D' : 'Inspect 3D CAD'}</button>
</div>
`;
container.appendChild(card);
});
}
function toggleFavorite(partId) {
if (favoritePartIds.has(partId)) {
favoritePartIds.delete(partId);
} else {
favoritePartIds.add(partId);
}
renderPartsGrid();
}
function toggleFavoriteFilter() {
favFilterActive = !favFilterActive;
const btn = document.getElementById('btn-fav-filter');
if (favFilterActive) {
btn.className = 'btn';
btn.style.background = 'var(--accent-amber)';
btn.style.color = '#000';
} else {
btn.className = 'btn btn-outline';
btn.style.background = 'transparent';
btn.style.color = 'var(--text-main)';
}
renderPartsGrid();
}
function setPartCategory(cat, btn) {
activeCategory = cat;
document.querySelectorAll('#category-pills button').forEach(b => b.className = 'btn btn-outline');
btn.className = 'btn';
renderPartsGrid();
}
function filterPartsLibrary() {
renderPartsGrid();
}
let targetViewOffset = 0;
let currentViewOffset = 0;
let wasInIsolatedMode = false;
let unitsMode = 'mm';
let lightModeIdx = 0;
const lightModes = [
{ name: 'Dark Studio', ambient: 0.7, dir: 0.85, dirColor: 0x38bdf8, bg: null },
{ name: 'Cleanroom White', ambient: 1.25, dir: 1.0, dirColor: 0xffffff, bg: 0x1e293b },
{ name: 'High-Contrast CAD', ambient: 0.4, dir: 1.6, dirColor: 0x06b6d4, bg: 0x030712 }
];
function updateViewportOffset() {
const isMobile = window.innerWidth <= 768;
const activePanel = document.querySelector('.content-panel.active');
const isMinimized = activePanel ? activePanel.classList.contains('minimized') : true;
// Shift view window gently to the left (140px) ONLY on desktop when sidebar is open
targetViewOffset = (!isMobile && activePanel && !isMinimized) ? 140 : 0;
}
function toggleTurntable() {
controls.autoRotate = !controls.autoRotate;
controls.autoRotateSpeed = 2.0;
const btn = document.getElementById('btn-quick-turntable');
btn.classList.toggle('active', controls.autoRotate);
}
function toggleGrid() {
if (gridHelper) {
gridHelper.visible = !gridHelper.visible;
document.getElementById('btn-quick-grid').classList.toggle('active', gridHelper.visible);
}
}
function toggleUnits() {
unitsMode = (unitsMode === 'mm' ? 'in' : 'mm');
document.getElementById('units-label').innerText = unitsMode === 'mm' ? 'Metric (mm)' : 'Imperial (in)';
updateDimensionHUD();
}
function updateDimensionHUD() {
const v = document.getElementById('variant-select')?.value || '8bay';
let d_mm = 500.0, d_in = 19.69;
let bays = '8x U.3 Universal Bays';
if (v === '16bay') {
d_mm = 650.0; d_in = 25.59;
bays = '16x U.3 Dual-Tier Array';
} else if (v === 'jbod') {
d_mm = 380.0; d_in = 14.96;
bays = '8x Direct JBOD Bays';
}
if (!activeInspectedPartId) {
document.getElementById('hud-depth-label').innerText = 'DEPTH';
if (unitsMode === 'mm') {
document.getElementById('hud-depth-val').innerText = `${d_mm.toFixed(1)} mm (${d_in.toFixed(2)}")`;
} else {
document.getElementById('hud-depth-val').innerText = `${d_in.toFixed(2)}" (${d_mm.toFixed(1)} mm)`;
}
document.getElementById('hud-bay-val').innerText = bays;
}
}
function cycleLighting() {
lightModeIdx = (lightModeIdx + 1) % lightModes.length;
const m = lightModes[lightModeIdx];
document.getElementById('light-label').innerText = m.name;
if (ambientLight) ambientLight.intensity = m.ambient;
if (dirLight) {
dirLight.intensity = m.dir;
dirLight.color.setHex(m.dirColor);
}
if (renderer) {
if (m.bg) {
scene.background = new THREE.Color(m.bg);
} else {
scene.background = null;
}
}
}
function downloadActiveCAD() {
const cadLinks = {
'facade-quad': { name: 'facade_quadrant.scad', url: 'file:///home/ethanpursley/p/data/nas-ressearch/mechanical/scad/facade_quadrant.scad' },
'caddy-u3': { name: 'drive_caddy_u3.scad', url: 'file:///home/ethanpursley/p/data/nas-ressearch/mechanical/scad/drive_caddy_u3.scad' },
'fan-shroud': { name: 'fan_wall_shroud.scad', url: 'file:///home/ethanpursley/p/data/nas-ressearch/mechanical/scad/fan_wall_shroud.scad' },
'scs-top': { name: 'top_bottom_panel.dxf', url: 'file:///home/ethanpursley/p/data/nas-ressearch/mechanical/dxf/top_bottom_panel.dxf' },
'scs-ears': { name: '6u_rack_ear_left.dxf', url: 'file:///home/ethanpursley/p/data/nas-ressearch/mechanical/dxf/6u_rack_ear_left.dxf' },
'ext-2020': { name: '01_2020_Extrusion_Skeleton_Blueprint.md', url: 'file:///home/ethanpursley/p/data/nas-ressearch/mechanical/01_2020_Extrusion_Skeleton_Blueprint.md' }
};
const part = cadLinks[activeInspectedPartId] || { name: 'facade_quadrant.scad', url: 'file:///home/ethanpursley/p/data/nas-ressearch/mechanical/scad/facade_quadrant.scad' };
const a = document.createElement('a');
a.href = part.url;
a.download = part.name;
a.target = '_blank';
a.click();
}
const SETTINGS_KEY = 'nas_6u_viewer_settings';
function saveSettings() {
const settings = {
variant: document.getElementById('variant-select')?.value || '8bay',
explodePct: document.getElementById('explode-slider')?.value || 0,
layers: {
skeleton: document.getElementById('layer-skeleton')?.checked ?? true,
sheets: document.getElementById('layer-sheets')?.checked ?? true,
facade: document.getElementById('layer-facade')?.checked ?? true,
drives: document.getElementById('layer-drives')?.checked ?? true,
fans: document.getElementById('layer-fans')?.checked ?? true,
pcba: document.getElementById('layer-pcba')?.checked ?? true,
xray: document.getElementById('layer-xray')?.checked ?? false,
},
unitsMode: unitsMode,
lightModeIdx: lightModeIdx,
autoRotate: controls?.autoRotate || false,
gridVisible: gridHelper?.visible ?? true,
favorites: Array.from(favoritePartIds),
activeCategory: activeCategory,
activeTab: document.querySelector('.content-panel.active')?.id || '3d-view',
activeInspectedPartId: activeInspectedPartId,
camera: camera ? { x: camera.position.x, y: camera.position.y, z: camera.position.z } : null,
target: controls ? { x: controls.target.x, y: controls.target.y, z: controls.target.z } : null
};
try {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
} catch(e) {}
}
function loadSettings() {
try {
const raw = localStorage.getItem(SETTINGS_KEY);
if (!raw) return;
const s = JSON.parse(raw);
if (s.variant && document.getElementById('variant-select')) {
document.getElementById('variant-select').value = s.variant;
buildAssembly(s.variant);
}
if (s.explodePct !== undefined && document.getElementById('explode-slider')) {
document.getElementById('explode-slider').value = s.explodePct;
document.getElementById('explode-val').innerText = s.explodePct + '%';
}
if (s.layers) {
['skeleton', 'sheets', 'facade', 'drives', 'fans', 'pcba', 'xray'].forEach(k => {
const el = document.getElementById('layer-' + k);
if (el && s.layers[k] !== undefined) el.checked = s.layers[k];
});
}
if (s.unitsMode) {
unitsMode = s.unitsMode;
document.getElementById('units-label').innerText = unitsMode === 'mm' ? 'Metric (mm)' : 'Imperial (in)';
}
if (s.lightModeIdx !== undefined) {
lightModeIdx = s.lightModeIdx;
const m = lightModes[lightModeIdx];
document.getElementById('light-label').innerText = m.name;
if (ambientLight) ambientLight.intensity = m.ambient;
if (dirLight) {
dirLight.intensity = m.dir;
dirLight.color.setHex(m.dirColor);
}
if (renderer) scene.background = m.bg ? new THREE.Color(m.bg) : null;
}
if (s.autoRotate !== undefined && controls) {
controls.autoRotate = s.autoRotate;
document.getElementById('btn-quick-turntable')?.classList.toggle('active', s.autoRotate);
}
if (s.gridVisible !== undefined && gridHelper) {
gridHelper.visible = s.gridVisible;
document.getElementById('btn-quick-grid')?.classList.toggle('active', s.gridVisible);
}
if (s.favorites && Array.isArray(s.favorites)) {
favoritePartIds = new Set(s.favorites);
}
if (s.activeCategory) {
activeCategory = s.activeCategory;
}
if (s.camera && camera && controls) {
camera.position.set(s.camera.x, s.camera.y, s.camera.z);
if (s.target) controls.target.set(s.target.x, s.target.y, s.target.z);
}
if (s.activeInspectedPartId) {
renderIsolatedPart(s.activeInspectedPartId);
}
if (s.activeTab && s.activeTab !== '3d-view') {
const tabBtn = Array.from(document.querySelectorAll('.nav-tab')).find(b => b.getAttribute('onclick')?.includes(s.activeTab));
if (tabBtn) tabBtn.click();
}
} catch(e) {}
}
function toggleMinimizePanel(btn) {
const panel = btn.closest('.content-panel');
panel.classList.toggle('minimized');
const icon = btn.querySelector('i');
if (panel.classList.contains('minimized')) {
icon.className = 'fa-solid fa-expand';
btn.title = 'Expand Panel';
} else {
icon.className = 'fa-solid fa-compress';
btn.title = 'Minimize Panel';
}
updateViewportOffset();
saveSettings();
}
function exitIsolatedPartMode() {
activeInspectedPartId = null;
if (groupIsolatedPart) {
removeAndDisposeGroup(groupIsolatedPart);
groupIsolatedPart = null;
}
if (wasInIsolatedMode) {
// Smoothly restore camera distance to full chassis while preserving user view angle
const curDist = camera.position.distanceTo(controls.target);
if (curDist < 400) {
const curDir = camera.position.clone().sub(controls.target).normalize();
camera.position.copy(curDir.multiplyScalar(950));
controls.target.set(0, 0, 0);
}
wasInIsolatedMode = false;
}
renderPartsGrid();
setViewportMode(currentViewportMode);
updateDimensionHUD();
updateViewportOffset();
saveSettings();
}
// Isolated 3D Part CAD Geometry Generator
function renderIsolatedPart(partId) {
activeInspectedPartId = partId;
renderPartsGrid(); // Keep side panel open and highlight active card!
updateViewportOffset();
// Clear previous isolated part
if (groupIsolatedPart) {
removeAndDisposeGroup(groupIsolatedPart);
groupIsolatedPart = null;
}
// Clear current chassis & PCB scene objects and reclaim GPU memory
[groupSkeleton, groupSheets, groupFacade, groupDrives, groupFans, groupPcba, groupPcbEda].forEach(g => {
if (g) removeAndDisposeGroup(g);
});
pcbClickables = [];
groupIsolatedPart = new THREE.Group();
const matAlu = new THREE.MeshStandardMaterial({ color: 0xcfd8dc, metalness: 0.9, roughness: 0.2 });
const matPlastic = new THREE.MeshStandardMaterial({ color: 0x0284c7, roughness: 0.4, metalness: 0.2 });
const matDark = new THREE.MeshStandardMaterial({ color: 0x111827, roughness: 0.3, metalness: 0.7 });
const matGold = new THREE.MeshStandardMaterial({ color: 0xf59e0b, metalness: 0.9, roughness: 0.2 });
const returnHtml = '<button class="btn btn-outline" style="padding:0.2rem 0.4rem; font-size:0.65rem; margin-left:0.5rem; vertical-align:middle;" onclick="exitIsolatedPartMode()"><i class="fa-solid fa-arrow-left"></i> Exit to 6U Case</button>';
if (partId === 'ext-2020') {
const ext = new THREE.Mesh(new THREE.BoxGeometry(20, 260, 20), matAlu);
groupIsolatedPart.add(ext);
document.getElementById('hud-depth-label').innerText = 'ISOLATED CAD';
document.getElementById('hud-depth-val').innerHTML = '2020 Extrusion (20x20x260mm)' + returnHtml;
} else if (partId === 'scs-top' || partId === 'scs-ears') {
const plate = new THREE.Mesh(new THREE.BoxGeometry(260, 2, 200), matAlu);
groupIsolatedPart.add(plate);
document.getElementById('hud-depth-label').innerText = 'ISOLATED CAD';
document.getElementById('hud-depth-val').innerHTML = 'SendCutSend 5052 Plate (1.27mm)' + returnHtml;
} else if (partId === 'caddy-u3') {
const sled = new THREE.Mesh(new THREE.BoxGeometry(26, 140, 160), matPlastic);
const hdd = new THREE.Mesh(new THREE.BoxGeometry(24, 130, 145), matAlu);
groupIsolatedPart.add(sled);
groupIsolatedPart.add(hdd);
document.getElementById('hud-depth-label').innerText = 'ISOLATED CAD';
document.getElementById('hud-depth-val').innerHTML = 'Toolless U.3 Drive Sled' + returnHtml;
} else if (partId === 'mcio-cable') {
const ribbon = new THREE.Mesh(new THREE.BoxGeometry(28, 2, 240), new THREE.MeshStandardMaterial({ color: 0x475569, roughness: 0.5 }));
const conn1 = new THREE.Mesh(new THREE.BoxGeometry(32, 10, 18), matDark);
conn1.position.set(0, 4, -120);
const conn2 = new THREE.Mesh(new THREE.BoxGeometry(32, 10, 18), matDark);
conn2.position.set(0, 4, 120);
groupIsolatedPart.add(ribbon);
groupIsolatedPart.add(conn1);
groupIsolatedPart.add(conn2);
document.getElementById('hud-depth-label').innerText = 'ISOLATED CAD';
document.getElementById('hud-depth-val').innerHTML = 'MCIO 8i Twinax Ribbon (30AWG)' + returnHtml;
} else if (partId === 'fan-noctua') {
const frame = new THREE.Mesh(new THREE.BoxGeometry(120, 120, 25), matDark);
const rotor = new THREE.Mesh(new THREE.CylinderGeometry(55, 55, 28, 32), new THREE.MeshStandardMaterial({ color: 0x7c2d12 }));
rotor.rotation.x = Math.PI / 2;
groupIsolatedPart.add(frame);
groupIsolatedPart.add(rotor);
document.getElementById('hud-depth-label').innerText = 'ISOLATED CAD';
document.getElementById('hud-depth-val').innerHTML = 'Noctua 120mm iPPC-3000 Fan' + returnHtml;
} else if (partId === 'broadcom-ioc') {
const bcm = new THREE.Mesh(new THREE.BoxGeometry(27, 3, 27), matDark);
const lid = new THREE.Mesh(new THREE.BoxGeometry(23, 1.5, 23), matAlu);
lid.position.y = 2;
groupIsolatedPart.add(bcm);
groupIsolatedPart.add(lid);
document.getElementById('hud-depth-label').innerText = 'ISOLATED CAD';
document.getElementById('hud-depth-val').innerHTML = 'Broadcom SAS3408 (BGA-484)' + returnHtml;
} else if (partId === 'eaton-supercap') {
const cap = new THREE.Mesh(new THREE.CylinderGeometry(10, 10, 40, 24), new THREE.MeshStandardMaterial({ color: 0x1d4ed8 }));
groupIsolatedPart.add(cap);
document.getElementById('hud-depth-label').innerText = 'ISOLATED CAD';
document.getElementById('hud-depth-val').innerHTML = 'Eaton 10.0F 2.7V Supercapacitor' + returnHtml;
} else {
const generic = new THREE.Mesh(new THREE.BoxGeometry(80, 40, 80), matPlastic);
groupIsolatedPart.add(generic);
document.getElementById('hud-depth-label').innerText = 'ISOLATED CAD';
document.getElementById('hud-depth-val').innerHTML = partId.toUpperCase() + returnHtml;
}
scene.add(groupIsolatedPart);
// PRESERVE CAMERA ZOOM & ORIENTATION
if (!wasInIsolatedMode) {
const curDist = camera.position.distanceTo(controls.target);
if (curDist > 400) {
const curDir = camera.position.clone().sub(controls.target).normalize();
camera.position.copy(curDir.multiplyScalar(220));
controls.target.set(0, 0, 0);
}
wasInIsolatedMode = true;
}
// When switching between multiple isolated parts, NEVER reset or jump camera position or zoom!
saveSettings();
}
function updatePrintFarmStats() {
const p = document.getElementById('printer-select')?.value || 'prusa-core-l';
const s = printStats[p];
if (s) {
document.getElementById('pf-bed-fit').innerText = s.fit;
document.getElementById('pf-time').innerText = s.time;
document.getElementById('pf-weight').innerText = s.weight;
document.getElementById('pf-elec').innerText = `${s.elec} (@ $0.15/kWh)`;
}
}
let serialPort = null;
let serialConnected = false;
async function toggleSerialConnect() {
const btn = document.getElementById('btn-serial-connect');
if (!('serial' in navigator)) {
// Graceful fallback for non-Chromium browsers (Firefox/Safari)
serialConnected = !serialConnected;
if (serialConnected) {
btn.innerHTML = '<i class="fa-solid fa-check"></i> Simulation Connected';
btn.style.background = 'var(--accent-emerald)';
} else {
btn.innerHTML = '<i class="fa-solid fa-link"></i> Connect RP2040 (Simulated)';
btn.style.background = 'var(--accent-blue)';
}
return;
}
try {
if (!serialConnected) {
serialPort = await navigator.serial.requestPort();
await serialPort.open({ baudRate: 115200 });
serialConnected = true;
btn.innerHTML = '<i class="fa-solid fa-check"></i> Connected (RP2040 Live)';
btn.style.background = 'var(--accent-emerald)';
} else {
if (serialPort) await serialPort.close();
serialConnected = false;
btn.innerHTML = '<i class="fa-solid fa-link"></i> Connect RP2040';
btn.style.background = 'var(--accent-blue)';
}
} catch (err) {
console.warn('WebSerial connection cancelled or running simulated mode:', err);
serialConnected = !serialConnected;
if (serialConnected) {
btn.innerHTML = '<i class="fa-solid fa-check"></i> Connected (COM4 / Simulated)';
btn.style.background = 'var(--accent-emerald)';
} else {
btn.innerHTML = '<i class="fa-solid fa-link"></i> Connect RP2040';
btn.style.background = 'var(--accent-blue)';
}
}
}
// triggerSupercapPlpTest is dynamically provided by hil.js
function updateQAProgress() {
const total = document.querySelectorAll('.check-item input').length;
const checked = document.querySelectorAll('.check-item input:checked').length;
const pct = Math.round((checked / total) * 100);
document.getElementById('qa-progress-label').innerText = `${pct}% Complete`;
document.getElementById('qa-progress-bar').style.width = `${pct}%`;
}
function switchTab(tabId) {
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.content-panel').forEach(p => p.classList.remove('active'));
if (tabId === '3d-view') {
document.querySelector('.nav-tab:nth-child(1)').classList.add('active');
} else {
const btn = Array.from(document.querySelectorAll('.nav-tab')).find(b => b.getAttribute('onclick')?.includes(tabId));
if (btn) btn.classList.add('active');
document.getElementById(tabId)?.classList.add('active');
}
updateViewportOffset();
saveSettings();
}
function closePanels() {
document.querySelectorAll('.content-panel').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
document.querySelector('.nav-tab:nth-child(1)').classList.add('active');
updateViewportOffset();
saveSettings();
}
function onWindowResize() {
const container = document.getElementById('viewport-container');
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
if (currentViewOffset > 0) {
camera.setViewOffset(container.clientWidth, container.clientHeight, currentViewOffset, 0, container.clientWidth, container.clientHeight);
}
}
function animate() {
requestAnimationFrame(animate);
// Smooth subtle left offset when sidebar is active and expanded
if (Math.abs(currentViewOffset - targetViewOffset) > 0.5) {
currentViewOffset += (targetViewOffset - currentViewOffset) * 0.1;
const container = document.getElementById('viewport-container');
camera.setViewOffset(container.clientWidth, container.clientHeight, currentViewOffset, 0, container.clientWidth, container.clientHeight);
} else if (targetViewOffset === 0 && currentViewOffset !== 0) {
currentViewOffset = 0;
camera.clearViewOffset();
}
controls.update();
renderer.render(scene, camera);
}
window.onload = function() {
loadQaChecklist();
loadBomData();
loadPrintFarmData();
loadTravelers();
init3D();
updatePrintFarmStats();
updateMaterialPhysics();
renderPartsGrid();
loadSettings();
// Wire auto-save on camera movement
controls.addEventListener('end', saveSettings);
document.getElementById('explode-slider')?.addEventListener('input', saveSettings);
document.querySelectorAll('.toggle-item input').forEach(input => {
input.addEventListener('change', saveSettings);
});
};
</script>
<feature-lens></feature-lens>
</body>
</html>