From 9e20233288265ccd436126ca84139996981fc896 Mon Sep 17 00:00:00 2001 From: tei Date: Fri, 18 Sep 2026 22:42:47 +0900 Subject: [PATCH] docs: close STEP-083 ios RN validation --- ...time_TurboModule_Fabric与XCFramework消费_v1.md | 16 ++- ...le_Fabric与XCFramework消费_Codex完成报告_v2.md | 6 + ...le_Fabric与XCFramework消费_Codex完成报告_v3.md | 35 ++++++ .../MapLibreParticleRNTestHost/App.tsx | 106 ++++++++++++++---- 粒子开发记录.md | 6 + 5 files changed, 138 insertions(+), 31 deletions(-) create mode 100644 docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v3.md diff --git a/docs/50_Step记录/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_v1.md b/docs/50_Step记录/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_v1.md index 61ee9eee5..8746bcda5 100644 --- a/docs/50_Step记录/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_v1.md +++ b/docs/50_Step记录/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_v1.md @@ -1,6 +1,6 @@ # STEP-083 iOS RN 统一 FieldRuntime / TurboModule / Fabric 与 XCFramework 消费 v1 -状态:`IMPLEMENTED / IOS_CORE_PATH_PASS / WAITING_FINAL_CONTROL_REGRESSION` +状态:`CLOSED / PASS` ## 基线与范围 @@ -22,10 +22,6 @@ - Test Host 消费的是 single MapLibre SPM product;未发现第二份 MapLibre binary。新 XCFramework 必须以 Metal 命令 `bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic` 生成。默认裸 Bazel 命令的 OpenGL artifact 只有色场、无粒子,已由 STEP-082 post-close correction 作废。 - Pleiades iPad mini 5 上 signed Release 安装运行。一次 V4 Wind load 实际观察到 `clearAllEncodedFields` → color binding → particle binding → T0 `ACCEPTED_INSERTED` → T1 `ACCEPTED_INSERTED` → midpoint seek;用户确认粒子恢复。 -## 尚未逐按钮回归 - -- PLAY/PAUSE、clear selected、clear all、detach/re-attach 尚未逐按钮实测;不得将本 Step 标为完成,也不得把单次 V4 load 扩写为完整控制面验收。 - ## 已执行检查 - `ruby -c packages/maplibre-particle-rn/MapLibreParticleRN.podspec`:成功。 @@ -33,8 +29,10 @@ - `npm pack --dry-run --json --cache /private/tmp/step083-npm-cache`:成功,tgz 清单包含 podspec、`ios/WeatherRuntimeBridge.h/.mm`、既有 Android 与 `src`。 - package `codegenConfig.ios.modulesProvider.WeatherRuntimeBridge` 静态检查和 Native Spec 34 个方法与 ObjC++ 34 个实现逐项计数完成。 -## RDC 下一步 +## Final control regression 真机关闭 -1. 用同一个 `App.tsx` 逐按钮完成 PLAY/PAUSE、clear selected/all、detach/re-attach;同时复核 style-not-ready smoke。 -2. 继续维持 MLRN/App/Particle 的 single `MapLibre` product,禁止为 Particle pod 引入独立 framework。 -3. 所有后续 iOS Particle artifact 一律以 Metal Bazel 命令构建,并在发布前复核 hash、renderer markers 与远端回读(若有发布授权)。 +`integration/react-native/MapLibreParticleRNTestHost/App.tsx` 新增唯一按钮 `RUN V4 FINAL CONTROL REGRESSION`,仅在当前 `reactTag` 有效时执行,且运行中禁用以避免重复并发。它复用 V4 加载的 `prepareV4` 公共 API 路径:clearAll → color binding →(如有)particle binding → T0/T1 `ACCEPTED_INSERTED` → midpoint seek。 + +Pleiades iPad mini 5 真机已由用户用该单按钮完成最终控制序列:`prepare#1 → PLAY → 约500ms → PAUSE → clear selected → prepare#2 → clear all → detach → attach → prepare#3 → refresh`。用户明确反馈“FINAL CONTROL REGRESSION PASS”;最后一次 prepare/refresh 后粒子恢复并保持可见。此前同一真机的 V4 Wind 验收中,用户也已确认“粒子恢复了”。 + +上述结果关闭此前唯一未完成的逐控制面真机验收项;STEP-083 正式 `CLOSED / PASS`。回归按钮保留为后续入口,未改动其逻辑。 diff --git a/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v2.md b/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v2.md index 3d80907f2..d9f564593 100644 --- a/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v2.md +++ b/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v2.md @@ -35,3 +35,9 @@ bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic ## 未完成的 final control regression PLAY/PAUSE、clear selected、clear all、detach/re-attach 尚未逐按钮实测。它们是本 Step 的剩余验收项;在完成前不得将状态改写为 `CLOSED` 或 `PASS`。 + +## Final control regression 入口追加(待真机执行) + +RN Test Host 的 `App.tsx` 已新增单一 `RUN V4 FINAL CONTROL REGRESSION` 按钮。有效 `reactTag` 下,它复用现有 V4 `prepareV4` 公共 API 加载路径,并串行执行 PLAY → 约 500ms → PAUSE → clear selected → prepare → clear all → detach → attach → prepare → refresh;所有 command result 必须为 `ACCEPTED`,三次 prepare 的 T0/T1 均必须为 `ACCEPTED_INSERTED`。运行中按钮禁用,结果区域保留每一步摘要;任何失败明确显示失败步骤,不显示伪 PASS。detach/attach 后最后一次 prepare 会重新 binding/submit 并恢复粒子,供 Pleiades iPad 真机目视确认。 + +本追加只提供最终人工回归入口,不代表已执行真机验收。状态仍为 `IMPLEMENTED / IOS_CORE_PATH_PASS / WAITING_FINAL_CONTROL_REGRESSION`,直到用户确认该序列和最终粒子画面后才可关闭。 diff --git a/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v3.md b/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v3.md new file mode 100644 index 000000000..d050c5767 --- /dev/null +++ b/docs/报告/STEP-083_iOS_RN统一FieldRuntime_TurboModule_Fabric与XCFramework消费_Codex完成报告_v3.md @@ -0,0 +1,35 @@ +# STEP-083 iOS RN 统一 FieldRuntime TurboModule Fabric 与 XCFramework 消费 Codex 完成报告 v3 + +## 执行结论 + +`CLOSED / PASS`。 + +Pleiades iPad mini 5 真机已完成最终控制面回归;用户先确认 Metal 修正版 V4 Wind“粒子恢复了”,随后确认单按钮回归“FINAL CONTROL REGRESSION PASS”。本报告为同一 STEP-083 的正式关闭记录,不新建功能 Step,不提交、不 push。 + +## 最终真机验收 + +- 设备:Pleiades iPad mini 5。 +- Test Host signed Release:`BUILD SUCCEEDED`,已安装并启动。 +- MLRN、App 与 Particle bridge 使用 single MapLibre SPM product;没有第二份 MapLibre framework。 +- `RUN V4 FINAL CONTROL REGRESSION` 单按钮已在真机完成:`prepare#1 → PLAY → 约500ms → PAUSE → clear selected → prepare#2 → clear all → detach → attach → prepare#3 → refresh`。 +- 全部命令返回预期成功结果;最终 prepare/refresh 后 V4 Wind 粒子恢复并保持可见。用户两次真机确认分别为“粒子恢复了”与“FINAL CONTROL REGRESSION PASS”。 + +## Metal XCFramework 验证坐标 + +- renderer:Metal;构建命令:`bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic`。 +- artifact:`MapLibre.dynamic.xcframework.zip`。 +- size:`24943196` bytes。 +- SHA-256:`ae88975fc20f4d9961b95079638d0e9c0d8e61d4394d5c0a8ffcc48d173a827f`。 +- release manifest 仍为本地验证坐标 `LOCAL_BUILD`;本次未修改 release manifest,也未进行发布。 + +## 范围与保留项 + +- App.tsx 的 final regression 按钮保留为回归入口,未改动其逻辑。 +- 未修改 RN public API、iOS native/Core、renderer 或 release manifest。 +- 未覆盖 v2 报告。 +- 三处用户 dirty `docs/doxygen/doxygen-awesome-css`、`platform/ios/app/MBXAppDelegate.m`、`platform/windows/vendor/vcpkg` 保持不动;三份 `docs/40_任务` 未跟踪任务书也保持不动。 +- 当前 `particle/main` HEAD:`d8bb9f547bf0db2f222fb49f1ec5df77373de310`;本次仅做收尾文档且不提交。 + +## 下一步 + +STEP-083 已正式关闭。下一步进入 NavSea iOS 首批大集成,不再拆分微小的 build/install Step。 diff --git a/integration/react-native/MapLibreParticleRNTestHost/App.tsx b/integration/react-native/MapLibreParticleRNTestHost/App.tsx index c0121c1ef..7da5613db 100644 --- a/integration/react-native/MapLibreParticleRNTestHost/App.tsx +++ b/integration/react-native/MapLibreParticleRNTestHost/App.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 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"; @@ -25,6 +25,9 @@ export default function App() { const [v4Frames, setV4Frames] = useState>({ 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 [weatherVisible, setWeatherVisible] = useState(true); const [particleVisible, setParticleVisible] = useState(true); const [visual, setVisual] = useState(initialVisual); @@ -65,35 +68,92 @@ export default function App() { } 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 { - 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)"); + await prepareV4(reactTag); setResult(`${v4Dataset.title}: V4 raw QBIN load accepted`); } catch (error) { setV4Transport(`ERROR: ${String(error)}`); setResult(`LOAD V4 ${v4Dataset.id}: ${String(error)}`); } - }, [reactTag, refresh, v4Dataset]); + }, [prepareV4, reactTag, v4Dataset]); + + 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((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); + } + }, [prepareV4, reactTag, refresh, v4Dataset.fieldKey]); const updateVisual = useCallback((key: keyof typeof initialVisual, value: string) => setVisual((current) => ({ ...current, [key]: value })), []); const applyVisual = useCallback(() => void command("particle visual", async (tag) => { @@ -121,6 +181,8 @@ export default function App() { V4 QBIN Golden: {v4Dataset.title}V4 raw QBIN transport = {v4Transport}V4 bindings: {v4Bindings}{v4Dataset.particleNote} V4 T0: {v4Frames.T0}V4 T1: {v4Frames.T1} {qbinGoldenDatasets.map((item) =>