feat: animated execution log with truth table + waveform viewer
When the player executes wiring, the panel transitions to a full execution log showing: - Animated truth table with rows revealing one by one (flash effect) - Digital waveform viewer with square wave signals for each port - Scanning cursor animation during evaluation - Pulsing glow verdict (pass/fail) after all test cases complete - Color-coded columns: orange for inputs, blue for outputs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
// wiringPanel.js — Wiring panel for connecting gadget ports to module ports
|
// 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
|
// 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.
|
// 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 { worldState, solvePuzzle, isPuzzleSolved, startDialog } from './worldState.js';
|
||||||
import { showNotification } from './inventory.js';
|
import { showNotification } from './inventory.js';
|
||||||
@@ -17,15 +18,14 @@ let selectedGadget = null;
|
|||||||
let result = null; // { message, color } or null
|
let result = null; // { message, color } or null
|
||||||
let resultTimer = 0;
|
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 ====================
|
// ==================== Public API ====================
|
||||||
|
|
||||||
export function isWiringOpen() { return panelOpen; }
|
export function isWiringOpen() { return panelOpen; }
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the wiring panel for a module interaction with a chosen gadget.
|
|
||||||
* @param {Object} inter - The module interaction from the map
|
|
||||||
* @param {Object} gad - The gadget from inventory
|
|
||||||
*/
|
|
||||||
export function openWiringPanel(inter, gad) {
|
export function openWiringPanel(inter, gad) {
|
||||||
panelOpen = true;
|
panelOpen = true;
|
||||||
moduleInter = inter;
|
moduleInter = inter;
|
||||||
@@ -35,6 +35,7 @@ export function openWiringPanel(inter, gad) {
|
|||||||
selectedModule = null;
|
selectedModule = null;
|
||||||
selectedGadget = null;
|
selectedGadget = null;
|
||||||
result = null;
|
result = null;
|
||||||
|
execLog = null;
|
||||||
worldState.mode = 'wiring';
|
worldState.mode = 'wiring';
|
||||||
console.log(`[wiring] opened for module "${inter.label}" with gadget "${gad.name}"`);
|
console.log(`[wiring] opened for module "${inter.label}" with gadget "${gad.name}"`);
|
||||||
}
|
}
|
||||||
@@ -44,12 +45,12 @@ export function closeWiringPanel() {
|
|||||||
moduleInter = null;
|
moduleInter = null;
|
||||||
gadget = null;
|
gadget = null;
|
||||||
wires = [];
|
wires = [];
|
||||||
|
execLog = null;
|
||||||
worldState.mode = 'world';
|
worldState.mode = 'world';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Gadget port helpers ====================
|
// ==================== Gadget port helpers ====================
|
||||||
|
|
||||||
/** Build a flat list of gadget ports (inputs first, then outputs) */
|
|
||||||
function getGadgetPorts() {
|
function getGadgetPorts() {
|
||||||
if (!gadget) return [];
|
if (!gadget) return [];
|
||||||
const ports = [];
|
const ports = [];
|
||||||
@@ -69,46 +70,47 @@ function getGadgetPorts() {
|
|||||||
export function handleWiringInput(key) {
|
export function handleWiringInput(key) {
|
||||||
if (!panelOpen) return false;
|
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 mPorts = moduleInter.ports || [];
|
||||||
const gPorts = getGadgetPorts();
|
const gPorts = getGadgetPorts();
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'ArrowUp':
|
case 'ArrowUp': case 'w': case 'W':
|
||||||
case 'w':
|
|
||||||
case 'W':
|
|
||||||
cursor.index = Math.max(0, cursor.index - 1);
|
cursor.index = Math.max(0, cursor.index - 1);
|
||||||
break;
|
break;
|
||||||
case 'ArrowDown':
|
case 'ArrowDown': case 's': case 'S': {
|
||||||
case 's':
|
|
||||||
case 'S': {
|
|
||||||
const max = cursor.side === 'module' ? mPorts.length - 1 : gPorts.length - 1;
|
const max = cursor.side === 'module' ? mPorts.length - 1 : gPorts.length - 1;
|
||||||
cursor.index = Math.min(Math.max(max, 0), cursor.index + 1);
|
cursor.index = Math.min(Math.max(max, 0), cursor.index + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft': case 'ArrowRight': case 'a': case 'A': case 'd': case 'D':
|
||||||
case 'ArrowRight':
|
|
||||||
case 'a':
|
|
||||||
case 'A':
|
|
||||||
case 'd':
|
|
||||||
case 'D':
|
|
||||||
cursor.side = cursor.side === 'module' ? 'gadget' : 'module';
|
cursor.side = cursor.side === 'module' ? 'gadget' : 'module';
|
||||||
{
|
{ const max = cursor.side === 'module' ? mPorts.length - 1 : gPorts.length - 1;
|
||||||
const max = cursor.side === 'module' ? mPorts.length - 1 : gPorts.length - 1;
|
cursor.index = Math.min(cursor.index, Math.max(max, 0)); }
|
||||||
cursor.index = Math.min(cursor.index, Math.max(max, 0));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'e':
|
case 'e': case 'E': case ' ':
|
||||||
case 'E':
|
|
||||||
case ' ':
|
|
||||||
selectPort();
|
selectPort();
|
||||||
break;
|
break;
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
executeWiring();
|
executeWiring();
|
||||||
break;
|
break;
|
||||||
case 'Backspace':
|
case 'Backspace': case 'Delete': case 'x': case 'X':
|
||||||
case 'Delete':
|
|
||||||
case 'x':
|
|
||||||
case 'X':
|
|
||||||
removeWireAtCursor();
|
removeWireAtCursor();
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
@@ -144,7 +146,6 @@ function tryCreateWire() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation: module-out → gadget-in, OR gadget-out → module-in
|
|
||||||
const valid =
|
const valid =
|
||||||
(mPort.dir === 'out' && gPort.dir === 'in') ||
|
(mPort.dir === 'out' && gPort.dir === 'in') ||
|
||||||
(mPort.dir === 'in' && gPort.dir === 'out');
|
(mPort.dir === 'in' && gPort.dir === 'out');
|
||||||
@@ -157,13 +158,10 @@ function tryCreateWire() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any existing wire on either port
|
|
||||||
wires = wires.filter(w => w.moduleIdx !== selectedModule && w.gadgetIdx !== selectedGadget);
|
wires = wires.filter(w => w.moduleIdx !== selectedModule && w.gadgetIdx !== selectedGadget);
|
||||||
|
|
||||||
wires.push({ moduleIdx: selectedModule, gadgetIdx: selectedGadget });
|
wires.push({ moduleIdx: selectedModule, gadgetIdx: selectedGadget });
|
||||||
result = { message: `✓ Wired ${mPort.name} ↔ ${gPort.name}`, color: '#00e599' };
|
result = { message: `✓ Wired ${mPort.name} ↔ ${gPort.name}`, color: '#00e599' };
|
||||||
resultTimer = Date.now();
|
resultTimer = Date.now();
|
||||||
|
|
||||||
selectedModule = null;
|
selectedModule = null;
|
||||||
selectedGadget = null;
|
selectedGadget = null;
|
||||||
}
|
}
|
||||||
@@ -180,28 +178,17 @@ function removeWireAtCursor() {
|
|||||||
|
|
||||||
// ==================== Circuit evaluation ====================
|
// ==================== Circuit evaluation ====================
|
||||||
|
|
||||||
/**
|
|
||||||
* Mini circuit evaluator — runs a cloned gadget circuit with given input values.
|
|
||||||
* Returns an object mapping OUTPUT gate IDs → computed values.
|
|
||||||
*/
|
|
||||||
function evaluateGadgetCircuit(gates, connections, inputValues) {
|
function evaluateGadgetCircuit(gates, connections, inputValues) {
|
||||||
// inputValues = { gateId: 0|1 }
|
|
||||||
const evalGates = JSON.parse(JSON.stringify(gates));
|
const evalGates = JSON.parse(JSON.stringify(gates));
|
||||||
|
|
||||||
// Set input gate values
|
|
||||||
for (const g of evalGates) {
|
for (const g of evalGates) {
|
||||||
if (g.type === 'INPUT' && inputValues[g.id] !== undefined) {
|
if (g.type === 'INPUT' && inputValues[g.id] !== undefined) {
|
||||||
g.value = inputValues[g.id];
|
g.value = inputValues[g.id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixed-point iteration (max 20 passes)
|
|
||||||
for (let iter = 0; iter < 20; iter++) {
|
for (let iter = 0; iter < 20; iter++) {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
for (const g of evalGates) {
|
for (const g of evalGates) {
|
||||||
if (g.type === 'INPUT' || g.type === 'CLOCK') continue;
|
if (g.type === 'INPUT' || g.type === 'CLOCK') continue;
|
||||||
|
|
||||||
// Gather inputs from connections
|
|
||||||
const inCount = (g.type === 'NOT' || g.type === 'OUTPUT') ? 1 : 2;
|
const inCount = (g.type === 'NOT' || g.type === 'OUTPUT') ? 1 : 2;
|
||||||
const ins = [];
|
const ins = [];
|
||||||
for (let p = 0; p < inCount; p++) {
|
for (let p = 0; p < inCount; p++) {
|
||||||
@@ -209,11 +196,8 @@ function evaluateGadgetCircuit(gates, connections, inputValues) {
|
|||||||
if (conn) {
|
if (conn) {
|
||||||
const src = evalGates.find(s => s.id === conn.from);
|
const src = evalGates.find(s => s.id === conn.from);
|
||||||
ins.push(src ? (src.value || 0) : 0);
|
ins.push(src ? (src.value || 0) : 0);
|
||||||
} else {
|
} else { ins.push(0); }
|
||||||
ins.push(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let val = 0;
|
let val = 0;
|
||||||
switch (g.type) {
|
switch (g.type) {
|
||||||
case 'AND': val = (ins[0] && ins[1]) ? 1 : 0; break;
|
case 'AND': val = (ins[0] && ins[1]) ? 1 : 0; break;
|
||||||
@@ -225,43 +209,39 @@ function evaluateGadgetCircuit(gates, connections, inputValues) {
|
|||||||
case 'OUTPUT': val = ins[0] || 0; break;
|
case 'OUTPUT': val = ins[0] || 0; break;
|
||||||
default: val = g.value || 0;
|
default: val = g.value || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (val !== g.value) { g.value = val; changed = true; }
|
if (val !== g.value) { g.value = val; changed = true; }
|
||||||
}
|
}
|
||||||
if (!changed) break;
|
if (!changed) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return evalGates;
|
return evalGates;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the wiring — build a test() function from the wires,
|
* Build the test function + run all combos to produce the truth table,
|
||||||
* then run the module's verify function to check if the gadget passes.
|
* then pass it to the verify function. Store results for the execution log.
|
||||||
*/
|
*/
|
||||||
function executeWiring() {
|
function executeWiring() {
|
||||||
const mPorts = moduleInter.ports || [];
|
const mPorts = moduleInter.ports || [];
|
||||||
const gPorts = getGadgetPorts();
|
const gPorts = getGadgetPorts();
|
||||||
|
|
||||||
// Build the test function that the verify code will call.
|
// Identify module out ports (inputs to the gadget) and in ports (outputs from gadget)
|
||||||
// test(moduleOutputValues) → moduleInputValues
|
const outPorts = mPorts.filter(p => p.dir === 'out');
|
||||||
function test(moduleOutputs) {
|
const inPorts = mPorts.filter(p => p.dir === 'in');
|
||||||
// moduleOutputs = { A: 0, B: 1, ... } — values for module's "out" ports
|
const n = outPorts.length;
|
||||||
|
|
||||||
// Map module output values → gadget input gates via wires
|
// Collect truth table rows: for each input combo, run the circuit
|
||||||
|
const rows = [];
|
||||||
|
|
||||||
|
function testOnce(moduleOutputs) {
|
||||||
const inputValues = {};
|
const inputValues = {};
|
||||||
for (const wire of wires) {
|
for (const wire of wires) {
|
||||||
const mp = mPorts[wire.moduleIdx];
|
const mp = mPorts[wire.moduleIdx];
|
||||||
const gp = gPorts[wire.gadgetIdx];
|
const gp = gPorts[wire.gadgetIdx];
|
||||||
if (mp.dir === 'out' && gp.dir === 'in') {
|
if (mp.dir === 'out' && gp.dir === 'in') {
|
||||||
// Module provides value → gadget receives it
|
|
||||||
inputValues[gp.gateId] = moduleOutputs[mp.name] || 0;
|
inputValues[gp.gateId] = moduleOutputs[mp.name] || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate the gadget circuit
|
|
||||||
const evaluated = evaluateGadgetCircuit(gadget.gates, gadget.connections, inputValues);
|
const evaluated = evaluateGadgetCircuit(gadget.gates, gadget.connections, inputValues);
|
||||||
|
|
||||||
// Read gadget output gates → map back to module input ports via wires
|
|
||||||
const moduleInputs = {};
|
const moduleInputs = {};
|
||||||
for (const wire of wires) {
|
for (const wire of wires) {
|
||||||
const mp = mPorts[wire.moduleIdx];
|
const mp = mPorts[wire.moduleIdx];
|
||||||
@@ -271,37 +251,66 @@ function executeWiring() {
|
|||||||
moduleInputs[mp.name] = outGate ? (outGate.value || 0) : 0;
|
moduleInputs[mp.name] = outGate ? (outGate.value || 0) : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return moduleInputs;
|
return moduleInputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the verify function
|
// 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 {
|
try {
|
||||||
const verifyFn = new Function('return ' + moduleInter.verify)();
|
const verifyFn = new Function('return ' + moduleInter.verify)();
|
||||||
const passed = verifyFn(test);
|
const passed = verifyFn(test);
|
||||||
|
|
||||||
if (passed) {
|
// Build port name lists for display
|
||||||
result = { message: '⚡ VERIFIED! Module unlocked!', color: '#00ff88' };
|
const portNames = {
|
||||||
resultTimer = Date.now();
|
inputs: outPorts.map(p => p.name),
|
||||||
|
outputs: inPorts.map(p => p.name)
|
||||||
|
};
|
||||||
|
|
||||||
// Mark as solved if it has an ID
|
// Set up execution log with animated reveal
|
||||||
if (moduleInter.moduleId) {
|
execLog = {
|
||||||
solvePuzzle(moduleInter.moduleId);
|
rows,
|
||||||
}
|
portNames,
|
||||||
|
passed,
|
||||||
|
startTime: Date.now(),
|
||||||
|
totalRows: rows.length
|
||||||
|
};
|
||||||
|
|
||||||
// Close after a brief delay and show dialog
|
if (passed && moduleInter.moduleId) {
|
||||||
setTimeout(() => {
|
solvePuzzle(moduleInter.moduleId);
|
||||||
closeWiringPanel();
|
|
||||||
showNotification('Module unlocked!', '⚡', '#00ff88');
|
|
||||||
startDialog([
|
|
||||||
`⚡ "${gadget.name}" passed the verification!`,
|
|
||||||
'The module hums to life and the door unlocks.'
|
|
||||||
], 'System');
|
|
||||||
}, 800);
|
|
||||||
} else {
|
|
||||||
result = { message: '✗ Verification failed — wrong logic', color: '#ff4444' };
|
|
||||||
resultTimer = Date.now();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result = { message: `Error: ${e.message}`, color: '#ff4444' };
|
result = { message: `Error: ${e.message}`, color: '#ff4444' };
|
||||||
resultTimer = Date.now();
|
resultTimer = Date.now();
|
||||||
@@ -319,9 +328,25 @@ const WIRE_COLOR = '#ffdd44';
|
|||||||
const SELECTED_COLOR = '#ffffff';
|
const SELECTED_COLOR = '#ffffff';
|
||||||
const CURSOR_COLOR = '#00ffcc';
|
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) {
|
export function drawWiringPanel(ctx, canvasW, canvasH) {
|
||||||
if (!panelOpen || !moduleInter || !gadget) return;
|
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 mPorts = moduleInter.ports || [];
|
||||||
const gPorts = getGadgetPorts();
|
const gPorts = getGadgetPorts();
|
||||||
|
|
||||||
@@ -364,15 +389,11 @@ export function drawWiringPanel(ctx, canvasW, canvasH) {
|
|||||||
|
|
||||||
ctx.font = 'bold 13px "Segoe UI", system-ui, sans-serif';
|
ctx.font = 'bold 13px "Segoe UI", system-ui, sans-serif';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
|
|
||||||
// Module ports header
|
|
||||||
ctx.fillStyle = '#aaa';
|
ctx.fillStyle = '#aaa';
|
||||||
ctx.fillText('MODULE PORTS', leftX + 80, colY);
|
ctx.fillText('MODULE PORTS', leftX + 80, colY);
|
||||||
|
|
||||||
// Gadget ports header
|
|
||||||
ctx.fillText('GADGET PORTS', rightX - 80, colY);
|
ctx.fillText('GADGET PORTS', rightX - 80, colY);
|
||||||
|
|
||||||
// Draw column separator
|
// Column separator
|
||||||
ctx.strokeStyle = '#333';
|
ctx.strokeStyle = '#333';
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -380,142 +401,90 @@ export function drawWiringPanel(ctx, canvasW, canvasH) {
|
|||||||
ctx.lineTo(px + pw / 2, py + ph - 50);
|
ctx.lineTo(px + pw / 2, py + ph - 50);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Port positions for wire drawing
|
|
||||||
const modulePortPositions = [];
|
const modulePortPositions = [];
|
||||||
const gadgetPortPositions = [];
|
const gadgetPortPositions = [];
|
||||||
|
|
||||||
// Draw module ports
|
// Draw module ports
|
||||||
ctx.font = '13px "Segoe UI", system-ui, sans-serif';
|
|
||||||
ctx.textAlign = 'left';
|
|
||||||
for (let i = 0; i < mPorts.length; i++) {
|
for (let i = 0; i < mPorts.length; i++) {
|
||||||
const port = mPorts[i];
|
const port = mPorts[i];
|
||||||
const yPos = portStartY + i * portSpacing;
|
const yPos = portStartY + i * portSpacing;
|
||||||
const isOut = port.dir === 'out';
|
const isOut = port.dir === 'out';
|
||||||
const dotX = leftX;
|
const dotX = leftX;
|
||||||
const dotRadius = 8;
|
|
||||||
|
|
||||||
// Port dot position (for wires)
|
|
||||||
const wireX = leftX + 170;
|
const wireX = leftX + 170;
|
||||||
modulePortPositions.push({ x: wireX, y: yPos + 6 });
|
modulePortPositions.push({ x: wireX, y: yPos + 6 });
|
||||||
|
|
||||||
// Highlight if cursor is here
|
|
||||||
const isCursor = cursor.side === 'module' && cursor.index === i;
|
const isCursor = cursor.side === 'module' && cursor.index === i;
|
||||||
const isSelected = selectedModule === i;
|
const isSelected = selectedModule === i;
|
||||||
|
|
||||||
// Background highlight
|
|
||||||
if (isCursor) {
|
if (isCursor) {
|
||||||
ctx.fillStyle = 'rgba(0, 255, 200, 0.1)';
|
ctx.fillStyle = 'rgba(0, 255, 200, 0.1)';
|
||||||
ctx.fillRect(leftX - 10, yPos - 6, 190, portSpacing - 8);
|
ctx.fillRect(leftX - 10, yPos - 6, 190, portSpacing - 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Port dot
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(dotX, yPos + 6, dotRadius, 0, Math.PI * 2);
|
ctx.arc(dotX, yPos + 6, 8, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = isSelected ? SELECTED_COLOR : (isOut ? PORT_OUT_COLOR : PORT_IN_COLOR);
|
ctx.fillStyle = isSelected ? SELECTED_COLOR : (isOut ? PORT_OUT_COLOR : PORT_IN_COLOR);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
if (isCursor) {
|
if (isCursor) { ctx.strokeStyle = CURSOR_COLOR; ctx.lineWidth = 2; ctx.stroke(); }
|
||||||
ctx.strokeStyle = CURSOR_COLOR;
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Direction arrow inside dot
|
ctx.fillStyle = '#000'; ctx.font = 'bold 10px monospace';
|
||||||
ctx.fillStyle = '#000';
|
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||||
ctx.font = 'bold 10px monospace';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.fillText(isOut ? '→' : '←', dotX, yPos + 6);
|
ctx.fillText(isOut ? '→' : '←', dotX, yPos + 6);
|
||||||
|
|
||||||
// Port label
|
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
|
||||||
ctx.textAlign = 'left';
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
ctx.font = '13px "Segoe UI", system-ui, sans-serif';
|
ctx.font = '13px "Segoe UI", system-ui, sans-serif';
|
||||||
ctx.fillStyle = isCursor ? CURSOR_COLOR : '#ddd';
|
ctx.fillStyle = isCursor ? CURSOR_COLOR : '#ddd';
|
||||||
ctx.fillText(`${port.name}`, dotX + 14, yPos);
|
ctx.fillText(port.name, dotX + 14, yPos);
|
||||||
|
|
||||||
// Direction tag
|
|
||||||
ctx.font = '10px monospace';
|
ctx.font = '10px monospace';
|
||||||
ctx.fillStyle = isOut ? PORT_OUT_COLOR : PORT_IN_COLOR;
|
ctx.fillStyle = isOut ? PORT_OUT_COLOR : PORT_IN_COLOR;
|
||||||
ctx.fillText(isOut ? 'OUT' : 'IN', dotX + 14, yPos + 16);
|
ctx.fillText(isOut ? 'OUT' : 'IN', dotX + 14, yPos + 16);
|
||||||
|
if (wires.find(w => w.moduleIdx === i)) {
|
||||||
// Wire connected indicator
|
ctx.fillStyle = WIRE_COLOR; ctx.fillText('⚡', dotX + 50, yPos + 16);
|
||||||
const wire = wires.find(w => w.moduleIdx === i);
|
|
||||||
if (wire) {
|
|
||||||
ctx.fillStyle = WIRE_COLOR;
|
|
||||||
ctx.fillText('⚡', dotX + 50, yPos + 16);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw gadget ports
|
// Draw gadget ports
|
||||||
ctx.textAlign = 'right';
|
|
||||||
for (let i = 0; i < gPorts.length; i++) {
|
for (let i = 0; i < gPorts.length; i++) {
|
||||||
const port = gPorts[i];
|
const port = gPorts[i];
|
||||||
const yPos = portStartY + i * portSpacing;
|
const yPos = portStartY + i * portSpacing;
|
||||||
const isOut = port.dir === 'out';
|
const isOut = port.dir === 'out';
|
||||||
const dotX = rightX;
|
const dotX = rightX;
|
||||||
const dotRadius = 8;
|
|
||||||
|
|
||||||
const wireX = rightX - 170;
|
const wireX = rightX - 170;
|
||||||
gadgetPortPositions.push({ x: wireX, y: yPos + 6 });
|
gadgetPortPositions.push({ x: wireX, y: yPos + 6 });
|
||||||
|
|
||||||
const isCursor = cursor.side === 'gadget' && cursor.index === i;
|
const isCursor = cursor.side === 'gadget' && cursor.index === i;
|
||||||
const isSelected = selectedGadget === i;
|
const isSelected = selectedGadget === i;
|
||||||
|
|
||||||
// Background highlight
|
|
||||||
if (isCursor) {
|
if (isCursor) {
|
||||||
ctx.fillStyle = 'rgba(0, 255, 200, 0.1)';
|
ctx.fillStyle = 'rgba(0, 255, 200, 0.1)';
|
||||||
ctx.fillRect(rightX - 180, yPos - 6, 190, portSpacing - 8);
|
ctx.fillRect(rightX - 180, yPos - 6, 190, portSpacing - 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Port dot
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(dotX, yPos + 6, dotRadius, 0, Math.PI * 2);
|
ctx.arc(dotX, yPos + 6, 8, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = isSelected ? SELECTED_COLOR : (isOut ? PORT_OUT_COLOR : PORT_IN_COLOR);
|
ctx.fillStyle = isSelected ? SELECTED_COLOR : (isOut ? PORT_OUT_COLOR : PORT_IN_COLOR);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
if (isCursor) {
|
if (isCursor) { ctx.strokeStyle = CURSOR_COLOR; ctx.lineWidth = 2; ctx.stroke(); }
|
||||||
ctx.strokeStyle = CURSOR_COLOR;
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Direction arrow
|
ctx.fillStyle = '#000'; ctx.font = 'bold 10px monospace';
|
||||||
ctx.fillStyle = '#000';
|
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||||
ctx.font = 'bold 10px monospace';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.fillText(isOut ? '→' : '←', dotX, yPos + 6);
|
ctx.fillText(isOut ? '→' : '←', dotX, yPos + 6);
|
||||||
|
|
||||||
// Port label
|
ctx.textAlign = 'right'; ctx.textBaseline = 'top';
|
||||||
ctx.textAlign = 'right';
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
ctx.font = '13px "Segoe UI", system-ui, sans-serif';
|
ctx.font = '13px "Segoe UI", system-ui, sans-serif';
|
||||||
ctx.fillStyle = isCursor ? CURSOR_COLOR : '#ddd';
|
ctx.fillStyle = isCursor ? CURSOR_COLOR : '#ddd';
|
||||||
ctx.fillText(`${port.name}`, dotX - 14, yPos);
|
ctx.fillText(port.name, dotX - 14, yPos);
|
||||||
|
|
||||||
// Direction tag
|
|
||||||
ctx.font = '10px monospace';
|
ctx.font = '10px monospace';
|
||||||
ctx.fillStyle = isOut ? PORT_OUT_COLOR : PORT_IN_COLOR;
|
ctx.fillStyle = isOut ? PORT_OUT_COLOR : PORT_IN_COLOR;
|
||||||
ctx.fillText(isOut ? 'OUT' : 'IN', dotX - 14, yPos + 16);
|
ctx.fillText(isOut ? 'OUT' : 'IN', dotX - 14, yPos + 16);
|
||||||
|
if (wires.find(w => w.gadgetIdx === i)) {
|
||||||
// Wire connected indicator
|
ctx.fillStyle = WIRE_COLOR; ctx.fillText('⚡', dotX - 60, yPos + 16);
|
||||||
const wire = wires.find(w => w.gadgetIdx === i);
|
|
||||||
if (wire) {
|
|
||||||
ctx.fillStyle = WIRE_COLOR;
|
|
||||||
ctx.fillText('⚡', dotX - 60, yPos + 16);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw wires
|
// Draw wires (bezier)
|
||||||
ctx.strokeStyle = WIRE_COLOR;
|
ctx.strokeStyle = WIRE_COLOR; ctx.lineWidth = 2; ctx.setLineDash([6, 4]);
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.setLineDash([6, 4]);
|
|
||||||
for (const wire of wires) {
|
for (const wire of wires) {
|
||||||
const mp = modulePortPositions[wire.moduleIdx];
|
const mp = modulePortPositions[wire.moduleIdx];
|
||||||
const gp = gadgetPortPositions[wire.gadgetIdx];
|
const gp = gadgetPortPositions[wire.gadgetIdx];
|
||||||
if (mp && gp) {
|
if (mp && gp) {
|
||||||
ctx.beginPath();
|
ctx.beginPath(); ctx.moveTo(mp.x, mp.y);
|
||||||
ctx.moveTo(mp.x, mp.y);
|
|
||||||
// Bezier curve for nice look
|
|
||||||
const midX = (mp.x + gp.x) / 2;
|
const midX = (mp.x + gp.x) / 2;
|
||||||
ctx.bezierCurveTo(midX, mp.y, midX, gp.y, gp.x, gp.y);
|
ctx.bezierCurveTo(midX, mp.y, midX, gp.y, gp.x, gp.y);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
@@ -523,41 +492,304 @@ export function drawWiringPanel(ctx, canvasW, canvasH) {
|
|||||||
}
|
}
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
|
|
||||||
// Pending wire (half-selected)
|
// Pending wire indicator
|
||||||
if (selectedModule !== null && selectedGadget === null) {
|
if (selectedModule !== null && selectedGadget === null) {
|
||||||
const mp = modulePortPositions[selectedModule];
|
const mp = modulePortPositions[selectedModule];
|
||||||
if (mp) {
|
if (mp) { ctx.beginPath(); ctx.arc(mp.x, mp.y, 5, 0, Math.PI * 2); ctx.fillStyle = SELECTED_COLOR; ctx.fill(); }
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(mp.x, mp.y, 5, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = SELECTED_COLOR;
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (selectedGadget !== null && selectedModule === null) {
|
if (selectedGadget !== null && selectedModule === null) {
|
||||||
const gp = gadgetPortPositions[selectedGadget];
|
const gp = gadgetPortPositions[selectedGadget];
|
||||||
if (gp) {
|
if (gp) { ctx.beginPath(); ctx.arc(gp.x, gp.y, 5, 0, Math.PI * 2); ctx.fillStyle = SELECTED_COLOR; ctx.fill(); }
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(gp.x, gp.y, 5, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = SELECTED_COLOR;
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Result message
|
// Result message (transient)
|
||||||
if (result && (Date.now() - resultTimer < 3000)) {
|
if (result && (Date.now() - resultTimer < 3000)) {
|
||||||
ctx.fillStyle = result.color;
|
ctx.fillStyle = result.color;
|
||||||
ctx.font = 'bold 14px "Segoe UI", system-ui, sans-serif';
|
ctx.font = 'bold 14px "Segoe UI", system-ui, sans-serif';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||||||
ctx.textBaseline = 'bottom';
|
|
||||||
ctx.fillText(result.message, px + pw / 2, py + ph - 35);
|
ctx.fillText(result.message, px + pw / 2, py + ph - 35);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Controls hint
|
// Controls hint
|
||||||
ctx.fillStyle = '#555';
|
ctx.fillStyle = '#555'; ctx.font = '11px "Segoe UI", system-ui, sans-serif';
|
||||||
ctx.font = '11px "Segoe UI", system-ui, sans-serif';
|
ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||||||
ctx.textAlign = 'center';
|
ctx.fillText('↑↓←→: Navigate | E: Wire | X: Remove | Enter: Execute | ESC: Close', px + pw / 2, py + ph - 10);
|
||||||
ctx.textBaseline = 'bottom';
|
}
|
||||||
ctx.fillText('↑↓←→: Navigate | E: Select/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 ====================
|
// ==================== Helpers ====================
|
||||||
|
|||||||
Reference in New Issue
Block a user