Auto-saves every 3 seconds and on page unload. Restores the full state (circuit, camera, custom components) on page load. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
195 lines
5.4 KiB
JavaScript
195 lines
5.4 KiB
JavaScript
// Save/Load system — export and import circuits and progress
|
|
import { state } from './state.js';
|
|
import { progress } from './levels.js';
|
|
|
|
const STORAGE_KEY = 'logiclab_state';
|
|
|
|
/**
|
|
* Save complete application state to JSON
|
|
*/
|
|
export function saveState() {
|
|
return {
|
|
timestamp: new Date().toISOString(),
|
|
circuit: {
|
|
gates: state.gates,
|
|
connections: state.connections,
|
|
nextId: state.nextId
|
|
},
|
|
camera: {
|
|
camX: state.camX,
|
|
camY: state.camY,
|
|
zoom: state.zoom
|
|
},
|
|
components: state.customComponents || {},
|
|
progress: {
|
|
unlockedLevels: progress.unlockedLevels,
|
|
completedLevels: progress.completedLevels,
|
|
currentLevel: progress.currentLevel,
|
|
customComponents: progress.customComponents
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Load application state from JSON
|
|
*/
|
|
export function loadState(data) {
|
|
if (!data || !data.circuit) {
|
|
return { success: false, error: 'Invalid save data' };
|
|
}
|
|
|
|
try {
|
|
// Load circuit
|
|
state.gates = JSON.parse(JSON.stringify(data.circuit.gates));
|
|
state.connections = JSON.parse(JSON.stringify(data.circuit.connections));
|
|
state.nextId = data.circuit.nextId;
|
|
|
|
// Load camera
|
|
if (data.camera) {
|
|
state.camX = data.camera.camX;
|
|
state.camY = data.camera.camY;
|
|
state.zoom = data.camera.zoom;
|
|
}
|
|
|
|
// Load components
|
|
if (data.components) {
|
|
state.customComponents = JSON.parse(JSON.stringify(data.components));
|
|
}
|
|
|
|
// Load progress
|
|
if (data.progress) {
|
|
progress.unlockedLevels = data.progress.unlockedLevels || ['buffer'];
|
|
progress.completedLevels = data.progress.completedLevels || [];
|
|
progress.currentLevel = data.progress.currentLevel || null;
|
|
progress.customComponents = data.progress.customComponents || {};
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (e) {
|
|
return { success: false, error: e.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export as JSON file (for download)
|
|
*/
|
|
export function exportAsFile(filename = 'logic-circuit.json') {
|
|
const data = saveState();
|
|
const json = JSON.stringify(data, null, 2);
|
|
const blob = new Blob([json], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
/**
|
|
* Import from file (returns Promise)
|
|
*/
|
|
export function importFromFile(file) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
try {
|
|
const data = JSON.parse(e.target.result);
|
|
const result = loadState(data);
|
|
resolve(result);
|
|
} catch (err) {
|
|
reject(new Error('Failed to parse JSON'));
|
|
}
|
|
};
|
|
reader.onerror = () => reject(new Error('Failed to read file'));
|
|
reader.readAsText(file);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Export circuit as a simple netlist (text format)
|
|
*/
|
|
export function exportAsNetlist() {
|
|
let netlist = '# Logic Circuit Netlist\n\n';
|
|
netlist += '## Gates\n';
|
|
|
|
state.gates.forEach(gate => {
|
|
netlist += `${gate.type} ${gate.id} at (${gate.x.toFixed(0)}, ${gate.y.toFixed(0)})\n`;
|
|
});
|
|
|
|
netlist += '\n## Connections\n';
|
|
state.connections.forEach(conn => {
|
|
netlist += `${conn.from}:${conn.fromPort} -> ${conn.to}:${conn.toPort}\n`;
|
|
});
|
|
|
|
return netlist;
|
|
}
|
|
|
|
/**
|
|
* Copy state to clipboard as JSON (useful for sharing)
|
|
*/
|
|
export function copyToClipboard() {
|
|
const data = saveState();
|
|
const json = JSON.stringify(data);
|
|
navigator.clipboard.writeText(json).then(() => {
|
|
alert('State copied to clipboard');
|
|
}).catch(() => {
|
|
alert('Failed to copy to clipboard');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Paste state from clipboard
|
|
*/
|
|
export async function pasteFromClipboard() {
|
|
try {
|
|
const text = await navigator.clipboard.readText();
|
|
const data = JSON.parse(text);
|
|
return loadState(data);
|
|
} catch (e) {
|
|
return { success: false, error: e.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save state to localStorage
|
|
*/
|
|
export function saveToStorage() {
|
|
try {
|
|
const data = saveState();
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
} catch (e) {
|
|
console.warn('[storage] failed to save:', e.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load state from localStorage (returns true if state was restored)
|
|
*/
|
|
export function loadFromStorage() {
|
|
try {
|
|
const json = localStorage.getItem(STORAGE_KEY);
|
|
if (!json) return false;
|
|
const data = JSON.parse(json);
|
|
const result = loadState(data);
|
|
if (result.success) {
|
|
console.log('[storage] restored state from localStorage');
|
|
}
|
|
return result.success;
|
|
} catch (e) {
|
|
console.warn('[storage] failed to load:', e.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start auto-saving to localStorage on an interval
|
|
*/
|
|
let autoSaveInterval = null;
|
|
export function startAutoSave(intervalMs = 3000) {
|
|
if (autoSaveInterval) clearInterval(autoSaveInterval);
|
|
autoSaveInterval = setInterval(saveToStorage, intervalMs);
|
|
// Also save on page unload
|
|
window.addEventListener('beforeunload', saveToStorage);
|
|
console.log(`[storage] auto-save enabled (every ${intervalMs}ms)`);
|
|
}
|