From 0a9558d8ebe750d2c63dae64cddc4dc73ff19947 Mon Sep 17 00:00:00 2001 From: thinkinggis Date: Mon, 30 Dec 2019 15:35:47 +0800 Subject: [PATCH] feat: point text add overlap --- .../core/src/services/config/ConfigService.ts | 2 +- .../core/src/services/layer/ILayerService.ts | 1 + .../core/src/services/scene/SceneService.ts | 8 +- packages/layers/src/core/BaseLayer.ts | 11 +- packages/layers/src/core/BaseModel.ts | 3 + packages/layers/src/heatmap/index.ts | 8 +- packages/layers/src/index.ts | 11 +- packages/layers/src/line/index.ts | 8 -- .../layers/src/plugins/LayerStylePlugin.ts | 3 + .../layers/src/plugins/UpdateModelPlugin.ts | 17 +++ packages/layers/src/point/index.ts | 22 +-- packages/layers/src/point/models/extrude.ts | 10 +- packages/layers/src/point/models/image.ts | 10 +- packages/layers/src/point/models/text.ts | 125 +++++++++++++++--- .../layers/src/point/shaders/text_vert.glsl | 2 +- packages/layers/src/polygon/index.ts | 10 -- packages/layers/src/utils/collision-index.ts | 52 ++++---- packages/layers/src/utils/grid-index.ts | 2 +- stories/Layers/components/Text.tsx | 12 +- 19 files changed, 202 insertions(+), 115 deletions(-) create mode 100644 packages/layers/src/plugins/UpdateModelPlugin.ts diff --git a/packages/core/src/services/config/ConfigService.ts b/packages/core/src/services/config/ConfigService.ts index b0660c99a9..ea6a0245e2 100644 --- a/packages/core/src/services/config/ConfigService.ts +++ b/packages/core/src/services/config/ConfigService.ts @@ -47,7 +47,7 @@ const defaultLayerConfig: Partial = { ], shape3d: ['cylinder', 'triangleColumn', 'hexagonColumn', 'squareColumn'], minZoom: 0, - maxZoom: 20, + maxZoom: 24, visible: true, autoFit: false, zIndex: 0, diff --git a/packages/core/src/services/layer/ILayerService.ts b/packages/core/src/services/layer/ILayerService.ts index 71fb3f9dc7..0baef65a96 100644 --- a/packages/core/src/services/layer/ILayerService.ts +++ b/packages/core/src/services/layer/ILayerService.ts @@ -55,6 +55,7 @@ export interface ILayerModel { getDefaultStyle(): unknown; getAnimateUniforms(): IModelUniform; buildModels(): IModel[]; + needUpdate(): boolean; } export interface IModelUniform { [key: string]: IUniform; diff --git a/packages/core/src/services/scene/SceneService.ts b/packages/core/src/services/scene/SceneService.ts index 1803cf8e4a..7f5332bb28 100644 --- a/packages/core/src/services/scene/SceneService.ts +++ b/packages/core/src/services/scene/SceneService.ts @@ -161,7 +161,9 @@ export default class Scene extends EventEmitter implements ISceneService { this.$container as HTMLDivElement, this.handleWindowResized, ); - // window.addEventListener('resize', this.handleWindowResized, false); + window + .matchMedia('screen and (-webkit-min-device-pixel-ratio: 1.5)') + .addListener(this.handleWindowResized); } else { this.logger.error('容器 id 不存在'); } @@ -227,7 +229,9 @@ export default class Scene extends EventEmitter implements ISceneService { this.rendererService.destroy(); this.map.destroy(); unbind(this.$container as HTMLDivElement, this.handleWindowResized); - // window.removeEventListener('resize', this.handleWindowResized, false); + window + .matchMedia('screen and (min-resolution: 2dppx)') + .removeListener(this.handleWindowResized); } private handleWindowResized = () => { diff --git a/packages/layers/src/core/BaseLayer.ts b/packages/layers/src/core/BaseLayer.ts index 4fa1bf3045..ce2148ae35 100644 --- a/packages/layers/src/core/BaseLayer.ts +++ b/packages/layers/src/core/BaseLayer.ts @@ -759,7 +759,16 @@ export default class BaseLayer extends EventEmitter } protected renderModels() { - throw new Error('Method not implemented.'); + if (this.layerModelNeedUpdate) { + this.models = this.layerModel.buildModels(); + this.layerModelNeedUpdate = false; + } + this.models.forEach((model) => { + model.draw({ + uniforms: this.layerModel.getUninforms(), + }); + }); + return this; } protected getModelType(): unknown { diff --git a/packages/layers/src/core/BaseModel.ts b/packages/layers/src/core/BaseModel.ts index 732354af42..5e95fe2295 100644 --- a/packages/layers/src/core/BaseModel.ts +++ b/packages/layers/src/core/BaseModel.ts @@ -77,6 +77,9 @@ export default class BaseModel return {}; } + public needUpdate(): boolean { + return false; + } public buildModels(): IModel[] { throw new Error('Method not implemented.'); } diff --git a/packages/layers/src/heatmap/index.ts b/packages/layers/src/heatmap/index.ts index 0569550ee7..f5253f2318 100644 --- a/packages/layers/src/heatmap/index.ts +++ b/packages/layers/src/heatmap/index.ts @@ -21,8 +21,12 @@ export default class HeatMapLayer extends BaseLayer { protected renderModels() { const shape = this.getModelType(); if (shape === 'heatmap') { - this.layerModel.render(); - return; + this.layerModel.render(); // 独立的渲染流程 + return this; + } + if (this.layerModelNeedUpdate) { + this.models = this.layerModel.buildModels(); + this.layerModelNeedUpdate = false; } this.models.forEach((model) => model.draw({ diff --git a/packages/layers/src/index.ts b/packages/layers/src/index.ts index 3f95eaa2e8..b5a8435969 100644 --- a/packages/layers/src/index.ts +++ b/packages/layers/src/index.ts @@ -20,8 +20,8 @@ import MultiPassRendererPlugin from './plugins/MultiPassRendererPlugin'; import PixelPickingPlugin from './plugins/PixelPickingPlugin'; import RegisterStyleAttributePlugin from './plugins/RegisterStyleAttributePlugin'; import ShaderUniformPlugin from './plugins/ShaderUniformPlugin'; +import UpdateModelPlugin from './plugins/UpdateModelPlugin'; import UpdateStyleAttributePlugin from './plugins/UpdateStyleAttributePlugin'; - /** * 校验传入参数配置项的正确性 * @see /dev-docs/ConfigSchemaValidation.md @@ -74,6 +74,15 @@ container .bind(TYPES.ILayerPlugin) .to(UpdateStyleAttributePlugin) .inRequestScope(); + +/** + * 负责Model更新 + */ +container + .bind(TYPES.ILayerPlugin) + .to(UpdateModelPlugin) + .inRequestScope(); + /** * Multi Pass 自定义渲染管线 */ diff --git a/packages/layers/src/line/index.ts b/packages/layers/src/line/index.ts index 4678c555b4..2f1054c0ed 100644 --- a/packages/layers/src/line/index.ts +++ b/packages/layers/src/line/index.ts @@ -26,14 +26,6 @@ export default class LineLayer extends BaseLayer { }; return defaultConfig[type]; } - protected renderModels() { - this.models.forEach((model) => - model.draw({ - uniforms: this.layerModel.getUninforms(), - }), - ); - return this; - } protected buildModels() { const shape = this.getModelType(); diff --git a/packages/layers/src/plugins/LayerStylePlugin.ts b/packages/layers/src/plugins/LayerStylePlugin.ts index 9b61ae1bbc..4a3e426d75 100644 --- a/packages/layers/src/plugins/LayerStylePlugin.ts +++ b/packages/layers/src/plugins/LayerStylePlugin.ts @@ -2,6 +2,9 @@ import { ILayer, ILayerPlugin, IMapService, TYPES } from '@antv/l7-core'; import Source from '@antv/l7-source'; import { encodePickingColor, rgb2arr } from '@antv/l7-utils'; import { injectable } from 'inversify'; +/** + * 更新图层样式,初始图层相关配置 + */ @injectable() export default class LayerStylePlugin implements ILayerPlugin { public apply(layer: ILayer) { diff --git a/packages/layers/src/plugins/UpdateModelPlugin.ts b/packages/layers/src/plugins/UpdateModelPlugin.ts new file mode 100644 index 0000000000..467cca1d26 --- /dev/null +++ b/packages/layers/src/plugins/UpdateModelPlugin.ts @@ -0,0 +1,17 @@ +import { ILayer, ILayerPlugin, IMapService, TYPES } from '@antv/l7-core'; +import { injectable } from 'inversify'; +/** + * Model 更新 + */ +@injectable() +export default class UpdateModelPlugin implements ILayerPlugin { + public apply(layer: ILayer) { + layer.hooks.beforeRender.tap('UpdateModelPlugin', () => { + // 处理文本更新 + layer.layerModel.needUpdate(); + // if (layer.layerModel.needUpdate()) { + // layer.layerModelNeedUpdate = true; + // } + }); + } +} diff --git a/packages/layers/src/point/index.ts b/packages/layers/src/point/index.ts index 9ea48d5fe7..fc5242dba6 100644 --- a/packages/layers/src/point/index.ts +++ b/packages/layers/src/point/index.ts @@ -28,23 +28,12 @@ export default class PointLayer extends BaseLayer { fill: {}, extrude: {}, image: {}, - text: {}, + text: { + blend: 'normal', + }, }; return defaultConfig[type]; } - protected renderModels() { - if (this.layerModelNeedUpdate) { - this.models = this.layerModel.buildModels(); - this.layerModelNeedUpdate = false; - } - this.models.forEach((model) => { - model.draw({ - uniforms: this.layerModel.getUninforms(), - }); - }); - return this; - } - protected buildModels() { const modelType = this.getModelType(); this.layerModel = new PointModels[modelType](this); @@ -79,9 +68,4 @@ export default class PointLayer extends BaseLayer { return 'text'; } } - - private updateData() { - // const bounds = this.mapService.getBounds(); - // console.log(bounds); - } } diff --git a/packages/layers/src/point/models/extrude.ts b/packages/layers/src/point/models/extrude.ts index 47636c6dff..bf618e9341 100644 --- a/packages/layers/src/point/models/extrude.ts +++ b/packages/layers/src/point/models/extrude.ts @@ -21,15 +21,7 @@ export default class ExtrudeModel extends BaseModel { vertexShader: pointExtrudeVert, fragmentShader: pointExtrudeFrag, triangulation: PointExtrudeTriangulation, - blend: { - enable: true, - func: { - srcRGB: gl.SRC_ALPHA, - srcAlpha: 1, - dstRGB: gl.ONE_MINUS_SRC_ALPHA, - dstAlpha: 1, - }, - }, + blend: this.getBlend(), }), ]; } diff --git a/packages/layers/src/point/models/image.ts b/packages/layers/src/point/models/image.ts index 1b35976989..6d26b6eb27 100644 --- a/packages/layers/src/point/models/image.ts +++ b/packages/layers/src/point/models/image.ts @@ -42,15 +42,7 @@ export default class ImageModel extends BaseModel { triangulation: PointImageTriangulation, primitive: gl.POINTS, depth: { enable: false }, - blend: { - enable: true, - func: { - srcRGB: gl.SRC_ALPHA, - srcAlpha: 1, - dstRGB: gl.ONE_MINUS_SRC_ALPHA, - dstAlpha: 1, - }, - }, + blend: this.getBlend(), }), ]; } diff --git a/packages/layers/src/point/models/text.ts b/packages/layers/src/point/models/text.ts index 085c1edec3..dfba9beb0c 100644 --- a/packages/layers/src/point/models/text.ts +++ b/packages/layers/src/point/models/text.ts @@ -3,6 +3,7 @@ import { BlendType, gl, IEncodeFeature, + ILayer, ILayerConfig, IModel, IModelUniform, @@ -10,7 +11,7 @@ import { } from '@antv/l7-core'; import { rgb2arr } from '@antv/l7-utils'; import BaseModel from '../../core/BaseModel'; -import { PointFillTriangulation } from '../../core/triangulation'; +import CollisionIndex from '../../utils/collision-index'; import { getGlyphQuads, IGlyphQuad, @@ -81,14 +82,18 @@ export function TextTriangulation(feature: IEncodeFeature) { export default class TextModel extends BaseModel { private texture: ITexture2D; + private glyphInfo: IEncodeFeature[]; + private currentZoom: number = -1; + private extent: [[number, number], [number, number]]; + public getUninforms(): IModelUniform { const { - fontWeight = 'normal', + fontWeight = 800, fontFamily, stroke, strokeWidth, } = this.layer.getLayerConfig() as IPointTextLayerStyleOptions; - const { canvas, fontAtlas, mapping } = this.fontService; + const { canvas } = this.fontService; return { u_opacity: 1.0, u_sdf_map: this.texture, @@ -100,10 +105,9 @@ export default class TextModel extends BaseModel { } public buildModels(): IModel[] { - this.initTextFont(); - this.generateGlyphLayout(); - this.registerBuiltinAttributes(); + this.initGlyph(); this.updateTexture(); + this.filterGlyphs(); return [ this.layer.buildLayerModel({ moduleName: 'pointText', @@ -115,9 +119,36 @@ export default class TextModel extends BaseModel { }), ]; } + public needUpdate() { + const { + textAllowOverlap = false, + } = this.layer.getLayerConfig() as IPointTextLayerStyleOptions; + const zoom = this.mapService.getZoom(); + const extent = this.mapService.getBounds(); + const flag = + extent[0][0] < this.extent[0][0] || + extent[0][1] < this.extent[0][1] || + extent[1][0] > this.extent[1][0] || + extent[1][1] < this.extent[1][1]; + + if (!textAllowOverlap && (Math.abs(this.currentZoom - zoom) > 1 || flag)) { + this.filterGlyphs(); + this.layer.models = [ + this.layer.buildLayerModel({ + moduleName: 'pointText', + vertexShader: textVert, + fragmentShader: textFrag, + triangulation: TextTriangulation, + depth: { enable: false }, + blend: this.getBlend(), + }), + ]; + return true; + } + return false; + } protected registerBuiltinAttributes() { - const viewProjection = this.cameraService.getViewProjectionMatrix(); this.styleAttributeService.registerStyleAttribute({ name: 'textOffsets', type: AttributeType.Attribute, @@ -190,6 +221,18 @@ export default class TextModel extends BaseModel { }, }); } + private textExtent(): [[number, number], [number, number]] { + const bounds = this.mapService.getBounds(); + const step = + Math.min(bounds[1][0] - bounds[0][0], bounds[1][1] - bounds[1][0]) / 2; + return [ + [bounds[0][0] - step, bounds[0][1] - step], + [bounds[1][0] + step, bounds[1][1] + step], + ]; + } + /** + * 生成文字纹理 + */ private initTextFont() { const { fontWeight = 'normal', @@ -213,20 +256,19 @@ export default class TextModel extends BaseModel { fontFamily, }); } + /** + * 生成文字布局 + */ private generateGlyphLayout() { - const { canvas, fontAtlas, mapping } = this.fontService; + const { mapping } = this.fontService; const { spacing = 2, textAnchor = 'center', textOffset, - padding = [4, 4], - textAllowOverlap, } = this.layer.getLayerConfig() as IPointTextLayerStyleOptions; const data = this.layer.getEncodedData(); - data.forEach((feature: IEncodeFeature) => { - const { coordinates, shape = '' } = feature; - const size = feature.size as number; - const fontScale = size / 24; + this.glyphInfo = data.map((feature: IEncodeFeature) => { + const { shape = '' } = feature; const shaping = shapeText( shape.toString(), mapping, @@ -239,19 +281,60 @@ export default class TextModel extends BaseModel { const glyphQuads = getGlyphQuads(shaping, textOffset, false); feature.shaping = shaping; feature.glyphQuads = glyphQuads; + return feature; }); } - - private drawGlyph() { + /** + * 文字避让 + */ + private filterGlyphs() { const { - spacing = 2, - textAnchor = 'center', - textOffset = [0, 0], padding = [4, 4], - textAllowOverlap, + textAllowOverlap = false, } = this.layer.getLayerConfig() as IPointTextLayerStyleOptions; - const viewProjection = this.cameraService.getViewProjectionMatrix(); + if (textAllowOverlap) { + return; + } + this.currentZoom = this.mapService.getZoom(); + this.extent = this.textExtent(); + const { width, height } = this.rendererService.getViewportSize(); + const collisionIndex = new CollisionIndex(width, height); + const filterData = this.glyphInfo.filter((feature: IEncodeFeature) => { + const { shaping, id = 0 } = feature; + const coordinates = feature.coordinates as [number, number]; + const size = feature.size as number; + const fontScale: number = size / 24; + const pixels = this.mapService.lngLatToContainer(coordinates); + const { box } = collisionIndex.placeCollisionBox({ + x1: shaping.left * fontScale - padding[0], + x2: shaping.right * fontScale + padding[0], + y1: shaping.top * fontScale - padding[1], + y2: shaping.bottom * fontScale + padding[1], + anchorPointX: pixels.x, + anchorPointY: pixels.y, + }); + if (box && box.length) { + // TODO:featureIndex + collisionIndex.insertCollisionBox(box, id); + return true; + } else { + return false; + } + }); + this.layer.setEncodedData(filterData); } + /** + * 初始化文字布局 + */ + private initGlyph() { + // 1.生成文字纹理 + this.initTextFont(); + // 2.生成文字布局 + this.generateGlyphLayout(); + } + /** + * 更新文字纹理 + */ private updateTexture() { const { createTexture2D } = this.rendererService; const { canvas } = this.fontService; diff --git a/packages/layers/src/point/shaders/text_vert.glsl b/packages/layers/src/point/shaders/text_vert.glsl index 729b446105..268c31c124 100644 --- a/packages/layers/src/point/shaders/text_vert.glsl +++ b/packages/layers/src/point/shaders/text_vert.glsl @@ -27,7 +27,7 @@ void main() { vec4 projected_position = project_common_position_to_clipspace(vec4(project_pos.xyz, 1.0)); gl_Position = vec4(projected_position.xy / projected_position.w - + a_textOffsets * fontScale / u_ViewportSize * 2., 0.0, 1.0); + + a_textOffsets * fontScale / u_ViewportSize * 2. * u_DevicePixelRatio, 0.0, 1.0); v_gamma_scale = gl_Position.w; diff --git a/packages/layers/src/polygon/index.ts b/packages/layers/src/polygon/index.ts index 55e46cd1b3..4ea9dbfa79 100644 --- a/packages/layers/src/polygon/index.ts +++ b/packages/layers/src/polygon/index.ts @@ -20,16 +20,6 @@ export default class PolygonLayer extends BaseLayer { }, }; } - - protected renderModels() { - this.models.forEach((model) => - model.draw({ - uniforms: this.layerModel.getUninforms(), - }), - ); - return this; - } - protected buildModels() { const shape = this.getModelType(); this.layerModel = new PolygonModels[shape](this); diff --git a/packages/layers/src/utils/collision-index.ts b/packages/layers/src/utils/collision-index.ts index 21e8873938..5fe762e324 100644 --- a/packages/layers/src/utils/collision-index.ts +++ b/packages/layers/src/utils/collision-index.ts @@ -9,10 +9,6 @@ export interface ICollisionBox { // @mapbox/grid-index 并没有类似 hitTest 的单纯获取碰撞检测结果的方法,query 将导致计算大量多余的包围盒结果,因此使用改良版 import { mat4, vec4 } from 'gl-matrix'; import GridIndex from './grid-index'; - -// 为 viewport 加上 buffer,避免边缘处的文本无法显示 -const viewportPadding = 100; - /** * 基于网格实现文本避让,大幅提升包围盒碰撞检测效率 * @see https://zhuanlan.zhihu.com/p/74373214 @@ -21,6 +17,7 @@ export default class CollisionIndex { private width: number; private height: number; private grid: GridIndex; + private viewportPadding: number = 100; private screenRightBoundary: number; private screenBottomBoundary: number; private gridRightBoundary: number; @@ -28,30 +25,35 @@ export default class CollisionIndex { constructor(width: number, height: number) { this.width = width; this.height = height; + this.viewportPadding = Math.max(width, height); // 创建网格索引 this.grid = new GridIndex( - width + 2 * viewportPadding, - height + 2 * viewportPadding, + width + this.viewportPadding, + height + this.viewportPadding, 25, ); - this.screenRightBoundary = width + viewportPadding; - this.screenBottomBoundary = height + viewportPadding; - this.gridRightBoundary = width + 2 * viewportPadding; - this.gridBottomBoundary = height + 2 * viewportPadding; + this.screenRightBoundary = width + this.viewportPadding; + this.screenBottomBoundary = height + this.viewportPadding; + this.gridRightBoundary = width + 2 * this.viewportPadding; + this.gridBottomBoundary = height + 2 * this.viewportPadding; } - public placeCollisionBox(collisionBox: ICollisionBox, mvpMatrix: mat4) { - const projectedPoint = this.project( - mvpMatrix, - collisionBox.anchorPointX, - collisionBox.anchorPointY, - ); + public placeCollisionBox(collisionBox: ICollisionBox) { + // const projectedPoint = this.project( + // mvpMatrix, + // collisionBox.anchorPointX, + // collisionBox.anchorPointY, + // ); - const tlX = collisionBox.x1 + projectedPoint.x; - const tlY = collisionBox.y1 + projectedPoint.y; - const brX = collisionBox.x2 + projectedPoint.x; - const brY = collisionBox.y2 + projectedPoint.y; + const tlX = + collisionBox.x1 + collisionBox.anchorPointX + this.viewportPadding; + const tlY = + collisionBox.y1 + collisionBox.anchorPointY + this.viewportPadding; + const brX = + collisionBox.x2 + collisionBox.anchorPointX + this.viewportPadding; + const brY = + collisionBox.y2 + collisionBox.anchorPointY + this.viewportPadding; if ( !this.isInsideGrid(tlX, tlY, brX, brY) || @@ -79,14 +81,16 @@ export default class CollisionIndex { * @param {number} y P20 平面坐标Y * @return {Point} projectedPoint */ - public project(mvpMatrix: mat4, x: number, y: number) { + public project(mvpMatrix: number[], x: number, y: number) { const point = vec4.fromValues(x, y, 0, 1); const out = vec4.create(); - vec4.transformMat4(out, point, mvpMatrix); + // @ts-ignore + const mat = mat4.fromValues(...mvpMatrix); + vec4.transformMat4(out, point, mat); // GL 坐标系[-1, 1] -> viewport 坐标系[width, height] return { - x: ((out[0] / out[3] + 1) / 2) * this.width + viewportPadding, - y: ((-out[1] / out[3] + 1) / 2) * this.height + viewportPadding, + x: ((out[0] / out[3] + 1) / 2) * this.width + this.viewportPadding, + y: ((-out[1] / out[3] + 1) / 2) * this.height + this.viewportPadding, }; } diff --git a/packages/layers/src/utils/grid-index.ts b/packages/layers/src/utils/grid-index.ts index 4f56a11b58..63da50615b 100644 --- a/packages/layers/src/utils/grid-index.ts +++ b/packages/layers/src/utils/grid-index.ts @@ -8,7 +8,7 @@ type CallBack = (...args: any[]) => any; * @see https://zhuanlan.zhihu.com/p/74373214 */ class GridIndex { - private boxCells: number[][]; + private boxCells: number[][] = []; private xCellCount: number; private yCellCount: number; private boxKeys: string[]; diff --git a/stories/Layers/components/Text.tsx b/stories/Layers/components/Text.tsx index 12bc4d9c21..ad5ed57e67 100644 --- a/stories/Layers/components/Text.tsx +++ b/stories/Layers/components/Text.tsx @@ -44,7 +44,7 @@ export default class TextLayerDemo extends React.Component { const scene = new Scene({ id: 'map', - map: new GaodeMap({ + map: new Mapbox({ center: [120.19382669582967, 30.258134], pitch: 0, style: 'dark', @@ -61,16 +61,16 @@ export default class TextLayerDemo extends React.Component { }, }) .shape('m', 'text') - .size(24) + .size(12) .color('#fff') .style({ - fontWeight: 800, + fontWeight: 200, textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left textOffset: [0, 0], // 文本相对锚点的偏移量 [水平, 垂直] spacing: 2, // 字符间距 - padding: [4, 4], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近 - strokeColor: 'white', // 描边颜色 - strokeWidth: 4, // 描边宽度 + padding: [1, 1], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近 + stroke: 'red', // 描边颜色 + strokeWidth: 2, // 描边宽度 strokeOpacity: 1.0, }); scene.addLayer(pointLayer);