What to build: 从全球飞到县城视角的全过程不出现肉眼可辨的矩形色块跳变。全球/引导态先隐藏精细叠加影像、保留粗底图;进入时等雾先形成保护、目标视野可显示,再渐显精细层;返回时反向交接。这是需求③的落地。
Blocked by: 38
Status: resolved Labels: ready-for-agent
cockpit-primary-area-guardrails/spec-fog.md 的「与影像的边界」;cockpit-primary-area-guardrails/spec.md 的进出状态机。cockpit-primary-area-guardrails/issues/08-coarse-to-fine-transition-solution.md(已 resolved,含维护者在真实场景的三条反馈)。src/modules/cockpit/prototypes/TransitionPrototype.vue 与同目录 scene 脚本、prototypes/prototype-coarse-to-fine-transition.html;打开方式写在决策票 08 的 Answer。/cockpit 入口的 DEV 原型接线(transitionPrototype 一路)在本票内收敛,生产只吸收已确认的交接语义。按维护者裁定收窄为"隔离":生产交接是 alpha 唯一写入方、与原型显式互斥;删除与归档归 40(见下)。实现完成、过闸门;真机观感由维护者 2026-09-30 现场调定并确认通过,本票收口。
维护者在真机上自行完成一轮时长/阈值调试并裁定"以我为准"(部署值见
「维护者真机调定与收口」),观感判据(验收第 8 条)据此通过。
两条有意放宽(requireTilesLoaded: false、退出发白窗口 ≈387 ms)已如实记录在票面与部署手册。
按维护者裁定(2026-09-28):39 只做隔离,原型的删除与归档留给 40。
一处口径按维护者裁定收窄(见下「原型接线的归属」)。
验收第 10 条原文要求 transitionPrototype 那路 DEV 接线「在本票内收敛」,但票 40 的验收又写着
「可丢弃原型与 /cockpit 入口的 DEV 接线单独保存到 throwaway 分支,并在本票留分支指针」——
两条重叠、归属不清。维护者裁定:39 只做隔离,删除与归档留给 40。
据此本票的实际落点(不是原文的字面"删掉接线"):
ImageryLayer.alpha,同时跑必然互相覆盖。
现在生产交接是 alpha 的唯一写入方;原型挂载时调 setHandoverPrototypeActive(true)
显式接管(不是"谁后跑谁赢"),卸载时交还并重画一次。if (false) 会被闸门打红。domain/imageryHandover.js:交接状态机(相位 / alpha 推进 / 落定必为 0 或 1)、
揭示闸门(全球不揭示、雾保护、目标视野、边缘风险、瓦片失败)、返回同步判据、
边缘遮蔽率(与雾曲线同源)、过期判定、配置归一化、读数。
纯函数、不 import cesium、不读 DOM。services/cesiumEngine.js:applyOverlays 改为返回句柄(不再只返回层数)——
句柄的 setAlpha() 是 alpha 的唯一写入点;新增 createOverlayReadiness()
采集 globe.tilesLoaded 与瓦片失败(后者很关键:Cesium 在没有 errorEvent 监听者时
会把瓦片错误直接抛到窗口,见 ImageryLayer.js:1708,走查的「无页面级报错」会因此误伤)。services/mapService.js:交接的策略与生命周期唯一所有者;
门控与顺序收口在 setAreaState 与渲染循环;宿主读数 8 个 data-map-handover*;
DEV 把手 getHandoverState / getHandoverParams / advanceHandover / setHandoverPrototypeActive。services/cockpitConfig.js:map.handover(缺省=默认;minEdgeOcclusion 越界退回默认并记 issue)。docs/cockpit-deployment.md:新增 2.12 粗→精影像交接 + 一页速查 + 自检两行;
config.example.json 同步该字段。minEdgeOcclusion(默认 0.9)」。
阈值与雾参数同源,日后调雾端点时边缘判据自动跟随,不会各调各的。
实算证明不能照抄:区内最坏点(西南角)距影像边 35.8 km,遮蔽率 0.967(过闸);
若照抄 38 km 字面值,这个点会被误判为不安全而挡住本可显示的影像。globe.tilesLoaded 仅表示加载队列为空,不证明目标视野逐像素就绪
(决策票 08 明确警告)。读数里带 tilesReadyIsProxy: true、宿主带 data-map-handover-tiles-proxy="yes"
如实标注,免得下游把它当充分证明。另有两处实现上的判断:
alpha = 0 + show = true。 瓦片是否被请求由 GlobeSurfaceTile.js:691
按 layer.show 决定,不看 alpha。用 show = false 会真的停掉预取,
揭示时才开始下载 —— 正好造成"先空白再跳方块"。读数里 layerProbe 可直接核对。逻辑闸门 verify/t39-handover.mjs 通过,覆盖:相位与 alpha 端点(落定必为 0/1)、
闸门四类拦截与"三条件齐备才放行"、降级(雾不可用仍揭示但不承诺遮边、且不绕过瓦片就绪)、
瓦片失败保持隐藏且优先报出、边缘判据与雾曲线同源、返回同步(按下返回那一帧绝不撤雾)、
过期取消与幂等、配置可调且脏值退回默认、读数不承诺已遮边、静态守卫 8 条。
变异测试 15 处,全部被对应断言打红(一次性脚本跑完即删):闸门不看雾强度/瓦片/边缘、
全球态也揭示、瓦片失败也放行、降级锁死业务、返回立刻撤雾、落定停中间值、
边缘判据自写一套曲线、读数谎称已遮边、隐藏改用 show=false、原型不互斥、
配置脏值不退回默认、瓦片信号不标代理、进出动作被稳态节流吞掉、
渐显途中不幂等(见下方真 bug ②)。
真机时序证据(页面可见时采集,逐 100 ms 采样)——进入:
| 时间 | 雾强度 | 精细层 alpha | 挡板 |
|---|---|---|---|
| 102–901 ms | 0.03 → 1.00 | 恒为 0 | fog(等待雾形成保护) |
| 1009 ms | 1.00 | 0.025 | 放行 |
| 1009–1710 ms | 1.00 | 0.025 → 0.142 → 0.323 → 0.534 → 0.770 → 0.904 → 0.993 | 渐显 |
| 1710 ms 起 | 1.00 | 1.00(revealed) |
— |
返回:
| 时间 | 精细层 alpha | 雾强度 | 说明 |
|---|---|---|---|
| 103–703 ms | 0.942 → 0.797 → 0.603 → 0.390 → 0.196 → 0.053 → 0 | 恒为 1.00 | 影像先收起,雾全程保持保护 |
| 803–1502 ms | 0(已收起) | 1.00 → 0.96 → 0.85 → 0.69 → 0.50 → 0.32 → 0.16 → 0.04 → 0 | 收起之后才开始撤雾 |
全程 appliedAlpha 与状态机 alpha 逐帧一致 —— 图层真的被写(票 38 的"读数对、画面没变"陷阱)。
另有隐藏期间的可核事实:layerProbe = [{ show: true, alpha: 0 }]
—— alpha 归零但 show 仍为 true,即"看不见 ≠ 停止加载"成立。
全量回归:26 个零依赖逻辑闸门全绿(t05 守卫面纳入 imageryHandover.js,236→248)。
① 隐藏若用 show = false 会停掉预取。 实现时查了 Cesium GlobeSurfaceTile.js:691,
确认瓦片请求只看 show 不看 alpha,据此定成 alpha=0 + show=true;
并加静态守卫禁止 layer.show = false(变异确认有牙)。
② 渐显途中 beginHandover 不幂等 → alpha 永远到不了 1,精细层永不出现。
服务层每渲染帧都会调一次 beginHandover("条件还成立吗"天然要重复问)。
第一版只在端点(REVEALED/GLOBAL)幂等,于是渐显途中每帧都被当成新一轮:
since 每帧被重置 → elapsed 恒 ≈0 → alpha 一直是 0 附近。
最阴的地方:所有读数都显示"entering / 正在渐显 / 保护成立",看起来完全正常 ——
只有画面不动。逻辑闸门当时抓不到(它只验相位,不验"推进时间后 alpha 是否真的变大")。
已修(同一相位幂等、不重置时间轴;换方向才重置),并补一条行为断言:
同一相位连续调 beginHandover 后 since 不得变、且推进一个 fadeMs 后 alpha 必须到 1
(变异确认能抓住)。
维护者复核票 38 第一版的高度收口后,指出两处新问题;详细的根因与反解过程记在 票 38 的「维护者第二次现场复核」一节(雾是那边的实现面),此处只记与交接有关的部分:
HANDOVER_DEFAULTS.fadeMs 700 → 380 ms(比原型 900 ms 短得多,
但仍是一段可辨的渐变而不是硬切)。刻意没有去调雾的强度 —— 按上面的几何,调强度对中心无效。range 是 20,000,000 m、
起点 12,000 m(跨 1.7e6 倍),指数逼近起步最快,头一帧就越过整条雾带(实算 0.15 ms)。
即"雾几乎一瞬间就散了"、且那时雾还满着 ⇒ 整球发白。
修法在相机侧(RETURN_FLIGHT_MS + 慢起缓动),但约束是交接定的:
雾淡完于 handover.fadeMs + fog.fadeOutMs,那一刻相机应仍在 42 km 以下。
该跨模块关系已固化成本票闸门的第 12 节 ——
但 2026-09-30 维护者按现场观感有意放宽了它,故第 12 节改为
"把窗口算出来并钉住已接受上限(400 ms)"而非硬断言(详见下方收口一节)。map.handover.fadeMs 保持可配;380 ms 是当时的取值,最终由维护者现场定为 500 ms。
下面四项原本是 agent 不能代判的观感项。维护者在真机上走查并调定时长后 确认"目前的效果就行"(见「维护者真机调定与收口」),故本票据此收口。 保留原文以便日后回归时知道该看什么、以及当时依据的是哪条判据。
map.fog.fadeInMs/fadeOutMs 可调)、
雾色是否"冷白灰不发绿"(票 38 已确认色相)、返回时高度与雾是否"看起来同步"(本票已保证时序)。altitudeSlewMs),最终现场值为 RETURN_FLIGHT_MS = 1000、altitudeSlewMs = 600。document.hidden=true:rAF 被冻结,
雾的时间轴推进与交接的逐帧推进都停摆(读数为 0)。上表的时序证据是在标签页可见时采集的;
隐藏标签页下闸门行为本身仍正确(如实报 block: fog、拒绝过早揭示),但不能用隐藏页判断推进。@puppeteer/browsers 为空目录),故
verify/t39-handover-walkthrough.mjs 已按既有写法备好、供修复后一键复跑;
本票的真机证据由 DSH 浏览器逐项采集(上表),两者逐条对应。维护者在本机对时长与阈值做了完整一轮现场调试,并明确裁定:以现场观感为准。 原话(两轮问答):
①「就按我调整的数值来,不要继续检查和改动了,目前的效果就行」 ②(关于高度带)「我已改为
altitudeFullM: 14000/altitudeZeroM: 3600000(后修至 3000000),以我为准」
据此的最终落点(部署层=public/cockpit/config.json,已 gitignore):
| 项 | 代码默认(规格取向) | 本部署现场值 |
|---|---|---|
fog.fadeInMs |
800 | 900 |
fog.fadeOutMs |
400 | 300 |
fog.altitudeFullM |
20000 | 14000 |
fog.altitudeZeroM |
42000 | 3000000 |
fog.altitudeSlewMs |
400 | 600 |
handover.fadeMs |
380 | 500 |
handover.minEdgeOcclusion |
0.9 | 0.7 |
handover.requireTilesLoaded |
true | false |
RETURN_FLIGHT_MS |
— | 1000 |
| 返回缓动 | — | smoothstep^6.5 混入 0.1% 线性项 |
两条必须如实记录的放宽(都是维护者的明确取舍,不是疏漏):
requireTilesLoaded: false —— 不再等"瓦片加载队列为空"就允许揭示。
决策 08 当初设这一条正是为了避免"先空白再跳方块";关掉后牺牲的就是那个保障。
已写进部署手册 2.12 的字段说明(含代价与回退方法)。500 + 300 = 800 ms 才淡完,窗口 ≈ 387 ms。
结构性约束(雾应先淡完)在部署层被有意放宽;维护者已确认当前观感可接受。
没有把它偷偷写成"通过":见下面对闸门的处理。闸门的相应处理(不掩盖、保留牙):原第 12 节是一条硬断言
("雾淡完时相机必须仍在 42 km 以下"),它在 T=1000 下不可能成立,
留着就是一条永远红的假门。已改写为把窗口算出来并钉住已接受上限(400 ms):
flightMs 而非阻尼)仍硬断言;fadeMs/fadeOutMs、
返回退回阻尼、缓动停在中途)。同时修掉一处会误导人的注释:camera.js 里 RETURN_FLIGHT_MS 与缓动的 JSDoc 原先写着
"指数 7 由不变量反解"的推导,而维护者已把缓动改成 6.5 + 0.1% 线性项、
且约束已被放宽 —— 注释若照旧就是在源码里说谎。已改写成:
说明结构性约束本身、说明本部署按观感有意放宽、并指出代码默认值仍保持"雾先淡完"的取向。
新增一条配置层守卫:altitudeZeroM 必须大于 altitudeFullM。
写反会让高度系数恒为 1(等于把这层保护关掉),而它正是"退出整球发白"的防线。
只报 issue、不替改值 —— 两个数各自都合法,"要不要在高空保留雾"是部署取舍
(本部署有意把上端放到 3000 km,使飞入全程都有雾)。
验证:node --check 全部通过;两个配置文件 JSON 合法且归一化 issues 为空;
t38-fog / t39-handover 均绿;全量 29 个闸门通过,
仅 2 个红且与本票无关(t04-config 的"不该硬编码后端地址"与 t44-device-assets
由并发会话正在改的 cockpitApi.js / deviceAssets.js 引起,我未触碰这些文件)。
环境限制(如实记录):本会话的工具清单里已没有浏览器工具
(browser_refresh 报 "unknown tool"),故本轮的"观感通过"是维护者在真机上自行确认的,
不是我这边采集的证据。我提供的是算术、闸门与变异证据。
验收第 8 条(观感判据)由维护者真机确认通过("目前的效果就行"); 其余 9 条此前已逐条落地并有闸门/变异证据。本票按维护者裁定收口。