feat: validate ios RN particle metal path
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -32,3 +32,9 @@
|
||||
## 下一步
|
||||
|
||||
STEP-082 已关闭。远端发布仍未执行,manifest 的 artifact URL 保持 `LOCAL_BUILD`;后续发布必须另行上传并以发布后的 URL/校验结果更新 release 元数据。STEP-083 不因本记录自动开始。
|
||||
|
||||
## STEP-083 post-close correction(2026-09-18)
|
||||
|
||||
本段追加纠正,不改写上方当时的历史记录。后续真实 RN device 验证确认,上方使用裸 `bazel build //platform/ios:MapLibre.dynamic` 取得的默认 OpenGL artifact(`24639200` bytes,SHA-256=`64faa174c5c0dc0a2da3e289f0beaf2dedf7298665e3345f39836729b668e239`,source commit `136ed726…`)只有色场、没有 Particle render,故作废且不再作为 iOS Particle 发行基线。
|
||||
|
||||
iOS Particle 发行固定改用 `bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic`。Metal XCFramework 为 `24943196` bytes,SHA-256=`ae88975fc20f4d9961b95079638d0e9c0d8e61d4394d5c0a8ffcc48d173a827f`,source commit=`bdd7f40f9029031816c9bf4db6e1cd6c7c69b7bd`。其 device binary 含 `MLNMapView`、Metal/`MTLDevice`/`ParticleGPU` 标记且无 OpenGL marker;Pleiades iPad mini 5 RN Test Host signed Release 安装后,用户已确认粒子恢复。artifact 仍为本地构建,URL 保持 `LOCAL_BUILD`。
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# STEP-083 iOS RN 统一 FieldRuntime / TurboModule / Fabric 与 XCFramework 消费 v1
|
||||
|
||||
状态:`IMPLEMENTED / IOS_CORE_PATH_PASS / WAITING_FINAL_CONTROL_REGRESSION`
|
||||
|
||||
## 基线与范围
|
||||
|
||||
- branch:`particle/main`;开始 HEAD:`bdd7f40f9029031816c9bf4db6e1cd6c7c69b7bd`。
|
||||
- STEP-082 public facade 与 `//platform/ios:MapLibre.dynamic` Bazel build 已由 RDC 真实验证 PASS。
|
||||
- 本批次仅增加 RN package iOS bridge/pod/codegen/autolinking、Test Host iOS identity 和记录;没有修改 Android、Shared Core、QBIN contract、renderer、NavSea 或 frozen TS public API。
|
||||
|
||||
## 已实现
|
||||
|
||||
- `WeatherRuntimeBridge` 是 RN 0.85.2 codegen TurboModule:`NativeWeatherRuntimeBridgeSpecJSI`、Promise `resolve/reject` ABI 及 package `ios.modulesProvider` 均使用当前本地 RN 0.85.2 source 的真实 pattern。
|
||||
- 每个 Map command 先校验正整数 `reactTag`,再通过 RN 注入的当前 Fabric/UIManager `viewRegistry_DEPRECATED` 进入 UI block,重新确认 `MLRNMapViewComponentView` → `contentView` → `MLRNMapView` → public `MLNMapView`。不缓存 tag、map、Core pointer 或建立第二 registry。
|
||||
- V3、V4、runtime state、weather/particle visibility、timeline、binding 与所有视觉命令均映射到现有 public `MLNParticleFieldRuntime`。V3 `clearWindFields` 的 Android 语义本身等价 `clearFields`。
|
||||
- V3/QBIN file ingress 均只接收 absolute path 或 `file://`,要求 readable regular file、1..64 MiB,并比较读前后 length;metadata、contract、UTF-8 identifier、safe integer 与 inactive `(0,0)` selector 同 Android guard。
|
||||
- pod 只 dependency `MapLibreReactNative 11.3.6`,不声明 MapLibre pod/framework/SPM product。MLRN 11.3.6 的真实 `$MLRN_SPM_SPEC` 将唯一 `MapLibre` product 注入 Pods 和 app target。
|
||||
|
||||
## RDC 实际 iOS 验证(2026-09-18)
|
||||
|
||||
- RDC 已实际执行 npm dependency install(`npm ci`)、typecheck、RN codegen、Expo prebuild、Pod integration、device arm64 build、signed Release build 与安装。MapLibre.framework 签名成功。
|
||||
- Test Host 消费的是 single MapLibre SPM product;未发现第二份 MapLibre binary。新 XCFramework 必须以 Metal 命令 `bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic` 生成。默认裸 Bazel 命令的 OpenGL artifact 只有色场、无粒子,已由 STEP-082 post-close correction 作废。
|
||||
- Pleiades iPad mini 5 上 signed Release 安装运行。一次 V4 Wind load 实际观察到 `clearAllEncodedFields` → color binding → particle binding → T0 `ACCEPTED_INSERTED` → T1 `ACCEPTED_INSERTED` → midpoint seek;用户确认粒子恢复。
|
||||
|
||||
## 尚未逐按钮回归
|
||||
|
||||
- PLAY/PAUSE、clear selected、clear all、detach/re-attach 尚未逐按钮实测;不得将本 Step 标为完成,也不得把单次 V4 load 扩写为完整控制面验收。
|
||||
|
||||
## 已执行检查
|
||||
|
||||
- `ruby -c packages/maplibre-particle-rn/MapLibreParticleRN.podspec`:成功。
|
||||
- `npm ci`、typecheck、RN codegen、Expo prebuild、Pod integration、device arm64 build、signed Release build/installation:均由 RDC 实际执行;Release 日志为 `BUILD SUCCEEDED`。
|
||||
- `npm pack --dry-run --json --cache /private/tmp/step083-npm-cache`:成功,tgz 清单包含 podspec、`ios/WeatherRuntimeBridge.h/.mm`、既有 Android 与 `src`。
|
||||
- package `codegenConfig.ios.modulesProvider.WeatherRuntimeBridge` 静态检查和 Native Spec 34 个方法与 ObjC++ 34 个实现逐项计数完成。
|
||||
|
||||
## RDC 下一步
|
||||
|
||||
1. 用同一个 `App.tsx` 逐按钮完成 PLAY/PAUSE、clear selected/all、detach/re-attach;同时复核 style-not-ready smoke。
|
||||
2. 继续维持 MLRN/App/Particle 的 single `MapLibre` product,禁止为 Particle pod 引入独立 framework。
|
||||
3. 所有后续 iOS Particle artifact 一律以 Metal Bazel 命令构建,并在发布前复核 hash、renderer markers 与远端回读(若有发布授权)。
|
||||
@@ -0,0 +1,24 @@
|
||||
# STEP-082 iOS Native FieldRuntime Facade 与 XCFramework 发行基线 Codex 完成报告 v3
|
||||
|
||||
## 执行结论
|
||||
|
||||
`IMPLEMENTED / IOS_CORE_PATH_PASS / WAITING_FINAL_CONTROL_REGRESSION`。
|
||||
|
||||
本报告是 STEP-083 发现 renderer 选择错误后的 post-close correction;保留 v1/v2 的历史证据,但旧默认 OpenGL XCFramework 不再具有 iOS Particle 发行资格。
|
||||
|
||||
## Renderer 纠正与发行 artifact
|
||||
|
||||
- 不带 renderer flag 的 `bazel build //platform/ios:MapLibre.dynamic` 默认选择 OpenGL。旧 zip:`24639200` bytes,SHA-256=`64faa174c5c0dc0a2da3e289f0beaf2dedf7298665e3345f39836729b668e239`,source commit=`136ed726cf749c24b3673c046581a38ef48be390`。它在 RN Test Host 中可显示色场但无粒子,因此作废,不能发布为 Particle iOS baseline。
|
||||
- 固定发行命令:`bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic`。
|
||||
- 新 Metal zip:`MapLibre.dynamic.xcframework.zip`,`24943196` bytes,SHA-256=`ae88975fc20f4d9961b95079638d0e9c0d8e61d4394d5c0a8ffcc48d173a827f`,source commit=`bdd7f40f9029031816c9bf4db6e1cd6c7c69b7bd`。
|
||||
- device binary 审计含 `MLNMapView`、Metal/`MTLDevice`/`ParticleGPU`,无 OpenGL marker。release manifest 保持 `VALIDATED`、URL=`LOCAL_BUILD`;未伪造远端发布。
|
||||
|
||||
## 真实消费证据与边界
|
||||
|
||||
Pleiades iPad mini 5 上,RN Test Host 的 single-MapLibre SPM signed Release 已构建、MapLibre.framework 已签名并安装。一次 V4 Wind load 实际执行 `clearAllEncodedFields`、color/particle binding、T0/T1 `ACCEPTED_INSERTED` 和 midpoint seek,用户确认“粒子恢复了”。
|
||||
|
||||
本次未逐按钮完成 PLAY/PAUSE、clear selected/all、detach/re-attach 控制回归;这部分保留给 STEP-083 final control regression,不能据此扩展为完整交互验收。
|
||||
|
||||
## 范围与 Git
|
||||
|
||||
新 Metal binary 的可追溯 source commit 是已提交 facade baseline `bdd7f40f9029031816c9bf4db6e1cd6c7c69b7bd`;RN/Test Host 的未提交文件不影响该 MapLibre binary 内容。本报告不提交、不 push,且未修改 Android、Shared Core、renderer 算法或 `NativeWeatherRuntimeBridge.ts`。
|
||||
@@ -0,0 +1,47 @@
|
||||
# STEP-083 iOS RN 统一 FieldRuntime / TurboModule / Fabric 与 XCFramework 消费 Codex 完成报告 v1
|
||||
|
||||
## 1. 执行结论
|
||||
|
||||
`IMPLEMENTED / WAITING_IOS_VALIDATION`。未提交、未 push,最终环境验收交给 RDC。
|
||||
|
||||
## 2. 调查结论
|
||||
|
||||
- MLRN `11.3.6`:Fabric class 是 `MLRNMapViewComponentView`;它在 `prepareView` 设 `contentView = [[MLRNMapView alloc] ...]`;`MLRNMapView : MLNMapView`。
|
||||
- MLRN 当前 module 的真实 lookup 是注入 `viewRegistry_DEPRECATED` 的 `addUIBlock`,在 block 内按 tag 查 component,再验证 component/content classes。bridge 同样使用该当前 RN UIManager registry;没有复制它或保留 view pointer。
|
||||
- RN `0.85.2` generated ObjC++ spec 的 module pattern 为 protocol + `getTurboModule:` 返回 `std::make_shared<...SpecJSI>(params)`,Promise selector 使用 `resolve:(RCTPromiseResolveBlock) reject:(RCTPromiseRejectBlock)`;`codegenConfig.ios.modulesProvider` 生成 provider registration。
|
||||
- MLRN podspec 的 `$MLRN_SPM_SPEC` 确实存在,默认 product 为 `MapLibre`;post-install 将其加入 MLRN pod target 与 user target。
|
||||
|
||||
## 3. Public API parity matrix
|
||||
|
||||
| Spec methods | iOS public seam | status |
|
||||
| --- | --- | --- |
|
||||
| attach, detach, getState | `MLNMapView` category + runtime snapshot | implemented |
|
||||
| clearWindFields, clearFields, submitWindField, submitField | V3 runtime facade | implemented |
|
||||
| submitEncodedField, encoded binding/time/playback/clear (8) | V4 runtime facade | implemented |
|
||||
| value source, advection, enabled/visibility, FPS/time/playback/speed (9) | V3 control facade | implemented |
|
||||
| color stops, particle/trail color, count, line width, length, opacities, fade (8) | visual facade | implemented |
|
||||
|
||||
The 34 `NativeWeatherRuntimeBridge.ts` methods are present as 34 ObjC++ selectors. No TS public type, union, method name, argument ordering, result string, or Android implementation changed; only the Android-specific missing-module text became platform-neutral.
|
||||
|
||||
## 4. Package and binary identity
|
||||
|
||||
- Added `ios/WeatherRuntimeBridge.h/.mm`, `MapLibreParticleRN.podspec`, iOS autolinking and codegen provider configuration; npm files include all of them.
|
||||
- The Particle pod depends only on `MapLibreReactNative` `11.3.6`; it has no MapLibre dependency, vendored framework, or SPM reference. Thus it cannot introduce a second MapLibre product.
|
||||
- The Test Host gained only an iOS bundle identifier. Its `App.tsx` retains one shared JS path and no `Platform.OS` branch.
|
||||
|
||||
## 5. Checks actually run
|
||||
|
||||
- Podspec Ruby syntax: PASS.
|
||||
- package dry-run tarball, with temporary cache: PASS; includes iOS native files, podspec, Android, and src.
|
||||
- codegen provider configuration static assertion: PASS.
|
||||
- Spec/implementation method-count parity: 34/34 PASS.
|
||||
- `git diff --check`: PASS.
|
||||
- `npm ci --offline --ignore-scripts`: BLOCKED by absent locally cached `update-browserslist-db-1.3.2.tgz`; no network was used. Therefore TypeScript/codegen/Xcode/Pod/Test Host runtime have not run.
|
||||
|
||||
## 6. Required RDC validation
|
||||
|
||||
The STEP-082 XCFramework artifact is a local zip and its manifest URL is `LOCAL_BUILD`; it is not yet an MLRN-consumable Swift Package distribution. Configure a real custom `$MLRN_SPM_SPEC` coordinate first, then run codegen/typecheck/Pods/Xcode and Test Host smoke. Check a single MapLibre product/class set and the V4 lifecycle sequence before closing STEP-083.
|
||||
|
||||
## 7. Working tree protection
|
||||
|
||||
The user dirty entries `docs/doxygen/doxygen-awesome-css`, `platform/ios/app/MBXAppDelegate.m`, and `platform/windows/vendor/vcpkg`, plus the two pre-existing STEP-082 task files, were not touched. No Git commit or push was performed.
|
||||
@@ -0,0 +1,37 @@
|
||||
# STEP-083 iOS RN 统一 FieldRuntime TurboModule Fabric 与 XCFramework 消费 Codex 完成报告 v2
|
||||
|
||||
## 执行结论
|
||||
|
||||
`IMPLEMENTED / IOS_CORE_PATH_PASS / WAITING_FINAL_CONTROL_REGRESSION`。
|
||||
|
||||
未提交、未 push。本报告只记录 RDC 已发生的 iOS consumer 验证和 Metal artifact 纠正;它不把未逐按钮测试的控制面写成完成。
|
||||
|
||||
## Git 基线与范围
|
||||
|
||||
- branch:`particle/main`;MapLibre Metal binary source commit:`bdd7f40f9029031816c9bf4db6e1cd6c7c69b7bd`。
|
||||
- RN/Test Host 未提交文件不影响该已提交 MapLibre binary 内容。
|
||||
- 未修改 `NativeWeatherRuntimeBridge.ts`、Android、Shared Core 或 renderer 算法。
|
||||
- 用户既有 dirty `docs/doxygen/doxygen-awesome-css`、`platform/ios/app/MBXAppDelegate.m`、`platform/windows/vendor/vcpkg` 与三份未跟踪 `docs/40_任务` 任务书均不纳入本次修改或提交范围。
|
||||
|
||||
## Renderer 与 artifact
|
||||
|
||||
旧 `bazel build //platform/ios:MapLibre.dynamic` 默认使用 OpenGL。其 artifact 在 RN Test Host 中呈现色场而无粒子,不能用于 Particle iOS 发行。
|
||||
|
||||
固定命令为:
|
||||
|
||||
```sh
|
||||
bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic
|
||||
```
|
||||
|
||||
新 Metal `MapLibre.dynamic.xcframework.zip`:`24943196` bytes,SHA-256=`ae88975fc20f4d9961b95079638d0e9c0d8e61d4394d5c0a8ffcc48d173a827f`。device binary 含 `MLNMapView`、Metal/`MTLDevice`/`ParticleGPU`,无 OpenGL marker。manifest 为 `VALIDATED`,URL 仍为 `LOCAL_BUILD`,未声明远端发布。
|
||||
|
||||
## RDC 已发生验证
|
||||
|
||||
- npm dependency install(`npm ci`)、typecheck、RN codegen、Expo prebuild 和 Pod integration 均已实际执行。
|
||||
- device arm64 build 与 signed Release 均出现 `BUILD SUCCEEDED`;MapLibre.framework 签名成功,Pleiades iPad mini 5 安装运行。
|
||||
- MLRN、App 与 Particle pod 共享 single MapLibre SPM product,没有第二份 MapLibre binary。
|
||||
- 一次 V4 Wind load 实际完成 `clearAllEncodedFields`、color binding、particle binding、T0 `ACCEPTED_INSERTED`、T1 `ACCEPTED_INSERTED` 和 midpoint seek;用户确认“粒子恢复了”。
|
||||
|
||||
## 未完成的 final control regression
|
||||
|
||||
PLAY/PAUSE、clear selected、clear all、detach/re-attach 尚未逐按钮实测。它们是本 Step 的剩余验收项;在完成前不得将状态改写为 `CLOSED` 或 `PASS`。
|
||||
2
integration/react-native/MapLibreParticleRNTestHost/.gitignore
vendored
Normal file
2
integration/react-native/MapLibreParticleRNTestHost/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Expo/RN prebuild output is generated locally and must not enter this source host.
|
||||
/ios/
|
||||
@@ -6,6 +6,7 @@
|
||||
"orientation": "landscape",
|
||||
"newArchEnabled": true,
|
||||
"plugins": ["./plugins/withParticleNativeAndroid"],
|
||||
"android": { "package": "org.maplibre.particle.rntesthost", "minSdkVersion": 24 }
|
||||
"android": { "package": "org.maplibre.particle.rntesthost", "minSdkVersion": 24 },
|
||||
"ios": { "bundleIdentifier": "org.maplibre.particle.rntesthost", "supportsTablet": true }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,23 @@ const { getDefaultConfig } = require("expo/metro-config");
|
||||
const config = getDefaultConfig(__dirname);
|
||||
config.resolver.assetExts = [...new Set([...config.resolver.assetExts, "f32", "u8", "qbin"])];
|
||||
|
||||
const packageRoot = path.resolve(__dirname, "../../../packages/maplibre-particle-rn");
|
||||
const fixtureCandidates = [
|
||||
path.resolve(__dirname, "..", "particle-v3-fields"),
|
||||
path.resolve(__dirname, "..", "..", "particle-v3-fields"),
|
||||
];
|
||||
config.watchFolders = fixtureCandidates.filter((candidate) => fs.existsSync(candidate));
|
||||
config.watchFolders = [
|
||||
...new Set([
|
||||
...(config.watchFolders ?? []),
|
||||
...fixtureCandidates.filter((candidate) => fs.existsSync(candidate)),
|
||||
packageRoot,
|
||||
]),
|
||||
];
|
||||
config.resolver.nodeModulesPaths = [
|
||||
...new Set([
|
||||
path.resolve(__dirname, "node_modules"),
|
||||
...(config.resolver.nodeModulesPaths ?? []),
|
||||
]),
|
||||
];
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -14,20 +14,20 @@ function two(id: string, title: string, fieldValueSource: V3Dataset["fieldValueS
|
||||
}
|
||||
export const fieldDatasets: readonly V3Dataset[] = [
|
||||
two("wind-real", "Wind(本地真实 canonical)", "VECTOR_MAGNITUDE", true, 1,
|
||||
frame("T0", require("../../particle-v3-fields/wind-real/2026080700/metadata.json") as Metadata, require("../../particle-v3-fields/wind-real/2026080700/validity.u8"), undefined, require("../../particle-v3-fields/wind-real/2026080700/vector.f32")),
|
||||
frame("T1", require("../../particle-v3-fields/wind-real/2026080706/metadata.json") as Metadata, require("../../particle-v3-fields/wind-real/2026080706/validity.u8"), undefined, require("../../particle-v3-fields/wind-real/2026080706/vector.f32"))),
|
||||
frame("T0", require("../../../particle-v3-fields/wind-real/2026080700/metadata.json") as Metadata, require("../../../particle-v3-fields/wind-real/2026080700/validity.u8"), undefined, require("../../../particle-v3-fields/wind-real/2026080700/vector.f32")),
|
||||
frame("T1", require("../../../particle-v3-fields/wind-real/2026080706/metadata.json") as Metadata, require("../../../particle-v3-fields/wind-real/2026080706/validity.u8"), undefined, require("../../../particle-v3-fields/wind-real/2026080706/vector.f32"))),
|
||||
two("rain-decoder-fixture", "Rain(decoder fallback)", "SCALAR", false, 1,
|
||||
frame("T0", require("../../particle-v3-fields/rain-decoder-fixture/2026090912/metadata.json") as Metadata, require("../../particle-v3-fields/rain-decoder-fixture/2026090912/validity.u8"), require("../../particle-v3-fields/rain-decoder-fixture/2026090912/scalar.f32")),
|
||||
frame("T1", require("../../particle-v3-fields/rain-decoder-fixture/2026090918/metadata.json") as Metadata, require("../../particle-v3-fields/rain-decoder-fixture/2026090918/validity.u8"), require("../../particle-v3-fields/rain-decoder-fixture/2026090918/scalar.f32"))),
|
||||
frame("T0", require("../../../particle-v3-fields/rain-decoder-fixture/2026090912/metadata.json") as Metadata, require("../../../particle-v3-fields/rain-decoder-fixture/2026090912/validity.u8"), require("../../../particle-v3-fields/rain-decoder-fixture/2026090912/scalar.f32")),
|
||||
frame("T1", require("../../../particle-v3-fields/rain-decoder-fixture/2026090918/metadata.json") as Metadata, require("../../../particle-v3-fields/rain-decoder-fixture/2026090918/validity.u8"), require("../../../particle-v3-fields/rain-decoder-fixture/2026090918/scalar.f32"))),
|
||||
two("wave-real", "Wave(本地真实 WAVEBIN1)", "SCALAR", true, 1.75,
|
||||
frame("T0", require("../../particle-v3-fields/wave-real/2026090918/metadata.json") as Metadata, require("../../particle-v3-fields/wave-real/2026090918/validity.u8"), require("../../particle-v3-fields/wave-real/2026090918/scalar.f32"), require("../../particle-v3-fields/wave-real/2026090918/vector.f32")),
|
||||
frame("T1", require("../../particle-v3-fields/wave-real/2026090919/metadata.json") as Metadata, require("../../particle-v3-fields/wave-real/2026090919/validity.u8"), require("../../particle-v3-fields/wave-real/2026090919/scalar.f32"), require("../../particle-v3-fields/wave-real/2026090919/vector.f32"))),
|
||||
frame("T0", require("../../../particle-v3-fields/wave-real/2026090918/metadata.json") as Metadata, require("../../../particle-v3-fields/wave-real/2026090918/validity.u8"), require("../../../particle-v3-fields/wave-real/2026090918/scalar.f32"), require("../../../particle-v3-fields/wave-real/2026090918/vector.f32")),
|
||||
frame("T1", require("../../../particle-v3-fields/wave-real/2026090919/metadata.json") as Metadata, require("../../../particle-v3-fields/wave-real/2026090919/validity.u8"), require("../../../particle-v3-fields/wave-real/2026090919/scalar.f32"), require("../../../particle-v3-fields/wave-real/2026090919/vector.f32"))),
|
||||
two("tide-real", "Tide/Current(本地真实 BIN)", "VECTOR_MAGNITUDE", true, 1,
|
||||
frame("T0", require("../../particle-v3-fields/tide-real/2026090901/metadata.json") as Metadata, require("../../particle-v3-fields/tide-real/2026090901/validity.u8"), undefined, require("../../particle-v3-fields/tide-real/2026090901/vector.f32")),
|
||||
frame("T1", require("../../particle-v3-fields/tide-real/2026090902/metadata.json") as Metadata, require("../../particle-v3-fields/tide-real/2026090902/validity.u8"), undefined, require("../../particle-v3-fields/tide-real/2026090902/vector.f32"))),
|
||||
frame("T0", require("../../../particle-v3-fields/tide-real/2026090901/metadata.json") as Metadata, require("../../../particle-v3-fields/tide-real/2026090901/validity.u8"), undefined, require("../../../particle-v3-fields/tide-real/2026090901/vector.f32")),
|
||||
frame("T1", require("../../../particle-v3-fields/tide-real/2026090902/metadata.json") as Metadata, require("../../../particle-v3-fields/tide-real/2026090902/validity.u8"), undefined, require("../../../particle-v3-fields/tide-real/2026090902/vector.f32"))),
|
||||
two("wave-synthetic-independence", "Wave synthetic independence", "SCALAR", true, 2.5,
|
||||
frame("T0", require("../../particle-v3-fields/wave-synthetic-independence/2026010100/metadata.json") as Metadata, require("../../particle-v3-fields/wave-synthetic-independence/2026010100/validity.u8"), require("../../particle-v3-fields/wave-synthetic-independence/2026010100/scalar.f32"), require("../../particle-v3-fields/wave-synthetic-independence/2026010100/vector.f32")),
|
||||
frame("T1", require("../../particle-v3-fields/wave-synthetic-independence/2026010106/metadata.json") as Metadata, require("../../particle-v3-fields/wave-synthetic-independence/2026010106/validity.u8"), require("../../particle-v3-fields/wave-synthetic-independence/2026010106/scalar.f32"), require("../../particle-v3-fields/wave-synthetic-independence/2026010106/vector.f32"))),
|
||||
frame("T0", require("../../../particle-v3-fields/wave-synthetic-independence/2026010100/metadata.json") as Metadata, require("../../../particle-v3-fields/wave-synthetic-independence/2026010100/validity.u8"), require("../../../particle-v3-fields/wave-synthetic-independence/2026010100/scalar.f32"), require("../../../particle-v3-fields/wave-synthetic-independence/2026010100/vector.f32")),
|
||||
frame("T1", require("../../../particle-v3-fields/wave-synthetic-independence/2026010106/metadata.json") as Metadata, require("../../../particle-v3-fields/wave-synthetic-independence/2026010106/validity.u8"), require("../../../particle-v3-fields/wave-synthetic-independence/2026010106/scalar.f32"), require("../../../particle-v3-fields/wave-synthetic-independence/2026010106/vector.f32"))),
|
||||
];
|
||||
|
||||
async function uri(module: number, label: string): Promise<string> {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 变更记录
|
||||
|
||||
## Unreleased
|
||||
|
||||
- 新增 iOS `WeatherRuntimeBridge` RN 0.85 codegen TurboModule,实现既有 V3/V4 与视觉控制 ABI;每个命令从当前 Fabric/UIManager view registry 重新解析 MLRN MapView,不缓存 map/runtime 指针。
|
||||
- 新增 iOS pod/autolinking/codegen provider。pod 复用 `MapLibreReactNative` 的唯一 MapLibre product,不嵌入第二份 framework。
|
||||
- iOS Podfile 的 post-install 现在必须同时调用 `$MLRN.post_install(installer)` 和 `$MLRN_PARTICLE.post_install(installer)`;两者消费同一 `$MLRN_SPM_SPEC`,以让 MLRN、App 与 Particle pod target 共享一个 MapLibre package/product。
|
||||
|
||||
## 13.4.1-particle.5(当前 Android QBIN V4 Renderer 基线)
|
||||
|
||||
- 新增 QBIN V4 `BindingProjection` → renderer-facing canonical snapshot materialization,并由 `RasterParticle` / `WeatherColor` Renderer 正式消费。
|
||||
|
||||
51
packages/maplibre-particle-rn/MapLibreParticleRN.podspec
Normal file
51
packages/maplibre-particle-rn/MapLibreParticleRN.podspec
Normal file
@@ -0,0 +1,51 @@
|
||||
require "json"
|
||||
|
||||
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
||||
|
||||
# MapLibreReactNative owns the package reference. This helper only adds its
|
||||
# already-configured MapLibre product to this pod target, so ObjC++ sources can
|
||||
# import the same module without introducing another binary dependency.
|
||||
$MLRN_PARTICLE = Object.new
|
||||
|
||||
def $MLRN_PARTICLE.post_install(installer)
|
||||
unless defined?($MLRN) && $MLRN.respond_to?(:_add_spm_to_target)
|
||||
raise "MapLibreParticleRN requires MapLibreReactNative's $MLRN._add_spm_to_target helper. Ensure MapLibreReactNative 11.3.6 is installed before calling $MLRN_PARTICLE.post_install."
|
||||
end
|
||||
|
||||
unless defined?($MLRN_SPM_SPEC) && $MLRN_SPM_SPEC.is_a?(Hash)
|
||||
raise "MapLibreParticleRN requires $MLRN_SPM_SPEC from MapLibreReactNative or the host Podfile before calling $MLRN_PARTICLE.post_install."
|
||||
end
|
||||
|
||||
spm_spec = $MLRN_SPM_SPEC
|
||||
[:url, :requirement, :product_name].each do |key|
|
||||
raise "MapLibreParticleRN requires $MLRN_SPM_SPEC[:#{key}]." if spm_spec[key].nil?
|
||||
end
|
||||
|
||||
project = installer.pods_project
|
||||
target = project.targets.find { |candidate| candidate.name == "MapLibreParticleRN" }
|
||||
raise "MapLibreParticleRN pod target was not found in the Pods project." unless target
|
||||
|
||||
$MLRN._add_spm_to_target(
|
||||
project,
|
||||
target,
|
||||
spm_spec[:url],
|
||||
spm_spec[:requirement],
|
||||
spm_spec[:product_name]
|
||||
)
|
||||
end
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "MapLibreParticleRN"
|
||||
s.version = package["version"]
|
||||
s.summary = package["description"]
|
||||
s.homepage = package.dig("repository", "url")
|
||||
s.license = package["license"]
|
||||
s.authors = { "NavSea" => "dev@navsea.invalid" }
|
||||
s.platforms = { :ios => "15.1" }
|
||||
s.source = { :git => package.dig("repository", "url"), :tag => s.version.to_s }
|
||||
s.source_files = "ios/**/*.{h,m,mm,cpp}"
|
||||
s.dependency "MapLibreReactNative", "11.3.6"
|
||||
# $MLRN_PARTICLE.post_install reuses MapLibreReactNative's one MapLibre SPM
|
||||
# product. Do not declare a MapLibre pod, vendored framework, or SPM product here.
|
||||
install_modules_dependencies(s)
|
||||
end
|
||||
@@ -14,7 +14,7 @@ await FieldRuntime.setEncodedParticleBinding(reactTag, particleBinding);
|
||||
await FieldRuntime.setEncodedForecastTime(reactTag, timestampMs);
|
||||
```
|
||||
|
||||
Android 链路为:`FieldRuntime.submitEncodedField` → `WeatherRuntimeBridgeModule` → `WeatherRuntime.submitEncodedField` → JNI → `RasterArraySource::submitEncodedField` → `QbinV2Decoder::decode`。因此 QBIN 解码发生在 MapLibre Native,不在 JS/TS。
|
||||
Android 链路为:`FieldRuntime.submitEncodedField` → `WeatherRuntimeBridgeModule` → `WeatherRuntime.submitEncodedField` → JNI → `RasterArraySource::submitEncodedField` → `QbinV2Decoder::decode`。iOS 使用同一 `WeatherRuntimeBridge` TurboModule,按每次调用的 Fabric `reactTag` 即时解析当前 MLRN `MapView`,再调用 public `MLNParticleFieldRuntime`。两端的 QBIN 解码都发生在 MapLibre Native,不在 JS/TS。
|
||||
|
||||
V4 主要公共方法:`submitEncodedField`、`setEncodedColorBinding`、`setEncodedParticleBinding`、`setEncodedForecastTime`、`setEncodedForecastPlaybackState`、`setEncodedForecastPlaybackSpeed`、`clearEncodedField`。ColorBinding 支持 `SCALAR`、`VELOCITY_COMPONENT`、`DIRECTION_ANGLE`;ParticleBinding 支持 U/V vector components 或 direction angle。
|
||||
|
||||
@@ -26,6 +26,12 @@ V4 主要公共方法:`submitEncodedField`、`setEncodedColorBinding`、`setEn
|
||||
|
||||
Map 与 Camera 直接从 `@maplibre/maplibre-react-native@11.3.6` 使用。本包把 MLRN 作为 peer dependency,不转导出 MapLibre React Native。
|
||||
|
||||
## iOS 集成
|
||||
|
||||
autolinking 使用 `MapLibreParticleRN.podspec` 和 RN codegen 的 `WeatherRuntimeBridge` provider。pod 只依赖 `MapLibreReactNative`,不声明、链接或嵌入第二份 `MapLibre.framework`。Consumer 必须以 MLRN 11.3.6 的 `$MLRN_SPM_SPEC` 配置同一个 custom `MapLibre` product;Particle pod 不接受独立 MapLibre binary。
|
||||
|
||||
Podfile 的 `post_install do |installer|` 必须同时调用 `$MLRN.post_install(installer)` 与 `$MLRN_PARTICLE.post_install(installer)`。两者复用同一个 `$MLRN_SPM_SPEC`:前者接入 MLRN/App,后者将同一 package reference/product 加到 `MapLibreParticleRN` pod target。Expo plugin 会幂等注入后一个调用;它不设置 `$MLRN_SPM_SPEC`,该值仍由 MLRN 官方 plugin 或宿主 Podfile 提供。
|
||||
|
||||
## Android 发布
|
||||
|
||||
RN tgz:
|
||||
|
||||
@@ -1,12 +1,38 @@
|
||||
const { withProjectBuildGradle } = require("@expo/config-plugins");
|
||||
// Resolve through the consuming Expo project first. This keeps a config plugin
|
||||
// installed from an npm tarball aligned with the host's Expo SDK instead of
|
||||
// accidentally loading a nested or globally-hoisted copy.
|
||||
const configPlugins = require(
|
||||
require.resolve("expo/config-plugins", {
|
||||
paths: [process.cwd(), __dirname],
|
||||
}),
|
||||
);
|
||||
|
||||
const marker = "// @navsea/maplibre-particle-rn Android OpenGL release";
|
||||
const {
|
||||
CodeGenerator,
|
||||
withPodfile,
|
||||
withProjectBuildGradle,
|
||||
} = configPlugins;
|
||||
|
||||
const androidMarker = "// @navsea/maplibre-particle-rn Android OpenGL release";
|
||||
const iosTag = "navsea-maplibre-particle-rn-maplibre-spm";
|
||||
|
||||
/** Resolves MLRN's single Native SDK request to the NavSea release artifact, never a second AAR. */
|
||||
module.exports = function withNavSeaMapLibreParticle(config) {
|
||||
return withProjectBuildGradle(config, (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.5")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
|
||||
config = withProjectBuildGradle(config, (project) => {
|
||||
if (project.modResults.contents.includes(androidMarker)) return project;
|
||||
project.modResults.contents += `\n${androidMarker}\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.5")\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 withPodfile(config, (podfile) => {
|
||||
podfile.modResults.contents = CodeGenerator.mergeContents({
|
||||
src: podfile.modResults.contents,
|
||||
newSrc: "$MLRN_PARTICLE.post_install(installer)",
|
||||
tag: iosTag,
|
||||
comment: "#",
|
||||
anchor: /post_install do \|installer\|/,
|
||||
offset: 1,
|
||||
}).contents;
|
||||
return podfile;
|
||||
});
|
||||
};
|
||||
|
||||
4
packages/maplibre-particle-rn/ios/WeatherRuntimeBridge.h
Normal file
4
packages/maplibre-particle-rn/ios/WeatherRuntimeBridge.h
Normal file
@@ -0,0 +1,4 @@
|
||||
#import <WeatherRuntimeBridgeSpec/WeatherRuntimeBridgeSpec.h>
|
||||
|
||||
@interface WeatherRuntimeBridge : NSObject <NativeWeatherRuntimeBridgeSpec>
|
||||
@end
|
||||
229
packages/maplibre-particle-rn/ios/WeatherRuntimeBridge.mm
Normal file
229
packages/maplibre-particle-rn/ios/WeatherRuntimeBridge.mm
Normal file
@@ -0,0 +1,229 @@
|
||||
#import "WeatherRuntimeBridge.h"
|
||||
|
||||
#import <MapLibre/MapLibre.h>
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
using namespace facebook::react;
|
||||
|
||||
namespace {
|
||||
constexpr NSUInteger MaxPayloadBytes = 64u * 1024u * 1024u;
|
||||
constexpr NSUInteger MaxMetadataBytes = 1'048'576u;
|
||||
constexpr NSUInteger MaxContractBytes = 256u * 1024u;
|
||||
constexpr double MaxSafeInteger = 9007199254740991.0;
|
||||
|
||||
BOOL validIdentifier(NSString *value) {
|
||||
const NSUInteger length = [value lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
|
||||
return length >= 1 && length <= 64;
|
||||
}
|
||||
BOOL validSafeInteger(double value, BOOL positive) {
|
||||
return isfinite(value) && floor(value) == value && value >= (positive ? 1 : 0) && value <= MaxSafeInteger;
|
||||
}
|
||||
BOOL validUInt16(double value, BOOL positive) {
|
||||
return isfinite(value) && floor(value) == value && value >= (positive ? 1 : 0) && value <= UINT16_MAX;
|
||||
}
|
||||
NSString *commandString(MLNParticleFieldRuntimeCommandResult result) {
|
||||
return result == MLNParticleFieldRuntimeCommandResultAccepted ? @"ACCEPTED" : @"REJECTED";
|
||||
}
|
||||
NSString *v3String(MLNParticleHostFieldSubmitResult result) {
|
||||
switch (result) {
|
||||
case MLNParticleHostFieldSubmitResultRejectedSourceMode: return @"REJECTED_SOURCE_MODE";
|
||||
case MLNParticleHostFieldSubmitResultRejectedAdapter: return @"REJECTED_ADAPTER";
|
||||
case MLNParticleHostFieldSubmitResultRejectedIncompatible: return @"REJECTED_INCOMPATIBLE";
|
||||
case MLNParticleHostFieldSubmitResultRejectedNonMonotonic: return @"REJECTED_NON_MONOTONIC";
|
||||
case MLNParticleHostFieldSubmitResultRejectedCapacity: return @"REJECTED_CAPACITY";
|
||||
case MLNParticleHostFieldSubmitResultAcceptedPendingInitialWindow: return @"ACCEPTED_PENDING_INITIAL_WINDOW";
|
||||
case MLNParticleHostFieldSubmitResultInitialWindowReady: return @"INITIAL_WINDOW_READY";
|
||||
case MLNParticleHostFieldSubmitResultAcceptedNextSnapshot: return @"ACCEPTED_NEXT_SNAPSHOT";
|
||||
case MLNParticleHostFieldSubmitResultDuplicate: return @"DUPLICATE";
|
||||
default: return @"UNKNOWN";
|
||||
}
|
||||
}
|
||||
NSString *v4String(MLNParticleEncodedFieldSubmitResult result) {
|
||||
switch (result) {
|
||||
case MLNParticleEncodedFieldSubmitResultAcceptedInserted: return @"ACCEPTED_INSERTED";
|
||||
case MLNParticleEncodedFieldSubmitResultAcceptedReplacedRevision: return @"ACCEPTED_REPLACED_REVISION";
|
||||
case MLNParticleEncodedFieldSubmitResultDuplicate: return @"DUPLICATE";
|
||||
case MLNParticleEncodedFieldSubmitResultRejectedDecode: return @"REJECTED_DECODE";
|
||||
case MLNParticleEncodedFieldSubmitResultRejectedContract: return @"REJECTED_CONTRACT";
|
||||
case MLNParticleEncodedFieldSubmitResultRejectedInvalidFieldKey: return @"REJECTED_INVALID_FIELD_KEY";
|
||||
case MLNParticleEncodedFieldSubmitResultRejectedIdentityConflict: return @"REJECTED_IDENTITY_CONFLICT";
|
||||
case MLNParticleEncodedFieldSubmitResultRejectedStaleRevision: return @"REJECTED_STALE_REVISION";
|
||||
case MLNParticleEncodedFieldSubmitResultRejectedIncompatible: return @"REJECTED_INCOMPATIBLE";
|
||||
case MLNParticleEncodedFieldSubmitResultRejectedCapacity: return @"REJECTED_CAPACITY";
|
||||
default: return @"REJECTED_CONTRACT";
|
||||
}
|
||||
}
|
||||
BOOL isWeatherObjectsNotReadyError(MLNParticleFieldRuntimeErrorCode code) {
|
||||
switch (code) {
|
||||
case MLNParticleFieldRuntimeErrorSourceMissing:
|
||||
case MLNParticleFieldRuntimeErrorSourceTypeMismatch:
|
||||
case MLNParticleFieldRuntimeErrorWeatherColorLayerMissing:
|
||||
case MLNParticleFieldRuntimeErrorWeatherColorLayerTypeMismatch:
|
||||
case MLNParticleFieldRuntimeErrorParticleLayerMissing:
|
||||
case MLNParticleFieldRuntimeErrorParticleLayerTypeMismatch:
|
||||
case MLNParticleFieldRuntimeErrorLayerSourceMismatch:
|
||||
return YES;
|
||||
default:
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
NSString *attachErrorCode(NSError *error) {
|
||||
switch (error.code) {
|
||||
case MLNParticleFieldRuntimeErrorStyleNotReady: return @"STYLE_NOT_READY";
|
||||
case MLNParticleFieldRuntimeErrorMapUnavailable: return @"MAP_NOT_READY";
|
||||
default: return isWeatherObjectsNotReadyError((MLNParticleFieldRuntimeErrorCode)error.code) ? @"WEATHER_OBJECTS_NOT_READY" : @"NATIVE_REJECTED";
|
||||
}
|
||||
}
|
||||
NSString *encodedFacadeErrorCode(NSError *error) {
|
||||
switch (error.code) {
|
||||
case MLNParticleFieldRuntimeErrorInvalidArgument: return @"INVALID_ARGUMENT";
|
||||
case MLNParticleFieldRuntimeErrorDisposed: return @"RUNTIME_DISPOSED";
|
||||
case MLNParticleFieldRuntimeErrorStyleNotReady: return @"SOURCE_MISSING";
|
||||
default: return isWeatherObjectsNotReadyError((MLNParticleFieldRuntimeErrorCode)error.code) ? @"SOURCE_MISSING" : @"NATIVE_REJECTED";
|
||||
}
|
||||
}
|
||||
NSDictionary *snapshot(MLNParticleFieldRuntime *runtime) {
|
||||
NSString *state[] = {@"DISABLED", @"WAITING_FOR_FIELDS", @"READY", @"ERROR"};
|
||||
NSString *playback[] = {@"PLAY", @"PAUSE"};
|
||||
NSString *fps[] = {@"AUTO", @"FPS_30", @"FPS_60"};
|
||||
return @{ @"state": state[runtime.state], @"playbackState": playback[runtime.playbackState], @"targetFps": fps[runtime.targetFps], @"disposed": @(runtime.disposed) };
|
||||
}
|
||||
}
|
||||
|
||||
@implementation WeatherRuntimeBridge
|
||||
|
||||
@synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED;
|
||||
|
||||
RCT_EXPORT_MODULE(WeatherRuntimeBridge)
|
||||
|
||||
- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params {
|
||||
return std::make_shared<NativeWeatherRuntimeBridgeSpecJSI>(params);
|
||||
}
|
||||
|
||||
// RN 0.85.2 injects this registry from the current Fabric/UIManager interop layer. It is not
|
||||
// a Map registry: every call schedules a fresh UI block and validates the MLRN component class.
|
||||
- (void)withMapForTag:(double)reactTag requireRuntime:(BOOL)requireRuntime resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject action:(void (^)(MLNMapView *, MLNParticleFieldRuntime * _Nullable))action {
|
||||
if (!validSafeInteger(reactTag, YES)) { reject(@"INVALID_ARGUMENT", @"reactTag must be a positive integer", nil); return; }
|
||||
RCTViewRegistry *registry = _viewRegistry_DEPRECATED;
|
||||
if (!registry) { reject(@"MAP_VIEW_NOT_FOUND", @"Fabric UIManager view registry is unavailable", nil); return; }
|
||||
[registry addUIBlock:^(RCTViewRegistry *currentRegistry) {
|
||||
UIView *component = [currentRegistry viewForReactTag:@((NSInteger)reactTag)];
|
||||
// MLRN's iOS pod intentionally marks its implementation headers private. Its verified
|
||||
// Fabric seam is this component class + contentView; use runtime checks, not private imports.
|
||||
if (![component isKindOfClass:NSClassFromString(@"MLRNMapViewComponentView")]) { reject(@"MAP_VIEW_NOT_FOUND", @"reactTag does not resolve to a current MLRNMapView", nil); return; }
|
||||
UIView *content = [component valueForKey:@"contentView"];
|
||||
if (![content isKindOfClass:NSClassFromString(@"MLRNMapView")] || ![content isKindOfClass:MLNMapView.class]) { reject(@"MAP_VIEW_NOT_FOUND", @"MLRN MapView content view is unavailable", nil); return; }
|
||||
MLNMapView *map = (MLNMapView *)content;
|
||||
MLNParticleFieldRuntime *runtime = map.particleFieldRuntime;
|
||||
if (requireRuntime && (!runtime || runtime.disposed)) { reject(@"RUNTIME_DISPOSED", @"WeatherRuntime is not attached to this map", nil); return; }
|
||||
action(map, runtime);
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSData *)readPayload:(NSString *)reference error:(NSString **)message {
|
||||
if (reference.length == 0) { *message = @"payload file reference is empty"; return nil; }
|
||||
NSURL *url = nil;
|
||||
if ([reference hasPrefix:@"file://"]) url = [NSURL URLWithString:reference];
|
||||
else if ([reference hasPrefix:@"/"]) url = [NSURL fileURLWithPath:reference];
|
||||
else { *message = @"payload file reference must use file:// or an absolute local path"; return nil; }
|
||||
if (!url.isFileURL) { *message = @"payload file reference must be local"; return nil; }
|
||||
NSNumber *regular = nil, *readable = nil, *size = nil;
|
||||
if (![url getResourceValue:®ular forKey:NSURLIsRegularFileKey error:nil] || !regular.boolValue ||
|
||||
![url getResourceValue:&readable forKey:NSURLIsReadableKey error:nil] || !readable.boolValue ||
|
||||
![url getResourceValue:&size forKey:NSURLFileSizeKey error:nil] || size.unsignedLongLongValue == 0 || size.unsignedLongLongValue > MaxPayloadBytes) {
|
||||
*message = @"payload file must be a readable regular file of 1..67108864 bytes"; return nil;
|
||||
}
|
||||
const unsigned long long expected = size.unsignedLongLongValue;
|
||||
NSData *data = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:nil];
|
||||
NSNumber *after = nil;
|
||||
[url getResourceValue:&after forKey:NSURLFileSizeKey error:nil];
|
||||
if (!data || data.length != expected || after.unsignedLongLongValue != expected) { *message = @"payload file changed while being read"; return nil; }
|
||||
return data;
|
||||
}
|
||||
|
||||
- (MLNParticleFieldRuntimeConfiguration *)configuration {
|
||||
return [[MLNParticleFieldRuntimeConfiguration alloc] initWithSourceIdentifier:@"rn-weather-fields" weatherColorLayerIdentifier:@"rn-weather-color" particleLayerIdentifier:@"rn-weather-particles"];
|
||||
}
|
||||
|
||||
- (void)attach:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||
[self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) {
|
||||
NSError *error = nil; MLNParticleFieldRuntime *attached = [map replaceParticleFieldRuntimeWithConfiguration:self.configuration error:&error];
|
||||
if (!attached) {
|
||||
reject(attachErrorCode(error), error.localizedDescription ?: @"WeatherRuntime attach rejected", error); return;
|
||||
}
|
||||
resolve(snapshot(attached));
|
||||
}];
|
||||
}
|
||||
- (void)detach:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||
[self withMapForTag:reactTag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { [map disposeParticleFieldRuntime]; resolve(nil); }];
|
||||
}
|
||||
- (void)getState:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self withMapForTag:reactTag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { resolve(snapshot(runtime)); }]; }
|
||||
|
||||
- (void)command:(double)tag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject action:(MLNParticleFieldRuntimeCommandResult (^)(MLNParticleFieldRuntime *, NSError **))action {
|
||||
[self withMapForTag:tag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleFieldRuntimeCommandResult result = action(runtime, &error); resolve(error ? @"REJECTED" : commandString(result)); }];
|
||||
}
|
||||
- (void)encodedCommand:(double)tag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject action:(MLNParticleFieldRuntimeCommandResult (^)(MLNParticleFieldRuntime *, NSError **))action {
|
||||
[self withMapForTag:tag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleFieldRuntimeCommandResult result = action(runtime, &error); if (error) reject(encodedFacadeErrorCode(error), error.localizedDescription, error); else resolve(commandString(result)); }];
|
||||
}
|
||||
- (void)clearWindFields:(double)t resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self clearFields:t resolve:r reject:j]; }
|
||||
- (void)clearFields:(double)t resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x, NSError **e) { return [x clearFieldsWithError:e]; }]; }
|
||||
- (void)setWeatherEnabled:(double)t enabled:(BOOL)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setWeatherEnabled:v error:e];}]; }
|
||||
- (void)setWeatherColorVisible:(double)t visible:(BOOL)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setWeatherColorVisible:v error:e];}]; }
|
||||
- (void)setParticleVisible:(double)t visible:(BOOL)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setParticleVisible:v error:e];}]; }
|
||||
|
||||
- (void)submitWindField:(double)t metadataJson:(NSString *)metadata payloadFileUri:(NSString *)uri resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j {
|
||||
if (!metadata.length || [metadata lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > MaxMetadataBytes) { j(@"INVALID_ARGUMENT", @"metadataJson UTF-8 payload must be non-empty and at most 1048576 bytes", nil); return; }
|
||||
NSString *message = nil; NSData *data = [self readPayload:uri error:&message]; if (!data) { j(@"PAYLOAD_FILE_UNREADABLE", message, nil); return; }
|
||||
[self withMapForTag:t requireRuntime:YES resolve:r reject:j action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleHostFieldSubmitResult result = [runtime submitWindFieldWithMetadataJSON:metadata payload:data error:&error]; if (error) j(@"NATIVE_REJECTED", error.localizedDescription, error); else r(v3String(result)); }];
|
||||
}
|
||||
- (void)submitField:(double)t metadataJson:(NSString *)metadata validityFileUri:(NSString *)validity scalarFileUri:(NSString *)scalar vectorFileUri:(NSString *)vector resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j {
|
||||
if (!metadata.length || [metadata lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > MaxMetadataBytes) { j(@"INVALID_ARGUMENT", @"metadataJson UTF-8 payload must be non-empty and at most 1048576 bytes", nil); return; }
|
||||
NSString *message = nil; NSData *v = [self readPayload:validity error:&message]; NSData *s = scalar.length ? [self readPayload:scalar error:&message] : nil; NSData *p = vector.length ? [self readPayload:vector error:&message] : nil;
|
||||
if (!v || (scalar.length && !s) || (vector.length && !p) || (!s && !p) || v.length + s.length + p.length > MaxPayloadBytes) { j(@"PAYLOAD_FILE_UNREADABLE", message ?: @"payloadRefs requires scalarFileUri or vectorFileUri and at most 67108864 bytes", nil); return; }
|
||||
[self withMapForTag:t requireRuntime:YES resolve:r reject:j action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleHostFieldSubmitResult result = [runtime submitFieldWithMetadataJSON:metadata scalar:s vector:p validity:v error:&error]; if (error) j(@"NATIVE_REJECTED", error.localizedDescription, error); else r(v3String(result)); }];
|
||||
}
|
||||
- (void)submitEncodedField:(double)t fieldKey:(NSString *)key fieldID:(NSString *)fieldID generation:(double)generation dataVersion:(double)dataVersion contractJson:(NSString *)contract qbinFileUri:(NSString *)uri resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j {
|
||||
if (!validIdentifier(key) || !validIdentifier(fieldID) || !validSafeInteger(generation, YES) || !validSafeInteger(dataVersion, NO) || !contract.length || [contract lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > MaxContractBytes) { j(@"INVALID_ARGUMENT", @"invalid V4 encoded field control-plane argument", nil); return; }
|
||||
NSError *jsonError = nil; id json = [NSJSONSerialization JSONObjectWithData:[contract dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&jsonError];
|
||||
if (![json isKindOfClass:NSDictionary.class] || !validIdentifier(((NSDictionary *)json)[@"profileID"])) { j(@"INVALID_ARGUMENT", @"invalid V4 encoded field control-plane argument", nil); return; }
|
||||
NSString *message = nil; NSData *qbin = [self readPayload:uri error:&message]; if (!qbin) { j(@"PAYLOAD_FILE_UNREADABLE", message, nil); return; }
|
||||
[self withMapForTag:t requireRuntime:YES resolve:r reject:j action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleEncodedFieldSubmitResult result = [runtime submitEncodedFieldWithFieldKey:key fieldID:fieldID generation:(uint64_t)generation dataVersion:(uint64_t)dataVersion contractJSON:contract qbin:qbin error:&error]; if (error) j(encodedFacadeErrorCode(error), error.localizedDescription, error); else r(v4String(result)); }];
|
||||
}
|
||||
- (void)setEncodedColorBinding:(double)t fieldKey:(NSString *)key semanticID:(double)semantic groupID:(double)group expectedValueClass:(NSString *)valueClass expectedUnitID:(double)unit resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j {
|
||||
NSInteger value = [valueClass isEqual:@"SCALAR"] ? 0 : [valueClass isEqual:@"VELOCITY_COMPONENT"] ? 1 : [valueClass isEqual:@"DIRECTION_ANGLE"] ? 2 : -1;
|
||||
if (!validIdentifier(key) || !validUInt16(semantic, YES) || !validUInt16(group, NO) || !validUInt16(unit, NO) || value < 0) { j(@"INVALID_ARGUMENT", @"invalid encoded color binding selector", nil); return; }
|
||||
[self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){ return [x setEncodedColorBindingWithFieldKey:key semanticID:semantic groupID:group expectedValueClass:(MLNParticleFieldValueClass)value expectedUnitID:unit error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected; }];
|
||||
}
|
||||
- (void)setEncodedParticleBinding:(double)t fieldKey:(NSString *)key mode:(NSString *)mode uSemanticID:(double)us uGroupID:(double)ug vSemanticID:(double)vs vGroupID:(double)vg directionSemanticID:(double)ds directionGroupID:(double)dg expectedUnitID:(double)unit resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j {
|
||||
BOOL inactiveU = us == 0 && ug == 0, inactiveV = vs == 0 && vg == 0, inactiveD = ds == 0 && dg == 0;
|
||||
BOOL vector = [mode isEqual:@"VECTOR_COMPONENTS"] && validUInt16(us, YES) && validUInt16(ug, NO) && validUInt16(vs, YES) && validUInt16(vg, NO) && inactiveD;
|
||||
BOOL direction = [mode isEqual:@"DIRECTION_ANGLE"] && inactiveU && inactiveV && validUInt16(ds, YES) && validUInt16(dg, NO);
|
||||
if (!validIdentifier(key) || !validUInt16(unit, NO) || (!vector && !direction)) { j(@"INVALID_ARGUMENT", @"active encoded particle selectors must be uint16 with non-zero semanticID; inactive slots are exactly zero", nil); return; }
|
||||
[self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){ return [x setEncodedParticleBindingWithFieldKey:key mode:(vector ? MLNParticleEncodedParticleBindingModeVectorComponents : MLNParticleEncodedParticleBindingModeDirectionAngle) uSemanticID:us uGroupID:ug vSemanticID:vs vGroupID:vg directionSemanticID:ds directionGroupID:dg expectedUnitID:unit error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected; }];
|
||||
}
|
||||
- (void)clearEncodedField:(double)t fieldKey:(NSString *)key resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!validIdentifier(key)) { j(@"INVALID_ARGUMENT", @"invalid fieldKey", nil); return; } [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x clearEncodedFieldWithFieldKey:key error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; }
|
||||
- (void)clearAllEncodedFields:(double)t resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x clearAllEncodedFieldsWithError:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; }
|
||||
- (void)setEncodedForecastTime:(double)t timestampMs:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!validSafeInteger(v, NO)) { j(@"INVALID_ARGUMENT", @"encoded forecast time must be safe integer",nil); return; } [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setEncodedForecastTime:v error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; }
|
||||
- (void)setEncodedForecastPlaybackState:(double)t playback:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!([v isEqual:@"PLAY"] || [v isEqual:@"PAUSE"])) { j(@"INVALID_ARGUMENT", @"invalid encoded playback",nil); return; } [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setEncodedForecastPlaybackState:[v isEqual:@"PLAY"] error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; }
|
||||
- (void)setEncodedForecastPlaybackSpeed:(double)t speed:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!isfinite(v) || v <= 0 || v > FLT_MAX) { j(@"INVALID_ARGUMENT", @"invalid encoded playback speed",nil); return; } [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setEncodedForecastPlaybackSpeed:v error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; }
|
||||
|
||||
- (void)setTargetFps:(double)t target:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { MLNParticleFieldRuntimeTargetFps x = [v isEqual:@"AUTO"] ? MLNParticleFieldRuntimeTargetFpsAuto : [v isEqual:@"FPS_30"] ? MLNParticleFieldRuntimeTargetFps30 : [v isEqual:@"FPS_60"] ? MLNParticleFieldRuntimeTargetFps60 : (MLNParticleFieldRuntimeTargetFps)99; if (x == 99) { j(@"INVALID_ARGUMENT", @"unsupported enum value",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setTargetFps:x error:e];}]; }
|
||||
- (void)setForecastTime:(double)t timestampMs:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!validSafeInteger(v, NO)) { j(@"INVALID_ARGUMENT", @"forecast timestamp must be a non-negative epoch millisecond integer",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setForecastTime:v error:e];}]; }
|
||||
- (void)setPlaybackState:(double)t playback:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { MLNParticleFieldRuntimePlaybackState x = [v isEqual:@"PLAY"] ? MLNParticleFieldRuntimePlaybackStatePlay : [v isEqual:@"PAUSE"] ? MLNParticleFieldRuntimePlaybackStatePause : (MLNParticleFieldRuntimePlaybackState)99; if (x == 99) { j(@"INVALID_ARGUMENT", @"unsupported enum value",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setPlaybackState:x error:e];}]; }
|
||||
- (void)setPlaybackSpeed:(double)t speed:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { float x = [v isEqual:@"HALF"] ? .5f : [v isEqual:@"NORMAL"] ? 1.f : [v isEqual:@"DOUBLE"] ? 2.f : -1.f; if (x < 0) { j(@"INVALID_ARGUMENT", @"unsupported enum value",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setPlaybackSpeed:x error:e];}]; }
|
||||
- (void)setFieldValueSource:(double)t source:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { MLNParticleFieldValueSource x = [v isEqual:@"SCALAR"] ? MLNParticleFieldValueSourceScalar : [v isEqual:@"VECTOR_MAGNITUDE"] ? MLNParticleFieldValueSourceVectorMagnitude : (MLNParticleFieldValueSource)99; if (x == 99) { j(@"INVALID_ARGUMENT", @"unsupported enum value",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setFieldValueSource:x error:e];}]; }
|
||||
- (void)setParticleAdvectionScale:(double)t scale:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!isfinite(v) || v <= 0) { j(@"INVALID_ARGUMENT", @"advectionScale must be finite and > 0",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setParticleAdvectionScale:v error:e];}]; }
|
||||
- (void)floatCommand:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j action:(MLNParticleFieldRuntimeCommandResult (^)(MLNParticleFieldRuntime *, float, NSError **))a { if (!isfinite(v)) { j(@"INVALID_ARGUMENT", @"value must be finite",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return a(q, v, e);}]; }
|
||||
- (void)setWeatherColorOpacity:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setWeatherColorOpacity:x error:e];}]; }
|
||||
- (void)setParticleLineWidth:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setParticleLineWidth:x error:e];}]; }
|
||||
- (void)setParticleLength:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setParticleLength:x error:e];}]; }
|
||||
- (void)setParticleOpacity:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setParticleOpacity:x error:e];}]; }
|
||||
- (void)setTrailOpacity:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setTrailOpacity:x error:e];}]; }
|
||||
- (void)setTrailFade:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setTrailFade:x error:e];}]; }
|
||||
- (void)setParticleCount:(double)t count:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!isfinite(v) || v < 0 || v > 2147483647.0 || floor(v) != v) { j(@"INVALID_ARGUMENT", @"particle count must be a non-negative integer",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setParticleCount:v error:e];}]; }
|
||||
- (void)colorCommand:(double)t red:(double)red green:(double)green blue:(double)blue alpha:(double)alpha resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j action:(MLNParticleFieldRuntimeCommandResult (^)(MLNParticleFieldRuntime *, float, float, float, float, NSError **))a { if (!isfinite(red)||!isfinite(green)||!isfinite(blue)||!isfinite(alpha)||red<0||red>1||green<0||green>1||blue<0||blue>1||alpha<0||alpha>1) { j(@"INVALID_ARGUMENT", @"RGBA must be finite values in [0,1]",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return a(q,red,green,blue,alpha,e);}]; }
|
||||
- (void)setParticleColor:(double)t red:(double)red green:(double)green blue:(double)blue alpha:(double)alpha resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self colorCommand:t red:red green:green blue:blue alpha:alpha resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float a,float b,float c,float d,NSError **e){return [q setParticleColorRed:a green:b blue:c alpha:d error:e];}]; }
|
||||
- (void)setTrailColor:(double)t red:(double)red green:(double)green blue:(double)blue alpha:(double)alpha resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self colorCommand:t red:red green:green blue:blue alpha:alpha resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float a,float b,float c,float d,NSError **e){return [q setTrailColorRed:a green:b blue:c alpha:d error:e];}]; }
|
||||
- (void)setWeatherColorStops:(double)t stops:(NSArray *)stops resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { NSMutableArray *native = NSMutableArray.array; float previous = -INFINITY; for (id entry in stops) { if (![entry isKindOfClass:NSDictionary.class]) { j(@"INVALID_ARGUMENT", @"color stop must be an object",nil); return; } NSDictionary *d = entry; NSNumber *speed=d[@"speedMps"],*red=d[@"red"],*green=d[@"green"],*blue=d[@"blue"],*alpha=d[@"alpha"]; if (!speed||!red||!green||!blue||!alpha||!isfinite(speed.floatValue)||speed.floatValue<=previous||!isfinite(red.floatValue)||!isfinite(green.floatValue)||!isfinite(blue.floatValue)||!isfinite(alpha.floatValue)||red.floatValue<0||red.floatValue>1||green.floatValue<0||green.floatValue>1||blue.floatValue<0||blue.floatValue>1||alpha.floatValue<0||alpha.floatValue>1) { j(@"INVALID_ARGUMENT", @"stops require finite, strictly increasing speedMps and finite RGBA in [0,1]",nil); return; } previous=speed.floatValue; [native addObject:[[MLNParticleWeatherColorStop alloc] initWithValue:previous red:red.floatValue green:green.floatValue blue:blue.floatValue alpha:alpha.floatValue]]; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setWeatherColorStops:native error:e];}]; }
|
||||
|
||||
@end
|
||||
@@ -9,6 +9,8 @@
|
||||
"files": [
|
||||
"src",
|
||||
"android",
|
||||
"ios",
|
||||
"MapLibreParticleRN.podspec",
|
||||
"app.plugin.js",
|
||||
"react-native.config.js",
|
||||
"README.md",
|
||||
@@ -28,6 +30,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@maplibre/maplibre-react-native": "11.3.6",
|
||||
"expo": ">=54.0.0",
|
||||
"react": ">=19.1.0",
|
||||
"react-native": ">=0.80.0"
|
||||
},
|
||||
@@ -42,6 +45,11 @@
|
||||
"jsSrcsDir": "src",
|
||||
"android": {
|
||||
"javaPackageName": "org.navsea.maplibre.particle.weather"
|
||||
},
|
||||
"ios": {
|
||||
"modulesProvider": {
|
||||
"WeatherRuntimeBridge": "WeatherRuntimeBridge"
|
||||
}
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
module.exports = { dependency: { platforms: { android: { sourceDir: "./android" } } } };
|
||||
module.exports = {
|
||||
dependency: {
|
||||
platforms: {
|
||||
android: { sourceDir: "./android" },
|
||||
ios: { podspecPath: "./MapLibreParticleRN.podspec" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ function encodedContractJson(contract: ResolvedFieldProfileContractInput): strin
|
||||
|
||||
function nativeBridge() {
|
||||
const bridge = getNativeFieldRuntimeBridge();
|
||||
if (bridge == null) throw new Error("@navsea/maplibre-particle-rn FieldRuntime requires the Android WeatherRuntimeBridge native module");
|
||||
if (bridge == null) throw new Error("@navsea/maplibre-particle-rn FieldRuntime requires the WeatherRuntimeBridge native module");
|
||||
return bridge;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
|
||||
模块和 product 始终为 `MapLibre`。MLRN 11.3.6 与后续 Particle Bridge 必须通过同一份自定义 MapLibre binary 消费,不能再链接第二个同功能但不同模块名的 framework。
|
||||
|
||||
Particle iOS 发行固定使用 Metal renderer。必须在 macOS 对目标 commit 执行:
|
||||
|
||||
```sh
|
||||
bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic
|
||||
```
|
||||
|
||||
裸命令 `bazel build //platform/ios:MapLibre.dynamic` 会采用默认 OpenGL renderer;即使成功生成 XCFramework,也不得作为 Particle iOS 发行基线。发行前还应核验 device binary 含 `MLNMapView`、Metal/`MTLDevice` 与 `ParticleGPU` 标记,且无 OpenGL marker。
|
||||
|
||||
发布时:
|
||||
|
||||
1. 在 macOS 上从对应 Git commit 成功生成 `MapLibre.dynamic.xcframework`;
|
||||
@@ -11,4 +19,4 @@
|
||||
3. 用真实 URL、checksum、commit 和构建记录填充 `release-manifest.json`;
|
||||
4. 将相同 URL/checksum 代入 `Package.swift.template`,再在独立 distribution repository 发布。
|
||||
|
||||
外部 binary distribution 版本独立于 `platform/ios/VERSION`;首个建议坐标为 `6.28.0-particle.1`。本 Step 尚未取得可发布 XCFramework zip,因此 manifest 保持 `NOT_BUILT`,不提供伪造 hash。
|
||||
外部 binary distribution 版本独立于 `platform/ios/VERSION`;首个建议坐标为 `6.28.0-particle.1`。Metal XCFramework 已在本地构建和 Pleiades iPad mini 5 RN Test Host signed Release 路径验证;artifact 尚未发布远端,manifest 的 URL 仍为 `LOCAL_BUILD`,不得伪造下载地址。
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"artifact": {
|
||||
"fileName": "MapLibre.dynamic.xcframework.zip",
|
||||
"url": "LOCAL_BUILD",
|
||||
"sizeBytes": 24639200,
|
||||
"sha256": "64faa174c5c0dc0a2da3e289f0beaf2dedf7298665e3345f39836729b668e239"
|
||||
"sizeBytes": 24943196,
|
||||
"sha256": "ae88975fc20f4d9961b95079638d0e9c0d8e61d4394d5c0a8ffcc48d173a827f"
|
||||
},
|
||||
"sourceCommit": "136ed726cf749c24b3673c046581a38ef48be390",
|
||||
"notes": "STEP-082 local Bazel XCFramework build has been validated. The artifact has not been published; no remote URL is available."
|
||||
"sourceCommit": "bdd7f40f9029031816c9bf4db6e1cd6c7c69b7bd",
|
||||
"notes": "Metal renderer artifact built with bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic. Validated by the Pleiades iPad mini 5 RN Test Host signed Release path; user confirmed particle rendering recovered. The artifact has not been published; no remote URL is available."
|
||||
}
|
||||
|
||||
13
粒子开发记录.md
13
粒子开发记录.md
@@ -843,3 +843,16 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2`
|
||||
- 真实本地产物 `bazel-bin/platform/ios/MapLibre.dynamic.xcframework.zip`:`24639200` bytes,SHA-256=`64faa174c5c0dc0a2da3e289f0beaf2dedf7298665e3345f39836729b668e239`。device `ios-arm64` 和 simulator `ios-arm64_x86_64-simulator` 都有 public `Headers/MLNParticleFieldRuntime.h`,两份 `Mapbox.h` 均导入该 header。
|
||||
- 两 slice binary 都有 `_OBJC_CLASS_$_MLNParticleFieldRuntime` 和 `_OBJC_CLASS_$_MLNParticleFieldRuntimeConfiguration`;`MLNMapView_Private.h` public export count=0。device 为 arm64,simulator 同时含 x86_64 与 arm64。
|
||||
- iOS release manifest 现为 `VALIDATED`,distributionVersion 仍为 `6.28.0-particle.1`、module 为 `MapLibre`;artifact 尚未发布,URL 明确为 `LOCAL_BUILD`。STEP-082 CLOSED / PASS;Git closeout commit/push 将由宿主在本轮文档生成后完成,最终 hash 以宿主回填/最终回复为准。
|
||||
|
||||
### 2026-09-18 STEP-083 iOS RN TurboModule / Fabric bridge 实现批次 2
|
||||
|
||||
- package 新增 RN 0.85 codegen `WeatherRuntimeBridge` iOS ObjC++ module、podspec、autolinking/provider;公共 TS API 未分叉,仅把缺 module 文案改为平台中性。每条命令由当前 Fabric/UIManager injected registry UI block 重新验证 MLRN component/content view,再使用 public `MLNMapView` facade;不缓存 map/runtime,也无第二 registry。
|
||||
- V3、V4、bindings、forecast 与全部视觉命令映射现有 iOS facade;本地 payload guard 限 file:///absolute readable regular file、1..64 MiB 和读取前后长度一致。pod 只依赖 `MapLibreReactNative`,复用 MLRN `$MLRN_SPM_SPEC` 的唯一 `MapLibre` product。
|
||||
- podspec syntax、package dry-run tarball、provider config、34/34 method parity 和 `git diff --check` PASS。离线 npm cache 缺 package,故 TypeScript/codegen/Pod/Xcode/runtime 未执行;STEP-082 的 `LOCAL_BUILD` XCFramework zip 也尚非 MLRN 可消费的 custom SwiftPM URL。状态 `IMPLEMENTED / WAITING_IOS_VALIDATION`,由 RDC 在真实 single-binary consumer 环境继续验证;未 commit/push,既有 dirty 未触碰。
|
||||
|
||||
### 2026-09-18 STEP-083 iOS Metal artifact 与 RN 真机验证纠正
|
||||
|
||||
- 已确认裸 `bazel build //platform/ios:MapLibre.dynamic` 默认走 OpenGL:旧 XCFramework 虽可显示色场,但 RN Test Host 不渲染粒子,旧 `24639200` bytes / `64faa174c5c0dc0a2da3e289f0beaf2dedf7298665e3345f39836729b668e239` artifact 作废,不再作为 iOS Particle 发行基线。
|
||||
- 发行固定为 `bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic`。新 Metal zip 为 `24943196` bytes,SHA-256=`ae88975fc20f4d9961b95079638d0e9c0d8e61d4394d5c0a8ffcc48d173a827f`,source commit=`bdd7f40f9029031816c9bf4db6e1cd6c7c69b7bd`;device binary 含 Metal/`MTLDevice`/`ParticleGPU` 且无 OpenGL marker,远端 URL 仍为 `LOCAL_BUILD`。
|
||||
- RDC 已实际完成 npm ci、typecheck、RN codegen、Expo prebuild、Pod integration、device arm64 build、signed Release build/签名/安装;MLRN/App/Particle 使用 single MapLibre SPM product。Pleiades iPad mini 5 上一次 V4 Wind load 观察到 clearAll、bindings、T0/T1 accepted 与 midpoint seek,用户确认粒子恢复。
|
||||
- 当前状态 `IMPLEMENTED / IOS_CORE_PATH_PASS / WAITING_FINAL_CONTROL_REGRESSION`:PLAY/PAUSE、clear selected/all、detach/re-attach 尚未逐按钮实测;不将单次 load 证据扩大为完整控制面验收。未 commit/push。
|
||||
|
||||
Reference in New Issue
Block a user