-
Notifications
You must be signed in to change notification settings - Fork 660
feat: 新增 PointLayer shape: image 新增 allowOverlap 配置,用于控制图片之间自动避让隐藏 #2828
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,10 +1,13 @@ | ||||||
| import type { IEncodeFeature, IModel, IModelUniform, ITexture2D } from '@antv/l7-core'; | ||||||
| import { AttributeType, gl } from '@antv/l7-core'; | ||||||
| import { boundsContains, calculateCentroid, padBounds } from '@antv/l7-utils'; | ||||||
| import BaseModel from '../../core/BaseModel'; | ||||||
| import type { IPointLayerStyleOptions } from '../../core/interface'; | ||||||
| import { PointImageTriangulation } from '../../core/triangulation'; | ||||||
| import CollisionIndex from '../../utils/collision-index'; | ||||||
| import pointImageFrag from '../shaders/image/image_frag.glsl'; | ||||||
| import pointImageVert from '../shaders/image/image_vert.glsl'; | ||||||
|
|
||||||
| export default class ImageModel extends BaseModel { | ||||||
| protected get attributeLocation() { | ||||||
| return Object.assign(super.attributeLocation, { | ||||||
|
|
@@ -16,6 +19,12 @@ export default class ImageModel extends BaseModel { | |||||
|
|
||||||
| private texture: ITexture2D; | ||||||
|
|
||||||
| // 通过碰撞检测的 feature id 集合,由 filterImages() 填充;null 表示不启用过滤(allowOverlap: true) | ||||||
| public imageFilterMap: { [key: number]: boolean } | null = null; | ||||||
| private currentZoom: number = -1; | ||||||
| private extent: [[number, number], [number, number]]; | ||||||
| private preAllowOverlap: boolean = true; | ||||||
|
|
||||||
| public getUninforms(): IModelUniform { | ||||||
| // ThreeJS 图层兼容 | ||||||
| if (this.rendererService.getDirty()) { | ||||||
|
|
@@ -53,23 +62,32 @@ export default class ImageModel extends BaseModel { | |||||
| } | ||||||
|
|
||||||
| public async initModels(): Promise<IModel[]> { | ||||||
| const { allowOverlap = true } = this.layer.getLayerConfig() as IPointLayerStyleOptions; | ||||||
| this.extent = this.imageExtent(); | ||||||
| this.preAllowOverlap = allowOverlap; | ||||||
| this.iconService.on('imageUpdate', this.updateTexture); | ||||||
| this.updateTexture(); | ||||||
| return this.buildModels(); | ||||||
| } | ||||||
|
|
||||||
| public clearModels() { | ||||||
| this.texture?.destroy(); | ||||||
| this.iconService.off('imageUpdate', this.updateTexture); | ||||||
| } | ||||||
|
|
||||||
| public async buildModels(): Promise<IModel[]> { | ||||||
| const { allowOverlap = true } = this.layer.getLayerConfig() as IPointLayerStyleOptions; | ||||||
|
|
||||||
| this.initUniformsBuffer(); | ||||||
|
|
||||||
| if (!allowOverlap) { | ||||||
| this.filterImages(); | ||||||
| } else { | ||||||
| // 允许压盖时置 null,PointImageTriangulation 检测到 null 则跳过过滤 | ||||||
| this.imageFilterMap = null; | ||||||
| } | ||||||
|
|
||||||
| const model = await this.layer.buildLayerModel({ | ||||||
| moduleName: 'pointImage', | ||||||
| vertexShader: pointImageVert, | ||||||
| fragmentShader: pointImageFrag, | ||||||
| triangulation: PointImageTriangulation, | ||||||
| // 绑定 this,让 PointImageTriangulation 能读取 imageFilterMap | ||||||
| triangulation: PointImageTriangulation.bind(this), | ||||||
| defines: this.getDefines(), | ||||||
| inject: this.getInject(), | ||||||
| depth: { enable: false }, | ||||||
|
|
@@ -78,6 +96,38 @@ export default class ImageModel extends BaseModel { | |||||
| return [model]; | ||||||
| } | ||||||
|
|
||||||
| public async needUpdate(): Promise<boolean> { | ||||||
| const { allowOverlap = true } = this.layer.getLayerConfig() as IPointLayerStyleOptions; | ||||||
|
|
||||||
| // allowOverlap 开关发生变化时,强制重建 | ||||||
| if (allowOverlap !== this.preAllowOverlap) { | ||||||
| this.preAllowOverlap = allowOverlap; | ||||||
| await this.reBuildModel(); | ||||||
| return true; | ||||||
| } | ||||||
|
|
||||||
| // 允许压盖时无需每帧检测 | ||||||
| if (allowOverlap) { | ||||||
| return false; | ||||||
| } | ||||||
|
|
||||||
| // 不允许压盖时,zoom 变化 >0.5 或视野超出上次范围则重算碰撞 | ||||||
| const zoom = this.mapService.getZoom(); | ||||||
| const extent = this.mapService.getBounds(); | ||||||
| const flag = boundsContains(this.extent, extent); | ||||||
| if (Math.abs(this.currentZoom - zoom) > 0.5 || !flag) { | ||||||
| await this.reBuildModel(); | ||||||
| return true; | ||||||
| } | ||||||
|
|
||||||
| return false; | ||||||
| } | ||||||
|
|
||||||
| public clearModels() { | ||||||
| this.texture?.destroy(); | ||||||
| this.iconService.off('imageUpdate', this.updateTexture); | ||||||
| } | ||||||
|
|
||||||
| protected registerBuiltinAttributes() { | ||||||
| // 注册 Position 属性 64 位地位部分,经纬度数据开启双精度,避免大于 20层级以上出现数据偏移 | ||||||
| this.registerPosition64LowAttribute(); | ||||||
|
|
@@ -127,6 +177,69 @@ export default class ImageModel extends BaseModel { | |||||
| }); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * 图标碰撞避让:遍历所有 feature,在屏幕空间做 AABB 碰撞检测, | ||||||
| * 将通过检测的 feature id 写入 imageFilterMap。 | ||||||
| * PointImageTriangulation 绑定 this 后,会跳过不在 map 中的 feature。 | ||||||
| */ | ||||||
| private filterImages() { | ||||||
| const { padding = [0, 0] } = this.layer.getLayerConfig() as IPointLayerStyleOptions; | ||||||
|
|
||||||
| this.imageFilterMap = {}; | ||||||
| this.currentZoom = this.mapService.getZoom(); | ||||||
| this.extent = this.imageExtent(); | ||||||
|
|
||||||
| const { width, height } = this.rendererService.getViewportSize(); | ||||||
| const collisionIndex = new CollisionIndex(width, height); | ||||||
| const data = this.layer.getEncodedData(); | ||||||
|
|
||||||
| data.forEach((feature: IEncodeFeature) => { | ||||||
| const { id = 0, size = 5 } = feature; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 此处为 建议修改此行,并在下一行添加对 if (id === undefined) {
// console.warn('Feature without id is skipped in collision detection.');
return;
}
Suggested change
|
||||||
| const iconSize = Array.isArray(size) ? size[0] : (size as number); | ||||||
| const centroid = calculateCentroid(feature.coordinates) as [number, number]; | ||||||
| const pixels = this.mapService.lngLatToContainer(centroid); | ||||||
|
|
||||||
| const { box } = collisionIndex.placeCollisionBox({ | ||||||
| x1: -iconSize - padding[0], | ||||||
| x2: iconSize + padding[0], | ||||||
| y1: -iconSize - padding[1], | ||||||
| y2: iconSize + padding[1], | ||||||
| anchorPointX: pixels.x, | ||||||
| anchorPointY: pixels.y, | ||||||
| }); | ||||||
|
|
||||||
| if (box && box.length) { | ||||||
| collisionIndex.insertCollisionBox(box, id as number); | ||||||
| this.imageFilterMap![id as number] = true; | ||||||
| } | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| private imageExtent(): [[number, number], [number, number]] { | ||||||
| const bounds = this.mapService.getBounds(); | ||||||
| return padBounds(bounds, 0.5); | ||||||
| } | ||||||
|
|
||||||
| private async reBuildModel() { | ||||||
| const { allowOverlap = true } = this.layer.getLayerConfig() as IPointLayerStyleOptions; | ||||||
| if (!allowOverlap) { | ||||||
| this.filterImages(); | ||||||
| } else { | ||||||
| this.imageFilterMap = null; // 允许压盖:清空过滤表,全量渲染 | ||||||
| } | ||||||
| const model = await this.layer.buildLayerModel({ | ||||||
| moduleName: 'pointImage', | ||||||
| vertexShader: pointImageVert, | ||||||
| fragmentShader: pointImageFrag, | ||||||
| triangulation: PointImageTriangulation.bind(this), | ||||||
| defines: this.getDefines(), | ||||||
| inject: this.getInject(), | ||||||
| depth: { enable: false }, | ||||||
| primitive: gl.POINTS, | ||||||
| }); | ||||||
| this.layer.models = [model]; | ||||||
| } | ||||||
|
Comment on lines
+223
to
+241
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 此方法 例如,可以创建一个 private async _buildModel(): Promise<IModel> {
const { allowOverlap = true } = this.layer.getLayerConfig() as IPointLayerStyleOptions;
if (!allowOverlap) {
this.filterImages();
} else {
this.imageFilterMap = null;
}
return await this.layer.buildLayerModel({
moduleName: 'pointImage',
vertexShader: pointImageVert,
fragmentShader: pointImageFrag,
triangulation: PointImageTriangulation.bind(this),
defines: this.getDefines(),
inject: this.getInject(),
depth: { enable: false },
primitive: gl.POINTS,
});
}然后 public async buildModels(): Promise<IModel[]> {
this.initUniformsBuffer();
const model = await this._buildModel();
return [model];
}
private async reBuildModel() {
const model = await this._buildModel();
this.layer.models = [model];
} |
||||||
|
|
||||||
| private updateTexture = () => { | ||||||
| const { createTexture2D } = this.rendererService; | ||||||
| if (this.texture) { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import { PointLayer, Scene } from '@antv/l7'; | ||
| import { GaodeMap } from '@antv/l7-maps'; | ||
|
|
||
| const scene = new Scene({ | ||
| id: 'map', | ||
| map: new GaodeMap({ | ||
| style: 'light', | ||
| center: [104.0, 36.0], | ||
| zoom: 4, | ||
| }), | ||
| }); | ||
|
|
||
| scene.addImage( | ||
| 'marker', | ||
| 'https://gw.alipayobjects.com/zos/basement_prod/604b5e7f-309e-40db-b95b-4fac746c5153.svg', | ||
| ); | ||
|
|
||
| // 在中国范围内生成密集随机点,使压盖效果明显 | ||
| function generateData(count) { | ||
| const points = []; | ||
| for (let i = 0; i < count; i++) { | ||
| points.push({ | ||
| lng: 99 + Math.random() * 20, | ||
| lat: 25 + Math.random() * 18, | ||
| id: i, | ||
| }); | ||
| } | ||
| return points; | ||
| } | ||
|
|
||
| scene.on('loaded', () => { | ||
| const data = generateData(300); | ||
|
|
||
| const layer = new PointLayer() | ||
| .source(data, { | ||
| parser: { type: 'json', x: 'lng', y: 'lat' }, | ||
| }) | ||
| .shape('marker') | ||
| .size(15) | ||
| .style({ | ||
| allowOverlap: false, // 默认关闭压盖,开启碰撞避让 | ||
| }); | ||
|
|
||
| scene.addLayer(layer); | ||
|
|
||
| // 控制面板 | ||
| const panel = document.createElement('div'); | ||
| panel.style.cssText = | ||
| 'position:absolute;top:10px;left:10px;z-index:100;background:#fff;padding:12px 16px;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,.15);font-size:13px;'; | ||
|
|
||
| panel.innerHTML = ` | ||
| <div style="margin-bottom:10px;font-weight:600;">Image allowOverlap</div> | ||
| <label style="display:flex;align-items:center;gap:8px;cursor:pointer;"> | ||
| <input type="checkbox" id="overlap-toggle" /> | ||
| <span id="overlap-label">allowOverlap: false(碰撞避让开启)</span> | ||
| </label> | ||
| <div style="margin-top:8px;color:#888;font-size:12px;"> | ||
| 点数量:300 | 缩放地图可触发重新计算 | ||
| </div> | ||
| `; | ||
|
|
||
| document.getElementById('map').appendChild(panel); | ||
|
|
||
| const checkbox = document.getElementById('overlap-toggle'); | ||
| const label = document.getElementById('overlap-label'); | ||
|
|
||
| checkbox.addEventListener('change', () => { | ||
| const allowOverlap = checkbox.checked; | ||
| label.textContent = allowOverlap | ||
| ? 'allowOverlap: true(允许压盖,全部显示)' | ||
| : 'allowOverlap: false(碰撞避让开启)'; | ||
| layer.style({ allowOverlap }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
使用
@ts-ignore和that变量来为this提供类型不是最佳实践。TypeScript 支持在函数的第一个参数位置声明this的类型,这样做可以更清晰地表达函数对this上下文的依赖,并且能获得更好的类型检查。建议将函数签名修改为
export function PointImageTriangulation(this: { imageFilterMap?: { [id: number]: boolean } }, feature: IEncodeFeature),然后就可以在函数体内直接安全地使用this,无需that变量和类型断言。