gis-view-webgl2 0.1.6 → 0.1.8

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 (55) hide show
  1. package/README.md +9 -8
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +316 -233
  4. package/dist/index.js +1 -1
  5. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +106 -0
  6. package/docs/GisView.md +1 -1
  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 +110 -0
  8. package/docs/demos/BasicDemo.vue +2 -2
  9. package/docs/demos/BatchAddDemo.vue +6 -6
  10. package/docs/demos/ConnectDemo.vue +164 -33
  11. package/docs/demos/CopyMergeDemo.vue +27 -14
  12. package/docs/demos/EventDemo.vue +17 -20
  13. package/docs/demos/LabelDemo.vue +7 -7
  14. package/docs/demos/StyleDemo.vue +1 -1
  15. package/docs/demos/api-index.js +34 -31
  16. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +1 -1
  17. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +1 -1
  18. package/docs/demos/gis-view-demo/panels/DataPanels.vue +1 -1
  19. package/docs/demos/gis-view-demo/useGisViewDemo.js +33 -35
  20. package/docs/format/07-/344/272/213/344/273/266/350/275/275/350/215/267/344/270/216/347/244/272/344/276/213/346/225/260/346/215/256.md +8 -7
  21. package/docs/from0/05-/347/233/270/346/234/272/344/270/216/344/272/244/344/272/222.md +1 -1
  22. package/docs/from0/07-/351/200/232/351/201/223/345/210/206/347/273/204/344/270/216/345/244/232/345/233/236/347/272/277.md +26 -10
  23. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +1 -1
  24. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +49 -16
  25. package/docs/manual/06-/346/225/260/346/215/256/344/270/216/345/234/272/346/231/257/346/233/264/346/226/260.md +9 -10
  26. package/docs/manual/07-/346/214/207/351/222/210/344/270/216/350/246/201/347/264/240/347/274/226/350/276/221.md +60 -36
  27. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +60 -4
  28. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +17 -1
  29. 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 +226 -493
  30. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +2 -2
  31. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +12 -13
  32. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +26 -23
  33. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +23 -20
  34. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +132 -79
  35. package/docs/manual/19-/350/257/273/346/225/260/344/270/216/346/213/226/346/213/275/351/242/204/350/247/210.md +1 -1
  36. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +2 -1
  37. 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 +4 -4
  38. package/docs/steps/README.md +9 -7
  39. package/docs/steps/bench-scale.html +1 -1
  40. package/docs/steps/index.html +6 -6
  41. package/docs/steps/selftest-actions.html +26 -25
  42. package/docs/steps/selftest-data.html +1 -1
  43. package/docs/steps/selftest-demos.html +102 -6
  44. package/docs/steps/selftest-docs.html +3 -3
  45. package/docs/steps/selftest-features.html +284 -52
  46. package/docs/steps/selftest-full.html +3 -3
  47. package/docs/steps/selftest-labels.html +1 -1
  48. package/docs/steps/selftest-perf.html +31 -4
  49. package/docs/steps/selftest-select.html +183 -1
  50. package/docs/steps/step-05-pick.js +4 -8
  51. package/docs/steps/step-07-channels.js +3 -5
  52. package/docs/steps/step-08-labels.js +3 -3
  53. package/docs/steps/step-10-full.js +3 -3
  54. package/docs/steps/step-12-events.js +41 -24
  55. package/package.json +1 -1
@@ -265,7 +265,7 @@
265
265
  { file: 'step-09-minimap.js', factory: 'createMinimapView', load: () => import('./step-09-minimap.js'), title: '鹰眼图', api: '小图 + 视口框', note: '小画布画全景、DOM 画视口框、点与拖都能联动主图(文档第 8 章)' },
266
266
  { file: 'step-10-full.js', factory: 'createFullView', load: () => import('./step-10-full.js'), title: '换成真组件', api: 'createGisView', note: '同一份数据换成 src/gis-view 的完整实现,API 与前面几步一一对应(文档 12.11)' },
267
267
  { file: 'step-11-background.js', factory: 'createBackgroundView', load: () => import('./step-11-background.js'), title: '背景层与初始视图', api: 'background / initialView / applyInitialView', note: '底图走 background.geoJson(几何与 demo 共用同一份生成器),最后一档换成 SVG 路径图片(background.image 只给 d 字符串);GeoJSON 还演示 aspectScale、逐区域配色(regions / itemStyle)与区域文字(label)。首屏落位走 initialView:默认聚焦数据中心放大 1.8 倍,不再一上来铺满整屏(GisView.md:背景层 / 初始视图)' },
268
- { file: 'step-12-events.js', factory: 'createActionView', load: () => import('./step-12-events.js'), title: '事件的动作判读与只读查询', api: 'action / pickAt / getFeaturesInRect / hoverAt', note: '每一条指针事件都带 action:type 是操作(按下时是意图、松开时是刚做完的那件事、点击与右键是真正发生的那一件)、target 是点到了什么(通道回线单独给 channel)、data 是按 target 取到的那份数据,button / modifiers / moved / double 与 left / right、press / duration / longPress 一并给全(中键按住就是临时平移,长按按 longPress 认);两个只读查询 pickAt(鼠标坐标问「这儿是什么」)与 getFeaturesInRect(画布矩形里有什么,与框选同一套判定),再加程序化悬停 hoverAt / clearHover / getHover(把鼠标停到「程序化悬停」按钮上试试)。点一下、框一下、右键一下,读数栏的「判读」都会跟着换(GisView.md:action:一次操作的判读 / 实例方法)' },
268
+ { file: 'step-12-events.js', factory: 'createActionView', load: () => import('./step-12-events.js'), title: '事件的动作判读与只读查询', api: 'action / pickAt / getFeaturesInRect / hoverAt', note: '每一条指针事件都带 action:type 是操作(按下时是意图、松开时是刚做完的那件事、点击与右键是真正发生的那一件)、target 是点到了什么(通道回线单独给 channel)、命中的数据直接读载荷顶层(节点 / 线路 item、通道 channel、标签 label),moved / double / press / duration / longPress 是同一份操作判读;按键、修饰键与 clientX / clientY 读 originalEvent(中键就是 originalEvent.button === 1,长按按 action.longPress 认),选中集读数用 getSelectionInfo();两个只读查询 pickAt(鼠标坐标问「这儿是什么」)与 getFeaturesInRect(画布矩形里有什么,与框选同一套判定),再加程序化悬停 hoverAt / clearHover / getHover(把鼠标停到「程序化悬停」按钮上试试)。点一下、框一下、右键一下,读数栏的「判读」都会跟着换(GisView.md:action:一次操作的判读 / 实例方法)' },
269
269
  { file: 'step-13-engineering.js', factory: 'createEngineeringView', load: () => import('./step-13-engineering.js'), title: '工程化四件套', api: 'quality / batch / history / features', note: '上线前要配的四项:画质四档(setQuality / getQuality,换档只影响绘制,不重排、不动数据)、batch(一批改动作一条 onDataChange 与一步历史,一千次 updatePoint 也只算一步)、history(undo / redo / getHistoryState,Ctrl+Z / Ctrl+Shift+Z 也接上;缺省是关的,这一步显式打开)、features(edit 全开 / view 只读 / static 静态图三个预设,一次切好交互口径)。工具条的「画质档」「批量移动」「撤销 / 重做」「功能预设」逐个点点,读数栏最后三行跟着换(GisView.md:画质 / 批量更新 / 历史 / 功能开关)' }
270
270
  ]
271
271
 
@@ -359,12 +359,12 @@
359
359
  },
360
360
  onDblClick: payload => log('onDblClick', describeElement(payload)),
361
361
  onSelect: payload => log('onSelect', payload ? `${payload.items.length} 个要素` : '清空选中'),
362
- onPointerDown: (payload, event) => {
363
- setReadout('pointer', `按下:${describeElement(payload)}(button ${event.button})`)
362
+ onPointerDown: payload => {
363
+ setReadout('pointer', `按下:${describeElement(payload)}(button ${payload.originalEvent?.button})`)
364
364
  },
365
- onPointerUp: (payload, event) => log('onPointerUp', `${describeElement(payload)}(button ${event.button})`),
366
- onContextMenu: (payload, event) => {
367
- event.preventDefault()
365
+ onPointerUp: payload => log('onPointerUp', `${describeElement(payload)}(button ${payload.originalEvent?.button})`),
366
+ onContextMenu: payload => {
367
+ payload.originalEvent?.preventDefault()
368
368
  log('onContextMenu', describeElement(payload))
369
369
  },
370
370
  // 需求 1 的核心:草稿里每点一次都回调,第一次拿起点、第二次拿终点
@@ -145,12 +145,13 @@
145
145
  const upP1 = last('up')
146
146
  const clickP1 = last('click')
147
147
  check('左键按下:action.type 是这一次按下的意图(节点上是 dragPoint)、target 是 point', downP1?.action?.type === 'dragPoint' && downP1?.action?.target === 'point', JSON.stringify(downP1?.action))
148
- check('左键按下:action.data 就是数据里那个节点(同一份引用,不用再 getData 反查)', downP1?.action?.data === view.getData().points[0] && downP1?.action?.data?.id === 'p1', String(downP1?.action?.data?.id))
149
- check('左键按下:button 是 left、modifiers 四个键齐全', downP1?.action?.button === 'left' && downP1?.action?.modifiers?.ctrl === false && downP1?.action?.modifiers?.shift === false, JSON.stringify(downP1?.action?.modifiers))
148
+ check('左键按下:命中的节点直接读 payload.item(与 getData() 同一份引用)', downP1?.item === view.getData().points[0] && downP1?.item?.id === 'p1' && downP1?.action?.data === undefined, String(downP1?.item?.id))
149
+ check('左键按下:按键 / 修饰键读 originalEvent,不再复制到 action', downP1?.originalEvent?.button === 0 && downP1?.originalEvent?.ctrlKey === false && downP1?.originalEvent?.shiftKey === false && downP1?.action?.button === undefined && downP1?.action?.modifiers === undefined, JSON.stringify({ button: downP1?.originalEvent?.button, ctrl: downP1?.originalEvent?.ctrlKey }))
150
150
  check('左键松开:没拖动过 → action.type 是 click、moved 为 false', upP1?.action?.type === 'click' && upP1?.action?.moved === false && upP1?.action?.target === 'point', JSON.stringify(upP1?.action))
151
151
  check('onClick:action.type 是 select(点中要素)、double 为 false', clickP1?.action?.type === 'select' && clickP1?.action?.double === false && clickP1?.action?.moved === false, JSON.stringify(clickP1?.action))
152
- check('onClick:action.button / modifiers 与指针载荷一个口径', clickP1?.action?.button === 'left' && clickP1?.action?.modifiers?.ctrl === false, String(clickP1?.action?.button))
153
- check('onClick:selection 是点完之后的读数(count / points / ids)', clickP1?.selection?.count === 1 && clickP1?.selection?.points === 1 && clickP1?.selection?.ids?.[0] === 'p1', JSON.stringify(clickP1?.selection))
152
+ check('onClick:按键 / 修饰键同样从 originalEvent 读', clickP1?.originalEvent?.button === 0 && clickP1?.originalEvent?.ctrlKey === false && clickP1?.action?.button === undefined, String(clickP1?.originalEvent?.button))
153
+ const clickSelection = view.getSelectionInfo()
154
+ check('onClick:选中集读数改调 getSelectionInfo(),载荷不再重复挂 selection', clickP1?.selection === undefined && clickSelection.count === 1 && clickSelection.points === 1 && clickSelection.ids?.[0] === 'p1', JSON.stringify(clickSelection))
154
155
 
155
156
  /* ---------- Ctrl 点击取反:不经过 onClick,要看 selectChange 的 action ---------- */
156
157
  const togglesBefore = countOf('selectChange')
@@ -161,7 +162,7 @@
161
162
  const togglePayload = last('selectChange')
162
163
  check('Ctrl 点击取反:selectChange.action.type 是 toggleSelect(取反取没了也是它)', countOf('selectChange') === togglesBefore + 1 && togglePayload?.action?.type === 'toggleSelect', JSON.stringify(togglePayload?.action))
163
164
  check('取反之后选中集空了:items / ids / count 都是空的一份、target 是 empty', togglePayload?.count === 0 && togglePayload?.ids?.length === 0 && togglePayload?.items?.length === 0 && togglePayload?.action?.target === 'empty', JSON.stringify({ ids: togglePayload?.ids, count: togglePayload?.count }))
164
- check('取反载荷带着修饰键与这一批数据(action.modifiers.ctrl / action.data)', togglePayload?.action?.modifiers?.ctrl === true && togglePayload?.action?.data === togglePayload?.items, JSON.stringify(togglePayload?.action?.modifiers))
165
+ check('取反载荷:修饰键读 originalEvent、这一批数据直接读 items', togglePayload?.originalEvent?.ctrlKey === true && togglePayload?.items?.length === 0 && togglePayload?.action?.modifiers === undefined && togglePayload?.action?.data === undefined, JSON.stringify({ ctrl: togglePayload?.originalEvent?.ctrlKey, items: togglePayload?.items?.length }))
165
166
  check('取反这一路不发 onClick:要按操作分支的界面认 selectChange.action', countOf('click') === 1, `${countOf('click')} 条 click`)
166
167
 
167
168
  /* ---------- 程序化选中与点空白 ---------- */
@@ -204,8 +205,8 @@
204
205
  fire(canvas, 'contextmenu', atP3, { button: 2, buttons: 2 })
205
206
  await settle(40)
206
207
  const menuP3 = last('contextmenu')
207
- check('右键节点:action.type 是 contextMenu、target 是 point、button 是 right', menuP3?.action?.type === 'contextMenu' && menuP3?.action?.target === 'point' && menuP3?.action?.button === 'right', JSON.stringify(menuP3?.action))
208
- check('右键节点:action.data 是那个节点,hit / selected / selection 都在', menuP3?.action?.data?.id === 'p3' && menuP3?.hit === true && typeof menuP3?.selected === 'boolean' && typeof menuP3?.selection?.count === 'number', JSON.stringify({ id: menuP3?.action?.data?.id, selected: menuP3?.selected }))
208
+ check('右键节点:action.type 是 contextMenu、target 是 point、button 读 originalEvent', menuP3?.action?.type === 'contextMenu' && menuP3?.action?.target === 'point' && menuP3?.originalEvent?.button === 2 && menuP3?.action?.button === undefined, JSON.stringify(menuP3?.action))
209
+ check('右键节点:节点读 item,selected 是命中时的快照,总读数走 getSelectionInfo()', menuP3?.item?.id === 'p3' && menuP3?.hit === true && typeof menuP3?.selected === 'boolean' && menuP3?.selection === undefined && typeof view.getSelectionInfo().count === 'number', JSON.stringify({ id: menuP3?.item?.id, selected: menuP3?.selected }))
209
210
  const lineInfo = view.listLines({ inView: true }).find(entry => entry.index === 0)
210
211
  const linePath = lineInfo?.path ?? []
211
212
  const channelAt = linePath.length >= 2
@@ -218,8 +219,8 @@
218
219
  await settle(40)
219
220
  const menuChannel = last('contextmenu')
220
221
  check('通道回线右键:同一份 onContextMenu 载荷的 action.type / target 是 contextMenu / channel', Boolean(channelAt) && menuChannel?.action?.type === 'contextMenu' && menuChannel?.action?.target === 'channel' && menuChannel?.channelContext?.channelId === 'c1', JSON.stringify({ action: menuChannel?.action, channel: menuChannel?.channelContext?.channelId }))
221
- check('通道回线右键:action.data 是通道定义(通道回线按通道处理)', menuChannel?.action?.data === menuChannel?.channel && menuChannel?.action?.data?.id === 'c1' && Array.isArray(menuChannel?.action?.data?.lineIds), String(menuChannel?.action?.data?.id))
222
- check('通道回线右键:action.button 与通用右键一个口径(right)', menuChannel?.action?.button === 'right', String(menuChannel?.action?.button))
222
+ check('通道回线右键:通道定义读 payload.channel,不再打包 action.data', menuChannel?.channel?.id === 'c1' && Array.isArray(menuChannel?.channel?.lineIds) && menuChannel?.action?.data === undefined, String(menuChannel?.channel?.id))
223
+ check('通道回线右键:右键按键与通用右键同一口径', menuChannel?.originalEvent?.button === 2 && menuChannel?.action?.button === undefined, String(menuChannel?.originalEvent?.button))
223
224
 
224
225
  /* ---------- 拐点:Ctrl 点击加点、点选、拖动 ---------- */
225
226
  pointerId += 1
@@ -229,9 +230,9 @@
229
230
  await settle(50)
230
231
  const wpAdd = last('waypoint')
231
232
  check('Ctrl 点击线路:拐点事件的 action.type 是 addWaypoint、target 是 waypoint', wpAdd?.phase === 'add' && wpAdd?.action?.type === 'addWaypoint' && wpAdd?.action?.target === 'waypoint', JSON.stringify(wpAdd?.action))
232
- check('拐点 action.data:哪条线、第几个顶点、坐标与改完的线', wpAdd?.action?.data?.lineIndex === 1 && wpAdd?.action?.data?.pathIndex === 1 && Array.isArray(wpAdd?.action?.data?.coordinate) && wpAdd?.action?.data?.line?.id === 'l2', JSON.stringify(wpAdd?.action?.data && { lineIndex: wpAdd.action.data.lineIndex, pathIndex: wpAdd.action.data.pathIndex, id: wpAdd.action.data.line.id }))
233
- check('拐点事件:button / modifiers 也补齐了(左键 + Ctrl)', wpAdd?.action?.button === 'left' && wpAdd?.action?.modifiers?.ctrl === true, JSON.stringify(wpAdd?.action?.modifiers))
234
- const wpCoordinate = wpAdd?.action?.data?.coordinate ?? [180, 90]
233
+ check('拐点:哪条线、第几个顶点、坐标与改完的线直接读载荷顶层', wpAdd?.lineIndex === 1 && wpAdd?.pathIndex === 1 && Array.isArray(wpAdd?.coordinate) && wpAdd?.line?.id === 'l2' && wpAdd?.action?.data === undefined, JSON.stringify({ lineIndex: wpAdd?.lineIndex, pathIndex: wpAdd?.pathIndex, id: wpAdd?.line?.id }))
234
+ check('拐点事件:按键 / 修饰键读 originalEvent(左键 + Ctrl)', wpAdd?.originalEvent?.button === 0 && wpAdd?.originalEvent?.ctrlKey === true && wpAdd?.action?.button === undefined && wpAdd?.action?.modifiers === undefined, JSON.stringify({ button: wpAdd?.originalEvent?.button, ctrl: wpAdd?.originalEvent?.ctrlKey }))
235
+ const wpCoordinate = wpAdd?.coordinate ?? [180, 90]
235
236
  pointerId += 1
236
237
  const wpPoint = pointOf(wpCoordinate[0], wpCoordinate[1])
237
238
  fire(canvas, 'pointerdown', wpPoint)
@@ -253,7 +254,7 @@
253
254
  const wpEnd = lastOfPhase('waypoint', 'end')
254
255
  check('拖动拐点:start / move / end 三条都带 action.type 是 dragWaypoint', wpStart?.action?.type === 'dragWaypoint' && wpMove?.action?.type === 'dragWaypoint' && wpEnd?.action?.type === 'dragWaypoint', JSON.stringify([wpStart?.action?.type, wpMove?.action?.type, wpEnd?.action?.type]))
255
256
  check('拖动拐点:moved 依次是 false / true / true(起手不算拖过)', wpStart?.action?.moved === false && wpMove?.action?.moved === true && wpEnd?.action?.moved === true, JSON.stringify([wpStart?.action?.moved, wpMove?.action?.moved, wpEnd?.action?.moved]))
256
- check('拖动拐点:end 的 action.data 里是挪完的坐标与新的线路', wpEnd?.action?.data?.coordinate?.[1] > wpCoordinate[1] && wpEnd?.action?.data?.line?.id === 'l2', JSON.stringify(wpEnd?.action?.data?.coordinate))
257
+ check('拖动拐点:end 顶层是挪完的坐标与新的线路', wpEnd?.coordinate?.[1] > wpCoordinate[1] && wpEnd?.line?.id === 'l2', JSON.stringify(wpEnd?.coordinate))
257
258
 
258
259
  /* ---------- 标签:点选 / 拖动 / 右键,同一套 action ---------- */
259
260
  const labelNode = labelNodeOf(1)
@@ -265,9 +266,9 @@
265
266
  fire(window, 'pointerup', at)
266
267
  await settle(80)
267
268
  const labelClick = last('labelClick')
268
- check('标签点选:action.type 是 selectLabel、target 是 label、button 是 left', labelClick?.action?.type === 'selectLabel' && labelClick?.action?.target === 'label' && labelClick?.action?.button === 'left', JSON.stringify(labelClick?.action))
269
- check('标签点选:action.data 给出这一条标签与它挂在哪条要素上', labelClick?.action?.data?.label?.id === labelClick?.labelId && labelClick?.action?.data?.parent?.id === 'p2' && labelClick?.action?.data?.parent?.type === 'point', JSON.stringify(labelClick?.action?.data?.parent))
270
- check('标签点选:originalEvent 一起给出来(宿主按它读修饰键)', labelClick?.originalEvent instanceof Event && labelClick?.action?.modifiers?.ctrl === false, String(labelClick?.originalEvent?.type))
269
+ check('标签点选:action.type 是 selectLabel、target 是 label、按键读 originalEvent', labelClick?.action?.type === 'selectLabel' && labelClick?.action?.target === 'label' && labelClick?.originalEvent?.button === 0 && labelClick?.action?.button === undefined, JSON.stringify(labelClick?.action))
270
+ check('标签点选:标签与 parent 直接读载荷顶层', labelClick?.label?.id === labelClick?.labelId && labelClick?.parent?.id === 'p2' && labelClick?.parent?.type === 'point' && labelClick?.action?.data === undefined, JSON.stringify(labelClick?.parent))
271
+ check('标签点选:修饰键从 originalEvent 读', labelClick?.originalEvent instanceof Event && labelClick?.originalEvent?.ctrlKey === false && labelClick?.action?.modifiers === undefined, String(labelClick?.originalEvent?.type))
271
272
  pointerId += 1
272
273
  const dragFrom = centerOf(labelNode)
273
274
  const dragTo = { clientX: dragFrom.clientX + 34, clientY: dragFrom.clientY + 24 }
@@ -278,13 +279,13 @@
278
279
  const labelDragStart = lastOfPhase('labelDrag', 'start')
279
280
  const labelDragEnd = lastOfPhase('labelDrag', 'end')
280
281
  check('标签拖动:start / end 的 action.type 都是 dragLabel、moved 是 false / true', labelDragStart?.action?.type === 'dragLabel' && labelDragStart?.action?.moved === false && labelDragEnd?.action?.type === 'dragLabel' && labelDragEnd?.action?.moved === true, JSON.stringify([labelDragStart?.action?.moved, labelDragEnd?.action?.moved]))
281
- check('标签拖动:end 带 originalEvent 与 action.data(这一条标签 + 挂在谁身上)', Boolean(labelDragEnd?.originalEvent) && labelDragEnd?.action?.data?.label?.id === labelDragEnd?.labelId && labelDragEnd?.action?.data?.parent?.type === 'point', String(labelDragEnd?.originalEvent?.type))
282
+ check('标签拖动:end 带 originalEvent,标签与 parent 读顶层', Boolean(labelDragEnd?.originalEvent) && labelDragEnd?.label?.id === labelDragEnd?.labelId && labelDragEnd?.parent?.type === 'point' && labelDragEnd?.action?.data === undefined, String(labelDragEnd?.originalEvent?.type))
282
283
  pointerId += 1
283
284
  const menuAt = centerOf(labelNode)
284
285
  fire(labelNode, 'contextmenu', menuAt, { button: 2, buttons: 2 })
285
286
  await settle(60)
286
287
  const labelMenu = last('labelMenu')
287
- check('标签右键:action.type 是 contextMenu、target 是 label、button 是 right', labelMenu?.action?.type === 'contextMenu' && labelMenu?.action?.target === 'label' && labelMenu?.action?.button === 'right', JSON.stringify(labelMenu?.action))
288
+ check('标签右键:action.type 是 contextMenu、target 是 label、button 读 originalEvent', labelMenu?.action?.type === 'contextMenu' && labelMenu?.action?.target === 'label' && labelMenu?.originalEvent?.button === 2 && labelMenu?.clientX === undefined && labelMenu?.action?.button === undefined, JSON.stringify(labelMenu?.action))
288
289
  }
289
290
 
290
291
  /* ---------- pickAt / getFeaturesInRect / getChannelsInView ---------- */
@@ -334,7 +335,7 @@
334
335
  const dbl = last('dblclick')
335
336
  const dblClick = last('click')
336
337
  check('双击:第二下的 onClick 带 double 标记、onDblClick 的 action.type 是 dblClick', dblClick?.action?.double === true && dbl?.action?.type === 'dblClick' && dbl?.action?.target === 'point', JSON.stringify({ click: dblClick?.action?.double, dbl: dbl?.action?.type }))
337
- check('双击:action.button 与 action.data 同样齐全', dbl?.action?.button === 'left' && dbl?.action?.data?.id === 'p4' && dbl?.action?.moved === false, JSON.stringify({ button: dbl?.action?.button, id: dbl?.action?.data?.id }))
338
+ check('双击:按键读 originalEvent、节点读 item、moved 仍在 action 上', dbl?.originalEvent?.button === 0 && dbl?.item?.id === 'p4' && dbl?.action?.moved === false && dbl?.action?.button === undefined && dbl?.action?.data === undefined, JSON.stringify({ button: dbl?.originalEvent?.button, id: dbl?.item?.id }))
338
339
 
339
340
  /* ---------- 这一轮:左右键更细的判读(左 / 右键布尔、按下意图与时长、长按、中键平移、悬停)与更多实用方法 ---------- */
340
341
  const holder2 = document.createElement('div')
@@ -392,13 +393,13 @@
392
393
  const downA2 = last2('down')
393
394
  const upA2 = last2('up')
394
395
  const clickA2 = last2('click')
395
- check('左键:action.left 为真、action.right 为假(左右键不必再比对 button 数字)', downA2?.action?.left === true && downA2?.action?.right === false && downA2?.action?.button === 'left', JSON.stringify({ left: downA2?.action?.left, right: downA2?.action?.right, button: downA2?.action?.button }))
396
+ check('左键:按键读 originalEvent(左键 button 为 0)', downA2?.originalEvent?.button === 0 && downA2?.action?.button === undefined && downA2?.action?.left === undefined && downA2?.action?.right === undefined, JSON.stringify({ button: downA2?.originalEvent?.button }))
396
397
  check('松开:action.press 是按下时的意图、duration 读到毫秒、快速点击不算长按', upA2?.action?.press === downA2?.action?.type && typeof upA2?.action?.duration === 'number' && upA2.action.duration < 500 && upA2?.action?.longPress === false, JSON.stringify({ press: upA2?.action?.press, duration: upA2?.action?.duration, longPress: upA2?.action?.longPress }))
397
398
  check('点击:press / duration / longPress 与松开那一份同口径', clickA2?.action?.press === downA2?.action?.type && clickA2?.action?.longPress === false && typeof clickA2?.action?.duration === 'number', JSON.stringify({ press: clickA2?.action?.press, duration: clickA2?.action?.duration }))
398
399
  fire2('pointerdown', atA2, { button: 2, buttons: 2 })
399
400
  fire2('contextmenu', atA2, { button: 2, buttons: 2 })
400
401
  const menuA2 = last2('menu')
401
- check('右键:action.right 为真、left 为假、button 是 right', menuA2?.action?.right === true && menuA2?.action?.left === false && menuA2?.action?.button === 'right', JSON.stringify({ left: menuA2?.action?.left, right: menuA2?.action?.right }))
402
+ check('右键:按键读 originalEvent(右键 button 为 2)', menuA2?.originalEvent?.button === 2 && menuA2?.action?.button === undefined && menuA2?.action?.left === undefined && menuA2?.action?.right === undefined, JSON.stringify({ button: menuA2?.originalEvent?.button }))
402
403
  const beforeCenter = view2.getCamera().center
403
404
  const clicksBeforeMiddle = count2('click')
404
405
  fire2('pointerdown', { clientX: box2.left + 80, clientY: box2.top + 80 }, { button: 1, buttons: 4 })
@@ -407,7 +408,7 @@
407
408
  const middleUp2 = last2('up')
408
409
  const afterCenter = view2.getCamera().center
409
410
  check('中键拖动:相机跟着平移(中键不再是「按下什么都不做」)', Math.abs(afterCenter[0] - beforeCenter[0]) > 1 && Math.abs(afterCenter[1] - beforeCenter[1]) > 1, JSON.stringify({ before: beforeCenter, after: afterCenter }))
410
- check('中键:按下意图与松开动作都是 pan,中键那一下不算点击', middleUp2?.action?.type === 'pan' && middleUp2?.action?.button === 'middle' && count2('click') === clicksBeforeMiddle, JSON.stringify({ type: middleUp2?.action?.type, button: middleUp2?.action?.button }))
411
+ check('中键:按下意图与松开动作都是 pan,中键那一下不算点击', middleUp2?.action?.type === 'pan' && middleUp2?.originalEvent?.button === 1 && middleUp2?.action?.button === undefined && count2('click') === clicksBeforeMiddle, JSON.stringify({ type: middleUp2?.action?.type, button: middleUp2?.originalEvent?.button }))
411
412
  const clicksBeforeLong = count2('click')
412
413
  fire2('pointerdown', atA2)
413
414
  await settle(560)
@@ -421,8 +422,8 @@
421
422
  const hoverBlank2 = view2.hoverAt(box2.left + 6, box2.top + 6)
422
423
  check('hoverAt:悬停到空白处返回 null(没有命中就不是一条读数)', hoverBlank2 === null && view2.getHover() === null, JSON.stringify(hoverBlank2))
423
424
  const hoveredA2 = view2.hoverAt(atHover2.clientX, atHover2.clientY)
424
- check('hoverAt:悬停载荷也带 action(type 是 hover、target 是 point、button 是 none、data 是命中的那一份)', hoveredA2?.hit === true && hoveredA2?.action?.type === 'hover' && hoveredA2?.action?.target === 'point' && hoveredA2?.action?.button === 'none' && hoveredA2?.action?.data?.id === 'a' && hoveredA2?.action?.left === false && hoveredA2?.action?.right === false, JSON.stringify(hoveredA2 && { type: hoveredA2.action.type, target: hoveredA2.action.target, button: hoveredA2.action.button, id: hoveredA2.item?.id }))
425
- check('hoverAt:读数里有光标位置(x / y / world)与选中集读数,和指针载荷同一层', typeof hoveredA2?.x === 'number' && typeof hoveredA2?.y === 'number' && Array.isArray(hoveredA2?.world) && hoveredA2.world.length === 2 && hoveredA2?.selection?.count === view2.getSelections().length && view2.getHover() === hoveredA2, JSON.stringify({ x: hoveredA2?.x, world: hoveredA2?.world }))
425
+ check('hoverAt:悬停载荷的 action 只留判读,命中数据读 item', hoveredA2?.hit === true && hoveredA2?.action?.type === 'hover' && hoveredA2?.action?.target === 'point' && hoveredA2?.item?.id === 'a' && hoveredA2?.action?.button === undefined && hoveredA2?.action?.data === undefined, JSON.stringify(hoveredA2 && { type: hoveredA2.action.type, target: hoveredA2.action.target, id: hoveredA2.item?.id }))
426
+ check('hoverAt:读数里有光标位置(x / y / world),选中集总读数走 getSelectionInfo()', typeof hoveredA2?.x === 'number' && typeof hoveredA2?.y === 'number' && Array.isArray(hoveredA2?.world) && hoveredA2.world.length === 2 && hoveredA2?.selection === undefined && view2.getSelectionInfo().count === view2.getSelections().length && view2.getHover() === hoveredA2, JSON.stringify({ x: hoveredA2?.x, world: hoveredA2?.world }))
426
427
  check('onHover 回调与 hoverAt 读到的是同一份读数(那一次悬停也发了回调)', last2('hover')?.item?.id === 'a' && last2('hover')?.action?.type === 'hover' && last2('hover')?.x === hoveredA2?.x, JSON.stringify({ id: last2('hover')?.item?.id, x: last2('hover')?.x }))
427
428
  check('clearHover:清掉程序化悬停并补发一条 onHover(null)', view2.clearHover() === null && view2.getHover() === null && last2('hover') === null, JSON.stringify(last2('hover')))
428
429
 
@@ -492,7 +493,7 @@
492
493
  check('step-12:指针事件往 onAction 报一句判读(按下 / 松开 / 选中,且带着数据本身)', reports.some(text => text.startsWith('按下:')) && reports.some(text => text.startsWith('松开:')) && reports.some(text => text.startsWith('选中:')) && reports.some(text => text.includes('→ 节点') && text.includes(parkStation.name)), reports.slice(-3).join(' | '))
493
494
  // 工具栏「程序化悬停」按钮走的那条路:probeHover 让指针「停」到视口里第一个节点上,再调一次撤掉
494
495
  const hoverProbe = actionView.probeHover(true)
495
- check('step-12:probeHover(程序化悬停) 停在节点上,读数与 onHover 同一份(带 action 与光标位置)', hoverProbe?.hit === true && hoverProbe?.type === 'point' && hoverProbe?.action?.type === 'hover' && hoverProbe?.action?.button === 'none' && Array.isArray(hoverProbe?.world) && actionView.view.getHover() === hoverProbe, JSON.stringify(hoverProbe && { type: hoverProbe.type, action: hoverProbe.action.type, index: hoverProbe.index }))
496
+ check('step-12:probeHover(程序化悬停) 停在节点上,读数与 onHover 同一份(带 action 与光标位置)', hoverProbe?.hit === true && hoverProbe?.type === 'point' && hoverProbe?.action?.type === 'hover' && hoverProbe?.action?.button === undefined && Array.isArray(hoverProbe?.world) && actionView.view.getHover() === hoverProbe, JSON.stringify(hoverProbe && { type: hoverProbe.type, action: hoverProbe.action.type, index: hoverProbe.index }))
496
497
  check('step-12:probeHover(false) 撤掉悬停(getHover 回到 null)', actionView.probeHover(false) === null && actionView.view.getHover() === null, '')
497
498
  actionView.destroy()
498
499
  holder.remove()
@@ -305,7 +305,7 @@
305
305
  // 先把两条新场站追加进来(addPoints 的增量路),再按它们新增通道
306
306
  const addedPoints = sceneView.addPoints([{ id: 'c', x: 2, y: 0, name: '丙站' }, { id: 'd', x: 2, y: 2, name: '丁站' }])
307
307
  const added = sceneView.addChannels([
308
- { from: 'a', to: 'c', circuits: 2, name: '甲-丙' },
308
+ { from: 'a', to: 'c', lineData: [{}, {}], name: '甲-丙' },
309
309
  { from: 'b', to: 'd', name: '乙-丁' },
310
310
  { from: 'a', to: 'nope', name: '坏的那条' }
311
311
  ])
@@ -559,6 +559,85 @@
559
559
  const afterObjectChannel = view.getData()
560
560
  const objectChannel = channelList().at(-1)
561
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 ?? [])}`)
562
+
563
+ // 单一通道 / 已有分组:预置一条 node-0 ↔ node-1 的通道,再分别验证阻断、新分组、指定分组与 beginAddLine 挂回线
564
+ document.querySelector('[data-demo=connect-seed-existing]').click()
565
+ const seededExisting = await until(() => view.getChannelGroups('connect-existing-channel').length === 2, 3000)
566
+ check('可视化连线:预置已有通道带两个分组,面板读数同步', Boolean(seededExisting) && channelList().length === 3 && text('connect-existing-readout').includes('已有通道'), `${channelList().length} 通道 / ${text('connect-existing-readout')}`)
567
+ const singleChannelInput = document.querySelector('[data-demo=connect-single-channel]')
568
+ const existingModeSelect = document.querySelector('[data-demo=connect-existing-mode]')
569
+ const groupTargetSelect = document.querySelector('[data-demo=connect-group-target]')
570
+ const setExistingMode = async value => {
571
+ existingModeSelect.value = value
572
+ existingModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
573
+ await wait(60)
574
+ }
575
+ const beginPresetPair = async () => {
576
+ startPointSelect.value = 'node-0'
577
+ startPointSelect.dispatchEvent(new Event('change', { bubbles: true }))
578
+ await wait(50)
579
+ document.querySelector('[data-demo=connect-start]').click()
580
+ await wait(70)
581
+ await clickNode('node-1')
582
+ return until(() => {
583
+ const current = view.getAddChannelDraft()
584
+ return current?.phase === 'ready' ? current : null
585
+ }, 2500)
586
+ }
587
+ await setExistingMode('none')
588
+ if (!singleChannelInput.checked) singleChannelInput.click()
589
+ await wait(50)
590
+ const blockedLinesBefore = view.getData().lines.length
591
+ const blockedDraft = await beginPresetPair()
592
+ document.querySelector('[data-demo=connect-default]').click()
593
+ const blockedEventReady = await until(() => text('connect-event-readout').includes('phase=blocked'), 2500)
594
+ check('可视化连线:singleChannel + 已有通道触发 ready blocked 与提交失败事件', Boolean(blockedDraft) && blockedDraft.blocked === true && blockedDraft.existingChannel === 'block' && Boolean(blockedEventReady) && text('connect-event-readout').includes('singleChannel=true') && text('connect-action-readout').includes('blocked'), `${text('connect-event-readout')} / ${view.getData().lines.length} 线`)
595
+ check('可视化连线:阻断失败不改线路 / 通道数据', view.getData().lines.length === blockedLinesBefore && channelList().length === 3, `${blockedLinesBefore} → ${view.getData().lines.length} 线 / ${channelList().length} 通道`)
596
+
597
+ await setExistingMode('newGroup')
598
+ const groupsBeforeNew = view.getChannelGroups('connect-existing-channel').length
599
+ const newGroupLinesBefore = view.getData().lines.length
600
+ const newGroupDraft = await beginPresetPair()
601
+ const newGroupCommitReady = newGroupDraft?.preferred === 'newGroup' && newGroupDraft?.blocked === false
602
+ document.querySelector('[data-demo=connect-default]').click()
603
+ await wait(160)
604
+ check('可视化连线:existingChannel: newGroup 把 2 回新线作为新分组加入已有通道', Boolean(newGroupCommitReady) && view.getChannelGroups('connect-existing-channel').length === groupsBeforeNew + 1 && view.getData().lines.length === newGroupLinesBefore + 2 && channelList().length === 3, `${groupsBeforeNew} → ${view.getChannelGroups('connect-existing-channel').length} 组 / ${newGroupLinesBefore} → ${view.getData().lines.length} 线 / ${text('connect-action-readout')}`)
605
+
606
+ await setExistingMode('joinGroup')
607
+ groupTargetSelect.value = 'connect-existing-group-b'
608
+ groupTargetSelect.dispatchEvent(new Event('change', { bubbles: true }))
609
+ await wait(50)
610
+ const joinGroupBefore = view.getChannelGroups('connect-existing-channel').find(group => group.id === 'connect-existing-group-b')?.lineIds.length ?? 0
611
+ const joinGroupChannelsBefore = channelList().length
612
+ const joinGroupDraft = await beginPresetPair()
613
+ const joinGroupCommitReady = joinGroupDraft?.targetGroupId === 'connect-existing-group-b'
614
+ document.querySelector('[data-demo=connect-default]').click()
615
+ await wait(160)
616
+ const joinGroupAfter = view.getChannelGroups('connect-existing-channel').find(group => group.id === 'connect-existing-group-b')?.lineIds.length ?? 0
617
+ check('可视化连线:existingChannel: joinGroup + groupId 把 2 回新线并入指定分组', Boolean(joinGroupCommitReady) && joinGroupAfter === joinGroupBefore + 2 && channelList().length === joinGroupChannelsBefore, `group-b ${joinGroupBefore} → ${joinGroupAfter} 回 / ${channelList().length} 通道 / ${text('connect-action-readout')}`)
618
+
619
+ kindSelect.value = 'line'
620
+ kindSelect.dispatchEvent(new Event('change', { bubbles: true }))
621
+ await wait(60)
622
+ groupTargetSelect.value = 'connect-existing-group-a'
623
+ groupTargetSelect.dispatchEvent(new Event('change', { bubbles: true }))
624
+ await wait(50)
625
+ const attachGroupBefore = view.getChannelGroups('connect-existing-channel').find(group => group.id === 'connect-existing-group-a')?.lineIds.length ?? 0
626
+ const attachChannelsBefore = channelList().length
627
+ const attachDraft = await beginPresetPair()
628
+ const attachDraftReady = attachDraft?.kind === 'line' && attachDraft?.existingChannel === 'joinGroup' && attachDraft?.targetGroupId === 'connect-existing-group-a'
629
+ document.querySelector('[data-demo=connect-default]').click()
630
+ await wait(160)
631
+ const attachGroupAfter = view.getChannelGroups('connect-existing-channel').find(group => group.id === 'connect-existing-group-a')?.lineIds.length ?? 0
632
+ check('可视化连线:beginAddLine + existingChannel: joinGroup 也能把新回线挂进指定分组,不新建通道', Boolean(attachDraftReady) && attachGroupAfter === attachGroupBefore + 1 && channelList().length === attachChannelsBefore && text('connect-log').includes('beginAddLine'), `${text('connect-log')} / group-a ${attachGroupBefore} → ${attachGroupAfter} 回`)
633
+
634
+ kindSelect.value = 'channel-2'
635
+ kindSelect.dispatchEvent(new Event('change', { bubbles: true }))
636
+ await setExistingMode('auto')
637
+ const autoDraft = await beginPresetPair()
638
+ check('可视化连线:existingChannel: auto 在分组有余量时推荐新分组', autoDraft?.blocked === false && autoDraft?.preferred === 'newGroup' && autoDraft?.existingChannel === 'auto', JSON.stringify({ preferred: autoDraft?.preferred, blocked: autoDraft?.blocked }))
639
+ view.cancelAddChannel()
640
+ await wait(60)
562
641
  check('可视化连线:读数面板同步报出纯点 + 线 / 通道数量', text('connect-data-readout').includes('节点') && text('connect-data-readout').includes('通道'), text('connect-data-readout'))
563
642
  }
564
643
  else {
@@ -931,7 +1010,7 @@
931
1010
  const click = key => document.querySelector(`[data-demo="${key}"]`)?.click()
932
1011
  const text = key => document.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
933
1012
  if (ready && view) {
934
- const groupsOf = () => view.getChannels().length
1013
+ const groupsOf = () => (view.getOptions().channels ?? []).reduce((sum, channel) => sum + view.getChannelGroups(channel.id).length, 0)
935
1014
  const lineIndex = view.getData().lines.findIndex(line => String(line.id) === 'line-0')
936
1015
  const waypointCount = () => view.getWaypoints(lineIndex).length
937
1016
  const pointsBefore = view.getData().points.length
@@ -1211,17 +1290,34 @@
1211
1290
  check('批量复制与合并:409 后 rebase 生成新 opId 并成功落库',
1212
1291
  Boolean(copyRebased) && setup.backendSnapshot?.version === conflictVersion + 2 && setup.backendSnapshot?.applied === conflictApplied + 1,
1213
1292
  `v${conflictVersion} → v${setup.backendSnapshot?.version} / applied ${conflictApplied} → ${setup.backendSnapshot?.applied} / ${text('copy-backend-readout')}`)
1214
- // 拆分:先选一组,再点演示按钮;节点 +1、读数落到面板,撤销一步回到拆分前
1293
+ // 解除合并:先选一组,再点演示按钮;节点 +1、autoLines 分配读数落到面板,撤销一步回到拆分前
1215
1294
  click('copy-select-sources')
1216
1295
  const splitSelectionReady = await until(() => view.getSelectionData().pointCount === 4, 5000)
1217
1296
  const splitSource = view.getSelectionData().points?.[0] ?? view.getData().points[0]
1297
+ const splitChannelsBefore = view.getOptions().channels ?? []
1218
1298
  const pointsBeforeSplit = view.getData().points.length
1219
1299
  click('split-selected')
1220
1300
  const splitLanded = await until(() => view.getData().points.length === pointsBeforeSplit + 1 && text('split-readout').includes('splitPoint'), 6000)
1221
1301
  const splitReadout = text('split-readout')
1222
- check('批量复制与合并:拆分选中节点走 splitPoint,第二份与改线 / 改通道读数落面板',
1223
- Boolean(splitSelectionReady) && Boolean(splitLanded) && text('copy-action').includes('splitPoint') && splitReadout.includes('改线') && splitReadout.includes('改通道'),
1302
+ const splitResult = unwrap(setup.lastSplitResult)
1303
+ const splitSlotByLine = new Map()
1304
+ for (const assignment of splitResult?.lineAssignments ?? []) {
1305
+ for (const lineId of assignment.lineIds ?? []) splitSlotByLine.set(String(lineId), assignment.index)
1306
+ }
1307
+ const splitChannelsKept = splitChannelsBefore.every((channel) => {
1308
+ const slots = new Set()
1309
+ for (const lineId of channel.lineIds ?? []) {
1310
+ const slot = splitSlotByLine.get(String(lineId))
1311
+ if (slot !== undefined) slots.add(slot)
1312
+ }
1313
+ return slots.size <= 1
1314
+ })
1315
+ check('批量复制与合并:解除合并走 splitPoint,autoLines / 自动分线与改线 / 改通道读数落面板',
1316
+ Boolean(splitSelectionReady) && Boolean(splitLanded) && text('copy-action').includes('splitPoint') && splitReadout.includes('autoLines=alternate') && splitReadout.includes('自动分线') && splitReadout.includes('改线') && splitReadout.includes('改通道'),
1224
1317
  `${pointsBeforeSplit} → ${view.getData().points.length} · ${splitReadout}`)
1318
+ check('批量复制与合并:解除合并按通道成组分配,同一条通道的回线不会落到两个节点',
1319
+ splitResult?.autoLines === 'alternate' && splitResult?.autoAssignedLines > 0 && splitResult.lineAssignments.reduce((sum, item) => sum + item.lineCount, 0) === splitResult.autoAssignedLines && splitChannelsKept && splitReadout.includes('通道成组保持完整'),
1320
+ JSON.stringify({ autoLines: splitResult?.autoLines, assigned: splitResult?.autoAssignedLines, channelsKept: splitChannelsKept, counts: splitResult?.lineAssignments?.map(item => item.lineCount) }))
1225
1321
  view.undo()
1226
1322
  const splitUndone = await until(() => view.getData().points.length === pointsBeforeSplit && view.getData().points.some(point => String(point.id) === String(splitSource?.id)), 5000)
1227
1323
  check('批量复制与合并:拆分是一步历史,撤销后节点与连线恢复', Boolean(splitUndone), `${view.getData().points.length} 点 / ${splitSource?.id ?? '—'}`)
@@ -2493,8 +2589,8 @@
2493
2589
  await wait(150)
2494
2590
  const apiItems = () => [...document.querySelectorAll('.demo-api-list:not(.demo-api-list--options) .demo-api-item')]
2495
2591
  const optionItems = () => [...document.querySelectorAll('.demo-api-list--options .demo-api-item')]
2496
- check('实例方法速查:整份成员清单按分组列全(16 组 / 264 个成员)',
2497
- Boolean(ready) && document.querySelectorAll('.demo-api-group').length === 16 && apiItems().length === 264,
2592
+ check('实例方法速查:整份成员清单按分组列全(16 组 / 267 个成员)',
2593
+ Boolean(ready) && document.querySelectorAll('.demo-api-group').length === 16 && apiItems().length === 267,
2498
2594
  `${document.querySelectorAll('.demo-api-group').length} 组 / ${apiItems().length} 个成员`)
2499
2595
  const apiNames = apiItems().map(node => node.querySelector('b')?.textContent)
2500
2596
  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(264 个成员 / 259 个方法 / 5 个属性 / 96 个 options / 16 个分组)',
524
- summary.members === 264 && summary.methods === 259 && summary.properties === 5 && summary.options === 96 && summary.sections === 16,
523
+ check('API 速查:清单来自 api-index(267 个成员 / 262 个方法 / 5 个属性 / 96 个 options / 16 个分组)',
524
+ summary.members === 267 && summary.methods === 262 && summary.properties === 5 && 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(' / ') : '264 张卡片都有示例')
562
+ noExample.length ? noExample.map(node => node.dataset.name).join(' / ') : '267 张卡片都有示例')
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} 个`)