diff options
Diffstat (limited to 'web-timeplot/src/core')
| -rw-r--r-- | web-timeplot/src/core/event-bus.js | 32 | ||||
| -rw-r--r-- | web-timeplot/src/core/store.js | 291 | ||||
| -rw-r--r-- | web-timeplot/src/core/time-controller.js | 80 |
3 files changed, 0 insertions, 403 deletions
diff --git a/web-timeplot/src/core/event-bus.js b/web-timeplot/src/core/event-bus.js deleted file mode 100644 index 192eb6d..0000000 --- a/web-timeplot/src/core/event-bus.js +++ /dev/null @@ -1,32 +0,0 @@ -export class EventBus { - constructor() { - this.listeners = new Map(); - } - - on(eventName, listener) { - if (!this.listeners.has(eventName)) { - this.listeners.set(eventName, new Set()); - } - - const listeners = this.listeners.get(eventName); - listeners.add(listener); - - return () => { - listeners.delete(listener); - if (listeners.size === 0) { - this.listeners.delete(eventName); - } - }; - } - - emit(eventName, payload) { - const listeners = this.listeners.get(eventName); - if (!listeners) { - return; - } - - for (const listener of listeners) { - listener(payload); - } - } -} diff --git a/web-timeplot/src/core/store.js b/web-timeplot/src/core/store.js deleted file mode 100644 index 38052eb..0000000 --- a/web-timeplot/src/core/store.js +++ /dev/null @@ -1,291 +0,0 @@ -const STORAGE_KEY = 'timeplot.app-state.v1'; - -function clonePanelState(panels) { - return Object.fromEntries(Object.entries(panels).map(([key, value]) => [key, { ...value }])); -} - -function cloneNamedState(items) { - return Object.fromEntries(Object.entries(items).map(([key, value]) => [key, { ...value }])); -} - -function sanitizePersistedSource(source) { - return { - type: source.type, - preset: source.preset, - sampleRateHz: source.sampleRateHz, - amplitude: source.amplitude, - noise: source.noise, - replayRate: source.replayRate, - dataFileName: source.dataFileName, - wsUrl: source.wsUrl, - wsReconnectMs: source.wsReconnectMs, - }; -} - -function createPersistableState(state) { - return { - plot: { - showGrid: state.plot.showGrid, - showPoints: state.plot.showPoints, - windowDurationMs: state.plot.windowDurationMs, - maxPoints: state.plot.maxPoints, - }, - time: { - speed: state.time.speed, - }, - panels: clonePanelState(state.panels), - graphs: cloneNamedState(state.graphs), - sources: Object.fromEntries(Object.entries(state.sources).map(([key, value]) => [ - key, - sanitizePersistedSource(value), - ])), - }; -} - -function mergePersistedState(baseState, persistedState) { - if (!persistedState || typeof persistedState !== 'object') { - return baseState; - } - - const mergedState = { - ...baseState, - time: persistedState.time - ? { - ...baseState.time, - speed: persistedState.time.speed ?? baseState.time.speed, - paused: false, - } - : baseState.time, - plot: persistedState.plot - ? { - ...baseState.plot, - ...persistedState.plot, - valueRange: baseState.plot.valueRange, - hoveredPoint: null, - tooltip: { ...baseState.plot.tooltip }, - } - : baseState.plot, - panels: persistedState.panels - ? clonePanelState(Object.fromEntries(Object.entries(baseState.panels).map(([key, value]) => [ - key, - { - ...value, - ...(persistedState.panels[key] ?? {}), - }, - ]))) - : baseState.panels, - graphs: persistedState.graphs - ? cloneNamedState(Object.fromEntries(Object.entries(baseState.graphs).map(([key, value]) => [ - key, - { - ...value, - ...(persistedState.graphs[key] ?? {}), - }, - ]))) - : baseState.graphs, - sources: persistedState.sources - ? Object.fromEntries(Object.entries(baseState.sources).map(([key, value]) => { - const persistedSource = persistedState.sources[key] ?? {}; - const nextType = persistedSource.type ?? value.type; - - return [ - key, - { - ...value, - ...persistedSource, - type: nextType, - dataset: [], - datasetPointCount: 0, - datasetDurationMs: 0, - loadError: nextType === 'csv-replay' && persistedSource.dataFileName - ? `Reload ${persistedSource.dataFileName} to restore replay data` - : '', - wsStatus: 'idle', - wsStatusDetail: '', - }, - ]; - })) - : baseState.sources, - }; - - return mergedState; -} - -function loadPersistedState() { - if (typeof localStorage === 'undefined') { - return null; - } - - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) { - return null; - } - - return JSON.parse(raw); - } catch (error) { - console.warn('[timeplot] failed to load persisted state', error); - return null; - } -} - -function savePersistedState(state) { - if (typeof localStorage === 'undefined') { - return; - } - - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(createPersistableState(state))); - } catch (error) { - console.warn('[timeplot] failed to persist state', error); - } -} - -export function createInitialState() { - return { - app: { - title: 'TimePlot', - renderer: 'pending', - }, - time: { - realNowMs: Date.now(), - realElapsedMs: 0, - plotTimeMs: 0, - speed: 1, - paused: false, - }, - plot: { - showGrid: true, - showPoints: true, - windowDurationMs: 20000, - maxPoints: 1600, - valueRange: { - min: -1.6, - max: 1.6, - }, - hoveredPoint: null, - tooltip: { - visible: false, - x: 0, - y: 0, - point: null, - }, - }, - sources: { - signalA: { - id: 'signal-a', - label: 'Signal A', - type: 'synthetic-wave', - preset: 'telemetry', - sampleRateHz: 60, - amplitude: 1, - noise: 0.08, - replayRate: 1, - dataset: [], - dataFileName: '', - datasetPointCount: 0, - datasetDurationMs: 0, - loadError: '', - wsUrl: 'ws://localhost:8080', - wsReconnectMs: 2000, - wsStatus: 'idle', - wsStatusDetail: '', - }, - signalB: { - id: 'signal-b', - label: 'Signal B', - type: 'synthetic-wave', - preset: 'chirp', - sampleRateHz: 48, - amplitude: 0.8, - noise: 0.04, - replayRate: 1, - dataset: [], - dataFileName: '', - datasetPointCount: 0, - datasetDurationMs: 0, - loadError: '', - wsUrl: 'ws://localhost:8080', - wsReconnectMs: 2000, - wsStatus: 'idle', - wsStatusDetail: '', - }, - }, - graphs: { - primary: { - sourceKey: 'signalA', - transform: 'raw', - title: 'Primary signal', - }, - secondary: { - sourceKey: 'signalB', - transform: 'delta', - title: 'Secondary signal', - }, - }, - panels: { - status: { title: 'Status', visible: true }, - source: { title: 'Data Source', visible: true }, - config: { title: 'Config', visible: true }, - help: { title: 'Help', visible: false }, - }, - }; -} - -export class Store { - constructor(initialState = createInitialState()) { - this.state = mergePersistedState(initialState, loadPersistedState()); - this.listeners = new Set(); - } - - getState() { - return this.state; - } - - subscribe(listener) { - this.listeners.add(listener); - return () => this.listeners.delete(listener); - } - - setState(updater) { - const nextState = typeof updater === 'function' ? updater(this.state) : updater; - this.state = nextState; - savePersistedState(this.state); - for (const listener of this.listeners) { - listener(this.state); - } - } - - patch(partial) { - this.setState((state) => ({ - ...state, - ...partial, - time: partial.time ? { ...state.time, ...partial.time } : state.time, - plot: partial.plot - ? { - ...state.plot, - ...partial.plot, - valueRange: partial.plot.valueRange - ? { ...state.plot.valueRange, ...partial.plot.valueRange } - : state.plot.valueRange, - tooltip: partial.plot.tooltip - ? { ...state.plot.tooltip, ...partial.plot.tooltip } - : state.plot.tooltip, - } - : state.plot, - sources: partial.sources - ? Object.fromEntries(Object.entries({ ...state.sources, ...partial.sources }).map(([key, value]) => [ - key, - { ...state.sources[key], ...value }, - ])) - : state.sources, - graphs: partial.graphs - ? cloneNamedState(Object.fromEntries(Object.entries({ ...state.graphs, ...partial.graphs }).map(([key, value]) => [ - key, - { ...state.graphs[key], ...value }, - ]))) - : state.graphs, - panels: partial.panels ? clonePanelState({ ...state.panels, ...partial.panels }) : state.panels, - })); - } -} diff --git a/web-timeplot/src/core/time-controller.js b/web-timeplot/src/core/time-controller.js deleted file mode 100644 index 7cd57c7..0000000 --- a/web-timeplot/src/core/time-controller.js +++ /dev/null @@ -1,80 +0,0 @@ -export class TimeController { - constructor(store) { - this.store = store; - this.lastFrameTime = performance.now(); - } - - tick(now = performance.now()) { - const deltaMs = now - this.lastFrameTime; - this.lastFrameTime = now; - - this.store.setState((state) => { - const realElapsedMs = state.time.realElapsedMs + deltaMs; - const plotDeltaMs = state.time.paused ? 0 : deltaMs * state.time.speed; - - return { - ...state, - time: { - ...state.time, - realNowMs: Date.now(), - realElapsedMs, - plotTimeMs: Math.max(0, state.time.plotTimeMs + plotDeltaMs), - }, - }; - }); - - return deltaMs; - } - - togglePause() { - this.store.setState((state) => ({ - ...state, - time: { - ...state.time, - paused: !state.time.paused, - }, - })); - } - - setPaused(paused) { - this.store.setState((state) => ({ - ...state, - time: { - ...state.time, - paused, - }, - })); - } - - setSpeed(speed) { - const clampedSpeed = Math.max(0.1, Math.min(12, speed)); - this.store.setState((state) => ({ - ...state, - time: { - ...state.time, - speed: clampedSpeed, - }, - })); - } - - reset() { - this.store.setState((state) => ({ - ...state, - time: { - ...state.time, - realElapsedMs: 0, - plotTimeMs: 0, - }, - plot: { - ...state.plot, - hoveredPoint: null, - tooltip: { - ...state.plot.tooltip, - visible: false, - point: null, - }, - }, - })); - this.lastFrameTime = performance.now(); - } -} |
