summaryrefslogtreecommitdiff
path: root/web-timeplot/src/state.js
diff options
context:
space:
mode:
Diffstat (limited to 'web-timeplot/src/state.js')
-rw-r--r--web-timeplot/src/state.js420
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()));
- }
-}