Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 47861de

Browse files
committed
feat: Phase 4 — Live, Vitals, Zones, MAT, Settings screens
LiveScreen: GaussianSplatWebView + gaussian-splats.html (Three.js 3D viz), LiveHUD VitalsScreen: BreathingGauge, HeartRateGauge, MetricCard (Reanimated arcs) ZonesScreen: FloorPlanSvg (SVG heatmap 20x20), ZoneLegend, useOccupancyGrid MATScreen: MatWebView + mat-dashboard.html (pure-JS disaster response), AlertCard/List, SurvivorCounter SettingsScreen: ServerUrlInput (URL validation + test), ThemePicker, RssiToggle Verified: tsc 0 errors, jest passes
1 parent 779bf8f commit 47861de

26 files changed

Lines changed: 3395 additions & 11 deletions

mobile/src/assets/webview/gaussian-splats.html

Lines changed: 585 additions & 0 deletions
Large diffs are not rendered by default.

mobile/src/assets/webview/mat-dashboard.html

Lines changed: 505 additions & 0 deletions
Large diffs are not rendered by default.

mobile/src/components/GaugeArc.tsx

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,58 @@
1-
import { useEffect } from 'react';
1+
import { useEffect, useMemo } from 'react';
22
import { StyleSheet, View } from 'react-native';
3-
import Animated, { useAnimatedProps, useSharedValue, withTiming } from 'react-native-reanimated';
3+
import Animated, { interpolateColor, useAnimatedProps, useSharedValue, withSpring } from 'react-native-reanimated';
44
import Svg, { Circle, G, Text as SvgText } from 'react-native-svg';
55

66
type GaugeArcProps = {
77
value: number;
8+
min?: number;
89
max: number;
910
label: string;
1011
unit: string;
1112
color: string;
13+
colorTo?: string;
1214
size?: number;
1315
};
1416

1517
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
1618

17-
export const GaugeArc = ({ value, max, label, unit, color, size = 140 }: GaugeArcProps) => {
19+
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
20+
21+
export const GaugeArc = ({ value, min = 0, max, label, unit, color, colorTo, size = 140 }: GaugeArcProps) => {
1822
const radius = (size - 20) / 2;
1923
const circumference = 2 * Math.PI * radius;
2024
const arcLength = circumference * 0.75;
2125
const strokeWidth = 12;
2226
const progress = useSharedValue(0);
2327

24-
const normalized = Math.max(0, Math.min(max > 0 ? value / max : 0, 1));
25-
const displayText = `${value.toFixed(1)} ${unit}`;
28+
const normalized = useMemo(() => {
29+
const span = max - min;
30+
const safeSpan = span > 0 ? span : 1;
31+
return clamp((value - min) / safeSpan, 0, 1);
32+
}, [value, min, max]);
33+
34+
const displayValue = useMemo(() => {
35+
if (!Number.isFinite(value)) {
36+
return '--';
37+
}
38+
return `${Math.max(min, Math.min(max, value)).toFixed(1)} ${unit}`;
39+
}, [max, min, unit, value]);
2640

2741
useEffect(() => {
28-
progress.value = withTiming(normalized, { duration: 600 });
42+
progress.value = withSpring(normalized, {
43+
damping: 16,
44+
stiffness: 140,
45+
mass: 1,
46+
});
2947
}, [normalized, progress]);
3048

3149
const animatedStroke = useAnimatedProps(() => {
3250
const dashOffset = arcLength - arcLength * progress.value;
51+
const strokeColor = colorTo ? interpolateColor(progress.value, [0, 1], [color, colorTo]) : color;
52+
3353
return {
3454
strokeDashoffset: dashOffset,
55+
stroke: strokeColor,
3556
};
3657
});
3758

@@ -63,20 +84,20 @@ export const GaugeArc = ({ value, max, label, unit, color, size = 140 }: GaugeAr
6384
</G>
6485
<SvgText
6586
x={size / 2}
66-
y={size / 2 - 4}
87+
y={size / 2 - 8}
6788
fill="#E2E8F0"
68-
fontSize={18}
89+
fontSize={Math.round(size * 0.16)}
6990
fontFamily="Courier New"
7091
fontWeight="700"
7192
textAnchor="middle"
7293
>
73-
{displayText}
94+
{displayValue}
7495
</SvgText>
7596
<SvgText
7697
x={size / 2}
77-
y={size / 2 + 16}
98+
y={size / 2 + 18}
7899
fill="#94A3B8"
79-
fontSize={10}
100+
fontSize={Math.round(size * 0.085)}
80101
fontFamily="Courier New"
81102
textAnchor="middle"
82103
letterSpacing="0.6"
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { LayoutChangeEvent, StyleSheet } from 'react-native';
2+
import type { RefObject } from 'react';
3+
import { WebView, type WebViewMessageEvent } from 'react-native-webview';
4+
import GAUSSIAN_SPLATS_HTML from '@/assets/webview/gaussian-splats.html';
5+
6+
type GaussianSplatWebViewProps = {
7+
onMessage: (event: WebViewMessageEvent) => void;
8+
onError: () => void;
9+
webViewRef: RefObject<WebView | null>;
10+
onLayout?: (event: LayoutChangeEvent) => void;
11+
};
12+
13+
export const GaussianSplatWebView = ({
14+
onMessage,
15+
onError,
16+
webViewRef,
17+
onLayout,
18+
}: GaussianSplatWebViewProps) => {
19+
const html = typeof GAUSSIAN_SPLATS_HTML === 'string' ? GAUSSIAN_SPLATS_HTML : '';
20+
21+
return (
22+
<WebView
23+
ref={webViewRef}
24+
source={{ html }}
25+
originWhitelist={['*']}
26+
allowFileAccess={false}
27+
javaScriptEnabled
28+
onMessage={onMessage}
29+
onError={onError}
30+
onLayout={onLayout}
31+
style={styles.webView}
32+
/>
33+
);
34+
};
35+
36+
const styles = StyleSheet.create({
37+
webView: {
38+
flex: 1,
39+
backgroundColor: '#0A0E1A',
40+
},
41+
});
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { Pressable, StyleSheet, View } from 'react-native';
2+
import { memo, useCallback, useState } from 'react';
3+
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
4+
import { StatusDot } from '@/components/StatusDot';
5+
import { ModeBadge } from '@/components/ModeBadge';
6+
import { ThemedText } from '@/components/ThemedText';
7+
import { formatConfidence, formatRssi } from '@/utils/formatters';
8+
import { colors, spacing } from '@/theme';
9+
import type { ConnectionStatus } from '@/types/sensing';
10+
11+
type LiveMode = 'LIVE' | 'SIM' | 'RSSI';
12+
13+
type LiveHUDProps = {
14+
rssi?: number;
15+
connectionStatus: ConnectionStatus;
16+
fps: number;
17+
confidence: number;
18+
personCount: number;
19+
mode: LiveMode;
20+
};
21+
22+
const statusTextMap: Record<ConnectionStatus, string> = {
23+
connected: 'Connected',
24+
simulated: 'Simulated',
25+
connecting: 'Connecting',
26+
disconnected: 'Disconnected',
27+
};
28+
29+
const statusDotStatusMap: Record<ConnectionStatus, 'connected' | 'simulated' | 'disconnected' | 'connecting'> = {
30+
connected: 'connected',
31+
simulated: 'simulated',
32+
connecting: 'connecting',
33+
disconnected: 'disconnected',
34+
};
35+
36+
export const LiveHUD = memo(
37+
({ rssi, connectionStatus, fps, confidence, personCount, mode }: LiveHUDProps) => {
38+
const [panelVisible, setPanelVisible] = useState(true);
39+
const panelAlpha = useSharedValue(1);
40+
41+
const togglePanel = useCallback(() => {
42+
const next = !panelVisible;
43+
setPanelVisible(next);
44+
panelAlpha.value = withTiming(next ? 1 : 0, { duration: 220 });
45+
}, [panelAlpha, panelVisible]);
46+
47+
const animatedPanelStyle = useAnimatedStyle(() => ({
48+
opacity: panelAlpha.value,
49+
}));
50+
51+
const statusText = statusTextMap[connectionStatus];
52+
53+
return (
54+
<Pressable style={StyleSheet.absoluteFill} onPress={togglePanel}>
55+
<Animated.View pointerEvents="none" style={[StyleSheet.absoluteFill, animatedPanelStyle]}>
56+
{/* App title */}
57+
<View style={styles.topLeft}>
58+
<ThemedText preset="labelLg" style={styles.appTitle}>
59+
WiFi-DensePose
60+
</ThemedText>
61+
</View>
62+
63+
{/* Status + FPS */}
64+
<View style={styles.topRight}>
65+
<View style={styles.row}>
66+
<StatusDot status={statusDotStatusMap[connectionStatus]} size={10} />
67+
<ThemedText preset="labelMd" style={styles.statusText}>
68+
{statusText}
69+
</ThemedText>
70+
</View>
71+
{fps > 0 && (
72+
<View style={styles.row}>
73+
<ThemedText preset="labelMd">{fps} FPS</ThemedText>
74+
</View>
75+
)}
76+
</View>
77+
78+
{/* Bottom panel */}
79+
<View style={styles.bottomPanel}>
80+
<View style={styles.bottomCell}>
81+
<ThemedText preset="bodySm">RSSI</ThemedText>
82+
<ThemedText preset="displayMd" style={styles.bigValue}>
83+
{formatRssi(rssi)}
84+
</ThemedText>
85+
</View>
86+
87+
<View style={styles.bottomCell}>
88+
<ModeBadge mode={mode} />
89+
</View>
90+
91+
<View style={styles.bottomCellRight}>
92+
<ThemedText preset="bodySm">Confidence</ThemedText>
93+
<ThemedText preset="bodyMd" style={styles.metaText}>
94+
{formatConfidence(confidence)}
95+
</ThemedText>
96+
<ThemedText preset="bodySm">People: {personCount}</ThemedText>
97+
</View>
98+
</View>
99+
</Animated.View>
100+
</Pressable>
101+
);
102+
},
103+
);
104+
105+
const styles = StyleSheet.create({
106+
topLeft: {
107+
position: 'absolute',
108+
top: spacing.md,
109+
left: spacing.md,
110+
},
111+
appTitle: {
112+
color: colors.textPrimary,
113+
},
114+
topRight: {
115+
position: 'absolute',
116+
top: spacing.md,
117+
right: spacing.md,
118+
alignItems: 'flex-end',
119+
gap: 4,
120+
},
121+
row: {
122+
flexDirection: 'row',
123+
alignItems: 'center',
124+
gap: spacing.sm,
125+
},
126+
statusText: {
127+
color: colors.textPrimary,
128+
},
129+
bottomPanel: {
130+
position: 'absolute',
131+
left: spacing.sm,
132+
right: spacing.sm,
133+
bottom: spacing.sm,
134+
minHeight: 72,
135+
borderRadius: 12,
136+
backgroundColor: 'rgba(10,14,26,0.72)',
137+
borderWidth: 1,
138+
borderColor: 'rgba(50,184,198,0.35)',
139+
paddingHorizontal: spacing.md,
140+
paddingVertical: spacing.sm,
141+
flexDirection: 'row',
142+
justifyContent: 'space-between',
143+
alignItems: 'center',
144+
},
145+
bottomCell: {
146+
flex: 1,
147+
alignItems: 'center',
148+
},
149+
bottomCellRight: {
150+
flex: 1,
151+
alignItems: 'flex-end',
152+
},
153+
bigValue: {
154+
color: colors.accent,
155+
marginTop: 2,
156+
marginBottom: 2,
157+
},
158+
metaText: {
159+
color: colors.textPrimary,
160+
marginBottom: 4,
161+
},
162+
});
163+
164+
LiveHUD.displayName = 'LiveHUD';

0 commit comments

Comments
 (0)