// src/scripts/pcBuilder.js // Computing:Box — Advanced PC Sandbox (() => { const workspace = document.getElementById("workspace"); const viewport = document.getElementById("viewport"); const wireLayer = document.getElementById("wireLayer"); const specsContainer = document.getElementById("buildSpecsContainer"); const toolboxGrid = document.getElementById("toolboxGrid"); const btnClearBoard = document.getElementById("btnClearBoard"); const toolboxToggle = document.getElementById("toolboxToggle"); const pcPage = document.getElementById("pcPage"); /* --- ULTRA-REALISTIC COMPONENT LIBRARY --- */ const PC_PARTS = { 'CASE': { name: 'ATX PC Case', w: 600, h: 550, z: 5, ports: [], slots: { 'MB1': { x: 20, y: 20, accepts: 'MB' }, 'PSU1': { x: 20, y: 440, accepts: 'PSU' }, 'HDD1': { x: 440, y: 20, accepts: 'HDD' }, 'HDD2': { x: 440, y: 170, accepts: 'HDD' }, 'SATA_SSD1': { x: 440, y: 320, accepts: 'SATA_SSD' }, 'SATA_SSD2': { x: 440, y: 400, accepts: 'SATA_SSD' } }, svg: `` }, 'MB': { name: 'Motherboard', w: 360, h: 400, z: 10, ports: [ { id: 'atx_pwr', x: 340, y: 150 }, { id: 'sata1', x: 340, y: 300 }, { id: 'sata2', x: 340, y: 330 }, { id: 'usb1', x: 10, y: 40 }, { id: 'usb2', x: 10, y: 70 }, { id: 'audio', x: 10, y: 170 }, { id: 'disp', x: 10, y: 210 } ], slots: { 'CPU1': { x: 120, y: 40, accepts: 'CPU' }, 'COOLER1': { x: 100, y: 20, accepts: 'COOLER' }, 'RAM1': { x: 230, y: 30, accepts: 'RAM' }, 'RAM2': { x: 250, y: 30, accepts: 'RAM' }, 'RAM3': { x: 270, y: 30, accepts: 'RAM' }, 'RAM4': { x: 290, y: 30, accepts: 'RAM' }, 'M2_1': { x: 120, y: 170, accepts: 'M2_SSD' }, 'M2_2': { x: 120, y: 250, accepts: 'M2_SSD' }, 'PCIE1': { x: 40, y: 200, accepts: 'GPU' }, 'PCIE2': { x: 40, y: 300, accepts: 'GPU' } }, svg: `M.2_1M.2_2` }, 'CPU': { name: 'Processor', w: 80, h: 80, z: 20, ports: [], slots: {}, svg: `INTELCORE i914900K` }, 'COOLER': { name: 'Liquid AIO', w: 120, h: 120, z: 30, ports: [], slots: {}, svg: `32°C2400 RPM` }, 'RAM': { name: 'RGB Memory', w: 15, h: 100, z: 20, ports: [], slots: {}, svg: `` }, 'GPU': { name: 'Graphics Card', w: 280, h: 80, z: 40, slots: {}, ports: [{ id: 'pwr_in', x: 270, y: 10 }, { id: 'disp_out', x: 10, y: 40 }], svg: `GEFORCE RTX` }, 'M2_SSD': { name: 'M.2 NVMe SSD', w: 80, h: 22, z: 20, ports: [], slots: {}, svg: `990 PRO 2TB` }, 'SATA_SSD': { name: '2.5" SATA SSD', w: 100, h: 70, z: 20, slots: {}, ports: [{id:'data', x:90, y:20}, {id:'pwr', x:90, y:50}], svg: `SAMSUNG` }, 'HDD': { name: '3.5" Mech HDD', w: 120, h: 140, z: 20, slots: {}, ports: [{id:'data', x:110, y:20}, {id:'pwr', x:110, y:120}], svg: `WD BLACK12TB HDD` }, 'PSU': { name: 'Power Supply', w: 160, h: 90, z: 20, slots: {}, ports: [{id:'out1',x:150,y:20}, {id:'out2',x:150,y:40}, {id:'out3',x:150,y:60}, {id:'out4',x:150,y:80}], svg: `1200W` }, 'MONITOR': { name: 'Monitor', w: 240, h: 180, z: 30, slots: {}, ports: [{id:'disp', x:120, y:140}], svg: `ASUS` }, 'KEYBOARD': { name: 'Keyboard', w: 180, h: 60, z: 30, slots: {}, ports: [{id:'usb', x:90, y:10}], svg: `` }, 'MOUSE': { name: 'Mouse', w: 30, h: 54, z: 30, slots: {}, ports: [{id:'usb', x:15, y:5}], svg: `` }, 'SPEAKER': { name: 'Speakers', w: 46, h: 90, z: 30, slots: {}, ports: [{id:'audio', x:23, y:10}], svg: `` } }; let nodes = {}; let connections = []; let nextNodeId = 1, nextWireId = 1; let isDraggingNode = null, dragOffset = { x: 0, y: 0 }; let wiringStart = null, tempWirePath = null; let selectedWireId = null, selectedNodeId = null; let panX = 0, panY = 0, zoom = 1; let isPanning = false, panStart = { x: 0, y: 0 }, isSystemBooted = false; /* --- Toolbox & Base Init --- */ function initToolbox() { if(!toolboxGrid) return; let html = ''; Object.keys(PC_PARTS).forEach(partKey => { html += `
${PC_PARTS[partKey].svg}
${partKey}
`; }); toolboxGrid.innerHTML = html; document.querySelectorAll('.drag-item').forEach(item => { item.addEventListener('dragstart', (e) => { e.dataTransfer.setData('spawnType', item.dataset.spawn); }); }); } /* --- Viewport Math --- */ function updateViewport() { viewport.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`; workspace.style.backgroundSize = `${32 * zoom}px ${32 * zoom}px`; workspace.style.backgroundPosition = `${panX}px ${panY}px`; } function zoomWorkspace(factor, mouseX, mouseY) { const newZoom = Math.min(Math.max(0.1, zoom * factor), 2); panX = mouseX - (mouseX - panX) * (newZoom / zoom); panY = mouseY - (mouseY - panY) * (newZoom / zoom); zoom = newZoom; updateViewport(); } function getPortCoords(nodeId, portDataAttr) { const node = nodes[nodeId]; if (!node || !node.el) return {x:0, y:0}; const portEl = node.el.querySelector(`[data-port="${portDataAttr}"]`); if (!portEl) return {x:0, y:0}; const wsRect = workspace.getBoundingClientRect(); const portRect = portEl.getBoundingClientRect(); return { x: (portRect.left - wsRect.left - panX + portRect.width / 2) / zoom, y: (portRect.top - wsRect.top - panY + portRect.height / 2) / zoom }; } function drawBezier(x1, y1, x2, y2) { const cpDist = Math.abs(x2 - x1) * 0.6 + 20; return `M ${x1} ${y1} C ${x1 + cpDist} ${y1}, ${x2 - cpDist} ${y2}, ${x2} ${y2}`; } /* --- Rendering --- */ function renderWires() { let svgHTML = ''; connections.forEach(conn => { const from = getPortCoords(conn.fromNode, conn.fromPort); const to = getPortCoords(conn.toNode, conn.toPort); svgHTML += ``; }); if (wiringStart && tempWirePath) svgHTML += ``; wireLayer.innerHTML = svgHTML; } function updateNodePositions() { Object.values(nodes).forEach(n => { if (n.el) { n.el.style.left = `${n.x}px`; n.el.style.top = `${n.y}px`; } }); renderWires(); } function clearSelection() { selectedWireId = null; selectedNodeId = null; document.querySelectorAll('.pb-node.selected').forEach(el => el.classList.remove('selected')); renderWires(); } /* --- Node Logic --- */ function createNodeElement(node) { const el = document.createElement('div'); el.className = `pb-node`; el.dataset.id = node.id; el.style.left = `${node.x}px`; el.style.top = `${node.y}px`; el.style.width = `${PC_PARTS[node.type].w}px`; el.style.height = `${PC_PARTS[node.type].h}px`; el.style.zIndex = PC_PARTS[node.type].z; let innerHTML = `${PC_PARTS[node.type].svg}`; PC_PARTS[node.type].ports.forEach(p => { innerHTML += `
`; }); el.innerHTML = innerHTML; viewport.appendChild(el); node.el = el; return el; } function spawnNode(type, dropX = null, dropY = null) { const id = `node_${nextNodeId++}`; const x = dropX !== null ? dropX : 300 + Math.random()*40; const y = dropY !== null ? dropY : 150 + Math.random()*40; const node = { id, type, x, y, snappedTo: null, el: null }; if (PC_PARTS[type].slots) { node.slots = { ...PC_PARTS[type].slots }; for(let k in node.slots) { node.slots[k] = null; } } nodes[id] = node; createNodeElement(node); evaluateBuild(); return id; } function moveNodeRecursive(nodeId, dx, dy) { const n = nodes[nodeId]; if(!n) return; n.x += dx; n.y += dy; if(n.slots) { Object.keys(n.slots).forEach(k => { if(typeof n.slots[k] === 'string') moveNodeRecursive(n.slots[k], dx, dy); }); } } /* --- SYSTEM DIAGNOSTICS & VARIABLE BOOT SPEED --- */ function evaluateBuild() { if(!specsContainer) return; let hasCase=false, hasMB=false, hasCPU=false, hasCooler=false, hasRAM=false, hasPSU=false, hasStorage=false, hasGPU=false; let mbPwr=false, gpuPwr=false, storPwr=false, storData=false, dispConn=false, usbCount=0; let caseNode = Object.values(nodes).find(n => n.type === 'CASE'); let mbNode = Object.values(nodes).find(n => n.type === 'MB'); if (caseNode) { hasCase = true; if (caseNode.slots['MB1']) hasMB = true; if (caseNode.slots['PSU1']) hasPSU = true; if (caseNode.slots['HDD1'] || caseNode.slots['HDD2'] || caseNode.slots['SATA_SSD1'] || caseNode.slots['SATA_SSD2']) hasStorage = true; } else if (mbNode) { hasMB = true; } if (mbNode) { if (mbNode.slots['CPU1']) hasCPU = true; if (mbNode.slots['COOLER1']) hasCooler = true; if (mbNode.slots['RAM1'] || mbNode.slots['RAM2'] || mbNode.slots['RAM3'] || mbNode.slots['RAM4']) hasRAM = true; if (mbNode.slots['PCIE1'] || mbNode.slots['PCIE2']) hasGPU = true; if (mbNode.slots['M2_1'] || mbNode.slots['M2_2']) { hasStorage = true; storPwr = true; storData = true; } } connections.forEach(c => { let n1 = nodes[c.fromNode], n2 = nodes[c.toNode]; if(!n1 || !n2) return; let types = [n1.type, n2.type], ports = [c.fromPort, c.toPort]; if(types.includes('MB') && types.includes('PSU')) mbPwr = true; if(types.includes('GPU') && types.includes('PSU')) gpuPwr = true; if(types.includes('PSU') && (types.includes('HDD') || types.includes('SATA_SSD')) && ports.includes('pwr')) storPwr = true; if(types.includes('MB') && (types.includes('HDD') || types.includes('SATA_SSD')) && ports.includes('data')) storData = true; if(types.includes('MB') && ['KEYBOARD','MOUSE'].some(t => types.includes(t))) usbCount++; if((types.includes('MB') || types.includes('GPU')) && types.includes('MONITOR')) dispConn = true; }); const isBootable = (hasMB && hasCPU && hasCooler && hasRAM && hasPSU && hasStorage && mbPwr && (hasGPU ? gpuPwr : true) && dispConn); // Determine the Boot Speed based on the connected drive let bootSpeed = 8000; // Default slow HDD let activeDrive = 'HDD'; if (mbNode && (mbNode.slots['M2_1'] || mbNode.slots['M2_2'])) { activeDrive = 'M2_SSD'; } else { Object.values(nodes).forEach(n => { if ((n.type === 'SATA_SSD' || n.type === 'HDD') && n.snappedTo) activeDrive = n.type; }); } if (activeDrive === 'M2_SSD') bootSpeed = 1500; else if (activeDrive === 'SATA_SSD') bootSpeed = 3500; // Auto-Trigger the Boot Animation if (isBootable && !isSystemBooted) { isSystemBooted = true; triggerBootSequence(bootSpeed); } else if (!isBootable) { isSystemBooted = false; resetMonitor(); } specsContainer.innerHTML = `
CORE SYSTEM
CHASSIS${hasCase ? 'OK' : 'ERR'}
MOTHERBOARD${hasMB ? 'OK' : 'ERR'}
CPU${hasCPU ? 'OK' : 'ERR'}
COOLING${hasCooler ? 'OK' : 'ERR'}
MEMORY${hasRAM ? 'OK' : 'ERR'}
POWER SPLY${hasPSU ? 'OK' : 'ERR'}
CONNECTIONS
MB POWER${mbPwr ? 'OK' : 'ERR'}
STORAGE${(hasStorage && storPwr && storData) ? 'OK' : 'ERR'}
GPU POWER${!hasGPU ? 'N/A' : (gpuPwr ? 'OK' : 'ERR')}
DISPLAY${dispConn ? 'OK' : 'ERR'}
USB DEVS${usbCount}

${isBootable ? 'BOOTING...' : 'HALTED'}
`; } function triggerBootSequence(duration) { const monitor = Object.values(nodes).find(n => n.type === 'MONITOR'); if (!monitor) return; const bootContent = monitor.el.querySelector('#boot-content'); const durSeconds = (duration / 1000).toFixed(1); bootContent.innerHTML = `Starting Windows`; setTimeout(() => { bootContent.innerHTML = ``; }, duration + 300); // Small buffer to let the bar finish } function resetMonitor() { const monitor = Object.values(nodes).find(n => n.type === 'MONITOR'); if (monitor) monitor.el.querySelector('#boot-content').innerHTML = ''; } /* --- INTERACTION (Drag, Drop, Snap, Wire) --- */ document.getElementById("btnZoomIn")?.addEventListener('click', () => { const r = workspace.getBoundingClientRect(); zoomWorkspace(1.2, r.width/2, r.height/2); }); document.getElementById("btnZoomOut")?.addEventListener('click', () => { const r = workspace.getBoundingClientRect(); zoomWorkspace(1/1.2, r.width/2, r.height/2); }); document.getElementById("btnZoomReset")?.addEventListener('click', () => { panX = 0; panY = 0; zoom = 1; updateViewport(); }); workspace.addEventListener('wheel', (e) => { e.preventDefault(); const wsRect = workspace.getBoundingClientRect(); zoomWorkspace(e.deltaY < 0 ? 1.1 : (1/1.1), e.clientX - wsRect.left, e.clientY - wsRect.top); }); workspace.addEventListener('mousedown', (e) => { const port = e.target.closest('.pb-port'); if (port) { const nodeEl = port.closest('.pb-node'); const portId = port.dataset.port; const existingIdx = connections.findIndex(c => (c.toNode === nodeEl.dataset.id && c.toPort === portId) || (c.fromNode === nodeEl.dataset.id && c.fromPort === portId)); if (existingIdx !== -1) { connections.splice(existingIdx, 1); evaluateBuild(); renderWires(); return; } const coords = getPortCoords(nodeEl.dataset.id, portId); wiringStart = { node: nodeEl.dataset.id, port: portId, x: coords.x, y: coords.y }; tempWirePath = { x: coords.x, y: coords.y }; return; } const wire = e.target.closest('.pb-wire'); if (wire && wire.dataset.connId) { clearSelection(); selectedWireId = wire.dataset.connId; renderWires(); e.stopPropagation(); return; } const nodeEl = e.target.closest('.pb-node'); if (nodeEl) { clearSelection(); selectedNodeId = nodeEl.dataset.id; nodeEl.classList.add('selected'); isDraggingNode = nodeEl.dataset.id; const rect = nodeEl.getBoundingClientRect(); dragOffset = { x: (e.clientX - rect.left) / zoom, y: (e.clientY - rect.top) / zoom }; const node = nodes[isDraggingNode]; if (node.snappedTo) { const parent = nodes[node.snappedTo.id]; if (parent && parent.slots[node.snappedTo.key] === node.id) parent.slots[node.snappedTo.key] = null; node.snappedTo = null; node.el.style.zIndex = PC_PARTS[node.type].z; evaluateBuild(); } return; } clearSelection(); isPanning = true; panStart = { x: e.clientX - panX, y: e.clientY - panY }; }); window.addEventListener('mousemove', (e) => { const wsRect = workspace.getBoundingClientRect(); if (isPanning) { panX = e.clientX - panStart.x; panY = e.clientY - panStart.y; updateViewport(); return; } if (isDraggingNode) { const node = nodes[isDraggingNode]; let newX = (e.clientX - wsRect.left - panX) / zoom - dragOffset.x; let newY = (e.clientY - wsRect.top - panY) / zoom - dragOffset.y; moveNodeRecursive(node.id, newX - node.x, newY - node.y); updateNodePositions(); } if (wiringStart) { tempWirePath = { x: (e.clientX - wsRect.left - panX) / zoom, y: (e.clientY - wsRect.top - panY) / zoom }; renderWires(); } }); window.addEventListener('mouseup', (e) => { if (isDraggingNode) { const node = nodes[isDraggingNode]; let snapped = false; Object.values(nodes).forEach(target => { if (target.slots && !snapped && target.id !== node.id) { for(let slotKey in target.slots) { let slotDef = PC_PARTS[target.type].slots[slotKey]; if(slotDef.accepts === node.type && target.slots[slotKey] === null) { let tX = target.x + slotDef.x; let tY = target.y + slotDef.y; if (Math.hypot(node.x - tX, node.y - tY) < 80) { moveNodeRecursive(node.id, tX - node.x, tY - node.y); node.snappedTo = { id: target.id, key: slotKey }; target.slots[slotKey] = node.id; node.el.style.zIndex = PC_PARTS[target.type].z + 5; snapped = true; break; } } } } }); isDraggingNode = null; updateNodePositions(); evaluateBuild(); } if (wiringStart) { const port = e.target.closest('.pb-port'); if (port) { const targetNodeId = port.closest('.pb-node').dataset.id; const targetPortId = port.dataset.port; if (targetNodeId !== wiringStart.node) { connections.push({ id: `conn_${nextWireId++}`, fromNode: wiringStart.node, fromPort: wiringStart.port, toNode: targetNodeId, toPort: targetPortId }); } } wiringStart = null; tempWirePath = null; evaluateBuild(); renderWires(); } isPanning = false; }); /* --- Deletion & Toolbox UI --- */ function deleteNodeRecursive(id) { const n = nodes[id]; if(!n) return; if(n.slots) { Object.keys(n.slots).forEach(k => { if(typeof n.slots[k] === 'string') deleteNodeRecursive(n.slots[k]); }); } if(n.snappedTo) { const p = nodes[n.snappedTo.id]; if(p) p.slots[n.snappedTo.key] = null; } connections = connections.filter(c => c.fromNode !== id && c.toNode !== id); viewport.removeChild(n.el); delete nodes[id]; } window.addEventListener('keydown', (e) => { if ((e.key === 'Delete' || e.key === 'Backspace') && selectedNodeId) { deleteNodeRecursive(selectedNodeId); clearSelection(); evaluateBuild(); renderWires(); } if ((e.key === 'Delete' || e.key === 'Backspace') && selectedWireId) { connections = connections.filter(c => c.id !== selectedWireId); clearSelection(); evaluateBuild(); renderWires(); } }); workspace.addEventListener('dragover', (e) => { e.preventDefault(); }); workspace.addEventListener('drop', (e) => { e.preventDefault(); const type = e.dataTransfer.getData('spawnType'); if (type) { const r = workspace.getBoundingClientRect(); spawnNode(type, (e.clientX - r.left - panX) / zoom - (PC_PARTS[type].w / 2), (e.clientY - r.top - panY) / zoom - (PC_PARTS[type].h / 2)); } }); btnClearBoard?.addEventListener('click', () => { viewport.querySelectorAll('.pb-node').forEach(el => el.remove()); nodes = {}; connections = []; evaluateBuild(); renderWires(); }); toolboxToggle?.addEventListener("click", () => { const c = pcPage?.classList.contains("toolboxCollapsed"); pcPage.classList.toggle("toolboxCollapsed", !c); toolboxToggle?.setAttribute("aria-expanded", c ? "true" : "false"); }); /* --- Auto-Assemble Engine --- */ function autoAssemble(sT) { btnClearBoard.click(); const mId = spawnNode('MONITOR', 200, 100), kId = spawnNode('KEYBOARD', 230, 320), moId = spawnNode('MOUSE', 450, 330), spId = spawnNode('SPEAKER', 150, 300); const cId = spawnNode('CASE', 550, 100), mbId = spawnNode('MB', 1250, 250), pId = spawnNode('PSU', 1250, 100), cpId = spawnNode('CPU', 1450, 100), coId = spawnNode('COOLER', 1450, 250), rId = spawnNode('RAM', 1600, 100), gId = spawnNode('GPU', 1450, 400), stId = spawnNode(sT, 1600, 250); const plan = [{c:mbId,p:cId,s:'MB1'},{c:pId,p:cId,s:'PSU1'},{c:cpId,p:mbId,s:'CPU1'},{c:coId,p:mbId,s:'COOLER1'},{c:rId,p:mbId,s:'RAM1'},{c:gId,p:mbId,s:'PCIE1'}]; if(sT==='HDD') plan.push({c:stId,p:cId,s:'HDD1'}); if(sT==='SATA_SSD') plan.push({c:stId,p:cId,s:'SATA_SSD1'}); if(sT==='M2_SSD') plan.push({c:stId,p:mbId,s:'M2_1'}); plan.forEach(s => { const ch = nodes[s.c], p = nodes[s.p]; const sD = PC_PARTS[p.type].slots[s.s]; moveNodeRecursive(ch.id, (p.x + sD.x) - ch.x, (p.y + sD.y) - ch.y); ch.snappedTo = { id: p.id, key: s.s }; p.slots[s.s] = ch.id; ch.el.style.zIndex = PC_PARTS[p.type].z + 5; }); const conn = (n1, p1, n2, p2) => connections.push({ id: `conn_${nextWireId++}`, fromNode: n1, fromPort: p1, toNode: n2, toPort: p2 }); conn(pId, 'out1', mbId, 'atx_pwr'); conn(pId, 'out2', gId, 'pwr_in'); if (sT !== 'M2_SSD') { conn(pId, 'out3', stId, 'pwr'); conn(mbId, 'sata1', stId, 'data'); } conn(gId, 'disp_out', mId, 'disp'); conn(mbId, 'usb1', kId, 'usb'); conn(mbId, 'usb2', moId, 'usb'); conn(mbId, 'audio', spId, 'audio'); updateNodePositions(); evaluateBuild(); } document.getElementById('btnAssembleHDD')?.addEventListener('click', () => autoAssemble('HDD')); document.getElementById('btnAssembleSATA')?.addEventListener('click', () => autoAssemble('SATA_SSD')); document.getElementById('btnAssembleM2')?.addEventListener('click', () => autoAssemble('M2_SSD')); initToolbox(); evaluateBuild(); })();