gis-view-webgl2 0.1.5 → 0.1.7

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 (39) hide show
  1. package/README.md +6 -3
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +172 -8
  4. package/dist/index.js +1 -1
  5. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +121 -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 +117 -0
  8. package/docs/demos/LabelDemo.vue +2 -2
  9. package/docs/demos/MinimapDemo.vue +154 -1
  10. package/docs/demos/SearchFilterDemo.vue +113 -1
  11. package/docs/demos/StyleDemo.vue +1 -1
  12. package/docs/demos/api-index.js +22 -14
  13. package/docs/demos/demos.js +1 -1
  14. package/docs/demos/gis-view-demo/useGisViewDemo.js +2 -2
  15. package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +2 -0
  16. package/docs/internals/16-01-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/212.md +13 -2
  17. package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +11 -1
  18. 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 +61 -1
  19. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +5 -4
  20. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +38 -10
  21. 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 +1 -1
  22. 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 +7 -6
  23. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +59 -3
  24. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +14 -1
  25. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +23 -5
  26. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +12 -7
  27. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +16 -8
  28. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +27 -2
  29. 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 +4 -0
  30. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +9 -3
  31. 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 +1 -1
  32. package/docs/steps/README.md +5 -5
  33. package/docs/steps/selftest-demos.html +105 -6
  34. package/docs/steps/selftest-docs.html +3 -3
  35. package/docs/steps/selftest-features.html +1 -1
  36. package/docs/steps/selftest-full.html +42 -1
  37. package/docs/steps/selftest-perf.html +600 -421
  38. package/docs/steps/selftest-select.html +168 -1
  39. package/package.json +1 -1
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
11
11
  import { createGisView } from '../../src/index.js'
12
- import { buildDemoData } from './shared/demoData.js'
12
+ import { buildDemoData, DEMO_CENTER } from './shared/demoData.js'
13
13
  import './shared/demoStyle.css'
14
14
 
15
15
  const emit = defineEmits(['notice'])
@@ -44,8 +44,15 @@ const searchResult = ref(null)
44
44
  const filterResult = ref(null)
45
45
  const visibleDataText = ref('未读取')
46
46
  const activeFilter = ref('none')
47
+ const cameraReadout = ref('cameraAnimating: false')
48
+ const apiReadout = ref('dataVersion: -')
49
+ const lineStyleReadout = ref('getLineStyle(0): -')
50
+ const lastDataVersion = ref(null)
51
+ const lineStyleWarm = ref(false)
47
52
  let view = null
48
53
  let requestSeq = 0
54
+ let cameraReadoutFrame = 0
55
+ let pointTweakCursor = 0
49
56
 
50
57
  const SCOPES = [
51
58
  { id: 'all', label: '全部四类' },
@@ -186,6 +193,59 @@ const focusAllHits = () => {
186
193
  view?.focusOn(hits, { select: true, zoom: true })
187
194
  actionText.value = `focusOn(hits, { select: true, zoom: true }):${hits.length} 条命中一起进包围盒并选中`
188
195
  }
196
+ const syncCameraReadout = () => {
197
+ const animation = view?.getCameraAnimation() ?? null
198
+ cameraReadout.value = animation
199
+ ? `cameraAnimating: true · ${Math.round(animation.progress * 100)}% · ${animation.duration} ms`
200
+ : `cameraAnimating: ${view?.cameraAnimating === true} · 没有飞行`
201
+ }
202
+ const syncApiReadout = () => {
203
+ const data = view?.getData()
204
+ const style = view?.getLineStyle(0) ?? null
205
+ apiReadout.value = `dataVersion=${view?.dataVersion ?? '-'} · onDataChange=${lastDataVersion.value ?? '-'} · ${data?.points.length ?? 0} 节点 / ${data?.lines.length ?? 0} 线路`
206
+ lineStyleReadout.value = style
207
+ ? `getLineStyle(0):width=${style.width.toFixed(2)} · color=[${style.color.map(value => value.toFixed(2)).join(', ')}] · opacity=${style.opacity.toFixed(2)} · zIndex=${style.zIndex}`
208
+ : 'getLineStyle(0):null'
209
+ }
210
+ const pumpCameraReadout = () => {
211
+ if (cameraReadoutFrame) {
212
+ cancelAnimationFrame(cameraReadoutFrame)
213
+ }
214
+ const tick = () => {
215
+ cameraReadoutFrame = 0
216
+ syncCameraReadout()
217
+ if (view?.cameraAnimating) {
218
+ cameraReadoutFrame = requestAnimationFrame(tick)
219
+ }
220
+ }
221
+ cameraReadoutFrame = requestAnimationFrame(tick)
222
+ }
223
+ const flyToAllHits = () => {
224
+ const hits = searchResult.value?.hits ?? []
225
+ if (!hits.length) {
226
+ actionText.value = '当前没有可飞向的命中'
227
+ return
228
+ }
229
+ view?.flyTo(hits, { select: true, zoom: true, duration: 700, easing: 'ease-in-out' })
230
+ actionText.value = `flyTo(hits, { select: true, zoom: true, duration: 700 }):${hits.length} 条命中飞向同一包围盒`
231
+ syncCameraReadout()
232
+ pumpCameraReadout()
233
+ }
234
+ const flyToCameraPreset = () => {
235
+ if (!view) {
236
+ return
237
+ }
238
+ const scale = Math.max((view.getZoomBaseScale() || 1) * 1.6, 1)
239
+ view.flyToCamera({ center: [...DEMO_CENTER], scale }, { duration: 600, easing: 'ease-out' })
240
+ actionText.value = `flyToCamera({ center: [${DEMO_CENTER.join(', ')}], scale: ${scale.toFixed(2)} }, { duration: 600 }):不经过要素解析,直接飞相机`
241
+ syncCameraReadout()
242
+ pumpCameraReadout()
243
+ }
244
+ const stopCameraFlight = () => {
245
+ view?.stopCameraAnimation()
246
+ syncCameraReadout()
247
+ actionText.value = 'stopCameraAnimation():停在当前帧,不补跳到终点'
248
+ }
189
249
  const readVisibleData = () => {
190
250
  if (!view) {
191
251
  return null
@@ -214,6 +274,35 @@ const applyFilter = (filter) => {
214
274
  filterResult.value = view.getFilterResult()
215
275
  readVisibleData()
216
276
  }
277
+ const bumpDataVersion = () => {
278
+ if (!view) {
279
+ return
280
+ }
281
+ const points = view.getData().points
282
+ if (!points.length) {
283
+ return
284
+ }
285
+ const item = points[pointTweakCursor % points.length]
286
+ pointTweakCursor += 1
287
+ const nextLoading = Number(((Number(item.max_loading_rate) || 0) + 0.01).toFixed(3))
288
+ view.updatePoint(item.id ?? pointTweakCursor - 1, { max_loading_rate: nextLoading })
289
+ actionText.value = `updatePoint(${item.id ?? pointTweakCursor - 1}, { max_loading_rate: ${nextLoading} }):数据变了,dataVersion 应 +1`
290
+ syncApiReadout()
291
+ }
292
+ const toggleLineStyle = () => {
293
+ if (!view) {
294
+ return
295
+ }
296
+ lineStyleWarm.value = !lineStyleWarm.value
297
+ if (lineStyleWarm.value) {
298
+ view.updateLineStyle({ color: '#f59f4b', width: 3.2, opacity: 1 })
299
+ }
300
+ else {
301
+ view.updateLineStyle({ color: 'rgba(120, 168, 226, 0.43)', width: 1.2, opacity: 0.43 })
302
+ }
303
+ actionText.value = `updateLineStyle(${lineStyleWarm.value ? '暖色 + 3.2px' : '恢复 1.2px' }):只重算线宽 / 颜色链,dataVersion 不变`
304
+ syncApiReadout()
305
+ }
217
306
 
218
307
  onMounted(() => {
219
308
  view = createGisView(container.value, {
@@ -241,6 +330,10 @@ onMounted(() => {
241
330
  onReady: () => {
242
331
  emit('notice', `${dataset.points.length} 节点 / ${dataset.lines.length} 线路,搜索与筛选都是只读口径`)
243
332
  },
333
+ onDataChange: payload => {
334
+ lastDataVersion.value = payload.dataVersion
335
+ syncApiReadout()
336
+ },
244
337
  onFilter: result => syncFilterReadout(result),
245
338
  onWarning: text => {
246
339
  actionText.value = `warning:${text}`
@@ -248,10 +341,15 @@ onMounted(() => {
248
341
  })
249
342
  runSearch('sync')
250
343
  readVisibleData()
344
+ syncApiReadout()
251
345
  })
252
346
 
253
347
  onBeforeUnmount(() => {
254
348
  requestSeq += 1
349
+ if (cameraReadoutFrame) {
350
+ cancelAnimationFrame(cameraReadoutFrame)
351
+ cameraReadoutFrame = 0
352
+ }
255
353
  view?.destroy()
256
354
  view = null
257
355
  })
@@ -301,9 +399,12 @@ onBeforeUnmount(() => {
301
399
  <p class='dv-hint'>scope 可写 points / lines / channels / groups,也认 point / line / channel / group;显式 fields 能搜默认会跳过的 symbol。搜索基于全量数据,不隐藏、也不自动移动视角。</p>
302
400
  <div class='dv-row'>
303
401
  <button type='button' class='dv-button' data-demo='search-filter-focus-all' @click='focusAllHits'>聚焦并选中全部命中</button>
402
+ <button type='button' class='dv-button dv-button--primary' data-demo='search-filter-fly-all' @click='flyToAllHits'>飞向选中(flyTo)</button>
403
+ <button type='button' class='dv-button' data-demo='search-filter-stop-flight' @click='stopCameraFlight'>停止飞行</button>
304
404
  </div>
305
405
  <p class='dv-readout' data-demo='search-filter-summary'>{{ searchSummary }}</p>
306
406
  <p class='dv-readout' data-demo='search-filter-meta'>{{ searchMeta }}</p>
407
+ <p class='dv-readout' data-demo='search-filter-camera'>{{ cameraReadout }}</p>
307
408
  <p class='dv-hint' data-demo='search-filter-action'>{{ actionText }}</p>
308
409
  <ul v-if='hitRows.length' class='sf-hits'>
309
410
  <li v-for='row in hitRows' :key='row.key'>
@@ -335,6 +436,17 @@ onBeforeUnmount(() => {
335
436
  </div>
336
437
  <p class='dv-hint' data-demo='search-filter-filter-action'>{{ filterAction }}</p>
337
438
  </div>
439
+ <div class='dv-group'>
440
+ <h3>相机、版本与线路读数<span class='dv-chip'>flyToCamera</span></h3>
441
+ <div class='dv-row'>
442
+ <button type='button' class='dv-button dv-button--primary' data-demo='search-filter-fly-camera' @click='flyToCameraPreset'>飞向固定相机</button>
443
+ <button type='button' class='dv-button' data-demo='search-filter-data-bump' @click='bumpDataVersion'>改一个点(dataVersion +1)</button>
444
+ <button type='button' class='dv-button' data-demo='search-filter-line-recolor' @click='toggleLineStyle'>线路换色 / 加宽</button>
445
+ </div>
446
+ <p class='dv-readout' data-demo='search-filter-data-version'>{{ apiReadout }}</p>
447
+ <p class='dv-readout' data-demo='search-filter-line-style'>{{ lineStyleReadout }}</p>
448
+ <p class='dv-hint'>flyToCamera 直接收 <code>{ center?, scale? }</code>,没写的字段沿用当前相机;updateLineStyle 只改全局线宽 / 颜色时走窄路,dataVersion 不增加,getLineStyle 仍能读到最新解析值。</p>
449
+ </div>
338
450
  <div class='dv-group'>
339
451
  <h3>两件事别混用</h3>
340
452
  <p class='dv-hint'>search 只把要素找出来,结果带 bounds;要居中就再调 focusOn。setFilter 只隐藏显示、不删数据,也不清空选中集:条件放宽后,原本选中的要素仍保持选中。</p>
@@ -86,7 +86,7 @@ const buildOptions = () => ({
86
86
  syncRules()
87
87
  },
88
88
  /**
89
- * 读数接 onSelectChange 而不是 onSelect:前者对「点选、框选、API select()」都会发,
89
+ * 读数接 onSelectChange 而不是 onSelect:前者对「点选、框选、API selectFeatures()」都会发,
90
90
  * 后者只代表画布上的那一次点击 —— 用接口选中时读数也不会停在「未选中」。
91
91
  * 选中高亮的颜色 / 尺寸写在 pointSelectStyle / lineSelectStyle 里,这里只让面板知道选中集变了。
92
92
  */
@@ -7,8 +7,8 @@
7
7
  * 源码或文档改过之后重跑一遍;`node docs/steps/check-doc-coverage.mjs` 会对账。
8
8
  */
9
9
 
10
- /** { members: 257, methods: 254, properties: 3, options: 96, sections: 16 } */
11
- export const API_SUMMARY = {"members":257,"methods":254,"properties":3,"options":96,"sections":16}
10
+ /** { members: 265, methods: 260, properties: 5, options: 96, sections: 16 } */
11
+ export const API_SUMMARY = {"members":265,"methods":260,"properties":5,"options":96,"sections":16}
12
12
 
13
13
  /** 实例成员,按 docs/GisView.md「参数速查」的分组排 */
14
14
  export const API_SECTIONS = [
@@ -51,13 +51,17 @@ export const API_SECTIONS = [
51
51
  { name: "zoomTo", kind: "method", params: "scale: number 目标缩放值(世界单位 → CSS 像素的比例);origin?: [x, y] 缩放中心", returns: "this", doc: "直接设成指定比例(像素 / 世界单位)", examples: [{"group":"相机与视图","code":"// zoomTo(scale, origin?):直接给目标比例(世界单位 → CSS 像素)\nview.zoomTo(12)\n","from":"header"}] },
52
52
  { name: "zoomToBounds", kind: "method", params: "bounds: GisViewBounds 要装进画面的世界矩形 [minX, minY, maxX, maxY](也认 { minX, minY, maxX, maxY });padding?: number 四周留白", returns: "this", doc: "把一段世界坐标矩形适应到视图内,退化成一点时只居中不改比例", examples: [{"group":"相机与视图","code":"// zoomToBounds(bounds, padding?):把世界矩形装进画面(数组与对象写法都认)\nview.zoomToBounds([0, 0, 400, 200], 40)\nview.zoomToBounds({ minX: 116, minY: 39, maxX: 117, maxY: 40 })\n","from":"header"}] },
53
53
  { name: "focusOn", kind: "method", params: "target: GisViewFocusTargets 要对准的东西:id / { type: 'point' | 'line' | 'channel' | 'group', id | index } / { channelId }(整条通道)/ { type: 'group', channelId?, groupId } / [x, y] 坐标 / 矩形 / { lngLat } / { center },或者一批(数组取整体包围盒)。字符串按「先节点、再线路」,裸字符串不能表示通道;同 id 用显式 type 消歧,分组必须用 type: 'group'(groupId 全局唯一时可省略 channelId)。缺省只居中、比例一点不动;options.zoom: true 才按解析出的真实范围适配,四类目标共用这一条、没有额外放大倍率;scale 优先级高于 zoom,maxZoom 只给单点 / 单线这类零面积目标补最小范围。options?: GisViewFocusOptions:zoom: true 显式适配、scale 直接给比例、center: true 显式只居中(与缺省一致)、padding 要四周留白(CSS 像素,缺省 40)、maxZoom 限制这次适配的最大比例、select 顺带选中(true 选这次目标,也可另给一批写法)、mode 决定怎么并进当前选中集。目标认不出来或参数非法时什么都不做", returns: "this(相机更新后发 onCameraChange;select 生效时另发 onSelect / onSelectChange)", doc: "把视角移到目标上:单条(搜索命中 / 要素 id / { type: 'point' | 'line' | 'channel', id | index } / { channelId } 整条通道 / { type: 'group', channelId?, groupId } / 坐标 / 矩形 / { lngLat })或一批(数组取整体包围盒 —— 搜到三个节点就是这三个点的中心当屏幕中心);字符串先节点后线路,裸字符串不表示通道,同 id 用 type 消歧,分组必须显式写 type: 'group'(全局唯一 groupId 可省略通道)。缺省只居中、比例一点不动;options.zoom: true 才按解析出的真实范围适配(节点 / 线路 / 通道 / 分组共用这一条,不额外放大),scale 优先级高于 zoom。options 还认 center: true(显式只居中,与缺省一致)、padding、maxZoom、select(顺带选中这批,mode 决定怎么并进当前选中集)", examples: [{"group":"相机与视图","code":"// focusOn(target, options?):对准一个或多个目标(id / 显式类型 / 坐标 / 矩形 / 搜索命中 / 一批)\nview.focusOn('a') // 缺省只居中,比例不动\nview.focusOn('a', { zoom: true, maxZoom: 8 }) // 显式按范围适配\nview.focusOn(['a', 'b'], { zoom: true, padding: 60, select: true })\nview.focusOn({ type: 'point', id: 'a' }) // 节点与线路同 id 时显式消歧\nview.focusOn({ type: 'line', id: 'a' })\nview.focusOn({ type: 'channel', channelId: 'c1' }) // 通道也可以直接写 { channelId: 'c1' }\nview.focusOn({ type: 'group', channelId: 'c1', groupId: 'c1::2' })\nview.focusOn({ type: 'group', groupId: 'global-group' }) // groupId 全局唯一时可省略通道\nview.focusOn([120, 80], { center: true }) // 显式只居中(与缺省一致)\nview.focusOn({ lngLat: [116.4, 39.9] })\n","from":"header"}] },
54
+ { name: "flyTo", kind: "method", params: "目标与世界坐标解析、zoom / scale / center / padding / maxZoom / select / mode 均与 focusOn 同义。options?: GisViewFlyOptions 另认 duration(毫秒,缺省 450;<= 0 立即落位)与 easing('linear' / 'ease-in' / 'ease-out' / 'ease-in-out',缺省 'ease-in-out',也可给自定义 (progress) => eased)。中心线性、比例按对数插值;系统 prefers-reduced-motion: reduce 时立即落位。再次 flyTo、用户拖拽 / 滚轮、setCamera / centerOn / zoom* / fitView / resetView / applyInitialView 都会停在当前帧", returns: "this(相机变化时发 onCameraChange;select 生效时另发选中事件)", doc: "平滑飞到与 focusOn 相同的目标:目标写法与 zoom / scale / center / padding / maxZoom / select / mode 完全同义,另认 duration(毫秒,缺省 450)与 easing(linear / ease-in / ease-out / ease-in-out 或 (progress) => progress)。中心线性插值、比例按对数插值;duration <= 0 或系统要求减少动态效果时立即落位。再次飞行、用户拖拽 / 滚轮或任何程序化改相机都会停在当前帧,不补跳到终点", examples: [{"group":"相机与视图","code":"// flyTo(target, options?):与 focusOn 同一套目标解析,平滑飞过去\nview.flyTo(hit, { zoom: true, duration: 600, easing: 'ease-in-out' })\nview.flyTo(['station-1', 'station-2'], { select: true, duration: 0 }) // 立即落位\nview.flyTo({ center: [120, 80], scale: 12, easing: progress => progress * progress })\n","from":"header"}] },
55
+ { name: "flyToCamera", kind: "method", params: "camera: GisViewCameraTarget 直接给相机目标 { center?, scale? }:没写 center / scale 的那一项沿用当前相机,不经过要素解析,也不改选中与数据;options?: GisViewFlyCameraOptions 的 duration / easing 与 flyTo 同义。中心 / 比例插值、减少动态效果、用户手势取消都走同一条相机动画", returns: "this(相机变化时发 onCameraChange;目标非法时保持当前相机)", doc: "直接飞到相机目标 { center?, scale? }:不写的那一项沿用当前相机,不经过要素解析、也不改选中与数据;duration / easing 与 flyTo 同义,参数非法时保持当前相机", examples: [{"group":"相机与视图","code":"// flyToCamera(camera, options?):直接飞相机目标;没写的 center / scale 沿用当前相机\nview.flyToCamera({ center: [120, 80], scale: 3 }, { duration: 600, easing: 'ease-out' })\nview.flyToCamera({ scale: 5 }, { duration: 0 }) // 中心不动,比例立即落到 5\nview.flyToCamera({ center: [116.4, 39.9] }) // 比例沿用当前相机\n","from":"header"}] },
54
56
  { name: "search", kind: "method", params: "input: string | GisViewSearchOptions 关键字,或 { keyword, scope, fields, limit } 这类选项。scope 可选 'points' / 'lines' / 'channels' / 'groups',也认与 focusOn 一致的单数别名 'point' / 'line' / 'channel' / 'group' 及数组;省略或写错时四类全搜。limit 对每一类分别计数。返回的 groups / hits 带分组命中(channelIndex / groupId / groupIndex,item 是轻量快照)—— 只返回结果、不动视角,要定位就自己调 focusOn(result.hits, …)", returns: "GisViewSearchResult:keyword / scope / points / lines / channels / groups / hits / total / truncated / durationMs", doc: "按关键字搜索节点 / 线路 / 通道 / 通道分组并返回命中:scope 认复数的 points / lines / channels / groups,也认与 focusOn 一致的单数别名 point / line / channel / group;结果分 points / lines / channels / groups 四类,分组命中带 channelIndex / groupId / groupIndex,可直接交给 focusOn / selectFeatures。不动视角:要不要聚焦由调用方自己调 focusOn,见「搜索与定位」", examples: [{"group":"相机与视图","code":"// search(input) / searchAsync(input):搜索(只返回结果、不动视角)\nconst hits = view.search('甲站')\nconst scoped = view.search({ keyword: '甲', scope: ['points', 'channels', 'groups'], limit: 10 })\nconst groupHits = view.search({ keyword: 'c1::2', scope: 'group' }) // 单数别名与 focusOn 一致\nview.focusOn(groupHits.groups[0], { zoom: true, select: true }) // 按整组范围适配并选中成员回线\nview.focusOn(hits.hits, { zoom: true, maxZoom: 8 }) // 命中集直接丢给 focusOn\nconst asyncHits = await view.searchAsync('甲站') // 大表走 Worker;stale 说明这一份不作数了\n","from":"header"}] },
55
57
  { name: "searchAsync", kind: "method", params: "同上;分组范围的当前轻量快照会随请求交给 Worker", returns: "Promise<GisViewSearchResult>:多一个 worker(是不是在 Worker 里算的)与 stale(回来时数据已经改过、或又搜过一次,这一份不作数了)", doc: "同一份口径的异步版:数据量大时把扫描放进 Worker(10 万点 / 10 万线下同步要占主线程 40 ms 上下的那一段),返回 Promise<GisViewSearchResult>;结果带 worker 与 stale 两个字段,见「异步搜索」", examples: [{"group":"相机与视图","code":"// search(input) / searchAsync(input):搜索(只返回结果、不动视角)\nconst hits = view.search('甲站')\nconst scoped = view.search({ keyword: '甲', scope: ['points', 'channels', 'groups'], limit: 10 })\nconst groupHits = view.search({ keyword: 'c1::2', scope: 'group' }) // 单数别名与 focusOn 一致\nview.focusOn(groupHits.groups[0], { zoom: true, select: true }) // 按整组范围适配并选中成员回线\nview.focusOn(hits.hits, { zoom: true, maxZoom: 8 }) // 命中集直接丢给 focusOn\nconst asyncHits = await view.searchAsync('甲站') // 大表走 Worker;stale 说明这一份不作数了\n","from":"header"}] },
56
58
  { name: "setCamera", kind: "method", params: "camera: Partial<GisViewCamera> 要改的相机字段 { x, y, scale }(只写一个也行)", returns: "this", doc: "读写 { center, scale }", examples: [{"group":"相机与视图","code":"// setCamera(camera) / getCamera():读写相机(只写一个字段也行)\nview.setCamera({ scale: 10 })\nview.setCamera({ x: 200, y: 120 })\nconst camera = view.getCamera()\n","from":"header"}] },
57
59
  { name: "getCamera", kind: "method", params: "无", returns: "GisViewCamera", doc: "读写 { center, scale }", examples: [{"group":"相机与视图","code":"// setCamera(camera) / getCamera():读写相机(只写一个字段也行)\nview.setCamera({ scale: 10 })\nview.setCamera({ x: 200, y: 120 })\nconst camera = view.getCamera()\n","from":"header"}] },
60
+ { name: "stopCameraAnimation", kind: "method", params: "无", returns: "this:停止当前相机飞行并停在当前帧;没有飞行时是空操作", doc: "停止当前相机飞行(停在当前帧,返回实例)/ 读当前飞行 { active, progress, duration, from, to }(没有动画时 null)", examples: [{"group":"相机与视图","code":"// stopCameraAnimation() / getCameraAnimation():停止飞行 / 读当前飞行读数(没有动画时为 null)\nconst flight = view.getCameraAnimation() // { active, progress, duration, from, to }\nview.stopCameraAnimation() // 停在当前帧,不补跳到终点\n","from":"header"}] },
61
+ { name: "getCameraAnimation", kind: "method", params: "无", returns: "GisViewCameraAnimation | null:{ active, progress, duration, from, to },progress 是时间进度(0 ~ 1),没有动画时 null", doc: "停止当前相机飞行(停在当前帧,返回实例)/ 读当前飞行 { active, progress, duration, from, to }(没有动画时 null)", examples: [{"group":"相机与视图","code":"// stopCameraAnimation() / getCameraAnimation():停止飞行 / 读当前飞行读数(没有动画时为 null)\nconst flight = view.getCameraAnimation() // { active, progress, duration, from, to }\nview.stopCameraAnimation() // 停在当前帧,不补跳到终点\n","from":"header"}] },
58
62
  { name: "getZoomBaseScale", kind: "method", params: "无", returns: "number 缩放联动的基准比例(pointZoomScale / lineZoomScale 共用那一份;缺省是首次适应视图时的 camera.scale)", doc: "读 / 重定缩放联动的基准比例(缺省是首次适应视图时的比例):不传参数就取当下相机的比例 —— 把现在这一屏当成「原样大小」,之后放大缩小都从它算倍数;配合 pointZoomScale / lineZoomScale 使用", examples: [{"group":"相机与视图","code":"// getZoomBaseScale() / setZoomBaseScale(scale?):缩放联动的基准比例(配合 pointZoomScale / lineZoomScale)\nconst zoomBase = view.getZoomBaseScale() // 基准(缺省首次适应视图时的 camera.scale)\nview.setZoomBaseScale() // 以当下这一屏为「原样大小」,之后放大缩小都从它算倍数\nview.setZoomBaseScale(2.5) // 也可以直接给一个比例\n","from":"header"}] },
59
63
  { name: "setZoomBaseScale", kind: "method", params: "scale?: number 新的基准比例;不传取当下相机的比例(把现在这一屏当成「原样大小」)", returns: "this", doc: "读 / 重定缩放联动的基准比例(缺省是首次适应视图时的比例):不传参数就取当下相机的比例 —— 把现在这一屏当成「原样大小」,之后放大缩小都从它算倍数;配合 pointZoomScale / lineZoomScale 使用", examples: [{"group":"相机与视图","code":"// getZoomBaseScale() / setZoomBaseScale(scale?):缩放联动的基准比例(配合 pointZoomScale / lineZoomScale)\nconst zoomBase = view.getZoomBaseScale() // 基准(缺省首次适应视图时的 camera.scale)\nview.setZoomBaseScale() // 以当下这一屏为「原样大小」,之后放大缩小都从它算倍数\nview.setZoomBaseScale(2.5) // 也可以直接给一个比例\n","from":"header"}] },
60
- { name: "getBounds", kind: "method", params: "无", returns: "GisViewBounds", doc: "数据范围(没有点时回退到背景 GeoJSON 的 rawBounds,也就是 aspectScale 之前的真实经纬度范围)", examples: [{"group":"相机与视图","code":"// getBounds() / getViewRect() / getViewport():数据范围 / 视口世界矩形 / 画布尺寸读数\nconst dataBounds = view.getBounds()\nconst viewRect = view.getViewRect()\nconst { cssWidth, cssHeight, pixelRatio } = view.getViewport()\n","from":"header"}] },
64
+ { name: "getBounds", kind: "method", params: "无", returns: "GisViewBounds:每次返回独立副本;重复调用命中数据包围盒缓存,改点 / 改线 / 追加或拖拽后下一次读数自动重建", doc: "数据范围(没有点时回退到背景 GeoJSON 的 rawBounds,也就是 aspectScale 之前的真实经纬度范围);每次返回独立副本,重复调用命中缓存", examples: [{"group":"相机与视图","code":"// getBounds() / getViewRect() / getViewport():数据范围 / 视口世界矩形 / 画布尺寸读数\nconst dataBounds = view.getBounds()\nconst viewRect = view.getViewRect()\nconst { cssWidth, cssHeight, pixelRatio } = view.getViewport()\n","from":"header"}] },
61
65
  { name: "getViewport", kind: "method", params: "无", returns: "{ cssWidth, cssHeight, pixelRatio, displayScale }:画布 CSS 尺寸、设备像素比与渲染分辨率比例", doc: "/** * 当前视口。", examples: [{"group":"相机与视图","code":"// getBounds() / getViewRect() / getViewport():数据范围 / 视口世界矩形 / 画布尺寸读数\nconst dataBounds = view.getBounds()\nconst viewRect = view.getViewRect()\nconst { cssWidth, cssHeight, pixelRatio } = view.getViewport()\n","from":"header"}] },
62
66
  { name: "getViewRect", kind: "method", params: "无", returns: "GisViewBounds", doc: "当前可视范围的世界坐标矩形;自建缩略图 / 坐标读数用它换算视口框,内置鹰眼图见「鹰眼图」", examples: [{"group":"相机与视图","code":"// getBounds() / getViewRect() / getViewport():数据范围 / 视口世界矩形 / 画布尺寸读数\nconst dataBounds = view.getBounds()\nconst viewRect = view.getViewRect()\nconst { cssWidth, cssHeight, pixelRatio } = view.getViewport()\n","from":"header"}] },
63
67
  { name: "isLayoutReady", kind: "method", params: "无", returns: "boolean", doc: "大数据发散渲染(progressiveInit)是否已经铺完;没铺完时标签与拾取只看得到已经画出去的那部分", examples: [{"group":"相机与视图","code":"// isLayoutReady():容器量到尺寸、首批布局完成没有\nconst layoutReady = view.isLayoutReady()\n","from":"header"}] },
@@ -94,8 +98,9 @@ export const API_SECTIONS = [
94
98
  { name: "createGeoMapper", kind: "method", params: "options?: GisViewGeoMapperOptions 映射选项(lngKey / latKey / xKey / yKey / fit…)", returns: "GisViewGeoMapper | null", doc: "建一个「数据坐标 → GeoJSON 坐标」的映射器:mode: 'lngLat' | 'fit' | 'scale'、scale(节点与 geojson 的大小比例)、scaleX / scaleY / offsetX / offsetY / center / bounds / data / padding / target,返回 map / unmap / mapPoint / mapLine / mapChannel / mapData 与最终的比例偏移;没有 GeoJSON 背景时是 null", examples: [{"group":"坐标、拾取与读数","code":"// createGeoMapper(options?) / mapDataToGeoJson(data, options?):把另一套坐标系的数据搬到画出来的底图上\nconst mapper = view.createGeoMapper({ mode: 'fit', padding: 0.08, data })\nconst mappedPoints = view.getData().points.map(mapper.mapPoint)\nconst mapped = view.mapDataToGeoJson({ points, lines, channels }, { mode: 'fit' })\nview.updateData(mapped.points, mapped.lines)\n","from":"header"}] },
95
99
  { name: "mapDataToGeoJson", kind: "method", params: "data: { points?, lines?, channels? } 要搬的数据;options?: GisViewGeoMapperOptions 同 createGeoMapper", returns: "GisViewPoint[]; lines?: GisViewLine[]; channels?: GisViewChannel[] }>(data: T, options?: GisViewGeoMapperOptions): T | null", doc: "createGeoMapper(options).mapData(data) 的快捷写法:把 { points, lines, channels } 整份搬到 GeoJSON 坐标,原数据不动;没有 GeoJSON 背景时是 null", examples: [{"group":"坐标、拾取与读数","code":"// createGeoMapper(options?) / mapDataToGeoJson(data, options?):把另一套坐标系的数据搬到画出来的底图上\nconst mapper = view.createGeoMapper({ mode: 'fit', padding: 0.08, data })\nconst mappedPoints = view.getData().points.map(mapper.mapPoint)\nconst mapped = view.mapDataToGeoJson({ points, lines, channels }, { mode: 'fit' })\nview.updateData(mapped.points, mapped.lines)\n","from":"header"}] },
96
100
  { name: "getPointer", kind: "method", params: "无", returns: "GisViewPointerState", doc: "当前光标状态 { inside, screen, world, lngLat }(onPointer 回调的同一份数据)", examples: [{"group":"坐标、拾取与读数","code":"// getPointer():光标位置读数 { inside, screen, world, lngLat }\nconst pointer = view.getPointer()\n","from":"header"}] },
97
- { name: "pick", kind: "method", params: "x / y 画布 CSS 像素", returns: "GisViewPickResult | null", doc: "拾取指定画布位置上的要素(clickable: false 的要素不会返回)", examples: [{"group":"坐标、拾取与读数","code":"//(pick / getContextAt / nearestPointsAt 这些吃画布像素的方法先用这一对转;自动处理 canvas rect 与宿主缩放)\nconst [sx, sy] = view.clientToScreen(event.clientX, event.clientY)\nconst [fixedX, fixedY] = view.screenToClient(320, 200) // 摆 position: fixed 的浮层 / 菜单\n","from":"header"},{"group":"坐标、拾取与读数","code":"// pick(x, y) / pickAt(clientX, clientY):按画布像素 / 客户端坐标拾取\nconst hit = view.pick(320, 200)\nconst hitAt = view.pickAt(event.clientX, event.clientY)\n","from":"header"}] },
98
- { name: "pickAt", kind: "method", params: "clientX / clientY 浏览器客户端坐标(PointerEvent 原样给)", returns: "GisViewPickAtResult | null", doc: "按浏览器客户端坐标拾取(就是鼠标事件里的 clientX / clientY):与指针事件载荷同一份结果(type / index / item / distance / screen / coordinate 与通道字段),另加 target('point' / 'line' / 'channel')与 lngLat;没命中返回 null。只读,不改选中也不动光标", examples: [{"group":"坐标、拾取与读数","code":"// pick(x, y) / pickAt(clientX, clientY):按画布像素 / 客户端坐标拾取\nconst hit = view.pick(320, 200)\nconst hitAt = view.pickAt(event.clientX, event.clientY)\n","from":"header"}] },
101
+ { name: "pick", kind: "method", params: "x / y 画布 CSS 像素", returns: "GisViewPickResult | null", doc: "拾取指定画布位置上的要素(clickable: false 的要素不会返回)", examples: [{"group":"坐标、拾取与读数","code":"//(pick / getContextAt / nearestPointsAt 这些吃画布像素的方法先用这一对转;自动处理 canvas rect 与宿主缩放)\nconst [sx, sy] = view.clientToScreen(event.clientX, event.clientY)\nconst [fixedX, fixedY] = view.screenToClient(320, 200) // 摆 position: fixed 的浮层 / 菜单\n","from":"header"},{"group":"坐标、拾取与读数","code":"// pick(x, y) / pickAt(clientX, clientY):按画布像素 / 客户端坐标拾取\nconst hit = view.pick(320, 200)\nconst hitAt = view.pickAt(event.clientX, event.clientY)\nhitAt?.groups // 命中通道回线:同一通道的全部分组;hitAt.group 是当前这一组\n","from":"header"}] },
102
+ { name: "pickAt", kind: "method", params: "clientX / clientY 浏览器客户端坐标(PointerEvent 原样给);命中通道回线时返回的 group 是当前组、groups 是整条通道的全部分组", returns: "GisViewPickAtResult | null", doc: "按浏览器客户端坐标拾取(就是鼠标事件里的 clientX / clientY):与指针事件载荷同一份结果(type / index / item / distance / screen / coordinate 与通道字段),另加 target('point' / 'line' / 'channel')与 lngLat;命中通道回线时 group 是当前组、groups 是整条通道的全部分组;没命中返回 null。只读,不改选中也不动光标", examples: [{"group":"坐标、拾取与读数","code":"// pick(x, y) / pickAt(clientX, clientY):按画布像素 / 客户端坐标拾取\nconst hit = view.pick(320, 200)\nconst hitAt = view.pickAt(event.clientX, event.clientY)\nhitAt?.groups // 命中通道回线:同一通道的全部分组;hitAt.group 是当前这一组\n","from":"header"}] },
103
+ { name: "selectAt", kind: "method", params: "clientX / clientY 浏览器客户端坐标;按普通单击的命中与通道摊开规则立即选中,空白 / 聚合圆返回 null、不改选中,不派发 onClick / onDblClick。每次成功命中都发 onSelect,选中集实际变化时再发 onSelectChange(cause: 'api',顺序 onSelect → onSelectChange);通道回线载荷带当前 group / 完整 groups,多通道看 channelContexts。已知 id 时优先 selectFeatures", returns: "GisViewSelectPayload | null", doc: "只有客户端坐标时用它像普通单击一样立即选中命中要素:节点 / 线路直接选中,通道回线按 channelSelectMode 摊开;空白或聚合圆返回 null 且不改选中。不等待 selectTrigger: 'dblClick',也不派发 onClick / onDblClick;每次成功命中都发 onSelect,选中集实际变化时再发 onSelectChange,顺序 onSelect → onSelectChange(cause: 'api')。选中通道回线时载荷带当前 group / 完整 groups,多通道看 channelContexts。已知节点 / 线路 id 时直接用 selectFeatures,见「按坐标单击选中(selectAt)」", examples: [{"group":"选中、复制与合并","code":"// selectAt(clientX, clientY):每次命中发 onSelect,集合变化时再发 onSelectChange(不动指针、不派发 onClick);已知 id 直接用 selectFeatures\nview.selectAt(event.clientX, event.clientY) // 节点 / 线路命中即选中;空白返回 null\nview.updateOptions({ channelSelectMode: 'channel' })\nview.selectAt(event.clientX, event.clientY) // 命中任一回线 = 选中整条通道\n","from":"header"}] },
99
104
  { name: "hoverAt", kind: "method", params: "clientX / clientY 客户端坐标", returns: "GisViewHoverPayload | null", doc: "程序化悬停:hoverAt 让指针「停」到某个客户端坐标上(悬停高亮、光标与 onHover 一起更新,返回与 onHover 载荷同一份读数,空白处 null;不走悬停阈值,也不用先订阅事件)、clearHover() 撤掉并补发一条 onHover(null)、getHover() 随时读当前这一份 —— 外部列表 / 表格 hover 到某一行时联动高亮图上的要素用它", examples: [{"group":"坐标、拾取与读数","code":"// hoverAt(clientX, clientY) / clearHover() / getHover():程序化悬停 / 撤掉 / 读当前那一份\nview.hoverAt(event.clientX, event.clientY)\nview.clearHover()\nconst hover = view.getHover()\n","from":"header"}] },
100
105
  { name: "clearHover", kind: "method", params: "无", returns: "null", doc: "程序化悬停:hoverAt 让指针「停」到某个客户端坐标上(悬停高亮、光标与 onHover 一起更新,返回与 onHover 载荷同一份读数,空白处 null;不走悬停阈值,也不用先订阅事件)、clearHover() 撤掉并补发一条 onHover(null)、getHover() 随时读当前这一份 —— 外部列表 / 表格 hover 到某一行时联动高亮图上的要素用它", examples: [{"group":"坐标、拾取与读数","code":"// hoverAt(clientX, clientY) / clearHover() / getHover():程序化悬停 / 撤掉 / 读当前那一份\nview.hoverAt(event.clientX, event.clientY)\nview.clearHover()\nconst hover = view.getHover()\n","from":"header"}] },
101
106
  { name: "getHover", kind: "method", params: "无", returns: "GisViewHoverPayload | null", doc: "程序化悬停:hoverAt 让指针「停」到某个客户端坐标上(悬停高亮、光标与 onHover 一起更新,返回与 onHover 载荷同一份读数,空白处 null;不走悬停阈值,也不用先订阅事件)、clearHover() 撤掉并补发一条 onHover(null)、getHover() 随时读当前这一份 —— 外部列表 / 表格 hover 到某一行时联动高亮图上的要素用它", examples: [{"group":"坐标、拾取与读数","code":"// hoverAt(clientX, clientY) / clearHover() / getHover():程序化悬停 / 撤掉 / 读当前那一份\nview.hoverAt(event.clientX, event.clientY)\nview.clearHover()\nconst hover = view.getHover()\n","from":"header"}] },
@@ -106,7 +111,8 @@ export const API_SECTIONS = [
106
111
  { name: "setDropPreview", kind: "method", params: "preview: GisViewDropPreviewInput | null:kind('point' / 'line' / 'channel' / 'waypoint' / 'decoration' / 'label',缺省按 item 推断)、screen(画布 CSS 像素)或 world(世界坐标)至少给一个、item 是准备落地的那一份字段(与 addPoint / addLine 同一份)、from(line / channel 的起点);传 null 清掉预览", returns: "GisViewDropPreview | null 解析后的读数 { kind, screen, world, size, styleSize, radius, color, strokeColor, strokeWidth, opacity, symbol, width, dashed }:走的是与真实要素完全相同的样式链(规则 -> 全局样式 -> 这一份字段),styleSize 与落地后 getPointStyle().size 是同一个数(size 是这一刻画出来的宽度,不悬停 / 不选中时两者相同)", doc: "/** * 拖拽落点预览。", examples: [{"group":"坐标、拾取与读数","code":"// setDropPreview(preview) / getDropPreview():拖拽落点预览(setDropPreview(null) 清掉)\nview.setDropPreview({ kind: 'point', screen: [320, 200], item: { name: '新站' } })\nconst preview = view.getDropPreview()\nview.setDropPreview(null)\n","from":"header"}] },
107
112
  { name: "getDropPreview", kind: "method", params: "无", returns: "GisViewDropPreview | null:当前拖拽落点预览的读数,没有预览时 null", doc: "/** * 拖拽落点预览。", examples: [{"group":"坐标、拾取与读数","code":"// setDropPreview(preview) / getDropPreview():拖拽落点预览(setDropPreview(null) 清掉)\nview.setDropPreview({ kind: 'point', screen: [320, 200], item: { name: '新站' } })\nconst preview = view.getDropPreview()\nview.setDropPreview(null)\n","from":"header"}] },
108
113
  { name: "getPoint", kind: "method", params: "ref: GisViewFeatureRef 节点定位:id、下标、{ id } / { index }", returns: "GisViewPointInfo | null", doc: "按 id / 下标 / { id } / { index } 取一个节点:拾取载荷(item / x / y / layer / layerRank)之外再给一份屏幕半径 radius;找不到返回 null。自建读数面板、右键菜单用它,省掉「自己遍历 getData() + 算投影」那一段", examples: [{"group":"坐标、拾取与读数","code":"// getPoint(ref) / getLine(ref) / getChannel(ref):一个要素的几何读数\nconst pointInfo = view.getPoint('a')\nconst lineInfo = view.getLine('l1')\nconst channelInfo = view.getChannel('c1')\n","from":"header"}] },
109
- { name: "getPointStyle", kind: "method", params: "index: number 节点下标", returns: "GisViewPointStyleSnapshot | null:节点解析后的样式快照 { index, size, symbol, zIndex },size / symbol 取样式链(节点自身 > 规则 > pointStyle)定下来的那一份,含 sizeScale、不含悬停 / 选中样式与热力倍率;落点预览的读数与它同源", doc: "/** * 节点解析之后的样式快照(size / symbol / zIndex,含 sizeScale、不含状态样式与热力倍率)。", examples: [{"group":"坐标、拾取与读数","code":"// getPointStyle(index):节点解析后的样式快照(样式链定下来的那一份)\nconst pointStyle = view.getPointStyle(0)\n","from":"header"}] },
114
+ { name: "getPointStyle", kind: "method", params: "index: number 节点下标", returns: "GisViewPointStyleSnapshot | null:节点解析后的样式快照 { index, size, symbol, zIndex },size / symbol 取样式链(节点自身 > 规则 > pointStyle)定下来的那一份,含 sizeScale、不含悬停 / 选中样式与热力倍率;落点预览的读数与它同源", doc: "节点 / 线路解析之后的样式快照:节点的 size / symbol 取样式链(自身 > 规则 > 全局),线路的 width / color / opacity / zIndex 同源;不含悬停 / 选中样式、整体倍率与热力倍率,下标不存在时返回 null。合并节点与鹰眼图沿用主图尺寸时走这一份", examples: [{"group":"坐标、拾取与读数","code":"// getPointStyle(index):节点解析后的样式快照(样式链定下来的那一份)\nconst pointStyle = view.getPointStyle(0)\n","from":"header"}] },
115
+ { name: "getLineStyle", kind: "method", params: "index: number 线路在 lines 里的下标", returns: "GisViewLineStyleSnapshot | null:{ index, width, color, opacity, zIndex };width 取样式链(线路自身 > 规则 > lineStyle)定下来的原始线宽,不含 lineWidthScale / 缩放联动 / 热力倍率,color 是解析后的 RGBA(0 ~ 1)。局部更新与线路窄样式快路径之后读到的都是最新值;下标不存在时 null", doc: "节点 / 线路解析之后的样式快照:节点的 size / symbol 取样式链(自身 > 规则 > 全局),线路的 width / color / opacity / zIndex 同源;不含悬停 / 选中样式、整体倍率与热力倍率,下标不存在时返回 null。合并节点与鹰眼图沿用主图尺寸时走这一份", examples: [{"group":"坐标、拾取与读数","code":"// getLineStyle(index):线路解析后的样式快照(原始线宽 / RGBA / 透明度 / 层级)\nconst lineStyle = view.getLineStyle(0) // { index, width, color, opacity, zIndex } 或 null\n","from":"header"}] },
110
116
  { name: "getLine", kind: "method", params: "ref 线路定位(同上)", returns: "GisViewLineInfo | null", doc: "取一条线路:拾取载荷 + path / bounds / color / width(与 listLines() 的一项同一份口径),通道回线还带 channelId / groupId / channelIndex;找不到返回 null", examples: [{"group":"坐标、拾取与读数","code":"// getPoint(ref) / getLine(ref) / getChannel(ref):一个要素的几何读数\nconst pointInfo = view.getPoint('a')\nconst lineInfo = view.getLine('l1')\nconst channelInfo = view.getChannel('c1')\n","from":"header"}] },
111
117
  { name: "getChannel", kind: "method", params: "ref 通道定位:通道 id 或下标", returns: "GisViewChannelSnapshot | null", doc: "取一条通道:数据那一份(index / item / id)+ 成员表(lineIndexes / skeleton / offsets / paths)+ 分组上下文 context(几组 / 当前哪组 / 每组拐点数)、from / to / anchored / spacing;找不到返回 null", examples: [{"group":"坐标、拾取与读数","code":"// getPoint(ref) / getLine(ref) / getChannel(ref):一个要素的几何读数\nconst pointInfo = view.getPoint('a')\nconst lineInfo = view.getLine('l1')\nconst channelInfo = view.getChannel('c1')\n","from":"header"}] },
112
118
  { name: "listPoints", kind: "method", params: "options?: GisViewListOptions:indexes(只看这几个下标)、inView: boolean(只要当前视口里的)、withScreen: boolean(连屏幕坐标一起给)、visibleOnly: boolean(跳过隐藏的)、limit: number(最多几条)", returns: "GisViewListedPoint[]", doc: "按条件列节点快照 { index, item, x, y, radius, layerRank, screen? }:选项见 GisViewListOptions(indexes / inView / limit / withScreen / visibleOnly),可见判定与 isPointVisible 是同一份", examples: [{"group":"坐标、拾取与读数","code":"// listPoints(options?) / listLines(options?):按条件列一批\nconst listedPoints = view.listPoints({ limit: 5, withScreen: true })\nconst listedLines = view.listLines({ inView: true, limit: 200 })\n","from":"header"}] },
@@ -132,14 +138,14 @@ export const API_SECTIONS = [
132
138
  {
133
139
  title: "选中、复制与合并",
134
140
  items: [
135
- { name: "selectFeatures", kind: "method", params: "refs: GisViewSelectionRefs 要选中的东西:节点 / 线路 id、id 数组、{ type, index }、通道写法({ type: 'channel', channelId | channelIndex },可再给 groupIds / lineIds / lineIndexes 取子集)、分组写法({ type: 'group', channelId?, groupId },groupId 全局唯一时可省略通道)、拾取载荷 / 搜索命中,传 null / 空数组等于清空一份新的选中集。认不出来的条目逐条跳过;options?: GisViewSelectOptions 只有 mode:'replace' 缺省换一份、'add' 追加、'toggle' 在就取消 / 不在就选上、'remove' 去掉", returns: "this(选中真的变了才发 onSelect 与 onSelectChange,cause 为 api)", doc: "接口选中(换一份选中集;selectFeatures(null) / 空数组就是清空):refs 认节点 / 线路 id、id 数组、{ type, index }、通道写法({ type: 'channel', channelId } / { channelId, groupId } / { channelId, lineIds })、分组写法({ type: 'group', channelId?, groupId },全局唯一 groupId 可省略通道)以及拾取载荷与搜索命中;options.mode 给 'replace'(缺省,换一份)/ 'add' / 'toggle'(在就取消、不在就选上)/ 'remove',见「接口选中(selectFeatures)与读回选中」", examples: [{"group":"选中、复制与合并","code":"// selectFeatures(refs, options?):接口选中(与点击选中走同一份 state)\nview.selectFeatures('a') // 选中一个\nview.selectFeatures(['a', 'b']) // 一批\nview.selectFeatures('a', { mode: 'add' }) // mode:replace(缺省)/ add / toggle / remove\nview.selectFeatures([{ type: 'line', index: 0 }], { mode: 'toggle' })\nview.selectFeatures({ type: 'channel', channelId: 'c1' }) // 整条通道的全部回线\nview.selectFeatures({ type: 'group', channelId: 'c1', groupId: 'c1::2' })\nview.selectFeatures({ type: 'group', groupId: 'global-group' }) // groupId 全局唯一时可省略通道\nview.selectFeatures(null) // 清空(空数组同义)\n","from":"header"}] },
141
+ { name: "selectFeatures", kind: "method", params: "refs: GisViewSelectionRefs 要选中的东西:节点 / 线路 id、id 数组、{ type, index }、通道写法({ type: 'channel', channelId | channelIndex },可再给 groupIds / lineIds / lineIndexes 取子集)、分组写法({ type: 'group', channelId?, groupId },groupId 全局唯一时可省略通道)、拾取载荷 / 搜索命中,传 null / 空数组等于清空一份新的选中集。认不出来的条目逐条跳过;options?: GisViewSelectOptions 只有 mode:'replace' 缺省换一份、'add' 追加、'toggle' 在就取消 / 不在就选上、'remove' 去掉", returns: "this(选中真的变了才按 onSelect → onSelectChange 发,cause 为 api;载荷带通道上下文)", doc: "接口选中(换一份选中集;selectFeatures(null) / 空数组就是清空):refs 认节点 / 线路 id、id 数组、{ type, index }、通道写法({ type: 'channel', channelId } / { channelId, groupId } / { channelId, lineIds })、分组写法({ type: 'group', channelId?, groupId },全局唯一 groupId 可省略通道)以及拾取载荷与搜索命中;options.mode 给 'replace'(缺省,换一份)/ 'add' / 'toggle'(在就取消、不在就选上)/ 'remove'。会同步选中样式与读数;选中集实际变化时按 onSelect → onSelectChange 派发(cause: 'api'),载荷带同一份通道上下文(单通道摊平,多通道看 channelContexts),不发 onClick / onDblClick,见「接口选中(selectFeatures)与读回选中」", examples: [{"group":"选中、复制与合并","code":"// selectFeatures(refs, options?):接口选中(与点击选中走同一份 state;实际变化时按 onSelect → onSelectChange 发,载荷带通道上下文)\nview.selectFeatures('a') // 选中一个\nview.selectFeatures(['a', 'b']) // 一批\nview.selectFeatures('a', { mode: 'add' }) // mode:replace(缺省)/ add / toggle / remove\nview.selectFeatures([{ type: 'line', index: 0 }], { mode: 'toggle' })\nview.selectFeatures({ type: 'channel', channelId: 'c1' }) // 整条通道的全部回线\nview.selectFeatures({ type: 'group', channelId: 'c1', groupId: 'c1::2' })\nview.selectFeatures({ type: 'group', groupId: 'global-group' }) // groupId 全局唯一时可省略通道\nview.selectFeatures(null) // 清空(空数组同义)\n","from":"header"}] },
136
142
  { name: "selectWaypoint", kind: "method", params: "lineRef 线路 id、lines 下标或 { lineIndex, pathIndex } / { lineId, pathIndex } 对象;pathIndex?: number 必须是 getWaypoints() 给出的可编辑中间拐点下标(首尾端点不算,通常直接从读数里取);options?: GisViewSelectOptions 的 mode 决定所在线路怎么并进当前选中集(缺省 replace)。有效拐点会同时选中线路;线路不存在 / 下标非法 / 拐点不可见时只清拐点选中,不猜位置;传 null 也只清拐点,不改要素选中集", returns: "this", doc: "代码选中一个拐点:selectWaypoint('line-1', 2) / { lineIndex, pathIndex } / { lineId, pathIndex },并同时选中所在线路;有效下标从 getWaypoints() 取。传 null 只取消拐点选中、不改要素选中集,见「代码选中拐点(selectWaypoint)」", examples: [{"group":"选中、复制与合并","code":"// selectWaypoint(lineRef, pathIndex? / { lineIndex | lineId, pathIndex }, options?):选中拐点并带选线路\nview.selectWaypoint('l1', 2) // pathIndex 从 getWaypoints() 取\nview.selectWaypoint({ lineIndex: 0, pathIndex: 1 }, { mode: 'add' })\nview.selectWaypoint({ lineId: 'l1', pathIndex: 2 })\nview.selectWaypoint(null) // 只取消拐点选中,不改要素选中集\n","from":"header"}] },
137
- { name: "deselect", kind: "method", params: "refs?: GisViewSelectionRefs 要取消的东西(写法与 selectFeatures 一致:节点 / 通道 / 线路 / 混合一批);不传 / null 等于取消全部(= selectFeatures(null)),空数组是「没有目标」什么都不做", returns: "this", doc: "取消选中:写法与 selectFeatures 完全一样,把这些从当前选中集里去掉、其余选中原样留着;不传 / null 等于取消全部(也就是 selectFeatures(null)),空数组是「没有目标」什么都不做,见「取消选中(deselect / selectFeatures(null))」", examples: [{"group":"选中、复制与合并","code":"// deselect(refs?):取消选中(写法与 selectFeatures 一致;不传 / null 全清,空数组什么都不做)\nview.deselect('a')\nview.deselect(['a', 'b'])\nview.deselect()\n","from":"header"}] },
143
+ { name: "deselect", kind: "method", params: "refs?: GisViewSelectionRefs 要取消的东西(写法与 selectFeatures 一致:节点 / 通道 / 线路 / 混合一批);不传 / null 等于取消全部(= selectFeatures(null)),空数组是「没有目标」什么都不做", returns: "this(选中真的变了才按 onSelect → onSelectChange 发,cause 为 api;清空时 onSelect 载荷为 null)", doc: "取消选中:写法与 selectFeatures 完全一样,把这些从当前选中集里去掉、其余选中原样留着;不传 / null 等于取消全部(也就是 selectFeatures(null)),空数组是「没有目标」什么都不做。改变选中集时按 onSelect → onSelectChange 派发(cause: 'api';清空时 onSelect 载荷为 null),见「取消选中(deselect / selectFeatures(null))」", examples: [{"group":"选中、复制与合并","code":"// deselect(refs?):取消选中(写法与 selectFeatures 一致;不传 / null 全清,空数组什么都不做)\nview.deselect('a')\nview.deselect(['a', 'b'])\nview.deselect()\n","from":"header"}] },
138
144
  { name: "getSelections", kind: "method", params: "无", returns: "GisViewPickResult[]", doc: "读回选中结果 [{ type, index, item, … }](按选中顺序):要 id 列表 / 个数 / 包围盒看 getSelectionData()", examples: [{"group":"选中、复制与合并","code":"// getSelections() / getSelectionData() / isSelected(ref):读回选中\nconst picks = view.getSelections()\nconst selection = view.getSelectionData() // { ids, count, points, lines, channels, bounds, … }\nconst selected = view.isSelected('a')\n","from":"header"}] },
139
145
  { name: "isSelected", kind: "method", params: "ref: GisViewFeatureRef | GisViewFeatureKey | GisViewSelectionRefObject 要素定位(id / 下标 / { id } / { index } / { type, index } / { item })", returns: "boolean", doc: "这个要素在不在当前选中集里:isSelected('node-0')(id)/ isSelected({ type: 'line', index: 12 })(也可以只写下标,按 id 认不出来时当下标),返回 true / false。面板上高亮「当前这一行」时不必自己比对 getSelections();选中集变化时缓存一份键集合(见「接口选中」),十万级选中下这一问也是常数级", examples: [{"group":"选中、复制与合并","code":"// getSelections() / getSelectionData() / isSelected(ref):读回选中\nconst picks = view.getSelections()\nconst selection = view.getSelectionData() // { ids, count, points, lines, channels, bounds, … }\nconst selected = view.isSelected('a')\n","from":"header"}] },
140
146
  { name: "getSelectionData", kind: "method", params: "无", returns: "GisViewSelectionData:{ ids, count, pointCount, lineCount, channelCount, points, lines, channels, items, pointIndexes, lineIndexes, bounds }(bounds 是世界包围盒,空选中时 null)", doc: "选中集一次取全:读数(ids / count / pointCount / lineCount / channelCount)加要素本身(points / lines / channels(按通道去重,与 getData() 同一批对象)),另有 items(与 getSelections() 同一份)、pointIndexes / lineIndexes 与 bounds(世界包围盒,空选中时 null),见「接口选中(selectFeatures)与读回选中」", examples: [{"group":"选中、复制与合并","code":"// getSelections() / getSelectionData() / isSelected(ref):读回选中\nconst picks = view.getSelections()\nconst selection = view.getSelectionData() // { ids, count, points, lines, channels, bounds, … }\nconst selected = view.isSelected('a')\n","from":"header"}] },
141
147
  { name: "getCascadeHighlight", kind: "method", params: "无", returns: "GisViewCascadeHighlightInfo | null:当前点击级联高亮的来源、范围与节点 / 线路读数 { origin, scope, points, lines, pointIndexes, lineIndexes },不改变选中集", doc: "当前点击级联高亮的读数:{ origin, scope, points, lines, pointIndexes, lineIndexes };没有高亮或 cascadeHighlight 关着时返回 null。它只读、不改选中集,见「点击级联高亮」", examples: [{"group":"选中、复制与合并","code":"// getCascadeHighlight():读点击级联高亮(与选中集分开;没有高亮时为 null)\nconst cascade = view.getCascadeHighlight() // { origin, scope, points, lines, pointIndexes, lineIndexes }\nview.updateOptions({\n cascadeHighlight: {\n scope: 'points',\n triggerOnClick: true,\n triggerOnMultiSelect: false,\n triggerOnBoxSelect: true,\n clearOnEmptyClick: true\n }\n})\nview.on('cascadeHighlight', payload => console.log(payload?.pointIndexes, payload?.lineIndexes))\n","from":"header"}] },
142
- { name: "selectInBox", kind: "method", params: "box: GisViewRect 画布 CSS 像素矩形 { x, y, width, height }(与手动框选同一套坐标);options?: GisViewSelectInBoxOptions:selectionKinds 临时覆盖这一次保留哪几类('point' / 'line' / 'channel'、数组、'all' / true / false / null,不写沿用全局 selectionKinds),originalEvent 原样带进 onSelect;第二段也兼容旧的 Event 写法。空框不会选中任何要素", returns: "this(发 onSelect / onSelectChange,cause 为框选)", doc: "按画布 CSS 像素矩形框选,等价于 Shift + 拖动(跟随 shortcuts.boxSelect);options.selectionKinds 可只覆盖这一次的节点 / 线路 / 通道类型过滤", examples: [{"group":"选中、复制与合并","code":"// selectInBox(box, options?):用矩形框选(画布 CSS 像素,与手动框选同一套判定)\nview.selectInBox({ x: 40, y: 40, width: 320, height: 200 })\nview.selectInBox({ x: 40, y: 40, width: 320, height: 200 }, { selectionKinds: ['line', 'channel'] })\n","from":"header"}] },
148
+ { name: "selectInBox", kind: "method", params: "box: GisViewRect 画布 CSS 像素矩形 { x, y, width, height }(与手动框选同一套坐标);options?: GisViewSelectInBoxOptions:selectionKinds 临时覆盖这一次保留哪几类('point' / 'line' / 'channel'、数组、'all' / true / false / null,不写沿用全局 selectionKinds),originalEvent 原样带进 onSelect;第二段也兼容旧的 Event 写法。空框不会选中任何要素", returns: "this(每次框选都发 onSelect,空框为 null;集合变化时再发 onSelectChange,顺序 onSelect → onSelectChange,cause 为 boxSelect)", doc: "按画布 CSS 像素矩形框选,等价于 Shift + 拖动(跟随 shortcuts.boxSelect);options.selectionKinds 可只覆盖这一次的节点 / 线路 / 通道类型过滤。每次框选操作都发 onSelect(空框的载荷为 null),选中集实际变化时再发 onSelectChange,顺序 onSelect → onSelectChange(cause: 'boxSelect');通道回线的分组字段与鼠标框选同一份", examples: [{"group":"选中、复制与合并","code":"// selectInBox(box, options?):用矩形框选(画布 CSS 像素,与手动框选同一套判定)\nview.selectInBox({ x: 40, y: 40, width: 320, height: 200 })\nview.selectInBox({ x: 40, y: 40, width: 320, height: 200 }, { selectionKinds: ['line', 'channel'] })\n","from":"header"}] },
143
149
  { name: "beginCopyPoint", kind: "method", params: "options: GisViewCopyStartOptions:id / index / item 三选一给来源,都不给就进入「下一次点选节点」模式;coordinate / screen 是落点(缺省按 copy.offset);follow 让预览跟指针;autoCommit 覆盖 copy.autoCommit;commit: true 出预览即落地;via / originalEvent 原样带回。必须配置 copy(enabled 不为 false),否则返回 null。预览节点插在 points 末尾且不带 id,不会把绑定源 id 的线路 / 通道带过去", returns: "GisViewPointCopyPayload | null:phase: 'start' 的预览载荷;来源认不出来时返回 null", doc: "开始复制一个节点:给 id / index 直接出预览(commit: true 出预览即落地,follow: true 跟随鼠标,autoCommit: false 停在预览态),不给来源就是「先点按钮再选节点」模式,见「节点复制」", examples: [{"group":"选中、复制与合并","code":"// beginCopyPoint(options) / getPointCopy() / updateCopyPoint(patch) / commitPointCopy() / cancelPointCopy()\nview.updateOptions({ copy: { enabled: true, autoCommit: false } }) // API 复制入口要先开 copy 配置\nview.beginCopyPoint({ id: 'a', coordinate: [40, 30] }) // 出预览并钉在世界坐标 [40, 30]\nview.beginCopyPoint({ id: 'a', follow: true }) // 预览跟着指针走(autoCommit 缺省停住)\nview.beginCopyPoint() // 先点按钮再选节点(点中下一个节点当来源)\nview.updateCopyPoint({ name: '新节点' }) // 字段级改预览节点(落点仍由 coordinate / 指针决定)\nconst copyEnd = view.commitPointCopy() // 落地:返回 end 载荷;没有预览时为 null\nview.cancelPointCopy() // 取消:预览撤掉,数据与历史不动\nconst copyState = view.getPointCopy() // 当前预览 / 选点态(没有就是 null)\n","from":"header"}] },
144
150
  { name: "commitPointCopy", kind: "method", params: "无:把当前复制预览落地,先发 commit、落地后发 end;commit 阶段载荷里的 cancel() 可以拦住这次复制", returns: "GisViewPointCopyPayload | null:成功的 end 载荷;没有预览或已被取消时为 null", doc: "落地 / 取消预览中的复制(返回 end / cancel 载荷)", examples: [{"group":"选中、复制与合并","code":"// beginCopyPoint(options) / getPointCopy() / updateCopyPoint(patch) / commitPointCopy() / cancelPointCopy()\nview.updateOptions({ copy: { enabled: true, autoCommit: false } }) // API 复制入口要先开 copy 配置\nview.beginCopyPoint({ id: 'a', coordinate: [40, 30] }) // 出预览并钉在世界坐标 [40, 30]\nview.beginCopyPoint({ id: 'a', follow: true }) // 预览跟着指针走(autoCommit 缺省停住)\nview.beginCopyPoint() // 先点按钮再选节点(点中下一个节点当来源)\nview.updateCopyPoint({ name: '新节点' }) // 字段级改预览节点(落点仍由 coordinate / 指针决定)\nconst copyEnd = view.commitPointCopy() // 落地:返回 end 载荷;没有预览时为 null\nview.cancelPointCopy() // 取消:预览撤掉,数据与历史不动\nconst copyState = view.getPointCopy() // 当前预览 / 选点态(没有就是 null)\n","from":"header"}] },
145
151
  { name: "cancelPointCopy", kind: "method", params: "无:撤掉预览并补一条 cancel,数据与历史不动", returns: "GisViewPointCopyPayload | null:取消载荷;没有预览时为 null", doc: "落地 / 取消预览中的复制(返回 end / cancel 载荷)", examples: [{"group":"选中、复制与合并","code":"// beginCopyPoint(options) / getPointCopy() / updateCopyPoint(patch) / commitPointCopy() / cancelPointCopy()\nview.updateOptions({ copy: { enabled: true, autoCommit: false } }) // API 复制入口要先开 copy 配置\nview.beginCopyPoint({ id: 'a', coordinate: [40, 30] }) // 出预览并钉在世界坐标 [40, 30]\nview.beginCopyPoint({ id: 'a', follow: true }) // 预览跟着指针走(autoCommit 缺省停住)\nview.beginCopyPoint() // 先点按钮再选节点(点中下一个节点当来源)\nview.updateCopyPoint({ name: '新节点' }) // 字段级改预览节点(落点仍由 coordinate / 指针决定)\nconst copyEnd = view.commitPointCopy() // 落地:返回 end 载荷;没有预览时为 null\nview.cancelPointCopy() // 取消:预览撤掉,数据与历史不动\nconst copyState = view.getPointCopy() // 当前预览 / 选点态(没有就是 null)\n","from":"header"}] },
@@ -344,9 +350,11 @@ export const API_SECTIONS = [
344
350
  {
345
351
  title: "属性",
346
352
  items: [
347
- { name: "el", kind: "property", params: "HTMLElement", returns: "HTMLElement", doc: "组件插入的 .gis-view 根节点(destroy() 会移除它;容器本身不动)", examples: [{"group":"属性","code":"// el / canvas / gl:容器、画布与 WebGL2 上下文(gl 可能为 null —— 还没建好或走了别的路)\nconst host = view.el\nconst canvas = view.canvas\nconst gl = view.gl","from":"header"}] },
348
- { name: "canvas", kind: "property", params: "HTMLCanvasElement", returns: "HTMLCanvasElement", doc: "WebGL2 画布元素(导出 / 截图 / 自己叠加 DOM 时定位用它)", examples: [{"group":"坐标、拾取与读数","code":"//(pick / getContextAt / nearestPointsAt 这些吃画布像素的方法先用这一对转;自动处理 canvas rect 与宿主缩放)\nconst [sx, sy] = view.clientToScreen(event.clientX, event.clientY)\nconst [fixedX, fixedY] = view.screenToClient(320, 200) // 摆 position: fixed 的浮层 / 菜单\n","from":"header"},{"group":"属性","code":"// el / canvas / gl:容器、画布与 WebGL2 上下文(gl 可能为 null —— 还没建好或走了别的路)\nconst host = view.el\nconst canvas = view.canvas\nconst gl = view.gl","from":"header"}] },
349
- { name: "gl", kind: "property", params: "WebGL2RenderingContext | null", returns: "WebGL2RenderingContext | null", doc: "引擎拿到的 WebGL2 上下文,创建失败时 null(这时 onError 会报一条)", examples: [{"group":"属性","code":"// el / canvas / gl:容器、画布与 WebGL2 上下文(gl 可能为 null —— 还没建好或走了别的路)\nconst host = view.el\nconst canvas = view.canvas\nconst gl = view.gl","from":"header"}] },
353
+ { name: "el", kind: "property", params: "HTMLElement", returns: "HTMLElement", doc: "组件插入的 .gis-view 根节点(destroy() 会移除它;容器本身不动)", examples: [{"group":"属性","code":"// el / canvas / gl / cameraAnimating / dataVersion:容器、画布、WebGL2 上下文、相机是否在飞行、数据修订号\nconst host = view.el\nconst canvas = view.canvas\nconst gl = view.gl\nconst cameraAnimating = view.cameraAnimating\nconst dataVersion = view.dataVersion // 数据变更 +1;只改全局样式 / 配置不变","from":"header"}] },
354
+ { name: "canvas", kind: "property", params: "HTMLCanvasElement", returns: "HTMLCanvasElement", doc: "WebGL2 画布元素(导出 / 截图 / 自己叠加 DOM 时定位用它)", examples: [{"group":"坐标、拾取与读数","code":"//(pick / getContextAt / nearestPointsAt 这些吃画布像素的方法先用这一对转;自动处理 canvas rect 与宿主缩放)\nconst [sx, sy] = view.clientToScreen(event.clientX, event.clientY)\nconst [fixedX, fixedY] = view.screenToClient(320, 200) // 摆 position: fixed 的浮层 / 菜单\n","from":"header"},{"group":"属性","code":"// el / canvas / gl / cameraAnimating / dataVersion:容器、画布、WebGL2 上下文、相机是否在飞行、数据修订号\nconst host = view.el\nconst canvas = view.canvas\nconst gl = view.gl\nconst cameraAnimating = view.cameraAnimating\nconst dataVersion = view.dataVersion // 数据变更 +1;只改全局样式 / 配置不变","from":"header"}] },
355
+ { name: "gl", kind: "property", params: "WebGL2RenderingContext | null", returns: "WebGL2RenderingContext | null", doc: "引擎拿到的 WebGL2 上下文,创建失败时 null(这时 onError 会报一条)", examples: [{"group":"属性","code":"// el / canvas / gl / cameraAnimating / dataVersion:容器、画布、WebGL2 上下文、相机是否在飞行、数据修订号\nconst host = view.el\nconst canvas = view.canvas\nconst gl = view.gl\nconst cameraAnimating = view.cameraAnimating\nconst dataVersion = view.dataVersion // 数据变更 +1;只改全局样式 / 配置不变","from":"header"}] },
356
+ { name: "cameraAnimating", kind: "property", params: "boolean", returns: "boolean", doc: "相机是否正在飞行(只读)。按钮据此在「飞向 / 停止」之间切换,不触发重绘", examples: [{"group":"属性","code":"// el / canvas / gl / cameraAnimating / dataVersion:容器、画布、WebGL2 上下文、相机是否在飞行、数据修订号\nconst host = view.el\nconst canvas = view.canvas\nconst gl = view.gl\nconst cameraAnimating = view.cameraAnimating\nconst dataVersion = view.dataVersion // 数据变更 +1;只改全局样式 / 配置不变","from":"header"}] },
357
+ { name: "dataVersion", kind: "property", params: "number", returns: "number", doc: "数据修订号(只读):每次数据变更 +1,onDataChange 载荷里是同一个数;只改全局配置 / 样式不推。外部缓存、Worker 请求与增量计算据它判断是否重算", examples: [{"group":"属性","code":"// el / canvas / gl / cameraAnimating / dataVersion:容器、画布、WebGL2 上下文、相机是否在飞行、数据修订号\nconst host = view.el\nconst canvas = view.canvas\nconst gl = view.gl\nconst cameraAnimating = view.cameraAnimating\nconst dataVersion = view.dataVersion // 数据变更 +1;只改全局样式 / 配置不变","from":"header"}] },
350
358
  ]
351
359
  },
352
360
  ]
@@ -385,7 +393,7 @@ export const API_OPTIONS = [
385
393
  { name: "labelStates", type: "GisViewLabelState[]", defaultValue: "无", doc: "标签的运行时状态(getLabelStates() 的那一份:拖出来的偏移、改过的内容与样式、运行时加的标签、隐藏覆盖):创建时喂回来就回显,等价于创建之后调 setLabelStates(states);updateOptions({ labelStates }) 同样认。exportData({ format: 'input' }) 的结果里就叫 labelStates,存档 → 回灌直接接上" },
386
394
  { name: "graphics", type: "GisViewGraphic[]", defaultValue: "无", doc: "与节点无关的扩展图形:按世界坐标放 SVG 路径 / 图片 / Canvas,支持锚点、偏移、旋转、透明度、zIndex 与 scaleWithView;位于画布之上、标签之下,运行时用 addGraphic / addGraphics / updateGraphic / removeGraphic / clearGraphics 增删改,见「扩展图形(graphic)」" },
387
395
  { name: "tooltip", type: "GisViewTooltipOptions", defaultValue: "无", doc: "悬停提示:点 / 线 / 通道分别配置内容,见「悬浮提示」" },
388
- { name: "minimap", type: "GisViewMinimapOptions | null", defaultValue: "无", doc: "内置鹰眼图:位置、尺寸、节点 / 线路 / 视口框三份样式与交互都可配,extent 决定小图按数据范围还是「数据 ∪ 底图」铺(默认 auto,GeoJSON 底图不会缩成一个点),extentFollowCamera 默认 false、打开后才允许 auto 范围随相机收窄。zoomable 三态互斥:false 关闭、true 手动缩放小图自身、'follow' 让整体范围跟随主图(followZoomScale 是主图倍数的指数,跟随模式下 setMinimapZoom / zoomMinimapBy 不抢控制权);空白处拖动可平移小图或直接框选让主图显示过去(dragAction,box 不依赖缩放,手动模式下 Ctrl / ⌘ 才切换动作),每格倍率 zoomStep。通道还能只画一根;小图同样支持规则(pointRules / lineRules,缺省沿用主图规则的颜色、层级、最终节点尺寸与图标、样式链线宽,节点 / 线路尺寸按两边视口比例缩放且不受主图相机倍数影响),可以只画某几层(levels)、关掉分层叠画(layered)、按三组系数给节点与线路定尺寸(整体倍数 pointSizeScale / lineWidthScale 对自有与继承要素都生效;跟随小图倍数 pointZoomScale / lineZoomScale 在小图自身放大时也作用于继承要素;跟随主视图地图倍数 pointMapZoomScale / lineMapZoomScale 只作用于自有规则 / 样式),视口框与框选矩形的颜色 / 背景 / 边框 / 圆角也能配(boxStyle / selectStyle),extent 还能直接写死一份世界范围、容器可加 className / style、layers 按 placement 分数据前 / 数据后、段内按 order 拆出多层自绘内容(旧 draw 钩子仍兼容并排在最后)、onInteraction 在手势结束时给出类型 / 世界坐标 / 相机、maxPixelRatio 限制备份倍率上限,不配则不产生任何 DOM,见「鹰眼图」" },
396
+ { name: "minimap", type: "GisViewMinimapOptions | null", defaultValue: "无", doc: "内置鹰眼图:位置、尺寸、节点 / 线路 / 视口框三份样式与交互都可配,extent 决定小图按数据范围还是「数据 ∪ 底图」铺(默认 auto,GeoJSON 底图不会缩成一个点),extentFollowContent 默认 false,首次铺好后内容变化不再改小图范围;extentFollowCamera 默认 false、打开后才允许 auto 范围随相机收窄。zoomable 三态互斥:false 关闭、true 手动缩放小图自身、'follow' 让整体范围跟随主图(followZoomScale 是主图倍数的指数,跟随模式下 setMinimapZoom / zoomMinimapBy 不抢控制权);空白处拖动可平移小图或直接框选让主图显示过去(dragAction,box 不依赖缩放,手动模式下 Ctrl / ⌘ 才切换动作),每格倍率 zoomStep。通道还能只画一根;小图同样支持规则(pointRules / lineRules,缺省沿用主图规则的颜色、层级、最终节点尺寸与图标、样式链线宽,节点 / 线路尺寸按两边视口比例缩放且不受主图相机倍数影响),可以只画某几层(levels)、关掉分层叠画(layered)、按三组系数给节点与线路定尺寸(整体倍数 pointSizeScale / lineWidthScale 对自有与继承要素都生效;跟随小图倍数 pointZoomScale / lineZoomScale 在小图自身放大时也作用于继承要素;跟随主视图地图倍数 pointMapZoomScale / lineMapZoomScale 只作用于自有规则 / 样式),视口框与框选矩形的颜色 / 背景 / 边框 / 圆角也能配(boxStyle / selectStyle),extent 还能直接写死一份世界范围、容器可加 className / style、layers 按 placement 分数据前 / 数据后、段内按 order 拆出多层自绘内容(旧 draw 钩子仍兼容并排在最后)、onInteraction 在手势结束时给出类型 / 世界坐标 / 相机、maxPixelRatio 限制备份倍率上限,不配则不产生任何 DOM,见「鹰眼图」" },
389
397
  { name: "filter", type: "GisViewFilterInput | null", defaultValue: "无", doc: "初始筛选条件,等价于创建之后调用 setFilter;mode 缺省 'hide',写 'remove' 则删除不命中的要素,见「筛选」" },
390
398
  { name: "filterWorker", type: "boolean | GisViewFilterWorkerOptions", defaultValue: "{ threshold: 20000 }", doc: "筛选是否用 Web Worker 计算:默认点 + 线总数超过阈值才用,false 表示始终同步算" },
391
399
  { name: "searchWorker", type: "boolean | GisViewSearchWorkerOptions", defaultValue: "{ threshold: 20000 }", doc: "[异步搜索](14-视图与筛选.md#搜索与定位)是否用 Web Worker 计算:默认点 + 线总数超过阈值才用,false 表示 searchAsync 也同步算;同步的 search 一直在主线程算,不受这一项影响" },
@@ -445,7 +453,7 @@ export const API_OPTIONS = [
445
453
  { name: "pixelRatio", type: "number", defaultValue: "devicePixelRatio", doc: "手动指定设备像素比(夹在 0.5 ~ 3);宿主缩放(CSS zoom / transform: scale)的比例会乘在它上面,合计夹在 0.5 ~ 4" },
446
454
  { name: "maxCanvasPixels", type: "number", defaultValue: "4000000", doc: "画布总像素预算:cssWidth × cssHeight × 倍率² 超过它时按 sqrt(预算 / CSS 面积) 压渲染倍率(全屏与高分屏不因像素数翻十倍而掉帧);传 0 不设上限" },
447
455
  { name: "antialias", type: "boolean", defaultValue: "开(软件光栅自动关)", doc: "画布是否开 MSAA。同一份十万级线段实测:MSAA 4× 下单帧光栅化是关掉时的近 5 倍(见「画质」里的实测表),关掉能明显提速、代价是线条与圆形边缘有锯齿;当前实际状态见 getStats().antialias(软件光栅下引擎默认关,写 true 可强制打开)" },
448
- { name: "interactionThinLines", type: "boolean", defaultValue: "true", doc: "交互期细线快路径:可见线段全部细到 3px 以内且没有虚线时,拖动 / 缩放期间改用 gl.LINES 画(1 设备像素、无光晕与虚线),停手立刻回到四边形展开。位置不跳、悬停 / 选中不受影响;这一帧走没走快路径见 getStats().thinLines,实测见「画质」" },
456
+ { name: "interactionThinLines", type: "boolean", defaultValue: "true", doc: "交互期细线快路径:可见线段最大半宽 ≤ 0.6 设备像素(dpr = 1 时约 1.2px 线宽)且整批没有虚线时,拖动 / 缩放期间改用 gl.LINES 画(1 设备像素、无光晕与虚线),停手立刻回到四边形展开。2px 实线不会走这一档,但全实线四边形另有轻量片元。位置不跳、悬停 / 选中不受影响;这一帧走没走快路径见 getStats().thinLines,实测见「画质」" },
449
457
  { name: "interactionNoMsaa", type: "boolean", defaultValue: "true", doc: "非 MSAA 后备缓冲(交互 / 动画期):拖动 / 缩放 / 框选期间与动画在跑的那些帧,整帧先画进一张不带 MSAA 的后备贴图,帧末整幅贴回画布;停手(或动画停帧)之后那一帧照旧直接画进带 MSAA 的画布,所以静止画面逐像素不变(代价是这些活动帧的边缘暂时没有抗锯齿)。十万级实测交互期单帧 133 → 36.6 ms、拖动 p50 130 → 16 ~ 22 ms,动画帧 212 → 53 ms/帧(只开涟漪 187 → 48、只开线路动点 199 → 47);透明底 / 自定义底色的导出场景与没开 MSAA 的画布不走这条,这一帧走没走见 getStats().noMsaa,实测见「画质」" },
450
458
  { name: "renderer", type: "'auto' | 'webgl2' | 'webgpu'(也认布尔开关)", defaultValue: "'auto'", doc: "渲染后端开关:当前实装的绘制后端只有 WebGL2 —— 十万级下 frameMs 只有 0.05 ~ 0.2 ms,开销在填充率与 DOM 覆盖层上,换 WebGPU 省不到(第 18 章),所以 'auto' 只会在后台探一次 WebGPU 能力、绘制仍走 WebGL2,'webgl2' 连探测都不跑(无 GPU 的远程桌面里省一次 requestAdapter),'webgpu' 探完明确报一句「已回退 WebGL2、为什么」;true / false 等价于 'webgpu' / 'webgl2'。读数用 getRendererInfo(),见「渲染后端(WebGPU 开关)」" },
451
459
  { name: "ariaLabel", type: "string", defaultValue: "GIS 视图", doc: "画布的无障碍名称" },
@@ -57,7 +57,7 @@ export const DEMOS = [
57
57
  id: 'minimap',
58
58
  title: '鹰眼图配置',
59
59
  group: 'interact',
60
- blurb: '尺寸 / 位置 / 范围 / 规则 / 层级 / 小图缩放与备份倍率一页调完;另有两层自定义绘制演示 layers 的扩展方式。',
60
+ blurb: '尺寸 / 位置 / 范围 / 规则 / 层级 / 小图缩放与备份倍率一页调完;范围默认锁定、筛选不跳,顶部手柄可拖动整张鹰眼图并复位;另有两层自定义绘制演示 layers 的扩展方式。',
61
61
  doc: DOC_VIEW,
62
62
  component: () => import('./MinimapDemo.vue')
63
63
  },
@@ -582,7 +582,7 @@ export const useGisViewDemo = () => {
582
582
  }
583
583
  /**
584
584
  * onSelectChange:只在选中集真的变了时发(接口选中也算),cause 说明这次是怎么选上的。
585
- * 选中镜像挂在这一条上而不是 onSelect:引擎交互 / 接口 select() / 框选接口 / 数据变更清空
585
+ * 选中镜像挂在这一条上而不是 onSelect:引擎交互 / 接口 selectFeatures() / 框选接口 / 数据变更清空
586
586
  * 都会经过它,「以谁为主」的下拉才不会漏掉接口选中那一路。
587
587
  */
588
588
  const handleSelectChange = payload => {
@@ -2789,7 +2789,7 @@ export const useGisViewDemo = () => {
2789
2789
  }
2790
2790
  }
2791
2791
  const handleSelect = (payload) => {
2792
- // 选中镜像不在这里刷新:create.js 先发 onSelectChange 再发 onSelect,上面那条已经同步过了
2792
+ // 选中镜像不在这里刷新:create.js 先发 onSelect 再发 onSelectChange,上面那条已经同步过了
2793
2793
  if (!payload) {
2794
2794
  return
2795
2795
  }
@@ -5,9 +5,11 @@
5
5
  - **底图**:`minimap.js` 把节点与线条直接写进一块离屏像素缓冲(`ImageData`),按「层级 + 颜色 + 线宽 + 透明度」分桶后再画 —— 绘制次数随分桶数走,不随数据量走;底图只在数据或筛选结果变化时重画;节点像素结果按「数据引用 + 尺寸 + 范围 + 变换 + 样式」缓存,同一份数据前后两次重画直接复用(十万点重铺 20ms → 0.3ms),数据范围也按数据对象进 `WeakMap`。
6
6
  - **视口框**:单独一个 DOM 元素,相机变化只改 `transform`(不重画底图),所以拖动小图不掉帧。
7
7
  - **挂载与位置**:`minimap.container` 接受 `HTMLElement` 或选择器,把 `.gis-view__minimap` 根节点挂到外部宿主;换容器只 `append` 同一根节点,画布、缓存与事件监听都不重建,`destroy()` 从当前宿主摘掉。`offset` 是相对 `position` 角落的 `[x, y]` 位移(正数向右 / 向下),`updateOptions` 只改 `left` / `right` / `top` / `bottom`,因此整图拖动不需要重画小图;根节点通过 `getMinimapElement()` 暴露给外部手势。
8
+ - **整图拖动示例**:`docs/demos/MinimapDemo.vue` 在根节点顶部追加一个独立手柄,手柄的 `pointerdown` 直接 `stopPropagation`,窗口级 `pointermove` / `pointerup` 累计位移并写回 `minimap.offset`;小图内部的框选、平移与视口框拖动都不受影响。方向键按 2px(Shift 10px)微调,复位按钮把 `offset` 写回 `[0, 0]`。
8
9
  - **规则**:小图可以有自己的 `pointRules` / `lineRules`(颜色、半径 / 线宽、形状、层级),不配时沿用主图样式链:颜色 / 描边色 / 层级取规则,节点尺寸与符号取引擎最终解析结果(点自身 > 主图规则 > `pointStyle`),线路线宽取样式链的原始线宽(不含主图整体倍数 / 缩放联动 / 热力倍数)。`inheritSize` / `inheritWidth` 标记的节点 / 线路再乘一次「小图 CSS 视口宽高 ÷ 主图 CSS 视口宽高」的较小比例(`inheritedSizeFactor()`),这一份只跟两边视口尺寸有关,主图相机缩放不会改继承要素的大小;小图自身放大时仍可按 `pointZoomScale` / `lineZoomScale` 叠加,`pointSizeScale` / `lineWidthScale` 对自有与继承要素统一生效,`pointMapZoomScale` / `lineMapZoomScale` 只作用于小图自有规则 / 样式。小图自己的尺寸不受视口比例这一步影响。圆形按主图的面积对齐系数还原真实直径;图片 / SVG 图标与自定义图元异步解码后单独盖在点层上,解码前先用小图基准尺寸的圆 / 方回落,完成后只失效点层并补一帧。
9
10
  - **交互**:视口框上按下 = 拖框;空白处长按(`longPressDelay`)= 视角跟随;`zoomable` 三态互斥:`true` 手动缩放小图自身(滚轮按 `zoomStep`、锚点落光标处,Ctrl / ⌘ + 滚轮留给主图),`'follow'` 让整体范围跟随主图(`followZoomScale` 是主图倍数的指数,`setMinimapZoom` / `zoomMinimapBy` 不抢控制权),`false` 只缩放主图。`dragAction` 决定空白处拖动是平移小图还是框选,`box` 不依赖 `zoomable`;只有手动模式按住 Ctrl / ⌘ 才自动切到另一个动作。拖框与视角跟随带来的相机变化是「程序化」的:标签层不进交互态(`hideOnInteract` 不会把主视图的标签收起来),拖动结束补一次真实排版。每次真正有位移的手势结束时通过 `onInteraction` 发一条 `{ type, originalEvent, local, world, zoom, extent, camera, viewport }`(`type` 为 `box-drag` / `select` / `pan` / `zoom` / `wheel-main` / `long-press`);无位移的点击不发,`pointermove` 不逐帧回调,长按在触发跟随时发。
10
11
  - **范围**:`extent` 决定小图按哪段世界范围铺 —— `measure(data)` 量出可见数据的范围,`backgroundBoundsOf(background)` 量出底图范围(GeoJSON 用画出来的 `bounds`,图片用四角 quad 的包围盒),`auto`(默认)取两者的并集,`data` 只看数据(旧行为),`background` 只看底图。数据为空时 `measure()` 返回 `null`、不会当成「范围是正负一」;修掉的问题见 15.12。`extentFollowCamera` 默认 `false`,`auto` 始终用并集,外围内容消失不会让包围盒与视口框跳动;打开后才有例外:相机正落在其中一份范围里、而那一份在并集里连 6 个像素都占不到时(数据还没映射到底图坐标系就是这样)只画正在看的那一份;`followFocusedExtent()` 在相机挪进 / 挪出这份小范围时补一次重画 —— 判断只用上一次重画缓存的两份范围,每帧几次比较。
12
+ - **内容范围锁定**:`extentFollowContent` 默认 `false`。`resolveExtent()` 首次拿到当前模式需要的数据 / 底图范围后把它放进 `pinnedExtent`,之后筛选、删除、隐藏或新增内容只重画可见要素,不再改 `bounds` / `scale` / 中心,视口框也就不会跟着外围内容消失而漂移;打开开关后每次重画都用当前可见内容重算。显式修改 `extent`(模式或固定范围对象按值比较)会清掉锁定,立刻按新配置铺;切换 `extentFollowContent` 会重新锁定当前内容。`extentFollowCamera` 的相机取舍也使用锁定后的两份范围。
11
13
  - **通道**:`channelCircuits: false` 时每个分组按当前可见成员逐顶点平均成一根代表线,双回线因此落在两根正中间,三回线落在中间那条;只剩一条可见成员时就是它自己,顶点数对不上时退回第一条可见成员(底图分辨率本来就在一像素上下,多回线只会糊成一片)。
12
14
  - **背景**:默认 `background: 'inherit'`,直接拿引擎解析好的那份背景在 Canvas2D 上重画:底色铺满,图片按 quad 拆成两组三角形做仿射 + 裁剪(任意四边形都不会画歪),GeoJSON 面用奇偶规则填充(洞自动镂空)、纯线几何的区域再描一遍、点要素画成固定 1.2px 半径的小圆;底图的线宽(取配置值、最低 0.4px 保可见)与区域文字字号(固定档,夹在 5 ~ 12px)都是小图上的固定像素,不乘「小图视口 ÷ 主图视口」的比例 —— 缩放主视图时底图不会单独放大 / 缩小(本轮删掉了 `mapRatio`,见 15.17)。也可以给 `minimap.background` 单独配一份(结构与主视图的 `background` 完全一致,只是换 Canvas2D 画)。`backgroundStyle` 显式设置时由容器 CSS 接管底色:继承或独立的底图仍画图片与 GeoJSON,但画布底色改成透明,不再用主图的不透明底色盖住容器;不写时仍按主图底色铺满。区域文字先量「区域在小图上的屏幕矩形 ≥ 文字尺寸 × 0.85」再画,放不下就整条跳过(缩略图上那些小块本来也读不出来);能画的按区域从大到小排,和已经放下的标签重叠就跳过 —— 小图上几十个区域各画一行字只会叠成一团。
13
15
  - **扩展层**:`layers` 接收一组 `{ id?, order?, placement?, enabled?, draw }`。`placement: 'below-data'` 在内置线路 / 节点之前执行,适合网格、分区底色;`'above-data'`(缺省)在数据之后执行,适合指北针、业务标记。两段内部各按 `order` 从小到大,相同或缺省 `order` 按数组顺序。`setOptions` 时把过滤、排序与分段结果缓存成两条数组,绘制帧只遍历当前段,不再每帧 `map` / `filter` / `sort`。每个回调独立 `save` / `restore`,一层改过的画布状态不会漏进下一层;`enabled: false` 可临时关掉一层,旧的单层 `draw` 保持兼容并固定最后执行,回调里的 `stage` 为 `'overlay'`。这样网格、指北针、业务标记可以各自维护,不必把所有内容堆进一个钩子。
@@ -266,9 +266,20 @@
266
266
 
267
267
  ② 也在本轮落了地(见 15.55):写实例的那几个口子(`writeCircle` / `writeLine` / `writeSpriteInstance`、追加点的那一段循环、`patchPointSlots` 的贴图分支)就地记光晕账本(`heat > 0 && opacity > 0`,与解析那一趟同一份口径),补丁路径不再置 `heatGlowDirty`,`refreshHeatGlow` 只剩「补丁改过解析结果」(换热力图配置)这一条兜底;顺手把样式快路径漏写回的 `point.heat` 补上(线路那侧的 `refreshLine` 一直有)。
268
268
 
269
- ### 16.34 线路侧的「只换线色 / 线宽」窄路(候选,本轮量过)
269
+ ### 16.34 线路侧的「只换线色 / 线宽」窄路(已完成,本轮)
270
270
 
271
- 点侧有 15.53 / 15.54 两条链的窄路,线侧还没有:`updateOptions({ lineStyle })` 里只动线色 / 线宽时,线路那一半照旧整份重解析(线侧解析结果的字段比点少,颜色与线宽都在同一条 `refreshLine` 里)。本轮给基准页加了 `lineStyleColor` / `lineStyleWidth` 两个 op 量过一遍:20 万条线只换线色 62 / 56 / 45.5 / 43 / 44.1 / 41.3 ms,profile 里 `applyResolvedStyles` 只占 18.1%,其余是 `patchLineSlots`(16.0%)+ `refreshLine`(13.4%)+ `patchLineSlotsInner`(12.8%)与实例缓冲上传 —— 重解析这一半只占三成上下,窄路能省的上限就是这十几毫秒,而线侧本来就要整批重写槽位(线色进实例数据、线宽还要过细线账本)。等「大部分线自带颜色 / 线宽」这种数据真的出现、或者槽位那一半也变窄了再做。
271
+ 点侧在 15.53 / 15.54 已有尺寸链 / 配色链两条窄路,线侧原先仍是整份重解析:`updateOptions({ lineStyle })` 只动线色 / 线宽时,规则、热力、虚线、阴影、动点与方向箭头都要重新归一化,`resolveLineStyleInto` 还会再走一遍完整对象写入。上一轮量过 20 万线,重解析约占三成、槽位补丁与实例上传占大头,因此把它留成候选;这一轮先把最常走的基准样式补丁收掉,同时让窄路与整份解析共用同一份取值链,避免两套实现以后漂开。
272
+
273
+ **改法**:从 `line.js` 抽出 `resolveLinePaintInto(target, line, style, defaults, state, heat, rule)`,只负责「基准宽 / 实际半宽 / 颜色 / 透明度」;`resolveLineStyleInto` 继续在它之上补 `zIndex` / 拐角 / 虚线 / 阴影 / 效果 / 方向。`engine.js` 新增 `NARROW_LINE_STYLE_BUCKETS` 与 `lineStyleNarrowChange()`:只在补丁确实动到 `width` / `color` / `opacity`、且没有线路规则 / 热力图、`effects` 与 `lineDirection` 引用未变时命中;`applyLineNarrowFast()` 逐条重算 paint,结果与上一轮相同就不进补丁列表,最后仍走 `patchLineSlots(changedLines, true)`。逐线自己写死的字段和状态样式继续优先;线宽变化会让交互期细线的账本作废,但不再重跑线型、阴影、动点等结构输入。`getLineStyle(index)` 读到的也是这条链落下来的最新快照。
274
+
275
+ 1000 点 + 50000 线连续 5 次换色 / 换宽(无头 Chrome + SwiftShader,`scripts/bench-scale.mjs`):
276
+
277
+ | 操作 | 改前(稳态) | 改后(稳态) |
278
+ | --- | --- | --- |
279
+ | `lineStyle.color` 换色 | 10.1 ~ 11.7 ms | **6.1 ~ 7.4 ms** |
280
+ | `lineStyle.width` 换宽 | 9.2 ~ 10.4 ms | **5.8 ~ 6.4 ms** |
281
+
282
+ 改后 profile 顶部是 `patchLineSlotsInner` 30.8% / `patchLineSlots` 17.3% / `applyLineNarrowFast` 13.0%,`resolveLinePaintInto` 3.8%;原先整份路里占大头的 `applyResolvedStyles` 与 `refreshLine` 不再出现。首轮仍有 18.9 / 17.3 ms(槽位表与交互期细线账本首次重建),稳态收益约 40%。正确性侧:`selftest-perf.html` 核对窄路后的 `getLineStyle`、逐线自身线宽 / 颜色优先、虚线不被覆盖,以及存在线路规则时回退整份解析;`selftest-demos.html` 核对 demo 的换色 / 加宽与 `dataVersion` 不递增。
272
283
 
273
284
  ### 16.35 通道回线拖动剩下的账(已完成,见 15.61)
274
285
 
@@ -1,6 +1,6 @@
1
1
  # 16-03 优化与取舍(下)
2
2
 
3
- > 16.57 ~ 16.73:坐标、通道、筛选与文档口径的取舍。
3
+ > 16.57 ~ 16.75:坐标、通道、筛选、相机快路径与文档口径的取舍。
4
4
 
5
5
  ## 16 还能怎么优化(续)
6
6
 
@@ -140,6 +140,16 @@
140
140
  - **为什么 `exportViewportData` 的结果要展开补元数据**:自定义适配器只负责业务结构,不知道视口范围;`viewRect` / `margin` 是这次查询的上下文,必须由 `exportViewportData` 统一补上,且不能覆盖适配器同名字段之外的内容。对象结果才展开、非对象原样返回,避免适配器返回 `null` 时凭空造一个对象。
141
141
  - **为什么严格契约放在 demos 自测里**:这两个接口是「演示即文档」的一部分,后端同学拿到的第一个可执行样例就是假后端。把 400 / 409 断言放在 `selftest-demos.html`,每次演示页回归都会重新验证契约,不需要另起一套测试设施。
142
142
 
143
+ ### 16.75 程序化相机运动为什么也走交互快路径、聚合关闭为什么不能再整份重排(本轮已做 / 留档)
144
+
145
+ - **根因不在绘制,而在「谁算运动帧」**:`zoomBy` / `setCamera` / `centerOn` / `flyToCamera` 都只改相机再 `notifyCameraChange()`,并不经过 `beginInteraction()`。轻量画法的判定只看 `interacting` 或动画,于是同一份十万级场景,用户拖一下能走细线与非 MSAA 后备缓冲,外部控件调一次缩放却仍按全质量画。这一轮把「程序化相机运动」记成一个 160 ms 的短窗口,`offscreenFrame` 与 `thinLinesDrawn` 共用同一套判定;每次真实位置变化重新计时,停稳后补一张全质量帧,并刷新通道宽度、恢复符号预烤。
146
+ - **为什么要比较相机位置,而不是每次 `setCamera` 都续期**:业务侧常把 `setCamera` 放在定时器或响应式同步里,即使视角没变也会反复调用。若无条件续期,画面会永久停在轻量档,线宽、圆头与 MSAA 都回不来。现在的 `cameraMotionLast` 只认真实变化的中心 / 比例;同一位置重复调用既不进入快路径,也不会重置停稳计时。
147
+ - **为什么关闭聚合也要单独处理**:旧代码只要 `camera.scale !== clusterScale` 就标 `clusterDirty`,而 `flushPointClusterLayout()` 无条件调用 `layoutScene()`。聚合没开时,这一趟没有任何成员可算,却会把十万级点的实例缓冲整份重写上传 —— 缩放看起来慢,慢在这里而不是 WebGL 绘制。现在只在聚合启用且当前档可能聚合、或上一帧确实有聚合圆 / 隐藏点时才标脏;关闭聚合且没有旧画面时只清标记。跨出 `maxZoom`、改配置、数据 / 选中 / 拖动变化仍立即重排,避免为了省一次计算让状态延迟生效。
148
+ - **为什么相机引起的聚合变化可以晚一帧**:聚合半径是屏幕像素,运动过程中每帧都重算成员只会把主线程占满,而圆本身的成员变化在快速缩放里很难看清。于是只把「纯相机缩放导致的成员变化」推迟到停稳后,拖动期间沿用上一组成员是同一口径;平移不改比例,一次都不标脏。
149
+ - **为什么停稳条件不能只看「画过轻量帧」**:聚合成员被推迟时,如果场景没有线、又显式 `interactionNoMsaa: false`,这一轮可能没有任何轻量几何,但 `clusterDirty` 仍在。停稳计时器现在同时检查 `cameraMotionFastDrawn` 与「被相机推迟的聚合脏」,后者也补一帧,否则聚合会停在旧缩放档。`selftest-perf.html` 用 12000 点、无边、关后备缓冲的场景专门钉住这一条。
150
+ - **为什么不改公开接口**:快路径只改「什么时候画成哪一档」,相机、聚合、选项与返回结构都没变,不使用这条路的调用方行为不变;`getStats().thinLines` / `noMsaa` 仍是最近一帧的真实读数,静态画面照旧回到全质量。
151
+ - **验证**:`node --check`、`npx tsc --noEmit`、`node docs/steps/gen-api-index.mjs --check`、`npm run check:docs`(264 个实例成员 / 96 个 options / 250 个数据字段 / 43 个事件 / 17 个功能开关)、四页自测 `full 94` / `demos 365` / `docs 134` / `perf 181` 共 **774** 条通过 / 0 失败、`npm run build` 与 `git diff --check` 全部通过。10 万点 / 5 万线实测连续缩放 p50 **67.9 → 22.5 ms**、p95 **76.2 → 25.1 ms**、avg **60.77 → 20.57 ms**;平移 p50 17.1 → 17.2 ms(帧节拍波动),`refresh()` 56.2 → 53.7 ms。profile 显示剩余成本主要是 GPU 光栅化 / 帧节拍,`render` 自身约 0.1%,因此本轮收益应表述为「省掉整份重排与上传、运动期省掉 MSAA 细线光栅化」,不是「单帧渲染降到 1 ms」。
152
+
143
153
  ---
144
154
 
145
155
  [← 16-02 优化与取舍(中)](16-02-%E4%BC%98%E5%8C%96%E4%B8%8E%E5%8F%96%E8%88%8D-%E4%B8%AD.md) · [实现原理目录](../GisView实现原理.md) · [17 调试与扩展建议 →](17-%E8%B0%83%E8%AF%95%E4%B8%8E%E6%89%A9%E5%B1%95%E5%BB%BA%E8%AE%AE.md)