chore: prepare V3-08 android validation
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-11 12:42:49 +08:00
parent dd356a277d
commit 11cf58d344
24 changed files with 383 additions and 398 deletions

View File

@@ -0,0 +1,50 @@
# STEP-072A V3-08 RN Test Host 移除 Legacy 双 Bridge
## 基线
- 仓库:`/mnt/sda1/MapLibreParticleLab`
- 分支:`particle/main`
- 已提交 HEAD:`dd356a277db05ce0075bee43f431ddeb2aba19cc`
- 当前工作区已有 STEP-072 未提交修改,必须保留,不得回退或覆盖。
- 只处理 RN Test Host legacy bridge 重复 autolink 问题。
## 已确认事实
- App 已只从 `@navsea/maplibre-particle-rn` 使用 `FieldRuntime`。
- Expo autolinking 当前同时链接:
- `@maplibre-particle/weather-runtime-bridge`
- `@navsea/maplibre-particle-rn`
- 两者都注册 Native module `WeatherRuntimeBridge`,codegen library 都是 `WeatherRuntimeBridgeSpec`。
- Test Host 根 `react-native.config.js` 主动注册 `./modules/weather-runtime-bridge`。
- `tsconfig.json` 仍 include `modules/weather-runtime-bridge/src`。
- V3-08 必须只保留正式 package `.3` 的 bridge。
## 必做修改
1. 删除整个 tracked legacy 目录:
`integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/`
- 只删除其中 Git tracked 源码/配置;不要清理、提交或处理其 untracked `android/build/`。
2. 删除 Test Host 根:
`integration/react-native/MapLibreParticleRNTestHost/react-native.config.js`
- 该文件目前唯一用途是注册 legacy bridge;删除后不要新造替代配置。
3. 修改:
`integration/react-native/MapLibreParticleRNTestHost/tsconfig.json`
- include 收敛为 `App.tsx` 与 `src`,移除 legacy module src。
4. 更新 Test Host README 当前 V3-08 章节:
- 明确 legacy local `@maplibre-particle/weather-runtime-bridge` 已移除。
- `@navsea/maplibre-particle-rn@13.4.1-particle.3` 是唯一 `WeatherRuntimeBridge` 提供者。
- 历史 STEP-061/062 章节可保留为历史说明,不得改写历史事实。
## 禁止事项
- 不得运行 Android Gradle、AAR/APK build、publish、install 或 ADB。
- 不得运行或加入 Jest。
- 不得修改 `packages/maplibre-particle-rn/`、MapLibre Core/JNI/WeatherRuntime、fixture、NavSea 或 release manifest。
- 不得删除/清理任何 untracked/generated 文件;尤其不得处理 legacy 目录下现有 `android/build/`。
- 不得执行 `git add .`、`git add -A`、commit、push、reset、clean、checkout 或 stash。
- 不得修改 STEP-062 dirty 文档和其它历史 dirty/untracked 文件。
## 完成后必做报告
逐项报告“已完成 / 未完成 / 无法验证”,并说明原因:
1. tracked legacy bridge 源码/配置是否全部删除。
2. 根 `react-native.config.js` 是否删除。
3. `tsconfig.json` 是否只包含 `App.tsx` 与 `src`。
4. README 是否明确正式 package 为唯一 bridge provider。
5. 明确说明未执行 Android/ADB/Jest/Git。
不要自行做 Android 验证;完成文件修改后停止。

View File

@@ -0,0 +1,81 @@
# STEP-072 V3-08 Android Native v2 与单次综合验收准备
## 基线
- 仓库:`/mnt/sda1/MapLibreParticleLab`
- 分支:`particle/main`
- 基线 HEAD:`dd356a277db05ce0075bee43f431ddeb2aba19cc`
- 当前 RN 包:`13.4.1-particle.3`
- 当前旧 Native AAR:`13.4.1-particle.1`
## 已确认事实
- Maven Local 仅存在 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`。
- 该 AAR 的 `WeatherRuntime` 只有 v1 `submitWindField(String, byte[])`,不含 STEP-069 的 v2 multi-plane `submitField(...)`。
- 当前源码已包含 v2 scalar/vector/shared validity、`clearFields`、`setFieldValueSource`、`setParticleAdvectionScale`。
- 因此 V3-08 不得继续消费 Native `.1`,也不得覆盖 `.1`。
## 目标
准备新的 Android Native artifact 坐标 `13.4.1-particle.2`,并让 RN `.3` 与 RN Test Host 全部指向该新坐标;只做源码/配置/文档准备,不执行 Android 构建或发布。
## 必做修改
1. `platform/android/VERSION`
- 改为 `13.4.1-particle.2`。
2. `packages/maplibre-particle-rn/android/build.gradle`
- Native dependency 改为 `.2`。
3. `packages/maplibre-particle-rn/app.plugin.js`
- MLRN dependency substitution 改为 `.2`。
4. `integration/react-native/MapLibreParticleRNTestHost/modules/weather-runtime-bridge/android/build.gradle`
- Native dependency 改为 `.2`。
5. `integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js`
- dependency substitution 改为 `.2`。
6. 不修改或提交生成目录 `integration/react-native/MapLibreParticleRNTestHost/android/`。
## RN `.3` 文档关系
更新 `packages/maplibre-particle-rn/README.md` 与 `CHANGELOG.md`:
- RN/TS 为 `13.4.1-particle.3`。
- 对应目标 Native artifact 为 `13.4.1-particle.2`。
- `.2` Native 是 STEP-069 v2 multi-plane 所需的新 artifact,不能复用/覆盖 `.1`。
- 当前只完成坐标准备;Android AAR 尚未由用户手工构建/发布/验收,因此不得写成已发布或 PASS。
## Release manifest
更新 `release/maplibre-particle/release-manifest.json` 为当前 `.3` 开发/验收准备状态:
- release:`13.4.1-particle.3`
- releaseState:明确为等待 Android 手工构建/发布/验收,不得使用 `released`
- reactNativePackage.version:`13.4.1-particle.3`
- nativeArtifact.version:`13.4.1-particle.2`
- canonicalField.schema:`maplibre-particle-field/v2`
- transport 描述为 metadataJson + validity local ref + optional scalar/vector local refs
- 不伪造 AAR SHA、发布时间、release tag、Android PASS。
- 保留可追溯到 `.2` 已发布 RN 版本的 previousBaseline 信息;不要抹掉历史验证语义。
## Test Host README
更新 `integration/react-native/MapLibreParticleRNTestHost/README.md` 的当前版本/构建说明:
- 当前 V3-08 目标为 RN `.3` + Native `.2`。
- 用户必须先手工 build/publish `.2` Native AAR,再 prebuild/build Test Host。
- 历史 STEP-061/062 可保留为历史章节,但不能让顶部“当前版本”继续写旧 `.1`/step061。
- Metro 当前 binary asset extension 为 `.f32` + `.u8`。
## 新建 Android 单次验收文档
新建:`docs/60_测试/STEP-072_V3-08_Android单次综合验收_v1.md`。
只写用户手工执行流程,不执行命令。必须使用 `--console=plain`,ADB 命令固定显式 `adb -s 192.168.1.146 ...`。
验收文档至少覆盖:
- Native `.2` Release OpenGL AAR build + Maven Local publish;核对 AAR SHA 与 Maven Local artifact 一致。
- `javap` 核对 `.2` 的 `WeatherRuntime.submitField`、`clearFields`、`setFieldValueSource`、`setParticleAdvectionScale` 存在。
- Test Host clean prebuild/build/install/start,一次 Android 周期完成。
- Wind real、Rain scalar-only、Wave scalar+DIRECTION、Tide VELOCITY、Wave synthetic independence 五组数据逐组加载。
- 每组要求 T0=`ACCEPTED_PENDING_INITIAL_WINDOW`、T1=`INITIAL_WINDOW_READY`、最终 Runtime=`READY`。
- PLAY/PAUSE、HALF/NORMAL/DOUBLE、seek T0/T1、AUTO/30/60 FPS、Field/Particle 独立显隐。
- Wave synthetic 检查 scalar 色场与 direction particle 独立;Rain 不显示粒子。
- logcat 观察 v2 submit / initial window ready / 无 fatal bridge/JNI 错误。
## 禁止事项
- 不修改 Core/Runtime/JNI/renderer 业务实现。
- 不执行 Gradle Android build、AAR/APK build、Maven publish、Expo Android build、ADB、安装或真机操作。
- 不执行 Jest。
- 不修改 STEP-062 用户 dirty 文件或历史 untracked 文件。
- 不执行 git add/commit/push。
- 不修改生成的 `integration/react-native/MapLibreParticleRNTestHost/android/`。
- 不创建或覆盖 Native `.1` artifact。
## 完成后报告
只报告修改文件、Native `.2` 坐标同步情况、验收文档路径,以及“Android build/publish/ADB 未执行”。

View File

@@ -0,0 +1,40 @@
# STEP-072 — V3-08 Android Native v2 与单次综合验收准备
状态:IMPLEMENTED / WAITING_ANDROID_VALIDATION
日期:2026-09-11
基线:`dd356a277db05ce0075bee43f431ddeb2aba19cc`
## 目标
在不执行 Android 编译/安装的前提下,完成 V3-08 前的 Native/RN 版本坐标准备、Test Host 单 Bridge 收口与单次实体机验收流程冻结。
## 已完成
- [x] Native Android `VERSION`:`13.4.1-particle.1 → 13.4.1-particle.2`。
- [x] RN package `.3` 的 Android dependency 与 Expo plugin 统一指向 Native `.2`。
- [x] RN Test Host plugin 统一指向 Native `.2`。
- [x] release manifest 升为 RN `.3` + Native `.2`,Android 状态保持 `not_run`,并保留 `.2` 已发布历史追溯。
- [x] 删除 Test Host legacy local `@maplibre-particle/weather-runtime-bridge` tracked 源码/配置。
- [x] 删除 Test Host 根 `react-native.config.js`,`tsconfig.json` 只保留 `App.tsx` 与 `src`。
- [x] Expo autolinking 复核:只剩正式 `@navsea/maplibre-particle-rn` 提供 `WeatherRuntimeBridge`。
- [x] 建立 `docs/60_测试/STEP-072_V3-08_Android单次综合验收_v1.md`。
## 静态验证
- [x] RN Test Host `npm run typecheck`:PASS。
- [x] package TypeScript:PASS。
- [x] RN TurboModule codegen:仅生成 `NativeWeatherRuntimeBridge`,PASS。
- [x] canonical fixture validator:PASS。
- [x] Expo autolinking:3 dependencies;包含 MLRN、正式 particle package、Expo;不再包含 legacy bridge。
- [x] 两个 Expo plugin `node --check`:PASS。
- [x] release manifest JSON:PASS。
- [x] `git diff --check`:PASS。
## 关键发现与修正
准备阶段发现 Test Host 会同时 autolink legacy local bridge 与正式 package bridge;二者 Native module name 都是 `WeatherRuntimeBridge`、codegen library 都是 `WeatherRuntimeBridgeSpec`。STEP-072A 删除了旧本地 bridge 注册与源码,使 V3-08 clean prebuild 前只剩正式 package bridge。
验收文档同时修正了 AAR `javap` 方法、Makefile `MLN_ANDROID_GRADLE` 的命令行覆盖方式、Linux→Win11 Maven Local handoff,以及 Win11 `gradlew.bat`/Metro/固定 ADB 设备流程。
## 未执行
- Android Gradle / AAR build / Maven publish / APK build / ADB / 实体机验收:未执行。
- Jest:未执行。
- 因此本 Step 不声明 Android PASS,也不创建 `.3` release tag。
## 下一步
用户按 `docs/60_测试/STEP-072_V3-08_Android单次综合验收_v1.md` 只执行一次 Android build/install 周期,覆盖 Wind、Rain、Wave、Tide 与 synthetic Wave 五组场数据。

View File

@@ -0,0 +1,108 @@
# STEP-072 V3-08 Android 单次综合验收
## 状态与边界
本文件只列出用户手工执行的单次 Android 验收流程。目标组合是 RN/TS `13.4.1-particle.3` 与 Native `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.2`。`.2` 是 STEP-069 v2 multi-plane 所需的新坐标;不得复用、覆盖或误用旧 `.1`。
执行前记录当前 commit。所有 Gradle 调用均使用 `--console=plain`;所有设备命令均显式指定 `adb -s 192.168.1.146`。本流程完成前不得将 AAR、发布、Android 或 bridge 验收写为 PASS。
## 1. Native `.2` Release AAR 与 Maven Local
在 `/mnt/sda1/MapLibreParticleLab/platform/android` 手工确认 `VERSION` 为 `13.4.1-particle.2`,然后执行 OpenGL Release AAR 构建与 Maven Local 发布:
```bash
make RENDERER=opengl BUILDTYPE=Release JOBS=1 \
'MLN_ANDROID_GRADLE=./gradlew --console=plain --parallel --max-workers=$(JOBS) -Pmapbox.buildtype=$(buildtype) -Pmapbox.stl=$(MLN_ANDROID_STL) -Dorg.gradle.internal.http.socketTimeout=$(SOCKET_TIMEOUT) -Dorg.gradle.internal.http.connectionTimeout=$(CONNECTION_TIMEOUT)' \
apackage
./gradlew :MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal --console=plain
```
上述 `make` 写法已用 `make -n` 验证,最终 Gradle 命令包含 `--console=plain`、`--max-workers=1`、`-Pmapbox.buildtype=release` 和 `-Pmaplibre.abis=all`;不要把 `MLN_ANDROID_GRADLE` 只作为环境变量放在 `make` 前面,因为 Makefile 的普通赋值会覆盖环境变量。
记录构建产物 `MapLibreAndroid/build/outputs/aar/MapLibreAndroid-opengl-release.aar` 的 SHA-256,并与 Maven Local `.2` AAR 的 SHA-256 比对;二者必须完全一致。
AAR 不是可直接传给 `javap` 的 JAR;先提取 `classes.jar`:
```bash
M2_AAR="$HOME/.m2/repository/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.2/android-sdk-particle-opengl-13.4.1-particle.2.aar"
unzip -p "$M2_AAR" classes.jar > /tmp/step072-particle2-classes.jar
javap -classpath /tmp/step072-particle2-classes.jar -public org.maplibre.android.maps.WeatherRuntime \
| grep -E 'submitField|clearFields|setFieldValueSource|setParticleAdvectionScale'
```
若任一方法缺失、AAR 坐标不是 `.2` 或 SHA 不一致,停止后续 Test Host 验收。
## 2. Linux → Win11 Native `.2` handoff
若 Test Host 继续按已验证的 CARBON7i/Win11 流程构建,则 Linux Maven Local 发布完成后,必须把同一 `.2` 的 AAR、POM、module 三个文件传到 Win11,并放入 Win11 当前用户的 Maven Local 对应目录;不能只复制 AAR,也不能让 Win11 fallback 到 `.1` 或官方 SDK。
目标目录结构:
```text
%USERPROFILE%/.m2/repository/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.2/
android-sdk-particle-opengl-13.4.1-particle.2.aar
android-sdk-particle-opengl-13.4.1-particle.2.pom
android-sdk-particle-opengl-13.4.1-particle.2.module
```
Win11 上再次计算 AAR SHA-256,必须与 Linux build output 及 Linux Maven Local AAR 三者一致。可用 `Get-FileHash <AAR路径> -Algorithm SHA256`。完成后再进入 Test Host clean prebuild。若直接在 Linux 同机编译 Test Host,则可使用该机 Maven Local,不需要此 handoff;但不得因此省略 artifact SHA/API 检查。
## 3. Test Host 单次 Android 周期
沿用已验证的 CARBON7i/Win11 流程时,在 **Win11 当前 Test Host checkout 根目录**执行下面的 PowerShell 命令。先 clean prebuild,再用 dependencyInsight 确认 app 最终只解析到 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.2`,然后只编译一次 Debug APK:
```powershell
npm ci
npx expo prebuild --platform android --clean
Set-Location android
.\gradlew.bat :app:dependencyInsight --dependency android-sdk-particle-opengl --configuration debugRuntimeClasspath --console=plain
.\gradlew.bat :app:assembleDebug --console=plain
Set-Location ..
```
`dependencyInsight` 中若出现 `.1`、官方 `org.maplibre.gl:android-sdk-opengl` 最终胜出,或 `.2` 未解析,立即停止,不安装 APK。
在另一个 Win11 PowerShell 窗口启动 Metro,固定监听 8091:
```powershell
npm run start
```
Metro ready 后执行固定设备安装/启动命令;每条 ADB 都必须显式带设备地址:
```powershell
adb -s 192.168.1.146 reverse tcp:8091 tcp:8091
adb -s 192.168.1.146 reverse tcp:8081 tcp:8091
adb -s 192.168.1.146 install -r android/app/build/outputs/apk/debug/app-debug.apk
adb -s 192.168.1.146 shell am force-stop org.maplibre.particle.rntesthost
adb -s 192.168.1.146 shell am start -n org.maplibre.particle.rntesthost/.MainActivity
```
Metro binary asset extension 必须仍为 `.f32` 与 `.u8`。本轮 Test Host 只做这一轮 clean prebuild + Debug APK 编译/安装,不为单个数据组重复编译 APK。
## 4. 五组 field 数据逐组验收
对每一组分别先 clear,再按 T0、T1 顺序加载,且每次均记录页面结果和 logcat:
| 数据组 | plane/粒子期望 |
| --- | --- |
| Wind real | vector;Wind/GLOBAL 粒子显示 |
| Rain scalar-only | scalar;不得显示粒子 |
| Wave scalar+DIRECTION | scalar + direction vector;色场与粒子均可用 |
| Tide VELOCITY | vector velocity;粒子可用 |
| Wave synthetic independence | scalar + direction vector;专用于独立性检查 |
每一组都必须依序满足 T0=`ACCEPTED_PENDING_INITIAL_WINDOW`、T1=`INITIAL_WINDOW_READY`,并使最终 Runtime=`READY`。任一状态不同、出现拒绝或 Runtime 非 READY,记录失败组并停止将该组标记为通过。
对每个 Runtime READY 的数据组验证:PLAY/PAUSE、HALF/NORMAL/DOUBLE、seek T0/T1、AUTO/30/60 FPS,以及 Field/Particle 的独立显隐。额外检查 Wave synthetic independence:scalar 色场和 direction particle 的表现及开关互不绑定;Rain scalar-only 不得出现粒子。
## 5. Logcat 与结论
使用固定设备筛查 logcat,观察 v2 submit、initial window ready,以及 fatal bridge/JNI 错误:
```powershell
adb -s 192.168.1.146 logcat -c
adb -s 192.168.1.146 logcat -v time | Select-String -Pattern 'submitField|INITIAL_WINDOW_READY|initial window ready|WeatherRuntime|FATAL EXCEPTION|JNI|bridge'
```
记录每组的 T0/T1/READY 证据、控制项结果、AAR SHA 比对、`javap` 输出摘要及 logcat。仅当五组均符合要求且无 fatal bridge/JNI 错误时,才可由用户决定更新 Android 验收状态;本文件本身不代表已执行或通过。

View File

@@ -0,0 +1,40 @@
# STEP-072 — V3-08 Android Native v2 与单次综合验收准备完成报告
结论:IMPLEMENTED / WAITING_ANDROID_VALIDATION
日期:2026-09-11
## 实际完成
V3-08 前置准备已完成:Native Android 版本目标升为 `13.4.1-particle.2`,RN package `13.4.1-particle.3`、Expo plugin、RN Test Host 与 release manifest 均统一指向该新 Native 坐标;旧 `.1` 不再作为当前 active dependency。
Test Host 同时存在两套 `WeatherRuntimeBridge` 的问题已消除。Expo autolinking 复核后仅保留正式 `@navsea/maplibre-particle-rn` bridge,legacy local bridge 不再参与 autolink/codegen。
## 静态验证结果
- Expo autolinking:PASS;legacy bridge 不在 dependency graph。
- RN Test Host TypeScript:PASS。
- package TypeScript:PASS。
- RN TurboModule codegen:PASS;仅 `NativeWeatherRuntimeBridge`。
- V3 fixture validator:PASS。
- Expo plugin JavaScript syntax:PASS。
- release manifest JSON:PASS。
- `git diff --check`:PASS。
## 验收流程冻结
新增 `docs/60_测试/STEP-072_V3-08_Android单次综合验收_v1.md`。流程按既有成功路径明确分为:Linux 构建/发布 Native `.2` → SHA/API 检查 → Linux→Win11 Maven Local handoff → Win11 clean prebuild/dependencyInsight/单次 APK build → Metro 8091 → 固定设备 `192.168.1.146` 安装/启动 → 五组 field 数据综合验收。
文档已修正两个容易导致误操作的问题:AAR 需先提取 `classes.jar` 再 `javap`;Makefile 的 `MLN_ANDROID_GRADLE` 必须作为 make 命令行变量传入,不能仅作为环境变量。
## 未执行与边界
- 未执行 Android Gradle、AAR build、Maven publish、APK build、ADB 或实体机测试。
- 未运行或加入 Jest。
- 未修改 MapLibre Core/JNI/WeatherRuntime/renderer、WeatherV2、NavSea 或 V3 field runtime 语义。
- STEP-062 两份用户 dirty 文档、STEP-062 Android 最终报告及历史 untracked/build/cache/tgz 均未纳入。
- legacy bridge 的 untracked `android/build/` 按任务要求原样保留,未清理。
## Git handoff
- 实现/准备提交:待本报告后的 scoped commit 回填。
- 提交信息:待回填。
- Push:待回填。
- Remote branch:`origin/particle/main`。
- Final HEAD:待回填。
## 下一步
仅由用户执行 V3-08 单次 Android 综合验收。验收通过前 release manifest 保持 `awaiting_android_manual_build_publish_acceptance` / Android `not_run`,不得把 `.3` 标记 released 或创建 release tag。

View File

@@ -2,14 +2,18 @@
这是 MapLibreParticleLab 内的独立 React Native 开发宿主,不是 NavSea 业务应用。它只有一个 `MLRNMapView` 与 WeatherRuntime 控制页,用于证明低频控制面:React 的 `reactTag` 经薄 Android 模块定位该 View,再定位该 View 的 `MapLibreMap` 与 map-owned `WeatherRuntime`。 这是 MapLibreParticleLab 内的独立 React Native 开发宿主,不是 NavSea 业务应用。它只有一个 `MLRNMapView` 与 WeatherRuntime 控制页,用于证明低频控制面:React 的 `reactTag` 经薄 Android 模块定位该 View,再定位该 View 的 `MapLibreMap` 与 map-owned `WeatherRuntime`。
## 版本与首次 prebuild 边界 ## 当前 V3-08 版本与首次 prebuild 边界
- 当前验收目标是 RN/TS `13.4.1-particle.3` 配合 Native `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.2`。`.2` 是 STEP-069 v2 multi-plane 新 artifact;不得复用或覆盖 `.1`。
- 本地 legacy `@maplibre-particle/weather-runtime-bridge` 已移除;`@navsea/maplibre-particle-rn@13.4.1-particle.3` 是唯一的 `WeatherRuntimeBridge` 提供者。
- 必须先由用户手工构建并 Maven Local publish Native `.2`,再对 Test Host 执行 clean prebuild、build、install 与启动。当前仅完成坐标准备,AAR 尚未构建、发布或验收。
- Metro 当前 binary asset extension 为 `.f32` 与 `.u8`。
- package manager 为 npm(`packageManager: npm@10.9.4`),锁文件为 `package-lock.json`;当前环境的 `npm ci --dry-run --ignore-scripts` 无须 `--legacy-peer-deps` 即可解析。 - package manager 为 npm(`packageManager: npm@10.9.4`),锁文件为 `package-lock.json`;当前环境的 `npm ci --dry-run --ignore-scripts` 无须 `--legacy-peer-deps` 即可解析。
- package 明确固定 React Native `0.85.2`、React `19.2.3`、Expo `56.0.4`、`expo-file-system` `56.0.11`、TypeScript `5.9.3`、MLRN `11.3.6`;`app.json` 明确请求 New Architecture。Hermes 的最终启用状态必须在首次 Android prebuild 后检查生成工程。 - package 明确固定 React Native `0.85.2`、React `19.2.3`、Expo `56.0.4`、`expo-file-system` `56.0.11`、TypeScript `5.9.3`、MLRN `11.3.6`;`app.json` 明确请求 New Architecture。Hermes 的最终启用状态必须在首次 Android prebuild 后检查生成工程。
- AGP `8.5.0`、Gradle `8.14.3`、Kotlin `2.1.20`、NDK `27.1.12297006`、JDK 17、minSdk 24、compile/targetSdk 36 是本 Test Host 的**人工验收配置目标**,不是尚未生成 Android 工程的已验证结果。首次 prebuild 后必须以实际 Gradle wrapper、AGP/Kotlin plugin、SDK/NDK 与 Hermes/New Architecture 配置为准核对。 - AGP `8.5.0`、Gradle `8.14.3`、Kotlin `2.1.20`、NDK `27.1.12297006`、JDK 17、minSdk 24、compile/targetSdk 36 是本 Test Host 的**人工验收配置目标**,不是尚未生成 Android 工程的已验证结果。首次 prebuild 后必须以实际 Gradle wrapper、AGP/Kotlin plugin、SDK/NDK 与 Hermes/New Architecture 配置为准核对。
- MLRN 的 root Gradle extras 固定 `org.maplibre.reactnative.nativeVersion=13.4.1-step061` 与 `nativeVariant=opengl`。STEP-061 必须构建并 Maven Local publish 这个唯一的新坐标,禁止覆盖已经冻结的 `13.4.1-step060f`;本轮 RDC 修复仅推进坐标和文档,未构建或发布。
## Custom SDK 来源(手工执行) ## 历史:STEP-061/062 Custom SDK 来源(手工执行)
Gate C 封存环境负责生成 custom AAR;Test Host 只负责 Maven Local handoff。Expo plugin 对唯一 custom 坐标 `org.maplibre.gl:android-sdk-opengl` 排除 Maven Central,而将 Maven Local 排在首位。因此未先发布本仓库 custom SDK 时 dependency resolution 必须失败,不能悄悄使用官方 SDK;MLRN 的独立 turf/annotation/scalebar 依赖不被这个 SDK-only 排除误伤。 Gate C 封存环境负责生成 custom AAR;Test Host 只负责 Maven Local handoff。Expo plugin 对唯一 custom 坐标 `org.maplibre.gl:android-sdk-opengl` 排除 Maven Central,而将 Maven Local 排在首位。因此未先发布本仓库 custom SDK 时 dependency resolution 必须失败,不能悄悄使用官方 SDK;MLRN 的独立 turf/annotation/scalebar 依赖不被这个 SDK-only 排除误伤。
@@ -51,7 +55,7 @@ Style JSON 明确创建 background、host-field `RasterArraySource`、`WeatherCo
Test Host 固定消费已验收的 `org.maplibre.gl:android-sdk-opengl:13.4.1-step061`,本阶段不构建或发布 AAR。页面 style 顺序是 `background → GSI raster → WeatherColorLayer → RasterParticleLayer`;GSI 使用国土地理院标准地图 `https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png`(256、maxzoom 18、含 attribution),相机初始聚焦日本及周边 WeatherV2 bounds。 Test Host 固定消费已验收的 `org.maplibre.gl:android-sdk-opengl:13.4.1-step061`,本阶段不构建或发布 AAR。页面 style 顺序是 `background → GSI raster → WeatherColorLayer → RasterParticleLayer`;GSI 使用国土地理院标准地图 `https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png`(256、maxzoom 18、含 attribution),相机初始聚焦日本及周边 WeatherV2 bounds。
受控 assets `assets/weather/ecmwf-hres-wind-20260807/` 原样复制 Android Validation 的 `dataset.json` 与四个 `metadata.json`/`field.f32`。`expo-asset` 是明确直接依赖,`metro.config.js` 仅增加 `.f32` asset extension。点击 `LOAD REAL WEATHER T0-T3` 会先 clear,然后通过 `Asset.fromModule(...).downloadAsync()` 取得四个 Expo cache `file://` URI,按 producer validTime 提交;JS 只传小型 metadata JSON 和文件引用,不 materialize 3.01 MiB payload 为数组、JSON、base64 或 hex。 受控 assets `assets/weather/ecmwf-hres-wind-20260807/` 原样复制 Android Validation 的 `dataset.json` 与四个 `metadata.json`/`field.f32`。`expo-asset` 是明确直接依赖,`metro.config.js` 当前增加 `.f32` 与 `.u8` binary asset extension。点击 `LOAD REAL WEATHER T0-T3` 会先 clear,然后通过 `Asset.fromModule(...).downloadAsync()` 取得四个 Expo cache `file://` URI,按 producer validTime 提交;JS 只传小型 metadata JSON 和文件引用,不 materialize 3.01 MiB payload 为数组、JSON、base64 或 hex。
硬断言依次为 T0=`ACCEPTED_PENDING_INITIAL_WINDOW`、T1=`INITIAL_WINDOW_READY`、T2/T3=`ACCEPTED_NEXT_SNAPSHOT`,并在最后要求 Native state=`READY`;任何偏差均显示 `ERROR`。界面提供 PLAY/PAUSE、HALF/NORMAL/DOUBLE、四个 epoch-ms forecast seek、AUTO/30/60 FPS、两层可见性、冻结 WeatherColor stops、独立 particle/trail 色彩,以及粒子/轨迹视觉参数。Expo/系统管理 asset cache;本页不自建 `.f32` 临时文件。 硬断言依次为 T0=`ACCEPTED_PENDING_INITIAL_WINDOW`、T1=`INITIAL_WINDOW_READY`、T2/T3=`ACCEPTED_NEXT_SNAPSHOT`,并在最后要求 Native state=`READY`;任何偏差均显示 `ERROR`。界面提供 PLAY/PAUSE、HALF/NORMAL/DOUBLE、四个 epoch-ms forecast seek、AUTO/30/60 FPS、两层可见性、冻结 WeatherColor stops、独立 particle/trail 色彩,以及粒子/轨迹视觉参数。Expo/系统管理 asset cache;本页不自建 `.f32` 临时文件。

View File

@@ -1,25 +0,0 @@
plugins {
id "com.android.library"
id "org.jetbrains.kotlin.android"
}
apply plugin: "com.facebook.react"
android {
namespace "org.maplibre.particle.rntesthost.weather"
compileSdk 36
defaultConfig { minSdk 24 }
}
dependencies {
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.20"
implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1"
compileOnly project(":maplibre_maplibre-react-native")
}
react {
jsRootDir = file("../src/")
libraryName = "WeatherRuntimeBridge"
codegenJavaPackageName = "org.maplibre.particle.rntesthost.weather"
}

View File

@@ -1 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />

View File

@@ -1,225 +0,0 @@
package org.maplibre.particle.rntesthost.weather
import android.net.Uri
import android.view.View
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.turbomodule.core.interfaces.TurboModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.uimanager.IllegalViewOperationException
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.common.UIManagerType
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.Style
import org.maplibre.android.maps.WeatherRuntime
import org.maplibre.reactnative.components.mapview.MLRNMapView
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
/**
* A low-frequency, per-view bridge. It resolves the Fabric view for each call and never stores a
* reactTag, current map, timeline, playback state, or a second Map registry.
*/
class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) : NativeWeatherRuntimeBridgeSpec(context), TurboModule {
@ReactMethod
override fun attach(reactTag: Double, promise: Promise) = withMapView(reactTag, promise) { view, map ->
val style = map.style ?: return@withMapView reject(promise, "STYLE_NOT_READY", "The MapLibre style is not loaded")
val sourceExists = map.hasStyleSource(WeatherIds.SOURCE)
val weatherColorLayerExists = map.hasStyleLayer(WeatherIds.COLOR_LAYER)
val particleLayerExists = map.hasStyleLayer(WeatherIds.PARTICLE_LAYER)
if (!sourceExists || !weatherColorLayerExists || !particleLayerExists) {
return@withMapView reject(
promise,
"WEATHER_OBJECTS_NOT_READY",
weatherObjectsNotReadyDiagnostic(
reactTag.toInt(),
style,
sourceExists,
weatherColorLayerExists,
particleLayerExists,
),
)
}
val runtime = map.replaceWeatherRuntime(WeatherRuntime.Config(WeatherIds.SOURCE, WeatherIds.COLOR_LAYER, WeatherIds.PARTICLE_LAYER))
// MLRN drops its MapView by detaching it. This listener captures only that concrete map,
// disposes its map-owned facade, and cannot target a replacement view.
view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) = Unit
override fun onViewDetachedFromWindow(v: View) { map.disposeWeatherRuntime() }
})
promise.resolve(snapshot(runtime))
}
@ReactMethod
override fun detach(reactTag: Double, promise: Promise) = withMap(reactTag, promise) { map ->
if (map.weatherRuntime == null) reject(promise, "RUNTIME_DISPOSED", "No attached WeatherRuntime exists for this map")
else { map.disposeWeatherRuntime(); promise.resolve(null) }
}
@ReactMethod override fun getState(reactTag: Double, promise: Promise) = withRuntime(reactTag, promise) { runtime -> promise.resolve(snapshot(runtime)) }
@ReactMethod override fun clearWindFields(reactTag: Double, promise: Promise) = command(reactTag, promise) { it.clearWindFields() }
/** Binary ingress is a local file reference, never a JS number array or encoded string. */
@ReactMethod
override fun submitWindField(reactTag: Double, metadataJson: String, payloadFileUri: String, promise: Promise) {
if (metadataJson.isEmpty() || metadataJson.toByteArray(Charsets.UTF_8).size > MAX_METADATA_UTF8_BYTES) return reject(promise, "INVALID_ARGUMENT", "metadataJson UTF-8 payload must be non-empty and at most $MAX_METADATA_UTF8_BYTES bytes")
val payload = try { readLocalPayload(payloadFileUri) }
catch (error: IllegalArgumentException) { return reject(promise, "PAYLOAD_FILE_UNREADABLE", error.message ?: "payload file is unreadable") }
catch (error: IOException) { return reject(promise, "PAYLOAD_FILE_UNREADABLE", "payload file could not be read: ${error.message ?: "I/O error"}") }
catch (error: RuntimeException) { return reject(promise, "PAYLOAD_FILE_UNREADABLE", "payload file could not be read") }
if (payload.isEmpty()) return reject(promise, "PAYLOAD_FILE_UNREADABLE", "payload file is empty")
withRuntime(reactTag, promise) { runtime ->
try { promise.resolve(runtime.submitWindField(metadataJson, payload).name) }
catch (error: IllegalStateException) { reject(promise, "NATIVE_REJECTED", error.message ?: "WeatherRuntime rejected the field") }
}
}
@ReactMethod override fun setWeatherEnabled(reactTag: Double, enabled: Boolean, promise: Promise) = command(reactTag, promise) { it.setWeatherEnabled(enabled) }
@ReactMethod override fun setWeatherColorVisible(reactTag: Double, visible: Boolean, promise: Promise) = command(reactTag, promise) { it.setWeatherColorVisible(visible) }
@ReactMethod override fun setParticleVisible(reactTag: Double, visible: Boolean, promise: Promise) = command(reactTag, promise) { it.setParticleVisible(visible) }
@ReactMethod override fun setTargetFps(reactTag: Double, target: String, promise: Promise) = enumCommand(reactTag, promise, target, WeatherRuntime.TargetFps.values()) { runtime, value -> runtime.setTargetFps(value) }
@ReactMethod override fun setPlaybackState(reactTag: Double, playback: String, promise: Promise) = enumCommand(reactTag, promise, playback, WeatherRuntime.PlaybackState.values()) { runtime, value -> runtime.setPlaybackState(value) }
@ReactMethod override fun setPlaybackSpeed(reactTag: Double, speed: String, promise: Promise) = enumCommand(reactTag, promise, speed, WeatherRuntime.PlaybackSpeed.values()) { runtime, value -> runtime.setPlaybackSpeed(value) }
@ReactMethod
override fun setForecastTime(reactTag: Double, timestampMs: Double, promise: Promise) {
if (!timestampMs.isFinite() || timestampMs < 0 || timestampMs > Long.MAX_VALUE.toDouble() || timestampMs != kotlin.math.floor(timestampMs)) {
return reject(promise, "INVALID_ARGUMENT", "forecast timestamp must be a non-negative epoch millisecond integer")
}
command(reactTag, promise) { it.setForecastTime(timestampMs.toLong()) }
}
@ReactMethod override fun setParticleCount(reactTag: Double, count: Double, promise: Promise) {
if (!count.isFinite() || count < 0 || count > Int.MAX_VALUE || count != kotlin.math.floor(count)) return reject(promise, "INVALID_ARGUMENT", "particle count must be a non-negative integer")
command(reactTag, promise) { it.setParticleCount(count.toInt()) }
}
@ReactMethod override fun setParticleLineWidth(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setParticleLineWidth(valid) }
@ReactMethod override fun setParticleLength(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setParticleLength(valid) }
@ReactMethod override fun setParticleOpacity(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setParticleOpacity(valid) }
@ReactMethod override fun setTrailOpacity(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setTrailOpacity(valid) }
@ReactMethod override fun setTrailFade(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setTrailFade(valid) }
@ReactMethod override fun setWeatherColorOpacity(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setWeatherColorOpacity(valid) }
@ReactMethod override fun setParticleColor(reactTag: Double, red: Double, green: Double, blue: Double, alpha: Double, promise: Promise) = colorCommand(reactTag, red, green, blue, alpha, promise) { runtime, r, g, b, a -> runtime.setParticleColor(r, g, b, a) }
@ReactMethod override fun setTrailColor(reactTag: Double, red: Double, green: Double, blue: Double, alpha: Double, promise: Promise) = colorCommand(reactTag, red, green, blue, alpha, promise) { runtime, r, g, b, a -> runtime.setTrailColor(r, g, b, a) }
@ReactMethod
override fun setWeatherColorStops(reactTag: Double, stops: ReadableArray, promise: Promise) {
val parsed = ArrayList<WeatherRuntime.ColorStop>()
for (index in 0 until stops.size()) {
val stop: ReadableMap = try { stops.getMap(index) ?: throw IllegalArgumentException() } catch (_: RuntimeException) { return reject(promise, "INVALID_ARGUMENT", "color stop $index must be an object") }
try {
val speed = stop.getDouble("speedMps"); val red = stop.getDouble("red"); val green = stop.getDouble("green"); val blue = stop.getDouble("blue"); val alpha = stop.getDouble("alpha")
if (!speed.isFinite() || !red.isFinite() || !green.isFinite() || !blue.isFinite() || !alpha.isFinite() || red !in 0.0..1.0 || green !in 0.0..1.0 || blue !in 0.0..1.0 || alpha !in 0.0..1.0 || (parsed.isNotEmpty() && speed <= parsed.last().speedMps)) {
return reject(promise, "INVALID_ARGUMENT", "stops require finite, strictly increasing speedMps and finite RGBA in [0,1]")
}
parsed.add(WeatherRuntime.ColorStop(speed.toFloat(), red.toFloat(), green.toFloat(), blue.toFloat(), alpha.toFloat()))
} catch (_: RuntimeException) { return reject(promise, "INVALID_ARGUMENT", "color stop $index is incomplete") }
}
command(reactTag, promise) { it.setWeatherColorStops(parsed) }
}
private fun command(reactTag: Double, promise: Promise, fn: (WeatherRuntime) -> WeatherRuntime.CommandResult) = withRuntime(reactTag, promise) { runtime ->
try {
promise.resolve(fn(runtime).name)
} catch (error: IllegalStateException) {
// WeatherRuntime rejects disabled/disposed commands by contract. Do not let an
// exception escape the UI queue or turn a stale command into an implicit success.
reject(promise, "NATIVE_REJECTED", error.message ?: "WeatherRuntime rejected the command")
}
}
private fun floatCommand(reactTag: Double, value: Double, promise: Promise, fn: (WeatherRuntime, Float) -> WeatherRuntime.CommandResult) {
if (!value.isFinite()) return reject(promise, "INVALID_ARGUMENT", "value must be finite")
command(reactTag, promise) { fn(it, value.toFloat()) }
}
private fun colorCommand(reactTag: Double, red: Double, green: Double, blue: Double, alpha: Double, promise: Promise, fn: (WeatherRuntime, Float, Float, Float, Float) -> WeatherRuntime.CommandResult) {
if (listOf(red, green, blue, alpha).any { !it.isFinite() || it !in 0.0..1.0 }) return reject(promise, "INVALID_ARGUMENT", "RGBA must be finite values in [0,1]")
command(reactTag, promise) { fn(it, red.toFloat(), green.toFloat(), blue.toFloat(), alpha.toFloat()) }
}
private fun <T : Enum<T>> enumCommand(reactTag: Double, promise: Promise, raw: String, values: Array<T>, fn: (WeatherRuntime, T) -> WeatherRuntime.CommandResult) {
val value = values.firstOrNull { it.name == raw } ?: return reject(promise, "INVALID_ARGUMENT", "unsupported enum value: $raw")
command(reactTag, promise) { fn(it, value) }
}
private fun readLocalPayload(reference: String): ByteArray {
if (reference.isBlank()) throw IllegalArgumentException("payload file reference is empty")
val uri = Uri.parse(reference)
if (uri.scheme != null && uri.scheme != "file") throw IllegalArgumentException("payload file reference must use file:// or an absolute local path")
val file = if (uri.scheme == "file") File(uri.path ?: throw IllegalArgumentException("payload file URI has no path")) else File(reference).also {
if (!it.isAbsolute) throw IllegalArgumentException("payload path without a URI scheme must be absolute")
}
if (!file.isFile || !file.canRead()) throw IllegalArgumentException("payload file is not a readable local file")
if (file.length() <= 0L || file.length() > MAX_PAYLOAD_BYTES) throw IllegalArgumentException("payload file size must be in 1..$MAX_PAYLOAD_BYTES bytes")
return file.inputStream().use(::readBounded)
}
private fun readBounded(input: InputStream): ByteArray {
val output = ByteArrayOutputStream(); val buffer = ByteArray(DEFAULT_READ_BUFFER_BYTES)
while (true) {
val count = input.read(buffer)
if (count < 0) break
if (output.size().toLong() + count > MAX_PAYLOAD_BYTES) throw IllegalArgumentException("payload file exceeds $MAX_PAYLOAD_BYTES bytes")
output.write(buffer, 0, count)
}
return output.toByteArray()
}
private fun withRuntime(reactTag: Double, promise: Promise, fn: (WeatherRuntime) -> Unit) = withMap(reactTag, promise) { map ->
val runtime = map.weatherRuntime
if (runtime == null || runtime.isDisposed) reject(promise, "RUNTIME_DISPOSED", "WeatherRuntime is not attached to this map") else fn(runtime)
}
private fun withMap(reactTag: Double, promise: Promise, fn: (MapLibreMap) -> Unit) = withMapView(reactTag, promise) { _, map -> fn(map) }
private fun withMapView(reactTag: Double, promise: Promise, fn: (MLRNMapView, MapLibreMap) -> Unit) {
if (!reactTag.isFinite() || reactTag <= 0 || reactTag != kotlin.math.floor(reactTag)) return reject(promise, "INVALID_ARGUMENT", "reactTag must be a positive integer")
context.runOnUiQueueThread {
val view = try { UIManagerHelper.getUIManager(context, UIManagerType.FABRIC)?.resolveView(reactTag.toInt()) } catch (_: IllegalViewOperationException) { null }
if (view !is MLRNMapView) return@runOnUiQueueThread reject(promise, "MAP_VIEW_NOT_FOUND", "reactTag does not resolve to a current MLRNMapView")
val map = view.mapLibreMap ?: return@runOnUiQueueThread reject(promise, "MAP_NOT_READY", "MLRNMapView has no MapLibreMap yet")
fn(view, map)
}
}
private fun snapshot(runtime: WeatherRuntime) = Arguments.createMap().apply {
putString("state", runtime.state.name); putString("playbackState", runtime.playbackState.name); putString("targetFps", runtime.targetFps.name); putBoolean("disposed", runtime.isDisposed)
}
/**
* Core-only layer types do not have a Java Layer peer. Presence is checked directly in Core
* before this diagnostic is built; the style URI distinguishes a JSON style (empty URI) from
* a URI-backed style without materialising all sources or layers.
*/
private fun weatherObjectsNotReadyDiagnostic(
reactTag: Int,
style: Style,
sourceExists: Boolean,
weatherColorLayerExists: Boolean,
particleLayerExists: Boolean,
): String {
return buildString {
append("WEATHER_OBJECTS_NOT_READY reactTag=").append(reactTag)
append(" mapView=present mapLibreMap=present style=present")
append(" styleUri=").append(style.getUri().ifEmpty { "<empty>" })
append(" source[").append(WeatherIds.SOURCE).append("]=")
.append(presence(sourceExists))
append(" layer[").append(WeatherIds.COLOR_LAYER).append("]=")
.append(presence(weatherColorLayerExists))
append(" layer[").append(WeatherIds.PARTICLE_LAYER).append("]=")
.append(presence(particleLayerExists))
}
}
private fun presence(exists: Boolean) = if (exists) "present" else "missing"
private fun reject(promise: Promise, code: String, message: String) = promise.reject(code, message)
private object WeatherIds {
const val SOURCE = "rn-weather-fields"
const val COLOR_LAYER = "rn-weather-color"
const val PARTICLE_LAYER = "rn-weather-particles"
}
companion object {
const val NAME = "WeatherRuntimeBridge"
private const val MAX_METADATA_UTF8_BYTES = 1_048_576
private const val MAX_PAYLOAD_BYTES = 64L * 1024L * 1024L
private const val DEFAULT_READ_BUFFER_BYTES = 16 * 1024
}
}

View File

@@ -1,20 +0,0 @@
package org.maplibre.particle.rntesthost.weather
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.TurboReactPackage
class WeatherRuntimeBridgePackage : TurboReactPackage() {
override fun getModule(name: String, context: ReactApplicationContext): NativeModule? =
if (name == WeatherRuntimeBridgeModule.NAME) WeatherRuntimeBridgeModule(context) else null
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
mapOf(WeatherRuntimeBridgeModule.NAME to ReactModuleInfo(
WeatherRuntimeBridgeModule.NAME,
WeatherRuntimeBridgeModule::class.java.name,
false, false, false, false, true
))
}
}

View File

@@ -1,14 +0,0 @@
{
"name": "@maplibre-particle/weather-runtime-bridge",
"version": "0.1.0",
"private": true,
"main": "src/index.ts",
"react-native": "src/index.ts",
"peerDependencies": { "react-native": "0.85.2" },
"codegenConfig": {
"name": "WeatherRuntimeBridgeSpec",
"type": "modules",
"jsSrcsDir": "src",
"android": { "javaPackageName": "org.maplibre.particle.rntesthost.weather" }
}
}

View File

@@ -1 +0,0 @@
module.exports = { dependency: { platforms: { android: { sourceDir: "./android" } } } };

View File

@@ -1,37 +0,0 @@
import type { TurboModule } from "react-native";
import { TurboModuleRegistry } from "react-native";
type RuntimeState = "DISABLED" | "WAITING_FOR_FIELDS" | "READY" | "ERROR";
type PlaybackState = "PLAY" | "PAUSE";
type TargetFps = "AUTO" | "FPS_30" | "FPS_60";
type CommandResult = "ACCEPTED" | "REJECTED";
type FieldSubmitResult = "REJECTED_SOURCE_MODE" | "REJECTED_ADAPTER" | "REJECTED_INCOMPATIBLE" | "REJECTED_NON_MONOTONIC" | "REJECTED_CAPACITY" | "ACCEPTED_PENDING_INITIAL_WINDOW" | "INITIAL_WINDOW_READY" | "ACCEPTED_NEXT_SNAPSHOT" | "DUPLICATE" | "UNKNOWN";
type ColorStop = { speedMps: number; red: number; green: number; blue: number; alpha: number };
type RuntimeSnapshot = { state: RuntimeState; playbackState: PlaybackState; targetFps: TargetFps; disposed: boolean };
export interface Spec extends TurboModule {
attach(reactTag: number): Promise<RuntimeSnapshot>;
detach(reactTag: number): Promise<void>;
getState(reactTag: number): Promise<RuntimeSnapshot>;
clearWindFields(reactTag: number): Promise<CommandResult>;
submitWindField(reactTag: number, metadataJson: string, payloadFileUri: string): Promise<FieldSubmitResult>;
setWeatherEnabled(reactTag: number, enabled: boolean): Promise<CommandResult>;
setWeatherColorVisible(reactTag: number, visible: boolean): Promise<CommandResult>;
setParticleVisible(reactTag: number, visible: boolean): Promise<CommandResult>;
setTargetFps(reactTag: number, target: TargetFps): Promise<CommandResult>;
setForecastTime(reactTag: number, timestampMs: number): Promise<CommandResult>;
setPlaybackState(reactTag: number, playback: PlaybackState): Promise<CommandResult>;
setPlaybackSpeed(reactTag: number, speed: "HALF" | "NORMAL" | "DOUBLE"): Promise<CommandResult>;
setWeatherColorStops(reactTag: number, stops: ColorStop[]): Promise<CommandResult>;
setParticleColor(reactTag: number, red: number, green: number, blue: number, alpha: number): Promise<CommandResult>;
setTrailColor(reactTag: number, red: number, green: number, blue: number, alpha: number): Promise<CommandResult>;
setParticleCount(reactTag: number, count: number): Promise<CommandResult>;
setParticleLineWidth(reactTag: number, value: number): Promise<CommandResult>;
setParticleLength(reactTag: number, value: number): Promise<CommandResult>;
setParticleOpacity(reactTag: number, value: number): Promise<CommandResult>;
setTrailOpacity(reactTag: number, value: number): Promise<CommandResult>;
setTrailFade(reactTag: number, value: number): Promise<CommandResult>;
setWeatherColorOpacity(reactTag: number, value: number): Promise<CommandResult>;
}
export default TurboModuleRegistry.getEnforcing<Spec>("WeatherRuntimeBridge");

View File

@@ -1,10 +0,0 @@
export type RuntimeState = "DISABLED" | "WAITING_FOR_FIELDS" | "READY" | "ERROR";
export type PlaybackState = "PLAY" | "PAUSE";
export type TargetFps = "AUTO" | "FPS_30" | "FPS_60";
export type CommandResult = "ACCEPTED" | "REJECTED";
export type FieldSubmitResult = "REJECTED_SOURCE_MODE" | "REJECTED_ADAPTER" | "REJECTED_INCOMPATIBLE" | "REJECTED_NON_MONOTONIC" | "REJECTED_CAPACITY" | "ACCEPTED_PENDING_INITIAL_WINDOW" | "INITIAL_WINDOW_READY" | "ACCEPTED_NEXT_SNAPSHOT" | "DUPLICATE" | "UNKNOWN";
export type ColorStop = Readonly<{ speedMps: number; red: number; green: number; blue: number; alpha: number }>;
export type RuntimeSnapshot = Readonly<{ state: RuntimeState; playbackState: PlaybackState; targetFps: TargetFps; disposed: boolean }>;
export { default as WeatherRuntimeBridge } from "./NativeWeatherRuntimeBridge";

View File

@@ -6,7 +6,7 @@ const marker = "// MapLibreParticleRNTestHost NavSea Particle release SDK";
module.exports = function withParticleNativeAndroid(config) { module.exports = function withParticleNativeAndroid(config) {
return withProjectBuildGradle(config, (project) => { return withProjectBuildGradle(config, (project) => {
if (project.modResults.contents.includes(marker)) return project; if (project.modResults.contents.includes(marker)) return project;
project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.mavenLocal()\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`; project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.mavenLocal()\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.2")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
return project; return project;
}); });
}; };

View File

@@ -1,7 +0,0 @@
module.exports = {
dependencies: {
"@maplibre-particle/weather-runtime-bridge": {
root: "./modules/weather-runtime-bridge"
}
}
};

View File

@@ -1,5 +1,5 @@
{ {
"extends": "expo/tsconfig.base", "extends": "expo/tsconfig.base",
"compilerOptions": { "strict": true, "noEmit": true, "preserveSymlinks": true }, "compilerOptions": { "strict": true, "noEmit": true, "preserveSymlinks": true },
"include": ["App.tsx", "src", "modules/weather-runtime-bridge/src"] "include": ["App.tsx", "src"]
} }

View File

@@ -2,11 +2,12 @@
## 13.4.1-particle.3 ## 13.4.1-particle.3
- V3-08 将 RN package、Expo plugin 与 Test Host 的目标 Native 坐标同步为 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.2`;`.2` 是 STEP-069 v2 multi-plane 所需的新 artifact,绝不复用或覆盖 `.1`。
- 此项仅为 Android 手工构建、Maven Local 发布与单次综合验收做准备;AAR 尚未发布或验收,未执行 Android Gradle/AAR/APK/ADB 或 Jest。
- `FieldRuntime.submitField` 升级为 `maplibre-particle-field/v2` 的明确 `FieldPayloadRefs`:shared validity 加可选 scalar/vector 本地文件,不再接受模糊单 payload。 - `FieldRuntime.submitField` 升级为 `maplibre-particle-field/v2` 的明确 `FieldPayloadRefs`:shared validity 加可选 scalar/vector 本地文件,不再接受模糊单 payload。
- 新增 `setFieldValueSource` 与 `setParticleAdvectionScale`,仅桥接既有 renderer 配置,不重传 field 或改变 Timeline。 - 新增 `setFieldValueSource` 与 `setParticleAdvectionScale`,仅桥接既有 renderer 配置,不重传 field 或改变 Timeline。
- 保留 `submitWindField` native/internal v1 ABI;公共兼容入口改名为 `submitLegacyVectorField`,避免与 v2 混淆。 - 保留 `submitWindField` native/internal v1 ABI;公共兼容入口改名为 `submitLegacyVectorField`,避免与 v2 混淆。
- Android bridge 在读取前校验全部 local file、非空 plane 与合计 64 MiB;Java/JNI 用有 presence flags 和长度的 v2 packed container 恢复 Core `ParticleFieldPayloadBundle`。 - Android bridge 在读取前校验全部 local file、非空 plane 与合计 64 MiB;Java/JNI 用有 presence flags 和长度的 v2 packed container 恢复 Core `ParticleFieldPayloadBundle`。
- Android artifact 未构建、未发布;Android Gradle/AAR/APK/ADB 与 Jest 均未执行。
## 13.4.1-particle.2 ## 13.4.1-particle.2

View File

@@ -7,9 +7,9 @@ Map 与 Camera 必须由 Consumer 直接从 `@maplibre/maplibre-react-native@11.
## 版本关系 ## 版本关系
- RN/TS 包:`13.4.1-particle.3` - RN/TS 包:`13.4.1-particle.3`
- Android Native artifact:`cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1` - Android Native artifact(V3-08 目标):`cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.2`
本次仅整理 RN/TS 公共接口。Native artifact 未构建、未修改、未发布;Expo plugin 继续解析上述冻结 artifact。 `.2` 是 STEP-069 v2 multi-plane(shared validity 加可选 scalar/vector)的新 Native artifact 坐标,不能复用或覆盖旧 `.1`。本次只完成坐标准备;用户尚未手工构建、发布或验收 Android AAR,Expo plugin 现指向目标 `.2`,不得将其表述为已发布或已通过。
## 最小接入 ## 最小接入
@@ -61,7 +61,7 @@ await FieldRuntime.setFieldColorStops(reactTag, [
`submitField` 的 v2 `payloadRefs` 必须有 `validityFileUri`,并且必须有 `scalarFileUri` 或 `vectorFileUri`。每个引用只能是 `file://` URI 或绝对本地路径;桥接层会在读入前检查全部文件、非空性和合计 64 MiB 上限。它不接受 JS number array、Base64 或大 JSON payload。Core 继续按 metadata 精确校验各 plane 的长度、encoding、layout 和 validity。 `submitField` 的 v2 `payloadRefs` 必须有 `validityFileUri`,并且必须有 `scalarFileUri` 或 `vectorFileUri`。每个引用只能是 `file://` URI 或绝对本地路径;桥接层会在读入前检查全部文件、非空性和合计 64 MiB 上限。它不接受 JS number array、Base64 或大 JSON payload。Core 继续按 metadata 精确校验各 plane 的长度、encoding、layout 和 validity。
`.2` 的 `submitField(reactTag, metadataJson, payloadFileUri)` 是单一 v1 vector payload。`.3` 不再以该模糊形状作为主入口;确有旧 native seam 的 Consumer 可显式调用 `submitLegacyVectorField`,它仅转发冻结的 v1 `submitWindField` ABI。新代码应使用 v2 `submitField`。 旧 Native `.1` 的 `submitWindField(metadataJson, byte[])` 是单一 v1 vector ABI。`.3` 不再以该模糊形状作为主入口;确有旧 native seam 的 Consumer 可显式调用 `submitLegacyVectorField`,它仅转发冻结的 v1 ABI。新代码应使用目标 Native `.2` 提供的 v2 `submitField`。
## 安装与发布 ## 安装与发布

View File

@@ -14,7 +14,7 @@ android {
dependencies { dependencies {
implementation "com.facebook.react:react-android" implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.20" implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.20"
implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1" implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.2"
compileOnly project(":maplibre_maplibre-react-native") compileOnly project(":maplibre_maplibre-react-native")
} }

View File

@@ -6,7 +6,7 @@ const marker = "// @navsea/maplibre-particle-rn Android OpenGL release";
module.exports = function withNavSeaMapLibreParticle(config) { module.exports = function withNavSeaMapLibreParticle(config) {
return withProjectBuildGradle(config, (project) => { return withProjectBuildGradle(config, (project) => {
if (project.modResults.contents.includes(marker)) return project; if (project.modResults.contents.includes(marker)) return project;
project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`; project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.2")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
return project; return project;
}); });
}; };

View File

@@ -1 +1 @@
13.4.1-particle.1 13.4.1-particle.2

View File

@@ -1,10 +1,10 @@
{ {
"release": "13.4.1-particle.2", "release": "13.4.1-particle.3",
"releaseState": "released", "releaseState": "awaiting_android_manual_build_publish_acceptance",
"upstreamMapLibreNative": "13.4.1", "upstreamMapLibreNative": "13.4.1",
"upstreamCoreCommit": "18545ca10d296704b2cba675b182ff2c8da24588", "upstreamCoreCommit": "18545ca10d296704b2cba675b182ff2c8da24588",
"extensionLine": "particle", "extensionLine": "particle",
"extensionRevision": 2, "extensionRevision": 3,
"platform": { "platform": {
"android": { "android": {
"renderer": "opengl" "renderer": "opengl"
@@ -12,68 +12,63 @@
}, },
"reactNativePackage": { "reactNativePackage": {
"name": "@navsea/maplibre-particle-rn", "name": "@navsea/maplibre-particle-rn",
"version": "13.4.1-particle.2", "version": "13.4.1-particle.3",
"mapLibreReactNative": "11.3.6" "mapLibreReactNative": "11.3.6"
}, },
"nativeArtifact": { "nativeArtifact": {
"group": "cc.navsea.maplibre", "group": "cc.navsea.maplibre",
"artifact": "android-sdk-particle-opengl", "artifact": "android-sdk-particle-opengl",
"version": "13.4.1-particle.1" "version": "13.4.1-particle.2",
"state": "awaiting_manual_build_publish_acceptance"
}, },
"releaseRelationship": "13.4.1-particle.2 仅变更 RN/TS Field Renderer 公共接口;Native artifact 保持 13.4.1-particle.1,未构建、修改或发布 AAR。", "releaseRelationship": "13.4.1-particle.3 的 v2 multi-plane RN/TS API 目标为新的 Native artifact 13.4.1-particle.2;不得复用、覆盖或宣称发布旧 13.4.1-particle.1。",
"canonicalField": { "canonicalField": {
"schema": "maplibre-particle-field/v1", "schema": "maplibre-particle-field/v2",
"transport": "metadataJson + local file:// payload", "transport": "metadataJson + validity local ref + optional scalar/vector local refs",
"encoding": "float32-le", "encoding": "float32-le",
"layout": "interleaved-uv" "layout": "plane-specific; vector is interleaved-uv"
}, },
"validation": { "validation": {
"androidPackageConsumer": { "android": {
"status": "not_run",
"required": [
"Native .2 Release OpenGL AAR build and Maven Local publish",
"AAR SHA consistency check",
"javap WeatherRuntime v2 API check",
"Test Host clean prebuild/build/install/start",
"five field datasets and Runtime READY acceptance"
]
},
"previousBaseline": {
"reactNativePackage": "13.4.1-particle.2",
"nativeArtifact": "13.4.1-particle.1",
"status": "pass", "status": "pass",
"targetReactNativePackage": "13.4.1-particle.2",
"validatedAt": "2026-09-10", "validatedAt": "2026-09-10",
"consumer": "integration/react-native/MapLibreParticlePackageConsumerTestHost", "consumer": "integration/react-native/MapLibreParticlePackageConsumerTestHost",
"dataset": "ECMWF HRES 20260807 T0-T3", "dataset": "ECMWF HRES 20260807 T0-T3",
"transport": "FILE_REF / FLOAT32_LE / INTERLEAVED_UV", "transport": "FILE_REF / FLOAT32_LE / INTERLEAVED_UV",
"nativeArtifact": "13.4.1-particle.1",
"checks": [ "checks": [
"FieldRuntime attach", "FieldRuntime attach",
"T0-T3 ordered submit", "T0-T3 ordered submit",
"runtime READY", "runtime READY",
"field OFF + particle ON", "field and particle independent visibility",
"field ON + particle OFF", "no fatal native bridge errors"
"restore both ON", ],
"hard-gate no fatal/native bridge errors" "releaseTag": "maplibre-particle/rn/13.4.1-particle.2",
] "releaseSourceCommit": "e43b58707cfd0f87965424477ed3f95625b3f724",
}, "npmTarball": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.2.tgz",
"previousBaseline": { "npmTarballSha256": "ace9e0401824839d18a93cc4201c1f5d4cccbff0636570cfde82fd80b60779b5",
"reactNativePackage": "13.4.1-particle.1", "npmTarballSizeBytes": 10660,
"status": "pass", "publicHttpStatus": 200,
"validatedAt": "2026-09-09", "publicBytesVerified": true
"consumer": "integration/react-native/MapLibreParticlePackageConsumerTestHost",
"dataset": "ECMWF HRES 20260807 T0-T3",
"transport": "FILE_REF / FLOAT32_LE / INTERLEAVED_UV"
},
"publicHttpsConsumerInstall": {
"status": "pass",
"validatedAt": "2026-09-10",
"url": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.2.tgz",
"reactNativePackage": "13.4.1-particle.2",
"mapLibreReactNative": "11.3.6",
"typecheck": "pass",
"dependencyTree": "single MLRN 11.3.6 deduped"
} }
}, },
"publication": { "publication": {
"npmRegistry": null, "nativeArtifact": {
"mavenRepository": "https://pbf.jpcn.ccwu.cc/maven", "status": "not_published",
"npmTarball": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.2.tgz", "aarSha256": null,
"npmTarballSha256": "ace9e0401824839d18a93cc4201c1f5d4cccbff0636570cfde82fd80b60779b5", "publishedAt": null
"npmTarballSizeBytes": 10660, },
"publishedAt": "2026-09-10", "releaseTag": null
"publicHttpStatus": 200, }
"publicBytesVerified": true
},
"releaseTag": "maplibre-particle/rn/13.4.1-particle.2",
"releaseSourceCommit": "e43b58707cfd0f87965424477ed3f95625b3f724"
} }

View File

@@ -673,3 +673,9 @@ WeatherColorLayer 的 `ParticleGpuValidationUBO` designated initializer 补齐 `
- 最终冻结链:fixture validator、package tsc、RN Test Host typecheck、RN codegen、Linux OpenGL `mbgl-test`/`mbgl-test-runner` target 均 PASS;V3 综合过滤 21/21 PASS,`git diff --check` PASS,验证链退出码 0。 - 最终冻结链:fixture validator、package tsc、RN Test Host typecheck、RN codegen、Linux OpenGL `mbgl-test`/`mbgl-test-runner` target 均 PASS;V3 综合过滤 21/21 PASS,`git diff --check` PASS,验证链退出码 0。
- 综合回归发现并修正两个测试 fixture 陈旧问题:Host T0/T1 重复 GPU content 导致 Duplicate,以及旧 east/north/vortex `timeIndex=0` 导致 Timeline fail-closed。只改测试数据组合,不改 Runtime/Timeline/Renderer 生产逻辑。 - 综合回归发现并修正两个测试 fixture 陈旧问题:Host T0/T1 重复 GPU content 导致 Duplicate,以及旧 east/north/vortex `timeIndex=0` 导致 Timeline fail-closed。只改测试数据组合,不改 Runtime/Timeline/Renderer 生产逻辑。
- Android Gradle/AAR/APK/ADB/Jest 未运行;V3-07 Linux 冻结关闭,下一步统一进入 V3-08 Android 综合验收。STEP-062 用户 dirty 与历史 untracked/build/cache/tgz 未纳入。 - Android Gradle/AAR/APK/ADB/Jest 未运行;V3-07 Linux 冻结关闭,下一步统一进入 V3-08 Android 综合验收。STEP-062 用户 dirty 与历史 untracked/build/cache/tgz 未纳入。
## 2026-09-11 — STEP-072 V3-08 Android Native v2 与单次综合验收准备
- Native Android 目标版本升为 `13.4.1-particle.2`;RN package `13.4.1-particle.3`、Expo plugin、RN Test Host 与 release manifest 统一到该坐标,当前 Android 状态保持 `not_run`,未伪造 AAR SHA、发布时间或 release tag。
- 准备审计发现 Test Host 同时 autolink legacy local bridge 与正式 package bridge,二者均注册 `WeatherRuntimeBridge` / `WeatherRuntimeBridgeSpec`。STEP-072A 删除 legacy tracked bridge、根 `react-native.config.js`,并收敛 `tsconfig.json`;Expo autolinking 复核后只剩正式 `@navsea/maplibre-particle-rn` bridge。
- 静态 hard gate:Host/package TypeScript、RN codegen、V3 fixture validator、两个 Expo plugin 语法、release manifest JSON、`git diff --check` 全部 PASS;legacy untracked `android/build/` 原样保留。
- 已冻结 V3-08 单次 Android 验收流程:Linux 构建/发布 Native `.2` → SHA/API 检查 → Win11 Maven Local handoff → clean prebuild/dependencyInsight/一次 APK build → Metro 8091 → 固定设备 `192.168.1.146` → Wind/Rain/Wave/Tide/synthetic Wave 五组综合验收。Android Gradle/AAR/APK/ADB/Jest 均未执行,状态 `IMPLEMENTED / WAITING_ANDROID_VALIDATION`。