feat: double-click component gates to edit their blueprint

Opens the component editor with the internal circuit loaded for
modification. On save, updates the component definition and all
existing instances in the main circuit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-20 04:22:45 +01:00
parent 817dab43df
commit eb22a5ff62
3 changed files with 77 additions and 8 deletions

View File

@@ -208,7 +208,7 @@ export function importComponent(data) {
}
/**
* Enter component editor mode
* Enter component editor mode (new component)
*/
export function enterComponentEditor() {
// Save current main circuit
@@ -223,6 +223,7 @@ export function enterComponentEditor() {
state.connections = [];
state.nextId = 1;
state.componentEditorActive = true;
state.editingComponentId = null; // new component, not editing existing
state.placingGate = null;
state.connecting = null;
@@ -235,6 +236,42 @@ export function enterComponentEditor() {
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
*/
@@ -242,9 +279,26 @@ 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
saveComponentFromCircuit(name);
// Save the component (works for both new and edited)
const result = saveComponentFromCircuit(name);
// If editing an existing component, update all placed instances in the main circuit
if (editingId && result.success && state.savedMainCircuit) {
const updatedComp = state.customComponents[result.component.id];
if (updatedComp) {
for (const gate of state.savedMainCircuit.gates) {
if (gate.component && gate.component.id === editingId) {
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
@@ -256,6 +310,7 @@ export function exitComponentEditor(name, shouldSave) {
}
state.componentEditorActive = false;
state.editingComponentId = null;
state.placingGate = null;
// Update component buttons to show newly saved component

View File

@@ -8,7 +8,7 @@ import { resize, screenToWorld } from './renderer.js';
import { puzzleMode, currentLevel, showLevelPanel } from './puzzleUI.js';
import { getLevel } from './levels.js';
import { saveState, loadState, exportAsFile, importFromFile } from './saveLoad.js';
import { enterComponentEditor, exitComponentEditor, updateComponentButtons, setResizeCallback } from './components.js';
import { enterComponentEditor, editComponentBlueprint, exitComponentEditor, updateComponentButtons, setResizeCallback } from './components.js';
import { getExampleList, loadExample } from './examples.js';
const PAN_SPEED = 40;
@@ -147,11 +147,20 @@ export function initEvents() {
dragStartPos = null;
});
// Double-click to rename INPUT/OUTPUT/CLOCK gates
// Double-click to rename INPUT/OUTPUT/CLOCK gates, or edit component blueprint
canvas.addEventListener('dblclick', e => {
const world = screenToWorld(e.offsetX, e.offsetY);
const gate = findGateAt(world.x, world.y);
if (gate && (gate.type === 'INPUT' || gate.type === 'OUTPUT' || gate.type === 'CLOCK')) {
if (!gate) return;
// Double-click on component gate → edit its blueprint
if (gate.type.startsWith('COMPONENT:') && gate.component) {
editComponentBlueprint(gate);
return;
}
// Double-click on I/O gates → rename
if (gate.type === 'INPUT' || gate.type === 'OUTPUT' || gate.type === 'CLOCK') {
const current = gate.label || '';
const label = prompt(`Label for ${gate.type}#${gate.id}:`, current);
if (label !== null) {
@@ -415,7 +424,11 @@ export function initEvents() {
});
document.getElementById('component-editor-save').addEventListener('click', () => {
const name = prompt('Component name:', 'MyComponent');
// If editing existing, pre-fill with current name
const existingName = state.editingComponentId
? (state.customComponents[state.editingComponentId]?.name || 'MyComponent')
: 'MyComponent';
const name = prompt('Component name:', existingName);
if (name && name.trim()) {
exitComponentEditor(name.trim(), true);
}

View File

@@ -41,5 +41,6 @@ export const state = {
// Component Editor
componentEditorActive: false,
savedMainCircuit: null, // { gates, connections, nextId } saved before entering editor
componentEditorName: ''
componentEditorName: '',
editingComponentId: null // ID of component being edited (null = new component)
};