You've already forked computing-box
feat(app): update branding, improve button animations, and complete PC Components simulator
- Replace logo with updated Computing:Box branding assets - Fix animations for Random and Reset buttons for smoother interaction - Implement full first version of the PC Components simulator - Update built output to reflect new assets, layout, and functionality - Remove legacy assets and outdated build files Signed-off-by: Alexander Lyall <alex@adcm.uk>
This commit is contained in:
@@ -375,7 +375,7 @@
|
||||
|
||||
setRandomRunning(true);
|
||||
const start = Date.now();
|
||||
const durationMs = 1125;
|
||||
const durationMs = 1500;
|
||||
const tickMs = 80;
|
||||
|
||||
randomTimer = setInterval(() => {
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
let nextNodeId = 1;
|
||||
let nextWireId = 1;
|
||||
let discoveredStates = new Set();
|
||||
|
||||
// Interaction State
|
||||
let isDraggingNode = null;
|
||||
@@ -199,41 +200,97 @@
|
||||
}
|
||||
|
||||
/* --- Truth Table Generation --- */
|
||||
function generateTruthTable() {
|
||||
if (!ttContainer) return;
|
||||
function generateTruthTable() {
|
||||
// 1. Find the target container
|
||||
let container = document.getElementById("truthTableContainer");
|
||||
|
||||
// Fail-safe: Find the card if the specific ID is missing
|
||||
if (!container) {
|
||||
const cards = document.querySelectorAll('.card');
|
||||
const ttCard = Array.from(cards).find(c => c.innerText.includes('LIVE TRUTH TABLE'));
|
||||
if (ttCard) {
|
||||
container = ttCard.querySelector('.cardBodyInner') || ttCard;
|
||||
}
|
||||
}
|
||||
|
||||
const inNodes = Object.values(nodes).filter(n => n.type === 'INPUT').sort((a,b) => a.label.localeCompare(b.label));
|
||||
const outNodes = Object.values(nodes).filter(n => n.type === 'OUTPUT').sort((a,b) => a.label.localeCompare(b.label));
|
||||
if (!container) return;
|
||||
|
||||
// 2. Identify and sort Inputs and Outputs
|
||||
const inNodes = Object.values(nodes)
|
||||
.filter(n => n.type === 'INPUT')
|
||||
.sort((a,b) => a.label.localeCompare(b.label));
|
||||
const outNodes = Object.values(nodes)
|
||||
.filter(n => n.type === 'OUTPUT')
|
||||
.sort((a,b) => a.label.localeCompare(b.label));
|
||||
|
||||
// 3. Handle Empty State
|
||||
if (inNodes.length === 0 || outNodes.length === 0) {
|
||||
ttContainer.innerHTML = '<div style="padding: 16px; color: var(--muted); text-align:center;">Add inputs and outputs to generate table.</div>'; return;
|
||||
}
|
||||
if (inNodes.length > 6) {
|
||||
ttContainer.innerHTML = '<div style="padding: 16px; color: var(--muted); text-align:center;">Maximum 6 inputs supported.</div>'; return;
|
||||
container.innerHTML = '<div style="padding: 20px; color: var(--muted); text-align:center; font-family: var(--bit-font); font-size: 12px; letter-spacing: 1px;">CONNECT INPUTS & OUTPUTS</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<table class="tt-table"><thead><tr>';
|
||||
// 4. Build Table within the styled wrapper
|
||||
let html = '<div class="tt-table-wrap"><table class="tt-table"><thead><tr>';
|
||||
|
||||
// Headers
|
||||
inNodes.forEach(n => html += `<th>${n.label}</th>`);
|
||||
outNodes.forEach(n => html += `<th style="color:var(--text);">${n.label}</th>`);
|
||||
outNodes.forEach(n => html += `<th style="color:var(--text); border-left: 1px solid rgba(255,255,255,0.1);">${n.label}</th>`);
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
// 5. Generate Rows
|
||||
const numRows = Math.pow(2, inNodes.length);
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
let override = {};
|
||||
inNodes.forEach((n, idx) => { override[n.id] = ((i >> (inNodes.length - 1 - idx)) & 1) === 1; });
|
||||
let outStates = evaluateGraph(override);
|
||||
let stateArr = [];
|
||||
|
||||
// Calculate binary state for this row
|
||||
inNodes.forEach((n, idx) => {
|
||||
let val = ((i >> (inNodes.length - 1 - idx)) & 1) === 1;
|
||||
override[n.id] = val;
|
||||
stateArr.push(val ? '1' : '0');
|
||||
});
|
||||
|
||||
let stateStr = stateArr.join('');
|
||||
let isFound = discoveredStates.has(stateStr);
|
||||
let outResults = evaluateGraph(override); // Simulate the board logic for this state
|
||||
|
||||
html += '<tr>';
|
||||
inNodes.forEach(n => { let val = override[n.id]; html += `<td class="${val ? 'tt-on' : ''}">${val ? 1 : 0}</td>`; });
|
||||
outNodes.forEach(n => { let val = outStates[n.id]; html += `<td class="${val ? 'tt-on' : ''}" style="font-weight:bold;">${val ? 1 : 0}</td>`; });
|
||||
|
||||
// Input Cells
|
||||
inNodes.forEach(n => {
|
||||
let v = override[n.id];
|
||||
html += `<td class="${v ? 'tt-on' : ''}">${v ? 1 : 0}</td>`;
|
||||
});
|
||||
|
||||
// Output Cells (Discovery Logic)
|
||||
outNodes.forEach(n => {
|
||||
if (isFound) {
|
||||
let v = outResults[n.id];
|
||||
html += `<td class="${v ? 'tt-on' : ''}" style="font-weight:bold; border-left: 1px solid rgba(255,255,255,0.05);">${v ? 1 : 0}</td>`;
|
||||
} else {
|
||||
html += `<td style="color: #444; border-left: 1px solid rgba(255,255,255,0.05); opacity: 0.6;">?</td>`;
|
||||
}
|
||||
});
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
ttContainer.innerHTML = html;
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function runSimulation() {
|
||||
function runSimulation(topologyChanged = false) {
|
||||
// If you add/remove wires, reset the table memory because the logic changed
|
||||
if (topologyChanged) discoveredStates.clear();
|
||||
|
||||
evaluateGraph();
|
||||
|
||||
// Check the current board state (e.g., "10") and save it to memory
|
||||
const inNodes = Object.values(nodes).filter(n => n.type === 'INPUT').sort((a,b) => a.label.localeCompare(b.label));
|
||||
if (inNodes.length > 0) {
|
||||
let currentStateStr = inNodes.map(n => n.value ? '1' : '0').join('');
|
||||
discoveredStates.add(currentStateStr);
|
||||
}
|
||||
|
||||
renderWires();
|
||||
generateTruthTable();
|
||||
}
|
||||
@@ -288,19 +345,18 @@
|
||||
viewport.appendChild(el);
|
||||
node.el = el;
|
||||
|
||||
if (node.type === 'INPUT') {
|
||||
el.querySelector('.switch').addEventListener('click', (e) => {
|
||||
const dist = Math.hypot(e.clientX - clickStartX, e.clientY - clickStartY);
|
||||
if (dist > 3) {
|
||||
e.preventDefault(); // Prevents toggle if it was a drag motion
|
||||
} else {
|
||||
node.value = !node.value;
|
||||
el.querySelector('.switch').classList.toggle('active-sim', node.value);
|
||||
el.querySelector('.slider').style.background = node.value ? 'rgba(40,240,122,.25)' : '';
|
||||
el.querySelector('.slider').style.borderColor = node.value ? 'rgba(40,240,122,.30)' : '';
|
||||
el.querySelector('.slider').innerHTML = node.value ? `<style>#logicPage [data-id="${node.id}"] .slider::before { transform: translateX(28px); }</style>` : '';
|
||||
runSimulation();
|
||||
}
|
||||
if (node.type === 'INPUT') {
|
||||
const sw = el.querySelector('.switch');
|
||||
sw.addEventListener('click', (e) => {
|
||||
// ... (keep your clickStartX/Y drag check) ...
|
||||
|
||||
node.value = !node.value;
|
||||
|
||||
// This targets the exact class your CSS needs for the glow and move
|
||||
sw.classList.toggle('active-sim', node.value);
|
||||
|
||||
// This ensures the table and logic update
|
||||
runSimulation();
|
||||
});
|
||||
}
|
||||
return el;
|
||||
@@ -312,7 +368,8 @@
|
||||
if (type === 'OUTPUT') label = getNextOutputLabel();
|
||||
if (type === 'GATE') label = gateType;
|
||||
|
||||
const id = `node_${nextNodeId++}`;
|
||||
// Double check this line in logicGates.js
|
||||
const id = `node_${Date.now()}_${nextNodeId++}`;
|
||||
const offset = Math.floor(Math.random() * 40);
|
||||
const x = dropX !== null ? dropX : (type === 'INPUT' ? 50 : (type === 'OUTPUT' ? 600 : 300) + offset);
|
||||
const y = dropY !== null ? dropY : 150 + offset;
|
||||
@@ -320,7 +377,8 @@
|
||||
const node = { id, type, gateType, label, x, y, value: false, el: null };
|
||||
nodes[id] = node;
|
||||
createNodeElement(node);
|
||||
runSimulation();
|
||||
// Change the very last line to:
|
||||
runSimulation(true);
|
||||
}
|
||||
|
||||
/* --- Global Interaction Handlers --- */
|
||||
@@ -433,8 +491,9 @@
|
||||
connections.push({ id: `conn_${nextWireId++}`, fromNode: wiringStart.node, fromPort: 'out', toNode: targetNodeId, toPort: targetPortId });
|
||||
}
|
||||
}
|
||||
// Change the very last line of the if(wiringStart) block to:
|
||||
wiringStart = null; tempWirePath = null;
|
||||
runSimulation();
|
||||
runSimulation(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -451,7 +510,8 @@
|
||||
viewport.removeChild(nodes[selectedNodeId].el);
|
||||
}
|
||||
delete nodes[selectedNodeId];
|
||||
clearSelection(); runSimulation();
|
||||
// Change the two deletion triggers to:
|
||||
clearSelection(); runSimulation(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -471,10 +531,17 @@
|
||||
});
|
||||
|
||||
/* --- Init --- */
|
||||
btnClearBoard?.addEventListener('click', () => {
|
||||
btnClearBoard?.addEventListener('click', () => {
|
||||
viewport.querySelectorAll('.lg-node').forEach(el => el.remove());
|
||||
nodes = {}; connections = [];
|
||||
runSimulation();
|
||||
|
||||
// Target your specific SVG layer class
|
||||
const svgLayer = document.querySelector('.lg-svg-layer');
|
||||
if (svgLayer) svgLayer.innerHTML = '';
|
||||
|
||||
nodes = {};
|
||||
connections = [];
|
||||
discoveredStates.clear();
|
||||
runSimulation(true);
|
||||
});
|
||||
|
||||
toolboxToggle?.addEventListener("click", () => {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
const toolboxToggle = document.getElementById("toolboxToggle");
|
||||
const pcPage = document.getElementById("pcPage");
|
||||
|
||||
/* --- Extensive PC Component Library --- */
|
||||
/* --- ULTRA-REALISTIC COMPONENT LIBRARY --- */
|
||||
const PC_PARTS = {
|
||||
'CASE': {
|
||||
name: 'ATX PC Case', w: 600, h: 550, z: 5, ports: [],
|
||||
@@ -29,8 +29,7 @@
|
||||
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: 'usb3', x: 10, y: 100 }, { id: 'usb4', x: 10, y: 130 },
|
||||
{ id: 'audio', x: 10, y: 170 }, { id: 'disp', x: 10, y: 210 }
|
||||
{ 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' },
|
||||
@@ -40,21 +39,56 @@
|
||||
'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' }
|
||||
},
|
||||
// Uses a lighter slate grey #2C303A to stand out from the case
|
||||
svg: `<rect width="360" height="400" fill="#2C303A" rx="8" stroke="#4b5060" stroke-width="3"/><rect x="120" y="40" width="80" height="80" fill="#1f2229" stroke="#4b5060"/><rect x="230" y="30" width="15" height="100" fill="#1f2229"/><rect x="250" y="30" width="15" height="100" fill="#1f2229"/><rect x="270" y="30" width="15" height="100" fill="#1f2229"/><rect x="290" y="30" width="15" height="100" fill="#1f2229"/><rect x="40" y="200" width="280" height="15" fill="#15171c"/><rect x="40" y="300" width="280" height="15" fill="#15171c"/><rect x="120" y="170" width="80" height="15" fill="#1f2229"/><rect x="120" y="250" width="80" height="15" fill="#1f2229"/>`
|
||||
svg: `<rect width="360" height="400" fill="#2C303A" rx="8" stroke="#4b5060" stroke-width="3"/><rect x="120" y="40" width="80" height="80" fill="#1f2229" stroke="#4b5060"/><rect x="230" y="30" width="15" height="100" fill="#1f2229"/><rect x="250" y="30" width="15" height="100" fill="#1f2229"/><rect x="270" y="30" width="15" height="100" fill="#1f2229"/><rect x="290" y="30" width="15" height="100" fill="#1f2229"/><rect x="40" y="200" width="280" height="15" fill="#15171c"/><rect x="40" y="300" width="280" height="15" fill="#15171c"/><rect x="120" y="170" width="80" height="15" fill="#1f2229" stroke="#4b5060" stroke-dasharray="2 2"/><text x="160" y="182" fill="#555" font-size="10" font-family="sans-serif" text-anchor="middle">M.2_1</text><rect x="120" y="250" width="80" height="15" fill="#1f2229" stroke="#4b5060" stroke-dasharray="2 2"/><text x="160" y="262" fill="#555" font-size="10" font-family="sans-serif" text-anchor="middle">M.2_2</text>`
|
||||
},
|
||||
'CPU': { name: 'Processor', w: 80, h: 80, z: 20, ports: [], slots: {}, svg: `<rect width="80" height="80" fill="#0b381a"/><rect x="10" y="10" width="60" height="60" rx="4" fill="#d4d4d4"/><polygon points="5,75 15,75 5,65" fill="#ffd700"/><text x="40" y="45" fill="#555" font-family="sans-serif" font-size="14" font-weight="bold" text-anchor="middle">CPU</text>` },
|
||||
'COOLER': { name: 'CPU Fan', w: 120, h: 120, z: 30, ports: [], slots: {}, svg: `<rect width="120" height="120" rx="60" fill="#1a1c23" stroke="#aaa" stroke-width="3"/><circle cx="60" cy="60" r="50" fill="#111"/><path d="M60,15 A45,45 0 0,1 105,60 L60,60 Z" fill="#444"/><path d="M105,60 A45,45 0 0,1 60,105 L60,60 Z" fill="#555"/><path d="M60,105 A45,45 0 0,1 15,60 L60,60 Z" fill="#444"/><path d="M15,60 A45,45 0 0,1 60,15 L60,60 Z" fill="#555"/><circle cx="60" cy="60" r="20" fill="#222"/>` },
|
||||
'RAM': { name: 'DDR4 Memory', w: 15, h: 100, z: 20, ports: [], slots: {}, svg: `<rect width="15" height="100" fill="#111"/><rect x="2" y="5" width="11" height="80" fill="#2a2a2a"/><rect x="0" y="90" width="15" height="10" fill="#ffd700"/>` },
|
||||
'GPU': { name: 'Graphics Card', w: 280, h: 60, z: 40, slots: {}, ports: [{ id: 'pwr_in', x: 270, y: 10 }, { id: 'disp_out', x: 10, y: 30 }], svg: `<rect width="280" height="60" rx="5" fill="#1a1a1a"/><circle cx="70" cy="30" r="22" fill="#111" stroke="#333" stroke-width="2"/><circle cx="140" cy="30" r="22" fill="#111" stroke="#333" stroke-width="2"/><circle cx="210" cy="30" r="22" fill="#111" stroke="#333" stroke-width="2"/><rect x="20" y="55" width="80" height="5" fill="#ffd700"/>` },
|
||||
'M2_SSD': { name: 'M.2 NVMe SSD', w: 80, h: 15, z: 20, ports: [], slots: {}, svg: `<rect width="80" height="15" rx="1" fill="#000"/><rect x="10" y="2" width="20" height="11" fill="#1a1a1a"/><rect x="35" y="2" width="20" height="11" fill="#1a1a1a"/><rect x="60" y="2" width="10" height="11" fill="#ccc"/><rect x="0" y="0" width="4" height="15" fill="#ffd700"/>` },
|
||||
'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: `<rect width="100" height="70" fill="#111" rx="4" stroke="#444"/><rect x="10" y="10" width="80" height="50" fill="#1a1a1a" rx="2" stroke="#222"/><text x="50" y="40" fill="#888" font-family="sans-serif" font-size="14" font-weight="bold" text-anchor="middle">SSD</text>` },
|
||||
'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: `<rect width="120" height="140" fill="#d0d0d0" rx="4" stroke="#888"/><rect x="10" y="10" width="100" height="100" fill="#e0e0e0" rx="50"/><circle cx="60" cy="60" r="35" fill="#ddd" stroke="#aaa"/><circle cx="60" cy="60" r="10" fill="#999"/><rect x="30" y="120" width="60" height="10" fill="#111"/>` },
|
||||
'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: `<rect width="160" height="90" rx="4" fill="#1a1a1a" stroke="#333" stroke-width="2"/><circle cx="80" cy="45" r="35" fill="#0a0a0a" stroke="#222" stroke-width="2"/><line x1="80" y1="10" x2="80" y2="80" stroke="#333" stroke-width="2"/><line x1="45" y1="45" x2="115" y2="45" stroke="#333" stroke-width="2"/><circle cx="80" cy="45" r="10" fill="#222"/>` },
|
||||
'MONITOR': { name: 'Monitor', w: 240, h: 160, z: 30, slots: {}, ports: [{id:'disp', x:120, y:140}], svg: `<rect width="240" height="160" fill="#111" rx="5"/><rect x="10" y="10" width="220" height="120" fill="#000"/><rect x="100" y="140" width="40" height="20" fill="#222"/><rect x="60" y="150" width="120" height="10" fill="#222"/>` },
|
||||
'KEYBOARD': { name: 'Keyboard', w: 180, h: 60, z: 30, slots: {}, ports: [{id:'usb', x:90, y:10}], svg: `<rect width="180" height="60" fill="#111" rx="3"/><rect x="5" y="5" width="170" height="50" fill="#222" rx="2" stroke="#333" stroke-dasharray="8 8"/>` },
|
||||
'MOUSE': { name: 'Mouse', w: 30, h: 50, z: 30, slots: {}, ports: [{id:'usb', x:15, y:5}], svg: `<rect width="30" height="50" fill="#111" rx="15"/><line x1="15" y1="0" x2="15" y2="20" stroke="#333" stroke-width="2"/><circle cx="15" cy="15" r="4" fill="#333"/>` },
|
||||
'SPEAKER': { name: 'Speakers', w: 40, h: 80, z: 30, slots: {}, ports: [{id:'audio', x:20, y:10}], svg: `<rect width="40" height="80" fill="#111" rx="4"/><circle cx="20" cy="25" r="12" fill="#222"/><circle cx="20" cy="60" r="16" fill="#222"/>` }
|
||||
'CPU': {
|
||||
name: 'Processor', w: 80, h: 80, z: 20, ports: [], slots: {},
|
||||
svg: `<rect width="80" height="80" fill="#0c4a22" rx="4"/><rect x="2" y="2" width="76" height="76" fill="none" stroke="#ffd700" stroke-width="1" stroke-dasharray="2 4"/><rect x="12" y="12" width="56" height="56" fill="#e0e4e8" rx="6" stroke="#b0b5b9" stroke-width="2"/><text x="40" y="35" fill="#666" font-family="sans-serif" font-size="10" font-weight="900" text-anchor="middle">INTEL</text><text x="40" y="50" fill="#555" font-family="sans-serif" font-size="16" font-weight="900" text-anchor="middle">CORE i9</text><text x="40" y="60" fill="#777" font-family="sans-serif" font-size="7" font-weight="bold" text-anchor="middle">14900K</text><polygon points="5,75 15,75 5,65" fill="#ffd700"/>`
|
||||
},
|
||||
'COOLER': {
|
||||
name: 'Liquid AIO', w: 120, h: 120, z: 30, ports: [], slots: {},
|
||||
svg: `<circle cx="60" cy="60" r="55" fill="#15171e" stroke="#2d313d" stroke-width="4"/><circle cx="60" cy="60" r="45" fill="#050505"/><text x="60" y="55" fill="#28f07a" font-family="var(--num-font)" font-size="20" font-weight="bold" text-anchor="middle">32°C</text><text x="60" y="75" fill="#55aaff" font-family="var(--ui-font)" font-size="10" text-anchor="middle">2400 RPM</text><path d="M 110 40 Q 140 40 140 10 M 110 80 Q 150 80 150 110" fill="none" stroke="#111" stroke-width="12" stroke-linecap="round"/><circle cx="60" cy="60" r="50" fill="none" stroke="cyan" stroke-width="2" opacity="0.8"/>`
|
||||
},
|
||||
'RAM': {
|
||||
name: 'RGB Memory', w: 15, h: 100, z: 20, ports: [], slots: {},
|
||||
svg: `<rect width="15" height="100" fill="#111" rx="2"/><rect x="0" y="90" width="15" height="10" fill="#ffd700"/><rect x="0" y="94" width="15" height="1" fill="#b8860b"/><path d="M -2 15 L 17 15 L 17 85 L -2 85 Z" fill="#2d313d" stroke="#111"/><path d="M 0 20 L 15 30 L 15 80 L 0 70 Z" fill="#1a1c23"/><path d="M -2 2 L 17 2 L 17 15 L -2 15 Z" fill="#ff0055"/><path d="M 0 2 L 5 10 L 10 2 L 15 10" fill="none" stroke="#fff" stroke-width="1" opacity="0.5"/>`
|
||||
},
|
||||
'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: `<rect width="280" height="80" rx="8" fill="#15171e" stroke="#333742" stroke-width="2"/><rect x="5" y="5" width="270" height="70" rx="6" fill="#0f1015"/><path d="M 20 5 L 60 75 M 110 5 L 150 75 M 200 5 L 240 75" stroke="#1a1c23" stroke-width="4"/><g transform="translate(50, 40)"><circle r="32" fill="#111" stroke="#2d313d" stroke-width="2"/><circle r="10" fill="#222"/><path d="M0 -10 L15 -28 L25 -20 Z M0 10 L-15 28 L-25 20 Z M-10 0 L-28 -15 L-20 -25 Z M10 0 L28 15 L20 25 Z" fill="#1a1c23"/></g><g transform="translate(140, 40)"><circle r="32" fill="#111" stroke="#2d313d" stroke-width="2"/><circle r="10" fill="#222"/><path d="M0 -10 L15 -28 L25 -20 Z M0 10 L-15 28 L-25 20 Z M-10 0 L-28 -15 L-20 -25 Z M10 0 L28 15 L20 25 Z" fill="#1a1c23"/></g><g transform="translate(230, 40)"><circle r="32" fill="#111" stroke="#2d313d" stroke-width="2"/><circle r="10" fill="#222"/><path d="M0 -10 L15 -28 L25 -20 Z M0 10 L-15 28 L-25 20 Z M-10 0 L-28 -15 L-20 -25 Z M10 0 L28 15 L20 25 Z" fill="#1a1c23"/></g><rect x="20" y="80" width="160" height="8" fill="#ffd700" rx="2"/><rect x="100" y="32" width="80" height="16" fill="#000" rx="2" opacity="0.8"/><text x="140" y="43" fill="#28f07a" font-family="sans-serif" font-size="10" font-weight="900" text-anchor="middle">GEFORCE RTX</text>`
|
||||
},
|
||||
'M2_SSD': {
|
||||
name: 'M.2 NVMe SSD', w: 80, h: 22, z: 20, ports: [], slots: {},
|
||||
svg: `<rect width="80" height="22" fill="#111" rx="2"/><rect x="0" y="0" width="5" height="22" fill="#ffd700"/><rect x="3" y="14" width="3" height="4" fill="#111"/><rect x="15" y="4" width="18" height="14" fill="#1a1c23" rx="1"/><rect x="38" y="4" width="18" height="14" fill="#1a1c23" rx="1"/><rect x="60" y="6" width="10" height="10" fill="#2d313d" rx="1"/><rect x="10" y="8" width="50" height="6" fill="#fff" opacity="0.8"/><text x="35" y="13" fill="#000" font-family="sans-serif" font-size="4" font-weight="bold" text-anchor="middle">990 PRO 2TB</text><circle cx="76" cy="11" r="3" fill="#222"/>`
|
||||
},
|
||||
'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: `<rect width="100" height="70" fill="#1a1c23" rx="4" stroke="#4b5162" stroke-width="1"/><rect x="2" y="2" width="96" height="66" fill="#2d313d" rx="2"/><rect x="15" y="15" width="70" height="40" fill="#111" rx="2"/><rect x="15" y="45" width="70" height="10" fill="#e74c3c"/><text x="50" y="35" fill="#fff" font-family="sans-serif" font-size="14" font-weight="900" text-anchor="middle" letter-spacing="1px">SAMSUNG</text><circle cx="5" cy="5" r="1.5" fill="#111"/><circle cx="95" cy="5" r="1.5" fill="#111"/><circle cx="5" cy="65" r="1.5" fill="#111"/><circle cx="95" cy="65" r="1.5" fill="#111"/>`
|
||||
},
|
||||
'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: `<rect width="120" height="140" fill="#bdc3c7" rx="4" stroke="#7f8c8d" stroke-width="2"/><path d="M 5 5 L 115 5 L 115 110 C 80 120, 40 120, 5 110 Z" fill="#e0e4e8" stroke="#95a5a6" stroke-width="1"/><circle cx="60" cy="55" r="45" fill="none" stroke="#bdc3c7" stroke-width="2"/><circle cx="60" cy="55" r="12" fill="#bdc3c7" stroke="#95a5a6"/><circle cx="100" cy="100" r="8" fill="#bdc3c7" stroke="#95a5a6"/><path d="M 100 100 L 70 60" stroke="#7f8c8d" stroke-width="6" stroke-linecap="round"/><rect x="30" y="80" width="60" height="30" fill="#fff" rx="2"/><text x="60" y="92" fill="#000" font-family="sans-serif" font-size="8" font-weight="bold" text-anchor="middle">WD BLACK</text><text x="60" y="102" fill="#333" font-family="sans-serif" font-size="6" text-anchor="middle">12TB HDD</text><rect x="20" y="120" width="80" height="15" fill="#0b3d21" rx="2"/>`
|
||||
},
|
||||
'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: `<rect width="160" height="90" fill="#15171e" rx="4" stroke="#333742" stroke-width="2"/><rect x="40" y="5" width="80" height="80" fill="#0a0a0a" rx="40"/><circle cx="80" cy="45" r="38" fill="none" stroke="#2d313d" stroke-width="2"/><circle cx="80" cy="45" r="28" fill="none" stroke="#2d313d" stroke-width="2"/><circle cx="80" cy="45" r="18" fill="none" stroke="#2d313d" stroke-width="2"/><path d="M 80 5 L 80 85 M 40 45 L 120 45 M 52 20 L 108 70 M 108 20 L 52 70" stroke="#2d313d" stroke-width="2"/><circle cx="80" cy="45" r="8" fill="#111" stroke="#e74c3c"/><rect x="145" y="10" width="15" height="70" fill="#0a0a0a"/><rect x="5" y="15" width="25" height="60" fill="#333742" rx="2"/><text x="17" y="45" fill="#fff" font-family="sans-serif" font-size="10" font-weight="bold" text-anchor="middle" transform="rotate(-90 17,45)">1200W</text>`
|
||||
},
|
||||
'MONITOR': {
|
||||
name: 'Monitor', w: 240, h: 180, z: 30, slots: {}, ports: [{id:'disp', x:120, y:140}],
|
||||
svg: `<rect width="240" height="160" fill="#1a1a1a" rx="6" stroke="#333"/><rect x="8" y="8" width="224" height="124" fill="#000" id="screen-bg"/><g id="boot-content"></g><rect x="6" y="140" width="228" height="15" fill="#1a1c23"/><text x="120" y="150" fill="#fff" font-family="sans-serif" font-size="6" text-anchor="middle">ASUS</text><circle cx="220" cy="147" r="2" fill="#28f07a"/><path d="M 100 160 L 110 180 L 130 180 L 140 160 Z" fill="#222"/><rect x="80" y="180" width="80" height="5" fill="#333" rx="2"/>`
|
||||
},
|
||||
'KEYBOARD': {
|
||||
name: 'Keyboard', w: 180, h: 60, z: 30, slots: {}, ports: [{id:'usb', x:90, y:10}],
|
||||
svg: `<rect width="180" height="60" fill="#15171e" rx="4" stroke="#2d313d" stroke-width="2"/><rect x="0" y="45" width="180" height="15" fill="#111" rx="2"/><rect x="5" y="5" width="170" height="36" fill="#0a0a0a" rx="2"/><g fill="#222" stroke="#111" stroke-width="1"><rect x="8" y="8" width="10" height="10" rx="2"/><rect x="20" y="8" width="10" height="10" rx="2"/><rect x="32" y="8" width="10" height="10" rx="2"/><rect x="44" y="8" width="10" height="10" rx="2"/><rect x="56" y="8" width="10" height="10" rx="2"/><rect x="68" y="8" width="10" height="10" rx="2"/><rect x="80" y="8" width="10" height="10" rx="2"/><rect x="92" y="8" width="10" height="10" rx="2"/><rect x="104" y="8" width="10" height="10" rx="2"/><rect x="116" y="8" width="10" height="10" rx="2"/><rect x="128" y="8" width="10" height="10" rx="2"/><rect x="140" y="8" width="18" height="10" rx="2"/><rect x="8" y="20" width="14" height="10" rx="2"/><rect x="24" y="20" width="10" height="10" rx="2"/><rect x="36" y="20" width="10" height="10" rx="2"/><rect x="48" y="20" width="10" height="10" rx="2"/><rect x="60" y="20" width="10" height="10" rx="2"/><rect x="72" y="20" width="10" height="10" rx="2"/><rect x="84" y="20" width="10" height="10" rx="2"/><rect x="96" y="20" width="10" height="10" rx="2"/><rect x="108" y="20" width="10" height="10" rx="2"/><rect x="120" y="20" width="10" height="10" rx="2"/><rect x="132" y="20" width="26" height="10" rx="2"/></g><rect x="56" y="32" width="60" height="10" fill="#222" stroke="#111" rx="2"/><rect x="4" y="4" width="172" height="38" fill="none" stroke="cyan" stroke-width="1" opacity="0.3"/>`
|
||||
},
|
||||
'MOUSE': {
|
||||
name: 'Mouse', w: 30, h: 54, z: 30, slots: {}, ports: [{id:'usb', x:15, y:5}],
|
||||
svg: `<rect width="30" height="54" fill="#15171e" rx="15" stroke="#2d313d" stroke-width="2"/><path d="M 15 0 L 15 20 M 5 25 Q 15 30 25 25" stroke="#0a0a0a" stroke-width="2" fill="none"/><rect x="13" y="6" width="4" height="10" fill="#111" rx="2"/><rect x="14" y="7" width="2" height="8" fill="#28f07a"/><path d="M 10 45 Q 15 50 20 45" stroke="cyan" stroke-width="2" fill="none" opacity="0.8"/><path d="M 0 15 Q 4 25 0 35 M 30 15 Q 26 25 30 35" stroke="#111" stroke-width="2" fill="none"/>`
|
||||
},
|
||||
'SPEAKER': {
|
||||
name: 'Speakers', w: 46, h: 90, z: 30, slots: {}, ports: [{id:'audio', x:23, y:10}],
|
||||
svg: `<rect width="46" height="90" fill="#1a1c23" rx="4" stroke="#333742" stroke-width="2"/><rect x="4" y="4" width="38" height="82" fill="#111" rx="2"/><circle cx="23" cy="22" r="10" fill="#2d313d" stroke="#0a0a0a" stroke-width="2"/><circle cx="23" cy="22" r="4" fill="#15171e"/><circle cx="23" cy="58" r="16" fill="#2d313d" stroke="#0a0a0a" stroke-width="3"/><circle cx="23" cy="58" r="6" fill="#15171e"/><circle cx="23" cy="80" r="4" fill="#000"/>`
|
||||
}
|
||||
};
|
||||
|
||||
let nodes = {};
|
||||
@@ -66,27 +100,22 @@
|
||||
let selectedWireId = null, selectedNodeId = null;
|
||||
|
||||
let panX = 0, panY = 0, zoom = 1;
|
||||
let isPanning = false, panStart = { x: 0, y: 0 };
|
||||
let isPanning = false, panStart = { x: 0, y: 0 }, isSystemBooted = false;
|
||||
|
||||
/* --- Setup Toolbox --- */
|
||||
/* --- Toolbox & Base Init --- */
|
||||
function initToolbox() {
|
||||
if(!toolboxGrid) return;
|
||||
let html = '';
|
||||
Object.keys(PC_PARTS).forEach(partKey => {
|
||||
html += `
|
||||
<div draggable="true" data-spawn="${partKey}" class="drag-item tb-icon-box" title="${PC_PARTS[partKey].name}">
|
||||
html += `<div draggable="true" data-spawn="${partKey}" class="drag-item tb-icon-box" title="${PC_PARTS[partKey].name}">
|
||||
<svg viewBox="0 0 ${PC_PARTS[partKey].w} ${PC_PARTS[partKey].h}" style="max-width:80%; max-height:40px; pointer-events:none;">${PC_PARTS[partKey].svg}</svg>
|
||||
<div class="tb-icon-label">${partKey}</div>
|
||||
</div>
|
||||
`;
|
||||
<div class="tb-icon-label">${partKey}</div></div>`;
|
||||
});
|
||||
toolboxGrid.innerHTML = html;
|
||||
document.querySelectorAll('.drag-item').forEach(item => {
|
||||
item.addEventListener('dragstart', (e) => { e.dataTransfer.setData('spawnType', item.dataset.spawn); });
|
||||
});
|
||||
document.querySelectorAll('.drag-item').forEach(item => { item.addEventListener('dragstart', (e) => { e.dataTransfer.setData('spawnType', item.dataset.spawn); }); });
|
||||
}
|
||||
|
||||
/* --- Camera Math --- */
|
||||
/* --- Viewport Math --- */
|
||||
function updateViewport() {
|
||||
viewport.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`;
|
||||
workspace.style.backgroundSize = `${32 * zoom}px ${32 * zoom}px`;
|
||||
@@ -94,63 +123,58 @@
|
||||
}
|
||||
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);
|
||||
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}`;
|
||||
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);
|
||||
const isSelected = conn.id === selectedWireId;
|
||||
svgHTML += `<path class="pb-wire active ${isSelected ? 'selected' : ''}" d="${drawBezier(from.x, from.y, to.x, to.y)}" data-conn-id="${conn.id}" />`;
|
||||
const from = getPortCoords(conn.fromNode, conn.fromPort); const to = getPortCoords(conn.toNode, conn.toPort);
|
||||
svgHTML += `<path class="pb-wire active ${conn.id === selectedWireId ? 'selected' : ''}" d="${drawBezier(from.x, from.y, to.x, to.y)}" data-conn-id="${conn.id}" />`;
|
||||
});
|
||||
if (wiringStart && tempWirePath) {
|
||||
svgHTML += `<path class="pb-wire pb-wire-temp" d="${drawBezier(wiringStart.x, wiringStart.y, tempWirePath.x, tempWirePath.y)}" />`;
|
||||
}
|
||||
if (wiringStart && tempWirePath) svgHTML += `<path class="pb-wire pb-wire-temp" d="${drawBezier(wiringStart.x, wiringStart.y, tempWirePath.x, tempWirePath.y)}" />`;
|
||||
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(); }
|
||||
|
||||
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();
|
||||
/* --- 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 = `<svg class="pb-part-svg" viewBox="0 0 ${PC_PARTS[node.type].w} ${PC_PARTS[node.type].h}">${PC_PARTS[node.type].svg}</svg>`;
|
||||
PC_PARTS[node.type].ports.forEach(p => { innerHTML += `<div class="pb-port" data-port="${p.id}" style="left: ${p.x}px; top: ${p.y}px;"></div>`; });
|
||||
el.innerHTML = innerHTML; viewport.appendChild(el); node.el = el; return el;
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedWireId = null; selectedNodeId = null;
|
||||
document.querySelectorAll('.pb-node.selected').forEach(el => el.classList.remove('selected'));
|
||||
renderWires();
|
||||
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;
|
||||
}
|
||||
|
||||
/* --- Seven-Segment Diagnostics Engine --- */
|
||||
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;
|
||||
let hasStorage = false, hasGPU = false;
|
||||
let mbPwr = false, gpuPwr = false;
|
||||
let usbCount = 0, dispConn = false, audConn = false;
|
||||
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');
|
||||
@@ -160,145 +184,92 @@
|
||||
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; // Motherboard exists outside case
|
||||
}
|
||||
} 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;
|
||||
if (mbNode.slots['M2_1'] || mbNode.slots['M2_2']) { hasStorage = true; storPwr = true; storData = true; }
|
||||
}
|
||||
|
||||
// Check Cables
|
||||
connections.forEach(c => {
|
||||
let n1 = nodes[c.fromNode], n2 = nodes[c.toNode];
|
||||
if(!n1 || !n2) return;
|
||||
let types = [n1.type, n2.type];
|
||||
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('MB') && ['KEYBOARD','MOUSE','WEBCAM','MIC','PRINTER'].some(t => types.includes(t))) usbCount++;
|
||||
if(types.includes('MB') && types.includes('SPEAKER')) audConn = 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 = `
|
||||
<div class="diag-cat">Core System</div>
|
||||
<div class="diag-cat">CORE SYSTEM</div>
|
||||
<div class="diag-row"><span>CHASSIS</span><span style="color: ${hasCase ? '#28f07a' : '#ff5555'}">${hasCase ? 'OK' : 'ERR'}</span></div>
|
||||
<div class="diag-row"><span>MOTHERBOARD</span><span style="color: ${hasMB ? '#28f07a' : '#ff5555'}">${hasMB ? 'OK' : 'ERR'}</span></div>
|
||||
<div class="diag-row"><span>CPU</span><span style="color: ${hasCPU ? '#28f07a' : '#ff5555'}">${hasCPU ? 'OK' : 'ERR'}</span></div>
|
||||
<div class="diag-row"><span>COOLING</span><span style="color: ${hasCooler ? '#28f07a' : '#ff5555'}">${hasCooler ? 'OK' : 'ERR'}</span></div>
|
||||
<div class="diag-row"><span>MEMORY</span><span style="color: ${hasRAM ? '#28f07a' : '#ff5555'}">${hasRAM ? 'OK' : 'ERR'}</span></div>
|
||||
<div class="diag-row"><span>POWER SPLY</span><span style="color: ${hasPSU ? '#28f07a' : '#ff5555'}">${hasPSU ? 'OK' : 'ERR'}</span></div>
|
||||
<div class="diag-cat">Connections</div>
|
||||
<div class="diag-cat">CONNECTIONS</div>
|
||||
<div class="diag-row"><span>MB POWER</span><span style="color: ${mbPwr ? '#28f07a' : '#ff5555'}">${mbPwr ? 'OK' : 'ERR'}</span></div>
|
||||
<div class="diag-row"><span>STORAGE</span><span style="color: ${hasStorage ? '#28f07a' : '#ff5555'}">${hasStorage ? 'OK' : 'ERR'}</span></div>
|
||||
<div class="diag-row"><span>STORAGE</span><span style="color: ${(hasStorage && storPwr && storData) ? '#28f07a' : '#ff5555'}">${(hasStorage && storPwr && storData) ? 'OK' : 'ERR'}</span></div>
|
||||
<div class="diag-row"><span>GPU POWER</span><span style="color: ${!hasGPU ? '#888' : (gpuPwr ? '#28f07a' : '#ff5555')}">${!hasGPU ? 'N/A' : (gpuPwr ? 'OK' : 'ERR')}</span></div>
|
||||
<div class="diag-row"><span>DISPLAY</span><span style="color: ${dispConn ? '#28f07a' : '#ff5555'}">${dispConn ? 'OK' : 'ERR'}</span></div>
|
||||
<div class="diag-row"><span>USB DEVS</span><span style="color: #55aaff">${usbCount}</span></div>
|
||||
<hr style="border-color: rgba(255,255,255,0.1); margin: 12px 0 8px 0;">
|
||||
<div style="text-align:center; font-size: 28px; color: ${isBootable ? '#28f07a' : '#ff5555'}; font-family: var(--bit-font); letter-spacing: 2px;">
|
||||
${isBootable ? 'BOOTING...' : 'HALTED'}
|
||||
</div>
|
||||
<div style="text-align:center; font-size: 28px; color: ${isBootable ? '#28f07a' : '#ff5555'}; font-family: var(--bit-font); letter-spacing: 2px;">${isBootable ? 'BOOTING...' : 'HALTED'}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/* --- Node Creation & Snapping --- */
|
||||
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 = `<svg class="pb-part-svg" viewBox="0 0 ${PC_PARTS[node.type].w} ${PC_PARTS[node.type].h}">${PC_PARTS[node.type].svg}</svg>`;
|
||||
PC_PARTS[node.type].ports.forEach(p => {
|
||||
innerHTML += `<div class="pb-port" data-port="${p.id}" style="left: ${p.x}px; top: ${p.y}px;"></div>`;
|
||||
});
|
||||
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);
|
||||
|
||||
// Debug Labels for bare parts
|
||||
if(node.type !== 'CASE' && node.type !== 'MB') {
|
||||
innerHTML += `<div style="position:absolute; top:-20px; font-family:var(--ui-font); font-size:12px; color:var(--muted);">${node.type}</div>`;
|
||||
}
|
||||
|
||||
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 }; // Copy slots schema, values will be filled with IDs
|
||||
bootContent.innerHTML = `<text x="120" y="70" fill="white" font-family="sans-serif" font-size="12" text-anchor="middle">Starting Windows</text><rect x="85" y="85" width="0" height="4" fill="#28f07a" rx="2"><animate attributeName="width" from="0" to="70" dur="${durSeconds}s" fill="freeze" /></rect>`;
|
||||
|
||||
// Reset slot values to null
|
||||
if(node.slots) {
|
||||
for(let k in node.slots) { node.slots[k] = null; }
|
||||
}
|
||||
|
||||
nodes[id] = node;
|
||||
createNodeElement(node);
|
||||
evaluateBuild();
|
||||
setTimeout(() => {
|
||||
bootContent.innerHTML = `<image href="/Microsoft_Nostalgic_Windows_Wallpaper_4k.jpg" x="10" y="10" width="220" height="120" preserveAspectRatio="xMidYMid slice" />`;
|
||||
}, duration + 300); // Small buffer to let the bar finish
|
||||
}
|
||||
|
||||
// Recursive movement to handle nested snaps (MB inside CASE inside ...)
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
function resetMonitor() { const monitor = Object.values(nodes).find(n => n.type === 'MONITOR'); if (monitor) monitor.el.querySelector('#boot-content').innerHTML = ''; }
|
||||
|
||||
/* --- Inspect Mode --- */
|
||||
let inspectZoom = 1, inspectRotX = 0, inspectRotY = 0;
|
||||
workspace.addEventListener('dblclick', (e) => {
|
||||
const nodeEl = e.target.closest('.pb-node');
|
||||
if (nodeEl) {
|
||||
const node = nodes[nodeEl.dataset.id];
|
||||
document.getElementById('inspectModal').classList.add('active');
|
||||
document.getElementById('inspectObject').innerHTML = `<svg viewBox="0 0 ${PC_PARTS[node.type].w} ${PC_PARTS[node.type].h}" style="width:100%; height:100%;">${PC_PARTS[node.type].svg}</svg>`;
|
||||
document.getElementById('inspectName').innerText = PC_PARTS[node.type].name;
|
||||
inspectZoom = 1.5; inspectRotX = 0; inspectRotY = 0; updateInspectTransform(); clearSelection();
|
||||
}
|
||||
});
|
||||
document.getElementById('inspectStage')?.addEventListener('mousemove', (e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
inspectRotY = (e.clientX - rect.left - rect.width/2) / 5;
|
||||
inspectRotX = -(e.clientY - rect.top - rect.height/2) / 5;
|
||||
updateInspectTransform();
|
||||
});
|
||||
document.getElementById('inspectStage')?.addEventListener('wheel', (e) => {
|
||||
e.preventDefault(); inspectZoom += e.deltaY < 0 ? 0.1 : -0.1;
|
||||
inspectZoom = Math.max(0.5, Math.min(inspectZoom, 4)); updateInspectTransform();
|
||||
});
|
||||
function updateInspectTransform() { const obj = document.getElementById('inspectObject'); if(obj) obj.style.transform = `scale(${inspectZoom}) rotateX(${inspectRotX}deg) rotateY(${inspectRotY}deg)`; }
|
||||
document.getElementById('inspectClose')?.addEventListener('click', () => { document.getElementById('inspectModal').classList.remove('active'); });
|
||||
|
||||
/* --- Interaction --- */
|
||||
/* --- 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 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);
|
||||
@@ -314,18 +285,14 @@
|
||||
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 };
|
||||
|
||||
// Unsnap from parent when picked up
|
||||
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; // Reset Z
|
||||
evaluateBuild();
|
||||
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 };
|
||||
});
|
||||
|
||||
@@ -334,20 +301,16 @@
|
||||
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();
|
||||
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;
|
||||
const node = nodes[isDraggingNode]; let snapped = false;
|
||||
|
||||
// Check all other nodes for compatible slots
|
||||
Object.values(nodes).forEach(target => {
|
||||
if (target.slots && !snapped && target.id !== node.id) {
|
||||
for(let slotKey in target.slots) {
|
||||
@@ -358,7 +321,7 @@
|
||||
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; // Layer above parent
|
||||
node.el.style.zIndex = PC_PARTS[target.type].z + 5;
|
||||
snapped = true; break;
|
||||
}
|
||||
}
|
||||
@@ -371,8 +334,7 @@
|
||||
if (wiringStart) {
|
||||
const port = e.target.closest('.pb-port');
|
||||
if (port) {
|
||||
const targetNodeId = port.closest('.pb-node').dataset.id;
|
||||
const targetPortId = port.dataset.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();
|
||||
@@ -380,7 +342,8 @@
|
||||
isPanning = false;
|
||||
});
|
||||
|
||||
/* --- Deletion (Recursive) --- */
|
||||
|
||||
/* --- 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]); }); }
|
||||
@@ -390,28 +353,17 @@
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
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));
|
||||
}
|
||||
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();
|
||||
});
|
||||
btnClearBoard?.addEventListener('click', () => { viewport.querySelectorAll('.pb-node').forEach(el => el.remove()); nodes = {}; connections = []; evaluateBuild(); renderWires(); });
|
||||
|
||||
toolboxToggle?.addEventListener("click", () => {
|
||||
const c = pcPage?.classList.contains("toolboxCollapsed");
|
||||
@@ -419,5 +371,29 @@
|
||||
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();
|
||||
})();
|
||||
Reference in New Issue
Block a user