2314 lines
120 KiB
HTML
2314 lines
120 KiB
HTML
<!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;
|
||
}
|
||
|
||
/* 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);
|
||
}
|
||
</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;">×</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="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="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="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="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="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="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="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="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>
|
||
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();
|
||
camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 1, 3000);
|
||
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.05;
|
||
|
||
// 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 1–2 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 activePanel = document.querySelector('.content-panel.active');
|
||
const isMinimized = activePanel ? activePanel.classList.contains('minimized') : true;
|
||
// Shift view window gently to the left (140px) when sidebar is open and expanded
|
||
targetViewOffset = (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>
|
||
</body>
|
||
</html>
|