From 20ad7540974fdf0a6e902e2ea3700dcd5cf28d1d Mon Sep 17 00:00:00 2001 From: thinkinggis Date: Thu, 26 Dec 2019 22:36:50 +0800 Subject: [PATCH] feat(layer): pointLayer add text model --- .../core/src/services/layer/ILayerService.ts | 1 + .../services/layer/IStyleAttributeService.ts | 2 +- packages/layers/src/core/BaseModel.ts | 4 + .../layers/src/plugins/FeatureScalePlugin.ts | 28 +- .../src/plugins/LayerAnimateStylePlugin.ts | 33 +++ packages/layers/src/point/models/index.ts | 3 +- packages/layers/src/point/models/text.ts | 259 +++++++++++++++++- .../layers/src/point/shaders/text_frag.glsl | 5 +- .../layers/src/point/shaders/text_vert.glsl | 19 +- packages/layers/src/point/text.ts | 177 ------------ packages/layers/src/utils/collision-index.ts | 109 ++++++++ packages/layers/src/utils/grid-index.ts | 210 ++++++++++++++ packages/layers/src/utils/symbol-layout.ts | 29 +- stories/Layers/Layers.stories.tsx | 2 + stories/Layers/components/Text.tsx | 95 ++++--- 15 files changed, 726 insertions(+), 250 deletions(-) create mode 100644 packages/layers/src/plugins/LayerAnimateStylePlugin.ts delete mode 100644 packages/layers/src/point/text.ts create mode 100644 packages/layers/src/utils/collision-index.ts create mode 100644 packages/layers/src/utils/grid-index.ts diff --git a/packages/core/src/services/layer/ILayerService.ts b/packages/core/src/services/layer/ILayerService.ts index 3cad0f053c..f601451d9e 100644 --- a/packages/core/src/services/layer/ILayerService.ts +++ b/packages/core/src/services/layer/ILayerService.ts @@ -53,6 +53,7 @@ export interface ILayerModel { render(): void; getUninforms(): IModelUniform; getDefaultStyle(): unknown; + getAnimateOption(): unknown; buildModels(): IModel[]; } export interface IModelUniform { diff --git a/packages/core/src/services/layer/IStyleAttributeService.ts b/packages/core/src/services/layer/IStyleAttributeService.ts index 9396ef0b9c..2f00b415dc 100644 --- a/packages/core/src/services/layer/IStyleAttributeService.ts +++ b/packages/core/src/services/layer/IStyleAttributeService.ts @@ -119,7 +119,7 @@ export interface IStyleAttributeInitializationOptions { type: AttributeType; scale?: { field: StyleAttributeField; - values: unknown[]; + values: unknown[] | string; names: string[]; type: StyleScaleType; callback?: (...args: any[]) => []; diff --git a/packages/layers/src/core/BaseModel.ts b/packages/layers/src/core/BaseModel.ts index fdacc33722..87226d6e10 100644 --- a/packages/layers/src/core/BaseModel.ts +++ b/packages/layers/src/core/BaseModel.ts @@ -70,6 +70,10 @@ export default class BaseModel throw new Error('Method not implemented.'); } + public getAnimateOption(): IModelUniform { + throw new Error('Method not implemented.'); + } + public buildModels(): IModel[] { throw new Error('Method not implemented.'); } diff --git a/packages/layers/src/plugins/FeatureScalePlugin.ts b/packages/layers/src/plugins/FeatureScalePlugin.ts index 2bcfdb0901..955f904afd 100644 --- a/packages/layers/src/plugins/FeatureScalePlugin.ts +++ b/packages/layers/src/plugins/FeatureScalePlugin.ts @@ -118,7 +118,7 @@ export default class FeatureScalePlugin implements ILayerPlugin { scales.forEach((scale) => { // 如果设置了回调, 这不需要设置让range if (!attributeScale.callback) { - if (attributeScale.values) { + if (attributeScale.values && attributeScale.values !== 'text') { if ( scale.option?.type === 'linear' && attributeScale.values.length > 2 @@ -131,6 +131,7 @@ export default class FeatureScalePlugin implements ILayerPlugin { scale.scale.range(attributeScale.values); } else if (scale.option?.type === 'cat') { // 如果没有设置初值且 类型为cat,range ==domain; + scale.scale.range(scale.option.domain); } } @@ -159,20 +160,13 @@ export default class FeatureScalePlugin implements ILayerPlugin { dataArray: IParseDataItem[], ) { const scalekey = [field, attribute.name].join('_'); + const values = attribute.scale?.values; if (this.scaleCache[scalekey]) { return this.scaleCache[scalekey]; } - const styleScale = this.createScale(field, dataArray); + const styleScale = this.createScale(field, values, dataArray); this.scaleCache[scalekey] = styleScale; - // if ( - // styleScale.type === StyleScaleType.VARIABLE && - // attribute.scale?.values && - // attribute.scale?.values.length > 0 - // ) { // 只有变量初始化range - // styleScale.scale.range(attribute.scale?.values); - // } - return this.scaleCache[scalekey]; } @@ -191,7 +185,11 @@ export default class FeatureScalePlugin implements ILayerPlugin { return [field]; } - private createScale(field: string, data?: IParseDataItem[]): IStyleScale { + private createScale( + field: string, + values: unknown[] | string | undefined, + data?: IParseDataItem[], + ): IStyleScale { // 首先查找全局默认配置例如 color const scaleOption: IScale | undefined = this.scaleOptions[field]; const styleScale: IStyleScale = { @@ -200,6 +198,7 @@ export default class FeatureScalePlugin implements ILayerPlugin { type: StyleScaleType.VARIABLE, option: scaleOption, }; + if (!data || !data.length) { if (scaleOption && scaleOption.type) { styleScale.scale = this.createDefaultScale(scaleOption); @@ -216,9 +215,12 @@ export default class FeatureScalePlugin implements ILayerPlugin { styleScale.type = StyleScaleType.CONSTANT; } else { // 根据数据类型判断 默认等分位,时间,和枚举类型 - const type = + let type = (scaleOption && scaleOption.type) || this.getDefaultType(firstValue); - + if (values === 'text') { + // text 为内置变 如果是文本则为cat + type = ScaleTypes.CAT; + } const cfg = this.createDefaultScaleConfig(type, field, data); Object.assign(cfg, scaleOption); styleScale.scale = this.createDefaultScale(cfg); diff --git a/packages/layers/src/plugins/LayerAnimateStylePlugin.ts b/packages/layers/src/plugins/LayerAnimateStylePlugin.ts new file mode 100644 index 0000000000..7b63e6cca3 --- /dev/null +++ b/packages/layers/src/plugins/LayerAnimateStylePlugin.ts @@ -0,0 +1,33 @@ +import { + CameraUniform, + CoordinateUniform, + ICameraService, + ICoordinateSystemService, + ILayer, + ILayerPlugin, + IModel, + IRendererService, + TYPES, +} from '@antv/l7-core'; +import { inject, injectable } from 'inversify'; + +@injectable() +export default class LayerAnimateStylePlugin implements ILayerPlugin { + @inject(TYPES.ICameraService) + private readonly cameraService: ICameraService; + + @inject(TYPES.IRendererService) + private readonly rendererService: IRendererService; + + public apply(layer: ILayer) { + layer.hooks.beforeRender.tap('ShaderUniformPlugin', () => { + // 重新计算坐标系参数 + + layer.models.forEach((model: IModel) => { + model.addUniforms({ + // 相机参数,包含 VP 矩阵、缩放等级 + }); + }); + }); + } +} diff --git a/packages/layers/src/point/models/index.ts b/packages/layers/src/point/models/index.ts index e0b6ef8c87..ea4c8b9647 100644 --- a/packages/layers/src/point/models/index.ts +++ b/packages/layers/src/point/models/index.ts @@ -3,6 +3,7 @@ import ExtrudeModel from './extrude'; import FillModel from './fill'; import IMageModel from './image'; import NormalModel from './normal'; +import TextModel from './text'; export type PointType = 'fill' | 'image' | 'normal' | 'extrude' | 'text'; @@ -11,7 +12,7 @@ const PointModels: { [key in PointType]: any } = { image: IMageModel, normal: NormalModel, extrude: ExtrudeModel, - text: null, + text: TextModel, }; export default PointModels; diff --git a/packages/layers/src/point/models/text.ts b/packages/layers/src/point/models/text.ts index 4b643b340c..085c1edec3 100644 --- a/packages/layers/src/point/models/text.ts +++ b/packages/layers/src/point/models/text.ts @@ -1,15 +1,264 @@ -import { IModel, IModelUniform } from '@antv/l7-core'; +import { + AttributeType, + BlendType, + gl, + IEncodeFeature, + ILayerConfig, + IModel, + IModelUniform, + ITexture2D, +} from '@antv/l7-core'; +import { rgb2arr } from '@antv/l7-utils'; import BaseModel from '../../core/BaseModel'; +import { PointFillTriangulation } from '../../core/triangulation'; +import { + getGlyphQuads, + IGlyphQuad, + shapeText, +} from '../../utils/symbol-layout'; +import textFrag from '../shaders/text_frag.glsl'; +import textVert from '../shaders/text_vert.glsl'; +interface IPointTextLayerStyleOptions { + opacity: number; + textAnchor: string; + spacing: number; + padding: [number, number]; + stroke: string; + strokeWidth: number; + strokeOpacity: number; + fontWeight: string; + fontFamily: string; + textOffset: [number, number]; + textAllowOverlap: boolean; +} +export function TextTriangulation(feature: IEncodeFeature) { + const coordinates = feature.coordinates as number[]; + const { glyphQuads } = feature; + const vertices: number[] = []; + const indices: number[] = []; + const coord = + coordinates.length === 2 + ? [coordinates[0], coordinates[1], 0] + : coordinates; + glyphQuads.forEach((quad: IGlyphQuad, index: number) => { + vertices.push( + ...coord, + quad.tex.x, + quad.tex.y + quad.tex.height, + quad.tl.x, + quad.tl.y, + ...coord, + quad.tex.x + quad.tex.width, + quad.tex.y + quad.tex.height, + quad.tr.x, + quad.tr.y, + ...coord, + quad.tex.x + quad.tex.width, + quad.tex.y, + quad.br.x, + quad.br.y, + ...coord, + quad.tex.x, + quad.tex.y, + quad.bl.x, + quad.bl.y, + ); + indices.push( + 0 + index * 4, + 1 + index * 4, + 2 + index * 4, + 2 + index * 4, + 3 + index * 4, + 0 + index * 4, + ); + }); + return { + vertices, // [ x, y, z, tex.x,tex.y, offset.x. offset.y] + indices, + size: 7, + }; +} -export default class ExtrudeModel extends BaseModel { +export default class TextModel extends BaseModel { + private texture: ITexture2D; public getUninforms(): IModelUniform { - throw new Error('Method not implemented.'); + const { + fontWeight = 'normal', + fontFamily, + stroke, + strokeWidth, + } = this.layer.getLayerConfig() as IPointTextLayerStyleOptions; + const { canvas, fontAtlas, mapping } = this.fontService; + return { + u_opacity: 1.0, + u_sdf_map: this.texture, + u_stroke: rgb2arr(stroke), + u_halo_blur: 0.5, + u_sdf_map_size: [canvas.width, canvas.height], + u_strokeWidth: strokeWidth, + }; } public buildModels(): IModel[] { - throw new Error('Method not implemented.'); + this.initTextFont(); + this.generateGlyphLayout(); + this.registerBuiltinAttributes(); + this.updateTexture(); + return [ + this.layer.buildLayerModel({ + moduleName: 'pointText', + vertexShader: textVert, + fragmentShader: textFrag, + triangulation: TextTriangulation, + depth: { enable: false }, + blend: this.getBlend(), + }), + ]; } + protected registerBuiltinAttributes() { - throw new Error('Method not implemented.'); + const viewProjection = this.cameraService.getViewProjectionMatrix(); + this.styleAttributeService.registerStyleAttribute({ + name: 'textOffsets', + type: AttributeType.Attribute, + descriptor: { + name: 'a_textOffsets', + buffer: { + // give the WebGL driver a hint that this buffer may change + usage: gl.STATIC_DRAW, + data: [], + type: gl.FLOAT, + }, + size: 2, + update: ( + feature: IEncodeFeature, + featureIdx: number, + vertex: number[], + attributeIdx: number, + ) => { + return [vertex[5], vertex[6]]; + }, + }, + }); + + // point layer size; + this.styleAttributeService.registerStyleAttribute({ + name: 'size', + type: AttributeType.Attribute, + descriptor: { + name: 'a_Size', + buffer: { + // give the WebGL driver a hint that this buffer may change + usage: gl.DYNAMIC_DRAW, + data: [], + type: gl.FLOAT, + }, + size: 1, + update: ( + feature: IEncodeFeature, + featureIdx: number, + vertex: number[], + attributeIdx: number, + ) => { + const { size } = feature; + return Array.isArray(size) ? [size[0]] : [size as number]; + }, + }, + }); + + // point layer size; + this.styleAttributeService.registerStyleAttribute({ + name: 'textUv', + type: AttributeType.Attribute, + descriptor: { + name: 'a_tex', + buffer: { + // give the WebGL driver a hint that this buffer may change + usage: gl.DYNAMIC_DRAW, + data: [], + type: gl.FLOAT, + }, + size: 2, + update: ( + feature: IEncodeFeature, + featureIdx: number, + vertex: number[], + attributeIdx: number, + ) => { + return [vertex[3], vertex[4]]; + }, + }, + }); + } + private initTextFont() { + const { + fontWeight = 'normal', + fontFamily, + } = this.layer.getLayerConfig() as IPointTextLayerStyleOptions; + const data = this.layer.getEncodedData(); + const characterSet: string[] = []; + data.forEach((item: IEncodeFeature) => { + let { shape = '' } = item; + shape = shape.toString(); + for (const char of shape) { + // 去重 + if (characterSet.indexOf(char) === -1) { + characterSet.push(char); + } + } + }); + this.fontService.setFontOptions({ + characterSet, + fontWeight, + fontFamily, + }); + } + private generateGlyphLayout() { + const { canvas, fontAtlas, 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; + const shaping = shapeText( + shape.toString(), + mapping, + 24, + textAnchor, + 'center', + spacing, + textOffset, + ); + const glyphQuads = getGlyphQuads(shaping, textOffset, false); + feature.shaping = shaping; + feature.glyphQuads = glyphQuads; + }); + } + + private drawGlyph() { + const { + spacing = 2, + textAnchor = 'center', + textOffset = [0, 0], + padding = [4, 4], + textAllowOverlap, + } = this.layer.getLayerConfig() as IPointTextLayerStyleOptions; + const viewProjection = this.cameraService.getViewProjectionMatrix(); + } + private updateTexture() { + const { createTexture2D } = this.rendererService; + const { canvas } = this.fontService; + this.texture = createTexture2D({ + data: canvas, + width: canvas.width, + height: canvas.height, + }); } } diff --git a/packages/layers/src/point/shaders/text_frag.glsl b/packages/layers/src/point/shaders/text_frag.glsl index d3dfa93034..11b8520478 100644 --- a/packages/layers/src/point/shaders/text_frag.glsl +++ b/packages/layers/src/point/shaders/text_frag.glsl @@ -1,3 +1,5 @@ +#define SDF_PX 8.0 +#define EDGE_GAMMA 0.105 uniform sampler2D u_sdf_map; uniform float u_gamma_scale : 0.5; uniform float u_font_size : 24; @@ -5,6 +7,7 @@ uniform float u_opacity : 1.0; uniform vec4 u_stroke : [0, 0, 0, 1]; uniform float u_strokeWidth : 2.0; uniform float u_halo_blur : 0.5; +uniform float u_DevicePixelRatio; varying vec4 v_color; varying vec2 v_uv; @@ -17,7 +20,7 @@ void main() { float fontScale = u_font_size / 24.0; lowp float buff = (6.0 - u_strokeWidth / fontScale) / SDF_PX; - highp float gamma = (u_halo_blur * 1.19 / SDF_PX + EDGE_GAMMA) / (fontScale * u_gamma_scale); + highp float gamma = (u_halo_blur * 1.19 / SDF_PX + EDGE_GAMMA) / (fontScale * u_gamma_scale) / 1.0; highp float gamma_scaled = gamma * v_gamma_scale; diff --git a/packages/layers/src/point/shaders/text_vert.glsl b/packages/layers/src/point/shaders/text_vert.glsl index 496a5180c2..729b446105 100644 --- a/packages/layers/src/point/shaders/text_vert.glsl +++ b/packages/layers/src/point/shaders/text_vert.glsl @@ -1,14 +1,13 @@ +#define SDF_PX 8.0 +#define EDGE_GAMMA 0.105 attribute vec3 a_Position; attribute vec2 a_tex; -attribute vec2 a_offset; -attribute vec4 a_color; -attribute float a_size; +attribute vec2 a_textOffsets; +attribute vec4 a_Color; +attribute float a_Size; uniform vec2 u_sdf_map_size; -uniform vec2 u_viewport_size; - -uniform float u_activeId : 0; -uniform vec4 u_activeColor : [1.0, 0.0, 0.0, 1.0]; +uniform mat4 u_ModelMatrix; varying vec2 v_uv; varying float v_gamma_scale; @@ -17,18 +16,18 @@ varying vec4 v_color; #pragma include "projection" void main() { - v_color = a_color; + v_color = a_Color; v_uv = a_tex / u_sdf_map_size; // 文本缩放比例 - float fontScale = a_size / 24.; + float fontScale = a_Size / 24.; vec4 project_pos = project_position(vec4(a_Position, 1.0)); vec4 projected_position = project_common_position_to_clipspace(vec4(project_pos.xyz, 1.0)); gl_Position = vec4(projected_position.xy / projected_position.w - + a_offset * fontScale / u_viewport_size * 2., 0.0, 1.0); + + a_textOffsets * fontScale / u_ViewportSize * 2., 0.0, 1.0); v_gamma_scale = gl_Position.w; diff --git a/packages/layers/src/point/text.ts b/packages/layers/src/point/text.ts deleted file mode 100644 index becc151b72..0000000000 --- a/packages/layers/src/point/text.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { AttributeType, gl, IEncodeFeature } from '@antv/l7-core'; -import BaseLayer from '../core/BaseLayer'; -import { getGlyphQuads, shapeText } from '../utils/symbol-layout'; -import textFrag from './shaders/text_frag.glsl'; -import textVert from './shaders/text_vert.glsl'; -interface IPointTextLayerStyleOptions { - opacity: number; - textAnchor: string; - textOffset: [number, number]; - spacing: number; - padding: [number, number]; - stroke: string; - strokeWidth: number; - strokeOpacity: number; - fontWeight: string; - fontFamily: string; - - textAllowOverlap: boolean; -} -export function PointTriangulation(feature: IEncodeFeature) { - const coordinates = feature.coordinates as number[]; - return { - vertices: [...coordinates, ...coordinates, ...coordinates, ...coordinates], - indices: [0, 1, 2, 2, 3, 0], - size: coordinates.length, - }; -} -export default class TextLayer extends BaseLayer { - public type: string = 'PointLayer'; - - protected getConfigSchema() { - return { - properties: { - opacity: { - type: 'number', - minimum: 0, - maximum: 1, - }, - }, - }; - } - - protected renderModels() { - const { opacity } = this.getLayerConfig(); - this.models.forEach((model) => - model.draw({ - uniforms: { - u_opacity: opacity || 1.0, - }, - }), - ); - return this; - } - - protected buildModels() { - this.registerBuiltinAttributes(); - this.models = [ - this.buildLayerModel({ - moduleName: 'pointText', - vertexShader: textVert, - fragmentShader: textFrag, - triangulation: PointTriangulation, - depth: { enable: false }, - blend: { - enable: true, - func: { - srcRGB: gl.SRC_ALPHA, - srcAlpha: 1, - dstRGB: gl.ONE_MINUS_SRC_ALPHA, - dstAlpha: 1, - }, - }, - }), - ]; - } - - private registerBuiltinAttributes() { - this.styleAttributeService.registerStyleAttribute({ - name: 'textOffsets', - type: AttributeType.Attribute, - descriptor: { - name: 'a_textOffsets', - buffer: { - // give the WebGL driver a hint that this buffer may change - usage: gl.STATIC_DRAW, - data: [], - type: gl.FLOAT, - }, - size: 2, - update: ( - feature: IEncodeFeature, - featureIdx: number, - vertex: number[], - attributeIdx: number, - ) => { - const extrude = [-1, -1, 1, -1, 1, 1, -1, 1]; - const extrudeIndex = (attributeIdx % 4) * 2; - return [extrude[extrudeIndex], extrude[extrudeIndex + 1]]; - }, - }, - }); - - // point layer size; - this.styleAttributeService.registerStyleAttribute({ - name: 'size', - type: AttributeType.Attribute, - descriptor: { - name: 'a_Size', - buffer: { - // give the WebGL driver a hint that this buffer may change - usage: gl.DYNAMIC_DRAW, - data: [], - type: gl.FLOAT, - }, - size: 1, - update: ( - feature: IEncodeFeature, - featureIdx: number, - vertex: number[], - attributeIdx: number, - ) => { - const { size } = feature; - return Array.isArray(size) ? [size[0]] : [size as number]; - }, - }, - }); - - // point layer size; - this.styleAttributeService.registerStyleAttribute({ - name: 'shape', - type: AttributeType.Attribute, - descriptor: { - name: 'a_Shape', - buffer: { - // give the WebGL driver a hint that this buffer may change - usage: gl.DYNAMIC_DRAW, - data: [], - type: gl.FLOAT, - }, - size: 1, - update: ( - feature: IEncodeFeature, - featureIdx: number, - vertex: number[], - attributeIdx: number, - ) => { - const { shape = 2 } = feature; - const shape2d = this.getLayerConfig().shape2d as string[]; - const shapeIndex = shape2d.indexOf(shape as string); - return [shapeIndex]; - }, - }, - }); - } - - private initTextFont() { - const { fontWeight = 'normal', fontFamily } = this.getLayerConfig(); - const data = this.getEncodedData(); - const characterSet: string[] = []; - data.forEach((item: IEncodeFeature) => { - let { shape = '' } = item; - shape = shape.toString(); - for (const char of shape) { - // 去重 - if (characterSet.indexOf(char) === -1) { - characterSet.push(char); - } - } - }); - - this.fontService.setFontOptions({ - characterSet, - fontWeight, - fontFamily, - }); - } -} diff --git a/packages/layers/src/utils/collision-index.ts b/packages/layers/src/utils/collision-index.ts new file mode 100644 index 0000000000..21e8873938 --- /dev/null +++ b/packages/layers/src/utils/collision-index.ts @@ -0,0 +1,109 @@ +export interface ICollisionBox { + x1: number; + y1: number; + x2: number; + y2: number; + anchorPointX: number; + anchorPointY: number; +} +// @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 + */ +export default class CollisionIndex { + private width: number; + private height: number; + private grid: GridIndex; + private screenRightBoundary: number; + private screenBottomBoundary: number; + private gridRightBoundary: number; + private gridBottomBoundary: number; + constructor(width: number, height: number) { + this.width = width; + this.height = height; + // 创建网格索引 + this.grid = new GridIndex( + width + 2 * viewportPadding, + height + 2 * viewportPadding, + 25, + ); + + this.screenRightBoundary = width + viewportPadding; + this.screenBottomBoundary = height + viewportPadding; + this.gridRightBoundary = width + 2 * viewportPadding; + this.gridBottomBoundary = height + 2 * viewportPadding; + } + + public placeCollisionBox(collisionBox: ICollisionBox, mvpMatrix: mat4) { + 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; + + if ( + !this.isInsideGrid(tlX, tlY, brX, brY) || + this.grid.hitTest(tlX, tlY, brX, brY) + ) { + return { + box: [], + }; + } + + return { + box: [tlX, tlY, brX, brY], + }; + } + + public insertCollisionBox(box: number[], featureIndex: number) { + const key = { featureIndex }; + this.grid.insert(key, box[0], box[1], box[2], box[3]); + } + + /** + * 后续碰撞检测都需要投影到 viewport 坐标系 + * @param {THREE.Matrix4} mvpMatrix mvp矩阵 + * @param {number} x P20 平面坐标X + * @param {number} y P20 平面坐标Y + * @return {Point} projectedPoint + */ + public project(mvpMatrix: mat4, x: number, y: number) { + const point = vec4.fromValues(x, y, 0, 1); + const out = vec4.create(); + vec4.transformMat4(out, point, mvpMatrix); + // 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, + }; + } + + /** + * 判断包围盒是否在整个网格内,需要加上 buffer + * @param {number} x1 x1 + * @param {number} y1 y1 + * @param {number} x2 x2 + * @param {number} y2 y2 + * @return {Point} isInside + */ + public isInsideGrid(x1: number, y1: number, x2: number, y2: number) { + return ( + x2 >= 0 && + x1 < this.gridRightBoundary && + y2 >= 0 && + y1 < this.gridBottomBoundary + ); + } +} diff --git a/packages/layers/src/utils/grid-index.ts b/packages/layers/src/utils/grid-index.ts new file mode 100644 index 0000000000..76a179c8e2 --- /dev/null +++ b/packages/layers/src/utils/grid-index.ts @@ -0,0 +1,210 @@ +interface IQueryArgs { + hitTest: boolean; + seenUids: { box: any; circle: any }; +} +type CallBack = (...args: any[]) => any; +/** + * 网格索引,相比 @mapbox/grid-index,在简单计算碰撞检测结果时效率更高 + * @see https://zhuanlan.zhihu.com/p/74373214 + */ +class GridIndex { + private boxCells: number[][]; + private xCellCount: number; + private yCellCount: number; + private boxKeys: string[]; + private bboxes: number[]; + private width: number; + private height: number; + private xScale: number; + private yScale: number; + private boxUid: number; + + constructor(width: number, height: number, cellSize: number) { + const boxCells = this.boxCells; + + this.xCellCount = Math.ceil(width / cellSize); + this.yCellCount = Math.ceil(height / cellSize); + + for (let i = 0; i < this.xCellCount * this.yCellCount; i++) { + boxCells.push([]); + } + this.boxKeys = []; + this.bboxes = []; + + this.width = width; + this.height = height; + this.xScale = this.xCellCount / width; + this.yScale = this.yCellCount / height; + this.boxUid = 0; + } + + public insert(key: any, x1: number, y1: number, x2: number, y2: number) { + this.forEachCell(x1, y1, x2, y2, this.insertBoxCell, this.boxUid++); + this.boxKeys.push(key); + this.bboxes.push(x1); + this.bboxes.push(y1); + this.bboxes.push(x2); + this.bboxes.push(y2); + } + + public query( + x1: number, + y1: number, + x2: number, + y2: number, + predicate?: CallBack, + ) { + return this.queryHitTest(x1, y1, x2, y2, false, predicate); + } + + public hitTest( + x1: number, + y1: number, + x2: number, + y2: number, + predicate?: CallBack, + ) { + return this.queryHitTest(x1, y1, x2, y2, true, predicate); + } + + private insertBoxCell( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + uid: number, + ) { + this.boxCells[cellIndex].push(uid); + } + + private queryHitTest( + x1: number, + y1: number, + x2: number, + y2: number, + hitTest: boolean, + predicate?: CallBack, + ) { + if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) { + return hitTest ? false : []; + } + const result = []; + if (x1 <= 0 && y1 <= 0 && this.width <= x2 && this.height <= y2) { + // 这一步是高效的关键,后续精确碰撞检测结果在计算文本可见性时并不需要 + if (hitTest) { + return true; + } + for (let boxUid = 0; boxUid < this.boxKeys.length; boxUid++) { + result.push({ + key: this.boxKeys[boxUid], + x1: this.bboxes[boxUid * 4], + y1: this.bboxes[boxUid * 4 + 1], + x2: this.bboxes[boxUid * 4 + 2], + y2: this.bboxes[boxUid * 4 + 3], + }); + } + return predicate ? result.filter(predicate) : result; + } + + const queryArgs = { + hitTest, + seenUids: { box: {}, circle: {} }, + }; + this.forEachCell( + x1, + y1, + x2, + y2, + this.queryCell, + result, + queryArgs, + predicate, + ); + return hitTest ? result.length > 0 : result; + } + + private queryCell( + x1: number, + y1: number, + x2: number, + y2: number, + cellIndex: number, + result: any[], + queryArgs?: any, + predicate?: CallBack, + ) { + const seenUids = queryArgs.seenUids; + const boxCell = this.boxCells[cellIndex]; + if (boxCell !== null) { + const bboxes = this.bboxes; + for (const boxUid of boxCell) { + if (!seenUids.box[boxUid]) { + seenUids.box[boxUid] = true; + const offset = boxUid * 4; + if ( + x1 <= bboxes[offset + 2] && + y1 <= bboxes[offset + 3] && + x2 >= bboxes[offset + 0] && + y2 >= bboxes[offset + 1] && + (!predicate || predicate(this.boxKeys[boxUid])) + ) { + if (queryArgs.hitTest) { + result.push(true); + return true; + } + result.push({ + key: this.boxKeys[boxUid], + x1: bboxes[offset], + y1: bboxes[offset + 1], + x2: bboxes[offset + 2], + y2: bboxes[offset + 3], + }); + } + } + } + } + return false; + } + + private forEachCell( + x1: number, + y1: number, + x2: number, + y2: number, + fn: CallBack, + arg1: any[] | number, + arg2?: IQueryArgs, + predicate?: CallBack, + ) { + const cx1 = this.convertToXCellCoord(x1); + const cy1 = this.convertToYCellCoord(y1); + const cx2 = this.convertToXCellCoord(x2); + const cy2 = this.convertToYCellCoord(y2); + + for (let x = cx1; x <= cx2; x++) { + for (let y = cy1; y <= cy2; y++) { + const cellIndex = this.xCellCount * y + x; + if (fn.call(this, x1, y1, x2, y2, cellIndex, arg1, arg2, predicate)) { + return; + } + } + } + } + + private convertToXCellCoord(x: number) { + return Math.max( + 0, + Math.min(this.xCellCount - 1, Math.floor(x * this.xScale)), + ); + } + + private convertToYCellCoord(y: number) { + return Math.max( + 0, + Math.min(this.yCellCount - 1, Math.floor(y * this.yScale)), + ); + } +} + +export default GridIndex; diff --git a/packages/layers/src/utils/symbol-layout.ts b/packages/layers/src/utils/symbol-layout.ts index 204e192395..4ccf46d2d9 100644 --- a/packages/layers/src/utils/symbol-layout.ts +++ b/packages/layers/src/utils/symbol-layout.ts @@ -1,3 +1,22 @@ +interface IPoint { + x: number; + y: number; +} +export interface IGlyphQuad { + tr: IPoint; + tl: IPoint; + bl: IPoint; + br: IPoint; + tex: { + x: number; + y: number; + height: number; + width: number; + advance: number; + }; + glyphOffset: [number, number]; +} + /** * 返回文本相对锚点位置 * @param {string} anchor 锚点位置 @@ -181,7 +200,7 @@ export function shapeText( textAnchor: string, textJustify: string, spacing: number, - translate: [number, number], + translate: [number, number] = [0, 0], ) { // TODO:处理换行 const lines = text.split('\n'); @@ -215,11 +234,11 @@ export function shapeText( export function getGlyphQuads( shaping: any, - textOffset: [number, number], + textOffset: [number, number] = [0, 0], alongLine: boolean, -) { +): IGlyphQuad[] { const { positionedGlyphs } = shaping; - const quads = []; + const quads: IGlyphQuad[] = []; for (const positionedGlyph of positionedGlyphs) { const rect = positionedGlyph.metrics; @@ -229,7 +248,7 @@ export function getGlyphQuads( const halfAdvance = (rect.advance * positionedGlyph.scale) / 2; - const glyphOffset = alongLine + const glyphOffset: [number, number] = alongLine ? [positionedGlyph.x + halfAdvance, positionedGlyph.y] : [0, 0]; diff --git a/stories/Layers/Layers.stories.tsx b/stories/Layers/Layers.stories.tsx index 2e0ee5fda8..b86b68a2fc 100644 --- a/stories/Layers/Layers.stories.tsx +++ b/stories/Layers/Layers.stories.tsx @@ -15,6 +15,7 @@ import PointImage from './components/PointImage'; import Polygon3D from './components/Polygon3D'; import ImageLayerDemo from './components/RasterImage'; import RasterLayerDemo from './components/RasterLayer'; +import TextLayerDemo from './components/Text'; // @ts-ignore storiesOf('图层', module) @@ -22,6 +23,7 @@ storiesOf('图层', module) .add('数据更新', () => ) .add('亮度图', () => ) .add('3D点', () => ) + .add('文字', () => ) .add('Column', () => ) .add('图片标注', () => ) .add('面3d图层', () => ) diff --git a/stories/Layers/components/Text.tsx b/stories/Layers/components/Text.tsx index fca96db54f..12bc4d9c21 100644 --- a/stories/Layers/components/Text.tsx +++ b/stories/Layers/components/Text.tsx @@ -1,61 +1,82 @@ import { PointLayer, Scene } from '@antv/l7'; +import { GaodeMap, Mapbox } from '@antv/l7-maps'; import * as React from 'react'; +// @ts-ignore import data from '../data/data.json'; -export default class Point3D extends React.Component { +export default class TextLayerDemo extends React.Component { + // @ts-ignore private scene: Scene; public componentWillUnmount() { this.scene.destroy(); } - public componentDidMount() { - const scene = new Scene({ - center: [120.19382669582967, 30.258134], - id: 'map', - pitch: 0, - type: 'mapbox', - style: 'mapbox://styles/mapbox/streets-v9', - zoom: 1, - }); - const pointLayer = new PointLayer({}); - const p1 = { + public async componentDidMount() { + const data = { type: 'FeatureCollection', features: [ { type: 'Feature', - properties: {}, + properties: { + name: '中华人民共和国', + }, geometry: { type: 'Point', - coordinates: [83.671875, 44.84029065139799], + coordinates: [103.0078125, 36.03133177633187], + }, + }, + { + type: 'Feature', + properties: { + name: '中华人民共和国', + }, + geometry: { + type: 'Point', + coordinates: [122.6953125, 10.833305983642491], }, }, ], }; - pointLayer - .source(data) - .color('name', [ - '#FFF5B8', - '#FFDC7D', - '#FFAB5C', - '#F27049', - '#D42F31', - '#730D1C', - ]) - .shape('subregion', [ - 'circle', - 'triangle', - 'square', - 'pentagon', - 'hexagon', - 'octogon', - 'hexagram', - 'rhombus', - 'vesica', - ]) - .size('scalerank', [2, 4, 6, 8, 10]); + const response = await fetch( + 'https://gw.alipayobjects.com/os/rmsportal/oVTMqfzuuRFKiDwhPSFL.json', + ); + const pointsData = await response.json(); + + const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + center: [120.19382669582967, 30.258134], + pitch: 0, + style: 'dark', + zoom: 3, + }), + }); + // scene.on('loaded', () => { + const pointLayer = new PointLayer({}) + .source(pointsData.list, { + parser: { + type: 'json', + x: 'j', + y: 'w', + }, + }) + .shape('m', 'text') + .size(24) + .color('#fff') + .style({ + fontWeight: 800, + textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left + textOffset: [0, 0], // 文本相对锚点的偏移量 [水平, 垂直] + spacing: 2, // 字符间距 + padding: [4, 4], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近 + strokeColor: 'white', // 描边颜色 + strokeWidth: 4, // 描边宽度 + strokeOpacity: 1.0, + }); scene.addLayer(pointLayer); - scene.render(); + this.scene = scene; + // }); } public render() {