gis-view-webgl2 0.1.11 → 0.1.13

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 (54) hide show
  1. package/README.md +0 -24
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +524 -33
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +156 -8
  7. package/docs/GisView.md +6 -6
  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 +157 -7
  9. package/docs/demos/BackgroundDemo.vue +600 -0
  10. package/docs/demos/ChannelDemo.vue +145 -3
  11. package/docs/demos/StyleDemo.vue +280 -69
  12. package/docs/demos/api-index.js +35 -19
  13. package/docs/demos/demos.js +26 -18
  14. package/docs/demos/gis-view-demo/demo.css +76 -0
  15. package/docs/demos/gis-view-demo/panels/StylePanels.vue +24 -8
  16. package/docs/demos/gis-view-demo/panels/ViewPanels.vue +33 -3
  17. package/docs/demos/gis-view-demo/useGisViewDemo.js +236 -22
  18. package/docs/format/02-/350/212/202/347/202/271/344/270/216/347/272/277/350/267/257.md +1 -1
  19. package/docs/format/03-/346/213/220/347/202/271/350/243/205/351/245/260/344/270/216/346/240/267/345/274/217.md +3 -3
  20. package/docs/from0/06-/344/273/216/346/225/260/346/215/256/345/210/260/345/256/236/344/276/213-/350/247/243/346/236/220/345/261/202.md +13 -11
  21. package/docs/from0/15-/351/231/204/345/275/225-/345/270/270/351/207/217/351/200/237/346/237/245/344/270/216/350/270/251/345/235/221/346/270/205/345/215/225.md +1 -1
  22. package/docs/index.html +2 -2
  23. package/docs/internals/10-/350/247/204/345/210/231/346/235/241/344/273/266/344/270/216/347/212/266/346/200/201/346/240/267/345/274/217.md +3 -3
  24. package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +1 -1
  25. package/docs/internals/14-/350/203/214/346/231/257/345/261/202.md +1 -0
  26. package/docs/internals/15-03-/351/200/220/350/275/256/350/256/260/345/275/225-/346/200/247/350/203/275/344/270/216/344/277/256/345/244/215-/344/270/213.md +199 -0
  27. package/docs/internals/17-/350/260/203/350/257/225/344/270/216/346/211/251/345/261/225/345/273/272/350/256/256.md +1 -1
  28. package/docs/internals/18-/346/200/247/350/203/275/345/256/236/346/265/213/344/270/216WebGPU/350/257/204/344/274/260.md +32 -0
  29. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +1 -1
  30. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +217 -26
  31. 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 +2 -2
  32. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +37 -4
  33. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +58 -1
  34. package/docs/manual/11-/346/240/267/345/274/217/344/270/216/350/247/204/345/210/231.md +59 -8
  35. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +1 -1
  36. package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +220 -2
  37. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +77 -16
  38. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +30 -17
  39. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +11 -4
  40. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +17 -8
  41. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +128 -4
  42. 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 +2 -2
  43. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +13 -1
  44. package/docs/manual/21-/350/257/264/346/230/216/344/270/216/345/270/270/350/247/201/351/227/256/351/242/230.md +4 -1
  45. package/docs/steps/index.html +1 -1
  46. package/docs/steps/selftest-demo.html +2 -2
  47. package/docs/steps/selftest-demos.html +626 -11
  48. package/docs/steps/selftest-docs.html +8 -4
  49. package/docs/steps/selftest-features.html +1428 -2
  50. package/docs/steps/selftest-perf.html +296 -25
  51. package/docs/steps/selftest-scale.html +61 -3
  52. package/docs/steps/selftest-select.html +303 -1
  53. package/docs/steps/step-06-style.js +3 -2
  54. package/package.json +2 -2
@@ -4,29 +4,31 @@
4
4
 
5
5
  ### 5.1 样式优先级链
6
6
 
7
- 写样式系统之前先定死优先级,否则后面每加一个特性都要重新想一遍。库里是五层,
8
- 从高到低:
7
+ 写样式系统之前先定死优先级,否则后面每加一个特性都要重新想一遍。主链是六层,
8
+ 从高到低(热力图在颜色与尺寸字段上另插一层):
9
9
 
10
10
  ```text
11
11
  状态样式(hoverStyle / selectStyle / multiSelectStyle / boxSelectStyle)
12
- > 要素自身字段(point.color、line.width …)
13
- > 批量规则(pointRules / lineRules 里命中的条件)
14
- > 全局样式(pointStyle / lineStyle)
15
- > 内置默认值
12
+ > override 规则(单条规则写 override: true)
13
+ > 要素自身字段(point.color、line.width …)
14
+ > 普通规则(pointRules / lineRules 里命中的条件)
15
+ > 全局样式(pointStyle / lineStyle)
16
+ > 内置默认值
16
17
  ```
17
18
 
18
19
  解析代码就是一条 `??` 链,读起来与优先级表一模一样(`point.js`):
19
20
 
20
21
  ```js
21
- const color = parseColor(state?.color ?? point.color ?? rule?.color, baseColor)
22
- const sizeScale = resolveSizeScale(point.sizeScale ?? rule?.sizeScale ?? base.sizeScale) * resolveSizeScale(state?.sizeScale)
23
- const size = resolveNumber(state?.size ?? point.size ?? rule?.size, baseSize, 2) * sizeScale
22
+ const color = parseColor(state?.color ?? overrideRule?.color ?? point.color ?? rule?.color, baseColor)
23
+ const sizeScale = resolveSizeScale(overrideRule?.sizeScale ?? point.sizeScale ?? rule?.sizeScale ?? base.sizeScale) * resolveSizeScale(state?.sizeScale)
24
+ const size = resolveNumber(state?.size ?? overrideRule?.size ?? point.size ?? rule?.size, baseSize, 2) * sizeScale
24
25
  ```
25
26
 
26
27
  两个刻意的设计:
27
28
 
28
- - **规则只补要素没写的字段**:`point.color ?? rule?.color` 的顺序意味着逐点配置过的节点不会被
29
- 批量规则改色。这就是「完全按电压等级配色」与「个别节点特殊处理」能同时存在的原因;
29
+ - **普通规则只补要素没写的字段**:`point.color ?? rule?.color` 的顺序意味着逐点配置过的节点不会被
30
+ 批量规则改色;规则写 `override: true` 时改走前面的高优先级暂存,只压它真正写了的字段。这就是
31
+ 「完全按电压等级配色」与「个别节点特殊处理」能同时存在的原因;
30
32
  - **`sizeScale` 是乘法而不是赋值**:`size` 决定基准大小,`sizeScale` 在它之上乘一层。
31
33
  状态样式里的 `sizeScale: 1.25` 于是变成「在它现在的大小上再放大 25%」,
32
34
  大小各异的节点各自按比例变大,而不是被压成同一个尺寸。
@@ -11,7 +11,7 @@
11
11
  | `SPRITE_STRIDE` | 8 | `engine.js` | 每个贴图符号实例的 float 数 |
12
12
  | `IMAGE_STRIDE` | 4 | `engine.js` | 背景图片每个顶点的 float 数 |
13
13
  | `GRID_TARGET_PIXELS` / `MAX_GRID_LINES` | 96 / 400 | `engine.js` | 网格的目标屏幕间距与上限 |
14
- | `MIN_SCALE` / `MAX_SCALE` | 0.002 / 20000 | `types.d.ts` 默认值 | 缩放范围(像素 / 世界单位) |
14
+ | `MIN_SCALE` / `MAX_SCALE` | 0.002 / 1000000000 | `types.d.ts` 默认值 | 缩放范围(像素 / 世界单位) |
15
15
  | `fitPadding` | 48 | `types.d.ts` 默认值 | 适应视图四周留白 |
16
16
  | `zoomStep` | 1.4 | `create.js` | `zoomIn()` / `zoomOut()` 的步进 |
17
17
  | 滚轮倍率 | 1.12 | `engine.js` | 一格滚轮的缩放倍率 |
package/docs/index.html CHANGED
@@ -1421,7 +1421,7 @@
1421
1421
  { file: 'manual/14-视图与筛选.md', title: '手册 14 · 视图与筛选', blurb: '筛选 / 搜索 / 视图与背景', parent: 'GisView.md' },
1422
1422
  { file: 'manual/15-导出与集成.md', title: '手册 15 · 导出与集成', blurb: '导出 / 插件 / 自建界面', parent: 'GisView.md' },
1423
1423
  { file: 'manual/16-实例方法总表.md', title: '手册 16 · 实例方法总表', blurb: '实例方法主表', parent: 'GisView.md' },
1424
- { file: 'manual/17-参数速查.md', title: '手册 17 · 参数速查', blurb: '274 个成员逐个列全', parent: 'GisView.md' },
1424
+ { file: 'manual/17-参数速查.md', title: '手册 17 · 参数速查', blurb: '283 个成员逐个列全', parent: 'GisView.md' },
1425
1425
  { file: 'manual/18-逐方法示例.md', title: '手册 18 · 逐方法示例', blurb: '每个成员一段示例', parent: 'GisView.md' },
1426
1426
  { file: 'manual/19-读数与拖拽预览.md', title: '手册 19 · 读数与拖拽预览', blurb: '读数 / 定位 / 落点预览', parent: 'GisView.md' },
1427
1427
  { file: 'manual/20-性能与大数据.md', title: '手册 20 · 性能与大数据', blurb: '大数据开关与实测', parent: 'GisView.md' },
@@ -2074,7 +2074,7 @@
2074
2074
  { badge: 'START', title: '第一次使用', desc: '引入、最小数据与首屏配置,先把图画出来。', meta: '01 快速开始 · 约 10 分钟', file: 'manual/01-快速开始.md' },
2075
2075
  { badge: 'BACKEND', title: '接后端与导出', desc: '编辑接口、导出回灌、插件与自建界面。', meta: '15 导出与集成', file: 'manual/15-导出与集成.md' },
2076
2076
  { badge: 'PERF', title: '十万级性能', desc: 'Worker 筛选 / 异步搜索、批量更新与质量档。', meta: '20 性能与大数据', file: 'manual/20-性能与大数据.md' },
2077
- { badge: 'API', title: '查方法与参数', desc: '实例方法、options 与可复制示例集中检索。', meta: 'API 速查 · 274 个成员', href: 'api/index.html', external: true }
2077
+ { badge: 'API', title: '查方法与参数', desc: '实例方法、options 与可复制示例集中检索。', meta: 'API 速查 · 283 个成员', href: 'api/index.html', external: true }
2078
2078
  ]
2079
2079
  const renderReadingPaths = () => `<section class='doc-paths' aria-label='推荐阅读路径'>` +
2080
2080
  `<div class='doc-paths__head'><strong>按任务选一条阅读路径</strong><span>第一次进来先看快速开始;带着明确目标时直接跳到对应章节。</span></div>` +
@@ -3,9 +3,9 @@
3
3
  > 条件语言怎么编译成判定函数、样式优先级怎么叠。
4
4
 
5
5
  - **条件编译**(`condition.js`):简写(`{ vn_kv: { gte: 500 } }`)、显式(`{ field, op, not }`)、数组(或)、`$and` / `$or` / `$not`、函数都能编译成 `(item, index) => boolean`;操作符判定按「操作符 + 参数」记忆化(`OPERATOR_MEMO_LIMIT = 1024`),正则做缓存,重复条件不重复解析。
6
- - **规则合并**(`rules.js`):`compileRules` 把规则表编成 `{ style, condition }`;`applyRuleStyle` 先把目标对象的规则字段清空,再把命中的规则按顺序合并(复用同一个 scratch 对象,逐要素零分配)。
7
- - **解析优先级**(`point.js` / `line.js`):`状态样式 > 热力图 > 要素自身字段 > 规则 > 全局样式 > 内置兜底`(热力图按业务字段给色带颜色与尺寸倍数,见 15.18)。规则「只补没写的字段」,因此写在数据上的值规则盖不住。
8
- - **状态样式**(`applyStateStyles`):悬停 / 选中 / 多选 / 框选四档样式按同样的链合并,基准样式(全局 pointStyle / lineStyle)与规则合并结果共用一份暂存;`featureStateStyles` 标记「有没有要素自己配了状态样式」,没有的话悬停切换连重解析都省了。
6
+ - **规则合并**(`rules.js`):`compileRules(rules, keys)` 把规则表编成 `{ style, fields, override, condition }`,`fields` 预先按样式字段表记下这条规则真正写到的键;`applyRuleStyle` 只遍历命中的 `fields`,并把普通规则与 `override: true` 规则分进两份 scratch(`ruleScratch` / `ruleOverrideScratch`)。两份暂存都复用,逐要素零分配;没有 override 规则时用数组上的 `RULE_HAS_OVERRIDE` 私有标记跳过第二份清理。后命中的同层规则覆盖先命中的。
7
+ - **解析优先级**(`point.js` / `line.js`):`状态样式 > 热力图 > override 规则 > 要素自身字段 > 普通规则 > 全局样式 > 内置兜底`(热力图按业务字段给色带颜色与尺寸倍数,见 15.18)。普通规则只补没写的字段,因此写在数据上的值规则盖不住;`override` 规则写了的字段才会压过要素自身,规则没写的字段继续回落。`line.js` 的 `resolveLinePaintInto` 与整份解析共用同一条链。
8
+ - **状态样式**(`applyStateStyles`):悬停 / 选中 / 多选 / 框选四档样式按同样的链合并,基准样式(全局 pointStyle / lineStyle)与规则合并结果共用一份暂存;`featureStateStyles` 标记「有没有要素自己配了状态样式」,没有的话悬停切换连重解析都省了。状态样式仍排在 override 规则与热力图之上。
9
9
  - **绘制顺序**:节点的 `zIndex` 只进绘制顺序,不参与样式合并;`layoutScene` 把节点按 `zIndex` 分层,层内再切「贴图组 + 圆点批次」的连续段(`pointLayerRuns`),一层最多两次 draw。
10
10
 
11
11
  ---
@@ -6,7 +6,7 @@
6
6
  - **视口框**:单独一个 DOM 元素,相机变化只改 `transform`(不重画底图),所以拖动小图不掉帧。
7
7
  - **挂载与位置**:`minimap.container` 接受 `HTMLElement` 或选择器,把 `.gis-view__minimap` 根节点挂到外部宿主;换容器只 `append` 同一根节点,画布、缓存与事件监听都不重建,`destroy()` 从当前宿主摘掉。`offset` 是相对 `position` 角落的 `[x, y]` 位移(正数向右 / 向下),`updateOptions` 只改 `left` / `right` / `top` / `bottom`,因此整图拖动不需要重画小图;根节点通过 `getMinimapElement()` 暴露给外部手势。
8
8
  - **整图拖动示例**:`docs/demos/MinimapDemo.vue` 在根节点顶部追加一个独立手柄,手柄的 `pointerdown` 直接 `stopPropagation`,窗口级 `pointermove` / `pointerup` 累计位移并写回 `minimap.offset`;小图内部的框选、平移与视口框拖动都不受影响。方向键按 2px(Shift 10px)微调,复位按钮把 `offset` 写回 `[0, 0]`。
9
- - **规则**:小图可以有自己的 `pointRules` / `lineRules`(颜色、半径 / 线宽、形状、层级),不配时沿用主图样式链:颜色 / 描边色 / 层级取规则,节点尺寸与符号取引擎最终解析结果(点自身 > 主图规则 > `pointStyle`),线路线宽取样式链的原始线宽(不含主图整体倍数 / 缩放联动 / 热力倍数)。`inheritSize` / `inheritWidth` 标记的节点 / 线路再乘一次「小图 CSS 视口宽高 ÷ 主图 CSS 视口宽高」的较小比例(`inheritedSizeFactor()`),这一份只跟两边视口尺寸有关,主图相机缩放不会改继承要素的大小;小图自身放大时仍可按 `pointZoomScale` / `lineZoomScale` 叠加,`pointSizeScale` / `lineWidthScale` 对自有与继承要素统一生效,`pointMapZoomScale` / `lineMapZoomScale` 只作用于小图自有规则 / 样式。小图自己的尺寸不受视口比例这一步影响。圆形按主图的面积对齐系数还原真实直径;图片 / SVG 图标与自定义图元异步解码后单独盖在点层上,解码前先用小图基准尺寸的圆 / 方回落,完成后只失效点层并补一帧。
9
+ - **规则**:小图可以有自己的 `pointRules` / `lineRules`(颜色、半径 / 线宽、形状、层级),单条规则也可写 `override: true`;不配时沿用主图样式链:颜色 / 描边色 / 层级取规则,节点尺寸与符号取引擎最终解析结果(`override` 规则 > 点自身 > 普通规则 > `pointStyle`),线路线宽取样式链的原始线宽(不含主图整体倍数 / 缩放联动 / 热力倍数)。`inheritSize` / `inheritWidth` 标记的节点 / 线路再乘一次「小图 CSS 视口宽高 ÷ 主图 CSS 视口宽高」的较小比例(`inheritedSizeFactor()`),这一份只跟两边视口尺寸有关,主图相机缩放不会改继承要素的大小;小图自身放大时仍可按 `pointZoomScale` / `lineZoomScale` 叠加,`pointSizeScale` / `lineWidthScale` 对自有与继承要素统一生效,`pointMapZoomScale` / `lineMapZoomScale` 只作用于小图自有规则 / 样式。小图自己的尺寸不受视口比例这一步影响。圆形按主图的面积对齐系数还原真实直径;图片 / SVG 图标与自定义图元异步解码后单独盖在点层上,解码前先用小图基准尺寸的圆 / 方回落,完成后只失效点层并补一帧。
10
10
  - **交互**:视口框上按下 = 拖框;空白处长按(`longPressDelay`)= 视角跟随;`zoomable` 三态互斥:`true` 手动缩放小图自身(滚轮按 `zoomStep`、锚点落光标处,Ctrl / ⌘ + 滚轮留给主图),`'follow'` 让整体范围跟随主图(`followZoomScale` 是主图倍数的指数,`setMinimapZoom` / `zoomMinimapBy` 不抢控制权),`false` 只缩放主图。`dragAction` 决定空白处拖动是平移小图还是框选,`box` 不依赖 `zoomable`;只有手动模式按住 Ctrl / ⌘ 才自动切到另一个动作。拖框与视角跟随带来的相机变化是「程序化」的:标签层不进交互态(`hideOnInteract` 不会把主视图的标签收起来),拖动结束补一次真实排版。每次真正有位移的手势结束时通过 `onInteraction` 发一条 `{ type, originalEvent, local, world, zoom, extent, camera, viewport }`(`type` 为 `box-drag` / `select` / `pan` / `zoom` / `wheel-main` / `long-press`);无位移的点击不发,`pointermove` 不逐帧回调,长按在触发跟随时发。
11
11
  - **范围**:`extent` 决定小图按哪段世界范围铺 —— `measure(data)` 量出可见数据的范围,`backgroundBoundsOf(background)` 量出底图范围(GeoJSON 用画出来的 `bounds`,图片用四角 quad 的包围盒),`auto`(默认)取两者的并集,`data` 只看数据(旧行为),`background` 只看底图。数据为空时 `measure()` 返回 `null`、不会当成「范围是正负一」;修掉的问题见 15.12。`extentFollowCamera` 默认 `false`,`auto` 始终用并集,外围内容消失不会让包围盒与视口框跳动;打开后才有例外:相机正落在其中一份范围里、而那一份在并集里连 6 个像素都占不到时(数据还没映射到底图坐标系就是这样)只画正在看的那一份;`followFocusedExtent()` 在相机挪进 / 挪出这份小范围时补一次重画 —— 判断只用上一次重画缓存的两份范围,每帧几次比较。
12
12
  - **内容范围锁定**:`extentFollowContent` 默认 `false`。`resolveExtent()` 首次拿到当前模式需要的数据 / 底图范围后把它放进 `pinnedExtent`,之后筛选、删除、隐藏或新增内容只重画可见要素,不再改 `bounds` / `scale` / 中心,视口框也就不会跟着外围内容消失而漂移;打开开关后每次重画都用当前可见内容重算。显式修改 `extent`(模式或固定范围对象按值比较)会清掉锁定,立刻按新配置铺;切换 `extentFollowContent` 会重新锁定当前内容。`extentFollowCamera` 的相机取舍也使用锁定后的两份范围。
@@ -8,6 +8,7 @@
8
8
  - **aspectScale**:与 ECharts 的 `geo.aspectScale` 一致 —— 解析时坐标原样保留,解析完再围绕整体范围的 x 中心把经度乘一遍(只压经度、纬度不动)。`rawBounds` 存缩放前的真实范围,相机落位与 `fitView` 用它;`bounds` 是画出来的范围。
9
9
  - **区域文字**:解析出的世界坐标 + 字号 / 颜色交给覆盖层的背景标签层(`.gis-view__label-layer--geo`),`labelBoxes()` 把它算进导出图片,所以背景文字也进导出的图。
10
10
  - 图片与 GeoJSON 都在**世界坐标**里画,跟随相机平移缩放;背景重新解析(图片解码完成、GeoJSON 变化)会触发一次 `buildScene()`:背景里的线也进线路批次,因此要整份重排,`onBackgroundChange` 让鹰眼图跟着重画。
11
+ - **内置编辑器**:`background-editor.js` 是可选的表现层,不在普通渲染路径里。第一次 `beginBackgroundEdit()` 才创建 DOM 覆盖层,从 `background.image.corners` 或 `geoJsonStyle.bounds + rotation` 反推中心 / 宽高 / 角度;拖动统一投影到背景局部坐标后计算,图片写回四角,GeoJSON 写回目标矩形与角度,其余背景字段保留。指针手势合并到动画帧,帧内用 `updateBackground()` 提交并以 `refresh({ reflow: false })` 重绘,覆盖层与 WebGL 底图同帧移动。`endBackgroundEdit()` 移除覆盖层并恢复进入前的 `backgroundEditing`,`destroy()` 同样清理。
11
12
 
12
13
  ---
13
14
 
@@ -516,4 +516,203 @@
516
516
 
517
517
  ---
518
518
 
519
+ ### 15.100 四条修改:规则优先级分层、逐规则 override、单要素样式说明与解析性能(2026-10-30,对着「修改.txt」四条)
520
+
521
+ **需求**:① 规则样式始终高于外层 `pointStyle` / `lineStyle`,但默认低于要素自身样式;② 允许某条规则显式压过要素自身样式;③ 补充如何修改单个节点 / 单条线路的颜色、尺寸、图标等样式;④ 继续优化组件性能。
522
+
523
+ **① 规则优先级分层**:解析顺序统一为「状态样式 / 热力图 > `override: true` 规则 > 要素自身样式 > 普通规则 > 外层 `pointStyle` / `lineStyle` > 默认值」。普通规则不再被外层样式遮住,但也不抢要素自己写的字段;逐字段比较,某一层没写的字段继续向下回落。状态样式和热力图仍位于规则之上,悬停、选中、多选、框选不会被普通规则或 override 规则打回原样。
524
+
525
+ **② 逐规则 `override`**:规则类型增加 `override?: boolean`,只有严格 `true` 生效。`rules.js` 在编译期把规则分成普通样式与 override 样式两份,命中后分别写入 `ruleScratch` / `ruleOverrideScratch`;同一层多条规则按声明顺序后命中覆盖先命中。override 规则只覆盖它实际写了的字段,未写字段继续交给要素自身、普通规则与外层样式,不需要把整份规则样式复制到数据里。主图与鹰眼图使用同一套优先级,小图不会再出现与主图颜色 / 线宽不一致。
526
+
527
+ **③ 单节点 / 单线路样式文档**:手册 11 新增「改单个节点 / 线路的样式」示例,分别演示 `updatePoint` 改颜色、尺寸、`symbol` 图标,`updateLine` 改颜色、线宽、虚线,`patchPoints` / `patchLines` 批量改写,以及线路 `decorations` 逐条替换装饰图标;同时说明用 `null` 摘除逐要素字段后会回落到规则 / 外层样式。`updatePoint` / `updateLine` 只走单条更新路径,不要求整份 `updateData()` 或重建规则。
528
+
529
+ **④ 规则解析性能**:`compileRules(rules, keys)` 编译时预计算每条规则实际写到的 `fields`,逐要素只遍历这几个键,不再每条规则都扫描整套样式字段;新增私有 `RULE_HAS_OVERRIDE`,规则表没有 override 时直接跳过第二份暂存清理。`point.js` / `line.js` 用双 scratch 合并,`engine.js` 的整批解析、状态刷新、定向更新、拖拽 / 复制预览和锚点尺寸读数全部接入 `ruleOverrideScratch`。10 万要素、4 条规则、9 个样式字段的 Node 微基准:规则合并中位 **21.82 → 17.69 ms**(节省 **4.13 ms**,约 **1.23×**),校验和一致。
530
+
531
+ **核心文件与配套**:`src/gis-view/rules.js`、`point.js`、`line.js`、`engine.js`、`create.js`、`types.d.ts`;手册 11 及 02 / 03 / 07 / 09 / 12 / 13 / 14 / 16 / 17 / 18 / 19 / 20 / 21、格式 02 / 03、internals 10 / 13 / 17 / 18、from0 06、教学页、`src/gis-view/README.md` 同步;`StyleDemo.vue` 增加「规则强制覆盖」开关,可把当前预设的所有规则临时改为 `override: true`。
532
+
533
+ **验证与已知失败**:`npx tsc --noEmit`、`npm run check:docs`(276 个实例成员 / 102 个 options / 270 个数据字段)、`node docs/steps/gen-api-index.mjs`、`npm run build`、`git diff --check` 通过。`selftest-features` **771/0**(新增优先级、override、状态样式、`updatePoint` / `updateLine` 共 11 条),actions 96/0、data 103/0、decoration 75/0、demo 133/0、demos 407/0、docs 138/0、effects 96/0、full 94/0、labels 178/0、select 102/0、selftest 19/0。`selftest-perf` 为 **186/1**,失败项是既有的标签布局读数量测;`selftest-scale` 为 **120/2**,失败项是既有的筛选回调时序(`onFilter 0 条`)。两项单页重跑仍复现,与本轮样式链改动无关。
534
+
535
+ ### 15.101 交互样式优先级回归、单 / 多要素 Demo 与节点 / 线路 / 通道详情(2026-10-30,对着「修改.txt」三条)
536
+
537
+ **需求**:① 选中、框选、多选、级联等交互颜色始终最高,不受规则束缚;② 增加修改单个或多个节点 / 线路样式的 demo;③ 说明如何查询单个或多个节点、线路、通道的详情。用户补充「不考虑分组详情」,本轮只处理节点、线路、通道。
538
+
539
+ **交互样式优先级审计**:`resolvePointWithBase` / `resolveLineStyleInto` 的字段链已经是「状态样式 / 热力图 > override 规则 > 要素自身 > 普通规则 > 全局样式 > 默认值」;本轮继续核对 `applyPointNarrowFast` / `applyLineNarrowFast`,两条窄路都只在 `pointRuleEntries` / `lineRuleEntries` 为空、无热力图且规则输入快照未变时命中,规则存在时不会跳过状态层,因此没有新增生产代码分支。`selftest-features` 加了一组独立回归:先确认 override 规则把未交互节点 / 线路画成规则色,再分别验证单选、动态换一套 override 规则、多选节点、多选线路、框选节点、框选线路和级联;级联时起始节点仍走选中色,邻接节点 / 线路走级联色,所有颜色都高于规则。线路框选沿用 `lineSelectStyle`,不新增 `lineBoxSelectStyle`。
540
+
541
+ **单 / 多要素样式 Demo**:`StyleDemo.vue` 新增节点 / 线路下拉和 `updatePoint` / `updateLine` 单条补丁按钮,`patchPoints` / `patchLines` 批量补丁按钮与「恢复原始数据」;另加 `selectFeatures` / `selectInBox` 的单选、多选、框选按钮,显示 `pointHoverStyle` / `pointSelectStyle` / `pointMultiSelectStyle` / `pointBoxSelectStyle`、线路对应样式与 `cascadeHighlight` 五类状态色,并保留「规则强制覆盖」开关用于直观看优先级。`demos.js` 简介同步。
542
+
543
+ **节点 / 线路 / 通道详情**:Demo 的「详情查询」面板同时调 `getPoint`、`listPoints`、`getLine`、`listLines`、`getChannel`、`getChannels`,读数展示单条下标 / 屏幕坐标 / 半径、线路折点数 / 线宽、通道回线数 / 骨架点数。手册 18 新增「单条 / 多条要素详情」,说明 id / 下标 / `{ type, index }` 引用、单条 `null` 与批量空数组、`indexes` / `inView` / `limit` / `withScreen` / `visibleOnly`,以及按 id 取下标后再列表的写法;通道只查单条或按 id 批量,不展开分组。手册 11 把单 / 多要素样式合成一节,并明确交互颜色高于任何规则。
544
+
545
+ **核心文件**:`docs/demos/StyleDemo.vue`、`docs/demos/demos.js`、`docs/manual/11-样式与规则.md`、`docs/manual/18-逐方法示例.md`、`docs/steps/selftest-features.html`、`docs/steps/selftest-demos.html`、`docs/AI对话汇总.md`、`docs/ai/06-会话索引与复查方法.md`。没有新增公开方法、options 或数据字段,API 计数保持 276 / 102 / 270。
546
+
547
+ **验证**:`npx tsc --noEmit` 0 错;`npm run check:docs` 四项全绿(成员 276、options 102、数据字段 270 全部命中);`npm run build` 通过(仅既有大 chunk 提示);`git diff --check` 通过。浏览器串行自测:`selftest-features.html` **778/0**、`selftest-demos.html` **415/0**、`selftest-docs.html` **138/0**。本轮未改样式解析与窄路实现,未重跑已知受环境时序影响的 perf / scale 两页。
548
+
549
+ ### 15.102 背景添加、GeoJSON 目标矩形与网架整体显隐 / 透明度(2026-10-30,对着「修改.txt」一条)
550
+
551
+ **需求**:增加背景图片或 GeoJSON 添加能力,可拖动四角设置自定义宽高比与大小,方便先渲染网架再添加背景;支持一键完全隐藏网架,也能调整网架整体透明度,并在 Demo 中体现。
552
+
553
+ **背景添加与目标矩形**:图片继续通过 `background.image.corners` 给出四个世界坐标,Demo 的四角手柄按 `view.project()` 定位、拖动时用 `clientToWorld()` 换算,写回后立即 `updateBackground()`;面板同时提供锁宽高比、宽高输入和本地图片 / GeoJSON 导入。GeoJSON 新增 `geoJsonStyle.bounds`:`resolveGeoJson()` 解析完原始几何后,按自身范围到目标矩形的 x / y 比例把填充、描边、点、逐区域 `bounds`、重心与区域文字整体搬迁;`rawBounds` 保留原坐标范围,因而 `getGeoProjection()`、经纬度映射和相机落位口径不变。目标是任意世界矩形,两轴可独立缩放,所以不改原始 GeoJSON 也能调宽高比和大小。
554
+
555
+ **网架整体显隐与透明度**:引擎新增 `networkVisible`(缺省 `true`)与 `networkOpacity`(0 ~ 1,缺省 1)。绘制帧在数据段打开 `drawingNetwork`,统一让节点、线路、通道、装饰、动画、热力光晕、聚合与框选框走同一个跳过 / alpha 乘数;底图、网格与 `webglLayers` 在此之前或之后绘制,不受影响。`networkVisible: false` 时 `pick` / `nearestPointsAt` / `nearestLineAt` / 框选 / `queryRect` 都不返回网架命中,并清掉悬停;`networkOpacity` 只在可见时乘在逐要素 alpha 之后,`0` 仍保留拾取语义,所以文档明确「完全隐藏用 visible」。`create.js` 的应用层快路同步 DOM 标签层并通过 `overlay.setNetworkPresentation()` 更新显隐 / 不透明度,创建结束也立即对齐一次初始值;`update-policy.js` 把两项登记为 presentation-only,`setOptions()` 不重排场景。
556
+
557
+ **Demo 与文档**:新增 `docs/demos/BackgroundDemo.vue` 和「背景与网架」页签(Demo 22 → 23):初始只显示网架,可添加内置 / 本地图片或 GeoJSON,四角拖动、锁比例、宽高数值、背景透明度、一键隐藏和网架透明度都在同页;隐藏网架后背景与编辑手柄保留。手册 03 / 14 / 16 / 18 / 21、`types.d.ts`、`docs/demos/api-index.js` 同步;`selftest-features` 增加像素级「剩余墨量 0 / 拾取失效 / alpha 变淡」与 `resolveGeoJson` 目标矩形断言,`selftest-demos` 增加背景页交互回归,`selftest-docs` 同步 options 104 与 Demo 23 的计数。
558
+
559
+ **核心文件**:`src/gis-view/engine.js`、`background.js`、`create.js`、`overlay.js`、`update-policy.js`、`types.d.ts`;`docs/demos/BackgroundDemo.vue`、`demos.js`、`docs/demos/api-index.js`、`docs/manual/03` / `14` / `16` / `18` / `21`、`docs/steps/selftest-features.html`、`selftest-demos.html`、`selftest-docs.html`、`docs/AI对话汇总.md`、`docs/ai/06-会话索引与复查方法.md`。
560
+
561
+ **验证**:`npx tsc --noEmit` 0 错;`node docs/steps/gen-api-index.mjs` 更新索引;`npm run check:docs` 全绿(276 个实例成员 / 104 个 options / 272 个数据字段);`npm run build` 通过(仅既有大 chunk 提示);`git diff --check` 通过。浏览器串行自测:`selftest-features.html` **782/0**(含创建时隐藏网架的 DOM 标签层断言)、`selftest-demos.html` **426/0**、`selftest-docs.html` **138/0**。
562
+
563
+ ### 15.103 背景移动 / 旋转、背景编辑模式与 Demo 修复(2026-10-30,对着「修改.txt」两条)
564
+
565
+ **需求**:背景可以调整位置,还可以旋转;调整背景时网架不能做任何操作,不能拖拽、点击等,但能和背景图整体放大缩小,相当于只能编辑背景或进入背景编辑模式。Demo 明显有问题。
566
+
567
+ **GeoJSON 旋转与外框**:`resolveGeoJson()` 先按 `geoJsonStyle.bounds` 把整份几何独立缩放 / 平移到目标世界矩形,再绕矩形中心顺时针旋转。旋转矩阵使用世界坐标角度的反角,填充、描边、点、逐区域 `bounds`、面积重心与区域文字全部同步变换;`shapes.bounds` 改为映射后四角求出的旋转 AABB,`rawBounds` 仍保留原始坐标,所以经纬度投影、相机与数据映射口径不变。类型声明中的 `rotation` 也从误放的 `GisViewBackgroundImage` 移回 `GisViewGeoJsonStyle`。
568
+
569
+ **背景编辑模式**:新增顶层 `backgroundEditing`,`create.js` 转发给引擎并登记为表现型快路。`engine.js` 在 pointer down / move / up、click、contextmenu、hover 与 cursor 更新入口统一短路,进入模式时释放已有拖拽并清掉悬停;`create.js` 的数据快捷键只保留 `fitView`,撤销、删除、复制、清空选中等全部不响应。`overlay.js` 的 DOM 标签拖拽与指针目标解析同时检查编辑模式,避免网架标签绕过画布屏蔽。滚轮缩放没有进入短路路径,网架和背景仍一起放大缩小。
570
+
571
+ **Demo 修复**:`BackgroundDemo.vue` 的背景覆盖层支持拖内部整体移动、四角缩放、顶部圆点旋转;Shift 以 15° 吸附,另有 ±15° 按钮与精确角度输入。添加背景后自动进入编辑模式并调用 `zoomToBounds()` 把背景和手柄放回视野;覆盖层上的滚轮按指针位置转发 `zoomBy()`。图片把旋转后的四角写入 `image.corners`,GeoJSON 写入 `geoJsonStyle.bounds + rotation`,退出编辑后恢复网架交互。`demos.js` 简介、手册与 FAQ 同步。
572
+
573
+ **测试与文档**:`selftest-features` 增加 GeoJSON 旋转中心、AABB、原始 `rawBounds`,以及 `backgroundEditing` 下点击 / 拖点 / 平移 / 右键 / Delete 失效、滚轮仍缩放和退出恢复的断言。`selftest-demos` 增加自动进入编辑、背景移动、GeoJSON 拖拽旋转到 90°、精确输入复位、图片旋转四角、编辑层滚轮转发和编辑开关显隐的断言。手册 03 / 14 / 16 / 18 / 21、`types.d.ts`、API 索引与 `selftest-docs` 同步。
574
+
575
+ **核心文件**:`src/gis-view/background.js`、`engine.js`、`create.js`、`overlay.js`、`update-policy.js`、`types.d.ts`;`docs/demos/BackgroundDemo.vue`、`demos.js`、`docs/demos/api-index.js`、`docs/manual/03` / `14` / `16` / `18` / `21`、`docs/steps/selftest-features.html`、`selftest-demos.html`、`selftest-docs.html`、`docs/AI对话汇总.md`、`docs/ai/06-会话索引与复查方法.md`。
576
+
577
+ **验证**:`npx tsc --noEmit` 0 错;`node docs/steps/gen-api-index.mjs` 更新索引;`npm run check:docs` 全绿(276 个实例成员 / 105 个 options / 273 个数据字段);`npm run build` 通过(仅既有大 chunk 提示);`git diff --check` 通过。浏览器串行自测:`selftest-features.html` **786/0**、`selftest-demos.html` **433/0**、`selftest-docs.html` **139/0**。
578
+
579
+ ### 15.104 背景编辑 4 / 8 / 9 点、旋转开关、可定制编辑框与拖动晃动修复(2026-10-30,对着「修改.txt」五条)
580
+
581
+ **需求**:只开启旋转时才显示旋转按钮;取消宽高锁定时由四角改成常见八点,开启旋转后考虑角度并变成九点;背景编辑框样式支持自定义;修复移动背景图的轻微晃动;全部在 demo 中体现。
582
+
583
+ **编辑点与旋转开关**:`BackgroundDemo.vue` 把宽高锁定与旋转拆成两个独立状态。锁定时只生成四角等比缩放手柄;解锁后加入 `n/e/s/w` 四个边中点,共八个缩放手柄;`rotationEnabled` 为真时才生成顶部旋转点和旋转连线,所以“解锁 + 旋转”是 8 + 1 = 9 个编辑点。关闭旋转只收起旋转点,当前角度、背景数据与八个缩放手柄都保留;角度输入与 ±15° 按钮也随开关显隐。
584
+
585
+ **旋转后的缩放**:所有手柄先按 `start.rotation` 把指针从世界坐标反算到未旋转的局部坐标。角手柄同时计算局部宽高,锁定比例时沿用“相对位移更大的轴主导”并补足最小尺寸;四个边手柄只改所在边的单轴,另一轴保持快照值,中心沿该轴移动半个增量,因此对边中点固定。中心偏移再用 `rotateOffset(..., -rotation)` 转回世界坐标。手柄光标方向与边手柄朝向也按同一角度计算,旋转后不会出现拖动方向与视觉方向不一致。
586
+
587
+ **可定制编辑框**:编辑层通过 `editorStyleVars` 注入 `--bg-edit-color` / `--bg-edit-fill` / `--bg-edit-line-width` / `--bg-edit-dash` / `--bg-edit-handle-size` / `--bg-edit-handle-radius` 等 CSS 变量;面板提供颜色、线宽、虚线、手柄尺寸、圆形 / 方形控件,轮廓、旋转连线、缩放手柄与旋转点共用这套变量。它是使用方编辑 UI 的样式,不新增组件 options;`backgroundEditing` 仍只负责锁住网架交互。
588
+
589
+ **拖动晃动**:旧路径在每次 `pointermove` 里同步 `syncFrameInputs()` 与 `updateHandles()`,但 WebGL 背景要等下一次引擎渲染,DOM 手柄会先跑一帧。现在指针移动只更新待提交状态并调用 `scheduleBackground()`;动画帧内用 `view.updateBackground()` 更新背景,再以 `view.refresh({ reflow: false })` 立即重绘这一帧,随后统一刷新输入框、手柄和读数,`pointerup` 时 `flushBackground()` 落最终状态。自测用同一帧内“移动前手柄不动、刷新后手柄位移等于背景四角投影位移”验证不会再抢跑。
590
+
591
+ **测试与文档**:`selftest-demos.html` 由 433 条增至 **440/0**,新增默认 4 点、开旋转 5 点、解锁 8 点、解锁并旋转 9 点、四边手柄单轴缩放、旋转 30° 后固定对边、编辑框 CSS 变量、关闭旋转收起按钮,以及手柄与底图同帧更新断言。`demos.js` 简介、手册 03 / 14 / 21、AI 汇总与维护索引同步;没有修改公开 API、options、数据字段或 Demo 数量(276 / 105 / 273 / 23)。
592
+
593
+ **核心文件**:`docs/demos/BackgroundDemo.vue`、`docs/demos/demos.js`、`docs/manual/03-配置项.md`、`docs/manual/14-视图与筛选.md`、`docs/manual/21-说明与常见问题.md`、`docs/steps/selftest-demos.html`、`docs/AI对话汇总.md`、`docs/ai/06-会话索引与复查方法.md`。
594
+
595
+ **验证**:`npx tsc --noEmit` 0 错;`npm run check:docs` 全绿(276 个实例成员 / 105 个 options / 273 个数据字段);`npm run build` 通过(仅既有大 chunk 提示);`git diff --check` 通过。浏览器串行自测:`selftest-features.html` **786/0**、`selftest-demos.html` **440/0**、`selftest-docs.html` **139/0**。
596
+
597
+ ### 15.105 内置背景编辑器与旋转按钮命中修复(2026-10-30,对着「修改.txt」两条)
598
+
599
+ **需求**:① 背景编辑器的旋转按钮始终不旋转;② 把背景编辑模式做成组件内置模块,通过方法开启与配置,不再由 Demo 手写整套交互。
600
+
601
+ **旋转按钮根因**:旧 Demo 的 `.bg-edit-layer` 设为 `pointer-events: none`,缩放手柄与轮廓分别恢复 `pointer-events: auto`,但旋转按钮漏了同一条规则。按钮本身可见,指针事件却穿透到画布,因此拖动不会进入旋转手势。内置编辑器统一管理覆盖层,并给旋转按钮显式恢复 `pointer-events: auto`;自测不再直接向按钮派发合成事件,而是先用 `document.elementFromPoint()` 证明按钮在屏幕坐标上真实可命中,再发起拖拽。
602
+
603
+ **内置编辑器**:新增 `background-editor.js`,编辑器只改背景原生字段:图片写回 `image.corners`,GeoJSON 写回 `geoJsonStyle.bounds + rotation`,底色、透明度、阴影与其它 GeoJSON 样式原样保留。`create.js` 懒初始化并公开四个实例方法:`beginBackgroundEdit(options)` 进入并自动打开 `backgroundEditing`;`updateBackgroundEdit(patch)` 增量改配置 / 中心 / 宽高 / 角度 / 样式;`endBackgroundEdit()` 移除覆盖层并恢复进入前的 `backgroundEditing`;`getBackgroundEdit()` 读取当前状态,未开启时返回 `null`。
604
+
605
+ **交互与刷新**:锁比例时只出四角等比缩放手柄,解锁后出四角 + 四边中点共八个,`rotation: true` 再增加顶部旋转点,因此 4 / 5 / 8 / 9 点都由同一套配置推导。所有手柄先换算到背景局部坐标,旋转后的拖动方向与视觉一致;四边手柄只改一个局部轴并固定对边。旋转按顺时针角度写回并归一化到 -180° ~ 180°,按住 Shift 按 `snapAngle`(缺省 15°)吸附。指针拖动合并到 RAF,帧内先 `updateBackground()` 再 `refresh({ reflow: false })`,随后同步编辑层与读数,避免底图晚一帧造成晃动。
606
+
607
+ **生命周期与 Demo**:编辑器第一次 `beginBackgroundEdit()` 才挂 DOM / 监听,未使用背景编辑的页面零额外成本;`endBackgroundEdit()` 与 `destroy()` 都走完整清理。没有图片 / GeoJSON 背景时只报 `onWarning`,不进入编辑。`BackgroundDemo.vue` 删除手写拖动、缩放、旋转、投影和编辑框 CSS,只调用内置 API;本地导入、宽高 / 角度输入、编辑框样式、网架显隐与透明度保留。
608
+
609
+ **公开面与测试**:新增 `GisViewBackgroundEditorOptions` / `GisViewBackgroundEditorPatch` / `GisViewBackgroundEditorState` / `GisViewBackgroundEditorStyle` 类型,实例成员 276 → **280**(方法 271 → 275),options 仍 105、数据字段仍 273、Demo 仍 23。`selftest-demos.html` 覆盖 4 / 8 / 9 点、旋转按钮真实命中、旋转角度写回、样式实时更新与退出恢复;`selftest-features.html` 覆盖公开 API 与未开启时读数。
610
+
611
+ **核心文件**:`src/gis-view/background-editor.js`(新增)、`gis-view.css`、`create.js`、`types.d.ts`;`docs/demos/BackgroundDemo.vue`、`demos.js`、`docs/manual/03` / `14` / `16` / `17` / `18` / `21`、`docs/internals/14`、`docs/steps/selftest-demos.html`、`selftest-features.html`、`selftest-docs.html`、`docs/AI对话汇总.md`、`docs/ai/06-会话索引与复查方法.md`。
612
+
613
+ **验证**:`npx tsc --noEmit` 0 错;`node docs/steps/gen-api-index.mjs` 更新索引;`npm run check:docs` 全绿(280 个实例成员 / 105 个 options / 273 个数据字段);`npm run build` 通过;`git diff --check` 通过。浏览器串行自测:`selftest-features.html` **793/0**、`selftest-demos.html` **441/0**、`selftest-docs.html` **139/0**。
614
+
615
+ ### 15.106 GeoJSON 背景文字同帧刷新与 fitSpacing.minDistance 上限修复(2026-10-30,对着「问题.txt」两条)
616
+
617
+ **需求**:① 拖动 GeoJSON 背景时仍有晃动,文字尤其明显;② `fitSpacing.minDistance` 有时怎么设置都不生效,`maxDistance` 却正常,看起来像内部做了强制限制。
618
+
619
+ **背景文字根因与修复**:内置背景编辑器的 `applyBackground()` 每帧先 `updateBackground()` 再 `refresh({ reflow: false })`,WebGL 底图和编辑框已经同步到新位置;区域文字却由覆盖层的 `setBackgroundLabels()` 排进下一帧 rAF,合成时文字还停在上一位置,于是底图与文字错开一帧。`overlay.refreshBackgroundLabels()` 改为立即按当前相机重排版,并取消已经排队的那一帧,避免随后重复排版;编辑器在引擎同步重画后调用它,三者在同一任务内提交。`selftest-features` 把测试背景改成带 `Feature.properties.name` 的标准 `FeatureCollection`,确认区域文字确实生成,并断言 `updateBackgroundEdit({ center })` 返回时文字 transform 已经变化。
620
+
621
+ **minDistance 根因与修复**:`fitSpacing` 的距离公式没有被额外改写,真正截住比例的是相机缺省上限 `maxScale: 20000`。小量纲数据的最近世界距离可能只有约 `0.00536`,`20000 × 0.00536 ≈ 107.2px`,所以 `minDistance` 继续调大也停在同一屏距;`maxDistance` 是往下压比例,不会撞这道上限,因此看起来「只有 minDistance 失效」。`engine.js` 新增 `DEFAULT_MAX_SCALE = 1000000000` 并作为未配置时的上限,显式传入的 `maxScale` 仍优先,`minScale` / `maxScale` 也仍是 `fitSpacing` 最后一道夹取。手册补上「固定屏距时先看显式 `maxScale` / `maxZoom`,用 `getPointSpacing()` 读实际 CSS 像素」的排查路径。
622
+
623
+ **测试与文档**:`selftest-scale` 新增世界距离 `0.01`、目标 `minDistance: 1000` 的用例,断言相机比例超过旧上限 `20000` 且屏幕间距约 `1000px`;`selftest-demo` 与 `useGisViewDemo.js` 的缩放限制缺省值同步为 `0.002 ~ 1000000000`。手册 03 / 14、`docs/from0/15`、`types.d.ts` 注释与 API 索引一起更新;公开面不变(280 个实例成员 / 105 个 options / 273 个数据字段)。
624
+
625
+ **核心文件**:`src/gis-view/engine.js`、`overlay.js`、`create.js`、`types.d.ts`、`README.md`;`docs/manual/03-配置项.md`、`14-视图与筛选.md`、`docs/from0/15-附录-常量速查与踩坑清单.md`、`docs/demos/api-index.js`、`docs/demos/gis-view-demo/useGisViewDemo.js`、`docs/steps/selftest-features.html`、`selftest-scale.html`、`selftest-demo.html`、`docs/AI对话汇总.md`、`docs/ai/06-会话索引与复查方法.md`。
626
+
627
+ **验证**:`npx tsc --noEmit` 0 错;`node docs/steps/gen-api-index.mjs` 更新索引;`npm run check:docs` 全绿;`npm run build` 通过;`git diff --check` 通过。浏览器串行自测:`selftest-features.html` **794/0**、`selftest-demos.html` **441/0**、`selftest-docs.html` **139/0**、`selftest-demo.html` **133/0**;`selftest-scale.html` **121 通过 / 2 失败**,新增断言通过,两条失败仍是既有筛选回调时序(`onFilter 0 条`)。
628
+
629
+ ### 15.107 fitSpacing.minDistance 精确落位与指定节点组间距(2026-10-30,对着「修改.txt」三条)
630
+
631
+ **需求**:① `minDistance` 一旦设置就必须显示为该屏幕距离,初始值更大或更小都要生效,并排除“最大放大比例”影响;② 新增方法,传一组节点返回整组的 `minDistance` / `maxDistance`;③ 文档说明距离如何由世界坐标与相机比例计算。
632
+
633
+ **根因**:`fitSpacingAdjustment()` 原先把 `minDistance / 最近世界距离` 与普通适应比例取 `Math.max`,再用 `maxZoom` 做相对上限。前者让目标比例只能高于普通适应,后者继续截住放大目标,因此初始屏距较疏或 `minDistance` 改小时都没有变化。问题不在贴边留白,也不在 `cameraForBounds()` 的包围盒计算。
634
+
635
+ **精确落位**:`engine.js` 的 `minDistance` 分支现在直接令 `target = minDistance / nearestWorldDistance`,不再与 `baseScale` 取最大值,也不读 `maxZoom`。`maxDistance` 保留原语义,仍是 `min(target, maxDistance / farthestWorldDistance)` 的上限模式。两种模式最终都由 `minScale` / `maxScale` 夹取,相机中心仍取普通适应的包围盒中心。`maxZoom` 为旧配置兼容保留,类型注释明确它不再限制距离。
636
+
637
+ **指定节点组读数**:`create.js` 新增 `getPointsSpacing(refs)`,复用 `resolveCopySources()` 把单个 / 数组引用去重、按数据顺序定位;直接对这批节点的世界坐标调用 `utils.measurePointSpacing()`,再把最近 / 最远距离乘当前 `camera.scale`。返回沿用 `GisViewPointSpacing`,端点带 `{ id, index, item }`。它只统计显式传入的节点,筛选 / 隐藏不影响;现有 `getPointSpacing()` 继续只读筛选 / 隐藏后的可见节点。两者空输入、单节点与全部重合时的形状一致。
638
+
639
+ **公式与 Demo**:手册 03 / 14 补 `d_world = sqrt((x2 - x1)^2 + (y2 - y1)^2)`、`d_screen = d_world * camera.scale`、`minDistance` 精确目标与 `maxDistance` 上限目标;同时写明最近距离只取不同坐标节点的最小正距离,最远距离走凸包直径。手册 16 / 17 / 18 补方法签名、参数与示例。Demo 面板删除已经无效的“最大放大”输入,避免继续暴露 `maxZoom` 的旧语义。
640
+
641
+ **公开面与测试**:实例成员 280 → **281**,方法 275 → **276**;options 105、数据字段 273、分组 16 不变。`selftest-scale` 新增 5 条断言:普通适应屏距更大时缩到 `minDistance`、继续改小时比例实际下降、`maxZoom` 不截断、指定组引用去重、空 / 单节点与显式隐藏节点读数。`selftest-docs` / `selftest-demos` 的成员计数同步为 281。
642
+
643
+ **核心文件**:`src/gis-view/engine.js`、`create.js`、`types.d.ts`、`src/gis-view/README.md`;`docs/manual/03` / `14` / `16` / `17` / `18`、`docs/demos/gis-view-demo/useGisViewDemo.js`、`panels/StylePanels.vue`、`docs/steps/selftest-scale.html`、`selftest-docs.html`、`selftest-demos.html`、`docs/demos/api-index.js`、`docs/AI对话汇总.md`、`docs/ai/06-会话索引与复查方法.md`。
644
+
645
+ **验证**:`npx tsc --noEmit` 0 错;`node docs/steps/gen-api-index.mjs` 生成 281 个成员;`npm run check:docs` 全绿;`npm run build` 通过(仅既有大 chunk 提示);`git diff --check` 通过。浏览器串行自测:`selftest-features.html` **794/0**、`selftest-demos.html` **441/0**、`selftest-docs.html` **139/0**、`selftest-demo.html` **133/0**;`selftest-scale.html` **126 通过 / 2 失败**,新增断言全过,两条失败仍是既有筛选回调时序(`onFilter 0 条`)。
646
+
647
+ ### 15.108 visualMap 图例、共享色带插值与图例专用快路径(2026-10-31,对着「修改.txt」两条)
648
+
649
+ **需求**(「修改.txt」两条):① 参考 ECharts `visualMap`,给组件加一个类似图例,并结合 ruler 样式;② 继续优化组件性能。
650
+
651
+ **图例模型**:新增 `src/gis-view/visual-map.js`,`options.visualMap` 配置对象本身即开关。`type: 'ruler'`(`'continuous'` 同义)画一条连续渐变尺:纵向低温在下、高温在上,横向从左往右递增,刻度按 `tickCount` 均分;`type: 'piecewise'` 画分段色块,`pieces` 支持 ECharts 的 `gte` / `gt` / `lte` / `lt` / `min` / `max`,可逐段写 `label` / `color`,不写分段时按值域与刻度数等分。另有 `domain`、单端 `min` / `max`、`colors` / `stops` / `opacity`、`title` / `unit` / `formatter`、`orient` / `position` / `width` / `height` / `precision`、`container` / `className` / `style`。默认按 `heatmap.target` 的 `point → line → channel` 顺序继承目标,字段 / 自动值域 / 色带 / 透明度继续继承热力图;没有热力图时按自身 `field` 扫当前数据,也能完全独立于热力图。
652
+
653
+ **读数与生命周期**:`create.js` 单独建一份只读 `visualMapHeatmap` 解析器,不把引擎内部的解析器暴露出去;`readHeatmap(target, withDomain)` 把 `withDomain` 原样传给 `describe()`,显式 `domain` 时不会误触发自动值域扫描。公开 `getVisualMapElement()` 与 `getVisualMap()`:前者返回根节点,后者返回类型、目标、字段、值域、色带、方向、标题、单位以及刻度 / 分段副本。不配 / `null` / `enabled: false` 时根节点从 DOM 摘掉;外部 `container` 也按同一路径卸载。图例本身不参与拾取,默认 CSS `pointer-events: none`,需要区间交互时由使用方在根节点上扩展。
654
+
655
+ **共享色带插值**:`heatmap.js` 导出 `createHeatmapRamp(colors, stops, opacity)`。它复用逐要素上色同一套 `normalizeStops()`、`buildRampLut()`、`opacityScale()` 与 `rgbaCss()`,图例的 CSS 渐变和画布取色不会再各写一份插值;每条图例色带只建一次 128 档 LUT,临时颜色配置不会挤占主热力图的 `rampCache`。`createHeatmap()` 同时补 `primaryTarget()` 与 `describe(kind, withDomain)`,后者自动值域仍走它自己的 `domainOf` 数组缓存。
656
+
657
+ **图例专用快路径**:`visual-map.js` 把更新合并到一帧,并用「类型 / 方向 / 目标 / 字段 / 值域 / 有效色带 / 标题 / 单位 / 尺寸 / 定位 / 容器 / formatter / 刻度与分段内容」组成渲染签名;签名没变时只更新内部状态,不写任何 DOM。数据变化由 `create.js` 的 `emitDataChange()` 调 `markDataDirty()`:清图例独立值域缓存、让热力图解析器的自动值域失效,只在确实需要自动值域时排帧;图例关闭期间发生的数据变化先记账,下次开启再补失效。`updateOptions({ visualMap })`(可带 `on*` 回调)被识别为 `visualMapOnly`,不重排引擎场景、不刷新鹰眼图,也跳过标签层重排;`heatmap` 配置变化另走 `markSourceDirty()` 重读继承的字段与色带。值域两端写死时连补另一端都不扫数据:显式 `domain` 和同时给出 `min` / `max` 都保持零字段读取,只有单端配置才扫自动值域。
658
+
659
+ **Demo 与文档**:`docs/demos/gis-view-demo/useGisViewDemo.js` 增加 `visualMapEnabled` / `visualMapType` 与 `buildVisualMapOptions()`,图例默认关闭,开启且热力图开启时跟随当前字段、值域与色带;`panels/StylePanels.vue` 的热力面板补「显示图例」「连续尺 / 分段」。手册 03 / 11 / 13 / 15 / 16 / 17 / 18 补配置、样式、接入、方法表、参数速查与示例;`docs/GisView.md`、`docs/index.html`、组件 README 与文档中心同步,CSS 增加 `.gis-view__visual-map*` 与 `--gis-view-visual-map-*` 主题变量。
660
+
661
+ **公开面与测试**:实例成员 281 → **283**(方法 276 → **278**,属性仍 5),options 105 → **106**,数据字段 273 → **274**,事件 / 功能开关 / 分组分别保持 44 / 17 / 16。`selftest-features` 新增 2 条字段读取断言:显式 `domain` 与两端 `min` / `max` 都不读数据字段;同时覆盖连续尺 / 分段 / 继承色带 / 外部容器 / 关闭卸载 / 只改图例不重排标签。`selftest-perf` 补 1.2 万点自动值域、50 次同签名更新零 mutation、定向改数据后值域跟随;在稳态标签量测前等待 `document.fonts.ready` 后再 settle,避免冷启动字体失效把真实一次量测误算成回归,原有阈值与断言不放宽。`selftest-docs` / `selftest-demos` 的成员与 options 计数同步为 283 / 106。
662
+
663
+ **核心文件**:`src/gis-view/visual-map.js`、`heatmap.js`、`create.js`、`gis-view.css`、`types.d.ts`、`src/gis-view/README.md`;`docs/manual/03` / `11` / `13` / `15` / `16` / `17` / `18`、`docs/GisView.md`、`docs/index.html`、`docs/demos/gis-view-demo/useGisViewDemo.js`、`panels/StylePanels.vue`、`docs/steps/selftest-features.html` / `selftest-perf.html` / `selftest-demos.html` / `selftest-docs.html`、`docs/demos/api-index.js`、`docs/AI对话汇总.md`、`docs/ai/06-会话索引与复查方法.md`。
664
+
665
+ **验证**:`npx tsc --noEmit` 0 错;`node --check` 覆盖图例 / 热力图 / 组件入口 / Demo 组合式函数;`node docs/steps/gen-api-index.mjs` 生成 283 个成员,`--check` 与 `npm run check:docs` 全绿(283 个实例成员 / 278 个方法 / 5 个属性 / 106 个 options / 274 个数据字段);`npm run build` 通过(仅既有大 chunk 提示);`git diff --check` 通过。浏览器自测:`selftest-features` **808/0**、`selftest-perf` **190/0**、`selftest-demos` **441/0**、`selftest-docs` **139/0**、`selftest-demo` **133/0**。
666
+ ### 15.109 线路热力交互连续显示、热力快路径与 visualMap 自定义交互(2026-10-31,对着「修改.txt」三条)
667
+
668
+ **需求**(「修改.txt」三条):① 开启热力图后平移 / 缩放时线路热力先消失再出现;② 继续优化开启热力图后的交互性能;③ 图例支持完全自定义和交互,参考 ECharts `visualMap`。
669
+
670
+ **热力消失的两个根因**:相机移动会重铺线路实例,`buildLineInstances()` 原来没有把 `heat` 写进重铺后的实例,线路热力值随即归零;同时 `LINE_VERTEX_SHADER` 在顶点阶段把横向坐标写成了 `abs(a_corner.y)`,两个端点都是 `1`,插值后整块四边形恒为 `1`,片元的光晕衰减失效。现在实例统一补 `heat: line.heat ?? 0`,顶点保留带符号的 `v_edge`,片元里再取 `abs(v_edge)`。
671
+
672
+ **交互期合并线**:新增 `HEAT_LINE_FRAGMENT_SHADER` 与 `heatLineProgram`,交互 / 程序化相机运动期由 `drawHeatLineBatch()` 用一笔实例化绘制同时输出本色核与外侧光晕,不再先画本色、再补一遍光晕,也不额外增加 draw call;`u_coreWidthBoost` / `v_coreHalfWidth` 让扩到光晕宽度的四边形仍按原线宽恢复内核。合并批次使用预乘普通混合 `(ONE, ONE_MINUS_SRC_ALPHA)`,避免内核与光晕单遍相加时多条重叠线被推到白色;停稳后的两遍绘制仍按 `glow.blend` 的加法 / 普通混合走原路。
673
+
674
+ **热力性能收口**:密度场四边形改存独立的 `heatDensityBuffer`,按 `STATIC_DRAW` 上传,不再每帧重传;交互与相机运动期跳过密度场重烤,停稳后再补烤。节点光晕在交互期继续让路(`pointGlowOn = glowOn && !(interacting || cameraMoving)`),线路热力则通过合并管线保持连续可见,兼顾“不闪”和减少圆面片开销。
675
+
676
+ **visualMap 交互与完全自定义**:`visual-map.js` 增加 `interactive`、`selectedMode: 'single' | 'multiple' | false`、`selected`、`range`、`calculable`、`onChange` 与 `render(context)`。分段图例支持鼠标点击和 Enter / Space 切换,并可单选 / 多选;连续尺支持区间遮罩、双滑块、拖动单端或整体,以及方向键 / Shift / Alt / PageUp / PageDown / Home / End。`onChange` 通过 rAF 合帧一次,载荷含 `type,target,field,domain,interactive,selectedMode,calculable,selected,range,rangeRatios,element,source,reason,originalEvent`;受控 `updateOptions({ visualMap })` 不回声触发回调。`render(context)` 可返回 DOM / DocumentFragment / HTML 字符串,或直接写 `context.body`,上下文提供 `setSelection` / `setRange` / `clearSelection` / `emitChange`;图例读数同步返回 `interactive,selectedMode,calculable,selected,range,rangeRatios`。图例仍不自动改业务筛选,选择结果由使用方在 `onChange` 中处理。
677
+
678
+ **Demo、文档与公开面**:热力面板增加图例交互开关、完全自定义开关与最近一次 `onChange` 读数,自定义示例用分段色块按钮和连续尺按钮演示 `setSelection` / `setRange` / `clearSelection`。手册 03 / 11 / 13 / 15 / 16 / 17 / 18、`docs/GisView.md`、组件 README、CSS 与四个自测页同步。没有新增实例成员 / options / 数据字段,公开面保持 283 个实例成员(278 方法 + 5 属性)/ 106 options / 274 数据字段。
679
+
680
+ **核心文件**:`src/gis-view/shaders.js`、`engine.js`、`heatmap.js`、`visual-map.js`、`create.js`、`types.d.ts`、`gis-view.css`、`src/gis-view/README.md`;`docs/demos/gis-view-demo/useGisViewDemo.js`、`panels/StylePanels.vue`、`demo.css`、手册 03 / 11 / 13 / 15 / 16 / 17 / 18、`docs/GisView.md`、`docs/steps/selftest-features.html` / `selftest-perf.html` / `selftest-demos.html` / `selftest-docs.html`、`docs/AI对话汇总.md`、`docs/ai/06-会话索引与复查方法.md`。
681
+
682
+ **验证**:`node --check` 覆盖着色器 / 引擎 / 热力图 / 图例 / 组件入口 / Demo;`npx tsc --noEmit` 0 错;`node docs/steps/gen-api-index.mjs` 生成 283 个成员,`--check` 与 `npm run check:docs` 全绿(283 个实例成员 / 278 个方法 / 5 个属性 / 106 个 options / 274 个数据字段);`npm run build` 通过(仅既有大 chunk 提示);`git diff --check` 通过。浏览器串行自测:`selftest-features` **823/0**、`selftest-perf` **195/0**、`selftest-demos` **441/0**、`selftest-docs` **139/0**。
683
+
684
+ ### 15.110 热力密度几何隔离、线路热力连续与 visualMap 运行时门控(2026-11-01,对着「修改.txt」三条)
685
+
686
+ **需求**:① 开启热力图后平移 / 缩放时,仍有部分线路热力先消失再出现;② 开启热力图后背景也会异常放大缩小;③ `visualMap` 交互当前没有实际效果,应参考 ECharts 让选区真正影响热力表现。
687
+
688
+ **背景缩放根因与修复**:密度场虽然有自己的 `heatDensityBuffer`,`renderHeatDensity()` 也把四边形写进了这块缓冲,但 `drawImageQuad()` 没有接收 VAO 参数,始终绑定背景图片的 `imageVao`。VAO 记的是创建时绑定的缓冲,因此密度场实际按背景图片的四边形绘制,相机 / 背景一变就一起缩放。现在新增独立 `heatDensityVao`,`drawImageQuad()` 支持可选 VAO,密度场用 `createImageVao(heatDensityBuffer)` 建自己的几何;背景仍用 `imageVao`,销毁时两块 VAO 分别释放。
689
+
690
+ **线路热力连续性**:旧实现在交互 / 相机运动时改用「无光晕 + 细线快路径」,自动画质降档还可能直接关闭热力光晕,于是线路热力会先消失、停稳后再出现。现在 `engine.js` 不再让 `(interacting || cameraMoving)` 决定线路热力管线:普通 / 自动浅底配置固定走 `drawHeatLineBatch()`,一笔输出本色核与外侧光晕;需要加法混合或驱动属性槽位不匹配时固定走两遍 `drawLineHeatGlow()`。两条路径都只由配置决定,运动与停稳帧一致。节点光晕仍按交互 / 质量档让路,但不会再连带丢掉线路热力;无光晕时才允许细线快路径。
691
+
692
+ **visualMap 运行时门控**:`visual-map.js` 新增 `visualStateSnapshot()` / `emitVisualState()`,把 `type` / `target` / `field` / `selected` / `range` / 选中的 `pieces` 发成内部纯数据,状态不变不重复通知。`create.js` 保存首帧状态,在引擎创建后调用内部 `setVisualMapState(state, false)`,避免首次解析后再多刷一次;后续选择、键盘、API 与受控配置更新都会同步。`heatmap.js` 的 `visualStateAllows()` 只门控与图例同 `target` 且同 `field` 的热力;落选值回普通样式,数据不删除、筛选不变,也不误伤独立图例或另一类要素。默认继承主图热力的鹰眼图读取同一状态,选区变化时只刷新小图配色;显式 `minimap.heatmap` 不跟随。分段空选择关闭对应热力,连续尺按闭区间判断;`setVisualMapState()` 只重算热力样式、光晕与密度权重,几何 / 背景 / 标签不重排,且没有加入公开实例 API。
693
+
694
+ **测试与文档**:`selftest-features` 增加分段取消 / 受控选择、连续尺区间外门控、密度场重采样、继承鹰眼图同选区与显式 `minimap.heatmap` 独立断言;`selftest-perf` 增加图片背景 + 密度场范围断言、18 条线路中点逐条采样,以及加法混合管线断言。新增的密度背景用例原来在 `sleep(300)` 后调用静态画面的 `whenRendered()`,会一直等不到下一帧;现改为先兑现相机帧、等图片解码后再 `refresh()` 并等帧。手册 03 / 11 / 13 / 14 / 15、`docs/GisView.md`、`types.d.ts`、组件 README、Demo 提示与 API 索引同步为「内置门控 + onChange 额外联动」。
695
+
696
+ **核心文件**:`src/gis-view/engine.js`、`heatmap.js`、`visual-map.js`、`create.js`、`types.d.ts`、`src/gis-view/README.md`;`docs/manual/03` / `11` / `13` / `15`、`docs/GisView.md`、`docs/demos/gis-view-demo/useGisViewDemo.js`、`panels/StylePanels.vue`、`docs/steps/selftest-features.html` / `selftest-perf.html`、`docs/AI对话汇总.md`、`docs/ai/06-会话索引与复查方法.md`。
697
+
698
+ **验证**:`node --check` 覆盖引擎 / 热力图 / 图例 / 组件入口;`npx tsc --noEmit` 0 错;`npm run check:docs`、`npm run build`、`git diff --check` 通过;API 索引保持 283 个实例成员(278 方法 + 5 属性)/ 106 options / 274 数据字段。浏览器串行自测:`selftest-features` **828/0**、`selftest-demos` **441/0**、`selftest-docs` **139/0**、`selftest-perf` **197/0**。性能页此前一次完整运行曾出现既有的「改一条可见标签之后的定向重排」布局读数断言失败(1313 次),最终复跑未再出现,与本轮改动无调用关系。
699
+
700
+ ### 15.111 toggleSelect、visualMap 落选门控与交互快路径(2026-11-01,对着「修改.txt」三条;不考虑分组详情)
701
+
702
+ **需求**:① 新增取消选中开关,默认关闭;打开后单击选中的要素再次单击取消,双击选中的再次双击取消,Ctrl / ⌘ 先选中后再普通单击也要取消;② 继续优化交互性能;③ `visualMap` 落选时与热力图 / 网架联动,可选择只退热力、隐藏对应网架,或完全交给业务处理。用户明确「不考虑分组详情」,本轮没有扩展任何分组详情能力。
703
+
704
+ **toggleSelect**:顶层 `options.toggleSelect` 缺省 `false`,`create.js` 的 `pickEngineOptions()` 透传,`engine.js` 收进 `settings`,`updateOptions({ toggleSelect })` 走纯表现快路径,不重建场景,也没有新增实例方法。点击分流按原始选中结果判断:单击模式下,命中项已在选中集且本来会改选中集时,先走 `toggleSelection()` 取反;同一轮双击的第二下由 `keepSelection` 保持,不再二次取反。`selectTrigger: 'dblClick'` 下第一次点击只派发 `onClick`,第二下才取反;再次双击同一要素取消。Ctrl / ⌘ 先选中的要素,后续不按修饰键普通单击也会命中「已选中」分支并取消,不必同时打开 `multiSelect`。取反时 `onClick.action.type` 与 `onSelectChange.action.type` 都是 `toggleSelect`,双击仍补发原有 `onDblClick`;开关关闭时所有路径保持原行为。
705
+
706
+ **outOfRange 归一化**:`visual-map.js` 新增 `OUT_OF_RANGE_MODES` / `normalizeOutOfRange()`,只接受 `'heatmap' | 'network' | 'none'`,缺省或非法值按 `'heatmap'`。归一化结果进入图例状态快照、`getVisualMap()`、`render(context)` 与 `onChange` 载荷;`types.d.ts` 增加 `GisViewVisualMapOutOfRange` 并补齐这些读数。`heatmap.js` 抽出 `visualStateIncludes()`,主图热力与网架隐藏共用同一套分段开闭端点 / 连续尺闭区间判定。
707
+
708
+ **network 隐藏账**:引擎把原来的隐藏集合拆成筛选基线 `filterHiddenPointIndexes` / `filterHiddenLineIndexes` 与 visualMap 派生集合 `visualHiddenPointIndexes` / `visualHiddenLineIndexes`,解析、渲染、拾取和包围盒读取两者并集。`computeVisualHiddenIndexes()` 只在 `outOfRange: 'network'` 时按 `target` 计算:`point` 检查节点自身字段;`line` 按通道声明先收全部成员回线,排除成员后只隐藏独立线路,避免误伤通道;`channel` 检查通道声明自身字段,落选时隐藏该通道声明下的全部成员回线,包括暂时没有锚点或处在隐藏分组里的成员。缺值、空串与非数值不参与隐藏。三档都只改视觉;数据、筛选结果、选中集与通道分组声明不删除、不改写。
709
+
710
+ **刷新与性能**:`setVisualMapState()` 先重推隐藏集合再决定后续:集合内容不变时返回 `{ hiddenChanged: false }`,不重建场景;集合变了才 `markHeatDensityDirty()` + `buildScene()`,并让组件同步标签、提示与鹰眼图。`outOfRange: 'none'` 直接视为不门控;只有旧 / 新状态至少一侧真的门控节点或线路时,才让对应那一半的样式缓存失效。`resolvedStyleInputs` 由单一 `visualMapKey` 拆成 `pointVisualMapKey` / `lineVisualMapKey`,图例只影响节点时不再重算线路的热力样式,反之亦然。`create.js` 在 `network` 隐藏集合变化时重刷标签与提示;鹰眼图的刷新条件包含 `hiddenChanged`,所以即使显式配置了独立 `minimap.heatmap`,落选网架也不会留在小图里。
711
+
712
+ **Demo、测试与文档**:Demo 的视图面板增加「再次选中即取消」开关,热力面板增加落选值下拉并同步三档提示。`selftest-select` 新增 5 条,覆盖单击模式再次单击、双击模式再次双击、Ctrl 先选后普通单击、两类事件的 `toggleSelect` 动作与关闭态;`selftest-features` 新增 8 条,覆盖三种 `outOfRange`、节点 / 独立线路 / 通道、数据与筛选不变、隐藏集合不变不重建与鹰眼图同步。`selftest-perf` 的「改一条可见标签之后」原来把 4 万点 / 4 万线视图挂在 `body` 末尾,视口外的发散渲染会暂停;六轮 `updateOptions` 读到 0 次后,`updatePoint` 的兜底重排才第一次把 1313 条标签真正排出来,导致偶发误报。该用例改为 `progressiveInit: false`、显式 `labelOptimize: true`,并在计量前等待 `isLayoutReady()`;连续三次均 197/0。手册 03 补 `visualMap.outOfRange` 和 `toggleSelect`,手册 08 补选中取反语义,手册 13 补门控载荷与 `network` 细节,手册 14 补鹰眼图同步;`docs/GisView.md`、组件 README、API 索引、`docs/AI对话汇总.md` 与本页同步。
713
+
714
+ **核心文件**:`src/gis-view/engine.js`、`create.js`、`heatmap.js`、`visual-map.js`、`update-policy.js`、`types.d.ts`;`docs/manual/03` / `08` / `13` / `14`、`docs/GisView.md`、`src/gis-view/README.md`、`docs/demos/gis-view-demo/useGisViewDemo.js`、`panels/ViewPanels.vue`、`panels/StylePanels.vue`、`docs/steps/selftest-select.html` / `selftest-features.html` / `selftest-perf.html`、`docs/AI对话汇总.md`、`docs/ai/06-会话索引与复查方法.md`。
715
+
716
+ **验证**:`npx tsc --noEmit` 0 错;`node docs/steps/gen-api-index.mjs` 生成 283 个成员;`npm run check:docs` 为 283 个实例成员(278 方法 + 5 属性)/ 107 个 options / 275 个数据字段;`npm run build`、`git diff --check` 通过。浏览器串行自测 `selftest-select` **107/0**、`selftest-features` **836/0**、`selftest-demos` **441/0**、`selftest-docs` **139/0**、`selftest-perf` **197/0**。
717
+
519
718
  [← 15-02 逐轮记录:性能与修复(上)](15-02-%E9%80%90%E8%BD%AE%E8%AE%B0%E5%BD%95-%E6%80%A7%E8%83%BD%E4%B8%8E%E4%BF%AE%E5%A4%8D-%E4%B8%8A.md) · [实现原理目录](../GisView实现原理.md) · [15-04 逐轮记录:热力图与性能 →](15-04-%E9%80%90%E8%BD%AE%E8%AE%B0%E5%BD%95-%E7%83%AD%E5%8A%9B%E5%9B%BE%E4%B8%8E%E6%80%A7%E8%83%BD.md)
@@ -9,7 +9,7 @@
9
9
  - **想把视图级逻辑打包复用**:写成 `(view, context) => hook` 的插件 —— 创建时挂走 `options.plugins`,之后才决定要挂的走 `view.use(plugin)`(立即执行,返回幂等的卸载函数;清理写在插件自己的 `destroy` 里,见「插件」一节)。插件拿到的就是实例,只用公开 API 的插件不随内部重构失效。
10
10
  - **想加一种符号**:只是一支画笔(三角 / 星形 / 带角标的图标…)直接 `registerSymbolPainter(name, painter)` 注册,或在那一条数据上写 `{ type: 'custom', draw }`,不用改库 —— 走贴图那条路(尺寸档 / 缓存 / 预热全复用,见 16.61);要的是新图元类型本身(新的形状着色器、新的贴图来源),才在 `symbols.js` 的 `normalizeSymbol` 里加分支(返回 `{ kind, texture | shape, width, height }`),形状类的在点着色器里加一段距离场。
11
11
  - **想加一种交互**:`handlePointerDown` / `handlePointerMove` / `handlePointerUp` 三个入口里加状态机分支,光标分类在 `CURSOR_KEYS` 里加一个键。
12
- - **想加一种数据字段**:以「要素自身字段 > 规则 > 全局样式」的顺序接通解析(`point.js` / `line.js` 各一处),再补 `types.d.ts` 与文档。
12
+ - **想加一种数据字段**:按「`override` 规则 > 要素自身字段 > 普通规则 > 全局样式」的顺序接通解析(`point.js` / `line.js` 各一处),再补 `types.d.ts` 与文档。
13
13
 
14
14
  ---
15
15
 
@@ -323,6 +323,38 @@ node scripts/bench-scale.mjs --points=100000 --lines=50000 --gpu=true --panFrame
323
323
 
324
324
  16 ~ 18 ms 已接近 60Hz 帧节拍,不能再用它宣称无上限提升;要继续明显提速,只能减少可见线段数或屏幕覆盖面积。细线快路径的实际门槛是最大半宽 ≤ 0.6 设备像素且无虚线(dpr = 1 时约 1.2px 线宽),2px 实线不会走 `gl.LINES`,这也是「某些场景感觉提升有限」的直接原因。`selftest-perf.html` 新增 1 条像素级对拍,验证细线与四边形在同一交互状态下位置 / 颜色一致、不会整批画丢;该页 182 / 1(共 183 条,标签读数失败在 HEAD 已复现)。
325
325
 
326
+ #### 第二十轮:DOM 保留框跟随相机与拾取去分配
327
+
328
+ `selectionBoxStyle.render: 'div'` 的保留框原来只有屏幕矩形:相机一移动,canvas 里的框随世界顶点走,DOM 框却停在原处。这一轮在 `setRetainedSelectionBox()` 时同时记下 `selectionBoxWorld`,`render()` 在校准相机后调用 `syncSelectionBoxToCamera()`;`getSelectionBox()` 与 `selectionBoxContains()` 也先按当前相机同步,避免读数 / 命中与画面差一帧。框内整组拖动改用 `selectionBoxWorldStart + 世界坐标增量`,和节点共用同一份位移。
329
+
330
+ DOM 写入按变化分层:平移只改 `translate3d`,宽高只在缩放 / 拖动时写,填充、描边、线型、圆角与类名由 `selectionBoxStyleVersion` 控制,相机移动不会重复执行 `parseColor()`、`className` 或 CSS 变量写入;`syncSelectionBoxScreenFromWorld()` 直接改写现有矩形,不再每帧造投影数组和中间对象。`.gis-view__selection-box` 加 `will-change: transform`,让平移留在合成层。
331
+
332
+ 拾取热路径也去掉逐候选分配:`pick()` / `pickDraggablePoint()` 把相机中心、比例、半视口提到循环外,内联投影后用平方距离比较,只在最终命中时开方并构造 `screen`;聚合圆复用同一组常量。每次拾取的候选只有光标附近的一小撮,因此收益主要是减少 GC 抖动,而不是把亚毫秒的 p50 再压低。
333
+
334
+ 同一台机器、同一参数各跑 3 次(无头 Chrome + ANGLE / AMD 集显,1280×800;改前为上一版单次基线):
335
+
336
+ | 读数 | 改前 | 改后 3 次 |
337
+ | --- | --- | --- |
338
+ | 平移 p50 / p95 | 6.9 / 33.8 ms | 7 / 14.9 ~ 22.4 ms |
339
+ | 缩放 p50 / p95 | 18.4 / 25.2 ms | 18.2 ~ 19.0 / 24.0 ~ 24.9 ms |
340
+ | 拾取 p95 / avg | 0.2 / 0.06 ms | 0.2 ~ 0.3 / 0.05 ~ 0.07 ms |
341
+ | `refresh()` | 92.9 ms | 88.5 ~ 135.2 ms |
342
+
343
+ 平移与刷新的跨度里混着无头浏览器帧节拍、GC 和合成抖动,不能把差值单独算作这两处优化的功劳;结论只落在确定的部分:保留框平移不再反复写样式 / 造矩形,拾取不再逐候选造数组,`selftest-select` 对 div 框平移 / 缩放后的 `getSelectionBox()`、DOM 位置与尺寸做了像素级校验。
344
+
345
+ ### 规则合并微基准(2026-10-30)
346
+
347
+ 本轮没有动渲染管线,量的是解析期最容易随规则条数放大的一段:10 万要素、4 条规则、每条覆盖 9 个样式字段中的若干项。基准在同进程交替跑旧实现与新实现,各取中位,输出逐要素样式校验和:
348
+
349
+ | 场景 | 中位耗时 | 相对 |
350
+ | --- | --- | --- |
351
+ | 旧实现:每条命中规则遍历全部 9 个字段并判空 | 21.82 ms | 1.00× |
352
+ | 新实现:编译期记 `fields`,运行时只遍历实际写到的键 | **17.69 ms** | **1.23×** |
353
+
354
+ 节省约 `4.13 ms`;两版校验和一致。另一半收益来自分流:普通规则写 `ruleScratch`,`override: true` 规则写 `ruleOverrideScratch`;规则表上没有 override 时 `RULE_HAS_OVERRIDE` 为 false,逐要素连第二份 scratch 的清理都跳过。两份 scratch 都是复用的,逐要素仍零分配。
355
+
356
+ 这是 Node 侧规则合并微基准,不是浏览器帧耗时;组件页面的主成本仍是填充率、标签覆盖层与实例上传,相关数据看上面的实测表。功能语义由 `selftest-features.html` 的优先级断言覆盖。
357
+
326
358
  ### WebGPU 值不值得上
327
359
 
328
360
  | 维度 | 现状 | WebGPU 能带来的变化 |
@@ -14,7 +14,7 @@
14
14
  - **选哪一对**:只要世界坐标(读落点、对账)用 `clientToWorld` / `worldToClient`;要把鼠标位置喂给只吃画布像素的接口 —— `pick` / `getContextAt` / `nearestPointsAt` / `nearestLineAt` / `getFeaturesInRect` / `selectInBox` / `setDropPreview({ screen })` / `zoomTo(scale, origin)` 的锚点 —— 先用 `clientToScreen` 转一档(世界坐标喂不进这些接口);手上已经握着画布像素(`getPointer().screen`、拾取载荷的 `screen`、`listPoints({ withScreen })`),要摆 `position: fixed` 的浮层用 `screenToClient`。两对数学上可互推(`clientToScreen` 的结果就等于 `project(clientToWorld(...))`),专名省掉绕世界坐标那一步,也与指针事件共用同一份 canvas rect 缓存 —— 组件自己的提示框跟随(`handlePointerMove`)走的也是它。
15
15
  - **像素比**:默认取 `window.devicePixelRatio`,用 `pixelRatio` 覆盖。线宽、节点尺寸、虚线图案、光标热点都是 **CSS 像素**,在高分屏上会按像素比放大到设备像素,因此屏幕上看起来始终一样大。另外有一层画布**像素预算**(`maxCanvasPixels`,默认 400 万):全屏 / 高分屏下 CSS 面积一大,渲染倍率就按 `sqrt(预算 / CSS 面积)` 压下来 —— 填充率不随屏幕面积线性上涨(全屏 + dpr 2 曾经是窗口态的十倍像素),线宽与字号仍然是 CSS 像素,观感基本不变。
16
16
  - **宿主缩放**:页面用 CSS `zoom`(大屏适配的常见做法:`document.body.style.zoom = innerWidth / 1920`)或祖先的 `transform: scale` 把这一层整体放大 / 缩小时,组件里的 **CSS 像素一律是布局像素** —— 线宽、节点尺寸、字号跟着页面一起变大,与旁边的 DOM 一致(不是保持设备像素大小不变)。画布备份像素按「设备像素比 × 显示比例」开,缩放之后仍然 1:1 落在设备像素上,画面不发糊。`getViewport()` 里的 `displayScale` 就是这个比例(1 表示没有缩放):自建 DOM 覆盖层时,`getBoundingClientRect()` 量到的是缩放后的视觉像素,除以它才是与 `project()` / `unproject()` 同一套坐标。指针事件(`onHover` / `onPointer` / `getPointer`)与 `project` / `unproject` 已经是布局像素,直接用;`onViewport` 回调也会带上这个字段。**运行期**改 CSS `zoom` 不用自己做什么:内容盒与设备像素两档 `ResizeObserver` 各挂一个实例(同一个实例对同一元素再 `observe` 一次是改选项、会把前一次顶掉),放大 / 缩小都会自己补一次重排(逐帧实测:第 2 帧起显示比例 / 备份像素 / 标签偏移 / 指针命中全部对齐,demo 的「宿主缩放」滑杆来回拉也跟得上),运行期改祖先的 `transform: scale` 不动布局、观察不到,把新比例交给 `view.resize()`(或在窗口 resize 时一起改)即可同步;显示比例 1% 以内按 1 处理、夹在 0.1 ~ 8,鹰眼图的备份像素在这条乘积小于 1 时按 1 倍开(轻微超采样,只会更清楚)。
17
- - **尺寸的三个层次**(节点与线路都有):要素自身的 `size` / `width` → 规则的 `size` / `width` → 全局 `pointStyle` / `lineStyle`;再乘 `pointSizeScale` / `lineWidthScale`(常数倍数)与地图缩放的联动指数 `pointZoomScale` / `lineZoomScale`(0 表示不跟随,1 表示与地图同倍率)。
17
+ - **尺寸的四个层次**(节点与线路都有):全局 `pointStyle` / `lineStyle` → 普通规则的 `size` / `width` → 要素自身 → `override: true` 的规则。普通规则始终高于全局,但仍低于要素自身;只有 `override` 规则写了的字段才压过要素字段。再乘 `pointSizeScale` / `lineWidthScale`(常数倍数)与地图缩放的联动指数 `pointZoomScale` / `lineZoomScale`(0 表示不跟随,1 表示与地图同倍率)。
18
18
 
19
19
  - **经纬度与 GeoJSON**:配了 GeoJSON 背景(`background.geoJson`)就多一套换算:`lngLatToWorld` / `worldToLngLat` 把经纬度接到画出来的世界坐标上(`aspectScale` 只压经度、绕整体范围中心),`lngLatToScreen` / `screenToLngLat` 直接连到画布 CSS 像素;数据不在底图的坐标系里时用 `createGeoMapper` / `mapDataToGeoJson` 把点、线路与通道搬到画出来的底图上,见「经纬度与数据映射」。
20
20