@ingissa/navcore-simulator 1.0.4 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/LICENSE +3 -0
  2. package/README.md +10 -18
  3. package/dist/core/src/NavCore.d.ts +142 -0
  4. package/dist/core/src/NavCore.d.ts.map +1 -0
  5. package/dist/core/src/NavCore.js +390 -0
  6. package/dist/core/src/bearing/BearingEngine.d.ts +24 -0
  7. package/dist/core/src/bearing/BearingEngine.d.ts.map +1 -0
  8. package/dist/core/src/bearing/BearingEngine.js +50 -0
  9. package/dist/core/src/constants.d.ts +34 -0
  10. package/dist/core/src/constants.d.ts.map +1 -0
  11. package/dist/core/src/constants.js +41 -0
  12. package/dist/core/src/corridor/DeviationDetector.d.ts +33 -0
  13. package/dist/core/src/corridor/DeviationDetector.d.ts.map +1 -0
  14. package/dist/core/src/corridor/DeviationDetector.js +83 -0
  15. package/dist/core/src/corridor/DynamicCorridor.d.ts +13 -0
  16. package/dist/core/src/corridor/DynamicCorridor.d.ts.map +1 -0
  17. package/dist/core/src/corridor/DynamicCorridor.js +26 -0
  18. package/dist/core/src/dead-reckoning/DeadReckoning.d.ts +35 -0
  19. package/dist/core/src/dead-reckoning/DeadReckoning.d.ts.map +1 -0
  20. package/dist/core/src/dead-reckoning/DeadReckoning.js +49 -0
  21. package/dist/core/src/directions/DirectionsProvider.d.ts +70 -0
  22. package/dist/core/src/directions/DirectionsProvider.d.ts.map +1 -0
  23. package/dist/core/src/directions/DirectionsProvider.js +7 -0
  24. package/dist/core/src/directions/OSRMDirectionsProvider.d.ts +26 -0
  25. package/dist/core/src/directions/OSRMDirectionsProvider.d.ts.map +1 -0
  26. package/dist/core/src/directions/OSRMDirectionsProvider.js +90 -0
  27. package/dist/core/src/directions/OpenRouteServiceProvider.d.ts +30 -0
  28. package/dist/core/src/directions/OpenRouteServiceProvider.d.ts.map +1 -0
  29. package/dist/core/src/directions/OpenRouteServiceProvider.js +111 -0
  30. package/dist/core/src/directions/ValhallaDirectionsProvider.d.ts +33 -0
  31. package/dist/core/src/directions/ValhallaDirectionsProvider.d.ts.map +1 -0
  32. package/dist/core/src/directions/ValhallaDirectionsProvider.js +155 -0
  33. package/dist/core/src/eta/ETAEngine.d.ts +61 -0
  34. package/dist/core/src/eta/ETAEngine.d.ts.map +1 -0
  35. package/dist/core/src/eta/ETAEngine.js +74 -0
  36. package/dist/core/src/features.d.ts +76 -0
  37. package/dist/core/src/features.d.ts.map +1 -0
  38. package/dist/core/src/features.js +76 -0
  39. package/dist/core/src/geo/bearing.d.ts +11 -0
  40. package/dist/core/src/geo/bearing.d.ts.map +1 -0
  41. package/dist/core/src/geo/bearing.js +25 -0
  42. package/dist/core/src/geo/cumulative.d.ts +9 -0
  43. package/dist/core/src/geo/cumulative.d.ts.map +1 -0
  44. package/dist/core/src/geo/cumulative.js +20 -0
  45. package/dist/core/src/geo/haversine.d.ts +13 -0
  46. package/dist/core/src/geo/haversine.d.ts.map +1 -0
  47. package/dist/core/src/geo/haversine.js +39 -0
  48. package/dist/core/src/geo/projection.d.ts +7 -0
  49. package/dist/core/src/geo/projection.d.ts.map +1 -0
  50. package/dist/core/src/geo/projection.js +17 -0
  51. package/dist/core/src/geofencing/GeofencingEngine.d.ts +93 -0
  52. package/dist/core/src/geofencing/GeofencingEngine.d.ts.map +1 -0
  53. package/dist/core/src/geofencing/GeofencingEngine.js +154 -0
  54. package/dist/core/src/index.d.ts +66 -0
  55. package/dist/core/src/index.d.ts.map +1 -0
  56. package/dist/core/src/index.js +62 -0
  57. package/dist/core/src/kalman/KalmanFilter2D.d.ts +46 -0
  58. package/dist/core/src/kalman/KalmanFilter2D.d.ts.map +1 -0
  59. package/dist/core/src/kalman/KalmanFilter2D.js +77 -0
  60. package/dist/core/src/license/LicenseManager.d.ts +40 -0
  61. package/dist/core/src/license/LicenseManager.d.ts.map +1 -0
  62. package/dist/core/src/license/LicenseManager.js +119 -0
  63. package/dist/core/src/license/verifyP256Signature.d.ts +2 -0
  64. package/dist/core/src/license/verifyP256Signature.d.ts.map +1 -0
  65. package/dist/core/src/license/verifyP256Signature.js +26 -0
  66. package/dist/core/src/off-route/NavigationFSM.d.ts +42 -0
  67. package/dist/core/src/off-route/NavigationFSM.d.ts.map +1 -0
  68. package/dist/core/src/off-route/NavigationFSM.js +56 -0
  69. package/dist/core/src/off-route/OffRouteFSM.d.ts +70 -0
  70. package/dist/core/src/off-route/OffRouteFSM.d.ts.map +1 -0
  71. package/dist/core/src/off-route/OffRouteFSM.js +151 -0
  72. package/dist/core/src/predictor/RouteOffsetPredictor.d.ts +24 -0
  73. package/dist/core/src/predictor/RouteOffsetPredictor.d.ts.map +1 -0
  74. package/dist/core/src/predictor/RouteOffsetPredictor.js +55 -0
  75. package/dist/core/src/progress/InstructionEditor.d.ts +77 -0
  76. package/dist/core/src/progress/InstructionEditor.d.ts.map +1 -0
  77. package/dist/core/src/progress/InstructionEditor.js +151 -0
  78. package/dist/core/src/progress/InstructionResolver.d.ts +25 -0
  79. package/dist/core/src/progress/InstructionResolver.d.ts.map +1 -0
  80. package/dist/core/src/progress/InstructionResolver.js +78 -0
  81. package/dist/core/src/progress/ProgressTracker.d.ts +12 -0
  82. package/dist/core/src/progress/ProgressTracker.d.ts.map +1 -0
  83. package/dist/core/src/progress/ProgressTracker.js +22 -0
  84. package/dist/core/src/renderer/MapRendererAdapter.d.ts +72 -0
  85. package/dist/core/src/renderer/MapRendererAdapter.d.ts.map +1 -0
  86. package/dist/core/src/renderer/MapRendererAdapter.js +19 -0
  87. package/dist/core/src/route-builder/CustomRouteBuilder.d.ts +83 -0
  88. package/dist/core/src/route-builder/CustomRouteBuilder.d.ts.map +1 -0
  89. package/dist/core/src/route-builder/CustomRouteBuilder.js +160 -0
  90. package/dist/core/src/route-plan/RoutePlan.d.ts +38 -0
  91. package/dist/core/src/route-plan/RoutePlan.d.ts.map +1 -0
  92. package/dist/core/src/route-plan/RoutePlan.js +102 -0
  93. package/dist/core/src/session/NavigationSession.d.ts +81 -0
  94. package/dist/core/src/session/NavigationSession.d.ts.map +1 -0
  95. package/dist/core/src/session/NavigationSession.js +115 -0
  96. package/dist/core/src/snapper/ParallelRoadResolver.d.ts +86 -0
  97. package/dist/core/src/snapper/ParallelRoadResolver.d.ts.map +1 -0
  98. package/dist/core/src/snapper/ParallelRoadResolver.js +83 -0
  99. package/dist/core/src/snapper/RouteSnapper.d.ts +38 -0
  100. package/dist/core/src/snapper/RouteSnapper.d.ts.map +1 -0
  101. package/dist/core/src/snapper/RouteSnapper.js +80 -0
  102. package/dist/core/src/voice/VoiceTriggerEngine.d.ts +80 -0
  103. package/dist/core/src/voice/VoiceTriggerEngine.d.ts.map +1 -0
  104. package/dist/core/src/voice/VoiceTriggerEngine.js +108 -0
  105. package/dist/simulator/src/core/NoiseEngine.d.ts +46 -0
  106. package/dist/simulator/src/core/NoiseEngine.d.ts.map +1 -0
  107. package/dist/simulator/src/core/NoiseEngine.js +106 -0
  108. package/dist/simulator/src/core/RouteWalker.d.ts +78 -0
  109. package/dist/simulator/src/core/RouteWalker.d.ts.map +1 -0
  110. package/dist/simulator/src/core/RouteWalker.js +180 -0
  111. package/dist/simulator/src/core/SimulatorEngine.d.ts +53 -0
  112. package/dist/simulator/src/core/SimulatorEngine.d.ts.map +1 -0
  113. package/dist/simulator/src/core/SimulatorEngine.js +298 -0
  114. package/dist/simulator/src/core/types.d.ts +157 -0
  115. package/dist/simulator/src/core/types.d.ts.map +1 -0
  116. package/dist/simulator/src/core/types.js +7 -0
  117. package/dist/simulator/src/fleet/FleetSimulator.d.ts +60 -0
  118. package/dist/simulator/src/fleet/FleetSimulator.d.ts.map +1 -0
  119. package/dist/simulator/src/fleet/FleetSimulator.js +114 -0
  120. package/dist/simulator/src/index.d.ts +28 -0
  121. package/dist/simulator/src/index.d.ts.map +1 -0
  122. package/dist/simulator/src/index.js +29 -0
  123. package/dist/simulator/src/node.d.ts +26 -0
  124. package/dist/simulator/src/node.d.ts.map +1 -0
  125. package/dist/simulator/src/node.js +25 -0
  126. package/dist/simulator/src/react-native/SimulatorControls.d.ts +35 -0
  127. package/dist/simulator/src/react-native/SimulatorControls.d.ts.map +1 -0
  128. package/dist/simulator/src/react-native/SimulatorControls.js +254 -0
  129. package/dist/simulator/src/react-native/useSimulator.d.ts +59 -0
  130. package/dist/simulator/src/react-native/useSimulator.d.ts.map +1 -0
  131. package/dist/simulator/src/react-native/useSimulator.js +100 -0
  132. package/dist/simulator/src/replay/GpxParser.d.ts +40 -0
  133. package/dist/simulator/src/replay/GpxParser.d.ts.map +1 -0
  134. package/dist/simulator/src/replay/GpxParser.js +95 -0
  135. package/dist/simulator/src/replay/SessionRecorder.d.ts +37 -0
  136. package/dist/simulator/src/replay/SessionRecorder.d.ts.map +1 -0
  137. package/dist/simulator/src/replay/SessionRecorder.js +120 -0
  138. package/dist/simulator/src/scenarios/ScenarioInjector.d.ts +55 -0
  139. package/dist/simulator/src/scenarios/ScenarioInjector.d.ts.map +1 -0
  140. package/dist/simulator/src/scenarios/ScenarioInjector.js +150 -0
  141. package/package.json +23 -23
  142. package/src/__tests__/integration/simulator-pipeline.test.ts +233 -0
  143. package/src/core/NoiseEngine.ts +137 -0
  144. package/src/core/RouteWalker.ts +227 -0
  145. package/src/core/SimulatorEngine.ts +364 -0
  146. package/src/core/__tests__/NoiseEngine.test.ts +131 -0
  147. package/src/core/__tests__/RouteWalker.test.ts +139 -0
  148. package/src/core/__tests__/SimulatorEngine.test.ts +317 -0
  149. package/src/core/types.ts +191 -0
  150. package/src/fleet/FleetSimulator.ts +162 -0
  151. package/src/index.ts +54 -0
  152. package/src/node.ts +48 -0
  153. package/src/react-native/SimulatorControls.tsx +473 -0
  154. package/src/react-native/useSimulator.ts +155 -0
  155. package/src/replay/GpxParser.ts +122 -0
  156. package/src/replay/SessionRecorder.ts +138 -0
  157. package/src/replay/__tests__/GpxParser.test.ts +136 -0
  158. package/src/scenarios/ScenarioInjector.ts +170 -0
  159. package/src/scenarios/__tests__/ScenarioInjector.test.ts +171 -0
  160. package/dist/index-i9TY71RY.d.mts +0 -494
  161. package/dist/index-i9TY71RY.d.ts +0 -494
  162. package/dist/index.d.mts +0 -1
  163. package/dist/index.d.ts +0 -1
  164. package/dist/index.js +0 -937
  165. package/dist/index.mjs +0 -929
  166. package/dist/node.d.mts +0 -1
  167. package/dist/node.d.ts +0 -1
  168. package/dist/node.js +0 -937
  169. package/dist/node.mjs +0 -929
  170. package/dist/react-native.d.mts +0 -82
  171. package/dist/react-native.d.ts +0 -82
  172. package/dist/react-native.js +0 -1370
  173. package/dist/react-native.mjs +0 -1360
package/src/index.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @navcore/simulator — Public API barrel.
3
+ *
4
+ * Import everything from this file. Do NOT import from sub-paths directly.
5
+ *
6
+ * Example:
7
+ * import { SimulatorEngine, NoiseEngine, ScenarioInjector, GpxParser } from '@ingissa/navcore-simulator'
8
+ *
9
+ * React Native consumers also get:
10
+ * import { useSimulator, SimulatorControls } from '@ingissa/navcore-simulator'
11
+ */
12
+
13
+ // ── Core types ────────────────────────────────────────────────────────────────
14
+ export type {
15
+ Coordinate,
16
+ SimPosition,
17
+ SimLocationCoords,
18
+ SimLocationObject,
19
+ SimulatorConfig,
20
+ NoiseProfile,
21
+ Scenario,
22
+ SimSession,
23
+ } from './core/types';
24
+
25
+ // ── Engine ────────────────────────────────────────────────────────────────────
26
+ export { SimulatorEngine } from './core/SimulatorEngine';
27
+ export type { EngineState } from './core/SimulatorEngine';
28
+
29
+ // ── RouteWalker ───────────────────────────────────────────────────────────────
30
+ export { RouteWalker } from './core/RouteWalker';
31
+ export type { WalkerPosition } from './core/RouteWalker';
32
+
33
+ // ── NoiseEngine ───────────────────────────────────────────────────────────────
34
+ export { NoiseEngine } from './core/NoiseEngine';
35
+
36
+ // ── Scenarios ─────────────────────────────────────────────────────────────────
37
+ export { ScenarioInjector } from './scenarios/ScenarioInjector';
38
+
39
+ // ── Replay ────────────────────────────────────────────────────────────────────
40
+ export { GpxParser } from './replay/GpxParser';
41
+ export type { GpxParseResult } from './replay/GpxParser';
42
+ export { SessionRecorder } from './replay/SessionRecorder';
43
+ export type { ExportFormat } from './replay/SessionRecorder';
44
+
45
+ // ── Fleet (Phase 2) ───────────────────────────────────────────────────────────
46
+ export { FleetSimulator } from './fleet/FleetSimulator';
47
+ export type { VehicleProfile, FleetReport } from './fleet/FleetSimulator';
48
+
49
+ // ── React Native ─────────────────────────────────────────────────────────────
50
+ // Import these from the top-level package — no sub-path imports required:
51
+ // import { useSimulator, SimulatorControls } from '@ingissa/navcore-simulator'
52
+ export { useSimulator } from './react-native/useSimulator';
53
+ export { SimulatorControls } from './react-native/SimulatorControls';
54
+ export type { ScenarioLogEntry } from './react-native/SimulatorControls';
package/src/node.ts ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @navcore/simulator — Node.js / CI barrel
3
+ *
4
+ * This barrel exports ONLY the headless classes that have no React Native
5
+ * dependencies. It is used by vitest (via the @navcore/simulator alias in
6
+ * vitest.config.ts) so that unit tests never pull in react-native and
7
+ * trigger Vite's Flow-syntax parser failure.
8
+ *
9
+ * React Native consumers (useSimulator, SimulatorControls) must import
10
+ * from the main barrel:
11
+ * import { useSimulator } from '@ingissa/navcore-simulator' ← full barrel
12
+ */
13
+
14
+ // ── Core types ─────────────────────────────────────────────────────────────────
15
+ export type {
16
+ Coordinate,
17
+ SimPosition,
18
+ SimLocationCoords,
19
+ SimLocationObject,
20
+ SimulatorConfig,
21
+ NoiseProfile,
22
+ Scenario,
23
+ SimSession,
24
+ } from './core/types';
25
+
26
+ // ── Engine ─────────────────────────────────────────────────────────────────────
27
+ export { SimulatorEngine } from './core/SimulatorEngine';
28
+ export type { EngineState } from './core/SimulatorEngine';
29
+
30
+ // ── RouteWalker ────────────────────────────────────────────────────────────────
31
+ export { RouteWalker } from './core/RouteWalker';
32
+ export type { WalkerPosition } from './core/RouteWalker';
33
+
34
+ // ── NoiseEngine ────────────────────────────────────────────────────────────────
35
+ export { NoiseEngine } from './core/NoiseEngine';
36
+
37
+ // ── Scenarios ──────────────────────────────────────────────────────────────────
38
+ export { ScenarioInjector } from './scenarios/ScenarioInjector';
39
+
40
+ // ── Replay ─────────────────────────────────────────────────────────────────────
41
+ export { GpxParser } from './replay/GpxParser';
42
+ export type { GpxParseResult } from './replay/GpxParser';
43
+ export { SessionRecorder } from './replay/SessionRecorder';
44
+ export type { ExportFormat } from './replay/SessionRecorder';
45
+
46
+ // ── Fleet (Phase 2 skeleton) ───────────────────────────────────────────────────
47
+ export { FleetSimulator } from './fleet/FleetSimulator';
48
+ export type { VehicleProfile, FleetReport } from './fleet/FleetSimulator';
@@ -0,0 +1,473 @@
1
+ /**
2
+ * SimulatorControls — Self-contained React Native control panel for SimulatorEngine.
3
+ *
4
+ * Three style variants:
5
+ * 'minimal' — compact horizontal bar: speed selector + pause/resume
6
+ * 'full' — full card with speed, seek, scenario log, recorder, stats
7
+ * 'floating' — collapsible bottom sheet (Animated, no external libs)
8
+ *
9
+ * Colour tokens match the Klaroways design system used in NavCoreTestScreen.
10
+ */
11
+
12
+ import React, { useState, useRef, useCallback } from 'react';
13
+ import {
14
+ View,
15
+ Text,
16
+ TouchableOpacity,
17
+ StyleSheet,
18
+ ScrollView,
19
+ Animated,
20
+ Platform,
21
+ } from 'react-native';
22
+ import type { SimulatorControls as Controls, SessionControls } from './useSimulator';
23
+ import type { SimSession } from '../core/types';
24
+
25
+ // ─── Colour Tokens ────────────────────────────────────────────────────────────
26
+ const KW = {
27
+ navyDeep: '#081525',
28
+ navyCard: '#152240',
29
+ navyBorder: '#1E3255',
30
+ emerald: '#00BF76',
31
+ emeraldDark: '#04342C',
32
+ emeraldMid: '#009A5F',
33
+ amber: '#F5A623',
34
+ amberDark: '#412402',
35
+ slate: '#6B7FA3',
36
+ slateLight: '#8FA3C8',
37
+ light: '#EBEFF8',
38
+ fault: '#FF4848',
39
+ faultDark: '#3D0000',
40
+ white: '#FFFFFF',
41
+ mono: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
42
+ } as const;
43
+
44
+ // ─── Types ────────────────────────────────────────────────────────────────────
45
+
46
+ export interface ScenarioLogEntry {
47
+ ts: number;
48
+ name: string;
49
+ type: string;
50
+ }
51
+
52
+ export interface SimulatorControlsProps {
53
+ simulator: Controls;
54
+ session?: SessionControls;
55
+ style?: 'minimal' | 'full' | 'floating';
56
+ showScenarioLog?: boolean;
57
+ showSessionRecorder?: boolean;
58
+ scenarioLog?: ScenarioLogEntry[];
59
+ /** Current speed multiplier (controlled externally) */
60
+ currentSpeed?: 1 | 2 | 4 | 8;
61
+ /** Current engine state */
62
+ engineState?: 'idle' | 'running' | 'paused' | 'complete';
63
+ isRecording?: boolean;
64
+ onExportSession?: (session: SimSession) => void;
65
+ /** Additional style for the root container */
66
+ containerStyle?: object;
67
+ }
68
+
69
+ // ─── Speed Selector ───────────────────────────────────────────────────────────
70
+
71
+ function SpeedSelector({
72
+ current,
73
+ onChange,
74
+ }: {
75
+ current: 1 | 2 | 4 | 8;
76
+ onChange: (v: 1 | 2 | 4 | 8) => void;
77
+ }) {
78
+ return (
79
+ <View style={spd.row}>
80
+ <Text style={spd.label}>SIM SPEED</Text>
81
+ {([1, 2, 4, 8] as const).map((v) => (
82
+ <TouchableOpacity
83
+ key={v}
84
+ style={[spd.btn, current === v && spd.btnActive]}
85
+ onPress={() => onChange(v)}
86
+ activeOpacity={0.7}
87
+ >
88
+ <Text style={[spd.btnText, current === v && { color: KW.emeraldDark }]}>
89
+ {v}×
90
+ </Text>
91
+ </TouchableOpacity>
92
+ ))}
93
+ </View>
94
+ );
95
+ }
96
+
97
+ const spd = StyleSheet.create({
98
+ row: { flexDirection: 'row', alignItems: 'center', gap: 6 },
99
+ label: { fontSize: 9, letterSpacing: 1.2, color: KW.slate, marginRight: 4 },
100
+ btn: {
101
+ paddingHorizontal: 10,
102
+ paddingVertical: 5,
103
+ borderRadius: 6,
104
+ borderWidth: 1,
105
+ borderColor: KW.navyBorder,
106
+ },
107
+ btnActive: { backgroundColor: KW.emerald, borderColor: KW.emerald },
108
+ btnText: { fontSize: 11, fontWeight: '700', color: KW.slate },
109
+ });
110
+
111
+ // ─── Pause / Resume ───────────────────────────────────────────────────────────
112
+
113
+ function PauseResumeButton({
114
+ state,
115
+ onPause,
116
+ onResume,
117
+ }: {
118
+ state: 'idle' | 'running' | 'paused' | 'complete';
119
+ onPause: () => void;
120
+ onResume: () => void;
121
+ }) {
122
+ if (state !== 'running' && state !== 'paused') return null;
123
+ return (
124
+ <TouchableOpacity
125
+ style={[pr.btn, state === 'paused' && pr.btnResume]}
126
+ onPress={state === 'running' ? onPause : onResume}
127
+ activeOpacity={0.75}
128
+ >
129
+ <Text style={pr.text}>{state === 'running' ? '⏸' : '▶'}</Text>
130
+ </TouchableOpacity>
131
+ );
132
+ }
133
+
134
+ const pr = StyleSheet.create({
135
+ btn: {
136
+ paddingHorizontal: 14,
137
+ paddingVertical: 7,
138
+ borderRadius: 8,
139
+ backgroundColor: KW.navyDeep,
140
+ borderWidth: 1,
141
+ borderColor: KW.navyBorder,
142
+ alignItems: 'center',
143
+ },
144
+ btnResume: { borderColor: KW.emerald },
145
+ text: { fontSize: 16 },
146
+ });
147
+
148
+ // ─── Scenario Log ─────────────────────────────────────────────────────────────
149
+
150
+ function ScenarioLog({ entries }: { entries: ScenarioLogEntry[] }) {
151
+ return (
152
+ <View style={log.container}>
153
+ <Text style={log.title}>// scenario log</Text>
154
+ <ScrollView style={log.scroll} showsVerticalScrollIndicator={false}>
155
+ {entries.length === 0 ? (
156
+ <Text style={log.empty}>No scenarios fired yet…</Text>
157
+ ) : (
158
+ entries.map((e, i) => {
159
+ const d = new Date(e.ts);
160
+ const time = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
161
+ return (
162
+ <Text key={i} style={log.entry}>
163
+ <Text style={log.time}>{time} </Text>
164
+ <Text style={{ color: KW.amber }}>[{e.type}]</Text>
165
+ {' '}{e.name}
166
+ </Text>
167
+ );
168
+ })
169
+ )}
170
+ </ScrollView>
171
+ </View>
172
+ );
173
+ }
174
+
175
+ const log = StyleSheet.create({
176
+ container: {
177
+ backgroundColor: KW.navyDeep,
178
+ borderRadius: 8,
179
+ padding: 8,
180
+ borderWidth: 1,
181
+ borderColor: KW.navyBorder,
182
+ maxHeight: 100,
183
+ marginTop: 8,
184
+ },
185
+ title: {
186
+ fontSize: 9,
187
+ letterSpacing: 1,
188
+ color: KW.slate,
189
+ fontFamily: KW.mono,
190
+ marginBottom: 4,
191
+ },
192
+ scroll: { flex: 1 },
193
+ empty: { fontSize: 10, color: KW.slate, fontFamily: KW.mono },
194
+ entry: { fontSize: 10, color: KW.slateLight, fontFamily: KW.mono, marginBottom: 2 },
195
+ time: { color: KW.slate },
196
+ });
197
+
198
+ // ─── Session Recorder Controls ────────────────────────────────────────────────
199
+
200
+ function SessionRecorderControls({
201
+ isRecording,
202
+ session,
203
+ onExport,
204
+ lastSession,
205
+ }: {
206
+ isRecording: boolean;
207
+ session?: SessionControls;
208
+ onExport?: (s: SimSession) => void;
209
+ lastSession: SimSession | null;
210
+ }) {
211
+ if (!session) return null;
212
+
213
+ return (
214
+ <View style={rec.row}>
215
+ <TouchableOpacity
216
+ style={[rec.btn, isRecording && rec.btnActive]}
217
+ onPress={() => {
218
+ if (isRecording) {
219
+ // session stop returns SimSession | null from hook
220
+ } else {
221
+ session.start();
222
+ }
223
+ }}
224
+ activeOpacity={0.75}
225
+ >
226
+ <Text style={rec.btnText}>{isRecording ? '⏹ STOP REC' : '⏺ RECORD'}</Text>
227
+ </TouchableOpacity>
228
+
229
+ {lastSession && onExport && (
230
+ <TouchableOpacity
231
+ style={rec.exportBtn}
232
+ onPress={() => onExport(lastSession)}
233
+ activeOpacity={0.75}
234
+ >
235
+ <Text style={rec.exportText}>EXPORT JSON</Text>
236
+ </TouchableOpacity>
237
+ )}
238
+ </View>
239
+ );
240
+ }
241
+
242
+ const rec = StyleSheet.create({
243
+ row: { flexDirection: 'row', gap: 8, marginTop: 8 },
244
+ btn: {
245
+ flex: 1,
246
+ paddingVertical: 8,
247
+ borderRadius: 8,
248
+ backgroundColor: KW.navyDeep,
249
+ borderWidth: 1,
250
+ borderColor: KW.navyBorder,
251
+ alignItems: 'center',
252
+ },
253
+ btnActive: { borderColor: KW.fault, backgroundColor: KW.faultDark },
254
+ btnText: { fontSize: 11, fontWeight: '700', color: KW.slateLight, letterSpacing: 0.8 },
255
+ exportBtn: {
256
+ paddingHorizontal: 12,
257
+ paddingVertical: 8,
258
+ borderRadius: 8,
259
+ backgroundColor: KW.navyDeep,
260
+ borderWidth: 1,
261
+ borderColor: KW.emeraldMid,
262
+ alignItems: 'center',
263
+ },
264
+ exportText: { fontSize: 10, fontWeight: '700', color: KW.emerald, letterSpacing: 0.5 },
265
+ });
266
+
267
+ // ─── Main Component ───────────────────────────────────────────────────────────
268
+
269
+ export function SimulatorControls({
270
+ simulator,
271
+ session,
272
+ style = 'minimal',
273
+ showScenarioLog = false,
274
+ showSessionRecorder = false,
275
+ scenarioLog = [],
276
+ currentSpeed = 1,
277
+ engineState = 'idle',
278
+ isRecording = false,
279
+ onExportSession,
280
+ containerStyle,
281
+ }: SimulatorControlsProps) {
282
+ const [speed, setSpeedState] = useState<1 | 2 | 4 | 8>(currentSpeed);
283
+ const [lastSession, setLastSession] = useState<SimSession | null>(null);
284
+ // Floating sheet animation
285
+ const sheetAnim = useRef(new Animated.Value(0)).current;
286
+ const [isExpanded, setIsExpanded] = useState(false);
287
+
288
+ const handleSpeedChange = useCallback(
289
+ (v: 1 | 2 | 4 | 8) => {
290
+ setSpeedState(v);
291
+ simulator.setSpeed(v);
292
+ },
293
+ [simulator],
294
+ );
295
+
296
+ const handleStopRecording = useCallback(() => {
297
+ const s = session?.stop();
298
+ if (s) setLastSession(s);
299
+ }, [session]);
300
+
301
+ const toggleSheet = useCallback(() => {
302
+ const toValue = isExpanded ? 0 : 1;
303
+ setIsExpanded(!isExpanded);
304
+ Animated.spring(sheetAnim, {
305
+ toValue,
306
+ useNativeDriver: false,
307
+ bounciness: 4,
308
+ }).start();
309
+ }, [isExpanded, sheetAnim]);
310
+
311
+ // ── Minimal ────────────────────────────────────────────────────────────────
312
+ if (style === 'minimal') {
313
+ return (
314
+ <View style={[s.minimal, containerStyle]}>
315
+ <SpeedSelector current={speed} onChange={handleSpeedChange} />
316
+ <PauseResumeButton
317
+ state={engineState}
318
+ onPause={simulator.pause}
319
+ onResume={simulator.resume}
320
+ />
321
+ </View>
322
+ );
323
+ }
324
+
325
+ // ── Full ───────────────────────────────────────────────────────────────────
326
+ if (style === 'full') {
327
+ return (
328
+ <View style={[s.full, containerStyle]}>
329
+ <View style={s.fullHeader}>
330
+ <Text style={s.fullTitle}>SIMULATOR</Text>
331
+ <View style={[s.stateBadge, engineState === 'running' && s.stateBadgeActive]}>
332
+ <Text style={s.stateText}>{engineState.toUpperCase()}</Text>
333
+ </View>
334
+ </View>
335
+
336
+ <SpeedSelector current={speed} onChange={handleSpeedChange} />
337
+
338
+ <View style={s.pauseRow}>
339
+ <PauseResumeButton
340
+ state={engineState}
341
+ onPause={simulator.pause}
342
+ onResume={simulator.resume}
343
+ />
344
+ </View>
345
+
346
+ {showScenarioLog && <ScenarioLog entries={scenarioLog} />}
347
+
348
+ {showSessionRecorder && (
349
+ <SessionRecorderControls
350
+ isRecording={isRecording}
351
+ session={session}
352
+ onExport={onExportSession}
353
+ lastSession={lastSession}
354
+ />
355
+ )}
356
+ </View>
357
+ );
358
+ }
359
+
360
+ // ── Floating ───────────────────────────────────────────────────────────────
361
+ const sheetHeight = sheetAnim.interpolate({
362
+ inputRange: [0, 1],
363
+ outputRange: [44, showScenarioLog ? 260 : 160],
364
+ });
365
+
366
+ return (
367
+ <Animated.View style={[s.floating, { height: sheetHeight }, containerStyle]}>
368
+ {/* Handle */}
369
+ <TouchableOpacity style={s.floatHandle} onPress={toggleSheet} activeOpacity={0.8}>
370
+ <View style={s.handleBar} />
371
+ <Text style={s.floatTitle}>SIMULATOR {isExpanded ? '▼' : '▲'}</Text>
372
+ </TouchableOpacity>
373
+
374
+ {/* Content (only visible when expanded) */}
375
+ {isExpanded && (
376
+ <View style={s.floatContent}>
377
+ <SpeedSelector current={speed} onChange={handleSpeedChange} />
378
+ <View style={s.pauseRow}>
379
+ <PauseResumeButton
380
+ state={engineState}
381
+ onPause={simulator.pause}
382
+ onResume={simulator.resume}
383
+ />
384
+ </View>
385
+ {showScenarioLog && <ScenarioLog entries={scenarioLog} />}
386
+ </View>
387
+ )}
388
+ </Animated.View>
389
+ );
390
+ }
391
+
392
+ // ─── Styles ───────────────────────────────────────────────────────────────────
393
+ const s = StyleSheet.create({
394
+ // Minimal
395
+ minimal: {
396
+ flexDirection: 'row',
397
+ alignItems: 'center',
398
+ gap: 12,
399
+ paddingHorizontal: 4,
400
+ paddingVertical: 6,
401
+ },
402
+
403
+ // Full
404
+ full: {
405
+ backgroundColor: KW.navyCard,
406
+ borderRadius: 12,
407
+ padding: 14,
408
+ borderWidth: 1,
409
+ borderColor: KW.navyBorder,
410
+ },
411
+ fullHeader: {
412
+ flexDirection: 'row',
413
+ justifyContent: 'space-between',
414
+ alignItems: 'center',
415
+ marginBottom: 10,
416
+ },
417
+ fullTitle: {
418
+ fontSize: 11,
419
+ fontWeight: '800',
420
+ color: KW.slateLight,
421
+ letterSpacing: 1.5,
422
+ },
423
+ stateBadge: {
424
+ paddingHorizontal: 8,
425
+ paddingVertical: 3,
426
+ borderRadius: 4,
427
+ backgroundColor: KW.navyDeep,
428
+ borderWidth: 1,
429
+ borderColor: KW.navyBorder,
430
+ },
431
+ stateBadgeActive: { borderColor: KW.emerald },
432
+ stateText: { fontSize: 9, fontWeight: '700', color: KW.slate, letterSpacing: 1 },
433
+ pauseRow: { marginTop: 8 },
434
+
435
+ // Floating
436
+ floating: {
437
+ position: 'absolute',
438
+ bottom: 0,
439
+ left: 0,
440
+ right: 0,
441
+ backgroundColor: KW.navyCard,
442
+ borderTopLeftRadius: 16,
443
+ borderTopRightRadius: 16,
444
+ borderTopWidth: 1,
445
+ borderColor: KW.navyBorder,
446
+ overflow: 'hidden',
447
+ zIndex: 100,
448
+ },
449
+ floatHandle: {
450
+ height: 44,
451
+ alignItems: 'center',
452
+ justifyContent: 'center',
453
+ flexDirection: 'row',
454
+ gap: 10,
455
+ },
456
+ handleBar: {
457
+ width: 36,
458
+ height: 4,
459
+ backgroundColor: KW.navyBorder,
460
+ borderRadius: 2,
461
+ },
462
+ floatTitle: {
463
+ fontSize: 10,
464
+ fontWeight: '700',
465
+ color: KW.slate,
466
+ letterSpacing: 1.2,
467
+ },
468
+ floatContent: {
469
+ paddingHorizontal: 16,
470
+ paddingBottom: 16,
471
+ gap: 8,
472
+ },
473
+ });