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

keplergl / kepler.gl / 24702790845

21 Apr 2026 03:41AM UTC coverage: 59.5% (-0.03%) from 59.525%
24702790845

Pull #3380

github

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

6795 of 13687 branches covered (49.65%)

Branch coverage included in aggregate %.

1 of 15 new or added lines in 2 files covered. (6.67%)

11 existing lines in 1 file now uncovered.

14002 of 21266 relevant lines covered (65.84%)

75.99 hits per line

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

22.09
/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

13✔
62
  mod.uniformTypes = {
63
    ...shadow.uniformTypes,
64
    outputUniformShadow: 'f32'
65
  };
66

13✔
67
  const originalGetUniforms = shadow.getUniforms as (
68
    opts: Record<string, unknown>,
69
    prevUniforms: Record<string, unknown>
70
  ) => Record<string, unknown>;
13!
UNCOV
71
  mod.getUniforms = (opts: CustomShadowProps = {}, context = {}) => {
×
72
    const u = originalGetUniforms(opts, context);
×
73
    u.outputUniformShadow = opts.outputUniformShadow ? 1.0 : 0.0;
×
74
    return u;
75
  };
76

13✔
77
  return mod as unknown as ShaderModule;
78
}
79

13✔
80
const CustomShadowModule = createCustomShadowModule();
81

82
/**
83
 * Custom LightingEffect for kepler.gl.
84
 *
85
 * Extends deck.gl's LightingEffect with:
86
 * - A patched shadow module with `outputUniformShadow` for uniform shadow
87
 *   during nighttime (avoids partial shadows from below).
88
 * - getShaderModuleProps override that always provides dummyShadowMap
89
 *   to prevent "Bad texture binding" errors when shadows are disabled.
90
 */
91
class CustomDeckLightingEffect extends LightingEffect {
92
  outputUniformShadow: boolean;
93
  isExportMode: boolean;
94

UNCOV
95
  private get _private(): LightingEffectPrivate {
×
96
    return this as unknown as LightingEffectPrivate;
97
  }
98

99
  constructor(props) {
7✔
100
    super(props);
7✔
101
    this.outputUniformShadow = false;
7✔
102
    this.isExportMode = false;
103
  }
104

UNCOV
105
  setup(context) {
×
106
    this.context = context;
×
107
    const {device, deck} = context;
×
108
    if (this._private.shadow && !this._private.dummyShadowMap) {
×
109
      this._private._createShadowPasses(device);
×
110
      deck._addDefaultShaderModule(CustomShadowModule || shadow);
×
111
      this._private.dummyShadowMap = device.createTexture({width: 1, height: 1});
112
    }
113
  }
114

UNCOV
115
  preRender(opts) {
×
116
    if (!this._private.shadow) return;
117

NEW
118
    // Filter editor layers out of the shadow pass so they don't cast shadows.
×
NEW
119
    const originalLayers = opts.layers;
×
120
    opts.layers = originalLayers.filter(l => !l.id.startsWith(EDITOR_LAYER_ID));
121

122
    let unpatch: (() => void) | undefined;
×
123
    if (this.isExportMode) {
×
124
      unpatch = patchTileViewportIds(opts);
125
    }
UNCOV
126

×
127
    super.preRender(opts);
UNCOV
128

×
129
    unpatch?.();
NEW
130

×
131
    opts.layers = originalLayers;
132
  }
133

UNCOV
134
  cleanup(context) {
×
135
    for (const shadowPass of this._private.shadowPasses) {
×
136
      shadowPass.delete();
UNCOV
137
    }
×
138
    this._private.shadowPasses.length = 0;
×
139
    if (this._private.dummyShadowMap) {
×
140
      this._private.dummyShadowMap.destroy();
×
141
      this._private.dummyShadowMap = null;
×
142
      context.deck._removeDefaultShaderModule(CustomShadowModule || shadow);
143
    }
144
  }
145

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

161
    // When the effect is disabled (ghost kept alive to preserve the shadow
162
    // shader module), return neutral lighting props. Without this, the
163
    // base LightingEffect.getShaderModuleProps returns the stale custom
164
    // light sources (ambient color/intensity, directional lights) from
NEW
165
    // when the effect was last active, causing tiles to be tinted/dimmed.
×
NEW
166
    if (!this._private.shadow) {
×
167
      return {
168
        shadow: {
169
          shadowEnabled: false,
170
          dummyShadowMap: this._private.dummyShadowMap
171
        },
172
        lighting: {enabled: false},
173
        pbrMaterial: {unlit: 1}
174
      } as unknown as ReturnType<LightingEffect['getShaderModuleProps']>;
175
    }
NEW
176

×
177
    const props = super.getShaderModuleProps(layer, otherShaderModuleProps);
UNCOV
178

×
179
    if (
×
180
      props.shadow &&
181
      !(props.shadow as CustomShadowProps).dummyShadowMap &&
182
      this._private.dummyShadowMap
UNCOV
183
    ) {
×
184
      (props.shadow as CustomShadowProps).dummyShadowMap = this._private.dummyShadowMap;
185
    }
UNCOV
186

×
187
    if (props.shadow) {
×
188
      (props.shadow as CustomShadowProps).outputUniformShadow = this.outputUniformShadow;
189
    }
190

191
    // Reset unlit to 0 so PBR models respond to lighting. Without this,
192
    // models that had unlit=1 set by the ghost effect (disabled phase)
NEW
193
    // would permanently ignore lighting uniform changes.
×
194
    (props as any).pbrMaterial = {unlit: 0};
NEW
195

×
196
    return props;
197
  }
198
}
199

200
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