diff --git a/build/generate-struct-arrays.js b/build/generate-struct-arrays.js index 92f8bfde695..ccbc1ecd179 100644 --- a/build/generate-struct-arrays.js +++ b/build/generate-struct-arrays.js @@ -151,6 +151,7 @@ const { collisionBoxLayout, collisionCircleLayout, collisionVertexAttributes, + quadTriangle, placement, symbolInstance, glyphOffset, @@ -164,6 +165,7 @@ createStructArrayType('collision_box', collisionBox, true); createStructArrayType(`collision_box_layout`, collisionBoxLayout); createStructArrayType(`collision_circle_layout`, collisionCircleLayout); createStructArrayType(`collision_vertex`, collisionVertexAttributes); +createStructArrayType(`quad_triangle`, quadTriangle); createStructArrayType('placed_symbol', placement, true); createStructArrayType('symbol_instance', symbolInstance, true); createStructArrayType('glyph_offset', glyphOffset, true); diff --git a/src/data/array_types.js b/src/data/array_types.js index c502d723a53..30b2c8c5f2f 100644 --- a/src/data/array_types.js +++ b/src/data/array_types.js @@ -304,11 +304,10 @@ register('StructArrayLayout1ul4', StructArrayLayout1ul4); * [0]: Int16[6] * [12]: Uint32[1] * [16]: Uint16[2] - * [20]: Int16[2] * * @private */ -class StructArrayLayout6i1ul2ui2i24 extends StructArray { +class StructArrayLayout6i1ul2ui20 extends StructArray { uint8: Uint8Array; int16: Int16Array; uint32: Uint32Array; @@ -321,15 +320,15 @@ class StructArrayLayout6i1ul2ui2i24 extends StructArray { this.uint16 = new Uint16Array(this.arrayBuffer); } - emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number, v9: number, v10: number) { + emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number) { const i = this.length; this.resize(i + 1); - return this.emplace(i, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10); + return this.emplace(i, v0, v1, v2, v3, v4, v5, v6, v7, v8); } - emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number, v9: number, v10: number) { - const o2 = i * 12; - const o4 = i * 6; + emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number) { + const o2 = i * 10; + const o4 = i * 5; this.int16[o2 + 0] = v0; this.int16[o2 + 1] = v1; this.int16[o2 + 2] = v2; @@ -339,14 +338,12 @@ class StructArrayLayout6i1ul2ui2i24 extends StructArray { this.uint32[o4 + 3] = v6; this.uint16[o2 + 8] = v7; this.uint16[o2 + 9] = v8; - this.int16[o2 + 10] = v9; - this.int16[o2 + 11] = v10; return i; } } -StructArrayLayout6i1ul2ui2i24.prototype.bytesPerElement = 24; -register('StructArrayLayout6i1ul2ui2i24', StructArrayLayout6i1ul2ui2i24); +StructArrayLayout6i1ul2ui20.prototype.bytesPerElement = 20; +register('StructArrayLayout6i1ul2ui20', StructArrayLayout6i1ul2ui20); /** * Implementation of the StructArray layout: @@ -386,6 +383,46 @@ class StructArrayLayout2i2i2i12 extends StructArray { StructArrayLayout2i2i2i12.prototype.bytesPerElement = 12; register('StructArrayLayout2i2i2i12', StructArrayLayout2i2i2i12); +/** + * Implementation of the StructArray layout: + * [0]: Float32[2] + * [8]: Float32[1] + * [12]: Int16[2] + * + * @private + */ +class StructArrayLayout2f1f2i16 extends StructArray { + uint8: Uint8Array; + float32: Float32Array; + int16: Int16Array; + + _refreshViews() { + this.uint8 = new Uint8Array(this.arrayBuffer); + this.float32 = new Float32Array(this.arrayBuffer); + this.int16 = new Int16Array(this.arrayBuffer); + } + + emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number) { + const i = this.length; + this.resize(i + 1); + return this.emplace(i, v0, v1, v2, v3, v4); + } + + emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number) { + const o4 = i * 4; + const o2 = i * 8; + this.float32[o4 + 0] = v0; + this.float32[o4 + 1] = v1; + this.float32[o4 + 2] = v2; + this.int16[o2 + 6] = v3; + this.int16[o2 + 7] = v4; + return i; + } +} + +StructArrayLayout2f1f2i16.prototype.bytesPerElement = 16; +register('StructArrayLayout2f1f2i16', StructArrayLayout2f1f2i16); + /** * Implementation of the StructArray layout: * [0]: Uint8[2] @@ -422,6 +459,39 @@ class StructArrayLayout2ub2f12 extends StructArray { StructArrayLayout2ub2f12.prototype.bytesPerElement = 12; register('StructArrayLayout2ub2f12', StructArrayLayout2ub2f12); +/** + * Implementation of the StructArray layout: + * [0]: Uint16[3] + * + * @private + */ +class StructArrayLayout3ui6 extends StructArray { + uint8: Uint8Array; + uint16: Uint16Array; + + _refreshViews() { + this.uint8 = new Uint8Array(this.arrayBuffer); + this.uint16 = new Uint16Array(this.arrayBuffer); + } + + emplaceBack(v0: number, v1: number, v2: number) { + const i = this.length; + this.resize(i + 1); + return this.emplace(i, v0, v1, v2); + } + + emplace(i: number, v0: number, v1: number, v2: number) { + const o2 = i * 3; + this.uint16[o2 + 0] = v0; + this.uint16[o2 + 1] = v1; + this.uint16[o2 + 2] = v2; + return i; + } +} + +StructArrayLayout3ui6.prototype.bytesPerElement = 6; +register('StructArrayLayout3ui6', StructArrayLayout3ui6); + /** * Implementation of the StructArray layout: * [0]: Int16[2] @@ -487,13 +557,13 @@ register('StructArrayLayout2i2ui3ul3ui2f3ub1ul1i48', StructArrayLayout2i2ui3ul3u /** * Implementation of the StructArray layout: * [0]: Int16[8] - * [16]: Uint16[14] - * [44]: Uint32[1] - * [48]: Float32[3] + * [16]: Uint16[15] + * [48]: Uint32[1] + * [52]: Float32[4] * * @private */ -class StructArrayLayout8i14ui1ul3f60 extends StructArray { +class StructArrayLayout8i15ui1ul4f68 extends StructArray { uint8: Uint8Array; int16: Int16Array; uint16: Uint16Array; @@ -508,15 +578,15 @@ class StructArrayLayout8i14ui1ul3f60 extends StructArray { this.float32 = new Float32Array(this.arrayBuffer); } - emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number, v9: number, v10: number, v11: number, v12: number, v13: number, v14: number, v15: number, v16: number, v17: number, v18: number, v19: number, v20: number, v21: number, v22: number, v23: number, v24: number, v25: number) { + emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number, v9: number, v10: number, v11: number, v12: number, v13: number, v14: number, v15: number, v16: number, v17: number, v18: number, v19: number, v20: number, v21: number, v22: number, v23: number, v24: number, v25: number, v26: number, v27: number) { const i = this.length; this.resize(i + 1); - return this.emplace(i, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25); + return this.emplace(i, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27); } - emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number, v9: number, v10: number, v11: number, v12: number, v13: number, v14: number, v15: number, v16: number, v17: number, v18: number, v19: number, v20: number, v21: number, v22: number, v23: number, v24: number, v25: number) { - const o2 = i * 30; - const o4 = i * 15; + emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number, v9: number, v10: number, v11: number, v12: number, v13: number, v14: number, v15: number, v16: number, v17: number, v18: number, v19: number, v20: number, v21: number, v22: number, v23: number, v24: number, v25: number, v26: number, v27: number) { + const o2 = i * 34; + const o4 = i * 17; this.int16[o2 + 0] = v0; this.int16[o2 + 1] = v1; this.int16[o2 + 2] = v2; @@ -539,16 +609,18 @@ class StructArrayLayout8i14ui1ul3f60 extends StructArray { this.uint16[o2 + 19] = v19; this.uint16[o2 + 20] = v20; this.uint16[o2 + 21] = v21; - this.uint32[o4 + 11] = v22; - this.float32[o4 + 12] = v23; + this.uint16[o2 + 22] = v22; + this.uint32[o4 + 12] = v23; this.float32[o4 + 13] = v24; this.float32[o4 + 14] = v25; + this.float32[o4 + 15] = v26; + this.float32[o4 + 16] = v27; return i; } } -StructArrayLayout8i14ui1ul3f60.prototype.bytesPerElement = 60; -register('StructArrayLayout8i14ui1ul3f60', StructArrayLayout8i14ui1ul3f60); +StructArrayLayout8i15ui1ul4f68.prototype.bytesPerElement = 68; +register('StructArrayLayout8i15ui1ul4f68', StructArrayLayout8i15ui1ul4f68); /** * Implementation of the StructArray layout: @@ -651,39 +723,6 @@ class StructArrayLayout1ul2ui8 extends StructArray { StructArrayLayout1ul2ui8.prototype.bytesPerElement = 8; register('StructArrayLayout1ul2ui8', StructArrayLayout1ul2ui8); -/** - * Implementation of the StructArray layout: - * [0]: Uint16[3] - * - * @private - */ -class StructArrayLayout3ui6 extends StructArray { - uint8: Uint8Array; - uint16: Uint16Array; - - _refreshViews() { - this.uint8 = new Uint8Array(this.arrayBuffer); - this.uint16 = new Uint16Array(this.arrayBuffer); - } - - emplaceBack(v0: number, v1: number, v2: number) { - const i = this.length; - this.resize(i + 1); - return this.emplace(i, v0, v1, v2); - } - - emplace(i: number, v0: number, v1: number, v2: number) { - const o2 = i * 3; - this.uint16[o2 + 0] = v0; - this.uint16[o2 + 1] = v1; - this.uint16[o2 + 2] = v2; - return i; - } -} - -StructArrayLayout3ui6.prototype.bytesPerElement = 6; -register('StructArrayLayout3ui6', StructArrayLayout3ui6); - /** * Implementation of the StructArray layout: * [0]: Uint16[2] @@ -824,8 +863,6 @@ class CollisionBoxStruct extends Struct { featureIndex: number; sourceLayerIndex: number; bucketIndex: number; - radius: number; - signedDistanceFromAnchor: number; anchorPoint: Point; get anchorPointX() { return this._structArray.int16[this._pos2 + 0]; } get anchorPointY() { return this._structArray.int16[this._pos2 + 1]; } @@ -836,19 +873,17 @@ class CollisionBoxStruct extends Struct { get featureIndex() { return this._structArray.uint32[this._pos4 + 3]; } get sourceLayerIndex() { return this._structArray.uint16[this._pos2 + 8]; } get bucketIndex() { return this._structArray.uint16[this._pos2 + 9]; } - get radius() { return this._structArray.int16[this._pos2 + 10]; } - get signedDistanceFromAnchor() { return this._structArray.int16[this._pos2 + 11]; } get anchorPoint() { return new Point(this.anchorPointX, this.anchorPointY); } } -CollisionBoxStruct.prototype.size = 24; +CollisionBoxStruct.prototype.size = 20; export type CollisionBox = CollisionBoxStruct; /** * @private */ -export class CollisionBoxArray extends StructArrayLayout6i1ul2ui2i24 { +export class CollisionBoxArray extends StructArrayLayout6i1ul2ui20 { /** * Return the CollisionBoxStruct at the given location in the array. * @param {number} index The index of the element. @@ -948,10 +983,12 @@ class SymbolInstanceStruct extends Struct { numVerticalGlyphVertices: number; numIconVertices: number; numVerticalIconVertices: number; + useRuntimeCollisionCircles: number; crossTileID: number; textBoxScale: number; textOffset0: number; textOffset1: number; + collisionCircleDiameter: number; get anchorX() { return this._structArray.int16[this._pos2 + 0]; } get anchorY() { return this._structArray.int16[this._pos2 + 1]; } get rightJustifiedTextSymbolIndex() { return this._structArray.int16[this._pos2 + 2]; } @@ -974,21 +1011,23 @@ class SymbolInstanceStruct extends Struct { get numVerticalGlyphVertices() { return this._structArray.uint16[this._pos2 + 19]; } get numIconVertices() { return this._structArray.uint16[this._pos2 + 20]; } get numVerticalIconVertices() { return this._structArray.uint16[this._pos2 + 21]; } - get crossTileID() { return this._structArray.uint32[this._pos4 + 11]; } - set crossTileID(x: number) { this._structArray.uint32[this._pos4 + 11] = x; } - get textBoxScale() { return this._structArray.float32[this._pos4 + 12]; } - get textOffset0() { return this._structArray.float32[this._pos4 + 13]; } - get textOffset1() { return this._structArray.float32[this._pos4 + 14]; } + get useRuntimeCollisionCircles() { return this._structArray.uint16[this._pos2 + 22]; } + get crossTileID() { return this._structArray.uint32[this._pos4 + 12]; } + set crossTileID(x: number) { this._structArray.uint32[this._pos4 + 12] = x; } + get textBoxScale() { return this._structArray.float32[this._pos4 + 13]; } + get textOffset0() { return this._structArray.float32[this._pos4 + 14]; } + get textOffset1() { return this._structArray.float32[this._pos4 + 15]; } + get collisionCircleDiameter() { return this._structArray.float32[this._pos4 + 16]; } } -SymbolInstanceStruct.prototype.size = 60; +SymbolInstanceStruct.prototype.size = 68; export type SymbolInstance = SymbolInstanceStruct; /** * @private */ -export class SymbolInstanceArray extends StructArrayLayout8i14ui1ul3f60 { +export class SymbolInstanceArray extends StructArrayLayout8i15ui1ul4f68 { /** * Return the SymbolInstanceStruct at the given location in the array. * @param {number} index The index of the element. @@ -1062,15 +1101,16 @@ export { StructArrayLayout4i4ui4i24, StructArrayLayout3f12, StructArrayLayout1ul4, - StructArrayLayout6i1ul2ui2i24, + StructArrayLayout6i1ul2ui20, StructArrayLayout2i2i2i12, + StructArrayLayout2f1f2i16, StructArrayLayout2ub2f12, + StructArrayLayout3ui6, StructArrayLayout2i2ui3ul3ui2f3ub1ul1i48, - StructArrayLayout8i14ui1ul3f60, + StructArrayLayout8i15ui1ul4f68, StructArrayLayout1f4, StructArrayLayout3i6, StructArrayLayout1ul2ui8, - StructArrayLayout3ui6, StructArrayLayout2ui4, StructArrayLayout1ui2, StructArrayLayout2f8, @@ -1087,8 +1127,9 @@ export { StructArrayLayout3f12 as SymbolDynamicLayoutArray, StructArrayLayout1ul4 as SymbolOpacityArray, StructArrayLayout2i2i2i12 as CollisionBoxLayoutArray, - StructArrayLayout2i2i2i12 as CollisionCircleLayoutArray, + StructArrayLayout2f1f2i16 as CollisionCircleLayoutArray, StructArrayLayout2ub2f12 as CollisionVertexArray, + StructArrayLayout3ui6 as QuadTriangleArray, StructArrayLayout3ui6 as TriangleIndexArray, StructArrayLayout2ui4 as LineIndexArray, StructArrayLayout1ui2 as LineStripIndexArray diff --git a/src/data/bucket/symbol_attributes.js b/src/data/bucket/symbol_attributes.js index 014d922e595..5d307d2ada6 100644 --- a/src/data/bucket/symbol_attributes.js +++ b/src/data/bucket/symbol_attributes.js @@ -38,12 +38,6 @@ export const collisionBox = createLayout([ {type: 'Uint16', name: 'sourceLayerIndex'}, // the bucket the feature appears in {type: 'Uint16', name: 'bucketIndex'}, - - // collision circles for lines store their distance to the anchor in tile units - // so that they can be ignored if the projected label doesn't extend into - // the box area - {type: 'Int16', name: 'radius'}, - {type: 'Int16', name: 'signedDistanceFromAnchor'} ]); export const collisionBoxLayout = createLayout([ // used to render collision boxes for debugging purposes @@ -53,11 +47,15 @@ export const collisionBoxLayout = createLayout([ // used to render collision box ], 4); export const collisionCircleLayout = createLayout([ // used to render collision circles for debugging purposes - {name: 'a_pos', components: 2, type: 'Int16'}, - {name: 'a_anchor_pos', components: 2, type: 'Int16'}, - {name: 'a_extrude', components: 2, type: 'Int16'} + {name: 'a_pos', components: 2, type: 'Float32'}, + {name: 'a_radius', components: 1, type: 'Float32'}, + {name: 'a_flags', components: 2, type: 'Int16'} ], 4); +export const quadTriangle = createLayout([ + {name: 'triangle', components: 3, type: 'Uint16'}, +]); + export const placement = createLayout([ {type: 'Int16', name: 'anchorX'}, {type: 'Int16', name: 'anchorY'}, @@ -101,9 +99,11 @@ export const symbolInstance = createLayout([ {type: 'Uint16', name: 'numVerticalGlyphVertices'}, {type: 'Uint16', name: 'numIconVertices'}, {type: 'Uint16', name: 'numVerticalIconVertices'}, + {type: 'Uint16', name: 'useRuntimeCollisionCircles'}, {type: 'Uint32', name: 'crossTileID'}, {type: 'Float32', name: 'textBoxScale'}, - {type: 'Float32', components: 2, name: 'textOffset'} + {type: 'Float32', components: 2, name: 'textOffset'}, + {type: 'Float32', name: 'collisionCircleDiameter'}, ]); export const glyphOffset = createLayout([ diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 2e57143c896..cd7aadfaf66 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -3,7 +3,6 @@ import {symbolLayoutAttributes, collisionVertexAttributes, collisionBoxLayout, - collisionCircleLayout, dynamicLayoutAttributes } from './symbol_attributes'; @@ -11,7 +10,6 @@ import {SymbolLayoutArray, SymbolDynamicLayoutArray, SymbolOpacityArray, CollisionBoxLayoutArray, - CollisionCircleLayoutArray, CollisionVertexArray, PlacedSymbolArray, SymbolInstanceArray, @@ -39,6 +37,7 @@ import EvaluationParameters from '../../style/evaluation_parameters'; import Formatted from '../../style-spec/expression/types/formatted'; import ResolvedImage from '../../style-spec/expression/types/resolved_image'; import {plugin as globalRTLTextPlugin, getRTLTextPluginStatus} from '../../source/rtl_text_plugin'; +import {mat4} from 'gl-matrix'; import type {CanonicalTileID} from '../../source/tile_id'; import type { @@ -72,7 +71,6 @@ export type CollisionArrays = { verticalTextBox?: SingleCollisionBox; iconBox?: SingleCollisionBox; verticalIconBox?: SingleCollisionBox; - textCircles?: Array; textFeatureIndex?: number; verticalTextFeatureIndex?: number; iconFeatureIndex?: number; @@ -280,8 +278,6 @@ register('CollisionBuffers', CollisionBuffers); * `this.icons`: SymbolBuffers for icons * `this.iconCollisionBox`: Debug SymbolBuffers for icon collision boxes * `this.textCollisionBox`: Debug SymbolBuffers for text collision boxes - * `this.iconCollisionCircle`: Debug SymbolBuffers for icon collision circles - * `this.textCollisionCircle`: Debug SymbolBuffers for text collision circles * The results are sent to the foreground for rendering * * 4. performSymbolPlacement(bucket, collisionIndex) is run on the foreground, @@ -329,12 +325,14 @@ class SymbolBucket implements Bucket { sortedAngle: number; featureSortOrder: Array; + collisionCircleArray: Array; + placementInvProjMatrix: mat4; + placementViewportMatrix: mat4; + text: SymbolBuffers; icon: SymbolBuffers; textCollisionBox: CollisionBuffers; iconCollisionBox: CollisionBuffers; - textCollisionCircle: CollisionBuffers; - iconCollisionCircle: CollisionBuffers; uploaded: boolean; sourceLayerIndex: number; sourceID: string; @@ -356,6 +354,10 @@ class SymbolBucket implements Bucket { this.hasRTLText = false; this.sortKeyRanges = []; + this.collisionCircleArray = []; + this.placementInvProjMatrix = mat4.identity([]); + this.placementViewportMatrix = mat4.identity([]); + const layer = this.layers[0]; const unevaluatedLayoutValues = layer._unevaluatedLayout._values; @@ -552,8 +554,6 @@ class SymbolBucket implements Bucket { if (!this.uploaded && this.hasDebugData()) { this.textCollisionBox.upload(context); this.iconCollisionBox.upload(context); - this.textCollisionCircle.upload(context); - this.iconCollisionCircle.upload(context); } this.text.upload(context, this.sortFeaturesByY, !this.uploaded, this.text.programConfigurations.needsUpload); this.icon.upload(context, this.sortFeaturesByY, !this.uploaded, this.icon.programConfigurations.needsUpload); @@ -563,8 +563,6 @@ class SymbolBucket implements Bucket { destroyDebugData() { this.textCollisionBox.destroy(); this.iconCollisionBox.destroy(); - this.textCollisionCircle.destroy(); - this.iconCollisionCircle.destroy(); } destroy() { @@ -682,7 +680,7 @@ class SymbolBucket implements Bucket { Math.round(extrude.y)); } - addCollisionDebugVertices(x1: number, y1: number, x2: number, y2: number, arrays: CollisionBuffers, boxAnchorPoint: Point, symbolInstance: SymbolInstance, isCircle: boolean) { + addCollisionDebugVertices(x1: number, y1: number, x2: number, y2: number, arrays: CollisionBuffers, boxAnchorPoint: Point, symbolInstance: SymbolInstance) { const segment = arrays.segments.prepareSegment(4, arrays.layoutVertexArray, arrays.indexArray); const index = segment.vertexLength; @@ -698,21 +696,14 @@ class SymbolBucket implements Bucket { this._addCollisionDebugVertex(layoutVertexArray, collisionVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(x1, y2)); segment.vertexLength += 4; - if (isCircle) { - const indexArray: TriangleIndexArray = (arrays.indexArray: any); - indexArray.emplaceBack(index, index + 1, index + 2); - indexArray.emplaceBack(index, index + 2, index + 3); - segment.primitiveLength += 2; - } else { - const indexArray: LineIndexArray = (arrays.indexArray: any); - indexArray.emplaceBack(index, index + 1); - indexArray.emplaceBack(index + 1, index + 2); - indexArray.emplaceBack(index + 2, index + 3); - indexArray.emplaceBack(index + 3, index); + const indexArray: LineIndexArray = (arrays.indexArray: any); + indexArray.emplaceBack(index, index + 1); + indexArray.emplaceBack(index + 1, index + 2); + indexArray.emplaceBack(index + 2, index + 3); + indexArray.emplaceBack(index + 3, index); - segment.primitiveLength += 4; - } + segment.primitiveLength += 4; } addDebugCollisionBoxes(startIndex: number, endIndex: number, symbolInstance: SymbolInstance, isText: boolean) { @@ -723,12 +714,9 @@ class SymbolBucket implements Bucket { const x2 = box.x2; const y2 = box.y2; - // If the radius > 0, this collision box is actually a circle - // The data we add to the buffers is exactly the same, but we'll render with a different shader. - const isCircle = box.radius > 0; - this.addCollisionDebugVertices(x1, y1, x2, y2, isCircle ? - (isText ? this.textCollisionCircle : this.iconCollisionCircle) : (isText ? this.textCollisionBox : this.iconCollisionBox), - box.anchorPoint, symbolInstance, isCircle); + this.addCollisionDebugVertices(x1, y1, x2, y2, + isText ? this.textCollisionBox : this.iconCollisionBox, + box.anchorPoint, symbolInstance); } } @@ -739,8 +727,6 @@ class SymbolBucket implements Bucket { this.textCollisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray); this.iconCollisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray); - this.textCollisionCircle = new CollisionBuffers(CollisionCircleLayoutArray, collisionCircleLayout.members, TriangleIndexArray); - this.iconCollisionCircle = new CollisionBuffers(CollisionCircleLayoutArray, collisionCircleLayout.members, TriangleIndexArray); for (let i = 0; i < this.symbolInstances.length; i++) { const symbolInstance = this.symbolInstances.get(i); @@ -762,44 +748,29 @@ class SymbolBucket implements Bucket { const collisionArrays = {}; for (let k = textStartIndex; k < textEndIndex; k++) { const box: CollisionBox = (collisionBoxArray.get(k): any); - if (box.radius === 0) { - collisionArrays.textBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; - collisionArrays.textFeatureIndex = box.featureIndex; - break; // Only one box allowed per instance - } else { - if (!collisionArrays.textCircles) { - collisionArrays.textCircles = []; - collisionArrays.textFeatureIndex = box.featureIndex; - } - const used = 1; // May be updated at collision detection time - collisionArrays.textCircles.push(box.anchorPointX, box.anchorPointY, box.radius, box.signedDistanceFromAnchor, used); - } + collisionArrays.textBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; + collisionArrays.textFeatureIndex = box.featureIndex; + break; // Only one box allowed per instance } for (let k = verticalTextStartIndex; k < verticalTextEndIndex; k++) { const box: CollisionBox = (collisionBoxArray.get(k): any); - if (box.radius === 0) { - collisionArrays.verticalTextBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; - collisionArrays.verticalTextFeatureIndex = box.featureIndex; - break; // Only one box allowed per instance - } + collisionArrays.verticalTextBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; + collisionArrays.verticalTextFeatureIndex = box.featureIndex; + break; // Only one box allowed per instance } for (let k = iconStartIndex; k < iconEndIndex; k++) { // An icon can only have one box now, so this indexing is a bit vestigial... const box: CollisionBox = (collisionBoxArray.get(k): any); - if (box.radius === 0) { - collisionArrays.iconBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; - collisionArrays.iconFeatureIndex = box.featureIndex; - break; // Only one box allowed per instance - } + collisionArrays.iconBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; + collisionArrays.iconFeatureIndex = box.featureIndex; + break; // Only one box allowed per instance } for (let k = verticalIconStartIndex; k < verticalIconEndIndex; k++) { // An icon can only have one box now, so this indexing is a bit vestigial... const box: CollisionBox = (collisionBoxArray.get(k): any); - if (box.radius === 0) { - collisionArrays.verticalIconBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; - collisionArrays.verticalIconFeatureIndex = box.featureIndex; - break; // Only one box allowed per instance - } + collisionArrays.verticalIconBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY}; + collisionArrays.verticalIconFeatureIndex = box.featureIndex; + break; // Only one box allowed per instance } return collisionArrays; } @@ -831,7 +802,7 @@ class SymbolBucket implements Bucket { } hasDebugData() { - return this.textCollisionBox && this.iconCollisionBox && this.textCollisionCircle && this.iconCollisionCircle; + return this.textCollisionBox && this.iconCollisionBox; } hasTextCollisionBoxData() { @@ -842,14 +813,6 @@ class SymbolBucket implements Bucket { return this.hasDebugData() && this.iconCollisionBox.segments.get().length > 0; } - hasTextCollisionCircleData() { - return this.hasDebugData() && this.textCollisionCircle.segments.get().length > 0; - } - - hasIconCollisionCircleData() { - return this.hasDebugData() && this.iconCollisionCircle.segments.get().length > 0; - } - addIndicesForPlacedSymbol(iconOrText: SymbolBuffers, placedSymbolIndex: number) { const placedSymbol = iconOrText.placedSymbolArray.get(placedSymbolIndex); diff --git a/src/render/draw_collision_debug.js b/src/render/draw_collision_debug.js index 47369dd63ec..473238be026 100644 --- a/src/render/draw_collision_debug.js +++ b/src/render/draw_collision_debug.js @@ -8,28 +8,68 @@ import type SymbolBucket from '../data/bucket/symbol_bucket'; import DepthMode from '../gl/depth_mode'; import StencilMode from '../gl/stencil_mode'; import CullFaceMode from '../gl/cull_face_mode'; -import {collisionUniformValues} from './program/collision_program'; +import {collisionUniformValues, collisionCircleUniformValues} from './program/collision_program'; + +import {QuadTriangleArray, CollisionCircleLayoutArray} from '../data/array_types'; +import {collisionCircleLayout} from '../data/bucket/symbol_attributes'; +import SegmentVector from '../data/segment'; +import {mat4} from 'gl-matrix'; +import VertexBuffer from '../gl/vertex_buffer'; +import IndexBuffer from '../gl/index_buffer'; export default drawCollisionDebug; -function drawCollisionDebugGeometry(painter: Painter, sourceCache: SourceCache, layer: StyleLayer, coords: Array, drawCircles: boolean, - translate: [number, number], translateAnchor: 'map' | 'viewport', isText: boolean) { +type TileBatch = { + circleArray: Array, + circleOffset: number, + transform: mat4, + invTransform: mat4 +}; + +let quadTriangles: ?QuadTriangleArray; + +function drawCollisionDebug(painter: Painter, sourceCache: SourceCache, layer: StyleLayer, coords: Array, translate: [number, number], translateAnchor: 'map' | 'viewport', isText: boolean) { const context = painter.context; const gl = context.gl; - const program = drawCircles ? painter.useProgram('collisionCircle') : painter.useProgram('collisionBox'); + const program = painter.useProgram('collisionBox'); + const tileBatches: Array = []; + let circleCount = 0; + let circleOffset = 0; for (let i = 0; i < coords.length; i++) { const coord = coords[i]; const tile = sourceCache.getTile(coord); const bucket: ?SymbolBucket = (tile.getBucket(layer): any); if (!bucket) continue; - const buffers = drawCircles ? (isText ? bucket.textCollisionCircle : bucket.iconCollisionCircle) : (isText ? bucket.textCollisionBox : bucket.iconCollisionBox); - if (!buffers) continue; let posMatrix = coord.posMatrix; if (translate[0] !== 0 || translate[1] !== 0) { posMatrix = painter.translatePosMatrix(coord.posMatrix, tile, translate, translateAnchor); } - program.draw(context, drawCircles ? gl.TRIANGLES : gl.LINES, + const buffers = isText ? bucket.textCollisionBox : bucket.iconCollisionBox; + // Get collision circle data of this bucket + const circleArray: Array = bucket.collisionCircleArray; + if (circleArray.length > 0) { + // We need to know the projection matrix that was used for projecting collision circles to the screen. + // This might vary between buckets as the symbol placement is a continous process. This matrix is + // required for transforming points from previous screen space to the current one + const invTransform = mat4.create(); + const transform = posMatrix; + + mat4.mul(invTransform, bucket.placementInvProjMatrix, painter.transform.glCoordMatrix); + mat4.mul(invTransform, invTransform, bucket.placementViewportMatrix); + + tileBatches.push({ + circleArray, + circleOffset, + transform, + invTransform + }); + + circleCount += circleArray.length / 4; // 4 values per circle + circleOffset = circleCount; + } + if (!buffers) continue; + program.draw(context, gl.LINES, DepthMode.disabled, StencilMode.disabled, painter.colorModeForRenderPass(), CullFaceMode.disabled, @@ -41,9 +81,92 @@ function drawCollisionDebugGeometry(painter: Painter, sourceCache: SourceCache, buffers.segments, null, painter.transform.zoom, null, null, buffers.collisionVertexBuffer); } + + if (!isText || !tileBatches.length) { + return; + } + + // Render collision circles + const circleProgram = painter.useProgram('collisionCircle'); + + // Construct vertex data + const vertexData = new CollisionCircleLayoutArray(); + vertexData.resize(circleCount * 4); + vertexData._trim(); + + let vertexOffset = 0; + + for (const batch of tileBatches) { + for (let i = 0; i < batch.circleArray.length / 4; i++) { + const circleIdx = i * 4; + const x = batch.circleArray[circleIdx + 0]; + const y = batch.circleArray[circleIdx + 1]; + const radius = batch.circleArray[circleIdx + 2]; + const collision = batch.circleArray[circleIdx + 3]; + + // 4 floats per vertex, 4 vertices per quad + vertexData.emplace(vertexOffset++, x, y, radius, collision, 0); + vertexData.emplace(vertexOffset++, x, y, radius, collision, 1); + vertexData.emplace(vertexOffset++, x, y, radius, collision, 2); + vertexData.emplace(vertexOffset++, x, y, radius, collision, 3); + } + } + if (!quadTriangles || quadTriangles.length < circleCount * 2) { + quadTriangles = createQuadTriangles(circleCount); + } + + const indexBuffer: IndexBuffer = context.createIndexBuffer(quadTriangles, true); + const vertexBuffer: VertexBuffer = context.createVertexBuffer(vertexData, collisionCircleLayout.members, true); + + // Render batches + for (const batch of tileBatches) { + const uniforms = collisionCircleUniformValues( + batch.transform, + batch.invTransform, + painter.transform + ); + + circleProgram.draw( + context, + gl.TRIANGLES, + DepthMode.disabled, + StencilMode.disabled, + painter.colorModeForRenderPass(), + CullFaceMode.disabled, + uniforms, + layer.id, + vertexBuffer, + indexBuffer, + SegmentVector.simpleSegment(0, batch.circleOffset * 2, batch.circleArray.length, batch.circleArray.length / 2), + null, + painter.transform.zoom, + null, + null, + null); + } + + vertexBuffer.destroy(); + indexBuffer.destroy(); } -function drawCollisionDebug(painter: Painter, sourceCache: SourceCache, layer: StyleLayer, coords: Array, translate: [number, number], translateAnchor: 'map' | 'viewport', isText: boolean) { - drawCollisionDebugGeometry(painter, sourceCache, layer, coords, false, translate, translateAnchor, isText); - drawCollisionDebugGeometry(painter, sourceCache, layer, coords, true, translate, translateAnchor, isText); +function createQuadTriangles(quadCount: number): QuadTriangleArray { + const triCount = quadCount * 2; + const array = new QuadTriangleArray(); + + array.resize(triCount); + array._trim(); + + // Two triangles and 4 vertices per quad. + for (let i = 0; i < triCount; i++) { + const idx = i * 6; + + array.uint16[idx + 0] = i * 4 + 0; + array.uint16[idx + 1] = i * 4 + 1; + array.uint16[idx + 2] = i * 4 + 2; + array.uint16[idx + 3] = i * 4 + 2; + array.uint16[idx + 4] = i * 4 + 3; + array.uint16[idx + 5] = i * 4 + 0; + } + + return array; } diff --git a/src/render/draw_symbol.js b/src/render/draw_symbol.js index f20d48ffc75..12970150534 100644 --- a/src/render/draw_symbol.js +++ b/src/render/draw_symbol.js @@ -156,7 +156,7 @@ function updateVariableAnchorsForBucket(bucket, rotateWithMap, pitchWithMap, var } else { const tileAnchor = new Point(symbol.anchorX, symbol.anchorY); const projectedAnchor = symbolProjection.project(tileAnchor, pitchWithMap ? posMatrix : labelPlaneMatrix); - const perspectiveRatio = 0.5 + 0.5 * (transform.cameraToCenterDistance / projectedAnchor.signedDistanceFromCamera); + const perspectiveRatio = symbolProjection.getPerspectiveRatio(transform.cameraToCenterDistance, projectedAnchor.signedDistanceFromCamera); let renderTextSize = symbolSize.evaluateSizeForFeature(bucket.textSizeData, size, symbol) * perspectiveRatio / ONE_EM; if (pitchWithMap) { // Go from size in pixels to equivalent size in tile units diff --git a/src/render/program/collision_program.js b/src/render/program/collision_program.js index 542b6dc2cbd..82a5372b84d 100644 --- a/src/render/program/collision_program.js +++ b/src/render/program/collision_program.js @@ -20,6 +20,13 @@ export type CollisionUniformsType = {| 'u_overscale_factor': Uniform1f |}; +export type CollisionCircleUniformsType = {| + 'u_matrix': UniformMatrix4f, + 'u_inv_matrix': UniformMatrix4f, + 'u_camera_to_center_distance': Uniform1f, + 'u_viewport_size': Uniform2f +|}; + const collisionUniforms = (context: Context, locations: UniformLocations): CollisionUniformsType => ({ 'u_matrix': new UniformMatrix4f(context, locations.u_matrix), 'u_camera_to_center_distance': new Uniform1f(context, locations.u_camera_to_center_distance), @@ -28,6 +35,13 @@ const collisionUniforms = (context: Context, locations: UniformLocations): Colli 'u_overscale_factor': new Uniform1f(context, locations.u_overscale_factor) }); +const collisionCircleUniforms = (context: Context, locations: UniformLocations): CollisionCircleUniformsType => ({ + 'u_matrix': new UniformMatrix4f(context, locations.u_matrix), + 'u_inv_matrix': new UniformMatrix4f(context, locations.u_inv_matrix), + 'u_camera_to_center_distance': new Uniform1f(context, locations.u_camera_to_center_distance), + 'u_viewport_size': new Uniform2f(context, locations.u_viewport_size) +}); + const collisionUniformValues = ( matrix: Float32Array, transform: Transform, @@ -46,4 +60,17 @@ const collisionUniformValues = ( }; }; -export {collisionUniforms, collisionUniformValues}; +const collisionCircleUniformValues = ( + matrix: Float32Array, + invMatrix: Float32Array, + transform: Transform +): UniformValues => { + return { + 'u_matrix': matrix, + 'u_inv_matrix': invMatrix, + 'u_camera_to_center_distance': transform.cameraToCenterDistance, + 'u_viewport_size': [transform.width, transform.height] + }; +}; + +export {collisionUniforms, collisionUniformValues, collisionCircleUniforms, collisionCircleUniformValues}; diff --git a/src/render/program/program_uniforms.js b/src/render/program/program_uniforms.js index 9ccc73bfd64..e36ec6f6e83 100644 --- a/src/render/program/program_uniforms.js +++ b/src/render/program/program_uniforms.js @@ -3,7 +3,7 @@ import {fillExtrusionUniforms, fillExtrusionPatternUniforms} from './fill_extrusion_program'; import {fillUniforms, fillPatternUniforms, fillOutlineUniforms, fillOutlinePatternUniforms} from './fill_program'; import {circleUniforms} from './circle_program'; -import {collisionUniforms} from './collision_program'; +import {collisionUniforms, collisionCircleUniforms} from './collision_program'; import {debugUniforms} from './debug_program'; import {clippingMaskUniforms} from './clipping_mask_program'; import {heatmapUniforms, heatmapTextureUniforms} from './heatmap_program'; @@ -22,7 +22,7 @@ export const programUniforms = { fillOutlinePattern: fillOutlinePatternUniforms, circle: circleUniforms, collisionBox: collisionUniforms, - collisionCircle: collisionUniforms, + collisionCircle: collisionCircleUniforms, debug: debugUniforms, clippingMask: clippingMaskUniforms, heatmap: heatmapUniforms, diff --git a/src/shaders/collision_circle.fragment.glsl b/src/shaders/collision_circle.fragment.glsl index e7eedb19e34..3cd66b1155e 100644 --- a/src/shaders/collision_circle.fragment.glsl +++ b/src/shaders/collision_circle.fragment.glsl @@ -1,34 +1,17 @@ -uniform float u_overscale_factor; - -varying float v_placed; -varying float v_notUsed; varying float v_radius; varying vec2 v_extrude; -varying vec2 v_extrude_scale; +varying float v_perspective_ratio; +varying float v_collision; void main() { - float alpha = 0.5; - - // Red = collision, hide label - vec4 color = vec4(1.0, 0.0, 0.0, 1.0) * alpha; - - // Blue = no collision, label is showing - if (v_placed > 0.5) { - color = vec4(0.0, 0.0, 1.0, 0.5) * alpha; - } - - if (v_notUsed > 0.5) { - // This box not used, fade it out - color *= .2; - } + float alpha = 0.5 * min(v_perspective_ratio, 1.0); + float stroke_radius = 0.9 * max(v_perspective_ratio, 1.0); - float extrude_scale_length = length(v_extrude_scale); - float extrude_length = length(v_extrude) * extrude_scale_length; - float stroke_width = 15.0 * extrude_scale_length / u_overscale_factor; - float radius = v_radius * extrude_scale_length; + float distance_to_center = length(v_extrude); + float distance_to_edge = abs(distance_to_center - v_radius); + float opacity_t = smoothstep(-stroke_radius, 0.0, -distance_to_edge); - float distance_to_edge = abs(extrude_length - radius); - float opacity_t = smoothstep(-stroke_width, 0.0, -distance_to_edge); + vec4 color = mix(vec4(0.0, 0.0, 1.0, 0.5), vec4(1.0, 0.0, 0.0, 1.0), v_collision); - gl_FragColor = opacity_t * color; + gl_FragColor = color * alpha * opacity_t; } diff --git a/src/shaders/collision_circle.vertex.glsl b/src/shaders/collision_circle.vertex.glsl index 24e06ed1c17..e64f1728297 100644 --- a/src/shaders/collision_circle.vertex.glsl +++ b/src/shaders/collision_circle.vertex.glsl @@ -1,36 +1,59 @@ attribute vec2 a_pos; -attribute vec2 a_anchor_pos; -attribute vec2 a_extrude; -attribute vec2 a_placed; +attribute float a_radius; +attribute vec2 a_flags; uniform mat4 u_matrix; -uniform vec2 u_extrude_scale; +uniform mat4 u_inv_matrix; +uniform vec2 u_viewport_size; uniform float u_camera_to_center_distance; -varying float v_placed; -varying float v_notUsed; varying float v_radius; - varying vec2 v_extrude; -varying vec2 v_extrude_scale; +varying float v_perspective_ratio; +varying float v_collision; + +vec3 toTilePosition(vec2 screenPos) { + // Shoot a ray towards the ground to reconstruct the depth-value + vec4 rayStart = u_inv_matrix * vec4(screenPos, -1.0, 1.0); + vec4 rayEnd = u_inv_matrix * vec4(screenPos, 1.0, 1.0); + + rayStart.xyz /= rayStart.w; + rayEnd.xyz /= rayEnd.w; + + highp float t = (0.0 - rayStart.z) / (rayEnd.z - rayStart.z); + return mix(rayStart.xyz, rayEnd.xyz, t); +} void main() { - vec4 projectedPoint = u_matrix * vec4(a_anchor_pos, 0, 1); - highp float camera_to_anchor_distance = projectedPoint.w; + vec2 quadCenterPos = a_pos; + float radius = a_radius; + float collision = a_flags.x; + float vertexIdx = a_flags.y; + + vec2 quadVertexOffset = vec2( + mix(-1.0, 1.0, float(vertexIdx >= 2.0)), + mix(-1.0, 1.0, float(vertexIdx >= 1.0 && vertexIdx <= 2.0))); + + vec2 quadVertexExtent = quadVertexOffset * radius; + + // Screen position of the quad might have been computed with different camera parameters. + // Transform the point to a proper position on the current viewport + vec3 tilePos = toTilePosition(quadCenterPos); + vec4 clipPos = u_matrix * vec4(tilePos, 1.0); + + highp float camera_to_anchor_distance = clipPos.w; highp float collision_perspective_ratio = clamp( 0.5 + 0.5 * (u_camera_to_center_distance / camera_to_anchor_distance), 0.0, // Prevents oversized near-field circles in pitched/overzoomed tiles 4.0); - gl_Position = u_matrix * vec4(a_pos, 0.0, 1.0); - - highp float padding_factor = 1.2; // Pad the vertices slightly to make room for anti-alias blur - gl_Position.xy += a_extrude * u_extrude_scale * padding_factor * gl_Position.w * collision_perspective_ratio; - - v_placed = a_placed.x; - v_notUsed = a_placed.y; - v_radius = abs(a_extrude.y); // We don't pitch the circles, so both units of the extrusion vector are equal in magnitude to the radius + // Apply small padding for the anti-aliasing effect to fit the quad + // Note that v_radius and v_extrude are in screen coordinates already + float padding_factor = 1.2; + v_radius = radius; + v_extrude = quadVertexExtent * padding_factor; + v_perspective_ratio = collision_perspective_ratio; + v_collision = collision; - v_extrude = a_extrude * padding_factor; - v_extrude_scale = u_extrude_scale * u_camera_to_center_distance * collision_perspective_ratio; + gl_Position = vec4(clipPos.xyz / clipPos.w, 1.0) + vec4(quadVertexExtent * padding_factor / u_viewport_size * 2.0, 0.0, 0.0); } diff --git a/src/symbol/collision_feature.js b/src/symbol/collision_feature.js index 178bea72f0b..faf3eb48a59 100644 --- a/src/symbol/collision_feature.js +++ b/src/symbol/collision_feature.js @@ -15,11 +15,12 @@ import type Anchor from './anchor'; class CollisionFeature { boxStartIndex: number; boxEndIndex: number; + circleDiameter: ?number; /** * Create a CollisionFeature, adding its collision box data to the given collisionBoxArray in the process. + * For line aligned labels a collision circle diameter is computed instead. * - * @param line The geometry the label is placed on. * @param anchor The point along the line around which the label is anchored. * @param shaped The text or icon shaping results. * @param boxScale A magic number used to convert from glyph metrics units to geometry units. @@ -28,7 +29,6 @@ class CollisionFeature { * @private */ constructor(collisionBoxArray: CollisionBoxArray, - line: Array, anchor: Anchor, featureIndex: number, sourceLayerIndex: number, @@ -37,36 +37,43 @@ class CollisionFeature { boxScale: number, padding: number, alignLine: boolean, - overscaling: number, rotate: number) { - let y1 = shaped.top * boxScale - padding; - let y2 = shaped.bottom * boxScale + padding; - let x1 = shaped.left * boxScale - padding; - let x2 = shaped.right * boxScale + padding; - - const collisionPadding = shaped.collisionPadding; - if (collisionPadding) { - x1 -= collisionPadding[0] * boxScale; - y1 -= collisionPadding[1] * boxScale; - x2 += collisionPadding[2] * boxScale; - y2 += collisionPadding[3] * boxScale; - } this.boxStartIndex = collisionBoxArray.length; if (alignLine) { + // Compute height of the shape in glyph metrics and apply collision padding. + // Note that the pixel based 'text-padding' is applied at runtime + let top = shaped.top; + let bottom = shaped.bottom; + const collisionPadding = shaped.collisionPadding; + + if (collisionPadding) { + top -= collisionPadding[1]; + bottom += collisionPadding[3]; + } - let height = y2 - y1; - const length = x2 - x1; + let height = bottom - top; if (height > 0) { // set minimum box height to avoid very many small labels - height = Math.max(10 * boxScale, height); - - this._addLineCollisionCircles(collisionBoxArray, line, anchor, (anchor.segment: any), length, height, featureIndex, sourceLayerIndex, bucketIndex, overscaling); + height = Math.max(10, height); + this.circleDiameter = height; } - } else { + let y1 = shaped.top * boxScale - padding; + let y2 = shaped.bottom * boxScale + padding; + let x1 = shaped.left * boxScale - padding; + let x2 = shaped.right * boxScale + padding; + + const collisionPadding = shaped.collisionPadding; + if (collisionPadding) { + x1 -= collisionPadding[0] * boxScale; + y1 -= collisionPadding[1] * boxScale; + x2 += collisionPadding[2] * boxScale; + y2 += collisionPadding[3] * boxScale; + } + if (rotate) { // Account for *-rotate in point collision boxes // See https://github.com/mapbox/mapbox-gl-js/issues/6075 @@ -92,126 +99,11 @@ class CollisionFeature { y1 = Math.min(tl.y, tr.y, bl.y, br.y); y2 = Math.max(tl.y, tr.y, bl.y, br.y); } - collisionBoxArray.emplaceBack(anchor.x, anchor.y, x1, y1, x2, y2, featureIndex, sourceLayerIndex, bucketIndex, - 0, 0); + collisionBoxArray.emplaceBack(anchor.x, anchor.y, x1, y1, x2, y2, featureIndex, sourceLayerIndex, bucketIndex); } this.boxEndIndex = collisionBoxArray.length; } - - /** - * Create a set of CollisionBox objects for a line. - * - * @param labelLength The length of the label in geometry units. - * @param anchor The point along the line around which the label is anchored. - * @param boxSize The size of the collision boxes that will be created. - * @private - */ - _addLineCollisionCircles(collisionBoxArray: CollisionBoxArray, - line: Array, - anchor: Anchor, - segment: number, - labelLength: number, - boxSize: number, - featureIndex: number, - sourceLayerIndex: number, - bucketIndex: number, - overscaling: number) { - const step = boxSize / 2; - const nBoxes = Math.floor(labelLength / step) || 1; - // We calculate line collision circles out to 300% of what would normally be our - // max size, to allow collision detection to work on labels that expand as - // they move into the distance - // Vertically oriented labels in the distant field can extend past this padding - // This is a noticeable problem in overscaled tiles where the pitch 0-based - // symbol spacing will put labels very close together in a pitched map. - // To reduce the cost of adding extra collision circles, we slowly increase - // them for overscaled tiles. - const overscalingPaddingFactor = 1 + .4 * Math.log(overscaling) / Math.LN2; - const nPitchPaddingBoxes = Math.floor(nBoxes * overscalingPaddingFactor / 2); - - // offset the center of the first box by half a box so that the edge of the - // box is at the edge of the label. - const firstBoxOffset = -boxSize / 2; - - let p = anchor; - let index = segment + 1; - let anchorDistance = firstBoxOffset; - const labelStartDistance = -labelLength / 2; - const paddingStartDistance = labelStartDistance - labelLength / 4; - // move backwards along the line to the first segment the label appears on - do { - index--; - - if (index < 0) { - if (anchorDistance > labelStartDistance) { - // there isn't enough room for the label after the beginning of the line - // checkMaxAngle should have already caught this - return; - } else { - // The line doesn't extend far enough back for all of our padding, - // but we got far enough to show the label under most conditions. - index = 0; - break; - } - } else { - anchorDistance -= line[index].dist(p); - p = line[index]; - } - } while (anchorDistance > paddingStartDistance); - - let segmentLength = line[index].dist(line[index + 1]); - - for (let i = -nPitchPaddingBoxes; i < nBoxes + nPitchPaddingBoxes; i++) { - - // the distance the box will be from the anchor - const boxOffset = i * step; - let boxDistanceToAnchor = labelStartDistance + boxOffset; - - // make the distance between pitch padding boxes bigger - if (boxOffset < 0) boxDistanceToAnchor += boxOffset; - if (boxOffset > labelLength) boxDistanceToAnchor += boxOffset - labelLength; - - if (boxDistanceToAnchor < anchorDistance) { - // The line doesn't extend far enough back for this box, skip it - // (This could allow for line collisions on distant tiles) - continue; - } - - // the box is not on the current segment. Move to the next segment. - while (anchorDistance + segmentLength < boxDistanceToAnchor) { - anchorDistance += segmentLength; - index++; - - // There isn't enough room before the end of the line. - if (index + 1 >= line.length) { - return; - } - - segmentLength = line[index].dist(line[index + 1]); - } - - // the distance the box will be from the beginning of the segment - const segmentBoxDistance = boxDistanceToAnchor - anchorDistance; - - const p0 = line[index]; - const p1 = line[index + 1]; - const boxAnchorPoint = p1.sub(p0)._unit()._mult(segmentBoxDistance)._add(p0)._round(); - - // If the box is within boxSize of the anchor, force the box to be used - // (so even 0-width labels use at least one box) - // Otherwise, the .8 multiplication gives us a little bit of conservative - // padding in choosing which boxes to use (see CollisionIndex#placedCollisionCircles) - const paddedAnchorDistance = Math.abs(boxDistanceToAnchor - firstBoxOffset) < step ? - 0 : - (boxDistanceToAnchor - firstBoxOffset) * 0.8; - - collisionBoxArray.emplaceBack(boxAnchorPoint.x, boxAnchorPoint.y, - -boxSize / 2, -boxSize / 2, boxSize / 2, boxSize / 2, - featureIndex, sourceLayerIndex, bucketIndex, - boxSize / 2, paddedAnchorDistance); - } - } } export default CollisionFeature; diff --git a/src/symbol/collision_index.js b/src/symbol/collision_index.js index 66cc64e6fb9..f553e610761 100644 --- a/src/symbol/collision_index.js +++ b/src/symbol/collision_index.js @@ -1,10 +1,14 @@ // @flow import Point from '@mapbox/point-geometry'; +import clipLine from './clip_line'; +import PathInterpolator from './path_interpolator'; import * as intersectionTests from '../util/intersection_tests'; import Grid from './grid_index'; import {mat4} from 'gl-matrix'; +import ONE_EM from '../symbol/one_em'; +import assert from 'assert'; import * as projection from '../symbol/projection'; @@ -84,56 +88,35 @@ class CollisionIndex { }; } - approximateTileDistance(tileDistance: any, lastSegmentAngle: number, pixelsToTileUnits: number, cameraToAnchorDistance: number, pitchWithMap: boolean): number { - // This is a quick and dirty solution for chosing which collision circles to use (since collision circles are - // laid out in tile units). Ideally, I think we should generate collision circles on the fly in viewport coordinates - // at the time we do collision detection. - // See https://github.com/mapbox/mapbox-gl-js/issues/5474 - - // incidenceStretch is the ratio of how much y space a label takes up on a tile while drawn perpendicular to the viewport vs - // how much space it would take up if it were drawn flat on the tile - // Using law of sines, camera_to_anchor/sin(ground_angle) = camera_to_center/sin(incidence_angle) - // Incidence angle 90 -> head on, sin(incidence_angle) = 1, no stretch - // Incidence angle 1 -> very oblique, sin(incidence_angle) =~ 0, lots of stretch - // ground_angle = u_pitch + PI/2 -> sin(ground_angle) = cos(u_pitch) - // incidenceStretch = 1 / sin(incidenceAngle) - - const incidenceStretch = pitchWithMap ? 1 : cameraToAnchorDistance / this.pitchfactor; - const lastSegmentTile = tileDistance.lastSegmentViewportDistance * pixelsToTileUnits; - return tileDistance.prevTileDistance + - lastSegmentTile + - (incidenceStretch - 1) * lastSegmentTile * Math.abs(Math.sin(lastSegmentAngle)); - } - - placeCollisionCircles(collisionCircles: Array, - allowOverlap: boolean, - scale: number, - textPixelRatio: number, + placeCollisionCircles(allowOverlap: boolean, symbol: any, lineVertexArray: SymbolLineVertexArray, glyphOffsetArray: GlyphOffsetArray, fontSize: number, posMatrix: mat4, labelPlaneMatrix: mat4, + labelToScreenMatrix?: mat4, showCollisionCircles: boolean, pitchWithMap: boolean, - collisionGroupPredicate?: any): { circles: Array, offscreen: boolean } { + collisionGroupPredicate?: any, + circlePixelDiameter: number, + textPixelPadding: number): { circles: Array, offscreen: boolean, collisionDetected: boolean } { const placedCollisionCircles = []; - const projectedAnchor = this.projectAnchor(posMatrix, symbol.anchorX, symbol.anchorY); + const tileUnitAnchorPoint = new Point(symbol.anchorX, symbol.anchorY); + const screenAnchorPoint = projection.project(tileUnitAnchorPoint, posMatrix); + const perspectiveRatio = projection.getPerspectiveRatio(this.transform.cameraToCenterDistance, screenAnchorPoint.signedDistanceFromCamera); + const labelPlaneFontSize = pitchWithMap ? fontSize / perspectiveRatio : fontSize * perspectiveRatio; + const labelPlaneFontScale = labelPlaneFontSize / ONE_EM; + + const labelPlaneAnchorPoint = projection.project(tileUnitAnchorPoint, labelPlaneMatrix).point; const projectionCache = {}; - const fontScale = fontSize / 24; - const lineOffsetX = symbol.lineOffsetX * fontSize; - const lineOffsetY = symbol.lineOffsetY * fontSize; + const lineOffsetX = symbol.lineOffsetX * labelPlaneFontScale; + const lineOffsetY = symbol.lineOffsetY * labelPlaneFontScale; - const tileUnitAnchorPoint = new Point(symbol.anchorX, symbol.anchorY); - // projection.project generates NDC coordinates, as opposed to the - // pixel-based grid coordinates generated by this.projectPoint - const labelPlaneAnchorPoint = - projection.project(tileUnitAnchorPoint, labelPlaneMatrix).point; const firstAndLastGlyph = projection.placeFirstAndLastGlyph( - fontScale, + labelPlaneFontScale, glyphOffsetArray, lineOffsetX, lineOffsetY, @@ -143,99 +126,128 @@ class CollisionIndex { symbol, lineVertexArray, labelPlaneMatrix, - projectionCache, - /*return tile distance*/ true); + projectionCache); let collisionDetected = false; let inGrid = false; let entirelyOffscreen = true; - const tileToViewport = projectedAnchor.perspectiveRatio * textPixelRatio; - // pixelsToTileUnits is used for translating line geometry to tile units - // ... so we care about 'scale' but not 'perspectiveRatio' - // equivalent to pixel_to_tile_units - const pixelsToTileUnits = 1 / (textPixelRatio * scale); - - let firstTileDistance = 0, lastTileDistance = 0; if (firstAndLastGlyph) { - firstTileDistance = this.approximateTileDistance(firstAndLastGlyph.first.tileDistance, firstAndLastGlyph.first.angle, pixelsToTileUnits, projectedAnchor.cameraDistance, pitchWithMap); - lastTileDistance = this.approximateTileDistance(firstAndLastGlyph.last.tileDistance, firstAndLastGlyph.last.angle, pixelsToTileUnits, projectedAnchor.cameraDistance, pitchWithMap); - } + const radius = circlePixelDiameter * 0.5 * perspectiveRatio + textPixelPadding; + const screenPlaneMin = new Point(-viewportPadding, -viewportPadding); + const screenPlaneMax = new Point(this.screenRightBoundary, this.screenBottomBoundary); + const interpolator = new PathInterpolator(); + + // Construct a projected path from projected line vertices. Anchor points are ignored and removed + const first = firstAndLastGlyph.first; + const last = firstAndLastGlyph.last; + + let projectedPath = []; + for (let i = first.path.length - 1; i >= 1; i--) { + projectedPath.push(first.path[i]); + } + for (let i = 1; i < last.path.length; i++) { + projectedPath.push(last.path[i]); + } + assert(projectedPath.length >= 2); - for (let k = 0; k < collisionCircles.length; k += 5) { - const anchorPointX = collisionCircles[k]; - const anchorPointY = collisionCircles[k + 1]; - const tileUnitRadius = collisionCircles[k + 2]; - const boxSignedDistanceFromAnchor = collisionCircles[k + 3]; - if (!firstAndLastGlyph || - (boxSignedDistanceFromAnchor < -firstTileDistance) || - (boxSignedDistanceFromAnchor > lastTileDistance)) { - // The label either doesn't fit on its line or we - // don't need to use this circle because the label - // doesn't extend this far. Either way, mark the circle unused. - markCollisionCircleUsed(collisionCircles, k, false); - continue; + // Tolerate a slightly longer distance than one diameter between two adjacent circles + const circleDist = radius * 2.5; + + // The path might need to be converted into screen space if a pitched map is used as the label space + if (labelToScreenMatrix) { + const screenSpacePath = projectedPath.map(p => projection.project(p, labelToScreenMatrix)); + + // Do not try to place collision circles if even of the points is behind the camera. + // This is a plausible scenario with big camera pitch angles + if (screenSpacePath.some(point => point.signedDistanceFromCamera <= 0)) { + projectedPath = []; + } else { + projectedPath = screenSpacePath.map(p => p.point); + } } - const projectedPoint = this.projectPoint(posMatrix, anchorPointX, anchorPointY); - const radius = tileUnitRadius * tileToViewport; - - const atLeastOneCirclePlaced = placedCollisionCircles.length > 0; - if (atLeastOneCirclePlaced) { - const dx = projectedPoint.x - placedCollisionCircles[placedCollisionCircles.length - 4]; - const dy = projectedPoint.y - placedCollisionCircles[placedCollisionCircles.length - 3]; - // The circle edges touch when the distance between their centers is 2x the radius - // When the distance is 1x the radius, they're doubled up, and we could remove - // every other circle while keeping them all in touch. - // We actually start removing circles when the distance is √2x the radius: - // thinning the number of circles as much as possible is a major performance win, - // and the small gaps introduced don't make a very noticeable difference. - const placedTooDensely = radius * radius * 2 > dx * dx + dy * dy; - if (placedTooDensely) { - const atLeastOneMoreCircle = (k + 8) < collisionCircles.length; - if (atLeastOneMoreCircle) { - const nextBoxDistanceToAnchor = collisionCircles[k + 8]; - if ((nextBoxDistanceToAnchor > -firstTileDistance) && - (nextBoxDistanceToAnchor < lastTileDistance)) { - // Hide significantly overlapping circles, unless this is the last one we can - // use, in which case we want to keep it in place even if it's tightly packed - // with the one before it. - markCollisionCircleUsed(collisionCircles, k, false); - continue; - } - } + let segments = []; + + if (projectedPath.length > 0) { + // Quickly check if the path is fully inside or outside of the padded collision region. + // For overlapping paths we'll only create collision circles for the visible segments + const minPoint = projectedPath[0].clone(); + const maxPoint = projectedPath[0].clone(); + + for (let i = 1; i < projectedPath.length; i++) { + minPoint.x = Math.min(minPoint.x, projectedPath[i].x); + minPoint.y = Math.min(minPoint.y, projectedPath[i].y); + maxPoint.x = Math.max(maxPoint.x, projectedPath[i].x); + maxPoint.y = Math.max(maxPoint.y, projectedPath[i].y); + } + + if (minPoint.x >= screenPlaneMin.x && maxPoint.x <= screenPlaneMax.x && + minPoint.y >= screenPlaneMin.y && maxPoint.y <= screenPlaneMax.y) { + // Quad fully visible + segments = [projectedPath]; + } else if (maxPoint.x < screenPlaneMin.x || minPoint.x > screenPlaneMax.x || + maxPoint.y < screenPlaneMin.y || minPoint.y > screenPlaneMax.y) { + // Not visible + segments = []; + } else { + segments = clipLine([projectedPath], screenPlaneMin.x, screenPlaneMin.y, screenPlaneMax.x, screenPlaneMax.y); } } - const collisionBoxArrayIndex = k / 5; - placedCollisionCircles.push(projectedPoint.x, projectedPoint.y, radius, collisionBoxArrayIndex); - markCollisionCircleUsed(collisionCircles, k, true); - - const x1 = projectedPoint.x - radius; - const y1 = projectedPoint.y - radius; - const x2 = projectedPoint.x + radius; - const y2 = projectedPoint.y + radius; - entirelyOffscreen = entirelyOffscreen && this.isOffscreen(x1, y1, x2, y2); - inGrid = inGrid || this.isInsideGrid(x1, y1, x2, y2); - - if (!allowOverlap) { - if (this.grid.hitTestCircle(projectedPoint.x, projectedPoint.y, radius, collisionGroupPredicate)) { - if (!showCollisionCircles) { - return { - circles: [], - offscreen: false - }; - } else { - // Don't early exit if we're showing the debug circles because we still want to calculate - // which circles are in use - collisionDetected = true; + + for (const seg of segments) { + // interpolate positions for collision circles. Add a small padding to both ends of the segment + assert(seg.length > 0); + interpolator.reset(seg, radius * 0.25); + + let numCircles = 0; + + if (interpolator.length <= 0.5 * radius) { + numCircles = 1; + } else { + numCircles = Math.ceil(interpolator.paddedLength / circleDist) + 1; + } + + for (let i = 0; i < numCircles; i++) { + const t = i / Math.max(numCircles - 1, 1); + const circlePosition = interpolator.lerp(t); + + // add viewport padding to the position and perform initial collision check + const centerX = circlePosition.x + viewportPadding; + const centerY = circlePosition.y + viewportPadding; + + placedCollisionCircles.push(centerX, centerY, radius, 0); + + const x1 = centerX - radius; + const y1 = centerY - radius; + const x2 = centerX + radius; + const y2 = centerY + radius; + + entirelyOffscreen = entirelyOffscreen && this.isOffscreen(x1, y1, x2, y2); + inGrid = inGrid || this.isInsideGrid(x1, y1, x2, y2); + + if (!allowOverlap) { + if (this.grid.hitTestCircle(centerX, centerY, radius, collisionGroupPredicate)) { + // Don't early exit if we're showing the debug circles because we still want to calculate + // which circles are in use + collisionDetected = true; + if (!showCollisionCircles) { + return { + circles: [], + offscreen: false, + collisionDetected + }; + } + } } } } } return { - circles: (collisionDetected || !inGrid) ? [] : placedCollisionCircles, - offscreen: entirelyOffscreen + circles: ((!showCollisionCircles && collisionDetected) || !inGrid) ? [] : placedCollisionCircles, + offscreen: entirelyOffscreen, + collisionDetected }; } @@ -322,24 +334,6 @@ class CollisionIndex { } } - projectAnchor(posMatrix: mat4, x: number, y: number) { - const p = [x, y, 0, 1]; - projection.xyTransformMat4(p, p, posMatrix); - return { - perspectiveRatio: 0.5 + 0.5 * (this.transform.cameraToCenterDistance / p[3]), - cameraDistance: p[3] - }; - } - - projectPoint(posMatrix: mat4, x: number, y: number) { - const p = [x, y, 0, 1]; - projection.xyTransformMat4(p, p, posMatrix); - return new Point( - (((p[0] / p[3] + 1) / 2) * this.transform.width) + viewportPadding, - (((-p[1] / p[3] + 1) / 2) * this.transform.height) + viewportPadding - ); - } - projectAndGetPerspectiveRatio(posMatrix: mat4, x: number, y: number) { const p = [x, y, 0, 1]; projection.xyTransformMat4(p, p, posMatrix); @@ -363,10 +357,17 @@ class CollisionIndex { isInsideGrid(x1: number, y1: number, x2: number, y2: number) { return x2 >= 0 && x1 < this.gridRightBoundary && y2 >= 0 && y1 < this.gridBottomBoundary; } -} -function markCollisionCircleUsed(collisionCircles: Array, index: number, used: boolean) { - collisionCircles[index + 4] = used ? 1 : 0; + /* + * Returns a matrix for transforming collision shapes to viewport coordinate space. + * Use this function to render e.g. collision circles on the screen. + * example transformation: clipPos = glCoordMatrix * viewportMatrix * circle_pos + */ + getViewportMatrix(): mat4 { + const m = mat4.identity([]); + mat4.translate(m, m, [-viewportPadding, -viewportPadding, 0.0]); + return m; + } } export default CollisionIndex; diff --git a/src/symbol/path_interpolator.js b/src/symbol/path_interpolator.js new file mode 100644 index 00000000000..f64467a5a99 --- /dev/null +++ b/src/symbol/path_interpolator.js @@ -0,0 +1,61 @@ +// @flow + +import {clamp} from '../util/util'; +import Point from '@mapbox/point-geometry'; +import assert from 'assert'; + +class PathInterpolator { + points: Array; + length: number; + paddedLength: number; + padding: number; + _distances: Array; + + constructor(points_: ?Array, padding_: ?number) { + this.reset(points_, padding_); + } + + reset(points_: ?Array, padding_: ?number) { + this.points = points_ || []; + + // Compute cumulative distance from first point to every other point in the segment. + // Last entry in the array is total length of the path + this._distances = [0.0]; + + for (let i = 1; i < this.points.length; i++) { + this._distances[i] = this._distances[i - 1] + this.points[i].dist(this.points[i - 1]); + } + + this.length = this._distances[this._distances.length - 1]; + this.padding = Math.min(padding_ || 0, this.length * 0.5); + this.paddedLength = this.length - this.padding * 2.0; + } + + lerp(t: number): Point { + assert(this.points.length > 0); + if (this.points.length === 1) { + return this.points[0]; + } + + t = clamp(t, 0, 1); + + // Find the correct segment [p0, p1] where p0 <= x < p1 + let currentIndex = 1; + let distOfCurrentIdx = this._distances[currentIndex]; + const distToTarget = t * this.paddedLength + this.padding; + + while (distOfCurrentIdx < distToTarget && currentIndex < this._distances.length) { + distOfCurrentIdx = this._distances[++currentIndex]; + } + + // Interpolate between the two points of the segment + const idxOfPrevPoint = currentIndex - 1; + const distOfPrevIdx = this._distances[idxOfPrevPoint]; + const segmentLength = distOfCurrentIdx - distOfPrevIdx; + const segmentT = segmentLength > 0 ? (distToTarget - distOfPrevIdx) / segmentLength : 0; + + return this.points[idxOfPrevPoint].mult(1.0 - segmentT).add(this.points[currentIndex].mult(segmentT)); + } +} + +export default PathInterpolator; diff --git a/src/symbol/placement.js b/src/symbol/placement.js index cb3eda53511..b7e06fb865a 100644 --- a/src/symbol/placement.js +++ b/src/symbol/placement.js @@ -6,6 +6,7 @@ import * as symbolSize from './symbol_size'; import * as projection from './projection'; import {getAnchorJustification, evaluateVariableOffset} from './symbol_layout'; import {getAnchorAlignment, WritingMode} from './shaping'; +import {mat4} from 'gl-matrix'; import assert from 'assert'; import pixelsToTileUnits from '../source/pixels_to_tile_units'; import Point from '@mapbox/point-geometry'; @@ -14,7 +15,6 @@ import type StyleLayer from '../style/style_layer'; import type Tile from '../source/tile'; import type SymbolBucket, {CollisionArrays, SingleCollisionBox} from '../data/bucket/symbol_bucket'; -import type {mat4} from 'gl-matrix'; import type {CollisionBoxArray, CollisionVertexArray, SymbolInstance} from '../data/array_types'; import type FeatureIndex from '../data/feature_index'; import type {OverscaledTileID} from '../source/tile_id'; @@ -63,6 +63,19 @@ class JointPlacement { } } +class CollisionCircleArray { + // Stores collision circles and placement matrices of a bucket for debug rendering. + invProjMatrix: mat4; + viewportMatrix: mat4; + circles: Array; + + constructor() { + this.invProjMatrix = mat4.create(); + this.viewportMatrix = mat4.create(); + this.circles = []; + } +} + export class RetainedQueryData { bucketInstanceId: number; featureIndex: FeatureIndex; @@ -162,6 +175,7 @@ type TileLayerParameters = { layout: any, posMatrix: mat4, textLabelPlaneMatrix: mat4, + labelToScreenMatrix: mat4, scale: number, textPixelRatio: number, holdingForFade: boolean, @@ -195,6 +209,7 @@ export class Placement { collisionGroups: CollisionGroups; prevPlacement: ?Placement; zoomAtLastRecencyCheck: number; + collisionCircleArrays: {[any]: CollisionCircleArray}; constructor(transform: Transform, fadeDuration: number, crossSourceCollisions: boolean, prevPlacement?: Placement) { this.transform = transform.clone(); @@ -207,6 +222,7 @@ export class Placement { this.fadeDuration = fadeDuration; this.retainedQueryData = {}; this.collisionGroups = new CollisionGroups(crossSourceCollisions); + this.collisionCircleArrays = {}; this.prevPlacement = prevPlacement; if (prevPlacement) { @@ -231,11 +247,28 @@ export class Placement { const posMatrix = this.transform.calculatePosMatrix(tile.tileID.toUnwrapped()); + const pitchWithMap = layout.get('text-pitch-alignment') === 'map'; + const rotateWithMap = layout.get('text-rotation-alignment') === 'map'; + const pixelsToTiles = pixelsToTileUnits(tile, 1, this.transform.zoom); + const textLabelPlaneMatrix = projection.getLabelPlaneMatrix(posMatrix, - layout.get('text-pitch-alignment') === 'map', - layout.get('text-rotation-alignment') === 'map', + pitchWithMap, + rotateWithMap, + this.transform, + pixelsToTiles); + + let labelToScreenMatrix = null; + + if (pitchWithMap) { + const glMatrix = projection.getGlCoordMatrix( + posMatrix, + pitchWithMap, + rotateWithMap, this.transform, - pixelsToTileUnits(tile, 1, this.transform.zoom)); + pixelsToTiles); + + labelToScreenMatrix = mat4.multiply([], this.transform.labelPlaneMatrix, glMatrix); + } // As long as this placement lives, we have to hold onto this bucket's // matching FeatureIndex/data for querying purposes @@ -252,6 +285,7 @@ export class Placement { layout, posMatrix, textLabelPlaneMatrix, + labelToScreenMatrix, scale, textPixelRatio, holdingForFade: tile.holdingForFade(), @@ -334,7 +368,7 @@ export class Placement { layout, posMatrix, textLabelPlaneMatrix, - scale, + labelToScreenMatrix, textPixelRatio, holdingForFade, collisionBoxArray, @@ -398,6 +432,8 @@ export class Placement { if (collisionArrays.textFeatureIndex) { textFeatureIndex = collisionArrays.textFeatureIndex; + } else if (symbolInstance.useRuntimeCollisionCircles) { + textFeatureIndex = symbolInstance.featureIndex; } if (collisionArrays.verticalTextFeatureIndex) { verticalTextFeatureIndex = collisionArrays.verticalTextFeatureIndex; @@ -544,28 +580,34 @@ export class Placement { placeText = placedGlyphBoxes && placedGlyphBoxes.box && placedGlyphBoxes.box.length > 0; offscreen = placedGlyphBoxes && placedGlyphBoxes.offscreen; - const textCircles = collisionArrays.textCircles; - if (textCircles) { + + if (symbolInstance.useRuntimeCollisionCircles) { const placedSymbol = bucket.text.placedSymbolArray.get(symbolInstance.centerJustifiedTextSymbolIndex); const fontSize = symbolSize.evaluateSizeForFeature(bucket.textSizeData, partiallyEvaluatedTextSize, placedSymbol); - placedGlyphCircles = this.collisionIndex.placeCollisionCircles(textCircles, - textAllowOverlap, - scale, - textPixelRatio, + + const textPixelPadding = layout.get('text-padding'); + const circlePixelDiameter = symbolInstance.collisionCircleDiameter; + + placedGlyphCircles = this.collisionIndex.placeCollisionCircles(textAllowOverlap, placedSymbol, bucket.lineVertexArray, bucket.glyphOffsetArray, fontSize, posMatrix, textLabelPlaneMatrix, + labelToScreenMatrix, showCollisionBoxes, pitchWithMap, - collisionGroup.predicate); + collisionGroup.predicate, + circlePixelDiameter, + textPixelPadding); + + assert(!placedGlyphCircles.circles.length || (!placedGlyphCircles.collisionDetected || showCollisionBoxes)); // If text-allow-overlap is set, force "placedCircles" to true // In theory there should always be at least one circle placed // in this case, but for now quirks in text-anchor // and text-offset may prevent that from being true. - placeText = textAllowOverlap || placedGlyphCircles.circles.length > 0; + placeText = textAllowOverlap || (placedGlyphCircles.circles.length > 0 && !placedGlyphCircles.collisionDetected); offscreen = offscreen && placedGlyphCircles.offscreen; } @@ -622,9 +664,28 @@ export class Placement { this.collisionIndex.insertCollisionBox(placedIconBoxes.box, layout.get('icon-ignore-placement'), bucket.bucketInstanceId, iconFeatureIndex, collisionGroup.ID); } - if (placeText && placedGlyphCircles) { - this.collisionIndex.insertCollisionCircles(placedGlyphCircles.circles, layout.get('text-ignore-placement'), + if (placedGlyphCircles) { + if (placeText) { + this.collisionIndex.insertCollisionCircles(placedGlyphCircles.circles, layout.get('text-ignore-placement'), bucket.bucketInstanceId, textFeatureIndex, collisionGroup.ID); + } + + if (showCollisionBoxes) { + const id = bucket.bucketInstanceId; + let circleArray = this.collisionCircleArrays[id]; + + // Group collision circles together by bucket. Circles can't be pushed forward for rendering yet as the symbol placement + // for a bucket is not guaranteed to be complete before the commit-function has been called + if (circleArray === undefined) + circleArray = this.collisionCircleArrays[id] = new CollisionCircleArray(); + + for (let i = 0; i < placedGlyphCircles.circles.length; i += 4) { + circleArray.circles.push(placedGlyphCircles.circles[i + 0]); // x + circleArray.circles.push(placedGlyphCircles.circles[i + 1]); // y + circleArray.circles.push(placedGlyphCircles.circles[i + 2]); // radius + circleArray.circles.push(placedGlyphCircles.collisionDetected ? 1 : 0); // collisionDetected-flag + } + } } assert(symbolInstance.crossTileID !== 0); @@ -647,6 +708,14 @@ export class Placement { } } + if (showCollisionBoxes && bucket.bucketInstanceId in this.collisionCircleArrays) { + const circleArray = this.collisionCircleArrays[bucket.bucketInstanceId]; + + // Store viewport and inverse projection matrices per bucket + mat4.invert(circleArray.invProjMatrix, posMatrix); + circleArray.viewportMatrix = this.collisionIndex.getViewportMatrix(); + } + bucket.justReloaded = false; } @@ -781,8 +850,6 @@ export class Placement { if (bucket.hasIconData()) bucket.icon.opacityVertexArray.clear(); if (bucket.hasIconCollisionBoxData()) bucket.iconCollisionBox.collisionVertexArray.clear(); if (bucket.hasTextCollisionBoxData()) bucket.textCollisionBox.collisionVertexArray.clear(); - if (bucket.hasIconCollisionCircleData()) bucket.iconCollisionCircle.collisionVertexArray.clear(); - if (bucket.hasTextCollisionCircleData()) bucket.textCollisionCircle.collisionVertexArray.clear(); const layout = bucket.layers[0].layout; const duplicateOpacityState = new JointOpacityState(null, 0, false, false, true); @@ -801,8 +868,7 @@ export class Placement { iconAllowOverlap && (textAllowOverlap || !bucket.hasTextData() || layout.get('text-optional')), true); - if (!bucket.collisionArrays && collisionBoxArray && ((bucket.hasIconCollisionBoxData() || bucket.hasIconCollisionCircleData() || - bucket.hasTextCollisionBoxData() || bucket.hasTextCollisionCircleData()))) { + if (!bucket.collisionArrays && collisionBoxArray && ((bucket.hasIconCollisionBoxData() || bucket.hasTextCollisionBoxData()))) { bucket.deserializeCollisionBoxes(collisionBoxArray); } @@ -900,8 +966,7 @@ export class Placement { } } - if (bucket.hasIconCollisionBoxData() || bucket.hasIconCollisionCircleData() || - bucket.hasTextCollisionBoxData() || bucket.hasTextCollisionCircleData()) { + if (bucket.hasIconCollisionBoxData() || bucket.hasTextCollisionBoxData()) { const collisionArrays = bucket.collisionArrays[s]; if (collisionArrays) { let shift = new Point(0, 0); @@ -951,14 +1016,6 @@ export class Placement { hasIconTextFit ? shift.x : 0, hasIconTextFit ? shift.y : 0); } - - const textCircles = collisionArrays.textCircles; - if (textCircles && bucket.hasTextCollisionCircleData()) { - for (let k = 0; k < textCircles.length; k += 5) { - const notUsed = isDuplicate || textCircles[k + 4] === 0; - updateCollisionVertices(bucket.textCollisionCircle.collisionVertexArray, opacityState.text.placed, notUsed); - } - } } } } @@ -980,15 +1037,20 @@ export class Placement { if (bucket.hasTextCollisionBoxData() && bucket.textCollisionBox.collisionVertexBuffer) { bucket.textCollisionBox.collisionVertexBuffer.updateData(bucket.textCollisionBox.collisionVertexArray); } - if (bucket.hasIconCollisionCircleData() && bucket.iconCollisionCircle.collisionVertexBuffer) { - bucket.iconCollisionCircle.collisionVertexBuffer.updateData(bucket.iconCollisionCircle.collisionVertexArray); - } - if (bucket.hasTextCollisionCircleData() && bucket.textCollisionCircle.collisionVertexBuffer) { - bucket.textCollisionCircle.collisionVertexBuffer.updateData(bucket.textCollisionCircle.collisionVertexArray); - } assert(bucket.text.opacityVertexArray.length === bucket.text.layoutVertexArray.length / 4); assert(bucket.icon.opacityVertexArray.length === bucket.icon.layoutVertexArray.length / 4); + + // Push generated collision circles to the bucket for debug rendering + if (bucket.bucketInstanceId in this.collisionCircleArrays) { + const instance = this.collisionCircleArrays[bucket.bucketInstanceId]; + + bucket.placementInvProjMatrix = instance.invProjMatrix; + bucket.placementViewportMatrix = instance.viewportMatrix; + bucket.collisionCircleArray = instance.circles; + + delete this.collisionCircleArrays[bucket.bucketInstanceId]; + } } symbolFadeChange(now: number) { diff --git a/src/symbol/projection.js b/src/symbol/projection.js index 406989fc859..e0c16142989 100644 --- a/src/symbol/projection.js +++ b/src/symbol/projection.js @@ -16,7 +16,7 @@ import type { } from '../data/array_types'; import {WritingMode} from '../symbol/shaping'; -export {updateLineLabels, hideGlyphs, getLabelPlaneMatrix, getGlCoordMatrix, project, placeFirstAndLastGlyph, xyTransformMat4}; +export {updateLineLabels, hideGlyphs, getLabelPlaneMatrix, getGlCoordMatrix, project, getPerspectiveRatio, placeFirstAndLastGlyph, placeGlyphAlongLine, xyTransformMat4}; /* * # Overview of coordinate spaces @@ -113,6 +113,10 @@ function project(point: Point, matrix: mat4) { }; } +function getPerspectiveRatio(cameraToCenterDistance: number, signedDistanceFromCamera: number): number { + return 0.5 + 0.5 * (cameraToCenterDistance / signedDistanceFromCamera); +} + function isVisible(anchorPos: [number, number, number, number], clippingBuffer: [number, number]) { const x = anchorPos[0] / anchorPos[3]; @@ -157,6 +161,7 @@ function updateLineLabels(bucket: SymbolBucket, for (let s = 0; s < placedSymbols.length; s++) { const symbol: any = placedSymbols.get(s); + // Don't do calculations for vertical glyphs unless the previous symbol was horizontal // and we determined that vertical glyphs were necessary. // Also don't do calculations for symbols that are collided and fully faded out @@ -177,12 +182,10 @@ function updateLineLabels(bucket: SymbolBucket, } const cameraToAnchorDistance = anchorPos[3]; - const perspectiveRatio = 0.5 + 0.5 * (cameraToAnchorDistance / painter.transform.cameraToCenterDistance); + const perspectiveRatio = getPerspectiveRatio(painter.transform.cameraToCenterDistance, cameraToAnchorDistance); const fontSize = symbolSize.evaluateSizeForFeature(sizeData, partiallyEvaluatedSize, symbol); - const pitchScaledFontSize = pitchWithMap ? - fontSize * perspectiveRatio : - fontSize / perspectiveRatio; + const pitchScaledFontSize = pitchWithMap ? fontSize / perspectiveRatio : fontSize * perspectiveRatio; const tileAnchorPoint = new Point(symbol.anchorX, symbol.anchorY); const anchorPoint = project(tileAnchorPoint, labelPlaneMatrix).point; @@ -208,7 +211,7 @@ function updateLineLabels(bucket: SymbolBucket, } } -function placeFirstAndLastGlyph(fontScale: number, glyphOffsetArray: GlyphOffsetArray, lineOffsetX: number, lineOffsetY: number, flip: boolean, anchorPoint: Point, tileAnchorPoint: Point, symbol: any, lineVertexArray: SymbolLineVertexArray, labelPlaneMatrix: mat4, projectionCache: any, returnTileDistance: boolean) { +function placeFirstAndLastGlyph(fontScale: number, glyphOffsetArray: GlyphOffsetArray, lineOffsetX: number, lineOffsetY: number, flip: boolean, anchorPoint: Point, tileAnchorPoint: Point, symbol: any, lineVertexArray: SymbolLineVertexArray, labelPlaneMatrix: mat4, projectionCache: any) { const glyphEndIndex = symbol.glyphStartIndex + symbol.numGlyphs; const lineStartIndex = symbol.lineStartIndex; const lineEndIndex = symbol.lineStartIndex + symbol.lineLength; @@ -217,12 +220,12 @@ function placeFirstAndLastGlyph(fontScale: number, glyphOffsetArray: GlyphOffset const lastGlyphOffset = glyphOffsetArray.getoffsetX(glyphEndIndex - 1); const firstPlacedGlyph = placeGlyphAlongLine(fontScale * firstGlyphOffset, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment, - lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, returnTileDistance); + lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache); if (!firstPlacedGlyph) return null; const lastPlacedGlyph = placeGlyphAlongLine(fontScale * lastGlyphOffset, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment, - lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, returnTileDistance); + lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache); if (!lastPlacedGlyph) return null; @@ -263,7 +266,7 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la // Place the first and the last glyph in the label first, so we can figure out // the overall orientation of the label and determine whether it needs to be flipped in keepUpright mode - const firstAndLastGlyph = placeFirstAndLastGlyph(fontScale, glyphOffsetArray, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol, lineVertexArray, labelPlaneMatrix, projectionCache, false); + const firstAndLastGlyph = placeFirstAndLastGlyph(fontScale, glyphOffsetArray, lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol, lineVertexArray, labelPlaneMatrix, projectionCache); if (!firstAndLastGlyph) { return {notEnoughRoom: true}; } @@ -282,7 +285,7 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la // Since first and last glyph fit on the line, we're sure that the rest of the glyphs can be placed // $FlowFixMe placedGlyphs.push(placeGlyphAlongLine(fontScale * glyphOffsetArray.getoffsetX(glyphIndex), lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment, - lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache, false)); + lineStartIndex, lineEndIndex, lineVertexArray, labelPlaneMatrix, projectionCache)); } placedGlyphs.push(firstAndLastGlyph.last); } else { @@ -308,7 +311,7 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la } // $FlowFixMe const singleGlyph = placeGlyphAlongLine(fontScale * glyphOffsetArray.getoffsetX(symbol.glyphStartIndex), lineOffsetX, lineOffsetY, flip, anchorPoint, tileAnchorPoint, symbol.segment, - symbol.lineStartIndex, symbol.lineStartIndex + symbol.lineLength, lineVertexArray, labelPlaneMatrix, projectionCache, false); + symbol.lineStartIndex, symbol.lineStartIndex + symbol.lineLength, lineVertexArray, labelPlaneMatrix, projectionCache); if (!singleGlyph) return {notEnoughRoom: true}; @@ -343,8 +346,7 @@ function placeGlyphAlongLine(offsetX: number, lineEndIndex: number, lineVertexArray: SymbolLineVertexArray, labelPlaneMatrix: mat4, - projectionCache: {[_: number]: Point}, - returnTileDistance: boolean) { + projectionCache: {[_: number]: Point}) { const combinedOffsetX = flip ? offsetX - lineOffsetX : @@ -366,12 +368,12 @@ function placeGlyphAlongLine(offsetX: number, lineStartIndex + anchorSegment : lineStartIndex + anchorSegment + 1; - const initialIndex = currentIndex; let current = anchorPoint; let prev = anchorPoint; let distanceToPrev = 0; let currentSegmentDistance = 0; const absOffsetX = Math.abs(combinedOffsetX); + const pathVertices = []; while (distanceToPrev + currentSegmentDistance <= absOffsetX) { currentIndex += dir; @@ -381,6 +383,7 @@ function placeGlyphAlongLine(offsetX: number, return null; prev = current; + pathVertices.push(current); current = projectionCache[currentIndex]; if (current === undefined) { @@ -414,14 +417,12 @@ function placeGlyphAlongLine(offsetX: number, const segmentAngle = angle + Math.atan2(current.y - prev.y, current.x - prev.x); + pathVertices.push(p); + return { point: p, angle: segmentAngle, - tileDistance: returnTileDistance ? - { - prevTileDistance: (currentIndex - dir) === initialIndex ? 0 : lineVertexArray.gettileUnitDistanceFromAnchor(currentIndex - dir), - lastSegmentViewportDistance: absOffsetX - distanceToPrev - } : null + path: pathVertices }; } diff --git a/src/symbol/symbol_layout.js b/src/symbol/symbol_layout.js index 9dec812e28d..990c8417282 100644 --- a/src/symbol/symbol_layout.js +++ b/src/symbol/symbol_layout.js @@ -409,7 +409,7 @@ function addFeature(bucket: SymbolBucket, bucket.collisionBoxArray, feature.index, feature.sourceLayerIndex, bucket.index, textBoxScale, textPadding, textAlongLine, textOffset, iconBoxScale, iconPadding, iconAlongLine, iconOffset, - feature, sizes, isSDFIcon, canonical); + feature, sizes, isSDFIcon, canonical, layoutTextSize); }; if (symbolPlacement === 'line') { @@ -571,7 +571,8 @@ function addSymbol(bucket: SymbolBucket, feature: SymbolFeature, sizes: Sizes, isSDFIcon: boolean, - canonical: CanonicalTileID) { + canonical: CanonicalTileID, + layoutTextSize: number) { const lineArray = bucket.addToLineVertexArray(anchor, line); let textCollisionFeature, iconCollisionFeature, verticalTextCollisionFeature, verticalIconCollisionFeature; @@ -598,10 +599,10 @@ function addSymbol(bucket: SymbolBucket, const textRotation = layer.layout.get('text-rotate').evaluate(feature, {}, canonical); const verticalTextRotation = textRotation + 90.0; const verticalShaping = shapedTextOrientations.vertical; - verticalTextCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, verticalShaping, textBoxScale, textPadding, textAlongLine, bucket.overscaling, verticalTextRotation); + verticalTextCollisionFeature = new CollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, verticalShaping, textBoxScale, textPadding, textAlongLine, verticalTextRotation); if (verticallyShapedIcon) { - verticalIconCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, verticallyShapedIcon, iconBoxScale, iconPadding, textAlongLine, bucket.overscaling, verticalTextRotation); + verticalIconCollisionFeature = new CollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, verticallyShapedIcon, iconBoxScale, iconPadding, textAlongLine, verticalTextRotation); } } @@ -614,7 +615,7 @@ function addSymbol(bucket: SymbolBucket, const hasIconTextFit = layer.layout.get('icon-text-fit') !== 'none'; const iconQuads = getIconQuads(shapedIcon, iconRotate, isSDFIcon, hasIconTextFit); const verticalIconQuads = verticallyShapedIcon ? getIconQuads(verticallyShapedIcon, iconRotate, isSDFIcon, hasIconTextFit) : undefined; - iconCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedIcon, iconBoxScale, iconPadding, /*align boxes to line*/false, bucket.overscaling, iconRotate); + iconCollisionFeature = new CollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedIcon, iconBoxScale, iconPadding, /*align boxes to line*/false, iconRotate); numIconVertices = iconQuads.length * 4; @@ -683,7 +684,7 @@ function addSymbol(bucket: SymbolBucket, const textRotate = layer.layout.get('text-rotate').evaluate(feature, {}, canonical); // As a collision approximation, we can use either the vertical or any of the horizontal versions of the feature // We're counting on all versions having similar dimensions - textCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shaping, textBoxScale, textPadding, textAlongLine, bucket.overscaling, textRotate); + textCollisionFeature = new CollisionFeature(collisionBoxArray, anchor, featureIndex, sourceLayerIndex, bucketIndex, shaping, textBoxScale, textPadding, textAlongLine, textRotate); } const singleLine = shaping.positionedLines.length === 1; @@ -716,6 +717,27 @@ function addSymbol(bucket: SymbolBucket, const verticalIconBoxStartIndex = verticalIconCollisionFeature ? verticalIconCollisionFeature.boxStartIndex : bucket.collisionBoxArray.length; const verticalIconBoxEndIndex = verticalIconCollisionFeature ? verticalIconCollisionFeature.boxEndIndex : bucket.collisionBoxArray.length; + // Check if runtime collision circles should be used for any of the collision features. + // It is enough to choose the tallest feature shape as circles are always placed on a line. + // All measurements are in glyph metrics and later converted into pixels using proper font size "layoutTextSize" + let collisionCircleDiameter = -1; + + const getCollisionCircleHeight = (feature: ?CollisionFeature, prevHeight: number): number => { + if (feature && feature.circleDiameter) + return Math.max(feature.circleDiameter, prevHeight); + return prevHeight; + }; + + collisionCircleDiameter = getCollisionCircleHeight(textCollisionFeature, collisionCircleDiameter); + collisionCircleDiameter = getCollisionCircleHeight(verticalTextCollisionFeature, collisionCircleDiameter); + collisionCircleDiameter = getCollisionCircleHeight(iconCollisionFeature, collisionCircleDiameter); + collisionCircleDiameter = getCollisionCircleHeight(verticalIconCollisionFeature, collisionCircleDiameter); + const useRuntimeCollisionCircles = (collisionCircleDiameter > -1) ? 1 : 0; + + // Convert circle collision height into pixels + if (useRuntimeCollisionCircles) + collisionCircleDiameter *= layoutTextSize / ONE_EM; + if (bucket.glyphOffsetArray.length >= SymbolBucket.MAX_GLYPHS) warnOnce( "Too many glyphs being rendered in a tile. See https://github.com/mapbox/mapbox-gl-js/issues/2907" ); @@ -747,10 +769,12 @@ function addSymbol(bucket: SymbolBucket, numVerticalGlyphVertices, numIconVertices, numVerticalIconVertices, + useRuntimeCollisionCircles, 0, textBoxScale, textOffset0, - textOffset1); + textOffset1, + collisionCircleDiameter); } function anchorIsTooClose(bucket: any, text: string, repeatDistance: number, anchor: Point) { diff --git a/test/integration/query-tests/regressions/mapbox-gl-js#3534/style.json b/test/integration/query-tests/regressions/mapbox-gl-js#3534/style.json index a48e146804f..5c0b63b7269 100644 --- a/test/integration/query-tests/regressions/mapbox-gl-js#3534/style.json +++ b/test/integration/query-tests/regressions/mapbox-gl-js#3534/style.json @@ -7,7 +7,7 @@ "height": 500, "queryGeometry": [ 266, - 271 + 275 ], "queryOptions": { "layers": [ diff --git a/test/integration/query-tests/symbol-features-in/tilted-outside/style.json b/test/integration/query-tests/symbol-features-in/tilted-outside/style.json index ad8f0abd2e6..ee2e0a493a5 100644 --- a/test/integration/query-tests/symbol-features-in/tilted-outside/style.json +++ b/test/integration/query-tests/symbol-features-in/tilted-outside/style.json @@ -7,12 +7,12 @@ "height": 500, "queryGeometry": [ [ - 230, - 250 + 160, + 120 ], [ - 300, - 300 + 220, + 160 ] ] } diff --git a/test/integration/query-tests/symbol/inside/style.json b/test/integration/query-tests/symbol/inside/style.json index b7fd69dbca0..af99de2cd48 100644 --- a/test/integration/query-tests/symbol/inside/style.json +++ b/test/integration/query-tests/symbol/inside/style.json @@ -6,8 +6,8 @@ "width": 500, "height": 500, "queryGeometry": [ - 266, - 271 + 265, + 267 ] } }, diff --git a/test/integration/query-tests/symbol/rotated-inside/style.json b/test/integration/query-tests/symbol/rotated-inside/style.json index 420555c5a63..e90cb1432fc 100644 --- a/test/integration/query-tests/symbol/rotated-inside/style.json +++ b/test/integration/query-tests/symbol/rotated-inside/style.json @@ -6,8 +6,8 @@ "width": 500, "height": 500, "queryGeometry": [ - 370, - 305 + 440, + 215 ] } }, diff --git a/test/integration/render-tests/debug/collision-icon-text-line-translate/expected.png b/test/integration/render-tests/debug/collision-icon-text-line-translate/expected.png index 87cb7b39db2..dcb0727a4f1 100644 Binary files a/test/integration/render-tests/debug/collision-icon-text-line-translate/expected.png and b/test/integration/render-tests/debug/collision-icon-text-line-translate/expected.png differ diff --git a/test/integration/render-tests/debug/collision-lines-large-font/expected.png b/test/integration/render-tests/debug/collision-lines-large-font/expected.png new file mode 100644 index 00000000000..17828d650fd Binary files /dev/null and b/test/integration/render-tests/debug/collision-lines-large-font/expected.png differ diff --git a/test/integration/render-tests/debug/collision-lines-large-font/style.json b/test/integration/render-tests/debug/collision-lines-large-font/style.json new file mode 100644 index 00000000000..27a720bd2d7 --- /dev/null +++ b/test/integration/render-tests/debug/collision-lines-large-font/style.json @@ -0,0 +1,61 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "width": 512, + "height": 128 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 4, + "sources": { + "geojson": { + "type": "geojson", + "maxzoom": 1, + "data": { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -10, + 0 + ], + [ + 10, + 0 + ] + ] + } + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "symbol", + "source": "geojson", + "layout": { + "text-field": "Cover me in Circles", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-size": 36, + "symbol-placement": "line-center" + } + } + ] +} diff --git a/test/integration/render-tests/debug/collision-lines-notocjk/expected.png b/test/integration/render-tests/debug/collision-lines-notocjk/expected.png new file mode 100644 index 00000000000..bb94f1d5c48 Binary files /dev/null and b/test/integration/render-tests/debug/collision-lines-notocjk/expected.png differ diff --git a/test/integration/render-tests/debug/collision-lines-notocjk/style.json b/test/integration/render-tests/debug/collision-lines-notocjk/style.json new file mode 100644 index 00000000000..c94c3800d64 --- /dev/null +++ b/test/integration/render-tests/debug/collision-lines-notocjk/style.json @@ -0,0 +1,58 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "width": 512, + "height": 128 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 4, + "sources": { + "geojson": { + "type": "geojson", + "maxzoom": 1, + "data": { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -10, + 0 + ], + [ + 10, + 0 + ] + ] + } + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "symbol", + "source": "geojson", + "layout": { + "text-field": "光中輪的態指那差車", + "text-font": [ "NotoCJK" ], + "text-size": 24, + "symbol-placement": "line-center" + } + } + ] +} diff --git a/test/integration/render-tests/debug/collision-lines-overscaled/expected.png b/test/integration/render-tests/debug/collision-lines-overscaled/expected.png index 4c68f473ba3..b85bbcbd606 100644 Binary files a/test/integration/render-tests/debug/collision-lines-overscaled/expected.png and b/test/integration/render-tests/debug/collision-lines-overscaled/expected.png differ diff --git a/test/integration/render-tests/debug/collision-lines-pitched-2/expected.png b/test/integration/render-tests/debug/collision-lines-pitched-2/expected.png new file mode 100644 index 00000000000..fc3cceeb79f Binary files /dev/null and b/test/integration/render-tests/debug/collision-lines-pitched-2/expected.png differ diff --git a/test/integration/render-tests/debug/collision-lines-pitched-2/style.json b/test/integration/render-tests/debug/collision-lines-pitched-2/style.json new file mode 100644 index 00000000000..bc99bfa3536 --- /dev/null +++ b/test/integration/render-tests/debug/collision-lines-pitched-2/style.json @@ -0,0 +1,76 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "width": 512, + "height": 512 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 4, + "pitch": 60, + "bearing": 90, + "sources": { + "geojson": { + "type": "geojson", + "maxzoom": 1, + "data": { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "properties": { + "name": "Some sentence that is quite long" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ -20, 4 ], [ 20, 4 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "Another sentence that is even longer than the previous one" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0, 0 ], [ 50, 0 ] + ] + } + + }] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "symbol", + "source": "geojson", + "layout": { + "text-field": "{name}", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "symbol-placement": "line", + "text-pitch-alignment": "map" + } + } + ] +} diff --git a/test/integration/render-tests/debug/collision-lines-pitched/expected.png b/test/integration/render-tests/debug/collision-lines-pitched/expected.png index bb2254df577..0a7568e32e6 100644 Binary files a/test/integration/render-tests/debug/collision-lines-pitched/expected.png and b/test/integration/render-tests/debug/collision-lines-pitched/expected.png differ diff --git a/test/integration/render-tests/debug/collision-lines-simple-words-pitched/expected.png b/test/integration/render-tests/debug/collision-lines-simple-words-pitched/expected.png new file mode 100644 index 00000000000..fc999d109a5 Binary files /dev/null and b/test/integration/render-tests/debug/collision-lines-simple-words-pitched/expected.png differ diff --git a/test/integration/render-tests/debug/collision-lines-simple-words-pitched/style.json b/test/integration/render-tests/debug/collision-lines-simple-words-pitched/style.json new file mode 100644 index 00000000000..d484aa75f89 --- /dev/null +++ b/test/integration/render-tests/debug/collision-lines-simple-words-pitched/style.json @@ -0,0 +1,127 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "width": 512, + "height": 512 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 4, + "pitch": 60, + "sources": { + "geojson": { + "type": "geojson", + "maxzoom": 1, + "data": { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "properties": { + "name": "a" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ -5, 4 ], [ -1, 4 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "ab" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ -5, 0 ], [ -1, 0 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "abc" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ -5, -4 ], [ -1, -4 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "abcd" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ 1, 4 ], [ 5, 4 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "abcde" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ 1, 0 ], [ 5, 0 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "abcdef" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ 1, -4 ], [ 5, -4 ] + ] + } + + }] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "symbol", + "source": "geojson", + "layout": { + "text-field": "{name}", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "symbol-placement": "line-center", + "text-pitch-alignment": "viewport" + } + } + ] +} diff --git a/test/integration/render-tests/debug/collision-lines-simple-words/expected.png b/test/integration/render-tests/debug/collision-lines-simple-words/expected.png new file mode 100644 index 00000000000..5cf9c50334d Binary files /dev/null and b/test/integration/render-tests/debug/collision-lines-simple-words/expected.png differ diff --git a/test/integration/render-tests/debug/collision-lines-simple-words/style.json b/test/integration/render-tests/debug/collision-lines-simple-words/style.json new file mode 100644 index 00000000000..e9f9e7c09fb --- /dev/null +++ b/test/integration/render-tests/debug/collision-lines-simple-words/style.json @@ -0,0 +1,126 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "width": 512, + "height": 128 + } + }, + "center": [ + 0, + 0 + ], + "zoom": 4, + "sources": { + "geojson": { + "type": "geojson", + "maxzoom": 1, + "data": { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "properties": { + "name": "a" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ -5, 2 ], [ -1, 2 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "ab" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ -5, 0 ], [ -1, 0 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "abc" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ -5, -2 ], [ -1, -2 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "abcd" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ 1, 2 ], [ 5, 2 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "abcde" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ 1, 0 ], [ 5, 0 ] + ] + } + + }, + { + "type": "Feature", + "properties": { + "name": "abcdef" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ 1, -2 ], [ 5, -2 ] + ] + } + + }] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "symbol", + "source": "geojson", + "layout": { + "text-field": "{name}", + "symbol-spacing": 30, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "symbol-placement": "line-center" + } + } + ] +} diff --git a/test/integration/render-tests/debug/collision-lines/expected.png b/test/integration/render-tests/debug/collision-lines/expected.png index a2476e20f8c..f062d08c71a 100644 Binary files a/test/integration/render-tests/debug/collision-lines/expected.png and b/test/integration/render-tests/debug/collision-lines/expected.png differ diff --git a/test/integration/render-tests/regressions/mapbox-gl-js#5911a/expected.png b/test/integration/render-tests/regressions/mapbox-gl-js#5911a/expected.png index 7dd35ba9bf5..3aa0e1cd5a8 100644 Binary files a/test/integration/render-tests/regressions/mapbox-gl-js#5911a/expected.png and b/test/integration/render-tests/regressions/mapbox-gl-js#5911a/expected.png differ diff --git a/test/integration/render-tests/symbol-placement/line-center/expected.png b/test/integration/render-tests/symbol-placement/line-center/expected.png index 07af2a7cbad..ea7fabda29e 100644 Binary files a/test/integration/render-tests/symbol-placement/line-center/expected.png and b/test/integration/render-tests/symbol-placement/line-center/expected.png differ diff --git a/test/integration/render-tests/text-keep-upright/line-placement-true-pitched/expected.png b/test/integration/render-tests/text-keep-upright/line-placement-true-pitched/expected.png index 3b9d23158e9..981724f542c 100644 Binary files a/test/integration/render-tests/text-keep-upright/line-placement-true-pitched/expected.png and b/test/integration/render-tests/text-keep-upright/line-placement-true-pitched/expected.png differ diff --git a/test/integration/render-tests/text-pitch-alignment/auto-text-rotation-alignment-map/expected.png b/test/integration/render-tests/text-pitch-alignment/auto-text-rotation-alignment-map/expected.png index 124e31a9bcc..79deb753886 100644 Binary files a/test/integration/render-tests/text-pitch-alignment/auto-text-rotation-alignment-map/expected.png and b/test/integration/render-tests/text-pitch-alignment/auto-text-rotation-alignment-map/expected.png differ diff --git a/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-map/expected.png b/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-map/expected.png index 124e31a9bcc..79deb753886 100644 Binary files a/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-map/expected.png and b/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-map/expected.png differ diff --git a/test/integration/render-tests/text-pitch-alignment/viewport-overzoomed/expected.png b/test/integration/render-tests/text-pitch-alignment/viewport-overzoomed/expected.png index f2b0fe8404e..584ad04f0bc 100644 Binary files a/test/integration/render-tests/text-pitch-alignment/viewport-overzoomed/expected.png and b/test/integration/render-tests/text-pitch-alignment/viewport-overzoomed/expected.png differ diff --git a/test/integration/render-tests/text-pitch-scaling/line-half/expected.png b/test/integration/render-tests/text-pitch-scaling/line-half/expected.png index 9e703607656..bdaf1617a64 100644 Binary files a/test/integration/render-tests/text-pitch-scaling/line-half/expected.png and b/test/integration/render-tests/text-pitch-scaling/line-half/expected.png differ diff --git a/test/unit/symbol/clip_line.test.js b/test/unit/symbol/clip_line.test.js new file mode 100644 index 00000000000..9e33c25a34e --- /dev/null +++ b/test/unit/symbol/clip_line.test.js @@ -0,0 +1,165 @@ +import {test} from '../../util/test'; +import Point from '@mapbox/point-geometry'; +import clipLine from '../../../src/symbol/clip_line'; + +test('clipLines', (t) => { + + const minX = -300; + const maxX = 300; + const minY = -200; + const maxY = 200; + + const clipLineTest = (lines) => { + return clipLine(lines, minX, minY, maxX, maxY); + }; + + t.test('Single line fully inside', (t) => { + const line = [ + new Point(-100, -100), + new Point(-40, -100), + new Point(200, 0), + new Point(-80, 195) + ]; + + t.deepEqual(clipLineTest([line]), [line]); + t.end(); + }); + + t.test('Multiline fully inside', (t) => { + const line0 = [ + new Point(-250, -150), + new Point(-250, 150), + new Point(-10, 150), + new Point(-10, -150) + ]; + + const line1 = [ + new Point(250, -150), + new Point(250, 150), + new Point(10, 150), + new Point(10, -150) + ]; + + const lines = [line0, line1]; + + t.deepEqual(clipLineTest(lines), lines); + t.end(); + }); + + t.test('Lines fully outside', (t) => { + const line0 = [ + new Point(-400, -300), + new Point(-350, 0), + new Point(-300, 300) + ]; + + const line1 = [ + new Point(1000, 210), + new Point(10000, 500) + ]; + + t.deepEqual(clipLineTest([line0, line1]), []); + t.end(); + }); + + t.test('Intersect with single border', (t) => { + const line0 = [ + new Point(-400, 0), + new Point(0, 0) + ]; + + const result0 = [ + new Point(minX, 0), + new Point(0, 0) + ]; + + const line1 = [ + new Point(250, -50), + new Point(350, 50) + ]; + + const result1 = [ + new Point(250, -50), + new Point(maxX, 0) + ]; + + t.deepEqual(clipLineTest([line0, line1]), [result0, result1]); + t.end(); + }); + + t.test('Intersect with multiple borders', (t) => { + const line0 = [ + new Point(-350, -100), + new Point(-200, -250) + ]; + + const line1 = [ + new Point(-100, 250), + new Point(0, 150), + new Point(100, 250) + ]; + + const result0 = [ + new Point(minX, -150), + new Point(-250, minY) + ]; + + const result1 = [ + new Point(-50, maxY), + new Point(0, 150), + new Point(50, maxY) + ]; + + t.deepEqual(clipLineTest([line0, line1]), [result0, result1]); + t.end(); + }); + + t.test('Single line can be split into multiple segments', (t) => { + const line = [ + new Point(-80, 150), + new Point(-80, 350), + new Point(120, 1000), + new Point(120, 0) + ]; + + const result0 = [ + new Point(-80, 150), + new Point(-80, maxY), + ]; + + const result1 = [ + new Point(120, maxY), + new Point(120, 0), + ]; + + t.deepEqual(clipLineTest([line]), [result0, result1]); + t.end(); + }); + + t.test('Non-clipped points are bit exact', (t) => { + const line = [ + new Point(-500, -200), + new Point(131.2356763, 0.956732) + ]; + + t.deepEqual(clipLineTest([line])[1], line[0][1]); + t.end(); + }); + + t.test('Clipped points are rounded to the nearest integer', (t) => { + const line = [ + new Point(310, 2.9), + new Point(290, 2.5) + ]; + + const result = [ + new Point(maxX, 3), + new Point(290, 2.5) + ]; + + t.deepEqual(clipLineTest([line]), [result]); + t.end(); + }); + + t.end(); +}); diff --git a/test/unit/symbol/collision_feature.js b/test/unit/symbol/collision_feature.js index eff6d362ebd..b039c02f0b2 100644 --- a/test/unit/symbol/collision_feature.js +++ b/test/unit/symbol/collision_feature.js @@ -19,7 +19,8 @@ test('CollisionFeature', (t) => { const point = new Point(500, 0); const anchor = new Anchor(point.x, point.y, 0, undefined); - const cf = new CollisionFeature(collisionBoxArray, [point], anchor, 0, 0, 0, shapedText, 1, 0, false, 1); + const cf = new CollisionFeature(collisionBoxArray, anchor, 0, 0, 0, shapedText, 1, 0, false); + t.notOk(cf.circleDiameter); t.equal(cf.boxEndIndex - cf.boxStartIndex, 1); const box = collisionBoxArray.get(cf.boxStartIndex); @@ -30,89 +31,16 @@ test('CollisionFeature', (t) => { t.end(); }); - test('line label', (t) => { - const line = [new Point(0, 0), new Point(500, 100), new Point(510, 90), new Point(700, 0)]; + test('Compute line height for runtime collision circles (line label)', (t) => { const anchor = new Anchor(505, 95, 0, 1); - const cf = new CollisionFeature(collisionBoxArray, line, anchor, 0, 0, 0, shapedText, 1, 0, true, 1); - const boxPoints = pluckAnchorPoints(cf); - t.deepEqual(boxPoints, [ - {x: 370, y: 74}, - {x: 389, y: 78}, - {x: 409, y: 82}, - {x: 428, y: 86}, - {x: 448, y: 90}, - {x: 468, y: 94}, - {x: 478, y: 96}, - {x: 487, y: 97}, - {x: 497, y: 99}, - {x: 505, y: 95}, - {x: 513, y: 89}, - {x: 522, y: 84}, - {x: 531, y: 80}, - {x: 540, y: 76}, - {x: 549, y: 72}, - {x: 558, y: 67}, - {x: 576, y: 59}, - {x: 594, y: 50}, - {x: 612, y: 42}, - {x: 630, y: 33} ]); - t.end(); - }); - - test('boxes for handling pitch underzooming', (t) => { - const line = [new Point(0, 0), new Point(500, 100), new Point(510, 90), new Point(700, 0)]; - const anchor = new Anchor(505, 95, 0, 1); - const cf = new CollisionFeature(collisionBoxArray, line, anchor, 0, 0, 0, shapedText, 1, 0, true, 1); - const distancesToAnchor = pluckDistancesToAnchor(cf); - t.deepEqual(distancesToAnchor, [ - -112, - -96, - -80, - -64, - -48, - -32, - -24, - -16, - -8, - 0, - 8, - 16, - 24, - 32, - 40, - 48, - 64, - 80, - 96, - 112]); - t.end(); - }); - - test('vertical line label', (t) => { - const line = [new Point(0, 0), new Point(0, 100), new Point(0, 111), new Point(0, 112), new Point(0, 200)]; - const anchor = new Anchor(0, 110, 0, 1); - const cf = new CollisionFeature(collisionBoxArray, line, anchor, 0, 0, 0, shapedText, 1, 0, true, 1); - const boxPoints = pluckAnchorPoints(cf); - t.deepEqual(boxPoints, [ - {x: 0, y: 10}, - {x: 0, y: 30}, - {x: 0, y: 50}, - {x: 0, y: 70}, - {x: 0, y: 80}, - {x: 0, y: 90}, - {x: 0, y: 100}, - {x: 0, y: 110}, - {x: 0, y: 120}, - {x: 0, y: 130}, - {x: 0, y: 140}, - {x: 0, y: 150}, - {x: 0, y: 160}, - {x: 0, y: 170}, - {x: 0, y: 190} ]); + const cf = new CollisionFeature(collisionBoxArray, anchor, 0, 0, 0, shapedText, 1, 0, true); + t.ok(cf.circleDiameter); + t.equal(cf.circleDiameter, shapedText.bottom - shapedText.top); + t.equal(cf.boxEndIndex - cf.boxStartIndex, 0); t.end(); }); - test('doesnt create any boxes for features with zero height', (t) => { + test('Collision circle diameter is not computed for features with zero height', (t) => { const shapedText = { left: -50, top: -10, @@ -120,14 +48,14 @@ test('CollisionFeature', (t) => { bottom: -10 }; - const line = [new Point(0, 0), new Point(500, 100), new Point(510, 90), new Point(700, 0)]; const anchor = new Anchor(505, 95, 0, 1); - const cf = new CollisionFeature(collisionBoxArray, line, anchor, 0, 0, 0, shapedText, 1, 0, true, 1); + const cf = new CollisionFeature(collisionBoxArray, anchor, 0, 0, 0, shapedText, 1, 0, true); t.equal(cf.boxEndIndex - cf.boxStartIndex, 0); + t.notOk(cf.circleDiameter); t.end(); }); - test('doesnt create any boxes for features with negative height', (t) => { + test('Collision circle diameter is not computed for features with negative height', (t) => { const shapedText = { left: -50, top: 10, @@ -135,14 +63,14 @@ test('CollisionFeature', (t) => { bottom: -10 }; - const line = [new Point(0, 0), new Point(500, 100), new Point(510, 90), new Point(700, 0)]; const anchor = new Anchor(505, 95, 0, 1); - const cf = new CollisionFeature(collisionBoxArray, line, anchor, 0, 0, 0, shapedText, 1, 0, true, 1); + const cf = new CollisionFeature(collisionBoxArray, anchor, 0, 0, 0, shapedText, 1, 0, true); t.equal(cf.boxEndIndex - cf.boxStartIndex, 0); + t.notOk(cf.circleDiameter); t.end(); }); - test('doesnt create way too many tiny boxes for features with really low height', (t) => { + test('Use minimum collision circle diameter', (t) => { const shapedText = { left: -50, top: 10, @@ -150,37 +78,12 @@ test('CollisionFeature', (t) => { bottom: 10.00001 }; - const line = [new Point(0, 0), new Point(500, 100), new Point(510, 90), new Point(700, 0)]; const anchor = new Anchor(505, 95, 0, 1); - const cf = new CollisionFeature(collisionBoxArray, line, anchor, 0, 0, 0, shapedText, 1, 0, true, 1); - t.ok(cf.boxEndIndex - cf.boxStartIndex < 45); - t.end(); - }); - - test('height is big enough that first box can be placed *after* anchor', (t) => { - const line = [new Point(3103, 4068), new Point(3225.6206896551726, 4096)]; - const anchor = new Anchor(3144.5959947505007, 4077.498298013894, 0.22449735614507618, 0); - const shaping = {right: 256, left: 0, bottom: 256, top: 0}; - const cf = new CollisionFeature(collisionBoxArray, line, anchor, 0, 0, 0, shaping, 1, 0, true, 1); - t.equal(cf.boxEndIndex - cf.boxStartIndex, 1); + const cf = new CollisionFeature(collisionBoxArray, anchor, 0, 0, 0, shapedText, 1, 0, true); + t.equal(cf.boxEndIndex - cf.boxStartIndex, 0); + t.equal(cf.circleDiameter, 10); t.end(); }); t.end(); - - function pluckAnchorPoints(cf) { - const result = []; - for (let i = cf.boxStartIndex; i < cf.boxEndIndex; i++) { - result.push(collisionBoxArray.get(i).anchorPoint); - } - return result; - } - - function pluckDistancesToAnchor(cf) { - const result = []; - for (let i = cf.boxStartIndex; i < cf.boxEndIndex; i++) { - result.push(collisionBoxArray.get(i).signedDistanceFromAnchor); - } - return result; - } }); diff --git a/test/unit/symbol/path_interpolator.test.js b/test/unit/symbol/path_interpolator.test.js new file mode 100644 index 00000000000..ac772d5c63d --- /dev/null +++ b/test/unit/symbol/path_interpolator.test.js @@ -0,0 +1,146 @@ +import {test} from '../../util/test'; +import Point from '@mapbox/point-geometry'; +import PathInterpolator from '../../../src/symbol/path_interpolator'; + +test('PathInterpolator', (t) => { + + const pointEquals = (p0, p1) => { + const e = 0.000001; + return Math.abs(p0.x - p1.x) < e && Math.abs(p0.y - p1.y) < e; + }; + + t.test('Interpolate single segment path', (t) => { + const line = [ + new Point(0, 0), + new Point(10, 0) + ]; + + const interpolator = new PathInterpolator(line); + + t.deepEqual(interpolator.lerp(0.0), line[0]); + t.deepEqual(interpolator.lerp(0.5), new Point(5, 0)); + t.deepEqual(interpolator.lerp(1.0), line[1]); + t.end(); + }); + + t.test('t < 0', (t) => { + const line = [ + new Point(0, 0), + new Point(10, 0) + ]; + + const interpolator = new PathInterpolator(line); + t.deepEqual(interpolator.lerp(-100.0), line[0]); + t.end(); + }); + + t.test('t > 0', (t) => { + const line = [ + new Point(0, 0), + new Point(10, 0) + ]; + + const interpolator = new PathInterpolator(line); + t.deepEqual(interpolator.lerp(100.0), line[1]); + t.end(); + }); + + t.test('Interpolate multi-segment path', (t) => { + const line = [ + new Point(-3, 3), + new Point(-1, 3), + new Point(-1, -2), + new Point(2, -2), + new Point(2, 1), + new Point(-3, 1) + ]; + + const interpolator = new PathInterpolator(line); + t.ok(pointEquals(interpolator.lerp(1.0), new Point(-3, 1))); + t.ok(pointEquals(interpolator.lerp(0.95), new Point(-2.1, 1))); + t.ok(pointEquals(interpolator.lerp(0.5), new Point(1, -2))); + t.ok(pointEquals(interpolator.lerp(0.25), new Point(-1, 0.5))); + t.ok(pointEquals(interpolator.lerp(0.1), new Point(-1.2, 3))); + t.ok(pointEquals(interpolator.lerp(0.0), new Point(-3, 3))); + t.end(); + }); + + t.test('Small padding', (t) => { + const line = [ + new Point(-4, 1), + new Point(4, 1) + ]; + + const padding = 0.5; + const interpolator = new PathInterpolator(line, padding); + + t.ok(pointEquals(interpolator.lerp(0.0), new Point(-3.5, 1))); + t.ok(pointEquals(interpolator.lerp(0.25), new Point(-1.75, 1))); + t.ok(pointEquals(interpolator.lerp(0.5), new Point(0, 1))); + t.ok(pointEquals(interpolator.lerp(1.0), new Point(3.5, 1))); + t.end(); + }); + + t.test('Padding cannot be larger than the length / 2', (t) => { + const line = [ + new Point(-3, 0), + new Point(3, 0) + ]; + + const padding = 10.0; + const interpolator = new PathInterpolator(line, padding); + + t.ok(pointEquals(interpolator.lerp(0.0), new Point(0, 0))); + t.ok(pointEquals(interpolator.lerp(0.4), new Point(0, 0))); + t.ok(pointEquals(interpolator.lerp(1.0), new Point(0, 0))); + t.end(); + }); + + t.test('Single point path', (t) => { + const interpolator = new PathInterpolator([new Point(0, 0)]); + t.ok(pointEquals(interpolator.lerp(0), new Point(0, 0))); + t.ok(pointEquals(interpolator.lerp(1.0), new Point(0, 0))); + t.end(); + }); + + t.test('Interpolator instance can be reused by calling reset()', (t) => { + const line0 = [ + new Point(0, 0), + new Point(10, 0) + ]; + + const line1 = [ + new Point(-10, 10), + new Point(10, -10) + ]; + + const interpolator = new PathInterpolator(line0); + + t.deepEqual(interpolator.lerp(0.0), line0[0]); + t.deepEqual(interpolator.lerp(0.5), new Point(5, 0)); + t.deepEqual(interpolator.lerp(1.0), line0[1]); + + interpolator.reset(line1); + t.ok(pointEquals(interpolator.lerp(0.0), line1[0])); + t.ok(pointEquals(interpolator.lerp(0.5), new Point(0, 0))); + t.ok(pointEquals(interpolator.lerp(1.0), line1[1])); + t.end(); + }); + + t.test('Path with zero length segment', (t) => { + const line = [ + new Point(-1, 0), + new Point(1, 0), + new Point(1, 0) + ]; + + const interpolator = new PathInterpolator(line); + t.ok(pointEquals(interpolator.lerp(0), line[0])); + t.ok(pointEquals(interpolator.lerp(0.5), new Point(0, 0))); + t.ok(pointEquals(interpolator.lerp(1), line[1])); + t.ok(pointEquals(interpolator.lerp(1), line[2])); + t.end(); + }); + + t.end(); +});