Connect outputs to inputs to build circuits. Toggle inputs to test behavior.
35 | 36 | 37 |├── client ├── js │ ├── dom-ready.js │ ├── sim │ │ ├── constants.js │ │ ├── geometry.js │ │ ├── custom-gate-utils.js │ │ ├── vhdl.js │ │ └── snapshot.js │ ├── help-loader.js │ ├── websocket-bridge.js │ └── status-controller.js ├── resources │ ├── gates │ │ ├── input.svg │ │ ├── output.svg │ │ ├── buffer.svg │ │ ├── and.svg │ │ ├── not.svg │ │ ├── nand.svg │ │ ├── or.svg │ │ ├── xor.svg │ │ └── nor.svg │ └── rotate.svg ├── gate-config.json ├── app.js ├── custom-gates │ ├── README.md │ ├── 3AND.json │ └── SR Latch.json ├── initial_state.json ├── index.html ├── help-modal.js ├── help-content-template.html ├── gate-registry.js ├── logic-sim.css └── bespoke.css ├── .gitignore ├── user.vhdl ├── package.json ├── state.json ├── scripts └── export-task-state.js ├── LICENSE ├── test-integration.html ├── AGENTS.md ├── README.md ├── vhdl-serializer.js ├── server.js └── circuit-report.js /client/js/dom-ready.js: -------------------------------------------------------------------------------- 1 | export function onDocumentReady(callback) { 2 | if (document.readyState === 'loading') { 3 | document.addEventListener('DOMContentLoaded', callback, { once: true }); 4 | } else { 5 | callback(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # http://www.westwind.com/reference/os-x/invisibles.html 2 | .DS_Store 3 | .Trashes 4 | *.swp 5 | 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 11 | node_modules -------------------------------------------------------------------------------- /client/resources/gates/input.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/resources/gates/output.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/resources/gates/buffer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/resources/rotate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/resources/gates/and.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/resources/gates/not.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /user.vhdl: -------------------------------------------------------------------------------- 1 | -- Logic Circuit Lab export 2 | 3 | library IEEE; 4 | use IEEE.STD_LOGIC_1164.ALL; 5 | 6 | entity logic_circuit_lab is 7 | port ( 8 | test : out STD_LOGIC 9 | ); 10 | end entity logic_circuit_lab; 11 | 12 | architecture behavioral of logic_circuit_lab is 13 | signal and_g1_0 : STD_LOGIC; 14 | signal a : STD_LOGIC; 15 | signal b : STD_LOGIC; 16 | begin 17 | and_g1_0 <= (a) and (b); -- AND 18 | a <= '1'; -- Input A 19 | b <= '0'; -- Input B 20 | test <= and_g1_0; -- Output Test 21 | end architecture behavioral; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logic-circuit-lab", 3 | "version": "0.0.1", 4 | "description": "Embeddable Bespoke-powered logic circuit playground with VHDL exports", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "node server.js", 9 | "export:task": "node scripts/export-task-state.js" 10 | }, 11 | "keywords": [ 12 | "logic", 13 | "circuit", 14 | "playground", 15 | "bespoke", 16 | "vhdl" 17 | ], 18 | "author": "", 19 | "license": "MIT", 20 | "dependencies": { 21 | "ws": "^8.14.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/resources/gates/nand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/gate-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultZoom": 1.5, 3 | "paletteOrder": [ 4 | "input", 5 | "output", 6 | "buffer", 7 | "not", 8 | "and", 9 | "nand", 10 | "or", 11 | "nor", 12 | "xor" 13 | ], 14 | "exportReport": { 15 | "enabled": true, 16 | "sections": { 17 | "summary": true, 18 | "gateCounts": true, 19 | "gatePositions": true, 20 | "spatialMetrics": true, 21 | "connectionSummary": true, 22 | "floatingPins": true, 23 | "truthTable": true 24 | }, 25 | "truthTable": { 26 | "maxInputs": 6, 27 | "maxRows": 64 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/resources/gates/or.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/resources/gates/xor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/resources/gates/nor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | import { onDocumentReady } from './js/dom-ready.js'; 2 | import { createStatusController } from './js/status-controller.js'; 3 | import { initializeHelpModal } from './js/help-loader.js'; 4 | import { initializeWebSocketBridge } from './js/websocket-bridge.js'; 5 | 6 | import './gate-registry.js'; 7 | import './logic-sim.js'; 8 | 9 | const statusController = createStatusController(); 10 | window.setStatus = statusController.setStatus; 11 | 12 | onDocumentReady(async () => { 13 | statusController.attach(document.getElementById('status')); 14 | 15 | await initializeHelpModal({ 16 | triggerSelector: '#btn-help', 17 | statusController 18 | }); 19 | 20 | initializeWebSocketBridge({ 21 | onExportRequest: () => { 22 | if (window.logicSim && typeof window.logicSim.exportToVhdl === 'function') { 23 | window.logicSim.exportToVhdl(); 24 | } 25 | } 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /client/js/sim/constants.js: -------------------------------------------------------------------------------- 1 | export const STORAGE_KEY = 'logic-circuit-lab-state-v1'; 2 | export const BASE_ICON_SIZE = 64; 3 | export const WORKSPACE_SIZE = 6000; 4 | export const DEFAULT_SCALE = 1.5; 5 | export const MIN_SCALE = 0.5; 6 | export const MAX_SCALE = 2.75; 7 | export const PORT_SIZE = 8; 8 | export const GRID_SIZE = 16; 9 | export const SAVE_DEBOUNCE = 500; 10 | export const RETRY_DELAY = 3000; 11 | export const GATE_SCALE = 1; 12 | export const GATE_PIXEL_SIZE = BASE_ICON_SIZE * GATE_SCALE; 13 | export const HALF_WORKSPACE = WORKSPACE_SIZE / 2; 14 | export const WORLD_MIN_X = -HALF_WORKSPACE; 15 | export const WORLD_MAX_X = HALF_WORKSPACE - GATE_PIXEL_SIZE; 16 | export const WORLD_MIN_Y = -HALF_WORKSPACE; 17 | export const WORLD_MAX_Y = HALF_WORKSPACE - GATE_PIXEL_SIZE; 18 | export const COORDINATE_VERSION = 2; 19 | export const ROTATION_STEP = 90; 20 | export const DEFAULT_GATE_DIMENSIONS = { width: BASE_ICON_SIZE, height: BASE_ICON_SIZE }; 21 | export const CUSTOM_GATE_PORT_SPACING = GRID_SIZE; 22 | -------------------------------------------------------------------------------- /client/custom-gates/README.md: -------------------------------------------------------------------------------- 1 | # Custom Gates Folder 2 | 3 | Drop reusable JSON snapshots (same schema as `state.json`) here to make them available in the Logic Circuit Lab palette. 4 | 5 | - Each file must include the normal `gates`, `connections`, and `nextId` fields. 6 | - Use `input` gates to define the custom gate’s pins (labels will become the port names) and `output` gates to expose the outputs. 7 | - Files are discovered automatically when the client loads, so placing a new JSON file here instantly registers a new reusable custom gate without having to rebuild it on the canvas. 8 | - Add an optional `description` string at the top level to control the text shown in the inspector/context menu for that gate (otherwise the app generates one automatically). 9 | - You can also include an optional `customVhdl` string to describe how this gate should be emitted during a VHDL export. Use template placeholders like `{{input:0}}`, `{{output:0}}`, `{{gateId}}`, `{{label}}`, and `{{type}}` to reference the resolved signal names. 10 | -------------------------------------------------------------------------------- /client/js/help-loader.js: -------------------------------------------------------------------------------- 1 | import HelpModal from '../help-modal.js'; 2 | 3 | async function fetchHelpContent() { 4 | const response = await fetch('./help-content-template.html', { cache: 'no-store' }); 5 | if (!response.ok) { 6 | throw new Error(`Unexpected response: ${response.status}`); 7 | } 8 | return response.text(); 9 | } 10 | 11 | export async function initializeHelpModal({ triggerSelector = '#btn-help', statusController } = {}) { 12 | const setStatus = statusController?.setStatus || (() => {}); 13 | setStatus('Loading...'); 14 | 15 | try { 16 | const content = await fetchHelpContent(); 17 | HelpModal.init({ 18 | triggerSelector, 19 | content, 20 | theme: 'auto' 21 | }); 22 | setStatus('Ready'); 23 | } catch (error) { 24 | console.error('Failed to load help content:', error); 25 | HelpModal.init({ 26 | triggerSelector, 27 | content: '
Help content could not be loaded. Please try again later.
', 28 | theme: 'auto' 29 | }); 30 | setStatus('Failed to load data', { revertDelay: 3000 }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/js/websocket-bridge.js: -------------------------------------------------------------------------------- 1 | function getWebSocketUrl() { 2 | const isSecure = window.location.protocol === 'https:'; 3 | const protocol = isSecure ? 'wss' : 'ws'; 4 | return `${protocol}://${window.location.host}`; 5 | } 6 | 7 | export function initializeWebSocketBridge({ onExportRequest } = {}) { 8 | if (typeof window.WebSocket !== 'function') { 9 | console.warn('WebSocket is not supported in this browser; export bridge disabled.'); 10 | return null; 11 | } 12 | 13 | let socket; 14 | try { 15 | socket = new WebSocket(getWebSocketUrl()); 16 | } catch (error) { 17 | console.error('Failed to establish WebSocket connection:', error); 18 | return null; 19 | } 20 | 21 | socket.addEventListener('message', (event) => { 22 | try { 23 | const payload = JSON.parse(event.data); 24 | if (payload?.type === 'logic-sim:export-vhdl' && typeof onExportRequest === 'function') { 25 | onExportRequest(); 26 | } 27 | } catch (error) { 28 | console.error('Failed to handle WebSocket message:', error); 29 | } 30 | }); 31 | 32 | socket.addEventListener('error', (error) => { 33 | console.error('WebSocket error:', error); 34 | }); 35 | 36 | return () => { 37 | if (socket && socket.readyState === WebSocket.OPEN) { 38 | socket.close(); 39 | } 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /client/js/status-controller.js: -------------------------------------------------------------------------------- 1 | const allowedStatuses = new Set([ 2 | 'Ready', 3 | 'Loading...', 4 | 'Saving...', 5 | 'Changes saved', 6 | 'Save failed (will retry)', 7 | 'Failed to load data', 8 | 'Auto-save initialized' 9 | ]); 10 | 11 | export function createStatusController(initialElement = null) { 12 | let statusElement = initialElement; 13 | let revertTimer = null; 14 | let lastMessage = 'Ready'; 15 | 16 | const applyStatus = (message) => { 17 | lastMessage = message; 18 | if (statusElement) { 19 | statusElement.textContent = message; 20 | } 21 | }; 22 | 23 | const setStatus = (message, options = {}) => { 24 | if (!allowedStatuses.has(message)) { 25 | console.warn(`Ignoring unsupported status message: ${message}`); 26 | return; 27 | } 28 | 29 | if (revertTimer) { 30 | window.clearTimeout(revertTimer); 31 | revertTimer = null; 32 | } 33 | 34 | applyStatus(message); 35 | 36 | if (message !== 'Ready' && typeof options.revertDelay === 'number') { 37 | revertTimer = window.setTimeout(() => { 38 | applyStatus('Ready'); 39 | revertTimer = null; 40 | }, options.revertDelay); 41 | } 42 | }; 43 | 44 | const attach = (element) => { 45 | statusElement = element || null; 46 | if (statusElement) { 47 | statusElement.textContent = lastMessage; 48 | } 49 | }; 50 | 51 | return { setStatus, attach }; 52 | } 53 | -------------------------------------------------------------------------------- /state.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "origin": "center", 4 | "nextId": 11, 5 | "gates": [ 6 | { 7 | "id": "g1", 8 | "type": "and", 9 | "x": 8, 10 | "y": -152, 11 | "label": "", 12 | "state": 0 13 | }, 14 | { 15 | "id": "g6", 16 | "type": "input", 17 | "x": -120, 18 | "y": -200, 19 | "label": "A", 20 | "state": 1 21 | }, 22 | { 23 | "id": "g7", 24 | "type": "input", 25 | "x": -120, 26 | "y": -104, 27 | "label": "B", 28 | "state": 0 29 | }, 30 | { 31 | "id": "g8", 32 | "type": "output", 33 | "x": 136, 34 | "y": -152, 35 | "label": "Test", 36 | "state": 0 37 | } 38 | ], 39 | "connections": [ 40 | { 41 | "id": "cmhm63wgts4zi", 42 | "from": { 43 | "gateId": "g6", 44 | "portIndex": 0 45 | }, 46 | "to": { 47 | "gateId": "g1", 48 | "portIndex": 0 49 | } 50 | }, 51 | { 52 | "id": "cmhm63xtqzdqc", 53 | "from": { 54 | "gateId": "g7", 55 | "portIndex": 0 56 | }, 57 | "to": { 58 | "gateId": "g1", 59 | "portIndex": 1 60 | } 61 | }, 62 | { 63 | "id": "cmhm63zqffhpw", 64 | "from": { 65 | "gateId": "g1", 66 | "portIndex": 0 67 | }, 68 | "to": { 69 | "gateId": "g8", 70 | "portIndex": 0 71 | } 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /client/initial_state.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "origin": "center", 4 | "nextId": 11, 5 | "gates": [ 6 | { 7 | "id": "g1", 8 | "type": "and", 9 | "x": 8, 10 | "y": -152, 11 | "label": "", 12 | "state": 0 13 | }, 14 | { 15 | "id": "g6", 16 | "type": "input", 17 | "x": -120, 18 | "y": -200, 19 | "label": "A", 20 | "state": 1 21 | }, 22 | { 23 | "id": "g7", 24 | "type": "input", 25 | "x": -120, 26 | "y": -104, 27 | "label": "B", 28 | "state": 0 29 | }, 30 | { 31 | "id": "g8", 32 | "type": "output", 33 | "x": 136, 34 | "y": -152, 35 | "label": "Test", 36 | "state": 0 37 | } 38 | ], 39 | "connections": [ 40 | { 41 | "id": "cmhm63wgts4zi", 42 | "from": { 43 | "gateId": "g6", 44 | "portIndex": 0 45 | }, 46 | "to": { 47 | "gateId": "g1", 48 | "portIndex": 0 49 | } 50 | }, 51 | { 52 | "id": "cmhm63xtqzdqc", 53 | "from": { 54 | "gateId": "g7", 55 | "portIndex": 0 56 | }, 57 | "to": { 58 | "gateId": "g1", 59 | "portIndex": 1 60 | } 61 | }, 62 | { 63 | "id": "cmhm63zqffhpw", 64 | "from": { 65 | "gateId": "g1", 66 | "portIndex": 0 67 | }, 68 | "to": { 69 | "gateId": "g8", 70 | "portIndex": 0 71 | } 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /client/custom-gates/3AND.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "description": "Outputs 1 only when inputs A, B, and C are all high.", 4 | "origin": "center", 5 | "nextId": 7, 6 | "gates": [ 7 | { 8 | "id": "g1", 9 | "type": "input", 10 | "x": -320, 11 | "y": -64, 12 | "label": "A", 13 | "state": 0 14 | }, 15 | { 16 | "id": "g2", 17 | "type": "input", 18 | "x": -320, 19 | "y": 0, 20 | "label": "B", 21 | "state": 0 22 | }, 23 | { 24 | "id": "g3", 25 | "type": "input", 26 | "x": -320, 27 | "y": 64, 28 | "label": "C", 29 | "state": 0 30 | }, 31 | { 32 | "id": "g4", 33 | "type": "and", 34 | "x": -128, 35 | "y": -32, 36 | "label": "" 37 | }, 38 | { 39 | "id": "g5", 40 | "type": "and", 41 | "x": 32, 42 | "y": -32, 43 | "label": "" 44 | }, 45 | { 46 | "id": "g6", 47 | "type": "output", 48 | "x": 224, 49 | "y": -32, 50 | "label": "OUT" 51 | } 52 | ], 53 | "connections": [ 54 | { 55 | "id": "c1", 56 | "from": { "gateId": "g1", "portIndex": 0 }, 57 | "to": { "gateId": "g4", "portIndex": 0 } 58 | }, 59 | { 60 | "id": "c2", 61 | "from": { "gateId": "g2", "portIndex": 0 }, 62 | "to": { "gateId": "g4", "portIndex": 1 } 63 | }, 64 | { 65 | "id": "c3", 66 | "from": { "gateId": "g4", "portIndex": 0 }, 67 | "to": { "gateId": "g5", "portIndex": 0 } 68 | }, 69 | { 70 | "id": "c4", 71 | "from": { "gateId": "g3", "portIndex": 0 }, 72 | "to": { "gateId": "g5", "portIndex": 1 } 73 | }, 74 | { 75 | "id": "c5", 76 | "from": { "gateId": "g5", "portIndex": 0 }, 77 | "to": { "gateId": "g6", "portIndex": 0 } 78 | } 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |Connect outputs to inputs to build circuits. Toggle inputs to test behavior.
35 | 36 | 37 |Logic Circuit Lab is a drag-and-drop playground for experimenting with digital logic right inside your browser. Build combinational circuits, see how signals move through each gate, and share the results with classmates or teammates.
16 |Frequently used gates (AND, OR, XOR, NOT, buffer, etc.) live in the left sidebar. Hover to read a short description or use the search field to jump directly to what you need.
34 | 35 |Place, move, and rotate gates freely across the grid. Wires glow with the accent color whenever a signal is high so you can trace activity at a glance.
37 | 38 |The canvas saves itself after each change. Keep an eye on the status pill in the header for messages such as “Saving…” and “Changes saved”.
40 | 41 |Use the Export VHDL action to create a VHDL snapshot of your current circuit. When the export finishes you’ll see a confirmation toast along with any warnings from the report.
43 |The browser occasionally blocks local saves (usually because storage is full or disabled). The simulator retries in the background; clearing space or re-enabling storage fixes the warning.
83 |Click the wire’s destination input hub. Each input accepts only one connection, so removing it frees the port immediately.
88 |The layout engine recalculates after every resize. If anything still looks off, nudge a gate slightly or press Reset to trigger a clean redraw.
93 |Yes. After every change the header cycles through “Saving…”, “Changes saved”, and finally “Ready”. You can keep building without pressing a save button.
98 |Main Content Area
156 |This area would contain your main application content
157 |