// wiringPanel.js — Wiring panel for connecting gadget ports to module ports // The player wires a gadget's I/O to a module door's ports, then executes // to verify the circuit satisfies the module's logic. // After execution, shows an animated truth table + digital waveform viewer. import { worldState, solvePuzzle, isPuzzleSolved, startDialog } from './worldState.js'; import { showNotification } from './inventory.js'; // ==================== State ==================== let panelOpen = false; let moduleInter = null; // the full interaction object from the map let gadget = null; // the selected gadget from backpack let wires = []; // [{ moduleIdx, gadgetIdx }] let cursor = { side: 'module', index: 0 }; let selectedModule = null; // index or null (first port of a pending wire) let selectedGadget = null; let result = null; // { message, color } or null let resultTimer = 0; // Execution log state let execLog = null; // { rows, portNames, passed, startTime, revealedRows } const ROW_REVEAL_MS = 300; // ms between each row appearing // ==================== Public API ==================== export function isWiringOpen() { return panelOpen; } export function openWiringPanel(inter, gad) { panelOpen = true; moduleInter = inter; gadget = gad; wires = []; cursor = { side: 'module', index: 0 }; selectedModule = null; selectedGadget = null; result = null; execLog = null; worldState.mode = 'wiring'; console.log(`[wiring] opened for module "${inter.label}" with gadget "${gad.name}"`); } export function closeWiringPanel() { panelOpen = false; moduleInter = null; gadget = null; wires = []; execLog = null; worldState.mode = 'world'; } // ==================== Gadget port helpers ==================== function getGadgetPorts() { if (!gadget) return []; const ports = []; const inputIds = gadget.inputIds || []; const outputIds = gadget.outputIds || []; for (let i = 0; i < inputIds.length; i++) { ports.push({ name: `In ${i + 1}`, dir: 'in', gateId: inputIds[i] }); } for (let i = 0; i < outputIds.length; i++) { ports.push({ name: `Out ${i + 1}`, dir: 'out', gateId: outputIds[i] }); } return ports; } // ==================== Input handling ==================== export function handleWiringInput(key) { if (!panelOpen) return false; // If execution log is showing, ESC or Enter dismisses it if (execLog) { if (key === 'Escape' || key === 'Enter') { if (execLog.passed) { closeWiringPanel(); showNotification('Module unlocked!', '⚡', '#00ff88'); startDialog([ `⚡ "${gadget.name}" passed the verification!`, 'The module hums to life and the door unlocks.' ], 'System'); } else { execLog = null; // dismiss log, back to wiring } } return true; } const mPorts = moduleInter.ports || []; const gPorts = getGadgetPorts(); switch (key) { case 'ArrowUp': case 'w': case 'W': cursor.index = Math.max(0, cursor.index - 1); break; case 'ArrowDown': case 's': case 'S': { const max = cursor.side === 'module' ? mPorts.length - 1 : gPorts.length - 1; cursor.index = Math.min(Math.max(max, 0), cursor.index + 1); break; } case 'ArrowLeft': case 'ArrowRight': case 'a': case 'A': case 'd': case 'D': cursor.side = cursor.side === 'module' ? 'gadget' : 'module'; { const max = cursor.side === 'module' ? mPorts.length - 1 : gPorts.length - 1; cursor.index = Math.min(cursor.index, Math.max(max, 0)); } break; case 'e': case 'E': case ' ': selectPort(); break; case 'Enter': executeWiring(); break; case 'Backspace': case 'Delete': case 'x': case 'X': removeWireAtCursor(); break; case 'Escape': closeWiringPanel(); break; default: return false; } return true; } // ==================== Wire management ==================== function selectPort() { if (cursor.side === 'module') { selectedModule = cursor.index; if (selectedGadget !== null) tryCreateWire(); } else { selectedGadget = cursor.index; if (selectedModule !== null) tryCreateWire(); } } function tryCreateWire() { const mPorts = moduleInter.ports || []; const gPorts = getGadgetPorts(); const mPort = mPorts[selectedModule]; const gPort = gPorts[selectedGadget]; if (!mPort || !gPort) { selectedModule = null; selectedGadget = null; return; } const valid = (mPort.dir === 'out' && gPort.dir === 'in') || (mPort.dir === 'in' && gPort.dir === 'out'); if (!valid) { result = { message: '✗ Invalid! Wire out→in only', color: '#ff4444' }; resultTimer = Date.now(); selectedModule = null; selectedGadget = null; return; } wires = wires.filter(w => w.moduleIdx !== selectedModule && w.gadgetIdx !== selectedGadget); wires.push({ moduleIdx: selectedModule, gadgetIdx: selectedGadget }); result = { message: `✓ Wired ${mPort.name} ↔ ${gPort.name}`, color: '#00e599' }; resultTimer = Date.now(); selectedModule = null; selectedGadget = null; } function removeWireAtCursor() { const key = cursor.side === 'module' ? 'moduleIdx' : 'gadgetIdx'; const before = wires.length; wires = wires.filter(w => w[key] !== cursor.index); if (wires.length < before) { result = { message: 'Wire removed', color: '#ffaa00' }; resultTimer = Date.now(); } } // ==================== Circuit evaluation ==================== function evaluateGadgetCircuit(gates, connections, inputValues) { const evalGates = JSON.parse(JSON.stringify(gates)); for (const g of evalGates) { if (g.type === 'INPUT' && inputValues[g.id] !== undefined) { g.value = inputValues[g.id]; } } for (let iter = 0; iter < 20; iter++) { let changed = false; for (const g of evalGates) { if (g.type === 'INPUT' || g.type === 'CLOCK') continue; const inCount = (g.type === 'NOT' || g.type === 'OUTPUT') ? 1 : 2; const ins = []; for (let p = 0; p < inCount; p++) { const conn = connections.find(c => c.to === g.id && c.toPort === p); if (conn) { const src = evalGates.find(s => s.id === conn.from); ins.push(src ? (src.value || 0) : 0); } else { ins.push(0); } } let val = 0; switch (g.type) { case 'AND': val = (ins[0] && ins[1]) ? 1 : 0; break; case 'OR': val = (ins[0] || ins[1]) ? 1 : 0; break; case 'NOT': val = ins[0] ? 0 : 1; break; case 'NAND': val = (ins[0] && ins[1]) ? 0 : 1; break; case 'NOR': val = (ins[0] || ins[1]) ? 0 : 1; break; case 'XOR': val = (ins[0] !== ins[1]) ? 1 : 0; break; case 'OUTPUT': val = ins[0] || 0; break; default: val = g.value || 0; } if (val !== g.value) { g.value = val; changed = true; } } if (!changed) break; } return evalGates; } /** * Build the test function + run all combos to produce the truth table, * then pass it to the verify function. Store results for the execution log. */ function executeWiring() { const mPorts = moduleInter.ports || []; const gPorts = getGadgetPorts(); // Identify module out ports (inputs to the gadget) and in ports (outputs from gadget) const outPorts = mPorts.filter(p => p.dir === 'out'); const inPorts = mPorts.filter(p => p.dir === 'in'); const n = outPorts.length; // Collect truth table rows: for each input combo, run the circuit const rows = []; function testOnce(moduleOutputs) { const inputValues = {}; for (const wire of wires) { const mp = mPorts[wire.moduleIdx]; const gp = gPorts[wire.gadgetIdx]; if (mp.dir === 'out' && gp.dir === 'in') { inputValues[gp.gateId] = moduleOutputs[mp.name] || 0; } } const evaluated = evaluateGadgetCircuit(gadget.gates, gadget.connections, inputValues); const moduleInputs = {}; for (const wire of wires) { const mp = mPorts[wire.moduleIdx]; const gp = gPorts[wire.gadgetIdx]; if (mp.dir === 'in' && gp.dir === 'out') { const outGate = evaluated.find(g => g.id === gp.gateId); moduleInputs[mp.name] = outGate ? (outGate.value || 0) : 0; } } return moduleInputs; } // Generate all input combos and record results for (let combo = 0; combo < (1 << n); combo++) { const inputs = {}; for (let i = 0; i < n; i++) { inputs[outPorts[i].name] = (combo >> i) & 1; } const outputs = testOnce(inputs); rows.push({ inputs: { ...inputs }, outputs: { ...outputs } }); } // Build test function that the verify code calls (using our precomputed results) function test(moduleOutputs) { const inputValues = {}; for (const wire of wires) { const mp = mPorts[wire.moduleIdx]; const gp = gPorts[wire.gadgetIdx]; if (mp.dir === 'out' && gp.dir === 'in') { inputValues[gp.gateId] = moduleOutputs[mp.name] || 0; } } const evaluated = evaluateGadgetCircuit(gadget.gates, gadget.connections, inputValues); const moduleInputs = {}; for (const wire of wires) { const mp = mPorts[wire.moduleIdx]; const gp = gPorts[wire.gadgetIdx]; if (mp.dir === 'in' && gp.dir === 'out') { const outGate = evaluated.find(g => g.id === gp.gateId); moduleInputs[mp.name] = outGate ? (outGate.value || 0) : 0; } } return moduleInputs; } // Run verify try { const verifyFn = new Function('return ' + moduleInter.verify)(); const passed = verifyFn(test); // Build port name lists for display const portNames = { inputs: outPorts.map(p => p.name), outputs: inPorts.map(p => p.name) }; // Set up execution log with animated reveal execLog = { rows, portNames, passed, startTime: Date.now(), totalRows: rows.length }; if (passed && moduleInter.moduleId) { solvePuzzle(moduleInter.moduleId); } } catch (e) { result = { message: `Error: ${e.message}`, color: '#ff4444' }; resultTimer = Date.now(); console.error('[wiring] verify error:', e); } } // ==================== Rendering ==================== const PANEL_BG = 'rgba(10, 12, 20, 0.95)'; const PANEL_BORDER = '#00e599'; const PORT_OUT_COLOR = '#ff6644'; const PORT_IN_COLOR = '#44aaff'; const WIRE_COLOR = '#ffdd44'; const SELECTED_COLOR = '#ffffff'; const CURSOR_COLOR = '#00ffcc'; // Waveform/log colors const WAVE_HIGH = '#00e599'; const WAVE_LOW = '#334'; const WAVE_GRID = '#1a1d2e'; const TABLE_HEADER_BG = '#141828'; const TABLE_ROW_BG = '#0d1018'; const TABLE_ROW_ALT = '#111520'; const PASS_COLOR = '#00ff88'; const FAIL_COLOR = '#ff4444'; export function drawWiringPanel(ctx, canvasW, canvasH) { if (!panelOpen || !moduleInter || !gadget) return; // If execution log is active, draw that instead if (execLog) { drawExecutionLog(ctx, canvasW, canvasH); return; } const mPorts = moduleInter.ports || []; const gPorts = getGadgetPorts(); // Panel dimensions const pw = Math.min(640, canvasW - 40); const ph = Math.min(480, canvasH - 40); const px = (canvasW - pw) / 2; const py = (canvasH - ph) / 2; // Dim background ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.fillRect(0, 0, canvasW, canvasH); // Panel background ctx.fillStyle = PANEL_BG; ctx.strokeStyle = PANEL_BORDER; ctx.lineWidth = 2; roundRect(ctx, px, py, pw, ph, 8); ctx.fill(); ctx.stroke(); // Title ctx.fillStyle = '#00e599'; ctx.font = 'bold 16px "Segoe UI", system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.fillText(`⚡ WIRING PANEL — ${moduleInter.label || 'Module'}`, px + pw / 2, py + 12); // Gadget name ctx.fillStyle = '#ff44aa'; ctx.font = '12px "Segoe UI", system-ui, sans-serif'; ctx.fillText(`Gadget: ${gadget.icon || '🔧'} ${gadget.name}`, px + pw / 2, py + 34); // Column headers const colY = py + 60; const leftX = px + 30; const rightX = px + pw - 30; const portStartY = colY + 30; const portSpacing = 40; ctx.font = 'bold 13px "Segoe UI", system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.fillStyle = '#aaa'; ctx.fillText('MODULE PORTS', leftX + 80, colY); ctx.fillText('GADGET PORTS', rightX - 80, colY); // Column separator ctx.strokeStyle = '#333'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(px + pw / 2, colY + 15); ctx.lineTo(px + pw / 2, py + ph - 50); ctx.stroke(); const modulePortPositions = []; const gadgetPortPositions = []; // Draw module ports for (let i = 0; i < mPorts.length; i++) { const port = mPorts[i]; const yPos = portStartY + i * portSpacing; const isOut = port.dir === 'out'; const dotX = leftX; const wireX = leftX + 170; modulePortPositions.push({ x: wireX, y: yPos + 6 }); const isCursor = cursor.side === 'module' && cursor.index === i; const isSelected = selectedModule === i; if (isCursor) { ctx.fillStyle = 'rgba(0, 255, 200, 0.1)'; ctx.fillRect(leftX - 10, yPos - 6, 190, portSpacing - 8); } ctx.beginPath(); ctx.arc(dotX, yPos + 6, 8, 0, Math.PI * 2); ctx.fillStyle = isSelected ? SELECTED_COLOR : (isOut ? PORT_OUT_COLOR : PORT_IN_COLOR); ctx.fill(); if (isCursor) { ctx.strokeStyle = CURSOR_COLOR; ctx.lineWidth = 2; ctx.stroke(); } ctx.fillStyle = '#000'; ctx.font = 'bold 10px monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(isOut ? '→' : '←', dotX, yPos + 6); ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.font = '13px "Segoe UI", system-ui, sans-serif'; ctx.fillStyle = isCursor ? CURSOR_COLOR : '#ddd'; ctx.fillText(port.name, dotX + 14, yPos); ctx.font = '10px monospace'; ctx.fillStyle = isOut ? PORT_OUT_COLOR : PORT_IN_COLOR; ctx.fillText(isOut ? 'OUT' : 'IN', dotX + 14, yPos + 16); if (wires.find(w => w.moduleIdx === i)) { ctx.fillStyle = WIRE_COLOR; ctx.fillText('⚡', dotX + 50, yPos + 16); } } // Draw gadget ports for (let i = 0; i < gPorts.length; i++) { const port = gPorts[i]; const yPos = portStartY + i * portSpacing; const isOut = port.dir === 'out'; const dotX = rightX; const wireX = rightX - 170; gadgetPortPositions.push({ x: wireX, y: yPos + 6 }); const isCursor = cursor.side === 'gadget' && cursor.index === i; const isSelected = selectedGadget === i; if (isCursor) { ctx.fillStyle = 'rgba(0, 255, 200, 0.1)'; ctx.fillRect(rightX - 180, yPos - 6, 190, portSpacing - 8); } ctx.beginPath(); ctx.arc(dotX, yPos + 6, 8, 0, Math.PI * 2); ctx.fillStyle = isSelected ? SELECTED_COLOR : (isOut ? PORT_OUT_COLOR : PORT_IN_COLOR); ctx.fill(); if (isCursor) { ctx.strokeStyle = CURSOR_COLOR; ctx.lineWidth = 2; ctx.stroke(); } ctx.fillStyle = '#000'; ctx.font = 'bold 10px monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(isOut ? '→' : '←', dotX, yPos + 6); ctx.textAlign = 'right'; ctx.textBaseline = 'top'; ctx.font = '13px "Segoe UI", system-ui, sans-serif'; ctx.fillStyle = isCursor ? CURSOR_COLOR : '#ddd'; ctx.fillText(port.name, dotX - 14, yPos); ctx.font = '10px monospace'; ctx.fillStyle = isOut ? PORT_OUT_COLOR : PORT_IN_COLOR; ctx.fillText(isOut ? 'OUT' : 'IN', dotX - 14, yPos + 16); if (wires.find(w => w.gadgetIdx === i)) { ctx.fillStyle = WIRE_COLOR; ctx.fillText('⚡', dotX - 60, yPos + 16); } } // Draw wires (bezier) ctx.strokeStyle = WIRE_COLOR; ctx.lineWidth = 2; ctx.setLineDash([6, 4]); for (const wire of wires) { const mp = modulePortPositions[wire.moduleIdx]; const gp = gadgetPortPositions[wire.gadgetIdx]; if (mp && gp) { ctx.beginPath(); ctx.moveTo(mp.x, mp.y); const midX = (mp.x + gp.x) / 2; ctx.bezierCurveTo(midX, mp.y, midX, gp.y, gp.x, gp.y); ctx.stroke(); } } ctx.setLineDash([]); // Pending wire indicator if (selectedModule !== null && selectedGadget === null) { const mp = modulePortPositions[selectedModule]; if (mp) { ctx.beginPath(); ctx.arc(mp.x, mp.y, 5, 0, Math.PI * 2); ctx.fillStyle = SELECTED_COLOR; ctx.fill(); } } if (selectedGadget !== null && selectedModule === null) { const gp = gadgetPortPositions[selectedGadget]; if (gp) { ctx.beginPath(); ctx.arc(gp.x, gp.y, 5, 0, Math.PI * 2); ctx.fillStyle = SELECTED_COLOR; ctx.fill(); } } // Result message (transient) if (result && (Date.now() - resultTimer < 3000)) { ctx.fillStyle = result.color; ctx.font = 'bold 14px "Segoe UI", system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; ctx.fillText(result.message, px + pw / 2, py + ph - 35); } // Controls hint ctx.fillStyle = '#555'; ctx.font = '11px "Segoe UI", system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; ctx.fillText('↑↓←→: Navigate | E: Wire | X: Remove | Enter: Execute | ESC: Close', px + pw / 2, py + ph - 10); } // ==================== Execution Log ==================== function drawExecutionLog(ctx, canvasW, canvasH) { const { rows, portNames, passed, startTime, totalRows } = execLog; const elapsed = Date.now() - startTime; const revealedRows = Math.min(totalRows, Math.floor(elapsed / ROW_REVEAL_MS)); const allRevealed = revealedRows >= totalRows; // Show verdict after all rows + a small delay const showVerdict = allRevealed && (elapsed > totalRows * ROW_REVEAL_MS + 400); const allPortNames = [...portNames.inputs, ...portNames.outputs]; const numInputs = portNames.inputs.length; const numOutputs = portNames.outputs.length; const numCols = allPortNames.length; // Panel sizing — full-width execution log const pw = Math.min(720, canvasW - 40); const tableH = 28 * (totalRows + 1) + 8; // header + rows const waveH = 40 * numCols + 20; // waveform area const verdictH = showVerdict ? 60 : 0; const ph = Math.min(canvasH - 40, 70 + tableH + 20 + waveH + verdictH + 50); const px = (canvasW - pw) / 2; const py = (canvasH - ph) / 2; // Dim background ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; ctx.fillRect(0, 0, canvasW, canvasH); // Panel ctx.fillStyle = PANEL_BG; ctx.strokeStyle = passed && showVerdict ? PASS_COLOR : PANEL_BORDER; ctx.lineWidth = 2; roundRect(ctx, px, py, pw, ph, 8); ctx.fill(); ctx.stroke(); // Glow effect if passed if (passed && showVerdict) { const glowIntensity = 0.15 + 0.1 * Math.sin(Date.now() / 300); ctx.shadowColor = PASS_COLOR; ctx.shadowBlur = 20; ctx.strokeStyle = PASS_COLOR; ctx.lineWidth = 2; roundRect(ctx, px, py, pw, ph, 8); ctx.stroke(); ctx.shadowBlur = 0; } // Title ctx.fillStyle = '#00e599'; ctx.font = 'bold 15px "Segoe UI", system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.fillText('⚡ EXECUTION LOG', px + pw / 2, py + 12); // Subtitle — scanning animation const dots = '.'.repeat((Math.floor(elapsed / 400) % 4)); const subtitle = allRevealed ? `All ${totalRows} test cases evaluated` : `Running test cases${dots}`; ctx.fillStyle = '#888'; ctx.font = '11px "Segoe UI", system-ui, sans-serif'; ctx.fillText(subtitle, px + pw / 2, py + 32); let curY = py + 52; // ==================== Truth Table ==================== const colW = Math.min(70, (pw - 60) / numCols); const tableX = px + (pw - colW * numCols) / 2; const rowH = 28; // Header background ctx.fillStyle = TABLE_HEADER_BG; roundRect(ctx, tableX - 8, curY, colW * numCols + 16, rowH, 4); ctx.fill(); // Header labels ctx.font = 'bold 12px "Cascadia Code", "Fira Code", monospace'; ctx.textBaseline = 'middle'; for (let c = 0; c < numCols; c++) { const cx = tableX + c * colW + colW / 2; const isInput = c < numInputs; ctx.fillStyle = isInput ? PORT_OUT_COLOR : PORT_IN_COLOR; ctx.textAlign = 'center'; ctx.fillText(allPortNames[c], cx, curY + rowH / 2); } // Separator line under header curY += rowH; ctx.strokeStyle = '#333'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(tableX - 4, curY); ctx.lineTo(tableX + colW * numCols + 4, curY); ctx.stroke(); // Vertical separator between inputs and outputs if (numInputs > 0 && numOutputs > 0) { const sepX = tableX + numInputs * colW; ctx.strokeStyle = '#444'; ctx.lineWidth = 1; ctx.setLineDash([3, 3]); ctx.beginPath(); ctx.moveTo(sepX, py + 52); ctx.lineTo(sepX, curY + rowH * Math.min(revealedRows, totalRows) + 4); ctx.stroke(); ctx.setLineDash([]); } // Data rows (animated reveal) for (let r = 0; r < revealedRows; r++) { const row = rows[r]; const rowY = curY + r * rowH; const rowAge = elapsed - r * ROW_REVEAL_MS; const alpha = Math.min(1, rowAge / 200); // fade in // Row background ctx.fillStyle = r % 2 === 0 ? TABLE_ROW_BG : TABLE_ROW_ALT; ctx.globalAlpha = alpha; ctx.fillRect(tableX - 8, rowY, colW * numCols + 16, rowH); // Flash effect on new row if (rowAge < 250) { const flash = 1 - rowAge / 250; ctx.fillStyle = `rgba(0, 229, 153, ${flash * 0.15})`; ctx.fillRect(tableX - 8, rowY, colW * numCols + 16, rowH); } // Values ctx.font = '13px "Cascadia Code", "Fira Code", monospace'; ctx.textBaseline = 'middle'; for (let c = 0; c < numCols; c++) { const cx = tableX + c * colW + colW / 2; const isInput = c < numInputs; const portName = allPortNames[c]; const val = isInput ? row.inputs[portName] : row.outputs[portName]; // Color: high = bright, low = dim ctx.fillStyle = val ? WAVE_HIGH : '#555'; ctx.textAlign = 'center'; ctx.fillText(val !== undefined ? String(val) : '?', cx, rowY + rowH / 2); } ctx.globalAlpha = 1; } curY += totalRows * rowH + 12; // ==================== Waveform Viewer ==================== if (revealedRows > 0) { const waveX = tableX; const waveW = colW * numCols + 16; const sigH = 28; const sigSpacing = 38; const labelW = 36; const sigAreaW = waveW - labelW - 8; // Section header ctx.fillStyle = '#666'; ctx.font = 'bold 10px "Segoe UI", system-ui, sans-serif'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillText('SIGNAL VIEW', waveX, curY); curY += 16; // Background ctx.fillStyle = '#0a0c14'; roundRect(ctx, waveX - 8, curY - 4, waveW, sigSpacing * numCols + 12, 4); ctx.fill(); // Grid lines (vertical, one per test case) ctx.strokeStyle = WAVE_GRID; ctx.lineWidth = 1; const stepW = sigAreaW / Math.max(totalRows, 1); for (let i = 0; i <= totalRows; i++) { const gx = waveX + labelW + i * stepW; ctx.beginPath(); ctx.moveTo(gx, curY); ctx.lineTo(gx, curY + sigSpacing * numCols); ctx.stroke(); } // Draw each signal for (let s = 0; s < numCols; s++) { const sigY = curY + s * sigSpacing; const isInput = s < numInputs; const portName = allPortNames[s]; const color = isInput ? PORT_OUT_COLOR : PORT_IN_COLOR; // Label ctx.fillStyle = color; ctx.font = 'bold 11px "Cascadia Code", "Fira Code", monospace'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(portName, waveX, sigY + sigH / 2); // Draw square wave ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.beginPath(); let prevVal = null; for (let r = 0; r < revealedRows; r++) { const row = rows[r]; const val = isInput ? (row.inputs[portName] || 0) : (row.outputs[portName] || 0); const x1 = waveX + labelW + r * stepW; const x2 = x1 + stepW; const yHigh = sigY + 3; const yLow = sigY + sigH - 3; const yVal = val ? yHigh : yLow; if (r === 0) { ctx.moveTo(x1, yVal); } else if (prevVal !== val) { // Transition — vertical edge ctx.lineTo(x1, yVal); } ctx.lineTo(x2, yVal); // Fill area under high signals if (val) { ctx.save(); ctx.fillStyle = color.replace(')', ', 0.08)').replace('rgb', 'rgba'); if (color.startsWith('#')) { const r2 = parseInt(color.slice(1,3), 16); const g = parseInt(color.slice(3,5), 16); const b = parseInt(color.slice(5,7), 16); ctx.fillStyle = `rgba(${r2},${g},${b},0.1)`; } ctx.fillRect(x1, yHigh, stepW, yLow - yHigh); ctx.restore(); } prevVal = val; } ctx.stroke(); // Scanline animation (moving cursor) if (!allRevealed) { const scanX = waveX + labelW + revealedRows * stepW; ctx.strokeStyle = '#00e599'; ctx.lineWidth = 1; ctx.setLineDash([2, 2]); ctx.beginPath(); ctx.moveTo(scanX, sigY); ctx.lineTo(scanX, sigY + sigH); ctx.stroke(); ctx.setLineDash([]); } } curY += sigSpacing * numCols + 16; } // ==================== Verdict ==================== if (showVerdict) { const verdictY = curY; const pulse = 0.8 + 0.2 * Math.sin(Date.now() / 200); if (passed) { ctx.fillStyle = PASS_COLOR; ctx.font = `bold ${18 * pulse}px "Segoe UI", system-ui, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.fillText('⚡ VERIFICATION PASSED ⚡', px + pw / 2, verdictY); ctx.fillStyle = '#aaa'; ctx.font = '12px "Segoe UI", system-ui, sans-serif'; ctx.fillText('Press Enter to unlock', px + pw / 2, verdictY + 26); } else { ctx.fillStyle = FAIL_COLOR; ctx.font = 'bold 18px "Segoe UI", system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.fillText('✗ VERIFICATION FAILED', px + pw / 2, verdictY); ctx.fillStyle = '#888'; ctx.font = '12px "Segoe UI", system-ui, sans-serif'; ctx.fillText('Press Esc to go back and adjust your wiring', px + pw / 2, verdictY + 26); } } // Footer hint ctx.fillStyle = '#444'; ctx.font = '10px "Segoe UI", system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; const hint = showVerdict ? (passed ? 'Enter: Continue | Esc: Close' : 'Esc: Back to wiring') : 'Evaluating...'; ctx.fillText(hint, px + pw / 2, py + ph - 8); } // ==================== Helpers ==================== function roundRect(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); }