// 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) { alert('Component must have at least one INPUT and one OUTPUT'); return { success: false, error: 'Component must have at least one INPUT and one OUTPUT' }; } // Store the input/output gate IDs in order so we can map ports consistently const inputIds = inputGates.map(g => g.id); const outputIds = outputGates.map(g => g.id); // Create component definition const component = { id: sanitizeComponentName(name), name, inputCount: inputGates.length, outputCount: outputGates.length, inputIds, outputIds, 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; console.log(`[component] saved "${name}" (${component.inputCount} in, ${component.outputCount} out)`, `inputIds=${inputIds}`, `outputIds=${outputIds}`); return { success: true, component }; } /** * Evaluate a component instance. * Simulates the internal circuit and returns an array of output values. * IMPORTANT: Uses persistent internal state so latches/flip-flops retain * their values between evaluations (just like the main circuit). */ export function evaluateComponent(gate, inputs) { if (!gate.component) { console.warn('[component] evaluateComponent called without component data', gate); return [0]; } const comp = gate.component; // Persist internal gate state on the gate instance so latches hold their value if (!gate._internalGates) { gate._internalGates = JSON.parse(JSON.stringify(comp.gates)); } const internalGates = gate._internalGates; const internalConns = comp.connections; // read-only, no need to clone // Map external inputs to internal INPUT gates using stored inputIds const inputIds = comp.inputIds || []; for (let i = 0; i < inputs.length; i++) { const targetId = inputIds[i]; const inputGate = targetId != null ? internalGates.find(g => g.id === targetId) : internalGates.filter(g => g.type === 'INPUT')[i]; // fallback for old components if (inputGate) { inputGate.value = inputs[i]; } } // Iterative fixed-point evaluation (same approach as main evaluateAll) const MAX_ITER = 20; for (let iter = 0; iter < MAX_ITER; iter++) { let changed = false; for (const g of internalGates) { if (g.type === 'INPUT' || g.type === 'CLOCK') continue; const inCount = getGateInputCount(g.type); const gInputs = []; for (let j = 0; j < inCount; j++) { const conn = internalConns.find(c => c.to === g.id && c.toPort === j); if (conn) { const src = internalGates.find(s => s.id === conn.from); gInputs.push(src ? (src.value || 0) : 0); } else { gInputs.push(0); } } let result = 0; switch (g.type) { case 'AND': result = (gInputs[0] && gInputs[1]) ? 1 : 0; break; case 'OR': result = (gInputs[0] || gInputs[1]) ? 1 : 0; break; case 'NOT': result = gInputs[0] ? 0 : 1; break; case 'NAND': result = (gInputs[0] && gInputs[1]) ? 0 : 1; break; case 'NOR': result = (gInputs[0] || gInputs[1]) ? 0 : 1; break; case 'XOR': result = (gInputs[0] !== gInputs[1]) ? 1 : 0; break; case 'OUTPUT': result = gInputs[0] || 0; break; default: result = 0; } if (result !== g.value) { g.value = result; changed = true; } } if (!changed) break; } // Read outputs using stored outputIds const outputIds = comp.outputIds || []; const outputs = []; if (outputIds.length > 0) { for (const outId of outputIds) { const outGate = internalGates.find(g => g.id === outId); outputs.push(outGate ? (outGate.value || 0) : 0); } } else { // Fallback for old components without outputIds const outputGates = internalGates.filter(g => g.type === 'OUTPUT'); for (const g of outputGates) { outputs.push(g.value || 0); } } console.log(`[component] eval "${comp.name}" inputs=[${inputs}] → outputs=[${outputs}]`, `(internal state preserved: ${gate._internalGates ? 'yes' : 'no'})`); return outputs; } /** * Get input count for a gate type */ function getGateInputCount(type) { if (type === 'CLOCK' || type === 'INPUT') return 0; if (type === 'NOT' || type === 'OUTPUT') return 1; return 2; } /** * 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 (new component) */ 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.editingComponentId = null; // new component, not editing existing 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(); } /** * Enter component editor to edit an existing component's blueprint. * Loads the component's internal circuit for modification. */ export function editComponentBlueprint(gate) { if (!gate.component) return; const comp = gate.component; // Save current main circuit state.savedMainCircuit = { gates: JSON.parse(JSON.stringify(state.gates)), connections: JSON.parse(JSON.stringify(state.connections)), nextId: state.nextId }; // Load the component's internal circuit into the canvas state.gates = JSON.parse(JSON.stringify(comp.gates)); state.connections = JSON.parse(JSON.stringify(comp.connections)); // Set nextId to max existing id + 1 so new gates don't collide state.nextId = state.gates.reduce((max, g) => Math.max(max, g.id), 0) + 1; state.componentEditorActive = true; state.editingComponentId = comp.id; // track which component we're editing 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: ${comp.name}`; console.log(`[component] editing blueprint of "${comp.name}" (${comp.inputCount} in, ${comp.outputCount} out)`); // 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'; const editingId = state.editingComponentId; if (shouldSave && name) { // Save the component (works for both new and edited) const result = saveComponentFromCircuit(name); // Update all placed instances of this component in the main circuit. // Handles both: editing existing component (editingId matches) AND // creating a "new" component that overwrites an existing one (same sanitized name). if (result.success && state.savedMainCircuit) { const updatedComp = state.customComponents[result.component.id]; if (updatedComp) { const matchId = editingId || result.component.id; for (const gate of state.savedMainCircuit.gates) { if (gate.component && gate.component.id === matchId) { gate.component = updatedComp; // Clear persisted internal state so it re-initializes from updated blueprint delete gate._internalGates; console.log(`[component] updated instance #${gate.id} with new blueprint`); } } } } } // 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.editingComponentId = null; 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); }); }