diff --git a/integration/react-native/MapLibreParticleRNTestHost/App.tsx b/integration/react-native/MapLibreParticleRNTestHost/App.tsx index e696942fb..827e76f11 100644 --- a/integration/react-native/MapLibreParticleRNTestHost/App.tsx +++ b/integration/react-native/MapLibreParticleRNTestHost/App.tsx @@ -1,107 +1,88 @@ 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 { Camera, Map } from "@maplibre/maplibre-react-native"; +import { WeatherRuntimeBridge, type ColorStop, type RuntimeSnapshot, type TargetFps } from "@maplibre-particle/weather-runtime-bridge"; +import { materializeRealWeatherFrames, realWeatherFrames } from "./src/realWeatherDataset"; import { weatherTestStyle } from "./src/weatherStyle"; const initialSnapshot: RuntimeSnapshot = { state: "WAITING_FOR_FIELDS", playbackState: "PAUSE", targetFps: "AUTO", disposed: false }; +const initialVisual = { count: "4096", width: "1.5", length: "0.012", opacity: "0.9", trailOpacity: "0.8", trailFade: "0.94", weatherOpacity: "0.62" }; +const frozenStops: ColorStop[] = [ + { speedMps: 0, red: 0.08, green: 0.2, blue: 0.8, alpha: 0.72 }, { speedMps: 8, red: 0, green: 0.85, blue: 0.9, alpha: 0.72 }, + { speedMps: 16, red: 1, green: 0.85, blue: 0.05, alpha: 0.72 }, { speedMps: 30, red: 0.9, green: 0.08, blue: 0.08, alpha: 0.72 }, +]; export default function App() { const [reactTag, setReactTag] = useState(null); const [snapshot, setSnapshot] = useState(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 [basemap, setBasemap] = useState("LOADING"); + const [frames, setFrames] = useState>({ T0: "PENDING", T1: "PENDING", T2: "PENDING", T3: "PENDING" }); + const [transport, setTransport] = useState("PENDING: FILE_REF / FLOAT32_LE / INTERLEAVED_UV"); + const [weatherVisible, setWeatherVisible] = useState(true); + const [particleVisible, setParticleVisible] = useState(true); + const [visual, setVisual] = useState(initialVisual); + const refresh = useCallback(async (tag: number) => setSnapshot(await WeatherRuntimeBridge.getState(tag)), []); 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. + try { await WeatherRuntimeBridge.attach(tag); await refresh(tag); setReactTag(tag); setResult("WeatherRuntime attached; style ready"); } + catch (error) { setResult(`style/attach error: ${String(error)}`); } + }, [refresh]); 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]); + useEffect(() => () => { if (reactTag != null) void WeatherRuntimeBridge.detach(reactTag).catch(() => undefined); }, [reactTag]); const command = useCallback(async (name: string, operation: (tag: number) => Promise) => { - 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)); } + if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready"); + try { const value = await operation(reactTag); await refresh(reactTag); setResult(`${name}: ${String(value)}`); } catch (error) { setResult(`${name}: ${String(error)}`); } - }, [reactTag]); + }, [reactTag, refresh]); - const submitBinaryFixture = useCallback(async () => { - if (reactTag == null) return setResult("MAP_NOT_READY: wait for style ready"); + const loadRealWeather = useCallback(async () => { + if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready"); + const expected = ["ACCEPTED_PENDING_INITIAL_WINDOW", "INITIAL_WINDOW_READY", "ACCEPTED_NEXT_SNAPSHOT", "ACCEPTED_NEXT_SNAPSHOT"]; 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); + setTransport("LOADING: materializing Expo Asset local file:// URIs"); setFrames({ T0: "PENDING", T1: "PENDING", T2: "PENDING", T3: "PENDING" }); + if (await WeatherRuntimeBridge.clearWindFields(reactTag) !== "ACCEPTED") throw new Error("clearWindFields was rejected"); + const localFrames = await materializeRealWeatherFrames(); + for (let index = 0; index < localFrames.length; index += 1) { + const frame = localFrames[index]; const submit = await WeatherRuntimeBridge.submitWindField(reactTag, frame.metadataJson, frame.payloadFileUri); + setFrames((current) => ({ ...current, [frame.label]: submit })); + if (submit !== expected[index]) throw new Error(`${frame.label} expected ${expected[index]}, got ${submit}`); + } + 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"); - } + setTransport("READY: FILE_REF / FLOAT32_LE / INTERLEAVED_UV"); setResult("T0-T3 accepted in strict producer timestamp order; Native runtime READY"); + } catch (error) { setTransport(`ERROR: ${String(error)}`); setResult(`LOAD REAL WEATHER T0-T3: ${String(error)}`); } }, [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")] + const updateVisual = useCallback((key: keyof typeof initialVisual, value: string) => setVisual((current) => ({ ...current, [key]: value })), []); + const applyVisual = useCallback(() => void command("particle visual", async (tag) => { + const results = await Promise.all([ + WeatherRuntimeBridge.setParticleCount(tag, Number(visual.count)), WeatherRuntimeBridge.setParticleLineWidth(tag, Number(visual.width)), WeatherRuntimeBridge.setParticleLength(tag, Number(visual.length)), WeatherRuntimeBridge.setParticleOpacity(tag, Number(visual.opacity)), WeatherRuntimeBridge.setTrailOpacity(tag, Number(visual.trailOpacity)), WeatherRuntimeBridge.setTrailFade(tag, Number(visual.trailFade)), WeatherRuntimeBridge.setWeatherColorOpacity(tag, Number(visual.weatherOpacity)), + ]); + if (results.some((value) => value !== "ACCEPTED")) throw new Error(results.join(", ")); return "ACCEPTED"; + }), [command, visual]); + + const controls = useMemo(() => [ + ["PLAY", (tag: number) => WeatherRuntimeBridge.setPlaybackState(tag, "PLAY")], ["PAUSE", (tag: number) => WeatherRuntimeBridge.setPlaybackState(tag, "PAUSE")], ["HALF", (tag: number) => WeatherRuntimeBridge.setPlaybackSpeed(tag, "HALF")], ["NORMAL", (tag: number) => WeatherRuntimeBridge.setPlaybackSpeed(tag, "NORMAL")], ["DOUBLE", (tag: number) => WeatherRuntimeBridge.setPlaybackSpeed(tag, "DOUBLE")], + ["FPS AUTO", (tag: number) => WeatherRuntimeBridge.setTargetFps(tag, "AUTO" as TargetFps)], ["FPS 30", (tag: number) => WeatherRuntimeBridge.setTargetFps(tag, "FPS_30" as TargetFps)], ["FPS 60", (tag: number) => WeatherRuntimeBridge.setTargetFps(tag, "FPS_60" as TargetFps)], ["Weather stops", (tag: number) => WeatherRuntimeBridge.setWeatherColorStops(tag, frozenStops)], ["Particle color", (tag: number) => WeatherRuntimeBridge.setParticleColor(tag, 1, 1, 1, 0.95)], ["Trail color", (tag: number) => WeatherRuntimeBridge.setTrailColor(tag, 0.35, 0.85, 1, 0.8)], ] as const, []); return - - - reactTag: {reactTag ?? "not attached"} - runtime: {snapshot.state} / {snapshot.playbackState} / {snapshot.targetFps} - Binary Transport: {binaryTransport} - Invalid Probe: {invalidProbe} - T0 Submit Result: {t0Submit} - T1 Submit Result: {t1Submit} - Runtime Field Window: {fieldWindow} - {result} -