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>
This commit is contained in:
145
packages/client/src/engine/state.js
Normal file
145
packages/client/src/engine/state.js
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
Reference in New Issue
Block a user