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>
293 lines
8.4 KiB
JavaScript
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);
|
|
});
|
|
}
|