Files
MapLibreParticleLab/integration/react-native/MapLibreParticleRNTestHost/App.tsx
OpenAI Codex 4467adcce0
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
feat: add RN binary wind field transport
2026-09-09 07:11:17 +08:00

108 lines
6.6 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from "react";
import { Button, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle } from "react-native";
import { Map } from "@maplibre/maplibre-react-native";
import { WeatherRuntimeBridge, type RuntimeSnapshot, type TargetFps } from "@maplibre-particle/weather-runtime-bridge";
import { writeCanonicalWindFixture } from "./src/canonicalWindFixture";
import { weatherTestStyle } from "./src/weatherStyle";
const initialSnapshot: RuntimeSnapshot = { state: "WAITING_FOR_FIELDS", playbackState: "PAUSE", targetFps: "AUTO", disposed: false };
export default function App() {
const [reactTag, setReactTag] = useState<number | null>(null);
const [snapshot, setSnapshot] = useState<RuntimeSnapshot>(initialSnapshot);
const [result, setResult] = useState("Style is loading");
const [opacity, setOpacity] = useState("0.62");
const [binaryTransport, setBinaryTransport] = useState("PENDING: submit fixture to validate FILE_REF / FLOAT32_LE / INTERLEAVED_UV");
const [invalidProbe, setInvalidProbe] = useState("PENDING");
const [t0Submit, setT0Submit] = useState("PENDING");
const [t1Submit, setT1Submit] = useState("PENDING");
const [fieldWindow, setFieldWindow] = useState("TIMELINE_NOT_READY");
const attach = useCallback(async (tag: number) => {
try { setSnapshot(await WeatherRuntimeBridge.attach(tag)); setReactTag(tag); setResult("WeatherRuntime attached"); }
catch (error) { setResult(String(error)); }
}, []);
// MLRN forwards the native MapView event. Its target is the concrete Fabric MLRNMapView tag;
// this is the same tag consumed by MLRN's own NativeMapViewModule.
const onStyleReady = useCallback((event: unknown) => {
const tag = findNodeHandle((event as { target?: unknown }).target as never);
if (tag != null) void attach(tag);
}, [attach]);
useEffect(() => () => {
if (reactTag != null) void WeatherRuntimeBridge.detach(reactTag).catch(() => undefined);
}, [reactTag]);
const command = useCallback(async (name: string, operation: (tag: number) => Promise<unknown>) => {
if (reactTag == null) return setResult("MAP_NOT_READY: wait for style ready");
try { const value = await operation(reactTag); setResult(`${name}: ${String(value)}`); setSnapshot(await WeatherRuntimeBridge.getState(reactTag)); }
catch (error) { setResult(`${name}: ${String(error)}`); }
}, [reactTag]);
const submitBinaryFixture = useCallback(async () => {
if (reactTag == null) return setResult("MAP_NOT_READY: wait for style ready");
try {
setBinaryTransport("PENDING: clearing fields and validating FILE_REF / FLOAT32_LE / INTERLEAVED_UV");
setInvalidProbe("PENDING");
setT0Submit("PENDING");
setT1Submit("PENDING");
setFieldWindow("PENDING: awaiting Core initial window");
const clear = await WeatherRuntimeBridge.clearWindFields(reactTag);
if (clear !== "ACCEPTED") throw new Error(`clearWindFields: ${clear}`);
const [t0, t1, invalid] = writeCanonicalWindFixture();
const rejected = await WeatherRuntimeBridge.submitWindField(reactTag, invalid.metadataJson, invalid.payloadFileUri);
setInvalidProbe(rejected);
if (rejected !== "REJECTED_ADAPTER") throw new Error(`invalid probe expected REJECTED_ADAPTER, got ${rejected}`);
const t0Result = await WeatherRuntimeBridge.submitWindField(reactTag, t0.metadataJson, t0.payloadFileUri);
setT0Submit(t0Result);
if (t0Result !== "ACCEPTED_PENDING_INITIAL_WINDOW") throw new Error(`T0 expected ACCEPTED_PENDING_INITIAL_WINDOW, got ${t0Result}`);
const t1Result = await WeatherRuntimeBridge.submitWindField(reactTag, t1.metadataJson, t1.payloadFileUri);
setT1Submit(t1Result);
if (t1Result !== "INITIAL_WINDOW_READY") throw new Error(`T1 expected INITIAL_WINDOW_READY, got ${t1Result}`);
const native = await WeatherRuntimeBridge.getState(reactTag);
setSnapshot(native);
if (native.state !== "READY") throw new Error(`Native WeatherRuntime expected READY, got ${native.state}`);
setFieldWindow(`Core evidence: INITIAL_WINDOW_READY + runtime READY; producer identity: T0=${t0.generation}@${t0.validTime}, T1=${t1.generation}@${t1.validTime}; MIX: Core-owned / not exposed`);
setBinaryTransport("READY: FILE_REF / FLOAT32_LE / INTERLEAVED_UV");
setResult(`Binary field submit: invalid=${rejected}, T0=${t0Result}, T1=${t1Result}`);
} catch (error) {
setBinaryTransport(`ERROR: ${String(error)}`);
setResult(`Binary field submit: ${String(error)}`);
setFieldWindow("TIMELINE_NOT_READY");
}
}, [reactTag]);
const buttons = useMemo(() => [
["getState", (tag: number) => WeatherRuntimeBridge.getState(tag)],
["enable", (tag: number) => WeatherRuntimeBridge.setWeatherEnabled(tag, true)],
["weather visible", (tag: number) => WeatherRuntimeBridge.setWeatherColorVisible(tag, true)],
["particle visible", (tag: number) => WeatherRuntimeBridge.setParticleVisible(tag, true)],
["FPS 30", (tag: number) => WeatherRuntimeBridge.setTargetFps(tag, "FPS_30" as TargetFps)],
["PLAY", (tag: number) => WeatherRuntimeBridge.setPlaybackState(tag, "PLAY")],
["PAUSE", (tag: number) => WeatherRuntimeBridge.setPlaybackState(tag, "PAUSE")]
] as const, []);
return <SafeAreaView style={styles.root}>
<View style={styles.map}><Map mapStyle={weatherTestStyle} onDidFinishLoadingStyle={onStyleReady} /></View>
<ScrollView style={styles.controls} contentContainerStyle={styles.controlContent}>
<Text>reactTag: {reactTag ?? "not attached"}</Text>
<Text>runtime: {snapshot.state} / {snapshot.playbackState} / {snapshot.targetFps}</Text>
<Text selectable>Binary Transport: {binaryTransport}</Text>
<Text selectable>Invalid Probe: {invalidProbe}</Text>
<Text selectable>T0 Submit Result: {t0Submit}</Text>
<Text selectable>T1 Submit Result: {t1Submit}</Text>
<Text selectable>Runtime Field Window: {fieldWindow}</Text>
<Text selectable>{result}</Text>
<Button title="Submit binary WIND T0/T1" onPress={() => void submitBinaryFixture()} />
{buttons.map(([label, operation]) => <Button key={label} title={label} onPress={() => void command(label, operation)} />)}
<TextInput value={opacity} onChangeText={setOpacity} keyboardType="decimal-pad" style={styles.input} />
<Button title="weather opacity" onPress={() => void command("weather opacity", (tag) => WeatherRuntimeBridge.setWeatherColorOpacity(tag, Number(opacity)))} />
</ScrollView>
</SafeAreaView>;
}
const styles = StyleSheet.create({
root: { flex: 1 }, map: { flex: 1 }, controls: { maxHeight: 280 }, controlContent: { gap: 8, padding: 12 }, input: { borderWidth: 1, padding: 6 }
});