// src/scripts/logicGates.js // Computing:Box — Drag & Drop Logic Builder (() => { /* --- DOM Elements --- */ const workspace = document.getElementById("workspace"); const viewport = document.getElementById("viewport"); const wireLayer = document.getElementById("wireLayer"); const ttContainer = document.getElementById("truthTableContainer"); const toolboxGrid = document.getElementById("toolboxGrid"); const btnClearBoard = document.getElementById("btnClearBoard"); const toolboxToggle = document.getElementById("toolboxToggle"); const logicPage = document.getElementById("logicPage"); /* --- ANSI Gate SVGs --- */ const GATE_SVGS = { 'AND': ``, 'OR': ``, 'NOT': ``, 'NAND': ``, 'NOR': ``, 'XOR': ``, 'XNOR': `` }; const INPUT_SVG = ``; const OUTPUT_SVG = ``; /* --- State --- */ let nodes = {}; let connections = []; let nextNodeId = 1; let nextWireId = 1; let discoveredStates = new Set(); // Interaction State let isDraggingNode = null; let dragOffset = { x: 0, y: 0 }; let clickStartX = 0, clickStartY = 0; let wiringStart = null; let tempWirePath = null; let selectedWireId = null; let selectedNodeId = null; // Camera State (Pan & Zoom) let panX = 0, panY = 0, zoom = 1; let isPanning = false; let panStart = { x: 0, y: 0 }; /* --- Setup Toolbox --- */ function initToolbox() { if(!toolboxGrid) return; let html = `
Input
Output
`; Object.keys(GATE_SVGS).forEach(gate => { html += `
${GATE_SVGS[gate]}
${gate}
`; }); toolboxGrid.innerHTML = html; document.querySelectorAll('.drag-item').forEach(item => { item.addEventListener('dragstart', (e) => { e.dataTransfer.setData('spawnType', item.dataset.spawn); if(item.dataset.spawn === 'GATE') e.dataTransfer.setData('gateType', item.dataset.gate); }); }); } /* --- Camera Math --- */ function updateViewport() { viewport.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`; workspace.style.backgroundSize = `${24 * zoom}px ${24 * zoom}px`; workspace.style.backgroundPosition = `${panX}px ${panY}px`; } function zoomWorkspace(factor, mouseX, mouseY) { const newZoom = Math.min(Math.max(0.2, zoom * factor), 3); 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(); // Calculate backwards through camera scale/pan to find true local coordinates 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, 'out'); const to = getPortCoords(conn.toNode, `in${conn.toPort}`); const sourceNode = nodes[conn.fromNode]; const isActive = sourceNode && sourceNode.value === true; const isSelected = conn.id === selectedWireId; 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('.lg-node.selected').forEach(el => el.classList.remove('selected')); renderWires(); } /* --- Logic Evaluation --- */ function evaluateGraph(overrideInputs = null) { let context = {}; Object.values(nodes).filter(n => n.type === 'INPUT').forEach(n => { context[n.id] = overrideInputs ? overrideInputs[n.id] : n.value; }); let changed = true; let loops = 0; while (changed && loops < 10) { changed = false; loops++; Object.values(nodes).filter(n => n.type === 'GATE').forEach(gate => { let in1Conn = connections.find(c => c.toNode === gate.id && c.toPort === '1'); let in2Conn = connections.find(c => c.toNode === gate.id && c.toPort === '2'); let val1 = in1Conn ? (context[in1Conn.fromNode] || false) : false; let val2 = in2Conn ? (context[in2Conn.fromNode] || false) : false; let res = false; switch(gate.gateType) { case 'AND': res = val1 && val2; break; case 'OR': res = val1 || val2; break; case 'NOT': res = !val1; break; case 'NAND': res = !(val1 && val2); break; case 'NOR': res = !(val1 || val2); break; case 'XOR': res = val1 !== val2; break; case 'XNOR': res = val1 === val2; break; } if (context[gate.id] !== res) { context[gate.id] = res; changed = true; } }); } let outStates = {}; Object.values(nodes).filter(n => n.type === 'OUTPUT').forEach(out => { let conn = connections.find(c => c.toNode === out.id); let res = conn ? (context[conn.fromNode] || false) : false; outStates[out.id] = res; }); if (!overrideInputs) { Object.values(nodes).forEach(n => { if (n.type === 'GATE') n.value = context[n.id] || false; if (n.type === 'OUTPUT') { n.value = outStates[n.id] || false; const bulb = n.el.querySelector('.bulb'); if (bulb) bulb.classList.toggle('on', n.value); } }); } return outStates; } /* --- Truth Table Generation --- */ 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; } } 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) { container.innerHTML = '
CONNECT INPUTS & OUTPUTS
'; return; } // 4. Build Table within the styled wrapper let html = '
'; // Headers inNodes.forEach(n => html += ``); outNodes.forEach(n => html += ``); html += ''; // 5. Generate Rows const numRows = Math.pow(2, inNodes.length); for (let i = 0; i < numRows; i++) { let 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 += ''; // Input Cells inNodes.forEach(n => { let v = override[n.id]; html += ``; }); // Output Cells (Discovery Logic) outNodes.forEach(n => { if (isFound) { let v = outResults[n.id]; html += ``; } else { html += ``; } }); html += ''; } html += '
${n.label}${n.label}
${v ? 1 : 0}${v ? 1 : 0}?
'; container.innerHTML = html; } 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(); } /* --- Smart Label Generation --- */ function getNextInputLabel() { let charCode = 65; while (Object.values(nodes).some(n => n.type === 'INPUT' && n.label === String.fromCharCode(charCode))) { charCode++; } return String.fromCharCode(charCode); } function getNextOutputLabel() { let idx = 1; while (Object.values(nodes).some(n => n.type === 'OUTPUT' && n.label === ('Q' + idx))) { idx++; } return 'Q' + idx; } /* --- Node Creation --- */ function createNodeElement(node) { const el = document.createElement('div'); el.className = `lg-node`; el.dataset.id = node.id; el.style.left = `${node.x}px`; el.style.top = `${node.y}px`; let innerHTML = `
${node.label}
`; if (node.type === 'INPUT') { innerHTML += `
${INPUT_SVG}
`; } else if (node.type === 'OUTPUT') { innerHTML += `
${OUTPUT_SVG}
`; } else if (node.type === 'GATE') { const isNot = node.gateType === 'NOT'; innerHTML += `
${!isNot ? `
` : ''} ${GATE_SVGS[node.gateType]}
`; } innerHTML += `
`; el.innerHTML = innerHTML; viewport.appendChild(el); node.el = el; 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; } function spawnNode(type, gateType = null, dropX = null, dropY = null) { let label = ''; if (type === 'INPUT') label = getNextInputLabel(); if (type === 'OUTPUT') label = getNextOutputLabel(); if (type === 'GATE') label = gateType; // 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; const node = { id, type, gateType, label, x, y, value: false, el: null }; nodes[id] = node; createNodeElement(node); // Change the very last line to: runSimulation(true); } /* --- Global Interaction Handlers --- */ // Camera Zoom Controls 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(); const factor = e.deltaY < 0 ? 1.1 : (1/1.1); zoomWorkspace(factor, e.clientX - wsRect.left, e.clientY - wsRect.top); }); workspace.addEventListener('mousedown', (e) => { clickStartX = e.clientX; clickStartY = e.clientY; const port = e.target.closest('.lg-port'); if (port) { const nodeEl = port.closest('.lg-node'); const portId = port.dataset.port; if (portId.startsWith('in')) { const existingIdx = connections.findIndex(c => c.toNode === nodeEl.dataset.id && c.toPort === portId.replace('in', '')); if (existingIdx !== -1) { connections.splice(existingIdx, 1); runSimulation(); return; } } if (portId === 'out') { const coords = getPortCoords(nodeEl.dataset.id, 'out'); 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('.lg-wire'); if (wire && wire.dataset.connId) { clearSelection(); selectedWireId = wire.dataset.connId; renderWires(); e.stopPropagation(); return; } const nodeEl = e.target.closest('.lg-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 }; return; } // Clicked empty space -> Pan Camera 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; node.x = newX; node.y = newY; updateNodePositions(); } if (wiringStart) { tempWirePath = { x: (e.clientX - wsRect.left - panX) / zoom, y: (e.clientY - wsRect.top - panY) / zoom }; renderWires(); } }); window.addEventListener('mouseup', (e) => { isDraggingNode = null; isPanning = false; if (wiringStart) { const port = e.target.closest('.lg-port'); if (port && port.dataset.port.startsWith('in')) { const targetNodeId = port.closest('.lg-node').dataset.id; const targetPortId = port.dataset.port.replace('in', ''); if (targetNodeId !== wiringStart.node) { connections = connections.filter(c => !(c.toNode === targetNodeId && c.toPort === targetPortId)); 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(true); } }); /* --- Deletion --- */ window.addEventListener('keydown', (e) => { if (e.key === 'Delete' || e.key === 'Backspace') { if (selectedWireId) { connections = connections.filter(c => c.id !== selectedWireId); clearSelection(); runSimulation(); } else if (selectedNodeId) { connections = connections.filter(c => c.fromNode !== selectedNodeId && c.toNode !== selectedNodeId); if (nodes[selectedNodeId] && nodes[selectedNodeId].el) { viewport.removeChild(nodes[selectedNodeId].el); } delete nodes[selectedNodeId]; // Change the two deletion triggers to: clearSelection(); runSimulation(true); } } }); /* --- Drag and Drop --- */ workspace.addEventListener('dragover', (e) => { e.preventDefault(); }); workspace.addEventListener('drop', (e) => { e.preventDefault(); const spawnType = e.dataTransfer.getData('spawnType'); if (spawnType) { const gateType = e.dataTransfer.getData('gateType'); const wsRect = workspace.getBoundingClientRect(); const x = (e.clientX - wsRect.left - panX) / zoom - 40; const y = (e.clientY - wsRect.top - panY) / zoom - 30; spawnNode(spawnType, gateType || null, x, y); } }); /* --- Init --- */ btnClearBoard?.addEventListener('click', () => { viewport.querySelectorAll('.lg-node').forEach(el => el.remove()); // 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", () => { const isCollapsed = logicPage?.classList.contains("toolboxCollapsed"); logicPage.classList.toggle("toolboxCollapsed", !isCollapsed); toolboxToggle?.setAttribute("aria-expanded", isCollapsed ? "true" : "false"); setTimeout(renderWires, 450); }); initToolbox(); })();