From cf74bbe8bbbd26c1f6950a03ff1c724654c00f9e Mon Sep 17 00:00:00 2001 From: thinkinggis Date: Tue, 13 Aug 2019 18:18:04 +0800 Subject: [PATCH 1/3] feat(src) update version inteaction --- package.json | 2 +- src/core/scene.js | 16 ++++++++-------- src/interaction/active.js | 2 ++ src/interaction/select.js | 1 + 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 16540a6840..4bfd95b7ed 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@antv/l7", - "version": "1.2.3", + "version": "1.3.0", "description": "Large-scale WebGL-powered Geospatial Data Visualization", "main": "build/L7.js", "browser": "build/L7-min.js", diff --git a/src/core/scene.js b/src/core/scene.js index a94034fd1a..0a8359aa54 100644 --- a/src/core/scene.js +++ b/src/core/scene.js @@ -28,8 +28,8 @@ export default class Scene extends Base { _initEngine(mapContainer) { this._engine = new Engine(mapContainer, this); - this.registerMapEvent(); - this._engine.run(); + this.registerMapEvent(); // 和高德地图同步状态 + // this._engine.run(); compileBuiltinModules(); } _initContoller() { @@ -53,13 +53,13 @@ export default class Scene extends Base { const Map = new MapProvider(this._attrs); Map.mixMap(this); this._container = Map.container; - this._markerContainier = Map.l7_marker_Container; Map.on('mapLoad', () => { this.map = Map.map; + this._markerContainier = Map.l7_marker_Container; this._initEngine(Map.renderDom); Map.asyncCamera(this._engine); this.initLayer(); - // this._registEvents(); + this._registEvents(); const hash = this.get('hash'); if (hash) { const Ctor = getInteraction('hash'); @@ -174,14 +174,14 @@ export default class Scene extends Base { // 地图状态变化时更新可视化渲染 registerMapEvent() { this._updateRender = () => this._engine.update(); - this.map.on('mousemove', this._updateRender); - // this.map.on('mapmove', this._updateRender); + // this.map.on('mousemove', this._updateRender); + this.map.on('mapmove', this._updateRender); this.map.on('camerachange', this._updateRender); } unRegsterMapEvent() { - this.map.off('mousemove', this._updateRender); - // this.map.off('mapmove', this._updateRender); + // this.map.off('mousemove', this._updateRender); + this.map.off('mapmove', this._updateRender); this.map.off('camerachange', this._updateRender); } // control diff --git a/src/interaction/active.js b/src/interaction/active.js index d4e8807d45..7269fc37bf 100644 --- a/src/interaction/active.js +++ b/src/interaction/active.js @@ -9,8 +9,10 @@ export default class Active extends Interaction { } process(ev) { this.layer._addActiveFeature(ev); + this.layer.scene._engine.update(); } reset() { this.layer._resetStyle(); + this.layer.scene._engine.update(); } } diff --git a/src/interaction/select.js b/src/interaction/select.js index 2cf5e4d001..2481872694 100644 --- a/src/interaction/select.js +++ b/src/interaction/select.js @@ -8,5 +8,6 @@ export default class Select extends Interaction { } process(ev) { this.layer._addActiveFeature(ev); + this.layer.scene._engine.update(); } } From 86d89dd5bff0d83a2b2c4acfb14853e549148310 Mon Sep 17 00:00:00 2001 From: xiaoiver Date: Wed, 14 Aug 2019 19:52:44 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E4=BA=8E=E7=82=B9=E8=A6=81=E7=B4=A0=E7=9A=84=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=87=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demos/06_text.html | 37 ++- src/core/scene.js | 29 ++- src/geom/buffer/point/text.js | 259 ++++++++++++--------- src/geom/buffer/point/text/font-manager.js | 17 +- src/geom/material/textMaterial.js | 23 +- src/geom/shader/text_frag.glsl | 57 +++-- src/geom/shader/text_vert.glsl | 53 +++-- src/layer/index.js | 2 + src/layer/render/index.js | 6 + src/layer/render/text/drawText.js | 104 +++++++++ src/layer/textLayer.js | 28 +++ src/map/AMap.js | 4 +- src/util/collision-index.js | 81 +++++++ src/util/font-util.js | 24 +- src/util/grid-index.js | 131 +++++++++++ src/util/symbol-layout.js | 234 +++++++++++++++++++ 16 files changed, 876 insertions(+), 213 deletions(-) create mode 100644 src/layer/render/text/drawText.js create mode 100644 src/layer/textLayer.js create mode 100644 src/util/collision-index.js create mode 100644 src/util/grid-index.js create mode 100644 src/util/symbol-layout.js diff --git a/demos/06_text.html b/demos/06_text.html index 3fd2bc4f61..074a8f1d4a 100644 --- a/demos/06_text.html +++ b/demos/06_text.html @@ -6,7 +6,7 @@ - point_circle + text layer @@ -29,21 +29,42 @@ const scene = new L7.Scene({ }); window.scene = scene; scene.on('loaded', () => { - $.get('./data/provincePoint.json', data => { + $.get('https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_regions_points.geojson', data => { scene.PointLayer({ - zIndex: 2 + zIndex: 3 + }) + .source(data) + .shape('circle') + .active(true) + .size(4) + .color('#fff') + .style({ + stroke: '#999', + strokeWidth: 1, + opacity: 1.0 + }) + .render(); + + scene.TextLayer({ + zIndex: 4 }) .source(data) .shape('name', 'text') .active(true) - .size(12) // default 1 - .color('name') + .size('scalerank', [ 10, 20, 24 ]) + .color('scalerank', [ 'red', 'blue', 'black' ]) .style({ - stroke: '#999', - strokeWidth: 0, + // fontFamily: 'Monaco, monospace', // 字体 + fontWeight: 400, + textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left + textOffset: [ 0, 0 ], // 文本相对锚点的偏移量 [水平, 垂直] + spacing: 2, // 字符间距 + padding: [ 4, 4 ], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近 + strokeColor: 'white', // 描边颜色 + strokeWidth: 2, // 描边宽度 opacity: 1.0 }) - .render(); + .render(); }); }); diff --git a/src/core/scene.js b/src/core/scene.js index 6786dde1db..4db7a7c7dc 100644 --- a/src/core/scene.js +++ b/src/core/scene.js @@ -21,6 +21,7 @@ export default class Scene extends Base { this.fontAtlasManager = new FontAtlasManager(); this._layers = []; this.animateCount = 0; + this.inited = false; } _initEngine(mapContainer) { @@ -44,16 +45,24 @@ export default class Scene extends Base { this.map = Map.map; this._initEngine(Map.renderDom); Map.asyncCamera(this._engine); - this.initLayer(); - this._registEvents(); - const hash = this.get('hash'); - if (hash) { - const Ctor = getInteraction('hash'); - const interaction = new Ctor({ layer: this }); - interaction._onHashChange(); - } - this.emit('loaded'); - this._engine.update(); + + // 等待相机同步之后再进行首次渲染 + Map.on('cameraloaded', () => { + if (!this.inited) { + this.initLayer(); + this._registEvents(); + const hash = this.get('hash'); + if (hash) { + const Ctor = getInteraction('hash'); + const interaction = new Ctor({ layer: this }); + interaction._onHashChange(); + } + this.emit('loaded'); + this.inited = true; + } + this._engine.update(); + this.emit('cameraloaded'); + }); }); } initLayer() { diff --git a/src/geom/buffer/point/text.js b/src/geom/buffer/point/text.js index 9b2f5f50b8..95614ea296 100644 --- a/src/geom/buffer/point/text.js +++ b/src/geom/buffer/point/text.js @@ -1,130 +1,165 @@ -export default function TextBuffer(layerData, fontAtlasManager) { +/** + * 为文本构建顶点数据,仅支持点要素自动标注。 + * @see https://zhuanlan.zhihu.com/p/72222549 + * @see https://zhuanlan.zhihu.com/p/74373214 + */ +import { shapeText, getGlyphQuads } from '../../../util/symbol-layout'; + +export default function TextBuffer( + layerData, + sourceData, + options, + fontAtlasManager, + collisionIndex, + mvpMatrix +) { + const { + textField, + fontWeight, + fontFamily + } = options; const characterSet = []; - layerData.forEach(element => { - let text = element.shape || ''; + sourceData.forEach(element => { + // shape 存储了 text-field + let text = element[textField] || ''; text = text.toString(); for (let j = 0; j < text.length; j++) { + // 去重 if (characterSet.indexOf(text[j]) === -1) { characterSet.push(text[j]); } } }); fontAtlasManager.setProps({ - characterSet + characterSet, + fontFamily, + fontWeight }); - const attr = drawGlyph(layerData, fontAtlasManager); - return attr; + return drawGlyph(layerData, sourceData, options, fontAtlasManager, collisionIndex, mvpMatrix); } -function drawGlyph(layerData, fontAtlasManager) { + +function drawGlyph( + layerData, sourceData, + { + textField, + spacing = 2, + textAnchor = 'center', + textOffset = [ 0, 0 ], + padding = [ 4, 4 ] + }, + fontAtlasManager, + collisionIndex, + mvpMatrix +) { + const { texture, fontAtlas, mapping } = fontAtlasManager; + const attributes = { - originPoints: [], - textSizes: [], - textOffsets: [], + fontAtlas, + texture, + positions: [], colors: [], - textureElements: [], - pickingIds: [] + pickingIds: [], + textUVs: [], + textOffsets: [], + textSizes: [], + index: [] }; - const { texture, fontAtlas, mapping, scale } = fontAtlasManager; - layerData.forEach(function(element) { - const size = element.size; - const pos = element.coordinates; - let text = element.shape || ''; - text = text.toString(); - const pen = { - x: (-text.length * size) / 2, - y: 0 - }; - for (let i = 0; i < text.length; i++) { - const metric = mapping[text[i]]; - const { x, y, width, height } = metric; - const color = element.color; - const offsetX = pen.x; - const offsetY = pen.y; - attributes.pickingIds.push( - element.id, - element.id, - element.id, - element.id, - element.id, - element.id - ); - attributes.textOffsets.push( - // 文字在词语的偏移量 - offsetX, - offsetY, - offsetX, - offsetY, - offsetX, - offsetY, - offsetX, - offsetY, - offsetX, - offsetY, - offsetX, - offsetY - ); - attributes.originPoints.push( - // 词语的经纬度坐标 - pos[0], - pos[1], - 0, - pos[0], - pos[1], - 0, - pos[0], - pos[1], - 0, - pos[0], - pos[1], - 0, - pos[0], - pos[1], - 0, - pos[0], - pos[1], - 0 - ); - attributes.textSizes.push( - size, - size * scale, - 0, - size * scale, - 0, - 0, - size, - size * scale, - 0, - 0, - size, - 0 - ); - attributes.colors.push( - ...color, - ...color, - ...color, - ...color, - ...color, - ...color - ); - attributes.textureElements.push( - // 文字纹理坐标 - x + width, - y, - x, - y, - x, - y + height, - x + width, - y, - x, - y + height, - x + width, - y + height - ); - pen.x = pen.x + size; + let indexCounter = 0; + layerData.forEach((feature, i) => { + const { size, coordinates } = feature; + // 根据字段获取文本 + const text = `${sourceData[i][textField] || ''}`; + // sdf 中默认字号为 24 + const fontScale = size / 24; + + // 1. 计算每个字符相对锚点的位置 + const shaping = shapeText(text, mapping, 24, textAnchor, 'center', spacing, textOffset); + + if (shaping) { + // 2. 尝试加入空间索引,获取碰撞检测结果 + // TODO:按照 feature 中指定字段排序,确定插入权重,保证优先级高的文本优先展示 + 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: coordinates[0], + anchorPointY: coordinates[1] + }, mvpMatrix); + + // 无碰撞则加入空间索引 + if (box && box.length) { + // TODO:featureIndex + collisionIndex.insertCollisionBox(box, 0); + + // 3. 计算可供渲染的文本块,其中每个字符都包含纹理坐标 + const glyphQuads = getGlyphQuads(shaping, textOffset, false); + + // 4. 构建顶点数据,四个顶点组成一个 quad + indexCounter = addAttributeForFeature(feature, attributes, glyphQuads, indexCounter); + } } }); - attributes.texture = texture; - attributes.fontAtlas = fontAtlas; return attributes; } + +function addAttributeForFeature(feature, attributes, glyphQuads, indexCounter) { + const { id, size, color, coordinates } = feature; + glyphQuads.forEach(quad => { + + attributes.pickingIds.push( + id, + id, + id, + id + ); + + attributes.colors.push( + ...color, + ...color, + ...color, + ...color + ); + + attributes.positions.push( + coordinates[0], coordinates[1], + coordinates[0], coordinates[1], + coordinates[0], coordinates[1], + coordinates[0], coordinates[1] + ); + + attributes.textUVs.push( + quad.tex.x, quad.tex.y + quad.tex.height, + quad.tex.x + quad.tex.width, quad.tex.y + quad.tex.height, + quad.tex.x + quad.tex.width, quad.tex.y, + quad.tex.x, quad.tex.y, + ); + + attributes.textOffsets.push( + quad.tl.x, quad.tl.y, + quad.tr.x, quad.tr.y, + quad.br.x, quad.br.y, + quad.bl.x, quad.bl.y + ); + + attributes.textSizes.push( + size, + size, + size, + size + ); + + attributes.index.push( + 0 + indexCounter, + 1 + indexCounter, + 2 + indexCounter, + 2 + indexCounter, + 3 + indexCounter, + 0 + indexCounter + ); + indexCounter += 4; + }); + + return indexCounter; +} diff --git a/src/geom/buffer/point/text/font-manager.js b/src/geom/buffer/point/text/font-manager.js index 0fdb8bb53f..3820fa14b3 100644 --- a/src/geom/buffer/point/text/font-manager.js +++ b/src/geom/buffer/point/text/font-manager.js @@ -10,8 +10,8 @@ export const DEFAULT_BUFFER = 3; export const DEFAULT_CUTOFF = 0.25; export const DEFAULT_RADIUS = 8; const MAX_CANVAS_WIDTH = 1024; -const BASELINE_SCALE = 0.9; -const HEIGHT_SCALE = 1.2; +const BASELINE_SCALE = 1.0; +const HEIGHT_SCALE = 1.0; const CACHE_LIMIT = 3; const cache = new LRUCache(CACHE_LIMIT); @@ -36,9 +36,9 @@ function getDefaultCharacterSet() { function setTextStyle(ctx, fontFamily, fontSize, fontWeight) { ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`; - ctx.fillStyle = '#000'; - ctx.textBaseline = 'baseline'; - ctx.textAlign = 'left'; + ctx.fillStyle = 'black'; + ctx.textBaseline = 'middle'; + // ctx.textAlign = 'left'; } function getNewChars(key, characterSet) { const cachedFontAtlas = cache.get(key); @@ -146,10 +146,10 @@ export default class FontAtlasManager { _updateTexture({ data: canvas }) { this._texture = new THREE.CanvasTexture(canvas); - this._texture.wrapS = THREE.ClampToEdgeWrapping; - this._texture.wrapT = THREE.ClampToEdgeWrapping; this._texture.minFilter = THREE.LinearFilter; + this._texture.magFilter = THREE.LinearFilter; this._texture.flipY = false; + this._texture.format = THREE.AlphaFormat; this._texture.needUpdate = true; } @@ -200,7 +200,8 @@ export default class FontAtlasManager { for (const char of characterSet) { populateAlphaChannel(tinySDF.draw(char), imageData); - ctx.putImageData(imageData, mapping[char].x - buffer, mapping[char].y - buffer); + // 考虑到描边,需要保留 sdf 的 buffer,不能像 deck.gl 一样直接减去 + ctx.putImageData(imageData, mapping[char].x, mapping[char].y); } } else { for (const char of characterSet) { diff --git a/src/geom/material/textMaterial.js b/src/geom/material/textMaterial.js index 7e9d52ca1b..267ee808bd 100644 --- a/src/geom/material/textMaterial.js +++ b/src/geom/material/textMaterial.js @@ -1,23 +1,14 @@ import Material from './material'; -import { getModule } from '../../util/shaderModule'; +import { getModule, wrapUniforms } from '../../util/shaderModule'; +import merge from '@antv/util/lib/deep-mix'; -export default function TextMaterial(options) { - const { vs, fs } = getModule('text'); +export default function TextMaterial(_uniforms) { + const { vs, fs, uniforms } = getModule('text'); const material = new Material({ - uniforms: { - u_opacity: { value: options.u_opacity || 1.0 }, - u_texture: { value: options.u_texture }, - u_strokeWidth: { value: options.u_strokeWidth }, - u_stroke: { value: options.u_stroke }, - u_textTextureSize: { value: options.u_textTextureSize }, - u_scale: { value: options.u_scale }, - u_gamma: { value: options.u_gamma }, - u_buffer: { value: options.u_buffer }, - u_glSize: { value: options.u_glSize }, - u_activeId: { value: options.u_activeId || 0 }, - u_activeColor: { value: options.u_activeColor } - + defines: { + DEVICE_PIXEL_RATIO: window.devicePixelRatio }, + uniforms: wrapUniforms(merge(uniforms, _uniforms)), vertexShader: vs, fragmentShader: fs, transparent: true diff --git a/src/geom/shader/text_frag.glsl b/src/geom/shader/text_frag.glsl index 7fe59c4257..23d86a5b3b 100644 --- a/src/geom/shader/text_frag.glsl +++ b/src/geom/shader/text_frag.glsl @@ -1,31 +1,30 @@ -precision mediump float; -uniform sampler2D u_texture; -varying vec4 v_color; -uniform vec4 u_stroke; -uniform float u_strokeWidth; -uniform float u_buffer; -uniform float u_gamma; -uniform float u_opacity; -varying vec2 v_texcoord; -varying float v_size; -void main(){ - float dist=texture2D(u_texture,vec2(v_texcoord.x,v_texcoord.y)).a; - float alpha; +#define SDF_PX 8.0 +#define EDGE_GAMMA 0.105 / float(DEVICE_PIXEL_RATIO) - if(u_strokeWidth==0.){ - alpha=smoothstep(u_buffer-u_gamma,u_buffer+u_gamma,dist); - gl_FragColor=vec4(v_color.rgb,alpha*v_color.a); - }else{ - if(dist<=u_buffer-u_gamma){ - alpha=smoothstep(u_strokeWidth-u_gamma,u_strokeWidth+u_gamma,dist); - gl_FragColor=vec4(u_stroke.rgb,alpha*u_stroke.a); - }else if(dist e / 255), + u_halo_width: strokeWidth, + u_halo_blur: 0.5, + u_font_opacity: opacity, + u_sdf_map_size: [ fontAtlas.width, fontAtlas.height ], + u_viewport_size: [ width, height ], + u_activeColor: activeOption.fill + }); + const mesh = new THREE.Mesh(geometry, material); + + // 更新 viewport + window.addEventListener('resize', () => { + const { width, height } = layer.scene.getSize(); + material.uniforms.u_viewport_size.value = [ width, height ]; + material.uniforms.needsUpdate = true; + }, false); + + // 关闭视锥裁剪 + mesh.frustumCulled = false; + return mesh; +} + diff --git a/src/layer/textLayer.js b/src/layer/textLayer.js new file mode 100644 index 0000000000..1f07e7a0cd --- /dev/null +++ b/src/layer/textLayer.js @@ -0,0 +1,28 @@ +import Layer from '../core/layer'; +import { getRender } from './render/'; +import CollisionIndex from '../util/collision-index'; +export default class TextLayer extends Layer { + shape(textField, shape = 'text') { + this.textField = textField; + this.shape = shape; + + // 创建碰撞检测索引 + const { width, height } = this.scene.getSize(); + this.collisionIndex = new CollisionIndex(width, height); + + // 相机变化,需要重新构建索引,由于文本可见性的改变,也需要重新组装顶点数据 + this.scene.on('cameraloaded', () => { + this.collisionIndex = new CollisionIndex(width, height); + + this.layerMesh.geometry = getRender(this.type, this.shape)(this.layerData, this, true); + this.layerMesh.geometry.needsUpdate = true; + }); + + return this; + } + draw() { + this.init(); + this.type = 'text'; + this.add(getRender(this.type, this.shape)(this.layerData, this)); + } +} diff --git a/src/map/AMap.js b/src/map/AMap.js index a6a101e7ce..b8dcfaf708 100644 --- a/src/map/AMap.js +++ b/src/map/AMap.js @@ -82,7 +82,9 @@ export default class GaodeMap extends Base { camera.lookAt(0, 0, 0); camera.position.x += e.camera.position.x; camera.position.y += -e.camera.position.y; - this._engine.update(); + + // 相机同步成功,通知 scene 开始渲染 + this.emit('cameraloaded'); }); } diff --git a/src/util/collision-index.js b/src/util/collision-index.js new file mode 100644 index 0000000000..1ef7165d7d --- /dev/null +++ b/src/util/collision-index.js @@ -0,0 +1,81 @@ +// import GridIndex from 'grid-index'; +// @mapbox/grid-index 并没有类似 hitTest 的单纯获取碰撞检测结果的方法,query 将导致计算大量多余的包围盒结果,因此使用改良版 +import GridIndex from '../util/grid-index'; +import { Vector4 } from '../core/three'; + +// 为 viewport 加上 buffer,避免边缘处的文本无法显示 +const viewportPadding = 100; + +/** + * 基于网格实现文本避让,大幅提升包围盒碰撞检测效率 + * @see https://zhuanlan.zhihu.com/p/74373214 + */ +export default class CollisionIndex { + constructor(width, height) { + 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; + } + + placeCollisionBox(collisionBox, mvpMatrix) { + 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 ] + }; + } + + insertCollisionBox(collisionBox, featureIndex) { + const key = { featureIndex }; + this.grid.insert(key, collisionBox[0], collisionBox[1], collisionBox[2], collisionBox[3]); + } + + /** + * 后续碰撞检测都需要投影到 viewport 坐标系 + * @param {THREE.Matrix4} mvpMatrix mvp矩阵 + * @param {number} x P20 平面坐标X + * @param {number} y P20 平面坐标Y + * @return {Point} projectedPoint + */ + project(mvpMatrix, x, y) { + const p = new Vector4(x, y, 0, 1) + .applyMatrix4(mvpMatrix); + + // GL 坐标系[-1, 1] -> viewport 坐标系[width, height] + return { + x: (((p.x / p.w + 1) / 2) * this.width) + viewportPadding, + y: (((-p.y / p.w + 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 + */ + isInsideGrid(x1, y1, x2, y2) { + return x2 >= 0 && x1 < this.gridRightBoundary && y2 >= 0 && y1 < this.gridBottomBoundary; + } +} diff --git a/src/util/font-util.js b/src/util/font-util.js index 42c2387cb3..db24c24e1f 100644 --- a/src/util/font-util.js +++ b/src/util/font-util.js @@ -1,3 +1,8 @@ +/** + * tiny-sdf 中每个 glyph 的宽度(加上 buffer 24 + 3 + 3 = 30) + */ +const glyphSizeInSDF = 30; + export function nextPowOfTwo(number) { return Math.pow(2, Math.ceil(Math.log2(number))); } @@ -17,18 +22,23 @@ export function buildMapping({ Array.from(characterSet).forEach((char, i) => { if (!mapping[char]) { const width = getFontWidth(char, i); - if (x + width + buffer * 2 > maxCanvasWidth) { + if (x + glyphSizeInSDF > maxCanvasWidth) { + // if (x + width + buffer * 2 > maxCanvasWidth) { x = 0; row++; } mapping[char] = { - x: x + buffer, - y: yOffset + row * (fontHeight + buffer * 2) + buffer, - width, - height: fontHeight, - mask: true + // x: x + buffer, + x, + y: yOffset + row * glyphSizeInSDF, + // y: yOffset + row * (fontHeight + buffer * 2) + buffer, + width: glyphSizeInSDF, + // height: fontHeight, + height: glyphSizeInSDF, + advance: width }; - x += width + buffer * 2; + // x += width + buffer * 2; + x += glyphSizeInSDF; } }); diff --git a/src/util/grid-index.js b/src/util/grid-index.js new file mode 100644 index 0000000000..d5ec365965 --- /dev/null +++ b/src/util/grid-index.js @@ -0,0 +1,131 @@ +/** + * 网格索引,相比 @mapbox/grid-index,在简单计算碰撞检测结果时效率更高 + * @see https://zhuanlan.zhihu.com/p/74373214 + */ +class GridIndex { + constructor(width, height, cellSize) { + 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; + } + + insert(key, x1, y1, x2, y2) { + 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); + } + + _insertBoxCell(x1, y1, x2, y2, cellIndex, uid) { + this.boxCells[cellIndex].push(uid); + } + + _query(x1, y1, x2, y2, hitTest, predicate) { + 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; + } + + query(x1, y1, x2, y2, predicate) { + return this._query(x1, y1, x2, y2, false, predicate); + } + + hitTest(x1, y1, x2, y2, predicate) { + return this._query(x1, y1, x2, y2, true, predicate); + } + + _queryCell(x1, y1, x2, y2, cellIndex, result, queryArgs, predicate) { + 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; + } + + _forEachCell(x1, y1, x2, y2, fn, arg1, arg2, predicate) { + 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; + } + } + } + + _convertToXCellCoord(x) { + return Math.max(0, Math.min(this.xCellCount - 1, Math.floor(x * this.xScale))); + } + + _convertToYCellCoord(y) { + return Math.max(0, Math.min(this.yCellCount - 1, Math.floor(y * this.yScale))); + } +} + +export default GridIndex; diff --git a/src/util/symbol-layout.js b/src/util/symbol-layout.js new file mode 100644 index 0000000000..5022cba51a --- /dev/null +++ b/src/util/symbol-layout.js @@ -0,0 +1,234 @@ +/** + * 返回文本相对锚点位置 + * @param {string} anchor 锚点位置 + * @return {alignment} alignment + */ +function getAnchorAlignment(anchor) { + let horizontalAlign = 0.5; + let verticalAlign = 0.5; + + switch (anchor) { + case 'right': + case 'top-right': + case 'bottom-right': + horizontalAlign = 1; + break; + case 'left': + case 'top-left': + case 'bottom-left': + horizontalAlign = 0; + break; + default: + horizontalAlign = 0.5; + } + + switch (anchor) { + case 'bottom': + case 'bottom-right': + case 'bottom-left': + verticalAlign = 1; + break; + case 'top': + case 'top-right': + case 'top-left': + verticalAlign = 0; + break; + default: + verticalAlign = 0.5; + } + + return { horizontalAlign, verticalAlign }; +} + +// justify right = 1, left = 0, center = 0.5 +function justifyLine( + positionedGlyphs, + glyphMap, + start, + end, + justify) { + if (!justify) { + return; + } + + const lastPositionedGlyph = positionedGlyphs[end]; + const glyph = lastPositionedGlyph.glyph; + if (glyph) { + const lastAdvance = glyphMap[glyph].advance * lastPositionedGlyph.scale; + const lineIndent = (positionedGlyphs[end].x + lastAdvance) * justify; + + for (let j = start; j <= end; j++) { + positionedGlyphs[j].x -= lineIndent; + } + } +} + +// justify right=1 left=0 center=0.5 +// horizontalAlign right=1 left=0 center=0.5 +// verticalAlign right=1 left=0 center=0.5 +function align( + positionedGlyphs, + justify, + horizontalAlign, + verticalAlign, + maxLineLength, + lineHeight, + lineCount +) { + const shiftX = (justify - horizontalAlign) * maxLineLength; + const shiftY = (-verticalAlign * lineCount + 0.5) * lineHeight; + + for (let j = 0; j < positionedGlyphs.length; j++) { + positionedGlyphs[j].x += shiftX; + positionedGlyphs[j].y += shiftY; + } +} + +function shapeLines( + shaping, + glyphMap, + lines, + lineHeight, + textAnchor, + textJustify, + spacing +) { + // buffer 为 4 + const yOffset = -8; + + let x = 0; + let y = yOffset; + + let maxLineLength = 0; + const positionedGlyphs = shaping.positionedGlyphs; + + const justify = + textJustify === 'right' ? 1 : + textJustify === 'left' ? 0 : 0.5; + + const lineStartIndex = positionedGlyphs.length; + lines.forEach(line => { + line.split('').forEach(char => { + const glyph = glyphMap[char]; + const baselineOffset = 0; + + if (glyph) { + positionedGlyphs.push({ + glyph: char, + x, + y: y + baselineOffset, + vertical: false, // TODO:目前只支持水平方向 + scale: 1, + metrics: glyph + }); + x += glyph.advance + spacing; + } + }); + + // 左右对齐 + if (positionedGlyphs.length !== lineStartIndex) { + const lineLength = x - spacing; + maxLineLength = Math.max(lineLength, maxLineLength); + justifyLine(positionedGlyphs, glyphMap, lineStartIndex, positionedGlyphs.length - 1, justify); + } + + x = 0; + y += lineHeight; + }); + + const { horizontalAlign, verticalAlign } = getAnchorAlignment(textAnchor); + align(positionedGlyphs, justify, horizontalAlign, verticalAlign, maxLineLength, lineHeight, lines.length); + + // 计算包围盒 + const height = y - yOffset; + + shaping.top += -verticalAlign * height; + shaping.bottom = shaping.top + height; + shaping.left += -horizontalAlign * maxLineLength; + shaping.right = shaping.left + maxLineLength; +} + +/** + * 计算文本中每个独立字符相对锚点的位置 + * + * @param {string} text 原始文本 + * @param {*} glyphs mapping + * @param {number} lineHeight 行高 + * @param {string} textAnchor 文本相对于锚点的位置 + * @param {string} textJustify 左右对齐 + * @param {number} spacing 字符间距 + * @param {[number, number]} translate 文本水平 & 垂直偏移量 + * @return {boolean|shaping} 每个字符相对于锚点的位置 + */ +export function shapeText( + text, + glyphs, + lineHeight, + textAnchor, + textJustify, + spacing, + translate +) { + + // TODO:处理换行 + const lines = text.split('\n'); + + const positionedGlyphs = []; + const shaping = { + positionedGlyphs, + top: translate[1], + bottom: translate[1], + left: translate[0], + right: translate[0], + lineCount: lines.length, + text + }; + + shapeLines(shaping, glyphs, lines, lineHeight, textAnchor, textJustify, spacing); + if (!positionedGlyphs.length) return false; + + return shaping; +} + +export function getGlyphQuads( + shaping, + textOffset, + alongLine +) { + const { positionedGlyphs } = shaping; + const quads = []; + + for (let k = 0; k < positionedGlyphs.length; k++) { + const positionedGlyph = positionedGlyphs[k]; + const rect = positionedGlyph.metrics; + + // The rects have an addditional buffer that is not included in their size. + const rectBuffer = 4; + + const halfAdvance = rect.advance * positionedGlyph.scale / 2; + + const glyphOffset = alongLine ? + [ positionedGlyph.x + halfAdvance, positionedGlyph.y ] : + [ 0, 0 ]; + + const builtInOffset = alongLine ? + [ 0, 0 ] : + [ positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1] ]; + + const x1 = (0 - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset[0]; + const y1 = (0 - rectBuffer) * positionedGlyph.scale + builtInOffset[1]; + const x2 = x1 + rect.width * positionedGlyph.scale; + const y2 = y1 + rect.height * positionedGlyph.scale; + + const tl = { x: x1, y: y1 }; + const tr = { x: x2, y: y1 }; + const bl = { x: x1, y: y2 }; + const br = { x: x2, y: y2 }; + + // TODO:处理字符旋转的情况 + + quads.push({ tl, tr, bl, br, tex: rect, glyphOffset }); + } + + return quads; +} From 367d24519dcc941a29e817cbbbf42a026853925c Mon Sep 17 00:00:00 2001 From: thinkinggis Date: Fri, 16 Aug 2019 11:50:25 +0800 Subject: [PATCH 3/3] merge github master --- rollup.config.js | 4 ++-- src/component/popup.js | 2 +- src/core/controller/mapping.js | 2 +- src/core/scene.js | 2 +- src/geom/buffer/buffer.js | 2 +- src/geom/buffer/heatmap/grid_3d.js | 6 +++--- src/geom/material/lineMaterial.js | 2 +- src/layer/tile/image_tile.js | 2 +- src/layer/tile/tile.js | 2 +- src/layer/tile/tile_layer.js | 4 ++-- src/layer/tile/vector_tile.js | 4 ++-- src/layer/tile/vector_tile_mesh.js | 4 ++-- src/source/parser/geojson.js | 2 +- src/source/source_cache.js | 4 ++-- src/util/polyline-normals copy.js | 6 +++--- src/util/polyline-normals.js | 6 +++--- 16 files changed, 27 insertions(+), 27 deletions(-) diff --git a/rollup.config.js b/rollup.config.js index f0d12db60b..5ea4e2c523 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -14,8 +14,8 @@ const production = BUILD === 'production'; const outputFile = !production ? 'build/L7.js' : minified - ? 'build/L7-min.js' - : 'build/L7-unminified.js'; + ? 'build/L7-min.js' + : 'build/L7-unminified.js'; const config = [ { diff --git a/src/component/popup.js b/src/component/popup.js index e7314c86d3..22e99f69e1 100644 --- a/src/component/popup.js +++ b/src/component/popup.js @@ -37,7 +37,7 @@ export default class Popup extends Base { this._container.appendChild(this._content); if (this.get('className')) { this.get('className').split(' ').forEach(name => - this._container.classList.add(name)); + this._container.classList.add(name)); } } if (this.get('maxWidth') && this._container.style.maxWidth !== this.get('maxWidth')) { diff --git a/src/core/controller/mapping.js b/src/core/controller/mapping.js index 5020431735..9f11125799 100644 --- a/src/core/controller/mapping.js +++ b/src/core/controller/mapping.js @@ -3,7 +3,7 @@ import Global from '../../global'; import ScaleController from './scale'; import Attr from '../../attr/index'; export default class Mapping { - /** 初始化mapping + /** 初始化mapping * 初始化mapping * @param {*} cfg 配置 * @param {*} cfg.layer layer对象 diff --git a/src/core/scene.js b/src/core/scene.js index 524ee5304c..00f169c056 100644 --- a/src/core/scene.js +++ b/src/core/scene.js @@ -190,7 +190,7 @@ export default class Scene extends Base { this.map.off('mapmove', this._updateRender); this.map.off('camerachange', this._updateRender); } - // control + // control addControl(ctr) { this.get('controlController').addControl(ctr); diff --git a/src/geom/buffer/buffer.js b/src/geom/buffer/buffer.js index 95b08d231a..86ea4294d1 100644 --- a/src/geom/buffer/buffer.js +++ b/src/geom/buffer/buffer.js @@ -112,7 +112,7 @@ export default class BufferBase extends Base { prePoint[0], prePoint[1], 0, nextPoint[0], nextPoint[1], 0 ], - positionOffset * 3); + positionOffset * 3); const indexArray = [ 1, 2, 0, 3, 2, 1 ].map(v => { return v + positionOffset; }); if (this.get('uv')) { this.attributes.uv.set([ 0.1, 0, 0, 0, 0.1, size / 2000, 0, size / 2000 ], positionOffset * 2); diff --git a/src/geom/buffer/heatmap/grid_3d.js b/src/geom/buffer/heatmap/grid_3d.js index 01b1e0bc51..71e0c68b87 100644 --- a/src/geom/buffer/heatmap/grid_3d.js +++ b/src/geom/buffer/heatmap/grid_3d.js @@ -41,7 +41,7 @@ export default class Grid3D extends BufferBase { this._encodeArray(feature, 4); this.attributes.positions.set([ x, y, size, x, y, size, x, y, size, x, y, size ], this._offset * 3); this.attributes.miters.set([ -1, 1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1 ], this._offset * 3); - this.attributes.normals.set([ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1 ], this._offset * 3); // top normal + this.attributes.normals.set([ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1 ], this._offset * 3); // top normal const indexArray = [ 0, 2, 1, 2, 3, 1 ].map(v => { return v + this._offset; }); this.indexArray.set(indexArray, this._offset * 1.5); this._offset += 4; @@ -60,7 +60,7 @@ export default class Grid3D extends BufferBase { -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, // left 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, // top 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0 // right - ], this._offset * 3); // top normal + ], this._offset * 3); // top normal for (let i = 0; i < 4; i++) { this.attributes.positions.set([ x, y, 1, x, y, 1, x, y, 1, x, y, 1 ], this._offset * 3); @@ -77,7 +77,7 @@ export default class Grid3D extends BufferBase { prePoint[0], prePoint[1], 0, nextPoint[0], nextPoint[1], 0 ], - positionOffset * 3); + positionOffset * 3); const indexArray = [ 0, 1, 2, 1, 3, 2 ].map(v => { return v + positionOffset; }); if (this.get('uv')) { // temp 点亮城市demo diff --git a/src/geom/material/lineMaterial.js b/src/geom/material/lineMaterial.js index 8ff74a6a78..47ab63fd7c 100644 --- a/src/geom/material/lineMaterial.js +++ b/src/geom/material/lineMaterial.js @@ -16,7 +16,7 @@ export function LineMaterial(options) { vertexShader: vs, fragmentShader: fs, transparent: true - // blending: THREE.AdditiveBlending + // blending: THREE.AdditiveBlending }); return material; } diff --git a/src/layer/tile/image_tile.js b/src/layer/tile/image_tile.js index 73c7c335e6..1888fa4c99 100644 --- a/src/layer/tile/image_tile.js +++ b/src/layer/tile/image_tile.js @@ -7,7 +7,7 @@ export default class ImageTile extends Tile { // Making this asynchronous really speeds up the LOD framerate setTimeout(() => { if (!this._mesh) { - // this._mesh = this._createMesh(); + // this._mesh = this._createMesh(); this._requestTile(); } }, 0); diff --git a/src/layer/tile/tile.js b/src/layer/tile/tile.js index 7c862018f2..71b2552a3b 100644 --- a/src/layer/tile/tile.js +++ b/src/layer/tile/tile.js @@ -80,7 +80,7 @@ export default class Tile extends Base { return urlParams[key]; }); } - // 经纬度范围转瓦片范围 + // 经纬度范围转瓦片范围 _tileBounds(lnglatBound) { const ne = this.layer.scene.project([ lnglatBound.getNorthEast().lng, lnglatBound.getNorthEast().lat ]); const sw = this.layer.scene.project([ lnglatBound.getSouthWest().lng, lnglatBound.getSouthWest().lat ]); diff --git a/src/layer/tile/tile_layer.js b/src/layer/tile/tile_layer.js index 84c3e9a7d3..52c22485d0 100644 --- a/src/layer/tile/tile_layer.js +++ b/src/layer/tile/tile_layer.js @@ -116,7 +116,7 @@ export default class TileLayer extends Layer { const tileRange = this._pxBoundsToTileRange(pixelBounds); const margin = this.get('keepBuffer'); this.noPruneRange = new Bounds(tileRange.getBottomLeft().subtract([ margin, -margin ]), - tileRange.getTopRight().add([ margin, -margin ])); + tileRange.getTopRight().add([ margin, -margin ])); if (!(isFinite(tileRange.min.x) && isFinite(tileRange.min.y) && isFinite(tileRange.max.x) && @@ -424,7 +424,7 @@ export default class TileLayer extends Layer { if (!this._tiles.children.length > 0 || !this._object3D.visible) { return; } - // 更新数据颜色 过滤 filter + // 更新数据颜色 过滤 filter if (!Util.isEqual(preAttrs.color, nextAttrs.color) || !Util.isEqual(preAttrs.filter, nextAttrs.filter)) { this._tileCache.setNeedUpdate(); this._tiles.children.forEach(tile => { diff --git a/src/layer/tile/vector_tile.js b/src/layer/tile/vector_tile.js index a8defd052c..39def90153 100644 --- a/src/layer/tile/vector_tile.js +++ b/src/layer/tile/vector_tile.js @@ -58,9 +58,9 @@ export default class VectorTile extends Tile { context.depthMask(false); renderer.clearDepth(); - // only render where stencil is set to 1 + // only render where stencil is set to 1 - context.stencilFunc(context.EQUAL, 1, 0xffffffff); // draw if == 1 + context.stencilFunc(context.EQUAL, 1, 0xffffffff); // draw if == 1 context.stencilOp(context.KEEP, context.KEEP, context.KEEP); } _tileMaskMesh() { diff --git a/src/layer/tile/vector_tile_mesh.js b/src/layer/tile/vector_tile_mesh.js index 531b33a25a..afb66aaf33 100644 --- a/src/layer/tile/vector_tile_mesh.js +++ b/src/layer/tile/vector_tile_mesh.js @@ -79,9 +79,9 @@ export default class VectorTileMesh { context.depthMask(false); renderer.clearDepth(); - // only render where stencil is set to 1 + // only render where stencil is set to 1 - context.stencilFunc(context.EQUAL, 1, 0xffffffff); // draw if == 1 + context.stencilFunc(context.EQUAL, 1, 0xffffffff); // draw if == 1 context.stencilOp(context.KEEP, context.KEEP, context.KEEP); } _tileMaskMesh() { diff --git a/src/source/parser/geojson.js b/src/source/parser/geojson.js index ac70b2fbdf..6ccf286b0c 100644 --- a/src/source/parser/geojson.js +++ b/src/source/parser/geojson.js @@ -3,7 +3,7 @@ import { getCoords } from '@turf/invariant'; import { djb2hash } from '../../util/bkdr-hash'; import rewind from '@mapbox/geojson-rewind'; export default function geoJSON(data, cfg) { - // 矢量瓦片图层不做 rewind + // 矢量瓦片图层不做 rewind rewind(data, true); const resultData = []; diff --git a/src/source/source_cache.js b/src/source/source_cache.js index 3f1c0d3898..4deb0ecce6 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -122,7 +122,7 @@ export default class SouceCache extends Base { _calculateTileIDs() { this._tileMap = {}; this.updateTileList = []; - const zoom = Math.floor(this.scene.getZoom()); // - window.window.devicePixelRatio + 1; // zoom - 1 + const zoom = Math.floor(this.scene.getZoom()); // - window.window.devicePixelRatio + 1; // zoom - 1 const minSourceZoom = this.get('minZoom'); const maxSourceZoom = this.get('maxZoom'); this.tileZoom = zoom > maxSourceZoom ? maxSourceZoom : zoom; @@ -300,7 +300,7 @@ export default class SouceCache extends Base { } } } - // 地图拾取 + // 地图拾取 _addPickMesh(layer, meshObj) { if (this.type === 'image') { return; diff --git a/src/util/polyline-normals copy.js b/src/util/polyline-normals copy.js index 1be6aa15fe..b84b4b0f84 100644 --- a/src/util/polyline-normals copy.js +++ b/src/util/polyline-normals copy.js @@ -69,9 +69,9 @@ export default function(points, closed, indexOffset) { attrIndex.push([ index + 0, index + 2, index + 1 ]); - // no miter, simple segment + // no miter, simple segment if (!next) { - // reset normal + // reset normal normal(_normal, lineA); extrusions(attrPos, out, cur, _normal, 1); attrDistance.push(d, d); @@ -97,7 +97,7 @@ export default function(points, closed, indexOffset) { attrDistance.push(d, d); attrIndex.push( _lastFlip === 1 ? [ index + 1, index + 3, index + 2 ] - : [ index, index + 2, index + 3 ] + : [ index, index + 2, index + 3 ] ); // 避免在 Material 中使用 THREE.DoubleSide diff --git a/src/util/polyline-normals.js b/src/util/polyline-normals.js index 25288ea32c..b3cd09a3aa 100644 --- a/src/util/polyline-normals.js +++ b/src/util/polyline-normals.js @@ -71,9 +71,9 @@ export default function(points, closed, indexOffset) { attrIndex.push(index + 0, index + 2, index + 1); - // no miter, simple segment + // no miter, simple segment if (!next) { - // reset normal + // reset normal normal(_normal, lineA); extrusions(attrPos, out, miters, cur, _normal, 1); attrDistance.push(d, d); @@ -98,7 +98,7 @@ export default function(points, closed, indexOffset) { extrusions(attrPos, out, miters, cur, _normal, 1); attrDistance.push(d, d); const indexData = _lastFlip === 1 ? [ index + 1, index + 3, index + 2 ] - : [ index, index + 2, index + 3 ]; + : [ index, index + 2, index + 3 ]; attrIndex.push(...indexData); // 避免在 Material 中使用 THREE.DoubleSide