# 21: 3D Tiles 与地形基线 **What to build:** 接入本地或甲方提供的 3D Tiles、地形和倾斜摄影资源,实现地面视图与管线视图切换。首期不依赖 Cesium Ion。 Blocked by: 03, 04 Status: claimed Labels: ready-for-agent > 认领:2026-09-17 会话接手。维护者已给出本机 GIS 数据位置(`D:\GIS\GIS\yuanlingDEM` 量化网格地形 + `D:\GIS\GIS\yuanlingPic3` 影像切片),本轮先做「地形 + 本地影像 + 免费全局底图」;地面/管线视图切换与 3D Tiles/倾斜摄影(数据未提供)留在本票后续。 ## 依赖与事实 - 引擎已就绪:票 27 已把 Cesium Viewer 接进地图服务层,本票在其上接真资源即可。 - **影像底图已有配置化通道**(票 29 已关闭):`map.basemap.source` 支持 `none` / `natural-earth`(Cesium 自带离线影像)/ `tianditu`(填 `map.tianditu.token`,开发走 `/tianditu.map` 代理)/ `xyz`(任意 `{z}/{x}/{y}` 模板)。甲方本地瓦片若已是 XYZ/WMTS 切片,先按 `xyz` 配即可;本票专注地形、3D Tiles、倾斜摄影与地面-管线视图切换,不要重做底图影像层。天地图的**真实取图**也还没用真 tk 联调过,可在本票顺手验收。 - 维护者 2026-09-17 明确:**本机已有本展示区域所需的 GIS 数据**;具体地址、数据格式(瓦片/地形/模型)以及**正式部署后如何编辑更新**的方案,等维护者提供 —— 开始实施前先向维护者索取这两项。 - 禁止项沿用决策地图调研:不使用 Ion token、`Terrain.fromWorldTerrain`、`createOsmBuildings`。 - [x] 支持本地或甲方资源地址配置。 - [ ] 地形、3D Tiles 和倾斜摄影资源可加载。—— **地形已可加载**(本机 DEM 实测取到 14 级瓦片、零 404);**3D Tiles 入口已实现并验证链路**(`map.tilesets[]` → `Cesium3DTileset`,合法 tileset 能挂上、坏的单独记失败);**甲方模型数据仍不可加载**:`D:\GIS\GIS\yuanlingSHP` 是 cesiumlab `model2tiles` 导出,缺根 `tileset.json` 且分块 `boundingVolume` 是局部坐标(无 `transform`),需重导或提供模型原点。← **本票唯一的硬阻塞,待维护者动作** - [x] 资源加载中、成功、失败和空状态可区分。—— 引擎 booting/ready/failed/disabled;地形 `ready`/`invalid`;3D Tiles `ready`/`invalid`(`data-map-tilesets` + `stats.tilesetErrors`)。 **说明**:「空状态」在本引擎里**没有稳定信号**——Cesium 按视锥懒加载,资源挂上了但当前视野无瓦片是正常态(此时 `numberOfTilesWithContentReady` 也为 0),据此报「空」会把好数据误判成空。故只保证三态如实,未伪造第四态;要不要在 UI 上区分「空」请维护者定夺。 - [x] 地面视图与管线视图可切换:管线视图压低相机并把地形与 3D Tiles 压成半透明,地面视图还原。 - [x] 切换视图不重置旋转中心,也不改变左键/右键/滚轮三键分工。 - [x] 不使用硬编码 Cesium Ion token。 - [x] 有资源加载状态单测或走查记录。 > **地形感知的相机已由票 30 做掉**(原先记在本票「下一步」第 3 条):`pickGroundAtCenter()` 改为按地形 > 拾取,并在地形就绪后采样「家」视角的实测高程 —— 拉到距离下限 120 m 时中心高程 111.6 m,近景不再黑屏。 ## 进度:90% **本轮(2026-09-17)做完「本地地形 + 本地影像 + 免费全局底图」这条主线**,维护者要的「基础 GIS 地球地图」已经能看到沅陵县城真数据。 ### 改动 - 新增 `src/common/localGisStaticPlugin.js`:dev server 把 `COCKPIT_GIS_MOUNTS`(`.env.local` 里给的 `{prefix,dir}` 列表)挂成静态路径 —— DEM 与影像上百 MB,既不进仓库也不进产物;没配就什么也不挂,别的机器照常跑。 - `domain/mapLayers.js`(新):`planTerrain()` / `planOverlays()` / `planTilesets()` 纯函数计划层;`services/cockpitConfig.js` 增加 `map.terrain{url,requestVertexNormals}`、`map.overlays[]{url,minimumLevel,maximumLevel,bounds,credit}`、`map.tilesets[]{url,maximumScreenSpaceError,credit}` 的归一化与校验。 - `services/cesiumEngine.js`:`applyTerrain()` 走 `CesiumTerrainProvider.fromUrl`(量化网格,`scheme:"tms"` 由 Cesium 自己认,**服务端不用翻 y**);`applyOverlays()` 把本地精细切片叠在全局底图之上(TMS 用 `{reverseY}`,并按 `bounds` 设 `rectangle`,范围外不发请求);**`applyTilesets()` 逐项挂 `Cesium3DTileset`,单项失败不影响其它项**。 - `domain/basemap.js` 新增免费全局层 preset `arcgis-imagery`(ArcGIS 官方世界影像,免费免 key,模板顺序 `{z}/{y}/{x}`);**离线优先**:默认与推荐都是 `natural-earth`。 - `services/mapService.js`:引擎启动时按计划挂地形 / 叠加层 / 3D Tiles,宿主带 `data-map-terrain`、`data-map-tilesets`,`getLifecycleStats()` 增 `terrain` / `terrainError` / `overlays` / `tilesets` / `tilesetErrors`;浮标变成「底图:… · 地形:… · 叠加影像 N 层 · 3D Tiles N 个」。 - 配置与数据对齐:`public/cockpit/config.json`(本机真实配置,gitignore)按 `yuanlingPic3/meta.json` 的 bounds 设 `bounds`、按 TMS 用 `{reverseY}`;`.env.local` 挂目录并打开配置文件层。 - **新增 `docs/cockpit-deployment.md`(给运维的生产部署手册)**:一页速查表、配置四层与开启方式、逐字段改法(后端/底图/地形/影像/3D Tiles)、nginx `location` 片段与两条硬要求(别走 SPA 回退、要 CORS)、完整最小配置、8 条自检表、常见问题。`public/cockpit/README.md` 顶部指路过去(它偏开发期接数据)。 - 新增走查夹具 `.scratch/cockpit-shell-panel-layout/verify/fixtures/tiles/tileset.json`(只有包围盒、无 content 的合法 tileset,用来验证入口链路,不伪造任何模型/影像)。 ### 验证证据(`yarn dev` @ 8001,Chrome 152 headless) - 纯逻辑 `t21-terrain.mjs`:通过。含**配置与磁盘数据对齐核对**(`layer.json` 的 quantized-mesh-1.0 / EPSG:4326 / `scheme:"tms"` / octvertexnormals,`meta.json` 的 tms / 3857 / bounds 与配置一致)。 - 真机走查 `t21-gis-walkthrough.mjs` → **9/9**:三层都挂上;细层影像只请求声明范围内的瓦片;压到沅陵拉近后 **DEM 192 块(最高 14 级)+ 影像 199 块,全部 HTTP 200、零 404**(这条同时证明 TMS 的 y 约定、availability、`{reverseY}` 都对);镜头停在沅陵上空 4.6 km;DEM 地址配错时 `invalid` + 明确失败文案 + 回退椭球;**3D Tiles 入口:合法 tileset 挂上(`stats.tilesets==1`)而同批里坏的 URL 单独记失败(`tilesetErrors` 1 条)、引擎与其它图层不受影响**;全程无页面级报错。 - 视觉:`yarn dev` 不注入任何东西直接打开 `/cockpit` —— 离线全局影像球 + 顶栏「配置就绪 · 数据源 mock」+ 浮标「底图:内置离线影像 Natural Earth II · 地形:dem · 叠加影像 1 层」;换中心 + 滚轮拉近 4.6 km 后是沅陵县城真影像(河道、桥、体育场、建筑)。 - 回归:`t01` 走查 **20/20**、`t04` **9/9**、`t29` **6/6**、`t06`/`t28`/`t03-camera-contract`/`t01-panel-catalog`/`t21-terrain`/`t29-basemap` 逻辑脚本全通过、`yarn build` exit 0。 - 环境事实:本机**外网瓦片不可达**(ArcGIS / OSM 从 node 与浏览器都被挡),所以全局层实测用离线 `natural-earth`;在线源只做模板与署名断言,**未真机验证**(维护者明确"能用内网离线就不用在线",故不作阻塞项)。天地图无 key,同样不做主路径。 ### 3D Tiles 数据的现状(需要维护者决定) `D:\GIS\GIS\yuanlingSHP` 确实是 3D Tiles:cesiumlab `model2tiles` 导出,5 个层级(16–20)、769 个分块 `*.json` + 772 个 `*.glb`,每个分块 json 是**自洽的 tileset 文档**。但**缺根 `tileset.json`**,且分块 `boundingVolume.box` 是局部坐标(例:中心 6265/2293;叶子 20 级为 `-3258, 9736, 105`),**没有任何 `transform`** —— 也就是没有把模型放到地球上的地理参考。 **2026-09-18 结论:维护者目测判定「明显错位 + 只是白色立方体,效果非常差」→ 不采用。** 合成的根 `tileset.json` 已删除,`/gis/models` 挂载已移除,`yuanlingSHP` 那份数据不再使用。 替代方案见 **票 34**(参照 GIS 数据做轻量体块模型)。 当时的做法与机器验证留档(供以后判断"错位"到底出在哪): 1. 源数据是**地理坐标**:`h49c004002/*.prj` 为 `GEOGCS["CGCS_2000", ... UNIT["Degree"]]`,`output/*.json` 是 GeoJSON; 2. 13 个 level-16 分块里,稠密簇(10 块)是县城核心,用目标坐标锚定它反推局部系原点,写成 ENU→ECEF `transform`; 3. 回验:簇心经 transform 回算与目标差 ≈5 m、13 个分块中心全落在影像 bounds 内、真机加载零报错。 **为什么仍然错位**:锚定假设("稠密簇中心 == 维护者给的目标坐标")没有依据 —— 原始导出本就没有原点, 靠推测必然偏;且分块体块是**单色白模**,与影像里的城市形态对不上。这条路先放下。 ### 本轮追加(2026-09-17 晚):地面/管线视图切换 决策地图票 05 已把语义锁死,本轮按它落地: - 新增 `domain/viewModes.js`(纯函数):`planViewMode()` 把「哪个视图」翻译成相机预设 + 图层透明度; `viewHprFor()` 只产出 `{heading, pitch, range}` —— **签名里根本没有中心参数**,从构造上保证「切换视图不重置中心」。 管线视图:pitch 抬到 `-18°`(上限放宽到 `+5°` 允许平视/轻微仰视)、弹簧臂压到当前距离的 `35%`(夹在 400–6000 m)、 地形 α`0.45` / 3D Tiles α`0.4`;地面视图:还原为 α`1` 与常规俯视。 - `stores/filterStore.js`:视图条件并入 `filterStore`(决策地图指定 —— 与行业筛选同属全局条件,**切一级模式不重置**)。 - `services/cameraControl.js`:新增 `setPitchLimits()` 与 `setHpr()` —— 只改限制与 `pitch/range`,不碰输入绑定、不碰中心。 - `services/cesiumEngine.js`:`applyTilesets()` 现在回传实例(供改样式);新增 `applyViewModeLayers()` —— 地形走 `globe.translucency`,3D Tiles 走 `Cesium3DTileStyle` 的 color alpha;地面视图**清掉样式**而不是设成 1。 - `services/mapService.js`:`setViewMode()` / `getViewMode()`;进管线前记下地面基线(倾角+距离),回地面时按它还原; 宿主带 `data-map-view-mode`(启动即为 `ground`,不是切换过才有)。 - `CockpitShell.vue`:筛选条加「视图:地面视图 / 管线视图」两个按钮(`data-view-mode` + `aria-pressed`), 与范围/行业同级、切一级模式不重置。 - `services/cockpitConfig.js` + 部署手册 2.7:`map.viewMode.pipeline{ pitch, rangeFactor, terrainAlpha, tilesetAlpha }` 可覆盖默认值,脏值忽略退回默认。 - 新增 `verify/t21-view-modes.mjs`(逻辑)与 `verify/t21-view-modes-walkthrough.mjs`(真机)。 ### 验证证据(本轮追加) - 纯逻辑 `t21-view-modes.mjs`:通过。含**构造性断言**:`viewHprFor()` 的返回值里没有 `pivot` 字段、 计划里也没有中心 —— 「不重置中心」不是靠人眼比对,而是类型层面就不可能;另有地面还原(按基线)、 clamp 边界(min>max 的坑已修)、脏配置兜底。 - 真机走查 `t21-view-modes-walkthrough.mjs` → **10/10**:初始 `ground`;切管线后距离 `12000 → 4200 m`、 pitch `-55° → -18°`,而**中心经纬高完全一致**(`110.39028164000001, 28.4436787, 111.57951503970997` 前后逐位相同); 按钮 `aria-pressed` 与 `data-map-view-mode` 同步;**管线视图里左键仍只平移**(中心移动 −0.0036°、距离/倾角不变)、 **滚轮仍只改距离**(4200 → 6787 m、倾角不变);回地面按基线还原(-55° / 12000 m); **切一级模式后仍是管线视图**(全局条件);全程无页面级报错。 - 视觉:管线视图截图 —— 相机贴着地面侧看(河湾与县城呈侧视,不是俯视)。 - 回归:7 个浏览器套件全绿(t01 20/20、t04 9/9、t21 9/9、t21 视图 10/10、t28 7/7、t29 6/6、t30 7/7)、 8 个逻辑脚本全通过、`yarn build` exit 0(57 s)。 ### 下一步(本票未完成的部分) 1. **3D Tiles / 倾斜摄影数据源** —— 甲方那份 `yuanlingSHP` 经目测否决(错位 + 白模), 替代方案已由 **票 34**(轻量体块模型)接手,等维护者对其远景效果判定; 若判定采用,本票的验收第 2 条改为「**示意模型**可加载」,并在部署文档里显著标注非实测。 2. 「空状态」是否要在 UI 上区分 —— 技术上没有稳定信号(见验收第 2 条的说明),需维护者定夺要不要做。 3. 若后续拿到**带正确原点的**真实 3D Tiles / 倾斜摄影(cesiumlab 重导并勾选生成根 tileset), 直接填 `map.tilesets` 即可替换示意模型 —— 入口与三态可见性都已就绪。