// src/scripts/logicGates.js // Computing:Box — Drag & Drop Logic Builder (() => { /* --- DOM Elements --- */ const workspace = document.getElementById("workspace"); 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 (Strict 100x50 with built-in tails) --- */ 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 inputCount = 0; let outputCount = 0; let isDraggingNode = null; let dragOffset = { x: 0, y: 0 }; let clickStartX = 0, clickStartY = 0; // Fixes switch drag conflict let wiringStart = null; let tempWirePath = null; let selectedWireId = null; let selectedNodeId = null; /* --- 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); }); }); } /* --- Math & Geometry --- */ 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 + (portRect.width / 2), y: portRect.top - wsRect.top + (portRect.height / 2) }; } 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() { if (!ttContainer) return; 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 (inNodes.length === 0 || outNodes.length === 0) { ttContainer.innerHTML = '
Add inputs and outputs to generate table.
'; return; } if (inNodes.length > 6) { ttContainer.innerHTML = '
Maximum 6 inputs supported.
'; return; } let html = ''; inNodes.forEach(n => html += ``); outNodes.forEach(n => html += ``); html += ''; 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); html += ''; inNodes.forEach(n => { let val = override[n.id]; html += ``; }); outNodes.forEach(n => { let val = outStates[n.id]; html += ``; }); html += ''; } html += '
${n.label}${n.label}
${val ? 1 : 0}${val ? 1 : 0}
'; ttContainer.innerHTML = html; } function runSimulation() { evaluateGraph(); renderWires(); generateTruthTable(); } /* --- 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; workspace.appendChild(el); node.el = el; if (node.type === 'INPUT') { // Custom click handler to prevent dragging from toggling the switch el.querySelector('.switch').addEventListener('click', (e) => { const dist = Math.hypot(e.clientX - clickStartX, e.clientY - clickStartY); if (isDraggingNode || dist > 3) { e.preventDefault(); } 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 ? `` : ''; runSimulation(); } }); } return el; } function spawnNode(type, gateType = null, dropX = null, dropY = null) { let label = ''; if (type === 'INPUT') { inputCount++; label = String.fromCharCode(64 + inputCount); } if (type === 'OUTPUT') { outputCount++; label = `Q${outputCount}`; } if (type === 'GATE') { label = gateType; } const id = `node_${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); runSimulation(); } /* --- Global Interaction Handlers --- */ 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, y: e.clientY - rect.top }; return; } clearSelection(); }); window.addEventListener('mousemove', (e) => { const wsRect = workspace.getBoundingClientRect(); if (isDraggingNode) { const node = nodes[isDraggingNode]; let newX = e.clientX - wsRect.left - dragOffset.x; let newY = e.clientY - wsRect.top - dragOffset.y; node.x = Math.max(10, Math.min(newX, wsRect.width - 80)); node.y = Math.max(20, Math.min(newY, wsRect.height - 60)); updateNodePositions(); } if (wiringStart) { tempWirePath = { x: e.clientX - wsRect.left, y: e.clientY - wsRect.top }; renderWires(); } }); window.addEventListener('mouseup', (e) => { isDraggingNode = null; 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 }); } } wiringStart = null; tempWirePath = null; runSimulation(); } }); /* --- 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) { workspace.removeChild(nodes[selectedNodeId].el); } delete nodes[selectedNodeId]; clearSelection(); runSimulation(); } } }); /* --- 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 - 40; const y = e.clientY - wsRect.top - 30; spawnNode(spawnType, gateType || null, x, y); } }); /* --- Init --- */ btnClearBoard?.addEventListener('click', () => { workspace.querySelectorAll('.lg-node').forEach(el => el.remove()); nodes = {}; connections = []; inputCount = 0; outputCount = 0; runSimulation(); }); 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(); spawnNode('INPUT', null, 80, 150); spawnNode('INPUT', null, 80, 250); spawnNode('GATE', 'AND', 320, 200); spawnNode('OUTPUT', null, 600, 200); })();