// 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}
`;
});
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 += `
${n.label}
`);
outNodes.forEach(n => html += `
${n.label}
`);
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 += `
${v ? 1 : 0}
`;
});
// Output Cells (Discovery Logic)
outNodes.forEach(n => {
if (isFound) {
let v = outResults[n.id];
html += `
${v ? 1 : 0}
`;
} else {
html += `
?
`;
}
});
html += '
';
}
html += '
';
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 = `