Files
logic-gates/js/components.js
Jose Luis 2a58ad372e feat: toolbar sections as dropdown menus
Replace horizontal toolbar sections with dropdown buttons (I/O, Gates,
Components). Each opens a dropdown menu on click, keeping the toolbar
clean and compact. Dropdowns close on outside click or after selecting
a gate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 03:14:43 +01:00

295 lines
8.5 KiB
JavaScript

// Custom components system — save and reuse circuits as components
import { state } from './state.js';
import { GATE_W, GATE_H } from './constants.js';
// Avoid circular imports - resize will be called from events.js
let resizeCallback = null;
export function setResizeCallback(fn) {
resizeCallback = fn;
}
/**
* Save current circuit as a reusable component
* Returns the component ID if successful
*/
export function saveComponentFromCircuit(name) {
// Validate inputs exist
const inputGates = state.gates.filter(g => g.type === 'INPUT');
const outputGates = state.gates.filter(g => g.type === 'OUTPUT');
if (inputGates.length === 0 || outputGates.length === 0) {
return { success: false, error: 'Component must have at least one INPUT and one OUTPUT' };
}
// Create component definition
const component = {
id: sanitizeComponentName(name),
name,
inputCount: inputGates.length,
outputCount: outputGates.length,
gates: JSON.parse(JSON.stringify(state.gates)),
connections: JSON.parse(JSON.stringify(state.connections))
};
// Store in state
if (!state.customComponents) {
state.customComponents = {};
}
state.customComponents[component.id] = component;
return { success: true, component };
}
/**
* Instantiate a component on the canvas
*/
export function instantiateComponent(componentId, x, y) {
if (!state.customComponents || !state.customComponents[componentId]) {
return { success: false, error: 'Component not found' };
}
const component = state.customComponents[componentId];
const instanceId = state.nextId++;
// Create a component instance gate
const gate = {
id: instanceId,
type: `COMPONENT:${componentId}`,
x,
y,
value: 0,
component
};
state.gates.push(gate);
return { success: true, gate };
}
/**
* Evaluate a component instance
* Simulates the internal circuit and returns output
*/
export function evaluateComponent(gate, inputs) {
if (!gate.component) return 0;
const comp = gate.component;
const internalState = {
gates: JSON.parse(JSON.stringify(comp.gates)),
connections: JSON.parse(JSON.stringify(comp.connections)),
nextId: Math.max(...comp.gates.map(g => g.id), 0) + 1
};
// Set inputs
const inputGates = internalState.gates.filter(g => g.type === 'INPUT');
inputs.forEach((val, i) => {
if (inputGates[i]) inputGates[i].value = val;
});
// Evaluate internal circuit
evaluateInternalCircuit(internalState);
// Get outputs
const outputGates = internalState.gates.filter(g => g.type === 'OUTPUT');
const outputs = outputGates.map(g => g.value || 0);
return outputs;
}
/**
* Helper to evaluate internal circuit
*/
function evaluateInternalCircuit(internalState) {
const { gates, connections } = internalState;
// Simple evaluation - may need optimization for complex circuits
for (let i = 0; i < 10; i++) {
for (const gate of gates) {
if (gate.type === 'INPUT' || gate.type === 'CLOCK') continue;
const inputCount = getGateInputCount(gate.type);
const inputs = [];
for (let j = 0; j < inputCount; j++) {
const conn = connections.find(c => c.to === gate.id && c.toPort === j);
if (conn) {
const srcGate = gates.find(g => g.id === conn.from);
inputs.push(srcGate ? srcGate.value || 0 : 0);
} else {
inputs.push(0);
}
}
// Evaluate based on gate type
let result = 0;
if (gate.type === 'AND') result = (inputs[0] && inputs[1]) ? 1 : 0;
else if (gate.type === 'OR') result = (inputs[0] || inputs[1]) ? 1 : 0;
else if (gate.type === 'NOT') result = inputs[0] ? 0 : 1;
else if (gate.type === 'NAND') result = (inputs[0] && inputs[1]) ? 0 : 1;
else if (gate.type === 'NOR') result = (inputs[0] || inputs[1]) ? 0 : 1;
else if (gate.type === 'XOR') result = (inputs[0] !== inputs[1]) ? 1 : 0;
else if (gate.type === 'OUTPUT') result = inputs[0] || 0;
gate.value = result;
}
}
}
/**
* Get input count for a gate type (includes component types)
*/
function getGateInputCount(type) {
if (type === 'CLOCK' || type === 'INPUT') return 0;
if (type === 'NOT' || type === 'OUTPUT') return 1;
if (type.startsWith('COMPONENT:')) {
// Return the component's input count
return 2; // Default for now, should lookup
}
return 2;
}
/**
* Get output count for a gate type
*/
function getGateOutputCount(type) {
if (type === 'OUTPUT') return 0;
return 1;
}
/**
* Sanitize component name for use as ID
*/
function sanitizeComponentName(name) {
return name
.toLowerCase()
.replace(/[^a-z0-9_]/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '');
}
/**
* Get all custom components
*/
export function getAllComponents() {
return state.customComponents || {};
}
/**
* Delete a component
*/
export function deleteComponent(componentId) {
if (state.customComponents) {
delete state.customComponents[componentId];
return { success: true };
}
return { success: false, error: 'Component not found' };
}
/**
* Export component data as JSON
*/
export function exportComponent(componentId) {
if (!state.customComponents || !state.customComponents[componentId]) {
return { success: false, error: 'Component not found' };
}
return { success: true, data: state.customComponents[componentId] };
}
/**
* Import component from JSON
*/
export function importComponent(data) {
if (!data.id || !data.gates || !data.connections) {
return { success: false, error: 'Invalid component data' };
}
if (!state.customComponents) {
state.customComponents = {};
}
state.customComponents[data.id] = data;
return { success: true, component: data };
}
/**
* Enter component editor mode
*/
export function enterComponentEditor() {
// Save current main circuit
state.savedMainCircuit = {
gates: JSON.parse(JSON.stringify(state.gates)),
connections: JSON.parse(JSON.stringify(state.connections)),
nextId: state.nextId
};
// Clear canvas for sub-circuit design
state.gates = [];
state.connections = [];
state.nextId = 1;
state.componentEditorActive = true;
state.placingGate = null;
state.connecting = null;
// Show editor overlay
const overlay = document.getElementById('component-editor-overlay');
overlay.style.display = 'flex';
document.getElementById('component-editor-title').textContent = 'Editing Component: (New)';
// Resize canvas to account for editor bar
if (resizeCallback) resizeCallback();
}
/**
* Exit component editor mode
*/
export function exitComponentEditor(name, shouldSave) {
const overlay = document.getElementById('component-editor-overlay');
overlay.style.display = 'none';
if (shouldSave && name) {
// Save the component
saveComponentFromCircuit(name);
}
// Restore main circuit
if (state.savedMainCircuit) {
state.gates = state.savedMainCircuit.gates;
state.connections = state.savedMainCircuit.connections;
state.nextId = state.savedMainCircuit.nextId;
state.savedMainCircuit = null;
}
state.componentEditorActive = false;
state.placingGate = null;
// Update component buttons to show newly saved component
updateComponentButtons();
// Resize canvas via callback
if (resizeCallback) resizeCallback();
}
/**
* Update component buttons in toolbar
*/
export function updateComponentButtons() {
const container = document.getElementById('saved-components');
container.innerHTML = '';
const components = getAllComponents();
Object.values(components).forEach(comp => {
const btn = document.createElement('button');
btn.className = 'component-btn';
btn.dataset.componentId = comp.id;
btn.textContent = comp.name;
btn.title = `${comp.inputCount} input(s), ${comp.outputCount} output(s)`;
btn.addEventListener('click', (e) => {
e.stopPropagation();
state.placingGate = `COMPONENT:${comp.id}`;
// Close dropdown
document.querySelectorAll('.toolbar-dropdown.open').forEach(d => d.classList.remove('open'));
});
container.appendChild(btn);
});
}