FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
index.ts
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
3
4import { create } from 'zustand';
5import { immer } from 'zustand/middleware/immer';
6import { ScenarioStore } from './types';
7import { defaultGlobalParameters } from './defaults';
8import { createAssetSlice } from './slices/assetSlice';
9import { createBackendSlice } from './slices/backendSlice';
10import { createPlatformSlice } from './slices/platformSlice';
11import { createScenarioSlice } from './slices/scenarioSlice';
12export * from './types';
13export * from './utils';
14
15export const useScenarioStore = create<ScenarioStore>()(
16 immer((set, get, store) => ({
17 // Initial State
18 globalParameters: defaultGlobalParameters,
19 waveforms: [],
20 timings: [],
21 antennas: [],
22 platforms: [],
23 selectedItemId: null,
24 isDirty: false,
25 isPlaying: false,
26 currentTime: 0,
27 targetPlaybackDuration: null,
28 isSimulating: false,
29 isBackendSyncing: false,
30 backendVersion: 0,
31 errorSnackbar: {
32 open: false,
33 message: '',
34 },
35 viewControlAction: { type: null, timestamp: 0 },
36 visibility: {
37 showAxes: true,
38 showPatterns: true,
39 showBoresights: true,
40 showLinks: true,
41 showLinkLabels: true,
42 showLinkMonostatic: true,
43 showLinkIlluminator: true,
44 showLinkScattered: true,
45 showLinkDirect: true,
46 showVelocities: true,
47 showPlatforms: true,
48 showPlatformLabels: true,
49 showMotionPaths: true,
50 },
51
52 // Slices
53 ...createAssetSlice(set, get, store),
54 ...createBackendSlice(set, get, store),
55 ...createPlatformSlice(set, get, store),
56 ...createScenarioSlice(set, get, store),
57
58 // Playback Actions
59 togglePlayPause: () =>
60 set((state) => ({ isPlaying: !state.isPlaying })),
61 setCurrentTime: (time) => {
62 const { start, end } = get().globalParameters;
63 const newTime =
64 typeof time === 'function' ? time(get().currentTime) : time;
65 // Clamp time to simulation bounds
66 const clampedTime = Math.max(start, Math.min(end, newTime));
67 set({ currentTime: clampedTime });
68 },
69 setTargetPlaybackDuration: (duration) =>
70 set({
71 targetPlaybackDuration:
72 duration !== null && duration > 0 ? duration : null,
73 }),
74 setIsSimulating: (isSimulating) => set({ isSimulating }),
75
76 frameScene: () =>
77 set({
78 viewControlAction: { type: 'frame', timestamp: Date.now() },
79 }),
80 focusOnItem: (itemId) =>
81 set({
82 viewControlAction: {
83 type: 'focus',
84 targetId: itemId,
85 timestamp: Date.now(),
86 },
87 }),
88 toggleFollowItem: (itemId) => {
89 const currentAction = get().viewControlAction;
90 if (
91 currentAction.type === 'follow' &&
92 currentAction.targetId === itemId
93 ) {
94 set({
95 viewControlAction: { type: null, timestamp: Date.now() },
96 });
97 } else {
98 set({
99 viewControlAction: {
100 type: 'follow',
101 targetId: itemId,
102 timestamp: Date.now(),
103 },
104 });
105 }
106 },
107 clearViewControlAction: () =>
108 set((state) => {
109 if (state.viewControlAction.type !== 'follow') {
110 return {
111 viewControlAction: {
112 type: null,
113 timestamp: state.viewControlAction.timestamp,
114 },
115 };
116 }
117 return {};
118 }),
119 toggleLayer: (layer) =>
120 set((state) => {
121 state.visibility[layer] = !state.visibility[layer];
122 }),
123
124 // Error Actions
125 showError: (message) => set({ errorSnackbar: { open: true, message } }),
126 hideError: () =>
127 set((state) => ({
128 errorSnackbar: { ...state.errorSnackbar, open: false },
129 })),
130 }))
131);
132
133let debounceTimer: ReturnType<typeof setTimeout>;
134
135useScenarioStore.subscribe((state, prevState) => {
136 // Check for structural changes in the scenario data using reference equality.
137 // Immer ensures that if data changes, the array/object reference changes.
138 const hasStructuralChanges =
139 state.platforms !== prevState.platforms ||
140 state.antennas !== prevState.antennas ||
141 state.waveforms !== prevState.waveforms ||
142 state.timings !== prevState.timings ||
143 state.globalParameters !== prevState.globalParameters;
144
145 // We only trigger sync if data changed AND we aren't currently simulating/playing
146 // (though simulation usually locks UI, checking prevents edge cases).
147 if (hasStructuralChanges && !state.isSimulating) {
148 if (debounceTimer) clearTimeout(debounceTimer);
149
150 debounceTimer = setTimeout(() => {
151 // Trigger the sync action defined in backendSlice
152 void state.syncBackend();
153 }, 500);
154 }
155});