gis-view-webgl2 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -14
- package/dist/gis-view.css +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +421 -76
- package/dist/index.js +1 -1
- package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +151 -0
- package/docs/GisView.md +4 -4
- package/docs/ai/06-/344/274/232/350/257/235/347/264/242/345/274/225/344/270/216/345/244/215/346/237/245/346/226/271/346/263/225.md +137 -0
- package/docs/api/index.html +23 -9
- package/docs/demos/ApiCoverageDemo.vue +644 -0
- package/docs/demos/ChannelDemo.vue +14 -12
- package/docs/demos/ClusterDemo.vue +251 -0
- package/docs/demos/CopyMergeDemo.vue +3 -4
- package/docs/demos/EventDemo.vue +3 -3
- package/docs/demos/ExtensionDemo.vue +69 -12
- package/docs/demos/GisViewDemo.vue +64 -8809
- package/docs/demos/SearchFilterDemo.vue +13 -11
- package/docs/demos/api-index.js +58 -47
- package/docs/demos/demos.js +34 -17
- package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +29 -0
- package/docs/demos/gis-view-demo/GisViewDemoDialogs.vue +82 -0
- package/docs/demos/gis-view-demo/GisViewDemoFooter.vue +16 -0
- package/docs/demos/gis-view-demo/GisViewDemoHeading.vue +16 -0
- package/docs/demos/gis-view-demo/GisViewDemoPanelStack.vue +71 -0
- package/docs/demos/gis-view-demo/GisViewDemoStage.vue +219 -0
- package/docs/demos/gis-view-demo/demo.css +1230 -0
- package/docs/demos/gis-view-demo/demoContext.js +12 -0
- package/docs/demos/gis-view-demo/panels/DataPanels.vue +303 -0
- package/docs/demos/gis-view-demo/panels/OpsPanels.vue +174 -0
- package/docs/demos/gis-view-demo/panels/StylePanels.vue +561 -0
- package/docs/demos/gis-view-demo/panels/ViewPanels.vue +396 -0
- package/docs/demos/gis-view-demo/useGisViewDemo.js +6188 -0
- package/docs/format/07-/344/272/213/344/273/266/350/275/275/350/215/267/344/270/216/347/244/272/344/276/213/346/225/260/346/215/256.md +3 -2
- package/docs/index.html +41 -4
- package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +1 -1
- package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +3 -2
- package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +104 -16
- package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +15 -14
- package/docs/manual/07-/346/214/207/351/222/210/344/270/216/350/246/201/347/264/240/347/274/226/350/276/221.md +40 -3
- package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +50 -3
- package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +6 -2
- package/docs/manual/10-/351/200/232/351/201/223/347/274/226/350/276/221/344/270/216/346/226/260/345/242/236.md +39 -7
- package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +13 -4
- package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +3 -0
- package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +121 -49
- package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +37 -3
- package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +17 -8
- package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +45 -25
- package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +91 -20
- package/docs/manual/19-/350/257/273/346/225/260/344/270/216/346/213/226/346/213/275/351/242/204/350/247/210.md +3 -3
- package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +25 -1
- package/docs/manual/21-/350/257/264/346/230/216/344/270/216/345/270/270/350/247/201/351/227/256/351/242/230.md +11 -8
- package/docs/steps/README.md +17 -11
- package/docs/steps/selftest-actions.html +3 -5
- package/docs/steps/selftest-demo.html +27 -18
- package/docs/steps/selftest-demos.html +409 -32
- package/docs/steps/selftest-docs.html +33 -4
- package/docs/steps/selftest-effects.html +200 -0
- package/docs/steps/selftest-features.html +603 -21
- package/docs/steps/selftest-labels.html +74 -0
- package/docs/steps/selftest-perf.html +293 -20
- package/docs/steps/selftest-scale.html +150 -29
- package/docs/steps/selftest-select.html +90 -0
- package/docs/steps/step-10-full.js +1 -1
- package/docs/steps/step-12-events.js +1 -1
- package/package.json +1 -1
|
@@ -1592,6 +1592,293 @@
|
|
|
1592
1592
|
cwView.destroy()
|
|
1593
1593
|
cwHost.remove()
|
|
1594
1594
|
|
|
1595
|
+
/* 问题 3(本轮):setFilter 只让节点不画,不该改变仍显示的通道宽度 ——
|
|
1596
|
+
从前锚点被筛掉就不在 pointByIndex 里,通道退回默认宽度;只筛掉一端时还会改按另一端尺寸算 */
|
|
1597
|
+
const cwfHost = document.createElement('div')
|
|
1598
|
+
cwfHost.style.cssText = 'width:320px;height:200px'
|
|
1599
|
+
document.body.appendChild(cwfHost)
|
|
1600
|
+
const cwfView = createGisView(cwfHost, {
|
|
1601
|
+
points: [
|
|
1602
|
+
{ id: 'cwf-a', x: -30, y: 0, size: 8, symbol: 'square', level: 1 },
|
|
1603
|
+
{ id: 'cwf-b', x: 30, y: 0, size: 24, symbol: 'square', level: 2 }
|
|
1604
|
+
],
|
|
1605
|
+
lines: [
|
|
1606
|
+
{ id: 'cwf-l1', from: 'cwf-a', to: 'cwf-b', width: 1 },
|
|
1607
|
+
{ id: 'cwf-l2', from: 'cwf-a', to: 'cwf-b', width: 1 }
|
|
1608
|
+
],
|
|
1609
|
+
channels: [{ id: 'cwf-ch', from: 'cwf-a', to: 'cwf-b', lineIds: ['cwf-l1', 'cwf-l2'], tangentInset: 0 }],
|
|
1610
|
+
initialView: { center: [0, 0], scale: 1 },
|
|
1611
|
+
showGrid: false
|
|
1612
|
+
})
|
|
1613
|
+
await settle(150)
|
|
1614
|
+
const cwfGap = () => {
|
|
1615
|
+
const tops = cwfView.listLines().filter(line => String(line.item.id).startsWith('cwf-')).map(line => line.path[0][1])
|
|
1616
|
+
return tops.length === 2 ? Math.abs(tops[0] - tops[1]) : 0
|
|
1617
|
+
}
|
|
1618
|
+
const cwfBase = cwfGap()
|
|
1619
|
+
check('筛选前通道宽度基线:两端取小的尺寸 8(tangentInset: 0)', Math.abs(cwfBase - 8) < 0.01, `间距 ${cwfBase}`)
|
|
1620
|
+
cwfView.setFilter({ points: { level: 2 }, cascade: false })
|
|
1621
|
+
await settle(100)
|
|
1622
|
+
check('setFilter 只筛掉小尺寸锚点:回线仍按两端较小值取宽,不跳到大节点尺寸', Math.abs(cwfGap() - cwfBase) < 0.01, `${cwfBase} → ${cwfGap()}`)
|
|
1623
|
+
cwfView.setFilter({ points: { level: 0 }, cascade: false })
|
|
1624
|
+
await settle(100)
|
|
1625
|
+
check('setFilter 把两端锚点都筛掉:通道回线仍显示,宽度不退到默认 channelWidth', cwfView.listLines().filter(line => String(line.item.id).startsWith('cwf-')).length === 2 && Math.abs(cwfGap() - cwfBase) < 0.01, `间距 ${cwfBase} → ${cwfGap()}`)
|
|
1626
|
+
cwfView.setFilter(null)
|
|
1627
|
+
await settle(100)
|
|
1628
|
+
check('清除筛选:通道宽度与筛选前完全一致', Math.abs(cwfGap() - cwfBase) < 0.01, `间距 ${cwfBase} → ${cwfGap()}`)
|
|
1629
|
+
// 可见锚点若按选中 / 悬停后的尺寸算,筛掉另一端时它会变成唯一锚点,通道宽度就会跳
|
|
1630
|
+
cwfView.updateOptions({ pointSelectStyle: { sizeScale: 2 } })
|
|
1631
|
+
cwfView.selectFeatures('cwf-b')
|
|
1632
|
+
await settle(80)
|
|
1633
|
+
cwfView.setFilter({ points: { level: 1 }, cascade: false })
|
|
1634
|
+
await settle(100)
|
|
1635
|
+
check('筛选叠加选中样式:锚点宽度仍按中性尺寸取,不随选中放大而跳', Math.abs(cwfGap() - cwfBase) < 0.01, `间距 ${cwfBase} → ${cwfGap()}`)
|
|
1636
|
+
cwfView.setFilter({ points: { level: 0 }, cascade: false })
|
|
1637
|
+
await settle(100)
|
|
1638
|
+
check('选中状态下把两端都筛掉:通道宽度仍保持原值', Math.abs(cwfGap() - cwfBase) < 0.01, `间距 ${cwfBase} → ${cwfGap()}`)
|
|
1639
|
+
cwfView.setFilter(null)
|
|
1640
|
+
await settle(80)
|
|
1641
|
+
cwfView.destroy()
|
|
1642
|
+
cwfHost.remove()
|
|
1643
|
+
|
|
1644
|
+
/* 分组通道 + 成员回线被筛:筛选只影响可见性,保留下来的回线仍按原来的车道与宽度走 */
|
|
1645
|
+
const gfwHost = document.createElement('div')
|
|
1646
|
+
gfwHost.style.cssText = 'width:340px;height:220px'
|
|
1647
|
+
document.body.appendChild(gfwHost)
|
|
1648
|
+
const gfwView = createGisView(gfwHost, {
|
|
1649
|
+
points: [
|
|
1650
|
+
{ id: 'gfw-a', x: -30, y: 0, size: 8, symbol: 'square', level: 1 },
|
|
1651
|
+
{ id: 'gfw-b', x: 30, y: 0, size: 24, symbol: 'square', level: 2 }
|
|
1652
|
+
],
|
|
1653
|
+
lines: [
|
|
1654
|
+
{ id: 'gfw-l1', from: 'gfw-a', to: 'gfw-b', color: '#ff0000' },
|
|
1655
|
+
{ id: 'gfw-l2', from: 'gfw-a', to: 'gfw-b', color: '#ff0000' },
|
|
1656
|
+
{ id: 'gfw-l3', from: 'gfw-a', to: 'gfw-b', color: '#0000ff' },
|
|
1657
|
+
{ id: 'gfw-l4', from: 'gfw-a', to: 'gfw-b', color: '#0000ff' }
|
|
1658
|
+
],
|
|
1659
|
+
channels: [{
|
|
1660
|
+
id: 'gfw-ch', from: 'gfw-a', to: 'gfw-b', lineIds: ['gfw-l1', 'gfw-l2', 'gfw-l3', 'gfw-l4'],
|
|
1661
|
+
tangentInset: 0, offsetMode: 'inset', inset: 0.1,
|
|
1662
|
+
groups: [
|
|
1663
|
+
{ id: 'gfw-top', lineIds: ['gfw-l1', 'gfw-l2'], path: [[0, 24]] },
|
|
1664
|
+
{ id: 'gfw-bot', lineIds: ['gfw-l3', 'gfw-l4'], path: [[0, -24]] }
|
|
1665
|
+
]
|
|
1666
|
+
}],
|
|
1667
|
+
initialView: { center: [0, 0], scale: 1 },
|
|
1668
|
+
showGrid: false
|
|
1669
|
+
})
|
|
1670
|
+
await settle(150)
|
|
1671
|
+
const gfwPaths = () => Object.fromEntries(gfwView.listLines()
|
|
1672
|
+
.filter(line => String(line.item.id).startsWith('gfw-'))
|
|
1673
|
+
.map(line => [String(line.item.id), JSON.stringify(line.path)]))
|
|
1674
|
+
const gfwBase = gfwPaths()
|
|
1675
|
+
const gfwGeometry = () => JSON.stringify({
|
|
1676
|
+
channels: gfwView.getChannels(),
|
|
1677
|
+
groups: gfwView.getChannelGroups('gfw-ch')
|
|
1678
|
+
})
|
|
1679
|
+
const gfwGeometryBase = gfwGeometry()
|
|
1680
|
+
gfwView.setFilter({ points: { level: 0 }, cascade: false })
|
|
1681
|
+
await settle(100)
|
|
1682
|
+
const gfwNoPoints = gfwPaths()
|
|
1683
|
+
check('分组通道:两端节点被筛掉时四条回线仍在原车道', Object.keys(gfwNoPoints).length === 4 && Object.keys(gfwBase).every(id => gfwNoPoints[id] === gfwBase[id]), JSON.stringify({ base: gfwBase, current: gfwNoPoints }))
|
|
1684
|
+
gfwView.setFilter({ lines: { color: '#ff0000' }, cascade: false })
|
|
1685
|
+
await settle(100)
|
|
1686
|
+
const gfwFiltered = gfwPaths()
|
|
1687
|
+
check('分组通道:筛掉一半成员回线后,保留回线的几何与筛选前完全一致', Object.keys(gfwFiltered).length === 2 && gfwFiltered['gfw-l1'] === gfwBase['gfw-l1'] && gfwFiltered['gfw-l2'] === gfwBase['gfw-l2'], JSON.stringify({ base: [gfwBase['gfw-l1'], gfwBase['gfw-l2']], current: gfwFiltered }))
|
|
1688
|
+
gfwView.setFilter(null)
|
|
1689
|
+
await settle(80)
|
|
1690
|
+
const gfwRestored = gfwPaths()
|
|
1691
|
+
check('分组通道:清除筛选后四条回线逐顶点回到筛选前的几何', Object.keys(gfwRestored).length === 4 && Object.keys(gfwBase).every(id => gfwRestored[id] === gfwBase[id]), JSON.stringify({ base: gfwBase, current: gfwRestored }))
|
|
1692
|
+
gfwView.setFilter({ lines: { color: '#ff0000' }, cascade: false })
|
|
1693
|
+
await settle(80)
|
|
1694
|
+
gfwView.setFilter(null)
|
|
1695
|
+
await settle(80)
|
|
1696
|
+
const gfwRestoredAgain = gfwPaths()
|
|
1697
|
+
check('分组通道:连续隐藏 / 恢复成员回线后仍按原车道与骨架绘制', Object.keys(gfwRestoredAgain).length === 4 && Object.keys(gfwBase).every(id => gfwRestoredAgain[id] === gfwBase[id]), JSON.stringify({ base: gfwBase, current: gfwRestoredAgain }))
|
|
1698
|
+
// 默认 binding 级联会把两端节点、绑定回线与整条通道一起收起;恢复时引擎必须按当前锚点重新接上,
|
|
1699
|
+
// 不能沿用「隐藏期间没有骨架」的通道状态。这里连通道 / 分组读数一起对,避免只看 listLines 漏掉串线。
|
|
1700
|
+
gfwView.setFilter({ points: { level: 0 } })
|
|
1701
|
+
await settle(100)
|
|
1702
|
+
check('分组通道:默认 binding 级联筛掉两端节点时回线一起收起', gfwView.listLines().filter(line => String(line.item.id).startsWith('gfw-')).length === 0, JSON.stringify({ lines: gfwPaths() }))
|
|
1703
|
+
gfwView.setFilter(null)
|
|
1704
|
+
await settle(100)
|
|
1705
|
+
check('分组通道:默认级联恢复后回线、通道骨架与分组骨架逐项回到原值', Object.keys(gfwPaths()).length === 4 && Object.keys(gfwBase).every(id => gfwPaths()[id] === gfwBase[id]) && gfwGeometry() === gfwGeometryBase, JSON.stringify({ base: gfwGeometryBase, current: gfwGeometry() }))
|
|
1706
|
+
gfwView.hideFeatures('point', ['gfw-a', 'gfw-b'])
|
|
1707
|
+
await settle(100)
|
|
1708
|
+
gfwView.showFeatures('point', ['gfw-a', 'gfw-b'])
|
|
1709
|
+
await settle(100)
|
|
1710
|
+
check('分组通道:visible 隐藏 / 显示两端节点后几何仍逐项回到原值', Object.keys(gfwPaths()).length === 4 && Object.keys(gfwBase).every(id => gfwPaths()[id] === gfwBase[id]) && gfwGeometry() === gfwGeometryBase, JSON.stringify({ base: gfwGeometryBase, current: gfwGeometry() }))
|
|
1711
|
+
gfwView.destroy()
|
|
1712
|
+
gfwHost.remove()
|
|
1713
|
+
|
|
1714
|
+
/* 焦点类型消歧 + 分组选中 + 拐点选中 */
|
|
1715
|
+
const faHost = document.createElement('div')
|
|
1716
|
+
faHost.style.cssText = 'width:420px;height:260px'
|
|
1717
|
+
document.body.appendChild(faHost)
|
|
1718
|
+
const faView = createGisView(faHost, {
|
|
1719
|
+
points: [
|
|
1720
|
+
{ id: 'same', x: 0, y: 0 },
|
|
1721
|
+
{ id: 'fa-low-a', x: -50, y: -40 },
|
|
1722
|
+
{ id: 'fa-low-b', x: 50, y: -40 },
|
|
1723
|
+
{ id: 'fa-high-a', x: -50, y: 40 },
|
|
1724
|
+
{ id: 'fa-high-b', x: 50, y: 40 }
|
|
1725
|
+
],
|
|
1726
|
+
lines: [
|
|
1727
|
+
{ id: 'same', path: [[-100, 0], [-80, 0]] },
|
|
1728
|
+
{ id: 'fa-l1', from: 'fa-low-a', to: 'fa-low-b' },
|
|
1729
|
+
{ id: 'fa-l2', from: 'fa-low-a', to: 'fa-low-b' },
|
|
1730
|
+
{ id: 'fa-l3', from: 'fa-high-a', to: 'fa-high-b' },
|
|
1731
|
+
{ id: 'fa-l4', from: 'fa-high-a', to: 'fa-high-b' },
|
|
1732
|
+
{ id: 'fa-around', path: [[0, 80], [50, 100], [100, 80]] }
|
|
1733
|
+
],
|
|
1734
|
+
channels: [
|
|
1735
|
+
{ id: 'fa-low-ch', from: 'fa-low-a', to: 'fa-low-b', lineIds: ['fa-l1', 'fa-l2'], groups: [{ id: 'fa-low', lineIds: ['fa-l1', 'fa-l2'] }] },
|
|
1736
|
+
{ id: 'fa-high-ch', from: 'fa-high-a', to: 'fa-high-b', lineIds: ['fa-l3', 'fa-l4'], groups: [{ id: 'fa-high', lineIds: ['fa-l3', 'fa-l4'] }] }
|
|
1737
|
+
],
|
|
1738
|
+
initialView: { center: [0, 0], scale: 1 },
|
|
1739
|
+
waypointEditable: true,
|
|
1740
|
+
showGrid: false
|
|
1741
|
+
})
|
|
1742
|
+
await settle(100)
|
|
1743
|
+
faView.focusOn('same')
|
|
1744
|
+
const faStringCenter = faView.getCamera().center
|
|
1745
|
+
faView.focusOn({ type: 'line', id: 'same' })
|
|
1746
|
+
const faLineCenter = faView.getCamera().center
|
|
1747
|
+
faView.focusOn({ type: 'point', id: 'same' })
|
|
1748
|
+
const faPointCenter = faView.getCamera().center
|
|
1749
|
+
faView.focusOn({ type: 'channel', id: 'fa-low-ch' })
|
|
1750
|
+
const faChannelCenter = faView.getCamera().center
|
|
1751
|
+
faView.focusOn({ type: 'group', groupId: 'fa-high' })
|
|
1752
|
+
const faHighCenter = faView.getCamera().center
|
|
1753
|
+
faView.focusOn({ type: 'group', channelId: 'fa-low-ch', groupId: 'fa-low' })
|
|
1754
|
+
const faLowCenter = faView.getCamera().center
|
|
1755
|
+
check('focusOn:字符串同 id 先聚焦节点,显式 type 可聚焦同名线路', Math.abs(faStringCenter[0]) < 0.01 && Math.abs(faPointCenter[0]) < 0.01 && Math.abs(faLineCenter[0] + 90) < 0.01, JSON.stringify({ string: faStringCenter, point: faPointCenter, line: faLineCenter }))
|
|
1756
|
+
check('focusOn:显式通道 / 分组类型按各自范围居中(groupId 唯一时可不写 channelId)', Math.abs(faChannelCenter[0]) < 0.01 && Math.abs(faHighCenter[1] - 40) < 0.01 && Math.abs(faLowCenter[1] + 40) < 0.01, JSON.stringify({ channel: faChannelCenter, high: faHighCenter, low: faLowCenter }))
|
|
1757
|
+
faView.selectFeatures({ type: 'group', channelId: 'fa-low-ch', groupId: 'fa-low' })
|
|
1758
|
+
const faSelectedGroup = faView.getSelections().filter(item => item.type === 'line').map(item => item.item.id).sort()
|
|
1759
|
+
faView.selectFeatures({ type: 'group', groupId: 'fa-high' })
|
|
1760
|
+
const faSelectedUnique = faView.getSelections().filter(item => item.type === 'line').map(item => item.item.id).sort()
|
|
1761
|
+
check('selectFeatures:type=group 与唯一 groupId 都只选中该分组', faSelectedGroup.join(',') === 'fa-l1,fa-l2' && faSelectedUnique.join(',') === 'fa-l3,fa-l4', JSON.stringify({ low: faSelectedGroup, high: faSelectedUnique }))
|
|
1762
|
+
faView.selectWaypoint('fa-around', 1)
|
|
1763
|
+
const faWaypointSelected = faView.getWaypoints().some(item => item.itemId === undefined && item.lineIndex === 5 && item.pathIndex === 1 && item.selected)
|
|
1764
|
+
const faWaypointLineSelected = faView.getSelections().some(item => item.type === 'line' && item.item.id === 'fa-around')
|
|
1765
|
+
faView.selectFeatures('fa-around')
|
|
1766
|
+
const faWaypointCleared = faView.getWaypoints().filter(item => item.lineIndex === 5).every(item => !item.selected)
|
|
1767
|
+
check('selectWaypoint:按线路 + 下标选中拐点并带选线路,selectFeatures 会清掉拐点选中', faWaypointSelected && faWaypointLineSelected && faWaypointCleared, JSON.stringify({ selected: faWaypointSelected, line: faWaypointLineSelected, cleared: faWaypointCleared, all: faView.getWaypoints(), lines: faView.getData().lines }))
|
|
1768
|
+
// search 也认 focusOn 的四种目标;分组命中可以直接交给 focusOn / selectFeatures
|
|
1769
|
+
const faGroupSearch = faView.search({ keyword: 'fa-high', scope: 'group', limit: 10 })
|
|
1770
|
+
const faGroupHit = faGroupSearch.groups[0]
|
|
1771
|
+
faView.focusOn(faGroupHit)
|
|
1772
|
+
const faGroupSearchCenter = faView.getCamera().center
|
|
1773
|
+
faView.selectFeatures(faGroupHit)
|
|
1774
|
+
const faGroupHitSelected = faView.getSelections().filter(item => item.type === 'line').map(item => item.item.id).sort()
|
|
1775
|
+
const faDefaultGroupSearch = await faView.searchAsync({ keyword: 'fa-high', limit: 10 })
|
|
1776
|
+
check('search:单数 group 范围返回分组命中,命中可直接 focusOn / selectFeatures', faGroupSearch.scope.length === 1 && faGroupSearch.scope[0] === 'groups' && faGroupHit?.kind === 'group' && faGroupHit.channelIndex === 1 && faGroupHit.groupId === 'fa-high' && faGroupHit.groupIndex === 0 && Math.abs(faGroupSearchCenter[1] - 40) < 0.01 && faGroupHitSelected.join(',') === 'fa-l3,fa-l4', JSON.stringify({ scope: faGroupSearch.scope, hit: faGroupHit, center: faGroupSearchCenter, selected: faGroupHitSelected }))
|
|
1777
|
+
check('searchAsync:缺省范围包含 groups,Worker / 同步回落都带 groups 结果', faDefaultGroupSearch.groups.length === 1 && faDefaultGroupSearch.hits.some(hit => hit.kind === 'group' && hit.groupId === 'fa-high'), JSON.stringify({ scope: faDefaultGroupSearch.scope, groups: faDefaultGroupSearch.groups.length, worker: faDefaultGroupSearch.worker }))
|
|
1778
|
+
// 线路 / 通道 / 分组都只在 zoom: true 时适配:与显式 zoomToBounds 的比例逐位一致,没有类型专属放大倍率
|
|
1779
|
+
const faLineHit = faView.search({ keyword: 'fa-around', scope: 'line', limit: 1 }).lines[0]
|
|
1780
|
+
const faChannelHit = faView.search({ keyword: 'fa-low-ch', scope: 'channel', limit: 1 }).channels[0]
|
|
1781
|
+
const faFocusScales = [faLineHit, faChannelHit, faGroupHit].map((hit) => {
|
|
1782
|
+
faView.zoomToBounds(hit.bounds)
|
|
1783
|
+
const expected = faView.getCamera().scale
|
|
1784
|
+
faView.focusOn(hit, { zoom: true })
|
|
1785
|
+
return [expected, faView.getCamera().scale]
|
|
1786
|
+
})
|
|
1787
|
+
check('focusOn({ zoom: true }):线路 / 通道 / 分组适配后的比例与同一份 bounds 的 zoomToBounds 完全一致', faFocusScales.every(([expected, actual]) => Math.abs(expected - actual) < 1e-9), JSON.stringify(faFocusScales))
|
|
1788
|
+
faView.destroy()
|
|
1789
|
+
faHost.remove()
|
|
1790
|
+
|
|
1791
|
+
/* 筛选是隐藏、不是删除:选中的要素被筛掉时保留在选中集里,条件清空后原样恢复 */
|
|
1792
|
+
const fsHost = document.createElement('div')
|
|
1793
|
+
fsHost.style.cssText = 'width:320px;height:200px'
|
|
1794
|
+
document.body.appendChild(fsHost)
|
|
1795
|
+
const fsChanges = []
|
|
1796
|
+
const fsView = createGisView(fsHost, {
|
|
1797
|
+
points: [
|
|
1798
|
+
{ id: 'fs-a', x: -30, y: 0, level: 0 },
|
|
1799
|
+
{ id: 'fs-b', x: 30, y: 0, level: 1 }
|
|
1800
|
+
],
|
|
1801
|
+
lines: [{ id: 'fs-l1', from: 'fs-a', to: 'fs-b' }],
|
|
1802
|
+
initialView: { center: [0, 0], scale: 1 },
|
|
1803
|
+
showGrid: false,
|
|
1804
|
+
onSelectChange: payload => fsChanges.push(payload)
|
|
1805
|
+
})
|
|
1806
|
+
await settle(100)
|
|
1807
|
+
fsView.selectFeatures(['fs-a', 'fs-l1'])
|
|
1808
|
+
const fsChangesBeforeFilter = fsChanges.length
|
|
1809
|
+
fsView.setFilter({ points: { level: 1 }, cascade: false })
|
|
1810
|
+
await settle(100)
|
|
1811
|
+
const fsHiddenIds = fsView.getSelections().map(item => String(item.item.id)).sort().join(',')
|
|
1812
|
+
const fsHiddenVisible = fsView.getVisibleData().points.some(point => point.id === 'fs-a')
|
|
1813
|
+
const fsChangesAfterFilter = fsChanges.length
|
|
1814
|
+
fsView.setFilter(null)
|
|
1815
|
+
await settle(100)
|
|
1816
|
+
const fsRestoredIds = fsView.getSelections().map(item => String(item.item.id)).sort().join(',')
|
|
1817
|
+
check('setFilter 只隐藏要素:选中项保留,条件清空后仍选中且不额外发选中变更', fsHiddenIds === 'fs-a,fs-l1' && fsHiddenVisible === false && fsChangesAfterFilter === fsChangesBeforeFilter && fsRestoredIds === 'fs-a,fs-l1', JSON.stringify({ hidden: fsHiddenIds, visible: fsHiddenVisible, changes: [fsChangesBeforeFilter, fsChangesAfterFilter], restored: fsRestoredIds }))
|
|
1818
|
+
fsView.destroy()
|
|
1819
|
+
fsHost.remove()
|
|
1820
|
+
|
|
1821
|
+
/* lineFollowsNode: false:隐藏 / 筛选 / 删除只动节点,回线与通道停在最后坐标 */
|
|
1822
|
+
const lfHost = document.createElement('div')
|
|
1823
|
+
lfHost.style.cssText = 'width:360px;height:220px'
|
|
1824
|
+
document.body.appendChild(lfHost)
|
|
1825
|
+
const lfView = createGisView(lfHost, {
|
|
1826
|
+
points: [
|
|
1827
|
+
{ id: 'lf-a', x: -30, y: 0, size: 16, level: 0 },
|
|
1828
|
+
{ id: 'lf-b', x: 30, y: 0, size: 16, level: 1 }
|
|
1829
|
+
],
|
|
1830
|
+
lines: [
|
|
1831
|
+
{ id: 'lf-l1', from: 'lf-a', to: 'lf-b', color: '#d9480f' },
|
|
1832
|
+
{ id: 'lf-l2', from: 'lf-a', to: 'lf-b', color: '#d9480f' }
|
|
1833
|
+
],
|
|
1834
|
+
channels: [{
|
|
1835
|
+
id: 'lf-ch', from: 'lf-a', to: 'lf-b', lineIds: ['lf-l1', 'lf-l2'],
|
|
1836
|
+
tangentInset: 0, offsetMode: 'inset', inset: 0.12,
|
|
1837
|
+
groups: [{ id: 'lf-g', lineIds: ['lf-l1', 'lf-l2'], path: [[0, 18]] }]
|
|
1838
|
+
}],
|
|
1839
|
+
lineFollowsNode: false,
|
|
1840
|
+
history: true,
|
|
1841
|
+
initialView: { center: [0, 0], scale: 1 },
|
|
1842
|
+
showGrid: false
|
|
1843
|
+
})
|
|
1844
|
+
await settle(120)
|
|
1845
|
+
const lfPaths = () => Object.fromEntries(lfView.listLines()
|
|
1846
|
+
.filter(line => String(line.item.id).startsWith('lf-'))
|
|
1847
|
+
.map(line => [String(line.item.id), JSON.stringify(line.path)]))
|
|
1848
|
+
const lfBase = lfPaths()
|
|
1849
|
+
lfView.setFilter({ points: { level: 0 } })
|
|
1850
|
+
await settle(120)
|
|
1851
|
+
const lfFiltered = lfPaths()
|
|
1852
|
+
check('lineFollowsNode:false:筛选未写 cascade 时只隐藏节点,回线几何不变', lfView.getVisibleData().points.length === 1 && lfView.listLines().filter(line => String(line.item.id).startsWith('lf-')).length === 2 && Object.keys(lfBase).every(id => lfFiltered[id] === lfBase[id]), JSON.stringify({ base: lfBase, current: lfFiltered }))
|
|
1853
|
+
lfView.updateOptions({ lineFollowsNode: true })
|
|
1854
|
+
await settle(120)
|
|
1855
|
+
check('lineFollowsNode:动态切到 true 后按默认 cascade 收起回线', lfView.listLines().filter(line => String(line.item.id).startsWith('lf-')).length === 0, JSON.stringify(lfPaths()))
|
|
1856
|
+
lfView.updateOptions({ lineFollowsNode: false })
|
|
1857
|
+
await settle(120)
|
|
1858
|
+
const lfToggledBack = lfPaths()
|
|
1859
|
+
check('lineFollowsNode:动态切回 false 后按节点最后坐标恢复回线', lfView.getVisibleData().points.length === 1 && Object.keys(lfBase).every(id => lfToggledBack[id] === lfBase[id]), JSON.stringify({ base: lfBase, current: lfToggledBack }))
|
|
1860
|
+
lfView.setFilter({ points: { level: 0 }, cascade: 'binding' })
|
|
1861
|
+
await settle(120)
|
|
1862
|
+
check('lineFollowsNode:false:筛选显式 cascade 仍然优先', lfView.listLines().filter(line => String(line.item.id).startsWith('lf-')).length === 0, JSON.stringify(lfPaths()))
|
|
1863
|
+
lfView.setFilter(null)
|
|
1864
|
+
await settle(120)
|
|
1865
|
+
lfView.hideFeatures('point', 'lf-a')
|
|
1866
|
+
await settle(120)
|
|
1867
|
+
const lfHidden = lfPaths()
|
|
1868
|
+
check('lineFollowsNode:false:visible:false 隐藏节点后回线停在最后坐标', lfView.listLines().filter(line => String(line.item.id).startsWith('lf-')).length === 2 && Object.keys(lfBase).every(id => lfHidden[id] === lfBase[id]), JSON.stringify({ base: lfBase, current: lfHidden }))
|
|
1869
|
+
lfView.showFeatures('point', 'lf-a')
|
|
1870
|
+
await settle(120)
|
|
1871
|
+
const lfRemoved = lfView.removePoints('lf-a')
|
|
1872
|
+
await settle(120)
|
|
1873
|
+
const lfAfterRemove = lfPaths()
|
|
1874
|
+
const lfChannelCount = lfView.getData().channels?.length ?? lfView.getChannels().length
|
|
1875
|
+
check('lineFollowsNode:false:删除节点不删回线 / 通道,几何继续停在最后坐标', lfRemoved.ok && lfRemoved.removedLines === 0 && lfRemoved.removedChannels === 0 && lfView.getData().points.length === 1 && lfView.getData().lines.length === 2 && lfChannelCount === 1 && Object.keys(lfBase).every(id => lfAfterRemove[id] === lfBase[id]), JSON.stringify({ removed: lfRemoved, paths: lfAfterRemove }))
|
|
1876
|
+
lfView.undo()
|
|
1877
|
+
await settle(120)
|
|
1878
|
+
check('lineFollowsNode:false:撤销删除后节点与原有几何一起回来', lfView.getData().points.length === 2 && lfView.getData().lines.length === 2 && Object.keys(lfBase).every(id => lfPaths()[id] === lfBase[id]), JSON.stringify({ base: lfBase, current: lfPaths(), points: lfView.getData().points }))
|
|
1879
|
+
lfView.destroy()
|
|
1880
|
+
lfHost.remove()
|
|
1881
|
+
|
|
1595
1882
|
/* 问题 2(续):图标节点走的永远是「整份重排」那一支 —— 跟随几何与宽度重排从前欠在分支之后,
|
|
1596
1883
|
节点换了大小 / 图标之后回线还铺在旧宽度上,要再点一下画布才追齐(同一份问题.txt 的再一轮)。
|
|
1597
1884
|
这两条用贴图符号(SVG,size 8 / 24)+ tangentInset: 0:间距就是节点外框本身 */
|
|
@@ -2767,13 +3054,12 @@
|
|
|
2767
3054
|
busView.destroy()
|
|
2768
3055
|
busHost.remove()
|
|
2769
3056
|
|
|
2770
|
-
/*
|
|
3057
|
+
/* 右键统一入口:节点 / 线路 / 通道都只走 onContextMenu,通道上下文挂在同一份载荷上 */
|
|
2771
3058
|
const rcHost = document.createElement('div')
|
|
2772
3059
|
rcHost.style.cssText = 'width:420px;height:300px'
|
|
2773
3060
|
document.body.appendChild(rcHost)
|
|
2774
3061
|
const rcBuilt = createChannels([{ id: 'ch-rc', from: 'rc-n1', to: 'rc-n2', lineIds: ['rc-1', 'rc-2'] }])
|
|
2775
3062
|
const rcHits = []
|
|
2776
|
-
const rcChannels = []
|
|
2777
3063
|
const rcView = createGisView(rcHost, {
|
|
2778
3064
|
points: [
|
|
2779
3065
|
{ id: 'rc-n1', x: 0, y: 0, name: '甲站', size: 20 },
|
|
@@ -2783,11 +3069,9 @@
|
|
|
2783
3069
|
channels: rcBuilt.channels,
|
|
2784
3070
|
fitPadding: 60,
|
|
2785
3071
|
showGrid: false,
|
|
2786
|
-
//
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
onContextMenu: payload => rcHits.push(payload),
|
|
2790
|
-
onChannelContextMenu: payload => rcChannels.push(payload)
|
|
3072
|
+
// 右键回调本体是 contextMenu;通道不再需要第二个功能开关
|
|
3073
|
+
features: { contextMenu: true },
|
|
3074
|
+
onContextMenu: payload => rcHits.push(payload)
|
|
2791
3075
|
})
|
|
2792
3076
|
await settle(140)
|
|
2793
3077
|
const rcRightClick = async (screen) => {
|
|
@@ -2802,20 +3086,195 @@
|
|
|
2802
3086
|
}))
|
|
2803
3087
|
await settle(60)
|
|
2804
3088
|
}
|
|
2805
|
-
//
|
|
3089
|
+
// 端点上右键:这个位置同时压着节点与回线,拾取按「点高于线」给节点,不附通道上下文
|
|
2806
3090
|
await rcRightClick(rcView.project(0, 0))
|
|
2807
3091
|
check('右键落在通道端点(节点压着回线):onContextMenu 给的是节点', rcHits[0]?.type === 'point' && rcHits[0]?.item?.id === 'rc-n1', JSON.stringify({ type: rcHits[0]?.type, id: rcHits[0]?.item?.id }))
|
|
2808
|
-
check('
|
|
3092
|
+
check('右键落在节点上:只发一条 onContextMenu,且没有 channelContext', rcHits.length === 1 && rcHits[0]?.channelContext === undefined, JSON.stringify({ count: rcHits.length, channelContext: rcHits[0]?.channelContext ?? null }))
|
|
2809
3093
|
// 回线中段(离两端节点都远):这里才是「通道右键」
|
|
2810
3094
|
const rcLine = rcView.getData().lines.find(item => String(item.id) === 'rc-1')
|
|
2811
3095
|
const rcMidWorld = Array.isArray(rcLine?.path) && rcLine.path.length > 1 ? rcLine.path[Math.floor(rcLine.path.length / 2)] : [60, 0]
|
|
2812
3096
|
diag('右键回线中段', `世界 ${rcMidWorld.map(value => Number(value).toFixed(2)).join(',')} · 回线折点 ${Array.isArray(rcLine?.path) ? rcLine.path.length : 0}`)
|
|
2813
3097
|
await rcRightClick(rcView.project(rcMidWorld[0], rcMidWorld[1]))
|
|
2814
|
-
|
|
2815
|
-
check('
|
|
3098
|
+
const rcContext = rcHits[1]?.channelContext
|
|
3099
|
+
check('右键落在回线中段:同一份 onContextMenu 载荷里带 channelContext(组数 / 当前分组 / 可用交互)', rcHits.length === 2 && rcContext?.channelId === 'ch-rc' && rcHits[1]?.lineId === 'rc-1' && rcContext?.groupCount === 1 && (rcContext?.actions ?? []).length > 0, JSON.stringify({ count: rcHits.length, channel: rcContext?.channelId, line: rcHits[1]?.lineId, groups: rcContext?.groupCount }))
|
|
3100
|
+
check('右键通道只发这一条(不再额外发 onChannelContextMenu),线路下标 / id 也在载荷上', rcHits[1]?.type === 'line' && rcHits[1]?.channelId === 'ch-rc' && rcHits[1]?.lineId === 'rc-1' && Number.isInteger(rcHits[1]?.lineIndex) && rcHits[1]?.action?.target === 'channel', JSON.stringify({ count: rcHits.length, type: rcHits[1]?.type, lineIndex: rcHits[1]?.lineIndex, target: rcHits[1]?.action?.target }))
|
|
2816
3101
|
rcView.destroy()
|
|
2817
3102
|
rcHost.remove()
|
|
2818
3103
|
|
|
3104
|
+
/* 点击级联高亮:节点亮自身 + 直连线路 / 节点,线路 / 通道亮两端;与选中集完全分开 */
|
|
3105
|
+
const casHost = document.createElement('div')
|
|
3106
|
+
casHost.style.cssText = 'width:520px;height:320px'
|
|
3107
|
+
document.body.appendChild(casHost)
|
|
3108
|
+
const casBuilt = createChannels([{ id: 'cas-ch', from: 'cas-b', to: 'cas-c', circuits: 2, lineIds: ['cas-c1', 'cas-c2'] }])
|
|
3109
|
+
const casEvents = []
|
|
3110
|
+
const casBusEvents = []
|
|
3111
|
+
const casView = createGisView(casHost, {
|
|
3112
|
+
points: [
|
|
3113
|
+
{ id: 'cas-a', x: 0, y: 0, size: 14 },
|
|
3114
|
+
{ id: 'cas-b', x: 120, y: 0, size: 14 },
|
|
3115
|
+
{ id: 'cas-c', x: 240, y: 0, size: 14 },
|
|
3116
|
+
{ id: 'cas-d', x: 60, y: 90, size: 14 }
|
|
3117
|
+
],
|
|
3118
|
+
lines: [
|
|
3119
|
+
{ id: 'cas-ab', from: 'cas-a', to: 'cas-b', path: [[0, 0], [120, 0]] },
|
|
3120
|
+
...casBuilt.lines
|
|
3121
|
+
],
|
|
3122
|
+
channels: casBuilt.channels,
|
|
3123
|
+
fitPadding: 60,
|
|
3124
|
+
showGrid: false,
|
|
3125
|
+
cascadeHighlight: true,
|
|
3126
|
+
// 级联高亮本身不依赖 select;这里打开选中,用来验证两套状态互不污染
|
|
3127
|
+
features: { select: true, contextMenu: true },
|
|
3128
|
+
onCascadeHighlight: payload => casEvents.push(payload)
|
|
3129
|
+
})
|
|
3130
|
+
casView.on('cascadeHighlight', payload => casBusEvents.push(payload))
|
|
3131
|
+
await settle(140)
|
|
3132
|
+
const casClick = async (screen, extra = {}) => {
|
|
3133
|
+
cmSend(casView, 'pointerdown', screen, { buttons: 1, ...extra })
|
|
3134
|
+
cmSend(casView, 'pointerup', screen, { buttons: 0, button: 0, ...extra })
|
|
3135
|
+
await settle(60)
|
|
3136
|
+
}
|
|
3137
|
+
await casClick(casView.project(0, 0))
|
|
3138
|
+
const casPointInfo = casView.getCascadeHighlight()
|
|
3139
|
+
check('级联高亮:点节点亮自身与直连线路 / 另一端节点(默认 both)',
|
|
3140
|
+
casPointInfo?.origin?.target === 'point' && casPointInfo?.origin?.item?.id === 'cas-a' &&
|
|
3141
|
+
casPointInfo.points.map(entry => entry.item.id).sort().join(',') === 'cas-a,cas-b' &&
|
|
3142
|
+
casPointInfo.lines.map(entry => entry.item.id).join(',') === 'cas-ab',
|
|
3143
|
+
JSON.stringify({ origin: casPointInfo?.origin?.item?.id, points: casPointInfo?.points?.map(entry => entry.item.id), lines: casPointInfo?.lines?.map(entry => entry.item.id) }))
|
|
3144
|
+
check('级联高亮不写选中集:亮两个节点与一条线路时选中集仍只是这次点中的节点',
|
|
3145
|
+
casPointInfo?.pointIndexes?.length === 2 && casPointInfo?.lineIndexes?.length === 1 && casView.getSelectionData().count === 1,
|
|
3146
|
+
JSON.stringify({ cascade: [casPointInfo?.pointIndexes, casPointInfo?.lineIndexes], selected: casView.getSelectionData().count }))
|
|
3147
|
+
check('级联高亮:onCascadeHighlight 与 cascadeHighlight 事件都收到同一份读数',
|
|
3148
|
+
casEvents.length === 1 && casBusEvents.length === 1 && casEvents[0]?.origin?.item?.id === 'cas-a' && casBusEvents[0]?.pointIndexes?.length === 2,
|
|
3149
|
+
JSON.stringify({ callback: casEvents.length, event: casBusEvents.length }))
|
|
3150
|
+
|
|
3151
|
+
casEvents.length = 0
|
|
3152
|
+
casBusEvents.length = 0
|
|
3153
|
+
await casClick(casView.project(60, 0))
|
|
3154
|
+
const casLineInfo = casView.getCascadeHighlight()
|
|
3155
|
+
check('级联高亮:点线路只亮两端节点(不把线路自身算进去)',
|
|
3156
|
+
casLineInfo?.origin?.target === 'line' && casLineInfo?.origin?.item?.id === 'cas-ab' &&
|
|
3157
|
+
casLineInfo.points.map(entry => entry.item.id).sort().join(',') === 'cas-a,cas-b' && casLineInfo.lines.length === 0 && casEvents.length === 1,
|
|
3158
|
+
JSON.stringify({ origin: casLineInfo?.origin?.item?.id, points: casLineInfo?.points?.map(entry => entry.item.id), lines: casLineInfo?.lines?.length }))
|
|
3159
|
+
|
|
3160
|
+
casEvents.length = 0
|
|
3161
|
+
const casChannelGeometry = casView.getLine('cas-c1')?.path ?? [[120, 0], [240, 0]]
|
|
3162
|
+
const casChannelStart = casChannelGeometry[0]
|
|
3163
|
+
const casChannelEnd = casChannelGeometry[casChannelGeometry.length - 1]
|
|
3164
|
+
await casClick(casView.project((casChannelStart[0] + casChannelEnd[0]) / 2, (casChannelStart[1] + casChannelEnd[1]) / 2))
|
|
3165
|
+
const casChannelInfo = casView.getCascadeHighlight()
|
|
3166
|
+
check('级联高亮:点通道回线亮两端节点,origin 记通道 / 分组(target 为 channel)',
|
|
3167
|
+
casChannelInfo?.origin?.target === 'channel' && casChannelInfo?.origin?.channelId === 'cas-ch' &&
|
|
3168
|
+
casChannelInfo.points.map(entry => entry.item.id).sort().join(',') === 'cas-b,cas-c' && casChannelInfo.lines.length === 0 && casEvents.length === 1,
|
|
3169
|
+
JSON.stringify({ origin: casChannelInfo?.origin, points: casChannelInfo?.points?.map(entry => entry.item.id) }))
|
|
3170
|
+
|
|
3171
|
+
casEvents.length = 0
|
|
3172
|
+
await casClick([4, 4])
|
|
3173
|
+
check('级联高亮:点空白处收掉,getCascadeHighlight 返回 null 并发一条 null',
|
|
3174
|
+
casView.getCascadeHighlight() === null && casEvents.length === 1 && casEvents[0] === null,
|
|
3175
|
+
JSON.stringify({ info: casView.getCascadeHighlight(), events: casEvents.length }))
|
|
3176
|
+
|
|
3177
|
+
casView.updateOptions({ cascadeHighlight: { scope: 'points' } })
|
|
3178
|
+
await settle(60)
|
|
3179
|
+
casEvents.length = 0
|
|
3180
|
+
await casClick(casView.project(240, 0))
|
|
3181
|
+
const casPointsOnly = casView.getCascadeHighlight()
|
|
3182
|
+
check('级联高亮 scope: points:点节点只亮节点,不亮直连线路',
|
|
3183
|
+
casPointsOnly?.scope === 'points' && casPointsOnly.points.map(entry => entry.item.id).sort().join(',') === 'cas-b,cas-c' && casPointsOnly.lines.length === 0,
|
|
3184
|
+
JSON.stringify({ scope: casPointsOnly?.scope, points: casPointsOnly?.points?.map(entry => entry.item.id), lines: casPointsOnly?.lines?.length }))
|
|
3185
|
+
|
|
3186
|
+
casView.updateOptions({
|
|
3187
|
+
pointSelectStyle: { color: '#00cc44', strokeColor: '#006622', strokeWidth: 1, sizeScale: 1 },
|
|
3188
|
+
cascadeHighlight: {
|
|
3189
|
+
scope: 'both',
|
|
3190
|
+
pointStyle: { color: '#112233', strokeColor: '#445566', strokeWidth: 3, sizeScale: 1.4 },
|
|
3191
|
+
lineStyle: { color: '#778899', width: 7, opacity: 1 }
|
|
3192
|
+
}
|
|
3193
|
+
})
|
|
3194
|
+
await settle(60)
|
|
3195
|
+
const casPointPaint = (snapshot, id) => {
|
|
3196
|
+
const item = casView.getData().points.find(point => point.id === id)
|
|
3197
|
+
const [x, y] = casView.project(item.x, item.y)
|
|
3198
|
+
return [...snapshot.points, ...snapshot.raised].find(point => Math.hypot(point.x - x, point.y - y) < 3)
|
|
3199
|
+
}
|
|
3200
|
+
casEvents.length = 0
|
|
3201
|
+
await casClick(casView.project(0, 0))
|
|
3202
|
+
const casStyledSnapshot = casView.snapshotScene({ background: false })
|
|
3203
|
+
const casSelectedPaint = casPointPaint(casStyledSnapshot, 'cas-a')
|
|
3204
|
+
const casCascadePaint = casPointPaint(casStyledSnapshot, 'cas-b')
|
|
3205
|
+
check('级联高亮样式可单独配置:邻接节点与线路按 pointStyle / lineStyle 解析到画面',
|
|
3206
|
+
casCascadePaint?.fill === 'rgba(17,34,51,1)' &&
|
|
3207
|
+
casStyledSnapshot.lines.some(line => line.color === 'rgba(119,136,153,1)' && Math.abs(line.width - 7) < 0.2),
|
|
3208
|
+
JSON.stringify({ points: [...casStyledSnapshot.points, ...casStyledSnapshot.raised].map(point => point.fill), lines: casStyledSnapshot.lines.map(line => [line.color, line.width]) }))
|
|
3209
|
+
check('级联高亮:直接选中的节点仍走 pointSelectStyle,不套级联样式',
|
|
3210
|
+
casSelectedPaint?.fill === 'rgba(0,204,68,1)' && casCascadePaint?.fill !== casSelectedPaint?.fill,
|
|
3211
|
+
JSON.stringify({ selected: casSelectedPaint, cascade: casCascadePaint }))
|
|
3212
|
+
|
|
3213
|
+
casView.updateOptions({ pointSelectStyle: null })
|
|
3214
|
+
await settle(40)
|
|
3215
|
+
await casClick(casView.project(240, 0))
|
|
3216
|
+
const casNoSelectPaint = casPointPaint(casView.snapshotScene({ background: false }), 'cas-c')
|
|
3217
|
+
check('没配选中样式时直接选中项也不回落到级联样式(保持普通外观)',
|
|
3218
|
+
casNoSelectPaint?.fill !== 'rgba(17,34,51,1)' && JSON.stringify(casView.getSelectionData().ids) === JSON.stringify(['cas-c']),
|
|
3219
|
+
JSON.stringify({ paint: casNoSelectPaint, selected: casView.getSelectionData().ids }))
|
|
3220
|
+
|
|
3221
|
+
// 普通单击 / 多选分开控制:关闭的那一路只改选中,并把旧级联收掉;另一路照常触发
|
|
3222
|
+
casView.updateOptions({ cascadeHighlight: { scope: 'both', triggerOnClick: false, triggerOnMultiSelect: true } })
|
|
3223
|
+
await settle(40)
|
|
3224
|
+
await casClick(casView.project(0, 0))
|
|
3225
|
+
check('triggerOnClick: false:普通单击不触发级联', casView.getCascadeHighlight() === null, JSON.stringify(casView.getCascadeHighlight()))
|
|
3226
|
+
await casClick(casView.project(240, 0), { ctrlKey: true })
|
|
3227
|
+
check('triggerOnMultiSelect: true:Ctrl 多选仍触发级联', casView.getCascadeHighlight()?.origin?.item?.id === 'cas-c' && casView.getCascadeHighlight()?.pointIndexes?.length === 2, JSON.stringify({ origin: casView.getCascadeHighlight()?.origin?.item?.id, points: casView.getCascadeHighlight()?.pointIndexes }))
|
|
3228
|
+
casView.updateOptions({ cascadeHighlight: { scope: 'both', triggerOnClick: true, triggerOnMultiSelect: false } })
|
|
3229
|
+
await settle(40)
|
|
3230
|
+
await casClick(casView.project(120, 0))
|
|
3231
|
+
const casClickOn = casView.getCascadeHighlight()
|
|
3232
|
+
await casClick(casView.project(60, 90), { ctrlKey: true })
|
|
3233
|
+
check('triggerOnMultiSelect: false:普通单击照常触发,Ctrl 多选不再更新而是收掉旧级联',
|
|
3234
|
+
casClickOn?.origin?.item?.id === 'cas-b' && casView.getCascadeHighlight() === null,
|
|
3235
|
+
JSON.stringify({ click: casClickOn?.origin?.item?.id, multi: casView.getCascadeHighlight() }))
|
|
3236
|
+
|
|
3237
|
+
// 框选:默认不触发;打开后整框邻接并集参与级联,直接框中的节点仍走 boxSelectStyle
|
|
3238
|
+
casView.updateOptions({
|
|
3239
|
+
pointBoxSelectStyle: { color: '#00ccff', strokeColor: '#005577', strokeWidth: 1, sizeScale: 1 },
|
|
3240
|
+
cascadeHighlight: { scope: 'both', triggerOnBoxSelect: true }
|
|
3241
|
+
})
|
|
3242
|
+
await settle(40)
|
|
3243
|
+
const casBoxA = casView.project(0, 0)
|
|
3244
|
+
const casBoxB = casView.project(120, 0)
|
|
3245
|
+
const casBox = {
|
|
3246
|
+
x: Math.min(casBoxA[0], casBoxB[0]) - 30,
|
|
3247
|
+
y: Math.min(casBoxA[1], casBoxB[1]) - 30,
|
|
3248
|
+
width: Math.abs(casBoxA[0] - casBoxB[0]) + 60,
|
|
3249
|
+
height: Math.abs(casBoxA[1] - casBoxB[1]) + 60
|
|
3250
|
+
}
|
|
3251
|
+
casView.selectInBox(casBox, { selectionKinds: ['point'] })
|
|
3252
|
+
await settle(40)
|
|
3253
|
+
const casBoxInfo = casView.getCascadeHighlight()
|
|
3254
|
+
const casBoxSnapshot = casView.snapshotScene({ background: false })
|
|
3255
|
+
const casBoxPaint = casPointPaint(casBoxSnapshot, 'cas-a')
|
|
3256
|
+
const casBoxNeighborPaint = casPointPaint(casBoxSnapshot, 'cas-c')
|
|
3257
|
+
check('triggerOnBoxSelect: true:框内所有命中项一起算邻接并集',
|
|
3258
|
+
casBoxInfo?.pointIndexes?.length === 3 && casBoxInfo?.origin?.item?.id === 'cas-a' && casBoxInfo.lineIndexes.length >= 3,
|
|
3259
|
+
JSON.stringify({ origin: casBoxInfo?.origin?.item?.id, points: casBoxInfo?.pointIndexes, lines: casBoxInfo?.lineIndexes }))
|
|
3260
|
+
check('框选直接命中的节点走 boxSelectStyle,只有框外邻接节点套级联样式',
|
|
3261
|
+
casBoxPaint?.fill === 'rgba(0,204,255,1)' && casBoxNeighborPaint?.fill === 'rgba(255,247,237,1)',
|
|
3262
|
+
JSON.stringify({ boxed: casBoxPaint, neighbor: casBoxNeighborPaint }))
|
|
3263
|
+
casView.updateOptions({ cascadeHighlight: { scope: 'both', triggerOnBoxSelect: false } })
|
|
3264
|
+
await settle(40)
|
|
3265
|
+
const casBoxD = casView.project(60, 90)
|
|
3266
|
+
casView.selectInBox({ x: casBoxD[0] - 30, y: casBoxD[1] - 30, width: 60, height: 60 }, { selectionKinds: ['point'] })
|
|
3267
|
+
check('triggerOnBoxSelect: false(缺省):框选只改选中集并收掉旧级联', casView.getCascadeHighlight() === null, JSON.stringify(casView.getCascadeHighlight()))
|
|
3268
|
+
|
|
3269
|
+
casEvents.length = 0
|
|
3270
|
+
casView.updateOptions({ cascadeHighlight: false })
|
|
3271
|
+
await settle(60)
|
|
3272
|
+
check('关掉级联高亮:当前读数与事件都收成 null',
|
|
3273
|
+
casView.getCascadeHighlight() === null && casEvents.length === 1 && casEvents[0] === null,
|
|
3274
|
+
JSON.stringify({ info: casView.getCascadeHighlight(), events: casEvents.length }))
|
|
3275
|
+
casView.destroy()
|
|
3276
|
+
casHost.remove()
|
|
3277
|
+
|
|
2819
3278
|
/* 导出选中的节点:图片 / 数据二选一,线路按「两端都在选中集里」决定带不带 */
|
|
2820
3279
|
const exHost = document.createElement('div')
|
|
2821
3280
|
exHost.style.cssText = 'width:420px;height:300px'
|
|
@@ -2925,6 +3384,67 @@
|
|
|
2925
3384
|
svgView.destroy()
|
|
2926
3385
|
svgHost.remove()
|
|
2927
3386
|
|
|
3387
|
+
/* exportImage 的范围:viewport 只导当前可视区域;all 临时适应全部内容,出完恢复相机 */
|
|
3388
|
+
const scopeHost = document.createElement('div')
|
|
3389
|
+
scopeHost.style.cssText = 'width:360px;height:240px'
|
|
3390
|
+
document.body.appendChild(scopeHost)
|
|
3391
|
+
const scopeView = createGisView(scopeHost, {
|
|
3392
|
+
points: [
|
|
3393
|
+
{ id: 'scope-a', x: 0, y: 0, size: 40, color: '#ff0000' },
|
|
3394
|
+
{ id: 'scope-b', x: 1000, y: 0, size: 40, color: '#0000ff' }
|
|
3395
|
+
],
|
|
3396
|
+
background: { color: '#ffffff' },
|
|
3397
|
+
showGrid: false
|
|
3398
|
+
})
|
|
3399
|
+
await settle(140)
|
|
3400
|
+
scopeView.setCamera({ center: [0, 0], scale: 6 })
|
|
3401
|
+
await settle(60)
|
|
3402
|
+
const scopeCamera = scopeView.getCamera()
|
|
3403
|
+
const scopeColorCounts = async (url) => {
|
|
3404
|
+
const image = new Image()
|
|
3405
|
+
await new Promise((resolve, reject) => {
|
|
3406
|
+
image.onload = resolve
|
|
3407
|
+
image.onerror = () => reject(new Error('导出图片加载失败'))
|
|
3408
|
+
image.src = url
|
|
3409
|
+
})
|
|
3410
|
+
const buffer = document.createElement('canvas')
|
|
3411
|
+
buffer.width = image.width
|
|
3412
|
+
buffer.height = image.height
|
|
3413
|
+
const context = buffer.getContext('2d')
|
|
3414
|
+
context.drawImage(image, 0, 0)
|
|
3415
|
+
const data = context.getImageData(0, 0, buffer.width, buffer.height).data
|
|
3416
|
+
let red = 0
|
|
3417
|
+
let blue = 0
|
|
3418
|
+
for (let index = 0; index < data.length; index += 4) {
|
|
3419
|
+
if (data[index] > 180 && data[index + 1] < 90 && data[index + 2] < 90) {
|
|
3420
|
+
red += 1
|
|
3421
|
+
}
|
|
3422
|
+
if (data[index + 2] > 180 && data[index] < 90 && data[index + 1] < 90) {
|
|
3423
|
+
blue += 1
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
return { red, blue, width: buffer.width, height: buffer.height }
|
|
3427
|
+
}
|
|
3428
|
+
const scopeViewport = await scopeColorCounts(await scopeView.exportImage({ scope: 'viewport', scale: 1, includeLabels: false }))
|
|
3429
|
+
const scopeAll = await scopeColorCounts(await scopeView.exportImage({ scope: 'all', padding: 16, scale: 1, includeLabels: false }))
|
|
3430
|
+
const scopeAfter = scopeView.getCamera()
|
|
3431
|
+
check('exportImage scope: viewport 只含当前视口内容', scopeViewport.red > 50 && scopeViewport.blue === 0, JSON.stringify(scopeViewport))
|
|
3432
|
+
check('exportImage scope: all 临时适应全部内容(两个节点都进画面)', scopeAll.red > 50 && scopeAll.blue > 50, JSON.stringify(scopeAll))
|
|
3433
|
+
check('exportImage scope: all 出图后相机原样恢复', scopeAfter.scale === scopeCamera.scale && scopeAfter.center[0] === scopeCamera.center[0] && scopeAfter.center[1] === scopeCamera.center[1], JSON.stringify({ before: scopeCamera, after: scopeAfter }))
|
|
3434
|
+
const scopeSvg = decodeSvgUrl(await scopeView.exportImage({ type: 'svg', scope: 'all', padding: 12, includeLabels: false }))
|
|
3435
|
+
const scopeSvgAfter = scopeView.getCamera()
|
|
3436
|
+
check('exportImage scope: all 在 SVG 分支同样生效并恢复相机', (scopeSvg.match(/<circle/g) || []).length >= 2 && scopeSvgAfter.scale === scopeCamera.scale, JSON.stringify({ circles: (scopeSvg.match(/<circle/g) || []).length, camera: scopeSvgAfter }))
|
|
3437
|
+
let scopeError = ''
|
|
3438
|
+
try {
|
|
3439
|
+
await scopeView.exportImage({ scope: 'window' })
|
|
3440
|
+
}
|
|
3441
|
+
catch (error) {
|
|
3442
|
+
scopeError = String(error?.message ?? error)
|
|
3443
|
+
}
|
|
3444
|
+
check('exportImage scope 写错直接抛异常(不静默按 viewport 导)', scopeError.includes('viewport') && scopeError.includes('all'), scopeError)
|
|
3445
|
+
scopeView.destroy()
|
|
3446
|
+
scopeHost.remove()
|
|
3447
|
+
|
|
2928
3448
|
/* 适应视图的留白:padding 比画布还大时,扣完留白的可用尺寸不能被钳到 1px(那会把整幅图缩成一个点) */
|
|
2929
3449
|
const fitHost = document.createElement('div')
|
|
2930
3450
|
fitHost.style.cssText = 'width:220px;height:140px'
|
|
@@ -2935,13 +3455,13 @@
|
|
|
2935
3455
|
showGrid: false
|
|
2936
3456
|
})
|
|
2937
3457
|
await settle(160)
|
|
2938
|
-
fitView.focusOn('fl1', { padding: 220 })
|
|
3458
|
+
fitView.focusOn('fl1', { zoom: true, padding: 220 })
|
|
2939
3459
|
await settle(80)
|
|
2940
3460
|
const fitStart = fitView.project(0, 0)
|
|
2941
3461
|
const fitEnd = fitView.project(300, 200)
|
|
2942
3462
|
const fitSpan = Math.hypot(fitStart[0] - fitEnd[0], fitStart[1] - fitEnd[1])
|
|
2943
3463
|
check('适应视图的留白比画布还大时:内容不会被挤成一个点(至少留给它 48px)', fitSpan > 40, `跨度 ${fitSpan.toFixed(1)}px / 画布 ${fitView.getStats().cssWidth}×${fitView.getStats().cssHeight}`)
|
|
2944
|
-
fitView.focusOn('fl1', { padding: 20 })
|
|
3464
|
+
fitView.focusOn('fl1', { zoom: true, padding: 20 })
|
|
2945
3465
|
await settle(80)
|
|
2946
3466
|
const fitPadStart = fitView.project(0, 0)
|
|
2947
3467
|
const fitPadEnd = fitView.project(300, 200)
|
|
@@ -3451,13 +3971,17 @@
|
|
|
3451
3971
|
apiSend('pointerup', [apiRight, apiBottom], { button: 0, buttons: 0, shiftKey: true })
|
|
3452
3972
|
await settle()
|
|
3453
3973
|
check('框选的结果用同一份读数读回来(Shift + 拖动)', apiIds() === 'api-a,api-b,api-c' && apiView.getSelectionData().count === 3, apiIds())
|
|
3454
|
-
// focusOn
|
|
3974
|
+
// focusOn 一批目标:缺省只居中;显式 zoom: true 才按整体包围盒适配
|
|
3975
|
+
const apiDefaultScale = apiView.getCamera().scale
|
|
3455
3976
|
apiView.focusOn(['api-a', 'api-b', 'api-c'])
|
|
3977
|
+
const apiCenteredBatch = apiView.getCamera()
|
|
3978
|
+
check('focusOn(一批目标):缺省只把中心挪到 (200, 20),比例一点不动', Math.abs(apiCenteredBatch.center[0] - 200) < 1e-6 && Math.abs(apiCenteredBatch.center[1] - 20) < 1e-6 && apiCenteredBatch.scale === apiDefaultScale, JSON.stringify({ scale: [apiDefaultScale, apiCenteredBatch.scale], center: apiCenteredBatch.center }))
|
|
3979
|
+
apiView.focusOn(['api-a', 'api-b', 'api-c'], { zoom: true })
|
|
3456
3980
|
const apiBatch = apiView.getCamera()
|
|
3457
3981
|
const apiStats = apiView.getStats()
|
|
3458
3982
|
const apiCorners = [[0, 0], [400, 40]].map(spot => apiView.project(spot[0], spot[1]))
|
|
3459
3983
|
const apiInside = apiCorners.every(spot => spot[0] >= 0 && spot[0] <= apiStats.cssWidth && spot[1] >= 0 && spot[1] <= apiStats.cssHeight)
|
|
3460
|
-
check('focusOn(
|
|
3984
|
+
check('focusOn(一批目标, { zoom: true }):整体包围盒居中,三个点都在视口里', Math.abs(apiBatch.center[0] - 200) < 1e-6 && Math.abs(apiBatch.center[1] - 20) < 1e-6 && apiBatch.scale > 0 && apiInside, JSON.stringify({ center: apiBatch.center, corners: apiCorners }))
|
|
3461
3985
|
// center: true 是原来 centerOn 的语义:只挪中心,比例一点不动
|
|
3462
3986
|
apiView.focusOn(['api-a', 'api-b'], { center: true })
|
|
3463
3987
|
const apiCenter1 = apiView.getCamera()
|
|
@@ -3468,9 +3992,11 @@
|
|
|
3468
3992
|
apiView.focusOn('api-b', { center: true })
|
|
3469
3993
|
const apiCenter3 = apiView.getCamera().center
|
|
3470
3994
|
check('focusOn(id, { center: true }):中心挪到这个要素上', Math.abs(apiCenter3[0] - 200) < 1e-6 && Math.abs(apiCenter3[1] - 40) < 1e-6, JSON.stringify(apiCenter3))
|
|
3471
|
-
apiView.
|
|
3472
|
-
|
|
3473
|
-
|
|
3995
|
+
const apiChannelScale = apiView.getCamera().scale
|
|
3996
|
+
apiView.focusOn({ type: 'channel', channelId: 'api-ch' })
|
|
3997
|
+
const apiChannelCamera = apiView.getCamera()
|
|
3998
|
+
const apiCenter4 = apiChannelCamera.center
|
|
3999
|
+
check('focusOn(通道写法) 缺省只居中,线路 / 通道不再自动放大', Math.abs(apiCenter4[0] - 100) < 5 && Math.abs(apiCenter4[1] - 20) < 5 && apiChannelCamera.scale === apiChannelScale, JSON.stringify({ center: apiCenter4, scale: [apiChannelScale, apiChannelCamera.scale] }))
|
|
3474
4000
|
apiView.focusOn('没有这个 id', { center: true })
|
|
3475
4001
|
const apiCenter5 = apiView.getCamera().center
|
|
3476
4002
|
check('focusOn:认不出来的目标什么都不做(视角停在原处)', apiCenter5[0] === apiCenter4[0] && apiCenter5[1] === apiCenter4[1], JSON.stringify(apiCenter5))
|
|
@@ -3479,16 +4005,41 @@
|
|
|
3479
4005
|
check('focusOn(一段范围, { center: true }):取中心', Math.abs(apiCenter6[0] - 200) < 1e-6 && Math.abs(apiCenter6[1] - 20) < 1e-6, JSON.stringify(apiCenter6))
|
|
3480
4006
|
// select: true:被聚焦的要素同时变成选中状态(与接口选中同一份 state);mode 决定怎么并进当前选中集
|
|
3481
4007
|
const apiBeforeFocusSelect = apiView.getSelectionData().ids.join(',')
|
|
3482
|
-
apiView.focusOn(['api-a', 'api-b', 'api-c'], { select: true, padding: 20 })
|
|
4008
|
+
apiView.focusOn(['api-a', 'api-b', 'api-c'], { select: true, zoom: true, padding: 20 })
|
|
3483
4009
|
check('focusOn(一批, { select: true }):被聚焦的这一批同时选中(整份选中集换成这三个)', apiView.getSelectionData().ids.join(',') === 'api-a,api-b,api-c' && apiBeforeFocusSelect === 'api-a,api-b,api-c' && apiView.getSelectionData().count === 3, `${apiBeforeFocusSelect} -> ${apiView.getSelectionData().ids.join(',')}`)
|
|
3484
4010
|
apiView.focusOn('api-a', { select: true, mode: 'remove' })
|
|
3485
4011
|
check("focusOn(..., { select: true, mode: 'remove' }):只把聚焦的这个从选中集里去掉", apiView.getSelectionData().ids.join(',') === 'api-b,api-c', apiView.getSelectionData().ids.join(','))
|
|
3486
4012
|
apiView.selectFeatures(null)
|
|
3487
4013
|
const apiAreaScale = apiView.getCamera().scale
|
|
3488
|
-
apiView.focusOn('api-b', { maxZoom: 12 })
|
|
4014
|
+
apiView.focusOn('api-b', { zoom: true, maxZoom: 12 })
|
|
3489
4015
|
const apiZoomCamera = apiView.getCamera()
|
|
3490
|
-
check('focusOn(单个节点, { maxZoom }):零面积目标撑出最小范围,放大并居中', apiZoomCamera.scale > apiAreaScale && Math.abs(apiZoomCamera.center[0] - 200) < 1e-6 && Math.abs(apiZoomCamera.center[1] - 40) < 1e-6, JSON.stringify({ scale: apiZoomCamera.scale, center: apiZoomCamera.center }))
|
|
4016
|
+
check('focusOn(单个节点, { zoom: true, maxZoom }):零面积目标撑出最小范围,放大并居中', apiZoomCamera.scale > apiAreaScale && Math.abs(apiZoomCamera.center[0] - 200) < 1e-6 && Math.abs(apiZoomCamera.center[1] - 40) < 1e-6, JSON.stringify({ scale: apiZoomCamera.scale, center: apiZoomCamera.center }))
|
|
4017
|
+
// 实例命令:注册 / 执行 / 同名覆盖 / 幂等卸载;名字与 handler 非法时只提醒、不动注册表
|
|
4018
|
+
let commandPayload = null
|
|
4019
|
+
let commandContext = null
|
|
4020
|
+
const disposeEcho = apiView.registerCommand(' demo:echo ', (payload, context) => {
|
|
4021
|
+
commandPayload = payload
|
|
4022
|
+
commandContext = context
|
|
4023
|
+
return { doubled: Number(payload?.value ?? 0) * 2 }
|
|
4024
|
+
})
|
|
4025
|
+
const echoResult = apiView.executeCommand('demo:echo', { value: 21 })
|
|
4026
|
+
check('registerCommand / executeCommand:名字规范化、payload 与冻结上下文可用,handler 的返回值原样带回', apiView.listCommands().join(',') === 'demo:echo' && echoResult.ok === true && echoResult.name === 'demo:echo' && echoResult.value?.doubled === 42 && commandPayload?.value === 21 && commandContext?.view === apiView && commandContext?.name === 'demo:echo' && Object.isFrozen(commandContext), JSON.stringify({ commands: apiView.listCommands(), result: echoResult, context: commandContext?.name }))
|
|
4027
|
+
const disposeReplacement = apiView.registerCommand('demo:echo', payload => `replacement:${payload}`)
|
|
4028
|
+
disposeEcho()
|
|
4029
|
+
const replacementResult = apiView.executeCommand('demo:echo', 'still-here')
|
|
4030
|
+
check('registerCommand:同名后注册覆盖先生效,旧卸载函数不会误删新命令', apiView.listCommands().join(',') === 'demo:echo' && replacementResult.ok === true && replacementResult.value === 'replacement:still-here', JSON.stringify({ commands: apiView.listCommands(), result: replacementResult }))
|
|
4031
|
+
disposeReplacement()
|
|
4032
|
+
const invalidDispose = apiView.registerCommand(' ', () => 'never')
|
|
4033
|
+
const invalidResult = apiView.executeCommand('', {})
|
|
4034
|
+
const missingResult = apiView.executeCommand('demo:missing', {})
|
|
4035
|
+
check('实例命令:非法名 / 未注册 / 空名字都返回 ok: false,注册表不受影响', typeof invalidDispose === 'function' && invalidResult.ok === false && missingResult.ok === false && apiView.listCommands().length === 0, JSON.stringify({ invalid: invalidResult, missing: missingResult, commands: apiView.listCommands() }))
|
|
4036
|
+
apiView.registerCommand('demo:throw', () => { throw new Error('命令执行失败') })
|
|
4037
|
+
const thrownResult = apiView.executeCommand('demo:throw', null)
|
|
4038
|
+
check('实例命令:handler 抛错收成 ok: false + reason,不把异常甩给调用方', thrownResult.ok === false && thrownResult.name === 'demo:throw' && thrownResult.reason === '命令执行失败', JSON.stringify(thrownResult))
|
|
4039
|
+
apiView.registerCommand('demo:destroyed', () => 'never')
|
|
3491
4040
|
apiView.destroy()
|
|
4041
|
+
const afterDestroy = apiView.executeCommand('demo:destroyed', null)
|
|
4042
|
+
check('destroy:实例命令表清空,销毁后执行返回 ok: false', apiView.listCommands().length === 0 && afterDestroy.ok === false, JSON.stringify({ commands: apiView.listCommands(), result: afterDestroy }))
|
|
3492
4043
|
apiHost.remove()
|
|
3493
4044
|
}
|
|
3494
4045
|
/* ---------------- 线路 zIndex:电压高的线压在上面(见 15.47) ---------------- */
|
|
@@ -4294,6 +4845,37 @@
|
|
|
4294
4845
|
nwView.cancelAddChannel()
|
|
4295
4846
|
await settle(30)
|
|
4296
4847
|
check('提前结束:cancelAddChannel 收掉草稿并补一条 cancel(不新建)', nwView.getAddChannelDraft() === null && nwEvents.some(payload => payload.phase === 'cancel') && nwEvents.every(payload => payload.phase !== 'commit'), JSON.stringify(nwEvents.map(payload => payload.phase)))
|
|
4848
|
+
|
|
4849
|
+
// ⑩b 预指定起点:beginAddChannel({ from }) 直接进入「等终点」,下一次点击就是 ready
|
|
4850
|
+
nwEvents.length = 0
|
|
4851
|
+
nwSelectChanges.length = 0
|
|
4852
|
+
nwView.beginAddChannel({ from: 'na', select: true })
|
|
4853
|
+
await settle(40)
|
|
4854
|
+
const nwPresetFrom = nwView.getAddChannelDraft()
|
|
4855
|
+
check('beginAddChannel({ from }):草稿直接以该节点为起点进入 from 相位',
|
|
4856
|
+
nwPresetFrom?.phase === 'from' && nwPresetFrom.from?.id === 'na' && nwPresetFrom.to === null && nwEvents[0]?.phase === 'from',
|
|
4857
|
+
JSON.stringify({ phase: nwPresetFrom?.phase, from: nwPresetFrom?.from?.id, events: nwEvents.map(payload => payload.phase) }))
|
|
4858
|
+
check('beginAddChannel({ from, select: true }):预指定起点当场进选中集', JSON.stringify(nwView.getSelectionData().ids) === JSON.stringify(['na']), JSON.stringify(nwView.getSelectionData().ids))
|
|
4859
|
+
nwClick(nwScreenOf('nb'))
|
|
4860
|
+
await settle(60)
|
|
4861
|
+
const nwPresetReady = nwEvents.find(payload => payload.phase === 'ready')
|
|
4862
|
+
check('预指定起点后只需再选终点:一次点击得到 from=na / to=nb 的 ready',
|
|
4863
|
+
nwPresetReady?.from?.id === 'na' && nwPresetReady?.to?.id === 'nb' && nwView.getAddChannelDraft()?.phase === 'ready',
|
|
4864
|
+
JSON.stringify({ from: nwPresetReady?.from?.id, to: nwPresetReady?.to?.id, phases: nwEvents.map(payload => payload.phase) }))
|
|
4865
|
+
nwView.cancelAddChannel()
|
|
4866
|
+
await settle(20)
|
|
4867
|
+
nwView.continueAddChannel({ from: { id: 'nc' } })
|
|
4868
|
+
await settle(30)
|
|
4869
|
+
check('continueAddChannel({ from }):没有草稿时也按预指定起点重新进入', nwView.getAddChannelDraft()?.phase === 'from' && nwView.getAddChannelDraft()?.from?.id === 'nc', JSON.stringify(nwView.getAddChannelDraft()))
|
|
4870
|
+
nwView.cancelAddChannel()
|
|
4871
|
+
await settle(20)
|
|
4872
|
+
const nwWarnings = []
|
|
4873
|
+
nwView.updateOptions({ onWarning: message => nwWarnings.push(message) })
|
|
4874
|
+
nwView.beginAddChannel({ from: '不存在的起点' })
|
|
4875
|
+
await settle(20)
|
|
4876
|
+
check('beginAddChannel({ from }) 找不到节点:草稿不开始并报 warning',
|
|
4877
|
+
nwView.getAddChannelDraft() === null && nwWarnings.some(message => message.includes('找不到起点')),
|
|
4878
|
+
JSON.stringify({ draft: nwView.getAddChannelDraft(), warnings: nwWarnings }))
|
|
4297
4879
|
nwView.updateOptions({ onChannelDraft: null })
|
|
4298
4880
|
// ⑪ getChannelBetween:两个节点之间的通道读数(只读,与方案表同一份)
|
|
4299
4881
|
const cbPair = nwView.getChannelBetween('na', 'nb')
|