// 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 = `
`;
Object.keys(GATE_SVGS).forEach(gate => {
html += `
${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 += `| ${n.label} | `);
outNodes.forEach(n => html += `${n.label} | `);
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 += `| ${val ? 1 : 0} | `;
});
outNodes.forEach(n => {
let val = outStates[n.id];
html += `${val ? 1 : 0} | `;
});
html += '
';
}
html += '
';
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 = ``;
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 ? `
` : ''}
`;
}
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);
})();