gis-view-webgl2 0.1.7 → 0.1.9

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 (70) hide show
  1. package/README.md +14 -11
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +445 -241
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +65 -498
  7. package/docs/GisView.md +1 -1
  8. package/docs/GisView/345/256/236/347/216/260/345/216/237/347/220/206.md +1 -1
  9. 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 +114 -478
  10. package/docs/demos/BasicDemo.vue +144 -27
  11. package/docs/demos/BatchAddDemo.vue +133 -7
  12. package/docs/demos/ChannelDemo.vue +61 -0
  13. package/docs/demos/ConnectDemo.vue +164 -33
  14. package/docs/demos/CopyMergeDemo.vue +144 -42
  15. package/docs/demos/EventDemo.vue +29 -24
  16. package/docs/demos/LabelDemo.vue +5 -5
  17. package/docs/demos/api-index.js +53 -41
  18. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +1 -1
  19. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +1 -1
  20. package/docs/demos/gis-view-demo/panels/DataPanels.vue +1 -1
  21. package/docs/demos/gis-view-demo/useGisViewDemo.js +31 -33
  22. package/docs/format/02-/350/212/202/347/202/271/344/270/216/347/272/277/350/267/257.md +1 -0
  23. package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +3 -0
  24. 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
  25. package/docs/from0/05-/347/233/270/346/234/272/344/270/216/344/272/244/344/272/222.md +1 -1
  26. 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
  27. package/docs/index.html +136 -13
  28. package/docs/internals/15-04-/351/200/220/350/275/256/350/256/260/345/275/225-/347/203/255/345/212/233/345/233/276/344/270/216/346/200/247/350/203/275.md +10 -0
  29. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +1 -1
  30. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +82 -14
  31. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +23 -17
  32. package/docs/manual/05-/347/224/273/350/264/250/344/270/216/346/211/271/351/207/217/346/233/264/346/226/260.md +65 -0
  33. 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
  34. 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 +100 -40
  35. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +11 -7
  36. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +66 -6
  37. 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
  38. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +28 -5
  39. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +46 -14
  40. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +5 -2
  41. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +34 -29
  42. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +30 -22
  43. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +168 -81
  44. 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
  45. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +5 -3
  46. 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 +9 -6
  47. package/docs/steps/README.md +201 -120
  48. package/docs/steps/bench-scale.html +1 -1
  49. package/docs/steps/index.html +222 -6
  50. package/docs/steps/selftest-actions.html +26 -25
  51. package/docs/steps/selftest-data.html +1 -1
  52. package/docs/steps/selftest-demos.html +290 -16
  53. package/docs/steps/selftest-docs.html +17 -6
  54. package/docs/steps/selftest-features.html +594 -54
  55. package/docs/steps/selftest-full.html +3 -3
  56. package/docs/steps/selftest-labels.html +82 -1
  57. package/docs/steps/selftest-perf.html +62 -7
  58. package/docs/steps/selftest-select.html +71 -6
  59. package/docs/steps/step-05-pick.js +4 -8
  60. package/docs/steps/step-07-channels.js +3 -5
  61. package/docs/steps/step-08-labels.js +3 -3
  62. package/docs/steps/step-10-full.js +3 -3
  63. package/docs/steps/step-12-events.js +41 -24
  64. package/docs//344/273/2160/345/210/260/345/256/214/346/225/264/347/232/204/345/256/236/347/216/260.md +1 -0
  65. package/package.json +1 -1
  66. package/docs/ai/01-/350/265/267/346/272/220/344/270/216/350/277/231/344/273/275/346/261/207/346/200/273/347/232/204/347/224/261/346/235/245.md +0 -29
  67. package/docs/ai/02-/346/214/211/345/206/205/345/256/271/345/210/206/347/261/273-/350/277/231/344/270/200/350/267/257/351/203/275/345/234/250/346/224/271/344/273/200/344/271/210.md +0 -66
  68. package/docs/ai/03-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/270/200/357/274/211/357/274/232/351/200/211/345/236/213/351/200/232/351/201/223/344/270/216/346/240/267/345/274/217.md +0 -178
  69. package/docs/ai/04-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/272/214/357/274/211/357/274/232/344/272/244/344/272/222/346/200/247/350/203/275/344/270/216/346/225/260/346/215/256.md +0 -140
  70. package/docs/ai/05-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/270/211/357/274/211/357/274/232/345/256/277/344/270/273/346/227/201/346/224/257/344/270/216/346/224/266/345/217/243.md +0 -43
@@ -171,6 +171,119 @@
171
171
  .log b {
172
172
  color: #2f6fd0;
173
173
  }
174
+ .learn-route {
175
+ display: grid;
176
+ grid-template-columns: repeat(4, minmax(0, 1fr));
177
+ gap: 8px;
178
+ margin-bottom: 10px;
179
+ }
180
+ .learn-route a {
181
+ position: relative;
182
+ display: flex;
183
+ flex-direction: column;
184
+ gap: 3px;
185
+ min-height: 76px;
186
+ padding: 10px 12px;
187
+ border: 1px solid #d7e2ee;
188
+ border-radius: 10px;
189
+ background: rgba(255, 255, 255, 0.78);
190
+ color: #27476b;
191
+ text-decoration: none;
192
+ transition: border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease;
193
+ }
194
+ .learn-route a:hover {
195
+ border-color: #8fb4dd;
196
+ box-shadow: 0 7px 20px rgba(47, 111, 208, 0.1);
197
+ transform: translateY(-1px);
198
+ }
199
+ .learn-route span {
200
+ color: #2f6fd0;
201
+ font-size: 11px;
202
+ font-weight: 700;
203
+ letter-spacing: 0.04em;
204
+ }
205
+ .learn-route b {
206
+ font-size: 13px;
207
+ }
208
+ .learn-route small {
209
+ color: #6b8099;
210
+ font-size: 11px;
211
+ line-height: 1.45;
212
+ }
213
+ .step-brief {
214
+ display: grid;
215
+ grid-template-columns: 1.1fr 1.1fr 0.8fr;
216
+ gap: 8px;
217
+ margin-bottom: 10px;
218
+ }
219
+ .step-brief__card {
220
+ min-width: 0;
221
+ padding: 10px 12px;
222
+ border: 1px solid #d7e2ee;
223
+ border-radius: 9px;
224
+ background: rgba(255, 255, 255, 0.8);
225
+ }
226
+ .step-brief__card > span {
227
+ display: block;
228
+ margin-bottom: 3px;
229
+ color: #2f6fd0;
230
+ font-size: 11px;
231
+ font-weight: 700;
232
+ }
233
+ .step-brief h2 {
234
+ margin: 0 0 3px;
235
+ color: #1f3550;
236
+ font-size: 14px;
237
+ }
238
+ .step-brief p {
239
+ margin: 0;
240
+ color: #526b8d;
241
+ font-size: 12px;
242
+ line-height: 1.55;
243
+ }
244
+ .step-brief code,
245
+ .step-brief small {
246
+ display: block;
247
+ overflow-wrap: anywhere;
248
+ }
249
+ .step-brief code {
250
+ margin-top: 3px;
251
+ }
252
+ .step-brief small {
253
+ margin-top: 5px;
254
+ color: #8191a8;
255
+ font-size: 11px;
256
+ }
257
+ .afterword {
258
+ margin-top: 10px;
259
+ border: 1px solid #d7e2ee;
260
+ border-radius: 10px;
261
+ background: rgba(255, 255, 255, 0.76);
262
+ }
263
+ .afterword summary {
264
+ padding: 10px 12px;
265
+ color: #27476b;
266
+ font-weight: 700;
267
+ cursor: pointer;
268
+ }
269
+ .afterword__body {
270
+ padding: 0 12px 12px;
271
+ color: #526b8d;
272
+ font-size: 12px;
273
+ line-height: 1.65;
274
+ }
275
+ .afterword__body h2 {
276
+ margin: 8px 0 3px;
277
+ color: #1f3550;
278
+ font-size: 13px;
279
+ }
280
+ .afterword__body p,
281
+ .afterword__body ol {
282
+ margin: 3px 0 0;
283
+ }
284
+ .afterword__body ol {
285
+ padding-left: 20px;
286
+ }
174
287
  @media (max-width: 760px) {
175
288
  body {
176
289
  padding: 12px;
@@ -186,6 +299,10 @@
186
299
  .readout {
187
300
  grid-template-columns: 1fr;
188
301
  }
302
+ .learn-route,
303
+ .step-brief {
304
+ grid-template-columns: 1fr;
305
+ }
189
306
  }
190
307
  </style>
191
308
  </head>
@@ -208,9 +325,49 @@
208
325
  <a href="../api/index.html">API 速查</a>
209
326
  <a href="../playground/index.html">调试页</a>
210
327
  <a href="./selftest.html">自测</a>
328
+ <a href="../index.html#从0到完整的实现.md">从零讲义</a>
329
+ <a href="../index.html#steps/README.md">步骤说明</a>
211
330
  </nav>
212
331
  </header>
332
+ <section class="learn-route" id="learn-route" aria-label="从零学习路线">
333
+ <a href="#1" data-route-step="0">
334
+ <span>第 1 段 · 第 1 ~ 3 步</span>
335
+ <b>先让画面成立</b>
336
+ <small>画布、点、线:先把 WebGL 主干跑通。</small>
337
+ </a>
338
+ <a href="#4" data-route-step="3">
339
+ <span>第 2 段 · 第 4 ~ 5 步</span>
340
+ <b>再看懂视角与命中</b>
341
+ <small>相机矩阵、适应视图、拾取与指针回调。</small>
342
+ </a>
343
+ <a href="#6" data-route-step="5">
344
+ <span>第 3 段 · 第 6 ~ 9 步</span>
345
+ <b>长成业务组件</b>
346
+ <small>样式规则、通道、覆盖层与鹰眼图。</small>
347
+ </a>
348
+ <a href="#10" data-route-step="9">
349
+ <span>第 4 段 · 第 10 ~ 13 步</span>
350
+ <b>接入真组件并上线</b>
351
+ <small>初始视图、动作判读、画质、批量与历史。</small>
352
+ </a>
353
+ </section>
213
354
  <div class="steps" id="steps"></div>
355
+ <section class="step-brief" id="step-brief" aria-live="polite">
356
+ <div class="step-brief__card">
357
+ <span id="brief-stage">当前阶段</span>
358
+ <h2 id="brief-title">正在加载</h2>
359
+ <p id="brief-goal">每一步只解决一个问题;先读产出,再对照页面效果。</p>
360
+ </div>
361
+ <div class="step-brief__card">
362
+ <span>本步完成标准</span>
363
+ <p id="brief-accept">能说清输入、输出,并在页面上亲手验证。</p>
364
+ </div>
365
+ <div class="step-brief__card">
366
+ <span>本次调用的接口</span>
367
+ <code id="brief-api">—</code>
368
+ <small id="brief-source">对照源码:—</small>
369
+ </div>
370
+ </section>
214
371
  <div id="map"></div>
215
372
  <div class="bar">
216
373
  <button type="button" id="btn-fit">适应视图</button>
@@ -248,6 +405,23 @@
248
405
  <span id="rd-features">功能:—</span>
249
406
  </div>
250
407
  <div class="log" id="log"></div>
408
+ <details class="afterword" id="afterword">
409
+ <summary>通关之后:搭建思路、验收与优化顺序</summary>
410
+ <div class="afterword__body">
411
+ <h2>1. 先定契约,再写渲染</h2>
412
+ <p>每步开工前先回答四件事:输入是什么(世界坐标 / 业务字段)、输出是什么(像素 / DOM / 回调)、谁是唯一真相(数据副本还是 GPU 缓冲)、失败时怎么看得出来。把这四件事写清,后面加通道、标签和筛选才不会推倒重来。</p>
413
+ <h2>2. 沿一条数据流排查</h2>
414
+ <p>业务数据 → 解析成渲染实例 → 相机矩阵 → GPU / DOM 覆盖层 → 指针事件回查。画面不对时从最近一次「应该变但没变」的位置向前查,而不是同时改相机、着色器和事件。</p>
415
+ <h2>3. 优化只按瓶颈排队</h2>
416
+ <ol>
417
+ <li>先用 <code>getStats()</code>、性能和帧耗时确认瓶颈,再动代码。</li>
418
+ <li>减少每帧重排:批量更新、增量补丁、按需渲染优先于改写着色器。</li>
419
+ <li>再减可见工作:视口裁剪、标签上限与避让、聚合、线路批次。</li>
420
+ <li>最后才考虑 Worker、画质档或更换渲染后端;先把正确性和可读性守住。</li>
421
+ </ol>
422
+ <p>完整讲义见 <a href="../index.html#从0到完整的实现.md">《从 0 到完整的实现》</a>,每一步的验收表与源码对照见 <a href="../index.html#steps/README.md">docs/steps/README.md</a>。</p>
423
+ </div>
424
+ </details>
251
425
  </main>
252
426
  <script type="module">
253
427
  import { buildParkData } from './data.js'
@@ -265,10 +439,51 @@
265
439
  { file: 'step-09-minimap.js', factory: 'createMinimapView', load: () => import('./step-09-minimap.js'), title: '鹰眼图', api: '小图 + 视口框', note: '小画布画全景、DOM 画视口框、点与拖都能联动主图(文档第 8 章)' },
266
440
  { file: 'step-10-full.js', factory: 'createFullView', load: () => import('./step-10-full.js'), title: '换成真组件', api: 'createGisView', note: '同一份数据换成 src/gis-view 的完整实现,API 与前面几步一一对应(文档 12.11)' },
267
441
  { 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:一次操作的判读 / 实例方法)' },
442
+ { 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
443
  { 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
444
  ]
271
445
 
446
+ /** 每一步只回答三个问题:产出什么、怎么验收、回到库里看哪一段。 */
447
+ const STEP_GUIDES = [
448
+ { goal: '让容器里出现一块跟随尺寸变化的 canvas,并建立“改数据 -> 请求一帧”的按需渲染循环。', accept: '缩放窗口后画布清晰、不拉伸;静止时没有持续 rAF 空转。', source: 'src/gis-view/engine.js · 上下文 / resize / requestRender' },
449
+ { goal: '把点数组编成实例数据,用一次 draw call 画出所有节点。', accept: '改节点数后画面与缓冲同步更新;圆与方在片元里用距离场抗锯齿。', source: 'src/gis-view/point.js + shaders.js' },
450
+ { goal: '把每段线展开成四边形状,在顶点着色器里按法线撑开线宽。', accept: '线宽不随缩放变糊,拐点处不断裂,颜色与宽度能逐条覆盖。', source: 'src/gis-view/line.js + shaders.js' },
451
+ { goal: '把世界坐标经相机矩阵投到屏幕,并实现 fitView / 滚轮缩放 / 拖动平移。', accept: '滚轮以指针位置为锚点、拖动位移跟手,fitView 能完整包住数据范围。', source: 'src/gis-view/engine.js · 相机段' },
452
+ { goal: '用屏幕距离做拾取,把 hover / click / dblClick / pointer / contextMenu 接成一套回调。', accept: '每个回调都带 type / index / item;点空白不误命中,拖动不会被当成点击。', source: 'src/gis-view/utils.js + engine.js' },
453
+ { goal: '把业务字段翻译成样式,再按“点自身 > 状态 > 规则 > 全局”的优先级落地。', accept: '现场改规则只重算样式、不写业务数据;选中与悬停状态仍然优先。', source: 'src/gis-view/rules.js + condition.js' },
454
+ { goal: '先算通道公共骨架,再按车道生成平行回线,并把两次点击变成可取消的草稿。', accept: '点两个节点能落成一条通道;回线间距稳定;取消 / 落地 / 连续新增都有回调。', source: 'src/gis-view/channel.js + engine.js' },
455
+ { goal: '用 DOM 覆盖层承载标签和提示框,按视口、缩放级别与避让结果决定显示谁。', accept: '平移缩放后标签仍贴住要素;超过上限会隐藏或避让,悬停提示不阻塞画布。', source: 'src/gis-view/overlay.js' },
456
+ { goal: '用两层小画布画全景与视口框,并把点击、拖动映射回主图相机。', accept: '主图缩放时视口框准确变化;点小图或拖框都能定位主图;销毁后不残留帧任务。', source: 'src/gis-view/minimap.js' },
457
+ { goal: '把前面手写的实现替换成真实的 createGisView,对照同一份数据与同一套 API。', accept: '同样数据得到同样的点线关系;销毁后定时器、监听与 WebGL 资源都有归口。', source: 'src/gis-view/create.js' },
458
+ { goal: '接入背景层与首屏落位,区分 initialView、顶层 center / scale 与 applyInitialView。', accept: 'GeoJSON / 图片 / 无底图都能切换;首屏中心与比例符合配置;显式 initialView 整份优先。', source: 'src/gis-view/background.js + geojson.js + engine.js' },
459
+ { goal: '把原始指针事件压成稳定的 action 判读,并提供 pickAt / getFeaturesInRect / hoverAt 三组只读查询。', accept: '点击、框选、拖点与右键都能从 action.type / target 分清;查询结果与事件载荷同口径。', source: 'src/gis-view/engine.js + create.js' },
460
+ { goal: '补齐上线前的工程开关:画质档、批量更新、历史与功能预设。', accept: '一批更新只发一次数据变更、只记一步历史;撤销 / 重做正确;只读与静态预设真的关掉交互。', source: 'src/gis-view/quality.js + history.js + features.js' }
461
+ ]
462
+ const STEP_STAGES = [
463
+ '第 1 段 · 画面成立',
464
+ '第 1 段 · 画面成立',
465
+ '第 1 段 · 画面成立',
466
+ '第 2 段 · 视角与命中',
467
+ '第 2 段 · 视角与命中',
468
+ '第 3 段 · 业务组件',
469
+ '第 3 段 · 业务组件',
470
+ '第 3 段 · 业务组件',
471
+ '第 3 段 · 业务组件',
472
+ '第 4 段 · 接入与上线',
473
+ '第 4 段 · 接入与上线',
474
+ '第 4 段 · 接入与上线',
475
+ '第 4 段 · 接入与上线'
476
+ ]
477
+ const updateStepBrief = (index, step) => {
478
+ const guide = STEP_GUIDES[index] ?? { goal: '', accept: '', source: '' }
479
+ document.querySelector('#brief-stage').textContent = STEP_STAGES[index] ?? ''
480
+ document.querySelector('#brief-title').textContent = `${index + 1}. ${step.title}`
481
+ document.querySelector('#brief-goal').textContent = guide.goal
482
+ document.querySelector('#brief-accept').textContent = guide.accept
483
+ document.querySelector('#brief-api').textContent = step.api
484
+ document.querySelector('#brief-source').textContent = `对照源码:${guide.source}`
485
+ }
486
+
272
487
  const DATA = buildParkData(20240915)
273
488
  /** 业务字段 -> 样式:第 6 步之后的数据都先过这两张表 */
274
489
  const POINT_RULES = [
@@ -359,12 +574,12 @@
359
574
  },
360
575
  onDblClick: payload => log('onDblClick', describeElement(payload)),
361
576
  onSelect: payload => log('onSelect', payload ? `${payload.items.length} 个要素` : '清空选中'),
362
- onPointerDown: (payload, event) => {
363
- setReadout('pointer', `按下:${describeElement(payload)}(button ${event.button})`)
577
+ onPointerDown: payload => {
578
+ setReadout('pointer', `按下:${describeElement(payload)}(button ${payload.originalEvent?.button})`)
364
579
  },
365
- onPointerUp: (payload, event) => log('onPointerUp', `${describeElement(payload)}(button ${event.button})`),
366
- onContextMenu: (payload, event) => {
367
- event.preventDefault()
580
+ onPointerUp: payload => log('onPointerUp', `${describeElement(payload)}(button ${payload.originalEvent?.button})`),
581
+ onContextMenu: payload => {
582
+ payload.originalEvent?.preventDefault()
368
583
  log('onContextMenu', describeElement(payload))
369
584
  },
370
585
  // 需求 1 的核心:草稿里每点一次都回调,第一次拿起点、第二次拿终点
@@ -443,6 +658,7 @@
443
658
  log('mount', `第 ${index + 1} 步 ${step.file}`)
444
659
  syncCamera()
445
660
  updateButtons()
661
+ updateStepBrief(index, step)
446
662
  for (const button of stepsNode.children) {
447
663
  button.classList.toggle('on', Number(button.dataset.index) === index)
448
664
  }
@@ -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
  ])