gis-view-webgl2 0.1.3 → 0.1.4

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 (34) hide show
  1. package/README.md +12 -9
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +112 -17
  4. package/dist/index.js +1 -1
  5. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +34 -0
  6. package/docs/GisView.md +3 -3
  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 +30 -0
  8. package/docs/api/index.html +22 -8
  9. package/docs/demos/ApiCoverageDemo.vue +1 -1
  10. package/docs/demos/ExtensionDemo.vue +69 -12
  11. package/docs/demos/SearchFilterDemo.vue +13 -11
  12. package/docs/demos/api-index.js +13 -9
  13. package/docs/demos/demos.js +1 -1
  14. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +1 -0
  15. package/docs/demos/gis-view-demo/panels/OpsPanels.vue +2 -2
  16. package/docs/demos/gis-view-demo/useGisViewDemo.js +9 -9
  17. package/docs/index.html +41 -4
  18. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +18 -3
  19. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +7 -6
  20. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +24 -3
  21. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +90 -34
  22. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +32 -3
  23. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +7 -4
  24. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +9 -6
  25. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +39 -7
  26. 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 +3 -3
  27. 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 +6 -3
  28. package/docs/steps/README.md +7 -7
  29. package/docs/steps/selftest-demos.html +26 -9
  30. package/docs/steps/selftest-docs.html +33 -4
  31. package/docs/steps/selftest-features.html +91 -10
  32. package/docs/steps/selftest-scale.html +98 -0
  33. package/docs/steps/selftest-select.html +90 -0
  34. package/package.json +1 -1
@@ -7,8 +7,8 @@
7
7
  * 源码或文档改过之后重跑一遍;`node docs/steps/check-doc-coverage.mjs` 会对账。
8
8
  */
9
9
 
10
- /** { members: 253, methods: 250, properties: 3, options: 95, sections: 16 } */
11
- export const API_SUMMARY = {"members":253,"methods":250,"properties":3,"options":95,"sections":16}
10
+ /** { members: 256, methods: 253, properties: 3, options: 96, sections: 16 } */
11
+ export const API_SUMMARY = {"members":256,"methods":253,"properties":3,"options":96,"sections":16}
12
12
 
13
13
  /** 实例成员,按 docs/GisView.md「参数速查」的分组排 */
14
14
  export const API_SECTIONS = [
@@ -50,9 +50,9 @@ export const API_SECTIONS = [
50
50
  { name: "zoomBy", kind: "method", params: "factor: number 缩放倍数(> 1 放大、< 1 缩小);origin?: [number, number] 缩放中心(画布 CSS 像素,缺省画布中心)", returns: "this", doc: "相对缩放,origin 为画布 CSS 像素锚点(缺省视口中心)", examples: [{"group":"相机与视图","code":"// zoomBy(factor, origin?):按倍数缩放;origin 是画布 CSS 像素的缩放中心\nview.zoomBy(1.5)\nview.zoomBy(0.5, [320, 200])\n","from":"header"}] },
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
- { 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)。四种显式类型共用同一套包围盒适配,没有额外放大倍率;maxZoom 只给单点 / 单线这类零面积目标补最小范围。options?: GisViewFocusOptions: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 认 scale、center: true(只居中、比例一点不动)、padding、maxZoom、select(顺带选中这批,mode 决定怎么并进当前选中集)", examples: [{"group":"相机与视图","code":"// focusOn(target, options?):对准一个或多个目标(id / 显式类型 / 坐标 / 矩形 / 搜索命中 / 一批)\nview.focusOn('a')\nview.focusOn(['a', 'b'], { padding: 60, maxZoom: 8, 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: "search", kind: "method", params: "input: string | GisViewSearchOptions 关键字,或 { keyword, scope, fields, limit } 这类选项(scope 可选 'points' / 'lines' / 'channels',也认它们的数组)—— 只返回结果、不动视角,要定位就自己调 focusOn(result.hits, …)", returns: "GisViewSearchResult", doc: "按关键字搜索节点 / 线路 / 通道并返回命中(不动视角:要不要聚焦由调用方自己调 focusOn),见「搜索与定位」", examples: [{"group":"相机与视图","code":"// search(input) / searchAsync(input):搜索(只返回结果、不动视角)\nconst hits = view.search('甲站')\nconst scoped = view.search({ keyword: '甲', scope: ['points', 'channels'], limit: 10 })\nview.focusOn(hits.hits, { maxZoom: 8 }) // 命中集直接丢给 focusOn\nconst asyncHits = await view.searchAsync('甲站') // 大表走 Worker;stale 说明这一份不作数了\n","from":"header"}] },
55
- { name: "searchAsync", kind: "method", params: "同上", 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'], limit: 10 })\nview.focusOn(hits.hits, { maxZoom: 8 }) // 命中集直接丢给 focusOn\nconst asyncHits = await view.searchAsync('甲站') // 大表走 Worker;stale 说明这一份不作数了\n","from":"header"}] },
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: "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
+ { 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
56
  { 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
57
  { 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"}] },
58
58
  { 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"}] },
@@ -298,9 +298,9 @@ export const API_SECTIONS = [
298
298
  {
299
299
  title: "筛选",
300
300
  items: [
301
- { name: "setFilter", kind: "method", params: "filter: GisViewFilterInput | null 筛选条件:完整格式 { points, lines, cascade, worker };只筛节点时可以简写成节点条件本身;传 null 等于清空", returns: "this", doc: "设置或清空筛选条件(传 null 清空):完整格式 { points, lines, cascade, worker },只筛节点时可以简写成节点条件本身;见「筛选」", examples: [{"group":"筛选","code":"// setFilter(filter):设筛选条件(只筛节点可以写简写);传 null 等于清空\nview.setFilter({ vn_kv: { gte: 525 } })\nview.setFilter({ points: { station_type: 'coal' }, lines: { vn_kv: { gte: 525 } }, cascade: 'binding' })\nview.setFilter(null)\n","from":"header"}] },
302
- { name: "getFilter", kind: "method", params: "无", returns: "GisViewFilter | null", doc: "当前条件与最近一次结果:隐藏的点 / 线下标、可见数量、是否用了 Worker、耗时", examples: [{"group":"筛选","code":"// getFilter() / getFilterResult():当前条件(副本)/ 最近一次结果\nview.setFilter(null)\nconst filter = view.getFilter()\nconst filterResult = view.getFilterResult() // { hiddenPoints, hiddenLines, visiblePoints, visibleLines, worker, durationMs }","from":"header"}] },
303
- { name: "getFilterResult", kind: "method", params: "无", returns: "GisViewFilterResult | null", doc: "当前条件与最近一次结果:隐藏的点 / 线下标、可见数量、是否用了 Worker、耗时", examples: [{"group":"筛选","code":"// getFilter() / getFilterResult():当前条件(副本)/ 最近一次结果\nview.setFilter(null)\nconst filter = view.getFilter()\nconst filterResult = view.getFilterResult() // { hiddenPoints, hiddenLines, visiblePoints, visibleLines, worker, durationMs }","from":"header"}] },
301
+ { name: "setFilter", kind: "method", params: "filter: GisViewFilterInput | null 筛选条件:完整格式 { points, lines, cascade, mode, worker };只筛节点时可以简写成节点条件本身;传 null 等于清空。mode: 'hide'(缺省)只隐藏、不删数据、不清空选中集;mode: 'remove' 删除筛选结果,只发一条 filterRemove 与最终一条 onFilter", returns: "this", doc: "设置或清空筛选条件(传 null 清空):完整格式 { points, lines, cascade, mode, worker },只筛节点时可以简写成节点条件本身。mode: 'hide'(缺省)只隐藏、不删数据、也不清空选中集;mode: 'remove' 把筛选结果从数据里删掉,只发一条 filterRemove 数据变更与最终一条 onFilter,见「筛选」", examples: [{"group":"筛选","code":"// setFilter(filter):设筛选条件(只筛节点可以写简写);传 null 等于清空;默认只隐藏\nview.setFilter({ vn_kv: { gte: 525 } })\nview.setFilter({ points: { station_type: 'coal' }, lines: { vn_kv: { gte: 525 } }, cascade: 'binding' })\nview.setFilter(null)\n","from":"header"}] },
302
+ { name: "getFilter", kind: "method", params: "无", returns: "GisViewFilter | null", doc: "当前条件与最近一次结果:隐藏的点 / 线下标、可见数量、是否用了 Worker、耗时", examples: [{"group":"筛选","code":"// getFilter() / getFilterResult():当前条件(副本)/ 最近一次结果\nview.setFilter(null)\nconst filter = view.getFilter()\nconst filterResult = view.getFilterResult() // hide 模式是隐藏下标;remove 完成后 hidden 为空","from":"header"}] },
303
+ { name: "getFilterResult", kind: "method", params: "无", returns: "GisViewFilterResult | null", doc: "当前条件与最近一次结果:隐藏的点 / 线下标、可见数量、是否用了 Worker、耗时", examples: [{"group":"筛选","code":"// getFilter() / getFilterResult():当前条件(副本)/ 最近一次结果\nview.setFilter(null)\nconst filter = view.getFilter()\nconst filterResult = view.getFilterResult() // hide 模式是隐藏下标;remove 完成后 hidden 为空","from":"header"}] },
304
304
  ]
305
305
  },
306
306
  {
@@ -310,6 +310,9 @@ export const API_SECTIONS = [
310
310
  { name: "once", kind: "method", params: "与 on 同一套事件名与载荷;只触发一次,回调跑完自动摘掉。返回的取消函数可提前撤销;即使提前取消,重复调用也安全", returns: "() => void 取消订阅", doc: "订阅事件(与 options 里的回调同一条,可以挂多条):返回取消订阅的函数;'*' 订阅全部事件,回调拿到 (载荷, 事件名),见「事件订阅」", examples: [{"group":"事件、开关与历史","code":"// on(name, handler) / once(name, handler) / off(name, handler?):订阅('*' 订阅全部)\nconst offData = view.on('dataChange', payload => sync(payload))\nview.on('*', (payload, name) => log(name, payload))\nview.once('dataRendered', payload => { loading = false })\nview.off('dataChange') // 摘掉这个事件的全部回调\nview.off('dataChange', sync) // 只摘掉这一个(返回摘掉几条)\n","from":"header"}] },
311
311
  { name: "off", kind: "method", params: "name 事件名(也认 '*');handler? 只摘掉这一个回调,不传 = 摘掉这个事件的全部订阅", returns: "number 摘掉几条(没摘到返回 0)", doc: "取消订阅:不传 handler 就摘掉这个事件的全部订阅,返回摘掉几条", examples: [{"group":"事件、开关与历史","code":"// on(name, handler) / once(name, handler) / off(name, handler?):订阅('*' 订阅全部)\nconst offData = view.on('dataChange', payload => sync(payload))\nview.on('*', (payload, name) => log(name, payload))\nview.once('dataRendered', payload => { loading = false })\nview.off('dataChange') // 摘掉这个事件的全部回调\nview.off('dataChange', sync) // 只摘掉这一个(返回摘掉几条)\n","from":"header"}] },
312
312
  { name: "use", kind: "method", params: "plugin: GisViewPlugin((view, context) => hook,与 options.plugins 的写法一致):立即执行一次,返回卸载函数 —— 调它跑插件自己的 destroy(重复调用只清一次,实例销毁之后也安全)", returns: "() => void 卸载", doc: "运行时挂一个插件(写法与 options.plugins 里的一项一致):调用即执行,返回幂等的卸载函数(跑插件自己的 destroy),见「插件」", examples: [{"group":"事件、开关与历史","code":"// use(plugin):运行时挂插件(立即执行一次,返回卸载函数)\nconst dispose = view.use((view, context) => ({ destroy() { /* 插件自己的清理;context.apiVersion 当前为 1 */ } }))\ndispose()\n","from":"header"}] },
313
+ { name: "registerCommand", kind: "method", params: "name: string 命令名(会 trim,同名覆盖);handler: GisViewCommandHandler<Payload, Value> 同步处理函数 (payload, context) => Value,context 是冻结的 { view, name }。名字为空 / handler 不是函数只报 warning、不注册;视图销毁后注册不生效", returns: "() => void 幂等注销;同名已被后来者覆盖时不会误删新命令", doc: "注册一个实例命令:handler(payload, context) 同步执行,context 是冻结的 { view, name };同名覆盖,返回幂等的注销函数,旧卸载函数不会误删后来注册的同名命令,见「实例命令」", examples: [{"group":"事件、开关与历史","code":"// registerCommand(name, handler) / executeCommand(name, payload?) / listCommands():实例级业务命令\nconst disposeCommand = view.registerCommand('focusHighestLoad', (payload = {}, context) => {\n const threshold = Number(payload.threshold ?? 0.8)\n const target = view.getData().points\n .filter(point => Number(point.max_loading_rate ?? 0) >= threshold)\n .sort((left, right) => Number(right.max_loading_rate ?? 0) - Number(left.max_loading_rate ?? 0))[0]\n if (!target) return { focused: false, threshold }\n context.view.focusOn(target.id, { select: true, zoom: true, maxZoom: 12 })\n return { focused: true, id: target.id, command: context.name }\n})\nconst commandResult = view.executeCommand('focusHighestLoad', { threshold: 0.85 })\nconst commandNames = view.listCommands() // ['focusHighestLoad']\ndisposeCommand() // 幂等卸载;同名覆盖时不会误删新命令\n","from":"header"}] },
314
+ { name: "executeCommand", kind: "method", params: "name: string 命令名;payload?: unknown 原样交给 handler", returns: "GisViewCommandResult<Value>:{ ok: true, name, value } / { ok: false, name, reason }(未注册 / 名字非法 / handler 抛错)", doc: "执行实例命令:成功给 { ok: true, name, value };名字非法 / 未注册 / handler 抛错给 { ok: false, name, reason },见「实例命令」", examples: [{"group":"事件、开关与历史","code":"// registerCommand(name, handler) / executeCommand(name, payload?) / listCommands():实例级业务命令\nconst disposeCommand = view.registerCommand('focusHighestLoad', (payload = {}, context) => {\n const threshold = Number(payload.threshold ?? 0.8)\n const target = view.getData().points\n .filter(point => Number(point.max_loading_rate ?? 0) >= threshold)\n .sort((left, right) => Number(right.max_loading_rate ?? 0) - Number(left.max_loading_rate ?? 0))[0]\n if (!target) return { focused: false, threshold }\n context.view.focusOn(target.id, { select: true, zoom: true, maxZoom: 12 })\n return { focused: true, id: target.id, command: context.name }\n})\nconst commandResult = view.executeCommand('focusHighestLoad', { threshold: 0.85 })\nconst commandNames = view.listCommands() // ['focusHighestLoad']\ndisposeCommand() // 幂等卸载;同名覆盖时不会误删新命令\n","from":"header"}] },
315
+ { name: "listCommands", kind: "method", params: "无", returns: "string[] 当前注册的命令名(按注册顺序;同名覆盖不改变位置)", doc: "当前注册着哪些实例命令(按注册顺序;同名覆盖不改变原来的位置)", examples: [{"group":"事件、开关与历史","code":"// registerCommand(name, handler) / executeCommand(name, payload?) / listCommands():实例级业务命令\nconst disposeCommand = view.registerCommand('focusHighestLoad', (payload = {}, context) => {\n const threshold = Number(payload.threshold ?? 0.8)\n const target = view.getData().points\n .filter(point => Number(point.max_loading_rate ?? 0) >= threshold)\n .sort((left, right) => Number(right.max_loading_rate ?? 0) - Number(left.max_loading_rate ?? 0))[0]\n if (!target) return { focused: false, threshold }\n context.view.focusOn(target.id, { select: true, zoom: true, maxZoom: 12 })\n return { focused: true, id: target.id, command: context.name }\n})\nconst commandResult = view.executeCommand('focusHighestLoad', { threshold: 0.85 })\nconst commandNames = view.listCommands() // ['focusHighestLoad']\ndisposeCommand() // 幂等卸载;同名覆盖时不会误删新命令\n","from":"header"}] },
313
316
  { name: "getFeatures", kind: "method", params: "无", returns: "GisViewFeatureState", doc: "读当前生效的功能开关(跟随详细配置的也解析成 true / false)/ 换一份(预设名 / 开关表 / true / false),换完立刻生效并发一条 onFeatureChange", examples: [{"group":"事件、开关与历史","code":"// getFeatures() / setFeatures(next):功能开关的读 / 写(预设名 / 开关表 / 布尔值)\nconst features = view.getFeatures()\nview.setFeatures('view') // 'edit' / 'view' / 'static'\nview.setFeatures({ drag: false }) // 单个开关\nview.setFeatures(true) // 全开\n","from":"header"}] },
314
317
  { name: "setFeatures", kind: "method", params: "next: GisViewFeaturesInput 预设名('edit' / 'view' / 'static')、开关表或布尔值", returns: "this", doc: "读当前生效的功能开关(跟随详细配置的也解析成 true / false)/ 换一份(预设名 / 开关表 / true / false),换完立刻生效并发一条 onFeatureChange", examples: [{"group":"事件、开关与历史","code":"// getFeatures() / setFeatures(next):功能开关的读 / 写(预设名 / 开关表 / 布尔值)\nconst features = view.getFeatures()\nview.setFeatures('view') // 'edit' / 'view' / 'static'\nview.setFeatures({ drag: false }) // 单个开关\nview.setFeatures(true) // 全开\n","from":"header"}] },
315
318
  { name: "undo", kind: "method", params: "无:从撤销 / 重做栈回放一步;会同步数据、发 history 与对应的 dataChange,并更新相机以外的运行时读数", returns: "GisViewHistoryStepResult | null:{ type, label, canUndo, canRedo };历史没开或栈空时为 null", doc: "撤销 / 重做一步:返回 { type, label, canUndo, canRedo },历史没开或没得撤销 / 重做时返回 null", examples: [{"group":"事件、开关与历史","code":"// undo() / redo() / canUndo() / canRedo():撤销 / 重做(历史的开关在 options.history)\nview.undo()\nview.redo()\nconst canGoBack = view.canUndo()\nconst canGoForward = view.canRedo()\n","from":"header"}] },
@@ -382,7 +385,7 @@ export const API_OPTIONS = [
382
385
  { name: "graphics", type: "GisViewGraphic[]", defaultValue: "无", doc: "与节点无关的扩展图形:按世界坐标放 SVG 路径 / 图片 / Canvas,支持锚点、偏移、旋转、透明度、zIndex 与 scaleWithView;位于画布之上、标签之下,运行时用 addGraphic / addGraphics / updateGraphic / removeGraphic / clearGraphics 增删改,见「扩展图形(graphic)」" },
383
386
  { name: "tooltip", type: "GisViewTooltipOptions", defaultValue: "无", doc: "悬停提示:点 / 线 / 通道分别配置内容,见「悬浮提示」" },
384
387
  { name: "minimap", type: "GisViewMinimapOptions | null", defaultValue: "无", doc: "内置鹰眼图:位置、尺寸、节点 / 线路 / 视口框三份样式与交互都可配,extent 决定小图按数据范围还是「数据 ∪ 底图」铺(默认 auto,GeoJSON 底图不会缩成一个点),zoomable 打开后小图自身可整体放大缩小(空白处拖动可以平移小图或直接框选让主图显示过去,每格倍率 zoomStep),通道还能只画一根;小图同样支持规则(pointRules / lineRules,缺省沿用主图规则的颜色与层级),可以只画某几层(levels)、关掉分层叠画(layered)、按三组系数给节点与线路定尺寸(整体倍数 pointSizeScale / lineWidthScale、跟随小图倍数 pointZoomScale / lineZoomScale(只在小图自身倍数 ≠ 1 时起作用)、跟随主视图地图倍数 pointMapZoomScale / lineMapZoomScale),视口框与框选矩形的颜色 / 背景 / 边框 / 圆角也能配(boxStyle / selectStyle),extent 还能直接写死一份世界范围、容器可加 className / style、layers 按 placement 分数据前 / 数据后、段内按 order 拆出多层自绘内容(旧 draw 钩子仍兼容并排在最后)、onInteraction 在手势结束时给出类型 / 世界坐标 / 相机、maxPixelRatio 限制备份倍率上限,不配则不产生任何 DOM,见「鹰眼图」" },
385
- { name: "filter", type: "GisViewFilterInput | null", defaultValue: "无", doc: "初始筛选条件,等价于创建之后调用 setFilter,见「筛选」" },
388
+ { name: "filter", type: "GisViewFilterInput | null", defaultValue: "无", doc: "初始筛选条件,等价于创建之后调用 setFilter;mode 缺省 'hide',写 'remove' 则删除不命中的要素,见「筛选」" },
386
389
  { name: "filterWorker", type: "boolean | GisViewFilterWorkerOptions", defaultValue: "{ threshold: 20000 }", doc: "筛选是否用 Web Worker 计算:默认点 + 线总数超过阈值才用,false 表示始终同步算" },
387
390
  { name: "searchWorker", type: "boolean | GisViewSearchWorkerOptions", defaultValue: "{ threshold: 20000 }", doc: "[异步搜索](14-视图与筛选.md#搜索与定位)是否用 Web Worker 计算:默认点 + 线总数超过阈值才用,false 表示 searchAsync 也同步算;同步的 search 一直在主线程算,不受这一项影响" },
388
391
  { name: "pointHoverStyle", type: "GisViewPointStyle", defaultValue: "无", doc: "点的悬停 / 选中样式;没配就不改变外观(悬停仍然触发回调与提示、选中仍然进选中集合),要反馈就自己配一份" },
@@ -402,6 +405,7 @@ export const API_OPTIONS = [
402
405
  { name: "fitSpacing", type: "boolean | GisViewFitSpacingOptions", defaultValue: "关", doc: "适应视图的节点间距兜底:true 用 { minDistance: 24, maxZoom: 0 },对象可改最小中心距(CSS 像素)与相对普通适应比例的上限(0 表示不限);只在首次默认适应与 fitView() 生效,远处孤立点允许落到视口外,见「适应视图的节点间距(fitSpacing)」" },
403
406
  { name: "boxSelect", type: "boolean", defaultValue: "false", doc: "是否允许按住框选快捷键(shortcuts.boxSelect,默认 Shift)拖动鼠标框选要素;缺省关,写 boxSelect: true 或 features: { boxSelect: true } 才开。框选默认只收节点,selectionKinds 可把线路 / 通道也纳入" },
404
407
  { name: "multiSelect", type: "boolean", defaultValue: "false", doc: "多选开关:打开后不用按修饰键,直接单击节点 / 线路就取反累加选中(与按住 shortcuts.multiSelect 点击同一套行为、同一个 multiSelectStyle;取反那一下不经过 onClick,onSelectChange.action.type 是 toggleSelect);缺省关,关着时多选走框选(boxSelect)与按住多选快捷键(默认 Ctrl / ⌘)。运行时 updateOptions({ multiSelect }) 切换只重画一帧、不重排场景" },
408
+ { name: "selectTrigger", type: "'click' | 'dblClick'", defaultValue: "'click'", doc: "选中触发方式:click 第一次单击就改选中集;dblClick 第一下只派发 onClick,等双击判定命中的第二下才改选中集,并同时派发 onClick 与 onDblClick。Ctrl / ⌘ 多选与 multiSelect 在双击模式下同样等第二下才取反。运行时走 updateOptions({ selectTrigger }),只重画一帧、不重排场景" },
405
409
  { name: "selectionKinds", type: "GisViewSelectionKindsInput", defaultValue: "无(框选只收节点;Ctrl 多选收节点 / 线路 / 通道)", doc: "框选与 Ctrl / 多选点击的类型过滤:'point' 节点、'line' 通道外的独立线路、'channel' 通道回线;可写数组任意组合,'all' / true 全开,false / null 全关。通道回线只归 channel 管,通道外的线路只归 line 管;通道回线仍按 channelSelectMode 摊开。不写时保留旧口径;普通单选与 select / selectFeatures 不受它限制。运行时走 updateOptions({ selectionKinds }),见「框选」与「选择类型过滤」" },
406
410
  { name: "selectionBoxStyle", type: "GisViewSelectionBoxStyle", defaultValue: "蓝底 0.16 透明 + 1px 蓝框", doc: "框选矩形的填充与边框:fill、fillOpacity、stroke、strokeWidth" },
407
411
  { name: "selectionOnTop", type: "boolean", defaultValue: "true", doc: "选中的节点 / 拖动中的整组 / 待确认合并的目标节点,以及选中线路所在的整条通道,是否画在所有要素之上(与 zIndex 无关),见「选中 / 拖动的层级」" },
@@ -161,7 +161,7 @@ export const DEMOS = [
161
161
  id: 'extension',
162
162
  title: '扩展能力',
163
163
  group: 'data',
164
- blurb: '四个扩展点同屏:view.use 运行时插件、自定义图元、自定义条件运算符与 defineExportFormat 业务导出。',
164
+ blurb: '六个扩展点同屏:view.use 运行时插件、自定义图元、自定义条件运算符、defineExportFormat 业务导出、WebGL 自定义层与实例命令。',
165
165
  doc: DOC_EXTEND,
166
166
  component: () => import('./ExtensionDemo.vue')
167
167
  },
@@ -22,6 +22,7 @@ const {
22
22
  <p>这一轮补上三块:① 通道可以分组了——默认一条通道一组,「拆分分组」按每组几条回线把它拆开(第一组沿用当前拐点、新分组的线路不带拐点),「合并分组」把其余分组并进第一组(被并入的线路跟随第一组的拐点,第一组没有拐点就一条都不带),加点 / 拖点只作用于同一分组,分组声明写回 channels[].groups;② 鹰眼图也支持规则——「规则沿用主图」只搬颜色与层级(主图的像素尺寸不会搬过来),「按电压单独配色」给小图自己配一份(半径缩到 1 ~ 2.6px、高电压用方块、zIndex 沿用电压分档),「分层」控制分层叠画、层级下拉只画某几层,「放大节点 / 放大线路」让节点与线路跟着小图自身的倍数放大(滚轮或 - / + 都算);③ 定向更新——updatePoint / updateLine / updateChannel 改一个节点、线路或通道就重渲染相关的视图,只重解析受影响的那一小片,其余要素与缓存一个字节都不动,鹰眼图与标签覆盖层跟着重画。</p>
23
23
  <p>热力图这一轮把节点那一层补上了:合成数据的节点与线路都按一张「热区场」随机生成 max_loading_rate(在数据范围里撒 4 ~ 6 个热区,节点取自己坐标处的场值、线路取两端中点处的场值 —— 画出来是几团往外化开的色斑,而不是均匀噪点),作用对象因此可以切「节点 + 线路 + 通道」/「只按节点上色」/「线路 + 通道回线」/「只按通道自己的值」四档;面板上的「重新随机一份热力数据」能在任意数据集上现生成一份(换种子、按当前节点范围重建场,只改字段值,坐标与拓扑一动不动),真实数据 new.json 的节点本来没有这一列、线路那一列又恒为 1.5,点一下就能立刻看到热力。作用对象带上节点时图标不再跟着换:图标符号写了 tint(逐实例着色),填充按白色烘焙、由热力色逐实例染上,描边写 currentStrokeColor 不参与染色 —— 形状与描边还是那张图标,只是填充跟着值走;贴图缓存键里因此不再有逐点的热力色,900 节点时绘制调用仍然在 36 上下(以前一个节点一张贴图时会涨到约 1500,10 万级直接把主线程烤住),想看「完全不参与热力图」的效果就把节点从作用对象里去掉 —— 图标本身不会变成内置圆 / 方块。</p>
24
24
  <p>这一轮补上六件事:① <b>节点复制</b>——按住 Alt(面板上可以改成别的键)点一个节点,复制品立刻压在源节点上跟着鼠标走,松开左键(不再看 Alt 什么时候松)或再点一下落地;也可以点面板上的「先点按钮再选节点」再点一个节点(复制品落在它旁边),或者选中节点按 Ctrl+C、把鼠标移到目标位置按 Ctrl+V。复制全程走 <b>onPointCopy</b>:start / move / commit / end / cancel(复制到剪贴板另发一条 clipboard),落地前的 commit 阶段可以 payload.cancel() 撤掉、payload.update() / setItem() 改数据,id 与整份数据由 copy.createId / copy.createItem 定;落地之后的 end 阶段还能 payload.revert() 整份回滚(面板上「复制后模拟接口失败」那个勾就是它)。② <b>节点合并</b>——把一个节点拖到另一个节点上(碰撞按两个节点画在屏幕上的半径之和 + merge.distance 判,整组拖动不参与),<b>组件不弹任何对话框</b>:pending 停在原地等使用方裁决,面板上的「合并确认」三档分别是「停住(由这个自写弹层来确认)」/ false 直接合并 / 函数返回 Promise(先调接口再决定,resolve(false) 取消);确认会先推进到 confirm 阶段并把合并结果给你(可以改,也可以在这一步因为「接口失败」整份撤掉),确认之后来源节点的数据并进目标(缺省 { ...来源, ...目标 },可用 merge.createItem 整份接管)、<b>尺寸取双方最大、图标优先目标</b>、绑在它上面的线路改绑、合并出来的自环与零长线删掉;两端因此撞成同一条的通道按 merge.channelGroups 处理(各自保留分组 / 合成一个分组,自环的整条删),读数里会带上改端点的通道数与并掉的通道数。取消或接口失败则把拖动的节点与跟随它的线路还原回原位,并补发一条拖拽结束。③ <b>所有快捷键都能改</b>——options.shortcuts 按动作配(框选 / 多选 / 加点 / 复制 / 复制到剪贴板 / 粘贴 / 取消 / 删除 / 适应视图),写法是纯修饰键(ctrl / meta / shift / alt / mod)、组合键(ctrl+c)、单键(escape / 0)或数组,写 false 表示不绑定;面板「视图 · 交互」里那四个下拉就是改框选 / 多选 / 加点 / 复制的修饰键——比如把框选改成 Shift,之后 Shift + 拖动才出选框,按住 Shift 时连光标变体也跟着走。④ <b>选中抬层</b>——selectionOnTop(缺省开)把选中的节点、拖动中的整组、待确认合并的目标节点与选中线路所在的整条通道画在所有要素之上:选中了就一定看得见,与 zIndex 无关;关掉就完全按 zIndex 与数据顺序画。⑤ <b>删除(Delete)</b>——选中拐点 / 线路 / 节点之后按 Delete(或面板上的「删除选中」)先出一份删除载荷:要删什么、连带什么、能按哪些范围删。<b>组件不弹界面</b>:pending 停在原地等 <b>confirm(scope)</b> / cancel(),Esc 也能取消;范围三档是「只删选中的线路」/「删除整个分组」/「删除整条通道」(后两档只在删的是通道回线、且这条通道分过组时才有),选中的拐点与节点一律一起删 —— <b>删节点会连挂在它身上的线路、以及拿它当端点的整条通道一起删</b>,载荷里的 lineCount / channelCount 就是这份连带读数;确认之后整段合成一条历史,撤销一步全回来;开关是 features.remove,快捷键照样走 shortcuts.remove(可以单独配成 shift+delete 之类)。⑥ <b>通道选中模式</b>——channelSelectMode 决定点中一条通道回线时选中什么:line(缺省,只选这一条)/ group(连它所在分组一起)/ channel(整条通道一起);在 group / channel 下 Ctrl + 点击是<b>整批取反</b>,通道以外的线路永远只选它自己。它只影响「下一次点选怎么摊开」,切换走 updateOptions,场景一个字节都不用重排。面板「视图 · 复制合并」里那两项就是它们。</p>
25
+ <p>定位口径这一轮统一了:<code>focusOn(target)</code> 缺省只把中心移到目标上,比例一点不动;要按目标范围适配必须显式写 <code>{ zoom: true }</code>,可再配 <code>padding</code> / <code>maxZoom</code>。节点、线路、通道与分组走同一条规则,搜索面板与画布菜单里需要适配的按钮都已经写成显式开启。</p>
25
26
  </div>
26
27
  <span class="demo-badge">WebGL2 · 无内置 GUI</span>
27
28
  </div>
@@ -109,7 +109,7 @@ const {
109
109
  <button type="button" @click="probeApi('listPoints')">listPoints({ limit: 5 })</button>
110
110
  <button type="button" @click="probeApi('listLines')">listLines({ inView: true })</button>
111
111
  <button type="button" @click="probeApi('bounds')">getElementBounds(选中)</button>
112
- <button type="button" @click="probeApi('locate')">focusOn(选中, { maxZoom: 8 })</button>
112
+ <button type="button" @click="probeApi('locate')">focusOn(选中, { zoom: true, maxZoom: 8 })</button>
113
113
  <button type="button" @click="probeApi('contextAt')">getContextAt(光标处)</button>
114
114
  <button type="button" @click="probeApi('viewState')">getViewState()</button>
115
115
  </div>
@@ -118,7 +118,7 @@ const {
118
118
  <pre v-if="apiReadout" class="demo-groups">{{ apiReadout }}</pre>
119
119
  </div>
120
120
  <div class="demo-controls">
121
- <span class="demo-control-hint">整份实例 API({{ API_SUMMARY.members }} 个成员:{{ API_SUMMARY.methods }} 个方法 + {{ API_SUMMARY.properties }} 个属性;另有 {{ API_SUMMARY.options }} 个 options 键)按文档的 12 个分组列全 —— 清单由 <code>node docs/steps/gen-api-index.mjs</code> 从 <code>src/gis-view/create.js</code> 与 <code>docs/GisView.md</code> 的两张表生成,<code>check-doc-coverage.mjs</code> 会对账,因此搜索框里的每一条都能在文档里查到逐个方法的参数与返回。搜不到想要的名字,多半是它还没写进文档。</span>
121
+ <span class="demo-control-hint">整份实例 API({{ API_SUMMARY.members }} 个成员:{{ API_SUMMARY.methods }} 个方法 + {{ API_SUMMARY.properties }} 个属性;另有 {{ API_SUMMARY.options }} 个 options 键)按文档的 {{ API_SUMMARY.sections }} 个分组列全 —— 清单由 <code>node docs/steps/gen-api-index.mjs</code> 从 <code>src/gis-view/create.js</code> 与 <code>docs/GisView.md</code> 的两张表生成,<code>check-doc-coverage.mjs</code> 会对账,因此搜索框里的每一条都能在文档里查到逐个方法的参数与返回。搜不到想要的名字,多半是它还没写进文档。</span>
122
122
  </div>
123
123
  <div class="demo-controls demo-api-search">
124
124
  <input v-model="apiQuery" type="search" placeholder="搜成员名 / 参数 / 返回 / 说明(例如 focusOn、通道、Promise、历史)" />
@@ -1274,11 +1274,11 @@ export const useGisViewDemo = () => {
1274
1274
  hint = `getElementBounds(选中 ${selected.length} 个):世界包围盒;一个都没对上返回 null(先在画布上选几个)`
1275
1275
  }
1276
1276
  else if (kind === 'locate') {
1277
- // 一批目标(数组)取整体包围盒:这几个点的中心就是屏幕中心,比例按这一批的范围适配
1277
+ // 一批目标(数组)取整体包围盒:zoom: true 才按这一批的范围适配,缺省只居中
1278
1278
  const selection = view.getSelections()
1279
- view.focusOn(selection, { maxZoom: 8, padding: 40 })
1279
+ view.focusOn(selection, { zoom: true, maxZoom: 8, padding: 40 })
1280
1280
  value = { focused: selection.length }
1281
- hint = `focusOn(getSelections(), { maxZoom: 8, padding: 40 }):把这一批(${selection.length} 个)整体居中并适配;只给一个节点时也会放大到 8 倍`
1281
+ hint = `focusOn(getSelections(), { zoom: true, maxZoom: 8, padding: 40 }):把这一批(${selection.length} 个)整体居中并适配;只给一个节点时也会放大到 8 倍`
1282
1282
  }
1283
1283
  else if (kind === 'contextAt') {
1284
1284
  const pointer = view.getPointer()
@@ -1429,7 +1429,7 @@ export const useGisViewDemo = () => {
1429
1429
  const count = payload.selection?.count ?? 0
1430
1430
  return [
1431
1431
  { id: 'addStation', label: `新增场站(addPoint)· 落点 (${payload.world[0].toFixed(1)}, ${payload.world[1].toFixed(1)})`, enabled: !payload.hit, reason: '这一下命中了要素:在空白处右键才能新增场站' },
1432
- { id: 'locate', label: `定位到选中(focusOn)${count ? ` · ${count} 个` : ''}`, enabled: count > 0, reason: '先在画布上选一个 / 一批要素' },
1432
+ { id: 'locate', label: `定位并适配选中(focusOn)${count ? ` · ${count} 个` : ''}`, enabled: count > 0, reason: '先在画布上选一个 / 一批要素' },
1433
1433
  { id: 'focus', label: '聚焦到命中的那一个(focusOn)', enabled: Boolean(payload.hit), reason: '这一下点在空白处:没有命中要素' },
1434
1434
  { id: 'fit', label: '适应视图(fitView)', enabled: true, reason: '' },
1435
1435
  { id: 'inspect', label: '读一下选中集(getSelectionData)', enabled: count > 0, reason: '选中集是空的' },
@@ -1450,8 +1450,8 @@ export const useGisViewDemo = () => {
1450
1450
  }
1451
1451
  else if (row.id === 'locate') {
1452
1452
  const selection = view.getSelections()
1453
- view.focusOn(selection, { maxZoom: 8, padding: 40 })
1454
- lastEvent.value = `画布菜单:focusOn(选中 ${selection.length} 个, { maxZoom: 8, padding: 40 })${selection.length ? '(视角已经挪到这一批的整体中心,单个节点也会放大到 8 倍)' : '(没选中:什么都没做)'}`
1453
+ view.focusOn(selection, { zoom: true, maxZoom: 8, padding: 40 })
1454
+ lastEvent.value = `画布菜单:focusOn(选中 ${selection.length} 个, { zoom: true, maxZoom: 8, padding: 40 })${selection.length ? '(视角挪到这一批的整体中心并按范围适配,单个节点也会放大到 8 倍)' : '(没选中:什么都没做)'}`
1455
1455
  }
1456
1456
  else if (row.id === 'focus') {
1457
1457
  view.focusOn({ type: payload.type, index: payload.index })
@@ -2566,11 +2566,11 @@ export const useGisViewDemo = () => {
2566
2566
  const kind = first.kind === 'point' ? '节点' : first.kind === 'line' ? '线路' : '通道'
2567
2567
  const member = first.memberIndex === undefined ? '' : `第 ${first.memberIndex + 1} 回`
2568
2568
  const value = first.value.length > 24 ? `${first.value.slice(0, 24)}…` : first.value
2569
- // 命中的一批直接喂给 focusOn:数组取整体包围盒,这几个命中的中心就是屏幕中心;
2569
+ // 命中的一批直接喂给 focusOn:数组取整体包围盒,zoom: true 才按这批命中适配;
2570
2570
  // select: true 顺带把它们选上(不想动视角就取消勾选,组件这边不会自己动)
2571
2571
  const focused = searchFocus.value
2572
2572
  if (focused) {
2573
- view.focusOn(result.hits, { select: true, padding: 60 })
2573
+ view.focusOn(result.hits, { select: true, zoom: true, padding: 60 })
2574
2574
  }
2575
2575
  searchReadout.value = `${focused ? '已聚焦并选中' : '只返回结果(视角没动)'} ${kind}(${first.field} = ${value}${member ? ` · ${member}` : ''})· 命中 ${result.total}:节点 ${result.points.length} / 线路 ${result.lines.length} / 通道 ${result.channels.length}${result.truncated ? ' · 已截断' : ''}`
2576
2576
  }
@@ -4980,7 +4980,7 @@ export const useGisViewDemo = () => {
4980
4980
  return
4981
4981
  }
4982
4982
  // 聚焦与选中一步做完:select: true 就把这一条同时变成选中状态(一批目标时同样管用)
4983
- view.focusOn(id, { padding: 220, select: true })
4983
+ view.focusOn(id, { zoom: true, padding: 220, select: true })
4984
4984
  syncSelectionFromView()
4985
4985
  lastEvent.value = `已定位到直流线路「${id}」:focusOn 一次把定位与选中做完,中点(position 0.5)挂着 PATH_DC_LINE 装饰,颜色跟线路的电压分档走`
4986
4986
  }
package/docs/index.html CHANGED
@@ -1323,7 +1323,7 @@
1323
1323
  { file: 'manual/14-视图与筛选.md', title: '手册 14 · 视图与筛选', blurb: '筛选 / 搜索 / 视图与背景', parent: 'GisView.md' },
1324
1324
  { file: 'manual/15-导出与集成.md', title: '手册 15 · 导出与集成', blurb: '导出 / 插件 / 自建界面', parent: 'GisView.md' },
1325
1325
  { file: 'manual/16-实例方法总表.md', title: '手册 16 · 实例方法总表', blurb: '实例方法主表', parent: 'GisView.md' },
1326
- { file: 'manual/17-参数速查.md', title: '手册 17 · 参数速查', blurb: '248 个成员逐个列全', parent: 'GisView.md' },
1326
+ { file: 'manual/17-参数速查.md', title: '手册 17 · 参数速查', blurb: '256 个成员逐个列全', parent: 'GisView.md' },
1327
1327
  { file: 'manual/18-逐方法示例.md', title: '手册 18 · 逐方法示例', blurb: '每个成员一段示例', parent: 'GisView.md' },
1328
1328
  { file: 'manual/19-读数与拖拽预览.md', title: '手册 19 · 读数与拖拽预览', blurb: '读数 / 定位 / 落点预览', parent: 'GisView.md' },
1329
1329
  { file: 'manual/20-性能与大数据.md', title: '手册 20 · 性能与大数据', blurb: '大数据开关与实测', parent: 'GisView.md' },
@@ -1399,7 +1399,7 @@
1399
1399
  { file: '../src/gis-view/README.md', title: 'src/gis-view/README.md', blurb: '源码导读与逐轮实现记录' }
1400
1400
  ]
1401
1401
  const PAGES = [
1402
- { href: 'api/index.html', title: 'API 速查(方法 + 示例)', blurb: '258 个实例成员 / 88 个 options,逐个带示例与复制' },
1402
+ { href: 'api/index.html', title: 'API 速查(方法 + 示例)', blurb: '256 个实例成员 / 96 个 options,逐个带示例与复制' },
1403
1403
  { href: 'playground/index.html', title: '调试演示页(粘代码就跑)', blurb: '左侧编辑、右侧真实实例 + GeoJSON 底图:内置示例与「这一份文档」的示例同在一个下拉,粘代码直接跑' },
1404
1404
  { href: 'guide/sample-document.html', title: 'sample-document.json 图解', blurb: '点数据看端点 / 拐点 / 分组骨架' },
1405
1405
  { href: 'steps/index.html', title: '从 0 手写分步 demo', blurb: '13 步,每步一个文件' },
@@ -1576,7 +1576,44 @@
1576
1576
  continue
1577
1577
  }
1578
1578
  if (/^\s*\|/.test(line) && index + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[index + 1])) {
1579
- const parseRow = row => row.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(cell => cell.trim())
1579
+ /**
1580
+ * 表格行拆分:反引号里的竖线(类型联合 `'a' \| 'b'` 很常见)不是分隔符,
1581
+ * `\|` 按转义后的普通竖线收进单元格;只看分隔位置,不做第二次 replace。
1582
+ */
1583
+ const parseRow = (row) => {
1584
+ const text = row.trim()
1585
+ const start = text.startsWith('|') ? 1 : 0
1586
+ let trailingBackslashes = 0
1587
+ for (let cursor = text.length - 2; cursor >= 0 && text[cursor] === '\\'; cursor -= 1) {
1588
+ trailingBackslashes += 1
1589
+ }
1590
+ const trailing = text.endsWith('|') && trailingBackslashes % 2 === 0
1591
+ const end = trailing ? text.length - 1 : text.length
1592
+ const cells = []
1593
+ let cell = ''
1594
+ let inCode = false
1595
+ for (let cursor = start; cursor < end; cursor += 1) {
1596
+ const character = text[cursor]
1597
+ if (character === '\\' && text[cursor + 1] === '|') {
1598
+ cell += '|'
1599
+ cursor += 1
1600
+ continue
1601
+ }
1602
+ if (character === '`') {
1603
+ inCode = !inCode
1604
+ cell += character
1605
+ continue
1606
+ }
1607
+ if (character === '|' && !inCode) {
1608
+ cells.push(cell.trim())
1609
+ cell = ''
1610
+ continue
1611
+ }
1612
+ cell += character
1613
+ }
1614
+ cells.push(cell.trim())
1615
+ return cells
1616
+ }
1580
1617
  const head = parseRow(line)
1581
1618
  index += 2
1582
1619
  const rows = []
@@ -1919,7 +1956,7 @@
1919
1956
  { badge: 'START', title: '第一次使用', desc: '引入、最小数据与首屏配置,先把图画出来。', meta: '01 快速开始 · 约 10 分钟', file: 'manual/01-快速开始.md' },
1920
1957
  { badge: 'BACKEND', title: '接后端与导出', desc: '编辑接口、导出回灌、插件与自建界面。', meta: '15 导出与集成', file: 'manual/15-导出与集成.md' },
1921
1958
  { badge: 'PERF', title: '十万级性能', desc: 'Worker 筛选 / 异步搜索、批量更新与质量档。', meta: '20 性能与大数据', file: 'manual/20-性能与大数据.md' },
1922
- { badge: 'API', title: '查方法与参数', desc: '实例方法、options 与可复制示例集中检索。', meta: 'API 速查 · 248 个成员', href: 'api/index.html', external: true }
1959
+ { badge: 'API', title: '查方法与参数', desc: '实例方法、options 与可复制示例集中检索。', meta: 'API 速查 · 256 个成员', href: 'api/index.html', external: true }
1923
1960
  ]
1924
1961
  const renderReadingPaths = () => `<section class='doc-paths' aria-label='推荐阅读路径'>` +
1925
1962
  `<div class='doc-paths__head'><strong>按任务选一条阅读路径</strong><span>第一次进来先看快速开始;带着明确目标时直接跳到对应章节。</span></div>` +
@@ -44,7 +44,7 @@
44
44
  | `graphics` | `GisViewGraphic[]` | 无 | 与节点无关的扩展图形:按世界坐标放 SVG 路径 / 图片 / Canvas,支持锚点、偏移、旋转、透明度、`zIndex` 与 `scaleWithView`;位于画布之上、标签之下,运行时用 `addGraphic` / `addGraphics` / `updateGraphic` / `removeGraphic` / `clearGraphics` 增删改,见「扩展图形(graphic)」 |
45
45
  | `tooltip` | `GisViewTooltipOptions` | 无 | 悬停提示:点 / 线 / 通道分别配置内容,见「悬浮提示」 |
46
46
  | `minimap` | `GisViewMinimapOptions \| null` | 无 | 内置鹰眼图:位置、尺寸、节点 / 线路 / 视口框三份样式与交互都可配,`extent` 决定小图按数据范围还是「数据 ∪ 底图」铺(默认 `auto`,GeoJSON 底图不会缩成一个点),`zoomable` 打开后小图自身可整体放大缩小(空白处拖动可以平移小图或直接框选让主图显示过去,每格倍率 `zoomStep`),通道还能只画一根;小图同样支持规则(`pointRules` / `lineRules`,缺省沿用主图规则的颜色与层级),可以只画某几层(`levels`)、关掉分层叠画(`layered`)、按三组系数给节点与线路定尺寸(整体倍数 `pointSizeScale` / `lineWidthScale`、跟随小图倍数 `pointZoomScale` / `lineZoomScale`(只在小图自身倍数 ≠ 1 时起作用)、跟随主视图地图倍数 `pointMapZoomScale` / `lineMapZoomScale`),视口框与框选矩形的颜色 / 背景 / 边框 / 圆角也能配(`boxStyle` / `selectStyle`),`extent` 还能直接写死一份世界范围、容器可加 `className` / `style`、`layers` 按 `placement` 分数据前 / 数据后、段内按 `order` 拆出多层自绘内容(旧 `draw` 钩子仍兼容并排在最后)、`onInteraction` 在手势结束时给出类型 / 世界坐标 / 相机、`maxPixelRatio` 限制备份倍率上限,不配则不产生任何 DOM,见「鹰眼图」 |
47
- | `filter` | `GisViewFilterInput \| null` | 无 | 初始筛选条件,等价于创建之后调用 `setFilter`,见「筛选」 |
47
+ | `filter` | `GisViewFilterInput \| null` | 无 | 初始筛选条件,等价于创建之后调用 `setFilter`;`mode` 缺省 `'hide'`,写 `'remove'` 则删除不命中的要素,见「筛选」 |
48
48
  | `filterWorker` | `boolean \| GisViewFilterWorkerOptions` | `{ threshold: 20000 }` | 筛选是否用 Web Worker 计算:默认点 + 线总数超过阈值才用,`false` 表示始终同步算 |
49
49
  | `searchWorker` | `boolean \| GisViewSearchWorkerOptions` | `{ threshold: 20000 }` | [异步搜索](14-视图与筛选.md#搜索与定位)是否用 Web Worker 计算:默认点 + 线总数超过阈值才用,`false` 表示 `searchAsync` 也同步算;同步的 `search` 一直在主线程算,不受这一项影响 |
50
50
  | `pointHoverStyle` / `pointSelectStyle` | `GisViewPointStyle` | 无 | 点的悬停 / 选中样式;没配就不改变外观(悬停仍然触发回调与提示、选中仍然进选中集合),要反馈就自己配一份 |
@@ -57,6 +57,7 @@
57
57
  | `fitSpacing` | `boolean \| GisViewFitSpacingOptions` | 关 | 适应视图的节点间距兜底:`true` 用 `{ minDistance: 24, maxZoom: 0 }`,对象可改最小中心距(CSS 像素)与相对普通适应比例的上限(`0` 表示不限);只在首次默认适应与 `fitView()` 生效,远处孤立点允许落到视口外,见「适应视图的节点间距(fitSpacing)」 |
58
58
  | `boxSelect` | `boolean` | `false` | 是否允许按住框选快捷键(`shortcuts.boxSelect`,默认 Shift)拖动鼠标框选要素;**缺省关**,写 `boxSelect: true` 或 `features: { boxSelect: true }` 才开。框选默认只收节点,`selectionKinds` 可把线路 / 通道也纳入 |
59
59
  | `multiSelect` | `boolean` | `false` | 多选开关:打开后不用按修饰键,直接单击节点 / 线路就取反累加选中(与按住 `shortcuts.multiSelect` 点击同一套行为、同一个 `multiSelectStyle`;取反那一下不经过 `onClick`,`onSelectChange.action.type` 是 `toggleSelect`);**缺省关**,关着时多选走框选(`boxSelect`)与按住多选快捷键(默认 Ctrl / ⌘)。运行时 `updateOptions({ multiSelect })` 切换只重画一帧、不重排场景 |
60
+ | `selectTrigger` | `'click' \| 'dblClick'` | `'click'` | 选中触发方式:`click` 第一次单击就改选中集;`dblClick` 第一下只派发 `onClick`,等双击判定命中的第二下才改选中集,并同时派发 `onClick` 与 `onDblClick`。Ctrl / ⌘ 多选与 `multiSelect` 在双击模式下同样等第二下才取反。运行时走 `updateOptions({ selectTrigger })`,只重画一帧、不重排场景 |
60
61
  | `selectionKinds` | `GisViewSelectionKindsInput` | 无(框选只收节点;Ctrl 多选收节点 / 线路 / 通道) | 框选与 Ctrl / 多选点击的类型过滤:`'point'` 节点、`'line'` 通道外的独立线路、`'channel'` 通道回线;可写数组任意组合,`'all'` / `true` 全开,`false` / `null` 全关。通道回线只归 `channel` 管,通道外的线路只归 `line` 管;通道回线仍按 `channelSelectMode` 摊开。不写时保留旧口径;普通单选与 `select` / `selectFeatures` 不受它限制。运行时走 `updateOptions({ selectionKinds })`,见「框选」与「选择类型过滤」 |
61
62
  | `selectionBoxStyle` | `GisViewSelectionBoxStyle` | 蓝底 0.16 透明 + 1px 蓝框 | 框选矩形的填充与边框:`fill`、`fillOpacity`、`stroke`、`strokeWidth` |
62
63
  | `selectionOnTop` | `boolean` | `true` | 选中的节点 / 拖动中的整组 / 待确认合并的目标节点,以及选中线路所在的整条通道,是否画在所有要素之上(与 `zIndex` 无关),见「选中 / 拖动的层级」 |
@@ -110,7 +111,7 @@
110
111
  | 通道几何 | `channelWidth` · `channelInset` · `channelNodeWidth` · `channelTangentInset` · `channelMaxGroups` · `channelGroupMaxLines` · `channelDraftStyle` · `channelSelectMode` · `channelWaypointDragMode` |
111
112
  | 样式与规则 | `pointStyle` · `lineStyle` · `pointRules` / `lineRules` · `heatmap` · `effects` · `lineDirection` · `prebake` · `label` · `labelOptimize` · `labelRender` · `lineLabel` · `labelStates` · `tooltip` · `pointSizeScale` / `lineWidthScale`、`pointZoomScale` / `lineZoomScale`、`zoomBaseScale` · `cursorStyle` · `gridColor` · `gridSize` · `showGrid` |
112
113
  | 状态样式与选中 | `pointHoverStyle` / `pointSelectStyle` · `lineHoverStyle` / `lineSelectStyle` · `pointMultiSelectStyle` / `pointBoxSelectStyle` · `lineMultiSelectStyle` · `defaultHighlight` · `selectionBoxStyle` · `selectionOnTop` · `waypointStyle` / `waypointSelectStyle` |
113
- | 交互开关 | `boxSelect` · `multiSelect` · `waypointEditable` · `waypointVisibility` · `interactive` · `preventContextMenu` · `clickable` · `draggable` · `liveDrag` · `lineFollowsNode` · `shortcuts` · `copy` · `remove` · `merge` |
114
+ | 交互开关 | `boxSelect` · `multiSelect` · `selectTrigger` · `waypointEditable` · `waypointVisibility` · `interactive` · `preventContextMenu` · `clickable` · `draggable` · `liveDrag` · `lineFollowsNode` · `shortcuts` · `copy` · `remove` · `merge` |
114
115
  | 视图与相机 | `minScale` / `maxScale` · `fitPadding` · `fitOnInit` · `initialView` · `zoomStep` |
115
116
  | 画质与渲染 | `quality` · `width` / `height` · `pixelRatio` · `maxCanvasPixels` · `antialias` · `interactionThinLines` · `interactionNoMsaa` · `renderer` · `webglLayers` |
116
117
  | 大数据(筛选 / 搜索 / 读数) | `denseMode` · `filter` · `filterWorker` · `searchWorker` · `progressiveInit` · `statsInterval` · `renderEmitInterval` |
@@ -634,6 +635,19 @@ createGisView(container, { multiSelect: true }) // 单击即取反累加,不
634
635
  // 按住快捷键的老写法照旧:Ctrl + 点击
635
636
  ```
636
637
 
638
+ #### `selectTrigger`
639
+
640
+ - **类型**:`'click' | 'dblClick'`(也认 `doubleClick` / `double` 两种别名)
641
+ - **默认值**:`'click'`
642
+ - **说明**:选中触发方式。`click` 保持缺省行为,第一次单击就改选中集;`dblClick` 下第一下只派发 `onClick`,同位置 320ms 内的第二下才应用选中 / 清空 / 取反,并在同一次点击里派发 `onClick`(`action.double: true`)与 `onDblClick`。Ctrl / ⌘ 多选与 `multiSelect` 也遵循这一档:双击模式下第二下才取反。普通双击在两种模式下都会于第二下派发 `onDblClick`,只是 `click` 模式第一次单击已经改过选中集;`click` 模式的 Ctrl / `multiSelect` 取反快捷路保持原口径,不额外发点击 / 双击事件。运行时切换会清掉上一次点击的双击判定状态,避免刚改完后的第一下被算成旧序列的第二下。
643
+ - **参考**:见「[08 选中与拐点](08-选中与拐点.md#点击选中与-dblclick)」与「[04 事件与回调](04-事件与回调.md#回调)」
644
+ - **示例**:
645
+
646
+ ```js
647
+ createGisView(container, { selectTrigger: 'dblClick' })
648
+ view.updateOptions({ selectTrigger: 'click' })
649
+ ```
650
+
637
651
  #### `selectionKinds`
638
652
 
639
653
  - **类型**:`GisViewSelectionKindsInput`
@@ -941,11 +955,12 @@ const { dense } = view.getStats() // { mode, active, reason, visiblePoin
941
955
 
942
956
  - **类型**:`GisViewFilterInput | null`
943
957
  - **默认值**:无
944
- - **说明**:初始筛选条件,等价于创建之后调用 `setFilter`,见「筛选」
958
+ - **说明**:初始筛选条件,等价于创建之后调用 `setFilter`。`mode` 缺省 `'hide'` 只隐藏;写 `'remove'` 把不命中的要素从数据里删掉(先删线路、再删节点,只发一条 `filterRemove`),见「筛选」
945
959
  - **参考**:见「筛选」一节,子字段在那里的表里逐个展开
946
960
 
947
961
  ```js
948
962
  view.setFilter({ points: { voltage: { gte: 500 } }, cascade: 'binding' })
963
+ view.setFilter({ points: { voltage: { lt: 220 } }, mode: 'remove' }) // 不命中的要素直接从数据删除
949
964
  view.getFilterResult() // { hiddenPoints, hiddenLines, visiblePoints, visibleLines, worker, durationMs }
950
965
  view.setFilter(null) // 清掉筛选
951
966
  ```
@@ -8,15 +8,15 @@
8
8
  | --- | --- | --- |
9
9
  | `onReady` | `{ canvas, gl }` | WebGL2 上下文与缓冲区就绪 |
10
10
  | `onSelect` | `GisViewSelectPayload \| null` | 选中状态变化:点击为 `{ items, box: null, originalEvent }`,框选(默认按住 Shift)为 `{ items, box, originalEvent }`,点击空白处清空时为 `null`;Ctrl 点击节点的累加选中也走这里 |
11
- | `onClick` | `GisViewClickPayload \| null` | 单击要素或空白处(空白处为 `null`):`type` / `item` / `index` / `screen` / `coordinate` / `button` / `selection` / `originalEvent` 之外还有 `action`(`type` 是 `select` / `clearSelection` / `click`,`double` 说明这一下是不是双击里的第二下,`press` / `duration` / `longPress` 说明这一次按下想做什么、按了多久、算不算长按),见「指针事件」 |
11
+ | `onClick` | `GisViewClickPayload \| null` | 单击要素或空白处(空白处为 `null`):`type` / `item` / `index` / `screen` / `coordinate` / `button` / `selection` / `originalEvent` 之外还有 `action`(`type` 是 `select` / `toggleSelect` / `clearSelection` / `click`,`double` 说明这一下是不是双击里的第二下,`press` / `duration` / `longPress` 说明这一次按下想做什么、按了多久、算不算长按),见「指针事件」。`selectTrigger: 'dblClick'` 时第一下只发 `action.type: 'click'`,第二下才应用选中并带 `double: true` |
12
12
  | `onClusterClick` | `GisViewClusterClickPayload` | 点中多成员聚合圆:带 `id` / `count` / `pointIndexes` / `pointIds` / `bounds` / `radius` / `coordinate` / `screen` / `selection` / `action`(`action.type` 恒为 `clusterClick`、`action.target` 为 `cluster`)。这一条**不会同时发 `onClick`**,也不会改动选中集;聚合圆内部的原始节点不参与普通点击 |
13
- | `onDblClick` | `GisViewClickPayload \| null` | 双击要素或空白处(320ms / 6px 内连续两次单击):`action.type` 是 `dblClick`、`action.double` 为 `true` |
13
+ | `onDblClick` | `GisViewClickPayload \| null` | 双击要素或空白处(320ms / 6px 内连续两次单击):`action.type` 是 `dblClick`、`action.double` 为 `true`。普通双击在两种 `selectTrigger` 下都会于第二下派发;`dblClick` 模式下选中 / 清空 / 多选取反也在这一下应用,`click` 模式下第一下已经应用过。`click` 模式的 Ctrl / `multiSelect` 取反快捷路保持原口径,不额外发 `onClick` / `onDblClick` |
14
14
  | `onPointerDown` | `GisViewPointerPayload` | 左键 / 中键 / 右键按下:命中的要素、`button` / `buttons` 与光标位置;`action.type` 是这一次按下的**意图**(`dragPoint` / `dragWaypoint` / `boxSelect` / `pan` / `copy` / `channelDraft` / `contextMenu`…),`action.target` 说明点的是什么、`action.data` 直接给到那份数据,见「指针事件」 |
15
15
  | `onPointerUp` | `GisViewPointerPayload` | 左键 / 中键 / 右键抬起,与 `onPointerDown` 成对:`action.type` 是**刚做完的那件事**(拖过就是 `dragPoint` / `dragWaypoint` / `boxSelect` / `pan`,没拖就是 `click`),`action.moved` 说明这一次是拖还是点,另带 `action.press`(按下时的意图)/ `duration`(按了多久)/ `longPress`(长按) |
16
16
  | `onContextMenu` | `GisViewPointerPayload` | 每一次右键:点、线、通道、空白都只发这一条(要素信息在载荷里);`action.type` 固定是 `contextMenu`、`action.target` 说明点的是点 / 线 / 通道 / 空白。命中通道回线时,同一载荷另带 `channelContext`(几组 / 当前哪组 / 可用交互),不再有单独的通道右键回调。要自己弹菜单就在回调里渲染。**右键回调缺省关**(`contextMenu` / `interactive` 都没写时组件不接右键),写 `interactive: true` / `contextMenu: true` 或 `features: 'view'` 才发;接住右键时系统菜单默认已被收掉(`preventContextMenu` 缺省 `true`),想看系统菜单写 `preventContextMenu: false` |
17
17
  | `onCascadeHighlight` | `GisViewCascadeHighlightInfo \| null` | 点击级联高亮变了:载荷带来源 `origin`(节点 / 线路 / 通道回线)、范围 `scope` 与亮着的 `points` / `lines` / `pointIndexes` / `lineIndexes`,与 `getCascadeHighlight()` 同一份;点空白或关掉开关时是 `null`。这条高亮不改选中集,事件总线名为 `cascadeHighlight` |
18
18
  | `onHover` | `GisViewHoverPayload \| null` | 悬停要素变化(空白处 / 移出画布是 `null`):与拾取结果同一份读数(`type` / `index` / `item` + 通道字段,另有光标位置 `x` / `y` / `world` / `clientX` / `clientY`、选中集 `selection` 与 `lngLat`)+ `action`(`type` 恒为 `hover`、`target` 说明停在什么上面、`button` 是 `none`、`modifiers` 是当下的修饰键);程序化悬停 `hoverAt()` 也走同一条 |
19
- | `onFilter` | `GisViewFilterResult` | 筛选完成(含 Worker 异步返回):隐藏的点 / 线下标、剩余可见数量、是否用了 Worker 与计算耗时 |
19
+ | `onFilter` | `GisViewFilterResult` | 筛选完成(含 Worker 异步返回):隐藏的点 / 线下标、剩余可见数量、是否用了 Worker 与计算耗时。`filter.mode: 'remove'` 时删除完成后只发最终一次,`hiddenPoints` / `hiddenLines` 为空 |
20
20
  | `onCursor` | `[number, number] \| null` | 光标世界坐标,移出画布时为 `null` |
21
21
  | `onPointer` | `GisViewPointerState` | 光标每次移动(含移出画布)都发:`{ inside, screen, world, lngLat }`,屏幕坐标是画布 CSS 像素、`lngLat` 要有 GeoJSON 背景才有;`getPointer()` 随时能读同一份,移出时 `inside: false` |
22
22
  | `onPointDrag` | `GisViewPointDragPayload` | 拖拽点:`phase` 为 `start` / `drag` / `end`,载荷带最新坐标、本次移动的全部节点 `points` 与跟随移动的线路 `lines` |
@@ -79,11 +79,11 @@ const view = createGisView('#map', {
79
79
 
80
80
  | 载荷字段 | 说明 |
81
81
  | --- | --- |
82
- | `type` | 动作名:`updateData` / `updatePoints` / `updateLines` / `updateOptions` / `updateBackground` / `updatePoint` / `updateLine` / `updateChannel` / `pointDrag` / `copyPoint` / `copyPoints` / `mergePoints` / `waypoint` / `addChannel` / `addChannels` / `addChannelGroups` / `addLine` / `addPoints` / `addLines` / `removePoints` / `removeSelected` / `removeLines` / `removeLine` / `removeChannel` / `channelGroups` / `batch` |
82
+ | `type` | 动作名:`updateData` / `updatePoints` / `updateLines` / `updateOptions` / `updateBackground` / `updatePoint` / `updateLine` / `updateChannel` / `pointDrag` / `copyPoint` / `copyPoints` / `mergePoints` / `waypoint` / `addChannel` / `addChannels` / `addChannelGroups` / `addLine` / `addPoints` / `addLines` / `removePoints` / `removeSelected` / `removeLines` / `filterRemove` / `removeLine` / `removeChannel` / `channelGroups` / `batch` |
83
83
  | `points` / `lines` / `channels` | 变更之后的最新数据(组件内部那一份的引用,只读,不要就地改) |
84
84
  | `counts` | `{ points, lines, channels }` 三个条数 |
85
85
  | `ok` / `reason` | 这一次调用成没成(`addPoints` / `addLines` / `removePoints` 这类批量接口会带上),`reason` 说明没成的原因 |
86
- | 动作专属字段 | 拖拽带 `movedPoints` / `movedLines`,复制带 `item` / `source` / `revert()`(`copyPoints` / 整组粘贴另有 `sources` / `items` / `indexes` / `lines` / `channels` / `count`),合并带 `source` / `target` / `removedLines` / `mergedChannels`,拐点带 `lineIndex` / `pathIndex` / `phase`,新增带 `lineIds` / `lineIndex` / `channelId` / `groupId`(`addPoints` / `addLines` 另有 `indexes` / `items` / `position`:新要素在数组里的位置与加进去的那几条,`addChannelGroups` 另有 `results` / `groupIds` / `items`),删除带 `indexes` / `item` / `removedLineIds`,分组带 `index` / `groups` / `action`(细分动作,与 `onChannelGroupChange` 同一份);批量补丁(`patchPoints` / `patchLines`)的 `updatePoints` / `updateLines` 带 `indexes` / `items` / `count` / `skipped`,`removeSelected` 带 `pointIds` / `lineIds` / `pointIndexes` / `lineIndexes` / `removedChannels` |
86
+ | 动作专属字段 | 拖拽带 `movedPoints` / `movedLines`,复制带 `item` / `source` / `revert()`(`copyPoints` / 整组粘贴另有 `sources` / `items` / `indexes` / `lines` / `channels` / `count`),合并带 `source` / `target` / `removedLines` / `mergedChannels`,拐点带 `lineIndex` / `pathIndex` / `phase`,新增带 `lineIds` / `lineIndex` / `channelId` / `groupId`(`addPoints` / `addLines` 另有 `indexes` / `items` / `position`:新要素在数组里的位置与加进去的那几条,`addChannelGroups` 另有 `results` / `groupIds` / `items`),删除带 `indexes` / `item` / `removedLineIds`,分组带 `index` / `groups` / `action`(细分动作,与 `onChannelGroupChange` 同一份);批量补丁(`patchPoints` / `patchLines`)的 `updatePoints` / `updateLines` 带 `indexes` / `items` / `count` / `skipped`,`removeSelected` 带 `pointIds` / `lineIds` / `pointIndexes` / `lineIndexes` / `removedChannels`,`filterRemove` 带 `mode: 'remove'` / `pointIndexes` / `removedPoints` / `removedLineIndexes` / `removedLineItems` / `lineIds` / `count` / `removedLines` / `removedChannels`(`points` / `lines` / `channels` 仍是删除后的最新数据) |
87
87
 
88
88
  `onSelectChange` 与 `onSelect` 的区别只有一个:**只在选中集真的变了时发** —— 框选过程中的重复同步、
89
89
  数据变化引起的补发都不发;并且**接口选中**(`selectFeatures()` / `selectFeatures(null)` / `selectInBox()`)也会发
@@ -93,7 +93,8 @@ const view = createGisView('#map', {
93
93
  `action` 是更细的一份判读(与指针事件同一套写法):`action.type` 是 `select`(点中一个或接口选中)/
94
94
  `toggleSelect`(Ctrl 点击取反,取反取没了也是它)/ `boxSelect`(框选,没框到东西也是它)/
95
95
  `clearSelection`(点空白、Esc 或接口清空),`action.target` 是 `selection` / `empty`,`action.data` 就是
96
- 这一批 `items`,`action.modifiers` 是这一次的修饰键。Ctrl 取反与点空白清空这两条路**不经过 `onClick`**,
96
+ 这一批 `items`,`action.modifiers` 是这一次的修饰键。单击触发模式下,Ctrl 取反与点空白清空这两条路**不经过 `onClick`**;
97
+ `selectTrigger: 'dblClick'` 时第一下仍会发 `onClick`,第二下应用选中 / 取反 / 清空后会同时发 `onClick` 与 `onDblClick`。
97
98
  要按操作分支的面板认这一个字段:
98
99
 
99
100
  ```js
@@ -34,7 +34,27 @@ createGisView(container, {
34
34
 
35
35
  拖动期间选框实时跟随,但事件只在松手时派发一次,不会拖动一帧报一次。**框选默认只收节点**,保持旧口径;写 `selectionKinds` 后可以把通道外的线路与通道回线一起纳入(三类任意组合),见下节。选中结果用 `getSelections()` / `getSelectionData().ids` 读回。用代码框选等价于调用 `selectInBox({ x, y, width, height })`,矩形同样是画布 CSS 像素;第二段还能临时覆盖类型过滤。框选命中的节点套用框选样式(`options.pointBoxSelectStyle`,节点自己的 `boxSelectStyle` 优先),缺省与选中样式完全一致;框选出来的线路走普通的 `lineSelectStyle`(线路没有单独的框选样式)。
36
36
 
37
- 不想拖动时也可以用 Ctrl(macOS 为 Cmd)逐个点击节点累加选中,命中已选中的节点会把它取消掉;两种方式得到的选中集合一样,之后按住组内任意一个节点拖动就是整组移动。不想按修饰键就把多选开关打开(`options.multiSelect: true`,运行时用 `updateOptions({ multiSelect })` 切):普通单击即取反累加,线路也认(受 `channelSelectMode` 的整组 / 整条摊开约束);双击的第二次点击不再取反,`onDblClick` 照常派发。多选命中的节点走多选样式(`options.pointMultiSelectStyle`,节点自己的 `multiSelectStyle` 优先),框选命中的走框选样式(`options.pointBoxSelectStyle`,节点自己的 `boxSelectStyle` 优先),两者缺省都与选中样式完全一致。注意框选样式只给节点;选中线路 / 通道回线时仍按 `lineSelectStyle` / `lineMultiSelectStyle` 上色。
37
+ 不想拖动时也可以用 Ctrl(macOS 为 Cmd)逐个点击节点累加选中,命中已选中的节点会把它取消掉;两种方式得到的选中集合一样,之后按住组内任意一个节点拖动就是整组移动。不想按修饰键就把多选开关打开(`options.multiSelect: true`,运行时用 `updateOptions({ multiSelect })` 切):普通单击即取反累加,线路也认(受 `channelSelectMode` 的整组 / 整条摊开约束);单击触发模式下双击的第二次点击不再取反,`onDblClick` 照常派发。多选命中的节点走多选样式(`options.pointMultiSelectStyle`,节点自己的 `multiSelectStyle` 优先),框选命中的走框选样式(`options.pointBoxSelectStyle`,节点自己的 `boxSelectStyle` 优先),两者缺省都与选中样式完全一致。注意框选样式只给节点;选中线路 / 通道回线时仍按 `lineSelectStyle` / `lineMultiSelectStyle` 上色。
38
+
39
+ ### 点击选中与 dblClick
40
+
41
+ `selectTrigger` 缺省是 `'click'`:命中的节点 / 线路在第一次单击时就进选中集,点空白也在第一次单击时清空。写 `selectTrigger: 'dblClick'` 后,第一次点击只派发 `onClick`(`action.type` 是 `'click'`),同一位置 320ms 内、6px 范围内的第二下才应用选择结果:
42
+
43
+ - 点要素:选中它;`multiSelect` 或按住 Ctrl / ⌘ 时取反累加。第二下同时派发 `onClick`(`action.double: true`,取反时 `action.type` 为 `'toggleSelect'`,普通选中为 `'select'`)与 `onDblClick`(`action.type: 'dblClick'`)。
44
+ - 点空白:第二下才清空选中集并派发 `onDblClick`。
45
+ - `click` 模式下普通双击的 `onDblClick` 也照常派发,只是第一次单击已经完成了选中;Ctrl / `multiSelect` 的取反快捷路保持原口径(不额外发 `onClick` / `onDblClick`)。两种模式都不影响拐点双击删除与通道草稿等既有双击交互。
46
+
47
+ ```js
48
+ createGisView(container, {
49
+ features: { select: true },
50
+ selectTrigger: 'dblClick',
51
+ onDblClick: payload => panel.open(payload) // 第二下必发;payload.action.double === true
52
+ })
53
+
54
+ view.updateOptions({ selectTrigger: 'click' }) // 运行时切回单击选中
55
+ ```
56
+
57
+ 运行时切换 `selectTrigger` 会清掉上一次点击的双击判定状态,切换后的第一下不会被算成旧点击序列的第二下。`selectFeatures()` / `select()` 这类接口选中不受触发方式影响。
38
58
 
39
59
  ### 选择类型过滤(selectionKinds)
40
60
 
@@ -115,8 +135,9 @@ selection.lines // 选中的线路数据(通道回线也算一条)
115
135
  selection.channels // 选中回线所属的通道(按通道去重,同一条通道的几回线算一条)
116
136
  selection.items // 与 getSelections() 同一份:带下标、屏幕坐标与通道上下文
117
137
  selection.bounds // 世界包围盒(空选中时 null),直接交给 zoomToBounds / focusOn
118
- view.focusOn(selection.items) // 把视角挪到这一批上(整体居中 + 按范围适配)
119
- view.focusOn(selection.items, { select: true }) // 反过来也行:聚焦的同时把它们选上
138
+ view.focusOn(selection.items) // 把视角中心挪到这一批(比例不动)
139
+ view.focusOn(selection.items, { zoom: true }) // 按这一批的整体范围适配
140
+ view.focusOn(selection.items, { select: true, zoom: true }) // 反过来也行:聚焦的同时把它们选上
120
141
  ```
121
142
 
122
143
  `mode` 只决定「这一批怎么并进当前选中集」,落地之后与手动选中完全一样(通道回线照旧按通道抬层,见上一节)。认不出来的条目跳过(与 `patchPoints` 的 `skipped` 一个态度)。按 id 定位用的是与 `patchPoints` 同一张 id 表(按数据版本缓存),`isSelected` 与 `selectFeatures` 的去重也都走常数级的键集合 —— 十万级数据下「按 id 选一批」是 O(选中数),不会每个 id 扫一遍全量。