gis-view-webgl2 0.1.8 → 0.1.9
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 +7 -5
- package/dist/gis-view.css +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +214 -14
- package/dist/index.js +1 -1
- package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +65 -547
- package/docs/GisView.md +1 -1
- package/docs/GisView/345/256/236/347/216/260/345/216/237/347/220/206.md +1 -1
- 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 +114 -526
- package/docs/demos/BasicDemo.vue +142 -25
- package/docs/demos/BatchAddDemo.vue +127 -1
- package/docs/demos/ChannelDemo.vue +61 -0
- package/docs/demos/CopyMergeDemo.vue +125 -36
- package/docs/demos/EventDemo.vue +12 -4
- package/docs/demos/api-index.js +26 -16
- package/docs/format/02-/350/212/202/347/202/271/344/270/216/347/272/277/350/267/257.md +1 -0
- package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +3 -0
- package/docs/index.html +136 -13
- package/docs/internals/15-04-/351/200/220/350/275/256/350/256/260/345/275/225-/347/203/255/345/212/233/345/233/276/344/270/216/346/200/247/350/203/275.md +10 -0
- package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +82 -14
- package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +2 -1
- package/docs/manual/05-/347/224/273/350/264/250/344/270/216/346/211/271/351/207/217/346/233/264/346/226/260.md +65 -0
- 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 +48 -11
- package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +8 -4
- package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +57 -0
- package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +26 -3
- package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +34 -1
- package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +5 -2
- package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +13 -10
- package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +14 -8
- package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +43 -3
- package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +3 -1
- 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 +5 -2
- package/docs/steps/README.md +200 -121
- package/docs/steps/index.html +216 -0
- package/docs/steps/selftest-demos.html +209 -31
- package/docs/steps/selftest-docs.html +17 -6
- package/docs/steps/selftest-features.html +311 -3
- package/docs/steps/selftest-labels.html +81 -0
- package/docs/steps/selftest-perf.html +31 -3
- package/docs/steps/selftest-select.html +51 -1
- package/docs//344/273/2160/345/210/260/345/256/214/346/225/264/347/232/204/345/256/236/347/216/260.md +1 -0
- package/package.json +1 -1
- package/docs/ai/01-/350/265/267/346/272/220/344/270/216/350/277/231/344/273/275/346/261/207/346/200/273/347/232/204/347/224/261/346/235/245.md +0 -29
- package/docs/ai/02-/346/214/211/345/206/205/345/256/271/345/210/206/347/261/273-/350/277/231/344/270/200/350/267/257/351/203/275/345/234/250/346/224/271/344/273/200/344/271/210.md +0 -66
- package/docs/ai/03-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/270/200/357/274/211/357/274/232/351/200/211/345/236/213/351/200/232/351/201/223/344/270/216/346/240/267/345/274/217.md +0 -178
- package/docs/ai/04-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/272/214/357/274/211/357/274/232/344/272/244/344/272/222/346/200/247/350/203/275/344/270/216/346/225/260/346/215/256.md +0 -140
- package/docs/ai/05-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/270/211/357/274/211/357/274/232/345/256/277/344/270/273/346/227/201/346/224/257/344/270/216/346/224/266/345/217/243.md +0 -43
|
@@ -1056,8 +1056,8 @@
|
|
|
1056
1056
|
])
|
|
1057
1057
|
const cmView2 = createGisView(cmHost2, {
|
|
1058
1058
|
points: [
|
|
1059
|
-
{ id: 'a', x: 0, y: 0, name: '甲', vn_kv: 500, size: 30, symbol: 'circle' },
|
|
1060
|
-
{ id: 'b', x: 110, y: 0, name: '乙', size: 12, symbol: 'square' },
|
|
1059
|
+
{ id: 'a', x: 0, y: 0, name: '甲', vn_kv: 500, size: 30, symbol: 'circle', source_only: '甲独有' },
|
|
1060
|
+
{ id: 'b', x: 110, y: 0, name: '乙', vn_kv: 220, size: 12, symbol: 'square', target_only: '乙独有' },
|
|
1061
1061
|
{ id: 'c', x: 220, y: 0, name: '丙' }
|
|
1062
1062
|
],
|
|
1063
1063
|
lines: cmChannelBuild.lines,
|
|
@@ -1118,8 +1118,9 @@
|
|
|
1118
1118
|
check('确认之后发一条 confirm,载荷里是合并结果(尺寸取双方最大、sizeScale 归 1)', Boolean(cmConfirmEvent) && cmConfirmEvent.item?.size === 30 && cmConfirmEvent.item?.sizeScale === 1, JSON.stringify({ size: cmConfirmEvent?.item?.size, sizeScale: cmConfirmEvent?.item?.sizeScale }))
|
|
1119
1119
|
check('合并结果的图标优先目标节点', cmConfirmEvent?.item?.symbol === 'square', JSON.stringify(cmConfirmEvent?.item?.symbol))
|
|
1120
1120
|
check('确认合并:来源节点并进目标节点', cmMerged.points.length === 2 && cmMerged.points[0].id === 'b', JSON.stringify(cmMerged.points.map(point => point.id)))
|
|
1121
|
-
check('
|
|
1121
|
+
check('确认合并:其它字段取保留节点、maxKeys 缺省不取电压最高', cmMerged.points[0].name === '乙' && cmMerged.points[0].vn_kv === 220 && cmMerged.points[0].target_only === '乙独有' && cmMerged.points[0].source_only === undefined, JSON.stringify(cmMerged.points[0]))
|
|
1122
1122
|
check('确认合并:图标大小取双方最大、图标类型取目标', cmMerged.points[0].size === 30 && cmMerged.points[0].symbol === 'square', `${cmMerged.points[0].size}/${cmMerged.points[0].symbol}`)
|
|
1123
|
+
check('确认合并:保留节点带 mergeInfo,记录来源、保留前快照与受影响线 / 通道', cmMerged.points[0].mergeInfo?.version === 1 && JSON.stringify(cmMerged.points[0].mergeInfo.sourceIds) === '["a"]' && cmMerged.points[0].mergeInfo.targetPoint?.id === 'b' && cmMerged.points[0].mergeInfo.targetPoint?.vn_kv === 220 && cmMerged.points[0].mergeInfo.points?.length === 1 && cmMerged.points[0].mergeInfo.lines?.length === 2 && cmMerged.points[0].mergeInfo.channels?.length === 3, JSON.stringify({ sources: cmMerged.points[0].mergeInfo?.sourceIds, targetVoltage: cmMerged.points[0].mergeInfo?.targetPoint?.vn_kv, lines: cmMerged.points[0].mergeInfo?.lines?.length, channels: cmMerged.points[0].mergeInfo?.channels?.length }))
|
|
1123
1124
|
check('确认合并:绑在来源上的线路改绑到目标', cmMerged.lines.length === 2 && cmMerged.lines[0].id === 'l1' && cmMerged.lines[0].from === 'b' && cmMerged.lines[0].to === 'c', JSON.stringify(cmMerged.lines))
|
|
1124
1125
|
check('确认合并:自环线路被删掉、结果里带读数', cmMerged.lines.every(line => line.id !== 'l2') && cmMergeDone.phase === 'merge' && cmMergeDone.removedLines === 1, JSON.stringify(cmMergeDone))
|
|
1125
1126
|
check('两端的通道撞成同一条:并成一条、默认各自保留分组', cmMergeDone.mergedChannels === 1 && cmMergeDone.channelsChanged === 2 && cmView2.getChannelGroups().length === 2, JSON.stringify({ merged: cmMergeDone.mergedChannels, changed: cmMergeDone.channelsChanged, groups: cmView2.getChannelGroups().length }))
|
|
@@ -1145,6 +1146,36 @@
|
|
|
1145
1146
|
JSON.stringify({ changes: cmMergeChangeIds, surviving: [...cmMergeSurvivingChannelIds], affected: cmMergeAffectedChannelIds }))
|
|
1146
1147
|
const cmApi = cmView2.mergePoints('c', 'b')
|
|
1147
1148
|
check('mergePoints 接口也能合并(不弹确认框)', cmApi.ok === true && cmView2.getData().points.length === 1 && cmView2.getData().lines.length === 0, JSON.stringify(cmApi).slice(0, 120))
|
|
1149
|
+
const cmCumulative = cmView2.getData().points[0]
|
|
1150
|
+
check('连续合并到同一保留节点:mergeInfo 累加两个来源,maxKeys 缺省仍保留目标电压', JSON.stringify(cmCumulative.mergeInfo?.sourceIds) === '["a","c"]' && cmCumulative.mergeInfo?.points?.length === 2 && cmCumulative.vn_kv === 220, JSON.stringify({ sources: cmCumulative.mergeInfo?.sourceIds, voltage: cmCumulative.vn_kv }))
|
|
1151
|
+
cmView2.updateOptions({ history: true })
|
|
1152
|
+
cmView2.selectFeatures('b')
|
|
1153
|
+
const cmAutoSplit = cmView2.splitPoint('b')
|
|
1154
|
+
await settle(80)
|
|
1155
|
+
const cmRestoredData = cmView2.getData()
|
|
1156
|
+
const cmRestoredChannels = cmView2.getChannels()
|
|
1157
|
+
const cmRestoredA = cmRestoredData.points.find(point => point.id === 'a')
|
|
1158
|
+
const cmRestoredB = cmRestoredData.points.find(point => point.id === 'b')
|
|
1159
|
+
const cmRestoredC = cmRestoredData.points.find(point => point.id === 'c')
|
|
1160
|
+
check('splitPoint 省略 parts:按 mergeInfo 自动恢复全部来源节点与合并前字段', cmAutoSplit.ok === true && cmAutoSplit.autoRestored === true && cmRestoredData.points.length === 3 && Boolean(cmRestoredA) && Boolean(cmRestoredB) && Boolean(cmRestoredC) && cmRestoredA?.vn_kv === 500 && cmRestoredB?.vn_kv === 220 && cmRestoredB?.mergeInfo === undefined, JSON.stringify({ auto: cmAutoSplit.autoRestored, points: cmRestoredData.points.map(point => point.id), voltages: [cmRestoredA?.vn_kv, cmRestoredB?.vn_kv, cmRestoredC?.vn_kv] }))
|
|
1161
|
+
check('splitPoint 自动解除默认不选中拆分结果(只摘掉来源节点的选中态)', cmView2.getSelectionData().pointCount === 0 && cmView2.getSelections().length === 0, JSON.stringify(cmView2.getSelectionData()))
|
|
1162
|
+
check('splitPoint 自动解除:线路与通道端点、成员及回线全部恢复', cmRestoredData.lines.length === 3 && cmRestoredData.lines.find(line => line.id === 'l2')?.from === 'a' && cmRestoredData.lines.find(line => line.id === 'l2')?.to === 'b' && cmRestoredData.lines.find(line => line.id === 'l1')?.from === 'a' && cmRestoredData.lines.find(line => line.id === 'l3')?.to === 'b' && cmRestoredChannels.length === 3 && cmRestoredChannels.find(channel => channel.id === 'ch-ab')?.from === 'a' && cmRestoredChannels.find(channel => channel.id === 'ch-ac')?.from === 'a' && cmRestoredChannels.find(channel => channel.id === 'ch-bc')?.to === 'b', JSON.stringify({ lines: cmRestoredData.lines.map(line => `${line.id}:${line.from}->${line.to}`), channels: cmRestoredChannels.map(channel => `${channel.id}:${channel.from}->${channel.to}`) }))
|
|
1163
|
+
check('splitPoint 自动解除:返回 mergeSourceIds / 可提交 changes', JSON.stringify(cmAutoSplit.mergeSourceIds) === '["a","c"]' && cmAutoSplit.changes?.points?.length === 3 && cmAutoSplit.changes?.lines?.length === 3 && cmAutoSplit.changes?.channels?.length === 3, JSON.stringify({ sources: cmAutoSplit.mergeSourceIds, changes: { points: cmAutoSplit.changes?.points?.length, lines: cmAutoSplit.changes?.lines?.length, channels: cmAutoSplit.changes?.channels?.length } }))
|
|
1164
|
+
cmView2.undo()
|
|
1165
|
+
await settle(80)
|
|
1166
|
+
const cmUnmergeUndo = cmView2.getData()
|
|
1167
|
+
const cmUnmergeUndoChannels = cmView2.getChannels()
|
|
1168
|
+
cmView2.redo()
|
|
1169
|
+
await settle(80)
|
|
1170
|
+
const cmUnmergeRedo = cmView2.getData()
|
|
1171
|
+
const cmUnmergeRedoChannels = cmView2.getChannels()
|
|
1172
|
+
check('splitPoint 自动解除是一步历史:撤销回到合并态、重做恢复自动解除结果', cmUnmergeUndo.points.length === 1 && cmUnmergeUndo.points[0]?.id === 'b' && cmUnmergeUndo.lines.length === 0 && cmUnmergeUndoChannels.length === 0 && cmUnmergeRedo.points.length === 3 && cmUnmergeRedo.lines.length === 3 && cmUnmergeRedoChannels.length === 3, JSON.stringify({ undo: [cmUnmergeUndo.points.length, cmUnmergeUndo.lines.length, cmUnmergeUndoChannels.length], redo: [cmUnmergeRedo.points.length, cmUnmergeRedo.lines.length, cmUnmergeRedoChannels.length] }))
|
|
1173
|
+
cmView2.undo()
|
|
1174
|
+
await settle(80)
|
|
1175
|
+
const cmAutoSplitSelected = cmView2.splitPoint('b', null, { select: true })
|
|
1176
|
+
await settle(80)
|
|
1177
|
+
const cmAutoSelectedData = cmView2.getSelectionData()
|
|
1178
|
+
check('splitPoint 自动解除 select:true 时选中全部恢复节点', cmAutoSplitSelected.ok === true && cmAutoSplitSelected.autoRestored === true && cmAutoSelectedData.pointCount === 3 && cmAutoSplitSelected.pointIds.every(id => cmAutoSelectedData.points.some(point => String(point.id) === String(id))), JSON.stringify({ pointCount: cmAutoSelectedData.pointCount, ids: cmAutoSplitSelected.pointIds }))
|
|
1148
1179
|
|
|
1149
1180
|
/* 移除节点:绑在它上面的线路与声明了它当端点的通道一起收拾 */
|
|
1150
1181
|
const cmHost7 = document.createElement('div')
|
|
@@ -1168,6 +1199,107 @@
|
|
|
1168
1199
|
cmView7.destroy()
|
|
1169
1200
|
cmHost7.remove()
|
|
1170
1201
|
|
|
1202
|
+
/* merge.maxKeys:缺省不取最高;数组 / 逗号字符串都按列出的数值字段取双方最高 */
|
|
1203
|
+
const mkHost = document.createElement('div')
|
|
1204
|
+
mkHost.style.cssText = 'width:460px;height:300px'
|
|
1205
|
+
document.body.appendChild(mkHost)
|
|
1206
|
+
const mkView = createGisView(mkHost, {
|
|
1207
|
+
points: [
|
|
1208
|
+
{ id: 'mk-a', x: 0, y: 0, name: '来源甲', vn_kv: 500, value: 9, loading: 0.2 },
|
|
1209
|
+
{ id: 'mk-b', x: 80, y: 0, name: '目标乙', vn_kv: 220, value: 3, loading: 0.8 },
|
|
1210
|
+
{ id: 'mk-c', x: 0, y: 80, name: '来源丙', vn_kv: 525, value: 12, loading: 0.1 },
|
|
1211
|
+
{ id: 'mk-d', x: 80, y: 80, name: '目标丁', vn_kv: 1050, value: 4, loading: 0.9 }
|
|
1212
|
+
],
|
|
1213
|
+
merge: { distance: 8, maxKeys: ['vn_kv', 'value'] },
|
|
1214
|
+
fitPadding: 60,
|
|
1215
|
+
showGrid: false
|
|
1216
|
+
})
|
|
1217
|
+
await settle(80)
|
|
1218
|
+
const mkFirst = mkView.mergePoints('mk-a', 'mk-b')
|
|
1219
|
+
const mkFirstItem = mkView.getData().points.find(point => point.id === 'mk-b')
|
|
1220
|
+
check('merge.maxKeys 数组:列出的 vn_kv / value 取双方最高,其余字段取目标', mkFirst.ok === true && mkFirstItem?.vn_kv === 500 && mkFirstItem?.value === 9 && mkFirstItem?.loading === 0.8 && mkFirstItem?.name === '目标乙', JSON.stringify(mkFirstItem))
|
|
1221
|
+
mkView.updateOptions({ merge: { enabled: true, maxKeys: 'vn_kv, value' } })
|
|
1222
|
+
const mkSecond = mkView.mergePoints('mk-c', 'mk-d')
|
|
1223
|
+
const mkSecondItem = mkView.getData().points.find(point => point.id === 'mk-d')
|
|
1224
|
+
check('merge.maxKeys 逗号字符串:同样只对列出的数值字段取最高', mkSecond.ok === true && mkSecondItem?.vn_kv === 1050 && mkSecondItem?.value === 12 && mkSecondItem?.loading === 0.9 && mkSecondItem?.name === '目标丁', JSON.stringify(mkSecondItem))
|
|
1225
|
+
mkView.destroy()
|
|
1226
|
+
mkHost.remove()
|
|
1227
|
+
|
|
1228
|
+
/* splitPoint.select:缺省不选中结果,也不清掉无关的线路选中;显式 true 才选中全部拆分结果 */
|
|
1229
|
+
const ssHost = document.createElement('div')
|
|
1230
|
+
ssHost.style.cssText = 'width:420px;height:260px'
|
|
1231
|
+
document.body.appendChild(ssHost)
|
|
1232
|
+
const ssView = createGisView(ssHost, {
|
|
1233
|
+
points: [{ id: 'ss-a', x: 0, y: 0, name: '拆分源' }, { id: 'ss-b', x: 80, y: 0 }],
|
|
1234
|
+
lines: [{ id: 'ss-line', from: 'ss-a', to: 'ss-b' }],
|
|
1235
|
+
fitPadding: 50,
|
|
1236
|
+
showGrid: false
|
|
1237
|
+
})
|
|
1238
|
+
await settle(80)
|
|
1239
|
+
ssView.selectFeatures(['ss-a', 'ss-line'])
|
|
1240
|
+
const ssDefault = ssView.splitPoint('ss-a', [
|
|
1241
|
+
{ x: -1, y: 0, name: 'A' },
|
|
1242
|
+
{ x: 1, y: 0, name: 'B' }
|
|
1243
|
+
])
|
|
1244
|
+
const ssDefaultSelection = ssView.getSelectionData()
|
|
1245
|
+
check('splitPoint 缺省不选中拆分结果,其它无关选中保持', ssDefault.ok === true && ssDefaultSelection.pointCount === 0 && ssDefaultSelection.lineCount === 1, JSON.stringify(ssDefaultSelection))
|
|
1246
|
+
ssView.undo()
|
|
1247
|
+
await settle(60)
|
|
1248
|
+
ssView.selectFeatures('ss-a')
|
|
1249
|
+
const ssSelected = ssView.splitPoint('ss-a', [
|
|
1250
|
+
{ x: -1, y: 0, name: 'A2' },
|
|
1251
|
+
{ x: 1, y: 0, name: 'B2' }
|
|
1252
|
+
], { select: true })
|
|
1253
|
+
const ssSelectedData = ssView.getSelectionData()
|
|
1254
|
+
const ssSelectedIndexes = [...ssSelectedData.pointIndexes].sort((left, right) => left - right)
|
|
1255
|
+
const ssResultIndexes = [...ssSelected.indexes].sort((left, right) => left - right)
|
|
1256
|
+
check('splitPoint({ select: true }) 选中全部拆分结果', ssSelected.ok === true && ssSelectedData.pointCount === 2 && JSON.stringify(ssSelectedIndexes) === JSON.stringify(ssResultIndexes) && ssSelectedData.lineCount === 0, JSON.stringify({ selected: ssSelectedIndexes, result: ssResultIndexes }))
|
|
1257
|
+
ssView.destroy()
|
|
1258
|
+
ssHost.remove()
|
|
1259
|
+
|
|
1260
|
+
/* 通道简略:只折叠严格超过阈值的分组;没有 groups 的通道按一整组算 */
|
|
1261
|
+
const csHost = document.createElement('div')
|
|
1262
|
+
csHost.style.cssText = 'width:520px;height:300px'
|
|
1263
|
+
document.body.appendChild(csHost)
|
|
1264
|
+
const csLineIds = Array.from({ length: 8 }, (_item, index) => `cs-l${index + 1}`)
|
|
1265
|
+
const csView = createGisView(csHost, {
|
|
1266
|
+
points: [{ id: 'cs-a', x: 0, y: 0 }, { id: 'cs-b', x: 180, y: 0 }],
|
|
1267
|
+
lines: csLineIds.map(id => ({ id, from: 'cs-a', to: 'cs-b' })),
|
|
1268
|
+
channels: [
|
|
1269
|
+
{ id: 'cs-plain', from: 'cs-a', to: 'cs-b', lineIds: csLineIds.slice(0, 3) },
|
|
1270
|
+
{
|
|
1271
|
+
id: 'cs-grouped',
|
|
1272
|
+
from: 'cs-a',
|
|
1273
|
+
to: 'cs-b',
|
|
1274
|
+
groups: [
|
|
1275
|
+
{ id: 'cs-g1', lineIds: csLineIds.slice(3, 6) },
|
|
1276
|
+
{ id: 'cs-g2', lineIds: csLineIds.slice(6) }
|
|
1277
|
+
]
|
|
1278
|
+
}
|
|
1279
|
+
],
|
|
1280
|
+
channelSimplify: { enabled: true, threshold: 2, style: { color: '#ff3300', width: 5 } },
|
|
1281
|
+
fitPadding: 60,
|
|
1282
|
+
showGrid: false
|
|
1283
|
+
})
|
|
1284
|
+
await settle(120)
|
|
1285
|
+
const csStyles = csLineIds.map((_id, index) => csView.getLineStyle(index))
|
|
1286
|
+
const csGroups = csView.getChannelGroups()
|
|
1287
|
+
const csSnapshot = csView.getChannels()
|
|
1288
|
+
const csSnapshotLaneCounts = csSnapshot.map(group => new Set(group.paths.map(path => JSON.stringify(path))).size)
|
|
1289
|
+
const csRepresentative = csStyles[0]
|
|
1290
|
+
const csDataCount = csView.getData().lines.length
|
|
1291
|
+
check('通道简略:超过 threshold 的分组只画一条代表线,无 groups 的通道也按一组折叠', csStyles.filter(Boolean).length === 4 && csStyles[1] === null && csStyles[2] === null && csStyles[3] !== null && csStyles[4] === null && csStyles[5] === null && csStyles[6] !== null && csStyles[7] !== null, JSON.stringify(csStyles.map(style => style ? style.index : null)))
|
|
1292
|
+
check('通道简略:代表线套自定义颜色 / 线宽,原始数据与分组成员不变', csRepresentative?.width === 5 && Math.abs(csRepresentative.color[0] - 1) < 1e-6 && Math.abs(csRepresentative.color[1] - 0.2) < 1e-6 && csDataCount === 8 && csGroups.length === 3 && csGroups.map(group => group.lineIds.length).join(',') === '3,3,2', JSON.stringify({ style: csRepresentative, data: csDataCount, groups: csGroups.map(group => group.lineIds.length) }))
|
|
1293
|
+
check('通道简略:getChannels 快照为每条成员回线保留完整路径与各自车道',
|
|
1294
|
+
csSnapshot.length === 3 && csSnapshot.every((group, index) => group.paths.length === group.lineIndexes.length && group.paths.every(path => Array.isArray(path) && path.length >= 2) && csSnapshotLaneCounts[index] === group.paths.length),
|
|
1295
|
+
JSON.stringify(csSnapshot.map((group, index) => [group.lineIndexes.length, group.paths.length, csSnapshotLaneCounts[index], group.paths.map(path => path.length)])))
|
|
1296
|
+
csView.updateOptions({ channelSimplify: false })
|
|
1297
|
+
await settle(100)
|
|
1298
|
+
const csRestoredStyles = csLineIds.map((_id, index) => csView.getLineStyle(index))
|
|
1299
|
+
check('通道简略关闭后全部回线恢复,数据与分组读数仍不变', csRestoredStyles.every(Boolean) && csView.getData().lines.length === 8 && csView.getChannelGroups().length === 3, JSON.stringify(csRestoredStyles.map(style => style ? style.index : null)))
|
|
1300
|
+
csView.destroy()
|
|
1301
|
+
csHost.remove()
|
|
1302
|
+
|
|
1171
1303
|
diag('复制 / 合并', `复制事件 ${cmEvents.length} 条,合并读数 linesChanged=${cmMergeDone.linesChanged} removedLines=${cmMergeDone.removedLines} removedChannels=${cmMergeDone.removedChannels} mergedChannels=${cmMergeDone.mergedChannels}`)
|
|
1172
1304
|
cmView2.destroy()
|
|
1173
1305
|
cmHost2.remove()
|
|
@@ -2412,6 +2544,24 @@
|
|
|
2412
2544
|
goView.destroy()
|
|
2413
2545
|
goHost.remove()
|
|
2414
2546
|
|
|
2547
|
+
const rocHost = document.createElement('div')
|
|
2548
|
+
rocHost.style.cssText = 'width:420px;height:280px'
|
|
2549
|
+
document.body.appendChild(rocHost)
|
|
2550
|
+
const rocView = createGisView(rocHost, {
|
|
2551
|
+
points: [{ id: 'ra', x: -80, y: 0 }, { id: 'rb', x: 80, y: 0 }],
|
|
2552
|
+
lines: [{ id: 'r1', from: 'ra', to: 'rb' }, { id: 'r2', from: 'ra', to: 'rb' }, { id: 'r3', from: 'ra', to: 'rb' }],
|
|
2553
|
+
channels: [{ id: 'roc', groups: [{ id: 'roc-g1', lineIds: ['r1', 'r2'] }, { id: 'roc-g2', lineIds: ['r3'] }] }],
|
|
2554
|
+
showGrid: false,
|
|
2555
|
+
fitPadding: 60
|
|
2556
|
+
})
|
|
2557
|
+
await settle(100)
|
|
2558
|
+
const rocRemoved = rocView.removeChannel('roc')
|
|
2559
|
+
check('removeChannel 删除只写 groups 的通道时连同组内成员回线一起删净',
|
|
2560
|
+
rocRemoved.ok === true && rocRemoved.lineIds.length === 3 && rocView.getData().lines.length === 0 && rocView.getChannelGroups('roc').length === 0,
|
|
2561
|
+
JSON.stringify({ lineIds: rocRemoved.lineIds, lines: rocView.getData().lines.length, groups: rocView.getChannelGroups('roc').length }))
|
|
2562
|
+
rocView.destroy()
|
|
2563
|
+
rocHost.remove()
|
|
2564
|
+
|
|
2415
2565
|
/* 这一轮新增:updateChannel 第三段(通道声明 + 逐条成员回线一次下发) */
|
|
2416
2566
|
const ucHost = document.createElement('div')
|
|
2417
2567
|
ucHost.style.cssText = 'width:420px;height:280px'
|
|
@@ -3886,6 +4036,46 @@ check('beginAddChannel({ lineData }):草稿按第二段对齐条数(circuits
|
|
|
3886
4036
|
check('exportData:format 写错直接抛异常(不静默兜底)', /view/.test(rdThrew) && /new/.test(rdThrew) && /test/.test(rdThrew), rdThrew)
|
|
3887
4037
|
rdView.destroy()
|
|
3888
4038
|
rdHost.remove()
|
|
4039
|
+
|
|
4040
|
+
/* 本轮:视口导出的线路 / 通道端点模式。半区只盖住左端点,和整区对照。 */
|
|
4041
|
+
const veHost = document.createElement('div')
|
|
4042
|
+
veHost.style.cssText = 'width:480px;height:300px'
|
|
4043
|
+
document.body.appendChild(veHost)
|
|
4044
|
+
const veView = createGisView(veHost, {
|
|
4045
|
+
points: [
|
|
4046
|
+
{ id: 've-a', x: -130, y: 0, size: 16 },
|
|
4047
|
+
{ id: 've-b', x: 130, y: 0, size: 16 }
|
|
4048
|
+
],
|
|
4049
|
+
lines: [
|
|
4050
|
+
{ id: 've-bound', from: 've-a', to: 've-b', width: 8 },
|
|
4051
|
+
{ id: 've-path', path: [[-80, 60], [80, 60]], width: 8 },
|
|
4052
|
+
{ id: 've-ch-line', from: 've-a', to: 've-b', width: 8 }
|
|
4053
|
+
],
|
|
4054
|
+
channels: [{ id: 've-ch', lineIds: ['ve-ch-line'] }],
|
|
4055
|
+
showGrid: false,
|
|
4056
|
+
fitOnInit: false,
|
|
4057
|
+
initialView: { center: [0, 0], scale: 1 },
|
|
4058
|
+
width: 480,
|
|
4059
|
+
height: 300
|
|
4060
|
+
})
|
|
4061
|
+
await settle(120)
|
|
4062
|
+
const veHalf = { minX: -200, minY: -100, maxX: 0, maxY: 100 }
|
|
4063
|
+
const veAll = { minX: -200, minY: -100, maxX: 200, maxY: 100 }
|
|
4064
|
+
const veLineIds = result => (result.lines ?? []).map(row => row.line_id).sort()
|
|
4065
|
+
const veLegacy = veView.exportData({ scope: 'viewport', rect: veHalf, selectionKinds: ['line'] })
|
|
4066
|
+
check('exportData 视口 line 旧模式:几何沾到视口的独立线路都导出', veLegacy.counts.points === 0 && veLegacy.counts.lines === 2 && veLineIds(veLegacy).join(',') === 've-bound,ve-path' && veLegacy.counts.channels === 0, JSON.stringify({ counts: veLegacy.counts, lines: veLineIds(veLegacy) }))
|
|
4067
|
+
const veLineHalf = veView.exportData({ scope: 'viewport', rect: veHalf, selectionKinds: ['lineEndpoints'] })
|
|
4068
|
+
check('exportData 视口 lineEndpoints:只盖住一端时不导出独立线路', veLineHalf.counts.lines === 0 && veLineHalf.lines.length === 0 && veLineHalf.counts.channels === 0, JSON.stringify(veLineHalf.counts))
|
|
4069
|
+
const veLineAll = veView.exportData({ scope: 'viewport', rect: veAll, selectionKinds: ['line-endpoints'] })
|
|
4070
|
+
check('exportData 视口 lineEndpoints:两端都在视口内才导出,纯 path 线路仍跳过', veLineAll.counts.points === 0 && veLineAll.counts.lines === 1 && veLineIds(veLineAll).join(',') === 've-bound', JSON.stringify({ counts: veLineAll.counts, lines: veLineIds(veLineAll) }))
|
|
4071
|
+
const veChannelLegacy = veView.exportData({ scope: 'viewport', rect: veHalf, selectionKinds: ['channel'] })
|
|
4072
|
+
check('exportData 视口 channel 旧模式:成员回线沾到视口就导出整条通道', veChannelLegacy.counts.channels === 1 && veChannelLegacy.channels[0].channel_name === 've-ch' && veChannelLegacy.counts.lines === 1, JSON.stringify({ counts: veChannelLegacy.counts, channels: veChannelLegacy.channels.map(row => row.channel_name) }))
|
|
4073
|
+
const veChannelHalf = veView.exportData({ scope: 'viewport', rect: veHalf, selectionKinds: ['channelEndpoints'] })
|
|
4074
|
+
check('exportData 视口 channelEndpoints:只盖住一端时不导出通道', veChannelHalf.counts.channels === 0 && veChannelHalf.counts.lines === 0, JSON.stringify(veChannelHalf.counts))
|
|
4075
|
+
const veChannelAll = veView.exportData({ scope: 'viewport', rect: veAll, selectionKinds: ['channel-endpoints'] })
|
|
4076
|
+
check('exportData 视口 channelEndpoints:两端都在视口内才导出整条通道与成员回线', veChannelAll.counts.points === 0 && veChannelAll.counts.channels === 1 && veChannelAll.counts.lines === 1 && veChannelAll.channels[0].channel_name === 've-ch' && veChannelAll.channels[0].line_data[0].line_id === 've-ch-line', JSON.stringify({ counts: veChannelAll.counts, lines: veLineIds({ lines: veChannelAll.channels[0].line_data }) }))
|
|
4077
|
+
veView.destroy()
|
|
4078
|
+
veHost.remove()
|
|
3889
4079
|
}
|
|
3890
4080
|
{
|
|
3891
4081
|
// 接口选中(selectFeatures)、读回选中(getSelectionData)与聚焦(focusOn:单个 / 一批 / 只居中)
|
|
@@ -4925,6 +5115,53 @@ check('beginAddChannel({ lineData }):草稿按第二段对齐条数(circuits
|
|
|
4925
5115
|
JSON.stringify({ removedChannels: cbManyRemoved.removedChannels?.map(item => item.id) }))
|
|
4926
5116
|
nwView.destroy()
|
|
4927
5117
|
|
|
5118
|
+
/* 这一轮新增:getDirectConnections 只取传入节点集合内部的直接通道 / 线路 */
|
|
5119
|
+
const dcHost = document.createElement('div')
|
|
5120
|
+
dcHost.style.cssText = 'width:420px;height:280px'
|
|
5121
|
+
document.body.appendChild(dcHost)
|
|
5122
|
+
const dcView = createGisView(dcHost, {
|
|
5123
|
+
points: [
|
|
5124
|
+
{ id: 'da', x: -120, y: -60 },
|
|
5125
|
+
{ id: 'db', x: 120, y: -60 },
|
|
5126
|
+
{ id: 'dc', x: 120, y: 60 },
|
|
5127
|
+
{ id: 'dd', x: -120, y: 60 }
|
|
5128
|
+
],
|
|
5129
|
+
lines: [
|
|
5130
|
+
{ id: 'dc-ab-1', from: 'da', to: 'db' },
|
|
5131
|
+
{ id: 'dc-ab-2', from: 'db', to: 'da' },
|
|
5132
|
+
{ id: 'dc-bc', from: 'db', to: 'dc' },
|
|
5133
|
+
{ id: 'dc-cd', from: 'dc', to: 'dd' },
|
|
5134
|
+
{ id: 'dc-loop', from: 'da', to: 'da' },
|
|
5135
|
+
{ id: 'dc-free', path: [[0, 0], [40, 40]] }
|
|
5136
|
+
],
|
|
5137
|
+
channels: [
|
|
5138
|
+
{ id: 'dc-ch-ab', groups: [{ id: 'dc-ch-ab-g1', lineIds: ['dc-ab-1'] }] },
|
|
5139
|
+
{ id: 'dc-ch-bc', from: 'db', to: 'dc', lineIds: ['dc-bc'] },
|
|
5140
|
+
{ id: 'dc-ch-ad', from: 'da', to: 'dd', lineIds: ['dc-cd'] },
|
|
5141
|
+
{ id: 'dc-ch-miss', from: 'da', to: 'missing', lineIds: [] },
|
|
5142
|
+
{ id: 'dc-ch-free', groups: [{ id: 'dc-ch-free-g1', lineIds: ['dc-free'] }] }
|
|
5143
|
+
],
|
|
5144
|
+
showGrid: false,
|
|
5145
|
+
fitPadding: 50
|
|
5146
|
+
})
|
|
5147
|
+
await settle(80)
|
|
5148
|
+
const dcAB = dcView.getDirectConnections(['da', 'db'])
|
|
5149
|
+
const dcAll = dcView.getDirectConnections(['da', 'db', 'dc'])
|
|
5150
|
+
check('getDirectConnections:只返回集合内部的直接通道 / 线路,通道与成员回线可同时命中',
|
|
5151
|
+
dcAB.map(item => item.type).join(',') === 'channel,line,line' && dcAB.map(item => item.channelId ?? item.lineId).join(',') === 'dc-ch-ab,dc-ab-1,dc-ab-2',
|
|
5152
|
+
JSON.stringify(dcAB.map(item => ({ type: item.type, id: item.channelId ?? item.lineId, from: item.from.id, to: item.to.id }))))
|
|
5153
|
+
check('getDirectConnections:通道端点可继承成员回线,线路保留声明方向与节点下标',
|
|
5154
|
+
dcAB[0].from.id === 'da' && dcAB[0].to.id === 'db' && JSON.stringify(dcAB[0].lineIds) === JSON.stringify(['dc-ab-1']) && dcAB[2].from.id === 'db' && dcAB[2].to.id === 'da' && dcAB[2].from.index === 1 && dcAB[2].to.index === 0,
|
|
5155
|
+
JSON.stringify({ channel: dcAB[0], reverse: dcAB[2] }))
|
|
5156
|
+
check('getDirectConnections:三个节点时连上集合内的第二条边,不把集合外的边一起带回来',
|
|
5157
|
+
dcAll.length === 5 && dcAll.some(item => item.type === 'channel' && item.channelId === 'dc-ch-bc') && dcAll.filter(item => item.type === 'line').map(item => item.lineId).join(',') === 'dc-ab-1,dc-ab-2,dc-bc',
|
|
5158
|
+
JSON.stringify(dcAll.map(item => ({ type: item.type, id: item.channelId ?? item.lineId }))))
|
|
5159
|
+
check('getDirectConnections:节点不足两个 / 有节点不存在 / 端点认不出 / 自环与未绑定线路都返回空表',
|
|
5160
|
+
dcView.getDirectConnections([]).length === 0 && dcView.getDirectConnections('da').length === 0 && dcView.getDirectConnections(['da', 'missing']).length === 0 && dcView.getDirectConnections(['da', 'db', 'da']).length === 3,
|
|
5161
|
+
JSON.stringify({ empty: dcView.getDirectConnections([]), missing: dcView.getDirectConnections(['da', 'missing']), duplicate: dcView.getDirectConnections(['da', 'db', 'da']).length }))
|
|
5162
|
+
dcView.destroy()
|
|
5163
|
+
dcHost.remove()
|
|
5164
|
+
|
|
4928
5165
|
/* 这一轮新增:addLine / addChannel 第二段的裸 id 归一(见 15.98)+ 视口量不出来时挂起的落位请求封存的是「请求那一刻」的目标(见 16.68) */
|
|
4929
5166
|
const bvHost = document.createElement('div')
|
|
4930
5167
|
bvHost.style.cssText = 'width:520px;height:340px'
|
|
@@ -5823,6 +6060,77 @@ check('beginAddChannel({ lineData }):草稿按第二段对齐条数(circuits
|
|
|
5823
6060
|
check('splitPoint({ autoLines: alternate }):通道端点跟随整组成员,lineAssignments 报出每一份的线路条数', autoSplit.lineAssignments.length === 2 && autoSplit.lineAssignments.reduce((sum, item) => sum + item.lineCount, 0) === 5 && String(autoSplitChannel?.from) === String(autoSplit.pointIds[autoChannelSlotA]) && String(autoSplitChannel?.to) === 'as-a' && autoSplitNewPoint !== 'as-hub', JSON.stringify({ channelFrom: autoSplitChannel?.from, newPoint: autoSplitNewPoint, counts: autoSplit.lineAssignments.map(item => item.lineCount) }))
|
|
5824
6061
|
autoSplitView.destroy()
|
|
5825
6062
|
autoSplitHost.remove()
|
|
6063
|
+
|
|
6064
|
+
/* 首屏 center / scale 简写、比例 API 与空闲检测 */
|
|
6065
|
+
const cameraApiHost = document.createElement('div')
|
|
6066
|
+
cameraApiHost.style.cssText = 'width:480px;height:280px'
|
|
6067
|
+
document.body.append(cameraApiHost)
|
|
6068
|
+
const cameraApiView = createGisView(cameraApiHost, {
|
|
6069
|
+
points: [{ id: 'camera-api-a', x: 0, y: 0 }],
|
|
6070
|
+
center: [12, -8],
|
|
6071
|
+
scale: 3,
|
|
6072
|
+
showGrid: false
|
|
6073
|
+
})
|
|
6074
|
+
await settle()
|
|
6075
|
+
const shorthandCamera = cameraApiView.getCamera()
|
|
6076
|
+
check('创建时顶层 center / scale 直接决定首屏相机', Math.abs(shorthandCamera.center[0] - 12) < 0.01 && Math.abs(shorthandCamera.center[1] + 8) < 0.01 && Math.abs(shorthandCamera.scale - 3) < 0.001, JSON.stringify(shorthandCamera))
|
|
6077
|
+
check('getScale / setScale:读写同一份相机比例', Math.abs(cameraApiView.getScale() - 3) < 0.001 && cameraApiView.setScale(5, [100, 80]) === cameraApiView && Math.abs(cameraApiView.getScale() - 5) < 0.001 && Math.abs(cameraApiView.getCamera().scale - 5) < 0.001, JSON.stringify({ before: 3, after: cameraApiView.getScale() }))
|
|
6078
|
+
cameraApiView.updateOptions({ scale: 4 })
|
|
6079
|
+
cameraApiView.applyInitialView()
|
|
6080
|
+
const mergedScaleCamera = cameraApiView.getCamera()
|
|
6081
|
+
cameraApiView.updateOptions({ center: [20, -10] })
|
|
6082
|
+
cameraApiView.applyInitialView()
|
|
6083
|
+
const mergedInitialView = cameraApiView.getCamera()
|
|
6084
|
+
check('updateOptions 的 center / scale 简写合进同一份 initialView,不会互相冲掉', Math.abs(mergedScaleCamera.center[0] - 12) < 0.01 && Math.abs(mergedScaleCamera.center[1] + 8) < 0.01 && Math.abs(mergedScaleCamera.scale - 4) < 0.001 && Math.abs(mergedInitialView.center[0] - 20) < 0.01 && Math.abs(mergedInitialView.center[1] + 10) < 0.01 && Math.abs(mergedInitialView.scale - 4) < 0.001, JSON.stringify({ afterScale: mergedScaleCamera, afterCenter: mergedInitialView }))
|
|
6085
|
+
cameraApiView.destroy()
|
|
6086
|
+
cameraApiHost.remove()
|
|
6087
|
+
|
|
6088
|
+
const precedenceHost = document.createElement('div')
|
|
6089
|
+
precedenceHost.style.cssText = 'width:480px;height:280px'
|
|
6090
|
+
document.body.append(precedenceHost)
|
|
6091
|
+
const precedenceView = createGisView(precedenceHost, {
|
|
6092
|
+
points: [{ id: 'precedence-a', x: 0, y: 0 }],
|
|
6093
|
+
center: [1, 2],
|
|
6094
|
+
scale: 9,
|
|
6095
|
+
initialView: { center: [30, 40], scale: 2 },
|
|
6096
|
+
showGrid: false
|
|
6097
|
+
})
|
|
6098
|
+
await settle()
|
|
6099
|
+
const precedenceCamera = precedenceView.getCamera()
|
|
6100
|
+
check('显式 initialView 整份优先于顶层 center / scale 简写', Math.abs(precedenceCamera.center[0] - 30) < 0.01 && Math.abs(precedenceCamera.center[1] - 40) < 0.01 && Math.abs(precedenceCamera.scale - 2) < 0.001, JSON.stringify(precedenceCamera))
|
|
6101
|
+
precedenceView.destroy()
|
|
6102
|
+
precedenceHost.remove()
|
|
6103
|
+
|
|
6104
|
+
const idleHost = document.createElement('div')
|
|
6105
|
+
idleHost.style.cssText = 'width:480px;height:280px'
|
|
6106
|
+
document.body.append(idleHost)
|
|
6107
|
+
const idleCalls = []
|
|
6108
|
+
const idleBusCalls = []
|
|
6109
|
+
const idleView = createGisView(idleHost, {
|
|
6110
|
+
points: [{ id: 'idle-a', x: 0, y: 0 }],
|
|
6111
|
+
idleTimeout: 45,
|
|
6112
|
+
onIdle: payload => idleCalls.push(payload),
|
|
6113
|
+
showGrid: false
|
|
6114
|
+
})
|
|
6115
|
+
idleView.on('idle', payload => idleBusCalls.push(payload))
|
|
6116
|
+
const idleStartedAt = idleView.getIdleTime()
|
|
6117
|
+
await settleMs(110)
|
|
6118
|
+
const firstIdle = idleCalls[0]
|
|
6119
|
+
const afterFirstIdle = idleView.getIdleTime()
|
|
6120
|
+
await settleMs(80)
|
|
6121
|
+
check('idleTimeout:到点只回调一次,getIdleTime 持续增长', idleStartedAt >= 0 && idleCalls.length === 1 && idleBusCalls.length === 1 && firstIdle?.timeout === 45 && firstIdle?.idleTime >= 40 && afterFirstIdle >= firstIdle?.idleTime, JSON.stringify({ start: idleStartedAt, after: afterFirstIdle, calls: idleCalls.length, bus: idleBusCalls.length, payload: firstIdle }))
|
|
6122
|
+
check('idleTimeout:同一空闲段不重复触发', idleCalls.length === 1 && idleBusCalls.length === 1, JSON.stringify({ calls: idleCalls.length, bus: idleBusCalls.length }))
|
|
6123
|
+
const idleBox = idleView.canvas.getBoundingClientRect()
|
|
6124
|
+
idleView.canvas.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true, pointerId: 31, pointerType: 'mouse', clientX: idleBox.left + 20, clientY: idleBox.top + 20 }))
|
|
6125
|
+
const afterActivity = idleView.getIdleTime()
|
|
6126
|
+
await settleMs(110)
|
|
6127
|
+
check('idleTimeout:用户操作重置 getIdleTime,并开启下一段空闲触发', afterActivity < 40 && idleCalls.length === 2 && idleBusCalls.length === 2, JSON.stringify({ afterActivity, calls: idleCalls.length, bus: idleBusCalls.length }))
|
|
6128
|
+
idleView.destroy()
|
|
6129
|
+
const idleBeforeDestroyWait = idleCalls.length
|
|
6130
|
+
await settleMs(80)
|
|
6131
|
+
check('destroy 后清除 idle 定时器,不再触发回调', idleCalls.length === idleBeforeDestroyWait, JSON.stringify({ before: idleBeforeDestroyWait, after: idleCalls.length }))
|
|
6132
|
+
idleHost.remove()
|
|
6133
|
+
|
|
5826
6134
|
lineOnlyView.destroy()
|
|
5827
6135
|
lineOnlyHost.remove()
|
|
5828
6136
|
splitView.destroy()
|
|
@@ -1363,6 +1363,87 @@
|
|
|
1363
1363
|
check('线路标签同样支持 labelRender: canvas(优化档、6 条全部进画布)', Number(lineCanvas?.dataset.labelCount || 0) === 6 && (lineCanvasLayer?.querySelectorAll('.gis-view__label').length ?? 0) === 0, `画布 ${lineCanvas?.dataset.labelCount ?? 0} 条 / DOM ${lineCanvasLayer?.querySelectorAll('.gis-view__label').length ?? 0} 条`)
|
|
1364
1364
|
canvasLineView.destroy()
|
|
1365
1365
|
canvasLineHost.remove()
|
|
1366
|
+
// 32. 中心点 API / selectionOnTop / 标签层级 / 跟随节点 / 字号缩放
|
|
1367
|
+
const layerHost = document.createElement('div')
|
|
1368
|
+
layerHost.style.cssText = 'width:640px;height:380px'
|
|
1369
|
+
document.body.append(layerHost)
|
|
1370
|
+
const layerView = createGisView(layerHost, {
|
|
1371
|
+
points: [
|
|
1372
|
+
{ id: 'layer-low', x: 0, y: 0, name: '低层', zIndex: 0 },
|
|
1373
|
+
{ id: 'layer-occluder', x: 30, y: 0, name: '遮挡', zIndex: 20, labelVisible: false },
|
|
1374
|
+
{ id: 'layer-high', x: 200, y: 0, name: '高层', zIndex: 20 },
|
|
1375
|
+
{ id: 'layer-far', x: -100, y: -100, name: '端点', zIndex: 0, labelVisible: false }
|
|
1376
|
+
],
|
|
1377
|
+
lines: [{ id: 'layer-line', from: 'layer-low', to: 'layer-far', name: '选中线' }],
|
|
1378
|
+
channels: [{ id: 'layer-channel', from: 'layer-low', to: 'layer-far', lineIds: ['layer-line'] }],
|
|
1379
|
+
initialView: { scale: 1, center: [0, 0] },
|
|
1380
|
+
zoomBaseScale: 1,
|
|
1381
|
+
label: { enabled: true, content: '{name}', followNodeZIndex: true, maxCount: 20 },
|
|
1382
|
+
showGrid: false
|
|
1383
|
+
})
|
|
1384
|
+
await settle(320)
|
|
1385
|
+
const layerLabel = index => layerHost.querySelector(`.gis-view__label[data-label-kind='point'][data-label-index='${index}']`)
|
|
1386
|
+
check('followNodeZIndex:低层标签先被更高层节点的图标盖住', !layerLabel(0) && Boolean(layerLabel(2)), `低层 ${layerLabel(0) ? '在' : '不在'} / 高层 ${layerLabel(2) ? '在' : '不在'}`)
|
|
1387
|
+
const lowLabelInfo = layerView.getLabels('point', 0)?.[0]
|
|
1388
|
+
const highLabelInfo = layerView.getLabels('point', 2)?.[0]
|
|
1389
|
+
check('getLabels 报出生效层级与跟随开关', lowLabelInfo?.zIndex === 0 && lowLabelInfo?.followNodeZIndex === true && highLabelInfo?.zIndex === 20, JSON.stringify({ low: lowLabelInfo && { zIndex: lowLabelInfo.zIndex, follow: lowLabelInfo.followNodeZIndex }, high: highLabelInfo && { zIndex: highLabelInfo.zIndex, follow: highLabelInfo.followNodeZIndex } }))
|
|
1390
|
+
layerView.updateLabel('point', 'layer-low', { zIndex: 40 })
|
|
1391
|
+
await settle(300)
|
|
1392
|
+
const explicitLow = layerLabel(0)
|
|
1393
|
+
const highNode = layerLabel(2)
|
|
1394
|
+
check('显式 zIndex 压过跟随层级:低层标签重新出现且 CSS 层级更高', Boolean(explicitLow) && Number(getComputedStyle(explicitLow).zIndex) > Number(getComputedStyle(highNode).zIndex), `低层 ${explicitLow ? getComputedStyle(explicitLow).zIndex : '不在'} / 高层 ${highNode ? getComputedStyle(highNode).zIndex : '不在'}`)
|
|
1395
|
+
layerView.updateLabel('point', 'layer-low', { zIndex: null })
|
|
1396
|
+
await settle(300)
|
|
1397
|
+
check('显式层级写回 null 后恢复跟随节点层级', !layerLabel(0) && layerView.getLabels('point', 0)?.[0]?.zIndex === 0, JSON.stringify(layerView.getLabels('point', 0)?.[0] ?? null))
|
|
1398
|
+
layerView.updatePoint('layer-occluder', { zIndex: 0 })
|
|
1399
|
+
await settle(320)
|
|
1400
|
+
check('节点样式链的 zIndex 变了会触发标签重排:遮挡消失后低层标签回来', Boolean(layerLabel(0)) && Number(getComputedStyle(layerLabel(0)).zIndex) === 1000000, layerLabel(0) ? getComputedStyle(layerLabel(0)).zIndex : '没有标签')
|
|
1401
|
+
layerView.selectFeatures(['layer-line'])
|
|
1402
|
+
await settle(120)
|
|
1403
|
+
const lineScene = layerView.snapshotScene()
|
|
1404
|
+
check('selectionOnTop:选中线路在普通节点之后、两端节点之前', lineScene.raisedLines.length === 1 && lineScene.lines.length === 0 && lineScene.points.length === 2 && lineScene.raised.length === 2, JSON.stringify({ raisedLines: lineScene.raisedLines.length, lines: lineScene.lines.length, points: lineScene.points.length, raised: lineScene.raised.length }))
|
|
1405
|
+
layerView.deselect()
|
|
1406
|
+
layerView.selectFeatures({ type: 'channel', channelId: 'layer-channel' })
|
|
1407
|
+
await settle(120)
|
|
1408
|
+
const channelScene = layerView.snapshotScene()
|
|
1409
|
+
check('selectionOnTop:选中通道整条抬层,两端节点仍压在线路上', channelScene.raisedLines.length === 1 && channelScene.lines.length === 0 && channelScene.raised.length === 2, JSON.stringify({ raisedLines: channelScene.raisedLines.length, lines: channelScene.lines.length, raised: channelScene.raised.length }))
|
|
1410
|
+
const labelScaleHost = document.createElement('div')
|
|
1411
|
+
labelScaleHost.style.cssText = 'width:640px;height:380px'
|
|
1412
|
+
document.body.append(labelScaleHost)
|
|
1413
|
+
const labelScaleView = createGisView(labelScaleHost, {
|
|
1414
|
+
points: [{ id: 'zoom-point', x: 0, y: 0, name: '缩放' }],
|
|
1415
|
+
initialView: { scale: 1, center: [0, 0] },
|
|
1416
|
+
zoomBaseScale: 1,
|
|
1417
|
+
label: { enabled: true, content: '{name}', maxCount: 1, style: { fontSize: '10px' }, zoomScale: 0 },
|
|
1418
|
+
showGrid: false
|
|
1419
|
+
})
|
|
1420
|
+
await settle(260)
|
|
1421
|
+
const labelScaleNode = () => labelScaleHost.querySelector(`.gis-view__label[data-label-kind='point'][data-label-index='0']`)
|
|
1422
|
+
const labelScaleFontSize = () => labelScaleNode() ? parseFloat(getComputedStyle(labelScaleNode()).fontSize) : 0
|
|
1423
|
+
const centerCopy = labelScaleView.getCenter()
|
|
1424
|
+
centerCopy[0] += 999
|
|
1425
|
+
const centerKept = labelScaleView.getCenter()
|
|
1426
|
+
const setCenterReturn = labelScaleView.setCenter([120, -80])
|
|
1427
|
+
const centerAfterSet = labelScaleView.getCenter()
|
|
1428
|
+
const centerScale = labelScaleView.getCamera().scale
|
|
1429
|
+
check('getCenter / setCenter:返回副本、只改中心且可链式调用', Math.abs(centerKept[0]) < 1e-6 && Math.abs(centerKept[1]) < 1e-6 && setCenterReturn === labelScaleView && Math.abs(centerAfterSet[0] - 120) < 1e-6 && Math.abs(centerAfterSet[1] + 80) < 1e-6 && Math.abs(centerScale - 1) < 1e-6, JSON.stringify({ kept: centerKept, set: centerAfterSet, scale: centerScale }))
|
|
1430
|
+
check('zoomScale: 0 时标签字号固定为屏幕字号(比例 2 仍是 10px)', labelScaleFontSize() === 10, `${labelScaleFontSize()}px`)
|
|
1431
|
+
labelScaleView.setCamera({ scale: 2, center: [0, 0] })
|
|
1432
|
+
await settle(320)
|
|
1433
|
+
check('zoomScale: 0 放大到 2 倍后字号不动', labelScaleFontSize() === 10, `${labelScaleFontSize()}px`)
|
|
1434
|
+
labelScaleView.updateLabel('point', 'zoom-point', { zoomScale: 1 })
|
|
1435
|
+
await settle(320)
|
|
1436
|
+
check('zoomScale: 1 时字号按相机比例放大到 20px', labelScaleFontSize() === 20 && labelScaleView.getLabels('point', 0)?.[0]?.zoomScale === 1, `${labelScaleFontSize()}px / ${labelScaleView.getLabels('point', 0)?.[0]?.zoomScale}`)
|
|
1437
|
+
labelScaleView.setCamera({ scale: 4, center: [0, 0] })
|
|
1438
|
+
await settle(360)
|
|
1439
|
+
check('zoomScale: 1 时字号继续跟随到 40px', labelScaleFontSize() === 40, `${labelScaleFontSize()}px`)
|
|
1440
|
+
labelScaleView.updateLabel('point', 'zoom-point', { zoomScale: 0 })
|
|
1441
|
+
await settle(320)
|
|
1442
|
+
check('zoomScale 改回 0 后恢复固定字号', labelScaleFontSize() === 10, `${labelScaleFontSize()}px`)
|
|
1443
|
+
labelScaleView.destroy()
|
|
1444
|
+
labelScaleHost.remove()
|
|
1445
|
+
layerView.destroy()
|
|
1446
|
+
layerHost.remove()
|
|
1366
1447
|
richView.destroy()
|
|
1367
1448
|
reRichView.destroy()
|
|
1368
1449
|
richHost.remove()
|
|
@@ -270,9 +270,37 @@
|
|
|
270
270
|
check('数据包围盒缓存:追加到范围外的节点会立即扩大读数',
|
|
271
271
|
boundsAfterAppend.minX === -600 && boundsAfterAppend.minY === -700,
|
|
272
272
|
JSON.stringify(boundsAfterAppend))
|
|
273
|
-
boundsView.destroy()
|
|
274
|
-
boundsHost.remove()
|
|
275
|
-
//
|
|
273
|
+
boundsView.destroy()
|
|
274
|
+
boundsHost.remove()
|
|
275
|
+
// 节点 id 索引:整份 refresh 后仍能解析 from / to;定向改 id 时旧键必须立即失效、新键马上可绑定
|
|
276
|
+
const idIndexHost = document.createElement('div')
|
|
277
|
+
document.querySelector('#map').append(idIndexHost)
|
|
278
|
+
const idIndexView = createGisView(idIndexHost, {
|
|
279
|
+
points: [{ id: 'ida', x: 0, y: 0 }, { id: 'idb', x: 100, y: 0 }],
|
|
280
|
+
lines: [{ id: 'idl', from: 'ida', to: 'idb' }],
|
|
281
|
+
width: 320,
|
|
282
|
+
height: 200
|
|
283
|
+
})
|
|
284
|
+
idIndexView.refresh()
|
|
285
|
+
// 隐藏节点插到最前面:可见 id 顺序不变、解析下标整体后移,缓存必须按新下标重建
|
|
286
|
+
idIndexView.updateData({
|
|
287
|
+
points: [{ id: 'hidden', x: -50, y: 0, visible: false }, { id: 'ida', x: 0, y: 0 }, { id: 'idb', x: 100, y: 0 }]
|
|
288
|
+
})
|
|
289
|
+
const idLineAfterShift = idIndexView.getLine('idl')
|
|
290
|
+
check('节点 id 索引:隐藏节点增删导致可见下标平移时仍按新下标解析',
|
|
291
|
+
idLineAfterShift?.path?.length === 2 && idLineAfterShift.path[0][0] === 0 && idLineAfterShift.path[1][0] === 100,
|
|
292
|
+
JSON.stringify({ path: idLineAfterShift?.path ?? null }))
|
|
293
|
+
const idLineAfterRefresh = idIndexView.getLine('idl')
|
|
294
|
+
idIndexView.updatePoint('ida', { id: 'ida-renamed' })
|
|
295
|
+
const idLineOldKey = idIndexView.getLine('idl')
|
|
296
|
+
const idLineAdded = idIndexView.addLine({ id: 'idl-new', from: 'ida-renamed', to: 'idb' })
|
|
297
|
+
const idLineNewKey = idIndexView.getLine('idl-new')
|
|
298
|
+
check('节点 id 索引:整份 refresh 后绑定线路仍可解析;updatePoint 改 id 后旧键立即失效、新键马上可绑定',
|
|
299
|
+
idLineAfterRefresh?.path?.length === 2 && (idLineOldKey?.path?.length ?? 0) < 2 && idLineAdded.ok && idLineNewKey?.path?.length === 2 && idLineNewKey.path[0][0] === 0 && idLineNewKey.path[1][0] === 100,
|
|
300
|
+
JSON.stringify({ refresh: idLineAfterRefresh?.path ?? null, oldAfterRename: idLineOldKey?.path ?? null, added: idLineAdded.ok, renamed: idLineNewKey?.path ?? null }))
|
|
301
|
+
idIndexView.destroy()
|
|
302
|
+
idIndexHost.remove()
|
|
303
|
+
// 两万个点会走发散渲染:等它铺完再拾取(没铺到的节点按设计不参与拾取)
|
|
276
304
|
const readyDeadline = now() + 8000
|
|
277
305
|
while (!big.isLayoutReady() && now() < readyDeadline) {
|
|
278
306
|
await sleep(50)
|
|
@@ -317,10 +317,60 @@
|
|
|
317
317
|
ckView.updateOptions({ selectionKinds: 'all' })
|
|
318
318
|
ckView.selectInBox({ x: 0, y: 0, width: ckBox.width, height: ckBox.height })
|
|
319
319
|
const ckBoxedInfo = ckView.getSelectionData()
|
|
320
|
-
check('selectInBox:线路框选与通道摊开沿用同一份判定(整框一次收下节点 / 独立线路 / 通道回线)', ckBoxedInfo.count === 4 && ckBoxedInfo.pointCount === 2 && ckBoxedInfo.lineCount === 2 && ckBoxedInfo.channelCount === 1, JSON.stringify({ items: ckBoxedInfo.count, points: ckBoxedInfo.pointCount, lines: ckBoxedInfo.lineCount, channels: ckBoxedInfo.channelCount }))
|
|
320
|
+
check('selectInBox:线路框选与通道摊开沿用同一份判定(整框一次收下节点 / 独立线路 / 通道回线)', ckBoxedInfo.count === 4 && ckBoxedInfo.pointCount === 2 && ckBoxedInfo.lineCount === 2 && ckBoxedInfo.channelCount === 1, JSON.stringify({ items: ckBoxedInfo.count, points: ckBoxedInfo.pointCount, lines: ckBoxedInfo.lineCount, channels: ckBoxedInfo.channelCount }))
|
|
321
321
|
ckView.destroy()
|
|
322
322
|
ckHost.remove()
|
|
323
323
|
|
|
324
|
+
/* ---------- 本轮:线路 / 通道的端点框选模式 ---------- */
|
|
325
|
+
// 半框只盖住左端点:普通 line / channel 仍按“几何沾框”命中,端点模式必须两个端点都在框里。
|
|
326
|
+
const epHost = document.createElement('div')
|
|
327
|
+
epHost.style.cssText = 'width:480px;height:320px'
|
|
328
|
+
document.body.append(epHost)
|
|
329
|
+
const epView = createGisView(epHost, {
|
|
330
|
+
points: [
|
|
331
|
+
{ id: 'ep-a', x: -130, y: 0, size: 16 },
|
|
332
|
+
{ id: 'ep-b', x: 130, y: 0, size: 16 }
|
|
333
|
+
],
|
|
334
|
+
lines: [
|
|
335
|
+
{ id: 'ep-bound', from: 'ep-a', to: 'ep-b', width: 8 },
|
|
336
|
+
{ id: 'ep-path', path: [[-80, 60], [80, 60]], width: 8 },
|
|
337
|
+
{ id: 'ep-ch-line', from: 'ep-a', to: 'ep-b', width: 8 }
|
|
338
|
+
],
|
|
339
|
+
channels: [{ id: 'ep-ch', lineIds: ['ep-ch-line'] }],
|
|
340
|
+
features: 'view',
|
|
341
|
+
selectionKinds: 'all',
|
|
342
|
+
showGrid: false,
|
|
343
|
+
fitOnInit: false,
|
|
344
|
+
initialView: { center: [0, 0], scale: 1 },
|
|
345
|
+
width: 480,
|
|
346
|
+
height: 320
|
|
347
|
+
})
|
|
348
|
+
await settle(120)
|
|
349
|
+
const epBox = epView.canvas.getBoundingClientRect()
|
|
350
|
+
const epHalf = { x: 0, y: 0, width: epBox.width / 2, height: epBox.height }
|
|
351
|
+
const epAll = { x: 0, y: 0, width: epBox.width, height: epBox.height }
|
|
352
|
+
const epInfo = () => epView.getSelectionData()
|
|
353
|
+
epView.selectInBox(epHalf, { selectionKinds: ['line'] })
|
|
354
|
+
const epLegacy = epInfo()
|
|
355
|
+
check('line 旧模式:线路几何沾到半框就选中', epLegacy.lineCount === 2 && epView.isSelected('ep-bound') && epView.isSelected('ep-path'), JSON.stringify({ lines: epLegacy.lineCount, ids: epLegacy.ids }))
|
|
356
|
+
epView.selectInBox(epHalf, { selectionKinds: ['lineEndpoints'] })
|
|
357
|
+
const epLineHalf = epInfo()
|
|
358
|
+
check('lineEndpoints:只框住一端时独立线路不选中', epLineHalf.lineCount === 0 && epLineHalf.channelCount === 0 && !epView.isSelected('ep-bound') && !epView.isSelected('ep-path'), JSON.stringify({ lines: epLineHalf.lineCount, channels: epLineHalf.channelCount, ids: epLineHalf.ids }))
|
|
359
|
+
epView.selectInBox(epAll, { selectionKinds: ['line-endpoints'] })
|
|
360
|
+
const epLineAll = epInfo()
|
|
361
|
+
check('lineEndpoints:两端都在框内才选中,纯 path 线路仍跳过(不依赖 point 档)', epLineAll.pointCount === 0 && epLineAll.lineCount === 1 && epView.isSelected('ep-bound') && !epView.isSelected('ep-path'), JSON.stringify({ points: epLineAll.pointCount, lines: epLineAll.lineCount, ids: epLineAll.ids }))
|
|
362
|
+
epView.selectInBox(epHalf, { selectionKinds: ['channel'] })
|
|
363
|
+
const epChannelHalfLegacy = epInfo()
|
|
364
|
+
check('channel 旧模式:成员回线沾到半框就选中整条通道', epChannelHalfLegacy.channelCount === 1 && epChannelHalfLegacy.lineCount === 1 && epView.isSelected('ep-ch-line'), JSON.stringify({ lines: epChannelHalfLegacy.lineCount, channels: epChannelHalfLegacy.channelCount, ids: epChannelHalfLegacy.ids }))
|
|
365
|
+
epView.selectInBox(epHalf, { selectionKinds: ['channelEndpoints'] })
|
|
366
|
+
const epChannelHalf = epInfo()
|
|
367
|
+
check('channelEndpoints:只框住一端时通道不选中', epChannelHalf.channelCount === 0 && epChannelHalf.lineCount === 0 && !epView.isSelected('ep-ch-line'), JSON.stringify({ lines: epChannelHalf.lineCount, channels: epChannelHalf.channelCount, ids: epChannelHalf.ids }))
|
|
368
|
+
epView.selectInBox(epAll, { selectionKinds: ['channel-endpoints'] })
|
|
369
|
+
const epChannelAll = epInfo()
|
|
370
|
+
check('channelEndpoints:两端节点都在框内才选中整条通道', epChannelAll.pointCount === 0 && epChannelAll.channelCount === 1 && epChannelAll.lineCount === 1 && epView.isSelected('ep-ch-line'), JSON.stringify({ points: epChannelAll.pointCount, lines: epChannelAll.lineCount, channels: epChannelAll.channelCount, ids: epChannelAll.ids }))
|
|
371
|
+
epView.destroy()
|
|
372
|
+
epHost.remove()
|
|
373
|
+
|
|
324
374
|
/* ---------- 问题 1(本轮):接口取消选中 —— deselect(单个 / 一批 / 整条通道)与「取消全部」的老写法 ---------- */
|
|
325
375
|
const dzHost = document.createElement('div')
|
|
326
376
|
dzHost.style.cssText = 'width:320px;height:200px'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# 从 0 到完整的实现(索引)
|
|
2
2
|
|
|
3
3
|
这一份是「照着源码从零写一遍」的讲义:先讲清地图的模型,再一块一块把渲染、相机、解析层、通道、覆盖层、筛选与性能写出来,最后给一份可以照抄的整套代码。
|
|
4
|
+
第一次从 0 上手,建议先走 [四段学习路线、每步验收与优化顺序](steps/README.md):第 1 ~ 9 步手写主干,第 10 步接入真组件,第 11 ~ 13 步补上线配置;卡住时再回到下面章节补原理,或直接照抄 `from0/13` / `14` 的整套代码。
|
|
4
5
|
只想用组件看 [GisView.md](GisView.md);每一帧怎么实现、为什么这么写看 [实现原理](GisView实现原理.md);分步 demo(每步一个文件、页面上直接跑)在 [docs/steps](steps/README.md)。
|
|
5
6
|
性能数据与 WebGPU 的取舍见 [实现原理 18 性能实测与 WebGPU 评估](internals/18-性能实测与WebGPU评估.md#18-性能实测与-webgpu-评估)。
|
|
6
7
|
|