Files
logic-gates/js/components.js
Jose Luis 53d600fcb0 fix: horizontal toolbar layout + fix component button placement
Redesign toolbar sections to use horizontal button rows instead of
vertical stacking. Fix component placement by attaching click handlers
directly to dynamically created buttons and passing correct gate object
shape to getComponentWidth/Height.

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

293 lines
8.4 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}`;
});
container.appendChild(btn);
});
}