/** * state.js — Centralized reactive state for the modular synth * Uses a simple pub/sub pattern for React integration */ import { playConnect, playDisconnect, playModuleAdd, playModuleDelete } from './uiSounds.js'; import { getModuleDef } from './moduleRegistry.js'; import { invalidateConnectionCache } from './audioEngine.js'; let _listeners = new Set(); let _nextModuleId = 1; let _nextConnectionId = 1; export const state = { modules: [], // { id, type, x, y, params, collapsed } connections: [], // { id, from: {moduleId, port}, to: {moduleId, port} } // Interaction selectedModuleId: null, dragging: null, // { moduleId, offsetX, offsetY } connecting: null, // { moduleId, port, portType, direction, x, y } (temp wire) // Camera camX: 0, camY: 0, zoom: 1, panning: false, panStart: null, // Audio isRunning: false, masterVolume: -6, // UI showPalette: true, presetModal: null, // null | 'save' | 'load' }; export function subscribe(fn) { _listeners.add(fn); return () => _listeners.delete(fn); } export function emit() { _listeners.forEach(fn => fn()); } export function addModule(type, x, y) { const id = _nextModuleId++; // Populate ALL default params so level checkers can read them immediately const def = getModuleDef(type); const defaults = def ? Object.fromEntries(Object.entries(def.params).map(([k, v]) => [k, v.default])) : {}; state.modules.push({ id, type, x, y, params: defaults, collapsed: false }); state.selectedModuleId = id; emit(); playModuleAdd(); return id; } export function removeModule(id) { state.modules = state.modules.filter(m => m.id !== id); state.connections = state.connections.filter( c => c.from.moduleId !== id && c.to.moduleId !== id ); if (state.selectedModuleId === id) state.selectedModuleId = null; emit(); playModuleDelete(); } export function updateModulePosition(id, x, y) { const m = state.modules.find(m => m.id === id); if (m) { m.x = x; m.y = y; emit(); } } export function updateModuleParam(id, paramName, value) { const m = state.modules.find(m => m.id === id); if (m) { m.params[paramName] = value; emit(); } } export function addConnection(fromModuleId, fromPort, toModuleId, toPort) { // Prevent duplicates const exists = state.connections.find(c => c.from.moduleId === fromModuleId && c.from.port === fromPort && c.to.moduleId === toModuleId && c.to.port === toPort ); if (exists) return null; // Prevent connecting to already-connected input const inputTaken = state.connections.find(c => c.to.moduleId === toModuleId && c.to.port === toPort ); if (inputTaken) { // Remove old connection to this input (silent — connect sound will play) removeConnection(inputTaken.id, true); } const id = _nextConnectionId++; state.connections.push({ id, from: { moduleId: fromModuleId, port: fromPort }, to: { moduleId: toModuleId, port: toPort } }); invalidateConnectionCache(); emit(); playConnect(); return id; } export function removeConnection(id, _silent = false) { state.connections = state.connections.filter(c => c.id !== id); invalidateConnectionCache(); emit(); if (!_silent) playDisconnect(); } export function getModule(id) { return state.modules.find(m => m.id === id) || null; } export function isPortConnected(moduleId, portName, direction) { return state.connections.some(c => direction === 'output' ? (c.from.moduleId === moduleId && c.from.port === portName) : (c.to.moduleId === moduleId && c.to.port === portName) ); } // Serialization export function serialize() { return { modules: state.modules.map(m => ({ id: m.id, type: m.type, x: m.x, y: m.y, params: { ...m.params } })), connections: state.connections.map(c => ({ ...c })), camera: { camX: state.camX, camY: state.camY, zoom: state.zoom }, masterVolume: state.masterVolume, }; } export function deserialize(data) { state.modules = data.modules || []; state.connections = data.connections || []; if (data.camera) { state.camX = data.camera.camX || 0; state.camY = data.camera.camY || 0; state.zoom = data.camera.zoom || 1; } state.masterVolume = data.masterVolume ?? -6; _nextModuleId = Math.max(1, ...state.modules.map(m => m.id)) + 1; _nextConnectionId = Math.max(1, ...state.connections.map(c => c.id)) + 1; state.selectedModuleId = null; emit(); }