gis-view-webgl2 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -8
- package/dist/gis-view.css +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1036 -249
- package/dist/index.js +1 -1
- package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +194 -0
- package/docs/GisView.md +9 -8
- package/docs/GisView/345/256/236/347/216/260/345/216/237/347/220/206.md +2 -2
- 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 +172 -0
- package/docs/api/index.html +545 -59
- package/docs/demos/BackendSyncDemo.vue +10 -8
- package/docs/demos/BasicDemo.vue +101 -101
- package/docs/demos/BatchAddDemo.vue +5 -5
- package/docs/demos/ChannelDemo.vue +617 -10
- package/docs/demos/ConnectDemo.vue +392 -0
- package/docs/demos/CopyMergeDemo.vue +980 -929
- package/docs/demos/DecorationDraftDemo.vue +1300 -0
- package/docs/demos/DemoShell.vue +569 -240
- package/docs/demos/DragDropDemo.vue +11 -11
- package/docs/demos/EffectsDemo.vue +6 -6
- package/docs/demos/EventDemo.vue +327 -327
- package/docs/demos/ExtensionDemo.vue +458 -0
- package/docs/demos/GisViewDemo.vue +61 -48
- package/docs/demos/GraphicDemo.vue +338 -0
- package/docs/demos/LabelDemo.vue +553 -497
- package/docs/demos/MinimapDemo.vue +358 -0
- package/docs/demos/PerfDemo.vue +29 -0
- package/docs/demos/SearchFilterDemo.vue +412 -0
- package/docs/demos/StyleDemo.vue +121 -121
- package/docs/demos/api-index.js +98 -77
- package/docs/demos/demos.js +159 -135
- package/docs/demos/shared/demoData.js +107 -107
- package/docs/demos/shared/demoStyle.css +249 -249
- package/docs/demos/shared/fakeBackend.js +93 -6
- package/docs/format/03-/346/213/220/347/202/271/350/243/205/351/245/260/344/270/216/346/240/267/345/274/217.md +28 -8
- package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +5 -4
- package/docs/format/06-/345/257/274/345/205/245/345/257/274/345/207/272/344/270/216/346/240/207/345/207/206/346/226/207/346/241/243.md +8 -6
- 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 +6 -6
- package/docs/guide/sample-document.html +4 -4
- package/docs/index.html +180 -9
- package/docs/internals/11-/350/246/206/347/233/226/345/261/202/346/240/207/347/255/276/344/270/216/346/217/220/347/244/272.md +1 -1
- package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +3 -2
- package/docs/internals/15-03-/351/200/220/350/275/256/350/256/260/345/275/225-/346/200/247/350/203/275/344/270/216/344/277/256/345/244/215-/344/270/213.md +16 -0
- package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +11 -0
- package/docs/internals/17-/350/260/203/350/257/225/344/270/216/346/211/251/345/261/225/345/273/272/350/256/256.md +1 -1
- package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +18 -4
- package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +110 -25
- package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +13 -8
- package/docs/manual/06-/346/225/260/346/215/256/344/270/216/345/234/272/346/231/257/346/233/264/346/226/260.md +111 -10
- 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 +47 -9
- package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +17 -15
- package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +61 -11
- 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 +74 -7
- package/docs/manual/11-/346/240/267/345/274/217/344/270/216/350/247/204/345/210/231.md +3 -2
- package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +260 -3
- package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +6 -5
- package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +27 -14
- package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +142 -49
- package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +31 -29
- package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +55 -39
- package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +189 -64
- 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 +4 -4
- package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +42 -2
- 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 +22 -6
- package/docs/playground/index.html +5 -5
- package/docs/steps/README.md +14 -10
- package/docs/steps/check-doc-truth.mjs +1 -1
- package/docs/steps/selftest-actions.html +10 -10
- package/docs/steps/selftest-data.html +96 -14
- package/docs/steps/selftest-decoration.html +577 -0
- package/docs/steps/selftest-demo.html +6 -5
- package/docs/steps/selftest-demos.html +2201 -1612
- package/docs/steps/selftest-docs.html +89 -16
- package/docs/steps/selftest-effects.html +5 -5
- package/docs/steps/selftest-features.html +757 -86
- package/docs/steps/selftest-full.html +583 -583
- package/docs/steps/selftest-labels.html +1034 -652
- package/docs/steps/selftest-perf.html +1899 -1844
- package/docs/steps/selftest-scale.html +117 -7
- package/docs/steps/selftest-select.html +24 -17
- package/docs/steps/step-10-full.js +1 -1
- package/docs//346/216/245/345/217/243/345/215/217/350/256/256.md +167 -17
- package/docs//346/225/260/346/215/256/346/240/274/345/274/217.md +1 -1
- package/package.json +43 -43
- package/docs/demos/HistoryDemo.vue +0 -230
- package/docs/demos/InterfaceDemo.vue +0 -309
- package/docs/demos/SampleDemo.vue +0 -283
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>decoration running</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<div id="map" style="width:900px;height:560px"></div>
|
|
9
|
+
<pre id="out"></pre>
|
|
10
|
+
<script type="module">
|
|
11
|
+
// 动态装饰器草稿:指针跟随 / 离开收掉 / 松手 ready / 作用域展开 / commit / cancel / 换数据失效
|
|
12
|
+
const results = []
|
|
13
|
+
const check = (name, ok, extra = '') => {
|
|
14
|
+
results.push(`${ok ? 'PASS' : 'FAIL'} ${name}${extra === '' ? '' : ` — ${extra}`}`)
|
|
15
|
+
}
|
|
16
|
+
// 合成 PointerEvent 没有真实指针:setPointerCapture 可能抛 InvalidPointerId,测试里吞掉
|
|
17
|
+
const nativeCapture = Element.prototype.setPointerCapture
|
|
18
|
+
const nativeRelease = Element.prototype.releasePointerCapture
|
|
19
|
+
Element.prototype.setPointerCapture = function (id) {
|
|
20
|
+
try {
|
|
21
|
+
nativeCapture.call(this, id)
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
void error
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
Element.prototype.releasePointerCapture = function (id) {
|
|
28
|
+
try {
|
|
29
|
+
nativeRelease.call(this, id)
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
void error
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const { createChannels, createGisView } = await import('../../src/gis-view/index.js')
|
|
37
|
+
const settle = (ms = 60) => new Promise(resolve => setTimeout(resolve, ms))
|
|
38
|
+
const points = [
|
|
39
|
+
{ id: 'p1', x: 0, y: 0, name: '甲站' },
|
|
40
|
+
{ id: 'p2', x: 520, y: 0, name: '乙站' },
|
|
41
|
+
{ id: 'p3', x: 520, y: 360, name: '丙站' },
|
|
42
|
+
{ id: 'p4', x: 0, y: 360, name: '丁站' }
|
|
43
|
+
]
|
|
44
|
+
const built = createChannels([{
|
|
45
|
+
id: 'channel-a',
|
|
46
|
+
name: '甲乙通道',
|
|
47
|
+
from: 'p1',
|
|
48
|
+
to: 'p2',
|
|
49
|
+
circuits: 4,
|
|
50
|
+
lineIds: ['ch-1', 'ch-2', 'ch-3', 'ch-4'],
|
|
51
|
+
lineWidth: 2
|
|
52
|
+
}])
|
|
53
|
+
built.channels[0].groups = [
|
|
54
|
+
{ id: 'group-a', lineIds: ['ch-1', 'ch-2'], path: [[0, 0], [520, 0]] },
|
|
55
|
+
{ id: 'group-b', lineIds: ['ch-3', 'ch-4'], path: [[0, 0], [520, 0]] }
|
|
56
|
+
]
|
|
57
|
+
const lines = [...built.lines, { id: 'diag', from: 'p1', to: 'p3', name: '斜线' }]
|
|
58
|
+
const draftEvents = []
|
|
59
|
+
const dataEvents = []
|
|
60
|
+
const pointerEvents = []
|
|
61
|
+
const view = createGisView(document.querySelector('#map'), {
|
|
62
|
+
points,
|
|
63
|
+
lines,
|
|
64
|
+
channels: built.channels,
|
|
65
|
+
showGrid: false,
|
|
66
|
+
features: 'view',
|
|
67
|
+
fitPadding: 60,
|
|
68
|
+
onPointerDown: payload => pointerEvents.push(['down', payload]),
|
|
69
|
+
onPointerUp: payload => pointerEvents.push(['up', payload])
|
|
70
|
+
})
|
|
71
|
+
view.on('decorationDraft', payload => draftEvents.push(payload))
|
|
72
|
+
view.on('dataChange', payload => dataEvents.push(payload))
|
|
73
|
+
view.fitView(60)
|
|
74
|
+
await settle(220)
|
|
75
|
+
const canvas = view.canvas
|
|
76
|
+
const box = canvas.getBoundingClientRect()
|
|
77
|
+
const countPhase = phase => draftEvents.filter(payload => payload.phase === phase).length
|
|
78
|
+
const lastPhase = phase => draftEvents.filter(payload => payload.phase === phase).at(-1) ?? null
|
|
79
|
+
let pointerId = 1
|
|
80
|
+
const fire = (type, at, extra = {}) => canvas.dispatchEvent(new PointerEvent(type, {
|
|
81
|
+
bubbles: true,
|
|
82
|
+
cancelable: true,
|
|
83
|
+
pointerId,
|
|
84
|
+
pointerType: 'mouse',
|
|
85
|
+
button: 0,
|
|
86
|
+
buttons: type === 'pointerdown' ? 1 : 0,
|
|
87
|
+
...at,
|
|
88
|
+
...extra
|
|
89
|
+
}))
|
|
90
|
+
const gestureAt = (at) => {
|
|
91
|
+
pointerId += 1
|
|
92
|
+
fire('pointerdown', at)
|
|
93
|
+
fire('pointerup', at)
|
|
94
|
+
}
|
|
95
|
+
const pointOnLine = (id, ratio = 0.5) => {
|
|
96
|
+
const path = view.getLine(id)?.path ?? []
|
|
97
|
+
const segments = []
|
|
98
|
+
let total = 0
|
|
99
|
+
for (let index = 1; index < path.length; index += 1) {
|
|
100
|
+
const from = path[index - 1]
|
|
101
|
+
const to = path[index]
|
|
102
|
+
const length = Math.hypot(to[0] - from[0], to[1] - from[1])
|
|
103
|
+
segments.push({ from, to, length })
|
|
104
|
+
total += length
|
|
105
|
+
}
|
|
106
|
+
let target = total * ratio
|
|
107
|
+
let world = path[0] ?? [0, 0]
|
|
108
|
+
for (const segment of segments) {
|
|
109
|
+
if (target <= segment.length) {
|
|
110
|
+
const t = segment.length > 0 ? target / segment.length : 0
|
|
111
|
+
world = [segment.from[0] + (segment.to[0] - segment.from[0]) * t, segment.from[1] + (segment.to[1] - segment.from[1]) * t]
|
|
112
|
+
break
|
|
113
|
+
}
|
|
114
|
+
target -= segment.length
|
|
115
|
+
}
|
|
116
|
+
const screen = view.project(world[0], world[1])
|
|
117
|
+
return { clientX: box.left + screen[0], clientY: box.top + screen[1], world }
|
|
118
|
+
}
|
|
119
|
+
const screenAtPoint = (id, offsetX = 0, offsetY = 0) => {
|
|
120
|
+
const point = view.getPoint(id)
|
|
121
|
+
const screen = point?.coordinate ? view.project(point.coordinate[0], point.coordinate[1]) : [0, 0]
|
|
122
|
+
return {
|
|
123
|
+
clientX: box.left + screen[0] + offsetX,
|
|
124
|
+
clientY: box.top + screen[1] + offsetY,
|
|
125
|
+
screen: [screen[0] + offsetX, screen[1] + offsetY]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const blank = { clientX: box.left + 10, clientY: box.top + 10 }
|
|
129
|
+
const customSymbol = { type: 'svg', path: 'M12 2l3 7 7 1-5 5 1 7-6-3-6 3 1-7-5-5 7-1z', viewBox: [0, 0, 24, 24], fill: 'currentColor' }
|
|
130
|
+
|
|
131
|
+
/* ---------- 开启与悬停 ---------- */
|
|
132
|
+
check('初始没有装饰器草稿', view.getDecorationDraft() === null, '')
|
|
133
|
+
const start = view.beginDecorationDraft({ symbol: customSymbol, size: 22, color: '#ffd166', parallel: true, scope: 'line' })
|
|
134
|
+
check('beginDecorationDraft 返回 start 载荷且草稿处于活跃状态', start?.phase === 'start' && view.getDecorationDraft() !== null, JSON.stringify({ returnPhase: start?.phase, activePhase: view.getDecorationDraft()?.phase }))
|
|
135
|
+
check('start 事件带归一化后的图标 / 尺寸 / 方向 / 范围', start?.symbol === 'svg' && start.size === 22 && start.parallel === true && start.scope === 'line', JSON.stringify({ symbol: start?.symbol, size: start?.size, parallel: start?.parallel, scope: start?.scope }))
|
|
136
|
+
check('默认触发距离收紧到 14px,每条线上限默认不限', start?.maxDistance === 14 && start?.maxPerLine === 0 && Array.isArray(start?.allowedLines) && start.allowedLines.length === 0, JSON.stringify({ maxDistance: start?.maxDistance, maxPerLine: start?.maxPerLine, allowed: start?.allowedLines }))
|
|
137
|
+
const ch1FarBase = pointOnLine('ch-1', 0.35)
|
|
138
|
+
const farPoint = { clientX: ch1FarBase.clientX, clientY: ch1FarBase.clientY - 30 }
|
|
139
|
+
const hoverBeforeFar = countPhase('hover')
|
|
140
|
+
fire('pointermove', farPoint)
|
|
141
|
+
await settle(30)
|
|
142
|
+
check('离线路约 30px:默认阈值下既不出预览也不发 hover', countPhase('hover') === hoverBeforeFar && view.getDecorationDraft()?.targets.length === 0 && view.getDecorationDraft()?.phase === 'leave', JSON.stringify({ hoverCount: countPhase('hover'), phase: view.getDecorationDraft()?.phase }))
|
|
143
|
+
const diag = pointOnLine('diag')
|
|
144
|
+
fire('pointermove', diag)
|
|
145
|
+
await settle(40)
|
|
146
|
+
const hover = lastPhase('hover')
|
|
147
|
+
check('指针移到线路上:发 hover,命中 diag、目标 1 条、沿线位置在 50% 附近', hover?.lineId === 'diag' && hover?.targetCount === 1 && hover?.scope === 'line' && hover.position > 0.45 && hover.position < 0.55, JSON.stringify({ lineId: hover?.lineId, count: hover?.targetCount, position: hover?.position }))
|
|
148
|
+
check('hover 载荷带世界坐标、屏幕坐标与归一化 decoration', Array.isArray(hover?.world) && Array.isArray(hover?.screen) && hover?.decoration?.anchor === 'follow' && hover.decoration.position === hover.position, JSON.stringify({ world: hover?.world, anchor: hover?.decoration?.anchor }))
|
|
149
|
+
check('parallel 打开时图标角度跟着斜线切向(不是 0)', Math.abs(hover?.angle ?? 0) > 0.1, String(hover?.angle))
|
|
150
|
+
const hoverCountBeforeRepeat = countPhase('hover')
|
|
151
|
+
fire('pointermove', diag)
|
|
152
|
+
await settle(20)
|
|
153
|
+
check('同一位置重复 pointermove 不重复发 hover(性能去抖)', countPhase('hover') === hoverCountBeforeRepeat, `${countPhase('hover')} / ${hoverCountBeforeRepeat}`)
|
|
154
|
+
|
|
155
|
+
/* ---------- 移开与空白松手 ---------- */
|
|
156
|
+
fire('pointermove', blank)
|
|
157
|
+
await settle(40)
|
|
158
|
+
const leave = lastPhase('leave')
|
|
159
|
+
check('移出目标线路:发 leave,假装饰与目标表清空', leave !== null && view.getDecorationDraft()?.targets.length === 0 && view.getDecorationDraft()?.decoration === null, JSON.stringify({ phase: view.getDecorationDraft()?.phase, targets: view.getDecorationDraft()?.targets.length }))
|
|
160
|
+
const readyBeforeBlank = countPhase('ready')
|
|
161
|
+
gestureAt(blank)
|
|
162
|
+
await settle(40)
|
|
163
|
+
check('空白处按下再松开:不发 ready', countPhase('ready') === readyBeforeBlank, `ready ${countPhase('ready')} / ${readyBeforeBlank}`)
|
|
164
|
+
|
|
165
|
+
/* ---------- 松手命中与冻结 ---------- */
|
|
166
|
+
fire('pointermove', diag)
|
|
167
|
+
await settle(30)
|
|
168
|
+
gestureAt(diag)
|
|
169
|
+
await settle(40)
|
|
170
|
+
const ready = lastPhase('ready')
|
|
171
|
+
check('线路上的 pointerup:发 ready(正好一条)', ready !== null && countPhase('ready') === readyBeforeBlank + 1 && ready.lineId === 'diag', JSON.stringify({ readyCount: countPhase('ready'), lineId: ready?.lineId }))
|
|
172
|
+
check('按下 / 抬起动作名标明是装饰器草稿', pointerEvents.at(-2)?.[1]?.action?.type === 'decorationDraft' && pointerEvents.at(-1)?.[1]?.action?.type === 'decorationDraft', JSON.stringify(pointerEvents.slice(-2).map(entry => entry[1]?.action?.type)))
|
|
173
|
+
fire('pointermove', blank)
|
|
174
|
+
await settle(30)
|
|
175
|
+
check('ready 之后等确认:指针离开也不收假装饰', view.getDecorationDraft()?.phase === 'ready' && view.getDecorationDraft()?.targets.length === 1, JSON.stringify({ phase: view.getDecorationDraft()?.phase, targets: view.getDecorationDraft()?.targets.length }))
|
|
176
|
+
view.cancelDecorationDraft()
|
|
177
|
+
|
|
178
|
+
/* ---------- 单线 / 分组 / 整条通道 ---------- */
|
|
179
|
+
view.beginDecorationDraft({ symbol: customSymbol, size: 31, color: '#3fa7ff', parallel: true, scope: 'line' })
|
|
180
|
+
const ch1 = pointOnLine('ch-1')
|
|
181
|
+
fire('pointermove', ch1)
|
|
182
|
+
await settle(30)
|
|
183
|
+
view.updateDecorationDraft({ symbol: customSymbol, size: 31, color: '#3fa7ff', parallel: true, scope: 'line' })
|
|
184
|
+
const lineScope = view.getDecorationDraft()
|
|
185
|
+
check('scope: line 只展开命中的这一条回线', lineScope?.targetCount === 1 && lineScope?.targets[0]?.lineId === 'ch-1' && lineScope?.targets[0]?.channelId === 'channel-a', JSON.stringify(lineScope?.targets))
|
|
186
|
+
view.updateDecorationDraft({ scope: 'group' })
|
|
187
|
+
const groupScope = view.getDecorationDraft()
|
|
188
|
+
check('scope: group 展开到同组两条回线', groupScope?.targetCount === 2 && groupScope?.targets.every(target => target.groupId === 'group-a'), JSON.stringify(groupScope?.targets))
|
|
189
|
+
view.updateDecorationDraft({ scope: 'channel' })
|
|
190
|
+
const channelScope = view.getDecorationDraft()
|
|
191
|
+
check('scope: channel 展开到整条通道四条回线', channelScope?.targetCount === 4 && channelScope?.targets.map(target => target.lineId).sort().join(',') === 'ch-1,ch-2,ch-3,ch-4', JSON.stringify(channelScope?.targets))
|
|
192
|
+
check('update 后的尺寸 / 图标已经进入真实 decoration 字段', channelScope?.size === 31 && channelScope?.decoration?.size === 31 && channelScope?.decoration?.symbol === customSymbol, JSON.stringify({ size: channelScope?.size, decorationSize: channelScope?.decoration?.size }))
|
|
193
|
+
fire('pointermove', diag)
|
|
194
|
+
view.updateDecorationDraft({ scope: 'group' })
|
|
195
|
+
check('非通道线路用 group / channel 范围时回退成单条', view.getDecorationDraft()?.targetCount === 1, String(view.getDecorationDraft()?.targetCount))
|
|
196
|
+
view.updateDecorationDraft({ scope: 'channel' })
|
|
197
|
+
check('非通道线路用 channel 范围也回退成单条', view.getDecorationDraft()?.targetCount === 1, String(view.getDecorationDraft()?.targetCount))
|
|
198
|
+
|
|
199
|
+
/* ---------- 提交:一次 batch 写四条 ---------- */
|
|
200
|
+
fire('pointermove', ch1)
|
|
201
|
+
view.updateDecorationDraft({ scope: 'channel', size: 29, parallel: true })
|
|
202
|
+
gestureAt(ch1)
|
|
203
|
+
await settle(40)
|
|
204
|
+
const beforeCommit = dataEvents.length
|
|
205
|
+
const commit = view.commitDecorationDraft()
|
|
206
|
+
await settle(40)
|
|
207
|
+
check('commitDecorationDraft 返回 ok / 4 条目标 / 4 份 decoration', commit.ok === true && commit.count === 4 && commit.decorations.length === 4 && commit.targets.length === 4, JSON.stringify({ ok: commit.ok, count: commit.count }))
|
|
208
|
+
check('提交只发一条 dataChange,类型是 batch', dataEvents.length === beforeCommit + 1 && dataEvents.at(-1)?.type === 'batch', JSON.stringify(dataEvents.slice(beforeCommit).map(payload => payload.type)))
|
|
209
|
+
const committedLines = commit.targets.map(target => view.getLineDecorations(target.lineId))
|
|
210
|
+
check('每条目标线路都写上同位置、同方向、同尺寸的 decoration', committedLines.every(list => list.length === 1 && list[0].anchor === 'follow' && list[0].parallel === true && list[0].size === 29 && list[0].position === commit.decorations[0].position), JSON.stringify(committedLines.map(list => list[0])))
|
|
211
|
+
check('提交后草稿清空', view.getDecorationDraft() === null, '')
|
|
212
|
+
check('没有草稿时 commit / update 都是空操作', view.commitDecorationDraft().ok === false && view.commitDecorationDraft().reason === 'no-draft' && view.updateDecorationDraft({ size: 12 }) === null, JSON.stringify(view.commitDecorationDraft()))
|
|
213
|
+
|
|
214
|
+
/* ---------- 取消:数据一个字节不动 ---------- */
|
|
215
|
+
const dataBeforeCancel = JSON.stringify(view.getData())
|
|
216
|
+
const dataEventsBeforeCancel = dataEvents.length
|
|
217
|
+
view.beginDecorationDraft({ symbol: customSymbol, size: 20, scope: 'line', parallel: false })
|
|
218
|
+
fire('pointermove', ch1)
|
|
219
|
+
gestureAt(ch1)
|
|
220
|
+
await settle(30)
|
|
221
|
+
const beforeCancel = view.getDecorationDraft()
|
|
222
|
+
const cancelled = view.cancelDecorationDraft()
|
|
223
|
+
await settle(30)
|
|
224
|
+
check('cancelDecorationDraft 发 cancel 并清空草稿', cancelled?.phase === 'cancel' && beforeCancel?.phase === 'ready' && view.getDecorationDraft() === null, JSON.stringify({ before: beforeCancel?.phase, cancel: cancelled?.phase }))
|
|
225
|
+
check('取消不改数据、不发 dataChange', JSON.stringify(view.getData()) === dataBeforeCancel && dataEvents.length === dataEventsBeforeCancel, JSON.stringify({ dataEvents: dataEvents.length - dataEventsBeforeCancel }))
|
|
226
|
+
|
|
227
|
+
/* ---------- 数量上限:满额不出现,提交前再拦一次 ---------- */
|
|
228
|
+
view.setLineDecorations('diag', [{ position: 0.5, symbol: 'circle', size: 12 }])
|
|
229
|
+
view.beginDecorationDraft({ scope: 'line', maxPerLine: 1 })
|
|
230
|
+
const diagBlocked = pointOnLine('diag', 0.64)
|
|
231
|
+
const hoverBeforeFull = countPhase('hover')
|
|
232
|
+
const readyBeforeFull = countPhase('ready')
|
|
233
|
+
fire('pointermove', diagBlocked)
|
|
234
|
+
await settle(30)
|
|
235
|
+
check('maxPerLine 满额:线路不出现、目标为空且不发 hover', countPhase('hover') === hoverBeforeFull && view.getDecorationDraft()?.targets.length === 0 && view.getDecorationDraft()?.blockedReason === 'limit', JSON.stringify({ hover: countPhase('hover'), targets: view.getDecorationDraft()?.targets.length, blocked: view.getDecorationDraft()?.blockedReason, line0Count: view.getLineDecorations('line-0').length, optionMax: view.getDecorationDraft()?.maxPerLine, target: view.getDecorationDraft()?.targets[0] }))
|
|
236
|
+
gestureAt(diagBlocked)
|
|
237
|
+
await settle(30)
|
|
238
|
+
check('maxPerLine 满额:在线路上松手也不发 ready', countPhase('ready') === readyBeforeFull, `${countPhase('ready')} / ${readyBeforeFull}`)
|
|
239
|
+
view.cancelDecorationDraft()
|
|
240
|
+
|
|
241
|
+
view.setLineDecorations('diag', [])
|
|
242
|
+
view.beginDecorationDraft({ scope: 'line', maxPerLine: 1 })
|
|
243
|
+
const diagLimit = pointOnLine('diag', 0.42)
|
|
244
|
+
fire('pointermove', diagLimit)
|
|
245
|
+
gestureAt(diagLimit)
|
|
246
|
+
await settle(30)
|
|
247
|
+
view.addLineDecoration('diag', { position: 0.25, symbol: 'square', size: 12 })
|
|
248
|
+
const dataEventsBeforeLimitCommit = dataEvents.length
|
|
249
|
+
const limitCommit = view.commitDecorationDraft()
|
|
250
|
+
await settle(20)
|
|
251
|
+
check('ready 之后数据被占满:commit 返回 limit 且一条都不写', limitCommit.ok === false && limitCommit.reason === 'limit' && view.getLineDecorations('diag').length === 1 && dataEvents.length === dataEventsBeforeLimitCommit, JSON.stringify({ ok: limitCommit.ok, reason: limitCommit.reason, decorations: view.getLineDecorations('diag').length }))
|
|
252
|
+
view.setLineDecorations('diag', [])
|
|
253
|
+
|
|
254
|
+
/* ---------- 白名单:不允许的线路静默,提交前也不放行 ---------- */
|
|
255
|
+
view.beginDecorationDraft({ scope: 'line', allowedLines: ['diag'] })
|
|
256
|
+
const hoverBeforeLineWhitelist = countPhase('hover')
|
|
257
|
+
fire('pointermove', ch1)
|
|
258
|
+
await settle(30)
|
|
259
|
+
check('allowedLines 外:不发 hover、不展开目标', countPhase('hover') === hoverBeforeLineWhitelist && view.getDecorationDraft()?.targets.length === 0 && view.getDecorationDraft()?.blockedReason === 'not-allowed', JSON.stringify({ hover: countPhase('hover'), blocked: view.getDecorationDraft()?.blockedReason }))
|
|
260
|
+
fire('pointermove', diag)
|
|
261
|
+
await settle(30)
|
|
262
|
+
check('allowedLines 内:命中线路正常 hover', lastPhase('hover')?.lineId === 'diag' && view.getDecorationDraft()?.targetCount === 1, JSON.stringify(lastPhase('hover')))
|
|
263
|
+
view.cancelDecorationDraft()
|
|
264
|
+
|
|
265
|
+
view.beginDecorationDraft({ scope: 'line', allowedChannels: ['channel-a'] })
|
|
266
|
+
const hoverBeforeChannelWhitelist = countPhase('hover')
|
|
267
|
+
fire('pointermove', diag)
|
|
268
|
+
await settle(30)
|
|
269
|
+
check('allowedChannels 外:非通道线路静默', countPhase('hover') === hoverBeforeChannelWhitelist && view.getDecorationDraft()?.blockedReason === 'not-allowed', JSON.stringify({ hover: countPhase('hover'), blocked: view.getDecorationDraft()?.blockedReason }))
|
|
270
|
+
fire('pointermove', ch1)
|
|
271
|
+
await settle(30)
|
|
272
|
+
check('allowedChannels 内:通道回线正常 hover', lastPhase('hover')?.lineId === 'ch-1', JSON.stringify(lastPhase('hover')))
|
|
273
|
+
view.cancelDecorationDraft()
|
|
274
|
+
|
|
275
|
+
view.beginDecorationDraft({ scope: 'line', allowedGroups: ['group-b'] })
|
|
276
|
+
const hoverBeforeGroupWhitelist = countPhase('hover')
|
|
277
|
+
fire('pointermove', ch1)
|
|
278
|
+
await settle(30)
|
|
279
|
+
check('allowedGroups 外:同通道其它分组静默', countPhase('hover') === hoverBeforeGroupWhitelist && view.getDecorationDraft()?.blockedReason === 'not-allowed', JSON.stringify({ hover: countPhase('hover'), blocked: view.getDecorationDraft()?.blockedReason }))
|
|
280
|
+
fire('pointermove', pointOnLine('ch-3'))
|
|
281
|
+
await settle(30)
|
|
282
|
+
check('allowedGroups 内:目标分组的回线正常 hover', lastPhase('hover')?.groupId === 'group-b', JSON.stringify(lastPhase('hover')?.groupId))
|
|
283
|
+
view.cancelDecorationDraft()
|
|
284
|
+
|
|
285
|
+
view.beginDecorationDraft({ scope: 'channel', allowedLines: ['ch-1', 'ch-3'] })
|
|
286
|
+
fire('pointermove', ch1)
|
|
287
|
+
await settle(30)
|
|
288
|
+
const filteredScope = view.getDecorationDraft()
|
|
289
|
+
check('scope 展开只提交白名单内的线路', filteredScope?.targets.map(target => target.lineId).sort().join(',') === 'ch-1,ch-3', JSON.stringify(filteredScope?.targets))
|
|
290
|
+
view.cancelDecorationDraft()
|
|
291
|
+
|
|
292
|
+
view.setLineDecorations('ch-1', [{ position: 0.2, symbol: 'circle', size: 12 }])
|
|
293
|
+
view.setLineDecorations('ch-2', [])
|
|
294
|
+
view.setLineDecorations('ch-3', [])
|
|
295
|
+
view.setLineDecorations('ch-4', [{ position: 0.8, symbol: 'circle', size: 12 }])
|
|
296
|
+
view.beginDecorationDraft({ scope: 'channel', maxPerLine: 1 })
|
|
297
|
+
fire('pointermove', pointOnLine('ch-2'))
|
|
298
|
+
await settle(30)
|
|
299
|
+
const partiallyFull = view.getDecorationDraft()
|
|
300
|
+
check('maxPerLine 只过滤满额线路,空余回线照常展开', partiallyFull?.targets.map(target => target.lineId).sort().join(',') === 'ch-2,ch-3' && partiallyFull?.targets.every(target => target.existingCount === 0), JSON.stringify(partiallyFull?.targets))
|
|
301
|
+
view.cancelDecorationDraft()
|
|
302
|
+
|
|
303
|
+
view.setLineDecorations('diag', [])
|
|
304
|
+
view.beginDecorationDraft({ scope: 'line', allowedLines: ['diag'] })
|
|
305
|
+
fire('pointermove', pointOnLine('diag', 0.55))
|
|
306
|
+
gestureAt(pointOnLine('diag', 0.55))
|
|
307
|
+
await settle(30)
|
|
308
|
+
const dataEventsBeforeWhitelistCommit = dataEvents.length
|
|
309
|
+
const whitelistCommit = view.commitDecorationDraft({ allowedLines: ['ch-1'] })
|
|
310
|
+
check('提交时 override 白名单收紧:返回 not-allowed 且不写数据', whitelistCommit.ok === false && whitelistCommit.reason === 'not-allowed' && view.getLineDecorations('diag').length === 0 && dataEvents.length === dataEventsBeforeWhitelistCommit, JSON.stringify({ ok: whitelistCommit.ok, reason: whitelistCommit.reason, prePhase: dataEventsBeforeWhitelistCommit, active: view.getDecorationDraft(), events: dataEvents.length }))
|
|
311
|
+
|
|
312
|
+
/* ---------- 节点装饰:相对方向 / 距离、固定坐标、鼠标位置 ---------- */
|
|
313
|
+
const p1At = screenAtPoint('p1')
|
|
314
|
+
view.beginDecorationDraft({
|
|
315
|
+
target: 'point',
|
|
316
|
+
placement: 'offset',
|
|
317
|
+
direction: 90,
|
|
318
|
+
distance: 36,
|
|
319
|
+
angle: 45,
|
|
320
|
+
connector: { color: '#7fa9dc', width: 1.5, dashed: true },
|
|
321
|
+
symbol: customSymbol,
|
|
322
|
+
size: 24
|
|
323
|
+
})
|
|
324
|
+
fire('pointermove', p1At)
|
|
325
|
+
await settle(40)
|
|
326
|
+
const pointHover = lastPhase('hover')
|
|
327
|
+
const pointOffset = pointHover?.pointDecoration?.offset ?? []
|
|
328
|
+
check('target=point:命中节点并只展开一个节点目标', pointHover?.hitKind === 'point' && pointHover?.pointId === 'p1' && pointHover?.lineId === null && pointHover?.pointTargetCount === 1 && pointHover?.targetCount === 1, JSON.stringify({ hitKind: pointHover?.hitKind, pointId: pointHover?.pointId, lineId: pointHover?.lineId, count: pointHover?.targetCount }))
|
|
329
|
+
check('节点方向 90° / 距离 36px:偏移换算成屏幕坐标 [36, 0]', Math.abs(pointOffset[0] - 36) < 0.01 && Math.abs(pointOffset[1]) < 0.01, JSON.stringify(pointOffset))
|
|
330
|
+
check('节点装饰预览带屏幕位置、度转角与连接线样式', pointHover?.decorationScreen?.[0] > p1At.screen[0] + 35 && Math.abs((pointHover?.angle ?? 0) - Math.PI / 4) < 1e-6 && pointHover?.pointDecoration?.connector?.color === '#7fa9dc' && pointHover.pointDecoration.connector.dashed === true, JSON.stringify({ screen: pointHover?.decorationScreen, angle: pointHover?.angle, connector: pointHover?.pointDecoration?.connector }))
|
|
331
|
+
const nodeDataBefore = dataEvents.length
|
|
332
|
+
gestureAt(p1At)
|
|
333
|
+
await settle(40)
|
|
334
|
+
const nodeReady = lastPhase('ready')
|
|
335
|
+
check('节点上 pointerup:发 ready 且命中仍是节点', nodeReady?.hitKind === 'point' && nodeReady?.pointId === 'p1', JSON.stringify({ hitKind: nodeReady?.hitKind, pointId: nodeReady?.pointId }))
|
|
336
|
+
const nodeCommit = view.commitDecorationDraft()
|
|
337
|
+
await settle(30)
|
|
338
|
+
const p1Decorations = view.getPointDecorations('p1')
|
|
339
|
+
check('节点装饰提交:一份 pointDecoration、一次 batch dataChange', nodeCommit.ok === true && nodeCommit.count === 1 && nodeCommit.pointDecorations.length === 1 && nodeCommit.targets.length === 0 && dataEvents.length === nodeDataBefore + 1 && dataEvents.at(-1)?.type === 'batch', JSON.stringify({ ok: nodeCommit.ok, count: nodeCommit.count, pointTargets: nodeCommit.pointTargets?.length, targets: nodeCommit.targets?.length, events: dataEvents.slice(nodeDataBefore).map(payload => payload.type) }))
|
|
340
|
+
check('提交后的节点装饰保留屏幕偏移、角度与连接线', p1Decorations.length === 1 && p1Decorations[0].anchor === 'offset' && Math.abs(p1Decorations[0].offset[0] - 36) < 0.01 && p1Decorations[0].angle === 45 && p1Decorations[0].connector?.dashed === true, JSON.stringify(p1Decorations[0]))
|
|
341
|
+
const pointScene = view.snapshotScene({ background: false })
|
|
342
|
+
const angleDecoration = pointScene?.decorations?.find(node => Math.abs((node.angle ?? 0) - Math.PI / 4) < 1e-6)
|
|
343
|
+
const connectorLine = pointScene?.lines?.find(line => line.dash?.join(',') === '5,4' && Math.abs(line.width - 1.5) < 0.01)
|
|
344
|
+
check('节点装饰按弧度进入矢量快照,连接线也在快照的线路层', Boolean(angleDecoration) && Boolean(connectorLine), JSON.stringify({ angle: angleDecoration?.angle, connector: connectorLine }))
|
|
345
|
+
|
|
346
|
+
const p2At = screenAtPoint('p2')
|
|
347
|
+
view.beginDecorationDraft({ target: 'point', placement: 'fixed', at: [560, 40], connector: true, symbol: 'circle', size: 18 })
|
|
348
|
+
fire('pointermove', p2At)
|
|
349
|
+
await settle(40)
|
|
350
|
+
const fixedHover = lastPhase('hover')
|
|
351
|
+
check('节点固定坐标:预览使用写死的世界坐标', fixedHover?.hitKind === 'point' && fixedHover?.pointId === 'p2' && fixedHover?.pointDecoration?.anchor === 'fixed' && fixedHover.pointDecoration.at?.[0] === 560 && fixedHover.pointDecoration.at?.[1] === 40, JSON.stringify({ hitKind: fixedHover?.hitKind, decoration: fixedHover?.pointDecoration }))
|
|
352
|
+
gestureAt(p2At)
|
|
353
|
+
await settle(30)
|
|
354
|
+
view.commitDecorationDraft()
|
|
355
|
+
await settle(20)
|
|
356
|
+
const p2Decoration = view.getPointDecorations('p2')[0]
|
|
357
|
+
check('固定坐标节点装饰写入 at,节点移动后仍不跟随', p2Decoration?.anchor === 'fixed' && p2Decoration.at?.[0] === 560 && p2Decoration.at?.[1] === 40 && p2Decoration.connector === true, JSON.stringify(p2Decoration))
|
|
358
|
+
|
|
359
|
+
const p3At = screenAtPoint('p3', 6, -5)
|
|
360
|
+
const p3World = view.unproject(p3At.screen[0], p3At.screen[1])
|
|
361
|
+
view.beginDecorationDraft({ target: 'point', placement: 'pointer', symbol: 'square', size: 16 })
|
|
362
|
+
fire('pointermove', p3At)
|
|
363
|
+
await settle(40)
|
|
364
|
+
const pointerHover = lastPhase('hover')
|
|
365
|
+
check('鼠标位置吸附:预览落在指针屏幕位置而不是节点中心', pointerHover?.hitKind === 'point' && pointerHover?.pointId === 'p3' && Math.abs(pointerHover.decorationScreen[0] - p3At.screen[0]) < 0.01 && Math.abs(pointerHover.decorationScreen[1] - p3At.screen[1]) < 0.01, JSON.stringify({ hitKind: pointerHover?.hitKind, screen: pointerHover?.decorationScreen, expected: p3At.screen }))
|
|
366
|
+
gestureAt(p3At)
|
|
367
|
+
await settle(30)
|
|
368
|
+
view.commitDecorationDraft()
|
|
369
|
+
await settle(20)
|
|
370
|
+
const p3Decoration = view.getPointDecorations('p3')[0]
|
|
371
|
+
check('鼠标位置落成固定世界坐标', p3Decoration?.anchor === 'fixed' && Math.abs(p3Decoration.at[0] - p3World[0]) < 0.01 && Math.abs(p3Decoration.at[1] - p3World[1]) < 0.01, JSON.stringify({ decoration: p3Decoration, expected: p3World }))
|
|
372
|
+
|
|
373
|
+
/* ---------- 吸附方式与位置来源正交:跨组合回归 ---------- */
|
|
374
|
+
const p4At = screenAtPoint('p4')
|
|
375
|
+
const p4Pointer = { clientX: p4At.clientX + 9, clientY: p4At.clientY - 7, screen: [p4At.screen[0] + 9, p4At.screen[1] - 7] }
|
|
376
|
+
view.beginDecorationDraft({ target: 'point', placement: 'pointer', anchor: 'follow', symbol: 'circle', size: 14 })
|
|
377
|
+
fire('pointermove', p4Pointer)
|
|
378
|
+
await settle(30)
|
|
379
|
+
const pointerFollowHover = lastPhase('hover')
|
|
380
|
+
check('pointer + follow:预览在指针位置,落地转成相对节点的屏幕偏移', pointerFollowHover?.placement === 'pointer' && pointerFollowHover?.anchor === 'follow' && pointerFollowHover?.pointDecoration?.anchor === 'offset' && Math.abs(pointerFollowHover.pointDecoration.offset[0] - 9) < 0.01 && Math.abs(pointerFollowHover.pointDecoration.offset[1] + 7) < 0.01, JSON.stringify({ placement: pointerFollowHover?.placement, anchor: pointerFollowHover?.anchor, decoration: pointerFollowHover?.pointDecoration }))
|
|
381
|
+
gestureAt(p4Pointer)
|
|
382
|
+
await settle(30)
|
|
383
|
+
view.commitDecorationDraft()
|
|
384
|
+
await settle(20)
|
|
385
|
+
const pointerFollowDecoration = view.getPointDecorations('p4')[0]
|
|
386
|
+
check('pointer + follow:提交后写 anchor=offset 与指针相对节点的偏移', pointerFollowDecoration?.anchor === 'offset' && Math.abs(pointerFollowDecoration.offset[0] - 9) < 0.01 && Math.abs(pointerFollowDecoration.offset[1] + 7) < 0.01 && pointerFollowDecoration.at === undefined, JSON.stringify(pointerFollowDecoration))
|
|
387
|
+
view.setPointDecorations('p4', [])
|
|
388
|
+
|
|
389
|
+
const fixedFollowScreen = [p4At.screen[0] + 24, p4At.screen[1] - 12]
|
|
390
|
+
const fixedFollowAt = view.unproject(fixedFollowScreen[0], fixedFollowScreen[1])
|
|
391
|
+
view.beginDecorationDraft({ target: 'point', placement: 'fixed', anchor: 'follow', at: fixedFollowAt, symbol: 'square', size: 14 })
|
|
392
|
+
fire('pointermove', p4At)
|
|
393
|
+
await settle(30)
|
|
394
|
+
const fixedFollowHover = lastPhase('hover')
|
|
395
|
+
check('fixed + follow:位置来自世界坐标,落地转成相对节点的屏幕偏移', fixedFollowHover?.placement === 'fixed' && fixedFollowHover?.anchor === 'follow' && fixedFollowHover?.pointDecoration?.anchor === 'offset' && Math.abs(fixedFollowHover.pointDecoration.offset[0] - 24) < 0.01 && Math.abs(fixedFollowHover.pointDecoration.offset[1] + 12) < 0.01, JSON.stringify({ placement: fixedFollowHover?.placement, anchor: fixedFollowHover?.anchor, decoration: fixedFollowHover?.pointDecoration }))
|
|
396
|
+
gestureAt(p4At)
|
|
397
|
+
await settle(30)
|
|
398
|
+
view.commitDecorationDraft()
|
|
399
|
+
await settle(20)
|
|
400
|
+
const fixedFollowDecoration = view.getPointDecorations('p4')[0]
|
|
401
|
+
check('fixed + follow:提交后只写 offset,不再保留 at', fixedFollowDecoration?.anchor === 'offset' && Math.abs(fixedFollowDecoration.offset[0] - 24) < 0.01 && Math.abs(fixedFollowDecoration.offset[1] + 12) < 0.01 && fixedFollowDecoration.at === undefined, JSON.stringify(fixedFollowDecoration))
|
|
402
|
+
view.setPointDecorations('p4', [])
|
|
403
|
+
|
|
404
|
+
const offsetFixedAt = view.unproject(p4At.screen[0] + 24, p4At.screen[1])
|
|
405
|
+
view.beginDecorationDraft({ target: 'point', placement: 'offset', anchor: 'fixed', direction: 90, distance: 24, symbol: 'circle', size: 14 })
|
|
406
|
+
fire('pointermove', p4At)
|
|
407
|
+
await settle(30)
|
|
408
|
+
const offsetFixedHover = lastPhase('hover')
|
|
409
|
+
check('offset + fixed:位置来自相对偏移,落地写世界坐标', offsetFixedHover?.placement === 'offset' && offsetFixedHover?.anchor === 'fixed' && offsetFixedHover?.pointDecoration?.anchor === 'fixed' && Math.abs(offsetFixedHover.pointDecoration.at[0] - offsetFixedAt[0]) < 0.01 && Math.abs(offsetFixedHover.pointDecoration.at[1] - offsetFixedAt[1]) < 0.01, JSON.stringify({ placement: offsetFixedHover?.placement, anchor: offsetFixedHover?.anchor, decoration: offsetFixedHover?.pointDecoration, expected: offsetFixedAt }))
|
|
410
|
+
gestureAt(p4At)
|
|
411
|
+
await settle(30)
|
|
412
|
+
view.commitDecorationDraft()
|
|
413
|
+
await settle(20)
|
|
414
|
+
const offsetFixedDecoration = view.getPointDecorations('p4')[0]
|
|
415
|
+
check('offset + fixed:提交后写 anchor=fixed 与 at,不写 offset', offsetFixedDecoration?.anchor === 'fixed' && Math.abs(offsetFixedDecoration.at[0] - offsetFixedAt[0]) < 0.01 && Math.abs(offsetFixedDecoration.at[1] - offsetFixedAt[1]) < 0.01 && offsetFixedDecoration.offset === undefined, JSON.stringify(offsetFixedDecoration))
|
|
416
|
+
view.setPointDecorations('p4', [])
|
|
417
|
+
|
|
418
|
+
/* ---------- 节点白名单与每节点上限 ---------- */
|
|
419
|
+
view.beginDecorationDraft({ target: 'point', allowedPoints: ['p2'] })
|
|
420
|
+
const hoverBeforeNodeWhitelist = countPhase('hover')
|
|
421
|
+
fire('pointermove', p1At)
|
|
422
|
+
await settle(30)
|
|
423
|
+
check('allowedPoints 外:不触发节点 hover,blockedReason=not-allowed', countPhase('hover') === hoverBeforeNodeWhitelist && view.getDecorationDraft()?.pointTargets.length === 0 && view.getDecorationDraft()?.blockedReason === 'not-allowed', JSON.stringify({ hover: countPhase('hover'), blocked: view.getDecorationDraft()?.blockedReason }))
|
|
424
|
+
fire('pointermove', p2At)
|
|
425
|
+
await settle(30)
|
|
426
|
+
check('allowedPoints 内:节点正常 hover', lastPhase('hover')?.hitKind === 'point' && lastPhase('hover')?.pointId === 'p2', JSON.stringify(lastPhase('hover')))
|
|
427
|
+
gestureAt(p2At)
|
|
428
|
+
await settle(30)
|
|
429
|
+
const dataBeforeNodeWhitelistCommit = dataEvents.length
|
|
430
|
+
const nodeWhitelistCommit = view.commitDecorationDraft({ allowedPoints: ['p1'] })
|
|
431
|
+
check('节点提交时 override 白名单收紧:not-allowed 且不写数据', nodeWhitelistCommit.ok === false && nodeWhitelistCommit.reason === 'not-allowed' && view.getPointDecorations('p2').length === 1 && dataEvents.length === dataBeforeNodeWhitelistCommit, JSON.stringify({ ok: nodeWhitelistCommit.ok, reason: nodeWhitelistCommit.reason, count: view.getPointDecorations('p2').length }))
|
|
432
|
+
|
|
433
|
+
view.setPointDecorations('p4', [{ symbol: 'circle', size: 10, offset: [0, -20] }])
|
|
434
|
+
view.beginDecorationDraft({ target: 'point', maxPerPoint: 1 })
|
|
435
|
+
const hoverBeforePointLimit = countPhase('hover')
|
|
436
|
+
const readyBeforePointLimit = countPhase('ready')
|
|
437
|
+
fire('pointermove', p4At)
|
|
438
|
+
await settle(30)
|
|
439
|
+
check('maxPerPoint 满额:节点不触发 hover,blockedReason=limit', countPhase('hover') === hoverBeforePointLimit && view.getDecorationDraft()?.pointTargets.length === 0 && view.getDecorationDraft()?.blockedReason === 'limit', JSON.stringify({ hover: countPhase('hover'), blocked: view.getDecorationDraft()?.blockedReason }))
|
|
440
|
+
gestureAt(p4At)
|
|
441
|
+
await settle(30)
|
|
442
|
+
check('maxPerPoint 满额:节点上松手也不发 ready', countPhase('ready') === readyBeforePointLimit, `${countPhase('ready')} / ${readyBeforePointLimit}`)
|
|
443
|
+
view.cancelDecorationDraft()
|
|
444
|
+
view.beginDecorationDraft({ target: 'point', maxPerPoint: 2 })
|
|
445
|
+
fire('pointermove', p4At)
|
|
446
|
+
gestureAt(p4At)
|
|
447
|
+
await settle(30)
|
|
448
|
+
view.addPointDecoration('p4', { symbol: 'square', size: 9, offset: [24, 0] })
|
|
449
|
+
const dataBeforePointLimitCommit = dataEvents.length
|
|
450
|
+
const pointLimitCommit = view.commitDecorationDraft()
|
|
451
|
+
await settle(20)
|
|
452
|
+
check('ready 之后节点被占满:提交复核 maxPerPoint 并一条都不写', pointLimitCommit.ok === false && pointLimitCommit.reason === 'limit' && view.getPointDecorations('p4').length === 2 && dataEvents.length === dataBeforePointLimitCommit, JSON.stringify({ ok: pointLimitCommit.ok, reason: pointLimitCommit.reason, decorations: view.getPointDecorations('p4').length }))
|
|
453
|
+
view.setPointDecorations('p4', [])
|
|
454
|
+
|
|
455
|
+
/* ---------- 两类同时开启:节点优先,不可用时回退线路 ---------- */
|
|
456
|
+
view.beginDecorationDraft({ target: 'both', scope: 'line', maxDistance: 20 })
|
|
457
|
+
fire('pointermove', p1At)
|
|
458
|
+
await settle(30)
|
|
459
|
+
const bothHover = lastPhase('hover')
|
|
460
|
+
check('target=both:等距时节点优先', bothHover?.hitKind === 'point' && bothHover?.pointId === 'p1' && bothHover?.target === 'both', JSON.stringify({ hitKind: bothHover?.hitKind, pointId: bothHover?.pointId, target: bothHover?.target }))
|
|
461
|
+
view.cancelDecorationDraft()
|
|
462
|
+
view.beginDecorationDraft({ target: 'both', scope: 'line', maxDistance: 20, allowedPoints: ['p2'] })
|
|
463
|
+
fire('pointermove', p1At)
|
|
464
|
+
await settle(30)
|
|
465
|
+
const fallbackHover = lastPhase('hover')
|
|
466
|
+
check('target=both:最近节点被白名单拦住时回退到线路', fallbackHover?.hitKind === 'line' && fallbackHover?.lineId !== null && fallbackHover?.blockedReason === null, JSON.stringify({ hitKind: fallbackHover?.hitKind, lineId: fallbackHover?.lineId, blocked: fallbackHover?.blockedReason }))
|
|
467
|
+
view.cancelDecorationDraft()
|
|
468
|
+
|
|
469
|
+
/* ---------- 同位置重复:节点 reject / offset ---------- */
|
|
470
|
+
view.beginDecorationDraft({ target: 'point', placement: 'offset', direction: 90, distance: 36, allowDuplicate: false, duplicateStrategy: 'reject' })
|
|
471
|
+
const hoverBeforeDuplicateReject = countPhase('hover')
|
|
472
|
+
const readyBeforeDuplicateReject = countPhase('ready')
|
|
473
|
+
fire('pointermove', p1At)
|
|
474
|
+
await settle(30)
|
|
475
|
+
check('节点重复 reject:不出现 hover,blockedReason=duplicate', countPhase('hover') === hoverBeforeDuplicateReject && view.getDecorationDraft()?.pointTargets.length === 0 && view.getDecorationDraft()?.blockedReason === 'duplicate', JSON.stringify({ hover: countPhase('hover'), blocked: view.getDecorationDraft()?.blockedReason }))
|
|
476
|
+
gestureAt(p1At)
|
|
477
|
+
await settle(30)
|
|
478
|
+
check('节点重复 reject:松手也不发 ready', countPhase('ready') === readyBeforeDuplicateReject, `${countPhase('ready')} / ${readyBeforeDuplicateReject}`)
|
|
479
|
+
view.cancelDecorationDraft()
|
|
480
|
+
view.beginDecorationDraft({ target: 'point', placement: 'offset', direction: 90, distance: 36, allowDuplicate: false, duplicateStrategy: 'offset', duplicateDistance: 8 })
|
|
481
|
+
fire('pointermove', p1At)
|
|
482
|
+
await settle(30)
|
|
483
|
+
const duplicateOffsetHover = lastPhase('hover')
|
|
484
|
+
check('节点重复 offset:自动沿原方向让开并标记 duplicateAdjusted', duplicateOffsetHover?.duplicateAdjusted === true && duplicateOffsetHover?.pointDecoration?.offset?.[0] > 36, JSON.stringify({ adjusted: duplicateOffsetHover?.duplicateAdjusted, offset: duplicateOffsetHover?.pointDecoration?.offset }))
|
|
485
|
+
gestureAt(p1At)
|
|
486
|
+
await settle(30)
|
|
487
|
+
view.commitDecorationDraft()
|
|
488
|
+
await settle(20)
|
|
489
|
+
const duplicatedP1 = view.getPointDecorations('p1')
|
|
490
|
+
check('节点重复避让后提交两份装饰', duplicatedP1.length === 2 && duplicatedP1[1].offset[0] > 36, JSON.stringify(duplicatedP1))
|
|
491
|
+
|
|
492
|
+
/* ---------- 同位置重复:线路 reject / offset ---------- */
|
|
493
|
+
view.setLineDecorations('diag', [{ position: 0.5, symbol: 'circle', size: 12 }])
|
|
494
|
+
await settle(60)
|
|
495
|
+
const diagMiddle = pointOnLine('diag', 0.5)
|
|
496
|
+
view.beginDecorationDraft({ target: 'line', scope: 'line', allowDuplicate: false, duplicateStrategy: 'reject' })
|
|
497
|
+
const hoverBeforeLineDuplicate = countPhase('hover')
|
|
498
|
+
fire('pointermove', diagMiddle)
|
|
499
|
+
await settle(30)
|
|
500
|
+
check('线路重复 reject:不触发 hover,blockedReason=duplicate', countPhase('hover') === hoverBeforeLineDuplicate && view.getDecorationDraft()?.blockedReason === 'duplicate', JSON.stringify({ hover: countPhase('hover'), before: hoverBeforeLineDuplicate, blocked: view.getDecorationDraft()?.blockedReason, allowDuplicate: view.getDecorationDraft()?.allowDuplicate, strategy: view.getDecorationDraft()?.duplicateStrategy, hitKind: view.getDecorationDraft()?.hitKind, lineId: view.getDecorationDraft()?.lineId, position: view.getDecorationDraft()?.position, existingCount: view.getDecorationDraft()?.targets?.[0]?.existingCount, lastHoverPosition: lastPhase('hover')?.position, lineDecorations: view.getLineDecorations('diag') }))
|
|
501
|
+
view.cancelDecorationDraft()
|
|
502
|
+
view.beginDecorationDraft({ target: 'line', scope: 'line', allowDuplicate: false, duplicateStrategy: 'offset', duplicateDistance: 8 })
|
|
503
|
+
fire('pointermove', diagMiddle)
|
|
504
|
+
await settle(30)
|
|
505
|
+
const lineDuplicateHover = lastPhase('hover')
|
|
506
|
+
check('线路重复 offset:沿线自动让开并标记 duplicateAdjusted', lineDuplicateHover?.duplicateAdjusted === true && Math.abs((lineDuplicateHover?.position ?? 0.5) - 0.5) > 0.0001, JSON.stringify({ adjusted: lineDuplicateHover?.duplicateAdjusted, position: lineDuplicateHover?.position }))
|
|
507
|
+
view.cancelDecorationDraft()
|
|
508
|
+
view.setLineDecorations('diag', [])
|
|
509
|
+
|
|
510
|
+
/* ---------- 节点装饰增删改 ---------- */
|
|
511
|
+
const p1ListCopy = view.getPointDecorations('p1')
|
|
512
|
+
const p1Updated = view.updatePointDecoration('p1', 0, { size: 27, color: '#ff6644' })
|
|
513
|
+
const p1Removed = view.removePointDecoration('p1', 0)
|
|
514
|
+
check('节点装饰 get / update / remove 走同一份 decorations', p1ListCopy.length === 2 && p1Updated?.size === 27 && p1Updated.color === '#ff6644' && p1Removed?.size === 27 && view.getPointDecorations('p1').length === 1, JSON.stringify({ before: p1ListCopy.length, updated: p1Updated, removed: p1Removed?.size, after: view.getPointDecorations('p1').length }))
|
|
515
|
+
view.setPointDecorations('p1', [])
|
|
516
|
+
view.setPointDecorations('p2', [])
|
|
517
|
+
view.setPointDecorations('p3', [])
|
|
518
|
+
|
|
519
|
+
/* ---------- 重复开启与换数据 ---------- */
|
|
520
|
+
view.beginDecorationDraft({ scope: 'line' })
|
|
521
|
+
const startsBefore = countPhase('start')
|
|
522
|
+
view.beginDecorationDraft({ scope: 'channel', size: 18 })
|
|
523
|
+
check('重复 begin 会替换上一份草稿并补一条 start', countPhase('start') === startsBefore + 1 && view.getDecorationDraft()?.scope === 'channel' && view.getDecorationDraft()?.size === 18, JSON.stringify({ starts: countPhase('start'), scope: view.getDecorationDraft()?.scope }))
|
|
524
|
+
fire('pointermove', pointOnLine('diag'))
|
|
525
|
+
await settle(20)
|
|
526
|
+
view.updateOptions({ points: points.map(point => ({ ...point })) })
|
|
527
|
+
await settle(80)
|
|
528
|
+
check('整份换数据后过期装饰草稿被静默清掉', view.getDecorationDraft() === null, '')
|
|
529
|
+
|
|
530
|
+
/* ---------- 装饰随节点 / 线路移动 ---------- */
|
|
531
|
+
view.setPointDecorations('p1', [{
|
|
532
|
+
anchor: 'offset',
|
|
533
|
+
offset: [24, -12],
|
|
534
|
+
symbol: 'circle',
|
|
535
|
+
size: 15,
|
|
536
|
+
color: '#ff00ff'
|
|
537
|
+
}])
|
|
538
|
+
view.setLineDecorations('diag', [{
|
|
539
|
+
anchor: 'follow',
|
|
540
|
+
position: 0.5,
|
|
541
|
+
symbol: 'square',
|
|
542
|
+
size: 15,
|
|
543
|
+
color: '#00ffff'
|
|
544
|
+
}])
|
|
545
|
+
await settle(100)
|
|
546
|
+
const followBefore = view.snapshotScene({ background: false })
|
|
547
|
+
const nodeDecorBefore = followBefore.decorations.find(item => item.fill?.includes('255,0,255'))
|
|
548
|
+
const lineDecorBefore = followBefore.decorations.find(item => item.fill?.includes('0,255,255'))
|
|
549
|
+
const p1BeforeScreen = view.project(0, 0)
|
|
550
|
+
const p1AfterScreen = view.project(120, 40)
|
|
551
|
+
const followDelta = [p1AfterScreen[0] - p1BeforeScreen[0], p1AfterScreen[1] - p1BeforeScreen[1]]
|
|
552
|
+
view.updatePoint('p1', { x: 120, y: 40 })
|
|
553
|
+
await settle(120)
|
|
554
|
+
const followAfter = view.snapshotScene({ background: false })
|
|
555
|
+
const nodeDecorAfter = followAfter.decorations.find(item => item.fill?.includes('255,0,255'))
|
|
556
|
+
const lineDecorAfter = followAfter.decorations.find(item => item.fill?.includes('0,255,255'))
|
|
557
|
+
const nodeMoved = Boolean(nodeDecorBefore && nodeDecorAfter) &&
|
|
558
|
+
Math.abs(nodeDecorAfter.x - nodeDecorBefore.x - followDelta[0]) < 1 &&
|
|
559
|
+
Math.abs(nodeDecorAfter.y - nodeDecorBefore.y - followDelta[1]) < 1
|
|
560
|
+
const lineMoved = Boolean(lineDecorBefore && lineDecorAfter) &&
|
|
561
|
+
Math.abs(lineDecorAfter.x - lineDecorBefore.x - followDelta[0] / 2) < 1 &&
|
|
562
|
+
Math.abs(lineDecorAfter.y - lineDecorBefore.y - followDelta[1] / 2) < 1
|
|
563
|
+
check('节点移动时 offset 装饰一起移动(保持同样的屏幕偏移)', nodeMoved, JSON.stringify({ before: nodeDecorBefore, after: nodeDecorAfter, delta: followDelta }))
|
|
564
|
+
check('节点移动带着线路重排时 follow 装饰跟到新的线路中点', lineMoved, JSON.stringify({ before: lineDecorBefore, after: lineDecorAfter, delta: followDelta }))
|
|
565
|
+
view.setPointDecorations('p1', [])
|
|
566
|
+
view.setLineDecorations('diag', [])
|
|
567
|
+
|
|
568
|
+
view.destroy()
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
results.push(`ERROR ${error && error.stack ? error.stack : String(error)}`)
|
|
572
|
+
}
|
|
573
|
+
document.querySelector('#out').textContent = results.join('\n')
|
|
574
|
+
document.title = results.some(line => line.startsWith('FAIL') || line.startsWith('ERROR')) ? 'decoration FAIL' : 'decoration PASS'
|
|
575
|
+
</script>
|
|
576
|
+
</body>
|
|
577
|
+
</html>
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// 装饰本身画没画、跟不跟着线走由 selftest-features.html 用像素验证,这里只管 demo 的这段流程。
|
|
15
15
|
import { createApp } from 'vue'
|
|
16
16
|
import App from '/src/App.vue'
|
|
17
|
+
import { FEATURE_NAMES } from '/src/gis-view/features.js'
|
|
17
18
|
const results = []
|
|
18
19
|
const check = (name, ok, extra = '') => {
|
|
19
20
|
results.push(`${ok ? 'PASS' : 'FAIL'} ${name}${extra === '' ? '' : ` — ${extra}`}`)
|
|
@@ -323,7 +324,7 @@
|
|
|
323
324
|
}
|
|
324
325
|
await pickSource('basemap')
|
|
325
326
|
}
|
|
326
|
-
// 这一轮新增的「开关 · 历史」页签:功能开关(预设 +
|
|
327
|
+
// 这一轮新增的「开关 · 历史」页签:功能开关(预设 + 全部复选框)、历史(开关 / 上限 / 撤销 / 重做 / 清空 +
|
|
327
328
|
// 步骤列表)与一条 view.on('*') 的事件计数。面板 DOM 与实例接口两边都核对:勾选框真的调到 setFeatures、
|
|
328
329
|
// 按钮真的调到 undo() / redo()(按 getHistoryState() 的读数说话,不靠读文字)
|
|
329
330
|
const opsTab = [...document.querySelectorAll('.demo-tabs button')].find(item => item.textContent.includes('开关 · 历史'))
|
|
@@ -340,7 +341,7 @@
|
|
|
340
341
|
check('点按钮弹出功能开关与历史面板', Boolean(opsPanel))
|
|
341
342
|
if (opsPanel) {
|
|
342
343
|
const boxes = [...opsPanel.querySelectorAll('.demo-feature-toggle input')]
|
|
343
|
-
check(
|
|
344
|
+
check(`功能开关按 FEATURE_NAMES 列了 ${FEATURE_NAMES.length} 个复选框`, boxes.length === FEATURE_NAMES.length, `${boxes.length} 个`)
|
|
344
345
|
const opsView = demoState()?.viewRef
|
|
345
346
|
const dragBox = [...opsPanel.querySelectorAll('.demo-feature-toggle')].find(item => item.textContent.includes('拖动节点'))?.querySelector('input')
|
|
346
347
|
if (opsView && dragBox) {
|
|
@@ -777,9 +778,9 @@
|
|
|
777
778
|
useButton.click()
|
|
778
779
|
await wait(160)
|
|
779
780
|
check('点「use(插件)」:插件立即执行、面板给出已挂载读数', /已挂上/.test(pluginHint()), pluginHint())
|
|
780
|
-
pluginView.
|
|
781
|
+
pluginView.selectFeatures(null)
|
|
781
782
|
const pluginIds = (pluginView.getData().points ?? []).slice(0, 2).map(item => item.id)
|
|
782
|
-
pluginView.
|
|
783
|
+
pluginView.selectFeatures(pluginIds[0])
|
|
783
784
|
await wait(220)
|
|
784
785
|
const countText = pluginHint()
|
|
785
786
|
check('插件订阅 selectChange 生效:选中集变一次就记一笔', /由 use\(\) 挂上的插件记的/.test(countText) && /第 [12] 次/.test(countText), countText)
|
|
@@ -787,7 +788,7 @@
|
|
|
787
788
|
await wait(200)
|
|
788
789
|
const offText = pluginHint()
|
|
789
790
|
check('点「卸载插件」:插件自己的 destroy 解绑订阅', /已卸载/.test(offText), offText)
|
|
790
|
-
pluginView.
|
|
791
|
+
pluginView.selectFeatures(pluginIds[1])
|
|
791
792
|
await wait(220)
|
|
792
793
|
check('卸载之后不再计数(订阅确实解绑了)', pluginHint() === offText && /已卸载/.test(pluginHint()), pluginHint())
|
|
793
794
|
}
|