gis-view-webgl2 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -11
- package/dist/gis-view.css +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +445 -241
- package/dist/index.js +1 -1
- package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +65 -498
- package/docs/GisView.md +1 -1
- package/docs/GisView/345/256/236/347/216/260/345/216/237/347/220/206.md +1 -1
- package/docs/ai/06-/344/274/232/350/257/235/347/264/242/345/274/225/344/270/216/345/244/215/346/237/245/346/226/271/346/263/225.md +114 -478
- package/docs/demos/BasicDemo.vue +144 -27
- package/docs/demos/BatchAddDemo.vue +133 -7
- package/docs/demos/ChannelDemo.vue +61 -0
- package/docs/demos/ConnectDemo.vue +164 -33
- package/docs/demos/CopyMergeDemo.vue +144 -42
- package/docs/demos/EventDemo.vue +29 -24
- package/docs/demos/LabelDemo.vue +5 -5
- package/docs/demos/api-index.js +53 -41
- package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +1 -1
- package/docs/demos/gis-view-demo/GisViewDemoStage.vue +1 -1
- package/docs/demos/gis-view-demo/panels/DataPanels.vue +1 -1
- package/docs/demos/gis-view-demo/useGisViewDemo.js +31 -33
- package/docs/format/02-/350/212/202/347/202/271/344/270/216/347/272/277/350/267/257.md +1 -0
- package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +3 -0
- package/docs/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 +8 -7
- package/docs/from0/05-/347/233/270/346/234/272/344/270/216/344/272/244/344/272/222.md +1 -1
- package/docs/from0/07-/351/200/232/351/201/223/345/210/206/347/273/204/344/270/216/345/244/232/345/233/236/347/272/277.md +26 -10
- package/docs/index.html +136 -13
- package/docs/internals/15-04-/351/200/220/350/275/256/350/256/260/345/275/225-/347/203/255/345/212/233/345/233/276/344/270/216/346/200/247/350/203/275.md +10 -0
- package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +1 -1
- package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +82 -14
- package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +23 -17
- package/docs/manual/05-/347/224/273/350/264/250/344/270/216/346/211/271/351/207/217/346/233/264/346/226/260.md +65 -0
- package/docs/manual/06-/346/225/260/346/215/256/344/270/216/345/234/272/346/231/257/346/233/264/346/226/260.md +9 -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 +100 -40
- package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +11 -7
- package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +66 -6
- 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 +226 -493
- package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +28 -5
- package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +46 -14
- package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +5 -2
- package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +34 -29
- package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +30 -22
- package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +168 -81
- 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 +1 -1
- package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +5 -3
- 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 +9 -6
- package/docs/steps/README.md +201 -120
- package/docs/steps/bench-scale.html +1 -1
- package/docs/steps/index.html +222 -6
- package/docs/steps/selftest-actions.html +26 -25
- package/docs/steps/selftest-data.html +1 -1
- package/docs/steps/selftest-demos.html +290 -16
- package/docs/steps/selftest-docs.html +17 -6
- package/docs/steps/selftest-features.html +594 -54
- package/docs/steps/selftest-full.html +3 -3
- package/docs/steps/selftest-labels.html +82 -1
- package/docs/steps/selftest-perf.html +62 -7
- package/docs/steps/selftest-select.html +71 -6
- package/docs/steps/step-05-pick.js +4 -8
- package/docs/steps/step-07-channels.js +3 -5
- package/docs/steps/step-08-labels.js +3 -3
- package/docs/steps/step-10-full.js +3 -3
- package/docs/steps/step-12-events.js +41 -24
- package/docs//344/273/2160/345/210/260/345/256/214/346/225/264/347/232/204/345/256/236/347/216/260.md +1 -0
- package/package.json +1 -1
- package/docs/ai/01-/350/265/267/346/272/220/344/270/216/350/277/231/344/273/275/346/261/207/346/200/273/347/232/204/347/224/261/346/235/245.md +0 -29
- package/docs/ai/02-/346/214/211/345/206/205/345/256/271/345/210/206/347/261/273-/350/277/231/344/270/200/350/267/257/351/203/275/345/234/250/346/224/271/344/273/200/344/271/210.md +0 -66
- package/docs/ai/03-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/270/200/357/274/211/357/274/232/351/200/211/345/236/213/351/200/232/351/201/223/344/270/216/346/240/267/345/274/217.md +0 -178
- package/docs/ai/04-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/272/214/357/274/211/357/274/232/344/272/244/344/272/222/346/200/247/350/203/275/344/270/216/346/225/260/346/215/256.md +0 -140
- package/docs/ai/05-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/270/211/357/274/211/357/274/232/345/256/277/344/270/273/346/227/201/346/224/257/344/270/216/346/224/266/345/217/243.md +0 -43
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
onClick: payload => push('click', payload),
|
|
43
43
|
onDblClick: payload => push('dblclick', payload),
|
|
44
44
|
onSelect: payload => push('select', payload),
|
|
45
|
-
onPointerDown:
|
|
46
|
-
onPointerUp:
|
|
47
|
-
onContextMenu:
|
|
45
|
+
onPointerDown: payload => push('down', payload),
|
|
46
|
+
onPointerUp: payload => push('up', payload),
|
|
47
|
+
onContextMenu: payload => push('contextmenu', payload),
|
|
48
48
|
onChannelDraft: payload => push('draft', payload),
|
|
49
49
|
onChannelDraftClick: payload => push('draftclick', payload)
|
|
50
50
|
})
|
|
@@ -536,7 +536,7 @@
|
|
|
536
536
|
const labelMenuEvent = new MouseEvent('contextmenu', { bubbles: true, cancelable: true, button: 2, clientX: menuRect.left + 4, clientY: menuRect.top + 4 })
|
|
537
537
|
firstMenu.dispatchEvent(labelMenuEvent)
|
|
538
538
|
const menuPayload = menuPayloads[menuPayloads.length - 1]
|
|
539
|
-
check('右键标签发 labelContextMenu:哪一条 / 内容 / 挂在谁 / 指针位置都在,系统菜单被拦下', menuPayload?.labelId === 'default' && menuPayload?.label?.content === '右键站' && menuPayload?.parent?.id === 'm1' && Math.abs(menuPayload.clientX - (menuRect.left + 4)) < 0.01 && labelMenuEvent.defaultPrevented === true, JSON.stringify({ labelId: menuPayload?.labelId, content: menuPayload?.label?.content, parent: menuPayload?.parent?.id, x: menuPayload?.clientX, prevented: labelMenuEvent.defaultPrevented }))
|
|
539
|
+
check('右键标签发 labelContextMenu:哪一条 / 内容 / 挂在谁 / 指针位置都在,系统菜单被拦下', menuPayload?.labelId === 'default' && menuPayload?.label?.content === '右键站' && menuPayload?.parent?.id === 'm1' && Math.abs(menuPayload.originalEvent.clientX - (menuRect.left + 4)) < 0.01 && menuPayload.clientX === undefined && labelMenuEvent.defaultPrevented === true, JSON.stringify({ labelId: menuPayload?.labelId, content: menuPayload?.label?.content, parent: menuPayload?.parent?.id, x: menuPayload?.originalEvent?.clientX, prevented: labelMenuEvent.defaultPrevented }))
|
|
540
540
|
check('右键载荷里的 state 与偏移读数和点选同一套', menuPayload?.state === null && Array.isArray(menuPayload?.offset) && menuPayload.offset[0] === 0 && menuPayload.offset[1] === 0, JSON.stringify({ state: menuPayload?.state, offset: menuPayload?.offset }))
|
|
541
541
|
}
|
|
542
542
|
menuView.updateOptions({ preventContextMenu: false })
|
|
@@ -1363,6 +1363,87 @@
|
|
|
1363
1363
|
check('线路标签同样支持 labelRender: canvas(优化档、6 条全部进画布)', Number(lineCanvas?.dataset.labelCount || 0) === 6 && (lineCanvasLayer?.querySelectorAll('.gis-view__label').length ?? 0) === 0, `画布 ${lineCanvas?.dataset.labelCount ?? 0} 条 / DOM ${lineCanvasLayer?.querySelectorAll('.gis-view__label').length ?? 0} 条`)
|
|
1364
1364
|
canvasLineView.destroy()
|
|
1365
1365
|
canvasLineHost.remove()
|
|
1366
|
+
// 32. 中心点 API / selectionOnTop / 标签层级 / 跟随节点 / 字号缩放
|
|
1367
|
+
const layerHost = document.createElement('div')
|
|
1368
|
+
layerHost.style.cssText = 'width:640px;height:380px'
|
|
1369
|
+
document.body.append(layerHost)
|
|
1370
|
+
const layerView = createGisView(layerHost, {
|
|
1371
|
+
points: [
|
|
1372
|
+
{ id: 'layer-low', x: 0, y: 0, name: '低层', zIndex: 0 },
|
|
1373
|
+
{ id: 'layer-occluder', x: 30, y: 0, name: '遮挡', zIndex: 20, labelVisible: false },
|
|
1374
|
+
{ id: 'layer-high', x: 200, y: 0, name: '高层', zIndex: 20 },
|
|
1375
|
+
{ id: 'layer-far', x: -100, y: -100, name: '端点', zIndex: 0, labelVisible: false }
|
|
1376
|
+
],
|
|
1377
|
+
lines: [{ id: 'layer-line', from: 'layer-low', to: 'layer-far', name: '选中线' }],
|
|
1378
|
+
channels: [{ id: 'layer-channel', from: 'layer-low', to: 'layer-far', lineIds: ['layer-line'] }],
|
|
1379
|
+
initialView: { scale: 1, center: [0, 0] },
|
|
1380
|
+
zoomBaseScale: 1,
|
|
1381
|
+
label: { enabled: true, content: '{name}', followNodeZIndex: true, maxCount: 20 },
|
|
1382
|
+
showGrid: false
|
|
1383
|
+
})
|
|
1384
|
+
await settle(320)
|
|
1385
|
+
const layerLabel = index => layerHost.querySelector(`.gis-view__label[data-label-kind='point'][data-label-index='${index}']`)
|
|
1386
|
+
check('followNodeZIndex:低层标签先被更高层节点的图标盖住', !layerLabel(0) && Boolean(layerLabel(2)), `低层 ${layerLabel(0) ? '在' : '不在'} / 高层 ${layerLabel(2) ? '在' : '不在'}`)
|
|
1387
|
+
const lowLabelInfo = layerView.getLabels('point', 0)?.[0]
|
|
1388
|
+
const highLabelInfo = layerView.getLabels('point', 2)?.[0]
|
|
1389
|
+
check('getLabels 报出生效层级与跟随开关', lowLabelInfo?.zIndex === 0 && lowLabelInfo?.followNodeZIndex === true && highLabelInfo?.zIndex === 20, JSON.stringify({ low: lowLabelInfo && { zIndex: lowLabelInfo.zIndex, follow: lowLabelInfo.followNodeZIndex }, high: highLabelInfo && { zIndex: highLabelInfo.zIndex, follow: highLabelInfo.followNodeZIndex } }))
|
|
1390
|
+
layerView.updateLabel('point', 'layer-low', { zIndex: 40 })
|
|
1391
|
+
await settle(300)
|
|
1392
|
+
const explicitLow = layerLabel(0)
|
|
1393
|
+
const highNode = layerLabel(2)
|
|
1394
|
+
check('显式 zIndex 压过跟随层级:低层标签重新出现且 CSS 层级更高', Boolean(explicitLow) && Number(getComputedStyle(explicitLow).zIndex) > Number(getComputedStyle(highNode).zIndex), `低层 ${explicitLow ? getComputedStyle(explicitLow).zIndex : '不在'} / 高层 ${highNode ? getComputedStyle(highNode).zIndex : '不在'}`)
|
|
1395
|
+
layerView.updateLabel('point', 'layer-low', { zIndex: null })
|
|
1396
|
+
await settle(300)
|
|
1397
|
+
check('显式层级写回 null 后恢复跟随节点层级', !layerLabel(0) && layerView.getLabels('point', 0)?.[0]?.zIndex === 0, JSON.stringify(layerView.getLabels('point', 0)?.[0] ?? null))
|
|
1398
|
+
layerView.updatePoint('layer-occluder', { zIndex: 0 })
|
|
1399
|
+
await settle(320)
|
|
1400
|
+
check('节点样式链的 zIndex 变了会触发标签重排:遮挡消失后低层标签回来', Boolean(layerLabel(0)) && Number(getComputedStyle(layerLabel(0)).zIndex) === 1000000, layerLabel(0) ? getComputedStyle(layerLabel(0)).zIndex : '没有标签')
|
|
1401
|
+
layerView.selectFeatures(['layer-line'])
|
|
1402
|
+
await settle(120)
|
|
1403
|
+
const lineScene = layerView.snapshotScene()
|
|
1404
|
+
check('selectionOnTop:选中线路在普通节点之后、两端节点之前', lineScene.raisedLines.length === 1 && lineScene.lines.length === 0 && lineScene.points.length === 2 && lineScene.raised.length === 2, JSON.stringify({ raisedLines: lineScene.raisedLines.length, lines: lineScene.lines.length, points: lineScene.points.length, raised: lineScene.raised.length }))
|
|
1405
|
+
layerView.deselect()
|
|
1406
|
+
layerView.selectFeatures({ type: 'channel', channelId: 'layer-channel' })
|
|
1407
|
+
await settle(120)
|
|
1408
|
+
const channelScene = layerView.snapshotScene()
|
|
1409
|
+
check('selectionOnTop:选中通道整条抬层,两端节点仍压在线路上', channelScene.raisedLines.length === 1 && channelScene.lines.length === 0 && channelScene.raised.length === 2, JSON.stringify({ raisedLines: channelScene.raisedLines.length, lines: channelScene.lines.length, raised: channelScene.raised.length }))
|
|
1410
|
+
const labelScaleHost = document.createElement('div')
|
|
1411
|
+
labelScaleHost.style.cssText = 'width:640px;height:380px'
|
|
1412
|
+
document.body.append(labelScaleHost)
|
|
1413
|
+
const labelScaleView = createGisView(labelScaleHost, {
|
|
1414
|
+
points: [{ id: 'zoom-point', x: 0, y: 0, name: '缩放' }],
|
|
1415
|
+
initialView: { scale: 1, center: [0, 0] },
|
|
1416
|
+
zoomBaseScale: 1,
|
|
1417
|
+
label: { enabled: true, content: '{name}', maxCount: 1, style: { fontSize: '10px' }, zoomScale: 0 },
|
|
1418
|
+
showGrid: false
|
|
1419
|
+
})
|
|
1420
|
+
await settle(260)
|
|
1421
|
+
const labelScaleNode = () => labelScaleHost.querySelector(`.gis-view__label[data-label-kind='point'][data-label-index='0']`)
|
|
1422
|
+
const labelScaleFontSize = () => labelScaleNode() ? parseFloat(getComputedStyle(labelScaleNode()).fontSize) : 0
|
|
1423
|
+
const centerCopy = labelScaleView.getCenter()
|
|
1424
|
+
centerCopy[0] += 999
|
|
1425
|
+
const centerKept = labelScaleView.getCenter()
|
|
1426
|
+
const setCenterReturn = labelScaleView.setCenter([120, -80])
|
|
1427
|
+
const centerAfterSet = labelScaleView.getCenter()
|
|
1428
|
+
const centerScale = labelScaleView.getCamera().scale
|
|
1429
|
+
check('getCenter / setCenter:返回副本、只改中心且可链式调用', Math.abs(centerKept[0]) < 1e-6 && Math.abs(centerKept[1]) < 1e-6 && setCenterReturn === labelScaleView && Math.abs(centerAfterSet[0] - 120) < 1e-6 && Math.abs(centerAfterSet[1] + 80) < 1e-6 && Math.abs(centerScale - 1) < 1e-6, JSON.stringify({ kept: centerKept, set: centerAfterSet, scale: centerScale }))
|
|
1430
|
+
check('zoomScale: 0 时标签字号固定为屏幕字号(比例 2 仍是 10px)', labelScaleFontSize() === 10, `${labelScaleFontSize()}px`)
|
|
1431
|
+
labelScaleView.setCamera({ scale: 2, center: [0, 0] })
|
|
1432
|
+
await settle(320)
|
|
1433
|
+
check('zoomScale: 0 放大到 2 倍后字号不动', labelScaleFontSize() === 10, `${labelScaleFontSize()}px`)
|
|
1434
|
+
labelScaleView.updateLabel('point', 'zoom-point', { zoomScale: 1 })
|
|
1435
|
+
await settle(320)
|
|
1436
|
+
check('zoomScale: 1 时字号按相机比例放大到 20px', labelScaleFontSize() === 20 && labelScaleView.getLabels('point', 0)?.[0]?.zoomScale === 1, `${labelScaleFontSize()}px / ${labelScaleView.getLabels('point', 0)?.[0]?.zoomScale}`)
|
|
1437
|
+
labelScaleView.setCamera({ scale: 4, center: [0, 0] })
|
|
1438
|
+
await settle(360)
|
|
1439
|
+
check('zoomScale: 1 时字号继续跟随到 40px', labelScaleFontSize() === 40, `${labelScaleFontSize()}px`)
|
|
1440
|
+
labelScaleView.updateLabel('point', 'zoom-point', { zoomScale: 0 })
|
|
1441
|
+
await settle(320)
|
|
1442
|
+
check('zoomScale 改回 0 后恢复固定字号', labelScaleFontSize() === 10, `${labelScaleFontSize()}px`)
|
|
1443
|
+
labelScaleView.destroy()
|
|
1444
|
+
labelScaleHost.remove()
|
|
1445
|
+
layerView.destroy()
|
|
1446
|
+
layerHost.remove()
|
|
1366
1447
|
richView.destroy()
|
|
1367
1448
|
reRichView.destroy()
|
|
1368
1449
|
richHost.remove()
|
|
@@ -270,9 +270,37 @@
|
|
|
270
270
|
check('数据包围盒缓存:追加到范围外的节点会立即扩大读数',
|
|
271
271
|
boundsAfterAppend.minX === -600 && boundsAfterAppend.minY === -700,
|
|
272
272
|
JSON.stringify(boundsAfterAppend))
|
|
273
|
-
boundsView.destroy()
|
|
274
|
-
boundsHost.remove()
|
|
275
|
-
//
|
|
273
|
+
boundsView.destroy()
|
|
274
|
+
boundsHost.remove()
|
|
275
|
+
// 节点 id 索引:整份 refresh 后仍能解析 from / to;定向改 id 时旧键必须立即失效、新键马上可绑定
|
|
276
|
+
const idIndexHost = document.createElement('div')
|
|
277
|
+
document.querySelector('#map').append(idIndexHost)
|
|
278
|
+
const idIndexView = createGisView(idIndexHost, {
|
|
279
|
+
points: [{ id: 'ida', x: 0, y: 0 }, { id: 'idb', x: 100, y: 0 }],
|
|
280
|
+
lines: [{ id: 'idl', from: 'ida', to: 'idb' }],
|
|
281
|
+
width: 320,
|
|
282
|
+
height: 200
|
|
283
|
+
})
|
|
284
|
+
idIndexView.refresh()
|
|
285
|
+
// 隐藏节点插到最前面:可见 id 顺序不变、解析下标整体后移,缓存必须按新下标重建
|
|
286
|
+
idIndexView.updateData({
|
|
287
|
+
points: [{ id: 'hidden', x: -50, y: 0, visible: false }, { id: 'ida', x: 0, y: 0 }, { id: 'idb', x: 100, y: 0 }]
|
|
288
|
+
})
|
|
289
|
+
const idLineAfterShift = idIndexView.getLine('idl')
|
|
290
|
+
check('节点 id 索引:隐藏节点增删导致可见下标平移时仍按新下标解析',
|
|
291
|
+
idLineAfterShift?.path?.length === 2 && idLineAfterShift.path[0][0] === 0 && idLineAfterShift.path[1][0] === 100,
|
|
292
|
+
JSON.stringify({ path: idLineAfterShift?.path ?? null }))
|
|
293
|
+
const idLineAfterRefresh = idIndexView.getLine('idl')
|
|
294
|
+
idIndexView.updatePoint('ida', { id: 'ida-renamed' })
|
|
295
|
+
const idLineOldKey = idIndexView.getLine('idl')
|
|
296
|
+
const idLineAdded = idIndexView.addLine({ id: 'idl-new', from: 'ida-renamed', to: 'idb' })
|
|
297
|
+
const idLineNewKey = idIndexView.getLine('idl-new')
|
|
298
|
+
check('节点 id 索引:整份 refresh 后绑定线路仍可解析;updatePoint 改 id 后旧键立即失效、新键马上可绑定',
|
|
299
|
+
idLineAfterRefresh?.path?.length === 2 && (idLineOldKey?.path?.length ?? 0) < 2 && idLineAdded.ok && idLineNewKey?.path?.length === 2 && idLineNewKey.path[0][0] === 0 && idLineNewKey.path[1][0] === 100,
|
|
300
|
+
JSON.stringify({ refresh: idLineAfterRefresh?.path ?? null, oldAfterRename: idLineOldKey?.path ?? null, added: idLineAdded.ok, renamed: idLineNewKey?.path ?? null }))
|
|
301
|
+
idIndexView.destroy()
|
|
302
|
+
idIndexHost.remove()
|
|
303
|
+
// 两万个点会走发散渲染:等它铺完再拾取(没铺到的节点按设计不参与拾取)
|
|
276
304
|
const readyDeadline = now() + 8000
|
|
277
305
|
while (!big.isLayoutReady() && now() < readyDeadline) {
|
|
278
306
|
await sleep(50)
|
|
@@ -319,9 +347,36 @@
|
|
|
319
347
|
big.removePoints(['bench-new', 'bench-new2'])
|
|
320
348
|
const channelRemoveMs = now() - channelRemoveStarted
|
|
321
349
|
diag('通道在场时补 2 个点', `${channelAddMs.toFixed(1)} ms(这一轮之前 40 ms 上下)`)
|
|
322
|
-
diag('通道在场时删这 2 个点', `${channelRemoveMs.toFixed(1)} ms(这一轮之前 35 ms 上下)`)
|
|
323
|
-
check('通道在场时的增删:通道成员表不动、点真的加上又删掉', big.getData().points.length === pointCount && big.getChannel('bench-ch').lineIndexes.length === 2, JSON.stringify({ points: big.getData().points.length, members: big.getChannel('bench-ch').lineIndexes }))
|
|
324
|
-
|
|
350
|
+
diag('通道在场时删这 2 个点', `${channelRemoveMs.toFixed(1)} ms(这一轮之前 35 ms 上下)`)
|
|
351
|
+
check('通道在场时的增删:通道成员表不动、点真的加上又删掉', big.getData().points.length === pointCount && big.getChannel('bench-ch').lineIndexes.length === 2, JSON.stringify({ points: big.getData().points.length, members: big.getChannel('bench-ch').lineIndexes }))
|
|
352
|
+
// addLine 是末尾补线:已有通道声明时也走追加式补线,不能为了加一条独立线路把整份场景重排。
|
|
353
|
+
// 这里同时盯正确性(新线进场景、通道声明一条不动)与代价(最快一次明显低于整份重排)。
|
|
354
|
+
const appendLineChannelsBefore = JSON.stringify(big.getOptions().channels)
|
|
355
|
+
big.setCamera({ center: [10, 0], scale: 2 })
|
|
356
|
+
const appendLineIds = []
|
|
357
|
+
let appendLineFastestMs = Infinity
|
|
358
|
+
const appendLineStarted = now()
|
|
359
|
+
for (let index = 0; index < 5; index += 1) {
|
|
360
|
+
const callStarted = now()
|
|
361
|
+
const appendedLine = big.addLine({ from: 'p0', to: 'p1', id: `bench-added-line-${index}`, lineProps: { color: '#ff3300', width: 2 } })
|
|
362
|
+
appendLineFastestMs = Math.min(appendLineFastestMs, now() - callStarted)
|
|
363
|
+
if (appendedLine.ok) {
|
|
364
|
+
appendLineIds.push(appendedLine.lineId)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const appendLineMs = (now() - appendLineStarted) / 5
|
|
368
|
+
const appendLineScene = big.snapshotScene({ background: false })
|
|
369
|
+
const appendLinePainted = appendLineScene.lines.filter(line => line.color === 'rgba(255,51,0,1)').length
|
|
370
|
+
const appendLineRefreshStarted = now()
|
|
371
|
+
big.refresh()
|
|
372
|
+
const appendLineRefreshMs = now() - appendLineRefreshStarted
|
|
373
|
+
check('通道声明在场时 addLine 走追加式补线:5 条都进场景、通道声明不动', appendLineIds.length === 5 && appendLinePainted === 5 && JSON.stringify(big.getOptions().channels) === appendLineChannelsBefore, JSON.stringify({ ids: appendLineIds, painted: appendLinePainted }))
|
|
374
|
+
check('addLine 的追加代价明显低于整份重排(有通道声明也不会回落整份重建)', appendLineFastestMs * 5 < appendLineRefreshMs, `最快一次 ${appendLineFastestMs.toFixed(2)} ms · 平均 ${appendLineMs.toFixed(2)} ms vs 整份重排 ${appendLineRefreshMs.toFixed(1)} ms`)
|
|
375
|
+
diag('通道在场时 addLine 追加 1 条', `${appendLineMs.toFixed(2)} ms / 次(最快一次 ${appendLineFastestMs.toFixed(2)} ms)· 同一场景整份重排 ${appendLineRefreshMs.toFixed(1)} ms`)
|
|
376
|
+
for (const id of appendLineIds) {
|
|
377
|
+
big.removeLine(id)
|
|
378
|
+
}
|
|
379
|
+
big.updateOptions({ channels: [] })
|
|
325
380
|
|
|
326
381
|
/* ---------------- 分区(分块)渲染值不值得:视口变化时一帧的成本怎么变 ---------------- */
|
|
327
382
|
// 「把地图拆成一块一块、只画视口里的块」省的是「视口外那些实例的顶点变换」。
|
|
@@ -2323,7 +2378,7 @@
|
|
|
2323
2378
|
await sleep(30)
|
|
2324
2379
|
const selectedAfter = clusterView.getSelections().map(entry => entry.item?.id ?? entry.index).join(',')
|
|
2325
2380
|
check('点聚合:点击聚合只发 clusterClick,不发普通 click,选中集不变',
|
|
2326
|
-
clusterEvents === 1 && plainEvents === 0 && selectedAfter === selectedBefore && lastClusterPayload?.action?.type === 'clusterClick' && lastClusterPayload?.
|
|
2381
|
+
clusterEvents === 1 && plainEvents === 0 && selectedAfter === selectedBefore && lastClusterPayload?.action?.type === 'clusterClick' && lastClusterPayload?.type === 'cluster' && lastClusterPayload?.count > 1 && Array.isArray(lastClusterPayload?.pointIds) && lastClusterPayload?.action?.data === undefined && lastClusterPayload?.originalEvent instanceof Event,
|
|
2327
2382
|
`clusterClick=${clusterEvents} click=${plainEvents} 选中 ${selectedBefore} → ${selectedAfter} action=${lastClusterPayload?.action?.type} clickPick=${clickPick?.type ?? 'null'} clickScreen=${clickScreen.map(value => value.toFixed(1)).join(',')} client=${clickClient.map(value => value.toFixed(1)).join(',')}`)
|
|
2328
2383
|
const fitScale = clusterView.getCamera().scale
|
|
2329
2384
|
// 聚合成员在相机运动期会推迟重算;这一档没有线、又显式关掉非 MSAA 后备缓冲,
|
|
@@ -317,10 +317,60 @@
|
|
|
317
317
|
ckView.updateOptions({ selectionKinds: 'all' })
|
|
318
318
|
ckView.selectInBox({ x: 0, y: 0, width: ckBox.width, height: ckBox.height })
|
|
319
319
|
const ckBoxedInfo = ckView.getSelectionData()
|
|
320
|
-
check('selectInBox:线路框选与通道摊开沿用同一份判定(整框一次收下节点 / 独立线路 / 通道回线)', ckBoxedInfo.count === 4 && ckBoxedInfo.pointCount === 2 && ckBoxedInfo.lineCount === 2 && ckBoxedInfo.channelCount === 1, JSON.stringify({ items: ckBoxedInfo.count, points: ckBoxedInfo.pointCount, lines: ckBoxedInfo.lineCount, channels: ckBoxedInfo.channelCount }))
|
|
320
|
+
check('selectInBox:线路框选与通道摊开沿用同一份判定(整框一次收下节点 / 独立线路 / 通道回线)', ckBoxedInfo.count === 4 && ckBoxedInfo.pointCount === 2 && ckBoxedInfo.lineCount === 2 && ckBoxedInfo.channelCount === 1, JSON.stringify({ items: ckBoxedInfo.count, points: ckBoxedInfo.pointCount, lines: ckBoxedInfo.lineCount, channels: ckBoxedInfo.channelCount }))
|
|
321
321
|
ckView.destroy()
|
|
322
322
|
ckHost.remove()
|
|
323
323
|
|
|
324
|
+
/* ---------- 本轮:线路 / 通道的端点框选模式 ---------- */
|
|
325
|
+
// 半框只盖住左端点:普通 line / channel 仍按“几何沾框”命中,端点模式必须两个端点都在框里。
|
|
326
|
+
const epHost = document.createElement('div')
|
|
327
|
+
epHost.style.cssText = 'width:480px;height:320px'
|
|
328
|
+
document.body.append(epHost)
|
|
329
|
+
const epView = createGisView(epHost, {
|
|
330
|
+
points: [
|
|
331
|
+
{ id: 'ep-a', x: -130, y: 0, size: 16 },
|
|
332
|
+
{ id: 'ep-b', x: 130, y: 0, size: 16 }
|
|
333
|
+
],
|
|
334
|
+
lines: [
|
|
335
|
+
{ id: 'ep-bound', from: 'ep-a', to: 'ep-b', width: 8 },
|
|
336
|
+
{ id: 'ep-path', path: [[-80, 60], [80, 60]], width: 8 },
|
|
337
|
+
{ id: 'ep-ch-line', from: 'ep-a', to: 'ep-b', width: 8 }
|
|
338
|
+
],
|
|
339
|
+
channels: [{ id: 'ep-ch', lineIds: ['ep-ch-line'] }],
|
|
340
|
+
features: 'view',
|
|
341
|
+
selectionKinds: 'all',
|
|
342
|
+
showGrid: false,
|
|
343
|
+
fitOnInit: false,
|
|
344
|
+
initialView: { center: [0, 0], scale: 1 },
|
|
345
|
+
width: 480,
|
|
346
|
+
height: 320
|
|
347
|
+
})
|
|
348
|
+
await settle(120)
|
|
349
|
+
const epBox = epView.canvas.getBoundingClientRect()
|
|
350
|
+
const epHalf = { x: 0, y: 0, width: epBox.width / 2, height: epBox.height }
|
|
351
|
+
const epAll = { x: 0, y: 0, width: epBox.width, height: epBox.height }
|
|
352
|
+
const epInfo = () => epView.getSelectionData()
|
|
353
|
+
epView.selectInBox(epHalf, { selectionKinds: ['line'] })
|
|
354
|
+
const epLegacy = epInfo()
|
|
355
|
+
check('line 旧模式:线路几何沾到半框就选中', epLegacy.lineCount === 2 && epView.isSelected('ep-bound') && epView.isSelected('ep-path'), JSON.stringify({ lines: epLegacy.lineCount, ids: epLegacy.ids }))
|
|
356
|
+
epView.selectInBox(epHalf, { selectionKinds: ['lineEndpoints'] })
|
|
357
|
+
const epLineHalf = epInfo()
|
|
358
|
+
check('lineEndpoints:只框住一端时独立线路不选中', epLineHalf.lineCount === 0 && epLineHalf.channelCount === 0 && !epView.isSelected('ep-bound') && !epView.isSelected('ep-path'), JSON.stringify({ lines: epLineHalf.lineCount, channels: epLineHalf.channelCount, ids: epLineHalf.ids }))
|
|
359
|
+
epView.selectInBox(epAll, { selectionKinds: ['line-endpoints'] })
|
|
360
|
+
const epLineAll = epInfo()
|
|
361
|
+
check('lineEndpoints:两端都在框内才选中,纯 path 线路仍跳过(不依赖 point 档)', epLineAll.pointCount === 0 && epLineAll.lineCount === 1 && epView.isSelected('ep-bound') && !epView.isSelected('ep-path'), JSON.stringify({ points: epLineAll.pointCount, lines: epLineAll.lineCount, ids: epLineAll.ids }))
|
|
362
|
+
epView.selectInBox(epHalf, { selectionKinds: ['channel'] })
|
|
363
|
+
const epChannelHalfLegacy = epInfo()
|
|
364
|
+
check('channel 旧模式:成员回线沾到半框就选中整条通道', epChannelHalfLegacy.channelCount === 1 && epChannelHalfLegacy.lineCount === 1 && epView.isSelected('ep-ch-line'), JSON.stringify({ lines: epChannelHalfLegacy.lineCount, channels: epChannelHalfLegacy.channelCount, ids: epChannelHalfLegacy.ids }))
|
|
365
|
+
epView.selectInBox(epHalf, { selectionKinds: ['channelEndpoints'] })
|
|
366
|
+
const epChannelHalf = epInfo()
|
|
367
|
+
check('channelEndpoints:只框住一端时通道不选中', epChannelHalf.channelCount === 0 && epChannelHalf.lineCount === 0 && !epView.isSelected('ep-ch-line'), JSON.stringify({ lines: epChannelHalf.lineCount, channels: epChannelHalf.channelCount, ids: epChannelHalf.ids }))
|
|
368
|
+
epView.selectInBox(epAll, { selectionKinds: ['channel-endpoints'] })
|
|
369
|
+
const epChannelAll = epInfo()
|
|
370
|
+
check('channelEndpoints:两端节点都在框内才选中整条通道', epChannelAll.pointCount === 0 && epChannelAll.channelCount === 1 && epChannelAll.lineCount === 1 && epView.isSelected('ep-ch-line'), JSON.stringify({ points: epChannelAll.pointCount, lines: epChannelAll.lineCount, channels: epChannelAll.channelCount, ids: epChannelAll.ids }))
|
|
371
|
+
epView.destroy()
|
|
372
|
+
epHost.remove()
|
|
373
|
+
|
|
324
374
|
/* ---------- 问题 1(本轮):接口取消选中 —— deselect(单个 / 一批 / 整条通道)与「取消全部」的老写法 ---------- */
|
|
325
375
|
const dzHost = document.createElement('div')
|
|
326
376
|
dzHost.style.cssText = 'width:320px;height:200px'
|
|
@@ -497,14 +547,19 @@
|
|
|
497
547
|
check('onPointerDown / onPointerUp / onClick(通道回线):每个鼠标载荷都带 groups 与当前 group',
|
|
498
548
|
[gpDowns.at(-1), gpUps.at(-1), gpClicks.at(-1)].every(payload => payload?.groups?.length === 2 && String(payload.group?.id) === String(payload.groupId) && payload.action?.target === 'channel'),
|
|
499
549
|
JSON.stringify({ down: gpDowns.at(-1)?.groups?.length, up: gpUps.at(-1)?.groups?.length, click: gpClicks.at(-1)?.groups?.length }))
|
|
550
|
+
const gpLineChannelFields = ['channelId', 'channelIndex', 'channel', 'groupId', 'groupIndex', 'group', 'groups', 'lineIndexes', 'lineIds', 'channelContexts']
|
|
551
|
+
const gpSelectedLineIndexes = payload => (payload?.items ?? []).filter(item => item.type === 'line').map(item => item.index).join(',')
|
|
552
|
+
const gpSelectedLineIds = payload => (payload?.items ?? []).filter(item => item.type === 'line').map(item => item.item?.id).join(',')
|
|
500
553
|
const gpChannelPayloadOK = payload => String(payload?.channelId) === 'gp-ch' &&
|
|
501
554
|
payload?.groups?.length === 2 &&
|
|
502
555
|
payload?.channelContexts === undefined &&
|
|
503
556
|
String(payload.group?.id) === String(payload.groupId) &&
|
|
504
|
-
payload.
|
|
505
|
-
|
|
557
|
+
payload.lineIndexes?.join(',') === gpSelectedLineIndexes(payload) &&
|
|
558
|
+
payload.lineIds?.join(',') === gpSelectedLineIds(payload) &&
|
|
559
|
+
payload.items?.every(item => item.type !== 'line' || gpLineChannelFields.every(key => item[key] === undefined))
|
|
560
|
+
check('onSelect / onSelectChange(通道回线):单通道上下文只在顶层,items 不重复挂任何通道字段',
|
|
506
561
|
gpChannelPayloadOK(gpSelects.at(-1)) && gpChannelPayloadOK(gpChanges.at(-1)) && gpSelects.at(-1)?.items === gpChanges.at(-1)?.items,
|
|
507
|
-
JSON.stringify({ select: gpSelects.at(-1)?.channelId, change: gpChanges.at(-1)?.channelId, groups: gpSelects.at(-1)?.groups?.length,
|
|
562
|
+
JSON.stringify({ select: gpSelects.at(-1)?.channelId, change: gpChanges.at(-1)?.channelId, groups: gpSelects.at(-1)?.groups?.length, lineIds: gpSelects.at(-1)?.lineIds, items: gpSelects.at(-1)?.items?.map(item => ({ id: item.item?.id, channelId: item.channelId, groupId: item.groupId })) }))
|
|
508
563
|
check('真实点击的派发顺序:onClick → onSelect → onSelectChange',
|
|
509
564
|
gpOrder.slice(-3).join('>') === 'click>select>selectChange',
|
|
510
565
|
gpOrder.join('>'))
|
|
@@ -513,8 +568,18 @@
|
|
|
513
568
|
gpOrder.length = 0
|
|
514
569
|
gpView.selectFeatures({ type: 'channel', channelId: 'gp-ch' })
|
|
515
570
|
check('selectFeatures(整条通道):onSelect / onSelectChange 都带通道上下文,顺序是 onSelect → onSelectChange',
|
|
516
|
-
gpOrder.join('>') === 'select>selectChange' && gpChannelPayloadOK(gpSelects.at(-1)) && gpChannelPayloadOK(gpChanges.at(-1)) && gpSelects.at(-1)?.items?.length === 3,
|
|
571
|
+
gpOrder.join('>') === 'select>selectChange' && gpChannelPayloadOK(gpSelects.at(-1)) && gpChannelPayloadOK(gpChanges.at(-1)) && gpSelects.at(-1)?.items?.length === 3 && gpSelects.at(-1)?.lineIds?.join(',') === 'gp-l1,gp-l2,gp-l3',
|
|
517
572
|
JSON.stringify({ order: gpOrder, select: gpSelects.at(-1), change: gpChanges.at(-1) }))
|
|
573
|
+
const gpSelectionLines = gpView.getSelections().filter(entry => entry.type === 'line')
|
|
574
|
+
const gpOwner = gpView.getLineChannel(gpSelectionLines[0]?.index)
|
|
575
|
+
const gpSelectionData = gpView.getSelectionData()
|
|
576
|
+
check('getSelections / getSelectionData / getLineChannel:选中镜像不带通道字段,单条线路仍能反查通道与分组',
|
|
577
|
+
gpSelectionLines.length === 3 &&
|
|
578
|
+
gpSelectionLines.every(entry => gpLineChannelFields.every(key => entry[key] === undefined)) &&
|
|
579
|
+
gpSelectionData.items.every(entry => entry.type !== 'line' || gpLineChannelFields.every(key => entry[key] === undefined)) &&
|
|
580
|
+
gpSelectionData.channels.map(channel => String(channel.id)).join(',') === 'gp-ch' &&
|
|
581
|
+
String(gpOwner?.channelId) === 'gp-ch' && String(gpOwner?.channel?.id) === 'gp-ch' && gpOwner?.channelIndex === 0 && String(gpOwner?.group?.id) === 'gp-g1',
|
|
582
|
+
JSON.stringify({ lines: gpSelectionLines.length, owner: gpOwner && { channelId: gpOwner.channelId, channelIndex: gpOwner.channelIndex, groupId: gpOwner.groupId }, channels: gpSelectionData.channels.map(channel => channel.id) }))
|
|
518
583
|
gpOrder.length = 0
|
|
519
584
|
gpView.selectFeatures([
|
|
520
585
|
{ type: 'channel', channelId: 'gp-ch' },
|
|
@@ -529,7 +594,7 @@
|
|
|
529
594
|
gpSelects.at(-1).channelContexts.map(context => `${context.lineIndexes.length}/${context.lineIds.length}`).join(',') === '3/3,1/1' &&
|
|
530
595
|
gpSelects.at(-1).channelContexts.map(context => context.lineIds.join('+')).join(',') === 'gp-l1+gp-l2+gp-l3,gp-l4' &&
|
|
531
596
|
gpSelects.at(-1).items === gpChanges.at(-1)?.items &&
|
|
532
|
-
gpSelects.at(-1).items.every(item => item.type !== 'line' || (
|
|
597
|
+
gpSelects.at(-1).items.every(item => item.type !== 'line' || gpLineChannelFields.every(key => item[key] === undefined)),
|
|
533
598
|
JSON.stringify({ order: gpOrder, contexts: gpSelects.at(-1)?.channelContexts?.map(context => ({ channelId: context.channelId, lines: context.lineIds })), items: gpSelects.at(-1)?.items?.length }))
|
|
534
599
|
gpOrder.length = 0
|
|
535
600
|
const gpAt = gpView.selectAt(gpClientX, gpClientY)
|
|
@@ -85,20 +85,16 @@ export const createPickView = (target, options = {}) => {
|
|
|
85
85
|
/** 点优先于线:两个都命中也算点中节点 */
|
|
86
86
|
const pick = (x, y) => pickPoint(x, y) ?? pickLine(x, y)
|
|
87
87
|
|
|
88
|
-
/** 统一的指针载荷:命中的元素 +
|
|
88
|
+
/** 统一的指针载荷:命中的元素 + 光标位置(空白处 type 为 null、hit 为 false;按键读 originalEvent) */
|
|
89
89
|
const pointerPayload = (hit, point, event) => ({
|
|
90
90
|
type: hit ? hit.type : null,
|
|
91
91
|
index: hit ? hit.index : -1,
|
|
92
92
|
item: hit ? hit.item : null,
|
|
93
93
|
hit: Boolean(hit),
|
|
94
94
|
...(hit ?? {}),
|
|
95
|
-
button: event.button,
|
|
96
|
-
buttons: event.buttons,
|
|
97
95
|
world: core.unproject(point[0], point[1]),
|
|
98
96
|
x: point[0],
|
|
99
97
|
y: point[1],
|
|
100
|
-
clientX: event.clientX,
|
|
101
|
-
clientY: event.clientY,
|
|
102
98
|
originalEvent: event
|
|
103
99
|
})
|
|
104
100
|
const canvasPointOf = event => {
|
|
@@ -180,7 +176,7 @@ export const createPickView = (target, options = {}) => {
|
|
|
180
176
|
const point = canvasPointOf(event)
|
|
181
177
|
const hit = pick(point[0], point[1])
|
|
182
178
|
// 左键 / 中键 / 右键都发一条:使用方在按下这一刻就能拿到光标下的元素
|
|
183
|
-
listeners.onPointerDown?.(pointerPayload(hit, point, event)
|
|
179
|
+
listeners.onPointerDown?.(pointerPayload(hit, point, event))
|
|
184
180
|
if (event.button !== 0) {
|
|
185
181
|
return
|
|
186
182
|
}
|
|
@@ -215,7 +211,7 @@ export const createPickView = (target, options = {}) => {
|
|
|
215
211
|
const point = canvasPointOf(event)
|
|
216
212
|
const hit = pick(point[0], point[1])
|
|
217
213
|
if (event.button === 0 || event.button === 2) {
|
|
218
|
-
listeners.onPointerUp?.(pointerPayload(hit, point, event)
|
|
214
|
+
listeners.onPointerUp?.(pointerPayload(hit, point, event))
|
|
219
215
|
}
|
|
220
216
|
const active = pressed
|
|
221
217
|
pressed = null
|
|
@@ -253,7 +249,7 @@ export const createPickView = (target, options = {}) => {
|
|
|
253
249
|
/** 右键:点、线、空白都发一条 onContextMenu,命中什么都在载荷里 */
|
|
254
250
|
const handleContextMenu = event => {
|
|
255
251
|
const point = canvasPointOf(event)
|
|
256
|
-
listeners.onContextMenu?.(pointerPayload(pick(point[0], point[1]), point, event)
|
|
252
|
+
listeners.onContextMenu?.(pointerPayload(pick(point[0], point[1]), point, event))
|
|
257
253
|
}
|
|
258
254
|
|
|
259
255
|
// 框选矩形:一个绝对定位的 DOM 层,样式由使用方决定(组件不内置界面)
|
|
@@ -221,9 +221,9 @@ export const createChannelView = (target, options = {}) => {
|
|
|
221
221
|
onClick: payload => listeners.onClick?.(channelize(payload)),
|
|
222
222
|
onDblClick: payload => listeners.onDblClick?.(channelize(payload)),
|
|
223
223
|
onSelect: payload => listeners.onSelect?.(payload ? { ...payload, items: payload.items.map(channelize).filter(Boolean) } : null),
|
|
224
|
-
onPointerDown:
|
|
225
|
-
onPointerUp:
|
|
226
|
-
onContextMenu:
|
|
224
|
+
onPointerDown: payload => listeners.onPointerDown?.(channelize(payload)),
|
|
225
|
+
onPointerUp: payload => listeners.onPointerUp?.(channelize(payload)),
|
|
226
|
+
onContextMenu: payload => listeners.onContextMenu?.(channelize(payload))
|
|
227
227
|
})
|
|
228
228
|
|
|
229
229
|
// ---------- 新增通道:草稿状态机 ----------
|
|
@@ -301,8 +301,6 @@ export const createChannelView = (target, options = {}) => {
|
|
|
301
301
|
x: point[0],
|
|
302
302
|
y: point[1],
|
|
303
303
|
world: core.unproject(point[0], point[1]),
|
|
304
|
-
clientX: event.clientX,
|
|
305
|
-
clientY: event.clientY,
|
|
306
304
|
originalEvent: event
|
|
307
305
|
})
|
|
308
306
|
|
|
@@ -130,9 +130,9 @@ export const createLabelView = (target, options = {}) => {
|
|
|
130
130
|
renderTooltip(payload, payload ? payload.screen : null)
|
|
131
131
|
listeners.onHover?.(payload)
|
|
132
132
|
},
|
|
133
|
-
onPointerDown:
|
|
134
|
-
onPointerUp:
|
|
135
|
-
onContextMenu:
|
|
133
|
+
onPointerDown: payload => listeners.onPointerDown?.(payload),
|
|
134
|
+
onPointerUp: payload => listeners.onPointerUp?.(payload),
|
|
135
|
+
onContextMenu: payload => listeners.onContextMenu?.(payload)
|
|
136
136
|
})
|
|
137
137
|
canvas.addEventListener('pointermove', handlePointerMove)
|
|
138
138
|
// 每帧同步一次标签:注册进第 1 步的渲染任务表,画的顺序在 WebGL 之后,谁都不挡谁
|
|
@@ -82,9 +82,9 @@ export const createFullView = (target, options = {}) => {
|
|
|
82
82
|
onClick: payload => forward.onClick?.(payload),
|
|
83
83
|
onDblClick: payload => forward.onDblClick?.(payload),
|
|
84
84
|
onSelect: payload => forward.onSelect?.(payload),
|
|
85
|
-
onPointerDown:
|
|
86
|
-
onPointerUp:
|
|
87
|
-
onContextMenu:
|
|
85
|
+
onPointerDown: payload => forward.onPointerDown?.(payload),
|
|
86
|
+
onPointerUp: payload => forward.onPointerUp?.(payload),
|
|
87
|
+
onContextMenu: payload => forward.onContextMenu?.(payload),
|
|
88
88
|
onChannelDraftClick: payload => forward.onChannelDraftClick?.(payload),
|
|
89
89
|
onChannelDraft: payload => {
|
|
90
90
|
if (payload?.phase === 'commit' && payload.lineId) {
|
|
@@ -9,21 +9,20 @@
|
|
|
9
9
|
* (select 选中 / clearSelection 点空白 / addWaypoint 加点 / contextMenu / dblClick);
|
|
10
10
|
* - action.target:点在了什么上面(point / line / channel / empty)。通道回线的 type 也是 line,
|
|
11
11
|
* 但要按通道处理,所以 target 单独给 channel;
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
* -
|
|
15
|
-
* action
|
|
16
|
-
* - action.
|
|
17
|
-
* action.press(**按下时的意图**,与那一刻 onPointerDown 的 action.type 同一份)、action.duration
|
|
12
|
+
* - 命中的数据直接读载荷:节点 / 线路读 payload.item、通道回线读 payload.channel、选中集读
|
|
13
|
+
* payload.items、拐点读 payload.line,不再另打包一份 action.data;
|
|
14
|
+
* - 按键 / 修饰键 / 指针位置读 payload.originalEvent(button / ctrlKey / metaKey / shiftKey / altKey /
|
|
15
|
+
* clientX / clientY),action 只留 type / target / moved / double 这些判读;
|
|
16
|
+
* - 松开与点击载荷另带 action.press(**按下时的意图**,与那一刻 onPointerDown 的 action.type 同一份)、action.duration
|
|
18
17
|
* (按了多久,毫秒)与 action.longPress(超过 500ms 且中间没拖动过 —— 触摸 / 触控笔按它出菜单)。
|
|
19
|
-
* 中键按住就是临时平移:按下 / 松开的 action.type 都是 pan、
|
|
18
|
+
* 中键按住就是临时平移:按下 / 松开的 action.type 都是 pan、originalEvent.button 是 1,不经过 onClick。
|
|
20
19
|
*
|
|
21
20
|
* 于是「按右键弹哪一份菜单」就是 switch (payload.action.target),「拖完的是节点还是拐点」就是
|
|
22
21
|
* switch (payload.action.type),不用再各记一套 payload 形状。选中集变更也有同一套 action.type:
|
|
23
22
|
* boxSelect / toggleSelect(Ctrl 点击取反)/ select / clearSelection —— 框选没框到东西与
|
|
24
23
|
* 「点空白清空」在载荷上是同一个形状(都是 null),只有这一份补充读数能把它们分开。
|
|
25
24
|
* 标签事件(labelClick / labelDrag / labelContextMenu / labelDblClick)同一套写法:
|
|
26
|
-
* target 恒为 'label'
|
|
25
|
+
* target 恒为 'label'、标签与归属直接读载荷顶层的 label / parent。
|
|
27
26
|
*
|
|
28
27
|
* 两个只读查询:想主动问「这儿是什么」而不必先订阅一遍事件再把最近一次载荷记住,用
|
|
29
28
|
* - pickAt(clientX, clientY):参数就是鼠标事件里的 clientX / clientY,返回与指针载荷同一份结果
|
|
@@ -59,7 +58,21 @@ const withChannels = (data) => {
|
|
|
59
58
|
/** action.target -> 中文:日志里读起来不用记英文名 */
|
|
60
59
|
const TARGET_NAMES = { point: '节点', line: '线路', channel: '通道回线', label: '标签', waypoint: '拐点', selection: '选中集', empty: '空白' }
|
|
61
60
|
|
|
62
|
-
/** action.
|
|
61
|
+
/** 按 action.target 取载荷上那一份数据:节点 / 线路读 item、通道读 channel、选中集读 items */
|
|
62
|
+
const dataOfAction = (payload, action) => {
|
|
63
|
+
if (Array.isArray(payload?.items)) {
|
|
64
|
+
return payload.items
|
|
65
|
+
}
|
|
66
|
+
if (payload?.label) {
|
|
67
|
+
return payload.label
|
|
68
|
+
}
|
|
69
|
+
if (action?.target === 'waypoint') {
|
|
70
|
+
return payload.line ?? null
|
|
71
|
+
}
|
|
72
|
+
return payload?.channel ?? payload?.item ?? null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 数据那一份的名字:节点 / 线路看 name 与 id,选中集与标签给条数或内容 */
|
|
63
76
|
const nameOfData = (data) => {
|
|
64
77
|
if (Array.isArray(data)) {
|
|
65
78
|
return `${data.length} 个要素`
|
|
@@ -67,10 +80,6 @@ const nameOfData = (data) => {
|
|
|
67
80
|
if (!data || typeof data !== 'object') {
|
|
68
81
|
return ''
|
|
69
82
|
}
|
|
70
|
-
if (data.label) {
|
|
71
|
-
// 标签事件的 data 是 { label, parent }
|
|
72
|
-
return data.label.text ?? data.label.id ?? '标签'
|
|
73
|
-
}
|
|
74
83
|
return data.name ?? data.id ?? ''
|
|
75
84
|
}
|
|
76
85
|
|
|
@@ -81,19 +90,27 @@ const describeAction = (payload, phase) => {
|
|
|
81
90
|
return `${phase}:—`
|
|
82
91
|
}
|
|
83
92
|
const parts = [action.type, `→ ${TARGET_NAMES[action.target] ?? action.target}`]
|
|
84
|
-
const name = nameOfData(action
|
|
93
|
+
const name = nameOfData(dataOfAction(payload, action))
|
|
85
94
|
if (name !== '') {
|
|
86
95
|
parts.push(String(name))
|
|
87
96
|
}
|
|
88
|
-
|
|
97
|
+
const button = payload.originalEvent?.button
|
|
98
|
+
if (button === 0 || button === 1 || button === 2) {
|
|
99
|
+
parts.push(button === 2 ? '右键' : button === 1 ? '中键' : '左键')
|
|
100
|
+
}
|
|
89
101
|
if (action.moved) {
|
|
90
102
|
parts.push('拖动过')
|
|
91
103
|
}
|
|
92
104
|
if (action.double) {
|
|
93
105
|
parts.push('双击的第二下')
|
|
94
106
|
}
|
|
95
|
-
const
|
|
96
|
-
const held =
|
|
107
|
+
const event = payload.originalEvent
|
|
108
|
+
const held = [
|
|
109
|
+
event?.ctrlKey ? 'ctrl' : null,
|
|
110
|
+
event?.metaKey ? 'meta' : null,
|
|
111
|
+
event?.shiftKey ? 'shift' : null,
|
|
112
|
+
event?.altKey ? 'alt' : null
|
|
113
|
+
].filter(Boolean).join('+')
|
|
97
114
|
if (held) {
|
|
98
115
|
parts.push(`按住 ${held}`)
|
|
99
116
|
}
|
|
@@ -121,13 +138,13 @@ export const createActionView = (target, options = {}) => {
|
|
|
121
138
|
waypointEditable: true,
|
|
122
139
|
features: { pan: true, zoom: true, hover: true, select: true, contextMenu: true, keyboard: true, channelDraft: true },
|
|
123
140
|
// 下面五条是同一条路:每个回调先报一句判读,再交给页面自己那一条(日志、草稿读数都还在)
|
|
124
|
-
onPointerDown:
|
|
141
|
+
onPointerDown: payload => {
|
|
125
142
|
report(describeAction(payload, '按下'))
|
|
126
|
-
options.onPointerDown?.(payload
|
|
143
|
+
options.onPointerDown?.(payload)
|
|
127
144
|
},
|
|
128
|
-
onPointerUp:
|
|
145
|
+
onPointerUp: payload => {
|
|
129
146
|
report(describeAction(payload, '松开'))
|
|
130
|
-
options.onPointerUp?.(payload
|
|
147
|
+
options.onPointerUp?.(payload)
|
|
131
148
|
},
|
|
132
149
|
onClick: payload => {
|
|
133
150
|
// 空白处的点击载荷是 null(选中已经清空):这一下的判读在 onSelectChange 那条路上
|
|
@@ -138,13 +155,13 @@ export const createActionView = (target, options = {}) => {
|
|
|
138
155
|
report(describeAction(payload, '双击'))
|
|
139
156
|
options.onDblClick?.(payload)
|
|
140
157
|
},
|
|
141
|
-
onContextMenu:
|
|
158
|
+
onContextMenu: payload => {
|
|
142
159
|
report(describeAction(payload, '右键'))
|
|
143
|
-
options.onContextMenu?.(payload
|
|
160
|
+
options.onContextMenu?.(payload)
|
|
144
161
|
},
|
|
145
162
|
// 选中集变更:action.type 把框选 / 取反 / 选中 / 清空分开(前两种不经过 onClick)
|
|
146
163
|
onSelectChange: event => {
|
|
147
|
-
report(`选中:${event.action.type} · ${event.count} 个(节点 ${event.points} / 线路 ${event.lines} / 通道 ${event.channels}
|
|
164
|
+
report(`选中:${event.action.type} · ${event.count} 个(节点 ${event.points} / 线路 ${event.lines} / 通道 ${event.channels})→ ${event.points ? '节点' : event.lines ? '线路' : event.channels ? '通道' : '空白'}`)
|
|
148
165
|
options.onSelectChange?.(event)
|
|
149
166
|
},
|
|
150
167
|
onCameraChange: camera => options.onCameraChange?.(camera)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# 从 0 到完整的实现(索引)
|
|
2
2
|
|
|
3
3
|
这一份是「照着源码从零写一遍」的讲义:先讲清地图的模型,再一块一块把渲染、相机、解析层、通道、覆盖层、筛选与性能写出来,最后给一份可以照抄的整套代码。
|
|
4
|
+
第一次从 0 上手,建议先走 [四段学习路线、每步验收与优化顺序](steps/README.md):第 1 ~ 9 步手写主干,第 10 步接入真组件,第 11 ~ 13 步补上线配置;卡住时再回到下面章节补原理,或直接照抄 `from0/13` / `14` 的整套代码。
|
|
4
5
|
只想用组件看 [GisView.md](GisView.md);每一帧怎么实现、为什么这么写看 [实现原理](GisView实现原理.md);分步 demo(每步一个文件、页面上直接跑)在 [docs/steps](steps/README.md)。
|
|
5
6
|
性能数据与 WebGPU 的取舍见 [实现原理 18 性能实测与 WebGPU 评估](internals/18-性能实测与WebGPU评估.md#18-性能实测与-webgpu-评估)。
|
|
6
7
|
|
package/package.json
CHANGED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# 01 起源与这份汇总的由来
|
|
2
|
-
|
|
3
|
-
> 这份汇总的来源与筛选口径、选型与立项(2026-09-15)、以及按时间读这条线的三段。
|
|
4
|
-
|
|
5
|
-
> 这一页只留**重要问答**:其余会话的逐轮过程都并进了《实现原理》的逐轮记录(`docs/internals/15-*` 与 `16-*`),本页只保留值得单独读的问答与索引。
|
|
6
|
-
|
|
7
|
-
## 这份汇总怎么来的
|
|
8
|
-
|
|
9
|
-
- **来源**:Codex CLI 的本地会话文件 `C:\Users\tode\.codex\sessions\<年>\<月>\<日>\rollout-*.jsonl`(一行一条 JSON:`session_meta` 记工作目录,`event_msg` / `response_item` 记对话)。
|
|
10
|
-
- **筛选**:截至 2026-10-14 本机共 **70 个会话**;按「工作目录在本组件的相关栈上 + 首条指令点名」筛出 **39 个**(组件本体 34 个、立项与选型 2 个、旁支 3 个),时间跨度 **2026-09-15 ~ 2026-10-14**,索引见第三节。
|
|
11
|
-
- **需求正文**:每一轮的详细问题写在仓库根的 `修改.txt` / `问题.txt`(`.gitignore` 里的 `*.txt` 不进版本库),会话里通常只有一句「根据修改.txt / 问题.txt 修改组件」。所以下文的「这一轮在做什么」按用户原话 + `docs/GisView实现原理.md` 的 15.x 轮次标题整理;要逐字看某轮的问题清单,翻实现原理对应小节,或看 `git log` 里那一笔「修改gis」。
|
|
12
|
-
- **剔除口径**:`d:\work\teap-frontend-master` 下关于 `gis.vue`(AntV G6 写的前身尝试,6 个会话)不算本组件的对话;teap-gis 宿主的 2 个会话算「旁支」,因为查的正是本组件在宿主里的表现。
|
|
13
|
-
|
|
14
|
-
## 一、起源:选型与立项(2026-09-15)
|
|
15
|
-
|
|
16
|
-
| 时间 | 用户提了什么 | 结论 / 落点 |
|
|
17
|
-
| --- | --- | --- |
|
|
18
|
-
| 09-15 11:34(13 条问答) | 「区间联络线 + 线路加拐点拖拽 + 10 万节点 / 20 万线路,用什么技术」;过程中逐条问过 ECharts / AntV G6 / deck.gl / VortxGL / luma.gl / gl-matrix 的取舍、WebGL2 的优缺点与浏览器兼容、WebGL2 + HTML GUI 能否组合 | 落成「自研 WebGL2 渲染引擎 + HTML 覆盖层」的路线;同一条会话后半段让 AI 通读前身 `D:\work\teap-gis\src\components\gis-core` 并生成实现说明 md。选型叙述见 `docs/GisView实现原理.md` 前几章与 `docs/GisView.md` 的「渲染后端(WebGPU 开关)」 |
|
|
19
|
-
| 09-15 16:09(3 条) | ① 在 `components/` 下新建 gis-view(WebGL2 + WebGUI)并写「两点一线」demo;② 按 `修改.txt` 改;③ 把组件整体迁到 `D:\work\gis-view`,做成「只导出 gis-view 一个组件」的独立项目 | 组件雏形 + 独立仓库诞生;工程口径照 `virtual-table-view-vue3`(Vite + lib 构建 + `docs/demos`) |
|
|
20
|
-
|
|
21
|
-
## 四、按时间读这条线(三段)
|
|
22
|
-
|
|
23
|
-
1. **09-15 ~ 09-20:从 0 到 1。** 选型 → 立项 → 通道 / 拐点 / 分组 → 点线样式与 SVG → 鹰眼图、标签、热力图。这段的「问题」多是从 demo 里肉眼发现的渲染与交互问题。
|
|
24
|
-
2. **09-20 ~ 10-08:补全能力面 + 压性能。** 事件总线、历史、画质、批量更新、导出(图片 / 选中 / 数据)、装饰、密度场、功能开关、框选合并、宿主缩放;性能从「样式快路径」一路做到「百万级」。
|
|
25
|
-
3. **10-09 ~ 10-14:接口与收口。** 分组 / 拐点跟后端怎么交互、`BackendSyncDemo`、`exportViewportData`、方法审计与文档加厚、标签与选中态的一串 bug 收口、以及本轮(问题.txt 六条)。
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
(第一章) · [汇总目录](../AI对话汇总.md) · [02 按内容分类:这一路都在改什么 →](02-%E6%8C%89%E5%86%85%E5%AE%B9%E5%88%86%E7%B1%BB-%E8%BF%99%E4%B8%80%E8%B7%AF%E9%83%BD%E5%9C%A8%E6%94%B9%E4%BB%80%E4%B9%88.md)
|