topo-engine 0.1.0 → 0.1.2

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.
package/README.md CHANGED
@@ -1,163 +1,366 @@
1
- # 配电台区单线图拓扑成图引擎
1
+ # topo-engine
2
2
 
3
- 基于 **Vue3 + AntV G6 v5 + Vite(Node)** 的配电台区单线图拓扑成图引擎。
4
- 解析国网 PMS 台区拓扑 JSON,在 Web 端以 **深色 SCADA / SVG 单线图风格** 渲染**横平竖直、0 交叉**的树状拓扑图。
3
+ 配电台区(低压配电网)**单线图拓扑成图引擎**,基于 **Vue 3 + AntV G6 v5**。
4
+
5
+ 输入一份台区拓扑 JSON,自动完成「解析 → 建树找根 → **横平竖直、0 交叉** 的正交布局 → 深色 SCADA 风格渲染」,并提供开箱即用的 Vue 组件与一整套拓扑查询 / 样式 / 动画 / 标注控制 API。
5
6
 
6
7
  ## 功能特性
7
8
 
8
- - 🔍 **完整的拓扑查询 API** - 节点查询、路径查找、邻居关系等
9
- - 🎨 **丰富的样式控制** - 节点样式、边样式、高亮标注
10
- - **动画效果** - 节点脉动、边流动动画
11
- - 📝 **文字标注** - 附加文字、用户名显示控制
12
- - 🃏 **卡片盒** - 节点信息弹窗
13
- - 🔗 **连接线/拓扑线** - 动态连接线、拓扑高亮线
14
- - 🎧 **事件系统** - 节点点击、悬停等事件监听
15
- - 💾 **导出功能** - PNG 导出、数据导出
9
+ - 🧩 **Vue 3 组件** — `TopoGraph`(画布,含标题栏 / 统计 / 适应画布 / 导出 PNG 按钮)、`SidePanel`(设备详情与上下游)、`DistanceTop10`(父子连线距离 Top10 面板)
10
+ - 📐 **自研正交布局** 主干沿最长路径横向铺开 + 分支带垂直直走,整图**横平竖直、0 交叉、0 重叠**,边缘路由为直线 / L 形折线
11
+ - 🔍 **拓扑查询 API** — 节点 / 边 / 邻居 / 子树 / 主干链 / 路径 / 坐标 查询
12
+ - 🎨 **样式控制** 节点、边样式覆盖与批量重置
13
+ - **动画与高亮** 节点脉动、边流动、路径高亮、其余变暗、异常标注
14
+ - 📝 **文字标注** 附加文字、用户名 full / short / hidden 切换
15
+ - 🃏 **卡片盒** 节点信息卡片(字段 + 按钮),适配"运行曲线/工单跳转"类场景
16
+ - 🔗 **连接线 / 拓扑线** — 任意两节点动态连接线、路径拓扑高亮线
17
+ - 🎧 **事件系统** — 点击 / 悬停 / 画布空白 / 视口变化事件总线
18
+ - 💾 **导出** — 整图 PNG 下载、DataURL、图数据 JSON
19
+ - 📦 **零配置渲染** — 组件自带深色主题与工具栏,`data` 或 `dataUrl` 二选一即可出图
16
20
 
17
21
  ## 安装
18
22
 
19
23
  ```bash
20
- # npm
21
24
  npm install topo-engine
25
+ # 或 yarn add topo-engine / pnpm add topo-engine
26
+ ```
22
27
 
23
- # yarn
24
- yarn add topo-engine
28
+ - 环境要求:**Vue 3.4+**(peerDependency);`@antv/g6` v5 已作为依赖随包安装,无需手动配置。
29
+ - 组件深色样式不自动注入,使用时需引入样式文件:
25
30
 
26
- # pnpm
27
- pnpm add topo-engine
31
+ ```js
32
+ import 'topo-engine/style.css';
28
33
  ```
29
34
 
30
35
  ## 快速开始
31
36
 
32
- ### 1. 作为 Vue 组件使用
37
+ ### 1. 最小示例 —— 只画一张图
33
38
 
34
39
  ```vue
35
- <template>
36
- <TopoGraph
37
- :data-url="dataUrl"
38
- @node-select="onNodeSelect"
39
- @loaded="onLoaded"
40
- />
41
- </template>
42
-
43
40
  <script setup>
44
41
  import { ref } from 'vue';
45
42
  import { TopoGraph } from 'topo-engine';
43
+ import 'topo-engine/style.css';
46
44
 
45
+ // 两种数据来源任选其一:
46
+ // - dataUrl:内部用 fetch 加载(注意放 public/ 下或跨域可达)
47
+ // - data :直接传入解析好的 JSON 对象(优先级高于 dataUrl)
47
48
  const dataUrl = ref('/data/topology.json');
48
49
 
49
- const onNodeSelect = (payload) => {
50
- console.log('选中节点:', payload.node);
50
+ const onLoaded = ({ title, stats }) => {
51
+ console.log('加载完成', title, stats); // { total, depth, leaves, byCat }
51
52
  };
53
+ </script>
52
54
 
53
- const onLoaded = ({ stats, title }) => {
54
- console.log('加载完成:', title, stats);
55
- };
55
+ <template>
56
+ <!-- 组件撑满父容器,父容器必须有明确高度 -->
57
+ <div class="page">
58
+ <TopoGraph :data-url="dataUrl" @loaded="onLoaded" />
59
+ </div>
60
+ </template>
61
+
62
+ <style scoped>
63
+ .page { height: 640px; }
64
+ </style>
65
+ ```
66
+
67
+ ### 2. 组合示例 —— 图 + 详情面板 + Top10 距离面板
68
+
69
+ ```vue
70
+ <script setup>
71
+ import { ref } from 'vue';
72
+ import { TopoGraph, SidePanel, DistanceTop10 } from 'topo-engine';
73
+ import 'topo-engine/style.css';
74
+
75
+ const topo = ref(null); // ref → 调用完整控制 API
76
+ const graphData = ref(null); // 直接传数据对象
77
+ const panelOpen = ref(false);
78
+ const selected = ref(null);
79
+ const upstream = ref([]);
80
+ const downstream = ref([]);
81
+ const stats = ref({});
82
+ const distances = ref([]);
83
+
84
+ function onSelect({ node, upstream: up, downstream: down }) {
85
+ selected.value = node;
86
+ upstream.value = up;
87
+ downstream.value = down;
88
+ panelOpen.value = true;
89
+ }
90
+ function onTopDistances(list) { distances.value = list; }
91
+ function jumpTo(id) { topo.value?.selectNode(id); }
56
92
  </script>
93
+
94
+ <template>
95
+ <div class="app">
96
+ <div class="graph-area">
97
+ <TopoGraph
98
+ ref="topo"
99
+ :data="graphData"
100
+ @node-select="onSelect"
101
+ @top-distances="onTopDistances"
102
+ />
103
+ <SidePanel
104
+ v-if="panelOpen"
105
+ :node="selected"
106
+ :upstream="upstream"
107
+ :downstream="downstream"
108
+ :stats="stats"
109
+ @close="panelOpen = false"
110
+ @jump="jumpTo"
111
+ />
112
+ </div>
113
+ <DistanceTop10
114
+ v-if="distances.length"
115
+ :distances="distances"
116
+ @close="distances = []"
117
+ @jump="jumpTo"
118
+ />
119
+ </div>
120
+ </template>
121
+
122
+ <style scoped>
123
+ .app { display: flex; height: 720px; }
124
+ .graph-area { flex: 1; display: flex; min-width: 0; position: relative; }
125
+ </style>
57
126
  ```
58
127
 
59
- ### 2. 作为独立 API 使用
128
+ ### 3. 只用布局 / 解析工具函数(不依赖组件)
60
129
 
61
130
  ```javascript
62
- import { TopoApi } from 'topo-engine';
63
-
64
- // 假设你已经有一个 G6 Graph 实例
65
- const graph = new Graph({ /* ... */ });
131
+ import { parseTopo, computeOrthogonalLayout, buildGraphData } from 'topo-engine';
66
132
 
67
- // 加载数据后创建 TopoApi 实例
68
- const topoApi = new TopoApi(graph, model, layout, nodeByIdMap);
133
+ const model = parseTopo(rawJson); // 原始 JSON → 图模型(含 nodes/edges/rootId/stats)
134
+ const layout = computeOrthogonalLayout(model); // 正交布局结果(节点坐标 + 每边折线点序列)
135
+ const g6Data = buildGraphData(model, layout); // → G6 可直接消费的 { nodes, edges }
136
+ ```
69
137
 
70
- // 使用 API
71
- const nodes = topoApi.getAllNodes();
72
- const path = topoApi.getPath('node1', 'node2');
73
- topoApi.highlightPath(['node1', 'node2', 'node3']);
74
- topoApi.setEdgeFlow('node1', 'node2', { color: '#FF0000' });
138
+ > 说明:包内渲染使用的自定义图元节点(`svg-symbol`)与正交折线边(`orth-polyline`)目前仅在 `TopoGraph` 内部注册使用,未作为公开 API 导出。直接用 G6 手工拼图时建议以 `TopoGraph` 组件(或它的 render 流程)为基准,保证节点与边风格一致。
139
+
140
+ ## 输入数据格式
141
+
142
+ 顶层可以是 `{ code, msg, data: { nodes, relationships, stations } }`(常见接口包装格式),也可以是直接 `{ nodes, relationships, stations }` —— 引擎两种都接受。
143
+
144
+ ```json
145
+ {
146
+ "data": {
147
+ "nodes": [
148
+ { "id": 1, "name": "1#台区.", "psrType": "0302", "symbolId": "1664029", "status": "0" },
149
+ { "id": 2, "name": "1#台区低压熔丝.", "psrType": "3301", "symbolId": "68688", "status": "0" }
150
+ ],
151
+ "relationships": [ { "from": 1, "to": 2 } ],
152
+ "stations": [
153
+ { "id": 1, "name": "1#台区.", "contain": ["8ff4786d548a7073a2517297e801518ff456732f4d"] }
154
+ ]
155
+ }
156
+ }
75
157
  ```
76
158
 
77
- ## 运行
159
+ | 字段 | 说明 |
160
+ | ---- | ---- |
161
+ | `nodes[]` | 设备/线路节点:`id`、`name`(可带结尾 `.`,渲染时会去掉)、`psrType`(图元类型码)、`symbolId`、`psrId`、`status`(`"0"` 视为正常) |
162
+ | `relationships[]` | 有向边 `{ from, to }`,方向为 电源 → 负荷;自动防环(BFS),入度为 0 者作为根,优先选变压器/开关 |
163
+ | `stations[]` | 台区/站房:`name` 作为画布标题,`contain` 内为箱内设备的 `psrId` 列表 |
78
164
 
79
- ```bash
80
- npm install # 安装依赖
81
- npm run dev # 开发服务器 http://127.0.0.1:5173/
82
- npm run build # 生产构建 → dist/
83
- npm run build:lib # 构建库文件
84
- npm run preview # 预览生产构建
165
+ `parseTopo` 后模型节点会附带派生抽像字段:`cat`、`catLabel`、`shortName`、`isLine`、`insideStation`、`isFuse` 等(完整类型见 `dist/index.d.ts`)。
166
+
167
+ 主要 `psrType`图元类别:
168
+
169
+ | psrType | 类别 | 渲染 |
170
+ | ------- | ---- | ---- |
171
+ | `0302` | 配电变压器 | 橙色圆 + 红圈 |
172
+ | `3301` | 开关 / 熔丝 | 红色矩形(开关名自动缩写为 K 编号) |
173
+ | `3303` | 低压母线 | 白色短粗线 |
174
+ | `3202` / `320300000` | 电缆终端头 / 分接点 | 三角 / 红色米字 |
175
+ | `310100000` | 低压导线 | 青色连接点(名称缩写为杆号区间) |
176
+ | `3112` | 计量箱 | 黑底白框 "JX" |
177
+ | `3218000` | 用户接入点 | 白圈 "J"(名称缩写为户号) |
178
+ | `32500000` | 低压配电箱(台区) | 站房 |
179
+
180
+ ## TopoGraph 组件参考
181
+
182
+ ### Props
183
+
184
+ | Prop | 类型 | 默认 | 说明 |
185
+ | ---- | ---- | ---- | ---- |
186
+ | `dataUrl` | `String` | `''` | 拓扑 JSON 的 URL(内部 `fetch` 加载)。变化后自动重新加载 |
187
+ | `data` | `Object` | `null` | 直接传入 JSON 对象;优先级高于 `dataUrl`;深度 watch,变化自动重绘 |
188
+
189
+ ### Emits
190
+
191
+ | 事件 | 载荷 | 触发时机 |
192
+ | ---- | ---- | ---- |
193
+ | `loaded` | `{ title, stats }`,`stats = { total, depth, leaves, byCat }` | 每次数据渲染完成 |
194
+ | `node-select` | `{ node, upstream, downstream }`(均为解析后模型节点) | 点击节点(含金框选中与详情联动) |
195
+ | `top-distances` | `Array<{ parent: {id,name,catLabel}, child: {id,name,catLabel}, distance }>`(≤10) | 渲染完成时,父-子连线折线长度 Top10(过滤站内边与主干鱼骨边) |
196
+
197
+ ### 内置交互与 UI
198
+
199
+ - 滚轮缩放、拖拽平移、悬停高亮(上下游与连线),点击选中(金色虚框,再次点空白取消)。
200
+ - 左上角深色工具栏:标题 / `节点 n · 边 m · 深度 d · 叶子 l` 统计 / 「适应画布」/「导出 PNG」按钮。
201
+ - 加载中 / 出错(如 dataUrl 404)有内置遮罩提示。
202
+ - 卸载时自动 `destroy()` 图实例与事件监听,无内存泄漏。
203
+
204
+ ### 通过 ref 调用的方法
205
+
206
+ `<TopoGraph ref="topo">` 后,`topo` 暴露 **`TopoApi` 的全部公开方法**(下节 API 表),外加:
207
+
208
+ | 方法 | 说明 |
209
+ | ---- | ---- |
210
+ | `loadRaw(jsonText)` | 用一段 JSON 文本(如本地文件上传内容)重新成图 |
211
+ | `selectNode(id)` | 选中某节点并联动 `node-select` 事件 |
212
+ | `resetView()` | 适应画布 |
213
+ | `exportPng()` | 导出整图 PNG(按画布标题命名下载) |
214
+ | `getLayoutData()` | 获取当前正交布局数据(坐标 / 折线 / stats) |
215
+ | `getTopDistances()` | 获取最近一次 Top10 距离结果 |
216
+
217
+ ## 控制 API(TopoApi 方法,经组件 ref 直接调用)
218
+
219
+ > 也可自行 `import { TopoApi }`,按 `new TopoApi(graph, model, layout, nodeByIdMap)` 构造后调用同一套方法;事件与查询见下。使用组件时无需关心内部构造。
220
+
221
+ ### 查询
222
+
223
+ | 方法 | 说明 |
224
+ | ---- | ---- |
225
+ | `getNode(nodeId)` | 按 ID 取节点(解析后模型节点) |
226
+ | `getAllNodes()` | 全部节点 |
227
+ | `findNodes(filter)` | 多条件 AND 过滤,`filter` 支持 `{ cat, psrType, name, shortName, status, insideStation, … }`,值可为字符串 / 布尔 / `RegExp` |
228
+ | `getSelectedId()` | 当前选中节点 ID |
229
+ | `getNeighbors(id)` | `{ upstream: [], downstream: [] }` |
230
+ | `getStreamNodes(id, 'upstream'\|'downstream')` | 沿流向 BFS 取节点链 |
231
+ | `getTrunkNodes()` / `getSubtreeNodes(id)` | 主干链节点 / 子树节点 |
232
+ | `getPath(a, b)` | 两节点间路径(不可达返回 `null`) |
233
+ | `getEdge(src, tgt)` / `getEdges()` | 按端点取边 / 全部边 |
234
+ | `getStreamEdges(id, dir)` / `getMainEdge()` | 沿流边的链 / 主干边 |
235
+ | `getNodePosition(id)` / `getEdgePath(src, tgt)` / `getGraphBounds()` | 坐标 / 边折线点序列 / 全图包围盒 |
236
+ | `getStats()` | `{ total, depth, leaves, byCat }` |
237
+
238
+ ### 视图
239
+
240
+ `fitView(padding?)`、`zoomTo(ratio, center?)`、`zoomIn(step?)`、`zoomOut(step?)`、`locateNode(id, zoom?)`、`getViewport()`
241
+
242
+ ### 样式
243
+
244
+ `setNodeStyle(id, style)`、`batchSetNodeStyle(ids, style)`、`resetNodeStyle(id)`、`resetAllNodeStyles()`
245
+ `setEdgeStyle(src, tgt, style)`、`batchSetEdgeStyle([[src,tgt],…], style)`、`resetEdgeStyle(src, tgt)`
246
+
247
+ ### 高亮 / 变暗
248
+
249
+ ```javascript
250
+ topo.highlightNode('5', { color: '#FF0000', label: '异常', labelColor: '#FF0000' });
251
+ topo.unhighlightNode('5');
252
+
253
+ const path = topo.getPath('1', '5'); // 高亮一条供电路径
254
+ if (path) topo.highlightPath(path.map(n => n.id), { color: '#00FF00' });
255
+
256
+ topo.dimOthers(['1', '2'], 0.15); // 除指定节点外整体变暗
257
+ topo.unhighlightAll();
85
258
  ```
86
259
 
87
- ## 数据
260
+ ### 动画
88
261
 
89
- 输入:`public/data/上屋胡2台区单线图.json`(源文件 `上屋胡2台区单线图.json` 的副本,UTF-8)。
262
+ ```javascript
263
+ topo.setNodePulse('2', { color: '#00C8FF', duration: 1500 }); // 节点脉动
264
+ topo.setEdgeFlow('1', '2', { color: '#00C8FF', duration: 2000 }); // 边流动
265
+ topo.removeAnimation('2');
266
+ topo.removeAllAnimations();
267
+ ```
90
268
 
91
- 数据结构:
269
+ ### 文字标注
92
270
 
93
- - `data.nodes[]` 设备/线路节点:`{ id, psrType, symbolId, name, psrId, status }`
94
- - `data.relationships[]` — 有向边:`{ from, to }`(电源 → 负荷,103 节点 102 边构成树)
95
- - `data.stations[]` — 台区根(低压配电箱),`contain` 列出箱内设备 psrId
271
+ `setText(id, text, { color, fontSize })`、`removeText(id)`、`removeAllTexts()`、`setDeviceText(cat, fontSize)`、`changeUserName('full' | 'short' | 'hidden')`
96
272
 
97
- psrType → 图元映射(见 `src/engine/parser.js`):
273
+ ### 卡片盒
98
274
 
99
- | psrType | 设备 | 图元样式 |
100
- | ------- | ---- | -------- |
101
- | 0302 | 配电变压器 | 橙圆 + 红圈(SVG 2030020) |
102
- | 3301 | 开关/熔丝 | 红色矩形(4030010 / 2100010) |
103
- | 3303 | 低压母线 | 白色短粗线 |
104
- | 3202 | 电缆终端头 | 红色三角(3030010) |
105
- | 310100000 | 低压导线 | 青色连接点 |
106
- | 3112 | 计量箱 | 黑底白框 "JX"(5040010) |
107
- | 3218000 | 低压用户接入点 | 白圈 "J"(5010030) |
108
- | 32500000 | 低压配电箱(台区) | — |
275
+ ```javascript
276
+ topo.showCard('5', {
277
+ title: '设备名称',
278
+ fields: [
279
+ { label: '类型', value: '配电变压器' },
280
+ { label: 'PSR编号', value: 'xxx' },
281
+ ],
282
+ buttons: [{ text: '查看运行曲线', type: 'primary', onClick: () => console.log('click') }],
283
+ closable: true,
284
+ });
285
+ topo.hideCard('5');
286
+ topo.hideAllCards();
287
+ ```
288
+
289
+ ### 连接线 / 拓扑线
109
290
 
110
- ## 代码结构
291
+ ```javascript
292
+ topo.addConnection('1', '8', { color: '#FF6600', width: 2, style: 'dashed' }); // 任意两节点连线
293
+ topo.removeAllConnections();
111
294
 
295
+ const lineId = topo.addTopoLine(['1', '3', '8'], { color: '#FF0000', width: 3 }); // 沿线路径画高亮线
296
+ topo.removeTopoLine(lineId);
297
+ topo.removeAllTopoLines();
112
298
  ```
113
- src/
114
- engine/
115
- parser.js # JSON → 图模型(建树、找根、短名提取、类型映射)
116
- layout.js # 主干交替式正交布局引擎(0 交叉)+ 数学校验器
117
- orthEdge.js # 自定义正交折线边(直接用布局引擎的完整点序列绘制)
118
- canvasNode.js # 自定义 canvas 图元节点(@antv/g 原生图形,与边同场景图)
119
- symbols.js # SVG 风格图元规格(尺寸/配色,供 canvasNode 与图例使用)
120
- graphData.js # 模型 G6 数据(节点/边样式、显式正交点序列)
121
- components/
122
- TopoGraph.vue # G6 图组件(布局调用、自定义边/节点、tooltip、点击联动)
123
- SidePanel.vue # 设备详情 / 台区概览
124
- LegendBar.vue # 图例
125
- App.vue # 布局与数据加载
126
- style.css
299
+
300
+ ### 事件总线(`on` / `off`)
301
+
302
+ | 事件 | 载荷 | 说明 |
303
+ | ---- | ---- | ---- |
304
+ | `node:click` | `{ nodeId, node }` | 节点点击 |
305
+ | `node:hover` / `node:unhover` | `{ nodeId, node }` / `{ nodeId }` | 悬停进入 / 离开 |
306
+ | `edge:click` | `{ edgeId, source, target }` | 边点击 |
307
+ | `canvas:click` | `{ event }` | 画布空白处点击 |
308
+ | `viewport:change` | `{ zoom, center }` | 视口缩放 / 平移变化 |
309
+ | `card:button` | `{ nodeId, buttonIndex }` | 卡片按钮点击 |
310
+
311
+ ```javascript
312
+ const handler = ({ nodeId }) => console.log('点击了', nodeId);
313
+ topo.on('node:click', handler);
314
+ topo.off('node:click', handler); // 不传 handler 则移除该事件全部监听
127
315
  ```
128
316
 
129
- ## 渲染要点(强参考 sample/*.svg)
317
+ ### 导出 / 底层
318
+
319
+ `exportPng(filename?)`(整图下载)、`toDataURL(opts?)`、`getGraphData()`、`getGraphInstance()`(原始 G6 Graph)、`getModel()`、`getLayoutData()`、`getNodeByIdMap()`、`update(graph, model, layout, map)`、`destroy()`
130
320
 
131
- - 黑色画布背景(`#050505`,对照 SVG `BackGround_Layer` 的 `rgb(0,0,0)`)
132
- - 导线双色:干线白色、支线灰色(对照 SVG 的 `rgb(255,255,255)` / `rgb(128,128,128)`)
133
- - 图元配色与几何逐项对照 sample SVG 的 `<symbol>` 定义
134
- - **正交布局(0 交叉,见 `src/engine/layout.js`)—— 单向直走式**:
135
- - 主干 = 从根沿**最长路径**前进(每步选到最深叶子距离最大的子节点),主干横向铺开
136
- - 主干节点的其余子节点为侧分支,交替挂下方/上方,带内按内容盒 x 排布
137
- - 侧分支 = **垂直列式(选定方向后不拐弯)**:分支内每步同样选最长路径子节点**同列垂直直走**,
138
- 其余子节点独占新列(同样垂直直走),列内递归同规则——整张图只有横向主干 + 垂直列,无方向交替拐弯
139
- - 边路由:同向链 = 直线;跨列/跨带 = L 形(垂直段 + 水平带线,带线统一高度)
140
- - 数学验证:`assertOrthogonal`(全线段横平竖直)+ `checkZeroCrossing`(0 交叉、0 压线、0 节点重叠)
141
- - 实测:102 条边端点全部落在节点边界、0 非正交段;分支链节点同列直走(如 K02 链 5 节点 x 完全一致)
142
- - 交互:滚轮缩放、拖拽平移、悬停 tooltip(设备/名称/编码/状态)、点击选中(金色虚框 + 右侧详情)
321
+ ## 布局与渲染参数
143
322
 
144
- ## 布局参数(`TopoGraph.vue` → `computeOrthogonalLayout`)
323
+ `computeOrthogonalLayout(model, options)`:
145
324
 
146
325
  | 参数 | 默认 | 含义 |
147
326
  | ---- | ---- | ---- |
148
- | nodeGap | 26 | 主干相邻节点边界间距 |
149
- | band | 50 | 主干轴线到分支带(带线)的距离 |
150
- | packGap | 22 | 分支带内兄弟子树间距 |
151
- | sideGap | 40 | 相邻主干节点分支带之间的隔离间距 |
152
- | margin | 40 | 画布外框留白 |
153
-
154
- ## 备注
155
-
156
- - 布局由自研引擎显式给定节点坐标与边路径点(`orth-polyline` 边直接使用完整点序列,
157
- 首尾即真实边界端点,绕开 G6 内置边对 HTML 节点失效的 `getIntersectPoint`)。
158
- - **节点为自定义 canvas 节点 `svg-symbol`**(`canvasNode.js`):用 @antv/g 原生图形绘制图元,
159
- 与边同在场景图、同一相机 —— 规避 G6 HTML 节点(DOM 覆盖层)不跟随相机变换导致的
160
- fitView/缩放后节点与边错位问题(实测:HTML 节点 DOM 定位停留在世界坐标、无缩放平移)。
161
- - key shape 为透明 Rect(`fill: rgba(255,255,255,0.01)`,alpha=0 不参与命中测试),
162
- 图元/标签/选中高亮框作为其子元素绘制。
163
- - 图元采用 G6 `html` 节点 + 内联 SVG 字符串渲染,文字(宋体)清晰且与 SVG 风格一致。
327
+ | `nodeGap` | `26` | 链上相邻节点边界间距 |
328
+ | `band` | `50` | 侧分支根到父节点中心线的距离(分支带宽) |
329
+ | `packGap` | `22` | 分支带内并排子分支间距 |
330
+ | `sideGap` | `40` | 相邻主干节点的分支带隔离间距 |
331
+ | `margin` | `40` | 画布外框留白 |
332
+
333
+ `TopoGraph` 组件内部即用上述默认参数,返回布局数据含每节点坐标、`edgeCP`(每条边的完整折线点序列)与 `fishboneRibs`,可配 `assertOrthogonal(model, edgeCP)` 做 0 交叉 / 正交性校验。
334
+
335
+ ## TypeScript
336
+
337
+ 类型声明随包发布(`dist/index.d.ts`),支持按需导入:
338
+
339
+ ```typescript
340
+ import { TopoApi, TopoGraph } from 'topo-engine';
341
+ import type { TopoApi } from 'topo-engine'; // 需要类型时
342
+ ```
343
+
344
+ ## 常见问题
345
+
346
+ - **图不显示 / 高度为 0**:`TopoGraph` 撑满父容器(内部 `width/height: 100%`),请给外层元素设置明确高度(如 `height: 600px`)。
347
+ - **样式错乱 / 没有深色背景**:确认已 `import 'topo-engine/style.css'`。
348
+ - **`dataUrl` 加载失败**:确认文件可被 `fetch` 访问(Vite 放 `public/`,不要用相对路径);失败会触发 `error` 态显示并 `console.error`。
349
+ - **切换数据源**:响应式修改 `:data` / `:data-url` 会自动重新加载;本地文件可用 `ref.loadRaw(text)`。
350
+ - **节点 ID 传字符串**:内部统一按字符串存储(`String(id)`),查询 API 请传字符串,如 `getNode('1')`。
351
+ - **和 Vue 2 / 其它 G6 版本混用**:仅支持 Vue 3.4+,图内部依赖 `@antv/g6` v5,请勿重复安装不同版本。
352
+
353
+ ## 本地开发
354
+
355
+ ```bash
356
+ npm install # 安装依赖
357
+ npm run dev # 开发演示(内置 API Playground,可试所有控制 API)→ http://127.0.0.1:5173/
358
+ npm run build # 生产构建(应用 + 库)
359
+ npm run build:lib # 仅构建库产物到 dist/
360
+ npm run preview # 预览生产构建
361
+ npm pack --dry-run # 查看发布包内容清单
362
+ ```
363
+
364
+ ## License
365
+
366
+ MIT
@@ -0,0 +1,115 @@
1
+ /**
2
+ * 拓扑引擎类型声明文件
3
+ */
4
+
5
+ // 核心 API
6
+ export class TopoApi {
7
+ constructor(graph: any, model: any, layout: any, nodeByIdMap: Map<string, any>);
8
+
9
+ // 查询 API
10
+ getNode(nodeId: string): any;
11
+ getAllNodes(): any[];
12
+ findNodes(filter: object): any[];
13
+ getSelectedId(): string | null;
14
+ getNeighbors(nodeId: string): { upstream: any[], downstream: any[] };
15
+ getStreamNodes(nodeId: string, direction: 'upstream' | 'downstream'): any[];
16
+ getTrunkNodes(): any[];
17
+ getSubtreeNodes(nodeId: string): any[];
18
+ getPath(fromId: string, toId: string): any[] | null;
19
+ getEdge(srcId: string, tgtId: string): any;
20
+ getEdges(): any[];
21
+ getStreamEdges(nodeId: string, direction: 'upstream' | 'downstream'): any[];
22
+ getMainEdge(): any[];
23
+ getNodePosition(nodeId: string): { x: number, y: number } | null;
24
+ getEdgePath(srcId: string, tgtId: string): number[][] | null;
25
+ getGraphBounds(): { minX: number, minY: number, maxX: number, maxY: number };
26
+ getStats(): any;
27
+
28
+ // 视图控制
29
+ fitView(padding?: number): void;
30
+ zoomTo(ratio: number, center?: { x: number, y: number }): void;
31
+ zoomIn(step?: number): void;
32
+ zoomOut(step?: number): void;
33
+ locateNode(nodeId: string, zoom?: number): void;
34
+ getViewport(): { zoom: number, center: { x: number, y: number } };
35
+
36
+ // 节点样式
37
+ setNodeStyle(nodeId: string, style: object): void;
38
+ batchSetNodeStyle(nodeIds: string[], style: object): void;
39
+ resetNodeStyle(nodeId: string): void;
40
+ resetAllNodeStyles(): void;
41
+
42
+ // 边样式
43
+ setEdgeStyle(srcId: string, tgtId: string, style: object): void;
44
+ batchSetEdgeStyle(edgeIds: [string, string][], style: object): void;
45
+ resetEdgeStyle(srcId: string, tgtId: string): void;
46
+
47
+ // 高亮与标注
48
+ highlightNode(nodeId: string, opts?: object): void;
49
+ unhighlightNode(nodeId: string): void;
50
+ highlightPath(nodeIds: string[], opts?: object): void;
51
+ unhighlightAll(): void;
52
+ dimOthers(keepIds: string[], opacity?: number): void;
53
+
54
+ // 动画
55
+ setNodePulse(nodeId: string, opts?: object): void;
56
+ setEdgeFlow(srcId: string, tgtId: string, opts?: object): void;
57
+ removeAnimation(targetId: string): void;
58
+ removeAllAnimations(): void;
59
+
60
+ // 文字标注
61
+ setText(nodeId: string, text: string, opts?: object): void;
62
+ removeText(nodeId: string): void;
63
+ removeAllTexts(): void;
64
+ setDeviceText(cat: string, fontSize: number): void;
65
+ changeUserName(mode: 'full' | 'short' | 'hidden'): void;
66
+
67
+ // 卡片盒
68
+ showCard(nodeId: string, content: object): void;
69
+ hideCard(nodeId: string): void;
70
+ hideAllCards(): void;
71
+
72
+ // 连接线与拓扑线
73
+ addConnection(fromId: string, toId: string, opts?: object): void;
74
+ removeConnection(fromId: string, toId: string): void;
75
+ removeAllConnections(): void;
76
+ addTopoLine(nodeIds: string[], opts?: object): string;
77
+ removeTopoLine(lineId: string): void;
78
+ removeAllTopoLines(): void;
79
+
80
+ // 事件系统
81
+ on(event: string, handler: Function): void;
82
+ off(event: string, handler?: Function): void;
83
+
84
+ // 导出
85
+ exportPng(filename?: string): Promise<void>;
86
+ toDataURL(opts?: object): Promise<string>;
87
+ getGraphData(): object;
88
+
89
+ // 底层访问
90
+ getGraphInstance(): any;
91
+ getModel(): any;
92
+ getLayoutData(): any;
93
+ getNodeByIdMap(): Map<string, any>;
94
+
95
+ // 数据刷新
96
+ update(graph: any, model: any, layout: any, nodeByIdMap: Map<string, any>): void;
97
+
98
+ // 销毁
99
+ destroy(): void;
100
+ }
101
+
102
+ // 工具函数
103
+ export function parseTopo(raw: object): any;
104
+ export function computeOrthogonalLayout(model: any, options?: object): any;
105
+ export function assertOrthogonal(model: any, edgeCP: Map<string, number[][]>): string[];
106
+ export function buildGraphData(model: any, layout: any): any;
107
+ export function edgeStyle(datum: any): any;
108
+ export function calculateTopDistances(edges: any[], nodes: any[], edgeCP: Map<string, number[][]>, fishboneRibs: Set<string>, topN?: number): any[];
109
+ export function getSymbolSpec(cat: string): { w: number, h: number };
110
+
111
+ // 常量
112
+ export const SYMBOL_SPEC: Record<string, { w: number, h: number }>;
113
+ export const FONT: string;
114
+ export const VERSION: string;
115
+ export const DESCRIPTION: string;
@@ -28396,7 +28396,7 @@ const _sfc_main = {
28396
28396
  }
28397
28397
  };
28398
28398
  const DistanceTop10 = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-936b875b"]]);
28399
- const VERSION = "0.1.0";
28399
+ const VERSION = "0.1.2";
28400
28400
  const DESCRIPTION = "配电台区单线图拓扑成图引擎";
28401
28401
  exports.DESCRIPTION = DESCRIPTION;
28402
28402
  exports.DistanceTop10 = DistanceTop10;