diff --git a/modules/carto/src/layers/point-label-layer.ts b/modules/carto/src/layers/point-label-layer.ts index 56162716f7b..069763f1c84 100644 --- a/modules/carto/src/layers/point-label-layer.ts +++ b/modules/carto/src/layers/point-label-layer.ts @@ -40,18 +40,43 @@ class EnhancedTextBackgroundLayer extends TextBackgroundLayer { } } -// TextLayer which includes modified text-background-layer-vertex shader and only renders the -// primary background layer in the collision pass +type EnhancedTextLayerProps = TextLayerProps & { + renderBackground?: boolean; + collisionBackgroundPadding?: TextLayerProps['backgroundPadding']; +}; + +// TextLayer which includes modified text-background-layer-vertex shader and uses a separate +// background layer for collision testing, while preserving TextLayer's visual background support. class EnhancedTextLayer extends TextLayer { static layerName = 'EnhancedTextLayer'; filterSubLayer({layer, renderPass}) { - const background = layer.id.includes('primary-background'); + const collisionBackground = layer.id.endsWith('-collision-background'); if (renderPass === 'collision') { - return background; // Only draw primary background layer in collision pass + return collisionBackground; // Only draw collision background layer in collision pass } - return !background; // Do not draw background layer in other passes + return !collisionBackground; // Do not draw collision background layer in other passes + } + + renderLayers(): ReturnType { + const layers = super.renderLayers(); + const backgroundLayer = layers[0] as TextBackgroundLayer | false; + const charactersLayer = layers[1]; + const {renderBackground, collisionBackgroundPadding} = this.props as EnhancedTextLayerProps; + + const collisionBackgroundLayer = + backgroundLayer && + new EnhancedTextBackgroundLayer(backgroundLayer.props, { + id: `${this.id}-collision-background`, + padding: collisionBackgroundPadding + }); + + return [ + Boolean(renderBackground) && backgroundLayer, + collisionBackgroundLayer, + charactersLayer + ]; } } @@ -154,13 +179,13 @@ export default class PointLabelLayer< ]; } - calculateBackgroundPadding() { + calculateBackgroundPadding(): [number, number, number, number] { const {getTextAnchor: anchor, getAlignmentBaseline: alignment, sizeScale} = this.props; // Heuristics to avoid label overlap const paddingX = 12 * sizeScale; const paddingY = 3 * sizeScale; - const backgroundPadding = [0, 0, 0, 0]; + const backgroundPadding: [number, number, number, number] = [0, 0, 0, 0]; if (alignment === 'top') { backgroundPadding[TOP] = paddingY; } else if (alignment === 'bottom') { @@ -192,6 +217,11 @@ export default class PointLabelLayer< outlineColor, outlineWidth, sizeScale, + getBackgroundColor, + getBorderColor, + getBorderWidth, + backgroundBorderRadius, + backgroundPadding, radiusScale, getAlignmentBaseline, @@ -223,6 +253,11 @@ export default class PointLabelLayer< outlineColor, outlineWidth, sizeScale, + getBackgroundColor, + getBorderColor, + getBorderWidth, + backgroundBorderRadius, + backgroundPadding, getAlignmentBaseline, getColor, @@ -242,8 +277,7 @@ export default class PointLabelLayer< } }), { - getSize: 1, - _subLayerProps: {background: {type: EnhancedTextBackgroundLayer}} + getSize: 1 }, props ); @@ -251,6 +285,7 @@ export default class PointLabelLayer< renderLayers(): Layer | null | LayersList { const { + background, getText, getSecondaryColor, getSecondaryText, @@ -259,14 +294,15 @@ export default class PointLabelLayer< updateTriggers } = this.props; const getPixelOffset = this.calculatePixelOffset(false); - const backgroundPadding = this.calculateBackgroundPadding(); + const collisionBackgroundPadding = this.calculateBackgroundPadding(); const out = [ // Text doesn't update via updateTrigger for some reason this.renderTextLayer(`${updateTriggers.getText}-primary`, { - backgroundPadding, + background: true, + renderBackground: background, getText, getPixelOffset, - background: true // Only use background for primary label for faster collisions + collisionBackgroundPadding }), Boolean(getSecondaryText) && this.renderTextLayer(`${updateTriggers.getSecondaryText}-secondary`, { diff --git a/test/modules/carto/layers/point-label-layer.spec.ts b/test/modules/carto/layers/point-label-layer.spec.ts index ba627627ec5..85419313157 100644 --- a/test/modules/carto/layers/point-label-layer.spec.ts +++ b/test/modules/carto/layers/point-label-layer.spec.ts @@ -37,7 +37,7 @@ test('PointLabelLayer', () => { expect( !textLayer.filterSubLayer({layer: textBackgroundLayer, renderPass: 'draw'}), - 'background not drawn in draw pass' + 'collision background not drawn in draw pass' ).toBeTruthy(); expect( textLayer.filterSubLayer({layer: multiIconLayer, renderPass: 'draw'}), @@ -53,6 +53,85 @@ test('PointLabelLayer', () => { ).toBeTruthy(); } }, + { + props: { + data: FIXTURES.geojson, + background: true, + getBackgroundColor: [255, 255, 255, 200], + getBorderColor: [255, 0, 0, 255], + getBorderWidth: 2, + backgroundBorderRadius: 4, + backgroundPadding: [5, 6, 7, 8] + }, + onAfterUpdate: ({subLayers}) => { + const [textLayer] = subLayers; + const textSubLayers = textLayer.getSubLayers(); + expect( + textSubLayers.length, + 'visual background, collision background and text created' + ).toBe(3); + + const visualBackgroundLayer = textSubLayers.find( + layer => layer.id.endsWith('-background') && !layer.id.endsWith('-collision-background') + )!; + const collisionBackgroundLayer = textSubLayers.find(layer => + layer.id.endsWith('-collision-background') + )!; + const multiIconLayer = textSubLayers.find( + layer => layer.constructor.layerName === 'MultiIconLayer' + )!; + + expect(visualBackgroundLayer, 'visual background subLayer created').toBeTruthy(); + expect(collisionBackgroundLayer, 'collision background subLayer created').toBeTruthy(); + expect(multiIconLayer, 'text subLayer created').toBeTruthy(); + expect( + visualBackgroundLayer.constructor.layerName, + 'visual background uses the standard TextLayer shader' + ).toBe('TextBackgroundLayer'); + expect( + collisionBackgroundLayer.constructor.layerName, + 'collision background uses the expanded collision shader' + ).toBe('EnhancedTextBackgroundLayer'); + + expect( + visualBackgroundLayer.props.padding, + 'visual background uses TextLayer padding' + ).toEqual([5, 6, 7, 8]); + expect( + visualBackgroundLayer.props.getFillColor, + 'visual background color forwarded' + ).toEqual([255, 255, 255, 200]); + expect( + visualBackgroundLayer.props.getLineColor, + 'visual background border color forwarded' + ).toEqual([255, 0, 0, 255]); + expect( + visualBackgroundLayer.props.getLineWidth, + 'visual background border width forwarded' + ).toBe(2); + expect( + visualBackgroundLayer.props.borderRadius, + 'visual background border radius forwarded' + ).toBe(4); + + expect( + textLayer.filterSubLayer({layer: visualBackgroundLayer, renderPass: 'draw'}), + 'visual background drawn in draw pass' + ).toBeTruthy(); + expect( + !textLayer.filterSubLayer({layer: collisionBackgroundLayer, renderPass: 'draw'}), + 'collision background not drawn in draw pass' + ).toBeTruthy(); + expect( + !textLayer.filterSubLayer({layer: visualBackgroundLayer, renderPass: 'collision'}), + 'visual background not drawn in collision pass' + ).toBeTruthy(); + expect( + textLayer.filterSubLayer({layer: collisionBackgroundLayer, renderPass: 'collision'}), + 'collision background drawn in collision pass' + ).toBeTruthy(); + } + }, { props: { data: FIXTURES.geojson, diff --git a/test/render/golden-images/point-label-layer-background.png b/test/render/golden-images/point-label-layer-background.png new file mode 100644 index 00000000000..4da8bb03721 Binary files /dev/null and b/test/render/golden-images/point-label-layer-background.png differ diff --git a/test/render/test-cases/text-layer.spec.ts b/test/render/test-cases/text-layer.spec.ts index 84b17da4ca6..f7f0958b31f 100644 --- a/test/render/test-cases/text-layer.spec.ts +++ b/test/render/test-cases/text-layer.spec.ts @@ -8,6 +8,7 @@ import type {TestCase} from '../deck-test-utils'; import {COORDINATE_SYSTEM, OrthographicView} from '@deck.gl/core'; import {TextLayer, PathLayer} from '@deck.gl/layers'; +import {PointLabelLayer} from '@deck.gl/carto'; import {PathStyleExtension} from '@deck.gl/extensions'; import {points} from 'deck.gl-test/data'; import fontMapping from '../../data/font-atlas.json'; @@ -318,6 +319,38 @@ const testCases = [ ], goldenImage: './test/render/golden-images/text-layer-background.png' }, + { + name: 'point-label-layer-background', + viewState: { + target: [0, 0, 0], + zoom: 0 + }, + views: [new OrthographicView()], + layers: [ + new PointLabelLayer({ + id: 'point-label', + data: [0], + getPosition: () => [0, 0], + getText: () => 'PointLabelLayer', + getRadius: 0, + sizeScale: 32, + getTextAnchor: 'middle', + getAlignmentBaseline: 'center', + getColor: [180, 0, 0], + background: true, + getBackgroundColor: [240, 250, 255], + getBorderWidth: 3, + getBorderColor: [0, 100, 180], + backgroundPadding: [12, 8], + backgroundBorderRadius: 8, + updateTriggers: {getText: 'point-label'}, + _subLayerProps: { + 'point-label-primary': {_getFontRenderer: () => fontRenderer} + } + }) + ], + goldenImage: './test/render/golden-images/point-label-layer-background.png' + }, { name: 'text-layer-auto-wrapping', viewState: {