// deck.gl-community // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import {CompositeLayer} from '@deck.gl/core'; import {PathStyleExtension} from '@deck.gl/extensions'; import {PathLayer} from '@deck.gl/layers'; import {WebGpuDashPathLayer} from './webgpu-dash-path-layer'; import type {Accessor, Color, DefaultProps, Layer, LayerExtension} from '@deck.gl/core'; import type {PathLayerProps} from '@deck.gl/layers'; const DEFAULT_OUTLINE_COLOR: Color = [15, 23, 42, 180]; const DEFAULT_OUTLINE_WIDTH_SCALE = 1.2; type PathOutlineLayerExtraProps = { /** Dash pattern accessor forwarded through `PathStyleExtension`. */ getDashArray?: Accessor; /** Whether dash lengths are stretched to align with path endpoints. */ dashJustified?: boolean; /** Color accessor used by the outline stroke rendered behind the path. */ getOutlineColor?: Accessor; /** Multiplier applied to `widthScale` for the outline stroke. */ outlineWidthScale?: number; /** Legacy z-order accessor retained for compatibility with older nebula.gl callers. */ getZLevel?: Accessor; }; /** Properties supported by {@link PathOutlineLayer}. */ export type PathOutlineLayerProps = PathLayerProps & PathOutlineLayerExtraProps; const defaultProps: DefaultProps> = { getDashArray: {type: 'accessor', value: null}, dashJustified: false, getOutlineColor: {type: 'accessor', value: DEFAULT_OUTLINE_COLOR}, outlineWidthScale: {type: 'number', min: 1, value: DEFAULT_OUTLINE_WIDTH_SCALE}, getZLevel: {type: 'accessor', value: 0} }; /** Renders a deck.gl `PathLayer` with a crisp outline stroke behind it. */ export class PathOutlineLayer< DataT = any, ExtraPropsT = Record > extends CompositeLayer< ExtraPropsT & PathOutlineLayerProps & Required> > { static override layerName = 'PathOutlineLayer'; static override defaultProps = defaultProps; override renderLayers(): Layer[] { const { extensions, getColor, getOutlineColor, outlineWidthScale, parameters, updateTriggers = {}, widthScale, dashJustified, getDashArray, getZLevel: _getZLevel } = this.props as PathOutlineLayerProps; const useWebGpuDashLayer = Boolean(getDashArray && this.context?.device?.type === 'webgpu'); const PathLayerType = useWebGpuDashLayer ? WebGpuDashPathLayer : PathLayer; const pathExtensions = getDashArray ? useWebGpuDashLayer ? removePathStyleExtensions(extensions) : ensurePathStyleExtension(extensions) : getLayerExtensions(extensions); const pathParameters = getPathRenderParameters(parameters); const baseWidthScale = widthScale ?? 1; const resolvedOutlineWidthScale = outlineWidthScale ?? DEFAULT_OUTLINE_WIDTH_SCALE; const pathDashProps = getDashArray ? { dashJustified, getDashArray: normalizeDashArrayAccessor(getDashArray) } : {}; const outlineDashProps = getDashArray ? { dashJustified, getDashArray: normalizeDashArrayAccessor(getDashArray, 1 / resolvedOutlineWidthScale) } : {}; return [ new PathLayerType( this.props as unknown as PathLayerProps, this.getSubLayerProps({ ...outlineDashProps, id: 'outline', extensions: pathExtensions, getColor: getOutlineColor, parameters: pathParameters, updateTriggers: { ...updateTriggers, getColor: updateTriggers['getOutlineColor'], getWidth: updateTriggers['getWidth'] }, widthScale: baseWidthScale * resolvedOutlineWidthScale }) ), new PathLayerType( this.props as unknown as PathLayerProps, this.getSubLayerProps({ ...pathDashProps, id: 'path', extensions: pathExtensions, getColor, parameters: pathParameters, updateTriggers, widthScale }) ) ]; } } function ensurePathStyleExtension(extensions: readonly LayerExtension[] = []): LayerExtension[] { const hasPathStyle = extensions.some( extension => (extension.constructor as typeof PathStyleExtension).extensionName === PathStyleExtension.extensionName ); return hasPathStyle ? [...extensions] : [...extensions, new PathStyleExtension({dash: true, highPrecisionDash: true})]; } function getLayerExtensions(extensions: readonly LayerExtension[] = []): LayerExtension[] { return [...extensions]; } function removePathStyleExtensions(extensions: readonly LayerExtension[] = []): LayerExtension[] { return extensions.filter( extension => (extension.constructor as typeof PathStyleExtension).extensionName !== PathStyleExtension.extensionName ); } function normalizeDashArrayAccessor( getDashArray: PathOutlineLayerProps['getDashArray'], scale = 1 ) { if (typeof getDashArray === 'function') { return (datum: DataT, info: any) => scaleDashArray(getDashArray(datum, info), scale); } return scaleDashArray(getDashArray, scale); } function scaleDashArray( dashArray: readonly [number, number] | null | undefined, scale: number ): [number, number] { const [dashSize, gapSize] = dashArray ?? [0, 0]; return [dashSize * scale, gapSize * scale]; } function getPathRenderParameters(parameters: PathLayerProps['parameters']) { const {depthTest: _depthTest, ...rest} = (parameters ?? {}) as Record; return { ...rest, depthCompare: 'always' as const, depthWriteEnabled: false }; }