Compare commits
6 Commits
maplibre-p
...
V10-完成
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f47469d2ab | ||
|
|
822663cd5d | ||
|
|
d5ce2006be | ||
|
|
73bebcf827 | ||
|
|
3f7ec99f85 | ||
|
|
a4cb727b16 |
@@ -2,7 +2,7 @@
|
||||
|
||||
## 状态
|
||||
|
||||
IMPLEMENTED / MAC VALIDATION IN PROGRESS
|
||||
CLOSED / PASS
|
||||
|
||||
## 已完成
|
||||
|
||||
@@ -20,12 +20,15 @@ IMPLEMENTED / MAC VALIDATION IN PROGRESS
|
||||
|
||||
真实 8 帧 benchmark:16×16 数值 payload 为 2,470,419 bytes;32×32 为 2,514,288 bytes,32×32 增加约 1.7758%。32×32 的 decode 总时间仅减少约 2.67 ms / 8 帧,因此最终冻结 16×16。详见 `docs/30_设计/MapLibreParticle_QBIN_V3_Decoder与BlockGeometry实施决策_v1.md`。
|
||||
|
||||
## 当前验证
|
||||
## 最终验证与发布
|
||||
|
||||
- `verify-qbin-v3-golden-fixtures.py`:PASS(8 files,block=16×16)。
|
||||
- QBIN V3 C++ 定向 tests:PASS 8 / FAIL 0。
|
||||
- iOS Metal `MapLibre.dynamic`:当前正在 Mac 专用 worktree 中做正式 Bazel 构建验证;此前仅因 worktree vendor submodule 未展开而在 analysis 阶段失败,已修复 submodule checkout。
|
||||
|
||||
## 下一步
|
||||
|
||||
完成 iOS Metal 正式构建结果确认后,执行 `git diff --check`、生成完成报告、提交并 push 到 `origin/particle/main`。Android Gradle / APK / ADB 不在本任务自动执行范围内。
|
||||
- Android Release 四 ABI:armeabi-v7a、arm64-v8a、x86、x86_64 全部构建通过。
|
||||
- iOS Metal `MapLibre.dynamic`:PASS;device-arm64 XCFramework 已生成并发布。
|
||||
- RN tgz:已生成并发布。
|
||||
- Android Maven / RN tgz / iOS XCFramework 公网 readback:PASS。
|
||||
- SwiftPM `6.28.0-particle.9`:已发布。
|
||||
- Release tag:`maplibre-particle/rn/13.4.1-particle.9`。
|
||||
- Release Final HEAD:`63047540cba703c8efeae0df71e759e165315f85`。
|
||||
- Android APK / ADB 仍按项目规则由用户手工执行,不属于本次自动发布门槛。
|
||||
|
||||
@@ -29,9 +29,10 @@ C++ benchmark 使用四类真实 Golden 数据共 8 帧:
|
||||
- QBIN V3 Golden Fixtures 校验:PASS,8 files,block=16×16。
|
||||
- C++ 定向测试:PASS 8 / FAIL 0。
|
||||
- Current / Rain / Wave / Wind V3 解码结果与对应 V2 canonical raw 做逐 plane / validity byte-for-byte 对比:PASS 8 / FAIL 0。
|
||||
- `git diff --check`:将在提交前执行。
|
||||
- iOS Metal `MapLibre.dynamic` 正式 Bazel 构建:__IOS_BUILD_RESULT__。
|
||||
- Android Gradle / APK / ADB:按项目规则未执行,由用户手工完成。
|
||||
- `git diff --check`:PASS。
|
||||
- iOS Metal `MapLibre.dynamic` 正式 Bazel 构建:PASS;生成 device-arm64 `MapLibre.dynamic.xcframework.zip`。
|
||||
- Android 统一 Release 构建:PASS,包含 armeabi-v7a、arm64-v8a、x86、x86_64 四 ABI。
|
||||
- Android APK / ADB:按项目规则未执行,由用户手工完成。
|
||||
|
||||
## 5. 文档与数据
|
||||
|
||||
@@ -42,8 +43,15 @@ C++ benchmark 使用四类真实 Golden 数据共 8 帧:
|
||||
|
||||
## 6. Git 收尾
|
||||
|
||||
- 实现提交:__IMPLEMENTATION_COMMIT__
|
||||
- 提交信息:__IMPLEMENTATION_MESSAGE__
|
||||
- Push:__PUSH_RESULT__
|
||||
- 实现提交:`0e8c87fe49fb45c23a88d8f6cbf79dbc70df393c`
|
||||
- 实现提交信息:`feat: add QBIN V3 decoder for particle.9`
|
||||
- 发布 finalize 提交:`63047540cba703c8efeae0df71e759e165315f85`
|
||||
- 发布 finalize 提交信息:`release: finalize unified particle.9`
|
||||
- Push:PASS
|
||||
- 远端分支:`origin/particle/main`
|
||||
- Final HEAD:__FINAL_HEAD__
|
||||
- Release Final HEAD / Tag HEAD:`63047540cba703c8efeae0df71e759e165315f85`
|
||||
- Release Tag:`maplibre-particle/rn/13.4.1-particle.9`
|
||||
- Android AAR:`6114067c45685e2d20b3dd5115c480a39341177ac8608f86ec431562f649e395`
|
||||
- RN tgz:`98dc94e88dc69c06c9d99cc8ece3547f0489ba650c4a3d71814eab45e8720dcb`
|
||||
- iOS XCFramework zip:`fbda91e09d53e3cd1362590bfc0410b6fb5f9fef4cf99796d4ab7b4b84c93562`
|
||||
- SwiftPM:`6.28.0-particle.9`,public readback PASS。
|
||||
|
||||
@@ -5,16 +5,23 @@
|
||||
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
|
||||
namespace mbgl::particle {
|
||||
|
||||
// Renderer-neutral canonical snapshots. QBIN stays confined to ingress and
|
||||
// field Core; renderers receive only the existing canonical field shape.
|
||||
enum class MaterializedColorValueSource : uint8_t {
|
||||
Scalar,
|
||||
VectorMagnitude,
|
||||
};
|
||||
|
||||
struct MaterializedColorProjection {
|
||||
std::shared_ptr<const ParticleFieldSnapshot> t0;
|
||||
std::shared_ptr<const ParticleFieldSnapshot> t1;
|
||||
ColorProjectionContentIdentity contentIdentity;
|
||||
FieldSampleState sampleState;
|
||||
MaterializedColorValueSource valueSource = MaterializedColorValueSource::Scalar;
|
||||
};
|
||||
|
||||
struct MaterializedParticleProjection {
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
# Expo/RN prebuild output is generated locally and must not enter this source host.
|
||||
/ios/
|
||||
|
||||
/.expo/
|
||||
|
||||
@@ -1,192 +1,453 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, findNodeHandle } from "react-native";
|
||||
import {
|
||||
Button,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
findNodeHandle,
|
||||
} from "react-native";
|
||||
import { Camera, Map } from "@maplibre/maplibre-react-native";
|
||||
import { FieldRuntime, type FieldColorStop, type RuntimeSnapshot, type TargetFps } from "@navsea/maplibre-particle-rn";
|
||||
import { fieldDatasets, materializeV3Frame, type V3Dataset } from "./src/fieldDatasets";
|
||||
import { materializeV4Frame, qbinGoldenDatasets, type V4Dataset } from "./src/qbinGoldenFixtures";
|
||||
import { FieldRuntime, type RuntimeSnapshot } from "@navsea/maplibre-particle-rn";
|
||||
import {
|
||||
materializeV4Frame as materializeV2Frame,
|
||||
qbinGoldenDatasets,
|
||||
} from "./src/qbinGoldenFixtures";
|
||||
import {
|
||||
materializeV4Frame as materializeV3Frame,
|
||||
qbinV3GoldenDatasets,
|
||||
type V4Dataset,
|
||||
} from "./src/qbinV3GoldenFixtures";
|
||||
import {
|
||||
colorStopsFor,
|
||||
legendFor,
|
||||
type WeatherDatasetId,
|
||||
} from "./src/weatherPalettes";
|
||||
import {
|
||||
downloadOnlineQbin,
|
||||
type QbinFormat,
|
||||
} from "./src/onlineQbin";
|
||||
import { weatherTestStyle } from "./src/weatherStyle";
|
||||
const initialSnapshot: RuntimeSnapshot = {
|
||||
state: "WAITING_FOR_FIELDS",
|
||||
playbackState: "PAUSE",
|
||||
targetFps: "AUTO",
|
||||
disposed: false,
|
||||
};
|
||||
|
||||
const initialSnapshot: RuntimeSnapshot = { state: "WAITING_FOR_FIELDS", playbackState: "PAUSE", targetFps: "AUTO", disposed: false };
|
||||
const initialVisual = { count: "4096", width: "1.5", length: "0.012", opacity: "0.9", trailOpacity: "0.8", trailFade: "0.94", weatherOpacity: "0.62" };
|
||||
const frozenStops: FieldColorStop[] = [
|
||||
{ value: 0, red: 0.08, green: 0.2, blue: 0.8, alpha: 0.72 }, { value: 8, red: 0, green: 0.85, blue: 0.9, alpha: 0.72 },
|
||||
{ value: 16, red: 1, green: 0.85, blue: 0.05, alpha: 0.72 }, { value: 30, red: 0.9, green: 0.08, blue: 0.08, alpha: 0.72 },
|
||||
];
|
||||
const initialVisual = {
|
||||
count: "4096",
|
||||
width: "1.8",
|
||||
length: "0.028",
|
||||
opacity: "0.85",
|
||||
trailOpacity: "0.72",
|
||||
trailFade: "0.975",
|
||||
weatherOpacity: "1.0",
|
||||
};
|
||||
|
||||
function datasetFor(format: QbinFormat, id: WeatherDatasetId): V4Dataset {
|
||||
const datasets = format === "V3" ? qbinV3GoldenDatasets : qbinGoldenDatasets;
|
||||
const found = datasets.find((item) => item.id === id);
|
||||
if (!found) throw new Error("Dataset missing: " + format + " / " + id);
|
||||
return found as V4Dataset;
|
||||
}
|
||||
|
||||
function valueSourceFor(id: WeatherDatasetId): "SCALAR" | "VECTOR_MAGNITUDE" {
|
||||
return id === "wind" || id === "current" ? "VECTOR_MAGNITUDE" : "SCALAR";
|
||||
}
|
||||
export default function App() {
|
||||
const [reactTag, setReactTag] = useState<number | null>(null);
|
||||
const [snapshot, setSnapshot] = useState<RuntimeSnapshot>(initialSnapshot);
|
||||
const [result, setResult] = useState("Style is loading");
|
||||
const [basemap, setBasemap] = useState("LOADING");
|
||||
const [frames, setFrames] = useState<Record<string, string>>({ T0: "PENDING", T1: "PENDING" });
|
||||
const [dataset, setDataset] = useState<V3Dataset>(fieldDatasets[0]);
|
||||
const [transport, setTransport] = useState("PENDING: V2 FILE_REF / FLOAT32_LE");
|
||||
const [v4Dataset, setV4Dataset] = useState<V4Dataset>(qbinGoldenDatasets[0]);
|
||||
const [v4Frames, setV4Frames] = useState<Record<"T0" | "T1", string>>({ T0: "PENDING", T1: "PENDING" });
|
||||
const [v4Bindings, setV4Bindings] = useState("PENDING");
|
||||
const [v4Transport, setV4Transport] = useState("PENDING: raw QBIN bundled asset → local file:// URI");
|
||||
const [finalControlRunning, setFinalControlRunning] = useState(false);
|
||||
const [finalControlSummary, setFinalControlSummary] = useState("FINAL CONTROL REGRESSION: not run");
|
||||
const finalControlRunRef = useRef(false);
|
||||
const [mapStatus, setMapStatus] = useState("LOADING");
|
||||
const [weatherStatus, setWeatherStatus] = useState("WAITING");
|
||||
const [result, setResult] = useState("Waiting for map/style");
|
||||
const [activeSource, setActiveSource] = useState("none");
|
||||
const [format, setFormat] = useState<QbinFormat>("V3");
|
||||
const [datasetId, setDatasetId] = useState<WeatherDatasetId>("wind");
|
||||
const [weatherVisible, setWeatherVisible] = useState(true);
|
||||
const [particleVisible, setParticleVisible] = useState(true);
|
||||
const [controlsCollapsed, setControlsCollapsed] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [visual, setVisual] = useState(initialVisual);
|
||||
const autoLoadTagRef = useRef<number | null>(null);
|
||||
|
||||
const selectedDataset = useMemo(
|
||||
() => datasetFor(format, datasetId),
|
||||
[format, datasetId],
|
||||
);
|
||||
|
||||
const refresh = useCallback(async (tag: number) => {
|
||||
setSnapshot(await FieldRuntime.getState(tag));
|
||||
}, []);
|
||||
const applyVisualToTag = useCallback(async (tag: number) => {
|
||||
const results = await Promise.all([
|
||||
FieldRuntime.setParticleCount(tag, Number(visual.count)),
|
||||
FieldRuntime.setParticleLineWidth(tag, Number(visual.width)),
|
||||
FieldRuntime.setParticleLength(tag, Number(visual.length)),
|
||||
FieldRuntime.setParticleOpacity(tag, Number(visual.opacity)),
|
||||
FieldRuntime.setTrailOpacity(tag, Number(visual.trailOpacity)),
|
||||
FieldRuntime.setTrailFade(tag, Number(visual.trailFade)),
|
||||
FieldRuntime.setFieldOpacity(tag, Number(visual.weatherOpacity)),
|
||||
]);
|
||||
if (results.some((value) => value !== "ACCEPTED")) {
|
||||
throw new Error(results.join(", "));
|
||||
}
|
||||
}, [visual]);
|
||||
|
||||
const prepareDataset = useCallback(async (
|
||||
tag: number,
|
||||
dataset: V4Dataset,
|
||||
) => {
|
||||
const clearEncoded = await FieldRuntime.clearAllEncodedFields(tag);
|
||||
if (clearEncoded !== "ACCEPTED") throw new Error("clearAllEncodedFields rejected");
|
||||
|
||||
const clearCanonical = await FieldRuntime.clearFields(tag);
|
||||
if (clearCanonical !== "ACCEPTED") throw new Error("clearFields rejected");
|
||||
|
||||
const valueSource = await FieldRuntime.setFieldValueSource(
|
||||
tag,
|
||||
valueSourceFor(dataset.id),
|
||||
);
|
||||
if (valueSource !== "ACCEPTED") throw new Error("setFieldValueSource rejected");
|
||||
const stops = await FieldRuntime.setFieldColorStops(
|
||||
tag,
|
||||
colorStopsFor(dataset.id),
|
||||
);
|
||||
if (stops !== "ACCEPTED") throw new Error("setFieldColorStops rejected");
|
||||
|
||||
const color = await FieldRuntime.setEncodedColorBinding(tag, dataset.colorBinding);
|
||||
if (color !== "ACCEPTED") throw new Error("setEncodedColorBinding rejected");
|
||||
|
||||
if (dataset.particleBinding != null) {
|
||||
const particle = await FieldRuntime.setEncodedParticleBinding(
|
||||
tag,
|
||||
dataset.particleBinding,
|
||||
);
|
||||
if (particle !== "ACCEPTED") {
|
||||
throw new Error("setEncodedParticleBinding rejected");
|
||||
}
|
||||
}
|
||||
|
||||
const hasParticle = dataset.particleBinding != null;
|
||||
await FieldRuntime.setFieldVisible(tag, true);
|
||||
await FieldRuntime.setParticleVisible(tag, hasParticle);
|
||||
await applyVisualToTag(tag);
|
||||
setWeatherVisible(true);
|
||||
setParticleVisible(hasParticle);
|
||||
}, [applyVisualToTag]);
|
||||
const finishLoad = useCallback(async (
|
||||
tag: number,
|
||||
dataset: V4Dataset,
|
||||
midpointMs: number,
|
||||
sourceLabel: string,
|
||||
) => {
|
||||
const midpoint = await FieldRuntime.setEncodedForecastTime(tag, midpointMs);
|
||||
if (midpoint !== "ACCEPTED") throw new Error("setEncodedForecastTime rejected");
|
||||
|
||||
await FieldRuntime.setFieldVisible(tag, true);
|
||||
await FieldRuntime.setParticleVisible(tag, dataset.particleBinding != null);
|
||||
await refresh(tag);
|
||||
|
||||
setWeatherStatus("READY");
|
||||
setActiveSource(sourceLabel);
|
||||
setResult(
|
||||
dataset.id + " READY / " + valueSourceFor(dataset.id) +
|
||||
" / " + legendFor(dataset.id),
|
||||
);
|
||||
}, [refresh]);
|
||||
|
||||
const loadBundled = useCallback(async (
|
||||
tag: number,
|
||||
loadFormat: QbinFormat,
|
||||
id: WeatherDatasetId,
|
||||
) => {
|
||||
const dataset = datasetFor(loadFormat, id);
|
||||
try {
|
||||
setLoading(true);
|
||||
setWeatherStatus("LOADING");
|
||||
setResult("Loading bundled " + loadFormat + " " + id);
|
||||
await prepareDataset(tag, dataset);
|
||||
|
||||
const materialize = loadFormat === "V3" ? materializeV3Frame : materializeV2Frame;
|
||||
const frames = await Promise.all(
|
||||
dataset.frames.map((frame) => materialize(frame as never)),
|
||||
);
|
||||
|
||||
for (const frame of frames) {
|
||||
const submit = await FieldRuntime.submitEncodedField(
|
||||
tag,
|
||||
dataset.fieldKey,
|
||||
frame.metadata,
|
||||
dataset.profileContract,
|
||||
frame.qbinFileUri,
|
||||
);
|
||||
if (submit !== "ACCEPTED_INSERTED") {
|
||||
throw new Error(frame.label + " submit=" + submit);
|
||||
}
|
||||
}
|
||||
|
||||
await finishLoad(
|
||||
tag,
|
||||
dataset,
|
||||
dataset.midpointMs,
|
||||
"BUNDLED " + loadFormat + " / " + id,
|
||||
);
|
||||
} catch (error) {
|
||||
setWeatherStatus("ERROR");
|
||||
setResult("Bundled load error: " + String(error));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [finishLoad, prepareDataset]);
|
||||
const loadOnline = useCallback(async (
|
||||
tag: number,
|
||||
loadFormat: QbinFormat,
|
||||
id: WeatherDatasetId,
|
||||
) => {
|
||||
const dataset = datasetFor(loadFormat, id);
|
||||
try {
|
||||
setLoading(true);
|
||||
setWeatherStatus("DOWNLOADING");
|
||||
setResult("Discovering online " + loadFormat + " " + id);
|
||||
await prepareDataset(tag, dataset);
|
||||
|
||||
const online = await downloadOnlineQbin(loadFormat, id);
|
||||
setResult(
|
||||
"Downloading " + online.datasetId + "/" + online.serverLayer +
|
||||
" ref=" + online.refTime,
|
||||
);
|
||||
|
||||
for (const frame of online.frames) {
|
||||
const submit = await FieldRuntime.submitEncodedField(
|
||||
tag,
|
||||
dataset.fieldKey,
|
||||
frame.metadata,
|
||||
dataset.profileContract,
|
||||
frame.qbinFileUri,
|
||||
);
|
||||
if (submit !== "ACCEPTED_INSERTED") {
|
||||
throw new Error(frame.label + " submit=" + submit);
|
||||
}
|
||||
}
|
||||
const times = online.frames
|
||||
.map((frame) => frame.targetTime)
|
||||
.join(" → ");
|
||||
await finishLoad(
|
||||
tag,
|
||||
dataset,
|
||||
online.midpointMs,
|
||||
"ONLINE " + loadFormat + " / " + online.datasetId +
|
||||
" / " + online.serverLayer + " / ref=" + online.refTime +
|
||||
" / " + times,
|
||||
);
|
||||
} catch (error) {
|
||||
setWeatherStatus("ERROR");
|
||||
setResult("Online load error: " + String(error));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [finishLoad, prepareDataset]);
|
||||
|
||||
const refresh = useCallback(async (tag: number) => setSnapshot(await FieldRuntime.getState(tag)), []);
|
||||
const attach = useCallback(async (tag: number) => {
|
||||
try { await FieldRuntime.attach(tag); await refresh(tag); setReactTag(tag); setResult("FieldRuntime attached; style ready"); }
|
||||
catch (error) { setResult(`style/attach error: ${String(error)}`); }
|
||||
try {
|
||||
await FieldRuntime.attach(tag);
|
||||
setReactTag(tag);
|
||||
await refresh(tag);
|
||||
setResult("FieldRuntime attached / particle.10");
|
||||
} catch (error) {
|
||||
setResult("attach error: " + String(error));
|
||||
}
|
||||
}, [refresh]);
|
||||
const onStyleReady = useCallback((event: unknown) => {
|
||||
const tag = findNodeHandle((event as { target?: unknown }).target as never);
|
||||
if (tag != null) void attach(tag);
|
||||
}, [attach]);
|
||||
useEffect(() => () => { if (reactTag != null) void FieldRuntime.detach(reactTag).catch(() => undefined); }, [reactTag]);
|
||||
|
||||
const command = useCallback(async (name: string, operation: (tag: number) => Promise<unknown>) => {
|
||||
if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready");
|
||||
try { const value = await operation(reactTag); await refresh(reactTag); setResult(`${name}: ${String(value)}`); }
|
||||
catch (error) { setResult(`${name}: ${String(error)}`); }
|
||||
}, [reactTag, refresh]);
|
||||
useEffect(() => {
|
||||
if (reactTag == null || autoLoadTagRef.current === reactTag) return;
|
||||
autoLoadTagRef.current = reactTag;
|
||||
void loadBundled(reactTag, "V3", "wind");
|
||||
}, [loadBundled, reactTag]);
|
||||
|
||||
const loadDataset = useCallback(async () => {
|
||||
if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready");
|
||||
const expected = ["ACCEPTED_PENDING_INITIAL_WINDOW", "INITIAL_WINDOW_READY"];
|
||||
useEffect(() => () => {
|
||||
if (reactTag != null) void FieldRuntime.detach(reactTag).catch(() => undefined);
|
||||
}, [reactTag]);
|
||||
|
||||
const applyVisual = useCallback(async () => {
|
||||
if (reactTag == null) return;
|
||||
try {
|
||||
setTransport("LOADING: V2 metadata + validity/scalar/vector local file:// URIs"); setFrames({ T0: "PENDING", T1: "PENDING" });
|
||||
if (await FieldRuntime.clearFields(reactTag) !== "ACCEPTED") throw new Error("clearFields was rejected");
|
||||
await FieldRuntime.setFieldValueSource(reactTag, dataset.fieldValueSource); await FieldRuntime.setParticleVisible(reactTag, dataset.particle); await FieldRuntime.setParticleAdvectionScale(reactTag, dataset.advectionScale);
|
||||
const localFrames = await Promise.all(dataset.frames.map(materializeV3Frame));
|
||||
for (let index = 0; index < localFrames.length; index += 1) {
|
||||
const frame = localFrames[index]; const submit = await FieldRuntime.submitField(reactTag, frame.metadataJson, frame.payloadRefs);
|
||||
setFrames((current) => ({ ...current, [frame.label]: submit }));
|
||||
if (submit !== expected[index]) throw new Error(`${frame.label} expected ${expected[index]}, got ${submit}`);
|
||||
}
|
||||
const native = await FieldRuntime.getState(reactTag); setSnapshot(native);
|
||||
if (native.state !== "READY") throw new Error(`Native WeatherRuntime expected READY, got ${native.state}`);
|
||||
setTransport("READY: V2 FILE_REF / FLOAT32_LE / shared validity"); setResult(`${dataset.title}: T0/T1 accepted; FieldRuntime READY`);
|
||||
} catch (error) { setTransport(`ERROR: ${String(error)}`); setResult(`LOAD ${dataset.id}: ${String(error)}`); }
|
||||
}, [reactTag, dataset]);
|
||||
|
||||
const prepareV4 = useCallback(async (tag: number) => {
|
||||
setV4Transport("LOADING: clear → binding → T0 → T1 → midpoint");
|
||||
setV4Frames({ T0: "PENDING", T1: "PENDING" });
|
||||
setV4Bindings("PENDING");
|
||||
const clear = await FieldRuntime.clearAllEncodedFields(tag);
|
||||
if (clear !== "ACCEPTED") throw new Error(`clearAllEncodedFields expected ACCEPTED, got ${clear}`);
|
||||
const color = await FieldRuntime.setEncodedColorBinding(tag, v4Dataset.colorBinding);
|
||||
if (color !== "ACCEPTED") throw new Error(`ColorBinding expected ACCEPTED, got ${color}`);
|
||||
const particle = v4Dataset.particleBinding == null ? "N/A" : await FieldRuntime.setEncodedParticleBinding(tag, v4Dataset.particleBinding);
|
||||
if (particle !== "N/A" && particle !== "ACCEPTED") throw new Error(`ParticleBinding expected ACCEPTED, got ${particle}`);
|
||||
setV4Bindings(`clearAll=${clear}; color=${color}; particle=${particle}`);
|
||||
const localFrames = await Promise.all(v4Dataset.frames.map(materializeV4Frame));
|
||||
const submitted: string[] = [];
|
||||
for (const frame of localFrames) {
|
||||
const submit = await FieldRuntime.submitEncodedField(tag, v4Dataset.fieldKey, frame.metadata, v4Dataset.profileContract, frame.qbinFileUri);
|
||||
submitted.push(`${frame.label}=${submit}`);
|
||||
setV4Frames((current) => ({ ...current, [frame.label]: submit }));
|
||||
if (submit !== "ACCEPTED_INSERTED") throw new Error(`${frame.label} expected ACCEPTED_INSERTED, got ${submit}`);
|
||||
}
|
||||
const midpoint = await FieldRuntime.setEncodedForecastTime(tag, v4Dataset.midpointMs);
|
||||
if (midpoint !== "ACCEPTED") throw new Error(`midpoint seek expected ACCEPTED, got ${midpoint}`);
|
||||
await refresh(tag);
|
||||
setV4Transport("READY: T0/T1 ACCEPTED_INSERTED; midpoint command ACCEPTED (mix=0.5 is not exposed)");
|
||||
return `clearAll=${clear}; color=${color}; particle=${particle}; ${submitted.join(", ")}; midpoint=${midpoint}`;
|
||||
}, [refresh, v4Dataset]);
|
||||
|
||||
const loadV4Dataset = useCallback(async () => {
|
||||
if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready");
|
||||
try {
|
||||
await prepareV4(reactTag);
|
||||
setResult(`${v4Dataset.title}: V4 raw QBIN load accepted`);
|
||||
await applyVisualToTag(reactTag);
|
||||
await refresh(reactTag);
|
||||
setResult("Visual parameters applied");
|
||||
} catch (error) {
|
||||
setV4Transport(`ERROR: ${String(error)}`);
|
||||
setResult(`LOAD V4 ${v4Dataset.id}: ${String(error)}`);
|
||||
setResult("Visual error: " + String(error));
|
||||
}
|
||||
}, [prepareV4, reactTag, v4Dataset]);
|
||||
}, [applyVisualToTag, reactTag, refresh]);
|
||||
|
||||
const runV4FinalControlRegression = useCallback(async () => {
|
||||
if (reactTag == null || !Number.isInteger(reactTag) || reactTag <= 0) return setResult("MAP_NOT_READY: wait for a valid reactTag/style ready");
|
||||
if (finalControlRunRef.current) return;
|
||||
const steps: string[] = [];
|
||||
const accepted = (step: string, value: string) => {
|
||||
if (value !== "ACCEPTED") throw new Error(`${step} expected ACCEPTED, got ${value}`);
|
||||
steps.push(`${step}=${value}`);
|
||||
};
|
||||
const prepare = async (attempt: number) => {
|
||||
try { steps.push(`prepare#${attempt} PASS (${await prepareV4(reactTag)})`); }
|
||||
catch (error) { throw new Error(`prepare#${attempt}: ${String(error)}`); }
|
||||
};
|
||||
finalControlRunRef.current = true;
|
||||
setFinalControlRunning(true);
|
||||
setFinalControlSummary("FINAL CONTROL REGRESSION: RUNNING");
|
||||
try {
|
||||
await prepare(1);
|
||||
accepted("PLAY", await FieldRuntime.setEncodedForecastPlaybackState(reactTag, "PLAY"));
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 500));
|
||||
steps.push("PLAY wait≈500ms=PASS");
|
||||
accepted("PAUSE", await FieldRuntime.setEncodedForecastPlaybackState(reactTag, "PAUSE"));
|
||||
accepted("clear selected", await FieldRuntime.clearEncodedField(reactTag, v4Dataset.fieldKey));
|
||||
await prepare(2);
|
||||
accepted("clear all", await FieldRuntime.clearAllEncodedFields(reactTag));
|
||||
try { await FieldRuntime.detach(reactTag); }
|
||||
catch (error) { throw new Error(`detach: ${String(error)}`); }
|
||||
steps.push("detach=PASS");
|
||||
let attached: RuntimeSnapshot;
|
||||
try { attached = await FieldRuntime.attach(reactTag); }
|
||||
catch (error) { throw new Error(`attach: ${String(error)}`); }
|
||||
setSnapshot(attached);
|
||||
steps.push("attach=PASS");
|
||||
await prepare(3);
|
||||
try { await refresh(reactTag); }
|
||||
catch (error) { throw new Error(`refresh: ${String(error)}`); }
|
||||
steps.push("refresh=PASS");
|
||||
const summary = steps.join(" | ");
|
||||
setFinalControlSummary(`FINAL CONTROL REGRESSION PASS: ${summary}`);
|
||||
setResult("FINAL CONTROL REGRESSION PASS");
|
||||
} catch (error) {
|
||||
const failure = `FAIL: ${String(error)}`;
|
||||
steps.push(failure);
|
||||
setFinalControlSummary(`FINAL CONTROL REGRESSION ${steps.join(" | ")}`);
|
||||
setResult(`FINAL CONTROL REGRESSION ${failure}`);
|
||||
} finally {
|
||||
finalControlRunRef.current = false;
|
||||
setFinalControlRunning(false);
|
||||
const toggleWeather = useCallback(async () => {
|
||||
if (reactTag == null) return;
|
||||
const next = !weatherVisible;
|
||||
if (await FieldRuntime.setFieldVisible(reactTag, next) === "ACCEPTED") {
|
||||
setWeatherVisible(next);
|
||||
}
|
||||
}, [prepareV4, reactTag, refresh, v4Dataset.fieldKey]);
|
||||
}, [reactTag, weatherVisible]);
|
||||
const toggleParticle = useCallback(async () => {
|
||||
if (reactTag == null) return;
|
||||
const next = !particleVisible;
|
||||
if (await FieldRuntime.setParticleVisible(reactTag, next) === "ACCEPTED") {
|
||||
setParticleVisible(next);
|
||||
}
|
||||
}, [particleVisible, reactTag]);
|
||||
|
||||
const updateVisual = useCallback((key: keyof typeof initialVisual, value: string) => setVisual((current) => ({ ...current, [key]: value })), []);
|
||||
const applyVisual = useCallback(() => void command("particle visual", async (tag) => {
|
||||
const results = await Promise.all([
|
||||
FieldRuntime.setParticleCount(tag, Number(visual.count)), FieldRuntime.setParticleLineWidth(tag, Number(visual.width)), FieldRuntime.setParticleLength(tag, Number(visual.length)), FieldRuntime.setParticleOpacity(tag, Number(visual.opacity)), FieldRuntime.setTrailOpacity(tag, Number(visual.trailOpacity)), FieldRuntime.setTrailFade(tag, Number(visual.trailFade)), FieldRuntime.setFieldOpacity(tag, Number(visual.weatherOpacity)),
|
||||
]);
|
||||
if (results.some((value) => value !== "ACCEPTED")) throw new Error(results.join(", ")); return "ACCEPTED";
|
||||
}), [command, visual]);
|
||||
const updateVisual = useCallback(
|
||||
(key: keyof typeof initialVisual, value: string) => {
|
||||
setVisual((current) => ({ ...current, [key]: value }));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const controls = useMemo(() => [
|
||||
["PLAY", (tag: number) => FieldRuntime.setPlaybackState(tag, "PLAY")], ["PAUSE", (tag: number) => FieldRuntime.setPlaybackState(tag, "PAUSE")], ["HALF", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "HALF")], ["NORMAL", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "NORMAL")], ["DOUBLE", (tag: number) => FieldRuntime.setPlaybackSpeed(tag, "DOUBLE")],
|
||||
["FPS AUTO", (tag: number) => FieldRuntime.setTargetFps(tag, "AUTO" as TargetFps)], ["FPS 30", (tag: number) => FieldRuntime.setTargetFps(tag, "FPS_30" as TargetFps)], ["FPS 60", (tag: number) => FieldRuntime.setTargetFps(tag, "FPS_60" as TargetFps)], ["Field stops", (tag: number) => FieldRuntime.setFieldColorStops(tag, frozenStops)], ["Particle color", (tag: number) => FieldRuntime.setParticleColor(tag, 1, 1, 1, 0.95)], ["Trail color", (tag: number) => FieldRuntime.setTrailColor(tag, 0.35, 0.85, 1, 0.8)],
|
||||
] as const, []);
|
||||
return (
|
||||
<SafeAreaView style={styles.root}>
|
||||
<View style={styles.map}>
|
||||
<Map
|
||||
mapStyle={weatherTestStyle}
|
||||
onDidFinishLoadingStyle={onStyleReady}
|
||||
onDidFinishLoadingMap={() => setMapStatus("READY")}
|
||||
onDidFailLoadingMap={() => {
|
||||
setMapStatus("ERROR");
|
||||
setResult("map loading error");
|
||||
}}
|
||||
>
|
||||
<Camera initialViewState={{ center: [137.5, 36.2], zoom: 4.8 }} />
|
||||
</Map>
|
||||
</View>
|
||||
<View style={styles.controlPanel}>
|
||||
<View style={styles.controlHeader}>
|
||||
<Text style={styles.controlHeaderTitle}>
|
||||
Particle V10 Test Controls
|
||||
</Text>
|
||||
<Button
|
||||
title={controlsCollapsed ? "展开 ▼" : "收起 ▲"}
|
||||
onPress={() => setControlsCollapsed((current) => !current)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
return <SafeAreaView style={styles.root}>
|
||||
<View style={styles.map}><Map mapStyle={weatherTestStyle} onDidFinishLoadingStyle={onStyleReady} onDidFinishLoadingMap={() => setBasemap("READY")} onDidFailLoadingMap={() => { setBasemap("ERROR"); setResult("map loading error"); }}><Camera initialViewState={{ center: [135, 32], zoom: 3.9 }} /></Map></View>
|
||||
<ScrollView style={styles.controls} contentContainerStyle={styles.content}>
|
||||
<Text>Basemap: GSI {basemap}</Text><Text>Dataset: {dataset.title}</Text><Text>Runtime: {snapshot.state} / {snapshot.playbackState} / {snapshot.targetFps}</Text><Text selectable>V2 FILE TRANSPORT = {transport}</Text>
|
||||
{dataset.frames.map((frame) => <Text selectable key={frame.label}>{frame.label}: {frames[frame.label]}</Text>)}
|
||||
<Text selectable>{result}</Text><View style={styles.row}>{fieldDatasets.map((item) => <Button key={item.id} title={item.id} onPress={() => setDataset(item)} />)}</View><Button title="LOAD SELECTED V3 DATASET" onPress={() => void loadDataset()} />
|
||||
<View style={styles.row}>{controls.map(([label, operation]) => <Button key={label} title={label} onPress={() => void command(label, operation)} />)}</View>
|
||||
<View style={styles.row}><Button title={`Weather ${weatherVisible ? "visible" : "hidden"}`} onPress={() => { const next = !weatherVisible; setWeatherVisible(next); void command("weather visible", (tag) => FieldRuntime.setFieldVisible(tag, next)); }} /><Button title={`Particle ${particleVisible ? "visible" : "hidden"}`} onPress={() => { const next = !particleVisible; setParticleVisible(next); void command("particle visible", (tag) => FieldRuntime.setParticleVisible(tag, next)); }} /></View>
|
||||
<View style={styles.row}>{dataset.frames.map((frame) => <Button key={`seek-${frame.label}`} title={`seek ${frame.label}`} onPress={() => void command(`seek ${frame.label}`, (tag) => FieldRuntime.setTargetTime(tag, frame.validTime))} />)}</View>
|
||||
<Text>Visual: count / width / length / particle opacity / trail opacity / trail fade / WeatherColor opacity</Text><View style={styles.row}>{(Object.keys(visual) as Array<keyof typeof initialVisual>).map((key) => <TextInput key={key} value={visual[key]} onChangeText={(value) => updateVisual(key, value)} keyboardType="decimal-pad" style={styles.input} />)}<Button title="Apply visual" onPress={applyVisual} /></View>
|
||||
<Text>V4 QBIN Golden: {v4Dataset.title}</Text><Text selectable>V4 raw QBIN transport = {v4Transport}</Text><Text selectable>V4 bindings: {v4Bindings}</Text><Text selectable>{v4Dataset.particleNote}</Text>
|
||||
<Text selectable>V4 T0: {v4Frames.T0}</Text><Text selectable>V4 T1: {v4Frames.T1}</Text>
|
||||
<View style={styles.row}>{qbinGoldenDatasets.map((item) => <Button key={item.id} title={`V4 ${item.id}`} onPress={() => setV4Dataset(item)} />)}</View><Button title="LOAD SELECTED V4 DATASET" onPress={() => void loadV4Dataset()} />
|
||||
<Button title={finalControlRunning ? "RUN V4 FINAL CONTROL REGRESSION (RUNNING)" : "RUN V4 FINAL CONTROL REGRESSION"} disabled={finalControlRunning} onPress={() => void runV4FinalControlRegression()} />
|
||||
<Text selectable>{finalControlSummary}</Text>
|
||||
<View style={styles.row}><Button title="V4 seek T0" onPress={() => void command("V4 seek T0", (tag) => FieldRuntime.setEncodedForecastTime(tag, v4Dataset.frames[0].validTimeMs))} /><Button title="V4 seek midpoint" onPress={() => void command("V4 seek midpoint (mix not exposed)", (tag) => FieldRuntime.setEncodedForecastTime(tag, v4Dataset.midpointMs))} /><Button title="V4 seek T1" onPress={() => void command("V4 seek T1", (tag) => FieldRuntime.setEncodedForecastTime(tag, v4Dataset.frames[1].validTimeMs))} /></View>
|
||||
<View style={styles.row}><Button title="V4 PLAY" onPress={() => void command("V4 PLAY", (tag) => FieldRuntime.setEncodedForecastPlaybackState(tag, "PLAY"))} /><Button title="V4 PAUSE" onPress={() => void command("V4 PAUSE", (tag) => FieldRuntime.setEncodedForecastPlaybackState(tag, "PAUSE"))} /><Button title="V4 speed 1.0" onPress={() => void command("V4 speed 1.0", (tag) => FieldRuntime.setEncodedForecastPlaybackSpeed(tag, 1.0))} /><Button title="V4 clear selected" onPress={() => void command("V4 clear selected", (tag) => FieldRuntime.clearEncodedField(tag, v4Dataset.fieldKey))} /><Button title="V4 clear all" onPress={() => void command("V4 clear all", (tag) => FieldRuntime.clearAllEncodedFields(tag))} /></View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>;
|
||||
{!controlsCollapsed && (
|
||||
<ScrollView style={styles.controls} contentContainerStyle={styles.content}>
|
||||
<Text>
|
||||
Map: {mapStatus} / Weather: {weatherStatus} / Runtime: {snapshot.state}
|
||||
</Text>
|
||||
<Text>
|
||||
Native: RN 13.4.1-particle.10 / iOS 6.28.0-particle.10
|
||||
</Text>
|
||||
<Text selectable>Source: {activeSource}</Text>
|
||||
<Text selectable>{result}</Text>
|
||||
|
||||
<Text>QBIN Format</Text>
|
||||
<View style={styles.row}>
|
||||
{(["V2", "V3"] as const).map((item) => (
|
||||
<Button
|
||||
key={item}
|
||||
title={(format === item ? "✓ " : "") + item}
|
||||
onPress={() => setFormat(item)}
|
||||
disabled={loading}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<Text>Dataset</Text>
|
||||
<View style={styles.row}>
|
||||
{(["wind", "wave", "current", "rain"] as const).map((item) => (
|
||||
<Button
|
||||
key={item}
|
||||
title={(datasetId === item ? "✓ " : "") + item}
|
||||
onPress={() => setDatasetId(item)}
|
||||
disabled={loading}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text>
|
||||
{valueSourceFor(datasetId)} / {legendFor(datasetId)}
|
||||
</Text>
|
||||
<View style={styles.row}>
|
||||
<Button
|
||||
title={"LOAD BUNDLED " + format}
|
||||
disabled={reactTag == null || loading}
|
||||
onPress={() => reactTag != null &&
|
||||
void loadBundled(reactTag, format, datasetId)}
|
||||
/>
|
||||
<Button
|
||||
title={"ONLINE GET " + format}
|
||||
disabled={reactTag == null || loading}
|
||||
onPress={() => reactTag != null &&
|
||||
void loadOnline(reactTag, format, datasetId)}
|
||||
/>
|
||||
<Button
|
||||
title={weatherVisible ? "Weather ON" : "Weather OFF"}
|
||||
disabled={reactTag == null || loading}
|
||||
onPress={() => void toggleWeather()}
|
||||
/>
|
||||
<Button
|
||||
title={particleVisible ? "Particle ON" : "Particle OFF"}
|
||||
disabled={
|
||||
reactTag == null ||
|
||||
loading ||
|
||||
selectedDataset.particleBinding == null
|
||||
}
|
||||
onPress={() => void toggleParticle()}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text>
|
||||
Visual: count / width / length / particle opacity / trail opacity /
|
||||
trail fade / WeatherColor opacity
|
||||
</Text>
|
||||
<View style={styles.row}>
|
||||
{(Object.keys(visual) as Array<keyof typeof initialVisual>).map((key) => (
|
||||
<TextInput
|
||||
key={key}
|
||||
value={visual[key]}
|
||||
onChangeText={(value) => updateVisual(key, value)}
|
||||
keyboardType="decimal-pad"
|
||||
style={styles.input}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
title="Apply visual"
|
||||
disabled={reactTag == null || loading}
|
||||
onPress={() => void applyVisual()}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({ root: { flex: 1 }, map: { flex: 1 }, controls: { maxHeight: 310, backgroundColor: "#fff" }, content: { gap: 7, padding: 10 }, row: { flexDirection: "row", flexWrap: "wrap", gap: 6, alignItems: "center" }, input: { borderWidth: 1, minWidth: 48, padding: 5 } });
|
||||
const styles = StyleSheet.create({
|
||||
root: { flex: 1 },
|
||||
map: { flex: 1 },
|
||||
controlPanel: { backgroundColor: "#fff" },
|
||||
controlHeader: {
|
||||
minHeight: 44,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: "#c7c7c7",
|
||||
},
|
||||
controlHeaderTitle: { fontSize: 15, fontWeight: "600" },
|
||||
controls: { maxHeight: 360, backgroundColor: "#fff" },
|
||||
content: { gap: 7, padding: 10, paddingTop: 4 },
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: 6,
|
||||
alignItems: "center",
|
||||
},
|
||||
input: { borderWidth: 1, minWidth: 48, padding: 5 },
|
||||
});
|
||||
|
||||
@@ -5,7 +5,18 @@
|
||||
"version": "0.1.0",
|
||||
"orientation": "landscape",
|
||||
"newArchEnabled": true,
|
||||
"plugins": ["./plugins/withParticleNativeAndroid"],
|
||||
"plugins": [
|
||||
[
|
||||
"@maplibre/maplibre-react-native",
|
||||
{
|
||||
"ios": {
|
||||
"spmSpec": "{ url: \"https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git\", requirement: { kind: \"exactVersion\", version: \"6.28.0-particle.10\" }, product_name: \"MapLibre\" }"
|
||||
}
|
||||
}
|
||||
],
|
||||
"./plugins/withParticleNativeIos",
|
||||
"./plugins/withParticleNativeAndroid"
|
||||
],
|
||||
"android": { "package": "org.maplibre.particle.rntesthost", "minSdkVersion": 24 },
|
||||
"ios": { "bundleIdentifier": "org.maplibre.particle.rntesthost", "supportsTablet": true }
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@maplibre/maplibre-react-native": "11.3.6",
|
||||
"@navsea/maplibre-particle-rn": "file:../../../packages/maplibre-particle-rn",
|
||||
"@navsea/maplibre-particle-rn": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.10.tgz",
|
||||
"expo": "56.0.4",
|
||||
"expo-asset": "~56.0.14",
|
||||
"expo-file-system": "56.0.11",
|
||||
@@ -21,24 +21,6 @@
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
},
|
||||
"../../../packages/maplibre-particle-rn": {
|
||||
"name": "@navsea/maplibre-particle-rn",
|
||||
"version": "13.4.1-particle.3",
|
||||
"license": "BSD-2-Clause",
|
||||
"devDependencies": {
|
||||
"@maplibre/maplibre-react-native": "11.3.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@maplibre/maplibre-react-native": "11.3.6",
|
||||
"react": ">=19.1.0",
|
||||
"react-native": ">=0.80.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"expo": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
@@ -1780,8 +1762,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@navsea/maplibre-particle-rn": {
|
||||
"resolved": "../../../packages/maplibre-particle-rn",
|
||||
"link": true
|
||||
"version": "13.4.1-particle.10",
|
||||
"resolved": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.10.tgz",
|
||||
"integrity": "sha512-SNXvZ71A+vhrWzTxYCLGLGNkg6evtl8hydgdMUzB2Ft78sf5o+SCLQFtJ7ieLJl9r4kVEYdmprG8uwRdJDlhYg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peerDependencies": {
|
||||
"@maplibre/maplibre-react-native": "11.3.6",
|
||||
"expo": ">=54.0.0",
|
||||
"react": ">=19.1.0",
|
||||
"react-native": ">=0.80.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"expo": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/assets-registry": {
|
||||
"version": "0.85.2",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"android": "expo run:android"
|
||||
},
|
||||
"dependencies": {
|
||||
"@navsea/maplibre-particle-rn": "file:../../../packages/maplibre-particle-rn",
|
||||
"@navsea/maplibre-particle-rn": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.10.tgz",
|
||||
"@maplibre/maplibre-react-native": "11.3.6",
|
||||
"expo": "56.0.4",
|
||||
"expo-asset": "~56.0.14",
|
||||
|
||||
@@ -6,7 +6,7 @@ const marker = "// MapLibreParticleRNTestHost NavSea Particle release SDK";
|
||||
module.exports = function withParticleNativeAndroid(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.9")\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.10")\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;
|
||||
});
|
||||
};
|
||||
|
||||
21
integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeIos.js
vendored
Normal file
21
integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeIos.js
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
const { withPodfile } = require("@expo/config-plugins");
|
||||
|
||||
const marker = "# MapLibreParticleRNTestHost Particle iOS SPM hook";
|
||||
|
||||
module.exports = function withParticleNativeIos(config) {
|
||||
return withPodfile(config, (project) => {
|
||||
const contents = project.modResults.contents;
|
||||
if (contents.includes(marker)) return project;
|
||||
|
||||
const needle = " post_install do |installer|";
|
||||
if (!contents.includes(needle)) {
|
||||
throw new Error("Expo post_install block was not generated.");
|
||||
}
|
||||
|
||||
project.modResults.contents = contents.replace(
|
||||
needle,
|
||||
needle + "\n " + marker + "\n $MLRN_PARTICLE.post_install(installer)",
|
||||
);
|
||||
return project;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
import { File, Paths } from "expo-file-system";
|
||||
import type { WeatherDatasetId } from "./weatherPalettes";
|
||||
|
||||
export type QbinFormat = "V2" | "V3";
|
||||
|
||||
type WeatherManifest = Readonly<{
|
||||
l: Record<string, Readonly<{
|
||||
d: string;
|
||||
ds: readonly Readonly<{ id: string; r: string; iu: string }>[];
|
||||
}>>;
|
||||
}>;
|
||||
|
||||
type WeatherTimeIndex = Readonly<{
|
||||
d: string;
|
||||
l: string;
|
||||
r: string;
|
||||
t: readonly string[];
|
||||
x: Readonly<{
|
||||
q2f?: string;
|
||||
q2v?: number;
|
||||
q3f?: string;
|
||||
q3v?: number;
|
||||
q3bw?: number;
|
||||
q3bh?: number;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
export type OnlineQbinFrame = Readonly<{
|
||||
label: "T0" | "T1";
|
||||
targetTime: string;
|
||||
validTimeMs: number;
|
||||
metadata: Readonly<{ fieldID: string; generation: number; dataVersion: number }>;
|
||||
qbinFileUri: string;
|
||||
sourceUrl: string;
|
||||
}>;
|
||||
export type OnlineQbinSelection = Readonly<{
|
||||
datasetId: string;
|
||||
serverLayer: string;
|
||||
refTime: string;
|
||||
frames: readonly OnlineQbinFrame[];
|
||||
midpointMs: number;
|
||||
}>;
|
||||
|
||||
const BASE = "https://weather.jpcn.ccwu.cc/";
|
||||
const HEADERS = { "User-Agent": "Mozilla/5.0" };
|
||||
|
||||
function layerFor(id: WeatherDatasetId): string {
|
||||
if (id === "wave") return "waves";
|
||||
if (id === "current") return "tide";
|
||||
return id;
|
||||
}
|
||||
|
||||
function timeMs(value: string): number {
|
||||
if (!/^\d{10}$/.test(value)) throw new Error("Invalid forecast time: " + value);
|
||||
return Date.UTC(
|
||||
Number(value.slice(0, 4)),
|
||||
Number(value.slice(4, 6)) - 1,
|
||||
Number(value.slice(6, 8)),
|
||||
Number(value.slice(8, 10)),
|
||||
);
|
||||
}
|
||||
|
||||
function chooseTimes(times: readonly string[]): readonly [string, string] {
|
||||
if (times.length < 2) throw new Error("time_index has fewer than two target times");
|
||||
const now = Date.now();
|
||||
let index = times.findIndex((value) => timeMs(value) >= now);
|
||||
if (index < 0) index = times.length - 1;
|
||||
if (index === 0) return [times[0], times[1]];
|
||||
return [times[index - 1], times[index]];
|
||||
}
|
||||
async function fetchJson<T>(url: string): Promise<T> {
|
||||
const response = await fetch(url, { headers: HEADERS });
|
||||
if (!response.ok) throw new Error("HTTP " + response.status + ": " + url);
|
||||
return await response.json() as T;
|
||||
}
|
||||
|
||||
export async function downloadOnlineQbin(
|
||||
format: QbinFormat,
|
||||
id: WeatherDatasetId,
|
||||
): Promise<OnlineQbinSelection> {
|
||||
const manifest = await fetchJson<WeatherManifest>(BASE + "weather_manifest.json");
|
||||
const serverLayer = layerFor(id);
|
||||
const layer = manifest.l[serverLayer];
|
||||
if (!layer) throw new Error("manifest layer missing: " + serverLayer);
|
||||
|
||||
const dataset = layer.ds.find((item) => item.id === layer.d) ?? layer.ds[0];
|
||||
if (!dataset) throw new Error("manifest dataset missing: " + serverLayer);
|
||||
|
||||
const indexUrl = BASE + dataset.iu.replace(/^\/+/, "");
|
||||
const timeIndex = await fetchJson<WeatherTimeIndex>(indexUrl);
|
||||
const template = format === "V3" ? timeIndex.x.q3f : timeIndex.x.q2f;
|
||||
const version = format === "V3" ? timeIndex.x.q3v : timeIndex.x.q2v;
|
||||
if (!template) throw new Error(format + " frame template missing");
|
||||
if (version !== (format === "V3" ? 3 : 2)) {
|
||||
throw new Error(format + " version mismatch: " + String(version));
|
||||
}
|
||||
|
||||
const [t0, t1] = chooseTimes(timeIndex.t);
|
||||
const baseUrl = indexUrl.slice(0, indexUrl.lastIndexOf("/") + 1);
|
||||
const targets = [t0, t1] as const;
|
||||
const frames: OnlineQbinFrame[] = [];
|
||||
for (let index = 0; index < targets.length; index += 1) {
|
||||
const targetTime = targets[index];
|
||||
const relative = template.replace("{target_time}", targetTime);
|
||||
const sourceUrl = baseUrl + relative;
|
||||
const destination = new File(
|
||||
Paths.cache,
|
||||
"online-" + serverLayer + "-" + format.toLowerCase() + "-" + targetTime + ".qbin",
|
||||
);
|
||||
const downloaded = await File.downloadFileAsync(
|
||||
sourceUrl,
|
||||
destination,
|
||||
{ headers: HEADERS, idempotent: true },
|
||||
);
|
||||
frames.push({
|
||||
label: index === 0 ? "T0" : "T1",
|
||||
targetTime,
|
||||
validTimeMs: timeMs(targetTime),
|
||||
metadata: {
|
||||
fieldID: "online-" + timeIndex.d + "-" + serverLayer,
|
||||
generation: index + 1,
|
||||
dataVersion: index + 1,
|
||||
},
|
||||
qbinFileUri: downloaded.uri,
|
||||
sourceUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
datasetId: timeIndex.d,
|
||||
serverLayer,
|
||||
refTime: timeIndex.r,
|
||||
frames,
|
||||
midpointMs: Math.round((frames[0].validTimeMs + frames[1].validTimeMs) / 2),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Asset } from "expo-asset";
|
||||
|
||||
export type V4CommandResult = "ACCEPTED" | "REJECTED";
|
||||
|
||||
type RequiredPlane = Readonly<{
|
||||
semanticID: number;
|
||||
valueClass: "SCALAR" | "VELOCITY_COMPONENT" | "DIRECTION_ANGLE" | "VALIDITY";
|
||||
unitID: number;
|
||||
groupID: number;
|
||||
component: "NONE" | "U_EAST" | "V_NORTH" | "W_UP";
|
||||
requiredFlag: boolean;
|
||||
}>;
|
||||
|
||||
export type V4Dataset = Readonly<{
|
||||
id: "wind" | "wave" | "current" | "rain";
|
||||
title: string;
|
||||
fieldKey: string;
|
||||
profileContract: Readonly<{
|
||||
contractSchemaVersion: 1;
|
||||
profileID: string;
|
||||
profileVersion: 1;
|
||||
allowAdditionalPlanes: boolean;
|
||||
sharedValidityRequired: boolean;
|
||||
timeRelation: "VALID_GTE_REFERENCE";
|
||||
requiredPlanes: readonly RequiredPlane[];
|
||||
}>;
|
||||
frames: readonly V4Frame[];
|
||||
midpointMs: number;
|
||||
colorBinding: Readonly<{
|
||||
fieldKey: string;
|
||||
semanticID: number;
|
||||
groupID: number;
|
||||
expectedValueClass: "SCALAR" | "VELOCITY_COMPONENT" | "DIRECTION_ANGLE";
|
||||
expectedUnitID: number;
|
||||
}>;
|
||||
particleBinding?: Readonly<{
|
||||
fieldKey: string;
|
||||
mode: "VECTOR_COMPONENTS" | "DIRECTION_ANGLE";
|
||||
uPlane?: Readonly<{ semanticID: number; groupID: number }>;
|
||||
vPlane?: Readonly<{ semanticID: number; groupID: number }>;
|
||||
directionPlane?: Readonly<{ semanticID: number; groupID: number }>;
|
||||
expectedUnitID: number;
|
||||
}>;
|
||||
particleNote: string;
|
||||
}>;
|
||||
|
||||
export type V4Frame = Readonly<{
|
||||
label: "T0" | "T1";
|
||||
validTimeMs: number;
|
||||
metadata: Readonly<{ fieldID: string; generation: number; dataVersion: number }>;
|
||||
assetModule: number;
|
||||
sourceRelativePath: string;
|
||||
bytes: number;
|
||||
sha256: string;
|
||||
}>;
|
||||
|
||||
const sharedValidity: RequiredPlane = {
|
||||
semanticID: 3, valueClass: "VALIDITY", unitID: 0, groupID: 0, component: "NONE", requiredFlag: true,
|
||||
};
|
||||
const vectorPlanes: readonly RequiredPlane[] = [
|
||||
{ semanticID: 1, valueClass: "VELOCITY_COMPONENT", unitID: 1, groupID: 1, component: "U_EAST", requiredFlag: true },
|
||||
{ semanticID: 2, valueClass: "VELOCITY_COMPONENT", unitID: 1, groupID: 1, component: "V_NORTH", requiredFlag: true },
|
||||
sharedValidity,
|
||||
];
|
||||
const wavePlanes: readonly RequiredPlane[] = [
|
||||
{ semanticID: 10, valueClass: "DIRECTION_ANGLE", unitID: 4, groupID: 0, component: "NONE", requiredFlag: true },
|
||||
{ semanticID: 20, valueClass: "SCALAR", unitID: 2, groupID: 0, component: "NONE", requiredFlag: true },
|
||||
{ semanticID: 21, valueClass: "SCALAR", unitID: 3, groupID: 0, component: "NONE", requiredFlag: true },
|
||||
sharedValidity,
|
||||
];
|
||||
const rainPlanes: readonly RequiredPlane[] = [
|
||||
{ semanticID: 30, valueClass: "SCALAR", unitID: 5, groupID: 0, component: "NONE", requiredFlag: true },
|
||||
sharedValidity,
|
||||
];
|
||||
|
||||
function contract(profileID: string, requiredPlanes: readonly RequiredPlane[]) {
|
||||
return {
|
||||
contractSchemaVersion: 1 as const,
|
||||
profileID,
|
||||
profileVersion: 1 as const,
|
||||
allowAdditionalPlanes: true,
|
||||
sharedValidityRequired: true,
|
||||
timeRelation: "VALID_GTE_REFERENCE" as const,
|
||||
requiredPlanes,
|
||||
};
|
||||
}
|
||||
|
||||
function frame(label: "T0" | "T1", fieldID: string, validTimeMs: number, assetModule: number, sourceRelativePath: string, bytes: number, sha256: string): V4Frame {
|
||||
const revision = label === "T0" ? 1 : 2;
|
||||
return { label, validTimeMs, metadata: { fieldID, generation: revision, dataVersion: revision }, assetModule, sourceRelativePath, bytes, sha256 };
|
||||
}
|
||||
|
||||
const windFrames = [
|
||||
frame("T0", "fixture-wind", 1789239600000, require("../assets/qbin-v3-golden/wind/T0.qbin"), "wind/frames/2026091219.qbin", 621120, "e815d94c18711c70e27c570cfd89acda3a27d64646d04e0b29846404ca325979"),
|
||||
frame("T1", "fixture-wind", 1789243200000, require("../assets/qbin-v3-golden/wind/T1.qbin"), "wind/frames/2026091220.qbin", 622832, "bb2dbde0a8984d2fda4140cb6393744ede14fc9327663091e7afd4d2e37606da"),
|
||||
] as const;
|
||||
const waveFrames = [
|
||||
frame("T0", "fixture-wave", 1789239600000, require("../assets/qbin-v3-golden/wave/T0.qbin"), "wave/frames/2026091219.qbin", 520688, "e4409ba0b09003e663dacdce8fc2ef35a1b929b5ccba8dc82e1ad0fc8c2f1202"),
|
||||
frame("T1", "fixture-wave", 1789243200000, require("../assets/qbin-v3-golden/wave/T1.qbin"), "wave/frames/2026091220.qbin", 520016, "4827e4c9a7917da944cdf65212cc6371b0b7276fb9ae3487625f1c539b27f178"),
|
||||
] as const;
|
||||
const currentFrames = [
|
||||
frame("T0", "fixture-current", 1789174800000, require("../assets/qbin-v3-golden/current/T0.qbin"), "tide/frames/2026091201.qbin", 104592, "b1b6d4a1dbb98971c3daca6b781587464e3528dd929da25196409b4b3b2bc052"),
|
||||
frame("T1", "fixture-current", 1789178400000, require("../assets/qbin-v3-golden/current/T1.qbin"), "tide/frames/2026091202.qbin", 104112, "0588b3b1ed9e2d491adefa308f7d0b0707a38d773cc8d0802cded77a24c2aa24"),
|
||||
] as const;
|
||||
const rainFrames = [
|
||||
frame("T0", "fixture-rain", 1789239600000, require("../assets/qbin-v3-golden/rain/T0.qbin"), "rain/frames/2026091219.qbin", 153904, "567d99c6309d185184193f203f6eb7b2891572730ec9299371e6a0db0a067d20"),
|
||||
frame("T1", "fixture-rain", 1789243200000, require("../assets/qbin-v3-golden/rain/T1.qbin"), "rain/frames/2026091220.qbin", 153904, "8688d0373534d1c67ab4eed836e1314f14eba639594666683e6c22ff091e14fc"),
|
||||
] as const;
|
||||
|
||||
const vectorColor = (fieldKey: string) => ({ fieldKey, semanticID: 1, groupID: 1, expectedValueClass: "VELOCITY_COMPONENT" as const, expectedUnitID: 1 });
|
||||
const vectorParticle = (fieldKey: string) => ({ fieldKey, mode: "VECTOR_COMPONENTS" as const, uPlane: { semanticID: 1, groupID: 1 }, vPlane: { semanticID: 2, groupID: 1 }, expectedUnitID: 1 });
|
||||
|
||||
/** QBIN V3 Golden fixtures for the encoded-field public contract. Production block geometry is fixed at 16x16. */
|
||||
export const qbinV3GoldenDatasets: readonly V4Dataset[] = [
|
||||
{ id: "wind", title: "Wind QBIN V3", fieldKey: "fixture.wind", profileContract: contract("wind", vectorPlanes), frames: windFrames, midpointMs: 1789241400000, colorBinding: vectorColor("fixture.wind"), particleBinding: vectorParticle("fixture.wind"), particleNote: "U ColorBinding (VELOCITY_COMPONENT); U/V particle vectors." },
|
||||
{ id: "wave", title: "Wave QBIN V3", fieldKey: "fixture.wave", profileContract: contract("wave", wavePlanes), frames: waveFrames, midpointMs: 1789241400000, colorBinding: { fieldKey: "fixture.wave", semanticID: 20, groupID: 0, expectedValueClass: "SCALAR", expectedUnitID: 2 }, particleBinding: { fieldKey: "fixture.wave", mode: "DIRECTION_ANGLE", directionPlane: { semanticID: 10, groupID: 0 }, expectedUnitID: 4 }, particleNote: "WAVE_HEIGHT color; DIRECTION angle particle (no direction conversion)." },
|
||||
{ id: "current", title: "Current QBIN V3", fieldKey: "fixture.current", profileContract: contract("current", vectorPlanes), frames: currentFrames, midpointMs: 1789176600000, colorBinding: vectorColor("fixture.current"), particleBinding: vectorParticle("fixture.current"), particleNote: "U ColorBinding (VELOCITY_COMPONENT); U/V particle vectors." },
|
||||
{ id: "rain", title: "Rain QBIN V3", fieldKey: "fixture.rain", profileContract: contract("rain", rainPlanes), frames: rainFrames, midpointMs: 1789241400000, colorBinding: { fieldKey: "fixture.rain", semanticID: 30, groupID: 0, expectedValueClass: "SCALAR", expectedUnitID: 5 }, particleNote: "Particle N/A: fixture has no particle-compatible plane." },
|
||||
] as const;
|
||||
|
||||
export async function materializeV4Frame(input: V4Frame): Promise<V4Frame & { qbinFileUri: string }> {
|
||||
const asset = Asset.fromModule(input.assetModule);
|
||||
await asset.downloadAsync();
|
||||
if (!asset.localUri?.startsWith("file://")) throw new Error(`${input.label} QBIN asset did not materialize a local file:// URI`);
|
||||
return { ...input, qbinFileUri: asset.localUri };
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { FieldColorStop } from "@navsea/maplibre-particle-rn";
|
||||
|
||||
export type WeatherDatasetId = "wind" | "wave" | "current" | "rain";
|
||||
|
||||
const wind: readonly FieldColorStop[] = [
|
||||
{ value: 0.00000, red: 0.16, green: 0.35, blue: 0.78, alpha: 1.00 },
|
||||
{ value: 2.57222, red: 0.10, green: 0.58, blue: 0.78, alpha: 1.00 },
|
||||
{ value: 5.14444, red: 0.10, green: 0.68, blue: 0.34, alpha: 1.00 },
|
||||
{ value: 10.28888, red: 0.70, green: 0.68, blue: 0.08, alpha: 1.00 },
|
||||
{ value: 15.43332, red: 0.88, green: 0.38, blue: 0.12, alpha: 1.00 },
|
||||
{ value: 20.57776, red: 0.78, green: 0.15, blue: 0.38, alpha: 1.00 },
|
||||
{ value: 30.86664, red: 0.48, green: 0.18, blue: 0.70, alpha: 1.00 },
|
||||
];
|
||||
|
||||
const wave: readonly FieldColorStop[] = [
|
||||
{ value: 0.5, red: 0.18, green: 0.58, blue: 0.68, alpha: 1.00 },
|
||||
{ value: 1.0, red: 0.18, green: 0.38, blue: 0.55, alpha: 1.00 },
|
||||
{ value: 1.5, red: 0.21, green: 0.38, blue: 0.71, alpha: 1.00 },
|
||||
{ value: 2.0, red: 0.22, green: 0.23, blue: 0.53, alpha: 1.00 },
|
||||
{ value: 6.0, red: 0.74, green: 0.29, blue: 0.35, alpha: 1.00 },
|
||||
{ value: 9.0, red: 0.73, green: 0.62, blue: 0.60, alpha: 1.00 },
|
||||
];
|
||||
const current: readonly FieldColorStop[] = [
|
||||
{ value: 0.000000, red: 0.20, green: 0.25, blue: 0.46, alpha: 1.00 },
|
||||
{ value: 0.205778, red: 0.19, green: 0.50, blue: 0.18, alpha: 1.00 },
|
||||
{ value: 0.411555, red: 0.50, green: 0.40, blue: 0.18, alpha: 1.00 },
|
||||
{ value: 0.823110, red: 0.37, green: 0.24, blue: 0.47, alpha: 1.00 },
|
||||
{ value: 1.028889, red: 0.24, green: 0.39, blue: 0.42, alpha: 1.00 },
|
||||
{ value: 1.646221, red: 0.53, green: 0.53, blue: 0.53, alpha: 1.00 },
|
||||
];
|
||||
|
||||
const rain: readonly FieldColorStop[] = [
|
||||
{ value: 1.5, red: 0.21, green: 0.44, blue: 0.57, alpha: 1.00 },
|
||||
{ value: 2.0, red: 0.19, green: 0.42, blue: 0.53, alpha: 1.00 },
|
||||
{ value: 3.0, red: 0.20, green: 0.47, blue: 0.56, alpha: 1.00 },
|
||||
{ value: 7.0, red: 0.18, green: 0.58, blue: 0.38, alpha: 1.00 },
|
||||
{ value: 10.0, red: 0.47, green: 0.59, blue: 0.22, alpha: 1.00 },
|
||||
{ value: 20.0, red: 0.58, green: 0.22, blue: 0.22, alpha: 1.00 },
|
||||
{ value: 30.0, red: 0.58, green: 0.21, blue: 0.55, alpha: 1.00 },
|
||||
];
|
||||
|
||||
export function colorStopsFor(id: WeatherDatasetId): readonly FieldColorStop[] {
|
||||
if (id === "wind") return wind;
|
||||
if (id === "wave") return wave;
|
||||
if (id === "current") return current;
|
||||
return rain;
|
||||
}
|
||||
|
||||
export function legendFor(id: WeatherDatasetId): string {
|
||||
if (id === "wind") return "0 / 5 / 10 / 20 / 30 / 40 / 60 kt";
|
||||
if (id === "wave") return "0.5 / 1 / 1.5 / 2 / 6 / 9 m";
|
||||
if (id === "current") return "0 / 0.4 / 0.8 / 1.6 / 2 / 3.2 kt";
|
||||
return "1.5 / 2 / 3 / 7 / 10 / 20 / 30 mm";
|
||||
}
|
||||
@@ -1,31 +1,56 @@
|
||||
/** No field is embedded here: RasterArraySource is reserved for STEP-061 file-reference submission. */
|
||||
/** Fixed visual baseline: WeatherColor -> Particle -> local darkmap PNG. */
|
||||
export const DARKMAP_SOURCE_ID = "navsea_darkmap_png";
|
||||
export const DARKMAP_TILE_URL =
|
||||
"http://192.168.200.133:8000/windy_tiles_japan/{z}/{x}/{y}.png";
|
||||
|
||||
export const WEATHER_SOURCE_ID = "rn-weather-fields";
|
||||
export const WEATHER_COLOR_LAYER_ID = "rn-weather-color";
|
||||
export const PARTICLE_LAYER_ID = "rn-weather-particles";
|
||||
export const GSI_SOURCE_ID = "gsi-standard";
|
||||
export const GSI_LAYER_ID = "gsi-standard";
|
||||
|
||||
export const weatherTestStyle = JSON.stringify({
|
||||
version: 8,
|
||||
sources: {
|
||||
[GSI_SOURCE_ID]: {
|
||||
[DARKMAP_SOURCE_ID]: {
|
||||
type: "raster",
|
||||
tiles: ["https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png"],
|
||||
tileSize: 256,
|
||||
maxzoom: 18,
|
||||
attribution: "\u56fd\u571f\u5730\u7406\u9662 / GSI"
|
||||
tiles: [DARKMAP_TILE_URL],
|
||||
minzoom: 4,
|
||||
maxzoom: 9,
|
||||
tileSize: 256
|
||||
},
|
||||
[WEATHER_SOURCE_ID]: { type: "raster-array", "host-field-submission": true }
|
||||
},
|
||||
layers: [
|
||||
{ id: "rn-basemap", type: "background", paint: { "background-color": "#10243a" } },
|
||||
{ id: GSI_LAYER_ID, type: "raster", source: GSI_SOURCE_ID, paint: { "raster-opacity": 1 } },
|
||||
{ id: WEATHER_COLOR_LAYER_ID, type: "weather-color", source: WEATHER_SOURCE_ID,
|
||||
paint: { "weather-color-opacity": 0.62 } },
|
||||
{ id: PARTICLE_LAYER_ID, type: "raster-particle", source: WEATHER_SOURCE_ID,
|
||||
paint: { "raster-particle-active": true, "raster-particle-particle-count": 4096,
|
||||
"raster-particle-line-width": 1.5, "raster-particle-particle-length": 0.012,
|
||||
"raster-particle-particle-opacity": 0.9, "raster-particle-trail-opacity": 0.8,
|
||||
"raster-particle-trail-fade": 0.94 } }
|
||||
[WEATHER_SOURCE_ID]: {
|
||||
type: "raster-array",
|
||||
"host-field-submission": true
|
||||
}
|
||||
}, layers: [
|
||||
{
|
||||
id: WEATHER_COLOR_LAYER_ID,
|
||||
type: "weather-color",
|
||||
source: WEATHER_SOURCE_ID,
|
||||
paint: {
|
||||
"weather-color-opacity": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: PARTICLE_LAYER_ID,
|
||||
type: "raster-particle",
|
||||
source: WEATHER_SOURCE_ID,
|
||||
paint: {
|
||||
"raster-particle-active": true,
|
||||
"raster-particle-particle-count": 4096,
|
||||
"raster-particle-line-width": 1.8,
|
||||
"raster-particle-particle-length": 0.028,
|
||||
"raster-particle-particle-opacity": 0.85,
|
||||
"raster-particle-trail-opacity": 0.72,
|
||||
"raster-particle-trail-fade": 0.975
|
||||
}
|
||||
}, {
|
||||
id: "darkmap-basemap",
|
||||
type: "raster",
|
||||
source: DARKMAP_SOURCE_ID,
|
||||
paint: {
|
||||
"raster-opacity": 1,
|
||||
"raster-fade-duration": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 13.4.1-particle.10(Android / iOS 统一发行)
|
||||
|
||||
- Mac unified release pipeline 统一生成 Android Native、RN package 与 iOS SwiftPM/XCFramework;共享 Particle revision 10。
|
||||
## 13.4.1-particle.9(QBIN V3 Decoder)
|
||||
|
||||
- 新增 QBV3/v3 Native Decoder,固定 16×16 Block Geometry,支持 Predictor + ZigZag + Rice 解码、Direction 环形残差、CRC32、canonical packing、validity 与资源上限校验。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# @navsea/maplibre-particle-rn
|
||||
|
||||
当前版本:`13.4.1-particle.9`。Android Native 必须配套 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.9`。本包是 React Native 的薄控制面,不负责 Map 组件、provider、HTTP、manifest、下载、缓存或 NavSea 业务策略。
|
||||
当前版本:`13.4.1-particle.10`。Android Native 必须配套 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.10`。本包是 React Native 的薄控制面,不负责 Map 组件、provider、HTTP、manifest、下载、缓存或 NavSea 业务策略。
|
||||
|
||||
## 当前推荐主链:QBIN V4
|
||||
|
||||
@@ -37,7 +37,7 @@ Podfile 的 `post_install do |installer|` 必须同时调用 `$MLRN.post_install
|
||||
RN tgz:
|
||||
|
||||
```text
|
||||
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.9.tgz
|
||||
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.10.tgz
|
||||
```
|
||||
|
||||
Native Maven repository:
|
||||
@@ -49,7 +49,7 @@ https://pbf.jpcn.ccwu.cc/maven
|
||||
Native coordinate:
|
||||
|
||||
```text
|
||||
cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.9
|
||||
cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.10
|
||||
```
|
||||
|
||||
Expo plugin 与 `android/build.gradle` 必须指向同一 `.8`。正常 Consumer 不复制 AAR、不依赖 MapLibreParticleLab 源码路径,也不以 Maven Local 作为公网验证主路径。完整 Linux 发布与 Win11 验证流程见 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`。
|
||||
Expo plugin 与 `android/build.gradle` 必须指向同一 `.10`。正常 Consumer 不复制 AAR、不依赖 MapLibreParticleLab 源码路径,也不以 Maven Local 作为公网验证主路径。完整 Linux 发布与 Win11 验证流程见 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`。
|
||||
|
||||
@@ -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.9"
|
||||
implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.10"
|
||||
compileOnly project(":maplibre_maplibre-react-native")
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ const iosTag = "navsea-maplibre-particle-rn-maplibre-spm";
|
||||
module.exports = function withNavSeaMapLibreParticle(config) {
|
||||
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.9")\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${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.10")\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;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navsea/maplibre-particle-rn",
|
||||
"version": "13.4.1-particle.9",
|
||||
"version": "13.4.1-particle.10",
|
||||
"description": "NavSea MapLibre Particle React Native pure Field Renderer API.",
|
||||
"license": "BSD-2-Clause",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1 +1 @@
|
||||
13.4.1-particle.9
|
||||
13.4.1-particle.10
|
||||
|
||||
@@ -9,8 +9,8 @@ let package = Package(
|
||||
targets: [
|
||||
.binaryTarget(
|
||||
name: "MapLibre",
|
||||
url: "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.9/MapLibre.dynamic.xcframework.zip",
|
||||
checksum: "fbda91e09d53e3cd1362590bfc0410b6fb5f9fef4cf99796d4ab7b4b84c93562"
|
||||
url: "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.10/MapLibre.dynamic.xcframework.zip",
|
||||
checksum: "1050573be4d2e29852a6ddce27f828e869eb51626ca91aa0fb4154656a57cd90"
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# MapLibre Particle iOS 发行
|
||||
|
||||
当前正式发行:6.28.0-particle.9
|
||||
当前正式发行:6.28.0-particle.10
|
||||
|
||||
- Renderer:Metal
|
||||
- Product / Module:MapLibre
|
||||
- Binary:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.9/MapLibre.dynamic.xcframework.zip
|
||||
- SHA-256:fbda91e09d53e3cd1362590bfc0410b6fb5f9fef4cf99796d4ab7b4b84c93562
|
||||
- Binary:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.10/MapLibre.dynamic.xcframework.zip
|
||||
- SHA-256:1050573be4d2e29852a6ddce27f828e869eb51626ca91aa0fb4154656a57cd90
|
||||
- SwiftPM:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git
|
||||
- Exact Version:6.28.0-particle.9
|
||||
- Source Commit:0e8c87fe49fb45c23a88d8f6cbf79dbc70df393c
|
||||
- Exact Version:6.28.0-particle.10
|
||||
- Source Commit:d5ce2006be99e44699bb8ea55380bc372da6c23d
|
||||
|
||||
构建命令固定为:
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"status": "PUBLISHED_PUBLIC_READBACK_PASS",
|
||||
"distributionVersion": "6.28.0-particle.9",
|
||||
"distributionVersion": "6.28.0-particle.10",
|
||||
"moduleName": "MapLibre",
|
||||
"artifact": {
|
||||
"fileName": "MapLibre.dynamic.xcframework.zip",
|
||||
"sizeBytes": 8321079,
|
||||
"sha256": "fbda91e09d53e3cd1362590bfc0410b6fb5f9fef4cf99796d4ab7b4b84c93562",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.9/MapLibre.dynamic.xcframework.zip"
|
||||
"sizeBytes": 8322801,
|
||||
"sha256": "1050573be4d2e29852a6ddce27f828e869eb51626ca91aa0fb4154656a57cd90",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.10/MapLibre.dynamic.xcframework.zip"
|
||||
},
|
||||
"sourceCommit": "0e8c87fe49fb45c23a88d8f6cbf79dbc70df393c",
|
||||
"sourceCommit": "d5ce2006be99e44699bb8ea55380bc372da6c23d",
|
||||
"swiftPackage": {
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git",
|
||||
"requirement": "exactVersion",
|
||||
"version": "6.28.0-particle.9",
|
||||
"version": "6.28.0-particle.10",
|
||||
"productName": "MapLibre"
|
||||
},
|
||||
"notes": "Metal renderer release built with bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic"
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"releaseState": "PUBLISHED_PUBLIC_READBACK_PASS",
|
||||
"particleRevision": 9,
|
||||
"sourceCommit": "0e8c87fe49fb45c23a88d8f6cbf79dbc70df393c",
|
||||
"generatedAtUtc": "2026-09-21T01:01:04.705768+00:00",
|
||||
"particleRevision": 10,
|
||||
"sourceCommit": "d5ce2006be99e44699bb8ea55380bc372da6c23d",
|
||||
"generatedAtUtc": "2026-09-21T10:10:08.877852+00:00",
|
||||
"reactNative": {
|
||||
"name": "@navsea/maplibre-particle-rn",
|
||||
"version": "13.4.1-particle.9",
|
||||
"version": "13.4.1-particle.10",
|
||||
"artifact": {
|
||||
"fileName": "navsea-maplibre-particle-rn-13.4.1-particle.9.tgz",
|
||||
"sizeBytes": 23411,
|
||||
"sha256": "98dc94e88dc69c06c9d99cc8ece3547f0489ba650c4a3d71814eab45e8720dcb",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.9.tgz"
|
||||
"fileName": "navsea-maplibre-particle-rn-13.4.1-particle.10.tgz",
|
||||
"sizeBytes": 23430,
|
||||
"sha256": "a8839c4603b9dec97bf92ff1116042eea98c4078fcd05f15befcd26e20c705b9",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.10.tgz"
|
||||
},
|
||||
"mapLibreReactNative": "11.3.6"
|
||||
},
|
||||
"android": {
|
||||
"version": "13.4.1-particle.9",
|
||||
"coordinate": "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.9",
|
||||
"version": "13.4.1-particle.10",
|
||||
"coordinate": "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.10",
|
||||
"renderer": "opengl",
|
||||
"artifact": {
|
||||
"fileName": "android-sdk-particle-opengl-13.4.1-particle.9.aar",
|
||||
"sizeBytes": 168214914,
|
||||
"sha256": "6114067c45685e2d20b3dd5115c480a39341177ac8608f86ec431562f649e395",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.9/android-sdk-particle-opengl-13.4.1-particle.9.aar"
|
||||
"fileName": "android-sdk-particle-opengl-13.4.1-particle.10.aar",
|
||||
"sizeBytes": 168205277,
|
||||
"sha256": "b126184186a5a96561e9a354a5959bc021ca0aedd72978f2fd856613b9df0f68",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.10/android-sdk-particle-opengl-13.4.1-particle.10.aar"
|
||||
},
|
||||
"ndk": "28.2.13676358",
|
||||
"gradle": "9.5.1",
|
||||
"jdk": "openjdk version \"17.0.20.1\" 2026-08-18"
|
||||
},
|
||||
"ios": {
|
||||
"distributionVersion": "6.28.0-particle.9",
|
||||
"distributionVersion": "6.28.0-particle.10",
|
||||
"renderer": "metal",
|
||||
"moduleName": "MapLibre",
|
||||
"artifact": {
|
||||
"fileName": "MapLibre.dynamic.xcframework.zip",
|
||||
"sizeBytes": 8321079,
|
||||
"sha256": "fbda91e09d53e3cd1362590bfc0410b6fb5f9fef4cf99796d4ab7b4b84c93562",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.9/MapLibre.dynamic.xcframework.zip"
|
||||
"sizeBytes": 8322801,
|
||||
"sha256": "1050573be4d2e29852a6ddce27f828e869eb51626ca91aa0fb4154656a57cd90",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.10/MapLibre.dynamic.xcframework.zip"
|
||||
},
|
||||
"swiftPackage": {
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git",
|
||||
"requirement": "exactVersion",
|
||||
"version": "6.28.0-particle.9",
|
||||
"version": "6.28.0-particle.10",
|
||||
"productName": "MapLibre"
|
||||
},
|
||||
"xcode": "Xcode 26.3 Build version 17C529 ",
|
||||
|
||||
@@ -138,23 +138,76 @@ bool vectorSnapshot(const DecodedFieldFrame& frame, const ParticleBinding& bindi
|
||||
|
||||
std::optional<MaterializedColorProjection> materializeColorProjection(const ColorProjection& projection) {
|
||||
if (!projection.t0Frame || !projection.t1Frame || !compatible(*projection.t0Frame, *projection.t1Frame)) return {};
|
||||
const auto* p0 = findPlane(*projection.t0Frame, projection.contentIdentity.binding.plane);
|
||||
const auto* p1 = findPlane(*projection.t1Frame, projection.contentIdentity.binding.plane);
|
||||
|
||||
const auto& binding = projection.contentIdentity.binding;
|
||||
if (binding.expectedValueClass == FieldValueClass::VelocityComponent) {
|
||||
const auto findComponent = [&](const DecodedFieldFrame& frame, const FieldComponent component) {
|
||||
const DecodedFieldPlane* result = nullptr;
|
||||
for (const auto& plane : frame.planes) {
|
||||
if (plane.valueClass != FieldValueClass::VelocityComponent || plane.unitID != binding.expectedUnitID ||
|
||||
plane.key.groupID != binding.plane.groupID || plane.key.component != component)
|
||||
continue;
|
||||
if (result) return static_cast<const DecodedFieldPlane*>(nullptr);
|
||||
result = &plane;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const auto* u0 = findComponent(*projection.t0Frame, FieldComponent::UEast);
|
||||
const auto* v0 = findComponent(*projection.t0Frame, FieldComponent::VNorth);
|
||||
const auto* u1 = findComponent(*projection.t1Frame, FieldComponent::UEast);
|
||||
const auto* v1 = findComponent(*projection.t1Frame, FieldComponent::VNorth);
|
||||
if (!u0 || !v0 || !u1 || !v1 || !u0->isInterpolationCompatibleWith(*u1) ||
|
||||
!v0->isInterpolationCompatibleWith(*v1))
|
||||
return {};
|
||||
|
||||
ParticleBinding vectorBinding{
|
||||
ParticleBindingMode::VectorComponents,
|
||||
binding.fieldKey,
|
||||
u0->key,
|
||||
v0->key,
|
||||
{},
|
||||
binding.expectedUnitID,
|
||||
};
|
||||
auto t0 = std::make_shared<ParticleFieldSnapshot>(
|
||||
baseSnapshot(*projection.t0Frame, u0->contentIdentity + "|" + v0->contentIdentity));
|
||||
auto t1 = std::make_shared<ParticleFieldSnapshot>(
|
||||
baseSnapshot(*projection.t1Frame, u1->contentIdentity + "|" + v1->contentIdentity));
|
||||
if (!vectorSnapshot(*projection.t0Frame, vectorBinding, *t0) ||
|
||||
!vectorSnapshot(*projection.t1Frame, vectorBinding, *t1))
|
||||
return {};
|
||||
return MaterializedColorProjection{std::move(t0),
|
||||
std::move(t1),
|
||||
projection.contentIdentity,
|
||||
projection.sampleState,
|
||||
MaterializedColorValueSource::VectorMagnitude};
|
||||
}
|
||||
|
||||
const auto* p0 = findPlane(*projection.t0Frame, binding.plane);
|
||||
const auto* p1 = findPlane(*projection.t1Frame, binding.plane);
|
||||
if (!p0 || !p1) return {};
|
||||
auto t0 = std::make_shared<ParticleFieldSnapshot>(baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0Plane));
|
||||
auto t1 = std::make_shared<ParticleFieldSnapshot>(baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1Plane));
|
||||
if (!scalarSnapshot(*projection.t0Frame, *p0, *t0) || !scalarSnapshot(*projection.t1Frame, *p1, *t1))
|
||||
return {};
|
||||
return MaterializedColorProjection{std::move(t0), std::move(t1), projection.contentIdentity, projection.sampleState};
|
||||
auto t0 = std::make_shared<ParticleFieldSnapshot>(
|
||||
baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0Plane));
|
||||
auto t1 = std::make_shared<ParticleFieldSnapshot>(
|
||||
baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1Plane));
|
||||
if (!scalarSnapshot(*projection.t0Frame, *p0, *t0) || !scalarSnapshot(*projection.t1Frame, *p1, *t1)) return {};
|
||||
return MaterializedColorProjection{std::move(t0),
|
||||
std::move(t1),
|
||||
projection.contentIdentity,
|
||||
projection.sampleState,
|
||||
MaterializedColorValueSource::Scalar};
|
||||
}
|
||||
|
||||
std::optional<MaterializedParticleProjection> materializeParticleProjection(const ParticleProjection& projection) {
|
||||
if (!projection.t0Frame || !projection.t1Frame || !compatible(*projection.t0Frame, *projection.t1Frame)) return {};
|
||||
auto t0 = std::make_shared<ParticleFieldSnapshot>(baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0FirstPlane));
|
||||
auto t1 = std::make_shared<ParticleFieldSnapshot>(baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1FirstPlane));
|
||||
auto t0 = std::make_shared<ParticleFieldSnapshot>(
|
||||
baseSnapshot(*projection.t0Frame, projection.contentIdentity.t0FirstPlane));
|
||||
auto t1 = std::make_shared<ParticleFieldSnapshot>(
|
||||
baseSnapshot(*projection.t1Frame, projection.contentIdentity.t1FirstPlane));
|
||||
if (!vectorSnapshot(*projection.t0Frame, projection.contentIdentity.binding, *t0) ||
|
||||
!vectorSnapshot(*projection.t1Frame, projection.contentIdentity.binding, *t1))
|
||||
return {};
|
||||
return MaterializedParticleProjection{std::move(t0), std::move(t1), projection.contentIdentity, projection.sampleState};
|
||||
return MaterializedParticleProjection{
|
||||
std::move(t0), std::move(t1), projection.contentIdentity, projection.sampleState};
|
||||
}
|
||||
} // namespace mbgl::particle
|
||||
|
||||
@@ -46,13 +46,13 @@ float scale(const particle::ParticleFieldSnapshot& field, style::FieldValueSourc
|
||||
float result = 1.0f;
|
||||
for (size_t index = 0; index < field.valid.size(); ++index)
|
||||
if (field.valid[index]) {
|
||||
if (valueSource == style::FieldValueSource::Scalar) {
|
||||
result = std::max(result, std::abs(field.scalar[index]));
|
||||
} else {
|
||||
result = std::max(result, std::abs(field.vector[index * 2]));
|
||||
result = std::max(result, std::abs(field.vector[index * 2 + 1]));
|
||||
if (valueSource == style::FieldValueSource::Scalar) {
|
||||
result = std::max(result, std::abs(field.scalar[index]));
|
||||
} else {
|
||||
result = std::max(result, std::abs(field.vector[index * 2]));
|
||||
result = std::max(result, std::abs(field.vector[index * 2 + 1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -62,17 +62,17 @@ std::shared_ptr<PremultipliedImage> image(const particle::ParticleFieldSnapshot&
|
||||
auto result = std::make_shared<PremultipliedImage>(Size{field.width, field.height});
|
||||
for (uint32_t y = 0; y < field.height; ++y)
|
||||
for (uint32_t x = 0; x < field.width; ++x) {
|
||||
const auto cell = static_cast<size_t>(y) * field.width + x;
|
||||
const auto pixel = (static_cast<size_t>(y) * field.width + x) * PremultipliedImage::channels;
|
||||
const auto cell = static_cast<size_t>(y) * field.width + x;
|
||||
const auto pixel = (static_cast<size_t>(y) * field.width + x) * PremultipliedImage::channels;
|
||||
const float first = valueSource == style::FieldValueSource::Scalar ? field.scalar[cell]
|
||||
: field.vector[cell * 2];
|
||||
const float second = valueSource == style::FieldValueSource::Scalar ? 0.0f : field.vector[cell * 2 + 1];
|
||||
result->data[pixel] = static_cast<uint8_t>(std::clamp(first / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f);
|
||||
const float second = valueSource == style::FieldValueSource::Scalar ? 0.0f : field.vector[cell * 2 + 1];
|
||||
result->data[pixel] = static_cast<uint8_t>(std::clamp(first / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f);
|
||||
result->data[pixel + 1] = static_cast<uint8_t>(std::clamp(second / fieldScale * .5f + .5f, 0.0f, 1.0f) *
|
||||
255.0f);
|
||||
result->data[pixel + 2] = 0;
|
||||
result->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0;
|
||||
}
|
||||
result->data[pixel + 2] = 0;
|
||||
result->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,8 @@ struct Resources {
|
||||
class Tweaker final : public gfx::DrawableTweaker {
|
||||
public:
|
||||
Tweaker(std::shared_ptr<Resources> resources_, int16_t wrap_)
|
||||
: resources(std::move(resources_)), wrap(wrap_) {}
|
||||
: resources(std::move(resources_)),
|
||||
wrap(wrap_) {}
|
||||
void init(gfx::Drawable&) override {}
|
||||
void execute(gfx::Drawable& drawable, PaintParameters& parameters) override {
|
||||
drawable.setTexture(resources->texture0, shaders::idParticleGpuValidationPreviousTexture);
|
||||
@@ -154,8 +155,8 @@ bool uploadField(gfx::Context& context,
|
||||
if (!outputTexture) return false;
|
||||
outputTexture->setImage(outputImage);
|
||||
outputTexture->setSamplerConfiguration({.filter = gfx::TextureFilterType::Linear,
|
||||
.wrapU = gfx::TextureWrapType::Clamp,
|
||||
.wrapV = gfx::TextureWrapType::Clamp});
|
||||
.wrapU = gfx::TextureWrapType::Clamp,
|
||||
.wrapV = gfx::TextureWrapType::Clamp});
|
||||
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::FieldTextureUpload);
|
||||
return true;
|
||||
}
|
||||
@@ -199,6 +200,7 @@ void RenderWeatherColorLayer::prepare(const LayerPrepareParameters& parameters)
|
||||
if (!parameters.source || parameters.source->baseImpl->type != style::SourceType::RasterArray) return;
|
||||
const auto& source = static_cast<const RenderRasterArraySource&>(*parameters.source);
|
||||
sourceUsesV4Projection = false;
|
||||
sourceProjectionUsesVectorMagnitude = false;
|
||||
sourceProjectionMix = 0.0f;
|
||||
if (source.impl().v4RendererState.color) {
|
||||
const auto& projection = *source.impl().v4RendererState.color;
|
||||
@@ -207,6 +209,8 @@ void RenderWeatherColorLayer::prepare(const LayerPrepareParameters& parameters)
|
||||
sourceProjectionMix = projection.sampleState.mix;
|
||||
sourceTimeline.reset();
|
||||
sourceUsesV4Projection = true;
|
||||
sourceProjectionUsesVectorMagnitude = projection.valueSource ==
|
||||
particle::MaterializedColorValueSource::VectorMagnitude;
|
||||
return;
|
||||
}
|
||||
sourceTimeline = source.impl().timeline;
|
||||
@@ -242,10 +246,11 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders,
|
||||
resources.reset();
|
||||
return;
|
||||
}
|
||||
// V4 ColorBinding has already selected and materialized its scalar plane;
|
||||
// it must not be reinterpreted through the legacy style value source.
|
||||
// Encoded ingress is decoder-only. After materialization, WeatherColor
|
||||
// consumes the same canonical scalar/vector semantics as the legacy path.
|
||||
const auto valueSource = sourceUsesV4Projection
|
||||
? style::FieldValueSource::Scalar
|
||||
? (sourceProjectionUsesVectorMagnitude ? style::FieldValueSource::VectorMagnitude
|
||||
: style::FieldValueSource::Scalar)
|
||||
: static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).valueSource;
|
||||
if (!supportsValueSource(*sourceField, valueSource) || !supportsValueSource(*sourceNextField, valueSource)) {
|
||||
if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables();
|
||||
|
||||
@@ -34,6 +34,7 @@ private:
|
||||
std::shared_ptr<style::RasterArrayTimelineRuntime> sourceTimeline;
|
||||
float sourceProjectionMix = 0.0f;
|
||||
bool sourceUsesV4Projection = false;
|
||||
bool sourceProjectionUsesVectorMagnitude = false;
|
||||
};
|
||||
|
||||
} // namespace mbgl
|
||||
|
||||
@@ -39,21 +39,21 @@ RasterArrayEncodedFieldSubmitResult mapEncodedFieldStoreResult(const particle::F
|
||||
switch (result) {
|
||||
case particle::FieldSeriesSubmitResult::Inserted:
|
||||
return RasterArrayEncodedFieldSubmitResult::AcceptedInserted;
|
||||
case particle::FieldSeriesSubmitResult::ReplacedRevision:
|
||||
return RasterArrayEncodedFieldSubmitResult::AcceptedReplacedRevision;
|
||||
case particle::FieldSeriesSubmitResult::ReplacedRevision:
|
||||
return RasterArrayEncodedFieldSubmitResult::AcceptedReplacedRevision;
|
||||
case particle::FieldSeriesSubmitResult::Duplicate:
|
||||
return RasterArrayEncodedFieldSubmitResult::Duplicate;
|
||||
case particle::FieldSeriesSubmitResult::RejectedInvalidFieldKey:
|
||||
return RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey;
|
||||
case particle::FieldSeriesSubmitResult::RejectedIdentityConflict:
|
||||
return RasterArrayEncodedFieldSubmitResult::RejectedIdentityConflict;
|
||||
case particle::FieldSeriesSubmitResult::RejectedStaleRevision:
|
||||
return RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision;
|
||||
case particle::FieldSeriesSubmitResult::RejectedIncompatibleFrame:
|
||||
case particle::FieldSeriesSubmitResult::RejectedInvalidFrame:
|
||||
return RasterArrayEncodedFieldSubmitResult::RejectedIncompatible;
|
||||
case particle::FieldSeriesSubmitResult::RejectedCapacity:
|
||||
return RasterArrayEncodedFieldSubmitResult::RejectedCapacity;
|
||||
case particle::FieldSeriesSubmitResult::RejectedInvalidFieldKey:
|
||||
return RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey;
|
||||
case particle::FieldSeriesSubmitResult::RejectedIdentityConflict:
|
||||
return RasterArrayEncodedFieldSubmitResult::RejectedIdentityConflict;
|
||||
case particle::FieldSeriesSubmitResult::RejectedStaleRevision:
|
||||
return RasterArrayEncodedFieldSubmitResult::RejectedStaleRevision;
|
||||
case particle::FieldSeriesSubmitResult::RejectedIncompatibleFrame:
|
||||
case particle::FieldSeriesSubmitResult::RejectedInvalidFrame:
|
||||
return RasterArrayEncodedFieldSubmitResult::RejectedIncompatible;
|
||||
case particle::FieldSeriesSubmitResult::RejectedCapacity:
|
||||
return RasterArrayEncodedFieldSubmitResult::RejectedCapacity;
|
||||
}
|
||||
return RasterArrayEncodedFieldSubmitResult::RejectedIncompatible;
|
||||
}
|
||||
@@ -66,8 +66,8 @@ template <class Projection>
|
||||
bool sameProjection(const std::shared_ptr<const Projection>& a, const std::shared_ptr<const Projection>& b) {
|
||||
if (static_cast<bool>(a) != static_cast<bool>(b)) return false;
|
||||
return !a || (a->contentIdentity == b->contentIdentity && a->sampleState.mix == b->sampleState.mix &&
|
||||
a->sampleState.requestedForecastTimeMs == b->sampleState.requestedForecastTimeMs &&
|
||||
a->t0 && b->t0 && a->t1 && b->t1 && sameSnapshot(*a->t0, *b->t0) && sameSnapshot(*a->t1, *b->t1));
|
||||
a->sampleState.requestedForecastTimeMs == b->sampleState.requestedForecastTimeMs && a->t0 && b->t0 &&
|
||||
a->t1 && b->t1 && sameSnapshot(*a->t0, *b->t0) && sameSnapshot(*a->t1, *b->t1));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -207,7 +207,7 @@ RasterArrayCadenceAdvance RasterArrayTimelineRuntime::advanceIfDue(const std::ch
|
||||
lastCadenceLog = now;
|
||||
Log::Info(Event::Render,
|
||||
"WeatherCadence: targetFps=" + std::to_string(static_cast<uint8_t>(targetFps_)) +
|
||||
" weatherTicks=" + std::to_string(cadenceTickCount));
|
||||
" weatherTicks=" + std::to_string(cadenceTickCount));
|
||||
}
|
||||
if (lastTick == std::chrono::steady_clock::time_point{}) {
|
||||
lastTick = now;
|
||||
@@ -341,8 +341,8 @@ bool RasterArrayTimelineRuntime::seekTimestamp(const uint64_t timestamp) {
|
||||
const auto previousState = state;
|
||||
const auto upper = std::upper_bound(
|
||||
entries.begin(), entries.end(), timestamp, [](uint64_t value, const Entry& entry) {
|
||||
return value < entry.timestamp;
|
||||
});
|
||||
return value < entry.timestamp;
|
||||
});
|
||||
if (upper == entries.begin()) return false;
|
||||
if (upper == entries.end()) {
|
||||
t0 = entries.size() - 2;
|
||||
@@ -353,7 +353,7 @@ bool RasterArrayTimelineRuntime::seekTimestamp(const uint64_t timestamp) {
|
||||
const auto duration = static_cast<float>(entries[t0 + 1].timestamp - entries[t0].timestamp);
|
||||
mix = duration > 0.0f ? static_cast<float>(timestamp - entries[t0].timestamp) / duration : 0.0f;
|
||||
state = previousState == RasterArrayPlaybackState::Playing ? RasterArrayPlaybackState::Playing
|
||||
: RasterArrayPlaybackState::Paused;
|
||||
: RasterArrayPlaybackState::Paused;
|
||||
}
|
||||
if (t0 != previousT0) ++windowRevision;
|
||||
lastTick = std::chrono::steady_clock::now();
|
||||
@@ -428,8 +428,7 @@ RasterArrayEncodedFieldSubmitResult RasterArraySource::submitEncodedField(
|
||||
// checks metadata, but validate it here to make the Host ordering explicit.
|
||||
if (!validEncodedFieldKey(fieldKey)) return RasterArrayEncodedFieldSubmitResult::RejectedInvalidFieldKey;
|
||||
if (!validRuntimeMetadata(runtimeMetadata)) return RasterArrayEncodedFieldSubmitResult::RejectedDecode;
|
||||
auto ingress = particle::ingress::EncodedFieldIngress::decodeAndValidate(
|
||||
runtimeMetadata, &contract, rawQbinBytes);
|
||||
auto ingress = particle::ingress::EncodedFieldIngress::decodeAndValidate(runtimeMetadata, &contract, rawQbinBytes);
|
||||
if (ingress.code == particle::ingress::EncodedFieldIngressCode::DecodeRejected)
|
||||
return RasterArrayEncodedFieldSubmitResult::RejectedDecode;
|
||||
if (ingress.code != particle::ingress::EncodedFieldIngressCode::Accepted)
|
||||
@@ -499,16 +498,19 @@ void RasterArraySource::refreshV4RendererState() {
|
||||
if (colorBinding) {
|
||||
const auto resolved = resolveColorProjection();
|
||||
if (const auto materialized = resolved.accepted() ? particle::materializeColorProjection(*resolved.projection)
|
||||
: std::nullopt)
|
||||
: std::nullopt)
|
||||
next.color = std::make_shared<const particle::MaterializedColorProjection>(std::move(*materialized));
|
||||
if (next.color) particle::incrementRendererDebugCounter(particle::RendererDebugCounter::V4ProjectionMaterialize);
|
||||
if (next.color)
|
||||
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::V4ProjectionMaterialize);
|
||||
}
|
||||
if (particleBinding) {
|
||||
const auto resolved = resolveParticleProjection();
|
||||
if (const auto materialized = resolved.accepted() ? particle::materializeParticleProjection(*resolved.projection)
|
||||
: std::nullopt)
|
||||
if (const auto materialized = resolved.accepted()
|
||||
? particle::materializeParticleProjection(*resolved.projection)
|
||||
: std::nullopt)
|
||||
next.particle = std::make_shared<const particle::MaterializedParticleProjection>(std::move(*materialized));
|
||||
if (next.particle) particle::incrementRendererDebugCounter(particle::RendererDebugCounter::V4ProjectionMaterialize);
|
||||
if (next.particle)
|
||||
particle::incrementRendererDebugCounter(particle::RendererDebugCounter::V4ProjectionMaterialize);
|
||||
}
|
||||
const auto& current = impl().v4RendererState;
|
||||
if (sameProjection(current.color, next.color) && sameProjection(current.particle, next.particle)) return;
|
||||
@@ -691,7 +693,7 @@ void RasterArraySource::tryLoad() {
|
||||
" dataVersion=" + std::to_string(loadedField.dataVersion));
|
||||
if (impl().nextField) {
|
||||
Log::Info(Event::Render,
|
||||
"ParticleField: source loaded next fieldId=" + impl().nextField->fieldID +
|
||||
"ParticleField: source loaded next fieldId=" + impl().nextField->fieldID +
|
||||
" generation=" + std::to_string(impl().nextField->generation) +
|
||||
" dataVersion=" + std::to_string(impl().nextField->dataVersion));
|
||||
}
|
||||
@@ -715,7 +717,7 @@ void RasterArraySource::tryLoadRuntimeField(const size_t index) {
|
||||
candidate.snapshot = particle::OfflineFixtureParticleAdapter::load(candidate.descriptor.fixtureID,
|
||||
*candidate.metadata,
|
||||
*candidate.fieldBytes,
|
||||
++nextRasterArrayFieldGeneration);
|
||||
++nextRasterArrayFieldGeneration);
|
||||
Log::Info(Event::Render,
|
||||
"ParticleField: runtime candidate ready fieldId=" + candidate.snapshot->fieldID +
|
||||
" dataVersion=" + std::to_string(candidate.snapshot->dataVersion));
|
||||
@@ -725,8 +727,8 @@ void RasterArraySource::tryLoadRuntimeField(const size_t index) {
|
||||
"ParticleField: event=ADAPTER_REJECTED sourceId=" + getID() +
|
||||
" candidateIndex=" + std::to_string(index) + " fieldId=" + candidate.descriptor.fixtureID +
|
||||
" generation=" + std::to_string(candidate.snapshot ? candidate.snapshot->generation : 0) +
|
||||
" validTime=" + std::to_string(candidate.snapshot ? candidate.snapshot->validTime : 0) +
|
||||
" error=" + error.what());
|
||||
" validTime=" + std::to_string(candidate.snapshot ? candidate.snapshot->validTime : 0) +
|
||||
" error=" + error.what());
|
||||
observer->onSourceError(*this, std::make_exception_ptr(std::runtime_error(error.what())));
|
||||
}
|
||||
}
|
||||
@@ -747,9 +749,9 @@ void RasterArraySource::tryLoadLocalField(const size_t index) {
|
||||
candidate.snapshot = std::move(*result.snapshot);
|
||||
Log::Info(Event::Render,
|
||||
"ParticleField: event=PRODUCTION_FEED_LOAD fieldId=" + candidate.snapshot->fieldID +
|
||||
" generation=" + std::to_string(candidate.snapshot->generation) +
|
||||
" validTime=" + std::to_string(candidate.snapshot->validTime) +
|
||||
" payloadBytes=" + std::to_string(candidate.fieldBytes->size()));
|
||||
" generation=" + std::to_string(candidate.snapshot->generation) +
|
||||
" validTime=" + std::to_string(candidate.snapshot->validTime) +
|
||||
" payloadBytes=" + std::to_string(candidate.fieldBytes->size()));
|
||||
tryInitializeTimeline();
|
||||
}
|
||||
|
||||
@@ -796,7 +798,7 @@ void RasterArraySource::tryInitializeTimeline() {
|
||||
auto field = *runtimeFields[index].snapshot;
|
||||
entries.push_back({std::make_shared<particle::ParticleFieldSnapshot>(std::move(field)),
|
||||
runtimeFields[index].descriptor.timestamp ? runtimeFields[index].descriptor.timestamp
|
||||
: 80000 + static_cast<uint64_t>(index) * 40000});
|
||||
: 80000 + static_cast<uint64_t>(index) * 40000});
|
||||
}
|
||||
try {
|
||||
timeline = std::make_shared<RasterArrayTimelineRuntime>(std::move(entries));
|
||||
@@ -1112,7 +1114,7 @@ bool RasterArraySource::seekRuntimeTimelineToTimestamp(const uint64_t timestamp)
|
||||
if (!accepted) {
|
||||
Log::Warning(Event::Render,
|
||||
"WeatherRuntime: event=SEEK_TIMESTAMP_REJECTED reason=OUT_OF_RANGE sourceId=" + getID() +
|
||||
" timestamp=" + std::to_string(timestamp));
|
||||
" timestamp=" + std::to_string(timestamp));
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
|
||||
@@ -124,8 +124,8 @@ particle::ResolvedFieldProfileContract windContract() {
|
||||
result.profileVersion = 1;
|
||||
result.requiredPlanes = {
|
||||
{1, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::UEast, true},
|
||||
{2, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::VNorth, true},
|
||||
{3, particle::FieldValueClass::Validity, 0, 0, particle::FieldComponent::None, true}};
|
||||
{2, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::VNorth, true},
|
||||
{3, particle::FieldValueClass::Validity, 0, 0, particle::FieldComponent::None, true}};
|
||||
return result;
|
||||
}
|
||||
particle::ResolvedFieldProfileContract waveContract() {
|
||||
@@ -414,6 +414,7 @@ TEST(Source, RasterArrayV4RendererStateMaterializesBindingsAndMixWithoutTouching
|
||||
EXPECT_EQ(particles.t0->vector, (std::vector<float>{1, 5, 2, 6, 3, 7, 4, 8}));
|
||||
EXPECT_EQ(particles.t0->valid, (std::vector<uint8_t>{1, 1, 1, 1}));
|
||||
EXPECT_EQ(color.t0->scalar, (std::vector<float>{1, 2, 3, 4}));
|
||||
EXPECT_EQ(color.valueSource, particle::MaterializedColorValueSource::Scalar);
|
||||
const auto particleIdentity = particles.contentIdentity;
|
||||
const auto retainedParticleT0 = particles.t0;
|
||||
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 4));
|
||||
@@ -429,6 +430,46 @@ TEST(Source, RasterArrayV4RendererStateMaterializesBindingsAndMixWithoutTouching
|
||||
EXPECT_FALSE(source.impl().v4RendererState.particle);
|
||||
EXPECT_FALSE(source.impl().v4RendererState.color);
|
||||
}
|
||||
TEST(Source, RasterArrayV4VelocityColorReconstructsDecodedUVIndependentlyOfParticles) {
|
||||
style::RasterArraySource source{"v4-vector-color"};
|
||||
submit(source, "wind", "wind", Base, 1);
|
||||
submit(source, "wind", "wind", Base + Hour, 2);
|
||||
|
||||
source.setColorBinding(
|
||||
{"wind", {1, 1, particle::FieldComponent::UEast}, particle::FieldValueClass::VelocityComponent, 1});
|
||||
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2));
|
||||
|
||||
ASSERT_TRUE(source.impl().v4RendererState.color);
|
||||
EXPECT_FALSE(source.impl().v4RendererState.particle);
|
||||
const auto& colorWithoutParticles = *source.impl().v4RendererState.color;
|
||||
EXPECT_EQ(colorWithoutParticles.valueSource, particle::MaterializedColorValueSource::VectorMagnitude);
|
||||
ASSERT_TRUE(colorWithoutParticles.t0);
|
||||
ASSERT_TRUE(colorWithoutParticles.t1);
|
||||
EXPECT_TRUE(colorWithoutParticles.t0->hasVector());
|
||||
EXPECT_FALSE(colorWithoutParticles.t0->hasScalar());
|
||||
EXPECT_EQ(colorWithoutParticles.t0->vector, (std::vector<float>{1, 5, 2, 6, 3, 7, 4, 8}));
|
||||
EXPECT_EQ(colorWithoutParticles.t0->valid, (std::vector<uint8_t>{1, 1, 1, 1}));
|
||||
EXPECT_FLOAT_EQ(colorWithoutParticles.sampleState.mix, 0.5f);
|
||||
|
||||
source.setParticleBinding({particle::ParticleBindingMode::VectorComponents,
|
||||
"wind",
|
||||
{1, 1, particle::FieldComponent::UEast},
|
||||
{2, 1, particle::FieldComponent::VNorth},
|
||||
{},
|
||||
1});
|
||||
ASSERT_TRUE(source.impl().v4RendererState.color);
|
||||
ASSERT_TRUE(source.impl().v4RendererState.particle);
|
||||
const auto& color = *source.impl().v4RendererState.color;
|
||||
const auto& particles = *source.impl().v4RendererState.particle;
|
||||
EXPECT_EQ(color.valueSource, particle::MaterializedColorValueSource::VectorMagnitude);
|
||||
EXPECT_EQ(color.t0->vector, particles.t0->vector);
|
||||
EXPECT_EQ(color.t1->vector, particles.t1->vector);
|
||||
EXPECT_EQ(color.t0->valid, particles.t0->valid);
|
||||
EXPECT_FALSE(source.impl().field);
|
||||
EXPECT_FALSE(source.impl().nextField);
|
||||
EXPECT_FALSE(source.impl().timeline);
|
||||
}
|
||||
|
||||
TEST(Source, RasterArrayV4MaterializerUsesExplicitLittleEndianScaleOffsetAndStableRevision) {
|
||||
const auto t0 = materializerFrame("scalar",
|
||||
Base,
|
||||
@@ -457,8 +498,8 @@ TEST(Source, RasterArrayV4MaterializerUsesExplicitLittleEndianScaleOffsetAndStab
|
||||
particle::ColorBinding binding{
|
||||
"scalar", {99, 0, particle::FieldComponent::None}, particle::FieldValueClass::Scalar, 77};
|
||||
particle::ColorProjection projection;
|
||||
projection.contentIdentity =
|
||||
{binding, {"scalar", Base}, {"scalar", Base + Hour}, "plane-t0", "plane-t1", "validity-t0", "validity-t1"};
|
||||
projection.contentIdentity = {
|
||||
binding, {"scalar", Base}, {"scalar", Base + Hour}, "plane-t0", "plane-t1", "validity-t0", "validity-t1"};
|
||||
projection.sampleState = {Base + Hour / 2, 0.5f};
|
||||
projection.t0Frame = t0;
|
||||
projection.t1Frame = t1;
|
||||
|
||||
@@ -102,8 +102,10 @@ build_release() {
|
||||
(cd "$ROOT" && bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic)
|
||||
IOS_SOURCE="$ROOT/bazel-out/darwin_x86_64-fastbuild/bin/platform/ios/$IOS_ZIP_NAME"
|
||||
[[ -f "$IOS_SOURCE" ]] || die "iOS XCFramework zip missing"
|
||||
unzip -Z1 "$IOS_SOURCE" | grep -Fq "/ios-arm64/" || die "iOS XCFramework missing ios-arm64 device slice"
|
||||
if unzip -Z1 "$IOS_SOURCE" | grep -Eiq "simulator|ios-arm64_x86_64"; then
|
||||
local ios_listing
|
||||
ios_listing="$(unzip -Z1 "$IOS_SOURCE")"
|
||||
grep -Fq "/ios-arm64/" <<<"$ios_listing" || die "iOS XCFramework missing ios-arm64 device slice"
|
||||
if grep -Eiq "simulator|ios-arm64_x86_64" <<<"$ios_listing"; then
|
||||
die "iOS XCFramework unexpectedly contains simulator slice"
|
||||
fi
|
||||
cp "$IOS_SOURCE" "$ARTIFACT_DIR/$IOS_ZIP_NAME"
|
||||
|
||||
Reference in New Issue
Block a user