• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

keplergl / kepler.gl / 24680555007

20 Apr 2026 05:24PM UTC coverage: 59.522% (-0.003%) from 59.525%
24680555007

Pull #3380

github

web-flow
Merge d01f8b83c into 992f5016d
Pull Request #3380: fix: polygon tool regression

6796 of 13685 branches covered (49.66%)

Branch coverage included in aggregate %.

3 of 5 new or added lines in 1 file covered. (60.0%)

1 existing line in 1 file now uncovered.

14005 of 21262 relevant lines covered (65.87%)

76.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

27.71
/src/effects/src/custom-deck-lighting-effect.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import {LightingEffect, shadow} from '@deck.gl/core';
5
import type {Texture} from '@luma.gl/core';
6
import type {ShaderModule} from '@luma.gl/shadertools';
7
import {EDITOR_LAYER_ID} from '@kepler.gl/constants';
8
import {patchTileViewportIds} from './tile-viewport-fix';
9

10
/**
11
 * Exposes private members of LightingEffect that we need to access.
12
 * These are runtime-accessible but TypeScript marks them as private.
13
 */
14
interface LightingEffectPrivate {
15
  shadow: boolean;
16
  shadowPasses: {delete(): void; render(params: Record<string, unknown>): void}[];
17
  dummyShadowMap: Texture | null;
18
  _createShadowPasses(device: unknown): void;
19
}
20

21
/** Extended shadow module props with our custom field. */
22
interface CustomShadowProps {
23
  outputUniformShadow?: boolean;
24
  dummyShadowMap?: Texture | null;
25
  [key: string]: unknown;
26
}
27

28
/**
29
 * Insert text before a target string in shader source.
30
 */
31
function insertBefore(source: string, target: string, textToInsert: string): string {
32
  const at = source.indexOf(target);
39✔
33
  if (at < 0) return source;
39!
34
  return source.slice(0, at) + textToInsert + source.slice(at);
39✔
35
}
36

37
/**
38
 * Create a patched shadow module that adds `outputUniformShadow` to the
39
 * shadow UBO. When true, `shadow_getShadowWeight` returns 1.0 (full
40
 * uniform shadow) instead of sampling the shadow map. Used for nighttime
41
 * rendering to avoid partial shadows from below.
42
 *
43
 * Also fixes the vertex injection to be a no-op when shadows are disabled,
44
 * preventing position corruption in billboard layers (e.g. the editor layer).
45
 */
46
function createCustomShadowModule(): ShaderModule | null {
47
  if (!shadow) return null;
13!
48

49
  const mod = {...shadow} as Record<string, any>;
13✔
50

51
  const uboField = '  float outputUniformShadow;\n';
13✔
52
  mod.vs = insertBefore(mod.vs, '} shadow;', uboField);
13✔
53
  mod.fs = insertBefore(mod.fs, '} shadow;', uboField);
13✔
54

55
  mod.fs = insertBefore(
13✔
56
    mod.fs,
57
    'vec4 rgbaDepth = texture(shadowMap, position.xy);',
58
    'if (shadow.outputUniformShadow > 0.5) return 1.0;\n  '
59
  );
60

61
  // Fix: guard the vertex injection so it's a no-op when shadows are disabled.
62
  // The original `return gl_Position` in shadow_setVertexPosition is incorrect
63
  // for billboard layers where gl_Position isn't yet assigned when the
64
  // DECKGL_FILTER_GL_POSITION hook fires.
65
  if (mod.inject) {
13!
66
    mod.inject = {...mod.inject};
13✔
67
    mod.inject['vs:DECKGL_FILTER_GL_POSITION'] = `
13✔
68
    if (shadow.drawShadowMap || shadow.useShadowMap) {
69
      position = shadow_setVertexPosition(geometry.position);
70
    }
71
    `;
72
  }
73

74
  mod.uniformTypes = {
13✔
75
    ...shadow.uniformTypes,
76
    outputUniformShadow: 'f32'
77
  };
78

79
  const originalGetUniforms = shadow.getUniforms as (
13✔
80
    opts: Record<string, unknown>,
81
    prevUniforms: Record<string, unknown>
82
  ) => Record<string, unknown>;
83
  mod.getUniforms = (opts: CustomShadowProps = {}, context = {}) => {
13!
84
    const u = originalGetUniforms(opts, context);
×
85
    u.outputUniformShadow = opts.outputUniformShadow ? 1.0 : 0.0;
×
86
    return u;
×
87
  };
88

89
  return mod as unknown as ShaderModule;
13✔
90
}
91

92
const CustomShadowModule = createCustomShadowModule();
13✔
93

94
/**
95
 * Custom LightingEffect for kepler.gl.
96
 *
97
 * Extends deck.gl's LightingEffect with:
98
 * - A patched shadow module with `outputUniformShadow` for uniform shadow
99
 *   during nighttime (avoids partial shadows from below).
100
 * - getShaderModuleProps override that always provides dummyShadowMap
101
 *   to prevent "Bad texture binding" errors when shadows are disabled.
102
 */
103
class CustomDeckLightingEffect extends LightingEffect {
104
  outputUniformShadow: boolean;
105
  isExportMode: boolean;
106

107
  private get _private(): LightingEffectPrivate {
108
    return this as unknown as LightingEffectPrivate;
×
109
  }
110

111
  constructor(props) {
112
    super(props);
7✔
113
    this.outputUniformShadow = false;
7✔
114
    this.isExportMode = false;
7✔
115
  }
116

117
  setup(context) {
118
    this.context = context;
×
119
    const {device, deck} = context;
×
120
    if (this._private.shadow && !this._private.dummyShadowMap) {
×
121
      this._private._createShadowPasses(device);
×
122
      deck._addDefaultShaderModule(CustomShadowModule || shadow);
×
123
      this._private.dummyShadowMap = device.createTexture({width: 1, height: 1});
×
124
    }
125
  }
126

127
  preRender(opts) {
128
    if (!this._private.shadow) return;
×
129

130
    let unpatch: (() => void) | undefined;
131
    if (this.isExportMode) {
×
132
      unpatch = patchTileViewportIds(opts);
×
133
    }
134

135
    super.preRender(opts);
×
136

137
    unpatch?.();
×
138
  }
139

140
  cleanup(context) {
141
    for (const shadowPass of this._private.shadowPasses) {
×
142
      shadowPass.delete();
×
143
    }
144
    this._private.shadowPasses.length = 0;
×
145
    if (this._private.dummyShadowMap) {
×
146
      this._private.dummyShadowMap.destroy();
×
147
      this._private.dummyShadowMap = null;
×
148
      context.deck._removeDefaultShaderModule(CustomShadowModule || shadow);
×
149
    }
150
  }
151

152
  getShaderModuleProps(layer, otherShaderModuleProps) {
153
    // Skip lighting/shadow for the editor layer and its sublayers.
154
    // These are 2D overlays that should not be affected by lighting effects.
NEW
155
    if (layer.id.startsWith(EDITOR_LAYER_ID)) {
×
NEW
156
      return {
×
157
        shadow: {
158
          shadowEnabled: false,
159
          dummyShadowMap: this._private.dummyShadowMap
160
        },
161
        lighting: {enabled: false},
162
        phongMaterial: null,
163
        gouraudMaterial: null
164
      } as unknown as ReturnType<LightingEffect['getShaderModuleProps']>;
165
    }
166

UNCOV
167
    const props = super.getShaderModuleProps(layer, otherShaderModuleProps);
×
168

169
    if (
×
170
      props.shadow &&
×
171
      !(props.shadow as CustomShadowProps).dummyShadowMap &&
172
      this._private.dummyShadowMap
173
    ) {
174
      (props.shadow as CustomShadowProps).dummyShadowMap = this._private.dummyShadowMap;
×
175
    }
176

177
    if (props.shadow) {
×
178
      (props.shadow as CustomShadowProps).outputUniformShadow = this.outputUniformShadow;
×
179
    }
180

181
    return props;
×
182
  }
183
}
184

185
export default CustomDeckLightingEffect;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc