three.js最小地图运行时(十):接入离线逻辑瓦片剖面

教程:接入离线逻辑瓦片剖面并完成 Stage 02

本教程完成第二篇的最小平面地图。你会把固定的逻辑瓦片剖面接入 Stage 02,用它约束 7×7 调试邻域,并在浏览器中证明整个阶段不依赖远端地图数据。

完成后,页面可以平移、缩放、旋转和倾斜;网格、TileKey 与 cursor inspector 会同步更新,但不会加载任何真实瓦片内容。

前置条件

你已经完成:

  • 不可变 ViewState;
  • PlanarTransform 与 MapRuntime;
  • TileKey 和 TileGridLayer;
  • Stage 02 的表单、交互与地面 cursor 探针。

启动 yinqing/ 中的示例服务器:

powershell 复制代码
npm start

打开:

text 复制代码
http://localhost:3001/vector-tile-handbook/stages/02-minimal-map/

1. 导入离线剖面

在 config.js 中建立唯一事实来源:

js 复制代码
export const DEBUG_TILE_PROFILE = Object.freeze({
  minZoom: 0,
  maxZoom: 15,
  tileSize: 512,
});

Stage 02 入口只导入逻辑剖面与固定教学视图:

js 复制代码
import {
  DEBUG_TILE_PROFILE,
  TEACHING_VIEW_STATES,
} from "../../config.js";

不要在 Stage 文件里再写一份 0、15 和 512。共享对象冻结后,坐标检查器、运行时和下一篇 cover 会读取相同边界。

**立即观察:**重新加载页面,控制区应出现绿色状态卡:

text 复制代码
OFFLINE DEBUG GRID · NO TILE DATA

2. 只夹取调试网格的整数 zoom

保持 ViewState.zoom 为连续相机状态。只有建立调试邻域时,才派生一个整数层级:

js 复制代码
function clamp(value, minimum, maximum) {
  return Math.min(maximum, Math.max(minimum, value));
}

const displayZ = clamp(
  Math.floor(viewState.zoom),
  DEBUG_TILE_PROFILE.minZoom,
  DEBUG_TILE_PROFILE.maxZoom,
);

表单提交时可以限制本实验允许的交互范围,但规范化 ViewState 模块本身不读取剖面:

js 复制代码
runtime.setViewState({
  center: [longitude, latitude],
  zoom: clamp(
    zoom,
    DEBUG_TILE_PROFILE.minZoom,
    DEBUG_TILE_PROFILE.maxZoom,
  ),
  bearing,
  pitch,
});

**立即观察:**输入 13.8,inspector 的 ViewState zoom 保留 13.800,而 TileKey 使用整数 displayZ=13。滚轮缩放时,线框只在跨越整数边界后换层。

3. 建立 7×7 中心邻域

先求中心所在的连续世界地址:

js 复制代码
const center = lngLatToTileAddress(
  viewState.center[0],
  viewState.center[1],
  displayZ,
);
const size = 2 ** displayZ;
const centerUnwrappedX = center.x + center.wrap * size;

再枚举半径 3:

js 复制代码
const keys = [];

for (let dy = -3; dy <= 3; dy += 1) {
  const y = center.y + dy;
  if (y < 0 || y >= size) continue;

  for (let dx = -3; dx <= 3; dx += 1) {
    const address = canonicalizeTileAddress(
      displayZ,
      centerUnwrappedX + dx,
      y,
    );
    keys.push(createTileKey({
      displayZ,
      canonicalZ: address.z,
      canonicalX: address.x,
      canonicalY: address.y,
      wrap: address.wrap,
    }));
  }
}

垂直地址越界时跳过,水平地址则规范化成 canonical x + wrap。不要 clamp 水平 x,否则邻域会在反经线断开。

把同代 snapshot 与 keys 一起交给网格:

js 复制代码
gridLayer.setKeys(keys, transformSnapshot);

**立即观察:**上海固定视图通常显示 49 个 key。把 longitude 改到 179.99 后向东拖动,标签中的 canonical x 会回到 0,同时 wrap 增加,线框仍连续。

4. 在 inspector 中展示剖面事实

第四组 inspector 不显示数据状态,只显示空间事实:

js 复制代码
const profileRows = [
  row("profile", "global XYZ", "logical tiles"),
  row(
    "zoom range",
    DEBUG_TILE_PROFILE.minZoom + " ... " + DEBUG_TILE_PROFILE.maxZoom,
    "display/canonical",
  ),
  row("tile size", String(DEBUG_TILE_PROFILE.tileSize), "CSS pixels"),
  row("network", "disabled", "no remote tile data"),
];

HUD 同时终止任何"已经完成视口选片"的误解:

text 复制代码
CENTER NEIGHBORHOOD · 49 KEYS
13/13/6859/3346@0
OFFLINE DEBUG GRID · NOT VIEWPORT COVER

**立即观察:**侧栏应稳定显示 global XYZ、0 ... 15、512 和 disabled。这些值不会随平移、bearing 或 pitch 改变。

5. 证明 Stage 02 没有网络数据源

先检查入口 import。它应只包含本地模块,不应创建 source、请求器或异步数据 promise。

然后打开浏览器开发者工具:

  1. 切换到 Network;
  2. 清空记录;
  3. 勾选 Preserve log 以外的普通默认选项即可;
  4. 重新加载 Stage 02;
  5. 平移、缩放、旋转并切换预设。

**立即观察:**请求列表只有页面、CSS、本地 ES modules、Three.js vendor 文件和浏览器自身资源;交互不会新增远端地图数据请求。页面右下角也没有数据署名,因为本阶段没有使用外部地图内容。

若看到远端地图请求,先检查 gallery、Stage 入口与共享 render 模块是否误导入了传输配置。不要用请求拦截器掩盖错误依赖。

6. 验证完整交互链路

依次完成以下操作:

操作 可观察结果
左键向东拖动 center longitude 改变,线框贴地移动
滚轮缩放 continuous zoom 连续变化,整数边界处更换网格层级
Shift+左键或右键水平拖动 bearing 改变,网格绕视图中心旋转
Shift+左键或右键垂直拖动 pitch 改变,线框遵循地面透视
点击 North ↑ bearing 回到 0°
点击 Top-down pitch 回到 0°
移动 cursor screen、render-local、Mercator、经纬度和 TileKey 同步变化
cursor 指向天空 橙色 ring 隐藏,面板显示没有地面交点

每次 viewchange 都使用事件携带的 viewState 与 transformSnapshot。不要在一次更新里混读旧 key 和新的 live camera。

**立即观察:**无论 bearing/pitch 如何变化,cursor 越过线框的时刻都应与 TileKey 地址切换一致。

7. 明确 7×7 邻域不是 viewport cover

固定半径邻域回答的是:

中心瓦片附近有哪些地址,足以用来检查坐标和身份吗?

真正的 viewport cover 回答的是:

当前整个屏幕可见地面与哪些瓦片相交?

二者不会恒等:

  • 宽屏、低 zoom 或高 pitch 时,7×7 可能漏掉可见地面;
  • 窄屏或高 zoom 时,它可能画出大量屏外瓦片;
  • bearing 改变不会改变中心邻域,却会改变真实地面 footprint;
  • 指向地平线时,四角射线可能无法形成完整地面 polygon。

**立即观察:**把 pitch 提高到 80°。远处地面明显延伸到固定邻域之外,这正是 Stage 03 必须从屏幕射线重新计算 cover 的原因。

8. 完成标准

  • Stage 02 只导入冻结的离线逻辑剖面;
  • ViewState zoom 保持连续,相机状态不被 source 范围改写;
  • 调试层级被限制在 z0--z15;
  • 中心附近最多绘制 49 个 TileKey;
  • 反经线邻域通过 wrap 连续;
  • inspector 明确显示逻辑剖面与网络禁用状态;
  • 所有交互与 cursor 转换继续工作;
  • 文档、HUD 与 UI 都没有把固定邻域称为完整 cover。

第二篇至此得到一张完全离线、可交互、可检查坐标链路的空地图。第三篇将把固定邻域替换为四角地面 footprint、精确相交测试和可恢复的四叉树遍历。


相关推荐
谢亮_vipxieliang22 分钟前
深入理解 JavaScript 数据类型与类型转换
开发语言·javascript·ecmascript
程序员Sunday10 小时前
JavaScript 事件循环面试题,宏任务与微任务怎么执行|Sunday面试指南
开发语言·javascript·面试·校招·事件循环·程序员sunday
默_笙11 小时前
🌊 向量库和 ES 都查不出"关系",我只好给奶茶建了一张人脉网
前端·javascript
思无邪6612 小时前
用 AI 做 JS 逆向:从抓包到复现的完整方法论
开发语言·javascript·人工智能
福兮说13 小时前
浏览器里把 iPhone 的 HEIC 转成 JPG:Chrome 解不开、转完大了七成、拍摄时间全丢,六个坑实测
前端·javascript·图像处理·chrome·ios·iphone·heic
可乐鸡翅yeah_14 小时前
video.js 集成 hls.js 开发 M3U8 播放器,新手高频踩坑
开发语言·前端·javascript·后端·ecmascript·m3u8·音视频在线播放
用户1179536547816 小时前
TRAE vs Claude Code 性能对比:从代码生成、多文件重构到底层吞吐,谁更适合你的日常开发?
javascript
kill52217 小时前
useRef
前端·javascript·react.js
米多科技17 小时前
我做了一个 3D 虚拟世界基底,在上面再搭 3D 世界或元宇宙,事半功倍
javascript·人工智能·github
霍营_托尼18 小时前
纯前端播 40GB 本地视频?我把浏览器改造成了「磁盘流式」播放器
前端·javascript·音视频开发