3js
This commit is contained in:
Generated
+66
-1
@@ -9,16 +9,25 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"react-dom": "^18.3.1",
|
||||
"three": "^0.186.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.8",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/three": "^0.186.0",
|
||||
"@vitejs/plugin-react": "^6.0.5",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^8.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@dimforge/rapier3d-compat": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.143.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz",
|
||||
@@ -292,6 +301,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "23.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
@@ -320,6 +336,35 @@
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/stats.js": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/three": {
|
||||
"version": "0.186.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.186.0.tgz",
|
||||
"integrity": "sha512-mxYSBpDC+D0pLfSP6sW4WZTcT+nrtmZcimMqnVmy36Hte3XpeYSrvgg4TRdaM1GemGog1AWzI5qL2VoIfMXbJQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
"@types/stats.js": "*",
|
||||
"@types/webxr": ">=0.5.17",
|
||||
"fflate": "~0.8.3",
|
||||
"meshoptimizer": "~1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/webxr": {
|
||||
"version": "0.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
|
||||
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz",
|
||||
@@ -381,6 +426,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
||||
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -687,6 +739,13 @@
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/meshoptimizer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
|
||||
"integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
@@ -832,6 +891,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.186.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.186.0.tgz",
|
||||
"integrity": "sha512-cr/fIM2ddMSVbYVgkfD4jLJv7Fh/8ZTjvo+7gQeSVGUZHxpx9FDwoL5iC7hUz/LiRA8wMbqfnb90xKfm1/HHkQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
|
||||
+3
-1
@@ -11,11 +11,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"react-dom": "^18.3.1",
|
||||
"three": "^0.186.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.8",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/three": "^0.186.0",
|
||||
"@vitejs/plugin-react": "^6.0.5",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^8.2.1"
|
||||
|
||||
+13
-102
@@ -1,4 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import ThreeViewport from './ThreeViewport'
|
||||
import { buildRackScene, buildClusterScene, buildNasScene } from './scenes'
|
||||
|
||||
const EMAIL = '0pietroquintavalle0@gmail.com'
|
||||
const GITHUB = 'https://github.com/Quinta0'
|
||||
@@ -15,6 +17,7 @@ type CaseStudy = {
|
||||
footerL: string
|
||||
footerR: string
|
||||
diagram: ReactNode
|
||||
interactive?: boolean
|
||||
}
|
||||
|
||||
const cases: CaseStudy[] = [
|
||||
@@ -25,50 +28,11 @@ const cases: CaseStudy[] = [
|
||||
tags: ['19" EIA-310 Cabinet', 'ZFS RAID 10', 'Structured Cabling'],
|
||||
tint: '#EFECE6',
|
||||
headerL: 'RACK_CHASSIS // EIA-310',
|
||||
headerR: 'ZFS RAID 10 POOL',
|
||||
headerR: '• DRAG TO ROTATE',
|
||||
footerL: 'STRUCTURED HARNESS',
|
||||
footerR: 'SUB-30M RTO SYNC',
|
||||
diagram: (
|
||||
<svg viewBox="0 0 460 210" className="case-svg" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="40" y="20" width="380" height="170" rx="3" stroke="var(--ink)" strokeWidth="1.5" />
|
||||
<line x1="56" y1="20" x2="56" y2="190" stroke="var(--border)" strokeWidth="1" strokeDasharray="2 2" />
|
||||
<line x1="404" y1="20" x2="404" y2="190" stroke="var(--border)" strokeWidth="1" strokeDasharray="2 2" />
|
||||
|
||||
<rect x="66" y="30" width="328" height="24" rx="2" fill="var(--canvas)" stroke="var(--ink)" strokeWidth="1" />
|
||||
<circle cx="85" cy="42" r="3" fill="var(--ink)" />
|
||||
<circle cx="98" cy="42" r="3" fill="var(--ink)" />
|
||||
<circle cx="111" cy="42" r="3" fill="var(--ink)" />
|
||||
<circle cx="124" cy="42" r="3" fill="var(--ink)" />
|
||||
<circle cx="340" cy="42" r="2.5" fill="var(--accent)" className="led-pulse" />
|
||||
<text x="355" y="45" fill="var(--secondary)" fontFamily="Space Mono" fontSize="8">PATCH</text>
|
||||
|
||||
<rect x="66" y="62" width="328" height="68" rx="2" fill="var(--canvas)" stroke="var(--ink)" strokeWidth="1.2" />
|
||||
<rect x="76" y="72" width="32" height="48" rx="1.5" stroke="var(--ink)" strokeWidth="1" />
|
||||
<circle cx="92" cy="112" r="1.5" fill="var(--accent)" />
|
||||
<rect x="114" y="72" width="32" height="48" rx="1.5" stroke="var(--ink)" strokeWidth="1" />
|
||||
<circle cx="130" cy="112" r="1.5" fill="var(--ink)" />
|
||||
<rect x="152" y="72" width="32" height="48" rx="1.5" stroke="var(--ink)" strokeWidth="1" />
|
||||
<circle cx="168" cy="112" r="1.5" fill="var(--ink)" />
|
||||
<rect x="190" y="72" width="32" height="48" rx="1.5" stroke="var(--ink)" strokeWidth="1" />
|
||||
<circle cx="206" cy="112" r="1.5" fill="var(--ink)" />
|
||||
|
||||
<rect x="238" y="72" width="32" height="48" rx="1.5" stroke="var(--ink)" strokeWidth="1" />
|
||||
<circle cx="254" cy="112" r="1.5" fill="var(--ink)" />
|
||||
<rect x="276" y="72" width="32" height="48" rx="1.5" stroke="var(--ink)" strokeWidth="1" />
|
||||
<circle cx="292" cy="112" r="1.5" fill="var(--ink)" />
|
||||
<rect x="314" y="72" width="32" height="48" rx="1.5" stroke="var(--ink)" strokeWidth="1" />
|
||||
<circle cx="330" cy="112" r="1.5" fill="var(--ink)" />
|
||||
<rect x="352" y="72" width="32" height="48" rx="1.5" stroke="var(--ink)" strokeWidth="1" />
|
||||
<circle cx="368" cy="112" r="1.5" fill="var(--accent)" />
|
||||
|
||||
<rect x="66" y="138" width="328" height="18" rx="2" fill="var(--canvas)" stroke="var(--border)" strokeWidth="1" />
|
||||
<path d="M 85 147 Q 160 165 240 147 T 370 147" stroke="var(--accent)" strokeWidth="1.2" fill="none" className="bus-flow" />
|
||||
|
||||
<rect x="66" y="162" width="328" height="22" rx="2" fill="var(--canvas)" stroke="var(--ink)" strokeWidth="1" />
|
||||
<line x1="80" y1="173" x2="130" y2="173" stroke="var(--ink)" strokeWidth="1.5" />
|
||||
<circle cx="370" cy="173" r="2" fill="var(--ink)" />
|
||||
</svg>
|
||||
),
|
||||
diagram: <ThreeViewport build={buildRackScene} className="case-canvas" />,
|
||||
interactive: true,
|
||||
},
|
||||
{
|
||||
label: '02 / Startup Infrastructure • DEC Energy',
|
||||
@@ -77,34 +41,11 @@ const cases: CaseStudy[] = [
|
||||
tags: ['Bare-Metal K8s', 'LLM Inference', 'Thermal Profiling'],
|
||||
tint: '#EAE8E1',
|
||||
headerL: 'DUAL_NODE // TOPOLOGY',
|
||||
headerR: 'BARE-METAL FABRIC',
|
||||
headerR: '• DRAG TO ROTATE',
|
||||
footerL: 'ON-PREMISES INGESTION',
|
||||
footerR: 'USI STARTUP',
|
||||
diagram: (
|
||||
<svg viewBox="0 0 460 210" className="case-svg" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="25" y="25" width="190" height="160" rx="3" stroke="var(--ink)" strokeWidth="1.2" fill="var(--canvas)" />
|
||||
<rect x="75" y="65" width="46" height="46" rx="2" stroke="var(--ink)" strokeWidth="1.5" fill="#FFFFFF" />
|
||||
<text x="98" y="92" fill="var(--ink)" fontFamily="Space Mono" fontSize="9" textAnchor="middle" fontWeight="bold">NODE 01</text>
|
||||
<line x1="130" y1="60" x2="130" y2="116" stroke="var(--ink)" strokeWidth="1" />
|
||||
<line x1="136" y1="60" x2="136" y2="116" stroke="var(--ink)" strokeWidth="1" />
|
||||
<line x1="142" y1="60" x2="142" y2="116" stroke="var(--accent)" strokeWidth="1" />
|
||||
<line x1="148" y1="60" x2="148" y2="116" stroke="var(--ink)" strokeWidth="1" />
|
||||
<rect x="40" y="135" width="120" height="14" rx="1" stroke="var(--ink)" strokeWidth="1" fill="#FFFFFF" />
|
||||
<text x="100" y="145" fill="var(--secondary)" fontFamily="Space Mono" fontSize="7" textAnchor="middle">PCIe // TENSOR LOAD</text>
|
||||
|
||||
<path d="M 215 105 L 245 105" stroke="var(--accent)" strokeWidth="2" strokeDasharray="3 3" className="bus-flow" />
|
||||
|
||||
<rect x="245" y="25" width="190" height="160" rx="3" stroke="var(--ink)" strokeWidth="1.2" fill="var(--canvas)" />
|
||||
<rect x="295" y="65" width="46" height="46" rx="2" stroke="var(--ink)" strokeWidth="1.5" fill="#FFFFFF" />
|
||||
<text x="318" y="92" fill="var(--ink)" fontFamily="Space Mono" fontSize="9" textAnchor="middle" fontWeight="bold">NODE 02</text>
|
||||
<line x1="350" y1="60" x2="350" y2="116" stroke="var(--ink)" strokeWidth="1" />
|
||||
<line x1="356" y1="60" x2="356" y2="116" stroke="var(--ink)" strokeWidth="1" />
|
||||
<line x1="362" y1="60" x2="362" y2="116" stroke="var(--accent)" strokeWidth="1" />
|
||||
<line x1="368" y1="60" x2="368" y2="116" stroke="var(--ink)" strokeWidth="1" />
|
||||
<rect x="260" y="135" width="120" height="14" rx="1" stroke="var(--ink)" strokeWidth="1" fill="#FFFFFF" />
|
||||
<text x="320" y="145" fill="var(--secondary)" fontFamily="Space Mono" fontSize="7" textAnchor="middle">STORAGE & GITEA</text>
|
||||
</svg>
|
||||
),
|
||||
diagram: <ThreeViewport build={buildClusterScene} className="case-canvas" />,
|
||||
interactive: true,
|
||||
},
|
||||
{
|
||||
label: '03 / Protocol Reverse Engineering',
|
||||
@@ -151,41 +92,11 @@ const cases: CaseStudy[] = [
|
||||
tags: ['Zero-Trust WAN', 'Ansible IaC', 'Proxmox / UNRAID'],
|
||||
tint: '#EDEAE3',
|
||||
headerL: 'TOPOLOGY MAP',
|
||||
headerR: 'ZERO EXPOSED WAN',
|
||||
headerR: '• HOVER BAYS',
|
||||
footerL: 'ANSIBLE PROVISIONED',
|
||||
footerR: 'DAILY PRODUCTION',
|
||||
diagram: (
|
||||
<svg viewBox="0 0 460 210" className="case-svg" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="105" r="24" stroke="var(--ink)" strokeWidth="1.2" fill="var(--canvas)" />
|
||||
<text x="50" y="103" fill="var(--ink)" fontFamily="Space Mono" fontSize="8" textAnchor="middle" fontWeight="bold">12+ USERS</text>
|
||||
<text x="50" y="115" fill="var(--secondary)" fontFamily="Space Mono" fontSize="7" textAnchor="middle">SSO / 2FA</text>
|
||||
|
||||
<path d="M 74 105 L 146 105" stroke="var(--accent)" strokeWidth="1.5" strokeDasharray="3 3" className="bus-flow" />
|
||||
|
||||
<rect x="146" y="70" width="80" height="70" rx="3" stroke="var(--ink)" strokeWidth="1.2" fill="var(--canvas)" />
|
||||
<text x="186" y="98" fill="var(--ink)" fontFamily="Space Mono" fontSize="8" textAnchor="middle" fontWeight="bold">CLOUDFLARE</text>
|
||||
<text x="186" y="112" fill="var(--accent)" fontFamily="Space Mono" fontSize="7" textAnchor="middle">TUNNELS</text>
|
||||
<text x="186" y="124" fill="var(--secondary)" fontFamily="Space Mono" fontSize="6" textAnchor="middle">WIREGUARD MESH</text>
|
||||
|
||||
<path d="M 226 105 L 280 105" stroke="var(--ink)" strokeWidth="1.2" />
|
||||
<line x1="280" y1="50" x2="280" y2="160" stroke="var(--ink)" strokeWidth="1.2" />
|
||||
|
||||
<line x1="280" y1="50" x2="310" y2="50" stroke="var(--ink)" strokeWidth="1.2" />
|
||||
<rect x="310" y="32" width="125" height="36" rx="2" stroke="var(--ink)" strokeWidth="1" fill="#FFFFFF" />
|
||||
<text x="322" y="47" fill="var(--ink)" fontFamily="Space Mono" fontSize="8" fontWeight="bold">UNRAID HOST</text>
|
||||
<text x="322" y="58" fill="var(--secondary)" fontFamily="Space Mono" fontSize="7">Docker Containers</text>
|
||||
|
||||
<line x1="280" y1="105" x2="310" y2="105" stroke="var(--ink)" strokeWidth="1.2" />
|
||||
<rect x="310" y="87" width="125" height="36" rx="2" stroke="var(--ink)" strokeWidth="1" fill="#FFFFFF" />
|
||||
<text x="322" y="102" fill="var(--ink)" fontFamily="Space Mono" fontSize="8" fontWeight="bold">PROXMOX VE 8</text>
|
||||
<text x="322" y="113" fill="var(--secondary)" fontFamily="Space Mono" fontSize="7">KVM / LXC Workloads</text>
|
||||
|
||||
<line x1="280" y1="160" x2="310" y2="160" stroke="var(--ink)" strokeWidth="1.2" />
|
||||
<rect x="310" y="142" width="125" height="36" rx="2" stroke="var(--ink)" strokeWidth="1" fill="#FFFFFF" />
|
||||
<text x="322" y="157" fill="var(--ink)" fontFamily="Space Mono" fontSize="8" fontWeight="bold">TRUENAS APPLIANCE</text>
|
||||
<text x="322" y="168" fill="var(--secondary)" fontFamily="Space Mono" fontSize="7">ZFS RAID 10</text>
|
||||
</svg>
|
||||
),
|
||||
diagram: <ThreeViewport build={buildNasScene} className="case-canvas" />,
|
||||
interactive: true,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -350,7 +261,7 @@ export default function App() {
|
||||
<div className="diagram-card">
|
||||
<div className="diagram-row diagram-header">
|
||||
<span>{c.headerL}</span>
|
||||
<span>{c.headerR}</span>
|
||||
<span className={c.interactive ? 'diagram-interactive' : undefined}>{c.headerR}</span>
|
||||
</div>
|
||||
<div className="diagram-svg-wrap">{c.diagram}</div>
|
||||
<div className="diagram-row diagram-footer">
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
|
||||
export type ViewportHandle = {
|
||||
update?: () => void
|
||||
dispose?: () => void
|
||||
}
|
||||
|
||||
export type BuildFn = (
|
||||
scene: THREE.Scene,
|
||||
camera: THREE.PerspectiveCamera,
|
||||
controls: OrbitControls,
|
||||
renderer: THREE.WebGLRenderer,
|
||||
container: HTMLDivElement,
|
||||
) => ViewportHandle | void
|
||||
|
||||
export default function ThreeViewport({ build, className }: { build: BuildFn; className?: string }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
|
||||
const scene = new THREE.Scene()
|
||||
const camera = new THREE.PerspectiveCamera(38, container.clientWidth / container.clientHeight, 0.1, 100)
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
|
||||
renderer.setSize(container.clientWidth, container.clientHeight)
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
||||
container.appendChild(renderer.domElement)
|
||||
|
||||
const controls = new OrbitControls(camera, renderer.domElement)
|
||||
controls.enableDamping = true
|
||||
controls.dampingFactor = 0.05
|
||||
controls.enableZoom = false
|
||||
controls.autoRotate = true
|
||||
controls.autoRotateSpeed = 1
|
||||
|
||||
const handle = build(scene, camera, controls, renderer, container) ?? undefined
|
||||
|
||||
let frameId = 0
|
||||
const animate = () => {
|
||||
frameId = requestAnimationFrame(animate)
|
||||
controls.update()
|
||||
handle?.update?.()
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
animate()
|
||||
|
||||
const onResize = () => {
|
||||
camera.aspect = container.clientWidth / container.clientHeight
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(container.clientWidth, container.clientHeight)
|
||||
}
|
||||
window.addEventListener('resize', onResize)
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frameId)
|
||||
window.removeEventListener('resize', onResize)
|
||||
handle?.dispose?.()
|
||||
controls.dispose()
|
||||
renderer.dispose()
|
||||
container.removeChild(renderer.domElement)
|
||||
}
|
||||
}, [build])
|
||||
|
||||
return <div ref={containerRef} className={className} />
|
||||
}
|
||||
@@ -109,6 +109,20 @@ body{
|
||||
.diagram-footer{padding-top:12px;border-top:1px solid rgba(230,228,221,.4);font-size:10px}
|
||||
.diagram-svg-wrap{width:100%;overflow-x:auto;padding:8px 0}
|
||||
.case-svg{width:100%;min-width:320px;height:auto}
|
||||
.diagram-interactive{color:var(--accent);font-weight:600}
|
||||
|
||||
.case-canvas{
|
||||
position:relative;width:100%;height:288px;border-radius:6px;overflow:hidden;
|
||||
cursor:grab;touch-action:pan-y;
|
||||
}
|
||||
.case-canvas:active{cursor:grabbing}
|
||||
.case-canvas canvas{display:block;touch-action:pan-y}
|
||||
.nas-hud{
|
||||
position:absolute;z-index:5;pointer-events:none;
|
||||
background:var(--ink);color:var(--canvas);
|
||||
font-family:var(--f-mono);font-size:10px;
|
||||
padding:4px 8px;border-radius:4px;box-shadow:0 1px 2px rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
.led-pulse{animation:ledBlink 2.4s ease-in-out infinite}
|
||||
@keyframes ledBlink{0%,100%{opacity:.3}50%{opacity:1}}
|
||||
@@ -197,5 +211,6 @@ body{
|
||||
.case-desc{font-size:12px}
|
||||
.case-diagram{padding:16px;border-radius:12px}
|
||||
.diagram-card{padding:16px}
|
||||
.case-canvas{height:256px}
|
||||
.site-footer{padding:48px var(--pad);flex-direction:column;align-items:flex-start}
|
||||
}
|
||||
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
import * as THREE from 'three'
|
||||
import type { BuildFn } from './ThreeViewport'
|
||||
|
||||
const wireMat = new THREE.LineBasicMaterial({ color: 0x121212 })
|
||||
const chassisMat = new THREE.MeshBasicMaterial({ color: 0xf7f6f2, transparent: true, opacity: 0.75 })
|
||||
const darkPartMat = new THREE.MeshBasicMaterial({ color: 0x1c1b19 })
|
||||
const swissRedMat = new THREE.MeshBasicMaterial({ color: 0xc1121f })
|
||||
const copperMat = new THREE.MeshBasicMaterial({ color: 0xb9814f })
|
||||
const tealDriveMat = new THREE.MeshBasicMaterial({ color: 0x1f8f86, transparent: true, opacity: 0.75 })
|
||||
|
||||
export const buildRackScene: BuildFn = (scene, camera) => {
|
||||
camera.position.set(4, 2.5, 4.5)
|
||||
const rack = new THREE.Group()
|
||||
rack.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.6, 2.8, 1.4)), wireMat))
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const server = new THREE.Mesh(new THREE.BoxGeometry(1.45, 0.38, 1.25), chassisMat)
|
||||
server.position.set(0, -0.9 + i * 0.55, 0)
|
||||
server.add(new THREE.LineSegments(new THREE.EdgesGeometry(server.geometry), wireMat))
|
||||
|
||||
for (let d = 0; d < 4; d++) {
|
||||
const drive = new THREE.Mesh(new THREE.BoxGeometry(0.24, 0.22, 0.05), darkPartMat)
|
||||
drive.position.set(-0.45 + d * 0.3, 0, 0.63)
|
||||
server.add(drive)
|
||||
}
|
||||
|
||||
const led = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.04, 0.04), swissRedMat)
|
||||
led.position.set(0.6, 0.1, 0.64)
|
||||
server.add(led)
|
||||
rack.add(server)
|
||||
}
|
||||
|
||||
scene.add(rack)
|
||||
}
|
||||
|
||||
export const buildClusterScene: BuildFn = (scene, camera) => {
|
||||
camera.position.set(3.8, 3.5, 3.8)
|
||||
const cluster = new THREE.Group()
|
||||
|
||||
const makeBoard = (x: number) => {
|
||||
const board = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.08, 1.8), chassisMat)
|
||||
board.position.set(x, 0, 0)
|
||||
board.add(new THREE.LineSegments(new THREE.EdgesGeometry(board.geometry), wireMat))
|
||||
const cpu = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.12, 0.45), darkPartMat)
|
||||
cpu.position.set(0, 0.06, -0.2)
|
||||
board.add(cpu)
|
||||
return board
|
||||
}
|
||||
|
||||
const b1 = makeBoard(-0.9)
|
||||
const gpu = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.4, 1.2), darkPartMat)
|
||||
gpu.position.set(0.5, 0.2, 0.1)
|
||||
b1.add(gpu)
|
||||
cluster.add(b1)
|
||||
|
||||
const b2 = makeBoard(0.9)
|
||||
const hba = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.35, 1), darkPartMat)
|
||||
hba.position.set(-0.5, 0.18, 0.1)
|
||||
b2.add(hba)
|
||||
cluster.add(b2)
|
||||
|
||||
const bridge = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.04, 0.06), swissRedMat)
|
||||
bridge.position.set(0, 0.08, -0.2)
|
||||
cluster.add(bridge)
|
||||
|
||||
scene.add(cluster)
|
||||
}
|
||||
|
||||
function makeGrilleTexture() {
|
||||
const c = document.createElement('canvas')
|
||||
c.width = c.height = 128
|
||||
const ctx = c.getContext('2d')!
|
||||
ctx.fillStyle = '#1a1a1a'
|
||||
ctx.fillRect(0, 0, 128, 128)
|
||||
ctx.fillStyle = '#3a3a3a'
|
||||
for (let x = 5; x < 128; x += 10) {
|
||||
for (let y = 5; y < 128; y += 10) ctx.fillRect(x - 1.5, y - 1.5, 3, 3)
|
||||
}
|
||||
const tex = new THREE.CanvasTexture(c)
|
||||
tex.wrapS = tex.wrapT = THREE.RepeatWrapping
|
||||
tex.repeat.set(2, 2)
|
||||
return tex
|
||||
}
|
||||
|
||||
export const buildNasScene: BuildFn = (scene, camera, controls, renderer, container) => {
|
||||
camera.position.set(3.6, 2.6, 3.8)
|
||||
|
||||
const nas = new THREE.Group()
|
||||
const W = 2.3, H = 2.4, D = 2.6
|
||||
const halfH = H / 2
|
||||
const stripY = 0.25
|
||||
const bayTopY = stripY - 0.15
|
||||
const bayHeight = bayTopY - (-halfH + 0.25)
|
||||
|
||||
nas.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(W, H, D)), wireMat))
|
||||
|
||||
const grilleTex = makeGrilleTexture()
|
||||
const topMat = new THREE.MeshBasicMaterial({ map: grilleTex, transparent: true, opacity: 0.85 })
|
||||
const top = new THREE.Mesh(new THREE.PlaneGeometry(W - 0.12, D - 0.12), topMat)
|
||||
top.rotation.x = -Math.PI / 2
|
||||
top.position.y = halfH - 0.05
|
||||
nas.add(top)
|
||||
|
||||
const strip = new THREE.Group()
|
||||
strip.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(W, 0.26, 0.08)), new THREE.LineBasicMaterial({ color: 0xb9814f })))
|
||||
const stripFace = new THREE.Mesh(new THREE.PlaneGeometry(W - 0.04, 0.22), copperMat)
|
||||
stripFace.position.z = 0.045
|
||||
strip.add(stripFace)
|
||||
strip.position.set(0, stripY, D / 2 + 0.02)
|
||||
nas.add(strip)
|
||||
|
||||
const bayWidth = 0.17, gap = 0.02
|
||||
const startX = -(9 * bayWidth + 8 * gap) / 2 + bayWidth / 2
|
||||
const hitMeshes: THREE.Mesh[] = []
|
||||
const leds: { mat: THREE.MeshBasicMaterial; phase: number }[] = []
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const x = startX + i * (bayWidth + gap)
|
||||
const bay = new THREE.Group()
|
||||
bay.position.set(x, bayTopY - bayHeight / 2, D / 2)
|
||||
bay.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(bayWidth, bayHeight, 0.18)), wireMat))
|
||||
|
||||
const drive = new THREE.Mesh(new THREE.BoxGeometry(bayWidth - 0.03, bayHeight - 0.12, 0.32), tealDriveMat)
|
||||
drive.position.z = -0.16
|
||||
bay.add(drive)
|
||||
|
||||
const hitMesh = new THREE.Mesh(new THREE.BoxGeometry(bayWidth, bayHeight, 0.5), new THREE.MeshBasicMaterial({ transparent: true, opacity: 0 }))
|
||||
hitMesh.position.z = -0.1
|
||||
hitMesh.userData.label = `BAY ${String(i + 1).padStart(2, '0')} // WD RED 10TB (ONLINE)`
|
||||
bay.add(hitMesh)
|
||||
hitMeshes.push(hitMesh)
|
||||
|
||||
const ledMat = new THREE.MeshBasicMaterial({ color: 0xc1121f, transparent: true, opacity: 0.5 })
|
||||
const led = new THREE.Mesh(new THREE.CircleGeometry(0.018, 8), ledMat)
|
||||
led.position.set(0, bayHeight / 2 - 0.08, 0.1)
|
||||
bay.add(led)
|
||||
leds.push({ mat: ledMat, phase: i * 0.5 })
|
||||
|
||||
nas.add(bay)
|
||||
}
|
||||
|
||||
scene.add(nas)
|
||||
|
||||
const hud = document.createElement('div')
|
||||
hud.className = 'nas-hud'
|
||||
hud.hidden = true
|
||||
container.appendChild(hud)
|
||||
|
||||
const raycaster = new THREE.Raycaster()
|
||||
const pointer = new THREE.Vector2()
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
const rect = renderer.domElement.getBoundingClientRect()
|
||||
pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1
|
||||
pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1
|
||||
|
||||
raycaster.setFromCamera(pointer, camera)
|
||||
const hit = raycaster.intersectObjects(hitMeshes)[0]
|
||||
|
||||
if (hit) {
|
||||
hud.textContent = hit.object.userData.label
|
||||
hud.style.left = `${Math.min(e.clientX - rect.left + 12, rect.width - 180)}px`
|
||||
hud.style.top = `${Math.max(e.clientY - rect.top - 28, 10)}px`
|
||||
hud.hidden = false
|
||||
controls.autoRotate = false
|
||||
} else {
|
||||
hud.hidden = true
|
||||
controls.autoRotate = true
|
||||
}
|
||||
}
|
||||
|
||||
const onPointerLeave = () => {
|
||||
hud.hidden = true
|
||||
controls.autoRotate = true
|
||||
}
|
||||
|
||||
renderer.domElement.addEventListener('pointermove', onPointerMove)
|
||||
renderer.domElement.addEventListener('pointerleave', onPointerLeave)
|
||||
|
||||
const clock = new THREE.Clock()
|
||||
|
||||
return {
|
||||
update: () => {
|
||||
const t = clock.getElapsedTime()
|
||||
for (const l of leds) l.mat.opacity = 0.3 + Math.abs(Math.sin(t * 1.6 + l.phase)) * 0.6
|
||||
},
|
||||
dispose: () => {
|
||||
renderer.domElement.removeEventListener('pointermove', onPointerMove)
|
||||
renderer.domElement.removeEventListener('pointerleave', onPointerLeave)
|
||||
container.removeChild(hud)
|
||||
grilleTex.dispose()
|
||||
topMat.dispose()
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user