diff options
Diffstat (limited to 'web-timeplot/src/state.js')
| -rw-r--r-- | web-timeplot/src/state.js | 420 |
1 files changed, 0 insertions, 420 deletions
diff --git a/web-timeplot/src/state.js b/web-timeplot/src/state.js deleted file mode 100644 index 53d8279..0000000 --- a/web-timeplot/src/state.js +++ /dev/null @@ -1,420 +0,0 @@ -/** - * StateManager - Centralized state management with Proxy-based reactivity - * - * Usage: - * state.time.speed = 2.0 // automatically emits events - * state.on('time.speed', (value) => console.log('Speed changed:', value)) - * state.on('time.*', (change) => console.log('Time domain changed:', change)) - * - * State Domains: - * - userPrefs: showGrid, showMetrics, theme, etc. - * - uiConfig: active panels, layout, dimensions - * - time: current time, speed, paused state, real elapsed time - * - rendering: graphs, renderer info - * - health: framerate, service connections, db access - * - dataInput: sources, structure, metadata - * - inputActions: keyboard/mouse/gamepad action mappings - */ - -// Simple EventEmitter implementation -class EventEmitter { - constructor() { - this.events = new Map(); - } - - on(event, callback) { - if (!this.events.has(event)) { - this.events.set(event, []); - } - this.events.get(event).push(callback); - - // Return unsubscribe function - return () => this.off(event, callback); - } - - off(event, callback) { - if (!this.events.has(event)) return; - const callbacks = this.events.get(event); - const index = callbacks.indexOf(callback); - if (index > -1) { - callbacks.splice(index, 1); - } - } - - emit(event, data) { - if (!this.events.has(event)) return; - this.events.get(event).forEach(callback => { - try { - callback(data); - } catch (e) { - console.error(`[State] Error in event handler for '${event}':`, e); - } - }); - } - - once(event, callback) { - const wrapper = (data) => { - callback(data); - this.off(event, wrapper); - }; - this.on(event, wrapper); - } - - clear() { - this.events.clear(); - } -} - -export class StateManager extends EventEmitter { - constructor() { - super(); - - // Internal state storage (not proxied) - this._state = { - userPrefs: { - showGrid: true, - showMetrics: true, - theme: 'dark', - rollingWindow: 60, - historyCapacity: 10000, - metricsUpdateInterval: 10, - }, - - uiConfig: { - activePanels: ['graph1', 'graph2'], - layout: 'horizontal-split', - canvasWidth: 0, - canvasHeight: 0, - }, - - time: { - current: 0, // Current plot time - realElapsed: 0, // Real time elapsed since start - speed: 1.0, // Time speed multiplier (0.1 to 5.0) - isPaused: false, // Pause state - startTimestamp: Date.now(), // Real timestamp when started - verticalScale: 1.0, // Vertical zoom for time history - }, - - rendering: { - rendererType: 'unknown', // 'webgpu' | 'webgl' | 'canvas' - frameCounter: 0, - // Note: graph instances are NOT stored here to avoid proxy wrapping - }, - - health: { - fps: 0, - updateMs: 0, - renderMs: 0, - vertexCount: 0, - lineCount: 0, - serviceConnections: {}, // e.g., { websocket: 'connected', mqtt: 'disconnected' } - }, - - dataInput: { - sources: [], // Array of data source configs - activeSource: null, // Currently active source - dataStructure: null, // Schema of incoming data - metadata: {}, // Additional metadata - }, - - inputActions: { - keyboardMap: new Map(), // Map of KeyboardEvent.code => action name - mouseMap: new Map(), // Map of mouse button => action name - actionHandlers: new Map(), // Map of action name => handler function - }, - }; - - // Track which domains should be persisted - this._persistedDomains = new Set(['userPrefs']); - - // Load persisted state - this._loadPersistedState(); - - // Create proxied state - this is what users interact with - this.state = this._createProxy(this._state, []); - } - - /** - * Create a reactive Proxy that emits events on property changes - * @param {Object} target - The object to proxy - * @param {Array} path - Current property path (e.g., ['time', 'speed']) - * @private - */ - _createProxy(target, path) { - // Don't proxy non-objects or special objects like Map/Set - if (typeof target !== 'object' || target === null) { - return target; - } - - // Don't proxy Maps and Sets - they need special handling - if (target instanceof Map || target instanceof Set) { - return target; - } - - const self = this; - - return new Proxy(target, { - get(obj, prop) { - const value = obj[prop]; - - // Return primitives and functions as-is - if (typeof value !== 'object' || value === null) { - return value; - } - - // Return nested objects as proxies - return self._createProxy(value, [...path, prop]); - }, - - set(obj, prop, value) { - const oldValue = obj[prop]; - - // Only emit if value actually changed - if (oldValue === value) { - return true; - } - - obj[prop] = value; - - // Build event path - const fullPath = [...path, prop]; - const pathString = fullPath.join('.'); - const domain = fullPath[0]; - - // Emit specific property change: "time.speed" - self.emit(pathString, { - path: fullPath, - value: value, - oldValue: oldValue, - }); - - // Emit domain wildcard: "time.*" - if (domain) { - self.emit(`${domain}.*`, { - path: fullPath, - property: prop, - value: value, - oldValue: oldValue, - }); - } - - // Emit global wildcard: "*" - self.emit('*', { - path: fullPath, - value: value, - oldValue: oldValue, - }); - - // Auto-persist certain domains - if (self._persistedDomains.has(domain)) { - self._persistDomain(domain); - } - - return true; - } - }); - } - - // ========================================================================= - // Persistence - // ========================================================================= - - _persistDomain(domain) { - try { - const data = this._state[domain]; - // Convert Maps to objects for JSON serialization - const serializable = this._makeSerializable(data); - localStorage.setItem(`timeplot-${domain}`, JSON.stringify(serializable)); - } catch (e) { - console.warn(`[State] Failed to persist ${domain}:`, e); - } - } - - _loadPersistedState() { - this._persistedDomains.forEach(domain => { - try { - const saved = localStorage.getItem(`timeplot-${domain}`); - if (saved) { - const data = JSON.parse(saved); - // Deep merge to preserve defaults for new properties - this._state[domain] = this._deepMerge(this._state[domain], data); - } - } catch (e) { - console.warn(`[State] Failed to load ${domain}:`, e); - } - }); - } - - _makeSerializable(obj) { - if (obj instanceof Map) { - return Object.fromEntries(obj); - } - if (obj instanceof Set) { - return Array.from(obj); - } - if (typeof obj === 'object' && obj !== null) { - const result = {}; - for (const [key, value] of Object.entries(obj)) { - result[key] = this._makeSerializable(value); - } - return result; - } - return obj; - } - - _deepMerge(target, source) { - const result = { ...target }; - for (const key in source) { - if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) { - result[key] = this._deepMerge(target[key] || {}, source[key]); - } else { - result[key] = source[key]; - } - } - return result; - } - - // ========================================================================= - // Convenience Methods - // ========================================================================= - - /** - * Toggle a boolean preference - */ - togglePref(key) { - const current = this.state.userPrefs[key]; - if (typeof current === 'boolean') { - this.state.userPrefs[key] = !current; - } - } - - /** - * Pause/resume time - */ - togglePause() { - this.state.time.isPaused = !this.state.time.isPaused; - } - - /** - * Set time speed (clamped 0.1 to 5.0) - */ - setTimeSpeed(speed) { - this.state.time.speed = Math.max(0.1, Math.min(5.0, speed)); - } - - /** - * Increment time (respects pause and speed) - */ - incrementTime(delta) { - if (this.state.time.isPaused) return; - this.state.time.current += delta * this.state.time.speed; - } - - /** - * Update real elapsed time - */ - updateRealElapsed() { - const elapsed = (Date.now() - this.state.time.startTimestamp) / 1000; - this.state.time.realElapsed = elapsed; - } - - // ========================================================================= - // Input Actions System - // ========================================================================= - - /** - * Register an input action handler - * @param {string} actionName - Name of the action (e.g., 'toggleGrid', 'pause') - * @param {Function} handler - Handler function to call - */ - registerAction(actionName, handler) { - this.state.inputActions.actionHandlers.set(actionName, handler); - } - - /** - * Map a keyboard key to an action - * @param {string} code - KeyboardEvent.code (e.g., 'KeyG', 'Space') - * @param {string} actionName - Action to trigger - */ - mapKey(code, actionName) { - this.state.inputActions.keyboardMap.set(code, actionName); - } - - /** - * Map a mouse button to an action - * @param {number} button - Mouse button number (0=left, 1=middle, 2=right) - * @param {string} actionName - Action to trigger - */ - mapMouseButton(button, actionName) { - this.state.inputActions.mouseMap.set(button, actionName); - } - - /** - * Execute an action by name - */ - executeAction(actionName, event) { - const handler = this.state.inputActions.actionHandlers.get(actionName); - if (handler) { - handler(event); - } else { - console.warn(`[State] No handler registered for action: ${actionName}`); - } - } - - /** - * Handle keyboard event through action system - */ - handleKeyboardEvent(event) { - const actionName = this.state.inputActions.keyboardMap.get(event.code); - if (actionName) { - this.executeAction(actionName, event); - return true; - } - return false; - } - - /** - * Handle mouse button event through action system - */ - handleMouseButtonEvent(event) { - const actionName = this.state.inputActions.mouseMap.get(event.button); - if (actionName) { - this.executeAction(actionName, event); - return true; - } - return false; - } - - // ========================================================================= - // Data Sources - // ========================================================================= - - addDataSource(source) { - this.state.dataInput.sources.push(source); - } - - removeDataSource(sourceId) { - const sources = this.state.dataInput.sources; - const index = sources.findIndex(s => s.id === sourceId); - if (index > -1) { - sources.splice(index, 1); - } - } - - setActiveDataSource(sourceId) { - this.state.dataInput.activeSource = sourceId; - } - - // ========================================================================= - // Debugging - // ========================================================================= - - dump() { - console.log('[State] Current state:', JSON.parse(JSON.stringify(this._state))); - } - - debugEvents() { - console.log('[State] Registered events:', Array.from(this.events.keys())); - } -} |
