gis-view-webgl2 0.1.2 → 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 (66) hide show
  1. package/README.md +21 -14
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +421 -76
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +151 -0
  7. package/docs/GisView.md +4 -4
  8. 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 +137 -0
  9. package/docs/api/index.html +23 -9
  10. package/docs/demos/ApiCoverageDemo.vue +644 -0
  11. package/docs/demos/ChannelDemo.vue +14 -12
  12. package/docs/demos/ClusterDemo.vue +251 -0
  13. package/docs/demos/CopyMergeDemo.vue +3 -4
  14. package/docs/demos/EventDemo.vue +3 -3
  15. package/docs/demos/ExtensionDemo.vue +69 -12
  16. package/docs/demos/GisViewDemo.vue +64 -8809
  17. package/docs/demos/SearchFilterDemo.vue +13 -11
  18. package/docs/demos/api-index.js +58 -47
  19. package/docs/demos/demos.js +34 -17
  20. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +29 -0
  21. package/docs/demos/gis-view-demo/GisViewDemoDialogs.vue +82 -0
  22. package/docs/demos/gis-view-demo/GisViewDemoFooter.vue +16 -0
  23. package/docs/demos/gis-view-demo/GisViewDemoHeading.vue +16 -0
  24. package/docs/demos/gis-view-demo/GisViewDemoPanelStack.vue +71 -0
  25. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +219 -0
  26. package/docs/demos/gis-view-demo/demo.css +1230 -0
  27. package/docs/demos/gis-view-demo/demoContext.js +12 -0
  28. package/docs/demos/gis-view-demo/panels/DataPanels.vue +303 -0
  29. package/docs/demos/gis-view-demo/panels/OpsPanels.vue +174 -0
  30. package/docs/demos/gis-view-demo/panels/StylePanels.vue +561 -0
  31. package/docs/demos/gis-view-demo/panels/ViewPanels.vue +396 -0
  32. package/docs/demos/gis-view-demo/useGisViewDemo.js +6188 -0
  33. package/docs/format/07-/344/272/213/344/273/266/350/275/275/350/215/267/344/270/216/347/244/272/344/276/213/346/225/260/346/215/256.md +3 -2
  34. package/docs/index.html +41 -4
  35. package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +1 -1
  36. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +3 -2
  37. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +104 -16
  38. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +15 -14
  39. 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 +40 -3
  40. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +50 -3
  41. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +6 -2
  42. package/docs/manual/10-/351/200/232/351/201/223/347/274/226/350/276/221/344/270/216/346/226/260/345/242/236.md +39 -7
  43. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +13 -4
  44. package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +3 -0
  45. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +121 -49
  46. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +37 -3
  47. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +17 -8
  48. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +45 -25
  49. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +91 -20
  50. 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
  51. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +25 -1
  52. 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 +11 -8
  53. package/docs/steps/README.md +17 -11
  54. package/docs/steps/selftest-actions.html +3 -5
  55. package/docs/steps/selftest-demo.html +27 -18
  56. package/docs/steps/selftest-demos.html +409 -32
  57. package/docs/steps/selftest-docs.html +33 -4
  58. package/docs/steps/selftest-effects.html +200 -0
  59. package/docs/steps/selftest-features.html +603 -21
  60. package/docs/steps/selftest-labels.html +74 -0
  61. package/docs/steps/selftest-perf.html +293 -20
  62. package/docs/steps/selftest-scale.html +150 -29
  63. package/docs/steps/selftest-select.html +90 -0
  64. package/docs/steps/step-10-full.js +1 -1
  65. package/docs/steps/step-12-events.js +1 -1
  66. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  不依赖任何图形库的 WebGL2 矢量视图:点、线、背景层(图片 / GeoJSON)与首屏落位(`initialView`)都通过 `createGisView(dom, options)` 创建,宽高默认跟随传入的 DOM 容器;跨项目复用的视图级逻辑用 `plugins` 挂载。
4
4
 
5
+ 本轮补齐四项:`focusOn` 缺省只居中、显式 `zoom: true` 才按范围适配;文档中心与 API 速查互跳,并修掉左侧分组跳转的慢速平滑滚动;新增实例命令扩展点与可交互 demo;筛选补丁、通道范围读取与事件分发做定向性能优化。
6
+
5
7
  - 渲染层:原生 WebGL2(GLSL ES 3.00 + 实例化绘制),没有任何运行时依赖,也不需要 Vue。
6
8
  - 交互层:拖拽平移、滚轮缩放、按关键字搜索并定位(节点 / 线路 / 通道)、点击 / 双击拾取、点拖拽(相连线路同步移动)、Shift 拖动框选与 Ctrl 点击累加多选(多选后整组同步移动;`selectionKinds` 可限定只选节点 / 线路 / 通道或任意组合)、复制只带选中要素、节点合并可指定保留端并改写最终信息与样式、线路拐点编辑与通道多回线,全部走配置与回调 —— **这些能力缺省全关**,要哪一项显式打开(`features` 预设 / 开关表,或 `interactive: true` / `clickable: true` / `draggable: true` 这类详细配置,见 `docs/GisView.md` 的「功能开关」)。
7
9
  - 数据层:按属性筛选(点被过滤时绑在它身上的线路与通道一起消失,大数据量走 Web Worker)、按属性批量配色 / 换符号、节点标签(内容、贴边方式、HTML 定制、数量与显隐可配,十万级数据下有候选上限兜底)与点 / 线 / 通道的悬浮提示,筛选、规则与标签共用同一套条件语言;节点标签与线路标签分开配置、都支持 `auto`(智能隐藏)/ `always` / `hidden` 三种显隐策略,线路标签的位置沿线走(比例 / 百分比 / 绝对距离)、候选还会按屏幕小格摊开(通道里的并行回线不会把扫描名额占光),线路上还能按同样的位置写法挂图片或 SVG 装饰(`decorations`,通道回线可以用 `linePropsByMember` 逐条配;装饰缺省 `auto` —— 线路上没有拐点时按比例跟随(挂在中点就一直保持中点),有了拐点就跟拐点一样钉在线路上(位置不再沿路径挪动,线路几何一变就把锚点投回线身最近的点),写 `anchor: 'follow'` 强制一直跟随、`anchor: 'fixed'` + `at: [x, y]` 则固定在写死的坐标上;装饰与线路标签都能写 `parallel: true` 跟着线路方向转,通道里的每条回线也能各显示各的标签)。节点也能用 `points[].decorations` 挂图片 / SVG:`offset` 或 `distance` + `direction` 是相对节点的屏幕像素偏移(缩放时保持像素距离),`anchor: 'fixed'` / `at` 是固定世界坐标,`connector` 可从节点中心画连接线。平移 / 缩放 / 拖拽期间两类标签都能各自配 `hideOnInteract`(`true` 或者「标签数超过 N 才收」),收起时只切一个 class、不写标签 DOM,停手自动补齐;线路标签另有 `followOnInteract`(默认开)—— 没收起时拖节点 / 拖拐点期间跟着线路走,关掉则停手后统一归位;通道回线的提示范围用 `tooltip.channelScope` 在整条通道 / 所属分组 / 单条回线之间切。
@@ -9,7 +11,9 @@
9
11
  - 动态装饰器:`beginDecorationDraft` 用 `target` 选择只挂线路、只挂节点或两类同时开启(`both` 时按屏幕距离取更近者、等距优先节点,最近目标不可用时回退另一类);线路侧保留单线 / 分组 / 整条通道三种范围与平行方向,节点侧可切相对偏移 / 固定坐标 / 鼠标位置并画连接线。触发半径默认收紧到 14px,离开目标立即收掉;`maxPerLine` / `maxPerPoint` 分别限制每线 / 每点数量,`allowedLines` / `allowedChannels` / `allowedGroups` / `allowedPoints` 限定白名单,超限或不在白名单的目标静默不触发,提交前还会按最新数据复核;`allowDuplicate: false` 时按 `duplicateDistance` 检测同一位置,`duplicateStrategy` 可选自动避让或直接拒绝。空白处松手不发事件,只有命中的左键松开才发 `ready`;确认走 `commitDecorationDraft` 一次批量写入(线路与节点同一批、一条历史),取消走 `cancelDecorationDraft`,数据与历史在确认前不动。演示页可从左侧图标拖到线路,也可分别开启节点 / 线路装饰并现场调吸附方式、白名单与重复策略。
10
12
  - 界面尽量不内置:控制面板、读数、图例都不生成,组件只提供画布与实例方法,样式完全由使用方决定(标签与提示框是覆盖层,默认外观可以整体改写);唯一可选的界面是内置鹰眼图——配 `minimap` 才挂一层小地图(位置、尺寸、节点 / 线路 / 视口框三份样式与交互都可配,`zoomable` 打开后小图自身也能整体放大缩小:空白处拖动可以平移小图、或直接框选让主图显示过去,每格倍率 `zoomStep` 单独配,通道还能只画一根回线;小图同样吃规则——`pointRules` / `lineRules` 决定每个节点 / 每条线的颜色、大小与层级,`levels` 只看某几层、`layered` 控制分层叠画、`pointZoomScale` / `lineZoomScale` 让节点与线路跟着小图倍数放大,不配自己的规则时自动沿用主图规则的颜色与层级;自定义内容可以用 `layers` 按 `placement` 分到数据前 / 数据后、段内按 `order` 叠放,旧的单层 `draw` 仍兼容,手势完成时可通过 `onInteraction` 获取类型 / 世界坐标 / 相机,`maxPixelRatio` 可限制高分屏 / 宿主放大时的备份倍率),不配不产生任何 DOM。
11
13
  - 数据切换与加载信号:`updateData(points, lines, channels)` 一次换完整个场景(也收 `{ points, lines, channels }`;接口列名不一样时先在调用方映射成这三列,见 [06 数据与场景更新](docs/manual/06-数据与场景更新.md#接口列名不一样映射放在调用方)),场站 / 回线按批补用 `addPoints` / `addChannels`(批量通道只重排一次、只记一步历史),`onDataRendered` / `whenRendered` / `whenDataRendered` 给出「这一份数据已经画到屏幕上」的信号 —— loading 遮罩在这之后收,不会闪一帧旧画面。
12
- - 整包只有一个组件:`gis-view`,只从根入口导出;扩展点集中在插件、自定义图元 / 条件运算符与自定义导出格式(`defineExportFormat` / `registerExportFormat`),不需要改组件源码。
14
+ - 整包只有一个组件:`gis-view`,只从根入口导出;扩展点集中在插件、自定义图元 / 条件运算符、自定义导出格式(`defineExportFormat` / `registerExportFormat`)、WebGL 自定义层与实例命令(`registerCommand`),不需要改组件源码。
15
+
16
+ - 点聚合:`pointCluster` 用屏幕半径把密集点合成带数量文字的圆,支持半径 / 最少点数 / `maxZoom` 配置;平移不重算,缩放、数据与编辑状态变化才重排。只改展示、拾取、快照与导出,不改原始 `points` / `getData()`;`getClusters()` 读当前展示组(含单成员),`getPointCluster()` 读 `{ enabled, radius, minPoints, maxZoom, active }`,数量统计在 `getStats().clusterCount` / `clusterPointCount` / `clusterMs`;选中、拖动、合并与复制预览中的点强制保持单成员,点击聚合圆只发 `clusterClick`。
13
17
 
14
18
  ## 开发
15
19
 
@@ -19,15 +23,15 @@ npm run dev
19
23
  ```
20
24
 
21
25
  打开终端提示的地址,页面就是 `docs/demos/GisViewDemo.vue`:下拉第一项是仓库根目录 `new.json` 的真实数据(2164 个站点 / 3669 条线路 / 2629 条通道,点位与走向直接照它渲染),也可以切到同在根目录的样例数据 `test.json`(320 个站点 / 449 条线路,按 `channel_id` 聚合成 107 条多回线通道),或切回 900 / 5000 / 10 万的合成数据;真实与样例数据的线色、线宽和站点符号照 `test.js` 的 `getGisStyle` / `getSymbolPath` 分档,转成组件的批量规则表(`docs/demos/gisStyle.js`),关掉「按属性批量配色 / 换符号」就能对比同一份数据的两套外观。演示点击、双击、悬停、拖拽节点、Shift 框选与 Ctrl 点击多选、拐点编辑(加过 / 拖过拐点的通道与线路会归到「新增拐点集合」,读数栏下面直接列出来,点「导出拐点集合」把 `getParallelGroups()` 的结果复制到剪贴板),两端锚在节点上的二 / 三 / 四 / 五回线通道(可切换偏移方式、通道宽度与内收距离,也可以勾「隐藏通道端点节点」看通道与回线一起消失),以及新增的筛选下拉框(电压、站点类型、名称关键字、联动方式与「线路也按电压过滤」)、按属性配色换符号开关(站点符号可选原 SVG 路径或内置方块,路径描边粗细可调)、按电压分层(`zIndex`,高电压等级压在上面)、节点形状(圆 / 正方形 / 圆角方形)、标签位置 / 内容 / HTML 渲染 / 数量 / 候选上限与悬浮提示开关;筛选读数会显示隐藏了多少点 / 线、这次判定走的是 Web Worker 还是主线程、耗时多少。右下角的鹰眼图现在由组件自带(`minimap`):demo 只给尺寸、线路显隐、回线根数、拖动动作、步进、三份样式,以及这一轮新增的规则配色 / 分层开关 / 只看某几层(`levels`)/ 节点与线路跟随小图放大(`pointZoomScale` / `lineZoomScale`)(「可缩放」打开后滚轮与 - / + 按钮按 `zoomStep` 整体放大缩小鹰眼图自身,Ctrl / ⌘ + 滚轮留给主图;空白处拖动按 `dragAction` 平移小图或直接框选,按住 Ctrl / ⌘ 是另一个动作),缩略图画的是筛选后仍可见的节点与线路(含通道回线,关掉「回线」一个通道只画中间那一根),在视口框上按下直接拖框、空白处长按后视角跟随、单击不再改动视角。通道面板下面还有「通道分组」与「定向更新」两行:前者把选中的通道按每组的回线数拆开或合并回去(读数里能看到每个分组几回线、有没有拐点),后者改一个节点 / 线路 / 通道就立刻重渲染相关的视图,演示 `splitChannelGroups` / `mergeChannelGroups` / `moveChannelLine` / `updatePoint` / `updateLine` / `updateChannel` 这几个接口(右键框里每条回线都能单独分配:拆成新分组、或并进第 n 组,两回线拆成两个分组各一条);界面(按钮、滑块、读数浮层)仍然全部写在 demo 里。这一轮把 demo 重新排了一遍:说明段落收进「展开说明」、配置按组折进可折叠面板,画布右上角多了「全屏」按钮(`requestFullscreen` + `resize()`);节点标签默认只标最高电压那一档、线路标签默认不显示(面板上各有一份「显示范围」),新增的「规则更新」面板可以把规则写成 JSON 再点一下重新渲染。这一轮再把面板收进「视图 / 样式 / 数据」三个分页(整块面板栈独立滚动、鹰眼图设置默认折叠),画布默认垫一层由 `docs/demos/demoBackground.js` 现生成的 GeoJSON 底图(街区 + 环线 + 经纬网,可切三档底图与透明度),并新增「初始视图与背景」面板:首屏不再铺满整屏,默认聚焦数据中心放大 3 倍,也能切「适应全部 / 2× / 3×」或自己填中心点与倍数;demo 里还内置了一份插件示例 `focusSelectionPlugin`(按 F 聚焦当前选中),演示 `plugins` 的挂载与随视图销毁。
22
- 通道面板这一轮又多了一块:分组按整条通道的车道错开,拆分 / 合并不会挪动任何一条回线(两回线拆成两组各一条时两根线一左一右分居骨架两侧,四回线拆成两组时四条线还停在原来的位置上),想把某一组拐到别的走廊上就用「加拐点分开分组 / 去掉分开的拐点」按组挪拐点(`addChannelGroupWaypoint` / `removeChannelGroupWaypoint`);右键点通道回线弹出交互框,`onChannelContextMenu` 里给出这条通道有几组、当前是哪一组、每组几条回线与可用交互(点空白处收到 `null` 收框);「新增通道」点两次节点、草稿线跟着光标走,第二个节点点完在框里选「新建通道 / 变成新分组 / 并入某一组」,也可以勾自动提交或直接调 `addChannel`。右键框这一轮又加了「线路分配」:当前分组的每条回线各有一个下拉(拆成新分组 / 并入第 n 组)+「分配」按钮,走 `moveChannelLine`,组数 / 回线数到上限会被拒绝并在读数里给出原因。这一轮右键框再补齐:分组列表点一下就是 `setCurrentChannelGroup`(顺带选中这一组的回线、当前组高亮),下面一排「选中该分组 / 加拐点 / 删拐点 / 拆成多组 / 合并回一组」走 `runChannelAction`(灰掉的按钮悬停能看到原因),Esc / × / 点空白处收框;这两件事都要显式开开关:`features: { drag: true, channelMenu: true }` 与 `waypointEditable: true`(开关缺省全关;通道右键框还要 `contextMenu` 开着,通常给 `features: 'view'` 再加 `channelMenu: true`)。
26
+ 通道面板这一轮又多了一块:分组按整条通道的车道错开,拆分 / 合并不会挪动任何一条回线(两回线拆成两组各一条时两根线一左一右分居骨架两侧,四回线拆成两组时四条线还停在原来的位置上),想把某一组拐到别的走廊上就用「加拐点分开分组 / 去掉分开的拐点」按组挪拐点(`addChannelGroupWaypoint` / `removeChannelGroupWaypoint`);右键点通道回线弹出交互框,`onContextMenu` 里给出这条通道有几组、当前是哪一组、每组几条回线与可用交互(点空白处或节点时载荷没有 `channelContext`,据此收框);「新增通道」点两次节点、草稿线跟着光标走,第二个节点点完在框里选「新建通道 / 变成新分组 / 并入某一组」,也可以勾自动提交或直接调 `addChannel`。右键框这一轮又加了「线路分配」:当前分组的每条回线各有一个下拉(拆成新分组 / 并入第 n 组)+「分配」按钮,走 `moveChannelLine`,组数 / 回线数到上限会被拒绝并在读数里给出原因。这一轮右键框再补齐:分组列表点一下就是 `setCurrentChannelGroup`(顺带选中这一组的回线、当前组高亮),下面一排「选中该分组 / 加拐点 / 删拐点 / 拆成多组 / 合并回一组」走 `runChannelAction`(灰掉的按钮悬停能看到原因),Esc / × / 点空白处收框;这两件事都要显式开开关:`features: { drag: true, contextMenu: true }` 与 `waypointEditable: true`(开关缺省全关;通道右键框还要 `contextMenu` 开着,通常给 `features: 'view'` 再加 `contextMenu: true`)。
23
27
 
24
- 这一轮的 demo 拆成了「一个壳 + 多个标签页」:入口是 `docs/demos/DemoShell.vue` 配 `docs/demos/demos.js`(id / 标题 / 一句说明 / 懒加载的 `component`),切标签页会把 `?demo=<id>` 用 `replaceState` 写回地址栏,收藏一个链接就能直接打开某一页;顶部按基础 / 交互 / 样式 / 数据 / 性能 / 综合分组,一次只展开当前组。页签各自只演示一件事(现在是 19 个功能页 + 综合示例):`basic` 最小画布与拾取、`labels` 节点与线路标签(拖动 / 点击 / 右键 / 双击 / 批量加标签 API)、`label-display` 标签内容与显示策略、`minimap` 鹰眼图配置(尺寸 / 位置 / 范围 / 规则 / 层级 / 备份倍率 / 小图缩放 / 数据前后分段绘制 / 手势回调)、`style` 规则配色与符号、`hover-tooltip` 悬停高亮与提示模板、`channel` 通道与分组、`events` 事件日志、`copy-merge` 框选类型过滤(节点 / 线路 / 通道任意组合)、只复制选中要素、手动 `addPoint` 落点与自动副本二选一、指定保留来源 / 目标并改写最终信息与样式的合并、剪贴板、`batch-add` 多节点 / 线路 / 通道 / 分组 / 拐点的批量新增、`search-filter` 同步 / Worker 检索与筛选、`drag` 拖拽落点、`connect` 可视化连线(无通道也能只用点与线;直线 / 正交折线、箭头、颜色与线宽,取消 / 默认提交 / `addLines` / `addChannels` 三种落地)、`decoration` 动态装饰器(线路 / 节点分开开关或同时开启、悬停预览、节点偏移 / 固定 / 鼠标吸附、白名单与重复策略、确认才写数据)、`extension` 四类扩展点(插件 / 图元 / 运算符 / 导出格式,图元支持同名换画笔并当场重烘焙)、`backend` 编辑与保存、`perf` 大数据与画质档位、`effects` 动画效果、`graphic` 扩展图形(SVG 路径 / 图片 / Canvas,相机联动与运行时增删改),`full` 保留原来那份全功能 demo(缺省页签);共用数据与样式抽在 `docs/demos/shared/demoData.js` / `docs/demos/shared/demoStyle.css`,新增自测页 `docs/steps/selftest-demos.html` 逐个页签挂载一遍。
28
+ 这一轮的 demo 拆成了「一个壳 + 多个标签页」:入口是 `docs/demos/DemoShell.vue` 配 `docs/demos/demos.js`(id / 标题 / 一句说明 / 懒加载的 `component`),切标签页会把 `?demo=<id>` 用 `replaceState` 写回地址栏,收藏一个链接就能直接打开某一页;顶部按基础 / 交互 / 样式 / 数据 / 性能 / 综合分组,一次只展开当前组。页签各自只演示一件事(现在是 21 个功能页 + 综合示例):`basic` 最小画布与拾取、`labels` 节点与线路标签(拖动 / 点击 / 右键 / 双击 / 批量加标签 API)、`label-display` 标签内容与显示策略、`minimap` 鹰眼图配置(尺寸 / 位置 / 范围 / 规则 / 层级 / 备份倍率 / 小图缩放 / 数据前后分段绘制 / 手势回调)、`style` 规则配色与符号、`hover-tooltip` 悬停高亮与提示模板、`channel` 通道与分组、`events` 事件日志、`api-coverage` 接口能力(级联三种触发、程序化选中、四类 `focusOn`、装饰 / 通道写入、背景投影、历史与运行时清理)、`copy-merge` 框选类型过滤(节点 / 线路 / 通道任意组合)、只复制选中要素、手动 `addPoint` 落点与自动副本二选一、指定保留来源 / 目标并改写最终信息与样式的合并、剪贴板、`batch-add` 多节点 / 线路 / 通道 / 分组 / 拐点的批量新增、`search-filter` 同步 / Worker 检索与筛选、`drag` 拖拽落点、`connect` 可视化连线(无通道也能只用点与线;直线 / 正交折线、箭头、颜色与线宽,取消 / 默认提交 / `addLines` / `addChannels` 三种落地)、`decoration` 动态装饰器(线路 / 节点分开开关或同时开启、悬停预览、节点偏移 / 固定 / 鼠标吸附、白名单与重复策略、确认才写数据)、`extension` 六类扩展点(插件 / 图元 / 运算符 / 导出格式 / WebGL 自定义层 / 实例命令,图元支持同名换画笔并当场重烘焙)、`backend` 编辑与保存、`perf` 大数据与画质档位、`cluster` 点聚合(2 千 / 1.2 万 / 6 万 / 10 万点现场切,调半径 / 最少点数 / `maxZoom`,看聚合耗时与点击载荷)、`effects` 动画效果、`graphic` 扩展图形(SVG 路径 / 图片 / Canvas,相机联动与运行时增删改),`full` 保留原来那份全功能 demo(缺省页签);共用数据与样式抽在 `docs/demos/shared/demoData.js` / `docs/demos/shared/demoStyle.css`,新增自测页 `docs/steps/selftest-demos.html` 逐个页签挂载一遍。
25
29
 
26
- 演示外壳会缓存已经加载过的模块;当前演示渲染完成后,利用空闲时间预取前后两个页签,失败时显示错误与「重新加载」,切页签不再重复下载。新增的 `extension` 页把 `view.use` / `registerSymbol` / `registerOperator` / `defineExportFormat` 四个扩展点放在同一块画布里,并给出注册表、插件挂载状态与业务 DTO 的同屏读数。
30
+ 演示外壳会缓存已经加载过的模块;当前演示渲染完成后,利用空闲时间预取前后两个页签,失败时显示错误与「重新加载」,切页签不再重复下载。新增的 `extension` 页把 `view.use` / `registerSymbol` / `registerOperator` / `defineExportFormat` / `addWebGLLayer` / `registerCommand` 六个扩展点放在同一块画布里,并给出注册表、插件挂载状态、业务 DTO、WebGL 层与实例命令的同屏读数。
27
31
 
28
32
  源码本身就是 JS:`src/gis-view/*.js` 逐文件带中文注释,对外类型集中在 `src/gis-view/types.d.ts`(构建时复制成 `dist/index.d.ts`),不用先编译就能直接读。想照着从零写一遍看 [从 0 到完整的实现](docs/从0到完整的实现.md),里面有一份可以照抄的 mini 实现与验收清单。
29
33
 
30
- 想知道组件到底有哪些属性和方法:[GisView 文档](docs/GisView.md) 的「实例方法」一节把 249 个实例成员(方法 + `el` / `canvas` / `gl` 这类属性)与 92 个 `options` 键逐个列全;demo 的「视图 · 交互」页签里有同一份清单(`?demo=full`,可搜索、点一条就对当前视图求值);`node docs/steps/check-doc-coverage.mjs` 负责 10 项双向对账,改完源码或文档先跑 `node docs/steps/gen-api-index.mjs` 重生成 demo 读的那一份(三类查询的口径见 [docs/steps/README.md](docs/steps/README.md) 的「怎么查全部属性与方法」)。
34
+ 想知道组件到底有哪些属性和方法:[GisView 文档](docs/GisView.md) 的「实例方法」一节把 256 个实例成员(方法 + `el` / `canvas` / `gl` 这类属性)与 96 个 `options` 键逐个列全;demo 的「视图 · 交互」页签里有同一份清单(`?demo=full`,可搜索、点一条就对当前视图求值);`node docs/steps/check-doc-coverage.mjs` 负责 10 项双向对账,改完源码或文档先跑 `node docs/steps/gen-api-index.mjs` 重生成 demo 读的那一份(三类查询的口径见 [docs/steps/README.md](docs/steps/README.md) 的「怎么查全部属性与方法」)。
31
35
 
32
36
  ## 构建 npm 包
33
37
 
@@ -121,8 +125,8 @@ const view = createGisView('#map', {
121
125
  // 把某一组拉走:拆分之后各组已经错开,想把某一组拐到别的走廊上就给第 2…n 组加一个拐点(第 1 组原地不动)
122
126
  // view.addChannelGroupWaypoint('corridor-1', 'corridor-1::2')
123
127
  // view.removeChannelGroupWaypoint('corridor-1', 'corridor-1::2')
124
- // 右键通道回线:onChannelContextMenu 给出「几组 / 当前哪组 / 每组几条回线与可用交互」,点空白处收到 null,用它收框
125
- // onChannelContextMenu: (context, event) => { if (context) view.setCurrentChannelGroup(context.channelId, context.groups[1].id) }
128
+ // 右键通道回线:onContextMenu 在 payload.channelContext 里给出「几组 / 当前哪组 / 每组几条回线与可用交互」,点空白 / 节点时没有这份上下文,用它收框
129
+ // onContextMenu: (payload, event) => { const context = payload.channelContext; if (context) view.setCurrentChannelGroup(context.channelId, context.groups[1].id) }
126
130
  // view.runChannelAction('add-waypoint', { channelId: 'corridor-1', groupId: 'corridor-1::2' })
127
131
  // 新增通道:点两个节点出草稿(onChannelDraft 给两个节点的信息与可选方案),或绕开交互直接加一条
128
132
  // view.beginAddChannel({ autoCommit: true }); view.addChannel({ from: 'station-a', to: 'station-b' })
@@ -207,7 +211,7 @@ view.setLabelStates(view.getLabelStates()) // 持久化结果整份灌回来(
207
211
  // 点标签的载荷:label 是这一条标签自己(id / content / source / 合并后的位置与样式),parent 是它挂在哪条要素
208
212
  // 线路在通道里时 parent.channel 给 { id, groupId, index, slot, item } ——「点的是哪条通道的哪一回线」直接读
209
213
  view.on('labelClick', payload => console.log(payload.label.id, payload.label.content, payload.parent.channel?.groupId))
210
- // 右键默认把浏览器的系统菜单收掉(onContextMenu / onChannelContextMenu 照常发);想留系统菜单写 false
214
+ // 右键默认把浏览器的系统菜单收掉(onContextMenu 照常发);想留系统菜单写 false
211
215
  view.updateOptions({ preventContextMenu: false })
212
216
 
213
217
  // 导出带 labels 列(缺省 'auto' 只给有标签要说的记录带):记录原样喂回 points / lines 就能回显
@@ -215,14 +219,17 @@ const rows = view.exportData({ parts: ['station'], labels: true }).points
215
219
  view.setFilter(null)
216
220
  ```
217
221
 
218
- 关于 `html: false`:它只关闭字符串 `content` 的 HTML 解析,标签容器仍然是逐条的 `.gis-view__label`(默认 `div`),所以单靠它不会减少 DOM —— 几万条纯文本标签仍会建几万个节点。顶层 `labelRender` 控制这个容器,缺省 `'auto'`:在强制显示档(`labelOptimize` 未开、`denseMode` 未激活)下,节点层或线路层里能画布的纯文本标签达到 1024 条时,该层自动改画到一张 Canvas,不再逐条建 `div`;`html: true`、函数返回元素、可拖拽 / 可编辑及 Canvas 画不全的样式仍留在 DOM。要求保持旧行为可写 `labelRender: 'dom'`,运行时用 `updateOptions({ labelRender })` 切换。
222
+ 关于 `html: false`:它只关闭字符串 `content` 的 HTML 解析,标签容器仍然是逐条的 `.gis-view__label`(默认 `div`),所以单靠它不会减少 DOM —— 几万条纯文本标签仍会建几万个节点。顶层 `labelRender: 'auto' | 'canvas' | 'dom'` 控制这个容器,缺省 `'auto'`:在强制显示档(`labelOptimize` 未开、`denseMode` 未激活)下,节点层或线路层里能画布的纯文本标签达到 1024 条时,该层自动改画到一张 Canvas;要求优化档也画布时写 `'canvas'`,它不设数量阈值,`maxCount` / 避让仍照常执行。`html: true`、函数返回元素、可拖拽 / 可编辑及 Canvas 画不全的样式仍留在 DOM。要求保持旧行为可写 `labelRender: 'dom'`,运行时用 `updateOptions({ labelRender })` 切换。
219
223
 
220
224
  搜索与相机定位(搜索只找不藏,筛选才负责隐藏):
221
225
 
222
226
  ```js
223
- // 搜索:scope 可以是 points / lines / channels,通道会连成员回线一起搜
227
+ // 搜索:scope 可以是 points / lines / channels / groups(也认 point / line / channel / group),通道会连成员回线一起搜
224
228
  const result = view.search({ keyword: '望京', scope: 'channels', limit: 50 })
225
- view.focusOn(result.hits[0]) // 命中、要素 id、{ type, index }、坐标或矩形都能直接定位
229
+ view.focusOn(result.hits[0]) // 缺省只居中,比例不动
230
+ view.focusOn(result.hits[0], { zoom: true }) // 显式按命中范围适配
231
+ const groupHit = view.search({ keyword: 'c1::2', scope: 'group' }).groups[0]
232
+ view.focusOn(groupHit, { select: true, zoom: true }) // 分组命中按整组范围适配,并选中成员回线
226
233
 
227
234
  // 相机:zoomTo 直接给比例,zoomToBounds 把一段世界坐标范围适应到视图内
228
235
  view.zoomTo(12).zoomToBounds({ minX: 116, minY: 39, maxX: 117, maxY: 40 }, 60)
@@ -246,7 +253,7 @@ view.zoomTo(12).zoomToBounds({ minX: 116, minY: 39, maxX: 117, maxY: 40 }, 60)
246
253
  - 点支持圆形、正方形(`radius` 给圆角,0 就是直角方形)、SVG 路径与图片符号,前两种由着色器按距离场直接画,缩放与整体放大都不会糊,可单独配置大小、颜色、描边、透明度与阴影;`size` 是节点的宽度,圆按面积对齐正方形(直径 = 宽度 × `2 / √π`),同一个 `size` 下圆与方的视觉重量一致;线支持颜色、粗细、透明度、端点、折线 `path` 与阴影。
247
254
  - 背景层支持底色、图片(`bounds` / `fit` / `opacity` / `shadow`)与 GeoJSON(填充色、填充透明度、描边色、线宽、点颜色与大小、整体透明度、阴影)。
248
255
  - 通道与多回线:`channels` 把若干条线路组织成并行敷设的走廊,回线几何由共用骨架按偏移推导,二 / 三 / 四 / 五回线都能等距排开;偏移方式可选最外侧切点均分(`tangent`)或从切点再向内收一段距离(`inset`),拖拐点整条通道同步折线;一条通道默认一个分组,可以拆成多组(每组至少一条回线,新拆出来的组不带拐点)再合并回去(被并入的线路跟随目标分组的拐点,目标没有拐点就一条都不带),加点 / 拖点只作用于同一分组,见 docs/GisView.md 的「通道分组」。通道两端可以用 `from` / `to` 绑在节点上:两端都有节点才显示,拖动节点两端跟着走,节点没了通道与回线一起消失;两端的切点还会默认往节点里收一个线宽(`channelTangentInset`,`0` 表示正好落在节点边上),线不会从节点边缘探出去。
249
- - 筛选只改表现不改数据:条件里出现的字段会被投影成扁平数组(`fields` 由条件语言静态提取),`getVisibleData()` 给使用方拿过滤后的数据、`listPoints({ visibleOnly: true })` 给可见节点快照,`onFilter` 回报隐藏下标、可见数量、是否用了 Worker 与耗时;通道两端锚在节点上,端点被过滤时整条通道与回线一起消失。
256
+ - 筛选默认只改表现、不删数据,也可以显式删除:条件里出现的字段会被投影成扁平数组(`fields` 由条件语言静态提取),`getVisibleData()` 给使用方拿过滤后的数据、`listPoints({ visibleOnly: true })` 给可见节点快照,`onFilter` 回报隐藏下标、可见数量、是否用了 Worker 与耗时;通道两端锚在节点上,端点被过滤时整条通道与回线一起消失。`mode: 'hide'`(缺省)只隐藏;`mode: 'remove'` 先删线路再删节点,只发一条 `filterRemove`,可撤销,历史回放不会把恢复的数据再删一次。
250
257
  - 大数据量交互不吃亏:点的拾取与框选走随数据范围重建的均匀网格索引,线也走随线段重建的网格索引,每次只比较光标附近的候选,耗时与总量基本脱钩,读数可以直接看 `getStats().pickMs`;悬停、选中、拖拽都只做增量解析与增量上传,拐点默认只显示选中线路的,10 万点 / 10 万线的交互量与要素总量基本无关。
251
258
  - 10 万级交互的专项优化:框选的选中集合用 `Set` 做差集(避免平方级比较),整组拖动先按实例槽位排序再按「连续槽位段」合并上传实例数据(十万次 `bufferSubData` 变成几十次,贴图符号点也走自己的槽位表,符号没换就地改位置)、拖动大组时圆头批次改为松手后分摊补齐(拖动期间一个字节都不传),按住拖动 / 滚轮缩放期间自动切轻量渲染(跳过阴影、强调环与高亮重算,停手 160 ms 后补一帧全质量);标签图层在交互期只做一次整体 `transform`、停手 180 ms 后才真实排版,拖点只挪动过的那几个标签;框选抬起时候选下标先按数值排序、差值去重键用数值、圆 / 方符号解析有热缓存、可见点缓存按需作废。demo 里的统计面板按最小间隔重画,避免每帧写响应式数据。这一轮又补了三处:初始化按到视图中心的距离发散渲染(10 万点首屏 1 帧内先出中心一片、3 帧内铺满)、整组拖动在 2048 个节点以上改成就地改坐标并复用事件载荷(10 万点整组拖动每帧 28 ms → 11 ms)、标签在连续平移里每 220 ms 补一次真实排版并把位置取整到像素(拖动中标签不再越拖越空,静止时文字不糊)。
252
259
  - 整组拖动的增量补丁这一轮补齐到每个实例批次:10 万点 / 10 万线整屏框选后连续拖动,每帧补丁 1391 ms → 339 ms(圆头那部分 8 帧合计 9.8 s 在拖动期间归零,松手后 4 帧内分摊补完);规则条件的运算符结果按值记忆,`vn_kv >= 500` 这类分档条件在 10 万级数据上从逐要素求值变成查表。
@@ -256,7 +263,7 @@ view.zoomTo(12).zoomToBounds({ minX: 116, minY: 39, maxX: 117, maxY: 40 }, 60)
256
263
  - 发散渲染与导出:2 万点以上默认从视图中心向外分帧铺开(`progressiveInit`,铺完前 `isLayoutReady()` 返回 `false`,标签与拾取只看已经画出去的那部分),首屏一帧内先出中心附近的一片;`exportImage({ scale, background, download })` 把画布内容与标签合成一张图导出(返回 data URL,`download: true` 直接下载)。
257
264
  - 这一轮(指针事件、连续新增与移除新增):鼠标的每个动作都有自己的回调 —— 悬停 `onHover`、左键单击 `onClick`、双击 `onDblClick`,外加通用的 `onPointerDown` / `onPointerUp`(左 / 中 / 右键按下与抬起)与 `onContextMenu`(每一次右键),后三者发同一种 `GisViewPointerPayload`:命中的要素(`type` / `index` / `item`,通道回线还带 `channelId` / `groupId` / `channelIndex`)、按钮(`button` / `buttons`)与光标位置(`world` / `x` / `y` / `clientX` / `clientY`),空白处 `hit` 为 false,要自己弹菜单就在回调里 `preventDefault()`;新增通道补上「每点一次都回调」的 `onChannelDraftClick`(`step` 为 1 是拿到起点、2 是拿到终点,载荷与 `onChannelDraft` 一致)与连续新增:`beginAddChannel({ continuous: true })` 落地之后草稿原地重开(发 `phase: 'restart'`),接着点下一对节点就能一直加,`continuous: 'chain'` 再以上一段的终点当下一段起点,`continueAddChannel()` 手动开下一段、`cancelAddChannel()` 退出草稿;新增错了用 `removeLine(lineId)` 把刚落地的那条回线摘掉(通道声明一起改,通道空了整条删掉),整条通道走 `removeChannel(channelId)`,两者都会把选中下标跟着往前挪。
258
265
  - 性能实测与 WebGPU 的结论见 [GisView 实现原理](docs/GisView实现原理.md) 第 18 章:10 万点 / 10 万线的连续平移,标签全开最初只有 0.8 FPS、关掉标签 10.3 FPS;加上标签候选上限(`label.scanLimit`)回到 36.5 / 48.9 FPS,再把标签图层做成「交互期整体变换 + 停手后重排」,平移 / 缩放 / 拖点分别到 57.9 / 57.9 / 55.0 FPS(与关掉标签同档),整屏框选约 9.9 万个节点从 97 ms 降到 52 ms;整组拖动的增量补丁与圆头欠账见效之后,10 万选中连续拖动每帧 1391 ms → 339 ms;引擎每帧渲染函数耗时一直只有 0.05 ~ 0.2 ms。瓶颈在 DOM 覆盖层与 JS 侧,换 WebGPU 省不到这笔开销,本轮不迁移;这一轮把它收成了 `renderer` 开关(`auto` / `webgl2` / `webgpu`)与 `getRendererInfo()` 读数,真要迁移的路线写在文档里。
259
- - 搜索与定位和筛选分开:`search()` 只找不藏(可限定 `points` / `lines` / `channels`,通道连回线一起搜并给出 `memberIndex` / `memberLineIndex`,见 `docs/GisView.md`),**但不再自己动视角** —— 搜到之后要不要聚焦、要不要选中由使用方决定(`GisViewDemo` 的搜索面板就是一个「搜到就聚焦 + 选中」的勾选框);`focusOn(target, options)` 定位到命中 / id / 坐标 / 矩形(给数组就是一批整体居中,`{ center: true }` 只居中,`select: true` 顺带选中),相机另有 `zoomTo(scale, origin?)` 与 `zoomToBounds(bounds, padding?)`。
266
+ - 搜索与定位和筛选分开:`search()` 只找不藏(可限定 `points` / `lines` / `channels` / `groups`,也认与 `focusOn` 一致的单数别名;通道连回线一起搜并给出 `memberIndex` / `memberLineIndex`,分组命中给 `channelIndex` / `groupId` / `groupIndex`,见 `docs/GisView.md`),**但不再自己动视角** —— 搜到之后要不要聚焦、要不要选中由使用方决定(`GisViewDemo` 的搜索面板就是一个「搜到就聚焦 + 选中」的勾选框);`focusOn(target, options)` 缺省只把中心移到目标上、比例一点不动,`{ zoom: true }` 才按目标范围走 `zoomToBounds` 适配,`{ center: true }` 保留显式只居中写法,`select: true` 顺带选中;节点 / 线路 / 通道 / 分组共用这条适配路径、没有额外放大倍率。`setFilter` 默认只隐藏显示,不删数据、也不清空选中集;显式写 `mode: 'remove'` 才从数据里删除,搜索随后也搜不到被删项。相机另有 `zoomTo(scale, origin?)` 与 `zoomToBounds(bounds, padding?)`。
260
267
  - 预览能力:鹰眼图是唯一内置的界面(配 `minimap` 才有,见 `docs/GisView.md` 的「鹰眼图」;底图先铺线、再把节点整层盖上去,节点层级高于线路;小图自己也能套规则:`pointRules` / `lineRules` 给每个节点 / 每条线定颜色、大小与层级,`levels` 只画某几层、`layered` 分层叠画、`pointZoomScale` / `lineZoomScale` 跟着小图倍数放大,没配自己的规则就用主图的颜色与层级),要更自由的实现仍然交给使用方——`getBounds()` 给全量范围、`getViewRect()` 给当前视口矩形,在小画布上按比例画出来并回写 `setCamera()`,需要缩放与框选放大时用 `project` / `unproject` 在两端换算即可。
261
268
  - 这一轮(demo 整理、标签显隐规则、全屏与规则更新):demo 页面重新排成「左边画布、右边配置」两列,说明段落默认收起、配置按组折进可折叠面板,画布右上角的「全屏」按钮走 `requestFullscreen()`(进出都等布局稳定后 `view.resize()`,按 Esc 退出也认);节点标签默认只给电压最高的那一档打(阈值取当前数据里最高的 `vn_kv`,new.json / test.json 都是 1050kV),面板上的「显示范围」还能切 ≥525kV 与全部;线路标签默认整个不显示,勾上「显示」之后再按同一套范围展开,打开 demo 不再满屏标签。新增「规则更新」面板:`pointRules` / `lineRules` 可以在文本框里直接改 JSON(或选面板配置 / 单色 / 高电压突出三份预设),点「更新规则并重渲染」把新规则表交给 `updateOptions`,读数里给出规则条数与整份重排耗时 —— 自定义规则生效期间面板上的样式开关不再抢着下发规则。
262
269
  - 这一轮(demo 分页与底图、初始视图、插件与三处性能优化):`initialView` 决定首次加载的相机落位(`{ mode, center, scale, zoom, bounds, padding }`,不配仍是适应数据范围),实例方法 `applyInitialView()` 在换数据 / 改配置之后重新套用同一份落位,见 `docs/GisView.md` 的「初始视图」;`plugins: [view => hook]` 是插件挂载点 —— 初始化按数组顺序执行,返回的 `destroy` 在视图销毁时倒序回调,单个插件抛错只走 `onError`,demo 里的 F 键聚焦就是一份完整示例;性能上 `drawGrid` 复用顶点缓冲(范围 / 步长 / 颜色不变就跳过重建)、`onStats` 按新增的 `statsInterval`(默认 250 ms)节流、悬停拾取合并到一帧一次。`docs/steps` 新增第 11 步 `step-11-background.js` 演示背景层 + 初始视图,步骤页工具条上多了「切换底图 / 切换初始视图」两个按钮。
@@ -293,7 +300,7 @@ view.zoomTo(12).zoomToBounds({ minX: 116, minY: 39, maxX: 117, maxY: 40 }, 60)
293
300
 
294
301
  - 这一轮(2 条需求:继续优化大数据性能并按场景把 WebGPU 做成开关 / 统一接收与导出格式并给一份包含全部数据的 JSON):① **标准数据文档**(`src/gis-view/document.js`)—— 顶层固定八列(`format` / `version` / `points` / `lines` / `channels` / `labelStates` / `view` / `filter`),节点 / 线路 / 通道 / 拐点(`path` 中间点)/ 标记(`decorations`)/ 标签(`labels`)与标签的运行时状态、相机、筛选全在里面;`normalizeDocument()` 做深拷贝 + JSON 安全(函数 / DOM / 画布 / `Map` / 循环引用跳过并记提醒,SVG → outerHTML、图片 → `{ type: 'image', src }`、`NaN` → null)+ 标准写法(坐标一律 `[x, y]`、`labels` / `decorations` 一律数组、通道一律展开 `groups`)+ 键序保留。整份文档可以直接当 `createGisView` 的入参(`view.camera` → `initialView`),`exportData({ format: 'full' })` 出的就是文档本身(没有 `ok` / `counts` / `warnings`),新增实例方法 `importData(input, options)` 整份 / 分列灌回(顶层出哪列应用哪列,返回 `{ ok, reason, applied, counts, warnings }`);`overlay.js` 的标签定位在要素被筛选隐藏时回落到原始数据,带筛选的文档回灌不丢标签状态。仓库根目录新增 `sample-document.json`(`scripts/build-sample-document.mjs` 从 `sample-full.json` 生成:10 点 / 22 线 / 3 通道 / 39 拐点 / 23 标记 / 44 标签 / 6 条标签状态),演示页「完整样本」多一枚「标准文档(整份)」芯片:与文件逐字节对账、导出 → 重建 → 再导出逐字节相同。② **WebGPU 做成开关**(`src/gis-view/renderer.js`):`renderer: 'auto' | 'webgl2' | 'webgpu'`(布尔值 `true` / `false` 等价于 `'webgpu'` / `'webgl2'`)加一份只碰 `navigator.gpu` 的能力探测(结果缓存、不创建画布)与实例方法 `getRendererInfo()`;当前绘制后端仍是 WebGL2(第 18 章的结论不变:十万级下 WebGPU 省不到填充率与 DOM 覆盖层的开销),`'auto'` 后台探一次不打扰使用方、`'webgl2'` 连探测都不跑、`'webgpu'` 明确报「已回退 WebGL2 + 原因」。③ **网格顶点缓存窗口**:`drawGrid` 原来每帧重建顶点并 `bufferData`,现在按「步长对齐的缓存窗口」(`GRID_WINDOW_PAD` 0.25)复用,真时间实测小步平移 40 帧只有 1 次上传(改前每帧一次)、大跳变 59 次,画面逐像素一致(16.19 已完成)。验证:新增 `docs/steps/selftest-data.html` 41 条断言全绿,十个老自测页真时间逐页全绿,`npx tsc --noEmit` 0 错、`npm run build` 通过、`node docs/steps/check-doc-coverage.mjs` 实例成员 193 / 数据字段 219 全部命中。
295
302
 
296
- - 这一轮(2 条需求:EventDemo 的合并 / 复制事件弹框 / ChannelDemo 的分组右键交互框):① **合并 / 复制弹框并解释「拖到一起鼠标为什么变」**(`docs/demos/EventDemo.vue`)—— 「拖到一起鼠标为什么会变」现在在面板上看得见:新增的光标读数直接读 `canvas.style.cursor`(事件触发 + 250ms 轮询),拖一个节点压到另一个节点上松手进「待确认合并」、Alt 点击出复制预览、`beginCopyPoint()` 等选点时都走 `cursorStyle.copy` 这一档(按住 Ctrl / ⌘ 还能用 `cursorStyle.ctrl.copy` 再换一份),读数下面写明引擎是在 `updateCursor()` 里切的档;合并 / 复制的中间态各挂一条确认条(与 `DragDropDemo` 的落点草稿同一套写法):`onPointMerge` 的 `pending`、`onPointCopy` 的 `start` / `move` 载荷存进 `shallowRef`,条上的按钮就是载荷给的裁决入口 —— 合并调 `payload.confirm()` / `payload.cancel()`,复制调 `payload.commit()` / `payload.cancel()`(Esc / 右键 / 再点一下也能收)。② **`copy.autoCommit` 真正接到交互入口**(`src/gis-view/engine.js`)—— 原先只有 `beginCopyPoint({ autoCommit: false })` 这条 API 路能停在预览态,Alt 点击与「先点按钮再选节点」两条交互路恒落地;现在 `beginPointCopy` / `insertCopyPreview` 都跟着 `settings.copy.autoCommit` 走,`copy: { autoCommit: false }` 时松开 Alt 的预览停在原地,等确认条上的 `payload.commit()` 或 `commitPointCopy()`(EventDemo 演示的就是这一条)。③ **分组右键交互框**(`docs/demos/ChannelDemo.vue`,参考 teap-gis 的 `ParallelGroupEditorDialog`)—— 显式开 `features: { drag: true, channelMenu: true }` 与 `waypointEditable: true` 之后右键通道回线出框(贴指针位置、按 `getStats()` 的 CSS 尺寸夹取,Esc / × / 点空白处收框),三段式:分组列表点一下 = `setCurrentChannelGroup`(顺带选中这一组的回线、当前组高亮)、每条回线一个下拉(拆成新分组 / 并入第 n 组)+「分配」= `moveChannelLine`(被拒的原因写进日志)、下面一排按钮 = `runChannelAction` 的五个动作(`enabled` / `reason` 由引擎算好,灰按钮的 `title` 就是原因)。验证:`npx tsc --noEmit` 0 错、`npm run build` 通过、`node docs/steps/check-doc-coverage.mjs` 实例成员 193 / 数据字段 219 全部命中;`selftest-demos.html` 新增第 4 节(合并 / 复制确认条 8 条)与第 5 节(右键交互框 7 条)断言、`selftest-features.html` 新增 5 条 `copy.autoCommit: false` 断言全绿(CDP 真时间:`selftest-demos` 74 通过 / `selftest-features` 264 通过);`selftest-demos` 里「标签交互」那条删除断言(删完标签数 13 → 14)在未改动的 HEAD 上同样失败,是既有问题,与本轮无关。
303
+ - 这一轮(2 条需求:EventDemo 的合并 / 复制事件弹框 / ChannelDemo 的分组右键交互框):① **合并 / 复制弹框并解释「拖到一起鼠标为什么变」**(`docs/demos/EventDemo.vue`)—— 「拖到一起鼠标为什么会变」现在在面板上看得见:新增的光标读数直接读 `canvas.style.cursor`(事件触发 + 250ms 轮询),拖一个节点压到另一个节点上松手进「待确认合并」、Alt 点击出复制预览、`beginCopyPoint()` 等选点时都走 `cursorStyle.copy` 这一档(按住 Ctrl / ⌘ 还能用 `cursorStyle.ctrl.copy` 再换一份),读数下面写明引擎是在 `updateCursor()` 里切的档;合并 / 复制的中间态各挂一条确认条(与 `DragDropDemo` 的落点草稿同一套写法):`onPointMerge` 的 `pending`、`onPointCopy` 的 `start` / `move` 载荷存进 `shallowRef`,条上的按钮就是载荷给的裁决入口 —— 合并调 `payload.confirm()` / `payload.cancel()`,复制调 `payload.commit()` / `payload.cancel()`(Esc / 右键 / 再点一下也能收)。② **`copy.autoCommit` 真正接到交互入口**(`src/gis-view/engine.js`)—— 原先只有 `beginCopyPoint({ autoCommit: false })` 这条 API 路能停在预览态,Alt 点击与「先点按钮再选节点」两条交互路恒落地;现在 `beginPointCopy` / `insertCopyPreview` 都跟着 `settings.copy.autoCommit` 走,`copy: { autoCommit: false }` 时松开 Alt 的预览停在原地,等确认条上的 `payload.commit()` 或 `commitPointCopy()`(EventDemo 演示的就是这一条)。③ **分组右键交互框**(`docs/demos/ChannelDemo.vue`,参考 teap-gis 的 `ParallelGroupEditorDialog`)—— 显式开 `features: { drag: true, contextMenu: true }` 与 `waypointEditable: true` 之后右键通道回线出框(贴指针位置、按 `getStats()` 的 CSS 尺寸夹取,Esc / × / 点空白处收框),三段式:分组列表点一下 = `setCurrentChannelGroup`(顺带选中这一组的回线、当前组高亮)、每条回线一个下拉(拆成新分组 / 并入第 n 组)+「分配」= `moveChannelLine`(被拒的原因写进日志)、下面一排按钮 = `runChannelAction` 的五个动作(`enabled` / `reason` 由引擎算好,灰按钮的 `title` 就是原因)。验证:`npx tsc --noEmit` 0 错、`npm run build` 通过、`node docs/steps/check-doc-coverage.mjs` 实例成员 193 / 数据字段 219 全部命中;`selftest-demos.html` 新增第 4 节(合并 / 复制确认条 8 条)与第 5 节(右键交互框 7 条)断言、`selftest-features.html` 新增 5 条 `copy.autoCommit: false` 断言全绿(CDP 真时间:`selftest-demos` 74 通过 / `selftest-features` 264 通过);`selftest-demos` 里「标签交互」那条删除断言(删完标签数 13 → 14)在未改动的 HEAD 上同样失败,是既有问题,与本轮无关。
297
304
 
298
305
  - 这一轮(5 条需求:选中抬层 / 直流标签 svg 图标 / Delete 删除(带确认框、开关、快捷键与事件)/ 通道选中模式 / 复制改为松开左键才结束):① **选中抬层**复核是引擎原有能力(`selectionOnTop` + `raisedPointIndexes` / `raisedLineIndexes`),没有代码改动。② **直流标签 svg**:`docs/demos/gisStyle.js` 的 `dcLineDecorationSymbol` 去掉 `tint`(白底被逐实例染成线路色 → 实心色块),装饰图标恢复成「白底 + 彩色图案」。③ **删除**:`engine.js` 新增 `normalizeRemoveOptions`(`enabled` / `confirm` / `scope`),`features.js` 补第 18 个开关 `remove`,`events.js` 补 `remove` 事件;`create.js` 抽出 `removeRefs` 并新增 `buildRemovePlan` / `removeImpactOfPoint` / `removeLineIdsOfScope` / `executeRemove` / `settleRemove` / `requestRemove` 与实例方法 `requestRemove` / `getRemoveRequest` / `resolveRemove` / `cancelRemove` —— Delete 与接口先出一份载荷(删什么 / 连带什么 / 「只删线路 / 整个分组 / 整条通道」三档范围 / counts 读数),`remove.confirm` 三档裁决(缺省停在 pending 由使用方弹框),一次删除只落一条历史、只发一条 `removeSelected`,确认时数据跟载荷对不上就整条作废;开关关着退回老行为(只删选中的拐点),`shortcuts.remove` 单独可配。④ **通道选中模式**:`channelSelectMode`(`'line'` 缺省 / `'group'` / `'channel'`)与引擎的 `expandChannelSelection`,点击 / Ctrl 点击按模式摊开与整批取反,`updateOptions` 只重画一帧。⑤ **复制落地口径**:松开左键才落地(`handlePointerUp` + `via: 'shortcut'` + `autoCommit`),松开 Alt 不再落地,`EventDemo.vue` 的确认条改成落地之后(`phase: 'end'`)才弹。验证:`npx tsc --noEmit` 0 错、`npm run build` 通过、`node docs/steps/check-doc-coverage.mjs` 实例成员 197 / 数据字段 221 全命中;`selftest-features.html` 284 条断言全绿,`selftest-demos.html` 80 PASS(唯一 FAIL 是 LabelDemo 既有的标签计数问题,与本轮无关)。
299
306
 
package/dist/gis-view.css CHANGED
@@ -1 +1 @@
1
- .gis-view{box-sizing:border-box;--gis-view-cursor-grab:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><path d='M12 2 16 6 13 6 13 11 18 11 18 8 22 12 18 16 18 13 13 13 13 18 16 18 12 22 8 18 11 18 11 13 6 13 6 16 2 12 6 8 6 11 11 11 11 6 8 6Z' fill='%23ffffff' stroke='%23ffffff' stroke-width='3' stroke-linejoin='round'/><path d='M12 2 16 6 13 6 13 11 18 11 18 8 22 12 18 16 18 13 13 13 13 18 16 18 12 22 8 18 11 18 11 13 6 13 6 16 2 12 6 8 6 11 11 11 11 6 8 6Z' fill='%2325313f'/></svg>") 12 12, grab;--gis-view-cursor-grabbing:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><path d='M12 2 16 6 13 6 13 11 18 11 18 8 22 12 18 16 18 13 13 13 13 18 16 18 12 22 8 18 11 18 11 13 6 13 6 16 2 12 6 8 6 11 11 11 11 6 8 6Z' fill='%23ffffff' stroke='%23ffffff' stroke-width='3' stroke-linejoin='round'/><path d='M12 2 16 6 13 6 13 11 18 11 18 8 22 12 18 16 18 13 13 13 13 18 16 18 12 22 8 18 11 18 11 13 6 13 6 16 2 12 6 8 6 11 11 11 11 6 8 6Z' fill='%232f6fd0'/></svg>") 12 12, grabbing;--gis-view-cursor-move:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><path d='M12 2 16 6 13 6 13 11 18 11 18 8 22 12 18 16 18 13 13 13 13 18 16 18 12 22 8 18 11 18 11 13 6 13 6 16 2 12 6 8 6 11 11 11 11 6 8 6Z' fill='%23ffffff' stroke='%23ffffff' stroke-width='3' stroke-linejoin='round'/><path d='M12 2 16 6 13 6 13 11 18 11 18 8 22 12 18 16 18 13 13 13 13 18 16 18 12 22 8 18 11 18 11 13 6 13 6 16 2 12 6 8 6 11 11 11 11 6 8 6Z' fill='%23c2761b'/></svg>") 12 12, move;--gis-view-cursor-box:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><path d='M12 3.5V9M12 15v5.5M3.5 12H9M15 12h5.5' fill='none' stroke='%23ffffff' stroke-width='4.6' stroke-linecap='round'/><circle cx='12' cy='12' r='2.8' fill='%23ffffff'/><path d='M12 3.5V9M12 15v5.5M3.5 12H9M15 12h5.5' fill='none' stroke='%2325313f' stroke-width='1.8' stroke-linecap='round'/><circle cx='12' cy='12' r='1.4' fill='%2325313f'/></svg>") 12 12, crosshair;--gis-view-cursor-copy:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><g fill='none' stroke-linejoin='round'><rect x='4' y='4' width='11' height='11' rx='2' stroke='%23ffffff' stroke-width='5.4'/><rect x='9' y='9' width='11' height='11' rx='2' stroke='%23ffffff' stroke-width='5.4'/><rect x='4' y='4' width='11' height='11' rx='2' stroke='%2325313f' stroke-width='2'/><rect x='9' y='9' width='11' height='11' rx='2' stroke='%2325313f' stroke-width='2'/></g></svg>") 12 12, copy;--gis-view-cursor-point:pointer;--gis-view-cursor-line:pointer;--gis-view-cursor-channel:pointer;--gis-view-cursor-waypoint:pointer;outline:none;width:100%;height:100%;position:relative;overflow:hidden}.gis-view__canvas{touch-action:none;width:100%;height:100%;display:block}.gis-view__graphics{pointer-events:none;z-index:1;isolation:isolate;position:absolute;inset:0;overflow:hidden}.gis-view__graphic{pointer-events:none;will-change:transform, left, top, width, height;position:absolute;top:0;left:0}.gis-view__graphic-image{pointer-events:none;-webkit-user-select:none;user-select:none}.gis-view__labels{pointer-events:none;z-index:2;contain:layout style paint;isolation:isolate;transform-origin:0 0;will-change:transform;position:absolute;inset:0;overflow:hidden}.gis-view__label-layer{position:absolute;inset:0}.gis-view__label-layer--hidden{visibility:hidden}.gis-view__label-canvas{pointer-events:none;position:absolute;top:0;left:0}.gis-view__label{white-space:nowrap;color:#1f3550;background:#ffffffe0;border-radius:4px;padding:1px 6px;font-size:12px;line-height:16px;position:absolute;top:0;left:0}.gis-view__label--draggable,.gis-view__label--editable{pointer-events:auto;touch-action:none}.gis-view__label--draggable{cursor:grab}.gis-view__label--editable{cursor:pointer}.gis-view__label--dragging{cursor:grabbing;z-index:2;-webkit-user-select:none;user-select:none}.gis-view__background-label{white-space:nowrap;pointer-events:none;text-shadow:0 1px 2px #ffffffe6,0 0 2px #fffc;position:absolute;top:0;left:0}.gis-view__tooltip{color:#f3f7fc;white-space:pre-line;pointer-events:none;will-change:transform;background:#172436eb;border-radius:6px;max-width:280px;padding:6px 10px;font-size:12px;line-height:18px;position:absolute;top:0;left:0}.gis-view__tooltip[hidden]{display:none}.gis-view__minimap{z-index:2;box-sizing:border-box;contain:layout paint;isolation:isolate;touch-action:none;cursor:default;position:absolute;overflow:hidden}.gis-view__minimap[hidden]{display:none}.gis-view__minimap-canvas{width:100%;height:100%;display:block}.gis-view__minimap-box{box-sizing:border-box;pointer-events:none;will-change:transform;position:absolute;top:0;left:0}.gis-view__minimap-select{box-sizing:border-box;pointer-events:none;will-change:transform;background:#3678bf1f;border:1px dashed #3678bf;display:none;position:absolute;top:0;left:0}
1
+ .gis-view{box-sizing:border-box;--gis-view-cursor-grab:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><path d='M12 2 16 6 13 6 13 11 18 11 18 8 22 12 18 16 18 13 13 13 13 18 16 18 12 22 8 18 11 18 11 13 6 13 6 16 2 12 6 8 6 11 11 11 11 6 8 6Z' fill='%23ffffff' stroke='%23ffffff' stroke-width='3' stroke-linejoin='round'/><path d='M12 2 16 6 13 6 13 11 18 11 18 8 22 12 18 16 18 13 13 13 13 18 16 18 12 22 8 18 11 18 11 13 6 13 6 16 2 12 6 8 6 11 11 11 11 6 8 6Z' fill='%2325313f'/></svg>") 12 12, grab;--gis-view-cursor-grabbing:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><path d='M12 2 16 6 13 6 13 11 18 11 18 8 22 12 18 16 18 13 13 13 13 18 16 18 12 22 8 18 11 18 11 13 6 13 6 16 2 12 6 8 6 11 11 11 11 6 8 6Z' fill='%23ffffff' stroke='%23ffffff' stroke-width='3' stroke-linejoin='round'/><path d='M12 2 16 6 13 6 13 11 18 11 18 8 22 12 18 16 18 13 13 13 13 18 16 18 12 22 8 18 11 18 11 13 6 13 6 16 2 12 6 8 6 11 11 11 11 6 8 6Z' fill='%232f6fd0'/></svg>") 12 12, grabbing;--gis-view-cursor-move:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><path d='M12 2 16 6 13 6 13 11 18 11 18 8 22 12 18 16 18 13 13 13 13 18 16 18 12 22 8 18 11 18 11 13 6 13 6 16 2 12 6 8 6 11 11 11 11 6 8 6Z' fill='%23ffffff' stroke='%23ffffff' stroke-width='3' stroke-linejoin='round'/><path d='M12 2 16 6 13 6 13 11 18 11 18 8 22 12 18 16 18 13 13 13 13 18 16 18 12 22 8 18 11 18 11 13 6 13 6 16 2 12 6 8 6 11 11 11 11 6 8 6Z' fill='%23c2761b'/></svg>") 12 12, move;--gis-view-cursor-box:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><path d='M12 3.5V9M12 15v5.5M3.5 12H9M15 12h5.5' fill='none' stroke='%23ffffff' stroke-width='4.6' stroke-linecap='round'/><circle cx='12' cy='12' r='2.8' fill='%23ffffff'/><path d='M12 3.5V9M12 15v5.5M3.5 12H9M15 12h5.5' fill='none' stroke='%2325313f' stroke-width='1.8' stroke-linecap='round'/><circle cx='12' cy='12' r='1.4' fill='%2325313f'/></svg>") 12 12, crosshair;--gis-view-cursor-copy:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><g fill='none' stroke-linejoin='round'><rect x='4' y='4' width='11' height='11' rx='2' stroke='%23ffffff' stroke-width='5.4'/><rect x='9' y='9' width='11' height='11' rx='2' stroke='%23ffffff' stroke-width='5.4'/><rect x='4' y='4' width='11' height='11' rx='2' stroke='%2325313f' stroke-width='2'/><rect x='9' y='9' width='11' height='11' rx='2' stroke='%2325313f' stroke-width='2'/></g></svg>") 12 12, copy;--gis-view-cursor-point:pointer;--gis-view-cursor-line:pointer;--gis-view-cursor-channel:pointer;--gis-view-cursor-waypoint:pointer;outline:none;width:100%;height:100%;position:relative;overflow:hidden}.gis-view__canvas{touch-action:none;width:100%;height:100%;display:block}.gis-view__graphics{pointer-events:none;z-index:1;isolation:isolate;position:absolute;inset:0;overflow:hidden}.gis-view__graphic{pointer-events:none;will-change:transform, left, top, width, height;position:absolute;top:0;left:0}.gis-view__graphic-image{pointer-events:none;-webkit-user-select:none;user-select:none}.gis-view__labels{pointer-events:none;z-index:2;contain:layout style paint;isolation:isolate;transform-origin:0 0;will-change:transform;position:absolute;inset:0;overflow:hidden}.gis-view__label-layer{position:absolute;inset:0}.gis-view__label-layer--hidden{visibility:hidden}.gis-view__label-canvas{pointer-events:none;position:absolute;top:0;left:0}.gis-view__label{white-space:nowrap;color:#1f3550;background:#ffffffe0;border-radius:4px;padding:1px 6px;font-size:12px;line-height:16px;position:absolute;top:0;left:0}.gis-view__cluster-label{color:#fff;letter-spacing:.2px;pointer-events:none;-webkit-user-select:none;user-select:none;text-shadow:0 1px 1px #102a5059;white-space:nowrap;justify-content:center;align-items:center;min-width:16px;height:16px;font-size:12px;font-weight:600;line-height:16px;display:flex;position:absolute;top:0;left:0}.gis-view__label--draggable,.gis-view__label--editable{pointer-events:auto;touch-action:none}.gis-view__label--draggable{cursor:grab}.gis-view__label--editable{cursor:pointer}.gis-view__label--dragging{cursor:grabbing;z-index:2;-webkit-user-select:none;user-select:none}.gis-view__background-label{white-space:nowrap;pointer-events:none;text-shadow:0 1px 2px #ffffffe6,0 0 2px #fffc;position:absolute;top:0;left:0}.gis-view__tooltip{color:#f3f7fc;white-space:pre-line;pointer-events:none;will-change:transform;background:#172436eb;border-radius:6px;max-width:280px;padding:6px 10px;font-size:12px;line-height:18px;position:absolute;top:0;left:0}.gis-view__tooltip[hidden]{display:none}.gis-view__minimap{z-index:2;box-sizing:border-box;contain:layout paint;isolation:isolate;touch-action:none;cursor:default;position:absolute;overflow:hidden}.gis-view__minimap[hidden]{display:none}.gis-view__minimap-canvas{width:100%;height:100%;display:block}.gis-view__minimap-box{box-sizing:border-box;pointer-events:none;will-change:transform;position:absolute;top:0;left:0}.gis-view__minimap-select{box-sizing:border-box;pointer-events:none;will-change:transform;background:#3678bf1f;border:1px dashed #3678bf;display:none;position:absolute;top:0;left:0}