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
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useWindowDimensions, View } from 'react-native';
|
||||
import { ConnectionBanner } from '@/components/ConnectionBanner';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import { colors } from '@/theme/colors';
|
||||
import { spacing } from '@/theme/spacing';
|
||||
import { usePoseStream } from '@/hooks/usePoseStream';
|
||||
import { useMatStore } from '@/stores/matStore';
|
||||
import { type ConnectionStatus } from '@/types/sensing';
|
||||
import { Alert, type Survivor } from '@/types/mat';
|
||||
import { AlertList } from './AlertList';
|
||||
import { MatWebView } from './MatWebView';
|
||||
import { SurvivorCounter } from './SurvivorCounter';
|
||||
import { useMatBridge } from './useMatBridge';
|
||||
|
||||
const isAlert = (value: unknown): value is Alert => {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
return typeof record.id === 'string' && typeof record.message === 'string';
|
||||
};
|
||||
|
||||
const isSurvivor = (value: unknown): value is Survivor => {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
return typeof record.id === 'string' && typeof record.zone_id === 'string';
|
||||
};
|
||||
|
||||
const resolveBannerState = (status: ConnectionStatus): 'connected' | 'simulated' | 'disconnected' => {
|
||||
if (status === 'connecting') {
|
||||
return 'disconnected';
|
||||
}
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
export const MATScreen = () => {
|
||||
const { connectionStatus, lastFrame } = usePoseStream();
|
||||
|
||||
const { survivors, alerts, upsertSurvivor, addAlert, upsertEvent } = useMatStore((state) => ({
|
||||
survivors: state.survivors,
|
||||
alerts: state.alerts,
|
||||
upsertSurvivor: state.upsertSurvivor,
|
||||
addAlert: state.addAlert,
|
||||
upsertEvent: state.upsertEvent,
|
||||
}));
|
||||
|
||||
const { webViewRef, ready, onMessage, sendFrameUpdate, postEvent } = useMatBridge({
|
||||
onSurvivorDetected: (survivor) => {
|
||||
if (isSurvivor(survivor)) {
|
||||
upsertSurvivor(survivor);
|
||||
}
|
||||
},
|
||||
onAlertGenerated: (alert) => {
|
||||
if (isAlert(alert)) {
|
||||
addAlert(alert);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const seededRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready || seededRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const createEvent = postEvent('CREATE_EVENT');
|
||||
createEvent({
|
||||
type: 'earthquake',
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
name: 'Training Scenario',
|
||||
});
|
||||
|
||||
const addZone = postEvent('ADD_ZONE');
|
||||
addZone({
|
||||
name: 'Zone A',
|
||||
zone_type: 'rectangle',
|
||||
x: 60,
|
||||
y: 60,
|
||||
width: 180,
|
||||
height: 120,
|
||||
});
|
||||
addZone({
|
||||
name: 'Zone B',
|
||||
zone_type: 'circle',
|
||||
center_x: 300,
|
||||
center_y: 170,
|
||||
radius: 60,
|
||||
});
|
||||
|
||||
upsertEvent({
|
||||
event_id: 'training-scenario',
|
||||
disaster_type: 1,
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
description: 'Training Scenario',
|
||||
});
|
||||
|
||||
seededRef.current = true;
|
||||
}, [postEvent, upsertEvent, ready]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ready && lastFrame) {
|
||||
sendFrameUpdate(lastFrame);
|
||||
}
|
||||
}, [lastFrame, ready, sendFrameUpdate]);
|
||||
|
||||
const { height } = useWindowDimensions();
|
||||
const webHeight = Math.max(240, Math.floor(height * 0.5));
|
||||
|
||||
return (
|
||||
<ThemedView style={{ flex: 1, backgroundColor: colors.bg, padding: spacing.md }}>
|
||||
<ConnectionBanner status={resolveBannerState(connectionStatus)} />
|
||||
<View style={{ marginTop: 20 }}>
|
||||
<SurvivorCounter survivors={survivors} />
|
||||
</View>
|
||||
<View style={{ height: webHeight }}>
|
||||
<MatWebView
|
||||
webViewRef={webViewRef}
|
||||
onMessage={onMessage}
|
||||
style={{ flex: 1, borderRadius: 12, overflow: 'hidden', backgroundColor: colors.surface }}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1, marginTop: spacing.md }}>
|
||||
<AlertList alerts={alerts} />
|
||||
</View>
|
||||
</ThemedView>
|
||||
);
|
||||
};
|
||||
|
||||
export default MATScreen;
|
||||
|
||||
Reference in New Issue
Block a user