feat: add V4 QBIN golden fixture test host
This commit is contained in:
@@ -3,6 +3,7 @@ import { Button, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, fi
|
||||
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 { weatherTestStyle } from "./src/weatherStyle";
|
||||
|
||||
const initialSnapshot: RuntimeSnapshot = { state: "WAITING_FOR_FIELDS", playbackState: "PAUSE", targetFps: "AUTO", disposed: false };
|
||||
@@ -20,6 +21,10 @@ export default function App() {
|
||||
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 [weatherVisible, setWeatherVisible] = useState(true);
|
||||
const [particleVisible, setParticleVisible] = useState(true);
|
||||
const [visual, setVisual] = useState(initialVisual);
|
||||
@@ -60,6 +65,36 @@ export default function App() {
|
||||
} catch (error) { setTransport(`ERROR: ${String(error)}`); setResult(`LOAD ${dataset.id}: ${String(error)}`); }
|
||||
}, [reactTag, dataset]);
|
||||
|
||||
const loadV4Dataset = useCallback(async () => {
|
||||
if (reactTag == null) return setResult("MAP_NOT_READY: wait for GSI/style ready");
|
||||
try {
|
||||
setV4Transport("LOADING: clear → binding → T0 → T1 → midpoint");
|
||||
setV4Frames({ T0: "PENDING", T1: "PENDING" });
|
||||
setV4Bindings("PENDING");
|
||||
const clear = await FieldRuntime.clearAllEncodedFields(reactTag);
|
||||
if (clear !== "ACCEPTED") throw new Error(`clearAllEncodedFields expected ACCEPTED, got ${clear}`);
|
||||
const color = await FieldRuntime.setEncodedColorBinding(reactTag, v4Dataset.colorBinding);
|
||||
if (color !== "ACCEPTED") throw new Error(`ColorBinding expected ACCEPTED, got ${color}`);
|
||||
const particle = v4Dataset.particleBinding == null ? "N/A" : await FieldRuntime.setEncodedParticleBinding(reactTag, 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));
|
||||
for (const frame of localFrames) {
|
||||
const submit = await FieldRuntime.submitEncodedField(reactTag, v4Dataset.fieldKey, frame.metadata, v4Dataset.profileContract, frame.qbinFileUri);
|
||||
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(reactTag, v4Dataset.midpointMs);
|
||||
if (midpoint !== "ACCEPTED") throw new Error(`midpoint seek expected ACCEPTED, got ${midpoint}`);
|
||||
await refresh(reactTag);
|
||||
setV4Transport("READY: T0/T1 ACCEPTED_INSERTED; midpoint command ACCEPTED (mix=0.5 is not exposed)");
|
||||
setResult(`${v4Dataset.title}: V4 raw QBIN load accepted`);
|
||||
} catch (error) {
|
||||
setV4Transport(`ERROR: ${String(error)}`);
|
||||
setResult(`LOAD V4 ${v4Dataset.id}: ${String(error)}`);
|
||||
}
|
||||
}, [reactTag, refresh, v4Dataset]);
|
||||
|
||||
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([
|
||||
@@ -83,6 +118,11 @@ export default function App() {
|
||||
<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()} />
|
||||
<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>;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,24 @@
|
||||
|
||||
这是 MapLibreParticleLab 内的独立 React Native 开发宿主,不是 NavSea 业务应用。它只有一个 `MLRNMapView` 与 WeatherRuntime 控制页,用于证明低频控制面:React 的 `reactTag` 经薄 Android 模块定位该 View,再定位该 View 的 `MapLibreMap` 与 map-owned `WeatherRuntime`。
|
||||
|
||||
## STEP-079 V4 QBIN Golden Fixture
|
||||
|
||||
`assets/qbin-golden/` 固化了 WeatherV2 的 8 个未压缩 QBIN V2 fixture:Wind、Wave、Current、Rain 各 T0/T1。它们从冻结来源 `/root/sourceserver/weatherV2/_artifacts/MapLibre_QBIN_V2_单元测试数据_v1/` 复制而来;运行时不依赖该来源、HTTP、CDN、NAS 或 gzip。每个 asset 经 `expo-asset` materialize 为本地 `file://` URI,再仅通过 `FieldRuntime.submitEncodedField(...)` 提交。
|
||||
|
||||
`src/qbinGoldenFixtures.ts` 保留 profile contract、稳定 field identity、validTime、size、SHA-256 和真实 binding。Wind/Current 的 ColorBinding 是 U `VELOCITY_COMPONENT`(semantic 1/group 1/unit 1),不是伪造 `SCALAR`;Wave 是 height `SCALAR` 与 direction particle;Rain 的 particle 明确为 N/A。页面保留全部 V3 控制,并新增独立 V4 dataset/load、T0/T1 结果、binding、seek、PLAY/PAUSE、正速度和 clear 控制。midpoint 只报告 command accepted,RN API 未暴露 mix,不能据此宣称观测到 0.5。
|
||||
|
||||
开发机可用以下只读校验核验 source 与 bundled 文件的 bytes/SHA-256(不属于运行时依赖):
|
||||
|
||||
```bash
|
||||
python3 integration/react-native/MapLibreParticleRNTestHost/scripts/verify-qbin-golden-fixtures.py
|
||||
```
|
||||
|
||||
## 当前 V3-08 版本与首次 prebuild 边界
|
||||
|
||||
- 当前验收目标是 RN/TS `13.4.1-particle.3` 配合 Native `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.2`。`.2` 是 STEP-069 v2 multi-plane 新 artifact;不得复用或覆盖 `.1`。
|
||||
- 本地 legacy `@maplibre-particle/weather-runtime-bridge` 已移除;`@navsea/maplibre-particle-rn@13.4.1-particle.3` 是唯一的 `WeatherRuntimeBridge` 提供者。
|
||||
- 必须先由用户手工构建并 Maven Local publish Native `.2`,再对 Test Host 执行 clean prebuild、build、install 与启动。当前仅完成坐标准备,AAR 尚未构建、发布或验收。
|
||||
- Metro 当前 binary asset extension 为 `.f32` 与 `.u8`。
|
||||
- Metro 当前 binary asset extension 为 `.f32`、`.u8` 与 `.qbin`。
|
||||
|
||||
- package manager 为 npm(`packageManager: npm@10.9.4`),锁文件为 `package-lock.json`;当前环境的 `npm ci --dry-run --ignore-scripts` 无须 `--legacy-peer-deps` 即可解析。
|
||||
- package 明确固定 React Native `0.85.2`、React `19.2.3`、Expo `56.0.4`、`expo-file-system` `56.0.11`、TypeScript `5.9.3`、MLRN `11.3.6`;`app.json` 明确请求 New Architecture。Hermes 的最终启用状态必须在首次 Android prebuild 后检查生成工程。
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
|
||||
const config = getDefaultConfig(__dirname);
|
||||
config.resolver.assetExts = [...new Set([...config.resolver.assetExts, "f32", "u8"])];
|
||||
config.resolver.assetExts = [...new Set([...config.resolver.assetExts, "f32", "u8", "qbin"])];
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail closed on the frozen WeatherV2 QBIN manifest and its bundled raw copies."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPOSITORY = Path(__file__).resolve().parents[4]
|
||||
SOURCE = Path("/root/sourceserver/weatherV2/_artifacts/MapLibre_QBIN_V2_单元测试数据_v1")
|
||||
BUNDLED = REPOSITORY / "integration/react-native/MapLibreParticleRNTestHost/assets/qbin-golden"
|
||||
DESTINATION_LAYER = {"wind": "wind", "wave": "wave", "tide": "current", "rain": "rain"}
|
||||
|
||||
|
||||
def digest(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
manifest = json.loads((SOURCE / "清单.json").read_text(encoding="utf-8"))
|
||||
checked = 0
|
||||
for source_layer, destination_layer in DESTINATION_LAYER.items():
|
||||
frames = manifest["layers"][source_layer]["frames"]
|
||||
if len(frames) != 2:
|
||||
raise RuntimeError(f"{source_layer}: expected exactly two frozen frames")
|
||||
for index, frame in enumerate(frames):
|
||||
label = "T0" if index == 0 else "T1"
|
||||
source = SOURCE / source_layer / frame["file"]
|
||||
bundled = BUNDLED / destination_layer / f"{label}.qbin"
|
||||
expected_bytes = frame["raw_bytes"]
|
||||
expected_sha256 = frame["sha256"]
|
||||
for name, path in (("source", source), ("bundled", bundled)):
|
||||
if not path.is_file() or path.stat().st_size != expected_bytes or digest(path) != expected_sha256:
|
||||
raise RuntimeError(f"{source_layer}/{frame['target_time']} {name} does not match frozen raw QBIN manifest")
|
||||
checked += 1
|
||||
if checked != 8:
|
||||
raise RuntimeError(f"expected eight frames, checked {checked}")
|
||||
print("QBIN_FIXTURE_SOURCE_AND_COPY=PASS (8 raw files)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -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-golden/wind/T0.qbin"), "wind/frames/2026091219.qbin", 1627552, "aca8408cc8f723fe8e92ae6ce7695edcb3482167d1eb00d516812f97a70f6371"),
|
||||
frame("T1", "fixture-wind", 1789243200000, require("../assets/qbin-golden/wind/T1.qbin"), "wind/frames/2026091220.qbin", 1627552, "cb3b46c5857ba258a7e0804c9fed5c76f481e567b88131c2d98cfe9c5e7d5adb"),
|
||||
] as const;
|
||||
const waveFrames = [
|
||||
frame("T0", "fixture-wave", 1789239600000, require("../assets/qbin-golden/wave/T0.qbin"), "wave/frames/2026091219.qbin", 2416592, "ffe43ce36529602bbfbabc0c15742502aadf4277fa0201cfa93d90327fb3bad3"),
|
||||
frame("T1", "fixture-wave", 1789243200000, require("../assets/qbin-golden/wave/T1.qbin"), "wave/frames/2026091220.qbin", 2416592, "28463578cab1dbf9b30949ae9ea1780441c9b396e63a8a1ebd0a13090c222a8d"),
|
||||
] as const;
|
||||
const currentFrames = [
|
||||
frame("T0", "fixture-current", 1789174800000, require("../assets/qbin-golden/current/T0.qbin"), "tide/frames/2026091201.qbin", 1085824, "20bf1f261550ce160b533e6929db66b95d287e8f93b10b3ce9cedb6d8cd37b9f"),
|
||||
frame("T1", "fixture-current", 1789178400000, require("../assets/qbin-golden/current/T1.qbin"), "tide/frames/2026091202.qbin", 1085824, "849c3c451dd6b6b810aa519e2c5dedead6ce2804b84c05b0fb6fd03c737eb30a"),
|
||||
] as const;
|
||||
const rainFrames = [
|
||||
frame("T0", "fixture-rain", 1789239600000, require("../assets/qbin-golden/rain/T0.qbin"), "rain/frames/2026091219.qbin", 559440, "c25b6a967af030f7cef9ea790d692a7cf298be4cb5691aaa453dc753c5f2e237"),
|
||||
frame("T1", "fixture-rain", 1789243200000, require("../assets/qbin-golden/rain/T1.qbin"), "rain/frames/2026091220.qbin", 559440, "bc030558e9304cd3a883cedbb15fbbe036508669491a7894c8b6f8aef7a35793"),
|
||||
] 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 });
|
||||
|
||||
/** Frozen WeatherV2 QBIN V2 manifest projected into the frozen V4 public contract. */
|
||||
export const qbinGoldenDatasets: readonly V4Dataset[] = [
|
||||
{ id: "wind", title: "Wind QBIN Golden (wind/1)", 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 Golden (wave/1)", 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 Golden (current/1)", 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 Golden (rain/1)", 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user