gis-view-webgl2 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +7 -7
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +140 -20
  4. package/dist/index.js +1 -1
  5. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +45 -0
  6. package/docs/GisView.md +53 -53
  7. package/docs/ai/06-/344/274/232/350/257/235/347/264/242/345/274/225/344/270/216/345/244/215/346/237/245/346/226/271/346/263/225.md +36 -0
  8. package/docs/demos/ConnectDemo.vue +114 -3
  9. package/docs/demos/MinimapDemo.vue +62 -16
  10. package/docs/demos/api-index.js +15 -14
  11. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +25 -16
  12. package/docs/demos/gis-view-demo/useGisViewDemo.js +27 -16
  13. package/docs/index.html +3 -3
  14. package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +6 -5
  15. package/docs/internals/16-01-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/212.md +5 -3
  16. package/docs/internals/18-/346/200/247/350/203/275/345/256/236/346/265/213/344/270/216WebGPU/350/257/204/344/274/260.md +12 -0
  17. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +11 -3
  18. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +1 -1
  19. package/docs/manual/10-/351/200/232/351/201/223/347/274/226/350/276/221/344/270/216/346/226/260/345/242/236.md +423 -3
  20. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +79 -28
  21. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +6 -5
  22. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +5 -4
  23. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +117 -5
  24. package/docs/manual/21-/350/257/264/346/230/216/344/270/216/345/270/270/350/247/201/351/227/256/351/242/230.md +5 -2
  25. package/docs/steps/README.md +5 -5
  26. package/docs/steps/selftest-demos.html +149 -5
  27. package/docs/steps/selftest-docs.html +3 -3
  28. package/docs/steps/selftest-full.html +315 -12
  29. package/docs/steps/selftest-perf.html +39 -1
  30. package/package.json +1 -1
@@ -16,11 +16,11 @@ npm run dev # 或 npx vite
16
16
  控制台里 `__gisSteps.view` 就是当前这一步的视图,`__gisSteps.DATA` 是那份园区数据,可以随手调 API。
17
17
  地址栏里带步骤号(`index.html#7`)可以直接打开某一步,也能收藏 / 分享。
18
18
 
19
- 自测页就在同一个目录下(`selftest.html` / `selftest-demo.html` / `selftest-demos.html` / `selftest-labels.html` / `selftest-select.html` / `selftest-actions.html` / `selftest-scale.html` / `selftest-perf.html` / `selftest-features.html` / `selftest-full.html` / `selftest-effects.html` / `selftest-data.html` / `selftest-decoration.html` / `selftest-docs.html`):打开就跑,标题报 `PASS` / `FAIL`,页面上的清单是逐条断言。**无头环境下 `requestAnimationFrame` 基本不推进**,动画类断言要用真时间(CDP 或直接开浏览器)跑 —— `selftest-effects.html` 因此自己装了一个「帧泵」(把 `requestAnimationFrame` 收进 Map、手动按帧推进),涟漪与动点的断言才测得动。另有 docs/index.html(文档中心:把仓库里的 markdown 读成 HTML)、docs/api/index.html(API 速查:256 个实例成员逐个一张卡,参数 / 返回 / 说明 / 示例与复制,搜索 + `#成员名` 深链、左侧分组跟随与深浅主题)、docs/guide/sample-document.html(sample-document.json 的交互图解)与 docs/steps/selftest-docs.html(上面这三个页面的自测:清单 / 渲染 / 点选标注一页跑完)。
19
+ 自测页就在同一个目录下(`selftest.html` / `selftest-demo.html` / `selftest-demos.html` / `selftest-labels.html` / `selftest-select.html` / `selftest-actions.html` / `selftest-scale.html` / `selftest-perf.html` / `selftest-features.html` / `selftest-full.html` / `selftest-effects.html` / `selftest-data.html` / `selftest-decoration.html` / `selftest-docs.html`):打开就跑,标题报 `PASS` / `FAIL`,页面上的清单是逐条断言。**无头环境下 `requestAnimationFrame` 基本不推进**,动画类断言要用真时间(CDP 或直接开浏览器)跑 —— `selftest-effects.html` 因此自己装了一个「帧泵」(把 `requestAnimationFrame` 收进 Map、手动按帧推进),涟漪与动点的断言才测得动。另有 docs/index.html(文档中心:把仓库里的 markdown 读成 HTML)、docs/api/index.html(API 速查:257 个实例成员逐个一张卡,参数 / 返回 / 说明 / 示例与复制,搜索 + `#成员名` 深链、左侧分组跟随与深浅主题)、docs/guide/sample-document.html(sample-document.json 的交互图解)与 docs/steps/selftest-docs.html(上面这三个页面的自测:清单 / 渲染 / 点选标注一页跑完)。
20
20
 
21
21
  ## 怎么查全部属性与方法
22
22
 
23
- 组件的完整 API 是同一份清单的五个去处(当前:实例成员 **256** 个 = 253 个方法 + `el` / `canvas` / `gl` 三个属性,`options` **96** 个键,数据字段 **250** 个):
23
+ 组件的完整 API 是同一份清单的五个去处(当前:实例成员 **257** 个 = 254 个方法 + `el` / `canvas` / `gl` 三个属性,`options` **96** 个键,数据字段 **250** 个):
24
24
 
25
25
  - **手册里查**:`docs/manual/` 把整本手册拆成 21 章(索引页 `docs/GisView.md` 有目录与按任务的快速跳转)—— 找方法先看 [`16 实例方法总表`](../manual/16-实例方法总表.md)、要把每个成员逐个列全(参数写「名字: 类型 = 默认值」、返回值只写类型)看 [`17 参数速查`](../manual/17-参数速查.md)、要能直接复制的调用看 [`18 逐方法示例`](../manual/18-逐方法示例.md);`options` 表与数据字段表分别在 [`03 配置项(options)`](../manual/03-配置项.md) 与 [`02 坐标与数据`](../manual/02-坐标与数据.md)。同名的接口在 `src/gis-view/types.d.ts`。
26
26
  - **页面上查**:`docs/api/index.html`(API 速查)—— 左栏按 16 个分组列出全部成员,带搜索(Ctrl+K)与方法 / 属性 / options 三档开关,当前读到的分组会跟着高亮;右栏每个成员一张卡,参数 / 返回 / 说明之外,示例块直接来自 [`18 逐方法示例`](../manual/18-逐方法示例.md),带高亮与「复制名字 / 复制代码」,旁边还有「在调试页跑 →」;地址栏带 `#updateLine` 这样的 hash 直接跳到那一条。右上角主题按钮与文档中心共用 `gisDocs.reading`,跟随系统 / 浅色 / 深色三档换页不会各记一套;深链落点还留了 `scroll-margin-top`,不会被吸顶栏盖住。页面数据来自 `docs/demos/api-index.js`(生成物,禁止手改),自测页 `docs/steps/selftest-docs.html` 盯着它别掉队。
@@ -105,7 +105,7 @@ API 名字与前面几步是一一对应的;第 11 步再补两件「数据之
105
105
 
106
106
  - `selftest.html`:手写版第 1 ~ 9 步的断言(拾取、草稿的 step 1 / 2、落地、移除、连续新增、平移……);
107
107
  - `selftest-full.html`:真组件上的同一套断言,外加把 `index.html` 的十三个步骤逐个挂载一遍(第 13 步再点一遍画质 / 批量 / 撤销 / 重做 / 功能预设五个按钮);
108
- 这一轮又补了四条:小图上的 GeoJSON 底图在主视图放大 3 倍前后红色描边像素数不变(容差 20%)、小图放大 6 倍时 `pointZoomScale` 指数 0 / 1 的像素对比(指数 1 至少 4 倍面积、指数 0 不长大,且倍数回到 1 时指数 1 也不再放大)、以及 stub host 上「拖节点没有结束事件、空闲计时器也会把 `hideOnInteract` 收起的标签层放出来」。
108
+ 本轮再补五条(双回线关闭后居中代表线、`backgroundStyle` 接管画布底色、主图方形 / 圆形尺寸与 SVG 图标、`container` 外部挂载与 `offset` 动态位移、换容器复用同一根节点并随 `destroy()` 清理);之前补的四条:小图上的 GeoJSON 底图在主视图放大 3 倍前后红色描边像素数不变(容差 20%)、小图放大 6 倍时 `pointZoomScale` 指数 0 / 1 的像素对比(指数 1 至少 4 倍面积、指数 0 不长大,且倍数回到 1 时指数 1 也不再放大)、以及 stub host 上「拖节点没有结束事件、空闲计时器也会把 `hideOnInteract` 收起的标签层放出来」。
109
109
  - `selftest-features.html`(672 条):真组件上的线型 / 线路装饰 / 光标 / 表现型配置断言 —— 把画面用 `exportImage` 读回 2D 画布数像素(实线 > 虚线 > 点线的墨量、`dc_line` 中点装饰的像素差、`showGrid` 只重画一帧),光标则用合成的指针事件与 Ctrl 键读出 `canvas.style.cursor`;装饰那几条还会量「几何变了装饰跟不跟着走」—— 写回新路径、拖绑定的节点、加 / 删拐点之后,装饰的紫色像素质心都要落在新中点上(容差 3px);另外两条量的是**固定档**:`anchor: 'fixed'` + `at` 先把装饰钉在写死的世界坐标上,再把线从水平拖成斜的,装饰必须留在原坐标、不能跑到新中点。线路标签在单独一张画布上验:默认挂在中点、内容取 `name`,`position: 0` 落到起点,`hideOnInteract` 在缩放期间给整层加 `--hidden`、停手恢复,关掉 `lineLabel` 之后标签消失。装饰缺省 `auto` 的那一串也在这里量:没有拐点时拖端点装饰保持同一个比例(一直挂在中点)、加上拐点之后钉在那一刻的坐标上(改折线也不动)、把拐点删回直线又回到比例跟随、`anchor: 'follow'` 则一直跟到折线的新中点;`parallel` 用一条横条符号验(默认包围盒 26×6,平行之后转成 4×26),方形符号走的是 gl.POINTS 那条路(旋转后外接盒从 24×24 撑成 32×32)。通道里「每条回线各显示自己的标签」也在这里验:没有逐条配置时会被 32px 屏幕小格去重(只显示 1 ~ 2 条),给每条回线写上自己的 `lineLabelPosition` 之后 8 条全都在、位置各不相同,斜线上的标签按线路方向转、朝左的自动翻半圈(rotate = 0)。线路的层序(`zIndex`)也在这里验:画布正中一横一竖两条粗线交叉,默认后写的竖线在上(中心像素蓝),规则里给横线写 `zIndex: 5` 就换成红,`updateLine` 给竖线写 `zIndex: 9` 又换回蓝,写回 0 再回到红。这一轮另外验了插件上下文:`options.plugins` 与 `view.use()` 拿到的第二个参数是同一个冻结对象(`apiVersion === 1`、`view` 指回实例),旧的一参插件写法照常可用。这一轮另补 3 条合并触发时机断言(center 差 10px 不触发、继续拖入目标区域触发并带 trigger、collision 同距离照旧触发)。这一轮再补 1 条:13 个旧快捷方法已从实例删除,`updatePoint` / `updateLine` / `updateChannel` / `updateOptions` / `setFilter` / `setDropPreview` 仍在。对账组另补 8 条:`getChangedChannels({ ignoreGroups })` 忽略分组增删、已有分组拐点仍报、`reasons` 收窄与线路字段 / 路径 / 显隐变化,以及删除通道 / 删除线路 / 无通道场景同步清账。本轮再补实例命令的注册 / 同名覆盖 / 异常与销毁断言,并再补 16 条:筛选恢复后通道宽度与回线车道、`focusOn` 显式 `point` / `line` / `channel` / `group` 与统一缩放口径、`selectFeatures` 分组写法、`selectWaypoint` 带选线路,以及 `lineFollowsNode` 的隐藏 / 筛选 / 删除 / 撤销与动态切换。
110
110
  - `selftest-decoration.html`(75 条):动态装饰器草稿专项自测 —— 开启后的 `start` 载荷、默认 14px 触发半径与每条线路不限量、指针在线路上 `hover`(命中线 / 自身 `decoration` / 平行角)、同一位置重复移动不重复发事件、移出目标 `leave`、在线路与空白处 `pointerup` 的 `ready` 差异、按下 / 抬起 `action.type` 都是 `decorationDraft`、单线 / 分组 / 整条通道三档作用域与非通道回退、`maxPerLine` 满额时悬浮与松手都不触发且提交前复核不写数据、`allowedLines` / `allowedChannels` / `allowedGroups` 三种白名单在悬浮与提交两处都拦截、`update` 改图标 / 尺寸 / 方向、`commit` 一次 `batch` 写四条且只发一条 `dataChange`、`cancel` 不改数据、重复 `begin` 替换旧草稿、整份换数据后过期草稿静默清掉。节点侧另有 32 条:`target: 'point'` 命中与只展开一个节点目标、方向 / 距离换算成屏幕偏移、相对偏移 / 固定世界坐标 / 鼠标位置三种位置来源、连接线样式随提交保存、`allowedPoints` 与 `maxPerPoint` 在悬浮 / 提交两处拦截、`both` 等距时节点优先且最近节点被白名单拦住时回退线路、节点与线路的重复 `reject` / `offset` 两种策略、节点装饰 get / update / remove 与整批提交、度转角与连接线进入矢量快照。这一轮另补 8 条节点断言:既有的 2 条「装饰随移动」仍保留;新增 6 条覆盖 `pointer + follow`、`fixed + follow`、`offset + fixed` 三种跨组合的预览与提交结果,确认位置来源和落地关系解耦后分别写成 `offset` 或 `at`。
111
111
  - `selftest-demo.html`:demo 级自测 —— 面板上的「定位直流线路」按当前数据集的 `dc_line` 条数走(默认数据集 `new.json` 里现在自带 5 条,点一下直接选中并定位到第一条;一条都没有的数据集才切到样例数据、定位到「直流送电」,中点挂着 `PATH_DC_LINE` 装饰),断言按数据算出来的条数写、不写死数字,读数与选中面板都跟着更新;打开时说明段落默认收起、面板按组折叠,节点标签只给最高电压那一档、线路标签默认整个不显示(面板上勾「显示」再按「显示范围」展开),面板上有 PATH_DC_LINE 的「标记定位」下拉(自动 / 一直跟随 / 写死坐标三档)。「通道回线各自显示」这一档从数据层核对(勾上之后 3669 条回线都带上了自己的 `lineLabelPosition`),再放大三步后勾「与线路平行」,画出来的标签 transform 里要出现 `rotate(...)`。这一轮又补了「数据映射」下拉(raw / lngLat / fit / scale)与「节点 / geojson 比例」滑块:切到 fit 后 320 个站点都要落进 GeoJSON 画出来的范围、读数里给出换算比例;「经纬度 → 屏幕」「屏幕 → 经纬度」两个按钮来回换算一致(屏幕读数保留 1 位小数,允许 0.01° 的回差);在画布上挪一下鼠标,面板上就要出现屏幕像素与经纬度两行读数。
@@ -116,11 +116,11 @@ API 名字与前面几步是一一对应的;第 11 步再补两件「数据之
116
116
  - `selftest-labels.html`(165 条):标签拖动与编辑、HTML 语义、高密度画布、导出分项多选的专项自测 —— 按住标签拖 90 px 之后标签真的挪了那么多、同时画布不平移,`labelDrag` 的 `start` / `move` / `end` 三态与 `offset` / `delta` 对得上,偏移进得了 `getLabelStates()`、再拖动节点时偏移照旧叠在节点位移上,`resetLabel` 之后回到默认锚点;`updateLabel` 改内容 / 字号 / 颜色(`style` 是增量合并,只改字号不动颜色)、`resetLabels` 一次清干净;点一下标签发 `labelClick` 并把这一条交给面板;线路标签的拖动与恢复默认同一条路;导出侧验「选什么导什么」(选场站只出场站、选通道自动带上拐点、不认识的分项记一条提示),`exportData({ scope: 'selection' })` 验场站 + 通道 / 只出线路 / 没选中时的 `ok: false` 三个分支。HTML 专项验全局 / 逐条 `true` / `false`、模板与 `field` / 默认名称转义、`updateLabel` 双向切换、线路标签富文本、点击载荷保留 `html` 与原标记、`getLabels()` 报运行时偏移、只改样式不替换 HTML 子节点、导出 → 回灌保持逐条语义,以及 120 条同内容标签首轮布局读取 ≤ 4 次的缓存断言。高密度专项用 1100 条纯文本验缺省 `auto` 只留一张画布、混入一条 `html: true` 仍保留那一条 DOM、`labelRender: 'dom'` 回退逐条 DOM 并清空画布、切回 `auto` 回收上一轮 div、SVG 导出包含画布标签与缩放只推一次;新增断言还验 1100 条同文案只调用 `measureText` ≤ 2 次、同一任务连续 5 次缩放只重绘 1 ~ 2 次、显式 `canvas` 在优化档遵守 `maxCount: 200`,以及 6 条线路标签在优化档全部进画布且 DOM 为 0。另验 GeoJSON 区域文字同一相机重复刷新 0 次 `style` 写入、只改字号复用同一 DOM 节点、离屏时改样式再回到视野仍能恢复显示、只改锚点位置也会重新投影,以及同一帧 12 次相机变化只做 1 次重投影。设备像素专项把 `devicePixelRatio` 临时设为 1.25,验标签落点与拖动期整层位移都落在物理像素网格上,并确认逐条标签不再各自声明 `will-change`。
117
117
  - `selftest-actions.html`:事件判读与只读接口的专项自测 —— 左右键、拖动、框选、拐点与标签的 `action` 三件套(type / target / data)、`button` / `modifiers` / `moved` / `double`、Shift 框选(含没框到东西的空框)与 Ctrl 取反在 `selectChange.action.type` 上的分辨、`pickAt` 的客户端坐标与只读性、`getFeaturesInRect`(含宽高为负的矩形)与 `getChannelsInView`、`addPoints` / `addPoint` / `addLines` / `removeLines` 的返回与 `dataChange` 类型;最后一段把第 12 步 `step-12-events.js` 挂起来跑一遍(判读读数、`probeAt` / `probeRect`、两条走廊)。页面底部流式打印 PASS / FAIL。
118
118
  - `selftest-select.html`:选择与框选的专项自测(69 条)—— 点选 / 悬停 / Ctrl 取反 / 多选开关的像素级状态样式与恢复,`select` / `deselect` / 通道写法 / 变更来源的接口断言,以及 SVG 状态贴图的预热与不同尺寸配方;这一轮补了 `selectionKinds: 'all'` 下 `selectInBox` 一次收下节点、独立线路与按 `channelSelectMode` 摊开的通道回线;本轮再补 SVG 导出 `currentStrokeColor` 的描边色还原,以及 `selectTrigger` 默认单击、双击选中、空白双击清空、运行时切回单击与双击 + `multiSelect` 的取反 / 累加 / 取消。
119
- - `selftest-demos.html`:演示外壳与 22 个页签(21 个功能 + 综合,按基础 / 交互 / 样式 / 数据 / 性能 / 综合六组排列)的自测(337 条)—— 逐个用 `?demo=<id>` 打开基础视图、标签交互、标签显示、鹰眼图配置、样式与规则、悬停与提示、通道与数据、编辑与保存、事件与方法、接口能力、批量复制与合并、拖拽落点、可视化连线、动态装饰器、批量新增、搜索与筛选、扩展能力、性能与质量、点聚合、动画效果、扩展图形与综合示例,画布与右栏读数都在、切页签会把 `?demo=` 写回地址栏、切走之后上一个实例被销毁(不留画布)、缺省页签是综合示例、页签总数与注册表一致、顶部六个分组按钮一次只展开一组且组名可见;鹰眼图配置页另验 `layers` 的自定义 `draw` 真被调用、`placement` 切到数据前 / 后时 `drawInfo.stage` 跟着变、框拖动触发 `onInteraction` 并带相机、尺寸切换走 `updateOptions` 重铺画布、DPR 2 下 `maxPixelRatio` 的 1× / 2× 备份尺寸、关掉扩展层与 `zoomMinimapBy` / `setMinimapZoom`;批量复制 / 合并、通道拐点 / 分组两页另有假接口断言(Shift 框选、整份请求体、标准响应包与资源级结果、版本号 / opId、200 / 网络失败 / 500 / 409、原 opId 重试与幂等重放不推进版本、409 rebase、本地回滚);假后端严格契约另有一组直接断言(请求头与 body 不一致返回 400、baseVersion 非有限数字返回 400、同 opId 换 body 返回 409 `IDEMPOTENCY_CONFLICT` 且版本不前进),通道页还验了自定义格式按钮注册 → 导出 → 注销;扩展能力页另验自定义运算符 / 图元 / `view.use` 插件 / `defineExportFormat` / 实例命令的注册、执行、同名覆盖、卸载与业务 DTO 结构,并验同名重新注册图元当场换画笔;可视化连线页另验初始 `channels` 为空、正交折线拐点、预览箭头、取消不写数据,以及默认提交 / `addLines` / `addChannels` 三种落地;动态装饰器页另验吸附方式只有「跟随 / 固定」、两档下位置都有「相对节点 / 固定位置 / 鼠标位置」三档,固定 + 相对节点只改 `anchor`,固定 / 跟随 + 鼠标位置分别映射为 `placement: pointer` + 对应 `anchor` 且不带 `at`,跟随 + 固定位置仍传 `at`;通道与数据页另验「线路分组」弹框:分组卡片与「场站对标标识」都在,行内「确认移入」只改本地草稿、组件数据不动,取消 / Esc / 遮罩关闭不写数据,「确定分组」只调用一次 `splitChannelGroups` 把整份 groups 一次落地。点聚合页另验配置控件、聚合点击只增聚合计数、`maxZoom` 展开与适应全部后恢复、半径 / 重建 / 关闭统计;接口能力页另验三种级联触发独立开关、通道 / 分组 / 拐点程序化选中、通道 `focusOn`、`lineFollowsNode` 联动,以及样式字段合并、节点 / 线路装饰 CRUD、通道线宽与线路对账、GeoJSON 背景投影、历史 / 复制 / 删除收尾、`once`、WebGL 层清理、注册表注销与初始化 options 读数;页面底部流式打印 PASS / FAIL。这一轮另补 3 条鹰眼图线路层断言:首帧只铺固定范围里的线路、同一份数据补画命中缓存、小图缩放只重铺一次且仍裁剪范围外线路。
119
+ - `selftest-demos.html`:演示外壳与 22 个页签(21 个功能 + 综合,按基础 / 交互 / 样式 / 数据 / 性能 / 综合六组排列)的自测(**347** 条)—— 逐个用 `?demo=<id>` 打开基础视图、标签交互、标签显示、鹰眼图配置、样式与规则、悬停与提示、通道与数据、编辑与保存、事件与方法、接口能力、批量复制与合并、拖拽落点、可视化连线、动态装饰器、批量新增、搜索与筛选、扩展能力、性能与质量、点聚合、动画效果、扩展图形与综合示例,画布与右栏读数都在、切页签会把 `?demo=` 写回地址栏、切走之后上一个实例被销毁(不留画布)、缺省页签是综合示例、页签总数与注册表一致、顶部六个分组按钮一次只展开一组且组名可见;鹰眼图配置页另验 `layers` 的自定义 `draw` 真被调用、`placement` 切到数据前 / 后时 `drawInfo.stage` 跟着变、框拖动触发 `onInteraction` 并带相机、尺寸切换走 `updateOptions` 重铺画布、DPR 2 下 `maxPixelRatio` 的 1× / 2× 备份尺寸、关掉扩展层与 `zoomMinimapBy` / `setMinimapZoom`,以及 `extentFollowCamera` 默认关闭 / 打开生效、缩放关闭时 `dragAction: 'box'` 仍可框选、`zoomable: 'follow'` 跟随主图且 `setMinimapZoom` 不抢控制权;批量复制 / 合并、通道拐点 / 分组两页另有假接口断言(Shift 框选、整份请求体、标准响应包与资源级结果、版本号 / opId、200 / 网络失败 / 500 / 409、原 opId 重试与幂等重放不推进版本、409 rebase、本地回滚);假后端严格契约另有一组直接断言(请求头与 body 不一致返回 400、baseVersion 非有限数字返回 400、同 opId 换 body 返回 409 `IDEMPOTENCY_CONFLICT` 且版本不前进),通道页还验了自定义格式按钮注册 → 导出 → 注销;扩展能力页另验自定义运算符 / 图元 / `view.use` 插件 / `defineExportFormat` / 实例命令的注册、执行、同名覆盖、卸载与业务 DTO 结构,并验同名重新注册图元当场换画笔;可视化连线页另验初始 `channels` 为空、正交折线拐点、预览箭头、取消不写数据,默认提交 / `addLines` / `addChannels` 三种落地,指定起点后一次点击即可 ready、两端示例点的显隐 / 尺寸 / 形状 / 分端颜色与 `below-points` 层级、`commitAddChannel` 直接传线路 / 通道对象(含对象 path / 颜色 / 线宽优先);动态装饰器页另验吸附方式只有「跟随 / 固定」、两档下位置都有「相对节点 / 固定位置 / 鼠标位置」三档,固定 + 相对节点只改 `anchor`,固定 / 跟随 + 鼠标位置分别映射为 `placement: pointer` + 对应 `anchor` 且不带 `at`,跟随 + 固定位置仍传 `at`;通道与数据页另验「线路分组」弹框:分组卡片与「场站对标标识」都在,行内「确认移入」只改本地草稿、组件数据不动,取消 / Esc / 遮罩关闭不写数据,「确定分组」只调用一次 `splitChannelGroups` 把整份 groups 一次落地。点聚合页另验配置控件、聚合点击只增聚合计数、`maxZoom` 展开与适应全部后恢复、半径 / 重建 / 关闭统计;接口能力页另验三种级联触发独立开关、通道 / 分组 / 拐点程序化选中、通道 `focusOn`、`lineFollowsNode` 联动,以及样式字段合并、节点 / 线路装饰 CRUD、通道线宽与线路对账、GeoJSON 背景投影、历史 / 复制 / 删除收尾、`once`、WebGL 层清理、注册表注销与初始化 options 读数;页面底部流式打印 PASS / FAIL。鹰眼图线路层另有 3 条断言:首帧只铺固定范围里的线路、同一份数据补画命中缓存、小图缩放只重铺一次且仍裁剪范围外线路。
120
120
  - `selftest-effects.html`:线路 / 节点动画与贴图预热的专项自测(96 条,页面自带「帧泵」—— 无头环境里 `requestAnimationFrame` 几乎不推进,它把 rAF 收进 Map、手动推帧,动画类断言才测得动):纯函数组(归一化、三层「关」的语义、按弧长采样、拐点连续、相位打散)、引擎组(铺批次 / 关掉一层 / 恢复 / 逐要素覆盖 / `getLineEffects` 全程贴线)、像素组(画布上真的有一个红点在两帧之间挪了位置)、**涟漪跟手组**(开着动画拖节点:拖动前圆心在节点坐标上、拖动中跟着走且是就地改实例缓冲、松手后整份重建也对齐、再拖回原位跟着回来,另有一条像素级断言量绿圈质心落在新坐标上)、**预热组**(全排列条数、第二次命中缓存、`getSymbolCombos()` 的去重与线路侧那一条、照着它配的一份**零张新画布**、反向一条「没画过的组合会当场烤、画布 +1」)、提醒组(`effects.line` 里写 `show` 被点名);这一轮另补 4 条动点缓存断言(5000 条普通线 + 2 条动点线:每帧只遍历动画线路,路径换短 / 换长后总长跟着更新)。
121
121
  - `selftest-data.html`:标准数据文档与渲染后端开关的自测(103 条)—— `normalizeDocument` 的标准写法与 JSON 安全(无效日期写 `null` 并告警、类实例不静默变成空对象)、整份文档直接当 `createGisView` 入参、`exportData({ format: 'full' })` 导出 → 重建 → 再导出逐字节相同、`importData` 的整份与分列、`renderer` 三档与能力探测、网格顶点缓存窗口,以及自定义导出格式的注册 / 上下文 DTO 映射 / 深拷贝 / 视口导出 / 注销后失效;断言全在接口层,不读像素。
122
122
  - `check-sample-fields.mjs`:不是页面、是对账脚本(`node docs/steps/check-sample-fields.mjs`)—— 核对 `sample-document.json` 自己就是标准写法(`normalizeDocument` 后逐字节相同、0 条提醒)、`docs/sample-document字段详解.md` 里的读数与 `documentCounts()` 一致、以及它的「字段索引」与从文件里摘出来的字段路径集合一个不多一个不少(当前 154 个);改完样本数据跑 `--write` 重写那两段机器块(幂等)。
123
- - `selftest-docs.html`:三个文档页 + 调试页的自测(134 条)—— 文档中心(`docs/index.html`)的清单 92 份 md(10 个根 + 82 份子文档,含新的 `接口协议.md`;树形侧栏按 `parent` / `groupLabel` 分成 6 组 `details`)、首屏 README 渲染、markdown 渲染器(表格 / 带语言代码块 / 两级列表 / 引用 / 行内代码 / md 链接 / 目录 / 首条摘要卡片 / 首段导语)、元信息里的阅读时长、索引页的子文档链接、换一份子文档、代码块复制按钮、跨文档搜索与深链、跨文档锚点链接(`文件.md#小节` 原地切文档并闪一下)、能直接跑的围栏才有的「在调试页跑」与元信息行的「这一份的调试页(N 段示例)」、390px 窄屏首屏 / 深链 / 横向溢出回归、阅读百分比与「回到顶部」的联动;调试演示页(`docs/playground/index.html`)的一个示例下拉(内置示例 + 「这一份文档」两组 optgroup)、`?doc=` 读文档抽示例、片段标注与来源子栏、顶层 await 运行、`channels` 快照、光标行列、草稿自动保存与恢复、`Ctrl+S` 运行、复制 / 清空输出、状态胶囊;API 速查页(`docs/api/index.html`)的 `__gisApi` 钩子、六张统计卡、256 张成员卡都带签名网格与示例、96 个 options 四列网格、搜索过滤与命中读数、`select(名字)` 高亮、类型开关能单独收起属性、左侧当前分组高亮与即时跳转、顶栏文档深链、每个分组的手册章节链接、深浅主题切换与阅读设置共用;图解页(`docs/guide/sample-document.html`)的 47 条数据列表、点线路 / 分组 / 标签状态的图上标注、`点击时居中` 与 `显示编号` 开关、清空标注;三个页面都跑在 iframe 里,要用 dev server 打开。
123
+ - `selftest-docs.html`:三个文档页 + 调试页的自测(134 条)—— 文档中心(`docs/index.html`)的清单 92 份 md(10 个根 + 82 份子文档,含新的 `接口协议.md`;树形侧栏按 `parent` / `groupLabel` 分成 6 组 `details`)、首屏 README 渲染、markdown 渲染器(表格 / 带语言代码块 / 两级列表 / 引用 / 行内代码 / md 链接 / 目录 / 首条摘要卡片 / 首段导语)、元信息里的阅读时长、索引页的子文档链接、换一份子文档、代码块复制按钮、跨文档搜索与深链、跨文档锚点链接(`文件.md#小节` 原地切文档并闪一下)、能直接跑的围栏才有的「在调试页跑」与元信息行的「这一份的调试页(N 段示例)」、390px 窄屏首屏 / 深链 / 横向溢出回归、阅读百分比与「回到顶部」的联动;调试演示页(`docs/playground/index.html`)的一个示例下拉(内置示例 + 「这一份文档」两组 optgroup)、`?doc=` 读文档抽示例、片段标注与来源子栏、顶层 await 运行、`channels` 快照、光标行列、草稿自动保存与恢复、`Ctrl+S` 运行、复制 / 清空输出、状态胶囊;API 速查页(`docs/api/index.html`)的 `__gisApi` 钩子、六张统计卡、257 张成员卡都带签名网格与示例、96 个 options 四列网格、搜索过滤与命中读数、`select(名字)` 高亮、类型开关能单独收起属性、左侧当前分组高亮与即时跳转、顶栏文档深链、每个分组的手册章节链接、深浅主题切换与阅读设置共用;图解页(`docs/guide/sample-document.html`)的 47 条数据列表、点线路 / 分组 / 标签状态的图上标注、`点击时居中` 与 `显示编号` 开关、清空标注;三个页面都跑在 iframe 里,要用 dev server 打开。
124
124
 
125
125
  上一轮补上的两组断言:`selftest-features.html` 新增 8 条 —— 3 条验 `channelSelectMode` 在 Ctrl + 点击加点后的高亮范围(`line` / `group` / `channel`),5 条验分组 `channelWaypointDragMode: 'vertex'`(`{ x, y }` 偏移归一化并参与实际路由、同组同序号拐点按世界增量平移、相邻拐点与其它分组不动、只写 `waypointOffsets` 不改公共 `path`、`parallel_groups.group_route` 带刚性偏移)。`selftest-demos.html` 新增 1 条,验通道演示页可切到 `vertex`,且「准备两分组拐点」会拆成两组、每组两个拐点。
126
126
 
@@ -142,12 +142,14 @@
142
142
  miniHost.style.cssText = 'position:absolute;left:-9999px;width:180px;height:120px'
143
143
  document.body.append(miniHost)
144
144
  const miniData = {
145
- points: [{ x: 50, y: 50 }],
145
+ points: [{ x: 50, y: 50, size: 10, inheritSize: true }],
146
146
  lines: [
147
147
  { path: [[0, 0], [100, 0]], minX: 0, minY: 0, maxX: 100, maxY: 0 },
148
148
  { path: [[1000, 1000], [1100, 1000]], minX: 1000, minY: 1000, maxX: 1100, maxY: 1000 }
149
149
  ]
150
150
  }
151
+ const miniViewport = { cssWidth: 450, cssHeight: 600, pixelRatio: 1, displayScale: 1 }
152
+ let miniCameraScale = 1
151
153
  const mini = createMinimap(miniHost, {
152
154
  enabled: true,
153
155
  width: 180,
@@ -157,8 +159,8 @@
157
159
  maxPixelRatio: 1
158
160
  }, {
159
161
  getData: () => miniData,
160
- getCamera: () => ({ center: [50, 50], scale: 1 }),
161
- getViewport: () => ({ width: 180, height: 120, pixelRatio: 1, displayScale: 1 }),
162
+ getCamera: () => ({ center: [50, 50], scale: miniCameraScale }),
163
+ getViewport: () => miniViewport,
162
164
  getFallbackBounds: () => ({ minX: 0, minY: 0, maxX: 100, maxY: 100 }),
163
165
  getZoomBaseScale: () => 0,
164
166
  getBackground: () => null
@@ -168,6 +170,21 @@
168
170
  check('鹰眼图线路层:首帧只铺固定范围里的线路(范围外那条被跳过)',
169
171
  miniFirst.lineRasterDraws > 0 && miniFirst.linePathsDrawn === miniFirst.lineRasterDraws,
170
172
  JSON.stringify(miniFirst))
173
+ const inheritedAtBase = miniFirst.paintedInheritedSizeFactor
174
+ miniCameraScale = 2
175
+ mini.refresh()
176
+ await wait(100)
177
+ const inheritedAfterMainZoom = mini.getPaintStats().paintedInheritedSizeFactor
178
+ check('鹰眼图:沿用主图尺寸按 CSS 视口比例缩放,主图放大不改变继承节点大小',
179
+ Math.abs(inheritedAtBase - 0.2) < 1e-6 && Math.abs(inheritedAfterMainZoom - inheritedAtBase) < 1e-6,
180
+ `${inheritedAtBase} -> ${inheritedAfterMainZoom}`)
181
+ miniViewport.cssHeight = 300
182
+ mini.refresh()
183
+ await wait(100)
184
+ const inheritedAfterResize = mini.getPaintStats().paintedInheritedSizeFactor
185
+ check('鹰眼图:主图视口尺寸变化后,沿用主图尺寸按新的视口比例重铺',
186
+ Math.abs(inheritedAfterResize - 0.4) < 1e-6,
187
+ String(inheritedAfterResize))
171
188
  mini.refresh()
172
189
  await wait(100)
173
190
  const miniRepeat = mini.getPaintStats()
@@ -422,6 +439,25 @@
422
439
  const draftArrow = channelDraftArrowPlacement([[0, 0], [60, 0], [60, 80]])
423
440
  check('可视化连线:预览箭头中心按整条折线弧长取中点(不是贴在最后一段末端),与落地线路的单箭头同口径', Boolean(draftArrow) && Math.abs(draftArrow.center[0] - 60) < 1e-9 && Math.abs(draftArrow.center[1] - 10) < 1e-9 && draftArrow.unitX === 0 && draftArrow.unitY === 1, JSON.stringify(draftArrow))
424
441
 
442
+ const startPointSelect = document.querySelector('[data-demo=connect-start-point]')
443
+ startPointSelect.value = 'node-0'
444
+ startPointSelect.dispatchEvent(new Event('change', { bubbles: true }))
445
+ await wait(60)
446
+ document.querySelector('[data-demo=connect-start]').click()
447
+ await wait(80)
448
+ const presetStartDraft = view.getAddChannelDraft()
449
+ check('可视化连线:下拉指定起点后,beginAddChannel 直接进入 from 草稿且下一次点击只选终点', presetStartDraft?.phase === 'from' && String(presetStartDraft?.from?.id) === 'node-0', presetStartDraft ? `${presetStartDraft.phase} / from ${presetStartDraft.from?.id}` : '没有草稿')
450
+ await clickNode('node-3')
451
+ const presetReadyDraft = await until(() => {
452
+ const current = view.getAddChannelDraft()
453
+ return current?.phase === 'ready' ? current : null
454
+ }, 2500)
455
+ check('可视化连线:指定起点 + 一次节点点击即可形成 ready 草稿', presetReadyDraft?.from?.id === 'node-0' && presetReadyDraft?.to?.id === 'node-3', presetReadyDraft ? `${presetReadyDraft.from?.id} → ${presetReadyDraft.to?.id}` : '没有 ready 草稿')
456
+ view.cancelAddChannel()
457
+ startPointSelect.value = ''
458
+ startPointSelect.dispatchEvent(new Event('change', { bubbles: true }))
459
+ await wait(80)
460
+
425
461
  const cancelBefore = view.getData()
426
462
  const cancelDraft = await selectPair('node-0', 'node-3')
427
463
  const style = cancelDraft?.style ?? view.getOptions().channelDraftStyle
@@ -457,6 +493,21 @@
457
493
  const spacingInput = document.querySelector('[data-demo=connect-spacing]')
458
494
  spacingInput.value = '26'
459
495
  spacingInput.dispatchEvent(new Event('input', { bubbles: true }))
496
+ const dotSizeInput = document.querySelector('[data-demo=connect-dot-size]')
497
+ dotSizeInput.value = '9'
498
+ dotSizeInput.dispatchEvent(new Event('input', { bubbles: true }))
499
+ const dotShapeSelect = document.querySelector('[data-demo=connect-dot-shape]')
500
+ dotShapeSelect.value = 'square'
501
+ dotShapeSelect.dispatchEvent(new Event('change', { bubbles: true }))
502
+ const startDotColorInput = document.querySelector('[data-demo=connect-dot-from-color]')
503
+ startDotColorInput.value = '#00aa88'
504
+ startDotColorInput.dispatchEvent(new Event('input', { bubbles: true }))
505
+ const endDotColorInput = document.querySelector('[data-demo=connect-dot-to-color]')
506
+ endDotColorInput.value = '#aa2200'
507
+ endDotColorInput.dispatchEvent(new Event('input', { bubbles: true }))
508
+ const draftLayerSelect = document.querySelector('[data-demo=connect-draft-layer]')
509
+ draftLayerSelect.value = 'below-points'
510
+ draftLayerSelect.dispatchEvent(new Event('change', { bubbles: true }))
460
511
  await wait(100)
461
512
  const beforeBlankCancel = view.getData()
462
513
  document.querySelector('[data-demo=connect-start]').click()
@@ -467,10 +518,14 @@
467
518
  return current?.phase === 'from' ? current : null
468
519
  }, 2500)
469
520
  check('可视化连线:3 回通道草稿把箭头位置与回线间距直接带进本次样式', stagedDraft?.kind === 'channel' && stagedDraft?.circuits === 3 && stagedDraft?.style?.arrowPositions?.join(',') === '0.25,0.75' && stagedDraft?.style?.circuitSpacing === 26, stagedDraft ? JSON.stringify({ kind: stagedDraft.kind, circuits: stagedDraft.circuits, arrows: stagedDraft.style?.arrowPositions, spacing: stagedDraft.style?.circuitSpacing }) : '没有 from 草稿')
521
+ check('可视化连线:两端示例点的尺寸 / 形状 / 分端颜色与 below-points 层级都进入草稿样式', stagedDraft?.style?.dots?.size === 9 && stagedDraft?.style?.dots?.shape === 'square' && stagedDraft?.style?.dots?.from?.color === '#00aa88' && stagedDraft?.style?.dots?.to?.color === '#aa2200' && stagedDraft?.style?.layer === 'below-points', JSON.stringify(stagedDraft?.style ? { dots: stagedDraft.style.dots, layer: stagedDraft.style.layer } : null))
470
522
  await clickCanvasAt(box.left + 18, box.top + box.height - 18)
471
523
  const blankCancelled = await until(() => view.getAddChannelDraft() === null, 2500)
472
524
  const afterBlankCancel = view.getData()
473
525
  check('可视化连线:选好起点后点空白立即取消,readout 报 click-outside-node 且数据不增加', Boolean(blankCancelled) && text('connect-cancel-readout').includes('click-outside-node') && afterBlankCancel.lines.length === beforeBlankCancel.lines.length && afterBlankCancel.points.length === beforeBlankCancel.points.length && channelList().length === 0, `${text('connect-cancel-readout')} / ${beforeBlankCancel.lines.length} -> ${afterBlankCancel.lines.length} 线`)
526
+ draftLayerSelect.value = 'above'
527
+ draftLayerSelect.dispatchEvent(new Event('change', { bubbles: true }))
528
+ await wait(60)
474
529
 
475
530
  // 恢复独立线路配置,继续验证后面的真实落地入口
476
531
  kindSelect.value = 'line'
@@ -480,6 +535,13 @@
480
535
  spacingInput.value = '12'
481
536
  spacingInput.dispatchEvent(new Event('input', { bubbles: true }))
482
537
  await wait(100)
538
+ const beforeObjectLine = view.getData()
539
+ const objectLineDraft = await selectPair('node-3', 'node-6')
540
+ document.querySelector('[data-demo=connect-commit-object]').click()
541
+ await wait(160)
542
+ const afterObjectLine = view.getData()
543
+ const objectLine = afterObjectLine.lines.at(-1)
544
+ check('可视化连线:commitAddChannel 直接传线路对象即可落地,对象里的 path / 颜色 / 线宽优先于草稿', Boolean(objectLineDraft) && afterObjectLine.lines.length === beforeObjectLine.lines.length + 1 && channelList().length === 0 && String(objectLine?.id).startsWith('connect-object-line-') && (objectLine?.path?.length ?? 0) > 2 && objectLine?.color === '#2f6fd0' && objectLine?.width === 2.4, `${beforeObjectLine.lines.length} -> ${afterObjectLine.lines.length} 线 / ${objectLine?.id ?? '无'} / path ${objectLine?.path?.length ?? 0} / ${objectLine?.color} ${objectLine?.width}`)
483
545
  kindSelect.value = 'channel-2'
484
546
  kindSelect.dispatchEvent(new Event('change', { bubbles: true }))
485
547
  await wait(80)
@@ -490,6 +552,13 @@
490
552
  const afterChannels = view.getData()
491
553
  const addedChannel = channelList().at(-1)
492
554
  check('可视化连线:addChannels 入口真实新建通道并按 circuits 加回线', Boolean(channelDraft) && channelList().length === 1 && afterChannels.lines.length === beforeChannels.lines.length + 2 && addedChannel?.path?.length === 1, `${channelList().length} 通道 / ${beforeChannels.lines.length} -> ${afterChannels.lines.length} 线 / path ${addedChannel?.path?.length ?? 0} / ${text('connect-action-readout')}`)
555
+ const beforeObjectChannel = view.getData()
556
+ const objectChannelDraft = await selectPair('node-2', 'node-10')
557
+ document.querySelector('[data-demo=connect-commit-object]').click()
558
+ await wait(180)
559
+ const afterObjectChannel = view.getData()
560
+ const objectChannel = channelList().at(-1)
561
+ check('可视化连线:commitAddChannel 直接传通道对象即可按 lineIds / groups 落地', Boolean(objectChannelDraft) && channelList().length === 2 && afterObjectChannel.lines.length === beforeObjectChannel.lines.length + 2 && String(objectChannel?.id).startsWith('connect-object-channel-') && objectChannel?.lineIds?.length === 2 && objectChannel.lineIds.every(id => String(id).startsWith('connect-object-line-')), `${channelList().length} 通道 / ${beforeObjectChannel.lines.length} -> ${afterObjectChannel.lines.length} 线 / ${objectChannel?.id ?? '无'} / ${JSON.stringify(objectChannel?.lineIds ?? [])}`)
493
562
  check('可视化连线:读数面板同步报出纯点 + 线 / 通道数量', text('connect-data-readout').includes('节点') && text('connect-data-readout').includes('通道'), text('connect-data-readout'))
494
563
  }
495
564
  else {
@@ -573,6 +642,81 @@
573
642
  document.querySelector('[data-demo=minimap-zoom-reset]').click()
574
643
  await wait(120)
575
644
  check('鹰眼图配置:1× 按钮把倍数复位成 1', Math.abs(view.getMinimapZoom() - 1) < 1e-9, text('minimap-zoom-readout'))
645
+ const extentFollowInput = document.querySelector('[data-demo=minimap-extent-follow]')
646
+ check('鹰眼图配置:extentFollowCamera 默认关闭,自动范围仍按数据 ∪ 底图', Boolean(extentFollowInput) && !extentFollowInput.checked && setup.minimapOptions().extentFollowCamera === false && view.getOptions().minimap.extentFollowCamera === false, extentFollowInput ? `checked ${extentFollowInput.checked} / option ${setup.minimapOptions().extentFollowCamera}` : '没有范围开关')
647
+ extentFollowInput.click()
648
+ await wait(160)
649
+ check('鹰眼图配置:范围跟相机开关打开后下发 extentFollowCamera=true', setup.minimapOptions().extentFollowCamera === true && view.getOptions().minimap.extentFollowCamera === true, String(setup.minimapOptions().extentFollowCamera))
650
+ extentFollowInput.click()
651
+ await wait(120)
652
+ const pointSizeScaleInput = document.querySelector('[data-demo=minimap-point-size-scale]')
653
+ const lineWidthScaleInput = document.querySelector('[data-demo=minimap-line-width-scale]')
654
+ check(
655
+ '鹰眼图配置:节点 / 线路独立整体倍数控件默认都是 1',
656
+ Boolean(pointSizeScaleInput) && Boolean(lineWidthScaleInput) &&
657
+ setup.minimapOptions().pointSizeScale === 1 && setup.minimapOptions().lineWidthScale === 1,
658
+ `${setup.minimapOptions().pointSizeScale} / ${setup.minimapOptions().lineWidthScale}`
659
+ )
660
+ pointSizeScaleInput.value = '2'
661
+ pointSizeScaleInput.dispatchEvent(new Event('input', { bubbles: true }))
662
+ lineWidthScaleInput.value = '1.6'
663
+ lineWidthScaleInput.dispatchEvent(new Event('input', { bubbles: true }))
664
+ pointSizeScaleInput.dispatchEvent(new Event('change', { bubbles: true }))
665
+ await wait(160)
666
+ check(
667
+ '鹰眼图配置:独立整体倍数会随控件下发到 pointSizeScale / lineWidthScale',
668
+ setup.minimapOptions().pointSizeScale === 2 && setup.minimapOptions().lineWidthScale === 1.6 &&
669
+ view.getOptions().minimap.pointSizeScale === 2 && view.getOptions().minimap.lineWidthScale === 1.6,
670
+ `${setup.minimapOptions().pointSizeScale} / ${setup.minimapOptions().lineWidthScale}`
671
+ )
672
+ pointSizeScaleInput.value = '1'
673
+ lineWidthScaleInput.value = '1'
674
+ pointSizeScaleInput.dispatchEvent(new Event('input', { bubbles: true }))
675
+ lineWidthScaleInput.dispatchEvent(new Event('input', { bubbles: true }))
676
+ pointSizeScaleInput.dispatchEvent(new Event('change', { bubbles: true }))
677
+ await wait(120)
678
+
679
+ setup.zoomMode = 'off'
680
+ setup.dragAction = 'box'
681
+ setup.applyMinimap()
682
+ view.zoomBy(2)
683
+ await wait(260)
684
+ const boxRoot = document.querySelector('.gis-view__minimap')
685
+ const boxElement = boxRoot?.querySelector('.gis-view__minimap-box')
686
+ const rootRect = boxRoot?.getBoundingClientRect()
687
+ const viewBoxRect = boxElement?.getBoundingClientRect()
688
+ let boxSelectHit = false
689
+ if (boxRoot && rootRect && viewBoxRect) {
690
+ const startX = rootRect.left + 6
691
+ const startY = rootRect.top + 6
692
+ const outside = startX < viewBoxRect.left || startX > viewBoxRect.right || startY < viewBoxRect.top || startY > viewBoxRect.bottom
693
+ if (outside) {
694
+ const pointer = { bubbles: true, cancelable: true, composed: true, pointerId: 19, pointerType: 'mouse', isPrimary: true, button: 0 }
695
+ boxRoot.dispatchEvent(new PointerEvent('pointerdown', { ...pointer, clientX: startX, clientY: startY }))
696
+ boxRoot.dispatchEvent(new PointerEvent('pointermove', { ...pointer, clientX: startX + 36, clientY: startY + 24 }))
697
+ boxRoot.dispatchEvent(new PointerEvent('pointerup', { ...pointer, clientX: startX + 36, clientY: startY + 24 }))
698
+ boxSelectHit = Boolean(await until(() => text('minimap-interaction').includes('select'), 1800))
699
+ }
700
+ }
701
+ check('鹰眼图配置:缩放关闭时 dragAction=box 仍能在空白处直接框选主图范围', boxSelectHit && setup.minimapOptions().zoomable === false && setup.dragAction === 'box', `${text('minimap-interaction')} / zoomable ${setup.minimapOptions().zoomable} / dragAction ${setup.dragAction}`)
702
+
703
+ setup.zoomMode = 'manual'
704
+ setup.dragAction = 'pan'
705
+ setup.applyMinimap()
706
+ view.fitView(44)
707
+ await wait(260)
708
+ setup.zoomMode = 'follow'
709
+ setup.followZoomScale = 1
710
+ setup.applyMinimap()
711
+ await wait(220)
712
+ const followBase = view.getMinimapZoom()
713
+ view.zoomBy(1.5)
714
+ const followed = await until(() => view.getMinimapZoom() > followBase + 0.05, 1800)
715
+ const followZoom = view.getMinimapZoom()
716
+ check('鹰眼图配置:zoomable=follow 时主图放大,小图整体倍数按 followZoomScale 跟着变化', Boolean(followed) && setup.minimapOptions().zoomable === 'follow' && followZoom > followBase, `${followBase.toFixed(3)} -> ${followZoom.toFixed(3)} / ${setup.minimapOptions().zoomable}`)
717
+ view.setMinimapZoom(1)
718
+ await wait(100)
719
+ check('鹰眼图配置:跟随主图时 setMinimapZoom 不抢控制权,倍数仍由主图决定', Math.abs(view.getMinimapZoom() - followZoom) < 1e-9, `${followZoom} -> ${view.getMinimapZoom()}`)
576
720
  }
577
721
  else {
578
722
  check('鹰眼图配置:画布与视图都起来了', false, `${ready ? 'canvas 就绪' : '没有 canvas'} / ${view ? 'view 就绪' : '没有 view'}`)
@@ -2254,8 +2398,8 @@
2254
2398
  await wait(150)
2255
2399
  const apiItems = () => [...document.querySelectorAll('.demo-api-list:not(.demo-api-list--options) .demo-api-item')]
2256
2400
  const optionItems = () => [...document.querySelectorAll('.demo-api-list--options .demo-api-item')]
2257
- check('实例方法速查:整份成员清单按分组列全(16 组 / 256 个成员)',
2258
- Boolean(ready) && document.querySelectorAll('.demo-api-group').length === 16 && apiItems().length === 256,
2401
+ check('实例方法速查:整份成员清单按分组列全(16 组 / 257 个成员)',
2402
+ Boolean(ready) && document.querySelectorAll('.demo-api-group').length === 16 && apiItems().length === 257,
2259
2403
  `${document.querySelectorAll('.demo-api-group').length} 组 / ${apiItems().length} 个成员`)
2260
2404
  const apiNames = apiItems().map(node => node.querySelector('b')?.textContent)
2261
2405
  const roundNames = ['registerSymbol', 'unregisterSymbol', 'listSymbols']
@@ -520,8 +520,8 @@
520
520
  if (check('API 速查:挂上了 __gisApi 钩子(数据来自 api-index.js,没钩子多半是 file:// 打开的)', Boolean(apiHook))) {
521
521
  const apiDoc = apiFrame.contentDocument
522
522
  const summary = apiHook.summary
523
- check('API 速查:清单来自 api-index(256 个成员 / 253 个方法 / 96 个 options / 16 个分组)',
524
- summary.members === 256 && summary.methods === 253 && summary.options === 96 && summary.sections === 16,
523
+ check('API 速查:清单来自 api-index(257 个成员 / 254 个方法 / 96 个 options / 16 个分组)',
524
+ summary.members === 257 && summary.methods === 254 && summary.options === 96 && summary.sections === 16,
525
525
  JSON.stringify(summary))
526
526
  const topJumps = [...apiDoc.querySelectorAll('.doc-jump')]
527
527
  const jumpTargets = topJumps.map(node => `${node.dataset.docJump}:${node.getAttribute('href')}`)
@@ -559,7 +559,7 @@
559
559
  `${apiDoc.querySelectorAll('.api-member').length} 张卡片 / 属性 ${apiDoc.querySelectorAll('.api-member[data-kind="property"]').length} 张`)
560
560
  const noExample = [...apiDoc.querySelectorAll('.api-member')].filter(node => Number(node.dataset.examples) < 1)
561
561
  check('API 速查:每个成员都带至少一段示例(不出现空卡片)', noExample.length === 0,
562
- noExample.length ? noExample.map(node => node.dataset.name).join(' / ') : '256 张卡片都有示例')
562
+ noExample.length ? noExample.map(node => node.dataset.name).join(' / ') : '257 张卡片都有示例')
563
563
  check('API 速查:96 个 options 都列全(名字 / 类型 / 默认值 / 说明四件套)',
564
564
  apiDoc.querySelectorAll('.api-option').length === summary.options && [...apiDoc.querySelectorAll('.api-option')].every(node => node.querySelector('.name code') && node.querySelector('.type') && node.querySelector('.def') && node.querySelector('.doc')),
565
565
  `${apiDoc.querySelectorAll('.api-option').length} 个`)