Files
reaktor/packages/client/src/engine/state.js
Jose Luis b058997889 refactor: restructure to monorepo with npm workspaces (Phase 0)
Move frontend to packages/client/, server to packages/server/.
Root package.json uses npm workspaces to orchestrate both.

Structure:
  reaktor/
    packages/client/  (React + Vite + Tone.js frontend)
    packages/server/  (static file server, future API)
    dist/             (built output, shared)
    docker-compose.yml (app + PostgreSQL for future backend)

- npm run dev → runs Vite dev server from client workspace
- npm run build → builds client, outputs to root dist/
- npm run start → runs server.js serving dist/
- Dockerfile updated for multi-stage monorepo build
- docker-compose.yml added with PostgreSQL service (ready for Phase 1)
- All imports and paths preserved, zero functionality change

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 19:52:57 +01:00

146 lines
4.3 KiB
JavaScript

/**
* 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();
}