feat: connect QBIN V4 renderer and release particle.5
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-15 14:40:17 +08:00
parent 3896c461fa
commit f23f4143b9
22 changed files with 1011 additions and 248 deletions

View File

@@ -1,6 +1,21 @@
# 变更记录
## 13.4.1-particle.3(当前 Android QBIN V4 基线)
## 13.4.1-particle.5(当前 Android QBIN V4 Renderer 基线)
- 新增 QBIN V4 `BindingProjection` → renderer-facing canonical snapshot materialization,并由 `RasterParticle` / `WeatherColor` Renderer 正式消费。
- 保持 V3/V4 隔离:V4 不回写旧 `field` / `nextField` / `timeline`,仅通过独立 `v4RendererState` 进入渲染路径。
- V4 midpoint / playback 仅改变 MIX 时保持 projection content identity,不要求重复上传相同 T0/T1 GPU field。
- materializer 对 Int16 / UInt16 / Float32 使用显式 little-endian 解码,并支持冻结 QBIN V2 `DIRECTION_TO` UInt16 方向角转向量。
- 修复 `13.4.1-particle.4` 在 Android Test Host + frozen Golden QBIN 条件下控制面 ACCEPTED、但 Renderer 未获得有效 V4 field window 而无可见粒子的链路缺口。
- RN package 与 Android Native artifact 统一升级到 `13.4.1-particle.5`。
## 13.4.1-particle.4(当前 Android QBIN V4 基线)
- RN 公共入口正式导出 QBIN V4 Consumer 类型:Encoded Runtime Metadata、Resolved Field Profile Contract、Color/Particle Binding 与 Encoded Submit Result。
- UTF-8 长度校验改为包内纯 TypeScript 实现,不再要求 Consumer TypeScript 环境提供 DOM `TextEncoder` 类型。
- RN 与 Android Native 发布坐标同步升级到 `13.4.1-particle.4`;QBIN V4 API、Native ABI 与渲染语义不变。
## 13.4.1-particle.3
- RN package 与 Android Native artifact 已统一为 `13.4.1-particle.3`。
- 新增 QBIN V4 encoded ingress:`submitEncodedField`、Color/Particle Binding、Forecast Time/Playback、encoded clear。

View File

@@ -1,6 +1,6 @@
# @navsea/maplibre-particle-rn
当前版本:`13.4.1-particle.3`。Android Native 必须配套 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.3`。本包是 React Native 的薄控制面,不负责 Map 组件、provider、HTTP、manifest、下载、缓存或 NavSea 业务策略。
当前版本:`13.4.1-particle.5`。Android Native 必须配套 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.5`。本包是 React Native 的薄控制面,不负责 Map 组件、provider、HTTP、manifest、下载、缓存或 NavSea 业务策略。
## 当前推荐主链:QBIN V4
@@ -31,7 +31,7 @@ Map 与 Camera 直接从 `@maplibre/maplibre-react-native@11.3.6` 使用。本
RN tgz:
```text
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.3.tgz
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.5.tgz
```
Native Maven repository:
@@ -43,7 +43,7 @@ https://pbf.jpcn.ccwu.cc/maven
Native coordinate:
```text
cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.3
cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.5
```
Expo plugin 与 `android/build.gradle` 必须指向同一 `.3`。正常 Consumer 不复制 AAR、不依赖 MapLibreParticleLab 源码路径,也不以 Maven Local 作为公网验证主路径。完整 Linux 发布与 Win11 验证流程见 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`。
Expo plugin 与 `android/build.gradle` 必须指向同一 `.4`。正常 Consumer 不复制 AAR、不依赖 MapLibreParticleLab 源码路径,也不以 Maven Local 作为公网验证主路径。完整 Linux 发布与 Win11 验证流程见 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`。

View File

@@ -14,7 +14,7 @@ android {
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.3"
implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.5"
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) {
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.3")\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.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;
});
};

View File

@@ -1,6 +1,6 @@
{
"name": "@navsea/maplibre-particle-rn",
"version": "13.4.1-particle.3",
"version": "13.4.1-particle.5",
"description": "NavSea MapLibre Particle React Native pure Field Renderer API.",
"license": "BSD-2-Clause",
"main": "src/index.ts",

View File

@@ -1,9 +1,19 @@
const MAX_ENCODED_ID_UTF8_BYTES = 64;
/** UTF-8 byte length without requiring DOM/TextEncoder types from the consumer project. */
export function utf8ByteLength(value: string): number {
let bytes = 0;
for (let index = 0; index < value.length; index += 1) {
const codePoint = value.codePointAt(index) ?? 0;
if (codePoint > 0xffff) index += 1;
bytes += codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4;
}
return bytes;
}
/** Transport guard only; Core retains its own field/profile structural rules. */
export function hasValidEncodedIdUtf8Length(value: unknown): value is string {
return typeof value === "string" && (() => {
const bytes = new TextEncoder().encode(value).length;
return bytes >= 1 && bytes <= MAX_ENCODED_ID_UTF8_BYTES;
})();
if (typeof value !== "string") return false;
const bytes = utf8ByteLength(value);
return bytes >= 1 && bytes <= MAX_ENCODED_ID_UTF8_BYTES;
}

View File

@@ -1,12 +1,12 @@
import { getNativeFieldRuntimeBridge } from "./NativeWeatherRuntimeBridge";
import { hasValidEncodedIdUtf8Length } from "./EncodedFieldGuards";
import { hasValidEncodedIdUtf8Length, utf8ByteLength } from "./EncodedFieldGuards";
import type { ColorChannel, CommandResult, EncodedColorBindingInput, EncodedFieldRuntimeMetadata, EncodedFieldSubmitResult, EncodedParticleBindingInput, FieldColorStop, FieldPayloadRefs, FieldSubmitResult, FieldValueSource, PlaybackState, ResolvedFieldProfileContractInput, RuntimeSnapshot, TargetFps } from "./types";
const MAX_SAFE = Number.MAX_SAFE_INTEGER;
const MAX_CONTRACT_BYTES = 256 * 1024;
const encodedString = (value: string, name: string) => { if (!hasValidEncodedIdUtf8Length(value)) throw new Error(`${name} must be a non-empty UTF-8 string of at most 64 bytes`); };
const uint = (value: number, name: string, maximum = MAX_SAFE, positive = false) => { if (!Number.isSafeInteger(value) || value < (positive ? 1 : 0) || value > maximum) throw new Error(`${name} must be a ${positive ? "positive" : "non-negative"} safe integer`); };
function encodedContractJson(contract: ResolvedFieldProfileContractInput): string { encodedString(contract.profileID, "profileID"); uint(contract.contractSchemaVersion, "contractSchemaVersion", 0xffff, true); if (contract.contractSchemaVersion !== 1) throw new Error("unsupported contractSchemaVersion"); uint(contract.profileVersion, "profileVersion", 0xffffffff, true); if (!Array.isArray(contract.requiredPlanes) || contract.requiredPlanes.length === 0 || contract.requiredPlanes.length > 64) throw new Error("requiredPlanes must contain 1..64 entries"); for (const p of contract.requiredPlanes) { uint(p.semanticID, "semanticID", 0xffff, true); uint(p.unitID, "unitID", 0xffff); uint(p.groupID, "groupID", 0xffff); if (!p.requiredFlag || !["SCALAR", "VELOCITY_COMPONENT", "DIRECTION_ANGLE", "VALIDITY"].includes(p.valueClass) || !["NONE", "U_EAST", "V_NORTH", "W_UP"].includes(p.component)) throw new Error("invalid required plane contract"); } const json = JSON.stringify(contract); if (new TextEncoder().encode(json).length > MAX_CONTRACT_BYTES) throw new Error("contractJson exceeds 256 KiB"); return json; }
function encodedContractJson(contract: ResolvedFieldProfileContractInput): string { encodedString(contract.profileID, "profileID"); uint(contract.contractSchemaVersion, "contractSchemaVersion", 0xffff, true); if (contract.contractSchemaVersion !== 1) throw new Error("unsupported contractSchemaVersion"); uint(contract.profileVersion, "profileVersion", 0xffffffff, true); if (!Array.isArray(contract.requiredPlanes) || contract.requiredPlanes.length === 0 || contract.requiredPlanes.length > 64) throw new Error("requiredPlanes must contain 1..64 entries"); for (const p of contract.requiredPlanes) { uint(p.semanticID, "semanticID", 0xffff, true); uint(p.unitID, "unitID", 0xffff); uint(p.groupID, "groupID", 0xffff); if (!p.requiredFlag || !["SCALAR", "VELOCITY_COMPONENT", "DIRECTION_ANGLE", "VALIDITY"].includes(p.valueClass) || !["NONE", "U_EAST", "V_NORTH", "W_UP"].includes(p.component)) throw new Error("invalid required plane contract"); } const json = JSON.stringify(contract); if (utf8ByteLength(json) > MAX_CONTRACT_BYTES) throw new Error("contractJson exceeds 256 KiB"); return json; }
function nativeBridge() {
const bridge = getNativeFieldRuntimeBridge();

View File

@@ -2,11 +2,17 @@ export { FieldRuntime } from "./FieldRuntime";
export type {
ColorChannel,
CommandResult,
EncodedColorBindingInput,
EncodedFieldRuntimeMetadata,
EncodedFieldSubmitResult,
EncodedParticleBindingInput,
FieldColorStop,
FieldPayloadRefs,
FieldSubmitResult,
FieldValueSource,
PlaybackState,
RequiredPlaneContractInput,
ResolvedFieldProfileContractInput,
RuntimeSnapshot,
RuntimeState,
TargetFps,