You've already forked computing-box
Updates to canvas of logic gate simulator:
All checks were successful
Pre-release on non-main branches / prerelease (push) Successful in 29s
All checks were successful
Pre-release on non-main branches / prerelease (push) Successful in 29s
- Addition of zoom feature Signed-off-by: Alexander Lyall <alex@adcm.uk>
This commit is contained in:
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "computing-box",
|
"name": "computing-box",
|
||||||
"version": "0.0.1",
|
"version": "2.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "computing-box",
|
"name": "computing-box",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.1",
|
"version": "2.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
|
|||||||
@@ -14,38 +14,41 @@ import "../styles/logic-gates.css";
|
|||||||
<div class="lg-top-header">
|
<div class="lg-top-header">
|
||||||
<div class="lg-title">Interactive Logic Circuit Builder</div>
|
<div class="lg-title">Interactive Logic Circuit Builder</div>
|
||||||
<div class="lg-subtitle">
|
<div class="lg-subtitle">
|
||||||
Drag items from the toolbox to the board. Drag from output ports to input ports to wire. Click a wire or node and press <kbd>Delete</kbd> to remove it.
|
Drag items from the toolbox. Drag from output ports to input ports to wire. Click a wire/node and press <kbd>Delete</kbd>. Scroll to Zoom.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lg-workspace" id="workspace">
|
<div class="lg-workspace" id="workspace">
|
||||||
<svg class="lg-svg-layer" id="wireLayer"></svg>
|
|
||||||
|
<div class="lg-zoom-controls">
|
||||||
|
<button class="lg-zoom-btn" id="btnZoomIn" title="Zoom In">+</button>
|
||||||
|
<button class="lg-zoom-btn" id="btnZoomOut" title="Zoom Out">−</button>
|
||||||
|
<button class="lg-zoom-btn" id="btnZoomReset" title="Reset View" style="font-size: 16px;">⌂</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="lg-viewport" id="viewport">
|
||||||
|
<svg class="lg-svg-layer" id="wireLayer"></svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<aside id="toolboxPanel" class="lg-toolbox" aria-label="Toolbox">
|
<aside id="toolboxPanel" class="lg-toolbox" aria-label="Toolbox">
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="cardTitle">Components</div>
|
<div class="cardTitle">Components</div>
|
||||||
|
<div class="tb-icon-grid" id="toolboxGrid"></div>
|
||||||
<div class="tb-icon-grid" id="toolboxGrid">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="cardTitle">Live Truth Table</div>
|
<div class="cardTitle">Live Truth Table</div>
|
||||||
<details open>
|
<details open>
|
||||||
<summary class="tt-summary">Show / Hide Table</summary>
|
<summary class="tt-summary">Show / Hide Table</summary>
|
||||||
<div style="font-family: var(--ui-font); font-size: 12px; color: var(--muted); margin-bottom: 12px;">Auto-generates based on current wiring. (Max 6 inputs)</div>
|
<div style="font-family: var(--ui-font); font-size: 12px; color: var(--muted); margin-bottom: 12px;">Auto-generates based on current wiring. (Max 6 inputs)</div>
|
||||||
<div class="tt-table-wrap" id="truthTableContainer">
|
<div class="tt-table-wrap" id="truthTableContainer"></div>
|
||||||
</div>
|
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="cardTitle">Tools</div>
|
<div class="cardTitle">Tools</div>
|
||||||
<button class="btn btnReset btnWide" id="btnClearBoard" type="button" style="margin-bottom:0;">Clear Board</button>
|
<button class="btn btnReset btnWide" id="btnClearBoard" type="button" style="margin-bottom:0;">Clear Board</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
(() => {
|
(() => {
|
||||||
/* --- DOM Elements --- */
|
/* --- DOM Elements --- */
|
||||||
const workspace = document.getElementById("workspace");
|
const workspace = document.getElementById("workspace");
|
||||||
|
const viewport = document.getElementById("viewport");
|
||||||
const wireLayer = document.getElementById("wireLayer");
|
const wireLayer = document.getElementById("wireLayer");
|
||||||
const ttContainer = document.getElementById("truthTableContainer");
|
const ttContainer = document.getElementById("truthTableContainer");
|
||||||
const toolboxGrid = document.getElementById("toolboxGrid");
|
const toolboxGrid = document.getElementById("toolboxGrid");
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
const toolboxToggle = document.getElementById("toolboxToggle");
|
const toolboxToggle = document.getElementById("toolboxToggle");
|
||||||
const logicPage = document.getElementById("logicPage");
|
const logicPage = document.getElementById("logicPage");
|
||||||
|
|
||||||
/* --- ANSI Gate SVGs (Strict 100x50 with built-in tails) --- */
|
/* --- ANSI Gate SVGs --- */
|
||||||
const GATE_SVGS = {
|
const GATE_SVGS = {
|
||||||
'AND': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L20,15 M0,35 L20,35 M70,25 L100,25"/><path d="M20,5 L50,5 A20,20 0 0 1 50,45 L20,45 Z" fill="var(--bg)"/></g>`,
|
'AND': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L20,15 M0,35 L20,35 M70,25 L100,25"/><path d="M20,5 L50,5 A20,20 0 0 1 50,45 L20,45 Z" fill="var(--bg)"/></g>`,
|
||||||
'OR': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L26,15 M0,35 L26,35 M70,25 L100,25"/><path d="M20,5 Q55,5 70,25 Q55,45 20,45 Q40,25 20,5 Z" fill="var(--bg)"/></g>`,
|
'OR': `<g stroke="#e8e8ee" stroke-width="3" fill="none"><path d="M0,15 L26,15 M0,35 L26,35 M70,25 L100,25"/><path d="M20,5 Q55,5 70,25 Q55,45 20,45 Q40,25 20,5 Z" fill="var(--bg)"/></g>`,
|
||||||
@@ -33,16 +34,21 @@
|
|||||||
let nextNodeId = 1;
|
let nextNodeId = 1;
|
||||||
let nextWireId = 1;
|
let nextWireId = 1;
|
||||||
|
|
||||||
|
// Interaction State
|
||||||
let isDraggingNode = null;
|
let isDraggingNode = null;
|
||||||
let dragOffset = { x: 0, y: 0 };
|
let dragOffset = { x: 0, y: 0 };
|
||||||
let clickStartX = 0, clickStartY = 0;
|
let clickStartX = 0, clickStartY = 0;
|
||||||
|
|
||||||
let wiringStart = null;
|
let wiringStart = null;
|
||||||
let tempWirePath = null;
|
let tempWirePath = null;
|
||||||
|
|
||||||
let selectedWireId = null;
|
let selectedWireId = null;
|
||||||
let selectedNodeId = 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 --- */
|
/* --- Setup Toolbox --- */
|
||||||
function initToolbox() {
|
function initToolbox() {
|
||||||
if(!toolboxGrid) return;
|
if(!toolboxGrid) return;
|
||||||
@@ -56,7 +62,6 @@
|
|||||||
<div class="tb-icon-label">Output</div>
|
<div class="tb-icon-label">Output</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
Object.keys(GATE_SVGS).forEach(gate => {
|
Object.keys(GATE_SVGS).forEach(gate => {
|
||||||
html += `
|
html += `
|
||||||
<div draggable="true" data-spawn="GATE" data-gate="${gate}" class="drag-item tb-icon-box" title="${gate} Gate">
|
<div draggable="true" data-spawn="GATE" data-gate="${gate}" class="drag-item tb-icon-box" title="${gate} Gate">
|
||||||
@@ -65,7 +70,6 @@
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
toolboxGrid.innerHTML = html;
|
toolboxGrid.innerHTML = html;
|
||||||
|
|
||||||
document.querySelectorAll('.drag-item').forEach(item => {
|
document.querySelectorAll('.drag-item').forEach(item => {
|
||||||
@@ -76,7 +80,21 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Math & Geometry --- */
|
/* --- 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) {
|
function getPortCoords(nodeId, portDataAttr) {
|
||||||
const node = nodes[nodeId];
|
const node = nodes[nodeId];
|
||||||
if (!node || !node.el) return {x:0, y:0};
|
if (!node || !node.el) return {x:0, y:0};
|
||||||
@@ -87,9 +105,10 @@
|
|||||||
const wsRect = workspace.getBoundingClientRect();
|
const wsRect = workspace.getBoundingClientRect();
|
||||||
const portRect = portEl.getBoundingClientRect();
|
const portRect = portEl.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Calculate backwards through camera scale/pan to find true local coordinates
|
||||||
return {
|
return {
|
||||||
x: portRect.left - wsRect.left + (portRect.width / 2),
|
x: (portRect.left - wsRect.left - panX + portRect.width / 2) / zoom,
|
||||||
y: portRect.top - wsRect.top + (portRect.height / 2)
|
y: (portRect.top - wsRect.top - panY + portRect.height / 2) / zoom
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,37 +120,30 @@
|
|||||||
/* --- Rendering --- */
|
/* --- Rendering --- */
|
||||||
function renderWires() {
|
function renderWires() {
|
||||||
let svgHTML = '';
|
let svgHTML = '';
|
||||||
|
|
||||||
connections.forEach(conn => {
|
connections.forEach(conn => {
|
||||||
const from = getPortCoords(conn.fromNode, 'out');
|
const from = getPortCoords(conn.fromNode, 'out');
|
||||||
const to = getPortCoords(conn.toNode, `in${conn.toPort}`);
|
const to = getPortCoords(conn.toNode, `in${conn.toPort}`);
|
||||||
const sourceNode = nodes[conn.fromNode];
|
const sourceNode = nodes[conn.fromNode];
|
||||||
const isActive = sourceNode && sourceNode.value === true;
|
const isActive = sourceNode && sourceNode.value === true;
|
||||||
const isSelected = conn.id === selectedWireId;
|
const isSelected = conn.id === selectedWireId;
|
||||||
|
|
||||||
svgHTML += `<path class="lg-wire ${isActive ? 'active' : ''} ${isSelected ? 'selected' : ''}" d="${drawBezier(from.x, from.y, to.x, to.y)}" data-conn-id="${conn.id}" />`;
|
svgHTML += `<path class="lg-wire ${isActive ? 'active' : ''} ${isSelected ? 'selected' : ''}" d="${drawBezier(from.x, from.y, to.x, to.y)}" data-conn-id="${conn.id}" />`;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (wiringStart && tempWirePath) {
|
if (wiringStart && tempWirePath) {
|
||||||
svgHTML += `<path class="lg-wire lg-wire-temp" d="${drawBezier(wiringStart.x, wiringStart.y, tempWirePath.x, tempWirePath.y)}" />`;
|
svgHTML += `<path class="lg-wire lg-wire-temp" d="${drawBezier(wiringStart.x, wiringStart.y, tempWirePath.x, tempWirePath.y)}" />`;
|
||||||
}
|
}
|
||||||
|
|
||||||
wireLayer.innerHTML = svgHTML;
|
wireLayer.innerHTML = svgHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNodePositions() {
|
function updateNodePositions() {
|
||||||
Object.values(nodes).forEach(n => {
|
Object.values(nodes).forEach(n => {
|
||||||
if (n.el) {
|
if (n.el) { n.el.style.left = `${n.x}px`; n.el.style.top = `${n.y}px`; }
|
||||||
n.el.style.left = `${n.x}px`;
|
|
||||||
n.el.style.top = `${n.y}px`;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
renderWires();
|
renderWires();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSelection() {
|
function clearSelection() {
|
||||||
selectedWireId = null;
|
selectedWireId = null; selectedNodeId = null;
|
||||||
selectedNodeId = null;
|
|
||||||
document.querySelectorAll('.lg-node.selected').forEach(el => el.classList.remove('selected'));
|
document.querySelectorAll('.lg-node.selected').forEach(el => el.classList.remove('selected'));
|
||||||
renderWires();
|
renderWires();
|
||||||
}
|
}
|
||||||
@@ -139,22 +151,16 @@
|
|||||||
/* --- Logic Evaluation --- */
|
/* --- Logic Evaluation --- */
|
||||||
function evaluateGraph(overrideInputs = null) {
|
function evaluateGraph(overrideInputs = null) {
|
||||||
let context = {};
|
let context = {};
|
||||||
|
|
||||||
Object.values(nodes).filter(n => n.type === 'INPUT').forEach(n => {
|
Object.values(nodes).filter(n => n.type === 'INPUT').forEach(n => {
|
||||||
context[n.id] = overrideInputs ? overrideInputs[n.id] : n.value;
|
context[n.id] = overrideInputs ? overrideInputs[n.id] : n.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
let changed = true;
|
let changed = true; let loops = 0;
|
||||||
let loops = 0;
|
|
||||||
|
|
||||||
while (changed && loops < 10) {
|
while (changed && loops < 10) {
|
||||||
changed = false;
|
changed = false; loops++;
|
||||||
loops++;
|
|
||||||
|
|
||||||
Object.values(nodes).filter(n => n.type === 'GATE').forEach(gate => {
|
Object.values(nodes).filter(n => n.type === 'GATE').forEach(gate => {
|
||||||
let in1Conn = connections.find(c => c.toNode === gate.id && c.toPort === '1');
|
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 in2Conn = connections.find(c => c.toNode === gate.id && c.toPort === '2');
|
||||||
|
|
||||||
let val1 = in1Conn ? (context[in1Conn.fromNode] || false) : false;
|
let val1 = in1Conn ? (context[in1Conn.fromNode] || false) : false;
|
||||||
let val2 = in2Conn ? (context[in2Conn.fromNode] || false) : false;
|
let val2 = in2Conn ? (context[in2Conn.fromNode] || false) : false;
|
||||||
|
|
||||||
@@ -168,11 +174,7 @@
|
|||||||
case 'XOR': res = val1 !== val2; break;
|
case 'XOR': res = val1 !== val2; break;
|
||||||
case 'XNOR': res = val1 === val2; break;
|
case 'XNOR': res = val1 === val2; break;
|
||||||
}
|
}
|
||||||
|
if (context[gate.id] !== res) { context[gate.id] = res; changed = true; }
|
||||||
if (context[gate.id] !== res) {
|
|
||||||
context[gate.id] = res;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +195,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return outStates;
|
return outStates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,12 +206,10 @@
|
|||||||
const outNodes = Object.values(nodes).filter(n => n.type === 'OUTPUT').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) {
|
if (inNodes.length === 0 || outNodes.length === 0) {
|
||||||
ttContainer.innerHTML = '<div style="padding: 16px; color: var(--muted); text-align:center;">Add inputs and outputs to generate table.</div>';
|
ttContainer.innerHTML = '<div style="padding: 16px; color: var(--muted); text-align:center;">Add inputs and outputs to generate table.</div>'; return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (inNodes.length > 6) {
|
if (inNodes.length > 6) {
|
||||||
ttContainer.innerHTML = '<div style="padding: 16px; color: var(--muted); text-align:center;">Maximum 6 inputs supported.</div>';
|
ttContainer.innerHTML = '<div style="padding: 16px; color: var(--muted); text-align:center;">Maximum 6 inputs supported.</div>'; return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = '<table class="tt-table"><thead><tr>';
|
let html = '<table class="tt-table"><thead><tr>';
|
||||||
@@ -219,27 +218,16 @@
|
|||||||
html += '</tr></thead><tbody>';
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
const numRows = Math.pow(2, inNodes.length);
|
const numRows = Math.pow(2, inNodes.length);
|
||||||
|
|
||||||
for (let i = 0; i < numRows; i++) {
|
for (let i = 0; i < numRows; i++) {
|
||||||
let override = {};
|
let override = {};
|
||||||
inNodes.forEach((n, idx) => {
|
inNodes.forEach((n, idx) => { override[n.id] = ((i >> (inNodes.length - 1 - idx)) & 1) === 1; });
|
||||||
override[n.id] = ((i >> (inNodes.length - 1 - idx)) & 1) === 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
let outStates = evaluateGraph(override);
|
let outStates = evaluateGraph(override);
|
||||||
|
|
||||||
html += '<tr>';
|
html += '<tr>';
|
||||||
inNodes.forEach(n => {
|
inNodes.forEach(n => { let val = override[n.id]; html += `<td class="${val ? 'tt-on' : ''}">${val ? 1 : 0}</td>`; });
|
||||||
let val = override[n.id];
|
outNodes.forEach(n => { let val = outStates[n.id]; html += `<td class="${val ? 'tt-on' : ''}" style="font-weight:bold;">${val ? 1 : 0}</td>`; });
|
||||||
html += `<td class="${val ? 'tt-on' : ''}">${val ? 1 : 0}</td>`;
|
|
||||||
});
|
|
||||||
outNodes.forEach(n => {
|
|
||||||
let val = outStates[n.id];
|
|
||||||
html += `<td class="${val ? 'tt-on' : ''}" style="font-weight:bold;">${val ? 1 : 0}</td>`;
|
|
||||||
});
|
|
||||||
html += '</tr>';
|
html += '</tr>';
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</tbody></table>';
|
html += '</tbody></table>';
|
||||||
ttContainer.innerHTML = html;
|
ttContainer.innerHTML = html;
|
||||||
}
|
}
|
||||||
@@ -252,28 +240,22 @@
|
|||||||
|
|
||||||
/* --- Smart Label Generation --- */
|
/* --- Smart Label Generation --- */
|
||||||
function getNextInputLabel() {
|
function getNextInputLabel() {
|
||||||
let charCode = 65; // Starts at 'A'
|
let charCode = 65;
|
||||||
while (Object.values(nodes).some(n => n.type === 'INPUT' && n.label === String.fromCharCode(charCode))) {
|
while (Object.values(nodes).some(n => n.type === 'INPUT' && n.label === String.fromCharCode(charCode))) { charCode++; }
|
||||||
charCode++;
|
|
||||||
}
|
|
||||||
return String.fromCharCode(charCode);
|
return String.fromCharCode(charCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNextOutputLabel() {
|
function getNextOutputLabel() {
|
||||||
let idx = 1;
|
let idx = 1;
|
||||||
while (Object.values(nodes).some(n => n.type === 'OUTPUT' && n.label === ('Q' + idx))) {
|
while (Object.values(nodes).some(n => n.type === 'OUTPUT' && n.label === ('Q' + idx))) { idx++; }
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
return 'Q' + idx;
|
return 'Q' + idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Node Creation --- */
|
/* --- Node Creation --- */
|
||||||
function createNodeElement(node) {
|
function createNodeElement(node) {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = `lg-node`;
|
el.className = `lg-node`; el.dataset.id = node.id;
|
||||||
el.dataset.id = node.id;
|
el.style.left = `${node.x}px`; el.style.top = `${node.y}px`;
|
||||||
el.style.left = `${node.x}px`;
|
|
||||||
el.style.top = `${node.y}px`;
|
|
||||||
|
|
||||||
let innerHTML = `<div class="lg-header">${node.label}</div><div class="lg-gate-container">`;
|
let innerHTML = `<div class="lg-header">${node.label}</div><div class="lg-gate-container">`;
|
||||||
|
|
||||||
@@ -281,12 +263,12 @@
|
|||||||
innerHTML += `
|
innerHTML += `
|
||||||
<div class="switch" style="margin:0;"><span class="slider"></span></div>
|
<div class="switch" style="margin:0;"><span class="slider"></span></div>
|
||||||
${INPUT_SVG}
|
${INPUT_SVG}
|
||||||
<div class="lg-port port-out" data-port="out" style="top: 25px; left: 86px;"></div>
|
<div class="lg-port" data-port="out" style="top: 25px; left: 86px;"></div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
else if (node.type === 'OUTPUT') {
|
else if (node.type === 'OUTPUT') {
|
||||||
innerHTML += `
|
innerHTML += `
|
||||||
<div class="lg-port port-in-1" data-port="in1" style="top: 25px; left: 0;"></div>
|
<div class="lg-port" data-port="in1" style="top: 25px; left: 0;"></div>
|
||||||
${OUTPUT_SVG}
|
${OUTPUT_SVG}
|
||||||
<div class="bulb" style="margin:0;"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/></svg></div>
|
<div class="bulb" style="margin:0;"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/></svg></div>
|
||||||
`;
|
`;
|
||||||
@@ -294,34 +276,33 @@
|
|||||||
else if (node.type === 'GATE') {
|
else if (node.type === 'GATE') {
|
||||||
const isNot = node.gateType === 'NOT';
|
const isNot = node.gateType === 'NOT';
|
||||||
innerHTML += `
|
innerHTML += `
|
||||||
<div class="lg-port port-in-1" data-port="in1" style="top: ${isNot ? '25px' : '15px'}; left: 0;"></div>
|
<div class="lg-port" data-port="in1" style="top: ${isNot ? '25px' : '15px'}; left: 0;"></div>
|
||||||
${!isNot ? `<div class="lg-port port-in-2" data-port="in2" style="top: 35px; left: 0;"></div>` : ''}
|
${!isNot ? `<div class="lg-port" data-port="in2" style="top: 35px; left: 0;"></div>` : ''}
|
||||||
<svg class="lg-gate-svg" viewBox="0 0 100 50">${GATE_SVGS[node.gateType]}</svg>
|
<svg class="lg-gate-svg" viewBox="0 0 100 50">${GATE_SVGS[node.gateType]}</svg>
|
||||||
<div class="lg-port port-out" data-port="out" style="top: 25px; left: 100px;"></div>
|
<div class="lg-port" data-port="out" style="top: 25px; left: 100px;"></div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
innerHTML += `</div>`;
|
innerHTML += `</div>`;
|
||||||
el.innerHTML = innerHTML;
|
el.innerHTML = innerHTML;
|
||||||
workspace.appendChild(el);
|
|
||||||
|
viewport.appendChild(el);
|
||||||
node.el = el;
|
node.el = el;
|
||||||
|
|
||||||
if (node.type === 'INPUT') {
|
if (node.type === 'INPUT') {
|
||||||
el.querySelector('.switch').addEventListener('click', (e) => {
|
el.querySelector('.switch').addEventListener('click', (e) => {
|
||||||
const dist = Math.hypot(e.clientX - clickStartX, e.clientY - clickStartY);
|
const dist = Math.hypot(e.clientX - clickStartX, e.clientY - clickStartY);
|
||||||
if (isDraggingNode || dist > 3) {
|
if (dist > 3) {
|
||||||
e.preventDefault();
|
e.preventDefault(); // Prevents toggle if it was a drag motion
|
||||||
} else {
|
} else {
|
||||||
node.value = !node.value;
|
node.value = !node.value;
|
||||||
el.querySelector('.switch').classList.toggle('active-sim', 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.background = node.value ? 'rgba(40,240,122,.25)' : '';
|
||||||
el.querySelector('.slider').style.borderColor = node.value ? 'rgba(40,240,122,.30)' : '';
|
el.querySelector('.slider').style.borderColor = node.value ? 'rgba(40,240,122,.30)' : '';
|
||||||
el.querySelector('.slider').innerHTML = node.value ? `<style>#${node.id} .slider::before { transform: translateX(28px); }</style>` : '';
|
el.querySelector('.slider').innerHTML = node.value ? `<style>#logicPage [data-id="${node.id}"] .slider::before { transform: translateX(28px); }</style>` : '';
|
||||||
runSimulation();
|
runSimulation();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +313,6 @@
|
|||||||
if (type === 'GATE') label = gateType;
|
if (type === 'GATE') label = gateType;
|
||||||
|
|
||||||
const id = `node_${nextNodeId++}`;
|
const id = `node_${nextNodeId++}`;
|
||||||
|
|
||||||
const offset = Math.floor(Math.random() * 40);
|
const offset = Math.floor(Math.random() * 40);
|
||||||
const x = dropX !== null ? dropX : (type === 'INPUT' ? 50 : (type === 'OUTPUT' ? 600 : 300) + offset);
|
const x = dropX !== null ? dropX : (type === 'INPUT' ? 50 : (type === 'OUTPUT' ? 600 : 300) + offset);
|
||||||
const y = dropY !== null ? dropY : 150 + offset;
|
const y = dropY !== null ? dropY : 150 + offset;
|
||||||
@@ -345,9 +325,26 @@
|
|||||||
|
|
||||||
/* --- Global Interaction Handlers --- */
|
/* --- Global Interaction Handlers --- */
|
||||||
|
|
||||||
|
// Camera Zoom Controls
|
||||||
|
document.getElementById("btnZoomIn")?.addEventListener('click', () => {
|
||||||
|
const r = workspace.getBoundingClientRect(); zoomWorkspace(1.2, r.width/2, r.height/2);
|
||||||
|
});
|
||||||
|
document.getElementById("btnZoomOut")?.addEventListener('click', () => {
|
||||||
|
const r = workspace.getBoundingClientRect(); zoomWorkspace(1/1.2, r.width/2, r.height/2);
|
||||||
|
});
|
||||||
|
document.getElementById("btnZoomReset")?.addEventListener('click', () => {
|
||||||
|
panX = 0; panY = 0; zoom = 1; updateViewport();
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace.addEventListener('wheel', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const wsRect = workspace.getBoundingClientRect();
|
||||||
|
const factor = e.deltaY < 0 ? 1.1 : (1/1.1);
|
||||||
|
zoomWorkspace(factor, e.clientX - wsRect.left, e.clientY - wsRect.top);
|
||||||
|
});
|
||||||
|
|
||||||
workspace.addEventListener('mousedown', (e) => {
|
workspace.addEventListener('mousedown', (e) => {
|
||||||
clickStartX = e.clientX;
|
clickStartX = e.clientX; clickStartY = e.clientY;
|
||||||
clickStartY = e.clientY;
|
|
||||||
|
|
||||||
const port = e.target.closest('.lg-port');
|
const port = e.target.closest('.lg-port');
|
||||||
if (port) {
|
if (port) {
|
||||||
@@ -356,11 +353,7 @@
|
|||||||
|
|
||||||
if (portId.startsWith('in')) {
|
if (portId.startsWith('in')) {
|
||||||
const existingIdx = connections.findIndex(c => c.toNode === nodeEl.dataset.id && c.toPort === portId.replace('in', ''));
|
const existingIdx = connections.findIndex(c => c.toNode === nodeEl.dataset.id && c.toPort === portId.replace('in', ''));
|
||||||
if (existingIdx !== -1) {
|
if (existingIdx !== -1) { connections.splice(existingIdx, 1); runSimulation(); return; }
|
||||||
connections.splice(existingIdx, 1);
|
|
||||||
runSimulation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (portId === 'out') {
|
if (portId === 'out') {
|
||||||
@@ -385,32 +378,41 @@
|
|||||||
clearSelection();
|
clearSelection();
|
||||||
selectedNodeId = nodeEl.dataset.id;
|
selectedNodeId = nodeEl.dataset.id;
|
||||||
nodeEl.classList.add('selected');
|
nodeEl.classList.add('selected');
|
||||||
|
|
||||||
isDraggingNode = nodeEl.dataset.id;
|
isDraggingNode = nodeEl.dataset.id;
|
||||||
|
|
||||||
const rect = nodeEl.getBoundingClientRect();
|
const rect = nodeEl.getBoundingClientRect();
|
||||||
dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
dragOffset = { x: (e.clientX - rect.left) / zoom, y: (e.clientY - rect.top) / zoom };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clicked empty space -> Pan Camera
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
isPanning = true;
|
||||||
|
panStart = { x: e.clientX - panX, y: e.clientY - panY };
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('mousemove', (e) => {
|
window.addEventListener('mousemove', (e) => {
|
||||||
const wsRect = workspace.getBoundingClientRect();
|
const wsRect = workspace.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (isPanning) {
|
||||||
|
panX = e.clientX - panStart.x;
|
||||||
|
panY = e.clientY - panStart.y;
|
||||||
|
updateViewport();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isDraggingNode) {
|
if (isDraggingNode) {
|
||||||
const node = nodes[isDraggingNode];
|
const node = nodes[isDraggingNode];
|
||||||
let newX = e.clientX - wsRect.left - dragOffset.x;
|
let newX = (e.clientX - wsRect.left - panX) / zoom - dragOffset.x;
|
||||||
let newY = e.clientY - wsRect.top - dragOffset.y;
|
let newY = (e.clientY - wsRect.top - panY) / zoom - dragOffset.y;
|
||||||
node.x = Math.max(10, Math.min(newX, wsRect.width - 80));
|
node.x = newX; node.y = newY;
|
||||||
node.y = Math.max(20, Math.min(newY, wsRect.height - 60));
|
|
||||||
updateNodePositions();
|
updateNodePositions();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wiringStart) {
|
if (wiringStart) {
|
||||||
tempWirePath = {
|
tempWirePath = {
|
||||||
x: e.clientX - wsRect.left,
|
x: (e.clientX - wsRect.left - panX) / zoom,
|
||||||
y: e.clientY - wsRect.top
|
y: (e.clientY - wsRect.top - panY) / zoom
|
||||||
};
|
};
|
||||||
renderWires();
|
renderWires();
|
||||||
}
|
}
|
||||||
@@ -418,6 +420,7 @@
|
|||||||
|
|
||||||
window.addEventListener('mouseup', (e) => {
|
window.addEventListener('mouseup', (e) => {
|
||||||
isDraggingNode = null;
|
isDraggingNode = null;
|
||||||
|
isPanning = false;
|
||||||
|
|
||||||
if (wiringStart) {
|
if (wiringStart) {
|
||||||
const port = e.target.closest('.lg-port');
|
const port = e.target.closest('.lg-port');
|
||||||
@@ -427,18 +430,10 @@
|
|||||||
|
|
||||||
if (targetNodeId !== wiringStart.node) {
|
if (targetNodeId !== wiringStart.node) {
|
||||||
connections = connections.filter(c => !(c.toNode === targetNodeId && c.toPort === targetPortId));
|
connections = connections.filter(c => !(c.toNode === targetNodeId && c.toPort === targetPortId));
|
||||||
|
connections.push({ id: `conn_${nextWireId++}`, fromNode: wiringStart.node, fromPort: 'out', toNode: targetNodeId, toPort: targetPortId });
|
||||||
connections.push({
|
|
||||||
id: `conn_${nextWireId++}`,
|
|
||||||
fromNode: wiringStart.node,
|
|
||||||
fromPort: 'out',
|
|
||||||
toNode: targetNodeId,
|
|
||||||
toPort: targetPortId
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wiringStart = null;
|
wiringStart = null; tempWirePath = null;
|
||||||
tempWirePath = null;
|
|
||||||
runSimulation();
|
runSimulation();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -448,17 +443,15 @@
|
|||||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
if (selectedWireId) {
|
if (selectedWireId) {
|
||||||
connections = connections.filter(c => c.id !== selectedWireId);
|
connections = connections.filter(c => c.id !== selectedWireId);
|
||||||
clearSelection();
|
clearSelection(); runSimulation();
|
||||||
runSimulation();
|
|
||||||
}
|
}
|
||||||
else if (selectedNodeId) {
|
else if (selectedNodeId) {
|
||||||
connections = connections.filter(c => c.fromNode !== selectedNodeId && c.toNode !== selectedNodeId);
|
connections = connections.filter(c => c.fromNode !== selectedNodeId && c.toNode !== selectedNodeId);
|
||||||
if (nodes[selectedNodeId] && nodes[selectedNodeId].el) {
|
if (nodes[selectedNodeId] && nodes[selectedNodeId].el) {
|
||||||
workspace.removeChild(nodes[selectedNodeId].el);
|
viewport.removeChild(nodes[selectedNodeId].el);
|
||||||
}
|
}
|
||||||
delete nodes[selectedNodeId];
|
delete nodes[selectedNodeId];
|
||||||
clearSelection();
|
clearSelection(); runSimulation();
|
||||||
runSimulation();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -471,17 +464,16 @@
|
|||||||
if (spawnType) {
|
if (spawnType) {
|
||||||
const gateType = e.dataTransfer.getData('gateType');
|
const gateType = e.dataTransfer.getData('gateType');
|
||||||
const wsRect = workspace.getBoundingClientRect();
|
const wsRect = workspace.getBoundingClientRect();
|
||||||
const x = e.clientX - wsRect.left - 40;
|
const x = (e.clientX - wsRect.left - panX) / zoom - 40;
|
||||||
const y = e.clientY - wsRect.top - 30;
|
const y = (e.clientY - wsRect.top - panY) / zoom - 30;
|
||||||
spawnNode(spawnType, gateType || null, x, y);
|
spawnNode(spawnType, gateType || null, x, y);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* --- Init --- */
|
/* --- Init --- */
|
||||||
btnClearBoard?.addEventListener('click', () => {
|
btnClearBoard?.addEventListener('click', () => {
|
||||||
workspace.querySelectorAll('.lg-node').forEach(el => el.remove());
|
viewport.querySelectorAll('.lg-node').forEach(el => el.remove());
|
||||||
nodes = {};
|
nodes = {}; connections = [];
|
||||||
connections = [];
|
|
||||||
runSimulation();
|
runSimulation();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -493,5 +485,4 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
initToolbox();
|
initToolbox();
|
||||||
// Starts completely blank as requested!
|
|
||||||
})();
|
})();
|
||||||
@@ -1,161 +1,109 @@
|
|||||||
/* === FULL PAGE OVERRIDES FOR LOGIC GATES === */
|
/* === FULL PAGE OVERRIDES FOR LOGIC GATES === */
|
||||||
body:has(#logicPage) {
|
body:has(#logicPage) { overflow: hidden; }
|
||||||
overflow: hidden; /* Prevents the entire page from scrolling */
|
|
||||||
}
|
|
||||||
|
|
||||||
body:has(#logicPage) .pageWrap {
|
body:has(#logicPage) .pageWrap {
|
||||||
max-width: 100% !important; /* Forces edge-to-edge canvas */
|
max-width: 100% !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
height: calc(100vh - var(--nav-h));
|
height: calc(100vh - var(--nav-h));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
#logicPage { padding: 0 !important; margin: 0 !important; }
|
||||||
#logicPage {
|
|
||||||
padding: 0 !important; /* CRITICAL: Stops the page/header from shifting when toolbox opens */
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MAIN CONTAINER === */
|
/* === MAIN CONTAINER === */
|
||||||
.lg-container {
|
.lg-container {
|
||||||
flex: 1;
|
flex: 1; display: flex; flex-direction: column; position: relative;
|
||||||
display: flex;
|
width: 100%; height: 100%; overflow: hidden;
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === FIXED HEADER (Ultra Compact) === */
|
/* === FIXED HEADER === */
|
||||||
.lg-top-header {
|
.lg-top-header {
|
||||||
width: 100%;
|
width: 100%; text-align: center; padding: 8px 20px 8px;
|
||||||
text-align: center;
|
background: var(--bg); z-index: 10; flex-shrink: 0;
|
||||||
padding: 8px 20px 8px; /* Extremely tight padding to maximize canvas */
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
background: var(--bg);
|
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||||
z-index: 10;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.08); /* Clean separation line */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lg-title {
|
.lg-title {
|
||||||
font-family: var(--bit-font);
|
font-family: var(--bit-font); font-size: 32px; letter-spacing: 0.12em;
|
||||||
font-size: 32px;
|
text-transform: uppercase; color: var(--text); margin: 0 0 2px 0; line-height: 1;
|
||||||
letter-spacing: 0.12em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--text);
|
|
||||||
margin: 0 0 2px 0; /* Minimal gap between title and subtitle */
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lg-subtitle {
|
.lg-subtitle {
|
||||||
color: var(--muted);
|
color: var(--muted); font-size: 14px; font-family: var(--ui-font);
|
||||||
font-size: 14px;
|
font-weight: 500; margin: 0; line-height: 1.2;
|
||||||
font-family: var(--ui-font);
|
|
||||||
font-weight: 500;
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lg-subtitle kbd {
|
.lg-subtitle kbd {
|
||||||
background: rgba(255,255,255,0.1);
|
background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px;
|
||||||
padding: 2px 6px;
|
font-family: var(--ui-font); color: #e8e8ee;
|
||||||
border-radius: 4px;
|
|
||||||
font-family: var(--ui-font);
|
|
||||||
color: #e8e8ee;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === DYNAMIC CANVAS === */
|
/* === DYNAMIC CANVAS & CAMERA VIEWPORT === */
|
||||||
.lg-workspace {
|
.lg-workspace {
|
||||||
flex: 1; /* Automatically fills all remaining vertical space */
|
flex: 1; position: relative; width: 100%;
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
background-image: radial-gradient(rgba(255,255,255,0.12) 1px, transparent 1px);
|
background-image: radial-gradient(rgba(255,255,255,0.15) 1px, transparent 1px);
|
||||||
background-size: 24px 24px;
|
background-size: 24px 24px; overflow: hidden;
|
||||||
overflow: hidden;
|
cursor: grab; /* Indicates pannable area */
|
||||||
}
|
}
|
||||||
|
.lg-workspace:active { cursor: grabbing; }
|
||||||
|
|
||||||
.lg-svg-layer {
|
.lg-viewport {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
transform-origin: 0 0;
|
||||||
z-index: 1;
|
pointer-events: none; /* Let events reach workspace, nodes will re-enable it */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zoom UI Controls */
|
||||||
|
.lg-zoom-controls {
|
||||||
|
position: absolute; bottom: 20px; left: 20px; z-index: 100;
|
||||||
|
display: flex; gap: 8px;
|
||||||
|
}
|
||||||
|
.lg-zoom-btn {
|
||||||
|
width: 40px; height: 40px; border-radius: 8px; font-family: var(--ui-font);
|
||||||
|
font-size: 22px; font-weight: 800; display: flex; align-items: center; justify-content: center;
|
||||||
|
background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
color: #e8e8ee; cursor: pointer; backdrop-filter: blur(8px); transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.lg-zoom-btn:hover { background: rgba(255,255,255,0.1); border-color: #28f07a; color: #28f07a; }
|
||||||
|
|
||||||
|
.lg-svg-layer {
|
||||||
|
position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Wires */
|
/* Wires */
|
||||||
.lg-wire {
|
.lg-wire {
|
||||||
stroke: rgba(255,255,255,0.25);
|
stroke: rgba(255,255,255,0.25); stroke-width: 6; fill: none; stroke-linecap: round;
|
||||||
stroke-width: 6;
|
|
||||||
fill: none;
|
|
||||||
stroke-linecap: round;
|
|
||||||
transition: stroke 0.1s ease, filter 0.1s ease, stroke-width 0.1s ease;
|
transition: stroke 0.1s ease, filter 0.1s ease, stroke-width 0.1s ease;
|
||||||
pointer-events: stroke;
|
pointer-events: stroke; cursor: pointer;
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.lg-wire:hover {
|
|
||||||
stroke: rgba(255,255,255,0.6);
|
|
||||||
stroke-width: 10;
|
|
||||||
}
|
|
||||||
.lg-wire.active {
|
|
||||||
stroke: #28f07a;
|
|
||||||
filter: drop-shadow(0 0 6px rgba(40,240,122,0.6));
|
|
||||||
}
|
}
|
||||||
|
.lg-wire:hover { stroke: rgba(255,255,255,0.6); stroke-width: 10; }
|
||||||
|
.lg-wire.active { stroke: #28f07a; filter: drop-shadow(0 0 6px rgba(40,240,122,0.6)); }
|
||||||
.lg-wire.active:hover { stroke: #5dff9e; }
|
.lg-wire.active:hover { stroke: #5dff9e; }
|
||||||
.lg-wire.selected {
|
.lg-wire.selected {
|
||||||
stroke: #ff5555 !important;
|
stroke: #ff5555 !important; stroke-width: 8 !important; stroke-dasharray: 8 8;
|
||||||
stroke-width: 8 !important;
|
filter: drop-shadow(0 0 8px rgba(255,85,85,0.8)) !important; animation: wireDash 1s linear infinite;
|
||||||
stroke-dasharray: 8 8;
|
|
||||||
filter: drop-shadow(0 0 8px rgba(255,85,85,0.8)) !important;
|
|
||||||
animation: wireDash 1s linear infinite;
|
|
||||||
}
|
}
|
||||||
@keyframes wireDash { to { stroke-dashoffset: -16; } }
|
@keyframes wireDash { to { stroke-dashoffset: -16; } }
|
||||||
|
.lg-wire-temp { stroke: rgba(255,255,255,0.4); stroke-dasharray: 8 8; pointer-events: none; }
|
||||||
.lg-wire-temp {
|
|
||||||
stroke: rgba(255,255,255,0.4);
|
|
||||||
stroke-dasharray: 8 8;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Nodes */
|
/* Nodes */
|
||||||
.lg-node {
|
.lg-node {
|
||||||
position: absolute;
|
position: absolute; background: transparent; border: none; border-radius: 0; padding: 4px;
|
||||||
background: transparent;
|
display: flex; flex-direction: column; align-items: center; cursor: grab;
|
||||||
border: none;
|
z-index: 10; user-select: none; transition: filter 0.2s;
|
||||||
border-radius: 0;
|
pointer-events: auto; /* Re-enables interaction inside the viewport */
|
||||||
padding: 4px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
cursor: grab;
|
|
||||||
z-index: 10;
|
|
||||||
user-select: none;
|
|
||||||
transition: filter 0.2s;
|
|
||||||
}
|
}
|
||||||
.lg-node:active { cursor: grabbing; z-index: 20; }
|
.lg-node:active { cursor: grabbing; z-index: 20; }
|
||||||
.lg-node.selected { filter: drop-shadow(0 0 10px rgba(255,85,85,0.8)); }
|
.lg-node.selected { filter: drop-shadow(0 0 10px rgba(255,85,85,0.8)); }
|
||||||
|
|
||||||
.lg-header {
|
.lg-header {
|
||||||
font-size: 24px;
|
font-size: 24px; color: var(--muted); font-family: var(--bit-font);
|
||||||
color: var(--muted);
|
letter-spacing: 2px; pointer-events: none; margin-bottom: 6px;
|
||||||
font-family: var(--bit-font);
|
|
||||||
letter-spacing: 2px;
|
|
||||||
pointer-events: none;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lg-gate-container {
|
.lg-gate-container { position: relative; display: inline-flex; align-items: center; }
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.lg-gate-svg { width: 100px; height: 50px; display: block; }
|
.lg-gate-svg { width: 100px; height: 50px; display: block; }
|
||||||
.lg-line-svg { width: 30px; height: 50px; display: block; }
|
.lg-line-svg { width: 30px; height: 50px; display: block; }
|
||||||
|
|
||||||
@@ -163,18 +111,14 @@ body:has(#logicPage) .pageWrap {
|
|||||||
.lg-port {
|
.lg-port {
|
||||||
width: 16px; height: 16px; background: #a9acb8; border-radius: 50%; cursor: crosshair;
|
width: 16px; height: 16px; background: #a9acb8; border-radius: 50%; cursor: crosshair;
|
||||||
border: 3px solid var(--bg); box-shadow: 0 0 0 1px rgba(255,255,255,0.2); transition: all 0.2s;
|
border: 3px solid var(--bg); box-shadow: 0 0 0 1px rgba(255,255,255,0.2); transition: all 0.2s;
|
||||||
position: absolute; z-index: 5;
|
position: absolute; z-index: 5; transform: translate(-50%, -50%);
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
}
|
||||||
.lg-port:hover { transform: translate(-50%, -50%) scale(1.3); background: #fff; }
|
.lg-port:hover { transform: translate(-50%, -50%) scale(1.3); background: #fff; }
|
||||||
.lg-port.active { background: #28f07a; box-shadow: 0 0 12px rgba(40,240,122,0.8); border-color: #1f2027; }
|
.lg-port.active { background: #28f07a; box-shadow: 0 0 12px rgba(40,240,122,0.8); border-color: #1f2027; }
|
||||||
|
|
||||||
/* === FLOATING TOOLBOX === */
|
/* === FLOATING TOOLBOX === */
|
||||||
.toolboxToggle {
|
.toolboxToggle {
|
||||||
position: absolute;
|
position: absolute; top: 10px; right: 20px; z-index: 90;
|
||||||
top: 10px; /* Snug inside the thinner header */
|
|
||||||
right: 20px;
|
|
||||||
z-index: 90;
|
|
||||||
display: flex; align-items: center; gap: 10px; padding: 8px 14px;
|
display: flex; align-items: center; gap: 10px; padding: 8px 14px;
|
||||||
border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.15);
|
border-radius: 12px; border: 1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.15);
|
||||||
backdrop-filter: blur(8px); color: rgba(232,232,238,.95); font-family: var(--bit-font);
|
backdrop-filter: blur(8px); color: rgba(232,232,238,.95); font-family: var(--bit-font);
|
||||||
@@ -184,53 +128,29 @@ body:has(#logicPage) .pageWrap {
|
|||||||
.toolboxToggle:hover { border-color: rgba(255,255,255,.22); }
|
.toolboxToggle:hover { border-color: rgba(255,255,255,.22); }
|
||||||
|
|
||||||
.lg-toolbox {
|
.lg-toolbox {
|
||||||
position: absolute;
|
position: absolute; top: 60px; right: 20px; bottom: 20px; width: var(--toolbox-w, 360px);
|
||||||
top: 60px; /* Sits right under the new thin header */
|
z-index: 80; display: flex; flex-direction: column; gap: 16px; transform: translateX(0);
|
||||||
right: 20px;
|
|
||||||
bottom: 20px; /* Constrains the height so it scrolls internally */
|
|
||||||
width: var(--toolbox-w, 360px);
|
|
||||||
z-index: 80;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
transform: translateX(0);
|
|
||||||
transition: transform 420ms cubic-bezier(.2,.9,.2,1), opacity 220ms ease;
|
transition: transform 420ms cubic-bezier(.2,.9,.2,1), opacity 220ms ease;
|
||||||
overflow-y: auto;
|
overflow-y: auto; pointer-events: auto; padding-right: 6px;
|
||||||
pointer-events: auto;
|
|
||||||
padding-right: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Faded Subdued Scrollbars */
|
|
||||||
.lg-toolbox::-webkit-scrollbar { width: 6px; }
|
.lg-toolbox::-webkit-scrollbar { width: 6px; }
|
||||||
.lg-toolbox::-webkit-scrollbar-track { background: transparent; }
|
.lg-toolbox::-webkit-scrollbar-track { background: transparent; }
|
||||||
.lg-toolbox::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.05); border-radius: 10px; transition: background 0.3s; }
|
.lg-toolbox::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.05); border-radius: 10px; transition: background 0.3s; }
|
||||||
.lg-toolbox:hover::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); }
|
.lg-toolbox:hover::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); }
|
||||||
.lg-toolbox::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.4); }
|
.lg-container.toolboxCollapsed .lg-toolbox { transform: translateX(calc(100% + 40px)); opacity: 0; pointer-events: none; }
|
||||||
|
|
||||||
.lg-container.toolboxCollapsed .lg-toolbox {
|
.tb-icon-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||||
transform: translateX(calc(100% + 40px));
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toolbox Grid */
|
|
||||||
.tb-icon-grid {
|
|
||||||
display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
|
|
||||||
}
|
|
||||||
.tb-icon-box {
|
.tb-icon-box {
|
||||||
background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.15);
|
background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.15); border-radius: 12px; width: 100%; padding: 12px 0;
|
||||||
border-radius: 12px; width: 100%; padding: 12px 0;
|
|
||||||
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
|
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
|
||||||
cursor: grab; transition: background 0.2s, border-color 0.2s;
|
cursor: grab; transition: background 0.2s, border-color 0.2s;
|
||||||
}
|
}
|
||||||
.tb-icon-box:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3); }
|
.tb-icon-box:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3); }
|
||||||
.tb-icon-label { font-family: var(--ui-font); font-size: 11px; font-weight: 800; color: var(--text); letter-spacing: 1px; text-transform: uppercase; }
|
.tb-icon-label { font-family: var(--ui-font); font-size: 11px; font-weight: 800; color: var(--text); letter-spacing: 1px; text-transform: uppercase; }
|
||||||
|
|
||||||
/* Truth Table */
|
|
||||||
.tt-summary {
|
.tt-summary {
|
||||||
font-family: var(--ui-font); font-size: 14px; font-weight: 800;
|
font-family: var(--ui-font); font-size: 14px; font-weight: 800; color: var(--accent, #28f07a);
|
||||||
color: var(--accent, #28f07a); cursor: pointer; user-select: none;
|
cursor: pointer; user-select: none; outline: none; margin-bottom: 10px; text-transform: uppercase;
|
||||||
outline: none; margin-bottom: 10px; text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
.tt-table-wrap {
|
.tt-table-wrap {
|
||||||
width: 100%; max-height: 250px; overflow-y: auto; overflow-x: auto;
|
width: 100%; max-height: 250px; overflow-y: auto; overflow-x: auto;
|
||||||
@@ -239,8 +159,6 @@ body:has(#logicPage) .pageWrap {
|
|||||||
.tt-table-wrap::-webkit-scrollbar { width: 6px; height: 6px; }
|
.tt-table-wrap::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
.tt-table-wrap::-webkit-scrollbar-track { background: transparent; }
|
.tt-table-wrap::-webkit-scrollbar-track { background: transparent; }
|
||||||
.tt-table-wrap::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
|
.tt-table-wrap::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
|
||||||
.tt-table-wrap:hover::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.3); }
|
|
||||||
|
|
||||||
.tt-table { width: 100%; border-collapse: collapse; text-align: center; font-family: var(--num-font); font-size: 14px; color: #e8e8ee; }
|
.tt-table { width: 100%; border-collapse: collapse; text-align: center; font-family: var(--num-font); font-size: 14px; color: #e8e8ee; }
|
||||||
.tt-table th { position: sticky; top: 0; background: rgba(31,32,39,0.95); padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.15); color: var(--muted); font-family: var(--bit-font); font-weight: normal; }
|
.tt-table th { position: sticky; top: 0; background: rgba(31,32,39,0.95); padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.15); color: var(--muted); font-family: var(--bit-font); font-weight: normal; }
|
||||||
.tt-table td { padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.05); }
|
.tt-table td { padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.05); }
|
||||||
|
|||||||
Reference in New Issue
Block a user